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.15 Kb · 252 lines
  1package guardians_pilot_v1
  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// The pilot owner reveals manually after the full 24-hour sale window.
105// Selling out early never starts reveal, and no timer invokes this function.
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	if !saleConfigured || time.Now().Unix() < saleConfig.publicEnd {
115		panic("guardians: public sale has not ended")
116	}
117	if ComputeRevealCommitment(secret) != revealCommitment {
118		panic("guardians: reveal secret does not match commitment")
119	}
120	if token.TotalSupply() == 0 {
121		panic("guardians: cannot reveal an empty collection")
122	}
123
124	revealSeedHash = hashBytes([]byte("GUARDIANS|SEED|" + secret))
125	permutationA = derivePermutationA(secret)
126	permutationB = deriveSeedNumber("B", secret) % maxSupply
127	revealTarget = token.TotalSupply()
128	revealProcessed = 0
129	revealState = revealStateRevealing
130
131	chain.Emit(
132		"RevealStarted",
133		"seedHash", revealSeedHash,
134		"target", ufmt.Sprintf("%d", revealTarget),
135	)
136}
137
138// ApplyRevealBatch updates sequentially minted tokens. Anyone may execute the
139// deterministic batch, so the project operator cannot block progress.
140func ApplyRevealBatch(startTokenID int64, quantity int) {
141	if revealState != revealStateRevealing {
142		panic("guardians: reveal batch is not active")
143	}
144	if quantity < 1 || quantity > maxRevealBatch {
145		panic("guardians: reveal batch quantity must be 1..20")
146	}
147	if startTokenID != revealProcessed+1 {
148		panic("guardians: reveal batch must continue sequentially")
149	}
150	end := startTokenID + int64(quantity) - 1
151	if end > revealTarget {
152		panic("guardians: reveal batch exceeds target")
153	}
154
155	for tokenID := startTokenID; tokenID <= end; tokenID++ {
156		metadataID := MetadataID(tokenID)
157		uri := finalBaseURI + metadataFilename(metadataID)
158		if err := metadataLedger.SetTokenURI(grc721.TokenID(ufmt.Sprintf("%d", tokenID)), uri); err != nil {
159			panic(err)
160		}
161	}
162	revealProcessed = end
163	if revealProcessed == revealTarget {
164		revealState = revealStateRevealed
165	}
166
167	chain.Emit(
168		"RevealBatchApplied",
169		"startTokenId", ufmt.Sprintf("%d", startTokenID),
170		"endTokenId", ufmt.Sprintf("%d", end),
171		"processed", ufmt.Sprintf("%d", revealProcessed),
172	)
173}
174
175// metadataFilename matches the immutable IPFS release layout (0001.json–3333.json).
176func metadataFilename(metadataID int64) string {
177	serial := ufmt.Sprintf("%d", metadataID)
178	for len(serial) < 4 {
179		serial = "0" + serial
180	}
181	return serial + ".json"
182}
183
184func MetadataID(tokenID int64) int64 {
185	if revealState != revealStateRevealing &&
186		revealState != revealStateRevealed &&
187		revealState != revealStateUnveiledFrozen {
188		panic("guardians: metadata mapping is not revealed")
189	}
190	if tokenID < 1 || tokenID > revealTarget {
191		panic("guardians: token id outside reveal target")
192	}
193	return (permutationA*(tokenID-1)+permutationB)%maxSupply + 1
194}
195
196func FreezeMetadata(cur realm) {
197	assertAdmin(cur.Previous().Address())
198	if revealState != revealStateRevealed || revealProcessed != revealTarget {
199		panic("guardians: complete reveal before freezing metadata")
200	}
201	revealState = revealStateUnveiledFrozen
202	chain.Emit("MetadataFrozen", "provenance", provenanceRoot, "seedHash", revealSeedHash)
203}
204
205func assertMintingAllowed() {
206	if revealState != revealStateVeiled && revealState != revealStateCommitted {
207		panic("guardians: minting is closed after reveal starts")
208	}
209}
210
211func validFinalBaseURI(uri string) bool {
212	return (strings.HasPrefix(uri, "ipfs://") || strings.HasPrefix(uri, "https://")) &&
213		strings.HasSuffix(uri, "/")
214}
215
216func derivePermutationA(secret string) int64 {
217	candidate := deriveSeedNumber("A", secret) % maxSupply
218	if candidate == 0 {
219		candidate = 1
220	}
221	for gcd(candidate, maxSupply) != 1 {
222		candidate++
223		if candidate >= maxSupply {
224			candidate = 1
225		}
226	}
227	return candidate
228}
229
230func deriveSeedNumber(label, secret string) int64 {
231	hash := hashBytes([]byte("GUARDIANS|PERMUTATION|" + label + "|" + secret))
232	value, err := strconv.ParseInt(hash[:15], 16, 64)
233	if err != nil {
234		panic(err)
235	}
236	return value
237}
238
239func gcd(first, second int64) int64 {
240	for second != 0 {
241		first, second = second, first%second
242	}
243	return first
244}
245
246func normalizeHash(value string) string {
247	decoded, err := hex.DecodeString(value)
248	if err != nil || len(decoded) != sha256.Size {
249		return ""
250	}
251	return hex.EncodeToString(decoded)
252}