Transaction

3D3A31A80E04E8…466866A7B5AF

Block 296,980 · index 0 · indexed

Summary

Hash
3D3A31A80E04E814A2372357F95646FF4712E4E7B5F3992C3576466866A7B5AF
Block
296,980
Size
57393 bytes
Gas used
74,970,381 / 100,000,000
Fee
100000 ugnot
Status
success

Messages

Attached funds
15000000 ugnot

Arguments · 24

  1. #1home
  2. #2banner.gno
  3. #3package home import "encoding/base64" // Adapted from the banner every Samouraï home realm shares, with a cyan accent // in place of the red. What differs between pages lives in content.gno. // toriiMask is the Samouraï Coop mark, redrawn as vectors: a torii gate cut // out of a full circle, in a 480x480 box. Everything painted through the mask // takes the shape of the mark. const toriiMask = `<mask id="torii"><circle cx="238" cy="240" r="240" fill="#fff"/><g fill="#000">` + `<path d="M-10 90 Q238 164 490 90 L490 140 Q238 190 -10 140 Z"/>` + `<path d="M102 140 L165 140 L162 206 L96 206 Z"/>` + `<path d="M302 140 L364 140 L370 206 L304 206 Z"/>` + `<rect x="-10" y="205" width="500" height="41"/>` + `<path d="M89 245 L159 245 L145 490 L55 490 Z"/>` + `<rect x="-10" y="274" width="100" height="32"/>` + `<path d="M307 245 L377 245 L411 490 L321 490 Z"/>` + `<path d="M382 296 L490 398 L490 455 L390 358 Z"/>` + `</g></mask>` type heroText struct { kicker string // small cyan caps line above the title title string subtitle string tagline string // cyan monospace line footline string // grey monospace line alt string } // heroSVG draws the page banner: the mark set in a cyan sun on the right, the // text stack on the left. func heroSVG(h heroText) string { return `<svg xmlns="http://www.w3.org/2000/svg" width="960" height="300" viewBox="0 0 960 300">` + `<defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#0e0e0e"/><stop offset="1" stop-color="#0f2226"/></linearGradient>` + toriiMask + `</defs>` + `<rect width="960" height="300" rx="18" fill="url(#bg)"/>` + `<g stroke="#ffffff" stroke-opacity=".05"><path d="M0 60H960M0 120H960M0 180H960M0 240H960"/></g>` + `<circle cx="790" cy="150" r="128" fill="#5eaab5"/>` + `<g transform="translate(686 46) scale(.4333)"><rect width="480" height="480" fill="#f4efe6" mask="url(#torii)"/></g>` + `<text x="56" y="78" font-family="ui-monospace,Menlo,monospace" font-size="13" letter-spacing="3" fill="#5eaab5">` + xmlText(h.kicker) + `</text>` + `<text x="52" y="160" font-family="system-ui,-apple-system,Segoe UI,sans-serif" font-weight="800" font-size="76" fill="#ffffff">` + xmlText(h.title) + `</text>` + `<text x="56" y="200" font-family="system-ui,-apple-system,Segoe UI,sans-serif" font-size="22" fill="#e8e2d8">` + xmlText(h.subtitle) + `</text>` + `<text x="56" y="236" font-family="ui-monospace,Menlo,monospace" font-size="16" fill="#5eaab5">` + xmlText(h.tagline) + `</text>` + `<text x="56" y="266" font-family="ui-monospace,Menlo,monospace" font-size="12" fill="#8a8a8a">` + xmlText(h.footline) + `</text>` + `</svg>` } func heroImage() string { return "![" + mdText(currentHero().alt) + "](data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(heroSVG(currentHero()))) + ")" }
  4. #4charts.gno
  5. #5package home import ( "encoding/base64" "strconv" "strings" ) // Two charts, merged pull requests and reviews per month, each stacked by // repository. A realm cannot read GitHub, so each chart's numbers live in a // section, "activity" and "reviewactivity", and scripts/activity.py prints the // Set commands that refresh them. A section reads, lines split by ";": // // title merged pull requests // subtitle per month, on public gno.land repositories // series gnolang/gno,gnodaokit,Others,Peer Dev // scale Peer Dev,10,commits // outline gnolang/gno pull requests opened // updated 2026-09-24 // 2025-07 4 0 1 48 | 3 // // Each month row carries one count per series. A scaled series counts another // unit: its bars are drawn at one step per 10, and it stays out of the totals. // The count after "|" draws a striped bar in the first series' colour beside // the stack. const ( activitySlot = "activity" reviewActivitySlot = "reviewactivity" chartWidth = 720 chartX = 24 maxBar = 90 scaledFill = "#fbbf24" othersFill = "#94a3b8" ) var ( monthNames = []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} // A repository keeps its colour in both charts; one not listed here takes // a spare colour picked from its name. repoFills = map[string]string{ "gnolang/gno": "#3ddc97", "gno-onboarding-bot": "#7aa2ff", "gnodaokit": "#c084fc", "awesome-gno": "#f472b6", "gnolove": "#2dd4bf", } spareFills = []string{"#fb923c", "#a3e635", "#38bdf8", "#e879f9"} ) type month struct { label string // "Jul", or "Jan 26" where a year starts counts []int outline int } type activity struct { noun string subtitle string series []string scaled string // the series counted in another unit, or "" scaleDiv int scaleUnit string outline string // the striped bar's legend, or "" updated string // "24 Sep 2026", the day the numbers were counted months []month } // parseActivity reads a chart section; a malformed line is skipped. func parseActivity(spec string) activity { var a activity for _, line := range strings.Split(strings.ReplaceAll(spec, "\n", ";"), ";") { line = strings.TrimSpace(line) switch { case strings.HasPrefix(line, "title "): a.noun = strings.TrimSpace(line[6:]) case strings.HasPrefix(line, "subtitle "): a.subtitle = strings.TrimSpace(line[9:]) case strings.HasPrefix(line, "series "): for _, s := range strings.Split(line[7:], ",") { a.series = append(a.series, strings.TrimSpace(s)) } case strings.HasPrefix(line, "scale "): parts := strings.Split(line[6:], ",") if len(parts) != 3 { continue } div, err := strconv.Atoi(strings.TrimSpace(parts[1])) if err != nil || div < 1 { continue } a.scaled, a.scaleDiv, a.scaleUnit = strings.TrimSpace(parts[0]), div, strings.TrimSpace(parts[2]) case strings.HasPrefix(line, "outline "): a.outline = strings.TrimSpace(line[8:]) case strings.HasPrefix(line, "updated "): a.updated = formatDay(strings.TrimSpace(line[8:])) default: if m, ok := parseMonth(line, len(a.series), len(a.months) == 0); ok { a.months = append(a.months, m) } } } return a } func parseMonth(line string, width int, first bool) (month, bool) { fields := strings.Fields(line) if len(fields) < 1 || len(fields[0]) != 7 || fields[0][4] != '-' { return month{}, false } m, err := strconv.Atoi(fields[0][5:]) if err != nil || m < 1 || m > 12 { return month{}, false } out := month{label: monthNames[m-1]} if m == 1 || first { out.label += " " + fields[0][2:4] } counts := fields[1:] if n := len(counts); n >= 2 && counts[n-2] == "|" { v, err := strconv.Atoi(counts[n-1]) if err != nil || v < 0 { return month{}, false } out.outline, counts = v, counts[:n-2] } if len(counts) != width || width == 0 { return month{}, false } for _, f := range counts { n, err := strconv.Atoi(f) if err != nil || n < 0 { return month{}, false } out.counts = append(out.counts, n) } return out, true } // formatDay turns 2026-09-24 into "24 Sep 2026", or "" when it is not a date. func formatDay(s string) string { if len(s) != 10 || s[4] != '-' || s[7] != '-' { return "" } m, err1 := strconv.Atoi(s[5:7]) d, err2 := strconv.Atoi(s[8:]) if err1 != nil || err2 != nil || m < 1 || m > 12 || d < 1 || d > 31 { return "" } return strconv.Itoa(d) + " " + monthNames[m-1] + " " + s[:4] } func (a activity) fill(i int) string { name := a.series[i] switch name { case "Others": return othersFill case a.scaled: return scaledFill } if f, ok := repoFills[name]; ok { return f } sum := 0 for j := 0; j < len(name); j++ { sum += int(name[j]) } return spareFills[sum%len(spareFills)] } // drawn returns how many steps series i takes in the bar, rounding a scaled // count up so a quiet month still shows. func (a activity) drawn(i, n int) int { if a.series[i] == a.scaled { return (n + a.scaleDiv - 1) / a.scaleDiv } return n } // renderChart draws a chart section as a markdown image, or "" when it holds // no month. func renderChart(slot string) string { a := parseActivity(Get(slot)) if len(a.months) == 0 || a.noun == "" { return "" } totals := make([]int, len(a.series)) peak, sum, outlined := 1, 0, 0 for _, m := range a.months { outlined += m.outline if m.outline > peak { peak = m.outline } steps := 0 for i, n := range m.counts { totals[i] += n steps += a.drawn(i, n) if a.series[i] != a.scaled { sum += n } } if steps > peak { peak = steps } } // Legend entries flow left to right and wrap, so the chart sits below them. var b, items strings.Builder x, y := chartX, 96 for i, s := range a.series { label := s + " " + strconv.Itoa(totals[i]) if s == a.scaled { label += " " + a.scaleUnit + ", drawn 1 per " + strconv.Itoa(a.scaleDiv) } w := 24 + len(label)*7 if x+w > chartWidth-chartX { x, y = chartX, y+22 } rect(&items, x, y-10, 12, 12, 0, a.fill(i)) text(&items, x+18, y, "s", "#ffffff", label) x += w + 12 } if a.outline != "" { label := a.outline + " " + strconv.Itoa(outlined) w := 24 + len(label)*7 if x+w > chartWidth-chartX { x, y = chartX, y+22 } rect(&items, x, y-10, 12, 12, 0, "url(#o)") text(&items, x+18, y, "s", "#ffffff", label) } baseline := y + 30 + maxBar height := baseline + 34 sh, sw := strconv.Itoa(height), strconv.Itoa(chartWidth) b.WriteString(`<svg xmlns="http://www.w3.org/2000/svg" width="` + sw + `" height="` + sh + `" viewBox="0 0 ` + sw + ` ` + sh + `">`) b.WriteString("<style>.h{font:700 26px sans-serif}.s{font:13px sans-serif}" + ".n{font:600 11px sans-serif;text-anchor:middle}.m{font:11px sans-serif;text-anchor:middle}" + ".u{font:12px sans-serif;text-anchor:end}</style>") b.WriteString(`<defs><pattern id="o" width="5" height="5" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">` + `<rect width="2" height="5" fill="` + repoFills["gnolang/gno"] + `"/></pattern></defs>`) rect(&b, 0, 0, chartWidth, height, 12, "#10231c") text(&b, chartX, 42, "h", "#ffffff", strconv.Itoa(sum)+" "+a.noun) if a.subtitle != "" { text(&b, chartX, 66, "s", "#9fc9b4", a.subtitle) } if a.updated != "" { text(&b, chartWidth-chartX, 42, "u", "#9fc9b4", "updated "+a.updated) } b.WriteString(items.String()) step := (chartWidth - 2*chartX) / len(a.months) barW := step * 2 / 3 stripeW := 0 if a.outline != "" { // The striped bar and the stack share the month's slot, opened first, // at the same width and touching. barW = step * 2 / 5 stripeW = barW } for i, m := range a.months { sx := chartX + i*step + (step-barW-stripeW)/2 mid := sx + (barW+stripeW)/2 bx := sx if stripeW > 0 { bx = sx + stripeW if m.outline > 0 { h := m.outline * maxBar / peak if h == 0 { h = 1 } rect(&b, sx, baseline-h, stripeW, h, 0, "url(#o)") } } top, total := baseline, 0 for j, n := range m.counts { d := a.drawn(j, n) h := d * maxBar / peak if d > 0 && h == 0 { h = 1 } if h == 0 { continue } top -= h if a.series[j] != a.scaled { total += n } rect(&b, bx, top, barW, h, 0, a.fill(j)) } if top == baseline { rect(&b, bx, baseline-2, barW, 2, 0, "#2c4a3e") } else if total > 0 { text(&b, bx+barW/2, top-5, "n", "#ffffff", strconv.Itoa(total)) } text(&b, mid, baseline+18, "m", "#9fc9b4", m.label) } b.WriteString("</svg>") alt := strings.ToUpper(a.noun[:1]) + a.noun[1:] + " per month, by repository" return "![" + alt + "](data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(b.String())) + ")\n" } // The chart is written straight into one builder: p/nt/svg formats each // element with ufmt and appends it to a growing string, which cost this page // close to a billion gas. func rect(b *strings.Builder, x, y, w, h, r int, fill string) { b.WriteString(`<rect x="` + strconv.Itoa(x) + `" y="` + strconv.Itoa(y) + `" width="` + strconv.Itoa(w) + `" height="` + strconv.Itoa(h) + `" rx="` + strconv.Itoa(r) + `" fill="` + fill + `"/>`) } // text writes s as SVG text; callers pass repository names and numbers, so it // escapes the three characters that could break the markup. func text(b *strings.Builder, x, y int, class, fill, s string) { s = strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;").Replace(s) b.WriteString(`<text x="` + strconv.Itoa(x) + `" y="` + strconv.Itoa(y) + `" class="` + class + `" fill="` + fill + `">` + s + `</text>`) } // The area chart is a ring of one number per area, from the "areas" section, // which reads, lines split by ";": // // title pull requests by area // subtitle merged into gnolang/gno // updated 2026-09-24 // slice 26 Docs // slice 17 GnoVM const areasSlot = "areas" var areaFills = []string{"#3ddc97", "#7aa2ff", "#c084fc", "#f472b6", "#2dd4bf", "#fb923c"} type slice struct { label string count int } func parseAreas(spec string) (noun, subtitle, updated string, slices []slice) { for _, line := range strings.Split(strings.ReplaceAll(spec, "\n", ";"), ";") { line = strings.TrimSpace(line) switch { case strings.HasPrefix(line, "title "): noun = strings.TrimSpace(line[6:]) case strings.HasPrefix(line, "subtitle "): subtitle = strings.TrimSpace(line[9:]) case strings.HasPrefix(line, "updated "): updated = formatDay(strings.TrimSpace(line[8:])) case strings.HasPrefix(line, "slice "): n, label, ok := strings.Cut(strings.TrimSpace(line[6:]), " ") c, err := strconv.Atoi(n) if ok && err == nil && c > 0 && strings.TrimSpace(label) != "" { slices = append(slices, slice{strings.TrimSpace(label), c}) } } } return noun, subtitle, updated, slices } // renderAreas draws the "areas" section as a ring, or "" when it holds no slice. func renderAreas() string { noun, subtitle, updated, slices := parseAreas(Get(areasSlot)) if len(slices) == 0 || noun == "" { return "" } total := 0 for _, s := range slices { total += s.count } const height, cx, cy, r = 300, 140, 190, 80 var b strings.Builder b.WriteString(`<svg xmlns="http://www.w3.org/2000/svg" width="720" height="300" viewBox="0 0 720 300">`) b.WriteString("<style>.h{font:700 26px sans-serif}.s{font:13px sans-serif}.t{font:700 34px sans-serif;text-anchor:middle}" + ".c{font:13px sans-serif;text-anchor:middle}.l{font:15px sans-serif}.p{font:13px sans-serif;text-anchor:end}" + ".u{font:12px sans-serif;text-anchor:end}</style>") rect(&b, 0, 0, chartWidth, height, 12, "#10231c") text(&b, chartX, 42, "h", "#ffffff", strings.ToUpper(noun[:1])+noun[1:]) if subtitle != "" { text(&b, chartX, 66, "s", "#9fc9b4", subtitle) } if updated != "" { text(&b, chartWidth-chartX, 42, "u", "#9fc9b4", "updated "+updated) } // Each slice is a circle stroke whose dash covers its share of a ring // measured as 1000 units, rotated so the first slice starts at the top. start := 0 for i, s := range slices { share := s.count * 1000 / total if i == len(slices)-1 { share = 1000 - start } b.WriteString(`<circle cx="` + strconv.Itoa(cx) + `" cy="` + strconv.Itoa(cy) + `" r="` + strconv.Itoa(r) + `" fill="none" stroke="` + areaFills[i%len(areaFills)] + `" stroke-width="34" pathLength="1000"` + ` stroke-dasharray="` + strconv.Itoa(share) + ` 1000" stroke-dashoffset="` + strconv.Itoa(-start) + `" transform="rotate(-90 ` + strconv.Itoa(cx) + ` ` + strconv.Itoa(cy) + `)"/>`) start += share } text(&b, cx, cy+8, "t", "#ffffff", strconv.Itoa(total)) text(&b, cx, cy+28, "c", "#9fc9b4", "merged") for i, s := range slices { y := 130 + i*30 rect(&b, 280, y-12, 14, 14, 3, areaFills[i%len(areaFills)]) text(&b, 304, y, "l", "#ffffff", s.label) text(&b, chartWidth-chartX-60, y, "p", "#ffffff", strconv.Itoa(s.count)) text(&b, chartWidth-chartX, y, "p", "#9fc9b4", strconv.Itoa(s.count*100/total)+"%") } b.WriteString("</svg>") return "![" + mdText(strings.ToUpper(noun[:1])+noun[1:]+", "+subtitle) + "](data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(b.String())) + ")\n" }
  6. #6charts_test.gno
  7. #7package home import ( "encoding/base64" "strconv" "strings" "testing" "gno.land/p/nt/avl/v0" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) func TestParseActivity(t *testing.T) { a := parseActivity("title reviews; series gnolang/gno,Peer Dev; scale Peer Dev,10,commits; scale bad\n" + "2025-12 3 21; 2026-01 0 0; bad; 2026-13 1 1; 2026-02 x 1; 2026-02 5; 2026-02 5 2") uassert.Equal(t, "reviews", a.noun) uassert.Equal(t, "Peer Dev", a.scaled) uassert.Equal(t, 10, a.scaleDiv) uassert.Equal(t, "commits", a.scaleUnit) uassert.Equal(t, 3, len(a.months)) uassert.Equal(t, "Dec 25", a.months[0].label) uassert.Equal(t, 21, a.months[0].counts[1]) uassert.Equal(t, 3, a.drawn(1, 21)) // rounded up uassert.Equal(t, 3, a.drawn(0, 3)) // an unscaled series draws its count uassert.Equal(t, "Jan 26", a.months[1].label) uassert.Equal(t, "Feb", a.months[2].label) uassert.Equal(t, 2, a.months[2].counts[1]) b := parseActivity("title x; series a; outline opened; updated 2026-09-24; " + "2026-01 1 | 8; 2026-02 2 |; 2026-03 3 | y; 2026-04 4") uassert.Equal(t, "opened", b.outline) uassert.Equal(t, "24 Sep 2026", b.updated) uassert.Equal(t, 2, len(b.months)) // "|" with no count and a non-number are rejected uassert.Equal(t, 8, b.months[0].outline) uassert.Equal(t, 0, b.months[1].outline) uassert.Equal(t, "", formatDay("2026-13-01")) } func TestFillKeepsRepoColour(t *testing.T) { // gnodaokit keeps its colour wherever it sits in a chart's series. a := activity{series: []string{"gnolang/gno", "gnodaokit"}} b := activity{series: []string{"gnodaokit"}} uassert.Equal(t, repoFills["gnodaokit"], a.fill(1)) uassert.Equal(t, repoFills["gnodaokit"], b.fill(0)) c := activity{series: []string{"unlisted-repo", "Others"}} uassert.Equal(t, c.fill(0), c.fill(0)) uassert.Equal(t, othersFill, c.fill(1)) } func decodeChart(t *testing.T, out, alt string) string { prefix := "![" + alt + "](data:image/svg+xml;base64," uassert.True(t, strings.HasPrefix(out, prefix), "prefix of "+alt) raw, err := base64.StdEncoding.DecodeString(strings.TrimSuffix(out[len(prefix):], ")\n")) uassert.NoError(t, err) return string(raw) } func TestCharts(t *testing.T) { prs := decodeChart(t, renderChart(activitySlot), "Merged pull requests per month, by repository") for _, want := range []string{ ">104 merged pull requests</text>", ">gnolang/gno 70</text>", ">gnodaokit 6</text>", ">Others 17</text>", ">Peer Dev 234 commits, drawn 1 per 10</text>", ">May 25</text>", ">Jan 26</text>", ">23</text>", // June 2026: 16 + 6 + 1 ">5</text>", // July 2025 counts its 5 pull requests and not its 48 commits } { uassert.True(t, strings.Contains(prs, want), "pull requests chart: "+want) } // One amber bar per month with Peer Dev commits, May to November 2025, plus the legend's. uassert.Equal(t, 8, strings.Count(prs, `fill="#fbbf24"`)) // One striped bar per month with opened pull requests, plus the legend's. uassert.Equal(t, 15, strings.Count(prs, `fill="url(#o)"`)) uassert.True(t, strings.Contains(prs, ">gnolang/gno pull requests opened 131</text>")) uassert.True(t, strings.Contains(prs, ">updated 24 Sep 2026</text>")) revs := decodeChart(t, renderChart(reviewActivitySlot), "Reviews per month, by repository") for _, want := range []string{">404 reviews</text>", ">gnolang/gno 357</text>", ">Others 47</text>", ">per month, on the same repositories</text>"} { uassert.True(t, strings.Contains(revs, want), "reviews chart: "+want) } uassert.False(t, strings.Contains(revs, "#fbbf24")) uassert.False(t, strings.Contains(revs, ">gnolove")) uassert.False(t, strings.Contains(revs, `fill="url(#o)"`)) uassert.True(t, strings.Contains(revs, ">updated 24 Sep 2026</text>")) page := Render("") uassert.True(t, strings.Index(page, "Merged pull requests per month") < strings.Index(page, "Reviews per month")) } func TestChartEmpty(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(admin)) old := Get(activitySlot) Set(cross(cur), activitySlot, "") uassert.Equal(t, "", renderChart(activitySlot)) Set(cross(cur), activitySlot, old) uassert.AbortsWithMessage(t, cur, "reserved section: banner", func() { Set(cross(cur), "banner", "x") }) uassert.AbortsWithMessage(t, cur, "reserved section: reviews", func() { Set(cross(cur), "reviews", "x") }) uassert.AbortsWithMessage(t, cur, "reserved section: hero", func() { Set(cross(cur), "hero", "x") }) uassert.AbortsWithMessage(t, cur, "reserved section: areachart", func() { Set(cross(cur), "areachart", "x") }) } func TestAreas(t *testing.T) { noun, subtitle, updated, slices := parseAreas("title x; subtitle y; updated 2026-09-24; slice 3 Docs; slice 0 Empty; slice y Bad; slice 2 Chain: tm2, consensus") uassert.Equal(t, "x", noun) uassert.Equal(t, "y", subtitle) uassert.Equal(t, "24 Sep 2026", updated) uassert.Equal(t, 2, len(slices)) uassert.Equal(t, "Chain: tm2, consensus", slices[1].label) ring := decodeChart(t, renderAreas(), "Pull requests by area, merged into gnolang/gno") for _, want := range []string{">70</text>", ">Pull requests by area</text>", ">merged into gnolang/gno</text>", ">Docs</text>", ">26</text>", ">37%</text>", ">updated 24 Sep 2026</text>"} { uassert.True(t, strings.Contains(ring, want), "area ring: "+want) } // The shares add up to the whole ring, the last one taking the rounding. uassert.True(t, strings.Contains(ring, `stroke-dasharray="371 1000" stroke-dashoffset="0"`)) uassert.True(t, strings.Contains(ring, `stroke-dashoffset="-941"`)) uassert.True(t, strings.Contains(ring, `stroke-dasharray="59 1000"`)) } func TestHeartsWall(cur realm, t *testing.T) { hearts = avl.NewTree() wall := decodeChart(t, renderHeartsWall(), "0 hearts") uassert.Equal(t, wallCols, strings.Count(wall, `<use href="#h"`)) uassert.Equal(t, 0, strings.Count(wall, `fill="#e63946"`)) for i := 0; i < 25; i++ { hearts.Set(testutils.TestAddress("fan"+strconv.Itoa(i)).String(), int64(i)) } wall = decodeChart(t, renderHeartsWall(), "25 hearts") uassert.Equal(t, 2*wallCols, strings.Count(wall, `<use href="#h"`)) uassert.Equal(t, 25, strings.Count(wall, `fill="#e63946"`)) uassert.False(t, strings.Contains(wall, "more</text>")) for i := 25; i < 250; i++ { hearts.Set(testutils.TestAddress("fan"+strconv.Itoa(i)).String(), int64(i)) } wall = decodeChart(t, renderHeartsWall(), "250 hearts") uassert.Equal(t, wallCols*wallMaxRows, strings.Count(wall, `fill="#e63946"`)) uassert.True(t, strings.Contains(wall, ">and 10 more</text>")) uassert.True(t, strings.Contains(wall, `<text x="348" y="316" text-anchor="middle" fill="#9fc9b4" style="font:13px sans-serif">and 10 more</text>`)) hearts = avl.NewTree() }
  8. #8content.gno
  9. #9package home // hero holds the banner text a "herotext" section leaves out. var hero = heroText{ kicker: "SAMOURAÏ COOP · GNO.LAND", title: "David G.", subtitle: "Developer engineer at Samouraï Coop", tagline: "", footline: "gno.land core contributor", alt: "David G., developer engineer at Samouraï Coop", } // defaultSlots seeds the page at deploy time. Later edits go through Set. var defaultSlots = []struct{ name, body string }{ {"order", "hero,header,activityhead,banner,reviews,gno,areachart,projects,peerdev,unrelated,game,hearts"}, {"activityhead", "## Activity"}, {heroSlot, "kicker SAMOURAÏ COOP · GNO.LAND; title David G.; subtitle Developer engineer at Samouraï Coop; footline gno.land core contributor; alt David G., developer engineer at Samouraï Coop"}, {"areas", "title pull requests by area; subtitle merged into gnolang/gno; updated 2026-09-24; slice 26 Docs; slice 17 GnoVM; slice 14 Examples and realms; slice 9 Chain: tm2, consensus, package loading; slice 4 Tools: gnoweb, gnokey, gnodev"}, {"activity", "title merged pull requests; subtitle per month, on public gno.land repositories; series gnolang/gno,gno-onboarding-bot,gnodaokit,awesome-gno,Others,Peer Dev; scale Peer Dev,10,commits; outline gnolang/gno pull requests opened; updated 2026-09-24; 2025-05 0 0 0 0 0 25 | 0; 2025-06 0 0 0 0 0 87 | 2; 2025-07 4 0 0 0 1 48 | 3; 2025-08 0 0 5 0 3 27 | 0; 2025-09 4 0 0 0 0 14 | 9; 2025-10 1 0 0 0 5 15 | 0; 2025-11 1 0 1 0 1 18 | 7; 2025-12 1 0 0 0 0 0 | 7; 2026-01 1 0 0 0 0 0 | 8; 2026-02 6 0 0 0 0 0 | 11; 2026-03 5 0 0 0 0 0 | 21; 2026-04 12 0 0 0 2 0 | 17; 2026-05 7 0 0 0 1 0 | 19; 2026-06 16 6 0 0 1 0 | 7; 2026-07 8 0 0 5 0 0 | 15; 2026-08 2 0 0 0 0 0 | 3; 2026-09 2 0 0 0 3 0 | 2"}, {"reviewactivity", "title reviews; subtitle per month, on the same repositories; series gnolang/gno,Others; updated 2026-09-24; 2025-05 0 1; 2025-06 0 0; 2025-07 0 0; 2025-08 0 11; 2025-09 11 7; 2025-10 2 13; 2025-11 5 1; 2025-12 13 0; 2026-01 24 0; 2026-02 32 2; 2026-03 47 0; 2026-04 30 1; 2026-05 34 0; 2026-06 49 5; 2026-07 51 1; 2026-08 35 0; 2026-09 24 5"}, {"header", `I'm a developer engineer at [Samouraï Coop](https://www.samourai.world/), in [samcrew](/u/samcrew), and an [Epitech](https://www.epitech.eu/) graduate. Contributing to gno.land since May 2025. GnoVM and chain fixes, docs including the getting started guide, and Peer Dev tutorials. I'm passionate about P2P, low level programming, travelling, urbanism and transportation. I'm a curious person who wants to contribute to the greater good. Open for contribution! 🙂 [GitHub](https://github.com/davd-gzl) · [Peer Dev on YouTube](https://www.youtube.com/@peerdevlearning) · [Personal wiki](https://davd-gzl.github.io/personal-wiki/)`}, {"gno", `## Gno core Most of my work goes into [gnolang/gno](https://github.com/gnolang/gno), the [gno.land](https://gno.land) monorepo. [All my merged pull requests are on GitHub](https://github.com/gnolang/gno/pulls?q=is%3Apr+author%3Adavd-gzl+is%3Amerged).`}, {"projects", `## Projects | Project | What I did | | --- | --- | | [GnoVM, explained](https://samouraiworld.github.io/gno-agent-workspace/) | I wrote a guide to how the GnoVM works, for Go developers who have never opened it: parsing, type-checking, the preprocess pass, the stack machine, realm storage and gas. | | [gnodaokit](https://github.com/samouraiworld/gnodaokit) | A toolkit for building DAOs on gno.land. [I contributed](https://github.com/samouraiworld/gnodaokit/pulls?q=is%3Apr+author%3Adavd-gzl+is%3Amerged) documentation, contributor instructions and a guided setup for the demo DAO. | | [awesome-gno](https://github.com/gnoverse/awesome-gno) | The curated list of gno.land resources. [I contributed](https://github.com/gnoverse/awesome-gno/pulls?q=is%3Apr+author%3Adavd-gzl+is%3Amerged) new entries, Peer Dev, r/docs and tx-exports among them, and a cleanup of moved links and outdated tutorials. |`}, {"unrelated", `## Unrelated projects Personal projects outside gno.land. | Project | What it is | | --- | --- | | [MAX-Finder](https://offware-apps.github.io/MAX-Finder/) (WIP) | An open-source finder for bookable MAX JEUNE and MAX SENIOR train seats, built on SNCF open data. | | [Postcards](https://offware-apps.github.io/Postcards/) (WIP) | A map of every place you have been, kept private and offline in one file you own. |`}, {"peerdev", `## Peer Dev I teach gno.land on [Peer Dev](https://www.youtube.com/@peerdevlearning), a YouTube channel of developer tutorials, from an empty machine to GovDAO. [Watch the gno.land playlist](https://www.youtube.com/playlist?list=PLJZrQikyfMc-kBojXgAojOz4UQPuq4DiY). The videos date from 2025, so some commands and screens are out of date. The main ideas still hold. - [Deploying Gno Packages](https://www.youtube.com/watch?v=B7bSreCwIls) - [Create your first dApp](https://www.youtube.com/watch?v=z5Qtc0Nuyqg) - [Governance DAO (GovDAO)](https://www.youtube.com/watch?v=jM-6ezyn_v4) - [Interrealm specification](https://www.youtube.com/watch?v=JJju7-GeRbA) - [Gno VS Solidity](https://www.youtube.com/watch?v=Oi77hvJCVbU)`}, }
  10. #10gnomod.toml
  11. #11module = "gno.land/r/g1rayfgklwl0aspz488wvrcrvt7t2quy6q06lgk2/home" gno = "0.9" private = true
  12. #12hearts.gno
  13. #13package home import ( "encoding/base64" "strconv" "strings" ) // The hearts wall draws one red heart per address that sent one, row by row, // with the slots still free in the last row left as outlines. Past // wallCols*wallMaxRows hearts the wall stays full and a line under it counts // the rest, so the image never grows past ten rows. const ( wallCols = 24 wallMaxRows = 10 wallCell = 28 heartRed = "#e63946" heartPath = "M12 21C12 21 3 15.5 3 9.5 3 6.5 5.4 4 8.3 4 10 4 11.2 5 12 6.2 12.8 5 14 4 15.7 4 18.6 4 21 6.5 21 9.5 21 15.5 12 21 12 21Z" ) // renderHeartsWall returns the wall as a markdown image; with no heart yet it // is one row of empty slots. func renderHeartsWall() string { n := Hearts() shown := n if limit := wallCols * wallMaxRows; shown > limit { shown = limit } rows := shown/wallCols + 1 if rows > wallMaxRows { rows = wallMaxRows } w := 24 + wallCols*wallCell h := 24 + rows*wallCell more := n - shown if more > 0 { h += 26 } var b strings.Builder b.WriteString(`<svg xmlns="http://www.w3.org/2000/svg" width="` + strconv.Itoa(w) + `" height="` + strconv.Itoa(h) + `" viewBox="0 0 ` + strconv.Itoa(w) + ` ` + strconv.Itoa(h) + `"><defs><path id="h" d="` + heartPath + `"/></defs>`) b.WriteString(`<rect width="` + strconv.Itoa(w) + `" height="` + strconv.Itoa(h) + `" rx="12" fill="#10231c"/>`) for i := 0; i < rows*wallCols; i++ { x := 14 + (i%wallCols)*wallCell y := 14 + (i/wallCols)*wallCell style := `fill="` + heartRed + `"` if i >= shown { style = `fill="none" stroke="#6b8f7f" stroke-width="1.5"` } b.WriteString(`<use href="#h" x="` + strconv.Itoa(x) + `" y="` + strconv.Itoa(y) + `" ` + style + `/>`) } if more > 0 { b.WriteString(`<text x="` + strconv.Itoa(w/2) + `" y="` + strconv.Itoa(h-14) + `" text-anchor="middle" ` + `fill="#9fc9b4" style="font:13px sans-serif">and ` + strconv.Itoa(more) + ` more</text>`) } b.WriteString("</svg>") return "![" + strconv.Itoa(n) + " hearts](data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(b.String())) + ")\n" }
  14. #14home.gno
  15. #15// Package home is David G.'s profile page on gno.land. // // gnoweb renders a profile by calling Render("") on the realm at exactly // /r/<address>/home, so this path carries no /vN suffix. // // The page is a list of sections, each a markdown body the owner edits with // one Set transaction, so changing the text never needs a redeploy. The slot // named "order" lists the sections to show. Six sections are drawn rather // than stored: "hero", the page banner, "banner" and "reviews", the activity // charts, "areachart", the ring of pull requests by area, "game", today's // Gnordle, and "hearts", the heart count and wall. package home import ( "strconv" "strings" "chain/runtime" "gno.land/p/nt/avl/v0" ) // admin is the only address allowed to edit sections. const admin = address("g1rayfgklwl0aspz488wvrcrvt7t2quy6q06lgk2") const ( home = "/r/g1rayfgklwl0aspz488wvrcrvt7t2quy6q06lgk2/home" orderSlot = "order" maxNameLen = 32 // maxPathLen is far above any link the page writes, and turns a long // crafted link away before the query parser, whose cost grows with it. maxPathLen = 256 ) var ( slots = avl.NewTree() // section name -> markdown body hearts = avl.NewTree() // address -> block height of the heart ) func init() { for _, s := range defaultSlots { slots.Set(s.name, s.body) } } // Set writes one section of the page. Only the admin can call it. // // name is the section to write and body is its markdown. The text sections are // "header", the bio at the top, then "gno", "projects", "peerdev" and // "unrelated"; any other name creates a new section. // // name "order" decides which sections show and in which order. Its body lists // every section name, comma separated, with no spaces. A name left out of the // list is hidden, so copy the current list, from Get("order"), and edit it. // // name "herotext" is the banner's text: "key value" pairs split by ";", with // the keys kicker, the small line on top, title, subtitle, tagline, footline, // the small line at the bottom, and alt, the banner's description for screen // readers. A key left out keeps its current default. // // "hero", "banner", "reviews", "areachart", "game" and "hearts" are drawn by // the page itself: they cannot be set, only placed through "order". // // Examples, as the two fields of the form: // // name: header // body: I'm a developer engineer at [Samouraï Coop](https://www.samourai.world/). // // name: herotext // body: title David G.; subtitle Developer engineer at Samouraï Coop; footline Open for contribution // // name: talks // body: ## Talks // // name: order // body: hero,header,activityhead,banner,reviews,gno,areachart,projects,peerdev,unrelated,talks,game,hearts func Set(cur realm, name, body string) { assertAdmin(cur) if name == "" || len(name) > maxNameLen || strings.ContainsAny(name, ", \n") { panic("invalid section name") } if drawnSections[name] { panic("reserved section: " + name) } slots.Set(name, body) } // SetActivity refreshes the three charts in one transaction. Only the admin // can call it. Its three arguments are the bodies of the "activity", // "reviewactivity" and "areas" sections, as scripts/activity.py prints them; // an empty argument leaves that chart as it is. func SetActivity(cur realm, activity, reviews, areas string) { assertAdmin(cur) for _, s := range []struct{ name, body string }{ {activitySlot, activity}, {reviewActivitySlot, reviews}, {areasSlot, areas}, } { if s.body != "" { slots.Set(s.name, s.body) } } } // Delete removes one section. Only the admin can call it. // // name is the section to remove, "talks" for example. Its name can stay in // "order": a name with no section shows nothing. func Delete(cur realm, name string) { assertAdmin(cur) if _, ok := slots.Remove(name); !ok { panic("no such section: " + name) } } // Get returns a section's markdown, or "" when it does not exist. // Get("order") returns the current section list, the one to edit before // setting "order". func Get(name string) string { if v := slots.Get(name); v != nil { return v.(string) } return "" } // Heart records one heart from the caller. Each address can send exactly one. func Heart(cur realm) { from := cur.Previous().Address().String() if hearts.Has(from) { panic("already sent: one heart per address") } hearts.Set(from, runtime.ChainHeight()) } // Hearts returns how many distinct addresses sent a heart. func Hearts() int { return hearts.Size() } // HasHeart reports whether addr already sent a heart. func HasHeart(addr address) bool { return hearts.Has(addr.String()) } func assertAdmin(cur realm) { if cur.Previous().Address() != admin { panic("unauthorized") } } // Render serves: // // "" the profile page // "wordle/..." the Gnordle game // "hearts[/<n>]" the addresses that sent a heart, heartsPerPage per page func Render(path string) string { if len(path) > maxPathLen { return "# Not found\n\n[Back home](" + home + ")\n" } // Only the game reads a query; elsewhere one is dropped, so a shared link // carrying ?utm_source=... still shows the page. if !strings.HasPrefix(path, "wordle") { path, _, _ = strings.Cut(path, "?") } switch { case path == "": return renderPage() case path == "wordle" || strings.HasPrefix(path, "wordle/") || strings.HasPrefix(path, "wordle?"): return renderWordle(path) case path == "hearts" || strings.HasPrefix(path, "hearts/"): return renderHearts(strings.TrimPrefix(strings.TrimPrefix(path, "hearts"), "/")) } return "# Not found\n\n[Back home](" + home + ")\n" } func renderPage() string { var b strings.Builder for _, name := range strings.Split(Get(orderSlot), ",") { switch name = strings.TrimSpace(name); name { case "": case "hero": b.WriteString(heroImage()) case "areachart": b.WriteString(renderAreas()) case "banner": b.WriteString(renderChart(activitySlot)) case "reviews": b.WriteString(renderChart(reviewActivitySlot)) case "game": b.WriteString(renderGameTeaser()) case "hearts": b.WriteString(renderHeartsTeaser()) default: b.WriteString(Get(name)) } b.WriteString("\n\n") } b.WriteString("---\n\nThis page is a contract: [gno.land" + home + "](" + home + "$source).\n") return b.String() } func renderHeartsTeaser() string { return "## Hearts\n\n**" + strconv.Itoa(Hearts()) + "** so far. " + "[Send one](" + home + "$help&func=Heart): one transaction, one per address, " + "forever on chain. [See who did](" + home + ":hearts).\n\n" + renderHeartsWall() } // drawnSections are rendered from code and state, so Set refuses their names. var drawnSections = map[string]bool{ "hero": true, "banner": true, "reviews": true, "areachart": true, "game": true, "hearts": true, } // heroSlot holds the banner's text, one "key value" pair per field, split by // ";": kicker, title, subtitle, tagline, footline and alt, the image's // description. A field the section leaves out keeps its value from hero. const heroSlot = "herotext" func currentHero() heroText { h := hero for _, pair := range strings.Split(Get(heroSlot), ";") { key, value, _ := strings.Cut(strings.TrimSpace(pair), " ") value = strings.TrimSpace(value) switch key { case "kicker": h.kicker = value case "title": h.title = value case "subtitle": h.subtitle = value case "tagline": h.tagline = value case "footline": h.footline = value case "alt": h.alt = value } } return h } // mdText keeps s from closing a markdown link or image label. func mdText(s string) string { return strings.NewReplacer("[", "(", "]", ")").Replace(s) } // xmlText escapes s for SVG text and attributes. func xmlText(s string) string { return strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;", "\"", "&quot;", "'", "&#39;").Replace(s) } // heartsPerPage keeps one page of the list far under the query gas limit, // which a list of every heart would pass at about 15,000 of them. const heartsPerPage = 100 func renderHearts(page string) string { n := 1 if page != "" { v, err := strconv.Atoi(page) if err != nil || v < 1 { return "# Hearts\n\nThere is no such page.\n\n[Back home](" + home + ")\n" } n = v } pages := (hearts.Size() + heartsPerPage - 1) / heartsPerPage if n > 1 && n > pages { return "# Hearts\n\nThere is no such page.\n\n[Back home](" + home + ")\n" } var b strings.Builder b.WriteString("# Hearts\n\n") if hearts.Size() == 0 { b.WriteString("Nobody yet. [Be the first](" + home + "$help&func=Heart).\n") } hearts.IterateByOffset((n-1)*heartsPerPage, heartsPerPage, func(addr string, h any) bool { b.WriteString("- " + addr + ", block " + strconv.FormatInt(h.(int64), 10) + "\n") return false }) if pages > 1 { b.WriteString("\nPage " + strconv.Itoa(n) + " of " + strconv.Itoa(pages) + ".") if n > 1 { b.WriteString(" [Previous](" + home + ":hearts/" + strconv.Itoa(n-1) + ")") } if n < pages { b.WriteString(" [Next](" + home + ":hearts/" + strconv.Itoa(n+1) + ")") } b.WriteString("\n") } b.WriteString("\n[Back home](" + home + ")\n") return b.String() }
  16. #16home_test.gno
  17. #17package home import ( "strings" "testing" "time" "gno.land/p/nt/avl/v0" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) var ( alice = testutils.TestAddress("alice") mallory = testutils.TestAddress("mallory") ) // setDay moves the block time to noon on the given day since 1970-01-01 UTC. func setDay(day int64) { ctx := testing.GetContext() ctx.Time = time.Unix(day*86400+43200, 0) testing.SetContext(ctx) } func TestDefaultPage(t *testing.T) { setDay(launchDay) page := Render("") for _, want := range []string{ "![David G., developer engineer at Samouraï Coop](data:image/svg+xml;base64,", "I'm a developer engineer at [Samouraï Coop](https://www.samourai.world/), in [samcrew](/u/samcrew)", "## Gno core", "## Projects", "## Peer Dev", "## Gnordle", "[Play puzzle #1](" + home + ":wordle)", "[GnoVM, explained](", "## Hearts", "**0** so far.", "$help&func=Heart", } { uassert.True(t, strings.Contains(page, want), "missing: "+want) } } func TestSetGetDelete(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(admin)) Set(cross(cur), "bio", "Building on gno.") uassert.Equal(t, "Building on gno.", Get("bio")) Set(cross(cur), "order", "bio") uassert.True(t, strings.HasPrefix(Render(""), "Building on gno.")) Delete(cross(cur), "bio") uassert.Equal(t, "", Get("bio")) uassert.AbortsWithMessage(t, cur, "no such section: bio", func() { Delete(cross(cur), "bio") }) uassert.AbortsWithMessage(t, cur, "reserved section: game", func() { Set(cross(cur), "game", "x") }) uassert.AbortsWithMessage(t, cur, "invalid section name", func() { Set(cross(cur), "a,b", "x") }) testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "unauthorized", func() { Set(cross(cur), "header", "pwned") }) uassert.AbortsWithMessage(t, cur, "unauthorized", func() { Delete(cross(cur), "header") }) for _, s := range defaultSlots { slots.Set(s.name, s.body) } } func TestHeart(cur realm, t *testing.T) { hearts = avl.NewTree() testing.SetRealm(testing.NewUserRealm(alice)) Heart(cross(cur)) uassert.Equal(t, 1, Hearts()) uassert.True(t, HasHeart(alice)) uassert.False(t, HasHeart(mallory)) uassert.AbortsWithMessage(t, cur, "already sent: one heart per address", func() { Heart(cross(cur)) }) uassert.True(t, strings.Contains(Render("hearts"), alice.String())) uassert.True(t, strings.Contains(Render(""), "**1** so far.")) } func TestNotFound(t *testing.T) { uassert.True(t, strings.HasPrefix(Render("nope"), "# Not found")) }
  18. #18security_test.gno
  19. #19package home import ( "chain/runtime" "encoding/base64" "net/url" "strconv" "strings" "testing" "gno.land/p/nt/avl/v0" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) // A guess arrives in the page link, so anyone can write one: none of it may // reach the rendered markdown. func TestGuessIsNeverEchoed(t *testing.T) { setDay(launchDay) uassert.True(t, strings.Contains(Render("wordle/1?guess=abc"), "> That guess is not five letters, A to Z.")) for _, guess := range []string{ "[claim your reward](https://evil.example)", "x\n\n# OFFICIAL NOTICE", "![](https://evil.example/pixel.png)", `<gno-form exec="Transfer"><gno-input name="to" /></gno-form>`, } { // Any rejected guess renders the very page a harmless rejected one does. page := Render("wordle/1?guess=" + url.QueryEscape(guess)) uassert.Equal(t, Render("wordle/1?guess=abc"), page, "guess leaked into the page: "+guess) } } func TestOnlyAdminWrites(cur realm, t *testing.T) { for _, caller := range []runtime.Realm{ testing.NewUserRealm(testutils.TestAddress("mallory")), testing.NewCodeRealm("gno.land/r/evil/relay"), } { testing.SetRealm(caller) uassert.AbortsWithMessage(t, cur, "unauthorized", func() { Set(cross(cur), "header", "pwned") }) uassert.AbortsWithMessage(t, cur, "unauthorized", func() { Delete(cross(cur), "header") }) } uassert.False(t, strings.Contains(Render(""), "pwned")) } func TestChartTextIsEscaped(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(admin)) old := Get(reviewActivitySlot) Set(cross(cur), reviewActivitySlot, `title reviews; series <script>alert(1)</script>,a&b; 2026-01 1 2`) out := renderChart(reviewActivitySlot) raw, err := base64.StdEncoding.DecodeString(out[strings.Index(out, "base64,")+7 : len(out)-2]) uassert.NoError(t, err) uassert.False(t, strings.Contains(string(raw), "<script>")) uassert.True(t, strings.Contains(string(raw), "&lt;script&gt;")) uassert.True(t, strings.Contains(string(raw), "a&amp;b")) Set(cross(cur), reviewActivitySlot, old) } func TestHeartsListIsPaged(t *testing.T) { hearts = avl.NewTree() for i := 0; i < 250; i++ { hearts.Set(testutils.TestAddress("fan"+strconv.Itoa(i)).String(), int64(i)) } first := Render("hearts") uassert.Equal(t, heartsPerPage, strings.Count(first, "\n- ")) uassert.True(t, strings.Contains(first, "Page 1 of 3.")) uassert.True(t, strings.Contains(first, "[Next]("+home+":hearts/2)")) uassert.False(t, strings.Contains(first, "[Previous]")) last := Render("hearts/3") uassert.Equal(t, 50, strings.Count(last, "\n- ")) uassert.True(t, strings.Contains(last, "[Previous]("+home+":hearts/2)")) uassert.False(t, strings.Contains(last, "[Next]")) for _, bad := range []string{"hearts/0", "hearts/4", "hearts/x", "hearts/-1"} { uassert.True(t, strings.Contains(Render(bad), "There is no such page."), bad) } hearts = avl.NewTree() } func TestOrderSurvivesBadEntries(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(admin)) old := Get(orderSlot) Set(cross(cur), orderSlot, "nosuchsection,,header") page := Render("") uassert.True(t, strings.HasPrefix(strings.TrimSpace(page), "I'm a developer engineer")) Delete(cross(cur), orderSlot) uassert.True(t, strings.Contains(Render(""), "This page is a contract")) Set(cross(cur), orderSlot, old) } func TestQueryDoesNotBreakPages(t *testing.T) { uassert.Equal(t, Render(""), Render("?utm_source=x")) uassert.Equal(t, Render(""), Render("?fbclid=abc&x=1")) uassert.Equal(t, Render("hearts"), Render("hearts?x=1")) // The game keeps its query: it carries the guess. setDay(launchDay) uassert.True(t, strings.Contains(Render("wordle/1?guess=crane"), "CRANE")) } func TestLongPathIsRefused(t *testing.T) { setDay(launchDay) // The longest link the game writes: six guesses and a typed one. longest := "wordle/99999/CRANE.SLATE.ROUTE.TRACE.QUERY.PROXY?guess=" + url.QueryEscape(" crane ") uassert.True(t, len(longest) <= maxPathLen) uassert.False(t, strings.HasPrefix(Render(longest), "# Not found")) for _, p := range []string{ "wordle/1?" + strings.Repeat("a=&", 100), "wordle/1/" + strings.Repeat(".", 300), "hearts/" + strings.Repeat("9", 300), } { uassert.True(t, strings.HasPrefix(Render(p), "# Not found"), p[:20]) } } func TestHeroTextIsEditable(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(admin)) old := Get(heroSlot) decode := func() string { out := heroImage() raw, err := base64.StdEncoding.DecodeString(out[strings.Index(out, "base64,")+7 : len(out)-1]) uassert.NoError(t, err) return string(raw) } uassert.True(t, strings.Contains(decode(), ">David G.</text>")) Set(cross(cur), heroSlot, "title Dave; subtitle <b>&co</b>; bogus x; tagline hello") svg := decode() uassert.True(t, strings.Contains(svg, ">Dave</text>")) uassert.True(t, strings.Contains(svg, ">&lt;b&gt;&amp;co&lt;/b&gt;</text>")) uassert.True(t, strings.Contains(svg, ">hello</text>")) // A key the section leaves out keeps its default. uassert.True(t, strings.Contains(svg, ">gno.land core contributor</text>")) uassert.True(t, strings.HasPrefix(heroImage(), "![David G., developer engineer at Samouraï Coop](")) Set(cross(cur), heroSlot, "alt a ]( trap") uassert.True(t, strings.HasPrefix(heroImage(), "![a )( trap](")) Set(cross(cur), heroSlot, old) } func TestSetActivity(cur realm, t *testing.T) { oldPR, oldRev, oldAreas := Get(activitySlot), Get(reviewActivitySlot), Get(areasSlot) testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("mallory"))) uassert.AbortsWithMessage(t, cur, "unauthorized", func() { SetActivity(cross(cur), "x", "x", "x") }) uassert.Equal(t, oldPR, Get(activitySlot)) testing.SetRealm(testing.NewUserRealm(admin)) SetActivity(cross(cur), "title a; series s; 2026-01 1", "", "title c; slice 3 Docs") uassert.Equal(t, "title a; series s; 2026-01 1", Get(activitySlot)) uassert.Equal(t, oldRev, Get(reviewActivitySlot)) // empty leaves it as it is uassert.Equal(t, "title c; slice 3 Docs", Get(areasSlot)) out := renderChart(activitySlot) raw, err := base64.StdEncoding.DecodeString(out[strings.Index(out, "base64,")+7 : len(out)-2]) uassert.NoError(t, err) uassert.True(t, strings.Contains(string(raw), ">1 a</text>")) SetActivity(cross(cur), oldPR, oldRev, oldAreas) }
  20. #20wordle.gno
  21. #21package home import ( "net/url" "strconv" "strings" "time" ) // Gnordle: guess the day's five-letter word in six tries. The game keeps no // state on chain. Past guesses ride in the page path, wordle/<n>/CRANE.SLATE, // and a gnoweb form adds the next one as ?guess=, so playing needs no wallet. const ( wordLen = 5 maxGuesses = 6 launchDay = 20719 // 2026-09-23, in days since 1970-01-01 UTC: puzzle #1 stride = 17 // coprime with len(words), so every word comes up once per cycle ) // words are the answers, gno and blockchain flavoured. var words = []string{ "REALM", "CROSS", "BLOCK", "CHAIN", "TOKEN", "STAKE", "PANIC", "DEFER", "SLICE", "ARRAY", "PRINT", "STACK", "QUEUE", "TUPLE", "FLOAT", "CONST", "PARSE", "PROOF", "NONCE", "VOTER", "SHARD", "MERGE", "AUDIT", "DEBUG", "TRACE", "CACHE", "INDEX", "QUERY", "ROUTE", "VALUE", "RUNES", "ERROR", "GUARD", "OWNER", "ADMIN", "TREES", "NODES", "PEERS", "GRAPH", "FORKS", "MINTS", "LOGIC", "BYTES", "CLASS", "FIELD", "LOOPS", "PROBE", "SCOPE", "TYPED", "BENCH", "CLONE", "PATCH", "EMBED", "FETCH", "LABEL", "LOCAL", "PROXY", "SCALE", "SPAWN", } // today returns the number of the current puzzle, from the last block's time. func today() int { return int(time.Now().Unix()/86400) - launchDay + 1 } // answer returns the word of puzzle n. func answer(n int) string { i := ((n-1)*stride%len(words) + len(words)) % len(words) return words[i] } // score colours a guess against the answer, handling repeated letters the way // the original game does: a letter is marked present only as many times as it // is still unmatched in the answer. func score(guess, ans string) string { marks := make([]string, wordLen) left := map[byte]int{} for i := 0; i < wordLen; i++ { if guess[i] == ans[i] { marks[i] = "🟩" } else { left[ans[i]]++ } } for i := 0; i < wordLen; i++ { if marks[i] != "" { continue } if left[guess[i]] > 0 { marks[i] = "🟨" left[guess[i]]-- } else { marks[i] = "⬛" } } return strings.Join(marks, "") } func validGuess(g string) bool { if len(g) != wordLen { return false } for i := 0; i < wordLen; i++ { if g[i] < 'A' || g[i] > 'Z' { return false } } return true } func renderWordle(path string) string { path, query, _ := strings.Cut(path, "?") parts := strings.Split(path, "/") // "wordle", [n], [guesses] n := today() var guesses []string if len(parts) > 1 { v, err := strconv.Atoi(parts[1]) if err != nil || v < 1 { return wordleError("There is no puzzle with that number.") } n = v } if n > today() { return wordleError("Puzzle #" + strconv.Itoa(n) + " is not out yet.") } if len(parts) > 2 && parts[2] != "" { guesses = strings.Split(parts[2], ".") } if len(parts) > 3 || len(guesses) > maxGuesses { return wordleError("This game link is broken.") } for _, g := range guesses { if !validGuess(g) { return wordleError("This game link is broken.") } } ans := answer(n) notice := "" if q, err := url.ParseQuery(query); err == nil && q.Get("guess") != "" && !solved(guesses, ans) && len(guesses) < maxGuesses { g := strings.ToUpper(strings.TrimSpace(q.Get("guess"))) if validGuess(g) { guesses = append(guesses, g) } else { // The guess is never echoed: it comes from the link, and markdown in // it would render on this page under the owner's address. notice = "> That guess is not five letters, A to Z.\n\n" } } num := strconv.Itoa(n) var b strings.Builder b.WriteString("# Gnordle #" + num + "\n\n") b.WriteString("Guess the word in six tries. 🟩 right letter, right place. 🟨 in the word, elsewhere. ⬛ not in the word. ") b.WriteString("Everyone gets the same word, and a new one comes out every day at 00:00 UTC.\n\n") b.WriteString(notice) grid := "" for _, g := range guesses { row := score(g, ans) grid += row + "\n" b.WriteString(row + " " + g + "\n\n") } if miss := missing(guesses, ans); miss != "" { b.WriteString("Not in the word: " + miss + "\n\n") } switch { case solved(guesses, ans): b.WriteString("**Solved in " + strconv.Itoa(len(guesses)) + "/6.** Copy your result:\n\n") b.WriteString("```\nGnordle #" + num + " " + strconv.Itoa(len(guesses)) + "/6\n" + grid + "```\n\n") case len(guesses) == maxGuesses: b.WriteString("**The word was " + ans + ".** Copy your result:\n\n") b.WriteString("```\nGnordle #" + num + " X/6\n" + grid + "```\n\n") default: b.WriteString("<gno-form path=\"wordle/" + num) if len(guesses) > 0 { b.WriteString("/" + strings.Join(guesses, ".")) } b.WriteString("\">\n <gno-input name=\"guess\" placeholder=\"Guess " + strconv.Itoa(len(guesses)+1) + " of 6: five letters\" />\n</gno-form>\n\n") } b.WriteString("[Start over](" + home + ":wordle/" + num + ")") if n < today() { b.WriteString(" · [Play today's puzzle](" + home + ":wordle)") } b.WriteString(" · [Back home](" + home + ")\n") return b.String() } func solved(guesses []string, ans string) bool { return len(guesses) > 0 && guesses[len(guesses)-1] == ans } // missing lists the guessed letters that are not in the answer, in guess order. func missing(guesses []string, ans string) string { var out []string seen := map[byte]bool{} for _, g := range guesses { for i := 0; i < wordLen; i++ { c := g[i] if !seen[c] && !strings.ContainsRune(ans, rune(c)) { out = append(out, string(c)) } seen[c] = true } } return strings.Join(out, " ") } func wordleError(msg string) string { return "# Gnordle\n\n" + msg + "\n\n[Play today's puzzle](" + home + ":wordle) · [Back home](" + home + ")\n" } func renderGameTeaser() string { return "## Gnordle\n\nGuess today's five-letter word in six tries. Same word for everyone, " + "a new one every day, no wallet needed. [Play puzzle #" + strconv.Itoa(today()) + "](" + home + ":wordle)\n" }
  22. #22wordle_test.gno
  23. #23package home import ( "strings" "testing" "gno.land/p/nt/uassert/v0" ) func TestWords(t *testing.T) { seen := map[string]bool{} for _, w := range words { uassert.True(t, validGuess(w), "bad word: "+w) uassert.False(t, seen[w], "duplicate word: "+w) seen[w] = true } // stride is coprime with the list, so a full cycle hits every word once. got := map[string]bool{} for n := 1; n <= len(words); n++ { got[answer(n)] = true } uassert.Equal(t, len(words), len(got)) } func TestScore(t *testing.T) { uassert.Equal(t, "🟩🟩🟩🟩🟩", score("REALM", "REALM")) uassert.Equal(t, "⬛⬛⬛⬛⬛", score("BUNNY", "REALM")) uassert.Equal(t, "🟨🟨🟨🟨🟨", score("EALMR", "REALM")) // One E in the answer: the second E in the guess gets nothing. uassert.Equal(t, "⬛🟩🟨⬛⬛", score("EERIE", "REALM")) uassert.Equal(t, "🟨⬛⬛⬛⬛", score("EXXEX", "REALM")) // The exact match claims the letter before a misplaced copy can. uassert.Equal(t, "⬛🟩🟩⬛⬛", score("AAXXX", "BAXYZ")) } func TestWordleFlow(t *testing.T) { setDay(launchDay + 2) // puzzle #3 ans := answer(3) page := Render("wordle") uassert.True(t, strings.Contains(page, "# Gnordle #3")) uassert.True(t, strings.Contains(page, `<gno-form path="wordle/3">`)) page = Render("wordle/3?guess=crane") uassert.True(t, strings.Contains(page, score("CRANE", ans)+" CRANE")) uassert.True(t, strings.Contains(page, `<gno-form path="wordle/3/CRANE">`)) uassert.True(t, strings.Contains(page, "Guess 2 of 6")) page = Render("wordle/3/CRANE?guess=" + ans) uassert.True(t, strings.Contains(page, "**Solved in 2/6.**")) uassert.True(t, strings.Contains(page, "Gnordle #3 2/6")) uassert.False(t, strings.Contains(page, "<gno-form")) page = Render("wordle/3?guess=abc") uassert.True(t, strings.Contains(page, "> That guess is not five letters, A to Z.")) uassert.True(t, strings.Contains(page, `<gno-form path="wordle/3">`)) page = Render("wordle/1") uassert.True(t, strings.Contains(page, "[Play today's puzzle]")) } func TestWordleLost(t *testing.T) { setDay(launchDay) ans := answer(1) wrong := "ZZZZZ" if ans == wrong { wrong = "YYYYY" } path := "wordle/1/" + strings.Repeat(wrong+".", 5) + wrong page := Render(path) uassert.True(t, strings.Contains(page, "**The word was "+ans+".**")) uassert.True(t, strings.Contains(page, "Gnordle #1 X/6")) } func TestWordleBadLinks(t *testing.T) { setDay(launchDay) uassert.True(t, strings.Contains(Render("wordle/2"), "is not out yet")) uassert.True(t, strings.Contains(Render("wordle/0"), "no puzzle with that number")) uassert.True(t, strings.Contains(Render("wordle/x"), "no puzzle with that number")) uassert.True(t, strings.Contains(Render("wordle/1/CRA"), "link is broken")) uassert.True(t, strings.Contains(Render("wordle/1/A.B/C"), "link is broken")) tooMany := "wordle/1/" + strings.Repeat("CRANE.", 6) + "CRANE" uassert.True(t, strings.Contains(Render(tooMany), "link is broken")) }
  24. #24/gno.MemPackageType

Result log

msg:0,success:true,log:,events:[]