@hyperbridge/simplex 0.16.8 → 0.16.12
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/README.md +31 -14
- package/dist/bin/simplex.js +23880 -2112
- package/dist/bin/simplex.js.map +1 -1
- package/dist/config-storage.cjs +186 -0
- package/dist/config-storage.cjs.map +1 -0
- package/dist/config-storage.d.cts +28 -0
- package/dist/config-storage.d.ts +28 -0
- package/dist/config-storage.js +163 -0
- package/dist/config-storage.js.map +1 -0
- package/dist/index.cjs +704 -295
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +246 -100
- package/dist/index.d.ts +246 -100
- package/dist/index.js +579 -175
- package/dist/index.js.map +1 -1
- package/dist/sqlite.cjs +12 -0
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +2 -1
- package/dist/sqlite.d.ts +2 -1
- package/dist/sqlite.js +12 -0
- package/dist/sqlite.js.map +1 -1
- package/dist/{types-C7rnnKWn.d.cts → types-B5zkFhxR.d.cts} +11 -1
- package/dist/{types-C7rnnKWn.d.ts → types-B5zkFhxR.d.ts} +11 -1
- package/dist/ui/assets/{index-BQu6BMxc.js → index-QU1Ik7IX.js} +35 -35
- package/dist/ui/assets/{index-C3T-gubp.css → index-XKHMOpY2.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +15 -3
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)) {
|
|
@@ -2781,11 +2795,21 @@ import { randomBytes, randomUUID } from "node:crypto";
|
|
|
2781
2795
|
import { getChainId, MAX_DECLARED_ENTRIES } from "@hyperbridge/sdk";
|
|
2782
2796
|
|
|
2783
2797
|
// src/orderbook/amounts.ts
|
|
2798
|
+
import { keccak256, stringToBytes } from "viem";
|
|
2784
2799
|
var ORDERBOOK_SCALE = 10n ** 18n;
|
|
2785
2800
|
function toRaw(amount, decimals) {
|
|
2786
2801
|
const shift = 18 - decimals;
|
|
2787
2802
|
return shift >= 0 ? amount / 10n ** BigInt(shift) : amount * 10n ** BigInt(-shift);
|
|
2788
2803
|
}
|
|
2804
|
+
function budgetIdFor(limitOrderId) {
|
|
2805
|
+
return keccak256(stringToBytes(limitOrderId));
|
|
2806
|
+
}
|
|
2807
|
+
function budgetFor(order, token, decimals) {
|
|
2808
|
+
return { budgetId: budgetIdFor(order.id), cap: toRaw(BigInt(order.size), decimals), token };
|
|
2809
|
+
}
|
|
2810
|
+
function budgetRoom(budget, spent, decimals) {
|
|
2811
|
+
return spent >= budget.cap ? 0n : toScaled(budget.cap - spent, decimals);
|
|
2812
|
+
}
|
|
2789
2813
|
function fromHuman(amount) {
|
|
2790
2814
|
const text = amount.trim();
|
|
2791
2815
|
if (!/^[0-9]+(\.[0-9]+)?$/.test(text)) {
|
|
@@ -2806,6 +2830,10 @@ function toScaled(amount, decimals) {
|
|
|
2806
2830
|
const shift = 18 - decimals;
|
|
2807
2831
|
return shift >= 0 ? amount * 10n ** BigInt(shift) : amount / 10n ** BigInt(-shift);
|
|
2808
2832
|
}
|
|
2833
|
+
function toScaledUp(amount, decimals) {
|
|
2834
|
+
const shift = 18 - decimals;
|
|
2835
|
+
return shift >= 0 ? amount * 10n ** BigInt(shift) : divCeil(amount, 10n ** BigInt(-shift));
|
|
2836
|
+
}
|
|
2809
2837
|
function divCeil(numerator, denominator) {
|
|
2810
2838
|
return (numerator + denominator - 1n) / denominator;
|
|
2811
2839
|
}
|
|
@@ -2847,6 +2875,87 @@ function inputFor(params) {
|
|
|
2847
2875
|
return divCeil(scaled, unit) * unit;
|
|
2848
2876
|
}
|
|
2849
2877
|
|
|
2878
|
+
// src/orderbook/matching.ts
|
|
2879
|
+
function limitOrderLegs(order) {
|
|
2880
|
+
return order.side === "BID" ? { input: order.base, output: order.quote } : { input: order.quote, output: order.base };
|
|
2881
|
+
}
|
|
2882
|
+
function matchLimitOrders(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
|
|
2883
|
+
const qualifying = orders.filter((order) => serves(order, incoming, resolve, now)).map((order) => {
|
|
2884
|
+
const offer = offerFor({
|
|
2885
|
+
side: order.side,
|
|
2886
|
+
inputAmount: incoming.inputNet,
|
|
2887
|
+
price: BigInt(order.price),
|
|
2888
|
+
outputDecimals: incoming.outputDecimals
|
|
2889
|
+
});
|
|
2890
|
+
const available = BigInt(order.remaining);
|
|
2891
|
+
return { order, offer, available, payout: offer < available ? offer : available };
|
|
2892
|
+
}).filter((candidate) => candidate.offer >= incoming.requestedOutput && candidate.payout > 0n).sort(byBestOfferFirst);
|
|
2893
|
+
return qualifying;
|
|
2894
|
+
}
|
|
2895
|
+
function byBestOfferFirst(a, b) {
|
|
2896
|
+
if (a.offer !== b.offer) return a.offer > b.offer ? -1 : 1;
|
|
2897
|
+
if (a.available !== b.available) return a.available > b.available ? -1 : 1;
|
|
2898
|
+
return a.order.id < b.order.id ? -1 : 1;
|
|
2899
|
+
}
|
|
2900
|
+
function whyUnmatched(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
|
|
2901
|
+
let furthest = -1;
|
|
2902
|
+
for (const order of orders) {
|
|
2903
|
+
const failed = firstFailed(order, incoming, resolve, now);
|
|
2904
|
+
furthest = Math.max(furthest, failed === null ? CHECKS.length : CHECKS.indexOf(failed));
|
|
2905
|
+
}
|
|
2906
|
+
const { source, destination, inputSymbol } = incoming;
|
|
2907
|
+
if (furthest < CHECKS.length) {
|
|
2908
|
+
switch (CHECKS[Math.max(furthest, 0)]) {
|
|
2909
|
+
case "open":
|
|
2910
|
+
return "no limit order is open";
|
|
2911
|
+
case "chain":
|
|
2912
|
+
return `no open limit order fills on ${destination}`;
|
|
2913
|
+
case "input":
|
|
2914
|
+
return `no limit order on ${destination} takes ${inputSymbol} in`;
|
|
2915
|
+
case "output":
|
|
2916
|
+
return `no limit order on ${destination} taking ${inputSymbol} in pays out the token this order asks for`;
|
|
2917
|
+
case "source":
|
|
2918
|
+
return `no limit order for this pair on ${destination} accepts swaps from ${source}`;
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
const priced = orders.filter((order) => firstFailed(order, incoming, resolve, now) === null).map((order) => ({
|
|
2922
|
+
order,
|
|
2923
|
+
offer: offerFor({
|
|
2924
|
+
side: order.side,
|
|
2925
|
+
inputAmount: incoming.inputNet,
|
|
2926
|
+
price: BigInt(order.price),
|
|
2927
|
+
outputDecimals: incoming.outputDecimals
|
|
2928
|
+
})
|
|
2929
|
+
}));
|
|
2930
|
+
if (priced.some(({ offer }) => offer >= incoming.requestedOutput)) {
|
|
2931
|
+
return "the limit orders that meet its rate have nothing left to pay out";
|
|
2932
|
+
}
|
|
2933
|
+
const best = priced.reduce((a, b) => b.offer > a.offer ? b : a);
|
|
2934
|
+
const symbol = limitOrderLegs(best.order).output;
|
|
2935
|
+
return `it asks for ${readable(incoming.requestedOutput)} ${symbol}; the best limit order offers ${readable(best.offer)} ${symbol}`;
|
|
2936
|
+
}
|
|
2937
|
+
var CHECKS = ["open", "chain", "input", "output", "source"];
|
|
2938
|
+
function serves(order, incoming, resolve, now) {
|
|
2939
|
+
return firstFailed(order, incoming, resolve, now) === null;
|
|
2940
|
+
}
|
|
2941
|
+
function firstFailed(order, incoming, resolve, now) {
|
|
2942
|
+
if (order.status !== "open") return "open";
|
|
2943
|
+
if (order.expiresAt !== null && new Date(order.expiresAt) <= now) return "open";
|
|
2944
|
+
if (order.fillChain !== incoming.destination) return "chain";
|
|
2945
|
+
const legs = limitOrderLegs(order);
|
|
2946
|
+
if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return "input";
|
|
2947
|
+
const outputToken = resolve(legs.output, order.fillChain);
|
|
2948
|
+
if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return "output";
|
|
2949
|
+
if (incoming.source === incoming.destination) return null;
|
|
2950
|
+
return order.acceptedSources.includes(incoming.source) ? null : "source";
|
|
2951
|
+
}
|
|
2952
|
+
function readable(amount) {
|
|
2953
|
+
const [whole, fraction] = toHuman(amount).split(".");
|
|
2954
|
+
const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
2955
|
+
const cut = fraction?.slice(0, 6).replace(/0+$/, "");
|
|
2956
|
+
return cut ? `${grouped}.${cut}` : grouped;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2850
2959
|
// src/orderbook/limit-orders.ts
|
|
2851
2960
|
var LIMITS_TTL_MS = 5 * 60 * 1e3;
|
|
2852
2961
|
var POSTABLE = ["open", "resizing"];
|
|
@@ -3361,6 +3470,58 @@ var LimitOrderService = class {
|
|
|
3361
3470
|
}
|
|
3362
3471
|
return report;
|
|
3363
3472
|
}
|
|
3473
|
+
/**
|
|
3474
|
+
* Lowers every open order's `remaining` to what the solver's account will still
|
|
3475
|
+
* pay for it, and puts the order back on the book at that size.
|
|
3476
|
+
*
|
|
3477
|
+
* The account tallies each order's payouts on the fill chain and refuses a fill
|
|
3478
|
+
* that would take the tally past the order's size. A fill that never settled
|
|
3479
|
+
* here, or a store restored from an older copy, leaves `remaining` above that
|
|
3480
|
+
* room and the order posted at a size the account will refuse. A tally behind
|
|
3481
|
+
* the store changes nothing: `remaining` is never raised.
|
|
3482
|
+
*
|
|
3483
|
+
* Answers how many orders were lowered.
|
|
3484
|
+
*/
|
|
3485
|
+
async reconcileTallies() {
|
|
3486
|
+
let lowered = 0;
|
|
3487
|
+
for (const order of await this.store.list({ status: "open" })) {
|
|
3488
|
+
try {
|
|
3489
|
+
const clamped = await this.clampToTally(order);
|
|
3490
|
+
if (!clamped) continue;
|
|
3491
|
+
lowered++;
|
|
3492
|
+
await this.resize(clamped, BigInt(order.remaining) - BigInt(clamped.remaining));
|
|
3493
|
+
} catch (err) {
|
|
3494
|
+
this.logger.error({ id: order.id, err }, "Could not bring the limit order in line with its on-chain tally");
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
return lowered;
|
|
3498
|
+
}
|
|
3499
|
+
/** The order as the store now holds it when its tally lowered it, null when it was left alone. */
|
|
3500
|
+
async clampToTally(order) {
|
|
3501
|
+
const token = this.assetRegistry.getAddress(limitOrderLegs(order).output, order.fillChain);
|
|
3502
|
+
if (!token) return null;
|
|
3503
|
+
const decimals = await this.contractService.getTokenDecimals(token, order.fillChain);
|
|
3504
|
+
const budget = budgetFor(order, token, decimals);
|
|
3505
|
+
const spent = await this.contractService.limitOrderSpent(order.fillChain, budget.budgetId);
|
|
3506
|
+
if (spent === null) return null;
|
|
3507
|
+
const room = budgetRoom(budget, spent, decimals);
|
|
3508
|
+
const over = BigInt(order.remaining) - (room + BigInt(order.reserved));
|
|
3509
|
+
if (over < toScaled(1n, decimals)) return null;
|
|
3510
|
+
const clamped = await this.store.clampRemaining(order.id, room.toString());
|
|
3511
|
+
if (!clamped) return null;
|
|
3512
|
+
this.logger.warn(
|
|
3513
|
+
{
|
|
3514
|
+
id: order.id,
|
|
3515
|
+
before: order.remaining,
|
|
3516
|
+
after: clamped.remaining,
|
|
3517
|
+
spent: spent.toString(),
|
|
3518
|
+
cap: budget.cap.toString(),
|
|
3519
|
+
reserved: clamped.reserved
|
|
3520
|
+
},
|
|
3521
|
+
"Limit order had more left than its on-chain tally allows; lowering it"
|
|
3522
|
+
);
|
|
3523
|
+
return clamped;
|
|
3524
|
+
}
|
|
3364
3525
|
/**
|
|
3365
3526
|
* Withdraws every order that has outlived the operator's own `expiresAt`.
|
|
3366
3527
|
*
|
|
@@ -3622,7 +3783,7 @@ async function patchRuntimeState(store, patch) {
|
|
|
3622
3783
|
}
|
|
3623
3784
|
|
|
3624
3785
|
// src/services/wallet/mpcvault.ts
|
|
3625
|
-
import { concatHex, keccak256, padHex, toHex } from "viem";
|
|
3786
|
+
import { concatHex, keccak256 as keccak2562, padHex, toHex } from "viem";
|
|
3626
3787
|
import * as grpc from "@grpc/grpc-js";
|
|
3627
3788
|
|
|
3628
3789
|
// src/proto/mpcvault/platform/v1/api.ts
|
|
@@ -12235,7 +12396,7 @@ var MpcVaultService = class {
|
|
|
12235
12396
|
const messageBytes = Buffer.from(messageHash.slice(2), "hex");
|
|
12236
12397
|
const prefix = Buffer.from(`Ethereum Signed Message:
|
|
12237
12398
|
${messageBytes.length}`);
|
|
12238
|
-
const ethSignedMessageHash =
|
|
12399
|
+
const ethSignedMessageHash = keccak2562(concatHex([toHex(prefix), messageHash]));
|
|
12239
12400
|
const ethSignedBytes = Buffer.from(ethSignedMessageHash.slice(2), "hex");
|
|
12240
12401
|
const uuid = await this.createSigningRequest({
|
|
12241
12402
|
vaultUuid: this.vaultUuid,
|
|
@@ -12323,11 +12484,12 @@ var SignerType = /* @__PURE__ */ ((SignerType2) => {
|
|
|
12323
12484
|
SignerType2["PrivateKey"] = "privateKey";
|
|
12324
12485
|
SignerType2["MpcVault"] = "mpcVault";
|
|
12325
12486
|
SignerType2["Turnkey"] = "turnkey";
|
|
12487
|
+
SignerType2["SecretPhrase"] = "secretPhrase";
|
|
12326
12488
|
return SignerType2;
|
|
12327
12489
|
})(SignerType || {});
|
|
12328
12490
|
|
|
12329
12491
|
// src/services/wallet/accounts/mpc.ts
|
|
12330
|
-
import { keccak256 as
|
|
12492
|
+
import { keccak256 as keccak2563, serializeTransaction } from "viem";
|
|
12331
12493
|
import { hashAuthorization } from "viem/utils";
|
|
12332
12494
|
function mpcVaultSigner(config) {
|
|
12333
12495
|
const service = new MpcVaultService({
|
|
@@ -12362,7 +12524,7 @@ function mpcVaultSigner(config) {
|
|
|
12362
12524
|
if (tx.authorizationList?.length) {
|
|
12363
12525
|
const serializable = { ...tx, type: "eip7702" };
|
|
12364
12526
|
const signature = await service.signRawHashComponents(
|
|
12365
|
-
|
|
12527
|
+
keccak2563(serializeTransaction(serializable))
|
|
12366
12528
|
);
|
|
12367
12529
|
return serializeTransaction(serializable, signature);
|
|
12368
12530
|
}
|
|
@@ -12444,6 +12606,51 @@ function privateKeySigner(privateKey) {
|
|
|
12444
12606
|
return viemSigner(privateKeyToAccount(privateKey));
|
|
12445
12607
|
}
|
|
12446
12608
|
|
|
12609
|
+
// src/services/wallet/accounts/secretphrase.ts
|
|
12610
|
+
import { validateMnemonic } from "@scure/bip39";
|
|
12611
|
+
import { english, mnemonicToAccount } from "viem/accounts";
|
|
12612
|
+
var WORD_COUNTS = [12, 15, 18, 21, 24];
|
|
12613
|
+
var MAX_DERIVATION_INDEX = 2 ** 31 - 1;
|
|
12614
|
+
function normaliseSecretPhrase(phrase) {
|
|
12615
|
+
return phrase.trim().split(/\s+/).join(" ").toLowerCase();
|
|
12616
|
+
}
|
|
12617
|
+
function validateSecretPhrase(phrase) {
|
|
12618
|
+
if (typeof phrase !== "string") throw new Error("Secret phrase must be a string");
|
|
12619
|
+
const normalised = normaliseSecretPhrase(phrase);
|
|
12620
|
+
const words = normalised === "" ? [] : normalised.split(" ");
|
|
12621
|
+
if (!WORD_COUNTS.includes(words.length)) {
|
|
12622
|
+
throw new Error(`Secret phrase must have ${WORD_COUNTS.join(", ")} words; got ${words.length}`);
|
|
12623
|
+
}
|
|
12624
|
+
if (words.some((word) => !english.includes(word))) {
|
|
12625
|
+
throw new Error("Secret phrase contains a word that is not in the BIP-39 English wordlist");
|
|
12626
|
+
}
|
|
12627
|
+
if (!validateMnemonic(normalised, english)) {
|
|
12628
|
+
throw new Error("Secret phrase checksum does not match; check the words and their order");
|
|
12629
|
+
}
|
|
12630
|
+
}
|
|
12631
|
+
function assertDerivationIndex(index, name = "Derivation index") {
|
|
12632
|
+
const expected = `${name} must be an integer between 0 and ${MAX_DERIVATION_INDEX}`;
|
|
12633
|
+
if (typeof index !== "number") throw new Error(expected);
|
|
12634
|
+
if (!Number.isInteger(index) || index < 0 || index > MAX_DERIVATION_INDEX) {
|
|
12635
|
+
throw new Error(`${expected}; got ${index}`);
|
|
12636
|
+
}
|
|
12637
|
+
}
|
|
12638
|
+
function secretPhraseSigner(config) {
|
|
12639
|
+
validateSecretPhrase(config.phrase);
|
|
12640
|
+
const phrase = normaliseSecretPhrase(config.phrase);
|
|
12641
|
+
const accountIndex = config.accountIndex ?? 0;
|
|
12642
|
+
assertDerivationIndex(accountIndex, "accountIndex");
|
|
12643
|
+
const derive = (index) => {
|
|
12644
|
+
assertDerivationIndex(index);
|
|
12645
|
+
return { ...viemSigner(mnemonicToAccount(phrase, { addressIndex: index })), mode: "secretPhrase" };
|
|
12646
|
+
};
|
|
12647
|
+
return { ...derive(accountIndex), accountIndex, derive };
|
|
12648
|
+
}
|
|
12649
|
+
function canDerive(signer) {
|
|
12650
|
+
const candidate = signer;
|
|
12651
|
+
return typeof candidate.derive === "function" && typeof candidate.accountIndex === "number";
|
|
12652
|
+
}
|
|
12653
|
+
|
|
12447
12654
|
// src/services/wallet/accounts/turnkey.ts
|
|
12448
12655
|
import { Turnkey } from "@turnkey/sdk-server";
|
|
12449
12656
|
import { createAccount } from "@turnkey/viem";
|
|
@@ -12467,6 +12674,7 @@ async function createSigner(config) {
|
|
|
12467
12674
|
if (config.type === "privateKey" /* PrivateKey */) return privateKeySigner(config.key);
|
|
12468
12675
|
if (config.type === "mpcVault" /* MpcVault */) return mpcVaultSigner(config);
|
|
12469
12676
|
if (config.type === "turnkey" /* Turnkey */) return turnkeySigner(config);
|
|
12677
|
+
if (config.type === "secretPhrase" /* SecretPhrase */) return secretPhraseSigner(config);
|
|
12470
12678
|
throw new Error(`Unsupported signer mode: ${config.type ?? "unknown"}`);
|
|
12471
12679
|
}
|
|
12472
12680
|
function validateSignerConfig(config) {
|
|
@@ -12492,13 +12700,21 @@ function validateSignerConfig(config) {
|
|
|
12492
12700
|
if (!config.signWith) throw new Error("simplex.signer.signWith is required");
|
|
12493
12701
|
return;
|
|
12494
12702
|
}
|
|
12703
|
+
if (config.type === "secretPhrase" /* SecretPhrase */) {
|
|
12704
|
+
if (!config.phrase) {
|
|
12705
|
+
throw new Error("simplex.signer.phrase is required when simplex.signer.type=secretPhrase");
|
|
12706
|
+
}
|
|
12707
|
+
validateSecretPhrase(config.phrase);
|
|
12708
|
+
if (config.accountIndex !== void 0) assertDerivationIndex(config.accountIndex, "simplex.signer.accountIndex");
|
|
12709
|
+
return;
|
|
12710
|
+
}
|
|
12495
12711
|
throw new Error(`Unsupported signer mode: ${config.type ?? "unknown"}`);
|
|
12496
12712
|
}
|
|
12497
12713
|
|
|
12498
12714
|
// src/services/wallet/account.ts
|
|
12499
12715
|
import {
|
|
12500
12716
|
hashTypedData,
|
|
12501
|
-
keccak256 as
|
|
12717
|
+
keccak256 as keccak2564,
|
|
12502
12718
|
serializeSignature,
|
|
12503
12719
|
serializeTransaction as serializeTransaction2
|
|
12504
12720
|
} from "viem";
|
|
@@ -12555,7 +12771,7 @@ function digestSigner(opts) {
|
|
|
12555
12771
|
),
|
|
12556
12772
|
signTransaction: async (tx) => {
|
|
12557
12773
|
const serializable = tx;
|
|
12558
|
-
const signature = await sign(
|
|
12774
|
+
const signature = await sign(keccak2564(serializeTransaction2(serializable)));
|
|
12559
12775
|
return serializeTransaction2(serializable, signature);
|
|
12560
12776
|
}
|
|
12561
12777
|
};
|
|
@@ -12585,6 +12801,12 @@ async function deriveSubstrateKeyPair(key) {
|
|
|
12585
12801
|
}
|
|
12586
12802
|
|
|
12587
12803
|
// src/services/CacheService.ts
|
|
12804
|
+
function budgetToCache(budget) {
|
|
12805
|
+
return { budgetId: budget.budgetId, cap: budget.cap.toString(), token: budget.token };
|
|
12806
|
+
}
|
|
12807
|
+
function budgetFromCache(budget) {
|
|
12808
|
+
return { budgetId: budget.budgetId, cap: BigInt(budget.cap), token: budget.token };
|
|
12809
|
+
}
|
|
12588
12810
|
var CacheService = class {
|
|
12589
12811
|
constructor(loggers = defaultLoggerContext()) {
|
|
12590
12812
|
this.CACHE_EXPIRY_MS = 1 * 60 * 1e3;
|
|
@@ -12598,6 +12820,7 @@ var CacheService = class {
|
|
|
12598
12820
|
matchedLimitOrders: {},
|
|
12599
12821
|
bidPlans: {},
|
|
12600
12822
|
partialFills: {},
|
|
12823
|
+
feeChecksWaived: {},
|
|
12601
12824
|
feeTokens: {},
|
|
12602
12825
|
tokenDecimals: {},
|
|
12603
12826
|
solverSelection: {}
|
|
@@ -12637,6 +12860,10 @@ var CacheService = class {
|
|
|
12637
12860
|
stalePartialIds.forEach((orderId) => {
|
|
12638
12861
|
delete this.cacheData.partialFills[orderId];
|
|
12639
12862
|
});
|
|
12863
|
+
const staleWaivedIds = Object.entries(this.cacheData.feeChecksWaived).filter(([_, data]) => !this.isCacheValid(data.timestamp)).map(([orderId]) => orderId);
|
|
12864
|
+
staleWaivedIds.forEach((orderId) => {
|
|
12865
|
+
delete this.cacheData.feeChecksWaived[orderId];
|
|
12866
|
+
});
|
|
12640
12867
|
}
|
|
12641
12868
|
getGasEstimate(orderId) {
|
|
12642
12869
|
try {
|
|
@@ -12742,7 +12969,24 @@ var CacheService = class {
|
|
|
12742
12969
|
return null;
|
|
12743
12970
|
}
|
|
12744
12971
|
}
|
|
12745
|
-
|
|
12972
|
+
/**
|
|
12973
|
+
* The budget of the limit order that priced the outputs cached for this order, or
|
|
12974
|
+
* null when no limit order did.
|
|
12975
|
+
*
|
|
12976
|
+
* Kept on the same entry as the outputs, so the two are replaced and expire
|
|
12977
|
+
* together: a bid can never sign one limit order's payout beside another's budget.
|
|
12978
|
+
*/
|
|
12979
|
+
getBidBudget(orderId) {
|
|
12980
|
+
try {
|
|
12981
|
+
const cache = this.cacheData.fillerOutputs[orderId];
|
|
12982
|
+
if (cache?.budget && this.isCacheValid(cache.timestamp)) return budgetFromCache(cache.budget);
|
|
12983
|
+
return null;
|
|
12984
|
+
} catch (error) {
|
|
12985
|
+
this.logger.error({ err: error }, "Error getting bid budget");
|
|
12986
|
+
return null;
|
|
12987
|
+
}
|
|
12988
|
+
}
|
|
12989
|
+
setFillerOutputs(orderId, outputs, inputs = [], budget) {
|
|
12746
12990
|
try {
|
|
12747
12991
|
this.cleanupStaleData();
|
|
12748
12992
|
this.cacheData.fillerOutputs[orderId] = {
|
|
@@ -12754,6 +12998,7 @@ var CacheService = class {
|
|
|
12754
12998
|
token: o.token,
|
|
12755
12999
|
amount: o.amount.toString()
|
|
12756
13000
|
})),
|
|
13001
|
+
...budget ? { budget: budgetToCache(budget) } : {},
|
|
12757
13002
|
timestamp: Date.now()
|
|
12758
13003
|
};
|
|
12759
13004
|
} catch (error) {
|
|
@@ -12799,7 +13044,8 @@ var CacheService = class {
|
|
|
12799
13044
|
data: call.data
|
|
12800
13045
|
})),
|
|
12801
13046
|
partialFill: plan.partialFill,
|
|
12802
|
-
profit: plan.profit
|
|
13047
|
+
profit: plan.profit,
|
|
13048
|
+
...plan.budget ? { budget: budgetFromCache(plan.budget) } : {}
|
|
12803
13049
|
}));
|
|
12804
13050
|
} catch (error) {
|
|
12805
13051
|
this.logger.error({ err: error }, "Error getting bid plans");
|
|
@@ -12828,7 +13074,8 @@ var CacheService = class {
|
|
|
12828
13074
|
data: call.data
|
|
12829
13075
|
})),
|
|
12830
13076
|
partialFill: plan.partialFill,
|
|
12831
|
-
profit: plan.profit
|
|
13077
|
+
profit: plan.profit,
|
|
13078
|
+
...plan.budget ? { budget: budgetToCache(plan.budget) } : {}
|
|
12832
13079
|
})),
|
|
12833
13080
|
timestamp: Date.now()
|
|
12834
13081
|
};
|
|
@@ -12929,6 +13176,24 @@ var CacheService = class {
|
|
|
12929
13176
|
clearPartialFill(orderId) {
|
|
12930
13177
|
delete this.cacheData.partialFills[orderId];
|
|
12931
13178
|
}
|
|
13179
|
+
/**
|
|
13180
|
+
* Whether the strategy's completed evaluation let a bid through without holding
|
|
13181
|
+
* `order.fees` to the execution cost, because the order is at or above the
|
|
13182
|
+
* operator's minimum size.
|
|
13183
|
+
*
|
|
13184
|
+
* Written and cleared the way {@link isPartialFill} is, for the same reason: the
|
|
13185
|
+
* caller exempts such an order from its profit floor.
|
|
13186
|
+
*/
|
|
13187
|
+
isFeeCheckWaived(orderId) {
|
|
13188
|
+
const cache = this.cacheData.feeChecksWaived[orderId];
|
|
13189
|
+
return cache !== void 0 && this.isCacheValid(cache.timestamp) && cache.waived;
|
|
13190
|
+
}
|
|
13191
|
+
setFeeCheckWaived(orderId, waived) {
|
|
13192
|
+
this.cacheData.feeChecksWaived[orderId] = { waived, timestamp: Date.now() };
|
|
13193
|
+
}
|
|
13194
|
+
clearFeeCheckWaived(orderId) {
|
|
13195
|
+
delete this.cacheData.feeChecksWaived[orderId];
|
|
13196
|
+
}
|
|
12932
13197
|
getFeeTokenWithDecimals(chain) {
|
|
12933
13198
|
try {
|
|
12934
13199
|
const cache = this.cacheData.feeTokens[chain];
|
|
@@ -13661,6 +13926,44 @@ import {
|
|
|
13661
13926
|
bytes20ToBytes32
|
|
13662
13927
|
} from "@hyperbridge/sdk";
|
|
13663
13928
|
|
|
13929
|
+
// src/config/abis/SolverAccount.ts
|
|
13930
|
+
var SOLVER_ACCOUNT_ABI = [
|
|
13931
|
+
{
|
|
13932
|
+
inputs: [
|
|
13933
|
+
{ name: "orderId", type: "bytes32" },
|
|
13934
|
+
{ name: "cap", type: "uint256" },
|
|
13935
|
+
{ name: "token", type: "address" },
|
|
13936
|
+
{ name: "approved", type: "uint256" },
|
|
13937
|
+
{ name: "fee", type: "uint256" }
|
|
13938
|
+
],
|
|
13939
|
+
name: "debitOrder",
|
|
13940
|
+
outputs: [],
|
|
13941
|
+
stateMutability: "nonpayable",
|
|
13942
|
+
type: "function"
|
|
13943
|
+
},
|
|
13944
|
+
{
|
|
13945
|
+
inputs: [{ name: "orderId", type: "bytes32" }],
|
|
13946
|
+
name: "spent",
|
|
13947
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
13948
|
+
stateMutability: "view",
|
|
13949
|
+
type: "function"
|
|
13950
|
+
},
|
|
13951
|
+
{
|
|
13952
|
+
inputs: [{ name: "sender", type: "address" }],
|
|
13953
|
+
name: "AccountUnauthorized",
|
|
13954
|
+
type: "error"
|
|
13955
|
+
},
|
|
13956
|
+
{
|
|
13957
|
+
inputs: [
|
|
13958
|
+
{ name: "orderId", type: "bytes32" },
|
|
13959
|
+
{ name: "total", type: "uint256" },
|
|
13960
|
+
{ name: "cap", type: "uint256" }
|
|
13961
|
+
],
|
|
13962
|
+
name: "LimitOrderExceeded",
|
|
13963
|
+
type: "error"
|
|
13964
|
+
}
|
|
13965
|
+
];
|
|
13966
|
+
|
|
13664
13967
|
// src/config/abis/EvmHost.ts
|
|
13665
13968
|
var EVM_HOST = [
|
|
13666
13969
|
{
|
|
@@ -18054,6 +18357,10 @@ function describeShortfall(balances) {
|
|
|
18054
18357
|
// src/services/ContractInteractionService.ts
|
|
18055
18358
|
Decimal2.config({ precision: 28, rounding: 4 });
|
|
18056
18359
|
var FUNDING_GAS_PER_CALL = 400000n;
|
|
18360
|
+
var DEBIT_ORDER_GAS = 60000n;
|
|
18361
|
+
function settles(budget) {
|
|
18362
|
+
return !!budget && budget.token.toLowerCase() !== ADDRESS_ZERO;
|
|
18363
|
+
}
|
|
18057
18364
|
var LIMIT_ORDER_CALL_GAS_LIMIT = 500000n;
|
|
18058
18365
|
var LIMIT_ORDER_VERIFICATION_GAS_LIMIT = 150000n;
|
|
18059
18366
|
var LIMIT_ORDER_PRE_VERIFICATION_GAS = 50000n;
|
|
@@ -18214,7 +18521,10 @@ var ContractInteractionService = class {
|
|
|
18214
18521
|
order,
|
|
18215
18522
|
prependCalls: void 0,
|
|
18216
18523
|
maxPriorityFeePerGasBumpPercent: gasFeeBumpConfig?.maxPriorityFeePerGasBumpPercent,
|
|
18217
|
-
maxFeePerGasBumpPercent: gasFeeBumpConfig?.maxFeePerGasBumpPercent
|
|
18524
|
+
maxFeePerGasBumpPercent: gasFeeBumpConfig?.maxFeePerGasBumpPercent,
|
|
18525
|
+
// No bid without the bundler's own figures: an op signed with the SDK's
|
|
18526
|
+
// fixed fallback limits does not execute, so a failed estimate skips the order.
|
|
18527
|
+
requireBundlerEstimate: true
|
|
18218
18528
|
});
|
|
18219
18529
|
this.logger.info({ orderId: order.id }, "Caching gas estimate");
|
|
18220
18530
|
this.logger.info({ estimate }, "Estimate");
|
|
@@ -18237,23 +18547,25 @@ var ContractInteractionService = class {
|
|
|
18237
18547
|
callGasLimit: estimate.callGasLimit
|
|
18238
18548
|
};
|
|
18239
18549
|
} catch (error) {
|
|
18240
|
-
this.logger.error({ err: error }, "Error estimating gas,
|
|
18550
|
+
this.logger.error({ err: error, orderId: order.id }, "Error estimating gas, not bidding on this order");
|
|
18241
18551
|
throw new Error(`Failed to estimate gas: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
18242
18552
|
}
|
|
18243
18553
|
}
|
|
18244
18554
|
/**
|
|
18245
18555
|
* The call gas a bid needs: the shared estimate plus room for the funding calls
|
|
18246
|
-
* this bid prepends.
|
|
18556
|
+
* this bid prepends and for settling its limit order's budget.
|
|
18247
18557
|
*
|
|
18248
18558
|
* Funding calls are not simulated (the V4 PositionManager's flash accounting does
|
|
18249
18559
|
* not resolve in the bundler's estimation context), so each one adds a fixed
|
|
18250
18560
|
* allowance for its modifyLiquidities + take. They differ between the bids on one
|
|
18251
18561
|
* order, since each draws on its own limit order and so on its own funding, which
|
|
18252
18562
|
* is why this is worked out per bid rather than baked into the cached estimate.
|
|
18563
|
+
* The estimate is of a lone fillOrder, so the budget call is not in it either.
|
|
18253
18564
|
*/
|
|
18254
|
-
callGasLimitFor(order, baseCallGasLimit) {
|
|
18565
|
+
callGasLimitFor(order, baseCallGasLimit, budget) {
|
|
18255
18566
|
const funding = order.id ? this.cacheService.getFundingPrepends(order.id) : null;
|
|
18256
|
-
|
|
18567
|
+
const fundingGas = FUNDING_GAS_PER_CALL * BigInt(funding?.calls?.length ?? 0);
|
|
18568
|
+
return baseCallGasLimit + fundingGas + (settles(budget) ? DEBIT_ORDER_GAS : 0n);
|
|
18257
18569
|
}
|
|
18258
18570
|
/**
|
|
18259
18571
|
* Gets the fee token address and decimals for a given chain.
|
|
@@ -18435,6 +18747,27 @@ var ContractInteractionService = class {
|
|
|
18435
18747
|
{ maxRetries: 3, backoffMs: 250, logMessage: "Failed to read token balance" }
|
|
18436
18748
|
);
|
|
18437
18749
|
}
|
|
18750
|
+
/**
|
|
18751
|
+
* What the solver's account has tallied against a limit order's budget on `chain`, in
|
|
18752
|
+
* the paid token's own units, or null when it cannot be read.
|
|
18753
|
+
*
|
|
18754
|
+
* Null rather than a throw: an account whose implementation keeps no tally answers
|
|
18755
|
+
* with a revert or with no data, and the caller carries on without the figure either
|
|
18756
|
+
* way. Not retried, since that answer does not change on a second ask.
|
|
18757
|
+
*/
|
|
18758
|
+
async limitOrderSpent(chain, budgetId) {
|
|
18759
|
+
try {
|
|
18760
|
+
return await this.clientManager.getPublicClient(chain).readContract({
|
|
18761
|
+
address: this.solverAccountAddress,
|
|
18762
|
+
abi: SOLVER_ACCOUNT_ABI,
|
|
18763
|
+
functionName: "spent",
|
|
18764
|
+
args: [budgetId]
|
|
18765
|
+
});
|
|
18766
|
+
} catch (err) {
|
|
18767
|
+
this.logger.warn({ err, chain, budgetId }, "Could not read the limit order's tally from the solver account");
|
|
18768
|
+
return null;
|
|
18769
|
+
}
|
|
18770
|
+
}
|
|
18438
18771
|
async getSolverEntryPointBalance(chain) {
|
|
18439
18772
|
const entryPointAddress = this.configService.getEntryPointAddress(chain);
|
|
18440
18773
|
if (!entryPointAddress) {
|
|
@@ -18575,7 +18908,8 @@ var ContractInteractionService = class {
|
|
|
18575
18908
|
if (!cachedEstimate) {
|
|
18576
18909
|
throw new Error(`No cached gas estimate found for order ${order.id}. Call estimateGasFillPost first.`);
|
|
18577
18910
|
}
|
|
18578
|
-
const
|
|
18911
|
+
const budget = this.cacheService.getBidBudget(order.id);
|
|
18912
|
+
const callGasLimit = this.callGasLimitFor(order, cachedEstimate.callGasLimit, budget);
|
|
18579
18913
|
const cachedFillerOutputs = this.cacheService.getFillerOutputs(order.id);
|
|
18580
18914
|
const cachedFillerInputs = this.cacheService.getFillerInputs(order.id);
|
|
18581
18915
|
if (!cachedFillerOutputs || !cachedFillerInputs) {
|
|
@@ -18602,7 +18936,8 @@ var ContractInteractionService = class {
|
|
|
18602
18936
|
order,
|
|
18603
18937
|
cachedFillerOutputs,
|
|
18604
18938
|
fillOptions,
|
|
18605
|
-
cachedEstimate.totalCostInSourceFeeToken + dispatchFeeTokenAmount
|
|
18939
|
+
cachedEstimate.totalCostInSourceFeeToken + dispatchFeeTokenAmount,
|
|
18940
|
+
budget ? { budget, solverAccount: solverAccountAddress } : void 0
|
|
18606
18941
|
);
|
|
18607
18942
|
const commitment = orderCommitment2(order);
|
|
18608
18943
|
const nonce = await this.clientManager.getPublicClient(order.destination).readContract({
|
|
@@ -18794,8 +19129,13 @@ var ContractInteractionService = class {
|
|
|
18794
19129
|
* Same-chain fills release escrow locally with no Hyperbridge dispatch, so the
|
|
18795
19130
|
* gateway never pulls the fee token — its approval is skipped. Only cross-chain
|
|
18796
19131
|
* fills, which dispatch a RedeemEscrow message paid in the fee token, need it.
|
|
19132
|
+
*
|
|
19133
|
+
* A bid priced by a limit order ends with `debitOrder`, which takes the budget's id
|
|
19134
|
+
* as its `orderId`, works out what the fill paid from what is left of the allowance
|
|
19135
|
+
* and reverts the whole batch if the order's payouts would pass its size. It has to
|
|
19136
|
+
* come last, after the gateway has drawn on the allowance.
|
|
18797
19137
|
*/
|
|
18798
|
-
async buildApprovalAndFillCalldata(order, fillerOutputs, fillOptions, requiredFeeTokenAmount) {
|
|
19138
|
+
async buildApprovalAndFillCalldata(order, fillerOutputs, fillOptions, requiredFeeTokenAmount, settlement) {
|
|
18799
19139
|
const chain = order.destination;
|
|
18800
19140
|
const intentGatewayV2Address = this.configService.getIntentGatewayAddress(chain);
|
|
18801
19141
|
const perTokenRequired = /* @__PURE__ */ new Map();
|
|
@@ -18805,9 +19145,10 @@ var ContractInteractionService = class {
|
|
|
18805
19145
|
const key = addr.toLowerCase();
|
|
18806
19146
|
perTokenRequired.set(key, (perTokenRequired.get(key) ?? 0n) + output.amount);
|
|
18807
19147
|
}
|
|
19148
|
+
let feeKey;
|
|
18808
19149
|
if (order.source !== order.destination) {
|
|
18809
19150
|
const feeToken = await this.getFeeTokenWithDecimals(chain);
|
|
18810
|
-
|
|
19151
|
+
feeKey = feeToken.address.toLowerCase();
|
|
18811
19152
|
perTokenRequired.set(feeKey, (perTokenRequired.get(feeKey) ?? 0n) + requiredFeeTokenAmount);
|
|
18812
19153
|
}
|
|
18813
19154
|
const fundingPrepends = order.id ? this.cacheService.getFundingPrepends(order.id) : null;
|
|
@@ -18833,6 +19174,26 @@ var ContractInteractionService = class {
|
|
|
18833
19174
|
value: nativeOutputValue,
|
|
18834
19175
|
data: encodeFillOrder(transformOrderForContract(order), fillOptions)
|
|
18835
19176
|
});
|
|
19177
|
+
if (settlement && settles(settlement.budget)) {
|
|
19178
|
+
const { budget, solverAccount } = settlement;
|
|
19179
|
+
const budgetToken = budget.token.toLowerCase();
|
|
19180
|
+
const approved = perTokenRequired.get(budgetToken) ?? 0n;
|
|
19181
|
+
if (approved === 0n) {
|
|
19182
|
+
throw new Error(
|
|
19183
|
+
`Bid on order ${order.id} approves nothing in ${budget.token}, the token budget ${budget.budgetId} is measured in`
|
|
19184
|
+
);
|
|
19185
|
+
}
|
|
19186
|
+
const fee = feeKey === budgetToken ? fillOptions.relayerFee : 0n;
|
|
19187
|
+
calls.push({
|
|
19188
|
+
target: solverAccount,
|
|
19189
|
+
value: 0n,
|
|
19190
|
+
data: encodeFunctionData2({
|
|
19191
|
+
abi: SOLVER_ACCOUNT_ABI,
|
|
19192
|
+
functionName: "debitOrder",
|
|
19193
|
+
args: [budget.budgetId, budget.cap, budgetToken, approved, fee]
|
|
19194
|
+
})
|
|
19195
|
+
});
|
|
19196
|
+
}
|
|
18836
19197
|
return encodeERC7821ExecuteBatch2(calls);
|
|
18837
19198
|
}
|
|
18838
19199
|
};
|
|
@@ -20685,11 +21046,11 @@ function byBestRate(a, b) {
|
|
|
20685
21046
|
return left === right ? 0 : left > right ? -1 : 1;
|
|
20686
21047
|
}
|
|
20687
21048
|
function chargedFor(hold, delivered, released) {
|
|
20688
|
-
|
|
20689
|
-
|
|
20690
|
-
|
|
20691
|
-
const atRate = (
|
|
20692
|
-
return atRate > delivered ? atRate : delivered;
|
|
21049
|
+
const take = hold.take === void 0 ? 0n : BigInt(hold.take);
|
|
21050
|
+
if (take === 0n || released === 0n) return toScaledUp(delivered.raw, delivered.decimals);
|
|
21051
|
+
const offered = toRaw(BigInt(hold.amount), delivered.decimals);
|
|
21052
|
+
const atRate = (offered * released + take - 1n) / take;
|
|
21053
|
+
return toScaledUp(atRate > delivered.raw ? atRate : delivered.raw, delivered.decimals);
|
|
20693
21054
|
}
|
|
20694
21055
|
var IntentFiller = class {
|
|
20695
21056
|
constructor(chainConfigs, strategies, config, configService, chainClientManager, contractService, signer, scanners, rebalancingService, bidStorage, limitOrders, assetRegistry) {
|
|
@@ -20766,6 +21127,17 @@ var IntentFiller = class {
|
|
|
20766
21127
|
setLimitOrderService(service) {
|
|
20767
21128
|
this.limitOrderService = service;
|
|
20768
21129
|
}
|
|
21130
|
+
/**
|
|
21131
|
+
* Brings the limit orders' `remaining` in line with their on-chain tallies.
|
|
21132
|
+
*
|
|
21133
|
+
* On the settlement queue, so the pass and a fill never resize the same order at
|
|
21134
|
+
* once. Resolves when the pass has run.
|
|
21135
|
+
*/
|
|
21136
|
+
async reconcileLimitOrderTallies() {
|
|
21137
|
+
await this.settlementQueue.add(async () => {
|
|
21138
|
+
await this.limitOrderService?.reconcileTallies();
|
|
21139
|
+
});
|
|
21140
|
+
}
|
|
20769
21141
|
/** Whether the given chain id is configured for watch-only (monitor, never fill). */
|
|
20770
21142
|
isChainWatchOnly(chainId) {
|
|
20771
21143
|
const watchOnly = this.config.watchOnly;
|
|
@@ -21343,6 +21715,23 @@ var IntentFiller = class {
|
|
|
21343
21715
|
this.monitor.emit("orderSkipped", { orderId: order.id, reason: "watch-only" });
|
|
21344
21716
|
return null;
|
|
21345
21717
|
}
|
|
21718
|
+
const destinationHead = await this.destinationHead(order);
|
|
21719
|
+
if (destinationHead !== null && order.deadline <= destinationHead) {
|
|
21720
|
+
this.logger.info(
|
|
21721
|
+
{
|
|
21722
|
+
orderId: order.id,
|
|
21723
|
+
destChain: order.destination,
|
|
21724
|
+
deadline: order.deadline.toString(),
|
|
21725
|
+
head: destinationHead.toString()
|
|
21726
|
+
},
|
|
21727
|
+
"Skipping order: its deadline has passed on the destination chain"
|
|
21728
|
+
);
|
|
21729
|
+
this.monitor.emit("orderSkipped", {
|
|
21730
|
+
orderId: order.id,
|
|
21731
|
+
reason: "Order deadline has passed on the destination chain"
|
|
21732
|
+
});
|
|
21733
|
+
return null;
|
|
21734
|
+
}
|
|
21346
21735
|
const evalStartMs = Date.now();
|
|
21347
21736
|
const eligibleStrategies = await Promise.all(
|
|
21348
21737
|
this.strategies.map(async (strategy) => {
|
|
@@ -21352,7 +21741,10 @@ var IntentFiller = class {
|
|
|
21352
21741
|
})
|
|
21353
21742
|
);
|
|
21354
21743
|
const fillsPartially = this.fillsPartially(order);
|
|
21355
|
-
const
|
|
21744
|
+
const feeCheckWaived = this.feeCheckWaived(order);
|
|
21745
|
+
const validStrategies = eligibleStrategies.filter(
|
|
21746
|
+
(s) => s !== null && (s.profitability > 0 || fillsPartially || feeCheckWaived)
|
|
21747
|
+
).sort((a, b) => b.profitability - a.profitability);
|
|
21356
21748
|
const evalDurationSec = (Date.now() - evalStartMs) / 1e3;
|
|
21357
21749
|
this.monitor.emit("orderTiming", { orderId: order.id, phase: "evaluation", durationSec: evalDurationSec });
|
|
21358
21750
|
if (validStrategies.length === 0) {
|
|
@@ -21377,6 +21769,23 @@ var IntentFiller = class {
|
|
|
21377
21769
|
);
|
|
21378
21770
|
return validStrategies[0];
|
|
21379
21771
|
}
|
|
21772
|
+
/**
|
|
21773
|
+
* The destination chain's latest block number, the clock `order.deadline` is read on.
|
|
21774
|
+
*
|
|
21775
|
+
* Null when the read fails. That is not an answer about the order, so evaluation goes
|
|
21776
|
+
* on: the gas estimate simulates the same deadline check and refuses an expired order.
|
|
21777
|
+
*/
|
|
21778
|
+
async destinationHead(order) {
|
|
21779
|
+
try {
|
|
21780
|
+
return await this.chainClientManager.getPublicClient(order.destination).getBlockNumber();
|
|
21781
|
+
} catch (err) {
|
|
21782
|
+
this.logger.warn(
|
|
21783
|
+
{ orderId: order.id, destChain: order.destination, err },
|
|
21784
|
+
"Could not read the destination head to check the order's deadline"
|
|
21785
|
+
);
|
|
21786
|
+
return null;
|
|
21787
|
+
}
|
|
21788
|
+
}
|
|
21380
21789
|
/**
|
|
21381
21790
|
* Whether the strategy's evaluation concluded in a deliberate partial fill.
|
|
21382
21791
|
*
|
|
@@ -21390,6 +21799,15 @@ var IntentFiller = class {
|
|
|
21390
21799
|
if (!order.id) return false;
|
|
21391
21800
|
return this.contractService.cacheService.isPartialFill(order.id);
|
|
21392
21801
|
}
|
|
21802
|
+
/**
|
|
21803
|
+
* Whether the strategy's evaluation let a bid through without the fee check, because
|
|
21804
|
+
* the order is at or above the operator's minimum size. Read from a flag the strategy
|
|
21805
|
+
* sets only once it has an answer, as {@link fillsPartially} is.
|
|
21806
|
+
*/
|
|
21807
|
+
feeCheckWaived(order) {
|
|
21808
|
+
if (!order.id) return false;
|
|
21809
|
+
return this.contractService.cacheService.isFeeCheckWaived(order.id);
|
|
21810
|
+
}
|
|
21393
21811
|
executeOrder(order, bestStrategy, solverSelectionActive, inputUsdValue, profitUsd) {
|
|
21394
21812
|
const chainQueue = this.chainQueues.get(getChainId3(order.destination));
|
|
21395
21813
|
if (!chainQueue) {
|
|
@@ -21411,7 +21829,7 @@ var IntentFiller = class {
|
|
|
21411
21829
|
let reservation = [];
|
|
21412
21830
|
if (plan) {
|
|
21413
21831
|
if (order.id) {
|
|
21414
|
-
cache.setFillerOutputs(order.id, plan.fillerOutputs, plan.fillerInputs);
|
|
21832
|
+
cache.setFillerOutputs(order.id, plan.fillerOutputs, plan.fillerInputs, plan.budget);
|
|
21415
21833
|
cache.setPartialFill(order.id, plan.partialFill);
|
|
21416
21834
|
if (plan.fundingCalls.length > 0) cache.setFundingPrepends(order.id, plan.fundingCalls);
|
|
21417
21835
|
else cache.clearFundingPrepends(order.id);
|
|
@@ -21612,7 +22030,7 @@ var IntentFiller = class {
|
|
|
21612
22030
|
await this.releaseAll(claimed);
|
|
21613
22031
|
return;
|
|
21614
22032
|
}
|
|
21615
|
-
const settled = executedHold(claimed, delivered, released);
|
|
22033
|
+
const settled = executedHold(claimed, delivered.scaled, released);
|
|
21616
22034
|
const charged = chargedFor(settled, delivered, released);
|
|
21617
22035
|
const settledBid = rows.find(
|
|
21618
22036
|
(row) => row.reservations.some(
|
|
@@ -21665,15 +22083,15 @@ var IntentFiller = class {
|
|
|
21665
22083
|
if (delivered === null) return [];
|
|
21666
22084
|
const settled = executedHold(
|
|
21667
22085
|
holds.map((entry) => entry.hold),
|
|
21668
|
-
delivered,
|
|
22086
|
+
delivered.scaled,
|
|
21669
22087
|
released
|
|
21670
22088
|
);
|
|
21671
22089
|
const row = holds.find((entry) => entry.hold === settled).row;
|
|
21672
22090
|
return this.bidStorage.claimReservation(commitment, row.bid ?? void 0);
|
|
21673
22091
|
}
|
|
21674
22092
|
/**
|
|
21675
|
-
* What this fill delivered of the token the limit order pays,
|
|
21676
|
-
*
|
|
22093
|
+
* What this fill delivered of the token the limit order pays, or null when the
|
|
22094
|
+
* fill names none of it.
|
|
21677
22095
|
*/
|
|
21678
22096
|
async deliveredAgainst(limitOrderId, chainId, outputs) {
|
|
21679
22097
|
const order = await this.limitOrders?.get(limitOrderId);
|
|
@@ -21689,7 +22107,7 @@ var IntentFiller = class {
|
|
|
21689
22107
|
}
|
|
21690
22108
|
if (total === 0n) return null;
|
|
21691
22109
|
const decimals = await this.contractService.getTokenDecimals(address, chain);
|
|
21692
|
-
return toScaled(total, decimals);
|
|
22110
|
+
return { scaled: toScaled(total, decimals), raw: total, decimals };
|
|
21693
22111
|
}
|
|
21694
22112
|
/**
|
|
21695
22113
|
* Gives back whatever this bid still holds against its limit order.
|
|
@@ -21785,87 +22203,6 @@ import {
|
|
|
21785
22203
|
import { formatUnits as formatUnits4 } from "viem";
|
|
21786
22204
|
import { Decimal as Decimal6 } from "decimal.js";
|
|
21787
22205
|
|
|
21788
|
-
// src/orderbook/matching.ts
|
|
21789
|
-
function limitOrderLegs(order) {
|
|
21790
|
-
return order.side === "BID" ? { input: order.base, output: order.quote } : { input: order.quote, output: order.base };
|
|
21791
|
-
}
|
|
21792
|
-
function matchLimitOrders(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
|
|
21793
|
-
const qualifying = orders.filter((order) => serves(order, incoming, resolve, now)).map((order) => {
|
|
21794
|
-
const offer = offerFor({
|
|
21795
|
-
side: order.side,
|
|
21796
|
-
inputAmount: incoming.inputNet,
|
|
21797
|
-
price: BigInt(order.price),
|
|
21798
|
-
outputDecimals: incoming.outputDecimals
|
|
21799
|
-
});
|
|
21800
|
-
const available = BigInt(order.remaining);
|
|
21801
|
-
return { order, offer, available, payout: offer < available ? offer : available };
|
|
21802
|
-
}).filter((candidate) => candidate.offer >= incoming.requestedOutput && candidate.payout > 0n).sort(byBestOfferFirst);
|
|
21803
|
-
return qualifying;
|
|
21804
|
-
}
|
|
21805
|
-
function byBestOfferFirst(a, b) {
|
|
21806
|
-
if (a.offer !== b.offer) return a.offer > b.offer ? -1 : 1;
|
|
21807
|
-
if (a.available !== b.available) return a.available > b.available ? -1 : 1;
|
|
21808
|
-
return a.order.id < b.order.id ? -1 : 1;
|
|
21809
|
-
}
|
|
21810
|
-
function whyUnmatched(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
|
|
21811
|
-
let furthest = -1;
|
|
21812
|
-
for (const order of orders) {
|
|
21813
|
-
const failed = firstFailed(order, incoming, resolve, now);
|
|
21814
|
-
furthest = Math.max(furthest, failed === null ? CHECKS.length : CHECKS.indexOf(failed));
|
|
21815
|
-
}
|
|
21816
|
-
const { source, destination, inputSymbol } = incoming;
|
|
21817
|
-
if (furthest < CHECKS.length) {
|
|
21818
|
-
switch (CHECKS[Math.max(furthest, 0)]) {
|
|
21819
|
-
case "open":
|
|
21820
|
-
return "no limit order is open";
|
|
21821
|
-
case "chain":
|
|
21822
|
-
return `no open limit order fills on ${destination}`;
|
|
21823
|
-
case "input":
|
|
21824
|
-
return `no limit order on ${destination} takes ${inputSymbol} in`;
|
|
21825
|
-
case "output":
|
|
21826
|
-
return `no limit order on ${destination} taking ${inputSymbol} in pays out the token this order asks for`;
|
|
21827
|
-
case "source":
|
|
21828
|
-
return `no limit order for this pair on ${destination} accepts swaps from ${source}`;
|
|
21829
|
-
}
|
|
21830
|
-
}
|
|
21831
|
-
const priced = orders.filter((order) => firstFailed(order, incoming, resolve, now) === null).map((order) => ({
|
|
21832
|
-
order,
|
|
21833
|
-
offer: offerFor({
|
|
21834
|
-
side: order.side,
|
|
21835
|
-
inputAmount: incoming.inputNet,
|
|
21836
|
-
price: BigInt(order.price),
|
|
21837
|
-
outputDecimals: incoming.outputDecimals
|
|
21838
|
-
})
|
|
21839
|
-
}));
|
|
21840
|
-
if (priced.some(({ offer }) => offer >= incoming.requestedOutput)) {
|
|
21841
|
-
return "the limit orders that meet its rate have nothing left to pay out";
|
|
21842
|
-
}
|
|
21843
|
-
const best = priced.reduce((a, b) => b.offer > a.offer ? b : a);
|
|
21844
|
-
const symbol = limitOrderLegs(best.order).output;
|
|
21845
|
-
return `it asks for ${readable(incoming.requestedOutput)} ${symbol}; the best limit order offers ${readable(best.offer)} ${symbol}`;
|
|
21846
|
-
}
|
|
21847
|
-
var CHECKS = ["open", "chain", "input", "output", "source"];
|
|
21848
|
-
function serves(order, incoming, resolve, now) {
|
|
21849
|
-
return firstFailed(order, incoming, resolve, now) === null;
|
|
21850
|
-
}
|
|
21851
|
-
function firstFailed(order, incoming, resolve, now) {
|
|
21852
|
-
if (order.status !== "open") return "open";
|
|
21853
|
-
if (order.expiresAt !== null && new Date(order.expiresAt) <= now) return "open";
|
|
21854
|
-
if (order.fillChain !== incoming.destination) return "chain";
|
|
21855
|
-
const legs = limitOrderLegs(order);
|
|
21856
|
-
if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return "input";
|
|
21857
|
-
const outputToken = resolve(legs.output, order.fillChain);
|
|
21858
|
-
if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return "output";
|
|
21859
|
-
if (incoming.source === incoming.destination) return null;
|
|
21860
|
-
return order.acceptedSources.includes(incoming.source) ? null : "source";
|
|
21861
|
-
}
|
|
21862
|
-
function readable(amount) {
|
|
21863
|
-
const [whole, fraction] = toHuman(amount).split(".");
|
|
21864
|
-
const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
21865
|
-
const cut = fraction?.slice(0, 6).replace(/0+$/, "");
|
|
21866
|
-
return cut ? `${grouped}.${cut}` : grouped;
|
|
21867
|
-
}
|
|
21868
|
-
|
|
21869
22206
|
// src/orderbook/usd.ts
|
|
21870
22207
|
import { Decimal as Decimal5 } from "decimal.js";
|
|
21871
22208
|
function limitOrderUsdEdges(orders) {
|
|
@@ -21920,9 +22257,13 @@ var FXFiller = class _FXFiller {
|
|
|
21920
22257
|
*/
|
|
21921
22258
|
constructor(signer, configService, clientManager, contractService, pairs, registry, options) {
|
|
21922
22259
|
this.name = "FXFiller";
|
|
21923
|
-
/**
|
|
22260
|
+
/**
|
|
22261
|
+
* Consecutive orders where the overfill clamp activated. Dormant: the clamp
|
|
22262
|
+
* existed for venue-priced legs, which went with Uniswap V4 pricing, so every
|
|
22263
|
+
* outcome is now recorded unclamped and this never climbs. See `recordOrderOutcome`.
|
|
22264
|
+
*/
|
|
21924
22265
|
this.consecutiveClamps = 0;
|
|
21925
|
-
/** Once set, the filler refuses all orders until
|
|
22266
|
+
/** Once set, the filler refuses all orders until reset. Unreachable while the clamp is dormant. */
|
|
21926
22267
|
this.halted = false;
|
|
21927
22268
|
this.logger = moduleLogger(configService.loggers, "fx-simplex");
|
|
21928
22269
|
const { confirmationPolicy, fundingVenues = [], limitOrders } = options ?? {};
|
|
@@ -21971,10 +22312,9 @@ var FXFiller = class _FXFiller {
|
|
|
21971
22312
|
this.pairs.push(pair);
|
|
21972
22313
|
}
|
|
21973
22314
|
/**
|
|
21974
|
-
* Removes a market from the running engine.
|
|
21975
|
-
*
|
|
21976
|
-
*
|
|
21977
|
-
* rechecks of orders on the removed pair simply stop matching.
|
|
22315
|
+
* Removes a market from the running engine. At least one market must remain.
|
|
22316
|
+
* Matching reads the limit orders rather than the pairs, so removing a market
|
|
22317
|
+
* changes what the dashboard lists, not what the filler fills.
|
|
21978
22318
|
*/
|
|
21979
22319
|
removePair(pair) {
|
|
21980
22320
|
const index = this.pairs.indexOf(pair);
|
|
@@ -22030,25 +22370,25 @@ var FXFiller = class _FXFiller {
|
|
|
22030
22370
|
}
|
|
22031
22371
|
}
|
|
22032
22372
|
/**
|
|
22033
|
-
* Evaluates whether an order is profitable to fill
|
|
22034
|
-
*
|
|
22373
|
+
* Evaluates whether an order is profitable to fill against the operator's
|
|
22374
|
+
* limit orders and the filler's current token balances.
|
|
22035
22375
|
*
|
|
22036
22376
|
* High-level flow:
|
|
22037
|
-
* -
|
|
22038
|
-
* -
|
|
22039
|
-
*
|
|
22040
|
-
*
|
|
22041
|
-
*
|
|
22042
|
-
*
|
|
22043
|
-
*
|
|
22044
|
-
*
|
|
22045
|
-
*
|
|
22046
|
-
*
|
|
22047
|
-
*
|
|
22048
|
-
* outputs if the pair pricing makes that attractive. This is how we stay competitive.
|
|
22377
|
+
* - Match each (input, output) leg against the open limit orders.
|
|
22378
|
+
* - Build one bid per matching order, best offer first, each paying the
|
|
22379
|
+
* order's whole offer for the input at its own rate, capped by what the
|
|
22380
|
+
* order has left. The gateway credits the swapper the ask and splits
|
|
22381
|
+
* anything above it between the swapper and the protocol.
|
|
22382
|
+
* - Treat a payout below the ask as a partial fill, which only orders
|
|
22383
|
+
* without output calldata and without an earlier partial fill allow.
|
|
22384
|
+
* - Cap each payout by the filler's balance plus funding-venue withdrawals,
|
|
22385
|
+
* then gate it on fees (full fills of orders below `minOrderSizeUsd`) and the
|
|
22386
|
+
* same-asset spread.
|
|
22387
|
+
* - Cache the resulting bids for later use in `executeOrder`.
|
|
22049
22388
|
*/
|
|
22050
22389
|
async calculateProfitability(order) {
|
|
22051
22390
|
if (order.id) this.contractService.cacheService.clearPartialFill(order.id);
|
|
22391
|
+
if (order.id) this.contractService.cacheService.clearFeeCheckWaived(order.id);
|
|
22052
22392
|
if (this.halted) {
|
|
22053
22393
|
this.logger.warn({ orderId: order.id }, "FXFiller halted \u2014 rejecting order");
|
|
22054
22394
|
return 0;
|
|
@@ -22063,6 +22403,16 @@ var FXFiller = class _FXFiller {
|
|
|
22063
22403
|
const multiLeg = order.inputs.length > 1;
|
|
22064
22404
|
const plans = [];
|
|
22065
22405
|
const plannedOn = /* @__PURE__ */ new Map();
|
|
22406
|
+
const spentOn = /* @__PURE__ */ new Map();
|
|
22407
|
+
let atOrAboveMinSize;
|
|
22408
|
+
const feeCheckWaived = async () => {
|
|
22409
|
+
if (atOrAboveMinSize === void 0) {
|
|
22410
|
+
const usd = await this.getOrderUsdValue(order);
|
|
22411
|
+
atOrAboveMinSize = usd !== null && usd.inputUsd.gte(this.configService.getMinOrderSizeUsd());
|
|
22412
|
+
}
|
|
22413
|
+
return atOrAboveMinSize;
|
|
22414
|
+
};
|
|
22415
|
+
let feesWaived = false;
|
|
22066
22416
|
for (let leg = 0; leg < order.inputs.length; leg++) {
|
|
22067
22417
|
const input = order.inputs[leg];
|
|
22068
22418
|
const output = order.output.assets[leg];
|
|
@@ -22084,7 +22434,30 @@ var FXFiller = class _FXFiller {
|
|
|
22084
22434
|
for (const candidate of matches) {
|
|
22085
22435
|
let partialFill = false;
|
|
22086
22436
|
const fundingCalls = [];
|
|
22087
|
-
const
|
|
22437
|
+
const budget = budgetFor(candidate.order, outputToken, outputDecimals);
|
|
22438
|
+
if (!spentOn.has(candidate.order.id)) {
|
|
22439
|
+
spentOn.set(
|
|
22440
|
+
candidate.order.id,
|
|
22441
|
+
await this.contractService.limitOrderSpent(destChain, budget.budgetId)
|
|
22442
|
+
);
|
|
22443
|
+
}
|
|
22444
|
+
const spent = spentOn.get(candidate.order.id) ?? null;
|
|
22445
|
+
const room = spent === null ? candidate.available : budgetRoom(budget, spent, outputDecimals);
|
|
22446
|
+
if (room === 0n) {
|
|
22447
|
+
this.logger.info(
|
|
22448
|
+
{
|
|
22449
|
+
orderId: order.id,
|
|
22450
|
+
leg,
|
|
22451
|
+
limitOrder: candidate.order.id,
|
|
22452
|
+
spent: spent?.toString(),
|
|
22453
|
+
cap: budget.cap.toString()
|
|
22454
|
+
},
|
|
22455
|
+
"Skipping a bid: the limit order has nothing left on chain"
|
|
22456
|
+
);
|
|
22457
|
+
continue;
|
|
22458
|
+
}
|
|
22459
|
+
const available = room < candidate.available ? room : candidate.available;
|
|
22460
|
+
const left = available - (plannedOn.get(candidate.order.id) ?? 0n);
|
|
22088
22461
|
if (left <= 0n) {
|
|
22089
22462
|
this.logger.info(
|
|
22090
22463
|
{ orderId: order.id, leg, limitOrder: candidate.order.id },
|
|
@@ -22119,10 +22492,7 @@ var FXFiller = class _FXFiller {
|
|
|
22119
22492
|
{
|
|
22120
22493
|
orderId: order.id,
|
|
22121
22494
|
limitOrder: candidate.order.id,
|
|
22122
|
-
available: formatUnits4(
|
|
22123
|
-
candidate.available,
|
|
22124
|
-
18
|
|
22125
|
-
),
|
|
22495
|
+
available: formatUnits4(available, 18),
|
|
22126
22496
|
userRequested: output.amount.toString(),
|
|
22127
22497
|
payout: targetOutput.toString(),
|
|
22128
22498
|
crossChain: sourceChain !== destChain
|
|
@@ -22167,7 +22537,10 @@ var FXFiller = class _FXFiller {
|
|
|
22167
22537
|
}
|
|
22168
22538
|
}
|
|
22169
22539
|
const effectiveBalance = walletContribution + credited;
|
|
22170
|
-
const finalOutputAmount =
|
|
22540
|
+
const finalOutputAmount = toRaw(
|
|
22541
|
+
toScaled(effectiveBalance > targetOutput ? targetOutput : effectiveBalance, outputDecimals),
|
|
22542
|
+
outputDecimals
|
|
22543
|
+
);
|
|
22171
22544
|
if (finalOutputAmount === 0n) {
|
|
22172
22545
|
this.logger.info(
|
|
22173
22546
|
{
|
|
@@ -22283,17 +22656,21 @@ var FXFiller = class _FXFiller {
|
|
|
22283
22656
|
const executionCost = totalCostInSourceFeeToken + relayerFeeInSourceFeeToken;
|
|
22284
22657
|
const partialEdgeUsd = sameAssetEdgeUsd.plus(payoutSurplusUsd);
|
|
22285
22658
|
if (!partialFill && order.fees < executionCost) {
|
|
22286
|
-
|
|
22287
|
-
|
|
22288
|
-
|
|
22289
|
-
|
|
22290
|
-
|
|
22291
|
-
|
|
22292
|
-
|
|
22293
|
-
|
|
22294
|
-
|
|
22295
|
-
|
|
22296
|
-
|
|
22659
|
+
if (!await feeCheckWaived()) {
|
|
22660
|
+
this.logger.info(
|
|
22661
|
+
{
|
|
22662
|
+
orderId: order.id,
|
|
22663
|
+
orderFees: formatUnits4(order.fees, feeTokenDecimals),
|
|
22664
|
+
fillGas: formatUnits4(totalCostInSourceFeeToken, feeTokenDecimals),
|
|
22665
|
+
relayerFee: formatUnits4(relayerFeeInSourceFeeToken, feeTokenDecimals),
|
|
22666
|
+
executionCost: formatUnits4(executionCost, feeTokenDecimals),
|
|
22667
|
+
minOrderSizeUsd: this.configService.getMinOrderSizeUsd()
|
|
22668
|
+
},
|
|
22669
|
+
"Skipping order: attached fees do not cover execution cost (fill gas + relayer fee)"
|
|
22670
|
+
);
|
|
22671
|
+
continue;
|
|
22672
|
+
}
|
|
22673
|
+
feesWaived = true;
|
|
22297
22674
|
}
|
|
22298
22675
|
if (sameAsset && !sameAssetProfitable) {
|
|
22299
22676
|
this.logger.info(
|
|
@@ -22327,7 +22704,8 @@ var FXFiller = class _FXFiller {
|
|
|
22327
22704
|
realizedSpreadProfit: formatUnits4(realizedSpreadProfit, feeTokenDecimals),
|
|
22328
22705
|
payoutSurplusUsd: payoutSurplusUsd.toString(),
|
|
22329
22706
|
totalProfit,
|
|
22330
|
-
profitable: totalProfit > 0
|
|
22707
|
+
profitable: totalProfit > 0,
|
|
22708
|
+
feeCheckWaived: !partialFill && order.fees < executionCost
|
|
22331
22709
|
},
|
|
22332
22710
|
"FX swap profitability evaluation"
|
|
22333
22711
|
);
|
|
@@ -22343,7 +22721,8 @@ var FXFiller = class _FXFiller {
|
|
|
22343
22721
|
fillerInputs,
|
|
22344
22722
|
fundingCalls: [...fundingCalls],
|
|
22345
22723
|
partialFill,
|
|
22346
|
-
profit: totalProfit
|
|
22724
|
+
profit: totalProfit,
|
|
22725
|
+
budget
|
|
22347
22726
|
});
|
|
22348
22727
|
if (!partialEligibleCheap) break;
|
|
22349
22728
|
}
|
|
@@ -22351,8 +22730,14 @@ var FXFiller = class _FXFiller {
|
|
|
22351
22730
|
if (plans.length === 0) return 0;
|
|
22352
22731
|
if (order.id) {
|
|
22353
22732
|
this.contractService.cacheService.setBidPlans(order.id, plans);
|
|
22354
|
-
this.contractService.cacheService.setFillerOutputs(
|
|
22733
|
+
this.contractService.cacheService.setFillerOutputs(
|
|
22734
|
+
order.id,
|
|
22735
|
+
plans[0].fillerOutputs,
|
|
22736
|
+
plans[0].fillerInputs,
|
|
22737
|
+
plans[0].budget
|
|
22738
|
+
);
|
|
22355
22739
|
this.contractService.cacheService.setPartialFill(order.id, plans[0].partialFill);
|
|
22740
|
+
this.contractService.cacheService.setFeeCheckWaived(order.id, feesWaived);
|
|
22356
22741
|
this.contractService.cacheService.setMatchedLimitOrder(order.id, [
|
|
22357
22742
|
{ limitOrderId: plans[0].limitOrderId, payout: plans[0].payout }
|
|
22358
22743
|
]);
|
|
@@ -22446,10 +22831,14 @@ var FXFiller = class _FXFiller {
|
|
|
22446
22831
|
// =========================================================================
|
|
22447
22832
|
/**
|
|
22448
22833
|
* Update consecutive-clamp counter after a successful order evaluation.
|
|
22449
|
-
*
|
|
22450
|
-
*
|
|
22451
|
-
*
|
|
22452
|
-
*
|
|
22834
|
+
*
|
|
22835
|
+
* Dormant. Only venue-priced legs ever fed a clamped outcome here: a streak
|
|
22836
|
+
* of those meant a live market source had gone off (stale pool, manipulated
|
|
22837
|
+
* venue). Venue pricing went with Uniswap V4, and limit orders are prices the
|
|
22838
|
+
* operator set, so the only caller records `false` and the filler never
|
|
22839
|
+
* halts. `isHalted`, `resetHalt` and `[simplex.overfillProtection]
|
|
22840
|
+
* maxConsecutiveClamps` are kept so existing configs and the halt API still
|
|
22841
|
+
* load; they can go once nothing reads them.
|
|
22453
22842
|
*/
|
|
22454
22843
|
recordOrderOutcome(clamped, orderId) {
|
|
22455
22844
|
if (clamped) {
|
|
@@ -22475,18 +22864,6 @@ var FXFiller = class _FXFiller {
|
|
|
22475
22864
|
this.consecutiveClamps = 0;
|
|
22476
22865
|
this.logger.warn("FXFiller halt reset by operator \u2014 resuming order evaluation");
|
|
22477
22866
|
}
|
|
22478
|
-
/**
|
|
22479
|
-
* Given a single (input, output) leg and the remaining token0 budget of its
|
|
22480
|
-
* pair, computes how much token0 notional to allocate to this leg and the
|
|
22481
|
-
* corresponding maximum output amount at the pair's rate.
|
|
22482
|
-
*
|
|
22483
|
-
* `rate` is **token1 per 1 token0**:
|
|
22484
|
-
* - token0 input → token1 output: token0 × rate → token1 amount.
|
|
22485
|
-
* - token1 input → token0 output: token1 ÷ rate → token0 amount.
|
|
22486
|
-
*
|
|
22487
|
-
* Returns `null` when this leg cannot consume any of the pair's remaining
|
|
22488
|
-
* budget (e.g. the cap has already been exhausted).
|
|
22489
|
-
*/
|
|
22490
22867
|
/**
|
|
22491
22868
|
* Whether any solver has already delivered output against this order.
|
|
22492
22869
|
*
|
|
@@ -22824,9 +23201,9 @@ var LimitOrderLifecycle = class {
|
|
|
22824
23201
|
const reconcileMs = this.options.reconcileIntervalSecs * 1e3;
|
|
22825
23202
|
this.every(heartbeatMs, "heartbeat", () => this.service.heartbeat());
|
|
22826
23203
|
this.every(EXPIRY_SWEEP_MS, "expiry", () => this.expire());
|
|
22827
|
-
this.every(reconcileMs, "reconciliation", () => this.
|
|
23204
|
+
this.every(reconcileMs, "reconciliation", () => this.upkeep());
|
|
22828
23205
|
this.logger.info({ heartbeatMs, expiryMs: EXPIRY_SWEEP_MS, reconcileMs }, "Orderbook lifecycle started");
|
|
22829
|
-
void this.
|
|
23206
|
+
void this.upkeep();
|
|
22830
23207
|
}
|
|
22831
23208
|
stop() {
|
|
22832
23209
|
this.timers.forEach((timer) => clearInterval(timer));
|
|
@@ -22837,6 +23214,14 @@ var LimitOrderLifecycle = class {
|
|
|
22837
23214
|
const expired = await this.service.expireStale();
|
|
22838
23215
|
if (expired > 0) this.logger.info({ expired }, "Withdrew limit orders that had outlived their expiry");
|
|
22839
23216
|
}
|
|
23217
|
+
/**
|
|
23218
|
+
* The tally pass first, so reconciliation against the orderbook reads the sizes
|
|
23219
|
+
* it left. Each fails on its own.
|
|
23220
|
+
*/
|
|
23221
|
+
async upkeep() {
|
|
23222
|
+
await this.run("tally reconciliation", async () => this.options.reconcileTallies());
|
|
23223
|
+
await this.run("reconciliation", () => this.reconcile());
|
|
23224
|
+
}
|
|
22840
23225
|
async reconcile() {
|
|
22841
23226
|
const report = await this.service.reconcile();
|
|
22842
23227
|
if (report.cancelled || report.reposted || report.underFunded) {
|
|
@@ -23594,6 +23979,7 @@ async function bootFiller(config, options) {
|
|
|
23594
23979
|
rebalancing: config.rebalancing,
|
|
23595
23980
|
targetGasUnits: config.simplex.targetGasUnits,
|
|
23596
23981
|
bidValiditySeconds: config.simplex.bidValiditySeconds,
|
|
23982
|
+
minOrderSizeUsd: config.simplex.minOrderSizeUsd,
|
|
23597
23983
|
blockScanIntervalSeconds: config.simplex.blockScanIntervalSeconds,
|
|
23598
23984
|
gasFeeBump: config.simplex.gasFeeBump,
|
|
23599
23985
|
overfillProtection: config.simplex.overfillProtection,
|
|
@@ -23637,7 +24023,7 @@ async function bootFiller(config, options) {
|
|
|
23637
24023
|
const sharedCacheService = new CacheService(options.loggers);
|
|
23638
24024
|
if (!options.signer && !globalWatchOnly && !allChainsWatchOnly(watchOnlyConfig, resolvedChains)) {
|
|
23639
24025
|
throw new Error(
|
|
23640
|
-
"A signer is required unless every chain is watch-only: pass `signer` to Simplex.start (privateKeySigner, turnkeySigner, mpcVaultSigner, viemSigner, or your own Signer implementation). Running the binary? Configure it under [simplex.signer]."
|
|
24026
|
+
"A signer is required unless every chain is watch-only: pass `signer` to Simplex.start (privateKeySigner, secretPhraseSigner, turnkeySigner, mpcVaultSigner, viemSigner, or your own Signer implementation). Running the binary? Configure it under [simplex.signer]."
|
|
23641
24027
|
);
|
|
23642
24028
|
}
|
|
23643
24029
|
const chainClientManager = new ChainClientManager(configService, options.signer);
|
|
@@ -23776,7 +24162,8 @@ async function bootFiller(config, options) {
|
|
|
23776
24162
|
const lifecycle = new LimitOrderLifecycle(
|
|
23777
24163
|
limitOrderService,
|
|
23778
24164
|
{
|
|
23779
|
-
reconcileIntervalSecs: config.orderbook?.reconcileIntervalSecs ?? DEFAULT_RECONCILE_INTERVAL_SECS
|
|
24165
|
+
reconcileIntervalSecs: config.orderbook?.reconcileIntervalSecs ?? DEFAULT_RECONCILE_INTERVAL_SECS,
|
|
24166
|
+
reconcileTallies: () => intentFiller.reconcileLimitOrderTallies()
|
|
23780
24167
|
},
|
|
23781
24168
|
options.loggers
|
|
23782
24169
|
);
|
|
@@ -24247,6 +24634,14 @@ var MemoryLimitOrderStore = class {
|
|
|
24247
24634
|
const remaining = BigInt(order.remaining) - BigInt(amount);
|
|
24248
24635
|
return this.patch(id, { remaining: (remaining > 0n ? remaining : 0n).toString() });
|
|
24249
24636
|
}
|
|
24637
|
+
async clampRemaining(id, room) {
|
|
24638
|
+
const order = this.orders.get(id);
|
|
24639
|
+
if (!order || order.status !== "open") return null;
|
|
24640
|
+
const sum = BigInt(room) + BigInt(order.reserved);
|
|
24641
|
+
const target = sum > 0n ? sum : 0n;
|
|
24642
|
+
if (target >= BigInt(order.remaining)) return null;
|
|
24643
|
+
return this.patch(id, { remaining: target.toString() });
|
|
24644
|
+
}
|
|
24250
24645
|
async release(id, amount) {
|
|
24251
24646
|
const order = this.orders.get(id);
|
|
24252
24647
|
if (!order) return;
|
|
@@ -24755,7 +25150,8 @@ var OrderScanner = class _OrderScanner {
|
|
|
24755
25150
|
* back from its own endpoints, which also proves they answer for one chain
|
|
24756
25151
|
* before a filler ever depends on them.
|
|
24757
25152
|
*
|
|
24758
|
-
* @throws if an endpoint set is invalid
|
|
25153
|
+
* @throws if an endpoint set is invalid, if none of a chain's endpoints answer,
|
|
25154
|
+
* if its endpoints disagree on the chain, if two entries resolve
|
|
24759
25155
|
* to the same chain, or if `scanIntervalSecs` is below the minimum.
|
|
24760
25156
|
*/
|
|
24761
25157
|
static async create(options) {
|
|
@@ -24815,7 +25211,10 @@ var OrderScanner = class _OrderScanner {
|
|
|
24815
25211
|
async addChainNow(chain) {
|
|
24816
25212
|
if (this.closed) throw new Error("This OrderScanner is closed");
|
|
24817
25213
|
const rpcUrls = validateRpcUrls(chain.rpcUrls);
|
|
24818
|
-
const chainId = chain.chainId ?? await
|
|
25214
|
+
const chainId = chain.chainId ?? (await resolveChainConfigs([{ rpcUrls, bundlerUrl: chain.bundlerUrl ?? "" }], {
|
|
25215
|
+
loggers: this.loggers,
|
|
25216
|
+
tolerateUnreachable: true
|
|
25217
|
+
}))[0].chainId;
|
|
24819
25218
|
if (this.closed) throw new Error("This OrderScanner is closed");
|
|
24820
25219
|
if (this.scanners.has(chainId)) {
|
|
24821
25220
|
throw new Error(`Chain ${chainId} is already in this scanner`);
|
|
@@ -24915,7 +25314,7 @@ var LimitOrderController = class {
|
|
|
24915
25314
|
get service() {
|
|
24916
25315
|
const service = this.runtime.limitOrders;
|
|
24917
25316
|
if (!service) {
|
|
24918
|
-
throw new Error("No orderbook is configured \u2014 set [orderbook]
|
|
25317
|
+
throw new Error("No orderbook is configured \u2014 set [orderbook] url to use limit orders");
|
|
24919
25318
|
}
|
|
24920
25319
|
return service;
|
|
24921
25320
|
}
|
|
@@ -25380,7 +25779,7 @@ var Simplex = class _Simplex extends EventEmitter4 {
|
|
|
25380
25779
|
const tomlSigner = options.config.simplex?.signer;
|
|
25381
25780
|
if (tomlSigner && !options.signer) {
|
|
25382
25781
|
throw new Error(
|
|
25383
|
-
"config.simplex.signer describes a signer but none was passed. Simplex.start takes a Signer instance: `signer: await createSigner(config.simplex.signer)`, or build one directly with privateKeySigner/turnkeySigner/mpcVaultSigner/viemSigner."
|
|
25782
|
+
"config.simplex.signer describes a signer but none was passed. Simplex.start takes a Signer instance: `signer: await createSigner(config.simplex.signer)`, or build one directly with privateKeySigner/secretPhraseSigner/turnkeySigner/mpcVaultSigner/viemSigner."
|
|
25384
25783
|
);
|
|
25385
25784
|
}
|
|
25386
25785
|
const loggers = new LoggerContext({
|
|
@@ -25583,6 +25982,8 @@ var Simplex = class _Simplex extends EventEmitter4 {
|
|
|
25583
25982
|
export {
|
|
25584
25983
|
AssetController,
|
|
25585
25984
|
ChainController,
|
|
25985
|
+
LimitOrderController,
|
|
25986
|
+
LimitOrderValidationError,
|
|
25586
25987
|
LoggerContext,
|
|
25587
25988
|
MemoryDataStore,
|
|
25588
25989
|
OrderScanner,
|
|
@@ -25595,6 +25996,7 @@ export {
|
|
|
25595
25996
|
WalletController,
|
|
25596
25997
|
addLogSink,
|
|
25597
25998
|
assertConfirmationCoverage,
|
|
25999
|
+
canDerive,
|
|
25598
26000
|
configureLogger,
|
|
25599
26001
|
createSigner,
|
|
25600
26002
|
digestSigner,
|
|
@@ -25607,10 +26009,12 @@ export {
|
|
|
25607
26009
|
pickAnchorStable,
|
|
25608
26010
|
privateKeySigner,
|
|
25609
26011
|
registrySymbols,
|
|
26012
|
+
secretPhraseSigner,
|
|
25610
26013
|
turnkeySigner,
|
|
25611
26014
|
validateAssetDefinitions,
|
|
25612
26015
|
validateConfig,
|
|
25613
26016
|
validatePairConfigs,
|
|
26017
|
+
validateSecretPhrase,
|
|
25614
26018
|
validateSignerConfig,
|
|
25615
26019
|
validateVaultToml,
|
|
25616
26020
|
viemSigner
|