gno.land/r/g17khqpukees4237dtn3astzapmp462vjhsz6st4/settlers/nft⧉ Realm Open in gnoweb ↗
Kind Realm (renderable)
Name nft
Namespace g17khqpukees4237dtn3astzapmp462vjhsz6st4 / settlers
Exported functions 27
Module gno.land/r/g17khqpukees4237dtn3astzapmp462vjhsz6st4/settlers/nft
gno 0.9 ⧉
1 package nft 2 3 import ( 4 "chain" 5 "chain/runtime" 6 "crypto/merkle" 7 "crypto/sha256" 8 Admin () string
Approve (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, to string, tid string)
BalanceOf (owner string) (int64, interface {Error func() string})
Cap () int
ClearNotice (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
ExternalURL (tid string) string
GetApproved (tid string) (string, interface {Error func() string})
GetName () string
GetSymbol () string
ID () string
IsApprovedForAll (owner string, operator string) bool
MerkleRoot () string
Mint (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, proof string) string
OwnerOf (tid string) (string, interface {Error func() string})
Render (path string) string
SetApprovalForAll (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, operator string, approved bool)
SetNotice (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, alertType string, msg string)
SetSnapshot (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, rootHex string, count int)
Token () interface {Approve func(int, .uverse.realm, .uverse.address, gno.land/p/nt/grc721/v0.TokenID) .uverse.error; BalanceOf func(.uverse.address) (int64, .uverse.error); GetApproved func(gno.land/p/nt/grc721/v0.TokenID) (.uverse.address, .uverse.error); GetName func() string; GetSymbol func() string; ID func() string; IsApprovedForAll func(.uverse.address, .uverse.address) bool; OwnerOf func(gno.land/p/nt/grc721/v0.TokenID) (.uverse.address, .uverse.error); SetApprovalForAll func(int, .uverse.realm, .uverse.address, bool) .uverse.error; TotalSupply func() int64; TransferFrom func(int, .uverse.realm, .uverse.address, .uverse.address, gno.land/p/nt/grc721/v0.TokenID) .uverse.error}
TokenMetadata (tid string) (string, interface {Error func() string})
TokenOf (addr string) string
TokenURI (tid string) (string, interface {Error func() string})
Total () int
TotalSupply () int64
TraitsOf (tid string) (struct{Skin uint8; Hat uint8; BeardColor uint8; Tunic uint8; Background uint8; HatShape uint8; Flip uint8; Glasses uint8; Beard uint8}, interface {Error func() string})
TransferAdmin (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, newAdmin string)
TransferFrom (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, from string, to string, tid string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
"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
22 const (
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.
41 var ErrSoulbound = errors.New( "settlers are soulbound, they can't be transferred or approved" )
42
43 var (
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
86 func 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.
95 type soulbound struct {}
96
97 func (soulbound) ExtensionKind() string { return "soulbound" }
98 func (soulbound) OnMint(address, grc721.TokenID) {}
99 func (soulbound) OnTransfer(address, address, grc721.TokenID) { panic(ErrSoulbound) }
100 func (soulbound) OnBurn(grc721.TokenID) {}
101
102 // newCollection creates the GRC721 token of the Settlers.
103 func 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.
114 func 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.
141 func Admin() address { return admin.Owner() }
142
143 // TransferAdmin makes another account the admin. Only the admin can call it.
144 func 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.
152 func 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.
163 func 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.
177 func 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.
239 func Cap() int { return supplyCap }
240
241 // MerkleRoot returns the Merkle root of the list of eligible accounts, in hex.
242 func MerkleRoot() string { return hex.EncodeToString(root) }
243
244 // Total returns the number of eligible accounts in the list.
245 func Total() int { return total }
246
247 // TokenOf returns the ID of the Settler of an account, or an empty ID if it has none.
248 func 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.
257 func 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.
266 func 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.
280 func 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").
285 func 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
293 func newTraitsFilter() *bloom.Bloom {
294 m := uint64 ( 28756 ) // bits
295 k := uint64 ( 7 ) // hashes
296 return bloom.NewWithMK(m, k)
297 }
298
299 func generateSeed(caller address, settlerID, nonce int ) string {
300 return hashSeed(seedPrefix(caller, settlerID), nonce)
301 }
302
303 func seedPrefix(caller address, settlerID int ) string {
304 return strconv.FormatInt(time.Now().UnixNano(), 10 ) + ":" + caller.String() +
305 ":" + strconv.Itoa(settlerID) + ":"
306 }
307
308 func hashSeed(prefix string , nonce int ) string {
309 sum := sha256.Sum256([] byte (prefix + strconv.Itoa(nonce)))
310 return hex.EncodeToString(sum[:])
311 }
312
313 func 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
331 func makeKey(settlerID int ) string {
332 s := strconv.Itoa(settlerID)
333 return strings.Repeat( "0" , 5 -len(s)) + s
334 }
335
336 func assertIsCurrent(cur realm) {
337 if !cur.IsCurrent() {
338 panic( "unauthorized" )
339 }
340 }
341
342 func 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 }
350
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.