@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
@@ -18,13 +18,13 @@ import { aaveAnyGetAggregatedPositionData } from '../helpers/aaveHelpers';
18
18
  import { getEthPrice } from '../services/priceService';
19
19
  import { getViemProvider, setViemBlockNumber } from '../services/viem';
20
20
 
21
- export const _getAaveV2MarketsData = async (provider: Client, network: NetworkNumber, selectedMarket: AaveMarketInfo) => {
21
+ export const _getAaveV2MarketsData = async (provider: Client, network: NetworkNumber, selectedMarket: AaveMarketInfo, blockNumber: Blockish = 'latest') => {
22
22
  const _addresses = selectedMarket.assets.map(a => getAssetInfo(ethToWeth(a)).address);
23
23
  const loanInfoContract = AaveLoanInfoV2ContractViem(provider, network);
24
24
  const marketAddress = selectedMarket.providerAddress;
25
25
  const [loanInfo, ethPrice] = await Promise.all([
26
- loanInfoContract.read.getFullTokensInfo([marketAddress, _addresses as EthAddress[]]),
27
- getEthPrice(provider),
26
+ loanInfoContract.read.getFullTokensInfo([marketAddress, _addresses as EthAddress[]], setViemBlockNumber(blockNumber)),
27
+ getEthPrice(provider, blockNumber),
28
28
  ]);
29
29
  const markets: AaveV2AssetData[] = loanInfo
30
30
  .map((market, i) => ({
@@ -87,7 +87,7 @@ export const _getAaveV2MarketsData = async (provider: Client, network: NetworkNu
87
87
  return { assetsData: payload };
88
88
  };
89
89
 
90
- export const getAaveV2MarketsData = async (provider: EthereumProvider, network: NetworkNumber, selectedMarket: AaveMarketInfo) => _getAaveV2MarketsData(getViemProvider(provider, network), network, selectedMarket);
90
+ export const getAaveV2MarketsData = async (provider: EthereumProvider, network: NetworkNumber, selectedMarket: AaveMarketInfo, blockNumber: Blockish = 'latest') => _getAaveV2MarketsData(getViemProvider(provider, network), network, selectedMarket, blockNumber);
91
91
 
92
92
  export const _getAaveV2AccountBalances = async (provider: Client, network: NetworkNumber, block: Blockish, addressMapping: boolean, address: EthAddress): Promise<PositionBalances> => {
93
93
  const balances = await getAaveLikeBalances(provider, network, block, addressMapping, address, AAVE_V2, 'AaveLoanInfoV2');
@@ -100,7 +100,7 @@ export const _getAaveV2AccountBalances = async (provider: Client, network: Netwo
100
100
 
101
101
  export const getAaveV2AccountBalances = async (provider: EthereumProvider, network: NetworkNumber, block: Blockish, addressMapping: boolean, address: EthAddress): Promise<PositionBalances> => _getAaveV2AccountBalances(getViemProvider(provider, network), network, block, addressMapping, address);
102
102
 
103
- export const _getAaveV2AccountData = async (provider: Client, network: NetworkNumber, address: EthAddress, assetsData: AaveV2AssetsData, market: AaveMarketInfo): Promise<AaveV2PositionData> => {
103
+ export const _getAaveV2AccountData = async (provider: Client, network: NetworkNumber, address: EthAddress, assetsData: AaveV2AssetsData, market: AaveMarketInfo, blockNumber: Blockish = 'latest'): Promise<AaveV2PositionData> => {
104
104
  if (!address) throw new Error('Address is required');
105
105
 
106
106
  let payload: AaveV2PositionData = {
@@ -111,7 +111,7 @@ export const _getAaveV2AccountData = async (provider: Client, network: NetworkNu
111
111
  const loanInfoContract = AaveLoanInfoV2ContractViem(provider, network);
112
112
  const marketAddress = market.providerAddress;
113
113
  const _addresses = market.assets.map(a => getAssetInfo(ethToWeth(a)).address);
114
- const loanInfo = await loanInfoContract.read.getTokenBalances([marketAddress, address, _addresses as EthAddress[]]);
114
+ const loanInfo = await loanInfoContract.read.getTokenBalances([marketAddress, address, _addresses as EthAddress[]], setViemBlockNumber(blockNumber));
115
115
  const usedAssets = {} as AaveV2UsedAssets;
116
116
  loanInfo.forEach((_tokenInfo, i) => {
117
117
  const asset = market.assets[i];
@@ -193,7 +193,7 @@ export const _getAaveV2AccountData = async (provider: Client, network: NetworkNu
193
193
  return payload;
194
194
  };
195
195
 
196
- export const getAaveV2AccountData = async (provider: EthereumProvider, network: NetworkNumber, address: EthAddress, assetsData: AaveV2AssetsData, market: AaveMarketInfo): Promise<AaveV2PositionData> => _getAaveV2AccountData(getViemProvider(provider, network), network, address, assetsData, market);
196
+ export const getAaveV2AccountData = async (provider: EthereumProvider, network: NetworkNumber, address: EthAddress, assetsData: AaveV2AssetsData, market: AaveMarketInfo, blockNumber: Blockish = 'latest'): Promise<AaveV2PositionData> => _getAaveV2AccountData(getViemProvider(provider, network), network, address, assetsData, market, blockNumber);
197
197
 
198
198
  export const getAaveV2FullPositionData = async (provider: EthereumProvider, network: NetworkNumber, address: EthAddress, market: AaveMarketInfo): Promise<AaveV2PositionData> => {
199
199
  const marketData = await getAaveV2MarketsData(provider, network, market);
@@ -42,7 +42,7 @@ import {
42
42
  PositionBalances,
43
43
  HexString,
44
44
  } from '../types/common';
45
- import { getViemProvider, setViemBlockNumber } from '../services/viem';
45
+ import { getBlockTimestamp, getViemProvider, setViemBlockNumber } from '../services/viem';
46
46
  import { getMeritCampaigns } from './merit';
47
47
  import { getAaveUnderlyingSymbol, getMerkleCampaigns } from './merkl';
48
48
  import { getSghoData } from './sgho';
@@ -81,7 +81,8 @@ export const aaveV3EmodeCategoriesMapping = (extractedState: any, usedAssets: Aa
81
81
  return categoriesMapping;
82
82
  };
83
83
 
84
- export async function _getAaveV3MarketData(provider: Client, network: NetworkNumber, market: AaveMarketInfo, blockNumber: 'latest' | number = 'latest'): Promise<AaveV3MarketData> {
84
+ /** `blockTimestamp` (unix seconds) is the historical block's timestamp when already known; otherwise it is read from the block. */
85
+ export async function _getAaveV3MarketData(provider: Client, network: NetworkNumber, market: AaveMarketInfo, blockNumber: 'latest' | number = 'latest', blockTimestamp?: number): Promise<AaveV3MarketData> {
85
86
  const _addresses = market.assets.map(a => getAssetInfo(getWrappedNativeAssetFromUnwrapped(a), network).address);
86
87
 
87
88
  const loanInfoContract = AaveV3ViewContractViem(provider, network);
@@ -100,12 +101,15 @@ export async function _getAaveV3MarketData(provider: Client, network: NetworkNum
100
101
  [marketAddress, addresses],
101
102
  setViemBlockNumber(blockNumber),
102
103
  ))).then((loanInfoChunks) => loanInfoChunks.flat());
104
+ // Merkl rewards are keyed by time, so a historical block resolves to its timestamp first (one
105
+ // RPC call unless given, none for 'latest') and the rewards in force at that moment are attached.
106
+ const merkleRewardsPromise = getBlockTimestamp(provider, blockNumber, blockTimestamp).then((asOfTimestamp) => getMerkleCampaigns(network, asOfTimestamp));
103
107
  // eslint-disable-next-line prefer-const
104
108
  let [loanInfo, eModesInfo, rewardInfo, merkleRewardsMap, meritRewardsMap] = await Promise.all([
105
109
  loanInfoPromise,
106
110
  loanInfoContract.read.getAllEmodes([marketAddress], setViemBlockNumber(blockNumber)),
107
111
  networksWithIncentives.includes(network) ? aaveIncentivesContract.read.getReservesIncentivesData([marketAddress], setViemBlockNumber(blockNumber)) : null,
108
- getMerkleCampaigns(network),
112
+ merkleRewardsPromise,
109
113
  getMeritCampaigns(network, market.value),
110
114
  ]);
111
115
  // same break logic as view contract
@@ -356,8 +360,8 @@ export async function _getAaveV3MarketData(provider: Client, network: NetworkNum
356
360
  return { assetsData: payload, eModeCategoriesData };
357
361
  }
358
362
 
359
- export async function getAaveV3MarketData(provider: EthereumProvider, network: NetworkNumber, market: AaveMarketInfo, blockNumber: 'latest' | number = 'latest'): Promise<AaveV3MarketData> {
360
- return _getAaveV3MarketData(getViemProvider(provider, network), network, market, blockNumber);
363
+ export async function getAaveV3MarketData(provider: EthereumProvider, network: NetworkNumber, market: AaveMarketInfo, blockNumber: 'latest' | number = 'latest', blockTimestamp?: number): Promise<AaveV3MarketData> {
364
+ return _getAaveV3MarketData(getViemProvider(provider, network), network, market, blockNumber, blockTimestamp);
361
365
  }
362
366
 
363
367
  export const EMPTY_AAVE_DATA = {
@@ -852,7 +856,7 @@ export const fetchYearlyMeritApyForStakingGho = async () => {
852
856
  }
853
857
  };
854
858
 
855
- export const getStakeAaveData = async (provider: Client, network: NetworkNumber, address: EthAddress) => {
859
+ export const getStakeAaveData = async (provider: Client, network: NetworkNumber, address: EthAddress, blockNumber: Blockish = 'latest') => {
856
860
  const stkGhoAddress = getAssetInfo('stkGHO').address as HexString;
857
861
  const stkAaveAddress = getAssetInfo('stkAAVE').address as HexString;
858
862
 
@@ -862,11 +866,11 @@ export const getStakeAaveData = async (provider: Client, network: NetworkNumber,
862
866
 
863
867
 
864
868
  const [aaveRewardsBalance, emissionsPerSecond, stkAAVEBalance, stkAAVETotalSupply, stkGHOBalance, ghoMeritApy, sgho] = await Promise.all([
865
- AaveIncentivesController.read.getRewardsBalance([REWARDABLE_ASSETS, address]),
866
- stkAAVE.read.assets([stkAaveAddress]),
867
- stkAAVE.read.balanceOf([address]),
868
- stkAAVE.read.totalSupply(),
869
- stkGHO.read.balanceOf([address]),
869
+ AaveIncentivesController.read.getRewardsBalance([REWARDABLE_ASSETS, address], setViemBlockNumber(blockNumber)),
870
+ stkAAVE.read.assets([stkAaveAddress], setViemBlockNumber(blockNumber)),
871
+ stkAAVE.read.balanceOf([address], setViemBlockNumber(blockNumber)),
872
+ stkAAVE.read.totalSupply(setViemBlockNumber(blockNumber)),
873
+ stkGHO.read.balanceOf([address], setViemBlockNumber(blockNumber)),
870
874
  fetchYearlyMeritApyForStakingGho(),
871
875
  getSghoData(network, address),
872
876
  ]);
@@ -1,5 +1,5 @@
1
1
  import { aprToApy } from '../moneymarket';
2
- import { fetchAllMerklOpportunities } from '../services/merkl';
2
+ import { fetchMerklOpportunities } from '../services/merkl';
3
3
  import { wethToEth } from '../services/utils';
4
4
  import {
5
5
  MerkleRewardMap, MerklOpportunity, OpportunityAction, OpportunityStatus,
@@ -30,12 +30,17 @@ export const formatAaveAsset = (_symbol: string) => {
30
30
  return _symbol;
31
31
  };
32
32
 
33
- export const getMerkleCampaigns = async (chainId: NetworkNumber): Promise<MerkleRewardMap> => {
33
+ /**
34
+ * Live Merkl reward campaigns for Aave V3 on the chain, keyed by aToken. With `asOfTimestamp` (unix
35
+ * seconds) the campaigns and APRs are the ones Merkl had at that moment instead.
36
+ */
37
+ export const getMerkleCampaigns = async (chainId: NetworkNumber, asOfTimestamp?: number): Promise<MerkleRewardMap> => {
34
38
  try {
35
- const opportunities = await fetchAllMerklOpportunities({
39
+ const opportunities = await fetchMerklOpportunities({
36
40
  mainProtocolId: 'aave',
41
+ chainId: chainId.toString(),
37
42
  status: OpportunityStatus.LIVE,
38
- });
43
+ }, asOfTimestamp);
39
44
  const relevantOpportunities = opportunities
40
45
  .filter((o: MerklOpportunity) => o.chainId === chainId)
41
46
  .filter((o: MerklOpportunity) => o.status === OpportunityStatus.LIVE);
@@ -1,7 +1,7 @@
1
1
  import { Client } from 'viem';
2
2
  import Dec from 'decimal.js';
3
3
  import { assetAmountInEth, assetAmountInWei, getAssetInfoByAddress } from '@defisaver/tokens';
4
- import { getViemProvider, setViemBlockNumber } from '../services/viem';
4
+ import { getBlockTimestamp, getViemProvider, setViemBlockNumber } from '../services/viem';
5
5
  import {
6
6
  AaveV4AccountData,
7
7
  AaveV4HubAssetOnChainData,
@@ -32,8 +32,8 @@ import { attachAaveV4MerklIncentives, getAaveV4MerkleCampaigns } from './merkl';
32
32
  export * as lend from './lend';
33
33
  export { getAaveV4MerkleCampaigns } from './merkl';
34
34
 
35
- const fetchHubData = async (viewContract: ReturnType<typeof AaveV4ViewContractViem>, hubAddress: EthAddress): Promise<AaveV4HubOnChainData> => {
36
- const hubData = await viewContract.read.getHubAllAssetsData([hubAddress]);
35
+ const fetchHubData = async (viewContract: ReturnType<typeof AaveV4ViewContractViem>, hubAddress: EthAddress, blockNumber: Blockish = 'latest'): Promise<AaveV4HubOnChainData> => {
36
+ const hubData = await viewContract.read.getHubAllAssetsData([hubAddress], setViemBlockNumber(blockNumber));
37
37
  return {
38
38
  assets: hubData.reduce((acc: Record<number, AaveV4HubAssetOnChainData>, assetOnChainData) => {
39
39
  acc[assetOnChainData.assetId] = {
@@ -162,19 +162,22 @@ const formatReserveAsset = async (reserveAsset: AaveV4ReserveAssetOnChain, hubAs
162
162
  });
163
163
  };
164
164
 
165
- export async function _getAaveV4SpokeData(provider: Client, network: NetworkNumber, market: AaveV4SpokeInfo, blockNumber: 'latest' | number = 'latest'): Promise<AaveV4SpokeData> {
165
+ /** `blockTimestamp` (unix seconds) is the historical block's timestamp when already known; otherwise it is read from the block. */
166
+ export async function _getAaveV4SpokeData(provider: Client, network: NetworkNumber, market: AaveV4SpokeInfo, blockNumber: 'latest' | number = 'latest', blockTimestamp?: number): Promise<AaveV4SpokeData> {
166
167
  const viewContract = AaveV4ViewContractViem(provider, network, blockNumber);
167
168
 
168
169
  const hubsData: Record<string, AaveV4HubOnChainData> = {};
169
170
  const loadHubData = async (hubAddress: EthAddress) => {
170
- hubsData[hubAddress.toLowerCase()] = await fetchHubData(viewContract, hubAddress);
171
+ hubsData[hubAddress.toLowerCase()] = await fetchHubData(viewContract, hubAddress, blockNumber);
171
172
  };
172
173
 
173
174
  // market.hubs is only a prefetch hint (lets known hubs load in parallel with the spoke data), so
174
175
  // failures are tolerated here — any hub the reserves actually reference is (re)fetched below.
175
176
  const [spokeData, merklCampaigns] = await Promise.all([
176
- viewContract.read.getSpokeDataFull([market.address]),
177
- getAaveV4MerkleCampaigns(network),
177
+ viewContract.read.getSpokeDataFull([market.address], setViemBlockNumber(blockNumber)),
178
+ // Merkl rewards are keyed by time: a historical block resolves to its timestamp first (one RPC
179
+ // call unless given, none for 'latest') so the rewards in force at that moment are attached.
180
+ getBlockTimestamp(provider, blockNumber, blockTimestamp).then((asOfTimestamp) => getAaveV4MerkleCampaigns(network, asOfTimestamp)),
178
181
  ...market.hubs.map((hubAddress) => loadHubData(hubAddress).catch(() => {})),
179
182
  ]);
180
183
 
@@ -205,14 +208,14 @@ export async function _getAaveV4SpokeData(provider: Client, network: NetworkNumb
205
208
  };
206
209
  }
207
210
 
208
- export async function getAaveV4SpokeData(provider: EthereumProvider, network: NetworkNumber, spoke: AaveV4SpokeInfo, blockNumber: 'latest' | number = 'latest'): Promise<AaveV4SpokeData> {
209
- return _getAaveV4SpokeData(getViemProvider(provider, network), network, spoke, blockNumber);
211
+ export async function getAaveV4SpokeData(provider: EthereumProvider, network: NetworkNumber, spoke: AaveV4SpokeInfo, blockNumber: 'latest' | number = 'latest', blockTimestamp?: number): Promise<AaveV4SpokeData> {
212
+ return _getAaveV4SpokeData(getViemProvider(provider, network), network, spoke, blockNumber, blockTimestamp);
210
213
  }
211
214
 
212
215
  export async function _getAaveV4AccountData(provider: Client, network: NetworkNumber, spokeData: AaveV4SpokeData, address: EthAddress, blockNumber: 'latest' | number = 'latest'): Promise<AaveV4AccountData> {
213
216
  const viewContract = AaveV4ViewContractViem(provider, network, blockNumber);
214
217
 
215
- const loanData = await viewContract.read.getLoanData([spokeData.address, address]);
218
+ const loanData = await viewContract.read.getLoanData([spokeData.address, address], setViemBlockNumber(blockNumber));
216
219
 
217
220
  const healthFactorFromContract = new Dec(loanData.healthFactor.toString());
218
221
  const healthFactor = isMaxUint(healthFactorFromContract.toString()) ? 'Infinity' : healthFactorFromContract.div(1e18).toString();
@@ -297,7 +300,7 @@ export const _getAaveV4AccountHasAnyBalance = async (
297
300
  const blockNumber = block === 'latest' ? 'latest' : Number(block);
298
301
  const viewContract = AaveV4ViewContractViem(provider, network, blockNumber);
299
302
 
300
- const loanData = await viewContract.read.getLoanData([spokeAddress, address]);
303
+ const loanData = await viewContract.read.getLoanData([spokeAddress, address], setViemBlockNumber(blockNumber));
301
304
  const reserves = loanData?.reserves || [];
302
305
 
303
306
  return reserves.some((reserveAsset) => {
@@ -379,14 +382,14 @@ export const getAaveV4AccountBalances = async (
379
382
  spokeData,
380
383
  );
381
384
 
382
- const _getAaveV4UnderlyingFromReserveId = async (provider: Client, network: NetworkNumber, spoke: EthAddress, reserveId: number): Promise<any> => {
383
- const viewContract = AaveV4ViewContractViem(provider, network);
385
+ const _getAaveV4UnderlyingFromReserveId = async (provider: Client, network: NetworkNumber, spoke: EthAddress, reserveId: number, blockNumber: Blockish = 'latest'): Promise<any> => {
386
+ const viewContract = AaveV4ViewContractViem(provider, network, blockNumber);
384
387
 
385
- const reserveData = await viewContract.read.getReserveData([spoke, BigInt(reserveId)]);
388
+ const reserveData = await viewContract.read.getReserveData([spoke, BigInt(reserveId)], setViemBlockNumber(blockNumber));
386
389
 
387
390
  return reserveData.underlying;
388
391
  };
389
392
 
390
- export async function getAaveV4UnderlyingFromReserveId(provider: EthereumProvider, network: NetworkNumber, spoke: EthAddress, reserveId: number): Promise<any> {
391
- return _getAaveV4UnderlyingFromReserveId(getViemProvider(provider, network), network, spoke, reserveId);
393
+ export async function getAaveV4UnderlyingFromReserveId(provider: EthereumProvider, network: NetworkNumber, spoke: EthAddress, reserveId: number, blockNumber: Blockish = 'latest'): Promise<any> {
394
+ return _getAaveV4UnderlyingFromReserveId(getViemProvider(provider, network), network, spoke, reserveId, blockNumber);
392
395
  }
@@ -1,6 +1,6 @@
1
1
  import Dec from 'decimal.js';
2
2
  import { aprToApy } from '../moneymarket';
3
- import { fetchAllMerklOpportunities } from '../services/merkl';
3
+ import { fetchMerklOpportunities } from '../services/merkl';
4
4
  import {
5
5
  AaveV4MerklRewardMap,
6
6
  AaveV4MerklIncentive,
@@ -46,7 +46,11 @@ const buildIncentive = (opportunity: MerklOpportunity): IncentiveData => {
46
46
  };
47
47
  };
48
48
 
49
- export const buildAaveV4MerklRewardMap = (opportunities: MerklOpportunity[], chainId: NetworkNumber): AaveV4MerklRewardMap => {
49
+ /**
50
+ * `asOfTimestamp` (unix seconds, defaults to now) decides which of an opportunity's campaigns count
51
+ * as running, so historical opportunity lists are judged at their own moment rather than today's.
52
+ */
53
+ export const buildAaveV4MerklRewardMap = (opportunities: MerklOpportunity[], chainId: NetworkNumber, asOfTimestamp: number = Date.now() / 1000): AaveV4MerklRewardMap => {
50
54
  const result: AaveV4MerklRewardMap = { hub: {}, spoke: {} };
51
55
 
52
56
  const endByCampaignId: Record<string, number> = {};
@@ -64,7 +68,7 @@ export const buildAaveV4MerklRewardMap = (opportunities: MerklOpportunity[], cha
64
68
  .filter((o) => typeof o.type === 'string' && o.type.startsWith('AAVE_V4_'))
65
69
  .forEach((o) => {
66
70
  const side = o.action === OpportunityAction.BORROW ? IncentiveSide.Borrow : IncentiveSide.Supply;
67
- const now = Date.now() / 1000;
71
+ const now = asOfTimestamp;
68
72
  const methods = o.campaigns
69
73
  ?.filter((c) => (c.startTimestamp === undefined || c.startTimestamp <= now)
70
74
  && (c.endTimestamp === undefined || c.endTimestamp > now))
@@ -136,15 +140,20 @@ export const buildAaveV4MerklRewardMap = (opportunities: MerklOpportunity[], cha
136
140
  return result;
137
141
  };
138
142
 
139
- export const getAaveV4MerkleCampaigns = async (chainId: NetworkNumber): Promise<AaveV4MerklRewardMap> => {
143
+ /**
144
+ * Live Aave V4 Merkl reward campaigns for the chain, split by hub/spoke scope. With `asOfTimestamp`
145
+ * (unix seconds) the campaigns and APRs are the ones Merkl had at that moment instead.
146
+ */
147
+ export const getAaveV4MerkleCampaigns = async (chainId: NetworkNumber, asOfTimestamp?: number): Promise<AaveV4MerklRewardMap> => {
140
148
  try {
141
- const opportunities = await fetchAllMerklOpportunities({
149
+ const opportunities = await fetchMerklOpportunities({
142
150
  mainProtocolId: 'aave',
151
+ chainId: chainId.toString(),
143
152
  type: 'AAVE_V4_HUB_SUPPLY,AAVE_V4_HUB_BORROW,AAVE_V4_SPOKE_SUPPLY,AAVE_V4_SPOKE_BORROW,AAVE_V4_HUB_NET_LENDING,AAVE_V4_HUB_NET_BORROWING,AAVE_V4_SPOKE_NET_LENDING,AAVE_V4_SPOKE_NET_BORROWING',
144
153
  status: OpportunityStatus.LIVE,
145
154
  campaigns: 'true',
146
- });
147
- return buildAaveV4MerklRewardMap(opportunities, chainId);
155
+ }, asOfTimestamp);
156
+ return buildAaveV4MerklRewardMap(opportunities, chainId, asOfTimestamp);
148
157
  } catch (e) {
149
158
  console.error('Failed to fetch Aave V4 Merkle campaigns', e);
150
159
  return { hub: {}, spoke: {} };
@@ -1,12 +1,13 @@
1
1
  import { Client } from 'viem';
2
2
  import Dec from 'decimal.js';
3
- import { EthAddress, NetworkNumber } from '../types';
3
+ import { Blockish, EthAddress, NetworkNumber } from '../types';
4
4
  import { ClaimableToken, ClaimType, MerklRewardsClaimableToken } from '../types/claiming';
5
5
  import {
6
6
  AaveIncentiveDataProviderV3ContractViem,
7
7
  AaveRewardsControllerViem,
8
8
  } from '../contracts';
9
9
  import { compareAddresses, getEthAmountForDecimals, LONGER_TIMEOUT } from '../services/utils';
10
+ import { setViemBlockNumber } from '../services/viem';
10
11
  import { getAaveUnderlyingSymbol } from '../helpers/aaveHelpers';
11
12
 
12
13
  type AaveReward = {
@@ -37,9 +38,10 @@ export async function getUnclaimedRewardsForAllMarkets(
37
38
  network: NetworkNumber,
38
39
  walletAddress: EthAddress,
39
40
  marketAddress: EthAddress,
41
+ blockNumber: Blockish = 'latest',
40
42
  ): Promise<ClaimableToken[]> {
41
43
  const contract = AaveIncentiveDataProviderV3ContractViem(provider, network);
42
- const tokensData = await contract.read.getUserReservesIncentivesData([marketAddress, walletAddress]);
44
+ const tokensData = await contract.read.getUserReservesIncentivesData([marketAddress, walletAddress], setViemBlockNumber(blockNumber));
43
45
  const allTokensDataArrays = tokensData.reduce((acc: any[], val) => {
44
46
  acc.push(val.aTokenIncentivesUserData.userRewardsInformation);
45
47
  acc.push(val.vTokenIncentivesUserData.userRewardsInformation);
@@ -54,7 +56,7 @@ export async function getUnclaimedRewardsForAllMarkets(
54
56
  // reward can be any token like wstETH, but also aToken like aWETH
55
57
  const aaveRewardsController = AaveRewardsControllerViem(provider, network);
56
58
  const rewardsPerAAsset = await Promise.all(allATokens.map(
57
- (token: string) => aaveRewardsController.read.getAllUserRewards([[token as EthAddress], walletAddress])));
59
+ (token: string) => aaveRewardsController.read.getAllUserRewards([[token as EthAddress], walletAddress], setViemBlockNumber(blockNumber))));
58
60
 
59
61
  // match amounts to token symbol, parse decimal amounts
60
62
  const rewardsPerAAssetWithInfo = rewardsPerAAsset.map((rewardForAAsset, aaIndex) => {
@@ -2,7 +2,10 @@ import Dec from 'decimal.js';
2
2
  import { assetAmountInEth } from '@defisaver/tokens';
3
3
  import { Client } from 'viem';
4
4
  import { UUPSViem } from '../contracts';
5
- import { EthAddress, HexString, NetworkNumber } from '../types/common';
5
+ import {
6
+ Blockish, EthAddress, HexString, NetworkNumber,
7
+ } from '../types/common';
8
+ import { setViemBlockNumber } from '../services/viem';
6
9
  import { ClaimType } from '../types/claiming';
7
10
 
8
11
  export const fetchKingRewards = async (walletAddress: EthAddress) => {
@@ -19,14 +22,14 @@ export const fetchKingRewards = async (walletAddress: EthAddress) => {
19
22
  }
20
23
  };
21
24
 
22
- export const getKingRewards = async (provider: Client, network: NetworkNumber, walletAddresses: EthAddress[]) => {
25
+ export const getKingRewards = async (provider: Client, network: NetworkNumber, walletAddresses: EthAddress[], blockNumber: Blockish = 'latest') => {
23
26
  // Fetch all API data in parallel (these are external API calls, can't be batched with multicall)
24
27
  const apiDataPromises = walletAddresses.map(address => fetchKingRewards(address));
25
28
  const apiDataArray = await Promise.all(apiDataPromises);
26
29
 
27
30
  // Batch all contract calls using multicall
28
31
  const contract = UUPSViem(provider, network);
29
- const cumulativePromises = walletAddresses.map(address => contract.read.cumulativeClaimed([address]),
32
+ const cumulativePromises = walletAddresses.map(address => contract.read.cumulativeClaimed([address], setViemBlockNumber(blockNumber)),
30
33
  );
31
34
  const cumulativeResults = await Promise.all(cumulativePromises);
32
35
 
@@ -6,9 +6,10 @@ import {
6
6
  SparkIncentiveDataProviderContractViem,
7
7
  SparkRewardsControllerViem,
8
8
  } from '../contracts';
9
- import { EthAddress, NetworkNumber } from '../types/common';
9
+ import { Blockish, EthAddress, NetworkNumber } from '../types/common';
10
10
  import { ClaimType, SparkAirdropType, SparkRewardsClaimableToken } from '../types/claiming';
11
11
  import { compareAddresses, getEthAmountForDecimals } from '../services/utils';
12
+ import { setViemBlockNumber } from '../services/viem';
12
13
 
13
14
  const IGNITION_REWARDS = '0xCBA0C0a2a0B6Bb11233ec4EA85C5bFfea33e724d';
14
15
  const PFL3_REWARDS = '0x7ac96180C4d6b2A328D3a19ac059D0E7Fc3C6d41';
@@ -31,7 +32,7 @@ type CumulativeClaimedSpkAirdropFuncProp = {
31
32
  user: string, tokenAddress: string, epoch: number, type: SparkAirdropType,
32
33
  };
33
34
 
34
- const getCumulativeClaimedSpkAirdrop = async (provider: Client, network: NetworkNumber, data: CumulativeClaimedSpkAirdropFuncProp[]) => {
35
+ const getCumulativeClaimedSpkAirdrop = async (provider: Client, network: NetworkNumber, data: CumulativeClaimedSpkAirdropFuncProp[], blockNumber: Blockish = 'latest') => {
35
36
  const contractIgnition = createViemContractFromConfigFunc('SparkAirdrop', IGNITION_REWARDS)(provider, network);
36
37
  const contractPFL = createViemContractFromConfigFunc('SparkAirdrop', PFL3_REWARDS)(provider, network);
37
38
 
@@ -40,7 +41,7 @@ const getCumulativeClaimedSpkAirdrop = async (provider: Client, network: Network
40
41
  user, tokenAddress, epoch, type,
41
42
  } = airdrop;
42
43
  const contract = type === SparkAirdropType.SPARK_IGNITION ? contractIgnition : contractPFL;
43
- return contract.read.cumulativeClaimed([user as EthAddress, tokenAddress as EthAddress, BigInt(epoch)]);
44
+ return contract.read.cumulativeClaimed([user as EthAddress, tokenAddress as EthAddress, BigInt(epoch)], setViemBlockNumber(blockNumber));
44
45
  }));
45
46
 
46
47
  return cumulativeAmounts.map((amount: bigint, i: number) => assetAmountInEth(amount.toString(), getAssetInfoByAddress(data[i].tokenAddress).symbol).toString());
@@ -50,6 +51,7 @@ export const fetchSparkAirdropRewards = async (
50
51
  provider: Client,
51
52
  network: NetworkNumber,
52
53
  walletAddresses: EthAddress[],
54
+ blockNumber: Blockish = 'latest',
53
55
  ) => {
54
56
  // Fetch all API data in parallel (these are external API calls, can't be batched with multicall)
55
57
  const apiDataPromises = walletAddresses.map(address => fetchSpkAirdropRewards(address));
@@ -89,7 +91,7 @@ export const fetchSparkAirdropRewards = async (
89
91
  }
90
92
 
91
93
  // Batch all contract calls
92
- const cumulativeClaimedAmounts = await getCumulativeClaimedSpkAirdrop(provider, network, contractCallsData);
94
+ const cumulativeClaimedAmounts = await getCumulativeClaimedSpkAirdrop(provider, network, contractCallsData, blockNumber);
93
95
 
94
96
  // Process results
95
97
  const results: Record<string, any[]> = {};
@@ -142,9 +144,10 @@ export const fetchSparkRewards = async (
142
144
  network: NetworkNumber,
143
145
  walletAddress: EthAddress,
144
146
  marketAddress: EthAddress,
147
+ blockNumber: Blockish = 'latest',
145
148
  ): Promise<SparkRewardsClaimableToken[]> => {
146
149
  const contract = SparkIncentiveDataProviderContractViem(provider, network);
147
- const tokensData = await contract.read.getUserReservesIncentivesData([marketAddress, walletAddress]);
150
+ const tokensData = await contract.read.getUserReservesIncentivesData([marketAddress, walletAddress], setViemBlockNumber(blockNumber));
148
151
  const allTokensDataArrays = tokensData.reduce((acc: any[], val) => {
149
152
  acc.push(val.aTokenIncentivesUserData.userRewardsInformation);
150
153
  acc.push(val.vTokenIncentivesUserData.userRewardsInformation);
@@ -165,7 +168,7 @@ export const fetchSparkRewards = async (
165
168
  };
166
169
  const rewardsPerAAsset = await Promise.all(allATokens.map((token: EthAddress) => {
167
170
  const sparkRewardsContract = SparkRewardsControllerViem(provider, network);
168
- return sparkRewardsContract.read.getAllUserRewards([[token], walletAddress]);
171
+ return sparkRewardsContract.read.getAllUserRewards([[token], walletAddress], setViemBlockNumber(blockNumber));
169
172
  }));
170
173
 
171
174
  // match amounts to token symbol, parse decimal amounts
@@ -1,7 +1,8 @@
1
1
  import Dec from 'decimal.js';
2
2
  import { Client } from 'viem';
3
3
  import { assetAmountInEth } from '@defisaver/tokens';
4
- import { EthAddress, NetworkNumber } from '../types';
4
+ import { Blockish, EthAddress, NetworkNumber } from '../types';
5
+ import { setViemBlockNumber } from '../services/viem';
5
6
  import { UniswapTokenDistributorViem } from '../contracts';
6
7
  import { ClaimType, UniswapAirdropClaimableToken } from '../types/claiming';
7
8
 
@@ -25,14 +26,14 @@ export const fetchUniswapRewardsData = async (walletAddress: EthAddress) => {
25
26
  }
26
27
  };
27
28
 
28
- export const getUniswapRewards = async (provider: Client, network: NetworkNumber, walletAddresses: EthAddress[]): Promise<Record<string, UniswapAirdropClaimableToken[]>> => {
29
+ export const getUniswapRewards = async (provider: Client, network: NetworkNumber, walletAddresses: EthAddress[], blockNumber: Blockish = 'latest'): Promise<Record<string, UniswapAirdropClaimableToken[]>> => {
29
30
  // Fetch all API data in parallel (these are external API calls, can't be batched with multicall)
30
31
  const apiDataPromises = walletAddresses.map(address => fetchUniswapRewardsData(address));
31
32
  const apiDataArray = await Promise.all(apiDataPromises);
32
33
 
33
34
  // Batch all contract calls using multicall
34
35
  const contract = UniswapTokenDistributorViem(provider, network);
35
- const cumulativePromises = apiDataArray.map(data => (data.index ? contract.read.isClaimed([data.index]) : Promise.resolve(false)),
36
+ const cumulativePromises = apiDataArray.map(data => (data.index ? contract.read.isClaimed([data.index], setViemBlockNumber(blockNumber)) : Promise.resolve(false)),
36
37
  );
37
38
  const cumulativeResults = await Promise.all(cumulativePromises);
38
39
 
@@ -17,11 +17,11 @@ import { getViemProvider, setViemBlockNumber } from '../services/viem';
17
17
 
18
18
  const compAddress = '0xc00e94cb662c3520282e6f5717214004a7f26888';
19
19
 
20
- export const _getCompoundV2MarketsData = async (provider: Client, network: NetworkNumber): Promise<CompoundV2MarketsData> => {
20
+ export const _getCompoundV2MarketsData = async (provider: Client, network: NetworkNumber, blockNumber: Blockish = 'latest'): Promise<CompoundV2MarketsData> => {
21
21
  const cAddresses = compoundV2CollateralAssets.map(a => a.address);
22
22
 
23
23
  const loanInfoContract = CompoundLoanInfoContractViem(provider, network);
24
- const loanInfo = await loanInfoContract.read.getFullTokensInfo([cAddresses as EthAddress[]]);
24
+ const loanInfo = await loanInfoContract.read.getFullTokensInfo([cAddresses as EthAddress[]], setViemBlockNumber(blockNumber));
25
25
 
26
26
  const compPrice = loanInfo.find(m => compareAddresses(m.underlyingTokenAddress, compAddress))!.price.toString();
27
27
 
@@ -86,7 +86,7 @@ export const _getCompoundV2MarketsData = async (provider: Client, network: Netwo
86
86
  return { assetsData: payload };
87
87
  };
88
88
 
89
- export const getCompoundV2MarketsData = async (provider: EthereumProvider, network: NetworkNumber): Promise<CompoundV2MarketsData> => _getCompoundV2MarketsData(getViemProvider(provider, network), network);
89
+ export const getCompoundV2MarketsData = async (provider: EthereumProvider, network: NetworkNumber, blockNumber: Blockish = 'latest'): Promise<CompoundV2MarketsData> => _getCompoundV2MarketsData(getViemProvider(provider, network), network, blockNumber);
90
90
 
91
91
  export const EMPTY_COMPOUND_DATA = {
92
92
  usedAssets: {},
@@ -103,10 +103,10 @@ export const EMPTY_COMPOUND_DATA = {
103
103
  exposure: 'N/A',
104
104
  };
105
105
 
106
- const getCollateralAssetsAddresses = async (provider: Client, network: NetworkNumber, account: EthAddress) => {
106
+ const getCollateralAssetsAddresses = async (provider: Client, network: NetworkNumber, account: EthAddress, blockNumber: Blockish = 'latest') => {
107
107
  const contract = ComptrollerContractViem(provider, network);
108
108
 
109
- return contract.read.getAssetsIn([account]);
109
+ return contract.read.getAssetsIn([account], setViemBlockNumber(blockNumber));
110
110
  };
111
111
 
112
112
  const getAllMarketAddresses = async (provider: Client, network: NetworkNumber, block: Blockish) => {
@@ -172,15 +172,15 @@ export const getCompoundV2AccountBalances = async (
172
172
  address: EthAddress,
173
173
  ): Promise<PositionBalances> => _getCompoundV2AccountBalances(getViemProvider(provider, network), network, block, addressMapping, address);
174
174
 
175
- export const _getCompoundV2AccountData = async (provider: Client, network: NetworkNumber, address: EthAddress, assetsData: CompoundV2AssetsData): Promise<CompoundV2PositionData> => {
175
+ export const _getCompoundV2AccountData = async (provider: Client, network: NetworkNumber, address: EthAddress, assetsData: CompoundV2AssetsData, blockNumber: Blockish = 'latest'): Promise<CompoundV2PositionData> => {
176
176
  if (!address) throw new Error('No address provided');
177
177
 
178
178
  let payload = { ...EMPTY_COMPOUND_DATA, lastUpdated: Date.now() };
179
179
 
180
180
  const loanInfoContract = CompoundLoanInfoContractViem(provider, network);
181
181
  const [loanInfo, collateralAssetsAddresses] = await Promise.all([
182
- loanInfoContract.read.getTokenBalances([address, compoundV2CollateralAssets.map(a => a.address) as EthAddress[]]),
183
- getCollateralAssetsAddresses(provider, network, address),
182
+ loanInfoContract.read.getTokenBalances([address, compoundV2CollateralAssets.map(a => a.address) as EthAddress[]], setViemBlockNumber(blockNumber)),
183
+ getCollateralAssetsAddresses(provider, network, address, blockNumber),
184
184
  ]);
185
185
 
186
186
  const usedAssets = {} as CompoundV2UsedAssets;
@@ -236,7 +236,8 @@ export const getCompoundV2AccountData = async (
236
236
  network: NetworkNumber,
237
237
  address: EthAddress,
238
238
  assetsData: CompoundV2AssetsData,
239
- ): Promise<CompoundV2PositionData> => _getCompoundV2AccountData(getViemProvider(provider, network), network, address, assetsData);
239
+ blockNumber: Blockish = 'latest',
240
+ ): Promise<CompoundV2PositionData> => _getCompoundV2AccountData(getViemProvider(provider, network), network, address, assetsData, blockNumber);
240
241
 
241
242
  export const getCompoundV2FullPositionData = async (provider: EthereumProvider, network: NetworkNumber, address: EthAddress): Promise<CompoundV2PositionData> => {
242
243
  const marketData = await getCompoundV2MarketsData(provider, network);
@@ -23,17 +23,18 @@ import { CompoundMarkets } from '../markets/compound';
23
23
  import {
24
24
  getEthPrice, getCompPrice, getUSDCPrice, getWstETHPrice,
25
25
  } from '../services/priceService';
26
- import { getViemProvider, setViemBlockNumber } from '../services/viem';
26
+ import { getBlockTimestamp, getViemProvider, setViemBlockNumber } from '../services/viem';
27
27
  import { attachCompoundV3MerklIncentives, getCompoundV3MerklOpportunities } from './merkl';
28
28
 
29
29
  export { getCompoundV3MerklOpportunities, attachCompoundV3MerklIncentives } from './merkl';
30
30
 
31
31
  const getSupportedAssetsAddressesForMarket = (selectedMarket: CompoundMarketData, network: NetworkNumber) => selectedMarket.collAssets.map(asset => getAssetInfo(ethToWeth(asset), network)).map(addr => addr.address.toLowerCase());
32
32
 
33
- const getBaseAssetPriceFunction = (asset: string) => {
33
+ // Every branch takes (client, block) so the caller can pin the price read to the market block.
34
+ const getBaseAssetPriceFunction = (asset: string): ((client: Client, block: Blockish) => Promise<string>) => {
34
35
  switch (asset) {
35
36
  case 'wstETH':
36
- return getWstETHPrice;
37
+ return (client, block) => getWstETHPrice(client, NetworkNumber.Eth, block);
37
38
  case 'ETH':
38
39
  return getEthPrice;
39
40
  default:
@@ -41,16 +42,19 @@ const getBaseAssetPriceFunction = (asset: string) => {
41
42
  }
42
43
  };
43
44
 
44
- export const _getCompoundV3MarketsData = async (provider: Client, network: NetworkNumber, selectedMarket: CompoundMarketData, defaultProvider: Client): Promise<CompoundV3MarketsData> => {
45
+ /** `blockTimestamp` (unix seconds) is the historical block's timestamp when already known; otherwise it is read from the block. */
46
+ export const _getCompoundV3MarketsData = async (provider: Client, network: NetworkNumber, selectedMarket: CompoundMarketData, defaultProvider: Client, blockNumber: Blockish = 'latest', blockTimestamp?: number): Promise<CompoundV3MarketsData> => {
45
47
  const contract = CompV3ViewContractViem(provider, network);
46
48
 
47
49
  const [baseAssetPrice, compPrice, baseTokenInfo, collInfos, govInfo, merklOpportunities] = await Promise.all([
48
- getBaseAssetPriceFunction(selectedMarket.baseAsset)(defaultProvider),
49
- getCompPrice(defaultProvider),
50
- contract.read.getFullBaseTokenInfo([selectedMarket.baseMarketAddress]),
51
- contract.read.getFullCollInfos([selectedMarket.baseMarketAddress]),
52
- contract.read.getGovernanceInfoFull([selectedMarket.baseMarketAddress]),
53
- getCompoundV3MerklOpportunities(),
50
+ getBaseAssetPriceFunction(selectedMarket.baseAsset)(defaultProvider, blockNumber),
51
+ getCompPrice(defaultProvider, blockNumber),
52
+ contract.read.getFullBaseTokenInfo([selectedMarket.baseMarketAddress], setViemBlockNumber(blockNumber)),
53
+ contract.read.getFullCollInfos([selectedMarket.baseMarketAddress], setViemBlockNumber(blockNumber)),
54
+ contract.read.getGovernanceInfoFull([selectedMarket.baseMarketAddress], setViemBlockNumber(blockNumber)),
55
+ // Merkl rewards are keyed by time: a historical block resolves to its timestamp first (one RPC
56
+ // call unless given, none for 'latest') so the rewards in force at that moment are attached.
57
+ getBlockTimestamp(provider, blockNumber, blockTimestamp).then((asOfTimestamp) => getCompoundV3MerklOpportunities(asOfTimestamp, network)),
54
58
  ]);
55
59
 
56
60
  const { isSupplyPaused, isWithdrawPaused } = govInfo;
@@ -95,7 +99,7 @@ export const _getCompoundV3MarketsData = async (provider: Client, network: Netwo
95
99
  };
96
100
  };
97
101
 
98
- export const getCompoundV3MarketsData = async (provider: EthereumProvider, network: NetworkNumber, selectedMarket: CompoundMarketData, defaultProvider: EthereumProvider): Promise<CompoundV3MarketsData> => _getCompoundV3MarketsData(getViemProvider(provider, network), network, selectedMarket, getViemProvider(defaultProvider, network));
102
+ export const getCompoundV3MarketsData = async (provider: EthereumProvider, network: NetworkNumber, selectedMarket: CompoundMarketData, defaultProvider: EthereumProvider, blockNumber: Blockish = 'latest', blockTimestamp?: number): Promise<CompoundV3MarketsData> => _getCompoundV3MarketsData(getViemProvider(provider, network), network, selectedMarket, getViemProvider(defaultProvider, network), blockNumber, blockTimestamp);
99
103
 
100
104
  export const EMPTY_COMPOUND_V3_DATA = {
101
105
  usedAssets: {},
@@ -183,6 +187,7 @@ export const _getCompoundV3AccountData = async (
183
187
  selectedMarket: CompoundMarketData,
184
188
  assetsData: CompoundV3AssetsData,
185
189
  }),
190
+ blockNumber: Blockish = 'latest',
186
191
  ): Promise<CompoundV3PositionData> => {
187
192
  if (!address) throw new Error('No address provided');
188
193
  const {
@@ -198,8 +203,8 @@ export const _getCompoundV3AccountData = async (
198
203
 
199
204
 
200
205
  const [loanData, isAllowed] = await Promise.all([
201
- contract.read.getLoanData([selectedMarket.baseMarketAddress, address]),
202
- contract.read.isAllowed([selectedMarket.baseMarketAddress, address, (proxyAddress || ZERO_ADDRESS)]),
206
+ contract.read.getLoanData([selectedMarket.baseMarketAddress, address], setViemBlockNumber(blockNumber)),
207
+ contract.read.isAllowed([selectedMarket.baseMarketAddress, address, (proxyAddress || ZERO_ADDRESS)], setViemBlockNumber(blockNumber)),
203
208
  ]);
204
209
 
205
210
  const usedAssets: CompoundV3UsedAssets = {};
@@ -274,7 +279,8 @@ export const getCompoundV3AccountData = async (
274
279
  selectedMarket: CompoundMarketData,
275
280
  assetsData: CompoundV3AssetsData,
276
281
  }),
277
- ): Promise<CompoundV3PositionData> => _getCompoundV3AccountData(getViemProvider(provider, network), network, address, proxyAddress, extractedState);
282
+ blockNumber: Blockish = 'latest',
283
+ ): Promise<CompoundV3PositionData> => _getCompoundV3AccountData(getViemProvider(provider, network), network, address, proxyAddress, extractedState, blockNumber);
278
284
 
279
285
  export const getCompoundV3FullPositionData = async (provider: EthereumProvider, network: NetworkNumber, address: EthAddress, proxyAddress: EthAddress, selectedMarket: CompoundMarketData, defaultProvider: EthereumProvider): Promise<CompoundV3PositionData> => {
280
286
  const marketData = await getCompoundV3MarketsData(provider, network, selectedMarket, defaultProvider);