exchange_route.gno
11.71 Kb · 350 lines
1package game33
2
3// ── P2P exchange: router (book first while it beats the pool, rest via pools)
4
5import (
6 "strconv"
7 "time"
8
9 "chain"
10)
11
12// poolPathOut quotes amountIn of coinIn → coinOut through the grin pools
13// (one hop if either side is grin, else coinIn→grin→coinOut). ok=false if a
14// needed pool is missing or not open. Read-only.
15func poolPathOut(coinIn, coinOut string, amountIn uint64) (uint64, bool) {
16 if amountIn == 0 || coinIn == coinOut {
17 return 0, false
18 }
19 if coinIn == hubCoin || coinOut == hubCoin {
20 c, inIsHub := coinIn, false
21 if coinIn == hubCoin {
22 c, inIsHub = coinOut, true
23 }
24 if !pools.Has(c) {
25 return 0, false
26 }
27 p := getPool(c)
28 if !poolOpenForSwaps(p) {
29 return 0, false
30 }
31 out, _ := poolQuote(p, inIsHub, amountIn)
32 return out, out > 0
33 }
34 mid, ok := poolPathOut(coinIn, hubCoin, amountIn)
35 if !ok {
36 return 0, false
37 }
38 return poolPathOut(hubCoin, coinOut, mid)
39}
40
41// gnotPathOut quotes amountIn of coinIn → coinOut through GNOT pools.
42// Handles: gnot↔coin (one GNOT pool hop), coin↔coin via gnot hub (two hops).
43// Read-only; ok=false if no suitable GNOT pool exists or is open.
44func gnotPathOut(coinIn, coinOut string, amountIn uint64) (uint64, bool) {
45 if amountIn == 0 || coinIn == coinOut {
46 return 0, false
47 }
48 if coinIn == gnotCoinId || coinOut == gnotCoinId {
49 c, inIsHub := coinIn, false
50 if coinIn == gnotCoinId {
51 c, inIsHub = coinOut, true
52 }
53 if !gnotPools.Has(c) {
54 return 0, false
55 }
56 p := gnotPool(c)
57 if !poolOpenForSwaps(p) {
58 return 0, false
59 }
60 out, _ := poolQuote(p, inIsHub, amountIn)
61 return out, out > 0
62 }
63 // coin→coin via gnot: coinIn→gnot→coinOut
64 mid, ok := gnotPathOut(coinIn, gnotCoinId, amountIn)
65 if !ok {
66 return 0, false
67 }
68 return gnotPathOut(gnotCoinId, coinOut, mid)
69}
70
71// bestPathOut returns the best available output from either GRIN or GNOT pools.
72// Read-only; used by routePlanAny for GNOT-aware routing.
73func bestPathOut(coinIn, coinOut string, amountIn uint64) (uint64, bool) {
74 gOut, gOk := poolPathOut(coinIn, coinOut, amountIn)
75 nOut, nOk := gnotPathOut(coinIn, coinOut, amountIn)
76 if gOk && nOk {
77 if nOut > gOut {
78 return nOut, true
79 }
80 return gOut, true
81 }
82 if gOk {
83 return gOut, true
84 }
85 if nOk {
86 return nOut, true
87 }
88 return 0, false
89}
90
91// routePlan splits amountIn between book fills (direct pair) and the pool
92// path. A book fill is taken while its rate is at least the pool path's
93// average rate for the whole remainder; everything else goes to the pools.
94func routePlan(caller, coinIn, coinOut string, amountIn uint64, maxFills int, now int64) ([]fillStep, []*order, uint64, uint64, uint64) {
95 steps, expired, _, _ := planFills(caller, coinIn, coinOut, amountIn, maxFills, now)
96 var take []fillStep
97 remaining := amountIn
98 bookOut := uint64(0)
99 for _, st := range steps {
100 cost := addChk(st.x, mulDivUp(st.x, takerFeeBps, 10000))
101 if cost > remaining {
102 break
103 }
104 if pOut, ok := poolPathOut(coinIn, coinOut, remaining); ok {
105 // book rate g/cost vs pool average rate pOut/remaining (128-bit compare)
106 if mulLess(st.g, remaining, pOut, cost) {
107 break
108 }
109 }
110 take = append(take, st)
111 remaining = subChk(remaining, cost)
112 bookOut = addChk(bookOut, st.g)
113 }
114 poolOut := uint64(0)
115 if remaining > 0 {
116 if out, ok := poolPathOut(coinIn, coinOut, remaining); ok {
117 poolOut = out
118 } else {
119 remaining = 0 // no pool path: only the book part executes
120 }
121 }
122 return take, expired, remaining, bookOut, poolOut
123}
124
125// SwapRoute swaps up to amountIn of coinIn for at least minOut of coinOut,
126// using the order book (direct pair) while it beats the pools and the grin
127// pools for the rest. Coins not needed are never debited.
128func SwapRoute(cur realm, coinIn string, coinOut string, amountIn uint64, minOut uint64, maxFills int, deadline int64) uint64 {
129 caller := exchangeCaller(cur)
130 requireMarketOpen(caller)
131 requireDeadline(deadline)
132 requireAmount("amountIn", amountIn)
133 requireCoin(coinIn)
134 requireCoin(coinOut)
135 if coinIn == coinOut {
136 panic("coinIn and coinOut must differ")
137 }
138 take, expired, poolIn, bookOut, poolOut := routePlan(caller, coinIn, coinOut, amountIn, maxFills, time.Now().Unix())
139 planned := addChk(bookOut, poolOut)
140 if planned == 0 || planned < minOut {
141 panic("slippage: output below minimum")
142 }
143 need := poolIn
144 for _, st := range take {
145 need = addChk(need, addChk(st.x, mulDivUp(st.x, takerFeeBps, 10000)))
146 }
147 requireBalance(caller, coinIn, need)
148 // ── all checks passed; writes start here ──
149 for _, o := range expired {
150 closeOrder(o, "expired")
151 }
152 out := executeFills(caller, coinIn, take)
153 if poolIn > 0 {
154 debitBal(caller, coinIn, poolIn)
155 if coinIn == hubCoin {
156 out = addChk(out, poolSwap(getPool(coinOut), true, poolIn))
157 } else if coinOut == hubCoin {
158 out = addChk(out, poolSwap(getPool(coinIn), false, poolIn))
159 } else {
160 mid := poolSwap(getPool(coinIn), false, poolIn)
161 out = addChk(out, poolSwap(getPool(coinOut), true, mid))
162 }
163 }
164 if out != planned {
165 panic("invariant: route plan mismatch")
166 }
167 creditBal(caller, coinOut, out)
168 chain.Emit("SwapRouted", "who", caller, "in", coinIn, "out", coinOut, "amountIn", u(amountIn),
169 "bookFills", strconv.Itoa(len(take)), "poolIn", u(poolIn), "amountOut", u(out))
170 return out
171}
172
173// QuoteRoute previews SwapRoute: how much goes to the book vs the pools and
174// the expected output (fees included).
175func QuoteRoute(caller string, coinIn string, coinOut string, amountIn uint64, maxFills int) string {
176 if amountIn == 0 || amountIn > maxAmount || coinIn == coinOut {
177 return `{"out":"0"}`
178 }
179 if _, ok := coinRarity[coinIn]; !ok {
180 return `{"out":"0"}`
181 }
182 if _, ok := coinRarity[coinOut]; !ok {
183 return `{"out":"0"}`
184 }
185 take, _, poolIn, bookOut, poolOut := routePlan(caller, coinIn, coinOut, amountIn, maxFills, time.Now().Unix())
186 bookIn := uint64(0)
187 for _, st := range take {
188 bookIn = addChk(bookIn, addChk(st.x, mulDivUp(st.x, takerFeeBps, 10000)))
189 }
190 return `{"bookIn":"` + u(bookIn) + `","bookOut":"` + u(bookOut) + `","bookFills":` + strconv.Itoa(len(take)) +
191 `,"poolIn":"` + u(poolIn) + `","poolOut":"` + u(poolOut) + `","out":"` + u(addChk(bookOut, poolOut)) +
192 `","unused":"` + u(amountIn-bookIn-poolIn) + `"}`
193}
194
195// ── GNOT-aware routing ────────────────────────────────────────────────────────
196//
197// SwapRouteGnot and QuoteRouteGnot mirror SwapRoute/QuoteRoute but accept
198// gnotCoinId ("gnot") as either coinIn or coinOut (or both sides being game
199// coins routed through GNOT pools). They bypass requireCoin for "gnot" since
200// that pseudo-coin is not in coinRarity.
201
202// requireCoinOrGnot validates a coin for GNOT-aware routes.
203func requireCoinOrGnot(coin string) {
204 if coin == gnotCoinId {
205 return
206 }
207 requireCoin(coin)
208}
209
210// gnotRoutePlan is routePlan extended to also check GNOT pools. It uses
211// bestPathOut (max of GRIN and GNOT pool outputs) for the pool comparison.
212func gnotRoutePlan(caller, coinIn, coinOut string, amountIn uint64, maxFills int, now int64) ([]fillStep, []*order, uint64, uint64, uint64) {
213 steps, expired, _, _ := planFills(caller, coinIn, coinOut, amountIn, maxFills, now)
214 var take []fillStep
215 remaining := amountIn
216 bookOut := uint64(0)
217 for _, st := range steps {
218 cost := addChk(st.x, mulDivUp(st.x, takerFeeBps, 10000))
219 if cost > remaining {
220 break
221 }
222 if pOut, ok := bestPathOut(coinIn, coinOut, remaining); ok {
223 if mulLess(st.g, remaining, pOut, cost) {
224 break
225 }
226 }
227 take = append(take, st)
228 remaining = subChk(remaining, cost)
229 bookOut = addChk(bookOut, st.g)
230 }
231 poolOut := uint64(0)
232 if remaining > 0 {
233 if out, ok := bestPathOut(coinIn, coinOut, remaining); ok {
234 poolOut = out
235 } else {
236 remaining = 0
237 }
238 }
239 return take, expired, remaining, bookOut, poolOut
240}
241
242// executeGnotPoolSwap executes the pool leg of a GNOT-aware swap, choosing
243// the better route (GRIN or GNOT pools) for the given pair.
244func executeGnotPoolSwap(coinIn, coinOut string, poolIn uint64) uint64 {
245 if poolIn == 0 {
246 return 0
247 }
248 // Try GNOT pool path first if it gives better output than GRIN path.
249 gnotOut, gnotOk := gnotPathOut(coinIn, coinOut, poolIn)
250 grinOut, grinOk := poolPathOut(coinIn, coinOut, poolIn)
251
252 useGnot := gnotOk && (!grinOk || gnotOut >= grinOut)
253
254 if useGnot {
255 // Execute via GNOT pools
256 if coinIn == gnotCoinId {
257 return poolSwapGnot(gnotPool(coinOut), true, poolIn)
258 } else if coinOut == gnotCoinId {
259 return poolSwapGnot(gnotPool(coinIn), false, poolIn)
260 } else {
261 // coin→coin via gnot: coinIn→gnot→coinOut
262 mid := poolSwapGnot(gnotPool(coinIn), false, poolIn)
263 return poolSwapGnot(gnotPool(coinOut), true, mid)
264 }
265 } else if grinOk {
266 // Execute via GRIN pools (existing logic)
267 if coinIn == hubCoin {
268 return poolSwap(getPool(coinOut), true, poolIn)
269 } else if coinOut == hubCoin {
270 return poolSwap(getPool(coinIn), false, poolIn)
271 } else {
272 mid := poolSwap(getPool(coinIn), false, poolIn)
273 return poolSwap(getPool(coinOut), true, mid)
274 }
275 }
276 panic("no pool path available for " + coinIn + "→" + coinOut)
277}
278
279// SwapRouteGnot swaps coinIn for coinOut where either side may be "gnot".
280// Uses the order book first (same as SwapRoute) then the best available pool
281// (GRIN or GNOT, whichever gives better output).
282func SwapRouteGnot(cur realm, coinIn string, coinOut string, amountIn uint64, minOut uint64, maxFills int, deadline int64) uint64 {
283 caller := exchangeCaller(cur)
284 requireMarketOpen(caller)
285 requireDeadline(deadline)
286 requireAmount("amountIn", amountIn)
287 requireCoinOrGnot(coinIn)
288 requireCoinOrGnot(coinOut)
289 if coinIn == coinOut {
290 panic("coinIn and coinOut must differ")
291 }
292 take, expired, poolIn, bookOut, poolOut := gnotRoutePlan(caller, coinIn, coinOut, amountIn, maxFills, time.Now().Unix())
293 planned := addChk(bookOut, poolOut)
294 if planned == 0 || planned < minOut {
295 panic("slippage: output below minimum")
296 }
297 need := poolIn
298 for _, st := range take {
299 need = addChk(need, addChk(st.x, mulDivUp(st.x, takerFeeBps, 10000)))
300 }
301 requireBalance(caller, coinIn, need)
302 // ── all checks passed; writes start here ──
303 for _, o := range expired {
304 closeOrder(o, "expired")
305 }
306 out := executeFills(caller, coinIn, take)
307 if poolIn > 0 {
308 debitBal(caller, coinIn, poolIn)
309 out = addChk(out, executeGnotPoolSwap(coinIn, coinOut, poolIn))
310 }
311 if out != planned {
312 panic("invariant: GNOT route plan mismatch")
313 }
314 creditBal(caller, coinOut, out)
315 chain.Emit("SwapRoutedGnot", "who", caller, "in", coinIn, "out", coinOut, "amountIn", u(amountIn),
316 "bookFills", strconv.Itoa(len(take)), "poolIn", u(poolIn), "amountOut", u(out))
317 return out
318}
319
320// QuoteRouteGnot previews SwapRouteGnot (either side may be "gnot").
321func QuoteRouteGnot(caller string, coinIn string, coinOut string, amountIn uint64, maxFills int) string {
322 if amountIn == 0 || amountIn > maxAmount || coinIn == coinOut {
323 return `{"out":"0"}`
324 }
325 if coinIn != gnotCoinId {
326 if _, ok := coinRarity[coinIn]; !ok {
327 return `{"out":"0"}`
328 }
329 }
330 if coinOut != gnotCoinId {
331 if _, ok := coinRarity[coinOut]; !ok {
332 return `{"out":"0"}`
333 }
334 }
335 take, _, poolIn, bookOut, poolOut := gnotRoutePlan(caller, coinIn, coinOut, amountIn, maxFills, time.Now().Unix())
336 bookIn := uint64(0)
337 for _, st := range take {
338 bookIn = addChk(bookIn, addChk(st.x, mulDivUp(st.x, takerFeeBps, 10000)))
339 }
340 // Determine which pool type serves the route (for informational output)
341 gnotOut, gnotOk := gnotPathOut(coinIn, coinOut, poolIn)
342 _, grinOk := poolPathOut(coinIn, coinOut, poolIn)
343 poolType := "grin"
344 if gnotOk && (!grinOk || gnotOut >= poolOut) {
345 poolType = "gnot"
346 }
347 return `{"bookIn":"` + u(bookIn) + `","bookOut":"` + u(bookOut) + `","bookFills":` + strconv.Itoa(len(take)) +
348 `,"poolIn":"` + u(poolIn) + `","poolOut":"` + u(poolOut) + `","poolType":"` + poolType + `"` +
349 `,"out":"` + u(addChk(bookOut, poolOut)) + `","unused":"` + u(amountIn-bookIn-poolIn) + `"}`
350}