@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.js
CHANGED
|
@@ -1103,6 +1103,7 @@ import { isAddress as isAddress2 } from "viem";
|
|
|
1103
1103
|
import { ChainConfigService, bytes32ToBytes20 } from "@hyperbridge/sdk";
|
|
1104
1104
|
var DEFAULT_BLOCK_SCAN_INTERVAL_SECONDS = 3;
|
|
1105
1105
|
var MIN_BLOCK_SCAN_INTERVAL_SECONDS = 0.1;
|
|
1106
|
+
var DEFAULT_MIN_ORDER_SIZE_USD = 20;
|
|
1106
1107
|
function validateRpcUrls(rpcUrls) {
|
|
1107
1108
|
if (rpcUrls.length === 0) {
|
|
1108
1109
|
throw new Error("rpcUrls must contain at least one URL");
|
|
@@ -1525,6 +1526,13 @@ var FillerConfigService = class {
|
|
|
1525
1526
|
getBidValiditySeconds() {
|
|
1526
1527
|
return this.fillerConfig?.bidValiditySeconds ?? 300;
|
|
1527
1528
|
}
|
|
1529
|
+
/**
|
|
1530
|
+
* The order size, in USD, below which `order.fees` must cover the execution cost.
|
|
1531
|
+
* Defaults to 20. An order at or above it is filled whatever fees it carries.
|
|
1532
|
+
*/
|
|
1533
|
+
getMinOrderSizeUsd() {
|
|
1534
|
+
return this.fillerConfig?.minOrderSizeUsd ?? DEFAULT_MIN_ORDER_SIZE_USD;
|
|
1535
|
+
}
|
|
1528
1536
|
/** Ceiling bps above user-requested output. Default 500 (5%). */
|
|
1529
1537
|
getMaxOverfillBps() {
|
|
1530
1538
|
return BigInt(this.fillerConfig?.overfillProtection?.maxOverfillBps ?? 500);
|
|
@@ -1628,6 +1636,12 @@ function validateConfig(config, cliWatchOnly = false) {
|
|
|
1628
1636
|
);
|
|
1629
1637
|
}
|
|
1630
1638
|
}
|
|
1639
|
+
const minOrderSize = config.simplex.minOrderSizeUsd;
|
|
1640
|
+
if (minOrderSize !== void 0) {
|
|
1641
|
+
if (!Number.isFinite(minOrderSize) || minOrderSize < 0) {
|
|
1642
|
+
throw new Error(`simplex.minOrderSizeUsd must be a number >= 0 (USD); got ${minOrderSize}`);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1631
1645
|
if (config.allowlist) {
|
|
1632
1646
|
for (const user of config.allowlist.users ?? []) {
|
|
1633
1647
|
if (!isAddress2(user)) {
|
|
@@ -2601,6 +2615,35 @@ var LIMITS_QUERY = `
|
|
|
2601
2615
|
chains { id name tokens { symbol decimals } }
|
|
2602
2616
|
}
|
|
2603
2617
|
`;
|
|
2618
|
+
var LEVEL_FIELDS = "fillChain priceBucket price worstPrice baseSize quoteSize orderCount solverCount";
|
|
2619
|
+
var SNAPSHOT_QUERY = `
|
|
2620
|
+
query OrderbookSnapshot($id: ID!, $sourceChain: String, $fillChain: String) {
|
|
2621
|
+
book(id: $id) {
|
|
2622
|
+
id base quote
|
|
2623
|
+
bids: levels(side: BID, sourceChain: $sourceChain, fillChain: $fillChain) { ${LEVEL_FIELDS} }
|
|
2624
|
+
asks: levels(side: ASK, sourceChain: $sourceChain, fillChain: $fillChain) { ${LEVEL_FIELDS} }
|
|
2625
|
+
bidLiquidity: availableLiquidity(side: BID, sourceChain: $sourceChain, fillChain: $fillChain)
|
|
2626
|
+
askLiquidity: availableLiquidity(side: ASK, sourceChain: $sourceChain, fillChain: $fillChain)
|
|
2627
|
+
}
|
|
2628
|
+
serverInfo { priceGranularities { book granularity } }
|
|
2629
|
+
}
|
|
2630
|
+
`;
|
|
2631
|
+
var LEVEL_ORDERS_QUERY = `
|
|
2632
|
+
query OrderbookLevelOrders($id: ID!, $side: Side!, $fillChain: String, $sourceChain: String, $after: String) {
|
|
2633
|
+
book(id: $id) {
|
|
2634
|
+
orders(side: $side, fillChain: $fillChain, sourceChain: $sourceChain, first: 50, after: $after) {
|
|
2635
|
+
edges {
|
|
2636
|
+
node {
|
|
2637
|
+
solver { address }
|
|
2638
|
+
commitment fillChain price priceBucket advertisedSize quotedAmount resized expiresAt acceptedSources
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
pageInfo { hasNextPage endCursor }
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
`;
|
|
2646
|
+
var LEVEL_ORDER_PAGES = 20;
|
|
2604
2647
|
var POSTED_ORDER_FIELDS = "commitment side status price quotedAmount advertisedSize expiresAt acceptedSources";
|
|
2605
2648
|
var SUBMIT_ORDER_MUTATION = `
|
|
2606
2649
|
mutation SubmitOrder($userOp: Bytes!) {
|
|
@@ -2665,6 +2708,44 @@ var OrderbookClient = class {
|
|
|
2665
2708
|
async limits() {
|
|
2666
2709
|
return this.request(LIMITS_QUERY, {});
|
|
2667
2710
|
}
|
|
2711
|
+
/** The public book across all solvers, restricted to the selected route. */
|
|
2712
|
+
async snapshot(id, filters = {}) {
|
|
2713
|
+
const { book, serverInfo } = await this.request(SNAPSHOT_QUERY, {
|
|
2714
|
+
id,
|
|
2715
|
+
sourceChain: filters.sourceChain || null,
|
|
2716
|
+
fillChain: filters.fillChain || null
|
|
2717
|
+
});
|
|
2718
|
+
if (!book) return null;
|
|
2719
|
+
const granularity = serverInfo.priceGranularities.find((entry) => entry.book === book.id)?.granularity ?? null;
|
|
2720
|
+
return { ...book, granularity };
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2723
|
+
* The orders behind one price level. The orderbook filters orders by side and route but not by
|
|
2724
|
+
* price bucket, so this walks the side's orders on the level's fill chain and keeps the bucket.
|
|
2725
|
+
* Null when the book does not exist.
|
|
2726
|
+
*/
|
|
2727
|
+
async levelOrders(id, level) {
|
|
2728
|
+
const found = [];
|
|
2729
|
+
let after = null;
|
|
2730
|
+
for (let page = 0; page < LEVEL_ORDER_PAGES; page++) {
|
|
2731
|
+
const { book } = await this.request(LEVEL_ORDERS_QUERY, {
|
|
2732
|
+
id,
|
|
2733
|
+
side: level.side,
|
|
2734
|
+
fillChain: level.fillChain,
|
|
2735
|
+
sourceChain: level.sourceChain || null,
|
|
2736
|
+
after
|
|
2737
|
+
});
|
|
2738
|
+
if (!book) return null;
|
|
2739
|
+
for (const { node } of book.orders.edges) {
|
|
2740
|
+
if (node.priceBucket !== level.priceBucket) continue;
|
|
2741
|
+
const { solver, priceBucket: _bucket, ...order } = node;
|
|
2742
|
+
found.push({ ...order, solver: solver.address });
|
|
2743
|
+
}
|
|
2744
|
+
if (!book.orders.pageInfo.hasNextPage || !book.orders.pageInfo.endCursor) break;
|
|
2745
|
+
after = book.orders.pageInfo.endCursor;
|
|
2746
|
+
}
|
|
2747
|
+
return found;
|
|
2748
|
+
}
|
|
2668
2749
|
async submitOrder(userOp) {
|
|
2669
2750
|
const { submitOrder } = await this.request(SUBMIT_ORDER_MUTATION, { userOp });
|
|
2670
2751
|
switch (submitOrder.__typename) {
|
|
@@ -2849,16 +2930,16 @@ function signedAmounts(params) {
|
|
|
2849
2930
|
function offerFor(params) {
|
|
2850
2931
|
const { side, inputAmount, price, outputDecimals } = params;
|
|
2851
2932
|
if (price <= 0n) throw new Error("A limit order's price must be greater than zero");
|
|
2852
|
-
const
|
|
2933
|
+
const scaled2 = side === "BID" ? inputAmount * price / ORDERBOOK_SCALE : inputAmount * ORDERBOOK_SCALE / price;
|
|
2853
2934
|
const unit = outputDecimals >= 18 ? 1n : 10n ** BigInt(18 - outputDecimals);
|
|
2854
|
-
return
|
|
2935
|
+
return scaled2 / unit * unit;
|
|
2855
2936
|
}
|
|
2856
2937
|
function inputFor(params) {
|
|
2857
2938
|
const { side, outputAmount, price, inputDecimals } = params;
|
|
2858
2939
|
if (price <= 0n) throw new Error("A limit order's price must be greater than zero");
|
|
2859
|
-
const
|
|
2940
|
+
const scaled2 = side === "BID" ? divCeil(outputAmount * ORDERBOOK_SCALE, price) : divCeil(outputAmount * price, ORDERBOOK_SCALE);
|
|
2860
2941
|
const unit = inputDecimals >= 18 ? 1n : 10n ** BigInt(18 - inputDecimals);
|
|
2861
|
-
return divCeil(
|
|
2942
|
+
return divCeil(scaled2, unit) * unit;
|
|
2862
2943
|
}
|
|
2863
2944
|
|
|
2864
2945
|
// src/orderbook/matching.ts
|
|
@@ -3114,6 +3195,14 @@ var LimitOrderService = class {
|
|
|
3114
3195
|
throw err;
|
|
3115
3196
|
}
|
|
3116
3197
|
}
|
|
3198
|
+
/** Public market levels read through the configured orderbook client. */
|
|
3199
|
+
orderbookSnapshot(book, filters = {}) {
|
|
3200
|
+
return this.client.snapshot(book, filters);
|
|
3201
|
+
}
|
|
3202
|
+
/** Every solver's order behind one public price level. */
|
|
3203
|
+
orderbookLevelOrders(book, level) {
|
|
3204
|
+
return this.client.levelOrders(book, level);
|
|
3205
|
+
}
|
|
3117
3206
|
/**
|
|
3118
3207
|
* The pairs an order can be written against, the smallest payout each token may carry, and
|
|
3119
3208
|
* the tokens the orderbook registers on each chain.
|
|
@@ -3168,16 +3257,16 @@ var LimitOrderService = class {
|
|
|
3168
3257
|
if (!this.configService.getConfiguredChainIds().includes(getChainId(request.fillChain) ?? -1)) {
|
|
3169
3258
|
throw new LimitOrderValidationError(`'${request.fillChain}' is not a chain this filler is configured for`);
|
|
3170
3259
|
}
|
|
3171
|
-
const
|
|
3260
|
+
const scaled2 = { amountIn: 0n, amountOut: 0n };
|
|
3172
3261
|
for (const name of ["amountIn", "amountOut"]) {
|
|
3173
3262
|
try {
|
|
3174
|
-
|
|
3263
|
+
scaled2[name] = fromHuman(request[name] ?? "");
|
|
3175
3264
|
} catch (err) {
|
|
3176
3265
|
throw new LimitOrderValidationError(
|
|
3177
3266
|
`${name} must be an amount in whole tokens, like "1000" or "1500.25"; ${err.message}`
|
|
3178
3267
|
);
|
|
3179
3268
|
}
|
|
3180
|
-
if (
|
|
3269
|
+
if (scaled2[name] <= 0n) throw new LimitOrderValidationError(`${name} must be greater than zero`);
|
|
3181
3270
|
}
|
|
3182
3271
|
const sources = request.acceptedSources ?? [];
|
|
3183
3272
|
if (sources.length === 0) {
|
|
@@ -3224,7 +3313,7 @@ var LimitOrderService = class {
|
|
|
3224
3313
|
);
|
|
3225
3314
|
}
|
|
3226
3315
|
const floor = limits.serverInfo.minOrderSizes.find((entry) => entry.symbol === request.tokenOut);
|
|
3227
|
-
if (floor &&
|
|
3316
|
+
if (floor && scaled2.amountOut < BigInt(floor.size)) {
|
|
3228
3317
|
throw new LimitOrderValidationError(
|
|
3229
3318
|
`amountOut is below the orderbook's dust floor for ${request.tokenOut} (${toHuman(BigInt(floor.size))})`
|
|
3230
3319
|
);
|
|
@@ -3234,7 +3323,7 @@ var LimitOrderService = class {
|
|
|
3234
3323
|
throw new LimitOrderValidationError(`'${symbol}' does not resolve to a token address on ${request.fillChain}`);
|
|
3235
3324
|
}
|
|
3236
3325
|
}
|
|
3237
|
-
return { amountIn:
|
|
3326
|
+
return { amountIn: scaled2.amountIn, amountOut: scaled2.amountOut };
|
|
3238
3327
|
}
|
|
3239
3328
|
/**
|
|
3240
3329
|
* Refuses an order the wallet cannot pay out.
|
|
@@ -3760,6 +3849,451 @@ function underFunded(entry) {
|
|
|
3760
3849
|
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";
|
|
3761
3850
|
}
|
|
3762
3851
|
|
|
3852
|
+
// src/orderbook/profitability.ts
|
|
3853
|
+
import { Decimal as BaseDecimal } from "decimal.js";
|
|
3854
|
+
|
|
3855
|
+
// src/data/inventory.ts
|
|
3856
|
+
var INVENTORY_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3857
|
+
function inventoryOf(snapshot) {
|
|
3858
|
+
if (snapshot.status !== "fresh") return null;
|
|
3859
|
+
const held = {};
|
|
3860
|
+
for (const chain of snapshot.chains) {
|
|
3861
|
+
for (const asset of chain.assets) {
|
|
3862
|
+
if (asset.total === null) return null;
|
|
3863
|
+
held[asset.symbol] = (held[asset.symbol] ?? 0) + asset.total;
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
return Object.keys(held).length > 0 ? held : null;
|
|
3867
|
+
}
|
|
3868
|
+
function parseUtc(value) {
|
|
3869
|
+
return Date.parse(`${value.replace(" ", "T")}Z`);
|
|
3870
|
+
}
|
|
3871
|
+
var InventoryRecorder = class {
|
|
3872
|
+
constructor(store, balances, logger, now = Date.now) {
|
|
3873
|
+
this.store = store;
|
|
3874
|
+
this.balances = balances;
|
|
3875
|
+
this.logger = logger;
|
|
3876
|
+
this.now = now;
|
|
3877
|
+
this.recording = false;
|
|
3878
|
+
this.onSnapshot = (snapshot) => void this.consider(snapshot);
|
|
3879
|
+
}
|
|
3880
|
+
start() {
|
|
3881
|
+
this.balances.on("snapshot", this.onSnapshot);
|
|
3882
|
+
void this.consider(this.balances.getSnapshot());
|
|
3883
|
+
}
|
|
3884
|
+
stop() {
|
|
3885
|
+
this.balances.off("snapshot", this.onSnapshot);
|
|
3886
|
+
}
|
|
3887
|
+
/** Takes a snapshot from `snapshot` if one is due and the read is complete. Never rejects. */
|
|
3888
|
+
async consider(snapshot) {
|
|
3889
|
+
if (this.recording) return;
|
|
3890
|
+
const held = inventoryOf(snapshot);
|
|
3891
|
+
if (!held) return;
|
|
3892
|
+
this.recording = true;
|
|
3893
|
+
try {
|
|
3894
|
+
if (this.lastTakenAt === void 0) {
|
|
3895
|
+
const latest = await this.store.latest();
|
|
3896
|
+
this.lastTakenAt = latest ? parseUtc(latest.takenAt) : 0;
|
|
3897
|
+
}
|
|
3898
|
+
if (this.now() - this.lastTakenAt < INVENTORY_INTERVAL_MS) return;
|
|
3899
|
+
await this.store.record(held);
|
|
3900
|
+
this.lastTakenAt = this.now();
|
|
3901
|
+
this.logger.info({ tokens: Object.keys(held).length }, "Recorded an inventory snapshot");
|
|
3902
|
+
} catch (err) {
|
|
3903
|
+
this.logger.warn({ err }, "Could not record an inventory snapshot");
|
|
3904
|
+
} finally {
|
|
3905
|
+
this.recording = false;
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
};
|
|
3909
|
+
|
|
3910
|
+
// src/orderbook/usd.ts
|
|
3911
|
+
import { Decimal as Decimal2 } from "decimal.js";
|
|
3912
|
+
function limitOrderUsdEdges(orders) {
|
|
3913
|
+
return orders.filter((order) => normalizeSymbol(order.base) !== normalizeSymbol(order.quote)).map((order) => ({
|
|
3914
|
+
base: normalizeSymbol(order.base),
|
|
3915
|
+
quote: normalizeSymbol(order.quote),
|
|
3916
|
+
rate: new Decimal2(order.price).div(ORDERBOOK_SCALE.toString())
|
|
3917
|
+
})).filter((edge) => edge.rate.isFinite() && edge.rate.gt(0));
|
|
3918
|
+
}
|
|
3919
|
+
function usdFactorsFrom(edges) {
|
|
3920
|
+
const factors = /* @__PURE__ */ new Map();
|
|
3921
|
+
for (const symbol of USD_STABLE_SYMBOLS) factors.set(symbol, new Decimal2(1));
|
|
3922
|
+
const ordered = [...edges].sort((a, b) => a.base.localeCompare(b.base) || a.quote.localeCompare(b.quote));
|
|
3923
|
+
let grew = true;
|
|
3924
|
+
while (grew) {
|
|
3925
|
+
grew = false;
|
|
3926
|
+
for (const edge of ordered) {
|
|
3927
|
+
const usdBase = factors.get(edge.base);
|
|
3928
|
+
const usdQuote = factors.get(edge.quote);
|
|
3929
|
+
if (usdBase && !usdQuote) {
|
|
3930
|
+
factors.set(edge.quote, usdBase.div(edge.rate));
|
|
3931
|
+
grew = true;
|
|
3932
|
+
} else if (usdQuote && !usdBase) {
|
|
3933
|
+
factors.set(edge.base, usdQuote.mul(edge.rate));
|
|
3934
|
+
grew = true;
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
return factors;
|
|
3939
|
+
}
|
|
3940
|
+
function usdValueOf(factors, symbol, amount) {
|
|
3941
|
+
const factor = factors.get(normalizeSymbol(symbol));
|
|
3942
|
+
return factor ? amount.mul(factor) : null;
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
// src/orderbook/profitability.ts
|
|
3946
|
+
var Decimal3 = BaseDecimal.clone({ precision: 40 });
|
|
3947
|
+
var PROFIT_PERIODS = {
|
|
3948
|
+
"7d": { bucket: "day", count: 7 },
|
|
3949
|
+
"30d": { bucket: "day", count: 30 },
|
|
3950
|
+
"12w": { bucket: "week", count: 12 },
|
|
3951
|
+
"12m": { bucket: "month", count: 12 },
|
|
3952
|
+
all: { bucket: "year", count: null }
|
|
3953
|
+
};
|
|
3954
|
+
var MINUTE = 6e4;
|
|
3955
|
+
var DAY = 864e5;
|
|
3956
|
+
var BALANCES_REACH_MS = 7 * DAY;
|
|
3957
|
+
function bucketStart(local, bucket) {
|
|
3958
|
+
const date = new Date(local);
|
|
3959
|
+
const year = date.getUTCFullYear();
|
|
3960
|
+
const month = date.getUTCMonth();
|
|
3961
|
+
if (bucket === "year") return Date.UTC(year, 0, 1);
|
|
3962
|
+
if (bucket === "month") return Date.UTC(year, month, 1);
|
|
3963
|
+
const day = Date.UTC(year, month, date.getUTCDate());
|
|
3964
|
+
return bucket === "day" ? day : day - (date.getUTCDay() + 6) % 7 * DAY;
|
|
3965
|
+
}
|
|
3966
|
+
function shiftBucket(start, bucket, steps) {
|
|
3967
|
+
if (bucket === "day") return start + steps * DAY;
|
|
3968
|
+
if (bucket === "week") return start + steps * 7 * DAY;
|
|
3969
|
+
const date = new Date(start);
|
|
3970
|
+
return bucket === "month" ? Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + steps, 1) : Date.UTC(date.getUTCFullYear() + steps, 0, 1);
|
|
3971
|
+
}
|
|
3972
|
+
function scaled(value) {
|
|
3973
|
+
return new Decimal3(value).div(ORDERBOOK_SCALE.toString());
|
|
3974
|
+
}
|
|
3975
|
+
function tradeOf(fill, at) {
|
|
3976
|
+
try {
|
|
3977
|
+
const paid = scaled(fill.amount);
|
|
3978
|
+
const taken = fill.amountIn === null ? null : scaled(fill.amountIn);
|
|
3979
|
+
const price = scaled(fill.price);
|
|
3980
|
+
if (!paid.gt(0) || !price.gt(0) || taken !== null && !taken.gt(0)) return null;
|
|
3981
|
+
const buying = fill.side === "BID";
|
|
3982
|
+
const [baseAmount, quoteAmount] = buying ? [taken ?? paid.div(price), paid] : [paid, taken ?? paid.mul(price)];
|
|
3983
|
+
return {
|
|
3984
|
+
at,
|
|
3985
|
+
book: fill.book,
|
|
3986
|
+
base: normalizeSymbol(fill.base),
|
|
3987
|
+
quote: normalizeSymbol(fill.quote),
|
|
3988
|
+
buying,
|
|
3989
|
+
baseAmount,
|
|
3990
|
+
quoteAmount,
|
|
3991
|
+
estimated: taken === null
|
|
3992
|
+
};
|
|
3993
|
+
} catch {
|
|
3994
|
+
return null;
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
function add(holdings, symbol, amount) {
|
|
3998
|
+
holdings.set(symbol, (holdings.get(symbol) ?? new Decimal3(0)).add(amount));
|
|
3999
|
+
}
|
|
4000
|
+
function apply(holdings, trade, sign) {
|
|
4001
|
+
const base = trade.buying ? trade.baseAmount : trade.baseAmount.neg();
|
|
4002
|
+
const quote = trade.buying ? trade.quoteAmount.neg() : trade.quoteAmount;
|
|
4003
|
+
add(holdings, trade.base, base.mul(sign));
|
|
4004
|
+
add(holdings, trade.quote, quote.mul(sign));
|
|
4005
|
+
}
|
|
4006
|
+
var Sums = class {
|
|
4007
|
+
constructor() {
|
|
4008
|
+
this.change = /* @__PURE__ */ new Map();
|
|
4009
|
+
this.boughtUsd = new Decimal3(0);
|
|
4010
|
+
this.soldUsd = new Decimal3(0);
|
|
4011
|
+
this.buys = 0;
|
|
4012
|
+
this.sells = 0;
|
|
4013
|
+
}
|
|
4014
|
+
};
|
|
4015
|
+
var BookLedger = class {
|
|
4016
|
+
constructor(id, base, quote) {
|
|
4017
|
+
this.id = id;
|
|
4018
|
+
this.base = base;
|
|
4019
|
+
this.quote = quote;
|
|
4020
|
+
this.bought = new Decimal3(0);
|
|
4021
|
+
this.boughtQuote = new Decimal3(0);
|
|
4022
|
+
this.sold = new Decimal3(0);
|
|
4023
|
+
this.soldQuote = new Decimal3(0);
|
|
4024
|
+
/** The rates of the book's latest buy and latest sell, over all of its history. */
|
|
4025
|
+
this.latestBuy = null;
|
|
4026
|
+
this.latestSell = null;
|
|
4027
|
+
this.inPeriod = false;
|
|
4028
|
+
}
|
|
4029
|
+
/**
|
|
4030
|
+
* The rate the book is valued at. Midway between the latest buy and the latest sell, because
|
|
4031
|
+
* either alone sits half a spread to one side and would price open volume against whichever
|
|
4032
|
+
* way the book happened to trade last.
|
|
4033
|
+
*/
|
|
4034
|
+
rate() {
|
|
4035
|
+
if (this.latestBuy && this.latestSell) return this.latestBuy.add(this.latestSell).div(2);
|
|
4036
|
+
return this.latestBuy ?? this.latestSell;
|
|
4037
|
+
}
|
|
4038
|
+
};
|
|
4039
|
+
function readingOf(reading) {
|
|
4040
|
+
const held = /* @__PURE__ */ new Map();
|
|
4041
|
+
const cover = /* @__PURE__ */ new Map();
|
|
4042
|
+
for (const [symbol, amount] of Object.entries(reading.balances)) {
|
|
4043
|
+
const chains = reading.chains[symbol];
|
|
4044
|
+
if (!Number.isFinite(amount) || !chains?.length) continue;
|
|
4045
|
+
add(held, normalizeSymbol(symbol), new Decimal3(amount));
|
|
4046
|
+
cover.set(normalizeSymbol(symbol), [...chains].sort().join(","));
|
|
4047
|
+
}
|
|
4048
|
+
return { held, cover };
|
|
4049
|
+
}
|
|
4050
|
+
function movedBetween(earlier, later) {
|
|
4051
|
+
if (!earlier || !later) return null;
|
|
4052
|
+
const moved = /* @__PURE__ */ new Map();
|
|
4053
|
+
for (const [symbol, cover] of later.cover) {
|
|
4054
|
+
if (earlier.cover.get(symbol) !== cover) continue;
|
|
4055
|
+
const before = earlier.held.get(symbol) ?? new Decimal3(0);
|
|
4056
|
+
moved.set(symbol, (later.held.get(symbol) ?? new Decimal3(0)).sub(before));
|
|
4057
|
+
}
|
|
4058
|
+
return moved;
|
|
4059
|
+
}
|
|
4060
|
+
function nearestRecord(instant, records) {
|
|
4061
|
+
let record;
|
|
4062
|
+
let gap = Number.POSITIVE_INFINITY;
|
|
4063
|
+
for (const candidate of records) {
|
|
4064
|
+
const distance = Math.abs(candidate.at - instant);
|
|
4065
|
+
if (candidate.live && distance > BALANCES_REACH_MS) continue;
|
|
4066
|
+
if (distance < gap || distance === gap && record?.live && !candidate.live) {
|
|
4067
|
+
record = candidate;
|
|
4068
|
+
gap = distance;
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
return record;
|
|
4072
|
+
}
|
|
4073
|
+
function inventoryAt(instant, records, trades, outflows, readings) {
|
|
4074
|
+
const record = nearestRecord(instant, records);
|
|
4075
|
+
if (!record) return null;
|
|
4076
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
4077
|
+
for (const [symbol, amount] of Object.entries(record.balances)) {
|
|
4078
|
+
if (Number.isFinite(amount)) add(recorded, normalizeSymbol(symbol), new Decimal3(amount));
|
|
4079
|
+
}
|
|
4080
|
+
const held = new Map(recorded);
|
|
4081
|
+
const after = record.at >= instant;
|
|
4082
|
+
const [from, to] = after ? [instant, record.at] : [record.at, instant];
|
|
4083
|
+
const sign = after ? -1 : 1;
|
|
4084
|
+
for (const trade of trades) {
|
|
4085
|
+
if (trade.at >= from && trade.at < to) apply(held, trade, sign);
|
|
4086
|
+
}
|
|
4087
|
+
for (const outflow of outflows) {
|
|
4088
|
+
if (outflow.at >= from && outflow.at < to) {
|
|
4089
|
+
add(held, normalizeSymbol(outflow.symbol), new Decimal3(outflow.amount).mul(-sign));
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
const moved = movedBetween(readings.get(from), readings.get(to));
|
|
4093
|
+
if (moved) {
|
|
4094
|
+
for (const [symbol, amount] of moved) {
|
|
4095
|
+
held.set(symbol, (recorded.get(symbol) ?? new Decimal3(0)).add(amount.mul(sign)));
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
for (const [symbol, amount] of held) if (amount.isNegative()) held.set(symbol, new Decimal3(0));
|
|
4099
|
+
return { held, record };
|
|
4100
|
+
}
|
|
4101
|
+
function tradesOf(fills, now) {
|
|
4102
|
+
const trades = [];
|
|
4103
|
+
for (const fill of fills) {
|
|
4104
|
+
const at = parseUtc(fill.filledAt);
|
|
4105
|
+
if (!Number.isFinite(at) || at > now) continue;
|
|
4106
|
+
const trade = tradeOf(fill, at);
|
|
4107
|
+
if (trade) trades.push(trade);
|
|
4108
|
+
}
|
|
4109
|
+
return trades.sort((a, b) => a.at - b.at);
|
|
4110
|
+
}
|
|
4111
|
+
function bucketStarts(period, now, offset, firstAt) {
|
|
4112
|
+
const { bucket, count } = PROFIT_PERIODS[period];
|
|
4113
|
+
const last = bucketStart(now - offset, bucket);
|
|
4114
|
+
const first = count === null ? bucketStart((firstAt ?? now) - offset, bucket) : shiftBucket(last, bucket, 1 - count);
|
|
4115
|
+
const starts = [];
|
|
4116
|
+
for (let start = first; start <= last; start = shiftBucket(start, bucket, 1)) starts.push(start);
|
|
4117
|
+
return starts;
|
|
4118
|
+
}
|
|
4119
|
+
function inventoryInstants(fills, options) {
|
|
4120
|
+
const { period, now } = options;
|
|
4121
|
+
const offset = (options.tzOffsetMinutes ?? 0) * MINUTE;
|
|
4122
|
+
const records = (options.inventory ?? []).filter((record) => record.at <= now);
|
|
4123
|
+
const instants = /* @__PURE__ */ new Set([now]);
|
|
4124
|
+
for (const start of bucketStarts(period, now, offset, tradesOf(fills, now)[0]?.at)) {
|
|
4125
|
+
instants.add(start + offset);
|
|
4126
|
+
const record = nearestRecord(start + offset, records);
|
|
4127
|
+
if (record) instants.add(record.at);
|
|
4128
|
+
}
|
|
4129
|
+
return [...instants].sort((a, b) => a - b);
|
|
4130
|
+
}
|
|
4131
|
+
function summarizeProfit(fills, options) {
|
|
4132
|
+
const { period, now } = options;
|
|
4133
|
+
const offset = (options.tzOffsetMinutes ?? 0) * MINUTE;
|
|
4134
|
+
const { bucket } = PROFIT_PERIODS[period];
|
|
4135
|
+
const records = (options.inventory ?? []).filter((record) => record.at <= now);
|
|
4136
|
+
const outflows = options.outflows ?? [];
|
|
4137
|
+
const readings = new Map((options.readings ?? []).map((reading) => [reading.at, readingOf(reading)]));
|
|
4138
|
+
const trades = tradesOf(fills, now);
|
|
4139
|
+
const starts = bucketStarts(period, now, offset, trades[0]?.at);
|
|
4140
|
+
const first = starts[0];
|
|
4141
|
+
const books = /* @__PURE__ */ new Map();
|
|
4142
|
+
const spelling = /* @__PURE__ */ new Map();
|
|
4143
|
+
for (const record of records) {
|
|
4144
|
+
for (const symbol of Object.keys(record.balances)) {
|
|
4145
|
+
if (!spelling.has(normalizeSymbol(symbol))) spelling.set(normalizeSymbol(symbol), symbol);
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
for (const fill of fills) {
|
|
4149
|
+
for (const symbol of [fill.base, fill.quote]) {
|
|
4150
|
+
if (!spelling.has(normalizeSymbol(symbol))) spelling.set(normalizeSymbol(symbol), symbol);
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
for (const trade of trades) {
|
|
4154
|
+
let book = books.get(trade.book);
|
|
4155
|
+
if (!book) {
|
|
4156
|
+
book = new BookLedger(trade.book, trade.base, trade.quote);
|
|
4157
|
+
books.set(trade.book, book);
|
|
4158
|
+
}
|
|
4159
|
+
const rate = trade.quoteAmount.div(trade.baseAmount);
|
|
4160
|
+
if (trade.buying) book.latestBuy = rate;
|
|
4161
|
+
else book.latestSell = rate;
|
|
4162
|
+
}
|
|
4163
|
+
const edges = [];
|
|
4164
|
+
for (const book of books.values()) {
|
|
4165
|
+
const rate = book.rate();
|
|
4166
|
+
if (rate && book.base !== book.quote) {
|
|
4167
|
+
edges.push({ base: book.base, quote: book.quote, rate: new BaseDecimal(rate.toString()) });
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
const factors = usdFactorsFrom(edges);
|
|
4171
|
+
const priceOf = (symbol) => {
|
|
4172
|
+
const factor = factors.get(symbol);
|
|
4173
|
+
return factor ? new Decimal3(factor.toString()) : null;
|
|
4174
|
+
};
|
|
4175
|
+
const unpriced = /* @__PURE__ */ new Set();
|
|
4176
|
+
const valueOf = (holdings) => {
|
|
4177
|
+
let total = new Decimal3(0);
|
|
4178
|
+
for (const [symbol, amount] of holdings) {
|
|
4179
|
+
if (amount.isZero()) continue;
|
|
4180
|
+
const price = priceOf(symbol);
|
|
4181
|
+
if (price) total = total.add(amount.mul(price));
|
|
4182
|
+
else unpriced.add(symbol);
|
|
4183
|
+
}
|
|
4184
|
+
return total;
|
|
4185
|
+
};
|
|
4186
|
+
const totals = new Sums();
|
|
4187
|
+
const buckets = new Map(starts.map((start) => [start, new Sums()]));
|
|
4188
|
+
let estimatedFills = 0;
|
|
4189
|
+
for (const trade of trades) {
|
|
4190
|
+
const sums = buckets.get(bucketStart(trade.at - offset, bucket));
|
|
4191
|
+
if (!sums) continue;
|
|
4192
|
+
const book = books.get(trade.book);
|
|
4193
|
+
if (!book) continue;
|
|
4194
|
+
book.inPeriod = true;
|
|
4195
|
+
if (trade.estimated) estimatedFills += 1;
|
|
4196
|
+
if (trade.buying) {
|
|
4197
|
+
book.bought = book.bought.add(trade.baseAmount);
|
|
4198
|
+
book.boughtQuote = book.boughtQuote.add(trade.quoteAmount);
|
|
4199
|
+
} else {
|
|
4200
|
+
book.sold = book.sold.add(trade.baseAmount);
|
|
4201
|
+
book.soldQuote = book.soldQuote.add(trade.quoteAmount);
|
|
4202
|
+
}
|
|
4203
|
+
const basePrice = priceOf(trade.base);
|
|
4204
|
+
const quotePrice = priceOf(trade.quote);
|
|
4205
|
+
const volume = basePrice ? trade.baseAmount.mul(basePrice) : quotePrice ? trade.quoteAmount.mul(quotePrice) : new Decimal3(0);
|
|
4206
|
+
for (const sum of [totals, sums]) {
|
|
4207
|
+
apply(sum.change, trade, 1);
|
|
4208
|
+
if (trade.buying) {
|
|
4209
|
+
sum.buys += 1;
|
|
4210
|
+
sum.boughtUsd = sum.boughtUsd.add(volume);
|
|
4211
|
+
} else {
|
|
4212
|
+
sum.sells += 1;
|
|
4213
|
+
sum.soldUsd = sum.soldUsd.add(volume);
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
const figures = (sums, startsAt, endsAt) => {
|
|
4218
|
+
const profit = valueOf(sums.change);
|
|
4219
|
+
const began2 = inventoryAt(startsAt, records, trades, outflows, readings);
|
|
4220
|
+
const inventory = began2 ? valueOf(began2.held) : null;
|
|
4221
|
+
const moved = movedBetween(readings.get(startsAt), readings.get(endsAt));
|
|
4222
|
+
if (moved) {
|
|
4223
|
+
for (const [symbol, amount] of sums.change) {
|
|
4224
|
+
if (moved.has(symbol)) moved.set(symbol, (moved.get(symbol) ?? new Decimal3(0)).sub(amount));
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
return {
|
|
4228
|
+
profitUsd: profit.toNumber(),
|
|
4229
|
+
startInventoryUsd: inventory ? inventory.toNumber() : null,
|
|
4230
|
+
returnPct: inventory?.gt(0) ? profit.div(inventory).mul(100).toNumber() : null,
|
|
4231
|
+
transfersUsd: moved && moved.size > 0 ? valueOf(moved).toNumber() : null,
|
|
4232
|
+
boughtUsd: sums.boughtUsd.toNumber(),
|
|
4233
|
+
soldUsd: sums.soldUsd.toNumber(),
|
|
4234
|
+
buys: sums.buys,
|
|
4235
|
+
sells: sums.sells
|
|
4236
|
+
};
|
|
4237
|
+
};
|
|
4238
|
+
const from = first + offset;
|
|
4239
|
+
const began = inventoryAt(from, records, trades, outflows, readings);
|
|
4240
|
+
const startInventory = began && {
|
|
4241
|
+
at: from,
|
|
4242
|
+
source: began.record.live ? "balances" : "snapshot",
|
|
4243
|
+
recordedAt: began.record.at,
|
|
4244
|
+
usd: valueOf(began.held).toNumber(),
|
|
4245
|
+
tokens: [...began.held].filter(([, amount]) => amount.gt(0)).map(([symbol, amount]) => {
|
|
4246
|
+
const price = priceOf(symbol);
|
|
4247
|
+
return {
|
|
4248
|
+
symbol: spelling.get(symbol) ?? symbol,
|
|
4249
|
+
amount: amount.toNumber(),
|
|
4250
|
+
usd: price ? amount.mul(price).toNumber() : null
|
|
4251
|
+
};
|
|
4252
|
+
}).sort((a, b) => (b.usd ?? 0) - (a.usd ?? 0) || a.symbol.localeCompare(b.symbol))
|
|
4253
|
+
};
|
|
4254
|
+
const rows = [];
|
|
4255
|
+
for (const book of books.values()) {
|
|
4256
|
+
if (!book.inPeriod) continue;
|
|
4257
|
+
const averageBuy = book.bought.gt(0) ? book.boughtQuote.div(book.bought) : null;
|
|
4258
|
+
const averageSell = book.sold.gt(0) ? book.soldQuote.div(book.sold) : null;
|
|
4259
|
+
const baseChange = book.bought.sub(book.sold);
|
|
4260
|
+
const quoteChange = book.soldQuote.sub(book.boughtQuote);
|
|
4261
|
+
const basePrice = priceOf(book.base);
|
|
4262
|
+
const quotePrice = priceOf(book.quote);
|
|
4263
|
+
rows.push({
|
|
4264
|
+
book: book.id,
|
|
4265
|
+
base: spelling.get(book.base) ?? book.base,
|
|
4266
|
+
quote: spelling.get(book.quote) ?? book.quote,
|
|
4267
|
+
bought: book.bought.toNumber(),
|
|
4268
|
+
sold: book.sold.toNumber(),
|
|
4269
|
+
averageBuy: averageBuy ? averageBuy.toNumber() : null,
|
|
4270
|
+
averageSell: averageSell ? averageSell.toNumber() : null,
|
|
4271
|
+
spreadPct: averageBuy && averageSell ? averageSell.div(averageBuy).sub(1).mul(100).toNumber() : null,
|
|
4272
|
+
baseChange: baseChange.toNumber(),
|
|
4273
|
+
quoteChange: quoteChange.toNumber(),
|
|
4274
|
+
rate: book.rate()?.toNumber() ?? null,
|
|
4275
|
+
profitUsd: basePrice && quotePrice ? baseChange.mul(basePrice).add(quoteChange.mul(quotePrice)).toNumber() : null
|
|
4276
|
+
});
|
|
4277
|
+
}
|
|
4278
|
+
rows.sort((a, b) => (b.profitUsd ?? 0) - (a.profitUsd ?? 0) || a.book.localeCompare(b.book));
|
|
4279
|
+
return {
|
|
4280
|
+
period,
|
|
4281
|
+
bucket,
|
|
4282
|
+
from,
|
|
4283
|
+
to: now,
|
|
4284
|
+
totals: figures(totals, from, now),
|
|
4285
|
+
series: starts.map((start, index) => {
|
|
4286
|
+
const sums = buckets.get(start) ?? new Sums();
|
|
4287
|
+
const endsAt = index + 1 < starts.length ? starts[index + 1] + offset : now;
|
|
4288
|
+
return { start: start + offset, ...figures(sums, start + offset, endsAt) };
|
|
4289
|
+
}),
|
|
4290
|
+
books: rows,
|
|
4291
|
+
startInventory,
|
|
4292
|
+
estimatedFills,
|
|
4293
|
+
unpricedTokens: [...unpriced].map((symbol) => spelling.get(symbol) ?? symbol).sort()
|
|
4294
|
+
};
|
|
4295
|
+
}
|
|
4296
|
+
|
|
3763
4297
|
// src/data/state.ts
|
|
3764
4298
|
async function patchRuntimeState(store, patch) {
|
|
3765
4299
|
if (store.patch) return store.patch(patch);
|
|
@@ -12806,6 +13340,7 @@ var CacheService = class {
|
|
|
12806
13340
|
matchedLimitOrders: {},
|
|
12807
13341
|
bidPlans: {},
|
|
12808
13342
|
partialFills: {},
|
|
13343
|
+
feeChecksWaived: {},
|
|
12809
13344
|
feeTokens: {},
|
|
12810
13345
|
tokenDecimals: {},
|
|
12811
13346
|
solverSelection: {}
|
|
@@ -12845,6 +13380,10 @@ var CacheService = class {
|
|
|
12845
13380
|
stalePartialIds.forEach((orderId) => {
|
|
12846
13381
|
delete this.cacheData.partialFills[orderId];
|
|
12847
13382
|
});
|
|
13383
|
+
const staleWaivedIds = Object.entries(this.cacheData.feeChecksWaived).filter(([_, data]) => !this.isCacheValid(data.timestamp)).map(([orderId]) => orderId);
|
|
13384
|
+
staleWaivedIds.forEach((orderId) => {
|
|
13385
|
+
delete this.cacheData.feeChecksWaived[orderId];
|
|
13386
|
+
});
|
|
12848
13387
|
}
|
|
12849
13388
|
getGasEstimate(orderId) {
|
|
12850
13389
|
try {
|
|
@@ -13026,6 +13565,7 @@ var CacheService = class {
|
|
|
13026
13565
|
})),
|
|
13027
13566
|
partialFill: plan.partialFill,
|
|
13028
13567
|
profit: plan.profit,
|
|
13568
|
+
...plan.inputDecimals !== void 0 ? { inputDecimals: plan.inputDecimals } : {},
|
|
13029
13569
|
...plan.budget ? { budget: budgetFromCache(plan.budget) } : {}
|
|
13030
13570
|
}));
|
|
13031
13571
|
} catch (error) {
|
|
@@ -13056,6 +13596,7 @@ var CacheService = class {
|
|
|
13056
13596
|
})),
|
|
13057
13597
|
partialFill: plan.partialFill,
|
|
13058
13598
|
profit: plan.profit,
|
|
13599
|
+
...plan.inputDecimals !== void 0 ? { inputDecimals: plan.inputDecimals } : {},
|
|
13059
13600
|
...plan.budget ? { budget: budgetToCache(plan.budget) } : {}
|
|
13060
13601
|
})),
|
|
13061
13602
|
timestamp: Date.now()
|
|
@@ -13157,6 +13698,24 @@ var CacheService = class {
|
|
|
13157
13698
|
clearPartialFill(orderId) {
|
|
13158
13699
|
delete this.cacheData.partialFills[orderId];
|
|
13159
13700
|
}
|
|
13701
|
+
/**
|
|
13702
|
+
* Whether the strategy's completed evaluation let a bid through without holding
|
|
13703
|
+
* `order.fees` to the execution cost, because the order is at or above the
|
|
13704
|
+
* operator's minimum size.
|
|
13705
|
+
*
|
|
13706
|
+
* Written and cleared the way {@link isPartialFill} is, for the same reason: the
|
|
13707
|
+
* caller exempts such an order from its profit floor.
|
|
13708
|
+
*/
|
|
13709
|
+
isFeeCheckWaived(orderId) {
|
|
13710
|
+
const cache = this.cacheData.feeChecksWaived[orderId];
|
|
13711
|
+
return cache !== void 0 && this.isCacheValid(cache.timestamp) && cache.waived;
|
|
13712
|
+
}
|
|
13713
|
+
setFeeCheckWaived(orderId, waived) {
|
|
13714
|
+
this.cacheData.feeChecksWaived[orderId] = { waived, timestamp: Date.now() };
|
|
13715
|
+
}
|
|
13716
|
+
clearFeeCheckWaived(orderId) {
|
|
13717
|
+
delete this.cacheData.feeChecksWaived[orderId];
|
|
13718
|
+
}
|
|
13160
13719
|
getFeeTokenWithDecimals(chain) {
|
|
13161
13720
|
try {
|
|
13162
13721
|
const cache = this.cacheData.feeTokens[chain];
|
|
@@ -15676,7 +16235,7 @@ var EVM_HOST = [
|
|
|
15676
16235
|
];
|
|
15677
16236
|
|
|
15678
16237
|
// src/services/ContractInteractionService.ts
|
|
15679
|
-
import { Decimal as
|
|
16238
|
+
import { Decimal as Decimal4 } from "decimal.js";
|
|
15680
16239
|
|
|
15681
16240
|
// src/config/abis/IntentGatewayV2.ts
|
|
15682
16241
|
var INTENT_GATEWAY_V2_ABI = [
|
|
@@ -18318,7 +18877,7 @@ function describeShortfall(balances) {
|
|
|
18318
18877
|
}
|
|
18319
18878
|
|
|
18320
18879
|
// src/services/ContractInteractionService.ts
|
|
18321
|
-
|
|
18880
|
+
Decimal4.config({ precision: 28, rounding: 4 });
|
|
18322
18881
|
var FUNDING_GAS_PER_CALL = 400000n;
|
|
18323
18882
|
var DEBIT_ORDER_GAS = 60000n;
|
|
18324
18883
|
function settles(budget) {
|
|
@@ -18484,7 +19043,10 @@ var ContractInteractionService = class {
|
|
|
18484
19043
|
order,
|
|
18485
19044
|
prependCalls: void 0,
|
|
18486
19045
|
maxPriorityFeePerGasBumpPercent: gasFeeBumpConfig?.maxPriorityFeePerGasBumpPercent,
|
|
18487
|
-
maxFeePerGasBumpPercent: gasFeeBumpConfig?.maxFeePerGasBumpPercent
|
|
19046
|
+
maxFeePerGasBumpPercent: gasFeeBumpConfig?.maxFeePerGasBumpPercent,
|
|
19047
|
+
// No bid without the bundler's own figures: an op signed with the SDK's
|
|
19048
|
+
// fixed fallback limits does not execute, so a failed estimate skips the order.
|
|
19049
|
+
requireBundlerEstimate: true
|
|
18488
19050
|
});
|
|
18489
19051
|
this.logger.info({ orderId: order.id }, "Caching gas estimate");
|
|
18490
19052
|
this.logger.info({ estimate }, "Estimate");
|
|
@@ -18507,7 +19069,7 @@ var ContractInteractionService = class {
|
|
|
18507
19069
|
callGasLimit: estimate.callGasLimit
|
|
18508
19070
|
};
|
|
18509
19071
|
} catch (error) {
|
|
18510
|
-
this.logger.error({ err: error }, "Error estimating gas,
|
|
19072
|
+
this.logger.error({ err: error, orderId: order.id }, "Error estimating gas, not bidding on this order");
|
|
18511
19073
|
throw new Error(`Failed to estimate gas: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
18512
19074
|
}
|
|
18513
19075
|
}
|
|
@@ -18656,7 +19218,7 @@ var ContractInteractionService = class {
|
|
|
18656
19218
|
* @returns The total USD value of inputs (sum of normalized stable amounts, or 0 if none)
|
|
18657
19219
|
*/
|
|
18658
19220
|
async getInputUsdValue(order) {
|
|
18659
|
-
let inputUsdValue = new
|
|
19221
|
+
let inputUsdValue = new Decimal4(0);
|
|
18660
19222
|
const inputs = order.inputs;
|
|
18661
19223
|
const sourceUsdc = this.configService.getUsdcAsset(order.source).toLowerCase();
|
|
18662
19224
|
const sourceUsdt = this.configService.getUsdtAsset(order.source).toLowerCase();
|
|
@@ -18665,7 +19227,7 @@ var ContractInteractionService = class {
|
|
|
18665
19227
|
const addr = tokenAddress.toLowerCase();
|
|
18666
19228
|
if (addr !== sourceUsdc && addr !== sourceUsdt) continue;
|
|
18667
19229
|
const decimals = await this.getTokenDecimals(tokenAddress, order.source);
|
|
18668
|
-
const tokenAmount = new
|
|
19230
|
+
const tokenAmount = new Decimal4(formatUnits2(input.amount, decimals));
|
|
18669
19231
|
inputUsdValue = inputUsdValue.plus(tokenAmount);
|
|
18670
19232
|
}
|
|
18671
19233
|
return inputUsdValue;
|
|
@@ -19794,7 +20356,7 @@ var DelegationService = class {
|
|
|
19794
20356
|
// src/services/RebalancingService.ts
|
|
19795
20357
|
import { formatUnits as formatUnits3 } from "viem";
|
|
19796
20358
|
import { parseStateMachineId as parseStateMachineId4 } from "@hyperbridge/sdk";
|
|
19797
|
-
import { Decimal as
|
|
20359
|
+
import { Decimal as Decimal5 } from "decimal.js";
|
|
19798
20360
|
|
|
19799
20361
|
// src/services/rebalancers/binance.ts
|
|
19800
20362
|
import { Wallet as Wallet2 } from "@binance/wallet";
|
|
@@ -20958,7 +21520,7 @@ var RebalancingService = class {
|
|
|
20958
21520
|
functionName: "balanceOf",
|
|
20959
21521
|
args: [this.walletAddress]
|
|
20960
21522
|
});
|
|
20961
|
-
const balanceDecimal = new
|
|
21523
|
+
const balanceDecimal = new Decimal5(formatUnits3(balanceWei, Number(decimals)));
|
|
20962
21524
|
return balanceDecimal.toNumber();
|
|
20963
21525
|
} catch (error) {
|
|
20964
21526
|
this.logger.error({ error, chain, asset }, "Failed to get current balance");
|
|
@@ -20991,7 +21553,7 @@ var RebalancingService = class {
|
|
|
20991
21553
|
};
|
|
20992
21554
|
|
|
20993
21555
|
// src/core/filler.ts
|
|
20994
|
-
import { Decimal as
|
|
21556
|
+
import { Decimal as Decimal6 } from "decimal.js";
|
|
20995
21557
|
var HYPERBRIDGE_CONNECT_TIMEOUT_MS = 3e4;
|
|
20996
21558
|
function byAmount(a, b) {
|
|
20997
21559
|
return BigInt(a.amount) < BigInt(b.amount) ? -1 : BigInt(a.amount) > BigInt(b.amount) ? 1 : 0;
|
|
@@ -21547,7 +22109,7 @@ var IntentFiller = class {
|
|
|
21547
22109
|
try {
|
|
21548
22110
|
const stratValue = await strategy.getOrderUsdValue(order);
|
|
21549
22111
|
if (stratValue != null) {
|
|
21550
|
-
inputUsdValue =
|
|
22112
|
+
inputUsdValue = Decimal6.max(baseInputUsd, stratValue.inputUsd);
|
|
21551
22113
|
valued = true;
|
|
21552
22114
|
break;
|
|
21553
22115
|
}
|
|
@@ -21675,6 +22237,23 @@ var IntentFiller = class {
|
|
|
21675
22237
|
this.monitor.emit("orderSkipped", { orderId: order.id, reason: "watch-only" });
|
|
21676
22238
|
return null;
|
|
21677
22239
|
}
|
|
22240
|
+
const destinationHead = await this.destinationHead(order);
|
|
22241
|
+
if (destinationHead !== null && order.deadline <= destinationHead) {
|
|
22242
|
+
this.logger.info(
|
|
22243
|
+
{
|
|
22244
|
+
orderId: order.id,
|
|
22245
|
+
destChain: order.destination,
|
|
22246
|
+
deadline: order.deadline.toString(),
|
|
22247
|
+
head: destinationHead.toString()
|
|
22248
|
+
},
|
|
22249
|
+
"Skipping order: its deadline has passed on the destination chain"
|
|
22250
|
+
);
|
|
22251
|
+
this.monitor.emit("orderSkipped", {
|
|
22252
|
+
orderId: order.id,
|
|
22253
|
+
reason: "Order deadline has passed on the destination chain"
|
|
22254
|
+
});
|
|
22255
|
+
return null;
|
|
22256
|
+
}
|
|
21678
22257
|
const evalStartMs = Date.now();
|
|
21679
22258
|
const eligibleStrategies = await Promise.all(
|
|
21680
22259
|
this.strategies.map(async (strategy) => {
|
|
@@ -21684,7 +22263,10 @@ var IntentFiller = class {
|
|
|
21684
22263
|
})
|
|
21685
22264
|
);
|
|
21686
22265
|
const fillsPartially = this.fillsPartially(order);
|
|
21687
|
-
const
|
|
22266
|
+
const feeCheckWaived = this.feeCheckWaived(order);
|
|
22267
|
+
const validStrategies = eligibleStrategies.filter(
|
|
22268
|
+
(s) => s !== null && (s.profitability > 0 || fillsPartially || feeCheckWaived)
|
|
22269
|
+
).sort((a, b) => b.profitability - a.profitability);
|
|
21688
22270
|
const evalDurationSec = (Date.now() - evalStartMs) / 1e3;
|
|
21689
22271
|
this.monitor.emit("orderTiming", { orderId: order.id, phase: "evaluation", durationSec: evalDurationSec });
|
|
21690
22272
|
if (validStrategies.length === 0) {
|
|
@@ -21709,6 +22291,23 @@ var IntentFiller = class {
|
|
|
21709
22291
|
);
|
|
21710
22292
|
return validStrategies[0];
|
|
21711
22293
|
}
|
|
22294
|
+
/**
|
|
22295
|
+
* The destination chain's latest block number, the clock `order.deadline` is read on.
|
|
22296
|
+
*
|
|
22297
|
+
* Null when the read fails. That is not an answer about the order, so evaluation goes
|
|
22298
|
+
* on: the gas estimate simulates the same deadline check and refuses an expired order.
|
|
22299
|
+
*/
|
|
22300
|
+
async destinationHead(order) {
|
|
22301
|
+
try {
|
|
22302
|
+
return await this.chainClientManager.getPublicClient(order.destination).getBlockNumber();
|
|
22303
|
+
} catch (err) {
|
|
22304
|
+
this.logger.warn(
|
|
22305
|
+
{ orderId: order.id, destChain: order.destination, err },
|
|
22306
|
+
"Could not read the destination head to check the order's deadline"
|
|
22307
|
+
);
|
|
22308
|
+
return null;
|
|
22309
|
+
}
|
|
22310
|
+
}
|
|
21712
22311
|
/**
|
|
21713
22312
|
* Whether the strategy's evaluation concluded in a deliberate partial fill.
|
|
21714
22313
|
*
|
|
@@ -21722,6 +22321,15 @@ var IntentFiller = class {
|
|
|
21722
22321
|
if (!order.id) return false;
|
|
21723
22322
|
return this.contractService.cacheService.isPartialFill(order.id);
|
|
21724
22323
|
}
|
|
22324
|
+
/**
|
|
22325
|
+
* Whether the strategy's evaluation let a bid through without the fee check, because
|
|
22326
|
+
* the order is at or above the operator's minimum size. Read from a flag the strategy
|
|
22327
|
+
* sets only once it has an answer, as {@link fillsPartially} is.
|
|
22328
|
+
*/
|
|
22329
|
+
feeCheckWaived(order) {
|
|
22330
|
+
if (!order.id) return false;
|
|
22331
|
+
return this.contractService.cacheService.isFeeCheckWaived(order.id);
|
|
22332
|
+
}
|
|
21725
22333
|
executeOrder(order, bestStrategy, solverSelectionActive, inputUsdValue, profitUsd) {
|
|
21726
22334
|
const chainQueue = this.chainQueues.get(getChainId3(order.destination));
|
|
21727
22335
|
if (!chainQueue) {
|
|
@@ -21749,7 +22357,12 @@ var IntentFiller = class {
|
|
|
21749
22357
|
else cache.clearFundingPrepends(order.id);
|
|
21750
22358
|
}
|
|
21751
22359
|
reservation = await this.holdAll([
|
|
21752
|
-
{
|
|
22360
|
+
{
|
|
22361
|
+
limitOrderId: plan.limitOrderId,
|
|
22362
|
+
payout: plan.payout,
|
|
22363
|
+
take: plan.fillerInputs[plan.leg]?.amount,
|
|
22364
|
+
takeDecimals: plan.inputDecimals
|
|
22365
|
+
}
|
|
21753
22366
|
]);
|
|
21754
22367
|
if (reservation.length === 0) {
|
|
21755
22368
|
this.logger.info(
|
|
@@ -21919,7 +22532,8 @@ var IntentFiller = class {
|
|
|
21919
22532
|
taken.push({
|
|
21920
22533
|
limitOrderId: hold.limitOrderId,
|
|
21921
22534
|
amount,
|
|
21922
|
-
...hold.take !== void 0 ? { take: hold.take.toString() } : {}
|
|
22535
|
+
...hold.take !== void 0 ? { take: hold.take.toString() } : {},
|
|
22536
|
+
...hold.takeDecimals !== void 0 ? { takeDecimals: hold.takeDecimals } : {}
|
|
21923
22537
|
});
|
|
21924
22538
|
}
|
|
21925
22539
|
return taken;
|
|
@@ -21946,6 +22560,7 @@ var IntentFiller = class {
|
|
|
21946
22560
|
}
|
|
21947
22561
|
const settled = executedHold(claimed, delivered.scaled, released);
|
|
21948
22562
|
const charged = chargedFor(settled, delivered, released);
|
|
22563
|
+
const takenIn = released > 0n && settled.takeDecimals !== void 0 ? toScaled(released, settled.takeDecimals) : null;
|
|
21949
22564
|
const settledBid = rows.find(
|
|
21950
22565
|
(row) => row.reservations.some(
|
|
21951
22566
|
(hold) => hold.limitOrderId === settled.limitOrderId && hold.amount === settled.amount && hold.take === settled.take
|
|
@@ -21964,6 +22579,7 @@ var IntentFiller = class {
|
|
|
21964
22579
|
commitment,
|
|
21965
22580
|
bid: settledBid,
|
|
21966
22581
|
amount: share.toString(),
|
|
22582
|
+
amountIn: takenIn?.toString() ?? null,
|
|
21967
22583
|
transactionHash: transactionHash ?? null
|
|
21968
22584
|
});
|
|
21969
22585
|
}
|
|
@@ -22115,44 +22731,7 @@ import {
|
|
|
22115
22731
|
ADDRESS_ZERO as ADDRESS_ZERO2
|
|
22116
22732
|
} from "@hyperbridge/sdk";
|
|
22117
22733
|
import { formatUnits as formatUnits4 } from "viem";
|
|
22118
|
-
import { Decimal as
|
|
22119
|
-
|
|
22120
|
-
// src/orderbook/usd.ts
|
|
22121
|
-
import { Decimal as Decimal5 } from "decimal.js";
|
|
22122
|
-
function limitOrderUsdEdges(orders) {
|
|
22123
|
-
return orders.filter((order) => normalizeSymbol(order.base) !== normalizeSymbol(order.quote)).map((order) => ({
|
|
22124
|
-
base: normalizeSymbol(order.base),
|
|
22125
|
-
quote: normalizeSymbol(order.quote),
|
|
22126
|
-
rate: new Decimal5(order.price).div(ORDERBOOK_SCALE.toString())
|
|
22127
|
-
})).filter((edge) => edge.rate.isFinite() && edge.rate.gt(0));
|
|
22128
|
-
}
|
|
22129
|
-
function usdFactorsFrom(edges) {
|
|
22130
|
-
const factors = /* @__PURE__ */ new Map();
|
|
22131
|
-
for (const symbol of USD_STABLE_SYMBOLS) factors.set(symbol, new Decimal5(1));
|
|
22132
|
-
const ordered = [...edges].sort((a, b) => a.base.localeCompare(b.base) || a.quote.localeCompare(b.quote));
|
|
22133
|
-
let grew = true;
|
|
22134
|
-
while (grew) {
|
|
22135
|
-
grew = false;
|
|
22136
|
-
for (const edge of ordered) {
|
|
22137
|
-
const usdBase = factors.get(edge.base);
|
|
22138
|
-
const usdQuote = factors.get(edge.quote);
|
|
22139
|
-
if (usdBase && !usdQuote) {
|
|
22140
|
-
factors.set(edge.quote, usdBase.div(edge.rate));
|
|
22141
|
-
grew = true;
|
|
22142
|
-
} else if (usdQuote && !usdBase) {
|
|
22143
|
-
factors.set(edge.base, usdQuote.mul(edge.rate));
|
|
22144
|
-
grew = true;
|
|
22145
|
-
}
|
|
22146
|
-
}
|
|
22147
|
-
}
|
|
22148
|
-
return factors;
|
|
22149
|
-
}
|
|
22150
|
-
function usdValueOf(factors, symbol, amount) {
|
|
22151
|
-
const factor = factors.get(normalizeSymbol(symbol));
|
|
22152
|
-
return factor ? amount.mul(factor) : null;
|
|
22153
|
-
}
|
|
22154
|
-
|
|
22155
|
-
// src/strategies/fx.ts
|
|
22734
|
+
import { Decimal as Decimal7 } from "decimal.js";
|
|
22156
22735
|
function adjustDecimalsFloor(amount, fromDecimals, toDecimals) {
|
|
22157
22736
|
if (fromDecimals === toDecimals) return amount;
|
|
22158
22737
|
if (fromDecimals < toDecimals) return amount * BigInt(10 ** (toDecimals - fromDecimals));
|
|
@@ -22200,7 +22779,7 @@ var FXFiller = class _FXFiller {
|
|
|
22200
22779
|
this.maxConsecutiveClamps = configService.getMaxConsecutiveClamps();
|
|
22201
22780
|
if (confirmationPolicy) {
|
|
22202
22781
|
this.confirmationPolicy = {
|
|
22203
|
-
getConfirmationBlocks: (chainId, amountUsd) => confirmationPolicy.getConfirmationBlocks(chainId, new
|
|
22782
|
+
getConfirmationBlocks: (chainId, amountUsd) => confirmationPolicy.getConfirmationBlocks(chainId, new Decimal7(amountUsd))
|
|
22204
22783
|
};
|
|
22205
22784
|
}
|
|
22206
22785
|
}
|
|
@@ -22296,11 +22875,13 @@ var FXFiller = class _FXFiller {
|
|
|
22296
22875
|
* - Treat a payout below the ask as a partial fill, which only orders
|
|
22297
22876
|
* without output calldata and without an earlier partial fill allow.
|
|
22298
22877
|
* - Cap each payout by the filler's balance plus funding-venue withdrawals,
|
|
22299
|
-
* then gate it on fees (full fills) and the
|
|
22878
|
+
* then gate it on fees (full fills of orders below `minOrderSizeUsd`) and the
|
|
22879
|
+
* same-asset spread.
|
|
22300
22880
|
* - Cache the resulting bids for later use in `executeOrder`.
|
|
22301
22881
|
*/
|
|
22302
22882
|
async calculateProfitability(order) {
|
|
22303
22883
|
if (order.id) this.contractService.cacheService.clearPartialFill(order.id);
|
|
22884
|
+
if (order.id) this.contractService.cacheService.clearFeeCheckWaived(order.id);
|
|
22304
22885
|
if (this.halted) {
|
|
22305
22886
|
this.logger.warn({ orderId: order.id }, "FXFiller halted \u2014 rejecting order");
|
|
22306
22887
|
return 0;
|
|
@@ -22316,6 +22897,15 @@ var FXFiller = class _FXFiller {
|
|
|
22316
22897
|
const plans = [];
|
|
22317
22898
|
const plannedOn = /* @__PURE__ */ new Map();
|
|
22318
22899
|
const spentOn = /* @__PURE__ */ new Map();
|
|
22900
|
+
let atOrAboveMinSize;
|
|
22901
|
+
const feeCheckWaived = async () => {
|
|
22902
|
+
if (atOrAboveMinSize === void 0) {
|
|
22903
|
+
const usd = await this.getOrderUsdValue(order);
|
|
22904
|
+
atOrAboveMinSize = usd !== null && usd.inputUsd.gte(this.configService.getMinOrderSizeUsd());
|
|
22905
|
+
}
|
|
22906
|
+
return atOrAboveMinSize;
|
|
22907
|
+
};
|
|
22908
|
+
let feesWaived = false;
|
|
22319
22909
|
for (let leg = 0; leg < order.inputs.length; leg++) {
|
|
22320
22910
|
const input = order.inputs[leg];
|
|
22321
22911
|
const output = order.output.assets[leg];
|
|
@@ -22525,17 +23115,17 @@ var FXFiller = class _FXFiller {
|
|
|
22525
23115
|
const outputSymbol = this.registry.symbolFor(outputToken, destChain);
|
|
22526
23116
|
const sameAsset = outputSymbol !== null && normalizeSymbol(outputSymbol) === normalizeSymbol(candidate.order.base) && normalizeSymbol(outputSymbol) === normalizeSymbol(candidate.order.quote);
|
|
22527
23117
|
let realizedSpreadProfit = 0n;
|
|
22528
|
-
let sameAssetEdgeUsd = new
|
|
23118
|
+
let sameAssetEdgeUsd = new Decimal7(0);
|
|
22529
23119
|
let sameAssetProfitable = true;
|
|
22530
23120
|
if (sameAsset) {
|
|
22531
23121
|
const convertedInput = adjustDecimalsFloor(releasedInput, inputDecimals, outputDecimals);
|
|
22532
23122
|
const spread = convertedInput - finalOutputAmount;
|
|
22533
23123
|
if (spread <= 0n) sameAssetProfitable = false;
|
|
22534
23124
|
realizedSpreadProfit = adjustDecimalsFloor(spread, outputDecimals, feeTokenDecimals);
|
|
22535
|
-
const spreadUsd = outputSymbol && usdValueOf(usdFactors, outputSymbol, new
|
|
23125
|
+
const spreadUsd = outputSymbol && usdValueOf(usdFactors, outputSymbol, new Decimal7(formatUnits4(spread, outputDecimals)));
|
|
22536
23126
|
if (spreadUsd) sameAssetEdgeUsd = spreadUsd;
|
|
22537
23127
|
}
|
|
22538
|
-
const payoutSurplusUsd = new
|
|
23128
|
+
const payoutSurplusUsd = new Decimal7(0);
|
|
22539
23129
|
const { totalCostInSourceFeeToken, relayerFeeInSourceFeeToken, dispatchFee } = await this.contractService.estimateGasFillPost(order);
|
|
22540
23130
|
if (sourceChain !== destChain && dispatchFee > 0n) {
|
|
22541
23131
|
const feeToken = await this.contractService.getFeeTokenWithDecimals(destChain);
|
|
@@ -22559,17 +23149,21 @@ var FXFiller = class _FXFiller {
|
|
|
22559
23149
|
const executionCost = totalCostInSourceFeeToken + relayerFeeInSourceFeeToken;
|
|
22560
23150
|
const partialEdgeUsd = sameAssetEdgeUsd.plus(payoutSurplusUsd);
|
|
22561
23151
|
if (!partialFill && order.fees < executionCost) {
|
|
22562
|
-
|
|
22563
|
-
|
|
22564
|
-
|
|
22565
|
-
|
|
22566
|
-
|
|
22567
|
-
|
|
22568
|
-
|
|
22569
|
-
|
|
22570
|
-
|
|
22571
|
-
|
|
22572
|
-
|
|
23152
|
+
if (!await feeCheckWaived()) {
|
|
23153
|
+
this.logger.info(
|
|
23154
|
+
{
|
|
23155
|
+
orderId: order.id,
|
|
23156
|
+
orderFees: formatUnits4(order.fees, feeTokenDecimals),
|
|
23157
|
+
fillGas: formatUnits4(totalCostInSourceFeeToken, feeTokenDecimals),
|
|
23158
|
+
relayerFee: formatUnits4(relayerFeeInSourceFeeToken, feeTokenDecimals),
|
|
23159
|
+
executionCost: formatUnits4(executionCost, feeTokenDecimals),
|
|
23160
|
+
minOrderSizeUsd: this.configService.getMinOrderSizeUsd()
|
|
23161
|
+
},
|
|
23162
|
+
"Skipping order: attached fees do not cover execution cost (fill gas + relayer fee)"
|
|
23163
|
+
);
|
|
23164
|
+
continue;
|
|
23165
|
+
}
|
|
23166
|
+
feesWaived = true;
|
|
22573
23167
|
}
|
|
22574
23168
|
if (sameAsset && !sameAssetProfitable) {
|
|
22575
23169
|
this.logger.info(
|
|
@@ -22603,7 +23197,8 @@ var FXFiller = class _FXFiller {
|
|
|
22603
23197
|
realizedSpreadProfit: formatUnits4(realizedSpreadProfit, feeTokenDecimals),
|
|
22604
23198
|
payoutSurplusUsd: payoutSurplusUsd.toString(),
|
|
22605
23199
|
totalProfit,
|
|
22606
|
-
profitable: totalProfit > 0
|
|
23200
|
+
profitable: totalProfit > 0,
|
|
23201
|
+
feeCheckWaived: !partialFill && order.fees < executionCost
|
|
22607
23202
|
},
|
|
22608
23203
|
"FX swap profitability evaluation"
|
|
22609
23204
|
);
|
|
@@ -22617,6 +23212,7 @@ var FXFiller = class _FXFiller {
|
|
|
22617
23212
|
payout: toScaled(finalOutputAmount, outputDecimals),
|
|
22618
23213
|
fillerOutputs,
|
|
22619
23214
|
fillerInputs,
|
|
23215
|
+
inputDecimals,
|
|
22620
23216
|
fundingCalls: [...fundingCalls],
|
|
22621
23217
|
partialFill,
|
|
22622
23218
|
profit: totalProfit,
|
|
@@ -22635,6 +23231,7 @@ var FXFiller = class _FXFiller {
|
|
|
22635
23231
|
plans[0].budget
|
|
22636
23232
|
);
|
|
22637
23233
|
this.contractService.cacheService.setPartialFill(order.id, plans[0].partialFill);
|
|
23234
|
+
this.contractService.cacheService.setFeeCheckWaived(order.id, feesWaived);
|
|
22638
23235
|
this.contractService.cacheService.setMatchedLimitOrder(order.id, [
|
|
22639
23236
|
{ limitOrderId: plans[0].limitOrderId, payout: plans[0].payout }
|
|
22640
23237
|
]);
|
|
@@ -22894,13 +23491,13 @@ var FXFiller = class _FXFiller {
|
|
|
22894
23491
|
async getOrderUsdValue(order) {
|
|
22895
23492
|
if (!this.limitOrders || order.inputs.length === 0) return null;
|
|
22896
23493
|
const factors = usdFactorsFrom(limitOrderUsdEdges(await this.limitOrders.open()));
|
|
22897
|
-
let inputUsd = new
|
|
23494
|
+
let inputUsd = new Decimal7(0);
|
|
22898
23495
|
for (const input of order.inputs) {
|
|
22899
23496
|
const inputToken = bytes32ToBytes204(input.token);
|
|
22900
23497
|
const symbol = this.registry.symbolFor(inputToken, order.source);
|
|
22901
23498
|
if (!symbol) return null;
|
|
22902
23499
|
const decimals = await this.contractService.getTokenDecimals(inputToken, order.source);
|
|
22903
|
-
const legUsd = usdValueOf(factors, symbol, new
|
|
23500
|
+
const legUsd = usdValueOf(factors, symbol, new Decimal7(formatUnits4(input.amount, decimals)));
|
|
22904
23501
|
if (!legUsd) return null;
|
|
22905
23502
|
inputUsd = inputUsd.plus(legUsd);
|
|
22906
23503
|
}
|
|
@@ -23876,6 +24473,7 @@ async function bootFiller(config, options) {
|
|
|
23876
24473
|
rebalancing: config.rebalancing,
|
|
23877
24474
|
targetGasUnits: config.simplex.targetGasUnits,
|
|
23878
24475
|
bidValiditySeconds: config.simplex.bidValiditySeconds,
|
|
24476
|
+
minOrderSizeUsd: config.simplex.minOrderSizeUsd,
|
|
23879
24477
|
blockScanIntervalSeconds: config.simplex.blockScanIntervalSeconds,
|
|
23880
24478
|
gasFeeBump: config.simplex.gasFeeBump,
|
|
23881
24479
|
overfillProtection: config.simplex.overfillProtection,
|
|
@@ -24084,9 +24682,10 @@ async function bootFiller(config, options) {
|
|
|
24084
24682
|
const activity = new ActivityRecorder(options.data.activity, options.loggers, { describeToken });
|
|
24085
24683
|
activity.attach(intentFiller.monitor);
|
|
24086
24684
|
const network = resolvedChains.some((chain) => chainByChainId(chain.chainId)?.network === "testnet") ? "testnet" : "mainnet";
|
|
24685
|
+
const indexerUrl = config.simplex.indexerUrl ?? DEFAULT_INDEXER_URLS[network];
|
|
24087
24686
|
void backfillOrderSummaries({
|
|
24088
24687
|
store: options.data.activity,
|
|
24089
|
-
indexerUrl
|
|
24688
|
+
indexerUrl,
|
|
24090
24689
|
fillerAddress: runtimeSigner.address,
|
|
24091
24690
|
describeToken,
|
|
24092
24691
|
onUpdated: (rows) => {
|
|
@@ -24152,6 +24751,15 @@ async function bootFiller(config, options) {
|
|
|
24152
24751
|
vaultVenue?.startSweeping();
|
|
24153
24752
|
started.push(() => balanceProvider.stop());
|
|
24154
24753
|
await balanceProvider.start();
|
|
24754
|
+
if (options.signer) {
|
|
24755
|
+
const inventoryRecorder = new InventoryRecorder(
|
|
24756
|
+
options.data.inventory,
|
|
24757
|
+
balanceProvider,
|
|
24758
|
+
moduleLogger(options.loggers, "inventory")
|
|
24759
|
+
);
|
|
24760
|
+
started.push(() => inventoryRecorder.stop());
|
|
24761
|
+
inventoryRecorder.start();
|
|
24762
|
+
}
|
|
24155
24763
|
const watchOnlyChains = watchOnlyConfig ? Object.entries(watchOnlyConfig).filter(([, value]) => value === true).map(([chainId]) => Number.parseInt(chainId, 10)) : [];
|
|
24156
24764
|
logger.info(
|
|
24157
24765
|
{
|
|
@@ -24197,6 +24805,8 @@ async function bootFiller(config, options) {
|
|
|
24197
24805
|
rebalancingService,
|
|
24198
24806
|
resolvedChains,
|
|
24199
24807
|
fillerAddress: runtimeSigner.address,
|
|
24808
|
+
indexerUrl,
|
|
24809
|
+
describeToken,
|
|
24200
24810
|
watchOnly: watchOnlyConfig,
|
|
24201
24811
|
config,
|
|
24202
24812
|
configPath: options.configPath,
|
|
@@ -24471,10 +25081,20 @@ var MemoryLimitOrderStore = class {
|
|
|
24471
25081
|
commitment: fill.commitment,
|
|
24472
25082
|
bid: fill.bid ?? null,
|
|
24473
25083
|
amount: fill.amount,
|
|
25084
|
+
amountIn: fill.amountIn ?? null,
|
|
24474
25085
|
transactionHash: fill.transactionHash ?? null,
|
|
24475
25086
|
filledAt: sqliteDatetime(/* @__PURE__ */ new Date())
|
|
24476
25087
|
});
|
|
24477
25088
|
}
|
|
25089
|
+
async fillHistory() {
|
|
25090
|
+
return this.fillRows.flatMap((fill) => {
|
|
25091
|
+
const order = this.orders.get(fill.limitOrderId);
|
|
25092
|
+
if (!order) return [];
|
|
25093
|
+
const { id, limitOrderId, amount, amountIn, filledAt } = fill;
|
|
25094
|
+
const { book, base, quote, side, price } = order;
|
|
25095
|
+
return [{ id, limitOrderId, book, base, quote, side, price, amount, amountIn, filledAt }];
|
|
25096
|
+
});
|
|
25097
|
+
}
|
|
24478
25098
|
async fills(limitOrderId, limit = 100) {
|
|
24479
25099
|
return this.fillRows.filter((fill) => fill.limitOrderId === limitOrderId).slice(-capLimit(limit)).reverse().map((fill) => ({ ...fill }));
|
|
24480
25100
|
}
|
|
@@ -24581,12 +25201,29 @@ var MemoryStateStore = class {
|
|
|
24581
25201
|
this.state = { ...state };
|
|
24582
25202
|
}
|
|
24583
25203
|
};
|
|
25204
|
+
var MemoryInventoryStore = class {
|
|
25205
|
+
constructor() {
|
|
25206
|
+
this.rows = [];
|
|
25207
|
+
}
|
|
25208
|
+
async record(balances) {
|
|
25209
|
+
this.rows.push({ id: this.rows.length + 1, takenAt: sqliteDatetime(/* @__PURE__ */ new Date()), balances: { ...balances } });
|
|
25210
|
+
}
|
|
25211
|
+
async latest() {
|
|
25212
|
+
const last = this.rows[this.rows.length - 1];
|
|
25213
|
+
return last ? { ...last, balances: { ...last.balances } } : null;
|
|
25214
|
+
}
|
|
25215
|
+
async since(from) {
|
|
25216
|
+
const cutoff = sqliteDatetime(from);
|
|
25217
|
+
return this.rows.filter((row) => row.takenAt >= cutoff).map((row) => ({ ...row, balances: { ...row.balances } }));
|
|
25218
|
+
}
|
|
25219
|
+
};
|
|
24584
25220
|
var MemoryDataStore = class {
|
|
24585
25221
|
constructor() {
|
|
24586
25222
|
this.bids = new MemoryBidStore();
|
|
24587
25223
|
this.activity = new MemoryActivityStore();
|
|
24588
25224
|
this.state = new MemoryStateStore();
|
|
24589
25225
|
this.limitOrders = new MemoryLimitOrderStore();
|
|
25226
|
+
this.inventory = new MemoryInventoryStore();
|
|
24590
25227
|
}
|
|
24591
25228
|
};
|
|
24592
25229
|
|
|
@@ -25191,6 +25828,106 @@ var OrderScanner = class _OrderScanner {
|
|
|
25191
25828
|
}
|
|
25192
25829
|
};
|
|
25193
25830
|
|
|
25831
|
+
// src/data/indexer-inventory.ts
|
|
25832
|
+
import { formatUnits as formatUnits7 } from "viem";
|
|
25833
|
+
var READ_TIMEOUT_MS = 1e4;
|
|
25834
|
+
var SETTLED_AFTER_MS = 10 * 6e4;
|
|
25835
|
+
var BATCH = 40;
|
|
25836
|
+
var MAX_KEPT = 2e3;
|
|
25837
|
+
var IndexerInventory = class {
|
|
25838
|
+
constructor(options) {
|
|
25839
|
+
this.options = options;
|
|
25840
|
+
this.kept = /* @__PURE__ */ new Map();
|
|
25841
|
+
this.described = /* @__PURE__ */ new Map();
|
|
25842
|
+
this.fetch = options.fetch ?? fetch;
|
|
25843
|
+
}
|
|
25844
|
+
/**
|
|
25845
|
+
* A reading for each of `instants` the indexer could answer for. Never rejects: an instant it
|
|
25846
|
+
* could not be asked about is simply missing from the answer.
|
|
25847
|
+
*/
|
|
25848
|
+
async at(instants, now) {
|
|
25849
|
+
const readings = [];
|
|
25850
|
+
const wanted = [];
|
|
25851
|
+
for (const instant of new Set(instants)) {
|
|
25852
|
+
const kept = this.kept.get(instant);
|
|
25853
|
+
if (kept) readings.push(kept);
|
|
25854
|
+
else wanted.push(instant);
|
|
25855
|
+
}
|
|
25856
|
+
try {
|
|
25857
|
+
for (let index = 0; index < wanted.length; index += BATCH) {
|
|
25858
|
+
for (const reading of await this.read(wanted.slice(index, index + BATCH))) {
|
|
25859
|
+
readings.push(reading);
|
|
25860
|
+
if (reading.at <= now - SETTLED_AFTER_MS) this.keep(reading);
|
|
25861
|
+
}
|
|
25862
|
+
}
|
|
25863
|
+
} catch (err) {
|
|
25864
|
+
this.options.logger.warn(
|
|
25865
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
25866
|
+
"Could not read inventory from the indexer; carrying records by fills and sends alone"
|
|
25867
|
+
);
|
|
25868
|
+
}
|
|
25869
|
+
return readings;
|
|
25870
|
+
}
|
|
25871
|
+
keep(reading) {
|
|
25872
|
+
if (this.kept.size >= MAX_KEPT) this.kept.clear();
|
|
25873
|
+
this.kept.set(reading.at, reading);
|
|
25874
|
+
}
|
|
25875
|
+
/** One request for several instants, each its own aliased query at that moment in history. */
|
|
25876
|
+
async read(instants) {
|
|
25877
|
+
const queries = instants.map(
|
|
25878
|
+
(instant, index) => `r${index}: solverInventories(filter: { solver: { equalTo: $solver } }, blockHeight: "${Math.floor(instant)}", first: 1000) { nodes { chain tokenAddress balance } }`
|
|
25879
|
+
);
|
|
25880
|
+
const response = await this.fetch(this.options.indexerUrl, {
|
|
25881
|
+
method: "POST",
|
|
25882
|
+
headers: { "content-type": "application/json" },
|
|
25883
|
+
body: JSON.stringify({
|
|
25884
|
+
query: `query SolverInventory($solver: String!) {
|
|
25885
|
+
${queries.join("\n")}
|
|
25886
|
+
}`,
|
|
25887
|
+
variables: { solver: this.options.solver.toLowerCase() }
|
|
25888
|
+
}),
|
|
25889
|
+
signal: AbortSignal.timeout(READ_TIMEOUT_MS)
|
|
25890
|
+
});
|
|
25891
|
+
if (!response.ok) throw new Error(`Indexer responded ${response.status}`);
|
|
25892
|
+
const body = await response.json();
|
|
25893
|
+
if (body.errors?.length) throw new Error(body.errors.map((error) => error.message).join("; "));
|
|
25894
|
+
const readings = [];
|
|
25895
|
+
for (const [index, instant] of instants.entries()) {
|
|
25896
|
+
const rows = body.data?.[`r${index}`]?.nodes;
|
|
25897
|
+
if (rows) readings.push(await this.readingOf(instant, rows));
|
|
25898
|
+
}
|
|
25899
|
+
return readings;
|
|
25900
|
+
}
|
|
25901
|
+
async readingOf(at, rows) {
|
|
25902
|
+
const balances = {};
|
|
25903
|
+
const chains = {};
|
|
25904
|
+
for (const row of rows) {
|
|
25905
|
+
const token = await this.describe(row.chain, row.tokenAddress);
|
|
25906
|
+
if (!token) continue;
|
|
25907
|
+
let amount;
|
|
25908
|
+
try {
|
|
25909
|
+
amount = Number(formatUnits7(BigInt(row.balance), token.decimals));
|
|
25910
|
+
} catch {
|
|
25911
|
+
continue;
|
|
25912
|
+
}
|
|
25913
|
+
balances[token.symbol] = (balances[token.symbol] ?? 0) + amount;
|
|
25914
|
+
chains[token.symbol] = [...chains[token.symbol] ?? [], row.chain];
|
|
25915
|
+
}
|
|
25916
|
+
return { at, balances, chains };
|
|
25917
|
+
}
|
|
25918
|
+
/** A token's symbol and decimals, remembered once both are known. */
|
|
25919
|
+
async describe(chain, token) {
|
|
25920
|
+
const key = `${chain}:${token.toLowerCase()}`;
|
|
25921
|
+
const known = this.described.get(key);
|
|
25922
|
+
if (known) return known;
|
|
25923
|
+
const { symbol, decimals } = await this.options.describeToken(chain, token);
|
|
25924
|
+
if (!symbol || decimals === null) return null;
|
|
25925
|
+
const description = { symbol, decimals };
|
|
25926
|
+
this.described.set(key, description);
|
|
25927
|
+
return description;
|
|
25928
|
+
}
|
|
25929
|
+
};
|
|
25930
|
+
|
|
25194
25931
|
// src/simplex.ts
|
|
25195
25932
|
var EVENT_MAP = {
|
|
25196
25933
|
newOrder: "order:detected",
|
|
@@ -25236,10 +25973,66 @@ var LimitOrderController = class {
|
|
|
25236
25973
|
]);
|
|
25237
25974
|
return { order, fills, bids };
|
|
25238
25975
|
}
|
|
25976
|
+
/**
|
|
25977
|
+
* What the operator's buys and sells earned over `period`, bucketed on a clock
|
|
25978
|
+
* `tzOffsetMinutes` behind UTC.
|
|
25979
|
+
*
|
|
25980
|
+
* Profit is the change the period's fills made to what the solver holds, and
|
|
25981
|
+
* it is compared with the inventory the period began with. That inventory
|
|
25982
|
+
* comes from the stored snapshots and, when the balances have been read in
|
|
25983
|
+
* full, from what is held now, each carried to the period's start by what
|
|
25984
|
+
* moved between. The indexer says what moved, deposits and withdrawals
|
|
25985
|
+
* included, wherever it has followed the solver; elsewhere the fills and
|
|
25986
|
+
* sends on record stand in. It answers whether or not an orderbook is
|
|
25987
|
+
* configured, and whether or not the indexer does.
|
|
25988
|
+
*/
|
|
25989
|
+
async profitability(period, tzOffsetMinutes = 0) {
|
|
25990
|
+
const now = Date.now();
|
|
25991
|
+
const { data, balanceProvider } = this.runtime;
|
|
25992
|
+
const [fills, snapshots, ledger] = await Promise.all([
|
|
25993
|
+
data.limitOrders.fillHistory(),
|
|
25994
|
+
data.inventory.since(/* @__PURE__ */ new Date(0)),
|
|
25995
|
+
data.activity.walletTxs(500)
|
|
25996
|
+
]);
|
|
25997
|
+
const inventory = snapshots.map((snapshot) => ({
|
|
25998
|
+
at: parseUtc(snapshot.takenAt),
|
|
25999
|
+
balances: snapshot.balances
|
|
26000
|
+
}));
|
|
26001
|
+
const held = inventoryOf(balanceProvider.getSnapshot());
|
|
26002
|
+
if (held) inventory.push({ at: now, balances: held, live: true });
|
|
26003
|
+
const outflows = [];
|
|
26004
|
+
for (const tx of ledger) {
|
|
26005
|
+
const amount = Number(tx.amount);
|
|
26006
|
+
if (tx.kind === "send" && tx.token && Number.isFinite(amount) && amount > 0) {
|
|
26007
|
+
outflows.push({ at: tx.ts, symbol: tx.token, amount });
|
|
26008
|
+
}
|
|
26009
|
+
}
|
|
26010
|
+
const instants = inventoryInstants(fills, { period, now, tzOffsetMinutes, inventory });
|
|
26011
|
+
const readings = await this.indexerInventory().at(instants, now);
|
|
26012
|
+
return summarizeProfit(fills, { period, now, tzOffsetMinutes, inventory, outflows, readings });
|
|
26013
|
+
}
|
|
26014
|
+
/** One reader for the life of the filler, so the readings it has already fetched are kept. */
|
|
26015
|
+
indexerInventory() {
|
|
26016
|
+
this.indexed ?? (this.indexed = new IndexerInventory({
|
|
26017
|
+
indexerUrl: this.runtime.indexerUrl,
|
|
26018
|
+
solver: this.runtime.fillerAddress,
|
|
26019
|
+
describeToken: this.runtime.describeToken,
|
|
26020
|
+
logger: this.runtime.loggers.get("inventory")
|
|
26021
|
+
}));
|
|
26022
|
+
return this.indexed;
|
|
26023
|
+
}
|
|
25239
26024
|
/** The pairs the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
|
|
25240
26025
|
books() {
|
|
25241
26026
|
return this.service.books();
|
|
25242
26027
|
}
|
|
26028
|
+
/** Public bids and asks across solvers, optionally restricted to a route. */
|
|
26029
|
+
orderbookSnapshot(book, filters = {}) {
|
|
26030
|
+
return this.service.orderbookSnapshot(book, filters);
|
|
26031
|
+
}
|
|
26032
|
+
/** Every solver's order behind one public price level. */
|
|
26033
|
+
orderbookLevelOrders(book, level) {
|
|
26034
|
+
return this.service.orderbookLevelOrders(book, level);
|
|
26035
|
+
}
|
|
25243
26036
|
/** Creates the order, posts it, and reports what the orderbook made of it. */
|
|
25244
26037
|
async create(request) {
|
|
25245
26038
|
const posted = await this.service.create(request);
|