Search Apps Documentation Source Content File Folder Download Copy Actions Download

collection.gno

5.37 Kb · 182 lines
  1package memba_nft_v2
  2
  3import (
  4	"chain"
  5	"chain/runtime/unsafe"
  6	"math/overflow"
  7	"strconv"
  8
  9	"gno.land/p/samcrew/grc721"
 10	"gno.land/p/nt/avl/v0"
 11)
 12
 13// AdminAddress is the Samouraï 2-of-2 multisig (samcrew-core-test1).
 14// Root of trust: mint authority + market registration + pause.
 15const AdminAddress = "g1x7k4628w93a7wzdhqc06atzx0v50rnshweuxu0"
 16
 17const MaxRoyaltyBPS = 1000 // 10% ceiling
 18
 19// collectionID -> *collection
 20var collections = avl.NewTree()
 21
 22// registered marketplace realm addresses allowed to call MarketTransfer.
 23// Adding one is a DRAIN KEY: multisig-only, event-logged.
 24var registeredMarkets = avl.NewTree() // addr.String() -> bool
 25
 26var paused bool // global pause
 27
 28// membaNFT is the local composite interface the unexported *grc721.royaltyNFT
 29// satisfies structurally (grc721 exposes no writer interface on test13).
 30type membaNFT interface {
 31	Name() string
 32	Symbol() string
 33	TokenCount() int64
 34	BalanceOf(addr address) (int64, error)
 35	OwnerOf(tid grc721.TokenID) (address, error)
 36	GetApproved(tid grc721.TokenID) (address, error)
 37	IsApprovedForAll(owner, operator address) bool
 38	TokenURI(tid grc721.TokenID) (string, error)
 39	Mint(to address, tid grc721.TokenID) error
 40	Approve(caller, to address, tid grc721.TokenID) error
 41	SetApprovalForAll(caller, operator address, approved bool) error
 42	TransferFrom(caller, from, to address, tid grc721.TokenID) error
 43	SetTokenURI(caller address, tid grc721.TokenID, tURI grc721.TokenURI) (bool, error)
 44	RoyaltyInfo(tid grc721.TokenID, salePrice int64) (address, int64, error)
 45	SetTokenRoyalty(caller address, tid grc721.TokenID, info grc721.RoyaltyInfo) error
 46}
 47
 48type collection struct {
 49	nft           membaNFT        // basic+metadata+royalty composite (internal ledger)
 50	admin         address         // per-collection admin (v1: AdminAddress)
 51	royaltyRecip  address         // MUTABLE
 52	royaltyBPS    int64           // <= MaxRoyaltyBPS
 53	phase         int             // 0=curated/multisig-only,1=allowlist,2=public (v1: 0)
 54	allowlistRoot string
 55	mintPrice     int64
 56	maxSupply     int64 // 0 = unlimited
 57	maxPerWallet  int64
 58	paused        bool // per-collection pause
 59}
 60
 61func mustGet(id string) *collection {
 62	v, ok := collections.Get(id)
 63	if !ok {
 64		panic("collection not found: " + id)
 65	}
 66	return v.(*collection)
 67}
 68
 69func assertAdmin() {
 70	if unsafe.PreviousRealm().Address() != address(AdminAddress) {
 71		panic("admin only")
 72	}
 73}
 74
 75func assertNotPaused(c *collection) {
 76	if paused || c.paused {
 77		panic("paused")
 78	}
 79}
 80
 81func itoa(n int64) string { return strconv.FormatInt(n, 10) }
 82
 83// ── Group 1: CreateCollection, SetRoyalty, RoyaltyInfo ───────────────────────
 84
 85func CreateCollection(cur realm, id, name, symbol string, royaltyBPS int64, royaltyRecip address, maxSupply, maxPerWallet int64) {
 86	assertAdmin()
 87	if _, ok := collections.Get(id); ok {
 88		panic("collection exists: " + id)
 89	}
 90	if royaltyBPS > MaxRoyaltyBPS {
 91		panic("royalty exceeds max")
 92	}
 93	c := &collection{
 94		nft:          grc721.NewNFTWithRoyalty(0, cur, name, symbol),
 95		admin:        address(AdminAddress),
 96		royaltyRecip: royaltyRecip,
 97		royaltyBPS:   royaltyBPS,
 98		maxSupply:    maxSupply,
 99		maxPerWallet: maxPerWallet,
100	}
101	collections.Set(id, c)
102	chain.Emit("CollectionCreated",
103		"collection", id,
104		"name", name,
105		"symbol", symbol,
106		"royaltyBPS", itoa(royaltyBPS),
107		"royaltyRecipient", royaltyRecip.String(),
108	)
109}
110
111func SetRoyalty(cur realm, id string, recip address, bps int64) {
112	assertAdmin()
113	if bps > MaxRoyaltyBPS {
114		panic("royalty exceeds max")
115	}
116	c := mustGet(id)
117	c.royaltyRecip, c.royaltyBPS = recip, bps
118	chain.Emit("RoyaltyChanged",
119		"collection", id,
120		"royaltyBPS", itoa(bps),
121		"recipient", recip.String(),
122	)
123}
124
125// RoyaltyInfo returns the collection-level royalty recipient and amount for a
126// given sale price. It uses the collection's BPS rate, not per-token royalty.
127func RoyaltyInfo(id string, tid grc721.TokenID, salePrice int64) (address, int64) {
128	c := mustGet(id)
129	return c.royaltyRecip, overflow.Mul64p(salePrice, c.royaltyBPS) / 10000
130}
131
132// ── Group 2: Mint ─────────────────────────────────────────────────────────────
133
134func Mint(cur realm, id string, to address, tid grc721.TokenID, tokenURI string) {
135	assertAdmin()
136	c := mustGet(id)
137	if c.maxSupply > 0 && c.nft.TokenCount() >= c.maxSupply {
138		panic("max supply reached")
139	}
140	if err := c.nft.Mint(to, tid); err != nil {
141		panic(err.Error())
142	}
143	if tokenURI != "" {
144		// SetTokenURI checks caller == owner; after Mint, owner == to.
145		if _, err := c.nft.SetTokenURI(to, tid, grc721.TokenURI(tokenURI)); err != nil {
146			panic(err.Error())
147		}
148	}
149	chain.Emit("Mint",
150		"collection", id,
151		"to", to.String(),
152		"tokenId", string(tid),
153	)
154}
155
156// ── Group 6 additions: pause fns ─────────────────────────────────────────────
157
158func PauseCollection(cur realm, id string) {
159	assertAdmin()
160	mustGet(id).paused = true
161	chain.Emit("CollectionPaused", "collection", id)
162}
163
164func UnpauseCollection(cur realm, id string) {
165	assertAdmin()
166	mustGet(id).paused = false
167	chain.Emit("CollectionUnpaused", "collection", id)
168}
169
170func Pause(cur realm) {
171	assertAdmin()
172	paused = true
173	chain.Emit("Paused")
174}
175
176func Unpause(cur realm) {
177	assertAdmin()
178	paused = false
179	chain.Emit("Unpaused")
180}
181
182func IsPaused() bool { return paused }