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

market.gno

6.93 Kb · 232 lines
  1package gnofly
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"strconv"
  7
  8	// Only for OriginSend, in Buy. Caller identity always comes from cur,
  9	// never from this package.
 10	"chain/runtime/unsafe"
 11
 12	"gno.land/p/nt/bptree/v0"
 13	"gno.land/p/nt/grc721/v0"
 14
 15	"gno.land/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/nfts/planes"
 16)
 17
 18const (
 19	payDenom = "ugnot"
 20
 21	// minPrice keeps the royalty above zero after rounding: 5% of 1 GNOT.
 22	minPrice = int64(1_000_000)
 23	// maxPrice keeps price * royalty bps far below an int64.
 24	maxPrice = int64(1_000_000_000_000)
 25
 26	supply = 777 // plane ids run from 1 to 777
 27)
 28
 29// listing is one plane for sale. The plane itself is held by this realm
 30// (escrow) until it is bought or the listing is cancelled.
 31type listing struct {
 32	seller address
 33	price  int64 // ugnot
 34}
 35
 36var (
 37	// listings by plane id, as a three-digit key ("042") so that iteration
 38	// runs in id order.
 39	listings bptree.BPTree
 40	// paused stops new listings and price changes, for a move to a newer
 41	// version of the market. Cancel and Buy keep working: nothing listed here
 42	// can be stuck.
 43	paused bool
 44	// self is this realm's address, recorded at init: what sellers approve
 45	// on the planes realm.
 46	self address
 47)
 48
 49func init(cur realm) {
 50	self = cur.Address()
 51}
 52
 53// List puts plane id up for sale at priceUgnot. The caller must own it and
 54// have approved this realm on the planes realm (Approve for this plane, or
 55// SetApprovalForAll): the plane moves here, in escrow, until it is bought or
 56// the listing is cancelled.
 57func List(cur realm, id int, priceUgnot int64) {
 58	seller := userOf(0, cur)
 59	if paused {
 60		panic(errPaused)
 61	}
 62	checkPrice(priceUgnot)
 63	if listings.Has(key(id)) {
 64		panic(errListed)
 65	}
 66	if owner := ownerOf(id); owner != seller {
 67		panic(errNotOwner)
 68	}
 69	if !planes.IsApprovedForAll(seller, self) && planes.GetApproved(int64(id)) != self {
 70		panic(errNotApproved)
 71	}
 72	listings.Set(key(id), &listing{seller: seller, price: priceUgnot})
 73	planes.TransferFrom(cross(cur), seller, self, int64(id))
 74	chain.Emit("List", "id", strconv.Itoa(id), "seller", seller.String(), "price", strconv.FormatInt(priceUgnot, 10))
 75}
 76
 77// UpdatePrice changes the price of a plane the caller listed.
 78func UpdatePrice(cur realm, id int, priceUgnot int64) {
 79	seller := userOf(0, cur)
 80	if paused {
 81		panic(errPaused)
 82	}
 83	checkPrice(priceUgnot)
 84	l := mustListing(id)
 85	if l.seller != seller {
 86		panic(errNotSeller)
 87	}
 88	l.price = priceUgnot
 89	chain.Emit("UpdatePrice", "id", strconv.Itoa(id), "seller", seller.String(), "price", strconv.FormatInt(priceUgnot, 10))
 90}
 91
 92// Cancel takes a listing down and gives the plane back to its seller. It
 93// works while the market is paused.
 94func Cancel(cur realm, id int) {
 95	seller := userOf(0, cur)
 96	l := mustListing(id)
 97	if l.seller != seller {
 98		panic(errNotSeller)
 99	}
100	listings.Remove(key(id))
101	planes.TransferFrom(cross(cur), cur.Address(), seller, int64(id))
102	chain.Emit("Cancel", "id", strconv.Itoa(id), "seller", seller.String())
103}
104
105// Buy buys a listed plane: send exactly its price in ugnot. The royalty of
106// the planes realm (RoyaltyInfo: 5% to the gnofly treasury) is paid out of
107// the price, the seller receives the rest, and the plane goes to the buyer.
108// It works while the market is paused.
109func Buy(cur realm, id int) {
110	// A direct user call: only then has the -send envelope landed at this
111	// realm's address, which the OriginSend banker below pays out of.
112	buyer := userOf(0, cur)
113	l := mustListing(id)
114	if l.seller == buyer {
115		panic(errOwnListing)
116	}
117	sent := unsafe.OriginSend()
118	if len(sent) != 1 || sent[0].Denom != payDenom || sent[0].Amount != l.price {
119		panic("gnofly market: wrong payment, send exactly " + strconv.FormatInt(l.price, 10) + payDenom)
120	}
121	// The listing goes before any coin or plane moves.
122	listings.Remove(key(id))
123
124	receiver, royalty := planes.RoyaltyInfo(int64(id), l.price)
125	if royalty < 0 || royalty > l.price {
126		royalty = 0
127	}
128	bank := banker.NewBanker(banker.BankerTypeOriginSend, cur)
129	if royalty > 0 && receiver.IsValid() {
130		bank.SendCoins(cur.Address(), receiver, chain.NewCoins(chain.NewCoin(payDenom, royalty)))
131	} else {
132		royalty = 0
133	}
134	bank.SendCoins(cur.Address(), l.seller, chain.NewCoins(chain.NewCoin(payDenom, l.price-royalty)))
135
136	planes.TransferFrom(cross(cur), cur.Address(), buyer, int64(id))
137	chain.Emit("Buy", "id", strconv.Itoa(id), "seller", l.seller.String(), "buyer", buyer.String(),
138		"price", strconv.FormatInt(l.price, 10), "royalty", strconv.FormatInt(royalty, 10))
139}
140
141// Transfer gives a plane the caller owns, not listed, to another address,
142// through this realm: the call Bazaar's site makes. It needs the same
143// approval as List. A plane can always be moved on the planes realm itself.
144func Transfer(cur realm, to address, id int) {
145	from := userOf(0, cur)
146	if !to.IsValid() {
147		panic(errBadRecipient)
148	}
149	if listings.Has(key(id)) {
150		panic(errListed)
151	}
152	if owner := ownerOf(id); owner != from {
153		panic(errNotOwner)
154	}
155	planes.TransferFrom(cross(cur), from, to, int64(id))
156	chain.Emit("Transfer", "id", strconv.Itoa(id), "from", from.String(), "to", to.String())
157}
158
159// SetPaused stops (or resumes) new listings and price changes. Admin of the
160// game realm only. Cancel and Buy keep working, so a move to a newer market
161// never strands a plane: sellers cancel here and list there.
162func SetPaused(cur realm, p bool) {
163	assertAdmin(0, cur)
164	paused = p
165	chain.Emit("Paused", "paused", strconv.FormatBool(p))
166}
167
168// ReturnStray gives back a plane this realm holds outside any listing: one
169// someone sent here by mistake. Admin of the game realm only; to is who it
170// came from, as the chain's transfer events show.
171func ReturnStray(cur realm, id int, to address) {
172	assertAdmin(0, cur)
173	if !to.IsValid() {
174		panic(errBadRecipient)
175	}
176	if listings.Has(key(id)) || ownerOf(id) != cur.Address() {
177		panic(errNotStray)
178	}
179	planes.TransferFrom(cross(cur), cur.Address(), to, int64(id))
180	chain.Emit("ReturnStray", "id", strconv.Itoa(id), "to", to.String())
181}
182
183// Paused reports whether new listings are stopped.
184func Paused() bool { return paused }
185
186// checkPrice panics unless p is a price this market takes.
187func checkPrice(p int64) {
188	if p < minPrice || p > maxPrice {
189		panic(errBadPrice)
190	}
191}
192
193// mustListing returns the listing of plane id, or panics.
194func mustListing(id int) *listing {
195	v := listings.Get(key(id))
196	if v == nil {
197		panic(errNotListed)
198	}
199	return v.(*listing)
200}
201
202// getListing returns the listing of plane id, or nil.
203func getListing(id int) *listing {
204	if v := listings.Get(key(id)); v != nil {
205		return v.(*listing)
206	}
207	return nil
208}
209
210// ownerOf returns the owner of plane id on the planes realm, or panics for
211// an id that is not a minted plane.
212func ownerOf(id int) address {
213	if id < 1 || id > supply {
214		panic(errUnknownPlane)
215	}
216	owner, err := planes.OwnerOf(tid(id))
217	if err != nil {
218		panic(errUnknownPlane)
219	}
220	return owner
221}
222
223// key is the listings key of plane id: three digits, "042".
224func key(id int) string {
225	s := strconv.Itoa(id)
226	for len(s) < 3 {
227		s = "0" + s
228	}
229	return s
230}
231
232func tid(id int) grc721.TokenID { return grc721.TokenID(strconv.Itoa(id)) }