Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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("![" + tokenName + "](" + image + ")\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}