@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.cjs CHANGED
@@ -9,7 +9,12 @@
9
9
  var _viem = require('viem');
10
10
  var _chains = require('viem/chains');
11
11
 
12
- // src/earn/reader.ts
12
+ // src/earn/morpho.ts
13
+
14
+
15
+
16
+
17
+ var _morphosdk = require('@morpho-org/morpho-sdk');
13
18
 
14
19
 
15
20
 
@@ -19,7 +24,6 @@ var _chains = require('viem/chains');
19
24
 
20
25
 
21
26
 
22
- var _morphosdk = require('@morpho-org/morpho-sdk');
23
27
 
24
28
  // src/errors.ts
25
29
 
@@ -39,7 +43,8 @@ var StableValidationError = class extends StableError {
39
43
  var StableQuoteError = class extends StableError {
40
44
  constructor(args) {
41
45
  const meta = [`Provider: ${args.provider}`];
42
- if (args.httpStatus !== void 0) meta.push(`HTTP status: ${args.httpStatus}`);
46
+ if (args.httpStatus !== void 0)
47
+ meta.push(`HTTP status: ${args.httpStatus}`);
43
48
  if (args.providerCode) meta.push(`Provider code: ${args.providerCode}`);
44
49
  super(args.shortMessage, {
45
50
  name: "StableQuoteError",
@@ -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 = _nullishCoalesce(opts.maxAttempts, () => ( 3));
@@ -142,550 +151,870 @@ 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 (!_optionalChain([config, 'access', _2 => _2.earn, 'optionalAccess', _3 => _3.vault])) {
171
- throw new StableValidationError(
172
- "earn.vault",
173
- void 0,
174
- "createStableReader: earn.vault is required"
175
- );
176
- }
177
- if (!_viem.isAddress.call(void 0, config.address)) {
178
- throw new StableValidationError(
179
- "address",
180
- config.address,
181
- `createStableReader: invalid user address "${config.address}"`
182
- );
183
- }
184
- if (!_viem.isAddress.call(void 0, config.earn.vault)) {
157
+ this.config = config;
158
+ this.inFlight = /* @__PURE__ */ new Map();
159
+ this.chain = makeChain(config.chainId, config.rpc);
160
+ this.wc = _nullishCoalesce(config.walletClient, () => ( _viem.createWalletClient.call(void 0, {
161
+ account: config.account,
162
+ chain: this.chain,
163
+ transport: _nullishCoalesce(config.transport, () => ( _viem.http.call(void 0, config.rpc)))
164
+ })));
165
+ const morphoWc = _optionalChain([this, 'access', _2 => _2.wc, 'access', _3 => _3.chain, 'optionalAccess', _4 => _4.id]) === config.chainId ? this.wc : _viem.createWalletClient.call(void 0, {
166
+ account: this.wc.account,
167
+ chain: this.chain,
168
+ transport: _viem.custom.call(void 0, {
169
+ request: this.wc.request.bind(this.wc)
170
+ })
171
+ });
172
+ this.morpho = new (0, _morphosdk.MorphoClient)(morphoWc, { supportSignature: true });
173
+ this.morphoNoSig = new (0, _morphosdk.MorphoClient)(morphoWc, { supportSignature: false });
174
+ this.pc = _viem.createPublicClient.call(void 0, {
175
+ chain: this.chain,
176
+ transport: _viem.http.call(void 0, 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 = _nullishCoalesce(config.network, () => ( "mainnet")) /* Mainnet */;
192
- const viemChain = network === "mainnet" /* Mainnet */ ? _chains.stable : _chains.stableTestnet;
193
- const rpc = (_nullishCoalesce(config.rpc, () => ( viemChain.rpcUrls.default.http[0]))).replace(/\/$/, "");
194
- this.chainId = viemChain.id;
195
- this.vaultAddr = config.earn.vault;
196
- this.userAddr = config.address;
197
- const chain = makeChain(viemChain.id, rpc);
198
- this.pc = _viem.createPublicClient.call(void 0, { chain, transport: _viem.http.call(void 0, rpc) });
199
- this.morpho = new (0, _morphosdk.MorphoClient)(this.pc);
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: _viem.erc20Abi,
208
- functionName: "balanceOf",
209
- args: [this.userAddr]
210
- })
211
- ]);
212
- const [tokenDecimals, shareDecimals] = await Promise.all([
213
- this.pc.readContract({ address: vaultData.asset, abi: _viem.erc20Abi, functionName: "decimals" }),
214
- this.pc.readContract({ address: this.vaultAddr, abi: _viem.erc20Abi, functionName: "decimals" })
215
- ]);
216
- const assetsRaw = vaultData.toAssets(shares);
217
- return {
218
- shares,
219
- sharesFormatted: Number(_viem.formatUnits.call(void 0, shares, shareDecimals)),
220
- shareDecimals,
221
- assets: Number(_viem.formatUnits.call(void 0, assetsRaw, tokenDecimals)),
222
- tokenDecimals,
223
- assetAddress: vaultData.asset
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 = await _asyncNullishCoalesce(params.tokenDecimals, async () => ( await this.pc.readContract({
242
- address: vaultData.asset,
243
- abi: _viem.erc20Abi,
244
- functionName: "decimals"
245
- })));
246
- const requestedRaw = _viem.parseUnits.call(void 0, String(params.amount), tokenDecimals);
247
- const availableLiquidityRaw = vaultData.assetBalance;
248
- return {
249
- isInstant: requestedRaw <= availableLiquidityRaw,
250
- availableLiquidity: Number(_viem.formatUnits.call(void 0, 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 = _nullishCoalesce(params.vault, () => ( this.config.vault));
324
+ const vault = this.morphoNoSig.vaultV2(vaultAddr, this.config.chainId);
325
+ const vaultData = await vault.getData();
326
+ const decimals = await _asyncNullishCoalesce(tokenDecimals, async () => ( await this._resolveDecimals(vaultData.asset, "prepareDepositCalldata")));
327
+ const assetsRaw = _viem.parseUnits.call(void 0, 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(_viem.formatUnits.call(void 0, maxAllowed, decimals));
333
+ const idleFormatted = Number(_viem.formatUnits.call(void 0, 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 = _optionalChain([body, 'optionalAccess', _4 => _4.data, 'optionalAccess', _5 => _5.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 ? _viem.parseUnits.call(void 0, String(slippageTolerance), 18) : void 0
346
+ });
347
+ const requirements = await depositAction.getRequirements();
348
+ const steps = [];
349
+ for (const req of requirements) {
350
+ if (_morphosdk.isRequirementApproval.call(void 0, 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: _nullishCoalesce(vault.netApyExcludingRewards, () => ( 0)),
319
- rewards,
320
- performanceFee: vault.performanceFee,
321
- managementFee: vault.managementFee
322
- };
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 };
323
357
  }
324
- };
325
-
326
- // src/services/lifi.ts
327
-
328
-
329
-
330
-
331
-
332
-
333
-
334
-
335
-
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
358
+ async prepareWithdrawCalldata(params) {
359
+ this._validateAmount(params.amount, "prepareWithdrawCalldata");
360
+ const { amount, tokenDecimals } = params;
361
+ const from = this._requireFrom();
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 = await _asyncNullishCoalesce(tokenDecimals, async () => ( await this._resolveDecimals(vaultData.asset, "prepareWithdrawCalldata")));
366
+ const assetsRaw = _viem.parseUnits.call(void 0, 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 };
406
377
  }
407
- };
408
-
409
- // src/services/lifi.ts
410
- var LiFiService = class {
411
- constructor(config) {
412
- this.config = config;
378
+ async prepareRedeemCalldata(params) {
379
+ this._validateShares(params.shares, "prepareRedeemCalldata");
380
+ const { shares, shareDecimals } = params;
381
+ const from = this._requireFrom();
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: _viem.erc20Abi,
389
+ functionName: "decimals"
390
+ });
391
+ } catch (e2) {
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
+ }
398
+ }
399
+ const sharesRaw = _viem.parseUnits.call(void 0, 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 };
413
413
  }
414
- async quoteSwap(params) {
415
- this._validateSwapParams(params);
416
- const { fromToken, toToken, amount, fromDecimals = 6, toAddress } = params;
414
+ // ─── Private operations ──────────────────────────────────────────────────
415
+ async _deposit(params) {
416
+ const {
417
+ amount,
418
+ tokenDecimals,
419
+ slippageTolerance,
420
+ idempotencyKey,
421
+ depositBuffer
422
+ } = params;
417
423
  const from = this._requireFrom();
418
- const chain = this.config.chainId.toString();
419
- const fromAmountRaw = _viem.parseUnits.call(void 0, 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 }
424
+ const vaultAddr = _nullishCoalesce(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 = await _asyncNullishCoalesce(tokenDecimals, async () => ( await this._resolveDecimals(vaultData.asset, "earn deposit")));
429
+ const assetsRaw = _viem.parseUnits.call(void 0, 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(_viem.formatUnits.call(void 0, maxAllowed, decimals));
435
+ const idleFormatted = Number(
436
+ _viem.formatUnits.call(void 0, 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
+ }
444
+ }
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
+ _viem.parseUnits.call(void 0, String(slippageTolerance), 18)
452
+ ) : void 0
428
453
  });
429
- const quote = await this._fetchQuote(qs);
430
- return this._toSwapQuote(quote);
454
+ const requirements = await depositAction.getRequirements();
455
+ let sig;
456
+ for (const req of requirements) {
457
+ if (_morphosdk.isRequirementApproval.call(void 0, 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 (_morphosdk.isRequirementSignature.call(void 0, req)) {
476
+ sig = await req.sign(this.wc, from);
477
+ }
478
+ }
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
+ });
489
+ }
490
+ return { txHash: receipt.transactionHash, idempotencyKey };
431
491
  }
432
- async swap(params) {
433
- this._validateSwapParams(params);
434
- const quote = await _asyncNullishCoalesce(params.quote, async () => ( await this.quoteSwap(params)));
435
- return this._executeSwapQuote(quote, this.config.rpc, this.config.chainId);
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 = await _asyncNullishCoalesce(tokenDecimals, async () => ( await this._resolveDecimals(vaultData.asset, "earn withdraw")));
500
+ const assetsRaw = _viem.parseUnits.call(void 0, 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
+ });
519
+ }
520
+ return { txHash: receipt.transactionHash, idempotencyKey };
436
521
  }
437
- async quoteBridge(params) {
438
- this._validateBridgeParams(params);
439
- const { fromChain, toChain, fromToken, toToken, amount, fromDecimals = 6, recipient } = params;
440
- const from = _nullishCoalesce(_nullishCoalesce(_optionalChain([this, 'access', _6 => _6.config, 'access', _7 => _7.walletClient, 'optionalAccess', _8 => _8.account, 'optionalAccess', _9 => _9.address]), () => ( _optionalChain([this, 'access', _10 => _10.config, 'access', _11 => _11.account, 'optionalAccess', _12 => _12.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: _viem.parseUnits.call(void 0, 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 };
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: _viem.erc20Abi,
533
+ functionName: "decimals"
534
+ });
535
+ } catch (e3) {
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
+ }
542
+ }
543
+ const sharesRaw = _viem.parseUnits.call(void 0, 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
+ });
565
+ }
566
+ return { txHash: receipt.transactionHash, idempotencyKey };
456
567
  }
457
- async bridge(params) {
458
- this._validateBridgeParams(params);
459
- const { fromChain, toChain, fromToken, toToken, amount, fromDecimals = 6, recipient } = params;
568
+ async _forceWithdraw(params) {
569
+ const { amount, tokenDecimals, deallocations, idempotencyKey } = params;
460
570
  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}"`);
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"
580
+ );
465
581
  }
466
- if (!dst) {
467
- throw new StableValidationError("toChain", toChain, `LI.FI bridge: unsupported destination chain "${toChain}"`);
582
+ const tx = vault.forceWithdraw({
583
+ deallocations: this._buildDeallocs(deallocations, decimals),
584
+ withdraw: { amount: _viem.parseUnits.call(void 0, 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
595
+ });
468
596
  }
469
- const fromAmountRaw = _viem.parseUnits.call(void 0, 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);
597
+ return { txHash: receipt.transactionHash, idempotencyKey };
481
598
  }
482
- _validateSwapParams({ fromToken, toToken, amount, toAddress }) {
483
- if (!_viem.isAddress.call(void 0, fromToken)) {
484
- throw new StableValidationError("fromToken", fromToken, `swap: invalid fromToken address "${fromToken}"`);
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: _viem.erc20Abi,
617
+ functionName: "decimals"
618
+ });
619
+ } catch (e4) {
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
+ );
625
+ }
485
626
  }
486
- if (!_viem.isAddress.call(void 0, toToken)) {
487
- throw new StableValidationError("toToken", toToken, `swap: invalid toToken address "${toToken}"`);
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
+ );
488
634
  }
489
- if (fromToken.toLowerCase() === toToken.toLowerCase()) {
490
- throw new StableValidationError("toToken", toToken, "swap: fromToken and toToken must differ");
635
+ const tx = vault.forceRedeem({
636
+ deallocations: this._buildDeallocs(deallocations, assetDec),
637
+ redeem: { shares: _viem.parseUnits.call(void 0, 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
648
+ });
491
649
  }
492
- if (!Number.isFinite(amount) || amount <= 0) {
493
- throw new StableValidationError("amount", amount, "swap: amount must be a finite positive number");
650
+ return { txHash: receipt.transactionHash, idempotencyKey };
651
+ }
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);
494
663
  }
495
- if (toAddress !== void 0 && !_viem.isAddress.call(void 0, toAddress)) {
496
- throw new StableValidationError("toAddress", toAddress, `swap: invalid toAddress "${toAddress}"`);
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
+ );
497
670
  }
671
+ return result;
498
672
  }
499
- _validateBridgeParams({ fromChain, toChain, fromToken, toToken, amount, recipient }) {
500
- if (!_viem.isAddress.call(void 0, fromToken)) {
501
- throw new StableValidationError("fromToken", fromToken, `LI.FI bridge: invalid fromToken address "${fromToken}"`);
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;
502
680
  }
503
- if (!_viem.isAddress.call(void 0, toToken)) {
504
- throw new StableValidationError("toToken", toToken, `LI.FI bridge: invalid toToken address "${toToken}"`);
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 = _nullishCoalesce(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;
505
694
  }
506
- if (!Number.isFinite(amount) || amount <= 0) {
507
- throw new StableValidationError("amount", amount, "LI.FI bridge: amount must be a finite positive number");
695
+ return rem;
696
+ }
697
+ _buildDeallocs(deallocations, assetDecimals) {
698
+ return deallocations.map((d) => ({
699
+ adapter: d.adapter,
700
+ amount: _viem.parseUnits.call(void 0, 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 = _optionalChain([this, 'access', _5 => _5.wc, 'access', _6 => _6.account, 'optionalAccess', _7 => _7.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
+ );
508
717
  }
509
- if (recipient !== void 0 && !_viem.isAddress.call(void 0, recipient)) {
510
- throw new StableValidationError("recipient", recipient, `LI.FI bridge: invalid recipient address "${recipient}"`);
718
+ return from;
719
+ }
720
+ async _switchChain() {
721
+ try {
722
+ await this.wc.switchChain({ id: this.config.chainId });
723
+ } catch (err) {
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
+ }
511
732
  }
512
- if (fromChain === toChain) {
513
- throw new StableValidationError("toChain", toChain, "LI.FI bridge: fromChain and toChain must differ");
733
+ }
734
+ async _sendTx(to, data, value, from) {
735
+ try {
736
+ return await this.wc.sendTransaction({
737
+ to,
738
+ data,
739
+ value,
740
+ account: from,
741
+ chain: this.chain
742
+ });
743
+ } catch (err) {
744
+ throw new StableTransactionError({
745
+ phase: "send",
746
+ shortMessage: "earn: transaction send failed",
747
+ chainId: this.config.chainId,
748
+ cause: err
749
+ });
750
+ }
751
+ }
752
+ async _resolveDecimals(asset, op) {
753
+ try {
754
+ return await retryWithBackoff(
755
+ () => this.pc.readContract({
756
+ address: asset,
757
+ abi: _viem.erc20Abi,
758
+ functionName: "decimals"
759
+ })
760
+ );
761
+ } catch (e5) {
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
+ );
514
784
  }
515
785
  }
516
- _requireFrom() {
517
- const from = _nullishCoalesce(_optionalChain([this, 'access', _13 => _13.config, 'access', _14 => _14.walletClient, 'optionalAccess', _15 => _15.account, 'optionalAccess', _16 => _16.address]), () => ( _optionalChain([this, 'access', _17 => _17.config, 'access', _18 => _18.account, 'optionalAccess', _19 => _19.address])));
518
- if (!from) {
786
+ _validateShares(shares, op) {
787
+ if (!Number.isFinite(shares) || shares <= 0) {
519
788
  throw new StableValidationError(
520
- "signer",
521
- void 0,
522
- "LI.FI: requires a signer \u2014 set walletClient or account in StableConfig"
789
+ "shares",
790
+ shares,
791
+ `${op}: shares must be a finite positive number`
523
792
  );
524
793
  }
525
- return from;
526
794
  }
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
545
- });
795
+ };
796
+
797
+ // src/earn/reader.ts
798
+
799
+
800
+
801
+
802
+
803
+
804
+
805
+
806
+
807
+
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 (!_optionalChain([config, 'access', _8 => _8.earn, 'optionalAccess', _9 => _9.vault])) {
835
+ throw new StableValidationError(
836
+ "earn.vault",
837
+ void 0,
838
+ "createStableReader: earn.vault is required"
839
+ );
546
840
  }
547
- }
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: ${_nullishCoalesce(providerMessage, () => ( res.statusText))}`, res.status, true);
556
- }
557
- throw new StableQuoteError({
558
- provider: "lifi",
559
- shortMessage: `LI.FI: ${_nullishCoalesce(providerMessage, () => ( res.statusText))}`,
560
- httpStatus: res.status,
561
- providerCode,
562
- body
563
- });
841
+ if (!_viem.isAddress.call(void 0, config.address)) {
842
+ throw new StableValidationError(
843
+ "address",
844
+ config.address,
845
+ `createStableReader: invalid user address "${config.address}"`
846
+ );
564
847
  }
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
571
- });
848
+ if (!_viem.isAddress.call(void 0, config.earn.vault)) {
849
+ throw new StableValidationError(
850
+ "earn.vault",
851
+ config.earn.vault,
852
+ `createStableReader: invalid vault address "${config.earn.vault}"`
853
+ );
572
854
  }
573
- return body;
855
+ const network = _nullishCoalesce(config.network, () => ( "mainnet")) /* Mainnet */;
856
+ const viemChain = network === "mainnet" /* Mainnet */ ? _chains.stable : _chains.stableTestnet;
857
+ const rpc = (_nullishCoalesce(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 = _viem.createPublicClient.call(void 0, { chain, transport: _viem.http.call(void 0, rpc) });
866
+ this.morpho = new (0, _morphosdk.MorphoClient)(this.pc);
574
867
  }
575
- _toSwapQuote(quote) {
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: _viem.erc20Abi,
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: _viem.erc20Abi,
883
+ functionName: "decimals"
884
+ }),
885
+ this.pc.readContract({
886
+ address: this.vaultAddr,
887
+ abi: _viem.erc20Abi,
888
+ functionName: "decimals"
889
+ })
890
+ ]);
891
+ const assetsRaw = vaultData.toAssets(shares);
576
892
  return {
577
- toAmount: Number(_viem.formatUnits.call(void 0, 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
893
+ shares,
894
+ sharesFormatted: Number(_viem.formatUnits.call(void 0, shares, shareDecimals)),
895
+ shareDecimals,
896
+ assets: Number(_viem.formatUnits.call(void 0, assetsRaw, tokenDecimals)),
897
+ tokenDecimals,
898
+ assetAddress: vaultData.asset
582
899
  };
583
900
  }
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
- });
595
- }
596
- const srcChain = makeChain(srcChainId, srcRpc);
597
- const wc = _nullishCoalesce(this.config.walletClient, () => ( _viem.createWalletClient.call(void 0, {
598
- account: this.config.account,
599
- chain: srcChain,
600
- transport: _nullishCoalesce(this.config.transport, () => ( _viem.http.call(void 0, srcRpc)))
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 = await _asyncNullishCoalesce(params.tokenDecimals, async () => ( await this.pc.readContract({
917
+ address: vaultData.asset,
918
+ abi: _viem.erc20Abi,
919
+ functionName: "decimals"
601
920
  })));
602
- try {
603
- await wc.switchChain({ id: srcChainId });
604
- } 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
- });
611
- }
612
- await this._ensureApproval(wc, srcChain, srcRpc, srcChainId, from, quote);
613
- let hash;
614
- 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
622
- });
623
- } catch (err) {
624
- throw new StableTransactionError({
625
- phase: "send",
626
- shortMessage: "LI.FI: swap/bridge transaction failed",
627
- chainId: srcChainId,
628
- cause: err
629
- });
921
+ const requestedRaw = _viem.parseUnits.call(void 0, String(params.amount), tokenDecimals);
922
+ let availableLiquidityRaw = vaultData.assetBalance;
923
+ if (vaultData.accrualLiquidityAdapter != null) {
924
+ availableLiquidityRaw += vaultData.accrualLiquidityAdapter.maxWithdraw(
925
+ vaultData.liquidityData
926
+ ).value;
630
927
  }
631
- return { txHash: hash, toAmount: quote.toAmount };
928
+ return {
929
+ isInstant: requestedRaw <= availableLiquidityRaw,
930
+ availableLiquidity: Number(
931
+ _viem.formatUnits.call(void 0, availableLiquidityRaw, tokenDecimals)
932
+ ),
933
+ tokenDecimals
934
+ };
632
935
  }
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 = _viem.createPublicClient.call(void 0, { chain: srcChain, transport: _viem.http.call(void 0, srcRpc) });
638
- const allowance = await pc.readContract({
639
- address: tokenAddr,
640
- abi: _viem.erc20Abi,
641
- functionName: "allowance",
642
- args: [from, quote.approvalAddress]
643
- });
644
- if (allowance >= quote.fromAmount) return;
645
- let approveHash;
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: _viem.erc20Abi,
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 = _optionalChain([body, 'optionalAccess', _10 => _10.data, 'optionalAccess', _11 => _11.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: _nullishCoalesce(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
-
685
- // src/earn/morpho.ts
686
-
687
-
688
1016
 
1017
+ // src/earn/merkl.ts
689
1018
 
690
1019
 
691
1020
 
@@ -694,386 +1023,751 @@ function isLiFiQuoteShape(v) {
694
1023
 
695
1024
 
696
1025
 
697
-
698
-
699
-
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 = _nullishCoalesce(config.walletClient, () => ( _viem.createWalletClient.call(void 0, {
706
- account: config.account,
707
- chain: this.chain,
708
- transport: _nullishCoalesce(config.transport, () => ( _viem.http.call(void 0, config.rpc)))
709
- })));
710
- const morphoWc = _optionalChain([this, 'access', _20 => _20.wc, 'access', _21 => _21.chain, 'optionalAccess', _22 => _22.id]) === config.chainId ? this.wc : _viem.createWalletClient.call(void 0, {
711
- account: this.wc.account,
712
- chain: this.chain,
713
- transport: _viem.custom.call(void 0, { request: this.wc.request.bind(this.wc) })
714
- });
715
- this.morpho = new (0, _morphosdk.MorphoClient)(morphoWc, { supportSignature: true });
716
- this.pc = _viem.createPublicClient.call(void 0, { chain: this.chain, transport: _viem.http.call(void 0, 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 (!_viem.isAddress.call(void 0, 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));
1054
+ const base = (_nullishCoalesce(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);
786
1058
  }
787
- forceWithdraw(params) {
788
- this._validateAmount(params.amount, "earn forceWithdraw");
789
- return this._withIdempotency(params.idempotencyKey, () => this._forceWithdraw(params));
790
- }
791
- forceRedeem(params) {
792
- this._validateShares(params.shares, "earn forceRedeem");
793
- return this._withIdempotency(params.idempotencyKey, () => this._forceRedeem(params));
794
- }
795
- // ─── Private operations ──────────────────────────────────────────────────
796
- async _deposit(params) {
797
- const { amount, tokenDecimals, slippageTolerance, idempotencyKey, depositBuffer } = params;
798
- const from = this._requireFrom();
799
- const vaultAddr = _nullishCoalesce(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 = await _asyncNullishCoalesce(tokenDecimals, async () => ( await this._resolveDecimals(vaultData.asset, "earn deposit")));
804
- const assetsRaw = _viem.parseUnits.call(void 0, 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(_viem.formatUnits.call(void 0, maxAllowed, decimals));
810
- const idleFormatted = Number(_viem.formatUnits.call(void 0, vaultData.assetBalance, decimals));
811
- throw new StableValidationError(
812
- "amount",
813
- amount,
814
- `earn deposit: amount ${amount} exceeds deposit buffer \u2014 max ${maxFormatted} (${depositBuffer * 100}% of ${idleFormatted} idle liquidity)`
815
- );
816
- }
817
- }
818
- const depositAction = vault.deposit({
819
- amount: assetsRaw,
820
- userAddress: from,
821
- accrualVault: vaultData,
822
- slippageTolerance: slippageTolerance !== void 0 ? _viem.parseUnits.call(void 0, String(slippageTolerance), 18) : void 0
823
- });
824
- const requirements = await depositAction.getRequirements();
825
- let sig;
826
- for (const req of requirements) {
827
- if (_morphosdk.isRequirementApproval.call(void 0, 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 (_morphosdk.isRequirementSignature.call(void 0, req)) {
839
- sig = await req.sign(this.wc, from);
840
- }
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
+ );
841
1071
  }
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") {
1072
+ const chain = makeChain(this.config.chainId, this.config.rpc);
1073
+ const wc = _nullishCoalesce(this.config.walletClient, () => ( _viem.createWalletClient.call(void 0, {
1074
+ account: this.config.account,
1075
+ chain,
1076
+ transport: _nullishCoalesce(this.config.transport, () => ( _viem.http.call(void 0, this.config.rpc)))
1077
+ })));
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 = await _asyncNullishCoalesce(tokenDecimals, async () => ( await this._resolveDecimals(vaultData.asset, "earn withdraw")));
863
- const assetsRaw = _viem.parseUnits.call(void 0, 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") {
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) {
868
1105
  throw new StableTransactionError({
869
- phase: "receipt",
870
- shortMessage: "earn withdraw: vault withdraw reverted on-chain",
871
- txHash: receipt.transactionHash,
872
- chainId: this.config.chainId
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: _viem.erc20Abi, functionName: "decimals" });
886
- } catch (e2) {
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 = _viem.parseUnits.call(void 0, 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 = _viem.createPublicClient.call(void 0, {
1113
+ chain,
1114
+ transport: _viem.http.call(void 0, 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 (!_viem.isAddress.call(void 0, 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: _viem.parseUnits.call(void 0, 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 = _viem.encodeFunctionData.call(void 0, {
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 = _nullishCoalesce(_optionalChain([this, 'access', _12 => _12.config, 'access', _13 => _13.walletClient, 'optionalAccess', _14 => _14.account, 'optionalAccess', _15 => _15.address]), () => ( _optionalChain([this, 'access', _16 => _16.config, 'access', _17 => _17.account, 'optionalAccess', _18 => _18.address])));
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 (_optionalChain([entry, 'optionalAccess', _19 => _19.chain, 'optionalAccess', _20 => _20.id]) !== chainId) continue;
1182
+ for (const reward of _nullishCoalesce(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 (!_optionalChain([reward, 'optionalAccess', _21 => _21.token, 'optionalAccess', _22 => _22.address])) return null;
1191
+ const net = BigInt(_nullishCoalesce(reward.amount, () => ( "0"))) - BigInt(_nullishCoalesce(reward.claimed, () => ( "0")));
1192
+ if (net <= 0n) return null;
1193
+ const rawAmount = net.toString();
1194
+ const decimals = _nullishCoalesce(reward.token.decimals, () => ( 18));
1195
+ return {
1196
+ token: {
1197
+ address: reward.token.address,
1198
+ symbol: _nullishCoalesce(reward.token.symbol, () => ( "")),
1199
+ decimals
1200
+ },
1201
+ rawAmount,
1202
+ amount: Number(_viem.formatUnits.call(void 0, BigInt(rawAmount), decimals)),
1203
+ proofs: _nullishCoalesce(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: _viem.erc20Abi, functionName: "decimals" });
947
- } catch (e3) {
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
+
1246
+
1247
+
1248
+
1249
+
1250
+
1251
+
1252
+
1253
+
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 = _viem.parseUnits.call(void 0,
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 = await _asyncNullishCoalesce(params.quote, async () => ( 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 = _nullishCoalesce(_nullishCoalesce(_optionalChain([this, 'access', _23 => _23.config, 'access', _24 => _24.walletClient, 'optionalAccess', _25 => _25.account, 'optionalAccess', _26 => _26.address]), () => ( _optionalChain([this, 'access', _27 => _27.config, 'access', _28 => _28.account, 'optionalAccess', _29 => _29.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: _viem.parseUnits.call(void 0, 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 = _viem.parseUnits.call(void 0,
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 (!_viem.isAddress.call(void 0, fromToken)) {
1457
+ throw new StableValidationError(
1458
+ "fromToken",
1459
+ fromToken,
1460
+ `swap: invalid fromToken address "${fromToken}"`
1461
+ );
1462
+ }
1463
+ if (!_viem.isAddress.call(void 0, toToken)) {
1464
+ throw new StableValidationError(
1465
+ "toToken",
1466
+ toToken,
1467
+ `swap: invalid toToken address "${toToken}"`
1468
+ );
1469
+ }
1470
+ if (fromToken.toLowerCase() === toToken.toLowerCase()) {
1471
+ throw new StableValidationError(
1472
+ "toToken",
1473
+ toToken,
1474
+ "swap: fromToken and toToken must differ"
1475
+ );
954
1476
  }
955
- let assetDec = tokenDecimals;
956
- if (assetDec === void 0) {
957
- const vaultData = await vault.getData();
958
- assetDec = await this._resolveDecimals(vaultData.asset, "earn forceRedeem");
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
+ );
959
1483
  }
960
- const tx = vault.forceRedeem({
961
- deallocations: this._buildDeallocs(deallocations, assetDec),
962
- redeem: { shares: _viem.parseUnits.call(void 0, 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
- });
1484
+ if (toAddress !== void 0 && !_viem.isAddress.call(void 0, 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: _viem.parseUnits.call(void 0, 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 (!_viem.isAddress.call(void 0, fromToken)) {
1501
+ throw new StableValidationError(
1502
+ "fromToken",
1503
+ fromToken,
1504
+ `LI.FI bridge: invalid fromToken address "${fromToken}"`
1505
+ );
1506
+ }
1507
+ if (!_viem.isAddress.call(void 0, 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 && !_viem.isAddress.call(void 0, 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 = _optionalChain([this, 'access', _23 => _23.wc, 'access', _24 => _24.account, 'optionalAccess', _25 => _25.address]);
1537
+ const from = _nullishCoalesce(_optionalChain([this, 'access', _30 => _30.config, 'access', _31 => _31.walletClient, 'optionalAccess', _32 => _32.account, 'optionalAccess', _33 => _33.address]), () => ( _optionalChain([this, 'access', _34 => _34.config, 'access', _35 => _35.account, 'optionalAccess', _36 => _36.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: ${_nullishCoalesce(providerMessage, () => ( res.statusText))}`,
1577
+ res.status,
1578
+ true
1579
+ );
1580
+ }
1581
+ throw new StableQuoteError({
1582
+ provider: "lifi",
1583
+ shortMessage: `LI.FI: ${_nullishCoalesce(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
+ _viem.formatUnits.call(void 0,
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 = _nullishCoalesce(this.config.walletClient, () => ( _viem.createWalletClient.call(void 0, {
1627
+ account: this.config.account,
1628
+ chain: srcChain,
1629
+ transport: _nullishCoalesce(this.config.transport, () => ( _viem.http.call(void 0, 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: _viem.erc20Abi, functionName: "decimals" })
1028
- );
1029
- } catch (e4) {
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 = _viem.createPublicClient.call(void 0, {
1674
+ chain: srcChain,
1675
+ transport: _viem.http.call(void 0, srcRpc)
1676
+ });
1677
+ const allowance = await pc.readContract({
1678
+ address: tokenAddr,
1679
+ abi: _viem.erc20Abi,
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: _viem.erc20Abi,
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 }) {
1727
+ function validateTransferParams({
1728
+ to,
1729
+ amount,
1730
+ token,
1731
+ tokenDecimals
1732
+ }) {
1059
1733
  if (!_viem.isAddress.call(void 0, to)) {
1060
- throw new StableValidationError("to", to, `transfer: invalid recipient address "${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
1747
  if (token !== void 0 && !_viem.isAddress.call(void 0, token)) {
1066
- throw new StableValidationError("token", token, `transfer: invalid token address "${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: _viem.erc20Abi, functionName: "decimals" });
1076
- } catch (e5) {
1765
+ return await publicClient.readContract({
1766
+ address: token,
1767
+ abi: _viem.erc20Abi,
1768
+ functionName: "decimals"
1769
+ });
1770
+ } catch (e6) {
1077
1771
  throw new StableValidationError(
1078
1772
  "tokenDecimals",
1079
1773
  void 0,
@@ -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: _viem.erc20Abi,
@@ -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 */ ? _chains.stable : _chains.stableTestnet;
1112
- const rpc = (_nullishCoalesce(rpcOverride, () => ( chain.rpcUrls.default.http[0]))).replace(/\/$/, "");
1811
+ const rpc = (_nullishCoalesce(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,10 +1831,21 @@ function createStable({
1123
1831
  }
1124
1832
  return morpho;
1125
1833
  }
1834
+ function requireSignerAddress() {
1835
+ const from = _nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _37 => _37.account, 'optionalAccess', _38 => _38.address]), () => ( _optionalChain([account, 'optionalAccess', _39 => _39.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
- const acc = _nullishCoalesce(_nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _26 => _26.account]), () => ( account)), () => ( from));
1848
+ const acc = _nullishCoalesce(_nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _40 => _40.account]), () => ( account)), () => ( from));
1130
1849
  const wc = _nullishCoalesce(walletClient, () => ( _viem.createWalletClient.call(void 0, {
1131
1850
  account: acc,
1132
1851
  chain,
@@ -1142,8 +1861,29 @@ function createStable({
1142
1861
  cause: err
1143
1862
  });
1144
1863
  }
1145
- const publicClient = _viem.createPublicClient.call(void 0, { chain, transport: _viem.http.call(void 0, 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: _viem.parseUnits.call(void 0, String(amount), 18) });
1864
+ const publicClient = _viem.createPublicClient.call(void 0, {
1865
+ chain,
1866
+ transport: _viem.http.call(void 0, 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: _viem.parseUnits.call(void 0, 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 */ ? _chains.stableTestnet : _chains.stable;
1925
+ const rpc = (_nullishCoalesce(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
 
1191
1944
 
1192
1945