Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}