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

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 }