@stablechain/sdk 0.1.0 → 1.0.0

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