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

metadata.gno

5.95 Kb · 166 lines
  1package nft
  2
  3import (
  4	"chain"
  5	"crypto/sha256"
  6	"encoding/hex"
  7	"strconv"
  8	"strings"
  9
 10	"gno.land/p/samcrew/launchpad/meta/v1"
 11)
 12
 13const (
 14	MetadataStatic  = "static"  // base URI fixed at creation
 15	MetadataReveal  = "reveal"  // placeholder until one committed reveal
 16	MetadataMutable = "mutable" // creator may change the base URI until frozen
 17)
 18
 19// metadata decides what TokenURI answers. Once frozen, the base URI can never
 20// change again; only then can traits be committed.
 21type metadata struct {
 22	mode           string
 23	baseURI        string  // empty until a reveal
 24	placeholderURI string  // reveal: served for every token until Reveal
 25	commitment     string  // reveal: RevealCommitment of the base URI to come
 26	committer      address // reveal: the creator who made the commitment
 27	provenance     string  // reveal: SHA-256 of the ordered metadata manifest
 28	frozen         bool
 29	revision       int64 // base URI changes since creation
 30}
 31
 32// contentAddressed accepts an IPFS path that resolves to the same files for
 33// good: a server behind any other scheme could change them at will, and an
 34// empty CID, a "." or ".." segment, an escape, a query or a fragment could
 35// make a gateway resolve it elsewhere, a mutable IPNS name included.
 36func contentAddressed(s string) bool {
 37	return strings.HasPrefix(s, "ipfs://") && len(s) > 7 && s[7] != '/' &&
 38		!strings.Contains(s, "/.") && !strings.ContainsAny(s, "%?#") && meta.Image(s) == nil
 39}
 40
 41// A base URI is a directory: token n resolves to <base><n>.json.
 42func validBaseURI(s string) bool {
 43	return contentAddressed(s) && s[len(s)-1] == '/'
 44}
 45
 46func validHash(s string) bool {
 47	if len(s) != 64 {
 48		return false
 49	}
 50	for i := 0; i < len(s); i++ {
 51		if c := s[i]; !(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') {
 52			return false
 53		}
 54	}
 55	return true
 56}
 57
 58// RevealCommitment is what a creator commits to at creation and opens at
 59// Reveal: sha256("launchpad-reveal-v1|<creator>|<salt>|<base URI>"), with a
 60// salt of 64 hex digits the creator keeps secret until then. The salt keeps a
 61// guessed URI from being confirmed before the reveal. The creator's address
 62// keeps another account from reusing the commitment: a collection's ID does
 63// not exist yet when its creator commits, so the account stands in for it.
 64func RevealCommitment(creator address, salt, baseURI string) string {
 65	h := sha256.Sum256([]byte("launchpad-reveal-v1|" + creator.String() + "|" + salt + "|" + baseURI))
 66	return hex.EncodeToString(h[:])
 67}
 68
 69func newMetadata(mode, baseURI, placeholderURI, commitment, provenance string, creator address) metadata {
 70	ok := false
 71	switch mode {
 72	case MetadataStatic, MetadataMutable:
 73		ok = validBaseURI(baseURI) && placeholderURI == "" && commitment == "" && provenance == ""
 74	case MetadataReveal:
 75		// What is hidden must be content-addressed, or the commitment would
 76		// say nothing about the files behind it.
 77		ok = baseURI == "" && contentAddressed(placeholderURI) &&
 78			strings.HasSuffix(placeholderURI, ".json") &&
 79			validHash(commitment) && commitment != strings.Repeat("0", 64) &&
 80			validHash(provenance)
 81	}
 82	if !ok {
 83		panic("launchpad/nft: invalid metadata terms")
 84	}
 85	m := metadata{mode: mode, baseURI: baseURI, placeholderURI: placeholderURI,
 86		commitment: commitment, provenance: provenance, frozen: mode == MetadataStatic}
 87	if mode == MetadataReveal {
 88		m.committer = creator
 89	}
 90	return m
 91}
 92
 93func (m metadata) tokenURI(number int64) string {
 94	if m.baseURI == "" {
 95		return m.placeholderURI
 96	}
 97	return m.baseURI + strconv.FormatInt(number, 10) + ".json"
 98}
 99
100func emitMetadata(id string, m metadata) {
101	chain.Emit("MetadataChanged", "collection", id, "base_uri", m.baseURI,
102		"revision", strconv.FormatInt(m.revision, 10),
103		"frozen", strconv.FormatBool(m.frozen))
104}
105
106// Reveal publishes the base URI committed at creation, with the salt that
107// opens the commitment, and fixes it for good. The commitment stays bound to
108// the account that created the collection, whoever holds the creator role now.
109func Reveal(cur realm, id, baseURI, salt string) {
110	m := &creatorOf(cur, id).metadata
111	if m.mode != MetadataReveal || m.frozen {
112		panic("launchpad/nft: nothing to reveal")
113	}
114	if !validBaseURI(baseURI) || !validHash(salt) ||
115		RevealCommitment(m.committer, salt, baseURI) != m.commitment {
116		panic("launchpad/nft: reveal does not match the commitment")
117	}
118	m.baseURI, m.frozen, m.revision = baseURI, true, 1
119	emitMetadata(id, *m)
120}
121
122func mutable(cur realm, id string) *metadata {
123	m := &creatorOf(cur, id).metadata
124	if m.mode != MetadataMutable || m.frozen {
125		panic("launchpad/nft: metadata is fixed")
126	}
127	return m
128}
129
130// SetBaseURI points a mutable collection at new metadata. Every change is
131// announced with its revision number.
132func SetBaseURI(cur realm, id, baseURI string) {
133	m := mutable(cur, id)
134	if !validBaseURI(baseURI) || baseURI == m.baseURI {
135		panic("launchpad/nft: invalid base URI")
136	}
137	m.baseURI = baseURI
138	m.revision++
139	emitMetadata(id, *m)
140}
141
142// FreezeMetadata gives up the right to change a mutable collection's base URI.
143func FreezeMetadata(cur realm, id string) {
144	m := mutable(cur, id)
145	m.frozen = true
146	emitMetadata(id, *m)
147}
148
149// CommitTraits records, once, the Merkle root of the collection's traits. It
150// waits for frozen metadata so the root cannot outlive the files it describes.
151// A leaf is sha256(0x00 || "launchpad-trait-v1|<collection>|<number>|<type>=<value>")
152// in a p/samcrew/launchpad/merkle/v1 tree, where neither the type nor the
153// value contains "=" or "|": a leaf has then one reading only, and a leaf
154// that breaks the rule matches no trait offer. The ledger cannot check the
155// root against the files; whoever relies on it must recompute it from them.
156func CommitTraits(cur realm, id, root string) {
157	c := creatorOf(cur, id)
158	if !c.metadata.frozen || c.traitsRoot != "" || !validHash(root) {
159		panic("launchpad/nft: traits cannot be committed")
160	}
161	c.traitsRoot = root
162	chain.Emit("TraitsCommitted", "collection", id, "root", root)
163}
164
165// TraitsRoot is empty until the creator commits one.
166func TraitsRoot(id string) string { return mustCollection(id).traitsRoot }