@stablechain/sdk 0.1.1 → 1.0.1
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 +612 -26
- package/dist/index.d.cts +225 -2
- package/dist/index.d.ts +225 -2
- package/dist/index.js +638 -52
- package/package.json +5 -2
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,17 +99,6 @@ function formatValue(v) {
|
|
|
79
99
|
}
|
|
80
100
|
}
|
|
81
101
|
|
|
82
|
-
// src/services/lifi.ts
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
102
|
// src/helpers/chain.ts
|
|
94
103
|
|
|
95
104
|
function makeChain(id, rpc) {
|
|
@@ -154,6 +163,177 @@ 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 (!_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 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
|
|
236
|
+
};
|
|
237
|
+
}
|
|
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
|
+
};
|
|
253
|
+
}
|
|
254
|
+
async _fetchNetApy() {
|
|
255
|
+
const { apy } = await this._fetchYield();
|
|
256
|
+
return apy;
|
|
257
|
+
}
|
|
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
|
|
296
|
+
);
|
|
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
|
+
});
|
|
304
|
+
}
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
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
|
+
);
|
|
314
|
+
}
|
|
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
|
+
};
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// src/services/lifi.ts
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
|
|
157
337
|
// src/chains.ts
|
|
158
338
|
var CHAIN_CONFIGS = {
|
|
159
339
|
// ─── Testnet ──────────────────────────────────────────────────────────────
|
|
@@ -257,7 +437,7 @@ var LiFiService = class {
|
|
|
257
437
|
async quoteBridge(params) {
|
|
258
438
|
this._validateBridgeParams(params);
|
|
259
439
|
const { fromChain, toChain, fromToken, toToken, amount, fromDecimals = 6, recipient } = params;
|
|
260
|
-
const from = _nullishCoalesce(_nullishCoalesce(_optionalChain([this, 'access',
|
|
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"));
|
|
261
441
|
const src = CHAIN_CONFIGS[fromChain];
|
|
262
442
|
const dst = CHAIN_CONFIGS[toChain];
|
|
263
443
|
if (!src) throw new StableValidationError("fromChain", fromChain, `LI.FI bridge: unsupported source chain "${fromChain}"`);
|
|
@@ -334,7 +514,7 @@ var LiFiService = class {
|
|
|
334
514
|
}
|
|
335
515
|
}
|
|
336
516
|
_requireFrom() {
|
|
337
|
-
const from = _nullishCoalesce(_optionalChain([this, 'access',
|
|
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])));
|
|
338
518
|
if (!from) {
|
|
339
519
|
throw new StableValidationError(
|
|
340
520
|
"signer",
|
|
@@ -502,6 +682,378 @@ function isLiFiQuoteShape(v) {
|
|
|
502
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);
|
|
503
683
|
}
|
|
504
684
|
|
|
685
|
+
// src/earn/morpho.ts
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
var MorphoService = class {
|
|
701
|
+
constructor(config) {
|
|
702
|
+
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
|
+
}
|
|
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");
|
|
722
|
+
}
|
|
723
|
+
return this._withIdempotency(params.idempotencyKey, () => this._deposit(params));
|
|
724
|
+
}
|
|
725
|
+
async bulkDeposit(items, options = {}) {
|
|
726
|
+
const { idempotencyKey, stopOnError = false } = options;
|
|
727
|
+
const run = async () => {
|
|
728
|
+
const results = [];
|
|
729
|
+
for (const item of items) {
|
|
730
|
+
try {
|
|
731
|
+
const result = await this.deposit(item);
|
|
732
|
+
results.push({ status: "fulfilled", txHash: result.txHash, vault: item.vault, idempotencyKey: item.idempotencyKey });
|
|
733
|
+
} catch (err) {
|
|
734
|
+
const entry = { status: "rejected", error: String(err), vault: item.vault, idempotencyKey: item.idempotencyKey };
|
|
735
|
+
results.push(entry);
|
|
736
|
+
if (stopOnError) {
|
|
737
|
+
const succeeded2 = results.filter((r) => r.status === "fulfilled").length;
|
|
738
|
+
throw new StableBulkError({
|
|
739
|
+
shortMessage: "bulkDeposit halted on first failure",
|
|
740
|
+
results,
|
|
741
|
+
succeeded: succeeded2,
|
|
742
|
+
failed: results.length - succeeded2
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
const succeeded = results.filter((r) => r.status === "fulfilled").length;
|
|
748
|
+
return { results, succeeded, failed: results.length - succeeded, idempotencyKey };
|
|
749
|
+
};
|
|
750
|
+
return this._withIdempotency(idempotencyKey, run);
|
|
751
|
+
}
|
|
752
|
+
async bulkWithdraw(items, options = {}) {
|
|
753
|
+
const { idempotencyKey, stopOnError = false } = options;
|
|
754
|
+
const run = async () => {
|
|
755
|
+
const results = [];
|
|
756
|
+
for (const item of items) {
|
|
757
|
+
try {
|
|
758
|
+
const result = await this.withdraw(item);
|
|
759
|
+
results.push({ status: "fulfilled", txHash: result.txHash, idempotencyKey: item.idempotencyKey });
|
|
760
|
+
} catch (err) {
|
|
761
|
+
const entry = { status: "rejected", error: String(err), idempotencyKey: item.idempotencyKey };
|
|
762
|
+
results.push(entry);
|
|
763
|
+
if (stopOnError) {
|
|
764
|
+
const succeeded2 = results.filter((r) => r.status === "fulfilled").length;
|
|
765
|
+
throw new StableBulkError({
|
|
766
|
+
shortMessage: "bulkWithdraw halted on first failure",
|
|
767
|
+
results,
|
|
768
|
+
succeeded: succeeded2,
|
|
769
|
+
failed: results.length - succeeded2
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const succeeded = results.filter((r) => r.status === "fulfilled").length;
|
|
775
|
+
return { results, succeeded, failed: results.length - succeeded, idempotencyKey };
|
|
776
|
+
};
|
|
777
|
+
return this._withIdempotency(idempotencyKey, run);
|
|
778
|
+
}
|
|
779
|
+
withdraw(params) {
|
|
780
|
+
this._validateAmount(params.amount, "earn withdraw");
|
|
781
|
+
return this._withIdempotency(params.idempotencyKey, () => this._withdraw(params));
|
|
782
|
+
}
|
|
783
|
+
redeem(params) {
|
|
784
|
+
this._validateShares(params.shares, "earn redeem");
|
|
785
|
+
return this._withIdempotency(params.idempotencyKey, () => this._redeem(params));
|
|
786
|
+
}
|
|
787
|
+
forceWithdraw(params) {
|
|
788
|
+
this._validateAmount(params.amount, "earn forceWithdraw");
|
|
789
|
+
return this._withIdempotency(params.idempotencyKey, () => this._forceWithdraw(params));
|
|
790
|
+
}
|
|
791
|
+
forceRedeem(params) {
|
|
792
|
+
this._validateShares(params.shares, "earn forceRedeem");
|
|
793
|
+
return this._withIdempotency(params.idempotencyKey, () => this._forceRedeem(params));
|
|
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
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const tx = depositAction.buildTx(sig);
|
|
843
|
+
const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
|
|
844
|
+
const receipt = await this.pc.waitForTransactionReceipt({ hash });
|
|
845
|
+
if (receipt.status === "reverted") {
|
|
846
|
+
throw new StableTransactionError({
|
|
847
|
+
phase: "receipt",
|
|
848
|
+
shortMessage: "earn deposit: vault deposit reverted on-chain",
|
|
849
|
+
txHash: receipt.transactionHash,
|
|
850
|
+
chainId: this.config.chainId
|
|
851
|
+
});
|
|
852
|
+
}
|
|
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") {
|
|
868
|
+
throw new StableTransactionError({
|
|
869
|
+
phase: "receipt",
|
|
870
|
+
shortMessage: "earn withdraw: vault withdraw reverted on-chain",
|
|
871
|
+
txHash: receipt.transactionHash,
|
|
872
|
+
chainId: this.config.chainId
|
|
873
|
+
});
|
|
874
|
+
}
|
|
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 });
|
|
899
|
+
if (receipt.status === "reverted") {
|
|
900
|
+
throw new StableTransactionError({
|
|
901
|
+
phase: "receipt",
|
|
902
|
+
shortMessage: "earn redeem: vault redeem reverted on-chain",
|
|
903
|
+
txHash: receipt.transactionHash,
|
|
904
|
+
chainId: this.config.chainId
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
return { txHash: receipt.transactionHash, idempotencyKey };
|
|
908
|
+
}
|
|
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");
|
|
919
|
+
}
|
|
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
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
return { txHash: receipt.transactionHash, idempotencyKey };
|
|
936
|
+
}
|
|
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`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
let assetDec = tokenDecimals;
|
|
956
|
+
if (assetDec === void 0) {
|
|
957
|
+
const vaultData = await vault.getData();
|
|
958
|
+
assetDec = await this._resolveDecimals(vaultData.asset, "earn forceRedeem");
|
|
959
|
+
}
|
|
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
|
+
});
|
|
974
|
+
}
|
|
975
|
+
return { txHash: receipt.transactionHash, idempotencyKey };
|
|
976
|
+
}
|
|
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
|
+
}));
|
|
985
|
+
}
|
|
986
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
987
|
+
_requireFrom() {
|
|
988
|
+
const from = _optionalChain([this, 'access', _23 => _23.wc, 'access', _24 => _24.account, 'optionalAccess', _25 => _25.address]);
|
|
989
|
+
if (!from) {
|
|
990
|
+
throw new StableValidationError(
|
|
991
|
+
"signer",
|
|
992
|
+
void 0,
|
|
993
|
+
"earn: requires a signer \u2014 set walletClient or account in StableConfig"
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
return from;
|
|
997
|
+
}
|
|
998
|
+
async _switchChain() {
|
|
999
|
+
try {
|
|
1000
|
+
await this.wc.switchChain({ id: this.config.chainId });
|
|
1001
|
+
} 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,
|
|
1007
|
+
cause: err
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
async _sendTx(to, data, value, from) {
|
|
1013
|
+
try {
|
|
1014
|
+
return await this.wc.sendTransaction({ to, data, value, account: from, chain: this.chain });
|
|
1015
|
+
} catch (err) {
|
|
1016
|
+
throw new StableTransactionError({
|
|
1017
|
+
phase: "send",
|
|
1018
|
+
shortMessage: "earn: transaction send failed",
|
|
1019
|
+
chainId: this.config.chainId,
|
|
1020
|
+
cause: err
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
async _resolveDecimals(asset, op) {
|
|
1025
|
+
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
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
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`);
|
|
1048
|
+
}
|
|
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`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
505
1057
|
// src/stable.ts
|
|
506
1058
|
function validateTransferParams({ to, amount, token, tokenDecimals }) {
|
|
507
1059
|
if (!_viem.isAddress.call(void 0, to)) {
|
|
@@ -521,7 +1073,7 @@ async function resolveTokenDecimals(publicClient, token, tokenDecimals) {
|
|
|
521
1073
|
if (tokenDecimals !== void 0) return tokenDecimals;
|
|
522
1074
|
try {
|
|
523
1075
|
return await publicClient.readContract({ address: token, abi: _viem.erc20Abi, functionName: "decimals" });
|
|
524
|
-
} catch (
|
|
1076
|
+
} catch (e5) {
|
|
525
1077
|
throw new StableValidationError(
|
|
526
1078
|
"tokenDecimals",
|
|
527
1079
|
void 0,
|
|
@@ -553,16 +1105,28 @@ function createStable({
|
|
|
553
1105
|
rpc: rpcOverride,
|
|
554
1106
|
account,
|
|
555
1107
|
transport,
|
|
556
|
-
walletClient
|
|
1108
|
+
walletClient,
|
|
1109
|
+
earn: earnConfig
|
|
557
1110
|
}) {
|
|
558
1111
|
const chain = network === "mainnet" /* Mainnet */ ? _chains.stable : _chains.stableTestnet;
|
|
559
1112
|
const rpc = (_nullishCoalesce(rpcOverride, () => ( chain.rpcUrls.default.http[0]))).replace(/\/$/, "");
|
|
560
1113
|
const signerConfig = { account, transport, walletClient };
|
|
561
1114
|
const lifi = new LiFiService({ ...signerConfig, rpc, chainId: chain.id });
|
|
1115
|
+
const morpho = earnConfig ? new MorphoService({ ...signerConfig, rpc, chainId: chain.id, vault: earnConfig.vault }) : null;
|
|
1116
|
+
function requireEarn() {
|
|
1117
|
+
if (!morpho) {
|
|
1118
|
+
throw new StableValidationError(
|
|
1119
|
+
"earn",
|
|
1120
|
+
void 0,
|
|
1121
|
+
"stable.earn.*: pass earn: { vault: '0x...' } to createStable() to enable earn operations"
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
return morpho;
|
|
1125
|
+
}
|
|
562
1126
|
async function transfer(params) {
|
|
563
1127
|
validateTransferParams(params);
|
|
564
1128
|
const { from, to, amount, token, tokenDecimals } = params;
|
|
565
|
-
const acc = _nullishCoalesce(_nullishCoalesce(_optionalChain([walletClient, 'optionalAccess',
|
|
1129
|
+
const acc = _nullishCoalesce(_nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _26 => _26.account]), () => ( account)), () => ( from));
|
|
566
1130
|
const wc = _nullishCoalesce(walletClient, () => ( _viem.createWalletClient.call(void 0, {
|
|
567
1131
|
account: acc,
|
|
568
1132
|
chain,
|
|
@@ -591,20 +1155,42 @@ function createStable({
|
|
|
591
1155
|
}
|
|
592
1156
|
return { txHash: receipt.transactionHash };
|
|
593
1157
|
}
|
|
594
|
-
function quoteBridge(params) {
|
|
595
|
-
return lifi.quoteBridge(params);
|
|
596
|
-
}
|
|
597
|
-
function bridge(params) {
|
|
598
|
-
return lifi.bridge(params);
|
|
599
|
-
}
|
|
600
1158
|
return {
|
|
601
1159
|
transfer,
|
|
602
|
-
quoteBridge,
|
|
603
|
-
bridge,
|
|
1160
|
+
quoteBridge: (params) => lifi.quoteBridge(params),
|
|
1161
|
+
bridge: (params) => lifi.bridge(params),
|
|
604
1162
|
quoteSwap: (params) => lifi.quoteSwap(params),
|
|
605
|
-
swap: (params) => lifi.swap(params)
|
|
1163
|
+
swap: (params) => lifi.swap(params),
|
|
1164
|
+
earn: {
|
|
1165
|
+
deposit: (params) => requireEarn().deposit(params),
|
|
1166
|
+
bulkDeposit: (items, options) => requireEarn().bulkDeposit(items, options),
|
|
1167
|
+
withdraw: (params) => requireEarn().withdraw(params),
|
|
1168
|
+
bulkWithdraw: (items, options) => requireEarn().bulkWithdraw(items, options),
|
|
1169
|
+
redeem: (params) => requireEarn().redeem(params),
|
|
1170
|
+
forceWithdraw: (params) => requireEarn().forceWithdraw(params),
|
|
1171
|
+
forceRedeem: (params) => requireEarn().forceRedeem(params)
|
|
1172
|
+
}
|
|
606
1173
|
};
|
|
607
1174
|
}
|
|
1175
|
+
function createStableReader(config) {
|
|
1176
|
+
const reader = new EarnReader(config);
|
|
1177
|
+
return {
|
|
1178
|
+
earn: {
|
|
1179
|
+
getYield: () => reader.getYield(),
|
|
1180
|
+
position: () => reader.position(),
|
|
1181
|
+
preview: (params) => reader.preview(params),
|
|
1182
|
+
withdrawability: (params) => reader.withdrawability(params)
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/constants.ts
|
|
1188
|
+
var STABLE_USDT_ADDRESS = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
|
|
1189
|
+
var STABLE_VAULT_ADDRESS = "0x461eF73F640456138242398a39AB48a557A49293";
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
|
|
608
1194
|
|
|
609
1195
|
|
|
610
1196
|
|
|
@@ -615,4 +1201,4 @@ function createStable({
|
|
|
615
1201
|
|
|
616
1202
|
|
|
617
1203
|
|
|
618
|
-
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;
|
|
1204
|
+
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;
|