@stablechain/sdk 1.0.0 → 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.
package/dist/index.cjs CHANGED
@@ -9,7 +9,12 @@
9
9
  var _viem = require('viem');
10
10
  var _chains = require('viem/chains');
11
11
 
12
- // src/earn/reader.ts
12
+ // src/earn/morpho.ts
13
+
14
+
15
+
16
+
17
+ var _morphosdk = require('@morpho-org/morpho-sdk');
13
18
 
14
19
 
15
20
 
@@ -19,7 +24,6 @@ var _chains = require('viem/chains');
19
24
 
20
25
 
21
26
 
22
- var _morphosdk = require('@morpho-org/morpho-sdk');
23
27
 
24
28
  // src/errors.ts
25
29
 
@@ -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,528 +147,703 @@ 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 (!_optionalChain([config, 'access', _2 => _2.earn, 'optionalAccess', _3 => _3.vault])) {
171
- throw new StableValidationError(
172
- "earn.vault",
173
- void 0,
174
- "createStableReader: earn.vault is required"
175
- );
176
- }
177
- if (!_viem.isAddress.call(void 0, config.address)) {
178
- throw new StableValidationError(
179
- "address",
180
- config.address,
181
- `createStableReader: invalid user address "${config.address}"`
182
- );
183
- }
184
- if (!_viem.isAddress.call(void 0, config.earn.vault)) {
153
+ this.config = config;
154
+ this.inFlight = /* @__PURE__ */ new Map();
155
+ this.chain = makeChain(config.chainId, config.rpc);
156
+ this.wc = _nullishCoalesce(config.walletClient, () => ( _viem.createWalletClient.call(void 0, {
157
+ account: config.account,
158
+ chain: this.chain,
159
+ transport: _nullishCoalesce(config.transport, () => ( _viem.http.call(void 0, config.rpc)))
160
+ })));
161
+ const morphoWc = _optionalChain([this, 'access', _2 => _2.wc, 'access', _3 => _3.chain, 'optionalAccess', _4 => _4.id]) === config.chainId ? this.wc : _viem.createWalletClient.call(void 0, {
162
+ account: this.wc.account,
163
+ chain: this.chain,
164
+ transport: _viem.custom.call(void 0, {
165
+ request: this.wc.request.bind(this.wc)
166
+ })
167
+ });
168
+ this.morpho = new (0, _morphosdk.MorphoClient)(morphoWc, { supportSignature: true });
169
+ this.pc = _viem.createPublicClient.call(void 0, {
170
+ chain: this.chain,
171
+ transport: _viem.http.call(void 0, config.rpc)
172
+ });
173
+ }
174
+ deposit(params) {
175
+ this._validateAmount(params.amount, "earn deposit");
176
+ if (params.depositBuffer !== void 0 && (params.depositBuffer <= 0 || params.depositBuffer > 1)) {
185
177
  throw new StableValidationError(
186
- "earn.vault",
187
- config.earn.vault,
188
- `createStableReader: invalid vault address "${config.earn.vault}"`
178
+ "depositBuffer",
179
+ params.depositBuffer,
180
+ "earn deposit: depositBuffer must be > 0 and \u2264 1"
189
181
  );
190
182
  }
191
- const network = _nullishCoalesce(config.network, () => ( "mainnet")) /* Mainnet */;
192
- const viemChain = network === "mainnet" /* Mainnet */ ? _chains.stable : _chains.stableTestnet;
193
- const rpc = (_nullishCoalesce(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 = _viem.createPublicClient.call(void 0, { chain, transport: _viem.http.call(void 0, rpc) });
199
- this.morpho = new (0, _morphosdk.MorphoClient)(this.pc);
183
+ return this._withIdempotency(
184
+ params.idempotencyKey,
185
+ () => this._deposit(params)
186
+ );
200
187
  }
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: _viem.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: _viem.erc20Abi, functionName: "decimals" }),
214
- this.pc.readContract({ address: this.vaultAddr, abi: _viem.erc20Abi, functionName: "decimals" })
215
- ]);
216
- const assetsRaw = vaultData.toAssets(shares);
217
- return {
218
- shares,
219
- sharesFormatted: Number(_viem.formatUnits.call(void 0, shares, shareDecimals)),
220
- shareDecimals,
221
- assets: Number(_viem.formatUnits.call(void 0, assetsRaw, tokenDecimals)),
222
- tokenDecimals,
223
- assetAddress: vaultData.asset
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
+ });
219
+ }
220
+ }
221
+ }
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
+ };
224
231
  };
232
+ return this._withIdempotency(idempotencyKey, run);
225
233
  }
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
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
245
+ });
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
+ });
263
+ }
264
+ }
265
+ }
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
+ };
233
275
  };
276
+ return this._withIdempotency(idempotencyKey, run);
234
277
  }
235
- async withdrawability(params) {
236
- const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
237
- const vaultData = await vault.getData();
238
- const tokenDecimals = await _asyncNullishCoalesce(params.tokenDecimals, async () => ( await this.pc.readContract({
239
- address: vaultData.asset,
240
- abi: _viem.erc20Abi,
241
- functionName: "decimals"
242
- })));
243
- const requestedRaw = _viem.parseUnits.call(void 0, String(params.amount), tokenDecimals);
244
- const availableLiquidityRaw = vaultData.assetBalance;
245
- return {
246
- isInstant: requestedRaw <= availableLiquidityRaw,
247
- availableLiquidity: Number(_viem.formatUnits.call(void 0, availableLiquidityRaw, tokenDecimals)),
248
- tokenDecimals
249
- };
278
+ withdraw(params) {
279
+ this._validateAmount(params.amount, "earn withdraw");
280
+ return this._withIdempotency(
281
+ params.idempotencyKey,
282
+ () => this._withdraw(params)
283
+ );
250
284
  }
251
- async _fetchNetApy() {
252
- const query = `
253
- query VaultNetApy($address: String!, $chainId: Int!) {
254
- vaultV2ByAddress(address: $address, chainId: $chainId) {
255
- netApy
256
- }
285
+ redeem(params) {
286
+ this._validateShares(params.shares, "earn redeem");
287
+ return this._withIdempotency(
288
+ params.idempotencyKey,
289
+ () => this._redeem(params)
290
+ );
291
+ }
292
+ forceWithdraw(params) {
293
+ this._validateAmount(params.amount, "earn forceWithdraw");
294
+ return this._withIdempotency(
295
+ params.idempotencyKey,
296
+ () => this._forceWithdraw(params)
297
+ );
298
+ }
299
+ forceRedeem(params) {
300
+ this._validateShares(params.shares, "earn forceRedeem");
301
+ return this._withIdempotency(
302
+ params.idempotencyKey,
303
+ () => this._forceRedeem(params)
304
+ );
305
+ }
306
+ // ─── Private operations ──────────────────────────────────────────────────
307
+ async _deposit(params) {
308
+ const {
309
+ amount,
310
+ tokenDecimals,
311
+ slippageTolerance,
312
+ idempotencyKey,
313
+ depositBuffer
314
+ } = params;
315
+ const from = this._requireFrom();
316
+ const vaultAddr = _nullishCoalesce(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 = await _asyncNullishCoalesce(tokenDecimals, async () => ( await this._resolveDecimals(vaultData.asset, "earn deposit")));
321
+ const assetsRaw = _viem.parseUnits.call(void 0, 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(_viem.formatUnits.call(void 0, maxAllowed, decimals));
327
+ const idleFormatted = Number(
328
+ _viem.formatUnits.call(void 0, 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
+ );
257
335
  }
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
336
+ }
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
+ _viem.parseUnits.call(void 0, String(slippageTolerance), 18)
344
+ ) : void 0
345
+ });
346
+ const requirements = await depositAction.getRequirements();
347
+ let sig;
348
+ for (const req of requirements) {
349
+ if (_morphosdk.isRequirementApproval.call(void 0, 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
289
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 (_morphosdk.isRequirementSignature.call(void 0, req)) {
368
+ sig = await req.sign(this.wc, from);
290
369
  }
291
- throw err;
292
370
  }
293
- const netApy = _optionalChain([body, 'optionalAccess', _4 => _4.data, 'optionalAccess', _5 => _5.vaultV2ByAddress, 'optionalAccess', _6 => _6.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
- );
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
+ });
300
381
  }
301
- return netApy;
382
+ return { txHash: receipt.transactionHash, idempotencyKey };
302
383
  }
303
- };
304
-
305
- // src/services/lifi.ts
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
- // src/chains.ts
317
- var CHAIN_CONFIGS = {
318
- // ─── Testnet ──────────────────────────────────────────────────────────────
319
- ["sepolia" /* Sepolia */]: {
320
- id: 11155111,
321
- rpc: "https://ethereum-sepolia-rpc.publicnode.com",
322
- usdt: "0xc4DCC311c028e341fd8602D8eB89c5de94625927",
323
- decimals: 18
324
- },
325
- ["stable-testnet" /* StableTestnet */]: {
326
- id: 2201,
327
- rpc: "https://rpc.testnet.stable.xyz",
328
- usdt: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9",
329
- decimals: 6
330
- },
331
- // ─── Mainnet ──────────────────────────────────────────────────────────────
332
- ["stable" /* Stable */]: {
333
- id: 988,
334
- rpc: "https://rpc.stable.xyz",
335
- usdt: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
336
- decimals: 6
337
- },
338
- ["ethereum" /* Ethereum */]: {
339
- id: 1,
340
- rpc: "https://ethereum-rpc.publicnode.com",
341
- usdt: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee",
342
- decimals: 6
343
- },
344
- ["arbitrum" /* Arbitrum */]: {
345
- id: 42161,
346
- rpc: "https://arbitrum-one-rpc.publicnode.com",
347
- usdt: "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92",
348
- decimals: 6
349
- },
350
- ["ink" /* Ink */]: {
351
- id: 57073,
352
- rpc: "https://rpc-gel.inkonchain.com",
353
- usdt: "0x0200C29006150606B650577BBE7B6248F58470c1",
354
- decimals: 6
355
- },
356
- ["bera" /* Bera */]: {
357
- id: 80094,
358
- rpc: "https://rpc.berachain.com",
359
- usdt: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
360
- decimals: 6
361
- },
362
- ["megaeth" /* MegaETH */]: {
363
- id: 4326,
364
- rpc: "https://carrot.megaeth.com",
365
- usdt: "0xb8ce59fc3717ada4c02eadf9682a9e934f625ebb",
366
- decimals: 6
367
- },
368
- ["base" /* Base */]: {
369
- id: 8453,
370
- rpc: "https://mainnet.base.org",
371
- usdt: "0x102d758f688a4C1C5a80b116bD945d4455460282",
372
- decimals: 6
373
- },
374
- ["bsc" /* BSC */]: {
375
- id: 56,
376
- rpc: "https://bsc-rpc.publicnode.com",
377
- usdt: "0x55d398326f99059fF775485246999027B3197955",
378
- decimals: 18
379
- },
380
- ["hyperevm" /* HyperEVM */]: {
381
- id: 999,
382
- rpc: "https://rpc.hyperliquid.xyz/evm",
383
- usdt: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb",
384
- decimals: 6
385
- }
386
- };
387
-
388
- // src/services/lifi.ts
389
- var LiFiService = class {
390
- constructor(config) {
391
- this.config = config;
392
- }
393
- async quoteSwap(params) {
394
- this._validateSwapParams(params);
395
- const { fromToken, toToken, amount, fromDecimals = 6, toAddress } = params;
384
+ async _withdraw(params) {
385
+ const { amount, tokenDecimals, idempotencyKey } = params;
396
386
  const from = this._requireFrom();
397
- const chain = this.config.chainId.toString();
398
- const fromAmountRaw = _viem.parseUnits.call(void 0, String(amount), fromDecimals).toString();
399
- const qs = new URLSearchParams({
400
- fromChain: chain,
401
- toChain: chain,
402
- fromToken,
403
- toToken,
404
- fromAmount: fromAmountRaw,
405
- fromAddress: from,
406
- ...toAddress && { toAddress }
407
- });
408
- const quote = await this._fetchQuote(qs);
409
- return this._toSwapQuote(quote);
410
- }
411
- async swap(params) {
412
- this._validateSwapParams(params);
413
- const quote = await _asyncNullishCoalesce(params.quote, async () => ( await this.quoteSwap(params)));
414
- return this._executeSwapQuote(quote, this.config.rpc, this.config.chainId);
415
- }
416
- async quoteBridge(params) {
417
- this._validateBridgeParams(params);
418
- const { fromChain, toChain, fromToken, toToken, amount, fromDecimals = 6, recipient } = params;
419
- const from = _nullishCoalesce(_nullishCoalesce(_optionalChain([this, 'access', _7 => _7.config, 'access', _8 => _8.walletClient, 'optionalAccess', _9 => _9.account, 'optionalAccess', _10 => _10.address]), () => ( _optionalChain([this, 'access', _11 => _11.config, 'access', _12 => _12.account, 'optionalAccess', _13 => _13.address]))), () => ( "0x0000000000000000000000000000000000000000"));
420
- const src = CHAIN_CONFIGS[fromChain];
421
- const dst = CHAIN_CONFIGS[toChain];
422
- if (!src) throw new StableValidationError("fromChain", fromChain, `LI.FI bridge: unsupported source chain "${fromChain}"`);
423
- if (!dst) throw new StableValidationError("toChain", toChain, `LI.FI bridge: unsupported destination chain "${toChain}"`);
424
- const qs = new URLSearchParams({
425
- fromChain: src.id.toString(),
426
- toChain: dst.id.toString(),
427
- fromToken,
428
- toToken,
429
- fromAmount: _viem.parseUnits.call(void 0, String(amount), fromDecimals).toString(),
430
- fromAddress: from,
431
- ...recipient && { toAddress: recipient }
432
- });
433
- const quote = this._toSwapQuote(await this._fetchQuote(qs));
434
- return { toAmount: quote.toAmount };
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 = await _asyncNullishCoalesce(tokenDecimals, async () => ( await this._resolveDecimals(vaultData.asset, "earn withdraw")));
392
+ const assetsRaw = _viem.parseUnits.call(void 0, 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
+ });
403
+ }
404
+ return { txHash: receipt.transactionHash, idempotencyKey };
435
405
  }
436
- async bridge(params) {
437
- this._validateBridgeParams(params);
438
- const { fromChain, toChain, fromToken, toToken, amount, fromDecimals = 6, recipient } = params;
406
+ async _redeem(params) {
407
+ const { shares, shareDecimals, idempotencyKey } = params;
439
408
  const from = this._requireFrom();
440
- const src = CHAIN_CONFIGS[fromChain];
441
- const dst = CHAIN_CONFIGS[toChain];
442
- if (!src) {
443
- throw new StableValidationError("fromChain", fromChain, `LI.FI bridge: unsupported source chain "${fromChain}"`);
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: _viem.erc20Abi,
417
+ functionName: "decimals"
418
+ });
419
+ } catch (e2) {
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
+ );
425
+ }
444
426
  }
445
- if (!dst) {
446
- throw new StableValidationError("toChain", toChain, `LI.FI bridge: unsupported destination chain "${toChain}"`);
427
+ const sharesRaw = _viem.parseUnits.call(void 0, 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
438
+ });
447
439
  }
448
- const fromAmountRaw = _viem.parseUnits.call(void 0, String(amount), fromDecimals).toString();
449
- const qs = new URLSearchParams({
450
- fromChain: src.id.toString(),
451
- toChain: dst.id.toString(),
452
- fromToken,
453
- toToken,
454
- fromAmount: fromAmountRaw,
455
- fromAddress: from,
456
- ...recipient && { toAddress: recipient }
457
- });
458
- const quote = this._toSwapQuote(await this._fetchQuote(qs));
459
- return this._executeSwapQuote(quote, src.rpc, src.id);
440
+ return { txHash: receipt.transactionHash, idempotencyKey };
460
441
  }
461
- _validateSwapParams({ fromToken, toToken, amount, toAddress }) {
462
- if (!_viem.isAddress.call(void 0, fromToken)) {
463
- throw new StableValidationError("fromToken", fromToken, `swap: invalid fromToken address "${fromToken}"`);
464
- }
465
- if (!_viem.isAddress.call(void 0, toToken)) {
466
- throw new StableValidationError("toToken", toToken, `swap: invalid toToken address "${toToken}"`);
467
- }
468
- if (fromToken.toLowerCase() === toToken.toLowerCase()) {
469
- throw new StableValidationError("toToken", toToken, "swap: fromToken and toToken must differ");
470
- }
471
- if (!Number.isFinite(amount) || amount <= 0) {
472
- throw new StableValidationError("amount", amount, "swap: amount must be a finite positive number");
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
+ );
473
455
  }
474
- if (toAddress !== void 0 && !_viem.isAddress.call(void 0, toAddress)) {
475
- throw new StableValidationError("toAddress", toAddress, `swap: invalid toAddress "${toAddress}"`);
456
+ const tx = vault.forceWithdraw({
457
+ deallocations: this._buildDeallocs(deallocations, decimals),
458
+ withdraw: { amount: _viem.parseUnits.call(void 0, 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
469
+ });
476
470
  }
471
+ return { txHash: receipt.transactionHash, idempotencyKey };
477
472
  }
478
- _validateBridgeParams({ fromChain, toChain, fromToken, toToken, amount, recipient }) {
479
- if (!_viem.isAddress.call(void 0, fromToken)) {
480
- throw new StableValidationError("fromToken", fromToken, `LI.FI bridge: invalid fromToken address "${fromToken}"`);
481
- }
482
- if (!_viem.isAddress.call(void 0, toToken)) {
483
- throw new StableValidationError("toToken", toToken, `LI.FI bridge: invalid toToken address "${toToken}"`);
484
- }
485
- if (!Number.isFinite(amount) || amount <= 0) {
486
- throw new StableValidationError("amount", amount, "LI.FI bridge: amount must be a finite positive number");
473
+ async _forceRedeem(params) {
474
+ const {
475
+ shares,
476
+ shareDecimals,
477
+ tokenDecimals,
478
+ deallocations,
479
+ idempotencyKey
480
+ } = params;
481
+ const from = this._requireFrom();
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: _viem.erc20Abi,
491
+ functionName: "decimals"
492
+ });
493
+ } catch (e3) {
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
+ }
487
500
  }
488
- if (recipient !== void 0 && !_viem.isAddress.call(void 0, recipient)) {
489
- throw new StableValidationError("recipient", recipient, `LI.FI bridge: invalid recipient address "${recipient}"`);
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
+ );
490
508
  }
491
- if (fromChain === toChain) {
492
- throw new StableValidationError("toChain", toChain, "LI.FI bridge: fromChain and toChain must differ");
509
+ const tx = vault.forceRedeem({
510
+ deallocations: this._buildDeallocs(deallocations, assetDec),
511
+ redeem: { shares: _viem.parseUnits.call(void 0, 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
522
+ });
493
523
  }
524
+ return { txHash: receipt.transactionHash, idempotencyKey };
525
+ }
526
+ _buildDeallocs(deallocations, assetDecimals) {
527
+ return deallocations.map((d) => ({
528
+ adapter: d.adapter,
529
+ amount: _viem.parseUnits.call(void 0, 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
+ }));
494
536
  }
537
+ // ─── Helpers ─────────────────────────────────────────────────────────────
495
538
  _requireFrom() {
496
- const from = _nullishCoalesce(_optionalChain([this, 'access', _14 => _14.config, 'access', _15 => _15.walletClient, 'optionalAccess', _16 => _16.account, 'optionalAccess', _17 => _17.address]), () => ( _optionalChain([this, 'access', _18 => _18.config, 'access', _19 => _19.account, 'optionalAccess', _20 => _20.address])));
539
+ const from = _optionalChain([this, 'access', _5 => _5.wc, 'access', _6 => _6.account, 'optionalAccess', _7 => _7.address]);
497
540
  if (!from) {
498
541
  throw new StableValidationError(
499
542
  "signer",
500
543
  void 0,
501
- "LI.FI: requires a signer \u2014 set walletClient or account in StableConfig"
544
+ "earn: requires a signer \u2014 set walletClient or account in StableConfig"
502
545
  );
503
546
  }
504
547
  return from;
505
548
  }
506
- async _fetchQuote(qs) {
507
- const url = `https://li.quest/v1/quote?${qs}`;
549
+ async _switchChain() {
508
550
  try {
509
- return await retryWithBackoff(() => this._fetchQuoteOnce(url));
551
+ await this.wc.switchChain({ id: this.config.chainId });
510
552
  } catch (err) {
511
- if (err instanceof StableQuoteError) throw err;
512
- if (err instanceof HttpError) {
513
- throw new StableQuoteError({
514
- provider: "lifi",
515
- shortMessage: err.message,
516
- httpStatus: err.status,
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,
517
558
  cause: err
518
559
  });
519
560
  }
520
- throw new StableNetworkError({
521
- shortMessage: "LI.FI: quote request failed before reaching the server",
522
- url,
561
+ }
562
+ }
563
+ async _sendTx(to, data, value, from) {
564
+ try {
565
+ return await this.wc.sendTransaction({
566
+ to,
567
+ data,
568
+ value,
569
+ account: from,
570
+ chain: this.chain
571
+ });
572
+ } catch (err) {
573
+ throw new StableTransactionError({
574
+ phase: "send",
575
+ shortMessage: "earn: transaction send failed",
576
+ chainId: this.config.chainId,
523
577
  cause: err
524
578
  });
525
579
  }
526
580
  }
527
- async _fetchQuoteOnce(url) {
528
- const res = await fetch(url);
529
- const body = await res.json().catch(() => void 0);
530
- if (!res.ok) {
531
- const providerMessage = isObject(body) && typeof body.message === "string" ? body.message : void 0;
532
- const providerCode = isObject(body) && typeof body.code === "string" ? body.code : void 0;
533
- if (res.status === 429 || res.status >= 500) {
534
- throw new HttpError(`LI.FI: ${_nullishCoalesce(providerMessage, () => ( res.statusText))}`, res.status, true);
535
- }
536
- throw new StableQuoteError({
537
- provider: "lifi",
538
- shortMessage: `LI.FI: ${_nullishCoalesce(providerMessage, () => ( res.statusText))}`,
539
- httpStatus: res.status,
540
- providerCode,
541
- body
542
- });
581
+ async _resolveDecimals(asset, op) {
582
+ try {
583
+ return await retryWithBackoff(
584
+ () => this.pc.readContract({
585
+ address: asset,
586
+ abi: _viem.erc20Abi,
587
+ functionName: "decimals"
588
+ })
589
+ );
590
+ } catch (e4) {
591
+ throw new StableValidationError(
592
+ "tokenDecimals",
593
+ void 0,
594
+ `${op}: could not fetch decimals for asset "${asset}" \u2014 pass tokenDecimals explicitly`
595
+ );
543
596
  }
544
- if (!isLiFiQuoteShape(body)) {
545
- throw new StableQuoteError({
546
- provider: "lifi",
547
- shortMessage: "LI.FI: unexpected quote response shape",
548
- httpStatus: res.status,
549
- body
550
- });
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
+ );
622
+ }
623
+ }
624
+ };
625
+
626
+ // src/earn/reader.ts
627
+
628
+
629
+
630
+
631
+
632
+
633
+
634
+
635
+
636
+
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 (!_optionalChain([config, 'access', _8 => _8.earn, 'optionalAccess', _9 => _9.vault])) {
664
+ throw new StableValidationError(
665
+ "earn.vault",
666
+ void 0,
667
+ "createStableReader: earn.vault is required"
668
+ );
669
+ }
670
+ if (!_viem.isAddress.call(void 0, config.address)) {
671
+ throw new StableValidationError(
672
+ "address",
673
+ config.address,
674
+ `createStableReader: invalid user address "${config.address}"`
675
+ );
676
+ }
677
+ if (!_viem.isAddress.call(void 0, config.earn.vault)) {
678
+ throw new StableValidationError(
679
+ "earn.vault",
680
+ config.earn.vault,
681
+ `createStableReader: invalid vault address "${config.earn.vault}"`
682
+ );
551
683
  }
552
- return body;
684
+ const network = _nullishCoalesce(config.network, () => ( "mainnet")) /* Mainnet */;
685
+ const viemChain = network === "mainnet" /* Mainnet */ ? _chains.stable : _chains.stableTestnet;
686
+ const rpc = (_nullishCoalesce(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 = _viem.createPublicClient.call(void 0, { chain, transport: _viem.http.call(void 0, rpc) });
695
+ this.morpho = new (0, _morphosdk.MorphoClient)(this.pc);
553
696
  }
554
- _toSwapQuote(quote) {
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: _viem.erc20Abi,
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: _viem.erc20Abi,
712
+ functionName: "decimals"
713
+ }),
714
+ this.pc.readContract({
715
+ address: this.vaultAddr,
716
+ abi: _viem.erc20Abi,
717
+ functionName: "decimals"
718
+ })
719
+ ]);
720
+ const assetsRaw = vaultData.toAssets(shares);
555
721
  return {
556
- toAmount: Number(_viem.formatUnits.call(void 0, BigInt(quote.estimate.toAmount), quote.action.toToken.decimals)),
557
- fromAmount: BigInt(quote.estimate.fromAmount),
558
- fromToken: quote.action.fromToken.address,
559
- approvalAddress: quote.estimate.approvalAddress,
560
- transactionRequest: quote.transactionRequest
722
+ shares,
723
+ sharesFormatted: Number(_viem.formatUnits.call(void 0, shares, shareDecimals)),
724
+ shareDecimals,
725
+ assets: Number(_viem.formatUnits.call(void 0, assetsRaw, tokenDecimals)),
726
+ tokenDecimals,
727
+ assetAddress: vaultData.asset
561
728
  };
562
729
  }
563
- async _executeSwapQuote(quote, srcRpc, expectedChainId) {
564
- const from = this._requireFrom();
565
- const { transactionRequest } = quote;
566
- const srcChainId = transactionRequest.chainId;
567
- if (srcChainId !== expectedChainId) {
568
- throw new StableQuoteError({
569
- provider: "lifi",
570
- shortMessage: `LI.FI: returned tx for chain ${srcChainId} but expected ${expectedChainId}`,
571
- providerCode: "chain_mismatch",
572
- body: quote
573
- });
574
- }
575
- const srcChain = makeChain(srcChainId, srcRpc);
576
- const wc = _nullishCoalesce(this.config.walletClient, () => ( _viem.createWalletClient.call(void 0, {
577
- account: this.config.account,
578
- chain: srcChain,
579
- transport: _nullishCoalesce(this.config.transport, () => ( _viem.http.call(void 0, srcRpc)))
730
+ async getYield() {
731
+ return this._fetchYield();
732
+ }
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
+ };
741
+ }
742
+ async withdrawability(params) {
743
+ const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
744
+ const vaultData = await vault.getData();
745
+ const tokenDecimals = await _asyncNullishCoalesce(params.tokenDecimals, async () => ( await this.pc.readContract({
746
+ address: vaultData.asset,
747
+ abi: _viem.erc20Abi,
748
+ functionName: "decimals"
580
749
  })));
581
- try {
582
- await wc.switchChain({ id: srcChainId });
583
- } catch (err) {
584
- throw new StableTransactionError({
585
- phase: "switch_chain",
586
- shortMessage: `LI.FI: wallet rejected or failed to switch to chain ${srcChainId}`,
587
- chainId: srcChainId,
588
- cause: err
589
- });
590
- }
591
- await this._ensureApproval(wc, srcChain, srcRpc, srcChainId, from, quote);
592
- let hash;
593
- try {
594
- hash = await wc.sendTransaction({
595
- to: transactionRequest.to,
596
- data: transactionRequest.data,
597
- value: transactionRequest.value && transactionRequest.value !== "0x" ? BigInt(transactionRequest.value) : 0n,
598
- gas: transactionRequest.gasLimit ? BigInt(transactionRequest.gasLimit) : void 0,
599
- account: wc.account,
600
- chain: srcChain
601
- });
602
- } catch (err) {
603
- throw new StableTransactionError({
604
- phase: "send",
605
- shortMessage: "LI.FI: swap/bridge transaction failed",
606
- chainId: srcChainId,
607
- cause: err
608
- });
750
+ const requestedRaw = _viem.parseUnits.call(void 0, String(params.amount), tokenDecimals);
751
+ let availableLiquidityRaw = vaultData.assetBalance;
752
+ if (vaultData.accrualLiquidityAdapter != null) {
753
+ availableLiquidityRaw += vaultData.accrualLiquidityAdapter.maxWithdraw(
754
+ vaultData.liquidityData
755
+ ).value;
609
756
  }
610
- return { txHash: hash, toAmount: quote.toAmount };
757
+ return {
758
+ isInstant: requestedRaw <= availableLiquidityRaw,
759
+ availableLiquidity: Number(
760
+ _viem.formatUnits.call(void 0, availableLiquidityRaw, tokenDecimals)
761
+ ),
762
+ tokenDecimals
763
+ };
611
764
  }
612
- async _ensureApproval(wc, srcChain, srcRpc, srcChainId, from, quote) {
613
- const tokenAddr = quote.fromToken;
614
- const isNative = tokenAddr.toLowerCase() === "0x0000000000000000000000000000000000000000" || tokenAddr.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
615
- if (isNative || !quote.approvalAddress) return;
616
- const pc = _viem.createPublicClient.call(void 0, { chain: srcChain, transport: _viem.http.call(void 0, srcRpc) });
617
- const allowance = await pc.readContract({
618
- address: tokenAddr,
619
- abi: _viem.erc20Abi,
620
- functionName: "allowance",
621
- args: [from, quote.approvalAddress]
622
- });
623
- if (allowance >= quote.fromAmount) return;
624
- let approveHash;
765
+ async _fetchNetApy() {
766
+ const { apy } = await this._fetchYield();
767
+ return apy;
768
+ }
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
+ }
782
+ }
783
+ `;
784
+ let body;
625
785
  try {
626
- approveHash = await wc.writeContract({
627
- address: tokenAddr,
628
- abi: _viem.erc20Abi,
629
- functionName: "approve",
630
- args: [quote.approvalAddress, quote.fromAmount],
631
- account: wc.account,
632
- chain: srcChain
633
- });
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
+ })
798
+ });
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
+ );
634
811
  } catch (err) {
635
- throw new StableTransactionError({
636
- phase: "approve",
637
- shortMessage: "LI.FI: token approve failed",
638
- chainId: srcChainId,
639
- cause: err
640
- });
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
+ });
820
+ }
821
+ throw err;
641
822
  }
642
- const approveReceipt = await pc.waitForTransactionReceipt({ hash: approveHash });
643
- if (approveReceipt.status === "reverted") {
644
- throw new StableTransactionError({
645
- phase: "approve",
646
- shortMessage: "LI.FI: token approve reverted on-chain",
647
- txHash: approveReceipt.transactionHash,
648
- chainId: srcChainId
649
- });
823
+ const vault = _optionalChain([body, 'optionalAccess', _10 => _10.data, 'optionalAccess', _11 => _11.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
+ );
650
830
  }
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: _nullishCoalesce(vault.avgNetApyExcludingRewards, () => ( 0)),
839
+ rewards,
840
+ performanceFee: vault.performanceFee,
841
+ managementFee: vault.managementFee
842
+ };
651
843
  }
652
844
  };
653
- function isObject(v) {
654
- return typeof v === "object" && v !== null;
655
- }
656
- function isLiFiQuoteShape(v) {
657
- if (!isObject(v)) return false;
658
- const tx = v.transactionRequest;
659
- const est = v.estimate;
660
- const act = v.action;
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);
662
- }
663
-
664
- // src/earn/morpho.ts
665
-
666
845
 
846
+ // src/services/lifi.ts
667
847
 
668
848
 
669
849
 
@@ -674,340 +854,521 @@ function isLiFiQuoteShape(v) {
674
854
 
675
855
 
676
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
+ };
677
928
 
678
- var MorphoService = class {
929
+ // src/services/lifi.ts
930
+ var LiFiService = class {
679
931
  constructor(config) {
680
932
  this.config = config;
681
- this.inFlight = /* @__PURE__ */ new Map();
682
- this.chain = makeChain(config.chainId, config.rpc);
683
- this.wc = _nullishCoalesce(config.walletClient, () => ( _viem.createWalletClient.call(void 0, {
684
- account: config.account,
685
- chain: this.chain,
686
- transport: _nullishCoalesce(config.transport, () => ( _viem.http.call(void 0, config.rpc)))
687
- })));
688
- const morphoWc = _optionalChain([this, 'access', _21 => _21.wc, 'access', _22 => _22.chain, 'optionalAccess', _23 => _23.id]) === config.chainId ? this.wc : _viem.createWalletClient.call(void 0, {
689
- account: this.wc.account,
690
- chain: this.chain,
691
- transport: _viem.custom.call(void 0, { request: this.wc.request.bind(this.wc) })
692
- });
693
- this.morpho = new (0, _morphosdk.MorphoClient)(morphoWc, { supportSignature: true });
694
- this.pc = _viem.createPublicClient.call(void 0, { chain: this.chain, transport: _viem.http.call(void 0, 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
933
  }
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));
934
+ async quoteSwap(params) {
935
+ this._validateSwapParams(params);
936
+ const {
937
+ fromToken,
938
+ toToken,
939
+ amount,
940
+ fromDecimals = 6,
941
+ toAddress
942
+ } = params;
943
+ const from = this._requireFrom();
944
+ const chain = this.config.chainId.toString();
945
+ const fromAmountRaw = _viem.parseUnits.call(void 0,
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);
734
960
  }
735
- forceWithdraw(params) {
736
- this._validateAmount(params.amount, "earn forceWithdraw");
737
- return this._withIdempotency(params.idempotencyKey, () => this._forceWithdraw(params));
961
+ async swap(params) {
962
+ this._validateSwapParams(params);
963
+ const quote = await _asyncNullishCoalesce(params.quote, async () => ( await this.quoteSwap(params)));
964
+ return this._executeSwapQuote(
965
+ quote,
966
+ this.config.rpc,
967
+ this.config.chainId
968
+ );
738
969
  }
739
- forceRedeem(params) {
740
- this._validateShares(params.shares, "earn forceRedeem");
741
- return this._withIdempotency(params.idempotencyKey, () => this._forceRedeem(params));
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 = _nullishCoalesce(_nullishCoalesce(_optionalChain([this, 'access', _12 => _12.config, 'access', _13 => _13.walletClient, 'optionalAccess', _14 => _14.account, 'optionalAccess', _15 => _15.address]), () => ( _optionalChain([this, 'access', _16 => _16.config, 'access', _17 => _17.account, 'optionalAccess', _18 => _18.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: _viem.parseUnits.call(void 0, 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 };
742
1007
  }
743
- // ─── Private operations ──────────────────────────────────────────────────
744
- async _deposit(params) {
745
- const { amount, tokenDecimals, slippageTolerance, idempotencyKey } = params;
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;
746
1019
  const from = this._requireFrom();
747
- const vaultAddr = _nullishCoalesce(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 = await _asyncNullishCoalesce(tokenDecimals, async () => ( await this._resolveDecimals(vaultData.asset, "earn deposit")));
752
- const assetsRaw = _viem.parseUnits.call(void 0, String(amount), decimals);
753
- const depositAction = vault.deposit({
754
- amount: assetsRaw,
755
- userAddress: from,
756
- accrualVault: vaultData,
757
- slippageTolerance: slippageTolerance !== void 0 ? _viem.parseUnits.call(void 0, String(slippageTolerance), 18) : void 0
758
- });
759
- const requirements = await depositAction.getRequirements();
760
- let sig;
761
- for (const req of requirements) {
762
- if (_morphosdk.isRequirementApproval.call(void 0, 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 (_morphosdk.isRequirementSignature.call(void 0, req)) {
774
- sig = await req.sign(this.wc, from);
775
- }
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
+ );
776
1028
  }
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
- });
1029
+ if (!dst) {
1030
+ throw new StableValidationError(
1031
+ "toChain",
1032
+ toChain,
1033
+ `LI.FI bridge: unsupported destination chain "${toChain}"`
1034
+ );
787
1035
  }
788
- return { txHash: receipt.transactionHash, idempotencyKey };
1036
+ const fromAmountRaw = _viem.parseUnits.call(void 0,
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);
789
1051
  }
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 = await _asyncNullishCoalesce(tokenDecimals, async () => ( await this._resolveDecimals(vaultData.asset, "earn withdraw")));
798
- const assetsRaw = _viem.parseUnits.call(void 0, 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
- });
1052
+ _validateSwapParams({
1053
+ fromToken,
1054
+ toToken,
1055
+ amount,
1056
+ toAddress
1057
+ }) {
1058
+ if (!_viem.isAddress.call(void 0, fromToken)) {
1059
+ throw new StableValidationError(
1060
+ "fromToken",
1061
+ fromToken,
1062
+ `swap: invalid fromToken address "${fromToken}"`
1063
+ );
809
1064
  }
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: _viem.erc20Abi, functionName: "decimals" });
821
- } catch (e2) {
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
- }
1065
+ if (!_viem.isAddress.call(void 0, toToken)) {
1066
+ throw new StableValidationError(
1067
+ "toToken",
1068
+ toToken,
1069
+ `swap: invalid toToken address "${toToken}"`
1070
+ );
828
1071
  }
829
- const sharesRaw = _viem.parseUnits.call(void 0, 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
- });
1072
+ if (fromToken.toLowerCase() === toToken.toLowerCase()) {
1073
+ throw new StableValidationError(
1074
+ "toToken",
1075
+ toToken,
1076
+ "swap: fromToken and toToken must differ"
1077
+ );
841
1078
  }
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");
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
+ );
854
1085
  }
855
- const tx = vault.forceWithdraw({
856
- deallocations: this._buildDeallocs(deallocations, decimals),
857
- withdraw: { amount: _viem.parseUnits.call(void 0, 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
- });
1086
+ if (toAddress !== void 0 && !_viem.isAddress.call(void 0, toAddress)) {
1087
+ throw new StableValidationError(
1088
+ "toAddress",
1089
+ toAddress,
1090
+ `swap: invalid toAddress "${toAddress}"`
1091
+ );
869
1092
  }
870
- return { txHash: receipt.transactionHash, idempotencyKey };
871
1093
  }
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: _viem.erc20Abi, functionName: "decimals" });
882
- } catch (e3) {
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
- }
1094
+ _validateBridgeParams({
1095
+ fromChain,
1096
+ toChain,
1097
+ fromToken,
1098
+ toToken,
1099
+ amount,
1100
+ recipient
1101
+ }) {
1102
+ if (!_viem.isAddress.call(void 0, fromToken)) {
1103
+ throw new StableValidationError(
1104
+ "fromToken",
1105
+ fromToken,
1106
+ `LI.FI bridge: invalid fromToken address "${fromToken}"`
1107
+ );
889
1108
  }
890
- let assetDec = tokenDecimals;
891
- if (assetDec === void 0) {
892
- const vaultData = await vault.getData();
893
- assetDec = await this._resolveDecimals(vaultData.asset, "earn forceRedeem");
1109
+ if (!_viem.isAddress.call(void 0, toToken)) {
1110
+ throw new StableValidationError(
1111
+ "toToken",
1112
+ toToken,
1113
+ `LI.FI bridge: invalid toToken address "${toToken}"`
1114
+ );
894
1115
  }
895
- const tx = vault.forceRedeem({
896
- deallocations: this._buildDeallocs(deallocations, assetDec),
897
- redeem: { shares: _viem.parseUnits.call(void 0, 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
- });
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 && !_viem.isAddress.call(void 0, 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
+ );
909
1136
  }
910
- return { txHash: receipt.transactionHash, idempotencyKey };
911
- }
912
- _buildDeallocs(deallocations, assetDecimals) {
913
- return deallocations.map((d) => ({
914
- adapter: d.adapter,
915
- amount: _viem.parseUnits.call(void 0, 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
1137
  }
921
- // ─── Helpers ─────────────────────────────────────────────────────────────
922
1138
  _requireFrom() {
923
- const from = _optionalChain([this, 'access', _24 => _24.wc, 'access', _25 => _25.account, 'optionalAccess', _26 => _26.address]);
1139
+ const from = _nullishCoalesce(_optionalChain([this, 'access', _19 => _19.config, 'access', _20 => _20.walletClient, 'optionalAccess', _21 => _21.account, 'optionalAccess', _22 => _22.address]), () => ( _optionalChain([this, 'access', _23 => _23.config, 'access', _24 => _24.account, 'optionalAccess', _25 => _25.address])));
924
1140
  if (!from) {
925
1141
  throw new StableValidationError(
926
1142
  "signer",
927
1143
  void 0,
928
- "earn: requires a signer \u2014 set walletClient or account in StableConfig"
1144
+ "LI.FI: requires a signer \u2014 set walletClient or account in StableConfig"
929
1145
  );
930
1146
  }
931
1147
  return from;
932
1148
  }
933
- async _switchChain() {
1149
+ async _fetchQuote(qs) {
1150
+ const url = `https://li.quest/v1/quote?${qs}`;
934
1151
  try {
935
- await this.wc.switchChain({ id: this.config.chainId });
1152
+ return await retryWithBackoff(() => this._fetchQuoteOnce(url));
936
1153
  } 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,
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,
942
1160
  cause: err
943
1161
  });
944
1162
  }
1163
+ throw new StableNetworkError({
1164
+ shortMessage: "LI.FI: quote request failed before reaching the server",
1165
+ url,
1166
+ cause: err
1167
+ });
945
1168
  }
946
1169
  }
947
- 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: ${_nullishCoalesce(providerMessage, () => ( res.statusText))}`,
1179
+ res.status,
1180
+ true
1181
+ );
1182
+ }
1183
+ throw new StableQuoteError({
1184
+ provider: "lifi",
1185
+ shortMessage: `LI.FI: ${_nullishCoalesce(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
+ _viem.formatUnits.call(void 0,
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 = _nullishCoalesce(this.config.walletClient, () => ( _viem.createWalletClient.call(void 0, {
1229
+ account: this.config.account,
1230
+ chain: srcChain,
1231
+ transport: _nullishCoalesce(this.config.transport, () => ( _viem.http.call(void 0, srcRpc)))
1232
+ })));
948
1233
  try {
949
- return await this.wc.sendTransaction({ to, data, value, account: from, chain: this.chain });
1234
+ await wc.switchChain({ id: srcChainId });
950
1235
  } catch (err) {
951
1236
  throw new StableTransactionError({
952
- phase: "send",
953
- shortMessage: "earn: transaction send failed",
954
- chainId: this.config.chainId,
1237
+ phase: "switch_chain",
1238
+ shortMessage: `LI.FI: wallet rejected or failed to switch to chain ${srcChainId}`,
1239
+ chainId: srcChainId,
955
1240
  cause: err
956
1241
  });
957
1242
  }
958
- }
959
- async _resolveDecimals(asset, op) {
1243
+ await this._ensureApproval(
1244
+ wc,
1245
+ srcChain,
1246
+ srcRpc,
1247
+ srcChainId,
1248
+ from,
1249
+ quote
1250
+ );
1251
+ let hash;
960
1252
  try {
961
- return await retryWithBackoff(
962
- () => this.pc.readContract({ address: asset, abi: _viem.erc20Abi, functionName: "decimals" })
963
- );
964
- } catch (e4) {
965
- throw new StableValidationError(
966
- "tokenDecimals",
967
- void 0,
968
- `${op}: could not fetch decimals for asset "${asset}" \u2014 pass tokenDecimals explicitly`
969
- );
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
+ });
970
1268
  }
1269
+ return { txHash: hash, toAmount: quote.toAmount };
971
1270
  }
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`);
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 = _viem.createPublicClient.call(void 0, {
1276
+ chain: srcChain,
1277
+ transport: _viem.http.call(void 0, srcRpc)
1278
+ });
1279
+ const allowance = await pc.readContract({
1280
+ address: tokenAddr,
1281
+ abi: _viem.erc20Abi,
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: _viem.erc20Abi,
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
+ });
983
1303
  }
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`);
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
+ });
988
1314
  }
989
1315
  }
990
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
+ }
991
1327
 
992
1328
  // src/stable.ts
993
- function validateTransferParams({ to, amount, token, tokenDecimals }) {
1329
+ function validateTransferParams({
1330
+ to,
1331
+ amount,
1332
+ token,
1333
+ tokenDecimals
1334
+ }) {
994
1335
  if (!_viem.isAddress.call(void 0, to)) {
995
- 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
+ );
996
1341
  }
997
1342
  if (!Number.isFinite(amount) || amount <= 0) {
998
- 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
+ );
999
1348
  }
1000
1349
  if (token !== void 0 && !_viem.isAddress.call(void 0, token)) {
1001
- 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
+ );
1002
1355
  }
1003
1356
  if (tokenDecimals !== void 0 && (!Number.isInteger(tokenDecimals) || tokenDecimals < 0 || tokenDecimals > 77)) {
1004
- 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
+ );
1005
1362
  }
1006
1363
  }
1007
1364
  async function resolveTokenDecimals(publicClient, token, tokenDecimals) {
1008
1365
  if (tokenDecimals !== void 0) return tokenDecimals;
1009
1366
  try {
1010
- return await publicClient.readContract({ address: token, abi: _viem.erc20Abi, functionName: "decimals" });
1367
+ return await publicClient.readContract({
1368
+ address: token,
1369
+ abi: _viem.erc20Abi,
1370
+ functionName: "decimals"
1371
+ });
1011
1372
  } catch (e5) {
1012
1373
  throw new StableValidationError(
1013
1374
  "tokenDecimals",
@@ -1025,7 +1386,11 @@ async function executeTokenTransfer(wc, publicClient, token, to, amount, tokenDe
1025
1386
  `transfer: no contract found at "${token}" on chain ${chain.id} \u2014 check the address and network`
1026
1387
  );
1027
1388
  }
1028
- const decimals = await resolveTokenDecimals(publicClient, token, tokenDecimals);
1389
+ const decimals = await resolveTokenDecimals(
1390
+ publicClient,
1391
+ token,
1392
+ tokenDecimals
1393
+ );
1029
1394
  return wc.writeContract({
1030
1395
  address: token,
1031
1396
  abi: _viem.erc20Abi,
@@ -1044,10 +1409,18 @@ function createStable({
1044
1409
  earn: earnConfig
1045
1410
  }) {
1046
1411
  const chain = network === "mainnet" /* Mainnet */ ? _chains.stable : _chains.stableTestnet;
1047
- const rpc = (_nullishCoalesce(rpcOverride, () => ( chain.rpcUrls.default.http[0]))).replace(/\/$/, "");
1412
+ const rpc = (_nullishCoalesce(rpcOverride, () => ( chain.rpcUrls.default.http[0]))).replace(
1413
+ /\/$/,
1414
+ ""
1415
+ );
1048
1416
  const signerConfig = { account, transport, walletClient };
1049
1417
  const lifi = new LiFiService({ ...signerConfig, rpc, chainId: chain.id });
1050
- 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;
1051
1424
  function requireEarn() {
1052
1425
  if (!morpho) {
1053
1426
  throw new StableValidationError(
@@ -1061,7 +1434,7 @@ function createStable({
1061
1434
  async function transfer(params) {
1062
1435
  validateTransferParams(params);
1063
1436
  const { from, to, amount, token, tokenDecimals } = params;
1064
- const acc = _nullishCoalesce(_nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _27 => _27.account]), () => ( account)), () => ( from));
1437
+ const acc = _nullishCoalesce(_nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _26 => _26.account]), () => ( account)), () => ( from));
1065
1438
  const wc = _nullishCoalesce(walletClient, () => ( _viem.createWalletClient.call(void 0, {
1066
1439
  account: acc,
1067
1440
  chain,
@@ -1077,8 +1450,29 @@ function createStable({
1077
1450
  cause: err
1078
1451
  });
1079
1452
  }
1080
- const publicClient = _viem.createPublicClient.call(void 0, { chain, transport: _viem.http.call(void 0, 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: _viem.parseUnits.call(void 0, String(amount), 18) });
1453
+ const publicClient = _viem.createPublicClient.call(void 0, {
1454
+ chain,
1455
+ transport: _viem.http.call(void 0, 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: _viem.parseUnits.call(void 0, String(amount), 18)
1474
+ })
1475
+ );
1082
1476
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
1083
1477
  if (receipt.status === "reverted") {
1084
1478
  throw new StableTransactionError({
@@ -1100,6 +1494,7 @@ function createStable({
1100
1494
  deposit: (params) => requireEarn().deposit(params),
1101
1495
  bulkDeposit: (items, options) => requireEarn().bulkDeposit(items, options),
1102
1496
  withdraw: (params) => requireEarn().withdraw(params),
1497
+ bulkWithdraw: (items, options) => requireEarn().bulkWithdraw(items, options),
1103
1498
  redeem: (params) => requireEarn().redeem(params),
1104
1499
  forceWithdraw: (params) => requireEarn().forceWithdraw(params),
1105
1500
  forceRedeem: (params) => requireEarn().forceRedeem(params)
@@ -1110,6 +1505,7 @@ function createStableReader(config) {
1110
1505
  const reader = new EarnReader(config);
1111
1506
  return {
1112
1507
  earn: {
1508
+ getYield: () => reader.getYield(),
1113
1509
  position: () => reader.position(),
1114
1510
  preview: (params) => reader.preview(params),
1115
1511
  withdrawability: (params) => reader.withdrawability(params)