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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

tokens.gno

10.95 Kb · 361 lines
  1package core
  2
  3import (
  4	"chain"
  5	"encoding/base64"
  6
  7	"gno.land/p/nt/avl/v0"
  8	"gno.land/p/nt/grc721/royalty/v0"
  9	"gno.land/p/nt/grc721/v0"
 10
 11	"gno.land/p/g1pq6pxhgxk2uw5shg5le6glfydpxcu74k0zh069/sinopia/engine/v1"
 12	"gno.land/p/g1pq6pxhgxk2uw5shg5le6glfydpxcu74k0zh069/sinopia/safe/v1"
 13)
 14
 15const tidWidth = 6
 16
 17// tokenState is the per-collection data the transfer hook needs. It is
 18// shared by the collection and its lockExt, so neither points at the other.
 19type tokenState struct {
 20	final   bool
 21	batches avl.Tree // safe.PadInt(first) -> *mintBatch
 22	moves   avl.Tree // tid -> *moveRecord, created on a token's first transfer
 23}
 24
 25// mintBatch records one mint call; mint height is derived per token from
 26// it, so a 10-token mint writes one record, not ten.
 27type mintBatch struct {
 28	first, last, height int64
 29}
 30
 31type moveRecord struct {
 32	moves, lastMove int64
 33}
 34
 35func (st *tokenState) addBatch(first, last, h int64) {
 36	st.batches.Set(safe.PadInt(first, tidWidth), &mintBatch{first: first, last: last, height: h})
 37}
 38
 39func (st *tokenState) mintHeight(tid int64) int64 {
 40	var h int64
 41	st.batches.ReverseIterate("", safe.PadInt(tid, tidWidth), func(_ string, v any) bool {
 42		b := v.(*mintBatch)
 43		if tid <= b.last {
 44			h = b.height
 45		}
 46		return true
 47	})
 48	return h
 49}
 50
 51func (st *tokenState) moveOf(tid string) (moves, lastMove int64) {
 52	if v := st.moves.Get(tid); v != nil {
 53		r := v.(*moveRecord)
 54		return r.moves, r.lastMove
 55	}
 56	return 0, 0
 57}
 58
 59// lockExt is the grc721 Extension behind two invariants:
 60//
 61//   - I-11: before Final no token moves or burns (the reveal lock that
 62//     closes RA H-3's information-advantage branch). The veto is a panic,
 63//     which aborts the whole transaction — the documented Extension
 64//     semantics of p/nt/grc721/v0.
 65//   - H-2: every transfer bumps the token's move counter, so a market
 66//     listing taken at moves=N is dead after any transfer, even if the
 67//     token later comes back to the seller.
 68//
 69// It is declared in this realm, so the hooks write core state with core's
 70// authority even when another realm moves the token via RealmTeller.
 71type lockExt struct {
 72	cid string
 73	st  *tokenState
 74}
 75
 76const lockKind = "art-lock"
 77
 78func (e *lockExt) ExtensionKind() string { return lockKind }
 79
 80// The hooks also emit the platform's per-token history events with the
 81// collection id ("Minted", "Transfer"); grc721's own Transfer event carries
 82// only the token's grc721 id.
 83func (e *lockExt) OnMint(to address, tid grc721.TokenID) {
 84	chain.Emit("Minted", "cid", e.cid, "tid", tid.String(), "to", to.String())
 85}
 86
 87func (e *lockExt) OnTransfer(from, to address, tid grc721.TokenID) {
 88	if !e.st.final {
 89		panic("tokens are locked until the collection is final (reveal)")
 90	}
 91	key := tid.String()
 92	r, _ := e.st.moves.Get(key).(*moveRecord)
 93	if r == nil {
 94		r = &moveRecord{}
 95		e.st.moves.Set(key, r)
 96	}
 97	r.moves++
 98	r.lastMove = height()
 99	chain.Emit("Transfer", "cid", e.cid, "tid", key, "from", from.String(), "to", to.String())
100}
101
102func (e *lockExt) OnBurn(tid grc721.TokenID) {
103	if !e.st.final {
104		panic("tokens are locked until the collection is final (reveal)")
105	}
106	e.st.moves.Remove(tid.String())
107}
108
109// --- owner actions (I-2: never paused) ---------------------------------------
110//
111// The acting account is always the immediate caller (cur.Previous()), the
112// same rule as grc721's CallerTeller: a realm a user merely calls cannot act
113// for that user. Another realm moves tokens only after an Approve, through
114// Token(cid).RealmTeller (market/v1 does exactly that).
115
116// Every Approve of a token and every SetApprovalForAll of an (owner,
117// operator) pair bumps a nonce. A market pins the nonces at listing time,
118// so revoking an approval kills a listing for good: approving again later
119// starts a new nonce and never revives the old listing (SA-L3).
120func Approve(cur realm, cid, tid string, to address) {
121	assertNoDeposit(cur)
122	c := mustCollection(cid)
123	if err := c.ledger.Approve(caller(cur), to, grc721.TokenID(tid)); err != nil {
124		panic(err)
125	}
126	bumpNonce(c, "t:"+tid)
127}
128
129func SetApprovalForAll(cur realm, cid string, operator address, approved bool) {
130	assertNoDeposit(cur)
131	c := mustCollection(cid)
132	owner := caller(cur)
133	if err := c.ledger.SetApprovalForAll(owner, operator, approved); err != nil {
134		panic(err)
135	}
136	bumpNonce(c, "o:"+owner.String()+":"+operator.String())
137}
138
139// ApprovalNonces returns the token's approval nonce and the (owner,
140// operator) approval-for-all nonce. Approvals made directly through
141// Token(cid).RealmTeller by a realm acting for itself do not bump them.
142func ApprovalNonces(cid, tid string, owner, operator address) (tokenNonce, operatorNonce int64) {
143	c := mustCollection(cid)
144	return nonceOf(c, "t:"+tid), nonceOf(c, "o:"+owner.String()+":"+operator.String())
145}
146
147func bumpNonce(c *collection, key string) {
148	c.approvals.Set(key, nonceOf(c, key)+1)
149}
150
151func nonceOf(c *collection, key string) int64 {
152	if v := c.approvals.Get(key); v != nil {
153		return v.(int64)
154	}
155	return 0
156}
157
158func TransferFrom(cur realm, cid, tid string, from, to address) {
159	assertNoDeposit(cur)
160	c := mustCollection(cid)
161	if err := c.ledger.TransferFrom(caller(cur), from, to, grc721.TokenID(tid)); err != nil {
162		panic(err)
163	}
164}
165
166// Burn is owner-only. The supply gate uses nextTID, so a burn never reopens
167// a slot (I-5).
168func Burn(cur realm, cid, tid string) {
169	assertNoDeposit(cur)
170	c := mustCollection(cid)
171	owner, err := c.token.OwnerOf(grc721.TokenID(tid))
172	if err != nil {
173		panic(err)
174	}
175	if owner != caller(cur) {
176		panic("only the owner can burn a token")
177	}
178	if err := c.ledger.Burn(grc721.TokenID(tid)); err != nil {
179		panic(err)
180	}
181}
182
183// --- token reads ---------------------------------------------------------------
184
185// Token publishes the collection's official *grc721.Token. Its exported
186// methods are reads or teller factories that need the caller's live realm;
187// the ledger itself is never published.
188func Token(cid string) *grc721.Token { return mustCollection(cid).token }
189
190// Royalty publishes the read-only royalty view (EIP-2981 RoyaltyInfo).
191func Royalty(cid string) *royalty.Royalty { return mustCollection(cid).roy }
192
193func OwnerOf(cid, tid string) (address, error) {
194	return mustCollection(cid).token.OwnerOf(grc721.TokenID(tid))
195}
196
197func BalanceOf(cid string, owner address) (int64, error) {
198	return mustCollection(cid).token.BalanceOf(owner)
199}
200
201// TokenMoves is the transfer counter market/v1 pins in a listing (H-2).
202func TokenMoves(cid, tid string) int64 {
203	n, _ := mustCollection(cid).st.moveOf(tid)
204	return n
205}
206
207// TokenSeed is "" until the collection is Final: no read path can leak the
208// rarity map early.
209func TokenSeed(cid, tid string) string {
210	c := mustCollection(cid)
211	if !c.st.final || !exists(c, tid) {
212		return ""
213	}
214	return safe.Seed(c.id, tid, c.prov.salt)
215}
216
217// TokenState is {mintHeight, lastMoveHeight, moves} as JSON.
218func TokenState(cid, tid string) string {
219	c := mustCollection(cid)
220	if !exists(c, tid) {
221		panic(grc721.ErrInvalidTokenId)
222	}
223	in := tokenInput(c, tid)
224	w := &safe.JSONWriter{}
225	w.BeginObject()
226	w.IntField("mintHeight", in.MintHeight)
227	w.IntField("lastMoveHeight", in.LastMoveHeight)
228	w.IntField("moves", in.Moves)
229	w.EndObject()
230	return w.String()
231}
232
233// TokenURI is a base64 SVG data URI. Before Final every token shows core's
234// neutral placeholder, never engine output (design §4.2).
235func TokenURI(cid, tid string) (string, error) {
236	c := mustCollection(cid)
237	if _, err := c.token.OwnerOf(grc721.TokenID(tid)); err != nil {
238		return "", err
239	}
240	if !c.st.final {
241		return dataURI(placeholderSVG(c.name, "Unrevealed")), nil
242	}
243	return dataURI(renderToken(c, tid)), nil
244}
245
246// TokenTraits is [{"trait_type":…,"value":…}] JSON.
247func TokenTraits(cid, tid string) string {
248	c := mustCollection(cid)
249	w := &safe.JSONWriter{}
250	writeTraits(w, tokenTraits(c, tid))
251	return w.String()
252}
253
254// TokenTraitList is the typed form of TokenTraits for other realms (a copy).
255func TokenTraitList(cid, tid string) []engine.Trait {
256	return append([]engine.Trait{}, tokenTraits(mustCollection(cid), tid)...)
257}
258
259// TokenMetadata is OpenSea-style metadata JSON.
260func TokenMetadata(cid, tid string) string {
261	c := mustCollection(cid)
262	uri, err := TokenURI(cid, tid)
263	if err != nil {
264		panic(err)
265	}
266	w := &safe.JSONWriter{}
267	w.BeginObject()
268	w.StrField("name", c.name+" #"+tid)
269	w.StrField("description", c.description)
270	w.StrField("image", uri)
271	w.Key("attributes")
272	writeTraits(w, tokenTraits(c, tid))
273	w.EndObject()
274	return w.String()
275}
276
277func exists(c *collection, tid string) bool {
278	_, err := c.token.OwnerOf(grc721.TokenID(tid))
279	return err == nil
280}
281
282func tokenInput(c *collection, tid string) engine.Input {
283	n := safe.ParseID("tid", tid)
284	moves, lastMove := c.st.moveOf(tid)
285	mh := c.st.mintHeight(n)
286	if lastMove == 0 {
287		lastMove = mh
288	}
289	in := engine.Input{
290		Params:         c.params,
291		CollectionID:   c.id,
292		TokenID:        tid,
293		MintHeight:     mh,
294		LastMoveHeight: lastMove,
295		Moves:          moves,
296		Height:         height(),
297	}
298	if c.st.final {
299		in.Seed = safe.Seed(c.id, tid, c.prov.salt)
300	}
301	return in
302}
303
304// renderToken runs the collection's engine on a read path. A bad output
305// degrades to a placeholder; a panicking engine only fails reads of its own
306// collections (interrealm v2 §9: cross-realm panics abort the query).
307func renderToken(c *collection, tid string) string {
308	rec := mustEngine(c.engineID)
309	svg := rec.impl.Render(tokenInput(c, tid))
310	if err := engine.CheckSVG(rec.manifest, svg); err != nil {
311		return placeholderSVG(c.name, "Engine output rejected")
312	}
313	return svg
314}
315
316func tokenTraits(c *collection, tid string) []engine.Trait {
317	if !exists(c, tid) {
318		panic(grc721.ErrInvalidTokenId)
319	}
320	if !c.st.final {
321		return []engine.Trait{{Type: "Status", Value: "Unrevealed"}}
322	}
323	rec := mustEngine(c.engineID)
324	ts := rec.impl.Traits(tokenInput(c, tid))
325	if err := engine.ValidateTraits(rec.manifest, ts); err != nil {
326		return []engine.Trait{{Type: "Status", Value: "Engine output rejected"}}
327	}
328	return ts
329}
330
331func writeTraits(w *safe.JSONWriter, ts []engine.Trait) {
332	w.BeginArray()
333	for _, t := range ts {
334		w.BeginObject()
335		w.StrField("trait_type", t.Type)
336		w.StrField("value", t.Value)
337		w.EndObject()
338	}
339	w.EndArray()
340}
341
342// placeholderSVG is core's own neutral art: identical for every token of a
343// collection, so it carries no rarity signal.
344func placeholderSVG(name, label string) string {
345	return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">` +
346		`<rect width="512" height="512" fill="#111"/>` +
347		`<text x="256" y="236" fill="#ddd" font-family="monospace" font-size="24" text-anchor="middle">` +
348		safe.XMLEscape(name) + `</text>` +
349		`<text x="256" y="286" fill="#888" font-family="monospace" font-size="18" text-anchor="middle">` +
350		safe.XMLEscape(label) + `</text></svg>`
351}
352
353func dataURI(svg string) string {
354	return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))
355}
356
357// emitBatch announces one MintTo ("MintBatch") or ReserveTo ("Reserved")
358// call; each token also gets its own "Minted" event from lockExt.
359func emitBatch(kind string, c *collection, to address, first, last int64) {
360	chain.Emit(kind, "cid", c.id, "to", to.String(), "first", itoa(first), "last", itoa(last))
361}