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.

reveal.gno

7.30 Kb · 254 lines
  1package guardians
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"crypto/sha256"
  7	"encoding/hex"
  8	"strconv"
  9	"strings"
 10	"time"
 11
 12	"gno.land/p/nt/grc721/v0"
 13	"gno.land/p/nt/ufmt/v0"
 14)
 15
 16const (
 17	revealStateVeiled         = "VEILED"
 18	revealStateCommitted      = "REVEAL_COMMITTED"
 19	revealStateRevealing      = "REVEALING"
 20	revealStateRevealed       = "REVEALED"
 21	revealStateUnveiledFrozen = "UNVEILED_FROZEN"
 22	maxRevealBatch            = 20
 23)
 24
 25var (
 26	revealState       = revealStateVeiled
 27	provenanceRoot    string
 28	revealCommitment  string
 29	finalBaseURI      string
 30	revealDeadline    int64
 31	revealSeedHash    string
 32	permutationA      int64
 33	permutationB      int64
 34	revealTarget      int64
 35	revealProcessed   int64
 36)
 37
 38func RevealState() string { return revealState }
 39
 40func ProvenanceRoot() string { return provenanceRoot }
 41
 42func RevealCommitment() string { return revealCommitment }
 43
 44func FinalBaseURI() string { return finalBaseURI }
 45
 46func RevealDeadline() int64 { return revealDeadline }
 47
 48func RevealSeedHash() string { return revealSeedHash }
 49
 50func RevealTarget() int64 { return revealTarget }
 51
 52func RevealProcessed() int64 { return revealProcessed }
 53
 54func RevealOverdue() bool {
 55	return revealState == revealStateCommitted && time.Now().Unix() > revealDeadline
 56}
 57
 58// ConfigureReveal permanently commits provenance, secret hash and final URI
 59// layout before the first token can be minted.
 60func ConfigureReveal(cur realm, provenance, commitment, baseURI string, deadline int64) {
 61	assertAdmin(cur.Previous().Address())
 62	if revealState != revealStateVeiled {
 63		panic("guardians: reveal configuration already committed")
 64	}
 65	if token.TotalSupply() != 0 || !salePaused {
 66		panic("guardians: configure reveal before minting and sale activation")
 67	}
 68	normalizedProvenance := normalizeHash(provenance)
 69	if normalizedProvenance == "" {
 70		panic("guardians: invalid provenance root")
 71	}
 72	normalizedCommitment := normalizeHash(commitment)
 73	if normalizedCommitment == "" {
 74		panic("guardians: invalid reveal commitment")
 75	}
 76	if !validFinalBaseURI(baseURI) {
 77		panic("guardians: final base uri must be ipfs/https and end with slash")
 78	}
 79	if deadline <= time.Now().Unix() {
 80		panic("guardians: reveal deadline must be in the future")
 81	}
 82
 83	provenanceRoot = normalizedProvenance
 84	revealCommitment = normalizedCommitment
 85	finalBaseURI = baseURI
 86	revealDeadline = deadline
 87	revealState = revealStateCommitted
 88
 89	chain.Emit(
 90		"RevealConfigured",
 91		"provenance", provenanceRoot,
 92		"commitment", revealCommitment,
 93		"deadline", ufmt.Sprintf("%d", revealDeadline),
 94	)
 95}
 96
 97// ComputeRevealCommitment is deterministic but production secrets should be
 98// committed with the offline helper, never exposed through a public RPC query.
 99func ComputeRevealCommitment(secret string) string {
100	payload := runtime.ChainID() + "|" + CollectionID() + "|REVEAL|" + secret
101	return hashBytes([]byte(payload))
102}
103
104// Only the administrator can manually begin reveal after sale end or sellout.
105// Neither a deadline nor knowledge of the secret alone starts the reveal.
106func Reveal(cur realm, secret string) {
107	assertAdmin(cur.Previous().Address())
108	if revealState != revealStateCommitted {
109		panic("guardians: reveal is not committed")
110	}
111	if len(secret) < 8 || len(secret) > 256 {
112		panic("guardians: reveal secret length must be 8..256")
113	}
114	// publicEnd=0 means still open, so it must never unlock an early reveal.
115	ended := saleConfig.publicEnd > 0 && time.Now().Unix() >= saleConfig.publicEnd
116	if !saleConfigured || (!ended && token.TotalSupply() < maxSupply) {
117		panic("guardians: public sale has not ended")
118	}
119	if ComputeRevealCommitment(secret) != revealCommitment {
120		panic("guardians: reveal secret does not match commitment")
121	}
122	if token.TotalSupply() == 0 {
123		panic("guardians: cannot reveal an empty collection")
124	}
125
126	revealSeedHash = hashBytes([]byte("GUARDIANS|SEED|" + secret))
127	permutationA = derivePermutationA(secret)
128	permutationB = deriveSeedNumber("B", secret) % maxSupply
129	revealTarget = token.TotalSupply()
130	revealProcessed = 0
131	revealState = revealStateRevealing
132
133	chain.Emit(
134		"RevealStarted",
135		"seedHash", revealSeedHash,
136		"target", ufmt.Sprintf("%d", revealTarget),
137	)
138}
139
140// ApplyRevealBatch updates sequentially minted tokens. Anyone may execute the
141// deterministic batch, so the project operator cannot block progress.
142func ApplyRevealBatch(startTokenID int64, quantity int) {
143	if revealState != revealStateRevealing {
144		panic("guardians: reveal batch is not active")
145	}
146	if quantity < 1 || quantity > maxRevealBatch {
147		panic("guardians: reveal batch quantity must be 1..20")
148	}
149	if startTokenID != revealProcessed+1 {
150		panic("guardians: reveal batch must continue sequentially")
151	}
152	end := startTokenID + int64(quantity) - 1
153	if end > revealTarget {
154		panic("guardians: reveal batch exceeds target")
155	}
156
157	for tokenID := startTokenID; tokenID <= end; tokenID++ {
158		metadataID := MetadataID(tokenID)
159		uri := finalBaseURI + metadataFilename(metadataID)
160		if err := metadataLedger.SetTokenURI(grc721.TokenID(ufmt.Sprintf("%d", tokenID)), uri); err != nil {
161			panic(err)
162		}
163	}
164	revealProcessed = end
165	if revealProcessed == revealTarget {
166		revealState = revealStateRevealed
167	}
168
169	chain.Emit(
170		"RevealBatchApplied",
171		"startTokenId", ufmt.Sprintf("%d", startTokenID),
172		"endTokenId", ufmt.Sprintf("%d", end),
173		"processed", ufmt.Sprintf("%d", revealProcessed),
174	)
175}
176
177// metadataFilename matches the immutable IPFS release layout (0001.json–3333.json).
178func metadataFilename(metadataID int64) string {
179	serial := ufmt.Sprintf("%d", metadataID)
180	for len(serial) < 4 {
181		serial = "0" + serial
182	}
183	return serial + ".json"
184}
185
186func MetadataID(tokenID int64) int64 {
187	if revealState != revealStateRevealing &&
188		revealState != revealStateRevealed &&
189		revealState != revealStateUnveiledFrozen {
190		panic("guardians: metadata mapping is not revealed")
191	}
192	if tokenID < 1 || tokenID > revealTarget {
193		panic("guardians: token id outside reveal target")
194	}
195	return (permutationA*(tokenID-1)+permutationB)%maxSupply + 1
196}
197
198func FreezeMetadata(cur realm) {
199	assertAdmin(cur.Previous().Address())
200	if revealState != revealStateRevealed || revealProcessed != revealTarget {
201		panic("guardians: complete reveal before freezing metadata")
202	}
203	revealState = revealStateUnveiledFrozen
204	chain.Emit("MetadataFrozen", "provenance", provenanceRoot, "seedHash", revealSeedHash)
205}
206
207func assertMintingAllowed() {
208	if revealState != revealStateVeiled && revealState != revealStateCommitted {
209		panic("guardians: minting is closed after reveal starts")
210	}
211}
212
213func validFinalBaseURI(uri string) bool {
214	return (strings.HasPrefix(uri, "ipfs://") || strings.HasPrefix(uri, "https://")) &&
215		strings.HasSuffix(uri, "/")
216}
217
218func derivePermutationA(secret string) int64 {
219	candidate := deriveSeedNumber("A", secret) % maxSupply
220	if candidate == 0 {
221		candidate = 1
222	}
223	for gcd(candidate, maxSupply) != 1 {
224		candidate++
225		if candidate >= maxSupply {
226			candidate = 1
227		}
228	}
229	return candidate
230}
231
232func deriveSeedNumber(label, secret string) int64 {
233	hash := hashBytes([]byte("GUARDIANS|PERMUTATION|" + label + "|" + secret))
234	value, err := strconv.ParseInt(hash[:15], 16, 64)
235	if err != nil {
236		panic(err)
237	}
238	return value
239}
240
241func gcd(first, second int64) int64 {
242	for second != 0 {
243		first, second = second, first%second
244	}
245	return first
246}
247
248func normalizeHash(value string) string {
249	decoded, err := hex.DecodeString(value)
250	if err != nil || len(decoded) != sha256.Size {
251		return ""
252	}
253	return hex.EncodeToString(decoded)
254}