@defisaver/positions-sdk 2.1.137-audit-dev → 2.1.138

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.
@@ -44,6 +44,11 @@ export interface AaveV4SpokeInfo {
44
44
  value: AaveV4SpokesType;
45
45
  url: string;
46
46
  address: EthAddress;
47
+ /**
48
+ * Known hub addresses, used only to prefetch hub data in parallel with the spoke data.
49
+ * The on-chain reserves are the source of truth — hubs found there but missing here are
50
+ * fetched dynamically, so this list going stale can't break spoke loading.
51
+ */
47
52
  hubs: EthAddress[];
48
53
  }
49
54
  export interface AaveV4SpokeData {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defisaver/positions-sdk",
3
- "version": "2.1.137-audit-dev",
3
+ "version": "2.1.138",
4
4
  "description": "",
5
5
  "main": "./cjs/index.js",
6
6
  "module": "./esm/index.js",
@@ -25,17 +25,11 @@
25
25
  "@types/lodash": "^4.17.15",
26
26
  "@types/memoizee": "^0.4.12",
27
27
  "decimal.js": "^10.6.0",
28
+ "graphql-request": "^1.8.2",
28
29
  "lodash": "^4.17.21",
29
30
  "memoizee": "^0.4.17",
30
31
  "viem": "^2.37.9"
31
32
  },
32
- "overrides": {
33
- "serialize-javascript": "^7.1.0",
34
- "uuid": "^11.1.1",
35
- "@typescript-eslint/typescript-estree": {
36
- "minimatch": "^9.0.9"
37
- }
38
- },
39
33
  "devDependencies": {
40
34
  "@defisaver/eslint-config": "^1.0.1",
41
35
  "@metamask/eth-json-rpc-middleware": "^15.0.1",
@@ -46,7 +40,6 @@
46
40
  "dotenv": "^16.3.1",
47
41
  "eslint": "^8.49.0",
48
42
  "eslint-plugin-import": "^2.31.0",
49
- "eslint-webpack-plugin": "^4.0.1",
50
43
  "mocha": "^10.2.0",
51
44
  "nock": "^14.0.0",
52
45
  "ts-node": "^10.9.2",
@@ -21,7 +21,9 @@ import {
21
21
  } from '../types';
22
22
  import { AaveV4ViewContractViem } from '../contracts';
23
23
  import { getStakingApy, STAKING_ASSETS } from '../staking';
24
- import { isMaxUint, wethToEth, wethToEthByAddress } from '../services/utils';
24
+ import {
25
+ isMaxUint, shortenAddress, wethToEth, wethToEthByAddress,
26
+ } from '../services/utils';
25
27
  import { aaveV4GetAggregatedPositionData, calcUserRiskPremiumBps } from '../helpers/aaveV4Helpers';
26
28
  import { findAaveV4SpokeByAddress, getAaveV4HubByAddress } from '../markets/aaveV4';
27
29
  import { aprToApy } from '../moneymarket';
@@ -56,10 +58,10 @@ const formatReserveAsset = async (reserveAsset: AaveV4ReserveAssetOnChain, hubAs
56
58
  // tokens package. Flag it so consumers can render it read-only instead of feeding NaN into amounts.
57
59
  const isUnsupported = assetInfo.symbol === '?';
58
60
  const symbol = wethToEth(assetInfo.symbol);
61
+ // The hub registry only provides display metadata — a hub missing from it (newly deployed by
62
+ // Aave, SDK not yet updated) must not prevent the reserve from loading, so fall back to a
63
+ // generated label instead of failing.
59
64
  const hubInfo = getAaveV4HubByAddress(network, reserveAsset.hub);
60
- if (!hubInfo) {
61
- throw new Error(`Hub not found with address: ${reserveAsset.hub}`);
62
- }
63
65
 
64
66
  const isStakingAsset = STAKING_ASSETS.includes(symbol);
65
67
  const supplyIncentives: IncentiveData[] = [];
@@ -124,8 +126,8 @@ const formatReserveAsset = async (reserveAsset: AaveV4ReserveAssetOnChain, hubAs
124
126
  decimals: reserveAsset.decimals,
125
127
  isUnsupported,
126
128
  underlying: reserveAsset.underlying,
127
- hub: hubInfo.address,
128
- hubName: hubInfo?.label,
129
+ hub: hubInfo?.address ?? reserveAsset.hub,
130
+ hubName: hubInfo?.label ?? `Hub ${shortenAddress(reserveAsset.hub)}`,
129
131
  assetId: reserveAsset.assetId,
130
132
  reserveId,
131
133
  paused: reserveAsset.paused,
@@ -163,16 +165,32 @@ const formatReserveAsset = async (reserveAsset: AaveV4ReserveAssetOnChain, hubAs
163
165
  export async function _getAaveV4SpokeData(provider: Client, network: NetworkNumber, market: AaveV4SpokeInfo, blockNumber: 'latest' | number = 'latest'): Promise<AaveV4SpokeData> {
164
166
  const viewContract = AaveV4ViewContractViem(provider, network, blockNumber);
165
167
 
166
- const hubsData: Record<EthAddress, AaveV4HubOnChainData> = {};
168
+ const hubsData: Record<string, AaveV4HubOnChainData> = {};
169
+ const loadHubData = async (hubAddress: EthAddress) => {
170
+ hubsData[hubAddress.toLowerCase()] = await fetchHubData(viewContract, hubAddress);
171
+ };
172
+
173
+ // market.hubs is only a prefetch hint (lets known hubs load in parallel with the spoke data), so
174
+ // failures are tolerated here — any hub the reserves actually reference is (re)fetched below.
167
175
  const [spokeData, merklCampaigns] = await Promise.all([
168
176
  viewContract.read.getSpokeDataFull([market.address]),
169
177
  getAaveV4MerkleCampaigns(network),
170
- ...market.hubs.map(async (hubAddress) => {
171
- hubsData[hubAddress] = await fetchHubData(viewContract, hubAddress);
172
- }),
178
+ ...market.hubs.map((hubAddress) => loadHubData(hubAddress).catch(() => {})),
173
179
  ]);
174
180
 
175
- const reserveAssetsArray = await Promise.all(spokeData[1].map(async (reserveAssetOnChain: AaveV4ReserveAssetOnChain, index: number) => formatReserveAsset(reserveAssetOnChain, hubsData[reserveAssetOnChain.hub].assets[reserveAssetOnChain.assetId], index, +spokeData[0].oracleDecimals.toString(), network)));
181
+ // The on-chain reserves are the source of truth for which hubs the spoke uses — fetch any hub
182
+ // the prefetch didn't cover, so an asset listed from a hub unknown to the SDK can't break the spoke.
183
+ const missingHubs = [...new Set(spokeData[1].map((reserveAsset: AaveV4ReserveAssetOnChain) => reserveAsset.hub.toLowerCase() as EthAddress))]
184
+ .filter((hubAddress) => !hubsData[hubAddress]);
185
+ await Promise.all(missingHubs.map(loadHubData));
186
+
187
+ const reserveAssetsArray = (await Promise.all(spokeData[1].map(async (reserveAssetOnChain: AaveV4ReserveAssetOnChain, index: number) => {
188
+ const hubAsset = hubsData[reserveAssetOnChain.hub.toLowerCase()]?.assets[reserveAssetOnChain.assetId];
189
+ // A reserve whose hub-side asset data can't be resolved is skipped instead of failing the
190
+ // whole spoke (position math degrades for that one asset only).
191
+ if (!hubAsset) return null;
192
+ return formatReserveAsset(reserveAssetOnChain, hubAsset, index, +spokeData[0].oracleDecimals.toString(), network);
193
+ }))).filter((asset): asset is AaveV4ReserveAssetData => asset !== null);
176
194
 
177
195
  const enrichedAssets = reserveAssetsArray.map((asset) => attachAaveV4MerklIncentives(asset, market.address, merklCampaigns));
178
196
 
package/src/contracts.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { getContract, Client, GetContractReturnType } from 'viem';
1
+ import { getContract, Client } from 'viem';
2
2
  import * as configRaw from './config/contracts';
3
3
  import {
4
4
  Blockish, EthAddress, HexString, NetworkNumber,
@@ -57,7 +57,7 @@ export const getConfigContractAbi = <TKey extends ConfigKey>(name: TKey, network
57
57
  return latestAbi as unknown as typeof configRaw[TKey]['abi'];
58
58
  };
59
59
 
60
- export const createViemContractFromConfigFunc = <TKey extends ConfigKey>(name: TKey, _address?: HexString) => (client: Client, network: NetworkNumber, block?: Blockish): GetContractReturnType<typeof configRaw[TKey]['abi'], Client, HexString> => {
60
+ export const createViemContractFromConfigFunc = <TKey extends ConfigKey>(name: TKey, _address?: HexString) => (client: Client, network: NetworkNumber, block?: Blockish) => {
61
61
  const address = (_address || getConfigContractAddress(name, network, block));
62
62
  const abi = getConfigContractAbi(name, network, block) as typeof configRaw[TKey]['abi'];
63
63
  return getContract({
@@ -1,9 +1,9 @@
1
1
  import { Client } from 'viem';
2
2
  import Dec from 'decimal.js';
3
+ import { request as graphqlRequest } from 'graphql-request';
3
4
  import { assetAmountInEth } from '@defisaver/tokens';
4
5
  import * as morphoVaultsOptions from './options';
5
6
  import { EthAddress, EthereumProvider, NetworkNumber } from '../../types/common';
6
- import { LONGER_TIMEOUT } from '../../services/utils';
7
7
  import { getViemProvider } from '../../services/viem';
8
8
  import { getMorphoVaultContractViem } from '../../contracts';
9
9
  import { MorphoVault, SavingsVaultData } from '../../types';
@@ -30,22 +30,6 @@ const vaultDataQuery = `
30
30
 
31
31
  const MORPHO_BLUE_API = 'https://api.morpho.org/graphql';
32
32
 
33
- const fetchVaultData = async (address: EthAddress, chainId: NetworkNumber) => {
34
- const res = await fetch(MORPHO_BLUE_API, {
35
- method: 'POST',
36
- headers: { 'Content-Type': 'application/json' },
37
- body: JSON.stringify({
38
- query: vaultDataQuery,
39
- variables: { address, chainId },
40
- }),
41
- signal: AbortSignal.timeout(LONGER_TIMEOUT),
42
- });
43
- if (!res.ok) throw new Error(`Morpho vault request failed: ${res.status}`);
44
- const body = await res.json();
45
- if (!body?.data) throw new Error('Morpho vault response missing data');
46
- return body.data;
47
- };
48
-
49
33
  export const _getMorphoVaultData = async (provider: Client, network: NetworkNumber, morphoVault: MorphoVault, accounts: EthAddress[]): Promise<SavingsVaultData> => {
50
34
  const morphoVaultContract = getMorphoVaultContractViem(provider, morphoVault.address);
51
35
 
@@ -56,7 +40,7 @@ export const _getMorphoVaultData = async (provider: Client, network: NetworkNumb
56
40
  morphoVaultContract.read.totalSupply(),
57
41
  morphoVaultContract.read.decimals(),
58
42
  morphoVaultContract.read.DECIMALS_OFFSET(),
59
- fetchVaultData(morphoVault.address, network),
43
+ graphqlRequest(MORPHO_BLUE_API, vaultDataQuery, { address: morphoVault.address, chainId: network }),
60
44
  ...accounts.map(async (account) => {
61
45
  const share = await morphoVaultContract.read.balanceOf([account]);
62
46
  shares[account] = share;
@@ -20,6 +20,8 @@ export const isAddress = (address: string) => typeof address === 'string' && (ne
20
20
 
21
21
  export const compareAddresses = (addr1 = '', addr2 = '') => addr1.toLowerCase() === addr2.toLowerCase();
22
22
 
23
+ export const shortenAddress = (address = '', start = 8, end = 4) => `${address.substring(0, start)}...${address.slice(-end)}`;
24
+
23
25
  export const getWeiAmountForDecimals = (amount: string | number, decimals: number) => new Dec(amount).mul(10 ** decimals).floor().toString();
24
26
 
25
27
  export const getEthAmountForDecimals = (amount: string | number, decimals: string | number) => new Dec(amount).div(10 ** +decimals).toString();
@@ -52,6 +52,11 @@ export interface AaveV4SpokeInfo {
52
52
  value: AaveV4SpokesType,
53
53
  url: string,
54
54
  address: EthAddress,
55
+ /**
56
+ * Known hub addresses, used only to prefetch hub data in parallel with the spoke data.
57
+ * The on-chain reserves are the source of truth — hubs found there but missing here are
58
+ * fetched dynamically, so this list going stale can't break spoke loading.
59
+ */
55
60
  hubs: EthAddress[],
56
61
  }
57
62