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)) }