Transaction
16AC16397BD196…48F3D6307658
Block 333,568 · index 0 · indexed
Summary
- Hash
- 16AC16397BD196287C1B02433B221EB1756475BE8C67CD740EBB48F3D6307658
- Block
- 333,568
- Size
- 17461 bytes
- Gas used
- 24,139,487 / 300,000,000
- Fee
- 1000000 ugnot
- Status
- success
Messages
- Attached funds
- 50000000 ugnot
Arguments · 11
- #1pixelgnomes
- #2art.gno
- #3package pixelgnomes import ( "strconv" "strings" "gno.land/p/nt/ufmt/v0" ) // The drawing. A port of the Classic trait sheet: a 20×28 gnome sprite on a // 32×32 canvas, with nine traits layered on top. Integer arithmetic only. const ( side = 32 traitCount = 9 ink = "#1b1b22" white = "#ffffff" ) type option struct{ name, color string } var traitNames = [traitCount]string{"hat", "pattern", "beard", "coat", "skin", "eyes", "accessory", "friend", "background"} var ( hats = []option{{"red", "#d63c3c"}, {"blue", "#3867d6"}, {"green", "#4f9d4a"}, {"purple", "#8e4fc9"}, {"orange", "#f07f2d"}, {"pink", "#ff7fb5"}, {"teal", "#2aa198"}, {"navy", "#283a8f"}, {"ice", "#6fd3e8"}, {"berry", "#a33b5c"}, {"slate", "#5c6b73"}, {"gold", "#f2c230"}} patterns = []option{{"plain", ""}, {"stripes", ""}, {"dots", ""}, {"star", ""}} beards = []option{{"snow", "#f4f1ea"}, {"silver", "#cfd6dc"}, {"brown", "#8a5a3b"}, {"ginger", "#d9793b"}, {"black", "#2d2a2a"}, {"blond", "#f2d27a"}, {"lilac", "#d8c4f2"}, {"frost", "#9fd0ff"}} coats = []option{{"blue", "#3b6fd8"}, {"red", "#c9443a"}, {"moss", "#3f8f4f"}, {"leather", "#7a4e2d"}, {"mustard", "#f2b53a"}, {"denim", "#23406e"}, {"teal", "#2aa198"}, {"mint", "#7ed9c3"}, {"charcoal", "#2c2f36"}, {"violet", "#6a3fa0"}} skins = []option{{"light", "#f6d3b4"}, {"peach", "#eab28a"}, {"tan", "#c98b5e"}, {"brown", "#95603f"}, {"deep", "#6b422c"}} eyeTypes = []option{{"open", ""}, {"happy", ""}, {"sleepy", ""}, {"wink", ""}} accs = []option{{"none", ""}, {"rosy cheeks", ""}, {"scarf", ""}, {"glasses", ""}, {"beard flower", ""}} friends = []option{{"none", ""}, {"mushroom", ""}, {"flower", ""}, {"lantern", ""}, {"star", ""}, {"snail", ""}} bgs = []option{{"mint", "#bfe8d6"}, {"lemon", "#fbeaa0"}, {"peach", "#ffd6c2"}, {"sky", "#c9e6ff"}, {"mist", "#d9ecf2"}, {"lavender", "#ddd5f5"}, {"sand", "#f3dfb3"}, {"frost", "#cfe6f5"}, {"butter", "#ffe3a3"}, {"blush", "#ffd6e8"}, {"dusk", "#1d2a4a"}, {"midnight", "#141a33"}, {"plum night", "#2a1f4a"}, {"deep night", "#1c2447"}, {"space", "#0f1633"}} ) func pools() [traitCount][]option { return [traitCount][]option{hats, patterns, beards, coats, skins, eyeTypes, accs, friends, bgs} } func optionName(trait, idx int) string { return pools()[trait][idx].name } const digits = "0123456789abcdefghijklmnopqrstuvwxyz" func traitsOf(id int) [traitCount]int { var t [traitCount]int row := traitTable[(id-1)*traitCount : id*traitCount] p := pools() for i := 0; i < traitCount; i++ { v := strings.IndexByte(digits, row[i]) if v < 0 || v >= len(p[i]) { panic("gnomes: corrupt trait table") } t[i] = v } return t } var body = []string{ "...........H........", "..........HHh.......", ".........HHHh.......", ".........HHHHh......", "........HHHHHh......", "........HHHHHHh.....", ".......HHHHHHHh.....", "......HHHHHHHHHh....", ".....HHHHHHHHHHHh...", "....HHHHHHHHHHHHHh..", "...hhhhhhhhhhhhhhhh.", ".....SSSSSSSSSSSS...", ".....S..SSSSS..SS...", ".....S..SSSSS..SS...", ".....RSSSSNNSSSSR...", "....BBSSSSSSSSSSBB..", "....BBBBBBBBBBBBBB..", "...BBBBBBBBBBBBBBBB.", "..CBBBBBBBBBBBBBBBBC", "..CCBBBBBBBBBBBBBBCC", "..SCCCBBBBBBBBBBCCCS", "...CCCCbBBBBBBbCCCC.", "...LLLLLLBBBBLLLLLL.", "...CCCCCCCBBCCCCCCC.", "...PPPPPPPPPPPPPPPP.", "...PPPPPPP..PPPPPPP.", "..OOOOOOOO..OOOOOOOO", "..OOOOOOOO..OOOOOOOO", } // Eyes fill the two 2×2 holes: row, col, paint. var eyeCells = map[string][][3]int{ "open": {{12, 6, 'W'}, {12, 7, 'E'}, {13, 6, 'E'}, {13, 7, 'E'}, {12, 13, 'W'}, {12, 14, 'E'}, {13, 13, 'E'}, {13, 14, 'E'}}, "happy": {{12, 6, 'E'}, {12, 7, 'S'}, {13, 6, 'S'}, {13, 7, 'E'}, {12, 13, 'S'}, {12, 14, 'E'}, {13, 13, 'E'}, {13, 14, 'S'}}, "sleepy": {{12, 6, 'S'}, {12, 7, 'S'}, {13, 6, 'E'}, {13, 7, 'E'}, {12, 13, 'S'}, {12, 14, 'S'}, {13, 13, 'E'}, {13, 14, 'E'}}, "wink": {{12, 6, 'W'}, {12, 7, 'E'}, {13, 6, 'E'}, {13, 7, 'E'}, {12, 13, 'S'}, {12, 14, 'S'}, {13, 13, 'E'}, {13, 14, 'E'}}, } type friendSprite struct { rows []string x, y int } var friendSprites = map[string]friendSprite{ "mushroom": {[]string{"..RRR..", ".RWRRR.", "RRRRWRR", "...T...", "..TTT.."}, 0, 25}, "star": {[]string{".Y.", "YYY", ".Y."}, 17, 0}, "lantern": {[]string{".K.", "KYK", "KYK", ".K."}, 26, 22}, "flower": {[]string{".F.", "FYF", ".F.", ".G."}, 6, 19}, "snail": {[]string{"..KKK", ".KYYK", "TTTKK"}, 26, 28}, } var friendPaint = map[byte]string{'R': "#e8453c", 'W': white, 'T': "#efe6d2", 'Y': "#ffd84a", 'K': "#5a4636", 'F': "#ff8fb8", 'G': "#4c9a52"} type canvas struct{ px [side * side]string } func (c *canvas) put(x, y int, col string) { if x >= 0 && y >= 0 && x < side && y < side { c.px[y*side+x] = col } } const hexDigits = "0123456789abcdef" // shade scales a #rrggbb colour by pct percent. func shade(hex string, pct int) string { v, err := strconv.ParseUint(hex[1:], 16, 32) if err != nil { panic("gnomes: bad colour " + hex) } out := []byte{'#'} for _, shift := range []uint{16, 8, 0} { ch := int((v>>shift)&255) * pct / 100 if ch > 255 { ch = 255 } out = append(out, hexDigits[ch>>4], hexDigits[ch&15]) } return string(out) } // Art draws gnome `tid` as an SVG. func Art(tid string) string { id := mustID(tid) t := traitsOf(id) hat, beard, coat, skin, bg := hats[t[0]].color, beards[t[2]].color, coats[t[3]].color, skins[t[4]].color, bgs[t[8]] night := t[8] >= 10 paint := map[byte]string{ 'H': hat, 'h': shade(hat, 72), 'S': skin, 'R': shade(skin, 88), 'N': shade(skin, 82), 'B': beard, 'b': shade(beard, 82), 'C': coat, 'L': "#3a2a1f", 'P': shade(coat, 55), 'O': "#2a2220", 'E': ink, 'W': white, } var c canvas const ox, oy = 6, 3 sp := func(x, y int, col string) { c.put(ox+x, oy+y, col) } for y, row := range body { for x := 0; x < len(row); x++ { if row[x] != '.' { sp(x, y, paint[row[x]]) } } } switch patterns[t[1]].name { case "stripes": for y, row := range body { if y == 3 || y == 6 || y == 9 { for x := 0; x < len(row); x++ { if row[x] == 'H' { sp(x, y, shade(hat, 140)) } } } } case "dots": for _, d := range [][2]int{{10, 4}, {9, 7}, {12, 6}, {7, 9}, {11, 9}, {14, 8}} { sp(d[0], d[1], white) } case "star": for _, d := range [][2]int{{10, 5}, {9, 6}, {10, 6}, {11, 6}, {10, 7}} { sp(d[0], d[1], "#ffd84a") } } for _, e := range eyeCells[eyeTypes[t[5]].name] { sp(e[1], e[0], paint[byte(e[2])]) } switch accs[t[6]].name { case "rosy cheeks": sp(5, 14, "#ff7f96") sp(6, 14, "#ff9fae") sp(15, 14, "#ff9fae") sp(16, 14, "#ff7f96") case "glasses": const frame = "#3b2f2a" for _, cols := range [][4]int{{5, 6, 7, 8}, {12, 13, 14, 15}} { for _, x := range cols { sp(x, 11, frame) sp(x, 14, frame) } sp(cols[0], 12, frame) sp(cols[0], 13, frame) sp(cols[3], 12, frame) sp(cols[3], 13, frame) } sp(9, 12, frame) sp(10, 12, frame) sp(11, 12, frame) case "scarf": for x := 4; x <= 17; x++ { if x%2 == 1 { sp(x, 15, "#e8b53a") } else { sp(x, 15, "#c7922a") } } for y := 16; y <= 20; y++ { sp(14, y, "#e8b53a") sp(15, y, "#c7922a") } case "beard flower": sp(12, 18, "#ff8fb8") sp(11, 19, "#ff8fb8") sp(13, 19, "#ff8fb8") sp(12, 20, "#ff8fb8") sp(12, 19, "#ffd84a") } if f, ok := friendSprites[friends[t[7]].name]; ok { for y, row := range f.rows { for x := 0; x < len(row); x++ { if col, ok := friendPaint[row[x]]; ok { c.put(f.x+x, f.y+y, col) } } } } var b strings.Builder b.WriteString(ufmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" shape-rendering="crispEdges">`, side, side)) b.WriteString(`<rect width="32" height="32" fill="` + bg.color + `"/>`) grass := "#6fbf73" if night { grass = "#2a4a3c" for _, s := range [][2]int{{3, 4}, {9, 2}, {27, 5}, {23, 11}, {2, 14}, {29, 16}, {14, 1}} { b.WriteString(ufmt.Sprintf(`<rect x="%d" y="%d" width="1" height="1" fill="#fff6c9"/>`, s[0], s[1])) } } b.WriteString(`<rect y="29" width="32" height="3" fill="` + grass + `"/>`) b.WriteString(`<rect y="29" width="32" height="1" fill="` + shade(grass, 115) + `"/>`) for y := 0; y < side; y++ { x := 0 for x < side { col := c.px[y*side+x] if col == "" { x++ continue } n := 1 for x+n < side && c.px[y*side+x+n] == col { n++ } b.WriteString(ufmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="1" fill="%s"/>`, x, y, n, col)) x += n } } b.WriteString(`</svg>`) return b.String() }
- #4gnomes.gno
- #5// Package gnomes — Pixel Gnomes, one hundred of them. // // Every gnome is drawn by this realm from its row in traitTable (traits.gno): // no image files, nothing hosted. The table was picked off-chain, once, with // a fixed seed (tools/gen-gnome-traits.py), so rarity is deliberate and no // two gnomes share all nine traits. // // ANYONE MAY CALL Mint, once. Every gnome goes to `owner`, fixed in the // source, so it does not matter who sends the transaction. // // OWNERSHIP IS LAZY: all hundred belong to `owner` from the mint without a // hundred writes. A gnome is written down individually only the first time // it moves. // // ROYALTY is 5% to `royaltyReceiver`, reported by RoyaltyInfo in the EIP-2981 shape. // It is advisory, as on every chain: a marketplace has to choose to pay it, // and a direct wallet-to-wallet transfer does not. package pixelgnomes import ( "chain" "chain/runtime" "encoding/base64" "strconv" "strings" "gno.land/p/nt/avl/v0" "gno.land/p/nt/ufmt/v0" ) const ( collectionName = "Pixel Gnomes" collectionSymbol = "GNOME" supply = 100 royaltyBps = int64(500) // 5% owner = address("g1fjr5eezf73tmqzh0ml9qkhj7t8dkn4cgmxgp5f") // receives all 100 at mint royaltyReceiver = address("g1d57q42jcymc7qvcea9de6kvadz7pfrvdg7zrgz") // receives the 5% royalty ) var ( mintHeight int64 owners avl.Tree // token id -> address, only tokens that have moved balances avl.Tree // address -> int64 approvals avl.Tree // token id -> address operators avl.Tree // owner + "/" + operator -> true ) // Mint brings all hundred into existence, once. func Mint(cur realm) { if mintHeight != 0 { panic(ufmt.Sprintf("gnomes: already minted, in block %d", mintHeight)) } if len(traitTable) != supply*traitCount { panic("gnomes: trait table does not match the supply") } mintHeight = runtime.ChainHeight() balances.Set(owner.String(), int64(supply)) for i := 1; i <= supply; i++ { chain.Emit("Transfer", "from", "", "to", owner.String(), "tid", strconv.Itoa(i)) } chain.Emit("Minted", "height", strconv.FormatInt(mintHeight, 10), "supply", strconv.Itoa(supply), "to", owner.String()) } // -- GRC-721 surface -- func Name() string { return collectionName } func Symbol() string { return collectionSymbol } func Owner() address { return owner } func RoyaltyReceiver() address { return royaltyReceiver } func MintHeight() int64 { return mintHeight } func TotalSupply() int64 { if mintHeight == 0 { return 0 } return supply } func BalanceOf(addr address) int64 { v := balances.Get(addr.String()) if v == nil { return 0 } return v.(int64) } func OwnerOf(tid string) address { return ownerOf(mustID(tid)) } func GetApproved(tid string) address { mustID(tid) v := approvals.Get(tid) if v == nil { return "" } return v.(address) } func IsApprovedForAll(holder, operator address) bool { return operators.Get(holder.String()+"/"+operator.String()) != nil } func Approve(cur realm, to address, tid string) { caller := cur.Previous().Address() holder := ownerOf(mustID(tid)) if caller != holder && !IsApprovedForAll(holder, caller) { panic("gnomes: only the holder or an operator may approve") } if to == holder { panic("gnomes: the holder does not need approving") } if to == "" { approvals.Remove(tid) } else { approvals.Set(tid, to) } chain.Emit("Approval", "owner", holder.String(), "approved", to.String(), "tid", tid) } func SetApprovalForAll(cur realm, operator address, approved bool) { caller := cur.Previous().Address() if operator == caller { panic("gnomes: you cannot make yourself your own operator") } key := caller.String() + "/" + operator.String() if approved { operators.Set(key, true) } else { operators.Remove(key) } chain.Emit("ApprovalForAll", "owner", caller.String(), "operator", operator.String(), "approved", strconv.FormatBool(approved)) } func TransferFrom(cur realm, from, to address, tid string) { caller := cur.Previous().Address() id := mustID(tid) holder := ownerOf(id) if holder != from { panic("gnomes: `from` does not hold this gnome") } if !to.IsValid() { panic("gnomes: that is not a valid address") } if caller != holder && GetApproved(tid) != caller && !IsApprovedForAll(holder, caller) { panic("gnomes: not the holder, not approved, not an operator") } approvals.Remove(tid) owners.Set(tid, to) addBalance(from, -1) addBalance(to, 1) chain.Emit("Transfer", "from", from.String(), "to", to.String(), "tid", tid) } // SafeTransferFrom is TransferFrom: there is no receiver hook on gno.land. func SafeTransferFrom(cur realm, from, to address, tid string) { TransferFrom(cross(cur), from, to, tid) } // RoyaltyInfo answers the EIP-2981 question: for a sale at salePrice, who // should receive a royalty, and how much. func RoyaltyInfo(tid string, salePrice int64) (address, int64) { mustID(tid) if salePrice < 0 || salePrice > (int64(1)<<62)/royaltyBps { panic("gnomes: sale price out of range") } return royaltyReceiver, salePrice * royaltyBps / 10000 } func RoyaltyBps() int64 { return royaltyBps } // TokenURI is the artwork itself, as a data URI. func TokenURI(tid string) string { return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(Art(tid))) } // Traits lists one gnome's traits as "trait:option" pairs. func Traits(tid string) string { t := traitsOf(mustID(tid)) parts := make([]string, traitCount) for i := 0; i < traitCount; i++ { parts[i] = traitNames[i] + ":" + optionName(i, t[i]) } return strings.Join(parts, ",") } // -- helpers -- func mustID(tid string) int { n, err := strconv.Atoi(tid) if err != nil || n < 1 || n > supply || strconv.Itoa(n) != tid { panic("gnomes: gnome ids are 1 to 100") } return n } func ownerOf(id int) address { if mintHeight == 0 { panic("gnomes: not minted yet") } if v := owners.Get(strconv.Itoa(id)); v != nil { return v.(address) } return owner } func addBalance(a address, d int64) { b := BalanceOf(a) + d if b == 0 { balances.Remove(a.String()) return } balances.Set(a.String(), b) } // Render: "" is the collection, "N" is gnome N. func Render(path string) string { if path == "" { var b strings.Builder b.WriteString("# " + collectionName + "\n\n") if mintHeight == 0 { b.WriteString("100 pixel gnomes. Not minted yet.\n") return b.String() } b.WriteString(ufmt.Sprintf("100 pixel gnomes, minted in block %d. Royalty 5%%.\n\n", mintHeight)) for i := 1; i <= supply; i++ { b.WriteString(ufmt.Sprintf("[#%d](:%d) ", i, i)) } b.WriteString("\n") return b.String() } id := mustID(path) tid := strconv.Itoa(id) return ufmt.Sprintf("# %s #%d\n\n\n\n%s\n\nHolder: %s\n", collectionName, id, id, TokenURI(tid), strings.ReplaceAll(Traits(tid), ",", " · "), ownerOf(id).String()) }
- #6gnomod.toml
- #7module = "gno.land/r/g1r6luttvrkksxh9h4asjq2qd8zsd5nlkrzvyjur/pixelgnomes/v2" gno = "0.9"
- #8traits.gno
- #9package pixelgnomes // Generated by tools/gen-gnome-traits.py — one 9-character row per gnome, // each character the index of that gnome's option for one trait, in order: // hat, pattern, beard, coat, skin, eyes, acc, friend, bg. const traitTable = "413010115500110217304210211101040127400122007b3300005920233221633731100730014121571102012010321111440721100540314100310044234510432313e42752110462314102232230000781340330740032102a405331141301321111122341028302410005312502239a1260121c905542050a01632011822001017a03130200002421004103240147003320137206200225401310005b13231004326240121b2301111580432011080241101610514025920452313680071114211053004040222331201232001560533310000210301230543310390354110110344304562411331530342101721304100032214001430604324e93542111520043003390414114080104020440412200030280000611224012b12242212681331310451041302720180110621310121441302010702143323b411310117902710202011213301a2212100110362004070311110140310010112134200102302000710040112400762231302323122690433202b212431217001410001921810223221520015402000006814040000231240107103210107201031226103143127200242006a04030000a1790111220130001a803941000a2174012540391301b" /* distribution over 100 gnomes: hat [10, 16, 13, 13, 15, 2, 3, 2, 9, 7, 7, 3] pattern [59, 19, 18, 4] beard [11, 18, 20, 27, 10, 6, 3, 5] coat [15, 17, 14, 18, 16, 7, 4, 3, 3, 3] skin [16, 22, 22, 15, 25] eyes [39, 33, 12, 16] acc [42, 34, 18, 6, 0] friend [37, 28, 17, 6, 9, 3] bg [13, 12, 6, 5, 9, 14, 10, 18, 1, 3, 2, 4, 1, 0, 2] */
- #10/gno.MemPackageType
- #11 MPUserAll
Result log
msg:0,success:true,log:,events:[]