@hyperbridge/simplex 0.16.11 → 0.16.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/simplex.js +1134 -182
- package/dist/bin/simplex.js.map +1 -1
- package/dist/index.cjs +873 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +214 -13
- package/dist/index.d.ts +214 -13
- package/dist/index.js +873 -80
- package/dist/index.js.map +1 -1
- package/dist/sqlite.cjs +59 -3
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +21 -2
- package/dist/sqlite.d.ts +21 -2
- package/dist/sqlite.js +58 -3
- package/dist/sqlite.js.map +1 -1
- package/dist/{types-B5zkFhxR.d.cts → types-BISgqJhM.d.cts} +57 -1
- package/dist/{types-B5zkFhxR.d.ts → types-BISgqJhM.d.ts} +57 -1
- package/dist/ui/assets/Novatica-Medium-CjNgGPEL.ttf +0 -0
- package/dist/ui/assets/index-CK8E6aPx.css +1 -0
- package/dist/ui/assets/index-DShjZe_c.js +106 -0
- package/dist/ui/index.html +2 -2
- package/package.json +5 -4
- package/dist/ui/assets/index-B_p2oQkc.css +0 -1
- package/dist/ui/assets/index-CD2NZXpG.js +0 -106
package/dist/index.cjs
CHANGED
|
@@ -1165,6 +1165,7 @@ var import_viem2 = require("viem");
|
|
|
1165
1165
|
var import_sdk2 = require("@hyperbridge/sdk");
|
|
1166
1166
|
var DEFAULT_BLOCK_SCAN_INTERVAL_SECONDS = 3;
|
|
1167
1167
|
var MIN_BLOCK_SCAN_INTERVAL_SECONDS = 0.1;
|
|
1168
|
+
var DEFAULT_MIN_ORDER_SIZE_USD = 20;
|
|
1168
1169
|
function validateRpcUrls(rpcUrls) {
|
|
1169
1170
|
if (rpcUrls.length === 0) {
|
|
1170
1171
|
throw new Error("rpcUrls must contain at least one URL");
|
|
@@ -1587,6 +1588,13 @@ var FillerConfigService = class {
|
|
|
1587
1588
|
getBidValiditySeconds() {
|
|
1588
1589
|
return this.fillerConfig?.bidValiditySeconds ?? 300;
|
|
1589
1590
|
}
|
|
1591
|
+
/**
|
|
1592
|
+
* The order size, in USD, below which `order.fees` must cover the execution cost.
|
|
1593
|
+
* Defaults to 20. An order at or above it is filled whatever fees it carries.
|
|
1594
|
+
*/
|
|
1595
|
+
getMinOrderSizeUsd() {
|
|
1596
|
+
return this.fillerConfig?.minOrderSizeUsd ?? DEFAULT_MIN_ORDER_SIZE_USD;
|
|
1597
|
+
}
|
|
1590
1598
|
/** Ceiling bps above user-requested output. Default 500 (5%). */
|
|
1591
1599
|
getMaxOverfillBps() {
|
|
1592
1600
|
return BigInt(this.fillerConfig?.overfillProtection?.maxOverfillBps ?? 500);
|
|
@@ -1690,6 +1698,12 @@ function validateConfig(config, cliWatchOnly = false) {
|
|
|
1690
1698
|
);
|
|
1691
1699
|
}
|
|
1692
1700
|
}
|
|
1701
|
+
const minOrderSize = config.simplex.minOrderSizeUsd;
|
|
1702
|
+
if (minOrderSize !== void 0) {
|
|
1703
|
+
if (!Number.isFinite(minOrderSize) || minOrderSize < 0) {
|
|
1704
|
+
throw new Error(`simplex.minOrderSizeUsd must be a number >= 0 (USD); got ${minOrderSize}`);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1693
1707
|
if (config.allowlist) {
|
|
1694
1708
|
for (const user of config.allowlist.users ?? []) {
|
|
1695
1709
|
if (!(0, import_viem2.isAddress)(user)) {
|
|
@@ -2663,6 +2677,35 @@ var LIMITS_QUERY = `
|
|
|
2663
2677
|
chains { id name tokens { symbol decimals } }
|
|
2664
2678
|
}
|
|
2665
2679
|
`;
|
|
2680
|
+
var LEVEL_FIELDS = "fillChain priceBucket price worstPrice baseSize quoteSize orderCount solverCount";
|
|
2681
|
+
var SNAPSHOT_QUERY = `
|
|
2682
|
+
query OrderbookSnapshot($id: ID!, $sourceChain: String, $fillChain: String) {
|
|
2683
|
+
book(id: $id) {
|
|
2684
|
+
id base quote
|
|
2685
|
+
bids: levels(side: BID, sourceChain: $sourceChain, fillChain: $fillChain) { ${LEVEL_FIELDS} }
|
|
2686
|
+
asks: levels(side: ASK, sourceChain: $sourceChain, fillChain: $fillChain) { ${LEVEL_FIELDS} }
|
|
2687
|
+
bidLiquidity: availableLiquidity(side: BID, sourceChain: $sourceChain, fillChain: $fillChain)
|
|
2688
|
+
askLiquidity: availableLiquidity(side: ASK, sourceChain: $sourceChain, fillChain: $fillChain)
|
|
2689
|
+
}
|
|
2690
|
+
serverInfo { priceGranularities { book granularity } }
|
|
2691
|
+
}
|
|
2692
|
+
`;
|
|
2693
|
+
var LEVEL_ORDERS_QUERY = `
|
|
2694
|
+
query OrderbookLevelOrders($id: ID!, $side: Side!, $fillChain: String, $sourceChain: String, $after: String) {
|
|
2695
|
+
book(id: $id) {
|
|
2696
|
+
orders(side: $side, fillChain: $fillChain, sourceChain: $sourceChain, first: 50, after: $after) {
|
|
2697
|
+
edges {
|
|
2698
|
+
node {
|
|
2699
|
+
solver { address }
|
|
2700
|
+
commitment fillChain price priceBucket advertisedSize quotedAmount resized expiresAt acceptedSources
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
pageInfo { hasNextPage endCursor }
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
`;
|
|
2708
|
+
var LEVEL_ORDER_PAGES = 20;
|
|
2666
2709
|
var POSTED_ORDER_FIELDS = "commitment side status price quotedAmount advertisedSize expiresAt acceptedSources";
|
|
2667
2710
|
var SUBMIT_ORDER_MUTATION = `
|
|
2668
2711
|
mutation SubmitOrder($userOp: Bytes!) {
|
|
@@ -2727,6 +2770,44 @@ var OrderbookClient = class {
|
|
|
2727
2770
|
async limits() {
|
|
2728
2771
|
return this.request(LIMITS_QUERY, {});
|
|
2729
2772
|
}
|
|
2773
|
+
/** The public book across all solvers, restricted to the selected route. */
|
|
2774
|
+
async snapshot(id, filters = {}) {
|
|
2775
|
+
const { book, serverInfo } = await this.request(SNAPSHOT_QUERY, {
|
|
2776
|
+
id,
|
|
2777
|
+
sourceChain: filters.sourceChain || null,
|
|
2778
|
+
fillChain: filters.fillChain || null
|
|
2779
|
+
});
|
|
2780
|
+
if (!book) return null;
|
|
2781
|
+
const granularity = serverInfo.priceGranularities.find((entry) => entry.book === book.id)?.granularity ?? null;
|
|
2782
|
+
return { ...book, granularity };
|
|
2783
|
+
}
|
|
2784
|
+
/**
|
|
2785
|
+
* The orders behind one price level. The orderbook filters orders by side and route but not by
|
|
2786
|
+
* price bucket, so this walks the side's orders on the level's fill chain and keeps the bucket.
|
|
2787
|
+
* Null when the book does not exist.
|
|
2788
|
+
*/
|
|
2789
|
+
async levelOrders(id, level) {
|
|
2790
|
+
const found = [];
|
|
2791
|
+
let after = null;
|
|
2792
|
+
for (let page = 0; page < LEVEL_ORDER_PAGES; page++) {
|
|
2793
|
+
const { book } = await this.request(LEVEL_ORDERS_QUERY, {
|
|
2794
|
+
id,
|
|
2795
|
+
side: level.side,
|
|
2796
|
+
fillChain: level.fillChain,
|
|
2797
|
+
sourceChain: level.sourceChain || null,
|
|
2798
|
+
after
|
|
2799
|
+
});
|
|
2800
|
+
if (!book) return null;
|
|
2801
|
+
for (const { node } of book.orders.edges) {
|
|
2802
|
+
if (node.priceBucket !== level.priceBucket) continue;
|
|
2803
|
+
const { solver, priceBucket: _bucket, ...order } = node;
|
|
2804
|
+
found.push({ ...order, solver: solver.address });
|
|
2805
|
+
}
|
|
2806
|
+
if (!book.orders.pageInfo.hasNextPage || !book.orders.pageInfo.endCursor) break;
|
|
2807
|
+
after = book.orders.pageInfo.endCursor;
|
|
2808
|
+
}
|
|
2809
|
+
return found;
|
|
2810
|
+
}
|
|
2730
2811
|
async submitOrder(userOp) {
|
|
2731
2812
|
const { submitOrder } = await this.request(SUBMIT_ORDER_MUTATION, { userOp });
|
|
2732
2813
|
switch (submitOrder.__typename) {
|
|
@@ -2911,16 +2992,16 @@ function signedAmounts(params) {
|
|
|
2911
2992
|
function offerFor(params) {
|
|
2912
2993
|
const { side, inputAmount, price, outputDecimals } = params;
|
|
2913
2994
|
if (price <= 0n) throw new Error("A limit order's price must be greater than zero");
|
|
2914
|
-
const
|
|
2995
|
+
const scaled2 = side === "BID" ? inputAmount * price / ORDERBOOK_SCALE : inputAmount * ORDERBOOK_SCALE / price;
|
|
2915
2996
|
const unit = outputDecimals >= 18 ? 1n : 10n ** BigInt(18 - outputDecimals);
|
|
2916
|
-
return
|
|
2997
|
+
return scaled2 / unit * unit;
|
|
2917
2998
|
}
|
|
2918
2999
|
function inputFor(params) {
|
|
2919
3000
|
const { side, outputAmount, price, inputDecimals } = params;
|
|
2920
3001
|
if (price <= 0n) throw new Error("A limit order's price must be greater than zero");
|
|
2921
|
-
const
|
|
3002
|
+
const scaled2 = side === "BID" ? divCeil(outputAmount * ORDERBOOK_SCALE, price) : divCeil(outputAmount * price, ORDERBOOK_SCALE);
|
|
2922
3003
|
const unit = inputDecimals >= 18 ? 1n : 10n ** BigInt(18 - inputDecimals);
|
|
2923
|
-
return divCeil(
|
|
3004
|
+
return divCeil(scaled2, unit) * unit;
|
|
2924
3005
|
}
|
|
2925
3006
|
|
|
2926
3007
|
// src/orderbook/matching.ts
|
|
@@ -3176,6 +3257,14 @@ var LimitOrderService = class {
|
|
|
3176
3257
|
throw err;
|
|
3177
3258
|
}
|
|
3178
3259
|
}
|
|
3260
|
+
/** Public market levels read through the configured orderbook client. */
|
|
3261
|
+
orderbookSnapshot(book, filters = {}) {
|
|
3262
|
+
return this.client.snapshot(book, filters);
|
|
3263
|
+
}
|
|
3264
|
+
/** Every solver's order behind one public price level. */
|
|
3265
|
+
orderbookLevelOrders(book, level) {
|
|
3266
|
+
return this.client.levelOrders(book, level);
|
|
3267
|
+
}
|
|
3179
3268
|
/**
|
|
3180
3269
|
* The pairs an order can be written against, the smallest payout each token may carry, and
|
|
3181
3270
|
* the tokens the orderbook registers on each chain.
|
|
@@ -3230,16 +3319,16 @@ var LimitOrderService = class {
|
|
|
3230
3319
|
if (!this.configService.getConfiguredChainIds().includes((0, import_sdk4.getChainId)(request.fillChain) ?? -1)) {
|
|
3231
3320
|
throw new LimitOrderValidationError(`'${request.fillChain}' is not a chain this filler is configured for`);
|
|
3232
3321
|
}
|
|
3233
|
-
const
|
|
3322
|
+
const scaled2 = { amountIn: 0n, amountOut: 0n };
|
|
3234
3323
|
for (const name of ["amountIn", "amountOut"]) {
|
|
3235
3324
|
try {
|
|
3236
|
-
|
|
3325
|
+
scaled2[name] = fromHuman(request[name] ?? "");
|
|
3237
3326
|
} catch (err) {
|
|
3238
3327
|
throw new LimitOrderValidationError(
|
|
3239
3328
|
`${name} must be an amount in whole tokens, like "1000" or "1500.25"; ${err.message}`
|
|
3240
3329
|
);
|
|
3241
3330
|
}
|
|
3242
|
-
if (
|
|
3331
|
+
if (scaled2[name] <= 0n) throw new LimitOrderValidationError(`${name} must be greater than zero`);
|
|
3243
3332
|
}
|
|
3244
3333
|
const sources = request.acceptedSources ?? [];
|
|
3245
3334
|
if (sources.length === 0) {
|
|
@@ -3286,7 +3375,7 @@ var LimitOrderService = class {
|
|
|
3286
3375
|
);
|
|
3287
3376
|
}
|
|
3288
3377
|
const floor = limits.serverInfo.minOrderSizes.find((entry) => entry.symbol === request.tokenOut);
|
|
3289
|
-
if (floor &&
|
|
3378
|
+
if (floor && scaled2.amountOut < BigInt(floor.size)) {
|
|
3290
3379
|
throw new LimitOrderValidationError(
|
|
3291
3380
|
`amountOut is below the orderbook's dust floor for ${request.tokenOut} (${toHuman(BigInt(floor.size))})`
|
|
3292
3381
|
);
|
|
@@ -3296,7 +3385,7 @@ var LimitOrderService = class {
|
|
|
3296
3385
|
throw new LimitOrderValidationError(`'${symbol}' does not resolve to a token address on ${request.fillChain}`);
|
|
3297
3386
|
}
|
|
3298
3387
|
}
|
|
3299
|
-
return { amountIn:
|
|
3388
|
+
return { amountIn: scaled2.amountIn, amountOut: scaled2.amountOut };
|
|
3300
3389
|
}
|
|
3301
3390
|
/**
|
|
3302
3391
|
* Refuses an order the wallet cannot pay out.
|
|
@@ -3822,6 +3911,451 @@ function underFunded(entry) {
|
|
|
3822
3911
|
return entry.resized ? `UNDER_FUNDED: the orderbook is advertising ${entry.advertisedSize} of the ${entry.quotedAmount} quoted` : "UNDER_FUNDED: the solver's balance does not cover this posting";
|
|
3823
3912
|
}
|
|
3824
3913
|
|
|
3914
|
+
// src/orderbook/profitability.ts
|
|
3915
|
+
var import_decimal3 = require("decimal.js");
|
|
3916
|
+
|
|
3917
|
+
// src/data/inventory.ts
|
|
3918
|
+
var INVENTORY_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3919
|
+
function inventoryOf(snapshot) {
|
|
3920
|
+
if (snapshot.status !== "fresh") return null;
|
|
3921
|
+
const held = {};
|
|
3922
|
+
for (const chain of snapshot.chains) {
|
|
3923
|
+
for (const asset of chain.assets) {
|
|
3924
|
+
if (asset.total === null) return null;
|
|
3925
|
+
held[asset.symbol] = (held[asset.symbol] ?? 0) + asset.total;
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
return Object.keys(held).length > 0 ? held : null;
|
|
3929
|
+
}
|
|
3930
|
+
function parseUtc(value) {
|
|
3931
|
+
return Date.parse(`${value.replace(" ", "T")}Z`);
|
|
3932
|
+
}
|
|
3933
|
+
var InventoryRecorder = class {
|
|
3934
|
+
constructor(store, balances, logger, now = Date.now) {
|
|
3935
|
+
this.store = store;
|
|
3936
|
+
this.balances = balances;
|
|
3937
|
+
this.logger = logger;
|
|
3938
|
+
this.now = now;
|
|
3939
|
+
this.recording = false;
|
|
3940
|
+
this.onSnapshot = (snapshot) => void this.consider(snapshot);
|
|
3941
|
+
}
|
|
3942
|
+
start() {
|
|
3943
|
+
this.balances.on("snapshot", this.onSnapshot);
|
|
3944
|
+
void this.consider(this.balances.getSnapshot());
|
|
3945
|
+
}
|
|
3946
|
+
stop() {
|
|
3947
|
+
this.balances.off("snapshot", this.onSnapshot);
|
|
3948
|
+
}
|
|
3949
|
+
/** Takes a snapshot from `snapshot` if one is due and the read is complete. Never rejects. */
|
|
3950
|
+
async consider(snapshot) {
|
|
3951
|
+
if (this.recording) return;
|
|
3952
|
+
const held = inventoryOf(snapshot);
|
|
3953
|
+
if (!held) return;
|
|
3954
|
+
this.recording = true;
|
|
3955
|
+
try {
|
|
3956
|
+
if (this.lastTakenAt === void 0) {
|
|
3957
|
+
const latest = await this.store.latest();
|
|
3958
|
+
this.lastTakenAt = latest ? parseUtc(latest.takenAt) : 0;
|
|
3959
|
+
}
|
|
3960
|
+
if (this.now() - this.lastTakenAt < INVENTORY_INTERVAL_MS) return;
|
|
3961
|
+
await this.store.record(held);
|
|
3962
|
+
this.lastTakenAt = this.now();
|
|
3963
|
+
this.logger.info({ tokens: Object.keys(held).length }, "Recorded an inventory snapshot");
|
|
3964
|
+
} catch (err) {
|
|
3965
|
+
this.logger.warn({ err }, "Could not record an inventory snapshot");
|
|
3966
|
+
} finally {
|
|
3967
|
+
this.recording = false;
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
};
|
|
3971
|
+
|
|
3972
|
+
// src/orderbook/usd.ts
|
|
3973
|
+
var import_decimal2 = require("decimal.js");
|
|
3974
|
+
function limitOrderUsdEdges(orders) {
|
|
3975
|
+
return orders.filter((order) => normalizeSymbol(order.base) !== normalizeSymbol(order.quote)).map((order) => ({
|
|
3976
|
+
base: normalizeSymbol(order.base),
|
|
3977
|
+
quote: normalizeSymbol(order.quote),
|
|
3978
|
+
rate: new import_decimal2.Decimal(order.price).div(ORDERBOOK_SCALE.toString())
|
|
3979
|
+
})).filter((edge) => edge.rate.isFinite() && edge.rate.gt(0));
|
|
3980
|
+
}
|
|
3981
|
+
function usdFactorsFrom(edges) {
|
|
3982
|
+
const factors = /* @__PURE__ */ new Map();
|
|
3983
|
+
for (const symbol of USD_STABLE_SYMBOLS) factors.set(symbol, new import_decimal2.Decimal(1));
|
|
3984
|
+
const ordered = [...edges].sort((a, b) => a.base.localeCompare(b.base) || a.quote.localeCompare(b.quote));
|
|
3985
|
+
let grew = true;
|
|
3986
|
+
while (grew) {
|
|
3987
|
+
grew = false;
|
|
3988
|
+
for (const edge of ordered) {
|
|
3989
|
+
const usdBase = factors.get(edge.base);
|
|
3990
|
+
const usdQuote = factors.get(edge.quote);
|
|
3991
|
+
if (usdBase && !usdQuote) {
|
|
3992
|
+
factors.set(edge.quote, usdBase.div(edge.rate));
|
|
3993
|
+
grew = true;
|
|
3994
|
+
} else if (usdQuote && !usdBase) {
|
|
3995
|
+
factors.set(edge.base, usdQuote.mul(edge.rate));
|
|
3996
|
+
grew = true;
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
return factors;
|
|
4001
|
+
}
|
|
4002
|
+
function usdValueOf(factors, symbol, amount) {
|
|
4003
|
+
const factor = factors.get(normalizeSymbol(symbol));
|
|
4004
|
+
return factor ? amount.mul(factor) : null;
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
// src/orderbook/profitability.ts
|
|
4008
|
+
var Decimal3 = import_decimal3.Decimal.clone({ precision: 40 });
|
|
4009
|
+
var PROFIT_PERIODS = {
|
|
4010
|
+
"7d": { bucket: "day", count: 7 },
|
|
4011
|
+
"30d": { bucket: "day", count: 30 },
|
|
4012
|
+
"12w": { bucket: "week", count: 12 },
|
|
4013
|
+
"12m": { bucket: "month", count: 12 },
|
|
4014
|
+
all: { bucket: "year", count: null }
|
|
4015
|
+
};
|
|
4016
|
+
var MINUTE = 6e4;
|
|
4017
|
+
var DAY = 864e5;
|
|
4018
|
+
var BALANCES_REACH_MS = 7 * DAY;
|
|
4019
|
+
function bucketStart(local, bucket) {
|
|
4020
|
+
const date = new Date(local);
|
|
4021
|
+
const year = date.getUTCFullYear();
|
|
4022
|
+
const month = date.getUTCMonth();
|
|
4023
|
+
if (bucket === "year") return Date.UTC(year, 0, 1);
|
|
4024
|
+
if (bucket === "month") return Date.UTC(year, month, 1);
|
|
4025
|
+
const day = Date.UTC(year, month, date.getUTCDate());
|
|
4026
|
+
return bucket === "day" ? day : day - (date.getUTCDay() + 6) % 7 * DAY;
|
|
4027
|
+
}
|
|
4028
|
+
function shiftBucket(start, bucket, steps) {
|
|
4029
|
+
if (bucket === "day") return start + steps * DAY;
|
|
4030
|
+
if (bucket === "week") return start + steps * 7 * DAY;
|
|
4031
|
+
const date = new Date(start);
|
|
4032
|
+
return bucket === "month" ? Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + steps, 1) : Date.UTC(date.getUTCFullYear() + steps, 0, 1);
|
|
4033
|
+
}
|
|
4034
|
+
function scaled(value) {
|
|
4035
|
+
return new Decimal3(value).div(ORDERBOOK_SCALE.toString());
|
|
4036
|
+
}
|
|
4037
|
+
function tradeOf(fill, at) {
|
|
4038
|
+
try {
|
|
4039
|
+
const paid = scaled(fill.amount);
|
|
4040
|
+
const taken = fill.amountIn === null ? null : scaled(fill.amountIn);
|
|
4041
|
+
const price = scaled(fill.price);
|
|
4042
|
+
if (!paid.gt(0) || !price.gt(0) || taken !== null && !taken.gt(0)) return null;
|
|
4043
|
+
const buying = fill.side === "BID";
|
|
4044
|
+
const [baseAmount, quoteAmount] = buying ? [taken ?? paid.div(price), paid] : [paid, taken ?? paid.mul(price)];
|
|
4045
|
+
return {
|
|
4046
|
+
at,
|
|
4047
|
+
book: fill.book,
|
|
4048
|
+
base: normalizeSymbol(fill.base),
|
|
4049
|
+
quote: normalizeSymbol(fill.quote),
|
|
4050
|
+
buying,
|
|
4051
|
+
baseAmount,
|
|
4052
|
+
quoteAmount,
|
|
4053
|
+
estimated: taken === null
|
|
4054
|
+
};
|
|
4055
|
+
} catch {
|
|
4056
|
+
return null;
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
function add(holdings, symbol, amount) {
|
|
4060
|
+
holdings.set(symbol, (holdings.get(symbol) ?? new Decimal3(0)).add(amount));
|
|
4061
|
+
}
|
|
4062
|
+
function apply(holdings, trade, sign) {
|
|
4063
|
+
const base = trade.buying ? trade.baseAmount : trade.baseAmount.neg();
|
|
4064
|
+
const quote = trade.buying ? trade.quoteAmount.neg() : trade.quoteAmount;
|
|
4065
|
+
add(holdings, trade.base, base.mul(sign));
|
|
4066
|
+
add(holdings, trade.quote, quote.mul(sign));
|
|
4067
|
+
}
|
|
4068
|
+
var Sums = class {
|
|
4069
|
+
constructor() {
|
|
4070
|
+
this.change = /* @__PURE__ */ new Map();
|
|
4071
|
+
this.boughtUsd = new Decimal3(0);
|
|
4072
|
+
this.soldUsd = new Decimal3(0);
|
|
4073
|
+
this.buys = 0;
|
|
4074
|
+
this.sells = 0;
|
|
4075
|
+
}
|
|
4076
|
+
};
|
|
4077
|
+
var BookLedger = class {
|
|
4078
|
+
constructor(id, base, quote) {
|
|
4079
|
+
this.id = id;
|
|
4080
|
+
this.base = base;
|
|
4081
|
+
this.quote = quote;
|
|
4082
|
+
this.bought = new Decimal3(0);
|
|
4083
|
+
this.boughtQuote = new Decimal3(0);
|
|
4084
|
+
this.sold = new Decimal3(0);
|
|
4085
|
+
this.soldQuote = new Decimal3(0);
|
|
4086
|
+
/** The rates of the book's latest buy and latest sell, over all of its history. */
|
|
4087
|
+
this.latestBuy = null;
|
|
4088
|
+
this.latestSell = null;
|
|
4089
|
+
this.inPeriod = false;
|
|
4090
|
+
}
|
|
4091
|
+
/**
|
|
4092
|
+
* The rate the book is valued at. Midway between the latest buy and the latest sell, because
|
|
4093
|
+
* either alone sits half a spread to one side and would price open volume against whichever
|
|
4094
|
+
* way the book happened to trade last.
|
|
4095
|
+
*/
|
|
4096
|
+
rate() {
|
|
4097
|
+
if (this.latestBuy && this.latestSell) return this.latestBuy.add(this.latestSell).div(2);
|
|
4098
|
+
return this.latestBuy ?? this.latestSell;
|
|
4099
|
+
}
|
|
4100
|
+
};
|
|
4101
|
+
function readingOf(reading) {
|
|
4102
|
+
const held = /* @__PURE__ */ new Map();
|
|
4103
|
+
const cover = /* @__PURE__ */ new Map();
|
|
4104
|
+
for (const [symbol, amount] of Object.entries(reading.balances)) {
|
|
4105
|
+
const chains = reading.chains[symbol];
|
|
4106
|
+
if (!Number.isFinite(amount) || !chains?.length) continue;
|
|
4107
|
+
add(held, normalizeSymbol(symbol), new Decimal3(amount));
|
|
4108
|
+
cover.set(normalizeSymbol(symbol), [...chains].sort().join(","));
|
|
4109
|
+
}
|
|
4110
|
+
return { held, cover };
|
|
4111
|
+
}
|
|
4112
|
+
function movedBetween(earlier, later) {
|
|
4113
|
+
if (!earlier || !later) return null;
|
|
4114
|
+
const moved = /* @__PURE__ */ new Map();
|
|
4115
|
+
for (const [symbol, cover] of later.cover) {
|
|
4116
|
+
if (earlier.cover.get(symbol) !== cover) continue;
|
|
4117
|
+
const before = earlier.held.get(symbol) ?? new Decimal3(0);
|
|
4118
|
+
moved.set(symbol, (later.held.get(symbol) ?? new Decimal3(0)).sub(before));
|
|
4119
|
+
}
|
|
4120
|
+
return moved;
|
|
4121
|
+
}
|
|
4122
|
+
function nearestRecord(instant, records) {
|
|
4123
|
+
let record;
|
|
4124
|
+
let gap = Number.POSITIVE_INFINITY;
|
|
4125
|
+
for (const candidate of records) {
|
|
4126
|
+
const distance = Math.abs(candidate.at - instant);
|
|
4127
|
+
if (candidate.live && distance > BALANCES_REACH_MS) continue;
|
|
4128
|
+
if (distance < gap || distance === gap && record?.live && !candidate.live) {
|
|
4129
|
+
record = candidate;
|
|
4130
|
+
gap = distance;
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
return record;
|
|
4134
|
+
}
|
|
4135
|
+
function inventoryAt(instant, records, trades, outflows, readings) {
|
|
4136
|
+
const record = nearestRecord(instant, records);
|
|
4137
|
+
if (!record) return null;
|
|
4138
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
4139
|
+
for (const [symbol, amount] of Object.entries(record.balances)) {
|
|
4140
|
+
if (Number.isFinite(amount)) add(recorded, normalizeSymbol(symbol), new Decimal3(amount));
|
|
4141
|
+
}
|
|
4142
|
+
const held = new Map(recorded);
|
|
4143
|
+
const after = record.at >= instant;
|
|
4144
|
+
const [from, to] = after ? [instant, record.at] : [record.at, instant];
|
|
4145
|
+
const sign = after ? -1 : 1;
|
|
4146
|
+
for (const trade of trades) {
|
|
4147
|
+
if (trade.at >= from && trade.at < to) apply(held, trade, sign);
|
|
4148
|
+
}
|
|
4149
|
+
for (const outflow of outflows) {
|
|
4150
|
+
if (outflow.at >= from && outflow.at < to) {
|
|
4151
|
+
add(held, normalizeSymbol(outflow.symbol), new Decimal3(outflow.amount).mul(-sign));
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
const moved = movedBetween(readings.get(from), readings.get(to));
|
|
4155
|
+
if (moved) {
|
|
4156
|
+
for (const [symbol, amount] of moved) {
|
|
4157
|
+
held.set(symbol, (recorded.get(symbol) ?? new Decimal3(0)).add(amount.mul(sign)));
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
for (const [symbol, amount] of held) if (amount.isNegative()) held.set(symbol, new Decimal3(0));
|
|
4161
|
+
return { held, record };
|
|
4162
|
+
}
|
|
4163
|
+
function tradesOf(fills, now) {
|
|
4164
|
+
const trades = [];
|
|
4165
|
+
for (const fill of fills) {
|
|
4166
|
+
const at = parseUtc(fill.filledAt);
|
|
4167
|
+
if (!Number.isFinite(at) || at > now) continue;
|
|
4168
|
+
const trade = tradeOf(fill, at);
|
|
4169
|
+
if (trade) trades.push(trade);
|
|
4170
|
+
}
|
|
4171
|
+
return trades.sort((a, b) => a.at - b.at);
|
|
4172
|
+
}
|
|
4173
|
+
function bucketStarts(period, now, offset, firstAt) {
|
|
4174
|
+
const { bucket, count } = PROFIT_PERIODS[period];
|
|
4175
|
+
const last = bucketStart(now - offset, bucket);
|
|
4176
|
+
const first = count === null ? bucketStart((firstAt ?? now) - offset, bucket) : shiftBucket(last, bucket, 1 - count);
|
|
4177
|
+
const starts = [];
|
|
4178
|
+
for (let start = first; start <= last; start = shiftBucket(start, bucket, 1)) starts.push(start);
|
|
4179
|
+
return starts;
|
|
4180
|
+
}
|
|
4181
|
+
function inventoryInstants(fills, options) {
|
|
4182
|
+
const { period, now } = options;
|
|
4183
|
+
const offset = (options.tzOffsetMinutes ?? 0) * MINUTE;
|
|
4184
|
+
const records = (options.inventory ?? []).filter((record) => record.at <= now);
|
|
4185
|
+
const instants = /* @__PURE__ */ new Set([now]);
|
|
4186
|
+
for (const start of bucketStarts(period, now, offset, tradesOf(fills, now)[0]?.at)) {
|
|
4187
|
+
instants.add(start + offset);
|
|
4188
|
+
const record = nearestRecord(start + offset, records);
|
|
4189
|
+
if (record) instants.add(record.at);
|
|
4190
|
+
}
|
|
4191
|
+
return [...instants].sort((a, b) => a - b);
|
|
4192
|
+
}
|
|
4193
|
+
function summarizeProfit(fills, options) {
|
|
4194
|
+
const { period, now } = options;
|
|
4195
|
+
const offset = (options.tzOffsetMinutes ?? 0) * MINUTE;
|
|
4196
|
+
const { bucket } = PROFIT_PERIODS[period];
|
|
4197
|
+
const records = (options.inventory ?? []).filter((record) => record.at <= now);
|
|
4198
|
+
const outflows = options.outflows ?? [];
|
|
4199
|
+
const readings = new Map((options.readings ?? []).map((reading) => [reading.at, readingOf(reading)]));
|
|
4200
|
+
const trades = tradesOf(fills, now);
|
|
4201
|
+
const starts = bucketStarts(period, now, offset, trades[0]?.at);
|
|
4202
|
+
const first = starts[0];
|
|
4203
|
+
const books = /* @__PURE__ */ new Map();
|
|
4204
|
+
const spelling = /* @__PURE__ */ new Map();
|
|
4205
|
+
for (const record of records) {
|
|
4206
|
+
for (const symbol of Object.keys(record.balances)) {
|
|
4207
|
+
if (!spelling.has(normalizeSymbol(symbol))) spelling.set(normalizeSymbol(symbol), symbol);
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
for (const fill of fills) {
|
|
4211
|
+
for (const symbol of [fill.base, fill.quote]) {
|
|
4212
|
+
if (!spelling.has(normalizeSymbol(symbol))) spelling.set(normalizeSymbol(symbol), symbol);
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
for (const trade of trades) {
|
|
4216
|
+
let book = books.get(trade.book);
|
|
4217
|
+
if (!book) {
|
|
4218
|
+
book = new BookLedger(trade.book, trade.base, trade.quote);
|
|
4219
|
+
books.set(trade.book, book);
|
|
4220
|
+
}
|
|
4221
|
+
const rate = trade.quoteAmount.div(trade.baseAmount);
|
|
4222
|
+
if (trade.buying) book.latestBuy = rate;
|
|
4223
|
+
else book.latestSell = rate;
|
|
4224
|
+
}
|
|
4225
|
+
const edges = [];
|
|
4226
|
+
for (const book of books.values()) {
|
|
4227
|
+
const rate = book.rate();
|
|
4228
|
+
if (rate && book.base !== book.quote) {
|
|
4229
|
+
edges.push({ base: book.base, quote: book.quote, rate: new import_decimal3.Decimal(rate.toString()) });
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
const factors = usdFactorsFrom(edges);
|
|
4233
|
+
const priceOf = (symbol) => {
|
|
4234
|
+
const factor = factors.get(symbol);
|
|
4235
|
+
return factor ? new Decimal3(factor.toString()) : null;
|
|
4236
|
+
};
|
|
4237
|
+
const unpriced = /* @__PURE__ */ new Set();
|
|
4238
|
+
const valueOf = (holdings) => {
|
|
4239
|
+
let total = new Decimal3(0);
|
|
4240
|
+
for (const [symbol, amount] of holdings) {
|
|
4241
|
+
if (amount.isZero()) continue;
|
|
4242
|
+
const price = priceOf(symbol);
|
|
4243
|
+
if (price) total = total.add(amount.mul(price));
|
|
4244
|
+
else unpriced.add(symbol);
|
|
4245
|
+
}
|
|
4246
|
+
return total;
|
|
4247
|
+
};
|
|
4248
|
+
const totals = new Sums();
|
|
4249
|
+
const buckets = new Map(starts.map((start) => [start, new Sums()]));
|
|
4250
|
+
let estimatedFills = 0;
|
|
4251
|
+
for (const trade of trades) {
|
|
4252
|
+
const sums = buckets.get(bucketStart(trade.at - offset, bucket));
|
|
4253
|
+
if (!sums) continue;
|
|
4254
|
+
const book = books.get(trade.book);
|
|
4255
|
+
if (!book) continue;
|
|
4256
|
+
book.inPeriod = true;
|
|
4257
|
+
if (trade.estimated) estimatedFills += 1;
|
|
4258
|
+
if (trade.buying) {
|
|
4259
|
+
book.bought = book.bought.add(trade.baseAmount);
|
|
4260
|
+
book.boughtQuote = book.boughtQuote.add(trade.quoteAmount);
|
|
4261
|
+
} else {
|
|
4262
|
+
book.sold = book.sold.add(trade.baseAmount);
|
|
4263
|
+
book.soldQuote = book.soldQuote.add(trade.quoteAmount);
|
|
4264
|
+
}
|
|
4265
|
+
const basePrice = priceOf(trade.base);
|
|
4266
|
+
const quotePrice = priceOf(trade.quote);
|
|
4267
|
+
const volume = basePrice ? trade.baseAmount.mul(basePrice) : quotePrice ? trade.quoteAmount.mul(quotePrice) : new Decimal3(0);
|
|
4268
|
+
for (const sum of [totals, sums]) {
|
|
4269
|
+
apply(sum.change, trade, 1);
|
|
4270
|
+
if (trade.buying) {
|
|
4271
|
+
sum.buys += 1;
|
|
4272
|
+
sum.boughtUsd = sum.boughtUsd.add(volume);
|
|
4273
|
+
} else {
|
|
4274
|
+
sum.sells += 1;
|
|
4275
|
+
sum.soldUsd = sum.soldUsd.add(volume);
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
const figures = (sums, startsAt, endsAt) => {
|
|
4280
|
+
const profit = valueOf(sums.change);
|
|
4281
|
+
const began2 = inventoryAt(startsAt, records, trades, outflows, readings);
|
|
4282
|
+
const inventory = began2 ? valueOf(began2.held) : null;
|
|
4283
|
+
const moved = movedBetween(readings.get(startsAt), readings.get(endsAt));
|
|
4284
|
+
if (moved) {
|
|
4285
|
+
for (const [symbol, amount] of sums.change) {
|
|
4286
|
+
if (moved.has(symbol)) moved.set(symbol, (moved.get(symbol) ?? new Decimal3(0)).sub(amount));
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
return {
|
|
4290
|
+
profitUsd: profit.toNumber(),
|
|
4291
|
+
startInventoryUsd: inventory ? inventory.toNumber() : null,
|
|
4292
|
+
returnPct: inventory?.gt(0) ? profit.div(inventory).mul(100).toNumber() : null,
|
|
4293
|
+
transfersUsd: moved && moved.size > 0 ? valueOf(moved).toNumber() : null,
|
|
4294
|
+
boughtUsd: sums.boughtUsd.toNumber(),
|
|
4295
|
+
soldUsd: sums.soldUsd.toNumber(),
|
|
4296
|
+
buys: sums.buys,
|
|
4297
|
+
sells: sums.sells
|
|
4298
|
+
};
|
|
4299
|
+
};
|
|
4300
|
+
const from = first + offset;
|
|
4301
|
+
const began = inventoryAt(from, records, trades, outflows, readings);
|
|
4302
|
+
const startInventory = began && {
|
|
4303
|
+
at: from,
|
|
4304
|
+
source: began.record.live ? "balances" : "snapshot",
|
|
4305
|
+
recordedAt: began.record.at,
|
|
4306
|
+
usd: valueOf(began.held).toNumber(),
|
|
4307
|
+
tokens: [...began.held].filter(([, amount]) => amount.gt(0)).map(([symbol, amount]) => {
|
|
4308
|
+
const price = priceOf(symbol);
|
|
4309
|
+
return {
|
|
4310
|
+
symbol: spelling.get(symbol) ?? symbol,
|
|
4311
|
+
amount: amount.toNumber(),
|
|
4312
|
+
usd: price ? amount.mul(price).toNumber() : null
|
|
4313
|
+
};
|
|
4314
|
+
}).sort((a, b) => (b.usd ?? 0) - (a.usd ?? 0) || a.symbol.localeCompare(b.symbol))
|
|
4315
|
+
};
|
|
4316
|
+
const rows = [];
|
|
4317
|
+
for (const book of books.values()) {
|
|
4318
|
+
if (!book.inPeriod) continue;
|
|
4319
|
+
const averageBuy = book.bought.gt(0) ? book.boughtQuote.div(book.bought) : null;
|
|
4320
|
+
const averageSell = book.sold.gt(0) ? book.soldQuote.div(book.sold) : null;
|
|
4321
|
+
const baseChange = book.bought.sub(book.sold);
|
|
4322
|
+
const quoteChange = book.soldQuote.sub(book.boughtQuote);
|
|
4323
|
+
const basePrice = priceOf(book.base);
|
|
4324
|
+
const quotePrice = priceOf(book.quote);
|
|
4325
|
+
rows.push({
|
|
4326
|
+
book: book.id,
|
|
4327
|
+
base: spelling.get(book.base) ?? book.base,
|
|
4328
|
+
quote: spelling.get(book.quote) ?? book.quote,
|
|
4329
|
+
bought: book.bought.toNumber(),
|
|
4330
|
+
sold: book.sold.toNumber(),
|
|
4331
|
+
averageBuy: averageBuy ? averageBuy.toNumber() : null,
|
|
4332
|
+
averageSell: averageSell ? averageSell.toNumber() : null,
|
|
4333
|
+
spreadPct: averageBuy && averageSell ? averageSell.div(averageBuy).sub(1).mul(100).toNumber() : null,
|
|
4334
|
+
baseChange: baseChange.toNumber(),
|
|
4335
|
+
quoteChange: quoteChange.toNumber(),
|
|
4336
|
+
rate: book.rate()?.toNumber() ?? null,
|
|
4337
|
+
profitUsd: basePrice && quotePrice ? baseChange.mul(basePrice).add(quoteChange.mul(quotePrice)).toNumber() : null
|
|
4338
|
+
});
|
|
4339
|
+
}
|
|
4340
|
+
rows.sort((a, b) => (b.profitUsd ?? 0) - (a.profitUsd ?? 0) || a.book.localeCompare(b.book));
|
|
4341
|
+
return {
|
|
4342
|
+
period,
|
|
4343
|
+
bucket,
|
|
4344
|
+
from,
|
|
4345
|
+
to: now,
|
|
4346
|
+
totals: figures(totals, from, now),
|
|
4347
|
+
series: starts.map((start, index) => {
|
|
4348
|
+
const sums = buckets.get(start) ?? new Sums();
|
|
4349
|
+
const endsAt = index + 1 < starts.length ? starts[index + 1] + offset : now;
|
|
4350
|
+
return { start: start + offset, ...figures(sums, start + offset, endsAt) };
|
|
4351
|
+
}),
|
|
4352
|
+
books: rows,
|
|
4353
|
+
startInventory,
|
|
4354
|
+
estimatedFills,
|
|
4355
|
+
unpricedTokens: [...unpriced].map((symbol) => spelling.get(symbol) ?? symbol).sort()
|
|
4356
|
+
};
|
|
4357
|
+
}
|
|
4358
|
+
|
|
3825
4359
|
// src/data/state.ts
|
|
3826
4360
|
async function patchRuntimeState(store, patch) {
|
|
3827
4361
|
if (store.patch) return store.patch(patch);
|
|
@@ -12861,6 +13395,7 @@ var CacheService = class {
|
|
|
12861
13395
|
matchedLimitOrders: {},
|
|
12862
13396
|
bidPlans: {},
|
|
12863
13397
|
partialFills: {},
|
|
13398
|
+
feeChecksWaived: {},
|
|
12864
13399
|
feeTokens: {},
|
|
12865
13400
|
tokenDecimals: {},
|
|
12866
13401
|
solverSelection: {}
|
|
@@ -12900,6 +13435,10 @@ var CacheService = class {
|
|
|
12900
13435
|
stalePartialIds.forEach((orderId) => {
|
|
12901
13436
|
delete this.cacheData.partialFills[orderId];
|
|
12902
13437
|
});
|
|
13438
|
+
const staleWaivedIds = Object.entries(this.cacheData.feeChecksWaived).filter(([_, data]) => !this.isCacheValid(data.timestamp)).map(([orderId]) => orderId);
|
|
13439
|
+
staleWaivedIds.forEach((orderId) => {
|
|
13440
|
+
delete this.cacheData.feeChecksWaived[orderId];
|
|
13441
|
+
});
|
|
12903
13442
|
}
|
|
12904
13443
|
getGasEstimate(orderId) {
|
|
12905
13444
|
try {
|
|
@@ -13081,6 +13620,7 @@ var CacheService = class {
|
|
|
13081
13620
|
})),
|
|
13082
13621
|
partialFill: plan.partialFill,
|
|
13083
13622
|
profit: plan.profit,
|
|
13623
|
+
...plan.inputDecimals !== void 0 ? { inputDecimals: plan.inputDecimals } : {},
|
|
13084
13624
|
...plan.budget ? { budget: budgetFromCache(plan.budget) } : {}
|
|
13085
13625
|
}));
|
|
13086
13626
|
} catch (error) {
|
|
@@ -13111,6 +13651,7 @@ var CacheService = class {
|
|
|
13111
13651
|
})),
|
|
13112
13652
|
partialFill: plan.partialFill,
|
|
13113
13653
|
profit: plan.profit,
|
|
13654
|
+
...plan.inputDecimals !== void 0 ? { inputDecimals: plan.inputDecimals } : {},
|
|
13114
13655
|
...plan.budget ? { budget: budgetToCache(plan.budget) } : {}
|
|
13115
13656
|
})),
|
|
13116
13657
|
timestamp: Date.now()
|
|
@@ -13212,6 +13753,24 @@ var CacheService = class {
|
|
|
13212
13753
|
clearPartialFill(orderId) {
|
|
13213
13754
|
delete this.cacheData.partialFills[orderId];
|
|
13214
13755
|
}
|
|
13756
|
+
/**
|
|
13757
|
+
* Whether the strategy's completed evaluation let a bid through without holding
|
|
13758
|
+
* `order.fees` to the execution cost, because the order is at or above the
|
|
13759
|
+
* operator's minimum size.
|
|
13760
|
+
*
|
|
13761
|
+
* Written and cleared the way {@link isPartialFill} is, for the same reason: the
|
|
13762
|
+
* caller exempts such an order from its profit floor.
|
|
13763
|
+
*/
|
|
13764
|
+
isFeeCheckWaived(orderId) {
|
|
13765
|
+
const cache = this.cacheData.feeChecksWaived[orderId];
|
|
13766
|
+
return cache !== void 0 && this.isCacheValid(cache.timestamp) && cache.waived;
|
|
13767
|
+
}
|
|
13768
|
+
setFeeCheckWaived(orderId, waived) {
|
|
13769
|
+
this.cacheData.feeChecksWaived[orderId] = { waived, timestamp: Date.now() };
|
|
13770
|
+
}
|
|
13771
|
+
clearFeeCheckWaived(orderId) {
|
|
13772
|
+
delete this.cacheData.feeChecksWaived[orderId];
|
|
13773
|
+
}
|
|
13215
13774
|
getFeeTokenWithDecimals(chain) {
|
|
13216
13775
|
try {
|
|
13217
13776
|
const cache = this.cacheData.feeTokens[chain];
|
|
@@ -15707,7 +16266,7 @@ var EVM_HOST = [
|
|
|
15707
16266
|
];
|
|
15708
16267
|
|
|
15709
16268
|
// src/services/ContractInteractionService.ts
|
|
15710
|
-
var
|
|
16269
|
+
var import_decimal4 = require("decimal.js");
|
|
15711
16270
|
|
|
15712
16271
|
// src/config/abis/IntentGatewayV2.ts
|
|
15713
16272
|
var INTENT_GATEWAY_V2_ABI = [
|
|
@@ -18335,7 +18894,7 @@ function describeShortfall(balances) {
|
|
|
18335
18894
|
}
|
|
18336
18895
|
|
|
18337
18896
|
// src/services/ContractInteractionService.ts
|
|
18338
|
-
|
|
18897
|
+
import_decimal4.Decimal.config({ precision: 28, rounding: 4 });
|
|
18339
18898
|
var FUNDING_GAS_PER_CALL = 400000n;
|
|
18340
18899
|
var DEBIT_ORDER_GAS = 60000n;
|
|
18341
18900
|
function settles(budget) {
|
|
@@ -18501,7 +19060,10 @@ var ContractInteractionService = class {
|
|
|
18501
19060
|
order,
|
|
18502
19061
|
prependCalls: void 0,
|
|
18503
19062
|
maxPriorityFeePerGasBumpPercent: gasFeeBumpConfig?.maxPriorityFeePerGasBumpPercent,
|
|
18504
|
-
maxFeePerGasBumpPercent: gasFeeBumpConfig?.maxFeePerGasBumpPercent
|
|
19063
|
+
maxFeePerGasBumpPercent: gasFeeBumpConfig?.maxFeePerGasBumpPercent,
|
|
19064
|
+
// No bid without the bundler's own figures: an op signed with the SDK's
|
|
19065
|
+
// fixed fallback limits does not execute, so a failed estimate skips the order.
|
|
19066
|
+
requireBundlerEstimate: true
|
|
18505
19067
|
});
|
|
18506
19068
|
this.logger.info({ orderId: order.id }, "Caching gas estimate");
|
|
18507
19069
|
this.logger.info({ estimate }, "Estimate");
|
|
@@ -18524,7 +19086,7 @@ var ContractInteractionService = class {
|
|
|
18524
19086
|
callGasLimit: estimate.callGasLimit
|
|
18525
19087
|
};
|
|
18526
19088
|
} catch (error) {
|
|
18527
|
-
this.logger.error({ err: error }, "Error estimating gas,
|
|
19089
|
+
this.logger.error({ err: error, orderId: order.id }, "Error estimating gas, not bidding on this order");
|
|
18528
19090
|
throw new Error(`Failed to estimate gas: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
18529
19091
|
}
|
|
18530
19092
|
}
|
|
@@ -18673,7 +19235,7 @@ var ContractInteractionService = class {
|
|
|
18673
19235
|
* @returns The total USD value of inputs (sum of normalized stable amounts, or 0 if none)
|
|
18674
19236
|
*/
|
|
18675
19237
|
async getInputUsdValue(order) {
|
|
18676
|
-
let inputUsdValue = new
|
|
19238
|
+
let inputUsdValue = new import_decimal4.Decimal(0);
|
|
18677
19239
|
const inputs = order.inputs;
|
|
18678
19240
|
const sourceUsdc = this.configService.getUsdcAsset(order.source).toLowerCase();
|
|
18679
19241
|
const sourceUsdt = this.configService.getUsdtAsset(order.source).toLowerCase();
|
|
@@ -18682,7 +19244,7 @@ var ContractInteractionService = class {
|
|
|
18682
19244
|
const addr = tokenAddress.toLowerCase();
|
|
18683
19245
|
if (addr !== sourceUsdc && addr !== sourceUsdt) continue;
|
|
18684
19246
|
const decimals = await this.getTokenDecimals(tokenAddress, order.source);
|
|
18685
|
-
const tokenAmount = new
|
|
19247
|
+
const tokenAmount = new import_decimal4.Decimal((0, import_viem23.formatUnits)(input.amount, decimals));
|
|
18686
19248
|
inputUsdValue = inputUsdValue.plus(tokenAmount);
|
|
18687
19249
|
}
|
|
18688
19250
|
return inputUsdValue;
|
|
@@ -19811,7 +20373,7 @@ var DelegationService = class {
|
|
|
19811
20373
|
// src/services/RebalancingService.ts
|
|
19812
20374
|
var import_viem28 = require("viem");
|
|
19813
20375
|
var import_sdk13 = require("@hyperbridge/sdk");
|
|
19814
|
-
var
|
|
20376
|
+
var import_decimal5 = require("decimal.js");
|
|
19815
20377
|
|
|
19816
20378
|
// src/services/rebalancers/binance.ts
|
|
19817
20379
|
var import_wallet2 = require("@binance/wallet");
|
|
@@ -20962,7 +21524,7 @@ var RebalancingService = class {
|
|
|
20962
21524
|
functionName: "balanceOf",
|
|
20963
21525
|
args: [this.walletAddress]
|
|
20964
21526
|
});
|
|
20965
|
-
const balanceDecimal = new
|
|
21527
|
+
const balanceDecimal = new import_decimal5.Decimal((0, import_viem28.formatUnits)(balanceWei, Number(decimals)));
|
|
20966
21528
|
return balanceDecimal.toNumber();
|
|
20967
21529
|
} catch (error) {
|
|
20968
21530
|
this.logger.error({ error, chain, asset }, "Failed to get current balance");
|
|
@@ -20995,7 +21557,7 @@ var RebalancingService = class {
|
|
|
20995
21557
|
};
|
|
20996
21558
|
|
|
20997
21559
|
// src/core/filler.ts
|
|
20998
|
-
var
|
|
21560
|
+
var import_decimal6 = require("decimal.js");
|
|
20999
21561
|
var HYPERBRIDGE_CONNECT_TIMEOUT_MS = 3e4;
|
|
21000
21562
|
function byAmount(a, b) {
|
|
21001
21563
|
return BigInt(a.amount) < BigInt(b.amount) ? -1 : BigInt(a.amount) > BigInt(b.amount) ? 1 : 0;
|
|
@@ -21551,7 +22113,7 @@ var IntentFiller = class {
|
|
|
21551
22113
|
try {
|
|
21552
22114
|
const stratValue = await strategy.getOrderUsdValue(order);
|
|
21553
22115
|
if (stratValue != null) {
|
|
21554
|
-
inputUsdValue =
|
|
22116
|
+
inputUsdValue = import_decimal6.Decimal.max(baseInputUsd, stratValue.inputUsd);
|
|
21555
22117
|
valued = true;
|
|
21556
22118
|
break;
|
|
21557
22119
|
}
|
|
@@ -21679,6 +22241,23 @@ var IntentFiller = class {
|
|
|
21679
22241
|
this.monitor.emit("orderSkipped", { orderId: order.id, reason: "watch-only" });
|
|
21680
22242
|
return null;
|
|
21681
22243
|
}
|
|
22244
|
+
const destinationHead = await this.destinationHead(order);
|
|
22245
|
+
if (destinationHead !== null && order.deadline <= destinationHead) {
|
|
22246
|
+
this.logger.info(
|
|
22247
|
+
{
|
|
22248
|
+
orderId: order.id,
|
|
22249
|
+
destChain: order.destination,
|
|
22250
|
+
deadline: order.deadline.toString(),
|
|
22251
|
+
head: destinationHead.toString()
|
|
22252
|
+
},
|
|
22253
|
+
"Skipping order: its deadline has passed on the destination chain"
|
|
22254
|
+
);
|
|
22255
|
+
this.monitor.emit("orderSkipped", {
|
|
22256
|
+
orderId: order.id,
|
|
22257
|
+
reason: "Order deadline has passed on the destination chain"
|
|
22258
|
+
});
|
|
22259
|
+
return null;
|
|
22260
|
+
}
|
|
21682
22261
|
const evalStartMs = Date.now();
|
|
21683
22262
|
const eligibleStrategies = await Promise.all(
|
|
21684
22263
|
this.strategies.map(async (strategy) => {
|
|
@@ -21688,7 +22267,10 @@ var IntentFiller = class {
|
|
|
21688
22267
|
})
|
|
21689
22268
|
);
|
|
21690
22269
|
const fillsPartially = this.fillsPartially(order);
|
|
21691
|
-
const
|
|
22270
|
+
const feeCheckWaived = this.feeCheckWaived(order);
|
|
22271
|
+
const validStrategies = eligibleStrategies.filter(
|
|
22272
|
+
(s) => s !== null && (s.profitability > 0 || fillsPartially || feeCheckWaived)
|
|
22273
|
+
).sort((a, b) => b.profitability - a.profitability);
|
|
21692
22274
|
const evalDurationSec = (Date.now() - evalStartMs) / 1e3;
|
|
21693
22275
|
this.monitor.emit("orderTiming", { orderId: order.id, phase: "evaluation", durationSec: evalDurationSec });
|
|
21694
22276
|
if (validStrategies.length === 0) {
|
|
@@ -21713,6 +22295,23 @@ var IntentFiller = class {
|
|
|
21713
22295
|
);
|
|
21714
22296
|
return validStrategies[0];
|
|
21715
22297
|
}
|
|
22298
|
+
/**
|
|
22299
|
+
* The destination chain's latest block number, the clock `order.deadline` is read on.
|
|
22300
|
+
*
|
|
22301
|
+
* Null when the read fails. That is not an answer about the order, so evaluation goes
|
|
22302
|
+
* on: the gas estimate simulates the same deadline check and refuses an expired order.
|
|
22303
|
+
*/
|
|
22304
|
+
async destinationHead(order) {
|
|
22305
|
+
try {
|
|
22306
|
+
return await this.chainClientManager.getPublicClient(order.destination).getBlockNumber();
|
|
22307
|
+
} catch (err) {
|
|
22308
|
+
this.logger.warn(
|
|
22309
|
+
{ orderId: order.id, destChain: order.destination, err },
|
|
22310
|
+
"Could not read the destination head to check the order's deadline"
|
|
22311
|
+
);
|
|
22312
|
+
return null;
|
|
22313
|
+
}
|
|
22314
|
+
}
|
|
21716
22315
|
/**
|
|
21717
22316
|
* Whether the strategy's evaluation concluded in a deliberate partial fill.
|
|
21718
22317
|
*
|
|
@@ -21726,6 +22325,15 @@ var IntentFiller = class {
|
|
|
21726
22325
|
if (!order.id) return false;
|
|
21727
22326
|
return this.contractService.cacheService.isPartialFill(order.id);
|
|
21728
22327
|
}
|
|
22328
|
+
/**
|
|
22329
|
+
* Whether the strategy's evaluation let a bid through without the fee check, because
|
|
22330
|
+
* the order is at or above the operator's minimum size. Read from a flag the strategy
|
|
22331
|
+
* sets only once it has an answer, as {@link fillsPartially} is.
|
|
22332
|
+
*/
|
|
22333
|
+
feeCheckWaived(order) {
|
|
22334
|
+
if (!order.id) return false;
|
|
22335
|
+
return this.contractService.cacheService.isFeeCheckWaived(order.id);
|
|
22336
|
+
}
|
|
21729
22337
|
executeOrder(order, bestStrategy, solverSelectionActive, inputUsdValue, profitUsd) {
|
|
21730
22338
|
const chainQueue = this.chainQueues.get((0, import_sdk14.getChainId)(order.destination));
|
|
21731
22339
|
if (!chainQueue) {
|
|
@@ -21753,7 +22361,12 @@ var IntentFiller = class {
|
|
|
21753
22361
|
else cache.clearFundingPrepends(order.id);
|
|
21754
22362
|
}
|
|
21755
22363
|
reservation = await this.holdAll([
|
|
21756
|
-
{
|
|
22364
|
+
{
|
|
22365
|
+
limitOrderId: plan.limitOrderId,
|
|
22366
|
+
payout: plan.payout,
|
|
22367
|
+
take: plan.fillerInputs[plan.leg]?.amount,
|
|
22368
|
+
takeDecimals: plan.inputDecimals
|
|
22369
|
+
}
|
|
21757
22370
|
]);
|
|
21758
22371
|
if (reservation.length === 0) {
|
|
21759
22372
|
this.logger.info(
|
|
@@ -21923,7 +22536,8 @@ var IntentFiller = class {
|
|
|
21923
22536
|
taken.push({
|
|
21924
22537
|
limitOrderId: hold.limitOrderId,
|
|
21925
22538
|
amount,
|
|
21926
|
-
...hold.take !== void 0 ? { take: hold.take.toString() } : {}
|
|
22539
|
+
...hold.take !== void 0 ? { take: hold.take.toString() } : {},
|
|
22540
|
+
...hold.takeDecimals !== void 0 ? { takeDecimals: hold.takeDecimals } : {}
|
|
21927
22541
|
});
|
|
21928
22542
|
}
|
|
21929
22543
|
return taken;
|
|
@@ -21950,6 +22564,7 @@ var IntentFiller = class {
|
|
|
21950
22564
|
}
|
|
21951
22565
|
const settled = executedHold(claimed, delivered.scaled, released);
|
|
21952
22566
|
const charged = chargedFor(settled, delivered, released);
|
|
22567
|
+
const takenIn = released > 0n && settled.takeDecimals !== void 0 ? toScaled(released, settled.takeDecimals) : null;
|
|
21953
22568
|
const settledBid = rows.find(
|
|
21954
22569
|
(row) => row.reservations.some(
|
|
21955
22570
|
(hold) => hold.limitOrderId === settled.limitOrderId && hold.amount === settled.amount && hold.take === settled.take
|
|
@@ -21968,6 +22583,7 @@ var IntentFiller = class {
|
|
|
21968
22583
|
commitment,
|
|
21969
22584
|
bid: settledBid,
|
|
21970
22585
|
amount: share.toString(),
|
|
22586
|
+
amountIn: takenIn?.toString() ?? null,
|
|
21971
22587
|
transactionHash: transactionHash ?? null
|
|
21972
22588
|
});
|
|
21973
22589
|
}
|
|
@@ -22116,44 +22732,7 @@ var IntentFiller = class {
|
|
|
22116
22732
|
// src/strategies/fx.ts
|
|
22117
22733
|
var import_sdk15 = require("@hyperbridge/sdk");
|
|
22118
22734
|
var import_viem29 = require("viem");
|
|
22119
|
-
var
|
|
22120
|
-
|
|
22121
|
-
// src/orderbook/usd.ts
|
|
22122
|
-
var import_decimal5 = require("decimal.js");
|
|
22123
|
-
function limitOrderUsdEdges(orders) {
|
|
22124
|
-
return orders.filter((order) => normalizeSymbol(order.base) !== normalizeSymbol(order.quote)).map((order) => ({
|
|
22125
|
-
base: normalizeSymbol(order.base),
|
|
22126
|
-
quote: normalizeSymbol(order.quote),
|
|
22127
|
-
rate: new import_decimal5.Decimal(order.price).div(ORDERBOOK_SCALE.toString())
|
|
22128
|
-
})).filter((edge) => edge.rate.isFinite() && edge.rate.gt(0));
|
|
22129
|
-
}
|
|
22130
|
-
function usdFactorsFrom(edges) {
|
|
22131
|
-
const factors = /* @__PURE__ */ new Map();
|
|
22132
|
-
for (const symbol of USD_STABLE_SYMBOLS) factors.set(symbol, new import_decimal5.Decimal(1));
|
|
22133
|
-
const ordered = [...edges].sort((a, b) => a.base.localeCompare(b.base) || a.quote.localeCompare(b.quote));
|
|
22134
|
-
let grew = true;
|
|
22135
|
-
while (grew) {
|
|
22136
|
-
grew = false;
|
|
22137
|
-
for (const edge of ordered) {
|
|
22138
|
-
const usdBase = factors.get(edge.base);
|
|
22139
|
-
const usdQuote = factors.get(edge.quote);
|
|
22140
|
-
if (usdBase && !usdQuote) {
|
|
22141
|
-
factors.set(edge.quote, usdBase.div(edge.rate));
|
|
22142
|
-
grew = true;
|
|
22143
|
-
} else if (usdQuote && !usdBase) {
|
|
22144
|
-
factors.set(edge.base, usdQuote.mul(edge.rate));
|
|
22145
|
-
grew = true;
|
|
22146
|
-
}
|
|
22147
|
-
}
|
|
22148
|
-
}
|
|
22149
|
-
return factors;
|
|
22150
|
-
}
|
|
22151
|
-
function usdValueOf(factors, symbol, amount) {
|
|
22152
|
-
const factor = factors.get(normalizeSymbol(symbol));
|
|
22153
|
-
return factor ? amount.mul(factor) : null;
|
|
22154
|
-
}
|
|
22155
|
-
|
|
22156
|
-
// src/strategies/fx.ts
|
|
22735
|
+
var import_decimal7 = require("decimal.js");
|
|
22157
22736
|
function adjustDecimalsFloor(amount, fromDecimals, toDecimals) {
|
|
22158
22737
|
if (fromDecimals === toDecimals) return amount;
|
|
22159
22738
|
if (fromDecimals < toDecimals) return amount * BigInt(10 ** (toDecimals - fromDecimals));
|
|
@@ -22201,7 +22780,7 @@ var FXFiller = class _FXFiller {
|
|
|
22201
22780
|
this.maxConsecutiveClamps = configService.getMaxConsecutiveClamps();
|
|
22202
22781
|
if (confirmationPolicy) {
|
|
22203
22782
|
this.confirmationPolicy = {
|
|
22204
|
-
getConfirmationBlocks: (chainId, amountUsd) => confirmationPolicy.getConfirmationBlocks(chainId, new
|
|
22783
|
+
getConfirmationBlocks: (chainId, amountUsd) => confirmationPolicy.getConfirmationBlocks(chainId, new import_decimal7.Decimal(amountUsd))
|
|
22205
22784
|
};
|
|
22206
22785
|
}
|
|
22207
22786
|
}
|
|
@@ -22297,11 +22876,13 @@ var FXFiller = class _FXFiller {
|
|
|
22297
22876
|
* - Treat a payout below the ask as a partial fill, which only orders
|
|
22298
22877
|
* without output calldata and without an earlier partial fill allow.
|
|
22299
22878
|
* - Cap each payout by the filler's balance plus funding-venue withdrawals,
|
|
22300
|
-
* then gate it on fees (full fills) and the
|
|
22879
|
+
* then gate it on fees (full fills of orders below `minOrderSizeUsd`) and the
|
|
22880
|
+
* same-asset spread.
|
|
22301
22881
|
* - Cache the resulting bids for later use in `executeOrder`.
|
|
22302
22882
|
*/
|
|
22303
22883
|
async calculateProfitability(order) {
|
|
22304
22884
|
if (order.id) this.contractService.cacheService.clearPartialFill(order.id);
|
|
22885
|
+
if (order.id) this.contractService.cacheService.clearFeeCheckWaived(order.id);
|
|
22305
22886
|
if (this.halted) {
|
|
22306
22887
|
this.logger.warn({ orderId: order.id }, "FXFiller halted \u2014 rejecting order");
|
|
22307
22888
|
return 0;
|
|
@@ -22317,6 +22898,15 @@ var FXFiller = class _FXFiller {
|
|
|
22317
22898
|
const plans = [];
|
|
22318
22899
|
const plannedOn = /* @__PURE__ */ new Map();
|
|
22319
22900
|
const spentOn = /* @__PURE__ */ new Map();
|
|
22901
|
+
let atOrAboveMinSize;
|
|
22902
|
+
const feeCheckWaived = async () => {
|
|
22903
|
+
if (atOrAboveMinSize === void 0) {
|
|
22904
|
+
const usd = await this.getOrderUsdValue(order);
|
|
22905
|
+
atOrAboveMinSize = usd !== null && usd.inputUsd.gte(this.configService.getMinOrderSizeUsd());
|
|
22906
|
+
}
|
|
22907
|
+
return atOrAboveMinSize;
|
|
22908
|
+
};
|
|
22909
|
+
let feesWaived = false;
|
|
22320
22910
|
for (let leg = 0; leg < order.inputs.length; leg++) {
|
|
22321
22911
|
const input = order.inputs[leg];
|
|
22322
22912
|
const output = order.output.assets[leg];
|
|
@@ -22526,17 +23116,17 @@ var FXFiller = class _FXFiller {
|
|
|
22526
23116
|
const outputSymbol = this.registry.symbolFor(outputToken, destChain);
|
|
22527
23117
|
const sameAsset = outputSymbol !== null && normalizeSymbol(outputSymbol) === normalizeSymbol(candidate.order.base) && normalizeSymbol(outputSymbol) === normalizeSymbol(candidate.order.quote);
|
|
22528
23118
|
let realizedSpreadProfit = 0n;
|
|
22529
|
-
let sameAssetEdgeUsd = new
|
|
23119
|
+
let sameAssetEdgeUsd = new import_decimal7.Decimal(0);
|
|
22530
23120
|
let sameAssetProfitable = true;
|
|
22531
23121
|
if (sameAsset) {
|
|
22532
23122
|
const convertedInput = adjustDecimalsFloor(releasedInput, inputDecimals, outputDecimals);
|
|
22533
23123
|
const spread = convertedInput - finalOutputAmount;
|
|
22534
23124
|
if (spread <= 0n) sameAssetProfitable = false;
|
|
22535
23125
|
realizedSpreadProfit = adjustDecimalsFloor(spread, outputDecimals, feeTokenDecimals);
|
|
22536
|
-
const spreadUsd = outputSymbol && usdValueOf(usdFactors, outputSymbol, new
|
|
23126
|
+
const spreadUsd = outputSymbol && usdValueOf(usdFactors, outputSymbol, new import_decimal7.Decimal((0, import_viem29.formatUnits)(spread, outputDecimals)));
|
|
22537
23127
|
if (spreadUsd) sameAssetEdgeUsd = spreadUsd;
|
|
22538
23128
|
}
|
|
22539
|
-
const payoutSurplusUsd = new
|
|
23129
|
+
const payoutSurplusUsd = new import_decimal7.Decimal(0);
|
|
22540
23130
|
const { totalCostInSourceFeeToken, relayerFeeInSourceFeeToken, dispatchFee } = await this.contractService.estimateGasFillPost(order);
|
|
22541
23131
|
if (sourceChain !== destChain && dispatchFee > 0n) {
|
|
22542
23132
|
const feeToken = await this.contractService.getFeeTokenWithDecimals(destChain);
|
|
@@ -22560,17 +23150,21 @@ var FXFiller = class _FXFiller {
|
|
|
22560
23150
|
const executionCost = totalCostInSourceFeeToken + relayerFeeInSourceFeeToken;
|
|
22561
23151
|
const partialEdgeUsd = sameAssetEdgeUsd.plus(payoutSurplusUsd);
|
|
22562
23152
|
if (!partialFill && order.fees < executionCost) {
|
|
22563
|
-
|
|
22564
|
-
|
|
22565
|
-
|
|
22566
|
-
|
|
22567
|
-
|
|
22568
|
-
|
|
22569
|
-
|
|
22570
|
-
|
|
22571
|
-
|
|
22572
|
-
|
|
22573
|
-
|
|
23153
|
+
if (!await feeCheckWaived()) {
|
|
23154
|
+
this.logger.info(
|
|
23155
|
+
{
|
|
23156
|
+
orderId: order.id,
|
|
23157
|
+
orderFees: (0, import_viem29.formatUnits)(order.fees, feeTokenDecimals),
|
|
23158
|
+
fillGas: (0, import_viem29.formatUnits)(totalCostInSourceFeeToken, feeTokenDecimals),
|
|
23159
|
+
relayerFee: (0, import_viem29.formatUnits)(relayerFeeInSourceFeeToken, feeTokenDecimals),
|
|
23160
|
+
executionCost: (0, import_viem29.formatUnits)(executionCost, feeTokenDecimals),
|
|
23161
|
+
minOrderSizeUsd: this.configService.getMinOrderSizeUsd()
|
|
23162
|
+
},
|
|
23163
|
+
"Skipping order: attached fees do not cover execution cost (fill gas + relayer fee)"
|
|
23164
|
+
);
|
|
23165
|
+
continue;
|
|
23166
|
+
}
|
|
23167
|
+
feesWaived = true;
|
|
22574
23168
|
}
|
|
22575
23169
|
if (sameAsset && !sameAssetProfitable) {
|
|
22576
23170
|
this.logger.info(
|
|
@@ -22604,7 +23198,8 @@ var FXFiller = class _FXFiller {
|
|
|
22604
23198
|
realizedSpreadProfit: (0, import_viem29.formatUnits)(realizedSpreadProfit, feeTokenDecimals),
|
|
22605
23199
|
payoutSurplusUsd: payoutSurplusUsd.toString(),
|
|
22606
23200
|
totalProfit,
|
|
22607
|
-
profitable: totalProfit > 0
|
|
23201
|
+
profitable: totalProfit > 0,
|
|
23202
|
+
feeCheckWaived: !partialFill && order.fees < executionCost
|
|
22608
23203
|
},
|
|
22609
23204
|
"FX swap profitability evaluation"
|
|
22610
23205
|
);
|
|
@@ -22618,6 +23213,7 @@ var FXFiller = class _FXFiller {
|
|
|
22618
23213
|
payout: toScaled(finalOutputAmount, outputDecimals),
|
|
22619
23214
|
fillerOutputs,
|
|
22620
23215
|
fillerInputs,
|
|
23216
|
+
inputDecimals,
|
|
22621
23217
|
fundingCalls: [...fundingCalls],
|
|
22622
23218
|
partialFill,
|
|
22623
23219
|
profit: totalProfit,
|
|
@@ -22636,6 +23232,7 @@ var FXFiller = class _FXFiller {
|
|
|
22636
23232
|
plans[0].budget
|
|
22637
23233
|
);
|
|
22638
23234
|
this.contractService.cacheService.setPartialFill(order.id, plans[0].partialFill);
|
|
23235
|
+
this.contractService.cacheService.setFeeCheckWaived(order.id, feesWaived);
|
|
22639
23236
|
this.contractService.cacheService.setMatchedLimitOrder(order.id, [
|
|
22640
23237
|
{ limitOrderId: plans[0].limitOrderId, payout: plans[0].payout }
|
|
22641
23238
|
]);
|
|
@@ -22895,13 +23492,13 @@ var FXFiller = class _FXFiller {
|
|
|
22895
23492
|
async getOrderUsdValue(order) {
|
|
22896
23493
|
if (!this.limitOrders || order.inputs.length === 0) return null;
|
|
22897
23494
|
const factors = usdFactorsFrom(limitOrderUsdEdges(await this.limitOrders.open()));
|
|
22898
|
-
let inputUsd = new
|
|
23495
|
+
let inputUsd = new import_decimal7.Decimal(0);
|
|
22899
23496
|
for (const input of order.inputs) {
|
|
22900
23497
|
const inputToken = (0, import_sdk15.bytes32ToBytes20)(input.token);
|
|
22901
23498
|
const symbol = this.registry.symbolFor(inputToken, order.source);
|
|
22902
23499
|
if (!symbol) return null;
|
|
22903
23500
|
const decimals = await this.contractService.getTokenDecimals(inputToken, order.source);
|
|
22904
|
-
const legUsd = usdValueOf(factors, symbol, new
|
|
23501
|
+
const legUsd = usdValueOf(factors, symbol, new import_decimal7.Decimal((0, import_viem29.formatUnits)(input.amount, decimals)));
|
|
22905
23502
|
if (!legUsd) return null;
|
|
22906
23503
|
inputUsd = inputUsd.plus(legUsd);
|
|
22907
23504
|
}
|
|
@@ -23877,6 +24474,7 @@ async function bootFiller(config, options) {
|
|
|
23877
24474
|
rebalancing: config.rebalancing,
|
|
23878
24475
|
targetGasUnits: config.simplex.targetGasUnits,
|
|
23879
24476
|
bidValiditySeconds: config.simplex.bidValiditySeconds,
|
|
24477
|
+
minOrderSizeUsd: config.simplex.minOrderSizeUsd,
|
|
23880
24478
|
blockScanIntervalSeconds: config.simplex.blockScanIntervalSeconds,
|
|
23881
24479
|
gasFeeBump: config.simplex.gasFeeBump,
|
|
23882
24480
|
overfillProtection: config.simplex.overfillProtection,
|
|
@@ -24085,9 +24683,10 @@ async function bootFiller(config, options) {
|
|
|
24085
24683
|
const activity = new ActivityRecorder(options.data.activity, options.loggers, { describeToken });
|
|
24086
24684
|
activity.attach(intentFiller.monitor);
|
|
24087
24685
|
const network = resolvedChains.some((chain) => chainByChainId(chain.chainId)?.network === "testnet") ? "testnet" : "mainnet";
|
|
24686
|
+
const indexerUrl = config.simplex.indexerUrl ?? DEFAULT_INDEXER_URLS[network];
|
|
24088
24687
|
void backfillOrderSummaries({
|
|
24089
24688
|
store: options.data.activity,
|
|
24090
|
-
indexerUrl
|
|
24689
|
+
indexerUrl,
|
|
24091
24690
|
fillerAddress: runtimeSigner.address,
|
|
24092
24691
|
describeToken,
|
|
24093
24692
|
onUpdated: (rows) => {
|
|
@@ -24153,6 +24752,15 @@ async function bootFiller(config, options) {
|
|
|
24153
24752
|
vaultVenue?.startSweeping();
|
|
24154
24753
|
started.push(() => balanceProvider.stop());
|
|
24155
24754
|
await balanceProvider.start();
|
|
24755
|
+
if (options.signer) {
|
|
24756
|
+
const inventoryRecorder = new InventoryRecorder(
|
|
24757
|
+
options.data.inventory,
|
|
24758
|
+
balanceProvider,
|
|
24759
|
+
moduleLogger(options.loggers, "inventory")
|
|
24760
|
+
);
|
|
24761
|
+
started.push(() => inventoryRecorder.stop());
|
|
24762
|
+
inventoryRecorder.start();
|
|
24763
|
+
}
|
|
24156
24764
|
const watchOnlyChains = watchOnlyConfig ? Object.entries(watchOnlyConfig).filter(([, value]) => value === true).map(([chainId]) => Number.parseInt(chainId, 10)) : [];
|
|
24157
24765
|
logger.info(
|
|
24158
24766
|
{
|
|
@@ -24198,6 +24806,8 @@ async function bootFiller(config, options) {
|
|
|
24198
24806
|
rebalancingService,
|
|
24199
24807
|
resolvedChains,
|
|
24200
24808
|
fillerAddress: runtimeSigner.address,
|
|
24809
|
+
indexerUrl,
|
|
24810
|
+
describeToken,
|
|
24201
24811
|
watchOnly: watchOnlyConfig,
|
|
24202
24812
|
config,
|
|
24203
24813
|
configPath: options.configPath,
|
|
@@ -24472,10 +25082,20 @@ var MemoryLimitOrderStore = class {
|
|
|
24472
25082
|
commitment: fill.commitment,
|
|
24473
25083
|
bid: fill.bid ?? null,
|
|
24474
25084
|
amount: fill.amount,
|
|
25085
|
+
amountIn: fill.amountIn ?? null,
|
|
24475
25086
|
transactionHash: fill.transactionHash ?? null,
|
|
24476
25087
|
filledAt: sqliteDatetime(/* @__PURE__ */ new Date())
|
|
24477
25088
|
});
|
|
24478
25089
|
}
|
|
25090
|
+
async fillHistory() {
|
|
25091
|
+
return this.fillRows.flatMap((fill) => {
|
|
25092
|
+
const order = this.orders.get(fill.limitOrderId);
|
|
25093
|
+
if (!order) return [];
|
|
25094
|
+
const { id, limitOrderId, amount, amountIn, filledAt } = fill;
|
|
25095
|
+
const { book, base, quote, side, price } = order;
|
|
25096
|
+
return [{ id, limitOrderId, book, base, quote, side, price, amount, amountIn, filledAt }];
|
|
25097
|
+
});
|
|
25098
|
+
}
|
|
24479
25099
|
async fills(limitOrderId, limit = 100) {
|
|
24480
25100
|
return this.fillRows.filter((fill) => fill.limitOrderId === limitOrderId).slice(-capLimit(limit)).reverse().map((fill) => ({ ...fill }));
|
|
24481
25101
|
}
|
|
@@ -24582,12 +25202,29 @@ var MemoryStateStore = class {
|
|
|
24582
25202
|
this.state = { ...state };
|
|
24583
25203
|
}
|
|
24584
25204
|
};
|
|
25205
|
+
var MemoryInventoryStore = class {
|
|
25206
|
+
constructor() {
|
|
25207
|
+
this.rows = [];
|
|
25208
|
+
}
|
|
25209
|
+
async record(balances) {
|
|
25210
|
+
this.rows.push({ id: this.rows.length + 1, takenAt: sqliteDatetime(/* @__PURE__ */ new Date()), balances: { ...balances } });
|
|
25211
|
+
}
|
|
25212
|
+
async latest() {
|
|
25213
|
+
const last = this.rows[this.rows.length - 1];
|
|
25214
|
+
return last ? { ...last, balances: { ...last.balances } } : null;
|
|
25215
|
+
}
|
|
25216
|
+
async since(from) {
|
|
25217
|
+
const cutoff = sqliteDatetime(from);
|
|
25218
|
+
return this.rows.filter((row) => row.takenAt >= cutoff).map((row) => ({ ...row, balances: { ...row.balances } }));
|
|
25219
|
+
}
|
|
25220
|
+
};
|
|
24585
25221
|
var MemoryDataStore = class {
|
|
24586
25222
|
constructor() {
|
|
24587
25223
|
this.bids = new MemoryBidStore();
|
|
24588
25224
|
this.activity = new MemoryActivityStore();
|
|
24589
25225
|
this.state = new MemoryStateStore();
|
|
24590
25226
|
this.limitOrders = new MemoryLimitOrderStore();
|
|
25227
|
+
this.inventory = new MemoryInventoryStore();
|
|
24591
25228
|
}
|
|
24592
25229
|
};
|
|
24593
25230
|
|
|
@@ -25192,6 +25829,106 @@ var OrderScanner = class _OrderScanner {
|
|
|
25192
25829
|
}
|
|
25193
25830
|
};
|
|
25194
25831
|
|
|
25832
|
+
// src/data/indexer-inventory.ts
|
|
25833
|
+
var import_viem33 = require("viem");
|
|
25834
|
+
var READ_TIMEOUT_MS = 1e4;
|
|
25835
|
+
var SETTLED_AFTER_MS = 10 * 6e4;
|
|
25836
|
+
var BATCH = 40;
|
|
25837
|
+
var MAX_KEPT = 2e3;
|
|
25838
|
+
var IndexerInventory = class {
|
|
25839
|
+
constructor(options) {
|
|
25840
|
+
this.options = options;
|
|
25841
|
+
this.kept = /* @__PURE__ */ new Map();
|
|
25842
|
+
this.described = /* @__PURE__ */ new Map();
|
|
25843
|
+
this.fetch = options.fetch ?? fetch;
|
|
25844
|
+
}
|
|
25845
|
+
/**
|
|
25846
|
+
* A reading for each of `instants` the indexer could answer for. Never rejects: an instant it
|
|
25847
|
+
* could not be asked about is simply missing from the answer.
|
|
25848
|
+
*/
|
|
25849
|
+
async at(instants, now) {
|
|
25850
|
+
const readings = [];
|
|
25851
|
+
const wanted = [];
|
|
25852
|
+
for (const instant of new Set(instants)) {
|
|
25853
|
+
const kept = this.kept.get(instant);
|
|
25854
|
+
if (kept) readings.push(kept);
|
|
25855
|
+
else wanted.push(instant);
|
|
25856
|
+
}
|
|
25857
|
+
try {
|
|
25858
|
+
for (let index = 0; index < wanted.length; index += BATCH) {
|
|
25859
|
+
for (const reading of await this.read(wanted.slice(index, index + BATCH))) {
|
|
25860
|
+
readings.push(reading);
|
|
25861
|
+
if (reading.at <= now - SETTLED_AFTER_MS) this.keep(reading);
|
|
25862
|
+
}
|
|
25863
|
+
}
|
|
25864
|
+
} catch (err) {
|
|
25865
|
+
this.options.logger.warn(
|
|
25866
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
25867
|
+
"Could not read inventory from the indexer; carrying records by fills and sends alone"
|
|
25868
|
+
);
|
|
25869
|
+
}
|
|
25870
|
+
return readings;
|
|
25871
|
+
}
|
|
25872
|
+
keep(reading) {
|
|
25873
|
+
if (this.kept.size >= MAX_KEPT) this.kept.clear();
|
|
25874
|
+
this.kept.set(reading.at, reading);
|
|
25875
|
+
}
|
|
25876
|
+
/** One request for several instants, each its own aliased query at that moment in history. */
|
|
25877
|
+
async read(instants) {
|
|
25878
|
+
const queries = instants.map(
|
|
25879
|
+
(instant, index) => `r${index}: solverInventories(filter: { solver: { equalTo: $solver } }, blockHeight: "${Math.floor(instant)}", first: 1000) { nodes { chain tokenAddress balance } }`
|
|
25880
|
+
);
|
|
25881
|
+
const response = await this.fetch(this.options.indexerUrl, {
|
|
25882
|
+
method: "POST",
|
|
25883
|
+
headers: { "content-type": "application/json" },
|
|
25884
|
+
body: JSON.stringify({
|
|
25885
|
+
query: `query SolverInventory($solver: String!) {
|
|
25886
|
+
${queries.join("\n")}
|
|
25887
|
+
}`,
|
|
25888
|
+
variables: { solver: this.options.solver.toLowerCase() }
|
|
25889
|
+
}),
|
|
25890
|
+
signal: AbortSignal.timeout(READ_TIMEOUT_MS)
|
|
25891
|
+
});
|
|
25892
|
+
if (!response.ok) throw new Error(`Indexer responded ${response.status}`);
|
|
25893
|
+
const body = await response.json();
|
|
25894
|
+
if (body.errors?.length) throw new Error(body.errors.map((error) => error.message).join("; "));
|
|
25895
|
+
const readings = [];
|
|
25896
|
+
for (const [index, instant] of instants.entries()) {
|
|
25897
|
+
const rows = body.data?.[`r${index}`]?.nodes;
|
|
25898
|
+
if (rows) readings.push(await this.readingOf(instant, rows));
|
|
25899
|
+
}
|
|
25900
|
+
return readings;
|
|
25901
|
+
}
|
|
25902
|
+
async readingOf(at, rows) {
|
|
25903
|
+
const balances = {};
|
|
25904
|
+
const chains = {};
|
|
25905
|
+
for (const row of rows) {
|
|
25906
|
+
const token = await this.describe(row.chain, row.tokenAddress);
|
|
25907
|
+
if (!token) continue;
|
|
25908
|
+
let amount;
|
|
25909
|
+
try {
|
|
25910
|
+
amount = Number((0, import_viem33.formatUnits)(BigInt(row.balance), token.decimals));
|
|
25911
|
+
} catch {
|
|
25912
|
+
continue;
|
|
25913
|
+
}
|
|
25914
|
+
balances[token.symbol] = (balances[token.symbol] ?? 0) + amount;
|
|
25915
|
+
chains[token.symbol] = [...chains[token.symbol] ?? [], row.chain];
|
|
25916
|
+
}
|
|
25917
|
+
return { at, balances, chains };
|
|
25918
|
+
}
|
|
25919
|
+
/** A token's symbol and decimals, remembered once both are known. */
|
|
25920
|
+
async describe(chain, token) {
|
|
25921
|
+
const key = `${chain}:${token.toLowerCase()}`;
|
|
25922
|
+
const known = this.described.get(key);
|
|
25923
|
+
if (known) return known;
|
|
25924
|
+
const { symbol, decimals } = await this.options.describeToken(chain, token);
|
|
25925
|
+
if (!symbol || decimals === null) return null;
|
|
25926
|
+
const description = { symbol, decimals };
|
|
25927
|
+
this.described.set(key, description);
|
|
25928
|
+
return description;
|
|
25929
|
+
}
|
|
25930
|
+
};
|
|
25931
|
+
|
|
25195
25932
|
// src/simplex.ts
|
|
25196
25933
|
var EVENT_MAP = {
|
|
25197
25934
|
newOrder: "order:detected",
|
|
@@ -25237,10 +25974,66 @@ var LimitOrderController = class {
|
|
|
25237
25974
|
]);
|
|
25238
25975
|
return { order, fills, bids };
|
|
25239
25976
|
}
|
|
25977
|
+
/**
|
|
25978
|
+
* What the operator's buys and sells earned over `period`, bucketed on a clock
|
|
25979
|
+
* `tzOffsetMinutes` behind UTC.
|
|
25980
|
+
*
|
|
25981
|
+
* Profit is the change the period's fills made to what the solver holds, and
|
|
25982
|
+
* it is compared with the inventory the period began with. That inventory
|
|
25983
|
+
* comes from the stored snapshots and, when the balances have been read in
|
|
25984
|
+
* full, from what is held now, each carried to the period's start by what
|
|
25985
|
+
* moved between. The indexer says what moved, deposits and withdrawals
|
|
25986
|
+
* included, wherever it has followed the solver; elsewhere the fills and
|
|
25987
|
+
* sends on record stand in. It answers whether or not an orderbook is
|
|
25988
|
+
* configured, and whether or not the indexer does.
|
|
25989
|
+
*/
|
|
25990
|
+
async profitability(period, tzOffsetMinutes = 0) {
|
|
25991
|
+
const now = Date.now();
|
|
25992
|
+
const { data, balanceProvider } = this.runtime;
|
|
25993
|
+
const [fills, snapshots, ledger] = await Promise.all([
|
|
25994
|
+
data.limitOrders.fillHistory(),
|
|
25995
|
+
data.inventory.since(/* @__PURE__ */ new Date(0)),
|
|
25996
|
+
data.activity.walletTxs(500)
|
|
25997
|
+
]);
|
|
25998
|
+
const inventory = snapshots.map((snapshot) => ({
|
|
25999
|
+
at: parseUtc(snapshot.takenAt),
|
|
26000
|
+
balances: snapshot.balances
|
|
26001
|
+
}));
|
|
26002
|
+
const held = inventoryOf(balanceProvider.getSnapshot());
|
|
26003
|
+
if (held) inventory.push({ at: now, balances: held, live: true });
|
|
26004
|
+
const outflows = [];
|
|
26005
|
+
for (const tx of ledger) {
|
|
26006
|
+
const amount = Number(tx.amount);
|
|
26007
|
+
if (tx.kind === "send" && tx.token && Number.isFinite(amount) && amount > 0) {
|
|
26008
|
+
outflows.push({ at: tx.ts, symbol: tx.token, amount });
|
|
26009
|
+
}
|
|
26010
|
+
}
|
|
26011
|
+
const instants = inventoryInstants(fills, { period, now, tzOffsetMinutes, inventory });
|
|
26012
|
+
const readings = await this.indexerInventory().at(instants, now);
|
|
26013
|
+
return summarizeProfit(fills, { period, now, tzOffsetMinutes, inventory, outflows, readings });
|
|
26014
|
+
}
|
|
26015
|
+
/** One reader for the life of the filler, so the readings it has already fetched are kept. */
|
|
26016
|
+
indexerInventory() {
|
|
26017
|
+
this.indexed ?? (this.indexed = new IndexerInventory({
|
|
26018
|
+
indexerUrl: this.runtime.indexerUrl,
|
|
26019
|
+
solver: this.runtime.fillerAddress,
|
|
26020
|
+
describeToken: this.runtime.describeToken,
|
|
26021
|
+
logger: this.runtime.loggers.get("inventory")
|
|
26022
|
+
}));
|
|
26023
|
+
return this.indexed;
|
|
26024
|
+
}
|
|
25240
26025
|
/** The pairs the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
|
|
25241
26026
|
books() {
|
|
25242
26027
|
return this.service.books();
|
|
25243
26028
|
}
|
|
26029
|
+
/** Public bids and asks across solvers, optionally restricted to a route. */
|
|
26030
|
+
orderbookSnapshot(book, filters = {}) {
|
|
26031
|
+
return this.service.orderbookSnapshot(book, filters);
|
|
26032
|
+
}
|
|
26033
|
+
/** Every solver's order behind one public price level. */
|
|
26034
|
+
orderbookLevelOrders(book, level) {
|
|
26035
|
+
return this.service.orderbookLevelOrders(book, level);
|
|
26036
|
+
}
|
|
25244
26037
|
/** Creates the order, posts it, and reports what the orderbook made of it. */
|
|
25245
26038
|
async create(request) {
|
|
25246
26039
|
const posted = await this.service.create(request);
|