@hyperbridge/simplex 0.17.1 → 0.18.0

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.cjs CHANGED
@@ -37,7 +37,6 @@ __export(src_exports, {
37
37
  LoggerContext: () => LoggerContext,
38
38
  MemoryDataStore: () => MemoryDataStore,
39
39
  OrderScanner: () => OrderScanner,
40
- PairController: () => PairController,
41
40
  RebalanceController: () => RebalanceController,
42
41
  SignerType: () => SignerType,
43
42
  Simplex: () => Simplex,
@@ -56,14 +55,12 @@ __export(src_exports, {
56
55
  mpcVaultSigner: () => mpcVaultSigner,
57
56
  normalizeSymbol: () => normalizeSymbol,
58
57
  parseChainKey: () => parseChainKey,
59
- pickAnchorStable: () => pickAnchorStable,
60
58
  privateKeySigner: () => privateKeySigner,
61
59
  registrySymbols: () => registrySymbols,
62
60
  secretPhraseSigner: () => secretPhraseSigner,
63
61
  turnkeySigner: () => turnkeySigner,
64
62
  validateAssetDefinitions: () => validateAssetDefinitions,
65
63
  validateConfig: () => validateConfig,
66
- validatePairConfigs: () => validatePairConfigs,
67
64
  validateSecretPhrase: () => validateSecretPhrase,
68
65
  validateSignerConfig: () => validateSignerConfig,
69
66
  validateVaultToml: () => validateVaultToml,
@@ -75,7 +72,8 @@ module.exports = __toCommonJS(src_exports);
75
72
  var import_node_events3 = require("node:events");
76
73
 
77
74
  // src/core/boot.ts
78
- var import_viem32 = require("viem");
75
+ var import_node_path = require("node:path");
76
+ var import_viem34 = require("viem");
79
77
 
80
78
  // src/core/filler.ts
81
79
  var import_util_crypto = require("@polkadot/util-crypto");
@@ -769,92 +767,6 @@ var AssetRegistry = class {
769
767
  }
770
768
  };
771
769
 
772
- // src/config/pairs.ts
773
- var RETIRED_PAIR_KEYS = ["bidPriceCurve", "askPriceCurve", "maxOrderSize", "referenceOnly"];
774
- function retiredPairKeys(pairs) {
775
- const carried = /* @__PURE__ */ new Set();
776
- for (const pair of pairs) {
777
- for (const key of RETIRED_PAIR_KEYS) {
778
- if (pair[key] !== void 0) carried.add(key);
779
- }
780
- }
781
- return RETIRED_PAIR_KEYS.filter((key) => carried.has(key));
782
- }
783
- function isKnownSymbol(symbol, userAssets) {
784
- const normalized = normalizeSymbol(symbol);
785
- if (isRegistrySymbol(normalized)) return true;
786
- return Object.keys(userAssets ?? {}).some((key) => normalizeSymbol(key) === normalized);
787
- }
788
- function assertPairSymbolsResolve(pairs, registry, chainNames) {
789
- const pairSymbols = new Set(pairs.flatMap((pair) => [pair.token0, pair.token1]));
790
- for (const pair of pairs) {
791
- for (const symbol of [pair.token0, pair.token1]) {
792
- const resolvesSomewhere = chainNames.some((chainName2) => registry.getAddress(symbol, chainName2) !== null);
793
- if (!resolvesSomewhere) {
794
- throw new Error(
795
- `pairs.${pair.token0}/${pair.token1}: '${symbol}' does not resolve to a deployed contract on any configured chain`
796
- );
797
- }
798
- }
799
- }
800
- for (const chainName2 of chainNames) {
801
- const addressOwner = /* @__PURE__ */ new Map();
802
- for (const symbol of pairSymbols) {
803
- const address = registry.getAddress(symbol, chainName2)?.toLowerCase();
804
- if (!address) continue;
805
- const owner = addressOwner.get(address);
806
- if (owner && normalizeSymbol(owner) !== normalizeSymbol(symbol)) {
807
- throw new Error(
808
- `assets: '${symbol}' and '${owner}' both resolve to ${address} on ${chainName2} \u2014 symbols must map to distinct contracts`
809
- );
810
- }
811
- addressOwner.set(address, symbol);
812
- }
813
- }
814
- }
815
- function pickAnchorStable(pairs, symbol) {
816
- const target = normalizeSymbol(symbol);
817
- for (const stable of USD_STABLE_SYMBOLS) {
818
- const taken = pairs.some((pair) => {
819
- const token0 = normalizeSymbol(pair.token0);
820
- const token1 = normalizeSymbol(pair.token1);
821
- return token0 === stable && token1 === target || token0 === target && token1 === stable;
822
- });
823
- if (!taken) return stable;
824
- }
825
- return null;
826
- }
827
- function validatePairConfigs(pairs, userAssets) {
828
- if (!Array.isArray(pairs) || pairs.length === 0) {
829
- throw new Error("pairs: at least one [[pairs]] entry is required");
830
- }
831
- const seen = /* @__PURE__ */ new Set();
832
- for (const pair of pairs) {
833
- if (!pair.token0 || !pair.token1) {
834
- throw new Error("pairs: each entry needs 'token0' and 'token1' symbols");
835
- }
836
- const token0 = normalizeSymbol(pair.token0);
837
- const token1 = normalizeSymbol(pair.token1);
838
- const label = `${token0}/${token1}`;
839
- if (seen.has(label)) {
840
- throw new Error(`pairs.${label}: pair is declared twice`);
841
- }
842
- if (seen.has(`${token1}/${token0}`)) {
843
- throw new Error(
844
- `pairs.${label}: ${token1}/${token0} is already declared \u2014 a market has one orientation, and a limit order on either side trades it`
845
- );
846
- }
847
- seen.add(label);
848
- for (const symbol of [token0, token1]) {
849
- if (!isKnownSymbol(symbol, userAssets)) {
850
- throw new Error(
851
- `pairs.${label}: unknown symbol '${symbol}' \u2014 the registry ships ${registrySymbols().join(", ")}; anything else needs an [assets.${symbol}] entry`
852
- );
853
- }
854
- }
855
- }
856
- }
857
-
858
770
  // src/config/abis/ERC20.ts
859
771
  var ERC20_ABI = [
860
772
  {
@@ -1672,7 +1584,7 @@ function validateOrderbookConfig(orderbook) {
1672
1584
  function validateConfig(config, cliWatchOnly = false) {
1673
1585
  if ("strategies" in config) {
1674
1586
  throw new Error(
1675
- "[[strategies]] was removed \u2014 declare top-level [[pairs]] instead (a same-token pair like USDC/USDC with an ask curve below par replaces the stable strategy; engine settings moved to [confirmationPolicies])"
1587
+ "[[strategies]] was removed \u2014 simplex prices every fill from the limit orders you post. Run `simplex init` to migrate this config; per-strategy confirmation policies move to [confirmationPolicies]"
1676
1588
  );
1677
1589
  }
1678
1590
  const allChainsWatchOnly2 = cliWatchOnly || config.simplex?.watchOnly === true;
@@ -1737,9 +1649,6 @@ function validateConfig(config, cliWatchOnly = false) {
1737
1649
  if (config.assets) {
1738
1650
  validateAssetDefinitions(config.assets);
1739
1651
  }
1740
- if ((config.pairs?.length ?? 0) > 0) {
1741
- validatePairConfigs(config.pairs, config.assets);
1742
- }
1743
1652
  if (config.confirmationPolicies) {
1744
1653
  void new ConfirmationPolicy(config.confirmationPolicies);
1745
1654
  }
@@ -18469,6 +18378,10 @@ var import_viem22 = require("viem");
18469
18378
  var import_viem18 = require("viem");
18470
18379
  var RECOMMENDED_AMOUNT_USD = 5n;
18471
18380
  var VERIFICATION_GAS_LIMIT_PERMIT2 = 200000n;
18381
+ var PERMIT2_VERIFICATION_GAS_BY_CHAIN = { 137: 300000n };
18382
+ function permit2VerificationGasLimit(chainId) {
18383
+ return PERMIT2_VERIFICATION_GAS_BY_CHAIN[chainId] ?? VERIFICATION_GAS_LIMIT_PERMIT2;
18384
+ }
18472
18385
  var VERIFICATION_GAS_LIMIT_PERMIT = 250000n;
18473
18386
  var PERMIT2_DEADLINE_SECONDS = 3600n;
18474
18387
  var POST_OP_GAS_LIMIT_SIMPLEX = 40000n;
@@ -18500,6 +18413,13 @@ var EIP2612_ABI = [
18500
18413
  outputs: [{ internalType: "string", name: "", type: "string" }],
18501
18414
  stateMutability: "view",
18502
18415
  type: "function"
18416
+ },
18417
+ {
18418
+ inputs: [],
18419
+ name: "DOMAIN_SEPARATOR",
18420
+ outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }],
18421
+ stateMutability: "view",
18422
+ type: "function"
18503
18423
  }
18504
18424
  ];
18505
18425
 
@@ -18561,25 +18481,73 @@ function normalizeSignature65(signature) {
18561
18481
  }
18562
18482
 
18563
18483
  // src/services/paymaster/permit.ts
18484
+ var DOMAIN_SHAPES = [
18485
+ {
18486
+ fields: [
18487
+ { name: "name", type: "string" },
18488
+ { name: "version", type: "string" },
18489
+ { name: "chainId", type: "uint256" },
18490
+ { name: "verifyingContract", type: "address" }
18491
+ ],
18492
+ domain: (name, version, chainId, verifyingContract) => ({ name, version, chainId, verifyingContract })
18493
+ },
18494
+ // USDT0 on Polygon: the chain id goes in a salt instead of a chainId field.
18495
+ {
18496
+ fields: [
18497
+ { name: "name", type: "string" },
18498
+ { name: "version", type: "string" },
18499
+ { name: "verifyingContract", type: "address" },
18500
+ { name: "salt", type: "bytes32" }
18501
+ ],
18502
+ domain: (name, version, chainId, verifyingContract) => ({
18503
+ name,
18504
+ version,
18505
+ verifyingContract,
18506
+ salt: (0, import_viem21.pad)((0, import_viem21.toHex)(chainId), { size: 32 })
18507
+ })
18508
+ }
18509
+ ];
18510
+ var FALLBACK_VERSIONS = ["1", "2"];
18511
+ async function readIfPresent(read) {
18512
+ try {
18513
+ return await read();
18514
+ } catch (error) {
18515
+ const isRevert = error instanceof import_viem21.BaseError && error.walk((e) => e instanceof import_viem21.ContractFunctionRevertedError || e instanceof import_viem21.ContractFunctionZeroDataError);
18516
+ if (isRevert) return void 0;
18517
+ throw error;
18518
+ }
18519
+ }
18520
+ async function resolvePermitDomain(client, tokenAddress, chainId) {
18521
+ const read = (functionName) => readIfPresent(() => client.readContract({ address: tokenAddress, abi: EIP2612_ABI, functionName }));
18522
+ const separator = await read("DOMAIN_SEPARATOR");
18523
+ if (!separator) return null;
18524
+ const name = await read("name");
18525
+ if (name === void 0) return null;
18526
+ const version = await read("version");
18527
+ for (const candidate of version === void 0 ? FALLBACK_VERSIONS : [version]) {
18528
+ for (const shape of DOMAIN_SHAPES) {
18529
+ const domain = shape.domain(name, candidate, chainId, tokenAddress);
18530
+ const hash = (0, import_viem21.hashDomain)({ domain, types: { EIP712Domain: shape.fields } });
18531
+ if (hash.toLowerCase() === separator.toLowerCase()) return { domain, fields: shape.fields };
18532
+ }
18533
+ }
18534
+ return null;
18535
+ }
18564
18536
  async function signEip2612Permit(client, signer, owner, spender, tokenAddress, value, chainId) {
18565
- const token = (0, import_viem21.getContract)({
18566
- client,
18537
+ const permitDomain = await resolvePermitDomain(client, tokenAddress, chainId);
18538
+ if (!permitDomain) {
18539
+ throw new Error(`${tokenAddress} on chain ${chainId} has no EIP-2612 permit domain Simplex can sign for`);
18540
+ }
18541
+ const nonce = await client.readContract({
18567
18542
  address: tokenAddress,
18568
- abi: EIP2612_ABI
18543
+ abi: EIP2612_ABI,
18544
+ functionName: "nonces",
18545
+ args: [owner]
18569
18546
  });
18570
- const [name, version, nonce] = await Promise.all([
18571
- token.read.name(),
18572
- token.read.version(),
18573
- token.read.nonces([owner])
18574
- ]);
18575
18547
  const typedData = {
18576
18548
  types: {
18577
- EIP712Domain: [
18578
- { name: "name", type: "string" },
18579
- { name: "version", type: "string" },
18580
- { name: "chainId", type: "uint256" },
18581
- { name: "verifyingContract", type: "address" }
18582
- ],
18549
+ // Listed explicitly: custody backends hash the domain from this entry, not from `domain`.
18550
+ EIP712Domain: permitDomain.fields,
18583
18551
  Permit: [
18584
18552
  { name: "owner", type: "address" },
18585
18553
  { name: "spender", type: "address" },
@@ -18589,12 +18557,7 @@ async function signEip2612Permit(client, signer, owner, spender, tokenAddress, v
18589
18557
  ]
18590
18558
  },
18591
18559
  primaryType: "Permit",
18592
- domain: {
18593
- name,
18594
- version,
18595
- chainId,
18596
- verifyingContract: tokenAddress
18597
- },
18560
+ domain: permitDomain.domain,
18598
18561
  message: {
18599
18562
  owner,
18600
18563
  spender,
@@ -18668,7 +18631,7 @@ async function buildSimplexPaymasterData(client, walletClient, signer, solverAcc
18668
18631
  );
18669
18632
  }
18670
18633
  if (permit2Allowance < recommended) {
18671
- if (permitBootstrap && await tokenSupportsPermit(client, tokenAddress)) {
18634
+ if (permitBootstrap && await tokenSupportsPermit(client, tokenAddress, chainId)) {
18672
18635
  const pm = await buildPermitMode(
18673
18636
  client,
18674
18637
  signer,
@@ -18704,7 +18667,7 @@ async function resolvePendingPermit2Approval(client, solverAccount, paymasterAdd
18704
18667
  return {
18705
18668
  token: selected.address,
18706
18669
  spender: permit2,
18707
- permitCapable: await tokenSupportsPermit(client, selected.address)
18670
+ permitCapable: await tokenSupportsPermit(client, selected.address, configService.getChainId(chain))
18708
18671
  };
18709
18672
  }
18710
18673
  function isConfigured(address) {
@@ -18724,7 +18687,7 @@ function configuredFeeTokens(chain, configService) {
18724
18687
  }
18725
18688
  async function selectToken(client, solverAccount, tokens, permit2) {
18726
18689
  const balances = [];
18727
- let fallback2 = null;
18690
+ let fallback3 = null;
18728
18691
  for (const token of tokens) {
18729
18692
  const balance = await client.readContract({
18730
18693
  address: token.address,
@@ -18738,12 +18701,12 @@ async function selectToken(client, solverAccount, tokens, permit2) {
18738
18701
  const permit2Allowance = await readAllowance(client, token.address, solverAccount, permit2);
18739
18702
  const candidate = { ...token, permit2Allowance };
18740
18703
  if (permit2Allowance >= RECOMMENDED_AMOUNT_USD * required) return { selected: candidate, balances };
18741
- fallback2 ?? (fallback2 = candidate);
18704
+ fallback3 ?? (fallback3 = candidate);
18742
18705
  } else {
18743
18706
  balances.push({ symbol: token.symbol, balance, required });
18744
18707
  }
18745
18708
  }
18746
- return { selected: fallback2, balances };
18709
+ return { selected: fallback3, balances };
18747
18710
  }
18748
18711
  var PERMIT2_SUPPORT_NEGATIVE_TTL_MS = 5 * 6e4;
18749
18712
  var permit2Support = /* @__PURE__ */ new Map();
@@ -18796,7 +18759,7 @@ async function buildPermit2Mode(signer, p) {
18796
18759
  return {
18797
18760
  paymaster: p.spender,
18798
18761
  paymasterData,
18799
- paymasterVerificationGasLimit: VERIFICATION_GAS_LIMIT_PERMIT2,
18762
+ paymasterVerificationGasLimit: permit2VerificationGasLimit(p.chainId),
18800
18763
  paymasterPostOpGasLimit: POST_OP_GAS_LIMIT_SIMPLEX
18801
18764
  };
18802
18765
  }
@@ -18866,27 +18829,8 @@ async function sendApproveTx(client, walletClient, tokenAddress, spender, amount
18866
18829
  });
18867
18830
  await client.waitForTransactionReceipt({ hash, confirmations: 2 });
18868
18831
  }
18869
- async function tokenSupportsPermit(client, tokenAddress) {
18870
- try {
18871
- await client.readContract({
18872
- address: tokenAddress,
18873
- abi: [
18874
- {
18875
- inputs: [],
18876
- name: "version",
18877
- outputs: [{ type: "string" }],
18878
- stateMutability: "view",
18879
- type: "function"
18880
- }
18881
- ],
18882
- functionName: "version"
18883
- });
18884
- return true;
18885
- } catch (error) {
18886
- const isRevert = error instanceof import_viem22.BaseError && error.walk((e) => e instanceof import_viem22.ContractFunctionRevertedError || e instanceof import_viem22.ContractFunctionZeroDataError);
18887
- if (isRevert) return false;
18888
- throw error;
18889
- }
18832
+ async function tokenSupportsPermit(client, tokenAddress, chainId) {
18833
+ return await resolvePermitDomain(client, tokenAddress, chainId) !== null;
18890
18834
  }
18891
18835
 
18892
18836
  // src/services/paymaster/index.ts
@@ -18902,7 +18846,7 @@ async function buildPaymasterAndData(options) {
18902
18846
  const shortfall = await depositShortfall(
18903
18847
  options,
18904
18848
  simplexAddr,
18905
- (permitBootstrap ? VERIFICATION_GAS_LIMIT_PERMIT : VERIFICATION_GAS_LIMIT_PERMIT2) + POST_OP_GAS_LIMIT_SIMPLEX
18849
+ (permitBootstrap ? VERIFICATION_GAS_LIMIT_PERMIT : permit2VerificationGasLimit(configService.getChainId(chain))) + POST_OP_GAS_LIMIT_SIMPLEX
18906
18850
  );
18907
18851
  if (shortfall) {
18908
18852
  return { paymasterAndData: "0x", type: "none", reason: shortfall };
@@ -19694,12 +19638,12 @@ var ContractInteractionService = class {
19694
19638
  const declared = declaredMs ? declaredMs / 1e3 : void 0;
19695
19639
  const seconds = this.measureBlockTimeSeconds(client).then((measured) => declared ? Math.min(measured, declared) : measured).catch((err) => {
19696
19640
  this.blockTimes.delete(chain);
19697
- const fallback2 = declared ?? FALLBACK_BLOCK_TIME_SECONDS;
19641
+ const fallback3 = declared ?? FALLBACK_BLOCK_TIME_SECONDS;
19698
19642
  this.logger.warn(
19699
- { err, chain, seconds: fallback2 },
19643
+ { err, chain, seconds: fallback3 },
19700
19644
  "Could not measure the block time; using the declared one"
19701
19645
  );
19702
- return fallback2;
19646
+ return fallback3;
19703
19647
  });
19704
19648
  this.blockTimes.set(chain, seconds);
19705
19649
  return seconds;
@@ -22883,18 +22827,17 @@ function adjustDecimalsFloor(amount, fromDecimals, toDecimals) {
22883
22827
  if (fromDecimals < toDecimals) return amount * BigInt(10 ** (toDecimals - fromDecimals));
22884
22828
  return amount / BigInt(10 ** (fromDecimals - toDecimals));
22885
22829
  }
22886
- var FXFiller = class _FXFiller {
22830
+ var FXFiller = class {
22887
22831
  /**
22888
22832
  * @param signer Filler's signing account for UserOp signatures.
22889
22833
  * @param configService Network/config provider for addresses and decimals.
22890
22834
  * @param clientManager Used to get viem PublicClients for chains.
22891
22835
  * @param contractService Shared contract interaction service.
22892
- * @param pairs Trading pairs with their bid/ask price policies and per-order caps.
22893
- * @param registry Asset symbol registry resolving pair symbols per chain.
22836
+ * @param registry Asset symbol registry resolving symbols per chain.
22894
22837
  * @param options.confirmationPolicy Optional per-chain confirmation policy for cross-chain orders.
22895
22838
  * @param options.fundingVenues Optional funding venues for on-chain liquidity sourcing.
22896
22839
  */
22897
- constructor(signer, configService, clientManager, contractService, pairs, registry, options) {
22840
+ constructor(signer, configService, clientManager, contractService, registry, options) {
22898
22841
  this.name = "FXFiller";
22899
22842
  /**
22900
22843
  * Consecutive orders where the overfill clamp activated. Dormant: the clamp
@@ -22907,17 +22850,9 @@ var FXFiller = class _FXFiller {
22907
22850
  this.logger = moduleLogger(configService.loggers, "fx-simplex");
22908
22851
  const { confirmationPolicy, fundingVenues = [], limitOrders } = options ?? {};
22909
22852
  this.limitOrders = limitOrders;
22910
- if (pairs.length === 0) {
22911
- throw new Error("FXFiller requires at least one trading pair");
22912
- }
22913
- const seenPairs = /* @__PURE__ */ new Set();
22914
- for (const pair of pairs) {
22915
- _FXFiller.assertPairValid(pair, seenPairs);
22916
- }
22917
22853
  this.configService = configService;
22918
22854
  this.clientManager = clientManager;
22919
22855
  this.contractService = contractService;
22920
- this.pairs = pairs;
22921
22856
  this.registry = registry;
22922
22857
  this.fundingVenues = fundingVenues;
22923
22858
  this.signer = signer;
@@ -22929,43 +22864,6 @@ var FXFiller = class _FXFiller {
22929
22864
  };
22930
22865
  }
22931
22866
  }
22932
- /** Per-pair invariants, shared by the constructor and addPair. Adds the accepted pair's label to `seenPairs`. */
22933
- static assertPairValid(pair, seenPairs) {
22934
- const label = `${normalizeSymbol(pair.token0)}/${normalizeSymbol(pair.token1)}`;
22935
- const reversed = `${normalizeSymbol(pair.token1)}/${normalizeSymbol(pair.token0)}`;
22936
- if (seenPairs.has(label) || seenPairs.has(reversed)) {
22937
- throw new Error(
22938
- `FXFiller pair ${pair.token0}/${pair.token1}: duplicate market (a pair and its reverse are the same market)`
22939
- );
22940
- }
22941
- seenPairs.add(label);
22942
- }
22943
- /**
22944
- * Adds a market to the running engine. All-or-nothing: the pair passes the
22945
- * same duplicate and reverse-orientation checks the constructor enforces
22946
- * before it is pushed.
22947
- */
22948
- addPair(pair) {
22949
- const seenPairs = new Set(this.pairs.map((p) => `${normalizeSymbol(p.token0)}/${normalizeSymbol(p.token1)}`));
22950
- _FXFiller.assertPairValid(pair, seenPairs);
22951
- this.pairs.push(pair);
22952
- }
22953
- /**
22954
- * Removes a market from the running engine. At least one market must remain.
22955
- * Matching reads the limit orders rather than the pairs, so removing a market
22956
- * changes what the dashboard lists, not what the filler fills.
22957
- */
22958
- removePair(pair) {
22959
- const index = this.pairs.indexOf(pair);
22960
- if (index < 0) {
22961
- throw new Error(`FXFiller pair ${pair.token0}/${pair.token1}: not a live market`);
22962
- }
22963
- const remaining = this.pairs.filter((_, i) => i !== index);
22964
- if (remaining.length === 0) {
22965
- throw new Error("FXFiller requires at least one trading pair \u2014 the last market cannot be removed live");
22966
- }
22967
- this.pairs.splice(index, 1);
22968
- }
22969
22867
  // =========================================================================
22970
22868
  // Lifecycle
22971
22869
  // =========================================================================
@@ -23728,7 +23626,7 @@ var TokenSender = class {
23728
23626
  });
23729
23627
  }
23730
23628
  }
23731
- const result = await this.submit(chain, calls);
23629
+ const result = await this.submitCalls(chain, calls);
23732
23630
  this.logger.warn({ chain, token, amount, to, redeemed, ...result }, "Operator send executed");
23733
23631
  return { ...result, redeemed };
23734
23632
  }
@@ -23784,16 +23682,23 @@ var TokenSender = class {
23784
23682
  }
23785
23683
  return null;
23786
23684
  }
23787
- async submit(chain, calls) {
23685
+ /**
23686
+ * Executes a batch from the filler wallet: sponsored UserOp first, native tx
23687
+ * when no sponsor is available. Shared by every operator-initiated outbound flow.
23688
+ */
23689
+ async submitCalls(chain, calls) {
23788
23690
  if (this.userOpSender?.canSponsor(chain)) {
23691
+ const bootstrap = await this.permit2Bootstrap(chain);
23692
+ const batch = bootstrap ? [bootstrap, ...calls] : calls;
23789
23693
  const result = await this.userOpSender.trySendSponsored({
23790
23694
  chain,
23791
- callData: (0, import_sdk16.encodeERC7821ExecuteBatch)(calls),
23695
+ callData: (0, import_sdk16.encodeERC7821ExecuteBatch)(batch),
23792
23696
  gas: {
23793
23697
  verificationGasLimit: 60000n,
23794
- callGasLimit: 450000n * BigInt(calls.length),
23698
+ callGasLimit: 450000n * BigInt(batch.length),
23795
23699
  preVerificationGas: 150000n
23796
- }
23700
+ },
23701
+ permitBootstrap: !!bootstrap
23797
23702
  });
23798
23703
  if (result) return { txHash: result.txHash, sponsored: true };
23799
23704
  this.logger.warn({ chain }, "Sponsored send unavailable, sending native tx");
@@ -23815,7 +23720,1009 @@ var TokenSender = class {
23815
23720
  if (receipt.status !== "success") throw new Error(`Send tx reverted: ${tx}`);
23816
23721
  return { txHash: tx, sponsored: false };
23817
23722
  }
23723
+ /**
23724
+ * The `approve(Permit2, max)` to run first when the wallet has never approved Permit2 for its fee
23725
+ * token on this chain, and that token has an EIP-2612 permit. The op then pays its gas with a permit
23726
+ * and installs the approval, so a wallet with no native can still send; later ops use Permit2.
23727
+ * Undefined otherwise, or when the check fails, and the send goes ahead as before.
23728
+ */
23729
+ async permit2Bootstrap(chain) {
23730
+ const paymaster = this.configService?.getSimplexPaymasterAddress(chain);
23731
+ if (!this.configService || !paymaster) return void 0;
23732
+ try {
23733
+ const pending = await resolvePendingPermit2Approval(
23734
+ this.clientManager.getPublicClient(chain),
23735
+ this.solver,
23736
+ paymaster,
23737
+ chain,
23738
+ this.configService
23739
+ );
23740
+ if (!pending?.permitCapable) return void 0;
23741
+ return {
23742
+ target: pending.token,
23743
+ value: 0n,
23744
+ data: (0, import_viem30.encodeFunctionData)({
23745
+ abi: ERC20_ABI,
23746
+ functionName: "approve",
23747
+ args: [pending.spender, import_viem30.maxUint256]
23748
+ })
23749
+ };
23750
+ } catch (error) {
23751
+ this.logger.warn({ chain, error }, "Could not check whether this send must install the Permit2 approval");
23752
+ return void 0;
23753
+ }
23754
+ }
23755
+ };
23756
+
23757
+ // src/services/BridgeTopUp.ts
23758
+ var import_sdk17 = require("@hyperbridge/sdk");
23759
+ var import_node_crypto2 = require("node:crypto");
23760
+ var import_promises = require("node:timers/promises");
23761
+ var import_viem31 = require("viem");
23762
+ var import_chains2 = require("viem/chains");
23763
+ var SIMPLEX_REFERRER = (0, import_viem31.stringToHex)("Simplex", { size: 32 });
23764
+ var BRIDGE_TOP_UP_CHAIN = "EVM-56";
23765
+ var BRIDGE_TOKEN_BSC = "0x5b0c50fDd52ECC0d4c682c441eaBaD41FfDEABBB";
23766
+ var BRIDGE_DECIMALS = 18;
23767
+ var NEXUS_STATE_MACHINE = "POLKADOT-3367";
23768
+ var NEXUS_CONSENSUS_STATE = "DOT0";
23769
+ var NEXUS_WS_URL = "wss://nexus.rpc.polytope.technology";
23770
+ var NEXUS_UNIT = 10n ** 6n;
23771
+ var QUOTE_SLIPPAGE_BPS = 100n;
23772
+ var ORDER_WINDOW_SECONDS = 30 * 60;
23773
+ var AUCTION_TIME_MS = 15e3;
23774
+ var REFUND_POLL_MS = 1e4;
23775
+ var RECEIPT_TIMEOUT_MS = 12e4;
23776
+ var MAX_EVENTS = 50;
23777
+ var DELIVERY_POLL_MS = 15e3;
23778
+ var DELIVERY_RETRY_MS = 6e4;
23779
+ var SELF_RELAY_GRACE_MS = 9e4;
23780
+ var RELAYER_DELIVERY_WAIT_MS = 3 * 6e4;
23781
+ var STATUS_BACKFILL_MS = 30 * 6e4;
23782
+ var NETWORK_TIMEOUT_MS = 3e4;
23783
+ var LOG_CHUNK_BLOCKS = 300n;
23784
+ var FILL_SEARCH_BEFORE_MS = 3 * 6e4;
23785
+ var FILL_SEARCH_AFTER_MS = 6e4;
23786
+ var ORDER_FILLED_EVENT = import_sdk17.IntentGatewayABI.find(
23787
+ (item) => item.type === "event" && item.name === "OrderFilled"
23788
+ );
23789
+ var DELIVERY_REQUEST_QUERY = `query TopUpDelivery($hash: String!) {
23790
+ requestV2s(
23791
+ filter: {
23792
+ source: { equalTo: "EVM-56" }
23793
+ dest: { equalTo: "POLKADOT-3367" }
23794
+ statusMetadata: { some: { transactionHash: { equalTo: $hash } } }
23795
+ }
23796
+ ) {
23797
+ nodes { commitment }
23798
+ }
23799
+ }`;
23800
+ var ORDER_QUERY2 = `query TopUpOrder($commitment: String!) {
23801
+ iOrderV3s(filter: { commitment: { equalTo: $commitment } }) {
23802
+ nodes {
23803
+ status
23804
+ statusMetadata { nodes { status timestamp transactionHash filler } }
23805
+ }
23806
+ }
23807
+ }`;
23808
+ var CALL_TUPLE = [
23809
+ {
23810
+ type: "tuple[]",
23811
+ components: [
23812
+ { name: "to", type: "address" },
23813
+ { name: "value", type: "uint256" },
23814
+ { name: "data", type: "bytes" }
23815
+ ]
23816
+ }
23817
+ ];
23818
+ var PENDING_STAGES = /* @__PURE__ */ new Set([
23819
+ "placing",
23820
+ "awaiting_bids",
23821
+ "filling",
23822
+ "delivering",
23823
+ "unfilled",
23824
+ "reclaiming"
23825
+ ]);
23826
+ function isBridgeTopUpPending(stage) {
23827
+ return PENDING_STAGES.has(stage);
23828
+ }
23829
+ function encodeBridgeTopUpCall(bridgeToken, recipient, amount) {
23830
+ const data = (0, import_viem31.encodeFunctionData)({
23831
+ abi: import_sdk17.HyperFungibleTokenABI,
23832
+ functionName: "send",
23833
+ args: [
23834
+ {
23835
+ dest: (0, import_viem31.toHex)(NEXUS_STATE_MACHINE),
23836
+ to: recipient,
23837
+ amount,
23838
+ timeout: 0n,
23839
+ relayerFee: 0n,
23840
+ data: "0x"
23841
+ }
23842
+ ]
23843
+ });
23844
+ return (0, import_viem31.encodeAbiParameters)(CALL_TUPLE, [[{ to: bridgeToken, value: 0n, data }]]);
23845
+ }
23846
+ function bidPreview(bid) {
23847
+ return {
23848
+ solverAddress: bid.solverAddress,
23849
+ outputs: bid.outputs.map((output) => ({ token: output.token, amount: output.amount.toString() }))
23850
+ };
23851
+ }
23852
+ function bridgeTopUpOutput(quotedOut) {
23853
+ const afterSlippage = quotedOut * (10000n - QUOTE_SLIPPAGE_BPS) / 10000n;
23854
+ return afterSlippage / NEXUS_UNIT * NEXUS_UNIT;
23855
+ }
23856
+ function serializeOrder(order) {
23857
+ return JSON.stringify(order, (_key, value) => typeof value === "bigint" ? { $bigint: value.toString() } : value);
23858
+ }
23859
+ function deserializeOrder(json) {
23860
+ return JSON.parse(
23861
+ json,
23862
+ (_key, value) => value && typeof value === "object" && typeof value.$bigint === "string" ? BigInt(value.$bigint) : value
23863
+ );
23864
+ }
23865
+ var BridgeTopUp = class {
23866
+ constructor(deps) {
23867
+ this.deps = deps;
23868
+ this.gateways = /* @__PURE__ */ new Map();
23869
+ /** Serializes state writes so a later stage never lands before an earlier one. */
23870
+ this.writes = Promise.resolve();
23871
+ this.logger = moduleLogger(deps.loggers, "bridge-top-up");
23872
+ }
23873
+ get bridgeToken() {
23874
+ return this.deps.configService.getAssetBySymbol(BRIDGE_TOP_UP_CHAIN, "BRIDGE") ?? BRIDGE_TOKEN_BSC;
23875
+ }
23876
+ /** Loads the stored top-up and resumes driving it when it was left open. Never rejects. */
23877
+ restore() {
23878
+ this.loaded ?? (this.loaded = (async () => {
23879
+ try {
23880
+ this.record = (await this.deps.state.get()).bridgeTopUp;
23881
+ } catch (err) {
23882
+ this.logger.warn({ err }, "Could not read the stored BRIDGE top-up");
23883
+ return;
23884
+ }
23885
+ const record = this.record;
23886
+ if (record?.stage === "failed" && record.placeTxHash && !record.commitment && record.sessionPrivateKey) {
23887
+ void this.recoverPlacement();
23888
+ return;
23889
+ }
23890
+ if (!record || !isBridgeTopUpPending(record.stage)) return;
23891
+ if (record.stage === "placing" && !record.placeTxHash) {
23892
+ await this.update({
23893
+ stage: "failed",
23894
+ error: "Simplex restarted before the order was placed. Nothing was escrowed."
23895
+ });
23896
+ return;
23897
+ }
23898
+ if (record.stage === "placing") {
23899
+ void this.recoverPlacement();
23900
+ return;
23901
+ }
23902
+ if (record.stage === "awaiting_bids" || record.stage === "filling") void this.resumeOrder();
23903
+ if (record.stage === "reclaiming") void this.driveReclaim();
23904
+ if (record.stage === "delivering") void this.resumeDelivery();
23905
+ })());
23906
+ return this.loaded;
23907
+ }
23908
+ async current() {
23909
+ await this.restore();
23910
+ return this.record ? publicState(this.record) : void 0;
23911
+ }
23912
+ /** Networks the filler runs and has delegated its account on. */
23913
+ async sourceChains() {
23914
+ const chainIds = this.deps.configService.getConfiguredChainIds();
23915
+ const delegated = await Promise.all(
23916
+ chainIds.map((chainId) => this.deps.delegation.isDelegated(`EVM-${chainId}`).catch(() => false))
23917
+ );
23918
+ return chainIds.filter((_, index) => delegated[index]);
23919
+ }
23920
+ /** USDC and USDT wallet balances on every network a top-up can be paid from. */
23921
+ async sources() {
23922
+ const rows = (await this.sourceChains()).flatMap(
23923
+ (chainId) => ["USDC", "USDT"].map(async (token) => {
23924
+ const chain = `EVM-${chainId}`;
23925
+ let tokenAddress;
23926
+ let decimals;
23927
+ try {
23928
+ ;
23929
+ ({ tokenAddress, decimals } = this.payToken(chain, token));
23930
+ } catch {
23931
+ return void 0;
23932
+ }
23933
+ const reserve = this.reserve(chain, tokenAddress);
23934
+ const row = { chain, chainId, token, address: tokenAddress, reserve: (0, import_viem31.formatUnits)(reserve, decimals) };
23935
+ try {
23936
+ const balance = await this.walletBalance(chain, tokenAddress);
23937
+ return {
23938
+ ...row,
23939
+ balance: (0, import_viem31.formatUnits)(balance, decimals),
23940
+ spendable: (0, import_viem31.formatUnits)(balance > reserve ? balance - reserve : 0n, decimals)
23941
+ };
23942
+ } catch (err) {
23943
+ this.logger.debug({ err, chain, token }, "Could not read a top-up source balance");
23944
+ return { ...row, balance: null, spendable: null };
23945
+ }
23946
+ })
23947
+ );
23948
+ return (await Promise.all(rows)).filter((row) => row !== void 0);
23949
+ }
23950
+ async quote(params) {
23951
+ const { tokenAddress, decimals } = this.payToken(params.chain, params.token);
23952
+ const amountIn = (0, import_viem31.parseUnits)(params.amount, decimals);
23953
+ if (amountIn <= 0n) throw new Error("Amount must be greater than 0");
23954
+ const gateway = await this.gateway(params.chain);
23955
+ const quote = await gateway.quoteIntent({ tokenIn: tokenAddress, tokenOut: this.bridgeToken, amountIn });
23956
+ const bridgeOut = quote.fillable ? bridgeTopUpOutput(quote.amountOut) : 0n;
23957
+ const rate = quote.amountIn > 0n ? Number((0, import_viem31.formatUnits)(quote.amountOut, BRIDGE_DECIMALS)) / Number((0, import_viem31.formatUnits)(quote.amountIn, decimals)) : 0;
23958
+ return {
23959
+ chain: params.chain,
23960
+ token: params.token,
23961
+ tokenAddress,
23962
+ amountIn: (0, import_viem31.formatUnits)(amountIn, decimals),
23963
+ bridgeOut: (0, import_viem31.formatUnits)(bridgeOut, BRIDGE_DECIMALS),
23964
+ rate: rate.toString(),
23965
+ fillable: quote.fillable && bridgeOut > 0n,
23966
+ maxFillableIn: (0, import_viem31.formatUnits)(quote.maxFillableIn, decimals),
23967
+ route: quote.route
23968
+ };
23969
+ }
23970
+ /** Re-quotes, then places and drives the order in the background. Returns its state. */
23971
+ async start(params) {
23972
+ if (!(await this.sourceChains()).includes((0, import_sdk17.getChainId)(params.chain) ?? -1)) {
23973
+ throw new Error(`${params.chain} is not a network the filler fills on`);
23974
+ }
23975
+ await this.restore();
23976
+ if (this.record && isBridgeTopUpPending(this.record.stage)) {
23977
+ throw new Error("A BRIDGE top-up is already pending");
23978
+ }
23979
+ const quote = await this.quote(params);
23980
+ if (!quote.fillable) {
23981
+ throw new Error(`The orderbook cannot fill ${quote.amountIn} ${params.token} (max ${quote.maxFillableIn})`);
23982
+ }
23983
+ const { tokenAddress, decimals } = this.payToken(params.chain, params.token);
23984
+ const amountIn = (0, import_viem31.parseUnits)(quote.amountIn, decimals);
23985
+ const balance = await this.balanceOf(params.chain, tokenAddress);
23986
+ if (balance < amountIn) {
23987
+ const reserve = (0, import_viem31.formatUnits)(this.reserve(params.chain, tokenAddress), decimals);
23988
+ throw new Error(
23989
+ `Not enough ${params.token}: ${(0, import_viem31.formatUnits)(balance, decimals)} spendable after the ${reserve} ${params.token} kept for gas, need ${quote.amountIn}`
23990
+ );
23991
+ }
23992
+ const order = await this.buildOrder(params.chain, tokenAddress, amountIn, (0, import_viem31.parseUnits)(quote.bridgeOut, BRIDGE_DECIMALS));
23993
+ if (this.record && isBridgeTopUpPending(this.record.stage)) {
23994
+ throw new Error("A BRIDGE top-up is already pending");
23995
+ }
23996
+ const now = Date.now();
23997
+ this.record = {
23998
+ id: (0, import_node_crypto2.randomUUID)(),
23999
+ chain: params.chain,
24000
+ token: params.token,
24001
+ amountIn: quote.amountIn,
24002
+ bridgeOut: quote.bridgeOut,
24003
+ stage: "placing",
24004
+ createdAt: now,
24005
+ updatedAt: now,
24006
+ stageTimes: { placing: now },
24007
+ order: serializeOrder(order)
24008
+ };
24009
+ await this.persist();
24010
+ void this.place(order, tokenAddress);
24011
+ return publicState(this.record);
24012
+ }
24013
+ /** Cancels an unfilled order and refunds its escrowed input to the filler wallet. */
24014
+ async reclaim() {
24015
+ await this.restore();
24016
+ const record = this.record;
24017
+ if (!record || record.stage !== "unfilled") throw new Error("There is no unfilled top-up to reclaim");
24018
+ await this.update({ stage: "reclaiming", error: void 0 });
24019
+ void this.driveReclaim();
24020
+ return publicState(this.record);
24021
+ }
24022
+ /**
24023
+ * Places the order, then tracks it. The SDK builds the `placeOrder` calldata and the
24024
+ * session key; the receipt is read back here, through the filler's own multi-endpoint
24025
+ * client, rather than by the SDK from one endpoint, where a node that has not caught up
24026
+ * with the new block fails the read and loses track of an order that did land.
24027
+ */
24028
+ async place(order, tokenIn) {
24029
+ await this.drive(async (gateway) => {
24030
+ const gen = gateway.executeBest(order, SIMPLEX_REFERRER, { auctionTimeMs: AUCTION_TIME_MS });
24031
+ const first = await gen.next();
24032
+ await gen.return();
24033
+ if (first.done || first.value.status !== "AWAITING_PLACE_ORDER") {
24034
+ throw new Error("The SDK did not produce a placement transaction");
24035
+ }
24036
+ const placement = first.value;
24037
+ await this.update({ sessionPrivateKey: placement.sessionPrivateKey });
24038
+ const calls = this.placementCalls(order, tokenIn, placement);
24039
+ const { txHash } = await this.deps.tokenSender.submitCalls(this.record.chain, calls);
24040
+ await this.update({ placeTxHash: txHash });
24041
+ await this.recordPlacement(order, txHash);
24042
+ await this.trackOrder(gateway);
24043
+ });
24044
+ }
24045
+ /**
24046
+ * Reads the placement receipt and stores the order as the gateway committed it. Throws
24047
+ * when the transaction placed no order, so the top-up is marked failed.
24048
+ */
24049
+ async recordPlacement(order, txHash) {
24050
+ const receipt = await this.deps.clientManager.getPublicClient(this.record.chain).waitForTransactionReceipt({ hash: txHash, timeout: RECEIPT_TIMEOUT_MS });
24051
+ const [placed] = (0, import_viem31.parseEventLogs)({ abi: import_sdk17.IntentGatewayABI, logs: receipt.logs, eventName: "OrderPlaced" });
24052
+ if (!placed) throw new Error(`Transaction ${txHash} placed no order`);
24053
+ const canonical = (0, import_sdk17.deriveCanonicalPlacedOrder)(order, placed.args);
24054
+ await this.update({
24055
+ stage: "awaiting_bids",
24056
+ commitment: canonical.id,
24057
+ order: serializeOrder(canonical),
24058
+ error: void 0
24059
+ });
24060
+ }
24061
+ /** Continues an order whose placement landed but was not recorded. */
24062
+ async recoverPlacement() {
24063
+ await this.drive(async (gateway) => {
24064
+ const record = this.record;
24065
+ await this.recordPlacement(deserializeOrder(record.order), record.placeTxHash);
24066
+ await this.trackOrder(gateway);
24067
+ });
24068
+ }
24069
+ async resumeDelivery() {
24070
+ await this.drive(() => this.deliver());
24071
+ }
24072
+ async resumeOrder() {
24073
+ await this.drive((gateway) => this.trackOrder(gateway));
24074
+ }
24075
+ /** Follows a placed order through bids to a fill, or to its deadline. */
24076
+ async trackOrder(gateway) {
24077
+ const record = this.record;
24078
+ const order = deserializeOrder(record.order);
24079
+ if (await gateway.isOrderFilled(order)) {
24080
+ await this.update({ stage: "delivering" });
24081
+ await this.deliver();
24082
+ return;
24083
+ }
24084
+ if (!record.sessionPrivateKey) {
24085
+ await this.update({ stage: "unfilled", error: "The order's session key was not saved, so it cannot be resumed" });
24086
+ return;
24087
+ }
24088
+ const gen = gateway.resumeBest(order, {
24089
+ sessionPrivateKey: record.sessionPrivateKey,
24090
+ auctionTimeMs: AUCTION_TIME_MS
24091
+ });
24092
+ for await (const value of gen) {
24093
+ if (await this.applyExecutionUpdate(value)) break;
24094
+ }
24095
+ if (this.record.stage === "delivering") await this.deliver();
24096
+ }
24097
+ /**
24098
+ * Carries the bought BRIDGE to Hyperbridge. The order's output call dispatched it from
24099
+ * BSC with no relayer fee. Relayers may still deliver it; when none has shortly after BSC
24100
+ * is final on Hyperbridge, the filler relays it itself: a request into Hyperbridge goes
24101
+ * in as an unsigned extrinsic and costs nothing. Runs until the request is delivered.
24102
+ */
24103
+ async deliver() {
24104
+ let relayedAt = 0;
24105
+ while (!await this.syncDelivery()) {
24106
+ const finalized = this.record.deliveryStatuses?.find((status2) => status2.status === "SOURCE_FINALIZED");
24107
+ const due = finalized && Date.now() - finalized.timestamp > SELF_RELAY_GRACE_MS;
24108
+ if (due && Date.now() - relayedAt > DELIVERY_RETRY_MS) {
24109
+ relayedAt = Date.now();
24110
+ await this.selfRelay(this.record.deliveryCommitment).catch((err) => {
24111
+ this.logger.warn({ err }, "Self-relay of the BRIDGE to Hyperbridge failed; retrying");
24112
+ });
24113
+ }
24114
+ await (0, import_promises.setTimeout)(DELIVERY_POLL_MS);
24115
+ }
24116
+ const id = this.record.id;
24117
+ while (this.record?.id === id && this.needsStatusBackfill()) {
24118
+ await (0, import_promises.setTimeout)(DELIVERY_POLL_MS);
24119
+ await this.syncDelivery().catch((err) => this.logger.debug({ err }, "BRIDGE delivery backfill failed"));
24120
+ }
24121
+ }
24122
+ /** Delivered, but the indexer has yet to record the delivery, and not for too long. */
24123
+ needsStatusBackfill() {
24124
+ const record = this.record;
24125
+ if (!record || record.stage !== "delivered" || !record.deliveryCommitment) return false;
24126
+ if (record.deliveryStatuses?.some((status2) => status2.status === "DESTINATION")) return false;
24127
+ return Date.now() - (record.stageTimes?.delivered ?? 0) < STATUS_BACKFILL_MS;
24128
+ }
24129
+ /**
24130
+ * Reads where the order and its BRIDGE are, from the indexer and the chains, and records
24131
+ * it. Shared by the driver and {@link sync}, and never runs twice at once. True once the
24132
+ * BRIDGE has reached Hyperbridge.
24133
+ */
24134
+ syncDelivery() {
24135
+ this.deliverySync ?? (this.deliverySync = this.readDelivery().finally(() => {
24136
+ this.deliverySync = void 0;
24137
+ }));
24138
+ return this.deliverySync;
24139
+ }
24140
+ async readDelivery() {
24141
+ const record = this.record;
24142
+ if (record.stage !== "delivering" && record.stage !== "delivered") return false;
24143
+ if (!record.fillTxHash) {
24144
+ const fillTxHash = await this.findFill();
24145
+ if (!fillTxHash) return false;
24146
+ await this.update({ fillTxHash });
24147
+ }
24148
+ if (!record.deliveryCommitment) {
24149
+ await this.update({ deliveryCommitment: await this.deliveryRequest(record.fillTxHash) });
24150
+ }
24151
+ const hyperbridge = await this.hyperbridge();
24152
+ const request = await withTimeout(
24153
+ this.ismpClient(hyperbridge).queryRequestWithStatus(record.deliveryCommitment),
24154
+ "the BRIDGE request status"
24155
+ );
24156
+ const statuses = (request?.statuses ?? []).filter((status2) => DELIVERY_STATUSES.has(status2.status)).map((status2) => ({
24157
+ status: status2.status,
24158
+ timestamp: toMs(status2.metadata.timestamp),
24159
+ txHash: status2.metadata.transactionHash
24160
+ }));
24161
+ if (statuses.length > 0) await this.update({ deliveryStatuses: statuses });
24162
+ const delivered = statuses.some((status2) => status2.status === "DESTINATION") || !!await withTimeout(hyperbridge.queryRequestReceipt(record.deliveryCommitment), "the request receipt");
24163
+ if (delivered && record.stage !== "delivered") await this.update({ stage: "delivered", error: void 0 });
24164
+ return delivered;
24165
+ }
24166
+ /**
24167
+ * The HyperFungibleToken request the fill dispatched to Hyperbridge. The indexer finds it by
24168
+ * the fill's transaction, as the explorer's search does; the fill's own `Sent` log is the
24169
+ * fallback while the indexer catches up.
24170
+ */
24171
+ async deliveryRequest(fillTxHash) {
24172
+ const response = await withTimeout(
24173
+ fetch(this.deps.indexerUrl, {
24174
+ method: "POST",
24175
+ headers: { "content-type": "application/json" },
24176
+ body: JSON.stringify({ query: DELIVERY_REQUEST_QUERY, variables: { hash: fillTxHash } })
24177
+ }),
24178
+ "the indexer"
24179
+ );
24180
+ const body = await response.json();
24181
+ const indexed = body.data?.requestV2s?.nodes[0]?.commitment;
24182
+ if (indexed) return indexed;
24183
+ const fill = await withTimeout(this.bsc().getTransactionReceipt({ hash: fillTxHash }), "the fill receipt");
24184
+ const [sent] = (0, import_viem31.parseEventLogs)({ abi: import_sdk17.HyperFungibleTokenABI, logs: fill.logs, eventName: "Sent" });
24185
+ if (!sent) throw new Error(`The fill ${fillTxHash} sent no BRIDGE to Hyperbridge`);
24186
+ return sent.args.commitment;
24187
+ }
24188
+ /**
24189
+ * BSC, read through every endpoint there is: the filler need not run it, and a single public
24190
+ * endpoint refuses receipts it considers old.
24191
+ */
24192
+ bsc() {
24193
+ this.bscClient ?? (this.bscClient = (0, import_viem31.createPublicClient)({
24194
+ chain: import_chains2.bsc,
24195
+ transport: (0, import_viem31.fallback)(this.bscProofRpcUrls().map((url) => (0, import_viem31.http)(url, { timeout: 1e4, retryCount: 0 })))
24196
+ }));
24197
+ return this.bscClient;
24198
+ }
24199
+ /**
24200
+ * The transaction that filled the order. The executor reports it only for a fill this
24201
+ * process selected, so it comes from the indexer, or, while the indexer has yet to reach
24202
+ * BSC's latest blocks, from the gateway's `OrderFilled` log around when the fill was seen.
24203
+ */
24204
+ async findFill() {
24205
+ const record = this.record;
24206
+ const indexed = (await this.indexedOrder())?.find((status2) => status2.status === "FILLED");
24207
+ if (indexed) {
24208
+ if (indexed.filler && !record.filler) await this.update({ filler: indexed.filler });
24209
+ return indexed.transactionHash;
24210
+ }
24211
+ const seenAt = record.events?.find((event) => event.status === "FILLED")?.timestamp ?? record.stageTimes?.delivering;
24212
+ if (!seenAt) return void 0;
24213
+ const client = this.bsc();
24214
+ const [latest, blockTime] = await Promise.all([
24215
+ withTimeout(client.getBlock(), "the latest BSC block"),
24216
+ this.blockTimeSeconds()
24217
+ ]);
24218
+ const blockAt = (ms) => latest.number - BigInt(Math.ceil((Number(latest.timestamp) - ms / 1e3) / blockTime));
24219
+ const from = blockAt(seenAt - FILL_SEARCH_BEFORE_MS);
24220
+ const to = (() => {
24221
+ const end = blockAt(seenAt + FILL_SEARCH_AFTER_MS);
24222
+ return end < latest.number ? end : latest.number;
24223
+ })();
24224
+ for (let start = from; start <= to; start += LOG_CHUNK_BLOCKS) {
24225
+ const end = start + LOG_CHUNK_BLOCKS - 1n < to ? start + LOG_CHUNK_BLOCKS - 1n : to;
24226
+ const logs = await withTimeout(
24227
+ client.getLogs({
24228
+ address: this.deps.configService.getIntentGatewayAddress(BRIDGE_TOP_UP_CHAIN),
24229
+ event: ORDER_FILLED_EVENT,
24230
+ args: { commitment: record.commitment },
24231
+ fromBlock: start,
24232
+ toBlock: end
24233
+ }),
24234
+ "the fill on BSC"
24235
+ );
24236
+ if (logs[0]) return logs[0].transactionHash;
24237
+ }
24238
+ return void 0;
24239
+ }
24240
+ /** The order's statuses as the indexer has them: placed, filled, redeemed or refunded. */
24241
+ async indexedOrder() {
24242
+ const response = await withTimeout(
24243
+ fetch(this.deps.indexerUrl, {
24244
+ method: "POST",
24245
+ headers: { "content-type": "application/json" },
24246
+ body: JSON.stringify({ query: ORDER_QUERY2, variables: { commitment: this.record.commitment } })
24247
+ }),
24248
+ "the indexer"
24249
+ );
24250
+ const body = await response.json();
24251
+ return body.data?.iOrderV3s?.nodes[0]?.statusMetadata.nodes.map((node) => ({
24252
+ status: node.status,
24253
+ timestamp: toMs(node.timestamp),
24254
+ transactionHash: node.transactionHash,
24255
+ filler: node.filler ?? void 0
24256
+ }));
24257
+ }
24258
+ /**
24259
+ * Brings a pending top-up up to date from the chains and the indexer, for a dashboard that
24260
+ * is showing it: someone may have filled, delivered or refunded it while this process was
24261
+ * not looking. One sync at a time; a call made while one runs waits for that one.
24262
+ */
24263
+ sync() {
24264
+ this.syncing ?? (this.syncing = this.reconcile().catch((err) => this.logger.debug({ err }, "BRIDGE top-up sync failed")).finally(() => {
24265
+ this.syncing = void 0;
24266
+ }));
24267
+ return this.syncing;
24268
+ }
24269
+ async reconcile() {
24270
+ await this.restore();
24271
+ const record = this.record;
24272
+ if (this.needsStatusBackfill()) {
24273
+ await this.syncDelivery();
24274
+ return;
24275
+ }
24276
+ if (!record || !isBridgeTopUpPending(record.stage) || !record.commitment) return;
24277
+ if (record.stage === "awaiting_bids" || record.stage === "filling") {
24278
+ const statuses = await this.indexedOrder();
24279
+ const fill = statuses?.find((status2) => status2.status === "FILLED");
24280
+ if (fill) {
24281
+ await this.update({
24282
+ stage: "delivering",
24283
+ fillTxHash: fill.transactionHash,
24284
+ filler: fill.filler ?? record.filler,
24285
+ stageTimes: { ...record.stageTimes, delivering: fill.timestamp }
24286
+ });
24287
+ void this.resumeDelivery();
24288
+ } else if (statuses?.some((status2) => status2.status === "REFUNDED")) {
24289
+ await this.update({ stage: "reclaimed" });
24290
+ }
24291
+ return;
24292
+ }
24293
+ if (record.stage === "delivering") {
24294
+ await this.syncDelivery();
24295
+ return;
24296
+ }
24297
+ if (record.stage === "unfilled" || record.stage === "reclaiming") {
24298
+ const gateway = await this.gateway(record.chain);
24299
+ await this.refunded(gateway, deserializeOrder(record.order));
24300
+ }
24301
+ }
24302
+ /**
24303
+ * Submits the request to Hyperbridge with a proof from BSC state. Public BSC endpoints
24304
+ * differ in whether they serve `eth_getProof` for recent blocks, so each is tried in turn.
24305
+ */
24306
+ async selfRelay(commitment) {
24307
+ const hyperbridge = await this.hyperbridge();
24308
+ const failures = [];
24309
+ for (const rpcUrl of this.bscProofRpcUrls()) {
24310
+ if (await withTimeout(hyperbridge.queryRequestReceipt(commitment), "the request receipt")) return;
24311
+ try {
24312
+ const client = this.ismpClient(hyperbridge, rpcUrl);
24313
+ const receipt = await withTimeout(client.aggregateTransactionWithCommitment(commitment), "the relay", 12e4);
24314
+ await this.update({ deliveryTxHash: receipt.transactionHash });
24315
+ return;
24316
+ } catch (err) {
24317
+ const reason = err instanceof Error ? err.message.split("\n")[0] : String(err);
24318
+ failures.push(`${new URL(rpcUrl).host}: ${reason}`);
24319
+ }
24320
+ }
24321
+ throw new Error(`Could not relay the BRIDGE to Hyperbridge (${failures.join("; ")})`);
24322
+ }
24323
+ /** The operator's BSC endpoints first, then the free public ones. */
24324
+ bscProofRpcUrls() {
24325
+ const urls = [
24326
+ ...this.deps.configService.getRpcUrls(BRIDGE_TOP_UP_CHAIN),
24327
+ ...chainByChainId((0, import_sdk17.getChainId)(BRIDGE_TOP_UP_CHAIN))?.defaultRpcUrls ?? []
24328
+ ];
24329
+ return urls.filter((url, index) => url && urls.indexOf(url) === index);
24330
+ }
24331
+ /** Records an executor update. True when the order reached a final state. */
24332
+ async applyExecutionUpdate(value) {
24333
+ switch (value.status) {
24334
+ case "NEW_BID":
24335
+ case "BIDS_RECEIVED": {
24336
+ const bids = value.status === "NEW_BID" ? [value.bid] : value.bids;
24337
+ const bidCount = value.status === "BIDS_RECEIVED" ? Number(value.bidCount ?? bids.length) : void 0;
24338
+ await this.recordEvent({ status: value.status, bidCount, bids: bids.map(bidPreview) });
24339
+ if (bidCount !== void 0) await this.update({ bidCount: Math.max(this.record.bidCount ?? 0, bidCount) });
24340
+ return false;
24341
+ }
24342
+ case "BID_SELECTED":
24343
+ await this.recordEvent({
24344
+ status: "BID_SELECTED",
24345
+ selectedSolver: value.selectedSolver,
24346
+ userOpHash: value.userOpHash,
24347
+ txHash: value.transactionHash
24348
+ });
24349
+ await this.update({ stage: "filling", fillTxHash: value.transactionHash });
24350
+ return false;
24351
+ case "FILLED":
24352
+ await this.recordEvent({
24353
+ status: "FILLED",
24354
+ selectedSolver: value.selectedSolver,
24355
+ userOpHash: value.userOpHash,
24356
+ txHash: value.transactionHash
24357
+ });
24358
+ await this.update({
24359
+ stage: "delivering",
24360
+ fillTxHash: value.transactionHash ?? this.record.fillTxHash,
24361
+ error: void 0
24362
+ });
24363
+ return true;
24364
+ case "EXPIRED":
24365
+ case "FAILED":
24366
+ await this.update({ stage: "unfilled", error: value.error });
24367
+ return true;
24368
+ case "CANCELLED":
24369
+ await this.update({ stage: "reclaimed" });
24370
+ return true;
24371
+ default:
24372
+ return false;
24373
+ }
24374
+ }
24375
+ /**
24376
+ * Recovers an unfilled order's escrow, following the Hyperbridge app's recovery: the SDK
24377
+ * cancel flow from the paying chain (BSC's expiry height finalized, the cancel sent, the
24378
+ * cancellation finalized on the source, verified and finalized on Hyperbridge), then the
24379
+ * refund. A relayer normally delivers that refund; if none has within a few minutes of
24380
+ * Hyperbridge finalizing, the filler submits Hyperbridge's response itself. Each step is
24381
+ * recorded as it lands, and a restart resumes from the SDK's persisted progress.
24382
+ */
24383
+ async driveReclaim() {
24384
+ await this.drive(async (gateway) => {
24385
+ const record = this.record;
24386
+ const order = deserializeOrder(record.order);
24387
+ if (await this.refunded(gateway, order)) return;
24388
+ const sameChain = record.chain === BRIDGE_TOP_UP_CHAIN;
24389
+ const ismp = await this.cancelClient(record.chain);
24390
+ const gen = gateway.cancelOrder(order, ismp);
24391
+ let input;
24392
+ while (true) {
24393
+ const { value, done } = await gen.next(input);
24394
+ input = void 0;
24395
+ if (done) break;
24396
+ switch (value.status) {
24397
+ case "DESTINATION_FINALIZED":
24398
+ await this.recordRecoveryStep("DESTINATION_FINALIZED");
24399
+ break;
24400
+ case "AWAITING_CANCEL_TRANSACTION": {
24401
+ if (this.record.reclaimTxHash && !this.hasRecoveryStep("ORDER_CANCELED")) {
24402
+ input = this.record.reclaimTxHash;
24403
+ break;
24404
+ }
24405
+ const calls = [];
24406
+ const quote = await gateway.quoteCancelOrder(order);
24407
+ if (value.value === 0n && quote.relayerFee > 0n) {
24408
+ const feeToken = await this.feeToken(record.chain);
24409
+ calls.push({
24410
+ target: feeToken,
24411
+ value: 0n,
24412
+ data: (0, import_viem31.encodeFunctionData)({
24413
+ abi: ERC20_ABI,
24414
+ functionName: "approve",
24415
+ args: [value.to, quote.relayerFee]
24416
+ })
24417
+ });
24418
+ }
24419
+ calls.push({ target: value.to, value: value.value, data: value.data });
24420
+ const { txHash } = await this.deps.tokenSender.submitCalls(record.chain, calls);
24421
+ await this.update({ reclaimTxHash: txHash });
24422
+ input = txHash;
24423
+ break;
24424
+ }
24425
+ case "CANCEL_STARTED":
24426
+ await this.recordRecoveryStep("ORDER_CANCELED", value.receipt.transactionHash);
24427
+ break;
24428
+ case "SOURCE_FINALIZED":
24429
+ case "HYPERBRIDGE_DELIVERED":
24430
+ case "HYPERBRIDGE_FINALIZED": {
24431
+ const metadata = value.metadata;
24432
+ await this.recordRecoveryStep(
24433
+ value.status,
24434
+ metadata.transactionHash,
24435
+ metadata.timestamp ? toMs(metadata.timestamp) : void 0
24436
+ );
24437
+ if (value.status === "HYPERBRIDGE_FINALIZED" && metadata.calldata) {
24438
+ await this.update({ claimCalldata: metadata.calldata });
24439
+ }
24440
+ break;
24441
+ }
24442
+ case "CANCELLATION_COMPLETE":
24443
+ await this.recordRecoveryStep("ORDER_CANCELED", value.transactionHash);
24444
+ await this.recordRecoveryStep("FUNDS_CLAIMED", value.transactionHash);
24445
+ break;
24446
+ }
24447
+ }
24448
+ if (sameChain && this.hasRecoveryStep("FUNDS_CLAIMED")) {
24449
+ await this.update({ stage: "reclaimed", error: void 0 });
24450
+ return;
24451
+ }
24452
+ while (!await this.refunded(gateway, order)) {
24453
+ const finalizedAt = this.record.recoverySteps?.find((s) => s.step === "HYPERBRIDGE_FINALIZED")?.timestamp;
24454
+ const claimable = this.record.claimCalldata && finalizedAt && Date.now() - finalizedAt > RELAYER_DELIVERY_WAIT_MS;
24455
+ if (claimable && !this.record.claimTxHash) {
24456
+ await this.claimRefund(record.chain, this.record.claimCalldata);
24457
+ continue;
24458
+ }
24459
+ await (0, import_promises.setTimeout)(REFUND_POLL_MS);
24460
+ }
24461
+ });
24462
+ }
24463
+ /**
24464
+ * Submits Hyperbridge's finalized response to the source chain's ISMP handler, which
24465
+ * refunds the escrow, as the Hyperbridge app's "Claim funds" does.
24466
+ */
24467
+ async claimRefund(chain, calldata) {
24468
+ const params = await withTimeout(
24469
+ this.deps.clientManager.getPublicClient(chain).readContract({
24470
+ abi: EVM_HOST,
24471
+ address: this.deps.configService.getHostAddress(chain),
24472
+ functionName: "hostParams"
24473
+ }),
24474
+ "the host's handler"
24475
+ );
24476
+ const { txHash } = await this.deps.tokenSender.submitCalls(chain, [
24477
+ { target: params.handler, value: 0n, data: calldata }
24478
+ ]);
24479
+ await this.update({ claimTxHash: txHash });
24480
+ }
24481
+ /**
24482
+ * Whether the escrow is back with the filler, from the source gateway or the indexer's
24483
+ * REFUNDED status; records the refund as the recovery's last step when it is.
24484
+ */
24485
+ async refunded(gateway, order) {
24486
+ const onChain = await withTimeout(gateway.isOrderRefunded(order), "the refund").catch(() => false);
24487
+ const indexed = (await this.indexedOrder().catch(() => void 0))?.find((s) => s.status === "REFUNDED");
24488
+ if (!onChain && !indexed) return false;
24489
+ if (!this.hasRecoveryStep("FUNDS_CLAIMED")) {
24490
+ await this.recordRecoveryStep(
24491
+ "FUNDS_CLAIMED",
24492
+ this.record.claimTxHash ?? indexed?.transactionHash,
24493
+ indexed?.timestamp
24494
+ );
24495
+ }
24496
+ await this.update({ stage: "reclaimed", error: void 0 });
24497
+ return true;
24498
+ }
24499
+ hasRecoveryStep(step) {
24500
+ return !!this.record?.recoverySteps?.some((entry) => entry.step === step);
24501
+ }
24502
+ /** Records a recovery step once, at the time it landed (or now). */
24503
+ async recordRecoveryStep(step, txHash, timestamp) {
24504
+ if (this.hasRecoveryStep(step)) return;
24505
+ const steps = [...this.record.recoverySteps ?? [], { step, txHash, timestamp: timestamp ?? Date.now() }];
24506
+ await this.update({ recoverySteps: steps });
24507
+ }
24508
+ /** Runs one background flow for the record; a failure is recorded rather than thrown. */
24509
+ async drive(flow) {
24510
+ const record = this.record;
24511
+ if (!record || this.driving === record.id) return;
24512
+ this.driving = record.id;
24513
+ try {
24514
+ await flow(await this.gateway(record.chain));
24515
+ } catch (err) {
24516
+ const error = err instanceof Error ? err.message : String(err);
24517
+ this.logger.warn({ err, id: record.id, stage: record.stage }, "BRIDGE top-up step failed");
24518
+ if (record.stage === "placing" && !record.placeTxHash) {
24519
+ await this.update({ stage: "failed", error });
24520
+ } else if (record.stage === "delivering") {
24521
+ await this.update({ error: `Delivery to Hyperbridge is retrying: ${error}` });
24522
+ setTimeout(() => void this.resumeDelivery(), DELIVERY_RETRY_MS);
24523
+ } else if (record.stage === "reclaiming") {
24524
+ await this.update({ error: `Recovery is retrying: ${error}` });
24525
+ setTimeout(() => void this.driveReclaim(), DELIVERY_RETRY_MS);
24526
+ } else if (record.stage === "placing") {
24527
+ await this.update({ stage: "failed", error: `${error} (placement tx ${record.placeTxHash})` });
24528
+ } else {
24529
+ await this.update({ stage: "unfilled", error });
24530
+ }
24531
+ } finally {
24532
+ if (this.driving === record.id) this.driving = void 0;
24533
+ this.logger.info(publicState(record), "BRIDGE top-up step finished");
24534
+ }
24535
+ }
24536
+ /** Keeps what the executor reported, for the timeline's offers and its links. */
24537
+ async recordEvent(event) {
24538
+ const events = [...this.record.events ?? [], { ...event, timestamp: Date.now() }];
24539
+ await this.update({ events: events.slice(-MAX_EVENTS) });
24540
+ }
24541
+ async update(patch) {
24542
+ if (!this.record) return;
24543
+ const now = Date.now();
24544
+ if (patch.stage && patch.stage !== this.record.stage) {
24545
+ patch = { ...patch, stageTimes: { ...this.record.stageTimes, [patch.stage]: now } };
24546
+ }
24547
+ Object.assign(this.record, patch, { updatedAt: now });
24548
+ await this.persist();
24549
+ }
24550
+ persist() {
24551
+ const snapshot = this.record ? { ...this.record } : void 0;
24552
+ const write = this.writes.then(() => patchRuntimeState(this.deps.state, { bridgeTopUp: snapshot }));
24553
+ this.writes = write.catch((err) => this.logger.warn({ err }, "Could not save the BRIDGE top-up"));
24554
+ return write.then(
24555
+ () => void 0,
24556
+ () => void 0
24557
+ );
24558
+ }
24559
+ async buildOrder(chain, tokenIn, amountIn, bridgeOut) {
24560
+ const dispatcher = await this.callDispatcher();
24561
+ const bsc = this.bsc();
24562
+ const [block, blockTime] = await Promise.all([bsc.getBlockNumber(), this.blockTimeSeconds()]);
24563
+ return {
24564
+ user: (0, import_sdk17.bytes20ToBytes32)(this.deps.solver),
24565
+ source: chain,
24566
+ destination: BRIDGE_TOP_UP_CHAIN,
24567
+ deadline: block + BigInt(Math.ceil(ORDER_WINDOW_SECONDS / blockTime)),
24568
+ nonce: 0n,
24569
+ fees: 0n,
24570
+ session: "0x0000000000000000000000000000000000000000",
24571
+ predispatch: { assets: [], call: "0x" },
24572
+ inputs: [{ token: (0, import_sdk17.bytes20ToBytes32)(tokenIn), amount: amountIn }],
24573
+ output: {
24574
+ beneficiary: (0, import_sdk17.bytes20ToBytes32)(dispatcher),
24575
+ assets: [{ token: (0, import_sdk17.bytes20ToBytes32)(this.bridgeToken), amount: bridgeOut }],
24576
+ call: encodeBridgeTopUpCall(this.bridgeToken, this.deps.substratePublicKey, bridgeOut)
24577
+ }
24578
+ };
24579
+ }
24580
+ /**
24581
+ * Approvals for the gateway to pull the input and the order fee, then `placeOrder`.
24582
+ * The fee token is often the input token itself; one approval then covers both.
24583
+ */
24584
+ placementCalls(order, tokenIn, placement) {
24585
+ const amountIn = order.inputs[0].amount;
24586
+ const approvals = /* @__PURE__ */ new Map([[tokenIn.toLowerCase(), amountIn]]);
24587
+ if (placement.feeTokenAmount > 0n) {
24588
+ const fee = placement.feeTokenAddress.toLowerCase();
24589
+ approvals.set(fee, (approvals.get(fee) ?? 0n) + placement.feeTokenAmount);
24590
+ }
24591
+ const calls = [...approvals].map(([token, amount]) => ({
24592
+ target: token,
24593
+ value: 0n,
24594
+ data: (0, import_viem31.encodeFunctionData)({ abi: ERC20_ABI, functionName: "approve", args: [placement.to, amount] })
24595
+ }));
24596
+ calls.push({ target: placement.to, value: placement.value, data: placement.data });
24597
+ return calls;
24598
+ }
24599
+ payToken(chain, token) {
24600
+ const { configService } = this.deps;
24601
+ const tokenAddress = token === "USDC" ? configService.getUsdcAsset(chain) : configService.getUsdtAsset(chain);
24602
+ if (!tokenAddress || /^0x0+$/.test(tokenAddress)) throw new Error(`${token} is not configured on ${chain}`);
24603
+ const decimals = token === "USDC" ? configService.getUsdcDecimals(chain) : configService.getUsdtDecimals(chain);
24604
+ return { tokenAddress, decimals };
24605
+ }
24606
+ async walletBalance(chain, token) {
24607
+ return await this.deps.clientManager.getPublicClient(chain).readContract({
24608
+ abi: ERC20_ABI,
24609
+ address: token,
24610
+ functionName: "balanceOf",
24611
+ args: [this.deps.solver]
24612
+ });
24613
+ }
24614
+ /** What the paymaster pulls for gas on the sponsored path, which a top-up must leave. */
24615
+ reserve(chain, token) {
24616
+ return paymasterReserveForToken(chain, token.toLowerCase(), this.deps.configService);
24617
+ }
24618
+ /** Spendable wallet balance: the paymaster's gas reserve is held back. */
24619
+ async balanceOf(chain, token) {
24620
+ const balance = await this.walletBalance(chain, token);
24621
+ const reserve = this.reserve(chain, token);
24622
+ return balance > reserve ? balance - reserve : 0n;
24623
+ }
24624
+ async feeToken(chain) {
24625
+ return await this.deps.clientManager.getPublicClient(chain).readContract({
24626
+ abi: [{ type: "function", name: "feeToken", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] }],
24627
+ address: this.deps.configService.getHostAddress(chain),
24628
+ functionName: "feeToken"
24629
+ });
24630
+ }
24631
+ /** The CallDispatcher the BSC gateway runs output calls through, read from the gateway itself. */
24632
+ callDispatcher() {
24633
+ this.dispatcher ?? (this.dispatcher = (async () => {
24634
+ const params = await this.bsc().readContract({
24635
+ abi: INTENT_GATEWAY_V2_ABI,
24636
+ address: this.deps.configService.getIntentGatewayAddress(BRIDGE_TOP_UP_CHAIN),
24637
+ functionName: "params"
24638
+ });
24639
+ return params.dispatcher;
24640
+ })().catch((err) => {
24641
+ this.dispatcher = void 0;
24642
+ throw err;
24643
+ }));
24644
+ return this.dispatcher;
24645
+ }
24646
+ async blockTimeSeconds() {
24647
+ const client = this.bsc();
24648
+ const latest = await client.getBlock();
24649
+ const earlier = await client.getBlock({ blockNumber: latest.number - 100n });
24650
+ const seconds = Number(latest.timestamp - earlier.timestamp) / 100;
24651
+ return seconds > 0 ? seconds : 1;
24652
+ }
24653
+ evmChain(chain, rpcUrl) {
24654
+ const { configService } = this.deps;
24655
+ const bundlerUrl = chain === BRIDGE_TOP_UP_CHAIN ? configService.getBundlerUrl(chain) ?? hyperbridgeBundlerUrl((0, import_sdk17.getChainId)(chain)) : void 0;
24656
+ return import_sdk17.EvmChain.fromParams({
24657
+ chainId: (0, import_sdk17.getChainId)(chain),
24658
+ host: configService.getHostAddress(chain),
24659
+ rpcUrl: rpcUrl ?? configService.getRpcUrl(chain),
24660
+ bundlerUrl
24661
+ });
24662
+ }
24663
+ gateway(source) {
24664
+ let gateway = this.gateways.get(source);
24665
+ if (!gateway) {
24666
+ gateway = (async () => import_sdk17.IntentGateway.create(
24667
+ this.evmChain(source),
24668
+ this.evmChain(BRIDGE_TOP_UP_CHAIN),
24669
+ await this.deps.hyperbridge,
24670
+ { storagePath: this.deps.storagePath }
24671
+ ))().catch((err) => {
24672
+ this.gateways.delete(source);
24673
+ throw err;
24674
+ });
24675
+ this.gateways.set(source, gateway);
24676
+ }
24677
+ return gateway;
24678
+ }
24679
+ hyperbridge() {
24680
+ this.hyperbridgeChain ?? (this.hyperbridgeChain = withTimeout(import_sdk17.SubstrateChain.connect({
24681
+ wsUrl: this.deps.configService.getHyperbridgeWsUrl() ?? NEXUS_WS_URL,
24682
+ consensusStateId: NEXUS_CONSENSUS_STATE,
24683
+ hasher: "Keccak",
24684
+ stateMachineId: NEXUS_STATE_MACHINE
24685
+ }), "Hyperbridge").catch((err) => {
24686
+ this.hyperbridgeChain = void 0;
24687
+ throw err;
24688
+ }));
24689
+ return this.hyperbridgeChain;
24690
+ }
24691
+ /** Tracks requests from BSC into Hyperbridge, the route the bought BRIDGE takes. */
24692
+ ismpClient(hyperbridge, rpcUrl) {
24693
+ return new import_sdk17.IsmpClient({
24694
+ queryClient: (0, import_sdk17.createQueryClient)({ url: this.deps.indexerUrl }),
24695
+ source: this.evmChain(BRIDGE_TOP_UP_CHAIN, rpcUrl),
24696
+ dest: hyperbridge,
24697
+ hyperbridge
24698
+ });
24699
+ }
24700
+ /** Tracks the reclaim's cancellation, which runs between the paying chain and BSC. */
24701
+ async cancelClient(source) {
24702
+ return new import_sdk17.IsmpClient({
24703
+ queryClient: (0, import_sdk17.createQueryClient)({ url: this.deps.indexerUrl }),
24704
+ source: this.evmChain(source),
24705
+ dest: this.evmChain(BRIDGE_TOP_UP_CHAIN),
24706
+ hyperbridge: await this.hyperbridge()
24707
+ });
24708
+ }
23818
24709
  };
24710
+ function publicState(record) {
24711
+ const { order: _order, sessionPrivateKey: _key, ...rest } = record;
24712
+ return { ...rest, pending: isBridgeTopUpPending(record.stage) };
24713
+ }
24714
+ var DELIVERY_STATUSES = /* @__PURE__ */ new Set(["SOURCE", "SOURCE_FINALIZED", "DESTINATION"]);
24715
+ function toMs(timestamp) {
24716
+ const value = Number(timestamp ?? 0);
24717
+ return value > 0 && value < 1e12 ? value * 1e3 : value;
24718
+ }
24719
+ function withTimeout(promise, what, ms = NETWORK_TIMEOUT_MS) {
24720
+ let timer;
24721
+ const timeout = new Promise((_, reject) => {
24722
+ timer = setTimeout(() => reject(new Error(`Timed out reading ${what}`)), ms);
24723
+ });
24724
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
24725
+ }
23819
24726
 
23820
24727
  // src/services/bundler-preflight.ts
23821
24728
  var BUNDLER_PREFLIGHT_TIMEOUT_MS = 1e4;
@@ -23973,7 +24880,30 @@ var LimitOrderLifecycle = class {
23973
24880
 
23974
24881
  // src/services/BalanceProvider.ts
23975
24882
  var import_node_events = require("node:events");
23976
- var import_viem31 = require("viem");
24883
+ var import_viem33 = require("viem");
24884
+
24885
+ // src/services/native-setup.ts
24886
+ var import_viem32 = require("viem");
24887
+ async function nativeSetupNeed(client, configService, wallet, chain, nativeBalance) {
24888
+ const paymaster = configService.getSimplexPaymasterAddress(chain);
24889
+ if (!paymaster) return null;
24890
+ const gasPrice = await client.getGasPrice();
24891
+ if (nativeBalance >= (DELEGATION_TX_GAS_FLOOR + BATCHED_APPROVE_GAS) * gasPrice) return null;
24892
+ const pending = await resolvePendingPermit2Approval(client, wallet, paymaster, chain, configService);
24893
+ if (!pending || pending.permitCapable) return null;
24894
+ const solverAccount = configService.getSolverAccountContractAddress(chain);
24895
+ const code = await client.getCode({ address: wallet });
24896
+ const delegated = !!solverAccount && code?.toLowerCase() === (0, import_viem32.concat)(["0xef0100", solverAccount]).toLowerCase();
24897
+ const cost = (delegated ? APPROVE_TX_GAS : DELEGATION_TX_GAS_FLOOR + BATCHED_APPROVE_GAS) * gasPrice;
24898
+ if (nativeBalance >= cost) return null;
24899
+ const usdc = configService.getUsdcAsset(chain);
24900
+ return {
24901
+ required: Number((0, import_viem32.formatEther)(cost * 2n)),
24902
+ feeToken: pending.token.toLowerCase() === usdc.toLowerCase() ? "USDC" : "USDT"
24903
+ };
24904
+ }
24905
+
24906
+ // src/services/BalanceProvider.ts
23977
24907
  var BalanceProvider = class extends import_node_events.EventEmitter {
23978
24908
  constructor(options) {
23979
24909
  super();
@@ -24021,8 +24951,14 @@ var BalanceProvider = class extends import_node_events.EventEmitter {
24021
24951
  })
24022
24952
  );
24023
24953
  }
24954
+ let tokens = {};
24955
+ try {
24956
+ tokens = await this.options.tokens();
24957
+ } catch (err) {
24958
+ this.logger.warn({ err }, "Could not read the orderbook's books; reading USDC and USDT balances only");
24959
+ }
24024
24960
  const fxExoticByChain = /* @__PURE__ */ new Map();
24025
- for (const [chainKey, addrs] of Object.entries(this.options.token1)) {
24961
+ for (const [chainKey, addrs] of Object.entries(tokens)) {
24026
24962
  const id = Number.parseInt(chainKey.replace("EVM-", ""), 10);
24027
24963
  if (!isNaN(id)) fxExoticByChain.set(id, addrs);
24028
24964
  }
@@ -24066,7 +25002,14 @@ var BalanceProvider = class extends import_node_events.EventEmitter {
24066
25002
  try {
24067
25003
  const native = await client.getBalance({ address: fillerAddr });
24068
25004
  const symbol = nativeTokenSymbol(chainId);
24069
- row.native = { symbol, amount: Number.parseFloat((0, import_viem31.formatUnits)(native, 18)) };
25005
+ row.native = { symbol, amount: Number.parseFloat((0, import_viem33.formatUnits)(native, 18)) };
25006
+ const setup = await nativeSetupNeed(client, this.options.configService, fillerAddr, chain, native).catch(
25007
+ (err) => {
25008
+ this.logger.debug({ err, chainId }, "Could not check whether the chain needs native gas for setup");
25009
+ return null;
25010
+ }
25011
+ );
25012
+ if (setup) row.nativeSetup = setup;
24070
25013
  } catch (err) {
24071
25014
  issues.push({ chainId, source: "native", message: errorMessage(err) });
24072
25015
  }
@@ -24137,7 +25080,7 @@ var BalanceProvider = class extends import_node_events.EventEmitter {
24137
25080
  functionName: "balanceOf",
24138
25081
  args: [fillerAddr]
24139
25082
  });
24140
- wallet = Number.parseFloat((0, import_viem31.formatUnits)(balance, definition.decimals));
25083
+ wallet = Number.parseFloat((0, import_viem33.formatUnits)(balance, definition.decimals));
24141
25084
  } catch (err) {
24142
25085
  issues.push({
24143
25086
  chainId,
@@ -24165,8 +25108,8 @@ var BalanceProvider = class extends import_node_events.EventEmitter {
24165
25108
  available: wallet !== null && vaultDataAvailable ? Math.max(wallet - walletReserve, 0) + vaultAvailable : null,
24166
25109
  vaults: positions.map((position) => ({
24167
25110
  address: position.vault,
24168
- position: Number.parseFloat((0, import_viem31.formatUnits)(position.positionAssets, position.decimals)),
24169
- available: Number.parseFloat((0, import_viem31.formatUnits)(position.availableAssets, position.decimals)),
25111
+ position: Number.parseFloat((0, import_viem33.formatUnits)(position.positionAssets, position.decimals)),
25112
+ available: Number.parseFloat((0, import_viem33.formatUnits)(position.availableAssets, position.decimals)),
24170
25113
  acceptsDeposits: position.acceptsDeposits
24171
25114
  })),
24172
25115
  status: complete ? "fresh" : wallet !== null || positions.length > 0 ? "partial" : "unavailable"
@@ -24209,8 +25152,8 @@ var BalanceProvider = class extends import_node_events.EventEmitter {
24209
25152
  const api = await coprocessor.queryApi();
24210
25153
  const account = await api.query.system.account(address);
24211
25154
  const decimals = api.registry.chainDecimals[0] ?? 12;
24212
- const free = Number.parseFloat((0, import_viem31.formatUnits)(BigInt(account.data.free.toString()), decimals));
24213
- const reserved = Number.parseFloat((0, import_viem31.formatUnits)(BigInt(account.data.reserved.toString()), decimals));
25155
+ const free = Number.parseFloat((0, import_viem33.formatUnits)(BigInt(account.data.free.toString()), decimals));
25156
+ const reserved = Number.parseFloat((0, import_viem33.formatUnits)(BigInt(account.data.reserved.toString()), decimals));
24214
25157
  this.snapshot = { ...this.snapshot, hyperbridge: { address, free, reserved } };
24215
25158
  } catch (err) {
24216
25159
  this.logger.warn({ err }, "Failed to fetch Hyperbridge balance");
@@ -24230,7 +25173,7 @@ function isConfiguredAddress(address) {
24230
25173
  }
24231
25174
  function sumFormatted(positions, field) {
24232
25175
  return positions.reduce(
24233
- (total, position) => total + Number.parseFloat((0, import_viem31.formatUnits)(position[field], position.decimals)),
25176
+ (total, position) => total + Number.parseFloat((0, import_viem33.formatUnits)(position[field], position.decimals)),
24234
25177
  0
24235
25178
  );
24236
25179
  }
@@ -24474,7 +25417,7 @@ var DEFAULT_INDEXER_URLS = {
24474
25417
  mainnet: "https://nexus.indexer.polytope.technology/",
24475
25418
  testnet: "https://gargantua.indexer.polytope.technology/"
24476
25419
  };
24477
- var ORDER_QUERY2 = `
25420
+ var ORDER_QUERY3 = `
24478
25421
  query OrderSummary($commitment: String!) {
24479
25422
  iOrderV3s(filter: { commitment: { equalTo: $commitment } }) {
24480
25423
  nodes {
@@ -24567,7 +25510,7 @@ async function fetchOrder(fetchImpl, indexerUrl, commitment) {
24567
25510
  const response = await fetchImpl(indexerUrl, {
24568
25511
  method: "POST",
24569
25512
  headers: { "content-type": "application/json" },
24570
- body: JSON.stringify({ query: ORDER_QUERY2, variables: { commitment } }),
25513
+ body: JSON.stringify({ query: ORDER_QUERY3, variables: { commitment } }),
24571
25514
  signal: AbortSignal.timeout(15e3)
24572
25515
  });
24573
25516
  if (!response.ok) throw new Error(`Indexer responded ${response.status}`);
@@ -24673,12 +25616,8 @@ async function bootFiller(config, options) {
24673
25616
  }
24674
25617
  }
24675
25618
  };
24676
- const retired = retiredPairKeys(config.pairs ?? []);
24677
- if (retired.length > 0) {
24678
- logger.warn(
24679
- { keys: retired },
24680
- "These [[pairs]] keys are no longer read; prices come from limit orders, and the filler fills nothing until one is posted"
24681
- );
25619
+ if ("pairs" in config) {
25620
+ logger.warn("[[pairs]] is no longer read and can be deleted from the config");
24682
25621
  }
24683
25622
  logger.info("Resolving chain IDs from RPC endpoints...");
24684
25623
  const resolvedChains = await resolveChainConfigs(config.chains, {
@@ -24783,25 +25722,6 @@ async function bootFiller(config, options) {
24783
25722
  }
24784
25723
  logger.info("Initializing trading engine...");
24785
25724
  const assetRegistry = new AssetRegistry(configService, config.assets);
24786
- const configuredChainNames = resolvedChains.map((chain) => `EVM-${chain.chainId}`);
24787
- if (config.pairs?.length) {
24788
- assertPairSymbolsResolve(config.pairs, assetRegistry, configuredChainNames);
24789
- }
24790
- const adminStrategies = [];
24791
- const tradingPairs = (config.pairs ?? []).map((pair) => ({
24792
- token0: pair.token0,
24793
- token1: pair.token1
24794
- }));
24795
- tradingPairs.forEach((pair, pairIndex) => {
24796
- adminStrategies.push({
24797
- index: pairIndex,
24798
- pairIndex,
24799
- exotic: `${pair.token0}/${pair.token1}`,
24800
- token0: pair.token0,
24801
- token1: pair.token1,
24802
- sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
24803
- });
24804
- });
24805
25725
  const confirmationPolicy = assertConfirmationCoverage(
24806
25726
  config.confirmationPolicies,
24807
25727
  resolvedChains.map((c) => c.chainId)
@@ -24812,7 +25732,6 @@ async function bootFiller(config, options) {
24812
25732
  configService,
24813
25733
  chainClientManager,
24814
25734
  contractService,
24815
- tradingPairs,
24816
25735
  assetRegistry,
24817
25736
  { confirmationPolicy, fundingVenues, limitOrders: options.data.limitOrders }
24818
25737
  );
@@ -24920,8 +25839,8 @@ async function bootFiller(config, options) {
24920
25839
  if (vaultVenue) {
24921
25840
  vaultVenue.onTx = ({ chain, kind, txHash, sponsored, movements }) => {
24922
25841
  const rows = movements.length > 0 ? movements.map((move) => {
24923
- const underlying = { token: move.symbol, amount: (0, import_viem32.formatUnits)(move.amount, move.decimals) };
24924
- const shares = move.shares === void 0 ? { token: null, amount: null } : { token: move.shareSymbol, amount: (0, import_viem32.formatUnits)(move.shares, move.shareDecimals) };
25842
+ const underlying = { token: move.symbol, amount: (0, import_viem34.formatUnits)(move.amount, move.decimals) };
25843
+ const shares = move.shares === void 0 ? { token: null, amount: null } : { token: move.shareSymbol, amount: (0, import_viem34.formatUnits)(move.shares, move.shareDecimals) };
24925
25844
  const out = kind === "sweep" ? underlying : shares;
24926
25845
  const back = kind === "sweep" ? shares : underlying;
24927
25846
  return { token: out.token, amount: out.amount, to: move.vault, tokenIn: back.token, amountIn: back.amount };
@@ -24931,21 +25850,21 @@ async function bootFiller(config, options) {
24931
25850
  }
24932
25851
  };
24933
25852
  }
24934
- const token1 = {};
24935
- for (const pair of config.pairs ?? []) {
24936
- if (normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)) continue;
24937
- for (const chainName2 of configuredChainNames) {
24938
- const address = assetRegistry.getAddress(pair.token1, chainName2);
24939
- if (!address) continue;
24940
- const list = token1[chainName2] ?? (token1[chainName2] = []);
24941
- if (!list.includes(address)) list.push(address);
25853
+ const bookTokens = async () => {
25854
+ const { books } = await limitOrderService.books();
25855
+ const symbols = new Set(books.flatMap((book) => [normalizeSymbol(book.base), normalizeSymbol(book.quote)]));
25856
+ const tokens = {};
25857
+ for (const chainId of configService.getConfiguredChainIds()) {
25858
+ const chainName2 = `EVM-${chainId}`;
25859
+ tokens[chainName2] = [...symbols].flatMap((symbol) => assetRegistry.getAddress(symbol, chainName2) ?? []);
24942
25860
  }
24943
- }
25861
+ return tokens;
25862
+ };
24944
25863
  const balanceProvider = new BalanceProvider({
24945
25864
  chainClientManager,
24946
25865
  configService,
24947
25866
  fillerAddress: runtimeSigner.address,
24948
- token1,
25867
+ tokens: bookTokens,
24949
25868
  vaultBalances: vaultVenue,
24950
25869
  hyperbridge: intentFiller.hyperbridgeConnection,
24951
25870
  substratePrivateKey: config.simplex.substratePrivateKey
@@ -24988,7 +25907,6 @@ async function bootFiller(config, options) {
24988
25907
  logger.info(
24989
25908
  {
24990
25909
  chains: resolvedChains.map((c) => c.chainId),
24991
- pairs: (config.pairs ?? []).map((p) => `${p.token0}/${p.token1}`),
24992
25910
  maxConcurrentOrders: config.simplex.maxConcurrentOrders ?? DEFAULT_MAX_CONCURRENT_ORDERS,
24993
25911
  watchOnlyChains: watchOnlyChains.length > 0 ? watchOnlyChains : void 0
24994
25912
  },
@@ -25006,12 +25924,32 @@ async function bootFiller(config, options) {
25006
25924
  activity.detach();
25007
25925
  if (options.ownsData) await options.data.close?.();
25008
25926
  };
25927
+ const tokenSender = new TokenSender(
25928
+ chainClientManager,
25929
+ runtimeSigner.address,
25930
+ () => config.vault?.vaults ?? [],
25931
+ userOpSender,
25932
+ configService
25933
+ );
25934
+ const bridgeTopUp = config.simplex.substratePrivateKey && network === "mainnet" ? new BridgeTopUp({
25935
+ clientManager: chainClientManager,
25936
+ delegation: new DelegationService(chainClientManager, configService, runtimeSigner),
25937
+ configService,
25938
+ tokenSender,
25939
+ state: options.data.state,
25940
+ storagePath: (0, import_node_path.join)(options.data.dataDir ?? process.cwd(), ".hyperbridge-cache"),
25941
+ indexerUrl,
25942
+ solver: runtimeSigner.address,
25943
+ substratePublicKey: (0, import_viem34.toHex)((await deriveSubstrateKeyPair(config.simplex.substratePrivateKey)).publicKey),
25944
+ hyperbridge: intentFiller.hyperbridgeConnection,
25945
+ loggers: options.loggers
25946
+ }) : void 0;
25947
+ void bridgeTopUp?.restore();
25009
25948
  return {
25010
25949
  intentFiller,
25011
25950
  limitOrders: limitOrderService,
25012
25951
  balanceProvider,
25013
25952
  vaultVenue,
25014
- adminStrategies,
25015
25953
  haltControls,
25016
25954
  activity,
25017
25955
  data: options.data,
@@ -25023,9 +25961,6 @@ async function bootFiller(config, options) {
25023
25961
  confirmationPolicy,
25024
25962
  loggers: options.loggers,
25025
25963
  assetRegistry,
25026
- engine,
25027
- tradingPairs,
25028
- balanceTokens: token1,
25029
25964
  rebalancingService,
25030
25965
  resolvedChains,
25031
25966
  fillerAddress: runtimeSigner.address,
@@ -25035,13 +25970,8 @@ async function bootFiller(config, options) {
25035
25970
  config,
25036
25971
  configPath: options.configPath,
25037
25972
  startedAt: Date.now(),
25038
- tokenSender: new TokenSender(
25039
- chainClientManager,
25040
- runtimeSigner.address,
25041
- () => config.vault?.vaults ?? [],
25042
- userOpSender,
25043
- configService
25044
- ),
25973
+ tokenSender,
25974
+ bridgeTopUp,
25045
25975
  vaultPreflight: async (vaults) => {
25046
25976
  const byChain = {};
25047
25977
  for (const row of vaults) {
@@ -25453,11 +26383,11 @@ var MemoryDataStore = class {
25453
26383
  };
25454
26384
 
25455
26385
  // src/scanner/order-scanner.ts
25456
- var import_sdk18 = require("@hyperbridge/sdk");
26386
+ var import_sdk19 = require("@hyperbridge/sdk");
25457
26387
 
25458
26388
  // src/scanner/chain-scanner.ts
25459
26389
  var import_async_mutex2 = require("async-mutex");
25460
- var import_sdk17 = require("@hyperbridge/sdk");
26390
+ var import_sdk18 = require("@hyperbridge/sdk");
25461
26391
  var GATEWAY_EVENTS = INTENT_GATEWAY_V2_ABI.filter(
25462
26392
  (item) => item.type === "event" && (item.name === "OrderPlaced" || item.name === "OrderFilled" || item.name === "PartialFill")
25463
26393
  );
@@ -25639,7 +26569,7 @@ var ChainScanner = class {
25639
26569
  return { trace: (message) => this.logger.warn({ chainId: this.target.chainId }, message) };
25640
26570
  }
25641
26571
  async scan() {
25642
- const currentBlock = await (0, import_sdk17.retryPromise)(() => this.quorumClient.getBlockNumber(), {
26572
+ const currentBlock = await (0, import_sdk18.retryPromise)(() => this.quorumClient.getBlockNumber(), {
25643
26573
  maxRetries: 1,
25644
26574
  backoffMs: 250,
25645
26575
  logMessage: `Failed to get current block number on chain ${this.target.chainId}`,
@@ -25660,7 +26590,7 @@ var ChainScanner = class {
25660
26590
  this.enter("logs");
25661
26591
  let logs;
25662
26592
  try {
25663
- logs = await (0, import_sdk17.retryPromise)(
26593
+ logs = await (0, import_sdk18.retryPromise)(
25664
26594
  () => this.quorumClient.getLogs({
25665
26595
  address: this.target.gateway,
25666
26596
  events: GATEWAY_EVENTS,
@@ -25978,7 +26908,7 @@ var OrderScanner = class _OrderScanner {
25978
26908
  throw new Error(`Chain ${chainId} is already in this scanner`);
25979
26909
  }
25980
26910
  const name = `EVM-${chainId}`;
25981
- const gateway = chain.gateway ?? new import_sdk18.ChainConfigService({}).getIntentGatewayAddress(name);
26911
+ const gateway = chain.gateway ?? new import_sdk19.ChainConfigService({}).getIntentGatewayAddress(name);
25982
26912
  const scanner = new ChainScanner(
25983
26913
  { chain: name, chainId, gateway, rpcUrls },
25984
26914
  this.loggers,
@@ -26054,7 +26984,7 @@ var OrderScanner = class _OrderScanner {
26054
26984
  };
26055
26985
 
26056
26986
  // src/data/indexer-inventory.ts
26057
- var import_viem33 = require("viem");
26987
+ var import_viem35 = require("viem");
26058
26988
  var READ_TIMEOUT_MS = 1e4;
26059
26989
  var SETTLED_AFTER_MS = 10 * 6e4;
26060
26990
  var BATCH = 40;
@@ -26131,7 +27061,7 @@ ${queries.join("\n")}
26131
27061
  if (!token) continue;
26132
27062
  let amount;
26133
27063
  try {
26134
- amount = Number((0, import_viem33.formatUnits)(BigInt(row.balance), token.decimals));
27064
+ amount = Number((0, import_viem35.formatUnits)(BigInt(row.balance), token.decimals));
26135
27065
  } catch {
26136
27066
  continue;
26137
27067
  }
@@ -26246,7 +27176,7 @@ var LimitOrderController = class {
26246
27176
  }));
26247
27177
  return this.indexed;
26248
27178
  }
26249
- /** The pairs the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
27179
+ /** The books the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
26250
27180
  books() {
26251
27181
  return this.service.books();
26252
27182
  }
@@ -26278,85 +27208,6 @@ var LimitOrderController = class {
26278
27208
  return cancelled;
26279
27209
  }
26280
27210
  };
26281
- var PairController = class {
26282
- constructor(runtime, persist) {
26283
- this.runtime = runtime;
26284
- this.persist = persist;
26285
- }
26286
- get pairs() {
26287
- return this.runtime.config.pairs ?? [];
26288
- }
26289
- list() {
26290
- return this.pairs.map((pair, index) => ({
26291
- index,
26292
- token0: pair.token0,
26293
- token1: pair.token1,
26294
- sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
26295
- }));
26296
- }
26297
- /**
26298
- * Opens a new market on the running engine.
26299
- *
26300
- * Validated against the same rules boot applies, so a pair accepted here is
26301
- * one a restart would also accept. New `assets` are registered first, because
26302
- * the pair's symbols have to resolve before the market can be hydrated.
26303
- */
26304
- async add(pair, options) {
26305
- const { engine, assetRegistry, adminStrategies, balanceTokens, config } = this.runtime;
26306
- const assets = options?.assets;
26307
- const nextAssets = { ...config.assets ?? {}, ...assets ?? {} };
26308
- const nextPairs = [...this.pairs, pair];
26309
- validatePairConfigs(nextPairs, nextAssets);
26310
- if (assets && Object.keys(assets).length > 0) assetRegistry.addAssets(assets);
26311
- assertPairSymbolsResolve(
26312
- [pair],
26313
- assetRegistry,
26314
- this.runtime.resolvedChains.map((chain) => formatChainKey(chain.chainId))
26315
- );
26316
- const tradingPair = { token0: pair.token0, token1: pair.token1 };
26317
- engine.addPair(tradingPair);
26318
- config.pairs = nextPairs;
26319
- if (assets && Object.keys(assets).length > 0) config.assets = nextAssets;
26320
- if (normalizeSymbol(pair.token0) !== normalizeSymbol(pair.token1)) {
26321
- for (const chain of this.runtime.resolvedChains) {
26322
- const chainName2 = formatChainKey(chain.chainId);
26323
- const address = assetRegistry.getAddress(pair.token1, chainName2);
26324
- if (!address) continue;
26325
- const list = balanceTokens[chainName2] ?? (balanceTokens[chainName2] = []);
26326
- if (!list.includes(address)) list.push(address);
26327
- }
26328
- }
26329
- const pairIndex = nextPairs.length - 1;
26330
- adminStrategies.push({
26331
- index: adminStrategies.reduce((max, s) => Math.max(max, s.index), -1) + 1,
26332
- pairIndex,
26333
- exotic: `${pair.token0}/${pair.token1}`,
26334
- token0: pair.token0,
26335
- token1: pair.token1,
26336
- sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
26337
- });
26338
- await this.persist();
26339
- return this.list()[pairIndex];
26340
- }
26341
- /** Closes a market. Orders already in flight against it still complete. */
26342
- async remove(index) {
26343
- const { engine, tradingPairs, adminStrategies, config } = this.runtime;
26344
- if (index < 0 || index >= this.pairs.length) throw new Error(`Unknown pair ${index}`);
26345
- engine.removePair(tradingPairs[index]);
26346
- config.pairs = this.pairs.filter((_, i) => i !== index);
26347
- const position = adminStrategies.findIndex((s) => s.pairIndex === index);
26348
- if (position >= 0) adminStrategies.splice(position, 1);
26349
- for (const strategy of adminStrategies) {
26350
- if (strategy.pairIndex > index) strategy.pairIndex -= 1;
26351
- }
26352
- await this.persist();
26353
- }
26354
- livePair(index) {
26355
- const pair = this.runtime.tradingPairs[index];
26356
- if (!pair) throw new Error(`Unknown pair ${index}`);
26357
- return pair;
26358
- }
26359
- };
26360
27211
  var ChainController = class {
26361
27212
  constructor(runtime, persist, scanner, ownsScanner) {
26362
27213
  this.runtime = runtime;
@@ -26662,7 +27513,6 @@ var Simplex = class _Simplex extends import_node_events3.EventEmitter {
26662
27513
  this.stopped = false;
26663
27514
  this.logger = runtime.loggers.get("simplex");
26664
27515
  const persist = () => this.persistConfig();
26665
- this.pairs = new PairController(runtime, persist);
26666
27516
  this.chains = new ChainController(runtime, persist, runtime.orderScanner, ownedScanners.orders !== void 0);
26667
27517
  this.vaults = new VaultController(runtime, persist);
26668
27518
  this.assets = new AssetController(runtime, persist);
@@ -26812,8 +27662,7 @@ var Simplex = class _Simplex extends import_node_events3.EventEmitter {
26812
27662
  startedAt: this.runtime.startedAt,
26813
27663
  uptimeSec: (Date.now() - this.runtime.startedAt) / 1e3,
26814
27664
  chains: this.runtime.resolvedChains.map((chain) => chain.chainId),
26815
- watchOnly: this.runtime.intentFiller.getWatchOnly(),
26816
- pairs: this.runtime.config.pairs?.length ?? 0
27665
+ watchOnly: this.runtime.intentFiller.getWatchOnly()
26817
27666
  };
26818
27667
  }
26819
27668
  /** Order-activity history, newest first; pass `beforeId` to page backwards. */
@@ -26891,7 +27740,6 @@ var Simplex = class _Simplex extends import_node_events3.EventEmitter {
26891
27740
  LoggerContext,
26892
27741
  MemoryDataStore,
26893
27742
  OrderScanner,
26894
- PairController,
26895
27743
  RebalanceController,
26896
27744
  SignerType,
26897
27745
  Simplex,
@@ -26910,14 +27758,12 @@ var Simplex = class _Simplex extends import_node_events3.EventEmitter {
26910
27758
  mpcVaultSigner,
26911
27759
  normalizeSymbol,
26912
27760
  parseChainKey,
26913
- pickAnchorStable,
26914
27761
  privateKeySigner,
26915
27762
  registrySymbols,
26916
27763
  secretPhraseSigner,
26917
27764
  turnkeySigner,
26918
27765
  validateAssetDefinitions,
26919
27766
  validateConfig,
26920
- validatePairConfigs,
26921
27767
  validateSecretPhrase,
26922
27768
  validateSignerConfig,
26923
27769
  validateVaultToml,