@moonwell-fi/moonwell-sdk 0.9.26 → 0.9.28
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 +16 -0
- package/_cjs/actions/core/markets/common.js +291 -5
- package/_cjs/actions/core/markets/common.js.map +1 -1
- package/_cjs/actions/core/markets/getMarketSnapshots.js +83 -3
- package/_cjs/actions/core/markets/getMarketSnapshots.js.map +1 -1
- package/_cjs/actions/core/user-positions/common.js +61 -1
- package/_cjs/actions/core/user-positions/common.js.map +1 -1
- package/_cjs/actions/core/user-positions/getUserPositionSnapshots.js +70 -2
- package/_cjs/actions/core/user-positions/getUserPositionSnapshots.js.map +1 -1
- package/_cjs/actions/lunar-indexer-client.js +164 -0
- package/_cjs/actions/lunar-indexer-client.js.map +1 -0
- package/_cjs/actions/lunar-indexer-transformers.js +47 -0
- package/_cjs/actions/lunar-indexer-transformers.js.map +1 -0
- package/_cjs/environments/definitions/base/environment.js +2 -1
- package/_cjs/environments/definitions/base/environment.js.map +1 -1
- package/_cjs/environments/definitions/moonbeam/environment.js +2 -1
- package/_cjs/environments/definitions/moonbeam/environment.js.map +1 -1
- package/_cjs/environments/definitions/optimism/environment.js +2 -1
- package/_cjs/environments/definitions/optimism/environment.js.map +1 -1
- package/_cjs/environments/types/config.js +1 -0
- package/_cjs/environments/types/config.js.map +1 -1
- package/_cjs/errors/version.js +1 -1
- package/_cjs/utils/lunar-indexer-helpers.js +27 -0
- package/_cjs/utils/lunar-indexer-helpers.js.map +1 -0
- package/_esm/actions/core/markets/common.js +302 -5
- package/_esm/actions/core/markets/common.js.map +1 -1
- package/_esm/actions/core/markets/getMarketSnapshots.js +87 -3
- package/_esm/actions/core/markets/getMarketSnapshots.js.map +1 -1
- package/_esm/actions/core/user-positions/common.js +74 -1
- package/_esm/actions/core/user-positions/common.js.map +1 -1
- package/_esm/actions/core/user-positions/getUserPositionSnapshots.js +100 -2
- package/_esm/actions/core/user-positions/getUserPositionSnapshots.js.map +1 -1
- package/_esm/actions/lunar-indexer-client.js +201 -0
- package/_esm/actions/lunar-indexer-client.js.map +1 -0
- package/_esm/actions/lunar-indexer-transformers.js +80 -0
- package/_esm/actions/lunar-indexer-transformers.js.map +1 -0
- package/_esm/environments/definitions/base/environment.js +2 -1
- package/_esm/environments/definitions/base/environment.js.map +1 -1
- package/_esm/environments/definitions/moonbeam/environment.js +2 -1
- package/_esm/environments/definitions/moonbeam/environment.js.map +1 -1
- package/_esm/environments/definitions/optimism/environment.js +2 -1
- package/_esm/environments/definitions/optimism/environment.js.map +1 -1
- package/_esm/environments/types/config.js +1 -0
- package/_esm/environments/types/config.js.map +1 -1
- package/_esm/errors/version.js +1 -1
- package/_esm/utils/lunar-indexer-helpers.js +48 -0
- package/_esm/utils/lunar-indexer-helpers.js.map +1 -0
- package/_types/actions/core/markets/common.d.ts.map +1 -1
- package/_types/actions/core/markets/getMarketSnapshots.d.ts +4 -0
- package/_types/actions/core/markets/getMarketSnapshots.d.ts.map +1 -1
- package/_types/actions/core/user-positions/common.d.ts.map +1 -1
- package/_types/actions/core/user-positions/getUserPositionSnapshots.d.ts +28 -0
- package/_types/actions/core/user-positions/getUserPositionSnapshots.d.ts.map +1 -1
- package/_types/actions/lunar-indexer-client.d.ts +197 -0
- package/_types/actions/lunar-indexer-client.d.ts.map +1 -0
- package/_types/actions/lunar-indexer-transformers.d.ts +40 -0
- package/_types/actions/lunar-indexer-transformers.d.ts.map +1 -0
- package/_types/environments/definitions/base/environment.d.ts +1 -1
- package/_types/environments/definitions/base/environment.d.ts.map +1 -1
- package/_types/environments/definitions/moonbeam/environment.d.ts +1 -1
- package/_types/environments/definitions/moonbeam/environment.d.ts.map +1 -1
- package/_types/environments/definitions/optimism/environment.d.ts +1 -1
- package/_types/environments/definitions/optimism/environment.d.ts.map +1 -1
- package/_types/environments/types/config.d.ts +2 -0
- package/_types/environments/types/config.d.ts.map +1 -1
- package/_types/errors/version.d.ts +1 -1
- package/_types/utils/lunar-indexer-helpers.d.ts +38 -0
- package/_types/utils/lunar-indexer-helpers.d.ts.map +1 -0
- package/actions/core/markets/common.ts +500 -5
- package/actions/core/markets/getMarketSnapshots.ts +153 -2
- package/actions/core/user-positions/common.ts +139 -6
- package/actions/core/user-positions/getUserPositionSnapshots.ts +175 -1
- package/actions/lunar-indexer-client.ts +409 -0
- package/actions/lunar-indexer-transformers.ts +113 -0
- package/environments/definitions/base/environment.ts +3 -0
- package/environments/definitions/moonbeam/environment.ts +3 -0
- package/environments/definitions/optimism/environment.ts +3 -0
- package/environments/types/config.ts +3 -0
- package/errors/version.ts +1 -1
- package/package.json +1 -1
- package/utils/lunar-indexer-helpers.ts +57 -0
|
@@ -10,6 +10,13 @@ import {
|
|
|
10
10
|
import type { NetworkParameterType } from "../../../common/types.js";
|
|
11
11
|
import type { Chain, Environment } from "../../../environments/index.js";
|
|
12
12
|
import type { MarketSnapshot } from "../../../types/market.js";
|
|
13
|
+
import { buildMarketId } from "../../../utils/lunar-indexer-helpers.js";
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_LUNAR_TIMEOUT_MS,
|
|
16
|
+
createLunarIndexerClient,
|
|
17
|
+
shouldFallback,
|
|
18
|
+
} from "../../lunar-indexer-client.js";
|
|
19
|
+
import { transformMarketSnapshots } from "../../lunar-indexer-transformers.js";
|
|
13
20
|
import { getSubgraph } from "../../morpho/utils/graphql.js";
|
|
14
21
|
|
|
15
22
|
dayjs.extend(utc);
|
|
@@ -20,8 +27,50 @@ export type GetMarketSnapshotsParameters<
|
|
|
20
27
|
> = NetworkParameterType<environments, network> & {
|
|
21
28
|
type: "core" | "isolated";
|
|
22
29
|
marketId: `0x${string}`;
|
|
30
|
+
/** Predefined time period for snapshots */
|
|
31
|
+
period?: "1M" | "3M" | "1Y" | "ALL";
|
|
32
|
+
startTime?: number;
|
|
33
|
+
endTime?: number;
|
|
23
34
|
};
|
|
24
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Calculate start and end times based on period or custom timestamps.
|
|
38
|
+
* Priority: custom timestamps > period > default (365 days)
|
|
39
|
+
*/
|
|
40
|
+
function calculateTimeRange(
|
|
41
|
+
period?: "1M" | "3M" | "1Y" | "ALL",
|
|
42
|
+
startTime?: number,
|
|
43
|
+
endTime?: number,
|
|
44
|
+
): { startTime: number; endTime: number } {
|
|
45
|
+
const now = dayjs.utc();
|
|
46
|
+
const end = endTime ?? now.unix();
|
|
47
|
+
|
|
48
|
+
if (startTime !== undefined && endTime !== undefined) {
|
|
49
|
+
return { startTime, endTime: end };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let start: number;
|
|
53
|
+
switch (period) {
|
|
54
|
+
case "1M":
|
|
55
|
+
start = now.subtract(31, "days").unix();
|
|
56
|
+
break;
|
|
57
|
+
case "3M":
|
|
58
|
+
start = now.subtract(91, "days").unix();
|
|
59
|
+
break;
|
|
60
|
+
case "1Y":
|
|
61
|
+
start = now.subtract(366, "days").unix();
|
|
62
|
+
break;
|
|
63
|
+
case "ALL":
|
|
64
|
+
start = now.subtract(10, "years").unix();
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
start = now.subtract(365, "days").unix();
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { startTime: start, endTime: end };
|
|
72
|
+
}
|
|
73
|
+
|
|
25
74
|
export type GetMarketSnapshotsReturnType = Promise<MarketSnapshot[]>;
|
|
26
75
|
|
|
27
76
|
export async function getMarketSnapshots<
|
|
@@ -38,7 +87,13 @@ export async function getMarketSnapshots<
|
|
|
38
87
|
}
|
|
39
88
|
|
|
40
89
|
if (args?.type === "core") {
|
|
41
|
-
return fetchCoreMarketSnapshots(
|
|
90
|
+
return fetchCoreMarketSnapshots(
|
|
91
|
+
args.marketId,
|
|
92
|
+
environment,
|
|
93
|
+
args.period,
|
|
94
|
+
args.startTime,
|
|
95
|
+
args.endTime,
|
|
96
|
+
);
|
|
42
97
|
} else {
|
|
43
98
|
if (environment.custom.morpho?.minimalDeployment === false) {
|
|
44
99
|
return fetchIsolatedMarketSnapshots(args.marketId, environment);
|
|
@@ -51,6 +106,102 @@ export async function getMarketSnapshots<
|
|
|
51
106
|
async function fetchCoreMarketSnapshots(
|
|
52
107
|
marketAddress: string,
|
|
53
108
|
environment: Environment,
|
|
109
|
+
period?: "1M" | "3M" | "1Y" | "ALL",
|
|
110
|
+
startTime?: number,
|
|
111
|
+
endTime?: number,
|
|
112
|
+
): Promise<MarketSnapshot[]> {
|
|
113
|
+
if (environment.lunarIndexerUrl) {
|
|
114
|
+
try {
|
|
115
|
+
const result = await fetchCoreMarketSnapshotsFromLunar(
|
|
116
|
+
marketAddress,
|
|
117
|
+
environment,
|
|
118
|
+
period,
|
|
119
|
+
startTime,
|
|
120
|
+
endTime,
|
|
121
|
+
);
|
|
122
|
+
return result;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (!shouldFallback(error)) {
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
console.debug(
|
|
128
|
+
"[Lunar fallback] Falling back to Ponder for snapshots:",
|
|
129
|
+
error,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result = await fetchCoreMarketSnapshotsFromPonder(
|
|
135
|
+
marketAddress,
|
|
136
|
+
environment,
|
|
137
|
+
);
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function fetchCoreMarketSnapshotsFromLunar(
|
|
142
|
+
marketAddress: string,
|
|
143
|
+
environment: Environment,
|
|
144
|
+
period?: "1M" | "3M" | "1Y" | "ALL",
|
|
145
|
+
customStartTime?: number,
|
|
146
|
+
customEndTime?: number,
|
|
147
|
+
): Promise<MarketSnapshot[]> {
|
|
148
|
+
if (!environment.lunarIndexerUrl) {
|
|
149
|
+
throw new Error("Lunar Indexer URL not configured");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const client = createLunarIndexerClient({
|
|
153
|
+
baseUrl: environment.lunarIndexerUrl,
|
|
154
|
+
timeout: DEFAULT_LUNAR_TIMEOUT_MS,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const marketId = buildMarketId(environment.chainId, marketAddress);
|
|
158
|
+
const { startTime } = calculateTimeRange(
|
|
159
|
+
period,
|
|
160
|
+
customStartTime,
|
|
161
|
+
customEndTime,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const allSnapshots: MarketSnapshot[] = [];
|
|
165
|
+
let cursor: string | null = null;
|
|
166
|
+
|
|
167
|
+
do {
|
|
168
|
+
const response = await client.getMarketSnapshots(marketId, {
|
|
169
|
+
limit: 1000,
|
|
170
|
+
...(cursor && { cursor }),
|
|
171
|
+
granularity: "1d",
|
|
172
|
+
startTime,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const transformed = transformMarketSnapshots(
|
|
176
|
+
response.results,
|
|
177
|
+
environment.chainId,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const filteredSnapshots = transformed.filter((snapshot: MarketSnapshot) =>
|
|
181
|
+
isStartOfDay(Math.floor(snapshot.timestamp / 1000)),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
allSnapshots.push(...filteredSnapshots);
|
|
185
|
+
|
|
186
|
+
cursor = response.nextCursor;
|
|
187
|
+
} while (cursor !== null);
|
|
188
|
+
|
|
189
|
+
return allSnapshots.map((snapshot) => {
|
|
190
|
+
const supplied = snapshot.totalSupply;
|
|
191
|
+
const suppliedUsd = snapshot.totalSupplyUsd;
|
|
192
|
+
const price = supplied > 0 ? suppliedUsd / supplied : 0;
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
...snapshot,
|
|
196
|
+
collateralTokenPrice: price,
|
|
197
|
+
loanTokenPrice: price,
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function fetchCoreMarketSnapshotsFromPonder(
|
|
203
|
+
marketAddress: string,
|
|
204
|
+
environment: Environment,
|
|
54
205
|
): Promise<MarketSnapshot[]> {
|
|
55
206
|
const dailyData: MarketDailyData[] = [];
|
|
56
207
|
let hasNextPage = true;
|
|
@@ -82,7 +233,7 @@ async function fetchCoreMarketSnapshots(
|
|
|
82
233
|
}>(environment.indexerUrl, {
|
|
83
234
|
query: `
|
|
84
235
|
query {
|
|
85
|
-
marketDailySnapshots (
|
|
236
|
+
marketDailySnapshots (
|
|
86
237
|
limit: 1000,
|
|
87
238
|
orderBy: "timestamp"
|
|
88
239
|
orderDirection: "desc"
|
|
@@ -16,12 +16,35 @@ export const getUserPositionData = async (params: {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
try {
|
|
19
|
-
const [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
const [allMarketsResult, balancesResult, borrowsResult, membershipsResult] =
|
|
20
|
+
await Promise.allSettled([
|
|
21
|
+
viewsContract.read.getAllMarketsInfo(),
|
|
22
|
+
viewsContract.read.getUserBalances([params.account]),
|
|
23
|
+
viewsContract.read.getUserBorrowsBalances([params.account]),
|
|
24
|
+
viewsContract.read.getUserMarketsMemberships([params.account]),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const balances =
|
|
28
|
+
balancesResult.status === "fulfilled" ? balancesResult.value : [];
|
|
29
|
+
const borrows =
|
|
30
|
+
borrowsResult.status === "fulfilled" ? borrowsResult.value : [];
|
|
31
|
+
const memberships =
|
|
32
|
+
membershipsResult.status === "fulfilled" ? membershipsResult.value : [];
|
|
33
|
+
|
|
34
|
+
// If getAllMarketsInfo failed (e.g. broken on-chain oracle), fall back to
|
|
35
|
+
// per-mToken exchange rate calls. The user balance/borrow/membership calls
|
|
36
|
+
// don't touch the oracle so they can still succeed.
|
|
37
|
+
if (allMarketsResult.status === "rejected") {
|
|
38
|
+
return getUserPositionsFromMTokenFallback(
|
|
39
|
+
params,
|
|
40
|
+
balances as { amount: bigint; token: `0x${string}` }[],
|
|
41
|
+
borrows as { amount: bigint; token: `0x${string}` }[],
|
|
42
|
+
memberships as { membership: boolean; token: `0x${string}` }[],
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const allMarkets = allMarketsResult.value;
|
|
47
|
+
|
|
25
48
|
const markets = allMarkets
|
|
26
49
|
?.map((marketInfo) => {
|
|
27
50
|
const market = findMarketByAddress(
|
|
@@ -104,3 +127,113 @@ export const getUserPositionData = async (params: {
|
|
|
104
127
|
return [];
|
|
105
128
|
}
|
|
106
129
|
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Fallback for chains whose on-chain price oracle is non-functional (e.g.
|
|
133
|
+
* deprecated Moonriver). getUserBalances/getUserBorrowsBalances/getUserMarketsMemberships
|
|
134
|
+
* don't require the oracle, so we use those results directly. We fetch each
|
|
135
|
+
* mToken's exchangeRate individually to convert mToken balances to underlying.
|
|
136
|
+
* All USD values are set to 0 since oracle prices are unavailable.
|
|
137
|
+
*/
|
|
138
|
+
async function getUserPositionsFromMTokenFallback(
|
|
139
|
+
params: {
|
|
140
|
+
environment: Environment;
|
|
141
|
+
account: Address;
|
|
142
|
+
markets?: string[] | undefined;
|
|
143
|
+
},
|
|
144
|
+
balances: { amount: bigint; token: `0x${string}` }[],
|
|
145
|
+
borrows: { amount: bigint; token: `0x${string}` }[],
|
|
146
|
+
memberships: { membership: boolean; token: `0x${string}` }[],
|
|
147
|
+
): Promise<UserPosition[]> {
|
|
148
|
+
const positions: UserPosition[] = [];
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
150
|
+
const envAny = params.environment as any;
|
|
151
|
+
|
|
152
|
+
for (const marketKey of Object.keys(params.environment.config.markets)) {
|
|
153
|
+
const marketConfig = envAny.config.markets[marketKey] as
|
|
154
|
+
| { underlyingToken: string; marketToken: string }
|
|
155
|
+
| undefined;
|
|
156
|
+
if (!marketConfig) continue;
|
|
157
|
+
|
|
158
|
+
const underlyingToken = envAny.config.tokens[
|
|
159
|
+
marketConfig.underlyingToken
|
|
160
|
+
] as
|
|
161
|
+
| {
|
|
162
|
+
address: `0x${string}`;
|
|
163
|
+
decimals: number;
|
|
164
|
+
symbol: string;
|
|
165
|
+
name: string;
|
|
166
|
+
}
|
|
167
|
+
| undefined;
|
|
168
|
+
const marketToken = envAny.config.tokens[marketConfig.marketToken] as
|
|
169
|
+
| {
|
|
170
|
+
address: `0x${string}`;
|
|
171
|
+
decimals: number;
|
|
172
|
+
symbol: string;
|
|
173
|
+
name: string;
|
|
174
|
+
}
|
|
175
|
+
| undefined;
|
|
176
|
+
if (!underlyingToken || !marketToken) continue;
|
|
177
|
+
|
|
178
|
+
const mTokenAddress = marketToken.address.toLowerCase() as `0x${string}`;
|
|
179
|
+
|
|
180
|
+
const marketSuppliedRaw =
|
|
181
|
+
balances.find((r) => r.token.toLowerCase() === mTokenAddress)?.amount ??
|
|
182
|
+
0n;
|
|
183
|
+
const marketBorrowedRaw =
|
|
184
|
+
borrows.find((r) => r.token.toLowerCase() === mTokenAddress)?.amount ??
|
|
185
|
+
0n;
|
|
186
|
+
|
|
187
|
+
// Skip markets where the user has no position
|
|
188
|
+
if (marketSuppliedRaw === 0n && marketBorrowedRaw === 0n) continue;
|
|
189
|
+
|
|
190
|
+
const marketCollateralEnabled =
|
|
191
|
+
memberships.find((r) => r.token.toLowerCase() === mTokenAddress)
|
|
192
|
+
?.membership === true;
|
|
193
|
+
|
|
194
|
+
// Fetch exchange rate individually (not oracle-dependent)
|
|
195
|
+
const mTokenContract = envAny.markets[marketKey] as
|
|
196
|
+
| { read: Record<string, (...args: unknown[]) => Promise<bigint>> }
|
|
197
|
+
| undefined;
|
|
198
|
+
const defaultExchangeRate = 10n ** BigInt(10 + underlyingToken.decimals);
|
|
199
|
+
let exchangeRateRaw: bigint;
|
|
200
|
+
try {
|
|
201
|
+
exchangeRateRaw =
|
|
202
|
+
(await mTokenContract?.read.exchangeRateStored()) ??
|
|
203
|
+
defaultExchangeRate;
|
|
204
|
+
} catch {
|
|
205
|
+
exchangeRateRaw = defaultExchangeRate;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const exchangeRate = new Amount(
|
|
209
|
+
exchangeRateRaw,
|
|
210
|
+
10 + underlyingToken.decimals,
|
|
211
|
+
).value;
|
|
212
|
+
|
|
213
|
+
const borrowed = new Amount(marketBorrowedRaw, underlyingToken.decimals);
|
|
214
|
+
const marketSupplied = new Amount(marketSuppliedRaw, marketToken.decimals);
|
|
215
|
+
const supplied = new Amount(
|
|
216
|
+
marketSupplied.value * exchangeRate,
|
|
217
|
+
underlyingToken.decimals,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (params.markets && !params.markets.includes(marketToken.address)) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
positions.push({
|
|
225
|
+
chainId: params.environment.chainId,
|
|
226
|
+
account: params.account,
|
|
227
|
+
market: marketToken,
|
|
228
|
+
collateralEnabled: marketCollateralEnabled,
|
|
229
|
+
borrowed,
|
|
230
|
+
borrowedUsd: 0,
|
|
231
|
+
collateral: new Amount(0n, underlyingToken.decimals),
|
|
232
|
+
collateralUsd: 0,
|
|
233
|
+
supplied,
|
|
234
|
+
suppliedUsd: 0,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return positions;
|
|
239
|
+
}
|
|
@@ -7,6 +7,12 @@ import { getEnvironmentFromArgs, isStartOfDay } from "../../../common/index.js";
|
|
|
7
7
|
import type { NetworkParameterType } from "../../../common/types.js";
|
|
8
8
|
import type { Chain, Environment } from "../../../environments/index.js";
|
|
9
9
|
import type { UserPositionSnapshot } from "../../../types/userPosition.js";
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_LUNAR_TIMEOUT_MS,
|
|
12
|
+
createLunarIndexerClient,
|
|
13
|
+
shouldFallback,
|
|
14
|
+
} from "../../lunar-indexer-client.js";
|
|
15
|
+
import { transformPortfolioToSnapshots } from "../../lunar-indexer-transformers.js";
|
|
10
16
|
|
|
11
17
|
dayjs.extend(utc);
|
|
12
18
|
|
|
@@ -16,12 +22,82 @@ export type GetUserPositionSnapshotsParameters<
|
|
|
16
22
|
> = NetworkParameterType<environments, network> & {
|
|
17
23
|
/** User address*/
|
|
18
24
|
userAddress: Address;
|
|
25
|
+
/** Predefined time period for snapshots */
|
|
26
|
+
period?: "1M" | "3M" | "1Y" | "ALL";
|
|
27
|
+
/** Custom start time (unix timestamp in seconds). Overrides period if both startTime and endTime are provided. */
|
|
28
|
+
startTime?: number;
|
|
29
|
+
/** Custom end time (unix timestamp in seconds). Overrides period if both startTime and endTime are provided. */
|
|
30
|
+
endTime?: number;
|
|
31
|
+
/** Data granularity. Defaults to "1d" */
|
|
32
|
+
granularity?: "1h" | "6h" | "1d";
|
|
19
33
|
};
|
|
20
34
|
|
|
21
35
|
export type GetUserPositionSnapshotsReturnType = Promise<
|
|
22
36
|
UserPositionSnapshot[]
|
|
23
37
|
>;
|
|
24
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Calculate start and end times based on period or custom timestamps
|
|
41
|
+
* Priority: custom timestamps > period > default (365 days)
|
|
42
|
+
*/
|
|
43
|
+
function calculateTimeRange(
|
|
44
|
+
period?: "1M" | "3M" | "1Y" | "ALL",
|
|
45
|
+
startTime?: number,
|
|
46
|
+
endTime?: number,
|
|
47
|
+
): { startTime: number; endTime: number } {
|
|
48
|
+
const now = dayjs.utc();
|
|
49
|
+
const end = endTime ?? now.unix();
|
|
50
|
+
|
|
51
|
+
// If both startTime and endTime are provided, use them (custom range)
|
|
52
|
+
if (startTime !== undefined && endTime !== undefined) {
|
|
53
|
+
return { startTime, endTime: end };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Calculate based on period
|
|
57
|
+
let start: number;
|
|
58
|
+
switch (period) {
|
|
59
|
+
case "1M":
|
|
60
|
+
start = now.subtract(31, "days").unix();
|
|
61
|
+
break;
|
|
62
|
+
case "3M":
|
|
63
|
+
start = now.subtract(91, "days").unix();
|
|
64
|
+
break;
|
|
65
|
+
case "1Y":
|
|
66
|
+
start = now.subtract(366, "days").unix();
|
|
67
|
+
break;
|
|
68
|
+
case "ALL":
|
|
69
|
+
// Use a date far in the past to get all available data
|
|
70
|
+
start = now.subtract(10, "years").unix();
|
|
71
|
+
break;
|
|
72
|
+
default:
|
|
73
|
+
// Default to 365 days for backward compatibility
|
|
74
|
+
start = now.subtract(365, "days").unix();
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { startTime: start, endTime: end };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get historical snapshots of a user's positions across all markets
|
|
83
|
+
*
|
|
84
|
+
* @param client - Moonwell client instance
|
|
85
|
+
* @param args - Parameters including user address and optional time range
|
|
86
|
+
* @param args.userAddress - The user's wallet address
|
|
87
|
+
* @param args.period - Predefined time period: "1M" (31 days), "3M" (91 days), "1Y" (366 days), or "ALL" (all available history)
|
|
88
|
+
* @param args.startTime - Custom start time (unix timestamp in seconds). Overrides period if both startTime and endTime are provided.
|
|
89
|
+
* @param args.endTime - Custom end time (unix timestamp in seconds). Overrides period if both startTime and endTime are provided.
|
|
90
|
+
* @param args.granularity - Data granularity: "1h", "6h", or "1d" (default). Determines snapshot frequency.
|
|
91
|
+
*
|
|
92
|
+
* @returns Array of user position snapshots with USD values for supply, borrow, and collateral
|
|
93
|
+
*
|
|
94
|
+
* @remarks
|
|
95
|
+
* - Default behavior (no time parameters): Returns 365 days of history
|
|
96
|
+
* - Parameter priority: Custom timestamps > period > default (365 days)
|
|
97
|
+
* - When using Lunar Indexer, custom time ranges are supported
|
|
98
|
+
* - When falling back to Ponder, all available data is returned (client-side filtering may be needed)
|
|
99
|
+
* - Snapshots are filtered to start-of-day for "1d" granularity
|
|
100
|
+
*/
|
|
25
101
|
export async function getUserPositionSnapshots<
|
|
26
102
|
environments,
|
|
27
103
|
Network extends Chain | undefined,
|
|
@@ -35,12 +111,110 @@ export async function getUserPositionSnapshots<
|
|
|
35
111
|
return [];
|
|
36
112
|
}
|
|
37
113
|
|
|
38
|
-
return fetchUserPositionSnapshots(
|
|
114
|
+
return fetchUserPositionSnapshots(
|
|
115
|
+
args.userAddress,
|
|
116
|
+
environment,
|
|
117
|
+
args.period,
|
|
118
|
+
args.startTime,
|
|
119
|
+
args.endTime,
|
|
120
|
+
args.granularity,
|
|
121
|
+
);
|
|
39
122
|
}
|
|
40
123
|
|
|
41
124
|
async function fetchUserPositionSnapshots(
|
|
42
125
|
userAddress: Address,
|
|
43
126
|
environment: Environment,
|
|
127
|
+
period?: "1M" | "3M" | "1Y" | "ALL",
|
|
128
|
+
startTime?: number,
|
|
129
|
+
endTime?: number,
|
|
130
|
+
granularity?: "1h" | "6h" | "1d",
|
|
131
|
+
): Promise<UserPositionSnapshot[]> {
|
|
132
|
+
if (environment.lunarIndexerUrl) {
|
|
133
|
+
try {
|
|
134
|
+
const result = await fetchUserPositionSnapshotsFromLunar(
|
|
135
|
+
userAddress,
|
|
136
|
+
environment,
|
|
137
|
+
period,
|
|
138
|
+
startTime,
|
|
139
|
+
endTime,
|
|
140
|
+
granularity,
|
|
141
|
+
);
|
|
142
|
+
return result;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (!shouldFallback(error)) {
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
console.debug(
|
|
148
|
+
"[Lunar fallback] Falling back to Ponder for user snapshots:",
|
|
149
|
+
error,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Ponder fallback returns all available data (doesn't support time filtering)
|
|
155
|
+
const result = await fetchUserPositionSnapshotsFromPonder(
|
|
156
|
+
userAddress,
|
|
157
|
+
environment,
|
|
158
|
+
);
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function fetchUserPositionSnapshotsFromLunar(
|
|
163
|
+
userAddress: Address,
|
|
164
|
+
environment: Environment,
|
|
165
|
+
period?: "1M" | "3M" | "1Y" | "ALL",
|
|
166
|
+
customStartTime?: number,
|
|
167
|
+
customEndTime?: number,
|
|
168
|
+
granularity: "1h" | "6h" | "1d" = "1d",
|
|
169
|
+
): Promise<UserPositionSnapshot[]> {
|
|
170
|
+
if (!environment.lunarIndexerUrl) {
|
|
171
|
+
throw new Error("Lunar Indexer URL not configured");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const client = createLunarIndexerClient({
|
|
175
|
+
baseUrl: environment.lunarIndexerUrl,
|
|
176
|
+
timeout: DEFAULT_LUNAR_TIMEOUT_MS,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const { startTime, endTime } = calculateTimeRange(
|
|
180
|
+
period,
|
|
181
|
+
customStartTime,
|
|
182
|
+
customEndTime,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const portfolio = await client.getAccountPortfolio(
|
|
186
|
+
userAddress.toLowerCase(),
|
|
187
|
+
{
|
|
188
|
+
startTime,
|
|
189
|
+
endTime,
|
|
190
|
+
granularity,
|
|
191
|
+
chainId: environment.chainId,
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const snapshots = transformPortfolioToSnapshots(
|
|
196
|
+
portfolio,
|
|
197
|
+
environment.chainId,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// Find the first snapshot where user has any position
|
|
201
|
+
const firstNonZeroIndex = snapshots.findIndex(
|
|
202
|
+
(snapshot) =>
|
|
203
|
+
snapshot.totalSupplyUsd > 0 ||
|
|
204
|
+
snapshot.totalBorrowsUsd > 0 ||
|
|
205
|
+
snapshot.totalCollateralUsd > 0,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (firstNonZeroIndex === -1) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return snapshots.slice(firstNonZeroIndex);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function fetchUserPositionSnapshotsFromPonder(
|
|
216
|
+
userAddress: Address,
|
|
217
|
+
environment: Environment,
|
|
44
218
|
): Promise<UserPositionSnapshot[]> {
|
|
45
219
|
const dailyData: UserDailyData[] = [];
|
|
46
220
|
let hasNextPage = true;
|