@stablechain/sdk 1.0.1 → 1.0.2

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.
Files changed (3) hide show
  1. package/dist/index.cjs +1129 -800
  2. package/dist/index.js +1145 -816
  3. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -1,25 +1,29 @@
1
1
  // src/stable.ts
2
2
  import {
3
+ http as http4,
3
4
  createPublicClient as createPublicClient4,
4
5
  createWalletClient as createWalletClient3,
5
6
  erc20Abi as erc20Abi4,
6
- http as http4,
7
7
  isAddress as isAddress3,
8
8
  parseUnits as parseUnits4
9
9
  } from "viem";
10
10
  import { stable as stableMainnet2, stableTestnet as stableTestnet2 } from "viem/chains";
11
11
 
12
- // src/earn/reader.ts
12
+ // src/earn/morpho.ts
13
+ import {
14
+ MorphoClient,
15
+ isRequirementApproval,
16
+ isRequirementSignature
17
+ } from "@morpho-org/morpho-sdk";
13
18
  import {
19
+ http,
14
20
  createPublicClient,
21
+ createWalletClient,
22
+ custom,
15
23
  erc20Abi,
16
24
  formatUnits,
17
- http,
18
- isAddress,
19
25
  parseUnits
20
26
  } from "viem";
21
- import { stable as stableMainnet, stableTestnet } from "viem/chains";
22
- import { MorphoClient } from "@morpho-org/morpho-sdk";
23
27
 
24
28
  // src/errors.ts
25
29
  import { BaseError } from "viem";
@@ -39,7 +43,8 @@ var StableValidationError = class extends StableError {
39
43
  var StableQuoteError = class extends StableError {
40
44
  constructor(args) {
41
45
  const meta = [`Provider: ${args.provider}`];
42
- if (args.httpStatus !== void 0) meta.push(`HTTP status: ${args.httpStatus}`);
46
+ if (args.httpStatus !== void 0)
47
+ meta.push(`HTTP status: ${args.httpStatus}`);
43
48
  if (args.providerCode) meta.push(`Provider code: ${args.providerCode}`);
44
49
  super(args.shortMessage, {
45
50
  name: "StableQuoteError",
@@ -142,937 +147,1228 @@ async function retryWithBackoff(fn, isRetryable = isRetryableError, opts = {}) {
142
147
  throw lastErr;
143
148
  }
144
149
 
145
- // src/types.ts
146
- var Chain = /* @__PURE__ */ ((Chain2) => {
147
- Chain2["Sepolia"] = "sepolia";
148
- Chain2["StableTestnet"] = "stable-testnet";
149
- Chain2["Stable"] = "stable";
150
- Chain2["Ethereum"] = "ethereum";
151
- Chain2["Arbitrum"] = "arbitrum";
152
- Chain2["Ink"] = "ink";
153
- Chain2["Bera"] = "bera";
154
- Chain2["MegaETH"] = "megaeth";
155
- Chain2["Base"] = "base";
156
- Chain2["BSC"] = "bsc";
157
- Chain2["HyperEVM"] = "hyperevm";
158
- return Chain2;
159
- })(Chain || {});
160
- var Network = /* @__PURE__ */ ((Network2) => {
161
- Network2["Mainnet"] = "mainnet";
162
- Network2["Testnet"] = "testnet";
163
- return Network2;
164
- })(Network || {});
165
-
166
- // src/earn/reader.ts
167
- var MORPHO_API_URL = "https://blue-api.morpho.org/graphql";
168
- var EarnReader = class {
150
+ // src/earn/morpho.ts
151
+ var MorphoService = class {
169
152
  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]
153
+ this.config = config;
154
+ this.inFlight = /* @__PURE__ */ new Map();
155
+ this.chain = makeChain(config.chainId, config.rpc);
156
+ this.wc = config.walletClient ?? createWalletClient({
157
+ account: config.account,
158
+ chain: this.chain,
159
+ transport: config.transport ?? http(config.rpc)
160
+ });
161
+ const morphoWc = this.wc.chain?.id === config.chainId ? this.wc : createWalletClient({
162
+ account: this.wc.account,
163
+ chain: this.chain,
164
+ transport: custom({
165
+ request: this.wc.request.bind(this.wc)
210
166
  })
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
167
  });
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
- };
168
+ this.morpho = new MorphoClient(morphoWc, { supportSignature: true });
169
+ this.pc = createPublicClient({
170
+ chain: this.chain,
171
+ transport: http(config.rpc)
172
+ });
253
173
  }
254
- async _fetchNetApy() {
255
- const { apy } = await this._fetchYield();
256
- return apy;
174
+ deposit(params) {
175
+ this._validateAmount(params.amount, "earn deposit");
176
+ if (params.depositBuffer !== void 0 && (params.depositBuffer <= 0 || params.depositBuffer > 1)) {
177
+ throw new StableValidationError(
178
+ "depositBuffer",
179
+ params.depositBuffer,
180
+ "earn deposit: depositBuffer must be > 0 and \u2264 1"
181
+ );
182
+ }
183
+ return this._withIdempotency(
184
+ params.idempotencyKey,
185
+ () => this._deposit(params)
186
+ );
257
187
  }
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
188
+ async bulkDeposit(items, options = {}) {
189
+ const { idempotencyKey, stopOnError = false } = options;
190
+ const run = async () => {
191
+ const results = [];
192
+ for (const item of items) {
193
+ try {
194
+ const result = await this.deposit(item);
195
+ results.push({
196
+ status: "fulfilled",
197
+ txHash: result.txHash,
198
+ vault: item.vault,
199
+ idempotencyKey: item.idempotencyKey
200
+ });
201
+ } catch (err) {
202
+ const entry = {
203
+ status: "rejected",
204
+ error: String(err),
205
+ vault: item.vault,
206
+ idempotencyKey: item.idempotencyKey
207
+ };
208
+ results.push(entry);
209
+ if (stopOnError) {
210
+ const succeeded2 = results.filter(
211
+ (r) => r.status === "fulfilled"
212
+ ).length;
213
+ throw new StableBulkError({
214
+ shortMessage: "bulkDeposit halted on first failure",
215
+ results,
216
+ succeeded: succeeded2,
217
+ failed: results.length - succeeded2
218
+ });
269
219
  }
270
220
  }
271
221
  }
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
- })
222
+ const succeeded = results.filter(
223
+ (r) => r.status === "fulfilled"
224
+ ).length;
225
+ return {
226
+ results,
227
+ succeeded,
228
+ failed: results.length - succeeded,
229
+ idempotencyKey
230
+ };
231
+ };
232
+ return this._withIdempotency(idempotencyKey, run);
233
+ }
234
+ async bulkWithdraw(items, options = {}) {
235
+ const { idempotencyKey, stopOnError = false } = options;
236
+ const run = async () => {
237
+ const results = [];
238
+ for (const item of items) {
239
+ try {
240
+ const result = await this.withdraw(item);
241
+ results.push({
242
+ status: "fulfilled",
243
+ txHash: result.txHash,
244
+ idempotencyKey: item.idempotencyKey
284
245
  });
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
- );
246
+ } catch (err) {
247
+ const entry = {
248
+ status: "rejected",
249
+ error: String(err),
250
+ idempotencyKey: item.idempotencyKey
251
+ };
252
+ results.push(entry);
253
+ if (stopOnError) {
254
+ const succeeded2 = results.filter(
255
+ (r) => r.status === "fulfilled"
256
+ ).length;
257
+ throw new StableBulkError({
258
+ shortMessage: "bulkWithdraw halted on first failure",
259
+ results,
260
+ succeeded: succeeded2,
261
+ failed: results.length - succeeded2
262
+ });
292
263
  }
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
- });
264
+ }
304
265
  }
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
266
+ const succeeded = results.filter(
267
+ (r) => r.status === "fulfilled"
268
+ ).length;
269
+ return {
270
+ results,
271
+ succeeded,
272
+ failed: results.length - succeeded,
273
+ idempotencyKey
274
+ };
322
275
  };
276
+ return this._withIdempotency(idempotencyKey, run);
323
277
  }
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
-
337
- // src/chains.ts
338
- var CHAIN_CONFIGS = {
339
- // ─── Testnet ──────────────────────────────────────────────────────────────
340
- ["sepolia" /* Sepolia */]: {
341
- id: 11155111,
342
- rpc: "https://ethereum-sepolia-rpc.publicnode.com",
343
- usdt: "0xc4DCC311c028e341fd8602D8eB89c5de94625927",
344
- decimals: 18
345
- },
346
- ["stable-testnet" /* StableTestnet */]: {
347
- id: 2201,
348
- rpc: "https://rpc.testnet.stable.xyz",
349
- usdt: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9",
350
- decimals: 6
351
- },
352
- // ─── Mainnet ──────────────────────────────────────────────────────────────
353
- ["stable" /* Stable */]: {
354
- id: 988,
355
- rpc: "https://rpc.stable.xyz",
356
- usdt: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
357
- decimals: 6
358
- },
359
- ["ethereum" /* Ethereum */]: {
360
- id: 1,
361
- rpc: "https://ethereum-rpc.publicnode.com",
362
- usdt: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee",
363
- decimals: 6
364
- },
365
- ["arbitrum" /* Arbitrum */]: {
366
- id: 42161,
367
- rpc: "https://arbitrum-one-rpc.publicnode.com",
368
- usdt: "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92",
369
- decimals: 6
370
- },
371
- ["ink" /* Ink */]: {
372
- id: 57073,
373
- rpc: "https://rpc-gel.inkonchain.com",
374
- usdt: "0x0200C29006150606B650577BBE7B6248F58470c1",
375
- decimals: 6
376
- },
377
- ["bera" /* Bera */]: {
378
- id: 80094,
379
- rpc: "https://rpc.berachain.com",
380
- usdt: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
381
- decimals: 6
382
- },
383
- ["megaeth" /* MegaETH */]: {
384
- id: 4326,
385
- rpc: "https://carrot.megaeth.com",
386
- usdt: "0xb8ce59fc3717ada4c02eadf9682a9e934f625ebb",
387
- decimals: 6
388
- },
389
- ["base" /* Base */]: {
390
- id: 8453,
391
- rpc: "https://mainnet.base.org",
392
- usdt: "0x102d758f688a4C1C5a80b116bD945d4455460282",
393
- decimals: 6
394
- },
395
- ["bsc" /* BSC */]: {
396
- id: 56,
397
- rpc: "https://bsc-rpc.publicnode.com",
398
- usdt: "0x55d398326f99059fF775485246999027B3197955",
399
- decimals: 18
400
- },
401
- ["hyperevm" /* HyperEVM */]: {
402
- id: 999,
403
- rpc: "https://rpc.hyperliquid.xyz/evm",
404
- usdt: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb",
405
- decimals: 6
406
- }
407
- };
408
-
409
- // src/services/lifi.ts
410
- var LiFiService = class {
411
- constructor(config) {
412
- this.config = config;
278
+ withdraw(params) {
279
+ this._validateAmount(params.amount, "earn withdraw");
280
+ return this._withIdempotency(
281
+ params.idempotencyKey,
282
+ () => this._withdraw(params)
283
+ );
413
284
  }
414
- async quoteSwap(params) {
415
- this._validateSwapParams(params);
416
- const { fromToken, toToken, amount, fromDecimals = 6, toAddress } = params;
417
- const from = this._requireFrom();
418
- const chain = this.config.chainId.toString();
419
- const fromAmountRaw = parseUnits2(String(amount), fromDecimals).toString();
420
- const qs = new URLSearchParams({
421
- fromChain: chain,
422
- toChain: chain,
423
- fromToken,
424
- toToken,
425
- fromAmount: fromAmountRaw,
426
- fromAddress: from,
427
- ...toAddress && { toAddress }
428
- });
429
- const quote = await this._fetchQuote(qs);
430
- return this._toSwapQuote(quote);
285
+ redeem(params) {
286
+ this._validateShares(params.shares, "earn redeem");
287
+ return this._withIdempotency(
288
+ params.idempotencyKey,
289
+ () => this._redeem(params)
290
+ );
431
291
  }
432
- async swap(params) {
433
- this._validateSwapParams(params);
434
- const quote = params.quote ?? await this.quoteSwap(params);
435
- return this._executeSwapQuote(quote, this.config.rpc, this.config.chainId);
292
+ forceWithdraw(params) {
293
+ this._validateAmount(params.amount, "earn forceWithdraw");
294
+ return this._withIdempotency(
295
+ params.idempotencyKey,
296
+ () => this._forceWithdraw(params)
297
+ );
436
298
  }
437
- async quoteBridge(params) {
438
- this._validateBridgeParams(params);
439
- const { fromChain, toChain, fromToken, toToken, amount, fromDecimals = 6, recipient } = params;
440
- const from = this.config.walletClient?.account?.address ?? this.config.account?.address ?? "0x0000000000000000000000000000000000000000";
441
- const src = CHAIN_CONFIGS[fromChain];
442
- const dst = CHAIN_CONFIGS[toChain];
443
- if (!src) throw new StableValidationError("fromChain", fromChain, `LI.FI bridge: unsupported source chain "${fromChain}"`);
444
- if (!dst) throw new StableValidationError("toChain", toChain, `LI.FI bridge: unsupported destination chain "${toChain}"`);
445
- const qs = new URLSearchParams({
446
- fromChain: src.id.toString(),
447
- toChain: dst.id.toString(),
448
- fromToken,
449
- toToken,
450
- fromAmount: parseUnits2(String(amount), fromDecimals).toString(),
451
- fromAddress: from,
452
- ...recipient && { toAddress: recipient }
453
- });
454
- const quote = this._toSwapQuote(await this._fetchQuote(qs));
455
- return { toAmount: quote.toAmount };
299
+ forceRedeem(params) {
300
+ this._validateShares(params.shares, "earn forceRedeem");
301
+ return this._withIdempotency(
302
+ params.idempotencyKey,
303
+ () => this._forceRedeem(params)
304
+ );
456
305
  }
457
- async bridge(params) {
458
- this._validateBridgeParams(params);
459
- const { fromChain, toChain, fromToken, toToken, amount, fromDecimals = 6, recipient } = params;
306
+ // ─── Private operations ──────────────────────────────────────────────────
307
+ async _deposit(params) {
308
+ const {
309
+ amount,
310
+ tokenDecimals,
311
+ slippageTolerance,
312
+ idempotencyKey,
313
+ depositBuffer
314
+ } = params;
460
315
  const from = this._requireFrom();
461
- const src = CHAIN_CONFIGS[fromChain];
462
- const dst = CHAIN_CONFIGS[toChain];
463
- if (!src) {
464
- throw new StableValidationError("fromChain", fromChain, `LI.FI bridge: unsupported source chain "${fromChain}"`);
465
- }
466
- if (!dst) {
467
- throw new StableValidationError("toChain", toChain, `LI.FI bridge: unsupported destination chain "${toChain}"`);
316
+ const vaultAddr = params.vault ?? this.config.vault;
317
+ await this._switchChain();
318
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
319
+ const vaultData = await vault.getData();
320
+ const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn deposit");
321
+ const assetsRaw = parseUnits(String(amount), decimals);
322
+ if (depositBuffer !== void 0) {
323
+ const bps = BigInt(Math.round(depositBuffer * 1e4));
324
+ const maxAllowed = vaultData.assetBalance * bps / 10000n;
325
+ if (assetsRaw > maxAllowed) {
326
+ const maxFormatted = Number(formatUnits(maxAllowed, decimals));
327
+ const idleFormatted = Number(
328
+ formatUnits(vaultData.assetBalance, decimals)
329
+ );
330
+ throw new StableValidationError(
331
+ "amount",
332
+ amount,
333
+ `earn deposit: amount ${amount} exceeds deposit buffer \u2014 max ${maxFormatted} (${depositBuffer * 100}% of ${idleFormatted} idle liquidity)`
334
+ );
335
+ }
468
336
  }
469
- const fromAmountRaw = parseUnits2(String(amount), fromDecimals).toString();
470
- const qs = new URLSearchParams({
471
- fromChain: src.id.toString(),
472
- toChain: dst.id.toString(),
473
- fromToken,
474
- toToken,
475
- fromAmount: fromAmountRaw,
476
- fromAddress: from,
477
- ...recipient && { toAddress: recipient }
337
+ const depositAction = vault.deposit({
338
+ amount: assetsRaw,
339
+ userAddress: from,
340
+ accrualVault: vaultData,
341
+ slippageTolerance: slippageTolerance !== void 0 ? (
342
+ // Morpho SDK expects slippage as a WAD-scaled fraction (1e18 = 100%)
343
+ parseUnits(String(slippageTolerance), 18)
344
+ ) : void 0
478
345
  });
479
- const quote = this._toSwapQuote(await this._fetchQuote(qs));
480
- return this._executeSwapQuote(quote, src.rpc, src.id);
481
- }
482
- _validateSwapParams({ fromToken, toToken, amount, toAddress }) {
483
- if (!isAddress2(fromToken)) {
484
- throw new StableValidationError("fromToken", fromToken, `swap: invalid fromToken address "${fromToken}"`);
485
- }
486
- if (!isAddress2(toToken)) {
487
- throw new StableValidationError("toToken", toToken, `swap: invalid toToken address "${toToken}"`);
488
- }
489
- if (fromToken.toLowerCase() === toToken.toLowerCase()) {
490
- throw new StableValidationError("toToken", toToken, "swap: fromToken and toToken must differ");
491
- }
492
- if (!Number.isFinite(amount) || amount <= 0) {
493
- throw new StableValidationError("amount", amount, "swap: amount must be a finite positive number");
494
- }
495
- if (toAddress !== void 0 && !isAddress2(toAddress)) {
496
- throw new StableValidationError("toAddress", toAddress, `swap: invalid toAddress "${toAddress}"`);
497
- }
498
- }
499
- _validateBridgeParams({ fromChain, toChain, fromToken, toToken, amount, recipient }) {
500
- if (!isAddress2(fromToken)) {
501
- throw new StableValidationError("fromToken", fromToken, `LI.FI bridge: invalid fromToken address "${fromToken}"`);
502
- }
503
- if (!isAddress2(toToken)) {
504
- throw new StableValidationError("toToken", toToken, `LI.FI bridge: invalid toToken address "${toToken}"`);
505
- }
506
- if (!Number.isFinite(amount) || amount <= 0) {
507
- throw new StableValidationError("amount", amount, "LI.FI bridge: amount must be a finite positive number");
508
- }
509
- if (recipient !== void 0 && !isAddress2(recipient)) {
510
- throw new StableValidationError("recipient", recipient, `LI.FI bridge: invalid recipient address "${recipient}"`);
346
+ const requirements = await depositAction.getRequirements();
347
+ let sig;
348
+ for (const req of requirements) {
349
+ if (isRequirementApproval(req)) {
350
+ const approveHash = await this._sendTx(
351
+ req.to,
352
+ req.data,
353
+ req.value,
354
+ from
355
+ );
356
+ const approveReceipt = await this.pc.waitForTransactionReceipt({
357
+ hash: approveHash
358
+ });
359
+ if (approveReceipt.status === "reverted") {
360
+ throw new StableTransactionError({
361
+ phase: "approve",
362
+ shortMessage: "earn deposit: token approval reverted on-chain",
363
+ txHash: approveReceipt.transactionHash,
364
+ chainId: this.config.chainId
365
+ });
366
+ }
367
+ } else if (isRequirementSignature(req)) {
368
+ sig = await req.sign(this.wc, from);
369
+ }
511
370
  }
512
- if (fromChain === toChain) {
513
- throw new StableValidationError("toChain", toChain, "LI.FI bridge: fromChain and toChain must differ");
371
+ const tx = depositAction.buildTx(sig);
372
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
373
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
374
+ if (receipt.status === "reverted") {
375
+ throw new StableTransactionError({
376
+ phase: "receipt",
377
+ shortMessage: "earn deposit: vault deposit reverted on-chain",
378
+ txHash: receipt.transactionHash,
379
+ chainId: this.config.chainId
380
+ });
514
381
  }
382
+ return { txHash: receipt.transactionHash, idempotencyKey };
515
383
  }
516
- _requireFrom() {
517
- const from = this.config.walletClient?.account?.address ?? this.config.account?.address;
518
- if (!from) {
519
- throw new StableValidationError(
520
- "signer",
521
- void 0,
522
- "LI.FI: requires a signer \u2014 set walletClient or account in StableConfig"
523
- );
384
+ async _withdraw(params) {
385
+ const { amount, tokenDecimals, idempotencyKey } = params;
386
+ const from = this._requireFrom();
387
+ const vaultAddr = this.config.vault;
388
+ await this._switchChain();
389
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
390
+ const vaultData = await vault.getData();
391
+ const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn withdraw");
392
+ const assetsRaw = parseUnits(String(amount), decimals);
393
+ const tx = vault.withdraw({ amount: assetsRaw, userAddress: from }).buildTx();
394
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
395
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
396
+ if (receipt.status === "reverted") {
397
+ throw new StableTransactionError({
398
+ phase: "receipt",
399
+ shortMessage: "earn withdraw: vault withdraw reverted on-chain",
400
+ txHash: receipt.transactionHash,
401
+ chainId: this.config.chainId
402
+ });
524
403
  }
525
- return from;
404
+ return { txHash: receipt.transactionHash, idempotencyKey };
526
405
  }
527
- async _fetchQuote(qs) {
528
- const url = `https://li.quest/v1/quote?${qs}`;
529
- try {
530
- return await retryWithBackoff(() => this._fetchQuoteOnce(url));
531
- } catch (err) {
532
- if (err instanceof StableQuoteError) throw err;
533
- if (err instanceof HttpError) {
534
- throw new StableQuoteError({
535
- provider: "lifi",
536
- shortMessage: err.message,
537
- httpStatus: err.status,
538
- cause: err
406
+ async _redeem(params) {
407
+ const { shares, shareDecimals, idempotencyKey } = params;
408
+ const from = this._requireFrom();
409
+ const vaultAddr = this.config.vault;
410
+ await this._switchChain();
411
+ let decimals = shareDecimals;
412
+ if (decimals === void 0) {
413
+ try {
414
+ decimals = await this.pc.readContract({
415
+ address: vaultAddr,
416
+ abi: erc20Abi,
417
+ functionName: "decimals"
539
418
  });
419
+ } catch {
420
+ throw new StableValidationError(
421
+ "shareDecimals",
422
+ void 0,
423
+ `earn redeem: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
424
+ );
540
425
  }
541
- throw new StableNetworkError({
542
- shortMessage: "LI.FI: quote request failed before reaching the server",
543
- url,
544
- cause: err
426
+ }
427
+ const sharesRaw = parseUnits(String(shares), decimals);
428
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
429
+ const tx = vault.redeem({ shares: sharesRaw, userAddress: from }).buildTx();
430
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
431
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
432
+ if (receipt.status === "reverted") {
433
+ throw new StableTransactionError({
434
+ phase: "receipt",
435
+ shortMessage: "earn redeem: vault redeem reverted on-chain",
436
+ txHash: receipt.transactionHash,
437
+ chainId: this.config.chainId
545
438
  });
546
439
  }
440
+ return { txHash: receipt.transactionHash, idempotencyKey };
547
441
  }
548
- async _fetchQuoteOnce(url) {
549
- const res = await fetch(url);
550
- const body = await res.json().catch(() => void 0);
551
- if (!res.ok) {
552
- const providerMessage = isObject(body) && typeof body.message === "string" ? body.message : void 0;
553
- const providerCode = isObject(body) && typeof body.code === "string" ? body.code : void 0;
554
- if (res.status === 429 || res.status >= 500) {
555
- throw new HttpError(`LI.FI: ${providerMessage ?? res.statusText}`, res.status, true);
556
- }
557
- throw new StableQuoteError({
558
- provider: "lifi",
559
- shortMessage: `LI.FI: ${providerMessage ?? res.statusText}`,
560
- httpStatus: res.status,
561
- providerCode,
562
- body
563
- });
442
+ async _forceWithdraw(params) {
443
+ const { amount, tokenDecimals, deallocations, idempotencyKey } = params;
444
+ const from = this._requireFrom();
445
+ const vaultAddr = this.config.vault;
446
+ await this._switchChain();
447
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
448
+ let decimals = tokenDecimals;
449
+ if (decimals === void 0) {
450
+ const vaultData = await vault.getData();
451
+ decimals = await this._resolveDecimals(
452
+ vaultData.asset,
453
+ "earn forceWithdraw"
454
+ );
564
455
  }
565
- if (!isLiFiQuoteShape(body)) {
566
- throw new StableQuoteError({
567
- provider: "lifi",
568
- shortMessage: "LI.FI: unexpected quote response shape",
569
- httpStatus: res.status,
570
- body
456
+ const tx = vault.forceWithdraw({
457
+ deallocations: this._buildDeallocs(deallocations, decimals),
458
+ withdraw: { amount: parseUnits(String(amount), decimals) },
459
+ userAddress: from
460
+ }).buildTx();
461
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
462
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
463
+ if (receipt.status === "reverted") {
464
+ throw new StableTransactionError({
465
+ phase: "receipt",
466
+ shortMessage: "earn forceWithdraw: vault forceWithdraw reverted on-chain",
467
+ txHash: receipt.transactionHash,
468
+ chainId: this.config.chainId
571
469
  });
572
470
  }
573
- return body;
574
- }
575
- _toSwapQuote(quote) {
576
- return {
577
- toAmount: Number(formatUnits2(BigInt(quote.estimate.toAmount), quote.action.toToken.decimals)),
578
- fromAmount: BigInt(quote.estimate.fromAmount),
579
- fromToken: quote.action.fromToken.address,
580
- approvalAddress: quote.estimate.approvalAddress,
581
- transactionRequest: quote.transactionRequest
582
- };
471
+ return { txHash: receipt.transactionHash, idempotencyKey };
583
472
  }
584
- async _executeSwapQuote(quote, srcRpc, expectedChainId) {
473
+ async _forceRedeem(params) {
474
+ const {
475
+ shares,
476
+ shareDecimals,
477
+ tokenDecimals,
478
+ deallocations,
479
+ idempotencyKey
480
+ } = params;
585
481
  const from = this._requireFrom();
586
- const { transactionRequest } = quote;
587
- const srcChainId = transactionRequest.chainId;
588
- if (srcChainId !== expectedChainId) {
589
- throw new StableQuoteError({
590
- provider: "lifi",
591
- shortMessage: `LI.FI: returned tx for chain ${srcChainId} but expected ${expectedChainId}`,
592
- providerCode: "chain_mismatch",
593
- body: quote
482
+ const vaultAddr = this.config.vault;
483
+ await this._switchChain();
484
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
485
+ let shareDec = shareDecimals;
486
+ if (shareDec === void 0) {
487
+ try {
488
+ shareDec = await this.pc.readContract({
489
+ address: vaultAddr,
490
+ abi: erc20Abi,
491
+ functionName: "decimals"
492
+ });
493
+ } catch {
494
+ throw new StableValidationError(
495
+ "shareDecimals",
496
+ void 0,
497
+ `earn forceRedeem: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
498
+ );
499
+ }
500
+ }
501
+ let assetDec = tokenDecimals;
502
+ if (assetDec === void 0) {
503
+ const vaultData = await vault.getData();
504
+ assetDec = await this._resolveDecimals(
505
+ vaultData.asset,
506
+ "earn forceRedeem"
507
+ );
508
+ }
509
+ const tx = vault.forceRedeem({
510
+ deallocations: this._buildDeallocs(deallocations, assetDec),
511
+ redeem: { shares: parseUnits(String(shares), shareDec) },
512
+ userAddress: from
513
+ }).buildTx();
514
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
515
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
516
+ if (receipt.status === "reverted") {
517
+ throw new StableTransactionError({
518
+ phase: "receipt",
519
+ shortMessage: "earn forceRedeem: vault forceRedeem reverted on-chain",
520
+ txHash: receipt.transactionHash,
521
+ chainId: this.config.chainId
594
522
  });
595
523
  }
596
- const srcChain = makeChain(srcChainId, srcRpc);
597
- const wc = this.config.walletClient ?? createWalletClient({
598
- account: this.config.account,
599
- chain: srcChain,
600
- transport: this.config.transport ?? http2(srcRpc)
601
- });
524
+ return { txHash: receipt.transactionHash, idempotencyKey };
525
+ }
526
+ _buildDeallocs(deallocations, assetDecimals) {
527
+ return deallocations.map((d) => ({
528
+ adapter: d.adapter,
529
+ amount: parseUnits(String(d.amount), assetDecimals),
530
+ // MarketParams in the Morpho SDK has computed fields (id, liquidationIncentiveFactor)
531
+ // not present in our user-facing type; the plain object is structurally compatible for ABI encoding.
532
+ ...d.marketParams && {
533
+ marketParams: d.marketParams
534
+ }
535
+ }));
536
+ }
537
+ // ─── Helpers ─────────────────────────────────────────────────────────────
538
+ _requireFrom() {
539
+ const from = this.wc.account?.address;
540
+ if (!from) {
541
+ throw new StableValidationError(
542
+ "signer",
543
+ void 0,
544
+ "earn: requires a signer \u2014 set walletClient or account in StableConfig"
545
+ );
546
+ }
547
+ return from;
548
+ }
549
+ async _switchChain() {
602
550
  try {
603
- await wc.switchChain({ id: srcChainId });
551
+ await this.wc.switchChain({ id: this.config.chainId });
604
552
  } catch (err) {
605
- throw new StableTransactionError({
606
- phase: "switch_chain",
607
- shortMessage: `LI.FI: wallet rejected or failed to switch to chain ${srcChainId}`,
608
- chainId: srcChainId,
609
- cause: err
610
- });
553
+ if (err.code === 4001) {
554
+ throw new StableTransactionError({
555
+ phase: "switch_chain",
556
+ shortMessage: `earn: wallet rejected switch to chain ${this.config.chainId}`,
557
+ chainId: this.config.chainId,
558
+ cause: err
559
+ });
560
+ }
611
561
  }
612
- await this._ensureApproval(wc, srcChain, srcRpc, srcChainId, from, quote);
613
- let hash;
562
+ }
563
+ async _sendTx(to, data, value, from) {
614
564
  try {
615
- hash = await wc.sendTransaction({
616
- to: transactionRequest.to,
617
- data: transactionRequest.data,
618
- value: transactionRequest.value && transactionRequest.value !== "0x" ? BigInt(transactionRequest.value) : 0n,
619
- gas: transactionRequest.gasLimit ? BigInt(transactionRequest.gasLimit) : void 0,
620
- account: wc.account,
621
- chain: srcChain
565
+ return await this.wc.sendTransaction({
566
+ to,
567
+ data,
568
+ value,
569
+ account: from,
570
+ chain: this.chain
622
571
  });
623
572
  } catch (err) {
624
573
  throw new StableTransactionError({
625
574
  phase: "send",
626
- shortMessage: "LI.FI: swap/bridge transaction failed",
627
- chainId: srcChainId,
575
+ shortMessage: "earn: transaction send failed",
576
+ chainId: this.config.chainId,
628
577
  cause: err
629
578
  });
630
579
  }
631
- return { txHash: hash, toAmount: quote.toAmount };
632
580
  }
633
- async _ensureApproval(wc, srcChain, srcRpc, srcChainId, from, quote) {
634
- const tokenAddr = quote.fromToken;
635
- const isNative = tokenAddr.toLowerCase() === "0x0000000000000000000000000000000000000000" || tokenAddr.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
636
- if (isNative || !quote.approvalAddress) return;
637
- const pc = createPublicClient2({ chain: srcChain, transport: http2(srcRpc) });
638
- const allowance = await pc.readContract({
639
- address: tokenAddr,
640
- abi: erc20Abi2,
641
- functionName: "allowance",
642
- args: [from, quote.approvalAddress]
643
- });
644
- if (allowance >= quote.fromAmount) return;
645
- let approveHash;
581
+ async _resolveDecimals(asset, op) {
646
582
  try {
647
- approveHash = await wc.writeContract({
648
- address: tokenAddr,
649
- abi: erc20Abi2,
650
- functionName: "approve",
651
- args: [quote.approvalAddress, quote.fromAmount],
652
- account: wc.account,
653
- chain: srcChain
654
- });
655
- } catch (err) {
656
- throw new StableTransactionError({
657
- phase: "approve",
658
- shortMessage: "LI.FI: token approve failed",
659
- chainId: srcChainId,
660
- cause: err
661
- });
583
+ return await retryWithBackoff(
584
+ () => this.pc.readContract({
585
+ address: asset,
586
+ abi: erc20Abi,
587
+ functionName: "decimals"
588
+ })
589
+ );
590
+ } catch {
591
+ throw new StableValidationError(
592
+ "tokenDecimals",
593
+ void 0,
594
+ `${op}: could not fetch decimals for asset "${asset}" \u2014 pass tokenDecimals explicitly`
595
+ );
662
596
  }
663
- const approveReceipt = await pc.waitForTransactionReceipt({ hash: approveHash });
664
- if (approveReceipt.status === "reverted") {
665
- throw new StableTransactionError({
666
- phase: "approve",
667
- shortMessage: "LI.FI: token approve reverted on-chain",
668
- txHash: approveReceipt.transactionHash,
669
- chainId: srcChainId
670
- });
597
+ }
598
+ _withIdempotency(key, fn) {
599
+ if (!key) return fn();
600
+ const existing = this.inFlight.get(key);
601
+ if (existing) return existing;
602
+ const promise = fn().finally(() => this.inFlight.delete(key));
603
+ this.inFlight.set(key, promise);
604
+ return promise;
605
+ }
606
+ _validateAmount(amount, op) {
607
+ if (!Number.isFinite(amount) || amount <= 0) {
608
+ throw new StableValidationError(
609
+ "amount",
610
+ amount,
611
+ `${op}: amount must be a finite positive number`
612
+ );
613
+ }
614
+ }
615
+ _validateShares(shares, op) {
616
+ if (!Number.isFinite(shares) || shares <= 0) {
617
+ throw new StableValidationError(
618
+ "shares",
619
+ shares,
620
+ `${op}: shares must be a finite positive number`
621
+ );
671
622
  }
672
623
  }
673
624
  };
674
- function isObject(v) {
675
- return typeof v === "object" && v !== null;
676
- }
677
- function isLiFiQuoteShape(v) {
678
- if (!isObject(v)) return false;
679
- const tx = v.transactionRequest;
680
- const est = v.estimate;
681
- const act = v.action;
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);
683
- }
684
625
 
685
- // src/earn/morpho.ts
626
+ // src/earn/reader.ts
627
+ import { MorphoClient as MorphoClient2 } from "@morpho-org/morpho-sdk";
686
628
  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
629
+ http as http2,
630
+ createPublicClient as createPublicClient2,
631
+ erc20Abi as erc20Abi2,
632
+ formatUnits as formatUnits2,
633
+ isAddress,
634
+ parseUnits as parseUnits2
694
635
  } 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");
636
+ import { stable as stableMainnet, stableTestnet } from "viem/chains";
637
+
638
+ // src/types.ts
639
+ var Chain = /* @__PURE__ */ ((Chain2) => {
640
+ Chain2["Sepolia"] = "sepolia";
641
+ Chain2["StableTestnet"] = "stable-testnet";
642
+ Chain2["Stable"] = "stable";
643
+ Chain2["Ethereum"] = "ethereum";
644
+ Chain2["Arbitrum"] = "arbitrum";
645
+ Chain2["Ink"] = "ink";
646
+ Chain2["Bera"] = "bera";
647
+ Chain2["MegaETH"] = "megaeth";
648
+ Chain2["Base"] = "base";
649
+ Chain2["BSC"] = "bsc";
650
+ Chain2["HyperEVM"] = "hyperevm";
651
+ return Chain2;
652
+ })(Chain || {});
653
+ var Network = /* @__PURE__ */ ((Network2) => {
654
+ Network2["Mainnet"] = "mainnet";
655
+ Network2["Testnet"] = "testnet";
656
+ return Network2;
657
+ })(Network || {});
658
+
659
+ // src/earn/reader.ts
660
+ var MORPHO_API_URL = "https://blue-api.morpho.org/graphql";
661
+ var EarnReader = class {
662
+ constructor(config) {
663
+ if (!config.earn?.vault) {
664
+ throw new StableValidationError(
665
+ "earn.vault",
666
+ void 0,
667
+ "createStableReader: earn.vault is required"
668
+ );
722
669
  }
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);
670
+ if (!isAddress(config.address)) {
671
+ throw new StableValidationError(
672
+ "address",
673
+ config.address,
674
+ `createStableReader: invalid user address "${config.address}"`
675
+ );
676
+ }
677
+ if (!isAddress(config.earn.vault)) {
678
+ throw new StableValidationError(
679
+ "earn.vault",
680
+ config.earn.vault,
681
+ `createStableReader: invalid vault address "${config.earn.vault}"`
682
+ );
683
+ }
684
+ const network = config.network ?? "mainnet" /* Mainnet */;
685
+ const viemChain = network === "mainnet" /* Mainnet */ ? stableMainnet : stableTestnet;
686
+ const rpc = (config.rpc ?? viemChain.rpcUrls.default.http[0]).replace(
687
+ /\/$/,
688
+ ""
689
+ );
690
+ this.chainId = viemChain.id;
691
+ this.vaultAddr = config.earn.vault;
692
+ this.userAddr = config.address;
693
+ const chain = makeChain(viemChain.id, rpc);
694
+ this.pc = createPublicClient2({ chain, transport: http2(rpc) });
695
+ this.morpho = new MorphoClient2(this.pc);
751
696
  }
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 };
697
+ async position() {
698
+ const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
699
+ const [vaultData, shares] = await Promise.all([
700
+ vault.getData(),
701
+ this.pc.readContract({
702
+ address: this.vaultAddr,
703
+ abi: erc20Abi2,
704
+ functionName: "balanceOf",
705
+ args: [this.userAddr]
706
+ })
707
+ ]);
708
+ const [tokenDecimals, shareDecimals] = await Promise.all([
709
+ this.pc.readContract({
710
+ address: vaultData.asset,
711
+ abi: erc20Abi2,
712
+ functionName: "decimals"
713
+ }),
714
+ this.pc.readContract({
715
+ address: this.vaultAddr,
716
+ abi: erc20Abi2,
717
+ functionName: "decimals"
718
+ })
719
+ ]);
720
+ const assetsRaw = vaultData.toAssets(shares);
721
+ return {
722
+ shares,
723
+ sharesFormatted: Number(formatUnits2(shares, shareDecimals)),
724
+ shareDecimals,
725
+ assets: Number(formatUnits2(assetsRaw, tokenDecimals)),
726
+ tokenDecimals,
727
+ assetAddress: vaultData.asset
776
728
  };
777
- return this._withIdempotency(idempotencyKey, run);
778
729
  }
779
- withdraw(params) {
780
- this._validateAmount(params.amount, "earn withdraw");
781
- return this._withIdempotency(params.idempotencyKey, () => this._withdraw(params));
730
+ async getYield() {
731
+ return this._fetchYield();
782
732
  }
783
- redeem(params) {
784
- this._validateShares(params.shares, "earn redeem");
785
- return this._withIdempotency(params.idempotencyKey, () => this._redeem(params));
733
+ async preview(params) {
734
+ const { amount, horizonDays } = params;
735
+ const apy = await this._fetchNetApy();
736
+ return {
737
+ projectedYield: amount * ((1 + apy) ** (horizonDays / 365) - 1),
738
+ apy,
739
+ horizonDays
740
+ };
786
741
  }
787
- forceWithdraw(params) {
788
- this._validateAmount(params.amount, "earn forceWithdraw");
789
- return this._withIdempotency(params.idempotencyKey, () => this._forceWithdraw(params));
742
+ async withdrawability(params) {
743
+ const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
744
+ const vaultData = await vault.getData();
745
+ const tokenDecimals = params.tokenDecimals ?? await this.pc.readContract({
746
+ address: vaultData.asset,
747
+ abi: erc20Abi2,
748
+ functionName: "decimals"
749
+ });
750
+ const requestedRaw = parseUnits2(String(params.amount), tokenDecimals);
751
+ let availableLiquidityRaw = vaultData.assetBalance;
752
+ if (vaultData.accrualLiquidityAdapter != null) {
753
+ availableLiquidityRaw += vaultData.accrualLiquidityAdapter.maxWithdraw(
754
+ vaultData.liquidityData
755
+ ).value;
756
+ }
757
+ return {
758
+ isInstant: requestedRaw <= availableLiquidityRaw,
759
+ availableLiquidity: Number(
760
+ formatUnits2(availableLiquidityRaw, tokenDecimals)
761
+ ),
762
+ tokenDecimals
763
+ };
790
764
  }
791
- forceRedeem(params) {
792
- this._validateShares(params.shares, "earn forceRedeem");
793
- return this._withIdempotency(params.idempotencyKey, () => this._forceRedeem(params));
765
+ async _fetchNetApy() {
766
+ const { apy } = await this._fetchYield();
767
+ return apy;
794
768
  }
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
- );
769
+ async _fetchYield() {
770
+ const query = `
771
+ query VaultYield($address: String!, $chainId: Int!) {
772
+ vaultV2ByAddress(address: $address, chainId: $chainId) {
773
+ avgNetApy
774
+ avgNetApyExcludingRewards
775
+ performanceFee
776
+ managementFee
777
+ rewards {
778
+ asset { symbol address }
779
+ supplyApr
780
+ }
781
+ }
816
782
  }
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
783
+ `;
784
+ let body;
785
+ try {
786
+ body = await retryWithBackoff(
787
+ async () => {
788
+ const res = await fetch(MORPHO_API_URL, {
789
+ method: "POST",
790
+ headers: { "Content-Type": "application/json" },
791
+ body: JSON.stringify({
792
+ query,
793
+ variables: {
794
+ address: this.vaultAddr,
795
+ chainId: this.chainId
796
+ }
797
+ })
836
798
  });
837
- }
838
- } else if (isRequirementSignature(req)) {
839
- sig = await req.sign(this.wc, from);
799
+ if (!res.ok) {
800
+ const retryable = res.status === 429 || res.status >= 500;
801
+ throw new HttpError(
802
+ `earn: Morpho API returned ${res.status} \u2014 ${res.statusText}`,
803
+ res.status,
804
+ retryable
805
+ );
806
+ }
807
+ return res.json().catch(() => void 0);
808
+ },
809
+ (err) => err instanceof HttpError ? err.retryable : err instanceof TypeError
810
+ );
811
+ } catch (err) {
812
+ if (err instanceof TypeError || err instanceof HttpError) {
813
+ throw new StableNetworkError({
814
+ shortMessage: err instanceof HttpError ? `earn: Morpho API returned ${err.status} after retries` : `earn: failed to reach Morpho API after retries \u2014 ${String(
815
+ err
816
+ )}`,
817
+ url: MORPHO_API_URL,
818
+ cause: err
819
+ });
840
820
  }
821
+ throw err;
841
822
  }
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
- });
823
+ const vault = body?.data?.vaultV2ByAddress;
824
+ if (!vault || typeof vault.avgNetApy !== "number") {
825
+ throw new StableValidationError(
826
+ "apy",
827
+ void 0,
828
+ "earn: vault not found in Morpho API or unexpected response shape"
829
+ );
852
830
  }
853
- return { txHash: receipt.transactionHash, idempotencyKey };
831
+ const rewards = vault.rewards.filter((r) => typeof r.supplyApr === "number" && r.supplyApr > 0).map((r) => ({
832
+ symbol: r.asset.symbol,
833
+ address: r.asset.address,
834
+ apr: r.supplyApr
835
+ }));
836
+ return {
837
+ apy: vault.avgNetApy,
838
+ native: vault.avgNetApyExcludingRewards ?? 0,
839
+ rewards,
840
+ performanceFee: vault.performanceFee,
841
+ managementFee: vault.managementFee
842
+ };
854
843
  }
855
- async _withdraw(params) {
856
- const { amount, tokenDecimals, idempotencyKey } = params;
844
+ };
845
+
846
+ // src/services/lifi.ts
847
+ import {
848
+ http as http3,
849
+ createPublicClient as createPublicClient3,
850
+ createWalletClient as createWalletClient2,
851
+ erc20Abi as erc20Abi3,
852
+ formatUnits as formatUnits3,
853
+ isAddress as isAddress2,
854
+ parseUnits as parseUnits3
855
+ } from "viem";
856
+
857
+ // src/chains.ts
858
+ var CHAIN_CONFIGS = {
859
+ // ─── Testnet ──────────────────────────────────────────────────────────────
860
+ ["sepolia" /* Sepolia */]: {
861
+ id: 11155111,
862
+ rpc: "https://ethereum-sepolia-rpc.publicnode.com",
863
+ usdt: "0xc4DCC311c028e341fd8602D8eB89c5de94625927",
864
+ decimals: 18
865
+ },
866
+ ["stable-testnet" /* StableTestnet */]: {
867
+ id: 2201,
868
+ rpc: "https://rpc.testnet.stable.xyz",
869
+ usdt: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9",
870
+ decimals: 6
871
+ },
872
+ // ─── Mainnet ──────────────────────────────────────────────────────────────
873
+ ["stable" /* Stable */]: {
874
+ id: 988,
875
+ rpc: "https://rpc.stable.xyz",
876
+ usdt: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
877
+ decimals: 6
878
+ },
879
+ ["ethereum" /* Ethereum */]: {
880
+ id: 1,
881
+ rpc: "https://ethereum-rpc.publicnode.com",
882
+ usdt: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee",
883
+ decimals: 6
884
+ },
885
+ ["arbitrum" /* Arbitrum */]: {
886
+ id: 42161,
887
+ rpc: "https://arbitrum-one-rpc.publicnode.com",
888
+ usdt: "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92",
889
+ decimals: 6
890
+ },
891
+ ["ink" /* Ink */]: {
892
+ id: 57073,
893
+ rpc: "https://rpc-gel.inkonchain.com",
894
+ usdt: "0x0200C29006150606B650577BBE7B6248F58470c1",
895
+ decimals: 6
896
+ },
897
+ ["bera" /* Bera */]: {
898
+ id: 80094,
899
+ rpc: "https://rpc.berachain.com",
900
+ usdt: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
901
+ decimals: 6
902
+ },
903
+ ["megaeth" /* MegaETH */]: {
904
+ id: 4326,
905
+ rpc: "https://carrot.megaeth.com",
906
+ usdt: "0xb8ce59fc3717ada4c02eadf9682a9e934f625ebb",
907
+ decimals: 6
908
+ },
909
+ ["base" /* Base */]: {
910
+ id: 8453,
911
+ rpc: "https://mainnet.base.org",
912
+ usdt: "0x102d758f688a4C1C5a80b116bD945d4455460282",
913
+ decimals: 6
914
+ },
915
+ ["bsc" /* BSC */]: {
916
+ id: 56,
917
+ rpc: "https://bsc-rpc.publicnode.com",
918
+ usdt: "0x55d398326f99059fF775485246999027B3197955",
919
+ decimals: 18
920
+ },
921
+ ["hyperevm" /* HyperEVM */]: {
922
+ id: 999,
923
+ rpc: "https://rpc.hyperliquid.xyz/evm",
924
+ usdt: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb",
925
+ decimals: 6
926
+ }
927
+ };
928
+
929
+ // src/services/lifi.ts
930
+ var LiFiService = class {
931
+ constructor(config) {
932
+ this.config = config;
933
+ }
934
+ async quoteSwap(params) {
935
+ this._validateSwapParams(params);
936
+ const {
937
+ fromToken,
938
+ toToken,
939
+ amount,
940
+ fromDecimals = 6,
941
+ toAddress
942
+ } = params;
857
943
  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 };
944
+ const chain = this.config.chainId.toString();
945
+ const fromAmountRaw = parseUnits3(
946
+ String(amount),
947
+ fromDecimals
948
+ ).toString();
949
+ const qs = new URLSearchParams({
950
+ fromChain: chain,
951
+ toChain: chain,
952
+ fromToken,
953
+ toToken,
954
+ fromAmount: fromAmountRaw,
955
+ fromAddress: from,
956
+ ...toAddress && { toAddress }
957
+ });
958
+ const quote = await this._fetchQuote(qs);
959
+ return this._toSwapQuote(quote);
876
960
  }
877
- async _redeem(params) {
878
- const { shares, shareDecimals, idempotencyKey } = params;
961
+ async swap(params) {
962
+ this._validateSwapParams(params);
963
+ const quote = params.quote ?? await this.quoteSwap(params);
964
+ return this._executeSwapQuote(
965
+ quote,
966
+ this.config.rpc,
967
+ this.config.chainId
968
+ );
969
+ }
970
+ async quoteBridge(params) {
971
+ this._validateBridgeParams(params);
972
+ const {
973
+ fromChain,
974
+ toChain,
975
+ fromToken,
976
+ toToken,
977
+ amount,
978
+ fromDecimals = 6,
979
+ recipient
980
+ } = params;
981
+ const from = this.config.walletClient?.account?.address ?? this.config.account?.address ?? "0x0000000000000000000000000000000000000000";
982
+ const src = CHAIN_CONFIGS[fromChain];
983
+ const dst = CHAIN_CONFIGS[toChain];
984
+ if (!src)
985
+ throw new StableValidationError(
986
+ "fromChain",
987
+ fromChain,
988
+ `LI.FI bridge: unsupported source chain "${fromChain}"`
989
+ );
990
+ if (!dst)
991
+ throw new StableValidationError(
992
+ "toChain",
993
+ toChain,
994
+ `LI.FI bridge: unsupported destination chain "${toChain}"`
995
+ );
996
+ const qs = new URLSearchParams({
997
+ fromChain: src.id.toString(),
998
+ toChain: dst.id.toString(),
999
+ fromToken,
1000
+ toToken,
1001
+ fromAmount: parseUnits3(String(amount), fromDecimals).toString(),
1002
+ fromAddress: from,
1003
+ ...recipient && { toAddress: recipient }
1004
+ });
1005
+ const quote = this._toSwapQuote(await this._fetchQuote(qs));
1006
+ return { toAmount: quote.toAmount };
1007
+ }
1008
+ async bridge(params) {
1009
+ this._validateBridgeParams(params);
1010
+ const {
1011
+ fromChain,
1012
+ toChain,
1013
+ fromToken,
1014
+ toToken,
1015
+ amount,
1016
+ fromDecimals = 6,
1017
+ recipient
1018
+ } = params;
879
1019
  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
- }
1020
+ const src = CHAIN_CONFIGS[fromChain];
1021
+ const dst = CHAIN_CONFIGS[toChain];
1022
+ if (!src) {
1023
+ throw new StableValidationError(
1024
+ "fromChain",
1025
+ fromChain,
1026
+ `LI.FI bridge: unsupported source chain "${fromChain}"`
1027
+ );
893
1028
  }
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
- });
1029
+ if (!dst) {
1030
+ throw new StableValidationError(
1031
+ "toChain",
1032
+ toChain,
1033
+ `LI.FI bridge: unsupported destination chain "${toChain}"`
1034
+ );
906
1035
  }
907
- return { txHash: receipt.transactionHash, idempotencyKey };
1036
+ const fromAmountRaw = parseUnits3(
1037
+ String(amount),
1038
+ fromDecimals
1039
+ ).toString();
1040
+ const qs = new URLSearchParams({
1041
+ fromChain: src.id.toString(),
1042
+ toChain: dst.id.toString(),
1043
+ fromToken,
1044
+ toToken,
1045
+ fromAmount: fromAmountRaw,
1046
+ fromAddress: from,
1047
+ ...recipient && { toAddress: recipient }
1048
+ });
1049
+ const quote = this._toSwapQuote(await this._fetchQuote(qs));
1050
+ return this._executeSwapQuote(quote, src.rpc, src.id);
908
1051
  }
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");
1052
+ _validateSwapParams({
1053
+ fromToken,
1054
+ toToken,
1055
+ amount,
1056
+ toAddress
1057
+ }) {
1058
+ if (!isAddress2(fromToken)) {
1059
+ throw new StableValidationError(
1060
+ "fromToken",
1061
+ fromToken,
1062
+ `swap: invalid fromToken address "${fromToken}"`
1063
+ );
919
1064
  }
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
- });
1065
+ if (!isAddress2(toToken)) {
1066
+ throw new StableValidationError(
1067
+ "toToken",
1068
+ toToken,
1069
+ `swap: invalid toToken address "${toToken}"`
1070
+ );
1071
+ }
1072
+ if (fromToken.toLowerCase() === toToken.toLowerCase()) {
1073
+ throw new StableValidationError(
1074
+ "toToken",
1075
+ toToken,
1076
+ "swap: fromToken and toToken must differ"
1077
+ );
1078
+ }
1079
+ if (!Number.isFinite(amount) || amount <= 0) {
1080
+ throw new StableValidationError(
1081
+ "amount",
1082
+ amount,
1083
+ "swap: amount must be a finite positive number"
1084
+ );
1085
+ }
1086
+ if (toAddress !== void 0 && !isAddress2(toAddress)) {
1087
+ throw new StableValidationError(
1088
+ "toAddress",
1089
+ toAddress,
1090
+ `swap: invalid toAddress "${toAddress}"`
1091
+ );
934
1092
  }
935
- return { txHash: receipt.transactionHash, idempotencyKey };
936
1093
  }
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
- }
1094
+ _validateBridgeParams({
1095
+ fromChain,
1096
+ toChain,
1097
+ fromToken,
1098
+ toToken,
1099
+ amount,
1100
+ recipient
1101
+ }) {
1102
+ if (!isAddress2(fromToken)) {
1103
+ throw new StableValidationError(
1104
+ "fromToken",
1105
+ fromToken,
1106
+ `LI.FI bridge: invalid fromToken address "${fromToken}"`
1107
+ );
954
1108
  }
955
- let assetDec = tokenDecimals;
956
- if (assetDec === void 0) {
957
- const vaultData = await vault.getData();
958
- assetDec = await this._resolveDecimals(vaultData.asset, "earn forceRedeem");
1109
+ if (!isAddress2(toToken)) {
1110
+ throw new StableValidationError(
1111
+ "toToken",
1112
+ toToken,
1113
+ `LI.FI bridge: invalid toToken address "${toToken}"`
1114
+ );
959
1115
  }
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
- });
1116
+ if (!Number.isFinite(amount) || amount <= 0) {
1117
+ throw new StableValidationError(
1118
+ "amount",
1119
+ amount,
1120
+ "LI.FI bridge: amount must be a finite positive number"
1121
+ );
1122
+ }
1123
+ if (recipient !== void 0 && !isAddress2(recipient)) {
1124
+ throw new StableValidationError(
1125
+ "recipient",
1126
+ recipient,
1127
+ `LI.FI bridge: invalid recipient address "${recipient}"`
1128
+ );
1129
+ }
1130
+ if (fromChain === toChain) {
1131
+ throw new StableValidationError(
1132
+ "toChain",
1133
+ toChain,
1134
+ "LI.FI bridge: fromChain and toChain must differ"
1135
+ );
974
1136
  }
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
1137
  }
986
- // ─── Helpers ─────────────────────────────────────────────────────────────
987
1138
  _requireFrom() {
988
- const from = this.wc.account?.address;
1139
+ const from = this.config.walletClient?.account?.address ?? this.config.account?.address;
989
1140
  if (!from) {
990
1141
  throw new StableValidationError(
991
1142
  "signer",
992
1143
  void 0,
993
- "earn: requires a signer \u2014 set walletClient or account in StableConfig"
1144
+ "LI.FI: requires a signer \u2014 set walletClient or account in StableConfig"
994
1145
  );
995
1146
  }
996
1147
  return from;
997
1148
  }
998
- async _switchChain() {
1149
+ async _fetchQuote(qs) {
1150
+ const url = `https://li.quest/v1/quote?${qs}`;
999
1151
  try {
1000
- await this.wc.switchChain({ id: this.config.chainId });
1152
+ return await retryWithBackoff(() => this._fetchQuoteOnce(url));
1001
1153
  } 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,
1154
+ if (err instanceof StableQuoteError) throw err;
1155
+ if (err instanceof HttpError) {
1156
+ throw new StableQuoteError({
1157
+ provider: "lifi",
1158
+ shortMessage: err.message,
1159
+ httpStatus: err.status,
1007
1160
  cause: err
1008
1161
  });
1009
1162
  }
1163
+ throw new StableNetworkError({
1164
+ shortMessage: "LI.FI: quote request failed before reaching the server",
1165
+ url,
1166
+ cause: err
1167
+ });
1010
1168
  }
1011
1169
  }
1012
- async _sendTx(to, data, value, from) {
1170
+ async _fetchQuoteOnce(url) {
1171
+ const res = await fetch(url);
1172
+ const body = await res.json().catch(() => void 0);
1173
+ if (!res.ok) {
1174
+ const providerMessage = isObject(body) && typeof body.message === "string" ? body.message : void 0;
1175
+ const providerCode = isObject(body) && typeof body.code === "string" ? body.code : void 0;
1176
+ if (res.status === 429 || res.status >= 500) {
1177
+ throw new HttpError(
1178
+ `LI.FI: ${providerMessage ?? res.statusText}`,
1179
+ res.status,
1180
+ true
1181
+ );
1182
+ }
1183
+ throw new StableQuoteError({
1184
+ provider: "lifi",
1185
+ shortMessage: `LI.FI: ${providerMessage ?? res.statusText}`,
1186
+ httpStatus: res.status,
1187
+ providerCode,
1188
+ body
1189
+ });
1190
+ }
1191
+ if (!isLiFiQuoteShape(body)) {
1192
+ throw new StableQuoteError({
1193
+ provider: "lifi",
1194
+ shortMessage: "LI.FI: unexpected quote response shape",
1195
+ httpStatus: res.status,
1196
+ body
1197
+ });
1198
+ }
1199
+ return body;
1200
+ }
1201
+ _toSwapQuote(quote) {
1202
+ return {
1203
+ toAmount: Number(
1204
+ formatUnits3(
1205
+ BigInt(quote.estimate.toAmount),
1206
+ quote.action.toToken.decimals
1207
+ )
1208
+ ),
1209
+ fromAmount: BigInt(quote.estimate.fromAmount),
1210
+ fromToken: quote.action.fromToken.address,
1211
+ approvalAddress: quote.estimate.approvalAddress,
1212
+ transactionRequest: quote.transactionRequest
1213
+ };
1214
+ }
1215
+ async _executeSwapQuote(quote, srcRpc, expectedChainId) {
1216
+ const from = this._requireFrom();
1217
+ const { transactionRequest } = quote;
1218
+ const srcChainId = transactionRequest.chainId;
1219
+ if (srcChainId !== expectedChainId) {
1220
+ throw new StableQuoteError({
1221
+ provider: "lifi",
1222
+ shortMessage: `LI.FI: returned tx for chain ${srcChainId} but expected ${expectedChainId}`,
1223
+ providerCode: "chain_mismatch",
1224
+ body: quote
1225
+ });
1226
+ }
1227
+ const srcChain = makeChain(srcChainId, srcRpc);
1228
+ const wc = this.config.walletClient ?? createWalletClient2({
1229
+ account: this.config.account,
1230
+ chain: srcChain,
1231
+ transport: this.config.transport ?? http3(srcRpc)
1232
+ });
1013
1233
  try {
1014
- return await this.wc.sendTransaction({ to, data, value, account: from, chain: this.chain });
1234
+ await wc.switchChain({ id: srcChainId });
1015
1235
  } catch (err) {
1016
1236
  throw new StableTransactionError({
1017
- phase: "send",
1018
- shortMessage: "earn: transaction send failed",
1019
- chainId: this.config.chainId,
1237
+ phase: "switch_chain",
1238
+ shortMessage: `LI.FI: wallet rejected or failed to switch to chain ${srcChainId}`,
1239
+ chainId: srcChainId,
1020
1240
  cause: err
1021
1241
  });
1022
1242
  }
1023
- }
1024
- async _resolveDecimals(asset, op) {
1243
+ await this._ensureApproval(
1244
+ wc,
1245
+ srcChain,
1246
+ srcRpc,
1247
+ srcChainId,
1248
+ from,
1249
+ quote
1250
+ );
1251
+ let hash;
1025
1252
  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
- );
1253
+ hash = await wc.sendTransaction({
1254
+ to: transactionRequest.to,
1255
+ data: transactionRequest.data,
1256
+ value: transactionRequest.value && transactionRequest.value !== "0x" ? BigInt(transactionRequest.value) : 0n,
1257
+ gas: transactionRequest.gasLimit ? BigInt(transactionRequest.gasLimit) : void 0,
1258
+ account: wc.account,
1259
+ chain: srcChain
1260
+ });
1261
+ } catch (err) {
1262
+ throw new StableTransactionError({
1263
+ phase: "send",
1264
+ shortMessage: "LI.FI: swap/bridge transaction failed",
1265
+ chainId: srcChainId,
1266
+ cause: err
1267
+ });
1035
1268
  }
1269
+ return { txHash: hash, toAmount: quote.toAmount };
1036
1270
  }
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`);
1271
+ async _ensureApproval(wc, srcChain, srcRpc, srcChainId, from, quote) {
1272
+ const tokenAddr = quote.fromToken;
1273
+ const isNative = tokenAddr.toLowerCase() === "0x0000000000000000000000000000000000000000" || tokenAddr.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
1274
+ if (isNative || !quote.approvalAddress) return;
1275
+ const pc = createPublicClient3({
1276
+ chain: srcChain,
1277
+ transport: http3(srcRpc)
1278
+ });
1279
+ const allowance = await pc.readContract({
1280
+ address: tokenAddr,
1281
+ abi: erc20Abi3,
1282
+ functionName: "allowance",
1283
+ args: [from, quote.approvalAddress]
1284
+ });
1285
+ if (allowance >= quote.fromAmount) return;
1286
+ let approveHash;
1287
+ try {
1288
+ approveHash = await wc.writeContract({
1289
+ address: tokenAddr,
1290
+ abi: erc20Abi3,
1291
+ functionName: "approve",
1292
+ args: [quote.approvalAddress, quote.fromAmount],
1293
+ account: wc.account,
1294
+ chain: srcChain
1295
+ });
1296
+ } catch (err) {
1297
+ throw new StableTransactionError({
1298
+ phase: "approve",
1299
+ shortMessage: "LI.FI: token approve failed",
1300
+ chainId: srcChainId,
1301
+ cause: err
1302
+ });
1048
1303
  }
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`);
1304
+ const approveReceipt = await pc.waitForTransactionReceipt({
1305
+ hash: approveHash
1306
+ });
1307
+ if (approveReceipt.status === "reverted") {
1308
+ throw new StableTransactionError({
1309
+ phase: "approve",
1310
+ shortMessage: "LI.FI: token approve reverted on-chain",
1311
+ txHash: approveReceipt.transactionHash,
1312
+ chainId: srcChainId
1313
+ });
1053
1314
  }
1054
1315
  }
1055
1316
  };
1317
+ function isObject(v) {
1318
+ return typeof v === "object" && v !== null;
1319
+ }
1320
+ function isLiFiQuoteShape(v) {
1321
+ if (!isObject(v)) return false;
1322
+ const tx = v.transactionRequest;
1323
+ const est = v.estimate;
1324
+ const act = v.action;
1325
+ 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);
1326
+ }
1056
1327
 
1057
1328
  // src/stable.ts
1058
- function validateTransferParams({ to, amount, token, tokenDecimals }) {
1329
+ function validateTransferParams({
1330
+ to,
1331
+ amount,
1332
+ token,
1333
+ tokenDecimals
1334
+ }) {
1059
1335
  if (!isAddress3(to)) {
1060
- throw new StableValidationError("to", to, `transfer: invalid recipient address "${to}"`);
1336
+ throw new StableValidationError(
1337
+ "to",
1338
+ to,
1339
+ `transfer: invalid recipient address "${to}"`
1340
+ );
1061
1341
  }
1062
1342
  if (!Number.isFinite(amount) || amount <= 0) {
1063
- throw new StableValidationError("amount", amount, "transfer: amount must be a finite positive number");
1343
+ throw new StableValidationError(
1344
+ "amount",
1345
+ amount,
1346
+ "transfer: amount must be a finite positive number"
1347
+ );
1064
1348
  }
1065
1349
  if (token !== void 0 && !isAddress3(token)) {
1066
- throw new StableValidationError("token", token, `transfer: invalid token address "${token}"`);
1350
+ throw new StableValidationError(
1351
+ "token",
1352
+ token,
1353
+ `transfer: invalid token address "${token}"`
1354
+ );
1067
1355
  }
1068
1356
  if (tokenDecimals !== void 0 && (!Number.isInteger(tokenDecimals) || tokenDecimals < 0 || tokenDecimals > 77)) {
1069
- throw new StableValidationError("tokenDecimals", tokenDecimals, "transfer: tokenDecimals must be an integer between 0 and 77");
1357
+ throw new StableValidationError(
1358
+ "tokenDecimals",
1359
+ tokenDecimals,
1360
+ "transfer: tokenDecimals must be an integer between 0 and 77"
1361
+ );
1070
1362
  }
1071
1363
  }
1072
1364
  async function resolveTokenDecimals(publicClient, token, tokenDecimals) {
1073
1365
  if (tokenDecimals !== void 0) return tokenDecimals;
1074
1366
  try {
1075
- return await publicClient.readContract({ address: token, abi: erc20Abi4, functionName: "decimals" });
1367
+ return await publicClient.readContract({
1368
+ address: token,
1369
+ abi: erc20Abi4,
1370
+ functionName: "decimals"
1371
+ });
1076
1372
  } catch {
1077
1373
  throw new StableValidationError(
1078
1374
  "tokenDecimals",
@@ -1090,7 +1386,11 @@ async function executeTokenTransfer(wc, publicClient, token, to, amount, tokenDe
1090
1386
  `transfer: no contract found at "${token}" on chain ${chain.id} \u2014 check the address and network`
1091
1387
  );
1092
1388
  }
1093
- const decimals = await resolveTokenDecimals(publicClient, token, tokenDecimals);
1389
+ const decimals = await resolveTokenDecimals(
1390
+ publicClient,
1391
+ token,
1392
+ tokenDecimals
1393
+ );
1094
1394
  return wc.writeContract({
1095
1395
  address: token,
1096
1396
  abi: erc20Abi4,
@@ -1109,10 +1409,18 @@ function createStable({
1109
1409
  earn: earnConfig
1110
1410
  }) {
1111
1411
  const chain = network === "mainnet" /* Mainnet */ ? stableMainnet2 : stableTestnet2;
1112
- const rpc = (rpcOverride ?? chain.rpcUrls.default.http[0]).replace(/\/$/, "");
1412
+ const rpc = (rpcOverride ?? chain.rpcUrls.default.http[0]).replace(
1413
+ /\/$/,
1414
+ ""
1415
+ );
1113
1416
  const signerConfig = { account, transport, walletClient };
1114
1417
  const lifi = new LiFiService({ ...signerConfig, rpc, chainId: chain.id });
1115
- const morpho = earnConfig ? new MorphoService({ ...signerConfig, rpc, chainId: chain.id, vault: earnConfig.vault }) : null;
1418
+ const morpho = earnConfig ? new MorphoService({
1419
+ ...signerConfig,
1420
+ rpc,
1421
+ chainId: chain.id,
1422
+ vault: earnConfig.vault
1423
+ }) : null;
1116
1424
  function requireEarn() {
1117
1425
  if (!morpho) {
1118
1426
  throw new StableValidationError(
@@ -1142,8 +1450,29 @@ function createStable({
1142
1450
  cause: err
1143
1451
  });
1144
1452
  }
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) });
1453
+ const publicClient = createPublicClient4({
1454
+ chain,
1455
+ transport: http4(rpc)
1456
+ });
1457
+ const hash = token !== void 0 ? await executeTokenTransfer(
1458
+ wc,
1459
+ publicClient,
1460
+ token,
1461
+ to,
1462
+ amount,
1463
+ tokenDecimals,
1464
+ acc,
1465
+ chain
1466
+ ) : (
1467
+ // Native transfer on Stable mainnet/testnet. Native USDT0 uses 18 decimals
1468
+ // on both networks, matching standard EVM native-token decimals.
1469
+ await wc.sendTransaction({
1470
+ account: acc,
1471
+ chain,
1472
+ to,
1473
+ value: parseUnits4(String(amount), 18)
1474
+ })
1475
+ );
1147
1476
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
1148
1477
  if (receipt.status === "reverted") {
1149
1478
  throw new StableTransactionError({