@defisaver/positions-sdk 2.1.169-custom-fetching-dev → 2.1.169-custom-fetching-3-dev
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/cjs/aaveV2/index.d.ts +4 -4
- package/cjs/aaveV2/index.js +7 -7
- package/cjs/aaveV3/index.d.ts +4 -3
- package/cjs/aaveV3/index.js +14 -10
- package/cjs/aaveV3/merkl.d.ts +5 -1
- package/cjs/aaveV3/merkl.js +8 -3
- package/cjs/aaveV4/index.d.ts +4 -3
- package/cjs/aaveV4/index.js +19 -16
- package/cjs/aaveV4/merkl.d.ts +10 -2
- package/cjs/aaveV4/merkl.js +15 -6
- package/cjs/claiming/aaveV3.d.ts +2 -2
- package/cjs/claiming/aaveV3.js +5 -4
- package/cjs/claiming/king.d.ts +2 -2
- package/cjs/claiming/king.js +3 -2
- package/cjs/claiming/spark.d.ts +3 -3
- package/cjs/claiming/spark.js +8 -7
- package/cjs/claiming/uniswap.d.ts +2 -2
- package/cjs/claiming/uniswap.js +3 -2
- package/cjs/compoundV2/index.d.ts +4 -4
- package/cjs/compoundV2/index.js +9 -9
- package/cjs/compoundV3/index.d.ts +5 -4
- package/cjs/compoundV3/index.js +17 -13
- package/cjs/compoundV3/merkl.d.ts +1 -1
- package/cjs/compoundV3/merkl.js +2 -5
- package/cjs/curveUsd/index.d.ts +5 -5
- package/cjs/curveUsd/index.js +17 -17
- package/cjs/fluid/index.d.ts +23 -22
- package/cjs/fluid/index.js +75 -72
- package/cjs/fluid/merkl.d.ts +5 -3
- package/cjs/fluid/merkl.js +11 -6
- package/cjs/liquity/index.d.ts +4 -4
- package/cjs/liquity/index.js +23 -23
- package/cjs/liquityV2/index.d.ts +10 -10
- package/cjs/liquityV2/index.js +59 -59
- package/cjs/llamaLend/index.d.ts +5 -5
- package/cjs/llamaLend/index.js +13 -13
- package/cjs/maker/index.d.ts +6 -6
- package/cjs/maker/index.js +16 -16
- package/cjs/morphoBlue/index.d.ts +7 -7
- package/cjs/morphoBlue/index.js +28 -26
- package/cjs/morphoBlue/merkl.d.ts +5 -3
- package/cjs/morphoBlue/merkl.js +6 -8
- package/cjs/morphoMidnight/index.d.ts +5 -5
- package/cjs/morphoMidnight/index.js +18 -18
- package/cjs/portfolio/customDeFiData.js +45 -40
- package/cjs/services/merkl.d.ts +36 -2
- package/cjs/services/merkl.js +142 -19
- package/cjs/services/priceService.d.ts +12 -12
- package/cjs/services/priceService.js +30 -29
- package/cjs/services/viem.d.ts +10 -2
- package/cjs/services/viem.js +27 -1
- package/cjs/spark/index.d.ts +4 -4
- package/cjs/spark/index.js +9 -9
- package/cjs/types/merkl.d.ts +18 -0
- package/cjs/types/portfolio.d.ts +2 -0
- package/cjs/umbrella/index.d.ts +2 -2
- package/cjs/umbrella/index.js +5 -4
- package/esm/aaveV2/index.d.ts +4 -4
- package/esm/aaveV2/index.js +8 -8
- package/esm/aaveV3/index.d.ts +4 -3
- package/esm/aaveV3/index.js +15 -11
- package/esm/aaveV3/merkl.d.ts +5 -1
- package/esm/aaveV3/merkl.js +9 -4
- package/esm/aaveV4/index.d.ts +4 -3
- package/esm/aaveV4/index.js +20 -17
- package/esm/aaveV4/merkl.d.ts +10 -2
- package/esm/aaveV4/merkl.js +16 -7
- package/esm/claiming/aaveV3.d.ts +2 -2
- package/esm/claiming/aaveV3.js +5 -4
- package/esm/claiming/king.d.ts +2 -2
- package/esm/claiming/king.js +3 -2
- package/esm/claiming/spark.d.ts +3 -3
- package/esm/claiming/spark.js +8 -7
- package/esm/claiming/uniswap.d.ts +2 -2
- package/esm/claiming/uniswap.js +3 -2
- package/esm/compoundV2/index.d.ts +4 -4
- package/esm/compoundV2/index.js +9 -9
- package/esm/compoundV3/index.d.ts +5 -4
- package/esm/compoundV3/index.js +19 -15
- package/esm/compoundV3/merkl.d.ts +1 -1
- package/esm/compoundV3/merkl.js +3 -6
- package/esm/curveUsd/index.d.ts +5 -5
- package/esm/curveUsd/index.js +17 -17
- package/esm/fluid/index.d.ts +23 -22
- package/esm/fluid/index.js +76 -73
- package/esm/fluid/merkl.d.ts +5 -3
- package/esm/fluid/merkl.js +12 -7
- package/esm/liquity/index.d.ts +4 -4
- package/esm/liquity/index.js +23 -23
- package/esm/liquityV2/index.d.ts +10 -10
- package/esm/liquityV2/index.js +60 -60
- package/esm/llamaLend/index.d.ts +5 -5
- package/esm/llamaLend/index.js +13 -13
- package/esm/maker/index.d.ts +6 -6
- package/esm/maker/index.js +16 -16
- package/esm/morphoBlue/index.d.ts +7 -7
- package/esm/morphoBlue/index.js +29 -27
- package/esm/morphoBlue/merkl.d.ts +5 -3
- package/esm/morphoBlue/merkl.js +7 -9
- package/esm/morphoMidnight/index.d.ts +5 -5
- package/esm/morphoMidnight/index.js +18 -18
- package/esm/portfolio/customDeFiData.js +47 -42
- package/esm/services/merkl.d.ts +36 -2
- package/esm/services/merkl.js +136 -18
- package/esm/services/priceService.d.ts +12 -12
- package/esm/services/priceService.js +31 -30
- package/esm/services/viem.d.ts +10 -2
- package/esm/services/viem.js +25 -0
- package/esm/spark/index.d.ts +4 -4
- package/esm/spark/index.js +10 -10
- package/esm/types/merkl.d.ts +18 -0
- package/esm/types/portfolio.d.ts +2 -0
- package/esm/umbrella/index.d.ts +2 -2
- package/esm/umbrella/index.js +5 -4
- package/package.json +1 -1
- package/src/aaveV2/index.ts +7 -7
- package/src/aaveV3/index.ts +15 -11
- package/src/aaveV3/merkl.ts +9 -4
- package/src/aaveV4/index.ts +19 -16
- package/src/aaveV4/merkl.ts +16 -7
- package/src/claiming/aaveV3.ts +5 -3
- package/src/claiming/king.ts +6 -3
- package/src/claiming/spark.ts +9 -6
- package/src/claiming/uniswap.ts +4 -3
- package/src/compoundV2/index.ts +10 -9
- package/src/compoundV3/index.ts +20 -14
- package/src/compoundV3/merkl.ts +5 -4
- package/src/curveUsd/index.ts +19 -17
- package/src/fluid/index.ts +88 -70
- package/src/fluid/merkl.ts +12 -7
- package/src/liquity/index.ts +23 -23
- package/src/liquityV2/index.ts +64 -56
- package/src/llamaLend/index.ts +15 -13
- package/src/maker/index.ts +19 -16
- package/src/morphoBlue/index.ts +23 -20
- package/src/morphoBlue/merkl.ts +9 -6
- package/src/morphoMidnight/index.ts +13 -13
- package/src/portfolio/customDeFiData.ts +48 -41
- package/src/services/merkl.ts +152 -19
- package/src/services/priceService.ts +33 -30
- package/src/services/viem.ts +16 -0
- package/src/spark/index.ts +11 -8
- package/src/types/merkl.ts +20 -0
- package/src/types/portfolio.ts +2 -0
- package/src/umbrella/index.ts +6 -5
package/src/services/merkl.ts
CHANGED
|
@@ -1,31 +1,164 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
MerklAprRecord, MerklCampaignListItem, MerklOpportunity, OpportunityStatus,
|
|
3
|
+
} from '../types';
|
|
2
4
|
import { LONGER_TIMEOUT } from './utils';
|
|
3
5
|
|
|
4
|
-
const
|
|
6
|
+
const MERKL_API_URL = 'https://fe.defisaver.com/api/merkl';
|
|
7
|
+
const MERKL_OPPORTUNITIES_URL = `${MERKL_API_URL}/opportunities`;
|
|
8
|
+
const MERKL_CAMPAIGNS_URL = `${MERKL_API_URL}/campaigns`;
|
|
9
|
+
|
|
10
|
+
// Merkl caps list pages at 100 items; opportunity ids are batched below that so a single page
|
|
11
|
+
// covers every id in the batch.
|
|
12
|
+
const MERKL_MAX_PAGE_ITEMS = 100;
|
|
13
|
+
const OPPORTUNITY_ID_BATCH_SIZE = 50;
|
|
14
|
+
// APR history is fetched per opportunity, so historical lookups are throttled to keep a block with
|
|
15
|
+
// many live campaigns from firing every request at once.
|
|
16
|
+
const APR_RECORDS_CONCURRENCY = 10;
|
|
17
|
+
const SECONDS_PER_DAY = 24 * 60 * 60;
|
|
18
|
+
|
|
19
|
+
const merklGet = async <T>(url: string, query: Record<string, string>, label: string): Promise<T> => {
|
|
20
|
+
const res = await fetch(`${url}?${new URLSearchParams(query)}`, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
|
|
21
|
+
if (!res.ok) throw new Error(`Failed to fetch Merkl ${label}`);
|
|
22
|
+
return res.json();
|
|
23
|
+
};
|
|
5
24
|
|
|
6
25
|
/**
|
|
7
|
-
* Fetches every page matching the query
|
|
8
|
-
* `items`
|
|
26
|
+
* Fetches every page matching the query, advancing `page` until an empty page comes back. When
|
|
27
|
+
* `items` is omitted, Merkl's default page size applies and no client-side maximum is imposed.
|
|
9
28
|
*/
|
|
10
|
-
|
|
11
|
-
const
|
|
29
|
+
const fetchAllMerklPages = async <T>(url: string, query: Record<string, string>, label: string, items?: number): Promise<T[]> => {
|
|
30
|
+
const results: T[] = [];
|
|
12
31
|
let page = 0;
|
|
13
|
-
let
|
|
32
|
+
let pageResults: T[];
|
|
14
33
|
|
|
15
34
|
do {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
});
|
|
20
|
-
if (!res.ok) throw new Error(`Failed to fetch Merkl opportunities page ${page}`);
|
|
35
|
+
const pageQuery = { ...query, page: page.toString(), ...(items ? { items: items.toString() } : {}) };
|
|
36
|
+
const data = await merklGet<unknown>(url, pageQuery, `${label} page ${page}`); // eslint-disable-line no-await-in-loop
|
|
37
|
+
if (!Array.isArray(data)) throw new Error(`Invalid Merkl ${label} response on page ${page}`);
|
|
21
38
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
pageOpportunities = data as MerklOpportunity[];
|
|
26
|
-
opportunities.push(...pageOpportunities);
|
|
39
|
+
pageResults = data as T[];
|
|
40
|
+
results.push(...pageResults);
|
|
27
41
|
page += 1;
|
|
28
|
-
} while (
|
|
42
|
+
} while (pageResults.length > 0);
|
|
43
|
+
|
|
44
|
+
return results;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Fetches every opportunity matching the query. Merkl returns 20 opportunities by default, so omitting
|
|
49
|
+
* `items` and advancing `page` avoids imposing a client-side maximum on the result set.
|
|
50
|
+
*/
|
|
51
|
+
export const fetchAllMerklOpportunities = async (query: Record<string, string>): Promise<MerklOpportunity[]> => (
|
|
52
|
+
fetchAllMerklPages<MerklOpportunity>(MERKL_OPPORTUNITIES_URL, query, 'opportunities')
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/** Fetches every campaign matching the query, e.g. `{ mainProtocolId: 'aave', status: 'LIVE,PAST' }`. */
|
|
56
|
+
export const fetchAllMerklCampaigns = async (query: Record<string, string>): Promise<MerklCampaignListItem[]> => (
|
|
57
|
+
fetchAllMerklPages<MerklCampaignListItem>(MERKL_CAMPAIGNS_URL, query, 'campaigns', MERKL_MAX_PAGE_ITEMS)
|
|
58
|
+
);
|
|
29
59
|
|
|
30
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Fetches the opportunities with the given Merkl-internal ids, regardless of their current status.
|
|
62
|
+
* Ids Merkl no longer serves (hidden or test campaigns) are simply absent from the result.
|
|
63
|
+
*/
|
|
64
|
+
export const fetchMerklOpportunitiesByIds = async (ids: string[]): Promise<MerklOpportunity[]> => {
|
|
65
|
+
const batches: string[][] = [];
|
|
66
|
+
for (let i = 0; i < ids.length; i += OPPORTUNITY_ID_BATCH_SIZE) batches.push(ids.slice(i, i + OPPORTUNITY_ID_BATCH_SIZE));
|
|
67
|
+
const results = await Promise.all(batches.map((batch) => merklGet<unknown>(
|
|
68
|
+
MERKL_OPPORTUNITIES_URL,
|
|
69
|
+
{ id: batch.join(','), items: MERKL_MAX_PAGE_ITEMS.toString() },
|
|
70
|
+
'opportunities by id',
|
|
71
|
+
)));
|
|
72
|
+
return results.flatMap((data) => {
|
|
73
|
+
if (!Array.isArray(data)) throw new Error('Invalid Merkl opportunities by id response');
|
|
74
|
+
return data as MerklOpportunity[];
|
|
75
|
+
});
|
|
31
76
|
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns the reward APR Merkl recorded for the opportunity at the given unix timestamp (seconds),
|
|
80
|
+
* or `undefined` when no campaign was computing for it then. Records are hourly buckets keyed by
|
|
81
|
+
* their start, newest first, so the first bucket at or before the timestamp carries the value in
|
|
82
|
+
* force at that moment; the window is a day wide to tolerate computation gaps.
|
|
83
|
+
*/
|
|
84
|
+
export const fetchMerklOpportunityAprAt = async (opportunityId: string, timestamp: number): Promise<number | undefined> => {
|
|
85
|
+
const records = await merklGet<unknown>(`${MERKL_OPPORTUNITIES_URL}/${opportunityId}/apr-records`, {
|
|
86
|
+
grain: 'HOUR',
|
|
87
|
+
from: (timestamp - SECONDS_PER_DAY).toString(),
|
|
88
|
+
to: timestamp.toString(),
|
|
89
|
+
items: '1',
|
|
90
|
+
}, `APR records for opportunity ${opportunityId}`);
|
|
91
|
+
if (!Array.isArray(records)) throw new Error(`Invalid Merkl APR records response for opportunity ${opportunityId}`);
|
|
92
|
+
return (records as MerklAprRecord[])[0]?.apr;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const mapWithConcurrency = async <T, R>(items: T[], concurrency: number, fn: (item: T) => Promise<R>): Promise<R[]> => {
|
|
96
|
+
const results: R[] = [];
|
|
97
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
98
|
+
results.push(...await Promise.all(items.slice(i, i + concurrency).map(fn))); // eslint-disable-line no-await-in-loop
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Reconstructs the opportunities that were live at `timestamp` (unix seconds), shaped like the
|
|
105
|
+
* live listing so the per-protocol reward mappers can consume either without knowing which they got:
|
|
106
|
+
* 1. campaigns for the protocol (and chain, when the query names one) whose [start, end] covers
|
|
107
|
+
* the timestamp identify the opportunities that were rewarded then — the live listing can't,
|
|
108
|
+
* since campaigns live then are mostly PAST now and ones live now didn't exist then;
|
|
109
|
+
* 2. those opportunities are loaded by id (works for PAST ones too) and narrowed to `query.type`;
|
|
110
|
+
* 3. each one's `apr` is replaced with the value Merkl recorded at the timestamp, and ones with no
|
|
111
|
+
* record then are dropped, since nothing was being computed for them.
|
|
112
|
+
* `status` is forced to LIVE so status filters downstream keep working, `campaigns` carries the
|
|
113
|
+
* covering campaigns, and a reward token breakdown is synthesized from the campaign's reward token
|
|
114
|
+
* when Merkl no longer serves one for the opportunity (it drops them once an opportunity is PAST).
|
|
115
|
+
* `query.status` is ignored: it describes the present, not the requested moment.
|
|
116
|
+
*/
|
|
117
|
+
export const fetchMerklOpportunitiesAt = async (query: Record<string, string>, timestamp: number): Promise<MerklOpportunity[]> => {
|
|
118
|
+
const campaignQuery: Record<string, string> = { status: 'LIVE,PAST', endTimestamp: timestamp.toString() };
|
|
119
|
+
if (query.mainProtocolId) campaignQuery.mainProtocolId = query.mainProtocolId;
|
|
120
|
+
if (query.chainId) campaignQuery.chainId = query.chainId;
|
|
121
|
+
|
|
122
|
+
// `endTimestamp` only narrows the listing upstream — the exact window check happens here
|
|
123
|
+
const campaigns = (await fetchAllMerklCampaigns(campaignQuery))
|
|
124
|
+
.filter((c) => c.opportunityId && +(c.startTimestamp ?? 0) <= timestamp && timestamp <= +(c.endTimestamp ?? 0));
|
|
125
|
+
const campaignsByOpportunity: Record<string, MerklCampaignListItem[]> = {};
|
|
126
|
+
campaigns.forEach((c) => {
|
|
127
|
+
if (!campaignsByOpportunity[c.opportunityId]) campaignsByOpportunity[c.opportunityId] = [];
|
|
128
|
+
campaignsByOpportunity[c.opportunityId].push(c);
|
|
129
|
+
});
|
|
130
|
+
const opportunityIds = Object.keys(campaignsByOpportunity);
|
|
131
|
+
if (!opportunityIds.length) return [];
|
|
132
|
+
|
|
133
|
+
const types = query.type?.split(',').filter(Boolean);
|
|
134
|
+
const opportunities = (await fetchMerklOpportunitiesByIds(opportunityIds))
|
|
135
|
+
.filter((o) => !types?.length || types.includes(o.type));
|
|
136
|
+
|
|
137
|
+
const aprs = await mapWithConcurrency(opportunities, APR_RECORDS_CONCURRENCY, (o) => fetchMerklOpportunityAprAt(o.id, timestamp));
|
|
138
|
+
|
|
139
|
+
return opportunities.flatMap((o, i) => {
|
|
140
|
+
const apr = aprs[i];
|
|
141
|
+
if (apr === undefined) return [];
|
|
142
|
+
const coveringCampaigns = campaignsByOpportunity[o.id];
|
|
143
|
+
const rewardToken = coveringCampaigns.find((c) => c.rewardToken)?.rewardToken;
|
|
144
|
+
const hasBreakdown = !!o.rewardsRecord?.breakdowns?.length;
|
|
145
|
+
return [{
|
|
146
|
+
...o,
|
|
147
|
+
status: OpportunityStatus.LIVE,
|
|
148
|
+
apr,
|
|
149
|
+
campaigns: coveringCampaigns,
|
|
150
|
+
rewardsRecord: hasBreakdown || !rewardToken ? o.rewardsRecord : {
|
|
151
|
+
...o.rewardsRecord,
|
|
152
|
+
breakdowns: [{ token: rewardToken, campaignId: coveringCampaigns[0].campaignId } as MerklOpportunity['rewardsRecord']['breakdowns'][number]],
|
|
153
|
+
},
|
|
154
|
+
}];
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Live opportunities when no timestamp is given, otherwise the opportunities as they were at that
|
|
160
|
+
* unix timestamp (seconds) — see `fetchMerklOpportunitiesAt`.
|
|
161
|
+
*/
|
|
162
|
+
export const fetchMerklOpportunities = async (query: Record<string, string>, asOfTimestamp?: number): Promise<MerklOpportunity[]> => (
|
|
163
|
+
asOfTimestamp === undefined ? fetchAllMerklOpportunities(query) : fetchMerklOpportunitiesAt(query, asOfTimestamp)
|
|
164
|
+
);
|
|
@@ -10,36 +10,39 @@ import {
|
|
|
10
10
|
WeETHPriceFeedContractViem,
|
|
11
11
|
WstETHPriceFeedContractViem,
|
|
12
12
|
} from '../contracts';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
Blockish, EthAddress, NetworkNumber, HexString,
|
|
15
|
+
} from '../types/common';
|
|
14
16
|
import { getEthAmountForDecimals } from './utils';
|
|
17
|
+
import { setViemBlockNumber } from './viem';
|
|
15
18
|
import { USD_QUOTE } from '../constants';
|
|
16
19
|
|
|
17
|
-
export const getEthPrice = async (client: Client) => {
|
|
20
|
+
export const getEthPrice = async (client: Client, block: Blockish = 'latest') => {
|
|
18
21
|
const contract = ETHPriceFeedContractViem(client, NetworkNumber.Eth);
|
|
19
|
-
const price = await contract.read.latestAnswer();
|
|
22
|
+
const price = await contract.read.latestAnswer(setViemBlockNumber(block));
|
|
20
23
|
return new Dec(price.toString()).div(1e8).toString();
|
|
21
24
|
};
|
|
22
25
|
|
|
23
|
-
export const getUSDCPrice = async (client: Client) => {
|
|
26
|
+
export const getUSDCPrice = async (client: Client, block: Blockish = 'latest') => {
|
|
24
27
|
const contract = USDCPriceFeedContractViem(client, NetworkNumber.Eth);
|
|
25
|
-
const price = await contract.read.latestAnswer();
|
|
28
|
+
const price = await contract.read.latestAnswer(setViemBlockNumber(block));
|
|
26
29
|
return new Dec(price.toString()).div(1e8).toString();
|
|
27
30
|
};
|
|
28
31
|
|
|
29
|
-
export const getCompPrice = async (client: Client) => {
|
|
32
|
+
export const getCompPrice = async (client: Client, block: Blockish = 'latest') => {
|
|
30
33
|
const contract = COMPPriceFeedContractViem(client, NetworkNumber.Eth);
|
|
31
|
-
const price = await contract.read.latestAnswer();
|
|
34
|
+
const price = await contract.read.latestAnswer(setViemBlockNumber(block));
|
|
32
35
|
return new Dec(price.toString()).div(1e8).toString();
|
|
33
36
|
};
|
|
34
37
|
|
|
35
|
-
export const getWstETHPrice = async (client: Client, network: NetworkNumber = NetworkNumber.Eth) => {
|
|
38
|
+
export const getWstETHPrice = async (client: Client, network: NetworkNumber = NetworkNumber.Eth, block: Blockish = 'latest') => {
|
|
36
39
|
const wstETHFeedContract = WstETHPriceFeedContractViem(client, network);
|
|
37
40
|
const ethFeedContract = ETHPriceFeedContractViem(client, network);
|
|
38
41
|
|
|
39
42
|
const [ethPriceWei, wstETHRateWei, decimals] = await Promise.all([
|
|
40
|
-
ethFeedContract.read.latestAnswer(),
|
|
41
|
-
wstETHFeedContract.read.latestRoundData(),
|
|
42
|
-
wstETHFeedContract.read.decimals(),
|
|
43
|
+
ethFeedContract.read.latestAnswer(setViemBlockNumber(block)),
|
|
44
|
+
wstETHFeedContract.read.latestRoundData(setViemBlockNumber(block)),
|
|
45
|
+
wstETHFeedContract.read.decimals(setViemBlockNumber(block)),
|
|
43
46
|
]);
|
|
44
47
|
|
|
45
48
|
const ethPrice = new Dec(ethPriceWei.toString()).div(1e8);
|
|
@@ -49,14 +52,14 @@ export const getWstETHPrice = async (client: Client, network: NetworkNumber = Ne
|
|
|
49
52
|
return new Dec(ethPrice).mul(wstETHRate).toString();
|
|
50
53
|
};
|
|
51
54
|
|
|
52
|
-
export const getWeETHPrice = async (client: Client, network: NetworkNumber = NetworkNumber.Eth) => {
|
|
55
|
+
export const getWeETHPrice = async (client: Client, network: NetworkNumber = NetworkNumber.Eth, block: Blockish = 'latest') => {
|
|
53
56
|
const weETHFeedContract = WeETHPriceFeedContractViem(client, network);
|
|
54
57
|
const ethFeedContract = ETHPriceFeedContractViem(client, network);
|
|
55
58
|
|
|
56
59
|
const [ethPriceWei, weETHRateWei, decimals] = await Promise.all([
|
|
57
|
-
ethFeedContract.read.latestAnswer(),
|
|
58
|
-
weETHFeedContract.read.latestRoundData(),
|
|
59
|
-
weETHFeedContract.read.decimals(),
|
|
60
|
+
ethFeedContract.read.latestAnswer(setViemBlockNumber(block)),
|
|
61
|
+
weETHFeedContract.read.latestRoundData(setViemBlockNumber(block)),
|
|
62
|
+
weETHFeedContract.read.decimals(setViemBlockNumber(block)),
|
|
60
63
|
]);
|
|
61
64
|
|
|
62
65
|
const ethPrice = new Dec(ethPriceWei.toString()).div(1e8);
|
|
@@ -66,11 +69,11 @@ export const getWeETHPrice = async (client: Client, network: NetworkNumber = Net
|
|
|
66
69
|
return new Dec(ethPrice).mul(weETHRate).toString();
|
|
67
70
|
};
|
|
68
71
|
|
|
69
|
-
export const getWsrETHPrice = async (client: Client, network: NetworkNumber = NetworkNumber.Eth) => {
|
|
72
|
+
export const getWsrETHPrice = async (client: Client, network: NetworkNumber = NetworkNumber.Eth, block: Blockish = 'latest') => {
|
|
70
73
|
const feedRegistryContract = DFSFeedRegistryContractViem(client, network);
|
|
71
74
|
const [ethPriceWei, wrsETHRateWei] = await Promise.all([
|
|
72
|
-
feedRegistryContract.read.latestRoundData([getAssetInfo('WETH', network).address as HexString, USD_QUOTE]),
|
|
73
|
-
feedRegistryContract.read.latestRoundData([getAssetInfo('wrsETH', network).address as HexString, getAssetInfo('WETH', network).address as HexString]),
|
|
75
|
+
feedRegistryContract.read.latestRoundData([getAssetInfo('WETH', network).address as HexString, USD_QUOTE], setViemBlockNumber(block)),
|
|
76
|
+
feedRegistryContract.read.latestRoundData([getAssetInfo('wrsETH', network).address as HexString, getAssetInfo('WETH', network).address as HexString], setViemBlockNumber(block)),
|
|
74
77
|
]);
|
|
75
78
|
|
|
76
79
|
const ethPrice = new Dec(ethPriceWei[1].toString()).div(1e8);
|
|
@@ -80,11 +83,11 @@ export const getWsrETHPrice = async (client: Client, network: NetworkNumber = Ne
|
|
|
80
83
|
return new Dec(ethPrice).mul(wrsETHRate).toString();
|
|
81
84
|
};
|
|
82
85
|
|
|
83
|
-
export const getSyrupUSDTPrice = async (client: Client, network: NetworkNumber = NetworkNumber.Eth) => {
|
|
86
|
+
export const getSyrupUSDTPrice = async (client: Client, network: NetworkNumber = NetworkNumber.Eth, block: Blockish = 'latest') => {
|
|
84
87
|
const feedRegistryContract = DFSFeedRegistryContractViem(client, network);
|
|
85
88
|
const [usdtPriceWei, syrupUSDTRateWei] = await Promise.all([
|
|
86
|
-
feedRegistryContract.read.latestRoundData([getAssetInfo('USDT', network).address as HexString, USD_QUOTE]),
|
|
87
|
-
feedRegistryContract.read.latestRoundData([getAssetInfo('syrupUSDT', network).address as HexString, getAssetInfo('USDT', network).address as HexString]),
|
|
89
|
+
feedRegistryContract.read.latestRoundData([getAssetInfo('USDT', network).address as HexString, USD_QUOTE], setViemBlockNumber(block)),
|
|
90
|
+
feedRegistryContract.read.latestRoundData([getAssetInfo('syrupUSDT', network).address as HexString, getAssetInfo('USDT', network).address as HexString], setViemBlockNumber(block)),
|
|
88
91
|
]);
|
|
89
92
|
|
|
90
93
|
const usdtPrice = new Dec(usdtPriceWei[1].toString()).div(1e8);
|
|
@@ -94,11 +97,11 @@ export const getSyrupUSDTPrice = async (client: Client, network: NetworkNumber =
|
|
|
94
97
|
return new Dec(usdtPrice).mul(syrupUSDTPrice).toString();
|
|
95
98
|
};
|
|
96
99
|
|
|
97
|
-
export const getWstUSRPrice = async (client: Client, network: NetworkNumber = NetworkNumber.Eth) => {
|
|
100
|
+
export const getWstUSRPrice = async (client: Client, network: NetworkNumber = NetworkNumber.Eth, block: Blockish = 'latest') => {
|
|
98
101
|
const feedRegistryContract = DFSFeedRegistryContractViem(client, network);
|
|
99
102
|
const [usrPriceWei, wstUSRRateWei] = await Promise.all([
|
|
100
|
-
feedRegistryContract.read.latestRoundData([getAssetInfo('USR', network).address as HexString, USD_QUOTE]),
|
|
101
|
-
feedRegistryContract.read.latestRoundData([getAssetInfo('wstUSR', network).address as HexString, getAssetInfo('USR', network).address as HexString]),
|
|
103
|
+
feedRegistryContract.read.latestRoundData([getAssetInfo('USR', network).address as HexString, USD_QUOTE], setViemBlockNumber(block)),
|
|
104
|
+
feedRegistryContract.read.latestRoundData([getAssetInfo('wstUSR', network).address as HexString, getAssetInfo('USR', network).address as HexString], setViemBlockNumber(block)),
|
|
102
105
|
]);
|
|
103
106
|
|
|
104
107
|
const usrPrice = new Dec(usrPriceWei[1].toString()).div(1e8);
|
|
@@ -248,24 +251,24 @@ export const parseWeETHPriceCalls = (_ethPrice: string, weETHrateAnswer: string,
|
|
|
248
251
|
};
|
|
249
252
|
|
|
250
253
|
// this is a fixed version, the original version is above but requires to refactor comp v3 function, so it's easier to just copy the function for now
|
|
251
|
-
export const getWstETHPriceFluid = async (client: PublicClient, network: NetworkNumber) => {
|
|
254
|
+
export const getWstETHPriceFluid = async (client: PublicClient, network: NetworkNumber, block: Blockish = 'latest') => {
|
|
252
255
|
const calls = getWstETHChainLinkPriceCalls(client, network);
|
|
253
|
-
const results = await client.multicall({ contracts: calls });
|
|
256
|
+
const results = await client.multicall({ contracts: calls, ...setViemBlockNumber(block) });
|
|
254
257
|
// @ts-ignore
|
|
255
258
|
const { ethPrice, wstETHRate } = parseWstETHPriceCalls(results[0].result!.toString(), results[1].result[1]!.toString(), results[2].result!.toString());
|
|
256
259
|
|
|
257
260
|
return new Dec(ethPrice).mul(wstETHRate).toString();
|
|
258
261
|
};
|
|
259
262
|
|
|
260
|
-
export const getBTCPriceForFluid = async (client: PublicClient, network: NetworkNumber) => {
|
|
263
|
+
export const getBTCPriceForFluid = async (client: PublicClient, network: NetworkNumber, block: Blockish = 'latest') => {
|
|
261
264
|
const contract = BTCPriceFeedContractViem(client, network);
|
|
262
|
-
const price = await contract.read.latestAnswer();
|
|
265
|
+
const price = await contract.read.latestAnswer(setViemBlockNumber(block));
|
|
263
266
|
return new Dec(price).div(1e8).toString();
|
|
264
267
|
};
|
|
265
268
|
|
|
266
|
-
export const getEthPriceForFluid = async (client: PublicClient, network: NetworkNumber) => {
|
|
269
|
+
export const getEthPriceForFluid = async (client: PublicClient, network: NetworkNumber, block: Blockish = 'latest') => {
|
|
267
270
|
const contract = ETHPriceFeedContractViem(client, network);
|
|
268
|
-
const price = await contract.read.latestAnswer();
|
|
271
|
+
const price = await contract.read.latestAnswer(setViemBlockNumber(block));
|
|
269
272
|
return new Dec(price).div(1e8).toString();
|
|
270
273
|
};
|
|
271
274
|
|
package/src/services/viem.ts
CHANGED
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
custom,
|
|
4
4
|
encodeFunctionData,
|
|
5
5
|
type Abi,
|
|
6
|
+
type Client,
|
|
6
7
|
} from 'viem';
|
|
8
|
+
import { getBlock } from 'viem/actions';
|
|
7
9
|
import {
|
|
8
10
|
arbitrum, base, mainnet, optimism, linea, plasma,
|
|
9
11
|
} from 'viem/chains';
|
|
@@ -34,6 +36,20 @@ export const getViemProvider = (provider: EthereumProvider, network: NetworkNumb
|
|
|
34
36
|
...options,
|
|
35
37
|
});
|
|
36
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Unix timestamp (seconds) of the block, for data sources keyed by time rather than block (Merkl
|
|
41
|
+
* reward history). `'latest'` resolves to `undefined` without an RPC call, since live sources need
|
|
42
|
+
* no timestamp. A numeric block costs one `eth_getBlockByNumber` unless the caller already knows
|
|
43
|
+
* its timestamp and passes it — flows that fetch many markets at one block resolve it once and
|
|
44
|
+
* hand it down.
|
|
45
|
+
*/
|
|
46
|
+
export const getBlockTimestamp = async (client: Client, block: Blockish, blockTimestamp?: number): Promise<number | undefined> => {
|
|
47
|
+
if (block === 'latest') return undefined;
|
|
48
|
+
if (blockTimestamp !== undefined) return blockTimestamp;
|
|
49
|
+
const { timestamp } = await getBlock(client, { blockNumber: BigInt(block) });
|
|
50
|
+
return Number(timestamp);
|
|
51
|
+
};
|
|
52
|
+
|
|
37
53
|
export const setViemBlockNumber = (block: Blockish) => {
|
|
38
54
|
if (block === 'latest') return {};
|
|
39
55
|
return { blockNumber: BigInt(block) };
|
package/src/spark/index.ts
CHANGED
|
@@ -62,7 +62,7 @@ export const sparkEmodeCategoriesMapping = (extractedState: { assetsData: SparkA
|
|
|
62
62
|
return categoriesMapping;
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
export const _getSparkMarketsData = async (provider: Client, network: NetworkNumber, selectedMarket: SparkMarketData): Promise<SparkMarketsData> => {
|
|
65
|
+
export const _getSparkMarketsData = async (provider: Client, network: NetworkNumber, selectedMarket: SparkMarketData, blockNumber: Blockish = 'latest'): Promise<SparkMarketsData> => {
|
|
66
66
|
const marketAddress = selectedMarket.providerAddress;
|
|
67
67
|
|
|
68
68
|
const loanInfoContract = SparkViewContractViem(provider, network);
|
|
@@ -70,8 +70,8 @@ export const _getSparkMarketsData = async (provider: Client, network: NetworkNum
|
|
|
70
70
|
|
|
71
71
|
// eslint-disable-next-line prefer-const
|
|
72
72
|
let [loanInfo, rewardInfo] = await Promise.all([
|
|
73
|
-
loanInfoContract.read.getFullTokensInfo([marketAddress, selectedMarket.assets.map(a => getAssetInfo(ethToWeth(a)).address) as EthAddress[]]),
|
|
74
|
-
sparkIncentivesContract.read.getReservesIncentivesData([marketAddress]),
|
|
73
|
+
loanInfoContract.read.getFullTokensInfo([marketAddress, selectedMarket.assets.map(a => getAssetInfo(ethToWeth(a)).address) as EthAddress[]], setViemBlockNumber(blockNumber)),
|
|
74
|
+
sparkIncentivesContract.read.getReservesIncentivesData([marketAddress], setViemBlockNumber(blockNumber)),
|
|
75
75
|
]);
|
|
76
76
|
|
|
77
77
|
rewardInfo = rewardInfo.reduce((all: any, market: any) => {
|
|
@@ -246,7 +246,8 @@ export const getSparkMarketsData = async (
|
|
|
246
246
|
provider: EthereumProvider,
|
|
247
247
|
network: NetworkNumber,
|
|
248
248
|
selectedMarket: SparkMarketData,
|
|
249
|
-
|
|
249
|
+
blockNumber: Blockish = 'latest',
|
|
250
|
+
): Promise<SparkMarketsData> => _getSparkMarketsData(getViemProvider(provider, network), network, selectedMarket, blockNumber);
|
|
250
251
|
|
|
251
252
|
export const EMPTY_SPARK_DATA = {
|
|
252
253
|
usedAssets: {},
|
|
@@ -287,6 +288,7 @@ export const _getSparkAccountData = async (
|
|
|
287
288
|
network: NetworkNumber,
|
|
288
289
|
address: EthAddress,
|
|
289
290
|
extractedState: { selectedMarket: SparkMarketData, assetsData: SparkAssetsData, eModeCategoriesData: EModeCategoriesData },
|
|
291
|
+
blockNumber: Blockish = 'latest',
|
|
290
292
|
) => {
|
|
291
293
|
const {
|
|
292
294
|
selectedMarket: market, assetsData,
|
|
@@ -313,9 +315,9 @@ export const _getSparkAccountData = async (
|
|
|
313
315
|
const middleAddressIndex = Math.floor(_addresses.length / 2); // split addresses in half to avoid gas limit by multicall
|
|
314
316
|
|
|
315
317
|
const [userEMode, tokenBalances1, tokenBalances2] = await Promise.all([
|
|
316
|
-
lendingPoolContract.read.getUserEMode([address]),
|
|
317
|
-
loanInfoContract.read.getTokenBalances([marketAddress, address, _addresses.slice(0, middleAddressIndex) as EthAddress[]]),
|
|
318
|
-
loanInfoContract.read.getTokenBalances([marketAddress, address, _addresses.slice(middleAddressIndex, _addresses.length) as EthAddress[]]),
|
|
318
|
+
lendingPoolContract.read.getUserEMode([address], setViemBlockNumber(blockNumber)),
|
|
319
|
+
loanInfoContract.read.getTokenBalances([marketAddress, address, _addresses.slice(0, middleAddressIndex) as EthAddress[]], setViemBlockNumber(blockNumber)),
|
|
320
|
+
loanInfoContract.read.getTokenBalances([marketAddress, address, _addresses.slice(middleAddressIndex, _addresses.length) as EthAddress[]], setViemBlockNumber(blockNumber)),
|
|
319
321
|
]);
|
|
320
322
|
|
|
321
323
|
|
|
@@ -406,7 +408,8 @@ export const getSparkAccountData = async (
|
|
|
406
408
|
network: NetworkNumber,
|
|
407
409
|
address: EthAddress,
|
|
408
410
|
extractedState: { selectedMarket: SparkMarketData, assetsData: SparkAssetsData, eModeCategoriesData: EModeCategoriesData },
|
|
409
|
-
|
|
411
|
+
blockNumber: Blockish = 'latest',
|
|
412
|
+
) => _getSparkAccountData(getViemProvider(provider, network), network, address, extractedState, blockNumber);
|
|
410
413
|
|
|
411
414
|
export const getSparkFullPositionData = async (provider: EthereumProvider, network: NetworkNumber, address: EthAddress, market: SparkMarketData): Promise<SparkPositionData> => {
|
|
412
415
|
const marketData = await getSparkMarketsData(provider, network, market);
|
package/src/types/merkl.ts
CHANGED
|
@@ -114,6 +114,26 @@ export type MerklOpportunity = {
|
|
|
114
114
|
};
|
|
115
115
|
};
|
|
116
116
|
|
|
117
|
+
/** One entry of Merkl's campaign listing: a campaign plus the opportunity it rewards. */
|
|
118
|
+
export type MerklCampaignListItem = MerklCampaign & {
|
|
119
|
+
opportunityId: string;
|
|
120
|
+
type: string;
|
|
121
|
+
rewardToken?: MerklOpportunity['rewardsRecord']['breakdowns'][number]['token'];
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** One bucket of an opportunity's APR history (`apr` is the last value recorded in the bucket). */
|
|
125
|
+
export type MerklAprRecord = {
|
|
126
|
+
timestamp: string;
|
|
127
|
+
date: string;
|
|
128
|
+
apr: number;
|
|
129
|
+
open: number;
|
|
130
|
+
min: number;
|
|
131
|
+
max: number;
|
|
132
|
+
avg: number;
|
|
133
|
+
samples: number;
|
|
134
|
+
hasUntrustedContribution: boolean;
|
|
135
|
+
};
|
|
136
|
+
|
|
117
137
|
export type MerkleRewardInfo = { apy: string; rewardTokenSymbol: string, description: string, identifier: string };
|
|
118
138
|
export type MerkleRewardMap = Record<EthAddress, { supply?: MerkleRewardInfo; borrow?: MerkleRewardInfo }>;
|
|
119
139
|
|
package/src/types/portfolio.ts
CHANGED
|
@@ -95,6 +95,8 @@ export interface CustomDeFiDataOptions {
|
|
|
95
95
|
isSim?: boolean;
|
|
96
96
|
/** viem multicall calldata cap in bytes; defaults to 250k (2k when isSim). */
|
|
97
97
|
batchSize?: number;
|
|
98
|
+
/** Every read is made at this block; defaults to latest. */
|
|
99
|
+
blockNumber?: number;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
export interface CustomDeFiData {
|
package/src/umbrella/index.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { Client } from 'viem';
|
|
2
2
|
import { AaveUmbrellaViewViem, AaveV3ViewContractViem } from '../contracts';
|
|
3
|
-
import { EthAddress, NetworkNumber } from '../types/common';
|
|
3
|
+
import { Blockish, EthAddress, NetworkNumber } from '../types/common';
|
|
4
4
|
import { compareAddresses, convertHybridArraysToObjects, getEthAmountForDecimals } from '../services/utils';
|
|
5
|
+
import { setViemBlockNumber } from '../services/viem';
|
|
5
6
|
import { findMatching, tokenEntries } from './umbrellaUtils';
|
|
6
7
|
|
|
7
8
|
const umbrella = '0xD400fc38ED4732893174325693a63C30ee3881a8';
|
|
8
9
|
const aaveOracle = '0x54586bE62E3c3580375aE3723C145253060Ca0C2';
|
|
9
10
|
|
|
10
|
-
export const getUmbrellaData = async (provider: Client, network: NetworkNumber, address: EthAddress) => {
|
|
11
|
+
export const getUmbrellaData = async (provider: Client, network: NetworkNumber, address: EthAddress, blockNumber: Blockish = 'latest') => {
|
|
11
12
|
const umbrellaView = AaveUmbrellaViewViem(provider, network);
|
|
12
13
|
const aaveV3View = AaveV3ViewContractViem(provider, network);
|
|
13
14
|
const [tokensAggregatedData, additionalUmbrellaStakingData, userAggregatedData] = await Promise.all([
|
|
14
|
-
umbrellaView.read.getTokensAggregatedData([umbrella, aaveOracle]),
|
|
15
|
-
aaveV3View.read.getAdditionalUmbrellaStakingData([umbrella, address]),
|
|
16
|
-
umbrellaView.read.getUserAggregatedData([umbrella, address]),
|
|
15
|
+
umbrellaView.read.getTokensAggregatedData([umbrella, aaveOracle], setViemBlockNumber(blockNumber)),
|
|
16
|
+
aaveV3View.read.getAdditionalUmbrellaStakingData([umbrella, address], setViemBlockNumber(blockNumber)),
|
|
17
|
+
umbrellaView.read.getUserAggregatedData([umbrella, address], setViemBlockNumber(blockNumber)),
|
|
17
18
|
]);
|
|
18
19
|
|
|
19
20
|
return Object.fromEntries(
|