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

nft.gno

5.75 Kb · 199 lines
  1// Package nft holds GnoRadio's concert tickets as GRC721 NFTs. It is
  2// permanent, like the data realm: a tickets release changes the rules, never
  3// this ledger, so no ticket ever moves to a new contract.
  4//
  5// Only the realm that writes the tickets role now (data.Writer("tickets"))
  6// mints and transfers, only while that role is not paused, and only for the
  7// wallet that signs the transaction (OriginCaller): a release can never move
  8// the ticket of a holder who signs nothing. That is the whole guarantee. The
  9// signature proves the holder sent some transaction through the tickets
 10// writer, not that they asked for this move: a malicious tickets release (it
 11// takes over only after the data realm's public 72 h window, which the
 12// guardian can cancel) could move every ticket of a holder who then signs any
 13// call to it. This realm cannot see further up the call stack. Anyone reads.
 14//
 15// A ticket's metadata and artwork are drawn on the fly from the tickets data
 16// (art.gno), whose record layout below is frozen for that reason.
 17package nft
 18
 19import (
 20	"chain/runtime/unsafe"
 21	"strconv"
 22
 23	"gno.land/p/nym-alexiscolin000/gnoradio/role/v0"
 24	"gno.land/p/nym-alexiscolin000/gnoradio/store/v0"
 25	"gno.land/p/nt/grc721/v0"
 26	"gno.land/r/nym-alexiscolin000/gnoradio/data"
 27)
 28
 29// The tickets records this realm reads, written by the tickets writer. Every
 30// tickets release keeps this layout: the artwork of existing tickets depends
 31// on it.
 32const (
 33	CEvents  = "tickets/events"  // store.Pad(id) -> event record
 34	CTickets = "tickets/tickets" // store.Pad((id-1)/Chunk) -> Chunk ticket records
 35	Chunk    = 16
 36)
 37
 38// Event record fields (store.Rec); the one each sale changes comes first.
 39const (
 40	ESold = iota
 41	EArtist
 42	EStart
 43	EPrice
 44	ECapacity
 45	ECancelled // "1" or ""
 46	EHidden    // "1" or ""
 47	ERemoved   // the moderator's public reason while hidden
 48	ETitle
 49	EVenue
 50	ELink
 51	EFields
 52)
 53
 54// Ticket record fields.
 55const (
 56	TEvent = iota
 57	TSerial
 58	TAttended // "1" or ""
 59)
 60
 61// The catalog's artist heads, read for a ticket's artist line: every catalog
 62// release keeps this layout, since it shares the data realm's records
 63// (catalog/v1 records.gno). Reading them here, not through a catalog realm,
 64// keeps this permanent realm free of any versioned import.
 65const (
 66	CArtists    = "catalog/artists" // store.Pad((id-1)/ArtistChunk) -> ArtistChunk artist heads
 67	ArtistChunk = 8
 68	AHidden     = 0 // "1" or ""
 69	AName       = 6
 70)
 71
 72var (
 73	token  *grc721.Token
 74	ledger *grc721.PrivateLedger
 75)
 76
 77// init creates the token, so its storage is paid at deploy and never by a
 78// first buyer.
 79func init(cur realm) {
 80	token, ledger = grc721.NewToken("GnoRadio Ticket", "GRTIX", 1, cur)
 81}
 82
 83// writer lets only the tickets writer through, while its role is not paused.
 84func writer(cur realm) {
 85	if cur.Previous().PkgPath() != data.Writer("tickets") {
 86		panic("nft: only " + data.Writer("tickets") + " mints and transfers tickets")
 87	}
 88	if data.Paused("tickets") {
 89		panic("nft: tickets is paused")
 90	}
 91}
 92
 93// maxID is the largest id a data key holds (store.Pad); readers answer ""
 94// past it instead of panicking.
 95const maxID = 99999999
 96
 97func tid(id int) grc721.TokenID { return grc721.TokenID(strconv.Itoa(id)) }
 98
 99// Mint creates ticket id for to, the wallet that signed the transaction.
100// The tickets writer only.
101func Mint(cur realm, to address, id int) {
102	writer(cur)
103	if to != unsafe.OriginCaller() {
104		panic("nft: a ticket is minted to the wallet that signs")
105	}
106	if id < 1 || id > maxID {
107		panic("nft: invalid ticket id")
108	}
109	if err := ledger.Mint(to, tid(id)); err != nil {
110		panic("nft: " + err.Error())
111	}
112}
113
114// Transfer moves ticket id from its holder from to to. The tickets writer
115// only, and only in a transaction from signed: no release moves the tickets
116// of a holder who signs nothing (see the package comment for what a
117// malicious release could still do). The rules decide who may gift.
118func Transfer(cur realm, from, to address, id int) {
119	writer(cur)
120	if from != unsafe.OriginCaller() {
121		panic("nft: only the holder's own transaction moves a ticket")
122	}
123	if !role.Canonical(to) {
124		panic("nft: not an address")
125	}
126	if err := ledger.TransferFrom(from, from, to, tid(id)); err != nil {
127		panic("nft: " + err.Error())
128	}
129}
130
131// ---- GRC721 reads ----
132
133func Name() string       { return token.GetName() }
134func Symbol() string     { return token.GetSymbol() }
135func TotalSupply() int64 { return token.TotalSupply() }
136
137// BalanceOf is how many tickets owner holds.
138func BalanceOf(owner address) int64 {
139	n, err := token.BalanceOf(owner)
140	if err != nil {
141		panic("nft: " + err.Error())
142	}
143	return n
144}
145
146// OwnerOf is a ticket's holder; it panics for an unknown ticket.
147func OwnerOf(tokenID string) address {
148	owner, err := token.OwnerOf(grc721.TokenID(tokenID))
149	if err != nil {
150		panic("nft: " + err.Error())
151	}
152	return owner
153}
154
155// Holder is a ticket's holder, "" for an unknown ticket.
156func Holder(id int) address {
157	owner, _ := token.OwnerOf(tid(id))
158	return owner
159}
160
161// Render shows the collection.
162func Render(_ string) string {
163	return token.RenderHome() + "\nGnoRadio concert tickets. Each one is drawn on-chain: `TokenURI(id)`. Only `" +
164		data.Writer("tickets") + "` mints and transfers them.\n"
165}
166
167// ---- the tickets records ----
168
169// ticket returns ticket id's record ("" when absent).
170func ticket(id int) string {
171	if id < 1 || id > maxID {
172		return ""
173	}
174	ch, ok := data.Get(CTickets, store.Pad((id-1)/Chunk))
175	if !ok {
176		return ""
177	}
178	return store.Field(ch, (id-1)%Chunk)
179}
180
181// artist returns artist id's head record ("" when absent).
182func artist(id int) string {
183	if id < 1 || id > maxID {
184		return ""
185	}
186	ch, ok := data.Get(CArtists, store.Pad((id-1)/ArtistChunk))
187	if !ok {
188		return ""
189	}
190	return store.Field(ch, (id-1)%ArtistChunk)
191}
192
193func event(id int) string {
194	if id < 1 || id > maxID {
195		return ""
196	}
197	rec, _ := data.Get(CEvents, store.Pad(id))
198	return rec
199}