Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}