@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/README.md
CHANGED
|
@@ -64,6 +64,7 @@ const stable = createStable({
|
|
|
64
64
|
|-----------------|----------------|----------------------------------------------------------------------|
|
|
65
65
|
| `network` | `Network` | `Network.Mainnet` (default) or `Network.Testnet` |
|
|
66
66
|
| `rpc` | `string` | *(optional)* RPC override for the Stable chain |
|
|
67
|
+
| `rpcOverrides` | `Partial<Record<Chain, string>>` | *(optional)* Per-chain RPC endpoints for the bridge routes, e.g. `{ [Chain.Ethereum]: "https://..." }`. `rpc` covers the Stable chain only; a bridge reads and broadcasts on whichever chain it starts from, and defaults to this SDK's built-in public endpoint there |
|
|
67
68
|
| `account` | `Account` | *(optional)* viem `Account` for server-side signing |
|
|
68
69
|
| `transport` | `Transport` | *(optional)* viem `Transport` (e.g. `custom(window.ethereum)`) |
|
|
69
70
|
| `walletClient` | `WalletClient` | *(optional)* Pre-built viem `WalletClient` — takes precedence |
|
|
@@ -79,6 +80,7 @@ interface StableClient {
|
|
|
79
80
|
transfer(params: TransferParams): Promise<OperationResult>;
|
|
80
81
|
quoteBridge(params: BridgeParams): Promise<BridgeQuote>;
|
|
81
82
|
bridge(params: BridgeParams): Promise<OperationResult>;
|
|
83
|
+
prepareBridgeCalldata(params: PrepareBridgeParams): Promise<BridgeCalldata>;
|
|
82
84
|
estimateBridgeTime(params: Pick<BridgeParams, "fromChain" | "toChain"> & Partial<Pick<BridgeParams, "fromToken" | "toToken">>): Promise<BridgeTimeEstimate>;
|
|
83
85
|
trackBridge(srcTxHash: string): Promise<BridgeStatus>;
|
|
84
86
|
quoteSwap(params: SwapParams): Promise<SwapQuote>;
|
|
@@ -177,7 +179,8 @@ dust dropped by decimal conversion:
|
|
|
177
179
|
| Field | Type | What it is |
|
|
178
180
|
|----------------|---------------------|-------------------------------------------------------------------------------|
|
|
179
181
|
| `toAmount` | `number` | What the recipient receives, human units |
|
|
180
|
-
| `nativeFee` | `bigint` | LayerZero messaging fee (wei)
|
|
182
|
+
| `nativeFee` | `bigint` | LayerZero messaging fee (wei) as quoted — the number to show a user |
|
|
183
|
+
| `nativeFeeMax` | `bigint` | What a send built from this quote carries as its `value`: `nativeFee` padded by `feeBufferBps`. Check native balances against this |
|
|
181
184
|
| `lzTokenFee` | `bigint` | Same fee if paid in LZ token instead — `0n` on every route the SDK sends |
|
|
182
185
|
| `oft` | `BridgeOftQuote` | The adapter's own accounting: `amountSentLD`, `amountReceivedLD`, `fees[]`, and the pathway's `minAmountLD`/`maxAmountLD` |
|
|
183
186
|
| `srcGas` | `BridgeGasEstimate` | Source-chain execution cost: `gas`, `gasPriceWei`, `costWei`, `source` |
|
|
@@ -221,14 +224,41 @@ await stable.bridge({ ...params, dstNativeDrop: 10n ** 14n, quote });
|
|
|
221
224
|
Each pathway's executor caps the drop it will accept; asking for more makes the quote
|
|
222
225
|
throw `StableQuoteError` rather than silently clamping.
|
|
223
226
|
|
|
227
|
+
#### The messaging fee is padded before sending
|
|
228
|
+
|
|
229
|
+
A send has to carry the fee the pathway costs **at the moment it lands**, and `send`
|
|
230
|
+
demands an exact match: `msg.value` must equal `_fee.nativeFee`, the endpoint reverts
|
|
231
|
+
with `LZ_InsufficientFee` when that falls short of what it needs, and refunds the
|
|
232
|
+
excess to the sender when it does not. Gas moves between a quote and a signature, and
|
|
233
|
+
only one direction of that costs anything — a fee that rose reverts the send with the
|
|
234
|
+
gas already spent, while a fee that was padded comes back.
|
|
235
|
+
|
|
236
|
+
So `bridge()` and `prepareBridgeCalldata()` send `nativeFee` padded by `feeBufferBps`,
|
|
237
|
+
5% by default:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
await stable.bridge({ ...params, feeBufferBps: 1_000 }); // 10%
|
|
241
|
+
await stable.bridge({ ...params, feeBufferBps: 0 }); // exactly the quoted fee
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`quoteBridge()` reports both numbers and pads neither: `nativeFee` is the honest quote
|
|
245
|
+
to display, `nativeFeeMax` is what will actually leave the wallet.
|
|
246
|
+
|
|
224
247
|
---
|
|
225
248
|
|
|
226
249
|
### `bridge(params)`
|
|
227
250
|
|
|
228
251
|
Bridge tokens cross-chain, over the native LayerZero OFT mesh where the pair supports
|
|
229
252
|
it and LI.FI otherwise — see [Bridge routes](#bridge-routes). Handles ERC-20 approval
|
|
230
|
-
automatically
|
|
231
|
-
|
|
253
|
+
automatically. Switches the wallet to the source chain (MetaMask will prompt) before
|
|
254
|
+
sending.
|
|
255
|
+
|
|
256
|
+
Only a lockbox pathway needs an allowance at all — Ethereum for USDT0 and XAUt0, and
|
|
257
|
+
Sepolia on testnet. Every other deployment is an OFT that burns from the caller, so no
|
|
258
|
+
approve and no allowance read is issued there. Where one is needed and the current
|
|
259
|
+
allowance is short but not zero, it takes **two** approvals: legacy USDT refuses to
|
|
260
|
+
overwrite a non-zero allowance with another non-zero value, so the SDK sends
|
|
261
|
+
`approve(0)` first.
|
|
232
262
|
|
|
233
263
|
```ts
|
|
234
264
|
import { Chain } from "@stablechain/sdk";
|
|
@@ -249,9 +279,11 @@ const { txHash, toAmount } = await stable.bridge({
|
|
|
249
279
|
| `fromToken` | `string` | Source token contract address |
|
|
250
280
|
| `toToken` | `string` | Destination token contract address |
|
|
251
281
|
| `amount` | `number` | Amount to bridge (human-readable) |
|
|
252
|
-
| `fromDecimals` | `number` | *(optional)* Source token decimals — defaults to `6
|
|
282
|
+
| `fromDecimals` | `number` | *(optional)* Source token decimals — defaults to `6`. **LI.FI routes only:** native OFT routes parse the amount with the decimals the token has on the source chain, and a value disagreeing with those throws |
|
|
253
283
|
| `recipient` | `string` | *(optional)* Destination address — defaults to the signer |
|
|
254
284
|
| `dstNativeDrop`| `bigint` | *(optional)* Destination native token (wei) to airdrop to the recipient — native OFT routes only |
|
|
285
|
+
| `feeBufferBps` | `number` | *(optional)* Padding on the quoted messaging fee, in basis points — defaults to `500` (5%). Native OFT routes only |
|
|
286
|
+
| `refundAddress`| `string` | *(optional)* Where LayerZero returns messaging fee it did not need — defaults to the sender. Native OFT routes only |
|
|
255
287
|
| `quote` | `BridgeQuote` | *(optional)* On native OFT routes, a quote from `quoteBridge()` is reused as-is: it supplies the messaging fee and the slippage floor, skipping two reads. LI.FI routes re-fetch regardless |
|
|
256
288
|
| `route` | `BridgeRoute` | *(optional)* `"auto"` (default), `"oft"`, or `"lifi"` — see [Bridge routes](#bridge-routes) |
|
|
257
289
|
|
|
@@ -260,6 +292,77 @@ poll [`trackBridge(txHash)`](#trackbridgesrctxhash--bridgestatus) to follow it.
|
|
|
260
292
|
|
|
261
293
|
---
|
|
262
294
|
|
|
295
|
+
### `prepareBridgeCalldata(params)` → `BridgeCalldata`
|
|
296
|
+
|
|
297
|
+
Build a bridge's transactions without signing or sending any of them — for the split
|
|
298
|
+
where a backend quotes, builds and checks while the holder's wallet only signs. Native
|
|
299
|
+
OFT routes only: a pair that resolves to LI.FI throws, because LI.FI hands back its own
|
|
300
|
+
transaction request from its API.
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
import { Chain } from "@stablechain/sdk";
|
|
304
|
+
|
|
305
|
+
const built = await stable.prepareBridgeCalldata({
|
|
306
|
+
from: "0xUserWallet", // whose allowance decides the approvals
|
|
307
|
+
fromChain: Chain.Ethereum,
|
|
308
|
+
toChain: Chain.Stable,
|
|
309
|
+
fromToken: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
|
310
|
+
toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
|
|
311
|
+
amount: 100,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
for (const step of built.steps) {
|
|
315
|
+
// Send in order, each confirmed before the next.
|
|
316
|
+
await walletClient.sendTransaction(step);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Takes everything [`bridge()`](#bridgeparams) does, plus:
|
|
321
|
+
|
|
322
|
+
| Param | Type | Description |
|
|
323
|
+
|-----------------|----------|-----------------------------------------------------------------------------------|
|
|
324
|
+
| `from` | `string` | The address that will sign these transactions. Its allowance decides whether an approval step is needed, and it is the default recipient and refund address |
|
|
325
|
+
|
|
326
|
+
`steps` is ordered and must be sent in order, each confirmed before the next:
|
|
327
|
+
|
|
328
|
+
| Step | When |
|
|
329
|
+
|-------------|---------------------------------------------------------------------------------------|
|
|
330
|
+
| `approve(0)`| The pathway needs an allowance, and the current one is short **but not zero** — legacy USDT refuses to overwrite a non-zero allowance with another non-zero value |
|
|
331
|
+
| `approve(n)`| The pathway needs an allowance and the current one is short |
|
|
332
|
+
| `send` | Always. Carries the buffered messaging fee as its `value` |
|
|
333
|
+
|
|
334
|
+
Each step is `{ chainId, to, data, value }`, and `chainId` is the source chain on every
|
|
335
|
+
one of them. Alongside them:
|
|
336
|
+
|
|
337
|
+
| Field | Type | What it is |
|
|
338
|
+
|-----------|-----------------|---------------------------------------------------------------------|
|
|
339
|
+
| `chainId` | `number` | The source chain every step must be sent on |
|
|
340
|
+
| `asset` | `"usdt0" \| "xaut0"` | Which OFT mesh this send moves through |
|
|
341
|
+
| `quote` | `BridgeQuote` | The quote the steps were built from — supplied or freshly read |
|
|
342
|
+
| `send` | `object` | The `send` step's arguments decoded: `dstEid`, `recipient`, `to`, `amountLD`, `minAmountLD`, `extraOptions`, `nativeFee`, `lzTokenFee`, `refundAddress` |
|
|
343
|
+
|
|
344
|
+
`send` is there so a backend can check what it built without re-deriving the ABI
|
|
345
|
+
encoding — and `OFT_ADAPTER_ABI` and `buildLzOptions` are exported for the checks that
|
|
346
|
+
do want the encoding:
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
import { OFT_ADAPTER_ABI } from "@stablechain/sdk";
|
|
350
|
+
import { decodeFunctionData } from "viem";
|
|
351
|
+
|
|
352
|
+
const { args } = decodeFunctionData({
|
|
353
|
+
abi: OFT_ADAPTER_ABI,
|
|
354
|
+
data: built.steps.at(-1)!.data as `0x${string}`,
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
This refuses to build when the adapter will not answer `quoteOFT`: its receipt is what
|
|
359
|
+
floors the send at the amount it would actually deliver, and there is no safe stand-in
|
|
360
|
+
for that floor — the full input amount reverts with `SlippageExceeded` on any pathway
|
|
361
|
+
that charges a fee. `quoteBridge()` still degrades to a fee-only quote in that case;
|
|
362
|
+
only the paths that build a send refuse.
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
263
366
|
### Bridge routes
|
|
264
367
|
|
|
265
368
|
`bridge()` and `quoteBridge()` pick between two paths:
|