@stablechain/sdk 1.0.7 → 1.0.9

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 CHANGED
@@ -129,7 +129,13 @@ const { txHash } = await stable.transfer({
129
129
  });
130
130
  ```
131
131
 
132
- **ERC-20 (USDC, EURC, …)**
132
+ **ERC-20 (PYUSD, USDC, EURC, …)**
133
+
134
+ ```ts
135
+ import { STABLE_PYUSD_ADDRESS } from "@stablechain/sdk";
136
+
137
+ await stable.transfer({ to: "0xRecipient", amount: 25, token: STABLE_PYUSD_ADDRESS, tokenDecimals: 6 });
138
+ ```
133
139
 
134
140
  ```ts
135
141
  const { txHash } = await stable.transfer({
@@ -254,8 +260,9 @@ automatically. Switches the wallet to the source chain (MetaMask will prompt) be
254
260
  sending.
255
261
 
256
262
  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
263
+ Sepolia on testnet. Every other deployment burns from the caller, so no approve and no
264
+ allowance read is issued there. That includes PYUSD on Ethereum, which mints and burns
265
+ through the token instead of escrowing it. Where one is needed and the current
259
266
  allowance is short but not zero, it takes **two** approvals: legacy USDT refuses to
260
267
  overwrite a non-zero allowance with another non-zero value, so the SDK sends
261
268
  `approve(0)` first.
@@ -337,7 +344,7 @@ one of them. Alongside them:
337
344
  | Field | Type | What it is |
338
345
  |-----------|-----------------|---------------------------------------------------------------------|
339
346
  | `chainId` | `number` | The source chain every step must be sent on |
340
- | `asset` | `"usdt0" \| "xaut0"` | Which OFT mesh this send moves through |
347
+ | `asset` | `"usdt0" \| "xaut0" \| "pyusd"` | Which OFT mesh this send moves through |
341
348
  | `quote` | `BridgeQuote` | The quote the steps were built from — supplied or freshly read |
342
349
  | `send` | `object` | The `send` step's arguments decoded: `dstEid`, `recipient`, `to`, `amountLD`, `minAmountLD`, `extraOptions`, `nativeFee`, `lzTokenFee`, `refundAddress` |
343
350
 
@@ -381,7 +388,7 @@ addresses name, LI.FI otherwise. Forcing `"oft"` on an unsupported pair throws
381
388
 
382
389
  #### Native OFT assets
383
390
 
384
- Two assets have their own OFT mesh, each with its own adapter and token address per
391
+ Each OFT asset has its own mesh, with its own adapter and token address per
385
392
  chain. A bridge stays within one mesh: the OFT mints its own peer on the far side, so it
386
393
  cannot hand back a different token. A cross-asset request (USDT0 in, XAUt0 out) is a
387
394
  trade, not a bridge, and `"auto"` sends it to LI.FI.
@@ -409,6 +416,27 @@ chain burns from the caller and skips the approval transaction entirely.
409
416
 
410
417
  BSC is XAUt0-only: it has no USDT0 deployment, so a BSC USDT bridge routes to LI.FI.
411
418
 
419
+ **PYUSD** (PayPal USD, 6 decimals) — Stable and Ethereum. The OFT is live between those
420
+ two chains only, so `"auto"` takes it for Ethereum ↔ Stable and sends every other PYUSD
421
+ pair to LI.FI.
422
+
423
+ ```ts
424
+ // PYUSD: Ethereum → Stable, over the native OFT mesh
425
+ await stable.bridge({
426
+ fromChain: Chain.Ethereum,
427
+ toChain: Chain.Stable,
428
+ fromToken: "0x6c3ea9036406852006290770BEdFcAbA0e23A0e8", // PYUSD on Ethereum
429
+ toToken: STABLE_PYUSD_ADDRESS,
430
+ amount: 100, // 100 PYUSD
431
+ });
432
+ ```
433
+
434
+ Neither side is a lockbox: both adapters mint and burn through PYUSD itself, so no
435
+ approval is sent, even on Ethereum. Each direction is rate-limited by the adapter
436
+ (100,000,000 PYUSD per 24 hours as of 2026-10-27), and a send past the limit reverts.
437
+ From other chains, LI.FI had no PYUSD liquidity on Stable when last checked (2026-10-22),
438
+ so those bridges fail with "no available quotes".
439
+
412
440
  Addresses per chain are in `BRIDGE_CHAIN_CONFIGS[chain].tokens`, keyed by asset:
413
441
 
414
442
  ```ts
@@ -419,7 +447,7 @@ stableChain.tokens.xaut0?.address; // XAUt0 ERC-20 on Stable
419
447
  stableChain.tokens.xaut0?.oftAdapter; // its OFT
420
448
 
421
449
  // Which asset an address refers to on a chain, if any
422
- bridgeAssetFor(stableChain, someToken); // "usdt0" | "xaut0" | undefined
450
+ bridgeAssetFor(stableChain, someToken); // "usdt0" | "xaut0" | "pyusd" | undefined
423
451
  ```
424
452
 
425
453
  ---
@@ -455,44 +483,57 @@ Nearly all of the wait is **source-chain finality**: the DVNs securing the pathw
455
483
  not attest until the send transaction is buried under `confirmations` source blocks.
456
484
  Destination execution is a few blocks on top.
457
485
 
458
- The SDK resolves this from two independent inputs and reports both:
486
+ `estimateBridgeTime` reports that breakdown:
459
487
 
460
488
  | Field | Meaning |
461
489
  |-------------------|--------------------------------------------------------------------------------------------------|
462
- | `seconds` | Best available estimate — display this |
463
- | `source` | `"observed"` or `"formula"`, whichever produced `seconds` |
490
+ | `seconds` | The estimate — display this |
491
+ | `source` | `"formula"`, `"floor"`, or `"observed"` — which input produced `seconds` |
464
492
  | `observedSeconds` | Median delivery time of recent messages on this exact pathway, from LayerZero Scan |
465
493
  | `formulaSeconds` | LayerZero's documented estimate: `srcBlockTime × confirmations + dstBlockTime × (2 + dvnCount)` |
466
494
  | `confirmations` | Source blocks the DVNs wait for, read live from the pathway's ULN config |
467
495
  | `dvnCount` | DVNs verifying the pathway |
468
496
 
469
- `observed` wins when available. The formula assumes block confirmations are the whole
470
- wait, which holds for L1 sources but understates rollups badly — Arbitrum→Stable computes
471
- to ~19 s (60 blocks × 0.25 s) against a real median of ~18 min, because the DVNs wait for
472
- the L2 batch to finalize on Ethereum.
497
+ `seconds` comes from the formula — the pathway's live ULN config is what the DVNs are
498
+ actually bound by, so a config change is reflected the moment it lands. On every pathway
499
+ that is purely confirmation-bound it agrees with both usdt0.to's own estimate and the
500
+ delivery times LayerZero Scan reports to well under a percent.
501
+
502
+ Some source chains carry a floor. DVN attestation and destination execution sit outside the
503
+ formula, and on Ethereum and Arbitrum the DVNs additionally hold until the send finalizes on
504
+ the beacon chain. Where confirmations are high that overhead vanishes into the rounding;
505
+ where they are low it dominates — Arbitrum's 60 confirmations compute to ~19 s against a real
506
+ ~18 min. Those chains are floored at a measured minimum, and `source` reports `"floor"`.
507
+
508
+ `observedSeconds` is fetched alongside and reported for comparison, but does not drive
509
+ `seconds`: a 50-message window can reach back months, so on a pathway that has been
510
+ reconfigured or has gone quiet it answers from a config that no longer exists.
473
511
 
474
512
  Pathway configs and observed medians are cached for 5 minutes; call
475
513
  `clearBridgeEstimateCache()` to force a refetch.
476
514
 
477
- Roughly what to expect on mainnet, measured 2026-07-30:
515
+ Roughly what to expect on mainnet, from configs read 2026-08-27:
478
516
 
479
517
  | Pathway | Confirmations | Typical delivery |
480
518
  |---------------------|---------------|------------------|
481
- | Polygon → Stable | 32 | ~1 min |
482
- | Bera → Stable | 60 | ~2 min |
483
- | Unichain → Stable | 1600 | ~8 min |
484
- | Flare → Stable | 500 | ~11 min |
485
- | Optimism → Stable | 1000 | ~15 min |
486
- | Ethereum → Stable | 65 | ~17 min |
519
+ | Polygon → Stable | 32 | ~1 min ᶠ |
520
+ | Bera → Stable | 60 | ~2 min ᶠ |
521
+ | Flare → Stable | 500 | ~10 min ᶠ |
487
522
  | Sei → Stable | 2000 | ~17 min |
488
- | Arbitrum → Stable | 60 | ~18 min |
523
+ | Ethereum → Stable | 65 | ~19.5 min ᶠ |
524
+ | Arbitrum → Stable | 60 | ~22.5 min ᶠ |
525
+ | Unichain → Stable | 1600 | ~27 min |
489
526
  | Plasma → Stable | 1800 | ~30 min |
527
+ | Optimism → Stable | 1000 | ~33 min |
490
528
  | Stable → any | 3600 | ~42 min |
491
529
  | Rootstock → Stable | 120 | ~62 min |
492
530
  | MegaETH → Stable | 5400 | ~90 min |
493
531
  | X Layer → Stable | 9000 | ~2.5 h |
494
532
  | HyperEVM → Stable | 43200 | ~12 h |
495
533
 
534
+ ᶠ Floored at the source chain's measured minimum, because its confirmation count is not the
535
+ whole wait — Ethereum and Arbitrum would otherwise compute to ~13 min and ~19 s.
536
+
496
537
  ---
497
538
 
498
539
  ### `trackBridge(srcTxHash)` → `BridgeStatus`
@@ -618,6 +659,21 @@ if (check.ok) await stable.authorization.transferWithAuthorization({ signed });
618
659
 
619
660
  `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.
620
661
 
662
+ PYUSD on Stable implements ERC-3009 too. Pass `token: STABLE_PYUSD_ADDRESS` and the same
663
+ actions work with no domain override, because the `name()` + `"1"` fallback matches its
664
+ on-chain `DOMAIN_SEPARATOR`:
665
+
666
+ ```ts
667
+ import { STABLE_PYUSD_ADDRESS } from "@stablechain/sdk";
668
+
669
+ const signed = await stable.authorization.signTransferAuthorization({
670
+ token: STABLE_PYUSD_ADDRESS,
671
+ to: "0xVendor",
672
+ value: 1_250_000n, // 1.25 PYUSD (6 dp)
673
+ validBefore: BigInt(Math.floor(Date.now() / 1000) + 3600),
674
+ });
675
+ ```
676
+
621
677
  To relay gaslessly, pass `prepareTransferWithAuthorization`'s output to the [`@stablechain/enterprise`](../enterprise) gas-waiver rail.
622
678
 
623
679
  > **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.
@@ -640,6 +696,23 @@ const stable = createStable({
640
696
  const { txHash } = await stable.earn.deposit({ amount: 100 }); // 100 USDT0
641
697
  ```
642
698
 
699
+ Earn works with any Morpho V2 vault, not just USDT0. The SDK reads the vault's own
700
+ `asset()` and that token's decimals, and approves that token, so a PYUSD vault needs only
701
+ its address. `deposit` and `bulkDeposit` accept `vault` per call. Withdraw, redeem and
702
+ the reader always use the vault set in config, so manage a PYUSD position with a client
703
+ configured for that vault:
704
+
705
+ ```ts
706
+ const pyusdEarn = createStable({
707
+ network: Network.Mainnet,
708
+ earn: { vault: PYUSD_VAULT },
709
+ walletClient: myWalletClient,
710
+ });
711
+
712
+ await pyusdEarn.earn.deposit({ amount: 100 }); // 100 PYUSD
713
+ await pyusdEarn.earn.withdraw({ amount: 40 }); // 40 PYUSD back
714
+ ```
715
+
643
716
  | Param | Type | Description |
644
717
  |---------------------|----------|-------------------------------------------------------------------|
645
718
  | `amount` | `number` | Amount in human-readable units |
@@ -122,6 +122,7 @@ import { signTypedData } from "viem/actions";
122
122
 
123
123
  // src/constants.ts
124
124
  var STABLE_USDT_ADDRESS = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
125
+ var STABLE_PYUSD_ADDRESS = "0x28F1f38a9F7dB1f769A71aF0a13E8BACe2ab0E9f";
125
126
  var STABLE_VAULT_ADDRESS = "0xb7Df8db22A5DBBFA9ebeb94b3910aec6a4f05c08";
126
127
 
127
128
  // src/errors.ts
@@ -649,9 +650,10 @@ export {
649
650
  StableTransactionError,
650
651
  StableNetworkError,
651
652
  StableBulkError,
652
- ERC3009_ABI,
653
653
  STABLE_USDT_ADDRESS,
654
+ STABLE_PYUSD_ADDRESS,
654
655
  STABLE_VAULT_ADDRESS,
656
+ ERC3009_ABI,
655
657
  signTransferAuthorization,
656
658
  signReceiveAuthorization,
657
659
  transferWithAuthorization,
@@ -122,6 +122,7 @@ var _actions = require('viem/actions');
122
122
 
123
123
  // src/constants.ts
124
124
  var STABLE_USDT_ADDRESS = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
125
+ var STABLE_PYUSD_ADDRESS = "0x28F1f38a9F7dB1f769A71aF0a13E8BACe2ab0E9f";
125
126
  var STABLE_VAULT_ADDRESS = "0xb7Df8db22A5DBBFA9ebeb94b3910aec6a4f05c08";
126
127
 
127
128
  // src/errors.ts
@@ -662,4 +663,5 @@ function erc3009Actions(client) {
662
663
 
663
664
 
664
665
 
665
- exports.StableError = StableError; exports.StableValidationError = StableValidationError; exports.StableQuoteError = StableQuoteError; exports.StableTransactionError = StableTransactionError; exports.StableNetworkError = StableNetworkError; exports.StableBulkError = StableBulkError; exports.ERC3009_ABI = ERC3009_ABI; exports.STABLE_USDT_ADDRESS = STABLE_USDT_ADDRESS; exports.STABLE_VAULT_ADDRESS = STABLE_VAULT_ADDRESS; exports.signTransferAuthorization = signTransferAuthorization; exports.signReceiveAuthorization = signReceiveAuthorization; exports.transferWithAuthorization = transferWithAuthorization; exports.receiveWithAuthorization = receiveWithAuthorization; exports.cancelAuthorization = cancelAuthorization; exports.prepareTransferWithAuthorization = prepareTransferWithAuthorization; exports.verifyAuthorization = verifyAuthorization; exports.getAuthorizationState = getAuthorizationState; exports.getAuthorizationUsage = getAuthorizationUsage; exports.erc3009Actions = erc3009Actions;
666
+
667
+ exports.StableError = StableError; exports.StableValidationError = StableValidationError; exports.StableQuoteError = StableQuoteError; exports.StableTransactionError = StableTransactionError; exports.StableNetworkError = StableNetworkError; exports.StableBulkError = StableBulkError; exports.STABLE_USDT_ADDRESS = STABLE_USDT_ADDRESS; exports.STABLE_PYUSD_ADDRESS = STABLE_PYUSD_ADDRESS; exports.STABLE_VAULT_ADDRESS = STABLE_VAULT_ADDRESS; exports.ERC3009_ABI = ERC3009_ABI; exports.signTransferAuthorization = signTransferAuthorization; exports.signReceiveAuthorization = signReceiveAuthorization; exports.transferWithAuthorization = transferWithAuthorization; exports.receiveWithAuthorization = receiveWithAuthorization; exports.cancelAuthorization = cancelAuthorization; exports.prepareTransferWithAuthorization = prepareTransferWithAuthorization; exports.verifyAuthorization = verifyAuthorization; exports.getAuthorizationState = getAuthorizationState; exports.getAuthorizationUsage = getAuthorizationUsage; exports.erc3009Actions = erc3009Actions;
package/dist/erc3009.cjs CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
 
12
12
 
13
- var _chunkQQ26UI3Fcjs = require('./chunk-QQ26UI3F.cjs');
13
+ var _chunkMMZ7HVWZcjs = require('./chunk-MMZ7HVWZ.cjs');
14
14
 
15
15
 
16
16
 
@@ -23,4 +23,4 @@ var _chunkQQ26UI3Fcjs = require('./chunk-QQ26UI3F.cjs');
23
23
 
24
24
 
25
25
 
26
- exports.ERC3009_ABI = _chunkQQ26UI3Fcjs.ERC3009_ABI; exports.cancelAuthorization = _chunkQQ26UI3Fcjs.cancelAuthorization; exports.erc3009Actions = _chunkQQ26UI3Fcjs.erc3009Actions; exports.getAuthorizationState = _chunkQQ26UI3Fcjs.getAuthorizationState; exports.getAuthorizationUsage = _chunkQQ26UI3Fcjs.getAuthorizationUsage; exports.prepareTransferWithAuthorization = _chunkQQ26UI3Fcjs.prepareTransferWithAuthorization; exports.receiveWithAuthorization = _chunkQQ26UI3Fcjs.receiveWithAuthorization; exports.signReceiveAuthorization = _chunkQQ26UI3Fcjs.signReceiveAuthorization; exports.signTransferAuthorization = _chunkQQ26UI3Fcjs.signTransferAuthorization; exports.transferWithAuthorization = _chunkQQ26UI3Fcjs.transferWithAuthorization; exports.verifyAuthorization = _chunkQQ26UI3Fcjs.verifyAuthorization;
26
+ exports.ERC3009_ABI = _chunkMMZ7HVWZcjs.ERC3009_ABI; exports.cancelAuthorization = _chunkMMZ7HVWZcjs.cancelAuthorization; exports.erc3009Actions = _chunkMMZ7HVWZcjs.erc3009Actions; exports.getAuthorizationState = _chunkMMZ7HVWZcjs.getAuthorizationState; exports.getAuthorizationUsage = _chunkMMZ7HVWZcjs.getAuthorizationUsage; exports.prepareTransferWithAuthorization = _chunkMMZ7HVWZcjs.prepareTransferWithAuthorization; exports.receiveWithAuthorization = _chunkMMZ7HVWZcjs.receiveWithAuthorization; exports.signReceiveAuthorization = _chunkMMZ7HVWZcjs.signReceiveAuthorization; exports.signTransferAuthorization = _chunkMMZ7HVWZcjs.signTransferAuthorization; exports.transferWithAuthorization = _chunkMMZ7HVWZcjs.transferWithAuthorization; exports.verifyAuthorization = _chunkMMZ7HVWZcjs.verifyAuthorization;
package/dist/erc3009.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  signTransferAuthorization,
11
11
  transferWithAuthorization,
12
12
  verifyAuthorization
13
- } from "./chunk-VS6GBRDF.js";
13
+ } from "./chunk-FN7OKBRH.js";
14
14
  export {
15
15
  ERC3009_ABI,
16
16
  cancelAuthorization,