@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/README.md +107 -4
- package/dist/index.cjs +277 -78
- package/dist/index.d.cts +288 -4
- package/dist/index.d.ts +288 -4
- package/dist/index.js +257 -58
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1730,6 +1730,7 @@ import {
|
|
|
1730
1730
|
concat,
|
|
1731
1731
|
createPublicClient as createPublicClient5,
|
|
1732
1732
|
createWalletClient as createWalletClient4,
|
|
1733
|
+
encodeFunctionData as encodeFunctionData2,
|
|
1733
1734
|
erc20Abi as erc20Abi4,
|
|
1734
1735
|
http as http5,
|
|
1735
1736
|
isAddress as isAddress4,
|
|
@@ -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 = {
|
|
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 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 (!quote?.oft) return quote;
|
|
2003
2026
|
return quote.oft.amountSentLD <= amountLD ? quote : void 0;
|
|
@@ -2006,16 +2029,42 @@ 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(config.rpcOverrides ?? {}).flatMap(([chain, rpc]) => {
|
|
2034
|
+
const id = CHAIN_CONFIGS[chain]?.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 this.rpcById.get(cfg.id) ?? cfg.rpc;
|
|
2009
2046
|
}
|
|
2010
2047
|
async quoteBridge(params) {
|
|
2011
|
-
|
|
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,
|
|
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.
|
|
@@ -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
|
|
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 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.
|
|
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
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
};
|
|
2080
|
-
} catch {
|
|
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(formatUnits5(receipt.amountReceivedLD, decimals)),
|
|
2133
|
+
fees: feeDetails.map((detail) => ({
|
|
2134
|
+
amountLD: detail.feeAmountLD,
|
|
2135
|
+
amount: Number(formatUnits5(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,13 +2147,17 @@ 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 = 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,
|
|
@@ -2103,11 +2165,11 @@ var LayerZeroService = class {
|
|
|
2103
2165
|
functionName: "send",
|
|
2104
2166
|
args: [
|
|
2105
2167
|
this.buildSendParams(plan, oft?.amountReceivedLD ?? plan.amountLD),
|
|
2106
|
-
|
|
2168
|
+
sendFee,
|
|
2107
2169
|
account
|
|
2108
2170
|
],
|
|
2109
2171
|
account,
|
|
2110
|
-
value:
|
|
2172
|
+
value: sendFee.nativeFee
|
|
2111
2173
|
});
|
|
2112
2174
|
source = "simulated";
|
|
2113
2175
|
} catch {
|
|
@@ -2130,32 +2192,49 @@ var LayerZeroService = class {
|
|
|
2130
2192
|
}
|
|
2131
2193
|
}
|
|
2132
2194
|
/**
|
|
2133
|
-
*
|
|
2134
|
-
*
|
|
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
|
|
2137
|
-
|
|
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: erc20Abi4,
|
|
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: erc20Abi4,
|
|
2150
2229
|
functionName: "approve",
|
|
2151
|
-
args: [oftAdapter,
|
|
2230
|
+
args: [oftAdapter, value],
|
|
2152
2231
|
account: wc.account,
|
|
2153
2232
|
chain: srcChain
|
|
2154
2233
|
});
|
|
2155
2234
|
} catch (err) {
|
|
2156
2235
|
throw new StableTransactionError({
|
|
2157
2236
|
phase: "approve",
|
|
2158
|
-
shortMessage: `bridge: ${asset}
|
|
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 StableTransactionError({
|
|
2166
2245
|
phase: "approve",
|
|
2167
|
-
shortMessage: `bridge: ${asset}
|
|
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: params.recipient ?? from }, "bridge");
|
|
2183
|
-
const { src,
|
|
2262
|
+
const { src, oftAdapter, amountLD, chain: srcChain } = plan;
|
|
2263
|
+
const refundAddress = this.resolveRefundAddress(params.refundAddress, from, "bridge");
|
|
2184
2264
|
const wc = this.config.walletClient ?? createWalletClient4({
|
|
2185
2265
|
account: this.config.account,
|
|
2186
2266
|
chain: srcChain,
|
|
2187
|
-
|
|
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 ? http5(plan.rpc) : this.config.transport ?? http5(plan.rpc)
|
|
2188
2274
|
});
|
|
2189
2275
|
try {
|
|
2190
2276
|
await wc.switchChain({ id: src.id });
|
|
@@ -2197,18 +2283,19 @@ var LayerZeroService = class {
|
|
|
2197
2283
|
});
|
|
2198
2284
|
}
|
|
2199
2285
|
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 }));
|
|
2286
|
+
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", true).then((r) => ({ ...r.fee, oft: r.oft }));
|
|
2201
2287
|
const sendParams = this.buildSendParams(plan, oft?.amountReceivedLD ?? amountLD);
|
|
2202
|
-
|
|
2203
|
-
|
|
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 },
|
|
2211
|
-
value:
|
|
2297
|
+
args: [sendParams, { nativeFee: sendFee, lzTokenFee }, refundAddress],
|
|
2298
|
+
value: sendFee,
|
|
2212
2299
|
account: wc.account,
|
|
2213
2300
|
chain: srcChain
|
|
2214
2301
|
});
|
|
@@ -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 (!isAddress4(from)) {
|
|
2331
|
+
throw new 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: params.recipient ?? from }, method);
|
|
2339
|
+
const supplied = usableQuote(params.quote, plan.amountLD);
|
|
2340
|
+
const quote = supplied?.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: encodeFunctionData2({
|
|
2349
|
+
abi: erc20Abi4,
|
|
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: encodeFunctionData2({
|
|
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
|
*
|
|
@@ -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
|
|
2479
|
+
const rpc = this.rpcFor(src);
|
|
2480
|
+
const pc = createPublicClient5({ chain: makeChain(src.id, rpc), transport: http5(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,
|
|
@@ -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
|
-
|
|
2593
|
+
if (params.fromDecimals !== void 0 && params.fromDecimals !== srcToken.decimals) {
|
|
2594
|
+
throw new 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,10 +2609,28 @@ var LayerZeroService = class {
|
|
|
2441
2609
|
amountLD: parseUnits4(String(amount), srcToken.decimals),
|
|
2442
2610
|
recipient: recipientAddr,
|
|
2443
2611
|
nativeDrop: dstNativeDrop ?? 0n,
|
|
2612
|
+
rpc,
|
|
2613
|
+
feeBufferBps,
|
|
2444
2614
|
chain,
|
|
2445
|
-
pc: createPublicClient5({ chain, transport: http5(
|
|
2615
|
+
pc: createPublicClient5({ chain, transport: http5(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 (!isAddress4(refundAddress)) {
|
|
2626
|
+
throw new 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
2636
|
return this.config.walletClient?.account?.address ?? this.config.account?.address;
|
|
@@ -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,
|
|
@@ -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 StableValidationError(
|
|
2876
|
+
"route",
|
|
2877
|
+
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),
|
|
@@ -2729,6 +2926,7 @@ export {
|
|
|
2729
2926
|
Chain,
|
|
2730
2927
|
ERC3009_ABI,
|
|
2731
2928
|
Network,
|
|
2929
|
+
OFT_ADAPTER_ABI,
|
|
2732
2930
|
STABLE_USDT_ADDRESS,
|
|
2733
2931
|
STABLE_VAULT_ADDRESS,
|
|
2734
2932
|
StableBulkError,
|
|
@@ -2738,6 +2936,7 @@ export {
|
|
|
2738
2936
|
StableTransactionError,
|
|
2739
2937
|
StableValidationError,
|
|
2740
2938
|
bridgeAssetFor,
|
|
2939
|
+
buildLzOptions,
|
|
2741
2940
|
cancelAuthorization,
|
|
2742
2941
|
clearBridgeEstimateCache,
|
|
2743
2942
|
createStable,
|