@stablechain/sdk 1.0.6 → 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
@@ -1738,6 +1738,7 @@ function isLiFiQuoteShape(v) {
1738
1738
 
1739
1739
 
1740
1740
 
1741
+
1741
1742
  var C = CHAIN_CONFIGS;
1742
1743
  var XAUT0_DEPLOYMENTS = {
1743
1744
  ["ethereum" /* Ethereum */]: { address: "0x68749665FF8D2d112Fa859AA293F07A622782F38", decimals: 6, oftAdapter: "0xb9c2321BB7D0Db468f570D10A424d1Cc8EFd696C", approvalRequired: true },
@@ -1749,11 +1750,17 @@ var XAUT0_DEPLOYMENTS = {
1749
1750
  ["plasma" /* Plasma */]: { address: "0x1B64B9025EEbb9A6239575dF9Ea4b9Ac46D4d193", decimals: 6, oftAdapter: "0x63aB93cBC9d4ecD9c4947b1A38F458147C08E6F7", approvalRequired: false },
1750
1751
  ["bsc" /* BSC */]: { address: "0x21cAef8A43163Eea865baeE23b9C2E327696A3bf", decimals: 6, oftAdapter: "0x53C3A64c8942288e12813C1F8457Db45980BcfC2", approvalRequired: false }
1751
1752
  };
1753
+ var USDT0_LOCKBOX_CHAINS = /* @__PURE__ */ new Set(["ethereum" /* Ethereum */, "sepolia" /* Sepolia */]);
1752
1754
  function bridgeChain(chain, lzEid, blockTimeSec, usdt0Adapter) {
1753
1755
  const base = C[chain];
1754
1756
  const tokens = {};
1755
1757
  if (usdt0Adapter) {
1756
- tokens.usdt0 = { address: base.usdt, decimals: base.decimals, oftAdapter: usdt0Adapter };
1758
+ tokens.usdt0 = {
1759
+ address: base.usdt,
1760
+ decimals: base.decimals,
1761
+ oftAdapter: usdt0Adapter,
1762
+ approvalRequired: USDT0_LOCKBOX_CHAINS.has(chain)
1763
+ };
1757
1764
  }
1758
1765
  const xaut0 = XAUT0_DEPLOYMENTS[chain];
1759
1766
  if (xaut0) tokens.xaut0 = xaut0;
@@ -1998,6 +2005,22 @@ function buildLzOptions(gasLimit = 0n, nativeDrop = 0n, dropReceiver) {
1998
2005
  ]);
1999
2006
  }
2000
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
+ }
2001
2024
  function usableQuote(quote, amountLD) {
2002
2025
  if (!_optionalChain([quote, 'optionalAccess', _41 => _41.oft])) return quote;
2003
2026
  return quote.oft.amountSentLD <= amountLD ? quote : void 0;
@@ -2006,20 +2029,46 @@ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
2006
2029
  var LayerZeroService = class {
2007
2030
  constructor(config) {
2008
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));
2009
2046
  }
2010
2047
  async quoteBridge(params) {
2011
- const plan = this.plan(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 = {}) {
2012
2059
  const [{ fee, oft }, estimate] = await Promise.all([
2013
- this.quotePathway(plan, "quoteBridge"),
2060
+ this.quotePathway(plan, method, opts.requireOft),
2014
2061
  this.estimateBridgeTime(params).catch(() => void 0)
2015
2062
  ]);
2016
- const srcGas = await this.estimateSendGas(plan, fee, oft);
2063
+ const srcGas = await this.estimateSendGas(plan, fee, oft, opts.account);
2017
2064
  return {
2018
2065
  ...fee,
2066
+ // What a send built from this quote will actually carry as its `value`.
2067
+ nativeFeeMax: padFee(fee.nativeFee, plan.feeBufferBps),
2019
2068
  oft,
2020
2069
  // What the recipient is actually credited: input minus any OFT fee and
2021
2070
  // minus dust. Falls back to the input when quoteOFT could not be read.
2022
- toAmount: _nullishCoalesce(_optionalChain([oft, 'optionalAccess', _42 => _42.amountReceived]), () => ( params.amount)),
2071
+ toAmount: _nullishCoalesce(_optionalChain([oft, 'optionalAccess', _44 => _44.amountReceived]), () => ( params.amount)),
2023
2072
  srcGas,
2024
2073
  totalCostWei: srcGas ? srcGas.costWei + fee.nativeFee : void 0,
2025
2074
  estimate
@@ -2029,10 +2078,15 @@ var LayerZeroService = class {
2029
2078
  * The LayerZero messaging fee for a send, plus the OFT's own accounting.
2030
2079
  *
2031
2080
  * 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.
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.
2034
2088
  */
2035
- async quotePathway(plan, method) {
2089
+ async quotePathway(plan, method, requireOft = false) {
2036
2090
  const sendParam = this.buildSendParams(plan, 0n);
2037
2091
  const [msgFee, oft] = await Promise.all([
2038
2092
  plan.pc.readContract({
@@ -2047,39 +2101,43 @@ var LayerZeroService = class {
2047
2101
  cause: err
2048
2102
  });
2049
2103
  }),
2050
- this.readOftQuote(plan, sendParam)
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
+ })
2051
2112
  ]);
2052
2113
  const { nativeFee, lzTokenFee } = msgFee;
2053
2114
  return { fee: { nativeFee, lzTokenFee }, oft };
2054
2115
  }
2055
2116
  /**
2056
2117
  * 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`.
2118
+ * the far side. Throws when the adapter will not answer; `quotePathway` decides
2119
+ * whether that is fatal for the caller it is serving.
2058
2120
  */
2059
2121
  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(_viem.formatUnits.call(void 0, receipt.amountReceivedLD, decimals)),
2072
- fees: feeDetails.map((detail) => ({
2073
- amountLD: detail.feeAmountLD,
2074
- amount: Number(_viem.formatUnits.call(void 0, detail.feeAmountLD, decimals)),
2075
- description: detail.description
2076
- })),
2077
- minAmountLD: limit.minAmountLD,
2078
- maxAmountLD: limit.maxAmountLD
2079
- };
2080
- } catch (e5) {
2081
- return void 0;
2082
- }
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
+ };
2083
2141
  }
2084
2142
  /**
2085
2143
  * Source-chain cost of the `send` transaction.
@@ -2089,28 +2147,32 @@ var LayerZeroService = class {
2089
2147
  * before approving does not have. When the simulation reverts, fall back to a
2090
2148
  * measured constant so the caller still has a cost to show, flagged as such.
2091
2149
  */
2092
- async estimateSendGas(plan, fee, oft) {
2150
+ async estimateSendGas(plan, fee, oft, sender) {
2093
2151
  const gasPriceWei = await this.gasPrice(plan);
2094
2152
  if (gasPriceWei === void 0) return void 0;
2095
2153
  let gas = FALLBACK_SEND_GAS;
2096
2154
  let source = "fallback";
2097
- const account = this.signerAddress();
2155
+ const account = _nullishCoalesce(sender, () => ( this.signerAddress()));
2098
2156
  if (account) {
2157
+ const sendFee = {
2158
+ nativeFee: padFee(fee.nativeFee, plan.feeBufferBps),
2159
+ lzTokenFee: fee.lzTokenFee
2160
+ };
2099
2161
  try {
2100
2162
  gas = await plan.pc.estimateContractGas({
2101
2163
  address: plan.oftAdapter,
2102
2164
  abi: OFT_ADAPTER_ABI,
2103
2165
  functionName: "send",
2104
2166
  args: [
2105
- this.buildSendParams(plan, _nullishCoalesce(_optionalChain([oft, 'optionalAccess', _43 => _43.amountReceivedLD]), () => ( plan.amountLD))),
2106
- fee,
2167
+ this.buildSendParams(plan, _nullishCoalesce(_optionalChain([oft, 'optionalAccess', _45 => _45.amountReceivedLD]), () => ( plan.amountLD))),
2168
+ sendFee,
2107
2169
  account
2108
2170
  ],
2109
2171
  account,
2110
- value: fee.nativeFee
2172
+ value: sendFee.nativeFee
2111
2173
  });
2112
2174
  source = "simulated";
2113
- } catch (e6) {
2175
+ } catch (e5) {
2114
2176
  }
2115
2177
  }
2116
2178
  return { gas, gasPriceWei, costWei: gas * gasPriceWei, source };
@@ -2121,41 +2183,58 @@ var LayerZeroService = class {
2121
2183
  const fees = await plan.pc.estimateFeesPerGas();
2122
2184
  const price = _nullishCoalesce(fees.maxFeePerGas, () => ( fees.gasPrice));
2123
2185
  if (price !== void 0) return price;
2124
- } catch (e7) {
2186
+ } catch (e6) {
2125
2187
  }
2126
2188
  try {
2127
2189
  return await plan.pc.getGasPrice();
2128
- } catch (e8) {
2190
+ } catch (e7) {
2129
2191
  return void 0;
2130
2192
  }
2131
2193
  }
2132
2194
  /**
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.
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.
2135
2208
  */
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,
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,
2140
2213
  abi: _viem.erc20Abi,
2141
2214
  functionName: "allowance",
2142
- args: [owner, oftAdapter]
2215
+ args: [owner, plan.oftAdapter]
2143
2216
  });
2144
- if (allowance >= amountLD) return;
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";
2145
2224
  let approveHash;
2146
2225
  try {
2147
2226
  approveHash = await wc.writeContract({
2148
2227
  address: srcToken.address,
2149
2228
  abi: _viem.erc20Abi,
2150
2229
  functionName: "approve",
2151
- args: [oftAdapter, amountLD],
2230
+ args: [oftAdapter, value],
2152
2231
  account: wc.account,
2153
2232
  chain: srcChain
2154
2233
  });
2155
2234
  } catch (err) {
2156
2235
  throw new (0, _chunkQQ26UI3Fcjs.StableTransactionError)({
2157
2236
  phase: "approve",
2158
- shortMessage: `bridge: ${asset} approve failed`,
2237
+ shortMessage: `bridge: ${asset} ${what} failed`,
2159
2238
  chainId: src.id,
2160
2239
  cause: err
2161
2240
  });
@@ -2164,7 +2243,7 @@ var LayerZeroService = class {
2164
2243
  if (approveReceipt.status === "reverted") {
2165
2244
  throw new (0, _chunkQQ26UI3Fcjs.StableTransactionError)({
2166
2245
  phase: "approve",
2167
- shortMessage: `bridge: ${asset} approve reverted on-chain`,
2246
+ shortMessage: `bridge: ${asset} ${what} reverted on-chain`,
2168
2247
  txHash: approveReceipt.transactionHash,
2169
2248
  chainId: src.id
2170
2249
  });
@@ -2180,11 +2259,18 @@ var LayerZeroService = class {
2180
2259
  );
2181
2260
  }
2182
2261
  const plan = this.plan({ ...params, recipient: _nullishCoalesce(params.recipient, () => ( from)) }, "bridge");
2183
- const { src, srcToken, oftAdapter, amountLD, chain: srcChain } = plan;
2262
+ const { src, oftAdapter, amountLD, chain: srcChain } = plan;
2263
+ const refundAddress = this.resolveRefundAddress(params.refundAddress, from, "bridge");
2184
2264
  const wc = _nullishCoalesce(this.config.walletClient, () => ( _viem.createWalletClient.call(void 0, {
2185
2265
  account: this.config.account,
2186
2266
  chain: srcChain,
2187
- 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)))
2188
2274
  })));
2189
2275
  try {
2190
2276
  await wc.switchChain({ id: src.id });
@@ -2197,22 +2283,23 @@ var LayerZeroService = class {
2197
2283
  });
2198
2284
  }
2199
2285
  const quote = usableQuote(params.quote, amountLD);
2200
- const { nativeFee, lzTokenFee, oft } = _optionalChain([quote, 'optionalAccess', _44 => _44.nativeFee]) !== void 0 && _optionalChain([quote, 'optionalAccess', _45 => _45.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, _nullishCoalesce(_optionalChain([oft, 'optionalAccess', _46 => _46.amountReceivedLD]), () => ( amountLD)));
2202
- if (srcToken.approvalRequired !== false) {
2203
- await this.ensureAllowance(plan, wc, from, 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);
2204
2291
  }
2205
2292
  try {
2206
2293
  const hash = await wc.writeContract({
2207
2294
  address: oftAdapter,
2208
2295
  abi: OFT_ADAPTER_ABI,
2209
2296
  functionName: "send",
2210
- args: [sendParams, { nativeFee, lzTokenFee }, from],
2211
- value: nativeFee,
2297
+ args: [sendParams, { nativeFee: sendFee, lzTokenFee }, refundAddress],
2298
+ value: sendFee,
2212
2299
  account: wc.account,
2213
2300
  chain: srcChain
2214
2301
  });
2215
- return { txHash: hash, toAmount: _nullishCoalesce(_optionalChain([oft, 'optionalAccess', _47 => _47.amountReceived]), () => ( params.amount)) };
2302
+ return { txHash: hash, toAmount: _nullishCoalesce(_optionalChain([oft, 'optionalAccess', _49 => _49.amountReceived]), () => ( params.amount)) };
2216
2303
  } catch (err) {
2217
2304
  throw new (0, _chunkQQ26UI3Fcjs.StableTransactionError)({
2218
2305
  phase: "send",
@@ -2222,6 +2309,77 @@ var LayerZeroService = class {
2222
2309
  });
2223
2310
  }
2224
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
+ }
2225
2383
  /**
2226
2384
  * Estimate how long a USDT0 bridge will take, end to end.
2227
2385
  *
@@ -2244,8 +2402,8 @@ var LayerZeroService = class {
2244
2402
  }) {
2245
2403
  const src = BRIDGE_CHAIN_CONFIGS[fromChain];
2246
2404
  const dst = BRIDGE_CHAIN_CONFIGS[toChain];
2247
- const from = _nullishCoalesce(fromToken, () => ( _optionalChain([src, 'optionalAccess', _48 => _48.tokens, 'access', _49 => _49.usdt0, 'optionalAccess', _50 => _50.address])));
2248
- const to = _nullishCoalesce(toToken, () => ( _optionalChain([dst, 'optionalAccess', _51 => _51.tokens, 'access', _52 => _52.usdt0, 'optionalAccess', _53 => _53.address])));
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])));
2249
2407
  if (!(src && from)) {
2250
2408
  throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)("fromChain", fromChain, `estimateBridgeTime: no OFT adapter for "${fromChain}"`);
2251
2409
  }
@@ -2277,7 +2435,7 @@ var LayerZeroService = class {
2277
2435
  source: observedSeconds !== void 0 ? "observed" : "formula",
2278
2436
  observedSeconds,
2279
2437
  formulaSeconds,
2280
- confirmations: Number.isFinite(_optionalChain([uln, 'optionalAccess', _54 => _54.confirmations])) ? _optionalChain([uln, 'optionalAccess', _55 => _55.confirmations]) : void 0,
2438
+ confirmations: Number.isFinite(_optionalChain([uln, 'optionalAccess', _57 => _57.confirmations])) ? _optionalChain([uln, 'optionalAccess', _58 => _58.confirmations]) : void 0,
2281
2439
  dvnCount: Number.isFinite(dvnCount) ? dvnCount : void 0
2282
2440
  };
2283
2441
  }
@@ -2290,7 +2448,7 @@ var LayerZeroService = class {
2290
2448
  const scanUrl = `${LZ_SCAN_UI}/tx/${srcTxHash}`;
2291
2449
  let message;
2292
2450
  try {
2293
- message = await _asyncOptionalChain([(await scanFetch(`/messages/tx/${srcTxHash}`)), 'optionalAccess', async _56 => _56.data, 'optionalAccess', async _57 => _57[0]]);
2451
+ message = await _asyncOptionalChain([(await scanFetch(`/messages/tx/${srcTxHash}`)), 'optionalAccess', async _59 => _59.data, 'optionalAccess', async _60 => _60[0]]);
2294
2452
  } catch (err) {
2295
2453
  if (err instanceof ScanHttpError && err.status === 404) {
2296
2454
  return { state: "pending", srcTxHash, scanUrl };
@@ -2302,15 +2460,15 @@ var LayerZeroService = class {
2302
2460
  });
2303
2461
  }
2304
2462
  if (!message) return { state: "pending", srcTxHash, scanUrl };
2305
- const raw = _nullishCoalesce(_optionalChain([message, 'access', _58 => _58.status, 'optionalAccess', _59 => _59.name]), () => ( ""));
2306
- const sentAt = _optionalChain([message, 'access', _60 => _60.source, 'optionalAccess', _61 => _61.tx, 'optionalAccess', _62 => _62.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]);
2307
2465
  return {
2308
2466
  // Everything terminal that is not DELIVERED means the tokens will not land
2309
2467
  // on their own; `raw` carries which of those it was.
2310
2468
  state: raw === "DELIVERED" ? "delivered" : TERMINAL_FAILURE_STATUSES.has(raw) ? "failed" : "inflight",
2311
2469
  raw,
2312
2470
  srcTxHash,
2313
- dstTxHash: _optionalChain([message, 'access', _63 => _63.destination, 'optionalAccess', _64 => _64.tx, 'optionalAccess', _65 => _65.txHash]),
2471
+ dstTxHash: _optionalChain([message, 'access', _66 => _66.destination, 'optionalAccess', _67 => _67.tx, 'optionalAccess', _68 => _68.txHash]),
2314
2472
  elapsedSeconds: sentAt ? Math.max(0, Math.floor(Date.now() / 1e3) - sentAt) : void 0,
2315
2473
  scanUrl
2316
2474
  };
@@ -2318,7 +2476,8 @@ var LayerZeroService = class {
2318
2476
  /** Effective ULN config the DVNs enforce for this pathway, read from the source chain. */
2319
2477
  async getUlnConfig(src, dst, oapp) {
2320
2478
  return cached(`uln:${src.lzEid}-${dst.lzEid}-${oapp.toLowerCase()}`, CACHE_TTL_MS, async () => {
2321
- const pc = _viem.createPublicClient.call(void 0, { chain: makeChain(src.id, src.rpc), transport: _viem.http.call(void 0, src.rpc) });
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) });
2322
2481
  const endpoint = await pc.readContract({ address: oapp, abi: OAPP_ABI, functionName: "endpoint" });
2323
2482
  const sendLib = await pc.readContract({
2324
2483
  address: endpoint,
@@ -2344,7 +2503,7 @@ var LayerZeroService = class {
2344
2503
  const pathwayId = `${src.lzEid}-${dst.lzEid}-${srcOapp.toLowerCase()}-${dstOapp.toLowerCase()}`;
2345
2504
  return cached(`observed:${pathwayId}`, CACHE_TTL_MS, async () => {
2346
2505
  const { data } = await scanFetch(`/messages/pathway/${pathwayId}?limit=50`);
2347
- const durations = (_nullishCoalesce(data, () => ( []))).filter((m) => _optionalChain([m, 'access', _66 => _66.status, 'optionalAccess', _67 => _67.name]) === "DELIVERED").map((m) => (_nullishCoalesce(_optionalChain([m, 'access', _68 => _68.destination, 'optionalAccess', _69 => _69.tx, 'optionalAccess', _70 => _70.blockTimestamp]), () => ( 0))) - (_nullishCoalesce(_optionalChain([m, 'access', _71 => _71.source, 'optionalAccess', _72 => _72.tx, 'optionalAccess', _73 => _73.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);
2348
2507
  return durations.length >= 3 ? Math.round(median(durations)) : void 0;
2349
2508
  });
2350
2509
  }
@@ -2424,13 +2583,22 @@ var LayerZeroService = class {
2424
2583
  `${method}: dstNativeDrop must not be negative`
2425
2584
  );
2426
2585
  }
2586
+ const feeBufferBps = resolveFeeBuffer(params.feeBufferBps, method);
2427
2587
  const { src, dst, asset, srcToken, dstToken } = this.resolveRoute(
2428
2588
  fromChain,
2429
2589
  toChain,
2430
2590
  fromToken,
2431
2591
  toToken
2432
2592
  );
2433
- const chain = makeChain(src.id, src.rpc);
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);
2434
2602
  return {
2435
2603
  src,
2436
2604
  dst,
@@ -2441,13 +2609,31 @@ var LayerZeroService = class {
2441
2609
  amountLD: _viem.parseUnits.call(void 0, String(amount), srcToken.decimals),
2442
2610
  recipient: recipientAddr,
2443
2611
  nativeDrop: _nullishCoalesce(dstNativeDrop, () => ( 0n)),
2612
+ rpc,
2613
+ feeBufferBps,
2444
2614
  chain,
2445
- pc: _viem.createPublicClient.call(void 0, { chain, transport: _viem.http.call(void 0, src.rpc) })
2615
+ pc: _viem.createPublicClient.call(void 0, { chain, transport: _viem.http.call(void 0, rpc) })
2446
2616
  };
2447
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
+ }
2448
2634
  /** The address that will sign, or undefined when this service has no signer. */
2449
2635
  signerAddress() {
2450
- return _nullishCoalesce(_optionalChain([this, 'access', _74 => _74.config, 'access', _75 => _75.walletClient, 'optionalAccess', _76 => _76.account, 'optionalAccess', _77 => _77.address]), () => ( _optionalChain([this, 'access', _78 => _78.config, 'access', _79 => _79.account, 'optionalAccess', _80 => _80.address])));
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])));
2451
2637
  }
2452
2638
  /**
2453
2639
  * `SendParam` for one call. `minAmountLD` is the caller's slippage floor: 0 when
@@ -2542,7 +2728,7 @@ async function resolveTokenDecimals(publicClient, token, tokenDecimals) {
2542
2728
  abi: _viem.erc20Abi,
2543
2729
  functionName: "decimals"
2544
2730
  });
2545
- } catch (e9) {
2731
+ } catch (e8) {
2546
2732
  throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
2547
2733
  "tokenDecimals",
2548
2734
  void 0,
@@ -2576,6 +2762,7 @@ async function executeTokenTransfer(wc, publicClient, token, to, amount, tokenDe
2576
2762
  function createStable({
2577
2763
  network = "mainnet" /* Mainnet */,
2578
2764
  rpc: rpcOverride,
2765
+ rpcOverrides,
2579
2766
  account,
2580
2767
  transport,
2581
2768
  walletClient,
@@ -2589,7 +2776,7 @@ function createStable({
2589
2776
  );
2590
2777
  const signerConfig = { account, transport, walletClient };
2591
2778
  const lifi = new LiFiService({ ...signerConfig, rpc, chainId: chain.id });
2592
- const lz = new LayerZeroService(signerConfig);
2779
+ const lz = new LayerZeroService({ ...signerConfig, rpcOverrides });
2593
2780
  const merkl = new MerklService({ ...signerConfig, rpc, chainId: chain.id, merklApiBase });
2594
2781
  const morpho = earnConfig ? new MorphoService({
2595
2782
  ...signerConfig,
@@ -2614,7 +2801,7 @@ function createStable({
2614
2801
  return morpho;
2615
2802
  }
2616
2803
  function requireSignerAddress() {
2617
- const from = _nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _81 => _81.account, 'optionalAccess', _82 => _82.address]), () => ( _optionalChain([account, 'optionalAccess', _83 => _83.address])));
2804
+ const from = _nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _84 => _84.account, 'optionalAccess', _85 => _85.address]), () => ( _optionalChain([account, 'optionalAccess', _86 => _86.address])));
2618
2805
  if (!from) {
2619
2806
  throw new (0, _chunkQQ26UI3Fcjs.StableValidationError)(
2620
2807
  "signer",
@@ -2627,7 +2814,7 @@ function createStable({
2627
2814
  async function transfer(params) {
2628
2815
  validateTransferParams(params);
2629
2816
  const { from, to, amount, token, tokenDecimals } = params;
2630
- const acc = _nullishCoalesce(_nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _84 => _84.account]), () => ( account)), () => ( from));
2817
+ const acc = _nullishCoalesce(_nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _87 => _87.account]), () => ( account)), () => ( from));
2631
2818
  const wc = _nullishCoalesce(walletClient, () => ( _viem.createWalletClient.call(void 0, {
2632
2819
  account: acc,
2633
2820
  chain,
@@ -2683,6 +2870,16 @@ function createStable({
2683
2870
  // promise like every other validation error, not a synchronous throw.
2684
2871
  quoteBridge: async (params) => selectBridge(params) === "oft" ? lz.quoteBridge(params) : lifi.quoteBridge(params),
2685
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
+ },
2686
2883
  estimateBridgeTime: (params) => lz.estimateBridgeTime(params),
2687
2884
  trackBridge: (srcTxHash) => lz.trackBridge(srcTxHash),
2688
2885
  quoteSwap: (params) => lifi.quoteSwap(params),
@@ -2751,4 +2948,6 @@ function createStableReader(config) {
2751
2948
 
2752
2949
 
2753
2950
 
2754
- 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.bridgeAssetFor = bridgeAssetFor; 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;
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;