@hyperbridge/simplex 0.16.8 → 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({
@@ -18794,8 +19089,13 @@ var ContractInteractionService = class {
18794
19089
  * Same-chain fills release escrow locally with no Hyperbridge dispatch, so the
18795
19090
  * gateway never pulls the fee token — its approval is skipped. Only cross-chain
18796
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.
18797
19097
  */
18798
- async buildApprovalAndFillCalldata(order, fillerOutputs, fillOptions, requiredFeeTokenAmount) {
19098
+ async buildApprovalAndFillCalldata(order, fillerOutputs, fillOptions, requiredFeeTokenAmount, settlement) {
18799
19099
  const chain = order.destination;
18800
19100
  const intentGatewayV2Address = this.configService.getIntentGatewayAddress(chain);
18801
19101
  const perTokenRequired = /* @__PURE__ */ new Map();
@@ -18805,9 +19105,10 @@ var ContractInteractionService = class {
18805
19105
  const key = addr.toLowerCase();
18806
19106
  perTokenRequired.set(key, (perTokenRequired.get(key) ?? 0n) + output.amount);
18807
19107
  }
19108
+ let feeKey;
18808
19109
  if (order.source !== order.destination) {
18809
19110
  const feeToken = await this.getFeeTokenWithDecimals(chain);
18810
- const feeKey = feeToken.address.toLowerCase();
19111
+ feeKey = feeToken.address.toLowerCase();
18811
19112
  perTokenRequired.set(feeKey, (perTokenRequired.get(feeKey) ?? 0n) + requiredFeeTokenAmount);
18812
19113
  }
18813
19114
  const fundingPrepends = order.id ? this.cacheService.getFundingPrepends(order.id) : null;
@@ -18833,6 +19134,26 @@ var ContractInteractionService = class {
18833
19134
  value: nativeOutputValue,
18834
19135
  data: encodeFillOrder(transformOrderForContract(order), fillOptions)
18835
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
+ }
18836
19157
  return encodeERC7821ExecuteBatch2(calls);
18837
19158
  }
18838
19159
  };
@@ -20685,11 +21006,11 @@ function byBestRate(a, b) {
20685
21006
  return left === right ? 0 : left > right ? -1 : 1;
20686
21007
  }
20687
21008
  function chargedFor(hold, delivered, released) {
20688
- if (hold.take === void 0 || released === 0n) return delivered;
20689
- const take = BigInt(hold.take);
20690
- if (take === 0n) return delivered;
20691
- const atRate = (BigInt(hold.amount) * released + take - 1n) / take;
20692
- 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);
20693
21014
  }
20694
21015
  var IntentFiller = class {
20695
21016
  constructor(chainConfigs, strategies, config, configService, chainClientManager, contractService, signer, scanners, rebalancingService, bidStorage, limitOrders, assetRegistry) {
@@ -20766,6 +21087,17 @@ var IntentFiller = class {
20766
21087
  setLimitOrderService(service) {
20767
21088
  this.limitOrderService = service;
20768
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
+ }
20769
21101
  /** Whether the given chain id is configured for watch-only (monitor, never fill). */
20770
21102
  isChainWatchOnly(chainId) {
20771
21103
  const watchOnly = this.config.watchOnly;
@@ -21411,7 +21743,7 @@ var IntentFiller = class {
21411
21743
  let reservation = [];
21412
21744
  if (plan) {
21413
21745
  if (order.id) {
21414
- cache.setFillerOutputs(order.id, plan.fillerOutputs, plan.fillerInputs);
21746
+ cache.setFillerOutputs(order.id, plan.fillerOutputs, plan.fillerInputs, plan.budget);
21415
21747
  cache.setPartialFill(order.id, plan.partialFill);
21416
21748
  if (plan.fundingCalls.length > 0) cache.setFundingPrepends(order.id, plan.fundingCalls);
21417
21749
  else cache.clearFundingPrepends(order.id);
@@ -21612,7 +21944,7 @@ var IntentFiller = class {
21612
21944
  await this.releaseAll(claimed);
21613
21945
  return;
21614
21946
  }
21615
- const settled = executedHold(claimed, delivered, released);
21947
+ const settled = executedHold(claimed, delivered.scaled, released);
21616
21948
  const charged = chargedFor(settled, delivered, released);
21617
21949
  const settledBid = rows.find(
21618
21950
  (row) => row.reservations.some(
@@ -21665,15 +21997,15 @@ var IntentFiller = class {
21665
21997
  if (delivered === null) return [];
21666
21998
  const settled = executedHold(
21667
21999
  holds.map((entry) => entry.hold),
21668
- delivered,
22000
+ delivered.scaled,
21669
22001
  released
21670
22002
  );
21671
22003
  const row = holds.find((entry) => entry.hold === settled).row;
21672
22004
  return this.bidStorage.claimReservation(commitment, row.bid ?? void 0);
21673
22005
  }
21674
22006
  /**
21675
- * What this fill delivered of the token the limit order pays, at 1e18, or null
21676
- * 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.
21677
22009
  */
21678
22010
  async deliveredAgainst(limitOrderId, chainId, outputs) {
21679
22011
  const order = await this.limitOrders?.get(limitOrderId);
@@ -21689,7 +22021,7 @@ var IntentFiller = class {
21689
22021
  }
21690
22022
  if (total === 0n) return null;
21691
22023
  const decimals = await this.contractService.getTokenDecimals(address, chain);
21692
- return toScaled(total, decimals);
22024
+ return { scaled: toScaled(total, decimals), raw: total, decimals };
21693
22025
  }
21694
22026
  /**
21695
22027
  * Gives back whatever this bid still holds against its limit order.
@@ -21785,87 +22117,6 @@ import {
21785
22117
  import { formatUnits as formatUnits4 } from "viem";
21786
22118
  import { Decimal as Decimal6 } from "decimal.js";
21787
22119
 
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
22120
  // src/orderbook/usd.ts
21870
22121
  import { Decimal as Decimal5 } from "decimal.js";
21871
22122
  function limitOrderUsdEdges(orders) {
@@ -21920,9 +22171,13 @@ var FXFiller = class _FXFiller {
21920
22171
  */
21921
22172
  constructor(signer, configService, clientManager, contractService, pairs, registry, options) {
21922
22173
  this.name = "FXFiller";
21923
- /** 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
+ */
21924
22179
  this.consecutiveClamps = 0;
21925
- /** 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. */
21926
22181
  this.halted = false;
21927
22182
  this.logger = moduleLogger(configService.loggers, "fx-simplex");
21928
22183
  const { confirmationPolicy, fundingVenues = [], limitOrders } = options ?? {};
@@ -21971,10 +22226,9 @@ var FXFiller = class _FXFiller {
21971
22226
  this.pairs.push(pair);
21972
22227
  }
21973
22228
  /**
21974
- * Removes a market from the running engine. All-or-nothing: at least one
21975
- * market must remain and no other pair may lose its USD anchor (removing a
21976
- * reference feed that anchors a dependent market is rejected). In-flight
21977
- * 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.
21978
22232
  */
21979
22233
  removePair(pair) {
21980
22234
  const index = this.pairs.indexOf(pair);
@@ -22030,22 +22284,20 @@ var FXFiller = class _FXFiller {
22030
22284
  }
22031
22285
  }
22032
22286
  /**
22033
- * Evaluates whether an order is profitable to fill under the per-pair
22034
- * `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.
22035
22289
  *
22036
22290
  * High-level flow:
22037
- * - Resolve each (input, output) leg to a configured pair and direction.
22038
- * - Estimate each pair's total token0 notional in the order and cap it at
22039
- * the pair's `maxOrderSize` when one is set; pair curves are evaluated at
22040
- * that (possibly uncapped) notional.
22041
- * - Walk the legs, allocating from each pair's capped token0 budget and
22042
- * pricing outputs at the pair's rate.
22043
- * - Further cap each leg by the filler's current token balance plus
22044
- * funding-venue withdrawals.
22045
- * - Cache the resulting outputs for later use in `executeOrder`.
22046
- *
22047
- * Note: we may intentionally overfill relative to the user's requested
22048
- * 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`.
22049
22301
  */
22050
22302
  async calculateProfitability(order) {
22051
22303
  if (order.id) this.contractService.cacheService.clearPartialFill(order.id);
@@ -22063,6 +22315,7 @@ var FXFiller = class _FXFiller {
22063
22315
  const multiLeg = order.inputs.length > 1;
22064
22316
  const plans = [];
22065
22317
  const plannedOn = /* @__PURE__ */ new Map();
22318
+ const spentOn = /* @__PURE__ */ new Map();
22066
22319
  for (let leg = 0; leg < order.inputs.length; leg++) {
22067
22320
  const input = order.inputs[leg];
22068
22321
  const output = order.output.assets[leg];
@@ -22084,7 +22337,30 @@ var FXFiller = class _FXFiller {
22084
22337
  for (const candidate of matches) {
22085
22338
  let partialFill = false;
22086
22339
  const fundingCalls = [];
22087
- 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);
22088
22364
  if (left <= 0n) {
22089
22365
  this.logger.info(
22090
22366
  { orderId: order.id, leg, limitOrder: candidate.order.id },
@@ -22119,10 +22395,7 @@ var FXFiller = class _FXFiller {
22119
22395
  {
22120
22396
  orderId: order.id,
22121
22397
  limitOrder: candidate.order.id,
22122
- available: formatUnits4(
22123
- candidate.available,
22124
- 18
22125
- ),
22398
+ available: formatUnits4(available, 18),
22126
22399
  userRequested: output.amount.toString(),
22127
22400
  payout: targetOutput.toString(),
22128
22401
  crossChain: sourceChain !== destChain
@@ -22167,7 +22440,10 @@ var FXFiller = class _FXFiller {
22167
22440
  }
22168
22441
  }
22169
22442
  const effectiveBalance = walletContribution + credited;
22170
- const finalOutputAmount = effectiveBalance > targetOutput ? targetOutput : effectiveBalance;
22443
+ const finalOutputAmount = toRaw(
22444
+ toScaled(effectiveBalance > targetOutput ? targetOutput : effectiveBalance, outputDecimals),
22445
+ outputDecimals
22446
+ );
22171
22447
  if (finalOutputAmount === 0n) {
22172
22448
  this.logger.info(
22173
22449
  {
@@ -22343,7 +22619,8 @@ var FXFiller = class _FXFiller {
22343
22619
  fillerInputs,
22344
22620
  fundingCalls: [...fundingCalls],
22345
22621
  partialFill,
22346
- profit: totalProfit
22622
+ profit: totalProfit,
22623
+ budget
22347
22624
  });
22348
22625
  if (!partialEligibleCheap) break;
22349
22626
  }
@@ -22351,7 +22628,12 @@ var FXFiller = class _FXFiller {
22351
22628
  if (plans.length === 0) return 0;
22352
22629
  if (order.id) {
22353
22630
  this.contractService.cacheService.setBidPlans(order.id, plans);
22354
- 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
+ );
22355
22637
  this.contractService.cacheService.setPartialFill(order.id, plans[0].partialFill);
22356
22638
  this.contractService.cacheService.setMatchedLimitOrder(order.id, [
22357
22639
  { limitOrderId: plans[0].limitOrderId, payout: plans[0].payout }
@@ -22446,10 +22728,14 @@ var FXFiller = class _FXFiller {
22446
22728
  // =========================================================================
22447
22729
  /**
22448
22730
  * Update consecutive-clamp counter after a successful order evaluation.
22449
- * Only venue-priced legs feed this counter (see clamp site) — a streak of those
22450
- * is the signal that a live market source has gone off (stale pool, manipulated
22451
- * venue) and the filler should stop until an operator investigates. Offline
22452
- * 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.
22453
22739
  */
22454
22740
  recordOrderOutcome(clamped, orderId) {
22455
22741
  if (clamped) {
@@ -22475,18 +22761,6 @@ var FXFiller = class _FXFiller {
22475
22761
  this.consecutiveClamps = 0;
22476
22762
  this.logger.warn("FXFiller halt reset by operator \u2014 resuming order evaluation");
22477
22763
  }
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
22764
  /**
22491
22765
  * Whether any solver has already delivered output against this order.
22492
22766
  *
@@ -22824,9 +23098,9 @@ var LimitOrderLifecycle = class {
22824
23098
  const reconcileMs = this.options.reconcileIntervalSecs * 1e3;
22825
23099
  this.every(heartbeatMs, "heartbeat", () => this.service.heartbeat());
22826
23100
  this.every(EXPIRY_SWEEP_MS, "expiry", () => this.expire());
22827
- this.every(reconcileMs, "reconciliation", () => this.reconcile());
23101
+ this.every(reconcileMs, "reconciliation", () => this.upkeep());
22828
23102
  this.logger.info({ heartbeatMs, expiryMs: EXPIRY_SWEEP_MS, reconcileMs }, "Orderbook lifecycle started");
22829
- void this.run("reconciliation", () => this.reconcile());
23103
+ void this.upkeep();
22830
23104
  }
22831
23105
  stop() {
22832
23106
  this.timers.forEach((timer) => clearInterval(timer));
@@ -22837,6 +23111,14 @@ var LimitOrderLifecycle = class {
22837
23111
  const expired = await this.service.expireStale();
22838
23112
  if (expired > 0) this.logger.info({ expired }, "Withdrew limit orders that had outlived their expiry");
22839
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
+ }
22840
23122
  async reconcile() {
22841
23123
  const report = await this.service.reconcile();
22842
23124
  if (report.cancelled || report.reposted || report.underFunded) {
@@ -23637,7 +23919,7 @@ async function bootFiller(config, options) {
23637
23919
  const sharedCacheService = new CacheService(options.loggers);
23638
23920
  if (!options.signer && !globalWatchOnly && !allChainsWatchOnly(watchOnlyConfig, resolvedChains)) {
23639
23921
  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]."
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]."
23641
23923
  );
23642
23924
  }
23643
23925
  const chainClientManager = new ChainClientManager(configService, options.signer);
@@ -23776,7 +24058,8 @@ async function bootFiller(config, options) {
23776
24058
  const lifecycle = new LimitOrderLifecycle(
23777
24059
  limitOrderService,
23778
24060
  {
23779
- reconcileIntervalSecs: config.orderbook?.reconcileIntervalSecs ?? DEFAULT_RECONCILE_INTERVAL_SECS
24061
+ reconcileIntervalSecs: config.orderbook?.reconcileIntervalSecs ?? DEFAULT_RECONCILE_INTERVAL_SECS,
24062
+ reconcileTallies: () => intentFiller.reconcileLimitOrderTallies()
23780
24063
  },
23781
24064
  options.loggers
23782
24065
  );
@@ -24247,6 +24530,14 @@ var MemoryLimitOrderStore = class {
24247
24530
  const remaining = BigInt(order.remaining) - BigInt(amount);
24248
24531
  return this.patch(id, { remaining: (remaining > 0n ? remaining : 0n).toString() });
24249
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
+ }
24250
24541
  async release(id, amount) {
24251
24542
  const order = this.orders.get(id);
24252
24543
  if (!order) return;
@@ -24755,7 +25046,8 @@ var OrderScanner = class _OrderScanner {
24755
25046
  * back from its own endpoints, which also proves they answer for one chain
24756
25047
  * before a filler ever depends on them.
24757
25048
  *
24758
- * @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
24759
25051
  * to the same chain, or if `scanIntervalSecs` is below the minimum.
24760
25052
  */
24761
25053
  static async create(options) {
@@ -24815,7 +25107,10 @@ var OrderScanner = class _OrderScanner {
24815
25107
  async addChainNow(chain) {
24816
25108
  if (this.closed) throw new Error("This OrderScanner is closed");
24817
25109
  const rpcUrls = validateRpcUrls(chain.rpcUrls);
24818
- 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;
24819
25114
  if (this.closed) throw new Error("This OrderScanner is closed");
24820
25115
  if (this.scanners.has(chainId)) {
24821
25116
  throw new Error(`Chain ${chainId} is already in this scanner`);
@@ -24915,7 +25210,7 @@ var LimitOrderController = class {
24915
25210
  get service() {
24916
25211
  const service = this.runtime.limitOrders;
24917
25212
  if (!service) {
24918
- 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");
24919
25214
  }
24920
25215
  return service;
24921
25216
  }
@@ -25380,7 +25675,7 @@ var Simplex = class _Simplex extends EventEmitter4 {
25380
25675
  const tomlSigner = options.config.simplex?.signer;
25381
25676
  if (tomlSigner && !options.signer) {
25382
25677
  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."
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."
25384
25679
  );
25385
25680
  }
25386
25681
  const loggers = new LoggerContext({
@@ -25583,6 +25878,8 @@ var Simplex = class _Simplex extends EventEmitter4 {
25583
25878
  export {
25584
25879
  AssetController,
25585
25880
  ChainController,
25881
+ LimitOrderController,
25882
+ LimitOrderValidationError,
25586
25883
  LoggerContext,
25587
25884
  MemoryDataStore,
25588
25885
  OrderScanner,
@@ -25595,6 +25892,7 @@ export {
25595
25892
  WalletController,
25596
25893
  addLogSink,
25597
25894
  assertConfirmationCoverage,
25895
+ canDerive,
25598
25896
  configureLogger,
25599
25897
  createSigner,
25600
25898
  digestSigner,
@@ -25607,10 +25905,12 @@ export {
25607
25905
  pickAnchorStable,
25608
25906
  privateKeySigner,
25609
25907
  registrySymbols,
25908
+ secretPhraseSigner,
25610
25909
  turnkeySigner,
25611
25910
  validateAssetDefinitions,
25612
25911
  validateConfig,
25613
25912
  validatePairConfigs,
25913
+ validateSecretPhrase,
25614
25914
  validateSignerConfig,
25615
25915
  validateVaultToml,
25616
25916
  viemSigner