@stablechain/sdk 1.0.5 → 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 +233 -9
- package/dist/index.cjs +630 -163
- package/dist/index.d.cts +435 -20
- package/dist/index.d.ts +435 -20
- package/dist/index.js +620 -153
- 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,7 +80,8 @@ interface StableClient {
|
|
|
79
80
|
transfer(params: TransferParams): Promise<OperationResult>;
|
|
80
81
|
quoteBridge(params: BridgeParams): Promise<BridgeQuote>;
|
|
81
82
|
bridge(params: BridgeParams): Promise<OperationResult>;
|
|
82
|
-
|
|
83
|
+
prepareBridgeCalldata(params: PrepareBridgeParams): Promise<BridgeCalldata>;
|
|
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>;
|
|
85
87
|
swap(params: SwapParams): Promise<OperationResult>;
|
|
@@ -170,20 +172,93 @@ console.log("You receive:", quote.toAmount); // human units, on toChain
|
|
|
170
172
|
|
|
171
173
|
`BridgeQuote.toAmount` is the expected destination-side amount in human units.
|
|
172
174
|
|
|
173
|
-
On
|
|
174
|
-
|
|
175
|
+
On native OFT routes the quote is fully costed — `toAmount` comes from the adapter's
|
|
176
|
+
own `quoteOFT`, so it is the amount that actually lands, net of any OFT fee and of
|
|
177
|
+
dust dropped by decimal conversion:
|
|
178
|
+
|
|
179
|
+
| Field | Type | What it is |
|
|
180
|
+
|----------------|---------------------|-------------------------------------------------------------------------------|
|
|
181
|
+
| `toAmount` | `number` | What the recipient receives, human units |
|
|
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 |
|
|
184
|
+
| `lzTokenFee` | `bigint` | Same fee if paid in LZ token instead — `0n` on every route the SDK sends |
|
|
185
|
+
| `oft` | `BridgeOftQuote` | The adapter's own accounting: `amountSentLD`, `amountReceivedLD`, `fees[]`, and the pathway's `minAmountLD`/`maxAmountLD` |
|
|
186
|
+
| `srcGas` | `BridgeGasEstimate` | Source-chain execution cost: `gas`, `gasPriceWei`, `costWei`, `source` |
|
|
187
|
+
| `totalCostWei` | `bigint` | `srcGas.costWei + nativeFee` — everything leaving the sender's native balance |
|
|
188
|
+
| `estimate` | `BridgeTimeEstimate`| Delivery time — see [`estimateBridgeTime`](#estimatebridgetimeparams--bridgetimeestimate) |
|
|
175
189
|
|
|
176
190
|
```ts
|
|
191
|
+
import { formatEther } from "viem";
|
|
192
|
+
|
|
193
|
+
console.log("You receive:", quote.toAmount);
|
|
194
|
+
for (const fee of quote.oft?.fees ?? []) {
|
|
195
|
+
// Empty on every pathway the SDK configures today; a deployment that charges an
|
|
196
|
+
// OFT-level fee reports it here. Negative means a credit to the sender.
|
|
197
|
+
console.log(`${fee.description}: ${fee.amount}`);
|
|
198
|
+
}
|
|
199
|
+
if (quote.totalCostWei) {
|
|
200
|
+
console.log(`Costs ${formatEther(quote.totalCostWei)} of native token to send`);
|
|
201
|
+
}
|
|
177
202
|
if (quote.estimate) {
|
|
178
203
|
console.log(`Arrives in ~${Math.round(quote.estimate.seconds / 60)} min`);
|
|
179
204
|
}
|
|
180
205
|
```
|
|
181
206
|
|
|
207
|
+
`srcGas.source` says how much to trust the cost: `"simulated"` means the real `send`
|
|
208
|
+
was simulated, `"fallback"` means the simulation reverted — which is what happens when
|
|
209
|
+
you quote before approving the adapter — and `gas` is a typical-send constant instead.
|
|
210
|
+
Every field above is absent on LI.FI routes, and `oft`/`srcGas` are also absent when
|
|
211
|
+
the adapter or the RPC would not answer, so treat them as optional rather than assuming
|
|
212
|
+
a native route populates them.
|
|
213
|
+
|
|
214
|
+
Both `quoteBridge()` and `bridge()` accept `dstNativeDrop` — wei of the **destination**
|
|
215
|
+
chain's native token to airdrop to the recipient, what bridge UIs call *gas on
|
|
216
|
+
destination*. The executor prices it into `nativeFee`, so quote with the same value you
|
|
217
|
+
send with:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
const quote = await stable.quoteBridge({ ...params, dstNativeDrop: 10n ** 14n });
|
|
221
|
+
await stable.bridge({ ...params, dstNativeDrop: 10n ** 14n, quote });
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Each pathway's executor caps the drop it will accept; asking for more makes the quote
|
|
225
|
+
throw `StableQuoteError` rather than silently clamping.
|
|
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
|
+
|
|
182
247
|
---
|
|
183
248
|
|
|
184
249
|
### `bridge(params)`
|
|
185
250
|
|
|
186
|
-
Bridge tokens cross-chain
|
|
251
|
+
Bridge tokens cross-chain, over the native LayerZero OFT mesh where the pair supports
|
|
252
|
+
it and LI.FI otherwise — see [Bridge routes](#bridge-routes). Handles ERC-20 approval
|
|
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.
|
|
187
262
|
|
|
188
263
|
```ts
|
|
189
264
|
import { Chain } from "@stablechain/sdk";
|
|
@@ -204,19 +279,155 @@ const { txHash, toAmount } = await stable.bridge({
|
|
|
204
279
|
| `fromToken` | `string` | Source token contract address |
|
|
205
280
|
| `toToken` | `string` | Destination token contract address |
|
|
206
281
|
| `amount` | `number` | Amount to bridge (human-readable) |
|
|
207
|
-
| `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 |
|
|
208
283
|
| `recipient` | `string` | *(optional)* Destination address — defaults to the signer |
|
|
209
|
-
| `
|
|
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 |
|
|
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 |
|
|
288
|
+
| `route` | `BridgeRoute` | *(optional)* `"auto"` (default), `"oft"`, or `"lifi"` — see [Bridge routes](#bridge-routes) |
|
|
210
289
|
|
|
211
290
|
`txHash` is the **source-chain** transaction hash. Destination delivery is asynchronous —
|
|
212
291
|
poll [`trackBridge(txHash)`](#trackbridgesrctxhash--bridgestatus) to follow it.
|
|
213
292
|
|
|
214
293
|
---
|
|
215
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
|
+
|
|
366
|
+
### Bridge routes
|
|
367
|
+
|
|
368
|
+
`bridge()` and `quoteBridge()` pick between two paths:
|
|
369
|
+
|
|
370
|
+
| Route | What it is | Cost | Speed |
|
|
371
|
+
|----------|-------------------------------------|---------------------------------|--------|
|
|
372
|
+
| `"oft"` | The token's native LayerZero OFT | No protocol fee — gas + LZ messaging fee | Slower |
|
|
373
|
+
| `"lifi"` | The [LI.FI](https://li.fi) aggregator | Charges a fee | Fast |
|
|
374
|
+
|
|
375
|
+
`route` defaults to `"auto"`: the OFT path when both chains deploy the asset the token
|
|
376
|
+
addresses name, LI.FI otherwise. Forcing `"oft"` on an unsupported pair throws
|
|
377
|
+
`StableValidationError` rather than silently charging a LI.FI fee.
|
|
378
|
+
|
|
379
|
+
> `route: "usdt0"` is a deprecated alias for `"oft"`. It does not restrict the route to
|
|
380
|
+
> USDT0 — the token addresses decide the asset.
|
|
381
|
+
|
|
382
|
+
#### Native OFT assets
|
|
383
|
+
|
|
384
|
+
Two assets have their own OFT mesh, each with its own adapter and token address per
|
|
385
|
+
chain. A bridge stays within one mesh: the OFT mints its own peer on the far side, so it
|
|
386
|
+
cannot hand back a different token. A cross-asset request (USDT0 in, XAUt0 out) is a
|
|
387
|
+
trade, not a bridge, and `"auto"` sends it to LI.FI.
|
|
388
|
+
|
|
389
|
+
**USDT0** — Stable, Ethereum, Arbitrum, Bera, Flare, HyperEVM, Ink, MegaETH, Optimism,
|
|
390
|
+
Plasma, Polygon, Rootstock, Sei, Unichain, XLayer.
|
|
391
|
+
|
|
392
|
+
**XAUt0** (Tether Gold, 6 decimals) — Stable, Ethereum, Arbitrum, BSC, HyperEVM, Ink,
|
|
393
|
+
Plasma, Polygon. Every pair among those eight is wired in both directions.
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
// XAUt0: Ethereum → Stable, over the native OFT mesh
|
|
397
|
+
const { txHash } = await stable.bridge({
|
|
398
|
+
fromChain: Chain.Ethereum,
|
|
399
|
+
toChain: Chain.Stable,
|
|
400
|
+
fromToken: "0x68749665FF8D2d112Fa859AA293F07A622782F38", // XAUt on Ethereum
|
|
401
|
+
toToken: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb", // XAUt0 on Stable
|
|
402
|
+
amount: 2, // 2 XAUt0
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Ethereum is XAUt0's lockbox — canonical XAUt is escrowed there and minted elsewhere, so
|
|
407
|
+
it is the only chain where a send needs an ERC-20 approve. Bridging XAUt0 from any other
|
|
408
|
+
chain burns from the caller and skips the approval transaction entirely.
|
|
409
|
+
|
|
410
|
+
BSC is XAUt0-only: it has no USDT0 deployment, so a BSC USDT bridge routes to LI.FI.
|
|
411
|
+
|
|
412
|
+
Addresses per chain are in `BRIDGE_CHAIN_CONFIGS[chain].tokens`, keyed by asset:
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
import { BRIDGE_CHAIN_CONFIGS, bridgeAssetFor, Chain } from "@stablechain/sdk";
|
|
416
|
+
|
|
417
|
+
const stableChain = BRIDGE_CHAIN_CONFIGS[Chain.Stable];
|
|
418
|
+
stableChain.tokens.xaut0?.address; // XAUt0 ERC-20 on Stable
|
|
419
|
+
stableChain.tokens.xaut0?.oftAdapter; // its OFT
|
|
420
|
+
|
|
421
|
+
// Which asset an address refers to on a chain, if any
|
|
422
|
+
bridgeAssetFor(stableChain, someToken); // "usdt0" | "xaut0" | undefined
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
216
427
|
### `estimateBridgeTime(params)` → `BridgeTimeEstimate`
|
|
217
428
|
|
|
218
|
-
How long a **
|
|
219
|
-
chain pairs with no
|
|
429
|
+
How long a **native OFT (LayerZero)** bridge will take, before you send it. Throws for
|
|
430
|
+
chain pairs with no OFT adapter — LI.FI routes carry their own ETA in the quote.
|
|
220
431
|
|
|
221
432
|
```ts
|
|
222
433
|
const est = await stable.estimateBridgeTime({
|
|
@@ -227,6 +438,19 @@ const est = await stable.estimateBridgeTime({
|
|
|
227
438
|
console.log(`~${Math.round(est.seconds / 60)} min`); // ~42 min
|
|
228
439
|
```
|
|
229
440
|
|
|
441
|
+
Delivery time is per pathway, and USDT0 and XAUt0 are different pathways on the same
|
|
442
|
+
chain pair. Omitting the tokens estimates the USDT0 pathway; pass `fromToken`/`toToken`
|
|
443
|
+
to estimate a specific asset:
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
const est = await stable.estimateBridgeTime({
|
|
447
|
+
fromChain: Chain.Ethereum,
|
|
448
|
+
toChain: Chain.Stable,
|
|
449
|
+
fromToken: "0x68749665FF8D2d112Fa859AA293F07A622782F38", // XAUt on Ethereum
|
|
450
|
+
toToken: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb", // XAUt0 on Stable
|
|
451
|
+
});
|
|
452
|
+
```
|
|
453
|
+
|
|
230
454
|
Nearly all of the wait is **source-chain finality**: the DVNs securing the pathway will
|
|
231
455
|
not attest until the send transaction is buried under `confirmations` source blocks.
|
|
232
456
|
Destination execution is a few blocks on top.
|
|
@@ -273,7 +497,7 @@ Roughly what to expect on mainnet, measured 2026-07-30:
|
|
|
273
497
|
|
|
274
498
|
### `trackBridge(srcTxHash)` → `BridgeStatus`
|
|
275
499
|
|
|
276
|
-
Follow an in-flight
|
|
500
|
+
Follow an in-flight native OFT bridge, via LayerZero Scan. A message Scan has not indexed yet
|
|
277
501
|
reports `"pending"` rather than throwing, so it is safe to call immediately after `bridge()`.
|
|
278
502
|
|
|
279
503
|
```ts
|