nft.gno
9.48 Kb · 349 lines
1package nft
2
3import (
4 "chain"
5 "chain/runtime"
6 "crypto/merkle"
7 "crypto/sha256"
8 "encoding/hex"
9 "errors"
10 "strconv"
11 "strings"
12 "time"
13
14 "gno.land/p/g17khqpukees4237dtn3astzapmp462vjhsz6st4/bloom"
15 "gno.land/p/g17khqpukees4237dtn3astzapmp462vjhsz6st4/settlers/nft/v0"
16 "gno.land/p/nt/bptree/v0"
17 "gno.land/p/nt/grc721/v0"
18 "gno.land/p/nt/mdalert/v0"
19 "gno.land/p/nt/ownable/v0"
20)
21
22const (
23 // Name is the NFT token's name.
24 Name = "Settlers"
25
26 // Symbol is the NFT token's symbol.
27 Symbol = "SETTLER"
28
29 // Capacity is the total number of Settlers that can be minted.
30 capacity = 3000
31
32 // Max nonce is how many gnomes are tried for a token before giving up,
33 // when the ones that come out are already taken.
34 maxNonce = 64
35
36 // Max aunts is the longest proof accepted.
37 maxAunts = 40
38)
39
40// ErrSoulbound is returned for operation that are not allowed on a soulbound NFT.
41var ErrSoulbound = errors.New("settlers are soulbound, they can't be transferred or approved")
42
43var (
44 // Realm path is the path of this realm.
45 RealmPath string
46
47 // Notice is a realm-wide message the admin can post at any time, shown at the top of the page.
48 // It never affects SVG, URI or Markdown, it is purely informational.
49 Notice string
50
51 // NoticeType is the style Notice is shown as.
52 NoticeType mdalert.Type
53
54 // Admin account for the realm.
55 // This account is only valid to set snapshot params, once setted minting
56 // can start and after the first NFT is minted the account is not allowed to change
57 // snapshot params anymore.
58 // It is always allowed to post or clear realm-wide notices.
59 admin *ownable.Ownable
60
61 // Supply cap is the number of Settlers that can be minted.
62 supplyCap = capacity
63
64 // Root is the Merkle root value.
65 root []byte
66
67 // Total is the total number of leaves of the list of elegible accounts.
68 total int
69
70 // Collection is the GRC721 token, it keeps the owners and balances and emits the standard events.
71 collection *grc721.Token
72
73 // Ledger is the writable side of the collection, only used to mint.
74 ledger *grc721.PrivateLedger
75
76 // Tokens has the packed traits of every token, by ID (zero-padded, so it is in mint order).
77 tokens = bptree.NewBPTree32()
78
79 // Claimed has the ID of the token for each account.
80 claimed = bptree.NewBPTree32()
81
82 // Seen traits has the packed traits of minted tokens to check that two Settlers are never the same.
83 seenTraits = newTraitsFilter()
84)
85
86func init(cur realm) {
87 RealmPath = strings.TrimPrefix(cur.PkgPath(), runtime.ChainDomain())
88 admin = ownable.NewWithAddress(cur.Previous().Address())
89 collection, ledger = newCollection(cur)
90}
91
92// Soulbound is a GRC721 extension that aborts any transfer of a Settler.
93// The realm never exposes a teller that can write, so a transfer can't be started,
94// this makes sure that it could never finish either.
95type soulbound struct{}
96
97func (soulbound) ExtensionKind() string { return "soulbound" }
98func (soulbound) OnMint(address, grc721.TokenID) {}
99func (soulbound) OnTransfer(address, address, grc721.TokenID) { panic(ErrSoulbound) }
100func (soulbound) OnBurn(grc721.TokenID) {}
101
102// newCollection creates the GRC721 token of the Settlers.
103func newCollection(cur realm) (*grc721.Token, *grc721.PrivateLedger) {
104 tok, led := grc721.NewToken(Name, Symbol, 0, cur)
105 led.RegisterExtension(soulbound{})
106 return tok, led
107}
108
109// SetSnapshot opens the mint.
110// It sets the Merkle root of the list of eligible accounts, in hex, and the
111// number of accounts in the list. Only the owner can call it.
112// It can be called again to correct a mistake until the first Settler is minted,
113// from then on the list is frozen.
114func SetSnapshot(cur realm, rootHex string, count int) {
115 assertIsCurrent(cur)
116 admin.AssertOwnedBy(cur.Previous().Address())
117
118 if tokens.Size() > 0 {
119 panic("the snapshot is frozen: Settlers were already minted")
120 }
121
122 bz, err := hex.DecodeString(rootHex)
123 if err != nil || len(bz) != 32 {
124 panic("the root must be 32 bytes in hex")
125 }
126
127 if count < 1 {
128 panic("the list must have at least one account")
129 }
130
131 root = bz
132 total = count
133 chain.Emit(
134 "SnapshotSet",
135 "root", rootHex,
136 "total", strconv.Itoa(count),
137 )
138}
139
140// Admin returns the address of the admin account.
141func Admin() address { return admin.Owner() }
142
143// TransferAdmin makes another account the admin. Only the admin can call it.
144func TransferAdmin(cur realm, newAdmin address) {
145 assertIsCurrent(cur)
146 if err := admin.TransferOwnership(0, cur, newAdmin); err != nil {
147 panic(err)
148 }
149}
150
151// SetNotice posts a realm-wide message, shown at the top of the page.
152func SetNotice(cur realm, alertType mdalert.Type, msg string) {
153 assertIsCurrent(cur)
154 admin.AssertOwnedBy(cur.Previous().Address())
155 assertIsValidAlertType(alertType)
156
157 NoticeType = alertType
158 Notice = msg
159 chain.Emit("NoticeSet", "type", string(alertType))
160}
161
162// ClearNotice removes the current notice.
163func ClearNotice(cur realm) {
164 assertIsCurrent(cur)
165 admin.AssertOwnedBy(cur.Previous().Address())
166
167 NoticeType = ""
168 Notice = ""
169 chain.Emit("NoticeCleared")
170}
171
172// Mint mints a Settler to the caller and returns its ID.
173//
174// The proof shows that the caller is in the list of eligible accounts.
175// Proof format is "<index>.<aunts>".
176// Only user accounts can mint, either with a call or a run transaction, realms can't.
177func Mint(cur realm, proof string) grc721.TokenID {
178 assertIsCurrent(cur)
179 if !cur.Previous().IsUser() {
180 panic("only user accounts can mint")
181 }
182
183 if len(root) == 0 {
184 panic("the mint is not open yet")
185 }
186
187 minted := tokens.Size()
188 if minted >= supplyCap {
189 panic("sold out")
190 }
191
192 caller := cur.Previous().Address()
193 if claimed.Has(caller.String()) {
194 panic("this account has already minted")
195 }
196
197 index, aunts, err := splitProof(proof)
198 if err != "" {
199 panic(err)
200 }
201
202 if !merkle.VerifySimpleProof(root, []byte(caller), index, total, aunts) {
203 panic("the proof is not valid for this account: " + caller.String())
204 }
205
206 settlerID := minted + 1
207 prefix := seedPrefix(caller, settlerID)
208
209 var (
210 packed uint32
211 packedKey string
212 )
213 for nonce := 0; ; nonce++ {
214 if nonce == maxNonce {
215 panic("could not find a new gnome, try again")
216 }
217
218 seed := hashSeed(prefix, nonce)
219 traits := nft.NewTraits(seed)
220 packed = nft.Pack(traits.Normalized())
221 packedKey = strconv.FormatUint(uint64(packed), 10)
222 if !seenTraits.ContainsString(packedKey) {
223 break
224 }
225 }
226
227 tid := tokenID(settlerID)
228 if err := ledger.Mint(caller, tid); err != nil {
229 panic(err)
230 }
231
232 tokens.Set(makeKey(settlerID), packed)
233 seenTraits.AddString(packedKey)
234 claimed.Set(caller.String(), tid)
235 return tid
236}
237
238// Cap returns the number of Settlers that can be minted.
239func Cap() int { return supplyCap }
240
241// MerkleRoot returns the Merkle root of the list of eligible accounts, in hex.
242func MerkleRoot() string { return hex.EncodeToString(root) }
243
244// Total returns the number of eligible accounts in the list.
245func Total() int { return total }
246
247// TokenOf returns the ID of the Settler of an account, or an empty ID if it has none.
248func TokenOf(addr address) grc721.TokenID {
249 v := claimed.Get(addr.String())
250 if v == nil {
251 return ""
252 }
253 return v.(grc721.TokenID)
254}
255
256// TraitsOf returns the traits of a Settler.
257func TraitsOf(tid grc721.TokenID) (nft.Traits, error) {
258 packed, ok := traitsOf(tid)
259 if !ok {
260 return nft.Traits{}, grc721.ErrInvalidTokenId
261 }
262 return nft.Unpack(packed), nil
263}
264
265// traitsOf returns the packed traits of a Settler, or false if the ID is not a minted Settler.
266func traitsOf(tid grc721.TokenID) (uint32, bool) {
267 settlerID, ok := parseTokenID(tid)
268 if !ok {
269 return 0, false
270 }
271
272 v := tokens.Get(makeKey(settlerID))
273 if v == nil {
274 return 0, false
275 }
276 return v.(uint32), true
277}
278
279// tokenID returns the GRC721 ID of a Settler number.
280func tokenID(settlerID int) grc721.TokenID {
281 return grc721.TokenID(strconv.Itoa(settlerID))
282}
283
284// parseTokenID returns the Settler number of an ID, only for the canonical form ("1", not "01" or "+1").
285func parseTokenID(tid grc721.TokenID) (int, bool) {
286 settlerID, err := strconv.Atoi(string(tid))
287 if err != nil || settlerID < 1 || strconv.Itoa(settlerID) != string(tid) {
288 return 0, false
289 }
290 return settlerID, true
291}
292
293func newTraitsFilter() *bloom.Bloom {
294 m := uint64(28756) // bits
295 k := uint64(7) // hashes
296 return bloom.NewWithMK(m, k)
297}
298
299func generateSeed(caller address, settlerID, nonce int) string {
300 return hashSeed(seedPrefix(caller, settlerID), nonce)
301}
302
303func seedPrefix(caller address, settlerID int) string {
304 return strconv.FormatInt(time.Now().UnixNano(), 10) + ":" + caller.String() +
305 ":" + strconv.Itoa(settlerID) + ":"
306}
307
308func hashSeed(prefix string, nonce int) string {
309 sum := sha256.Sum256([]byte(prefix + strconv.Itoa(nonce)))
310 return hex.EncodeToString(sum[:])
311}
312
313func splitProof(proof string) (int, []byte, string) {
314 dot := strings.Index(proof, ".")
315 if dot < 1 {
316 return 0, nil, "the proof must be <index>.<aunts>"
317 }
318
319 index, err := strconv.Atoi(proof[:dot])
320 if err != nil || index < 0 || index >= total {
321 return 0, nil, "the index of the proof is not valid"
322 }
323
324 aunts, err := hex.DecodeString(proof[dot+1:])
325 if err != nil || len(aunts)%32 != 0 || len(aunts) > 32*maxAunts {
326 return 0, nil, "the aunts of the proof must be hashes of 32 bytes in hex"
327 }
328 return index, aunts, ""
329}
330
331func makeKey(settlerID int) string {
332 s := strconv.Itoa(settlerID)
333 return strings.Repeat("0", 5-len(s)) + s
334}
335
336func assertIsCurrent(cur realm) {
337 if !cur.IsCurrent() {
338 panic("unauthorized")
339 }
340}
341
342func assertIsValidAlertType(t mdalert.Type) {
343 switch t {
344 case mdalert.TypeCaution, mdalert.TypeInfo, mdalert.TypeNote,
345 mdalert.TypeSuccess, mdalert.TypeTip, mdalert.TypeWarning:
346 return
347 }
348 panic("invalid notice type")
349}