@moonwell-fi/moonwell-sdk 0.24.0 → 0.25.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 +18 -0
- package/_cjs/actions/core/getUserBalances.js +63 -32
- package/_cjs/actions/core/getUserBalances.js.map +1 -1
- package/_cjs/actions/core/markets/common.js +1 -7
- package/_cjs/actions/core/markets/common.js.map +1 -1
- package/_cjs/actions/governance/getStakingInfo.js +1 -7
- package/_cjs/actions/governance/getStakingInfo.js.map +1 -1
- package/_cjs/actions/governance/getUserStakingInfo.js +1 -7
- package/_cjs/actions/governance/getUserStakingInfo.js.map +1 -1
- package/_cjs/actions/governance/getWellPrice.js +51 -1
- package/_cjs/actions/governance/getWellPrice.js.map +1 -1
- package/_cjs/actions/governance/proposals/common.js +34 -10
- package/_cjs/actions/governance/proposals/common.js.map +1 -1
- package/_cjs/actions/governance/proposals/getProposals.js +5 -4
- package/_cjs/actions/governance/proposals/getProposals.js.map +1 -1
- package/_cjs/actions/morpho/markets/common.js +29 -2
- package/_cjs/actions/morpho/markets/common.js.map +1 -1
- package/_cjs/actions/morpho/user-rewards/common.js +1 -7
- package/_cjs/actions/morpho/user-rewards/common.js.map +1 -1
- package/_cjs/actions/morpho/vaults/common.js +104 -87
- package/_cjs/actions/morpho/vaults/common.js.map +1 -1
- package/_cjs/actions/morpho/vaults/lunarIndexerTransform.js +31 -3
- package/_cjs/actions/morpho/vaults/lunarIndexerTransform.js.map +1 -1
- package/_cjs/environments/types/config.js.map +1 -1
- package/_cjs/errors/version.js +1 -1
- package/_esm/actions/core/getUserBalances.js +72 -35
- package/_esm/actions/core/getUserBalances.js.map +1 -1
- package/_esm/actions/core/markets/common.js +2 -8
- package/_esm/actions/core/markets/common.js.map +1 -1
- package/_esm/actions/governance/getStakingInfo.js +2 -8
- package/_esm/actions/governance/getStakingInfo.js.map +1 -1
- package/_esm/actions/governance/getUserStakingInfo.js +2 -8
- package/_esm/actions/governance/getUserStakingInfo.js.map +1 -1
- package/_esm/actions/governance/getWellPrice.js +67 -7
- package/_esm/actions/governance/getWellPrice.js.map +1 -1
- package/_esm/actions/governance/proposals/common.js +37 -10
- package/_esm/actions/governance/proposals/common.js.map +1 -1
- package/_esm/actions/governance/proposals/getProposals.js +7 -5
- package/_esm/actions/governance/proposals/getProposals.js.map +1 -1
- package/_esm/actions/morpho/markets/common.js +34 -2
- package/_esm/actions/morpho/markets/common.js.map +1 -1
- package/_esm/actions/morpho/user-rewards/common.js +2 -8
- package/_esm/actions/morpho/user-rewards/common.js.map +1 -1
- package/_esm/actions/morpho/vaults/common.js +110 -91
- package/_esm/actions/morpho/vaults/common.js.map +1 -1
- package/_esm/actions/morpho/vaults/lunarIndexerTransform.js +36 -4
- package/_esm/actions/morpho/vaults/lunarIndexerTransform.js.map +1 -1
- package/_esm/environments/types/config.js.map +1 -1
- package/_esm/errors/version.js +1 -1
- package/_types/actions/core/getUserBalances.d.ts.map +1 -1
- package/_types/actions/core/markets/common.d.ts.map +1 -1
- package/_types/actions/governance/getStakingInfo.d.ts.map +1 -1
- package/_types/actions/governance/getUserStakingInfo.d.ts.map +1 -1
- package/_types/actions/governance/getWellPrice.d.ts +9 -1
- package/_types/actions/governance/getWellPrice.d.ts.map +1 -1
- package/_types/actions/governance/proposals/common.d.ts.map +1 -1
- package/_types/actions/governance/proposals/getProposals.d.ts +10 -1
- package/_types/actions/governance/proposals/getProposals.d.ts.map +1 -1
- package/_types/actions/morpho/markets/common.d.ts.map +1 -1
- package/_types/actions/morpho/user-rewards/common.d.ts.map +1 -1
- package/_types/actions/morpho/vaults/common.d.ts.map +1 -1
- package/_types/actions/morpho/vaults/lunarIndexerTransform.d.ts.map +1 -1
- package/_types/environments/types/config.d.ts +11 -1
- package/_types/environments/types/config.d.ts.map +1 -1
- package/_types/errors/version.d.ts +1 -1
- package/actions/core/getUserBalances.ts +98 -51
- package/actions/core/markets/common.ts +5 -8
- package/actions/governance/getStakingInfo.ts +2 -9
- package/actions/governance/getUserStakingInfo.ts +2 -8
- package/actions/governance/getWellPrice.ts +99 -7
- package/actions/governance/proposals/common.ts +44 -16
- package/actions/governance/proposals/getProposals.ts +22 -5
- package/actions/morpho/markets/common.ts +37 -2
- package/actions/morpho/user-rewards/common.ts +2 -8
- package/actions/morpho/vaults/common.ts +161 -127
- package/actions/morpho/vaults/lunarIndexerTransform.ts +41 -6
- package/environments/types/config.ts +11 -1
- package/errors/version.ts +1 -1
- package/package.json +1 -1
|
@@ -447,6 +447,29 @@ export const getProposalsOnChainData = async (
|
|
|
447
447
|
? governanceEnvironment
|
|
448
448
|
: getEnvironmentByChainId(chainId);
|
|
449
449
|
|
|
450
|
+
// viem batches these reads into one multicall, so a single failed request
|
|
451
|
+
// rejects every read in it. Collect failures per chain and report them once
|
|
452
|
+
// after the batch instead of once per read (Sentry MOONWELL-FRONTEND-1AX).
|
|
453
|
+
const readFailures = new Map<
|
|
454
|
+
number,
|
|
455
|
+
{ error: unknown; failed: number; total: number; proposalIds: Set<string> }
|
|
456
|
+
>();
|
|
457
|
+
const recordRead = (chainId: number, proposalId: number, error?: unknown) => {
|
|
458
|
+
const entry = readFailures.get(chainId) ?? {
|
|
459
|
+
error: undefined,
|
|
460
|
+
failed: 0,
|
|
461
|
+
total: 0,
|
|
462
|
+
proposalIds: new Set<string>(),
|
|
463
|
+
};
|
|
464
|
+
entry.total++;
|
|
465
|
+
if (error !== undefined) {
|
|
466
|
+
entry.error ??= error;
|
|
467
|
+
entry.failed++;
|
|
468
|
+
entry.proposalIds.add(String(proposalId));
|
|
469
|
+
}
|
|
470
|
+
readFailures.set(chainId, entry);
|
|
471
|
+
};
|
|
472
|
+
|
|
450
473
|
const onChainDataList = await Promise.all(
|
|
451
474
|
apiProposals.map(async (p) => {
|
|
452
475
|
const isLocal = p.chainId === governanceEnvironment.chainId;
|
|
@@ -501,29 +524,17 @@ export const getProposalsOnChainData = async (
|
|
|
501
524
|
]);
|
|
502
525
|
if (stateResult.status === "fulfilled") {
|
|
503
526
|
state = Number(stateResult.value);
|
|
527
|
+
recordRead(p.chainId, p.proposalId);
|
|
504
528
|
} else {
|
|
505
529
|
stateReadFailed = true;
|
|
506
|
-
|
|
507
|
-
`Failed to fetch state for proposal ${p.proposalId} (chainId=${p.chainId}):`,
|
|
508
|
-
stateResult.reason,
|
|
509
|
-
);
|
|
510
|
-
governanceEnvironment.onError?.(stateResult.reason, {
|
|
511
|
-
source: "governance-proposals",
|
|
512
|
-
chainId: p.chainId,
|
|
513
|
-
});
|
|
530
|
+
recordRead(p.chainId, p.proposalId, stateResult.reason);
|
|
514
531
|
}
|
|
515
532
|
if (proposalsResult.status === "fulfilled") {
|
|
516
533
|
proposalData = proposalsResult.value ?? null;
|
|
534
|
+
recordRead(p.chainId, p.proposalId);
|
|
517
535
|
} else {
|
|
518
536
|
proposalsReadFailed = true;
|
|
519
|
-
|
|
520
|
-
`Failed to fetch proposalData for proposal ${p.proposalId} (chainId=${p.chainId}):`,
|
|
521
|
-
proposalsResult.reason,
|
|
522
|
-
);
|
|
523
|
-
governanceEnvironment.onError?.(proposalsResult.reason, {
|
|
524
|
-
source: "governance-proposals",
|
|
525
|
-
chainId: p.chainId,
|
|
526
|
-
});
|
|
537
|
+
recordRead(p.chainId, p.proposalId, proposalsResult.reason);
|
|
527
538
|
}
|
|
528
539
|
}
|
|
529
540
|
|
|
@@ -609,6 +620,23 @@ export const getProposalsOnChainData = async (
|
|
|
609
620
|
}),
|
|
610
621
|
);
|
|
611
622
|
|
|
623
|
+
for (const [chainId, entry] of readFailures) {
|
|
624
|
+
if (entry.failed === 0) continue;
|
|
625
|
+
const proposalIds = [...entry.proposalIds];
|
|
626
|
+
console.warn(
|
|
627
|
+
`Failed ${entry.failed}/${entry.total} governor reads (chainId=${chainId}, proposals ${proposalIds.join(", ")}):`,
|
|
628
|
+
entry.error,
|
|
629
|
+
);
|
|
630
|
+
governanceEnvironment.onError?.(entry.error, {
|
|
631
|
+
source: "governance-proposals",
|
|
632
|
+
chainId,
|
|
633
|
+
operation: "proposal-onchain-read",
|
|
634
|
+
failedCount: entry.failed,
|
|
635
|
+
totalCount: entry.total,
|
|
636
|
+
items: proposalIds,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
612
640
|
return onChainDataList;
|
|
613
641
|
};
|
|
614
642
|
|
|
@@ -20,7 +20,16 @@ import {
|
|
|
20
20
|
export type GetProposalsParameters<
|
|
21
21
|
environments,
|
|
22
22
|
network extends Chain | undefined,
|
|
23
|
-
> = OptionalNetworkParameterType<environments, network
|
|
23
|
+
> = OptionalNetworkParameterType<environments, network> & {
|
|
24
|
+
/**
|
|
25
|
+
* Governance chains to fetch proposals from: 1 (Ethereum, active),
|
|
26
|
+
* 1284 (Moonbeam archive) and/or 1285 (Moonriver archive). Defaults to all
|
|
27
|
+
* of them. The archives hold almost all of the payload (~6.6MB of ~7MB), so
|
|
28
|
+
* views that only show recent proposals can pass `[1]` and load the archives
|
|
29
|
+
* separately when needed. Unsupported chainIds are ignored.
|
|
30
|
+
*/
|
|
31
|
+
proposalChainIds?: readonly number[];
|
|
32
|
+
};
|
|
24
33
|
|
|
25
34
|
export type GetProposalsReturnType = Promise<Proposal[]>;
|
|
26
35
|
|
|
@@ -42,7 +51,10 @@ export async function getProposals<
|
|
|
42
51
|
return [];
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
const proposals = await fetchGovernorProposals(
|
|
54
|
+
const proposals = await fetchGovernorProposals(
|
|
55
|
+
governanceEnvironment,
|
|
56
|
+
args?.proposalChainIds,
|
|
57
|
+
);
|
|
46
58
|
// Newer multigov-ethereum proposals (chainId=1) and historical Moonbeam
|
|
47
59
|
// proposals (chainId=1284) restart their proposalId counters from 1, so IDs
|
|
48
60
|
// may collide across chains. Sort by proposalId desc with chainId as a
|
|
@@ -58,7 +70,8 @@ export async function getProposals<
|
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
/**
|
|
61
|
-
* Fetch every proposal the Governor API serves, across all governance chains
|
|
73
|
+
* Fetch every proposal the Governor API serves, across all governance chains
|
|
74
|
+
* (or only `proposalChainIds`, when given).
|
|
62
75
|
*
|
|
63
76
|
* One indexer DO serves them all:
|
|
64
77
|
* - chainId=1 (Ethereum) — the active multigov contract
|
|
@@ -76,16 +89,20 @@ export async function getProposals<
|
|
|
76
89
|
*/
|
|
77
90
|
async function fetchGovernorProposals(
|
|
78
91
|
governanceEnvironment: Environment,
|
|
92
|
+
proposalChainIds?: readonly number[],
|
|
79
93
|
): Promise<Proposal[]> {
|
|
94
|
+
const chainIds = SUPPORTED_GOVERNOR_CHAIN_IDS.filter(
|
|
95
|
+
(chainId) => !proposalChainIds || proposalChainIds.includes(chainId),
|
|
96
|
+
);
|
|
80
97
|
const results = await Promise.allSettled(
|
|
81
|
-
|
|
98
|
+
chainIds.map((chainId) =>
|
|
82
99
|
fetchAllProposals(governanceEnvironment, { chainId }),
|
|
83
100
|
),
|
|
84
101
|
);
|
|
85
102
|
|
|
86
103
|
const apiProposals: ApiProposal[] = [];
|
|
87
104
|
results.forEach((result, index) => {
|
|
88
|
-
const chainId =
|
|
105
|
+
const chainId = chainIds[index];
|
|
89
106
|
if (result.status === "fulfilled") {
|
|
90
107
|
apiProposals.push(...result.value);
|
|
91
108
|
} else if (chainId !== undefined) {
|
|
@@ -76,6 +76,21 @@ function validateLoanDecimals(value: unknown): number {
|
|
|
76
76
|
return value;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/** Summarizes an unexpected top-level body so the error says what arrived. */
|
|
80
|
+
function describeSharedLiquidityBody(data: unknown): string {
|
|
81
|
+
if (data === null || data === undefined) return `received ${data}`;
|
|
82
|
+
if (typeof data === "string") {
|
|
83
|
+
return `received string of ${data.length} chars: ${JSON.stringify(data.slice(0, 100))}`;
|
|
84
|
+
}
|
|
85
|
+
if (typeof data !== "object") return `received ${typeof data}`;
|
|
86
|
+
if (Array.isArray(data)) return `received array of ${data.length}`;
|
|
87
|
+
const body = data as Record<string, unknown>;
|
|
88
|
+
const kind = (value: unknown): string =>
|
|
89
|
+
Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
90
|
+
const keys = Object.keys(body).slice(0, 10).join(", ");
|
|
91
|
+
return `received object with keys [${keys}]; vaults: ${kind(body.vaults)}, markets: ${kind(body.markets)}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
function validatedMarketLiquidity(
|
|
80
95
|
data: LunarSharedLiquidityResponse,
|
|
81
96
|
): Map<string, number> {
|
|
@@ -86,7 +101,9 @@ function validatedMarketLiquidity(
|
|
|
86
101
|
typeof data.markets !== "object" ||
|
|
87
102
|
Array.isArray(data.markets)
|
|
88
103
|
) {
|
|
89
|
-
throw new TypeError(
|
|
104
|
+
throw new TypeError(
|
|
105
|
+
`Invalid shared-liquidity response (${describeSharedLiquidityBody(data)})`,
|
|
106
|
+
);
|
|
90
107
|
}
|
|
91
108
|
for (const vault of data.vaults) {
|
|
92
109
|
parseLiquidityQuantity(vault.fee, "vault.fee", true);
|
|
@@ -158,13 +175,31 @@ export async function fetchSharedLiquidityFromLunar(
|
|
|
158
175
|
const abort = (): void => controller.abort();
|
|
159
176
|
if (options.signal?.aborted) abort();
|
|
160
177
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
161
|
-
|
|
178
|
+
let timedOut = false;
|
|
179
|
+
const timer = setTimeout(() => {
|
|
180
|
+
timedOut = true;
|
|
181
|
+
abort();
|
|
182
|
+
}, timeoutMs);
|
|
162
183
|
try {
|
|
163
184
|
const response = await getWithRetry<LunarSharedLiquidityResponse>(
|
|
164
185
|
`${lunarIndexerUrl}/api/v1/isolated/shared-liquidity/${chainId}`,
|
|
165
186
|
{ timeout: timeoutMs, signal: controller.signal },
|
|
166
187
|
);
|
|
167
188
|
return response.data;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// Our own deadline aborts the request, which axios reports as a bare
|
|
191
|
+
// `CanceledError: canceled` (Sentry MOONWELL-FRONTEND-1C7 & co). That is a
|
|
192
|
+
// timeout, not a cancellation: surface it as one. A caller's own abort
|
|
193
|
+
// still propagates as the CanceledError it is.
|
|
194
|
+
if (timedOut && !options.signal?.aborted) {
|
|
195
|
+
const timeoutError = new Error(
|
|
196
|
+
`Shared-liquidity request timed out after ${timeoutMs}ms (chainId=${chainId})`,
|
|
197
|
+
{ cause: error },
|
|
198
|
+
);
|
|
199
|
+
timeoutError.name = "TimeoutError";
|
|
200
|
+
throw timeoutError;
|
|
201
|
+
}
|
|
202
|
+
throw error;
|
|
168
203
|
} finally {
|
|
169
204
|
clearTimeout(timer);
|
|
170
205
|
options.signal?.removeEventListener("abort", abort);
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "../../../environments/utils/index.js";
|
|
14
14
|
import type { MorphoUserReward } from "../../../types/morphoUserReward.js";
|
|
15
15
|
import type { MorphoUserStakingReward } from "../../../types/morphoUserStakingReward.js";
|
|
16
|
-
import {
|
|
16
|
+
import { getGovernanceTokenPriceOrZero } from "../../governance/getWellPrice.js";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Error thrown for any failure communicating with the Merkl API: non-ok HTTP
|
|
@@ -166,13 +166,7 @@ export async function getUserMorphoStakingRewardsData(params: {
|
|
|
166
166
|
await Promise.all([
|
|
167
167
|
viewsContract?.read.getAllMarketsInfo(),
|
|
168
168
|
homeViewsContract?.read.getNativeTokenPrice(),
|
|
169
|
-
|
|
170
|
-
params.environment.onError?.(err, {
|
|
171
|
-
source: "governance-token-price",
|
|
172
|
-
chainId: params.environment.chainId,
|
|
173
|
-
});
|
|
174
|
-
return 0n;
|
|
175
|
-
}),
|
|
169
|
+
getGovernanceTokenPriceOrZero(params.environment),
|
|
176
170
|
]);
|
|
177
171
|
|
|
178
172
|
const governanceTokenPrice = new Amount(governanceTokenPriceRaw, 18);
|
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
MorphoVault,
|
|
20
20
|
MorphoVaultMarket,
|
|
21
21
|
} from "../../../types/morphoVault.js";
|
|
22
|
-
import {
|
|
22
|
+
import { getGovernanceTokenPriceOrZero } from "../../governance/getWellPrice.js";
|
|
23
23
|
import { getGraphQL, getVaultV2Apy } from "../utils/graphql.js";
|
|
24
24
|
import {
|
|
25
25
|
SECONDS_PER_YEAR,
|
|
@@ -300,144 +300,184 @@ async function getMorphoVaultsDataFromIndexer(params: {
|
|
|
300
300
|
|
|
301
301
|
// Add staking rewards if includeRewards is true
|
|
302
302
|
if (params.includeRewards) {
|
|
303
|
+
const vaultsByEnvironment = new Map<
|
|
304
|
+
Environment,
|
|
305
|
+
{ vault: MorphoVault; multiReward: Address }[]
|
|
306
|
+
>();
|
|
303
307
|
for (const vault of allVaults) {
|
|
304
308
|
const environment = environments.find(
|
|
305
309
|
(env) => env.chainId === vault.chainId,
|
|
306
310
|
);
|
|
307
311
|
if (!environment) continue;
|
|
308
312
|
|
|
309
|
-
const
|
|
310
|
-
|
|
313
|
+
const multiReward =
|
|
314
|
+
environment.config.vaults[vault.vaultKey]?.multiReward;
|
|
315
|
+
if (!multiReward) continue;
|
|
311
316
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
e.custom?.governance?.chainIds?.includes(environment.chainId),
|
|
317
|
-
) || environment;
|
|
318
|
-
|
|
319
|
-
const viewsContract = environment.contracts.views;
|
|
320
|
-
const homeViewsContract = homeEnvironment.contracts.views;
|
|
321
|
-
|
|
322
|
-
const data = await Promise.all([
|
|
323
|
-
viewsContract?.read.getAllMarketsInfo(),
|
|
324
|
-
homeViewsContract?.read.getNativeTokenPrice(),
|
|
325
|
-
getGovernanceTokenPriceFor(environment).catch((err) => {
|
|
326
|
-
environment.onError?.(err, {
|
|
327
|
-
source: "governance-token-price",
|
|
328
|
-
chainId: environment.chainId,
|
|
329
|
-
});
|
|
330
|
-
return 0n;
|
|
331
|
-
}),
|
|
332
|
-
]);
|
|
317
|
+
const group = vaultsByEnvironment.get(environment) ?? [];
|
|
318
|
+
group.push({ vault, multiReward });
|
|
319
|
+
vaultsByEnvironment.set(environment, group);
|
|
320
|
+
}
|
|
333
321
|
|
|
334
|
-
|
|
322
|
+
// Price reads are per environment, not per vault: read them once per
|
|
323
|
+
// environment (in parallel across environments) and report failures once.
|
|
324
|
+
await Promise.all(
|
|
325
|
+
[...vaultsByEnvironment].map(([environment, group]) =>
|
|
326
|
+
addStakingRewardsForEnvironment(environment, group),
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
}
|
|
335
330
|
|
|
336
|
-
|
|
337
|
-
|
|
331
|
+
return [...allVaults, ...fallbackVaults];
|
|
332
|
+
}
|
|
338
333
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
},
|
|
369
|
-
];
|
|
370
|
-
}
|
|
334
|
+
async function getStakingRewardTokenPrices(
|
|
335
|
+
environment: Environment,
|
|
336
|
+
): Promise<{ token: TokenConfig; tokenPrice: Amount }[]> {
|
|
337
|
+
// Fetch market prices from the views contract to calculate rewards
|
|
338
|
+
const homeEnvironment =
|
|
339
|
+
(Object.values(publicEnvironments) as Environment[]).find((e) =>
|
|
340
|
+
e.custom?.governance?.chainIds?.includes(environment.chainId),
|
|
341
|
+
) || environment;
|
|
342
|
+
|
|
343
|
+
const [allMarkets, nativeTokenPriceRaw, governanceTokenPriceRaw] =
|
|
344
|
+
await Promise.all([
|
|
345
|
+
environment.contracts.views?.read.getAllMarketsInfo(),
|
|
346
|
+
homeEnvironment.contracts.views?.read.getNativeTokenPrice(),
|
|
347
|
+
getGovernanceTokenPriceOrZero(environment),
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
const tokenPrices: { token: TokenConfig; tokenPrice: Amount }[] = [];
|
|
351
|
+
for (const marketInfo of allMarkets ?? []) {
|
|
352
|
+
const marketFound = findMarketByAddress(environment, marketInfo.market);
|
|
353
|
+
if (marketFound) {
|
|
354
|
+
tokenPrices.push({
|
|
355
|
+
token: marketFound.underlyingToken,
|
|
356
|
+
tokenPrice: new Amount(
|
|
357
|
+
marketInfo.underlyingPrice,
|
|
358
|
+
36 - marketFound.underlyingToken.decimals,
|
|
359
|
+
),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
371
363
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
364
|
+
// Add governance token to token prices
|
|
365
|
+
const governanceToken = environment.custom?.governance?.token
|
|
366
|
+
? environment.config.tokens[environment.custom.governance.token]
|
|
367
|
+
: undefined;
|
|
368
|
+
if (governanceToken) {
|
|
369
|
+
tokenPrices.push({
|
|
370
|
+
token: governanceToken,
|
|
371
|
+
tokenPrice: new Amount(governanceTokenPriceRaw, 18),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
380
374
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
375
|
+
// Add native token to token prices
|
|
376
|
+
const nativeToken = findTokenByAddress(environment, zeroAddress);
|
|
377
|
+
if (nativeToken) {
|
|
378
|
+
tokenPrices.push({
|
|
379
|
+
token: nativeToken,
|
|
380
|
+
tokenPrice: new Amount(nativeTokenPriceRaw ?? 0n, 18),
|
|
381
|
+
});
|
|
382
|
+
}
|
|
385
383
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
vaultConfig.multiReward,
|
|
389
|
-
);
|
|
384
|
+
return tokenPrices;
|
|
385
|
+
}
|
|
390
386
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
)
|
|
397
|
-
.forEach((reward) => {
|
|
398
|
-
const token = Object.values(environment.config.tokens).find(
|
|
399
|
-
(token) => token.address === reward?.token,
|
|
400
|
-
);
|
|
401
|
-
if (!token || !reward?.rewardRate) return;
|
|
387
|
+
async function addStakingRewardsForEnvironment(
|
|
388
|
+
environment: Environment,
|
|
389
|
+
group: { vault: MorphoVault; multiReward: Address }[],
|
|
390
|
+
): Promise<void> {
|
|
391
|
+
const vaultKeys = group.map(({ vault }) => vault.vaultKey);
|
|
402
392
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
393
|
+
let tokenPrices: { token: TokenConfig; tokenPrice: Amount }[];
|
|
394
|
+
try {
|
|
395
|
+
tokenPrices = await getStakingRewardTokenPrices(environment);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.warn(
|
|
398
|
+
`Failed to fetch staking reward prices for chain ${environment.chainId}:`,
|
|
399
|
+
error,
|
|
400
|
+
);
|
|
401
|
+
environment.onError?.(error, {
|
|
402
|
+
source: "morpho-vault-staking-rewards",
|
|
403
|
+
chainId: environment.chainId,
|
|
404
|
+
operation: "staking-reward-prices",
|
|
405
|
+
failedCount: group.length,
|
|
406
|
+
totalCount: group.length,
|
|
407
|
+
items: vaultKeys,
|
|
408
|
+
});
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
406
411
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
412
|
+
const settlements = await Promise.allSettled(
|
|
413
|
+
group.map(async ({ vault, multiReward }) => {
|
|
414
|
+
const [rewards, distributorTotalSupply] = await Promise.all([
|
|
415
|
+
getRewardsData(environment, multiReward),
|
|
416
|
+
getTotalSupplyData(environment, multiReward),
|
|
417
|
+
]);
|
|
418
|
+
|
|
419
|
+
rewards
|
|
420
|
+
.filter(
|
|
421
|
+
(reward) =>
|
|
422
|
+
reward?.periodFinish &&
|
|
423
|
+
dayjs.utc().isBefore(dayjs.unix(Number(reward.periodFinish))),
|
|
424
|
+
)
|
|
425
|
+
.forEach((reward) => {
|
|
426
|
+
const token = Object.values(environment.config.tokens).find(
|
|
427
|
+
(token) => token.address === reward?.token,
|
|
428
|
+
);
|
|
429
|
+
if (!token || !reward?.rewardRate) return;
|
|
430
|
+
|
|
431
|
+
const market = tokenPrices.find(
|
|
432
|
+
(m) => m?.token.address === reward.token,
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const rewardPriceUsd = market?.tokenPrice.value ?? 0;
|
|
436
|
+
|
|
437
|
+
const rewardsPerYear =
|
|
438
|
+
new Amount(reward.rewardRate, market?.token.decimals ?? 18).value *
|
|
439
|
+
SECONDS_PER_YEAR *
|
|
440
|
+
rewardPriceUsd;
|
|
441
|
+
|
|
442
|
+
vault.stakingRewards.push({
|
|
443
|
+
apr:
|
|
444
|
+
(rewardsPerYear /
|
|
445
|
+
(new Amount(distributorTotalSupply, vault.vaultToken.decimals)
|
|
446
|
+
.value *
|
|
447
|
+
vault.underlyingPrice)) *
|
|
448
|
+
100,
|
|
449
|
+
token: token,
|
|
424
450
|
});
|
|
451
|
+
});
|
|
425
452
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
`Failed to fetch staking rewards for vault ${vault.vaultKey}:`,
|
|
434
|
-
error,
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
453
|
+
vault.stakingRewardsApr = vault.stakingRewards.reduce(
|
|
454
|
+
(acc, curr) => acc + curr.apr,
|
|
455
|
+
0,
|
|
456
|
+
);
|
|
457
|
+
vault.totalStakingApr = vault.stakingRewardsApr + vault.baseApy;
|
|
458
|
+
}),
|
|
459
|
+
);
|
|
439
460
|
|
|
440
|
-
|
|
461
|
+
const failedVaultKeys = vaultKeys.filter(
|
|
462
|
+
(_, i) => settlements[i]?.status === "rejected",
|
|
463
|
+
);
|
|
464
|
+
const firstFailure = settlements.find(
|
|
465
|
+
(s): s is PromiseRejectedResult => s.status === "rejected",
|
|
466
|
+
);
|
|
467
|
+
if (firstFailure) {
|
|
468
|
+
console.warn(
|
|
469
|
+
`Failed to fetch staking rewards for vault(s) ${failedVaultKeys.join(", ")} (chain ${environment.chainId}):`,
|
|
470
|
+
firstFailure.reason,
|
|
471
|
+
);
|
|
472
|
+
environment.onError?.(firstFailure.reason, {
|
|
473
|
+
source: "morpho-vault-staking-rewards",
|
|
474
|
+
chainId: environment.chainId,
|
|
475
|
+
operation: "staking-rewards",
|
|
476
|
+
failedCount: failedVaultKeys.length,
|
|
477
|
+
totalCount: group.length,
|
|
478
|
+
items: failedVaultKeys,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
441
481
|
}
|
|
442
482
|
|
|
443
483
|
export async function getMorphoVaultsData(params: {
|
|
@@ -1060,13 +1100,7 @@ async function getMorphoVaultsDataFromOnChain(params: {
|
|
|
1060
1100
|
const data = await Promise.all([
|
|
1061
1101
|
viewsContract?.read.getAllMarketsInfo(),
|
|
1062
1102
|
homeViewsContract?.read.getNativeTokenPrice(),
|
|
1063
|
-
|
|
1064
|
-
environment.onError?.(err, {
|
|
1065
|
-
source: "governance-token-price",
|
|
1066
|
-
chainId: environment.chainId,
|
|
1067
|
-
});
|
|
1068
|
-
return 0n;
|
|
1069
|
-
}),
|
|
1103
|
+
getGovernanceTokenPriceOrZero(environment),
|
|
1070
1104
|
]);
|
|
1071
1105
|
|
|
1072
1106
|
const [allMarkets, nativeTokenPriceRaw, governanceTokenPriceRaw] = data;
|
|
@@ -248,13 +248,14 @@ export function transformVaultFromIndexer(
|
|
|
248
248
|
// where totalLiquidity briefly exceeds totalAssets.
|
|
249
249
|
const vaultSupplyValue = Math.max(0, totalAssetsValue - totalLiquidityValue);
|
|
250
250
|
|
|
251
|
-
// Transform markets
|
|
252
|
-
|
|
251
|
+
// Transform markets. The indexer has served vaults without `markets` /
|
|
252
|
+
// `rewards` (Sentry MOONWELL-FRONTEND-GX); keep the vault with empty lists.
|
|
253
|
+
const markets = (indexerVault.markets ?? []).map((market) =>
|
|
253
254
|
transformMarket(market, underlyingDecimals, totalAssetsValue, tokenMap),
|
|
254
255
|
);
|
|
255
256
|
|
|
256
257
|
// Transform rewards
|
|
257
|
-
const rewards = transformRewards(indexerVault.rewards, tokenMap);
|
|
258
|
+
const rewards = transformRewards(indexerVault.rewards ?? [], tokenMap);
|
|
258
259
|
|
|
259
260
|
// Build vault object
|
|
260
261
|
const vault: MorphoVault = {
|
|
@@ -313,9 +314,43 @@ export function transformVaultsFromIndexer(
|
|
|
313
314
|
environment: Environment,
|
|
314
315
|
tokenMap: Map<string, LunarIndexerToken>,
|
|
315
316
|
): MorphoVault[] {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
317
|
+
// A malformed record costs only that record, never the chain's whole list.
|
|
318
|
+
// Missing `markets` / `rewards` keep the vault (as empty lists); a vault
|
|
319
|
+
// that cannot be built at all is dropped. Both are reported once per call.
|
|
320
|
+
const malformed: string[] = [];
|
|
321
|
+
let firstError: unknown;
|
|
322
|
+
const vaults = indexerVaults.flatMap((vault) => {
|
|
323
|
+
if (!Array.isArray(vault.markets) || !Array.isArray(vault.rewards)) {
|
|
324
|
+
malformed.push(vault.address);
|
|
325
|
+
firstError ??= new Error(
|
|
326
|
+
`Lunar Indexer vault ${vault.address} is missing ${!Array.isArray(vault.markets) ? "markets" : "rewards"}`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
return [transformVaultFromIndexer(vault, environment, tokenMap)];
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if (!malformed.includes(vault.address)) malformed.push(vault.address);
|
|
333
|
+
firstError ??= error;
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
if (malformed.length > 0) {
|
|
339
|
+
console.warn(
|
|
340
|
+
`Malformed Lunar Indexer vault record(s) on chain ${environment.chainId}: ${malformed.join(", ")}`,
|
|
341
|
+
firstError,
|
|
342
|
+
);
|
|
343
|
+
environment.onError?.(firstError, {
|
|
344
|
+
source: "vaults",
|
|
345
|
+
chainId: environment.chainId,
|
|
346
|
+
operation: "malformed-vault-records",
|
|
347
|
+
failedCount: malformed.length,
|
|
348
|
+
totalCount: indexerVaults.length,
|
|
349
|
+
items: malformed,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return vaults;
|
|
319
354
|
}
|
|
320
355
|
|
|
321
356
|
/**
|
|
@@ -228,11 +228,21 @@ export type ContractsConfig<contracts, tokens> = {} extends contracts
|
|
|
228
228
|
? {}
|
|
229
229
|
: { [name in keyof ContractConfig<tokens>]?: ContractConfig<tokens>[name] };
|
|
230
230
|
|
|
231
|
-
/**
|
|
231
|
+
/**
|
|
232
|
+
* Context passed to `onError` callbacks. `token` is set for per-token read failures.
|
|
233
|
+
*
|
|
234
|
+
* Batched reads report once per operation rather than once per item: `error`
|
|
235
|
+
* is the first failure, `failedCount`/`totalCount` size the batch and `items`
|
|
236
|
+
* identifies what failed (proposalIds, token/vault addresses, chainIds...).
|
|
237
|
+
*/
|
|
232
238
|
export type OnErrorContext = {
|
|
233
239
|
source: string;
|
|
234
240
|
chainId: number;
|
|
235
241
|
token?: Address;
|
|
242
|
+
operation?: string;
|
|
243
|
+
failedCount?: number;
|
|
244
|
+
totalCount?: number;
|
|
245
|
+
items?: string[];
|
|
236
246
|
};
|
|
237
247
|
|
|
238
248
|
export type CustomConfig<custom> = {} extends custom
|
package/errors/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '0.
|
|
1
|
+
export const version = '0.25.0'
|