@stablechain/sdk 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,25 +1,29 @@
1
1
  // src/stable.ts
2
2
  import {
3
- createPublicClient as createPublicClient4,
4
- createWalletClient as createWalletClient3,
3
+ http as http5,
4
+ createPublicClient as createPublicClient5,
5
+ createWalletClient as createWalletClient4,
5
6
  erc20Abi as erc20Abi4,
6
- http as http4,
7
- isAddress as isAddress3,
7
+ isAddress as isAddress4,
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",
@@ -121,7 +126,11 @@ var HttpError = class extends Error {
121
126
  };
122
127
  function isRetryableError(err) {
123
128
  if (err instanceof HttpError) return err.retryable;
124
- return err instanceof TypeError;
129
+ if (err instanceof TypeError) {
130
+ const msg = err.message.toLowerCase();
131
+ return !(msg.includes("cors") || msg.includes("blocked"));
132
+ }
133
+ return false;
125
134
  }
126
135
  async function retryWithBackoff(fn, isRetryable = isRetryableError, opts = {}) {
127
136
  const maxAttempts = opts.maxAttempts ?? 3;
@@ -142,937 +151,1622 @@ async function retryWithBackoff(fn, isRetryable = isRetryableError, opts = {}) {
142
151
  throw lastErr;
143
152
  }
144
153
 
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 {
154
+ // src/earn/morpho.ts
155
+ var MorphoService = class {
169
156
  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)) {
157
+ this.config = config;
158
+ this.inFlight = /* @__PURE__ */ new Map();
159
+ this.chain = makeChain(config.chainId, config.rpc);
160
+ this.wc = config.walletClient ?? createWalletClient({
161
+ account: config.account,
162
+ chain: this.chain,
163
+ transport: config.transport ?? http(config.rpc)
164
+ });
165
+ const morphoWc = this.wc.chain?.id === config.chainId ? this.wc : createWalletClient({
166
+ account: this.wc.account,
167
+ chain: this.chain,
168
+ transport: custom({
169
+ request: this.wc.request.bind(this.wc)
170
+ })
171
+ });
172
+ this.morpho = new MorphoClient(morphoWc, { supportSignature: true });
173
+ this.morphoNoSig = new MorphoClient(morphoWc, { supportSignature: false });
174
+ this.pc = createPublicClient({
175
+ chain: this.chain,
176
+ transport: http(config.rpc)
177
+ });
178
+ }
179
+ deposit(params) {
180
+ this._validateAmount(params.amount, "earn deposit");
181
+ if (params.depositBuffer !== void 0 && (params.depositBuffer <= 0 || params.depositBuffer > 1)) {
185
182
  throw new StableValidationError(
186
- "earn.vault",
187
- config.earn.vault,
188
- `createStableReader: invalid vault address "${config.earn.vault}"`
183
+ "depositBuffer",
184
+ params.depositBuffer,
185
+ "earn deposit: depositBuffer must be > 0 and \u2264 1"
189
186
  );
190
187
  }
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);
188
+ return this._withIdempotency(
189
+ params.idempotencyKey,
190
+ () => this._deposit(params)
191
+ );
200
192
  }
201
- async position() {
202
- const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
203
- const [vaultData, shares] = await Promise.all([
204
- vault.getData(),
205
- this.pc.readContract({
206
- address: this.vaultAddr,
207
- abi: erc20Abi,
208
- functionName: "balanceOf",
209
- args: [this.userAddr]
210
- })
211
- ]);
212
- const [tokenDecimals, shareDecimals] = await Promise.all([
213
- this.pc.readContract({ address: vaultData.asset, abi: erc20Abi, functionName: "decimals" }),
214
- this.pc.readContract({ address: this.vaultAddr, abi: erc20Abi, functionName: "decimals" })
215
- ]);
216
- const assetsRaw = vaultData.toAssets(shares);
217
- return {
218
- shares,
219
- sharesFormatted: Number(formatUnits(shares, shareDecimals)),
220
- shareDecimals,
221
- assets: Number(formatUnits(assetsRaw, tokenDecimals)),
222
- tokenDecimals,
223
- assetAddress: vaultData.asset
193
+ async bulkDeposit(items, options = {}) {
194
+ const { idempotencyKey, stopOnError = false } = options;
195
+ const run = async () => {
196
+ const results = [];
197
+ for (const item of items) {
198
+ try {
199
+ const result = await this.deposit(item);
200
+ results.push({
201
+ status: "fulfilled",
202
+ txHash: result.txHash,
203
+ vault: item.vault,
204
+ idempotencyKey: item.idempotencyKey
205
+ });
206
+ } catch (err) {
207
+ const entry = {
208
+ status: "rejected",
209
+ error: String(err),
210
+ vault: item.vault,
211
+ idempotencyKey: item.idempotencyKey
212
+ };
213
+ results.push(entry);
214
+ if (stopOnError) {
215
+ const succeeded2 = results.filter(
216
+ (r) => r.status === "fulfilled"
217
+ ).length;
218
+ throw new StableBulkError({
219
+ shortMessage: "bulkDeposit halted on first failure",
220
+ results,
221
+ succeeded: succeeded2,
222
+ failed: results.length - succeeded2
223
+ });
224
+ }
225
+ }
226
+ }
227
+ const succeeded = results.filter(
228
+ (r) => r.status === "fulfilled"
229
+ ).length;
230
+ return {
231
+ results,
232
+ succeeded,
233
+ failed: results.length - succeeded,
234
+ idempotencyKey
235
+ };
224
236
  };
237
+ return this._withIdempotency(idempotencyKey, run);
225
238
  }
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
239
+ async bulkWithdraw(items, options = {}) {
240
+ const { idempotencyKey, stopOnError = false } = options;
241
+ const run = async () => {
242
+ const results = [];
243
+ for (const item of items) {
244
+ try {
245
+ const result = await this.withdraw(item);
246
+ results.push({
247
+ status: "fulfilled",
248
+ txHash: result.txHash,
249
+ idempotencyKey: item.idempotencyKey
250
+ });
251
+ } catch (err) {
252
+ const entry = {
253
+ status: "rejected",
254
+ error: String(err),
255
+ idempotencyKey: item.idempotencyKey
256
+ };
257
+ results.push(entry);
258
+ if (stopOnError) {
259
+ const succeeded2 = results.filter(
260
+ (r) => r.status === "fulfilled"
261
+ ).length;
262
+ throw new StableBulkError({
263
+ shortMessage: "bulkWithdraw halted on first failure",
264
+ results,
265
+ succeeded: succeeded2,
266
+ failed: results.length - succeeded2
267
+ });
268
+ }
269
+ }
270
+ }
271
+ const succeeded = results.filter(
272
+ (r) => r.status === "fulfilled"
273
+ ).length;
274
+ return {
275
+ results,
276
+ succeeded,
277
+ failed: results.length - succeeded,
278
+ idempotencyKey
279
+ };
236
280
  };
281
+ return this._withIdempotency(idempotencyKey, run);
237
282
  }
238
- async withdrawability(params) {
239
- const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
240
- const vaultData = await vault.getData();
241
- const tokenDecimals = params.tokenDecimals ?? await this.pc.readContract({
242
- address: vaultData.asset,
243
- abi: erc20Abi,
244
- functionName: "decimals"
245
- });
246
- const requestedRaw = parseUnits(String(params.amount), tokenDecimals);
247
- const availableLiquidityRaw = vaultData.assetBalance;
248
- return {
249
- isInstant: requestedRaw <= availableLiquidityRaw,
250
- availableLiquidity: Number(formatUnits(availableLiquidityRaw, tokenDecimals)),
251
- tokenDecimals
252
- };
283
+ withdraw(params) {
284
+ this._validateAmount(params.amount, "earn withdraw");
285
+ return this._withIdempotency(
286
+ params.idempotencyKey,
287
+ () => this._withdraw(params)
288
+ );
253
289
  }
254
- async _fetchNetApy() {
255
- const { apy } = await this._fetchYield();
256
- return apy;
290
+ redeem(params) {
291
+ this._validateShares(params.shares, "earn redeem");
292
+ return this._withIdempotency(
293
+ params.idempotencyKey,
294
+ () => this._redeem(params)
295
+ );
257
296
  }
258
- async _fetchYield() {
259
- const query = `
260
- query VaultYield($address: String!, $chainId: Int!) {
261
- vaultV2ByAddress(address: $address, chainId: $chainId) {
262
- netApy
263
- netApyExcludingRewards
264
- performanceFee
265
- managementFee
266
- rewards {
267
- asset { symbol address }
268
- supplyApr
269
- }
270
- }
271
- }
272
- `;
273
- let body;
274
- try {
275
- body = await retryWithBackoff(
276
- async () => {
277
- const res = await fetch(MORPHO_API_URL, {
278
- method: "POST",
279
- headers: { "Content-Type": "application/json" },
280
- body: JSON.stringify({
281
- query,
282
- variables: { address: this.vaultAddr, chainId: this.chainId }
283
- })
284
- });
285
- if (!res.ok) {
286
- const retryable = res.status === 429 || res.status >= 500;
287
- throw new HttpError(
288
- `earn: Morpho API returned ${res.status} \u2014 ${res.statusText}`,
289
- res.status,
290
- retryable
291
- );
292
- }
293
- return res.json().catch(() => void 0);
294
- },
295
- (err) => err instanceof HttpError ? err.retryable : err instanceof TypeError
297
+ forceWithdraw(params) {
298
+ this._validateAmount(params.amount, "earn forceWithdraw");
299
+ return this._withIdempotency(
300
+ params.idempotencyKey,
301
+ () => this._forceWithdraw(params)
302
+ );
303
+ }
304
+ forceRedeem(params) {
305
+ this._validateShares(params.shares, "earn forceRedeem");
306
+ return this._withIdempotency(
307
+ params.idempotencyKey,
308
+ () => this._forceRedeem(params)
309
+ );
310
+ }
311
+ // ─── Calldata preparation (no signing) ───────────────────────────────────
312
+ async prepareDepositCalldata(params) {
313
+ this._validateAmount(params.amount, "prepareDepositCalldata");
314
+ if (params.depositBuffer !== void 0 && (params.depositBuffer <= 0 || params.depositBuffer > 1)) {
315
+ throw new StableValidationError(
316
+ "depositBuffer",
317
+ params.depositBuffer,
318
+ "prepareDepositCalldata: depositBuffer must be > 0 and \u2264 1"
296
319
  );
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
- });
320
+ }
321
+ const { amount, tokenDecimals, slippageTolerance, depositBuffer } = params;
322
+ const from = this._requireFrom();
323
+ const vaultAddr = params.vault ?? this.config.vault;
324
+ const vault = this.morphoNoSig.vaultV2(vaultAddr, this.config.chainId);
325
+ const vaultData = await vault.getData();
326
+ const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "prepareDepositCalldata");
327
+ const assetsRaw = parseUnits(String(amount), decimals);
328
+ if (depositBuffer !== void 0) {
329
+ const bps = BigInt(Math.round(depositBuffer * 1e4));
330
+ const maxAllowed = vaultData.assetBalance * bps / 10000n;
331
+ if (assetsRaw > maxAllowed) {
332
+ const maxFormatted = Number(formatUnits(maxAllowed, decimals));
333
+ const idleFormatted = Number(formatUnits(vaultData.assetBalance, decimals));
334
+ throw new StableValidationError(
335
+ "amount",
336
+ amount,
337
+ `prepareDepositCalldata: amount ${amount} exceeds deposit buffer \u2014 max ${maxFormatted} (${depositBuffer * 100}% of ${idleFormatted} idle liquidity)`
338
+ );
304
339
  }
305
- throw err;
306
340
  }
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
- );
341
+ const depositAction = vault.deposit({
342
+ amount: assetsRaw,
343
+ userAddress: from,
344
+ accrualVault: vaultData,
345
+ slippageTolerance: slippageTolerance !== void 0 ? parseUnits(String(slippageTolerance), 18) : void 0
346
+ });
347
+ const requirements = await depositAction.getRequirements();
348
+ const steps = [];
349
+ for (const req of requirements) {
350
+ if (isRequirementApproval(req)) {
351
+ steps.push({ to: req.to, data: req.data, value: req.value });
352
+ }
314
353
  }
315
- const rewards = vault.rewards.filter((r) => typeof r.supplyApr === "number" && r.supplyApr > 0).map((r) => ({ symbol: r.asset.symbol, address: r.asset.address, apr: r.supplyApr }));
316
- return {
317
- apy: vault.netApy,
318
- native: vault.netApyExcludingRewards ?? 0,
319
- rewards,
320
- performanceFee: vault.performanceFee,
321
- managementFee: vault.managementFee
322
- };
323
- }
324
- };
325
-
326
- // src/services/lifi.ts
327
- import {
328
- createPublicClient as createPublicClient2,
329
- createWalletClient,
330
- erc20Abi as erc20Abi2,
331
- formatUnits as formatUnits2,
332
- http as http2,
333
- isAddress as isAddress2,
334
- parseUnits as parseUnits2
335
- } from "viem";
336
-
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
354
+ const tx = depositAction.buildTx(void 0);
355
+ steps.push({ to: tx.to, data: tx.data, value: tx.value });
356
+ return { steps, chainId: this.config.chainId };
406
357
  }
407
- };
408
-
409
- // src/services/lifi.ts
410
- var LiFiService = class {
411
- constructor(config) {
412
- this.config = config;
413
- }
414
- async quoteSwap(params) {
415
- this._validateSwapParams(params);
416
- const { fromToken, toToken, amount, fromDecimals = 6, toAddress } = params;
358
+ async prepareWithdrawCalldata(params) {
359
+ this._validateAmount(params.amount, "prepareWithdrawCalldata");
360
+ const { amount, tokenDecimals } = params;
417
361
  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);
431
- }
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);
436
- }
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 };
362
+ const vaultAddr = this.config.vault;
363
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
364
+ const vaultData = await vault.getData();
365
+ const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "prepareWithdrawCalldata");
366
+ const assetsRaw = parseUnits(String(amount), decimals);
367
+ const tx = vaultData.assetBalance >= assetsRaw ? vault.withdraw({ amount: assetsRaw, userAddress: from }).buildTx() : vault.forceWithdraw({
368
+ deallocations: this._buildAutoDeallocs(
369
+ vaultData.accrualAdapters,
370
+ assetsRaw - vaultData.assetBalance,
371
+ amount
372
+ ),
373
+ withdraw: { amount: assetsRaw },
374
+ userAddress: from
375
+ }).buildTx();
376
+ return { to: tx.to, data: tx.data, value: tx.value, chainId: this.config.chainId };
456
377
  }
457
- async bridge(params) {
458
- this._validateBridgeParams(params);
459
- const { fromChain, toChain, fromToken, toToken, amount, fromDecimals = 6, recipient } = params;
378
+ async prepareRedeemCalldata(params) {
379
+ this._validateShares(params.shares, "prepareRedeemCalldata");
380
+ const { shares, shareDecimals } = params;
460
381
  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}"`);
382
+ const vaultAddr = this.config.vault;
383
+ let decimals = shareDecimals;
384
+ if (decimals === void 0) {
385
+ try {
386
+ decimals = await this.pc.readContract({
387
+ address: vaultAddr,
388
+ abi: erc20Abi,
389
+ functionName: "decimals"
390
+ });
391
+ } catch {
392
+ throw new StableValidationError(
393
+ "shareDecimals",
394
+ void 0,
395
+ `prepareRedeemCalldata: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
396
+ );
397
+ }
468
398
  }
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 }
478
- });
479
- const quote = this._toSwapQuote(await this._fetchQuote(qs));
480
- return this._executeSwapQuote(quote, src.rpc, src.id);
399
+ const sharesRaw = parseUnits(String(shares), decimals);
400
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
401
+ const vaultData = await vault.getData();
402
+ const assetsEquivalent = vaultData.toAssets(sharesRaw);
403
+ const tx = vaultData.assetBalance >= assetsEquivalent ? vault.redeem({ shares: sharesRaw, userAddress: from }).buildTx() : vault.forceRedeem({
404
+ deallocations: this._buildAutoDeallocs(
405
+ vaultData.accrualAdapters,
406
+ assetsEquivalent - vaultData.assetBalance,
407
+ shares
408
+ ),
409
+ redeem: { shares: sharesRaw },
410
+ userAddress: from
411
+ }).buildTx();
412
+ return { to: tx.to, data: tx.data, value: tx.value, chainId: this.config.chainId };
481
413
  }
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");
414
+ // ─── Private operations ──────────────────────────────────────────────────
415
+ async _deposit(params) {
416
+ const {
417
+ amount,
418
+ tokenDecimals,
419
+ slippageTolerance,
420
+ idempotencyKey,
421
+ depositBuffer
422
+ } = params;
423
+ const from = this._requireFrom();
424
+ const vaultAddr = params.vault ?? this.config.vault;
425
+ await this._switchChain();
426
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
427
+ const vaultData = await vault.getData();
428
+ const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn deposit");
429
+ const assetsRaw = parseUnits(String(amount), decimals);
430
+ if (depositBuffer !== void 0) {
431
+ const bps = BigInt(Math.round(depositBuffer * 1e4));
432
+ const maxAllowed = vaultData.assetBalance * bps / 10000n;
433
+ if (assetsRaw > maxAllowed) {
434
+ const maxFormatted = Number(formatUnits(maxAllowed, decimals));
435
+ const idleFormatted = Number(
436
+ formatUnits(vaultData.assetBalance, decimals)
437
+ );
438
+ throw new StableValidationError(
439
+ "amount",
440
+ amount,
441
+ `earn deposit: amount ${amount} exceeds deposit buffer \u2014 max ${maxFormatted} (${depositBuffer * 100}% of ${idleFormatted} idle liquidity)`
442
+ );
443
+ }
491
444
  }
492
- if (!Number.isFinite(amount) || amount <= 0) {
493
- throw new StableValidationError("amount", amount, "swap: amount must be a finite positive number");
445
+ const depositAction = vault.deposit({
446
+ amount: assetsRaw,
447
+ userAddress: from,
448
+ accrualVault: vaultData,
449
+ slippageTolerance: slippageTolerance !== void 0 ? (
450
+ // Morpho SDK expects slippage as a WAD-scaled fraction (1e18 = 100%)
451
+ parseUnits(String(slippageTolerance), 18)
452
+ ) : void 0
453
+ });
454
+ const requirements = await depositAction.getRequirements();
455
+ let sig;
456
+ for (const req of requirements) {
457
+ if (isRequirementApproval(req)) {
458
+ const approveHash = await this._sendTx(
459
+ req.to,
460
+ req.data,
461
+ req.value,
462
+ from
463
+ );
464
+ const approveReceipt = await this.pc.waitForTransactionReceipt({
465
+ hash: approveHash
466
+ });
467
+ if (approveReceipt.status === "reverted") {
468
+ throw new StableTransactionError({
469
+ phase: "approve",
470
+ shortMessage: "earn deposit: token approval reverted on-chain",
471
+ txHash: approveReceipt.transactionHash,
472
+ chainId: this.config.chainId
473
+ });
474
+ }
475
+ } else if (isRequirementSignature(req)) {
476
+ sig = await req.sign(this.wc, from);
477
+ }
494
478
  }
495
- if (toAddress !== void 0 && !isAddress2(toAddress)) {
496
- throw new StableValidationError("toAddress", toAddress, `swap: invalid toAddress "${toAddress}"`);
479
+ const tx = depositAction.buildTx(sig);
480
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
481
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
482
+ if (receipt.status === "reverted") {
483
+ throw new StableTransactionError({
484
+ phase: "receipt",
485
+ shortMessage: "earn deposit: vault deposit reverted on-chain",
486
+ txHash: receipt.transactionHash,
487
+ chainId: this.config.chainId
488
+ });
497
489
  }
490
+ return { txHash: receipt.transactionHash, idempotencyKey };
498
491
  }
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");
492
+ async _withdraw(params) {
493
+ const { amount, tokenDecimals, idempotencyKey } = params;
494
+ const from = this._requireFrom();
495
+ const vaultAddr = this.config.vault;
496
+ await this._switchChain();
497
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
498
+ const vaultData = await vault.getData();
499
+ const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn withdraw");
500
+ const assetsRaw = parseUnits(String(amount), decimals);
501
+ const tx = vaultData.assetBalance >= assetsRaw ? vault.withdraw({ amount: assetsRaw, userAddress: from }).buildTx() : vault.forceWithdraw({
502
+ deallocations: this._buildAutoDeallocs(
503
+ vaultData.accrualAdapters,
504
+ assetsRaw - vaultData.assetBalance,
505
+ amount
506
+ ),
507
+ withdraw: { amount: assetsRaw },
508
+ userAddress: from
509
+ }).buildTx();
510
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
511
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
512
+ if (receipt.status === "reverted") {
513
+ throw new StableTransactionError({
514
+ phase: "receipt",
515
+ shortMessage: "earn withdraw: vault withdraw reverted on-chain",
516
+ txHash: receipt.transactionHash,
517
+ chainId: this.config.chainId
518
+ });
508
519
  }
509
- if (recipient !== void 0 && !isAddress2(recipient)) {
510
- throw new StableValidationError("recipient", recipient, `LI.FI bridge: invalid recipient address "${recipient}"`);
520
+ return { txHash: receipt.transactionHash, idempotencyKey };
521
+ }
522
+ async _redeem(params) {
523
+ const { shares, shareDecimals, idempotencyKey } = params;
524
+ const from = this._requireFrom();
525
+ const vaultAddr = this.config.vault;
526
+ await this._switchChain();
527
+ let decimals = shareDecimals;
528
+ if (decimals === void 0) {
529
+ try {
530
+ decimals = await this.pc.readContract({
531
+ address: vaultAddr,
532
+ abi: erc20Abi,
533
+ functionName: "decimals"
534
+ });
535
+ } catch {
536
+ throw new StableValidationError(
537
+ "shareDecimals",
538
+ void 0,
539
+ `earn redeem: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
540
+ );
541
+ }
511
542
  }
512
- if (fromChain === toChain) {
513
- throw new StableValidationError("toChain", toChain, "LI.FI bridge: fromChain and toChain must differ");
543
+ const sharesRaw = parseUnits(String(shares), decimals);
544
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
545
+ const vaultData = await vault.getData();
546
+ const assetsEquivalent = vaultData.toAssets(sharesRaw);
547
+ const tx = vaultData.assetBalance >= assetsEquivalent ? vault.redeem({ shares: sharesRaw, userAddress: from }).buildTx() : vault.forceRedeem({
548
+ deallocations: this._buildAutoDeallocs(
549
+ vaultData.accrualAdapters,
550
+ assetsEquivalent - vaultData.assetBalance,
551
+ shares
552
+ ),
553
+ redeem: { shares: sharesRaw },
554
+ userAddress: from
555
+ }).buildTx();
556
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
557
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
558
+ if (receipt.status === "reverted") {
559
+ throw new StableTransactionError({
560
+ phase: "receipt",
561
+ shortMessage: "earn redeem: vault redeem reverted on-chain",
562
+ txHash: receipt.transactionHash,
563
+ chainId: this.config.chainId
564
+ });
514
565
  }
566
+ return { txHash: receipt.transactionHash, idempotencyKey };
515
567
  }
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"
568
+ async _forceWithdraw(params) {
569
+ const { amount, tokenDecimals, deallocations, idempotencyKey } = params;
570
+ const from = this._requireFrom();
571
+ const vaultAddr = this.config.vault;
572
+ await this._switchChain();
573
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
574
+ let decimals = tokenDecimals;
575
+ if (decimals === void 0) {
576
+ const vaultData = await vault.getData();
577
+ decimals = await this._resolveDecimals(
578
+ vaultData.asset,
579
+ "earn forceWithdraw"
523
580
  );
524
581
  }
525
- return from;
526
- }
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
539
- });
540
- }
541
- throw new StableNetworkError({
542
- shortMessage: "LI.FI: quote request failed before reaching the server",
543
- url,
544
- cause: err
582
+ const tx = vault.forceWithdraw({
583
+ deallocations: this._buildDeallocs(deallocations, decimals),
584
+ withdraw: { amount: parseUnits(String(amount), decimals) },
585
+ userAddress: from
586
+ }).buildTx();
587
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
588
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
589
+ if (receipt.status === "reverted") {
590
+ throw new StableTransactionError({
591
+ phase: "receipt",
592
+ shortMessage: "earn forceWithdraw: vault forceWithdraw reverted on-chain",
593
+ txHash: receipt.transactionHash,
594
+ chainId: this.config.chainId
545
595
  });
546
596
  }
597
+ return { txHash: receipt.transactionHash, idempotencyKey };
547
598
  }
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);
599
+ async _forceRedeem(params) {
600
+ const {
601
+ shares,
602
+ shareDecimals,
603
+ tokenDecimals,
604
+ deallocations,
605
+ idempotencyKey
606
+ } = params;
607
+ const from = this._requireFrom();
608
+ const vaultAddr = this.config.vault;
609
+ await this._switchChain();
610
+ const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
611
+ let shareDec = shareDecimals;
612
+ if (shareDec === void 0) {
613
+ try {
614
+ shareDec = await this.pc.readContract({
615
+ address: vaultAddr,
616
+ abi: erc20Abi,
617
+ functionName: "decimals"
618
+ });
619
+ } catch {
620
+ throw new StableValidationError(
621
+ "shareDecimals",
622
+ void 0,
623
+ `earn forceRedeem: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
624
+ );
556
625
  }
557
- throw new StableQuoteError({
558
- provider: "lifi",
559
- shortMessage: `LI.FI: ${providerMessage ?? res.statusText}`,
560
- httpStatus: res.status,
561
- providerCode,
562
- body
563
- });
564
626
  }
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
627
+ let assetDec = tokenDecimals;
628
+ if (assetDec === void 0) {
629
+ const vaultData = await vault.getData();
630
+ assetDec = await this._resolveDecimals(
631
+ vaultData.asset,
632
+ "earn forceRedeem"
633
+ );
634
+ }
635
+ const tx = vault.forceRedeem({
636
+ deallocations: this._buildDeallocs(deallocations, assetDec),
637
+ redeem: { shares: parseUnits(String(shares), shareDec) },
638
+ userAddress: from
639
+ }).buildTx();
640
+ const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
641
+ const receipt = await this.pc.waitForTransactionReceipt({ hash });
642
+ if (receipt.status === "reverted") {
643
+ throw new StableTransactionError({
644
+ phase: "receipt",
645
+ shortMessage: "earn forceRedeem: vault forceRedeem reverted on-chain",
646
+ txHash: receipt.transactionHash,
647
+ chainId: this.config.chainId
571
648
  });
572
649
  }
573
- return body;
650
+ return { txHash: receipt.transactionHash, idempotencyKey };
574
651
  }
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
- };
652
+ // Automatically derives the minimum set of adapter deallocations needed to cover `deficit`
653
+ // when the vault has insufficient idle cash for a direct withdraw/redeem. Adds a 0.5%
654
+ // buffer to account for interest accrual between vaultData read and tx execution.
655
+ _buildAutoDeallocs(adapters, deficit, amount) {
656
+ const needed = deficit + deficit * 50n / 10000n;
657
+ const now = BigInt(Math.floor(Date.now() / 1e3));
658
+ const result = [];
659
+ let remaining = needed;
660
+ for (const adapter of adapters) {
661
+ if (remaining === 0n) break;
662
+ remaining = this._pullFromAdapter(adapter, remaining, now, result);
663
+ }
664
+ if (remaining > 0n) {
665
+ throw new StableValidationError(
666
+ "amount",
667
+ amount,
668
+ "earn withdraw: insufficient adapter liquidity \u2014 reduce the amount or wait for market liquidity to free up"
669
+ );
670
+ }
671
+ return result;
583
672
  }
584
- async _executeSwapQuote(quote, srcRpc, expectedChainId) {
585
- 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
594
- });
673
+ _pullFromAdapter(adapter, remaining, now, result) {
674
+ if (adapter.type !== "VaultV2MorphoMarketV1AdapterV2") {
675
+ const available = adapter.realAssets(now);
676
+ if (available === 0n) return remaining;
677
+ const pull = available < remaining ? available : remaining;
678
+ result.push({ adapter: adapter.address, amount: pull });
679
+ return remaining - pull;
595
680
  }
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
- });
681
+ const a = adapter;
682
+ return this._pullFromV2Markets(adapter.address, a.markets, a.supplyShares, remaining, result);
683
+ }
684
+ _pullFromV2Markets(adapterAddress, markets, supplyShares, remaining, result) {
685
+ let rem = remaining;
686
+ for (const mkt of markets) {
687
+ if (rem === 0n) break;
688
+ const adapterShares = supplyShares[mkt.id] ?? 0n;
689
+ const available = mkt.totalSupplyShares > 0n ? adapterShares * mkt.totalSupplyAssets / mkt.totalSupplyShares : 0n;
690
+ if (available === 0n) continue;
691
+ const pull = available < rem ? available : rem;
692
+ result.push({ adapter: adapterAddress, marketParams: mkt.params, amount: pull });
693
+ rem -= pull;
694
+ }
695
+ return rem;
696
+ }
697
+ _buildDeallocs(deallocations, assetDecimals) {
698
+ return deallocations.map((d) => ({
699
+ adapter: d.adapter,
700
+ amount: parseUnits(String(d.amount), assetDecimals),
701
+ // MarketParams in the Morpho SDK has computed fields (id, liquidationIncentiveFactor)
702
+ // not present in our user-facing type; the plain object is structurally compatible for ABI encoding.
703
+ ...d.marketParams && {
704
+ marketParams: d.marketParams
705
+ }
706
+ }));
707
+ }
708
+ // ─── Helpers ─────────────────────────────────────────────────────────────
709
+ _requireFrom() {
710
+ const from = this.wc.account?.address;
711
+ if (!from) {
712
+ throw new StableValidationError(
713
+ "signer",
714
+ void 0,
715
+ "earn: requires a signer \u2014 set walletClient or account in StableConfig"
716
+ );
717
+ }
718
+ return from;
719
+ }
720
+ async _switchChain() {
602
721
  try {
603
- await wc.switchChain({ id: srcChainId });
722
+ await this.wc.switchChain({ id: this.config.chainId });
604
723
  } 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
- });
724
+ if (err.code === 4001) {
725
+ throw new StableTransactionError({
726
+ phase: "switch_chain",
727
+ shortMessage: `earn: wallet rejected switch to chain ${this.config.chainId}`,
728
+ chainId: this.config.chainId,
729
+ cause: err
730
+ });
731
+ }
611
732
  }
612
- await this._ensureApproval(wc, srcChain, srcRpc, srcChainId, from, quote);
613
- let hash;
733
+ }
734
+ async _sendTx(to, data, value, from) {
614
735
  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
736
+ return await this.wc.sendTransaction({
737
+ to,
738
+ data,
739
+ value,
740
+ account: from,
741
+ chain: this.chain
622
742
  });
623
743
  } catch (err) {
624
744
  throw new StableTransactionError({
625
745
  phase: "send",
626
- shortMessage: "LI.FI: swap/bridge transaction failed",
627
- chainId: srcChainId,
746
+ shortMessage: "earn: transaction send failed",
747
+ chainId: this.config.chainId,
628
748
  cause: err
629
749
  });
630
750
  }
631
- return { txHash: hash, toAmount: quote.toAmount };
632
751
  }
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,
752
+ async _resolveDecimals(asset, op) {
753
+ try {
754
+ return await retryWithBackoff(
755
+ () => this.pc.readContract({
756
+ address: asset,
757
+ abi: erc20Abi,
758
+ functionName: "decimals"
759
+ })
760
+ );
761
+ } catch {
762
+ throw new StableValidationError(
763
+ "tokenDecimals",
764
+ void 0,
765
+ `${op}: could not fetch decimals for asset "${asset}" \u2014 pass tokenDecimals explicitly`
766
+ );
767
+ }
768
+ }
769
+ _withIdempotency(key, fn) {
770
+ if (!key) return fn();
771
+ const existing = this.inFlight.get(key);
772
+ if (existing) return existing;
773
+ const promise = fn().finally(() => this.inFlight.delete(key));
774
+ this.inFlight.set(key, promise);
775
+ return promise;
776
+ }
777
+ _validateAmount(amount, op) {
778
+ if (!Number.isFinite(amount) || amount <= 0) {
779
+ throw new StableValidationError(
780
+ "amount",
781
+ amount,
782
+ `${op}: amount must be a finite positive number`
783
+ );
784
+ }
785
+ }
786
+ _validateShares(shares, op) {
787
+ if (!Number.isFinite(shares) || shares <= 0) {
788
+ throw new StableValidationError(
789
+ "shares",
790
+ shares,
791
+ `${op}: shares must be a finite positive number`
792
+ );
793
+ }
794
+ }
795
+ };
796
+
797
+ // src/earn/reader.ts
798
+ import { MorphoClient as MorphoClient2 } from "@morpho-org/morpho-sdk";
799
+ import {
800
+ http as http2,
801
+ createPublicClient as createPublicClient2,
802
+ erc20Abi as erc20Abi2,
803
+ formatUnits as formatUnits2,
804
+ isAddress,
805
+ parseUnits as parseUnits2
806
+ } from "viem";
807
+ import { stable as stableMainnet, stableTestnet } from "viem/chains";
808
+
809
+ // src/types.ts
810
+ var Chain = /* @__PURE__ */ ((Chain2) => {
811
+ Chain2["Sepolia"] = "sepolia";
812
+ Chain2["StableTestnet"] = "stable-testnet";
813
+ Chain2["Stable"] = "stable";
814
+ Chain2["Ethereum"] = "ethereum";
815
+ Chain2["Arbitrum"] = "arbitrum";
816
+ Chain2["Ink"] = "ink";
817
+ Chain2["Bera"] = "bera";
818
+ Chain2["MegaETH"] = "megaeth";
819
+ Chain2["Base"] = "base";
820
+ Chain2["BSC"] = "bsc";
821
+ Chain2["HyperEVM"] = "hyperevm";
822
+ return Chain2;
823
+ })(Chain || {});
824
+ var Network = /* @__PURE__ */ ((Network2) => {
825
+ Network2["Mainnet"] = "mainnet";
826
+ Network2["Testnet"] = "testnet";
827
+ return Network2;
828
+ })(Network || {});
829
+
830
+ // src/earn/reader.ts
831
+ var MORPHO_API_URL = "https://blue-api.morpho.org/graphql";
832
+ var EarnReader = class {
833
+ constructor(config) {
834
+ if (!config.earn?.vault) {
835
+ throw new StableValidationError(
836
+ "earn.vault",
837
+ void 0,
838
+ "createStableReader: earn.vault is required"
839
+ );
840
+ }
841
+ if (!isAddress(config.address)) {
842
+ throw new StableValidationError(
843
+ "address",
844
+ config.address,
845
+ `createStableReader: invalid user address "${config.address}"`
846
+ );
847
+ }
848
+ if (!isAddress(config.earn.vault)) {
849
+ throw new StableValidationError(
850
+ "earn.vault",
851
+ config.earn.vault,
852
+ `createStableReader: invalid vault address "${config.earn.vault}"`
853
+ );
854
+ }
855
+ const network = config.network ?? "mainnet" /* Mainnet */;
856
+ const viemChain = network === "mainnet" /* Mainnet */ ? stableMainnet : stableTestnet;
857
+ const rpc = (config.rpc ?? viemChain.rpcUrls.default.http[0]).replace(
858
+ /\/$/,
859
+ ""
860
+ );
861
+ this.chainId = viemChain.id;
862
+ this.vaultAddr = config.earn.vault;
863
+ this.userAddr = config.address;
864
+ const chain = makeChain(viemChain.id, rpc);
865
+ this.pc = createPublicClient2({ chain, transport: http2(rpc) });
866
+ this.morpho = new MorphoClient2(this.pc);
867
+ }
868
+ async position() {
869
+ const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
870
+ const [vaultData, shares] = await Promise.all([
871
+ vault.getData(),
872
+ this.pc.readContract({
873
+ address: this.vaultAddr,
874
+ abi: erc20Abi2,
875
+ functionName: "balanceOf",
876
+ args: [this.userAddr]
877
+ })
878
+ ]);
879
+ const [tokenDecimals, shareDecimals] = await Promise.all([
880
+ this.pc.readContract({
881
+ address: vaultData.asset,
882
+ abi: erc20Abi2,
883
+ functionName: "decimals"
884
+ }),
885
+ this.pc.readContract({
886
+ address: this.vaultAddr,
887
+ abi: erc20Abi2,
888
+ functionName: "decimals"
889
+ })
890
+ ]);
891
+ const assetsRaw = vaultData.toAssets(shares);
892
+ return {
893
+ shares,
894
+ sharesFormatted: Number(formatUnits2(shares, shareDecimals)),
895
+ shareDecimals,
896
+ assets: Number(formatUnits2(assetsRaw, tokenDecimals)),
897
+ tokenDecimals,
898
+ assetAddress: vaultData.asset
899
+ };
900
+ }
901
+ async getYield() {
902
+ return this._fetchYield();
903
+ }
904
+ async preview(params) {
905
+ const { amount, horizonDays } = params;
906
+ const apy = await this._fetchNetApy();
907
+ return {
908
+ projectedYield: amount * ((1 + apy) ** (horizonDays / 365) - 1),
909
+ apy,
910
+ horizonDays
911
+ };
912
+ }
913
+ async withdrawability(params) {
914
+ const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
915
+ const vaultData = await vault.getData();
916
+ const tokenDecimals = params.tokenDecimals ?? await this.pc.readContract({
917
+ address: vaultData.asset,
640
918
  abi: erc20Abi2,
641
- functionName: "allowance",
642
- args: [from, quote.approvalAddress]
919
+ functionName: "decimals"
643
920
  });
644
- if (allowance >= quote.fromAmount) return;
645
- let approveHash;
921
+ const requestedRaw = parseUnits2(String(params.amount), tokenDecimals);
922
+ let availableLiquidityRaw = vaultData.assetBalance;
923
+ if (vaultData.accrualLiquidityAdapter != null) {
924
+ availableLiquidityRaw += vaultData.accrualLiquidityAdapter.maxWithdraw(
925
+ vaultData.liquidityData
926
+ ).value;
927
+ }
928
+ return {
929
+ isInstant: requestedRaw <= availableLiquidityRaw,
930
+ availableLiquidity: Number(
931
+ formatUnits2(availableLiquidityRaw, tokenDecimals)
932
+ ),
933
+ tokenDecimals
934
+ };
935
+ }
936
+ async _fetchNetApy() {
937
+ const { apy } = await this._fetchYield();
938
+ return apy;
939
+ }
940
+ async _fetchYield() {
941
+ const query = `
942
+ query VaultYield($address: String!, $chainId: Int!) {
943
+ vaultV2ByAddress(address: $address, chainId: $chainId) {
944
+ avgNetApy
945
+ avgNetApyExcludingRewards
946
+ performanceFee
947
+ managementFee
948
+ rewards {
949
+ asset { symbol address }
950
+ supplyApr
951
+ }
952
+ }
953
+ }
954
+ `;
955
+ let body;
646
956
  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
- });
957
+ body = await retryWithBackoff(
958
+ async () => {
959
+ const res = await fetch(MORPHO_API_URL, {
960
+ method: "POST",
961
+ headers: { "Content-Type": "application/json" },
962
+ body: JSON.stringify({
963
+ query,
964
+ variables: {
965
+ address: this.vaultAddr,
966
+ chainId: this.chainId
967
+ }
968
+ })
969
+ });
970
+ if (!res.ok) {
971
+ const retryable = res.status === 429 || res.status >= 500;
972
+ throw new HttpError(
973
+ `earn: Morpho API returned ${res.status} \u2014 ${res.statusText}`,
974
+ res.status,
975
+ retryable
976
+ );
977
+ }
978
+ return res.json().catch(() => void 0);
979
+ },
980
+ (err) => err instanceof HttpError ? err.retryable : err instanceof TypeError
981
+ );
655
982
  } catch (err) {
656
- throw new StableTransactionError({
657
- phase: "approve",
658
- shortMessage: "LI.FI: token approve failed",
659
- chainId: srcChainId,
660
- cause: err
661
- });
983
+ if (err instanceof TypeError || err instanceof HttpError) {
984
+ throw new StableNetworkError({
985
+ shortMessage: err instanceof HttpError ? `earn: Morpho API returned ${err.status} after retries` : `earn: failed to reach Morpho API after retries \u2014 ${String(
986
+ err
987
+ )}`,
988
+ url: MORPHO_API_URL,
989
+ cause: err
990
+ });
991
+ }
992
+ throw err;
662
993
  }
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
- });
994
+ const vault = body?.data?.vaultV2ByAddress;
995
+ if (!vault || typeof vault.avgNetApy !== "number") {
996
+ throw new StableValidationError(
997
+ "apy",
998
+ void 0,
999
+ "earn: vault not found in Morpho API or unexpected response shape"
1000
+ );
671
1001
  }
1002
+ const rewards = vault.rewards.filter((r) => typeof r.supplyApr === "number" && r.supplyApr > 0).map((r) => ({
1003
+ symbol: r.asset.symbol,
1004
+ address: r.asset.address,
1005
+ apr: r.supplyApr
1006
+ }));
1007
+ return {
1008
+ apy: vault.avgNetApy,
1009
+ native: vault.avgNetApyExcludingRewards ?? 0,
1010
+ rewards,
1011
+ performanceFee: vault.performanceFee,
1012
+ managementFee: vault.managementFee
1013
+ };
672
1014
  }
673
1015
  };
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
1016
 
685
- // src/earn/morpho.ts
1017
+ // src/earn/merkl.ts
686
1018
  import {
1019
+ http as http3,
687
1020
  createPublicClient as createPublicClient3,
688
1021
  createWalletClient as createWalletClient2,
689
- custom,
690
- erc20Abi as erc20Abi3,
1022
+ encodeFunctionData,
691
1023
  formatUnits as formatUnits3,
692
- http as http3,
693
- parseUnits as parseUnits3
1024
+ isAddress as isAddress2
694
1025
  } from "viem";
695
- import {
696
- MorphoClient as MorphoClient2,
697
- isRequirementApproval,
698
- isRequirementSignature
699
- } from "@morpho-org/morpho-sdk";
700
- var MorphoService = class {
1026
+ var MERKL_API_BASE_DEFAULT = "https://api.merkl.xyz";
1027
+ var DISTRIBUTOR_ADDRESS = "0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae";
1028
+ var DISTRIBUTOR_ABI = [
1029
+ {
1030
+ inputs: [
1031
+ { name: "users", type: "address[]" },
1032
+ { name: "tokens", type: "address[]" },
1033
+ { name: "amounts", type: "uint256[]" },
1034
+ { name: "proofs", type: "bytes32[][]" }
1035
+ ],
1036
+ name: "claim",
1037
+ outputs: [],
1038
+ stateMutability: "nonpayable",
1039
+ type: "function"
1040
+ }
1041
+ ];
1042
+ var MerklService = class {
701
1043
  constructor(config) {
702
1044
  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
1045
  }
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");
1046
+ async fetchRewards(address) {
1047
+ if (!isAddress2(address)) {
1048
+ throw new StableValidationError(
1049
+ "address",
1050
+ address,
1051
+ `merkl: invalid address "${address}"`
1052
+ );
722
1053
  }
723
- return this._withIdempotency(params.idempotencyKey, () => this._deposit(params));
724
- }
725
- async bulkDeposit(items, options = {}) {
726
- const { idempotencyKey, stopOnError = false } = options;
727
- const run = async () => {
728
- const results = [];
729
- for (const item of items) {
730
- try {
731
- const result = await this.deposit(item);
732
- results.push({ status: "fulfilled", txHash: result.txHash, vault: item.vault, idempotencyKey: item.idempotencyKey });
733
- } catch (err) {
734
- const entry = { status: "rejected", error: String(err), vault: item.vault, idempotencyKey: item.idempotencyKey };
735
- results.push(entry);
736
- if (stopOnError) {
737
- const succeeded2 = results.filter((r) => r.status === "fulfilled").length;
738
- throw new StableBulkError({
739
- shortMessage: "bulkDeposit halted on first failure",
740
- results,
741
- succeeded: succeeded2,
742
- failed: results.length - succeeded2
743
- });
744
- }
745
- }
746
- }
747
- const succeeded = results.filter((r) => r.status === "fulfilled").length;
748
- return { results, succeeded, failed: results.length - succeeded, idempotencyKey };
749
- };
750
- return this._withIdempotency(idempotencyKey, run);
751
- }
752
- async bulkWithdraw(items, options = {}) {
753
- const { idempotencyKey, stopOnError = false } = options;
754
- const run = async () => {
755
- const results = [];
756
- for (const item of items) {
757
- try {
758
- const result = await this.withdraw(item);
759
- results.push({ status: "fulfilled", txHash: result.txHash, idempotencyKey: item.idempotencyKey });
760
- } catch (err) {
761
- const entry = { status: "rejected", error: String(err), idempotencyKey: item.idempotencyKey };
762
- results.push(entry);
763
- if (stopOnError) {
764
- const succeeded2 = results.filter((r) => r.status === "fulfilled").length;
765
- throw new StableBulkError({
766
- shortMessage: "bulkWithdraw halted on first failure",
767
- results,
768
- succeeded: succeeded2,
769
- failed: results.length - succeeded2
770
- });
771
- }
772
- }
773
- }
774
- const succeeded = results.filter((r) => r.status === "fulfilled").length;
775
- return { results, succeeded, failed: results.length - succeeded, idempotencyKey };
776
- };
777
- return this._withIdempotency(idempotencyKey, run);
778
- }
779
- withdraw(params) {
780
- this._validateAmount(params.amount, "earn withdraw");
781
- return this._withIdempotency(params.idempotencyKey, () => this._withdraw(params));
782
- }
783
- redeem(params) {
784
- this._validateShares(params.shares, "earn redeem");
785
- return this._withIdempotency(params.idempotencyKey, () => this._redeem(params));
786
- }
787
- forceWithdraw(params) {
788
- this._validateAmount(params.amount, "earn forceWithdraw");
789
- return this._withIdempotency(params.idempotencyKey, () => this._forceWithdraw(params));
790
- }
791
- forceRedeem(params) {
792
- this._validateShares(params.shares, "earn forceRedeem");
793
- return this._withIdempotency(params.idempotencyKey, () => this._forceRedeem(params));
1054
+ const base = (this.config.merklApiBase ?? MERKL_API_BASE_DEFAULT).replace(/\/$/, "");
1055
+ const url = `${base}/v4/users/${address}/rewards?chainId=${this.config.chainId}`;
1056
+ const data = await this._fetch(url);
1057
+ return this._parseRewards(data);
794
1058
  }
795
- // ─── Private operations ──────────────────────────────────────────────────
796
- async _deposit(params) {
797
- const { amount, tokenDecimals, slippageTolerance, idempotencyKey, depositBuffer } = params;
798
- const from = this._requireFrom();
799
- const vaultAddr = params.vault ?? this.config.vault;
800
- await this._switchChain();
801
- const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
802
- const vaultData = await vault.getData();
803
- const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn deposit");
804
- const assetsRaw = parseUnits3(String(amount), decimals);
805
- if (depositBuffer !== void 0) {
806
- const bps = BigInt(Math.round(depositBuffer * 1e4));
807
- const maxAllowed = vaultData.assetBalance * bps / 10000n;
808
- if (assetsRaw > maxAllowed) {
809
- const maxFormatted = Number(formatUnits3(maxAllowed, decimals));
810
- const idleFormatted = Number(formatUnits3(vaultData.assetBalance, decimals));
811
- throw new StableValidationError(
812
- "amount",
813
- amount,
814
- `earn deposit: amount ${amount} exceeds deposit buffer \u2014 max ${maxFormatted} (${depositBuffer * 100}% of ${idleFormatted} idle liquidity)`
815
- );
816
- }
1059
+ async claim() {
1060
+ const address = this._requireFrom();
1061
+ const { rewards } = await this.fetchRewards(address);
1062
+ const claimable = rewards.filter(
1063
+ (r) => r.rawAmount !== "0" && r.proofs.length > 0
1064
+ );
1065
+ if (claimable.length === 0) {
1066
+ throw new StableValidationError(
1067
+ "rewards",
1068
+ [],
1069
+ rewards.length > 0 ? "merkl: rewards are pending the next merkle tree update \u2014 try again in ~2 hours" : "merkl: no rewards to claim on this chain"
1070
+ );
817
1071
  }
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
1072
+ const chain = makeChain(this.config.chainId, this.config.rpc);
1073
+ const wc = this.config.walletClient ?? createWalletClient2({
1074
+ account: this.config.account,
1075
+ chain,
1076
+ transport: this.config.transport ?? http3(this.config.rpc)
823
1077
  });
824
- const requirements = await depositAction.getRequirements();
825
- let sig;
826
- for (const req of requirements) {
827
- if (isRequirementApproval(req)) {
828
- const approveHash = await this._sendTx(req.to, req.data, req.value, from);
829
- const approveReceipt = await this.pc.waitForTransactionReceipt({ hash: approveHash });
830
- if (approveReceipt.status === "reverted") {
831
- throw new StableTransactionError({
832
- phase: "approve",
833
- shortMessage: "earn deposit: token approval reverted on-chain",
834
- txHash: approveReceipt.transactionHash,
835
- chainId: this.config.chainId
836
- });
837
- }
838
- } else if (isRequirementSignature(req)) {
839
- sig = await req.sign(this.wc, from);
840
- }
841
- }
842
- const tx = depositAction.buildTx(sig);
843
- const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
844
- const receipt = await this.pc.waitForTransactionReceipt({ hash });
845
- if (receipt.status === "reverted") {
1078
+ try {
1079
+ await wc.switchChain({ id: this.config.chainId });
1080
+ } catch (err) {
846
1081
  throw new StableTransactionError({
847
- phase: "receipt",
848
- shortMessage: "earn deposit: vault deposit reverted on-chain",
849
- txHash: receipt.transactionHash,
850
- chainId: this.config.chainId
1082
+ phase: "switch_chain",
1083
+ shortMessage: `merkl: wallet rejected or failed to switch to chain ${this.config.chainId}`,
1084
+ chainId: this.config.chainId,
1085
+ cause: err
851
1086
  });
852
1087
  }
853
- return { txHash: receipt.transactionHash, idempotencyKey };
854
- }
855
- async _withdraw(params) {
856
- const { amount, tokenDecimals, idempotencyKey } = params;
857
- const from = this._requireFrom();
858
- const vaultAddr = this.config.vault;
859
- await this._switchChain();
860
- const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
861
- const vaultData = await vault.getData();
862
- const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn withdraw");
863
- const assetsRaw = parseUnits3(String(amount), decimals);
864
- const tx = vault.withdraw({ amount: assetsRaw, userAddress: from }).buildTx();
865
- const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
866
- const receipt = await this.pc.waitForTransactionReceipt({ hash });
867
- if (receipt.status === "reverted") {
868
- throw new StableTransactionError({
869
- phase: "receipt",
870
- shortMessage: "earn withdraw: vault withdraw reverted on-chain",
871
- txHash: receipt.transactionHash,
872
- chainId: this.config.chainId
1088
+ const users = claimable.map(() => address);
1089
+ const tokens = claimable.map(
1090
+ (r) => r.token.address
1091
+ );
1092
+ const amounts = claimable.map((r) => BigInt(r.rawAmount));
1093
+ const proofs = claimable.map((r) => r.proofs);
1094
+ let hash;
1095
+ try {
1096
+ hash = await wc.writeContract({
1097
+ address: DISTRIBUTOR_ADDRESS,
1098
+ abi: DISTRIBUTOR_ABI,
1099
+ functionName: "claim",
1100
+ args: [users, tokens, amounts, proofs],
1101
+ account: wc.account,
1102
+ chain
1103
+ });
1104
+ } catch (err) {
1105
+ throw new StableTransactionError({
1106
+ phase: "send",
1107
+ shortMessage: "merkl: claim transaction failed",
1108
+ chainId: this.config.chainId,
1109
+ cause: err
873
1110
  });
874
1111
  }
875
- return { txHash: receipt.transactionHash, idempotencyKey };
876
- }
877
- async _redeem(params) {
878
- const { shares, shareDecimals, idempotencyKey } = params;
879
- const from = this._requireFrom();
880
- const vaultAddr = this.config.vault;
881
- await this._switchChain();
882
- let decimals = shareDecimals;
883
- if (decimals === void 0) {
884
- try {
885
- decimals = await this.pc.readContract({ address: vaultAddr, abi: erc20Abi3, functionName: "decimals" });
886
- } catch {
887
- throw new StableValidationError(
888
- "shareDecimals",
889
- void 0,
890
- `earn redeem: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
891
- );
892
- }
893
- }
894
- const sharesRaw = parseUnits3(String(shares), decimals);
895
- const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
896
- const tx = vault.redeem({ shares: sharesRaw, userAddress: from }).buildTx();
897
- const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
898
- const receipt = await this.pc.waitForTransactionReceipt({ hash });
1112
+ const pc = createPublicClient3({
1113
+ chain,
1114
+ transport: http3(this.config.rpc)
1115
+ });
1116
+ const receipt = await pc.waitForTransactionReceipt({ hash });
899
1117
  if (receipt.status === "reverted") {
900
1118
  throw new StableTransactionError({
901
1119
  phase: "receipt",
902
- shortMessage: "earn redeem: vault redeem reverted on-chain",
1120
+ shortMessage: "merkl: claim transaction reverted on-chain",
903
1121
  txHash: receipt.transactionHash,
904
1122
  chainId: this.config.chainId
905
1123
  });
906
1124
  }
907
- return { txHash: receipt.transactionHash, idempotencyKey };
1125
+ return {
1126
+ txHash: receipt.transactionHash,
1127
+ tokenCount: claimable.length
1128
+ };
908
1129
  }
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");
1130
+ async prepareClaimCalldata(address) {
1131
+ if (!isAddress2(address)) {
1132
+ throw new StableValidationError(
1133
+ "address",
1134
+ address,
1135
+ `merkl: invalid address "${address}"`
1136
+ );
919
1137
  }
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
1138
+ const { rewards } = await this.fetchRewards(address);
1139
+ const claimable = rewards.filter(
1140
+ (r) => r.rawAmount !== "0" && r.proofs.length > 0
1141
+ );
1142
+ if (claimable.length === 0) {
1143
+ throw new StableValidationError(
1144
+ "rewards",
1145
+ [],
1146
+ rewards.length > 0 ? "merkl: rewards are pending the next merkle tree update \u2014 try again in ~2 hours" : "merkl: no rewards to claim on this chain"
1147
+ );
1148
+ }
1149
+ const users = claimable.map(() => address);
1150
+ const tokens = claimable.map((r) => r.token.address);
1151
+ const amounts = claimable.map((r) => BigInt(r.rawAmount));
1152
+ const proofs = claimable.map((r) => r.proofs);
1153
+ const data = encodeFunctionData({
1154
+ abi: DISTRIBUTOR_ABI,
1155
+ functionName: "claim",
1156
+ args: [users, tokens, amounts, proofs]
1157
+ });
1158
+ return {
1159
+ to: DISTRIBUTOR_ADDRESS,
1160
+ data,
1161
+ chainId: this.config.chainId,
1162
+ rewards: claimable
1163
+ };
1164
+ }
1165
+ _requireFrom() {
1166
+ const from = this.config.walletClient?.account?.address ?? this.config.account?.address;
1167
+ if (!from) {
1168
+ throw new StableValidationError(
1169
+ "signer",
1170
+ void 0,
1171
+ "merkl.claim: requires a signer \u2014 set walletClient or account in StableConfig"
1172
+ );
1173
+ }
1174
+ return from;
1175
+ }
1176
+ _parseRewards(data) {
1177
+ const { chainId } = this.config;
1178
+ const rewards = [];
1179
+ const entries = Array.isArray(data) ? data : [data];
1180
+ for (const entry of entries) {
1181
+ if (entry?.chain?.id !== chainId) continue;
1182
+ for (const reward of entry.rewards ?? []) {
1183
+ const parsed = this._parseReward(reward);
1184
+ if (parsed) rewards.push(parsed);
1185
+ }
1186
+ }
1187
+ return { chainId, rewards };
1188
+ }
1189
+ _parseReward(reward) {
1190
+ if (!reward?.token?.address) return null;
1191
+ const net = BigInt(reward.amount ?? "0") - BigInt(reward.claimed ?? "0");
1192
+ if (net <= 0n) return null;
1193
+ const rawAmount = net.toString();
1194
+ const decimals = reward.token.decimals ?? 18;
1195
+ return {
1196
+ token: {
1197
+ address: reward.token.address,
1198
+ symbol: reward.token.symbol ?? "",
1199
+ decimals
1200
+ },
1201
+ rawAmount,
1202
+ amount: Number(formatUnits3(BigInt(rawAmount), decimals)),
1203
+ proofs: reward.proofs ?? []
1204
+ };
1205
+ }
1206
+ async _fetch(url) {
1207
+ try {
1208
+ return await retryWithBackoff(() => this._fetchOnce(url));
1209
+ } catch (err) {
1210
+ if (err instanceof StableNetworkError) throw err;
1211
+ if (err instanceof HttpError) {
1212
+ throw new StableNetworkError({
1213
+ shortMessage: `merkl: request failed \u2014 ${err.message}`,
1214
+ url,
1215
+ cause: err
1216
+ });
1217
+ }
1218
+ throw new StableNetworkError({
1219
+ shortMessage: "merkl: request failed before reaching the server",
1220
+ url,
1221
+ cause: err
933
1222
  });
934
1223
  }
935
- return { txHash: receipt.transactionHash, idempotencyKey };
936
1224
  }
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`
1225
+ async _fetchOnce(url) {
1226
+ const res = await fetch(url);
1227
+ if (!res.ok) {
1228
+ if (res.status === 429 || res.status >= 500) {
1229
+ throw new HttpError(
1230
+ `merkl: ${res.statusText}`,
1231
+ res.status,
1232
+ true
952
1233
  );
953
1234
  }
1235
+ throw new StableNetworkError({
1236
+ shortMessage: `merkl: API error ${res.status} ${res.statusText}`,
1237
+ url
1238
+ });
1239
+ }
1240
+ return res.json();
1241
+ }
1242
+ };
1243
+
1244
+ // src/services/lifi.ts
1245
+ import {
1246
+ http as http4,
1247
+ createPublicClient as createPublicClient4,
1248
+ createWalletClient as createWalletClient3,
1249
+ erc20Abi as erc20Abi3,
1250
+ formatUnits as formatUnits4,
1251
+ isAddress as isAddress3,
1252
+ parseUnits as parseUnits3
1253
+ } from "viem";
1254
+
1255
+ // src/chains.ts
1256
+ var CHAIN_CONFIGS = {
1257
+ // ─── Testnet ──────────────────────────────────────────────────────────────
1258
+ ["sepolia" /* Sepolia */]: {
1259
+ id: 11155111,
1260
+ rpc: "https://ethereum-sepolia-rpc.publicnode.com",
1261
+ usdt: "0xc4DCC311c028e341fd8602D8eB89c5de94625927",
1262
+ decimals: 18
1263
+ },
1264
+ ["stable-testnet" /* StableTestnet */]: {
1265
+ id: 2201,
1266
+ rpc: "https://rpc.testnet.stable.xyz",
1267
+ usdt: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9",
1268
+ decimals: 6
1269
+ },
1270
+ // ─── Mainnet ──────────────────────────────────────────────────────────────
1271
+ ["stable" /* Stable */]: {
1272
+ id: 988,
1273
+ rpc: "https://rpc.stable.xyz",
1274
+ usdt: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
1275
+ decimals: 6
1276
+ },
1277
+ ["ethereum" /* Ethereum */]: {
1278
+ id: 1,
1279
+ rpc: "https://ethereum-rpc.publicnode.com",
1280
+ usdt: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee",
1281
+ decimals: 6
1282
+ },
1283
+ ["arbitrum" /* Arbitrum */]: {
1284
+ id: 42161,
1285
+ rpc: "https://arbitrum-one-rpc.publicnode.com",
1286
+ usdt: "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92",
1287
+ decimals: 6
1288
+ },
1289
+ ["ink" /* Ink */]: {
1290
+ id: 57073,
1291
+ rpc: "https://rpc-gel.inkonchain.com",
1292
+ usdt: "0x0200C29006150606B650577BBE7B6248F58470c1",
1293
+ decimals: 6
1294
+ },
1295
+ ["bera" /* Bera */]: {
1296
+ id: 80094,
1297
+ rpc: "https://rpc.berachain.com",
1298
+ usdt: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
1299
+ decimals: 6
1300
+ },
1301
+ ["megaeth" /* MegaETH */]: {
1302
+ id: 4326,
1303
+ rpc: "https://carrot.megaeth.com",
1304
+ usdt: "0xb8ce59fc3717ada4c02eadf9682a9e934f625ebb",
1305
+ decimals: 6
1306
+ },
1307
+ ["base" /* Base */]: {
1308
+ id: 8453,
1309
+ rpc: "https://mainnet.base.org",
1310
+ usdt: "0x102d758f688a4C1C5a80b116bD945d4455460282",
1311
+ decimals: 6
1312
+ },
1313
+ ["bsc" /* BSC */]: {
1314
+ id: 56,
1315
+ rpc: "https://bsc-rpc.publicnode.com",
1316
+ usdt: "0x55d398326f99059fF775485246999027B3197955",
1317
+ decimals: 18
1318
+ },
1319
+ ["hyperevm" /* HyperEVM */]: {
1320
+ id: 999,
1321
+ rpc: "https://rpc.hyperliquid.xyz/evm",
1322
+ usdt: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb",
1323
+ decimals: 6
1324
+ }
1325
+ };
1326
+
1327
+ // src/services/lifi.ts
1328
+ var LiFiService = class {
1329
+ constructor(config) {
1330
+ this.config = config;
1331
+ }
1332
+ async quoteSwap(params) {
1333
+ this._validateSwapParams(params);
1334
+ const {
1335
+ fromToken,
1336
+ toToken,
1337
+ amount,
1338
+ fromDecimals = 6,
1339
+ toAddress
1340
+ } = params;
1341
+ const from = this._requireFrom();
1342
+ const chain = this.config.chainId.toString();
1343
+ const fromAmountRaw = parseUnits3(
1344
+ String(amount),
1345
+ fromDecimals
1346
+ ).toString();
1347
+ const qs = new URLSearchParams({
1348
+ fromChain: chain,
1349
+ toChain: chain,
1350
+ fromToken,
1351
+ toToken,
1352
+ fromAmount: fromAmountRaw,
1353
+ fromAddress: from,
1354
+ ...toAddress && { toAddress }
1355
+ });
1356
+ const quote = await this._fetchQuote(qs);
1357
+ return this._toSwapQuote(quote);
1358
+ }
1359
+ async swap(params) {
1360
+ this._validateSwapParams(params);
1361
+ const quote = params.quote ?? await this.quoteSwap(params);
1362
+ return this._executeSwapQuote(
1363
+ quote,
1364
+ this.config.rpc,
1365
+ this.config.chainId
1366
+ );
1367
+ }
1368
+ async quoteBridge(params) {
1369
+ this._validateBridgeParams(params);
1370
+ const {
1371
+ fromChain,
1372
+ toChain,
1373
+ fromToken,
1374
+ toToken,
1375
+ amount,
1376
+ fromDecimals = 6,
1377
+ recipient
1378
+ } = params;
1379
+ const from = this.config.walletClient?.account?.address ?? this.config.account?.address ?? "0x0000000000000000000000000000000000000000";
1380
+ const src = CHAIN_CONFIGS[fromChain];
1381
+ const dst = CHAIN_CONFIGS[toChain];
1382
+ if (!src)
1383
+ throw new StableValidationError(
1384
+ "fromChain",
1385
+ fromChain,
1386
+ `LI.FI bridge: unsupported source chain "${fromChain}"`
1387
+ );
1388
+ if (!dst)
1389
+ throw new StableValidationError(
1390
+ "toChain",
1391
+ toChain,
1392
+ `LI.FI bridge: unsupported destination chain "${toChain}"`
1393
+ );
1394
+ const qs = new URLSearchParams({
1395
+ fromChain: src.id.toString(),
1396
+ toChain: dst.id.toString(),
1397
+ fromToken,
1398
+ toToken,
1399
+ fromAmount: parseUnits3(String(amount), fromDecimals).toString(),
1400
+ fromAddress: from,
1401
+ ...recipient && { toAddress: recipient }
1402
+ });
1403
+ const quote = this._toSwapQuote(await this._fetchQuote(qs));
1404
+ return { toAmount: quote.toAmount };
1405
+ }
1406
+ async bridge(params) {
1407
+ this._validateBridgeParams(params);
1408
+ const {
1409
+ fromChain,
1410
+ toChain,
1411
+ fromToken,
1412
+ toToken,
1413
+ amount,
1414
+ fromDecimals = 6,
1415
+ recipient
1416
+ } = params;
1417
+ const from = this._requireFrom();
1418
+ const src = CHAIN_CONFIGS[fromChain];
1419
+ const dst = CHAIN_CONFIGS[toChain];
1420
+ if (!src) {
1421
+ throw new StableValidationError(
1422
+ "fromChain",
1423
+ fromChain,
1424
+ `LI.FI bridge: unsupported source chain "${fromChain}"`
1425
+ );
1426
+ }
1427
+ if (!dst) {
1428
+ throw new StableValidationError(
1429
+ "toChain",
1430
+ toChain,
1431
+ `LI.FI bridge: unsupported destination chain "${toChain}"`
1432
+ );
1433
+ }
1434
+ const fromAmountRaw = parseUnits3(
1435
+ String(amount),
1436
+ fromDecimals
1437
+ ).toString();
1438
+ const qs = new URLSearchParams({
1439
+ fromChain: src.id.toString(),
1440
+ toChain: dst.id.toString(),
1441
+ fromToken,
1442
+ toToken,
1443
+ fromAmount: fromAmountRaw,
1444
+ fromAddress: from,
1445
+ ...recipient && { toAddress: recipient }
1446
+ });
1447
+ const quote = this._toSwapQuote(await this._fetchQuote(qs));
1448
+ return this._executeSwapQuote(quote, src.rpc, src.id);
1449
+ }
1450
+ _validateSwapParams({
1451
+ fromToken,
1452
+ toToken,
1453
+ amount,
1454
+ toAddress
1455
+ }) {
1456
+ if (!isAddress3(fromToken)) {
1457
+ throw new StableValidationError(
1458
+ "fromToken",
1459
+ fromToken,
1460
+ `swap: invalid fromToken address "${fromToken}"`
1461
+ );
954
1462
  }
955
- let assetDec = tokenDecimals;
956
- if (assetDec === void 0) {
957
- const vaultData = await vault.getData();
958
- assetDec = await this._resolveDecimals(vaultData.asset, "earn forceRedeem");
1463
+ if (!isAddress3(toToken)) {
1464
+ throw new StableValidationError(
1465
+ "toToken",
1466
+ toToken,
1467
+ `swap: invalid toToken address "${toToken}"`
1468
+ );
959
1469
  }
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
- });
1470
+ if (fromToken.toLowerCase() === toToken.toLowerCase()) {
1471
+ throw new StableValidationError(
1472
+ "toToken",
1473
+ toToken,
1474
+ "swap: fromToken and toToken must differ"
1475
+ );
1476
+ }
1477
+ if (!Number.isFinite(amount) || amount <= 0) {
1478
+ throw new StableValidationError(
1479
+ "amount",
1480
+ amount,
1481
+ "swap: amount must be a finite positive number"
1482
+ );
1483
+ }
1484
+ if (toAddress !== void 0 && !isAddress3(toAddress)) {
1485
+ throw new StableValidationError(
1486
+ "toAddress",
1487
+ toAddress,
1488
+ `swap: invalid toAddress "${toAddress}"`
1489
+ );
974
1490
  }
975
- return { txHash: receipt.transactionHash, idempotencyKey };
976
1491
  }
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
- }));
1492
+ _validateBridgeParams({
1493
+ fromChain,
1494
+ toChain,
1495
+ fromToken,
1496
+ toToken,
1497
+ amount,
1498
+ recipient
1499
+ }) {
1500
+ if (!isAddress3(fromToken)) {
1501
+ throw new StableValidationError(
1502
+ "fromToken",
1503
+ fromToken,
1504
+ `LI.FI bridge: invalid fromToken address "${fromToken}"`
1505
+ );
1506
+ }
1507
+ if (!isAddress3(toToken)) {
1508
+ throw new StableValidationError(
1509
+ "toToken",
1510
+ toToken,
1511
+ `LI.FI bridge: invalid toToken address "${toToken}"`
1512
+ );
1513
+ }
1514
+ if (!Number.isFinite(amount) || amount <= 0) {
1515
+ throw new StableValidationError(
1516
+ "amount",
1517
+ amount,
1518
+ "LI.FI bridge: amount must be a finite positive number"
1519
+ );
1520
+ }
1521
+ if (recipient !== void 0 && !isAddress3(recipient)) {
1522
+ throw new StableValidationError(
1523
+ "recipient",
1524
+ recipient,
1525
+ `LI.FI bridge: invalid recipient address "${recipient}"`
1526
+ );
1527
+ }
1528
+ if (fromChain === toChain) {
1529
+ throw new StableValidationError(
1530
+ "toChain",
1531
+ toChain,
1532
+ "LI.FI bridge: fromChain and toChain must differ"
1533
+ );
1534
+ }
985
1535
  }
986
- // ─── Helpers ─────────────────────────────────────────────────────────────
987
1536
  _requireFrom() {
988
- const from = this.wc.account?.address;
1537
+ const from = this.config.walletClient?.account?.address ?? this.config.account?.address;
989
1538
  if (!from) {
990
1539
  throw new StableValidationError(
991
1540
  "signer",
992
1541
  void 0,
993
- "earn: requires a signer \u2014 set walletClient or account in StableConfig"
1542
+ "LI.FI: requires a signer \u2014 set walletClient or account in StableConfig"
994
1543
  );
995
1544
  }
996
1545
  return from;
997
1546
  }
998
- async _switchChain() {
1547
+ async _fetchQuote(qs) {
1548
+ const url = `https://li.quest/v1/quote?${qs}`;
999
1549
  try {
1000
- await this.wc.switchChain({ id: this.config.chainId });
1550
+ return await retryWithBackoff(() => this._fetchQuoteOnce(url));
1001
1551
  } 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,
1552
+ if (err instanceof StableQuoteError) throw err;
1553
+ if (err instanceof HttpError) {
1554
+ throw new StableQuoteError({
1555
+ provider: "lifi",
1556
+ shortMessage: err.message,
1557
+ httpStatus: err.status,
1007
1558
  cause: err
1008
1559
  });
1009
1560
  }
1561
+ throw new StableNetworkError({
1562
+ shortMessage: "LI.FI: quote request failed before reaching the server",
1563
+ url,
1564
+ cause: err
1565
+ });
1010
1566
  }
1011
1567
  }
1012
- async _sendTx(to, data, value, from) {
1568
+ async _fetchQuoteOnce(url) {
1569
+ const res = await fetch(url);
1570
+ const body = await res.json().catch(() => void 0);
1571
+ if (!res.ok) {
1572
+ const providerMessage = isObject(body) && typeof body.message === "string" ? body.message : void 0;
1573
+ const providerCode = isObject(body) && typeof body.code === "string" ? body.code : void 0;
1574
+ if (res.status === 429 || res.status >= 500) {
1575
+ throw new HttpError(
1576
+ `LI.FI: ${providerMessage ?? res.statusText}`,
1577
+ res.status,
1578
+ true
1579
+ );
1580
+ }
1581
+ throw new StableQuoteError({
1582
+ provider: "lifi",
1583
+ shortMessage: `LI.FI: ${providerMessage ?? res.statusText}`,
1584
+ httpStatus: res.status,
1585
+ providerCode,
1586
+ body
1587
+ });
1588
+ }
1589
+ if (!isLiFiQuoteShape(body)) {
1590
+ throw new StableQuoteError({
1591
+ provider: "lifi",
1592
+ shortMessage: "LI.FI: unexpected quote response shape",
1593
+ httpStatus: res.status,
1594
+ body
1595
+ });
1596
+ }
1597
+ return body;
1598
+ }
1599
+ _toSwapQuote(quote) {
1600
+ return {
1601
+ toAmount: Number(
1602
+ formatUnits4(
1603
+ BigInt(quote.estimate.toAmount),
1604
+ quote.action.toToken.decimals
1605
+ )
1606
+ ),
1607
+ fromAmount: BigInt(quote.estimate.fromAmount),
1608
+ fromToken: quote.action.fromToken.address,
1609
+ approvalAddress: quote.estimate.approvalAddress,
1610
+ transactionRequest: quote.transactionRequest
1611
+ };
1612
+ }
1613
+ async _executeSwapQuote(quote, srcRpc, expectedChainId) {
1614
+ const from = this._requireFrom();
1615
+ const { transactionRequest } = quote;
1616
+ const srcChainId = transactionRequest.chainId;
1617
+ if (srcChainId !== expectedChainId) {
1618
+ throw new StableQuoteError({
1619
+ provider: "lifi",
1620
+ shortMessage: `LI.FI: returned tx for chain ${srcChainId} but expected ${expectedChainId}`,
1621
+ providerCode: "chain_mismatch",
1622
+ body: quote
1623
+ });
1624
+ }
1625
+ const srcChain = makeChain(srcChainId, srcRpc);
1626
+ const wc = this.config.walletClient ?? createWalletClient3({
1627
+ account: this.config.account,
1628
+ chain: srcChain,
1629
+ transport: this.config.transport ?? http4(srcRpc)
1630
+ });
1013
1631
  try {
1014
- return await this.wc.sendTransaction({ to, data, value, account: from, chain: this.chain });
1632
+ await wc.switchChain({ id: srcChainId });
1015
1633
  } catch (err) {
1016
1634
  throw new StableTransactionError({
1017
- phase: "send",
1018
- shortMessage: "earn: transaction send failed",
1019
- chainId: this.config.chainId,
1635
+ phase: "switch_chain",
1636
+ shortMessage: `LI.FI: wallet rejected or failed to switch to chain ${srcChainId}`,
1637
+ chainId: srcChainId,
1020
1638
  cause: err
1021
1639
  });
1022
1640
  }
1023
- }
1024
- async _resolveDecimals(asset, op) {
1641
+ await this._ensureApproval(
1642
+ wc,
1643
+ srcChain,
1644
+ srcRpc,
1645
+ srcChainId,
1646
+ from,
1647
+ quote
1648
+ );
1649
+ let hash;
1025
1650
  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
- );
1651
+ hash = await wc.sendTransaction({
1652
+ to: transactionRequest.to,
1653
+ data: transactionRequest.data,
1654
+ value: transactionRequest.value && transactionRequest.value !== "0x" ? BigInt(transactionRequest.value) : 0n,
1655
+ gas: transactionRequest.gasLimit ? BigInt(transactionRequest.gasLimit) : void 0,
1656
+ account: wc.account,
1657
+ chain: srcChain
1658
+ });
1659
+ } catch (err) {
1660
+ throw new StableTransactionError({
1661
+ phase: "send",
1662
+ shortMessage: "LI.FI: swap/bridge transaction failed",
1663
+ chainId: srcChainId,
1664
+ cause: err
1665
+ });
1035
1666
  }
1667
+ return { txHash: hash, toAmount: quote.toAmount };
1036
1668
  }
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`);
1669
+ async _ensureApproval(wc, srcChain, srcRpc, srcChainId, from, quote) {
1670
+ const tokenAddr = quote.fromToken;
1671
+ const isNative = tokenAddr.toLowerCase() === "0x0000000000000000000000000000000000000000" || tokenAddr.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
1672
+ if (isNative || !quote.approvalAddress) return;
1673
+ const pc = createPublicClient4({
1674
+ chain: srcChain,
1675
+ transport: http4(srcRpc)
1676
+ });
1677
+ const allowance = await pc.readContract({
1678
+ address: tokenAddr,
1679
+ abi: erc20Abi3,
1680
+ functionName: "allowance",
1681
+ args: [from, quote.approvalAddress]
1682
+ });
1683
+ if (allowance >= quote.fromAmount) return;
1684
+ let approveHash;
1685
+ try {
1686
+ approveHash = await wc.writeContract({
1687
+ address: tokenAddr,
1688
+ abi: erc20Abi3,
1689
+ functionName: "approve",
1690
+ args: [quote.approvalAddress, quote.fromAmount],
1691
+ account: wc.account,
1692
+ chain: srcChain
1693
+ });
1694
+ } catch (err) {
1695
+ throw new StableTransactionError({
1696
+ phase: "approve",
1697
+ shortMessage: "LI.FI: token approve failed",
1698
+ chainId: srcChainId,
1699
+ cause: err
1700
+ });
1048
1701
  }
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`);
1702
+ const approveReceipt = await pc.waitForTransactionReceipt({
1703
+ hash: approveHash
1704
+ });
1705
+ if (approveReceipt.status === "reverted") {
1706
+ throw new StableTransactionError({
1707
+ phase: "approve",
1708
+ shortMessage: "LI.FI: token approve reverted on-chain",
1709
+ txHash: approveReceipt.transactionHash,
1710
+ chainId: srcChainId
1711
+ });
1053
1712
  }
1054
1713
  }
1055
1714
  };
1715
+ function isObject(v) {
1716
+ return typeof v === "object" && v !== null;
1717
+ }
1718
+ function isLiFiQuoteShape(v) {
1719
+ if (!isObject(v)) return false;
1720
+ const tx = v.transactionRequest;
1721
+ const est = v.estimate;
1722
+ const act = v.action;
1723
+ 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);
1724
+ }
1056
1725
 
1057
1726
  // src/stable.ts
1058
- function validateTransferParams({ to, amount, token, tokenDecimals }) {
1059
- if (!isAddress3(to)) {
1060
- throw new StableValidationError("to", to, `transfer: invalid recipient address "${to}"`);
1727
+ function validateTransferParams({
1728
+ to,
1729
+ amount,
1730
+ token,
1731
+ tokenDecimals
1732
+ }) {
1733
+ if (!isAddress4(to)) {
1734
+ throw new StableValidationError(
1735
+ "to",
1736
+ to,
1737
+ `transfer: invalid recipient address "${to}"`
1738
+ );
1061
1739
  }
1062
1740
  if (!Number.isFinite(amount) || amount <= 0) {
1063
- throw new StableValidationError("amount", amount, "transfer: amount must be a finite positive number");
1741
+ throw new StableValidationError(
1742
+ "amount",
1743
+ amount,
1744
+ "transfer: amount must be a finite positive number"
1745
+ );
1064
1746
  }
1065
- if (token !== void 0 && !isAddress3(token)) {
1066
- throw new StableValidationError("token", token, `transfer: invalid token address "${token}"`);
1747
+ if (token !== void 0 && !isAddress4(token)) {
1748
+ throw new StableValidationError(
1749
+ "token",
1750
+ token,
1751
+ `transfer: invalid token address "${token}"`
1752
+ );
1067
1753
  }
1068
1754
  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");
1755
+ throw new StableValidationError(
1756
+ "tokenDecimals",
1757
+ tokenDecimals,
1758
+ "transfer: tokenDecimals must be an integer between 0 and 77"
1759
+ );
1070
1760
  }
1071
1761
  }
1072
1762
  async function resolveTokenDecimals(publicClient, token, tokenDecimals) {
1073
1763
  if (tokenDecimals !== void 0) return tokenDecimals;
1074
1764
  try {
1075
- return await publicClient.readContract({ address: token, abi: erc20Abi4, functionName: "decimals" });
1765
+ return await publicClient.readContract({
1766
+ address: token,
1767
+ abi: erc20Abi4,
1768
+ functionName: "decimals"
1769
+ });
1076
1770
  } catch {
1077
1771
  throw new StableValidationError(
1078
1772
  "tokenDecimals",
@@ -1090,7 +1784,11 @@ async function executeTokenTransfer(wc, publicClient, token, to, amount, tokenDe
1090
1784
  `transfer: no contract found at "${token}" on chain ${chain.id} \u2014 check the address and network`
1091
1785
  );
1092
1786
  }
1093
- const decimals = await resolveTokenDecimals(publicClient, token, tokenDecimals);
1787
+ const decimals = await resolveTokenDecimals(
1788
+ publicClient,
1789
+ token,
1790
+ tokenDecimals
1791
+ );
1094
1792
  return wc.writeContract({
1095
1793
  address: token,
1096
1794
  abi: erc20Abi4,
@@ -1106,13 +1804,23 @@ function createStable({
1106
1804
  account,
1107
1805
  transport,
1108
1806
  walletClient,
1109
- earn: earnConfig
1807
+ earn: earnConfig,
1808
+ merklApiBase
1110
1809
  }) {
1111
1810
  const chain = network === "mainnet" /* Mainnet */ ? stableMainnet2 : stableTestnet2;
1112
- const rpc = (rpcOverride ?? chain.rpcUrls.default.http[0]).replace(/\/$/, "");
1811
+ const rpc = (rpcOverride ?? chain.rpcUrls.default.http[0]).replace(
1812
+ /\/$/,
1813
+ ""
1814
+ );
1113
1815
  const signerConfig = { account, transport, walletClient };
1114
1816
  const lifi = new LiFiService({ ...signerConfig, rpc, chainId: chain.id });
1115
- const morpho = earnConfig ? new MorphoService({ ...signerConfig, rpc, chainId: chain.id, vault: earnConfig.vault }) : null;
1817
+ const merkl = new MerklService({ ...signerConfig, rpc, chainId: chain.id, merklApiBase });
1818
+ const morpho = earnConfig ? new MorphoService({
1819
+ ...signerConfig,
1820
+ rpc,
1821
+ chainId: chain.id,
1822
+ vault: earnConfig.vault
1823
+ }) : null;
1116
1824
  function requireEarn() {
1117
1825
  if (!morpho) {
1118
1826
  throw new StableValidationError(
@@ -1123,14 +1831,25 @@ function createStable({
1123
1831
  }
1124
1832
  return morpho;
1125
1833
  }
1834
+ function requireSignerAddress() {
1835
+ const from = walletClient?.account?.address ?? account?.address;
1836
+ if (!from) {
1837
+ throw new StableValidationError(
1838
+ "signer",
1839
+ void 0,
1840
+ "stable.rewards.*: requires a signer \u2014 set walletClient or account in StableConfig"
1841
+ );
1842
+ }
1843
+ return from;
1844
+ }
1126
1845
  async function transfer(params) {
1127
1846
  validateTransferParams(params);
1128
1847
  const { from, to, amount, token, tokenDecimals } = params;
1129
1848
  const acc = walletClient?.account ?? account ?? from;
1130
- const wc = walletClient ?? createWalletClient3({
1849
+ const wc = walletClient ?? createWalletClient4({
1131
1850
  account: acc,
1132
1851
  chain,
1133
- transport: transport ?? http4(rpc)
1852
+ transport: transport ?? http5(rpc)
1134
1853
  });
1135
1854
  try {
1136
1855
  await wc.switchChain({ id: chain.id });
@@ -1142,8 +1861,29 @@ function createStable({
1142
1861
  cause: err
1143
1862
  });
1144
1863
  }
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) });
1864
+ const publicClient = createPublicClient5({
1865
+ chain,
1866
+ transport: http5(rpc)
1867
+ });
1868
+ const hash = token !== void 0 ? await executeTokenTransfer(
1869
+ wc,
1870
+ publicClient,
1871
+ token,
1872
+ to,
1873
+ amount,
1874
+ tokenDecimals,
1875
+ acc,
1876
+ chain
1877
+ ) : (
1878
+ // Native transfer on Stable mainnet/testnet. Native USDT0 uses 18 decimals
1879
+ // on both networks, matching standard EVM native-token decimals.
1880
+ await wc.sendTransaction({
1881
+ account: acc,
1882
+ chain,
1883
+ to,
1884
+ value: parseUnits4(String(amount), 18)
1885
+ })
1886
+ );
1147
1887
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
1148
1888
  if (receipt.status === "reverted") {
1149
1889
  throw new StableTransactionError({
@@ -1168,25 +1908,38 @@ function createStable({
1168
1908
  bulkWithdraw: (items, options) => requireEarn().bulkWithdraw(items, options),
1169
1909
  redeem: (params) => requireEarn().redeem(params),
1170
1910
  forceWithdraw: (params) => requireEarn().forceWithdraw(params),
1171
- forceRedeem: (params) => requireEarn().forceRedeem(params)
1911
+ forceRedeem: (params) => requireEarn().forceRedeem(params),
1912
+ prepareDepositCalldata: (params) => requireEarn().prepareDepositCalldata(params),
1913
+ prepareWithdrawCalldata: (params) => requireEarn().prepareWithdrawCalldata(params),
1914
+ prepareRedeemCalldata: (params) => requireEarn().prepareRedeemCalldata(params),
1915
+ incentiveRewards: {
1916
+ fetch: () => merkl.fetchRewards(requireSignerAddress()),
1917
+ claim: () => merkl.claim()
1918
+ }
1172
1919
  }
1173
1920
  };
1174
1921
  }
1175
1922
  function createStableReader(config) {
1176
1923
  const reader = new EarnReader(config);
1924
+ const chain = config.network === "testnet" /* Testnet */ ? stableTestnet2 : stableMainnet2;
1925
+ const rpc = (config.rpc ?? chain.rpcUrls.default.http[0]).replace(/\/$/, "");
1926
+ const merkl = new MerklService({ chainId: chain.id, rpc, merklApiBase: config.merklApiBase });
1177
1927
  return {
1178
1928
  earn: {
1179
1929
  getYield: () => reader.getYield(),
1180
1930
  position: () => reader.position(),
1181
1931
  preview: (params) => reader.preview(params),
1182
- withdrawability: (params) => reader.withdrawability(params)
1932
+ withdrawability: (params) => reader.withdrawability(params),
1933
+ incentiveRewards: {
1934
+ fetch: () => merkl.fetchRewards(config.address)
1935
+ }
1183
1936
  }
1184
1937
  };
1185
1938
  }
1186
1939
 
1187
1940
  // src/constants.ts
1188
1941
  var STABLE_USDT_ADDRESS = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
1189
- var STABLE_VAULT_ADDRESS = "0x461eF73F640456138242398a39AB48a557A49293";
1942
+ var STABLE_VAULT_ADDRESS = "0xb7Df8db22A5DBBFA9ebeb94b3910aec6a4f05c08";
1190
1943
  export {
1191
1944
  CHAIN_CONFIGS,
1192
1945
  Chain,