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 }