import BigNumberJs from 'bignumber.js'; import { DeploylessViewerClient } from 'deployless-view'; import { BigNumberish, BytesLike, providers } from 'ethers'; import { isAddress } from 'ethers/lib/utils'; import _ from 'lodash'; import { ReservesHelperInput, UserReservesHelperInput } from '../index'; import { WPunkService } from '../wpunk-contract'; import { UiPoolDataProvider as IUiPoolDataProvider } from './typechain/IUiPoolDataProvider'; import { UiPoolDataProvider__factory } from './typechain/IUiPoolDataProvider__factory'; import { AtomicTokenInfo, PoolBaseCurrencyHumanized, ReserveDataHumanized, ReservesData, ReservesDataHumanized, UserNtokenData, UserNtokenDataReq, UserNtokenHumanizedData, UserNtokenHumanizedDataReq, UserReserveData, UserReserveDataHumanized, UserReservesHumanizedHelperInput, } from './types'; export * from './types'; const ammSymbolMap: Record = { '0xae461ca67b15dc8dc81ce7615e0320da1a9ab8d5': 'UNIDAIUSDC', '0x004375dff511095cc5a197a54140a24efef3a416': 'UNIWBTCUSDC', '0xa478c2975ab1ea89e8196811f51a7b7ade33eb11': 'UNIDAIWETH', '0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc': 'UNIUSDCWETH', '0xdfc14d2af169b0d36c4eff567ada9b2e0cae044f': 'UNIAAVEWETH', '0xb6909b960dbbe7392d405429eb2b3649752b4838': 'UNIBATWETH', '0x3da1313ae46132a397d90d95b1424a9a7e3e0fce': 'UNICRVWETH', '0xa2107fa5b38d9bbd2c461d6edf11b11a50f6b974': 'UNILINKWETH', '0xc2adda861f89bbb333c90c492cb837741916a225': 'UNIMKRWETH', '0x8bd1661da98ebdd3bd080f0be4e6d9be8ce9858c': 'UNIRENWETH', '0x43ae24960e5534731fc831386c07755a2dc33d47': 'UNISNXWETH', '0xd3d2e2692501a5c9ca623199d38826e513033a17': 'UNIUNIWETH', '0xbb2b8038a1640196fbe3e38816f3e67cba72d940': 'UNIWBTCWETH', '0x2fdbadf3c4d5a8666bc06645b8358ab803996e28': 'UNIYFIWETH', '0x1eff8af5d577060ba4ac8a29a13525bb0ee2a3d5': 'BPTWBTCWETH', '0x59a19d8c652fa0284f44113d0ff9aba70bd46fb4': 'BPTBALWETH', }; const nftTypes: Record = { BAYC: 1, WPUNKS: 1, PUNKS: 2, Ͼ: 2, MAYC: 1, DOODLE: 1, DOODLES: 1, MOONBIRD: 3, MOON: 3, MEEBITS: 1, '⚇': 1, AZUKI: 1, OTHR: 1, CLONEX: 1, CloneX: 1, UNIV3POS: 1, 'UNI-V3-POS': 1, ATK: 1, BTK: 1, GTK: 1, BEANZ: 3, BLOCKS: 1, SQGL: 1, EXP: 3, VSL: 3, KODA: 3, DeGods: 3, DEGODS: 3, 'HV-MTL': 3, NVMTL: 3, HVMTL: 3, }; export interface NFTConfig { nft: string; nftType: number; useTotalSupplyAsMaxId: boolean; viewerStep: number; } export interface UiPoolDataProviderContext { uiPoolDataProviderAddress: string; provider: providers.Provider; chainId: number; } export interface ExtraTokenData { scaledXTokenBalance: string; collaterizedBalance: string; ownedTokens: number[]; suppliedTokens: number[]; collaterizedTokens: number[]; auctionedTokens: number[]; tokenTraitMultipliers: Map; } export interface UiPoolDataProviderInterface { getReservesList: (args: ReservesHelperInput) => Promise; getReservesData: (args: ReservesHelperInput) => Promise; getUserReservesData: ( args: UserReservesHelperInput, ) => Promise; getReservesHumanized: ( args: ReservesHelperInput, ) => Promise; getUserReservesHumanized: ( args: UserReservesHumanizedHelperInput, ) => Promise<{ userReserves: UserReserveDataHumanized[]; userEmodeCategoryId: number; }>; getNtokenData: (args: UserNtokenDataReq) => Promise; getNtokenDataHumanized: ( args: UserNtokenHumanizedDataReq, ) => Promise; } export class UiPoolDataProvider implements UiPoolDataProviderInterface { private readonly deployLessViewer; private readonly _contract: IUiPoolDataProvider; private readonly chainId: number; private readonly wpunk_servive: WPunkService; /** * Constructor * @param context The ui pool data provider context */ public constructor(context: UiPoolDataProviderContext) { if (!isAddress(context.uiPoolDataProviderAddress)) { throw new Error('contract address is not valid'); } this._contract = UiPoolDataProvider__factory.connect( context.uiPoolDataProviderAddress, context.provider, ); this.chainId = context.chainId; // eslint-disable-next-line @typescript-eslint/no-explicit-any this.deployLessViewer = new DeploylessViewerClient(context.provider as any); this.wpunk_servive = new WPunkService(context.provider); } /** * Get the underlying asset address for each lending pool reserve */ public async getReservesList({ lendingPoolAddressProvider, }: ReservesHelperInput): Promise { if (!isAddress(lendingPoolAddressProvider)) { throw new Error('Lending pool address is not valid'); } return this._contract.getReservesList(lendingPoolAddressProvider); } public async batchGetUniswapV3LpTokenData({ poolAddressProvider, tokenAddress, tokenIds, }: { oracleAddress: string; poolAddressProvider: string; tokenAddress: string; tokenIds: number[]; }) { const args = tokenIds.map(tokenId => { return { target: this._contract.address, callData: this._contract.interface.encodeFunctionData( 'getUniswapV3LpTokenData', [poolAddressProvider, tokenAddress, tokenId], ), }; }); const splitArgs = _.chunk(args, 30); const results = await Promise.all( splitArgs.map(async args => { const { resultsArray } = (await this.deployLessViewer.multicall( args, )) as unknown as { resultsArray: BytesLike[]; }; return resultsArray; }), ); return results .flat() .map( each => this._contract.interface.decodeFunctionResult( 'getUniswapV3LpTokenData', each, )[0] as Awaited< ReturnType >, ); } /** * Get data for each lending pool reserve */ public async getReservesData({ lendingPoolAddressProvider, }: ReservesHelperInput) { if (!isAddress(lendingPoolAddressProvider)) { throw new Error('Lending pool address is not valid'); } return this._contract.getReservesData(lendingPoolAddressProvider); } /** * Get data for user nToken useAscollectoral status */ public async getNtokenData({ lendingPoolAddressProvider, nTokenAddresses, tokenIds, }: UserNtokenDataReq): Promise { if (!isAddress(lendingPoolAddressProvider)) { throw new Error('Lending pool address is not valid'); } return this._contract.getNTokenData(nTokenAddresses, tokenIds); } /** * Get data for user nToken useAscollectoral status */ public async getNtokenDataHumanized({ lendingPoolAddressProvider, user, nTokenAddresses, }: UserNtokenHumanizedDataReq): Promise { if (!isAddress(lendingPoolAddressProvider)) { throw new Error('Lending pool address is not valid'); } if (!isAddress(user)) { throw new Error('user address is not valid'); } const userNtokenHumanizedDatas: UserNtokenHumanizedData[] = []; const tokenIds: BigNumberish[][] = []; const nftAndNtfType = nTokenAddresses.map(nTokenAddr => { userNtokenHumanizedDatas.push({ nTokenAddress: nTokenAddr.toLowerCase(), suppliedTokens: [], collaterizedTokens: [], }); return { nft: nTokenAddr.toLowerCase(), nftType: 1, }; }); if (nTokenAddresses.length > 0) { const raw = await this.deployLessViewer.batchGetAllTokensByOwner( user, nftAndNtfType, ); const positions = raw?.tokenInfos[0][0] as number[][]; positions.forEach((ids, index) => { tokenIds.push(ids); userNtokenHumanizedDatas[index].suppliedTokens = ids; }); const rst = await this.getNtokenData({ lendingPoolAddressProvider, nTokenAddresses, tokenIds, }); rst.forEach((ele, index) => { ele.forEach(e => { if (e.useAsCollateral) { userNtokenHumanizedDatas[index].collaterizedTokens?.push( e.tokenId.toNumber(), ); } }); }); } return userNtokenHumanizedDatas; } /** * Get data for each user reserve on the lending pool */ public async getUserReservesData({ lendingPoolAddressProvider, user, }: UserReservesHelperInput): Promise { if (!isAddress(lendingPoolAddressProvider)) { throw new Error('Lending pool address is not valid'); } if (!isAddress(user)) { throw new Error('User address is not a valid ethereum address'); } return this._contract.getUserReservesData(lendingPoolAddressProvider, user); } public async getReservesHumanized({ lendingPoolAddressProvider, }: ReservesHelperInput): Promise { const { 0: reservesRaw, 1: poolBaseCurrencyRaw } = await this.getReservesData({ lendingPoolAddressProvider }); const reservesData: ReserveDataHumanized[] = reservesRaw.map(reserveRaw => { const symbol = ammSymbolMap[reserveRaw.underlyingAsset.toLowerCase()] ? ammSymbolMap[reserveRaw.underlyingAsset.toLowerCase()] : reserveRaw.symbol; const { timeLockStrategyData, decimals } = reserveRaw; const normalizedTimeLockStrategyData = { minThreshold: new BigNumberJs( timeLockStrategyData.minThreshold.toString(), ).shiftedBy(-decimals.toNumber()), midThreshold: new BigNumberJs( timeLockStrategyData.midThreshold.toString(), ).shiftedBy(-decimals.toNumber()), minWaitTime: timeLockStrategyData.minWaitTime, midWaitTime: timeLockStrategyData.midWaitTime, maxWaitTime: timeLockStrategyData.maxWaitTime, poolPeriodWaitTime: timeLockStrategyData.poolPeriodWaitTime, poolPeriodLimit: new BigNumberJs( timeLockStrategyData.poolPeriodLimit.toString(), ).shiftedBy(-decimals.toNumber()), period: timeLockStrategyData.period.toNumber(), totalAmountInCurrentPeriod: new BigNumberJs( timeLockStrategyData.totalAmountInCurrentPeriod.toString(), ).shiftedBy(-decimals.toNumber()), lastResetTimestamp: timeLockStrategyData.lastResetTimestamp, }; return { id: `${this.chainId}-${reserveRaw.underlyingAsset}-${lendingPoolAddressProvider}`.toLowerCase(), underlyingAsset: reserveRaw.underlyingAsset.toLowerCase(), name: reserveRaw.name, assetType: reserveRaw.assetType, symbol: wrapTokenSymbol(symbol), decimals: reserveRaw.decimals.toNumber(), baseLTVasCollateral: reserveRaw.baseLTVasCollateral.toString(), reserveLiquidationThreshold: reserveRaw.reserveLiquidationThreshold.toString(), reserveLiquidationBonus: reserveRaw.reserveLiquidationBonus.toString(), reserveFactor: reserveRaw.reserveFactor.toString(), usageAsCollateralEnabled: reserveRaw.usageAsCollateralEnabled, borrowingEnabled: reserveRaw.borrowingEnabled, isActive: reserveRaw.isActive, isFrozen: reserveRaw.isFrozen, liquidityIndex: reserveRaw.liquidityIndex.toString(), variableBorrowIndex: reserveRaw.variableBorrowIndex.toString(), liquidityRate: reserveRaw.liquidityRate.toString(), variableBorrowRate: reserveRaw.variableBorrowRate.toString(), lastUpdateTimestamp: reserveRaw.lastUpdateTimestamp, aTokenAddress: reserveRaw.xTokenAddress.toString(), variableDebtTokenAddress: reserveRaw.variableDebtTokenAddress.toString(), interestRateStrategyAddress: reserveRaw.interestRateStrategyAddress.toString(), availableLiquidity: reserveRaw.availableLiquidity.toString(), totalScaledVariableDebt: reserveRaw.totalScaledVariableDebt.toString(), priceInMarketReferenceCurrency: reserveRaw.priceInMarketReferenceCurrency.toString(), priceOracle: reserveRaw.priceOracle, variableRateSlope1: reserveRaw.variableRateSlope1.toString(), variableRateSlope2: reserveRaw.variableRateSlope2.toString(), baseVariableBorrowRate: reserveRaw.baseVariableBorrowRate.toString(), optimalUsageRatio: reserveRaw.optimalUsageRatio.toString(), // new fields isPaused: reserveRaw.isPaused, borrowCap: reserveRaw.borrowCap.toString(), supplyCap: reserveRaw.supplyCap.toString(), accruedToTreasury: reserveRaw.accruedToTreasury.toString(), // new fields auctionEnabled: reserveRaw.auctionEnabled, auctionStrategyAddress: reserveRaw.auctionStrategyAddress, // short term workaround this should be updated in next deploy dynamicConfigsEnabled: reserveRaw.isAtomicPricing, isAtomic: reserveRaw.isAtomicPricing, // The contract is no longer returned, but the default value needed for the calculation debtCeiling: '0', eModeCategoryId: 0, eModeLtv: 0, eModeLiquidationThreshold: 0, eModeLiquidationBonus: 0, eModePriceSource: '', eModeLabel: '', borrowableInIsolation: false, unbacked: '0', isolationModeTotalDebt: '0', debtCeilingDecimals: 0, timeLockStrategyData: normalizedTimeLockStrategyData, }; }); const baseCurrencyData: PoolBaseCurrencyHumanized = { // this is to get the decimals from the unit so 1e18 = string length of 19 - 1 to get the number of 0 marketReferenceCurrencyDecimals: poolBaseCurrencyRaw.marketReferenceCurrencyUnit.toString().length - 1, marketReferenceCurrencyPriceInUsd: poolBaseCurrencyRaw.marketReferenceCurrencyPriceInUsd.toString(), networkBaseTokenPriceInUsd: poolBaseCurrencyRaw.networkBaseTokenPriceInUsd.toString(), networkBaseTokenPriceDecimals: poolBaseCurrencyRaw.networkBaseTokenPriceDecimals, }; return { reservesData, baseCurrencyData, }; } public async getUserReservesHumanized({ lendingPoolAddressProvider, user, reservesDataHumanized, }: UserReservesHumanizedHelperInput): Promise<{ userReserves: UserReserveDataHumanized[]; userEmodeCategoryId: number; }> { const userReservesRaw: UserReserveData = await this.getUserReservesData({ lendingPoolAddressProvider, user, }); const extraTokenDatas: Map = new Map(); const nftAddresses: string[] = []; const nTokenAddresses: string[] = []; const tokenIds: BigNumberish[][] = []; const underlyingNFTConfig: NFTConfig[] = []; const nTokenNFTConfig: NFTConfig[] = []; const punkReserveData = reservesDataHumanized.reservesData.find( reserve => reserve.symbol === 'WPUNKS', ); if (punkReserveData) { await this.wpunk_servive.connect(punkReserveData?.underlyingAsset); } userReservesRaw.forEach(userReserveRaw => { const reserveData = reservesDataHumanized.reservesData.find( reserve => reserve.underlyingAsset.toLowerCase() === userReserveRaw.underlyingAsset.toLowerCase(), ); if (reserveData?.decimals === 0) { const addr = userReserveRaw.underlyingAsset.toLowerCase(); const nTokenAddr = reserveData?.aTokenAddress.toLowerCase(); nftAddresses.push(addr); nTokenAddresses.push(nTokenAddr); extraTokenDatas.set(addr, { scaledXTokenBalance: userReserveRaw.scaledXTokenBalance.toString(), collaterizedBalance: userReserveRaw.collateralizedBalance.toString(), ownedTokens: [], suppliedTokens: [], collaterizedTokens: [], auctionedTokens: [], tokenTraitMultipliers: new Map(), }); nTokenNFTConfig.push({ nft: reserveData?.aTokenAddress?.toLowerCase(), nftType: 1, useTotalSupplyAsMaxId: useTotalSupplyAsMaxId(reserveData.symbol), viewerStep: getViewerStep(reserveData.symbol), }); underlyingNFTConfig.push({ nft: addr, nftType: nftTypes[reserveData.symbol] || 3, useTotalSupplyAsMaxId: useTotalSupplyAsMaxId(reserveData.symbol), viewerStep: getViewerStep(reserveData.symbol), }); } }); let stakeFishNtokenData: UserNtokenData[] = []; if (nTokenAddresses.length > 0) { const positionsRaw = await this.deployLessViewer.batchGetAllTokensByOwner( user, nTokenNFTConfig, ); const positions = positionsRaw?.tokenInfos[0][0] as number[][]; const owned: number[][] = await Promise.all( underlyingNFTConfig.map(async data => { let tokenIds: number[] = []; // for forknet, we bypass non enumerable nft to reduce indexing time if (data.nftType !== 1 && this.chainId === 522) { tokenIds = [] as number[]; } else if (data.nftType === 1) { const ownedRaw = await this.deployLessViewer.batchGetAllTokensByOwner(user, [ data, ]); tokenIds = ownedRaw.tokenInfos[0][0][0] as number[]; } else { const ownedRaw = await this.deployLessViewer.getAllTokensByOwner( user, data.nft, data.nftType, data.viewerStep, data.useTotalSupplyAsMaxId, ); tokenIds = ownedRaw.tokenInfos as number[]; } return tokenIds; }), ); positions.forEach((ids, index) => { tokenIds.push(ids); const data = extraTokenDatas.get(nftAddresses[index]); if (data) { data.suppliedTokens = ids; } }); owned.forEach((ids, index) => { tokenIds[index] = tokenIds[index]?.concat(ids); const data = extraTokenDatas.get(nftAddresses[index]); if (data) { data.ownedTokens = ids; } }); const userNTokensData = await this.getNtokenData({ lendingPoolAddressProvider, nTokenAddresses, tokenIds, }); userNTokensData.forEach((ele, index) => { const reserveData = reservesDataHumanized.reservesData.find( reserve => reserve.underlyingAsset.toLowerCase() === nftAddresses[index].toLowerCase(), ); if (reserveData?.symbol.includes('VLDR')) { stakeFishNtokenData = ele; } ele.forEach(e => { const data = extraTokenDatas.get(nftAddresses[index]); if (data) { if (e.useAsCollateral) { data.collaterizedTokens.push(e.tokenId.toNumber()); } if (e.isAuctioned) { data.auctionedTokens.push(e.tokenId.toNumber()); } data.tokenTraitMultipliers.set( e.tokenId.toString(), e.multiplier.toString(), ); } }); }); } const userReserves: UserReserveDataHumanized[] = await Promise.all( userReservesRaw.map(async userReserveRaw => { const underlyingAsset = userReserveRaw.underlyingAsset.toLowerCase() ?? ''; const reserveData = reservesDataHumanized.reservesData.find( reserve => reserve.underlyingAsset.toLowerCase() === underlyingAsset, ); const isAtomic = reserveData?.isAtomic ?? false; const suppliedTokens = extraTokenDatas.get(userReserveRaw.underlyingAsset.toLowerCase()) ?.suppliedTokens ?? []; const ownedTokens = extraTokenDatas.get(userReserveRaw.underlyingAsset.toLowerCase()) ?.ownedTokens ?? []; const atomicTokenIds = suppliedTokens.concat(ownedTokens); const atomicTokens: Map = new Map(); if (isAtomic && atomicTokenIds.length > 0) { if (reserveData?.symbol.includes('UNI-V3-POS')) { const lpTokenDatas = await this.batchGetUniswapV3LpTokenData({ poolAddressProvider: lendingPoolAddressProvider, tokenAddress: reserveData?.underlyingAsset ?? '', tokenIds: atomicTokenIds, oracleAddress: '', }); lpTokenDatas.forEach((data, index) => { atomicTokens.set(atomicTokenIds[index].toString(), { ...data, tokenId: atomicTokenIds[index].toString(), tokenPriceInMarketCurrency: data.tokenPrice.toString(), baseLTVasCollateral: data.baseLTVasCollateral.toString(), reserveLiquidationThreshold: data.reserveLiquidationThreshold.toString(), }); }); } } if ( stakeFishNtokenData.length !== 0 && reserveData?.symbol.includes('VLDR') ) { stakeFishNtokenData.forEach(data => { atomicTokens.set(data.tokenId.toString(), { ...data, tokenId: data.tokenId.toString(), tokenPriceInMarketCurrency: reserveData.priceInMarketReferenceCurrency, baseLTVasCollateral: reserveData.baseLTVasCollateral, reserveLiquidationThreshold: reserveData.reserveLiquidationThreshold, }); }); } return { id: `${this.chainId}-${user}-${userReserveRaw.underlyingAsset}-${lendingPoolAddressProvider}`.toLowerCase(), underlyingAsset, scaledXTokenBalance: userReserveRaw.scaledXTokenBalance.toString(), collaterizedBalance: userReserveRaw.collateralizedBalance.toString(), usageAsCollateralEnabledOnUser: userReserveRaw.usageAsCollateralEnabledOnUser, scaledVariableDebt: userReserveRaw.scaledVariableDebt.toString(), avgMultiplier: userReserveRaw.avgMultiplier.toString(), ownedTokens, suppliedTokens, collaterizedTokens: extraTokenDatas.get(userReserveRaw.underlyingAsset.toLowerCase()) ?.collaterizedTokens ?? [], auctionedTokens: extraTokenDatas.get(userReserveRaw.underlyingAsset.toLowerCase()) ?.auctionedTokens ?? [], isAtomic, atomicTokens, tokenTraitMultipliers: extraTokenDatas.get(userReserveRaw.underlyingAsset.toLowerCase()) ?.tokenTraitMultipliers ?? new Map(), }; }), ); return { userReserves, userEmodeCategoryId: 0, }; } public async getDelegatesForTokens(nTokenAddr: string, tokenIds: string[]) { return this._contract.getDelegatesForTokens(nTokenAddr, tokenIds); } public async getDategatesForTokensInBatch( tokenInfos: Array<{ nTokenAddr: string; tokenIds: string[] }>, ) { const raw = (await this.deployLessViewer.multicall( tokenInfos.map(asset => { return { target: this._contract.address, callData: this._contract.interface.encodeFunctionData( 'getDelegatesForTokens', [asset.nTokenAddr, asset.tokenIds], ), }; }), )) as { resultsArray: string[]; }; return raw.resultsArray.map(each => this._contract.interface.decodeFunctionResult( 'getDelegatesForTokens', each, ), ); } } function wrapTokenSymbol(symbol: string): string { if (symbol === 'M20') return 'APE'; if (symbol === 'ATK') return 'BAYC'; if (symbol === 'BTK') return 'MAYC'; if (symbol === 'GTK') return 'BAKC'; if (symbol === 'SewerPass') return 'SEWER'; if (symbol === 'SF-STAKE-VLDR') return 'SF-VLDR'; if (symbol === 'DeGods') return 'DEGODS'; if (symbol === 'SQGL') return 'BLOCKS'; if (symbol === 'NVMTL') return 'HV-MTL'; if (symbol === 'HVMTL') return 'HV-MTL'; return symbol; } // NOTE: keep symbol in upper case const NotUseTotalSupplyAsMaxIdTokens = ['KODA', 'EXP', 'VSL', 'DEGODS']; function useTotalSupplyAsMaxId(symbol: string): boolean { // The `totalSupply` of the following NFTs is not their max token id. return !NotUseTotalSupplyAsMaxIdTokens.includes(symbol.toUpperCase()); } function getViewerStep(symbol: string): number { return NotUseTotalSupplyAsMaxIdTokens.includes(symbol.toUpperCase()) ? 10000 : 3000; }