Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}