@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.
@@ -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 quoteAaveOperation({
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
- assertAaveSupportedNetwork(runtimeConfig.network);
1296
- const request = buildAaveOperationRequest({
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.#buildAaveOperationPlan({
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.#formatAaveOperationResponse({
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 sendAaveOperation({
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
- assertAaveSupportedNetwork(runtimeConfig.network);
1333
- const request = buildAaveOperationRequest({
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.#buildAaveOperationPlan({
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.#assertExpectedAaveFingerprint(
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 (approvalExecution.performed) {
1366
- finalPlan = await this.#buildAaveOperationPlan({
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.#assertExpectedAaveFingerprint(
2213
+ this.#assertExpectedMorphoFingerprint(
1373
2214
  normalizedExpectedQuoteFingerprint,
1374
2215
  finalPlan.quoteFingerprint
1375
2216
  );
1376
2217
  }
1377
2218
 
1378
- if (finalPlan.approval.required) {
2219
+ if (finalPlan.requirements.required) {
1379
2220
  throw createTaggedError(
1380
- "Aave operation still requires token approval after the approval step completed.",
1381
- "aave_approval_required",
2221
+ "Morpho operation still requires prerequisite transactions after the requirement step completed.",
2222
+ "morpho_requirements_unresolved",
1382
2223
  {
1383
- spender: finalPlan.spender,
1384
- requiredAllowance: finalPlan.amount.toString(),
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
- "Aave operation fee estimate was unavailable. Generate a new quote before sending.",
1393
- "aave_fee_unavailable",
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 = new AaveProtocolEvm(account);
2241
+ const protocol = this.#createMorphoProtocol(account, runtimeConfig, request);
1402
2242
  let result;
1403
2243
  try {
1404
- result = await protocol[request.operation]({
1405
- token: request.token,
1406
- amount: request.amount,
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 = approvalExecution.totalFee + resultFee;
2250
+ const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
2251
+ const totalFee = requirementExecution.totalFee + resultFee;
1413
2252
  return {
1414
- ...this.#formatAaveOperationResponse({
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
- approval: {
1424
- ...finalPlan.approval,
1425
- estimatedFee: approvalExecution.totalFee,
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
- approvalFee: approvalExecution.totalFee.toString(),
1434
- ...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
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.#restoreAllowanceAfterFailedAaveOperation({
2277
+ const cleanup = await this.#restoreMorphoRequirementsAfterFailedOperation({
1442
2278
  account,
1443
2279
  runtimeConfig,
1444
- tokenAddress: request.token,
1445
- spender: initialPlan.spender,
1446
- originalAllowance: initialPlan.currentAllowance,
1447
- approvalExecution,
2280
+ request,
2281
+ plan: initialPlan,
2282
+ requirementExecution,
1448
2283
  });
1449
- this.#throwAaveFailureWithCleanup(error, cleanup);
2284
+ this.#throwMorphoFailureWithCleanup(error, cleanup);
1450
2285
  }
1451
2286
  });
1452
2287
  }
1453
2288
 
1454
- async getLidoOverview({ seedPhrase, address, accountIndex = 0, network }) {
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
- assertLidoSupportedNetwork(runtimeConfig.network);
1627
- const request = buildLidoOperationRequest({ operation, amount });
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.#buildLidoOperationPlan({
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.#formatLidoOperationResponse({
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 sendLidoOperation({
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
- assertLidoSupportedNetwork(runtimeConfig.network);
1657
- const request = buildLidoOperationRequest({ operation, amount });
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.#buildLidoOperationPlan({
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.#assertExpectedLidoFingerprint(
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 (approvalExecution.performed) {
1685
- finalPlan = await this.#buildLidoOperationPlan({
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.#assertExpectedLidoFingerprint(
2390
+ this.#assertExpectedMorphoFingerprint(
1692
2391
  normalizedExpectedQuoteFingerprint,
1693
2392
  finalPlan.quoteFingerprint
1694
2393
  );
1695
2394
  }
1696
2395
 
1697
- if (finalPlan.approval.required) {
2396
+ if (finalPlan.requirements.required) {
1698
2397
  throw createTaggedError(
1699
- "Lido operation still requires token approval after the approval step completed.",
1700
- "lido_approval_required",
2398
+ "Morpho operation still requires prerequisite transactions after the requirement step completed.",
2399
+ "morpho_requirements_unresolved",
1701
2400
  {
1702
- spender: finalPlan.spender,
1703
- requiredAllowance: finalPlan.amount.toString(),
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
- "Lido operation fee estimate was unavailable. Generate a new quote before sending.",
1712
- "lido_fee_unavailable",
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 result = await account.sendTransaction(finalPlan.operationTx);
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 = approvalExecution.totalFee + resultFee;
2428
+ const totalFee = requirementExecution.totalFee + resultFee;
1723
2429
  return {
1724
- ...this.#formatLidoOperationResponse({
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
- approval: {
1734
- ...finalPlan.approval,
1735
- estimatedFee: approvalExecution.totalFee,
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
- approvalFee: approvalExecution.totalFee.toString(),
1744
- ...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
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.#restoreAllowanceAfterFailedLidoOperation({
2454
+ const cleanup = await this.#restoreMorphoRequirementsAfterFailedOperation({
1752
2455
  account,
1753
2456
  runtimeConfig,
1754
- tokenAddress: finalPlan.inputTokenAddress,
1755
- spender: initialPlan.spender,
1756
- originalAllowance: initialPlan.currentAllowance,
1757
- approvalExecution,
2457
+ request,
2458
+ plan: initialPlan,
2459
+ requirementExecution,
1758
2460
  });
1759
- this.#throwLidoFailureWithCleanup(error, cleanup);
2461
+ this.#throwMorphoFailureWithCleanup(error, cleanup);
1760
2462
  }
1761
2463
  });
1762
2464
  }
1763
2465
 
1764
- async quoteLidoWithdrawalOperation({
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
- assertLidoSupportedNetwork(runtimeConfig.network);
1777
- const request = buildLidoWithdrawalRequest({ operation, amount, requestId });
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.#buildLidoWithdrawalPlan({
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.#formatLidoWithdrawalResponse({
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 sendLidoWithdrawalOperation({
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
- assertLidoSupportedNetwork(runtimeConfig.network);
1808
- const request = buildLidoWithdrawalRequest({ operation, amount, requestId });
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.#buildLidoWithdrawalPlan({
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.#assertExpectedLidoWithdrawalFingerprint(
2535
+ this.#assertExpectedAaveFingerprint(
1822
2536
  normalizedExpectedQuoteFingerprint,
1823
2537
  initialPlan.quoteFingerprint
1824
2538
  );
1825
2539
 
1826
- const approvalExecution = await this.#executeLidoWithdrawalApprovalsIfNeeded({
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.#buildLidoWithdrawalPlan({
2550
+ finalPlan = await this.#buildAaveOperationPlan({
1837
2551
  account,
1838
2552
  runtimeConfig,
1839
2553
  address,
1840
2554
  request,
1841
2555
  });
1842
- this.#assertExpectedLidoWithdrawalFingerprint(
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
- "Lido withdrawal still requires token approval after the approval step completed.",
1851
- "lido_withdrawal_approval_required",
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.requiredAllowance.toString(),
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
- "Lido withdrawal fee estimate was unavailable. Generate a new quote before sending.",
1863
- "lido_withdrawal_fee_unavailable",
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 result = await account.sendTransaction(finalPlan.operationTx);
1872
- const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
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.#formatLidoWithdrawalResponse({
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.#restoreAllowanceAfterFailedLidoWithdrawal({
2625
+ const cleanup = await this.#restoreAllowanceAfterFailedAaveOperation({
1903
2626
  account,
1904
2627
  runtimeConfig,
1905
- tokenAddress: finalPlan.inputTokenAddress,
2628
+ tokenAddress: request.token,
1906
2629
  spender: initialPlan.spender,
1907
2630
  originalAllowance: initialPlan.currentAllowance,
1908
2631
  approvalExecution,
1909
2632
  });
1910
- this.#throwLidoWithdrawalFailureWithCleanup(error, cleanup);
2633
+ this.#throwAaveFailureWithCleanup(error, cleanup);
1911
2634
  }
1912
2635
  });
1913
2636
  }
1914
2637
 
1915
- async quoteSwap({
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
- assertVeloraSupportedNetwork(runtimeConfig.network);
1928
- const swapRequest = buildSwapRequest({ tokenIn, tokenOut, tokenInAmount });
1929
- const address = await account.getAddress();
1930
- const readOnlyAccount =
1931
- typeof account.toReadOnlyAccount === "function" ? await account.toReadOnlyAccount() : account;
1932
- try {
1933
- const plan = await this.#buildVeloraSwapPlan({
1934
- account: readOnlyAccount,
1935
- runtimeConfig,
1936
- swapRequest,
1937
- tolerateSwapFeeFailure: true,
1938
- });
1939
- const [tokenInMetadata, tokenOutMetadata] = await Promise.all([
1940
- this.#getSwapTokenMetadata(runtimeConfig, swapRequest.tokenIn, plan.priceRoute?.srcDecimals),
1941
- this.#getSwapTokenMetadata(runtimeConfig, swapRequest.tokenOut, plan.priceRoute?.destDecimals),
1942
- ]);
1943
- const quote = {
1944
- fee: plan.swapFee !== null ? plan.swapFee.toString() : null,
1945
- tokenInAmount: plan.tokenInAmount.toString(),
1946
- tokenOutAmount: plan.tokenOutAmount.toString(),
1947
- priceRoute: plan.priceRoute,
1948
- };
1949
- return {
1950
- network: runtimeConfig.network,
1951
- chainId: runtimeConfig.chainId,
1952
- accountIndex,
1953
- address,
1954
- protocol: "velora",
1955
- executionSupported: true,
1956
- swapRequest,
1957
- tokenInMetadata,
1958
- tokenOutMetadata,
1959
- inputAmountFormatted: formatUnits(swapRequest.tokenInAmount, tokenInMetadata.decimals),
1960
- outputAmountFormatted: formatUnits(plan.tokenOutAmount, tokenOutMetadata.decimals),
1961
- quoteFingerprint: plan.quoteFingerprint,
1962
- estimatedFeeWei:
1963
- plan.totalEstimatedFee !== null ? plan.totalEstimatedFee.toString() : null,
1964
- estimatedSwapFeeWei: plan.swapFee !== null ? plan.swapFee.toString() : null,
1965
- estimatedApprovalFeeWei: plan.approval.estimatedFee.toString(),
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 quoteLifiSwap({
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
- assertLifiSupportedNetwork(runtimeConfig.network);
2012
- const swapRequest = buildLifiEvmSwapRequest({
2013
- tokenIn,
2014
- destinationChain,
2015
- outputToken,
2016
- destinationAddress,
2017
- tokenInAmount,
2018
- slippage,
2019
- allowBridges,
2020
- denyBridges,
2021
- preferBridges,
2022
- });
2023
- const sourceAddress = await account.getAddress();
2024
- const plan = await this.#buildLifiEvmSwapPlan({
2025
- account,
2026
- runtimeConfig,
2027
- address: sourceAddress,
2028
- swapRequest,
2029
- tolerateSwapFeeFailure: true,
2030
- });
2031
- return this.#formatLifiSwapResponse({
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
- accountIndex,
2034
- address: sourceAddress,
2035
- swapRequest,
2036
- plan,
2711
+ operation: "unwrap_wsteth",
2712
+ amount: wstEthBalance,
2713
+ fromAddress: accountAddress,
2037
2714
  });
2038
- }
2039
- );
2040
- }
2041
-
2042
- async swap({
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
- tokenIn,
2045
- tokenOut,
2046
- tokenInAmount,
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
- assertVeloraSupportedNetwork(runtimeConfig.network);
2054
- const swapRequest = buildSwapRequest({ tokenIn, tokenOut, tokenInAmount });
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.#buildVeloraSwapPlan({
2847
+ let initialPlan = await this.#buildLidoOperationPlan({
2065
2848
  account,
2066
2849
  runtimeConfig,
2067
- swapRequest,
2850
+ address,
2851
+ request,
2852
+ tolerateOperationFeeFailure: true,
2068
2853
  });
2069
- const [tokenInMetadata, tokenOutMetadata] = await Promise.all([
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.#executeSwapApprovalsIfNeeded({
2859
+ const approvalExecution = await this.#executeLidoApprovalsIfNeeded({
2084
2860
  account,
2085
2861
  runtimeConfig,
2086
- swapRequest,
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.#buildVeloraSwapPlan({
2869
+ finalPlan = await this.#buildLidoOperationPlan({
2094
2870
  account,
2095
2871
  runtimeConfig,
2096
- swapRequest,
2872
+ address,
2873
+ request,
2097
2874
  });
2098
- this.#assertExpectedSwapFingerprint(
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 && !allowanceReadUncertain) {
2881
+ if (finalPlan.approval.required) {
2113
2882
  throw createTaggedError(
2114
- "Swap still requires token approval after the approval step completed.",
2115
- "swap_approval_required",
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.tokenInAmount.toString(),
2887
+ requiredAllowance: finalPlan.amount.toString(),
2119
2888
  currentAllowance: finalPlan.currentAllowance.toString(),
2120
2889
  }
2121
2890
  );
2122
2891
  }
2123
2892
 
2124
- const effectiveSimulation = allowanceReadUncertain
2125
- ? await this.#simulatePreparedTransaction({
2126
- runtimeConfig,
2127
- from: address,
2128
- tx: finalPlan.swapTx,
2129
- })
2130
- : finalPlan.simulation;
2131
- this.#assertSimulationSucceeded(effectiveSimulation);
2132
- const { hash } = await account.sendTransaction(finalPlan.swapTx);
2133
- const totalFee = approvalExecution.totalFee + finalPlan.swapFee;
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
- async sendLifiSwap({
2195
- seedPhrase,
2196
- tokenIn,
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.#formatLifiSwapResponse({
2908
+ ...this.#formatLidoOperationResponse({
2304
2909
  runtimeConfig,
2305
2910
  accountIndex,
2306
- address: sourceAddress,
2307
- swapRequest,
2911
+ address,
2912
+ request,
2308
2913
  plan: {
2309
2914
  ...finalPlan,
2310
- simulation: effectiveSimulation,
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.#restoreAllowanceAfterFailedSwap({
2935
+ const cleanup = await this.#restoreAllowanceAfterFailedLidoOperation({
2323
2936
  account,
2324
2937
  runtimeConfig,
2325
- tokenAddress: swapRequest.tokenIn,
2938
+ tokenAddress: finalPlan.inputTokenAddress,
2326
2939
  spender: initialPlan.spender,
2327
2940
  originalAllowance: initialPlan.currentAllowance,
2328
2941
  approvalExecution,
2329
2942
  });
2330
- this.#throwSwapFailureWithCleanup(error, cleanup);
2943
+ this.#throwLidoFailureWithCleanup(error, cleanup);
2331
2944
  }
2332
2945
  });
2333
2946
  }
2334
2947
 
2335
- async quoteUniswapSwap({
2948
+ async quoteLidoWithdrawalOperation({
2336
2949
  seedPhrase,
2337
2950
  address,
2338
- tokenIn,
2339
- tokenOut,
2340
- tokenInAmount,
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
- assertUniswapSupportedNetwork(runtimeConfig.network);
2349
- const swapRequest = buildUniswapSwapRequest({
2350
- tokenIn,
2351
- tokenOut,
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: swapperAddress,
2363
- swapRequest,
2966
+ address: accountAddress,
2967
+ request,
2968
+ tolerateOperationFeeFailure: true,
2364
2969
  });
2365
- return this.#formatUniswapSwapResponse({
2970
+ return this.#formatLidoWithdrawalResponse({
2366
2971
  runtimeConfig,
2367
2972
  accountIndex,
2368
- address: swapperAddress,
2369
- swapRequest,
2973
+ address: accountAddress,
2974
+ request,
2370
2975
  plan,
2371
2976
  });
2372
2977
  }
2373
2978
  );
2374
2979
  }
2375
2980
 
2376
- async sendUniswapSwap({
2981
+ async sendLidoWithdrawalOperation({
2377
2982
  seedPhrase,
2378
- tokenIn,
2379
- tokenOut,
2380
- tokenInAmount,
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
- assertUniswapSupportedNetwork(runtimeConfig.network);
2389
- const swapRequest = buildUniswapSwapRequest({
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.#buildUniswapSwapPlan({
2998
+ let initialPlan = await this.#buildLidoWithdrawalPlan({
2408
2999
  account,
2409
3000
  runtimeConfig,
2410
3001
  address,
2411
- swapRequest,
3002
+ request,
3003
+ tolerateOperationFeeFailure: true,
2412
3004
  });
2413
- this.#assertExpectedSwapFingerprint(
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.#executeSwapApprovalsIfNeeded({
3010
+ const approvalExecution = await this.#executeLidoWithdrawalApprovalsIfNeeded({
2424
3011
  account,
2425
3012
  runtimeConfig,
2426
- swapRequest: { tokenIn: swapRequest.tokenIn },
3013
+ request,
2427
3014
  plan: initialPlan,
2428
3015
  });
2429
3016
 
2430
3017
  let finalPlan = initialPlan;
2431
3018
  try {
2432
3019
  if (approvalExecution.performed) {
2433
- // Re-fetch after approval to obtain fresh permitData (the deadline/nonce are short-lived).
2434
- finalPlan = await this.#buildUniswapSwapPlan({
3020
+ finalPlan = await this.#buildLidoWithdrawalPlan({
2435
3021
  account,
2436
3022
  runtimeConfig,
2437
3023
  address,
2438
- swapRequest,
3024
+ request,
2439
3025
  });
2440
- this.#assertExpectedSwapFingerprint(
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
- const allowanceReadUncertain =
2452
- approvalExecution.performed && finalPlan.allowanceReadError !== null;
2453
- if (finalPlan.approval.required && !allowanceReadUncertain) {
3032
+ if (finalPlan.approval.required) {
2454
3033
  throw createTaggedError(
2455
- "Uniswap swap still requires token approval after the approval step completed.",
2456
- "swap_approval_required",
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.tokenInAmount.toString(),
3038
+ requiredAllowance: finalPlan.requiredAllowance.toString(),
2460
3039
  currentAllowance: finalPlan.currentAllowance.toString(),
2461
3040
  }
2462
3041
  );
2463
3042
  }
2464
3043
 
2465
- const signature =
2466
- finalPlan.isNativeTokenIn || !finalPlan.permitData
2467
- ? null
2468
- : await this.#signUniswapPermit(account, finalPlan.permitData, runtimeConfig);
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 swapTx = await this.#fetchUniswapSwapCalldata({
2471
- runtimeConfig,
2472
- quoteResponse: finalPlan.quoteResponse,
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
- ...(await this.#formatUniswapSwapResponse({
3059
+ ...this.#formatLidoWithdrawalResponse({
2497
3060
  runtimeConfig,
2498
3061
  accountIndex,
2499
3062
  address,
2500
- swapRequest,
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
- simulation,
2510
- swapTransaction: { to: swapTx.to, value: swapTx.value.toString(), dataHash: sha256Hex(swapTx.data) },
2511
- result,
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.#restoreAllowanceAfterFailedSwap({
3086
+ const cleanup = await this.#restoreAllowanceAfterFailedLidoWithdrawal({
2515
3087
  account,
2516
3088
  runtimeConfig,
2517
- tokenAddress: swapRequest.tokenIn,
3089
+ tokenAddress: finalPlan.inputTokenAddress,
2518
3090
  spender: initialPlan.spender,
2519
3091
  originalAllowance: initialPlan.currentAllowance,
2520
3092
  approvalExecution,
2521
3093
  });
2522
- this.#throwSwapFailureWithCleanup(error, cleanup);
3094
+ this.#throwLidoWithdrawalFailureWithCleanup(error, cleanup);
2523
3095
  }
2524
3096
  });
2525
3097
  }
2526
3098
 
2527
- async #getUniswapTokenMetadata(runtimeConfig, tokenAddress) {
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
- tokenAddress,
2622
- recipient,
2623
- amount,
3101
+ address,
3102
+ tokenIn,
3103
+ tokenOut,
3104
+ tokenInAmount,
2624
3105
  accountIndex = 0,
2625
3106
  network,
2626
3107
  }) {
2627
- return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
2628
- const transfer = {
2629
- token: normalizeAddress(tokenAddress, "tokenAddress"),
2630
- recipient: normalizeAddress(recipient, "recipient"),
2631
- amount: assertPositiveBigIntString(amount, "amount"),
2632
- };
2633
- const ownerAddress = await account.getAddress();
2634
- const { tokenMetadata } = await this.#prepareTokenTransferContext({
2635
- account,
2636
- runtimeConfig,
2637
- transfer,
2638
- ownerAddress,
2639
- });
2640
- let quote;
2641
- try {
2642
- quote = await account.quoteTransfer(transfer);
2643
- } catch (error) {
2644
- if (isRecoverableTokenTransferSimulationFailure(error)) {
2645
- throw createTaggedError(
2646
- "Token transfer could not be simulated by the token contract.",
2647
- "token_transfer_failed",
2648
- {
2649
- network: runtimeConfig.network,
2650
- tokenAddress: transfer.token,
2651
- ownerAddress,
2652
- recipient: transfer.recipient,
2653
- amount: transfer.amount.toString(),
2654
- underlying:
2655
- error instanceof Error
2656
- ? {
2657
- message: error.message,
2658
- code: String(error.errorCode || error.code || "").trim() || null,
2659
- }
2660
- : {
2661
- message: String(error),
2662
- code: null,
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
- return {
2670
- network: runtimeConfig.network,
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
- network: runtimeConfig.network,
2734
- chainId: runtimeConfig.chainId,
2735
- accountIndex,
2736
- transfer,
4251
+ quoteFingerprint,
4252
+ target,
4253
+ amount: request.amount ?? null,
4254
+ nativeAmount: request.nativeAmount ?? null,
2737
4255
  tokenMetadata,
2738
- amountFormatted: formatUnits(transfer.amount, tokenMetadata.decimals),
2739
- result,
2740
- source: "wdk-wallet-evm",
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 signX402ExactTypedData({
2746
- seedPhrase,
2747
- accountIndex = 0,
2748
- network,
2749
- domain,
2750
- types,
2751
- primaryType,
2752
- message,
4267
+ async #buildMorphoRequirementPlan({
4268
+ account,
4269
+ runtimeConfig,
4270
+ protocol,
4271
+ request,
4272
+ methods,
4273
+ operationOptions,
2753
4274
  }) {
2754
- return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
2755
- const typedData = normalizeX402ExactTypedData(
2756
- { domain, types, primaryType, message },
2757
- runtimeConfig
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
- const signerAddress = normalizeAddress(await account.getAddress(), "accountAddress");
2760
- if (typedData.message.from.toLowerCase() !== signerAddress.toLowerCase()) {
2761
- throw new Error("message.from must match the active wallet account address.");
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
- network: runtimeConfig.network,
2770
- chainId: runtimeConfig.chainId,
2771
- accountIndex,
2772
- address: signerAddress,
2773
- primaryType: typedData.primaryType,
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
- #resolveRuntimeConfig(networkOverride) {
2781
- const network = assertValidNetwork(networkOverride) || this.config.network;
2782
- const profile = this.config.networkProfiles?.[network];
2783
- if (!profile) {
2784
- throw new Error(`Missing RPC profile for network: ${network}`);
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
- ...this.config,
2788
- network,
2789
- chainId: profile.chainId,
2790
- providerUrl: profile.providerUrl,
2791
- nativeSymbol: profile.nativeSymbol,
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
- async #withWallet({ seedPhrase, network }, callback) {
2796
- const mnemonic = assertValidSeedPhrase(seedPhrase);
2797
- const runtimeConfig = this.#resolveRuntimeConfig(network);
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
- const wallet = new WalletManagerEvm(mnemonic, options);
2806
- try {
2807
- return await callback(wallet, runtimeConfig);
2808
- } finally {
2809
- await maybeDispose(wallet);
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 #withAccount({ seedPhrase, accountIndex, network }, callback) {
2814
- return this.#withWallet({ seedPhrase, network }, async (wallet, runtimeConfig) => {
2815
- const account = await wallet.getAccount(assertNonNegativeInteger(accountIndex, "accountIndex"));
2816
- return await callback(account, runtimeConfig);
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 #withReadableAccount({ seedPhrase, address, accountIndex, network }, callback) {
2821
- const normalizedAddress = String(address || "").trim();
2822
- if (normalizedAddress) {
2823
- const runtimeConfig = this.#resolveRuntimeConfig(network);
2824
- const account = new WalletAccountReadOnlyEvm(
2825
- normalizeAddress(normalizedAddress, "address"),
2826
- { provider: runtimeConfig.providerUrl }
2827
- );
2828
- try {
2829
- return await callback(account, runtimeConfig);
2830
- } finally {
2831
- await maybeDispose(account);
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
- return this.#withAccount({ seedPhrase, accountIndex, network }, callback);
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", {