@hyperbridge/simplex 0.16.7 → 0.16.11

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/index.js CHANGED
@@ -2781,11 +2781,21 @@ import { randomBytes, randomUUID } from "node:crypto";
2781
2781
  import { getChainId, MAX_DECLARED_ENTRIES } from "@hyperbridge/sdk";
2782
2782
 
2783
2783
  // src/orderbook/amounts.ts
2784
+ import { keccak256, stringToBytes } from "viem";
2784
2785
  var ORDERBOOK_SCALE = 10n ** 18n;
2785
2786
  function toRaw(amount, decimals) {
2786
2787
  const shift = 18 - decimals;
2787
2788
  return shift >= 0 ? amount / 10n ** BigInt(shift) : amount * 10n ** BigInt(-shift);
2788
2789
  }
2790
+ function budgetIdFor(limitOrderId) {
2791
+ return keccak256(stringToBytes(limitOrderId));
2792
+ }
2793
+ function budgetFor(order, token, decimals) {
2794
+ return { budgetId: budgetIdFor(order.id), cap: toRaw(BigInt(order.size), decimals), token };
2795
+ }
2796
+ function budgetRoom(budget, spent, decimals) {
2797
+ return spent >= budget.cap ? 0n : toScaled(budget.cap - spent, decimals);
2798
+ }
2789
2799
  function fromHuman(amount) {
2790
2800
  const text = amount.trim();
2791
2801
  if (!/^[0-9]+(\.[0-9]+)?$/.test(text)) {
@@ -2806,6 +2816,10 @@ function toScaled(amount, decimals) {
2806
2816
  const shift = 18 - decimals;
2807
2817
  return shift >= 0 ? amount * 10n ** BigInt(shift) : amount / 10n ** BigInt(-shift);
2808
2818
  }
2819
+ function toScaledUp(amount, decimals) {
2820
+ const shift = 18 - decimals;
2821
+ return shift >= 0 ? amount * 10n ** BigInt(shift) : divCeil(amount, 10n ** BigInt(-shift));
2822
+ }
2809
2823
  function divCeil(numerator, denominator) {
2810
2824
  return (numerator + denominator - 1n) / denominator;
2811
2825
  }
@@ -2847,6 +2861,87 @@ function inputFor(params) {
2847
2861
  return divCeil(scaled, unit) * unit;
2848
2862
  }
2849
2863
 
2864
+ // src/orderbook/matching.ts
2865
+ function limitOrderLegs(order) {
2866
+ return order.side === "BID" ? { input: order.base, output: order.quote } : { input: order.quote, output: order.base };
2867
+ }
2868
+ function matchLimitOrders(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
2869
+ const qualifying = orders.filter((order) => serves(order, incoming, resolve, now)).map((order) => {
2870
+ const offer = offerFor({
2871
+ side: order.side,
2872
+ inputAmount: incoming.inputNet,
2873
+ price: BigInt(order.price),
2874
+ outputDecimals: incoming.outputDecimals
2875
+ });
2876
+ const available = BigInt(order.remaining);
2877
+ return { order, offer, available, payout: offer < available ? offer : available };
2878
+ }).filter((candidate) => candidate.offer >= incoming.requestedOutput && candidate.payout > 0n).sort(byBestOfferFirst);
2879
+ return qualifying;
2880
+ }
2881
+ function byBestOfferFirst(a, b) {
2882
+ if (a.offer !== b.offer) return a.offer > b.offer ? -1 : 1;
2883
+ if (a.available !== b.available) return a.available > b.available ? -1 : 1;
2884
+ return a.order.id < b.order.id ? -1 : 1;
2885
+ }
2886
+ function whyUnmatched(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
2887
+ let furthest = -1;
2888
+ for (const order of orders) {
2889
+ const failed = firstFailed(order, incoming, resolve, now);
2890
+ furthest = Math.max(furthest, failed === null ? CHECKS.length : CHECKS.indexOf(failed));
2891
+ }
2892
+ const { source, destination, inputSymbol } = incoming;
2893
+ if (furthest < CHECKS.length) {
2894
+ switch (CHECKS[Math.max(furthest, 0)]) {
2895
+ case "open":
2896
+ return "no limit order is open";
2897
+ case "chain":
2898
+ return `no open limit order fills on ${destination}`;
2899
+ case "input":
2900
+ return `no limit order on ${destination} takes ${inputSymbol} in`;
2901
+ case "output":
2902
+ return `no limit order on ${destination} taking ${inputSymbol} in pays out the token this order asks for`;
2903
+ case "source":
2904
+ return `no limit order for this pair on ${destination} accepts swaps from ${source}`;
2905
+ }
2906
+ }
2907
+ const priced = orders.filter((order) => firstFailed(order, incoming, resolve, now) === null).map((order) => ({
2908
+ order,
2909
+ offer: offerFor({
2910
+ side: order.side,
2911
+ inputAmount: incoming.inputNet,
2912
+ price: BigInt(order.price),
2913
+ outputDecimals: incoming.outputDecimals
2914
+ })
2915
+ }));
2916
+ if (priced.some(({ offer }) => offer >= incoming.requestedOutput)) {
2917
+ return "the limit orders that meet its rate have nothing left to pay out";
2918
+ }
2919
+ const best = priced.reduce((a, b) => b.offer > a.offer ? b : a);
2920
+ const symbol = limitOrderLegs(best.order).output;
2921
+ return `it asks for ${readable(incoming.requestedOutput)} ${symbol}; the best limit order offers ${readable(best.offer)} ${symbol}`;
2922
+ }
2923
+ var CHECKS = ["open", "chain", "input", "output", "source"];
2924
+ function serves(order, incoming, resolve, now) {
2925
+ return firstFailed(order, incoming, resolve, now) === null;
2926
+ }
2927
+ function firstFailed(order, incoming, resolve, now) {
2928
+ if (order.status !== "open") return "open";
2929
+ if (order.expiresAt !== null && new Date(order.expiresAt) <= now) return "open";
2930
+ if (order.fillChain !== incoming.destination) return "chain";
2931
+ const legs = limitOrderLegs(order);
2932
+ if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return "input";
2933
+ const outputToken = resolve(legs.output, order.fillChain);
2934
+ if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return "output";
2935
+ if (incoming.source === incoming.destination) return null;
2936
+ return order.acceptedSources.includes(incoming.source) ? null : "source";
2937
+ }
2938
+ function readable(amount) {
2939
+ const [whole, fraction] = toHuman(amount).split(".");
2940
+ const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
2941
+ const cut = fraction?.slice(0, 6).replace(/0+$/, "");
2942
+ return cut ? `${grouped}.${cut}` : grouped;
2943
+ }
2944
+
2850
2945
  // src/orderbook/limit-orders.ts
2851
2946
  var LIMITS_TTL_MS = 5 * 60 * 1e3;
2852
2947
  var POSTABLE = ["open", "resizing"];
@@ -3361,6 +3456,58 @@ var LimitOrderService = class {
3361
3456
  }
3362
3457
  return report;
3363
3458
  }
3459
+ /**
3460
+ * Lowers every open order's `remaining` to what the solver's account will still
3461
+ * pay for it, and puts the order back on the book at that size.
3462
+ *
3463
+ * The account tallies each order's payouts on the fill chain and refuses a fill
3464
+ * that would take the tally past the order's size. A fill that never settled
3465
+ * here, or a store restored from an older copy, leaves `remaining` above that
3466
+ * room and the order posted at a size the account will refuse. A tally behind
3467
+ * the store changes nothing: `remaining` is never raised.
3468
+ *
3469
+ * Answers how many orders were lowered.
3470
+ */
3471
+ async reconcileTallies() {
3472
+ let lowered = 0;
3473
+ for (const order of await this.store.list({ status: "open" })) {
3474
+ try {
3475
+ const clamped = await this.clampToTally(order);
3476
+ if (!clamped) continue;
3477
+ lowered++;
3478
+ await this.resize(clamped, BigInt(order.remaining) - BigInt(clamped.remaining));
3479
+ } catch (err) {
3480
+ this.logger.error({ id: order.id, err }, "Could not bring the limit order in line with its on-chain tally");
3481
+ }
3482
+ }
3483
+ return lowered;
3484
+ }
3485
+ /** The order as the store now holds it when its tally lowered it, null when it was left alone. */
3486
+ async clampToTally(order) {
3487
+ const token = this.assetRegistry.getAddress(limitOrderLegs(order).output, order.fillChain);
3488
+ if (!token) return null;
3489
+ const decimals = await this.contractService.getTokenDecimals(token, order.fillChain);
3490
+ const budget = budgetFor(order, token, decimals);
3491
+ const spent = await this.contractService.limitOrderSpent(order.fillChain, budget.budgetId);
3492
+ if (spent === null) return null;
3493
+ const room = budgetRoom(budget, spent, decimals);
3494
+ const over = BigInt(order.remaining) - (room + BigInt(order.reserved));
3495
+ if (over < toScaled(1n, decimals)) return null;
3496
+ const clamped = await this.store.clampRemaining(order.id, room.toString());
3497
+ if (!clamped) return null;
3498
+ this.logger.warn(
3499
+ {
3500
+ id: order.id,
3501
+ before: order.remaining,
3502
+ after: clamped.remaining,
3503
+ spent: spent.toString(),
3504
+ cap: budget.cap.toString(),
3505
+ reserved: clamped.reserved
3506
+ },
3507
+ "Limit order had more left than its on-chain tally allows; lowering it"
3508
+ );
3509
+ return clamped;
3510
+ }
3364
3511
  /**
3365
3512
  * Withdraws every order that has outlived the operator's own `expiresAt`.
3366
3513
  *
@@ -3622,7 +3769,7 @@ async function patchRuntimeState(store, patch) {
3622
3769
  }
3623
3770
 
3624
3771
  // src/services/wallet/mpcvault.ts
3625
- import { concatHex, keccak256, padHex, toHex } from "viem";
3772
+ import { concatHex, keccak256 as keccak2562, padHex, toHex } from "viem";
3626
3773
  import * as grpc from "@grpc/grpc-js";
3627
3774
 
3628
3775
  // src/proto/mpcvault/platform/v1/api.ts
@@ -12235,7 +12382,7 @@ var MpcVaultService = class {
12235
12382
  const messageBytes = Buffer.from(messageHash.slice(2), "hex");
12236
12383
  const prefix = Buffer.from(`Ethereum Signed Message:
12237
12384
  ${messageBytes.length}`);
12238
- const ethSignedMessageHash = keccak256(concatHex([toHex(prefix), messageHash]));
12385
+ const ethSignedMessageHash = keccak2562(concatHex([toHex(prefix), messageHash]));
12239
12386
  const ethSignedBytes = Buffer.from(ethSignedMessageHash.slice(2), "hex");
12240
12387
  const uuid = await this.createSigningRequest({
12241
12388
  vaultUuid: this.vaultUuid,
@@ -12323,11 +12470,12 @@ var SignerType = /* @__PURE__ */ ((SignerType2) => {
12323
12470
  SignerType2["PrivateKey"] = "privateKey";
12324
12471
  SignerType2["MpcVault"] = "mpcVault";
12325
12472
  SignerType2["Turnkey"] = "turnkey";
12473
+ SignerType2["SecretPhrase"] = "secretPhrase";
12326
12474
  return SignerType2;
12327
12475
  })(SignerType || {});
12328
12476
 
12329
12477
  // src/services/wallet/accounts/mpc.ts
12330
- import { keccak256 as keccak2562, serializeTransaction } from "viem";
12478
+ import { keccak256 as keccak2563, serializeTransaction } from "viem";
12331
12479
  import { hashAuthorization } from "viem/utils";
12332
12480
  function mpcVaultSigner(config) {
12333
12481
  const service = new MpcVaultService({
@@ -12362,7 +12510,7 @@ function mpcVaultSigner(config) {
12362
12510
  if (tx.authorizationList?.length) {
12363
12511
  const serializable = { ...tx, type: "eip7702" };
12364
12512
  const signature = await service.signRawHashComponents(
12365
- keccak2562(serializeTransaction(serializable))
12513
+ keccak2563(serializeTransaction(serializable))
12366
12514
  );
12367
12515
  return serializeTransaction(serializable, signature);
12368
12516
  }
@@ -12444,6 +12592,51 @@ function privateKeySigner(privateKey) {
12444
12592
  return viemSigner(privateKeyToAccount(privateKey));
12445
12593
  }
12446
12594
 
12595
+ // src/services/wallet/accounts/secretphrase.ts
12596
+ import { validateMnemonic } from "@scure/bip39";
12597
+ import { english, mnemonicToAccount } from "viem/accounts";
12598
+ var WORD_COUNTS = [12, 15, 18, 21, 24];
12599
+ var MAX_DERIVATION_INDEX = 2 ** 31 - 1;
12600
+ function normaliseSecretPhrase(phrase) {
12601
+ return phrase.trim().split(/\s+/).join(" ").toLowerCase();
12602
+ }
12603
+ function validateSecretPhrase(phrase) {
12604
+ if (typeof phrase !== "string") throw new Error("Secret phrase must be a string");
12605
+ const normalised = normaliseSecretPhrase(phrase);
12606
+ const words = normalised === "" ? [] : normalised.split(" ");
12607
+ if (!WORD_COUNTS.includes(words.length)) {
12608
+ throw new Error(`Secret phrase must have ${WORD_COUNTS.join(", ")} words; got ${words.length}`);
12609
+ }
12610
+ if (words.some((word) => !english.includes(word))) {
12611
+ throw new Error("Secret phrase contains a word that is not in the BIP-39 English wordlist");
12612
+ }
12613
+ if (!validateMnemonic(normalised, english)) {
12614
+ throw new Error("Secret phrase checksum does not match; check the words and their order");
12615
+ }
12616
+ }
12617
+ function assertDerivationIndex(index, name = "Derivation index") {
12618
+ const expected = `${name} must be an integer between 0 and ${MAX_DERIVATION_INDEX}`;
12619
+ if (typeof index !== "number") throw new Error(expected);
12620
+ if (!Number.isInteger(index) || index < 0 || index > MAX_DERIVATION_INDEX) {
12621
+ throw new Error(`${expected}; got ${index}`);
12622
+ }
12623
+ }
12624
+ function secretPhraseSigner(config) {
12625
+ validateSecretPhrase(config.phrase);
12626
+ const phrase = normaliseSecretPhrase(config.phrase);
12627
+ const accountIndex = config.accountIndex ?? 0;
12628
+ assertDerivationIndex(accountIndex, "accountIndex");
12629
+ const derive = (index) => {
12630
+ assertDerivationIndex(index);
12631
+ return { ...viemSigner(mnemonicToAccount(phrase, { addressIndex: index })), mode: "secretPhrase" };
12632
+ };
12633
+ return { ...derive(accountIndex), accountIndex, derive };
12634
+ }
12635
+ function canDerive(signer) {
12636
+ const candidate = signer;
12637
+ return typeof candidate.derive === "function" && typeof candidate.accountIndex === "number";
12638
+ }
12639
+
12447
12640
  // src/services/wallet/accounts/turnkey.ts
12448
12641
  import { Turnkey } from "@turnkey/sdk-server";
12449
12642
  import { createAccount } from "@turnkey/viem";
@@ -12467,6 +12660,7 @@ async function createSigner(config) {
12467
12660
  if (config.type === "privateKey" /* PrivateKey */) return privateKeySigner(config.key);
12468
12661
  if (config.type === "mpcVault" /* MpcVault */) return mpcVaultSigner(config);
12469
12662
  if (config.type === "turnkey" /* Turnkey */) return turnkeySigner(config);
12663
+ if (config.type === "secretPhrase" /* SecretPhrase */) return secretPhraseSigner(config);
12470
12664
  throw new Error(`Unsupported signer mode: ${config.type ?? "unknown"}`);
12471
12665
  }
12472
12666
  function validateSignerConfig(config) {
@@ -12492,13 +12686,21 @@ function validateSignerConfig(config) {
12492
12686
  if (!config.signWith) throw new Error("simplex.signer.signWith is required");
12493
12687
  return;
12494
12688
  }
12689
+ if (config.type === "secretPhrase" /* SecretPhrase */) {
12690
+ if (!config.phrase) {
12691
+ throw new Error("simplex.signer.phrase is required when simplex.signer.type=secretPhrase");
12692
+ }
12693
+ validateSecretPhrase(config.phrase);
12694
+ if (config.accountIndex !== void 0) assertDerivationIndex(config.accountIndex, "simplex.signer.accountIndex");
12695
+ return;
12696
+ }
12495
12697
  throw new Error(`Unsupported signer mode: ${config.type ?? "unknown"}`);
12496
12698
  }
12497
12699
 
12498
12700
  // src/services/wallet/account.ts
12499
12701
  import {
12500
12702
  hashTypedData,
12501
- keccak256 as keccak2563,
12703
+ keccak256 as keccak2564,
12502
12704
  serializeSignature,
12503
12705
  serializeTransaction as serializeTransaction2
12504
12706
  } from "viem";
@@ -12555,7 +12757,7 @@ function digestSigner(opts) {
12555
12757
  ),
12556
12758
  signTransaction: async (tx) => {
12557
12759
  const serializable = tx;
12558
- const signature = await sign(keccak2563(serializeTransaction2(serializable)));
12760
+ const signature = await sign(keccak2564(serializeTransaction2(serializable)));
12559
12761
  return serializeTransaction2(serializable, signature);
12560
12762
  }
12561
12763
  };
@@ -12585,6 +12787,12 @@ async function deriveSubstrateKeyPair(key) {
12585
12787
  }
12586
12788
 
12587
12789
  // src/services/CacheService.ts
12790
+ function budgetToCache(budget) {
12791
+ return { budgetId: budget.budgetId, cap: budget.cap.toString(), token: budget.token };
12792
+ }
12793
+ function budgetFromCache(budget) {
12794
+ return { budgetId: budget.budgetId, cap: BigInt(budget.cap), token: budget.token };
12795
+ }
12588
12796
  var CacheService = class {
12589
12797
  constructor(loggers = defaultLoggerContext()) {
12590
12798
  this.CACHE_EXPIRY_MS = 1 * 60 * 1e3;
@@ -12742,7 +12950,24 @@ var CacheService = class {
12742
12950
  return null;
12743
12951
  }
12744
12952
  }
12745
- setFillerOutputs(orderId, outputs, inputs = []) {
12953
+ /**
12954
+ * The budget of the limit order that priced the outputs cached for this order, or
12955
+ * null when no limit order did.
12956
+ *
12957
+ * Kept on the same entry as the outputs, so the two are replaced and expire
12958
+ * together: a bid can never sign one limit order's payout beside another's budget.
12959
+ */
12960
+ getBidBudget(orderId) {
12961
+ try {
12962
+ const cache = this.cacheData.fillerOutputs[orderId];
12963
+ if (cache?.budget && this.isCacheValid(cache.timestamp)) return budgetFromCache(cache.budget);
12964
+ return null;
12965
+ } catch (error) {
12966
+ this.logger.error({ err: error }, "Error getting bid budget");
12967
+ return null;
12968
+ }
12969
+ }
12970
+ setFillerOutputs(orderId, outputs, inputs = [], budget) {
12746
12971
  try {
12747
12972
  this.cleanupStaleData();
12748
12973
  this.cacheData.fillerOutputs[orderId] = {
@@ -12754,6 +12979,7 @@ var CacheService = class {
12754
12979
  token: o.token,
12755
12980
  amount: o.amount.toString()
12756
12981
  })),
12982
+ ...budget ? { budget: budgetToCache(budget) } : {},
12757
12983
  timestamp: Date.now()
12758
12984
  };
12759
12985
  } catch (error) {
@@ -12799,7 +13025,8 @@ var CacheService = class {
12799
13025
  data: call.data
12800
13026
  })),
12801
13027
  partialFill: plan.partialFill,
12802
- profit: plan.profit
13028
+ profit: plan.profit,
13029
+ ...plan.budget ? { budget: budgetFromCache(plan.budget) } : {}
12803
13030
  }));
12804
13031
  } catch (error) {
12805
13032
  this.logger.error({ err: error }, "Error getting bid plans");
@@ -12828,7 +13055,8 @@ var CacheService = class {
12828
13055
  data: call.data
12829
13056
  })),
12830
13057
  partialFill: plan.partialFill,
12831
- profit: plan.profit
13058
+ profit: plan.profit,
13059
+ ...plan.budget ? { budget: budgetToCache(plan.budget) } : {}
12832
13060
  })),
12833
13061
  timestamp: Date.now()
12834
13062
  };
@@ -13661,6 +13889,44 @@ import {
13661
13889
  bytes20ToBytes32
13662
13890
  } from "@hyperbridge/sdk";
13663
13891
 
13892
+ // src/config/abis/SolverAccount.ts
13893
+ var SOLVER_ACCOUNT_ABI = [
13894
+ {
13895
+ inputs: [
13896
+ { name: "orderId", type: "bytes32" },
13897
+ { name: "cap", type: "uint256" },
13898
+ { name: "token", type: "address" },
13899
+ { name: "approved", type: "uint256" },
13900
+ { name: "fee", type: "uint256" }
13901
+ ],
13902
+ name: "debitOrder",
13903
+ outputs: [],
13904
+ stateMutability: "nonpayable",
13905
+ type: "function"
13906
+ },
13907
+ {
13908
+ inputs: [{ name: "orderId", type: "bytes32" }],
13909
+ name: "spent",
13910
+ outputs: [{ name: "", type: "uint256" }],
13911
+ stateMutability: "view",
13912
+ type: "function"
13913
+ },
13914
+ {
13915
+ inputs: [{ name: "sender", type: "address" }],
13916
+ name: "AccountUnauthorized",
13917
+ type: "error"
13918
+ },
13919
+ {
13920
+ inputs: [
13921
+ { name: "orderId", type: "bytes32" },
13922
+ { name: "total", type: "uint256" },
13923
+ { name: "cap", type: "uint256" }
13924
+ ],
13925
+ name: "LimitOrderExceeded",
13926
+ type: "error"
13927
+ }
13928
+ ];
13929
+
13664
13930
  // src/config/abis/EvmHost.ts
13665
13931
  var EVM_HOST = [
13666
13932
  {
@@ -18054,6 +18320,10 @@ function describeShortfall(balances) {
18054
18320
  // src/services/ContractInteractionService.ts
18055
18321
  Decimal2.config({ precision: 28, rounding: 4 });
18056
18322
  var FUNDING_GAS_PER_CALL = 400000n;
18323
+ var DEBIT_ORDER_GAS = 60000n;
18324
+ function settles(budget) {
18325
+ return !!budget && budget.token.toLowerCase() !== ADDRESS_ZERO;
18326
+ }
18057
18327
  var LIMIT_ORDER_CALL_GAS_LIMIT = 500000n;
18058
18328
  var LIMIT_ORDER_VERIFICATION_GAS_LIMIT = 150000n;
18059
18329
  var LIMIT_ORDER_PRE_VERIFICATION_GAS = 50000n;
@@ -18243,17 +18513,19 @@ var ContractInteractionService = class {
18243
18513
  }
18244
18514
  /**
18245
18515
  * The call gas a bid needs: the shared estimate plus room for the funding calls
18246
- * this bid prepends.
18516
+ * this bid prepends and for settling its limit order's budget.
18247
18517
  *
18248
18518
  * Funding calls are not simulated (the V4 PositionManager's flash accounting does
18249
18519
  * not resolve in the bundler's estimation context), so each one adds a fixed
18250
18520
  * allowance for its modifyLiquidities + take. They differ between the bids on one
18251
18521
  * order, since each draws on its own limit order and so on its own funding, which
18252
18522
  * is why this is worked out per bid rather than baked into the cached estimate.
18523
+ * The estimate is of a lone fillOrder, so the budget call is not in it either.
18253
18524
  */
18254
- callGasLimitFor(order, baseCallGasLimit) {
18525
+ callGasLimitFor(order, baseCallGasLimit, budget) {
18255
18526
  const funding = order.id ? this.cacheService.getFundingPrepends(order.id) : null;
18256
- return baseCallGasLimit + FUNDING_GAS_PER_CALL * BigInt(funding?.calls?.length ?? 0);
18527
+ const fundingGas = FUNDING_GAS_PER_CALL * BigInt(funding?.calls?.length ?? 0);
18528
+ return baseCallGasLimit + fundingGas + (settles(budget) ? DEBIT_ORDER_GAS : 0n);
18257
18529
  }
18258
18530
  /**
18259
18531
  * Gets the fee token address and decimals for a given chain.
@@ -18435,6 +18707,27 @@ var ContractInteractionService = class {
18435
18707
  { maxRetries: 3, backoffMs: 250, logMessage: "Failed to read token balance" }
18436
18708
  );
18437
18709
  }
18710
+ /**
18711
+ * What the solver's account has tallied against a limit order's budget on `chain`, in
18712
+ * the paid token's own units, or null when it cannot be read.
18713
+ *
18714
+ * Null rather than a throw: an account whose implementation keeps no tally answers
18715
+ * with a revert or with no data, and the caller carries on without the figure either
18716
+ * way. Not retried, since that answer does not change on a second ask.
18717
+ */
18718
+ async limitOrderSpent(chain, budgetId) {
18719
+ try {
18720
+ return await this.clientManager.getPublicClient(chain).readContract({
18721
+ address: this.solverAccountAddress,
18722
+ abi: SOLVER_ACCOUNT_ABI,
18723
+ functionName: "spent",
18724
+ args: [budgetId]
18725
+ });
18726
+ } catch (err) {
18727
+ this.logger.warn({ err, chain, budgetId }, "Could not read the limit order's tally from the solver account");
18728
+ return null;
18729
+ }
18730
+ }
18438
18731
  async getSolverEntryPointBalance(chain) {
18439
18732
  const entryPointAddress = this.configService.getEntryPointAddress(chain);
18440
18733
  if (!entryPointAddress) {
@@ -18575,7 +18868,8 @@ var ContractInteractionService = class {
18575
18868
  if (!cachedEstimate) {
18576
18869
  throw new Error(`No cached gas estimate found for order ${order.id}. Call estimateGasFillPost first.`);
18577
18870
  }
18578
- const callGasLimit = this.callGasLimitFor(order, cachedEstimate.callGasLimit);
18871
+ const budget = this.cacheService.getBidBudget(order.id);
18872
+ const callGasLimit = this.callGasLimitFor(order, cachedEstimate.callGasLimit, budget);
18579
18873
  const cachedFillerOutputs = this.cacheService.getFillerOutputs(order.id);
18580
18874
  const cachedFillerInputs = this.cacheService.getFillerInputs(order.id);
18581
18875
  if (!cachedFillerOutputs || !cachedFillerInputs) {
@@ -18602,7 +18896,8 @@ var ContractInteractionService = class {
18602
18896
  order,
18603
18897
  cachedFillerOutputs,
18604
18898
  fillOptions,
18605
- cachedEstimate.totalCostInSourceFeeToken + dispatchFeeTokenAmount
18899
+ cachedEstimate.totalCostInSourceFeeToken + dispatchFeeTokenAmount,
18900
+ budget ? { budget, solverAccount: solverAccountAddress } : void 0
18606
18901
  );
18607
18902
  const commitment = orderCommitment2(order);
18608
18903
  const nonce = await this.clientManager.getPublicClient(order.destination).readContract({
@@ -18630,20 +18925,32 @@ var ContractInteractionService = class {
18630
18925
  } else {
18631
18926
  this.logger.warn({ reason: pmResult.reason }, "No paymaster for bid UserOp; relying on EntryPoint deposit");
18632
18927
  }
18633
- const userOp = await sdkHelper.prepareSubmitBid({
18634
- order,
18635
- fillOptions,
18928
+ const bidGas = {
18636
18929
  solverAccount: solverAccountAddress,
18637
- solverSigner: sdkSigningAccount(this.signer),
18638
18930
  nonce,
18639
18931
  entryPointAddress,
18640
18932
  callGasLimit,
18641
18933
  verificationGasLimit: cachedEstimate.verificationGasLimit,
18642
- preVerificationGas: cachedEstimate.preVerificationGas,
18643
18934
  maxFeePerGas: cachedEstimate.maxFeePerGas,
18644
18935
  maxPriorityFeePerGas: cachedEstimate.maxPriorityFeePerGas,
18645
18936
  callData,
18646
18937
  paymasterAndData
18938
+ };
18939
+ let preVerificationGas = cachedEstimate.preVerificationGas;
18940
+ try {
18941
+ preVerificationGas = await sdkHelper.estimateBidPreVerificationGas(bidGas);
18942
+ } catch (err) {
18943
+ this.logger.warn(
18944
+ { orderId: order.id, err, preVerificationGas: preVerificationGas.toString() },
18945
+ "Could not estimate the bid's preVerificationGas; using the fill estimate's"
18946
+ );
18947
+ }
18948
+ const userOp = await sdkHelper.prepareSubmitBid({
18949
+ ...bidGas,
18950
+ order,
18951
+ fillOptions,
18952
+ solverSigner: sdkSigningAccount(this.signer),
18953
+ preVerificationGas
18647
18954
  });
18648
18955
  const encodedUserOp = encodeUserOpScale(userOp);
18649
18956
  this.logger.info(
@@ -18652,6 +18959,7 @@ var ContractInteractionService = class {
18652
18959
  solverAccount: solverAccountAddress,
18653
18960
  nonce: nonce.toString(),
18654
18961
  callGasLimit: callGasLimit.toString(),
18962
+ preVerificationGas: preVerificationGas.toString(),
18655
18963
  maxFeePerGas: cachedEstimate.maxFeePerGas.toString()
18656
18964
  },
18657
18965
  "Prepared bid UserOp"
@@ -18781,8 +19089,13 @@ var ContractInteractionService = class {
18781
19089
  * Same-chain fills release escrow locally with no Hyperbridge dispatch, so the
18782
19090
  * gateway never pulls the fee token — its approval is skipped. Only cross-chain
18783
19091
  * fills, which dispatch a RedeemEscrow message paid in the fee token, need it.
19092
+ *
19093
+ * A bid priced by a limit order ends with `debitOrder`, which takes the budget's id
19094
+ * as its `orderId`, works out what the fill paid from what is left of the allowance
19095
+ * and reverts the whole batch if the order's payouts would pass its size. It has to
19096
+ * come last, after the gateway has drawn on the allowance.
18784
19097
  */
18785
- async buildApprovalAndFillCalldata(order, fillerOutputs, fillOptions, requiredFeeTokenAmount) {
19098
+ async buildApprovalAndFillCalldata(order, fillerOutputs, fillOptions, requiredFeeTokenAmount, settlement) {
18786
19099
  const chain = order.destination;
18787
19100
  const intentGatewayV2Address = this.configService.getIntentGatewayAddress(chain);
18788
19101
  const perTokenRequired = /* @__PURE__ */ new Map();
@@ -18792,9 +19105,10 @@ var ContractInteractionService = class {
18792
19105
  const key = addr.toLowerCase();
18793
19106
  perTokenRequired.set(key, (perTokenRequired.get(key) ?? 0n) + output.amount);
18794
19107
  }
19108
+ let feeKey;
18795
19109
  if (order.source !== order.destination) {
18796
19110
  const feeToken = await this.getFeeTokenWithDecimals(chain);
18797
- const feeKey = feeToken.address.toLowerCase();
19111
+ feeKey = feeToken.address.toLowerCase();
18798
19112
  perTokenRequired.set(feeKey, (perTokenRequired.get(feeKey) ?? 0n) + requiredFeeTokenAmount);
18799
19113
  }
18800
19114
  const fundingPrepends = order.id ? this.cacheService.getFundingPrepends(order.id) : null;
@@ -18820,6 +19134,26 @@ var ContractInteractionService = class {
18820
19134
  value: nativeOutputValue,
18821
19135
  data: encodeFillOrder(transformOrderForContract(order), fillOptions)
18822
19136
  });
19137
+ if (settlement && settles(settlement.budget)) {
19138
+ const { budget, solverAccount } = settlement;
19139
+ const budgetToken = budget.token.toLowerCase();
19140
+ const approved = perTokenRequired.get(budgetToken) ?? 0n;
19141
+ if (approved === 0n) {
19142
+ throw new Error(
19143
+ `Bid on order ${order.id} approves nothing in ${budget.token}, the token budget ${budget.budgetId} is measured in`
19144
+ );
19145
+ }
19146
+ const fee = feeKey === budgetToken ? fillOptions.relayerFee : 0n;
19147
+ calls.push({
19148
+ target: solverAccount,
19149
+ value: 0n,
19150
+ data: encodeFunctionData2({
19151
+ abi: SOLVER_ACCOUNT_ABI,
19152
+ functionName: "debitOrder",
19153
+ args: [budget.budgetId, budget.cap, budgetToken, approved, fee]
19154
+ })
19155
+ });
19156
+ }
18823
19157
  return encodeERC7821ExecuteBatch2(calls);
18824
19158
  }
18825
19159
  };
@@ -20672,11 +21006,11 @@ function byBestRate(a, b) {
20672
21006
  return left === right ? 0 : left > right ? -1 : 1;
20673
21007
  }
20674
21008
  function chargedFor(hold, delivered, released) {
20675
- if (hold.take === void 0 || released === 0n) return delivered;
20676
- const take = BigInt(hold.take);
20677
- if (take === 0n) return delivered;
20678
- const atRate = (BigInt(hold.amount) * released + take - 1n) / take;
20679
- return atRate > delivered ? atRate : delivered;
21009
+ const take = hold.take === void 0 ? 0n : BigInt(hold.take);
21010
+ if (take === 0n || released === 0n) return toScaledUp(delivered.raw, delivered.decimals);
21011
+ const offered = toRaw(BigInt(hold.amount), delivered.decimals);
21012
+ const atRate = (offered * released + take - 1n) / take;
21013
+ return toScaledUp(atRate > delivered.raw ? atRate : delivered.raw, delivered.decimals);
20680
21014
  }
20681
21015
  var IntentFiller = class {
20682
21016
  constructor(chainConfigs, strategies, config, configService, chainClientManager, contractService, signer, scanners, rebalancingService, bidStorage, limitOrders, assetRegistry) {
@@ -20753,6 +21087,17 @@ var IntentFiller = class {
20753
21087
  setLimitOrderService(service) {
20754
21088
  this.limitOrderService = service;
20755
21089
  }
21090
+ /**
21091
+ * Brings the limit orders' `remaining` in line with their on-chain tallies.
21092
+ *
21093
+ * On the settlement queue, so the pass and a fill never resize the same order at
21094
+ * once. Resolves when the pass has run.
21095
+ */
21096
+ async reconcileLimitOrderTallies() {
21097
+ await this.settlementQueue.add(async () => {
21098
+ await this.limitOrderService?.reconcileTallies();
21099
+ });
21100
+ }
20756
21101
  /** Whether the given chain id is configured for watch-only (monitor, never fill). */
20757
21102
  isChainWatchOnly(chainId) {
20758
21103
  const watchOnly = this.config.watchOnly;
@@ -21398,7 +21743,7 @@ var IntentFiller = class {
21398
21743
  let reservation = [];
21399
21744
  if (plan) {
21400
21745
  if (order.id) {
21401
- cache.setFillerOutputs(order.id, plan.fillerOutputs, plan.fillerInputs);
21746
+ cache.setFillerOutputs(order.id, plan.fillerOutputs, plan.fillerInputs, plan.budget);
21402
21747
  cache.setPartialFill(order.id, plan.partialFill);
21403
21748
  if (plan.fundingCalls.length > 0) cache.setFundingPrepends(order.id, plan.fundingCalls);
21404
21749
  else cache.clearFundingPrepends(order.id);
@@ -21599,7 +21944,7 @@ var IntentFiller = class {
21599
21944
  await this.releaseAll(claimed);
21600
21945
  return;
21601
21946
  }
21602
- const settled = executedHold(claimed, delivered, released);
21947
+ const settled = executedHold(claimed, delivered.scaled, released);
21603
21948
  const charged = chargedFor(settled, delivered, released);
21604
21949
  const settledBid = rows.find(
21605
21950
  (row) => row.reservations.some(
@@ -21652,15 +21997,15 @@ var IntentFiller = class {
21652
21997
  if (delivered === null) return [];
21653
21998
  const settled = executedHold(
21654
21999
  holds.map((entry) => entry.hold),
21655
- delivered,
22000
+ delivered.scaled,
21656
22001
  released
21657
22002
  );
21658
22003
  const row = holds.find((entry) => entry.hold === settled).row;
21659
22004
  return this.bidStorage.claimReservation(commitment, row.bid ?? void 0);
21660
22005
  }
21661
22006
  /**
21662
- * What this fill delivered of the token the limit order pays, at 1e18, or null
21663
- * when the fill names none of it.
22007
+ * What this fill delivered of the token the limit order pays, or null when the
22008
+ * fill names none of it.
21664
22009
  */
21665
22010
  async deliveredAgainst(limitOrderId, chainId, outputs) {
21666
22011
  const order = await this.limitOrders?.get(limitOrderId);
@@ -21676,7 +22021,7 @@ var IntentFiller = class {
21676
22021
  }
21677
22022
  if (total === 0n) return null;
21678
22023
  const decimals = await this.contractService.getTokenDecimals(address, chain);
21679
- return toScaled(total, decimals);
22024
+ return { scaled: toScaled(total, decimals), raw: total, decimals };
21680
22025
  }
21681
22026
  /**
21682
22027
  * Gives back whatever this bid still holds against its limit order.
@@ -21772,91 +22117,6 @@ import {
21772
22117
  import { formatUnits as formatUnits4 } from "viem";
21773
22118
  import { Decimal as Decimal6 } from "decimal.js";
21774
22119
 
21775
- // src/orderbook/matching.ts
21776
- function limitOrderLegs(order) {
21777
- return order.side === "BID" ? { input: order.base, output: order.quote } : { input: order.quote, output: order.base };
21778
- }
21779
- function availableOn(order) {
21780
- const available = BigInt(order.remaining) - BigInt(order.reserved);
21781
- return available > 0n ? available : 0n;
21782
- }
21783
- function matchLimitOrders(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
21784
- const qualifying = orders.filter((order) => serves(order, incoming, resolve, now)).map((order) => {
21785
- const offer = offerFor({
21786
- side: order.side,
21787
- inputAmount: incoming.inputNet,
21788
- price: BigInt(order.price),
21789
- outputDecimals: incoming.outputDecimals
21790
- });
21791
- const available = availableOn(order);
21792
- return { order, offer, available, payout: offer < available ? offer : available };
21793
- }).filter((candidate) => candidate.offer >= incoming.requestedOutput && candidate.payout > 0n).sort(byBestOfferFirst);
21794
- return qualifying;
21795
- }
21796
- function byBestOfferFirst(a, b) {
21797
- if (a.offer !== b.offer) return a.offer > b.offer ? -1 : 1;
21798
- if (a.available !== b.available) return a.available > b.available ? -1 : 1;
21799
- return a.order.id < b.order.id ? -1 : 1;
21800
- }
21801
- function whyUnmatched(orders, incoming, resolve, now = /* @__PURE__ */ new Date()) {
21802
- let furthest = -1;
21803
- for (const order of orders) {
21804
- const failed = firstFailed(order, incoming, resolve, now);
21805
- furthest = Math.max(furthest, failed === null ? CHECKS.length : CHECKS.indexOf(failed));
21806
- }
21807
- const { source, destination, inputSymbol } = incoming;
21808
- if (furthest < CHECKS.length) {
21809
- switch (CHECKS[Math.max(furthest, 0)]) {
21810
- case "open":
21811
- return "no limit order is open";
21812
- case "chain":
21813
- return `no open limit order fills on ${destination}`;
21814
- case "input":
21815
- return `no limit order on ${destination} takes ${inputSymbol} in`;
21816
- case "output":
21817
- return `no limit order on ${destination} taking ${inputSymbol} in pays out the token this order asks for`;
21818
- case "source":
21819
- return `no limit order for this pair on ${destination} accepts swaps from ${source}`;
21820
- }
21821
- }
21822
- const priced = orders.filter((order) => firstFailed(order, incoming, resolve, now) === null).map((order) => ({
21823
- order,
21824
- offer: offerFor({
21825
- side: order.side,
21826
- inputAmount: incoming.inputNet,
21827
- price: BigInt(order.price),
21828
- outputDecimals: incoming.outputDecimals
21829
- })
21830
- }));
21831
- if (priced.some(({ offer }) => offer >= incoming.requestedOutput)) {
21832
- return "the limit orders that meet its rate have nothing left to pay out";
21833
- }
21834
- const best = priced.reduce((a, b) => b.offer > a.offer ? b : a);
21835
- const symbol = limitOrderLegs(best.order).output;
21836
- return `it asks for ${readable(incoming.requestedOutput)} ${symbol}; the best limit order offers ${readable(best.offer)} ${symbol}`;
21837
- }
21838
- var CHECKS = ["open", "chain", "input", "output", "source"];
21839
- function serves(order, incoming, resolve, now) {
21840
- return firstFailed(order, incoming, resolve, now) === null;
21841
- }
21842
- function firstFailed(order, incoming, resolve, now) {
21843
- if (order.status !== "open") return "open";
21844
- if (order.expiresAt !== null && new Date(order.expiresAt) <= now) return "open";
21845
- if (order.fillChain !== incoming.destination) return "chain";
21846
- const legs = limitOrderLegs(order);
21847
- if (normalizeSymbol(legs.input) !== normalizeSymbol(incoming.inputSymbol)) return "input";
21848
- const outputToken = resolve(legs.output, order.fillChain);
21849
- if (!outputToken || outputToken.toLowerCase() !== incoming.outputToken.toLowerCase()) return "output";
21850
- if (incoming.source === incoming.destination) return null;
21851
- return order.acceptedSources.includes(incoming.source) ? null : "source";
21852
- }
21853
- function readable(amount) {
21854
- const [whole, fraction] = toHuman(amount).split(".");
21855
- const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
21856
- const cut = fraction?.slice(0, 6).replace(/0+$/, "");
21857
- return cut ? `${grouped}.${cut}` : grouped;
21858
- }
21859
-
21860
22120
  // src/orderbook/usd.ts
21861
22121
  import { Decimal as Decimal5 } from "decimal.js";
21862
22122
  function limitOrderUsdEdges(orders) {
@@ -21911,9 +22171,13 @@ var FXFiller = class _FXFiller {
21911
22171
  */
21912
22172
  constructor(signer, configService, clientManager, contractService, pairs, registry, options) {
21913
22173
  this.name = "FXFiller";
21914
- /** Consecutive orders where overfill clamp activated. */
22174
+ /**
22175
+ * Consecutive orders where the overfill clamp activated. Dormant: the clamp
22176
+ * existed for venue-priced legs, which went with Uniswap V4 pricing, so every
22177
+ * outcome is now recorded unclamped and this never climbs. See `recordOrderOutcome`.
22178
+ */
21915
22179
  this.consecutiveClamps = 0;
21916
- /** Once set, the filler refuses all orders until restart — systemic pricing error suspected. */
22180
+ /** Once set, the filler refuses all orders until reset. Unreachable while the clamp is dormant. */
21917
22181
  this.halted = false;
21918
22182
  this.logger = moduleLogger(configService.loggers, "fx-simplex");
21919
22183
  const { confirmationPolicy, fundingVenues = [], limitOrders } = options ?? {};
@@ -21962,10 +22226,9 @@ var FXFiller = class _FXFiller {
21962
22226
  this.pairs.push(pair);
21963
22227
  }
21964
22228
  /**
21965
- * Removes a market from the running engine. All-or-nothing: at least one
21966
- * market must remain and no other pair may lose its USD anchor (removing a
21967
- * reference feed that anchors a dependent market is rejected). In-flight
21968
- * rechecks of orders on the removed pair simply stop matching.
22229
+ * Removes a market from the running engine. At least one market must remain.
22230
+ * Matching reads the limit orders rather than the pairs, so removing a market
22231
+ * changes what the dashboard lists, not what the filler fills.
21969
22232
  */
21970
22233
  removePair(pair) {
21971
22234
  const index = this.pairs.indexOf(pair);
@@ -22021,22 +22284,20 @@ var FXFiller = class _FXFiller {
22021
22284
  }
22022
22285
  }
22023
22286
  /**
22024
- * Evaluates whether an order is profitable to fill under the per-pair
22025
- * `maxOrderSize` caps (where set) and the filler's current token balances.
22287
+ * Evaluates whether an order is profitable to fill against the operator's
22288
+ * limit orders and the filler's current token balances.
22026
22289
  *
22027
22290
  * High-level flow:
22028
- * - Resolve each (input, output) leg to a configured pair and direction.
22029
- * - Estimate each pair's total token0 notional in the order and cap it at
22030
- * the pair's `maxOrderSize` when one is set; pair curves are evaluated at
22031
- * that (possibly uncapped) notional.
22032
- * - Walk the legs, allocating from each pair's capped token0 budget and
22033
- * pricing outputs at the pair's rate.
22034
- * - Further cap each leg by the filler's current token balance plus
22035
- * funding-venue withdrawals.
22036
- * - Cache the resulting outputs for later use in `executeOrder`.
22037
- *
22038
- * Note: we may intentionally overfill relative to the user's requested
22039
- * outputs if the pair pricing makes that attractive. This is how we stay competitive.
22291
+ * - Match each (input, output) leg against the open limit orders.
22292
+ * - Build one bid per matching order, best offer first, each paying the
22293
+ * order's whole offer for the input at its own rate, capped by what the
22294
+ * order has left. The gateway credits the swapper the ask and splits
22295
+ * anything above it between the swapper and the protocol.
22296
+ * - Treat a payout below the ask as a partial fill, which only orders
22297
+ * without output calldata and without an earlier partial fill allow.
22298
+ * - Cap each payout by the filler's balance plus funding-venue withdrawals,
22299
+ * then gate it on fees (full fills) and the same-asset spread.
22300
+ * - Cache the resulting bids for later use in `executeOrder`.
22040
22301
  */
22041
22302
  async calculateProfitability(order) {
22042
22303
  if (order.id) this.contractService.cacheService.clearPartialFill(order.id);
@@ -22054,6 +22315,7 @@ var FXFiller = class _FXFiller {
22054
22315
  const multiLeg = order.inputs.length > 1;
22055
22316
  const plans = [];
22056
22317
  const plannedOn = /* @__PURE__ */ new Map();
22318
+ const spentOn = /* @__PURE__ */ new Map();
22057
22319
  for (let leg = 0; leg < order.inputs.length; leg++) {
22058
22320
  const input = order.inputs[leg];
22059
22321
  const output = order.output.assets[leg];
@@ -22075,7 +22337,30 @@ var FXFiller = class _FXFiller {
22075
22337
  for (const candidate of matches) {
22076
22338
  let partialFill = false;
22077
22339
  const fundingCalls = [];
22078
- const left = candidate.available - (plannedOn.get(candidate.order.id) ?? 0n);
22340
+ const budget = budgetFor(candidate.order, outputToken, outputDecimals);
22341
+ if (!spentOn.has(candidate.order.id)) {
22342
+ spentOn.set(
22343
+ candidate.order.id,
22344
+ await this.contractService.limitOrderSpent(destChain, budget.budgetId)
22345
+ );
22346
+ }
22347
+ const spent = spentOn.get(candidate.order.id) ?? null;
22348
+ const room = spent === null ? candidate.available : budgetRoom(budget, spent, outputDecimals);
22349
+ if (room === 0n) {
22350
+ this.logger.info(
22351
+ {
22352
+ orderId: order.id,
22353
+ leg,
22354
+ limitOrder: candidate.order.id,
22355
+ spent: spent?.toString(),
22356
+ cap: budget.cap.toString()
22357
+ },
22358
+ "Skipping a bid: the limit order has nothing left on chain"
22359
+ );
22360
+ continue;
22361
+ }
22362
+ const available = room < candidate.available ? room : candidate.available;
22363
+ const left = available - (plannedOn.get(candidate.order.id) ?? 0n);
22079
22364
  if (left <= 0n) {
22080
22365
  this.logger.info(
22081
22366
  { orderId: order.id, leg, limitOrder: candidate.order.id },
@@ -22110,10 +22395,7 @@ var FXFiller = class _FXFiller {
22110
22395
  {
22111
22396
  orderId: order.id,
22112
22397
  limitOrder: candidate.order.id,
22113
- available: formatUnits4(
22114
- candidate.available,
22115
- 18
22116
- ),
22398
+ available: formatUnits4(available, 18),
22117
22399
  userRequested: output.amount.toString(),
22118
22400
  payout: targetOutput.toString(),
22119
22401
  crossChain: sourceChain !== destChain
@@ -22158,7 +22440,10 @@ var FXFiller = class _FXFiller {
22158
22440
  }
22159
22441
  }
22160
22442
  const effectiveBalance = walletContribution + credited;
22161
- const finalOutputAmount = effectiveBalance > targetOutput ? targetOutput : effectiveBalance;
22443
+ const finalOutputAmount = toRaw(
22444
+ toScaled(effectiveBalance > targetOutput ? targetOutput : effectiveBalance, outputDecimals),
22445
+ outputDecimals
22446
+ );
22162
22447
  if (finalOutputAmount === 0n) {
22163
22448
  this.logger.info(
22164
22449
  {
@@ -22334,7 +22619,8 @@ var FXFiller = class _FXFiller {
22334
22619
  fillerInputs,
22335
22620
  fundingCalls: [...fundingCalls],
22336
22621
  partialFill,
22337
- profit: totalProfit
22622
+ profit: totalProfit,
22623
+ budget
22338
22624
  });
22339
22625
  if (!partialEligibleCheap) break;
22340
22626
  }
@@ -22342,7 +22628,12 @@ var FXFiller = class _FXFiller {
22342
22628
  if (plans.length === 0) return 0;
22343
22629
  if (order.id) {
22344
22630
  this.contractService.cacheService.setBidPlans(order.id, plans);
22345
- this.contractService.cacheService.setFillerOutputs(order.id, plans[0].fillerOutputs, plans[0].fillerInputs);
22631
+ this.contractService.cacheService.setFillerOutputs(
22632
+ order.id,
22633
+ plans[0].fillerOutputs,
22634
+ plans[0].fillerInputs,
22635
+ plans[0].budget
22636
+ );
22346
22637
  this.contractService.cacheService.setPartialFill(order.id, plans[0].partialFill);
22347
22638
  this.contractService.cacheService.setMatchedLimitOrder(order.id, [
22348
22639
  { limitOrderId: plans[0].limitOrderId, payout: plans[0].payout }
@@ -22437,10 +22728,14 @@ var FXFiller = class _FXFiller {
22437
22728
  // =========================================================================
22438
22729
  /**
22439
22730
  * Update consecutive-clamp counter after a successful order evaluation.
22440
- * Only venue-priced legs feed this counter (see clamp site) — a streak of those
22441
- * is the signal that a live market source has gone off (stale pool, manipulated
22442
- * venue) and the filler should stop until an operator investigates. Offline
22443
- * price-curve clamps warn but never reach here.
22731
+ *
22732
+ * Dormant. Only venue-priced legs ever fed a clamped outcome here: a streak
22733
+ * of those meant a live market source had gone off (stale pool, manipulated
22734
+ * venue). Venue pricing went with Uniswap V4, and limit orders are prices the
22735
+ * operator set, so the only caller records `false` and the filler never
22736
+ * halts. `isHalted`, `resetHalt` and `[simplex.overfillProtection]
22737
+ * maxConsecutiveClamps` are kept so existing configs and the halt API still
22738
+ * load; they can go once nothing reads them.
22444
22739
  */
22445
22740
  recordOrderOutcome(clamped, orderId) {
22446
22741
  if (clamped) {
@@ -22466,18 +22761,6 @@ var FXFiller = class _FXFiller {
22466
22761
  this.consecutiveClamps = 0;
22467
22762
  this.logger.warn("FXFiller halt reset by operator \u2014 resuming order evaluation");
22468
22763
  }
22469
- /**
22470
- * Given a single (input, output) leg and the remaining token0 budget of its
22471
- * pair, computes how much token0 notional to allocate to this leg and the
22472
- * corresponding maximum output amount at the pair's rate.
22473
- *
22474
- * `rate` is **token1 per 1 token0**:
22475
- * - token0 input → token1 output: token0 × rate → token1 amount.
22476
- * - token1 input → token0 output: token1 ÷ rate → token0 amount.
22477
- *
22478
- * Returns `null` when this leg cannot consume any of the pair's remaining
22479
- * budget (e.g. the cap has already been exhausted).
22480
- */
22481
22764
  /**
22482
22765
  * Whether any solver has already delivered output against this order.
22483
22766
  *
@@ -22815,9 +23098,9 @@ var LimitOrderLifecycle = class {
22815
23098
  const reconcileMs = this.options.reconcileIntervalSecs * 1e3;
22816
23099
  this.every(heartbeatMs, "heartbeat", () => this.service.heartbeat());
22817
23100
  this.every(EXPIRY_SWEEP_MS, "expiry", () => this.expire());
22818
- this.every(reconcileMs, "reconciliation", () => this.reconcile());
23101
+ this.every(reconcileMs, "reconciliation", () => this.upkeep());
22819
23102
  this.logger.info({ heartbeatMs, expiryMs: EXPIRY_SWEEP_MS, reconcileMs }, "Orderbook lifecycle started");
22820
- void this.run("reconciliation", () => this.reconcile());
23103
+ void this.upkeep();
22821
23104
  }
22822
23105
  stop() {
22823
23106
  this.timers.forEach((timer) => clearInterval(timer));
@@ -22828,6 +23111,14 @@ var LimitOrderLifecycle = class {
22828
23111
  const expired = await this.service.expireStale();
22829
23112
  if (expired > 0) this.logger.info({ expired }, "Withdrew limit orders that had outlived their expiry");
22830
23113
  }
23114
+ /**
23115
+ * The tally pass first, so reconciliation against the orderbook reads the sizes
23116
+ * it left. Each fails on its own.
23117
+ */
23118
+ async upkeep() {
23119
+ await this.run("tally reconciliation", async () => this.options.reconcileTallies());
23120
+ await this.run("reconciliation", () => this.reconcile());
23121
+ }
22831
23122
  async reconcile() {
22832
23123
  const report = await this.service.reconcile();
22833
23124
  if (report.cancelled || report.reposted || report.underFunded) {
@@ -23628,7 +23919,7 @@ async function bootFiller(config, options) {
23628
23919
  const sharedCacheService = new CacheService(options.loggers);
23629
23920
  if (!options.signer && !globalWatchOnly && !allChainsWatchOnly(watchOnlyConfig, resolvedChains)) {
23630
23921
  throw new Error(
23631
- "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]."
23922
+ "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]."
23632
23923
  );
23633
23924
  }
23634
23925
  const chainClientManager = new ChainClientManager(configService, options.signer);
@@ -23675,48 +23966,42 @@ async function bootFiller(config, options) {
23675
23966
  assertPairSymbolsResolve(config.pairs, assetRegistry, configuredChainNames);
23676
23967
  }
23677
23968
  const adminStrategies = [];
23678
- const strategies = [];
23679
- let confirmationPolicy;
23680
- let tradingPairs;
23681
- let engine;
23682
- if (config.pairs?.length) {
23683
- tradingPairs = config.pairs.map((pair) => ({ token0: pair.token0, token1: pair.token1 }));
23684
- tradingPairs.forEach((pair, pairIndex) => {
23685
- adminStrategies.push({
23686
- index: pairIndex,
23687
- pairIndex,
23688
- exotic: `${pair.token0}/${pair.token1}`,
23689
- token0: pair.token0,
23690
- token1: pair.token1,
23691
- sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
23692
- });
23969
+ const tradingPairs = (config.pairs ?? []).map((pair) => ({
23970
+ token0: pair.token0,
23971
+ token1: pair.token1
23972
+ }));
23973
+ tradingPairs.forEach((pair, pairIndex) => {
23974
+ adminStrategies.push({
23975
+ index: pairIndex,
23976
+ pairIndex,
23977
+ exotic: `${pair.token0}/${pair.token1}`,
23978
+ token0: pair.token0,
23979
+ token1: pair.token1,
23980
+ sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
23693
23981
  });
23694
- confirmationPolicy = assertConfirmationCoverage(
23695
- config.confirmationPolicies,
23696
- resolvedChains.map((c) => c.chainId)
23697
- );
23698
- const fundingVenues = vaultVenue ? [vaultVenue] : [];
23699
- engine = new FXFiller(
23700
- runtimeSigner,
23701
- configService,
23702
- chainClientManager,
23703
- contractService,
23704
- tradingPairs,
23705
- assetRegistry,
23706
- { confirmationPolicy, fundingVenues, limitOrders: options.data.limitOrders }
23707
- );
23708
- logger.info("Hydrating funding venue state...");
23709
- await engine.initialise();
23710
- strategies.push(engine);
23711
- }
23982
+ });
23983
+ const confirmationPolicy = assertConfirmationCoverage(
23984
+ config.confirmationPolicies,
23985
+ resolvedChains.map((c) => c.chainId)
23986
+ );
23987
+ const fundingVenues = vaultVenue ? [vaultVenue] : [];
23988
+ const engine = new FXFiller(
23989
+ runtimeSigner,
23990
+ configService,
23991
+ chainClientManager,
23992
+ contractService,
23993
+ tradingPairs,
23994
+ assetRegistry,
23995
+ { confirmationPolicy, fundingVenues, limitOrders: options.data.limitOrders }
23996
+ );
23997
+ logger.info("Hydrating funding venue state...");
23998
+ await engine.initialise();
23999
+ const strategies = [engine];
23712
24000
  const haltControls = strategies.map((engine2, index) => ({
23713
24001
  index,
23714
24002
  isHalted: () => engine2.isHalted(),
23715
24003
  resetHalt: () => engine2.resetHalt()
23716
24004
  }));
23717
- if (vaultVenue) {
23718
- await vaultVenue.initialise(runtimeSigner.address);
23719
- }
23720
24005
  let rebalancingService;
23721
24006
  const rebalancingConfig = configService.getRebalancingConfig();
23722
24007
  if (rebalancingConfig?.triggerPercentage !== void 0 && rebalancingConfig?.baseBalances) {
@@ -23773,7 +24058,8 @@ async function bootFiller(config, options) {
23773
24058
  const lifecycle = new LimitOrderLifecycle(
23774
24059
  limitOrderService,
23775
24060
  {
23776
- reconcileIntervalSecs: config.orderbook?.reconcileIntervalSecs ?? DEFAULT_RECONCILE_INTERVAL_SECS
24061
+ reconcileIntervalSecs: config.orderbook?.reconcileIntervalSecs ?? DEFAULT_RECONCILE_INTERVAL_SECS,
24062
+ reconcileTallies: () => intentFiller.reconcileLimitOrderTallies()
23777
24063
  },
23778
24064
  options.loggers
23779
24065
  );
@@ -24233,8 +24519,8 @@ var MemoryLimitOrderStore = class {
24233
24519
  async reserve(id, amount) {
24234
24520
  const order = this.orders.get(id);
24235
24521
  if (!order || order.status !== "open") return false;
24522
+ if (BigInt(amount) > BigInt(order.remaining)) return false;
24236
24523
  const reserved = BigInt(order.reserved) + BigInt(amount);
24237
- if (reserved > BigInt(order.remaining)) return false;
24238
24524
  this.patch(id, { reserved: reserved.toString() });
24239
24525
  return true;
24240
24526
  }
@@ -24244,6 +24530,14 @@ var MemoryLimitOrderStore = class {
24244
24530
  const remaining = BigInt(order.remaining) - BigInt(amount);
24245
24531
  return this.patch(id, { remaining: (remaining > 0n ? remaining : 0n).toString() });
24246
24532
  }
24533
+ async clampRemaining(id, room) {
24534
+ const order = this.orders.get(id);
24535
+ if (!order || order.status !== "open") return null;
24536
+ const sum = BigInt(room) + BigInt(order.reserved);
24537
+ const target = sum > 0n ? sum : 0n;
24538
+ if (target >= BigInt(order.remaining)) return null;
24539
+ return this.patch(id, { remaining: target.toString() });
24540
+ }
24247
24541
  async release(id, amount) {
24248
24542
  const order = this.orders.get(id);
24249
24543
  if (!order) return;
@@ -24752,7 +25046,8 @@ var OrderScanner = class _OrderScanner {
24752
25046
  * back from its own endpoints, which also proves they answer for one chain
24753
25047
  * before a filler ever depends on them.
24754
25048
  *
24755
- * @throws if an endpoint set is invalid or unreachable, if two entries resolve
25049
+ * @throws if an endpoint set is invalid, if none of a chain's endpoints answer,
25050
+ * if its endpoints disagree on the chain, if two entries resolve
24756
25051
  * to the same chain, or if `scanIntervalSecs` is below the minimum.
24757
25052
  */
24758
25053
  static async create(options) {
@@ -24812,7 +25107,10 @@ var OrderScanner = class _OrderScanner {
24812
25107
  async addChainNow(chain) {
24813
25108
  if (this.closed) throw new Error("This OrderScanner is closed");
24814
25109
  const rpcUrls = validateRpcUrls(chain.rpcUrls);
24815
- const chainId = chain.chainId ?? await fetchChainId(rpcUrls[0]);
25110
+ const chainId = chain.chainId ?? (await resolveChainConfigs([{ rpcUrls, bundlerUrl: chain.bundlerUrl ?? "" }], {
25111
+ loggers: this.loggers,
25112
+ tolerateUnreachable: true
25113
+ }))[0].chainId;
24816
25114
  if (this.closed) throw new Error("This OrderScanner is closed");
24817
25115
  if (this.scanners.has(chainId)) {
24818
25116
  throw new Error(`Chain ${chainId} is already in this scanner`);
@@ -24912,7 +25210,7 @@ var LimitOrderController = class {
24912
25210
  get service() {
24913
25211
  const service = this.runtime.limitOrders;
24914
25212
  if (!service) {
24915
- throw new Error("No orderbook is configured \u2014 set [orderbook] enabled and url to use limit orders");
25213
+ throw new Error("No orderbook is configured \u2014 set [orderbook] url to use limit orders");
24916
25214
  }
24917
25215
  return service;
24918
25216
  }
@@ -24986,10 +25284,7 @@ var PairController = class {
24986
25284
  * the pair's symbols have to resolve before the market can be hydrated.
24987
25285
  */
24988
25286
  async add(pair, options) {
24989
- const { engine, tradingPairs, assetRegistry, adminStrategies, balanceTokens, config } = this.runtime;
24990
- if (!engine || !tradingPairs) {
24991
- throw new Error("No trading engine is running \u2014 this filler was started without pairs");
24992
- }
25287
+ const { engine, assetRegistry, adminStrategies, balanceTokens, config } = this.runtime;
24993
25288
  const assets = options?.assets;
24994
25289
  const nextAssets = { ...config.assets ?? {}, ...assets ?? {} };
24995
25290
  const nextPairs = [...this.pairs, pair];
@@ -25028,7 +25323,6 @@ var PairController = class {
25028
25323
  /** Closes a market. Orders already in flight against it still complete. */
25029
25324
  async remove(index) {
25030
25325
  const { engine, tradingPairs, adminStrategies, config } = this.runtime;
25031
- if (!engine || !tradingPairs) throw new Error("No trading engine is running");
25032
25326
  if (index < 0 || index >= this.pairs.length) throw new Error(`Unknown pair ${index}`);
25033
25327
  engine.removePair(tradingPairs[index]);
25034
25328
  config.pairs = this.pairs.filter((_, i) => i !== index);
@@ -25040,7 +25334,7 @@ var PairController = class {
25040
25334
  await this.persist();
25041
25335
  }
25042
25336
  livePair(index) {
25043
- const pair = this.runtime.tradingPairs?.[index];
25337
+ const pair = this.runtime.tradingPairs[index];
25044
25338
  if (!pair) throw new Error(`Unknown pair ${index}`);
25045
25339
  return pair;
25046
25340
  }
@@ -25132,9 +25426,9 @@ var ChainController = class {
25132
25426
  throw error;
25133
25427
  }
25134
25428
  const curve = confirmationPolicies[String(chainId)];
25135
- if (curve) this.runtime.confirmationPolicy?.add(chainId, curve);
25136
- else if (!this.runtime.confirmationPolicy?.has(chainId)) {
25137
- this.runtime.confirmationPolicy?.adopt(chainId, resolvedPolicy);
25429
+ if (curve) this.runtime.confirmationPolicy.add(chainId, curve);
25430
+ else if (!this.runtime.confirmationPolicy.has(chainId)) {
25431
+ this.runtime.confirmationPolicy.adopt(chainId, resolvedPolicy);
25138
25432
  }
25139
25433
  this.runtime.resolvedChains.push(resolved);
25140
25434
  config.chains = [...config.chains, { rpcUrls: chain.rpcUrls, bundlerUrl: chain.bundlerUrl }];
@@ -25162,7 +25456,7 @@ var ChainController = class {
25162
25456
  await intentFiller.removeChain(chainId);
25163
25457
  if (this.ownsScanner) await this.scanner.removeChain(chainId);
25164
25458
  configService.removeChain(chainId);
25165
- this.runtime.confirmationPolicy?.remove(chainId);
25459
+ this.runtime.confirmationPolicy.remove(chainId);
25166
25460
  this.runtime.chainClientManager.invalidate(chainKey);
25167
25461
  this.runtime.resolvedChains.splice(index, 1);
25168
25462
  config.chains = config.chains.filter((_, i) => i !== index);
@@ -25381,7 +25675,7 @@ var Simplex = class _Simplex extends EventEmitter4 {
25381
25675
  const tomlSigner = options.config.simplex?.signer;
25382
25676
  if (tomlSigner && !options.signer) {
25383
25677
  throw new Error(
25384
- "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."
25678
+ "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."
25385
25679
  );
25386
25680
  }
25387
25681
  const loggers = new LoggerContext({
@@ -25584,6 +25878,8 @@ var Simplex = class _Simplex extends EventEmitter4 {
25584
25878
  export {
25585
25879
  AssetController,
25586
25880
  ChainController,
25881
+ LimitOrderController,
25882
+ LimitOrderValidationError,
25587
25883
  LoggerContext,
25588
25884
  MemoryDataStore,
25589
25885
  OrderScanner,
@@ -25596,6 +25892,7 @@ export {
25596
25892
  WalletController,
25597
25893
  addLogSink,
25598
25894
  assertConfirmationCoverage,
25895
+ canDerive,
25599
25896
  configureLogger,
25600
25897
  createSigner,
25601
25898
  digestSigner,
@@ -25608,10 +25905,12 @@ export {
25608
25905
  pickAnchorStable,
25609
25906
  privateKeySigner,
25610
25907
  registrySymbols,
25908
+ secretPhraseSigner,
25611
25909
  turnkeySigner,
25612
25910
  validateAssetDefinitions,
25613
25911
  validateConfig,
25614
25912
  validatePairConfigs,
25913
+ validateSecretPhrase,
25615
25914
  validateSignerConfig,
25616
25915
  validateVaultToml,
25617
25916
  viemSigner