Skip to content

Instantly share code, notes, and snippets.

@mikekoro
Created August 18, 2025 03:00
Show Gist options
  • Save mikekoro/7f5ad5a0392a58b4f0d453462889252e to your computer and use it in GitHub Desktop.
Save mikekoro/7f5ad5a0392a58b4f0d453462889252e to your computer and use it in GitHub Desktop.
import { dropsToXrp } from "xrpl";
type IOU = { currency: string; issuer: string };
type Asset = "XRP" | IOU;
type QuoteParams = {
side: "buy" | "sell";
base: Asset; // asset you get on buy / sell on sell
quote: Asset; // asset you pay on buy / receive on sell
mode: "from" | "to"; // which field user typed
amount: number; // value in that field
};
type QuoteResult = {
side: "buy" | "sell";
base: Asset;
quote: Asset;
fromAmount: number; // amount in FROM asset (what user typed or computed)
toAmount: number; // amount in TO asset (computed or what user typed)
fromOrderbook: number; // portion sourced via OB (in QUOTE units for buy; in QUOTE units for sell)
fromAMM: number; // portion via AMM (same units as above)
blendedPriceQuotePerBase: number; // QUOTE per BASE
};
type XRPLAmount = string | { currency: string; issuer?: string; value: string };
const isXRP = (a: Asset): a is "XRP" => a === "XRP";
const amtToFloat = (amt: XRPLAmount) =>
typeof amt === "string" ? Number(dropsToXrp(amt)) : Number(amt.value);
async function transferRateOf(client: any, a: Asset): Promise<number> {
if (isXRP(a)) return 1;
const res = await client.request({
command: "account_info",
account: a.issuer,
ledger_index: "current",
});
const u = res.result.account_data.TransferRate;
return u ? Number(u) / 1_000_000_000 : 1;
}
async function fetchAMM(client: any, base: Asset, quote: Asset) {
const { result } = await client.request({
command: "amm_info",
asset: isXRP(base) ? "XRP" : base,
asset2: isXRP(quote) ? "XRP" : quote,
ledger_index: "current",
});
const x = amtToFloat(result.amm.amount); // base reserve
const y = amtToFloat(result.amm.amount2); // quote reserve
const feeUnits = result.amm.trading_fee ?? 0;
return { x, y, feeUnits };
}
function normalizeAsks(offers: any[]) {
// maker sells BASE for QUOTE → taker_pays = QUOTE, taker_gets = BASE
return (offers || [])
.map((o: any) => {
const qtyBase = amtToFloat(o.taker_gets);
const paysQuote = amtToFloat(o.taker_pays);
const price = paysQuote / qtyBase; // QUOTE per BASE
return { price, qtyBase };
})
.filter((x: any) => x.qtyBase > 0 && isFinite(x.price))
.sort((a: any, b: any) => a.price - b.price);
}
function normalizeBids(offers: any[]) {
// maker buys BASE paying QUOTE → taker_pays = BASE, taker_gets = QUOTE
return (offers || [])
.map((o: any) => {
const paysBase = amtToFloat(o.taker_pays);
const getsQuote = amtToFloat(o.taker_gets);
const price = getsQuote / paysBase; // QUOTE per BASE
const qtyBase = paysBase;
return { price, qtyBase };
})
.filter((x: any) => x.qtyBase > 0 && isFinite(x.price))
.sort((a: any, b: any) => b.price - a.price);
}
/** ---------- AMM math (handles fee + transfer rates) ---------- */
// BUY cost for q BASE out (QUOTE units)
function ammBuyCost(q: number, x: number, y: number, feeUnits: number, trIn: number, trOut: number) {
if (q <= 0) return 0;
const f = feeUnits / 100000;
const outGross = trOut * q;
if (outGross >= x) throw new Error("tokenOut >= pool reserve");
const inEff = (y * outGross) / (x - outGross);
return inEff / (trIn * (1 - f));
}
// BUY marginal price at cumulative q (QUOTE per BASE)
function ammBuyMarginal(q: number, x: number, y: number, feeUnits: number, trIn: number, trOut: number) {
const f = feeUnits / 100000;
const denom = x - trOut * q;
if (denom <= 0) return Infinity;
return (y * trOut * x) / (trIn * (1 - f) * denom * denom);
}
// Given extra budget ΔQuote, get Δq BASE using closed form (from q0)
function ammBuyDeltaQForBudget(q0: number, dQuote: number, x: number, y: number, feeUnits: number, trIn: number, trOut: number) {
if (dQuote <= 0) return 0;
const f = feeUnits / 100000;
const A = y / (trIn * (1 - f));
const B = x / trOut;
const F0 = q0 / (B - q0);
const F1 = F0 + dQuote / A;
return (B * F1) / (1 + F1) - q0;
}
function solveBuyQForMarginalEqual(pTarget: number, q0: number, x: number, y: number, feeUnits: number, trIn: number, trOut: number) {
const f = feeUnits / 100000;
const s = Math.sqrt((y * trOut * x) / (trIn * (1 - f) * pTarget));
const qStar = (x - s) / trOut;
return Math.max(q0, qStar);
}
// SELL proceeds for q BASE in (QUOTE units)
function ammSellOut(q: number, x: number, y: number, feeUnits: number, trIn: number, trOut: number) {
if (q <= 0) return 0;
const f = feeUnits / 100000;
const k = trIn * (1 - f);
const outGross = (y * (k * q)) / (x + k * q);
return outGross / trOut;
}
// SELL marginal price at cumulative q (QUOTE per BASE)
function ammSellMarginal(q: number, x: number, y: number, feeUnits: number, trIn: number, trOut: number) {
const f = feeUnits / 100000;
const k = trIn * (1 - f);
const denom = x + k * q;
return (y * k * x) / (trOut * denom * denom);
}
// Given extra proceeds ΔQuote, get Δq BASE using closed form (from q0)
function ammSellDeltaQForProceeds(q0: number, dQuote: number, x: number, y: number, feeUnits: number, trIn: number, trOut: number) {
if (dQuote <= 0) return 0;
const f = feeUnits / 100000;
const k = trIn * (1 - f);
const C = y / trOut;
const F0 = (k * q0) / (x + k * q0);
const F1 = F0 + dQuote / C; // target fraction of AMM reserve paid out
if (F1 >= 1) return Infinity; // asymptote guard
return ( (x / k) * (F1 / (1 - F1)) ) - q0;
}
/** ---------- Dual-input SOR ---------- */
export async function quoteFlexible(client: any, p: QuoteParams): Promise<QuoteResult> {
const { side, base, quote, mode, amount } = p;
if (amount <= 0) throw new Error("amount must be > 0");
// AMM + rates
const { x, y, feeUnits } = await fetchAMM(client, base, quote);
const trBase = await transferRateOf(client, base);
const trQuote = await transferRateOf(client, quote);
const trIn = side === "buy" ? trQuote : trBase;
const trOut = side === "buy" ? trBase : trQuote;
let fromOB = 0; // QUOTE units (spent on buy / received on sell)
let fromAMM = 0;
let qAMM = 0; // cumulative BASE routed to/from AMM
let remainingBase: number; // BASE still needed (buy 'to' mode / sell 'from' mode)
let remainingQuote: number; // QUOTE budget (buy 'from' mode) or target proceeds (sell 'to' mode)
let gotBase = 0; // BASE acquired (buy 'from' mode)
let spentQuote = 0; // QUOTE spent (buy 'to' mode)
if (side === "buy") {
// Get asks
const book = await client.request({
command: "book_offers",
taker_pays: isXRP(quote) ? "XRP" : quote,
taker_gets: isXRP(base) ? "XRP" : base,
ledger_index: "current",
limit: 200,
});
const asks = normalizeAsks(book.result.offers);
if (mode === "to") {
// want exact BASE out
remainingBase = amount;
for (const lvl of asks) {
if (remainingBase <= 0) break;
const pOB = lvl.price;
// Route to AMM while it's cheaper than this ask
const pAM = ammBuyMarginal(qAMM, x, y, feeUnits, trIn, trOut);
if (pAM < pOB) {
const qStar = solveBuyQForMarginalEqual(pOB, qAMM, x, y, feeUnits, trIn, trOut);
const take = Math.min(remainingBase, Math.max(0, qStar - qAMM));
if (take > 0) {
const before = ammBuyCost(qAMM, x, y, feeUnits, trIn, trOut);
const after = ammBuyCost(qAMM + take, x, y, feeUnits, trIn, trOut);
const cost = after - before;
fromAMM += cost; spentQuote += cost; qAMM += take; remainingBase -= take;
}
}
if (remainingBase <= 0) break;
// Take from OB at this level
const takeOB = Math.min(remainingBase, lvl.qtyBase);
if (takeOB > 0) {
const cost = takeOB * pOB;
fromOB += cost; spentQuote += cost; remainingBase -= takeOB;
}
}
// Finish on AMM if needed
if (remainingBase > 0) {
const before = ammBuyCost(qAMM, x, y, feeUnits, trIn, trOut);
const after = ammBuyCost(qAMM + remainingBase, x, y, feeUnits, trIn, trOut);
const cost = after - before;
fromAMM += cost; spentQuote += cost; qAMM += remainingBase; remainingBase = 0;
}
return {
side, base, quote,
fromAmount: spentQuote,
toAmount: amount,
fromOrderbook: fromOB,
fromAMM,
blendedPriceQuotePerBase: (fromOB + fromAMM) / amount,
};
} else {
// mode === 'from' → budget in QUOTE, get BASE out
remainingQuote = amount;
for (const lvl of asks) {
if (remainingQuote <= 0) break;
const pOB = lvl.price;
// While AMM cheaper than this level, spend on AMM up to equality or budget
const pAM = ammBuyMarginal(qAMM, x, y, feeUnits, trIn, trOut);
if (pAM < pOB) {
// Spend until AMM marginal reaches pOB
const qStar = solveBuyQForMarginalEqual(pOB, qAMM, x, y, feeUnits, trIn, trOut);
const needToEqual = ammBuyCost(qStar, x, y, feeUnits, trIn, trOut) - ammBuyCost(qAMM, x, y, feeUnits, trIn, trOut);
if (remainingQuote <= needToEqual) {
const dq = ammBuyDeltaQForBudget(qAMM, remainingQuote, x, y, feeUnits, trIn, trOut);
const before = ammBuyCost(qAMM, x, y, feeUnits, trIn, trOut);
const after = ammBuyCost(qAMM + dq, x, y, feeUnits, trIn, trOut);
const spent = after - before;
fromAMM += spent; gotBase += dq; qAMM += dq; remainingQuote = 0;
break;
} else {
const dq = Math.max(0, qStar - qAMM);
const before = ammBuyCost(qAMM, x, y, feeUnits, trIn, trOut);
const after = ammBuyCost(qAMM + dq, x, y, feeUnits, trIn, trOut);
const spent = after - before;
fromAMM += spent; gotBase += dq; qAMM += dq; remainingQuote -= spent;
}
}
if (remainingQuote <= 0) break;
// Take from OB at this level (limited by budget)
const maxFromThisLevel = Math.min(lvl.qtyBase, remainingQuote / pOB);
if (maxFromThisLevel > 0) {
const spent = maxFromThisLevel * pOB;
fromOB += spent; gotBase += maxFromThisLevel; remainingQuote -= spent;
}
}
// Finish remaining budget on AMM
if (remainingQuote > 0) {
const dq = ammBuyDeltaQForBudget(qAMM, remainingQuote, x, y, feeUnits, trIn, trOut);
const before = ammBuyCost(qAMM, x, y, feeUnits, trIn, trOut);
const after = ammBuyCost(qAMM + dq, x, y, feeUnits, trIn, trOut);
const spent = after - before;
fromAMM += spent; gotBase += dq; qAMM += dq; remainingQuote = 0;
}
return {
side, base, quote,
fromAmount: amount,
toAmount: gotBase,
fromOrderbook: fromOB,
fromAMM,
blendedPriceQuotePerBase: (fromOB + fromAMM) / Math.max(gotBase, 1e-12),
};
}
} else {
// side === 'sell'
const book = await client.request({
command: "book_offers",
taker_pays: isXRP(base) ? "XRP" : base,
taker_gets: isXRP(quote) ? "XRP" : quote,
ledger_index: "current",
limit: 200,
});
const bids = normalizeBids(book.result.offers);
if (mode === "from") {
// sell known BASE, receive QUOTE
remainingBase = amount;
for (const lvl of bids) {
if (remainingBase <= 0) break;
const pOB = lvl.price;
// While AMM pays more than this bid, route to AMM up to equality
const pAM = ammSellMarginal(qAMM, x, y, feeUnits, trIn, trOut);
if (pAM > pOB) {
const fStar = solveSellQForMarginalEqual(pOB, qAMM, x, y, feeUnits, trIn, trOut);
const take = Math.min(remainingBase, Math.max(0, fStar - qAMM));
if (take > 0) {
const before = ammSellOut(qAMM, x, y, feeUnits, trIn, trOut);
const after = ammSellOut(qAMM + take, x, y, feeUnits, trIn, trOut);
const got = after - before;
fromAMM += got; qAMM += take; remainingBase -= take;
}
}
if (remainingBase <= 0) break;
const takeOB = Math.min(remainingBase, lvl.qtyBase);
if (takeOB > 0) {
const got = takeOB * pOB;
fromOB += got; remainingBase -= takeOB;
}
}
// Finish on AMM
if (remainingBase > 0) {
const before = ammSellOut(qAMM, x, y, feeUnits, trIn, trOut);
const after = ammSellOut(qAMM + remainingBase, x, y, feeUnits, trIn, trOut);
const got = after - before;
fromAMM += got; qAMM += remainingBase; remainingBase = 0;
}
const total = fromOB + fromAMM;
return {
side, base, quote,
fromAmount: amount,
toAmount: total,
fromOrderbook: fromOB,
fromAMM,
blendedPriceQuotePerBase: total / amount,
};
} else {
// mode === 'to' → want exact QUOTE proceeds
remainingQuote = amount;
for (const lvl of bids) {
if (remainingQuote <= 0) break;
const pOB = lvl.price;
// While AMM pays more than this bid, take AMM until equal or reach proceeds target
const pAM = ammSellMarginal(qAMM, x, y, feeUnits, trIn, trOut);
if (pAM > pOB) {
const qStar = solveSellQForMarginalEqual(pOB, qAMM, x, y, feeUnits, trIn, trOut);
const needToEqual = ammSellOut(qStar, x, y, feeUnits, trIn, trOut) - ammSellOut(qAMM, x, y, feeUnits, trIn, trOut);
if (remainingQuote <= needToEqual) {
const dq = ammSellDeltaQForProceeds(qAMM, remainingQuote, x, y, feeUnits, trIn, trOut);
const before = ammSellOut(qAMM, x, y, feeUnits, trIn, trOut);
const after = ammSellOut(qAMM + dq, x, y, feeUnits, trIn, trOut);
const got = after - before;
fromAMM += got; qAMM += dq; remainingQuote = 0;
break;
} else {
const dq = Math.max(0, qStar - qAMM);
const before = ammSellOut(qAMM, x, y, feeUnits, trIn, trOut);
const after = ammSellOut(qAMM + dq, x, y, feeUnits, trIn, trOut);
const got = after - before;
fromAMM += got; qAMM += dq; remainingQuote -= got;
}
}
if (remainingQuote <= 0) break;
// Fill OB bid
const baseNeeded = Math.min(lvl.qtyBase, remainingQuote / pOB);
if (baseNeeded > 0) {
const got = baseNeeded * pOB;
fromOB += got; remainingQuote -= got;
}
}
// Finish remaining proceeds on AMM
if (remainingQuote > 0) {
const dq = ammSellDeltaQForProceeds(qAMM, remainingQuote, x, y, feeUnits, trIn, trOut);
const before = ammSellOut(qAMM, x, y, feeUnits, trIn, trOut);
const after = ammSellOut(qAMM + dq, x, y, feeUnits, trIn, trOut);
const got = after - before;
fromAMM += got; qAMM += dq; remainingQuote = 0;
}
const totalBase = (fromOB + fromAMM) / Math.max((fromOB + fromAMM) / Math.max(qAMM, 1e-12), 1e-12); // not crucial; you likely want to track base taken from OB too if needed
return {
side, base, quote,
fromAmount: (await (async () => {
// compute total base sold: AMM part = qAMM; OB part = fromOB / avg OB price (track if needed)
// For UI you mainly need fromAmount for 'to' mode sell if you show "you need to sell ~X base".
// Quick estimate: use bids again or track baseTakenOB in the loop similarly to buy.
return qAMM; // minimal; add OB base tally if you want exact
})()),
toAmount: amount,
fromOrderbook: fromOB,
fromAMM,
blendedPriceQuotePerBase: (fromOB + fromAMM) / Math.max(qAMM, 1e-12),
};
}
}
}
// Helper for sell marginal equality
function solveSellQForMarginalEqual(pTarget: number, q0: number, x: number, y: number, feeUnits: number, trIn: number, trOut: number) {
const f = feeUnits / 100000;
const k = trIn * (1 - f);
const s = Math.sqrt((y * k * x) / (trOut * pTarget));
const qStar = (s - x) / k;
return Math.max(q0, qStar);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment