@agentlayer.tech/wallet 0.1.44 → 0.1.48
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/.openclaw/extensions/agent-wallet/index.ts +99 -0
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +6 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +62 -0
- package/README.md +2 -2
- package/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/openclaw_adapter.py +884 -37
- package/agent-wallet/agent_wallet/providers/jupiter.py +5 -0
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +6 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +189 -0
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +14 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +404 -0
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/install_agent_wallet.py +33 -8
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +1 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/README.md +31 -0
- package/wdk-evm-wallet/package-lock.json +268 -64
- package/wdk-evm-wallet/package.json +4 -1
- package/wdk-evm-wallet/src/config.js +2 -0
- package/wdk-evm-wallet/src/server.js +66 -0
- package/wdk-evm-wallet/src/wdk_evm_wallet.js +2725 -939
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { Contract, Interface } from "ethers";
|
|
4
|
+
import { isRequirementApproval, isRequirementAuthorization } from "@morpho-org/morpho-sdk";
|
|
4
5
|
import WDK from "@tetherto/wdk";
|
|
6
|
+
import MorphoProtocolEvm from "@morpho-org/wdk-protocol-lending-morpho-evm";
|
|
5
7
|
import { AaveV3Base, AaveV3Ethereum } from "@bgd-labs/aave-address-book";
|
|
6
8
|
import AaveProtocolEvm from "@tetherto/wdk-protocol-lending-aave-evm";
|
|
7
9
|
import VeloraProtocolEvm from "@tetherto/wdk-protocol-swap-velora-evm";
|
|
@@ -71,9 +73,13 @@ const LIDO_WITHDRAWAL_QUEUE_ABI = [
|
|
|
71
73
|
"function getWithdrawalStatus(uint256[] _requestIds) view returns ((uint256 amountOfStETH,uint256 amountOfShares,address owner,uint256 timestamp,bool isFinalized,bool isClaimed)[] statuses)",
|
|
72
74
|
"function claimWithdrawal(uint256 _requestId)",
|
|
73
75
|
];
|
|
76
|
+
const MORPHO_AUTHORIZATION_ABI = [
|
|
77
|
+
"function setAuthorization(address authorized, bool isAuthorized)",
|
|
78
|
+
];
|
|
74
79
|
const LIDO_WSTETH_INTERFACE = new Interface(LIDO_WSTETH_ABI);
|
|
75
80
|
const LIDO_REFERRAL_STAKER_INTERFACE = new Interface(LIDO_REFERRAL_STAKER_ABI);
|
|
76
81
|
const LIDO_WITHDRAWAL_QUEUE_INTERFACE = new Interface(LIDO_WITHDRAWAL_QUEUE_ABI);
|
|
82
|
+
const MORPHO_AUTHORIZATION_INTERFACE = new Interface(MORPHO_AUTHORIZATION_ABI);
|
|
77
83
|
const LIFI_CHAIN_IDS_BY_NETWORK = {
|
|
78
84
|
ethereum: "1",
|
|
79
85
|
base: "8453",
|
|
@@ -88,6 +94,346 @@ const LIFI_CHAIN_ALIASES = {
|
|
|
88
94
|
sol: "1151111081099710",
|
|
89
95
|
solana: "1151111081099710",
|
|
90
96
|
};
|
|
97
|
+
const MORPHO_DEFAULT_LIST_LIMIT = 100;
|
|
98
|
+
const MORPHO_MAX_LIST_LIMIT = 500;
|
|
99
|
+
const MORPHO_VAULT_LIST_QUERY = `
|
|
100
|
+
query MorphoVaultV2List($first: Int!, $where: VaultV2sFilters, $orderBy: VaultV2OrderBy, $orderDirection: OrderDirection) {
|
|
101
|
+
vaultV2s(first: $first, where: $where, orderBy: $orderBy, orderDirection: $orderDirection) {
|
|
102
|
+
items {
|
|
103
|
+
address
|
|
104
|
+
symbol
|
|
105
|
+
name
|
|
106
|
+
listed
|
|
107
|
+
totalAssets
|
|
108
|
+
totalAssetsUsd
|
|
109
|
+
totalSupply
|
|
110
|
+
liquidity
|
|
111
|
+
liquidityUsd
|
|
112
|
+
idleAssets
|
|
113
|
+
idleAssetsUsd
|
|
114
|
+
sharePrice
|
|
115
|
+
avgNetApy
|
|
116
|
+
avgNetApyExcludingRewards
|
|
117
|
+
performanceFee
|
|
118
|
+
managementFee
|
|
119
|
+
maxRate
|
|
120
|
+
warnings {
|
|
121
|
+
type
|
|
122
|
+
level
|
|
123
|
+
}
|
|
124
|
+
asset {
|
|
125
|
+
id
|
|
126
|
+
address
|
|
127
|
+
decimals
|
|
128
|
+
symbol
|
|
129
|
+
name
|
|
130
|
+
priceUsd
|
|
131
|
+
yield {
|
|
132
|
+
apr
|
|
133
|
+
lookback
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
chain {
|
|
137
|
+
id
|
|
138
|
+
network
|
|
139
|
+
}
|
|
140
|
+
rewards {
|
|
141
|
+
supplyApr
|
|
142
|
+
asset {
|
|
143
|
+
address
|
|
144
|
+
symbol
|
|
145
|
+
chain {
|
|
146
|
+
id
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
`;
|
|
154
|
+
const MORPHO_VAULT_BY_ADDRESS_QUERY = `
|
|
155
|
+
query MorphoVaultV2ByAddress($address: String!, $chainId: Int!) {
|
|
156
|
+
vaultV2ByAddress(address: $address, chainId: $chainId) {
|
|
157
|
+
address
|
|
158
|
+
symbol
|
|
159
|
+
name
|
|
160
|
+
listed
|
|
161
|
+
totalAssets
|
|
162
|
+
totalAssetsUsd
|
|
163
|
+
totalSupply
|
|
164
|
+
liquidity
|
|
165
|
+
liquidityUsd
|
|
166
|
+
idleAssets
|
|
167
|
+
idleAssetsUsd
|
|
168
|
+
sharePrice
|
|
169
|
+
avgNetApy
|
|
170
|
+
avgNetApyExcludingRewards
|
|
171
|
+
performanceFee
|
|
172
|
+
managementFee
|
|
173
|
+
maxRate
|
|
174
|
+
metadata {
|
|
175
|
+
description
|
|
176
|
+
image
|
|
177
|
+
}
|
|
178
|
+
warnings {
|
|
179
|
+
type
|
|
180
|
+
level
|
|
181
|
+
}
|
|
182
|
+
asset {
|
|
183
|
+
id
|
|
184
|
+
address
|
|
185
|
+
decimals
|
|
186
|
+
symbol
|
|
187
|
+
name
|
|
188
|
+
priceUsd
|
|
189
|
+
yield {
|
|
190
|
+
apr
|
|
191
|
+
lookback
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
chain {
|
|
195
|
+
id
|
|
196
|
+
network
|
|
197
|
+
}
|
|
198
|
+
rewards {
|
|
199
|
+
supplyApr
|
|
200
|
+
asset {
|
|
201
|
+
address
|
|
202
|
+
symbol
|
|
203
|
+
chain {
|
|
204
|
+
id
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
adapters(first: 20) {
|
|
209
|
+
items {
|
|
210
|
+
__typename
|
|
211
|
+
address
|
|
212
|
+
type
|
|
213
|
+
assets
|
|
214
|
+
assetsUsd
|
|
215
|
+
... on MorphoMarketV1Adapter {
|
|
216
|
+
positions(first: 50) {
|
|
217
|
+
items {
|
|
218
|
+
market {
|
|
219
|
+
marketId
|
|
220
|
+
collateralAsset {
|
|
221
|
+
symbol
|
|
222
|
+
address
|
|
223
|
+
}
|
|
224
|
+
loanAsset {
|
|
225
|
+
symbol
|
|
226
|
+
address
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
state {
|
|
230
|
+
supplyAssets
|
|
231
|
+
supplyAssetsUsd
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
... on MetaMorphoAdapter {
|
|
237
|
+
metaMorpho {
|
|
238
|
+
address
|
|
239
|
+
name
|
|
240
|
+
symbol
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
... on MorphoVaultV2Adapter {
|
|
244
|
+
innerVault {
|
|
245
|
+
address
|
|
246
|
+
name
|
|
247
|
+
symbol
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
`;
|
|
255
|
+
const MORPHO_MARKET_LIST_QUERY = `
|
|
256
|
+
query MorphoMarketList($first: Int!, $where: MarketFilters, $orderBy: MarketOrderBy, $orderDirection: OrderDirection) {
|
|
257
|
+
markets(first: $first, where: $where, orderBy: $orderBy, orderDirection: $orderDirection) {
|
|
258
|
+
items {
|
|
259
|
+
marketId
|
|
260
|
+
lltv
|
|
261
|
+
irmAddress
|
|
262
|
+
warnings {
|
|
263
|
+
type
|
|
264
|
+
level
|
|
265
|
+
}
|
|
266
|
+
oracle {
|
|
267
|
+
address
|
|
268
|
+
type
|
|
269
|
+
}
|
|
270
|
+
loanAsset {
|
|
271
|
+
address
|
|
272
|
+
symbol
|
|
273
|
+
decimals
|
|
274
|
+
name
|
|
275
|
+
priceUsd
|
|
276
|
+
}
|
|
277
|
+
collateralAsset {
|
|
278
|
+
address
|
|
279
|
+
symbol
|
|
280
|
+
decimals
|
|
281
|
+
name
|
|
282
|
+
priceUsd
|
|
283
|
+
}
|
|
284
|
+
state {
|
|
285
|
+
collateralAssets
|
|
286
|
+
collateralAssetsUsd
|
|
287
|
+
borrowAssets
|
|
288
|
+
borrowAssetsUsd
|
|
289
|
+
supplyAssets
|
|
290
|
+
supplyAssetsUsd
|
|
291
|
+
liquidityAssets
|
|
292
|
+
liquidityAssetsUsd
|
|
293
|
+
borrowApy
|
|
294
|
+
avgBorrowApy
|
|
295
|
+
avgNetBorrowApy
|
|
296
|
+
supplyApy
|
|
297
|
+
avgSupplyApy
|
|
298
|
+
avgNetSupplyApy
|
|
299
|
+
fee
|
|
300
|
+
utilization
|
|
301
|
+
rewards {
|
|
302
|
+
supplyApr
|
|
303
|
+
borrowApr
|
|
304
|
+
asset {
|
|
305
|
+
address
|
|
306
|
+
symbol
|
|
307
|
+
chain {
|
|
308
|
+
id
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
`;
|
|
317
|
+
const MORPHO_MARKET_BY_ID_QUERY = `
|
|
318
|
+
query MorphoMarketById($marketId: String!, $chainId: Int!) {
|
|
319
|
+
marketById(marketId: $marketId, chainId: $chainId) {
|
|
320
|
+
marketId
|
|
321
|
+
lltv
|
|
322
|
+
irmAddress
|
|
323
|
+
warnings {
|
|
324
|
+
type
|
|
325
|
+
level
|
|
326
|
+
}
|
|
327
|
+
oracle {
|
|
328
|
+
address
|
|
329
|
+
type
|
|
330
|
+
}
|
|
331
|
+
loanAsset {
|
|
332
|
+
address
|
|
333
|
+
symbol
|
|
334
|
+
decimals
|
|
335
|
+
name
|
|
336
|
+
priceUsd
|
|
337
|
+
}
|
|
338
|
+
collateralAsset {
|
|
339
|
+
address
|
|
340
|
+
symbol
|
|
341
|
+
decimals
|
|
342
|
+
name
|
|
343
|
+
priceUsd
|
|
344
|
+
}
|
|
345
|
+
state {
|
|
346
|
+
collateralAssets
|
|
347
|
+
collateralAssetsUsd
|
|
348
|
+
borrowAssets
|
|
349
|
+
borrowAssetsUsd
|
|
350
|
+
supplyAssets
|
|
351
|
+
supplyAssetsUsd
|
|
352
|
+
liquidityAssets
|
|
353
|
+
liquidityAssetsUsd
|
|
354
|
+
borrowApy
|
|
355
|
+
avgBorrowApy
|
|
356
|
+
avgNetBorrowApy
|
|
357
|
+
supplyApy
|
|
358
|
+
avgSupplyApy
|
|
359
|
+
avgNetSupplyApy
|
|
360
|
+
fee
|
|
361
|
+
utilization
|
|
362
|
+
rewards {
|
|
363
|
+
supplyApr
|
|
364
|
+
borrowApr
|
|
365
|
+
asset {
|
|
366
|
+
address
|
|
367
|
+
symbol
|
|
368
|
+
chain {
|
|
369
|
+
id
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
supplyingVaults {
|
|
375
|
+
address
|
|
376
|
+
name
|
|
377
|
+
symbol
|
|
378
|
+
}
|
|
379
|
+
supplyingVaultV2s {
|
|
380
|
+
address
|
|
381
|
+
name
|
|
382
|
+
symbol
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
`;
|
|
387
|
+
const MORPHO_USER_OVERVIEW_QUERY = `
|
|
388
|
+
query MorphoUserByAddress($address: String!, $chainId: Int!) {
|
|
389
|
+
userByAddress(address: $address, chainId: $chainId) {
|
|
390
|
+
address
|
|
391
|
+
marketPositions {
|
|
392
|
+
market {
|
|
393
|
+
marketId
|
|
394
|
+
loanAsset {
|
|
395
|
+
address
|
|
396
|
+
symbol
|
|
397
|
+
decimals
|
|
398
|
+
name
|
|
399
|
+
}
|
|
400
|
+
collateralAsset {
|
|
401
|
+
address
|
|
402
|
+
symbol
|
|
403
|
+
decimals
|
|
404
|
+
name
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
state {
|
|
408
|
+
supplyShares
|
|
409
|
+
supplyAssets
|
|
410
|
+
supplyAssetsUsd
|
|
411
|
+
borrowShares
|
|
412
|
+
borrowAssets
|
|
413
|
+
borrowAssetsUsd
|
|
414
|
+
collateral
|
|
415
|
+
collateralUsd
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
vaultV2Positions {
|
|
419
|
+
vault {
|
|
420
|
+
address
|
|
421
|
+
name
|
|
422
|
+
symbol
|
|
423
|
+
asset {
|
|
424
|
+
address
|
|
425
|
+
symbol
|
|
426
|
+
decimals
|
|
427
|
+
name
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
shares
|
|
431
|
+
assets
|
|
432
|
+
assetsUsd
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
`;
|
|
91
437
|
|
|
92
438
|
function createTaggedError(message, code, details = {}) {
|
|
93
439
|
const error = new Error(message);
|
|
@@ -209,6 +555,136 @@ function assertLidoSupportedNetwork(network) {
|
|
|
209
555
|
}
|
|
210
556
|
}
|
|
211
557
|
|
|
558
|
+
function assertMorphoSupportedNetwork(network) {
|
|
559
|
+
if (!["ethereum", "base"].includes(network)) {
|
|
560
|
+
throw new Error("Morpho is currently supported only on ethereum and base mainnet.");
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function normalizeMorphoListLimit(value) {
|
|
565
|
+
if (value === undefined || value === null || value === "") {
|
|
566
|
+
return MORPHO_DEFAULT_LIST_LIMIT;
|
|
567
|
+
}
|
|
568
|
+
const limit = assertNonNegativeInteger(value, "limit");
|
|
569
|
+
if (limit < 1 || limit > MORPHO_MAX_LIST_LIMIT) {
|
|
570
|
+
throw new Error(`limit must be between 1 and ${MORPHO_MAX_LIST_LIMIT}.`);
|
|
571
|
+
}
|
|
572
|
+
return limit;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function normalizeMorphoListedOnly(value, fallback = true) {
|
|
576
|
+
if (value === undefined || value === null || value === "") {
|
|
577
|
+
return fallback;
|
|
578
|
+
}
|
|
579
|
+
if (typeof value !== "boolean") {
|
|
580
|
+
throw new Error("listedOnly must be a boolean.");
|
|
581
|
+
}
|
|
582
|
+
return value;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function buildOrderFieldLookup(fields) {
|
|
586
|
+
const lookup = new Map();
|
|
587
|
+
for (const field of fields) {
|
|
588
|
+
lookup.set(field.toLowerCase(), field);
|
|
589
|
+
}
|
|
590
|
+
return lookup;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const MORPHO_VAULT_ORDER_LOOKUP = buildOrderFieldLookup([
|
|
594
|
+
"Address",
|
|
595
|
+
"TotalAssets",
|
|
596
|
+
"TotalAssetsUsd",
|
|
597
|
+
"TotalSupply",
|
|
598
|
+
"Liquidity",
|
|
599
|
+
"LiquidityUsd",
|
|
600
|
+
"Apy",
|
|
601
|
+
"NetApy",
|
|
602
|
+
"RealAssets",
|
|
603
|
+
"RealAssetsUsd",
|
|
604
|
+
"IdleAssets",
|
|
605
|
+
"IdleAssetsUsd",
|
|
606
|
+
]);
|
|
607
|
+
|
|
608
|
+
const MORPHO_MARKET_ORDER_LOOKUP = buildOrderFieldLookup([
|
|
609
|
+
"UniqueKey",
|
|
610
|
+
"Lltv",
|
|
611
|
+
"BorrowAssets",
|
|
612
|
+
"BorrowAssetsUsd",
|
|
613
|
+
"SupplyAssets",
|
|
614
|
+
"SupplyAssetsUsd",
|
|
615
|
+
"BorrowShares",
|
|
616
|
+
"SupplyShares",
|
|
617
|
+
"Utilization",
|
|
618
|
+
"ApyAtTarget",
|
|
619
|
+
"SupplyApy",
|
|
620
|
+
"NetSupplyApy",
|
|
621
|
+
"BorrowApy",
|
|
622
|
+
"NetBorrowApy",
|
|
623
|
+
"Fee",
|
|
624
|
+
"LoanAssetSymbol",
|
|
625
|
+
"CollateralAssetSymbol",
|
|
626
|
+
"TotalLiquidityUsd",
|
|
627
|
+
"AvgBorrowApy",
|
|
628
|
+
"AvgNetBorrowApy",
|
|
629
|
+
"DailyBorrowApy",
|
|
630
|
+
"DailyNetBorrowApy",
|
|
631
|
+
"SizeUsd",
|
|
632
|
+
]);
|
|
633
|
+
|
|
634
|
+
function normalizeMorphoOrderBy(value, lookup, fallback, fieldName = "orderBy") {
|
|
635
|
+
if (value === undefined || value === null || value === "") {
|
|
636
|
+
return fallback;
|
|
637
|
+
}
|
|
638
|
+
const canonical = lookup.get(String(value).trim().toLowerCase());
|
|
639
|
+
if (!canonical) {
|
|
640
|
+
throw new Error(`${fieldName} must be one of: ${[...lookup.values()].join(", ")}.`);
|
|
641
|
+
}
|
|
642
|
+
return canonical;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function normalizeMorphoOrderDirection(value, fallback = "Desc") {
|
|
646
|
+
if (value === undefined || value === null || value === "") {
|
|
647
|
+
return fallback;
|
|
648
|
+
}
|
|
649
|
+
const normalized = String(value).trim().toLowerCase();
|
|
650
|
+
if (normalized === "asc") {
|
|
651
|
+
return "Asc";
|
|
652
|
+
}
|
|
653
|
+
if (normalized === "desc") {
|
|
654
|
+
return "Desc";
|
|
655
|
+
}
|
|
656
|
+
throw new Error("orderDirection must be 'asc' or 'desc'.");
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function normalizeMorphoSearch(value) {
|
|
660
|
+
if (value === undefined || value === null || value === "") {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
if (typeof value !== "string") {
|
|
664
|
+
throw new Error("search must be a string.");
|
|
665
|
+
}
|
|
666
|
+
return value.trim() || null;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function normalizeOptionalAddressFilter(value, fieldName) {
|
|
670
|
+
if (value === undefined || value === null || value === "") {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
const values = Array.isArray(value) ? value : [value];
|
|
674
|
+
const normalized = values
|
|
675
|
+
.filter((entry) => entry !== undefined && entry !== null && String(entry).trim())
|
|
676
|
+
.map((entry) => normalizeAddress(entry, fieldName));
|
|
677
|
+
return normalized.length > 0 ? normalized : null;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function assertMorphoMarketId(value, fieldName = "marketId") {
|
|
681
|
+
const id = assertNonEmptyString(value, fieldName);
|
|
682
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(id)) {
|
|
683
|
+
throw new Error(`${fieldName} must be a 32-byte hex string.`);
|
|
684
|
+
}
|
|
685
|
+
return id;
|
|
686
|
+
}
|
|
687
|
+
|
|
212
688
|
function normalizeAaveOperation(value) {
|
|
213
689
|
const operation = assertNonEmptyString(value, "operation").toLowerCase();
|
|
214
690
|
if (!["supply", "withdraw", "borrow", "repay"].includes(operation)) {
|
|
@@ -217,6 +693,24 @@ function normalizeAaveOperation(value) {
|
|
|
217
693
|
return operation;
|
|
218
694
|
}
|
|
219
695
|
|
|
696
|
+
function normalizeMorphoVaultOperation(value) {
|
|
697
|
+
const operation = assertNonEmptyString(value, "operation").toLowerCase();
|
|
698
|
+
if (!["supply", "withdraw"].includes(operation)) {
|
|
699
|
+
throw new Error("operation must be one of: supply, withdraw.");
|
|
700
|
+
}
|
|
701
|
+
return operation;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function normalizeMorphoMarketOperation(value) {
|
|
705
|
+
const operation = assertNonEmptyString(value, "operation").toLowerCase();
|
|
706
|
+
if (!["supply_collateral", "borrow", "repay", "withdraw_collateral"].includes(operation)) {
|
|
707
|
+
throw new Error(
|
|
708
|
+
"operation must be one of: supply_collateral, borrow, repay, withdraw_collateral."
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
return operation;
|
|
712
|
+
}
|
|
713
|
+
|
|
220
714
|
function normalizeLidoOperation(value) {
|
|
221
715
|
const operation = assertNonEmptyString(value, "operation").toLowerCase();
|
|
222
716
|
if (!["stake_eth_for_wsteth", "wrap_steth", "unwrap_wsteth"].includes(operation)) {
|
|
@@ -517,6 +1011,152 @@ function buildAaveOperationRequest({ operation, token, tokenAddress, amount, onB
|
|
|
517
1011
|
};
|
|
518
1012
|
}
|
|
519
1013
|
|
|
1014
|
+
function parseOptionalPositiveBigIntString(value, fieldName) {
|
|
1015
|
+
if (value === undefined || value === null || value === "") {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
return assertPositiveBigIntString(value, fieldName);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function normalizeMorphoVaultTarget({ vaultAddress, vaultPreset }) {
|
|
1022
|
+
const normalizedVaultAddress = vaultAddress ? normalizeAddress(vaultAddress, "vaultAddress") : null;
|
|
1023
|
+
const normalizedVaultPreset =
|
|
1024
|
+
vaultPreset !== undefined && vaultPreset !== null && String(vaultPreset).trim()
|
|
1025
|
+
? assertNonEmptyString(vaultPreset, "vaultPreset")
|
|
1026
|
+
: null;
|
|
1027
|
+
if (normalizedVaultAddress && normalizedVaultPreset) {
|
|
1028
|
+
throw new Error("Provide either vaultAddress or vaultPreset, not both.");
|
|
1029
|
+
}
|
|
1030
|
+
if (!normalizedVaultAddress && !normalizedVaultPreset) {
|
|
1031
|
+
throw new Error("vaultAddress or vaultPreset is required.");
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
vaultAddress: normalizedVaultAddress,
|
|
1035
|
+
vaultPreset: normalizedVaultPreset,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function normalizeMorphoMarketTarget({ marketId, marketPreset }) {
|
|
1040
|
+
const normalizedMarketId =
|
|
1041
|
+
marketId === undefined || marketId === null || marketId === ""
|
|
1042
|
+
? null
|
|
1043
|
+
: assertMorphoMarketId(marketId, "marketId");
|
|
1044
|
+
const normalizedMarketPreset =
|
|
1045
|
+
marketPreset !== undefined && marketPreset !== null && String(marketPreset).trim()
|
|
1046
|
+
? assertNonEmptyString(marketPreset, "marketPreset")
|
|
1047
|
+
: null;
|
|
1048
|
+
if (normalizedMarketId && normalizedMarketPreset) {
|
|
1049
|
+
throw new Error("Provide either marketId or marketPreset, not both.");
|
|
1050
|
+
}
|
|
1051
|
+
if (!normalizedMarketId && !normalizedMarketPreset) {
|
|
1052
|
+
throw new Error("marketId or marketPreset is required.");
|
|
1053
|
+
}
|
|
1054
|
+
return {
|
|
1055
|
+
marketId: normalizedMarketId,
|
|
1056
|
+
marketPreset: normalizedMarketPreset,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function buildMorphoVaultOperationRequest({
|
|
1061
|
+
operation,
|
|
1062
|
+
vaultAddress,
|
|
1063
|
+
vaultPreset,
|
|
1064
|
+
token,
|
|
1065
|
+
tokenAddress,
|
|
1066
|
+
amount,
|
|
1067
|
+
nativeAmount,
|
|
1068
|
+
onBehalfOf,
|
|
1069
|
+
to,
|
|
1070
|
+
}) {
|
|
1071
|
+
if (onBehalfOf !== undefined && onBehalfOf !== null && String(onBehalfOf).trim()) {
|
|
1072
|
+
throw new Error("Morpho delegated onBehalfOf operations are not exposed by this local wallet runtime.");
|
|
1073
|
+
}
|
|
1074
|
+
if (to !== undefined && to !== null && String(to).trim()) {
|
|
1075
|
+
throw new Error("Morpho third-party withdraw destinations are not exposed by this local wallet runtime.");
|
|
1076
|
+
}
|
|
1077
|
+
const preferredToken = tokenAddress ?? token;
|
|
1078
|
+
if (tokenAddress && token && String(tokenAddress).toLowerCase() !== String(token).toLowerCase()) {
|
|
1079
|
+
throw new Error("tokenAddress and token must refer to the same address.");
|
|
1080
|
+
}
|
|
1081
|
+
const request = {
|
|
1082
|
+
target: normalizeMorphoVaultTarget({ vaultAddress, vaultPreset }),
|
|
1083
|
+
operation: normalizeMorphoVaultOperation(operation),
|
|
1084
|
+
token: normalizeAddress(preferredToken, "tokenAddress"),
|
|
1085
|
+
};
|
|
1086
|
+
if (request.operation === "supply") {
|
|
1087
|
+
const supplyAmount = parseOptionalPositiveBigIntString(amount, "amount");
|
|
1088
|
+
const supplyNativeAmount = parseOptionalPositiveBigIntString(nativeAmount, "nativeAmount");
|
|
1089
|
+
if (supplyAmount === null && supplyNativeAmount === null) {
|
|
1090
|
+
throw new Error("amount or nativeAmount is required for Morpho vault supply.");
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
...request,
|
|
1094
|
+
amount: supplyAmount,
|
|
1095
|
+
nativeAmount: supplyNativeAmount,
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
return {
|
|
1099
|
+
...request,
|
|
1100
|
+
amount: assertPositiveBigIntString(amount, "amount"),
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function buildMorphoMarketOperationRequest({
|
|
1105
|
+
operation,
|
|
1106
|
+
marketId,
|
|
1107
|
+
marketPreset,
|
|
1108
|
+
token,
|
|
1109
|
+
tokenAddress,
|
|
1110
|
+
amount,
|
|
1111
|
+
nativeAmount,
|
|
1112
|
+
onBehalfOf,
|
|
1113
|
+
to,
|
|
1114
|
+
}) {
|
|
1115
|
+
if (onBehalfOf !== undefined && onBehalfOf !== null && String(onBehalfOf).trim()) {
|
|
1116
|
+
throw new Error("Morpho delegated onBehalfOf operations are not exposed by this local wallet runtime.");
|
|
1117
|
+
}
|
|
1118
|
+
if (to !== undefined && to !== null && String(to).trim()) {
|
|
1119
|
+
throw new Error("Morpho third-party withdraw destinations are not exposed by this local wallet runtime.");
|
|
1120
|
+
}
|
|
1121
|
+
const preferredToken = tokenAddress ?? token;
|
|
1122
|
+
if (tokenAddress && token && String(tokenAddress).toLowerCase() !== String(token).toLowerCase()) {
|
|
1123
|
+
throw new Error("tokenAddress and token must refer to the same address.");
|
|
1124
|
+
}
|
|
1125
|
+
const request = {
|
|
1126
|
+
target: normalizeMorphoMarketTarget({ marketId, marketPreset }),
|
|
1127
|
+
operation: normalizeMorphoMarketOperation(operation),
|
|
1128
|
+
token: normalizeAddress(preferredToken, "tokenAddress"),
|
|
1129
|
+
};
|
|
1130
|
+
if (request.operation === "repay") {
|
|
1131
|
+
const normalizedAmount = String(amount ?? "").trim().toLowerCase();
|
|
1132
|
+
return {
|
|
1133
|
+
...request,
|
|
1134
|
+
amount:
|
|
1135
|
+
normalizedAmount === "max"
|
|
1136
|
+
? "max"
|
|
1137
|
+
: assertPositiveBigIntString(amount, "amount"),
|
|
1138
|
+
nativeAmount: null,
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
if (request.operation === "supply_collateral") {
|
|
1142
|
+
const collateralAmount = parseOptionalPositiveBigIntString(amount, "amount");
|
|
1143
|
+
const collateralNativeAmount = parseOptionalPositiveBigIntString(nativeAmount, "nativeAmount");
|
|
1144
|
+
if (collateralAmount === null && collateralNativeAmount === null) {
|
|
1145
|
+
throw new Error("amount or nativeAmount is required for Morpho market supply_collateral.");
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
...request,
|
|
1149
|
+
amount: collateralAmount,
|
|
1150
|
+
nativeAmount: collateralNativeAmount,
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
return {
|
|
1154
|
+
...request,
|
|
1155
|
+
amount: assertPositiveBigIntString(amount, "amount"),
|
|
1156
|
+
nativeAmount: null,
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
520
1160
|
function buildLidoOperationRequest({ operation, amount }) {
|
|
521
1161
|
return {
|
|
522
1162
|
operation: normalizeLidoOperation(operation),
|
|
@@ -1279,35 +1919,231 @@ export class WdkEvmWalletService {
|
|
|
1279
1919
|
);
|
|
1280
1920
|
}
|
|
1281
1921
|
|
|
1282
|
-
async
|
|
1922
|
+
async getMorphoVaults({
|
|
1923
|
+
network,
|
|
1924
|
+
vaultAddress = null,
|
|
1925
|
+
limit,
|
|
1926
|
+
listedOnly = true,
|
|
1927
|
+
assetAddress = null,
|
|
1928
|
+
orderBy,
|
|
1929
|
+
orderDirection,
|
|
1930
|
+
}) {
|
|
1931
|
+
const runtimeConfig = this.#resolveRuntimeConfig(network);
|
|
1932
|
+
assertMorphoSupportedNetwork(runtimeConfig.network);
|
|
1933
|
+
if (vaultAddress !== null && vaultAddress !== undefined && String(vaultAddress).trim()) {
|
|
1934
|
+
const data = await this.#morphoGraphqlRequest({
|
|
1935
|
+
query: MORPHO_VAULT_BY_ADDRESS_QUERY,
|
|
1936
|
+
variables: {
|
|
1937
|
+
address: normalizeAddress(vaultAddress, "vaultAddress"),
|
|
1938
|
+
chainId: runtimeConfig.chainId,
|
|
1939
|
+
},
|
|
1940
|
+
operationName: "MorphoVaultV2ByAddress",
|
|
1941
|
+
});
|
|
1942
|
+
return {
|
|
1943
|
+
network: runtimeConfig.network,
|
|
1944
|
+
chainId: runtimeConfig.chainId,
|
|
1945
|
+
protocol: "morpho",
|
|
1946
|
+
vault: data.vaultV2ByAddress || null,
|
|
1947
|
+
found: Boolean(data.vaultV2ByAddress),
|
|
1948
|
+
source: "morpho-api",
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
const first = normalizeMorphoListLimit(limit);
|
|
1952
|
+
const onlyListed = normalizeMorphoListedOnly(listedOnly);
|
|
1953
|
+
const assetAddressFilter = normalizeOptionalAddressFilter(assetAddress, "assetAddress");
|
|
1954
|
+
const resolvedOrderBy = normalizeMorphoOrderBy(
|
|
1955
|
+
orderBy,
|
|
1956
|
+
MORPHO_VAULT_ORDER_LOOKUP,
|
|
1957
|
+
"TotalAssetsUsd"
|
|
1958
|
+
);
|
|
1959
|
+
const resolvedOrderDirection = normalizeMorphoOrderDirection(orderDirection);
|
|
1960
|
+
const where = { chainId_in: [runtimeConfig.chainId] };
|
|
1961
|
+
if (onlyListed) {
|
|
1962
|
+
where.listed = true;
|
|
1963
|
+
}
|
|
1964
|
+
if (assetAddressFilter) {
|
|
1965
|
+
where.assetAddress_in = assetAddressFilter;
|
|
1966
|
+
}
|
|
1967
|
+
const data = await this.#morphoGraphqlRequest({
|
|
1968
|
+
query: MORPHO_VAULT_LIST_QUERY,
|
|
1969
|
+
variables: {
|
|
1970
|
+
first,
|
|
1971
|
+
where,
|
|
1972
|
+
orderBy: resolvedOrderBy,
|
|
1973
|
+
orderDirection: resolvedOrderDirection,
|
|
1974
|
+
},
|
|
1975
|
+
operationName: "MorphoVaultV2List",
|
|
1976
|
+
});
|
|
1977
|
+
const vaults = Array.isArray(data?.vaultV2s?.items) ? data.vaultV2s.items : [];
|
|
1978
|
+
return {
|
|
1979
|
+
network: runtimeConfig.network,
|
|
1980
|
+
chainId: runtimeConfig.chainId,
|
|
1981
|
+
protocol: "morpho",
|
|
1982
|
+
listedOnly: onlyListed,
|
|
1983
|
+
requestedLimit: first,
|
|
1984
|
+
orderBy: resolvedOrderBy,
|
|
1985
|
+
orderDirection: resolvedOrderDirection,
|
|
1986
|
+
assetAddressFilter,
|
|
1987
|
+
vaultCount: vaults.length,
|
|
1988
|
+
vaults,
|
|
1989
|
+
source: "morpho-api",
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
async getMorphoMarkets({
|
|
1994
|
+
network,
|
|
1995
|
+
marketId = null,
|
|
1996
|
+
limit,
|
|
1997
|
+
listedOnly = true,
|
|
1998
|
+
search,
|
|
1999
|
+
collateralAssetAddress = null,
|
|
2000
|
+
loanAssetAddress = null,
|
|
2001
|
+
orderBy,
|
|
2002
|
+
orderDirection,
|
|
2003
|
+
}) {
|
|
2004
|
+
const runtimeConfig = this.#resolveRuntimeConfig(network);
|
|
2005
|
+
assertMorphoSupportedNetwork(runtimeConfig.network);
|
|
2006
|
+
if (marketId !== null && marketId !== undefined && String(marketId).trim()) {
|
|
2007
|
+
const data = await this.#morphoGraphqlRequest({
|
|
2008
|
+
query: MORPHO_MARKET_BY_ID_QUERY,
|
|
2009
|
+
variables: {
|
|
2010
|
+
marketId: assertMorphoMarketId(marketId, "marketId"),
|
|
2011
|
+
chainId: runtimeConfig.chainId,
|
|
2012
|
+
},
|
|
2013
|
+
operationName: "MorphoMarketById",
|
|
2014
|
+
});
|
|
2015
|
+
return {
|
|
2016
|
+
network: runtimeConfig.network,
|
|
2017
|
+
chainId: runtimeConfig.chainId,
|
|
2018
|
+
protocol: "morpho",
|
|
2019
|
+
market: data.marketById || null,
|
|
2020
|
+
found: Boolean(data.marketById),
|
|
2021
|
+
source: "morpho-api",
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
const first = normalizeMorphoListLimit(limit);
|
|
2025
|
+
const onlyListed = normalizeMorphoListedOnly(listedOnly);
|
|
2026
|
+
const searchTerm = normalizeMorphoSearch(search);
|
|
2027
|
+
const collateralFilter = normalizeOptionalAddressFilter(
|
|
2028
|
+
collateralAssetAddress,
|
|
2029
|
+
"collateralAssetAddress"
|
|
2030
|
+
);
|
|
2031
|
+
const loanFilter = normalizeOptionalAddressFilter(loanAssetAddress, "loanAssetAddress");
|
|
2032
|
+
const resolvedOrderBy = normalizeMorphoOrderBy(
|
|
2033
|
+
orderBy,
|
|
2034
|
+
MORPHO_MARKET_ORDER_LOOKUP,
|
|
2035
|
+
"SupplyAssetsUsd"
|
|
2036
|
+
);
|
|
2037
|
+
const resolvedOrderDirection = normalizeMorphoOrderDirection(orderDirection);
|
|
2038
|
+
const where = { chainId_in: [runtimeConfig.chainId] };
|
|
2039
|
+
if (onlyListed) {
|
|
2040
|
+
where.listed = true;
|
|
2041
|
+
}
|
|
2042
|
+
if (searchTerm) {
|
|
2043
|
+
where.search = searchTerm;
|
|
2044
|
+
}
|
|
2045
|
+
if (collateralFilter) {
|
|
2046
|
+
where.collateralAssetAddress_in = collateralFilter;
|
|
2047
|
+
}
|
|
2048
|
+
if (loanFilter) {
|
|
2049
|
+
where.loanAssetAddress_in = loanFilter;
|
|
2050
|
+
}
|
|
2051
|
+
const data = await this.#morphoGraphqlRequest({
|
|
2052
|
+
query: MORPHO_MARKET_LIST_QUERY,
|
|
2053
|
+
variables: {
|
|
2054
|
+
first,
|
|
2055
|
+
where,
|
|
2056
|
+
orderBy: resolvedOrderBy,
|
|
2057
|
+
orderDirection: resolvedOrderDirection,
|
|
2058
|
+
},
|
|
2059
|
+
operationName: "MorphoMarketList",
|
|
2060
|
+
});
|
|
2061
|
+
const markets = Array.isArray(data?.markets?.items) ? data.markets.items : [];
|
|
2062
|
+
return {
|
|
2063
|
+
network: runtimeConfig.network,
|
|
2064
|
+
chainId: runtimeConfig.chainId,
|
|
2065
|
+
protocol: "morpho",
|
|
2066
|
+
listedOnly: onlyListed,
|
|
2067
|
+
requestedLimit: first,
|
|
2068
|
+
orderBy: resolvedOrderBy,
|
|
2069
|
+
orderDirection: resolvedOrderDirection,
|
|
2070
|
+
search: searchTerm,
|
|
2071
|
+
collateralAssetFilter: collateralFilter,
|
|
2072
|
+
loanAssetFilter: loanFilter,
|
|
2073
|
+
marketCount: markets.length,
|
|
2074
|
+
markets,
|
|
2075
|
+
source: "morpho-api",
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
async getMorphoPositions({ seedPhrase, address, accountIndex = 0, network }) {
|
|
2080
|
+
return this.#withReadableAccount(
|
|
2081
|
+
{ seedPhrase, address, accountIndex, network },
|
|
2082
|
+
async (account, runtimeConfig) => {
|
|
2083
|
+
assertMorphoSupportedNetwork(runtimeConfig.network);
|
|
2084
|
+
const accountAddress = await account.getAddress();
|
|
2085
|
+
const data = await this.#morphoGraphqlRequest({
|
|
2086
|
+
query: MORPHO_USER_OVERVIEW_QUERY,
|
|
2087
|
+
variables: {
|
|
2088
|
+
address: accountAddress,
|
|
2089
|
+
chainId: runtimeConfig.chainId,
|
|
2090
|
+
},
|
|
2091
|
+
operationName: "MorphoUserByAddress",
|
|
2092
|
+
});
|
|
2093
|
+
const user = data.userByAddress || null;
|
|
2094
|
+
const marketPositions = Array.isArray(user?.marketPositions) ? user.marketPositions : [];
|
|
2095
|
+
const vaultPositions = Array.isArray(user?.vaultV2Positions) ? user.vaultV2Positions : [];
|
|
2096
|
+
return {
|
|
2097
|
+
network: runtimeConfig.network,
|
|
2098
|
+
chainId: runtimeConfig.chainId,
|
|
2099
|
+
accountIndex,
|
|
2100
|
+
address: accountAddress,
|
|
2101
|
+
protocol: "morpho",
|
|
2102
|
+
marketPositionCount: marketPositions.length,
|
|
2103
|
+
vaultPositionCount: vaultPositions.length,
|
|
2104
|
+
marketPositions,
|
|
2105
|
+
vaultPositions,
|
|
2106
|
+
source: "morpho-api",
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
async quoteMorphoVaultOperation({
|
|
1283
2113
|
seedPhrase,
|
|
1284
2114
|
address,
|
|
1285
2115
|
operation,
|
|
2116
|
+
vaultAddress,
|
|
2117
|
+
vaultPreset,
|
|
1286
2118
|
token,
|
|
1287
2119
|
tokenAddress,
|
|
1288
2120
|
amount,
|
|
2121
|
+
nativeAmount,
|
|
1289
2122
|
accountIndex = 0,
|
|
1290
2123
|
network,
|
|
1291
2124
|
}) {
|
|
1292
2125
|
return this.#withReadableAccount(
|
|
1293
2126
|
{ seedPhrase, address, accountIndex, network },
|
|
1294
2127
|
async (account, runtimeConfig) => {
|
|
1295
|
-
|
|
1296
|
-
const request =
|
|
2128
|
+
assertMorphoSupportedNetwork(runtimeConfig.network);
|
|
2129
|
+
const request = buildMorphoVaultOperationRequest({
|
|
1297
2130
|
operation,
|
|
2131
|
+
vaultAddress,
|
|
2132
|
+
vaultPreset,
|
|
1298
2133
|
token,
|
|
1299
2134
|
tokenAddress,
|
|
1300
2135
|
amount,
|
|
2136
|
+
nativeAmount,
|
|
1301
2137
|
});
|
|
1302
2138
|
const accountAddress = await account.getAddress();
|
|
1303
|
-
const plan = await this.#
|
|
2139
|
+
const plan = await this.#buildMorphoOperationPlan({
|
|
1304
2140
|
account,
|
|
1305
2141
|
runtimeConfig,
|
|
1306
2142
|
address: accountAddress,
|
|
1307
2143
|
request,
|
|
1308
2144
|
tolerateOperationFeeFailure: true,
|
|
1309
2145
|
});
|
|
1310
|
-
return this.#
|
|
2146
|
+
return this.#formatMorphoOperationResponse({
|
|
1311
2147
|
runtimeConfig,
|
|
1312
2148
|
accountIndex,
|
|
1313
2149
|
address: accountAddress,
|
|
@@ -1318,42 +2154,47 @@ export class WdkEvmWalletService {
|
|
|
1318
2154
|
);
|
|
1319
2155
|
}
|
|
1320
2156
|
|
|
1321
|
-
async
|
|
2157
|
+
async sendMorphoVaultOperation({
|
|
1322
2158
|
seedPhrase,
|
|
1323
2159
|
operation,
|
|
2160
|
+
vaultAddress,
|
|
2161
|
+
vaultPreset,
|
|
1324
2162
|
token,
|
|
1325
2163
|
tokenAddress,
|
|
1326
2164
|
amount,
|
|
2165
|
+
nativeAmount,
|
|
1327
2166
|
accountIndex = 0,
|
|
1328
2167
|
network,
|
|
1329
2168
|
expectedQuoteFingerprint = null,
|
|
1330
2169
|
}) {
|
|
1331
2170
|
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
1332
|
-
|
|
1333
|
-
const request =
|
|
2171
|
+
assertMorphoSupportedNetwork(runtimeConfig.network);
|
|
2172
|
+
const request = buildMorphoVaultOperationRequest({
|
|
1334
2173
|
operation,
|
|
2174
|
+
vaultAddress,
|
|
2175
|
+
vaultPreset,
|
|
1335
2176
|
token,
|
|
1336
2177
|
tokenAddress,
|
|
1337
2178
|
amount,
|
|
2179
|
+
nativeAmount,
|
|
1338
2180
|
});
|
|
1339
2181
|
const normalizedExpectedQuoteFingerprint =
|
|
1340
2182
|
typeof expectedQuoteFingerprint === "string" && expectedQuoteFingerprint.trim()
|
|
1341
2183
|
? expectedQuoteFingerprint.trim()
|
|
1342
2184
|
: null;
|
|
1343
2185
|
const address = await account.getAddress();
|
|
1344
|
-
let initialPlan = await this.#
|
|
2186
|
+
let initialPlan = await this.#buildMorphoOperationPlan({
|
|
1345
2187
|
account,
|
|
1346
2188
|
runtimeConfig,
|
|
1347
2189
|
address,
|
|
1348
2190
|
request,
|
|
1349
2191
|
tolerateOperationFeeFailure: true,
|
|
1350
2192
|
});
|
|
1351
|
-
this.#
|
|
2193
|
+
this.#assertExpectedMorphoFingerprint(
|
|
1352
2194
|
normalizedExpectedQuoteFingerprint,
|
|
1353
2195
|
initialPlan.quoteFingerprint
|
|
1354
2196
|
);
|
|
1355
|
-
|
|
1356
|
-
const approvalExecution = await this.#executeAaveApprovalsIfNeeded({
|
|
2197
|
+
const requirementExecution = await this.#executeMorphoRequirementsIfNeeded({
|
|
1357
2198
|
account,
|
|
1358
2199
|
runtimeConfig,
|
|
1359
2200
|
request,
|
|
@@ -1362,35 +2203,34 @@ export class WdkEvmWalletService {
|
|
|
1362
2203
|
|
|
1363
2204
|
let finalPlan = initialPlan;
|
|
1364
2205
|
try {
|
|
1365
|
-
if (
|
|
1366
|
-
finalPlan = await this.#
|
|
2206
|
+
if (requirementExecution.performed) {
|
|
2207
|
+
finalPlan = await this.#buildMorphoOperationPlan({
|
|
1367
2208
|
account,
|
|
1368
2209
|
runtimeConfig,
|
|
1369
2210
|
address,
|
|
1370
2211
|
request,
|
|
1371
2212
|
});
|
|
1372
|
-
this.#
|
|
2213
|
+
this.#assertExpectedMorphoFingerprint(
|
|
1373
2214
|
normalizedExpectedQuoteFingerprint,
|
|
1374
2215
|
finalPlan.quoteFingerprint
|
|
1375
2216
|
);
|
|
1376
2217
|
}
|
|
1377
2218
|
|
|
1378
|
-
if (finalPlan.
|
|
2219
|
+
if (finalPlan.requirements.required) {
|
|
1379
2220
|
throw createTaggedError(
|
|
1380
|
-
"
|
|
1381
|
-
"
|
|
2221
|
+
"Morpho operation still requires prerequisite transactions after the requirement step completed.",
|
|
2222
|
+
"morpho_requirements_unresolved",
|
|
1382
2223
|
{
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
currentAllowance: finalPlan.currentAllowance.toString(),
|
|
2224
|
+
requirementCount: finalPlan.requirements.steps.length,
|
|
2225
|
+
requirements: finalPlan.requirements.steps,
|
|
1386
2226
|
}
|
|
1387
2227
|
);
|
|
1388
2228
|
}
|
|
1389
2229
|
|
|
1390
2230
|
if (finalPlan.operationFee === null) {
|
|
1391
2231
|
throw createTaggedError(
|
|
1392
|
-
"
|
|
1393
|
-
"
|
|
2232
|
+
"Morpho operation fee estimate was unavailable. Generate a new quote before sending.",
|
|
2233
|
+
"morpho_fee_unavailable",
|
|
1394
2234
|
{
|
|
1395
2235
|
operation: request.operation,
|
|
1396
2236
|
feeEstimateError: finalPlan.operationFeeError,
|
|
@@ -1398,20 +2238,19 @@ export class WdkEvmWalletService {
|
|
|
1398
2238
|
);
|
|
1399
2239
|
}
|
|
1400
2240
|
|
|
1401
|
-
const protocol =
|
|
2241
|
+
const protocol = this.#createMorphoProtocol(account, runtimeConfig, request);
|
|
1402
2242
|
let result;
|
|
1403
2243
|
try {
|
|
1404
|
-
result = await protocol[request.
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
});
|
|
2244
|
+
result = await protocol[this.#getMorphoOperationMethods(request).sendMethod](
|
|
2245
|
+
this.#buildMorphoOperationOptions(request)
|
|
2246
|
+
);
|
|
1408
2247
|
} finally {
|
|
1409
2248
|
await maybeDispose(protocol);
|
|
1410
2249
|
}
|
|
1411
|
-
const resultFee = BigInt(result?.fee || 0);
|
|
1412
|
-
const totalFee =
|
|
2250
|
+
const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
|
|
2251
|
+
const totalFee = requirementExecution.totalFee + resultFee;
|
|
1413
2252
|
return {
|
|
1414
|
-
...this.#
|
|
2253
|
+
...this.#formatMorphoOperationResponse({
|
|
1415
2254
|
runtimeConfig,
|
|
1416
2255
|
accountIndex,
|
|
1417
2256
|
address,
|
|
@@ -1420,9 +2259,9 @@ export class WdkEvmWalletService {
|
|
|
1420
2259
|
...finalPlan,
|
|
1421
2260
|
operationFee: resultFee,
|
|
1422
2261
|
totalEstimatedFee: totalFee,
|
|
1423
|
-
|
|
1424
|
-
...finalPlan.
|
|
1425
|
-
estimatedFee:
|
|
2262
|
+
requirements: {
|
|
2263
|
+
...finalPlan.requirements,
|
|
2264
|
+
estimatedFee: requirementExecution.totalFee,
|
|
1426
2265
|
},
|
|
1427
2266
|
},
|
|
1428
2267
|
}),
|
|
@@ -1430,210 +2269,58 @@ export class WdkEvmWalletService {
|
|
|
1430
2269
|
...result,
|
|
1431
2270
|
fee: resultFee.toString(),
|
|
1432
2271
|
totalFee: totalFee.toString(),
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
...(approvalExecution.resetAllowanceHash
|
|
1436
|
-
? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
|
|
1437
|
-
: {}),
|
|
2272
|
+
requirementsFee: requirementExecution.totalFee.toString(),
|
|
2273
|
+
requirements: requirementExecution.transactions,
|
|
1438
2274
|
},
|
|
1439
2275
|
};
|
|
1440
2276
|
} catch (error) {
|
|
1441
|
-
const cleanup = await this.#
|
|
2277
|
+
const cleanup = await this.#restoreMorphoRequirementsAfterFailedOperation({
|
|
1442
2278
|
account,
|
|
1443
2279
|
runtimeConfig,
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
approvalExecution,
|
|
2280
|
+
request,
|
|
2281
|
+
plan: initialPlan,
|
|
2282
|
+
requirementExecution,
|
|
1448
2283
|
});
|
|
1449
|
-
this.#
|
|
2284
|
+
this.#throwMorphoFailureWithCleanup(error, cleanup);
|
|
1450
2285
|
}
|
|
1451
2286
|
});
|
|
1452
2287
|
}
|
|
1453
2288
|
|
|
1454
|
-
async
|
|
1455
|
-
return this.#withReadableAccount(
|
|
1456
|
-
{ seedPhrase, address, accountIndex, network },
|
|
1457
|
-
async (account, runtimeConfig) => {
|
|
1458
|
-
assertLidoSupportedNetwork(runtimeConfig.network);
|
|
1459
|
-
const contracts = this.#getLidoContracts(runtimeConfig.network);
|
|
1460
|
-
const [stEthMetadata, wstEthMetadata, rates, stakingAprResult] = await Promise.all([
|
|
1461
|
-
this.#getLidoTokenMetadata(runtimeConfig, contracts.steth),
|
|
1462
|
-
this.#getLidoTokenMetadata(runtimeConfig, contracts.wsteth),
|
|
1463
|
-
this.#readLidoSampleRates(runtimeConfig),
|
|
1464
|
-
this.#readLidoStakingApr(runtimeConfig),
|
|
1465
|
-
]);
|
|
1466
|
-
return {
|
|
1467
|
-
network: runtimeConfig.network,
|
|
1468
|
-
chainId: runtimeConfig.chainId,
|
|
1469
|
-
accountIndex,
|
|
1470
|
-
protocol: "lido",
|
|
1471
|
-
preferredPositionToken: "wstETH",
|
|
1472
|
-
stakingAsset: {
|
|
1473
|
-
type: "native",
|
|
1474
|
-
symbol: runtimeConfig.nativeSymbol,
|
|
1475
|
-
decimals: 18,
|
|
1476
|
-
},
|
|
1477
|
-
referralAddress: this.#getLidoReferralAddress(),
|
|
1478
|
-
contracts: {
|
|
1479
|
-
stETH: contracts.steth.address,
|
|
1480
|
-
wstETH: contracts.wsteth.address,
|
|
1481
|
-
referralStaker: contracts.referralStaker,
|
|
1482
|
-
withdrawalQueue: contracts.withdrawalQueue,
|
|
1483
|
-
},
|
|
1484
|
-
stEthMetadata,
|
|
1485
|
-
wstEthMetadata,
|
|
1486
|
-
sampleRates: rates,
|
|
1487
|
-
stakingApr: stakingAprResult.data,
|
|
1488
|
-
stakingAprError: stakingAprResult.error,
|
|
1489
|
-
withdrawalLimits: {
|
|
1490
|
-
minStEthAmountRaw: LIDO_MIN_STETH_WITHDRAWAL_AMOUNT.toString(),
|
|
1491
|
-
minStEthAmountFormatted: formatUnits(LIDO_MIN_STETH_WITHDRAWAL_AMOUNT, LIDO_STETH_DECIMALS),
|
|
1492
|
-
maxStEthAmountRaw: LIDO_MAX_STETH_WITHDRAWAL_AMOUNT.toString(),
|
|
1493
|
-
maxStEthAmountFormatted: formatUnits(LIDO_MAX_STETH_WITHDRAWAL_AMOUNT, LIDO_STETH_DECIMALS),
|
|
1494
|
-
},
|
|
1495
|
-
source: "lido-contracts",
|
|
1496
|
-
};
|
|
1497
|
-
}
|
|
1498
|
-
);
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
async getLidoPositions({ seedPhrase, address, accountIndex = 0, network }) {
|
|
1502
|
-
return this.#withReadableAccount(
|
|
1503
|
-
{ seedPhrase, address, accountIndex, network },
|
|
1504
|
-
async (account, runtimeConfig) => {
|
|
1505
|
-
assertLidoSupportedNetwork(runtimeConfig.network);
|
|
1506
|
-
const accountAddress = await account.getAddress();
|
|
1507
|
-
const contracts = this.#getLidoContracts(runtimeConfig.network);
|
|
1508
|
-
const [nativeBalance, stEthMetadata, wstEthMetadata, stEthBalance, wstEthBalance] = await Promise.all([
|
|
1509
|
-
account.getBalance(),
|
|
1510
|
-
this.#getLidoTokenMetadata(runtimeConfig, contracts.steth),
|
|
1511
|
-
this.#getLidoTokenMetadata(runtimeConfig, contracts.wsteth),
|
|
1512
|
-
this.#readTokenBalanceWithFallback({
|
|
1513
|
-
account,
|
|
1514
|
-
runtimeConfig,
|
|
1515
|
-
tokenAddress: contracts.steth.address,
|
|
1516
|
-
ownerAddress: accountAddress,
|
|
1517
|
-
}),
|
|
1518
|
-
this.#readTokenBalanceWithFallback({
|
|
1519
|
-
account,
|
|
1520
|
-
runtimeConfig,
|
|
1521
|
-
tokenAddress: contracts.wsteth.address,
|
|
1522
|
-
ownerAddress: accountAddress,
|
|
1523
|
-
}),
|
|
1524
|
-
]);
|
|
1525
|
-
const wstEthAsStEth = await this.#quoteLidoOutputRaw({
|
|
1526
|
-
runtimeConfig,
|
|
1527
|
-
operation: "unwrap_wsteth",
|
|
1528
|
-
amount: wstEthBalance,
|
|
1529
|
-
fromAddress: accountAddress,
|
|
1530
|
-
});
|
|
1531
|
-
const stEthEquivalentTotal = stEthBalance + wstEthAsStEth;
|
|
1532
|
-
const positions = [];
|
|
1533
|
-
if (stEthBalance > 0n) {
|
|
1534
|
-
positions.push({
|
|
1535
|
-
asset: "stETH",
|
|
1536
|
-
tokenAddress: contracts.steth.address,
|
|
1537
|
-
tokenMetadata: stEthMetadata,
|
|
1538
|
-
balanceRaw: stEthBalance.toString(),
|
|
1539
|
-
balanceFormatted: formatUnits(stEthBalance, stEthMetadata.decimals),
|
|
1540
|
-
stEthEquivalentRaw: stEthBalance.toString(),
|
|
1541
|
-
stEthEquivalentFormatted: formatUnits(stEthBalance, stEthMetadata.decimals),
|
|
1542
|
-
});
|
|
1543
|
-
}
|
|
1544
|
-
if (wstEthBalance > 0n) {
|
|
1545
|
-
positions.push({
|
|
1546
|
-
asset: "wstETH",
|
|
1547
|
-
tokenAddress: contracts.wsteth.address,
|
|
1548
|
-
tokenMetadata: wstEthMetadata,
|
|
1549
|
-
balanceRaw: wstEthBalance.toString(),
|
|
1550
|
-
balanceFormatted: formatUnits(wstEthBalance, wstEthMetadata.decimals),
|
|
1551
|
-
stEthEquivalentRaw: wstEthAsStEth.toString(),
|
|
1552
|
-
stEthEquivalentFormatted: formatUnits(wstEthAsStEth, stEthMetadata.decimals),
|
|
1553
|
-
});
|
|
1554
|
-
}
|
|
1555
|
-
return {
|
|
1556
|
-
network: runtimeConfig.network,
|
|
1557
|
-
chainId: runtimeConfig.chainId,
|
|
1558
|
-
accountIndex,
|
|
1559
|
-
address: accountAddress,
|
|
1560
|
-
protocol: "lido",
|
|
1561
|
-
preferredPositionToken: "wstETH",
|
|
1562
|
-
contracts: {
|
|
1563
|
-
stETH: contracts.steth.address,
|
|
1564
|
-
wstETH: contracts.wsteth.address,
|
|
1565
|
-
referralStaker: contracts.referralStaker,
|
|
1566
|
-
withdrawalQueue: contracts.withdrawalQueue,
|
|
1567
|
-
},
|
|
1568
|
-
nativeBalanceWei: BigInt(nativeBalance || 0).toString(),
|
|
1569
|
-
nativeBalanceFormatted: formatUnits(BigInt(nativeBalance || 0), 18),
|
|
1570
|
-
stEthEquivalentTotalRaw: stEthEquivalentTotal.toString(),
|
|
1571
|
-
stEthEquivalentTotalFormatted: formatUnits(stEthEquivalentTotal, stEthMetadata.decimals),
|
|
1572
|
-
positionCount: positions.length,
|
|
1573
|
-
positions,
|
|
1574
|
-
source: "lido-contracts",
|
|
1575
|
-
};
|
|
1576
|
-
}
|
|
1577
|
-
);
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
async getLidoWithdrawalRequests({ seedPhrase, address, accountIndex = 0, network }) {
|
|
1581
|
-
return this.#withReadableAccount(
|
|
1582
|
-
{ seedPhrase, address, accountIndex, network },
|
|
1583
|
-
async (account, runtimeConfig) => {
|
|
1584
|
-
assertLidoSupportedNetwork(runtimeConfig.network);
|
|
1585
|
-
const accountAddress = await account.getAddress();
|
|
1586
|
-
const contracts = this.#getLidoContracts(runtimeConfig.network);
|
|
1587
|
-
const [stEthMetadata, wstEthMetadata, requestIds] = await Promise.all([
|
|
1588
|
-
this.#getLidoTokenMetadata(runtimeConfig, contracts.steth),
|
|
1589
|
-
this.#getLidoTokenMetadata(runtimeConfig, contracts.wsteth),
|
|
1590
|
-
this.#getLidoWithdrawalRequestIds(runtimeConfig, accountAddress),
|
|
1591
|
-
]);
|
|
1592
|
-
const statuses = requestIds.length
|
|
1593
|
-
? await this.#getLidoWithdrawalStatuses(runtimeConfig, requestIds)
|
|
1594
|
-
: [];
|
|
1595
|
-
const requests = statuses.map((status) =>
|
|
1596
|
-
this.#formatLidoWithdrawalStatus(status, stEthMetadata, wstEthMetadata)
|
|
1597
|
-
);
|
|
1598
|
-
const claimableCount = requests.filter((request) => request.claimable).length;
|
|
1599
|
-
return {
|
|
1600
|
-
network: runtimeConfig.network,
|
|
1601
|
-
chainId: runtimeConfig.chainId,
|
|
1602
|
-
accountIndex,
|
|
1603
|
-
address: accountAddress,
|
|
1604
|
-
protocol: "lido",
|
|
1605
|
-
withdrawalQueue: contracts.withdrawalQueue,
|
|
1606
|
-
requestCount: requests.length,
|
|
1607
|
-
claimableCount,
|
|
1608
|
-
requests,
|
|
1609
|
-
source: "lido-contracts",
|
|
1610
|
-
};
|
|
1611
|
-
}
|
|
1612
|
-
);
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
async quoteLidoOperation({
|
|
2289
|
+
async quoteMorphoMarketOperation({
|
|
1616
2290
|
seedPhrase,
|
|
1617
2291
|
address,
|
|
1618
2292
|
operation,
|
|
2293
|
+
marketId,
|
|
2294
|
+
marketPreset,
|
|
2295
|
+
token,
|
|
2296
|
+
tokenAddress,
|
|
1619
2297
|
amount,
|
|
2298
|
+
nativeAmount,
|
|
1620
2299
|
accountIndex = 0,
|
|
1621
2300
|
network,
|
|
1622
2301
|
}) {
|
|
1623
2302
|
return this.#withReadableAccount(
|
|
1624
2303
|
{ seedPhrase, address, accountIndex, network },
|
|
1625
2304
|
async (account, runtimeConfig) => {
|
|
1626
|
-
|
|
1627
|
-
const request =
|
|
2305
|
+
assertMorphoSupportedNetwork(runtimeConfig.network);
|
|
2306
|
+
const request = buildMorphoMarketOperationRequest({
|
|
2307
|
+
operation,
|
|
2308
|
+
marketId,
|
|
2309
|
+
marketPreset,
|
|
2310
|
+
token,
|
|
2311
|
+
tokenAddress,
|
|
2312
|
+
amount,
|
|
2313
|
+
nativeAmount,
|
|
2314
|
+
});
|
|
1628
2315
|
const accountAddress = await account.getAddress();
|
|
1629
|
-
const plan = await this.#
|
|
2316
|
+
const plan = await this.#buildMorphoOperationPlan({
|
|
1630
2317
|
account,
|
|
1631
2318
|
runtimeConfig,
|
|
1632
2319
|
address: accountAddress,
|
|
1633
2320
|
request,
|
|
1634
2321
|
tolerateOperationFeeFailure: true,
|
|
1635
2322
|
});
|
|
1636
|
-
return this.#
|
|
2323
|
+
return this.#formatMorphoOperationResponse({
|
|
1637
2324
|
runtimeConfig,
|
|
1638
2325
|
accountIndex,
|
|
1639
2326
|
address: accountAddress,
|
|
@@ -1644,35 +2331,47 @@ export class WdkEvmWalletService {
|
|
|
1644
2331
|
);
|
|
1645
2332
|
}
|
|
1646
2333
|
|
|
1647
|
-
async
|
|
2334
|
+
async sendMorphoMarketOperation({
|
|
1648
2335
|
seedPhrase,
|
|
1649
2336
|
operation,
|
|
2337
|
+
marketId,
|
|
2338
|
+
marketPreset,
|
|
2339
|
+
token,
|
|
2340
|
+
tokenAddress,
|
|
1650
2341
|
amount,
|
|
2342
|
+
nativeAmount,
|
|
1651
2343
|
accountIndex = 0,
|
|
1652
2344
|
network,
|
|
1653
2345
|
expectedQuoteFingerprint = null,
|
|
1654
2346
|
}) {
|
|
1655
2347
|
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
1656
|
-
|
|
1657
|
-
const request =
|
|
2348
|
+
assertMorphoSupportedNetwork(runtimeConfig.network);
|
|
2349
|
+
const request = buildMorphoMarketOperationRequest({
|
|
2350
|
+
operation,
|
|
2351
|
+
marketId,
|
|
2352
|
+
marketPreset,
|
|
2353
|
+
token,
|
|
2354
|
+
tokenAddress,
|
|
2355
|
+
amount,
|
|
2356
|
+
nativeAmount,
|
|
2357
|
+
});
|
|
1658
2358
|
const normalizedExpectedQuoteFingerprint =
|
|
1659
2359
|
typeof expectedQuoteFingerprint === "string" && expectedQuoteFingerprint.trim()
|
|
1660
2360
|
? expectedQuoteFingerprint.trim()
|
|
1661
2361
|
: null;
|
|
1662
2362
|
const address = await account.getAddress();
|
|
1663
|
-
let initialPlan = await this.#
|
|
2363
|
+
let initialPlan = await this.#buildMorphoOperationPlan({
|
|
1664
2364
|
account,
|
|
1665
2365
|
runtimeConfig,
|
|
1666
2366
|
address,
|
|
1667
2367
|
request,
|
|
1668
2368
|
tolerateOperationFeeFailure: true,
|
|
1669
2369
|
});
|
|
1670
|
-
this.#
|
|
2370
|
+
this.#assertExpectedMorphoFingerprint(
|
|
1671
2371
|
normalizedExpectedQuoteFingerprint,
|
|
1672
2372
|
initialPlan.quoteFingerprint
|
|
1673
2373
|
);
|
|
1674
|
-
|
|
1675
|
-
const approvalExecution = await this.#executeLidoApprovalsIfNeeded({
|
|
2374
|
+
const requirementExecution = await this.#executeMorphoRequirementsIfNeeded({
|
|
1676
2375
|
account,
|
|
1677
2376
|
runtimeConfig,
|
|
1678
2377
|
request,
|
|
@@ -1681,35 +2380,34 @@ export class WdkEvmWalletService {
|
|
|
1681
2380
|
|
|
1682
2381
|
let finalPlan = initialPlan;
|
|
1683
2382
|
try {
|
|
1684
|
-
if (
|
|
1685
|
-
finalPlan = await this.#
|
|
2383
|
+
if (requirementExecution.performed) {
|
|
2384
|
+
finalPlan = await this.#buildMorphoOperationPlan({
|
|
1686
2385
|
account,
|
|
1687
2386
|
runtimeConfig,
|
|
1688
2387
|
address,
|
|
1689
2388
|
request,
|
|
1690
2389
|
});
|
|
1691
|
-
this.#
|
|
2390
|
+
this.#assertExpectedMorphoFingerprint(
|
|
1692
2391
|
normalizedExpectedQuoteFingerprint,
|
|
1693
2392
|
finalPlan.quoteFingerprint
|
|
1694
2393
|
);
|
|
1695
2394
|
}
|
|
1696
2395
|
|
|
1697
|
-
if (finalPlan.
|
|
2396
|
+
if (finalPlan.requirements.required) {
|
|
1698
2397
|
throw createTaggedError(
|
|
1699
|
-
"
|
|
1700
|
-
"
|
|
2398
|
+
"Morpho operation still requires prerequisite transactions after the requirement step completed.",
|
|
2399
|
+
"morpho_requirements_unresolved",
|
|
1701
2400
|
{
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
currentAllowance: finalPlan.currentAllowance.toString(),
|
|
2401
|
+
requirementCount: finalPlan.requirements.steps.length,
|
|
2402
|
+
requirements: finalPlan.requirements.steps,
|
|
1705
2403
|
}
|
|
1706
2404
|
);
|
|
1707
2405
|
}
|
|
1708
2406
|
|
|
1709
2407
|
if (finalPlan.operationFee === null) {
|
|
1710
2408
|
throw createTaggedError(
|
|
1711
|
-
"
|
|
1712
|
-
"
|
|
2409
|
+
"Morpho operation fee estimate was unavailable. Generate a new quote before sending.",
|
|
2410
|
+
"morpho_fee_unavailable",
|
|
1713
2411
|
{
|
|
1714
2412
|
operation: request.operation,
|
|
1715
2413
|
feeEstimateError: finalPlan.operationFeeError,
|
|
@@ -1717,11 +2415,19 @@ export class WdkEvmWalletService {
|
|
|
1717
2415
|
);
|
|
1718
2416
|
}
|
|
1719
2417
|
|
|
1720
|
-
const
|
|
2418
|
+
const protocol = this.#createMorphoProtocol(account, runtimeConfig, request);
|
|
2419
|
+
let result;
|
|
2420
|
+
try {
|
|
2421
|
+
result = await protocol[this.#getMorphoOperationMethods(request).sendMethod](
|
|
2422
|
+
this.#buildMorphoOperationOptions(request)
|
|
2423
|
+
);
|
|
2424
|
+
} finally {
|
|
2425
|
+
await maybeDispose(protocol);
|
|
2426
|
+
}
|
|
1721
2427
|
const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
|
|
1722
|
-
const totalFee =
|
|
2428
|
+
const totalFee = requirementExecution.totalFee + resultFee;
|
|
1723
2429
|
return {
|
|
1724
|
-
...this.#
|
|
2430
|
+
...this.#formatMorphoOperationResponse({
|
|
1725
2431
|
runtimeConfig,
|
|
1726
2432
|
accountIndex,
|
|
1727
2433
|
address,
|
|
@@ -1730,9 +2436,9 @@ export class WdkEvmWalletService {
|
|
|
1730
2436
|
...finalPlan,
|
|
1731
2437
|
operationFee: resultFee,
|
|
1732
2438
|
totalEstimatedFee: totalFee,
|
|
1733
|
-
|
|
1734
|
-
...finalPlan.
|
|
1735
|
-
estimatedFee:
|
|
2439
|
+
requirements: {
|
|
2440
|
+
...finalPlan.requirements,
|
|
2441
|
+
estimatedFee: requirementExecution.totalFee,
|
|
1736
2442
|
},
|
|
1737
2443
|
},
|
|
1738
2444
|
}),
|
|
@@ -1740,50 +2446,52 @@ export class WdkEvmWalletService {
|
|
|
1740
2446
|
...result,
|
|
1741
2447
|
fee: resultFee.toString(),
|
|
1742
2448
|
totalFee: totalFee.toString(),
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
...(approvalExecution.resetAllowanceHash
|
|
1746
|
-
? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
|
|
1747
|
-
: {}),
|
|
2449
|
+
requirementsFee: requirementExecution.totalFee.toString(),
|
|
2450
|
+
requirements: requirementExecution.transactions,
|
|
1748
2451
|
},
|
|
1749
2452
|
};
|
|
1750
2453
|
} catch (error) {
|
|
1751
|
-
const cleanup = await this.#
|
|
2454
|
+
const cleanup = await this.#restoreMorphoRequirementsAfterFailedOperation({
|
|
1752
2455
|
account,
|
|
1753
2456
|
runtimeConfig,
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
approvalExecution,
|
|
2457
|
+
request,
|
|
2458
|
+
plan: initialPlan,
|
|
2459
|
+
requirementExecution,
|
|
1758
2460
|
});
|
|
1759
|
-
this.#
|
|
2461
|
+
this.#throwMorphoFailureWithCleanup(error, cleanup);
|
|
1760
2462
|
}
|
|
1761
2463
|
});
|
|
1762
2464
|
}
|
|
1763
2465
|
|
|
1764
|
-
async
|
|
2466
|
+
async quoteAaveOperation({
|
|
1765
2467
|
seedPhrase,
|
|
1766
2468
|
address,
|
|
1767
2469
|
operation,
|
|
2470
|
+
token,
|
|
2471
|
+
tokenAddress,
|
|
1768
2472
|
amount,
|
|
1769
|
-
requestId,
|
|
1770
2473
|
accountIndex = 0,
|
|
1771
2474
|
network,
|
|
1772
2475
|
}) {
|
|
1773
2476
|
return this.#withReadableAccount(
|
|
1774
2477
|
{ seedPhrase, address, accountIndex, network },
|
|
1775
2478
|
async (account, runtimeConfig) => {
|
|
1776
|
-
|
|
1777
|
-
const request =
|
|
2479
|
+
assertAaveSupportedNetwork(runtimeConfig.network);
|
|
2480
|
+
const request = buildAaveOperationRequest({
|
|
2481
|
+
operation,
|
|
2482
|
+
token,
|
|
2483
|
+
tokenAddress,
|
|
2484
|
+
amount,
|
|
2485
|
+
});
|
|
1778
2486
|
const accountAddress = await account.getAddress();
|
|
1779
|
-
const plan = await this.#
|
|
2487
|
+
const plan = await this.#buildAaveOperationPlan({
|
|
1780
2488
|
account,
|
|
1781
2489
|
runtimeConfig,
|
|
1782
2490
|
address: accountAddress,
|
|
1783
2491
|
request,
|
|
1784
2492
|
tolerateOperationFeeFailure: true,
|
|
1785
2493
|
});
|
|
1786
|
-
return this.#
|
|
2494
|
+
return this.#formatAaveOperationResponse({
|
|
1787
2495
|
runtimeConfig,
|
|
1788
2496
|
accountIndex,
|
|
1789
2497
|
address: accountAddress,
|
|
@@ -1794,36 +2502,42 @@ export class WdkEvmWalletService {
|
|
|
1794
2502
|
);
|
|
1795
2503
|
}
|
|
1796
2504
|
|
|
1797
|
-
async
|
|
2505
|
+
async sendAaveOperation({
|
|
1798
2506
|
seedPhrase,
|
|
1799
2507
|
operation,
|
|
2508
|
+
token,
|
|
2509
|
+
tokenAddress,
|
|
1800
2510
|
amount,
|
|
1801
|
-
requestId,
|
|
1802
2511
|
accountIndex = 0,
|
|
1803
2512
|
network,
|
|
1804
2513
|
expectedQuoteFingerprint = null,
|
|
1805
2514
|
}) {
|
|
1806
2515
|
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
1807
|
-
|
|
1808
|
-
const request =
|
|
2516
|
+
assertAaveSupportedNetwork(runtimeConfig.network);
|
|
2517
|
+
const request = buildAaveOperationRequest({
|
|
2518
|
+
operation,
|
|
2519
|
+
token,
|
|
2520
|
+
tokenAddress,
|
|
2521
|
+
amount,
|
|
2522
|
+
});
|
|
1809
2523
|
const normalizedExpectedQuoteFingerprint =
|
|
1810
2524
|
typeof expectedQuoteFingerprint === "string" && expectedQuoteFingerprint.trim()
|
|
1811
2525
|
? expectedQuoteFingerprint.trim()
|
|
1812
2526
|
: null;
|
|
1813
2527
|
const address = await account.getAddress();
|
|
1814
|
-
let initialPlan = await this.#
|
|
2528
|
+
let initialPlan = await this.#buildAaveOperationPlan({
|
|
1815
2529
|
account,
|
|
1816
2530
|
runtimeConfig,
|
|
1817
2531
|
address,
|
|
1818
2532
|
request,
|
|
1819
2533
|
tolerateOperationFeeFailure: true,
|
|
1820
2534
|
});
|
|
1821
|
-
this.#
|
|
2535
|
+
this.#assertExpectedAaveFingerprint(
|
|
1822
2536
|
normalizedExpectedQuoteFingerprint,
|
|
1823
2537
|
initialPlan.quoteFingerprint
|
|
1824
2538
|
);
|
|
1825
2539
|
|
|
1826
|
-
const approvalExecution = await this.#
|
|
2540
|
+
const approvalExecution = await this.#executeAaveApprovalsIfNeeded({
|
|
1827
2541
|
account,
|
|
1828
2542
|
runtimeConfig,
|
|
1829
2543
|
request,
|
|
@@ -1833,13 +2547,13 @@ export class WdkEvmWalletService {
|
|
|
1833
2547
|
let finalPlan = initialPlan;
|
|
1834
2548
|
try {
|
|
1835
2549
|
if (approvalExecution.performed) {
|
|
1836
|
-
finalPlan = await this.#
|
|
2550
|
+
finalPlan = await this.#buildAaveOperationPlan({
|
|
1837
2551
|
account,
|
|
1838
2552
|
runtimeConfig,
|
|
1839
2553
|
address,
|
|
1840
2554
|
request,
|
|
1841
2555
|
});
|
|
1842
|
-
this.#
|
|
2556
|
+
this.#assertExpectedAaveFingerprint(
|
|
1843
2557
|
normalizedExpectedQuoteFingerprint,
|
|
1844
2558
|
finalPlan.quoteFingerprint
|
|
1845
2559
|
);
|
|
@@ -1847,11 +2561,11 @@ export class WdkEvmWalletService {
|
|
|
1847
2561
|
|
|
1848
2562
|
if (finalPlan.approval.required) {
|
|
1849
2563
|
throw createTaggedError(
|
|
1850
|
-
"
|
|
1851
|
-
"
|
|
2564
|
+
"Aave operation still requires token approval after the approval step completed.",
|
|
2565
|
+
"aave_approval_required",
|
|
1852
2566
|
{
|
|
1853
2567
|
spender: finalPlan.spender,
|
|
1854
|
-
requiredAllowance: finalPlan.
|
|
2568
|
+
requiredAllowance: finalPlan.amount.toString(),
|
|
1855
2569
|
currentAllowance: finalPlan.currentAllowance.toString(),
|
|
1856
2570
|
}
|
|
1857
2571
|
);
|
|
@@ -1859,8 +2573,8 @@ export class WdkEvmWalletService {
|
|
|
1859
2573
|
|
|
1860
2574
|
if (finalPlan.operationFee === null) {
|
|
1861
2575
|
throw createTaggedError(
|
|
1862
|
-
"
|
|
1863
|
-
"
|
|
2576
|
+
"Aave operation fee estimate was unavailable. Generate a new quote before sending.",
|
|
2577
|
+
"aave_fee_unavailable",
|
|
1864
2578
|
{
|
|
1865
2579
|
operation: request.operation,
|
|
1866
2580
|
feeEstimateError: finalPlan.operationFeeError,
|
|
@@ -1868,11 +2582,20 @@ export class WdkEvmWalletService {
|
|
|
1868
2582
|
);
|
|
1869
2583
|
}
|
|
1870
2584
|
|
|
1871
|
-
const
|
|
1872
|
-
|
|
2585
|
+
const protocol = new AaveProtocolEvm(account);
|
|
2586
|
+
let result;
|
|
2587
|
+
try {
|
|
2588
|
+
result = await protocol[request.operation]({
|
|
2589
|
+
token: request.token,
|
|
2590
|
+
amount: request.amount,
|
|
2591
|
+
});
|
|
2592
|
+
} finally {
|
|
2593
|
+
await maybeDispose(protocol);
|
|
2594
|
+
}
|
|
2595
|
+
const resultFee = BigInt(result?.fee || 0);
|
|
1873
2596
|
const totalFee = approvalExecution.totalFee + resultFee;
|
|
1874
2597
|
return {
|
|
1875
|
-
...this.#
|
|
2598
|
+
...this.#formatAaveOperationResponse({
|
|
1876
2599
|
runtimeConfig,
|
|
1877
2600
|
accountIndex,
|
|
1878
2601
|
address,
|
|
@@ -1899,416 +2622,297 @@ export class WdkEvmWalletService {
|
|
|
1899
2622
|
},
|
|
1900
2623
|
};
|
|
1901
2624
|
} catch (error) {
|
|
1902
|
-
const cleanup = await this.#
|
|
2625
|
+
const cleanup = await this.#restoreAllowanceAfterFailedAaveOperation({
|
|
1903
2626
|
account,
|
|
1904
2627
|
runtimeConfig,
|
|
1905
|
-
tokenAddress:
|
|
2628
|
+
tokenAddress: request.token,
|
|
1906
2629
|
spender: initialPlan.spender,
|
|
1907
2630
|
originalAllowance: initialPlan.currentAllowance,
|
|
1908
2631
|
approvalExecution,
|
|
1909
2632
|
});
|
|
1910
|
-
this.#
|
|
2633
|
+
this.#throwAaveFailureWithCleanup(error, cleanup);
|
|
1911
2634
|
}
|
|
1912
2635
|
});
|
|
1913
2636
|
}
|
|
1914
2637
|
|
|
1915
|
-
async
|
|
1916
|
-
seedPhrase,
|
|
1917
|
-
address,
|
|
1918
|
-
tokenIn,
|
|
1919
|
-
tokenOut,
|
|
1920
|
-
tokenInAmount,
|
|
1921
|
-
accountIndex = 0,
|
|
1922
|
-
network,
|
|
1923
|
-
}) {
|
|
2638
|
+
async getLidoOverview({ seedPhrase, address, accountIndex = 0, network }) {
|
|
1924
2639
|
return this.#withReadableAccount(
|
|
1925
2640
|
{ seedPhrase, address, accountIndex, network },
|
|
1926
2641
|
async (account, runtimeConfig) => {
|
|
1927
|
-
|
|
1928
|
-
const
|
|
1929
|
-
const
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
feeEstimateAvailable: plan.swapFee !== null,
|
|
1967
|
-
feeEstimateError: plan.swapFeeError,
|
|
1968
|
-
slippageBps: plan.slippageBps,
|
|
1969
|
-
minimumOutputAmountRaw: plan.minimumTokenOutAmount.toString(),
|
|
1970
|
-
allowance: {
|
|
1971
|
-
spender: plan.spender,
|
|
1972
|
-
currentAllowance: plan.currentAllowance.toString(),
|
|
1973
|
-
requiredAllowance: plan.tokenInAmount.toString(),
|
|
1974
|
-
approvalRequired: plan.approval.required,
|
|
1975
|
-
approvalSequence: plan.approval.steps,
|
|
1976
|
-
readError: plan.allowanceReadError,
|
|
1977
|
-
},
|
|
1978
|
-
router: plan.router,
|
|
1979
|
-
simulation: plan.simulation,
|
|
1980
|
-
swapTransaction: plan.swapTransaction,
|
|
1981
|
-
quote,
|
|
1982
|
-
source: "wdk-protocol-swap-velora-evm",
|
|
1983
|
-
};
|
|
1984
|
-
} finally {
|
|
1985
|
-
if (readOnlyAccount !== account) {
|
|
1986
|
-
await maybeDispose(readOnlyAccount);
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
2642
|
+
assertLidoSupportedNetwork(runtimeConfig.network);
|
|
2643
|
+
const contracts = this.#getLidoContracts(runtimeConfig.network);
|
|
2644
|
+
const [stEthMetadata, wstEthMetadata, rates, stakingAprResult] = await Promise.all([
|
|
2645
|
+
this.#getLidoTokenMetadata(runtimeConfig, contracts.steth),
|
|
2646
|
+
this.#getLidoTokenMetadata(runtimeConfig, contracts.wsteth),
|
|
2647
|
+
this.#readLidoSampleRates(runtimeConfig),
|
|
2648
|
+
this.#readLidoStakingApr(runtimeConfig),
|
|
2649
|
+
]);
|
|
2650
|
+
return {
|
|
2651
|
+
network: runtimeConfig.network,
|
|
2652
|
+
chainId: runtimeConfig.chainId,
|
|
2653
|
+
accountIndex,
|
|
2654
|
+
protocol: "lido",
|
|
2655
|
+
preferredPositionToken: "wstETH",
|
|
2656
|
+
stakingAsset: {
|
|
2657
|
+
type: "native",
|
|
2658
|
+
symbol: runtimeConfig.nativeSymbol,
|
|
2659
|
+
decimals: 18,
|
|
2660
|
+
},
|
|
2661
|
+
referralAddress: this.#getLidoReferralAddress(),
|
|
2662
|
+
contracts: {
|
|
2663
|
+
stETH: contracts.steth.address,
|
|
2664
|
+
wstETH: contracts.wsteth.address,
|
|
2665
|
+
referralStaker: contracts.referralStaker,
|
|
2666
|
+
withdrawalQueue: contracts.withdrawalQueue,
|
|
2667
|
+
},
|
|
2668
|
+
stEthMetadata,
|
|
2669
|
+
wstEthMetadata,
|
|
2670
|
+
sampleRates: rates,
|
|
2671
|
+
stakingApr: stakingAprResult.data,
|
|
2672
|
+
stakingAprError: stakingAprResult.error,
|
|
2673
|
+
withdrawalLimits: {
|
|
2674
|
+
minStEthAmountRaw: LIDO_MIN_STETH_WITHDRAWAL_AMOUNT.toString(),
|
|
2675
|
+
minStEthAmountFormatted: formatUnits(LIDO_MIN_STETH_WITHDRAWAL_AMOUNT, LIDO_STETH_DECIMALS),
|
|
2676
|
+
maxStEthAmountRaw: LIDO_MAX_STETH_WITHDRAWAL_AMOUNT.toString(),
|
|
2677
|
+
maxStEthAmountFormatted: formatUnits(LIDO_MAX_STETH_WITHDRAWAL_AMOUNT, LIDO_STETH_DECIMALS),
|
|
2678
|
+
},
|
|
2679
|
+
source: "lido-contracts",
|
|
2680
|
+
};
|
|
1989
2681
|
}
|
|
1990
2682
|
);
|
|
1991
2683
|
}
|
|
1992
2684
|
|
|
1993
|
-
async
|
|
1994
|
-
seedPhrase,
|
|
1995
|
-
address,
|
|
1996
|
-
tokenIn,
|
|
1997
|
-
destinationChain,
|
|
1998
|
-
outputToken,
|
|
1999
|
-
destinationAddress,
|
|
2000
|
-
tokenInAmount,
|
|
2001
|
-
slippage = DEFAULT_LIFI_SLIPPAGE,
|
|
2002
|
-
allowBridges = null,
|
|
2003
|
-
denyBridges = null,
|
|
2004
|
-
preferBridges = null,
|
|
2005
|
-
accountIndex = 0,
|
|
2006
|
-
network,
|
|
2007
|
-
}) {
|
|
2685
|
+
async getLidoPositions({ seedPhrase, address, accountIndex = 0, network }) {
|
|
2008
2686
|
return this.#withReadableAccount(
|
|
2009
2687
|
{ seedPhrase, address, accountIndex, network },
|
|
2010
2688
|
async (account, runtimeConfig) => {
|
|
2011
|
-
|
|
2012
|
-
const
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2689
|
+
assertLidoSupportedNetwork(runtimeConfig.network);
|
|
2690
|
+
const accountAddress = await account.getAddress();
|
|
2691
|
+
const contracts = this.#getLidoContracts(runtimeConfig.network);
|
|
2692
|
+
const [nativeBalance, stEthMetadata, wstEthMetadata, stEthBalance, wstEthBalance] = await Promise.all([
|
|
2693
|
+
account.getBalance(),
|
|
2694
|
+
this.#getLidoTokenMetadata(runtimeConfig, contracts.steth),
|
|
2695
|
+
this.#getLidoTokenMetadata(runtimeConfig, contracts.wsteth),
|
|
2696
|
+
this.#readTokenBalanceWithFallback({
|
|
2697
|
+
account,
|
|
2698
|
+
runtimeConfig,
|
|
2699
|
+
tokenAddress: contracts.steth.address,
|
|
2700
|
+
ownerAddress: accountAddress,
|
|
2701
|
+
}),
|
|
2702
|
+
this.#readTokenBalanceWithFallback({
|
|
2703
|
+
account,
|
|
2704
|
+
runtimeConfig,
|
|
2705
|
+
tokenAddress: contracts.wsteth.address,
|
|
2706
|
+
ownerAddress: accountAddress,
|
|
2707
|
+
}),
|
|
2708
|
+
]);
|
|
2709
|
+
const wstEthAsStEth = await this.#quoteLidoOutputRaw({
|
|
2032
2710
|
runtimeConfig,
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
plan,
|
|
2711
|
+
operation: "unwrap_wsteth",
|
|
2712
|
+
amount: wstEthBalance,
|
|
2713
|
+
fromAddress: accountAddress,
|
|
2037
2714
|
});
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2715
|
+
const stEthEquivalentTotal = stEthBalance + wstEthAsStEth;
|
|
2716
|
+
const positions = [];
|
|
2717
|
+
if (stEthBalance > 0n) {
|
|
2718
|
+
positions.push({
|
|
2719
|
+
asset: "stETH",
|
|
2720
|
+
tokenAddress: contracts.steth.address,
|
|
2721
|
+
tokenMetadata: stEthMetadata,
|
|
2722
|
+
balanceRaw: stEthBalance.toString(),
|
|
2723
|
+
balanceFormatted: formatUnits(stEthBalance, stEthMetadata.decimals),
|
|
2724
|
+
stEthEquivalentRaw: stEthBalance.toString(),
|
|
2725
|
+
stEthEquivalentFormatted: formatUnits(stEthBalance, stEthMetadata.decimals),
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
if (wstEthBalance > 0n) {
|
|
2729
|
+
positions.push({
|
|
2730
|
+
asset: "wstETH",
|
|
2731
|
+
tokenAddress: contracts.wsteth.address,
|
|
2732
|
+
tokenMetadata: wstEthMetadata,
|
|
2733
|
+
balanceRaw: wstEthBalance.toString(),
|
|
2734
|
+
balanceFormatted: formatUnits(wstEthBalance, wstEthMetadata.decimals),
|
|
2735
|
+
stEthEquivalentRaw: wstEthAsStEth.toString(),
|
|
2736
|
+
stEthEquivalentFormatted: formatUnits(wstEthAsStEth, stEthMetadata.decimals),
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
return {
|
|
2740
|
+
network: runtimeConfig.network,
|
|
2741
|
+
chainId: runtimeConfig.chainId,
|
|
2742
|
+
accountIndex,
|
|
2743
|
+
address: accountAddress,
|
|
2744
|
+
protocol: "lido",
|
|
2745
|
+
preferredPositionToken: "wstETH",
|
|
2746
|
+
contracts: {
|
|
2747
|
+
stETH: contracts.steth.address,
|
|
2748
|
+
wstETH: contracts.wsteth.address,
|
|
2749
|
+
referralStaker: contracts.referralStaker,
|
|
2750
|
+
withdrawalQueue: contracts.withdrawalQueue,
|
|
2751
|
+
},
|
|
2752
|
+
nativeBalanceWei: BigInt(nativeBalance || 0).toString(),
|
|
2753
|
+
nativeBalanceFormatted: formatUnits(BigInt(nativeBalance || 0), 18),
|
|
2754
|
+
stEthEquivalentTotalRaw: stEthEquivalentTotal.toString(),
|
|
2755
|
+
stEthEquivalentTotalFormatted: formatUnits(stEthEquivalentTotal, stEthMetadata.decimals),
|
|
2756
|
+
positionCount: positions.length,
|
|
2757
|
+
positions,
|
|
2758
|
+
source: "lido-contracts",
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
async getLidoWithdrawalRequests({ seedPhrase, address, accountIndex = 0, network }) {
|
|
2765
|
+
return this.#withReadableAccount(
|
|
2766
|
+
{ seedPhrase, address, accountIndex, network },
|
|
2767
|
+
async (account, runtimeConfig) => {
|
|
2768
|
+
assertLidoSupportedNetwork(runtimeConfig.network);
|
|
2769
|
+
const accountAddress = await account.getAddress();
|
|
2770
|
+
const contracts = this.#getLidoContracts(runtimeConfig.network);
|
|
2771
|
+
const [stEthMetadata, wstEthMetadata, requestIds] = await Promise.all([
|
|
2772
|
+
this.#getLidoTokenMetadata(runtimeConfig, contracts.steth),
|
|
2773
|
+
this.#getLidoTokenMetadata(runtimeConfig, contracts.wsteth),
|
|
2774
|
+
this.#getLidoWithdrawalRequestIds(runtimeConfig, accountAddress),
|
|
2775
|
+
]);
|
|
2776
|
+
const statuses = requestIds.length
|
|
2777
|
+
? await this.#getLidoWithdrawalStatuses(runtimeConfig, requestIds)
|
|
2778
|
+
: [];
|
|
2779
|
+
const requests = statuses.map((status) =>
|
|
2780
|
+
this.#formatLidoWithdrawalStatus(status, stEthMetadata, wstEthMetadata)
|
|
2781
|
+
);
|
|
2782
|
+
const claimableCount = requests.filter((request) => request.claimable).length;
|
|
2783
|
+
return {
|
|
2784
|
+
network: runtimeConfig.network,
|
|
2785
|
+
chainId: runtimeConfig.chainId,
|
|
2786
|
+
accountIndex,
|
|
2787
|
+
address: accountAddress,
|
|
2788
|
+
protocol: "lido",
|
|
2789
|
+
withdrawalQueue: contracts.withdrawalQueue,
|
|
2790
|
+
requestCount: requests.length,
|
|
2791
|
+
claimableCount,
|
|
2792
|
+
requests,
|
|
2793
|
+
source: "lido-contracts",
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
async quoteLidoOperation({
|
|
2043
2800
|
seedPhrase,
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2801
|
+
address,
|
|
2802
|
+
operation,
|
|
2803
|
+
amount,
|
|
2804
|
+
accountIndex = 0,
|
|
2805
|
+
network,
|
|
2806
|
+
}) {
|
|
2807
|
+
return this.#withReadableAccount(
|
|
2808
|
+
{ seedPhrase, address, accountIndex, network },
|
|
2809
|
+
async (account, runtimeConfig) => {
|
|
2810
|
+
assertLidoSupportedNetwork(runtimeConfig.network);
|
|
2811
|
+
const request = buildLidoOperationRequest({ operation, amount });
|
|
2812
|
+
const accountAddress = await account.getAddress();
|
|
2813
|
+
const plan = await this.#buildLidoOperationPlan({
|
|
2814
|
+
account,
|
|
2815
|
+
runtimeConfig,
|
|
2816
|
+
address: accountAddress,
|
|
2817
|
+
request,
|
|
2818
|
+
tolerateOperationFeeFailure: true,
|
|
2819
|
+
});
|
|
2820
|
+
return this.#formatLidoOperationResponse({
|
|
2821
|
+
runtimeConfig,
|
|
2822
|
+
accountIndex,
|
|
2823
|
+
address: accountAddress,
|
|
2824
|
+
request,
|
|
2825
|
+
plan,
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
);
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
async sendLidoOperation({
|
|
2832
|
+
seedPhrase,
|
|
2833
|
+
operation,
|
|
2834
|
+
amount,
|
|
2047
2835
|
accountIndex = 0,
|
|
2048
2836
|
network,
|
|
2049
2837
|
expectedQuoteFingerprint = null,
|
|
2050
|
-
minimumTokenOutAmount = null,
|
|
2051
2838
|
}) {
|
|
2052
2839
|
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
2053
|
-
|
|
2054
|
-
const
|
|
2840
|
+
assertLidoSupportedNetwork(runtimeConfig.network);
|
|
2841
|
+
const request = buildLidoOperationRequest({ operation, amount });
|
|
2055
2842
|
const normalizedExpectedQuoteFingerprint =
|
|
2056
2843
|
typeof expectedQuoteFingerprint === "string" && expectedQuoteFingerprint.trim()
|
|
2057
2844
|
? expectedQuoteFingerprint.trim()
|
|
2058
2845
|
: null;
|
|
2059
|
-
const requestedMinimumTokenOutAmount =
|
|
2060
|
-
minimumTokenOutAmount !== null && minimumTokenOutAmount !== undefined
|
|
2061
|
-
? assertPositiveBigIntString(minimumTokenOutAmount, "minimumTokenOutAmount")
|
|
2062
|
-
: null;
|
|
2063
2846
|
const address = await account.getAddress();
|
|
2064
|
-
let initialPlan = await this.#
|
|
2847
|
+
let initialPlan = await this.#buildLidoOperationPlan({
|
|
2065
2848
|
account,
|
|
2066
2849
|
runtimeConfig,
|
|
2067
|
-
|
|
2850
|
+
address,
|
|
2851
|
+
request,
|
|
2852
|
+
tolerateOperationFeeFailure: true,
|
|
2068
2853
|
});
|
|
2069
|
-
|
|
2070
|
-
this.#getSwapTokenMetadata(runtimeConfig, swapRequest.tokenIn, initialPlan.priceRoute?.srcDecimals),
|
|
2071
|
-
this.#getSwapTokenMetadata(runtimeConfig, swapRequest.tokenOut, initialPlan.priceRoute?.destDecimals),
|
|
2072
|
-
]);
|
|
2073
|
-
this.#assertExpectedSwapFingerprint(
|
|
2854
|
+
this.#assertExpectedLidoFingerprint(
|
|
2074
2855
|
normalizedExpectedQuoteFingerprint,
|
|
2075
2856
|
initialPlan.quoteFingerprint
|
|
2076
2857
|
);
|
|
2077
|
-
this.#assertMinimumSwapOutput(
|
|
2078
|
-
requestedMinimumTokenOutAmount,
|
|
2079
|
-
initialPlan.minimumTokenOutAmount,
|
|
2080
|
-
initialPlan.tokenOutAmount
|
|
2081
|
-
);
|
|
2082
2858
|
|
|
2083
|
-
const approvalExecution = await this.#
|
|
2859
|
+
const approvalExecution = await this.#executeLidoApprovalsIfNeeded({
|
|
2084
2860
|
account,
|
|
2085
2861
|
runtimeConfig,
|
|
2086
|
-
|
|
2862
|
+
request,
|
|
2087
2863
|
plan: initialPlan,
|
|
2088
2864
|
});
|
|
2089
2865
|
|
|
2090
2866
|
let finalPlan = initialPlan;
|
|
2091
2867
|
try {
|
|
2092
2868
|
if (approvalExecution.performed) {
|
|
2093
|
-
finalPlan = await this.#
|
|
2869
|
+
finalPlan = await this.#buildLidoOperationPlan({
|
|
2094
2870
|
account,
|
|
2095
2871
|
runtimeConfig,
|
|
2096
|
-
|
|
2872
|
+
address,
|
|
2873
|
+
request,
|
|
2097
2874
|
});
|
|
2098
|
-
this.#
|
|
2875
|
+
this.#assertExpectedLidoFingerprint(
|
|
2099
2876
|
normalizedExpectedQuoteFingerprint,
|
|
2100
2877
|
finalPlan.quoteFingerprint
|
|
2101
2878
|
);
|
|
2102
2879
|
}
|
|
2103
|
-
this.#assertMinimumSwapOutput(
|
|
2104
|
-
requestedMinimumTokenOutAmount,
|
|
2105
|
-
finalPlan.minimumTokenOutAmount,
|
|
2106
|
-
finalPlan.tokenOutAmount
|
|
2107
|
-
);
|
|
2108
|
-
|
|
2109
|
-
const allowanceReadUncertain =
|
|
2110
|
-
approvalExecution.performed && finalPlan.allowanceReadError !== null;
|
|
2111
2880
|
|
|
2112
|
-
if (finalPlan.approval.required
|
|
2881
|
+
if (finalPlan.approval.required) {
|
|
2113
2882
|
throw createTaggedError(
|
|
2114
|
-
"
|
|
2115
|
-
"
|
|
2883
|
+
"Lido operation still requires token approval after the approval step completed.",
|
|
2884
|
+
"lido_approval_required",
|
|
2116
2885
|
{
|
|
2117
2886
|
spender: finalPlan.spender,
|
|
2118
|
-
requiredAllowance: finalPlan.
|
|
2887
|
+
requiredAllowance: finalPlan.amount.toString(),
|
|
2119
2888
|
currentAllowance: finalPlan.currentAllowance.toString(),
|
|
2120
2889
|
}
|
|
2121
2890
|
);
|
|
2122
2891
|
}
|
|
2123
2892
|
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
const result = {
|
|
2135
|
-
hash,
|
|
2136
|
-
fee: totalFee.toString(),
|
|
2137
|
-
swapFee: finalPlan.swapFee.toString(),
|
|
2138
|
-
approvalFee: approvalExecution.totalFee.toString(),
|
|
2139
|
-
tokenInAmount: finalPlan.tokenInAmount.toString(),
|
|
2140
|
-
tokenOutAmount: finalPlan.tokenOutAmount.toString(),
|
|
2141
|
-
...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
|
|
2142
|
-
...(approvalExecution.resetAllowanceHash
|
|
2143
|
-
? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
|
|
2144
|
-
: {}),
|
|
2145
|
-
};
|
|
2146
|
-
return {
|
|
2147
|
-
network: runtimeConfig.network,
|
|
2148
|
-
chainId: runtimeConfig.chainId,
|
|
2149
|
-
accountIndex,
|
|
2150
|
-
address,
|
|
2151
|
-
protocol: "velora",
|
|
2152
|
-
executionSupported: true,
|
|
2153
|
-
swapRequest,
|
|
2154
|
-
tokenInMetadata,
|
|
2155
|
-
tokenOutMetadata,
|
|
2156
|
-
inputAmountFormatted: formatUnits(swapRequest.tokenInAmount, tokenInMetadata.decimals),
|
|
2157
|
-
outputAmountFormatted: formatUnits(finalPlan.tokenOutAmount, tokenOutMetadata.decimals),
|
|
2158
|
-
quoteFingerprint: finalPlan.quoteFingerprint,
|
|
2159
|
-
estimatedFeeWei: totalFee.toString(),
|
|
2160
|
-
estimatedSwapFeeWei: finalPlan.swapFee.toString(),
|
|
2161
|
-
estimatedApprovalFeeWei: approvalExecution.totalFee.toString(),
|
|
2162
|
-
feeEstimateAvailable: true,
|
|
2163
|
-
feeEstimateError: null,
|
|
2164
|
-
slippageBps: finalPlan.slippageBps,
|
|
2165
|
-
minimumOutputAmountRaw: finalPlan.minimumTokenOutAmount.toString(),
|
|
2166
|
-
allowance: {
|
|
2167
|
-
spender: finalPlan.spender,
|
|
2168
|
-
currentAllowance: finalPlan.currentAllowance.toString(),
|
|
2169
|
-
requiredAllowance: finalPlan.tokenInAmount.toString(),
|
|
2170
|
-
approvalRequired: finalPlan.approval.required,
|
|
2171
|
-
approvalSequence: finalPlan.approval.steps,
|
|
2172
|
-
readError: finalPlan.allowanceReadError,
|
|
2173
|
-
},
|
|
2174
|
-
router: finalPlan.router,
|
|
2175
|
-
simulation: effectiveSimulation,
|
|
2176
|
-
swapTransaction: finalPlan.swapTransaction,
|
|
2177
|
-
result,
|
|
2178
|
-
source: "wdk-protocol-swap-velora-evm",
|
|
2179
|
-
};
|
|
2180
|
-
} catch (error) {
|
|
2181
|
-
const cleanup = await this.#restoreAllowanceAfterFailedSwap({
|
|
2182
|
-
account,
|
|
2183
|
-
runtimeConfig,
|
|
2184
|
-
tokenAddress: swapRequest.tokenIn,
|
|
2185
|
-
spender: initialPlan.spender,
|
|
2186
|
-
originalAllowance: initialPlan.currentAllowance,
|
|
2187
|
-
approvalExecution,
|
|
2188
|
-
});
|
|
2189
|
-
this.#throwSwapFailureWithCleanup(error, cleanup);
|
|
2190
|
-
}
|
|
2191
|
-
});
|
|
2192
|
-
}
|
|
2893
|
+
if (finalPlan.operationFee === null) {
|
|
2894
|
+
throw createTaggedError(
|
|
2895
|
+
"Lido operation fee estimate was unavailable. Generate a new quote before sending.",
|
|
2896
|
+
"lido_fee_unavailable",
|
|
2897
|
+
{
|
|
2898
|
+
operation: request.operation,
|
|
2899
|
+
feeEstimateError: finalPlan.operationFeeError,
|
|
2900
|
+
}
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2193
2903
|
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
destinationChain,
|
|
2198
|
-
outputToken,
|
|
2199
|
-
destinationAddress,
|
|
2200
|
-
tokenInAmount,
|
|
2201
|
-
slippage = DEFAULT_LIFI_SLIPPAGE,
|
|
2202
|
-
allowBridges = null,
|
|
2203
|
-
denyBridges = null,
|
|
2204
|
-
preferBridges = null,
|
|
2205
|
-
accountIndex = 0,
|
|
2206
|
-
network,
|
|
2207
|
-
minimumTokenOutAmount = null,
|
|
2208
|
-
}) {
|
|
2209
|
-
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
2210
|
-
assertLifiSupportedNetwork(runtimeConfig.network);
|
|
2211
|
-
const swapRequest = buildLifiEvmSwapRequest({
|
|
2212
|
-
tokenIn,
|
|
2213
|
-
destinationChain,
|
|
2214
|
-
outputToken,
|
|
2215
|
-
destinationAddress,
|
|
2216
|
-
tokenInAmount,
|
|
2217
|
-
slippage,
|
|
2218
|
-
allowBridges,
|
|
2219
|
-
denyBridges,
|
|
2220
|
-
preferBridges,
|
|
2221
|
-
});
|
|
2222
|
-
const requestedMinimumTokenOutAmount =
|
|
2223
|
-
minimumTokenOutAmount !== null && minimumTokenOutAmount !== undefined
|
|
2224
|
-
? assertPositiveBigIntString(minimumTokenOutAmount, "minimumTokenOutAmount")
|
|
2225
|
-
: null;
|
|
2226
|
-
const sourceAddress = await account.getAddress();
|
|
2227
|
-
let initialPlan = await this.#buildLifiEvmSwapPlan({
|
|
2228
|
-
account,
|
|
2229
|
-
runtimeConfig,
|
|
2230
|
-
address: sourceAddress,
|
|
2231
|
-
swapRequest,
|
|
2232
|
-
});
|
|
2233
|
-
this.#assertMinimumSwapOutput(
|
|
2234
|
-
requestedMinimumTokenOutAmount,
|
|
2235
|
-
initialPlan.minimumTokenOutAmount,
|
|
2236
|
-
initialPlan.tokenOutAmount
|
|
2237
|
-
);
|
|
2238
|
-
|
|
2239
|
-
const approvalExecution = await this.#executeSwapApprovalsIfNeeded({
|
|
2240
|
-
account,
|
|
2241
|
-
runtimeConfig,
|
|
2242
|
-
swapRequest: {
|
|
2243
|
-
tokenIn: swapRequest.tokenIn,
|
|
2244
|
-
},
|
|
2245
|
-
plan: initialPlan,
|
|
2246
|
-
});
|
|
2247
|
-
|
|
2248
|
-
let finalPlan = initialPlan;
|
|
2249
|
-
try {
|
|
2250
|
-
if (approvalExecution.performed) {
|
|
2251
|
-
finalPlan = await this.#buildLifiEvmSwapPlan({
|
|
2252
|
-
account,
|
|
2253
|
-
runtimeConfig,
|
|
2254
|
-
address: sourceAddress,
|
|
2255
|
-
swapRequest,
|
|
2256
|
-
});
|
|
2257
|
-
}
|
|
2258
|
-
this.#assertMinimumSwapOutput(
|
|
2259
|
-
requestedMinimumTokenOutAmount,
|
|
2260
|
-
finalPlan.minimumTokenOutAmount,
|
|
2261
|
-
finalPlan.tokenOutAmount
|
|
2262
|
-
);
|
|
2263
|
-
|
|
2264
|
-
const allowanceReadUncertain =
|
|
2265
|
-
approvalExecution.performed && finalPlan.allowanceReadError !== null;
|
|
2266
|
-
|
|
2267
|
-
if (finalPlan.approval.required && !allowanceReadUncertain) {
|
|
2268
|
-
throw createTaggedError(
|
|
2269
|
-
"LI.FI cross-chain swap still requires token approval after the approval step completed.",
|
|
2270
|
-
"swap_approval_required",
|
|
2271
|
-
{
|
|
2272
|
-
spender: finalPlan.spender,
|
|
2273
|
-
requiredAllowance: finalPlan.tokenInAmount.toString(),
|
|
2274
|
-
currentAllowance: finalPlan.currentAllowance.toString(),
|
|
2275
|
-
}
|
|
2276
|
-
);
|
|
2277
|
-
}
|
|
2278
|
-
|
|
2279
|
-
const effectiveSimulation = allowanceReadUncertain
|
|
2280
|
-
? await this.#simulatePreparedTransaction({
|
|
2281
|
-
runtimeConfig,
|
|
2282
|
-
from: sourceAddress,
|
|
2283
|
-
tx: finalPlan.swapTx,
|
|
2284
|
-
})
|
|
2285
|
-
: finalPlan.simulation;
|
|
2286
|
-
this.#assertSimulationSucceeded(effectiveSimulation);
|
|
2287
|
-
|
|
2288
|
-
const { hash } = await account.sendTransaction(finalPlan.swapTx);
|
|
2289
|
-
const totalFee = approvalExecution.totalFee + finalPlan.swapFee;
|
|
2290
|
-
const result = {
|
|
2291
|
-
hash,
|
|
2292
|
-
fee: totalFee.toString(),
|
|
2293
|
-
swapFee: finalPlan.swapFee.toString(),
|
|
2294
|
-
approvalFee: approvalExecution.totalFee.toString(),
|
|
2295
|
-
tokenInAmount: finalPlan.tokenInAmount.toString(),
|
|
2296
|
-
tokenOutAmount: finalPlan.tokenOutAmount.toString(),
|
|
2297
|
-
...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
|
|
2298
|
-
...(approvalExecution.resetAllowanceHash
|
|
2299
|
-
? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
|
|
2300
|
-
: {}),
|
|
2301
|
-
};
|
|
2904
|
+
const result = await account.sendTransaction(finalPlan.operationTx);
|
|
2905
|
+
const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
|
|
2906
|
+
const totalFee = approvalExecution.totalFee + resultFee;
|
|
2302
2907
|
return {
|
|
2303
|
-
...this.#
|
|
2908
|
+
...this.#formatLidoOperationResponse({
|
|
2304
2909
|
runtimeConfig,
|
|
2305
2910
|
accountIndex,
|
|
2306
|
-
address
|
|
2307
|
-
|
|
2911
|
+
address,
|
|
2912
|
+
request,
|
|
2308
2913
|
plan: {
|
|
2309
2914
|
...finalPlan,
|
|
2310
|
-
|
|
2311
|
-
swapFee: totalFee,
|
|
2915
|
+
operationFee: resultFee,
|
|
2312
2916
|
totalEstimatedFee: totalFee,
|
|
2313
2917
|
approval: {
|
|
2314
2918
|
...finalPlan.approval,
|
|
@@ -2316,358 +2920,938 @@ export class WdkEvmWalletService {
|
|
|
2316
2920
|
},
|
|
2317
2921
|
},
|
|
2318
2922
|
}),
|
|
2319
|
-
result
|
|
2923
|
+
result: {
|
|
2924
|
+
...result,
|
|
2925
|
+
fee: resultFee.toString(),
|
|
2926
|
+
totalFee: totalFee.toString(),
|
|
2927
|
+
approvalFee: approvalExecution.totalFee.toString(),
|
|
2928
|
+
...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
|
|
2929
|
+
...(approvalExecution.resetAllowanceHash
|
|
2930
|
+
? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
|
|
2931
|
+
: {}),
|
|
2932
|
+
},
|
|
2320
2933
|
};
|
|
2321
2934
|
} catch (error) {
|
|
2322
|
-
const cleanup = await this.#
|
|
2935
|
+
const cleanup = await this.#restoreAllowanceAfterFailedLidoOperation({
|
|
2323
2936
|
account,
|
|
2324
2937
|
runtimeConfig,
|
|
2325
|
-
tokenAddress:
|
|
2938
|
+
tokenAddress: finalPlan.inputTokenAddress,
|
|
2326
2939
|
spender: initialPlan.spender,
|
|
2327
2940
|
originalAllowance: initialPlan.currentAllowance,
|
|
2328
2941
|
approvalExecution,
|
|
2329
2942
|
});
|
|
2330
|
-
this.#
|
|
2943
|
+
this.#throwLidoFailureWithCleanup(error, cleanup);
|
|
2331
2944
|
}
|
|
2332
2945
|
});
|
|
2333
2946
|
}
|
|
2334
2947
|
|
|
2335
|
-
async
|
|
2948
|
+
async quoteLidoWithdrawalOperation({
|
|
2336
2949
|
seedPhrase,
|
|
2337
2950
|
address,
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
slippageBps,
|
|
2951
|
+
operation,
|
|
2952
|
+
amount,
|
|
2953
|
+
requestId,
|
|
2342
2954
|
accountIndex = 0,
|
|
2343
2955
|
network,
|
|
2344
2956
|
}) {
|
|
2345
2957
|
return this.#withReadableAccount(
|
|
2346
2958
|
{ seedPhrase, address, accountIndex, network },
|
|
2347
2959
|
async (account, runtimeConfig) => {
|
|
2348
|
-
|
|
2349
|
-
const
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
tokenInAmount,
|
|
2353
|
-
slippageBps:
|
|
2354
|
-
slippageBps === undefined || slippageBps === null
|
|
2355
|
-
? this.config.uniswapDefaultSlippageBps
|
|
2356
|
-
: slippageBps,
|
|
2357
|
-
});
|
|
2358
|
-
const swapperAddress = await account.getAddress();
|
|
2359
|
-
const plan = await this.#buildUniswapSwapPlan({
|
|
2960
|
+
assertLidoSupportedNetwork(runtimeConfig.network);
|
|
2961
|
+
const request = buildLidoWithdrawalRequest({ operation, amount, requestId });
|
|
2962
|
+
const accountAddress = await account.getAddress();
|
|
2963
|
+
const plan = await this.#buildLidoWithdrawalPlan({
|
|
2360
2964
|
account,
|
|
2361
2965
|
runtimeConfig,
|
|
2362
|
-
address:
|
|
2363
|
-
|
|
2966
|
+
address: accountAddress,
|
|
2967
|
+
request,
|
|
2968
|
+
tolerateOperationFeeFailure: true,
|
|
2364
2969
|
});
|
|
2365
|
-
return this.#
|
|
2970
|
+
return this.#formatLidoWithdrawalResponse({
|
|
2366
2971
|
runtimeConfig,
|
|
2367
2972
|
accountIndex,
|
|
2368
|
-
address:
|
|
2369
|
-
|
|
2973
|
+
address: accountAddress,
|
|
2974
|
+
request,
|
|
2370
2975
|
plan,
|
|
2371
2976
|
});
|
|
2372
2977
|
}
|
|
2373
2978
|
);
|
|
2374
2979
|
}
|
|
2375
2980
|
|
|
2376
|
-
async
|
|
2981
|
+
async sendLidoWithdrawalOperation({
|
|
2377
2982
|
seedPhrase,
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
slippageBps,
|
|
2983
|
+
operation,
|
|
2984
|
+
amount,
|
|
2985
|
+
requestId,
|
|
2382
2986
|
accountIndex = 0,
|
|
2383
2987
|
network,
|
|
2384
2988
|
expectedQuoteFingerprint = null,
|
|
2385
|
-
minimumTokenOutAmount = null,
|
|
2386
2989
|
}) {
|
|
2387
2990
|
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
2388
|
-
|
|
2389
|
-
const
|
|
2390
|
-
tokenIn,
|
|
2391
|
-
tokenOut,
|
|
2392
|
-
tokenInAmount,
|
|
2393
|
-
slippageBps:
|
|
2394
|
-
slippageBps === undefined || slippageBps === null
|
|
2395
|
-
? this.config.uniswapDefaultSlippageBps
|
|
2396
|
-
: slippageBps,
|
|
2397
|
-
});
|
|
2991
|
+
assertLidoSupportedNetwork(runtimeConfig.network);
|
|
2992
|
+
const request = buildLidoWithdrawalRequest({ operation, amount, requestId });
|
|
2398
2993
|
const normalizedExpectedQuoteFingerprint =
|
|
2399
2994
|
typeof expectedQuoteFingerprint === "string" && expectedQuoteFingerprint.trim()
|
|
2400
2995
|
? expectedQuoteFingerprint.trim()
|
|
2401
2996
|
: null;
|
|
2402
|
-
const requestedMinimumTokenOutAmount =
|
|
2403
|
-
minimumTokenOutAmount !== null && minimumTokenOutAmount !== undefined
|
|
2404
|
-
? assertPositiveBigIntString(minimumTokenOutAmount, "minimumTokenOutAmount")
|
|
2405
|
-
: null;
|
|
2406
2997
|
const address = await account.getAddress();
|
|
2407
|
-
let initialPlan = await this.#
|
|
2998
|
+
let initialPlan = await this.#buildLidoWithdrawalPlan({
|
|
2408
2999
|
account,
|
|
2409
3000
|
runtimeConfig,
|
|
2410
3001
|
address,
|
|
2411
|
-
|
|
3002
|
+
request,
|
|
3003
|
+
tolerateOperationFeeFailure: true,
|
|
2412
3004
|
});
|
|
2413
|
-
this.#
|
|
3005
|
+
this.#assertExpectedLidoWithdrawalFingerprint(
|
|
2414
3006
|
normalizedExpectedQuoteFingerprint,
|
|
2415
3007
|
initialPlan.quoteFingerprint
|
|
2416
3008
|
);
|
|
2417
|
-
this.#assertMinimumSwapOutput(
|
|
2418
|
-
requestedMinimumTokenOutAmount,
|
|
2419
|
-
initialPlan.minimumTokenOutAmount,
|
|
2420
|
-
initialPlan.tokenOutAmount
|
|
2421
|
-
);
|
|
2422
3009
|
|
|
2423
|
-
const approvalExecution = await this.#
|
|
3010
|
+
const approvalExecution = await this.#executeLidoWithdrawalApprovalsIfNeeded({
|
|
2424
3011
|
account,
|
|
2425
3012
|
runtimeConfig,
|
|
2426
|
-
|
|
3013
|
+
request,
|
|
2427
3014
|
plan: initialPlan,
|
|
2428
3015
|
});
|
|
2429
3016
|
|
|
2430
3017
|
let finalPlan = initialPlan;
|
|
2431
3018
|
try {
|
|
2432
3019
|
if (approvalExecution.performed) {
|
|
2433
|
-
|
|
2434
|
-
finalPlan = await this.#buildUniswapSwapPlan({
|
|
3020
|
+
finalPlan = await this.#buildLidoWithdrawalPlan({
|
|
2435
3021
|
account,
|
|
2436
3022
|
runtimeConfig,
|
|
2437
3023
|
address,
|
|
2438
|
-
|
|
3024
|
+
request,
|
|
2439
3025
|
});
|
|
2440
|
-
this.#
|
|
3026
|
+
this.#assertExpectedLidoWithdrawalFingerprint(
|
|
2441
3027
|
normalizedExpectedQuoteFingerprint,
|
|
2442
3028
|
finalPlan.quoteFingerprint
|
|
2443
3029
|
);
|
|
2444
3030
|
}
|
|
2445
|
-
this.#assertMinimumSwapOutput(
|
|
2446
|
-
requestedMinimumTokenOutAmount,
|
|
2447
|
-
finalPlan.minimumTokenOutAmount,
|
|
2448
|
-
finalPlan.tokenOutAmount
|
|
2449
|
-
);
|
|
2450
3031
|
|
|
2451
|
-
|
|
2452
|
-
approvalExecution.performed && finalPlan.allowanceReadError !== null;
|
|
2453
|
-
if (finalPlan.approval.required && !allowanceReadUncertain) {
|
|
3032
|
+
if (finalPlan.approval.required) {
|
|
2454
3033
|
throw createTaggedError(
|
|
2455
|
-
"
|
|
2456
|
-
"
|
|
3034
|
+
"Lido withdrawal still requires token approval after the approval step completed.",
|
|
3035
|
+
"lido_withdrawal_approval_required",
|
|
2457
3036
|
{
|
|
2458
3037
|
spender: finalPlan.spender,
|
|
2459
|
-
requiredAllowance: finalPlan.
|
|
3038
|
+
requiredAllowance: finalPlan.requiredAllowance.toString(),
|
|
2460
3039
|
currentAllowance: finalPlan.currentAllowance.toString(),
|
|
2461
3040
|
}
|
|
2462
3041
|
);
|
|
2463
3042
|
}
|
|
2464
3043
|
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
3044
|
+
if (finalPlan.operationFee === null) {
|
|
3045
|
+
throw createTaggedError(
|
|
3046
|
+
"Lido withdrawal fee estimate was unavailable. Generate a new quote before sending.",
|
|
3047
|
+
"lido_withdrawal_fee_unavailable",
|
|
3048
|
+
{
|
|
3049
|
+
operation: request.operation,
|
|
3050
|
+
feeEstimateError: finalPlan.operationFeeError,
|
|
3051
|
+
}
|
|
3052
|
+
);
|
|
3053
|
+
}
|
|
2469
3054
|
|
|
2470
|
-
const
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
permitData: finalPlan.permitData,
|
|
2474
|
-
signature,
|
|
2475
|
-
});
|
|
2476
|
-
|
|
2477
|
-
const simulation = await this.#simulatePreparedTransaction({
|
|
2478
|
-
runtimeConfig,
|
|
2479
|
-
from: address,
|
|
2480
|
-
tx: swapTx,
|
|
2481
|
-
});
|
|
2482
|
-
this.#assertSimulationSucceeded(simulation);
|
|
2483
|
-
|
|
2484
|
-
const { hash } = await account.sendTransaction(swapTx);
|
|
2485
|
-
const result = {
|
|
2486
|
-
hash,
|
|
2487
|
-
approvalFee: approvalExecution.totalFee.toString(),
|
|
2488
|
-
tokenInAmount: finalPlan.tokenInAmount.toString(),
|
|
2489
|
-
tokenOutAmount: finalPlan.tokenOutAmount.toString(),
|
|
2490
|
-
...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
|
|
2491
|
-
...(approvalExecution.resetAllowanceHash
|
|
2492
|
-
? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
|
|
2493
|
-
: {}),
|
|
2494
|
-
};
|
|
3055
|
+
const result = await account.sendTransaction(finalPlan.operationTx);
|
|
3056
|
+
const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
|
|
3057
|
+
const totalFee = approvalExecution.totalFee + resultFee;
|
|
2495
3058
|
return {
|
|
2496
|
-
...
|
|
3059
|
+
...this.#formatLidoWithdrawalResponse({
|
|
2497
3060
|
runtimeConfig,
|
|
2498
3061
|
accountIndex,
|
|
2499
3062
|
address,
|
|
2500
|
-
|
|
3063
|
+
request,
|
|
2501
3064
|
plan: {
|
|
2502
3065
|
...finalPlan,
|
|
3066
|
+
operationFee: resultFee,
|
|
3067
|
+
totalEstimatedFee: totalFee,
|
|
2503
3068
|
approval: {
|
|
2504
3069
|
...finalPlan.approval,
|
|
2505
3070
|
estimatedFee: approvalExecution.totalFee,
|
|
2506
3071
|
},
|
|
2507
3072
|
},
|
|
2508
|
-
})
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
3073
|
+
}),
|
|
3074
|
+
result: {
|
|
3075
|
+
...result,
|
|
3076
|
+
fee: resultFee.toString(),
|
|
3077
|
+
totalFee: totalFee.toString(),
|
|
3078
|
+
approvalFee: approvalExecution.totalFee.toString(),
|
|
3079
|
+
...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
|
|
3080
|
+
...(approvalExecution.resetAllowanceHash
|
|
3081
|
+
? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
|
|
3082
|
+
: {}),
|
|
3083
|
+
},
|
|
2512
3084
|
};
|
|
2513
3085
|
} catch (error) {
|
|
2514
|
-
const cleanup = await this.#
|
|
3086
|
+
const cleanup = await this.#restoreAllowanceAfterFailedLidoWithdrawal({
|
|
2515
3087
|
account,
|
|
2516
3088
|
runtimeConfig,
|
|
2517
|
-
tokenAddress:
|
|
3089
|
+
tokenAddress: finalPlan.inputTokenAddress,
|
|
2518
3090
|
spender: initialPlan.spender,
|
|
2519
3091
|
originalAllowance: initialPlan.currentAllowance,
|
|
2520
3092
|
approvalExecution,
|
|
2521
3093
|
});
|
|
2522
|
-
this.#
|
|
3094
|
+
this.#throwLidoWithdrawalFailureWithCleanup(error, cleanup);
|
|
2523
3095
|
}
|
|
2524
3096
|
});
|
|
2525
3097
|
}
|
|
2526
3098
|
|
|
2527
|
-
async
|
|
2528
|
-
if (isZeroAddress(tokenAddress)) {
|
|
2529
|
-
return {
|
|
2530
|
-
address: ZERO_ADDRESS,
|
|
2531
|
-
name: runtimeConfig.nativeSymbol === "ETH" ? "Ether" : runtimeConfig.nativeSymbol,
|
|
2532
|
-
symbol: runtimeConfig.nativeSymbol,
|
|
2533
|
-
decimals: 18,
|
|
2534
|
-
verified: true,
|
|
2535
|
-
source: "native-asset",
|
|
2536
|
-
};
|
|
2537
|
-
}
|
|
2538
|
-
return this.#getTokenMetadata(runtimeConfig, tokenAddress);
|
|
2539
|
-
}
|
|
2540
|
-
|
|
2541
|
-
async #formatUniswapSwapResponse({ runtimeConfig, accountIndex, address, swapRequest, plan }) {
|
|
2542
|
-
const [tokenInMetadata, tokenOutMetadata] = await Promise.all([
|
|
2543
|
-
this.#getUniswapTokenMetadata(runtimeConfig, swapRequest.tokenIn),
|
|
2544
|
-
this.#getUniswapTokenMetadata(runtimeConfig, swapRequest.tokenOut),
|
|
2545
|
-
]);
|
|
2546
|
-
return {
|
|
2547
|
-
network: runtimeConfig.network,
|
|
2548
|
-
chainId: runtimeConfig.chainId,
|
|
2549
|
-
accountIndex,
|
|
2550
|
-
address,
|
|
2551
|
-
protocol: "uniswap",
|
|
2552
|
-
executionSupported: true,
|
|
2553
|
-
routing: "CLASSIC",
|
|
2554
|
-
swapRequest: {
|
|
2555
|
-
tokenIn: swapRequest.tokenIn,
|
|
2556
|
-
tokenOut: swapRequest.tokenOut,
|
|
2557
|
-
tokenInAmount: swapRequest.tokenInAmount.toString(),
|
|
2558
|
-
},
|
|
2559
|
-
tokenInMetadata,
|
|
2560
|
-
tokenOutMetadata,
|
|
2561
|
-
inputAmountFormatted: formatUnits(swapRequest.tokenInAmount, tokenInMetadata.decimals),
|
|
2562
|
-
outputAmountFormatted: formatUnits(plan.tokenOutAmount, tokenOutMetadata.decimals),
|
|
2563
|
-
quoteFingerprint: plan.quoteFingerprint,
|
|
2564
|
-
slippageBps: plan.slippageBps,
|
|
2565
|
-
minimumOutputAmountRaw: plan.minimumTokenOutAmount.toString(),
|
|
2566
|
-
permitRequired: plan.permitData !== null,
|
|
2567
|
-
gasFeeWei: plan.gasFee,
|
|
2568
|
-
gasFeeUSD: plan.gasFeeUSD,
|
|
2569
|
-
estimatedApprovalFeeWei: plan.approval.estimatedFee.toString(),
|
|
2570
|
-
allowance: {
|
|
2571
|
-
spender: plan.spender,
|
|
2572
|
-
currentAllowance: plan.currentAllowance.toString(),
|
|
2573
|
-
requiredAllowance: plan.tokenInAmount.toString(),
|
|
2574
|
-
approvalRequired: plan.approval.required,
|
|
2575
|
-
approvalSequence: plan.approval.steps,
|
|
2576
|
-
readError: plan.allowanceReadError,
|
|
2577
|
-
},
|
|
2578
|
-
router: plan.router,
|
|
2579
|
-
source: "uniswap-trading-api",
|
|
2580
|
-
};
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
|
-
async quoteNativeTransfer({ seedPhrase, to, value, accountIndex = 0, network }) {
|
|
2584
|
-
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
2585
|
-
const tx = {
|
|
2586
|
-
to: normalizeAddress(to, "to"),
|
|
2587
|
-
value: assertPositiveBigIntString(value, "value"),
|
|
2588
|
-
};
|
|
2589
|
-
const quote = await account.quoteSendTransaction(tx);
|
|
2590
|
-
return {
|
|
2591
|
-
network: runtimeConfig.network,
|
|
2592
|
-
chainId: runtimeConfig.chainId,
|
|
2593
|
-
accountIndex,
|
|
2594
|
-
transaction: tx,
|
|
2595
|
-
quote,
|
|
2596
|
-
source: "wdk-wallet-evm",
|
|
2597
|
-
};
|
|
2598
|
-
});
|
|
2599
|
-
}
|
|
2600
|
-
|
|
2601
|
-
async sendNativeTransfer({ seedPhrase, to, value, accountIndex = 0, network }) {
|
|
2602
|
-
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
2603
|
-
const tx = {
|
|
2604
|
-
to: normalizeAddress(to, "to"),
|
|
2605
|
-
value: assertPositiveBigIntString(value, "value"),
|
|
2606
|
-
};
|
|
2607
|
-
const result = await account.sendTransaction(tx);
|
|
2608
|
-
return {
|
|
2609
|
-
network: runtimeConfig.network,
|
|
2610
|
-
chainId: runtimeConfig.chainId,
|
|
2611
|
-
accountIndex,
|
|
2612
|
-
transaction: tx,
|
|
2613
|
-
result,
|
|
2614
|
-
source: "wdk-wallet-evm",
|
|
2615
|
-
};
|
|
2616
|
-
});
|
|
2617
|
-
}
|
|
2618
|
-
|
|
2619
|
-
async quoteTokenTransfer({
|
|
3099
|
+
async quoteSwap({
|
|
2620
3100
|
seedPhrase,
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
3101
|
+
address,
|
|
3102
|
+
tokenIn,
|
|
3103
|
+
tokenOut,
|
|
3104
|
+
tokenInAmount,
|
|
2624
3105
|
accountIndex = 0,
|
|
2625
3106
|
network,
|
|
2626
3107
|
}) {
|
|
2627
|
-
return this.#
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
3108
|
+
return this.#withReadableAccount(
|
|
3109
|
+
{ seedPhrase, address, accountIndex, network },
|
|
3110
|
+
async (account, runtimeConfig) => {
|
|
3111
|
+
assertVeloraSupportedNetwork(runtimeConfig.network);
|
|
3112
|
+
const swapRequest = buildSwapRequest({ tokenIn, tokenOut, tokenInAmount });
|
|
3113
|
+
const address = await account.getAddress();
|
|
3114
|
+
const readOnlyAccount =
|
|
3115
|
+
typeof account.toReadOnlyAccount === "function" ? await account.toReadOnlyAccount() : account;
|
|
3116
|
+
try {
|
|
3117
|
+
const plan = await this.#buildVeloraSwapPlan({
|
|
3118
|
+
account: readOnlyAccount,
|
|
3119
|
+
runtimeConfig,
|
|
3120
|
+
swapRequest,
|
|
3121
|
+
tolerateSwapFeeFailure: true,
|
|
3122
|
+
});
|
|
3123
|
+
const [tokenInMetadata, tokenOutMetadata] = await Promise.all([
|
|
3124
|
+
this.#getSwapTokenMetadata(runtimeConfig, swapRequest.tokenIn, plan.priceRoute?.srcDecimals),
|
|
3125
|
+
this.#getSwapTokenMetadata(runtimeConfig, swapRequest.tokenOut, plan.priceRoute?.destDecimals),
|
|
3126
|
+
]);
|
|
3127
|
+
const quote = {
|
|
3128
|
+
fee: plan.swapFee !== null ? plan.swapFee.toString() : null,
|
|
3129
|
+
tokenInAmount: plan.tokenInAmount.toString(),
|
|
3130
|
+
tokenOutAmount: plan.tokenOutAmount.toString(),
|
|
3131
|
+
priceRoute: plan.priceRoute,
|
|
3132
|
+
};
|
|
3133
|
+
return {
|
|
3134
|
+
network: runtimeConfig.network,
|
|
3135
|
+
chainId: runtimeConfig.chainId,
|
|
3136
|
+
accountIndex,
|
|
3137
|
+
address,
|
|
3138
|
+
protocol: "velora",
|
|
3139
|
+
executionSupported: true,
|
|
3140
|
+
swapRequest,
|
|
3141
|
+
tokenInMetadata,
|
|
3142
|
+
tokenOutMetadata,
|
|
3143
|
+
inputAmountFormatted: formatUnits(swapRequest.tokenInAmount, tokenInMetadata.decimals),
|
|
3144
|
+
outputAmountFormatted: formatUnits(plan.tokenOutAmount, tokenOutMetadata.decimals),
|
|
3145
|
+
quoteFingerprint: plan.quoteFingerprint,
|
|
3146
|
+
estimatedFeeWei:
|
|
3147
|
+
plan.totalEstimatedFee !== null ? plan.totalEstimatedFee.toString() : null,
|
|
3148
|
+
estimatedSwapFeeWei: plan.swapFee !== null ? plan.swapFee.toString() : null,
|
|
3149
|
+
estimatedApprovalFeeWei: plan.approval.estimatedFee.toString(),
|
|
3150
|
+
feeEstimateAvailable: plan.swapFee !== null,
|
|
3151
|
+
feeEstimateError: plan.swapFeeError,
|
|
3152
|
+
slippageBps: plan.slippageBps,
|
|
3153
|
+
minimumOutputAmountRaw: plan.minimumTokenOutAmount.toString(),
|
|
3154
|
+
allowance: {
|
|
3155
|
+
spender: plan.spender,
|
|
3156
|
+
currentAllowance: plan.currentAllowance.toString(),
|
|
3157
|
+
requiredAllowance: plan.tokenInAmount.toString(),
|
|
3158
|
+
approvalRequired: plan.approval.required,
|
|
3159
|
+
approvalSequence: plan.approval.steps,
|
|
3160
|
+
readError: plan.allowanceReadError,
|
|
3161
|
+
},
|
|
3162
|
+
router: plan.router,
|
|
3163
|
+
simulation: plan.simulation,
|
|
3164
|
+
swapTransaction: plan.swapTransaction,
|
|
3165
|
+
quote,
|
|
3166
|
+
source: "wdk-protocol-swap-velora-evm",
|
|
3167
|
+
};
|
|
3168
|
+
} finally {
|
|
3169
|
+
if (readOnlyAccount !== account) {
|
|
3170
|
+
await maybeDispose(readOnlyAccount);
|
|
3171
|
+
}
|
|
2666
3172
|
}
|
|
2667
|
-
throw error;
|
|
2668
3173
|
}
|
|
2669
|
-
|
|
2670
|
-
|
|
3174
|
+
);
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
async quoteLifiSwap({
|
|
3178
|
+
seedPhrase,
|
|
3179
|
+
address,
|
|
3180
|
+
tokenIn,
|
|
3181
|
+
destinationChain,
|
|
3182
|
+
outputToken,
|
|
3183
|
+
destinationAddress,
|
|
3184
|
+
tokenInAmount,
|
|
3185
|
+
slippage = DEFAULT_LIFI_SLIPPAGE,
|
|
3186
|
+
allowBridges = null,
|
|
3187
|
+
denyBridges = null,
|
|
3188
|
+
preferBridges = null,
|
|
3189
|
+
accountIndex = 0,
|
|
3190
|
+
network,
|
|
3191
|
+
}) {
|
|
3192
|
+
return this.#withReadableAccount(
|
|
3193
|
+
{ seedPhrase, address, accountIndex, network },
|
|
3194
|
+
async (account, runtimeConfig) => {
|
|
3195
|
+
assertLifiSupportedNetwork(runtimeConfig.network);
|
|
3196
|
+
const swapRequest = buildLifiEvmSwapRequest({
|
|
3197
|
+
tokenIn,
|
|
3198
|
+
destinationChain,
|
|
3199
|
+
outputToken,
|
|
3200
|
+
destinationAddress,
|
|
3201
|
+
tokenInAmount,
|
|
3202
|
+
slippage,
|
|
3203
|
+
allowBridges,
|
|
3204
|
+
denyBridges,
|
|
3205
|
+
preferBridges,
|
|
3206
|
+
});
|
|
3207
|
+
const sourceAddress = await account.getAddress();
|
|
3208
|
+
const plan = await this.#buildLifiEvmSwapPlan({
|
|
3209
|
+
account,
|
|
3210
|
+
runtimeConfig,
|
|
3211
|
+
address: sourceAddress,
|
|
3212
|
+
swapRequest,
|
|
3213
|
+
tolerateSwapFeeFailure: true,
|
|
3214
|
+
});
|
|
3215
|
+
return this.#formatLifiSwapResponse({
|
|
3216
|
+
runtimeConfig,
|
|
3217
|
+
accountIndex,
|
|
3218
|
+
address: sourceAddress,
|
|
3219
|
+
swapRequest,
|
|
3220
|
+
plan,
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
async swap({
|
|
3227
|
+
seedPhrase,
|
|
3228
|
+
tokenIn,
|
|
3229
|
+
tokenOut,
|
|
3230
|
+
tokenInAmount,
|
|
3231
|
+
accountIndex = 0,
|
|
3232
|
+
network,
|
|
3233
|
+
expectedQuoteFingerprint = null,
|
|
3234
|
+
minimumTokenOutAmount = null,
|
|
3235
|
+
}) {
|
|
3236
|
+
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
3237
|
+
assertVeloraSupportedNetwork(runtimeConfig.network);
|
|
3238
|
+
const swapRequest = buildSwapRequest({ tokenIn, tokenOut, tokenInAmount });
|
|
3239
|
+
const normalizedExpectedQuoteFingerprint =
|
|
3240
|
+
typeof expectedQuoteFingerprint === "string" && expectedQuoteFingerprint.trim()
|
|
3241
|
+
? expectedQuoteFingerprint.trim()
|
|
3242
|
+
: null;
|
|
3243
|
+
const requestedMinimumTokenOutAmount =
|
|
3244
|
+
minimumTokenOutAmount !== null && minimumTokenOutAmount !== undefined
|
|
3245
|
+
? assertPositiveBigIntString(minimumTokenOutAmount, "minimumTokenOutAmount")
|
|
3246
|
+
: null;
|
|
3247
|
+
const address = await account.getAddress();
|
|
3248
|
+
let initialPlan = await this.#buildVeloraSwapPlan({
|
|
3249
|
+
account,
|
|
3250
|
+
runtimeConfig,
|
|
3251
|
+
swapRequest,
|
|
3252
|
+
});
|
|
3253
|
+
const [tokenInMetadata, tokenOutMetadata] = await Promise.all([
|
|
3254
|
+
this.#getSwapTokenMetadata(runtimeConfig, swapRequest.tokenIn, initialPlan.priceRoute?.srcDecimals),
|
|
3255
|
+
this.#getSwapTokenMetadata(runtimeConfig, swapRequest.tokenOut, initialPlan.priceRoute?.destDecimals),
|
|
3256
|
+
]);
|
|
3257
|
+
this.#assertExpectedSwapFingerprint(
|
|
3258
|
+
normalizedExpectedQuoteFingerprint,
|
|
3259
|
+
initialPlan.quoteFingerprint
|
|
3260
|
+
);
|
|
3261
|
+
this.#assertMinimumSwapOutput(
|
|
3262
|
+
requestedMinimumTokenOutAmount,
|
|
3263
|
+
initialPlan.minimumTokenOutAmount,
|
|
3264
|
+
initialPlan.tokenOutAmount
|
|
3265
|
+
);
|
|
3266
|
+
|
|
3267
|
+
const approvalExecution = await this.#executeSwapApprovalsIfNeeded({
|
|
3268
|
+
account,
|
|
3269
|
+
runtimeConfig,
|
|
3270
|
+
swapRequest,
|
|
3271
|
+
plan: initialPlan,
|
|
3272
|
+
});
|
|
3273
|
+
|
|
3274
|
+
let finalPlan = initialPlan;
|
|
3275
|
+
try {
|
|
3276
|
+
if (approvalExecution.performed) {
|
|
3277
|
+
finalPlan = await this.#buildVeloraSwapPlan({
|
|
3278
|
+
account,
|
|
3279
|
+
runtimeConfig,
|
|
3280
|
+
swapRequest,
|
|
3281
|
+
});
|
|
3282
|
+
this.#assertExpectedSwapFingerprint(
|
|
3283
|
+
normalizedExpectedQuoteFingerprint,
|
|
3284
|
+
finalPlan.quoteFingerprint
|
|
3285
|
+
);
|
|
3286
|
+
}
|
|
3287
|
+
this.#assertMinimumSwapOutput(
|
|
3288
|
+
requestedMinimumTokenOutAmount,
|
|
3289
|
+
finalPlan.minimumTokenOutAmount,
|
|
3290
|
+
finalPlan.tokenOutAmount
|
|
3291
|
+
);
|
|
3292
|
+
|
|
3293
|
+
const allowanceReadUncertain =
|
|
3294
|
+
approvalExecution.performed && finalPlan.allowanceReadError !== null;
|
|
3295
|
+
|
|
3296
|
+
if (finalPlan.approval.required && !allowanceReadUncertain) {
|
|
3297
|
+
throw createTaggedError(
|
|
3298
|
+
"Swap still requires token approval after the approval step completed.",
|
|
3299
|
+
"swap_approval_required",
|
|
3300
|
+
{
|
|
3301
|
+
spender: finalPlan.spender,
|
|
3302
|
+
requiredAllowance: finalPlan.tokenInAmount.toString(),
|
|
3303
|
+
currentAllowance: finalPlan.currentAllowance.toString(),
|
|
3304
|
+
}
|
|
3305
|
+
);
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
const effectiveSimulation = allowanceReadUncertain
|
|
3309
|
+
? await this.#simulatePreparedTransaction({
|
|
3310
|
+
runtimeConfig,
|
|
3311
|
+
from: address,
|
|
3312
|
+
tx: finalPlan.swapTx,
|
|
3313
|
+
})
|
|
3314
|
+
: finalPlan.simulation;
|
|
3315
|
+
this.#assertSimulationSucceeded(effectiveSimulation);
|
|
3316
|
+
const { hash } = await account.sendTransaction(finalPlan.swapTx);
|
|
3317
|
+
const totalFee = approvalExecution.totalFee + finalPlan.swapFee;
|
|
3318
|
+
const result = {
|
|
3319
|
+
hash,
|
|
3320
|
+
fee: totalFee.toString(),
|
|
3321
|
+
swapFee: finalPlan.swapFee.toString(),
|
|
3322
|
+
approvalFee: approvalExecution.totalFee.toString(),
|
|
3323
|
+
tokenInAmount: finalPlan.tokenInAmount.toString(),
|
|
3324
|
+
tokenOutAmount: finalPlan.tokenOutAmount.toString(),
|
|
3325
|
+
...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
|
|
3326
|
+
...(approvalExecution.resetAllowanceHash
|
|
3327
|
+
? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
|
|
3328
|
+
: {}),
|
|
3329
|
+
};
|
|
3330
|
+
return {
|
|
3331
|
+
network: runtimeConfig.network,
|
|
3332
|
+
chainId: runtimeConfig.chainId,
|
|
3333
|
+
accountIndex,
|
|
3334
|
+
address,
|
|
3335
|
+
protocol: "velora",
|
|
3336
|
+
executionSupported: true,
|
|
3337
|
+
swapRequest,
|
|
3338
|
+
tokenInMetadata,
|
|
3339
|
+
tokenOutMetadata,
|
|
3340
|
+
inputAmountFormatted: formatUnits(swapRequest.tokenInAmount, tokenInMetadata.decimals),
|
|
3341
|
+
outputAmountFormatted: formatUnits(finalPlan.tokenOutAmount, tokenOutMetadata.decimals),
|
|
3342
|
+
quoteFingerprint: finalPlan.quoteFingerprint,
|
|
3343
|
+
estimatedFeeWei: totalFee.toString(),
|
|
3344
|
+
estimatedSwapFeeWei: finalPlan.swapFee.toString(),
|
|
3345
|
+
estimatedApprovalFeeWei: approvalExecution.totalFee.toString(),
|
|
3346
|
+
feeEstimateAvailable: true,
|
|
3347
|
+
feeEstimateError: null,
|
|
3348
|
+
slippageBps: finalPlan.slippageBps,
|
|
3349
|
+
minimumOutputAmountRaw: finalPlan.minimumTokenOutAmount.toString(),
|
|
3350
|
+
allowance: {
|
|
3351
|
+
spender: finalPlan.spender,
|
|
3352
|
+
currentAllowance: finalPlan.currentAllowance.toString(),
|
|
3353
|
+
requiredAllowance: finalPlan.tokenInAmount.toString(),
|
|
3354
|
+
approvalRequired: finalPlan.approval.required,
|
|
3355
|
+
approvalSequence: finalPlan.approval.steps,
|
|
3356
|
+
readError: finalPlan.allowanceReadError,
|
|
3357
|
+
},
|
|
3358
|
+
router: finalPlan.router,
|
|
3359
|
+
simulation: effectiveSimulation,
|
|
3360
|
+
swapTransaction: finalPlan.swapTransaction,
|
|
3361
|
+
result,
|
|
3362
|
+
source: "wdk-protocol-swap-velora-evm",
|
|
3363
|
+
};
|
|
3364
|
+
} catch (error) {
|
|
3365
|
+
const cleanup = await this.#restoreAllowanceAfterFailedSwap({
|
|
3366
|
+
account,
|
|
3367
|
+
runtimeConfig,
|
|
3368
|
+
tokenAddress: swapRequest.tokenIn,
|
|
3369
|
+
spender: initialPlan.spender,
|
|
3370
|
+
originalAllowance: initialPlan.currentAllowance,
|
|
3371
|
+
approvalExecution,
|
|
3372
|
+
});
|
|
3373
|
+
this.#throwSwapFailureWithCleanup(error, cleanup);
|
|
3374
|
+
}
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
async sendLifiSwap({
|
|
3379
|
+
seedPhrase,
|
|
3380
|
+
tokenIn,
|
|
3381
|
+
destinationChain,
|
|
3382
|
+
outputToken,
|
|
3383
|
+
destinationAddress,
|
|
3384
|
+
tokenInAmount,
|
|
3385
|
+
slippage = DEFAULT_LIFI_SLIPPAGE,
|
|
3386
|
+
allowBridges = null,
|
|
3387
|
+
denyBridges = null,
|
|
3388
|
+
preferBridges = null,
|
|
3389
|
+
accountIndex = 0,
|
|
3390
|
+
network,
|
|
3391
|
+
minimumTokenOutAmount = null,
|
|
3392
|
+
}) {
|
|
3393
|
+
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
3394
|
+
assertLifiSupportedNetwork(runtimeConfig.network);
|
|
3395
|
+
const swapRequest = buildLifiEvmSwapRequest({
|
|
3396
|
+
tokenIn,
|
|
3397
|
+
destinationChain,
|
|
3398
|
+
outputToken,
|
|
3399
|
+
destinationAddress,
|
|
3400
|
+
tokenInAmount,
|
|
3401
|
+
slippage,
|
|
3402
|
+
allowBridges,
|
|
3403
|
+
denyBridges,
|
|
3404
|
+
preferBridges,
|
|
3405
|
+
});
|
|
3406
|
+
const requestedMinimumTokenOutAmount =
|
|
3407
|
+
minimumTokenOutAmount !== null && minimumTokenOutAmount !== undefined
|
|
3408
|
+
? assertPositiveBigIntString(minimumTokenOutAmount, "minimumTokenOutAmount")
|
|
3409
|
+
: null;
|
|
3410
|
+
const sourceAddress = await account.getAddress();
|
|
3411
|
+
let initialPlan = await this.#buildLifiEvmSwapPlan({
|
|
3412
|
+
account,
|
|
3413
|
+
runtimeConfig,
|
|
3414
|
+
address: sourceAddress,
|
|
3415
|
+
swapRequest,
|
|
3416
|
+
});
|
|
3417
|
+
this.#assertMinimumSwapOutput(
|
|
3418
|
+
requestedMinimumTokenOutAmount,
|
|
3419
|
+
initialPlan.minimumTokenOutAmount,
|
|
3420
|
+
initialPlan.tokenOutAmount
|
|
3421
|
+
);
|
|
3422
|
+
|
|
3423
|
+
const approvalExecution = await this.#executeSwapApprovalsIfNeeded({
|
|
3424
|
+
account,
|
|
3425
|
+
runtimeConfig,
|
|
3426
|
+
swapRequest: {
|
|
3427
|
+
tokenIn: swapRequest.tokenIn,
|
|
3428
|
+
},
|
|
3429
|
+
plan: initialPlan,
|
|
3430
|
+
});
|
|
3431
|
+
|
|
3432
|
+
let finalPlan = initialPlan;
|
|
3433
|
+
try {
|
|
3434
|
+
if (approvalExecution.performed) {
|
|
3435
|
+
finalPlan = await this.#buildLifiEvmSwapPlan({
|
|
3436
|
+
account,
|
|
3437
|
+
runtimeConfig,
|
|
3438
|
+
address: sourceAddress,
|
|
3439
|
+
swapRequest,
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
this.#assertMinimumSwapOutput(
|
|
3443
|
+
requestedMinimumTokenOutAmount,
|
|
3444
|
+
finalPlan.minimumTokenOutAmount,
|
|
3445
|
+
finalPlan.tokenOutAmount
|
|
3446
|
+
);
|
|
3447
|
+
|
|
3448
|
+
const allowanceReadUncertain =
|
|
3449
|
+
approvalExecution.performed && finalPlan.allowanceReadError !== null;
|
|
3450
|
+
|
|
3451
|
+
if (finalPlan.approval.required && !allowanceReadUncertain) {
|
|
3452
|
+
throw createTaggedError(
|
|
3453
|
+
"LI.FI cross-chain swap still requires token approval after the approval step completed.",
|
|
3454
|
+
"swap_approval_required",
|
|
3455
|
+
{
|
|
3456
|
+
spender: finalPlan.spender,
|
|
3457
|
+
requiredAllowance: finalPlan.tokenInAmount.toString(),
|
|
3458
|
+
currentAllowance: finalPlan.currentAllowance.toString(),
|
|
3459
|
+
}
|
|
3460
|
+
);
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
const effectiveSimulation = allowanceReadUncertain
|
|
3464
|
+
? await this.#simulatePreparedTransaction({
|
|
3465
|
+
runtimeConfig,
|
|
3466
|
+
from: sourceAddress,
|
|
3467
|
+
tx: finalPlan.swapTx,
|
|
3468
|
+
})
|
|
3469
|
+
: finalPlan.simulation;
|
|
3470
|
+
this.#assertSimulationSucceeded(effectiveSimulation);
|
|
3471
|
+
|
|
3472
|
+
const { hash } = await account.sendTransaction(finalPlan.swapTx);
|
|
3473
|
+
const totalFee = approvalExecution.totalFee + finalPlan.swapFee;
|
|
3474
|
+
const result = {
|
|
3475
|
+
hash,
|
|
3476
|
+
fee: totalFee.toString(),
|
|
3477
|
+
swapFee: finalPlan.swapFee.toString(),
|
|
3478
|
+
approvalFee: approvalExecution.totalFee.toString(),
|
|
3479
|
+
tokenInAmount: finalPlan.tokenInAmount.toString(),
|
|
3480
|
+
tokenOutAmount: finalPlan.tokenOutAmount.toString(),
|
|
3481
|
+
...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
|
|
3482
|
+
...(approvalExecution.resetAllowanceHash
|
|
3483
|
+
? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
|
|
3484
|
+
: {}),
|
|
3485
|
+
};
|
|
3486
|
+
return {
|
|
3487
|
+
...this.#formatLifiSwapResponse({
|
|
3488
|
+
runtimeConfig,
|
|
3489
|
+
accountIndex,
|
|
3490
|
+
address: sourceAddress,
|
|
3491
|
+
swapRequest,
|
|
3492
|
+
plan: {
|
|
3493
|
+
...finalPlan,
|
|
3494
|
+
simulation: effectiveSimulation,
|
|
3495
|
+
swapFee: totalFee,
|
|
3496
|
+
totalEstimatedFee: totalFee,
|
|
3497
|
+
approval: {
|
|
3498
|
+
...finalPlan.approval,
|
|
3499
|
+
estimatedFee: approvalExecution.totalFee,
|
|
3500
|
+
},
|
|
3501
|
+
},
|
|
3502
|
+
}),
|
|
3503
|
+
result,
|
|
3504
|
+
};
|
|
3505
|
+
} catch (error) {
|
|
3506
|
+
const cleanup = await this.#restoreAllowanceAfterFailedSwap({
|
|
3507
|
+
account,
|
|
3508
|
+
runtimeConfig,
|
|
3509
|
+
tokenAddress: swapRequest.tokenIn,
|
|
3510
|
+
spender: initialPlan.spender,
|
|
3511
|
+
originalAllowance: initialPlan.currentAllowance,
|
|
3512
|
+
approvalExecution,
|
|
3513
|
+
});
|
|
3514
|
+
this.#throwSwapFailureWithCleanup(error, cleanup);
|
|
3515
|
+
}
|
|
3516
|
+
});
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
async quoteUniswapSwap({
|
|
3520
|
+
seedPhrase,
|
|
3521
|
+
address,
|
|
3522
|
+
tokenIn,
|
|
3523
|
+
tokenOut,
|
|
3524
|
+
tokenInAmount,
|
|
3525
|
+
slippageBps,
|
|
3526
|
+
accountIndex = 0,
|
|
3527
|
+
network,
|
|
3528
|
+
}) {
|
|
3529
|
+
return this.#withReadableAccount(
|
|
3530
|
+
{ seedPhrase, address, accountIndex, network },
|
|
3531
|
+
async (account, runtimeConfig) => {
|
|
3532
|
+
assertUniswapSupportedNetwork(runtimeConfig.network);
|
|
3533
|
+
const swapRequest = buildUniswapSwapRequest({
|
|
3534
|
+
tokenIn,
|
|
3535
|
+
tokenOut,
|
|
3536
|
+
tokenInAmount,
|
|
3537
|
+
slippageBps:
|
|
3538
|
+
slippageBps === undefined || slippageBps === null
|
|
3539
|
+
? this.config.uniswapDefaultSlippageBps
|
|
3540
|
+
: slippageBps,
|
|
3541
|
+
});
|
|
3542
|
+
const swapperAddress = await account.getAddress();
|
|
3543
|
+
const plan = await this.#buildUniswapSwapPlan({
|
|
3544
|
+
account,
|
|
3545
|
+
runtimeConfig,
|
|
3546
|
+
address: swapperAddress,
|
|
3547
|
+
swapRequest,
|
|
3548
|
+
});
|
|
3549
|
+
return this.#formatUniswapSwapResponse({
|
|
3550
|
+
runtimeConfig,
|
|
3551
|
+
accountIndex,
|
|
3552
|
+
address: swapperAddress,
|
|
3553
|
+
swapRequest,
|
|
3554
|
+
plan,
|
|
3555
|
+
});
|
|
3556
|
+
}
|
|
3557
|
+
);
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
async sendUniswapSwap({
|
|
3561
|
+
seedPhrase,
|
|
3562
|
+
tokenIn,
|
|
3563
|
+
tokenOut,
|
|
3564
|
+
tokenInAmount,
|
|
3565
|
+
slippageBps,
|
|
3566
|
+
accountIndex = 0,
|
|
3567
|
+
network,
|
|
3568
|
+
expectedQuoteFingerprint = null,
|
|
3569
|
+
minimumTokenOutAmount = null,
|
|
3570
|
+
}) {
|
|
3571
|
+
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
3572
|
+
assertUniswapSupportedNetwork(runtimeConfig.network);
|
|
3573
|
+
const swapRequest = buildUniswapSwapRequest({
|
|
3574
|
+
tokenIn,
|
|
3575
|
+
tokenOut,
|
|
3576
|
+
tokenInAmount,
|
|
3577
|
+
slippageBps:
|
|
3578
|
+
slippageBps === undefined || slippageBps === null
|
|
3579
|
+
? this.config.uniswapDefaultSlippageBps
|
|
3580
|
+
: slippageBps,
|
|
3581
|
+
});
|
|
3582
|
+
const normalizedExpectedQuoteFingerprint =
|
|
3583
|
+
typeof expectedQuoteFingerprint === "string" && expectedQuoteFingerprint.trim()
|
|
3584
|
+
? expectedQuoteFingerprint.trim()
|
|
3585
|
+
: null;
|
|
3586
|
+
const requestedMinimumTokenOutAmount =
|
|
3587
|
+
minimumTokenOutAmount !== null && minimumTokenOutAmount !== undefined
|
|
3588
|
+
? assertPositiveBigIntString(minimumTokenOutAmount, "minimumTokenOutAmount")
|
|
3589
|
+
: null;
|
|
3590
|
+
const address = await account.getAddress();
|
|
3591
|
+
let initialPlan = await this.#buildUniswapSwapPlan({
|
|
3592
|
+
account,
|
|
3593
|
+
runtimeConfig,
|
|
3594
|
+
address,
|
|
3595
|
+
swapRequest,
|
|
3596
|
+
});
|
|
3597
|
+
this.#assertExpectedSwapFingerprint(
|
|
3598
|
+
normalizedExpectedQuoteFingerprint,
|
|
3599
|
+
initialPlan.quoteFingerprint
|
|
3600
|
+
);
|
|
3601
|
+
this.#assertMinimumSwapOutput(
|
|
3602
|
+
requestedMinimumTokenOutAmount,
|
|
3603
|
+
initialPlan.minimumTokenOutAmount,
|
|
3604
|
+
initialPlan.tokenOutAmount
|
|
3605
|
+
);
|
|
3606
|
+
|
|
3607
|
+
const approvalExecution = await this.#executeSwapApprovalsIfNeeded({
|
|
3608
|
+
account,
|
|
3609
|
+
runtimeConfig,
|
|
3610
|
+
swapRequest: { tokenIn: swapRequest.tokenIn },
|
|
3611
|
+
plan: initialPlan,
|
|
3612
|
+
});
|
|
3613
|
+
|
|
3614
|
+
let finalPlan = initialPlan;
|
|
3615
|
+
try {
|
|
3616
|
+
if (approvalExecution.performed) {
|
|
3617
|
+
// Re-fetch after approval to obtain fresh permitData (the deadline/nonce are short-lived).
|
|
3618
|
+
finalPlan = await this.#buildUniswapSwapPlan({
|
|
3619
|
+
account,
|
|
3620
|
+
runtimeConfig,
|
|
3621
|
+
address,
|
|
3622
|
+
swapRequest,
|
|
3623
|
+
});
|
|
3624
|
+
this.#assertExpectedSwapFingerprint(
|
|
3625
|
+
normalizedExpectedQuoteFingerprint,
|
|
3626
|
+
finalPlan.quoteFingerprint
|
|
3627
|
+
);
|
|
3628
|
+
}
|
|
3629
|
+
this.#assertMinimumSwapOutput(
|
|
3630
|
+
requestedMinimumTokenOutAmount,
|
|
3631
|
+
finalPlan.minimumTokenOutAmount,
|
|
3632
|
+
finalPlan.tokenOutAmount
|
|
3633
|
+
);
|
|
3634
|
+
|
|
3635
|
+
const allowanceReadUncertain =
|
|
3636
|
+
approvalExecution.performed && finalPlan.allowanceReadError !== null;
|
|
3637
|
+
if (finalPlan.approval.required && !allowanceReadUncertain) {
|
|
3638
|
+
throw createTaggedError(
|
|
3639
|
+
"Uniswap swap still requires token approval after the approval step completed.",
|
|
3640
|
+
"swap_approval_required",
|
|
3641
|
+
{
|
|
3642
|
+
spender: finalPlan.spender,
|
|
3643
|
+
requiredAllowance: finalPlan.tokenInAmount.toString(),
|
|
3644
|
+
currentAllowance: finalPlan.currentAllowance.toString(),
|
|
3645
|
+
}
|
|
3646
|
+
);
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
const signature =
|
|
3650
|
+
finalPlan.isNativeTokenIn || !finalPlan.permitData
|
|
3651
|
+
? null
|
|
3652
|
+
: await this.#signUniswapPermit(account, finalPlan.permitData, runtimeConfig);
|
|
3653
|
+
|
|
3654
|
+
const swapTx = await this.#fetchUniswapSwapCalldata({
|
|
3655
|
+
runtimeConfig,
|
|
3656
|
+
quoteResponse: finalPlan.quoteResponse,
|
|
3657
|
+
permitData: finalPlan.permitData,
|
|
3658
|
+
signature,
|
|
3659
|
+
});
|
|
3660
|
+
|
|
3661
|
+
const simulation = await this.#simulatePreparedTransaction({
|
|
3662
|
+
runtimeConfig,
|
|
3663
|
+
from: address,
|
|
3664
|
+
tx: swapTx,
|
|
3665
|
+
});
|
|
3666
|
+
this.#assertSimulationSucceeded(simulation);
|
|
3667
|
+
|
|
3668
|
+
const { hash } = await account.sendTransaction(swapTx);
|
|
3669
|
+
const result = {
|
|
3670
|
+
hash,
|
|
3671
|
+
approvalFee: approvalExecution.totalFee.toString(),
|
|
3672
|
+
tokenInAmount: finalPlan.tokenInAmount.toString(),
|
|
3673
|
+
tokenOutAmount: finalPlan.tokenOutAmount.toString(),
|
|
3674
|
+
...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
|
|
3675
|
+
...(approvalExecution.resetAllowanceHash
|
|
3676
|
+
? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
|
|
3677
|
+
: {}),
|
|
3678
|
+
};
|
|
3679
|
+
return {
|
|
3680
|
+
...(await this.#formatUniswapSwapResponse({
|
|
3681
|
+
runtimeConfig,
|
|
3682
|
+
accountIndex,
|
|
3683
|
+
address,
|
|
3684
|
+
swapRequest,
|
|
3685
|
+
plan: {
|
|
3686
|
+
...finalPlan,
|
|
3687
|
+
approval: {
|
|
3688
|
+
...finalPlan.approval,
|
|
3689
|
+
estimatedFee: approvalExecution.totalFee,
|
|
3690
|
+
},
|
|
3691
|
+
},
|
|
3692
|
+
})),
|
|
3693
|
+
simulation,
|
|
3694
|
+
swapTransaction: { to: swapTx.to, value: swapTx.value.toString(), dataHash: sha256Hex(swapTx.data) },
|
|
3695
|
+
result,
|
|
3696
|
+
};
|
|
3697
|
+
} catch (error) {
|
|
3698
|
+
const cleanup = await this.#restoreAllowanceAfterFailedSwap({
|
|
3699
|
+
account,
|
|
3700
|
+
runtimeConfig,
|
|
3701
|
+
tokenAddress: swapRequest.tokenIn,
|
|
3702
|
+
spender: initialPlan.spender,
|
|
3703
|
+
originalAllowance: initialPlan.currentAllowance,
|
|
3704
|
+
approvalExecution,
|
|
3705
|
+
});
|
|
3706
|
+
this.#throwSwapFailureWithCleanup(error, cleanup);
|
|
3707
|
+
}
|
|
3708
|
+
});
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
async #getUniswapTokenMetadata(runtimeConfig, tokenAddress) {
|
|
3712
|
+
if (isZeroAddress(tokenAddress)) {
|
|
3713
|
+
return {
|
|
3714
|
+
address: ZERO_ADDRESS,
|
|
3715
|
+
name: runtimeConfig.nativeSymbol === "ETH" ? "Ether" : runtimeConfig.nativeSymbol,
|
|
3716
|
+
symbol: runtimeConfig.nativeSymbol,
|
|
3717
|
+
decimals: 18,
|
|
3718
|
+
verified: true,
|
|
3719
|
+
source: "native-asset",
|
|
3720
|
+
};
|
|
3721
|
+
}
|
|
3722
|
+
return this.#getTokenMetadata(runtimeConfig, tokenAddress);
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
async #formatUniswapSwapResponse({ runtimeConfig, accountIndex, address, swapRequest, plan }) {
|
|
3726
|
+
const [tokenInMetadata, tokenOutMetadata] = await Promise.all([
|
|
3727
|
+
this.#getUniswapTokenMetadata(runtimeConfig, swapRequest.tokenIn),
|
|
3728
|
+
this.#getUniswapTokenMetadata(runtimeConfig, swapRequest.tokenOut),
|
|
3729
|
+
]);
|
|
3730
|
+
return {
|
|
3731
|
+
network: runtimeConfig.network,
|
|
3732
|
+
chainId: runtimeConfig.chainId,
|
|
3733
|
+
accountIndex,
|
|
3734
|
+
address,
|
|
3735
|
+
protocol: "uniswap",
|
|
3736
|
+
executionSupported: true,
|
|
3737
|
+
routing: "CLASSIC",
|
|
3738
|
+
swapRequest: {
|
|
3739
|
+
tokenIn: swapRequest.tokenIn,
|
|
3740
|
+
tokenOut: swapRequest.tokenOut,
|
|
3741
|
+
tokenInAmount: swapRequest.tokenInAmount.toString(),
|
|
3742
|
+
},
|
|
3743
|
+
tokenInMetadata,
|
|
3744
|
+
tokenOutMetadata,
|
|
3745
|
+
inputAmountFormatted: formatUnits(swapRequest.tokenInAmount, tokenInMetadata.decimals),
|
|
3746
|
+
outputAmountFormatted: formatUnits(plan.tokenOutAmount, tokenOutMetadata.decimals),
|
|
3747
|
+
quoteFingerprint: plan.quoteFingerprint,
|
|
3748
|
+
slippageBps: plan.slippageBps,
|
|
3749
|
+
minimumOutputAmountRaw: plan.minimumTokenOutAmount.toString(),
|
|
3750
|
+
permitRequired: plan.permitData !== null,
|
|
3751
|
+
gasFeeWei: plan.gasFee,
|
|
3752
|
+
gasFeeUSD: plan.gasFeeUSD,
|
|
3753
|
+
estimatedApprovalFeeWei: plan.approval.estimatedFee.toString(),
|
|
3754
|
+
allowance: {
|
|
3755
|
+
spender: plan.spender,
|
|
3756
|
+
currentAllowance: plan.currentAllowance.toString(),
|
|
3757
|
+
requiredAllowance: plan.tokenInAmount.toString(),
|
|
3758
|
+
approvalRequired: plan.approval.required,
|
|
3759
|
+
approvalSequence: plan.approval.steps,
|
|
3760
|
+
readError: plan.allowanceReadError,
|
|
3761
|
+
},
|
|
3762
|
+
router: plan.router,
|
|
3763
|
+
source: "uniswap-trading-api",
|
|
3764
|
+
};
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
async quoteNativeTransfer({ seedPhrase, to, value, accountIndex = 0, network }) {
|
|
3768
|
+
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
3769
|
+
const tx = {
|
|
3770
|
+
to: normalizeAddress(to, "to"),
|
|
3771
|
+
value: assertPositiveBigIntString(value, "value"),
|
|
3772
|
+
};
|
|
3773
|
+
const quote = await account.quoteSendTransaction(tx);
|
|
3774
|
+
return {
|
|
3775
|
+
network: runtimeConfig.network,
|
|
3776
|
+
chainId: runtimeConfig.chainId,
|
|
3777
|
+
accountIndex,
|
|
3778
|
+
transaction: tx,
|
|
3779
|
+
quote,
|
|
3780
|
+
source: "wdk-wallet-evm",
|
|
3781
|
+
};
|
|
3782
|
+
});
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3785
|
+
async sendNativeTransfer({ seedPhrase, to, value, accountIndex = 0, network }) {
|
|
3786
|
+
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
3787
|
+
const tx = {
|
|
3788
|
+
to: normalizeAddress(to, "to"),
|
|
3789
|
+
value: assertPositiveBigIntString(value, "value"),
|
|
3790
|
+
};
|
|
3791
|
+
const result = await account.sendTransaction(tx);
|
|
3792
|
+
return {
|
|
3793
|
+
network: runtimeConfig.network,
|
|
3794
|
+
chainId: runtimeConfig.chainId,
|
|
3795
|
+
accountIndex,
|
|
3796
|
+
transaction: tx,
|
|
3797
|
+
result,
|
|
3798
|
+
source: "wdk-wallet-evm",
|
|
3799
|
+
};
|
|
3800
|
+
});
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
async quoteTokenTransfer({
|
|
3804
|
+
seedPhrase,
|
|
3805
|
+
tokenAddress,
|
|
3806
|
+
recipient,
|
|
3807
|
+
amount,
|
|
3808
|
+
accountIndex = 0,
|
|
3809
|
+
network,
|
|
3810
|
+
}) {
|
|
3811
|
+
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
3812
|
+
const transfer = {
|
|
3813
|
+
token: normalizeAddress(tokenAddress, "tokenAddress"),
|
|
3814
|
+
recipient: normalizeAddress(recipient, "recipient"),
|
|
3815
|
+
amount: assertPositiveBigIntString(amount, "amount"),
|
|
3816
|
+
};
|
|
3817
|
+
const ownerAddress = await account.getAddress();
|
|
3818
|
+
const { tokenMetadata } = await this.#prepareTokenTransferContext({
|
|
3819
|
+
account,
|
|
3820
|
+
runtimeConfig,
|
|
3821
|
+
transfer,
|
|
3822
|
+
ownerAddress,
|
|
3823
|
+
});
|
|
3824
|
+
let quote;
|
|
3825
|
+
try {
|
|
3826
|
+
quote = await account.quoteTransfer(transfer);
|
|
3827
|
+
} catch (error) {
|
|
3828
|
+
if (isRecoverableTokenTransferSimulationFailure(error)) {
|
|
3829
|
+
throw createTaggedError(
|
|
3830
|
+
"Token transfer could not be simulated by the token contract.",
|
|
3831
|
+
"token_transfer_failed",
|
|
3832
|
+
{
|
|
3833
|
+
network: runtimeConfig.network,
|
|
3834
|
+
tokenAddress: transfer.token,
|
|
3835
|
+
ownerAddress,
|
|
3836
|
+
recipient: transfer.recipient,
|
|
3837
|
+
amount: transfer.amount.toString(),
|
|
3838
|
+
underlying:
|
|
3839
|
+
error instanceof Error
|
|
3840
|
+
? {
|
|
3841
|
+
message: error.message,
|
|
3842
|
+
code: String(error.errorCode || error.code || "").trim() || null,
|
|
3843
|
+
}
|
|
3844
|
+
: {
|
|
3845
|
+
message: String(error),
|
|
3846
|
+
code: null,
|
|
3847
|
+
},
|
|
3848
|
+
}
|
|
3849
|
+
);
|
|
3850
|
+
}
|
|
3851
|
+
throw error;
|
|
3852
|
+
}
|
|
3853
|
+
return {
|
|
3854
|
+
network: runtimeConfig.network,
|
|
2671
3855
|
chainId: runtimeConfig.chainId,
|
|
2672
3856
|
accountIndex,
|
|
2673
3857
|
transfer,
|
|
@@ -2725,113 +3909,714 @@ export class WdkEvmWalletService {
|
|
|
2725
3909
|
code: null,
|
|
2726
3910
|
},
|
|
2727
3911
|
}
|
|
2728
|
-
);
|
|
2729
|
-
}
|
|
2730
|
-
throw error;
|
|
2731
|
-
}
|
|
3912
|
+
);
|
|
3913
|
+
}
|
|
3914
|
+
throw error;
|
|
3915
|
+
}
|
|
3916
|
+
return {
|
|
3917
|
+
network: runtimeConfig.network,
|
|
3918
|
+
chainId: runtimeConfig.chainId,
|
|
3919
|
+
accountIndex,
|
|
3920
|
+
transfer,
|
|
3921
|
+
tokenMetadata,
|
|
3922
|
+
amountFormatted: formatUnits(transfer.amount, tokenMetadata.decimals),
|
|
3923
|
+
result,
|
|
3924
|
+
source: "wdk-wallet-evm",
|
|
3925
|
+
};
|
|
3926
|
+
});
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
async signX402ExactTypedData({
|
|
3930
|
+
seedPhrase,
|
|
3931
|
+
accountIndex = 0,
|
|
3932
|
+
network,
|
|
3933
|
+
domain,
|
|
3934
|
+
types,
|
|
3935
|
+
primaryType,
|
|
3936
|
+
message,
|
|
3937
|
+
}) {
|
|
3938
|
+
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
3939
|
+
const typedData = normalizeX402ExactTypedData(
|
|
3940
|
+
{ domain, types, primaryType, message },
|
|
3941
|
+
runtimeConfig
|
|
3942
|
+
);
|
|
3943
|
+
const signerAddress = normalizeAddress(await account.getAddress(), "accountAddress");
|
|
3944
|
+
if (typedData.message.from.toLowerCase() !== signerAddress.toLowerCase()) {
|
|
3945
|
+
throw new Error("message.from must match the active wallet account address.");
|
|
3946
|
+
}
|
|
3947
|
+
const signature = await account.signTypedData({
|
|
3948
|
+
domain: typedData.domain,
|
|
3949
|
+
types: typedData.types,
|
|
3950
|
+
message: typedData.message,
|
|
3951
|
+
});
|
|
3952
|
+
return {
|
|
3953
|
+
network: runtimeConfig.network,
|
|
3954
|
+
chainId: runtimeConfig.chainId,
|
|
3955
|
+
accountIndex,
|
|
3956
|
+
address: signerAddress,
|
|
3957
|
+
primaryType: typedData.primaryType,
|
|
3958
|
+
signature,
|
|
3959
|
+
source: "wdk-wallet-evm",
|
|
3960
|
+
};
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
#resolveRuntimeConfig(networkOverride) {
|
|
3965
|
+
const network = assertValidNetwork(networkOverride) || this.config.network;
|
|
3966
|
+
const profile = this.config.networkProfiles?.[network];
|
|
3967
|
+
if (!profile) {
|
|
3968
|
+
throw new Error(`Missing RPC profile for network: ${network}`);
|
|
3969
|
+
}
|
|
3970
|
+
return {
|
|
3971
|
+
...this.config,
|
|
3972
|
+
network,
|
|
3973
|
+
chainId: profile.chainId,
|
|
3974
|
+
providerUrl: profile.providerUrl,
|
|
3975
|
+
nativeSymbol: profile.nativeSymbol,
|
|
3976
|
+
};
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
async #withWallet({ seedPhrase, network }, callback) {
|
|
3980
|
+
const mnemonic = assertValidSeedPhrase(seedPhrase);
|
|
3981
|
+
const runtimeConfig = this.#resolveRuntimeConfig(network);
|
|
3982
|
+
const options = {
|
|
3983
|
+
provider: runtimeConfig.providerUrl,
|
|
3984
|
+
chainId: runtimeConfig.chainId,
|
|
3985
|
+
};
|
|
3986
|
+
if (runtimeConfig.transferMaxFeeWei !== null) {
|
|
3987
|
+
options.transferMaxFee = runtimeConfig.transferMaxFeeWei;
|
|
3988
|
+
}
|
|
3989
|
+
const wallet = new WalletManagerEvm(mnemonic, options);
|
|
3990
|
+
try {
|
|
3991
|
+
return await callback(wallet, runtimeConfig);
|
|
3992
|
+
} finally {
|
|
3993
|
+
await maybeDispose(wallet);
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
async #withAccount({ seedPhrase, accountIndex, network }, callback) {
|
|
3998
|
+
return this.#withWallet({ seedPhrase, network }, async (wallet, runtimeConfig) => {
|
|
3999
|
+
const account = await wallet.getAccount(assertNonNegativeInteger(accountIndex, "accountIndex"));
|
|
4000
|
+
return await callback(account, runtimeConfig);
|
|
4001
|
+
});
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
async #withReadableAccount({ seedPhrase, address, accountIndex, network }, callback) {
|
|
4005
|
+
const normalizedAddress = String(address || "").trim();
|
|
4006
|
+
if (normalizedAddress) {
|
|
4007
|
+
const runtimeConfig = this.#resolveRuntimeConfig(network);
|
|
4008
|
+
const account = new WalletAccountReadOnlyEvm(
|
|
4009
|
+
normalizeAddress(normalizedAddress, "address"),
|
|
4010
|
+
{ provider: runtimeConfig.providerUrl }
|
|
4011
|
+
);
|
|
4012
|
+
try {
|
|
4013
|
+
return await callback(account, runtimeConfig);
|
|
4014
|
+
} finally {
|
|
4015
|
+
await maybeDispose(account);
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
return this.#withAccount({ seedPhrase, accountIndex, network }, callback);
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
async #morphoGraphqlRequest({ query, variables = {}, operationName = null }) {
|
|
4022
|
+
const baseUrl = String(this.config.morphoApiBaseUrl || "https://api.morpho.org/graphql").trim();
|
|
4023
|
+
if (!baseUrl) {
|
|
4024
|
+
throw createTaggedError("Morpho API base URL is not configured.", "morpho_api_failed");
|
|
4025
|
+
}
|
|
4026
|
+
let response;
|
|
4027
|
+
try {
|
|
4028
|
+
response = await fetch(baseUrl, {
|
|
4029
|
+
method: "POST",
|
|
4030
|
+
headers: {
|
|
4031
|
+
accept: "application/json",
|
|
4032
|
+
"content-type": "application/json",
|
|
4033
|
+
},
|
|
4034
|
+
body: JSON.stringify({
|
|
4035
|
+
query,
|
|
4036
|
+
variables,
|
|
4037
|
+
...(operationName ? { operationName } : {}),
|
|
4038
|
+
}),
|
|
4039
|
+
});
|
|
4040
|
+
} catch (error) {
|
|
4041
|
+
throw createTaggedError(
|
|
4042
|
+
`Morpho API request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
4043
|
+
"network_unavailable",
|
|
4044
|
+
{
|
|
4045
|
+
provider: "morpho",
|
|
4046
|
+
endpoint: baseUrl,
|
|
4047
|
+
operationName,
|
|
4048
|
+
}
|
|
4049
|
+
);
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
let payload;
|
|
4053
|
+
try {
|
|
4054
|
+
payload = await response.json();
|
|
4055
|
+
} catch (error) {
|
|
4056
|
+
throw createTaggedError(
|
|
4057
|
+
`Morpho API returned invalid JSON (HTTP ${response.status}).`,
|
|
4058
|
+
"morpho_api_failed",
|
|
4059
|
+
{
|
|
4060
|
+
provider: "morpho",
|
|
4061
|
+
endpoint: baseUrl,
|
|
4062
|
+
status: response.status,
|
|
4063
|
+
operationName,
|
|
4064
|
+
}
|
|
4065
|
+
);
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
if (!response.ok) {
|
|
4069
|
+
const message =
|
|
4070
|
+
Array.isArray(payload?.errors) && payload.errors.length > 0
|
|
4071
|
+
? String(payload.errors[0]?.message || "").trim()
|
|
4072
|
+
: "";
|
|
4073
|
+
throw createTaggedError(
|
|
4074
|
+
message || `Morpho API request failed with HTTP ${response.status}.`,
|
|
4075
|
+
"morpho_api_failed",
|
|
4076
|
+
{
|
|
4077
|
+
provider: "morpho",
|
|
4078
|
+
endpoint: baseUrl,
|
|
4079
|
+
status: response.status,
|
|
4080
|
+
operationName,
|
|
4081
|
+
errors: Array.isArray(payload?.errors) ? payload.errors : [],
|
|
4082
|
+
}
|
|
4083
|
+
);
|
|
4084
|
+
}
|
|
4085
|
+
|
|
4086
|
+
if (Array.isArray(payload?.errors) && payload.errors.length > 0) {
|
|
4087
|
+
const allNotFound = payload.errors.every(
|
|
4088
|
+
(entry) =>
|
|
4089
|
+
String(entry?.status || entry?.extensions?.code || "").toUpperCase() === "NOT_FOUND"
|
|
4090
|
+
);
|
|
4091
|
+
if (!allNotFound) {
|
|
4092
|
+
throw createTaggedError(
|
|
4093
|
+
String(payload.errors[0]?.message || "Morpho API query failed."),
|
|
4094
|
+
"morpho_api_failed",
|
|
4095
|
+
{
|
|
4096
|
+
provider: "morpho",
|
|
4097
|
+
endpoint: baseUrl,
|
|
4098
|
+
status: response.status,
|
|
4099
|
+
operationName,
|
|
4100
|
+
errors: payload.errors,
|
|
4101
|
+
}
|
|
4102
|
+
);
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
return payload?.data || {};
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
#createMorphoProtocol(account, runtimeConfig, request) {
|
|
4110
|
+
return new MorphoProtocolEvm(account, this.#buildMorphoProtocolOptions(runtimeConfig, request));
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
#buildMorphoProtocolOptions(runtimeConfig, request) {
|
|
4114
|
+
const options = {
|
|
4115
|
+
chainId: runtimeConfig.chainId,
|
|
4116
|
+
supportSignature: false,
|
|
4117
|
+
};
|
|
4118
|
+
if (request.target.vaultAddress) {
|
|
4119
|
+
options.earnVaultAddress = request.target.vaultAddress;
|
|
4120
|
+
} else if (request.target.vaultPreset) {
|
|
4121
|
+
options.presets = {
|
|
4122
|
+
...(options.presets || {}),
|
|
4123
|
+
earn: request.target.vaultPreset,
|
|
4124
|
+
};
|
|
4125
|
+
}
|
|
4126
|
+
if (request.target.marketId) {
|
|
4127
|
+
options.borrowMarketId = request.target.marketId;
|
|
4128
|
+
} else if (request.target.marketPreset) {
|
|
4129
|
+
options.presets = {
|
|
4130
|
+
...(options.presets || {}),
|
|
4131
|
+
borrow: request.target.marketPreset,
|
|
4132
|
+
};
|
|
4133
|
+
}
|
|
4134
|
+
return options;
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
#getMorphoOperationMethods(request) {
|
|
4138
|
+
const table =
|
|
4139
|
+
request.target.vaultAddress || request.target.vaultPreset
|
|
4140
|
+
? {
|
|
4141
|
+
supply: {
|
|
4142
|
+
requirementsMethod: "getSupplyRequirements",
|
|
4143
|
+
quoteMethod: "quoteSupply",
|
|
4144
|
+
sendMethod: "supply",
|
|
4145
|
+
},
|
|
4146
|
+
withdraw: {
|
|
4147
|
+
requirementsMethod: null,
|
|
4148
|
+
quoteMethod: "quoteWithdraw",
|
|
4149
|
+
sendMethod: "withdraw",
|
|
4150
|
+
},
|
|
4151
|
+
}
|
|
4152
|
+
: {
|
|
4153
|
+
supply_collateral: {
|
|
4154
|
+
requirementsMethod: "getSupplyCollateralRequirements",
|
|
4155
|
+
quoteMethod: "quoteSupplyCollateral",
|
|
4156
|
+
sendMethod: "supplyCollateral",
|
|
4157
|
+
},
|
|
4158
|
+
borrow: {
|
|
4159
|
+
requirementsMethod: "getBorrowRequirements",
|
|
4160
|
+
quoteMethod: "quoteBorrow",
|
|
4161
|
+
sendMethod: "borrow",
|
|
4162
|
+
},
|
|
4163
|
+
repay: {
|
|
4164
|
+
requirementsMethod: "getRepayRequirements",
|
|
4165
|
+
quoteMethod: "quoteRepay",
|
|
4166
|
+
sendMethod: "repay",
|
|
4167
|
+
},
|
|
4168
|
+
withdraw_collateral: {
|
|
4169
|
+
requirementsMethod: null,
|
|
4170
|
+
quoteMethod: "quoteWithdrawCollateral",
|
|
4171
|
+
sendMethod: "withdrawCollateral",
|
|
4172
|
+
},
|
|
4173
|
+
};
|
|
4174
|
+
const methods = table[request.operation];
|
|
4175
|
+
if (!methods) {
|
|
4176
|
+
throw new Error(`Unsupported Morpho operation '${request.operation}'.`);
|
|
4177
|
+
}
|
|
4178
|
+
return methods;
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
#buildMorphoOperationOptions(request) {
|
|
4182
|
+
const options = {
|
|
4183
|
+
token: request.token,
|
|
4184
|
+
};
|
|
4185
|
+
if (request.amount !== undefined && request.amount !== null) {
|
|
4186
|
+
options.amount = request.amount;
|
|
4187
|
+
}
|
|
4188
|
+
if (request.nativeAmount !== undefined && request.nativeAmount !== null) {
|
|
4189
|
+
options.nativeAmount = request.nativeAmount;
|
|
4190
|
+
}
|
|
4191
|
+
return options;
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
async #buildMorphoOperationPlan({
|
|
4195
|
+
account,
|
|
4196
|
+
runtimeConfig,
|
|
4197
|
+
address,
|
|
4198
|
+
request,
|
|
4199
|
+
tolerateOperationFeeFailure = false,
|
|
4200
|
+
}) {
|
|
4201
|
+
const protocol = this.#createMorphoProtocol(account, runtimeConfig, request);
|
|
4202
|
+
try {
|
|
4203
|
+
const methods = this.#getMorphoOperationMethods(request);
|
|
4204
|
+
const operationOptions = this.#buildMorphoOperationOptions(request);
|
|
4205
|
+
const requirements = await this.#buildMorphoRequirementPlan({
|
|
4206
|
+
account,
|
|
4207
|
+
runtimeConfig,
|
|
4208
|
+
protocol,
|
|
4209
|
+
request,
|
|
4210
|
+
methods,
|
|
4211
|
+
operationOptions,
|
|
4212
|
+
});
|
|
4213
|
+
const operationFeeQuote = await this.#quoteMorphoProtocolOperation({
|
|
4214
|
+
protocol,
|
|
4215
|
+
methods,
|
|
4216
|
+
operationOptions,
|
|
4217
|
+
tolerateFailure: tolerateOperationFeeFailure,
|
|
4218
|
+
});
|
|
4219
|
+
const tokenMetadata = await this.#getBestEffortTokenMetadata(runtimeConfig, request.token);
|
|
4220
|
+
const target =
|
|
4221
|
+
request.target.vaultAddress || request.target.vaultPreset
|
|
4222
|
+
? {
|
|
4223
|
+
type: "vault",
|
|
4224
|
+
vaultAddress: protocol.getVaultAddress().toLowerCase(),
|
|
4225
|
+
vaultPreset: request.target.vaultPreset || null,
|
|
4226
|
+
}
|
|
4227
|
+
: {
|
|
4228
|
+
type: "market",
|
|
4229
|
+
marketId: protocol.getBorrowMarketId(),
|
|
4230
|
+
marketPreset: request.target.marketPreset || null,
|
|
4231
|
+
};
|
|
4232
|
+
const quoteFingerprint = sha256Hex(
|
|
4233
|
+
JSON.stringify({
|
|
4234
|
+
chainId: runtimeConfig.chainId,
|
|
4235
|
+
network: runtimeConfig.network,
|
|
4236
|
+
from: address.toLowerCase(),
|
|
4237
|
+
protocol: "morpho",
|
|
4238
|
+
target,
|
|
4239
|
+
operation: request.operation,
|
|
4240
|
+
token: request.token.toLowerCase(),
|
|
4241
|
+
amount:
|
|
4242
|
+
typeof request.amount === "bigint"
|
|
4243
|
+
? request.amount.toString()
|
|
4244
|
+
: request.amount === "max"
|
|
4245
|
+
? "max"
|
|
4246
|
+
: null,
|
|
4247
|
+
nativeAmount: request.nativeAmount ? request.nativeAmount.toString() : null,
|
|
4248
|
+
})
|
|
4249
|
+
);
|
|
2732
4250
|
return {
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
4251
|
+
quoteFingerprint,
|
|
4252
|
+
target,
|
|
4253
|
+
amount: request.amount ?? null,
|
|
4254
|
+
nativeAmount: request.nativeAmount ?? null,
|
|
2737
4255
|
tokenMetadata,
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
4256
|
+
operationFee: operationFeeQuote.fee,
|
|
4257
|
+
operationFeeError: operationFeeQuote.error,
|
|
4258
|
+
totalEstimatedFee:
|
|
4259
|
+
operationFeeQuote.fee !== null ? operationFeeQuote.fee + requirements.estimatedFee : null,
|
|
4260
|
+
requirements,
|
|
2741
4261
|
};
|
|
2742
|
-
}
|
|
4262
|
+
} finally {
|
|
4263
|
+
await maybeDispose(protocol);
|
|
4264
|
+
}
|
|
2743
4265
|
}
|
|
2744
4266
|
|
|
2745
|
-
async
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
message,
|
|
4267
|
+
async #buildMorphoRequirementPlan({
|
|
4268
|
+
account,
|
|
4269
|
+
runtimeConfig,
|
|
4270
|
+
protocol,
|
|
4271
|
+
request,
|
|
4272
|
+
methods,
|
|
4273
|
+
operationOptions,
|
|
2753
4274
|
}) {
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
4275
|
+
if (!methods.requirementsMethod) {
|
|
4276
|
+
return {
|
|
4277
|
+
required: false,
|
|
4278
|
+
estimatedFee: 0n,
|
|
4279
|
+
approvalRequired: false,
|
|
4280
|
+
authorizationRequired: false,
|
|
4281
|
+
steps: [],
|
|
4282
|
+
transactions: [],
|
|
4283
|
+
approvalContexts: [],
|
|
4284
|
+
authorizationContexts: [],
|
|
4285
|
+
};
|
|
4286
|
+
}
|
|
4287
|
+
const requirements = await protocol[methods.requirementsMethod](operationOptions);
|
|
4288
|
+
const steps = [];
|
|
4289
|
+
const transactions = [];
|
|
4290
|
+
const approvalContexts = [];
|
|
4291
|
+
const authorizationContexts = [];
|
|
4292
|
+
let estimatedFee = 0n;
|
|
4293
|
+
for (const requirement of Array.isArray(requirements) ? requirements : []) {
|
|
4294
|
+
if (isRequirementApproval(requirement)) {
|
|
4295
|
+
const spender = normalizeAddress(
|
|
4296
|
+
String(requirement.action?.args?.spender || ""),
|
|
4297
|
+
"morphoRequirement.spender"
|
|
4298
|
+
).toLowerCase();
|
|
4299
|
+
const amount = BigInt(requirement.action?.args?.amount || 0);
|
|
4300
|
+
const quote = await account.quoteSendTransaction({
|
|
4301
|
+
to: requirement.to,
|
|
4302
|
+
value: requirement.value ?? 0n,
|
|
4303
|
+
data: requirement.data,
|
|
4304
|
+
});
|
|
4305
|
+
const fee = BigInt(quote?.fee || 0);
|
|
4306
|
+
this.#assertMaxFee(runtimeConfig, fee, `morpho approval`);
|
|
4307
|
+
estimatedFee += fee;
|
|
4308
|
+
steps.push({
|
|
4309
|
+
type: "approval",
|
|
4310
|
+
spender,
|
|
4311
|
+
amount: amount.toString(),
|
|
4312
|
+
estimatedFeeWei: fee.toString(),
|
|
4313
|
+
to: requirement.to.toLowerCase(),
|
|
4314
|
+
value: BigInt(requirement.value ?? 0).toString(),
|
|
4315
|
+
dataHash: sha256Hex(requirement.data),
|
|
4316
|
+
});
|
|
4317
|
+
transactions.push(requirement);
|
|
4318
|
+
if (!approvalContexts.some((entry) => entry.spender === spender)) {
|
|
4319
|
+
const originalAllowance = await account.getAllowance(request.token, spender);
|
|
4320
|
+
approvalContexts.push({
|
|
4321
|
+
tokenAddress: request.token,
|
|
4322
|
+
spender,
|
|
4323
|
+
originalAllowance,
|
|
4324
|
+
});
|
|
4325
|
+
}
|
|
4326
|
+
continue;
|
|
4327
|
+
}
|
|
4328
|
+
if (isRequirementAuthorization(requirement)) {
|
|
4329
|
+
const authorized = normalizeAddress(
|
|
4330
|
+
String(requirement.action?.args?.authorized || ""),
|
|
4331
|
+
"morphoRequirement.authorized"
|
|
4332
|
+
).toLowerCase();
|
|
4333
|
+
const quote = await account.quoteSendTransaction({
|
|
4334
|
+
to: requirement.to,
|
|
4335
|
+
value: requirement.value ?? 0n,
|
|
4336
|
+
data: requirement.data,
|
|
4337
|
+
});
|
|
4338
|
+
const fee = BigInt(quote?.fee || 0);
|
|
4339
|
+
this.#assertMaxFee(runtimeConfig, fee, `morpho authorization`);
|
|
4340
|
+
estimatedFee += fee;
|
|
4341
|
+
steps.push({
|
|
4342
|
+
type: "authorization",
|
|
4343
|
+
authorized,
|
|
4344
|
+
isAuthorized: Boolean(requirement.action?.args?.isAuthorized),
|
|
4345
|
+
estimatedFeeWei: fee.toString(),
|
|
4346
|
+
to: requirement.to.toLowerCase(),
|
|
4347
|
+
value: BigInt(requirement.value ?? 0).toString(),
|
|
4348
|
+
dataHash: sha256Hex(requirement.data),
|
|
4349
|
+
});
|
|
4350
|
+
transactions.push(requirement);
|
|
4351
|
+
if (
|
|
4352
|
+
!authorizationContexts.some(
|
|
4353
|
+
(entry) =>
|
|
4354
|
+
entry.contractAddress === requirement.to.toLowerCase() &&
|
|
4355
|
+
entry.authorized === authorized
|
|
4356
|
+
)
|
|
4357
|
+
) {
|
|
4358
|
+
authorizationContexts.push({
|
|
4359
|
+
contractAddress: requirement.to.toLowerCase(),
|
|
4360
|
+
authorized,
|
|
4361
|
+
});
|
|
4362
|
+
}
|
|
4363
|
+
continue;
|
|
4364
|
+
}
|
|
4365
|
+
throw createTaggedError(
|
|
4366
|
+
"Morpho returned a signature requirement, but this runtime only supports transaction-based requirements.",
|
|
4367
|
+
"morpho_requirements_unresolved",
|
|
4368
|
+
{
|
|
4369
|
+
requirementType: requirement?.action?.type || null,
|
|
4370
|
+
}
|
|
2758
4371
|
);
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
4372
|
+
}
|
|
4373
|
+
return {
|
|
4374
|
+
required: steps.length > 0,
|
|
4375
|
+
estimatedFee,
|
|
4376
|
+
approvalRequired: steps.some((step) => step.type === "approval"),
|
|
4377
|
+
authorizationRequired: steps.some((step) => step.type === "authorization"),
|
|
4378
|
+
steps,
|
|
4379
|
+
transactions,
|
|
4380
|
+
approvalContexts,
|
|
4381
|
+
authorizationContexts,
|
|
4382
|
+
};
|
|
4383
|
+
}
|
|
4384
|
+
|
|
4385
|
+
async #quoteMorphoProtocolOperation({
|
|
4386
|
+
protocol,
|
|
4387
|
+
methods,
|
|
4388
|
+
operationOptions,
|
|
4389
|
+
tolerateFailure,
|
|
4390
|
+
}) {
|
|
4391
|
+
try {
|
|
4392
|
+
const quote = await protocol[methods.quoteMethod](operationOptions);
|
|
4393
|
+
return {
|
|
4394
|
+
fee: BigInt(quote?.fee || 0),
|
|
4395
|
+
error: null,
|
|
4396
|
+
};
|
|
4397
|
+
} catch (error) {
|
|
4398
|
+
if (!tolerateFailure) {
|
|
4399
|
+
throw error;
|
|
2762
4400
|
}
|
|
2763
|
-
const signature = await account.signTypedData({
|
|
2764
|
-
domain: typedData.domain,
|
|
2765
|
-
types: typedData.types,
|
|
2766
|
-
message: typedData.message,
|
|
2767
|
-
});
|
|
2768
4401
|
return {
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
signature,
|
|
2775
|
-
source: "wdk-wallet-evm",
|
|
4402
|
+
fee: null,
|
|
4403
|
+
error: {
|
|
4404
|
+
code: normalizeErrorCodeValue(error) || null,
|
|
4405
|
+
message: error instanceof Error ? error.message : String(error),
|
|
4406
|
+
},
|
|
2776
4407
|
};
|
|
2777
|
-
}
|
|
4408
|
+
}
|
|
2778
4409
|
}
|
|
2779
4410
|
|
|
2780
|
-
#
|
|
2781
|
-
const
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
4411
|
+
#formatMorphoOperationResponse({ runtimeConfig, accountIndex, address, request, plan }) {
|
|
4412
|
+
const amountFormatted =
|
|
4413
|
+
typeof plan.amount === "bigint" &&
|
|
4414
|
+
plan.tokenMetadata &&
|
|
4415
|
+
Number.isInteger(plan.tokenMetadata.decimals)
|
|
4416
|
+
? formatUnits(plan.amount, plan.tokenMetadata.decimals)
|
|
4417
|
+
: null;
|
|
4418
|
+
const nativeAmountFormatted =
|
|
4419
|
+
typeof plan.nativeAmount === "bigint" ? formatUnits(plan.nativeAmount, 18) : null;
|
|
2786
4420
|
return {
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
4421
|
+
network: runtimeConfig.network,
|
|
4422
|
+
chainId: runtimeConfig.chainId,
|
|
4423
|
+
accountIndex,
|
|
4424
|
+
address,
|
|
4425
|
+
protocol: "morpho",
|
|
4426
|
+
executionSupported: true,
|
|
4427
|
+
surface: plan.target.type,
|
|
4428
|
+
operation: request.operation,
|
|
4429
|
+
target: plan.target,
|
|
4430
|
+
operationRequest: {
|
|
4431
|
+
token: request.token,
|
|
4432
|
+
amount:
|
|
4433
|
+
typeof plan.amount === "bigint"
|
|
4434
|
+
? plan.amount.toString()
|
|
4435
|
+
: plan.amount === "max"
|
|
4436
|
+
? "max"
|
|
4437
|
+
: null,
|
|
4438
|
+
nativeAmount: typeof plan.nativeAmount === "bigint" ? plan.nativeAmount.toString() : null,
|
|
4439
|
+
},
|
|
4440
|
+
tokenMetadata: plan.tokenMetadata,
|
|
4441
|
+
amountFormatted,
|
|
4442
|
+
nativeAmountFormatted,
|
|
4443
|
+
quoteFingerprint: plan.quoteFingerprint,
|
|
4444
|
+
estimatedFeeWei: plan.totalEstimatedFee !== null ? plan.totalEstimatedFee.toString() : null,
|
|
4445
|
+
estimatedOperationFeeWei: plan.operationFee !== null ? plan.operationFee.toString() : null,
|
|
4446
|
+
estimatedRequirementsFeeWei: plan.requirements.estimatedFee.toString(),
|
|
4447
|
+
feeEstimateAvailable: plan.operationFee !== null,
|
|
4448
|
+
feeEstimateError: plan.operationFeeError,
|
|
4449
|
+
requirements: {
|
|
4450
|
+
required: plan.requirements.required,
|
|
4451
|
+
requirementCount: plan.requirements.steps.length,
|
|
4452
|
+
approvalRequired: plan.requirements.approvalRequired,
|
|
4453
|
+
authorizationRequired: plan.requirements.authorizationRequired,
|
|
4454
|
+
sequence: plan.requirements.steps,
|
|
4455
|
+
},
|
|
4456
|
+
source: "wdk-protocol-lending-morpho-evm",
|
|
2792
4457
|
};
|
|
2793
4458
|
}
|
|
2794
4459
|
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
const options = {
|
|
2799
|
-
provider: runtimeConfig.providerUrl,
|
|
2800
|
-
chainId: runtimeConfig.chainId,
|
|
2801
|
-
};
|
|
2802
|
-
if (runtimeConfig.transferMaxFeeWei !== null) {
|
|
2803
|
-
options.transferMaxFee = runtimeConfig.transferMaxFeeWei;
|
|
4460
|
+
#assertExpectedMorphoFingerprint(expectedQuoteFingerprint, actualQuoteFingerprint) {
|
|
4461
|
+
if (!expectedQuoteFingerprint) {
|
|
4462
|
+
return;
|
|
2804
4463
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
4464
|
+
if (expectedQuoteFingerprint !== actualQuoteFingerprint) {
|
|
4465
|
+
throw createTaggedError(
|
|
4466
|
+
"Morpho quote changed since preview. Generate a new preview and approval before execute.",
|
|
4467
|
+
"morpho_quote_changed",
|
|
4468
|
+
{
|
|
4469
|
+
expectedQuoteFingerprint,
|
|
4470
|
+
actualQuoteFingerprint,
|
|
4471
|
+
}
|
|
4472
|
+
);
|
|
2810
4473
|
}
|
|
2811
4474
|
}
|
|
2812
4475
|
|
|
2813
|
-
async #
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
4476
|
+
async #executeMorphoRequirementsIfNeeded({ account, runtimeConfig, plan }) {
|
|
4477
|
+
if (!plan.requirements.required) {
|
|
4478
|
+
return {
|
|
4479
|
+
performed: false,
|
|
4480
|
+
totalFee: 0n,
|
|
4481
|
+
transactions: [],
|
|
4482
|
+
approvalContexts: plan.requirements.approvalContexts,
|
|
4483
|
+
authorizationContexts: plan.requirements.authorizationContexts,
|
|
4484
|
+
};
|
|
4485
|
+
}
|
|
4486
|
+
let totalFee = 0n;
|
|
4487
|
+
const transactions = [];
|
|
4488
|
+
for (let index = 0; index < plan.requirements.transactions.length; index += 1) {
|
|
4489
|
+
const requirementTx = plan.requirements.transactions[index];
|
|
4490
|
+
const step = plan.requirements.steps[index];
|
|
4491
|
+
const result = await account.sendTransaction({
|
|
4492
|
+
to: requirementTx.to,
|
|
4493
|
+
value: requirementTx.value ?? 0n,
|
|
4494
|
+
data: requirementTx.data,
|
|
4495
|
+
});
|
|
4496
|
+
const fee = BigInt(result?.fee || 0);
|
|
4497
|
+
totalFee += fee;
|
|
4498
|
+
transactions.push({
|
|
4499
|
+
...step,
|
|
4500
|
+
hash: result.hash,
|
|
4501
|
+
fee: fee.toString(),
|
|
4502
|
+
});
|
|
4503
|
+
await this.#waitForTransactionReceipt(runtimeConfig, result.hash);
|
|
4504
|
+
}
|
|
4505
|
+
return {
|
|
4506
|
+
performed: true,
|
|
4507
|
+
totalFee,
|
|
4508
|
+
transactions,
|
|
4509
|
+
approvalContexts: plan.requirements.approvalContexts,
|
|
4510
|
+
authorizationContexts: plan.requirements.authorizationContexts,
|
|
4511
|
+
};
|
|
2818
4512
|
}
|
|
2819
4513
|
|
|
2820
|
-
async #
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
4514
|
+
async #restoreMorphoRequirementsAfterFailedOperation({
|
|
4515
|
+
account,
|
|
4516
|
+
runtimeConfig,
|
|
4517
|
+
requirementExecution,
|
|
4518
|
+
}) {
|
|
4519
|
+
if (!requirementExecution?.performed) {
|
|
4520
|
+
return {
|
|
4521
|
+
attempted: false,
|
|
4522
|
+
restored: false,
|
|
4523
|
+
};
|
|
4524
|
+
}
|
|
4525
|
+
const cleanup = {
|
|
4526
|
+
attempted: true,
|
|
4527
|
+
restored: false,
|
|
4528
|
+
approvals: [],
|
|
4529
|
+
authorizations: [],
|
|
4530
|
+
error: null,
|
|
4531
|
+
};
|
|
4532
|
+
try {
|
|
4533
|
+
for (const context of requirementExecution.approvalContexts || []) {
|
|
4534
|
+
const restorePlan = await this.#buildAllowanceRestorePlan({
|
|
4535
|
+
account,
|
|
4536
|
+
runtimeConfig,
|
|
4537
|
+
tokenAddress: context.tokenAddress,
|
|
4538
|
+
spender: context.spender,
|
|
4539
|
+
targetAllowance: context.originalAllowance,
|
|
4540
|
+
});
|
|
4541
|
+
const entry = {
|
|
4542
|
+
tokenAddress: context.tokenAddress,
|
|
4543
|
+
spender: context.spender,
|
|
4544
|
+
originalAllowance: BigInt(context.originalAllowance || 0).toString(),
|
|
4545
|
+
restoreSteps: restorePlan.steps.map((step) => ({ ...step })),
|
|
4546
|
+
restoreHashes: [],
|
|
4547
|
+
};
|
|
4548
|
+
for (const step of restorePlan.steps) {
|
|
4549
|
+
const result = await account.approve({
|
|
4550
|
+
token: context.tokenAddress,
|
|
4551
|
+
spender: context.spender,
|
|
4552
|
+
amount: step.amount,
|
|
4553
|
+
});
|
|
4554
|
+
entry.restoreHashes.push({
|
|
4555
|
+
type: step.type,
|
|
4556
|
+
hash: result.hash,
|
|
4557
|
+
fee: BigInt(result.fee || 0).toString(),
|
|
4558
|
+
});
|
|
4559
|
+
await this.#waitForTransactionReceipt(runtimeConfig, result.hash);
|
|
4560
|
+
}
|
|
4561
|
+
const finalAllowance = await account.getAllowance(context.tokenAddress, context.spender);
|
|
4562
|
+
entry.finalAllowance = finalAllowance.toString();
|
|
4563
|
+
entry.restored = finalAllowance === BigInt(context.originalAllowance || 0);
|
|
4564
|
+
cleanup.approvals.push(entry);
|
|
4565
|
+
if (!entry.restored) {
|
|
4566
|
+
throw new Error("Morpho approval allowance restore did not reach the original allowance.");
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4570
|
+
for (const context of requirementExecution.authorizationContexts || []) {
|
|
4571
|
+
const result = await account.sendTransaction({
|
|
4572
|
+
to: context.contractAddress,
|
|
4573
|
+
value: 0n,
|
|
4574
|
+
data: MORPHO_AUTHORIZATION_INTERFACE.encodeFunctionData("setAuthorization", [
|
|
4575
|
+
context.authorized,
|
|
4576
|
+
false,
|
|
4577
|
+
]),
|
|
4578
|
+
});
|
|
4579
|
+
cleanup.authorizations.push({
|
|
4580
|
+
contractAddress: context.contractAddress,
|
|
4581
|
+
authorized: context.authorized,
|
|
4582
|
+
hash: result.hash,
|
|
4583
|
+
fee: BigInt(result.fee || 0).toString(),
|
|
4584
|
+
});
|
|
4585
|
+
await this.#waitForTransactionReceipt(runtimeConfig, result.hash);
|
|
2832
4586
|
}
|
|
4587
|
+
|
|
4588
|
+
cleanup.restored = true;
|
|
4589
|
+
return cleanup;
|
|
4590
|
+
} catch (cleanupError) {
|
|
4591
|
+
cleanup.error = {
|
|
4592
|
+
message: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
|
|
4593
|
+
code:
|
|
4594
|
+
cleanupError && typeof cleanupError === "object"
|
|
4595
|
+
? String(cleanupError.errorCode || cleanupError.code || "").trim() || null
|
|
4596
|
+
: null,
|
|
4597
|
+
};
|
|
4598
|
+
return cleanup;
|
|
2833
4599
|
}
|
|
2834
|
-
|
|
4600
|
+
}
|
|
4601
|
+
|
|
4602
|
+
#throwMorphoFailureWithCleanup(error, cleanup) {
|
|
4603
|
+
if (cleanup?.attempted && cleanup.restored !== true) {
|
|
4604
|
+
throw createTaggedError(
|
|
4605
|
+
"Morpho operation failed after prerequisite transactions and automatic cleanup did not complete.",
|
|
4606
|
+
"morpho_cleanup_failed",
|
|
4607
|
+
{
|
|
4608
|
+
originalError:
|
|
4609
|
+
error instanceof Error
|
|
4610
|
+
? {
|
|
4611
|
+
message: error.message,
|
|
4612
|
+
code: String(error.errorCode || error.code || "").trim() || null,
|
|
4613
|
+
}
|
|
4614
|
+
: { message: String(error), code: null },
|
|
4615
|
+
cleanup,
|
|
4616
|
+
}
|
|
4617
|
+
);
|
|
4618
|
+
}
|
|
4619
|
+
throw error;
|
|
2835
4620
|
}
|
|
2836
4621
|
|
|
2837
4622
|
async #getTokenMetadata(runtimeConfig, tokenAddress) {
|
|
@@ -2990,6 +4775,7 @@ export class WdkEvmWalletService {
|
|
|
2990
4775
|
#assertMaxFee(runtimeConfig, fee, operation) {
|
|
2991
4776
|
if (
|
|
2992
4777
|
runtimeConfig.transferMaxFeeWei !== null &&
|
|
4778
|
+
runtimeConfig.transferMaxFeeWei !== undefined &&
|
|
2993
4779
|
BigInt(fee) >= BigInt(runtimeConfig.transferMaxFeeWei)
|
|
2994
4780
|
) {
|
|
2995
4781
|
throw createTaggedError(`Exceeded maximum fee cost for ${operation}.`, "fee_limit_exceeded", {
|