@stablechain/sdk 1.0.5 → 1.0.7

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
@@ -1737,46 +1737,88 @@ function isLiFiQuoteShape(v) {
1737
1737
 
1738
1738
 
1739
1739
 
1740
+
1741
+
1740
1742
  var C = CHAIN_CONFIGS;
1743
+ var XAUT0_DEPLOYMENTS = {
1744
+ ["ethereum" /* Ethereum */]: { address: "0x68749665FF8D2d112Fa859AA293F07A622782F38", decimals: 6, oftAdapter: "0xb9c2321BB7D0Db468f570D10A424d1Cc8EFd696C", approvalRequired: true },
1745
+ ["stable" /* Stable */]: { address: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb", decimals: 6, oftAdapter: "0xD8479f87686ed263D00Ca7505F86327dbeD4171A", approvalRequired: false },
1746
+ ["arbitrum" /* Arbitrum */]: { address: "0x40461291347e1eCbb09499F3371D3f17f10d7159", decimals: 6, oftAdapter: "0xf40542a7B66AD7C68C459EE3679635D2fDB6dF39", approvalRequired: false },
1747
+ ["ink" /* Ink */]: { address: "0xF50258D3c1dd88946C567920B986A12e65b50dAc", decimals: 6, oftAdapter: "0xA1bE1572B4beef24f812EfDc58bdc41D56a0dAB2", approvalRequired: false },
1748
+ ["hyperevm" /* HyperEVM */]: { address: "0xf4D9235269a96aaDaFc9aDAe454a0618eBE37949", decimals: 6, oftAdapter: "0x4E41cfc3F3B19E29E323D2c36F8f202a1e151dAF", approvalRequired: false },
1749
+ ["polygon" /* Polygon */]: { address: "0xF1815bd50389c46847f0Bda824eC8da914045D14", decimals: 6, oftAdapter: "0x5421Cf4288d8007D3c43AC4246eaFCe5b049e352", approvalRequired: false },
1750
+ ["plasma" /* Plasma */]: { address: "0x1B64B9025EEbb9A6239575dF9Ea4b9Ac46D4d193", decimals: 6, oftAdapter: "0x63aB93cBC9d4ecD9c4947b1A38F458147C08E6F7", approvalRequired: false },
1751
+ ["bsc" /* BSC */]: { address: "0x21cAef8A43163Eea865baeE23b9C2E327696A3bf", decimals: 6, oftAdapter: "0x53C3A64c8942288e12813C1F8457Db45980BcfC2", approvalRequired: false }
1752
+ };
1753
+ var USDT0_LOCKBOX_CHAINS = /* @__PURE__ */ new Set(["ethereum" /* Ethereum */, "sepolia" /* Sepolia */]);
1754
+ function bridgeChain(chain, lzEid, blockTimeSec, usdt0Adapter) {
1755
+ const base = C[chain];
1756
+ const tokens = {};
1757
+ if (usdt0Adapter) {
1758
+ tokens.usdt0 = {
1759
+ address: base.usdt,
1760
+ decimals: base.decimals,
1761
+ oftAdapter: usdt0Adapter,
1762
+ approvalRequired: USDT0_LOCKBOX_CHAINS.has(chain)
1763
+ };
1764
+ }
1765
+ const xaut0 = XAUT0_DEPLOYMENTS[chain];
1766
+ if (xaut0) tokens.xaut0 = xaut0;
1767
+ return { ...base, lzEid, blockTimeSec, tokens, oftAdapter: usdt0Adapter };
1768
+ }
1741
1769
  var BRIDGE_CHAIN_CONFIGS = {
1742
1770
  // ─── Testnet ───────────────────────────────────────────────────────────────
1743
- ["sepolia" /* Sepolia */]: { ...C["sepolia" /* Sepolia */], lzEid: 40161, oftAdapter: "0xc099cD946d5efCC35A99D64E808c1430cEf08126", blockTimeSec: 12 },
1744
- ["stable-testnet" /* StableTestnet */]: { ...C["stable-testnet" /* StableTestnet */], lzEid: 40374, oftAdapter: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", blockTimeSec: 0.7 },
1771
+ ["sepolia" /* Sepolia */]: bridgeChain("sepolia" /* Sepolia */, 40161, 12, "0xc099cD946d5efCC35A99D64E808c1430cEf08126"),
1772
+ ["stable-testnet" /* StableTestnet */]: bridgeChain("stable-testnet" /* StableTestnet */, 40374, 0.7, "0x779Ded0c9e1022225f8E0630b35a9b54bE713736"),
1745
1773
  // ─── Mainnet ──────────────────────────────────────────────────────────────
1746
- ["stable" /* Stable */]: { ...C["stable" /* Stable */], lzEid: 30396, oftAdapter: "0xedaba024be4d87974d5aB11C6Dd586963CcCB027", blockTimeSec: 0.7 },
1747
- ["ethereum" /* Ethereum */]: { ...C["ethereum" /* Ethereum */], lzEid: 30101, oftAdapter: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee", blockTimeSec: 12 },
1748
- ["arbitrum" /* Arbitrum */]: { ...C["arbitrum" /* Arbitrum */], lzEid: 30110, oftAdapter: "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92", blockTimeSec: 0.25 },
1749
- ["ink" /* Ink */]: { ...C["ink" /* Ink */], lzEid: 30339, oftAdapter: "0x1cB6De532588fCA4a21B7209DE7C456AF8434A65", blockTimeSec: 1 },
1750
- ["bera" /* Bera */]: { ...C["bera" /* Bera */], lzEid: 30362, oftAdapter: "0x3Dc96399109df5ceb2C226664A086140bD0379cB", blockTimeSec: 2 },
1751
- ["megaeth" /* MegaETH */]: { ...C["megaeth" /* MegaETH */], lzEid: 30398, oftAdapter: "0x9151434b16b9763660705744891fa906f660ecc5", blockTimeSec: 1 },
1752
- ["hyperevm" /* HyperEVM */]: { ...C["hyperevm" /* HyperEVM */], lzEid: 30367, oftAdapter: "0x904861a24F30EC96ea7CFC3bE9EA4B476d237e98", blockTimeSec: 1 },
1753
- ["polygon" /* Polygon */]: { ...C["polygon" /* Polygon */], lzEid: 30109, oftAdapter: "0x6BA10300f0DC58B7a1e4c0e41f5daBb7D7829e13", blockTimeSec: 1.5 },
1754
- ["plasma" /* Plasma */]: { ...C["plasma" /* Plasma */], lzEid: 30383, oftAdapter: "0x02ca37966753bDdDf11216B73B16C1dE756A7CF9", blockTimeSec: 1 },
1755
- ["optimism" /* Optimism */]: { ...C["optimism" /* Optimism */], lzEid: 30111, oftAdapter: "0xF03b4d9AC1D5d1E7c4cEf54C2A313b9fe051A0aD", blockTimeSec: 2 },
1756
- ["unichain" /* Unichain */]: { ...C["unichain" /* Unichain */], lzEid: 30320, oftAdapter: "0xc07bE8994D035631c36fb4a89C918CeFB2f03EC3", blockTimeSec: 1 },
1757
- ["sei" /* Sei */]: { ...C["sei" /* Sei */], lzEid: 30280, oftAdapter: "0x56Fe74A2e3b484b921c447357203431a3485CC60", blockTimeSec: 0.5 },
1758
- ["flare" /* Flare */]: { ...C["flare" /* Flare */], lzEid: 30295, oftAdapter: "0x567287d2A9829215a37e3B88843d32f9221E7588", blockTimeSec: 1.1 },
1759
- ["xlayer" /* XLayer */]: { ...C["xlayer" /* XLayer */], lzEid: 30274, oftAdapter: "0x94BCCa6bdfd6A61817Ab0E960bFedE4984505554", blockTimeSec: 1 },
1760
- ["rootstock" /* Rootstock */]: { ...C["rootstock" /* Rootstock */], lzEid: 30333, oftAdapter: "0x1a594d5d5d1c426281C1064B07f23F57B2716B61", blockTimeSec: 31 }
1774
+ ["stable" /* Stable */]: bridgeChain("stable" /* Stable */, 30396, 0.7, "0xedaba024be4d87974d5aB11C6Dd586963CcCB027"),
1775
+ ["ethereum" /* Ethereum */]: bridgeChain("ethereum" /* Ethereum */, 30101, 12, "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee"),
1776
+ ["arbitrum" /* Arbitrum */]: bridgeChain("arbitrum" /* Arbitrum */, 30110, 0.25, "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92"),
1777
+ ["ink" /* Ink */]: bridgeChain("ink" /* Ink */, 30339, 1, "0x1cB6De532588fCA4a21B7209DE7C456AF8434A65"),
1778
+ ["bera" /* Bera */]: bridgeChain("bera" /* Bera */, 30362, 2, "0x3Dc96399109df5ceb2C226664A086140bD0379cB"),
1779
+ ["megaeth" /* MegaETH */]: bridgeChain("megaeth" /* MegaETH */, 30398, 1, "0x9151434b16b9763660705744891fa906f660ecc5"),
1780
+ ["hyperevm" /* HyperEVM */]: bridgeChain("hyperevm" /* HyperEVM */, 30367, 1, "0x904861a24F30EC96ea7CFC3bE9EA4B476d237e98"),
1781
+ ["polygon" /* Polygon */]: bridgeChain("polygon" /* Polygon */, 30109, 1.5, "0x6BA10300f0DC58B7a1e4c0e41f5daBb7D7829e13"),
1782
+ ["plasma" /* Plasma */]: bridgeChain("plasma" /* Plasma */, 30383, 1, "0x02ca37966753bDdDf11216B73B16C1dE756A7CF9"),
1783
+ ["optimism" /* Optimism */]: bridgeChain("optimism" /* Optimism */, 30111, 2, "0xF03b4d9AC1D5d1E7c4cEf54C2A313b9fe051A0aD"),
1784
+ ["unichain" /* Unichain */]: bridgeChain("unichain" /* Unichain */, 30320, 1, "0xc07bE8994D035631c36fb4a89C918CeFB2f03EC3"),
1785
+ ["sei" /* Sei */]: bridgeChain("sei" /* Sei */, 30280, 0.5, "0x56Fe74A2e3b484b921c447357203431a3485CC60"),
1786
+ ["flare" /* Flare */]: bridgeChain("flare" /* Flare */, 30295, 1.1, "0x567287d2A9829215a37e3B88843d32f9221E7588"),
1787
+ ["xlayer" /* XLayer */]: bridgeChain("xlayer" /* XLayer */, 30274, 1, "0x94BCCa6bdfd6A61817Ab0E960bFedE4984505554"),
1788
+ ["rootstock" /* Rootstock */]: bridgeChain("rootstock" /* Rootstock */, 30333, 31, "0x1a594d5d5d1c426281C1064B07f23F57B2716B61"),
1789
+ // XAUt0 only — BSC has no USDT0 deployment. Block time measured 2026-08-20.
1790
+ ["bsc" /* BSC */]: bridgeChain("bsc" /* BSC */, 30102, 0.45)
1791
+ };
1792
+ function bridgeAssetFor(cfg, token) {
1793
+ const needle = token.toLowerCase();
1794
+ return Object.keys(cfg.tokens).find(
1795
+ (asset) => _optionalChain([cfg, 'access', _36 => _36.tokens, 'access', _37 => _37[asset], 'optionalAccess', _38 => _38.address, 'access', _39 => _39.toLowerCase, 'call', _40 => _40()]) === needle
1796
+ );
1797
+ }
1798
+ function describeAssets(cfg) {
1799
+ const assets = Object.entries(cfg.tokens);
1800
+ if (assets.length === 0) return "no OFT assets";
1801
+ return assets.map(([asset, token]) => `${asset} (${token.address})`).join(" or ");
1802
+ }
1803
+ var SEND_PARAM_INPUT = {
1804
+ name: "_sendParam",
1805
+ type: "tuple",
1806
+ components: [
1807
+ { name: "dstEid", type: "uint32" },
1808
+ { name: "to", type: "bytes32" },
1809
+ { name: "amountLD", type: "uint256" },
1810
+ { name: "minAmountLD", type: "uint256" },
1811
+ { name: "extraOptions", type: "bytes" },
1812
+ { name: "composeMsg", type: "bytes" },
1813
+ { name: "oftCmd", type: "bytes" }
1814
+ ]
1761
1815
  };
1762
1816
  var OFT_ADAPTER_ABI = [
1763
1817
  {
1764
1818
  name: "quoteSend",
1765
1819
  type: "function",
1766
1820
  inputs: [
1767
- {
1768
- name: "_sendParam",
1769
- type: "tuple",
1770
- components: [
1771
- { name: "dstEid", type: "uint32" },
1772
- { name: "to", type: "bytes32" },
1773
- { name: "amountLD", type: "uint256" },
1774
- { name: "minAmountLD", type: "uint256" },
1775
- { name: "extraOptions", type: "bytes" },
1776
- { name: "composeMsg", type: "bytes" },
1777
- { name: "oftCmd", type: "bytes" }
1778
- ]
1779
- },
1821
+ SEND_PARAM_INPUT,
1780
1822
  { name: "_payInLzToken", type: "bool" }
1781
1823
  ],
1782
1824
  outputs: [
@@ -1792,22 +1834,48 @@ var OFT_ADAPTER_ABI = [
1792
1834
  stateMutability: "view"
1793
1835
  },
1794
1836
  {
1795
- name: "send",
1837
+ // OFT-level accounting, independent of the LayerZero messaging fee: the
1838
+ // pathway's accepted range, any fee the adapter charges itself, and what the
1839
+ // recipient actually ends up credited.
1840
+ name: "quoteOFT",
1796
1841
  type: "function",
1797
1842
  inputs: [
1843
+ SEND_PARAM_INPUT
1844
+ ],
1845
+ outputs: [
1798
1846
  {
1799
- name: "_sendParam",
1847
+ name: "oftLimit",
1800
1848
  type: "tuple",
1801
1849
  components: [
1802
- { name: "dstEid", type: "uint32" },
1803
- { name: "to", type: "bytes32" },
1804
- { name: "amountLD", type: "uint256" },
1805
1850
  { name: "minAmountLD", type: "uint256" },
1806
- { name: "extraOptions", type: "bytes" },
1807
- { name: "composeMsg", type: "bytes" },
1808
- { name: "oftCmd", type: "bytes" }
1851
+ { name: "maxAmountLD", type: "uint256" }
1809
1852
  ]
1810
1853
  },
1854
+ {
1855
+ name: "oftFeeDetails",
1856
+ type: "tuple[]",
1857
+ components: [
1858
+ // Signed: a negative detail is a credit to the sender, not a charge.
1859
+ { name: "feeAmountLD", type: "int256" },
1860
+ { name: "description", type: "string" }
1861
+ ]
1862
+ },
1863
+ {
1864
+ name: "oftReceipt",
1865
+ type: "tuple",
1866
+ components: [
1867
+ { name: "amountSentLD", type: "uint256" },
1868
+ { name: "amountReceivedLD", type: "uint256" }
1869
+ ]
1870
+ }
1871
+ ],
1872
+ stateMutability: "view"
1873
+ },
1874
+ {
1875
+ name: "send",
1876
+ type: "function",
1877
+ inputs: [
1878
+ SEND_PARAM_INPUT,
1811
1879
  {
1812
1880
  name: "_fee",
1813
1881
  type: "tuple",
@@ -1880,7 +1948,7 @@ var ScanHttpError = class extends Error {
1880
1948
  async function scanFetch(path, timeoutMs = 8e3) {
1881
1949
  const res = await fetch(`${LZ_SCAN_API}${path}`, { signal: AbortSignal.timeout(timeoutMs) });
1882
1950
  if (!res.ok) throw new ScanHttpError(res.status, path);
1883
- return res.json();
1951
+ return await res.json();
1884
1952
  }
1885
1953
  var TERMINAL_FAILURE_STATUSES = /* @__PURE__ */ new Set([
1886
1954
  "FAILED",
@@ -1896,62 +1964,293 @@ function median(values) {
1896
1964
  const mid = Math.floor(sorted.length / 2);
1897
1965
  return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
1898
1966
  }
1899
- function buildLzOptions(gasLimit = 0n) {
1967
+ var WORKER_EXECUTOR = 1;
1968
+ var OPTION_LZRECEIVE = 1;
1969
+ var OPTION_NATIVE_DROP = 2;
1970
+ function lzOption(optionType, payload) {
1971
+ const payloadBytes = (payload.length - 2) / 2;
1972
+ return _viem.concat.call(void 0, [
1973
+ _viem.numberToHex.call(void 0, WORKER_EXECUTOR, { size: 1 }),
1974
+ _viem.numberToHex.call(void 0, 1 + payloadBytes, { size: 2 }),
1975
+ _viem.numberToHex.call(void 0, optionType, { size: 1 }),
1976
+ payload
1977
+ ]);
1978
+ }
1979
+ function buildLzOptions(gasLimit = 0n, nativeDrop = 0n, dropReceiver) {
1980
+ const options = [
1981
+ lzOption(OPTION_LZRECEIVE, _viem.numberToHex.call(void 0, gasLimit, { size: 16 }))
1982
+ ];
1983
+ if (nativeDrop > 0n) {
1984
+ if (!dropReceiver) {
1985
+ throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
1986
+ "recipient",
1987
+ dropReceiver,
1988
+ "buildLzOptions: a native drop needs a receiver address"
1989
+ );
1990
+ }
1991
+ options.push(
1992
+ lzOption(
1993
+ OPTION_NATIVE_DROP,
1994
+ _viem.concat.call(void 0, [
1995
+ _viem.numberToHex.call(void 0, nativeDrop, { size: 16 }),
1996
+ _viem.padHex.call(void 0, dropReceiver, { dir: "left", size: 32 })
1997
+ ])
1998
+ )
1999
+ );
2000
+ }
1900
2001
  return _viem.concat.call(void 0, [
1901
2002
  _viem.numberToHex.call(void 0, 3, { size: 2 }),
1902
2003
  // TYPE_3 header
1903
- _viem.numberToHex.call(void 0, 1, { size: 1 }),
1904
- // Executor worker ID
1905
- _viem.numberToHex.call(void 0, 17, { size: 2 }),
1906
- // option payload length = 17 bytes
1907
- _viem.numberToHex.call(void 0, 1, { size: 1 }),
1908
- // LzReceive option type
1909
- _viem.numberToHex.call(void 0, gasLimit, { size: 16 })
1910
- // gas limit as uint128
2004
+ ...options
1911
2005
  ]);
1912
2006
  }
2007
+ var FALLBACK_SEND_GAS = 400000n;
2008
+ var DEFAULT_FEE_BUFFER_BPS = 500;
2009
+ var MAX_FEE_BUFFER_BPS = 1e4;
2010
+ function padFee(nativeFee, bufferBps) {
2011
+ return nativeFee * BigInt(1e4 + bufferBps) / 10000n;
2012
+ }
2013
+ function resolveFeeBuffer(feeBufferBps, method) {
2014
+ if (feeBufferBps === void 0) return DEFAULT_FEE_BUFFER_BPS;
2015
+ if (!Number.isInteger(feeBufferBps) || feeBufferBps < 0 || feeBufferBps > MAX_FEE_BUFFER_BPS) {
2016
+ throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
2017
+ "feeBufferBps",
2018
+ feeBufferBps,
2019
+ `${method}: feeBufferBps must be an integer between 0 and ${MAX_FEE_BUFFER_BPS}`
2020
+ );
2021
+ }
2022
+ return feeBufferBps;
2023
+ }
2024
+ function usableQuote(quote, amountLD) {
2025
+ if (!_optionalChain([quote, 'optionalAccess', _41 => _41.oft])) return quote;
2026
+ return quote.oft.amountSentLD <= amountLD ? quote : void 0;
2027
+ }
2028
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
1913
2029
  var LayerZeroService = class {
1914
2030
  constructor(config) {
1915
2031
  this.config = config;
2032
+ this.rpcById = new Map(
2033
+ Object.entries(_nullishCoalesce(config.rpcOverrides, () => ( {}))).flatMap(([chain, rpc]) => {
2034
+ const id = _optionalChain([CHAIN_CONFIGS, 'access', _42 => _42[chain], 'optionalAccess', _43 => _43.id]);
2035
+ return id === void 0 || !rpc ? [] : [[id, rpc.replace(/\/$/, "")]];
2036
+ })
2037
+ );
2038
+ }
2039
+ /**
2040
+ * The RPC to read and broadcast on for a chain: the caller's override when they
2041
+ * gave one, else the built-in public endpoint. Every bridge read goes through
2042
+ * here — a backend quoting on a loop needs an endpoint it controls.
2043
+ */
2044
+ rpcFor(cfg) {
2045
+ return _nullishCoalesce(this.rpcById.get(cfg.id), () => ( cfg.rpc));
1916
2046
  }
1917
2047
  async quoteBridge(params) {
1918
- const [fee, estimate] = await Promise.all([
1919
- this.quoteFee(params, "quoteBridge"),
2048
+ return this.buildQuote(this.plan(params, "quoteBridge"), params, "quoteBridge");
2049
+ }
2050
+ /**
2051
+ * The full quote for a planned send. `quoteBridge` reports one; the unsigned
2052
+ * build needs the same numbers and must not disagree with what a caller was
2053
+ * shown, so both come from here.
2054
+ *
2055
+ * `account` is who the gas simulation runs as — this service's signer when it
2056
+ * has one, and the address a prepared send will be signed by when it does not.
2057
+ */
2058
+ async buildQuote(plan, params, method, opts = {}) {
2059
+ const [{ fee, oft }, estimate] = await Promise.all([
2060
+ this.quotePathway(plan, method, opts.requireOft),
1920
2061
  this.estimateBridgeTime(params).catch(() => void 0)
1921
2062
  ]);
1922
- return { ...fee, toAmount: params.amount, estimate };
2063
+ const srcGas = await this.estimateSendGas(plan, fee, oft, opts.account);
2064
+ return {
2065
+ ...fee,
2066
+ // What a send built from this quote will actually carry as its `value`.
2067
+ nativeFeeMax: padFee(fee.nativeFee, plan.feeBufferBps),
2068
+ oft,
2069
+ // What the recipient is actually credited: input minus any OFT fee and
2070
+ // minus dust. Falls back to the input when quoteOFT could not be read.
2071
+ toAmount: _nullishCoalesce(_optionalChain([oft, 'optionalAccess', _44 => _44.amountReceived]), () => ( params.amount)),
2072
+ srcGas,
2073
+ totalCostWei: srcGas ? srcGas.costWei + fee.nativeFee : void 0,
2074
+ estimate
2075
+ };
1923
2076
  }
1924
2077
  /**
1925
- * LayerZero messaging fee for a send, with no delivery estimate attached — the
1926
- * send path needs the fee and nothing else, and must not wait on Scan for it.
2078
+ * The LayerZero messaging fee for a send, plus the OFT's own accounting.
2079
+ *
2080
+ * The fee is required — a send cannot be built without it. The OFT accounting is
2081
+ * best-effort for a quote: an adapter that does not answer `quoteOFT` costs a
2082
+ * caller the fee breakdown and the exact output amount, not the whole quote.
2083
+ *
2084
+ * `requireOft` flips that for the paths that go on to build a send. Without the
2085
+ * receipt the only slippage floor left is the full input amount, which reverts
2086
+ * with `SlippageExceeded` the moment a pathway charges a fee — so a send that
2087
+ * cannot read `quoteOFT` is one to refuse loudly rather than assemble blind.
1927
2088
  */
1928
- async quoteFee({ fromChain, toChain, fromToken, toToken, amount, recipient }, method) {
1929
- const recipientAddr = _nullishCoalesce(recipient, () => ( "0x0000000000000000000000000000000000000000"));
1930
- this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, method);
1931
- const { src, dst } = this.resolveChains(fromChain, toChain, fromToken, toToken);
1932
- const oftAdapter = src.oftAdapter;
1933
- const amountLD = _viem.parseUnits.call(void 0, String(amount), src.decimals);
1934
- try {
1935
- const { nativeFee, lzTokenFee } = await _viem.createPublicClient.call(void 0, {
1936
- chain: makeChain(src.id, src.rpc),
1937
- transport: _viem.http.call(void 0, src.rpc)
1938
- }).readContract({
1939
- address: oftAdapter,
2089
+ async quotePathway(plan, method, requireOft = false) {
2090
+ const sendParam = this.buildSendParams(plan, 0n);
2091
+ const [msgFee, oft] = await Promise.all([
2092
+ plan.pc.readContract({
2093
+ address: plan.oftAdapter,
1940
2094
  abi: OFT_ADAPTER_ABI,
1941
2095
  functionName: "quoteSend",
1942
- args: [this.buildSendParams(dst, amountLD, recipientAddr), false]
2096
+ args: [sendParam, false]
2097
+ }).catch((err) => {
2098
+ throw new (0, _chunkQQ26UI3Fcjs.StableQuoteError)({
2099
+ provider: "layerzero",
2100
+ shortMessage: plan.nativeDrop > 0n ? `${method}: quoteSend call failed \u2014 a dstNativeDrop over the pathway's executor nativeCap is the usual cause` : `${method}: quoteSend call failed`,
2101
+ cause: err
2102
+ });
2103
+ }),
2104
+ this.readOftQuote(plan, sendParam).catch((err) => {
2105
+ if (!requireOft) return void 0;
2106
+ throw new (0, _chunkQQ26UI3Fcjs.StableQuoteError)({
2107
+ provider: "layerzero",
2108
+ shortMessage: `${method}: quoteOFT call failed \u2014 the adapter's receipt is what floors the send at what it would actually deliver, and there is no safe default for it`,
2109
+ cause: err
2110
+ });
2111
+ })
2112
+ ]);
2113
+ const { nativeFee, lzTokenFee } = msgFee;
2114
+ return { fee: { nativeFee, lzTokenFee }, oft };
2115
+ }
2116
+ /**
2117
+ * The adapter's own view of a send: accepted range, its fees, and what lands on
2118
+ * the far side. Throws when the adapter will not answer; `quotePathway` decides
2119
+ * whether that is fatal for the caller it is serving.
2120
+ */
2121
+ async readOftQuote(plan, sendParam) {
2122
+ const [limit, feeDetails, receipt] = await plan.pc.readContract({
2123
+ address: plan.oftAdapter,
2124
+ abi: OFT_ADAPTER_ABI,
2125
+ functionName: "quoteOFT",
2126
+ args: [sendParam]
2127
+ });
2128
+ const decimals = plan.srcToken.decimals;
2129
+ return {
2130
+ amountSentLD: receipt.amountSentLD,
2131
+ amountReceivedLD: receipt.amountReceivedLD,
2132
+ amountReceived: Number(_viem.formatUnits.call(void 0, receipt.amountReceivedLD, decimals)),
2133
+ fees: feeDetails.map((detail) => ({
2134
+ amountLD: detail.feeAmountLD,
2135
+ amount: Number(_viem.formatUnits.call(void 0, detail.feeAmountLD, decimals)),
2136
+ description: detail.description
2137
+ })),
2138
+ minAmountLD: limit.minAmountLD,
2139
+ maxAmountLD: limit.maxAmountLD
2140
+ };
2141
+ }
2142
+ /**
2143
+ * Source-chain cost of the `send` transaction.
2144
+ *
2145
+ * Simulating the real call is the only honest answer, and it needs a sender with
2146
+ * the allowance and the native balance already in place — which a quote taken
2147
+ * before approving does not have. When the simulation reverts, fall back to a
2148
+ * measured constant so the caller still has a cost to show, flagged as such.
2149
+ */
2150
+ async estimateSendGas(plan, fee, oft, sender) {
2151
+ const gasPriceWei = await this.gasPrice(plan);
2152
+ if (gasPriceWei === void 0) return void 0;
2153
+ let gas = FALLBACK_SEND_GAS;
2154
+ let source = "fallback";
2155
+ const account = _nullishCoalesce(sender, () => ( this.signerAddress()));
2156
+ if (account) {
2157
+ const sendFee = {
2158
+ nativeFee: padFee(fee.nativeFee, plan.feeBufferBps),
2159
+ lzTokenFee: fee.lzTokenFee
2160
+ };
2161
+ try {
2162
+ gas = await plan.pc.estimateContractGas({
2163
+ address: plan.oftAdapter,
2164
+ abi: OFT_ADAPTER_ABI,
2165
+ functionName: "send",
2166
+ args: [
2167
+ this.buildSendParams(plan, _nullishCoalesce(_optionalChain([oft, 'optionalAccess', _45 => _45.amountReceivedLD]), () => ( plan.amountLD))),
2168
+ sendFee,
2169
+ account
2170
+ ],
2171
+ account,
2172
+ value: sendFee.nativeFee
2173
+ });
2174
+ source = "simulated";
2175
+ } catch (e5) {
2176
+ }
2177
+ }
2178
+ return { gas, gasPriceWei, costWei: gas * gasPriceWei, source };
2179
+ }
2180
+ /** Price per gas unit on the source chain, or undefined if the RPC won't say. */
2181
+ async gasPrice(plan) {
2182
+ try {
2183
+ const fees = await plan.pc.estimateFeesPerGas();
2184
+ const price = _nullishCoalesce(fees.maxFeePerGas, () => ( fees.gasPrice));
2185
+ if (price !== void 0) return price;
2186
+ } catch (e6) {
2187
+ }
2188
+ try {
2189
+ return await plan.pc.getGasPrice();
2190
+ } catch (e7) {
2191
+ return void 0;
2192
+ }
2193
+ }
2194
+ /**
2195
+ * The `approve` amounts to send, in order, before `send` can pull `amountLD` —
2196
+ * empty when the adapter needs no allowance or the existing one already covers
2197
+ * the send.
2198
+ *
2199
+ * A short but non-zero allowance takes two approvals, not one. Legacy USDT on
2200
+ * Ethereum — the only token a lockbox pathway ever approves — refuses to
2201
+ * overwrite a non-zero allowance with another non-zero value:
2202
+ *
2203
+ * require(!((_value != 0) && (allowed[msg.sender][_spender] != 0)))
2204
+ *
2205
+ * so a holder who once approved 5 USDT and now bridges 10 gets a reverted
2206
+ * approve and pays the gas for nothing. Zeroing first is harmless on every
2207
+ * other ERC-20, so it is unconditional rather than sniffed per token.
2208
+ */
2209
+ async approvalPlan(plan, owner, amountLD) {
2210
+ if (plan.srcToken.approvalRequired === false) return [];
2211
+ const allowance = await plan.pc.readContract({
2212
+ address: plan.srcToken.address,
2213
+ abi: _viem.erc20Abi,
2214
+ functionName: "allowance",
2215
+ args: [owner, plan.oftAdapter]
2216
+ });
2217
+ if (allowance >= amountLD) return [];
2218
+ return allowance > 0n ? [0n, amountLD] : [amountLD];
2219
+ }
2220
+ /** Send one `approve` and wait for it to land. */
2221
+ async sendApprove(plan, wc, value) {
2222
+ const { src, asset, srcToken, oftAdapter, pc, chain: srcChain } = plan;
2223
+ const what = value === 0n ? "allowance reset" : "approve";
2224
+ let approveHash;
2225
+ try {
2226
+ approveHash = await wc.writeContract({
2227
+ address: srcToken.address,
2228
+ abi: _viem.erc20Abi,
2229
+ functionName: "approve",
2230
+ args: [oftAdapter, value],
2231
+ account: wc.account,
2232
+ chain: srcChain
1943
2233
  });
1944
- return { nativeFee, lzTokenFee };
1945
2234
  } catch (err) {
1946
- throw new (0, _chunkQQ26UI3Fcjs.StableQuoteError)({
1947
- provider: "layerzero",
1948
- shortMessage: `${method}: quoteSend call failed`,
2235
+ throw new (0, _chunkQQ26UI3Fcjs.StableTransactionError)({
2236
+ phase: "approve",
2237
+ shortMessage: `bridge: ${asset} ${what} failed`,
2238
+ chainId: src.id,
1949
2239
  cause: err
1950
2240
  });
1951
2241
  }
2242
+ const approveReceipt = await pc.waitForTransactionReceipt({ hash: approveHash });
2243
+ if (approveReceipt.status === "reverted") {
2244
+ throw new (0, _chunkQQ26UI3Fcjs.StableTransactionError)({
2245
+ phase: "approve",
2246
+ shortMessage: `bridge: ${asset} ${what} reverted on-chain`,
2247
+ txHash: approveReceipt.transactionHash,
2248
+ chainId: src.id
2249
+ });
2250
+ }
1952
2251
  }
1953
- async bridge({ fromChain, toChain, fromToken, toToken, amount, recipient, quote }) {
1954
- const from = _nullishCoalesce(_optionalChain([this, 'access', _36 => _36.config, 'access', _37 => _37.walletClient, 'optionalAccess', _38 => _38.account, 'optionalAccess', _39 => _39.address]), () => ( _optionalChain([this, 'access', _40 => _40.config, 'access', _41 => _41.account, 'optionalAccess', _42 => _42.address])));
2252
+ async bridge(params) {
2253
+ const from = this.signerAddress();
1955
2254
  if (!from) {
1956
2255
  throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
1957
2256
  "signer",
@@ -1959,17 +2258,20 @@ var LayerZeroService = class {
1959
2258
  "bridge: requires a signer \u2014 set walletClient or account in StableConfig"
1960
2259
  );
1961
2260
  }
1962
- const recipientAddr = _nullishCoalesce(recipient, () => ( from));
1963
- this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, "bridge");
1964
- const { src, dst } = this.resolveChains(fromChain, toChain, fromToken, toToken);
1965
- const oftAdapter = src.oftAdapter;
1966
- const srcChain = makeChain(src.id, src.rpc);
2261
+ const plan = this.plan({ ...params, recipient: _nullishCoalesce(params.recipient, () => ( from)) }, "bridge");
2262
+ const { src, oftAdapter, amountLD, chain: srcChain } = plan;
2263
+ const refundAddress = this.resolveRefundAddress(params.refundAddress, from, "bridge");
1967
2264
  const wc = _nullishCoalesce(this.config.walletClient, () => ( _viem.createWalletClient.call(void 0, {
1968
2265
  account: this.config.account,
1969
2266
  chain: srcChain,
1970
- transport: _nullishCoalesce(this.config.transport, () => ( _viem.http.call(void 0, src.rpc)))
2267
+ // `config.transport` is the caller's *Stable-chain* transport. A browser
2268
+ // wallet transport is chain-agnostic — the wallet holds the keys and picks
2269
+ // the network — but an HTTP one is a broadcast channel bound to a single
2270
+ // chain, and broadcasting an Ethereum send to the Stable RPC fails. With a
2271
+ // local account we sign here and only need somewhere to broadcast, so the
2272
+ // source chain's own endpoint is always the right answer.
2273
+ transport: this.config.account ? _viem.http.call(void 0, plan.rpc) : _nullishCoalesce(this.config.transport, () => ( _viem.http.call(void 0, plan.rpc)))
1971
2274
  })));
1972
- const pc = _viem.createPublicClient.call(void 0, { chain: srcChain, transport: _viem.http.call(void 0, src.rpc) });
1973
2275
  try {
1974
2276
  await wc.switchChain({ id: src.id });
1975
2277
  } catch (err) {
@@ -1980,55 +2282,24 @@ var LayerZeroService = class {
1980
2282
  cause: err
1981
2283
  });
1982
2284
  }
1983
- const amountLD = _viem.parseUnits.call(void 0, String(amount), src.decimals);
1984
- const sendParams = this.buildSendParams(dst, amountLD, recipientAddr);
1985
- const { nativeFee, lzTokenFee } = _optionalChain([quote, 'optionalAccess', _43 => _43.nativeFee]) !== void 0 && _optionalChain([quote, 'optionalAccess', _44 => _44.lzTokenFee]) !== void 0 ? { nativeFee: quote.nativeFee, lzTokenFee: quote.lzTokenFee } : await this.quoteFee({ fromChain, toChain, fromToken, toToken, amount, recipient: recipientAddr }, "bridge");
1986
- const allowance = await pc.readContract({
1987
- address: src.usdt,
1988
- abi: _viem.erc20Abi,
1989
- functionName: "allowance",
1990
- args: [from, oftAdapter]
1991
- });
1992
- if (allowance < amountLD) {
1993
- let approveHash;
1994
- try {
1995
- approveHash = await wc.writeContract({
1996
- address: src.usdt,
1997
- abi: _viem.erc20Abi,
1998
- functionName: "approve",
1999
- args: [oftAdapter, amountLD],
2000
- account: wc.account,
2001
- chain: srcChain
2002
- });
2003
- } catch (err) {
2004
- throw new (0, _chunkQQ26UI3Fcjs.StableTransactionError)({
2005
- phase: "approve",
2006
- shortMessage: "bridge: USDT0 approve failed",
2007
- chainId: src.id,
2008
- cause: err
2009
- });
2010
- }
2011
- const approveReceipt = await pc.waitForTransactionReceipt({ hash: approveHash });
2012
- if (approveReceipt.status === "reverted") {
2013
- throw new (0, _chunkQQ26UI3Fcjs.StableTransactionError)({
2014
- phase: "approve",
2015
- shortMessage: "bridge: USDT0 approve reverted on-chain",
2016
- txHash: approveReceipt.transactionHash,
2017
- chainId: src.id
2018
- });
2019
- }
2285
+ const quote = usableQuote(params.quote, amountLD);
2286
+ const { nativeFee, lzTokenFee, oft } = _optionalChain([quote, 'optionalAccess', _46 => _46.nativeFee]) !== void 0 && _optionalChain([quote, 'optionalAccess', _47 => _47.lzTokenFee]) !== void 0 ? { nativeFee: quote.nativeFee, lzTokenFee: quote.lzTokenFee, oft: quote.oft } : await this.quotePathway(plan, "bridge", true).then((r) => ({ ...r.fee, oft: r.oft }));
2287
+ const sendParams = this.buildSendParams(plan, _nullishCoalesce(_optionalChain([oft, 'optionalAccess', _48 => _48.amountReceivedLD]), () => ( amountLD)));
2288
+ const sendFee = padFee(nativeFee, plan.feeBufferBps);
2289
+ for (const value of await this.approvalPlan(plan, from, amountLD)) {
2290
+ await this.sendApprove(plan, wc, value);
2020
2291
  }
2021
2292
  try {
2022
2293
  const hash = await wc.writeContract({
2023
2294
  address: oftAdapter,
2024
2295
  abi: OFT_ADAPTER_ABI,
2025
2296
  functionName: "send",
2026
- args: [sendParams, { nativeFee, lzTokenFee }, from],
2027
- value: nativeFee,
2297
+ args: [sendParams, { nativeFee: sendFee, lzTokenFee }, refundAddress],
2298
+ value: sendFee,
2028
2299
  account: wc.account,
2029
2300
  chain: srcChain
2030
2301
  });
2031
- return { txHash: hash, toAmount: amount };
2302
+ return { txHash: hash, toAmount: _nullishCoalesce(_optionalChain([oft, 'optionalAccess', _49 => _49.amountReceived]), () => ( params.amount)) };
2032
2303
  } catch (err) {
2033
2304
  throw new (0, _chunkQQ26UI3Fcjs.StableTransactionError)({
2034
2305
  phase: "send",
@@ -2038,6 +2309,77 @@ var LayerZeroService = class {
2038
2309
  });
2039
2310
  }
2040
2311
  }
2312
+ /**
2313
+ * Everything needed to send a bridge, without signing or sending any of it: the
2314
+ * ordered transactions, the quote they were built from, and the `SendParam` the
2315
+ * send step encodes, decoded.
2316
+ *
2317
+ * This is the split-responsibility shape — a backend quotes, builds and checks
2318
+ * while the holder's wallet only signs — and the only way to inspect a bridge
2319
+ * before it exists. Send `steps` in order from `from`, each one confirmed before
2320
+ * the next: an allowance reset and an approve where the lockbox pathway needs
2321
+ * them, then the OFT `send` carrying its fee as `value`.
2322
+ *
2323
+ * Refuses to build when the adapter will not answer `quoteOFT`: the receipt is
2324
+ * what floors the send at what it would actually deliver, and guessing the floor
2325
+ * is how a send reverts with `SlippageExceeded` after the gas is spent.
2326
+ */
2327
+ async prepareBridgeCalldata(params) {
2328
+ const method = "prepareBridgeCalldata";
2329
+ const from = params.from;
2330
+ if (!_viem.isAddress.call(void 0, from)) {
2331
+ throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
2332
+ "from",
2333
+ params.from,
2334
+ `${method}: invalid from address "${params.from}" \u2014 the address that will sign these transactions`
2335
+ );
2336
+ }
2337
+ const refundAddress = this.resolveRefundAddress(params.refundAddress, from, method);
2338
+ const plan = this.plan({ ...params, recipient: _nullishCoalesce(params.recipient, () => ( from)) }, method);
2339
+ const supplied = usableQuote(params.quote, plan.amountLD);
2340
+ const quote = _optionalChain([supplied, 'optionalAccess', _50 => _50.oft]) && supplied.nativeFee !== void 0 && supplied.lzTokenFee !== void 0 ? supplied : await this.buildQuote(plan, params, method, { account: from, requireOft: true });
2341
+ const oft = quote.oft;
2342
+ const nativeFee = padFee(quote.nativeFee, plan.feeBufferBps);
2343
+ const lzTokenFee = quote.lzTokenFee;
2344
+ const sendParam = this.buildSendParams(plan, oft.amountReceivedLD);
2345
+ const steps = (await this.approvalPlan(plan, from, plan.amountLD)).map((value) => ({
2346
+ chainId: plan.src.id,
2347
+ to: plan.srcToken.address,
2348
+ data: _viem.encodeFunctionData.call(void 0, {
2349
+ abi: _viem.erc20Abi,
2350
+ functionName: "approve",
2351
+ args: [plan.oftAdapter, value]
2352
+ }),
2353
+ value: 0n
2354
+ }));
2355
+ steps.push({
2356
+ chainId: plan.src.id,
2357
+ to: plan.oftAdapter,
2358
+ data: _viem.encodeFunctionData.call(void 0, {
2359
+ abi: OFT_ADAPTER_ABI,
2360
+ functionName: "send",
2361
+ args: [sendParam, { nativeFee, lzTokenFee }, refundAddress]
2362
+ }),
2363
+ value: nativeFee
2364
+ });
2365
+ return {
2366
+ steps,
2367
+ chainId: plan.src.id,
2368
+ asset: plan.asset,
2369
+ quote,
2370
+ send: {
2371
+ dstEid: sendParam.dstEid,
2372
+ recipient: plan.recipient,
2373
+ to: sendParam.to,
2374
+ amountLD: sendParam.amountLD,
2375
+ minAmountLD: sendParam.minAmountLD,
2376
+ extraOptions: sendParam.extraOptions,
2377
+ nativeFee,
2378
+ lzTokenFee,
2379
+ refundAddress
2380
+ }
2381
+ };
2382
+ }
2041
2383
  /**
2042
2384
  * Estimate how long a USDT0 bridge will take, end to end.
2043
2385
  *
@@ -2052,18 +2394,26 @@ var LayerZeroService = class {
2052
2394
  * computes to ~15s (60 blocks × 0.25s) while real deliveries take ~18min, because
2053
2395
  * the DVNs wait for the L2 batch to finalize on L1.
2054
2396
  */
2055
- async estimateBridgeTime({ fromChain, toChain }) {
2397
+ async estimateBridgeTime({
2398
+ fromChain,
2399
+ toChain,
2400
+ fromToken,
2401
+ toToken
2402
+ }) {
2056
2403
  const src = BRIDGE_CHAIN_CONFIGS[fromChain];
2057
2404
  const dst = BRIDGE_CHAIN_CONFIGS[toChain];
2058
- if (!_optionalChain([src, 'optionalAccess', _45 => _45.oftAdapter])) {
2405
+ const from = _nullishCoalesce(fromToken, () => ( _optionalChain([src, 'optionalAccess', _51 => _51.tokens, 'access', _52 => _52.usdt0, 'optionalAccess', _53 => _53.address])));
2406
+ const to = _nullishCoalesce(toToken, () => ( _optionalChain([dst, 'optionalAccess', _54 => _54.tokens, 'access', _55 => _55.usdt0, 'optionalAccess', _56 => _56.address])));
2407
+ if (!(src && from)) {
2059
2408
  throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)("fromChain", fromChain, `estimateBridgeTime: no OFT adapter for "${fromChain}"`);
2060
2409
  }
2061
- if (!_optionalChain([dst, 'optionalAccess', _46 => _46.oftAdapter])) {
2410
+ if (!(dst && to)) {
2062
2411
  throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)("toChain", toChain, `estimateBridgeTime: no OFT adapter for "${toChain}"`);
2063
2412
  }
2413
+ const { srcToken, dstToken } = this.resolveRoute(fromChain, toChain, from, to);
2064
2414
  const [uln, observedSeconds] = await Promise.all([
2065
- this.getUlnConfig(src, dst).catch(() => void 0),
2066
- this.observedBridgeSeconds(src, dst).catch(() => void 0)
2415
+ this.getUlnConfig(src, dst, srcToken.oftAdapter).catch(() => void 0),
2416
+ this.observedBridgeSeconds(src, dst, srcToken.oftAdapter, dstToken.oftAdapter).catch(() => void 0)
2067
2417
  ]);
2068
2418
  const dvnCount = uln ? uln.requiredDVNCount + uln.optionalDVNCount : void 0;
2069
2419
  let formulaSeconds;
@@ -2085,7 +2435,7 @@ var LayerZeroService = class {
2085
2435
  source: observedSeconds !== void 0 ? "observed" : "formula",
2086
2436
  observedSeconds,
2087
2437
  formulaSeconds,
2088
- confirmations: Number.isFinite(_optionalChain([uln, 'optionalAccess', _47 => _47.confirmations])) ? _optionalChain([uln, 'optionalAccess', _48 => _48.confirmations]) : void 0,
2438
+ confirmations: Number.isFinite(_optionalChain([uln, 'optionalAccess', _57 => _57.confirmations])) ? _optionalChain([uln, 'optionalAccess', _58 => _58.confirmations]) : void 0,
2089
2439
  dvnCount: Number.isFinite(dvnCount) ? dvnCount : void 0
2090
2440
  };
2091
2441
  }
@@ -2098,7 +2448,7 @@ var LayerZeroService = class {
2098
2448
  const scanUrl = `${LZ_SCAN_UI}/tx/${srcTxHash}`;
2099
2449
  let message;
2100
2450
  try {
2101
- message = await _asyncOptionalChain([(await scanFetch(`/messages/tx/${srcTxHash}`)), 'optionalAccess', async _49 => _49.data, 'optionalAccess', async _50 => _50[0]]);
2451
+ message = await _asyncOptionalChain([(await scanFetch(`/messages/tx/${srcTxHash}`)), 'optionalAccess', async _59 => _59.data, 'optionalAccess', async _60 => _60[0]]);
2102
2452
  } catch (err) {
2103
2453
  if (err instanceof ScanHttpError && err.status === 404) {
2104
2454
  return { state: "pending", srcTxHash, scanUrl };
@@ -2110,24 +2460,24 @@ var LayerZeroService = class {
2110
2460
  });
2111
2461
  }
2112
2462
  if (!message) return { state: "pending", srcTxHash, scanUrl };
2113
- const raw = _nullishCoalesce(_optionalChain([message, 'access', _51 => _51.status, 'optionalAccess', _52 => _52.name]), () => ( ""));
2114
- const sentAt = _optionalChain([message, 'access', _53 => _53.source, 'optionalAccess', _54 => _54.tx, 'optionalAccess', _55 => _55.blockTimestamp]);
2463
+ const raw = _nullishCoalesce(_optionalChain([message, 'access', _61 => _61.status, 'optionalAccess', _62 => _62.name]), () => ( ""));
2464
+ const sentAt = _optionalChain([message, 'access', _63 => _63.source, 'optionalAccess', _64 => _64.tx, 'optionalAccess', _65 => _65.blockTimestamp]);
2115
2465
  return {
2116
2466
  // Everything terminal that is not DELIVERED means the tokens will not land
2117
2467
  // on their own; `raw` carries which of those it was.
2118
2468
  state: raw === "DELIVERED" ? "delivered" : TERMINAL_FAILURE_STATUSES.has(raw) ? "failed" : "inflight",
2119
2469
  raw,
2120
2470
  srcTxHash,
2121
- dstTxHash: _optionalChain([message, 'access', _56 => _56.destination, 'optionalAccess', _57 => _57.tx, 'optionalAccess', _58 => _58.txHash]),
2471
+ dstTxHash: _optionalChain([message, 'access', _66 => _66.destination, 'optionalAccess', _67 => _67.tx, 'optionalAccess', _68 => _68.txHash]),
2122
2472
  elapsedSeconds: sentAt ? Math.max(0, Math.floor(Date.now() / 1e3) - sentAt) : void 0,
2123
2473
  scanUrl
2124
2474
  };
2125
2475
  }
2126
2476
  /** Effective ULN config the DVNs enforce for this pathway, read from the source chain. */
2127
- async getUlnConfig(src, dst) {
2128
- return cached(`uln:${src.lzEid}-${dst.lzEid}`, CACHE_TTL_MS, async () => {
2129
- const pc = _viem.createPublicClient.call(void 0, { chain: makeChain(src.id, src.rpc), transport: _viem.http.call(void 0, src.rpc) });
2130
- const oapp = src.oftAdapter;
2477
+ async getUlnConfig(src, dst, oapp) {
2478
+ return cached(`uln:${src.lzEid}-${dst.lzEid}-${oapp.toLowerCase()}`, CACHE_TTL_MS, async () => {
2479
+ const rpc = this.rpcFor(src);
2480
+ const pc = _viem.createPublicClient.call(void 0, { chain: makeChain(src.id, rpc), transport: _viem.http.call(void 0, rpc) });
2131
2481
  const endpoint = await pc.readContract({ address: oapp, abi: OAPP_ABI, functionName: "endpoint" });
2132
2482
  const sendLib = await pc.readContract({
2133
2483
  address: endpoint,
@@ -2149,11 +2499,11 @@ var LayerZeroService = class {
2149
2499
  });
2150
2500
  }
2151
2501
  /** Median delivery time of recently delivered messages on this pathway, in seconds. */
2152
- async observedBridgeSeconds(src, dst) {
2153
- const pathwayId = `${src.lzEid}-${dst.lzEid}-${src.oftAdapter.toLowerCase()}-${dst.oftAdapter.toLowerCase()}`;
2502
+ async observedBridgeSeconds(src, dst, srcOapp, dstOapp) {
2503
+ const pathwayId = `${src.lzEid}-${dst.lzEid}-${srcOapp.toLowerCase()}-${dstOapp.toLowerCase()}`;
2154
2504
  return cached(`observed:${pathwayId}`, CACHE_TTL_MS, async () => {
2155
2505
  const { data } = await scanFetch(`/messages/pathway/${pathwayId}?limit=50`);
2156
- const durations = (_nullishCoalesce(data, () => ( []))).filter((m) => _optionalChain([m, 'access', _59 => _59.status, 'optionalAccess', _60 => _60.name]) === "DELIVERED").map((m) => (_nullishCoalesce(_optionalChain([m, 'access', _61 => _61.destination, 'optionalAccess', _62 => _62.tx, 'optionalAccess', _63 => _63.blockTimestamp]), () => ( 0))) - (_nullishCoalesce(_optionalChain([m, 'access', _64 => _64.source, 'optionalAccess', _65 => _65.tx, 'optionalAccess', _66 => _66.blockTimestamp]), () => ( 0)))).filter((d) => d > 0);
2506
+ const durations = (_nullishCoalesce(data, () => ( []))).filter((m) => _optionalChain([m, 'access', _69 => _69.status, 'optionalAccess', _70 => _70.name]) === "DELIVERED").map((m) => (_nullishCoalesce(_optionalChain([m, 'access', _71 => _71.destination, 'optionalAccess', _72 => _72.tx, 'optionalAccess', _73 => _73.blockTimestamp]), () => ( 0))) - (_nullishCoalesce(_optionalChain([m, 'access', _74 => _74.source, 'optionalAccess', _75 => _75.tx, 'optionalAccess', _76 => _76.blockTimestamp]), () => ( 0)))).filter((d) => d > 0);
2157
2507
  return durations.length >= 3 ? Math.round(median(durations)) : void 0;
2158
2508
  });
2159
2509
  }
@@ -2180,37 +2530,122 @@ var LayerZeroService = class {
2180
2530
  );
2181
2531
  }
2182
2532
  }
2183
- resolveChains(fromChain, toChain, fromToken, toToken) {
2533
+ /**
2534
+ * Resolve a bridge request to one OFT mesh and its two endpoints.
2535
+ *
2536
+ * Both sides must name the same asset. An OFT escrows one specific ERC-20 and
2537
+ * mints its own peer on the far side — it cannot hand back a different token,
2538
+ * so a USDT0→XAUt0 request is a trade, not a bridge, and belongs to LI.FI.
2539
+ */
2540
+ resolveRoute(fromChain, toChain, fromToken, toToken) {
2184
2541
  const src = BRIDGE_CHAIN_CONFIGS[fromChain];
2185
2542
  const dst = BRIDGE_CHAIN_CONFIGS[toChain];
2186
2543
  if (!src) throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)("fromChain", fromChain, `Unsupported source chain: "${fromChain}"`);
2187
2544
  if (!dst) throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)("toChain", toChain, `Unsupported destination chain: "${toChain}"`);
2188
- if (!src.oftAdapter) {
2189
- throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)("fromChain", fromChain, `No OFT adapter configured for "${fromChain}"`);
2190
- }
2191
- if (fromToken.toLowerCase() !== src.usdt.toLowerCase()) {
2545
+ const asset = bridgeAssetFor(src, fromToken);
2546
+ if (!asset) {
2192
2547
  throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
2193
2548
  "fromToken",
2194
2549
  fromToken,
2195
- `USDT0 bridge only moves USDT0 \u2014 fromToken must be ${src.usdt} on "${fromChain}", got "${fromToken}". Use route "lifi" to bridge other tokens.`
2550
+ `Native OFT bridge does not recognize "${fromToken}" on "${fromChain}" \u2014 it moves ${describeAssets(src)}. Use route "lifi" to bridge other tokens.`
2551
+ );
2552
+ }
2553
+ const srcToken = src.tokens[asset];
2554
+ const dstToken = dst.tokens[asset];
2555
+ if (!dstToken) {
2556
+ throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
2557
+ "toChain",
2558
+ toChain,
2559
+ `Native OFT bridge cannot deliver ${asset} to "${toChain}" \u2014 no ${asset} deployment there. Use route "lifi" instead.`
2196
2560
  );
2197
2561
  }
2198
- if (toToken.toLowerCase() !== dst.usdt.toLowerCase()) {
2562
+ if (dstToken.address.toLowerCase() !== toToken.toLowerCase()) {
2199
2563
  throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
2200
2564
  "toToken",
2201
2565
  toToken,
2202
- `USDT0 bridge only moves USDT0 \u2014 toToken must be ${dst.usdt} on "${toChain}", got "${toToken}". Use route "lifi" to bridge other tokens.`
2566
+ `Native OFT bridge moves ${asset} to itself \u2014 toToken must be ${dstToken.address} on "${toChain}", got "${toToken}". Use route "lifi" to bridge between different tokens.`
2203
2567
  );
2204
2568
  }
2205
- return { src, dst };
2569
+ return { src, dst, asset, srcToken, dstToken };
2206
2570
  }
2207
- buildSendParams(dst, amountLD, recipient) {
2571
+ /**
2572
+ * Validate a request and resolve it to a single OFT pathway. No network calls —
2573
+ * everything here is config, arithmetic and encoding.
2574
+ */
2575
+ plan(params, method) {
2576
+ const { fromChain, toChain, fromToken, toToken, amount, recipient, dstNativeDrop } = params;
2577
+ const recipientAddr = _nullishCoalesce(recipient, () => ( ZERO_ADDRESS));
2578
+ this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, method);
2579
+ if (dstNativeDrop !== void 0 && dstNativeDrop < 0n) {
2580
+ throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
2581
+ "dstNativeDrop",
2582
+ dstNativeDrop,
2583
+ `${method}: dstNativeDrop must not be negative`
2584
+ );
2585
+ }
2586
+ const feeBufferBps = resolveFeeBuffer(params.feeBufferBps, method);
2587
+ const { src, dst, asset, srcToken, dstToken } = this.resolveRoute(
2588
+ fromChain,
2589
+ toChain,
2590
+ fromToken,
2591
+ toToken
2592
+ );
2593
+ if (params.fromDecimals !== void 0 && params.fromDecimals !== srcToken.decimals) {
2594
+ throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
2595
+ "fromDecimals",
2596
+ params.fromDecimals,
2597
+ `${method}: ${asset} has ${srcToken.decimals} decimals on "${fromChain}", not ${params.fromDecimals}. The native OFT route parses the amount with the token's own decimals; fromDecimals applies to LI.FI routes only.`
2598
+ );
2599
+ }
2600
+ const rpc = this.rpcFor(src);
2601
+ const chain = makeChain(src.id, rpc);
2208
2602
  return {
2209
- dstEid: dst.lzEid,
2210
- to: _viem.padHex.call(void 0, recipient, { dir: "left", size: 32 }),
2211
- amountLD,
2212
- minAmountLD: amountLD,
2213
- extraOptions: buildLzOptions(),
2603
+ src,
2604
+ dst,
2605
+ asset,
2606
+ srcToken,
2607
+ dstToken,
2608
+ oftAdapter: srcToken.oftAdapter,
2609
+ amountLD: _viem.parseUnits.call(void 0, String(amount), srcToken.decimals),
2610
+ recipient: recipientAddr,
2611
+ nativeDrop: _nullishCoalesce(dstNativeDrop, () => ( 0n)),
2612
+ rpc,
2613
+ feeBufferBps,
2614
+ chain,
2615
+ pc: _viem.createPublicClient.call(void 0, { chain, transport: _viem.http.call(void 0, rpc) })
2616
+ };
2617
+ }
2618
+ /**
2619
+ * Where `send` returns messaging fee it did not need. Defaults to the sender,
2620
+ * which is what a caller wants unless they are routing refunds somewhere of
2621
+ * their own — a backend paying the fee on a user's behalf, say.
2622
+ */
2623
+ resolveRefundAddress(refundAddress, sender, method) {
2624
+ if (refundAddress === void 0) return sender;
2625
+ if (!_viem.isAddress.call(void 0, refundAddress)) {
2626
+ throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
2627
+ "refundAddress",
2628
+ refundAddress,
2629
+ `${method}: invalid refundAddress "${refundAddress}"`
2630
+ );
2631
+ }
2632
+ return refundAddress;
2633
+ }
2634
+ /** The address that will sign, or undefined when this service has no signer. */
2635
+ signerAddress() {
2636
+ return _nullishCoalesce(_optionalChain([this, 'access', _77 => _77.config, 'access', _78 => _78.walletClient, 'optionalAccess', _79 => _79.account, 'optionalAccess', _80 => _80.address]), () => ( _optionalChain([this, 'access', _81 => _81.config, 'access', _82 => _82.account, 'optionalAccess', _83 => _83.address])));
2637
+ }
2638
+ /**
2639
+ * `SendParam` for one call. `minAmountLD` is the caller's slippage floor: 0 when
2640
+ * asking what a send would yield, and the quoted output when actually sending.
2641
+ */
2642
+ buildSendParams(plan, minAmountLD) {
2643
+ return {
2644
+ dstEid: plan.dst.lzEid,
2645
+ to: _viem.padHex.call(void 0, plan.recipient, { dir: "left", size: 32 }),
2646
+ amountLD: plan.amountLD,
2647
+ minAmountLD,
2648
+ extraOptions: buildLzOptions(0n, plan.nativeDrop, plan.recipient),
2214
2649
  composeMsg: "0x",
2215
2650
  oftCmd: "0x"
2216
2651
  };
@@ -2218,21 +2653,37 @@ var LayerZeroService = class {
2218
2653
  };
2219
2654
 
2220
2655
  // src/stable.ts
2221
- function isUsdt0Route(fromChain, toChain, fromToken, toToken) {
2656
+ function isNativeOftRoute(fromChain, toChain, fromToken, toToken) {
2222
2657
  const src = BRIDGE_CHAIN_CONFIGS[fromChain];
2223
2658
  const dst = BRIDGE_CHAIN_CONFIGS[toChain];
2224
- return !!src && !!dst && !!src.oftAdapter && src.lzOutbound !== false && src.usdt.toLowerCase() === fromToken.toLowerCase() && dst.usdt.toLowerCase() === toToken.toLowerCase();
2659
+ if (!(src && dst)) return false;
2660
+ const asset = bridgeAssetFor(src, fromToken);
2661
+ if (!asset) return false;
2662
+ const srcToken = src.tokens[asset];
2663
+ const dstToken = dst.tokens[asset];
2664
+ return src.lzOutbound !== false && !!srcToken && srcToken.lzOutbound !== false && !!dstToken && dstToken.address.toLowerCase() === toToken.toLowerCase();
2225
2665
  }
2226
2666
  function selectBridge(params) {
2667
+ const selected = resolveBridge(params);
2668
+ if (selected === "lifi" && (_nullishCoalesce(params.dstNativeDrop, () => ( 0n))) > 0n) {
2669
+ throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
2670
+ "dstNativeDrop",
2671
+ params.dstNativeDrop,
2672
+ `bridge: dstNativeDrop is only supported on the native OFT route, and ${params.fromChain}\u2192${params.toChain} resolved to LI.FI`
2673
+ );
2674
+ }
2675
+ return selected;
2676
+ }
2677
+ function resolveBridge(params) {
2227
2678
  const route = _nullishCoalesce(params.route, () => ( "auto"));
2228
- if (route === "usdt0") return "usdt0";
2679
+ if (route === "oft" || route === "usdt0") return "oft";
2229
2680
  if (route === "lifi") return "lifi";
2230
- return isUsdt0Route(
2681
+ return isNativeOftRoute(
2231
2682
  params.fromChain,
2232
2683
  params.toChain,
2233
2684
  params.fromToken,
2234
2685
  params.toToken
2235
- ) ? "usdt0" : "lifi";
2686
+ ) ? "oft" : "lifi";
2236
2687
  }
2237
2688
  function validateTransferParams({
2238
2689
  to,
@@ -2277,7 +2728,7 @@ async function resolveTokenDecimals(publicClient, token, tokenDecimals) {
2277
2728
  abi: _viem.erc20Abi,
2278
2729
  functionName: "decimals"
2279
2730
  });
2280
- } catch (e5) {
2731
+ } catch (e8) {
2281
2732
  throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
2282
2733
  "tokenDecimals",
2283
2734
  void 0,
@@ -2311,6 +2762,7 @@ async function executeTokenTransfer(wc, publicClient, token, to, amount, tokenDe
2311
2762
  function createStable({
2312
2763
  network = "mainnet" /* Mainnet */,
2313
2764
  rpc: rpcOverride,
2765
+ rpcOverrides,
2314
2766
  account,
2315
2767
  transport,
2316
2768
  walletClient,
@@ -2324,7 +2776,7 @@ function createStable({
2324
2776
  );
2325
2777
  const signerConfig = { account, transport, walletClient };
2326
2778
  const lifi = new LiFiService({ ...signerConfig, rpc, chainId: chain.id });
2327
- const lz = new LayerZeroService(signerConfig);
2779
+ const lz = new LayerZeroService({ ...signerConfig, rpcOverrides });
2328
2780
  const merkl = new MerklService({ ...signerConfig, rpc, chainId: chain.id, merklApiBase });
2329
2781
  const morpho = earnConfig ? new MorphoService({
2330
2782
  ...signerConfig,
@@ -2349,7 +2801,7 @@ function createStable({
2349
2801
  return morpho;
2350
2802
  }
2351
2803
  function requireSignerAddress() {
2352
- const from = _nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _67 => _67.account, 'optionalAccess', _68 => _68.address]), () => ( _optionalChain([account, 'optionalAccess', _69 => _69.address])));
2804
+ const from = _nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _84 => _84.account, 'optionalAccess', _85 => _85.address]), () => ( _optionalChain([account, 'optionalAccess', _86 => _86.address])));
2353
2805
  if (!from) {
2354
2806
  throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
2355
2807
  "signer",
@@ -2362,7 +2814,7 @@ function createStable({
2362
2814
  async function transfer(params) {
2363
2815
  validateTransferParams(params);
2364
2816
  const { from, to, amount, token, tokenDecimals } = params;
2365
- const acc = _nullishCoalesce(_nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _70 => _70.account]), () => ( account)), () => ( from));
2817
+ const acc = _nullishCoalesce(_nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _87 => _87.account]), () => ( account)), () => ( from));
2366
2818
  const wc = _nullishCoalesce(walletClient, () => ( _viem.createWalletClient.call(void 0, {
2367
2819
  account: acc,
2368
2820
  chain,
@@ -2414,8 +2866,20 @@ function createStable({
2414
2866
  }
2415
2867
  return {
2416
2868
  transfer,
2417
- quoteBridge: (params) => selectBridge(params) === "usdt0" ? lz.quoteBridge(params) : lifi.quoteBridge(params),
2418
- bridge: (params) => selectBridge(params) === "usdt0" ? lz.bridge(params) : lifi.bridge(params),
2869
+ // `async` so a rejected route (see selectBridge) surfaces as a rejected
2870
+ // promise like every other validation error, not a synchronous throw.
2871
+ quoteBridge: async (params) => selectBridge(params) === "oft" ? lz.quoteBridge(params) : lifi.quoteBridge(params),
2872
+ bridge: async (params) => selectBridge(params) === "oft" ? lz.bridge(params) : lifi.bridge(params),
2873
+ prepareBridgeCalldata: async (params) => {
2874
+ if (selectBridge(params) !== "oft") {
2875
+ throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
2876
+ "route",
2877
+ _nullishCoalesce(params.route, () => ( "auto")),
2878
+ `prepareBridgeCalldata: only the native OFT route can be built unsigned, and ${params.fromChain}\u2192${params.toChain} resolved to LI.FI`
2879
+ );
2880
+ }
2881
+ return lz.prepareBridgeCalldata(params);
2882
+ },
2419
2883
  estimateBridgeTime: (params) => lz.estimateBridgeTime(params),
2420
2884
  trackBridge: (srcTxHash) => lz.trackBridge(srcTxHash),
2421
2885
  quoteSwap: (params) => lifi.quoteSwap(params),
@@ -2483,4 +2947,7 @@ function createStableReader(config) {
2483
2947
 
2484
2948
 
2485
2949
 
2486
- exports.BRIDGE_CHAIN_CONFIGS = BRIDGE_CHAIN_CONFIGS; exports.CHAIN_CONFIGS = CHAIN_CONFIGS; exports.Chain = Chain; exports.ERC3009_ABI = _chunkQQ26UI3Fcjs.ERC3009_ABI; exports.Network = Network; exports.STABLE_USDT_ADDRESS = _chunkQQ26UI3Fcjs.STABLE_USDT_ADDRESS; exports.STABLE_VAULT_ADDRESS = _chunkQQ26UI3Fcjs.STABLE_VAULT_ADDRESS; exports.StableBulkError = _chunkQQ26UI3Fcjs.StableBulkError; exports.StableError = _chunkQQ26UI3Fcjs.StableError; exports.StableNetworkError = _chunkQQ26UI3Fcjs.StableNetworkError; exports.StableQuoteError = _chunkQQ26UI3Fcjs.StableQuoteError; exports.StableTransactionError = _chunkQQ26UI3Fcjs.StableTransactionError; exports.StableValidationError = _chunkQQ26UI3Fcjs.StableValidationError; exports.cancelAuthorization = _chunkQQ26UI3Fcjs.cancelAuthorization; exports.clearBridgeEstimateCache = clearBridgeEstimateCache; exports.createStable = createStable; exports.createStableReader = createStableReader; exports.erc3009Actions = _chunkQQ26UI3Fcjs.erc3009Actions; exports.getAuthorizationState = _chunkQQ26UI3Fcjs.getAuthorizationState; exports.getAuthorizationUsage = _chunkQQ26UI3Fcjs.getAuthorizationUsage; exports.prepareTransferWithAuthorization = _chunkQQ26UI3Fcjs.prepareTransferWithAuthorization; exports.receiveWithAuthorization = _chunkQQ26UI3Fcjs.receiveWithAuthorization; exports.signReceiveAuthorization = _chunkQQ26UI3Fcjs.signReceiveAuthorization; exports.signTransferAuthorization = _chunkQQ26UI3Fcjs.signTransferAuthorization; exports.transferWithAuthorization = _chunkQQ26UI3Fcjs.transferWithAuthorization; exports.verifyAuthorization = _chunkQQ26UI3Fcjs.verifyAuthorization;
2950
+
2951
+
2952
+
2953
+ exports.BRIDGE_CHAIN_CONFIGS = BRIDGE_CHAIN_CONFIGS; exports.CHAIN_CONFIGS = CHAIN_CONFIGS; exports.Chain = Chain; exports.ERC3009_ABI = _chunkQQ26UI3Fcjs.ERC3009_ABI; exports.Network = Network; exports.OFT_ADAPTER_ABI = OFT_ADAPTER_ABI; exports.STABLE_USDT_ADDRESS = _chunkQQ26UI3Fcjs.STABLE_USDT_ADDRESS; exports.STABLE_VAULT_ADDRESS = _chunkQQ26UI3Fcjs.STABLE_VAULT_ADDRESS; exports.StableBulkError = _chunkQQ26UI3Fcjs.StableBulkError; exports.StableError = _chunkQQ26UI3Fcjs.StableError; exports.StableNetworkError = _chunkQQ26UI3Fcjs.StableNetworkError; exports.StableQuoteError = _chunkQQ26UI3Fcjs.StableQuoteError; exports.StableTransactionError = _chunkQQ26UI3Fcjs.StableTransactionError; exports.StableValidationError = _chunkQQ26UI3Fcjs.StableValidationError; exports.bridgeAssetFor = bridgeAssetFor; exports.buildLzOptions = buildLzOptions; exports.cancelAuthorization = _chunkQQ26UI3Fcjs.cancelAuthorization; exports.clearBridgeEstimateCache = clearBridgeEstimateCache; exports.createStable = createStable; exports.createStableReader = createStableReader; exports.erc3009Actions = _chunkQQ26UI3Fcjs.erc3009Actions; exports.getAuthorizationState = _chunkQQ26UI3Fcjs.getAuthorizationState; exports.getAuthorizationUsage = _chunkQQ26UI3Fcjs.getAuthorizationUsage; exports.prepareTransferWithAuthorization = _chunkQQ26UI3Fcjs.prepareTransferWithAuthorization; exports.receiveWithAuthorization = _chunkQQ26UI3Fcjs.receiveWithAuthorization; exports.signReceiveAuthorization = _chunkQQ26UI3Fcjs.signReceiveAuthorization; exports.signTransferAuthorization = _chunkQQ26UI3Fcjs.signTransferAuthorization; exports.transferWithAuthorization = _chunkQQ26UI3Fcjs.transferWithAuthorization; exports.verifyAuthorization = _chunkQQ26UI3Fcjs.verifyAuthorization;