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.

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}