Transaction
0663CDB8600141…9804EDD34F0D
Block 747,754 · index 0 · indexed
Summary
- Hash
- 0663CDB8600141419240161A15BA7E19A5BAD08C6937ED35B0A09804EDD34F0D
- Block
- 747,754
- Size
- 28234 bytes
- Gas used
- 41,538,893 / 83,077,726
- Fee
- 83078 ugnot
- Status
- success
Messages
- Package
- gno.land/r/akkadia/beta/item
Arguments · 17
- #1item
- #2config.gno
- #3package item import ( "chain" "strconv" "strings" "gno.land/p/akkadia/beta/accesscontrol" "gno.land/p/nt/bptree/v0" "gno.land/r/akkadia/beta/admin" ) const ( SetListLimitEvent = "SetListLimit" SetMinterRealmEvent = "SetMinterRealm" UnsetMinterRealmEvent = "UnsetMinterRealm" ) var ( listLimit int = 100 // Max items per ListByOwner page minterRealms *bptree.BPTree ) func SetListLimit(cur realm, limit int) { accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin) if limit < 1 { panic("limit must be at least 1") } oldLimit := listLimit listLimit = limit chain.Emit( SetListLimitEvent, "oldLimit", strconv.Itoa(oldLimit), "newLimit", strconv.Itoa(limit), ) } func GetListLimit() int { return listLimit } func SetMinterRealm(cur realm, pkgPath string) { accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin) addr := minterRealmAddress(pkgPath) minterRealms.Set(addr.String(), true) chain.Emit( SetMinterRealmEvent, "pkgPath", pkgPath, "address", addr.String(), ) } func UnsetMinterRealm(cur realm, pkgPath string) { accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin) addr := minterRealmAddress(pkgPath) if !minterRealms.Has(addr.String()) { panic("package path is not a minter realm") } minterRealms.Remove(addr.String()) chain.Emit( UnsetMinterRealmEvent, "pkgPath", pkgPath, "address", addr.String(), ) } func IsMinterRealm(addr address) bool { return minterRealms.Has(addr.String()) } func minterRealmAddress(pkgPath string) address { if pkgPath == "" { panic("package path is required") } if !strings.HasPrefix(pkgPath, "gno.land/r/") || pkgPath == "gno.land/r/" { panic("invalid package path") } return chain.PackageAddress(pkgPath) }
- #4gnomod.toml
- #5module = "gno.land/r/akkadia/beta/item" gno = "0.9"
- #6item.gno
- #7package item import ( "chain/runtime/unsafe" "strconv" "gno.land/p/akkadia/beta/accesscontrol" "gno.land/p/akkadia/beta/grc1155" "gno.land/p/nt/bptree/v0" "gno.land/r/akkadia/beta/admin" ) const ( maxInt64 = int64(9223372036854775807) ) var ( itemToken = grc1155.NewBasicGRC1155Token("") itemSupplies *bptree.BPTree ownerTokenIDs *bptree.BPTree ) func init() { resetItemState() } func resetItemState() { itemToken = grc1155.NewBasicGRC1155Token("") itemSupplies = bptree.NewBPTree32() ownerTokenIDs = bptree.NewBPTree32() minterRealms = bptree.NewBPTree32() listLimit = 100 } func SafeMint(cur realm, recipient address, tokenID string, amount int64) { executor := accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertItemNoCoins() mintItem(cross(cur), executor, recipient, tokenID, amount) } func SafeMintFromRealm(cur realm, recipient address, tokenID string, amount int64) { executor := mustGetMinterRealmCaller(0, cur) if unsafe.OriginCaller() != recipient { panic("item recipient must be origin caller") } mintItem(cross(cur), executor, recipient, tokenID, amount) } func Uri() string { return itemToken.Uri() } func IsApprovedForAll(owner, operator address) bool { return itemToken.IsApprovedForAll(owner, operator) } func SetApprovalForAll(cur realm, operator address, approved bool) { caller := accesscontrol.MustGetUserCaller(0, cur) assertItemNoCoins() if err := itemToken.SetApprovalForAll(caller, operator, approved); err != nil { panic("item operator approval failed: " + err.Error()) } } func SafeTransferFrom(cur realm, from, to address, tokenID string, amount int64) { caller := accesscontrol.MustGetUserCaller(0, cur) assertItemNoCoins() assertItemTokenID(tokenID) assertItemAmount(amount, "transfer") if err := itemToken.SafeTransferFrom(caller, from, to, grc1155.TokenID(tokenID), amount); err != nil { panic("item transfer failed: " + err.Error()) } if from != to { addOwnerTokenID(to, tokenID) if BalanceOf(from, tokenID) == 0 { removeOwnerTokenID(from, tokenID) } } } func BalanceOf(owner address, tokenID string) int64 { if !owner.IsValid() { panic("invalid item owner") } assertItemTokenID(tokenID) balance, err := itemToken.BalanceOf(owner, grc1155.TokenID(tokenID)) if err != nil { panic("item balance query failed: " + err.Error()) } return balance } func TotalSupply(tokenID string) int64 { assertItemTokenID(tokenID) value := itemSupplies.Get(tokenID) if value == nil { return 0 } return value.(int64) } func GetOwnedCount(owner address) int { if !owner.IsValid() { panic("invalid item owner") } ids, found := getOwnerTokenIDs(owner) if !found { return 0 } return ids.Size() } func ListByOwner(owner address, page, count int) []map[string]string { if !owner.IsValid() { panic("invalid item owner") } assertItemListPageCount(page, count) ids, found := getOwnerTokenIDs(owner) if !found { return []map[string]string{} } result := []map[string]string{} ids.IterateByOffset((page-1)*count, count, func(tokenID string, _ any) bool { result = append(result, map[string]string{ "tokenId": tokenID, "amount": strconv.FormatInt(BalanceOf(owner, tokenID), 10), }) return false }) return result } func Burn(cur realm, from address, tokenID string, amount int64) { caller := accesscontrol.MustGetUserCaller(0, cur) assertItemNoCoins() assertItemTokenID(tokenID) if caller != from && !itemToken.IsApprovedForAll(from, caller) { panic("item owner or approved operator required") } assertItemAmount(amount, "burn") burnItem(cross(cur), caller, from, tokenID, amount) } func BurnFromRealm(cur realm, from address, tokenID string, amount int64) { caller := mustGetMinterRealmCaller(0, cur) if unsafe.OriginCaller() != from { panic("item owner must be origin caller") } assertItemTokenID(tokenID) assertItemAmount(amount, "burn") burnItem(cross(cur), caller, from, tokenID, amount) } func mintItem(cur realm, executor, recipient address, tokenID string, amount int64) { assertItemTokenID(tokenID) if !recipient.IsValid() || recipient == cur.Address() { panic("invalid item recipient") } if amount <= 0 { panic("item amount must be positive") } supply := TotalSupply(tokenID) if supply > maxInt64-amount { panic("item amount overflow") } if err := itemToken.SafeMint(executor, recipient, grc1155.TokenID(tokenID), amount); err != nil { panic("item mint failed: " + err.Error()) } itemSupplies.Set(tokenID, supply+amount) addOwnerTokenID(recipient, tokenID) } func burnItem(cur realm, operator, from address, tokenID string, amount int64) { supply := TotalSupply(tokenID) if supply < amount { panic("item burn amount exceeds supply") } if err := itemToken.Burn(operator, from, grc1155.TokenID(tokenID), amount); err != nil { panic("item burn failed: " + err.Error()) } itemSupplies.Set(tokenID, supply-amount) if BalanceOf(from, tokenID) == 0 { removeOwnerTokenID(from, tokenID) } } func mustGetMinterRealmCaller(_ int, rlm realm) address { accesscontrol.AssertCurrentRealm(0, rlm) prev := rlm.Previous() if prev.IsUserCall() { panic("user calls not allowed: function must be called by a realm") } caller := prev.Address() if !IsMinterRealm(caller) { panic("minter realm required") } return caller } func assertItemNoCoins() { if len(unsafe.OriginSend()) != 0 { panic("attached coins not allowed") } } func assertItemTokenID(tokenID string) { if tokenID == "" { panic("token id is required") } } func assertItemAmount(amount int64, action string) { if amount <= 0 { panic("item " + action + " amount must be positive") } } func assertItemListPageCount(page, count int) { if page < 1 { panic("page must be at least 1") } if count < 1 { panic("count must be at least 1") } if count > listLimit { panic("count exceeds listLimit (max: " + strconv.Itoa(listLimit) + ")") } } func addOwnerTokenID(owner address, tokenID string) { value := ownerTokenIDs.Get(owner.String()) var ids *bptree.BPTree if value != nil { ids = value.(*bptree.BPTree) } else { ids = bptree.NewBPTree32() ownerTokenIDs.Set(owner.String(), ids) } ids.Set(tokenID, true) } func removeOwnerTokenID(owner address, tokenID string) { ids, found := getOwnerTokenIDs(owner) if !found { return } ids.Remove(tokenID) } func getOwnerTokenIDs(owner address) (*bptree.BPTree, bool) { value := ownerTokenIDs.Get(owner.String()) if value == nil { return nil, false } return value.(*bptree.BPTree), true }
- #8item_test.gno
- #9package item import ( "chain" "testing" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" "gno.land/r/akkadia/beta/admin" ) var itemTest = struct { Admin, Operator, Owner, Other address SetupDone bool }{ Admin: testutils.TestAddress("itemAdmin"), Operator: testutils.TestAddress("itemOperator"), Owner: testutils.TestAddress("itemOwner"), Other: testutils.TestAddress("itemOther"), } func setupItemTest(cur realm, t *testing.T) { t.Helper() resetItemState() testing.SetOriginSend(chain.Coins{}) if itemTest.SetupDone { return } originalAdmin := admin.GetAdmin() testing.SetOriginCaller(originalAdmin) testing.SetRealm(testing.NewUserRealm(originalAdmin)) admin.TransferAdmin(cross(cur), itemTest.Admin) testing.SetOriginCaller(itemTest.Admin) testing.SetRealm(testing.NewUserRealm(itemTest.Admin)) admin.AcceptAdmin(cross(cur)) admin.SetOperator(cross(cur), itemTest.Operator) itemTest.SetupDone = true } func TestSafeMintTracksSupply(cur realm, t *testing.T) { setupItemTest(cross(cur), t) testing.SetOriginCaller(itemTest.Operator) testing.SetRealm(testing.NewUserRealm(itemTest.Operator)) SafeMint(cross(cur), itemTest.Owner, "42", 5) uassert.Equal(t, int64(5), BalanceOf(itemTest.Owner, "42")) uassert.Equal(t, int64(5), TotalSupply("42")) SafeMint(cross(cur), itemTest.Owner, "42", 1) SafeMint(cross(cur), itemTest.Other, "42", 2) uassert.Equal(t, int64(6), BalanceOf(itemTest.Owner, "42")) uassert.Equal(t, int64(2), BalanceOf(itemTest.Other, "42")) uassert.Equal(t, int64(8), TotalSupply("42")) } func TestItemTokenIDAcceptsOpaqueStrings(cur realm, t *testing.T) { setupItemTest(cross(cur), t) testing.SetOriginCaller(itemTest.Operator) testing.SetRealm(testing.NewUserRealm(itemTest.Operator)) SafeMint(cross(cur), itemTest.Owner, "0", 1) SafeMint(cross(cur), itemTest.Owner, "4294967295", 2) SafeMint(cross(cur), itemTest.Owner, "sword", 3) uassert.Equal(t, int64(1), BalanceOf(itemTest.Owner, "0")) uassert.Equal(t, int64(2), TotalSupply("4294967295")) uassert.Equal(t, int64(3), TotalSupply("sword")) uassert.AbortsWithMessage(t, cur, "token id is required", func() { SafeMint(cross(cur), itemTest.Owner, "", 1) }) } func TestItemTransferAndBurnUpdateSupply(cur realm, t *testing.T) { setupItemTest(cross(cur), t) testing.SetOriginCaller(itemTest.Operator) testing.SetRealm(testing.NewUserRealm(itemTest.Operator)) SafeMint(cross(cur), itemTest.Owner, "42", 5) testing.SetOriginCaller(itemTest.Owner) testing.SetRealm(testing.NewUserRealm(itemTest.Owner)) SafeTransferFrom(cross(cur), itemTest.Owner, itemTest.Other, "42", 2) Burn(cross(cur), itemTest.Owner, "42", 1) uassert.Equal(t, int64(2), BalanceOf(itemTest.Owner, "42")) uassert.Equal(t, int64(2), BalanceOf(itemTest.Other, "42")) uassert.Equal(t, int64(4), TotalSupply("42")) } func TestItemApprovalAllowsTransferAndBurn(cur realm, t *testing.T) { setupItemTest(cross(cur), t) testing.SetOriginCaller(itemTest.Operator) testing.SetRealm(testing.NewUserRealm(itemTest.Operator)) SafeMint(cross(cur), itemTest.Owner, "42", 5) testing.SetOriginCaller(itemTest.Owner) testing.SetRealm(testing.NewUserRealm(itemTest.Owner)) SetApprovalForAll(cross(cur), itemTest.Other, true) testing.SetOriginCaller(itemTest.Other) testing.SetRealm(testing.NewUserRealm(itemTest.Other)) SafeTransferFrom(cross(cur), itemTest.Owner, itemTest.Admin, "42", 2) Burn(cross(cur), itemTest.Owner, "42", 1) uassert.Equal(t, int64(2), BalanceOf(itemTest.Owner, "42")) uassert.Equal(t, int64(2), BalanceOf(itemTest.Admin, "42")) uassert.Equal(t, int64(4), TotalSupply("42")) } func TestSafeMintSecurity(cur realm, t *testing.T) { setupItemTest(cross(cur), t) testing.SetOriginCaller(itemTest.Owner) testing.SetRealm(testing.NewUserRealm(itemTest.Owner)) uassert.AbortsWithMessage(t, cur, "admin or operator access required", func() { SafeMint(cross(cur), itemTest.Owner, "1", 1) }) testing.SetOriginCaller(itemTest.Operator) testing.SetRealm(testing.NewCodeRealm("gno.land/r/example/item_relay")) uassert.AbortsWithMessage(t, cur, "admin or operator access required", func() { SafeMint(cross(cur), itemTest.Owner, "1", 1) }) testing.SetRealm(testing.NewUserRealm(itemTest.Operator)) testing.SetOriginSend(chain.Coins{{"ugnot", 1}}) uassert.AbortsWithMessage(t, cur, "attached coins not allowed", func() { SafeMint(cross(cur), itemTest.Owner, "1", 1) }) } func TestItemOwnedList(cur realm, t *testing.T) { setupItemTest(cross(cur), t) testing.SetOriginCaller(itemTest.Operator) testing.SetRealm(testing.NewUserRealm(itemTest.Operator)) uassert.Equal(t, 0, GetOwnedCount(itemTest.Owner)) uassert.Equal(t, 0, len(ListByOwner(itemTest.Owner, 1, 10))) uassert.PanicsWithMessage(t, cur, "invalid item owner", func() { GetOwnedCount(address("")) }) uassert.PanicsWithMessage(t, cur, "page must be at least 1", func() { ListByOwner(itemTest.Owner, 0, 1) }) uassert.PanicsWithMessage(t, cur, "count must be at least 1", func() { ListByOwner(itemTest.Owner, 1, 0) }) uassert.PanicsWithMessage(t, cur, "count exceeds listLimit (max: 100)", func() { ListByOwner(itemTest.Owner, 1, 101) }) uassert.Equal(t, 100, GetListLimit()) SafeMint(cross(cur), itemTest.Owner, "10", 5) SafeMint(cross(cur), itemTest.Owner, "20", 3) SafeMint(cross(cur), itemTest.Owner, "30", 1) uassert.Equal(t, 3, GetOwnedCount(itemTest.Owner)) page1 := ListByOwner(itemTest.Owner, 1, 2) uassert.Equal(t, 2, len(page1)) uassert.Equal(t, "10", page1[0]["tokenId"]) uassert.Equal(t, "5", page1[0]["amount"]) uassert.Equal(t, "20", page1[1]["tokenId"]) page2 := ListByOwner(itemTest.Owner, 2, 2) uassert.Equal(t, 1, len(page2)) uassert.Equal(t, "30", page2[0]["tokenId"]) uassert.Equal(t, 0, len(ListByOwner(itemTest.Owner, 3, 2))) page1[0]["amount"] = "hacked" uassert.Equal(t, "5", ListByOwner(itemTest.Owner, 1, 1)[0]["amount"]) testing.SetOriginCaller(itemTest.Owner) testing.SetRealm(testing.NewUserRealm(itemTest.Owner)) SafeTransferFrom(cross(cur), itemTest.Owner, itemTest.Other, "10", 2) uassert.Equal(t, 3, GetOwnedCount(itemTest.Owner)) uassert.Equal(t, 1, GetOwnedCount(itemTest.Other)) uassert.Equal(t, "3", ListByOwner(itemTest.Owner, 1, 10)[0]["amount"]) uassert.Equal(t, "2", ListByOwner(itemTest.Other, 1, 10)[0]["amount"]) SafeTransferFrom(cross(cur), itemTest.Owner, itemTest.Other, "10", 3) uassert.Equal(t, 2, GetOwnedCount(itemTest.Owner)) uassert.Equal(t, 1, GetOwnedCount(itemTest.Other)) Burn(cross(cur), itemTest.Owner, "20", 3) uassert.Equal(t, 1, GetOwnedCount(itemTest.Owner)) uassert.Equal(t, "30", ListByOwner(itemTest.Owner, 1, 10)[0]["tokenId"]) testing.SetOriginCaller(itemTest.Operator) testing.SetRealm(testing.NewUserRealm(itemTest.Operator)) SafeMint(cross(cur), itemTest.Owner, "30", 2) uassert.Equal(t, 1, GetOwnedCount(itemTest.Owner)) uassert.Equal(t, "3", ListByOwner(itemTest.Owner, 1, 10)[0]["amount"]) } func TestSetListLimit(cur realm, t *testing.T) { setupItemTest(cross(cur), t) testing.SetOriginCaller(itemTest.Admin) testing.SetRealm(testing.NewUserRealm(itemTest.Admin)) SetListLimit(cross(cur), 50) uassert.Equal(t, 50, GetListLimit()) SetListLimit(cross(cur), 1) uassert.PanicsWithMessage(t, cur, "count exceeds listLimit (max: 1)", func() { ListByOwner(itemTest.Owner, 1, 2) }) uassert.AbortsWithMessage(t, cur, "limit must be at least 1", func() { SetListLimit(cross(cur), 0) }) uassert.AbortsWithMessage(t, cur, "limit must be at least 1", func() { SetListLimit(cross(cur), -1) }) testing.SetOriginCaller(itemTest.Operator) testing.SetRealm(testing.NewUserRealm(itemTest.Operator)) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetListLimit(cross(cur), 50) }) testing.SetOriginCaller(itemTest.Owner) testing.SetRealm(testing.NewUserRealm(itemTest.Owner)) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetListLimit(cross(cur), 50) }) testing.SetOriginCaller(itemTest.Admin) testing.SetRealm(testing.NewCodeRealm("gno.land/r/example/item_admin_relay")) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetListLimit(cross(cur), 50) }) } func TestSetMinterRealm(cur realm, t *testing.T) { setupItemTest(cross(cur), t) minterPath := "gno.land/r/example/item_minter" minter := testing.NewCodeRealm(minterPath) minterAddr := minter.Address() uassert.Equal(t, minterAddr.String(), chain.PackageAddress(minterPath).String()) uassert.Equal(t, false, IsMinterRealm(minterAddr)) testing.SetOriginCaller(itemTest.Operator) testing.SetRealm(testing.NewUserRealm(itemTest.Operator)) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetMinterRealm(cross(cur), minterPath) }) testing.SetOriginCaller(itemTest.Admin) testing.SetRealm(testing.NewUserRealm(itemTest.Admin)) uassert.AbortsWithMessage(t, cur, "package path is required", func() { SetMinterRealm(cross(cur), "") }) uassert.AbortsWithMessage(t, cur, "invalid package path", func() { SetMinterRealm(cross(cur), "gno.land/p/akkadia/beta/item") }) SetMinterRealm(cross(cur), minterPath) uassert.Equal(t, true, IsMinterRealm(minterAddr)) SetMinterRealm(cross(cur), minterPath) uassert.Equal(t, true, IsMinterRealm(minterAddr)) UnsetMinterRealm(cross(cur), minterPath) uassert.Equal(t, false, IsMinterRealm(minterAddr)) uassert.AbortsWithMessage(t, cur, "package path is not a minter realm", func() { UnsetMinterRealm(cross(cur), minterPath) }) uassert.AbortsWithMessage(t, cur, "package path is required", func() { UnsetMinterRealm(cross(cur), "") }) testing.SetRealm(testing.NewCodeRealm("gno.land/r/example/item_admin_relay")) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetMinterRealm(cross(cur), minterPath) }) } func TestSafeMintFromRealm(cur realm, t *testing.T) { setupItemTest(cross(cur), t) minter := testing.NewCodeRealm("gno.land/r/example/item_minter") unregistered := testing.NewCodeRealm("gno.land/r/example/item_minter_other") testing.SetOriginCaller(itemTest.Owner) testing.SetRealm(testing.NewUserRealm(itemTest.Owner)) uassert.AbortsWithMessage(t, cur, "user calls not allowed: function must be called by a realm", func() { SafeMintFromRealm(cross(cur), itemTest.Owner, "1", 1) }) testing.SetRealm(unregistered) uassert.AbortsWithMessage(t, cur, "minter realm required", func() { SafeMintFromRealm(cross(cur), itemTest.Owner, "1", 1) }) testing.SetOriginCaller(itemTest.Admin) testing.SetRealm(testing.NewUserRealm(itemTest.Admin)) SetMinterRealm(cross(cur), "gno.land/r/example/item_minter") testing.SetOriginCaller(itemTest.Owner) testing.SetRealm(minter) SafeMintFromRealm(cross(cur), itemTest.Owner, "7", 3) uassert.Equal(t, int64(3), BalanceOf(itemTest.Owner, "7")) uassert.Equal(t, int64(3), TotalSupply("7")) uassert.Equal(t, 1, GetOwnedCount(itemTest.Owner)) uassert.AbortsWithMessage(t, cur, "item recipient must be origin caller", func() { SafeMintFromRealm(cross(cur), itemTest.Other, "8", 1) }) uassert.Equal(t, int64(0), TotalSupply("8")) testing.SetOriginSend(chain.Coins{{"ugnot", 1}}) SafeMintFromRealm(cross(cur), itemTest.Owner, "9", 1) testing.SetOriginSend(chain.Coins{}) uassert.Equal(t, int64(1), BalanceOf(itemTest.Owner, "9")) } func TestBurnFromRealm(cur realm, t *testing.T) { setupItemTest(cross(cur), t) minter := testing.NewCodeRealm("gno.land/r/example/item_minter") testing.SetOriginCaller(itemTest.Operator) testing.SetRealm(testing.NewUserRealm(itemTest.Operator)) SafeMint(cross(cur), itemTest.Owner, "42", 5) testing.SetOriginCaller(itemTest.Admin) testing.SetRealm(testing.NewUserRealm(itemTest.Admin)) SetMinterRealm(cross(cur), "gno.land/r/example/item_minter") testing.SetOriginCaller(itemTest.Owner) testing.SetRealm(testing.NewUserRealm(itemTest.Owner)) uassert.AbortsWithMessage(t, cur, "user calls not allowed: function must be called by a realm", func() { BurnFromRealm(cross(cur), itemTest.Owner, "42", 1) }) testing.SetRealm(minter) uassert.AbortsWithMessage(t, cur, "item owner must be origin caller", func() { BurnFromRealm(cross(cur), itemTest.Other, "42", 1) }) uassert.Equal(t, int64(5), BalanceOf(itemTest.Owner, "42")) BurnFromRealm(cross(cur), itemTest.Owner, "42", 2) uassert.Equal(t, int64(3), BalanceOf(itemTest.Owner, "42")) uassert.Equal(t, int64(3), TotalSupply("42")) uassert.AbortsWithMessage(t, cur, "item amount must be positive", func() { SafeMintFromRealm(cross(cur), itemTest.Owner, "99", 0) }) uassert.Equal(t, int64(3), BalanceOf(itemTest.Owner, "42")) uassert.Equal(t, int64(3), TotalSupply("42")) uassert.Equal(t, int64(0), TotalSupply("99")) uassert.Equal(t, 1, GetOwnedCount(itemTest.Owner)) testing.SetOriginCaller(itemTest.Admin) testing.SetRealm(testing.NewUserRealm(itemTest.Admin)) UnsetMinterRealm(cross(cur), "gno.land/r/example/item_minter") testing.SetOriginCaller(itemTest.Owner) testing.SetRealm(minter) uassert.AbortsWithMessage(t, cur, "minter realm required", func() { BurnFromRealm(cross(cur), itemTest.Owner, "42", 1) }) uassert.Equal(t, int64(3), BalanceOf(itemTest.Owner, "42")) }
- #10render.gno
- #11package item import ( "net/url" "strconv" "strings" "gno.land/p/jeronimoalbi/pager/v0" ) func Render(iURL string) string { assertRenderURLLen(iURL) u, err := url.Parse(iURL) if err != nil { return renderItemError("Invalid URL") } switch u.Path { case "": return renderItemHome() case "owner": return renderItemOwner(iURL, address(u.Query().Get("address"))) default: return renderItemError("Not found") } } func renderItemHome() string { return `# AKKADIA Item Look up fungible items held by an address. <gno-form path="owner"> <gno-input name="address" placeholder="Enter the address" /> </gno-form> ` } func renderItemOwner(iURL string, owner address) string { if !owner.IsValid() { return renderItemError("Invalid address") } output := `# Item Holdings [← Home](./item:) * **Address**: ` + owner.String() + ` ` total := GetOwnedCount(owner) output += "* **Total**: " + strconv.Itoa(total) + "\n\n" if total == 0 { return output + `*No items held.* ` } pageSize := 20 if pageSize > listLimit { pageSize = listLimit } pages, err := pager.New(iURL, total, pager.WithPageSize(pageSize)) if err != nil { return renderItemError("Invalid page") } rows := ListByOwner(owner, pages.Page(), pages.PageSize()) for _, row := range rows { output += "* **" + escapeItemMarkdown(row["tokenId"]) + "**: " + escapeItemMarkdown(row["amount"]) + "\n" } if pages.HasPages() { output += "\n" + pager.Picker(pages) } return output } func renderItemError(message string) string { return `# Error > ` + escapeItemMarkdown(message) + ` [← Home](./item:) ` } func escapeItemMarkdown(value string) string { value = strings.ReplaceAll(value, "\\", "\\\\") value = strings.ReplaceAll(value, "\n", "\\n") value = strings.ReplaceAll(value, "`", "\\`") value = strings.ReplaceAll(value, "*", "\\*") value = strings.ReplaceAll(value, "_", "\\_") value = strings.ReplaceAll(value, "{", "\\{") value = strings.ReplaceAll(value, "}", "\\}") value = strings.ReplaceAll(value, "[", "\\[") value = strings.ReplaceAll(value, "]", "\\]") value = strings.ReplaceAll(value, "(", "\\(") value = strings.ReplaceAll(value, ")", "\\)") value = strings.ReplaceAll(value, "#", "\\#") value = strings.ReplaceAll(value, "+", "\\+") value = strings.ReplaceAll(value, "-", "\\-") value = strings.ReplaceAll(value, ".", "\\.") value = strings.ReplaceAll(value, "!", "\\!") value = strings.ReplaceAll(value, "|", "\\|") value = strings.ReplaceAll(value, ">", "\\>") return value }
- #12render_test.gno
- #13package item import ( "strings" "testing" "gno.land/p/nt/uassert/v0" ) func TestRenderHomeAndOwner(cur realm, t *testing.T) { setupItemTest(cross(cur), t) home := Render("") uassert.True(t, strings.Contains(home, `<gno-form path="owner">`), "should render the owner lookup form") uassert.True(t, strings.Contains(home, `name="address"`), "should render the address input") empty := Render("owner?address=" + itemTest.Owner.String()) uassert.True(t, strings.Contains(empty, itemTest.Owner.String()), "should render the owner address") uassert.True(t, strings.Contains(empty, "Total**: 0"), "should render an empty total") uassert.True(t, strings.Contains(empty, "No items held"), "should render an empty holding list") testing.SetOriginCaller(itemTest.Operator) testing.SetRealm(testing.NewUserRealm(itemTest.Operator)) SafeMint(cross(cur), itemTest.Owner, "test-iron", 3) SafeMint(cross(cur), itemTest.Owner, "**x**", 2) listed := Render("owner?address=" + itemTest.Owner.String()) uassert.True(t, strings.Contains(listed, "Total**: 2"), "should render the owned token count") uassert.True(t, strings.Contains(listed, "test\\-iron"), "should escape item token ids") uassert.True(t, strings.Contains(listed, "**: 3"), "should render item amounts") uassert.True(t, strings.Contains(listed, `\*\*x\*\*`), "escaped markdown token id should be visible") uassert.False(t, strings.Contains(listed, "**x**"), "raw markdown token id should not remain") } func TestRenderOwnerPagesWhenCountExceedsLimit(cur realm, t *testing.T) { setupItemTest(cross(cur), t) testing.SetOriginCaller(itemTest.Admin) testing.SetRealm(testing.NewUserRealm(itemTest.Admin)) SetListLimit(cross(cur), 1) testing.SetOriginCaller(itemTest.Operator) testing.SetRealm(testing.NewUserRealm(itemTest.Operator)) SafeMint(cross(cur), itemTest.Owner, "10", 1) SafeMint(cross(cur), itemTest.Owner, "20", 1) page1 := Render("owner?address=" + itemTest.Owner.String()) uassert.True(t, strings.Contains(page1, "Total**: 2"), "should render total holdings") uassert.True(t, strings.Contains(page1, "**10**"), "page 1 should include the first token") uassert.False(t, strings.Contains(page1, "**20**"), "page 1 should not include the second token") uassert.True(t, strings.Contains(page1, "page 1 of 2"), "should render a page picker") uassert.True(t, strings.Contains(page1, "page=2"), "should link to the next page") page2 := Render("owner?address=" + itemTest.Owner.String() + "&page=2") uassert.True(t, strings.Contains(page2, "**20**"), "page 2 should include the second token") uassert.False(t, strings.Contains(page2, "**10**"), "page 2 should not include the first token") uassert.True(t, strings.Contains(page2, "page 2 of 2"), "should render the last page picker") invalidPage := Render("owner?address=" + itemTest.Owner.String() + "&page=3") uassert.True(t, strings.Contains(invalidPage, "Invalid page"), "should reject a page past the last page") } func TestRenderErrors(cur realm, t *testing.T) { setupItemTest(cross(cur), t) invalid := Render("owner?address=invalid-address") uassert.True(t, strings.Contains(invalid, "Invalid address"), "should reject an invalid address") unknown := Render("unknown") uassert.True(t, strings.Contains(unknown, "Not found"), "should reject an unknown route") } func TestRenderRejectsOversizedURL(cur realm, t *testing.T) { uassert.PanicsWithMessage(t, cur, "url exceeds max runes", func() { Render(strings.Repeat("a", renderURLMaxRunes+1)) }) Render(strings.Repeat("a", renderURLMaxRunes)) }
- #14validation.gno
- #15package item import "unicode/utf8" const renderURLMaxRunes = 1024 func assertRenderURLLen(value string) { if utf8.RuneCountInString(value) > renderURLMaxRunes { panic("url exceeds max runes") } }
- #16/gno.MemPackageType
- #17 MPUserAll
Result log
msg:0,success:true,log:,events:[]