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

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}