@stablechain/sdk 0.1.1 → 1.0.1

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/dist/index.js CHANGED
@@ -1,13 +1,25 @@
1
1
  // src/stable.ts
2
2
  import {
3
- createPublicClient as createPublicClient2,
4
- createWalletClient as createWalletClient2,
5
- erc20Abi as erc20Abi2,
6
- http as http2,
7
- isAddress as isAddress2,
8
- parseUnits as parseUnits2
3
+ createPublicClient as createPublicClient4,
4
+ createWalletClient as createWalletClient3,
5
+ erc20Abi as erc20Abi4,
6
+ http as http4,
7
+ isAddress as isAddress3,
8
+ parseUnits as parseUnits4
9
+ } from "viem";
10
+ import { stable as stableMainnet2, stableTestnet as stableTestnet2 } from "viem/chains";
11
+
12
+ // src/earn/reader.ts
13
+ import {
14
+ createPublicClient,
15
+ erc20Abi,
16
+ formatUnits,
17
+ http,
18
+ isAddress,
19
+ parseUnits
9
20
  } from "viem";
10
21
  import { stable as stableMainnet, stableTestnet } from "viem/chains";
22
+ import { MorphoClient } from "@morpho-org/morpho-sdk";
11
23
 
12
24
  // src/errors.ts
13
25
  import { BaseError } from "viem";
@@ -67,6 +79,14 @@ var StableNetworkError = class extends StableError {
67
79
  this.url = args.url;
68
80
  }
69
81
  };
82
+ var StableBulkError = class extends StableError {
83
+ constructor(args) {
84
+ super(args.shortMessage, { name: "StableBulkError" });
85
+ this.results = args.results;
86
+ this.succeeded = args.succeeded;
87
+ this.failed = args.failed;
88
+ }
89
+ };
70
90
  function formatValue(v) {
71
91
  if (typeof v === "bigint") return `${v}n`;
72
92
  if (typeof v === "string") return JSON.stringify(v);
@@ -79,17 +99,6 @@ function formatValue(v) {
79
99
  }
80
100
  }
81
101
 
82
- // src/services/lifi.ts
83
- import {
84
- createPublicClient,
85
- createWalletClient,
86
- erc20Abi,
87
- formatUnits,
88
- http,
89
- isAddress,
90
- parseUnits
91
- } from "viem";
92
-
93
102
  // src/helpers/chain.ts
94
103
  import { defineChain } from "viem";
95
104
  function makeChain(id, rpc) {
@@ -154,6 +163,177 @@ var Network = /* @__PURE__ */ ((Network2) => {
154
163
  return Network2;
155
164
  })(Network || {});
156
165
 
166
+ // src/earn/reader.ts
167
+ var MORPHO_API_URL = "https://blue-api.morpho.org/graphql";
168
+ var EarnReader = class {
169
+ constructor(config) {
170
+ if (!config.earn?.vault) {
171
+ throw new StableValidationError(
172
+ "earn.vault",
173
+ void 0,
174
+ "createStableReader: earn.vault is required"
175
+ );
176
+ }
177
+ if (!isAddress(config.address)) {
178
+ throw new StableValidationError(
179
+ "address",
180
+ config.address,
181
+ `createStableReader: invalid user address "${config.address}"`
182
+ );
183
+ }
184
+ if (!isAddress(config.earn.vault)) {
185
+ throw new StableValidationError(
186
+ "earn.vault",
187
+ config.earn.vault,
188
+ `createStableReader: invalid vault address "${config.earn.vault}"`
189
+ );
190
+ }
191
+ const network = config.network ?? "mainnet" /* Mainnet */;
192
+ const viemChain = network === "mainnet" /* Mainnet */ ? stableMainnet : stableTestnet;
193
+ const rpc = (config.rpc ?? viemChain.rpcUrls.default.http[0]).replace(/\/$/, "");
194
+ this.chainId = viemChain.id;
195
+ this.vaultAddr = config.earn.vault;
196
+ this.userAddr = config.address;
197
+ const chain = makeChain(viemChain.id, rpc);
198
+ this.pc = createPublicClient({ chain, transport: http(rpc) });
199
+ this.morpho = new MorphoClient(this.pc);
200
+ }
201
+ async position() {
202
+ const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
203
+ const [vaultData, shares] = await Promise.all([
204
+ vault.getData(),
205
+ this.pc.readContract({
206
+ address: this.vaultAddr,
207
+ abi: erc20Abi,
208
+ functionName: "balanceOf",
209
+ args: [this.userAddr]
210
+ })
211
+ ]);
212
+ const [tokenDecimals, shareDecimals] = await Promise.all([
213
+ this.pc.readContract({ address: vaultData.asset, abi: erc20Abi, functionName: "decimals" }),
214
+ this.pc.readContract({ address: this.vaultAddr, abi: erc20Abi, functionName: "decimals" })
215
+ ]);
216
+ const assetsRaw = vaultData.toAssets(shares);
217
+ return {
218
+ shares,
219
+ sharesFormatted: Number(formatUnits(shares, shareDecimals)),
220
+ shareDecimals,
221
+ assets: Number(formatUnits(assetsRaw, tokenDecimals)),
222
+ tokenDecimals,
223
+ assetAddress: vaultData.asset
224
+ };
225
+ }
226
+ async getYield() {
227
+ return this._fetchYield();
228
+ }
229
+ async preview(params) {
230
+ const { amount, horizonDays } = params;
231
+ const apy = await this._fetchNetApy();
232
+ return {
233
+ projectedYield: amount * apy * (horizonDays / 365),
234
+ apy,
235
+ horizonDays
236
+ };
237
+ }
238
+ async withdrawability(params) {
239
+ const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
240
+ const vaultData = await vault.getData();
241
+ const tokenDecimals = params.tokenDecimals ?? await this.pc.readContract({
242
+ address: vaultData.asset,
243
+ abi: erc20Abi,
244
+ functionName: "decimals"
245
+ });
246
+ const requestedRaw = parseUnits(String(params.amount), tokenDecimals);
247
+ const availableLiquidityRaw = vaultData.assetBalance;
248
+ return {
249
+ isInstant: requestedRaw <= availableLiquidityRaw,
250
+ availableLiquidity: Number(formatUnits(availableLiquidityRaw, tokenDecimals)),
251
+ tokenDecimals
252
+ };
253
+ }
254
+ async _fetchNetApy() {
255
+ const { apy } = await this._fetchYield();
256
+ return apy;
257
+ }
258
+ async _fetchYield() {
259
+ const query = `
260
+ query VaultYield($address: String!, $chainId: Int!) {
261
+ vaultV2ByAddress(address: $address, chainId: $chainId) {
262
+ netApy
263
+ netApyExcludingRewards
264
+ performanceFee
265
+ managementFee
266
+ rewards {
267
+ asset { symbol address }
268
+ supplyApr
269
+ }
270
+ }
271
+ }
272
+ `;
273
+ let body;
274
+ try {
275
+ body = await retryWithBackoff(
276
+ async () => {
277
+ const res = await fetch(MORPHO_API_URL, {
278
+ method: "POST",
279
+ headers: { "Content-Type": "application/json" },
280
+ body: JSON.stringify({
281
+ query,
282
+ variables: { address: this.vaultAddr, chainId: this.chainId }
283
+ })
284
+ });
285
+ if (!res.ok) {
286
+ const retryable = res.status === 429 || res.status >= 500;
287
+ throw new HttpError(
288
+ `earn: Morpho API returned ${res.status} \u2014 ${res.statusText}`,
289
+ res.status,
290
+ retryable
291
+ );
292
+ }
293
+ return res.json().catch(() => void 0);
294
+ },
295
+ (err) => err instanceof HttpError ? err.retryable : err instanceof TypeError
296
+ );
297
+ } catch (err) {
298
+ if (err instanceof TypeError || err instanceof HttpError) {
299
+ throw new StableNetworkError({
300
+ shortMessage: err instanceof HttpError ? `earn: Morpho API returned ${err.status} after retries` : `earn: failed to reach Morpho API after retries \u2014 ${String(err)}`,
301
+ url: MORPHO_API_URL,
302
+ cause: err
303
+ });
304
+ }
305
+ throw err;
306
+ }
307
+ const vault = body?.data?.vaultV2ByAddress;
308
+ if (!vault || typeof vault.netApy !== "number") {
309
+ throw new StableValidationError(
310
+ "apy",
311
+ void 0,
312
+ "earn: vault not found in Morpho API or unexpected response shape"
313
+ );
314
+ }
315
+ const rewards = vault.rewards.filter((r) => typeof r.supplyApr === "number" && r.supplyApr > 0).map((r) => ({ symbol: r.asset.symbol, address: r.asset.address, apr: r.supplyApr }));
316
+ return {
317
+ apy: vault.netApy,
318
+ native: vault.netApyExcludingRewards ?? 0,
319
+ rewards,
320
+ performanceFee: vault.performanceFee,
321
+ managementFee: vault.managementFee
322
+ };
323
+ }
324
+ };
325
+
326
+ // src/services/lifi.ts
327
+ import {
328
+ createPublicClient as createPublicClient2,
329
+ createWalletClient,
330
+ erc20Abi as erc20Abi2,
331
+ formatUnits as formatUnits2,
332
+ http as http2,
333
+ isAddress as isAddress2,
334
+ parseUnits as parseUnits2
335
+ } from "viem";
336
+
157
337
  // src/chains.ts
158
338
  var CHAIN_CONFIGS = {
159
339
  // ─── Testnet ──────────────────────────────────────────────────────────────
@@ -236,7 +416,7 @@ var LiFiService = class {
236
416
  const { fromToken, toToken, amount, fromDecimals = 6, toAddress } = params;
237
417
  const from = this._requireFrom();
238
418
  const chain = this.config.chainId.toString();
239
- const fromAmountRaw = parseUnits(String(amount), fromDecimals).toString();
419
+ const fromAmountRaw = parseUnits2(String(amount), fromDecimals).toString();
240
420
  const qs = new URLSearchParams({
241
421
  fromChain: chain,
242
422
  toChain: chain,
@@ -267,7 +447,7 @@ var LiFiService = class {
267
447
  toChain: dst.id.toString(),
268
448
  fromToken,
269
449
  toToken,
270
- fromAmount: parseUnits(String(amount), fromDecimals).toString(),
450
+ fromAmount: parseUnits2(String(amount), fromDecimals).toString(),
271
451
  fromAddress: from,
272
452
  ...recipient && { toAddress: recipient }
273
453
  });
@@ -286,7 +466,7 @@ var LiFiService = class {
286
466
  if (!dst) {
287
467
  throw new StableValidationError("toChain", toChain, `LI.FI bridge: unsupported destination chain "${toChain}"`);
288
468
  }
289
- const fromAmountRaw = parseUnits(String(amount), fromDecimals).toString();
469
+ const fromAmountRaw = parseUnits2(String(amount), fromDecimals).toString();
290
470
  const qs = new URLSearchParams({
291
471
  fromChain: src.id.toString(),
292
472
  toChain: dst.id.toString(),
@@ -300,10 +480,10 @@ var LiFiService = class {
300
480
  return this._executeSwapQuote(quote, src.rpc, src.id);
301
481
  }
302
482
  _validateSwapParams({ fromToken, toToken, amount, toAddress }) {
303
- if (!isAddress(fromToken)) {
483
+ if (!isAddress2(fromToken)) {
304
484
  throw new StableValidationError("fromToken", fromToken, `swap: invalid fromToken address "${fromToken}"`);
305
485
  }
306
- if (!isAddress(toToken)) {
486
+ if (!isAddress2(toToken)) {
307
487
  throw new StableValidationError("toToken", toToken, `swap: invalid toToken address "${toToken}"`);
308
488
  }
309
489
  if (fromToken.toLowerCase() === toToken.toLowerCase()) {
@@ -312,21 +492,21 @@ var LiFiService = class {
312
492
  if (!Number.isFinite(amount) || amount <= 0) {
313
493
  throw new StableValidationError("amount", amount, "swap: amount must be a finite positive number");
314
494
  }
315
- if (toAddress !== void 0 && !isAddress(toAddress)) {
495
+ if (toAddress !== void 0 && !isAddress2(toAddress)) {
316
496
  throw new StableValidationError("toAddress", toAddress, `swap: invalid toAddress "${toAddress}"`);
317
497
  }
318
498
  }
319
499
  _validateBridgeParams({ fromChain, toChain, fromToken, toToken, amount, recipient }) {
320
- if (!isAddress(fromToken)) {
500
+ if (!isAddress2(fromToken)) {
321
501
  throw new StableValidationError("fromToken", fromToken, `LI.FI bridge: invalid fromToken address "${fromToken}"`);
322
502
  }
323
- if (!isAddress(toToken)) {
503
+ if (!isAddress2(toToken)) {
324
504
  throw new StableValidationError("toToken", toToken, `LI.FI bridge: invalid toToken address "${toToken}"`);
325
505
  }
326
506
  if (!Number.isFinite(amount) || amount <= 0) {
327
507
  throw new StableValidationError("amount", amount, "LI.FI bridge: amount must be a finite positive number");
328
508
  }
329
- if (recipient !== void 0 && !isAddress(recipient)) {
509
+ if (recipient !== void 0 && !isAddress2(recipient)) {
330
510
  throw new StableValidationError("recipient", recipient, `LI.FI bridge: invalid recipient address "${recipient}"`);
331
511
  }
332
512
  if (fromChain === toChain) {
@@ -394,7 +574,7 @@ var LiFiService = class {
394
574
  }
395
575
  _toSwapQuote(quote) {
396
576
  return {
397
- toAmount: Number(formatUnits(BigInt(quote.estimate.toAmount), quote.action.toToken.decimals)),
577
+ toAmount: Number(formatUnits2(BigInt(quote.estimate.toAmount), quote.action.toToken.decimals)),
398
578
  fromAmount: BigInt(quote.estimate.fromAmount),
399
579
  fromToken: quote.action.fromToken.address,
400
580
  approvalAddress: quote.estimate.approvalAddress,
@@ -417,7 +597,7 @@ var LiFiService = class {
417
597
  const wc = this.config.walletClient ?? createWalletClient({
418
598
  account: this.config.account,
419
599
  chain: srcChain,
420
- transport: this.config.transport ?? http(srcRpc)
600
+ transport: this.config.transport ?? http2(srcRpc)
421
601
  });
422
602
  try {
423
603
  await wc.switchChain({ id: srcChainId });
@@ -454,10 +634,10 @@ var LiFiService = class {
454
634
  const tokenAddr = quote.fromToken;
455
635
  const isNative = tokenAddr.toLowerCase() === "0x0000000000000000000000000000000000000000" || tokenAddr.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
456
636
  if (isNative || !quote.approvalAddress) return;
457
- const pc = createPublicClient({ chain: srcChain, transport: http(srcRpc) });
637
+ const pc = createPublicClient2({ chain: srcChain, transport: http2(srcRpc) });
458
638
  const allowance = await pc.readContract({
459
639
  address: tokenAddr,
460
- abi: erc20Abi,
640
+ abi: erc20Abi2,
461
641
  functionName: "allowance",
462
642
  args: [from, quote.approvalAddress]
463
643
  });
@@ -466,7 +646,7 @@ var LiFiService = class {
466
646
  try {
467
647
  approveHash = await wc.writeContract({
468
648
  address: tokenAddr,
469
- abi: erc20Abi,
649
+ abi: erc20Abi2,
470
650
  functionName: "approve",
471
651
  args: [quote.approvalAddress, quote.fromAmount],
472
652
  account: wc.account,
@@ -502,15 +682,387 @@ function isLiFiQuoteShape(v) {
502
682
  return isObject(tx) && typeof tx.to === "string" && typeof tx.data === "string" && typeof tx.chainId === "number" && isObject(est) && typeof est.fromAmount === "string" && typeof est.toAmount === "string" && isObject(act) && isObject(act.fromToken) && isObject(act.toToken);
503
683
  }
504
684
 
685
+ // src/earn/morpho.ts
686
+ import {
687
+ createPublicClient as createPublicClient3,
688
+ createWalletClient as createWalletClient2,
689
+ custom,
690
+ erc20Abi as erc20Abi3,
691
+ formatUnits as formatUnits3,
692
+ http as http3,
693
+ parseUnits as parseUnits3
694
+ } from "viem";
695
+ import {
696
+ MorphoClient as MorphoClient2,
697
+ isRequirementApproval,
698
+ isRequirementSignature
699
+ } from "@morpho-org/morpho-sdk";
700
+ var MorphoService = class {
701
+ constructor(config) {
702
+ this.config = config;
703
+ this.inFlight = /* @__PURE__ */ new Map();
704
+ this.chain = makeChain(config.chainId, config.rpc);
705
+ this.wc = config.walletClient ?? createWalletClient2({
706
+ account: config.account,
707
+ chain: this.chain,
708
+ transport: config.transport ?? http3(config.rpc)
709
+ });
710
+ const morphoWc = this.wc.chain?.id === config.chainId ? this.wc : createWalletClient2({
711
+ account: this.wc.account,
712
+ chain: this.chain,
713
+ transport: custom({ request: this.wc.request.bind(this.wc) })
714
+ });
715
+ this.morpho = new MorphoClient2(morphoWc, { supportSignature: true });
716
+ this.pc = createPublicClient3({ chain: this.chain, transport: http3(config.rpc) });
717
+ }
718
+ deposit(params) {
719
+ this._validateAmount(params.amount, "earn deposit");
720
+ if (params.depositBuffer !== void 0 && (params.depositBuffer <= 0 || params.depositBuffer > 1)) {
721
+ throw new StableValidationError("depositBuffer", params.depositBuffer, "earn deposit: depositBuffer must be > 0 and \u2264 1");
722
+ }
723
+ return this._withIdempotency(params.idempotencyKey, () => this._deposit(params));
724
+ }
725
+ async bulkDeposit(items, options = {}) {
726
+ const { idempotencyKey, stopOnError = false } = options;
727
+ const run = async () => {
728
+ const results = [];
729
+ for (const item of items) {
730
+ try {
731
+ const result = await this.deposit(item);
732
+ results.push({ status: "fulfilled", txHash: result.txHash, vault: item.vault, idempotencyKey: item.idempotencyKey });
733
+ } catch (err) {
734
+ const entry = { status: "rejected", error: String(err), vault: item.vault, idempotencyKey: item.idempotencyKey };
735
+ results.push(entry);
736
+ if (stopOnError) {
737
+ const succeeded2 = results.filter((r) => r.status === "fulfilled").length;
738
+ throw new StableBulkError({
739
+ shortMessage: "bulkDeposit halted on first failure",
740
+ results,
741
+ succeeded: succeeded2,
742
+ failed: results.length - succeeded2
743
+ });
744
+ }
745
+ }
746
+ }
747
+ const succeeded = results.filter((r) => r.status === "fulfilled").length;
748
+ return { results, succeeded, failed: results.length - succeeded, idempotencyKey };
749
+ };
750
+ return this._withIdempotency(idempotencyKey, run);
751
+ }
752
+ async bulkWithdraw(items, options = {}) {
753
+ const { idempotencyKey, stopOnError = false } = options;
754
+ const run = async () => {
755
+ const results = [];
756
+ for (const item of items) {
757
+ try {
758
+ const result = await this.withdraw(item);
759
+ results.push({ status: "fulfilled", txHash: result.txHash, idempotencyKey: item.idempotencyKey });
760
+ } catch (err) {
761
+ const entry = { status: "rejected", error: String(err), idempotencyKey: item.idempotencyKey };
762
+ results.push(entry);
763
+ if (stopOnError) {
764
+ const succeeded2 = results.filter((r) => r.status === "fulfilled").length;
765
+ throw new StableBulkError({
766
+ shortMessage: "bulkWithdraw halted on first failure",
767
+ results,
768
+ succeeded: succeeded2,
769
+ failed: results.length - succeeded2
770
+ });
771
+ }
772
+ }
773
+ }
774
+ const succeeded = results.filter((r) => r.status === "fulfilled").length;
775
+ return { results, succeeded, failed: results.length - succeeded, idempotencyKey };
776
+ };
777
+ return this._withIdempotency(idempotencyKey, run);
778
+ }
779
+ withdraw(params) {
780
+ this._validateAmount(params.amount, "earn withdraw");
781
+ return this._withIdempotency(params.idempotencyKey, () => this._withdraw(params));
782
+ }
783
+ redeem(params) {
784
+ this._validateShares(params.shares, "earn redeem");
785
+ return this._withIdempotency(params.idempotencyKey, () => this._redeem(params));
786
+ }
787
+ forceWithdraw(params) {
788
+ this._validateAmount(params.amount, "earn forceWithdraw");
789
+ return this._withIdempotency(params.idempotencyKey, () => this._forceWithdraw(params));
790
+ }
791
+ forceRedeem(params) {
792
+ this._validateShares(params.shares, "earn forceRedeem");
793
+ return this._withIdempotency(params.idempotencyKey, () => this._forceRedeem(params));
794
+ }
795
+ // ─── Private operations ──────────────────────────────────────────────────
796
+ async _deposit(params) {
797
+ const { amount, tokenDecimals, slippageTolerance, idempotencyKey, depositBuffer } = params;
798
+ const from = this._requireFrom();
799
+ const vaultAddr = params.vault ?? this.config.vault;
800
+ await this._switchChain();
801
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
802
+ const vaultData = await vault.getData();
803
+ const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn deposit");
804
+ const assetsRaw = parseUnits3(String(amount), decimals);
805
+ if (depositBuffer !== void 0) {
806
+ const bps = BigInt(Math.round(depositBuffer * 1e4));
807
+ const maxAllowed = vaultData.assetBalance * bps / 10000n;
808
+ if (assetsRaw > maxAllowed) {
809
+ const maxFormatted = Number(formatUnits3(maxAllowed, decimals));
810
+ const idleFormatted = Number(formatUnits3(vaultData.assetBalance, decimals));
811
+ throw new StableValidationError(
812
+ "amount",
813
+ amount,
814
+ `earn deposit: amount ${amount} exceeds deposit buffer \u2014 max ${maxFormatted} (${depositBuffer * 100}% of ${idleFormatted} idle liquidity)`
815
+ );
816
+ }
817
+ }
818
+ const depositAction = vault.deposit({
819
+ amount: assetsRaw,
820
+ userAddress: from,
821
+ accrualVault: vaultData,
822
+ slippageTolerance: slippageTolerance !== void 0 ? parseUnits3(String(slippageTolerance), 18) : void 0
823
+ });
824
+ const requirements = await depositAction.getRequirements();
825
+ let sig;
826
+ for (const req of requirements) {
827
+ if (isRequirementApproval(req)) {
828
+ const approveHash = await this._sendTx(req.to, req.data, req.value, from);
829
+ const approveReceipt = await this.pc.waitForTransactionReceipt({ hash: approveHash });
830
+ if (approveReceipt.status === "reverted") {
831
+ throw new StableTransactionError({
832
+ phase: "approve",
833
+ shortMessage: "earn deposit: token approval reverted on-chain",
834
+ txHash: approveReceipt.transactionHash,
835
+ chainId: this.config.chainId
836
+ });
837
+ }
838
+ } else if (isRequirementSignature(req)) {
839
+ sig = await req.sign(this.wc, from);
840
+ }
841
+ }
842
+ const tx = depositAction.buildTx(sig);
843
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
844
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
845
+ if (receipt.status === "reverted") {
846
+ throw new StableTransactionError({
847
+ phase: "receipt",
848
+ shortMessage: "earn deposit: vault deposit reverted on-chain",
849
+ txHash: receipt.transactionHash,
850
+ chainId: this.config.chainId
851
+ });
852
+ }
853
+ return { txHash: receipt.transactionHash, idempotencyKey };
854
+ }
855
+ async _withdraw(params) {
856
+ const { amount, tokenDecimals, idempotencyKey } = params;
857
+ const from = this._requireFrom();
858
+ const vaultAddr = this.config.vault;
859
+ await this._switchChain();
860
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
861
+ const vaultData = await vault.getData();
862
+ const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn withdraw");
863
+ const assetsRaw = parseUnits3(String(amount), decimals);
864
+ const tx = vault.withdraw({ amount: assetsRaw, userAddress: from }).buildTx();
865
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
866
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
867
+ if (receipt.status === "reverted") {
868
+ throw new StableTransactionError({
869
+ phase: "receipt",
870
+ shortMessage: "earn withdraw: vault withdraw reverted on-chain",
871
+ txHash: receipt.transactionHash,
872
+ chainId: this.config.chainId
873
+ });
874
+ }
875
+ return { txHash: receipt.transactionHash, idempotencyKey };
876
+ }
877
+ async _redeem(params) {
878
+ const { shares, shareDecimals, idempotencyKey } = params;
879
+ const from = this._requireFrom();
880
+ const vaultAddr = this.config.vault;
881
+ await this._switchChain();
882
+ let decimals = shareDecimals;
883
+ if (decimals === void 0) {
884
+ try {
885
+ decimals = await this.pc.readContract({ address: vaultAddr, abi: erc20Abi3, functionName: "decimals" });
886
+ } catch {
887
+ throw new StableValidationError(
888
+ "shareDecimals",
889
+ void 0,
890
+ `earn redeem: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
891
+ );
892
+ }
893
+ }
894
+ const sharesRaw = parseUnits3(String(shares), decimals);
895
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
896
+ const tx = vault.redeem({ shares: sharesRaw, userAddress: from }).buildTx();
897
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
898
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
899
+ if (receipt.status === "reverted") {
900
+ throw new StableTransactionError({
901
+ phase: "receipt",
902
+ shortMessage: "earn redeem: vault redeem reverted on-chain",
903
+ txHash: receipt.transactionHash,
904
+ chainId: this.config.chainId
905
+ });
906
+ }
907
+ return { txHash: receipt.transactionHash, idempotencyKey };
908
+ }
909
+ async _forceWithdraw(params) {
910
+ const { amount, tokenDecimals, deallocations, idempotencyKey } = params;
911
+ const from = this._requireFrom();
912
+ const vaultAddr = this.config.vault;
913
+ await this._switchChain();
914
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
915
+ let decimals = tokenDecimals;
916
+ if (decimals === void 0) {
917
+ const vaultData = await vault.getData();
918
+ decimals = await this._resolveDecimals(vaultData.asset, "earn forceWithdraw");
919
+ }
920
+ const tx = vault.forceWithdraw({
921
+ deallocations: this._buildDeallocs(deallocations, decimals),
922
+ withdraw: { amount: parseUnits3(String(amount), decimals) },
923
+ userAddress: from
924
+ }).buildTx();
925
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
926
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
927
+ if (receipt.status === "reverted") {
928
+ throw new StableTransactionError({
929
+ phase: "receipt",
930
+ shortMessage: "earn forceWithdraw: vault forceWithdraw reverted on-chain",
931
+ txHash: receipt.transactionHash,
932
+ chainId: this.config.chainId
933
+ });
934
+ }
935
+ return { txHash: receipt.transactionHash, idempotencyKey };
936
+ }
937
+ async _forceRedeem(params) {
938
+ const { shares, shareDecimals, tokenDecimals, deallocations, idempotencyKey } = params;
939
+ const from = this._requireFrom();
940
+ const vaultAddr = this.config.vault;
941
+ await this._switchChain();
942
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
943
+ let shareDec = shareDecimals;
944
+ if (shareDec === void 0) {
945
+ try {
946
+ shareDec = await this.pc.readContract({ address: vaultAddr, abi: erc20Abi3, functionName: "decimals" });
947
+ } catch {
948
+ throw new StableValidationError(
949
+ "shareDecimals",
950
+ void 0,
951
+ `earn forceRedeem: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
952
+ );
953
+ }
954
+ }
955
+ let assetDec = tokenDecimals;
956
+ if (assetDec === void 0) {
957
+ const vaultData = await vault.getData();
958
+ assetDec = await this._resolveDecimals(vaultData.asset, "earn forceRedeem");
959
+ }
960
+ const tx = vault.forceRedeem({
961
+ deallocations: this._buildDeallocs(deallocations, assetDec),
962
+ redeem: { shares: parseUnits3(String(shares), shareDec) },
963
+ userAddress: from
964
+ }).buildTx();
965
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
966
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
967
+ if (receipt.status === "reverted") {
968
+ throw new StableTransactionError({
969
+ phase: "receipt",
970
+ shortMessage: "earn forceRedeem: vault forceRedeem reverted on-chain",
971
+ txHash: receipt.transactionHash,
972
+ chainId: this.config.chainId
973
+ });
974
+ }
975
+ return { txHash: receipt.transactionHash, idempotencyKey };
976
+ }
977
+ _buildDeallocs(deallocations, assetDecimals) {
978
+ return deallocations.map((d) => ({
979
+ adapter: d.adapter,
980
+ amount: parseUnits3(String(d.amount), assetDecimals),
981
+ // MarketParams in the Morpho SDK has computed fields (id, liquidationIncentiveFactor)
982
+ // not present in our user-facing type; the plain object is structurally compatible for ABI encoding.
983
+ ...d.marketParams && { marketParams: d.marketParams }
984
+ }));
985
+ }
986
+ // ─── Helpers ─────────────────────────────────────────────────────────────
987
+ _requireFrom() {
988
+ const from = this.wc.account?.address;
989
+ if (!from) {
990
+ throw new StableValidationError(
991
+ "signer",
992
+ void 0,
993
+ "earn: requires a signer \u2014 set walletClient or account in StableConfig"
994
+ );
995
+ }
996
+ return from;
997
+ }
998
+ async _switchChain() {
999
+ try {
1000
+ await this.wc.switchChain({ id: this.config.chainId });
1001
+ } catch (err) {
1002
+ if (err.code === 4001) {
1003
+ throw new StableTransactionError({
1004
+ phase: "switch_chain",
1005
+ shortMessage: `earn: wallet rejected switch to chain ${this.config.chainId}`,
1006
+ chainId: this.config.chainId,
1007
+ cause: err
1008
+ });
1009
+ }
1010
+ }
1011
+ }
1012
+ async _sendTx(to, data, value, from) {
1013
+ try {
1014
+ return await this.wc.sendTransaction({ to, data, value, account: from, chain: this.chain });
1015
+ } catch (err) {
1016
+ throw new StableTransactionError({
1017
+ phase: "send",
1018
+ shortMessage: "earn: transaction send failed",
1019
+ chainId: this.config.chainId,
1020
+ cause: err
1021
+ });
1022
+ }
1023
+ }
1024
+ async _resolveDecimals(asset, op) {
1025
+ try {
1026
+ return await retryWithBackoff(
1027
+ () => this.pc.readContract({ address: asset, abi: erc20Abi3, functionName: "decimals" })
1028
+ );
1029
+ } catch {
1030
+ throw new StableValidationError(
1031
+ "tokenDecimals",
1032
+ void 0,
1033
+ `${op}: could not fetch decimals for asset "${asset}" \u2014 pass tokenDecimals explicitly`
1034
+ );
1035
+ }
1036
+ }
1037
+ _withIdempotency(key, fn) {
1038
+ if (!key) return fn();
1039
+ const existing = this.inFlight.get(key);
1040
+ if (existing) return existing;
1041
+ const promise = fn().finally(() => this.inFlight.delete(key));
1042
+ this.inFlight.set(key, promise);
1043
+ return promise;
1044
+ }
1045
+ _validateAmount(amount, op) {
1046
+ if (!Number.isFinite(amount) || amount <= 0) {
1047
+ throw new StableValidationError("amount", amount, `${op}: amount must be a finite positive number`);
1048
+ }
1049
+ }
1050
+ _validateShares(shares, op) {
1051
+ if (!Number.isFinite(shares) || shares <= 0) {
1052
+ throw new StableValidationError("shares", shares, `${op}: shares must be a finite positive number`);
1053
+ }
1054
+ }
1055
+ };
1056
+
505
1057
  // src/stable.ts
506
1058
  function validateTransferParams({ to, amount, token, tokenDecimals }) {
507
- if (!isAddress2(to)) {
1059
+ if (!isAddress3(to)) {
508
1060
  throw new StableValidationError("to", to, `transfer: invalid recipient address "${to}"`);
509
1061
  }
510
1062
  if (!Number.isFinite(amount) || amount <= 0) {
511
1063
  throw new StableValidationError("amount", amount, "transfer: amount must be a finite positive number");
512
1064
  }
513
- if (token !== void 0 && !isAddress2(token)) {
1065
+ if (token !== void 0 && !isAddress3(token)) {
514
1066
  throw new StableValidationError("token", token, `transfer: invalid token address "${token}"`);
515
1067
  }
516
1068
  if (tokenDecimals !== void 0 && (!Number.isInteger(tokenDecimals) || tokenDecimals < 0 || tokenDecimals > 77)) {
@@ -520,7 +1072,7 @@ function validateTransferParams({ to, amount, token, tokenDecimals }) {
520
1072
  async function resolveTokenDecimals(publicClient, token, tokenDecimals) {
521
1073
  if (tokenDecimals !== void 0) return tokenDecimals;
522
1074
  try {
523
- return await publicClient.readContract({ address: token, abi: erc20Abi2, functionName: "decimals" });
1075
+ return await publicClient.readContract({ address: token, abi: erc20Abi4, functionName: "decimals" });
524
1076
  } catch {
525
1077
  throw new StableValidationError(
526
1078
  "tokenDecimals",
@@ -541,9 +1093,9 @@ async function executeTokenTransfer(wc, publicClient, token, to, amount, tokenDe
541
1093
  const decimals = await resolveTokenDecimals(publicClient, token, tokenDecimals);
542
1094
  return wc.writeContract({
543
1095
  address: token,
544
- abi: erc20Abi2,
1096
+ abi: erc20Abi4,
545
1097
  functionName: "transfer",
546
- args: [to, parseUnits2(String(amount), decimals)],
1098
+ args: [to, parseUnits4(String(amount), decimals)],
547
1099
  account: acc,
548
1100
  chain
549
1101
  });
@@ -553,20 +1105,32 @@ function createStable({
553
1105
  rpc: rpcOverride,
554
1106
  account,
555
1107
  transport,
556
- walletClient
1108
+ walletClient,
1109
+ earn: earnConfig
557
1110
  }) {
558
- const chain = network === "mainnet" /* Mainnet */ ? stableMainnet : stableTestnet;
1111
+ const chain = network === "mainnet" /* Mainnet */ ? stableMainnet2 : stableTestnet2;
559
1112
  const rpc = (rpcOverride ?? chain.rpcUrls.default.http[0]).replace(/\/$/, "");
560
1113
  const signerConfig = { account, transport, walletClient };
561
1114
  const lifi = new LiFiService({ ...signerConfig, rpc, chainId: chain.id });
1115
+ const morpho = earnConfig ? new MorphoService({ ...signerConfig, rpc, chainId: chain.id, vault: earnConfig.vault }) : null;
1116
+ function requireEarn() {
1117
+ if (!morpho) {
1118
+ throw new StableValidationError(
1119
+ "earn",
1120
+ void 0,
1121
+ "stable.earn.*: pass earn: { vault: '0x...' } to createStable() to enable earn operations"
1122
+ );
1123
+ }
1124
+ return morpho;
1125
+ }
562
1126
  async function transfer(params) {
563
1127
  validateTransferParams(params);
564
1128
  const { from, to, amount, token, tokenDecimals } = params;
565
1129
  const acc = walletClient?.account ?? account ?? from;
566
- const wc = walletClient ?? createWalletClient2({
1130
+ const wc = walletClient ?? createWalletClient3({
567
1131
  account: acc,
568
1132
  chain,
569
- transport: transport ?? http2(rpc)
1133
+ transport: transport ?? http4(rpc)
570
1134
  });
571
1135
  try {
572
1136
  await wc.switchChain({ id: chain.id });
@@ -578,8 +1142,8 @@ function createStable({
578
1142
  cause: err
579
1143
  });
580
1144
  }
581
- const publicClient = createPublicClient2({ chain, transport: http2(rpc) });
582
- const hash = token !== void 0 ? await executeTokenTransfer(wc, publicClient, token, to, amount, tokenDecimals, acc, chain) : await wc.sendTransaction({ account: acc, chain, to, value: parseUnits2(String(amount), 18) });
1145
+ const publicClient = createPublicClient4({ chain, transport: http4(rpc) });
1146
+ const hash = token !== void 0 ? await executeTokenTransfer(wc, publicClient, token, to, amount, tokenDecimals, acc, chain) : await wc.sendTransaction({ account: acc, chain, to, value: parseUnits4(String(amount), 18) });
583
1147
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
584
1148
  if (receipt.status === "reverted") {
585
1149
  throw new StableTransactionError({
@@ -591,28 +1155,50 @@ function createStable({
591
1155
  }
592
1156
  return { txHash: receipt.transactionHash };
593
1157
  }
594
- function quoteBridge(params) {
595
- return lifi.quoteBridge(params);
596
- }
597
- function bridge(params) {
598
- return lifi.bridge(params);
599
- }
600
1158
  return {
601
1159
  transfer,
602
- quoteBridge,
603
- bridge,
1160
+ quoteBridge: (params) => lifi.quoteBridge(params),
1161
+ bridge: (params) => lifi.bridge(params),
604
1162
  quoteSwap: (params) => lifi.quoteSwap(params),
605
- swap: (params) => lifi.swap(params)
1163
+ swap: (params) => lifi.swap(params),
1164
+ earn: {
1165
+ deposit: (params) => requireEarn().deposit(params),
1166
+ bulkDeposit: (items, options) => requireEarn().bulkDeposit(items, options),
1167
+ withdraw: (params) => requireEarn().withdraw(params),
1168
+ bulkWithdraw: (items, options) => requireEarn().bulkWithdraw(items, options),
1169
+ redeem: (params) => requireEarn().redeem(params),
1170
+ forceWithdraw: (params) => requireEarn().forceWithdraw(params),
1171
+ forceRedeem: (params) => requireEarn().forceRedeem(params)
1172
+ }
1173
+ };
1174
+ }
1175
+ function createStableReader(config) {
1176
+ const reader = new EarnReader(config);
1177
+ return {
1178
+ earn: {
1179
+ getYield: () => reader.getYield(),
1180
+ position: () => reader.position(),
1181
+ preview: (params) => reader.preview(params),
1182
+ withdrawability: (params) => reader.withdrawability(params)
1183
+ }
606
1184
  };
607
1185
  }
1186
+
1187
+ // src/constants.ts
1188
+ var STABLE_USDT_ADDRESS = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
1189
+ var STABLE_VAULT_ADDRESS = "0x461eF73F640456138242398a39AB48a557A49293";
608
1190
  export {
609
1191
  CHAIN_CONFIGS,
610
1192
  Chain,
611
1193
  Network,
1194
+ STABLE_USDT_ADDRESS,
1195
+ STABLE_VAULT_ADDRESS,
1196
+ StableBulkError,
612
1197
  StableError,
613
1198
  StableNetworkError,
614
1199
  StableQuoteError,
615
1200
  StableTransactionError,
616
1201
  StableValidationError,
617
- createStable
1202
+ createStable,
1203
+ createStableReader
618
1204
  };