@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.js CHANGED
@@ -4,7 +4,8 @@ import { createRequire } from 'module'; const require = createRequire(import.met
4
4
  import { EventEmitter as EventEmitter4 } from "node:events";
5
5
 
6
6
  // src/core/boot.ts
7
- import { formatUnits as formatUnits6 } from "viem";
7
+ import { join } from "node:path";
8
+ import { formatUnits as formatUnits7, toHex as toHex5 } from "viem";
8
9
 
9
10
  // src/core/filler.ts
10
11
  import { keccakAsU8a } from "@polkadot/util-crypto";
@@ -707,92 +708,6 @@ var AssetRegistry = class {
707
708
  }
708
709
  };
709
710
 
710
- // src/config/pairs.ts
711
- var RETIRED_PAIR_KEYS = ["bidPriceCurve", "askPriceCurve", "maxOrderSize", "referenceOnly"];
712
- function retiredPairKeys(pairs) {
713
- const carried = /* @__PURE__ */ new Set();
714
- for (const pair of pairs) {
715
- for (const key of RETIRED_PAIR_KEYS) {
716
- if (pair[key] !== void 0) carried.add(key);
717
- }
718
- }
719
- return RETIRED_PAIR_KEYS.filter((key) => carried.has(key));
720
- }
721
- function isKnownSymbol(symbol, userAssets) {
722
- const normalized = normalizeSymbol(symbol);
723
- if (isRegistrySymbol(normalized)) return true;
724
- return Object.keys(userAssets ?? {}).some((key) => normalizeSymbol(key) === normalized);
725
- }
726
- function assertPairSymbolsResolve(pairs, registry, chainNames) {
727
- const pairSymbols = new Set(pairs.flatMap((pair) => [pair.token0, pair.token1]));
728
- for (const pair of pairs) {
729
- for (const symbol of [pair.token0, pair.token1]) {
730
- const resolvesSomewhere = chainNames.some((chainName2) => registry.getAddress(symbol, chainName2) !== null);
731
- if (!resolvesSomewhere) {
732
- throw new Error(
733
- `pairs.${pair.token0}/${pair.token1}: '${symbol}' does not resolve to a deployed contract on any configured chain`
734
- );
735
- }
736
- }
737
- }
738
- for (const chainName2 of chainNames) {
739
- const addressOwner = /* @__PURE__ */ new Map();
740
- for (const symbol of pairSymbols) {
741
- const address = registry.getAddress(symbol, chainName2)?.toLowerCase();
742
- if (!address) continue;
743
- const owner = addressOwner.get(address);
744
- if (owner && normalizeSymbol(owner) !== normalizeSymbol(symbol)) {
745
- throw new Error(
746
- `assets: '${symbol}' and '${owner}' both resolve to ${address} on ${chainName2} \u2014 symbols must map to distinct contracts`
747
- );
748
- }
749
- addressOwner.set(address, symbol);
750
- }
751
- }
752
- }
753
- function pickAnchorStable(pairs, symbol) {
754
- const target = normalizeSymbol(symbol);
755
- for (const stable of USD_STABLE_SYMBOLS) {
756
- const taken = pairs.some((pair) => {
757
- const token0 = normalizeSymbol(pair.token0);
758
- const token1 = normalizeSymbol(pair.token1);
759
- return token0 === stable && token1 === target || token0 === target && token1 === stable;
760
- });
761
- if (!taken) return stable;
762
- }
763
- return null;
764
- }
765
- function validatePairConfigs(pairs, userAssets) {
766
- if (!Array.isArray(pairs) || pairs.length === 0) {
767
- throw new Error("pairs: at least one [[pairs]] entry is required");
768
- }
769
- const seen = /* @__PURE__ */ new Set();
770
- for (const pair of pairs) {
771
- if (!pair.token0 || !pair.token1) {
772
- throw new Error("pairs: each entry needs 'token0' and 'token1' symbols");
773
- }
774
- const token0 = normalizeSymbol(pair.token0);
775
- const token1 = normalizeSymbol(pair.token1);
776
- const label = `${token0}/${token1}`;
777
- if (seen.has(label)) {
778
- throw new Error(`pairs.${label}: pair is declared twice`);
779
- }
780
- if (seen.has(`${token1}/${token0}`)) {
781
- throw new Error(
782
- `pairs.${label}: ${token1}/${token0} is already declared \u2014 a market has one orientation, and a limit order on either side trades it`
783
- );
784
- }
785
- seen.add(label);
786
- for (const symbol of [token0, token1]) {
787
- if (!isKnownSymbol(symbol, userAssets)) {
788
- throw new Error(
789
- `pairs.${label}: unknown symbol '${symbol}' \u2014 the registry ships ${registrySymbols().join(", ")}; anything else needs an [assets.${symbol}] entry`
790
- );
791
- }
792
- }
793
- }
794
- }
795
-
796
711
  // src/config/abis/ERC20.ts
797
712
  var ERC20_ABI = [
798
713
  {
@@ -1610,7 +1525,7 @@ function validateOrderbookConfig(orderbook) {
1610
1525
  function validateConfig(config, cliWatchOnly = false) {
1611
1526
  if ("strategies" in config) {
1612
1527
  throw new Error(
1613
- "[[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])"
1528
+ "[[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]"
1614
1529
  );
1615
1530
  }
1616
1531
  const allChainsWatchOnly2 = cliWatchOnly || config.simplex?.watchOnly === true;
@@ -1675,9 +1590,6 @@ function validateConfig(config, cliWatchOnly = false) {
1675
1590
  if (config.assets) {
1676
1591
  validateAssetDefinitions(config.assets);
1677
1592
  }
1678
- if ((config.pairs?.length ?? 0) > 0) {
1679
- validatePairConfigs(config.pairs, config.assets);
1680
- }
1681
1593
  if (config.confirmationPolicies) {
1682
1594
  void new ConfirmationPolicy(config.confirmationPolicies);
1683
1595
  }
@@ -18437,15 +18349,19 @@ import {
18437
18349
  formatEther,
18438
18350
  maxUint256 as maxUint2562,
18439
18351
  erc20Abi as erc20Abi2,
18440
- BaseError,
18441
- ContractFunctionRevertedError,
18442
- ContractFunctionZeroDataError
18352
+ BaseError as BaseError2,
18353
+ ContractFunctionRevertedError as ContractFunctionRevertedError2,
18354
+ ContractFunctionZeroDataError as ContractFunctionZeroDataError2
18443
18355
  } from "viem";
18444
18356
 
18445
18357
  // src/services/paymaster/types.ts
18446
18358
  import { encodePacked } from "viem";
18447
18359
  var RECOMMENDED_AMOUNT_USD = 5n;
18448
18360
  var VERIFICATION_GAS_LIMIT_PERMIT2 = 200000n;
18361
+ var PERMIT2_VERIFICATION_GAS_BY_CHAIN = { 137: 300000n };
18362
+ function permit2VerificationGasLimit(chainId) {
18363
+ return PERMIT2_VERIFICATION_GAS_BY_CHAIN[chainId] ?? VERIFICATION_GAS_LIMIT_PERMIT2;
18364
+ }
18449
18365
  var VERIFICATION_GAS_LIMIT_PERMIT = 250000n;
18450
18366
  var PERMIT2_DEADLINE_SECONDS = 3600n;
18451
18367
  var POST_OP_GAS_LIMIT_SIMPLEX = 40000n;
@@ -18458,7 +18374,15 @@ function packPaymasterAndData(pm) {
18458
18374
  }
18459
18375
 
18460
18376
  // src/services/paymaster/permit.ts
18461
- import { maxUint256, getContract } from "viem";
18377
+ import {
18378
+ BaseError,
18379
+ ContractFunctionRevertedError,
18380
+ ContractFunctionZeroDataError,
18381
+ hashDomain,
18382
+ maxUint256,
18383
+ pad,
18384
+ toHex as toHex2
18385
+ } from "viem";
18462
18386
 
18463
18387
  // src/config/abis/EIP2612.ts
18464
18388
  import { erc20Abi } from "viem";
@@ -18477,6 +18401,13 @@ var EIP2612_ABI = [
18477
18401
  outputs: [{ internalType: "string", name: "", type: "string" }],
18478
18402
  stateMutability: "view",
18479
18403
  type: "function"
18404
+ },
18405
+ {
18406
+ inputs: [],
18407
+ name: "DOMAIN_SEPARATOR",
18408
+ outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }],
18409
+ stateMutability: "view",
18410
+ type: "function"
18480
18411
  }
18481
18412
  ];
18482
18413
 
@@ -18544,25 +18475,73 @@ function normalizeSignature65(signature) {
18544
18475
  }
18545
18476
 
18546
18477
  // src/services/paymaster/permit.ts
18478
+ var DOMAIN_SHAPES = [
18479
+ {
18480
+ fields: [
18481
+ { name: "name", type: "string" },
18482
+ { name: "version", type: "string" },
18483
+ { name: "chainId", type: "uint256" },
18484
+ { name: "verifyingContract", type: "address" }
18485
+ ],
18486
+ domain: (name, version, chainId, verifyingContract) => ({ name, version, chainId, verifyingContract })
18487
+ },
18488
+ // USDT0 on Polygon: the chain id goes in a salt instead of a chainId field.
18489
+ {
18490
+ fields: [
18491
+ { name: "name", type: "string" },
18492
+ { name: "version", type: "string" },
18493
+ { name: "verifyingContract", type: "address" },
18494
+ { name: "salt", type: "bytes32" }
18495
+ ],
18496
+ domain: (name, version, chainId, verifyingContract) => ({
18497
+ name,
18498
+ version,
18499
+ verifyingContract,
18500
+ salt: pad(toHex2(chainId), { size: 32 })
18501
+ })
18502
+ }
18503
+ ];
18504
+ var FALLBACK_VERSIONS = ["1", "2"];
18505
+ async function readIfPresent(read) {
18506
+ try {
18507
+ return await read();
18508
+ } catch (error) {
18509
+ const isRevert = error instanceof BaseError && error.walk((e) => e instanceof ContractFunctionRevertedError || e instanceof ContractFunctionZeroDataError);
18510
+ if (isRevert) return void 0;
18511
+ throw error;
18512
+ }
18513
+ }
18514
+ async function resolvePermitDomain(client, tokenAddress, chainId) {
18515
+ const read = (functionName) => readIfPresent(() => client.readContract({ address: tokenAddress, abi: EIP2612_ABI, functionName }));
18516
+ const separator = await read("DOMAIN_SEPARATOR");
18517
+ if (!separator) return null;
18518
+ const name = await read("name");
18519
+ if (name === void 0) return null;
18520
+ const version = await read("version");
18521
+ for (const candidate of version === void 0 ? FALLBACK_VERSIONS : [version]) {
18522
+ for (const shape of DOMAIN_SHAPES) {
18523
+ const domain = shape.domain(name, candidate, chainId, tokenAddress);
18524
+ const hash = hashDomain({ domain, types: { EIP712Domain: shape.fields } });
18525
+ if (hash.toLowerCase() === separator.toLowerCase()) return { domain, fields: shape.fields };
18526
+ }
18527
+ }
18528
+ return null;
18529
+ }
18547
18530
  async function signEip2612Permit(client, signer, owner, spender, tokenAddress, value, chainId) {
18548
- const token = getContract({
18549
- client,
18531
+ const permitDomain = await resolvePermitDomain(client, tokenAddress, chainId);
18532
+ if (!permitDomain) {
18533
+ throw new Error(`${tokenAddress} on chain ${chainId} has no EIP-2612 permit domain Simplex can sign for`);
18534
+ }
18535
+ const nonce = await client.readContract({
18550
18536
  address: tokenAddress,
18551
- abi: EIP2612_ABI
18537
+ abi: EIP2612_ABI,
18538
+ functionName: "nonces",
18539
+ args: [owner]
18552
18540
  });
18553
- const [name, version, nonce] = await Promise.all([
18554
- token.read.name(),
18555
- token.read.version(),
18556
- token.read.nonces([owner])
18557
- ]);
18558
18541
  const typedData = {
18559
18542
  types: {
18560
- EIP712Domain: [
18561
- { name: "name", type: "string" },
18562
- { name: "version", type: "string" },
18563
- { name: "chainId", type: "uint256" },
18564
- { name: "verifyingContract", type: "address" }
18565
- ],
18543
+ // Listed explicitly: custody backends hash the domain from this entry, not from `domain`.
18544
+ EIP712Domain: permitDomain.fields,
18566
18545
  Permit: [
18567
18546
  { name: "owner", type: "address" },
18568
18547
  { name: "spender", type: "address" },
@@ -18572,12 +18551,7 @@ async function signEip2612Permit(client, signer, owner, spender, tokenAddress, v
18572
18551
  ]
18573
18552
  },
18574
18553
  primaryType: "Permit",
18575
- domain: {
18576
- name,
18577
- version,
18578
- chainId,
18579
- verifyingContract: tokenAddress
18580
- },
18554
+ domain: permitDomain.domain,
18581
18555
  message: {
18582
18556
  owner,
18583
18557
  spender,
@@ -18651,7 +18625,7 @@ async function buildSimplexPaymasterData(client, walletClient, signer, solverAcc
18651
18625
  );
18652
18626
  }
18653
18627
  if (permit2Allowance < recommended) {
18654
- if (permitBootstrap && await tokenSupportsPermit(client, tokenAddress)) {
18628
+ if (permitBootstrap && await tokenSupportsPermit(client, tokenAddress, chainId)) {
18655
18629
  const pm = await buildPermitMode(
18656
18630
  client,
18657
18631
  signer,
@@ -18687,7 +18661,7 @@ async function resolvePendingPermit2Approval(client, solverAccount, paymasterAdd
18687
18661
  return {
18688
18662
  token: selected.address,
18689
18663
  spender: permit2,
18690
- permitCapable: await tokenSupportsPermit(client, selected.address)
18664
+ permitCapable: await tokenSupportsPermit(client, selected.address, configService.getChainId(chain))
18691
18665
  };
18692
18666
  }
18693
18667
  function isConfigured(address) {
@@ -18707,7 +18681,7 @@ function configuredFeeTokens(chain, configService) {
18707
18681
  }
18708
18682
  async function selectToken(client, solverAccount, tokens, permit2) {
18709
18683
  const balances = [];
18710
- let fallback2 = null;
18684
+ let fallback3 = null;
18711
18685
  for (const token of tokens) {
18712
18686
  const balance = await client.readContract({
18713
18687
  address: token.address,
@@ -18721,12 +18695,12 @@ async function selectToken(client, solverAccount, tokens, permit2) {
18721
18695
  const permit2Allowance = await readAllowance(client, token.address, solverAccount, permit2);
18722
18696
  const candidate = { ...token, permit2Allowance };
18723
18697
  if (permit2Allowance >= RECOMMENDED_AMOUNT_USD * required) return { selected: candidate, balances };
18724
- fallback2 ?? (fallback2 = candidate);
18698
+ fallback3 ?? (fallback3 = candidate);
18725
18699
  } else {
18726
18700
  balances.push({ symbol: token.symbol, balance, required });
18727
18701
  }
18728
18702
  }
18729
- return { selected: fallback2, balances };
18703
+ return { selected: fallback3, balances };
18730
18704
  }
18731
18705
  var PERMIT2_SUPPORT_NEGATIVE_TTL_MS = 5 * 6e4;
18732
18706
  var permit2Support = /* @__PURE__ */ new Map();
@@ -18741,7 +18715,7 @@ async function paymasterSupportsPermit2(client, chainId, paymasterAddress) {
18741
18715
  permit2Support.set(key, { supported: true, checkedAt: Date.now() });
18742
18716
  return true;
18743
18717
  } catch (error) {
18744
- const isRevert = error instanceof BaseError && error.walk((e) => e instanceof ContractFunctionRevertedError || e instanceof ContractFunctionZeroDataError);
18718
+ const isRevert = error instanceof BaseError2 && error.walk((e) => e instanceof ContractFunctionRevertedError2 || e instanceof ContractFunctionZeroDataError2);
18745
18719
  if (isRevert) {
18746
18720
  permit2Support.set(key, { supported: false, checkedAt: Date.now() });
18747
18721
  return false;
@@ -18779,7 +18753,7 @@ async function buildPermit2Mode(signer, p) {
18779
18753
  return {
18780
18754
  paymaster: p.spender,
18781
18755
  paymasterData,
18782
- paymasterVerificationGasLimit: VERIFICATION_GAS_LIMIT_PERMIT2,
18756
+ paymasterVerificationGasLimit: permit2VerificationGasLimit(p.chainId),
18783
18757
  paymasterPostOpGasLimit: POST_OP_GAS_LIMIT_SIMPLEX
18784
18758
  };
18785
18759
  }
@@ -18849,27 +18823,8 @@ async function sendApproveTx(client, walletClient, tokenAddress, spender, amount
18849
18823
  });
18850
18824
  await client.waitForTransactionReceipt({ hash, confirmations: 2 });
18851
18825
  }
18852
- async function tokenSupportsPermit(client, tokenAddress) {
18853
- try {
18854
- await client.readContract({
18855
- address: tokenAddress,
18856
- abi: [
18857
- {
18858
- inputs: [],
18859
- name: "version",
18860
- outputs: [{ type: "string" }],
18861
- stateMutability: "view",
18862
- type: "function"
18863
- }
18864
- ],
18865
- functionName: "version"
18866
- });
18867
- return true;
18868
- } catch (error) {
18869
- const isRevert = error instanceof BaseError && error.walk((e) => e instanceof ContractFunctionRevertedError || e instanceof ContractFunctionZeroDataError);
18870
- if (isRevert) return false;
18871
- throw error;
18872
- }
18826
+ async function tokenSupportsPermit(client, tokenAddress, chainId) {
18827
+ return await resolvePermitDomain(client, tokenAddress, chainId) !== null;
18873
18828
  }
18874
18829
 
18875
18830
  // src/services/paymaster/index.ts
@@ -18885,7 +18840,7 @@ async function buildPaymasterAndData(options) {
18885
18840
  const shortfall = await depositShortfall(
18886
18841
  options,
18887
18842
  simplexAddr,
18888
- (permitBootstrap ? VERIFICATION_GAS_LIMIT_PERMIT : VERIFICATION_GAS_LIMIT_PERMIT2) + POST_OP_GAS_LIMIT_SIMPLEX
18843
+ (permitBootstrap ? VERIFICATION_GAS_LIMIT_PERMIT : permit2VerificationGasLimit(configService.getChainId(chain))) + POST_OP_GAS_LIMIT_SIMPLEX
18889
18844
  );
18890
18845
  if (shortfall) {
18891
18846
  return { paymasterAndData: "0x", type: "none", reason: shortfall };
@@ -19677,12 +19632,12 @@ var ContractInteractionService = class {
19677
19632
  const declared = declaredMs ? declaredMs / 1e3 : void 0;
19678
19633
  const seconds = this.measureBlockTimeSeconds(client).then((measured) => declared ? Math.min(measured, declared) : measured).catch((err) => {
19679
19634
  this.blockTimes.delete(chain);
19680
- const fallback2 = declared ?? FALLBACK_BLOCK_TIME_SECONDS;
19635
+ const fallback3 = declared ?? FALLBACK_BLOCK_TIME_SECONDS;
19681
19636
  this.logger.warn(
19682
- { err, chain, seconds: fallback2 },
19637
+ { err, chain, seconds: fallback3 },
19683
19638
  "Could not measure the block time; using the declared one"
19684
19639
  );
19685
- return fallback2;
19640
+ return fallback3;
19686
19641
  });
19687
19642
  this.blockTimes.set(chain, seconds);
19688
19643
  return seconds;
@@ -19877,7 +19832,7 @@ import {
19877
19832
  BundlerMethod,
19878
19833
  rundlerUserOperationFees
19879
19834
  } from "@hyperbridge/sdk";
19880
- import { toHex as toHex2 } from "viem";
19835
+ import { toHex as toHex3 } from "viem";
19881
19836
  var FALLBACK_VERIFICATION_GAS_LIMIT = 250000n;
19882
19837
  var FALLBACK_CALL_GAS_LIMIT = 1500000n;
19883
19838
  var FALLBACK_PRE_VERIFICATION_GAS = 150000n;
@@ -19952,11 +19907,11 @@ var UserOpSender = class {
19952
19907
  });
19953
19908
  const formattedAuth = auth ? {
19954
19909
  address: auth.address,
19955
- chainId: toHex2(auth.chainId),
19956
- nonce: toHex2(auth.nonce),
19910
+ chainId: toHex3(auth.chainId),
19911
+ nonce: toHex3(auth.nonce),
19957
19912
  r: auth.r,
19958
19913
  s: auth.s,
19959
- yParity: toHex2(auth.yParity)
19914
+ yParity: toHex3(auth.yParity)
19960
19915
  } : void 0;
19961
19916
  const { verificationGasLimit, callGasLimit, preVerificationGas } = gas ?? await this.estimateGas({
19962
19917
  bundlerUrl,
@@ -22886,18 +22841,17 @@ function adjustDecimalsFloor(amount, fromDecimals, toDecimals) {
22886
22841
  if (fromDecimals < toDecimals) return amount * BigInt(10 ** (toDecimals - fromDecimals));
22887
22842
  return amount / BigInt(10 ** (fromDecimals - toDecimals));
22888
22843
  }
22889
- var FXFiller = class _FXFiller {
22844
+ var FXFiller = class {
22890
22845
  /**
22891
22846
  * @param signer Filler's signing account for UserOp signatures.
22892
22847
  * @param configService Network/config provider for addresses and decimals.
22893
22848
  * @param clientManager Used to get viem PublicClients for chains.
22894
22849
  * @param contractService Shared contract interaction service.
22895
- * @param pairs Trading pairs with their bid/ask price policies and per-order caps.
22896
- * @param registry Asset symbol registry resolving pair symbols per chain.
22850
+ * @param registry Asset symbol registry resolving symbols per chain.
22897
22851
  * @param options.confirmationPolicy Optional per-chain confirmation policy for cross-chain orders.
22898
22852
  * @param options.fundingVenues Optional funding venues for on-chain liquidity sourcing.
22899
22853
  */
22900
- constructor(signer, configService, clientManager, contractService, pairs, registry, options) {
22854
+ constructor(signer, configService, clientManager, contractService, registry, options) {
22901
22855
  this.name = "FXFiller";
22902
22856
  /**
22903
22857
  * Consecutive orders where the overfill clamp activated. Dormant: the clamp
@@ -22910,17 +22864,9 @@ var FXFiller = class _FXFiller {
22910
22864
  this.logger = moduleLogger(configService.loggers, "fx-simplex");
22911
22865
  const { confirmationPolicy, fundingVenues = [], limitOrders } = options ?? {};
22912
22866
  this.limitOrders = limitOrders;
22913
- if (pairs.length === 0) {
22914
- throw new Error("FXFiller requires at least one trading pair");
22915
- }
22916
- const seenPairs = /* @__PURE__ */ new Set();
22917
- for (const pair of pairs) {
22918
- _FXFiller.assertPairValid(pair, seenPairs);
22919
- }
22920
22867
  this.configService = configService;
22921
22868
  this.clientManager = clientManager;
22922
22869
  this.contractService = contractService;
22923
- this.pairs = pairs;
22924
22870
  this.registry = registry;
22925
22871
  this.fundingVenues = fundingVenues;
22926
22872
  this.signer = signer;
@@ -22932,43 +22878,6 @@ var FXFiller = class _FXFiller {
22932
22878
  };
22933
22879
  }
22934
22880
  }
22935
- /** Per-pair invariants, shared by the constructor and addPair. Adds the accepted pair's label to `seenPairs`. */
22936
- static assertPairValid(pair, seenPairs) {
22937
- const label = `${normalizeSymbol(pair.token0)}/${normalizeSymbol(pair.token1)}`;
22938
- const reversed = `${normalizeSymbol(pair.token1)}/${normalizeSymbol(pair.token0)}`;
22939
- if (seenPairs.has(label) || seenPairs.has(reversed)) {
22940
- throw new Error(
22941
- `FXFiller pair ${pair.token0}/${pair.token1}: duplicate market (a pair and its reverse are the same market)`
22942
- );
22943
- }
22944
- seenPairs.add(label);
22945
- }
22946
- /**
22947
- * Adds a market to the running engine. All-or-nothing: the pair passes the
22948
- * same duplicate and reverse-orientation checks the constructor enforces
22949
- * before it is pushed.
22950
- */
22951
- addPair(pair) {
22952
- const seenPairs = new Set(this.pairs.map((p) => `${normalizeSymbol(p.token0)}/${normalizeSymbol(p.token1)}`));
22953
- _FXFiller.assertPairValid(pair, seenPairs);
22954
- this.pairs.push(pair);
22955
- }
22956
- /**
22957
- * Removes a market from the running engine. At least one market must remain.
22958
- * Matching reads the limit orders rather than the pairs, so removing a market
22959
- * changes what the dashboard lists, not what the filler fills.
22960
- */
22961
- removePair(pair) {
22962
- const index = this.pairs.indexOf(pair);
22963
- if (index < 0) {
22964
- throw new Error(`FXFiller pair ${pair.token0}/${pair.token1}: not a live market`);
22965
- }
22966
- const remaining = this.pairs.filter((_, i) => i !== index);
22967
- if (remaining.length === 0) {
22968
- throw new Error("FXFiller requires at least one trading pair \u2014 the last market cannot be removed live");
22969
- }
22970
- this.pairs.splice(index, 1);
22971
- }
22972
22881
  // =========================================================================
22973
22882
  // Lifecycle
22974
22883
  // =========================================================================
@@ -23659,7 +23568,7 @@ function minBigInt(first, ...rest) {
23659
23568
 
23660
23569
  // src/services/TokenSender.ts
23661
23570
  import { encodeERC7821ExecuteBatch as encodeERC7821ExecuteBatch4 } from "@hyperbridge/sdk";
23662
- import { encodeFunctionData as encodeFunctionData4, isAddress as isAddress3, parseUnits as parseUnits4 } from "viem";
23571
+ import { encodeFunctionData as encodeFunctionData4, isAddress as isAddress3, maxUint256 as maxUint2565, parseUnits as parseUnits4 } from "viem";
23663
23572
  var TokenSender = class {
23664
23573
  constructor(clientManager, solver, getVaults, userOpSender, configService) {
23665
23574
  this.clientManager = clientManager;
@@ -23731,7 +23640,7 @@ var TokenSender = class {
23731
23640
  });
23732
23641
  }
23733
23642
  }
23734
- const result = await this.submit(chain, calls);
23643
+ const result = await this.submitCalls(chain, calls);
23735
23644
  this.logger.warn({ chain, token, amount, to, redeemed, ...result }, "Operator send executed");
23736
23645
  return { ...result, redeemed };
23737
23646
  }
@@ -23787,16 +23696,23 @@ var TokenSender = class {
23787
23696
  }
23788
23697
  return null;
23789
23698
  }
23790
- async submit(chain, calls) {
23699
+ /**
23700
+ * Executes a batch from the filler wallet: sponsored UserOp first, native tx
23701
+ * when no sponsor is available. Shared by every operator-initiated outbound flow.
23702
+ */
23703
+ async submitCalls(chain, calls) {
23791
23704
  if (this.userOpSender?.canSponsor(chain)) {
23705
+ const bootstrap = await this.permit2Bootstrap(chain);
23706
+ const batch = bootstrap ? [bootstrap, ...calls] : calls;
23792
23707
  const result = await this.userOpSender.trySendSponsored({
23793
23708
  chain,
23794
- callData: encodeERC7821ExecuteBatch4(calls),
23709
+ callData: encodeERC7821ExecuteBatch4(batch),
23795
23710
  gas: {
23796
23711
  verificationGasLimit: 60000n,
23797
- callGasLimit: 450000n * BigInt(calls.length),
23712
+ callGasLimit: 450000n * BigInt(batch.length),
23798
23713
  preVerificationGas: 150000n
23799
- }
23714
+ },
23715
+ permitBootstrap: !!bootstrap
23800
23716
  });
23801
23717
  if (result) return { txHash: result.txHash, sponsored: true };
23802
23718
  this.logger.warn({ chain }, "Sponsored send unavailable, sending native tx");
@@ -23818,7 +23734,1031 @@ var TokenSender = class {
23818
23734
  if (receipt.status !== "success") throw new Error(`Send tx reverted: ${tx}`);
23819
23735
  return { txHash: tx, sponsored: false };
23820
23736
  }
23737
+ /**
23738
+ * The `approve(Permit2, max)` to run first when the wallet has never approved Permit2 for its fee
23739
+ * token on this chain, and that token has an EIP-2612 permit. The op then pays its gas with a permit
23740
+ * and installs the approval, so a wallet with no native can still send; later ops use Permit2.
23741
+ * Undefined otherwise, or when the check fails, and the send goes ahead as before.
23742
+ */
23743
+ async permit2Bootstrap(chain) {
23744
+ const paymaster = this.configService?.getSimplexPaymasterAddress(chain);
23745
+ if (!this.configService || !paymaster) return void 0;
23746
+ try {
23747
+ const pending = await resolvePendingPermit2Approval(
23748
+ this.clientManager.getPublicClient(chain),
23749
+ this.solver,
23750
+ paymaster,
23751
+ chain,
23752
+ this.configService
23753
+ );
23754
+ if (!pending?.permitCapable) return void 0;
23755
+ return {
23756
+ target: pending.token,
23757
+ value: 0n,
23758
+ data: encodeFunctionData4({
23759
+ abi: ERC20_ABI,
23760
+ functionName: "approve",
23761
+ args: [pending.spender, maxUint2565]
23762
+ })
23763
+ };
23764
+ } catch (error) {
23765
+ this.logger.warn({ chain, error }, "Could not check whether this send must install the Permit2 approval");
23766
+ return void 0;
23767
+ }
23768
+ }
23769
+ };
23770
+
23771
+ // src/services/BridgeTopUp.ts
23772
+ import {
23773
+ bytes20ToBytes32 as bytes20ToBytes323,
23774
+ deriveCanonicalPlacedOrder,
23775
+ IntentGatewayABI,
23776
+ createQueryClient,
23777
+ EvmChain as EvmChain2,
23778
+ getChainId as getChainId4,
23779
+ HyperFungibleTokenABI,
23780
+ IntentGateway as IntentGateway2,
23781
+ IsmpClient,
23782
+ SubstrateChain
23783
+ } from "@hyperbridge/sdk";
23784
+ import { randomUUID as randomUUID2 } from "node:crypto";
23785
+ import { setTimeout as sleep } from "node:timers/promises";
23786
+ import {
23787
+ createPublicClient as createPublicClient3,
23788
+ encodeAbiParameters,
23789
+ encodeFunctionData as encodeFunctionData5,
23790
+ fallback as fallback2,
23791
+ formatUnits as formatUnits5,
23792
+ http as http3,
23793
+ parseEventLogs as parseEventLogs2,
23794
+ parseUnits as parseUnits5,
23795
+ stringToHex,
23796
+ toHex as toHex4
23797
+ } from "viem";
23798
+ import { bsc as bscChain } from "viem/chains";
23799
+ var SIMPLEX_REFERRER = stringToHex("Simplex", { size: 32 });
23800
+ var BRIDGE_TOP_UP_CHAIN = "EVM-56";
23801
+ var BRIDGE_TOKEN_BSC = "0x5b0c50fDd52ECC0d4c682c441eaBaD41FfDEABBB";
23802
+ var BRIDGE_DECIMALS = 18;
23803
+ var NEXUS_STATE_MACHINE = "POLKADOT-3367";
23804
+ var NEXUS_CONSENSUS_STATE = "DOT0";
23805
+ var NEXUS_WS_URL = "wss://nexus.rpc.polytope.technology";
23806
+ var NEXUS_UNIT = 10n ** 6n;
23807
+ var QUOTE_SLIPPAGE_BPS = 100n;
23808
+ var ORDER_WINDOW_SECONDS = 30 * 60;
23809
+ var AUCTION_TIME_MS = 15e3;
23810
+ var REFUND_POLL_MS = 1e4;
23811
+ var RECEIPT_TIMEOUT_MS = 12e4;
23812
+ var MAX_EVENTS = 50;
23813
+ var DELIVERY_POLL_MS = 15e3;
23814
+ var DELIVERY_RETRY_MS = 6e4;
23815
+ var SELF_RELAY_GRACE_MS = 9e4;
23816
+ var RELAYER_DELIVERY_WAIT_MS = 3 * 6e4;
23817
+ var STATUS_BACKFILL_MS = 30 * 6e4;
23818
+ var NETWORK_TIMEOUT_MS = 3e4;
23819
+ var LOG_CHUNK_BLOCKS = 300n;
23820
+ var FILL_SEARCH_BEFORE_MS = 3 * 6e4;
23821
+ var FILL_SEARCH_AFTER_MS = 6e4;
23822
+ var ORDER_FILLED_EVENT = IntentGatewayABI.find(
23823
+ (item) => item.type === "event" && item.name === "OrderFilled"
23824
+ );
23825
+ var DELIVERY_REQUEST_QUERY = `query TopUpDelivery($hash: String!) {
23826
+ requestV2s(
23827
+ filter: {
23828
+ source: { equalTo: "EVM-56" }
23829
+ dest: { equalTo: "POLKADOT-3367" }
23830
+ statusMetadata: { some: { transactionHash: { equalTo: $hash } } }
23831
+ }
23832
+ ) {
23833
+ nodes { commitment }
23834
+ }
23835
+ }`;
23836
+ var ORDER_QUERY2 = `query TopUpOrder($commitment: String!) {
23837
+ iOrderV3s(filter: { commitment: { equalTo: $commitment } }) {
23838
+ nodes {
23839
+ status
23840
+ statusMetadata { nodes { status timestamp transactionHash filler } }
23841
+ }
23842
+ }
23843
+ }`;
23844
+ var CALL_TUPLE = [
23845
+ {
23846
+ type: "tuple[]",
23847
+ components: [
23848
+ { name: "to", type: "address" },
23849
+ { name: "value", type: "uint256" },
23850
+ { name: "data", type: "bytes" }
23851
+ ]
23852
+ }
23853
+ ];
23854
+ var PENDING_STAGES = /* @__PURE__ */ new Set([
23855
+ "placing",
23856
+ "awaiting_bids",
23857
+ "filling",
23858
+ "delivering",
23859
+ "unfilled",
23860
+ "reclaiming"
23861
+ ]);
23862
+ function isBridgeTopUpPending(stage) {
23863
+ return PENDING_STAGES.has(stage);
23864
+ }
23865
+ function encodeBridgeTopUpCall(bridgeToken, recipient, amount) {
23866
+ const data = encodeFunctionData5({
23867
+ abi: HyperFungibleTokenABI,
23868
+ functionName: "send",
23869
+ args: [
23870
+ {
23871
+ dest: toHex4(NEXUS_STATE_MACHINE),
23872
+ to: recipient,
23873
+ amount,
23874
+ timeout: 0n,
23875
+ relayerFee: 0n,
23876
+ data: "0x"
23877
+ }
23878
+ ]
23879
+ });
23880
+ return encodeAbiParameters(CALL_TUPLE, [[{ to: bridgeToken, value: 0n, data }]]);
23881
+ }
23882
+ function bidPreview(bid) {
23883
+ return {
23884
+ solverAddress: bid.solverAddress,
23885
+ outputs: bid.outputs.map((output) => ({ token: output.token, amount: output.amount.toString() }))
23886
+ };
23887
+ }
23888
+ function bridgeTopUpOutput(quotedOut) {
23889
+ const afterSlippage = quotedOut * (10000n - QUOTE_SLIPPAGE_BPS) / 10000n;
23890
+ return afterSlippage / NEXUS_UNIT * NEXUS_UNIT;
23891
+ }
23892
+ function serializeOrder(order) {
23893
+ return JSON.stringify(order, (_key, value) => typeof value === "bigint" ? { $bigint: value.toString() } : value);
23894
+ }
23895
+ function deserializeOrder(json) {
23896
+ return JSON.parse(
23897
+ json,
23898
+ (_key, value) => value && typeof value === "object" && typeof value.$bigint === "string" ? BigInt(value.$bigint) : value
23899
+ );
23900
+ }
23901
+ var BridgeTopUp = class {
23902
+ constructor(deps) {
23903
+ this.deps = deps;
23904
+ this.gateways = /* @__PURE__ */ new Map();
23905
+ /** Serializes state writes so a later stage never lands before an earlier one. */
23906
+ this.writes = Promise.resolve();
23907
+ this.logger = moduleLogger(deps.loggers, "bridge-top-up");
23908
+ }
23909
+ get bridgeToken() {
23910
+ return this.deps.configService.getAssetBySymbol(BRIDGE_TOP_UP_CHAIN, "BRIDGE") ?? BRIDGE_TOKEN_BSC;
23911
+ }
23912
+ /** Loads the stored top-up and resumes driving it when it was left open. Never rejects. */
23913
+ restore() {
23914
+ this.loaded ?? (this.loaded = (async () => {
23915
+ try {
23916
+ this.record = (await this.deps.state.get()).bridgeTopUp;
23917
+ } catch (err) {
23918
+ this.logger.warn({ err }, "Could not read the stored BRIDGE top-up");
23919
+ return;
23920
+ }
23921
+ const record = this.record;
23922
+ if (record?.stage === "failed" && record.placeTxHash && !record.commitment && record.sessionPrivateKey) {
23923
+ void this.recoverPlacement();
23924
+ return;
23925
+ }
23926
+ if (!record || !isBridgeTopUpPending(record.stage)) return;
23927
+ if (record.stage === "placing" && !record.placeTxHash) {
23928
+ await this.update({
23929
+ stage: "failed",
23930
+ error: "Simplex restarted before the order was placed. Nothing was escrowed."
23931
+ });
23932
+ return;
23933
+ }
23934
+ if (record.stage === "placing") {
23935
+ void this.recoverPlacement();
23936
+ return;
23937
+ }
23938
+ if (record.stage === "awaiting_bids" || record.stage === "filling") void this.resumeOrder();
23939
+ if (record.stage === "reclaiming") void this.driveReclaim();
23940
+ if (record.stage === "delivering") void this.resumeDelivery();
23941
+ })());
23942
+ return this.loaded;
23943
+ }
23944
+ async current() {
23945
+ await this.restore();
23946
+ return this.record ? publicState(this.record) : void 0;
23947
+ }
23948
+ /** Networks the filler runs and has delegated its account on. */
23949
+ async sourceChains() {
23950
+ const chainIds = this.deps.configService.getConfiguredChainIds();
23951
+ const delegated = await Promise.all(
23952
+ chainIds.map((chainId) => this.deps.delegation.isDelegated(`EVM-${chainId}`).catch(() => false))
23953
+ );
23954
+ return chainIds.filter((_, index) => delegated[index]);
23955
+ }
23956
+ /** USDC and USDT wallet balances on every network a top-up can be paid from. */
23957
+ async sources() {
23958
+ const rows = (await this.sourceChains()).flatMap(
23959
+ (chainId) => ["USDC", "USDT"].map(async (token) => {
23960
+ const chain = `EVM-${chainId}`;
23961
+ let tokenAddress;
23962
+ let decimals;
23963
+ try {
23964
+ ;
23965
+ ({ tokenAddress, decimals } = this.payToken(chain, token));
23966
+ } catch {
23967
+ return void 0;
23968
+ }
23969
+ const reserve = this.reserve(chain, tokenAddress);
23970
+ const row = { chain, chainId, token, address: tokenAddress, reserve: formatUnits5(reserve, decimals) };
23971
+ try {
23972
+ const balance = await this.walletBalance(chain, tokenAddress);
23973
+ return {
23974
+ ...row,
23975
+ balance: formatUnits5(balance, decimals),
23976
+ spendable: formatUnits5(balance > reserve ? balance - reserve : 0n, decimals)
23977
+ };
23978
+ } catch (err) {
23979
+ this.logger.debug({ err, chain, token }, "Could not read a top-up source balance");
23980
+ return { ...row, balance: null, spendable: null };
23981
+ }
23982
+ })
23983
+ );
23984
+ return (await Promise.all(rows)).filter((row) => row !== void 0);
23985
+ }
23986
+ async quote(params) {
23987
+ const { tokenAddress, decimals } = this.payToken(params.chain, params.token);
23988
+ const amountIn = parseUnits5(params.amount, decimals);
23989
+ if (amountIn <= 0n) throw new Error("Amount must be greater than 0");
23990
+ const gateway = await this.gateway(params.chain);
23991
+ const quote = await gateway.quoteIntent({ tokenIn: tokenAddress, tokenOut: this.bridgeToken, amountIn });
23992
+ const bridgeOut = quote.fillable ? bridgeTopUpOutput(quote.amountOut) : 0n;
23993
+ const rate = quote.amountIn > 0n ? Number(formatUnits5(quote.amountOut, BRIDGE_DECIMALS)) / Number(formatUnits5(quote.amountIn, decimals)) : 0;
23994
+ return {
23995
+ chain: params.chain,
23996
+ token: params.token,
23997
+ tokenAddress,
23998
+ amountIn: formatUnits5(amountIn, decimals),
23999
+ bridgeOut: formatUnits5(bridgeOut, BRIDGE_DECIMALS),
24000
+ rate: rate.toString(),
24001
+ fillable: quote.fillable && bridgeOut > 0n,
24002
+ maxFillableIn: formatUnits5(quote.maxFillableIn, decimals),
24003
+ route: quote.route
24004
+ };
24005
+ }
24006
+ /** Re-quotes, then places and drives the order in the background. Returns its state. */
24007
+ async start(params) {
24008
+ if (!(await this.sourceChains()).includes(getChainId4(params.chain) ?? -1)) {
24009
+ throw new Error(`${params.chain} is not a network the filler fills on`);
24010
+ }
24011
+ await this.restore();
24012
+ if (this.record && isBridgeTopUpPending(this.record.stage)) {
24013
+ throw new Error("A BRIDGE top-up is already pending");
24014
+ }
24015
+ const quote = await this.quote(params);
24016
+ if (!quote.fillable) {
24017
+ throw new Error(`The orderbook cannot fill ${quote.amountIn} ${params.token} (max ${quote.maxFillableIn})`);
24018
+ }
24019
+ const { tokenAddress, decimals } = this.payToken(params.chain, params.token);
24020
+ const amountIn = parseUnits5(quote.amountIn, decimals);
24021
+ const balance = await this.balanceOf(params.chain, tokenAddress);
24022
+ if (balance < amountIn) {
24023
+ const reserve = formatUnits5(this.reserve(params.chain, tokenAddress), decimals);
24024
+ throw new Error(
24025
+ `Not enough ${params.token}: ${formatUnits5(balance, decimals)} spendable after the ${reserve} ${params.token} kept for gas, need ${quote.amountIn}`
24026
+ );
24027
+ }
24028
+ const order = await this.buildOrder(params.chain, tokenAddress, amountIn, parseUnits5(quote.bridgeOut, BRIDGE_DECIMALS));
24029
+ if (this.record && isBridgeTopUpPending(this.record.stage)) {
24030
+ throw new Error("A BRIDGE top-up is already pending");
24031
+ }
24032
+ const now = Date.now();
24033
+ this.record = {
24034
+ id: randomUUID2(),
24035
+ chain: params.chain,
24036
+ token: params.token,
24037
+ amountIn: quote.amountIn,
24038
+ bridgeOut: quote.bridgeOut,
24039
+ stage: "placing",
24040
+ createdAt: now,
24041
+ updatedAt: now,
24042
+ stageTimes: { placing: now },
24043
+ order: serializeOrder(order)
24044
+ };
24045
+ await this.persist();
24046
+ void this.place(order, tokenAddress);
24047
+ return publicState(this.record);
24048
+ }
24049
+ /** Cancels an unfilled order and refunds its escrowed input to the filler wallet. */
24050
+ async reclaim() {
24051
+ await this.restore();
24052
+ const record = this.record;
24053
+ if (!record || record.stage !== "unfilled") throw new Error("There is no unfilled top-up to reclaim");
24054
+ await this.update({ stage: "reclaiming", error: void 0 });
24055
+ void this.driveReclaim();
24056
+ return publicState(this.record);
24057
+ }
24058
+ /**
24059
+ * Places the order, then tracks it. The SDK builds the `placeOrder` calldata and the
24060
+ * session key; the receipt is read back here, through the filler's own multi-endpoint
24061
+ * client, rather than by the SDK from one endpoint, where a node that has not caught up
24062
+ * with the new block fails the read and loses track of an order that did land.
24063
+ */
24064
+ async place(order, tokenIn) {
24065
+ await this.drive(async (gateway) => {
24066
+ const gen = gateway.executeBest(order, SIMPLEX_REFERRER, { auctionTimeMs: AUCTION_TIME_MS });
24067
+ const first = await gen.next();
24068
+ await gen.return();
24069
+ if (first.done || first.value.status !== "AWAITING_PLACE_ORDER") {
24070
+ throw new Error("The SDK did not produce a placement transaction");
24071
+ }
24072
+ const placement = first.value;
24073
+ await this.update({ sessionPrivateKey: placement.sessionPrivateKey });
24074
+ const calls = this.placementCalls(order, tokenIn, placement);
24075
+ const { txHash } = await this.deps.tokenSender.submitCalls(this.record.chain, calls);
24076
+ await this.update({ placeTxHash: txHash });
24077
+ await this.recordPlacement(order, txHash);
24078
+ await this.trackOrder(gateway);
24079
+ });
24080
+ }
24081
+ /**
24082
+ * Reads the placement receipt and stores the order as the gateway committed it. Throws
24083
+ * when the transaction placed no order, so the top-up is marked failed.
24084
+ */
24085
+ async recordPlacement(order, txHash) {
24086
+ const receipt = await this.deps.clientManager.getPublicClient(this.record.chain).waitForTransactionReceipt({ hash: txHash, timeout: RECEIPT_TIMEOUT_MS });
24087
+ const [placed] = parseEventLogs2({ abi: IntentGatewayABI, logs: receipt.logs, eventName: "OrderPlaced" });
24088
+ if (!placed) throw new Error(`Transaction ${txHash} placed no order`);
24089
+ const canonical = deriveCanonicalPlacedOrder(order, placed.args);
24090
+ await this.update({
24091
+ stage: "awaiting_bids",
24092
+ commitment: canonical.id,
24093
+ order: serializeOrder(canonical),
24094
+ error: void 0
24095
+ });
24096
+ }
24097
+ /** Continues an order whose placement landed but was not recorded. */
24098
+ async recoverPlacement() {
24099
+ await this.drive(async (gateway) => {
24100
+ const record = this.record;
24101
+ await this.recordPlacement(deserializeOrder(record.order), record.placeTxHash);
24102
+ await this.trackOrder(gateway);
24103
+ });
24104
+ }
24105
+ async resumeDelivery() {
24106
+ await this.drive(() => this.deliver());
24107
+ }
24108
+ async resumeOrder() {
24109
+ await this.drive((gateway) => this.trackOrder(gateway));
24110
+ }
24111
+ /** Follows a placed order through bids to a fill, or to its deadline. */
24112
+ async trackOrder(gateway) {
24113
+ const record = this.record;
24114
+ const order = deserializeOrder(record.order);
24115
+ if (await gateway.isOrderFilled(order)) {
24116
+ await this.update({ stage: "delivering" });
24117
+ await this.deliver();
24118
+ return;
24119
+ }
24120
+ if (!record.sessionPrivateKey) {
24121
+ await this.update({ stage: "unfilled", error: "The order's session key was not saved, so it cannot be resumed" });
24122
+ return;
24123
+ }
24124
+ const gen = gateway.resumeBest(order, {
24125
+ sessionPrivateKey: record.sessionPrivateKey,
24126
+ auctionTimeMs: AUCTION_TIME_MS
24127
+ });
24128
+ for await (const value of gen) {
24129
+ if (await this.applyExecutionUpdate(value)) break;
24130
+ }
24131
+ if (this.record.stage === "delivering") await this.deliver();
24132
+ }
24133
+ /**
24134
+ * Carries the bought BRIDGE to Hyperbridge. The order's output call dispatched it from
24135
+ * BSC with no relayer fee. Relayers may still deliver it; when none has shortly after BSC
24136
+ * is final on Hyperbridge, the filler relays it itself: a request into Hyperbridge goes
24137
+ * in as an unsigned extrinsic and costs nothing. Runs until the request is delivered.
24138
+ */
24139
+ async deliver() {
24140
+ let relayedAt = 0;
24141
+ while (!await this.syncDelivery()) {
24142
+ const finalized = this.record.deliveryStatuses?.find((status2) => status2.status === "SOURCE_FINALIZED");
24143
+ const due = finalized && Date.now() - finalized.timestamp > SELF_RELAY_GRACE_MS;
24144
+ if (due && Date.now() - relayedAt > DELIVERY_RETRY_MS) {
24145
+ relayedAt = Date.now();
24146
+ await this.selfRelay(this.record.deliveryCommitment).catch((err) => {
24147
+ this.logger.warn({ err }, "Self-relay of the BRIDGE to Hyperbridge failed; retrying");
24148
+ });
24149
+ }
24150
+ await sleep(DELIVERY_POLL_MS);
24151
+ }
24152
+ const id = this.record.id;
24153
+ while (this.record?.id === id && this.needsStatusBackfill()) {
24154
+ await sleep(DELIVERY_POLL_MS);
24155
+ await this.syncDelivery().catch((err) => this.logger.debug({ err }, "BRIDGE delivery backfill failed"));
24156
+ }
24157
+ }
24158
+ /** Delivered, but the indexer has yet to record the delivery, and not for too long. */
24159
+ needsStatusBackfill() {
24160
+ const record = this.record;
24161
+ if (!record || record.stage !== "delivered" || !record.deliveryCommitment) return false;
24162
+ if (record.deliveryStatuses?.some((status2) => status2.status === "DESTINATION")) return false;
24163
+ return Date.now() - (record.stageTimes?.delivered ?? 0) < STATUS_BACKFILL_MS;
24164
+ }
24165
+ /**
24166
+ * Reads where the order and its BRIDGE are, from the indexer and the chains, and records
24167
+ * it. Shared by the driver and {@link sync}, and never runs twice at once. True once the
24168
+ * BRIDGE has reached Hyperbridge.
24169
+ */
24170
+ syncDelivery() {
24171
+ this.deliverySync ?? (this.deliverySync = this.readDelivery().finally(() => {
24172
+ this.deliverySync = void 0;
24173
+ }));
24174
+ return this.deliverySync;
24175
+ }
24176
+ async readDelivery() {
24177
+ const record = this.record;
24178
+ if (record.stage !== "delivering" && record.stage !== "delivered") return false;
24179
+ if (!record.fillTxHash) {
24180
+ const fillTxHash = await this.findFill();
24181
+ if (!fillTxHash) return false;
24182
+ await this.update({ fillTxHash });
24183
+ }
24184
+ if (!record.deliveryCommitment) {
24185
+ await this.update({ deliveryCommitment: await this.deliveryRequest(record.fillTxHash) });
24186
+ }
24187
+ const hyperbridge = await this.hyperbridge();
24188
+ const request = await withTimeout(
24189
+ this.ismpClient(hyperbridge).queryRequestWithStatus(record.deliveryCommitment),
24190
+ "the BRIDGE request status"
24191
+ );
24192
+ const statuses = (request?.statuses ?? []).filter((status2) => DELIVERY_STATUSES.has(status2.status)).map((status2) => ({
24193
+ status: status2.status,
24194
+ timestamp: toMs(status2.metadata.timestamp),
24195
+ txHash: status2.metadata.transactionHash
24196
+ }));
24197
+ if (statuses.length > 0) await this.update({ deliveryStatuses: statuses });
24198
+ const delivered = statuses.some((status2) => status2.status === "DESTINATION") || !!await withTimeout(hyperbridge.queryRequestReceipt(record.deliveryCommitment), "the request receipt");
24199
+ if (delivered && record.stage !== "delivered") await this.update({ stage: "delivered", error: void 0 });
24200
+ return delivered;
24201
+ }
24202
+ /**
24203
+ * The HyperFungibleToken request the fill dispatched to Hyperbridge. The indexer finds it by
24204
+ * the fill's transaction, as the explorer's search does; the fill's own `Sent` log is the
24205
+ * fallback while the indexer catches up.
24206
+ */
24207
+ async deliveryRequest(fillTxHash) {
24208
+ const response = await withTimeout(
24209
+ fetch(this.deps.indexerUrl, {
24210
+ method: "POST",
24211
+ headers: { "content-type": "application/json" },
24212
+ body: JSON.stringify({ query: DELIVERY_REQUEST_QUERY, variables: { hash: fillTxHash } })
24213
+ }),
24214
+ "the indexer"
24215
+ );
24216
+ const body = await response.json();
24217
+ const indexed = body.data?.requestV2s?.nodes[0]?.commitment;
24218
+ if (indexed) return indexed;
24219
+ const fill = await withTimeout(this.bsc().getTransactionReceipt({ hash: fillTxHash }), "the fill receipt");
24220
+ const [sent] = parseEventLogs2({ abi: HyperFungibleTokenABI, logs: fill.logs, eventName: "Sent" });
24221
+ if (!sent) throw new Error(`The fill ${fillTxHash} sent no BRIDGE to Hyperbridge`);
24222
+ return sent.args.commitment;
24223
+ }
24224
+ /**
24225
+ * BSC, read through every endpoint there is: the filler need not run it, and a single public
24226
+ * endpoint refuses receipts it considers old.
24227
+ */
24228
+ bsc() {
24229
+ this.bscClient ?? (this.bscClient = createPublicClient3({
24230
+ chain: bscChain,
24231
+ transport: fallback2(this.bscProofRpcUrls().map((url) => http3(url, { timeout: 1e4, retryCount: 0 })))
24232
+ }));
24233
+ return this.bscClient;
24234
+ }
24235
+ /**
24236
+ * The transaction that filled the order. The executor reports it only for a fill this
24237
+ * process selected, so it comes from the indexer, or, while the indexer has yet to reach
24238
+ * BSC's latest blocks, from the gateway's `OrderFilled` log around when the fill was seen.
24239
+ */
24240
+ async findFill() {
24241
+ const record = this.record;
24242
+ const indexed = (await this.indexedOrder())?.find((status2) => status2.status === "FILLED");
24243
+ if (indexed) {
24244
+ if (indexed.filler && !record.filler) await this.update({ filler: indexed.filler });
24245
+ return indexed.transactionHash;
24246
+ }
24247
+ const seenAt = record.events?.find((event) => event.status === "FILLED")?.timestamp ?? record.stageTimes?.delivering;
24248
+ if (!seenAt) return void 0;
24249
+ const client = this.bsc();
24250
+ const [latest, blockTime] = await Promise.all([
24251
+ withTimeout(client.getBlock(), "the latest BSC block"),
24252
+ this.blockTimeSeconds()
24253
+ ]);
24254
+ const blockAt = (ms) => latest.number - BigInt(Math.ceil((Number(latest.timestamp) - ms / 1e3) / blockTime));
24255
+ const from = blockAt(seenAt - FILL_SEARCH_BEFORE_MS);
24256
+ const to = (() => {
24257
+ const end = blockAt(seenAt + FILL_SEARCH_AFTER_MS);
24258
+ return end < latest.number ? end : latest.number;
24259
+ })();
24260
+ for (let start = from; start <= to; start += LOG_CHUNK_BLOCKS) {
24261
+ const end = start + LOG_CHUNK_BLOCKS - 1n < to ? start + LOG_CHUNK_BLOCKS - 1n : to;
24262
+ const logs = await withTimeout(
24263
+ client.getLogs({
24264
+ address: this.deps.configService.getIntentGatewayAddress(BRIDGE_TOP_UP_CHAIN),
24265
+ event: ORDER_FILLED_EVENT,
24266
+ args: { commitment: record.commitment },
24267
+ fromBlock: start,
24268
+ toBlock: end
24269
+ }),
24270
+ "the fill on BSC"
24271
+ );
24272
+ if (logs[0]) return logs[0].transactionHash;
24273
+ }
24274
+ return void 0;
24275
+ }
24276
+ /** The order's statuses as the indexer has them: placed, filled, redeemed or refunded. */
24277
+ async indexedOrder() {
24278
+ const response = await withTimeout(
24279
+ fetch(this.deps.indexerUrl, {
24280
+ method: "POST",
24281
+ headers: { "content-type": "application/json" },
24282
+ body: JSON.stringify({ query: ORDER_QUERY2, variables: { commitment: this.record.commitment } })
24283
+ }),
24284
+ "the indexer"
24285
+ );
24286
+ const body = await response.json();
24287
+ return body.data?.iOrderV3s?.nodes[0]?.statusMetadata.nodes.map((node) => ({
24288
+ status: node.status,
24289
+ timestamp: toMs(node.timestamp),
24290
+ transactionHash: node.transactionHash,
24291
+ filler: node.filler ?? void 0
24292
+ }));
24293
+ }
24294
+ /**
24295
+ * Brings a pending top-up up to date from the chains and the indexer, for a dashboard that
24296
+ * is showing it: someone may have filled, delivered or refunded it while this process was
24297
+ * not looking. One sync at a time; a call made while one runs waits for that one.
24298
+ */
24299
+ sync() {
24300
+ this.syncing ?? (this.syncing = this.reconcile().catch((err) => this.logger.debug({ err }, "BRIDGE top-up sync failed")).finally(() => {
24301
+ this.syncing = void 0;
24302
+ }));
24303
+ return this.syncing;
24304
+ }
24305
+ async reconcile() {
24306
+ await this.restore();
24307
+ const record = this.record;
24308
+ if (this.needsStatusBackfill()) {
24309
+ await this.syncDelivery();
24310
+ return;
24311
+ }
24312
+ if (!record || !isBridgeTopUpPending(record.stage) || !record.commitment) return;
24313
+ if (record.stage === "awaiting_bids" || record.stage === "filling") {
24314
+ const statuses = await this.indexedOrder();
24315
+ const fill = statuses?.find((status2) => status2.status === "FILLED");
24316
+ if (fill) {
24317
+ await this.update({
24318
+ stage: "delivering",
24319
+ fillTxHash: fill.transactionHash,
24320
+ filler: fill.filler ?? record.filler,
24321
+ stageTimes: { ...record.stageTimes, delivering: fill.timestamp }
24322
+ });
24323
+ void this.resumeDelivery();
24324
+ } else if (statuses?.some((status2) => status2.status === "REFUNDED")) {
24325
+ await this.update({ stage: "reclaimed" });
24326
+ }
24327
+ return;
24328
+ }
24329
+ if (record.stage === "delivering") {
24330
+ await this.syncDelivery();
24331
+ return;
24332
+ }
24333
+ if (record.stage === "unfilled" || record.stage === "reclaiming") {
24334
+ const gateway = await this.gateway(record.chain);
24335
+ await this.refunded(gateway, deserializeOrder(record.order));
24336
+ }
24337
+ }
24338
+ /**
24339
+ * Submits the request to Hyperbridge with a proof from BSC state. Public BSC endpoints
24340
+ * differ in whether they serve `eth_getProof` for recent blocks, so each is tried in turn.
24341
+ */
24342
+ async selfRelay(commitment) {
24343
+ const hyperbridge = await this.hyperbridge();
24344
+ const failures = [];
24345
+ for (const rpcUrl of this.bscProofRpcUrls()) {
24346
+ if (await withTimeout(hyperbridge.queryRequestReceipt(commitment), "the request receipt")) return;
24347
+ try {
24348
+ const client = this.ismpClient(hyperbridge, rpcUrl);
24349
+ const receipt = await withTimeout(client.aggregateTransactionWithCommitment(commitment), "the relay", 12e4);
24350
+ await this.update({ deliveryTxHash: receipt.transactionHash });
24351
+ return;
24352
+ } catch (err) {
24353
+ const reason = err instanceof Error ? err.message.split("\n")[0] : String(err);
24354
+ failures.push(`${new URL(rpcUrl).host}: ${reason}`);
24355
+ }
24356
+ }
24357
+ throw new Error(`Could not relay the BRIDGE to Hyperbridge (${failures.join("; ")})`);
24358
+ }
24359
+ /** The operator's BSC endpoints first, then the free public ones. */
24360
+ bscProofRpcUrls() {
24361
+ const urls = [
24362
+ ...this.deps.configService.getRpcUrls(BRIDGE_TOP_UP_CHAIN),
24363
+ ...chainByChainId(getChainId4(BRIDGE_TOP_UP_CHAIN))?.defaultRpcUrls ?? []
24364
+ ];
24365
+ return urls.filter((url, index) => url && urls.indexOf(url) === index);
24366
+ }
24367
+ /** Records an executor update. True when the order reached a final state. */
24368
+ async applyExecutionUpdate(value) {
24369
+ switch (value.status) {
24370
+ case "NEW_BID":
24371
+ case "BIDS_RECEIVED": {
24372
+ const bids = value.status === "NEW_BID" ? [value.bid] : value.bids;
24373
+ const bidCount = value.status === "BIDS_RECEIVED" ? Number(value.bidCount ?? bids.length) : void 0;
24374
+ await this.recordEvent({ status: value.status, bidCount, bids: bids.map(bidPreview) });
24375
+ if (bidCount !== void 0) await this.update({ bidCount: Math.max(this.record.bidCount ?? 0, bidCount) });
24376
+ return false;
24377
+ }
24378
+ case "BID_SELECTED":
24379
+ await this.recordEvent({
24380
+ status: "BID_SELECTED",
24381
+ selectedSolver: value.selectedSolver,
24382
+ userOpHash: value.userOpHash,
24383
+ txHash: value.transactionHash
24384
+ });
24385
+ await this.update({ stage: "filling", fillTxHash: value.transactionHash });
24386
+ return false;
24387
+ case "FILLED":
24388
+ await this.recordEvent({
24389
+ status: "FILLED",
24390
+ selectedSolver: value.selectedSolver,
24391
+ userOpHash: value.userOpHash,
24392
+ txHash: value.transactionHash
24393
+ });
24394
+ await this.update({
24395
+ stage: "delivering",
24396
+ fillTxHash: value.transactionHash ?? this.record.fillTxHash,
24397
+ error: void 0
24398
+ });
24399
+ return true;
24400
+ case "EXPIRED":
24401
+ case "FAILED":
24402
+ await this.update({ stage: "unfilled", error: value.error });
24403
+ return true;
24404
+ case "CANCELLED":
24405
+ await this.update({ stage: "reclaimed" });
24406
+ return true;
24407
+ default:
24408
+ return false;
24409
+ }
24410
+ }
24411
+ /**
24412
+ * Recovers an unfilled order's escrow, following the Hyperbridge app's recovery: the SDK
24413
+ * cancel flow from the paying chain (BSC's expiry height finalized, the cancel sent, the
24414
+ * cancellation finalized on the source, verified and finalized on Hyperbridge), then the
24415
+ * refund. A relayer normally delivers that refund; if none has within a few minutes of
24416
+ * Hyperbridge finalizing, the filler submits Hyperbridge's response itself. Each step is
24417
+ * recorded as it lands, and a restart resumes from the SDK's persisted progress.
24418
+ */
24419
+ async driveReclaim() {
24420
+ await this.drive(async (gateway) => {
24421
+ const record = this.record;
24422
+ const order = deserializeOrder(record.order);
24423
+ if (await this.refunded(gateway, order)) return;
24424
+ const sameChain = record.chain === BRIDGE_TOP_UP_CHAIN;
24425
+ const ismp = await this.cancelClient(record.chain);
24426
+ const gen = gateway.cancelOrder(order, ismp);
24427
+ let input;
24428
+ while (true) {
24429
+ const { value, done } = await gen.next(input);
24430
+ input = void 0;
24431
+ if (done) break;
24432
+ switch (value.status) {
24433
+ case "DESTINATION_FINALIZED":
24434
+ await this.recordRecoveryStep("DESTINATION_FINALIZED");
24435
+ break;
24436
+ case "AWAITING_CANCEL_TRANSACTION": {
24437
+ if (this.record.reclaimTxHash && !this.hasRecoveryStep("ORDER_CANCELED")) {
24438
+ input = this.record.reclaimTxHash;
24439
+ break;
24440
+ }
24441
+ const calls = [];
24442
+ const quote = await gateway.quoteCancelOrder(order);
24443
+ if (value.value === 0n && quote.relayerFee > 0n) {
24444
+ const feeToken = await this.feeToken(record.chain);
24445
+ calls.push({
24446
+ target: feeToken,
24447
+ value: 0n,
24448
+ data: encodeFunctionData5({
24449
+ abi: ERC20_ABI,
24450
+ functionName: "approve",
24451
+ args: [value.to, quote.relayerFee]
24452
+ })
24453
+ });
24454
+ }
24455
+ calls.push({ target: value.to, value: value.value, data: value.data });
24456
+ const { txHash } = await this.deps.tokenSender.submitCalls(record.chain, calls);
24457
+ await this.update({ reclaimTxHash: txHash });
24458
+ input = txHash;
24459
+ break;
24460
+ }
24461
+ case "CANCEL_STARTED":
24462
+ await this.recordRecoveryStep("ORDER_CANCELED", value.receipt.transactionHash);
24463
+ break;
24464
+ case "SOURCE_FINALIZED":
24465
+ case "HYPERBRIDGE_DELIVERED":
24466
+ case "HYPERBRIDGE_FINALIZED": {
24467
+ const metadata = value.metadata;
24468
+ await this.recordRecoveryStep(
24469
+ value.status,
24470
+ metadata.transactionHash,
24471
+ metadata.timestamp ? toMs(metadata.timestamp) : void 0
24472
+ );
24473
+ if (value.status === "HYPERBRIDGE_FINALIZED" && metadata.calldata) {
24474
+ await this.update({ claimCalldata: metadata.calldata });
24475
+ }
24476
+ break;
24477
+ }
24478
+ case "CANCELLATION_COMPLETE":
24479
+ await this.recordRecoveryStep("ORDER_CANCELED", value.transactionHash);
24480
+ await this.recordRecoveryStep("FUNDS_CLAIMED", value.transactionHash);
24481
+ break;
24482
+ }
24483
+ }
24484
+ if (sameChain && this.hasRecoveryStep("FUNDS_CLAIMED")) {
24485
+ await this.update({ stage: "reclaimed", error: void 0 });
24486
+ return;
24487
+ }
24488
+ while (!await this.refunded(gateway, order)) {
24489
+ const finalizedAt = this.record.recoverySteps?.find((s) => s.step === "HYPERBRIDGE_FINALIZED")?.timestamp;
24490
+ const claimable = this.record.claimCalldata && finalizedAt && Date.now() - finalizedAt > RELAYER_DELIVERY_WAIT_MS;
24491
+ if (claimable && !this.record.claimTxHash) {
24492
+ await this.claimRefund(record.chain, this.record.claimCalldata);
24493
+ continue;
24494
+ }
24495
+ await sleep(REFUND_POLL_MS);
24496
+ }
24497
+ });
24498
+ }
24499
+ /**
24500
+ * Submits Hyperbridge's finalized response to the source chain's ISMP handler, which
24501
+ * refunds the escrow, as the Hyperbridge app's "Claim funds" does.
24502
+ */
24503
+ async claimRefund(chain, calldata) {
24504
+ const params = await withTimeout(
24505
+ this.deps.clientManager.getPublicClient(chain).readContract({
24506
+ abi: EVM_HOST,
24507
+ address: this.deps.configService.getHostAddress(chain),
24508
+ functionName: "hostParams"
24509
+ }),
24510
+ "the host's handler"
24511
+ );
24512
+ const { txHash } = await this.deps.tokenSender.submitCalls(chain, [
24513
+ { target: params.handler, value: 0n, data: calldata }
24514
+ ]);
24515
+ await this.update({ claimTxHash: txHash });
24516
+ }
24517
+ /**
24518
+ * Whether the escrow is back with the filler, from the source gateway or the indexer's
24519
+ * REFUNDED status; records the refund as the recovery's last step when it is.
24520
+ */
24521
+ async refunded(gateway, order) {
24522
+ const onChain = await withTimeout(gateway.isOrderRefunded(order), "the refund").catch(() => false);
24523
+ const indexed = (await this.indexedOrder().catch(() => void 0))?.find((s) => s.status === "REFUNDED");
24524
+ if (!onChain && !indexed) return false;
24525
+ if (!this.hasRecoveryStep("FUNDS_CLAIMED")) {
24526
+ await this.recordRecoveryStep(
24527
+ "FUNDS_CLAIMED",
24528
+ this.record.claimTxHash ?? indexed?.transactionHash,
24529
+ indexed?.timestamp
24530
+ );
24531
+ }
24532
+ await this.update({ stage: "reclaimed", error: void 0 });
24533
+ return true;
24534
+ }
24535
+ hasRecoveryStep(step) {
24536
+ return !!this.record?.recoverySteps?.some((entry) => entry.step === step);
24537
+ }
24538
+ /** Records a recovery step once, at the time it landed (or now). */
24539
+ async recordRecoveryStep(step, txHash, timestamp) {
24540
+ if (this.hasRecoveryStep(step)) return;
24541
+ const steps = [...this.record.recoverySteps ?? [], { step, txHash, timestamp: timestamp ?? Date.now() }];
24542
+ await this.update({ recoverySteps: steps });
24543
+ }
24544
+ /** Runs one background flow for the record; a failure is recorded rather than thrown. */
24545
+ async drive(flow) {
24546
+ const record = this.record;
24547
+ if (!record || this.driving === record.id) return;
24548
+ this.driving = record.id;
24549
+ try {
24550
+ await flow(await this.gateway(record.chain));
24551
+ } catch (err) {
24552
+ const error = err instanceof Error ? err.message : String(err);
24553
+ this.logger.warn({ err, id: record.id, stage: record.stage }, "BRIDGE top-up step failed");
24554
+ if (record.stage === "placing" && !record.placeTxHash) {
24555
+ await this.update({ stage: "failed", error });
24556
+ } else if (record.stage === "delivering") {
24557
+ await this.update({ error: `Delivery to Hyperbridge is retrying: ${error}` });
24558
+ setTimeout(() => void this.resumeDelivery(), DELIVERY_RETRY_MS);
24559
+ } else if (record.stage === "reclaiming") {
24560
+ await this.update({ error: `Recovery is retrying: ${error}` });
24561
+ setTimeout(() => void this.driveReclaim(), DELIVERY_RETRY_MS);
24562
+ } else if (record.stage === "placing") {
24563
+ await this.update({ stage: "failed", error: `${error} (placement tx ${record.placeTxHash})` });
24564
+ } else {
24565
+ await this.update({ stage: "unfilled", error });
24566
+ }
24567
+ } finally {
24568
+ if (this.driving === record.id) this.driving = void 0;
24569
+ this.logger.info(publicState(record), "BRIDGE top-up step finished");
24570
+ }
24571
+ }
24572
+ /** Keeps what the executor reported, for the timeline's offers and its links. */
24573
+ async recordEvent(event) {
24574
+ const events = [...this.record.events ?? [], { ...event, timestamp: Date.now() }];
24575
+ await this.update({ events: events.slice(-MAX_EVENTS) });
24576
+ }
24577
+ async update(patch) {
24578
+ if (!this.record) return;
24579
+ const now = Date.now();
24580
+ if (patch.stage && patch.stage !== this.record.stage) {
24581
+ patch = { ...patch, stageTimes: { ...this.record.stageTimes, [patch.stage]: now } };
24582
+ }
24583
+ Object.assign(this.record, patch, { updatedAt: now });
24584
+ await this.persist();
24585
+ }
24586
+ persist() {
24587
+ const snapshot = this.record ? { ...this.record } : void 0;
24588
+ const write = this.writes.then(() => patchRuntimeState(this.deps.state, { bridgeTopUp: snapshot }));
24589
+ this.writes = write.catch((err) => this.logger.warn({ err }, "Could not save the BRIDGE top-up"));
24590
+ return write.then(
24591
+ () => void 0,
24592
+ () => void 0
24593
+ );
24594
+ }
24595
+ async buildOrder(chain, tokenIn, amountIn, bridgeOut) {
24596
+ const dispatcher = await this.callDispatcher();
24597
+ const bsc = this.bsc();
24598
+ const [block, blockTime] = await Promise.all([bsc.getBlockNumber(), this.blockTimeSeconds()]);
24599
+ return {
24600
+ user: bytes20ToBytes323(this.deps.solver),
24601
+ source: chain,
24602
+ destination: BRIDGE_TOP_UP_CHAIN,
24603
+ deadline: block + BigInt(Math.ceil(ORDER_WINDOW_SECONDS / blockTime)),
24604
+ nonce: 0n,
24605
+ fees: 0n,
24606
+ session: "0x0000000000000000000000000000000000000000",
24607
+ predispatch: { assets: [], call: "0x" },
24608
+ inputs: [{ token: bytes20ToBytes323(tokenIn), amount: amountIn }],
24609
+ output: {
24610
+ beneficiary: bytes20ToBytes323(dispatcher),
24611
+ assets: [{ token: bytes20ToBytes323(this.bridgeToken), amount: bridgeOut }],
24612
+ call: encodeBridgeTopUpCall(this.bridgeToken, this.deps.substratePublicKey, bridgeOut)
24613
+ }
24614
+ };
24615
+ }
24616
+ /**
24617
+ * Approvals for the gateway to pull the input and the order fee, then `placeOrder`.
24618
+ * The fee token is often the input token itself; one approval then covers both.
24619
+ */
24620
+ placementCalls(order, tokenIn, placement) {
24621
+ const amountIn = order.inputs[0].amount;
24622
+ const approvals = /* @__PURE__ */ new Map([[tokenIn.toLowerCase(), amountIn]]);
24623
+ if (placement.feeTokenAmount > 0n) {
24624
+ const fee = placement.feeTokenAddress.toLowerCase();
24625
+ approvals.set(fee, (approvals.get(fee) ?? 0n) + placement.feeTokenAmount);
24626
+ }
24627
+ const calls = [...approvals].map(([token, amount]) => ({
24628
+ target: token,
24629
+ value: 0n,
24630
+ data: encodeFunctionData5({ abi: ERC20_ABI, functionName: "approve", args: [placement.to, amount] })
24631
+ }));
24632
+ calls.push({ target: placement.to, value: placement.value, data: placement.data });
24633
+ return calls;
24634
+ }
24635
+ payToken(chain, token) {
24636
+ const { configService } = this.deps;
24637
+ const tokenAddress = token === "USDC" ? configService.getUsdcAsset(chain) : configService.getUsdtAsset(chain);
24638
+ if (!tokenAddress || /^0x0+$/.test(tokenAddress)) throw new Error(`${token} is not configured on ${chain}`);
24639
+ const decimals = token === "USDC" ? configService.getUsdcDecimals(chain) : configService.getUsdtDecimals(chain);
24640
+ return { tokenAddress, decimals };
24641
+ }
24642
+ async walletBalance(chain, token) {
24643
+ return await this.deps.clientManager.getPublicClient(chain).readContract({
24644
+ abi: ERC20_ABI,
24645
+ address: token,
24646
+ functionName: "balanceOf",
24647
+ args: [this.deps.solver]
24648
+ });
24649
+ }
24650
+ /** What the paymaster pulls for gas on the sponsored path, which a top-up must leave. */
24651
+ reserve(chain, token) {
24652
+ return paymasterReserveForToken(chain, token.toLowerCase(), this.deps.configService);
24653
+ }
24654
+ /** Spendable wallet balance: the paymaster's gas reserve is held back. */
24655
+ async balanceOf(chain, token) {
24656
+ const balance = await this.walletBalance(chain, token);
24657
+ const reserve = this.reserve(chain, token);
24658
+ return balance > reserve ? balance - reserve : 0n;
24659
+ }
24660
+ async feeToken(chain) {
24661
+ return await this.deps.clientManager.getPublicClient(chain).readContract({
24662
+ abi: [{ type: "function", name: "feeToken", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] }],
24663
+ address: this.deps.configService.getHostAddress(chain),
24664
+ functionName: "feeToken"
24665
+ });
24666
+ }
24667
+ /** The CallDispatcher the BSC gateway runs output calls through, read from the gateway itself. */
24668
+ callDispatcher() {
24669
+ this.dispatcher ?? (this.dispatcher = (async () => {
24670
+ const params = await this.bsc().readContract({
24671
+ abi: INTENT_GATEWAY_V2_ABI,
24672
+ address: this.deps.configService.getIntentGatewayAddress(BRIDGE_TOP_UP_CHAIN),
24673
+ functionName: "params"
24674
+ });
24675
+ return params.dispatcher;
24676
+ })().catch((err) => {
24677
+ this.dispatcher = void 0;
24678
+ throw err;
24679
+ }));
24680
+ return this.dispatcher;
24681
+ }
24682
+ async blockTimeSeconds() {
24683
+ const client = this.bsc();
24684
+ const latest = await client.getBlock();
24685
+ const earlier = await client.getBlock({ blockNumber: latest.number - 100n });
24686
+ const seconds = Number(latest.timestamp - earlier.timestamp) / 100;
24687
+ return seconds > 0 ? seconds : 1;
24688
+ }
24689
+ evmChain(chain, rpcUrl) {
24690
+ const { configService } = this.deps;
24691
+ const bundlerUrl = chain === BRIDGE_TOP_UP_CHAIN ? configService.getBundlerUrl(chain) ?? hyperbridgeBundlerUrl(getChainId4(chain)) : void 0;
24692
+ return EvmChain2.fromParams({
24693
+ chainId: getChainId4(chain),
24694
+ host: configService.getHostAddress(chain),
24695
+ rpcUrl: rpcUrl ?? configService.getRpcUrl(chain),
24696
+ bundlerUrl
24697
+ });
24698
+ }
24699
+ gateway(source) {
24700
+ let gateway = this.gateways.get(source);
24701
+ if (!gateway) {
24702
+ gateway = (async () => IntentGateway2.create(
24703
+ this.evmChain(source),
24704
+ this.evmChain(BRIDGE_TOP_UP_CHAIN),
24705
+ await this.deps.hyperbridge,
24706
+ { storagePath: this.deps.storagePath }
24707
+ ))().catch((err) => {
24708
+ this.gateways.delete(source);
24709
+ throw err;
24710
+ });
24711
+ this.gateways.set(source, gateway);
24712
+ }
24713
+ return gateway;
24714
+ }
24715
+ hyperbridge() {
24716
+ this.hyperbridgeChain ?? (this.hyperbridgeChain = withTimeout(SubstrateChain.connect({
24717
+ wsUrl: this.deps.configService.getHyperbridgeWsUrl() ?? NEXUS_WS_URL,
24718
+ consensusStateId: NEXUS_CONSENSUS_STATE,
24719
+ hasher: "Keccak",
24720
+ stateMachineId: NEXUS_STATE_MACHINE
24721
+ }), "Hyperbridge").catch((err) => {
24722
+ this.hyperbridgeChain = void 0;
24723
+ throw err;
24724
+ }));
24725
+ return this.hyperbridgeChain;
24726
+ }
24727
+ /** Tracks requests from BSC into Hyperbridge, the route the bought BRIDGE takes. */
24728
+ ismpClient(hyperbridge, rpcUrl) {
24729
+ return new IsmpClient({
24730
+ queryClient: createQueryClient({ url: this.deps.indexerUrl }),
24731
+ source: this.evmChain(BRIDGE_TOP_UP_CHAIN, rpcUrl),
24732
+ dest: hyperbridge,
24733
+ hyperbridge
24734
+ });
24735
+ }
24736
+ /** Tracks the reclaim's cancellation, which runs between the paying chain and BSC. */
24737
+ async cancelClient(source) {
24738
+ return new IsmpClient({
24739
+ queryClient: createQueryClient({ url: this.deps.indexerUrl }),
24740
+ source: this.evmChain(source),
24741
+ dest: this.evmChain(BRIDGE_TOP_UP_CHAIN),
24742
+ hyperbridge: await this.hyperbridge()
24743
+ });
24744
+ }
23821
24745
  };
24746
+ function publicState(record) {
24747
+ const { order: _order, sessionPrivateKey: _key, ...rest } = record;
24748
+ return { ...rest, pending: isBridgeTopUpPending(record.stage) };
24749
+ }
24750
+ var DELIVERY_STATUSES = /* @__PURE__ */ new Set(["SOURCE", "SOURCE_FINALIZED", "DESTINATION"]);
24751
+ function toMs(timestamp) {
24752
+ const value = Number(timestamp ?? 0);
24753
+ return value > 0 && value < 1e12 ? value * 1e3 : value;
24754
+ }
24755
+ function withTimeout(promise, what, ms = NETWORK_TIMEOUT_MS) {
24756
+ let timer;
24757
+ const timeout = new Promise((_, reject) => {
24758
+ timer = setTimeout(() => reject(new Error(`Timed out reading ${what}`)), ms);
24759
+ });
24760
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
24761
+ }
23822
24762
 
23823
24763
  // src/services/bundler-preflight.ts
23824
24764
  var BUNDLER_PREFLIGHT_TIMEOUT_MS = 1e4;
@@ -23976,7 +24916,30 @@ var LimitOrderLifecycle = class {
23976
24916
 
23977
24917
  // src/services/BalanceProvider.ts
23978
24918
  import { EventEmitter as EventEmitter2 } from "node:events";
23979
- import { formatUnits as formatUnits5 } from "viem";
24919
+ import { formatUnits as formatUnits6 } from "viem";
24920
+
24921
+ // src/services/native-setup.ts
24922
+ import { concat, formatEther as formatEther4 } from "viem";
24923
+ async function nativeSetupNeed(client, configService, wallet, chain, nativeBalance) {
24924
+ const paymaster = configService.getSimplexPaymasterAddress(chain);
24925
+ if (!paymaster) return null;
24926
+ const gasPrice = await client.getGasPrice();
24927
+ if (nativeBalance >= (DELEGATION_TX_GAS_FLOOR + BATCHED_APPROVE_GAS) * gasPrice) return null;
24928
+ const pending = await resolvePendingPermit2Approval(client, wallet, paymaster, chain, configService);
24929
+ if (!pending || pending.permitCapable) return null;
24930
+ const solverAccount = configService.getSolverAccountContractAddress(chain);
24931
+ const code = await client.getCode({ address: wallet });
24932
+ const delegated = !!solverAccount && code?.toLowerCase() === concat(["0xef0100", solverAccount]).toLowerCase();
24933
+ const cost = (delegated ? APPROVE_TX_GAS : DELEGATION_TX_GAS_FLOOR + BATCHED_APPROVE_GAS) * gasPrice;
24934
+ if (nativeBalance >= cost) return null;
24935
+ const usdc = configService.getUsdcAsset(chain);
24936
+ return {
24937
+ required: Number(formatEther4(cost * 2n)),
24938
+ feeToken: pending.token.toLowerCase() === usdc.toLowerCase() ? "USDC" : "USDT"
24939
+ };
24940
+ }
24941
+
24942
+ // src/services/BalanceProvider.ts
23980
24943
  var BalanceProvider = class extends EventEmitter2 {
23981
24944
  constructor(options) {
23982
24945
  super();
@@ -24024,8 +24987,14 @@ var BalanceProvider = class extends EventEmitter2 {
24024
24987
  })
24025
24988
  );
24026
24989
  }
24990
+ let tokens = {};
24991
+ try {
24992
+ tokens = await this.options.tokens();
24993
+ } catch (err) {
24994
+ this.logger.warn({ err }, "Could not read the orderbook's books; reading USDC and USDT balances only");
24995
+ }
24027
24996
  const fxExoticByChain = /* @__PURE__ */ new Map();
24028
- for (const [chainKey, addrs] of Object.entries(this.options.token1)) {
24997
+ for (const [chainKey, addrs] of Object.entries(tokens)) {
24029
24998
  const id = Number.parseInt(chainKey.replace("EVM-", ""), 10);
24030
24999
  if (!isNaN(id)) fxExoticByChain.set(id, addrs);
24031
25000
  }
@@ -24069,7 +25038,14 @@ var BalanceProvider = class extends EventEmitter2 {
24069
25038
  try {
24070
25039
  const native = await client.getBalance({ address: fillerAddr });
24071
25040
  const symbol = nativeTokenSymbol(chainId);
24072
- row.native = { symbol, amount: Number.parseFloat(formatUnits5(native, 18)) };
25041
+ row.native = { symbol, amount: Number.parseFloat(formatUnits6(native, 18)) };
25042
+ const setup = await nativeSetupNeed(client, this.options.configService, fillerAddr, chain, native).catch(
25043
+ (err) => {
25044
+ this.logger.debug({ err, chainId }, "Could not check whether the chain needs native gas for setup");
25045
+ return null;
25046
+ }
25047
+ );
25048
+ if (setup) row.nativeSetup = setup;
24073
25049
  } catch (err) {
24074
25050
  issues.push({ chainId, source: "native", message: errorMessage(err) });
24075
25051
  }
@@ -24140,7 +25116,7 @@ var BalanceProvider = class extends EventEmitter2 {
24140
25116
  functionName: "balanceOf",
24141
25117
  args: [fillerAddr]
24142
25118
  });
24143
- wallet = Number.parseFloat(formatUnits5(balance, definition.decimals));
25119
+ wallet = Number.parseFloat(formatUnits6(balance, definition.decimals));
24144
25120
  } catch (err) {
24145
25121
  issues.push({
24146
25122
  chainId,
@@ -24168,8 +25144,8 @@ var BalanceProvider = class extends EventEmitter2 {
24168
25144
  available: wallet !== null && vaultDataAvailable ? Math.max(wallet - walletReserve, 0) + vaultAvailable : null,
24169
25145
  vaults: positions.map((position) => ({
24170
25146
  address: position.vault,
24171
- position: Number.parseFloat(formatUnits5(position.positionAssets, position.decimals)),
24172
- available: Number.parseFloat(formatUnits5(position.availableAssets, position.decimals)),
25147
+ position: Number.parseFloat(formatUnits6(position.positionAssets, position.decimals)),
25148
+ available: Number.parseFloat(formatUnits6(position.availableAssets, position.decimals)),
24173
25149
  acceptsDeposits: position.acceptsDeposits
24174
25150
  })),
24175
25151
  status: complete ? "fresh" : wallet !== null || positions.length > 0 ? "partial" : "unavailable"
@@ -24212,8 +25188,8 @@ var BalanceProvider = class extends EventEmitter2 {
24212
25188
  const api = await coprocessor.queryApi();
24213
25189
  const account = await api.query.system.account(address);
24214
25190
  const decimals = api.registry.chainDecimals[0] ?? 12;
24215
- const free = Number.parseFloat(formatUnits5(BigInt(account.data.free.toString()), decimals));
24216
- const reserved = Number.parseFloat(formatUnits5(BigInt(account.data.reserved.toString()), decimals));
25191
+ const free = Number.parseFloat(formatUnits6(BigInt(account.data.free.toString()), decimals));
25192
+ const reserved = Number.parseFloat(formatUnits6(BigInt(account.data.reserved.toString()), decimals));
24217
25193
  this.snapshot = { ...this.snapshot, hyperbridge: { address, free, reserved } };
24218
25194
  } catch (err) {
24219
25195
  this.logger.warn({ err }, "Failed to fetch Hyperbridge balance");
@@ -24233,7 +25209,7 @@ function isConfiguredAddress(address) {
24233
25209
  }
24234
25210
  function sumFormatted(positions, field) {
24235
25211
  return positions.reduce(
24236
- (total, position) => total + Number.parseFloat(formatUnits5(position[field], position.decimals)),
25212
+ (total, position) => total + Number.parseFloat(formatUnits6(position[field], position.decimals)),
24237
25213
  0
24238
25214
  );
24239
25215
  }
@@ -24477,7 +25453,7 @@ var DEFAULT_INDEXER_URLS = {
24477
25453
  mainnet: "https://nexus.indexer.polytope.technology/",
24478
25454
  testnet: "https://gargantua.indexer.polytope.technology/"
24479
25455
  };
24480
- var ORDER_QUERY2 = `
25456
+ var ORDER_QUERY3 = `
24481
25457
  query OrderSummary($commitment: String!) {
24482
25458
  iOrderV3s(filter: { commitment: { equalTo: $commitment } }) {
24483
25459
  nodes {
@@ -24570,7 +25546,7 @@ async function fetchOrder(fetchImpl, indexerUrl, commitment) {
24570
25546
  const response = await fetchImpl(indexerUrl, {
24571
25547
  method: "POST",
24572
25548
  headers: { "content-type": "application/json" },
24573
- body: JSON.stringify({ query: ORDER_QUERY2, variables: { commitment } }),
25549
+ body: JSON.stringify({ query: ORDER_QUERY3, variables: { commitment } }),
24574
25550
  signal: AbortSignal.timeout(15e3)
24575
25551
  });
24576
25552
  if (!response.ok) throw new Error(`Indexer responded ${response.status}`);
@@ -24676,12 +25652,8 @@ async function bootFiller(config, options) {
24676
25652
  }
24677
25653
  }
24678
25654
  };
24679
- const retired = retiredPairKeys(config.pairs ?? []);
24680
- if (retired.length > 0) {
24681
- logger.warn(
24682
- { keys: retired },
24683
- "These [[pairs]] keys are no longer read; prices come from limit orders, and the filler fills nothing until one is posted"
24684
- );
25655
+ if ("pairs" in config) {
25656
+ logger.warn("[[pairs]] is no longer read and can be deleted from the config");
24685
25657
  }
24686
25658
  logger.info("Resolving chain IDs from RPC endpoints...");
24687
25659
  const resolvedChains = await resolveChainConfigs(config.chains, {
@@ -24786,25 +25758,6 @@ async function bootFiller(config, options) {
24786
25758
  }
24787
25759
  logger.info("Initializing trading engine...");
24788
25760
  const assetRegistry = new AssetRegistry(configService, config.assets);
24789
- const configuredChainNames = resolvedChains.map((chain) => `EVM-${chain.chainId}`);
24790
- if (config.pairs?.length) {
24791
- assertPairSymbolsResolve(config.pairs, assetRegistry, configuredChainNames);
24792
- }
24793
- const adminStrategies = [];
24794
- const tradingPairs = (config.pairs ?? []).map((pair) => ({
24795
- token0: pair.token0,
24796
- token1: pair.token1
24797
- }));
24798
- tradingPairs.forEach((pair, pairIndex) => {
24799
- adminStrategies.push({
24800
- index: pairIndex,
24801
- pairIndex,
24802
- exotic: `${pair.token0}/${pair.token1}`,
24803
- token0: pair.token0,
24804
- token1: pair.token1,
24805
- sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
24806
- });
24807
- });
24808
25761
  const confirmationPolicy = assertConfirmationCoverage(
24809
25762
  config.confirmationPolicies,
24810
25763
  resolvedChains.map((c) => c.chainId)
@@ -24815,7 +25768,6 @@ async function bootFiller(config, options) {
24815
25768
  configService,
24816
25769
  chainClientManager,
24817
25770
  contractService,
24818
- tradingPairs,
24819
25771
  assetRegistry,
24820
25772
  { confirmationPolicy, fundingVenues, limitOrders: options.data.limitOrders }
24821
25773
  );
@@ -24923,8 +25875,8 @@ async function bootFiller(config, options) {
24923
25875
  if (vaultVenue) {
24924
25876
  vaultVenue.onTx = ({ chain, kind, txHash, sponsored, movements }) => {
24925
25877
  const rows = movements.length > 0 ? movements.map((move) => {
24926
- const underlying = { token: move.symbol, amount: formatUnits6(move.amount, move.decimals) };
24927
- const shares = move.shares === void 0 ? { token: null, amount: null } : { token: move.shareSymbol, amount: formatUnits6(move.shares, move.shareDecimals) };
25878
+ const underlying = { token: move.symbol, amount: formatUnits7(move.amount, move.decimals) };
25879
+ const shares = move.shares === void 0 ? { token: null, amount: null } : { token: move.shareSymbol, amount: formatUnits7(move.shares, move.shareDecimals) };
24928
25880
  const out = kind === "sweep" ? underlying : shares;
24929
25881
  const back = kind === "sweep" ? shares : underlying;
24930
25882
  return { token: out.token, amount: out.amount, to: move.vault, tokenIn: back.token, amountIn: back.amount };
@@ -24934,21 +25886,21 @@ async function bootFiller(config, options) {
24934
25886
  }
24935
25887
  };
24936
25888
  }
24937
- const token1 = {};
24938
- for (const pair of config.pairs ?? []) {
24939
- if (normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)) continue;
24940
- for (const chainName2 of configuredChainNames) {
24941
- const address = assetRegistry.getAddress(pair.token1, chainName2);
24942
- if (!address) continue;
24943
- const list = token1[chainName2] ?? (token1[chainName2] = []);
24944
- if (!list.includes(address)) list.push(address);
25889
+ const bookTokens = async () => {
25890
+ const { books } = await limitOrderService.books();
25891
+ const symbols = new Set(books.flatMap((book) => [normalizeSymbol(book.base), normalizeSymbol(book.quote)]));
25892
+ const tokens = {};
25893
+ for (const chainId of configService.getConfiguredChainIds()) {
25894
+ const chainName2 = `EVM-${chainId}`;
25895
+ tokens[chainName2] = [...symbols].flatMap((symbol) => assetRegistry.getAddress(symbol, chainName2) ?? []);
24945
25896
  }
24946
- }
25897
+ return tokens;
25898
+ };
24947
25899
  const balanceProvider = new BalanceProvider({
24948
25900
  chainClientManager,
24949
25901
  configService,
24950
25902
  fillerAddress: runtimeSigner.address,
24951
- token1,
25903
+ tokens: bookTokens,
24952
25904
  vaultBalances: vaultVenue,
24953
25905
  hyperbridge: intentFiller.hyperbridgeConnection,
24954
25906
  substratePrivateKey: config.simplex.substratePrivateKey
@@ -24991,7 +25943,6 @@ async function bootFiller(config, options) {
24991
25943
  logger.info(
24992
25944
  {
24993
25945
  chains: resolvedChains.map((c) => c.chainId),
24994
- pairs: (config.pairs ?? []).map((p) => `${p.token0}/${p.token1}`),
24995
25946
  maxConcurrentOrders: config.simplex.maxConcurrentOrders ?? DEFAULT_MAX_CONCURRENT_ORDERS,
24996
25947
  watchOnlyChains: watchOnlyChains.length > 0 ? watchOnlyChains : void 0
24997
25948
  },
@@ -25009,12 +25960,32 @@ async function bootFiller(config, options) {
25009
25960
  activity.detach();
25010
25961
  if (options.ownsData) await options.data.close?.();
25011
25962
  };
25963
+ const tokenSender = new TokenSender(
25964
+ chainClientManager,
25965
+ runtimeSigner.address,
25966
+ () => config.vault?.vaults ?? [],
25967
+ userOpSender,
25968
+ configService
25969
+ );
25970
+ const bridgeTopUp = config.simplex.substratePrivateKey && network === "mainnet" ? new BridgeTopUp({
25971
+ clientManager: chainClientManager,
25972
+ delegation: new DelegationService(chainClientManager, configService, runtimeSigner),
25973
+ configService,
25974
+ tokenSender,
25975
+ state: options.data.state,
25976
+ storagePath: join(options.data.dataDir ?? process.cwd(), ".hyperbridge-cache"),
25977
+ indexerUrl,
25978
+ solver: runtimeSigner.address,
25979
+ substratePublicKey: toHex5((await deriveSubstrateKeyPair(config.simplex.substratePrivateKey)).publicKey),
25980
+ hyperbridge: intentFiller.hyperbridgeConnection,
25981
+ loggers: options.loggers
25982
+ }) : void 0;
25983
+ void bridgeTopUp?.restore();
25012
25984
  return {
25013
25985
  intentFiller,
25014
25986
  limitOrders: limitOrderService,
25015
25987
  balanceProvider,
25016
25988
  vaultVenue,
25017
- adminStrategies,
25018
25989
  haltControls,
25019
25990
  activity,
25020
25991
  data: options.data,
@@ -25026,9 +25997,6 @@ async function bootFiller(config, options) {
25026
25997
  confirmationPolicy,
25027
25998
  loggers: options.loggers,
25028
25999
  assetRegistry,
25029
- engine,
25030
- tradingPairs,
25031
- balanceTokens: token1,
25032
26000
  rebalancingService,
25033
26001
  resolvedChains,
25034
26002
  fillerAddress: runtimeSigner.address,
@@ -25038,13 +26006,8 @@ async function bootFiller(config, options) {
25038
26006
  config,
25039
26007
  configPath: options.configPath,
25040
26008
  startedAt: Date.now(),
25041
- tokenSender: new TokenSender(
25042
- chainClientManager,
25043
- runtimeSigner.address,
25044
- () => config.vault?.vaults ?? [],
25045
- userOpSender,
25046
- configService
25047
- ),
26009
+ tokenSender,
26010
+ bridgeTopUp,
25048
26011
  vaultPreflight: async (vaults) => {
25049
26012
  const byChain = {};
25050
26013
  for (const row of vaults) {
@@ -26057,7 +27020,7 @@ var OrderScanner = class _OrderScanner {
26057
27020
  };
26058
27021
 
26059
27022
  // src/data/indexer-inventory.ts
26060
- import { formatUnits as formatUnits7 } from "viem";
27023
+ import { formatUnits as formatUnits8 } from "viem";
26061
27024
  var READ_TIMEOUT_MS = 1e4;
26062
27025
  var SETTLED_AFTER_MS = 10 * 6e4;
26063
27026
  var BATCH = 40;
@@ -26134,7 +27097,7 @@ ${queries.join("\n")}
26134
27097
  if (!token) continue;
26135
27098
  let amount;
26136
27099
  try {
26137
- amount = Number(formatUnits7(BigInt(row.balance), token.decimals));
27100
+ amount = Number(formatUnits8(BigInt(row.balance), token.decimals));
26138
27101
  } catch {
26139
27102
  continue;
26140
27103
  }
@@ -26249,7 +27212,7 @@ var LimitOrderController = class {
26249
27212
  }));
26250
27213
  return this.indexed;
26251
27214
  }
26252
- /** The pairs the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
27215
+ /** The books the orderbook lists, the smallest payout each token may carry, and its tokens per chain. */
26253
27216
  books() {
26254
27217
  return this.service.books();
26255
27218
  }
@@ -26281,85 +27244,6 @@ var LimitOrderController = class {
26281
27244
  return cancelled;
26282
27245
  }
26283
27246
  };
26284
- var PairController = class {
26285
- constructor(runtime, persist) {
26286
- this.runtime = runtime;
26287
- this.persist = persist;
26288
- }
26289
- get pairs() {
26290
- return this.runtime.config.pairs ?? [];
26291
- }
26292
- list() {
26293
- return this.pairs.map((pair, index) => ({
26294
- index,
26295
- token0: pair.token0,
26296
- token1: pair.token1,
26297
- sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
26298
- }));
26299
- }
26300
- /**
26301
- * Opens a new market on the running engine.
26302
- *
26303
- * Validated against the same rules boot applies, so a pair accepted here is
26304
- * one a restart would also accept. New `assets` are registered first, because
26305
- * the pair's symbols have to resolve before the market can be hydrated.
26306
- */
26307
- async add(pair, options) {
26308
- const { engine, assetRegistry, adminStrategies, balanceTokens, config } = this.runtime;
26309
- const assets = options?.assets;
26310
- const nextAssets = { ...config.assets ?? {}, ...assets ?? {} };
26311
- const nextPairs = [...this.pairs, pair];
26312
- validatePairConfigs(nextPairs, nextAssets);
26313
- if (assets && Object.keys(assets).length > 0) assetRegistry.addAssets(assets);
26314
- assertPairSymbolsResolve(
26315
- [pair],
26316
- assetRegistry,
26317
- this.runtime.resolvedChains.map((chain) => formatChainKey(chain.chainId))
26318
- );
26319
- const tradingPair = { token0: pair.token0, token1: pair.token1 };
26320
- engine.addPair(tradingPair);
26321
- config.pairs = nextPairs;
26322
- if (assets && Object.keys(assets).length > 0) config.assets = nextAssets;
26323
- if (normalizeSymbol(pair.token0) !== normalizeSymbol(pair.token1)) {
26324
- for (const chain of this.runtime.resolvedChains) {
26325
- const chainName2 = formatChainKey(chain.chainId);
26326
- const address = assetRegistry.getAddress(pair.token1, chainName2);
26327
- if (!address) continue;
26328
- const list = balanceTokens[chainName2] ?? (balanceTokens[chainName2] = []);
26329
- if (!list.includes(address)) list.push(address);
26330
- }
26331
- }
26332
- const pairIndex = nextPairs.length - 1;
26333
- adminStrategies.push({
26334
- index: adminStrategies.reduce((max, s) => Math.max(max, s.index), -1) + 1,
26335
- pairIndex,
26336
- exotic: `${pair.token0}/${pair.token1}`,
26337
- token0: pair.token0,
26338
- token1: pair.token1,
26339
- sameToken: normalizeSymbol(pair.token0) === normalizeSymbol(pair.token1)
26340
- });
26341
- await this.persist();
26342
- return this.list()[pairIndex];
26343
- }
26344
- /** Closes a market. Orders already in flight against it still complete. */
26345
- async remove(index) {
26346
- const { engine, tradingPairs, adminStrategies, config } = this.runtime;
26347
- if (index < 0 || index >= this.pairs.length) throw new Error(`Unknown pair ${index}`);
26348
- engine.removePair(tradingPairs[index]);
26349
- config.pairs = this.pairs.filter((_, i) => i !== index);
26350
- const position = adminStrategies.findIndex((s) => s.pairIndex === index);
26351
- if (position >= 0) adminStrategies.splice(position, 1);
26352
- for (const strategy of adminStrategies) {
26353
- if (strategy.pairIndex > index) strategy.pairIndex -= 1;
26354
- }
26355
- await this.persist();
26356
- }
26357
- livePair(index) {
26358
- const pair = this.runtime.tradingPairs[index];
26359
- if (!pair) throw new Error(`Unknown pair ${index}`);
26360
- return pair;
26361
- }
26362
- };
26363
27247
  var ChainController = class {
26364
27248
  constructor(runtime, persist, scanner, ownsScanner) {
26365
27249
  this.runtime = runtime;
@@ -26665,7 +27549,6 @@ var Simplex = class _Simplex extends EventEmitter4 {
26665
27549
  this.stopped = false;
26666
27550
  this.logger = runtime.loggers.get("simplex");
26667
27551
  const persist = () => this.persistConfig();
26668
- this.pairs = new PairController(runtime, persist);
26669
27552
  this.chains = new ChainController(runtime, persist, runtime.orderScanner, ownedScanners.orders !== void 0);
26670
27553
  this.vaults = new VaultController(runtime, persist);
26671
27554
  this.assets = new AssetController(runtime, persist);
@@ -26815,8 +27698,7 @@ var Simplex = class _Simplex extends EventEmitter4 {
26815
27698
  startedAt: this.runtime.startedAt,
26816
27699
  uptimeSec: (Date.now() - this.runtime.startedAt) / 1e3,
26817
27700
  chains: this.runtime.resolvedChains.map((chain) => chain.chainId),
26818
- watchOnly: this.runtime.intentFiller.getWatchOnly(),
26819
- pairs: this.runtime.config.pairs?.length ?? 0
27701
+ watchOnly: this.runtime.intentFiller.getWatchOnly()
26820
27702
  };
26821
27703
  }
26822
27704
  /** Order-activity history, newest first; pass `beforeId` to page backwards. */
@@ -26893,7 +27775,6 @@ export {
26893
27775
  LoggerContext,
26894
27776
  MemoryDataStore,
26895
27777
  OrderScanner,
26896
- PairController,
26897
27778
  RebalanceController,
26898
27779
  SignerType,
26899
27780
  Simplex,
@@ -26912,14 +27793,12 @@ export {
26912
27793
  mpcVaultSigner,
26913
27794
  normalizeSymbol,
26914
27795
  parseChainKey,
26915
- pickAnchorStable,
26916
27796
  privateKeySigner,
26917
27797
  registrySymbols,
26918
27798
  secretPhraseSigner,
26919
27799
  turnkeySigner,
26920
27800
  validateAssetDefinitions,
26921
27801
  validateConfig,
26922
- validatePairConfigs,
26923
27802
  validateSecretPhrase,
26924
27803
  validateSignerConfig,
26925
27804
  validateVaultToml,