goldenticket.gno
4.25 Kb · 132 lines
1// Package goldenticket is a 1/1 GRC721 NFT: one golden ticket, minted to the
2// publisher when the package is added. It exists to check that a gno.land
3// collection shows in the Adena wallet before gnofly's planes go to mainnet,
4// so it is built the way the planes are:
5//
6// - the collection is a gno.land/p/nt/grc721/v0 token made with NewToken,
7// whose NewToken and Transfer events Adena finds on its indexer;
8// - the read functions take a grc721.TokenID and return (value, error), as
9// Adena calls them: OwnerOf("1"), BalanceOf("g1..."), TokenURI("1") as
10// the <img src>, TokenMetadata("1") as JSON with camelCase traitType.
11//
12// Transfer moves the ticket, so it can reach any Adena account.
13package goldenticket
14
15import (
16 "errors"
17 "strings"
18
19 "gno.land/p/nt/grc721/v0"
20)
21
22const (
23 tokenName = "Golden Ticket"
24 tokenSymbol = "GOLD"
25 ticket = grc721.TokenID("1")
26
27 // An https PNG, as the planes' images are: the SuperGnome of gnofly, from
28 // the Blob store that serves the collection. Adena loads https: and data:
29 // images only.
30 image = "https://xjfvwq1pdzcznycc.public.blob.vercel-storage.com/planes/777.png"
31
32 errNoTicket = "goldenticket: there is one ticket, id 1"
33 errNotOwner = "goldenticket: caller does not hold the ticket"
34 errBadOwner = "goldenticket: invalid recipient address"
35)
36
37var (
38 // Token is the collection, read-only: Token.OwnerOf("1"), Token.BalanceOf.
39 Token *grc721.Token
40 ledger *grc721.PrivateLedger
41)
42
43// init runs once, when the package is added to the chain. This is the only
44// moment cur.Previous() is the publisher, who receives the ticket.
45//
46// Under `gno test` there is no publisher and the address is empty. The tests
47// call bootstrap themselves with a real address.
48func init(cur realm) {
49 bootstrap(cur.Previous().Address(), cur)
50}
51
52// bootstrap creates the collection and mints the ticket to holder; with no
53// valid holder, under `gno test`, it mints nothing.
54func bootstrap(holder address, rlm realm) {
55 Token, ledger = grc721.NewToken(tokenName, tokenSymbol, 0, rlm)
56 if !holder.IsValid() {
57 return
58 }
59 if err := ledger.Mint(holder, ticket); err != nil {
60 panic(err)
61 }
62}
63
64// Name and Symbol, as GRC721 tokens have them. Adena reads both from the
65// NewToken event.
66func Name() string { return tokenName }
67func Symbol() string { return tokenSymbol }
68
69// OwnerOf returns who holds the ticket, or an error for any other id.
70func OwnerOf(tid grc721.TokenID) (address, error) {
71 if tid != ticket {
72 return "", errors.New(errNoTicket)
73 }
74 return Token.OwnerOf(tid)
75}
76
77// BalanceOf returns 1 for the holder of the ticket, 0 for everyone else.
78func BalanceOf(addr address) (int64, error) {
79 return Token.BalanceOf(addr)
80}
81
82// TokenURI returns the image of the ticket: what Adena shows.
83func TokenURI(tid grc721.TokenID) (string, error) {
84 if _, err := OwnerOf(tid); err != nil {
85 return "", err
86 }
87 return image, nil
88}
89
90// TokenMetadata returns the metadata of the ticket as JSON, with the keys
91// Adena reads.
92func TokenMetadata(tid grc721.TokenID) (string, error) {
93 if _, err := OwnerOf(tid); err != nil {
94 return "", err
95 }
96 return `{"name":"` + tokenName + ` #1","description":"A 1/1 on gno.land, minted to check that collections show in Adena.",` +
97 `"image":"` + image + `","attributes":[{"traitType":"edition","value":"1 of 1"},{"traitType":"metal","value":"gold"}]}`, nil
98}
99
100// Transfer gives the ticket to someone else. Only its holder can, by a
101// direct call (maketx call or Adena), not through another realm.
102func Transfer(cur realm, to address) {
103 caller := cur.Previous()
104 if !caller.IsUserCall() {
105 panic(errNotOwner)
106 }
107 if !to.IsValid() {
108 panic(errBadOwner)
109 }
110 holder, err := Token.OwnerOf(ticket)
111 if err != nil || holder != caller.Address() {
112 panic(errNotOwner)
113 }
114 if err := ledger.TransferFrom(holder, holder, to, ticket); err != nil {
115 panic(err)
116 }
117}
118
119// Render shows the ticket and who holds it.
120func Render(_ string) string {
121 var b strings.Builder
122 b.WriteString("# " + tokenName + "\n\n")
123 b.WriteString("A 1/1 GRC721 on gno.land, minted to check that collections show in Adena.\n\n")
124 b.WriteString("\n\n")
125 holder, err := Token.OwnerOf(ticket)
126 if err != nil {
127 b.WriteString("Not minted.\n")
128 return b.String()
129 }
130 b.WriteString("Held by `" + holder.String() + "`.\n")
131 return b.String()
132}