Transaction

53E7F9D6111B16…D1289C267E1A

Block 321,044 · index 0 · indexed

Summary

Hash
53E7F9D6111B16E4DA0E01621992F58ABE675BEDF525E74654D2D1289C267E1A
Block
321,044
Size
21628 bytes
Gas used
25,819,785 / 250,000,000
Fee
250000 ugnot
Status
success

Messages

Arguments · 15

  1. #1nft
  2. #2README.md
  3. #3# Gno.land Settlers Settlers are small pixel-art gnomes, and each one belongs to a single Gno.land account, forever. A Settler can't be sold, traded, or given to anyone else. Once it's yours, it stays yours. ## Why they exist Settlers are a thank-you to the people who were part of Gno.land from the very beginning. Anyone whose account already held at least 15 GNOT when the `gnoland-1` chain launched can claim one Settler. It's a simple, permanent way of saying "this account was here at the start." ## No two are alike Nobody picks how their Settler looks. Its skin tone, hat, beard, clothes, glasses and background are all worked out automatically from the account that claims it and the moment it's claimed, and the collection makes sure the same combination is never drawn twice. So every Settler is one of a kind, and it's a little surprise to see which one you get. ## One per account Each eligible account can claim exactly one Settler, on a first-come, first-served basis, until all of them are gone (there are 3,000 in total). There's no cost beyond the usual cost of sending a transaction. ## How to get one If your account was eligible, you can claim your Settler from the mint page at [jeronimoalbi.github.io/gnoland-settlers](https://jeronimoalbi.github.io/gnoland-settlers/). Once claimed, you can look up any Settler, and see who it belongs to, right here in this realm. The mint page is also the quickest way to get the proof for the [`Mint`](/r/g17khqpukees4237dtn3astzapmp462vjhsz6st4/settlers/nft$help&func=Mint) call, type your address there and it shows both the proof and the exact `gnokey` command, ready to copy. ## A friendly word of caution This project is made by one person, for fun. It has not been audited by anyone. Everything here is open source, so please have a look yourself, or ask someone you trust to, before connecting a wallet or signing anything. Nobody involved in this project is responsible for what happens to your account or your funds; using it is entirely your own choice and your own responsibility. ## Minting without the web page If you'd rather not use the mint page at all, or just want to see everything for yourself first, you can do the whole thing by hand with a terminal. Nothing here needs trusting a website: the list of eligible accounts and your proof are both worked out from the public genesis of `gnoland-1`, and you can check every step following the instructions you can find in [jeronimoalbi/gnoland-settlers](https://github.com/jeronimoalbi/gnoland-settlers) repository. ## See your Settler as a picture Once you have your Settler, a separate [avatar realm](/r/g17khqpukees4237dtn3astzapmp462vjhsz6st4/settlers/avatar) can draw it as a picture whenever it's asked to, so you, or any other realm or web app, can show it next to your address, as profile picture, in forum post, a leaderboard row, wherever you like.
  4. #4gnomod.toml
  5. #5module = "gno.land/r/g17khqpukees4237dtn3astzapmp462vjhsz6st4/settlers/nft" gno = "0.9"
  6. #6grc721.gno
  7. #7package nft import "gno.land/p/nt/grc721/v0" // Token returns the collection as a read-only GRC721 teller. func Token() grc721.Teller { return collection.ReadonlyTeller() } // GetName returns the name of the collection. func GetName() string { return collection.GetName() } // GetSymbol returns the symbol of the collection. func GetSymbol() string { return collection.GetSymbol() } // ID returns the ID of the collection. func ID() string { return collection.ID() } // TotalSupply returns the number of Settlers minted so far. func TotalSupply() int64 { return collection.TotalSupply() } // BalanceOf returns the number of Settlers of an account, 0 or 1. func BalanceOf(owner address) (int64, error) { return collection.BalanceOf(owner) } // OwnerOf returns the owner account of a Settler. func OwnerOf(tid grc721.TokenID) (address, error) { return collection.OwnerOf(tid) } // GetApproved returns the account allowed to move a Settler on behalf of its owner. // Settlers are soulbound, so it always fails. func GetApproved(tid grc721.TokenID) (address, error) { return collection.GetApproved(tid) } // IsApprovedForAll reports whether an account is allowed to move the Settler of another account. // Settlers are soulbound, so it is always false. func IsApprovedForAll(owner, operator address) bool { return collection.IsApprovedForAll(owner, operator) } // Approve always fails, Settlers are soulbound. func Approve(cur realm, to address, tid grc721.TokenID) { panic(ErrSoulbound) } // SetApprovalForAll always fails, Settlers are soulbound. func SetApprovalForAll(cur realm, operator address, approved bool) { panic(ErrSoulbound) } // TransferFrom always fails, Settlers are soulbound. func TransferFrom(cur realm, from, to address, tid grc721.TokenID) { panic(ErrSoulbound) }
  8. #8metadata.gno
  9. #9package nft import ( "chain/runtime" "gno.land/p/g17khqpukees4237dtn3astzapmp462vjhsz6st4/settlers/nft/v0" "gno.land/p/nt/grc721/v0" ) // TokenURI returns the Settler image as an SVG data URI. // Wallets such as Adena read TokenURI directly as the token image, // so this must be the image itself. The metadata lives in TokenMetadata. func TokenURI(tid grc721.TokenID) (string, error) { traits, err := TraitsOf(tid) if err != nil { return "", err } return nft.RenderDataURI(traits, nft.Options{Size: nft.CanonicalSize}), nil } // TokenMetadata returns a JSON string with the metadata of a Settler // in the format the Adena wallet can understand. func TokenMetadata(tid grc721.TokenID) (string, error) { traits, err := TraitsOf(tid) if err != nil { return "", err } var attrs string for i, attr := range traits.Attributes() { if i > 0 { attrs += "," } attrs += `{"traitType":"` + attr.Type + `","value":"` + attr.Value + `"}` } return `{"name":"Settler #` + string(tid) + `",` + `"description":"Soulbound gnome of the early settlers of Gno.land. ` + nft.Credits + `",` + `"image":"` + nft.RenderDataURI(traits, nft.Options{Size: nft.CanonicalSize}) + `",` + `"externalUrl":"` + ExternalURL(tid) + `",` + `"attributes":[` + attrs + `]}`, nil } // ExternalURL returns the URL to a Settler profile page. func ExternalURL(tid grc721.TokenID) string { return "https://" + runtime.ChainDomain() + RealmPath + ":token/" + string(tid) }
  10. #10nft.gno
  11. #11package nft import ( "chain" "chain/runtime" "crypto/merkle" "crypto/sha256" "encoding/hex" "errors" "strconv" "strings" "time" "gno.land/p/g17khqpukees4237dtn3astzapmp462vjhsz6st4/bloom/v0" "gno.land/p/g17khqpukees4237dtn3astzapmp462vjhsz6st4/settlers/nft/v0" "gno.land/p/nt/bptree/v0" "gno.land/p/nt/grc721/v0" "gno.land/p/nt/mdalert/v0" "gno.land/p/nt/ownable/v0" ) const ( // Name is the NFT token's name. Name = "Settlers" // Symbol is the NFT token's symbol. Symbol = "SETTLER" // Capacity is the total number of Settlers that can be minted. capacity = 3000 // Max nonce is how many gnomes are tried for a token before giving up, // when the ones that come out are already taken. maxNonce = 64 // Max aunts is the longest proof accepted. maxAunts = 40 ) // ErrSoulbound is returned for operation that are not allowed on a soulbound NFT. var ErrSoulbound = errors.New("settlers are soulbound, they can't be transferred or approved") var ( // Realm path is the path of this realm. RealmPath string // Notice is a realm-wide message the admin can post at any time, shown at the top of the page. // It never affects SVG, URI or Markdown, it is purely informational. Notice string // NoticeType is the style Notice is shown as. NoticeType mdalert.Type // Admin account for the realm. // This account is only valid to set snapshot params, once setted minting // can start and after the first NFT is minted the account is not allowed to change // snapshot params anymore. // It is always allowed to post or clear realm-wide notices. admin *ownable.Ownable // Supply cap is the number of Settlers that can be minted. supplyCap = capacity // Root is the Merkle root value. root []byte // Total is the total number of leaves of the list of elegible accounts. total int // Collection is the GRC721 token, it keeps the owners and balances and emits the standard events. collection *grc721.Token // Ledger is the writable side of the collection, only used to mint. ledger *grc721.PrivateLedger // Tokens has the packed traits of every token, by ID (zero-padded, so it is in mint order). tokens = bptree.NewBPTree32() // Claimed has the ID of the token for each account. claimed = bptree.NewBPTree32() // Seen traits has the packed traits of minted tokens to check that two Settlers are never the same. seenTraits = newTraitsFilter() ) func init(cur realm) { RealmPath = strings.TrimPrefix(cur.PkgPath(), runtime.ChainDomain()) admin = ownable.NewWithAddress(cur.Previous().Address()) collection, ledger = newCollection(cur) } // Soulbound is a GRC721 extension that aborts any transfer of a Settler. // The realm never exposes a teller that can write, so a transfer can't be started, // this makes sure that it could never finish either. type soulbound struct{} func (soulbound) ExtensionKind() string { return "soulbound" } func (soulbound) OnMint(address, grc721.TokenID) {} func (soulbound) OnTransfer(address, address, grc721.TokenID) { panic(ErrSoulbound) } func (soulbound) OnBurn(grc721.TokenID) {} // newCollection creates the GRC721 token of the Settlers. func newCollection(cur realm) (*grc721.Token, *grc721.PrivateLedger) { tok, led := grc721.NewToken(Name, Symbol, 0, cur) led.RegisterExtension(soulbound{}) return tok, led } // SetSnapshot opens the mint. // It sets the Merkle root of the list of eligible accounts, in hex, and the // number of accounts in the list. Only the owner can call it. // It can be called again to correct a mistake until the first Settler is minted, // from then on the list is frozen. func SetSnapshot(cur realm, rootHex string, count int) { assertIsCurrent(cur) admin.AssertOwnedBy(cur.Previous().Address()) if tokens.Size() > 0 { panic("the snapshot is frozen: Settlers were already minted") } bz, err := hex.DecodeString(rootHex) if err != nil || len(bz) != 32 { panic("the root must be 32 bytes in hex") } if count < 1 { panic("the list must have at least one account") } root = bz total = count chain.Emit( "SnapshotSet", "root", rootHex, "total", strconv.Itoa(count), ) } // Admin returns the address of the admin account. func Admin() address { return admin.Owner() } // TransferAdmin makes another account the admin. Only the admin can call it. func TransferAdmin(cur realm, newAdmin address) { assertIsCurrent(cur) if err := admin.TransferOwnership(0, cur, newAdmin); err != nil { panic(err) } } // SetNotice posts a realm-wide message, shown at the top of the page. func SetNotice(cur realm, alertType mdalert.Type, msg string) { assertIsCurrent(cur) admin.AssertOwnedBy(cur.Previous().Address()) assertIsValidAlertType(alertType) NoticeType = alertType Notice = msg chain.Emit("NoticeSet", "type", string(alertType)) } // ClearNotice removes the current notice. func ClearNotice(cur realm) { assertIsCurrent(cur) admin.AssertOwnedBy(cur.Previous().Address()) NoticeType = "" Notice = "" chain.Emit("NoticeCleared") } // Mint mints a Settler to the caller and returns its ID. // // The proof shows that the caller is in the list of eligible accounts. // Proof format is "<index>.<aunts>". // Only user accounts can mint, either with a call or a run transaction, realms can't. func Mint(cur realm, proof string) grc721.TokenID { assertIsCurrent(cur) if !cur.Previous().IsUser() { panic("only user accounts can mint") } if len(root) == 0 { panic("the mint is not open yet") } minted := tokens.Size() if minted >= supplyCap { panic("sold out") } caller := cur.Previous().Address() if claimed.Has(caller.String()) { panic("this account has already minted") } index, aunts, err := splitProof(proof) if err != "" { panic(err) } if !merkle.VerifySimpleProof(root, []byte(caller), index, total, aunts) { panic("the proof is not valid for this account: " + caller.String()) } settlerID := minted + 1 prefix := seedPrefix(caller, settlerID) var ( packed uint32 packedKey string ) for nonce := 0; ; nonce++ { if nonce == maxNonce { panic("could not find a new gnome, try again") } seed := hashSeed(prefix, nonce) traits := nft.NewTraits(seed) packed = nft.Pack(traits.Normalized()) packedKey = strconv.FormatUint(uint64(packed), 10) if !seenTraits.ContainsString(packedKey) { break } } tid := tokenID(settlerID) if err := ledger.Mint(caller, tid); err != nil { panic(err) } tokens.Set(makeKey(settlerID), packed) seenTraits.AddString(packedKey) claimed.Set(caller.String(), tid) return tid } // Cap returns the number of Settlers that can be minted. func Cap() int { return supplyCap } // MerkleRoot returns the Merkle root of the list of eligible accounts, in hex. func MerkleRoot() string { return hex.EncodeToString(root) } // Total returns the number of eligible accounts in the list. func Total() int { return total } // TokenOf returns the ID of the Settler of an account, or an empty ID if it has none. func TokenOf(addr address) grc721.TokenID { v := claimed.Get(addr.String()) if v == nil { return "" } return v.(grc721.TokenID) } // TraitsOf returns the traits of a Settler. func TraitsOf(tid grc721.TokenID) (nft.Traits, error) { packed, ok := traitsOf(tid) if !ok { return nft.Traits{}, grc721.ErrInvalidTokenId } return nft.Unpack(packed), nil } // traitsOf returns the packed traits of a Settler, or false if the ID is not a minted Settler. func traitsOf(tid grc721.TokenID) (uint32, bool) { settlerID, ok := parseTokenID(tid) if !ok { return 0, false } v := tokens.Get(makeKey(settlerID)) if v == nil { return 0, false } return v.(uint32), true } // tokenID returns the GRC721 ID of a Settler number. func tokenID(settlerID int) grc721.TokenID { return grc721.TokenID(strconv.Itoa(settlerID)) } // parseTokenID returns the Settler number of an ID, only for the canonical form ("1", not "01" or "+1"). func parseTokenID(tid grc721.TokenID) (int, bool) { settlerID, err := strconv.Atoi(string(tid)) if err != nil || settlerID < 1 || strconv.Itoa(settlerID) != string(tid) { return 0, false } return settlerID, true } func newTraitsFilter() *bloom.Bloom { m := uint64(28756) // bits k := uint64(7) // hashes return bloom.NewWithMK(m, k) } func generateSeed(caller address, settlerID, nonce int) string { return hashSeed(seedPrefix(caller, settlerID), nonce) } func seedPrefix(caller address, settlerID int) string { return strconv.FormatInt(time.Now().UnixNano(), 10) + ":" + caller.String() + ":" + strconv.Itoa(settlerID) + ":" } func hashSeed(prefix string, nonce int) string { sum := sha256.Sum256([]byte(prefix + strconv.Itoa(nonce))) return hex.EncodeToString(sum[:]) } func splitProof(proof string) (int, []byte, string) { dot := strings.Index(proof, ".") if dot < 1 { return 0, nil, "the proof must be <index>.<aunts>" } index, err := strconv.Atoi(proof[:dot]) if err != nil || index < 0 || index >= total { return 0, nil, "the index of the proof is not valid" } aunts, err := hex.DecodeString(proof[dot+1:]) if err != nil || len(aunts)%32 != 0 || len(aunts) > 32*maxAunts { return 0, nil, "the aunts of the proof must be hashes of 32 bytes in hex" } return index, aunts, "" } func makeKey(settlerID int) string { s := strconv.Itoa(settlerID) return strings.Repeat("0", 5-len(s)) + s } func assertIsCurrent(cur realm) { if !cur.IsCurrent() { panic("unauthorized") } } func assertIsValidAlertType(t mdalert.Type) { switch t { case mdalert.TypeCaution, mdalert.TypeInfo, mdalert.TypeNote, mdalert.TypeSuccess, mdalert.TypeTip, mdalert.TypeWarning: return } panic("invalid notice type") }
  12. #12render.gno
  13. #13package nft import ( "strconv" "gno.land/p/g17khqpukees4237dtn3astzapmp462vjhsz6st4/settlers/nft/v0" "gno.land/p/moul/md/v0" "gno.land/p/moul/mdtable/v0" "gno.land/p/nt/grc721/v0" "gno.land/p/nt/markdown/sanitize/v0" "gno.land/p/nt/mdalert/v0" "gno.land/p/nt/mux/v0" ) const ( // Gallery width is the width/height of gallery Settler images. galleryWidth = 96 // Detail width is the width/height of Settler the image for detail views. detailWidth = 256 // Gallery length is the number of last minted Settlers of the home page. galleryLen = 5 ) // Render renders the realm. // // By default renders the state of the mint and the last minted Settlers. // // Paths: // - `token/{id}`: A Settler with its traits and its owner. // - `addr/{address}`: The Settler of an account. func Render(path string) string { router := mux.NewRouter() router.NotFoundHandler = func(res *mux.ResponseWriter, _ *mux.Request) { res.Write("Path not found") } router.HandleFunc("", renderHome) router.HandleFunc("token/{id}", renderToken) router.HandleFunc("addr/{addr}", renderAddr) return router.Render(path) } func renderHome(res *mux.ResponseWriter, _ *mux.Request) { res.Write(md.H1("Settlers")) if Notice != "" { res.Write(mdalert.New(NoticeType, "Message", Notice, false).String() + "\n\n") } res.Write(md.Paragraph( "Soulbound pixel-art gnomes for the early settlers of Gno.land. " + "Every account that had at least 15 GNOT at the launch of `gnoland-1` can mint one, " + "first come first served. A Settler can't be transferred, it's soulbound", )) res.Write(md.Paragraph("Realm paths:")) pathsTable := mdtable.Table{Headers: []string{"Path", "Description"}} pathsTable.Append([]string{ md.InlineCode("token/{id}"), "A Settler with its traits and its owner.", }) pathsTable.Append([]string{ md.InlineCode("addr/{address}"), "The Settler of an account.", }) res.Write(pathsTable.String() + "\n") minted := tokens.Size() if len(root) == 0 { res.Write(md.Paragraph("The mint is not open yet.")) } else { res.Write(md.Paragraph( md.BulletItem(md.Bold("Minted:")+" "+strconv.Itoa(minted)+" of "+strconv.Itoa(supplyCap)) + md.BulletItem(md.Bold("Eligible accounts:")+" "+strconv.Itoa(total)) + md.BulletItem(md.Bold("Merkle root:")+" "+md.InlineCode(MerkleRoot())), )) res.Write(md.Paragraph( "To mint, get your proof from the list of eligible accounts and call " + md.Link("Mint", RealmPath+"$help&func=Mint") + " with it; the account that " + "signs the transaction is the one that gets the Settler.", )) res.Write(md.Paragraph( "Check the " + md.Link("README", RealmPath+"$source&file=README.md") + " for the full instructions.", )) } if minted > 0 { res.Write(md.H2("Last Settlers")) tokens.ReverseIterateByOffset(0, galleryLen, func(key string, value any) bool { settlerID, _ := strconv.Atoi(key) img := image(settlerID, nft.Unpack(value.(uint32)), galleryWidth) res.Write("[" + img + "](" + tokenPath(settlerID) + ") ") return false }) res.Write("\n\n") } res.Write(footer()) } func renderToken(res *mux.ResponseWriter, req *mux.Request) { tid := grc721.TokenID(req.GetVar("id")) settlerID, _ := parseTokenID(tid) packed, ok := traitsOf(tid) if !ok { res.Write(md.Paragraph("Settler not found.")) res.Write(md.Paragraph(backHomeLink())) return } res.Write(md.H1(settlerName(settlerID))) t := nft.Unpack(packed) res.Write(image(settlerID, t, detailWidth) + "\n\n") ownerAddr, _ := OwnerOf(tid) owner := ownerAddr.String() res.Write(md.Paragraph( "Owner: " + md.Link(owner, RealmPath+":addr/"+owner) + " · soulbound, it can't be transferred", )) res.Write("| Trait | Value |\n| --- | --- |\n") for _, attr := range t.Attributes() { res.Write("| " + attr.Type + " | " + attr.Value + " |\n") } res.Write("\n") res.Write(md.Paragraph(backHomeLink())) res.Write(footer()) } func renderAddr(res *mux.ResponseWriter, req *mux.Request) { addr := sanitize.BechString(req.GetVar("addr"), "g") if addr == "" { res.Write(md.Paragraph("Not a valid address.")) res.Write(md.Paragraph(backHomeLink())) return } tid := TokenOf(address(addr)) if tid == "" { res.Write(md.H1("Settler of " + md.InlineCode(addr))) res.Write(md.Paragraph("This account has not minted a Settler.")) res.Write(md.Paragraph(backHomeLink())) return } settlerID, _ := parseTokenID(tid) packed, _ := traitsOf(tid) img := image(settlerID, nft.Unpack(packed), detailWidth) res.Write(md.H1("Settler of " + md.InlineCode(addr))) res.Write("[" + img + "](" + tokenPath(settlerID) + ")\n\n") res.Write(md.Paragraph(backHomeLink())) res.Write(footer()) } func image(settlerID int, t nft.Traits, width int) string { alt := settlerName(settlerID) return "![" + alt + "](" + nft.RenderDataURI(t, nft.Options{Size: width}) + ")" } func tokenPath(settlerID int) string { return RealmPath + ":token/" + strconv.Itoa(settlerID) } func backHomeLink() string { return md.Link("< Home", RealmPath) } func settlerName(settlerID int) string { return "Settler #" + strconv.Itoa(settlerID) } func footer() string { return md.Blockquote(nft.Credits) }
  14. #14/gno.MemPackageType
  15. #15 MPUserAll

Result log

msg:0,success:true,log:,events:[]