@stablechain/sdk 1.0.4 → 1.0.5
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 +757 -0
- package/dist/chunk-QQ26UI3F.cjs +665 -0
- package/dist/chunk-VS6GBRDF.js +665 -0
- package/dist/erc3009.cjs +26 -0
- package/dist/erc3009.d.cts +429 -0
- package/dist/erc3009.d.ts +429 -0
- package/dist/erc3009.js +26 -0
- package/dist/index.cjs +451 -221
- package/dist/index.d.cts +82 -4
- package/dist/index.d.ts +82 -4
- package/dist/index.js +346 -116
- package/package.json +6 -1
package/README.md
ADDED
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="../../.github/assets/stable-logotype-light.svg">
|
|
4
|
+
<img alt="Stable" src="../../.github/assets/stable-logotype-dark.svg" width="96" height="96">
|
|
5
|
+
</picture>
|
|
6
|
+
|
|
7
|
+
<h1>@stablechain/sdk</h1>
|
|
8
|
+
|
|
9
|
+
<p><strong>TypeScript SDK for <a href="https://stable.xyz">Stable</a> — a USDT-native EVM blockchain.</strong></p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
Transfer, bridge, swap, and earn yield on Stable from a browser wallet or a
|
|
13
|
+
server-side key. For gasless relaying and premium blockspace, see
|
|
14
|
+
[`@stablechain/enterprise`](../enterprise).
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun add @stablechain/sdk viem
|
|
20
|
+
# or
|
|
21
|
+
npm install @stablechain/sdk viem
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`viem` is a peer dependency (>= 2.0.0).
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
**Server-side (private key):**
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createStable, Network } from "@stablechain/sdk";
|
|
32
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
33
|
+
|
|
34
|
+
const stable = createStable({
|
|
35
|
+
network: Network.Mainnet,
|
|
36
|
+
account: privateKeyToAccount("0x..."),
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Browser wallet (MetaMask, etc.):**
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { createStable, Network } from "@stablechain/sdk";
|
|
44
|
+
import { custom } from "viem";
|
|
45
|
+
|
|
46
|
+
const stable = createStable({
|
|
47
|
+
network: Network.Mainnet,
|
|
48
|
+
transport: custom(window.ethereum),
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Pre-built viem `WalletClient`:**
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const stable = createStable({
|
|
56
|
+
network: Network.Mainnet,
|
|
57
|
+
walletClient: myWalletClient,
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `StableConfig`
|
|
62
|
+
|
|
63
|
+
| Param | Type | Description |
|
|
64
|
+
|-----------------|----------------|----------------------------------------------------------------------|
|
|
65
|
+
| `network` | `Network` | `Network.Mainnet` (default) or `Network.Testnet` |
|
|
66
|
+
| `rpc` | `string` | *(optional)* RPC override for the Stable chain |
|
|
67
|
+
| `account` | `Account` | *(optional)* viem `Account` for server-side signing |
|
|
68
|
+
| `transport` | `Transport` | *(optional)* viem `Transport` (e.g. `custom(window.ethereum)`) |
|
|
69
|
+
| `walletClient` | `WalletClient` | *(optional)* Pre-built viem `WalletClient` — takes precedence |
|
|
70
|
+
| `earn` | `EarnConfig` | *(optional)* `{ vault: "0x..." }` — required for `stable.earn.*` |
|
|
71
|
+
| `merklApiBase` | `string` | *(optional)* Base URL for the Merkl rewards API — override to proxy through your own server and avoid CORS restrictions in browser environments (default: `https://api.merkl.xyz`) |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
interface StableClient {
|
|
79
|
+
transfer(params: TransferParams): Promise<OperationResult>;
|
|
80
|
+
quoteBridge(params: BridgeParams): Promise<BridgeQuote>;
|
|
81
|
+
bridge(params: BridgeParams): Promise<OperationResult>;
|
|
82
|
+
estimateBridgeTime(params: Pick<BridgeParams, "fromChain" | "toChain">): Promise<BridgeTimeEstimate>;
|
|
83
|
+
trackBridge(srcTxHash: string): Promise<BridgeStatus>;
|
|
84
|
+
quoteSwap(params: SwapParams): Promise<SwapQuote>;
|
|
85
|
+
swap(params: SwapParams): Promise<OperationResult>;
|
|
86
|
+
authorization: { /* ERC-3009 — see below */ };
|
|
87
|
+
earn: {
|
|
88
|
+
deposit(params: EarnDepositParams): Promise<EarnResult>;
|
|
89
|
+
bulkDeposit(items: EarnDepositParams[], options?: BulkDepositOptions): Promise<BulkEarnResult>;
|
|
90
|
+
withdraw(params: EarnWithdrawParams): Promise<EarnResult>;
|
|
91
|
+
bulkWithdraw(items: EarnWithdrawParams[], options?: BulkWithdrawOptions): Promise<BulkEarnResult>;
|
|
92
|
+
redeem(params: EarnRedeemParams): Promise<EarnResult>;
|
|
93
|
+
incentiveRewards: {
|
|
94
|
+
fetch(): Promise<MerklRewardsResult>;
|
|
95
|
+
claim(): Promise<MerklClaimResult>;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
All write methods resolve to `OperationResult` or `EarnResult`:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
interface OperationResult {
|
|
105
|
+
txHash: string;
|
|
106
|
+
toAmount?: number; // destination-side amount, when a conversion occurred
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface EarnResult {
|
|
110
|
+
txHash: string;
|
|
111
|
+
idempotencyKey?: string;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### `transfer(params)`
|
|
118
|
+
|
|
119
|
+
Send tokens on Stable. Native USDT0 transfers use 18 decimals; ERC-20 decimals are fetched on-chain when not provided.
|
|
120
|
+
|
|
121
|
+
**Native USDT0**
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const { txHash } = await stable.transfer({
|
|
125
|
+
to: "0xRecipient",
|
|
126
|
+
amount: 10, // 10 USDT0
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**ERC-20 (USDC, EURC, …)**
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const { txHash } = await stable.transfer({
|
|
134
|
+
to: "0xRecipient",
|
|
135
|
+
amount: 100,
|
|
136
|
+
token: "0xUSDCAddress", // ERC-20 contract address on Stable
|
|
137
|
+
tokenDecimals: 6, // optional — fetched on-chain if omitted
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
| Param | Type | Description |
|
|
142
|
+
|-----------------|----------|-----------------------------------------------------------------------------------|
|
|
143
|
+
| `to` | `string` | Recipient address |
|
|
144
|
+
| `amount` | `number` | Amount in human-readable units |
|
|
145
|
+
| `token` | `string` | *(optional)* ERC-20 contract address. Omit for native USDT0. |
|
|
146
|
+
| `tokenDecimals` | `number` | *(optional)* Token decimals — fetched on-chain if not provided |
|
|
147
|
+
| `from` | `string` | *(optional, advisory)* Sender label. The signer is taken from `walletClient`/`account`; this field does **not** override it. |
|
|
148
|
+
|
|
149
|
+
`transfer` switches the wallet to the configured Stable network, sends the tx, waits for the receipt, and throws a `StableTransactionError` if it reverts.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
### `quoteBridge(params)` → `BridgeQuote`
|
|
154
|
+
|
|
155
|
+
Preview a bridge before sending. Quotes are fetched from [LI.FI](https://li.fi).
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { Chain } from "@stablechain/sdk";
|
|
159
|
+
|
|
160
|
+
const quote = await stable.quoteBridge({
|
|
161
|
+
fromChain: Chain.Ethereum,
|
|
162
|
+
toChain: Chain.Stable,
|
|
163
|
+
fromToken: "0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT on Ethereum
|
|
164
|
+
toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", // USDT0 on Stable
|
|
165
|
+
amount: 100,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
console.log("You receive:", quote.toAmount); // human units, on toChain
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`BridgeQuote.toAmount` is the expected destination-side amount in human units.
|
|
172
|
+
|
|
173
|
+
On USDT0 (LayerZero OFT) routes the quote also carries `estimate` — how long delivery
|
|
174
|
+
should take. See [`estimateBridgeTime`](#estimatebridgetimeparams--bridgetimeestimate).
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
if (quote.estimate) {
|
|
178
|
+
console.log(`Arrives in ~${Math.round(quote.estimate.seconds / 60)} min`);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### `bridge(params)`
|
|
185
|
+
|
|
186
|
+
Bridge tokens cross-chain via LI.FI. Handles ERC-20 approval automatically. Switches the wallet to the source chain (MetaMask will prompt) before sending.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { Chain } from "@stablechain/sdk";
|
|
190
|
+
|
|
191
|
+
const { txHash, toAmount } = await stable.bridge({
|
|
192
|
+
fromChain: Chain.Ethereum,
|
|
193
|
+
toChain: Chain.Stable,
|
|
194
|
+
fromToken: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
|
195
|
+
toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
|
|
196
|
+
amount: 100,
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
| Param | Type | Description |
|
|
201
|
+
|----------------|---------------|--------------------------------------------------------------------------|
|
|
202
|
+
| `fromChain` | `Chain` | Source chain |
|
|
203
|
+
| `toChain` | `Chain` | Destination chain |
|
|
204
|
+
| `fromToken` | `string` | Source token contract address |
|
|
205
|
+
| `toToken` | `string` | Destination token contract address |
|
|
206
|
+
| `amount` | `number` | Amount to bridge (human-readable) |
|
|
207
|
+
| `fromDecimals` | `number` | *(optional)* Source token decimals — defaults to `6` |
|
|
208
|
+
| `recipient` | `string` | *(optional)* Destination address — defaults to the signer |
|
|
209
|
+
| `quote` | `BridgeQuote` | *(optional)* Reserved for future use; the current implementation re-fetches a LI.FI quote regardless |
|
|
210
|
+
|
|
211
|
+
`txHash` is the **source-chain** transaction hash. Destination delivery is asynchronous —
|
|
212
|
+
poll [`trackBridge(txHash)`](#trackbridgesrctxhash--bridgestatus) to follow it.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
### `estimateBridgeTime(params)` → `BridgeTimeEstimate`
|
|
217
|
+
|
|
218
|
+
How long a **USDT0 (LayerZero OFT)** bridge will take, before you send it. Throws for
|
|
219
|
+
chain pairs with no USDT0 adapter — LI.FI routes carry their own ETA in the quote.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const est = await stable.estimateBridgeTime({
|
|
223
|
+
fromChain: Chain.Stable,
|
|
224
|
+
toChain: Chain.Arbitrum,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
console.log(`~${Math.round(est.seconds / 60)} min`); // ~42 min
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Nearly all of the wait is **source-chain finality**: the DVNs securing the pathway will
|
|
231
|
+
not attest until the send transaction is buried under `confirmations` source blocks.
|
|
232
|
+
Destination execution is a few blocks on top.
|
|
233
|
+
|
|
234
|
+
The SDK resolves this from two independent inputs and reports both:
|
|
235
|
+
|
|
236
|
+
| Field | Meaning |
|
|
237
|
+
|-------------------|--------------------------------------------------------------------------------------------------|
|
|
238
|
+
| `seconds` | Best available estimate — display this |
|
|
239
|
+
| `source` | `"observed"` or `"formula"`, whichever produced `seconds` |
|
|
240
|
+
| `observedSeconds` | Median delivery time of recent messages on this exact pathway, from LayerZero Scan |
|
|
241
|
+
| `formulaSeconds` | LayerZero's documented estimate: `srcBlockTime × confirmations + dstBlockTime × (2 + dvnCount)` |
|
|
242
|
+
| `confirmations` | Source blocks the DVNs wait for, read live from the pathway's ULN config |
|
|
243
|
+
| `dvnCount` | DVNs verifying the pathway |
|
|
244
|
+
|
|
245
|
+
`observed` wins when available. The formula assumes block confirmations are the whole
|
|
246
|
+
wait, which holds for L1 sources but understates rollups badly — Arbitrum→Stable computes
|
|
247
|
+
to ~19 s (60 blocks × 0.25 s) against a real median of ~18 min, because the DVNs wait for
|
|
248
|
+
the L2 batch to finalize on Ethereum.
|
|
249
|
+
|
|
250
|
+
Pathway configs and observed medians are cached for 5 minutes; call
|
|
251
|
+
`clearBridgeEstimateCache()` to force a refetch.
|
|
252
|
+
|
|
253
|
+
Roughly what to expect on mainnet, measured 2026-07-30:
|
|
254
|
+
|
|
255
|
+
| Pathway | Confirmations | Typical delivery |
|
|
256
|
+
|---------------------|---------------|------------------|
|
|
257
|
+
| Polygon → Stable | 32 | ~1 min |
|
|
258
|
+
| Bera → Stable | 60 | ~2 min |
|
|
259
|
+
| Unichain → Stable | 1600 | ~8 min |
|
|
260
|
+
| Flare → Stable | 500 | ~11 min |
|
|
261
|
+
| Optimism → Stable | 1000 | ~15 min |
|
|
262
|
+
| Ethereum → Stable | 65 | ~17 min |
|
|
263
|
+
| Sei → Stable | 2000 | ~17 min |
|
|
264
|
+
| Arbitrum → Stable | 60 | ~18 min |
|
|
265
|
+
| Plasma → Stable | 1800 | ~30 min |
|
|
266
|
+
| Stable → any | 3600 | ~42 min |
|
|
267
|
+
| Rootstock → Stable | 120 | ~62 min |
|
|
268
|
+
| MegaETH → Stable | 5400 | ~90 min |
|
|
269
|
+
| X Layer → Stable | 9000 | ~2.5 h |
|
|
270
|
+
| HyperEVM → Stable | 43200 | ~12 h |
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
### `trackBridge(srcTxHash)` → `BridgeStatus`
|
|
275
|
+
|
|
276
|
+
Follow an in-flight USDT0 bridge, via LayerZero Scan. A message Scan has not indexed yet
|
|
277
|
+
reports `"pending"` rather than throwing, so it is safe to call immediately after `bridge()`.
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
const { txHash } = await stable.bridge({ /* … */ });
|
|
281
|
+
|
|
282
|
+
const status = await stable.trackBridge(txHash);
|
|
283
|
+
// { state: "inflight", elapsedSeconds: 1027, scanUrl: "https://layerzeroscan.com/tx/0x…" }
|
|
284
|
+
|
|
285
|
+
if (status.state === "delivered") console.log("Landed:", status.dstTxHash);
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`state` is one of `"pending" | "inflight" | "delivered" | "failed"`. `raw` carries the
|
|
289
|
+
underlying LayerZero Scan status when you need the exact value.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
### `quoteSwap(params)` → `SwapQuote`
|
|
294
|
+
|
|
295
|
+
Fetch a LI.FI swap quote on Stable without sending a transaction.
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
const quote = await stable.quoteSwap({
|
|
299
|
+
fromToken: "0x8a2B28364102Bea189D99A475C494330Ef2bDD0B", // e.g. USDCe
|
|
300
|
+
toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", // USDT0
|
|
301
|
+
amount: 100,
|
|
302
|
+
fromDecimals: 6,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
console.log("You'll receive:", quote.toAmount, "USDT0");
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
interface SwapQuote {
|
|
310
|
+
toAmount: number; // human-readable, formatted with toToken decimals
|
|
311
|
+
fromAmount: bigint; // raw source-side amount
|
|
312
|
+
fromToken: string; // ERC-20 to approve (or native sentinel)
|
|
313
|
+
approvalAddress?: string; // spender for ERC-20 approval (omitted for native)
|
|
314
|
+
transactionRequest: {
|
|
315
|
+
to: string;
|
|
316
|
+
data: string;
|
|
317
|
+
value: string;
|
|
318
|
+
gasLimit: string;
|
|
319
|
+
chainId: number;
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
### `swap(params)`
|
|
327
|
+
|
|
328
|
+
Swap tokens on Stable via [LI.FI](https://li.fi). Handles ERC-20 approval automatically; MetaMask is asked to switch to Stable if needed.
|
|
329
|
+
|
|
330
|
+
Pass a pre-fetched `quote` from `quoteSwap()` to skip the LI.FI quote call.
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
const quote = await stable.quoteSwap({ fromToken, toToken, amount: 100, fromDecimals: 6 });
|
|
334
|
+
const { txHash, toAmount } = await stable.swap({
|
|
335
|
+
fromToken,
|
|
336
|
+
toToken,
|
|
337
|
+
amount: 100,
|
|
338
|
+
fromDecimals: 6,
|
|
339
|
+
quote,
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
| Param | Type | Default | Description |
|
|
344
|
+
|----------------|-------------|---------|---------------------------------------------------|
|
|
345
|
+
| `fromToken` | `string` | | Token address on Stable |
|
|
346
|
+
| `toToken` | `string` | | Token address on Stable |
|
|
347
|
+
| `amount` | `number` | | Human-readable amount |
|
|
348
|
+
| `fromDecimals` | `number` | `6` | Source token decimals |
|
|
349
|
+
| `toAddress` | `string` | | *(optional)* Recipient — defaults to signer |
|
|
350
|
+
| `quote` | `SwapQuote` | | *(optional)* Pre-fetched quote — skips LI.FI call |
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
### `authorization.*` — ERC-3009 gasless transfers
|
|
355
|
+
|
|
356
|
+
[ERC-3009](https://docs.stable.xyz/en/explanation/erc-3009/) ("Transfer With Authorization") lets a payer **sign** a transfer offline; anyone else **submits** it on-chain later. The payer pays no gas and needn't be the submitter — the primitive behind x402 payments and net-30 invoicing. USDT0 on Stable implements it natively.
|
|
357
|
+
|
|
358
|
+
The actions are viem-style and fully standalone — 3009 works with **nothing but viem**, no `createStable` and no payment module required. Import the primitives directly, attach them to any viem client via `.extend(erc3009Actions)`, or use the `stable.authorization.*` convenience below.
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
// Isolated subpath — pulls in zero bridge/earn/swap code.
|
|
362
|
+
import { signTransferAuthorization, erc3009Actions } from "@stablechain/sdk/erc3009";
|
|
363
|
+
|
|
364
|
+
const signed = await signTransferAuthorization(walletClient, { to, value, validBefore });
|
|
365
|
+
// or: const client = walletClient.extend(erc3009Actions);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
> This module is a **leaf**: it depends only on viem. Any higher-level payments module is a *consumer* of it, never the reverse — so you can adopt 3009 à la carte.
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
// Payer side — sign offline, hand the result off over HTTP / QR / x402
|
|
372
|
+
const signed = await stable.authorization.signTransferAuthorization({
|
|
373
|
+
to: "0xVendor",
|
|
374
|
+
value: 1_250_000n, // raw units — 1.25 USDT0 (6 dp)
|
|
375
|
+
validBefore: BigInt(Math.floor(Date.now() / 1000) + 3600),
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Submitter side — screen it gas-free, then settle (or hand off to a relayer)
|
|
379
|
+
const check = await stable.authorization.verifyAuthorization({ signed });
|
|
380
|
+
if (check.ok) await stable.authorization.transferWithAuthorization({ signed });
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
| Method | Returns | Description |
|
|
384
|
+
|------------------------------------------------|-----------------------------|-----------------------------------------------------------------------------|
|
|
385
|
+
| `signTransferAuthorization(p)` | `SignedAuthorization` | Sign a portable `TransferWithAuthorization` (offline, gas-free) |
|
|
386
|
+
| `signReceiveAuthorization(p)` | `SignedAuthorization` | Same, but only the recipient (`to`) can submit it |
|
|
387
|
+
| `transferWithAuthorization(p)` | `Hash` | Submit a signed (or freshly-signed) authorization — anyone may submit |
|
|
388
|
+
| `receiveWithAuthorization(p)` | `Hash` | Submit as the recipient (enforced on-chain via `msg.sender`) |
|
|
389
|
+
| `cancelAuthorization(p)` | `Hash` | Revoke an unused nonce (best-effort — can race an in-flight settle) |
|
|
390
|
+
| `prepareTransferWithAuthorization(p)` | `PreparedAuthorizationCall` | Sign + return `{ to, data, value, chainId, signed }` for a relayer to submit |
|
|
391
|
+
| `verifyAuthorization(p)` | `VerifyAuthorizationResult` | Gas-free preflight: signer, nonce state, time window, solvency, `eth_call` |
|
|
392
|
+
| `getAuthorizationState(p)` | `boolean` | `true` once the nonce is used or cancelled |
|
|
393
|
+
| `getAuthorizationUsage(p)` | `AuthorizationUsage` | Resolve the consuming tx from the `AuthorizationUsed` event (no indexer) |
|
|
394
|
+
|
|
395
|
+
`value` is a **raw `bigint`** in the token's smallest unit (USDT0 is 6 decimals) — this is the 6-dp ERC-20 path, not the 18-dp native one. `token` defaults to USDT0; `nonce` defaults to a cryptographically-random 32 bytes; the EIP-712 `domain` is read from the token (EIP-5267 `eip712Domain()`, falling back to `name()` + version `"1"`) unless overridden.
|
|
396
|
+
|
|
397
|
+
To relay gaslessly, pass `prepareTransferWithAuthorization`'s output to the [`@stablechain/enterprise`](../enterprise) gas-waiver rail.
|
|
398
|
+
|
|
399
|
+
> **Contract signers:** canonical ERC-3009 uses `ecrecover`, so Safe / EIP-1271 accounts cannot sign an authorization. Use an EOA (optionally a 7702 session key) for the payer.
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
### `earn.deposit(params)` → `EarnResult`
|
|
404
|
+
|
|
405
|
+
Deposit USDT0 into the Stable vault to earn yield, powered by [Morpho](https://morpho.org). Handles ERC-20 approval and permit signatures automatically.
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
import { createStable, Network, STABLE_VAULT_ADDRESS } from "@stablechain/sdk";
|
|
409
|
+
|
|
410
|
+
const stable = createStable({
|
|
411
|
+
network: Network.Mainnet,
|
|
412
|
+
earn: { vault: STABLE_VAULT_ADDRESS },
|
|
413
|
+
walletClient: myWalletClient,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const { txHash } = await stable.earn.deposit({ amount: 100 }); // 100 USDT0
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
| Param | Type | Description |
|
|
420
|
+
|---------------------|----------|-------------------------------------------------------------------|
|
|
421
|
+
| `amount` | `number` | Amount in human-readable units |
|
|
422
|
+
| `vault` | `string` | *(optional)* Vault address — overrides the one set in config |
|
|
423
|
+
| `tokenDecimals` | `number` | *(optional)* Token decimals — fetched on-chain if omitted |
|
|
424
|
+
| `slippageTolerance` | `number` | *(optional)* Max slippage as a decimal (e.g. `0.01` = 1%) |
|
|
425
|
+
| `idempotencyKey` | `string` | *(optional)* Dedup key — concurrent calls with the same key return the same Promise |
|
|
426
|
+
| `depositBuffer` | `number` | *(optional)* Max fraction of vault idle liquidity to consume (e.g. `0.8` = 80%). Throws `StableValidationError` before sending if exceeded. Must be `> 0` and `≤ 1`. |
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
### `earn.withdraw(params)` / `earn.redeem(params)` → `EarnResult`
|
|
431
|
+
|
|
432
|
+
Exit the vault by asset amount (`withdraw`) or by share count (`redeem`).
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
// Withdraw by asset amount — most common
|
|
436
|
+
const { txHash } = await stable.earn.withdraw({ amount: 100 });
|
|
437
|
+
|
|
438
|
+
// Redeem by share count — use reader.earn.position() to get sharesFormatted
|
|
439
|
+
const { sharesFormatted } = await reader.earn.position();
|
|
440
|
+
const { txHash } = await stable.earn.redeem({ shares: sharesFormatted });
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
> **Shares ≠ assets.** The exchange rate increases over time as yield accrues. Use `withdraw` when you want a specific USDT0 amount back; use `redeem` when you want to exit by share count (e.g. close 100% of a position). Use `reader.earn.position()` to get the human-readable share balance (`sharesFormatted`).
|
|
444
|
+
|
|
445
|
+
When idle vault liquidity is insufficient, `withdraw` and `redeem` automatically pull liquidity from the underlying market adapters. No extra configuration needed.
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
### `earn.bulkDeposit(items, options?)` → `BulkEarnResult`
|
|
450
|
+
|
|
451
|
+
Execute multiple deposits sequentially from one signer. Each item can target a different vault — useful for distributing across vaults in a single call. Failures are isolated: a failed item is recorded in `results` without halting the rest (unless `stopOnError` is set).
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
import { type BulkEarnResult } from "@stablechain/sdk";
|
|
455
|
+
|
|
456
|
+
const result: BulkEarnResult = await stable.earn.bulkDeposit([
|
|
457
|
+
{ amount: 50, vault: "0xVaultA" },
|
|
458
|
+
{ amount: 30, vault: "0xVaultB" },
|
|
459
|
+
{ amount: 20 }, // falls back to the vault set in config
|
|
460
|
+
]);
|
|
461
|
+
|
|
462
|
+
console.log(`${result.succeeded} succeeded, ${result.failed} failed`);
|
|
463
|
+
result.results.forEach((r, i) => {
|
|
464
|
+
if (r.status === "fulfilled") console.log(`#${i + 1} [${r.vault}] txHash:`, r.txHash);
|
|
465
|
+
else console.error(`#${i + 1} [${r.vault}] error:`, r.error);
|
|
466
|
+
});
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
**`BulkDepositOptions`**
|
|
470
|
+
|
|
471
|
+
| Option | Type | Default | Description |
|
|
472
|
+
|------------------|-----------|---------|---------------------------------------------------------------|
|
|
473
|
+
| `stopOnError` | `boolean` | `false` | Halt on first failure and throw `StableBulkError` |
|
|
474
|
+
| `idempotencyKey` | `string` | | Batch-level dedup key |
|
|
475
|
+
|
|
476
|
+
**`BulkEarnResultItem`**
|
|
477
|
+
|
|
478
|
+
| Field | Type | Description |
|
|
479
|
+
|-------------------|----------|------------------------------------------|
|
|
480
|
+
| `status` | `string` | `"fulfilled"` or `"rejected"` |
|
|
481
|
+
| `txHash` | `string` | *(fulfilled only)* Transaction hash |
|
|
482
|
+
| `error` | `string` | *(rejected only)* Error message |
|
|
483
|
+
| `vault` | `string` | *(optional)* Vault address for this item |
|
|
484
|
+
| `idempotencyKey` | `string` | *(optional)* Item-level dedup key |
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
### `earn.bulkWithdraw(items, options?)` → `BulkEarnResult`
|
|
489
|
+
|
|
490
|
+
Execute multiple withdrawals sequentially from one signer. Same failure-isolation semantics as `bulkDeposit`.
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
const result = await stable.earn.bulkWithdraw([
|
|
494
|
+
{ amount: 50 },
|
|
495
|
+
{ amount: 30 },
|
|
496
|
+
]);
|
|
497
|
+
|
|
498
|
+
console.log(`${result.succeeded} succeeded, ${result.failed} failed`);
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**`BulkWithdrawOptions`**
|
|
502
|
+
|
|
503
|
+
| Option | Type | Default | Description |
|
|
504
|
+
|------------------|-----------|---------|------------------------------------------------|
|
|
505
|
+
| `stopOnError` | `boolean` | `false` | Halt on first failure and throw `StableBulkError` |
|
|
506
|
+
| `idempotencyKey` | `string` | | Batch-level dedup key |
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
### `earn.incentiveRewards.fetch()` → `MerklRewardsResult`
|
|
511
|
+
|
|
512
|
+
Fetch pending [Merkl](https://merkl.xyz) incentive rewards for the signer on the configured chain.
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
const { rewards } = await stable.earn.incentiveRewards.fetch();
|
|
516
|
+
|
|
517
|
+
rewards.forEach((r) => {
|
|
518
|
+
console.log(`${r.token.symbol}: ${r.amount} (${r.rawAmount} raw)`);
|
|
519
|
+
});
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**`MerklRewardsResult`**
|
|
523
|
+
|
|
524
|
+
| Field | Type | Description |
|
|
525
|
+
|-----------|----------------|------------------------------------------|
|
|
526
|
+
| `chainId` | `number` | Chain ID the rewards are from |
|
|
527
|
+
| `rewards` | `MerklReward[]`| Pending rewards, one entry per token |
|
|
528
|
+
|
|
529
|
+
**`MerklReward`**
|
|
530
|
+
|
|
531
|
+
| Field | Type | Description |
|
|
532
|
+
|-------------|--------------------|------------------------------------------------------|
|
|
533
|
+
| `token` | `MerklRewardToken` | Reward token info (`address`, `symbol`, `decimals`) |
|
|
534
|
+
| `amount` | `number` | Cumulative claimable amount (human-readable) |
|
|
535
|
+
| `rawAmount` | `string` | Cumulative claimable amount as a raw string (pass to `BigInt` for contract args) |
|
|
536
|
+
| `proofs` | `string[]` | Merkle proofs required for on-chain claiming |
|
|
537
|
+
|
|
538
|
+
> Merkl updates reward distributions roughly every 2 hours.
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
### `earn.incentiveRewards.claim()` → `MerklClaimResult`
|
|
543
|
+
|
|
544
|
+
Claim all pending Merkl rewards for the signer in a single transaction.
|
|
545
|
+
|
|
546
|
+
```ts
|
|
547
|
+
const { txHash, tokenCount } = await stable.earn.incentiveRewards.claim();
|
|
548
|
+
console.log(`Claimed ${tokenCount} reward token(s). Tx: ${txHash}`);
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Throws `StableValidationError` if there are no pending rewards to claim.
|
|
552
|
+
|
|
553
|
+
**`MerklClaimResult`**
|
|
554
|
+
|
|
555
|
+
| Field | Type | Description |
|
|
556
|
+
|--------------|----------|------------------------------------------|
|
|
557
|
+
| `txHash` | `string` | Transaction hash |
|
|
558
|
+
| `tokenCount` | `number` | Number of distinct reward tokens claimed |
|
|
559
|
+
|
|
560
|
+
#### CORS in browser environments
|
|
561
|
+
|
|
562
|
+
Merkl's API does not send `Access-Control-Allow-Origin` headers for all chains. When calling from a browser, proxy the Merkl API through your own server and pass the proxy URL via `merklApiBase`:
|
|
563
|
+
|
|
564
|
+
```ts
|
|
565
|
+
// In your backend — forward /merkl-proxy/* to https://api.merkl.xyz/*
|
|
566
|
+
const stable = createStable({
|
|
567
|
+
merklApiBase: "/merkl-proxy",
|
|
568
|
+
// ...
|
|
569
|
+
});
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
With Vite, add a dev-server proxy to `vite.config.ts`:
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
server: {
|
|
576
|
+
proxy: {
|
|
577
|
+
"/merkl-proxy": {
|
|
578
|
+
target: "https://api.merkl.xyz",
|
|
579
|
+
changeOrigin: true,
|
|
580
|
+
rewrite: (path) => path.replace(/^\/merkl-proxy/, ""),
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
### `createStableReader(config)` — read vault state
|
|
589
|
+
|
|
590
|
+
Read-only client for fetching position, yield, and rewards data — no wallet required.
|
|
591
|
+
|
|
592
|
+
```ts
|
|
593
|
+
import { createStableReader, Network, STABLE_VAULT_ADDRESS } from "@stablechain/sdk";
|
|
594
|
+
|
|
595
|
+
const reader = createStableReader({
|
|
596
|
+
network: Network.Mainnet,
|
|
597
|
+
earn: { vault: STABLE_VAULT_ADDRESS },
|
|
598
|
+
address: "0xYourAddress",
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// Current vault position
|
|
602
|
+
const { assets, sharesFormatted, shareDecimals } = await reader.earn.position();
|
|
603
|
+
console.log("Position:", assets, "USDT0");
|
|
604
|
+
console.log("Shares:", sharesFormatted); // human-readable share balance
|
|
605
|
+
|
|
606
|
+
// APY breakdown — native yield + per-token reward boosts
|
|
607
|
+
const { apy, native, rewards, performanceFee } = await reader.earn.getYield();
|
|
608
|
+
console.log("Net APY:", (apy * 100).toFixed(2) + "%");
|
|
609
|
+
console.log("Native:", (native * 100).toFixed(2) + "%");
|
|
610
|
+
rewards.forEach((r) => console.log(`${r.symbol} boost:`, (r.apr * 100).toFixed(2) + "%"));
|
|
611
|
+
|
|
612
|
+
// Projected yield over a horizon
|
|
613
|
+
const { projectedYield } = await reader.earn.preview({ amount: 1000, horizonDays: 365 });
|
|
614
|
+
console.log("Projected yield over 1 year:", projectedYield, "USDT0");
|
|
615
|
+
|
|
616
|
+
// Check instant withdrawability
|
|
617
|
+
const { isInstant, availableLiquidity } = await reader.earn.withdrawability({ amount: 500 });
|
|
618
|
+
|
|
619
|
+
// Pending Merkl rewards (read-only, no signer needed)
|
|
620
|
+
const { rewards: incentives } = await reader.earn.incentiveRewards.fetch();
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**`StableReaderConfig`**
|
|
624
|
+
|
|
625
|
+
| Param | Type | Description |
|
|
626
|
+
|----------------|----------|----------------------------------------------------------------------|
|
|
627
|
+
| `network` | `Network`| `Network.Mainnet` (default) or `Network.Testnet` |
|
|
628
|
+
| `rpc` | `string` | *(optional)* RPC override |
|
|
629
|
+
| `earn` | `object` | *(optional)* `{ vault: "0x..." }` — required for `reader.earn.*` |
|
|
630
|
+
| `address` | `string` | Address of the user whose position to read |
|
|
631
|
+
| `merklApiBase` | `string` | *(optional)* Base URL for the Merkl rewards API — see `StableConfig.merklApiBase` |
|
|
632
|
+
|
|
633
|
+
**`reader.earn` methods**
|
|
634
|
+
|
|
635
|
+
| Method | Returns | Description |
|
|
636
|
+
|------------------------------------|------------------|---------------------------------------------------------|
|
|
637
|
+
| `getYield()` | `VaultYield` | Current APY breakdown — native yield + per-token rewards |
|
|
638
|
+
| `position()` | `VaultPosition` | User's current shares and asset value |
|
|
639
|
+
| `preview({ amount, horizonDays })` | `YieldPreview` | Projected yield over a given horizon using live net APY |
|
|
640
|
+
| `withdrawability({ amount })` | `WithdrawStatus` | Whether the amount can be withdrawn from idle liquidity |
|
|
641
|
+
| `incentiveRewards.fetch()` | `MerklRewardsResult` | Pending Merkl rewards for the configured address |
|
|
642
|
+
|
|
643
|
+
**`VaultPosition`**
|
|
644
|
+
|
|
645
|
+
| Field | Type | Description |
|
|
646
|
+
|-------------------|----------|----------------------------------------------------|
|
|
647
|
+
| `assets` | `number` | Position value in USDT0 (human-readable) |
|
|
648
|
+
| `shares` | `bigint` | Raw vault share balance |
|
|
649
|
+
| `sharesFormatted` | `number` | Human-readable share balance (use this for `redeem`) |
|
|
650
|
+
| `shareDecimals` | `number` | Vault share token decimals |
|
|
651
|
+
| `tokenDecimals` | `number` | Underlying asset decimals |
|
|
652
|
+
| `assetAddress` | `string` | Underlying asset contract address |
|
|
653
|
+
|
|
654
|
+
**`VaultYield`**
|
|
655
|
+
|
|
656
|
+
| Field | Type | Description |
|
|
657
|
+
|------------------|---------------------|----------------------------------------------------------------------|
|
|
658
|
+
| `apy` | `number` | Total net APY after fees, including all rewards (e.g. `0.072` = 7.2%) |
|
|
659
|
+
| `native` | `number` | Base lending yield after fees, excluding reward boosts |
|
|
660
|
+
| `rewards` | `VaultYieldReward[]`| Per-token reward boosts (empty when no active incentive programs) |
|
|
661
|
+
| `performanceFee` | `number` | Performance fee as a decimal (e.g. `0.05` = 5%) |
|
|
662
|
+
| `managementFee` | `number` | Management fee as a decimal |
|
|
663
|
+
|
|
664
|
+
**`VaultYieldReward`**
|
|
665
|
+
|
|
666
|
+
| Field | Type | Description |
|
|
667
|
+
|-----------|----------|------------------------------------------------------|
|
|
668
|
+
| `symbol` | `string` | Reward token symbol (e.g. `"MORPHO"`) |
|
|
669
|
+
| `address` | `string` | Reward token contract address |
|
|
670
|
+
| `apr` | `number` | APR contribution from this token (e.g. `0.02` = 2%) |
|
|
671
|
+
|
|
672
|
+
**`YieldPreview`**
|
|
673
|
+
|
|
674
|
+
| Field | Type | Description |
|
|
675
|
+
|------------------|----------|-----------------------------------------|
|
|
676
|
+
| `apy` | `number` | Current net APY (e.g. `0.065` = 6.5%) |
|
|
677
|
+
| `projectedYield` | `number` | Expected yield over `horizonDays` |
|
|
678
|
+
| `horizonDays` | `number` | Horizon used for the projection |
|
|
679
|
+
|
|
680
|
+
> Earn is only available on Mainnet (`Network.Mainnet`).
|
|
681
|
+
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
## Supported chains
|
|
685
|
+
|
|
686
|
+
`bridge` and `quoteBridge` accept any of these via the `Chain` enum:
|
|
687
|
+
|
|
688
|
+
| Enum | Network | Chain ID | USDT decimals |
|
|
689
|
+
|-----------------------|------------------|-----------|---------------|
|
|
690
|
+
| `Chain.Sepolia` | Ethereum Sepolia | 11155111 | 6 |
|
|
691
|
+
| `Chain.StableTestnet` | Stable Testnet | 2201 | 6 |
|
|
692
|
+
| `Chain.Stable` | Stable Mainnet | 988 | 6 |
|
|
693
|
+
| `Chain.Ethereum` | Ethereum | 1 | 6 |
|
|
694
|
+
| `Chain.Arbitrum` | Arbitrum One | 42161 | 6 |
|
|
695
|
+
| `Chain.Ink` | Ink | 57073 | 6 |
|
|
696
|
+
| `Chain.Bera` | Berachain | 80094 | 6 |
|
|
697
|
+
| `Chain.MegaETH` | MegaETH | 4326 | 6 |
|
|
698
|
+
| `Chain.Base` | Base | 8453 | 6 |
|
|
699
|
+
| `Chain.BSC` | BSC | 56 | 18 |
|
|
700
|
+
| `Chain.HyperEVM` | HyperEVM | 999 | 6 |
|
|
701
|
+
| `Chain.Polygon` | Polygon | 137 | 6 |
|
|
702
|
+
| `Chain.Plasma` | Plasma | 9745 | 6 |
|
|
703
|
+
| `Chain.Optimism` | Optimism | 10 | 6 |
|
|
704
|
+
| `Chain.Unichain` | Unichain | 130 | 6 |
|
|
705
|
+
| `Chain.Sei` | Sei | 1329 | 6 |
|
|
706
|
+
| `Chain.Flare` | Flare | 14 | 6 |
|
|
707
|
+
| `Chain.XLayer` | X Layer | 196 | 6 |
|
|
708
|
+
| `Chain.Rootstock` | Rootstock | 30 | 6 |
|
|
709
|
+
|
|
710
|
+
Per-chain RPC and USDT contract addresses are exported as `CHAIN_CONFIGS` (`ChainConfig` type) from the package.
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
|
|
714
|
+
## Errors
|
|
715
|
+
|
|
716
|
+
All thrown errors extend `StableError` (which extends viem's `BaseError`). Catch by class to branch on the failure mode.
|
|
717
|
+
|
|
718
|
+
| Class | Thrown when |
|
|
719
|
+
|---------------------------|------------------------------------------------------------------------------|
|
|
720
|
+
| `StableValidationError` | Invalid input (bad address, non-positive amount, unsupported chain, missing signer). Carries `field` and `value`. |
|
|
721
|
+
| `StableQuoteError` | LI.FI quote rejected (4xx) or returned an unexpected shape / chain mismatch. Carries `provider`, `httpStatus`, `providerCode`, `body`. |
|
|
722
|
+
| `StableTransactionError` | Wallet or on-chain failure. Carries `phase: "switch_chain" \| "approve" \| "send" \| "receipt" \| "sign"`, optional `txHash`, `chainId`. |
|
|
723
|
+
| `StableNetworkError` | Transport-level failure reaching an external API (LI.FI, Merkl) after retries. |
|
|
724
|
+
|
|
725
|
+
LI.FI quote fetches retry automatically on 429 and 5xx responses with exponential backoff before surfacing.
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
|
|
729
|
+
## Testnet
|
|
730
|
+
|
|
731
|
+
```ts
|
|
732
|
+
import { createStable, Network } from "@stablechain/sdk";
|
|
733
|
+
import { custom } from "viem";
|
|
734
|
+
|
|
735
|
+
const stable = createStable({
|
|
736
|
+
network: Network.Testnet,
|
|
737
|
+
transport: custom(window.ethereum),
|
|
738
|
+
});
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
---
|
|
742
|
+
|
|
743
|
+
## Examples
|
|
744
|
+
|
|
745
|
+
Everything lives in [`examples/core`](../../examples/core). From the repo root:
|
|
746
|
+
|
|
747
|
+
```bash
|
|
748
|
+
cd examples/core
|
|
749
|
+
bun install
|
|
750
|
+
|
|
751
|
+
# React + Vite app — transfer, bridge (with quote), swap, and earn
|
|
752
|
+
# (deposit / withdraw / redeem / bulk deposit / incentive rewards)
|
|
753
|
+
bun dev
|
|
754
|
+
|
|
755
|
+
# Zero-setup Node script — the ERC-3009 sign → verify → settle flow
|
|
756
|
+
bun run erc3009
|
|
757
|
+
```
|