Transaction

F4716FDFE4F2A7…F0E3E895E9C4

Block 747,758 · index 0 · indexed

Summary

Hash
F4716FDFE4F2A7683CE5DBAA11337D0144E067F61C977BADAD0DF0E3E895E9C4
Block
747,758
Size
39496 bytes
Gas used
55,891,700 / 111,783,320
Fee
111784 ugnot
Status
success

Messages

#1AddPackagegno.land/r/akkadia/beta/equipment17 arguments

Arguments · 17

  1. #1equipment
  2. #2config.gno
  3. #3package equipment 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) }
  4. #4equipment.gno
  5. #5package equipment import ( "chain/runtime/unsafe" "net/url" "strconv" "time" "gno.land/p/akkadia/beta/accesscontrol" "gno.land/p/akkadia/beta/grc721" "gno.land/p/nt/bptree/v0" "gno.land/r/akkadia/beta/admin" ) const ( definitionIDKey = "definitionId" tokenIDKey = "tokenId" createdAtKey = "created_at" maxEquipmentTokenID uint64 = 18446744073709551615 ) var ( equipmentToken = grc721.NewBasicNFT("Akkadia Equipment", "AEQP") equipmentMetadata *bptree.BPTree ownerTokenIDs *bptree.BPTree latestTokenID uint64 ) func init() { resetEquipmentState() } func resetEquipmentState() { equipmentToken = grc721.NewBasicNFT("Akkadia Equipment", "AEQP") equipmentMetadata = bptree.NewBPTree32() ownerTokenIDs = bptree.NewBPTree32() minterRealms = bptree.NewBPTree32() listLimit = 100 latestTokenID = 0 } func Mint(cur realm, recipient address, definitionID uint32, attrKeys, attrValues string) uint64 { accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertEquipmentNoCoins() return mintEquipment(cross(cur), recipient, definitionID, attrKeys, attrValues) } func MintFromRealm(cur realm, recipient address, definitionID uint32, attrKeys, attrValues string) uint64 { mustGetMinterRealmCaller(0, cur) if unsafe.OriginCaller() != recipient { panic("equipment recipient must be origin caller") } return mintEquipment(cross(cur), recipient, definitionID, attrKeys, attrValues) } func GetMetadata(tokenID uint64) map[string]string { return copyStringMap(mustGetEquipmentMetadata(tokenID)) } func SetMetadata(cur realm, tokenID uint64, attrKeys, attrValues string) { accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator) assertEquipmentNoCoins() setEquipmentMetadata(tokenID, attrKeys, attrValues) } func SetMetadataFromRealm(cur realm, tokenID uint64, attrKeys, attrValues string) { mustGetMinterRealmCaller(0, cur) if unsafe.OriginCaller() != OwnerOf(tokenID) { panic("equipment owner must be origin caller") } setEquipmentMetadata(tokenID, attrKeys, attrValues) } func GetOwnedCount(owner address) int { if !owner.IsValid() { panic("invalid equipment 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 equipment owner") } assertEquipmentListPageCount(page, count) ids, found := getOwnerTokenIDs(owner) if !found { return []map[string]string{} } result := []map[string]string{} ids.IterateByOffset((page-1)*count, count, func(key string, _ any) bool { tokenID := parseUint64Key(key) row := GetMetadata(tokenID) row[tokenIDKey] = strconv.FormatUint(tokenID, 10) result = append(result, row) return false }) return result } func Name() string { return equipmentToken.Name() } func Symbol() string { return equipmentToken.Symbol() } func OwnerOf(tokenID uint64) address { owner, err := equipmentToken.OwnerOf(equipmentTokenID(tokenID)) if err != nil { panic("equipment owner query failed: " + err.Error()) } return owner } func Has(tokenID uint64) bool { _, err := equipmentToken.OwnerOf(equipmentTokenID(tokenID)) return err == nil } func GetLatestTokenID() uint64 { return latestTokenID } func BalanceOf(owner address) int64 { if !owner.IsValid() { panic("invalid equipment owner") } balance, err := equipmentToken.BalanceOf(owner) if err != nil { panic("equipment balance query failed: " + err.Error()) } return balance } func GetApproved(tokenID uint64) address { approved, err := equipmentToken.GetApproved(equipmentTokenID(tokenID)) if err != nil { panic("equipment approval query failed: " + err.Error()) } return approved } func IsApprovedForAll(owner, operator address) bool { return equipmentToken.IsApprovedForAll(owner, operator) } func Approve(cur realm, operator address, tokenID uint64) { caller := accesscontrol.MustGetUserCaller(0, cur) assertEquipmentNoCoins() if err := equipmentToken.Approve(caller, operator, equipmentTokenID(tokenID)); err != nil { panic("equipment approval failed: " + err.Error()) } } func SetApprovalForAll(cur realm, operator address, approved bool) { caller := accesscontrol.MustGetUserCaller(0, cur) assertEquipmentNoCoins() if err := equipmentToken.SetApprovalForAll(caller, operator, approved); err != nil { panic("equipment operator approval failed: " + err.Error()) } } func TransferFrom(cur realm, from, to address, tokenID uint64) { caller := accesscontrol.MustGetUserCaller(0, cur) assertEquipmentNoCoins() if err := equipmentToken.TransferFrom(caller, from, to, equipmentTokenID(tokenID)); err != nil { panic("equipment transfer failed: " + err.Error()) } if from != to { removeOwnerTokenID(from, tokenID) addOwnerTokenID(to, tokenID) } } func Burn(cur realm, tokenID uint64) { caller := accesscontrol.MustGetUserCaller(0, cur) assertEquipmentNoCoins() owner := OwnerOf(tokenID) approved, err := equipmentToken.GetApproved(equipmentTokenID(tokenID)) if caller != owner && !equipmentToken.IsApprovedForAll(owner, caller) && (err != nil || approved != caller) { panic("equipment owner or approved operator required") } burnEquipment(tokenID, owner) } func BurnFromRealm(cur realm, tokenID uint64) { mustGetMinterRealmCaller(0, cur) owner := OwnerOf(tokenID) if unsafe.OriginCaller() != owner { panic("equipment owner must be origin caller") } burnEquipment(tokenID, owner) } func mintEquipment(cur realm, recipient address, definitionID uint32, attrKeys, attrValues string) uint64 { if !recipient.IsValid() || recipient == cur.Address() { panic("invalid equipment recipient") } attrs := parseEquipmentAttributes(attrKeys, attrValues) tokenID := allocateTokenID() if err := equipmentToken.Mint(recipient, equipmentTokenID(tokenID)); err != nil { panic("equipment mint failed: " + err.Error()) } attrs[definitionIDKey] = strconv.FormatUint(uint64(definitionID), 10) attrs[createdAtKey] = strconv.FormatInt(time.Now().Unix(), 10) equipmentMetadata.Set(uint64Key(tokenID), attrs) addOwnerTokenID(recipient, tokenID) return tokenID } func allocateTokenID() uint64 { if latestTokenID == maxEquipmentTokenID { panic("equipment token id exhausted") } latestTokenID++ return latestTokenID } func mustGetEquipmentMetadata(tokenID uint64) map[string]string { value := equipmentMetadata.Get(uint64Key(tokenID)) if value == nil { panic("equipment metadata not found: " + strconv.FormatUint(tokenID, 10)) } return value.(map[string]string) } func setEquipmentMetadata(tokenID uint64, attrKeys, attrValues string) { current := mustGetEquipmentMetadata(tokenID) attrs := parseEquipmentAttributes(attrKeys, attrValues) attrs[definitionIDKey] = current[definitionIDKey] attrs[createdAtKey] = current[createdAtKey] equipmentMetadata.Set(uint64Key(tokenID), attrs) } func parseEquipmentAttributes(attrKeys, attrValues string) map[string]string { attrs := parseAttributesCSV(attrKeys, attrValues) if _, found := attrs[definitionIDKey]; found { panic("reserved key: " + definitionIDKey) } if _, found := attrs[tokenIDKey]; found { panic("reserved key: " + tokenIDKey) } if _, found := attrs[createdAtKey]; found { panic("reserved key: " + createdAtKey) } return attrs } func burnEquipment(tokenID uint64, owner address) { if err := equipmentToken.Burn(equipmentTokenID(tokenID)); err != nil { panic("equipment burn failed: " + err.Error()) } if _, removed := equipmentMetadata.Remove(uint64Key(tokenID)); !removed { panic("equipment metadata not found: " + strconv.FormatUint(tokenID, 10)) } removeOwnerTokenID(owner, 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 assertEquipmentNoCoins() { if len(unsafe.OriginSend()) != 0 { panic("attached coins not allowed") } } func assertEquipmentListPageCount(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 uint64) { 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(uint64Key(tokenID), true) } func removeOwnerTokenID(owner address, tokenID uint64) { ids, found := getOwnerTokenIDs(owner) if !found { return } ids.Remove(uint64Key(tokenID)) } func getOwnerTokenIDs(owner address) (*bptree.BPTree, bool) { value := ownerTokenIDs.Get(owner.String()) if value == nil { return nil, false } return value.(*bptree.BPTree), true } func equipmentTokenID(tokenID uint64) grc721.TokenID { return grc721.TokenID(strconv.FormatUint(tokenID, 10)) } func uint64Key(id uint64) string { s := strconv.FormatUint(id, 10) if len(s) >= 20 { return s } return "00000000000000000000"[len(s):] + s } func parseUint64Key(key string) uint64 { id, err := strconv.ParseUint(key, 10, 64) if err != nil { panic("invalid uint64 key: " + key) } return id } func copyStringMap(source map[string]string) map[string]string { result := map[string]string{} for key, value := range source { result[key] = value } return result } // parseAttributesCSV accepts comma-separated keys and URL-encoded values. // Empty keys and values produce an empty attribute map so mint can store // definitionId and created_at alone. func parseAttributesCSV(keys, values string) map[string]string { if keys == "" && values == "" { return map[string]string{} } result := map[string]string{} keyStart, valStart := 0, 0 keyIdx, valIdx := 0, 0 for { for keyIdx < len(keys) && keys[keyIdx] != ',' { keyIdx++ } for valIdx < len(values) && values[valIdx] != ',' { valIdx++ } key := keys[keyStart:keyIdx] if key == "" { panic("attributes key must be not empty") } if _, found := result[key]; found { panic("attributes key duplicated: " + key) } value, err := url.PathUnescape(values[valStart:valIdx]) if err != nil { panic("attributes value URL decode failed: " + key) } result[key] = value keyEnd := keyIdx >= len(keys) valEnd := valIdx >= len(values) if keyEnd != valEnd { panic("attributes keys and values count mismatch") } if keyEnd { break } keyIdx++ valIdx++ keyStart = keyIdx valStart = valIdx } return result }
  6. #6equipment_test.gno
  7. #7package equipment import ( "chain" "testing" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" "gno.land/r/akkadia/beta/admin" ) var equipmentTest = struct { Admin, Operator, Owner, Other address SetupDone bool }{ Admin: testutils.TestAddress("equipmentAdmin"), Operator: testutils.TestAddress("equipmentOperator"), Owner: testutils.TestAddress("equipmentOwner"), Other: testutils.TestAddress("equipmentOther"), } func setupEquipmentTest(cur realm, t *testing.T) { t.Helper() resetEquipmentState() testing.SetOriginSend(chain.Coins{}) if equipmentTest.SetupDone { return } originalAdmin := admin.GetAdmin() testing.SetOriginCaller(originalAdmin) testing.SetRealm(testing.NewUserRealm(originalAdmin)) admin.TransferAdmin(cross(cur), equipmentTest.Admin) testing.SetOriginCaller(equipmentTest.Admin) testing.SetRealm(testing.NewUserRealm(equipmentTest.Admin)) admin.AcceptAdmin(cross(cur)) admin.SetOperator(cross(cur), equipmentTest.Operator) equipmentTest.SetupDone = true } func TestMintAllocatesSequentialTokenIDs(cur realm, t *testing.T) { setupEquipmentTest(cross(cur), t) testing.SetOriginCaller(equipmentTest.Operator) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) uassert.Equal(t, uint64(0), GetLatestTokenID()) first := Mint(cross(cur), equipmentTest.Owner, 0, "", "") second := Mint(cross(cur), equipmentTest.Other, 0, "", "") uassert.Equal(t, uint64(1), first) uassert.Equal(t, uint64(2), second) uassert.Equal(t, uint64(2), GetLatestTokenID()) uassert.Equal(t, equipmentTest.Owner, OwnerOf(1)) uassert.Equal(t, equipmentTest.Other, OwnerOf(2)) uassert.True(t, Has(1)) uassert.True(t, Has(2)) uassert.False(t, Has(3)) latestTokenID = maxEquipmentTokenID uassert.AbortsWithMessage(t, cur, "equipment token id exhausted", func() { Mint(cross(cur), equipmentTest.Owner, 0, "", "") }) } func TestEquipmentTransferAndBurn(cur realm, t *testing.T) { setupEquipmentTest(cross(cur), t) testing.SetOriginCaller(equipmentTest.Operator) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) Mint(cross(cur), equipmentTest.Owner, 0, "", "") testing.SetOriginCaller(equipmentTest.Owner) testing.SetRealm(testing.NewUserRealm(equipmentTest.Owner)) TransferFrom(cross(cur), equipmentTest.Owner, equipmentTest.Other, 1) testing.SetOriginCaller(equipmentTest.Other) testing.SetRealm(testing.NewUserRealm(equipmentTest.Other)) Burn(cross(cur), 1) uassert.PanicsWithMessage(t, cur, "equipment metadata not found: 1", func() { GetMetadata(1) }) testing.SetOriginCaller(equipmentTest.Operator) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) Mint(cross(cur), equipmentTest.Owner, 1101000, "enhancement", "3") uassert.Equal(t, uint64(2), GetLatestTokenID()) uassert.Equal(t, equipmentTest.Owner, OwnerOf(2)) meta := GetMetadata(2) uassert.Equal(t, "1101000", meta["definitionId"]) uassert.Equal(t, "3", meta["enhancement"]) uassert.True(t, meta[createdAtKey] != "") } func TestEquipmentApprovalAllowsTransferAndBurn(cur realm, t *testing.T) { setupEquipmentTest(cross(cur), t) testing.SetOriginCaller(equipmentTest.Operator) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) Mint(cross(cur), equipmentTest.Owner, 0, "", "") Mint(cross(cur), equipmentTest.Owner, 0, "", "") testing.SetOriginCaller(equipmentTest.Owner) testing.SetRealm(testing.NewUserRealm(equipmentTest.Owner)) Approve(cross(cur), equipmentTest.Other, 1) SetApprovalForAll(cross(cur), equipmentTest.Other, true) testing.SetOriginCaller(equipmentTest.Other) testing.SetRealm(testing.NewUserRealm(equipmentTest.Other)) TransferFrom(cross(cur), equipmentTest.Owner, equipmentTest.Admin, 1) Burn(cross(cur), 2) uassert.Equal(t, equipmentTest.Admin, OwnerOf(1)) uassert.Equal(t, int64(0), BalanceOf(equipmentTest.Owner)) } func TestMintSecurity(cur realm, t *testing.T) { setupEquipmentTest(cross(cur), t) testing.SetOriginCaller(equipmentTest.Owner) testing.SetRealm(testing.NewUserRealm(equipmentTest.Owner)) uassert.AbortsWithMessage(t, cur, "admin or operator access required", func() { Mint(cross(cur), equipmentTest.Owner, 0, "", "") }) testing.SetOriginCaller(equipmentTest.Operator) testing.SetRealm(testing.NewCodeRealm("gno.land/r/example/equipment_relay")) uassert.AbortsWithMessage(t, cur, "admin or operator access required", func() { Mint(cross(cur), equipmentTest.Owner, 0, "", "") }) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) testing.SetOriginSend(chain.Coins{{"ugnot", 1}}) uassert.AbortsWithMessage(t, cur, "attached coins not allowed", func() { Mint(cross(cur), equipmentTest.Owner, 0, "", "") }) } func TestEquipmentMetadata(cur realm, t *testing.T) { setupEquipmentTest(cross(cur), t) testing.SetOriginCaller(equipmentTest.Operator) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) Mint(cross(cur), equipmentTest.Owner, 1101000, "enhancement,note", "3,a%2Cb") meta := GetMetadata(1) uassert.Equal(t, "1101000", meta["definitionId"]) uassert.Equal(t, "3", meta["enhancement"]) uassert.Equal(t, "a,b", meta["note"]) uassert.True(t, meta[createdAtKey] != "") uassert.Equal(t, 4, len(meta)) meta["definitionId"] = "hacked" uassert.Equal(t, "1101000", GetMetadata(1)["definitionId"]) cases := []struct { keys, values, panicMsg string }{ {"definitionId", "1", "reserved key: definitionId"}, {"tokenId", "1", "reserved key: tokenId"}, {"created_at", "1", "reserved key: created_at"}, {"a,a", "1,2", "attributes key duplicated: a"}, {"a,b", "1", "attributes keys and values count mismatch"}, {",a", "1,2", "attributes key must be not empty"}, } for _, tc := range cases { keys, values, panicMsg := tc.keys, tc.values, tc.panicMsg uassert.AbortsWithMessage(t, cur, panicMsg, func() { Mint(cross(cur), equipmentTest.Owner, 1, keys, values) }) } uassert.Equal(t, int64(1), BalanceOf(equipmentTest.Owner)) uassert.PanicsWithMessage(t, cur, "equipment metadata not found: 2", func() { GetMetadata(2) }) } func TestEquipmentOwnedList(cur realm, t *testing.T) { setupEquipmentTest(cross(cur), t) testing.SetOriginCaller(equipmentTest.Operator) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) uassert.Equal(t, 0, GetOwnedCount(equipmentTest.Owner)) uassert.Equal(t, 0, len(ListByOwner(equipmentTest.Owner, 1, 10))) uassert.PanicsWithMessage(t, cur, "invalid equipment owner", func() { GetOwnedCount(address("")) }) uassert.PanicsWithMessage(t, cur, "page must be at least 1", func() { ListByOwner(equipmentTest.Owner, 0, 1) }) uassert.PanicsWithMessage(t, cur, "count must be at least 1", func() { ListByOwner(equipmentTest.Owner, 1, 0) }) uassert.PanicsWithMessage(t, cur, "count exceeds listLimit (max: 100)", func() { ListByOwner(equipmentTest.Owner, 1, 101) }) uassert.Equal(t, 100, GetListLimit()) Mint(cross(cur), equipmentTest.Owner, 1101000, "enhancement", "3") Mint(cross(cur), equipmentTest.Owner, 1101001, "", "") Mint(cross(cur), equipmentTest.Owner, 1101002, "", "") uassert.Equal(t, 3, GetOwnedCount(equipmentTest.Owner)) uassert.Equal(t, int64(3), BalanceOf(equipmentTest.Owner)) page1 := ListByOwner(equipmentTest.Owner, 1, 2) uassert.Equal(t, 2, len(page1)) uassert.Equal(t, "1", page1[0]["tokenId"]) uassert.Equal(t, "1101000", page1[0]["definitionId"]) uassert.Equal(t, "3", page1[0]["enhancement"]) uassert.Equal(t, "2", page1[1]["tokenId"]) page2 := ListByOwner(equipmentTest.Owner, 2, 2) uassert.Equal(t, 1, len(page2)) uassert.Equal(t, "3", page2[0]["tokenId"]) uassert.Equal(t, 0, len(ListByOwner(equipmentTest.Owner, 3, 2))) page1[0]["definitionId"] = "hacked" uassert.Equal(t, "1101000", ListByOwner(equipmentTest.Owner, 1, 1)[0]["definitionId"]) testing.SetOriginCaller(equipmentTest.Owner) testing.SetRealm(testing.NewUserRealm(equipmentTest.Owner)) TransferFrom(cross(cur), equipmentTest.Owner, equipmentTest.Other, 1) uassert.Equal(t, 2, GetOwnedCount(equipmentTest.Owner)) uassert.Equal(t, 1, GetOwnedCount(equipmentTest.Other)) uassert.Equal(t, "1", ListByOwner(equipmentTest.Other, 1, 10)[0]["tokenId"]) testing.SetOriginCaller(equipmentTest.Other) testing.SetRealm(testing.NewUserRealm(equipmentTest.Other)) Burn(cross(cur), 1) uassert.Equal(t, 0, GetOwnedCount(equipmentTest.Other)) uassert.Equal(t, 2, GetOwnedCount(equipmentTest.Owner)) testing.SetOriginCaller(equipmentTest.Operator) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) Mint(cross(cur), equipmentTest.Admin, 1101999, "enhancement", "9") uassert.Equal(t, uint64(4), GetLatestTokenID()) uassert.Equal(t, 1, GetOwnedCount(equipmentTest.Admin)) uassert.Equal(t, "4", ListByOwner(equipmentTest.Admin, 1, 1)[0]["tokenId"]) uassert.Equal(t, "1101999", ListByOwner(equipmentTest.Admin, 1, 1)[0]["definitionId"]) Mint(cross(cur), equipmentTest.Other, 1, "", "") uassert.Equal(t, uint64(5), GetLatestTokenID()) uassert.Equal(t, 1, GetOwnedCount(equipmentTest.Other)) } func TestSetListLimit(cur realm, t *testing.T) { setupEquipmentTest(cross(cur), t) testing.SetOriginCaller(equipmentTest.Admin) testing.SetRealm(testing.NewUserRealm(equipmentTest.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(equipmentTest.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(equipmentTest.Operator) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetListLimit(cross(cur), 50) }) testing.SetOriginCaller(equipmentTest.Owner) testing.SetRealm(testing.NewUserRealm(equipmentTest.Owner)) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetListLimit(cross(cur), 50) }) testing.SetOriginCaller(equipmentTest.Admin) testing.SetRealm(testing.NewCodeRealm("gno.land/r/example/equipment_admin_relay")) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetListLimit(cross(cur), 50) }) } func TestSetMinterRealm(cur realm, t *testing.T) { setupEquipmentTest(cross(cur), t) minterPath := "gno.land/r/example/equipment_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(equipmentTest.Operator) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetMinterRealm(cross(cur), minterPath) }) testing.SetOriginCaller(equipmentTest.Admin) testing.SetRealm(testing.NewUserRealm(equipmentTest.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/equipment") }) 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/equipment_admin_relay")) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetMinterRealm(cross(cur), minterPath) }) } func TestMintFromRealm(cur realm, t *testing.T) { setupEquipmentTest(cross(cur), t) minter := testing.NewCodeRealm("gno.land/r/example/equipment_minter") unregistered := testing.NewCodeRealm("gno.land/r/example/equipment_minter_other") testing.SetOriginCaller(equipmentTest.Owner) testing.SetRealm(testing.NewUserRealm(equipmentTest.Owner)) uassert.AbortsWithMessage(t, cur, "user calls not allowed: function must be called by a realm", func() { MintFromRealm(cross(cur), equipmentTest.Owner, 0, "", "") }) testing.SetRealm(unregistered) uassert.AbortsWithMessage(t, cur, "minter realm required", func() { MintFromRealm(cross(cur), equipmentTest.Owner, 0, "", "") }) testing.SetOriginCaller(equipmentTest.Admin) testing.SetRealm(testing.NewUserRealm(equipmentTest.Admin)) SetMinterRealm(cross(cur), "gno.land/r/example/equipment_minter") testing.SetOriginCaller(equipmentTest.Owner) testing.SetRealm(minter) MintFromRealm(cross(cur), equipmentTest.Owner, 1101000, "enhancement", "3") uassert.Equal(t, uint64(1), GetLatestTokenID()) uassert.Equal(t, equipmentTest.Owner, OwnerOf(1)) uassert.Equal(t, "1101000", GetMetadata(1)["definitionId"]) uassert.Equal(t, "3", GetMetadata(1)["enhancement"]) uassert.True(t, GetMetadata(1)[createdAtKey] != "") uassert.Equal(t, 1, GetOwnedCount(equipmentTest.Owner)) uassert.AbortsWithMessage(t, cur, "equipment recipient must be origin caller", func() { MintFromRealm(cross(cur), equipmentTest.Other, 0, "", "") }) uassert.Equal(t, int64(1), BalanceOf(equipmentTest.Owner)) testing.SetOriginSend(chain.Coins{{"ugnot", 1}}) MintFromRealm(cross(cur), equipmentTest.Owner, 1101001, "", "") testing.SetOriginSend(chain.Coins{}) uassert.Equal(t, uint64(2), GetLatestTokenID()) uassert.Equal(t, equipmentTest.Owner, OwnerOf(2)) } func TestBurnFromRealm(cur realm, t *testing.T) { setupEquipmentTest(cross(cur), t) minter := testing.NewCodeRealm("gno.land/r/example/equipment_minter") testing.SetOriginCaller(equipmentTest.Operator) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) Mint(cross(cur), equipmentTest.Owner, 0, "", "") Mint(cross(cur), equipmentTest.Other, 0, "", "") testing.SetOriginCaller(equipmentTest.Admin) testing.SetRealm(testing.NewUserRealm(equipmentTest.Admin)) SetMinterRealm(cross(cur), "gno.land/r/example/equipment_minter") testing.SetOriginCaller(equipmentTest.Owner) testing.SetRealm(testing.NewUserRealm(equipmentTest.Owner)) uassert.AbortsWithMessage(t, cur, "user calls not allowed: function must be called by a realm", func() { BurnFromRealm(cross(cur), 1) }) testing.SetRealm(minter) uassert.AbortsWithMessage(t, cur, "equipment owner must be origin caller", func() { BurnFromRealm(cross(cur), 2) }) uassert.Equal(t, equipmentTest.Other, OwnerOf(2)) BurnFromRealm(cross(cur), 1) uassert.Equal(t, int64(0), BalanceOf(equipmentTest.Owner)) uassert.PanicsWithMessage(t, cur, "equipment metadata not found: 1", func() { GetMetadata(1) }) testing.SetOriginCaller(equipmentTest.Operator) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) Mint(cross(cur), equipmentTest.Owner, 1101000, "", "") testing.SetOriginCaller(equipmentTest.Owner) testing.SetRealm(minter) uassert.AbortsWithMessage(t, cur, "reserved key: definitionId", func() { MintFromRealm(cross(cur), equipmentTest.Owner, 1, "definitionId", "1") }) uassert.Equal(t, equipmentTest.Owner, OwnerOf(3)) uassert.Equal(t, 1, GetOwnedCount(equipmentTest.Owner)) uassert.PanicsWithMessage(t, cur, "equipment metadata not found: 4", func() { GetMetadata(4) }) testing.SetOriginCaller(equipmentTest.Admin) testing.SetRealm(testing.NewUserRealm(equipmentTest.Admin)) UnsetMinterRealm(cross(cur), "gno.land/r/example/equipment_minter") testing.SetOriginCaller(equipmentTest.Owner) testing.SetRealm(minter) uassert.AbortsWithMessage(t, cur, "minter realm required", func() { BurnFromRealm(cross(cur), 3) }) uassert.Equal(t, equipmentTest.Owner, OwnerOf(3)) } func TestSetMetadata(cur realm, t *testing.T) { setupEquipmentTest(cross(cur), t) testing.SetOriginCaller(equipmentTest.Operator) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) Mint(cross(cur), equipmentTest.Owner, 1101000, "enhancement,note", "3,a%2Cb") createdAt := GetMetadata(1)[createdAtKey] uassert.True(t, createdAt != "") SetMetadata(cross(cur), 1, "enhancement", "4") meta := GetMetadata(1) uassert.Equal(t, "1101000", meta["definitionId"]) uassert.Equal(t, "4", meta["enhancement"]) uassert.Equal(t, createdAt, meta[createdAtKey]) uassert.Equal(t, 3, len(meta)) uassert.Equal(t, "4", ListByOwner(equipmentTest.Owner, 1, 1)[0]["enhancement"]) meta["enhancement"] = "hacked" uassert.Equal(t, "4", GetMetadata(1)["enhancement"]) uassert.AbortsWithMessage(t, cur, "reserved key: definitionId", func() { SetMetadata(cross(cur), 1, "definitionId", "9") }) uassert.AbortsWithMessage(t, cur, "reserved key: created_at", func() { SetMetadata(cross(cur), 1, "created_at", "1") }) uassert.AbortsWithMessage(t, cur, "equipment metadata not found: 2", func() { SetMetadata(cross(cur), 2, "enhancement", "1") }) uassert.Equal(t, "1101000", GetMetadata(1)["definitionId"]) testing.SetOriginCaller(equipmentTest.Owner) testing.SetRealm(testing.NewUserRealm(equipmentTest.Owner)) uassert.AbortsWithMessage(t, cur, "admin or operator access required", func() { SetMetadata(cross(cur), 1, "enhancement", "5") }) testing.SetOriginCaller(equipmentTest.Operator) testing.SetRealm(testing.NewCodeRealm("gno.land/r/example/equipment_relay")) uassert.AbortsWithMessage(t, cur, "admin or operator access required", func() { SetMetadata(cross(cur), 1, "enhancement", "5") }) } func TestSetMetadataFromRealm(cur realm, t *testing.T) { setupEquipmentTest(cross(cur), t) minterPath := "gno.land/r/example/equipment_minter" minter := testing.NewCodeRealm(minterPath) testing.SetOriginCaller(equipmentTest.Operator) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) Mint(cross(cur), equipmentTest.Owner, 1101000, "enhancement", "3") Mint(cross(cur), equipmentTest.Other, 1101001, "enhancement", "1") testing.SetOriginCaller(equipmentTest.Owner) testing.SetRealm(testing.NewUserRealm(equipmentTest.Owner)) uassert.AbortsWithMessage(t, cur, "user calls not allowed: function must be called by a realm", func() { SetMetadataFromRealm(cross(cur), 1, "enhancement", "4") }) testing.SetRealm(minter) uassert.AbortsWithMessage(t, cur, "minter realm required", func() { SetMetadataFromRealm(cross(cur), 1, "enhancement", "4") }) testing.SetOriginCaller(equipmentTest.Admin) testing.SetRealm(testing.NewUserRealm(equipmentTest.Admin)) SetMinterRealm(cross(cur), minterPath) testing.SetOriginCaller(equipmentTest.Owner) testing.SetRealm(minter) uassert.AbortsWithMessage(t, cur, "equipment owner must be origin caller", func() { SetMetadataFromRealm(cross(cur), 2, "enhancement", "9") }) uassert.Equal(t, "1", GetMetadata(2)["enhancement"]) createdAt := GetMetadata(1)[createdAtKey] SetMetadataFromRealm(cross(cur), 1, "enhancement", "4") uassert.Equal(t, "1101000", GetMetadata(1)["definitionId"]) uassert.Equal(t, "4", GetMetadata(1)["enhancement"]) uassert.Equal(t, createdAt, GetMetadata(1)[createdAtKey]) testing.SetOriginSend(chain.Coins{{"ugnot", 1}}) SetMetadataFromRealm(cross(cur), 1, "enhancement", "5") testing.SetOriginSend(chain.Coins{}) uassert.Equal(t, "5", GetMetadata(1)["enhancement"]) }
  8. #8gnomod.toml
  9. #9module = "gno.land/r/akkadia/beta/equipment" gno = "0.9"
  10. #10render.gno
  11. #11package equipment 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 renderEquipmentError("Invalid URL") } switch u.Path { case "": return renderEquipmentHome() case "owner": return renderEquipmentOwner(iURL, address(u.Query().Get("address"))) default: return renderEquipmentError("Not found") } } func renderEquipmentHome() string { return `# AKKADIA Equipment Look up equipment held by an address. <gno-form path="owner"> <gno-input name="address" placeholder="Enter the address" /> </gno-form> ` } func renderEquipmentOwner(iURL string, owner address) string { if !owner.IsValid() { return renderEquipmentError("Invalid address") } output := `# Equipment Holdings [← Home](./equipment:) * **Address**: ` + owner.String() + ` ` total := GetOwnedCount(owner) output += "* **Total**: " + strconv.Itoa(total) + "\n\n" if total == 0 { return output + `*No equipment held.* ` } pageSize := 20 if pageSize > listLimit { pageSize = listLimit } pages, err := pager.New(iURL, total, pager.WithPageSize(pageSize)) if err != nil { return renderEquipmentError("Invalid page") } rows := ListByOwner(owner, pages.Page(), pages.PageSize()) for _, row := range rows { output += formatEquipmentHolding(row) } if pages.HasPages() { output += "\n" + pager.Picker(pages) } return output } func formatEquipmentHolding(row map[string]string) string { line := "* **" + escapeEquipmentMarkdown(row[tokenIDKey]) + "**: definitionId " + escapeEquipmentMarkdown(row[definitionIDKey]) for key, value := range row { if key == tokenIDKey || key == definitionIDKey || key == createdAtKey { continue } line += " · " + escapeEquipmentMarkdown(key) + " " + escapeEquipmentMarkdown(value) } return line + "\n" } func renderEquipmentError(message string) string { return `# Error > ` + escapeEquipmentMarkdown(message) + ` [← Home](./equipment:) ` } func escapeEquipmentMarkdown(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 }
  12. #12render_test.gno
  13. #13package equipment import ( "net/url" "strings" "testing" "gno.land/p/nt/uassert/v0" ) func TestRenderHomeAndOwner(cur realm, t *testing.T) { setupEquipmentTest(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=" + equipmentTest.Owner.String()) uassert.True(t, strings.Contains(empty, equipmentTest.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 equipment held"), "should render an empty holding list") testing.SetOriginCaller(equipmentTest.Operator) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) Mint(cross(cur), equipmentTest.Owner, 1101000, "enhancement", "3") Mint(cross(cur), equipmentTest.Owner, 1101001, "note", url.PathEscape("**x**")) listed := Render("owner?address=" + equipmentTest.Owner.String()) uassert.True(t, strings.Contains(listed, "Total**: 2"), "should render the owned token count") uassert.True(t, strings.Contains(listed, "**1**"), "should render the first token id") uassert.True(t, strings.Contains(listed, "definitionId 1101000"), "should render the definition id") uassert.True(t, strings.Contains(listed, "enhancement 3"), "should render extra attributes") uassert.True(t, strings.Contains(listed, `\*\*x\*\*`), "should escape attribute markdown") uassert.False(t, strings.Contains(listed, "**x**"), "raw markdown attributes should not remain") } func TestRenderOwnerPagesWhenCountExceedsLimit(cur realm, t *testing.T) { setupEquipmentTest(cross(cur), t) testing.SetOriginCaller(equipmentTest.Admin) testing.SetRealm(testing.NewUserRealm(equipmentTest.Admin)) SetListLimit(cross(cur), 1) testing.SetOriginCaller(equipmentTest.Operator) testing.SetRealm(testing.NewUserRealm(equipmentTest.Operator)) Mint(cross(cur), equipmentTest.Owner, 1101000, "", "") Mint(cross(cur), equipmentTest.Owner, 1101001, "", "") page1 := Render("owner?address=" + equipmentTest.Owner.String()) uassert.True(t, strings.Contains(page1, "Total**: 2"), "should render total holdings") uassert.True(t, strings.Contains(page1, "**1**"), "page 1 should include the first token") uassert.False(t, strings.Contains(page1, "**2**"), "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=" + equipmentTest.Owner.String() + "&page=2") uassert.True(t, strings.Contains(page2, "**2**"), "page 2 should include the second token") uassert.False(t, strings.Contains(page2, "**1**"), "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=" + equipmentTest.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) { setupEquipmentTest(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)) }
  14. #14validation.gno
  15. #15package equipment import "unicode/utf8" const renderURLMaxRunes = 1024 func assertRenderURLLen(value string) { if utf8.RuneCountInString(value) > renderURLMaxRunes { panic("url exceeds max runes") } }
  16. #16/gno.MemPackageType
  17. #17 MPUserAll

Result log

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