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.

mint.gno

9.45 Kb · 312 lines
  1package planes
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"errors"
  7
  8	// Only for OriginSend, in Mint. Caller identity always comes from
  9	// cur.Previous(), never from this package.
 10	"chain/runtime/unsafe"
 11
 12	"gno.land/p/nt/grc721/v0"
 13	"gno.land/p/onbloc/json/v0"
 14)
 15
 16// maxBatch is the most planes one Mint call mints. It is a constant, apart
 17// from the per-wallet cap that admins tune with SetMintCap.
 18const maxBatch = int64(7)
 19
 20// Mint mints count planes for the caller, 1 to 7 in one call, drawn at
 21// random among the ids not minted yet (draw.gno), and returns their ids in
 22// the order drawn. Exactly
 23// count times the price must be attached to the transaction (-send
 24// "<count*price>ugnot"); it is forwarded to the treasury of the game realm,
 25// in one send, before the call returns.
 26//
 27// Each id comes out once, while the mint is open. A batch is
 28// minted whole or not at all: one that would take the caller over the
 29// per-wallet cap, or past the last plane, is refused with how many are left.
 30// It must be called directly by a user (maketx call): a call relayed by
 31// another realm or by a maketx run script is refused. That is also what
 32// keeps the draw fair: a contract could otherwise look at the ids it got
 33// and panic to draw again.
 34//
 35// The caller pays the storage of their planes as a deposit on top of the
 36// price, given back on burn: the ledger entries only, the traits stay in the
 37// table (see the numbers in docs/spec.md, section 9.2).
 38func Mint(cur realm, count int64) []int64 {
 39	minter := callerOf(0, cur)
 40
 41	// Paid-call guard: two checks that are only correct together.
 42	//
 43	// (a) The caller is a plain user account. Only then is the -send
 44	//     envelope guaranteed to have landed at this realm's address. A
 45	//     relaying realm or a maketx run script can attach coins to the
 46	//     transaction and keep them, while OriginSend still reports them.
 47	if !cur.Previous().IsUserCall() {
 48		panic(errNotUserCall)
 49	}
 50	if count < 1 || count > maxBatch {
 51		panic(errInvalidCount)
 52	}
 53	if !mintOpen {
 54		panic(errMintClosed)
 55	}
 56	if n := left(); n < 1 {
 57		panic(errSoldOut)
 58	} else if count > n {
 59		panic("gnofly: only " + itoa(n) + " planes are left to mint")
 60	}
 61	if left := mintCap - mintedBy(minter); left < 1 {
 62		panic(errCapReached)
 63	} else if count > left {
 64		panic("gnofly: this wallet can mint " + itoa(left) + " more planes, the cap is " + itoa(mintCap))
 65	}
 66
 67	// (b) The envelope is exactly count times the price: one coin, the right
 68	//     denom, the right amount. Anything else would either underpay or
 69	//     leave coins behind in the realm.
 70	total := count * price
 71	sent := unsafe.OriginSend()
 72	if len(sent) != 1 || sent[0].Denom != payDenom || sent[0].Amount != total {
 73		panic("gnofly: wrong payment, send exactly " + itoa(total) + payDenom)
 74	}
 75
 76	// Forward the payment right away, so that nothing accrues in the realm.
 77	// An OriginSend banker can spend the envelope of this call and nothing
 78	// else.
 79	treasury := roles.Treasury()
 80	payment := chain.NewCoins(chain.NewCoin(payDenom, total))
 81	banker.NewBanker(banker.BankerTypeOriginSend, cur).SendCoins(cur.Address(), treasury, payment)
 82
 83	ids := make([]int64, 0, count)
 84	for i := int64(0); i < count; i++ {
 85		p := mustPlane(drawID(minter))
 86		mintCount++
 87		ids = append(ids, p.id)
 88		copy(recent[1:], recent[:len(recent)-1])
 89		recent[0] = p.id
 90		if err := ledger.Mint(minter, tokenID(p.id)); err != nil {
 91			panic(err)
 92		}
 93		tierMinted[tierIndex(p.tier)]++
 94		chain.Emit("PlaneMinted",
 95			"id", itoa(p.id),
 96			"minter", minter.String(),
 97			"price", itoa(price),
 98			"treasury", treasury.String(),
 99		)
100	}
101	mints.Set(minter.String(), mintedBy(minter)+count)
102	return ids
103}
104
105// mintedBy returns how many planes addr minted so far.
106func mintedBy(addr address) int64 {
107	v := mints.Get(addr.String())
108	if v == nil {
109		return 0
110	}
111	return v.(int64)
112}
113
114// The read functions a wallet calls take a grc721.TokenID and return an
115// error rather than panic, the way Adena calls them: OwnerOf("42") into two
116// values, TokenURI("42") as the image it shows, TokenMetadata("42") as JSON
117// for the traits. The errors are made on the call, not stored: a stored
118// error value costs a kilobyte of deposit (see errors.gno).
119
120// TokenMetadata returns the metadata of a minted plane as JSON, built from
121// the table on every read: the realm stores no metadata per token.
122//
123//	{"name":"Storm Saint","description":"Plane 42 of 777, ...",
124//	 "image":"https://gnofly.xyz/nft/collections/planes/042.png",
125//	 "attributes":[{"traitType":"model","value":"delta"}, ...]}
126//
127// The keys are the ones Adena reads, traitType included.
128func TokenMetadata(tid grc721.TokenID) (string, error) {
129	id, err := mintedID(tid)
130	if err != nil {
131		return "", err
132	}
133	p := mustPlane(id)
134	b := json.Builder().
135		WriteString("name", p.name).
136		WriteString("description", description(p)).
137		WriteString("image", imageURL(id)).
138		WriteArray("attributes", func(ab *json.ArrayBuilder) {
139			for _, t := range [][2]string{
140				{"model", p.model}, {"pilot", p.pilot}, {"body", p.body}, {"wing", p.wing},
141				{"accent", p.accent}, {"tier", p.tier}, {"laser", p.laser},
142			} {
143				ab.WriteObject(func(o *json.NodeBuilder) {
144					o.WriteString("traitType", t[0]).WriteString("value", t[1])
145				})
146			}
147		})
148	return encode(b.Node()), nil
149}
150
151// mintedID reads a token id, "1" to "777", and checks the plane is minted.
152func mintedID(tid grc721.TokenID) (int64, error) {
153	id := parseID(string(tid))
154	if ownerOf(id) == "" {
155		return 0, errors.New(errNotMinted)
156	}
157	return id, nil
158}
159
160// parseID reads "1" to "777" in decimal, 0 for anything else.
161func parseID(s string) int64 {
162	if len(s) == 0 || len(s) > 3 || s[0] == '0' {
163		return 0
164	}
165	n := int64(0)
166	for i := 0; i < len(s); i++ {
167		if s[i] < '0' || s[i] > '9' {
168			return 0
169		}
170		n = n*10 + int64(s[i]-'0')
171	}
172	if n > supply {
173		return 0
174	}
175	return n
176}
177
178// description is one sentence about a plane: "Plane 42 of 777, an epic delta
179// flown by a gnome." The tier is the only word that may start with a vowel.
180func description(p plane) string {
181	article := "a "
182	if p.tier == "epic" {
183		article = "an "
184	}
185	s := "Plane " + itoa(p.id) + " of " + itoa(supply) + ", " + article + p.tier + " " + p.model
186	if p.pilot == "gnome" {
187		s += " flown by a gnome"
188	}
189	return s + "."
190}
191
192// Burn destroys a plane. Only its owner can, and nobody can mint it again:
193// the id stays out of circulation.
194func Burn(cur realm, id int64) {
195	caller := callerOf(0, cur)
196	if ownerOf(id) != caller {
197		panic(errNotPlaneOwner)
198	}
199	if err := ledger.Burn(tokenID(id)); err != nil {
200		panic(err)
201	}
202	chain.Emit("PlaneBurned", "id", itoa(id), "owner", caller.String())
203}
204
205// The GRC721 transfer surface. These wrappers hand cur to the caller-scoped
206// teller, which acts as cur.Previous() and applies it to the private ledger:
207// a caller moves their own planes and the ones they were approved for,
208// nothing else. See p/nt/grc721/v0/tellers.gno.
209
210// TransferFrom moves plane id from one address to another. The caller must
211// own it or be approved for it.
212func TransferFrom(cur realm, from, to address, id int64) {
213	if err := teller.TransferFrom(0, cur, from, to, tokenID(id)); err != nil {
214		panic(err)
215	}
216}
217
218// Approve lets to move plane id once. The caller must own it. An empty
219// address revokes.
220func Approve(cur realm, to address, id int64) {
221	if err := teller.Approve(0, cur, to, tokenID(id)); err != nil {
222		panic(err)
223	}
224}
225
226// SetApprovalForAll lets operator move every plane of the caller, or
227// revokes it.
228func SetApprovalForAll(cur realm, operator address, approved bool) {
229	if err := teller.SetApprovalForAll(0, cur, operator, approved); err != nil {
230		panic(err)
231	}
232}
233
234// ownerOf returns who owns plane id, or "" when it is not minted.
235func ownerOf(id int64) address {
236	if id < 1 || id > supply {
237		return ""
238	}
239	owner, err := Token.OwnerOf(tokenID(id))
240	if err != nil {
241		return ""
242	}
243	return owner
244}
245
246// OwnerOf returns who owns plane tid, or an error when it is not minted.
247func OwnerOf(tid grc721.TokenID) (address, error) {
248	id, err := mintedID(tid)
249	if err != nil {
250		return "", err
251	}
252	return ownerOf(id), nil
253}
254
255// BalanceOf returns how many planes addr owns.
256func BalanceOf(addr address) (int64, error) {
257	return Token.BalanceOf(addr)
258}
259
260// TotalSupply returns how many planes exist: minted and not burned.
261func TotalSupply() int64 {
262	return Token.TotalSupply()
263}
264
265// GetApproved returns the address approved to move plane id, or "" when
266// there is none.
267func GetApproved(id int64) address {
268	addr, err := Token.GetApproved(tokenID(id))
269	if err == grc721.ErrTokenIdNotApproved {
270		return ""
271	}
272	if err != nil {
273		panic(err)
274	}
275	return addr
276}
277
278// IsApprovedForAll reports whether operator may move every plane of owner.
279func IsApprovedForAll(owner, operator address) bool {
280	return Token.IsApprovedForAll(owner, operator)
281}
282
283// TokenURI returns the image of a minted plane, "<image base><nnn>.png":
284// what a wallet shows (Adena puts it in an <img>). The metadata is
285// TokenMetadata, or "<image base><id>.json" on the website. It is computed,
286// not stored: a mint writes no URI.
287func TokenURI(tid grc721.TokenID) (string, error) {
288	id, err := mintedID(tid)
289	if err != nil {
290		return "", err
291	}
292	return imageURL(id), nil
293}
294
295// RoyaltyInfo returns who receives the royalty on a sale of plane id at
296// salePrice, and how much: EIP-2981.
297func RoyaltyInfo(id int64, salePrice int64) (address, int64) {
298	receiver, amount, err := Royalties.RoyaltyInfo(tokenID(id), salePrice)
299	if err != nil {
300		panic(err)
301	}
302	return receiver, amount
303}
304
305// RoyaltyBps returns the royalty of a resale, in basis points.
306func RoyaltyBps() int64 {
307	info, ok := Royalties.DefaultRoyalty()
308	if !ok {
309		return 0
310	}
311	return info.Bps
312}