@stablechain/sdk 0.1.0 → 1.0.0

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 createPublicClient3,
3
+ createPublicClient as createPublicClient4,
4
4
  createWalletClient as createWalletClient3,
5
- erc20Abi as erc20Abi3,
6
- http as http3,
5
+ erc20Abi as erc20Abi4,
6
+ http as http4,
7
7
  isAddress as isAddress3,
8
- parseUnits as parseUnits3
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,19 +99,6 @@ function formatValue(v) {
79
99
  }
80
100
  }
81
101
 
82
- // src/services/layerzero.ts
83
- import {
84
- concat,
85
- createPublicClient,
86
- createWalletClient,
87
- erc20Abi,
88
- http,
89
- isAddress,
90
- numberToHex,
91
- padHex,
92
- parseUnits
93
- } from "viem";
94
-
95
102
  // src/helpers/chain.ts
96
103
  import { defineChain } from "viem";
97
104
  function makeChain(id, rpc) {
@@ -103,6 +110,38 @@ function makeChain(id, rpc) {
103
110
  });
104
111
  }
105
112
 
113
+ // src/helpers/retry.ts
114
+ var HttpError = class extends Error {
115
+ constructor(message, status, retryable) {
116
+ super(message);
117
+ this.status = status;
118
+ this.retryable = retryable;
119
+ this.name = "HttpError";
120
+ }
121
+ };
122
+ function isRetryableError(err) {
123
+ if (err instanceof HttpError) return err.retryable;
124
+ return err instanceof TypeError;
125
+ }
126
+ async function retryWithBackoff(fn, isRetryable = isRetryableError, opts = {}) {
127
+ const maxAttempts = opts.maxAttempts ?? 3;
128
+ const base = opts.baseDelayMs ?? 250;
129
+ const max = opts.maxDelayMs ?? 4e3;
130
+ let lastErr;
131
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
132
+ try {
133
+ return await fn();
134
+ } catch (err) {
135
+ lastErr = err;
136
+ if (attempt === maxAttempts - 1 || !isRetryable(err)) throw err;
137
+ const delay = Math.min(max, base * 2 ** attempt);
138
+ const jittered = delay * (0.75 + Math.random() * 0.5);
139
+ await new Promise((r) => setTimeout(r, jittered));
140
+ }
141
+ }
142
+ throw lastErr;
143
+ }
144
+
106
145
  // src/types.ts
107
146
  var Chain = /* @__PURE__ */ ((Chain2) => {
108
147
  Chain2["Sepolia"] = "sepolia";
@@ -124,6 +163,156 @@ var Network = /* @__PURE__ */ ((Network2) => {
124
163
  return Network2;
125
164
  })(Network || {});
126
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 preview(params) {
227
+ const { amount, horizonDays } = params;
228
+ const apy = await this._fetchNetApy();
229
+ return {
230
+ projectedYield: amount * apy * (horizonDays / 365),
231
+ apy,
232
+ horizonDays
233
+ };
234
+ }
235
+ async withdrawability(params) {
236
+ const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
237
+ const vaultData = await vault.getData();
238
+ const tokenDecimals = params.tokenDecimals ?? await this.pc.readContract({
239
+ address: vaultData.asset,
240
+ abi: erc20Abi,
241
+ functionName: "decimals"
242
+ });
243
+ const requestedRaw = parseUnits(String(params.amount), tokenDecimals);
244
+ const availableLiquidityRaw = vaultData.assetBalance;
245
+ return {
246
+ isInstant: requestedRaw <= availableLiquidityRaw,
247
+ availableLiquidity: Number(formatUnits(availableLiquidityRaw, tokenDecimals)),
248
+ tokenDecimals
249
+ };
250
+ }
251
+ async _fetchNetApy() {
252
+ const query = `
253
+ query VaultNetApy($address: String!, $chainId: Int!) {
254
+ vaultV2ByAddress(address: $address, chainId: $chainId) {
255
+ netApy
256
+ }
257
+ }
258
+ `;
259
+ let body;
260
+ try {
261
+ body = await retryWithBackoff(
262
+ async () => {
263
+ const res = await fetch(MORPHO_API_URL, {
264
+ method: "POST",
265
+ headers: { "Content-Type": "application/json" },
266
+ body: JSON.stringify({
267
+ query,
268
+ variables: { address: this.vaultAddr, chainId: this.chainId }
269
+ })
270
+ });
271
+ if (!res.ok) {
272
+ const retryable = res.status === 429 || res.status >= 500;
273
+ throw new HttpError(
274
+ `earn preview: Morpho API returned ${res.status} \u2014 ${res.statusText}`,
275
+ res.status,
276
+ retryable
277
+ );
278
+ }
279
+ return res.json().catch(() => void 0);
280
+ },
281
+ (err) => err instanceof HttpError ? err.retryable : err instanceof TypeError
282
+ );
283
+ } catch (err) {
284
+ if (err instanceof TypeError || err instanceof HttpError) {
285
+ throw new StableNetworkError({
286
+ shortMessage: err instanceof HttpError ? `earn preview: Morpho API returned ${err.status} after retries` : `earn preview: failed to reach Morpho API after retries \u2014 ${String(err)}`,
287
+ url: MORPHO_API_URL,
288
+ cause: err
289
+ });
290
+ }
291
+ throw err;
292
+ }
293
+ const netApy = body?.data?.vaultV2ByAddress?.netApy;
294
+ if (typeof netApy !== "number") {
295
+ throw new StableValidationError(
296
+ "apy",
297
+ void 0,
298
+ "earn preview: vault not found in Morpho API or unexpected response shape"
299
+ );
300
+ }
301
+ return netApy;
302
+ }
303
+ };
304
+
305
+ // src/services/lifi.ts
306
+ import {
307
+ createPublicClient as createPublicClient2,
308
+ createWalletClient,
309
+ erc20Abi as erc20Abi2,
310
+ formatUnits as formatUnits2,
311
+ http as http2,
312
+ isAddress as isAddress2,
313
+ parseUnits as parseUnits2
314
+ } from "viem";
315
+
127
316
  // src/chains.ts
128
317
  var CHAIN_CONFIGS = {
129
318
  // ─── Testnet ──────────────────────────────────────────────────────────────
@@ -196,305 +385,6 @@ var CHAIN_CONFIGS = {
196
385
  }
197
386
  };
198
387
 
199
- // src/services/layerzero.ts
200
- var C = CHAIN_CONFIGS;
201
- var BRIDGE_CHAIN_CONFIGS = {
202
- // ─── Testnet ───────────────────────────────────────────────────────────────
203
- ["sepolia" /* Sepolia */]: { ...C["sepolia" /* Sepolia */], lzEid: 40161, oftAdapter: "0xc099cD946d5efCC35A99D64E808c1430cEf08126" },
204
- ["stable-testnet" /* StableTestnet */]: { ...C["stable-testnet" /* StableTestnet */], lzEid: 40374, oftAdapter: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736" },
205
- // ─── Mainnet ──────────────────────────────────────────────────────────────
206
- ["stable" /* Stable */]: { ...C["stable" /* Stable */], lzEid: 30396, oftAdapter: "0xedaba024be4d87974d5aB11C6Dd586963CcCB027" },
207
- ["ethereum" /* Ethereum */]: { ...C["ethereum" /* Ethereum */], lzEid: 30101, oftAdapter: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee" },
208
- ["arbitrum" /* Arbitrum */]: { ...C["arbitrum" /* Arbitrum */], lzEid: 30110, oftAdapter: "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92" },
209
- ["ink" /* Ink */]: { ...C["ink" /* Ink */], lzEid: 30339, oftAdapter: "0x1cB6De532588fCA4a21B7209DE7C456AF8434A65" },
210
- ["bera" /* Bera */]: { ...C["bera" /* Bera */], lzEid: 30362, oftAdapter: "0x3Dc96399109df5ceb2C226664A086140bD0379cB" },
211
- ["megaeth" /* MegaETH */]: { ...C["megaeth" /* MegaETH */], lzEid: 30398, oftAdapter: "0x9151434b16b9763660705744891fa906f660ecc5" },
212
- ["hyperevm" /* HyperEVM */]: { ...C["hyperevm" /* HyperEVM */], lzEid: 30367, oftAdapter: "0x904861a24F30EC96ea7CFC3bE9EA4B476d237e98" }
213
- };
214
- var OFT_ADAPTER_ABI = [
215
- {
216
- name: "quoteSend",
217
- type: "function",
218
- inputs: [
219
- {
220
- name: "_sendParam",
221
- type: "tuple",
222
- components: [
223
- { name: "dstEid", type: "uint32" },
224
- { name: "to", type: "bytes32" },
225
- { name: "amountLD", type: "uint256" },
226
- { name: "minAmountLD", type: "uint256" },
227
- { name: "extraOptions", type: "bytes" },
228
- { name: "composeMsg", type: "bytes" },
229
- { name: "oftCmd", type: "bytes" }
230
- ]
231
- },
232
- { name: "_payInLzToken", type: "bool" }
233
- ],
234
- outputs: [
235
- {
236
- name: "msgFee",
237
- type: "tuple",
238
- components: [
239
- { name: "nativeFee", type: "uint256" },
240
- { name: "lzTokenFee", type: "uint256" }
241
- ]
242
- }
243
- ],
244
- stateMutability: "view"
245
- },
246
- {
247
- name: "send",
248
- type: "function",
249
- inputs: [
250
- {
251
- name: "_sendParam",
252
- type: "tuple",
253
- components: [
254
- { name: "dstEid", type: "uint32" },
255
- { name: "to", type: "bytes32" },
256
- { name: "amountLD", type: "uint256" },
257
- { name: "minAmountLD", type: "uint256" },
258
- { name: "extraOptions", type: "bytes" },
259
- { name: "composeMsg", type: "bytes" },
260
- { name: "oftCmd", type: "bytes" }
261
- ]
262
- },
263
- {
264
- name: "_fee",
265
- type: "tuple",
266
- components: [
267
- { name: "nativeFee", type: "uint256" },
268
- { name: "lzTokenFee", type: "uint256" }
269
- ]
270
- },
271
- { name: "_refundAddress", type: "address" }
272
- ],
273
- outputs: [],
274
- stateMutability: "payable"
275
- }
276
- ];
277
- function buildLzOptions(gasLimit = 0n) {
278
- return concat([
279
- numberToHex(3, { size: 2 }),
280
- // TYPE_3 header
281
- numberToHex(1, { size: 1 }),
282
- // Executor worker ID
283
- numberToHex(17, { size: 2 }),
284
- // option payload length = 17 bytes
285
- numberToHex(1, { size: 1 }),
286
- // LzReceive option type
287
- numberToHex(gasLimit, { size: 16 })
288
- // gas limit as uint128
289
- ]);
290
- }
291
- var LayerZeroService = class {
292
- constructor(config) {
293
- this.config = config;
294
- }
295
- async quoteBridge({ fromChain, toChain, amount, recipient }) {
296
- const recipientAddr = recipient ?? "0x0000000000000000000000000000000000000000";
297
- this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, "quoteBridge");
298
- const { src, dst } = this.resolveChains(fromChain, toChain);
299
- const oftAdapter = src.oftAdapter;
300
- const amountLD = parseUnits(String(amount), src.decimals);
301
- try {
302
- const { nativeFee, lzTokenFee } = await createPublicClient({
303
- chain: makeChain(src.id, src.rpc),
304
- transport: http(src.rpc)
305
- }).readContract({
306
- address: oftAdapter,
307
- abi: OFT_ADAPTER_ABI,
308
- functionName: "quoteSend",
309
- args: [this.buildSendParams(dst, amountLD, recipientAddr), false]
310
- });
311
- return { nativeFee, lzTokenFee, toAmount: amount };
312
- } catch (err) {
313
- throw new StableQuoteError({
314
- provider: "layerzero",
315
- shortMessage: "quoteBridge: quoteSend call failed",
316
- cause: err
317
- });
318
- }
319
- }
320
- async bridge({ fromChain, toChain, fromToken, toToken, amount, recipient, quote }) {
321
- const from = this.config.walletClient?.account?.address ?? this.config.account?.address;
322
- if (!from) {
323
- throw new StableValidationError(
324
- "signer",
325
- void 0,
326
- "bridge: requires a signer \u2014 set walletClient or account in StableConfig"
327
- );
328
- }
329
- const recipientAddr = recipient ?? from;
330
- this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, "bridge");
331
- const { src, dst } = this.resolveChains(fromChain, toChain);
332
- const oftAdapter = src.oftAdapter;
333
- const srcChain = makeChain(src.id, src.rpc);
334
- const wc = this.config.walletClient ?? createWalletClient({
335
- account: this.config.account,
336
- chain: srcChain,
337
- transport: this.config.transport ?? http(src.rpc)
338
- });
339
- const pc = createPublicClient({ chain: srcChain, transport: http(src.rpc) });
340
- try {
341
- await wc.switchChain({ id: src.id });
342
- } catch (err) {
343
- throw new StableTransactionError({
344
- phase: "switch_chain",
345
- shortMessage: `bridge: wallet rejected or failed to switch to chain ${src.id}`,
346
- chainId: src.id,
347
- cause: err
348
- });
349
- }
350
- const amountLD = parseUnits(String(amount), src.decimals);
351
- const sendParams = this.buildSendParams(dst, amountLD, recipientAddr);
352
- const lzFee = quote?.nativeFee !== void 0 && quote?.lzTokenFee !== void 0 ? { nativeFee: quote.nativeFee, lzTokenFee: quote.lzTokenFee } : await this.quoteBridge({ fromChain, toChain, fromToken, toToken, amount, recipient: recipientAddr });
353
- const { nativeFee = 0n, lzTokenFee = 0n } = lzFee;
354
- const allowance = await pc.readContract({
355
- address: src.usdt,
356
- abi: erc20Abi,
357
- functionName: "allowance",
358
- args: [from, oftAdapter]
359
- });
360
- if (allowance < amountLD) {
361
- let approveHash;
362
- try {
363
- approveHash = await wc.writeContract({
364
- address: src.usdt,
365
- abi: erc20Abi,
366
- functionName: "approve",
367
- args: [oftAdapter, amountLD],
368
- account: wc.account,
369
- chain: srcChain
370
- });
371
- } catch (err) {
372
- throw new StableTransactionError({
373
- phase: "approve",
374
- shortMessage: "bridge: USDT0 approve failed",
375
- chainId: src.id,
376
- cause: err
377
- });
378
- }
379
- const approveReceipt = await pc.waitForTransactionReceipt({ hash: approveHash });
380
- if (approveReceipt.status === "reverted") {
381
- throw new StableTransactionError({
382
- phase: "approve",
383
- shortMessage: "bridge: USDT0 approve reverted on-chain",
384
- txHash: approveReceipt.transactionHash,
385
- chainId: src.id
386
- });
387
- }
388
- }
389
- try {
390
- const hash = await wc.writeContract({
391
- address: oftAdapter,
392
- abi: OFT_ADAPTER_ABI,
393
- functionName: "send",
394
- args: [sendParams, { nativeFee, lzTokenFee }, from],
395
- value: nativeFee,
396
- account: wc.account,
397
- chain: srcChain
398
- });
399
- return { txHash: hash, toAmount: amount };
400
- } catch (err) {
401
- throw new StableTransactionError({
402
- phase: "send",
403
- shortMessage: "bridge: OFT send failed",
404
- chainId: src.id,
405
- cause: err
406
- });
407
- }
408
- }
409
- validateBridgeParams(params, method) {
410
- if (!isAddress(params.recipient)) {
411
- throw new StableValidationError(
412
- "recipient",
413
- params.recipient,
414
- `${method}: invalid recipient address "${params.recipient}"`
415
- );
416
- }
417
- if (!Number.isFinite(params.amount) || params.amount <= 0) {
418
- throw new StableValidationError(
419
- "amount",
420
- params.amount,
421
- `${method}: amount must be a finite positive number`
422
- );
423
- }
424
- if (params.fromChain === params.toChain) {
425
- throw new StableValidationError(
426
- "toChain",
427
- params.toChain,
428
- `${method}: fromChain and toChain must differ`
429
- );
430
- }
431
- }
432
- resolveChains(fromChain, toChain) {
433
- const src = BRIDGE_CHAIN_CONFIGS[fromChain];
434
- const dst = BRIDGE_CHAIN_CONFIGS[toChain];
435
- if (!src) throw new StableValidationError("fromChain", fromChain, `Unsupported source chain: "${fromChain}"`);
436
- if (!dst) throw new StableValidationError("toChain", toChain, `Unsupported destination chain: "${toChain}"`);
437
- if (!src.oftAdapter) {
438
- throw new StableValidationError("fromChain", fromChain, `No OFT adapter configured for "${fromChain}"`);
439
- }
440
- return { src, dst };
441
- }
442
- buildSendParams(dst, amountLD, recipient) {
443
- return {
444
- dstEid: dst.lzEid,
445
- to: padHex(recipient, { dir: "left", size: 32 }),
446
- amountLD,
447
- minAmountLD: amountLD,
448
- extraOptions: buildLzOptions(),
449
- composeMsg: "0x",
450
- oftCmd: "0x"
451
- };
452
- }
453
- };
454
-
455
- // src/services/lifi.ts
456
- import {
457
- createPublicClient as createPublicClient2,
458
- createWalletClient as createWalletClient2,
459
- erc20Abi as erc20Abi2,
460
- formatUnits,
461
- http as http2,
462
- isAddress as isAddress2,
463
- parseUnits as parseUnits2
464
- } from "viem";
465
-
466
- // src/helpers/retry.ts
467
- var HttpError = class extends Error {
468
- constructor(message, status, retryable) {
469
- super(message);
470
- this.status = status;
471
- this.retryable = retryable;
472
- this.name = "HttpError";
473
- }
474
- };
475
- function isRetryableError(err) {
476
- if (err instanceof HttpError) return err.retryable;
477
- return err instanceof TypeError;
478
- }
479
- async function retryWithBackoff(fn, isRetryable = isRetryableError, opts = {}) {
480
- const maxAttempts = opts.maxAttempts ?? 3;
481
- const base = opts.baseDelayMs ?? 250;
482
- const max = opts.maxDelayMs ?? 4e3;
483
- let lastErr;
484
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
485
- try {
486
- return await fn();
487
- } catch (err) {
488
- lastErr = err;
489
- if (attempt === maxAttempts - 1 || !isRetryable(err)) throw err;
490
- const delay = Math.min(max, base * 2 ** attempt);
491
- const jittered = delay * (0.75 + Math.random() * 0.5);
492
- await new Promise((r) => setTimeout(r, jittered));
493
- }
494
- }
495
- throw lastErr;
496
- }
497
-
498
388
  // src/services/lifi.ts
499
389
  var LiFiService = class {
500
390
  constructor(config) {
@@ -663,7 +553,7 @@ var LiFiService = class {
663
553
  }
664
554
  _toSwapQuote(quote) {
665
555
  return {
666
- toAmount: Number(formatUnits(BigInt(quote.estimate.toAmount), quote.action.toToken.decimals)),
556
+ toAmount: Number(formatUnits2(BigInt(quote.estimate.toAmount), quote.action.toToken.decimals)),
667
557
  fromAmount: BigInt(quote.estimate.fromAmount),
668
558
  fromToken: quote.action.fromToken.address,
669
559
  approvalAddress: quote.estimate.approvalAddress,
@@ -683,7 +573,7 @@ var LiFiService = class {
683
573
  });
684
574
  }
685
575
  const srcChain = makeChain(srcChainId, srcRpc);
686
- const wc = this.config.walletClient ?? createWalletClient2({
576
+ const wc = this.config.walletClient ?? createWalletClient({
687
577
  account: this.config.account,
688
578
  chain: srcChain,
689
579
  transport: this.config.transport ?? http2(srcRpc)
@@ -771,12 +661,335 @@ function isLiFiQuoteShape(v) {
771
661
  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);
772
662
  }
773
663
 
664
+ // src/earn/morpho.ts
665
+ import {
666
+ createPublicClient as createPublicClient3,
667
+ createWalletClient as createWalletClient2,
668
+ custom,
669
+ erc20Abi as erc20Abi3,
670
+ http as http3,
671
+ parseUnits as parseUnits3
672
+ } from "viem";
673
+ import {
674
+ MorphoClient as MorphoClient2,
675
+ isRequirementApproval,
676
+ isRequirementSignature
677
+ } from "@morpho-org/morpho-sdk";
678
+ var MorphoService = class {
679
+ constructor(config) {
680
+ this.config = config;
681
+ this.inFlight = /* @__PURE__ */ new Map();
682
+ this.chain = makeChain(config.chainId, config.rpc);
683
+ this.wc = config.walletClient ?? createWalletClient2({
684
+ account: config.account,
685
+ chain: this.chain,
686
+ transport: config.transport ?? http3(config.rpc)
687
+ });
688
+ const morphoWc = this.wc.chain?.id === config.chainId ? this.wc : createWalletClient2({
689
+ account: this.wc.account,
690
+ chain: this.chain,
691
+ transport: custom({ request: this.wc.request.bind(this.wc) })
692
+ });
693
+ this.morpho = new MorphoClient2(morphoWc, { supportSignature: true });
694
+ this.pc = createPublicClient3({ chain: this.chain, transport: http3(config.rpc) });
695
+ }
696
+ deposit(params) {
697
+ this._validateAmount(params.amount, "earn deposit");
698
+ return this._withIdempotency(params.idempotencyKey, () => this._deposit(params));
699
+ }
700
+ async bulkDeposit(items, options = {}) {
701
+ const { idempotencyKey, stopOnError = false } = options;
702
+ const run = async () => {
703
+ const results = [];
704
+ for (const item of items) {
705
+ try {
706
+ const result = await this.deposit(item);
707
+ results.push({ status: "fulfilled", txHash: result.txHash, vault: item.vault, idempotencyKey: item.idempotencyKey });
708
+ } catch (err) {
709
+ const entry = { status: "rejected", error: String(err), vault: item.vault, idempotencyKey: item.idempotencyKey };
710
+ results.push(entry);
711
+ if (stopOnError) {
712
+ const succeeded2 = results.filter((r) => r.status === "fulfilled").length;
713
+ throw new StableBulkError({
714
+ shortMessage: "bulkDeposit halted on first failure",
715
+ results,
716
+ succeeded: succeeded2,
717
+ failed: results.length - succeeded2
718
+ });
719
+ }
720
+ }
721
+ }
722
+ const succeeded = results.filter((r) => r.status === "fulfilled").length;
723
+ return { results, succeeded, failed: results.length - succeeded, idempotencyKey };
724
+ };
725
+ return this._withIdempotency(idempotencyKey, run);
726
+ }
727
+ withdraw(params) {
728
+ this._validateAmount(params.amount, "earn withdraw");
729
+ return this._withIdempotency(params.idempotencyKey, () => this._withdraw(params));
730
+ }
731
+ redeem(params) {
732
+ this._validateShares(params.shares, "earn redeem");
733
+ return this._withIdempotency(params.idempotencyKey, () => this._redeem(params));
734
+ }
735
+ forceWithdraw(params) {
736
+ this._validateAmount(params.amount, "earn forceWithdraw");
737
+ return this._withIdempotency(params.idempotencyKey, () => this._forceWithdraw(params));
738
+ }
739
+ forceRedeem(params) {
740
+ this._validateShares(params.shares, "earn forceRedeem");
741
+ return this._withIdempotency(params.idempotencyKey, () => this._forceRedeem(params));
742
+ }
743
+ // ─── Private operations ──────────────────────────────────────────────────
744
+ async _deposit(params) {
745
+ const { amount, tokenDecimals, slippageTolerance, idempotencyKey } = params;
746
+ const from = this._requireFrom();
747
+ const vaultAddr = params.vault ?? this.config.vault;
748
+ await this._switchChain();
749
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
750
+ const vaultData = await vault.getData();
751
+ const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn deposit");
752
+ const assetsRaw = parseUnits3(String(amount), decimals);
753
+ const depositAction = vault.deposit({
754
+ amount: assetsRaw,
755
+ userAddress: from,
756
+ accrualVault: vaultData,
757
+ slippageTolerance: slippageTolerance !== void 0 ? parseUnits3(String(slippageTolerance), 18) : void 0
758
+ });
759
+ const requirements = await depositAction.getRequirements();
760
+ let sig;
761
+ for (const req of requirements) {
762
+ if (isRequirementApproval(req)) {
763
+ const approveHash = await this._sendTx(req.to, req.data, req.value, from);
764
+ const approveReceipt = await this.pc.waitForTransactionReceipt({ hash: approveHash });
765
+ if (approveReceipt.status === "reverted") {
766
+ throw new StableTransactionError({
767
+ phase: "approve",
768
+ shortMessage: "earn deposit: token approval reverted on-chain",
769
+ txHash: approveReceipt.transactionHash,
770
+ chainId: this.config.chainId
771
+ });
772
+ }
773
+ } else if (isRequirementSignature(req)) {
774
+ sig = await req.sign(this.wc, from);
775
+ }
776
+ }
777
+ const tx = depositAction.buildTx(sig);
778
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
779
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
780
+ if (receipt.status === "reverted") {
781
+ throw new StableTransactionError({
782
+ phase: "receipt",
783
+ shortMessage: "earn deposit: vault deposit reverted on-chain",
784
+ txHash: receipt.transactionHash,
785
+ chainId: this.config.chainId
786
+ });
787
+ }
788
+ return { txHash: receipt.transactionHash, idempotencyKey };
789
+ }
790
+ async _withdraw(params) {
791
+ const { amount, tokenDecimals, idempotencyKey } = params;
792
+ const from = this._requireFrom();
793
+ const vaultAddr = this.config.vault;
794
+ await this._switchChain();
795
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
796
+ const vaultData = await vault.getData();
797
+ const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn withdraw");
798
+ const assetsRaw = parseUnits3(String(amount), decimals);
799
+ const tx = vault.withdraw({ amount: assetsRaw, userAddress: from }).buildTx();
800
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
801
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
802
+ if (receipt.status === "reverted") {
803
+ throw new StableTransactionError({
804
+ phase: "receipt",
805
+ shortMessage: "earn withdraw: vault withdraw reverted on-chain",
806
+ txHash: receipt.transactionHash,
807
+ chainId: this.config.chainId
808
+ });
809
+ }
810
+ return { txHash: receipt.transactionHash, idempotencyKey };
811
+ }
812
+ async _redeem(params) {
813
+ const { shares, shareDecimals, idempotencyKey } = params;
814
+ const from = this._requireFrom();
815
+ const vaultAddr = this.config.vault;
816
+ await this._switchChain();
817
+ let decimals = shareDecimals;
818
+ if (decimals === void 0) {
819
+ try {
820
+ decimals = await this.pc.readContract({ address: vaultAddr, abi: erc20Abi3, functionName: "decimals" });
821
+ } catch {
822
+ throw new StableValidationError(
823
+ "shareDecimals",
824
+ void 0,
825
+ `earn redeem: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
826
+ );
827
+ }
828
+ }
829
+ const sharesRaw = parseUnits3(String(shares), decimals);
830
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
831
+ const tx = vault.redeem({ shares: sharesRaw, userAddress: from }).buildTx();
832
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
833
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
834
+ if (receipt.status === "reverted") {
835
+ throw new StableTransactionError({
836
+ phase: "receipt",
837
+ shortMessage: "earn redeem: vault redeem reverted on-chain",
838
+ txHash: receipt.transactionHash,
839
+ chainId: this.config.chainId
840
+ });
841
+ }
842
+ return { txHash: receipt.transactionHash, idempotencyKey };
843
+ }
844
+ async _forceWithdraw(params) {
845
+ const { amount, tokenDecimals, deallocations, idempotencyKey } = params;
846
+ const from = this._requireFrom();
847
+ const vaultAddr = this.config.vault;
848
+ await this._switchChain();
849
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
850
+ let decimals = tokenDecimals;
851
+ if (decimals === void 0) {
852
+ const vaultData = await vault.getData();
853
+ decimals = await this._resolveDecimals(vaultData.asset, "earn forceWithdraw");
854
+ }
855
+ const tx = vault.forceWithdraw({
856
+ deallocations: this._buildDeallocs(deallocations, decimals),
857
+ withdraw: { amount: parseUnits3(String(amount), decimals) },
858
+ userAddress: from
859
+ }).buildTx();
860
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
861
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
862
+ if (receipt.status === "reverted") {
863
+ throw new StableTransactionError({
864
+ phase: "receipt",
865
+ shortMessage: "earn forceWithdraw: vault forceWithdraw reverted on-chain",
866
+ txHash: receipt.transactionHash,
867
+ chainId: this.config.chainId
868
+ });
869
+ }
870
+ return { txHash: receipt.transactionHash, idempotencyKey };
871
+ }
872
+ async _forceRedeem(params) {
873
+ const { shares, shareDecimals, tokenDecimals, deallocations, idempotencyKey } = params;
874
+ const from = this._requireFrom();
875
+ const vaultAddr = this.config.vault;
876
+ await this._switchChain();
877
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
878
+ let shareDec = shareDecimals;
879
+ if (shareDec === void 0) {
880
+ try {
881
+ shareDec = await this.pc.readContract({ address: vaultAddr, abi: erc20Abi3, functionName: "decimals" });
882
+ } catch {
883
+ throw new StableValidationError(
884
+ "shareDecimals",
885
+ void 0,
886
+ `earn forceRedeem: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
887
+ );
888
+ }
889
+ }
890
+ let assetDec = tokenDecimals;
891
+ if (assetDec === void 0) {
892
+ const vaultData = await vault.getData();
893
+ assetDec = await this._resolveDecimals(vaultData.asset, "earn forceRedeem");
894
+ }
895
+ const tx = vault.forceRedeem({
896
+ deallocations: this._buildDeallocs(deallocations, assetDec),
897
+ redeem: { shares: parseUnits3(String(shares), shareDec) },
898
+ userAddress: from
899
+ }).buildTx();
900
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
901
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
902
+ if (receipt.status === "reverted") {
903
+ throw new StableTransactionError({
904
+ phase: "receipt",
905
+ shortMessage: "earn forceRedeem: vault forceRedeem reverted on-chain",
906
+ txHash: receipt.transactionHash,
907
+ chainId: this.config.chainId
908
+ });
909
+ }
910
+ return { txHash: receipt.transactionHash, idempotencyKey };
911
+ }
912
+ _buildDeallocs(deallocations, assetDecimals) {
913
+ return deallocations.map((d) => ({
914
+ adapter: d.adapter,
915
+ amount: parseUnits3(String(d.amount), assetDecimals),
916
+ // MarketParams in the Morpho SDK has computed fields (id, liquidationIncentiveFactor)
917
+ // not present in our user-facing type; the plain object is structurally compatible for ABI encoding.
918
+ ...d.marketParams && { marketParams: d.marketParams }
919
+ }));
920
+ }
921
+ // ─── Helpers ─────────────────────────────────────────────────────────────
922
+ _requireFrom() {
923
+ const from = this.wc.account?.address;
924
+ if (!from) {
925
+ throw new StableValidationError(
926
+ "signer",
927
+ void 0,
928
+ "earn: requires a signer \u2014 set walletClient or account in StableConfig"
929
+ );
930
+ }
931
+ return from;
932
+ }
933
+ async _switchChain() {
934
+ try {
935
+ await this.wc.switchChain({ id: this.config.chainId });
936
+ } catch (err) {
937
+ if (err.code === 4001) {
938
+ throw new StableTransactionError({
939
+ phase: "switch_chain",
940
+ shortMessage: `earn: wallet rejected switch to chain ${this.config.chainId}`,
941
+ chainId: this.config.chainId,
942
+ cause: err
943
+ });
944
+ }
945
+ }
946
+ }
947
+ async _sendTx(to, data, value, from) {
948
+ try {
949
+ return await this.wc.sendTransaction({ to, data, value, account: from, chain: this.chain });
950
+ } catch (err) {
951
+ throw new StableTransactionError({
952
+ phase: "send",
953
+ shortMessage: "earn: transaction send failed",
954
+ chainId: this.config.chainId,
955
+ cause: err
956
+ });
957
+ }
958
+ }
959
+ async _resolveDecimals(asset, op) {
960
+ try {
961
+ return await retryWithBackoff(
962
+ () => this.pc.readContract({ address: asset, abi: erc20Abi3, functionName: "decimals" })
963
+ );
964
+ } catch {
965
+ throw new StableValidationError(
966
+ "tokenDecimals",
967
+ void 0,
968
+ `${op}: could not fetch decimals for asset "${asset}" \u2014 pass tokenDecimals explicitly`
969
+ );
970
+ }
971
+ }
972
+ _withIdempotency(key, fn) {
973
+ if (!key) return fn();
974
+ const existing = this.inFlight.get(key);
975
+ if (existing) return existing;
976
+ const promise = fn().finally(() => this.inFlight.delete(key));
977
+ this.inFlight.set(key, promise);
978
+ return promise;
979
+ }
980
+ _validateAmount(amount, op) {
981
+ if (!Number.isFinite(amount) || amount <= 0) {
982
+ throw new StableValidationError("amount", amount, `${op}: amount must be a finite positive number`);
983
+ }
984
+ }
985
+ _validateShares(shares, op) {
986
+ if (!Number.isFinite(shares) || shares <= 0) {
987
+ throw new StableValidationError("shares", shares, `${op}: shares must be a finite positive number`);
988
+ }
989
+ }
990
+ };
991
+
774
992
  // src/stable.ts
775
- function isUsdt0Route(fromChain, toChain, fromToken, toToken) {
776
- const src = BRIDGE_CHAIN_CONFIGS[fromChain];
777
- const dst = BRIDGE_CHAIN_CONFIGS[toChain];
778
- return !!src && !!dst && src.lzOutbound !== false && src.usdt.toLowerCase() === fromToken.toLowerCase() && dst.usdt.toLowerCase() === toToken.toLowerCase();
779
- }
780
993
  function validateTransferParams({ to, amount, token, tokenDecimals }) {
781
994
  if (!isAddress3(to)) {
782
995
  throw new StableValidationError("to", to, `transfer: invalid recipient address "${to}"`);
@@ -794,7 +1007,7 @@ function validateTransferParams({ to, amount, token, tokenDecimals }) {
794
1007
  async function resolveTokenDecimals(publicClient, token, tokenDecimals) {
795
1008
  if (tokenDecimals !== void 0) return tokenDecimals;
796
1009
  try {
797
- return await publicClient.readContract({ address: token, abi: erc20Abi3, functionName: "decimals" });
1010
+ return await publicClient.readContract({ address: token, abi: erc20Abi4, functionName: "decimals" });
798
1011
  } catch {
799
1012
  throw new StableValidationError(
800
1013
  "tokenDecimals",
@@ -815,9 +1028,9 @@ async function executeTokenTransfer(wc, publicClient, token, to, amount, tokenDe
815
1028
  const decimals = await resolveTokenDecimals(publicClient, token, tokenDecimals);
816
1029
  return wc.writeContract({
817
1030
  address: token,
818
- abi: erc20Abi3,
1031
+ abi: erc20Abi4,
819
1032
  functionName: "transfer",
820
- args: [to, parseUnits3(String(amount), decimals)],
1033
+ args: [to, parseUnits4(String(amount), decimals)],
821
1034
  account: acc,
822
1035
  chain
823
1036
  });
@@ -827,13 +1040,24 @@ function createStable({
827
1040
  rpc: rpcOverride,
828
1041
  account,
829
1042
  transport,
830
- walletClient
1043
+ walletClient,
1044
+ earn: earnConfig
831
1045
  }) {
832
- const chain = network === "mainnet" /* Mainnet */ ? stableMainnet : stableTestnet;
1046
+ const chain = network === "mainnet" /* Mainnet */ ? stableMainnet2 : stableTestnet2;
833
1047
  const rpc = (rpcOverride ?? chain.rpcUrls.default.http[0]).replace(/\/$/, "");
834
1048
  const signerConfig = { account, transport, walletClient };
835
- const lz = new LayerZeroService(signerConfig);
836
1049
  const lifi = new LiFiService({ ...signerConfig, rpc, chainId: chain.id });
1050
+ const morpho = earnConfig ? new MorphoService({ ...signerConfig, rpc, chainId: chain.id, vault: earnConfig.vault }) : null;
1051
+ function requireEarn() {
1052
+ if (!morpho) {
1053
+ throw new StableValidationError(
1054
+ "earn",
1055
+ void 0,
1056
+ "stable.earn.*: pass earn: { vault: '0x...' } to createStable() to enable earn operations"
1057
+ );
1058
+ }
1059
+ return morpho;
1060
+ }
837
1061
  async function transfer(params) {
838
1062
  validateTransferParams(params);
839
1063
  const { from, to, amount, token, tokenDecimals } = params;
@@ -841,7 +1065,7 @@ function createStable({
841
1065
  const wc = walletClient ?? createWalletClient3({
842
1066
  account: acc,
843
1067
  chain,
844
- transport: transport ?? http3(rpc)
1068
+ transport: transport ?? http4(rpc)
845
1069
  });
846
1070
  try {
847
1071
  await wc.switchChain({ id: chain.id });
@@ -853,8 +1077,8 @@ function createStable({
853
1077
  cause: err
854
1078
  });
855
1079
  }
856
- const publicClient = createPublicClient3({ chain, transport: http3(rpc) });
857
- const hash = token !== void 0 ? await executeTokenTransfer(wc, publicClient, token, to, amount, tokenDecimals, acc, chain) : await wc.sendTransaction({ account: acc, chain, to, value: parseUnits3(String(amount), 18) });
1080
+ const publicClient = createPublicClient4({ chain, transport: http4(rpc) });
1081
+ 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) });
858
1082
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
859
1083
  if (receipt.status === "reverted") {
860
1084
  throw new StableTransactionError({
@@ -866,28 +1090,48 @@ function createStable({
866
1090
  }
867
1091
  return { txHash: receipt.transactionHash };
868
1092
  }
869
- function quoteBridge(params) {
870
- return isUsdt0Route(params.fromChain, params.toChain, params.fromToken, params.toToken) ? lz.quoteBridge(params) : lifi.quoteBridge(params);
871
- }
872
- function bridge(params) {
873
- return isUsdt0Route(params.fromChain, params.toChain, params.fromToken, params.toToken) ? lz.bridge(params) : lifi.bridge(params);
874
- }
875
1093
  return {
876
1094
  transfer,
877
- quoteBridge,
878
- bridge,
1095
+ quoteBridge: (params) => lifi.quoteBridge(params),
1096
+ bridge: (params) => lifi.bridge(params),
879
1097
  quoteSwap: (params) => lifi.quoteSwap(params),
880
- swap: (params) => lifi.swap(params)
1098
+ swap: (params) => lifi.swap(params),
1099
+ earn: {
1100
+ deposit: (params) => requireEarn().deposit(params),
1101
+ bulkDeposit: (items, options) => requireEarn().bulkDeposit(items, options),
1102
+ withdraw: (params) => requireEarn().withdraw(params),
1103
+ redeem: (params) => requireEarn().redeem(params),
1104
+ forceWithdraw: (params) => requireEarn().forceWithdraw(params),
1105
+ forceRedeem: (params) => requireEarn().forceRedeem(params)
1106
+ }
881
1107
  };
882
1108
  }
1109
+ function createStableReader(config) {
1110
+ const reader = new EarnReader(config);
1111
+ return {
1112
+ earn: {
1113
+ position: () => reader.position(),
1114
+ preview: (params) => reader.preview(params),
1115
+ withdrawability: (params) => reader.withdrawability(params)
1116
+ }
1117
+ };
1118
+ }
1119
+
1120
+ // src/constants.ts
1121
+ var STABLE_USDT_ADDRESS = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
1122
+ var STABLE_VAULT_ADDRESS = "0x461eF73F640456138242398a39AB48a557A49293";
883
1123
  export {
884
1124
  CHAIN_CONFIGS,
885
1125
  Chain,
886
1126
  Network,
1127
+ STABLE_USDT_ADDRESS,
1128
+ STABLE_VAULT_ADDRESS,
1129
+ StableBulkError,
887
1130
  StableError,
888
1131
  StableNetworkError,
889
1132
  StableQuoteError,
890
1133
  StableTransactionError,
891
1134
  StableValidationError,
892
- createStable
1135
+ createStable,
1136
+ createStableReader
893
1137
  };