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.

gnomes.gno

6.70 Kb · 235 lines
  1// Package gnomes — Pixel Gnomes, one hundred of them.
  2//
  3// Every gnome is drawn by this realm from its row in traitTable (traits.gno):
  4// no image files, nothing hosted. The table was picked off-chain, once, with
  5// a fixed seed (tools/gen-gnome-traits.py), so rarity is deliberate and no
  6// two gnomes share all nine traits.
  7//
  8// ANYONE MAY CALL Mint, once. Every gnome goes to `owner`, fixed in the
  9// source, so it does not matter who sends the transaction.
 10//
 11// OWNERSHIP IS LAZY: all hundred belong to `owner` from the mint without a
 12// hundred writes. A gnome is written down individually only the first time
 13// it moves.
 14//
 15// ROYALTY is 5% to `royaltyReceiver`, reported by RoyaltyInfo in the EIP-2981 shape.
 16// It is advisory, as on every chain: a marketplace has to choose to pay it,
 17// and a direct wallet-to-wallet transfer does not.
 18package pixelgnomes
 19
 20import (
 21	"chain"
 22	"chain/runtime"
 23	"encoding/base64"
 24	"strconv"
 25	"strings"
 26
 27	"gno.land/p/nt/avl/v0"
 28	"gno.land/p/nt/ufmt/v0"
 29)
 30
 31const (
 32	collectionName   = "Pixel Gnomes"
 33	collectionSymbol = "GNOME"
 34	supply           = 100
 35	royaltyBps       = int64(500) // 5%
 36
 37	owner           = address("g1fjr5eezf73tmqzh0ml9qkhj7t8dkn4cgmxgp5f") // receives all 100 at mint
 38	royaltyReceiver = address("g1d57q42jcymc7qvcea9de6kvadz7pfrvdg7zrgz") // receives the 5% royalty
 39)
 40
 41var (
 42	mintHeight int64
 43
 44	owners    avl.Tree // token id -> address, only tokens that have moved
 45	balances  avl.Tree // address -> int64
 46	approvals avl.Tree // token id -> address
 47	operators avl.Tree // owner + "/" + operator -> true
 48)
 49
 50// Mint brings all hundred into existence, once.
 51func Mint(cur realm) {
 52	if mintHeight != 0 {
 53		panic(ufmt.Sprintf("gnomes: already minted, in block %d", mintHeight))
 54	}
 55	if len(traitTable) != supply*traitCount {
 56		panic("gnomes: trait table does not match the supply")
 57	}
 58	mintHeight = runtime.ChainHeight()
 59	balances.Set(owner.String(), int64(supply))
 60	for i := 1; i <= supply; i++ {
 61		chain.Emit("Transfer", "from", "", "to", owner.String(), "tid", strconv.Itoa(i))
 62	}
 63	chain.Emit("Minted", "height", strconv.FormatInt(mintHeight, 10), "supply", strconv.Itoa(supply), "to", owner.String())
 64}
 65
 66// -- GRC-721 surface --
 67
 68func Name() string         { return collectionName }
 69func Symbol() string       { return collectionSymbol }
 70func Owner() address       { return owner }
 71func RoyaltyReceiver() address { return royaltyReceiver }
 72func MintHeight() int64    { return mintHeight }
 73func TotalSupply() int64 {
 74	if mintHeight == 0 {
 75		return 0
 76	}
 77	return supply
 78}
 79
 80func BalanceOf(addr address) int64 {
 81	v := balances.Get(addr.String())
 82	if v == nil {
 83		return 0
 84	}
 85	return v.(int64)
 86}
 87
 88func OwnerOf(tid string) address { return ownerOf(mustID(tid)) }
 89
 90func GetApproved(tid string) address {
 91	mustID(tid)
 92	v := approvals.Get(tid)
 93	if v == nil {
 94		return ""
 95	}
 96	return v.(address)
 97}
 98
 99func IsApprovedForAll(holder, operator address) bool {
100	return operators.Get(holder.String()+"/"+operator.String()) != nil
101}
102
103func Approve(cur realm, to address, tid string) {
104	caller := cur.Previous().Address()
105	holder := ownerOf(mustID(tid))
106	if caller != holder && !IsApprovedForAll(holder, caller) {
107		panic("gnomes: only the holder or an operator may approve")
108	}
109	if to == holder {
110		panic("gnomes: the holder does not need approving")
111	}
112	if to == "" {
113		approvals.Remove(tid)
114	} else {
115		approvals.Set(tid, to)
116	}
117	chain.Emit("Approval", "owner", holder.String(), "approved", to.String(), "tid", tid)
118}
119
120func SetApprovalForAll(cur realm, operator address, approved bool) {
121	caller := cur.Previous().Address()
122	if operator == caller {
123		panic("gnomes: you cannot make yourself your own operator")
124	}
125	key := caller.String() + "/" + operator.String()
126	if approved {
127		operators.Set(key, true)
128	} else {
129		operators.Remove(key)
130	}
131	chain.Emit("ApprovalForAll", "owner", caller.String(), "operator", operator.String(), "approved", strconv.FormatBool(approved))
132}
133
134func TransferFrom(cur realm, from, to address, tid string) {
135	caller := cur.Previous().Address()
136	id := mustID(tid)
137	holder := ownerOf(id)
138	if holder != from {
139		panic("gnomes: `from` does not hold this gnome")
140	}
141	if !to.IsValid() {
142		panic("gnomes: that is not a valid address")
143	}
144	if caller != holder && GetApproved(tid) != caller && !IsApprovedForAll(holder, caller) {
145		panic("gnomes: not the holder, not approved, not an operator")
146	}
147	approvals.Remove(tid)
148	owners.Set(tid, to)
149	addBalance(from, -1)
150	addBalance(to, 1)
151	chain.Emit("Transfer", "from", from.String(), "to", to.String(), "tid", tid)
152}
153
154// SafeTransferFrom is TransferFrom: there is no receiver hook on gno.land.
155func SafeTransferFrom(cur realm, from, to address, tid string) {
156	TransferFrom(cross(cur), from, to, tid)
157}
158
159// RoyaltyInfo answers the EIP-2981 question: for a sale at salePrice, who
160// should receive a royalty, and how much.
161func RoyaltyInfo(tid string, salePrice int64) (address, int64) {
162	mustID(tid)
163	if salePrice < 0 || salePrice > (int64(1)<<62)/royaltyBps {
164		panic("gnomes: sale price out of range")
165	}
166	return royaltyReceiver, salePrice * royaltyBps / 10000
167}
168
169func RoyaltyBps() int64 { return royaltyBps }
170
171// TokenURI is the artwork itself, as a data URI.
172func TokenURI(tid string) string {
173	return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(Art(tid)))
174}
175
176// Traits lists one gnome's traits as "trait:option" pairs.
177func Traits(tid string) string {
178	t := traitsOf(mustID(tid))
179	parts := make([]string, traitCount)
180	for i := 0; i < traitCount; i++ {
181		parts[i] = traitNames[i] + ":" + optionName(i, t[i])
182	}
183	return strings.Join(parts, ",")
184}
185
186// -- helpers --
187
188func mustID(tid string) int {
189	n, err := strconv.Atoi(tid)
190	if err != nil || n < 1 || n > supply || strconv.Itoa(n) != tid {
191		panic("gnomes: gnome ids are 1 to 100")
192	}
193	return n
194}
195
196func ownerOf(id int) address {
197	if mintHeight == 0 {
198		panic("gnomes: not minted yet")
199	}
200	if v := owners.Get(strconv.Itoa(id)); v != nil {
201		return v.(address)
202	}
203	return owner
204}
205
206func addBalance(a address, d int64) {
207	b := BalanceOf(a) + d
208	if b == 0 {
209		balances.Remove(a.String())
210		return
211	}
212	balances.Set(a.String(), b)
213}
214
215// Render: "" is the collection, "N" is gnome N.
216func Render(path string) string {
217	if path == "" {
218		var b strings.Builder
219		b.WriteString("# " + collectionName + "\n\n")
220		if mintHeight == 0 {
221			b.WriteString("100 pixel gnomes. Not minted yet.\n")
222			return b.String()
223		}
224		b.WriteString(ufmt.Sprintf("100 pixel gnomes, minted in block %d. Royalty 5%%.\n\n", mintHeight))
225		for i := 1; i <= supply; i++ {
226			b.WriteString(ufmt.Sprintf("[#%d](:%d) ", i, i))
227		}
228		b.WriteString("\n")
229		return b.String()
230	}
231	id := mustID(path)
232	tid := strconv.Itoa(id)
233	return ufmt.Sprintf("# %s #%d\n\n![Gnome #%d](%s)\n\n%s\n\nHolder: %s\n",
234		collectionName, id, id, TokenURI(tid), strings.ReplaceAll(Traits(tid), ",", " · "), ownerOf(id).String())
235}