@stablechain/sdk 1.0.5 → 1.0.6

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
@@ -1735,48 +1735,83 @@ import {
1735
1735
  isAddress as isAddress4,
1736
1736
  numberToHex,
1737
1737
  padHex,
1738
+ formatUnits as formatUnits5,
1738
1739
  parseUnits as parseUnits4
1739
1740
  } from "viem";
1740
1741
  var C = CHAIN_CONFIGS;
1742
+ var XAUT0_DEPLOYMENTS = {
1743
+ ["ethereum" /* Ethereum */]: { address: "0x68749665FF8D2d112Fa859AA293F07A622782F38", decimals: 6, oftAdapter: "0xb9c2321BB7D0Db468f570D10A424d1Cc8EFd696C", approvalRequired: true },
1744
+ ["stable" /* Stable */]: { address: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb", decimals: 6, oftAdapter: "0xD8479f87686ed263D00Ca7505F86327dbeD4171A", approvalRequired: false },
1745
+ ["arbitrum" /* Arbitrum */]: { address: "0x40461291347e1eCbb09499F3371D3f17f10d7159", decimals: 6, oftAdapter: "0xf40542a7B66AD7C68C459EE3679635D2fDB6dF39", approvalRequired: false },
1746
+ ["ink" /* Ink */]: { address: "0xF50258D3c1dd88946C567920B986A12e65b50dAc", decimals: 6, oftAdapter: "0xA1bE1572B4beef24f812EfDc58bdc41D56a0dAB2", approvalRequired: false },
1747
+ ["hyperevm" /* HyperEVM */]: { address: "0xf4D9235269a96aaDaFc9aDAe454a0618eBE37949", decimals: 6, oftAdapter: "0x4E41cfc3F3B19E29E323D2c36F8f202a1e151dAF", approvalRequired: false },
1748
+ ["polygon" /* Polygon */]: { address: "0xF1815bd50389c46847f0Bda824eC8da914045D14", decimals: 6, oftAdapter: "0x5421Cf4288d8007D3c43AC4246eaFCe5b049e352", approvalRequired: false },
1749
+ ["plasma" /* Plasma */]: { address: "0x1B64B9025EEbb9A6239575dF9Ea4b9Ac46D4d193", decimals: 6, oftAdapter: "0x63aB93cBC9d4ecD9c4947b1A38F458147C08E6F7", approvalRequired: false },
1750
+ ["bsc" /* BSC */]: { address: "0x21cAef8A43163Eea865baeE23b9C2E327696A3bf", decimals: 6, oftAdapter: "0x53C3A64c8942288e12813C1F8457Db45980BcfC2", approvalRequired: false }
1751
+ };
1752
+ function bridgeChain(chain, lzEid, blockTimeSec, usdt0Adapter) {
1753
+ const base = C[chain];
1754
+ const tokens = {};
1755
+ if (usdt0Adapter) {
1756
+ tokens.usdt0 = { address: base.usdt, decimals: base.decimals, oftAdapter: usdt0Adapter };
1757
+ }
1758
+ const xaut0 = XAUT0_DEPLOYMENTS[chain];
1759
+ if (xaut0) tokens.xaut0 = xaut0;
1760
+ return { ...base, lzEid, blockTimeSec, tokens, oftAdapter: usdt0Adapter };
1761
+ }
1741
1762
  var BRIDGE_CHAIN_CONFIGS = {
1742
1763
  // ─── 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 },
1764
+ ["sepolia" /* Sepolia */]: bridgeChain("sepolia" /* Sepolia */, 40161, 12, "0xc099cD946d5efCC35A99D64E808c1430cEf08126"),
1765
+ ["stable-testnet" /* StableTestnet */]: bridgeChain("stable-testnet" /* StableTestnet */, 40374, 0.7, "0x779Ded0c9e1022225f8E0630b35a9b54bE713736"),
1745
1766
  // ─── 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 }
1767
+ ["stable" /* Stable */]: bridgeChain("stable" /* Stable */, 30396, 0.7, "0xedaba024be4d87974d5aB11C6Dd586963CcCB027"),
1768
+ ["ethereum" /* Ethereum */]: bridgeChain("ethereum" /* Ethereum */, 30101, 12, "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee"),
1769
+ ["arbitrum" /* Arbitrum */]: bridgeChain("arbitrum" /* Arbitrum */, 30110, 0.25, "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92"),
1770
+ ["ink" /* Ink */]: bridgeChain("ink" /* Ink */, 30339, 1, "0x1cB6De532588fCA4a21B7209DE7C456AF8434A65"),
1771
+ ["bera" /* Bera */]: bridgeChain("bera" /* Bera */, 30362, 2, "0x3Dc96399109df5ceb2C226664A086140bD0379cB"),
1772
+ ["megaeth" /* MegaETH */]: bridgeChain("megaeth" /* MegaETH */, 30398, 1, "0x9151434b16b9763660705744891fa906f660ecc5"),
1773
+ ["hyperevm" /* HyperEVM */]: bridgeChain("hyperevm" /* HyperEVM */, 30367, 1, "0x904861a24F30EC96ea7CFC3bE9EA4B476d237e98"),
1774
+ ["polygon" /* Polygon */]: bridgeChain("polygon" /* Polygon */, 30109, 1.5, "0x6BA10300f0DC58B7a1e4c0e41f5daBb7D7829e13"),
1775
+ ["plasma" /* Plasma */]: bridgeChain("plasma" /* Plasma */, 30383, 1, "0x02ca37966753bDdDf11216B73B16C1dE756A7CF9"),
1776
+ ["optimism" /* Optimism */]: bridgeChain("optimism" /* Optimism */, 30111, 2, "0xF03b4d9AC1D5d1E7c4cEf54C2A313b9fe051A0aD"),
1777
+ ["unichain" /* Unichain */]: bridgeChain("unichain" /* Unichain */, 30320, 1, "0xc07bE8994D035631c36fb4a89C918CeFB2f03EC3"),
1778
+ ["sei" /* Sei */]: bridgeChain("sei" /* Sei */, 30280, 0.5, "0x56Fe74A2e3b484b921c447357203431a3485CC60"),
1779
+ ["flare" /* Flare */]: bridgeChain("flare" /* Flare */, 30295, 1.1, "0x567287d2A9829215a37e3B88843d32f9221E7588"),
1780
+ ["xlayer" /* XLayer */]: bridgeChain("xlayer" /* XLayer */, 30274, 1, "0x94BCCa6bdfd6A61817Ab0E960bFedE4984505554"),
1781
+ ["rootstock" /* Rootstock */]: bridgeChain("rootstock" /* Rootstock */, 30333, 31, "0x1a594d5d5d1c426281C1064B07f23F57B2716B61"),
1782
+ // XAUt0 only — BSC has no USDT0 deployment. Block time measured 2026-08-20.
1783
+ ["bsc" /* BSC */]: bridgeChain("bsc" /* BSC */, 30102, 0.45)
1784
+ };
1785
+ function bridgeAssetFor(cfg, token) {
1786
+ const needle = token.toLowerCase();
1787
+ return Object.keys(cfg.tokens).find(
1788
+ (asset) => cfg.tokens[asset]?.address.toLowerCase() === needle
1789
+ );
1790
+ }
1791
+ function describeAssets(cfg) {
1792
+ const assets = Object.entries(cfg.tokens);
1793
+ if (assets.length === 0) return "no OFT assets";
1794
+ return assets.map(([asset, token]) => `${asset} (${token.address})`).join(" or ");
1795
+ }
1796
+ var SEND_PARAM_INPUT = {
1797
+ name: "_sendParam",
1798
+ type: "tuple",
1799
+ components: [
1800
+ { name: "dstEid", type: "uint32" },
1801
+ { name: "to", type: "bytes32" },
1802
+ { name: "amountLD", type: "uint256" },
1803
+ { name: "minAmountLD", type: "uint256" },
1804
+ { name: "extraOptions", type: "bytes" },
1805
+ { name: "composeMsg", type: "bytes" },
1806
+ { name: "oftCmd", type: "bytes" }
1807
+ ]
1761
1808
  };
1762
1809
  var OFT_ADAPTER_ABI = [
1763
1810
  {
1764
1811
  name: "quoteSend",
1765
1812
  type: "function",
1766
1813
  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
- },
1814
+ SEND_PARAM_INPUT,
1780
1815
  { name: "_payInLzToken", type: "bool" }
1781
1816
  ],
1782
1817
  outputs: [
@@ -1792,22 +1827,48 @@ var OFT_ADAPTER_ABI = [
1792
1827
  stateMutability: "view"
1793
1828
  },
1794
1829
  {
1795
- name: "send",
1830
+ // OFT-level accounting, independent of the LayerZero messaging fee: the
1831
+ // pathway's accepted range, any fee the adapter charges itself, and what the
1832
+ // recipient actually ends up credited.
1833
+ name: "quoteOFT",
1796
1834
  type: "function",
1797
1835
  inputs: [
1836
+ SEND_PARAM_INPUT
1837
+ ],
1838
+ outputs: [
1798
1839
  {
1799
- name: "_sendParam",
1840
+ name: "oftLimit",
1800
1841
  type: "tuple",
1801
1842
  components: [
1802
- { name: "dstEid", type: "uint32" },
1803
- { name: "to", type: "bytes32" },
1804
- { name: "amountLD", type: "uint256" },
1805
1843
  { name: "minAmountLD", type: "uint256" },
1806
- { name: "extraOptions", type: "bytes" },
1807
- { name: "composeMsg", type: "bytes" },
1808
- { name: "oftCmd", type: "bytes" }
1844
+ { name: "maxAmountLD", type: "uint256" }
1845
+ ]
1846
+ },
1847
+ {
1848
+ name: "oftFeeDetails",
1849
+ type: "tuple[]",
1850
+ components: [
1851
+ // Signed: a negative detail is a credit to the sender, not a charge.
1852
+ { name: "feeAmountLD", type: "int256" },
1853
+ { name: "description", type: "string" }
1809
1854
  ]
1810
1855
  },
1856
+ {
1857
+ name: "oftReceipt",
1858
+ type: "tuple",
1859
+ components: [
1860
+ { name: "amountSentLD", type: "uint256" },
1861
+ { name: "amountReceivedLD", type: "uint256" }
1862
+ ]
1863
+ }
1864
+ ],
1865
+ stateMutability: "view"
1866
+ },
1867
+ {
1868
+ name: "send",
1869
+ type: "function",
1870
+ inputs: [
1871
+ SEND_PARAM_INPUT,
1811
1872
  {
1812
1873
  name: "_fee",
1813
1874
  type: "tuple",
@@ -1880,7 +1941,7 @@ var ScanHttpError = class extends Error {
1880
1941
  async function scanFetch(path, timeoutMs = 8e3) {
1881
1942
  const res = await fetch(`${LZ_SCAN_API}${path}`, { signal: AbortSignal.timeout(timeoutMs) });
1882
1943
  if (!res.ok) throw new ScanHttpError(res.status, path);
1883
- return res.json();
1944
+ return await res.json();
1884
1945
  }
1885
1946
  var TERMINAL_FAILURE_STATUSES = /* @__PURE__ */ new Set([
1886
1947
  "FAILED",
@@ -1896,62 +1957,221 @@ function median(values) {
1896
1957
  const mid = Math.floor(sorted.length / 2);
1897
1958
  return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
1898
1959
  }
1899
- function buildLzOptions(gasLimit = 0n) {
1960
+ var WORKER_EXECUTOR = 1;
1961
+ var OPTION_LZRECEIVE = 1;
1962
+ var OPTION_NATIVE_DROP = 2;
1963
+ function lzOption(optionType, payload) {
1964
+ const payloadBytes = (payload.length - 2) / 2;
1965
+ return concat([
1966
+ numberToHex(WORKER_EXECUTOR, { size: 1 }),
1967
+ numberToHex(1 + payloadBytes, { size: 2 }),
1968
+ numberToHex(optionType, { size: 1 }),
1969
+ payload
1970
+ ]);
1971
+ }
1972
+ function buildLzOptions(gasLimit = 0n, nativeDrop = 0n, dropReceiver) {
1973
+ const options = [
1974
+ lzOption(OPTION_LZRECEIVE, numberToHex(gasLimit, { size: 16 }))
1975
+ ];
1976
+ if (nativeDrop > 0n) {
1977
+ if (!dropReceiver) {
1978
+ throw new StableValidationError(
1979
+ "recipient",
1980
+ dropReceiver,
1981
+ "buildLzOptions: a native drop needs a receiver address"
1982
+ );
1983
+ }
1984
+ options.push(
1985
+ lzOption(
1986
+ OPTION_NATIVE_DROP,
1987
+ concat([
1988
+ numberToHex(nativeDrop, { size: 16 }),
1989
+ padHex(dropReceiver, { dir: "left", size: 32 })
1990
+ ])
1991
+ )
1992
+ );
1993
+ }
1900
1994
  return concat([
1901
1995
  numberToHex(3, { size: 2 }),
1902
1996
  // TYPE_3 header
1903
- numberToHex(1, { size: 1 }),
1904
- // Executor worker ID
1905
- numberToHex(17, { size: 2 }),
1906
- // option payload length = 17 bytes
1907
- numberToHex(1, { size: 1 }),
1908
- // LzReceive option type
1909
- numberToHex(gasLimit, { size: 16 })
1910
- // gas limit as uint128
1997
+ ...options
1911
1998
  ]);
1912
1999
  }
2000
+ var FALLBACK_SEND_GAS = 400000n;
2001
+ function usableQuote(quote, amountLD) {
2002
+ if (!quote?.oft) return quote;
2003
+ return quote.oft.amountSentLD <= amountLD ? quote : void 0;
2004
+ }
2005
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
1913
2006
  var LayerZeroService = class {
1914
2007
  constructor(config) {
1915
2008
  this.config = config;
1916
2009
  }
1917
2010
  async quoteBridge(params) {
1918
- const [fee, estimate] = await Promise.all([
1919
- this.quoteFee(params, "quoteBridge"),
2011
+ const plan = this.plan(params, "quoteBridge");
2012
+ const [{ fee, oft }, estimate] = await Promise.all([
2013
+ this.quotePathway(plan, "quoteBridge"),
1920
2014
  this.estimateBridgeTime(params).catch(() => void 0)
1921
2015
  ]);
1922
- return { ...fee, toAmount: params.amount, estimate };
2016
+ const srcGas = await this.estimateSendGas(plan, fee, oft);
2017
+ return {
2018
+ ...fee,
2019
+ oft,
2020
+ // What the recipient is actually credited: input minus any OFT fee and
2021
+ // minus dust. Falls back to the input when quoteOFT could not be read.
2022
+ toAmount: oft?.amountReceived ?? params.amount,
2023
+ srcGas,
2024
+ totalCostWei: srcGas ? srcGas.costWei + fee.nativeFee : void 0,
2025
+ estimate
2026
+ };
1923
2027
  }
1924
2028
  /**
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.
2029
+ * The LayerZero messaging fee for a send, plus the OFT's own accounting.
2030
+ *
2031
+ * The fee is required — a send cannot be built without it. The OFT accounting is
2032
+ * best-effort: an adapter that does not answer `quoteOFT` costs us the fee
2033
+ * breakdown and the exact output amount, not the whole quote.
1927
2034
  */
1928
- async quoteFee({ fromChain, toChain, fromToken, toToken, amount, recipient }, method) {
1929
- const recipientAddr = 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 = parseUnits4(String(amount), src.decimals);
1934
- try {
1935
- const { nativeFee, lzTokenFee } = await createPublicClient5({
1936
- chain: makeChain(src.id, src.rpc),
1937
- transport: http5(src.rpc)
1938
- }).readContract({
1939
- address: oftAdapter,
2035
+ async quotePathway(plan, method) {
2036
+ const sendParam = this.buildSendParams(plan, 0n);
2037
+ const [msgFee, oft] = await Promise.all([
2038
+ plan.pc.readContract({
2039
+ address: plan.oftAdapter,
1940
2040
  abi: OFT_ADAPTER_ABI,
1941
2041
  functionName: "quoteSend",
1942
- args: [this.buildSendParams(dst, amountLD, recipientAddr), false]
2042
+ args: [sendParam, false]
2043
+ }).catch((err) => {
2044
+ throw new StableQuoteError({
2045
+ provider: "layerzero",
2046
+ 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`,
2047
+ cause: err
2048
+ });
2049
+ }),
2050
+ this.readOftQuote(plan, sendParam)
2051
+ ]);
2052
+ const { nativeFee, lzTokenFee } = msgFee;
2053
+ return { fee: { nativeFee, lzTokenFee }, oft };
2054
+ }
2055
+ /**
2056
+ * The adapter's own view of a send: accepted range, its fees, and what lands on
2057
+ * the far side. Resolves to `undefined` rather than throwing — see `quoteSend`.
2058
+ */
2059
+ async readOftQuote(plan, sendParam) {
2060
+ try {
2061
+ const [limit, feeDetails, receipt] = await plan.pc.readContract({
2062
+ address: plan.oftAdapter,
2063
+ abi: OFT_ADAPTER_ABI,
2064
+ functionName: "quoteOFT",
2065
+ args: [sendParam]
2066
+ });
2067
+ const decimals = plan.srcToken.decimals;
2068
+ return {
2069
+ amountSentLD: receipt.amountSentLD,
2070
+ amountReceivedLD: receipt.amountReceivedLD,
2071
+ amountReceived: Number(formatUnits5(receipt.amountReceivedLD, decimals)),
2072
+ fees: feeDetails.map((detail) => ({
2073
+ amountLD: detail.feeAmountLD,
2074
+ amount: Number(formatUnits5(detail.feeAmountLD, decimals)),
2075
+ description: detail.description
2076
+ })),
2077
+ minAmountLD: limit.minAmountLD,
2078
+ maxAmountLD: limit.maxAmountLD
2079
+ };
2080
+ } catch {
2081
+ return void 0;
2082
+ }
2083
+ }
2084
+ /**
2085
+ * Source-chain cost of the `send` transaction.
2086
+ *
2087
+ * Simulating the real call is the only honest answer, and it needs a sender with
2088
+ * the allowance and the native balance already in place — which a quote taken
2089
+ * before approving does not have. When the simulation reverts, fall back to a
2090
+ * measured constant so the caller still has a cost to show, flagged as such.
2091
+ */
2092
+ async estimateSendGas(plan, fee, oft) {
2093
+ const gasPriceWei = await this.gasPrice(plan);
2094
+ if (gasPriceWei === void 0) return void 0;
2095
+ let gas = FALLBACK_SEND_GAS;
2096
+ let source = "fallback";
2097
+ const account = this.signerAddress();
2098
+ if (account) {
2099
+ try {
2100
+ gas = await plan.pc.estimateContractGas({
2101
+ address: plan.oftAdapter,
2102
+ abi: OFT_ADAPTER_ABI,
2103
+ functionName: "send",
2104
+ args: [
2105
+ this.buildSendParams(plan, oft?.amountReceivedLD ?? plan.amountLD),
2106
+ fee,
2107
+ account
2108
+ ],
2109
+ account,
2110
+ value: fee.nativeFee
2111
+ });
2112
+ source = "simulated";
2113
+ } catch {
2114
+ }
2115
+ }
2116
+ return { gas, gasPriceWei, costWei: gas * gasPriceWei, source };
2117
+ }
2118
+ /** Price per gas unit on the source chain, or undefined if the RPC won't say. */
2119
+ async gasPrice(plan) {
2120
+ try {
2121
+ const fees = await plan.pc.estimateFeesPerGas();
2122
+ const price = fees.maxFeePerGas ?? fees.gasPrice;
2123
+ if (price !== void 0) return price;
2124
+ } catch {
2125
+ }
2126
+ try {
2127
+ return await plan.pc.getGasPrice();
2128
+ } catch {
2129
+ return void 0;
2130
+ }
2131
+ }
2132
+ /**
2133
+ * Top the adapter's allowance up to `amountLD` when it is short. Only the
2134
+ * lockbox chain reaches here — remote deployments burn from the caller.
2135
+ */
2136
+ async ensureAllowance(plan, wc, owner, amountLD) {
2137
+ const { src, asset, srcToken, oftAdapter, pc, chain: srcChain } = plan;
2138
+ const allowance = await pc.readContract({
2139
+ address: srcToken.address,
2140
+ abi: erc20Abi4,
2141
+ functionName: "allowance",
2142
+ args: [owner, oftAdapter]
2143
+ });
2144
+ if (allowance >= amountLD) return;
2145
+ let approveHash;
2146
+ try {
2147
+ approveHash = await wc.writeContract({
2148
+ address: srcToken.address,
2149
+ abi: erc20Abi4,
2150
+ functionName: "approve",
2151
+ args: [oftAdapter, amountLD],
2152
+ account: wc.account,
2153
+ chain: srcChain
1943
2154
  });
1944
- return { nativeFee, lzTokenFee };
1945
2155
  } catch (err) {
1946
- throw new StableQuoteError({
1947
- provider: "layerzero",
1948
- shortMessage: `${method}: quoteSend call failed`,
2156
+ throw new StableTransactionError({
2157
+ phase: "approve",
2158
+ shortMessage: `bridge: ${asset} approve failed`,
2159
+ chainId: src.id,
1949
2160
  cause: err
1950
2161
  });
1951
2162
  }
2163
+ const approveReceipt = await pc.waitForTransactionReceipt({ hash: approveHash });
2164
+ if (approveReceipt.status === "reverted") {
2165
+ throw new StableTransactionError({
2166
+ phase: "approve",
2167
+ shortMessage: `bridge: ${asset} approve reverted on-chain`,
2168
+ txHash: approveReceipt.transactionHash,
2169
+ chainId: src.id
2170
+ });
2171
+ }
1952
2172
  }
1953
- async bridge({ fromChain, toChain, fromToken, toToken, amount, recipient, quote }) {
1954
- const from = this.config.walletClient?.account?.address ?? this.config.account?.address;
2173
+ async bridge(params) {
2174
+ const from = this.signerAddress();
1955
2175
  if (!from) {
1956
2176
  throw new StableValidationError(
1957
2177
  "signer",
@@ -1959,17 +2179,13 @@ var LayerZeroService = class {
1959
2179
  "bridge: requires a signer \u2014 set walletClient or account in StableConfig"
1960
2180
  );
1961
2181
  }
1962
- const recipientAddr = 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);
2182
+ const plan = this.plan({ ...params, recipient: params.recipient ?? from }, "bridge");
2183
+ const { src, srcToken, oftAdapter, amountLD, chain: srcChain } = plan;
1967
2184
  const wc = this.config.walletClient ?? createWalletClient4({
1968
2185
  account: this.config.account,
1969
2186
  chain: srcChain,
1970
2187
  transport: this.config.transport ?? http5(src.rpc)
1971
2188
  });
1972
- const pc = createPublicClient5({ chain: srcChain, transport: http5(src.rpc) });
1973
2189
  try {
1974
2190
  await wc.switchChain({ id: src.id });
1975
2191
  } catch (err) {
@@ -1980,43 +2196,11 @@ var LayerZeroService = class {
1980
2196
  cause: err
1981
2197
  });
1982
2198
  }
1983
- const amountLD = parseUnits4(String(amount), src.decimals);
1984
- const sendParams = this.buildSendParams(dst, amountLD, recipientAddr);
1985
- const { nativeFee, lzTokenFee } = quote?.nativeFee !== void 0 && quote?.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: erc20Abi4,
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: erc20Abi4,
1998
- functionName: "approve",
1999
- args: [oftAdapter, amountLD],
2000
- account: wc.account,
2001
- chain: srcChain
2002
- });
2003
- } catch (err) {
2004
- throw new 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 StableTransactionError({
2014
- phase: "approve",
2015
- shortMessage: "bridge: USDT0 approve reverted on-chain",
2016
- txHash: approveReceipt.transactionHash,
2017
- chainId: src.id
2018
- });
2019
- }
2199
+ const quote = usableQuote(params.quote, amountLD);
2200
+ const { nativeFee, lzTokenFee, oft } = quote?.nativeFee !== void 0 && quote?.lzTokenFee !== void 0 ? { nativeFee: quote.nativeFee, lzTokenFee: quote.lzTokenFee, oft: quote.oft } : await this.quotePathway(plan, "bridge").then((r) => ({ ...r.fee, oft: r.oft }));
2201
+ const sendParams = this.buildSendParams(plan, oft?.amountReceivedLD ?? amountLD);
2202
+ if (srcToken.approvalRequired !== false) {
2203
+ await this.ensureAllowance(plan, wc, from, amountLD);
2020
2204
  }
2021
2205
  try {
2022
2206
  const hash = await wc.writeContract({
@@ -2028,7 +2212,7 @@ var LayerZeroService = class {
2028
2212
  account: wc.account,
2029
2213
  chain: srcChain
2030
2214
  });
2031
- return { txHash: hash, toAmount: amount };
2215
+ return { txHash: hash, toAmount: oft?.amountReceived ?? params.amount };
2032
2216
  } catch (err) {
2033
2217
  throw new StableTransactionError({
2034
2218
  phase: "send",
@@ -2052,18 +2236,26 @@ var LayerZeroService = class {
2052
2236
  * computes to ~15s (60 blocks × 0.25s) while real deliveries take ~18min, because
2053
2237
  * the DVNs wait for the L2 batch to finalize on L1.
2054
2238
  */
2055
- async estimateBridgeTime({ fromChain, toChain }) {
2239
+ async estimateBridgeTime({
2240
+ fromChain,
2241
+ toChain,
2242
+ fromToken,
2243
+ toToken
2244
+ }) {
2056
2245
  const src = BRIDGE_CHAIN_CONFIGS[fromChain];
2057
2246
  const dst = BRIDGE_CHAIN_CONFIGS[toChain];
2058
- if (!src?.oftAdapter) {
2247
+ const from = fromToken ?? src?.tokens.usdt0?.address;
2248
+ const to = toToken ?? dst?.tokens.usdt0?.address;
2249
+ if (!(src && from)) {
2059
2250
  throw new StableValidationError("fromChain", fromChain, `estimateBridgeTime: no OFT adapter for "${fromChain}"`);
2060
2251
  }
2061
- if (!dst?.oftAdapter) {
2252
+ if (!(dst && to)) {
2062
2253
  throw new StableValidationError("toChain", toChain, `estimateBridgeTime: no OFT adapter for "${toChain}"`);
2063
2254
  }
2255
+ const { srcToken, dstToken } = this.resolveRoute(fromChain, toChain, from, to);
2064
2256
  const [uln, observedSeconds] = await Promise.all([
2065
- this.getUlnConfig(src, dst).catch(() => void 0),
2066
- this.observedBridgeSeconds(src, dst).catch(() => void 0)
2257
+ this.getUlnConfig(src, dst, srcToken.oftAdapter).catch(() => void 0),
2258
+ this.observedBridgeSeconds(src, dst, srcToken.oftAdapter, dstToken.oftAdapter).catch(() => void 0)
2067
2259
  ]);
2068
2260
  const dvnCount = uln ? uln.requiredDVNCount + uln.optionalDVNCount : void 0;
2069
2261
  let formulaSeconds;
@@ -2124,10 +2316,9 @@ var LayerZeroService = class {
2124
2316
  };
2125
2317
  }
2126
2318
  /** 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 () => {
2319
+ async getUlnConfig(src, dst, oapp) {
2320
+ return cached(`uln:${src.lzEid}-${dst.lzEid}-${oapp.toLowerCase()}`, CACHE_TTL_MS, async () => {
2129
2321
  const pc = createPublicClient5({ chain: makeChain(src.id, src.rpc), transport: http5(src.rpc) });
2130
- const oapp = src.oftAdapter;
2131
2322
  const endpoint = await pc.readContract({ address: oapp, abi: OAPP_ABI, functionName: "endpoint" });
2132
2323
  const sendLib = await pc.readContract({
2133
2324
  address: endpoint,
@@ -2149,8 +2340,8 @@ var LayerZeroService = class {
2149
2340
  });
2150
2341
  }
2151
2342
  /** 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()}`;
2343
+ async observedBridgeSeconds(src, dst, srcOapp, dstOapp) {
2344
+ const pathwayId = `${src.lzEid}-${dst.lzEid}-${srcOapp.toLowerCase()}-${dstOapp.toLowerCase()}`;
2154
2345
  return cached(`observed:${pathwayId}`, CACHE_TTL_MS, async () => {
2155
2346
  const { data } = await scanFetch(`/messages/pathway/${pathwayId}?limit=50`);
2156
2347
  const durations = (data ?? []).filter((m) => m.status?.name === "DELIVERED").map((m) => (m.destination?.tx?.blockTimestamp ?? 0) - (m.source?.tx?.blockTimestamp ?? 0)).filter((d) => d > 0);
@@ -2180,37 +2371,95 @@ var LayerZeroService = class {
2180
2371
  );
2181
2372
  }
2182
2373
  }
2183
- resolveChains(fromChain, toChain, fromToken, toToken) {
2374
+ /**
2375
+ * Resolve a bridge request to one OFT mesh and its two endpoints.
2376
+ *
2377
+ * Both sides must name the same asset. An OFT escrows one specific ERC-20 and
2378
+ * mints its own peer on the far side — it cannot hand back a different token,
2379
+ * so a USDT0→XAUt0 request is a trade, not a bridge, and belongs to LI.FI.
2380
+ */
2381
+ resolveRoute(fromChain, toChain, fromToken, toToken) {
2184
2382
  const src = BRIDGE_CHAIN_CONFIGS[fromChain];
2185
2383
  const dst = BRIDGE_CHAIN_CONFIGS[toChain];
2186
2384
  if (!src) throw new StableValidationError("fromChain", fromChain, `Unsupported source chain: "${fromChain}"`);
2187
2385
  if (!dst) throw new StableValidationError("toChain", toChain, `Unsupported destination chain: "${toChain}"`);
2188
- if (!src.oftAdapter) {
2189
- throw new StableValidationError("fromChain", fromChain, `No OFT adapter configured for "${fromChain}"`);
2190
- }
2191
- if (fromToken.toLowerCase() !== src.usdt.toLowerCase()) {
2386
+ const asset = bridgeAssetFor(src, fromToken);
2387
+ if (!asset) {
2192
2388
  throw new StableValidationError(
2193
2389
  "fromToken",
2194
2390
  fromToken,
2195
- `USDT0 bridge only moves USDT0 \u2014 fromToken must be ${src.usdt} on "${fromChain}", got "${fromToken}". Use route "lifi" to bridge other tokens.`
2391
+ `Native OFT bridge does not recognize "${fromToken}" on "${fromChain}" \u2014 it moves ${describeAssets(src)}. Use route "lifi" to bridge other tokens.`
2196
2392
  );
2197
2393
  }
2198
- if (toToken.toLowerCase() !== dst.usdt.toLowerCase()) {
2394
+ const srcToken = src.tokens[asset];
2395
+ const dstToken = dst.tokens[asset];
2396
+ if (!dstToken) {
2397
+ throw new StableValidationError(
2398
+ "toChain",
2399
+ toChain,
2400
+ `Native OFT bridge cannot deliver ${asset} to "${toChain}" \u2014 no ${asset} deployment there. Use route "lifi" instead.`
2401
+ );
2402
+ }
2403
+ if (dstToken.address.toLowerCase() !== toToken.toLowerCase()) {
2199
2404
  throw new StableValidationError(
2200
2405
  "toToken",
2201
2406
  toToken,
2202
- `USDT0 bridge only moves USDT0 \u2014 toToken must be ${dst.usdt} on "${toChain}", got "${toToken}". Use route "lifi" to bridge other tokens.`
2407
+ `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
2408
  );
2204
2409
  }
2205
- return { src, dst };
2410
+ return { src, dst, asset, srcToken, dstToken };
2206
2411
  }
2207
- buildSendParams(dst, amountLD, recipient) {
2412
+ /**
2413
+ * Validate a request and resolve it to a single OFT pathway. No network calls —
2414
+ * everything here is config, arithmetic and encoding.
2415
+ */
2416
+ plan(params, method) {
2417
+ const { fromChain, toChain, fromToken, toToken, amount, recipient, dstNativeDrop } = params;
2418
+ const recipientAddr = recipient ?? ZERO_ADDRESS;
2419
+ this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, method);
2420
+ if (dstNativeDrop !== void 0 && dstNativeDrop < 0n) {
2421
+ throw new StableValidationError(
2422
+ "dstNativeDrop",
2423
+ dstNativeDrop,
2424
+ `${method}: dstNativeDrop must not be negative`
2425
+ );
2426
+ }
2427
+ const { src, dst, asset, srcToken, dstToken } = this.resolveRoute(
2428
+ fromChain,
2429
+ toChain,
2430
+ fromToken,
2431
+ toToken
2432
+ );
2433
+ const chain = makeChain(src.id, src.rpc);
2434
+ return {
2435
+ src,
2436
+ dst,
2437
+ asset,
2438
+ srcToken,
2439
+ dstToken,
2440
+ oftAdapter: srcToken.oftAdapter,
2441
+ amountLD: parseUnits4(String(amount), srcToken.decimals),
2442
+ recipient: recipientAddr,
2443
+ nativeDrop: dstNativeDrop ?? 0n,
2444
+ chain,
2445
+ pc: createPublicClient5({ chain, transport: http5(src.rpc) })
2446
+ };
2447
+ }
2448
+ /** The address that will sign, or undefined when this service has no signer. */
2449
+ signerAddress() {
2450
+ return this.config.walletClient?.account?.address ?? this.config.account?.address;
2451
+ }
2452
+ /**
2453
+ * `SendParam` for one call. `minAmountLD` is the caller's slippage floor: 0 when
2454
+ * asking what a send would yield, and the quoted output when actually sending.
2455
+ */
2456
+ buildSendParams(plan, minAmountLD) {
2208
2457
  return {
2209
- dstEid: dst.lzEid,
2210
- to: padHex(recipient, { dir: "left", size: 32 }),
2211
- amountLD,
2212
- minAmountLD: amountLD,
2213
- extraOptions: buildLzOptions(),
2458
+ dstEid: plan.dst.lzEid,
2459
+ to: padHex(plan.recipient, { dir: "left", size: 32 }),
2460
+ amountLD: plan.amountLD,
2461
+ minAmountLD,
2462
+ extraOptions: buildLzOptions(0n, plan.nativeDrop, plan.recipient),
2214
2463
  composeMsg: "0x",
2215
2464
  oftCmd: "0x"
2216
2465
  };
@@ -2218,21 +2467,37 @@ var LayerZeroService = class {
2218
2467
  };
2219
2468
 
2220
2469
  // src/stable.ts
2221
- function isUsdt0Route(fromChain, toChain, fromToken, toToken) {
2470
+ function isNativeOftRoute(fromChain, toChain, fromToken, toToken) {
2222
2471
  const src = BRIDGE_CHAIN_CONFIGS[fromChain];
2223
2472
  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();
2473
+ if (!(src && dst)) return false;
2474
+ const asset = bridgeAssetFor(src, fromToken);
2475
+ if (!asset) return false;
2476
+ const srcToken = src.tokens[asset];
2477
+ const dstToken = dst.tokens[asset];
2478
+ return src.lzOutbound !== false && !!srcToken && srcToken.lzOutbound !== false && !!dstToken && dstToken.address.toLowerCase() === toToken.toLowerCase();
2225
2479
  }
2226
2480
  function selectBridge(params) {
2481
+ const selected = resolveBridge(params);
2482
+ if (selected === "lifi" && (params.dstNativeDrop ?? 0n) > 0n) {
2483
+ throw new StableValidationError(
2484
+ "dstNativeDrop",
2485
+ params.dstNativeDrop,
2486
+ `bridge: dstNativeDrop is only supported on the native OFT route, and ${params.fromChain}\u2192${params.toChain} resolved to LI.FI`
2487
+ );
2488
+ }
2489
+ return selected;
2490
+ }
2491
+ function resolveBridge(params) {
2227
2492
  const route = params.route ?? "auto";
2228
- if (route === "usdt0") return "usdt0";
2493
+ if (route === "oft" || route === "usdt0") return "oft";
2229
2494
  if (route === "lifi") return "lifi";
2230
- return isUsdt0Route(
2495
+ return isNativeOftRoute(
2231
2496
  params.fromChain,
2232
2497
  params.toChain,
2233
2498
  params.fromToken,
2234
2499
  params.toToken
2235
- ) ? "usdt0" : "lifi";
2500
+ ) ? "oft" : "lifi";
2236
2501
  }
2237
2502
  function validateTransferParams({
2238
2503
  to,
@@ -2414,8 +2679,10 @@ function createStable({
2414
2679
  }
2415
2680
  return {
2416
2681
  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),
2682
+ // `async` so a rejected route (see selectBridge) surfaces as a rejected
2683
+ // promise like every other validation error, not a synchronous throw.
2684
+ quoteBridge: async (params) => selectBridge(params) === "oft" ? lz.quoteBridge(params) : lifi.quoteBridge(params),
2685
+ bridge: async (params) => selectBridge(params) === "oft" ? lz.bridge(params) : lifi.bridge(params),
2419
2686
  estimateBridgeTime: (params) => lz.estimateBridgeTime(params),
2420
2687
  trackBridge: (srcTxHash) => lz.trackBridge(srcTxHash),
2421
2688
  quoteSwap: (params) => lifi.quoteSwap(params),
@@ -2470,6 +2737,7 @@ export {
2470
2737
  StableQuoteError,
2471
2738
  StableTransactionError,
2472
2739
  StableValidationError,
2740
+ bridgeAssetFor,
2473
2741
  cancelAuthorization,
2474
2742
  clearBridgeEstimateCache,
2475
2743
  createStable,