@lit-protocol/vincent-ability-morpho 0.1.18-mma → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +15 -15
- package/dist/CHANGELOG.md +15 -0
- package/dist/README.md +15 -15
- package/dist/package.json +2 -7
- package/dist/src/generated/lit-action.js +2 -2
- package/dist/src/generated/vincent-ability-metadata.json +1 -1
- package/dist/src/generated/vincent-bundled-ability.d.ts +10 -16
- package/dist/src/generated/vincent-bundled-ability.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/helpers/index.d.ts +26 -363
- package/dist/src/lib/helpers/index.d.ts.map +1 -1
- package/dist/src/lib/helpers/index.js +155 -983
- package/dist/src/lib/helpers/index.js.map +1 -1
- package/dist/src/lib/schemas.d.ts +11 -16
- package/dist/src/lib/schemas.d.ts.map +1 -1
- package/dist/src/lib/schemas.js +16 -14
- package/dist/src/lib/schemas.js.map +1 -1
- package/dist/src/lib/vincent-ability.d.ts +10 -16
- package/dist/src/lib/vincent-ability.d.ts.map +1 -1
- package/dist/src/lib/vincent-ability.js +98 -115
- package/dist/src/lib/vincent-ability.js.map +1 -1
- package/package.json +4 -9
- package/dist/ethers-v6-shim.js +0 -12
|
@@ -1,30 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.getTokenAddresses = getTokenAddresses;
|
|
5
|
-
exports.getTokenAddress = getTokenAddress;
|
|
6
|
-
exports.isSupportedChain = isSupportedChain;
|
|
7
|
-
exports.getSupportedChainIds = getSupportedChainIds;
|
|
8
|
-
exports.getChainName = getChainName;
|
|
9
|
-
exports.isValidAddress = isValidAddress;
|
|
10
|
-
exports.parseAmount = parseAmount;
|
|
11
|
-
exports.formatAmount = formatAmount;
|
|
3
|
+
exports.ERC4626_VAULT_ABI = exports.ERC20_ABI = exports.CHAIN_IDS = exports.SUPPORTED_CHAINS = void 0;
|
|
12
4
|
exports.validateOperationRequirements = validateOperationRequirements;
|
|
13
|
-
exports.
|
|
14
|
-
exports.
|
|
15
|
-
exports.getTopVaultsByTvl = getTopVaultsByTvl;
|
|
16
|
-
exports.searchVaults = searchVaults;
|
|
17
|
-
exports.getVaultsByPreset = getVaultsByPreset;
|
|
18
|
-
exports.getVaults = getVaults;
|
|
19
|
-
exports.getSupportedChainsWithVaults = getSupportedChainsWithVaults;
|
|
20
|
-
exports.getVaultDiscoverySummary = getVaultDiscoverySummary;
|
|
21
|
-
exports.normalizeLitSig = normalizeLitSig;
|
|
22
|
-
exports.buildDepositTxData = buildDepositTxData;
|
|
23
|
-
exports.buildRedeemTxData = buildRedeemTxData;
|
|
24
|
-
exports.executeChainOperation = executeChainOperation;
|
|
5
|
+
exports.getMorphoVaultByAddress = getMorphoVaultByAddress;
|
|
6
|
+
exports.executeMorphoOperation = executeMorphoOperation;
|
|
25
7
|
const vincent_scaffold_sdk_1 = require("@lit-protocol/vincent-scaffold-sdk");
|
|
26
|
-
const
|
|
27
|
-
|
|
8
|
+
const schemas_1 = require("../schemas");
|
|
9
|
+
/**
|
|
10
|
+
* Supported chain IDs and their names
|
|
11
|
+
*/
|
|
12
|
+
exports.SUPPORTED_CHAINS = {
|
|
13
|
+
1: 'ethereum',
|
|
14
|
+
8453: 'base',
|
|
15
|
+
42161: 'arbitrum',
|
|
16
|
+
10: 'optimism',
|
|
17
|
+
137: 'polygon',
|
|
18
|
+
};
|
|
28
19
|
/**
|
|
29
20
|
* Chain names to IDs mapping for backwards compatibility
|
|
30
21
|
*/
|
|
@@ -36,51 +27,50 @@ exports.CHAIN_IDS = {
|
|
|
36
27
|
polygon: 137,
|
|
37
28
|
};
|
|
38
29
|
/**
|
|
39
|
-
*
|
|
40
|
-
*/
|
|
41
|
-
exports.SUPPORTED_CHAINS = {
|
|
42
|
-
[exports.CHAIN_IDS.ethereum]: 'ethereum',
|
|
43
|
-
[exports.CHAIN_IDS.base]: 'base',
|
|
44
|
-
[exports.CHAIN_IDS.arbitrum]: 'arbitrum',
|
|
45
|
-
[exports.CHAIN_IDS.optimism]: 'optimism',
|
|
46
|
-
[exports.CHAIN_IDS.polygon]: 'polygon',
|
|
47
|
-
};
|
|
48
|
-
/**
|
|
49
|
-
* Well-known token addresses across different chains
|
|
50
|
-
* Using official Circle USDC and canonical WETH addresses
|
|
30
|
+
* ERC20 Token ABI - Essential methods only
|
|
51
31
|
*/
|
|
52
|
-
exports.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
USDC: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // Native USDC on Base
|
|
60
|
-
WETH: '0x4200000000000000000000000000000000000006', // WETH on Base
|
|
61
|
-
USDT: '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2', // USDT on Base
|
|
32
|
+
exports.ERC20_ABI = [
|
|
33
|
+
{
|
|
34
|
+
inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
|
|
35
|
+
name: 'balanceOf',
|
|
36
|
+
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
|
37
|
+
stateMutability: 'view',
|
|
38
|
+
type: 'function',
|
|
62
39
|
},
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
40
|
+
{
|
|
41
|
+
inputs: [
|
|
42
|
+
{ internalType: 'address', name: 'owner', type: 'address' },
|
|
43
|
+
{ internalType: 'address', name: 'spender', type: 'address' },
|
|
44
|
+
],
|
|
45
|
+
name: 'allowance',
|
|
46
|
+
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
|
47
|
+
stateMutability: 'view',
|
|
48
|
+
type: 'function',
|
|
67
49
|
},
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
50
|
+
{
|
|
51
|
+
inputs: [
|
|
52
|
+
{ internalType: 'address', name: 'spender', type: 'address' },
|
|
53
|
+
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
|
|
54
|
+
],
|
|
55
|
+
name: 'approve',
|
|
56
|
+
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
|
57
|
+
stateMutability: 'nonpayable',
|
|
58
|
+
type: 'function',
|
|
72
59
|
},
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
60
|
+
{
|
|
61
|
+
inputs: [],
|
|
62
|
+
name: 'decimals',
|
|
63
|
+
outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }],
|
|
64
|
+
stateMutability: 'view',
|
|
65
|
+
type: 'function',
|
|
77
66
|
},
|
|
78
|
-
|
|
67
|
+
];
|
|
79
68
|
/**
|
|
80
69
|
* ERC4626 Vault ABI - Essential methods for Morpho vaults
|
|
81
70
|
*/
|
|
82
71
|
exports.ERC4626_VAULT_ABI = [
|
|
83
|
-
//
|
|
72
|
+
// ERC4626 is an extension of ERC20, the token represents vault shares
|
|
73
|
+
...exports.ERC20_ABI,
|
|
84
74
|
{
|
|
85
75
|
inputs: [
|
|
86
76
|
{ internalType: 'uint256', name: 'assets', type: 'uint256' },
|
|
@@ -91,7 +81,6 @@ exports.ERC4626_VAULT_ABI = [
|
|
|
91
81
|
stateMutability: 'nonpayable',
|
|
92
82
|
type: 'function',
|
|
93
83
|
},
|
|
94
|
-
// Redeem
|
|
95
84
|
{
|
|
96
85
|
inputs: [
|
|
97
86
|
{ internalType: 'uint256', name: 'shares', type: 'uint256' },
|
|
@@ -103,7 +92,6 @@ exports.ERC4626_VAULT_ABI = [
|
|
|
103
92
|
stateMutability: 'nonpayable',
|
|
104
93
|
type: 'function',
|
|
105
94
|
},
|
|
106
|
-
// Asset (underlying token address)
|
|
107
95
|
{
|
|
108
96
|
inputs: [],
|
|
109
97
|
name: 'asset',
|
|
@@ -111,15 +99,6 @@ exports.ERC4626_VAULT_ABI = [
|
|
|
111
99
|
stateMutability: 'view',
|
|
112
100
|
type: 'function',
|
|
113
101
|
},
|
|
114
|
-
// Balance of shares
|
|
115
|
-
{
|
|
116
|
-
inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
|
|
117
|
-
name: 'balanceOf',
|
|
118
|
-
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
|
119
|
-
stateMutability: 'view',
|
|
120
|
-
type: 'function',
|
|
121
|
-
},
|
|
122
|
-
// Convert assets to shares
|
|
123
102
|
{
|
|
124
103
|
inputs: [{ internalType: 'uint256', name: 'assets', type: 'uint256' }],
|
|
125
104
|
name: 'convertToShares',
|
|
@@ -127,7 +106,6 @@ exports.ERC4626_VAULT_ABI = [
|
|
|
127
106
|
stateMutability: 'view',
|
|
128
107
|
type: 'function',
|
|
129
108
|
},
|
|
130
|
-
// Convert shares to assets
|
|
131
109
|
{
|
|
132
110
|
inputs: [{ internalType: 'uint256', name: 'shares', type: 'uint256' }],
|
|
133
111
|
name: 'convertToAssets',
|
|
@@ -135,7 +113,6 @@ exports.ERC4626_VAULT_ABI = [
|
|
|
135
113
|
stateMutability: 'view',
|
|
136
114
|
type: 'function',
|
|
137
115
|
},
|
|
138
|
-
// Total assets managed by the vault
|
|
139
116
|
{
|
|
140
117
|
inputs: [],
|
|
141
118
|
name: 'totalAssets',
|
|
@@ -143,219 +120,44 @@ exports.ERC4626_VAULT_ABI = [
|
|
|
143
120
|
stateMutability: 'view',
|
|
144
121
|
type: 'function',
|
|
145
122
|
},
|
|
146
|
-
{
|
|
147
|
-
inputs: [],
|
|
148
|
-
name: 'decimals',
|
|
149
|
-
outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }],
|
|
150
|
-
stateMutability: 'view',
|
|
151
|
-
type: 'function',
|
|
152
|
-
},
|
|
153
|
-
];
|
|
154
|
-
/**
|
|
155
|
-
* ERC20 Token ABI - Essential methods only
|
|
156
|
-
*/
|
|
157
|
-
exports.ERC20_ABI = [
|
|
158
|
-
{
|
|
159
|
-
inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
|
|
160
|
-
name: 'balanceOf',
|
|
161
|
-
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
|
162
|
-
stateMutability: 'view',
|
|
163
|
-
type: 'function',
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
inputs: [
|
|
167
|
-
{ internalType: 'address', name: 'owner', type: 'address' },
|
|
168
|
-
{ internalType: 'address', name: 'spender', type: 'address' },
|
|
169
|
-
],
|
|
170
|
-
name: 'allowance',
|
|
171
|
-
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
|
172
|
-
stateMutability: 'view',
|
|
173
|
-
type: 'function',
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
inputs: [
|
|
177
|
-
{ internalType: 'address', name: 'spender', type: 'address' },
|
|
178
|
-
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
|
|
179
|
-
],
|
|
180
|
-
name: 'approve',
|
|
181
|
-
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
|
182
|
-
stateMutability: 'nonpayable',
|
|
183
|
-
type: 'function',
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
inputs: [],
|
|
187
|
-
name: 'decimals',
|
|
188
|
-
outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }],
|
|
189
|
-
stateMutability: 'view',
|
|
190
|
-
type: 'function',
|
|
191
|
-
},
|
|
192
|
-
// EIP-2612 ERC20Permit extension
|
|
193
|
-
{
|
|
194
|
-
inputs: [],
|
|
195
|
-
name: 'name',
|
|
196
|
-
outputs: [{ internalType: 'string', name: '', type: 'string' }],
|
|
197
|
-
stateMutability: 'view',
|
|
198
|
-
type: 'function',
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
inputs: [],
|
|
202
|
-
name: 'version',
|
|
203
|
-
outputs: [{ internalType: 'string', name: '', type: 'string' }],
|
|
204
|
-
stateMutability: 'view',
|
|
205
|
-
type: 'function',
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
inputs: [{ internalType: 'address', name: 'owner', type: 'address' }],
|
|
209
|
-
name: 'nonces',
|
|
210
|
-
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
|
211
|
-
stateMutability: 'view',
|
|
212
|
-
type: 'function',
|
|
213
|
-
},
|
|
214
|
-
];
|
|
215
|
-
exports.BUNDLER_ABI = [
|
|
216
|
-
{
|
|
217
|
-
inputs: [
|
|
218
|
-
{
|
|
219
|
-
components: [
|
|
220
|
-
{ internalType: 'address', name: 'to', type: 'address' },
|
|
221
|
-
{ internalType: 'bytes', name: 'data', type: 'bytes' },
|
|
222
|
-
{ internalType: 'uint256', name: 'value', type: 'uint256' },
|
|
223
|
-
{ internalType: 'bool', name: 'skipRevert', type: 'bool' },
|
|
224
|
-
{ internalType: 'bytes32', name: 'callbackHash', type: 'bytes32' },
|
|
225
|
-
],
|
|
226
|
-
internalType: 'struct Call[]',
|
|
227
|
-
name: 'bundle',
|
|
228
|
-
type: 'tuple[]',
|
|
229
|
-
},
|
|
230
|
-
],
|
|
231
|
-
name: 'multicall',
|
|
232
|
-
outputs: [],
|
|
233
|
-
stateMutability: 'payable',
|
|
234
|
-
type: 'function',
|
|
235
|
-
},
|
|
236
123
|
];
|
|
237
|
-
exports.BUNDLER_ADDRESSES = {
|
|
238
|
-
[exports.CHAIN_IDS.ethereum]: '0x6566194141eefa99Af43Bb5Aa71460Ca2Dc90245',
|
|
239
|
-
[exports.CHAIN_IDS.base]: '0x6BFd8137e702540E7A42B74178A4a49Ba43920C4',
|
|
240
|
-
[exports.CHAIN_IDS.arbitrum]: '0x1FA4431bC113D308beE1d46B0e98Cb805FB48C13',
|
|
241
|
-
[exports.CHAIN_IDS.optimism]: '0xFBCd3C258feB131D8E038F2A3a670A7bE0507C05',
|
|
242
|
-
[exports.CHAIN_IDS.polygon]: '0x2d9C3A9E67c966C711208cc78b34fB9E9f8db589',
|
|
243
|
-
};
|
|
244
|
-
/**
|
|
245
|
-
* Pre-configured filter presets for common use cases
|
|
246
|
-
*/
|
|
247
|
-
exports.VAULT_FILTER_PRESETS = {
|
|
248
|
-
highYield: {
|
|
249
|
-
minNetApy: 0.08,
|
|
250
|
-
minTvl: 1000000,
|
|
251
|
-
sortBy: 'netApy',
|
|
252
|
-
sortOrder: 'desc',
|
|
253
|
-
excludeIdle: true,
|
|
254
|
-
limit: 10,
|
|
255
|
-
},
|
|
256
|
-
stable: {
|
|
257
|
-
minTvl: 5000000,
|
|
258
|
-
maxNetApy: 0.15,
|
|
259
|
-
whitelistedOnly: true,
|
|
260
|
-
sortBy: 'totalAssetsUsd',
|
|
261
|
-
sortOrder: 'desc',
|
|
262
|
-
excludeIdle: true,
|
|
263
|
-
limit: 10,
|
|
264
|
-
},
|
|
265
|
-
highTvl: {
|
|
266
|
-
minTvl: 10000000,
|
|
267
|
-
sortBy: 'totalAssetsUsd',
|
|
268
|
-
sortOrder: 'desc',
|
|
269
|
-
excludeIdle: true,
|
|
270
|
-
limit: 20,
|
|
271
|
-
},
|
|
272
|
-
};
|
|
273
|
-
/**
|
|
274
|
-
* Get well-known token addresses for a specific chain
|
|
275
|
-
*/
|
|
276
|
-
function getTokenAddresses(chainId) {
|
|
277
|
-
if (!(chainId in exports.WELL_KNOWN_TOKENS)) {
|
|
278
|
-
throw new Error(`Unsupported chain ID: ${chainId}. Supported chains: ${Object.keys(exports.WELL_KNOWN_TOKENS).join(', ')}`);
|
|
279
|
-
}
|
|
280
|
-
return exports.WELL_KNOWN_TOKENS[chainId];
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Get token address for a specific token symbol and chain
|
|
284
|
-
*/
|
|
285
|
-
function getTokenAddress(symbol, chainId) {
|
|
286
|
-
const tokens = getTokenAddresses(chainId);
|
|
287
|
-
const upperSymbol = symbol.toUpperCase();
|
|
288
|
-
if (!(upperSymbol in tokens)) {
|
|
289
|
-
throw new Error(`Token ${symbol} not found on chain ${chainId}. Available tokens: ${Object.keys(tokens).join(', ')}`);
|
|
290
|
-
}
|
|
291
|
-
return tokens[upperSymbol];
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Check if a chain is supported by Morpho
|
|
295
|
-
*/
|
|
296
|
-
function isSupportedChain(chainId) {
|
|
297
|
-
return chainId in exports.WELL_KNOWN_TOKENS;
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Get all supported chain IDs
|
|
301
|
-
*/
|
|
302
|
-
function getSupportedChainIds() {
|
|
303
|
-
return Object.keys(exports.WELL_KNOWN_TOKENS).map(Number);
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* Get chain name from chain ID
|
|
307
|
-
*/
|
|
308
|
-
function getChainName(chainId) {
|
|
309
|
-
return exports.SUPPORTED_CHAINS[chainId] || `chain-${chainId}`;
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Utility function to validate Ethereum address
|
|
313
|
-
*/
|
|
314
|
-
function isValidAddress(address) {
|
|
315
|
-
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Utility function to parse amount with decimals
|
|
319
|
-
*/
|
|
320
|
-
function parseAmount(amount, decimals = 18) {
|
|
321
|
-
return ethers_1.ethers.utils.parseUnits(amount, decimals).toString();
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Utility function to format amount from wei
|
|
325
|
-
*/
|
|
326
|
-
function formatAmount(amount, decimals = 18) {
|
|
327
|
-
return ethers_1.ethers.utils.formatUnits(amount, decimals);
|
|
328
|
-
}
|
|
329
124
|
/**
|
|
330
125
|
* Validate operation-specific requirements for Morpho vaults
|
|
331
126
|
*/
|
|
332
|
-
async function validateOperationRequirements(operation, userBalance, vaultShares,
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
127
|
+
async function validateOperationRequirements(operation, userBalance, allowance, vaultShares, amount) {
|
|
128
|
+
const debugParams = {
|
|
129
|
+
operation,
|
|
130
|
+
userBalance: userBalance.toString(),
|
|
131
|
+
allowance: allowance.toString(),
|
|
132
|
+
vaultShares: vaultShares.toString(),
|
|
133
|
+
amount: amount.toString(),
|
|
134
|
+
};
|
|
336
135
|
switch (operation) {
|
|
337
|
-
case
|
|
338
|
-
//
|
|
339
|
-
|
|
136
|
+
case schemas_1.MorphoOperation.APPROVE:
|
|
137
|
+
// No need to check anything, the user can always approve token spending even when not having enough of them
|
|
138
|
+
break;
|
|
139
|
+
case schemas_1.MorphoOperation.DEPOSIT:
|
|
140
|
+
// Check if the user has enough tokens to deposit
|
|
141
|
+
if (userBalance.lt(amount)) {
|
|
340
142
|
return {
|
|
341
143
|
valid: false,
|
|
342
|
-
error: `Insufficient balance for deposit operation.
|
|
144
|
+
error: `Insufficient balance for deposit operation. ${debugParams}`,
|
|
343
145
|
};
|
|
344
146
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
// For redeem, we need to check if user has enough vault shares
|
|
348
|
-
if (vaultSharesBN === 0n) {
|
|
147
|
+
// Check if the user has approved vault to take his tokens
|
|
148
|
+
if (allowance.lt(amount)) {
|
|
349
149
|
return {
|
|
350
150
|
valid: false,
|
|
351
|
-
error:
|
|
151
|
+
error: `Insufficient allowance for deposit operation. Please approve vault to take your tokens first. ${debugParams}`,
|
|
352
152
|
};
|
|
353
153
|
}
|
|
354
|
-
|
|
355
|
-
|
|
154
|
+
break;
|
|
155
|
+
case schemas_1.MorphoOperation.REDEEM:
|
|
156
|
+
// Check if the user can take enough vault shares
|
|
157
|
+
if (vaultShares.lt(amount)) {
|
|
356
158
|
return {
|
|
357
159
|
valid: false,
|
|
358
|
-
error: `Insufficient vault shares for redeem operation.
|
|
160
|
+
error: `Insufficient vault shares for redeem operation. ${debugParams}`,
|
|
359
161
|
};
|
|
360
162
|
}
|
|
361
163
|
break;
|
|
@@ -364,179 +166,69 @@ async function validateOperationRequirements(operation, userBalance, vaultShares
|
|
|
364
166
|
}
|
|
365
167
|
return { valid: true };
|
|
366
168
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
},
|
|
383
|
-
body: JSON.stringify({
|
|
384
|
-
query,
|
|
385
|
-
variables,
|
|
386
|
-
}),
|
|
387
|
-
});
|
|
388
|
-
if (!response.ok) {
|
|
389
|
-
const body = await response.text();
|
|
390
|
-
throw new Error(`HTTP error! status: ${response.status} and body: ${body}`);
|
|
391
|
-
}
|
|
392
|
-
const data = await response.json();
|
|
393
|
-
if (data.errors) {
|
|
394
|
-
throw new Error(`GraphQL error: ${data.errors.map((e) => e.message).join(', ')}`);
|
|
395
|
-
}
|
|
396
|
-
return data.data;
|
|
397
|
-
}
|
|
398
|
-
catch (error) {
|
|
399
|
-
console.error('Failed to fetch vault data:', error);
|
|
400
|
-
throw error;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Get all vaults with comprehensive information
|
|
405
|
-
* Now uses proper server-side filtering via GraphQL VaultFilters
|
|
406
|
-
*/
|
|
407
|
-
async getAllVaults(options = {}) {
|
|
408
|
-
// Build GraphQL where clause from options
|
|
409
|
-
const whereClause = this.buildVaultFilters(options);
|
|
410
|
-
const query = `
|
|
411
|
-
query GetAllVaults($first: Int, $orderBy: VaultOrderBy, $orderDirection: OrderDirection, $where: VaultFilters) {
|
|
412
|
-
vaults(first: $first, orderBy: $orderBy, orderDirection: $orderDirection, where: $where) {
|
|
413
|
-
items {
|
|
414
|
-
address
|
|
415
|
-
name
|
|
416
|
-
symbol
|
|
417
|
-
whitelisted
|
|
418
|
-
creationTimestamp
|
|
419
|
-
asset {
|
|
420
|
-
address
|
|
421
|
-
symbol
|
|
422
|
-
name
|
|
423
|
-
decimals
|
|
424
|
-
}
|
|
425
|
-
chain {
|
|
426
|
-
id
|
|
427
|
-
network
|
|
428
|
-
}
|
|
429
|
-
state {
|
|
430
|
-
apy
|
|
431
|
-
netApy
|
|
432
|
-
totalAssets
|
|
433
|
-
totalAssetsUsd
|
|
434
|
-
fee
|
|
435
|
-
rewards {
|
|
436
|
-
asset {
|
|
437
|
-
address
|
|
438
|
-
symbol
|
|
439
|
-
}
|
|
440
|
-
supplyApr
|
|
441
|
-
yearlySupplyTokens
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
`;
|
|
448
|
-
// Fetch more results than requested to account for client-side filtering
|
|
449
|
-
// If excludeIdle is true, we might need to filter some out, so fetch extra
|
|
450
|
-
// But never exceed 1000 (GraphQL API limit)
|
|
451
|
-
const calculateFetchLimit = (requestedLimit) => {
|
|
452
|
-
if (!requestedLimit)
|
|
453
|
-
return 100; // Default limit
|
|
454
|
-
// Always cap at 1000 to respect GraphQL API limits
|
|
455
|
-
const cappedLimit = Math.min(requestedLimit, 1000);
|
|
456
|
-
if (options.excludeIdle) {
|
|
457
|
-
// For excludeIdle filtering, fetch extra but never exceed 1000
|
|
458
|
-
return Math.max(cappedLimit, 1000);
|
|
459
|
-
}
|
|
460
|
-
// No client-side filtering, use requested limit (capped at 1000)
|
|
461
|
-
return cappedLimit;
|
|
462
|
-
};
|
|
463
|
-
const fetchLimit = calculateFetchLimit(options.limit);
|
|
464
|
-
const variables = {
|
|
465
|
-
first: fetchLimit,
|
|
466
|
-
orderBy: this.mapSortBy(options.sortBy || 'totalAssetsUsd'),
|
|
467
|
-
orderDirection: options.sortOrder === 'asc' ? 'Asc' : 'Desc',
|
|
468
|
-
where: whereClause,
|
|
469
|
-
};
|
|
470
|
-
const data = await this.fetchVaultData(query, variables);
|
|
471
|
-
const vaults = data.vaults.items.map((vault) => this.mapVaultData(vault));
|
|
472
|
-
// console.log("vaults after server-side filtering", vaults.length);
|
|
473
|
-
// Apply only remaining client-side filters not supported by GraphQL
|
|
474
|
-
const filtered = this.applyRemainingClientFilters(vaults, options);
|
|
475
|
-
// console.log("vaults after additional client filtering", filtered.length);
|
|
476
|
-
// Apply the limit AFTER client-side filtering to ensure we get the expected number of results
|
|
477
|
-
const finalResults = options.limit ? filtered.slice(0, options.limit) : filtered;
|
|
478
|
-
// Log a warning if we couldn't fetch enough results due to API limits
|
|
479
|
-
if (options.limit && options.limit > 1000 && finalResults.length < options.limit) {
|
|
480
|
-
console.warn(`Warning: Requested ${options.limit} vaults but GraphQL API limit is 1000. Got ${finalResults.length} results.`);
|
|
169
|
+
async function fetchVaultData(query, variables) {
|
|
170
|
+
try {
|
|
171
|
+
const response = await fetch('https://blue-api.morpho.org/graphql', {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: {
|
|
174
|
+
'Content-Type': 'application/json',
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
query,
|
|
178
|
+
variables,
|
|
179
|
+
}),
|
|
180
|
+
});
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
const body = await response.text();
|
|
183
|
+
throw new Error(`HTTP error! status: ${response.status} and body: ${body}`);
|
|
481
184
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
* Unified function to get vaults with flexible filtering
|
|
486
|
-
* Supports filtering by asset, chain, and all other options
|
|
487
|
-
*/
|
|
488
|
-
async getVaults(options = {}) {
|
|
489
|
-
// If specific asset or chain filters are provided, enhance the options
|
|
490
|
-
const enhancedOptions = { ...options };
|
|
491
|
-
// Handle chain filtering - support both chainId and chain name/ID
|
|
492
|
-
if (options.chainId) {
|
|
493
|
-
enhancedOptions.chain = options.chainId;
|
|
185
|
+
const data = await response.json();
|
|
186
|
+
if (data.errors) {
|
|
187
|
+
throw new Error(`GraphQL error: ${data.errors.map((e) => e.message).join(', ')}`);
|
|
494
188
|
}
|
|
495
|
-
return
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Get top vaults by APY
|
|
499
|
-
*/
|
|
500
|
-
async getTopVaultsByNetApy(limit = 10, minTvl = 0) {
|
|
501
|
-
return this.getAllVaults({
|
|
502
|
-
sortBy: 'netApy',
|
|
503
|
-
sortOrder: 'desc',
|
|
504
|
-
limit,
|
|
505
|
-
minTvl,
|
|
506
|
-
excludeIdle: true,
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
* Get top vaults by TVL
|
|
511
|
-
*/
|
|
512
|
-
async getTopVaultsByTvl(limit = 10) {
|
|
513
|
-
return this.getAllVaults({
|
|
514
|
-
sortBy: 'totalAssetsUsd',
|
|
515
|
-
sortOrder: 'desc',
|
|
516
|
-
limit,
|
|
517
|
-
excludeIdle: true,
|
|
518
|
-
});
|
|
189
|
+
return data.data;
|
|
519
190
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
async searchVaults(searchOptions) {
|
|
524
|
-
const allVaults = await this.getAllVaults({ limit: 500 }); // Reduced to avoid GraphQL limit issues
|
|
525
|
-
if (!searchOptions.query) {
|
|
526
|
-
return allVaults.slice(0, searchOptions.limit || 50);
|
|
527
|
-
}
|
|
528
|
-
const query = searchOptions.query.toLowerCase();
|
|
529
|
-
const filtered = allVaults.filter((vault) => vault.name.toLowerCase().includes(query) ||
|
|
530
|
-
vault.symbol.toLowerCase().includes(query) ||
|
|
531
|
-
vault.asset.symbol.toLowerCase().includes(query) ||
|
|
532
|
-
vault.asset.name.toLowerCase().includes(query));
|
|
533
|
-
return filtered.slice(0, searchOptions.limit || 50);
|
|
191
|
+
catch (error) {
|
|
192
|
+
console.error('Failed to fetch vault data:', error);
|
|
193
|
+
throw error;
|
|
534
194
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
195
|
+
}
|
|
196
|
+
function mapVaultData(vault) {
|
|
197
|
+
return {
|
|
198
|
+
address: vault.address,
|
|
199
|
+
name: vault.name,
|
|
200
|
+
symbol: vault.symbol,
|
|
201
|
+
asset: {
|
|
202
|
+
address: vault.asset.address,
|
|
203
|
+
symbol: vault.asset.symbol,
|
|
204
|
+
name: vault.asset.name,
|
|
205
|
+
decimals: vault.asset.decimals,
|
|
206
|
+
},
|
|
207
|
+
chain: {
|
|
208
|
+
id: vault.chain.id,
|
|
209
|
+
network: vault.chain.network,
|
|
210
|
+
},
|
|
211
|
+
metrics: {
|
|
212
|
+
apy: vault.state.apy || 0,
|
|
213
|
+
netApy: vault.state.netApy || 0,
|
|
214
|
+
totalAssets: vault.state.totalAssets || '0',
|
|
215
|
+
totalAssetsUsd: vault.state.totalAssetsUsd || 0,
|
|
216
|
+
fee: vault.state.fee || 0,
|
|
217
|
+
rewards: vault.state.rewards?.map((reward) => ({
|
|
218
|
+
asset: reward.asset.address,
|
|
219
|
+
supplyApr: reward.supplyApr,
|
|
220
|
+
yearlySupplyTokens: reward.yearlySupplyTokens,
|
|
221
|
+
})) || [],
|
|
222
|
+
},
|
|
223
|
+
whitelisted: vault.whitelisted,
|
|
224
|
+
creationTimestamp: vault.creationTimestamp,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Get Morpho vault details using their graphql api
|
|
229
|
+
*/
|
|
230
|
+
async function getMorphoVaultByAddress(address, chainId) {
|
|
231
|
+
const query = `
|
|
540
232
|
query GetVaultByAddress($address: String!, $chainId: Int!) {
|
|
541
233
|
vaultByAddress(address: $address, chainId: $chainId) {
|
|
542
234
|
address
|
|
@@ -572,542 +264,28 @@ class MorphoVaultClient {
|
|
|
572
264
|
}
|
|
573
265
|
}
|
|
574
266
|
`;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
return data.vaultByAddress ? this.mapVaultData(data.vaultByAddress) : null;
|
|
579
|
-
}
|
|
580
|
-
catch (error) {
|
|
581
|
-
console.error(`Failed to fetch vault ${address}:`, error);
|
|
582
|
-
return null;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
* Get best vaults for a specific asset
|
|
587
|
-
*/
|
|
588
|
-
async getBestVaultsForAsset(assetSymbol, limit = 5) {
|
|
589
|
-
const vaults = await this.getAllVaults({
|
|
590
|
-
sortBy: 'netApy',
|
|
591
|
-
sortOrder: 'desc',
|
|
592
|
-
limit: 100,
|
|
593
|
-
minTvl: 10000, // Minimum $10k TVL
|
|
594
|
-
excludeIdle: true,
|
|
595
|
-
});
|
|
596
|
-
return vaults
|
|
597
|
-
.filter((vault) => vault.asset.symbol.toLowerCase() === assetSymbol.toLowerCase())
|
|
598
|
-
.slice(0, limit);
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* Map vault data from GraphQL response
|
|
602
|
-
*/
|
|
603
|
-
mapVaultData(vault) {
|
|
604
|
-
return {
|
|
605
|
-
address: vault.address,
|
|
606
|
-
name: vault.name,
|
|
607
|
-
symbol: vault.symbol,
|
|
608
|
-
asset: {
|
|
609
|
-
address: vault.asset.address,
|
|
610
|
-
symbol: vault.asset.symbol,
|
|
611
|
-
name: vault.asset.name,
|
|
612
|
-
decimals: vault.asset.decimals,
|
|
613
|
-
},
|
|
614
|
-
chain: {
|
|
615
|
-
id: vault.chain.id,
|
|
616
|
-
network: vault.chain.network,
|
|
617
|
-
},
|
|
618
|
-
metrics: {
|
|
619
|
-
apy: vault.state.apy || 0,
|
|
620
|
-
netApy: vault.state.netApy || 0,
|
|
621
|
-
totalAssets: vault.state.totalAssets || '0',
|
|
622
|
-
totalAssetsUsd: vault.state.totalAssetsUsd || 0,
|
|
623
|
-
fee: vault.state.fee || 0,
|
|
624
|
-
rewards: vault.state.rewards?.map((reward) => ({
|
|
625
|
-
asset: reward.asset.address,
|
|
626
|
-
supplyApr: reward.supplyApr,
|
|
627
|
-
yearlySupplyTokens: reward.yearlySupplyTokens,
|
|
628
|
-
})) || [],
|
|
629
|
-
},
|
|
630
|
-
whitelisted: vault.whitelisted,
|
|
631
|
-
creationTimestamp: vault.creationTimestamp,
|
|
632
|
-
isIdle: vault.state.totalAssetsUsd < 100, // Consider vaults with < $100 TVL as idle
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Build GraphQL VaultFilters from filter options
|
|
637
|
-
* Uses proper server-side filtering for better performance
|
|
638
|
-
*/
|
|
639
|
-
buildVaultFilters(options) {
|
|
640
|
-
const filters = {};
|
|
641
|
-
// Chain filtering - server-side supported
|
|
642
|
-
if (options.chain !== undefined || options.chainId !== undefined) {
|
|
643
|
-
let targetChainId;
|
|
644
|
-
if (options.chainId !== undefined) {
|
|
645
|
-
targetChainId = options.chainId;
|
|
646
|
-
}
|
|
647
|
-
else if (options.chain !== undefined) {
|
|
648
|
-
targetChainId =
|
|
649
|
-
typeof options.chain === 'string'
|
|
650
|
-
? exports.CHAIN_IDS[options.chain]
|
|
651
|
-
: options.chain;
|
|
652
|
-
}
|
|
653
|
-
if (targetChainId !== undefined) {
|
|
654
|
-
filters.chainId_in = [targetChainId];
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
// Asset filtering - server-side supported
|
|
658
|
-
if (options.assetAddress) {
|
|
659
|
-
filters.assetAddress_in = [options.assetAddress.toLowerCase()];
|
|
660
|
-
}
|
|
661
|
-
if (options.assetSymbol) {
|
|
662
|
-
filters.assetSymbol_in = [options.assetSymbol.toUpperCase()];
|
|
663
|
-
}
|
|
664
|
-
// Whitelisted status filtering - server-side supported
|
|
665
|
-
if (options.whitelistedOnly) {
|
|
666
|
-
filters.whitelisted = true;
|
|
667
|
-
}
|
|
668
|
-
// Net APY filtering - server-side supported
|
|
669
|
-
if (options.minNetApy !== undefined) {
|
|
670
|
-
filters.netApy_gte = options.minNetApy;
|
|
671
|
-
}
|
|
672
|
-
if (options.maxNetApy !== undefined) {
|
|
673
|
-
filters.netApy_lte = options.maxNetApy;
|
|
674
|
-
}
|
|
675
|
-
// TVL filtering - server-side supported
|
|
676
|
-
if (options.minTvl !== undefined) {
|
|
677
|
-
filters.totalAssetsUsd_gte = options.minTvl;
|
|
678
|
-
}
|
|
679
|
-
if (options.maxTvl !== undefined) {
|
|
680
|
-
filters.totalAssetsUsd_lte = options.maxTvl;
|
|
681
|
-
}
|
|
682
|
-
// Total assets filtering - server-side supported
|
|
683
|
-
if (options.minTotalAssets !== undefined) {
|
|
684
|
-
filters.totalAssets_gte = options.minTotalAssets.toString();
|
|
685
|
-
}
|
|
686
|
-
if (options.maxTotalAssets !== undefined) {
|
|
687
|
-
filters.totalAssets_lte = options.maxTotalAssets.toString();
|
|
688
|
-
}
|
|
689
|
-
// Return null if no filters to avoid empty where clause
|
|
690
|
-
return Object.keys(filters).length > 0 ? filters : null;
|
|
691
|
-
}
|
|
692
|
-
/**
|
|
693
|
-
* Apply remaining client-side filters not supported by GraphQL
|
|
694
|
-
* Only handles computed properties like isIdle
|
|
695
|
-
*/
|
|
696
|
-
applyRemainingClientFilters(vaults, options) {
|
|
697
|
-
let filtered = vaults;
|
|
698
|
-
// Idle vault filtering (computed client-side)
|
|
699
|
-
if (options.excludeIdle) {
|
|
700
|
-
filtered = filtered.filter((vault) => !vault.isIdle);
|
|
701
|
-
}
|
|
702
|
-
return filtered;
|
|
703
|
-
}
|
|
704
|
-
/**
|
|
705
|
-
* Map sortBy option to GraphQL enum
|
|
706
|
-
*/
|
|
707
|
-
mapSortBy(sortBy) {
|
|
708
|
-
switch (sortBy) {
|
|
709
|
-
case 'netApy':
|
|
710
|
-
return 'NetApy';
|
|
711
|
-
case 'totalAssets':
|
|
712
|
-
return 'TotalAssets';
|
|
713
|
-
case 'totalAssetsUsd':
|
|
714
|
-
return 'TotalAssetsUsd';
|
|
715
|
-
case 'creationTimestamp':
|
|
716
|
-
return 'CreationTimestamp';
|
|
717
|
-
default:
|
|
718
|
-
return 'TotalAssetsUsd';
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
exports.MorphoVaultClient = MorphoVaultClient;
|
|
723
|
-
/**
|
|
724
|
-
* Create a singleton instance of MorphoVaultClient
|
|
725
|
-
*/
|
|
726
|
-
exports.morphoVaultClient = new MorphoVaultClient();
|
|
727
|
-
/**
|
|
728
|
-
* Helper function to get best vaults for a specific asset
|
|
729
|
-
*/
|
|
730
|
-
async function getBestVaultsForAsset(assetSymbol, limit = 5) {
|
|
731
|
-
return exports.morphoVaultClient.getBestVaultsForAsset(assetSymbol, limit);
|
|
732
|
-
}
|
|
733
|
-
/**
|
|
734
|
-
* Helper function to get top vaults by APY
|
|
735
|
-
*/
|
|
736
|
-
async function getTopVaultsByNetApy(limit = 10, minTvl = 10000) {
|
|
737
|
-
return exports.morphoVaultClient.getTopVaultsByNetApy(limit, minTvl);
|
|
738
|
-
}
|
|
739
|
-
/**
|
|
740
|
-
* Helper function to get top vaults by TVL
|
|
741
|
-
*/
|
|
742
|
-
async function getTopVaultsByTvl(limit = 10) {
|
|
743
|
-
return exports.morphoVaultClient.getTopVaultsByTvl(limit);
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* Helper function to search vaults
|
|
747
|
-
*/
|
|
748
|
-
async function searchVaults(query, limit = 20) {
|
|
749
|
-
return exports.morphoVaultClient.searchVaults({ query, limit });
|
|
750
|
-
}
|
|
751
|
-
/**
|
|
752
|
-
* 🚀 **Quick Vault Search with Presets**
|
|
753
|
-
*
|
|
754
|
-
* Get vaults using pre-configured filter presets for common use cases.
|
|
755
|
-
*
|
|
756
|
-
* @param preset - Pre-configured filter preset
|
|
757
|
-
* @param overrides - Additional options to override preset defaults
|
|
758
|
-
* @returns Promise resolving to array of vault information
|
|
759
|
-
*
|
|
760
|
-
* @example
|
|
761
|
-
* ```typescript
|
|
762
|
-
* // Find high-yield vaults
|
|
763
|
-
* const highYieldVaults = await getVaultsByPreset("highYield");
|
|
764
|
-
*
|
|
765
|
-
* // Find high-yield USDC vaults specifically
|
|
766
|
-
* const usdcHighYield = await getVaultsByPreset("highYield", {
|
|
767
|
-
* assetSymbol: "USDC"
|
|
768
|
-
* });
|
|
769
|
-
*
|
|
770
|
-
* // Find stable vaults on Base chain
|
|
771
|
-
* const stableBaseVaults = await getVaultsByPreset("stable", {
|
|
772
|
-
* chainId: 8453
|
|
773
|
-
* });
|
|
774
|
-
* ```
|
|
775
|
-
*/
|
|
776
|
-
async function getVaultsByPreset(preset, overrides = {}) {
|
|
777
|
-
const presetOptions = exports.VAULT_FILTER_PRESETS[preset];
|
|
778
|
-
const mergedOptions = { ...presetOptions, ...overrides };
|
|
779
|
-
return getVaults(mergedOptions);
|
|
780
|
-
}
|
|
781
|
-
/**
|
|
782
|
-
* 🔍 **Primary Vault Discovery Function**
|
|
783
|
-
*
|
|
784
|
-
* Get Morpho vaults with comprehensive filtering and sorting options.
|
|
785
|
-
* Uses server-side GraphQL queries for optimal performance.
|
|
786
|
-
*
|
|
787
|
-
* @param options - Vault filtering and sorting options
|
|
788
|
-
* @returns Promise resolving to array of vault information
|
|
789
|
-
*
|
|
790
|
-
* @example
|
|
791
|
-
* ```typescript
|
|
792
|
-
* // Find best USDC vaults across all chains
|
|
793
|
-
* const topVaults = await getVaults({
|
|
794
|
-
* assetSymbol: "USDC",
|
|
795
|
-
* minNetApy: 0.05,
|
|
796
|
-
* minTvl: 1000000,
|
|
797
|
-
* sortBy: "netApy",
|
|
798
|
-
* sortOrder: "desc",
|
|
799
|
-
* limit: 5
|
|
800
|
-
* });
|
|
801
|
-
*
|
|
802
|
-
* // Filter by specific chain
|
|
803
|
-
* const baseVaults = await getVaults({
|
|
804
|
-
* chainId: 8453, // Base
|
|
805
|
-
* excludeIdle: true,
|
|
806
|
-
* sortBy: "totalAssetsUsd"
|
|
807
|
-
* });
|
|
808
|
-
*
|
|
809
|
-
* // Search with multiple criteria
|
|
810
|
-
* const premiumVaults = await getVaults({
|
|
811
|
-
* minNetApy: 10.0,
|
|
812
|
-
* minTvl: 5000000,
|
|
813
|
-
* whitelistedOnly: true,
|
|
814
|
-
* sortBy: "netApy",
|
|
815
|
-
* limit: 3
|
|
816
|
-
* });
|
|
817
|
-
* ```
|
|
818
|
-
*/
|
|
819
|
-
async function getVaults(options = {}) {
|
|
820
|
-
return exports.morphoVaultClient.getVaults(options);
|
|
821
|
-
}
|
|
822
|
-
/**
|
|
823
|
-
* Get supported chains with active vaults
|
|
824
|
-
*/
|
|
825
|
-
async function getSupportedChainsWithVaults() {
|
|
826
|
-
const supportedChains = getSupportedChainIds();
|
|
827
|
-
const results = [];
|
|
828
|
-
for (const chainId of supportedChains) {
|
|
829
|
-
try {
|
|
830
|
-
const vaults = await exports.morphoVaultClient.getVaults({
|
|
831
|
-
chainId,
|
|
832
|
-
limit: 1,
|
|
833
|
-
excludeIdle: true,
|
|
834
|
-
});
|
|
835
|
-
if (vaults.length > 0) {
|
|
836
|
-
// Get total count - reduced limit to avoid GraphQL errors
|
|
837
|
-
const allVaults = await exports.morphoVaultClient.getVaults({
|
|
838
|
-
chainId,
|
|
839
|
-
limit: 500, // Reduced to avoid GraphQL limit issues
|
|
840
|
-
excludeIdle: true,
|
|
841
|
-
});
|
|
842
|
-
results.push({
|
|
843
|
-
chainId,
|
|
844
|
-
name: getChainName(chainId),
|
|
845
|
-
vaultCount: allVaults.length,
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
catch (error) {
|
|
850
|
-
console.warn(`Could not fetch vaults for chain ${chainId}:`, error instanceof Error ? error.message : String(error));
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
return results.sort((a, b) => b.vaultCount - a.vaultCount);
|
|
854
|
-
}
|
|
855
|
-
/**
|
|
856
|
-
* Get vault discovery summary for a chain
|
|
857
|
-
*/
|
|
858
|
-
async function getVaultDiscoverySummary(chainId) {
|
|
859
|
-
try {
|
|
860
|
-
const [topByTvl, topByNetApy, assetBreakdown] = await Promise.all([
|
|
861
|
-
exports.morphoVaultClient.getVaults({
|
|
862
|
-
chainId,
|
|
863
|
-
sortBy: 'totalAssetsUsd',
|
|
864
|
-
sortOrder: 'desc',
|
|
865
|
-
limit: 5,
|
|
866
|
-
excludeIdle: true,
|
|
867
|
-
}),
|
|
868
|
-
exports.morphoVaultClient.getVaults({
|
|
869
|
-
chainId,
|
|
870
|
-
sortBy: 'netApy',
|
|
871
|
-
sortOrder: 'desc',
|
|
872
|
-
limit: 5,
|
|
873
|
-
excludeIdle: true,
|
|
874
|
-
}),
|
|
875
|
-
exports.morphoVaultClient.getVaults({
|
|
876
|
-
chainId,
|
|
877
|
-
limit: 500, // Reduced to avoid GraphQL limit issues
|
|
878
|
-
excludeIdle: true,
|
|
879
|
-
}),
|
|
880
|
-
]);
|
|
881
|
-
// Group by asset
|
|
882
|
-
const assetGroups = assetBreakdown.reduce((acc, vault) => {
|
|
883
|
-
const symbol = vault.asset.symbol;
|
|
884
|
-
if (!acc[symbol]) {
|
|
885
|
-
acc[symbol] = { count: 0, totalTvl: 0, maxNetApy: 0 };
|
|
886
|
-
}
|
|
887
|
-
acc[symbol].count++;
|
|
888
|
-
acc[symbol].totalTvl += vault.metrics.totalAssetsUsd;
|
|
889
|
-
acc[symbol].maxNetApy = Math.max(acc[symbol].maxNetApy, vault.metrics.netApy);
|
|
890
|
-
return acc;
|
|
891
|
-
}, {});
|
|
892
|
-
return {
|
|
893
|
-
chainId,
|
|
894
|
-
chainName: getChainName(chainId),
|
|
895
|
-
totalVaults: assetBreakdown.length,
|
|
896
|
-
totalTvl: assetBreakdown.reduce((sum, v) => sum + v.metrics.totalAssetsUsd, 0),
|
|
897
|
-
topVaultsByTvl: topByTvl,
|
|
898
|
-
topVaultsByNetApy: topByNetApy,
|
|
899
|
-
assetBreakdown: Object.entries(assetGroups)
|
|
900
|
-
.map(([symbol, data]) => ({ symbol, ...data }))
|
|
901
|
-
.sort((a, b) => b.totalTvl - a.totalTvl),
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
catch (error) {
|
|
905
|
-
console.error(`Error getting vault summary for chain ${chainId}:`, error);
|
|
906
|
-
throw error;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
const N = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141');
|
|
910
|
-
const N2 = N >> 1n;
|
|
911
|
-
function add0x(h) {
|
|
912
|
-
return h.startsWith('0x') ? h : '0x' + h;
|
|
913
|
-
}
|
|
914
|
-
function to32Bytes(h) {
|
|
915
|
-
const bytes = ethers_1.ethers.utils.arrayify(add0x(h));
|
|
916
|
-
if (bytes.length === 32) {
|
|
917
|
-
return ethers_1.ethers.utils.hexlify(bytes);
|
|
918
|
-
}
|
|
919
|
-
// Lit sometimes prefixes a single "tag" byte. If so, drop it.
|
|
920
|
-
if (bytes.length === 33 && (bytes[0] === 0x00 || bytes[0] === 0x02 || bytes[0] === 0x03)) {
|
|
921
|
-
return ethers_1.ethers.utils.hexlify(bytes.slice(1));
|
|
922
|
-
}
|
|
923
|
-
// Left-pad if < 32
|
|
924
|
-
if (bytes.length < 32) {
|
|
925
|
-
const out = new Uint8Array(32);
|
|
926
|
-
out.set(bytes, 32 - bytes.length);
|
|
927
|
-
return ethers_1.ethers.utils.hexlify(out);
|
|
928
|
-
}
|
|
929
|
-
throw new Error(`Invalid 32-byte value length=${bytes.length} for ${h}`);
|
|
930
|
-
}
|
|
931
|
-
function normalizeLitSig(parsed) {
|
|
932
|
-
const r = to32Bytes(parsed.r);
|
|
933
|
-
let s = to32Bytes(parsed.s);
|
|
934
|
-
let v = parsed.v;
|
|
935
|
-
// map 0/1 -> 27/28 (ethers v5 wants that for many contracts)
|
|
936
|
-
if (v === 0 || v === 1)
|
|
937
|
-
v += 27;
|
|
938
|
-
// low-s normalization (OpenZeppelin ECDSA-compatible)
|
|
939
|
-
let sBN = BigInt(s);
|
|
940
|
-
if (sBN > N2) {
|
|
941
|
-
sBN = N - sBN;
|
|
942
|
-
s = '0x' + sBN.toString(16).padStart(64, '0');
|
|
943
|
-
v = v === 27 ? 28 : 27; // flip parity
|
|
944
|
-
}
|
|
945
|
-
return { r, s, v };
|
|
946
|
-
}
|
|
947
|
-
async function buildDepositTxData({ bundlerStepsLimit, skipRevertOnPermit, amount, pkpAddress, pkpPublicKey, provider, tokenAddress, vaultAddress, }) {
|
|
948
|
-
const { chainId } = await provider.getNetwork();
|
|
949
|
-
const bundlerAddress = exports.BUNDLER_ADDRESSES[chainId];
|
|
950
|
-
if (!bundlerAddress) {
|
|
951
|
-
throw new Error(`No bundler address found for chain ${chainId}`);
|
|
952
|
-
}
|
|
953
|
-
console.log('[@lit-protocol/vincent-ability-morpho/buildDepositTxData] Building EIP-2612 permit data in runOnce');
|
|
954
|
-
const permitDataResponse = await Lit.Actions.runOnce({ waitForResponse: true, name: 'permitData' }, async () => {
|
|
955
|
-
const tokenContract = new ethers_1.ethers.Contract(tokenAddress, exports.ERC20_ABI, provider);
|
|
956
|
-
const [name, nonce, version] = await Promise.all([
|
|
957
|
-
tokenContract.name(),
|
|
958
|
-
tokenContract.nonces(pkpAddress),
|
|
959
|
-
tokenContract.version(),
|
|
960
|
-
]);
|
|
961
|
-
const deadline = Math.floor(Date.now() / 1000) + 60 * 30; // 30 min
|
|
962
|
-
// EIP-2612 permit signature
|
|
963
|
-
const domain = {
|
|
964
|
-
name,
|
|
965
|
-
version,
|
|
966
|
-
chainId,
|
|
967
|
-
verifyingContract: tokenAddress,
|
|
968
|
-
};
|
|
969
|
-
const message = {
|
|
970
|
-
owner: pkpAddress,
|
|
971
|
-
spender: bundlerAddress,
|
|
972
|
-
value: amount,
|
|
973
|
-
nonce: nonce.toString(),
|
|
974
|
-
deadline,
|
|
975
|
-
};
|
|
976
|
-
console.log('[@lit-protocol/vincent-ability-morpho/buildDepositTxData] Built EIP-2612 permit properties', {
|
|
977
|
-
domain,
|
|
978
|
-
message,
|
|
979
|
-
});
|
|
980
|
-
return JSON.stringify({
|
|
981
|
-
deadline,
|
|
982
|
-
domain,
|
|
983
|
-
message,
|
|
984
|
-
});
|
|
985
|
-
});
|
|
986
|
-
if (!permitDataResponse ||
|
|
987
|
-
permitDataResponse[0] !== '{' ||
|
|
988
|
-
permitDataResponse.includes('error')) {
|
|
989
|
-
throw new Error(`permitData runOnce failed: ${permitDataResponse}`);
|
|
990
|
-
}
|
|
991
|
-
const { deadline, domain, message } = JSON.parse(permitDataResponse);
|
|
992
|
-
const types = {
|
|
993
|
-
Permit: [
|
|
994
|
-
{ name: 'owner', type: 'address' },
|
|
995
|
-
{ name: 'spender', type: 'address' },
|
|
996
|
-
{ name: 'value', type: 'uint256' },
|
|
997
|
-
{ name: 'nonce', type: 'uint256' },
|
|
998
|
-
{ name: 'deadline', type: 'uint256' },
|
|
999
|
-
],
|
|
1000
|
-
};
|
|
1001
|
-
const digest = ethers_1.ethers.utils._TypedDataEncoder.hash(domain, types, message);
|
|
1002
|
-
const toSign = ethers_1.ethers.utils.arrayify(digest);
|
|
1003
|
-
console.log('[@lit-protocol/vincent-ability-morpho/buildDepositTxData] Built EIP-2612 permit msg', {
|
|
1004
|
-
toSign,
|
|
1005
|
-
});
|
|
1006
|
-
const publicKey = pkpPublicKey.replace(/^0x/, '');
|
|
1007
|
-
const permitSignatureString = await Lit.Actions.signAndCombineEcdsa({
|
|
1008
|
-
publicKey,
|
|
1009
|
-
toSign,
|
|
1010
|
-
sigName: 'tokenPermit',
|
|
1011
|
-
});
|
|
1012
|
-
console.log('[@lit-protocol/vincent-ability-morpho/buildDepositTxData] Permit signature', {
|
|
1013
|
-
permitSignatureString,
|
|
1014
|
-
});
|
|
1015
|
-
const permitSignatureParsed = JSON.parse(permitSignatureString);
|
|
1016
|
-
const { r, s, v } = normalizeLitSig(permitSignatureParsed);
|
|
1017
|
-
console.log('[@lit-protocol/vincent-ability-morpho/buildDepositTxData] Parsed permit signature', {
|
|
1018
|
-
r,
|
|
1019
|
-
s,
|
|
1020
|
-
v,
|
|
1021
|
-
});
|
|
1022
|
-
// Signature sanity check
|
|
1023
|
-
const recovered = ethers_1.ethers.utils.recoverAddress(digest, { r, s, v });
|
|
1024
|
-
if (recovered.toLowerCase() !== pkpAddress.toLowerCase()) {
|
|
1025
|
-
throw new Error(`Permit sig does not recover to owner: ${recovered} != ${pkpAddress}`);
|
|
1026
|
-
}
|
|
1027
|
-
// Permit sanity check
|
|
1028
|
-
try {
|
|
1029
|
-
const tokenContract = new ethers_1.ethers.Contract(tokenAddress, [
|
|
1030
|
-
'function nonces(address) view returns (uint256)',
|
|
1031
|
-
'function permit(address owner,address spender,uint256 value,uint256 deadline,uint8 v,bytes32 r,bytes32 s)',
|
|
1032
|
-
], provider);
|
|
1033
|
-
await tokenContract.callStatic.permit(pkpAddress, bundlerAddress, amount, deadline, v, r, s);
|
|
1034
|
-
}
|
|
1035
|
-
catch (error) {
|
|
1036
|
-
console.error(`[@lit-protocol/vincent-ability-morpho/buildDepositTxData] Permit sanity check failed:`, error);
|
|
1037
|
-
throw new Error(`Permit sanity check failed: ${error}`);
|
|
1038
|
-
}
|
|
1039
|
-
console.log('[@lit-protocol/vincent-ability-morpho/buildDepositTxData] Building bundler calls');
|
|
1040
|
-
const calls = [
|
|
1041
|
-
// @ts-expect-error permit expects a full Signature but having v ∈ {27, 28} which is not what ethers gives us
|
|
1042
|
-
bundler_sdk_ethers_1.BundlerAction.permit(tokenAddress, amount, deadline, { r, s, v }, skipRevertOnPermit),
|
|
1043
|
-
bundler_sdk_ethers_1.BundlerAction.erc20TransferFrom(tokenAddress, amount),
|
|
1044
|
-
// Deposit into the ERC4626 vault (minShares = 0; set if you want protection)
|
|
1045
|
-
bundler_sdk_ethers_1.BundlerAction.erc4626Deposit(vaultAddress, amount, 0, pkpAddress),
|
|
1046
|
-
].slice(0, bundlerStepsLimit);
|
|
1047
|
-
console.log(`[@lit-protocol/vincent-ability-morpho/buildDepositTxData] Built calls:`, calls);
|
|
1048
|
-
const iface = new ethers_1.ethers.utils.Interface(exports.BUNDLER_ABI);
|
|
1049
|
-
const data = iface.encodeFunctionData('multicall', [calls]);
|
|
1050
|
-
console.log('[@lit-protocol/vincent-ability-morpho/buildDepositTxData] Built bundler encoded tx data', {
|
|
1051
|
-
data,
|
|
1052
|
-
});
|
|
1053
|
-
return {
|
|
1054
|
-
data,
|
|
1055
|
-
abi: exports.BUNDLER_ABI,
|
|
1056
|
-
functionName: 'multicall',
|
|
1057
|
-
args: [calls],
|
|
1058
|
-
to: bundlerAddress,
|
|
1059
|
-
value: '0x0',
|
|
1060
|
-
};
|
|
1061
|
-
}
|
|
1062
|
-
async function buildRedeemTxData({ bundlerStepsLimit, amount, pkpAddress, provider, vaultAddress, }) {
|
|
1063
|
-
const { chainId } = await provider.getNetwork();
|
|
1064
|
-
const bundlerAddress = exports.BUNDLER_ADDRESSES[chainId];
|
|
1065
|
-
if (!bundlerAddress) {
|
|
1066
|
-
throw new Error(`No bundler address found for chain ${chainId}`);
|
|
1067
|
-
}
|
|
1068
|
-
console.log('[@lit-protocol/vincent-ability-morpho/buildDepositTxData] Building bundler calls');
|
|
1069
|
-
const calls = [
|
|
1070
|
-
bundler_sdk_ethers_1.BundlerAction.erc4626Redeem(vaultAddress, amount, amount, pkpAddress, pkpAddress),
|
|
1071
|
-
].slice(0, bundlerStepsLimit);
|
|
1072
|
-
console.log(`[@lit-protocol/vincent-ability-morpho/buildDepositTxData] Built calls:`, calls);
|
|
1073
|
-
const iface = new ethers_1.ethers.utils.Interface(exports.BUNDLER_ABI);
|
|
1074
|
-
const data = iface.encodeFunctionData('multicall', [calls]);
|
|
1075
|
-
console.log('[@lit-protocol/vincent-ability-morpho/buildDepositTxData] Built bundler encoded tx data', {
|
|
1076
|
-
data,
|
|
1077
|
-
});
|
|
1078
|
-
return {
|
|
1079
|
-
data,
|
|
1080
|
-
abi: exports.BUNDLER_ABI,
|
|
1081
|
-
functionName: 'multicall',
|
|
1082
|
-
args: [calls],
|
|
1083
|
-
to: bundlerAddress,
|
|
1084
|
-
value: '0x0',
|
|
1085
|
-
};
|
|
267
|
+
const variables = { address, chainId };
|
|
268
|
+
const data = await fetchVaultData(query, variables);
|
|
269
|
+
return data.vaultByAddress ? mapVaultData(data.vaultByAddress) : null;
|
|
1086
270
|
}
|
|
1087
271
|
/**
|
|
1088
272
|
* Generic function to execute any Morpho operation, with optional gas sponsorship
|
|
1089
273
|
*/
|
|
1090
|
-
async function
|
|
274
|
+
async function executeMorphoOperation({ abi, args, alchemyGasSponsor = false, alchemyGasSponsorApiKey, alchemyGasSponsorPolicyId, contractAddress, chainId, functionName, provider, pkpInfo, }) {
|
|
1091
275
|
console.log(`[@lit-protocol/vincent-ability-morpho/executeMorphoOperation] Starting ${functionName} operation`, { sponsored: !!alchemyGasSponsor });
|
|
1092
276
|
// Use gas sponsorship if enabled and all required parameters are provided
|
|
1093
277
|
if (alchemyGasSponsor && alchemyGasSponsorApiKey && alchemyGasSponsorPolicyId) {
|
|
1094
278
|
console.log(`[@lit-protocol/vincent-ability-morpho/executeMorphoOperation] Using EIP-7702 gas sponsorship`, { contractAddress, functionName, args, policyId: alchemyGasSponsorPolicyId });
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
});
|
|
1106
|
-
}
|
|
1107
|
-
catch (error) {
|
|
1108
|
-
console.error(`[@lit-protocol/vincent-ability-morpho/executeMorphoOperation] EIP-7702 operation failed:`, error);
|
|
1109
|
-
throw error;
|
|
1110
|
-
}
|
|
279
|
+
return await vincent_scaffold_sdk_1.laUtils.transaction.handler.sponsoredGasContractCall({
|
|
280
|
+
abi,
|
|
281
|
+
args,
|
|
282
|
+
contractAddress,
|
|
283
|
+
chainId,
|
|
284
|
+
functionName,
|
|
285
|
+
eip7702AlchemyApiKey: alchemyGasSponsorApiKey,
|
|
286
|
+
eip7702AlchemyPolicyId: alchemyGasSponsorPolicyId,
|
|
287
|
+
pkpPublicKey: pkpInfo.publicKey,
|
|
288
|
+
});
|
|
1111
289
|
}
|
|
1112
290
|
else {
|
|
1113
291
|
// Use regular transaction without gas sponsorship
|
|
@@ -1115,22 +293,16 @@ async function executeChainOperation({ abi, provider, pkpEthAddress, pkpPublicKe
|
|
|
1115
293
|
if (!provider) {
|
|
1116
294
|
throw new Error('Provider is required for non-sponsored transactions');
|
|
1117
295
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
});
|
|
1129
|
-
}
|
|
1130
|
-
catch (error) {
|
|
1131
|
-
console.error(`[@lit-protocol/vincent-ability-morpho/executeMorphoOperation] Regular transaction failed:`, error);
|
|
1132
|
-
throw error;
|
|
1133
|
-
}
|
|
296
|
+
return await vincent_scaffold_sdk_1.laUtils.transaction.handler.contractCall({
|
|
297
|
+
abi,
|
|
298
|
+
args,
|
|
299
|
+
chainId,
|
|
300
|
+
contractAddress,
|
|
301
|
+
functionName,
|
|
302
|
+
provider,
|
|
303
|
+
callerAddress: pkpInfo.ethAddress,
|
|
304
|
+
pkpPublicKey: pkpInfo.publicKey,
|
|
305
|
+
});
|
|
1134
306
|
}
|
|
1135
307
|
}
|
|
1136
308
|
//# sourceMappingURL=index.js.map
|