@stablechain/sdk 1.0.4 → 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
@@ -1,3 +1,25 @@
1
+ import {
2
+ ERC3009_ABI,
3
+ STABLE_USDT_ADDRESS,
4
+ STABLE_VAULT_ADDRESS,
5
+ StableBulkError,
6
+ StableError,
7
+ StableNetworkError,
8
+ StableQuoteError,
9
+ StableTransactionError,
10
+ StableValidationError,
11
+ cancelAuthorization,
12
+ erc3009Actions,
13
+ getAuthorizationState,
14
+ getAuthorizationUsage,
15
+ prepareTransferWithAuthorization,
16
+ receiveWithAuthorization,
17
+ signReceiveAuthorization,
18
+ signTransferAuthorization,
19
+ transferWithAuthorization,
20
+ verifyAuthorization
21
+ } from "./chunk-VS6GBRDF.js";
22
+
1
23
  // src/stable.ts
2
24
  import {
3
25
  http as http6,
@@ -25,85 +47,6 @@ import {
25
47
  parseUnits
26
48
  } from "viem";
27
49
 
28
- // src/errors.ts
29
- import { BaseError } from "viem";
30
- var StableError = class extends BaseError {
31
- };
32
- var StableValidationError = class extends StableError {
33
- constructor(field, value, reason, opts) {
34
- super(reason, {
35
- name: "StableValidationError",
36
- cause: opts?.cause,
37
- metaMessages: [`Field: ${field}`, `Value: ${formatValue(value)}`]
38
- });
39
- this.field = field;
40
- this.value = value;
41
- }
42
- };
43
- var StableQuoteError = class extends StableError {
44
- constructor(args) {
45
- const meta = [`Provider: ${args.provider}`];
46
- if (args.httpStatus !== void 0)
47
- meta.push(`HTTP status: ${args.httpStatus}`);
48
- if (args.providerCode) meta.push(`Provider code: ${args.providerCode}`);
49
- super(args.shortMessage, {
50
- name: "StableQuoteError",
51
- cause: args.cause,
52
- metaMessages: meta
53
- });
54
- this.provider = args.provider;
55
- this.httpStatus = args.httpStatus;
56
- this.providerCode = args.providerCode;
57
- this.body = args.body;
58
- }
59
- };
60
- var StableTransactionError = class extends StableError {
61
- constructor(args) {
62
- const meta = [`Phase: ${args.phase}`];
63
- if (args.chainId !== void 0) meta.push(`Chain ID: ${args.chainId}`);
64
- if (args.txHash) meta.push(`Tx hash: ${args.txHash}`);
65
- if (args.revertReason) meta.push(`Revert reason: ${args.revertReason}`);
66
- super(args.shortMessage, {
67
- name: "StableTransactionError",
68
- cause: args.cause,
69
- metaMessages: meta
70
- });
71
- this.phase = args.phase;
72
- this.txHash = args.txHash;
73
- this.chainId = args.chainId;
74
- this.revertReason = args.revertReason;
75
- }
76
- };
77
- var StableNetworkError = class extends StableError {
78
- constructor(args) {
79
- super(args.shortMessage, {
80
- name: "StableNetworkError",
81
- cause: args.cause,
82
- metaMessages: args.url ? [`URL: ${args.url}`] : void 0
83
- });
84
- this.url = args.url;
85
- }
86
- };
87
- var StableBulkError = class extends StableError {
88
- constructor(args) {
89
- super(args.shortMessage, { name: "StableBulkError" });
90
- this.results = args.results;
91
- this.succeeded = args.succeeded;
92
- this.failed = args.failed;
93
- }
94
- };
95
- function formatValue(v) {
96
- if (typeof v === "bigint") return `${v}n`;
97
- if (typeof v === "string") return JSON.stringify(v);
98
- if (v === null) return "null";
99
- if (v === void 0) return "undefined";
100
- try {
101
- return JSON.stringify(v);
102
- } catch {
103
- return String(v);
104
- }
105
- }
106
-
107
50
  // src/helpers/chain.ts
108
51
  import { defineChain } from "viem";
109
52
  function makeChain(id, rpc) {
@@ -807,19 +750,27 @@ import {
807
750
  import { stable as stableMainnet, stableTestnet } from "viem/chains";
808
751
 
809
752
  // src/types.ts
810
- var Chain = /* @__PURE__ */ ((Chain3) => {
811
- Chain3["Sepolia"] = "sepolia";
812
- Chain3["StableTestnet"] = "stable-testnet";
813
- Chain3["Stable"] = "stable";
814
- Chain3["Ethereum"] = "ethereum";
815
- Chain3["Arbitrum"] = "arbitrum";
816
- Chain3["Ink"] = "ink";
817
- Chain3["Bera"] = "bera";
818
- Chain3["MegaETH"] = "megaeth";
819
- Chain3["Base"] = "base";
820
- Chain3["BSC"] = "bsc";
821
- Chain3["HyperEVM"] = "hyperevm";
822
- return Chain3;
753
+ var Chain = /* @__PURE__ */ ((Chain2) => {
754
+ Chain2["Sepolia"] = "sepolia";
755
+ Chain2["StableTestnet"] = "stable-testnet";
756
+ Chain2["Stable"] = "stable";
757
+ Chain2["Ethereum"] = "ethereum";
758
+ Chain2["Arbitrum"] = "arbitrum";
759
+ Chain2["Ink"] = "ink";
760
+ Chain2["Bera"] = "bera";
761
+ Chain2["MegaETH"] = "megaeth";
762
+ Chain2["Base"] = "base";
763
+ Chain2["BSC"] = "bsc";
764
+ Chain2["HyperEVM"] = "hyperevm";
765
+ Chain2["Polygon"] = "polygon";
766
+ Chain2["Plasma"] = "plasma";
767
+ Chain2["Optimism"] = "optimism";
768
+ Chain2["Unichain"] = "unichain";
769
+ Chain2["Sei"] = "sei";
770
+ Chain2["Flare"] = "flare";
771
+ Chain2["XLayer"] = "xlayer";
772
+ Chain2["Rootstock"] = "rootstock";
773
+ return Chain2;
823
774
  })(Chain || {});
824
775
  var Network = /* @__PURE__ */ ((Network2) => {
825
776
  Network2["Mainnet"] = "mainnet";
@@ -1259,7 +1210,7 @@ var CHAIN_CONFIGS = {
1259
1210
  id: 11155111,
1260
1211
  rpc: "https://ethereum-sepolia-rpc.publicnode.com",
1261
1212
  usdt: "0xc4DCC311c028e341fd8602D8eB89c5de94625927",
1262
- decimals: 18
1213
+ decimals: 6
1263
1214
  },
1264
1215
  ["stable-testnet" /* StableTestnet */]: {
1265
1216
  id: 2201,
@@ -1277,13 +1228,15 @@ var CHAIN_CONFIGS = {
1277
1228
  ["ethereum" /* Ethereum */]: {
1278
1229
  id: 1,
1279
1230
  rpc: "https://ethereum-rpc.publicnode.com",
1280
- usdt: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee",
1231
+ // Canonical USDT. Not the OFT adapter — this field is used as an ERC-20
1232
+ // (allowance/approve/balanceOf) and as the token address for LI.FI.
1233
+ usdt: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
1281
1234
  decimals: 6
1282
1235
  },
1283
1236
  ["arbitrum" /* Arbitrum */]: {
1284
1237
  id: 42161,
1285
1238
  rpc: "https://arbitrum-one-rpc.publicnode.com",
1286
- usdt: "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92",
1239
+ usdt: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
1287
1240
  decimals: 6
1288
1241
  },
1289
1242
  ["ink" /* Ink */]: {
@@ -1300,7 +1253,7 @@ var CHAIN_CONFIGS = {
1300
1253
  },
1301
1254
  ["megaeth" /* MegaETH */]: {
1302
1255
  id: 4326,
1303
- rpc: "https://carrot.megaeth.com",
1256
+ rpc: "https://mainnet.megaeth.com/rpc",
1304
1257
  usdt: "0xb8ce59fc3717ada4c02eadf9682a9e934f625ebb",
1305
1258
  decimals: 6
1306
1259
  },
@@ -1321,6 +1274,55 @@ var CHAIN_CONFIGS = {
1321
1274
  rpc: "https://rpc.hyperliquid.xyz/evm",
1322
1275
  usdt: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb",
1323
1276
  decimals: 6
1277
+ },
1278
+ ["polygon" /* Polygon */]: {
1279
+ id: 137,
1280
+ rpc: "https://polygon-bor-rpc.publicnode.com",
1281
+ // Polygon's canonical USDT, migrated to USDT0
1282
+ usdt: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
1283
+ decimals: 6
1284
+ },
1285
+ ["plasma" /* Plasma */]: {
1286
+ id: 9745,
1287
+ rpc: "https://rpc.plasma.to",
1288
+ usdt: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb",
1289
+ decimals: 6
1290
+ },
1291
+ ["optimism" /* Optimism */]: {
1292
+ id: 10,
1293
+ rpc: "https://optimism-rpc.publicnode.com",
1294
+ usdt: "0x01bFF41798a0BcF287b996046Ca68b395DbC1071",
1295
+ decimals: 6
1296
+ },
1297
+ ["unichain" /* Unichain */]: {
1298
+ id: 130,
1299
+ rpc: "https://unichain-rpc.publicnode.com",
1300
+ usdt: "0x9151434b16b9763660705744891fA906F660EcC5",
1301
+ decimals: 6
1302
+ },
1303
+ ["sei" /* Sei */]: {
1304
+ id: 1329,
1305
+ rpc: "https://sei-evm-rpc.publicnode.com",
1306
+ usdt: "0x9151434b16b9763660705744891fA906F660EcC5",
1307
+ decimals: 6
1308
+ },
1309
+ ["flare" /* Flare */]: {
1310
+ id: 14,
1311
+ rpc: "https://flare-api.flare.network/ext/C/rpc",
1312
+ usdt: "0xe7cd86e13AC4309349F30B3435a9d337750fC82D",
1313
+ decimals: 6
1314
+ },
1315
+ ["xlayer" /* XLayer */]: {
1316
+ id: 196,
1317
+ rpc: "https://rpc.xlayer.tech",
1318
+ usdt: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
1319
+ decimals: 6
1320
+ },
1321
+ ["rootstock" /* Rootstock */]: {
1322
+ id: 30,
1323
+ rpc: "https://public-node.rsk.co",
1324
+ usdt: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
1325
+ decimals: 6
1324
1326
  }
1325
1327
  };
1326
1328
 
@@ -1733,40 +1735,83 @@ import {
1733
1735
  isAddress as isAddress4,
1734
1736
  numberToHex,
1735
1737
  padHex,
1738
+ formatUnits as formatUnits5,
1736
1739
  parseUnits as parseUnits4
1737
1740
  } from "viem";
1738
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
+ }
1739
1762
  var BRIDGE_CHAIN_CONFIGS = {
1740
1763
  // ─── Testnet ───────────────────────────────────────────────────────────────
1741
- ["sepolia" /* Sepolia */]: { ...C["sepolia" /* Sepolia */], lzEid: 40161, oftAdapter: "0xc099cD946d5efCC35A99D64E808c1430cEf08126" },
1742
- ["stable-testnet" /* StableTestnet */]: { ...C["stable-testnet" /* StableTestnet */], lzEid: 40374, oftAdapter: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736" },
1764
+ ["sepolia" /* Sepolia */]: bridgeChain("sepolia" /* Sepolia */, 40161, 12, "0xc099cD946d5efCC35A99D64E808c1430cEf08126"),
1765
+ ["stable-testnet" /* StableTestnet */]: bridgeChain("stable-testnet" /* StableTestnet */, 40374, 0.7, "0x779Ded0c9e1022225f8E0630b35a9b54bE713736"),
1743
1766
  // ─── Mainnet ──────────────────────────────────────────────────────────────
1744
- ["stable" /* Stable */]: { ...C["stable" /* Stable */], lzEid: 30396, oftAdapter: "0xedaba024be4d87974d5aB11C6Dd586963CcCB027" },
1745
- ["ethereum" /* Ethereum */]: { ...C["ethereum" /* Ethereum */], lzEid: 30101, oftAdapter: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee" },
1746
- ["arbitrum" /* Arbitrum */]: { ...C["arbitrum" /* Arbitrum */], lzEid: 30110, oftAdapter: "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92" },
1747
- ["ink" /* Ink */]: { ...C["ink" /* Ink */], lzEid: 30339, oftAdapter: "0x1cB6De532588fCA4a21B7209DE7C456AF8434A65" },
1748
- ["bera" /* Bera */]: { ...C["bera" /* Bera */], lzEid: 30362, oftAdapter: "0x3Dc96399109df5ceb2C226664A086140bD0379cB" },
1749
- ["megaeth" /* MegaETH */]: { ...C["megaeth" /* MegaETH */], lzEid: 30398, oftAdapter: "0x9151434b16b9763660705744891fa906f660ecc5" },
1750
- ["hyperevm" /* HyperEVM */]: { ...C["hyperevm" /* HyperEVM */], lzEid: 30367, oftAdapter: "0x904861a24F30EC96ea7CFC3bE9EA4B476d237e98" }
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
+ ]
1751
1808
  };
1752
1809
  var OFT_ADAPTER_ABI = [
1753
1810
  {
1754
1811
  name: "quoteSend",
1755
1812
  type: "function",
1756
1813
  inputs: [
1757
- {
1758
- name: "_sendParam",
1759
- type: "tuple",
1760
- components: [
1761
- { name: "dstEid", type: "uint32" },
1762
- { name: "to", type: "bytes32" },
1763
- { name: "amountLD", type: "uint256" },
1764
- { name: "minAmountLD", type: "uint256" },
1765
- { name: "extraOptions", type: "bytes" },
1766
- { name: "composeMsg", type: "bytes" },
1767
- { name: "oftCmd", type: "bytes" }
1768
- ]
1769
- },
1814
+ SEND_PARAM_INPUT,
1770
1815
  { name: "_payInLzToken", type: "bool" }
1771
1816
  ],
1772
1817
  outputs: [
@@ -1782,22 +1827,48 @@ var OFT_ADAPTER_ABI = [
1782
1827
  stateMutability: "view"
1783
1828
  },
1784
1829
  {
1785
- 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",
1786
1834
  type: "function",
1787
1835
  inputs: [
1836
+ SEND_PARAM_INPUT
1837
+ ],
1838
+ outputs: [
1788
1839
  {
1789
- name: "_sendParam",
1840
+ name: "oftLimit",
1790
1841
  type: "tuple",
1791
1842
  components: [
1792
- { name: "dstEid", type: "uint32" },
1793
- { name: "to", type: "bytes32" },
1794
- { name: "amountLD", type: "uint256" },
1795
1843
  { name: "minAmountLD", type: "uint256" },
1796
- { name: "extraOptions", type: "bytes" },
1797
- { name: "composeMsg", type: "bytes" },
1798
- { name: "oftCmd", type: "bytes" }
1844
+ { name: "maxAmountLD", type: "uint256" }
1799
1845
  ]
1800
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" }
1854
+ ]
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,
1801
1872
  {
1802
1873
  name: "_fee",
1803
1874
  type: "tuple",
@@ -1812,51 +1883,295 @@ var OFT_ADAPTER_ABI = [
1812
1883
  stateMutability: "payable"
1813
1884
  }
1814
1885
  ];
1815
- function buildLzOptions(gasLimit = 0n) {
1886
+ var OAPP_ABI = [
1887
+ { name: "endpoint", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] }
1888
+ ];
1889
+ var ENDPOINT_V2_ABI = [
1890
+ {
1891
+ name: "getSendLibrary",
1892
+ type: "function",
1893
+ stateMutability: "view",
1894
+ inputs: [{ name: "_sender", type: "address" }, { name: "_dstEid", type: "uint32" }],
1895
+ outputs: [{ type: "address" }]
1896
+ }
1897
+ ];
1898
+ var SEND_ULN_ABI = [
1899
+ {
1900
+ name: "getUlnConfig",
1901
+ type: "function",
1902
+ stateMutability: "view",
1903
+ inputs: [{ name: "_oapp", type: "address" }, { name: "_remoteEid", type: "uint32" }],
1904
+ outputs: [
1905
+ {
1906
+ name: "rtnConfig",
1907
+ type: "tuple",
1908
+ components: [
1909
+ { name: "confirmations", type: "uint64" },
1910
+ { name: "requiredDVNCount", type: "uint8" },
1911
+ { name: "optionalDVNCount", type: "uint8" },
1912
+ { name: "optionalDVNThreshold", type: "uint8" },
1913
+ { name: "requiredDVNs", type: "address[]" },
1914
+ { name: "optionalDVNs", type: "address[]" }
1915
+ ]
1916
+ }
1917
+ ]
1918
+ }
1919
+ ];
1920
+ var LZ_SCAN_API = "https://scan.layerzero-api.com/v1";
1921
+ var LZ_SCAN_UI = "https://layerzeroscan.com";
1922
+ var CACHE_TTL_MS = 5 * 6e4;
1923
+ var cache = /* @__PURE__ */ new Map();
1924
+ function clearBridgeEstimateCache() {
1925
+ cache.clear();
1926
+ }
1927
+ async function cached(key, ttlMs, load) {
1928
+ const hit = cache.get(key);
1929
+ if (hit && Date.now() - hit.at < ttlMs) return hit.value;
1930
+ const value = await load();
1931
+ cache.set(key, { at: Date.now(), value });
1932
+ return value;
1933
+ }
1934
+ var ScanHttpError = class extends Error {
1935
+ constructor(status, path) {
1936
+ super(`LayerZero Scan ${status} for ${path}`);
1937
+ this.status = status;
1938
+ this.name = "ScanHttpError";
1939
+ }
1940
+ };
1941
+ async function scanFetch(path, timeoutMs = 8e3) {
1942
+ const res = await fetch(`${LZ_SCAN_API}${path}`, { signal: AbortSignal.timeout(timeoutMs) });
1943
+ if (!res.ok) throw new ScanHttpError(res.status, path);
1944
+ return await res.json();
1945
+ }
1946
+ var TERMINAL_FAILURE_STATUSES = /* @__PURE__ */ new Set([
1947
+ "FAILED",
1948
+ "BLOCKED",
1949
+ "PAYLOAD_STORED",
1950
+ "APPLICATION_BURNED",
1951
+ "APPLICATION_SKIPPED",
1952
+ "UNRESOLVABLE_COMMAND",
1953
+ "MALFORMED_COMMAND"
1954
+ ]);
1955
+ function median(values) {
1956
+ const sorted = [...values].sort((a, b) => a - b);
1957
+ const mid = Math.floor(sorted.length / 2);
1958
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
1959
+ }
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
+ }
1816
1994
  return concat([
1817
1995
  numberToHex(3, { size: 2 }),
1818
1996
  // TYPE_3 header
1819
- numberToHex(1, { size: 1 }),
1820
- // Executor worker ID
1821
- numberToHex(17, { size: 2 }),
1822
- // option payload length = 17 bytes
1823
- numberToHex(1, { size: 1 }),
1824
- // LzReceive option type
1825
- numberToHex(gasLimit, { size: 16 })
1826
- // gas limit as uint128
1997
+ ...options
1827
1998
  ]);
1828
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";
1829
2006
  var LayerZeroService = class {
1830
2007
  constructor(config) {
1831
2008
  this.config = config;
1832
2009
  }
1833
- async quoteBridge({ fromChain, toChain, fromToken, toToken, amount, recipient }) {
1834
- const recipientAddr = recipient ?? "0x0000000000000000000000000000000000000000";
1835
- this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, "quoteBridge");
1836
- const { src, dst } = this.resolveChains(fromChain, toChain, fromToken, toToken);
1837
- const oftAdapter = src.oftAdapter;
1838
- const amountLD = parseUnits4(String(amount), src.decimals);
1839
- try {
1840
- const { nativeFee, lzTokenFee } = await createPublicClient5({
1841
- chain: makeChain(src.id, src.rpc),
1842
- transport: http5(src.rpc)
1843
- }).readContract({
1844
- address: oftAdapter,
2010
+ async quoteBridge(params) {
2011
+ const plan = this.plan(params, "quoteBridge");
2012
+ const [{ fee, oft }, estimate] = await Promise.all([
2013
+ this.quotePathway(plan, "quoteBridge"),
2014
+ this.estimateBridgeTime(params).catch(() => void 0)
2015
+ ]);
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
+ };
2027
+ }
2028
+ /**
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.
2034
+ */
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,
1845
2040
  abi: OFT_ADAPTER_ABI,
1846
2041
  functionName: "quoteSend",
1847
- 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
1848
2154
  });
1849
- return { nativeFee, lzTokenFee, toAmount: amount };
1850
2155
  } catch (err) {
1851
- throw new StableQuoteError({
1852
- provider: "layerzero",
1853
- shortMessage: "quoteBridge: quoteSend call failed",
2156
+ throw new StableTransactionError({
2157
+ phase: "approve",
2158
+ shortMessage: `bridge: ${asset} approve failed`,
2159
+ chainId: src.id,
1854
2160
  cause: err
1855
2161
  });
1856
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
+ }
1857
2172
  }
1858
- async bridge({ fromChain, toChain, fromToken, toToken, amount, recipient, quote }) {
1859
- const from = this.config.walletClient?.account?.address ?? this.config.account?.address;
2173
+ async bridge(params) {
2174
+ const from = this.signerAddress();
1860
2175
  if (!from) {
1861
2176
  throw new StableValidationError(
1862
2177
  "signer",
@@ -1864,17 +2179,13 @@ var LayerZeroService = class {
1864
2179
  "bridge: requires a signer \u2014 set walletClient or account in StableConfig"
1865
2180
  );
1866
2181
  }
1867
- const recipientAddr = recipient ?? from;
1868
- this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, "bridge");
1869
- const { src, dst } = this.resolveChains(fromChain, toChain, fromToken, toToken);
1870
- const oftAdapter = src.oftAdapter;
1871
- 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;
1872
2184
  const wc = this.config.walletClient ?? createWalletClient4({
1873
2185
  account: this.config.account,
1874
2186
  chain: srcChain,
1875
2187
  transport: this.config.transport ?? http5(src.rpc)
1876
2188
  });
1877
- const pc = createPublicClient5({ chain: srcChain, transport: http5(src.rpc) });
1878
2189
  try {
1879
2190
  await wc.switchChain({ id: src.id });
1880
2191
  } catch (err) {
@@ -1885,44 +2196,11 @@ var LayerZeroService = class {
1885
2196
  cause: err
1886
2197
  });
1887
2198
  }
1888
- const amountLD = parseUnits4(String(amount), src.decimals);
1889
- const sendParams = this.buildSendParams(dst, amountLD, recipientAddr);
1890
- const lzFee = quote?.nativeFee !== void 0 && quote?.lzTokenFee !== void 0 ? { nativeFee: quote.nativeFee, lzTokenFee: quote.lzTokenFee } : await this.quoteBridge({ fromChain, toChain, fromToken, toToken, amount, recipient: recipientAddr });
1891
- const { nativeFee = 0n, lzTokenFee = 0n } = lzFee;
1892
- const allowance = await pc.readContract({
1893
- address: src.usdt,
1894
- abi: erc20Abi4,
1895
- functionName: "allowance",
1896
- args: [from, oftAdapter]
1897
- });
1898
- if (allowance < amountLD) {
1899
- let approveHash;
1900
- try {
1901
- approveHash = await wc.writeContract({
1902
- address: src.usdt,
1903
- abi: erc20Abi4,
1904
- functionName: "approve",
1905
- args: [oftAdapter, amountLD],
1906
- account: wc.account,
1907
- chain: srcChain
1908
- });
1909
- } catch (err) {
1910
- throw new StableTransactionError({
1911
- phase: "approve",
1912
- shortMessage: "bridge: USDT0 approve failed",
1913
- chainId: src.id,
1914
- cause: err
1915
- });
1916
- }
1917
- const approveReceipt = await pc.waitForTransactionReceipt({ hash: approveHash });
1918
- if (approveReceipt.status === "reverted") {
1919
- throw new StableTransactionError({
1920
- phase: "approve",
1921
- shortMessage: "bridge: USDT0 approve reverted on-chain",
1922
- txHash: approveReceipt.transactionHash,
1923
- chainId: src.id
1924
- });
1925
- }
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);
1926
2204
  }
1927
2205
  try {
1928
2206
  const hash = await wc.writeContract({
@@ -1934,7 +2212,7 @@ var LayerZeroService = class {
1934
2212
  account: wc.account,
1935
2213
  chain: srcChain
1936
2214
  });
1937
- return { txHash: hash, toAmount: amount };
2215
+ return { txHash: hash, toAmount: oft?.amountReceived ?? params.amount };
1938
2216
  } catch (err) {
1939
2217
  throw new StableTransactionError({
1940
2218
  phase: "send",
@@ -1944,6 +2222,132 @@ var LayerZeroService = class {
1944
2222
  });
1945
2223
  }
1946
2224
  }
2225
+ /**
2226
+ * Estimate how long a USDT0 bridge will take, end to end.
2227
+ *
2228
+ * Two independent inputs, both cheap:
2229
+ * - **observed** — median delivery time of recent messages LayerZero Scan has
2230
+ * already settled on this exact pathway.
2231
+ * - **formula** — LayerZero's documented estimate from the on-chain ULN config:
2232
+ * `srcBlockTime × confirmations + dstBlockTime × (2 + dvnCount)`.
2233
+ *
2234
+ * `observed` wins when available. The formula assumes `confirmations` is the whole
2235
+ * wait, which holds for L1 sources but badly understates rollups: Arbitrum→Stable
2236
+ * computes to ~15s (60 blocks × 0.25s) while real deliveries take ~18min, because
2237
+ * the DVNs wait for the L2 batch to finalize on L1.
2238
+ */
2239
+ async estimateBridgeTime({
2240
+ fromChain,
2241
+ toChain,
2242
+ fromToken,
2243
+ toToken
2244
+ }) {
2245
+ const src = BRIDGE_CHAIN_CONFIGS[fromChain];
2246
+ const dst = BRIDGE_CHAIN_CONFIGS[toChain];
2247
+ const from = fromToken ?? src?.tokens.usdt0?.address;
2248
+ const to = toToken ?? dst?.tokens.usdt0?.address;
2249
+ if (!(src && from)) {
2250
+ throw new StableValidationError("fromChain", fromChain, `estimateBridgeTime: no OFT adapter for "${fromChain}"`);
2251
+ }
2252
+ if (!(dst && to)) {
2253
+ throw new StableValidationError("toChain", toChain, `estimateBridgeTime: no OFT adapter for "${toChain}"`);
2254
+ }
2255
+ const { srcToken, dstToken } = this.resolveRoute(fromChain, toChain, from, to);
2256
+ const [uln, observedSeconds] = await Promise.all([
2257
+ this.getUlnConfig(src, dst, srcToken.oftAdapter).catch(() => void 0),
2258
+ this.observedBridgeSeconds(src, dst, srcToken.oftAdapter, dstToken.oftAdapter).catch(() => void 0)
2259
+ ]);
2260
+ const dvnCount = uln ? uln.requiredDVNCount + uln.optionalDVNCount : void 0;
2261
+ let formulaSeconds;
2262
+ if (uln && dvnCount !== void 0 && src.blockTimeSec && dst.blockTimeSec) {
2263
+ const computed = Math.round(
2264
+ uln.confirmations * src.blockTimeSec + dst.blockTimeSec * (2 + dvnCount)
2265
+ );
2266
+ formulaSeconds = Number.isFinite(computed) ? computed : void 0;
2267
+ }
2268
+ const seconds = observedSeconds ?? formulaSeconds;
2269
+ if (seconds === void 0) {
2270
+ throw new StableQuoteError({
2271
+ provider: "layerzero",
2272
+ shortMessage: `estimateBridgeTime: no pathway data for ${fromChain}\u2192${toChain}`
2273
+ });
2274
+ }
2275
+ return {
2276
+ seconds,
2277
+ source: observedSeconds !== void 0 ? "observed" : "formula",
2278
+ observedSeconds,
2279
+ formulaSeconds,
2280
+ confirmations: Number.isFinite(uln?.confirmations) ? uln?.confirmations : void 0,
2281
+ dvnCount: Number.isFinite(dvnCount) ? dvnCount : void 0
2282
+ };
2283
+ }
2284
+ /**
2285
+ * Follow an in-flight bridge by its source transaction hash. Poll this after
2286
+ * `bridge()` to drive a progress UI; a message LayerZero Scan has not indexed
2287
+ * yet reports `"pending"` rather than throwing.
2288
+ */
2289
+ async trackBridge(srcTxHash) {
2290
+ const scanUrl = `${LZ_SCAN_UI}/tx/${srcTxHash}`;
2291
+ let message;
2292
+ try {
2293
+ message = (await scanFetch(`/messages/tx/${srcTxHash}`))?.data?.[0];
2294
+ } catch (err) {
2295
+ if (err instanceof ScanHttpError && err.status === 404) {
2296
+ return { state: "pending", srcTxHash, scanUrl };
2297
+ }
2298
+ throw new StableQuoteError({
2299
+ provider: "layerzero",
2300
+ shortMessage: `trackBridge: LayerZero Scan lookup failed for ${srcTxHash}`,
2301
+ cause: err
2302
+ });
2303
+ }
2304
+ if (!message) return { state: "pending", srcTxHash, scanUrl };
2305
+ const raw = message.status?.name ?? "";
2306
+ const sentAt = message.source?.tx?.blockTimestamp;
2307
+ return {
2308
+ // Everything terminal that is not DELIVERED means the tokens will not land
2309
+ // on their own; `raw` carries which of those it was.
2310
+ state: raw === "DELIVERED" ? "delivered" : TERMINAL_FAILURE_STATUSES.has(raw) ? "failed" : "inflight",
2311
+ raw,
2312
+ srcTxHash,
2313
+ dstTxHash: message.destination?.tx?.txHash,
2314
+ elapsedSeconds: sentAt ? Math.max(0, Math.floor(Date.now() / 1e3) - sentAt) : void 0,
2315
+ scanUrl
2316
+ };
2317
+ }
2318
+ /** Effective ULN config the DVNs enforce for this pathway, read from the source chain. */
2319
+ async getUlnConfig(src, dst, oapp) {
2320
+ return cached(`uln:${src.lzEid}-${dst.lzEid}-${oapp.toLowerCase()}`, CACHE_TTL_MS, async () => {
2321
+ const pc = createPublicClient5({ chain: makeChain(src.id, src.rpc), transport: http5(src.rpc) });
2322
+ const endpoint = await pc.readContract({ address: oapp, abi: OAPP_ABI, functionName: "endpoint" });
2323
+ const sendLib = await pc.readContract({
2324
+ address: endpoint,
2325
+ abi: ENDPOINT_V2_ABI,
2326
+ functionName: "getSendLibrary",
2327
+ args: [oapp, dst.lzEid]
2328
+ });
2329
+ const cfg = await pc.readContract({
2330
+ address: sendLib,
2331
+ abi: SEND_ULN_ABI,
2332
+ functionName: "getUlnConfig",
2333
+ args: [oapp, dst.lzEid]
2334
+ });
2335
+ return {
2336
+ confirmations: Number(cfg.confirmations),
2337
+ requiredDVNCount: Number(cfg.requiredDVNCount),
2338
+ optionalDVNCount: Number(cfg.optionalDVNCount)
2339
+ };
2340
+ });
2341
+ }
2342
+ /** Median delivery time of recently delivered messages on this pathway, in seconds. */
2343
+ async observedBridgeSeconds(src, dst, srcOapp, dstOapp) {
2344
+ const pathwayId = `${src.lzEid}-${dst.lzEid}-${srcOapp.toLowerCase()}-${dstOapp.toLowerCase()}`;
2345
+ return cached(`observed:${pathwayId}`, CACHE_TTL_MS, async () => {
2346
+ const { data } = await scanFetch(`/messages/pathway/${pathwayId}?limit=50`);
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);
2348
+ return durations.length >= 3 ? Math.round(median(durations)) : void 0;
2349
+ });
2350
+ }
1947
2351
  validateBridgeParams(params, method) {
1948
2352
  if (!isAddress4(params.recipient)) {
1949
2353
  throw new StableValidationError(
@@ -1967,37 +2371,95 @@ var LayerZeroService = class {
1967
2371
  );
1968
2372
  }
1969
2373
  }
1970
- 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) {
1971
2382
  const src = BRIDGE_CHAIN_CONFIGS[fromChain];
1972
2383
  const dst = BRIDGE_CHAIN_CONFIGS[toChain];
1973
2384
  if (!src) throw new StableValidationError("fromChain", fromChain, `Unsupported source chain: "${fromChain}"`);
1974
2385
  if (!dst) throw new StableValidationError("toChain", toChain, `Unsupported destination chain: "${toChain}"`);
1975
- if (!src.oftAdapter) {
1976
- throw new StableValidationError("fromChain", fromChain, `No OFT adapter configured for "${fromChain}"`);
1977
- }
1978
- if (fromToken.toLowerCase() !== src.usdt.toLowerCase()) {
2386
+ const asset = bridgeAssetFor(src, fromToken);
2387
+ if (!asset) {
1979
2388
  throw new StableValidationError(
1980
2389
  "fromToken",
1981
2390
  fromToken,
1982
- `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.`
1983
2392
  );
1984
2393
  }
1985
- 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()) {
1986
2404
  throw new StableValidationError(
1987
2405
  "toToken",
1988
2406
  toToken,
1989
- `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.`
1990
2408
  );
1991
2409
  }
1992
- return { src, dst };
2410
+ return { src, dst, asset, srcToken, dstToken };
2411
+ }
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;
1993
2451
  }
1994
- buildSendParams(dst, amountLD, recipient) {
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) {
1995
2457
  return {
1996
- dstEid: dst.lzEid,
1997
- to: padHex(recipient, { dir: "left", size: 32 }),
1998
- amountLD,
1999
- minAmountLD: amountLD,
2000
- 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),
2001
2463
  composeMsg: "0x",
2002
2464
  oftCmd: "0x"
2003
2465
  };
@@ -2005,21 +2467,37 @@ var LayerZeroService = class {
2005
2467
  };
2006
2468
 
2007
2469
  // src/stable.ts
2008
- function isUsdt0Route(fromChain, toChain, fromToken, toToken) {
2470
+ function isNativeOftRoute(fromChain, toChain, fromToken, toToken) {
2009
2471
  const src = BRIDGE_CHAIN_CONFIGS[fromChain];
2010
2472
  const dst = BRIDGE_CHAIN_CONFIGS[toChain];
2011
- 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();
2012
2479
  }
2013
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) {
2014
2492
  const route = params.route ?? "auto";
2015
- if (route === "usdt0") return "usdt0";
2493
+ if (route === "oft" || route === "usdt0") return "oft";
2016
2494
  if (route === "lifi") return "lifi";
2017
- return isUsdt0Route(
2495
+ return isNativeOftRoute(
2018
2496
  params.fromChain,
2019
2497
  params.toChain,
2020
2498
  params.fromToken,
2021
2499
  params.toToken
2022
- ) ? "usdt0" : "lifi";
2500
+ ) ? "oft" : "lifi";
2023
2501
  }
2024
2502
  function validateTransferParams({
2025
2503
  to,
@@ -2119,6 +2597,12 @@ function createStable({
2119
2597
  chainId: chain.id,
2120
2598
  vault: earnConfig.vault
2121
2599
  }) : null;
2600
+ const erc3009Client = walletClient ? walletClient : account ? createWalletClient5({
2601
+ account,
2602
+ chain,
2603
+ transport: transport ?? http6(rpc)
2604
+ }) : createPublicClient6({ chain, transport: http6(rpc) });
2605
+ const authorization = erc3009Actions(erc3009Client);
2122
2606
  function requireEarn() {
2123
2607
  if (!morpho) {
2124
2608
  throw new StableValidationError(
@@ -2195,10 +2679,15 @@ function createStable({
2195
2679
  }
2196
2680
  return {
2197
2681
  transfer,
2198
- quoteBridge: (params) => selectBridge(params) === "usdt0" ? lz.quoteBridge(params) : lifi.quoteBridge(params),
2199
- 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),
2686
+ estimateBridgeTime: (params) => lz.estimateBridgeTime(params),
2687
+ trackBridge: (srcTxHash) => lz.trackBridge(srcTxHash),
2200
2688
  quoteSwap: (params) => lifi.quoteSwap(params),
2201
2689
  swap: (params) => lifi.swap(params),
2690
+ authorization,
2202
2691
  earn: {
2203
2692
  deposit: (params) => requireEarn().deposit(params),
2204
2693
  bulkDeposit: (items, options) => requireEarn().bulkDeposit(items, options),
@@ -2234,14 +2723,11 @@ function createStableReader(config) {
2234
2723
  }
2235
2724
  };
2236
2725
  }
2237
-
2238
- // src/constants.ts
2239
- var STABLE_USDT_ADDRESS = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
2240
- var STABLE_VAULT_ADDRESS = "0xb7Df8db22A5DBBFA9ebeb94b3910aec6a4f05c08";
2241
2726
  export {
2242
2727
  BRIDGE_CHAIN_CONFIGS,
2243
2728
  CHAIN_CONFIGS,
2244
2729
  Chain,
2730
+ ERC3009_ABI,
2245
2731
  Network,
2246
2732
  STABLE_USDT_ADDRESS,
2247
2733
  STABLE_VAULT_ADDRESS,
@@ -2251,6 +2737,18 @@ export {
2251
2737
  StableQuoteError,
2252
2738
  StableTransactionError,
2253
2739
  StableValidationError,
2740
+ bridgeAssetFor,
2741
+ cancelAuthorization,
2742
+ clearBridgeEstimateCache,
2254
2743
  createStable,
2255
- createStableReader
2744
+ createStableReader,
2745
+ erc3009Actions,
2746
+ getAuthorizationState,
2747
+ getAuthorizationUsage,
2748
+ prepareTransferWithAuthorization,
2749
+ receiveWithAuthorization,
2750
+ signReceiveAuthorization,
2751
+ signTransferAuthorization,
2752
+ transferWithAuthorization,
2753
+ verifyAuthorization
2256
2754
  };