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.

collections.gno

10.14 Kb · 310 lines
  1package core
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6
  7	"gno.land/p/nt/grc721/royalty/v0"
  8	"gno.land/p/nt/grc721/v0"
  9	"gno.land/p/nt/seqid/v0"
 10
 11	"gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/minhwa/safe/v1"
 12	"gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/minhwa/types/v1"
 13)
 14
 15// --- bound minter only -------------------------------------------------------
 16//
 17// A collection is created by an admin-approved minter realm and stays bound
 18// to it: only that realm can launch, abort, reserve and mint (I-5). The
 19// minter is trusted to pass the real creator; core trusts nothing else it
 20// is given and validates every field (I-13).
 21
 22// CreateCollection stores a Draft collection and returns its id. Nobody can
 23// mint a Draft, so a creator can configure phases across several messages
 24// without a race (OA B2).
 25func CreateCollection(cur realm, creator address, spec types.CollectionSpec) string {
 26	minter := cur.Previous().PkgPath()
 27	assertActiveMinter(minter)
 28	assertNotPaused("create")
 29	if !creator.IsValid() {
 30		panic("invalid creator address")
 31	}
 32	safe.AssertNonEmptyText("name", spec.Name, MaxNameLen)
 33	safe.AssertSymbol("symbol", spec.Symbol)
 34	safe.AssertText("description", spec.Description, MaxDescriptionLen)
 35	safe.AssertText("params", spec.Params, MaxParamsLen)
 36	if spec.MaxSupply <= 0 || spec.MaxSupply > MaxSupplyCap {
 37		panic("maxSupply must be 1.." + itoa(MaxSupplyCap))
 38	}
 39	safe.AssertBps("royaltyBps", spec.RoyaltyBps, RoyaltyCapBps)
 40	primary := safe.ParsePayees("primaryPayees", spec.PrimaryPayees, MaxPayees)
 41	royal := safe.ParsePayees("royaltyPayees", spec.RoyaltyPayees, MaxPayees)
 42
 43	rec := mustEngine(spec.EngineID)
 44	if !rec.claimed {
 45		panic("engine is not claimed by its author yet")
 46	}
 47	if rec.deprecated {
 48		panic("engine is deprecated")
 49	}
 50	// I-12 exception: the creator's own transaction runs the engine they chose.
 51	if err := rec.impl.ValidateParams(spec.Params); err != nil {
 52		panic("params rejected by engine: " + err.Error())
 53	}
 54
 55	collCount++
 56	cid := itoa(collCount)
 57
 58	// Each collection gets its own sub-realm identity, so Token.ID() and every
 59	// grc721 event carry "…/core#c/<cid>" (design §2.3; verified by
 60	// spike/subtoken and core tests on gno v1.7.0).
 61	tok, led := grc721.NewToken(spec.Name, spec.Symbol, seqid.ID(uint64(collCount)), cur.Sub("c/"+cid))
 62	st := &tokenState{}
 63	led.RegisterExtension(&lockExt{cid: cid, st: st})
 64	roy, royLed := royalty.NewRoyalty(led, RoyaltyCapBps)
 65	if spec.RoyaltyBps > 0 {
 66		if err := royLed.SetDefaultRoyalty(royaltyAddress(cid), spec.RoyaltyBps); err != nil {
 67			panic(err)
 68		}
 69	}
 70
 71	c := &collection{
 72		id:            cid,
 73		name:          spec.Name,
 74		symbol:        spec.Symbol,
 75		description:   spec.Description,
 76		creator:       creator,
 77		engineID:      spec.EngineID,
 78		params:        spec.Params,
 79		maxSupply:     spec.MaxSupply,
 80		nextTID:       1,
 81		royaltyBps:    spec.RoyaltyBps,
 82		primaryPayees: primary,
 83		royaltyPayees: royal,
 84		minter:        minter,
 85		state:         types.StateDraft,
 86		createdHeight: height(),
 87		token:         tok,
 88		ledger:        led,
 89		roy:           roy,
 90		st:            st,
 91	}
 92	collections.Set(collKey(cid), c)
 93	byCreator.Set(creator.String()+":"+collKey(cid), cid)
 94
 95	chain.Emit("CollectionCreated",
 96		"cid", cid, "creator", creator.String(), "minter", minter,
 97		"engine", spec.EngineID, "token", tok.ID())
 98	return cid
 99}
100
101// Launch freezes the configuration (I-6): sale window, reveal window and the
102// creator's commitment. The sale must start in the future so a beacon can
103// commit before the first mint.
104func Launch(cur realm, cid string, l types.LaunchSpec) {
105	c := mustCollection(cid)
106	assertBoundMinter(cur, c)
107	h := height()
108	if c.state != types.StateDraft {
109		panic("only a draft can be launched")
110	}
111	if l.StartHeight <= h {
112		panic("startHeight must be in the future")
113	}
114	if l.EndHeight < l.StartHeight || l.EndHeight-l.StartHeight > MaxSaleBlocks {
115		panic("sale window must be 0.." + itoa(MaxSaleBlocks) + " blocks")
116	}
117	if l.RevealWindow < RevealWindowMin || l.RevealWindow > RevealWindowMax {
118		panic("revealWindow must be " + itoa(RevealWindowMin) + ".." + itoa(RevealWindowMax) + " blocks")
119	}
120	commit := safe.AssertHex("creatorCommit", l.CreatorCommit, 32)
121	if usedCommits.Has(commit) {
122		panic("this commitment was already used; derive a new secret")
123	}
124	if l.UseBeacon && beacon == "" {
125		panic("no beacon is configured; launch without useBeacon")
126	}
127
128	usedCommits.Set(commit, cid)
129	c.prov = &provenance{creatorCommit: commit, acc: safe.InitialEntropy(runtime.ChainID(), cid)}
130	c.startHeight, c.endHeight, c.revealWindow = l.StartHeight, l.EndHeight, l.RevealWindow
131	c.useBeacon = l.UseBeacon
132	c.state = types.StateScheduled
133
134	chain.Emit("CollectionScheduled",
135		"cid", cid, "start", itoa(c.startHeight), "end", itoa(c.endHeight),
136		"revealWindow", itoa(c.revealWindow), "useBeacon", boolStr(c.useBeacon),
137		"creatorCommit", commit)
138}
139
140// AbortLaunch returns a launched collection to Draft while nothing has been
141// minted — the way out when a beacon never commits. Used commitments stay
142// used.
143func AbortLaunch(cur realm, cid string) {
144	c := mustCollection(cid)
145	assertBoundMinter(cur, c)
146	refresh(c, height())
147	if c.state == types.StateDraft || c.state == types.StateFinal {
148		panic("only a launched, unfinished collection can be aborted")
149	}
150	if c.minted() > 0 {
151		panic("tokens were minted; the launch can no longer be aborted")
152	}
153	c.prov = nil
154	c.startHeight, c.endHeight, c.revealWindow, c.closeHeight = 0, 0, 0, 0
155	c.useBeacon = false
156	c.state = types.StateDraft
157	chain.Emit("CollectionState", "cid", cid, "state", c.state.String())
158}
159
160// ReserveTo mints the creator's reserve before the sale starts. Reserve
161// mints feed the entropy like any mint, appear in Provenance, and do not
162// count toward wallet caps.
163func ReserveTo(cur realm, cid string, to address, n int64) (first, last int64) {
164	c := mustCollection(cid)
165	assertBoundMinter(cur, c)
166	assertNotPaused("mint")
167	h := height()
168	refresh(c, h)
169	if c.state != types.StateScheduled {
170		panic("reserve is only possible after launch and before the sale starts")
171	}
172	assertMintCount(c, n)
173	if c.reserved+n > c.maxSupply*MaxReserveBps/safe.BpsDenominator {
174		panic("reserve would exceed " + itoa(MaxReserveBps/100) + "% of supply")
175	}
176	first, last = mintBatchTo(c, to, n, h)
177	c.reserved += n
178	emitBatch("Reserved", c, to, first, last)
179	return first, last
180}
181
182// MintTo mints n tokens during the sale. The minter has already checked
183// phase, price, allowlist and wallet cap; core re-checks only what it owns:
184// state, supply, beacon commitment, and the lifetime counter (I-5, I-9).
185func MintTo(cur realm, cid string, to address, n int64) (first, last int64) {
186	c := mustCollection(cid)
187	assertBoundMinter(cur, c)
188	assertNotPaused("mint")
189	h := height()
190	refresh(c, h)
191	if c.state != types.StateMinting {
192		panic("collection is not minting (state " + c.state.String() + ")")
193	}
194	if c.useBeacon && c.prov.beaconCommit == "" {
195		panic("waiting for the beacon commitment; minting has not opened")
196	}
197	assertMintCount(c, n)
198	first, last = mintBatchTo(c, to, n, h)
199	c.mintedBy.Set(to.String(), mintedBy(c, to)+n)
200	if c.minted() == c.maxSupply {
201		c.closeHeight = h
202		c.state = types.StateClosed
203		chain.Emit("CollectionState", "cid", cid, "state", c.state.String())
204	}
205	emitBatch("MintBatch", c, to, first, last)
206	return first, last
207}
208
209func assertMintCount(c *collection, n int64) {
210	if n <= 0 || n > MaxMintPerTx {
211		panic("count must be 1.." + itoa(MaxMintPerTx))
212	}
213	if c.minted()+n > c.maxSupply {
214		panic("only " + itoa(c.maxSupply-c.minted()) + " tokens left")
215	}
216}
217
218// mintBatchTo is the only place tokens come into existence.
219func mintBatchTo(c *collection, to address, n, h int64) (first, last int64) {
220	first = c.nextTID
221	last = first + n - 1
222	for tid := first; tid <= last; tid++ {
223		if err := c.ledger.Mint(to, grc721.TokenID(itoa(tid))); err != nil {
224			panic(err)
225		}
226	}
227	c.nextTID = last + 1
228	c.st.addBatch(first, last, h)
229	c.prov.acc = safe.MixEntropy(c.prov.acc, to, h, first, last)
230	return first, last
231}
232
233func mintedBy(c *collection, addr address) int64 {
234	if v := c.mintedBy.Get(addr.String()); v != nil {
235		return v.(int64)
236	}
237	return 0
238}
239
240// CloseMint materializes Closed once the sale window has passed. Anyone may
241// call it; reveals and Finalize work without it.
242func CloseMint(cur realm, cid string) {
243	assertNoDeposit(cur)
244	c := mustCollection(cid)
245	refresh(c, height())
246	if c.state != types.StateClosed {
247		panic("the sale is not over (state " + c.state.String() + ")")
248	}
249}
250
251// --- creator -----------------------------------------------------------------
252
253// TransferCreator starts a two-step handover (RA M-11).
254func TransferCreator(cur realm, cid string, to address) {
255	assertNoDeposit(cur)
256	c := mustCollection(cid)
257	assertCreator(cur, c)
258	if !to.IsValid() {
259		panic("invalid address")
260	}
261	c.pendingCreator = to
262	chain.Emit("CreatorTransferStarted", "cid", cid, "from", c.creator.String(), "to", to.String())
263}
264
265func AcceptCreator(cur realm, cid string) {
266	assertNoDeposit(cur)
267	c := mustCollection(cid)
268	who := caller(cur)
269	if c.pendingCreator == "" || who != c.pendingCreator {
270		panic("no pending creator transfer to this address")
271	}
272	byCreator.Remove(c.creator.String() + ":" + collKey(cid))
273	byCreator.Set(who.String()+":"+collKey(cid), cid)
274	chain.Emit("CreatorTransferred", "cid", cid, "from", c.creator.String(), "to", who.String())
275	c.creator = who
276	c.pendingCreator = ""
277}
278
279// SetPayees changes the royalty split after Final. The primary-sale split is
280// frozen with the sale (I-6); the royalty receiver stays the collection's
281// sub-treasury, so third-party markets are unaffected.
282func SetPayees(cur realm, cid, payees string) {
283	assertNoDeposit(cur)
284	c := mustCollection(cid)
285	assertCreator(cur, c)
286	if c.state != types.StateFinal {
287		panic("royalty payees can change only after the collection is final")
288	}
289	c.royaltyPayees = safe.ParsePayees("royaltyPayees", payees, MaxPayees)
290	chain.Emit("RoyaltyPayees", "cid", cid, "payees", payees)
291}
292
293func assertCreator(cur realm, c *collection) {
294	if caller(cur) != c.creator {
295		panic("only the collection's creator can do this")
296	}
297}
298
299func assertBoundMinter(cur realm, c *collection) {
300	if cur.Previous().PkgPath() != c.minter {
301		panic("only the collection's bound minter (" + c.minter + ") can do this")
302	}
303}
304
305func boolStr(b bool) string {
306	if b {
307		return "true"
308	}
309	return "false"
310}