@stablechain/sdk 0.1.1 → 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 +545 -26
- package/dist/index.d.cts +187 -2
- package/dist/index.d.ts +187 -2
- package/dist/index.js +571 -52
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
// src/stable.ts
|
|
2
2
|
import {
|
|
3
|
-
createPublicClient as
|
|
4
|
-
createWalletClient as
|
|
5
|
-
erc20Abi as
|
|
6
|
-
http as
|
|
7
|
-
isAddress as
|
|
8
|
-
parseUnits as
|
|
3
|
+
createPublicClient as createPublicClient4,
|
|
4
|
+
createWalletClient as createWalletClient3,
|
|
5
|
+
erc20Abi as erc20Abi4,
|
|
6
|
+
http as http4,
|
|
7
|
+
isAddress as isAddress3,
|
|
8
|
+
parseUnits as parseUnits4
|
|
9
|
+
} from "viem";
|
|
10
|
+
import { stable as stableMainnet2, stableTestnet as stableTestnet2 } from "viem/chains";
|
|
11
|
+
|
|
12
|
+
// src/earn/reader.ts
|
|
13
|
+
import {
|
|
14
|
+
createPublicClient,
|
|
15
|
+
erc20Abi,
|
|
16
|
+
formatUnits,
|
|
17
|
+
http,
|
|
18
|
+
isAddress,
|
|
19
|
+
parseUnits
|
|
9
20
|
} from "viem";
|
|
10
21
|
import { stable as stableMainnet, stableTestnet } from "viem/chains";
|
|
22
|
+
import { MorphoClient } from "@morpho-org/morpho-sdk";
|
|
11
23
|
|
|
12
24
|
// src/errors.ts
|
|
13
25
|
import { BaseError } from "viem";
|
|
@@ -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,17 +99,6 @@ function formatValue(v) {
|
|
|
79
99
|
}
|
|
80
100
|
}
|
|
81
101
|
|
|
82
|
-
// src/services/lifi.ts
|
|
83
|
-
import {
|
|
84
|
-
createPublicClient,
|
|
85
|
-
createWalletClient,
|
|
86
|
-
erc20Abi,
|
|
87
|
-
formatUnits,
|
|
88
|
-
http,
|
|
89
|
-
isAddress,
|
|
90
|
-
parseUnits
|
|
91
|
-
} from "viem";
|
|
92
|
-
|
|
93
102
|
// src/helpers/chain.ts
|
|
94
103
|
import { defineChain } from "viem";
|
|
95
104
|
function makeChain(id, rpc) {
|
|
@@ -154,6 +163,156 @@ var Network = /* @__PURE__ */ ((Network2) => {
|
|
|
154
163
|
return Network2;
|
|
155
164
|
})(Network || {});
|
|
156
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 (!config.earn?.vault) {
|
|
171
|
+
throw new StableValidationError(
|
|
172
|
+
"earn.vault",
|
|
173
|
+
void 0,
|
|
174
|
+
"createStableReader: earn.vault is required"
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (!isAddress(config.address)) {
|
|
178
|
+
throw new StableValidationError(
|
|
179
|
+
"address",
|
|
180
|
+
config.address,
|
|
181
|
+
`createStableReader: invalid user address "${config.address}"`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (!isAddress(config.earn.vault)) {
|
|
185
|
+
throw new StableValidationError(
|
|
186
|
+
"earn.vault",
|
|
187
|
+
config.earn.vault,
|
|
188
|
+
`createStableReader: invalid vault address "${config.earn.vault}"`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const network = config.network ?? "mainnet" /* Mainnet */;
|
|
192
|
+
const viemChain = network === "mainnet" /* Mainnet */ ? stableMainnet : stableTestnet;
|
|
193
|
+
const rpc = (config.rpc ?? viemChain.rpcUrls.default.http[0]).replace(/\/$/, "");
|
|
194
|
+
this.chainId = viemChain.id;
|
|
195
|
+
this.vaultAddr = config.earn.vault;
|
|
196
|
+
this.userAddr = config.address;
|
|
197
|
+
const chain = makeChain(viemChain.id, rpc);
|
|
198
|
+
this.pc = createPublicClient({ chain, transport: http(rpc) });
|
|
199
|
+
this.morpho = new MorphoClient(this.pc);
|
|
200
|
+
}
|
|
201
|
+
async position() {
|
|
202
|
+
const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
|
|
203
|
+
const [vaultData, shares] = await Promise.all([
|
|
204
|
+
vault.getData(),
|
|
205
|
+
this.pc.readContract({
|
|
206
|
+
address: this.vaultAddr,
|
|
207
|
+
abi: erc20Abi,
|
|
208
|
+
functionName: "balanceOf",
|
|
209
|
+
args: [this.userAddr]
|
|
210
|
+
})
|
|
211
|
+
]);
|
|
212
|
+
const [tokenDecimals, shareDecimals] = await Promise.all([
|
|
213
|
+
this.pc.readContract({ address: vaultData.asset, abi: erc20Abi, functionName: "decimals" }),
|
|
214
|
+
this.pc.readContract({ address: this.vaultAddr, abi: erc20Abi, functionName: "decimals" })
|
|
215
|
+
]);
|
|
216
|
+
const assetsRaw = vaultData.toAssets(shares);
|
|
217
|
+
return {
|
|
218
|
+
shares,
|
|
219
|
+
sharesFormatted: Number(formatUnits(shares, shareDecimals)),
|
|
220
|
+
shareDecimals,
|
|
221
|
+
assets: Number(formatUnits(assetsRaw, tokenDecimals)),
|
|
222
|
+
tokenDecimals,
|
|
223
|
+
assetAddress: vaultData.asset
|
|
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 = params.tokenDecimals ?? await this.pc.readContract({
|
|
239
|
+
address: vaultData.asset,
|
|
240
|
+
abi: erc20Abi,
|
|
241
|
+
functionName: "decimals"
|
|
242
|
+
});
|
|
243
|
+
const requestedRaw = parseUnits(String(params.amount), tokenDecimals);
|
|
244
|
+
const availableLiquidityRaw = vaultData.assetBalance;
|
|
245
|
+
return {
|
|
246
|
+
isInstant: requestedRaw <= availableLiquidityRaw,
|
|
247
|
+
availableLiquidity: Number(formatUnits(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 = body?.data?.vaultV2ByAddress?.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
|
+
import {
|
|
307
|
+
createPublicClient as createPublicClient2,
|
|
308
|
+
createWalletClient,
|
|
309
|
+
erc20Abi as erc20Abi2,
|
|
310
|
+
formatUnits as formatUnits2,
|
|
311
|
+
http as http2,
|
|
312
|
+
isAddress as isAddress2,
|
|
313
|
+
parseUnits as parseUnits2
|
|
314
|
+
} from "viem";
|
|
315
|
+
|
|
157
316
|
// src/chains.ts
|
|
158
317
|
var CHAIN_CONFIGS = {
|
|
159
318
|
// ─── Testnet ──────────────────────────────────────────────────────────────
|
|
@@ -236,7 +395,7 @@ var LiFiService = class {
|
|
|
236
395
|
const { fromToken, toToken, amount, fromDecimals = 6, toAddress } = params;
|
|
237
396
|
const from = this._requireFrom();
|
|
238
397
|
const chain = this.config.chainId.toString();
|
|
239
|
-
const fromAmountRaw =
|
|
398
|
+
const fromAmountRaw = parseUnits2(String(amount), fromDecimals).toString();
|
|
240
399
|
const qs = new URLSearchParams({
|
|
241
400
|
fromChain: chain,
|
|
242
401
|
toChain: chain,
|
|
@@ -267,7 +426,7 @@ var LiFiService = class {
|
|
|
267
426
|
toChain: dst.id.toString(),
|
|
268
427
|
fromToken,
|
|
269
428
|
toToken,
|
|
270
|
-
fromAmount:
|
|
429
|
+
fromAmount: parseUnits2(String(amount), fromDecimals).toString(),
|
|
271
430
|
fromAddress: from,
|
|
272
431
|
...recipient && { toAddress: recipient }
|
|
273
432
|
});
|
|
@@ -286,7 +445,7 @@ var LiFiService = class {
|
|
|
286
445
|
if (!dst) {
|
|
287
446
|
throw new StableValidationError("toChain", toChain, `LI.FI bridge: unsupported destination chain "${toChain}"`);
|
|
288
447
|
}
|
|
289
|
-
const fromAmountRaw =
|
|
448
|
+
const fromAmountRaw = parseUnits2(String(amount), fromDecimals).toString();
|
|
290
449
|
const qs = new URLSearchParams({
|
|
291
450
|
fromChain: src.id.toString(),
|
|
292
451
|
toChain: dst.id.toString(),
|
|
@@ -300,10 +459,10 @@ var LiFiService = class {
|
|
|
300
459
|
return this._executeSwapQuote(quote, src.rpc, src.id);
|
|
301
460
|
}
|
|
302
461
|
_validateSwapParams({ fromToken, toToken, amount, toAddress }) {
|
|
303
|
-
if (!
|
|
462
|
+
if (!isAddress2(fromToken)) {
|
|
304
463
|
throw new StableValidationError("fromToken", fromToken, `swap: invalid fromToken address "${fromToken}"`);
|
|
305
464
|
}
|
|
306
|
-
if (!
|
|
465
|
+
if (!isAddress2(toToken)) {
|
|
307
466
|
throw new StableValidationError("toToken", toToken, `swap: invalid toToken address "${toToken}"`);
|
|
308
467
|
}
|
|
309
468
|
if (fromToken.toLowerCase() === toToken.toLowerCase()) {
|
|
@@ -312,21 +471,21 @@ var LiFiService = class {
|
|
|
312
471
|
if (!Number.isFinite(amount) || amount <= 0) {
|
|
313
472
|
throw new StableValidationError("amount", amount, "swap: amount must be a finite positive number");
|
|
314
473
|
}
|
|
315
|
-
if (toAddress !== void 0 && !
|
|
474
|
+
if (toAddress !== void 0 && !isAddress2(toAddress)) {
|
|
316
475
|
throw new StableValidationError("toAddress", toAddress, `swap: invalid toAddress "${toAddress}"`);
|
|
317
476
|
}
|
|
318
477
|
}
|
|
319
478
|
_validateBridgeParams({ fromChain, toChain, fromToken, toToken, amount, recipient }) {
|
|
320
|
-
if (!
|
|
479
|
+
if (!isAddress2(fromToken)) {
|
|
321
480
|
throw new StableValidationError("fromToken", fromToken, `LI.FI bridge: invalid fromToken address "${fromToken}"`);
|
|
322
481
|
}
|
|
323
|
-
if (!
|
|
482
|
+
if (!isAddress2(toToken)) {
|
|
324
483
|
throw new StableValidationError("toToken", toToken, `LI.FI bridge: invalid toToken address "${toToken}"`);
|
|
325
484
|
}
|
|
326
485
|
if (!Number.isFinite(amount) || amount <= 0) {
|
|
327
486
|
throw new StableValidationError("amount", amount, "LI.FI bridge: amount must be a finite positive number");
|
|
328
487
|
}
|
|
329
|
-
if (recipient !== void 0 && !
|
|
488
|
+
if (recipient !== void 0 && !isAddress2(recipient)) {
|
|
330
489
|
throw new StableValidationError("recipient", recipient, `LI.FI bridge: invalid recipient address "${recipient}"`);
|
|
331
490
|
}
|
|
332
491
|
if (fromChain === toChain) {
|
|
@@ -394,7 +553,7 @@ var LiFiService = class {
|
|
|
394
553
|
}
|
|
395
554
|
_toSwapQuote(quote) {
|
|
396
555
|
return {
|
|
397
|
-
toAmount: Number(
|
|
556
|
+
toAmount: Number(formatUnits2(BigInt(quote.estimate.toAmount), quote.action.toToken.decimals)),
|
|
398
557
|
fromAmount: BigInt(quote.estimate.fromAmount),
|
|
399
558
|
fromToken: quote.action.fromToken.address,
|
|
400
559
|
approvalAddress: quote.estimate.approvalAddress,
|
|
@@ -417,7 +576,7 @@ var LiFiService = class {
|
|
|
417
576
|
const wc = this.config.walletClient ?? createWalletClient({
|
|
418
577
|
account: this.config.account,
|
|
419
578
|
chain: srcChain,
|
|
420
|
-
transport: this.config.transport ??
|
|
579
|
+
transport: this.config.transport ?? http2(srcRpc)
|
|
421
580
|
});
|
|
422
581
|
try {
|
|
423
582
|
await wc.switchChain({ id: srcChainId });
|
|
@@ -454,10 +613,10 @@ var LiFiService = class {
|
|
|
454
613
|
const tokenAddr = quote.fromToken;
|
|
455
614
|
const isNative = tokenAddr.toLowerCase() === "0x0000000000000000000000000000000000000000" || tokenAddr.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
456
615
|
if (isNative || !quote.approvalAddress) return;
|
|
457
|
-
const pc =
|
|
616
|
+
const pc = createPublicClient2({ chain: srcChain, transport: http2(srcRpc) });
|
|
458
617
|
const allowance = await pc.readContract({
|
|
459
618
|
address: tokenAddr,
|
|
460
|
-
abi:
|
|
619
|
+
abi: erc20Abi2,
|
|
461
620
|
functionName: "allowance",
|
|
462
621
|
args: [from, quote.approvalAddress]
|
|
463
622
|
});
|
|
@@ -466,7 +625,7 @@ var LiFiService = class {
|
|
|
466
625
|
try {
|
|
467
626
|
approveHash = await wc.writeContract({
|
|
468
627
|
address: tokenAddr,
|
|
469
|
-
abi:
|
|
628
|
+
abi: erc20Abi2,
|
|
470
629
|
functionName: "approve",
|
|
471
630
|
args: [quote.approvalAddress, quote.fromAmount],
|
|
472
631
|
account: wc.account,
|
|
@@ -502,15 +661,343 @@ function isLiFiQuoteShape(v) {
|
|
|
502
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);
|
|
503
662
|
}
|
|
504
663
|
|
|
664
|
+
// src/earn/morpho.ts
|
|
665
|
+
import {
|
|
666
|
+
createPublicClient as createPublicClient3,
|
|
667
|
+
createWalletClient as createWalletClient2,
|
|
668
|
+
custom,
|
|
669
|
+
erc20Abi as erc20Abi3,
|
|
670
|
+
http as http3,
|
|
671
|
+
parseUnits as parseUnits3
|
|
672
|
+
} from "viem";
|
|
673
|
+
import {
|
|
674
|
+
MorphoClient as MorphoClient2,
|
|
675
|
+
isRequirementApproval,
|
|
676
|
+
isRequirementSignature
|
|
677
|
+
} from "@morpho-org/morpho-sdk";
|
|
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 = config.walletClient ?? createWalletClient2({
|
|
684
|
+
account: config.account,
|
|
685
|
+
chain: this.chain,
|
|
686
|
+
transport: config.transport ?? http3(config.rpc)
|
|
687
|
+
});
|
|
688
|
+
const morphoWc = this.wc.chain?.id === config.chainId ? this.wc : createWalletClient2({
|
|
689
|
+
account: this.wc.account,
|
|
690
|
+
chain: this.chain,
|
|
691
|
+
transport: custom({ request: this.wc.request.bind(this.wc) })
|
|
692
|
+
});
|
|
693
|
+
this.morpho = new MorphoClient2(morphoWc, { supportSignature: true });
|
|
694
|
+
this.pc = createPublicClient3({ chain: this.chain, transport: http3(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 = 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 = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn deposit");
|
|
752
|
+
const assetsRaw = parseUnits3(String(amount), decimals);
|
|
753
|
+
const depositAction = vault.deposit({
|
|
754
|
+
amount: assetsRaw,
|
|
755
|
+
userAddress: from,
|
|
756
|
+
accrualVault: vaultData,
|
|
757
|
+
slippageTolerance: slippageTolerance !== void 0 ? parseUnits3(String(slippageTolerance), 18) : void 0
|
|
758
|
+
});
|
|
759
|
+
const requirements = await depositAction.getRequirements();
|
|
760
|
+
let sig;
|
|
761
|
+
for (const req of requirements) {
|
|
762
|
+
if (isRequirementApproval(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 (isRequirementSignature(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 = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn withdraw");
|
|
798
|
+
const assetsRaw = parseUnits3(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: erc20Abi3, functionName: "decimals" });
|
|
821
|
+
} catch {
|
|
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 = parseUnits3(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: parseUnits3(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: erc20Abi3, functionName: "decimals" });
|
|
882
|
+
} catch {
|
|
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: parseUnits3(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: parseUnits3(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 = this.wc.account?.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: erc20Abi3, functionName: "decimals" })
|
|
963
|
+
);
|
|
964
|
+
} catch {
|
|
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
|
+
|
|
505
992
|
// src/stable.ts
|
|
506
993
|
function validateTransferParams({ to, amount, token, tokenDecimals }) {
|
|
507
|
-
if (!
|
|
994
|
+
if (!isAddress3(to)) {
|
|
508
995
|
throw new StableValidationError("to", to, `transfer: invalid recipient address "${to}"`);
|
|
509
996
|
}
|
|
510
997
|
if (!Number.isFinite(amount) || amount <= 0) {
|
|
511
998
|
throw new StableValidationError("amount", amount, "transfer: amount must be a finite positive number");
|
|
512
999
|
}
|
|
513
|
-
if (token !== void 0 && !
|
|
1000
|
+
if (token !== void 0 && !isAddress3(token)) {
|
|
514
1001
|
throw new StableValidationError("token", token, `transfer: invalid token address "${token}"`);
|
|
515
1002
|
}
|
|
516
1003
|
if (tokenDecimals !== void 0 && (!Number.isInteger(tokenDecimals) || tokenDecimals < 0 || tokenDecimals > 77)) {
|
|
@@ -520,7 +1007,7 @@ function validateTransferParams({ to, amount, token, tokenDecimals }) {
|
|
|
520
1007
|
async function resolveTokenDecimals(publicClient, token, tokenDecimals) {
|
|
521
1008
|
if (tokenDecimals !== void 0) return tokenDecimals;
|
|
522
1009
|
try {
|
|
523
|
-
return await publicClient.readContract({ address: token, abi:
|
|
1010
|
+
return await publicClient.readContract({ address: token, abi: erc20Abi4, functionName: "decimals" });
|
|
524
1011
|
} catch {
|
|
525
1012
|
throw new StableValidationError(
|
|
526
1013
|
"tokenDecimals",
|
|
@@ -541,9 +1028,9 @@ async function executeTokenTransfer(wc, publicClient, token, to, amount, tokenDe
|
|
|
541
1028
|
const decimals = await resolveTokenDecimals(publicClient, token, tokenDecimals);
|
|
542
1029
|
return wc.writeContract({
|
|
543
1030
|
address: token,
|
|
544
|
-
abi:
|
|
1031
|
+
abi: erc20Abi4,
|
|
545
1032
|
functionName: "transfer",
|
|
546
|
-
args: [to,
|
|
1033
|
+
args: [to, parseUnits4(String(amount), decimals)],
|
|
547
1034
|
account: acc,
|
|
548
1035
|
chain
|
|
549
1036
|
});
|
|
@@ -553,20 +1040,32 @@ function createStable({
|
|
|
553
1040
|
rpc: rpcOverride,
|
|
554
1041
|
account,
|
|
555
1042
|
transport,
|
|
556
|
-
walletClient
|
|
1043
|
+
walletClient,
|
|
1044
|
+
earn: earnConfig
|
|
557
1045
|
}) {
|
|
558
|
-
const chain = network === "mainnet" /* Mainnet */ ?
|
|
1046
|
+
const chain = network === "mainnet" /* Mainnet */ ? stableMainnet2 : stableTestnet2;
|
|
559
1047
|
const rpc = (rpcOverride ?? chain.rpcUrls.default.http[0]).replace(/\/$/, "");
|
|
560
1048
|
const signerConfig = { account, transport, walletClient };
|
|
561
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
|
+
}
|
|
562
1061
|
async function transfer(params) {
|
|
563
1062
|
validateTransferParams(params);
|
|
564
1063
|
const { from, to, amount, token, tokenDecimals } = params;
|
|
565
1064
|
const acc = walletClient?.account ?? account ?? from;
|
|
566
|
-
const wc = walletClient ??
|
|
1065
|
+
const wc = walletClient ?? createWalletClient3({
|
|
567
1066
|
account: acc,
|
|
568
1067
|
chain,
|
|
569
|
-
transport: transport ??
|
|
1068
|
+
transport: transport ?? http4(rpc)
|
|
570
1069
|
});
|
|
571
1070
|
try {
|
|
572
1071
|
await wc.switchChain({ id: chain.id });
|
|
@@ -578,8 +1077,8 @@ function createStable({
|
|
|
578
1077
|
cause: err
|
|
579
1078
|
});
|
|
580
1079
|
}
|
|
581
|
-
const publicClient =
|
|
582
|
-
const hash = token !== void 0 ? await executeTokenTransfer(wc, publicClient, token, to, amount, tokenDecimals, acc, chain) : await wc.sendTransaction({ account: acc, chain, to, value:
|
|
1080
|
+
const publicClient = createPublicClient4({ chain, transport: http4(rpc) });
|
|
1081
|
+
const hash = token !== void 0 ? await executeTokenTransfer(wc, publicClient, token, to, amount, tokenDecimals, acc, chain) : await wc.sendTransaction({ account: acc, chain, to, value: parseUnits4(String(amount), 18) });
|
|
583
1082
|
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
584
1083
|
if (receipt.status === "reverted") {
|
|
585
1084
|
throw new StableTransactionError({
|
|
@@ -591,28 +1090,48 @@ function createStable({
|
|
|
591
1090
|
}
|
|
592
1091
|
return { txHash: receipt.transactionHash };
|
|
593
1092
|
}
|
|
594
|
-
function quoteBridge(params) {
|
|
595
|
-
return lifi.quoteBridge(params);
|
|
596
|
-
}
|
|
597
|
-
function bridge(params) {
|
|
598
|
-
return lifi.bridge(params);
|
|
599
|
-
}
|
|
600
1093
|
return {
|
|
601
1094
|
transfer,
|
|
602
|
-
quoteBridge,
|
|
603
|
-
bridge,
|
|
1095
|
+
quoteBridge: (params) => lifi.quoteBridge(params),
|
|
1096
|
+
bridge: (params) => lifi.bridge(params),
|
|
604
1097
|
quoteSwap: (params) => lifi.quoteSwap(params),
|
|
605
|
-
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
|
+
}
|
|
606
1117
|
};
|
|
607
1118
|
}
|
|
1119
|
+
|
|
1120
|
+
// src/constants.ts
|
|
1121
|
+
var STABLE_USDT_ADDRESS = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
|
|
1122
|
+
var STABLE_VAULT_ADDRESS = "0x461eF73F640456138242398a39AB48a557A49293";
|
|
608
1123
|
export {
|
|
609
1124
|
CHAIN_CONFIGS,
|
|
610
1125
|
Chain,
|
|
611
1126
|
Network,
|
|
1127
|
+
STABLE_USDT_ADDRESS,
|
|
1128
|
+
STABLE_VAULT_ADDRESS,
|
|
1129
|
+
StableBulkError,
|
|
612
1130
|
StableError,
|
|
613
1131
|
StableNetworkError,
|
|
614
1132
|
StableQuoteError,
|
|
615
1133
|
StableTransactionError,
|
|
616
1134
|
StableValidationError,
|
|
617
|
-
createStable
|
|
1135
|
+
createStable,
|
|
1136
|
+
createStableReader
|
|
618
1137
|
};
|