Transaction

A12B12C655C399…C0237BAB9A6F

Block 555,742 · index 0 · indexed

Summary

Hash
A12B12C655C399448669346866C858322B6167B784EBD6A902ADC0237BAB9A6F
Block
555,742
Size
7743 bytes
Gas used
11,737,172 / 25,000,000
Fee
50000 ugnot
Status
success

Messages

Attached funds
2500000 ugnot

Arguments · 9

  1. #1goldenticket
  2. #2gnomod.toml
  3. #3module = "gno.land/r/g1sw5xklxjjuv0yvuxy5f5s3l3mnj0nqq626a9wr/goldenticket" gno = "0.9"
  4. #4goldenticket.gno
  5. #5// Package goldenticket is a 1/1 GRC721 NFT: one golden ticket, minted to the // publisher when the package is added. It exists to check that a gno.land // collection shows in the Adena wallet before gnofly's planes go to mainnet, // so it is built the way the planes are: // // - the collection is a gno.land/p/nt/grc721/v0 token made with NewToken, // whose NewToken and Transfer events Adena finds on its indexer; // - the read functions take a grc721.TokenID and return (value, error), as // Adena calls them: OwnerOf("1"), BalanceOf("g1..."), TokenURI("1") as // the <img src>, TokenMetadata("1") as JSON with camelCase traitType. // // Transfer moves the ticket, so it can reach any Adena account. package goldenticket import ( "errors" "strings" "gno.land/p/nt/grc721/v0" ) const ( tokenName = "Golden Ticket" tokenSymbol = "GOLD" ticket = grc721.TokenID("1") // An https PNG, as the planes' images are: the SuperGnome of gnofly, from // the Blob store that serves the collection. Adena loads https: and data: // images only. image = "https://xjfvwq1pdzcznycc.public.blob.vercel-storage.com/planes/777.png" errNoTicket = "goldenticket: there is one ticket, id 1" errNotOwner = "goldenticket: caller does not hold the ticket" errBadOwner = "goldenticket: invalid recipient address" ) var ( // Token is the collection, read-only: Token.OwnerOf("1"), Token.BalanceOf. Token *grc721.Token ledger *grc721.PrivateLedger ) // init runs once, when the package is added to the chain. This is the only // moment cur.Previous() is the publisher, who receives the ticket. // // Under `gno test` there is no publisher and the address is empty. The tests // call bootstrap themselves with a real address. func init(cur realm) { bootstrap(cur.Previous().Address(), cur) } // bootstrap creates the collection and mints the ticket to holder; with no // valid holder, under `gno test`, it mints nothing. func bootstrap(holder address, rlm realm) { Token, ledger = grc721.NewToken(tokenName, tokenSymbol, 0, rlm) if !holder.IsValid() { return } if err := ledger.Mint(holder, ticket); err != nil { panic(err) } } // Name and Symbol, as GRC721 tokens have them. Adena reads both from the // NewToken event. func Name() string { return tokenName } func Symbol() string { return tokenSymbol } // OwnerOf returns who holds the ticket, or an error for any other id. func OwnerOf(tid grc721.TokenID) (address, error) { if tid != ticket { return "", errors.New(errNoTicket) } return Token.OwnerOf(tid) } // BalanceOf returns 1 for the holder of the ticket, 0 for everyone else. func BalanceOf(addr address) (int64, error) { return Token.BalanceOf(addr) } // TokenURI returns the image of the ticket: what Adena shows. func TokenURI(tid grc721.TokenID) (string, error) { if _, err := OwnerOf(tid); err != nil { return "", err } return image, nil } // TokenMetadata returns the metadata of the ticket as JSON, with the keys // Adena reads. func TokenMetadata(tid grc721.TokenID) (string, error) { if _, err := OwnerOf(tid); err != nil { return "", err } return `{"name":"` + tokenName + ` #1","description":"A 1/1 on gno.land, minted to check that collections show in Adena.",` + `"image":"` + image + `","attributes":[{"traitType":"edition","value":"1 of 1"},{"traitType":"metal","value":"gold"}]}`, nil } // Transfer gives the ticket to someone else. Only its holder can, by a // direct call (maketx call or Adena), not through another realm. func Transfer(cur realm, to address) { caller := cur.Previous() if !caller.IsUserCall() { panic(errNotOwner) } if !to.IsValid() { panic(errBadOwner) } holder, err := Token.OwnerOf(ticket) if err != nil || holder != caller.Address() { panic(errNotOwner) } if err := ledger.TransferFrom(holder, holder, to, ticket); err != nil { panic(err) } } // Render shows the ticket and who holds it. func Render(_ string) string { var b strings.Builder b.WriteString("# " + tokenName + "\n\n") b.WriteString("A 1/1 GRC721 on gno.land, minted to check that collections show in Adena.\n\n") b.WriteString("![" + tokenName + "](" + image + ")\n\n") holder, err := Token.OwnerOf(ticket) if err != nil { b.WriteString("Not minted.\n") return b.String() } b.WriteString("Held by `" + holder.String() + "`.\n") return b.String() }
  6. #6goldenticket_test.gno
  7. #7package goldenticket import ( "strings" "testing" "gno.land/p/nt/grc721/v0" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) var ( aliceAddr = testutils.TestAddress("alice") bobAddr = testutils.TestAddress("bob") ) // reset redoes what init does on chain, with alice as the publisher. func reset(cur realm) { func(cur realm) { bootstrap(aliceAddr, cur) }(cross(cur)) } // The reads, called as Adena calls them: the id a quoted string, two values. func TestReads(cur realm, t *testing.T) { reset(cur) owner, err := OwnerOf("1") uassert.NoError(t, err) uassert.Equal(t, aliceAddr, owner) n, err := BalanceOf(aliceAddr) uassert.NoError(t, err) uassert.Equal(t, int64(1), n) n, err = BalanceOf(bobAddr) uassert.NoError(t, err) uassert.Equal(t, int64(0), n) uri, err := TokenURI("1") uassert.NoError(t, err) uassert.Equal(t, image, uri) uassert.True(t, strings.HasPrefix(uri, "https://")) meta, err := TokenMetadata("1") uassert.NoError(t, err) uassert.Equal(t, `{"name":"Golden Ticket #1","description":"A 1/1 on gno.land, minted to check that collections show in Adena.",`+ `"image":"`+image+`","attributes":[{"traitType":"edition","value":"1 of 1"},{"traitType":"metal","value":"gold"}]}`, meta) uassert.Equal(t, int64(1), Token.TotalSupply()) uassert.Equal(t, "Golden Ticket", Name()) uassert.Equal(t, "GOLD", Symbol()) // Any other id is an error, not a panic. for _, bad := range []grc721.TokenID{"", "0", "2", "01", " 1"} { _, err = OwnerOf(bad) uassert.ErrorContains(t, err, errNoTicket, string(bad)) _, err = TokenURI(bad) uassert.ErrorContains(t, err, errNoTicket, string(bad)) _, err = TokenMetadata(bad) uassert.ErrorContains(t, err, errNoTicket, string(bad)) } } func TestTransfer(cur realm, t *testing.T) { reset(cur) // Only the holder moves the ticket. testing.SetRealm(testing.NewUserRealm(bobAddr)) uassert.AbortsWithMessage(t, cur, errNotOwner, func() { Transfer(cross(cur), bobAddr) }) testing.SetRealm(testing.NewUserRealm(aliceAddr)) uassert.AbortsWithMessage(t, cur, errBadOwner, func() { Transfer(cross(cur), address("nope")) }) Transfer(cross(cur), bobAddr) owner, err := OwnerOf("1") uassert.NoError(t, err) uassert.Equal(t, bobAddr, owner) n, _ := BalanceOf(aliceAddr) uassert.Equal(t, int64(0), n) n, _ = BalanceOf(bobAddr) uassert.Equal(t, int64(1), n) // Not through another realm. testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/relay")) uassert.AbortsWithMessage(t, cur, errNotOwner, func() { Transfer(cross(cur), aliceAddr) }) } func TestRender(cur realm, t *testing.T) { reset(cur) out := Render("") uassert.True(t, strings.Contains(out, "# Golden Ticket\n")) uassert.True(t, strings.Contains(out, "!["+tokenName+"]("+image+")")) uassert.True(t, strings.Contains(out, "Held by `"+aliceAddr.String()+"`.")) }
  8. #8/gno.MemPackageType
  9. #9 MPUserAll

Result log

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