@stablechain/sdk 1.0.0 → 1.0.2
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 +1134 -738
- package/dist/index.d.cts +39 -1
- package/dist/index.d.ts +39 -1
- package/dist/index.js +1123 -727
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
// src/stable.ts
|
|
2
2
|
import {
|
|
3
|
+
http as http4,
|
|
3
4
|
createPublicClient as createPublicClient4,
|
|
4
5
|
createWalletClient as createWalletClient3,
|
|
5
6
|
erc20Abi as erc20Abi4,
|
|
6
|
-
http as http4,
|
|
7
7
|
isAddress as isAddress3,
|
|
8
8
|
parseUnits as parseUnits4
|
|
9
9
|
} from "viem";
|
|
10
10
|
import { stable as stableMainnet2, stableTestnet as stableTestnet2 } from "viem/chains";
|
|
11
11
|
|
|
12
|
-
// src/earn/
|
|
12
|
+
// src/earn/morpho.ts
|
|
13
|
+
import {
|
|
14
|
+
MorphoClient,
|
|
15
|
+
isRequirementApproval,
|
|
16
|
+
isRequirementSignature
|
|
17
|
+
} from "@morpho-org/morpho-sdk";
|
|
13
18
|
import {
|
|
19
|
+
http,
|
|
14
20
|
createPublicClient,
|
|
21
|
+
createWalletClient,
|
|
22
|
+
custom,
|
|
15
23
|
erc20Abi,
|
|
16
24
|
formatUnits,
|
|
17
|
-
http,
|
|
18
|
-
isAddress,
|
|
19
25
|
parseUnits
|
|
20
26
|
} from "viem";
|
|
21
|
-
import { stable as stableMainnet, stableTestnet } from "viem/chains";
|
|
22
|
-
import { MorphoClient } from "@morpho-org/morpho-sdk";
|
|
23
27
|
|
|
24
28
|
// src/errors.ts
|
|
25
29
|
import { BaseError } from "viem";
|
|
@@ -39,7 +43,8 @@ var StableValidationError = class extends StableError {
|
|
|
39
43
|
var StableQuoteError = class extends StableError {
|
|
40
44
|
constructor(args) {
|
|
41
45
|
const meta = [`Provider: ${args.provider}`];
|
|
42
|
-
if (args.httpStatus !== void 0)
|
|
46
|
+
if (args.httpStatus !== void 0)
|
|
47
|
+
meta.push(`HTTP status: ${args.httpStatus}`);
|
|
43
48
|
if (args.providerCode) meta.push(`Provider code: ${args.providerCode}`);
|
|
44
49
|
super(args.shortMessage, {
|
|
45
50
|
name: "StableQuoteError",
|
|
@@ -142,175 +147,711 @@ async function retryWithBackoff(fn, isRetryable = isRetryableError, opts = {}) {
|
|
|
142
147
|
throw lastErr;
|
|
143
148
|
}
|
|
144
149
|
|
|
145
|
-
// src/
|
|
146
|
-
var
|
|
147
|
-
Chain2["Sepolia"] = "sepolia";
|
|
148
|
-
Chain2["StableTestnet"] = "stable-testnet";
|
|
149
|
-
Chain2["Stable"] = "stable";
|
|
150
|
-
Chain2["Ethereum"] = "ethereum";
|
|
151
|
-
Chain2["Arbitrum"] = "arbitrum";
|
|
152
|
-
Chain2["Ink"] = "ink";
|
|
153
|
-
Chain2["Bera"] = "bera";
|
|
154
|
-
Chain2["MegaETH"] = "megaeth";
|
|
155
|
-
Chain2["Base"] = "base";
|
|
156
|
-
Chain2["BSC"] = "bsc";
|
|
157
|
-
Chain2["HyperEVM"] = "hyperevm";
|
|
158
|
-
return Chain2;
|
|
159
|
-
})(Chain || {});
|
|
160
|
-
var Network = /* @__PURE__ */ ((Network2) => {
|
|
161
|
-
Network2["Mainnet"] = "mainnet";
|
|
162
|
-
Network2["Testnet"] = "testnet";
|
|
163
|
-
return Network2;
|
|
164
|
-
})(Network || {});
|
|
165
|
-
|
|
166
|
-
// src/earn/reader.ts
|
|
167
|
-
var MORPHO_API_URL = "https://blue-api.morpho.org/graphql";
|
|
168
|
-
var EarnReader = class {
|
|
150
|
+
// src/earn/morpho.ts
|
|
151
|
+
var MorphoService = class {
|
|
169
152
|
constructor(config) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
153
|
+
this.config = config;
|
|
154
|
+
this.inFlight = /* @__PURE__ */ new Map();
|
|
155
|
+
this.chain = makeChain(config.chainId, config.rpc);
|
|
156
|
+
this.wc = config.walletClient ?? createWalletClient({
|
|
157
|
+
account: config.account,
|
|
158
|
+
chain: this.chain,
|
|
159
|
+
transport: config.transport ?? http(config.rpc)
|
|
160
|
+
});
|
|
161
|
+
const morphoWc = this.wc.chain?.id === config.chainId ? this.wc : createWalletClient({
|
|
162
|
+
account: this.wc.account,
|
|
163
|
+
chain: this.chain,
|
|
164
|
+
transport: custom({
|
|
165
|
+
request: this.wc.request.bind(this.wc)
|
|
166
|
+
})
|
|
167
|
+
});
|
|
168
|
+
this.morpho = new MorphoClient(morphoWc, { supportSignature: true });
|
|
169
|
+
this.pc = createPublicClient({
|
|
170
|
+
chain: this.chain,
|
|
171
|
+
transport: http(config.rpc)
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
deposit(params) {
|
|
175
|
+
this._validateAmount(params.amount, "earn deposit");
|
|
176
|
+
if (params.depositBuffer !== void 0 && (params.depositBuffer <= 0 || params.depositBuffer > 1)) {
|
|
185
177
|
throw new StableValidationError(
|
|
186
|
-
"
|
|
187
|
-
|
|
188
|
-
|
|
178
|
+
"depositBuffer",
|
|
179
|
+
params.depositBuffer,
|
|
180
|
+
"earn deposit: depositBuffer must be > 0 and \u2264 1"
|
|
189
181
|
);
|
|
190
182
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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);
|
|
183
|
+
return this._withIdempotency(
|
|
184
|
+
params.idempotencyKey,
|
|
185
|
+
() => this._deposit(params)
|
|
186
|
+
);
|
|
200
187
|
}
|
|
201
|
-
async
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
188
|
+
async bulkDeposit(items, options = {}) {
|
|
189
|
+
const { idempotencyKey, stopOnError = false } = options;
|
|
190
|
+
const run = async () => {
|
|
191
|
+
const results = [];
|
|
192
|
+
for (const item of items) {
|
|
193
|
+
try {
|
|
194
|
+
const result = await this.deposit(item);
|
|
195
|
+
results.push({
|
|
196
|
+
status: "fulfilled",
|
|
197
|
+
txHash: result.txHash,
|
|
198
|
+
vault: item.vault,
|
|
199
|
+
idempotencyKey: item.idempotencyKey
|
|
200
|
+
});
|
|
201
|
+
} catch (err) {
|
|
202
|
+
const entry = {
|
|
203
|
+
status: "rejected",
|
|
204
|
+
error: String(err),
|
|
205
|
+
vault: item.vault,
|
|
206
|
+
idempotencyKey: item.idempotencyKey
|
|
207
|
+
};
|
|
208
|
+
results.push(entry);
|
|
209
|
+
if (stopOnError) {
|
|
210
|
+
const succeeded2 = results.filter(
|
|
211
|
+
(r) => r.status === "fulfilled"
|
|
212
|
+
).length;
|
|
213
|
+
throw new StableBulkError({
|
|
214
|
+
shortMessage: "bulkDeposit halted on first failure",
|
|
215
|
+
results,
|
|
216
|
+
succeeded: succeeded2,
|
|
217
|
+
failed: results.length - succeeded2
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const succeeded = results.filter(
|
|
223
|
+
(r) => r.status === "fulfilled"
|
|
224
|
+
).length;
|
|
225
|
+
return {
|
|
226
|
+
results,
|
|
227
|
+
succeeded,
|
|
228
|
+
failed: results.length - succeeded,
|
|
229
|
+
idempotencyKey
|
|
230
|
+
};
|
|
224
231
|
};
|
|
232
|
+
return this._withIdempotency(idempotencyKey, run);
|
|
225
233
|
}
|
|
226
|
-
async
|
|
227
|
-
const {
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
234
|
+
async bulkWithdraw(items, options = {}) {
|
|
235
|
+
const { idempotencyKey, stopOnError = false } = options;
|
|
236
|
+
const run = async () => {
|
|
237
|
+
const results = [];
|
|
238
|
+
for (const item of items) {
|
|
239
|
+
try {
|
|
240
|
+
const result = await this.withdraw(item);
|
|
241
|
+
results.push({
|
|
242
|
+
status: "fulfilled",
|
|
243
|
+
txHash: result.txHash,
|
|
244
|
+
idempotencyKey: item.idempotencyKey
|
|
245
|
+
});
|
|
246
|
+
} catch (err) {
|
|
247
|
+
const entry = {
|
|
248
|
+
status: "rejected",
|
|
249
|
+
error: String(err),
|
|
250
|
+
idempotencyKey: item.idempotencyKey
|
|
251
|
+
};
|
|
252
|
+
results.push(entry);
|
|
253
|
+
if (stopOnError) {
|
|
254
|
+
const succeeded2 = results.filter(
|
|
255
|
+
(r) => r.status === "fulfilled"
|
|
256
|
+
).length;
|
|
257
|
+
throw new StableBulkError({
|
|
258
|
+
shortMessage: "bulkWithdraw halted on first failure",
|
|
259
|
+
results,
|
|
260
|
+
succeeded: succeeded2,
|
|
261
|
+
failed: results.length - succeeded2
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const succeeded = results.filter(
|
|
267
|
+
(r) => r.status === "fulfilled"
|
|
268
|
+
).length;
|
|
269
|
+
return {
|
|
270
|
+
results,
|
|
271
|
+
succeeded,
|
|
272
|
+
failed: results.length - succeeded,
|
|
273
|
+
idempotencyKey
|
|
274
|
+
};
|
|
233
275
|
};
|
|
276
|
+
return this._withIdempotency(idempotencyKey, run);
|
|
234
277
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
};
|
|
278
|
+
withdraw(params) {
|
|
279
|
+
this._validateAmount(params.amount, "earn withdraw");
|
|
280
|
+
return this._withIdempotency(
|
|
281
|
+
params.idempotencyKey,
|
|
282
|
+
() => this._withdraw(params)
|
|
283
|
+
);
|
|
250
284
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
285
|
+
redeem(params) {
|
|
286
|
+
this._validateShares(params.shares, "earn redeem");
|
|
287
|
+
return this._withIdempotency(
|
|
288
|
+
params.idempotencyKey,
|
|
289
|
+
() => this._redeem(params)
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
forceWithdraw(params) {
|
|
293
|
+
this._validateAmount(params.amount, "earn forceWithdraw");
|
|
294
|
+
return this._withIdempotency(
|
|
295
|
+
params.idempotencyKey,
|
|
296
|
+
() => this._forceWithdraw(params)
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
forceRedeem(params) {
|
|
300
|
+
this._validateShares(params.shares, "earn forceRedeem");
|
|
301
|
+
return this._withIdempotency(
|
|
302
|
+
params.idempotencyKey,
|
|
303
|
+
() => this._forceRedeem(params)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
// ─── Private operations ──────────────────────────────────────────────────
|
|
307
|
+
async _deposit(params) {
|
|
308
|
+
const {
|
|
309
|
+
amount,
|
|
310
|
+
tokenDecimals,
|
|
311
|
+
slippageTolerance,
|
|
312
|
+
idempotencyKey,
|
|
313
|
+
depositBuffer
|
|
314
|
+
} = params;
|
|
315
|
+
const from = this._requireFrom();
|
|
316
|
+
const vaultAddr = params.vault ?? this.config.vault;
|
|
317
|
+
await this._switchChain();
|
|
318
|
+
const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
|
|
319
|
+
const vaultData = await vault.getData();
|
|
320
|
+
const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn deposit");
|
|
321
|
+
const assetsRaw = parseUnits(String(amount), decimals);
|
|
322
|
+
if (depositBuffer !== void 0) {
|
|
323
|
+
const bps = BigInt(Math.round(depositBuffer * 1e4));
|
|
324
|
+
const maxAllowed = vaultData.assetBalance * bps / 10000n;
|
|
325
|
+
if (assetsRaw > maxAllowed) {
|
|
326
|
+
const maxFormatted = Number(formatUnits(maxAllowed, decimals));
|
|
327
|
+
const idleFormatted = Number(
|
|
328
|
+
formatUnits(vaultData.assetBalance, decimals)
|
|
329
|
+
);
|
|
330
|
+
throw new StableValidationError(
|
|
331
|
+
"amount",
|
|
332
|
+
amount,
|
|
333
|
+
`earn deposit: amount ${amount} exceeds deposit buffer \u2014 max ${maxFormatted} (${depositBuffer * 100}% of ${idleFormatted} idle liquidity)`
|
|
334
|
+
);
|
|
257
335
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
336
|
+
}
|
|
337
|
+
const depositAction = vault.deposit({
|
|
338
|
+
amount: assetsRaw,
|
|
339
|
+
userAddress: from,
|
|
340
|
+
accrualVault: vaultData,
|
|
341
|
+
slippageTolerance: slippageTolerance !== void 0 ? (
|
|
342
|
+
// Morpho SDK expects slippage as a WAD-scaled fraction (1e18 = 100%)
|
|
343
|
+
parseUnits(String(slippageTolerance), 18)
|
|
344
|
+
) : void 0
|
|
345
|
+
});
|
|
346
|
+
const requirements = await depositAction.getRequirements();
|
|
347
|
+
let sig;
|
|
348
|
+
for (const req of requirements) {
|
|
349
|
+
if (isRequirementApproval(req)) {
|
|
350
|
+
const approveHash = await this._sendTx(
|
|
351
|
+
req.to,
|
|
352
|
+
req.data,
|
|
353
|
+
req.value,
|
|
354
|
+
from
|
|
355
|
+
);
|
|
356
|
+
const approveReceipt = await this.pc.waitForTransactionReceipt({
|
|
357
|
+
hash: approveHash
|
|
289
358
|
});
|
|
359
|
+
if (approveReceipt.status === "reverted") {
|
|
360
|
+
throw new StableTransactionError({
|
|
361
|
+
phase: "approve",
|
|
362
|
+
shortMessage: "earn deposit: token approval reverted on-chain",
|
|
363
|
+
txHash: approveReceipt.transactionHash,
|
|
364
|
+
chainId: this.config.chainId
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
} else if (isRequirementSignature(req)) {
|
|
368
|
+
sig = await req.sign(this.wc, from);
|
|
290
369
|
}
|
|
291
|
-
throw err;
|
|
292
370
|
}
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
371
|
+
const tx = depositAction.buildTx(sig);
|
|
372
|
+
const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
|
|
373
|
+
const receipt = await this.pc.waitForTransactionReceipt({ hash });
|
|
374
|
+
if (receipt.status === "reverted") {
|
|
375
|
+
throw new StableTransactionError({
|
|
376
|
+
phase: "receipt",
|
|
377
|
+
shortMessage: "earn deposit: vault deposit reverted on-chain",
|
|
378
|
+
txHash: receipt.transactionHash,
|
|
379
|
+
chainId: this.config.chainId
|
|
380
|
+
});
|
|
300
381
|
}
|
|
301
|
-
return
|
|
382
|
+
return { txHash: receipt.transactionHash, idempotencyKey };
|
|
302
383
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
384
|
+
async _withdraw(params) {
|
|
385
|
+
const { amount, tokenDecimals, idempotencyKey } = params;
|
|
386
|
+
const from = this._requireFrom();
|
|
387
|
+
const vaultAddr = this.config.vault;
|
|
388
|
+
await this._switchChain();
|
|
389
|
+
const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
|
|
390
|
+
const vaultData = await vault.getData();
|
|
391
|
+
const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn withdraw");
|
|
392
|
+
const assetsRaw = parseUnits(String(amount), decimals);
|
|
393
|
+
const tx = vault.withdraw({ amount: assetsRaw, userAddress: from }).buildTx();
|
|
394
|
+
const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
|
|
395
|
+
const receipt = await this.pc.waitForTransactionReceipt({ hash });
|
|
396
|
+
if (receipt.status === "reverted") {
|
|
397
|
+
throw new StableTransactionError({
|
|
398
|
+
phase: "receipt",
|
|
399
|
+
shortMessage: "earn withdraw: vault withdraw reverted on-chain",
|
|
400
|
+
txHash: receipt.transactionHash,
|
|
401
|
+
chainId: this.config.chainId
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return { txHash: receipt.transactionHash, idempotencyKey };
|
|
405
|
+
}
|
|
406
|
+
async _redeem(params) {
|
|
407
|
+
const { shares, shareDecimals, idempotencyKey } = params;
|
|
408
|
+
const from = this._requireFrom();
|
|
409
|
+
const vaultAddr = this.config.vault;
|
|
410
|
+
await this._switchChain();
|
|
411
|
+
let decimals = shareDecimals;
|
|
412
|
+
if (decimals === void 0) {
|
|
413
|
+
try {
|
|
414
|
+
decimals = await this.pc.readContract({
|
|
415
|
+
address: vaultAddr,
|
|
416
|
+
abi: erc20Abi,
|
|
417
|
+
functionName: "decimals"
|
|
418
|
+
});
|
|
419
|
+
} catch {
|
|
420
|
+
throw new StableValidationError(
|
|
421
|
+
"shareDecimals",
|
|
422
|
+
void 0,
|
|
423
|
+
`earn redeem: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const sharesRaw = parseUnits(String(shares), decimals);
|
|
428
|
+
const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
|
|
429
|
+
const tx = vault.redeem({ shares: sharesRaw, userAddress: from }).buildTx();
|
|
430
|
+
const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
|
|
431
|
+
const receipt = await this.pc.waitForTransactionReceipt({ hash });
|
|
432
|
+
if (receipt.status === "reverted") {
|
|
433
|
+
throw new StableTransactionError({
|
|
434
|
+
phase: "receipt",
|
|
435
|
+
shortMessage: "earn redeem: vault redeem reverted on-chain",
|
|
436
|
+
txHash: receipt.transactionHash,
|
|
437
|
+
chainId: this.config.chainId
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
return { txHash: receipt.transactionHash, idempotencyKey };
|
|
441
|
+
}
|
|
442
|
+
async _forceWithdraw(params) {
|
|
443
|
+
const { amount, tokenDecimals, deallocations, idempotencyKey } = params;
|
|
444
|
+
const from = this._requireFrom();
|
|
445
|
+
const vaultAddr = this.config.vault;
|
|
446
|
+
await this._switchChain();
|
|
447
|
+
const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
|
|
448
|
+
let decimals = tokenDecimals;
|
|
449
|
+
if (decimals === void 0) {
|
|
450
|
+
const vaultData = await vault.getData();
|
|
451
|
+
decimals = await this._resolveDecimals(
|
|
452
|
+
vaultData.asset,
|
|
453
|
+
"earn forceWithdraw"
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
const tx = vault.forceWithdraw({
|
|
457
|
+
deallocations: this._buildDeallocs(deallocations, decimals),
|
|
458
|
+
withdraw: { amount: parseUnits(String(amount), decimals) },
|
|
459
|
+
userAddress: from
|
|
460
|
+
}).buildTx();
|
|
461
|
+
const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
|
|
462
|
+
const receipt = await this.pc.waitForTransactionReceipt({ hash });
|
|
463
|
+
if (receipt.status === "reverted") {
|
|
464
|
+
throw new StableTransactionError({
|
|
465
|
+
phase: "receipt",
|
|
466
|
+
shortMessage: "earn forceWithdraw: vault forceWithdraw reverted on-chain",
|
|
467
|
+
txHash: receipt.transactionHash,
|
|
468
|
+
chainId: this.config.chainId
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
return { txHash: receipt.transactionHash, idempotencyKey };
|
|
472
|
+
}
|
|
473
|
+
async _forceRedeem(params) {
|
|
474
|
+
const {
|
|
475
|
+
shares,
|
|
476
|
+
shareDecimals,
|
|
477
|
+
tokenDecimals,
|
|
478
|
+
deallocations,
|
|
479
|
+
idempotencyKey
|
|
480
|
+
} = params;
|
|
481
|
+
const from = this._requireFrom();
|
|
482
|
+
const vaultAddr = this.config.vault;
|
|
483
|
+
await this._switchChain();
|
|
484
|
+
const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
|
|
485
|
+
let shareDec = shareDecimals;
|
|
486
|
+
if (shareDec === void 0) {
|
|
487
|
+
try {
|
|
488
|
+
shareDec = await this.pc.readContract({
|
|
489
|
+
address: vaultAddr,
|
|
490
|
+
abi: erc20Abi,
|
|
491
|
+
functionName: "decimals"
|
|
492
|
+
});
|
|
493
|
+
} catch {
|
|
494
|
+
throw new StableValidationError(
|
|
495
|
+
"shareDecimals",
|
|
496
|
+
void 0,
|
|
497
|
+
`earn forceRedeem: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
let assetDec = tokenDecimals;
|
|
502
|
+
if (assetDec === void 0) {
|
|
503
|
+
const vaultData = await vault.getData();
|
|
504
|
+
assetDec = await this._resolveDecimals(
|
|
505
|
+
vaultData.asset,
|
|
506
|
+
"earn forceRedeem"
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const tx = vault.forceRedeem({
|
|
510
|
+
deallocations: this._buildDeallocs(deallocations, assetDec),
|
|
511
|
+
redeem: { shares: parseUnits(String(shares), shareDec) },
|
|
512
|
+
userAddress: from
|
|
513
|
+
}).buildTx();
|
|
514
|
+
const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
|
|
515
|
+
const receipt = await this.pc.waitForTransactionReceipt({ hash });
|
|
516
|
+
if (receipt.status === "reverted") {
|
|
517
|
+
throw new StableTransactionError({
|
|
518
|
+
phase: "receipt",
|
|
519
|
+
shortMessage: "earn forceRedeem: vault forceRedeem reverted on-chain",
|
|
520
|
+
txHash: receipt.transactionHash,
|
|
521
|
+
chainId: this.config.chainId
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return { txHash: receipt.transactionHash, idempotencyKey };
|
|
525
|
+
}
|
|
526
|
+
_buildDeallocs(deallocations, assetDecimals) {
|
|
527
|
+
return deallocations.map((d) => ({
|
|
528
|
+
adapter: d.adapter,
|
|
529
|
+
amount: parseUnits(String(d.amount), assetDecimals),
|
|
530
|
+
// MarketParams in the Morpho SDK has computed fields (id, liquidationIncentiveFactor)
|
|
531
|
+
// not present in our user-facing type; the plain object is structurally compatible for ABI encoding.
|
|
532
|
+
...d.marketParams && {
|
|
533
|
+
marketParams: d.marketParams
|
|
534
|
+
}
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
537
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
538
|
+
_requireFrom() {
|
|
539
|
+
const from = this.wc.account?.address;
|
|
540
|
+
if (!from) {
|
|
541
|
+
throw new StableValidationError(
|
|
542
|
+
"signer",
|
|
543
|
+
void 0,
|
|
544
|
+
"earn: requires a signer \u2014 set walletClient or account in StableConfig"
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return from;
|
|
548
|
+
}
|
|
549
|
+
async _switchChain() {
|
|
550
|
+
try {
|
|
551
|
+
await this.wc.switchChain({ id: this.config.chainId });
|
|
552
|
+
} catch (err) {
|
|
553
|
+
if (err.code === 4001) {
|
|
554
|
+
throw new StableTransactionError({
|
|
555
|
+
phase: "switch_chain",
|
|
556
|
+
shortMessage: `earn: wallet rejected switch to chain ${this.config.chainId}`,
|
|
557
|
+
chainId: this.config.chainId,
|
|
558
|
+
cause: err
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async _sendTx(to, data, value, from) {
|
|
564
|
+
try {
|
|
565
|
+
return await this.wc.sendTransaction({
|
|
566
|
+
to,
|
|
567
|
+
data,
|
|
568
|
+
value,
|
|
569
|
+
account: from,
|
|
570
|
+
chain: this.chain
|
|
571
|
+
});
|
|
572
|
+
} catch (err) {
|
|
573
|
+
throw new StableTransactionError({
|
|
574
|
+
phase: "send",
|
|
575
|
+
shortMessage: "earn: transaction send failed",
|
|
576
|
+
chainId: this.config.chainId,
|
|
577
|
+
cause: err
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async _resolveDecimals(asset, op) {
|
|
582
|
+
try {
|
|
583
|
+
return await retryWithBackoff(
|
|
584
|
+
() => this.pc.readContract({
|
|
585
|
+
address: asset,
|
|
586
|
+
abi: erc20Abi,
|
|
587
|
+
functionName: "decimals"
|
|
588
|
+
})
|
|
589
|
+
);
|
|
590
|
+
} catch {
|
|
591
|
+
throw new StableValidationError(
|
|
592
|
+
"tokenDecimals",
|
|
593
|
+
void 0,
|
|
594
|
+
`${op}: could not fetch decimals for asset "${asset}" \u2014 pass tokenDecimals explicitly`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
_withIdempotency(key, fn) {
|
|
599
|
+
if (!key) return fn();
|
|
600
|
+
const existing = this.inFlight.get(key);
|
|
601
|
+
if (existing) return existing;
|
|
602
|
+
const promise = fn().finally(() => this.inFlight.delete(key));
|
|
603
|
+
this.inFlight.set(key, promise);
|
|
604
|
+
return promise;
|
|
605
|
+
}
|
|
606
|
+
_validateAmount(amount, op) {
|
|
607
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
608
|
+
throw new StableValidationError(
|
|
609
|
+
"amount",
|
|
610
|
+
amount,
|
|
611
|
+
`${op}: amount must be a finite positive number`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
_validateShares(shares, op) {
|
|
616
|
+
if (!Number.isFinite(shares) || shares <= 0) {
|
|
617
|
+
throw new StableValidationError(
|
|
618
|
+
"shares",
|
|
619
|
+
shares,
|
|
620
|
+
`${op}: shares must be a finite positive number`
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// src/earn/reader.ts
|
|
627
|
+
import { MorphoClient as MorphoClient2 } from "@morpho-org/morpho-sdk";
|
|
628
|
+
import {
|
|
629
|
+
http as http2,
|
|
630
|
+
createPublicClient as createPublicClient2,
|
|
309
631
|
erc20Abi as erc20Abi2,
|
|
310
632
|
formatUnits as formatUnits2,
|
|
311
|
-
|
|
633
|
+
isAddress,
|
|
634
|
+
parseUnits as parseUnits2
|
|
635
|
+
} from "viem";
|
|
636
|
+
import { stable as stableMainnet, stableTestnet } from "viem/chains";
|
|
637
|
+
|
|
638
|
+
// src/types.ts
|
|
639
|
+
var Chain = /* @__PURE__ */ ((Chain2) => {
|
|
640
|
+
Chain2["Sepolia"] = "sepolia";
|
|
641
|
+
Chain2["StableTestnet"] = "stable-testnet";
|
|
642
|
+
Chain2["Stable"] = "stable";
|
|
643
|
+
Chain2["Ethereum"] = "ethereum";
|
|
644
|
+
Chain2["Arbitrum"] = "arbitrum";
|
|
645
|
+
Chain2["Ink"] = "ink";
|
|
646
|
+
Chain2["Bera"] = "bera";
|
|
647
|
+
Chain2["MegaETH"] = "megaeth";
|
|
648
|
+
Chain2["Base"] = "base";
|
|
649
|
+
Chain2["BSC"] = "bsc";
|
|
650
|
+
Chain2["HyperEVM"] = "hyperevm";
|
|
651
|
+
return Chain2;
|
|
652
|
+
})(Chain || {});
|
|
653
|
+
var Network = /* @__PURE__ */ ((Network2) => {
|
|
654
|
+
Network2["Mainnet"] = "mainnet";
|
|
655
|
+
Network2["Testnet"] = "testnet";
|
|
656
|
+
return Network2;
|
|
657
|
+
})(Network || {});
|
|
658
|
+
|
|
659
|
+
// src/earn/reader.ts
|
|
660
|
+
var MORPHO_API_URL = "https://blue-api.morpho.org/graphql";
|
|
661
|
+
var EarnReader = class {
|
|
662
|
+
constructor(config) {
|
|
663
|
+
if (!config.earn?.vault) {
|
|
664
|
+
throw new StableValidationError(
|
|
665
|
+
"earn.vault",
|
|
666
|
+
void 0,
|
|
667
|
+
"createStableReader: earn.vault is required"
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
if (!isAddress(config.address)) {
|
|
671
|
+
throw new StableValidationError(
|
|
672
|
+
"address",
|
|
673
|
+
config.address,
|
|
674
|
+
`createStableReader: invalid user address "${config.address}"`
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
if (!isAddress(config.earn.vault)) {
|
|
678
|
+
throw new StableValidationError(
|
|
679
|
+
"earn.vault",
|
|
680
|
+
config.earn.vault,
|
|
681
|
+
`createStableReader: invalid vault address "${config.earn.vault}"`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
const network = config.network ?? "mainnet" /* Mainnet */;
|
|
685
|
+
const viemChain = network === "mainnet" /* Mainnet */ ? stableMainnet : stableTestnet;
|
|
686
|
+
const rpc = (config.rpc ?? viemChain.rpcUrls.default.http[0]).replace(
|
|
687
|
+
/\/$/,
|
|
688
|
+
""
|
|
689
|
+
);
|
|
690
|
+
this.chainId = viemChain.id;
|
|
691
|
+
this.vaultAddr = config.earn.vault;
|
|
692
|
+
this.userAddr = config.address;
|
|
693
|
+
const chain = makeChain(viemChain.id, rpc);
|
|
694
|
+
this.pc = createPublicClient2({ chain, transport: http2(rpc) });
|
|
695
|
+
this.morpho = new MorphoClient2(this.pc);
|
|
696
|
+
}
|
|
697
|
+
async position() {
|
|
698
|
+
const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
|
|
699
|
+
const [vaultData, shares] = await Promise.all([
|
|
700
|
+
vault.getData(),
|
|
701
|
+
this.pc.readContract({
|
|
702
|
+
address: this.vaultAddr,
|
|
703
|
+
abi: erc20Abi2,
|
|
704
|
+
functionName: "balanceOf",
|
|
705
|
+
args: [this.userAddr]
|
|
706
|
+
})
|
|
707
|
+
]);
|
|
708
|
+
const [tokenDecimals, shareDecimals] = await Promise.all([
|
|
709
|
+
this.pc.readContract({
|
|
710
|
+
address: vaultData.asset,
|
|
711
|
+
abi: erc20Abi2,
|
|
712
|
+
functionName: "decimals"
|
|
713
|
+
}),
|
|
714
|
+
this.pc.readContract({
|
|
715
|
+
address: this.vaultAddr,
|
|
716
|
+
abi: erc20Abi2,
|
|
717
|
+
functionName: "decimals"
|
|
718
|
+
})
|
|
719
|
+
]);
|
|
720
|
+
const assetsRaw = vaultData.toAssets(shares);
|
|
721
|
+
return {
|
|
722
|
+
shares,
|
|
723
|
+
sharesFormatted: Number(formatUnits2(shares, shareDecimals)),
|
|
724
|
+
shareDecimals,
|
|
725
|
+
assets: Number(formatUnits2(assetsRaw, tokenDecimals)),
|
|
726
|
+
tokenDecimals,
|
|
727
|
+
assetAddress: vaultData.asset
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
async getYield() {
|
|
731
|
+
return this._fetchYield();
|
|
732
|
+
}
|
|
733
|
+
async preview(params) {
|
|
734
|
+
const { amount, horizonDays } = params;
|
|
735
|
+
const apy = await this._fetchNetApy();
|
|
736
|
+
return {
|
|
737
|
+
projectedYield: amount * ((1 + apy) ** (horizonDays / 365) - 1),
|
|
738
|
+
apy,
|
|
739
|
+
horizonDays
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
async withdrawability(params) {
|
|
743
|
+
const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
|
|
744
|
+
const vaultData = await vault.getData();
|
|
745
|
+
const tokenDecimals = params.tokenDecimals ?? await this.pc.readContract({
|
|
746
|
+
address: vaultData.asset,
|
|
747
|
+
abi: erc20Abi2,
|
|
748
|
+
functionName: "decimals"
|
|
749
|
+
});
|
|
750
|
+
const requestedRaw = parseUnits2(String(params.amount), tokenDecimals);
|
|
751
|
+
let availableLiquidityRaw = vaultData.assetBalance;
|
|
752
|
+
if (vaultData.accrualLiquidityAdapter != null) {
|
|
753
|
+
availableLiquidityRaw += vaultData.accrualLiquidityAdapter.maxWithdraw(
|
|
754
|
+
vaultData.liquidityData
|
|
755
|
+
).value;
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
isInstant: requestedRaw <= availableLiquidityRaw,
|
|
759
|
+
availableLiquidity: Number(
|
|
760
|
+
formatUnits2(availableLiquidityRaw, tokenDecimals)
|
|
761
|
+
),
|
|
762
|
+
tokenDecimals
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
async _fetchNetApy() {
|
|
766
|
+
const { apy } = await this._fetchYield();
|
|
767
|
+
return apy;
|
|
768
|
+
}
|
|
769
|
+
async _fetchYield() {
|
|
770
|
+
const query = `
|
|
771
|
+
query VaultYield($address: String!, $chainId: Int!) {
|
|
772
|
+
vaultV2ByAddress(address: $address, chainId: $chainId) {
|
|
773
|
+
avgNetApy
|
|
774
|
+
avgNetApyExcludingRewards
|
|
775
|
+
performanceFee
|
|
776
|
+
managementFee
|
|
777
|
+
rewards {
|
|
778
|
+
asset { symbol address }
|
|
779
|
+
supplyApr
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
`;
|
|
784
|
+
let body;
|
|
785
|
+
try {
|
|
786
|
+
body = await retryWithBackoff(
|
|
787
|
+
async () => {
|
|
788
|
+
const res = await fetch(MORPHO_API_URL, {
|
|
789
|
+
method: "POST",
|
|
790
|
+
headers: { "Content-Type": "application/json" },
|
|
791
|
+
body: JSON.stringify({
|
|
792
|
+
query,
|
|
793
|
+
variables: {
|
|
794
|
+
address: this.vaultAddr,
|
|
795
|
+
chainId: this.chainId
|
|
796
|
+
}
|
|
797
|
+
})
|
|
798
|
+
});
|
|
799
|
+
if (!res.ok) {
|
|
800
|
+
const retryable = res.status === 429 || res.status >= 500;
|
|
801
|
+
throw new HttpError(
|
|
802
|
+
`earn: Morpho API returned ${res.status} \u2014 ${res.statusText}`,
|
|
803
|
+
res.status,
|
|
804
|
+
retryable
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
return res.json().catch(() => void 0);
|
|
808
|
+
},
|
|
809
|
+
(err) => err instanceof HttpError ? err.retryable : err instanceof TypeError
|
|
810
|
+
);
|
|
811
|
+
} catch (err) {
|
|
812
|
+
if (err instanceof TypeError || err instanceof HttpError) {
|
|
813
|
+
throw new StableNetworkError({
|
|
814
|
+
shortMessage: err instanceof HttpError ? `earn: Morpho API returned ${err.status} after retries` : `earn: failed to reach Morpho API after retries \u2014 ${String(
|
|
815
|
+
err
|
|
816
|
+
)}`,
|
|
817
|
+
url: MORPHO_API_URL,
|
|
818
|
+
cause: err
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
throw err;
|
|
822
|
+
}
|
|
823
|
+
const vault = body?.data?.vaultV2ByAddress;
|
|
824
|
+
if (!vault || typeof vault.avgNetApy !== "number") {
|
|
825
|
+
throw new StableValidationError(
|
|
826
|
+
"apy",
|
|
827
|
+
void 0,
|
|
828
|
+
"earn: vault not found in Morpho API or unexpected response shape"
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
const rewards = vault.rewards.filter((r) => typeof r.supplyApr === "number" && r.supplyApr > 0).map((r) => ({
|
|
832
|
+
symbol: r.asset.symbol,
|
|
833
|
+
address: r.asset.address,
|
|
834
|
+
apr: r.supplyApr
|
|
835
|
+
}));
|
|
836
|
+
return {
|
|
837
|
+
apy: vault.avgNetApy,
|
|
838
|
+
native: vault.avgNetApyExcludingRewards ?? 0,
|
|
839
|
+
rewards,
|
|
840
|
+
performanceFee: vault.performanceFee,
|
|
841
|
+
managementFee: vault.managementFee
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
// src/services/lifi.ts
|
|
847
|
+
import {
|
|
848
|
+
http as http3,
|
|
849
|
+
createPublicClient as createPublicClient3,
|
|
850
|
+
createWalletClient as createWalletClient2,
|
|
851
|
+
erc20Abi as erc20Abi3,
|
|
852
|
+
formatUnits as formatUnits3,
|
|
312
853
|
isAddress as isAddress2,
|
|
313
|
-
parseUnits as
|
|
854
|
+
parseUnits as parseUnits3
|
|
314
855
|
} from "viem";
|
|
315
856
|
|
|
316
857
|
// src/chains.ts
|
|
@@ -378,636 +919,456 @@ var CHAIN_CONFIGS = {
|
|
|
378
919
|
decimals: 18
|
|
379
920
|
},
|
|
380
921
|
["hyperevm" /* HyperEVM */]: {
|
|
381
|
-
id: 999,
|
|
382
|
-
rpc: "https://rpc.hyperliquid.xyz/evm",
|
|
383
|
-
usdt: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb",
|
|
384
|
-
decimals: 6
|
|
385
|
-
}
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
// src/services/lifi.ts
|
|
389
|
-
var LiFiService = class {
|
|
390
|
-
constructor(config) {
|
|
391
|
-
this.config = config;
|
|
392
|
-
}
|
|
393
|
-
async quoteSwap(params) {
|
|
394
|
-
this._validateSwapParams(params);
|
|
395
|
-
const { fromToken, toToken, amount, fromDecimals = 6, toAddress } = params;
|
|
396
|
-
const from = this._requireFrom();
|
|
397
|
-
const chain = this.config.chainId.toString();
|
|
398
|
-
const fromAmountRaw = parseUnits2(String(amount), fromDecimals).toString();
|
|
399
|
-
const qs = new URLSearchParams({
|
|
400
|
-
fromChain: chain,
|
|
401
|
-
toChain: chain,
|
|
402
|
-
fromToken,
|
|
403
|
-
toToken,
|
|
404
|
-
fromAmount: fromAmountRaw,
|
|
405
|
-
fromAddress: from,
|
|
406
|
-
...toAddress && { toAddress }
|
|
407
|
-
});
|
|
408
|
-
const quote = await this._fetchQuote(qs);
|
|
409
|
-
return this._toSwapQuote(quote);
|
|
410
|
-
}
|
|
411
|
-
async swap(params) {
|
|
412
|
-
this._validateSwapParams(params);
|
|
413
|
-
const quote = params.quote ?? await this.quoteSwap(params);
|
|
414
|
-
return this._executeSwapQuote(quote, this.config.rpc, this.config.chainId);
|
|
415
|
-
}
|
|
416
|
-
async quoteBridge(params) {
|
|
417
|
-
this._validateBridgeParams(params);
|
|
418
|
-
const { fromChain, toChain, fromToken, toToken, amount, fromDecimals = 6, recipient } = params;
|
|
419
|
-
const from = this.config.walletClient?.account?.address ?? this.config.account?.address ?? "0x0000000000000000000000000000000000000000";
|
|
420
|
-
const src = CHAIN_CONFIGS[fromChain];
|
|
421
|
-
const dst = CHAIN_CONFIGS[toChain];
|
|
422
|
-
if (!src) throw new StableValidationError("fromChain", fromChain, `LI.FI bridge: unsupported source chain "${fromChain}"`);
|
|
423
|
-
if (!dst) throw new StableValidationError("toChain", toChain, `LI.FI bridge: unsupported destination chain "${toChain}"`);
|
|
424
|
-
const qs = new URLSearchParams({
|
|
425
|
-
fromChain: src.id.toString(),
|
|
426
|
-
toChain: dst.id.toString(),
|
|
427
|
-
fromToken,
|
|
428
|
-
toToken,
|
|
429
|
-
fromAmount: parseUnits2(String(amount), fromDecimals).toString(),
|
|
430
|
-
fromAddress: from,
|
|
431
|
-
...recipient && { toAddress: recipient }
|
|
432
|
-
});
|
|
433
|
-
const quote = this._toSwapQuote(await this._fetchQuote(qs));
|
|
434
|
-
return { toAmount: quote.toAmount };
|
|
435
|
-
}
|
|
436
|
-
async bridge(params) {
|
|
437
|
-
this._validateBridgeParams(params);
|
|
438
|
-
const { fromChain, toChain, fromToken, toToken, amount, fromDecimals = 6, recipient } = params;
|
|
439
|
-
const from = this._requireFrom();
|
|
440
|
-
const src = CHAIN_CONFIGS[fromChain];
|
|
441
|
-
const dst = CHAIN_CONFIGS[toChain];
|
|
442
|
-
if (!src) {
|
|
443
|
-
throw new StableValidationError("fromChain", fromChain, `LI.FI bridge: unsupported source chain "${fromChain}"`);
|
|
444
|
-
}
|
|
445
|
-
if (!dst) {
|
|
446
|
-
throw new StableValidationError("toChain", toChain, `LI.FI bridge: unsupported destination chain "${toChain}"`);
|
|
447
|
-
}
|
|
448
|
-
const fromAmountRaw = parseUnits2(String(amount), fromDecimals).toString();
|
|
449
|
-
const qs = new URLSearchParams({
|
|
450
|
-
fromChain: src.id.toString(),
|
|
451
|
-
toChain: dst.id.toString(),
|
|
452
|
-
fromToken,
|
|
453
|
-
toToken,
|
|
454
|
-
fromAmount: fromAmountRaw,
|
|
455
|
-
fromAddress: from,
|
|
456
|
-
...recipient && { toAddress: recipient }
|
|
457
|
-
});
|
|
458
|
-
const quote = this._toSwapQuote(await this._fetchQuote(qs));
|
|
459
|
-
return this._executeSwapQuote(quote, src.rpc, src.id);
|
|
460
|
-
}
|
|
461
|
-
_validateSwapParams({ fromToken, toToken, amount, toAddress }) {
|
|
462
|
-
if (!isAddress2(fromToken)) {
|
|
463
|
-
throw new StableValidationError("fromToken", fromToken, `swap: invalid fromToken address "${fromToken}"`);
|
|
464
|
-
}
|
|
465
|
-
if (!isAddress2(toToken)) {
|
|
466
|
-
throw new StableValidationError("toToken", toToken, `swap: invalid toToken address "${toToken}"`);
|
|
467
|
-
}
|
|
468
|
-
if (fromToken.toLowerCase() === toToken.toLowerCase()) {
|
|
469
|
-
throw new StableValidationError("toToken", toToken, "swap: fromToken and toToken must differ");
|
|
470
|
-
}
|
|
471
|
-
if (!Number.isFinite(amount) || amount <= 0) {
|
|
472
|
-
throw new StableValidationError("amount", amount, "swap: amount must be a finite positive number");
|
|
473
|
-
}
|
|
474
|
-
if (toAddress !== void 0 && !isAddress2(toAddress)) {
|
|
475
|
-
throw new StableValidationError("toAddress", toAddress, `swap: invalid toAddress "${toAddress}"`);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
_validateBridgeParams({ fromChain, toChain, fromToken, toToken, amount, recipient }) {
|
|
479
|
-
if (!isAddress2(fromToken)) {
|
|
480
|
-
throw new StableValidationError("fromToken", fromToken, `LI.FI bridge: invalid fromToken address "${fromToken}"`);
|
|
481
|
-
}
|
|
482
|
-
if (!isAddress2(toToken)) {
|
|
483
|
-
throw new StableValidationError("toToken", toToken, `LI.FI bridge: invalid toToken address "${toToken}"`);
|
|
484
|
-
}
|
|
485
|
-
if (!Number.isFinite(amount) || amount <= 0) {
|
|
486
|
-
throw new StableValidationError("amount", amount, "LI.FI bridge: amount must be a finite positive number");
|
|
487
|
-
}
|
|
488
|
-
if (recipient !== void 0 && !isAddress2(recipient)) {
|
|
489
|
-
throw new StableValidationError("recipient", recipient, `LI.FI bridge: invalid recipient address "${recipient}"`);
|
|
490
|
-
}
|
|
491
|
-
if (fromChain === toChain) {
|
|
492
|
-
throw new StableValidationError("toChain", toChain, "LI.FI bridge: fromChain and toChain must differ");
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
_requireFrom() {
|
|
496
|
-
const from = this.config.walletClient?.account?.address ?? this.config.account?.address;
|
|
497
|
-
if (!from) {
|
|
498
|
-
throw new StableValidationError(
|
|
499
|
-
"signer",
|
|
500
|
-
void 0,
|
|
501
|
-
"LI.FI: requires a signer \u2014 set walletClient or account in StableConfig"
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
return from;
|
|
505
|
-
}
|
|
506
|
-
async _fetchQuote(qs) {
|
|
507
|
-
const url = `https://li.quest/v1/quote?${qs}`;
|
|
508
|
-
try {
|
|
509
|
-
return await retryWithBackoff(() => this._fetchQuoteOnce(url));
|
|
510
|
-
} catch (err) {
|
|
511
|
-
if (err instanceof StableQuoteError) throw err;
|
|
512
|
-
if (err instanceof HttpError) {
|
|
513
|
-
throw new StableQuoteError({
|
|
514
|
-
provider: "lifi",
|
|
515
|
-
shortMessage: err.message,
|
|
516
|
-
httpStatus: err.status,
|
|
517
|
-
cause: err
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
throw new StableNetworkError({
|
|
521
|
-
shortMessage: "LI.FI: quote request failed before reaching the server",
|
|
522
|
-
url,
|
|
523
|
-
cause: err
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
async _fetchQuoteOnce(url) {
|
|
528
|
-
const res = await fetch(url);
|
|
529
|
-
const body = await res.json().catch(() => void 0);
|
|
530
|
-
if (!res.ok) {
|
|
531
|
-
const providerMessage = isObject(body) && typeof body.message === "string" ? body.message : void 0;
|
|
532
|
-
const providerCode = isObject(body) && typeof body.code === "string" ? body.code : void 0;
|
|
533
|
-
if (res.status === 429 || res.status >= 500) {
|
|
534
|
-
throw new HttpError(`LI.FI: ${providerMessage ?? res.statusText}`, res.status, true);
|
|
535
|
-
}
|
|
536
|
-
throw new StableQuoteError({
|
|
537
|
-
provider: "lifi",
|
|
538
|
-
shortMessage: `LI.FI: ${providerMessage ?? res.statusText}`,
|
|
539
|
-
httpStatus: res.status,
|
|
540
|
-
providerCode,
|
|
541
|
-
body
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
if (!isLiFiQuoteShape(body)) {
|
|
545
|
-
throw new StableQuoteError({
|
|
546
|
-
provider: "lifi",
|
|
547
|
-
shortMessage: "LI.FI: unexpected quote response shape",
|
|
548
|
-
httpStatus: res.status,
|
|
549
|
-
body
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
return body;
|
|
553
|
-
}
|
|
554
|
-
_toSwapQuote(quote) {
|
|
555
|
-
return {
|
|
556
|
-
toAmount: Number(formatUnits2(BigInt(quote.estimate.toAmount), quote.action.toToken.decimals)),
|
|
557
|
-
fromAmount: BigInt(quote.estimate.fromAmount),
|
|
558
|
-
fromToken: quote.action.fromToken.address,
|
|
559
|
-
approvalAddress: quote.estimate.approvalAddress,
|
|
560
|
-
transactionRequest: quote.transactionRequest
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
async _executeSwapQuote(quote, srcRpc, expectedChainId) {
|
|
564
|
-
const from = this._requireFrom();
|
|
565
|
-
const { transactionRequest } = quote;
|
|
566
|
-
const srcChainId = transactionRequest.chainId;
|
|
567
|
-
if (srcChainId !== expectedChainId) {
|
|
568
|
-
throw new StableQuoteError({
|
|
569
|
-
provider: "lifi",
|
|
570
|
-
shortMessage: `LI.FI: returned tx for chain ${srcChainId} but expected ${expectedChainId}`,
|
|
571
|
-
providerCode: "chain_mismatch",
|
|
572
|
-
body: quote
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
const srcChain = makeChain(srcChainId, srcRpc);
|
|
576
|
-
const wc = this.config.walletClient ?? createWalletClient({
|
|
577
|
-
account: this.config.account,
|
|
578
|
-
chain: srcChain,
|
|
579
|
-
transport: this.config.transport ?? http2(srcRpc)
|
|
580
|
-
});
|
|
581
|
-
try {
|
|
582
|
-
await wc.switchChain({ id: srcChainId });
|
|
583
|
-
} catch (err) {
|
|
584
|
-
throw new StableTransactionError({
|
|
585
|
-
phase: "switch_chain",
|
|
586
|
-
shortMessage: `LI.FI: wallet rejected or failed to switch to chain ${srcChainId}`,
|
|
587
|
-
chainId: srcChainId,
|
|
588
|
-
cause: err
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
await this._ensureApproval(wc, srcChain, srcRpc, srcChainId, from, quote);
|
|
592
|
-
let hash;
|
|
593
|
-
try {
|
|
594
|
-
hash = await wc.sendTransaction({
|
|
595
|
-
to: transactionRequest.to,
|
|
596
|
-
data: transactionRequest.data,
|
|
597
|
-
value: transactionRequest.value && transactionRequest.value !== "0x" ? BigInt(transactionRequest.value) : 0n,
|
|
598
|
-
gas: transactionRequest.gasLimit ? BigInt(transactionRequest.gasLimit) : void 0,
|
|
599
|
-
account: wc.account,
|
|
600
|
-
chain: srcChain
|
|
601
|
-
});
|
|
602
|
-
} catch (err) {
|
|
603
|
-
throw new StableTransactionError({
|
|
604
|
-
phase: "send",
|
|
605
|
-
shortMessage: "LI.FI: swap/bridge transaction failed",
|
|
606
|
-
chainId: srcChainId,
|
|
607
|
-
cause: err
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
return { txHash: hash, toAmount: quote.toAmount };
|
|
611
|
-
}
|
|
612
|
-
async _ensureApproval(wc, srcChain, srcRpc, srcChainId, from, quote) {
|
|
613
|
-
const tokenAddr = quote.fromToken;
|
|
614
|
-
const isNative = tokenAddr.toLowerCase() === "0x0000000000000000000000000000000000000000" || tokenAddr.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
615
|
-
if (isNative || !quote.approvalAddress) return;
|
|
616
|
-
const pc = createPublicClient2({ chain: srcChain, transport: http2(srcRpc) });
|
|
617
|
-
const allowance = await pc.readContract({
|
|
618
|
-
address: tokenAddr,
|
|
619
|
-
abi: erc20Abi2,
|
|
620
|
-
functionName: "allowance",
|
|
621
|
-
args: [from, quote.approvalAddress]
|
|
622
|
-
});
|
|
623
|
-
if (allowance >= quote.fromAmount) return;
|
|
624
|
-
let approveHash;
|
|
625
|
-
try {
|
|
626
|
-
approveHash = await wc.writeContract({
|
|
627
|
-
address: tokenAddr,
|
|
628
|
-
abi: erc20Abi2,
|
|
629
|
-
functionName: "approve",
|
|
630
|
-
args: [quote.approvalAddress, quote.fromAmount],
|
|
631
|
-
account: wc.account,
|
|
632
|
-
chain: srcChain
|
|
633
|
-
});
|
|
634
|
-
} catch (err) {
|
|
635
|
-
throw new StableTransactionError({
|
|
636
|
-
phase: "approve",
|
|
637
|
-
shortMessage: "LI.FI: token approve failed",
|
|
638
|
-
chainId: srcChainId,
|
|
639
|
-
cause: err
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
const approveReceipt = await pc.waitForTransactionReceipt({ hash: approveHash });
|
|
643
|
-
if (approveReceipt.status === "reverted") {
|
|
644
|
-
throw new StableTransactionError({
|
|
645
|
-
phase: "approve",
|
|
646
|
-
shortMessage: "LI.FI: token approve reverted on-chain",
|
|
647
|
-
txHash: approveReceipt.transactionHash,
|
|
648
|
-
chainId: srcChainId
|
|
649
|
-
});
|
|
650
|
-
}
|
|
922
|
+
id: 999,
|
|
923
|
+
rpc: "https://rpc.hyperliquid.xyz/evm",
|
|
924
|
+
usdt: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb",
|
|
925
|
+
decimals: 6
|
|
651
926
|
}
|
|
652
927
|
};
|
|
653
|
-
function isObject(v) {
|
|
654
|
-
return typeof v === "object" && v !== null;
|
|
655
|
-
}
|
|
656
|
-
function isLiFiQuoteShape(v) {
|
|
657
|
-
if (!isObject(v)) return false;
|
|
658
|
-
const tx = v.transactionRequest;
|
|
659
|
-
const est = v.estimate;
|
|
660
|
-
const act = v.action;
|
|
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);
|
|
662
|
-
}
|
|
663
928
|
|
|
664
|
-
// src/
|
|
665
|
-
|
|
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 {
|
|
929
|
+
// src/services/lifi.ts
|
|
930
|
+
var LiFiService = class {
|
|
679
931
|
constructor(config) {
|
|
680
932
|
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
933
|
}
|
|
735
|
-
|
|
736
|
-
this.
|
|
737
|
-
|
|
934
|
+
async quoteSwap(params) {
|
|
935
|
+
this._validateSwapParams(params);
|
|
936
|
+
const {
|
|
937
|
+
fromToken,
|
|
938
|
+
toToken,
|
|
939
|
+
amount,
|
|
940
|
+
fromDecimals = 6,
|
|
941
|
+
toAddress
|
|
942
|
+
} = params;
|
|
943
|
+
const from = this._requireFrom();
|
|
944
|
+
const chain = this.config.chainId.toString();
|
|
945
|
+
const fromAmountRaw = parseUnits3(
|
|
946
|
+
String(amount),
|
|
947
|
+
fromDecimals
|
|
948
|
+
).toString();
|
|
949
|
+
const qs = new URLSearchParams({
|
|
950
|
+
fromChain: chain,
|
|
951
|
+
toChain: chain,
|
|
952
|
+
fromToken,
|
|
953
|
+
toToken,
|
|
954
|
+
fromAmount: fromAmountRaw,
|
|
955
|
+
fromAddress: from,
|
|
956
|
+
...toAddress && { toAddress }
|
|
957
|
+
});
|
|
958
|
+
const quote = await this._fetchQuote(qs);
|
|
959
|
+
return this._toSwapQuote(quote);
|
|
738
960
|
}
|
|
739
|
-
|
|
740
|
-
this.
|
|
741
|
-
|
|
961
|
+
async swap(params) {
|
|
962
|
+
this._validateSwapParams(params);
|
|
963
|
+
const quote = params.quote ?? await this.quoteSwap(params);
|
|
964
|
+
return this._executeSwapQuote(
|
|
965
|
+
quote,
|
|
966
|
+
this.config.rpc,
|
|
967
|
+
this.config.chainId
|
|
968
|
+
);
|
|
742
969
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
970
|
+
async quoteBridge(params) {
|
|
971
|
+
this._validateBridgeParams(params);
|
|
972
|
+
const {
|
|
973
|
+
fromChain,
|
|
974
|
+
toChain,
|
|
975
|
+
fromToken,
|
|
976
|
+
toToken,
|
|
977
|
+
amount,
|
|
978
|
+
fromDecimals = 6,
|
|
979
|
+
recipient
|
|
980
|
+
} = params;
|
|
981
|
+
const from = this.config.walletClient?.account?.address ?? this.config.account?.address ?? "0x0000000000000000000000000000000000000000";
|
|
982
|
+
const src = CHAIN_CONFIGS[fromChain];
|
|
983
|
+
const dst = CHAIN_CONFIGS[toChain];
|
|
984
|
+
if (!src)
|
|
985
|
+
throw new StableValidationError(
|
|
986
|
+
"fromChain",
|
|
987
|
+
fromChain,
|
|
988
|
+
`LI.FI bridge: unsupported source chain "${fromChain}"`
|
|
989
|
+
);
|
|
990
|
+
if (!dst)
|
|
991
|
+
throw new StableValidationError(
|
|
992
|
+
"toChain",
|
|
993
|
+
toChain,
|
|
994
|
+
`LI.FI bridge: unsupported destination chain "${toChain}"`
|
|
995
|
+
);
|
|
996
|
+
const qs = new URLSearchParams({
|
|
997
|
+
fromChain: src.id.toString(),
|
|
998
|
+
toChain: dst.id.toString(),
|
|
999
|
+
fromToken,
|
|
1000
|
+
toToken,
|
|
1001
|
+
fromAmount: parseUnits3(String(amount), fromDecimals).toString(),
|
|
1002
|
+
fromAddress: from,
|
|
1003
|
+
...recipient && { toAddress: recipient }
|
|
758
1004
|
});
|
|
759
|
-
const
|
|
760
|
-
|
|
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 };
|
|
1005
|
+
const quote = this._toSwapQuote(await this._fetchQuote(qs));
|
|
1006
|
+
return { toAmount: quote.toAmount };
|
|
789
1007
|
}
|
|
790
|
-
async
|
|
791
|
-
|
|
1008
|
+
async bridge(params) {
|
|
1009
|
+
this._validateBridgeParams(params);
|
|
1010
|
+
const {
|
|
1011
|
+
fromChain,
|
|
1012
|
+
toChain,
|
|
1013
|
+
fromToken,
|
|
1014
|
+
toToken,
|
|
1015
|
+
amount,
|
|
1016
|
+
fromDecimals = 6,
|
|
1017
|
+
recipient
|
|
1018
|
+
} = params;
|
|
792
1019
|
const from = this._requireFrom();
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
});
|
|
1020
|
+
const src = CHAIN_CONFIGS[fromChain];
|
|
1021
|
+
const dst = CHAIN_CONFIGS[toChain];
|
|
1022
|
+
if (!src) {
|
|
1023
|
+
throw new StableValidationError(
|
|
1024
|
+
"fromChain",
|
|
1025
|
+
fromChain,
|
|
1026
|
+
`LI.FI bridge: unsupported source chain "${fromChain}"`
|
|
1027
|
+
);
|
|
809
1028
|
}
|
|
810
|
-
|
|
1029
|
+
if (!dst) {
|
|
1030
|
+
throw new StableValidationError(
|
|
1031
|
+
"toChain",
|
|
1032
|
+
toChain,
|
|
1033
|
+
`LI.FI bridge: unsupported destination chain "${toChain}"`
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
const fromAmountRaw = parseUnits3(
|
|
1037
|
+
String(amount),
|
|
1038
|
+
fromDecimals
|
|
1039
|
+
).toString();
|
|
1040
|
+
const qs = new URLSearchParams({
|
|
1041
|
+
fromChain: src.id.toString(),
|
|
1042
|
+
toChain: dst.id.toString(),
|
|
1043
|
+
fromToken,
|
|
1044
|
+
toToken,
|
|
1045
|
+
fromAmount: fromAmountRaw,
|
|
1046
|
+
fromAddress: from,
|
|
1047
|
+
...recipient && { toAddress: recipient }
|
|
1048
|
+
});
|
|
1049
|
+
const quote = this._toSwapQuote(await this._fetchQuote(qs));
|
|
1050
|
+
return this._executeSwapQuote(quote, src.rpc, src.id);
|
|
811
1051
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
if (
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
void 0,
|
|
825
|
-
`earn redeem: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
|
|
826
|
-
);
|
|
827
|
-
}
|
|
1052
|
+
_validateSwapParams({
|
|
1053
|
+
fromToken,
|
|
1054
|
+
toToken,
|
|
1055
|
+
amount,
|
|
1056
|
+
toAddress
|
|
1057
|
+
}) {
|
|
1058
|
+
if (!isAddress2(fromToken)) {
|
|
1059
|
+
throw new StableValidationError(
|
|
1060
|
+
"fromToken",
|
|
1061
|
+
fromToken,
|
|
1062
|
+
`swap: invalid fromToken address "${fromToken}"`
|
|
1063
|
+
);
|
|
828
1064
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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
|
-
});
|
|
1065
|
+
if (!isAddress2(toToken)) {
|
|
1066
|
+
throw new StableValidationError(
|
|
1067
|
+
"toToken",
|
|
1068
|
+
toToken,
|
|
1069
|
+
`swap: invalid toToken address "${toToken}"`
|
|
1070
|
+
);
|
|
841
1071
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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");
|
|
1072
|
+
if (fromToken.toLowerCase() === toToken.toLowerCase()) {
|
|
1073
|
+
throw new StableValidationError(
|
|
1074
|
+
"toToken",
|
|
1075
|
+
toToken,
|
|
1076
|
+
"swap: fromToken and toToken must differ"
|
|
1077
|
+
);
|
|
854
1078
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
if (
|
|
863
|
-
throw new
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
});
|
|
1079
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
1080
|
+
throw new StableValidationError(
|
|
1081
|
+
"amount",
|
|
1082
|
+
amount,
|
|
1083
|
+
"swap: amount must be a finite positive number"
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
if (toAddress !== void 0 && !isAddress2(toAddress)) {
|
|
1087
|
+
throw new StableValidationError(
|
|
1088
|
+
"toAddress",
|
|
1089
|
+
toAddress,
|
|
1090
|
+
`swap: invalid toAddress "${toAddress}"`
|
|
1091
|
+
);
|
|
869
1092
|
}
|
|
870
|
-
return { txHash: receipt.transactionHash, idempotencyKey };
|
|
871
1093
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
`earn forceRedeem: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
|
|
887
|
-
);
|
|
888
|
-
}
|
|
1094
|
+
_validateBridgeParams({
|
|
1095
|
+
fromChain,
|
|
1096
|
+
toChain,
|
|
1097
|
+
fromToken,
|
|
1098
|
+
toToken,
|
|
1099
|
+
amount,
|
|
1100
|
+
recipient
|
|
1101
|
+
}) {
|
|
1102
|
+
if (!isAddress2(fromToken)) {
|
|
1103
|
+
throw new StableValidationError(
|
|
1104
|
+
"fromToken",
|
|
1105
|
+
fromToken,
|
|
1106
|
+
`LI.FI bridge: invalid fromToken address "${fromToken}"`
|
|
1107
|
+
);
|
|
889
1108
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1109
|
+
if (!isAddress2(toToken)) {
|
|
1110
|
+
throw new StableValidationError(
|
|
1111
|
+
"toToken",
|
|
1112
|
+
toToken,
|
|
1113
|
+
`LI.FI bridge: invalid toToken address "${toToken}"`
|
|
1114
|
+
);
|
|
894
1115
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
if (
|
|
903
|
-
throw new
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1116
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
1117
|
+
throw new StableValidationError(
|
|
1118
|
+
"amount",
|
|
1119
|
+
amount,
|
|
1120
|
+
"LI.FI bridge: amount must be a finite positive number"
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
if (recipient !== void 0 && !isAddress2(recipient)) {
|
|
1124
|
+
throw new StableValidationError(
|
|
1125
|
+
"recipient",
|
|
1126
|
+
recipient,
|
|
1127
|
+
`LI.FI bridge: invalid recipient address "${recipient}"`
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
if (fromChain === toChain) {
|
|
1131
|
+
throw new StableValidationError(
|
|
1132
|
+
"toChain",
|
|
1133
|
+
toChain,
|
|
1134
|
+
"LI.FI bridge: fromChain and toChain must differ"
|
|
1135
|
+
);
|
|
909
1136
|
}
|
|
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
1137
|
}
|
|
921
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
922
1138
|
_requireFrom() {
|
|
923
|
-
const from = this.
|
|
1139
|
+
const from = this.config.walletClient?.account?.address ?? this.config.account?.address;
|
|
924
1140
|
if (!from) {
|
|
925
1141
|
throw new StableValidationError(
|
|
926
1142
|
"signer",
|
|
927
1143
|
void 0,
|
|
928
|
-
"
|
|
1144
|
+
"LI.FI: requires a signer \u2014 set walletClient or account in StableConfig"
|
|
929
1145
|
);
|
|
930
1146
|
}
|
|
931
1147
|
return from;
|
|
932
1148
|
}
|
|
933
|
-
async
|
|
1149
|
+
async _fetchQuote(qs) {
|
|
1150
|
+
const url = `https://li.quest/v1/quote?${qs}`;
|
|
934
1151
|
try {
|
|
935
|
-
await
|
|
1152
|
+
return await retryWithBackoff(() => this._fetchQuoteOnce(url));
|
|
936
1153
|
} catch (err) {
|
|
937
|
-
if (err
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1154
|
+
if (err instanceof StableQuoteError) throw err;
|
|
1155
|
+
if (err instanceof HttpError) {
|
|
1156
|
+
throw new StableQuoteError({
|
|
1157
|
+
provider: "lifi",
|
|
1158
|
+
shortMessage: err.message,
|
|
1159
|
+
httpStatus: err.status,
|
|
942
1160
|
cause: err
|
|
943
1161
|
});
|
|
944
1162
|
}
|
|
1163
|
+
throw new StableNetworkError({
|
|
1164
|
+
shortMessage: "LI.FI: quote request failed before reaching the server",
|
|
1165
|
+
url,
|
|
1166
|
+
cause: err
|
|
1167
|
+
});
|
|
945
1168
|
}
|
|
946
1169
|
}
|
|
947
|
-
async
|
|
1170
|
+
async _fetchQuoteOnce(url) {
|
|
1171
|
+
const res = await fetch(url);
|
|
1172
|
+
const body = await res.json().catch(() => void 0);
|
|
1173
|
+
if (!res.ok) {
|
|
1174
|
+
const providerMessage = isObject(body) && typeof body.message === "string" ? body.message : void 0;
|
|
1175
|
+
const providerCode = isObject(body) && typeof body.code === "string" ? body.code : void 0;
|
|
1176
|
+
if (res.status === 429 || res.status >= 500) {
|
|
1177
|
+
throw new HttpError(
|
|
1178
|
+
`LI.FI: ${providerMessage ?? res.statusText}`,
|
|
1179
|
+
res.status,
|
|
1180
|
+
true
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
throw new StableQuoteError({
|
|
1184
|
+
provider: "lifi",
|
|
1185
|
+
shortMessage: `LI.FI: ${providerMessage ?? res.statusText}`,
|
|
1186
|
+
httpStatus: res.status,
|
|
1187
|
+
providerCode,
|
|
1188
|
+
body
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
if (!isLiFiQuoteShape(body)) {
|
|
1192
|
+
throw new StableQuoteError({
|
|
1193
|
+
provider: "lifi",
|
|
1194
|
+
shortMessage: "LI.FI: unexpected quote response shape",
|
|
1195
|
+
httpStatus: res.status,
|
|
1196
|
+
body
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
return body;
|
|
1200
|
+
}
|
|
1201
|
+
_toSwapQuote(quote) {
|
|
1202
|
+
return {
|
|
1203
|
+
toAmount: Number(
|
|
1204
|
+
formatUnits3(
|
|
1205
|
+
BigInt(quote.estimate.toAmount),
|
|
1206
|
+
quote.action.toToken.decimals
|
|
1207
|
+
)
|
|
1208
|
+
),
|
|
1209
|
+
fromAmount: BigInt(quote.estimate.fromAmount),
|
|
1210
|
+
fromToken: quote.action.fromToken.address,
|
|
1211
|
+
approvalAddress: quote.estimate.approvalAddress,
|
|
1212
|
+
transactionRequest: quote.transactionRequest
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
async _executeSwapQuote(quote, srcRpc, expectedChainId) {
|
|
1216
|
+
const from = this._requireFrom();
|
|
1217
|
+
const { transactionRequest } = quote;
|
|
1218
|
+
const srcChainId = transactionRequest.chainId;
|
|
1219
|
+
if (srcChainId !== expectedChainId) {
|
|
1220
|
+
throw new StableQuoteError({
|
|
1221
|
+
provider: "lifi",
|
|
1222
|
+
shortMessage: `LI.FI: returned tx for chain ${srcChainId} but expected ${expectedChainId}`,
|
|
1223
|
+
providerCode: "chain_mismatch",
|
|
1224
|
+
body: quote
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
const srcChain = makeChain(srcChainId, srcRpc);
|
|
1228
|
+
const wc = this.config.walletClient ?? createWalletClient2({
|
|
1229
|
+
account: this.config.account,
|
|
1230
|
+
chain: srcChain,
|
|
1231
|
+
transport: this.config.transport ?? http3(srcRpc)
|
|
1232
|
+
});
|
|
948
1233
|
try {
|
|
949
|
-
|
|
1234
|
+
await wc.switchChain({ id: srcChainId });
|
|
950
1235
|
} catch (err) {
|
|
951
1236
|
throw new StableTransactionError({
|
|
952
|
-
phase: "
|
|
953
|
-
shortMessage:
|
|
954
|
-
chainId:
|
|
1237
|
+
phase: "switch_chain",
|
|
1238
|
+
shortMessage: `LI.FI: wallet rejected or failed to switch to chain ${srcChainId}`,
|
|
1239
|
+
chainId: srcChainId,
|
|
955
1240
|
cause: err
|
|
956
1241
|
});
|
|
957
1242
|
}
|
|
958
|
-
|
|
959
|
-
|
|
1243
|
+
await this._ensureApproval(
|
|
1244
|
+
wc,
|
|
1245
|
+
srcChain,
|
|
1246
|
+
srcRpc,
|
|
1247
|
+
srcChainId,
|
|
1248
|
+
from,
|
|
1249
|
+
quote
|
|
1250
|
+
);
|
|
1251
|
+
let hash;
|
|
960
1252
|
try {
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1253
|
+
hash = await wc.sendTransaction({
|
|
1254
|
+
to: transactionRequest.to,
|
|
1255
|
+
data: transactionRequest.data,
|
|
1256
|
+
value: transactionRequest.value && transactionRequest.value !== "0x" ? BigInt(transactionRequest.value) : 0n,
|
|
1257
|
+
gas: transactionRequest.gasLimit ? BigInt(transactionRequest.gasLimit) : void 0,
|
|
1258
|
+
account: wc.account,
|
|
1259
|
+
chain: srcChain
|
|
1260
|
+
});
|
|
1261
|
+
} catch (err) {
|
|
1262
|
+
throw new StableTransactionError({
|
|
1263
|
+
phase: "send",
|
|
1264
|
+
shortMessage: "LI.FI: swap/bridge transaction failed",
|
|
1265
|
+
chainId: srcChainId,
|
|
1266
|
+
cause: err
|
|
1267
|
+
});
|
|
970
1268
|
}
|
|
1269
|
+
return { txHash: hash, toAmount: quote.toAmount };
|
|
971
1270
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
const
|
|
975
|
-
if (
|
|
976
|
-
const
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1271
|
+
async _ensureApproval(wc, srcChain, srcRpc, srcChainId, from, quote) {
|
|
1272
|
+
const tokenAddr = quote.fromToken;
|
|
1273
|
+
const isNative = tokenAddr.toLowerCase() === "0x0000000000000000000000000000000000000000" || tokenAddr.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
1274
|
+
if (isNative || !quote.approvalAddress) return;
|
|
1275
|
+
const pc = createPublicClient3({
|
|
1276
|
+
chain: srcChain,
|
|
1277
|
+
transport: http3(srcRpc)
|
|
1278
|
+
});
|
|
1279
|
+
const allowance = await pc.readContract({
|
|
1280
|
+
address: tokenAddr,
|
|
1281
|
+
abi: erc20Abi3,
|
|
1282
|
+
functionName: "allowance",
|
|
1283
|
+
args: [from, quote.approvalAddress]
|
|
1284
|
+
});
|
|
1285
|
+
if (allowance >= quote.fromAmount) return;
|
|
1286
|
+
let approveHash;
|
|
1287
|
+
try {
|
|
1288
|
+
approveHash = await wc.writeContract({
|
|
1289
|
+
address: tokenAddr,
|
|
1290
|
+
abi: erc20Abi3,
|
|
1291
|
+
functionName: "approve",
|
|
1292
|
+
args: [quote.approvalAddress, quote.fromAmount],
|
|
1293
|
+
account: wc.account,
|
|
1294
|
+
chain: srcChain
|
|
1295
|
+
});
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
throw new StableTransactionError({
|
|
1298
|
+
phase: "approve",
|
|
1299
|
+
shortMessage: "LI.FI: token approve failed",
|
|
1300
|
+
chainId: srcChainId,
|
|
1301
|
+
cause: err
|
|
1302
|
+
});
|
|
983
1303
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1304
|
+
const approveReceipt = await pc.waitForTransactionReceipt({
|
|
1305
|
+
hash: approveHash
|
|
1306
|
+
});
|
|
1307
|
+
if (approveReceipt.status === "reverted") {
|
|
1308
|
+
throw new StableTransactionError({
|
|
1309
|
+
phase: "approve",
|
|
1310
|
+
shortMessage: "LI.FI: token approve reverted on-chain",
|
|
1311
|
+
txHash: approveReceipt.transactionHash,
|
|
1312
|
+
chainId: srcChainId
|
|
1313
|
+
});
|
|
988
1314
|
}
|
|
989
1315
|
}
|
|
990
1316
|
};
|
|
1317
|
+
function isObject(v) {
|
|
1318
|
+
return typeof v === "object" && v !== null;
|
|
1319
|
+
}
|
|
1320
|
+
function isLiFiQuoteShape(v) {
|
|
1321
|
+
if (!isObject(v)) return false;
|
|
1322
|
+
const tx = v.transactionRequest;
|
|
1323
|
+
const est = v.estimate;
|
|
1324
|
+
const act = v.action;
|
|
1325
|
+
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);
|
|
1326
|
+
}
|
|
991
1327
|
|
|
992
1328
|
// src/stable.ts
|
|
993
|
-
function validateTransferParams({
|
|
1329
|
+
function validateTransferParams({
|
|
1330
|
+
to,
|
|
1331
|
+
amount,
|
|
1332
|
+
token,
|
|
1333
|
+
tokenDecimals
|
|
1334
|
+
}) {
|
|
994
1335
|
if (!isAddress3(to)) {
|
|
995
|
-
throw new StableValidationError(
|
|
1336
|
+
throw new StableValidationError(
|
|
1337
|
+
"to",
|
|
1338
|
+
to,
|
|
1339
|
+
`transfer: invalid recipient address "${to}"`
|
|
1340
|
+
);
|
|
996
1341
|
}
|
|
997
1342
|
if (!Number.isFinite(amount) || amount <= 0) {
|
|
998
|
-
throw new StableValidationError(
|
|
1343
|
+
throw new StableValidationError(
|
|
1344
|
+
"amount",
|
|
1345
|
+
amount,
|
|
1346
|
+
"transfer: amount must be a finite positive number"
|
|
1347
|
+
);
|
|
999
1348
|
}
|
|
1000
1349
|
if (token !== void 0 && !isAddress3(token)) {
|
|
1001
|
-
throw new StableValidationError(
|
|
1350
|
+
throw new StableValidationError(
|
|
1351
|
+
"token",
|
|
1352
|
+
token,
|
|
1353
|
+
`transfer: invalid token address "${token}"`
|
|
1354
|
+
);
|
|
1002
1355
|
}
|
|
1003
1356
|
if (tokenDecimals !== void 0 && (!Number.isInteger(tokenDecimals) || tokenDecimals < 0 || tokenDecimals > 77)) {
|
|
1004
|
-
throw new StableValidationError(
|
|
1357
|
+
throw new StableValidationError(
|
|
1358
|
+
"tokenDecimals",
|
|
1359
|
+
tokenDecimals,
|
|
1360
|
+
"transfer: tokenDecimals must be an integer between 0 and 77"
|
|
1361
|
+
);
|
|
1005
1362
|
}
|
|
1006
1363
|
}
|
|
1007
1364
|
async function resolveTokenDecimals(publicClient, token, tokenDecimals) {
|
|
1008
1365
|
if (tokenDecimals !== void 0) return tokenDecimals;
|
|
1009
1366
|
try {
|
|
1010
|
-
return await publicClient.readContract({
|
|
1367
|
+
return await publicClient.readContract({
|
|
1368
|
+
address: token,
|
|
1369
|
+
abi: erc20Abi4,
|
|
1370
|
+
functionName: "decimals"
|
|
1371
|
+
});
|
|
1011
1372
|
} catch {
|
|
1012
1373
|
throw new StableValidationError(
|
|
1013
1374
|
"tokenDecimals",
|
|
@@ -1025,7 +1386,11 @@ async function executeTokenTransfer(wc, publicClient, token, to, amount, tokenDe
|
|
|
1025
1386
|
`transfer: no contract found at "${token}" on chain ${chain.id} \u2014 check the address and network`
|
|
1026
1387
|
);
|
|
1027
1388
|
}
|
|
1028
|
-
const decimals = await resolveTokenDecimals(
|
|
1389
|
+
const decimals = await resolveTokenDecimals(
|
|
1390
|
+
publicClient,
|
|
1391
|
+
token,
|
|
1392
|
+
tokenDecimals
|
|
1393
|
+
);
|
|
1029
1394
|
return wc.writeContract({
|
|
1030
1395
|
address: token,
|
|
1031
1396
|
abi: erc20Abi4,
|
|
@@ -1044,10 +1409,18 @@ function createStable({
|
|
|
1044
1409
|
earn: earnConfig
|
|
1045
1410
|
}) {
|
|
1046
1411
|
const chain = network === "mainnet" /* Mainnet */ ? stableMainnet2 : stableTestnet2;
|
|
1047
|
-
const rpc = (rpcOverride ?? chain.rpcUrls.default.http[0]).replace(
|
|
1412
|
+
const rpc = (rpcOverride ?? chain.rpcUrls.default.http[0]).replace(
|
|
1413
|
+
/\/$/,
|
|
1414
|
+
""
|
|
1415
|
+
);
|
|
1048
1416
|
const signerConfig = { account, transport, walletClient };
|
|
1049
1417
|
const lifi = new LiFiService({ ...signerConfig, rpc, chainId: chain.id });
|
|
1050
|
-
const morpho = earnConfig ? new MorphoService({
|
|
1418
|
+
const morpho = earnConfig ? new MorphoService({
|
|
1419
|
+
...signerConfig,
|
|
1420
|
+
rpc,
|
|
1421
|
+
chainId: chain.id,
|
|
1422
|
+
vault: earnConfig.vault
|
|
1423
|
+
}) : null;
|
|
1051
1424
|
function requireEarn() {
|
|
1052
1425
|
if (!morpho) {
|
|
1053
1426
|
throw new StableValidationError(
|
|
@@ -1077,8 +1450,29 @@ function createStable({
|
|
|
1077
1450
|
cause: err
|
|
1078
1451
|
});
|
|
1079
1452
|
}
|
|
1080
|
-
const publicClient = createPublicClient4({
|
|
1081
|
-
|
|
1453
|
+
const publicClient = createPublicClient4({
|
|
1454
|
+
chain,
|
|
1455
|
+
transport: http4(rpc)
|
|
1456
|
+
});
|
|
1457
|
+
const hash = token !== void 0 ? await executeTokenTransfer(
|
|
1458
|
+
wc,
|
|
1459
|
+
publicClient,
|
|
1460
|
+
token,
|
|
1461
|
+
to,
|
|
1462
|
+
amount,
|
|
1463
|
+
tokenDecimals,
|
|
1464
|
+
acc,
|
|
1465
|
+
chain
|
|
1466
|
+
) : (
|
|
1467
|
+
// Native transfer on Stable mainnet/testnet. Native USDT0 uses 18 decimals
|
|
1468
|
+
// on both networks, matching standard EVM native-token decimals.
|
|
1469
|
+
await wc.sendTransaction({
|
|
1470
|
+
account: acc,
|
|
1471
|
+
chain,
|
|
1472
|
+
to,
|
|
1473
|
+
value: parseUnits4(String(amount), 18)
|
|
1474
|
+
})
|
|
1475
|
+
);
|
|
1082
1476
|
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
1083
1477
|
if (receipt.status === "reverted") {
|
|
1084
1478
|
throw new StableTransactionError({
|
|
@@ -1100,6 +1494,7 @@ function createStable({
|
|
|
1100
1494
|
deposit: (params) => requireEarn().deposit(params),
|
|
1101
1495
|
bulkDeposit: (items, options) => requireEarn().bulkDeposit(items, options),
|
|
1102
1496
|
withdraw: (params) => requireEarn().withdraw(params),
|
|
1497
|
+
bulkWithdraw: (items, options) => requireEarn().bulkWithdraw(items, options),
|
|
1103
1498
|
redeem: (params) => requireEarn().redeem(params),
|
|
1104
1499
|
forceWithdraw: (params) => requireEarn().forceWithdraw(params),
|
|
1105
1500
|
forceRedeem: (params) => requireEarn().forceRedeem(params)
|
|
@@ -1110,6 +1505,7 @@ function createStableReader(config) {
|
|
|
1110
1505
|
const reader = new EarnReader(config);
|
|
1111
1506
|
return {
|
|
1112
1507
|
earn: {
|
|
1508
|
+
getYield: () => reader.getYield(),
|
|
1113
1509
|
position: () => reader.position(),
|
|
1114
1510
|
preview: (params) => reader.preview(params),
|
|
1115
1511
|
withdrawability: (params) => reader.withdrawability(params)
|