exchange_route.gno
4.75 Kb · 143 lines
1package game29
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// routePlan splits amountIn between book fills (direct pair) and the pool
42// path. A book fill is taken while its rate is at least the pool path's
43// average rate for the whole remainder; everything else goes to the pools.
44func routePlan(caller, coinIn, coinOut string, amountIn uint64, maxFills int, now int64) ([]fillStep, []*order, uint64, uint64, uint64) {
45 steps, expired, _, _ := planFills(caller, coinIn, coinOut, amountIn, maxFills, now)
46 var take []fillStep
47 remaining := amountIn
48 bookOut := uint64(0)
49 for _, st := range steps {
50 cost := addChk(st.x, mulDivUp(st.x, takerFeeBps, 10000))
51 if cost > remaining {
52 break
53 }
54 if pOut, ok := poolPathOut(coinIn, coinOut, remaining); ok {
55 // book rate g/cost vs pool average rate pOut/remaining (128-bit compare)
56 if mulLess(st.g, remaining, pOut, cost) {
57 break
58 }
59 }
60 take = append(take, st)
61 remaining = subChk(remaining, cost)
62 bookOut = addChk(bookOut, st.g)
63 }
64 poolOut := uint64(0)
65 if remaining > 0 {
66 if out, ok := poolPathOut(coinIn, coinOut, remaining); ok {
67 poolOut = out
68 } else {
69 remaining = 0 // no pool path: only the book part executes
70 }
71 }
72 return take, expired, remaining, bookOut, poolOut
73}
74
75// SwapRoute swaps up to amountIn of coinIn for at least minOut of coinOut,
76// using the order book (direct pair) while it beats the pools and the grin
77// pools for the rest. Coins not needed are never debited.
78func SwapRoute(cur realm, coinIn string, coinOut string, amountIn uint64, minOut uint64, maxFills int, deadline int64) uint64 {
79 caller := exchangeCaller(cur)
80 requireMarketOpen(caller)
81 requireDeadline(deadline)
82 requireAmount("amountIn", amountIn)
83 requireCoin(coinIn)
84 requireCoin(coinOut)
85 if coinIn == coinOut {
86 panic("coinIn and coinOut must differ")
87 }
88 take, expired, poolIn, bookOut, poolOut := routePlan(caller, coinIn, coinOut, amountIn, maxFills, time.Now().Unix())
89 planned := addChk(bookOut, poolOut)
90 if planned == 0 || planned < minOut {
91 panic("slippage: output below minimum")
92 }
93 need := poolIn
94 for _, st := range take {
95 need = addChk(need, addChk(st.x, mulDivUp(st.x, takerFeeBps, 10000)))
96 }
97 requireBalance(caller, coinIn, need)
98 // ── all checks passed; writes start here ──
99 for _, o := range expired {
100 closeOrder(o, "expired")
101 }
102 out := executeFills(caller, coinIn, take)
103 if poolIn > 0 {
104 debitBal(caller, coinIn, poolIn)
105 if coinIn == hubCoin {
106 out = addChk(out, poolSwap(getPool(coinOut), true, poolIn))
107 } else if coinOut == hubCoin {
108 out = addChk(out, poolSwap(getPool(coinIn), false, poolIn))
109 } else {
110 mid := poolSwap(getPool(coinIn), false, poolIn)
111 out = addChk(out, poolSwap(getPool(coinOut), true, mid))
112 }
113 }
114 if out != planned {
115 panic("invariant: route plan mismatch")
116 }
117 creditBal(caller, coinOut, out)
118 chain.Emit("SwapRouted", "who", caller, "in", coinIn, "out", coinOut, "amountIn", u(amountIn),
119 "bookFills", strconv.Itoa(len(take)), "poolIn", u(poolIn), "amountOut", u(out))
120 return out
121}
122
123// QuoteRoute previews SwapRoute: how much goes to the book vs the pools and
124// the expected output (fees included).
125func QuoteRoute(caller string, coinIn string, coinOut string, amountIn uint64, maxFills int) string {
126 if amountIn == 0 || amountIn > maxAmount || coinIn == coinOut {
127 return `{"out":"0"}`
128 }
129 if _, ok := coinRarity[coinIn]; !ok {
130 return `{"out":"0"}`
131 }
132 if _, ok := coinRarity[coinOut]; !ok {
133 return `{"out":"0"}`
134 }
135 take, _, poolIn, bookOut, poolOut := routePlan(caller, coinIn, coinOut, amountIn, maxFills, time.Now().Unix())
136 bookIn := uint64(0)
137 for _, st := range take {
138 bookIn = addChk(bookIn, addChk(st.x, mulDivUp(st.x, takerFeeBps, 10000)))
139 }
140 return `{"bookIn":"` + u(bookIn) + `","bookOut":"` + u(bookOut) + `","bookFills":` + strconv.Itoa(len(take)) +
141 `,"poolIn":"` + u(poolIn) + `","poolOut":"` + u(poolOut) + `","out":"` + u(addChk(bookOut, poolOut)) +
142 `","unused":"` + u(amountIn-bookIn-poolIn) + `"}`
143}