Transaction
158F5C5ED16609…9E8E45A0F2B5
Block 747,739 · index 0 · indexed
Summary
- Hash
- 158F5C5ED16609FBFBCDCA9F5958B46610341D7D82F510492EF49E8E45A0F2B5
- Block
- 747,739
- Size
- 18575 bytes
- Gas used
- 29,194,350 / 58,388,640
- Fee
- 58389 ugnot
- Status
- success
Messages
Arguments · 17
- #1lotto
- #2gnomod.toml
- #3module = "gno.land/r/akkadia/beta/lotto" gno = "0.9"
- #4lotto.gno
- #5package lotto import ( "chain" "strconv" "gno.land/p/akkadia/beta/accesscontrol" "gno.land/p/nt/bptree/v0" "gno.land/r/akkadia/beta/admin" ) const AddNumberEvent = "AddNumber" var holdings = bptree.NewBPTree32() // AddNumber increments the holding once per successful call, including retries. // Only the direct admin or operator caller may register a number. Individual // ticket identities, issuance times and past round balances are not retained. func AddNumber(cur realm, owner address, number uint64) { accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertValidAddress(owner) key := strconv.FormatUint(number, 10) + ":" + owner.String() count := uint64(0) if current := holdings.Get(key); current != nil { count = current.(uint64) } if count == ^uint64(0) { panic("number count overflow") } count++ holdings.Set(key, count) chain.Emit( AddNumberEvent, "owner", owner.String(), "number", strconv.FormatUint(number, 10), "count", strconv.FormatUint(count, 10), ) } func DeleteAll(cur realm) { accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin) holdings = bptree.NewBPTree32() } // GetNumberCount returns zero when the valid owner has no holding for number. func GetNumberCount(number uint64, owner address) uint64 { assertValidAddress(owner) current := holdings.Get(strconv.FormatUint(number, 10) + ":" + owner.String()) if current == nil { return 0 } return current.(uint64) } // ListOwnersByNumber returns fresh values in ascending address order. A cursor // need not be stored. Each row carries the page cursor; an empty cursor or empty // list ends pagination. Counts are decimal strings to preserve uint64 precision. // Pages reflect current state, not a historical snapshot. func ListOwnersByNumber(number uint64, afterOwner string, limit int) []map[string]string { if afterOwner != "" { assertValidAddress(address(afterOwner)) } assertListLimit(limit) numberStr := strconv.FormatUint(number, 10) prefix := numberStr + ":" start := prefix if afterOwner != "" { // Valid address strings cannot contain NUL, so this excludes the cursor // without visiting it or scanning preceding owners. start += afterOwner + "\x00" } result := make([]map[string]string, 0, limit) nextOwner := "" holdings.Iterate(start, numberStr+";", func(key string, count any) bool { if len(result) == limit { nextOwner = result[len(result)-1]["owner"] return true } result = append(result, map[string]string{ "owner": key[len(prefix):], "count": strconv.FormatUint(count.(uint64), 10), "nextOwner": "", }) return false }) for _, item := range result { item["nextOwner"] = nextOwner } return result }
- #6lotto_test.gno
- #7package lotto import ( "strconv" "testing" "gno.land/p/akkadia/beta/accesscontrol" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" "gno.land/r/akkadia/beta/admin" ) func TestAddNumber(cur realm, t *testing.T) { setupLotto(cross(cur)) for _, caller := range []address{testAdmin, testOperator} { testing.SetOriginCaller(caller) testing.SetRealm(testing.NewUserRealm(caller)) for i := 0; i < 3; i++ { AddNumber(cross(cur), testOwner, 123456) } } uassert.Equal(t, GetNumberCount(123456, testOwner), uint64(6)) uassert.Equal(t, holdings.Size(), 1) testing.SetRealm(testing.NewUserRealm(testAdmin)) AddNumber(cross(cur), testOther, 123456) AddNumber(cross(cur), testOwner, 654321) uassert.Equal(t, GetNumberCount(123456, testOwner), uint64(6)) uassert.Equal(t, GetNumberCount(123456, testOther), uint64(1)) uassert.Equal(t, GetNumberCount(654321, testOwner), uint64(1)) uassert.Equal(t, GetNumberCount(654321, testOther), uint64(0)) uassert.Equal(t, holdings.Size(), 3) page := ListOwnersByNumber(123456, "", 100) uassert.Equal(t, len(page), 2) uassert.Equal(t, page[0]["nextOwner"], "") for _, item := range page { uassert.Equal(t, item["count"], strconv.FormatUint(GetNumberCount(123456, address(item["owner"])), 10)) } } func TestAddNumberAuthority(cur realm, t *testing.T) { setupLotto(cross(cur)) testing.SetRealm(testing.NewUserRealm(testAdmin)) AddNumber(cross(cur), testOwner, 1) for _, caller := range []address{testOwner, testOther} { testing.SetOriginCaller(caller) testing.SetRealm(testing.NewUserRealm(caller)) uassert.AbortsWithMessage(t, cur, "admin or operator access required", func() { AddNumber(cross(cur), testOwner, 1) }) uassert.Equal(t, GetNumberCount(1, testOwner), uint64(1)) } testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) admin.UnsetOperator(cross(cur), testOperator) testing.SetOriginCaller(testOperator) testing.SetRealm(testing.NewUserRealm(testOperator)) uassert.AbortsWithMessage(t, cur, "admin or operator access required", func() { AddNumber(cross(cur), testOwner, 1) }) testing.SetRealm(testing.NewUserRealm(testAdmin)) admin.SetOperator(cross(cur), testOperator) for _, signer := range []address{testAdmin, testOperator} { testing.SetOriginCaller(signer) testing.SetRealm(testing.NewCodeRealm("gno.land/r/example/lotto_relay")) uassert.AbortsWithMessage(t, cur, "admin or operator access required", func() { AddNumber(cross(cur), testOwner, 1) }) } // Probe the actual helper with a foreign realm, without cross(stale) masking // its spoofed-realm assertion with a builtin crossing failure. spoofed := cur.Previous() uassert.PanicsWithMessage(t, cur, "spoofed realm", func() { accesscontrol.MustGetAdminOrOperatorCaller(0, spoofed, admin.IsAdmin, admin.IsOperator) }) uassert.Equal(t, GetNumberCount(1, testOwner), uint64(1)) uassert.Equal(t, holdings.Size(), 1) } func TestAddNumberValidation(cur realm, t *testing.T) { setupLotto(cross(cur)) for _, number := range []uint64{0, ^uint64(0)} { testing.SetRealm(testing.NewUserRealm(testAdmin)) AddNumber(cross(cur), testOwner, number) uassert.Equal(t, GetNumberCount(number, testOwner), uint64(1)) uassert.Equal(t, ListOwnersByNumber(number, "", 1)[0]["owner"], testOwner.String()) } for _, owner := range []address{"", "invalid", address(testOwner.String() + "x")} { testing.SetRealm(testing.NewUserRealm(testAdmin)) uassert.AbortsWithMessage(t, cur, "invalid address", func() { AddNumber(cross(cur), owner, 1) }) uassert.PanicsWithMessage(t, cur, "invalid address", func() { GetNumberCount(1, owner) }) if owner != "" { uassert.PanicsWithMessage(t, cur, "invalid address", func() { ListOwnersByNumber(1, owner.String(), 1) }) } } uassert.Equal(t, holdings.Size(), 2) // Overflow cannot be reached economically through repeated public calls. holdings.Set("1:"+testOwner.String(), ^uint64(0)) testing.SetRealm(testing.NewUserRealm(testAdmin)) uassert.AbortsWithMessage(t, cur, "number count overflow", func() { AddNumber(cross(cur), testOwner, 1) }) uassert.Equal(t, GetNumberCount(1, testOwner), ^uint64(0)) uassert.Equal(t, ListOwnersByNumber(1, "", 1)[0]["count"], "18446744073709551615") uassert.Equal(t, holdings.Size(), 3) } func TestListOwnersByNumber(cur realm, t *testing.T) { setupLotto(cross(cur)) uassert.Equal(t, len(ListOwnersByNumber(1, "", 100)), 0) uassert.Equal(t, GetNumberCount(1, testOwner), uint64(0)) uassert.Equal(t, holdings.Size(), 0) for _, limit := range []int{-1, 0, 101} { uassert.PanicsWithMessage(t, cur, "limit must be between 1 and 100", func() { ListOwnersByNumber(1, "", limit) }) } for _, number := range []uint64{1, 10, 100} { for i := 0; i < 105; i++ { testing.SetRealm(testing.NewUserRealm(testAdmin)) owner := testutils.TestAddress("lottoPage" + strconv.FormatUint(number, 10) + ":" + strconv.Itoa(i)) AddNumber(cross(cur), owner, number) if i%2 == 0 { AddNumber(cross(cur), owner, number) } } } for _, number := range []uint64{1, 10, 100} { for _, limit := range []int{1, 7, 100} { after := "" seen := 0 for { page := ListOwnersByNumber(number, after, limit) for _, item := range page { uassert.True(t, item["owner"] > after) uassert.Equal(t, item["nextOwner"], page[0]["nextOwner"]) uassert.Equal(t, item["count"], strconv.FormatUint(GetNumberCount(number, address(item["owner"])), 10)) after = item["owner"] seen++ } if len(page) == 0 || page[0]["nextOwner"] == "" { break } uassert.Equal(t, len(page), limit) uassert.Equal(t, page[0]["nextOwner"], after) } uassert.Equal(t, seen, 105) uassert.Equal(t, len(ListOwnersByNumber(number, after, limit)), 0) } } uassert.Equal(t, holdings.Size(), 315) uassert.Equal(t, len(ListOwnersByNumber(1000, "", 100)), 0) // A valid cursor between stored owners does not have to be stored itself. missing := testutils.TestAddress("lottoMissing").String() uassert.Equal(t, GetNumberCount(1, address(missing)), uint64(0)) first := ListOwnersByNumber(1, "", 100) all := append(first, ListOwnersByNumber(1, first[0]["nextOwner"], 100)...) expected := []map[string]string{} for _, item := range all { if item["owner"] > missing { expected = append(expected, item) } } page := ListOwnersByNumber(1, missing, 100) actual := page if len(page) > 0 && page[0]["nextOwner"] != "" { actual = append(actual, ListOwnersByNumber(1, page[0]["nextOwner"], 100)...) } uassert.Equal(t, len(actual), len(expected)) for i, item := range actual { uassert.Equal(t, item["owner"], expected[i]["owner"]) uassert.Equal(t, item["count"], expected[i]["count"]) } fresh := ListOwnersByNumber(1, "", 1) original := map[string]string{"owner": fresh[0]["owner"], "count": fresh[0]["count"]} fresh[0]["owner"] = testOther.String() fresh[0]["count"] = "999" fresh[0]["nextOwner"] = "changed" uassert.Equal(t, ListOwnersByNumber(1, "", 1)[0]["owner"], original["owner"]) uassert.Equal(t, ListOwnersByNumber(1, "", 1)[0]["count"], original["count"]) uassert.Equal(t, strconv.FormatUint(GetNumberCount(1, address(original["owner"])), 10), original["count"]) uassert.Equal(t, holdings.Size(), 315) } func TestDeleteAll(cur realm, t *testing.T) { setupLotto(cross(cur)) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) AddNumber(cross(cur), testOwner, 1) AddNumber(cross(cur), testOwner, 1) AddNumber(cross(cur), testOther, 1) AddNumber(cross(cur), testOwner, 2) DeleteAll(cross(cur)) uassert.Equal(t, holdings.Size(), 0) for _, number := range []uint64{1, 2} { for _, owner := range []address{testOwner, testOther} { uassert.Equal(t, GetNumberCount(number, owner), uint64(0)) } uassert.Equal(t, len(ListOwnersByNumber(number, "", 100)), 0) } DeleteAll(cross(cur)) // Operators can register again, starting from one instead of the old count. testing.SetOriginCaller(testOperator) testing.SetRealm(testing.NewUserRealm(testOperator)) AddNumber(cross(cur), testOwner, 1) uassert.Equal(t, GetNumberCount(1, testOwner), uint64(1)) uassert.Equal(t, holdings.Size(), 1) uassert.Equal(t, len(ListOwnersByNumber(1, "", 100)), 1) } func TestDeleteAllAuthority(cur realm, t *testing.T) { setupLotto(cross(cur)) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) AddNumber(cross(cur), testOwner, 1) for _, caller := range []address{testOwner, testOperator} { testing.SetOriginCaller(caller) testing.SetRealm(testing.NewUserRealm(caller)) uassert.AbortsWithMessage(t, cur, "admin access required", func() { DeleteAll(cross(cur)) }) uassert.Equal(t, GetNumberCount(1, testOwner), uint64(1)) uassert.Equal(t, holdings.Size(), 1) } testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewCodeRealm("gno.land/r/example/lotto_relay")) uassert.AbortsWithMessage(t, cur, "admin access required", func() { DeleteAll(cross(cur)) }) uassert.Equal(t, GetNumberCount(1, testOwner), uint64(1)) uassert.Equal(t, holdings.Size(), 1) }
- #8render.gno
- #9package lotto import ( "net/url" "strconv" ) func Render(path string) string { output := `# AKKADIA Lotto Look up owners and their quantities for a number. <gno-form path="number"> <gno-input name="number" placeholder="Enter a number" /> </gno-form> ` u, err := url.Parse(path) if err != nil { return output + "Invalid URL\n" } query, err := url.ParseQuery(u.RawQuery) if err != nil { return output + "Invalid query\n" } switch u.Path { case "": return output case "number": raw := query.Get("number") if raw == "" { return output + "Number is required\n" } number, err := strconv.ParseUint(raw, 10, 64) if err != nil { return output + "Invalid number\n" } afterOwner := query.Get("afterOwner") if afterOwner != "" && !address(afterOwner).IsValid() { return output + "Invalid cursor address\n" } numberStr := strconv.FormatUint(number, 10) page := ListOwnersByNumber(number, afterOwner, 20) output += "## Number " + numberStr + "\n\n" output += "[Home](./lotto:)\n\n" if afterOwner != "" { output += "[First page](./lotto:number?number=" + numberStr + ")\n\n" } if len(page) == 0 { return output + "No owners on this page.\n" } output += "| Owner | Count |\n| --- | ---: |\n" for _, item := range page { // Owners are validated addresses and counts are formatted integers. output += "| " + item["owner"] + " | " + item["count"] + " |\n" } if page[0]["nextOwner"] != "" { output += "\n[Next](./lotto:number?number=" + numberStr + "&afterOwner=" + url.QueryEscape(page[0]["nextOwner"]) + ")\n" } return output default: return output + "Page not found\n" } }
- #10render_test.gno
- #11package lotto import ( "net/url" "strconv" "strings" "testing" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) func TestRenderInput(cur realm, t *testing.T) { setupLotto(cross(cur)) home := Render("") uassert.True(t, strings.Contains(home, "# AKKADIA Lotto")) uassert.True(t, strings.Contains(home, `<gno-form path="number">`)) uassert.True(t, strings.Contains(home, `name="number"`)) for _, tc := range []struct{ path, message string }{ {"%", "Invalid URL"}, {"number?number=%zz", "Invalid query"}, {"unknown", "Page not found"}, {"number", "Number is required"}, {"number?number=-1", "Invalid number"}, {"number?number=1.5", "Invalid number"}, {"number?number=18446744073709551616", "Invalid number"}, {"number?number=" + url.QueryEscape(`<script>alert(1)</script>`), "Invalid number"}, {"number?number=1&afterOwner=" + url.QueryEscape(`[x](javascript:alert(1))`), "Invalid cursor address"}, {"number?number=1&afterOwner=invalid", "Invalid cursor address"}, } { result := Render(tc.path) uassert.True(t, strings.Contains(result, tc.message)) uassert.True(t, strings.Contains(result, `<gno-form path="number">`)) uassert.False(t, strings.Contains(result, "<script>")) uassert.False(t, strings.Contains(result, "javascript:")) } for _, number := range []string{"0", "18446744073709551615"} { result := Render("number?number=" + number) uassert.True(t, strings.Contains(result, "## Number "+number+"\n")) uassert.True(t, strings.Contains(result, "No owners on this page.")) uassert.False(t, strings.Contains(result, "[Next]")) } uassert.Equal(t, holdings.Size(), 0) } func TestRenderPagination(cur realm, t *testing.T) { setupLotto(cross(cur)) for i := 0; i < 41; i++ { testing.SetRealm(testing.NewUserRealm(testAdmin)) owner := testutils.TestAddress("lottoRender" + strconv.Itoa(i)) AddNumber(cross(cur), owner, 123) AddNumber(cross(cur), owner, 123) } testing.SetRealm(testing.NewUserRealm(testAdmin)) AddNumber(cross(cur), testOther, 1234) route := "number?number=000123" afterOwner := "" for _, expectedCount := range []int{20, 20, 1} { expected := ListOwnersByNumber(123, afterOwner, 20) result := Render(route) uassert.True(t, strings.Contains(result, "## Number 123\n")) uassert.True(t, strings.Contains(result, "[Home](./lotto:)")) uassert.False(t, strings.Contains(result, testOther.String())) uassert.Equal(t, strings.Count(result, " | 2 |\n"), expectedCount) uassert.Equal(t, strings.Contains(result, "[First page]"), afterOwner != "") previous := -1 for _, item := range expected { position := strings.Index(result, "| "+item["owner"]+" | 2 |") uassert.True(t, position > previous) previous = position uassert.Equal(t, GetNumberCount(123, address(item["owner"])), uint64(2)) } if expected[0]["nextOwner"] != "" { route = "number?number=123&afterOwner=" + url.QueryEscape(expected[0]["nextOwner"]) uassert.True(t, strings.Contains(result, "[Next](./lotto:"+route+")")) afterOwner = expected[0]["nextOwner"] } else { uassert.False(t, strings.Contains(result, "[Next]")) last := expected[len(expected)-1]["owner"] uassert.True(t, strings.Contains(Render("number?number=123&afterOwner="+url.QueryEscape(last)), "No owners on this page.")) } } uassert.Equal(t, holdings.Size(), 42) }
- #12setup_test.gno
- #13package lotto import ( "chain" "testing" "gno.land/p/nt/bptree/v0" "gno.land/p/nt/testutils/v0" "gno.land/r/akkadia/beta/admin" ) var ( testAdmin = testutils.TestAddress("lottoAdmin") testOperator = testutils.TestAddress("lottoOperator") testOwner = testutils.TestAddress("lottoOwner") testOther = testutils.TestAddress("lottoOther") ) // Call with cross(cur); each test resets holdings and restores the operator. func setupLotto(cur realm) { holdings = bptree.NewBPTree32() testing.SetOriginSend(chain.Coins{}) originalAdmin := admin.GetAdmin() if originalAdmin != testAdmin { testing.SetOriginCaller(originalAdmin) testing.SetRealm(testing.NewUserRealm(originalAdmin)) admin.TransferAdmin(cross(cur), testAdmin) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) admin.AcceptAdmin(cross(cur)) } testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) admin.SetOperator(cross(cur), testOperator) }
- #14validation.gno
- #15package lotto func assertValidAddress(addr address) { if !addr.IsValid() { panic("invalid address") } } func assertListLimit(limit int) { if limit < 1 || limit > 100 { panic("limit must be between 1 and 100") } }
- #16/gno.MemPackageType
- #17 MPUserAll
Result log
msg:0,success:true,log:,events:[]