@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.
Files changed (145) hide show
  1. package/cjs/aaveV2/index.d.ts +4 -4
  2. package/cjs/aaveV2/index.js +7 -7
  3. package/cjs/aaveV3/index.d.ts +4 -3
  4. package/cjs/aaveV3/index.js +14 -10
  5. package/cjs/aaveV3/merkl.d.ts +5 -1
  6. package/cjs/aaveV3/merkl.js +8 -3
  7. package/cjs/aaveV4/index.d.ts +4 -3
  8. package/cjs/aaveV4/index.js +19 -16
  9. package/cjs/aaveV4/merkl.d.ts +10 -2
  10. package/cjs/aaveV4/merkl.js +15 -6
  11. package/cjs/claiming/aaveV3.d.ts +2 -2
  12. package/cjs/claiming/aaveV3.js +5 -4
  13. package/cjs/claiming/king.d.ts +2 -2
  14. package/cjs/claiming/king.js +3 -2
  15. package/cjs/claiming/spark.d.ts +3 -3
  16. package/cjs/claiming/spark.js +8 -7
  17. package/cjs/claiming/uniswap.d.ts +2 -2
  18. package/cjs/claiming/uniswap.js +3 -2
  19. package/cjs/compoundV2/index.d.ts +4 -4
  20. package/cjs/compoundV2/index.js +9 -9
  21. package/cjs/compoundV3/index.d.ts +5 -4
  22. package/cjs/compoundV3/index.js +17 -13
  23. package/cjs/compoundV3/merkl.d.ts +1 -1
  24. package/cjs/compoundV3/merkl.js +2 -5
  25. package/cjs/curveUsd/index.d.ts +5 -5
  26. package/cjs/curveUsd/index.js +17 -17
  27. package/cjs/fluid/index.d.ts +23 -22
  28. package/cjs/fluid/index.js +75 -72
  29. package/cjs/fluid/merkl.d.ts +5 -3
  30. package/cjs/fluid/merkl.js +11 -6
  31. package/cjs/liquity/index.d.ts +4 -4
  32. package/cjs/liquity/index.js +23 -23
  33. package/cjs/liquityV2/index.d.ts +10 -10
  34. package/cjs/liquityV2/index.js +59 -59
  35. package/cjs/llamaLend/index.d.ts +5 -5
  36. package/cjs/llamaLend/index.js +13 -13
  37. package/cjs/maker/index.d.ts +6 -6
  38. package/cjs/maker/index.js +16 -16
  39. package/cjs/morphoBlue/index.d.ts +7 -7
  40. package/cjs/morphoBlue/index.js +28 -26
  41. package/cjs/morphoBlue/merkl.d.ts +5 -3
  42. package/cjs/morphoBlue/merkl.js +6 -8
  43. package/cjs/morphoMidnight/index.d.ts +5 -5
  44. package/cjs/morphoMidnight/index.js +18 -18
  45. package/cjs/portfolio/customDeFiData.js +45 -40
  46. package/cjs/services/merkl.d.ts +36 -2
  47. package/cjs/services/merkl.js +142 -19
  48. package/cjs/services/priceService.d.ts +12 -12
  49. package/cjs/services/priceService.js +30 -29
  50. package/cjs/services/viem.d.ts +10 -2
  51. package/cjs/services/viem.js +27 -1
  52. package/cjs/spark/index.d.ts +4 -4
  53. package/cjs/spark/index.js +9 -9
  54. package/cjs/types/merkl.d.ts +18 -0
  55. package/cjs/types/portfolio.d.ts +2 -0
  56. package/cjs/umbrella/index.d.ts +2 -2
  57. package/cjs/umbrella/index.js +5 -4
  58. package/esm/aaveV2/index.d.ts +4 -4
  59. package/esm/aaveV2/index.js +8 -8
  60. package/esm/aaveV3/index.d.ts +4 -3
  61. package/esm/aaveV3/index.js +15 -11
  62. package/esm/aaveV3/merkl.d.ts +5 -1
  63. package/esm/aaveV3/merkl.js +9 -4
  64. package/esm/aaveV4/index.d.ts +4 -3
  65. package/esm/aaveV4/index.js +20 -17
  66. package/esm/aaveV4/merkl.d.ts +10 -2
  67. package/esm/aaveV4/merkl.js +16 -7
  68. package/esm/claiming/aaveV3.d.ts +2 -2
  69. package/esm/claiming/aaveV3.js +5 -4
  70. package/esm/claiming/king.d.ts +2 -2
  71. package/esm/claiming/king.js +3 -2
  72. package/esm/claiming/spark.d.ts +3 -3
  73. package/esm/claiming/spark.js +8 -7
  74. package/esm/claiming/uniswap.d.ts +2 -2
  75. package/esm/claiming/uniswap.js +3 -2
  76. package/esm/compoundV2/index.d.ts +4 -4
  77. package/esm/compoundV2/index.js +9 -9
  78. package/esm/compoundV3/index.d.ts +5 -4
  79. package/esm/compoundV3/index.js +19 -15
  80. package/esm/compoundV3/merkl.d.ts +1 -1
  81. package/esm/compoundV3/merkl.js +3 -6
  82. package/esm/curveUsd/index.d.ts +5 -5
  83. package/esm/curveUsd/index.js +17 -17
  84. package/esm/fluid/index.d.ts +23 -22
  85. package/esm/fluid/index.js +76 -73
  86. package/esm/fluid/merkl.d.ts +5 -3
  87. package/esm/fluid/merkl.js +12 -7
  88. package/esm/liquity/index.d.ts +4 -4
  89. package/esm/liquity/index.js +23 -23
  90. package/esm/liquityV2/index.d.ts +10 -10
  91. package/esm/liquityV2/index.js +60 -60
  92. package/esm/llamaLend/index.d.ts +5 -5
  93. package/esm/llamaLend/index.js +13 -13
  94. package/esm/maker/index.d.ts +6 -6
  95. package/esm/maker/index.js +16 -16
  96. package/esm/morphoBlue/index.d.ts +7 -7
  97. package/esm/morphoBlue/index.js +29 -27
  98. package/esm/morphoBlue/merkl.d.ts +5 -3
  99. package/esm/morphoBlue/merkl.js +7 -9
  100. package/esm/morphoMidnight/index.d.ts +5 -5
  101. package/esm/morphoMidnight/index.js +18 -18
  102. package/esm/portfolio/customDeFiData.js +47 -42
  103. package/esm/services/merkl.d.ts +36 -2
  104. package/esm/services/merkl.js +136 -18
  105. package/esm/services/priceService.d.ts +12 -12
  106. package/esm/services/priceService.js +31 -30
  107. package/esm/services/viem.d.ts +10 -2
  108. package/esm/services/viem.js +25 -0
  109. package/esm/spark/index.d.ts +4 -4
  110. package/esm/spark/index.js +10 -10
  111. package/esm/types/merkl.d.ts +18 -0
  112. package/esm/types/portfolio.d.ts +2 -0
  113. package/esm/umbrella/index.d.ts +2 -2
  114. package/esm/umbrella/index.js +5 -4
  115. package/package.json +1 -1
  116. package/src/aaveV2/index.ts +7 -7
  117. package/src/aaveV3/index.ts +15 -11
  118. package/src/aaveV3/merkl.ts +9 -4
  119. package/src/aaveV4/index.ts +19 -16
  120. package/src/aaveV4/merkl.ts +16 -7
  121. package/src/claiming/aaveV3.ts +5 -3
  122. package/src/claiming/king.ts +6 -3
  123. package/src/claiming/spark.ts +9 -6
  124. package/src/claiming/uniswap.ts +4 -3
  125. package/src/compoundV2/index.ts +10 -9
  126. package/src/compoundV3/index.ts +20 -14
  127. package/src/compoundV3/merkl.ts +5 -4
  128. package/src/curveUsd/index.ts +19 -17
  129. package/src/fluid/index.ts +88 -70
  130. package/src/fluid/merkl.ts +12 -7
  131. package/src/liquity/index.ts +23 -23
  132. package/src/liquityV2/index.ts +64 -56
  133. package/src/llamaLend/index.ts +15 -13
  134. package/src/maker/index.ts +19 -16
  135. package/src/morphoBlue/index.ts +23 -20
  136. package/src/morphoBlue/merkl.ts +9 -6
  137. package/src/morphoMidnight/index.ts +13 -13
  138. package/src/portfolio/customDeFiData.ts +48 -41
  139. package/src/services/merkl.ts +152 -19
  140. package/src/services/priceService.ts +33 -30
  141. package/src/services/viem.ts +16 -0
  142. package/src/spark/index.ts +11 -8
  143. package/src/types/merkl.ts +20 -0
  144. package/src/types/portfolio.ts +2 -0
  145. package/src/umbrella/index.ts +6 -5
@@ -1,31 +1,164 @@
1
- import { MerklOpportunity } from '../types';
1
+ import {
2
+ MerklAprRecord, MerklCampaignListItem, MerklOpportunity, OpportunityStatus,
3
+ } from '../types';
2
4
  import { LONGER_TIMEOUT } from './utils';
3
5
 
4
- const MERKL_OPPORTUNITIES_URL = 'https://fe.defisaver.com/api/merkl/opportunities';
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. Merkl returns 20 opportunities by default, so omitting
8
- * `items` and advancing `page` avoids imposing a client-side maximum on the result set.
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
- export const fetchAllMerklOpportunities = async (query: Record<string, string>): Promise<MerklOpportunity[]> => {
11
- const opportunities: MerklOpportunity[] = [];
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 pageOpportunities: MerklOpportunity[];
32
+ let pageResults: T[];
14
33
 
15
34
  do {
16
- const searchParams = new URLSearchParams({ ...query, page: page.toString() });
17
- const res = await fetch(`${MERKL_OPPORTUNITIES_URL}?${searchParams}`, { // eslint-disable-line no-await-in-loop
18
- signal: AbortSignal.timeout(LONGER_TIMEOUT),
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
- const data = await res.json(); // eslint-disable-line no-await-in-loop
23
- if (!Array.isArray(data)) throw new Error(`Invalid Merkl opportunities response on page ${page}`);
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 (pageOpportunities.length > 0);
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
- return opportunities;
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 { EthAddress, NetworkNumber, HexString } from '../types/common';
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
 
@@ -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) };
@@ -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
- ): Promise<SparkMarketsData> => _getSparkMarketsData(getViemProvider(provider, network), network, selectedMarket);
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
- ) => _getSparkAccountData(getViemProvider(provider, network), network, address, extractedState);
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);
@@ -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
 
@@ -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 {
@@ -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(