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