diff --git a/.gitignore b/.gitignore index f13baffd..90a2af47 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ yarn-error.log* # typescript *.tsbuildinfo +pnpm-lock.yaml diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..d78bf0a5 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v18.20.4 diff --git a/next.config.js b/next.config.js index 369bee80..a4247f9b 100644 --- a/next.config.js +++ b/next.config.js @@ -11,7 +11,7 @@ const nextConfig = { outputStandalone: true, }, images: { - domains: ['realt.co'], + domains: ['realt.co', 'static.debank.com'], }, publicRuntimeConfig: { version, diff --git a/package.json b/package.json index 67014546..65fb65c1 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,11 @@ "prettier:check": "prettier --check src/.", "prettier:format": "prettier --write src/.", "export": "next build && next export", - "docker:local": "docker compose -f docker-compose.local.yml up --force-recreate --build" + "docker:local": "docker compose -f docker-compose.local.yml up --force-recreate --build", + "clean:cache": "del-cli .next", + "clean:mod": "del-cli node_modules", + "clean:all": "npm run clean:cache && npm run clean:mod", + "clean:build": "npm run clean:cache && npm run build" }, "dependencies": { "@apollo/client": "^3.9.5", @@ -27,6 +31,7 @@ "cookies-next": "^2.0.5", "date-fns": "^3.3.1", "dayjs": "^1.11.11", + "del-cli": "^6.0.0", "dexie": "^3.2.5", "ethers": "^6.11.1", "graphql": "^16.6.0", diff --git a/src/components/assetsView/AssetsView.tsx b/src/components/assetsView/AssetsView.tsx index fff92fbc..52825e55 100644 --- a/src/components/assetsView/AssetsView.tsx +++ b/src/components/assetsView/AssetsView.tsx @@ -3,8 +3,11 @@ import { useSelector } from 'react-redux' import { Grid } from '@mantine/core' -import { useRWA } from 'src/hooks/useRWA' -import { selectUserRealtokens } from 'src/store/features/wallets/walletsSelector' +import { selectUserIncludesOtherAssets } from 'src/store/features/settings/settingsSelector' +import { + OtherRealtoken, + UserRealtoken, +} from 'src/store/features/wallets/walletsSelector' import { AssetsViewSearch, useAssetsViewSearch } from './AssetsViewSearch' import { AssetsViewSelect, useAssetsViewSelect } from './assetsViewSelect' @@ -14,20 +17,37 @@ import { RealtimeIndicator } from './indicators/RealtimeIndicator' import { AssetViewType } from './types' import { AssetGrid, AssetTable } from './views' -export const AssetsView: FC = () => { +interface AssetsViewProps { + allAssetsData: (UserRealtoken | OtherRealtoken)[] +} + +export const AssetsView: FC = ({ + allAssetsData: assetsData, +}) => { const { assetsViewFilterFunction } = useAssetsViewFilters() const { assetSearchFunction, assetSearchProps } = useAssetsViewSearch() const { choosenAssetView } = useAssetsViewSelect() + const showOtherAssets = useSelector(selectUserIncludesOtherAssets) - const realtokens = useSelector(selectUserRealtokens) - const rwa = useRWA() + // Check if asset is a UserRealtoken or OtherRealtoken + const isOtherAsset = (asset: UserRealtoken | OtherRealtoken) => { + return !asset.hasOwnProperty('rentStatus') // rely on rentStatus to determine if it's a UserRealtoken + } - const data = useMemo(() => { - const assets = rwa ? [...realtokens, rwa] : realtokens - return assetsViewFilterFunction(assets.filter(assetSearchFunction)) - }, [realtokens, rwa, assetSearchFunction, assetsViewFilterFunction]) + // Apply search and filter functions + const filteredData = useMemo(() => { + // First filter by user advanced filters + const advancedFilteredAssets = assetsViewFilterFunction( + assetsData.filter(assetSearchFunction), + ) + // Then filter out OtherRealtoken + const othersAssetsFiltering = showOtherAssets + ? advancedFilteredAssets + : advancedFilteredAssets.filter((asset) => !isOtherAsset(asset)) + return othersAssetsFiltering + }, [assetsData, assetSearchFunction, assetsViewFilterFunction]) - return realtokens.length ? ( + return assetsData.length ? ( <> { {choosenAssetView == AssetViewType.TABLE && ( - + )} {choosenAssetView == AssetViewType.GRID && ( - + )} ) : null diff --git a/src/components/assetsView/AssetsViewSearch.tsx b/src/components/assetsView/AssetsViewSearch.tsx index 8b6bd419..2427868e 100644 --- a/src/components/assetsView/AssetsViewSearch.tsx +++ b/src/components/assetsView/AssetsViewSearch.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { TextInput } from '@mantine/core' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' @@ -39,11 +39,11 @@ export function useAssetsViewSearch() { [assetSearch], ) - function assetSearchFunction(asset: UserRealtoken | RWARealtoken) { + function assetSearchFunction(asset: UserRealtoken | OtherRealtoken) { return ( !cleanSearch || - asset.shortName.toLowerCase().includes(cleanSearch) || - asset.fullName.toLowerCase().includes(cleanSearch) + asset?.shortName?.toLowerCase().includes(cleanSearch) || + asset?.fullName?.toLowerCase().includes(cleanSearch) ) } diff --git a/src/components/assetsView/filters/AssetsViewRentStatusFilter.tsx b/src/components/assetsView/filters/AssetsViewRentStatusFilter.tsx index 5ac96507..fe905d9b 100644 --- a/src/components/assetsView/filters/AssetsViewRentStatusFilter.tsx +++ b/src/components/assetsView/filters/AssetsViewRentStatusFilter.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { Select } from '@mantine/core' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' @@ -61,7 +61,9 @@ AssetsViewRentStatusFilter.displayName = 'AssetsViewRentStatusFilter' export function useAssetsViewRentStatusFilter( filter: AssetsViewRentStatusFilterModel, ) { - function assetRentStatusFilterFunction(asset: UserRealtoken | RWARealtoken) { + function assetRentStatusFilterFunction( + asset: UserRealtoken | OtherRealtoken, + ) { const Asset = asset as UserRealtoken switch (filter.rentStatus) { case AssetRentStatusType.ALL: diff --git a/src/components/assetsView/filters/AssetsViewRmmStatusFilter.tsx b/src/components/assetsView/filters/AssetsViewRmmStatusFilter.tsx index f73949da..aa68f88f 100644 --- a/src/components/assetsView/filters/AssetsViewRmmStatusFilter.tsx +++ b/src/components/assetsView/filters/AssetsViewRmmStatusFilter.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { Select } from '@mantine/core' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' @@ -56,7 +56,7 @@ AssetsViewRmmStatusFilter.displayName = 'AssetsViewRmmStatusFilter' export function useAssetsViewRmmStatusFilter( filter: AssetsViewRmmStatusFilterModel, ) { - function assetRmmStatusFilterFunction(asset: UserRealtoken | RWARealtoken) { + function assetRmmStatusFilterFunction(asset: UserRealtoken | OtherRealtoken) { const Asset = asset as UserRealtoken switch (filter.rmmStatus) { case AssetRmmStatusType.ALL: diff --git a/src/components/assetsView/filters/AssetsViewSort.tsx b/src/components/assetsView/filters/AssetsViewSort.tsx index fe730516..b4fa4506 100644 --- a/src/components/assetsView/filters/AssetsViewSort.tsx +++ b/src/components/assetsView/filters/AssetsViewSort.tsx @@ -6,7 +6,7 @@ import { Grid, Select, Switch } from '@mantine/core' import { selectTransfersIsLoaded } from 'src/store/features/transfers/transfersSelector' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' @@ -92,51 +92,58 @@ export const AssetsViewSort: FC = ({ } AssetsViewSort.displayName = 'AssetsViewSort' +type MixedRealtoken = Partial & + Partial & + Pick + export function useAssetsViewSort(filter: AssetsViewSortFilter) { function assetSortFunction( - a: UserRealtoken | RWARealtoken, - b: UserRealtoken | RWARealtoken, + a: UserRealtoken | OtherRealtoken, + b: UserRealtoken | OtherRealtoken, ) { const value = getAssetSortValue(a, b) return filter.sortReverse ? value * -1 : value } - function getAssetSortValue( - a: UserRealtoken | RWARealtoken, - b: UserRealtoken | RWARealtoken, - ) { - const A = a as UserRealtoken - const B = b as UserRealtoken + function getAssetSortValue(a: MixedRealtoken, b: MixedRealtoken) { switch (filter.sortBy) { case AssetSortType.VALUE: - return B.value - A.value + return b.value - a.value case AssetSortType.APR: - return B.annualPercentageYield - A.annualPercentageYield + return (b.annualPercentageYield ?? 0) - (a.annualPercentageYield ?? 0) case AssetSortType.RENT: - return B.amount * B.netRentDayPerToken - A.amount * A.netRentDayPerToken + return ( + b.amount * (b.netRentDayPerToken ?? 0) - + a.amount * (a.netRentDayPerToken ?? 0) + ) case AssetSortType.RENT_START: - return B.rentStartDate?.date.localeCompare(A.rentStartDate?.date) + return (b.rentStartDate?.date ?? '').localeCompare( + a.rentStartDate?.date ?? '', + ) case AssetSortType.NAME: - return A.shortName.localeCompare(b.shortName) + return a.shortName.localeCompare(b.shortName) case AssetSortType.SUPPLY: - return B.totalInvestment - A.totalInvestment + return b.totalInvestment - a.totalInvestment case AssetSortType.TOKEN: - return B.amount - A.amount + return b.amount - a.amount case AssetSortType.TOTAL_UNIT: - return B.totalUnits - A.totalUnits + return (b.totalUnits ?? 0) - (a.totalUnits ?? 0) case AssetSortType.RENTED_UNIT: - return B.rentedUnits - A.rentedUnits + return (b.rentedUnits ?? 0) - (a.rentedUnits ?? 0) case AssetSortType.OCCUPANCY: - return B.rentedUnits / B.totalUnits - A.rentedUnits / A.totalUnits + return ( + (b.rentedUnits ?? 0) / (b.totalUnits ?? 1) - + (a.rentedUnits ?? 0) / (a.totalUnits ?? 1) + ) case AssetSortType.INITIAL_LAUNCH: - return B.initialLaunchDate?.date.localeCompare( - A.initialLaunchDate?.date, + return (b.initialLaunchDate?.date ?? '').localeCompare( + a.initialLaunchDate?.date ?? '', ) case AssetSortType.UNIT_PRICE_COST: - return (B.unitPriceCost ?? 0) - (A.unitPriceCost ?? 0) + return (b.unitPriceCost ?? 0) - (a.unitPriceCost ?? 0) case AssetSortType.UNREALIZED_CAPITAL_GAIN: - return (B.unrealizedCapitalGain ?? 0) - (A.unrealizedCapitalGain ?? 0) + return (b.unrealizedCapitalGain ?? 0) - (a.unrealizedCapitalGain ?? 0) case AssetSortType.LAST_CHANGE: - return B.lastChanges.localeCompare(A.lastChanges) ?? 0 + return (b.lastChanges ?? '').localeCompare(a.lastChanges ?? '') } } diff --git a/src/components/assetsView/filters/AssetsViewSubsidyFilter.tsx b/src/components/assetsView/filters/AssetsViewSubsidyFilter.tsx index fb465867..92aed5e6 100644 --- a/src/components/assetsView/filters/AssetsViewSubsidyFilter.tsx +++ b/src/components/assetsView/filters/AssetsViewSubsidyFilter.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { Select } from '@mantine/core' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' @@ -76,23 +76,40 @@ AssetsViewSubsidyFilter.displayName = 'AssetsViewSubsidyFilter' export function useAssetsViewSubsidyFilter( filter: AssetsViewSubsidyFilterModel, ) { - function assetSubsidyFilterFunction(asset: UserRealtoken | RWARealtoken) { + function assetSubsidyFilterFunction(asset: UserRealtoken | OtherRealtoken) { const Asset = asset as UserRealtoken switch (filter.subsidy) { case AssetSubsidyType.ALL: return true case AssetSubsidyType.SUBSIDIZED: - return Asset.subsidyStatus !== 'no' + return Asset.subsidyStatus && Asset.subsidyStatus !== 'no' case AssetSubsidyType.FULLY_SUBSIDIZED: - return Asset.subsidyStatus === 'yes' && !!Asset.subsidyStatusValue + return ( + Asset.subsidyStatus && + Asset.subsidyStatus === 'yes' && + !!Asset.subsidyStatusValue + ) case AssetSubsidyType.PARTIALLY_SUBSIDIZED: - return Asset.subsidyStatus !== 'no' && !!Asset.subsidyStatusValue + return ( + Asset.subsidyStatus && + Asset.subsidyStatus !== 'no' && + !!Asset.subsidyStatusValue + ) case AssetSubsidyType.SECTION_8: - return Asset.subsidyStatus !== 'no' && Asset.subsidyBy === 'Section 8' + return ( + Asset.subsidyStatus && + Asset.subsidyStatus !== 'no' && + Asset.subsidyBy === 'Section 8' + ) case AssetSubsidyType.SECTION_42: - return Asset.subsidyStatus !== 'no' && Asset.subsidyBy === 'Section 42' + return ( + Asset.subsidyStatus && + Asset.subsidyStatus !== 'no' && + Asset.subsidyBy === 'Section 42' + ) case AssetSubsidyType.OTHER_SUBSIDY: return ( + Asset.subsidyStatus && Asset.subsidyStatus !== 'no' && !['Section 8', 'Section 42'].includes(Asset.subsidyBy ?? '') ) diff --git a/src/components/assetsView/filters/AssetsViewUserProtocolFilter.tsx b/src/components/assetsView/filters/AssetsViewUserProtocolFilter.tsx index 8072b35e..533880e5 100644 --- a/src/components/assetsView/filters/AssetsViewUserProtocolFilter.tsx +++ b/src/components/assetsView/filters/AssetsViewUserProtocolFilter.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { Select } from '@mantine/core' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' @@ -66,20 +66,20 @@ export function useAssetsViewUserProtocolFilter( filter: AssetsViewUserProtocolFilterModel, ) { function assetUserProtocolFilterFunction( - asset: UserRealtoken | RWARealtoken, + asset: UserRealtoken | OtherRealtoken, ) { const Asset = asset as UserRealtoken switch (filter.userProtocol) { case AssetUserProtocolType.ALL: return true case AssetUserProtocolType.ETHEREUM: - return Asset.balance.ethereum.amount > 0 + return Asset.balance?.ethereum?.amount > 0 case AssetUserProtocolType.GNOSIS: - return Asset.balance.gnosis.amount > 0 + return Asset.balance?.gnosis?.amount > 0 case AssetUserProtocolType.RMM: - return Asset.balance.rmm.amount > 0 + return Asset.balance?.rmm?.amount > 0 case AssetUserProtocolType.LEVINSWAP: - return Asset.balance.levinSwap.amount > 0 + return Asset.balance?.levinSwap?.amount > 0 } } diff --git a/src/components/assetsView/filters/AssetsViewUserStatusFilter.tsx b/src/components/assetsView/filters/AssetsViewUserStatusFilter.tsx index 635ff06f..fd30fcf1 100644 --- a/src/components/assetsView/filters/AssetsViewUserStatusFilter.tsx +++ b/src/components/assetsView/filters/AssetsViewUserStatusFilter.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { Select } from '@mantine/core' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' @@ -69,7 +69,9 @@ AssetsViewUserStatusFilter.displayName = 'AssetsViewUserStatusFilter' export function useAssetsViewUserStatusFilter( filter: AssetsViewUserStatusFilterModel, ) { - function assetUserStatusFilterFunction(asset: UserRealtoken | RWARealtoken) { + function assetUserStatusFilterFunction( + asset: UserRealtoken | OtherRealtoken, + ) { const Asset = asset as UserRealtoken switch (filter.userStatus) { case AssetUserStatusType.ALL: diff --git a/src/components/assetsView/filters/useFilters.ts b/src/components/assetsView/filters/useFilters.ts index dae6dc0a..124f638c 100644 --- a/src/components/assetsView/filters/useFilters.ts +++ b/src/components/assetsView/filters/useFilters.ts @@ -2,7 +2,7 @@ import { useAtom } from 'jotai' import { assetsViewDefaultFilter, assetsViewFilterAtom } from 'src/states' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' @@ -30,7 +30,7 @@ export function useAssetsViewFilters() { useAssetsViewUserProtocolFilter(activeFilter) function assetsViewFilterFunction( - tokenList: (UserRealtoken | RWARealtoken)[], + tokenList: (UserRealtoken | OtherRealtoken)[], ) { return tokenList .filter(assetUserStatusFilterFunction) diff --git a/src/components/assetsView/views/AssetGrid.tsx b/src/components/assetsView/views/AssetGrid.tsx index 167d1240..ca08e038 100644 --- a/src/components/assetsView/views/AssetGrid.tsx +++ b/src/components/assetsView/views/AssetGrid.tsx @@ -1,8 +1,10 @@ import { FC, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' import { useRouter } from 'next/router' import { + CheckIcon, Combobox, Grid, Group, @@ -13,18 +15,20 @@ import { import FullyRentedAPRDisclaimer from 'src/components/commons/others/FullyRentedAPRDisclaimer' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' import { AssetCard } from '../../cards' -export const AssetGrid: FC<{ realtokens: (UserRealtoken | RWARealtoken)[] }> = ( - props, -) => { +export const AssetGrid: FC<{ + realtokens: (UserRealtoken | OtherRealtoken)[] +}> = (props) => { const router = useRouter() + const { t } = useTranslation('common', { keyPrefix: 'assetView' }) const [page, setPage] = useState(1) - const [pageSize, setPageSize] = useState(20) + const defaultPageSize = 20 + const [pageSize, setPageSize] = useState(defaultPageSize) function onPageChange(page: number) { setPage(page) @@ -32,8 +36,8 @@ export const AssetGrid: FC<{ realtokens: (UserRealtoken | RWARealtoken)[] }> = ( document.getElementsByClassName('asset-grid')[0]?.scrollIntoView() } - const paginationOffers: (UserRealtoken | RWARealtoken)[] = useMemo(() => { - if (pageSize === Infinity) return props.realtokens + const paginationOffers: (UserRealtoken | OtherRealtoken)[] = useMemo(() => { + if (!pageSize) return props.realtokens const start = (page - 1) * pageSize const end = start + pageSize return props.realtokens.slice(start, end) @@ -46,15 +50,20 @@ export const AssetGrid: FC<{ realtokens: (UserRealtoken | RWARealtoken)[] }> = ( onDropdownClose: () => combobox.resetSelectedOption(), }) - const values = [20, 40, 100, 200] + // 20, 40, 100, 200 + const values = [ + 0, // All + defaultPageSize, + defaultPageSize * 2, + defaultPageSize * 5, + defaultPageSize * 10, + ] const options = [ - - {'All'} - , - ...values.map((item) => ( + values.map((item) => ( - {item} + {item === pageSize && }  + {item ? item?.toString() : t('paging.all')} )), ] @@ -62,14 +71,27 @@ export const AssetGrid: FC<{ realtokens: (UserRealtoken | RWARealtoken)[] }> = ( return ( <> - {paginationOffers.map((item) => ( - - router.push(`/asset/${id}`)} - /> - - ))} + {paginationOffers.map((item) => { + const isAProperty = item.hasOwnProperty('rentStatus') + if (!isAProperty) { + return ( + + router.push(`/asset/${id}`)} + /> + + ) + } + return ( + + router.push(`/asset/${id}`)} + /> + + ) + })} = ( > = ( store={combobox} withinPortal={false} onOptionSubmit={(val) => { - if (val === 'All') return setPageSize(Infinity) setPageSize(Number(val)) - combobox.closeDropdown() }} > } - value={pageSize == Infinity ? 'All' : pageSize} + value={pageSize ? pageSize.toString() : t('paging.all')} type={'button'} onChange={() => { combobox.openDropdown() @@ -114,7 +130,7 @@ export const AssetGrid: FC<{ realtokens: (UserRealtoken | RWARealtoken)[] }> = ( onBlur={() => { combobox.closeDropdown() }} - placeholder={'Search value'} + placeholder={t('paging.placeholder')} rightSectionPointerEvents={'none'} /> diff --git a/src/components/assetsView/views/AssetTable.tsx b/src/components/assetsView/views/AssetTable.tsx index 3efd274f..546055d9 100644 --- a/src/components/assetsView/views/AssetTable.tsx +++ b/src/components/assetsView/views/AssetTable.tsx @@ -12,12 +12,12 @@ import { useCurrencyValue } from 'src/hooks/useCurrencyValue' import { useFullyRentedAPR } from 'src/hooks/useFullyRentedAPR' import { selectTransfersIsLoaded } from 'src/store/features/transfers/transfersSelector' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' export const AssetTable: FC<{ - realtokens: (UserRealtoken | RWARealtoken)[] + realtokens: (UserRealtoken | OtherRealtoken)[] }> = (props) => { return ( @@ -30,7 +30,12 @@ export const AssetTable: FC<{ {props.realtokens.map((item, index) => { const isAProperty = item.hasOwnProperty('rentStatus') if (!isAProperty) { - return + return ( + + ) } return })} @@ -160,7 +165,7 @@ const AssetTableRow: FC<{ value: UserRealtoken }> = (props) => { ) } -const RWATableRow: FC<{ value: RWARealtoken }> = (props) => { +const OtherTableRow: FC<{ value: OtherRealtoken }> = (props) => { const { t } = useTranslation('common', { keyPrefix: 'numbers' }) const transfersIsLoaded = useSelector(selectTransfersIsLoaded) diff --git a/src/components/cards/AssetCard.tsx b/src/components/cards/AssetCard.tsx index 6a659a84..24fb609c 100644 --- a/src/components/cards/AssetCard.tsx +++ b/src/components/cards/AssetCard.tsx @@ -12,7 +12,7 @@ import { useCurrencyValue } from 'src/hooks/useCurrencyValue' import { useFullyRentedAPR } from 'src/hooks/useFullyRentedAPR' import { selectUserRentCalculation } from 'src/store/features/settings/settingsSelector' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' import { RentCalculationState } from 'src/types/RentCalculation' @@ -27,7 +27,7 @@ import styles from './AssetCard.module.sass' import { RWACard } from './RWACard' interface AssetCardProps { - value: UserRealtoken | RWARealtoken + value: UserRealtoken | OtherRealtoken onClick?: (id: string) => unknown } @@ -163,7 +163,10 @@ const PropertyCardComponent: FC = (props) => {
-
{t('fullyRentedEstimation')}*
+
+ {t('fullyRentedEstimation')} + {'*'} +
{fullyRentedAPR ? tNumbers('percent', { value: fullyRentedAPR }) @@ -189,6 +192,6 @@ export const AssetCard: FC = (props) => { /> ) } else { - return + return } } diff --git a/src/components/cards/RWACard.tsx b/src/components/cards/RWACard.tsx index 2367578b..5f463478 100644 --- a/src/components/cards/RWACard.tsx +++ b/src/components/cards/RWACard.tsx @@ -6,13 +6,13 @@ import Image from 'next/image' import { Badge, Card, Group } from '@mantine/core' import { useCurrencyValue } from 'src/hooks/useCurrencyValue' -import { RWARealtoken } from 'src/store/features/wallets/walletsSelector' +import { OtherRealtoken } from 'src/store/features/wallets/walletsSelector' import { Divider } from '../commons' import styles from './AssetCard.module.sass' interface RWACardProps { - value: RWARealtoken + value: OtherRealtoken onClick?: (id: string) => unknown } @@ -60,6 +60,13 @@ const RWACardComponent: FC = (props) => {
+
+
{t('tokenPrice')}
+
+ {useCurrencyValue(props.value.tokenPrice)} +
+
+
{t('propertyValue')}
{useCurrencyValue(totalInvestment)}
diff --git a/src/components/cards/main/SummaryCard.tsx b/src/components/cards/main/SummaryCard.tsx index a500d048..6f1c97e2 100644 --- a/src/components/cards/main/SummaryCard.tsx +++ b/src/components/cards/main/SummaryCard.tsx @@ -3,30 +3,48 @@ import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { Box, Card, Text, Title } from '@mantine/core' +import { IconArchive, IconBolt, IconBoltOff } from '@tabler/icons' -import { useRWA } from 'src/hooks/useRWA' import { selectTransfersIsLoaded } from 'src/store/features/transfers/transfersSelector' +import { OtherRealtoken } from 'src/store/features/wallets/walletsSelector' import { selectOwnedRealtokensValue, selectRmmDetails, } from 'src/store/features/wallets/walletsSelector' -import { CurrencyField } from '../../commons' +import { CurrencyField, DecimalField } from '../../commons' -export const SummaryCard: FC = () => { +interface SummaryCardProps { + otherAssetsData: { + rwa: OtherRealtoken | null + reg: OtherRealtoken | null + regVotingPower: OtherRealtoken | null + } +} +export const SummaryCard: FC = ({ otherAssetsData }) => { const { t } = useTranslation('common', { keyPrefix: 'summaryCard' }) const realtokensValue = useSelector(selectOwnedRealtokensValue) const rmmDetails = useSelector(selectRmmDetails) const transfersIsLoaded = useSelector(selectTransfersIsLoaded) - const rwa = useRWA() - const stableDepositValue = rmmDetails.stableDeposit const stableDebtValue = rmmDetails.stableDebt - const rwaValue = rwa?.value ?? 0 + + const rwaValue = otherAssetsData?.rwa?.value ?? 0 + const regValue = otherAssetsData?.reg?.value ?? 0 + const regVotingPowerAmount = otherAssetsData?.regVotingPower?.amount ?? 0 + // Calculate the power logo size of the voting power depending on the amount + const additionnalPowerSize = Math.floor(Math.log10(regVotingPowerAmount)) + const iconPowerSize = 20 + additionnalPowerSize + // Change the fill color of the bolt icon based on the power size (filled if > 3 = > 1000) + const iconPowerFillColor = additionnalPowerSize > 3 ? 'orange' : 'none' const totalNetValue = - realtokensValue.total + stableDepositValue + rwaValue - stableDebtValue + realtokensValue.total + + stableDepositValue + + rwaValue + + regValue - + stableDebtValue return ( @@ -48,6 +66,23 @@ export const SummaryCard: FC = () => { + + } + value={regVotingPowerAmount} + unitIcon={ + regVotingPowerAmount > 0 ? ( + + ) : ( + + ) + } + /> ) diff --git a/src/components/commons/fields/DecimalField.tsx b/src/components/commons/fields/DecimalField.tsx index 16458a47..77f63248 100644 --- a/src/components/commons/fields/DecimalField.tsx +++ b/src/components/commons/fields/DecimalField.tsx @@ -5,18 +5,29 @@ import { StringField } from './StringField' interface DecimalFieldProps { label: string + labelIcon?: React.ReactNode value: number + prefix?: string suffix?: string + unitIcon?: React.ReactNode } export const DecimalField: FC = (props) => { const { t } = useTranslation('common', { keyPrefix: 'numbers' }) return ( - + <> + + ) } DecimalField.displayName = 'DecimalField' diff --git a/src/components/commons/fields/StringField.tsx b/src/components/commons/fields/StringField.tsx index 2551c4b4..9ac4c55a 100644 --- a/src/components/commons/fields/StringField.tsx +++ b/src/components/commons/fields/StringField.tsx @@ -1,21 +1,51 @@ import { FC } from 'react' import { useSelector } from 'react-redux' -import { Box, Group, Skeleton } from '@mantine/core' +import { Box, Flex, Group, Skeleton } from '@mantine/core' import { selectIsLoading } from 'src/store/features/settings/settingsSelector' -export const StringField: FC<{ label: string; value: string }> = (props) => { +export const StringField: FC<{ + label: string + labelIcon?: React.ReactNode + value: string + unitIcon?: React.ReactNode +}> = (props) => { const isLoading = useSelector(selectIsLoading) return ( -
{props.label}
+ +
{props.label}
+ {props.labelIcon && ( + + {props.labelIcon} + + )} +
{isLoading ? ( ) : ( - {props.value} + + {props.value} + + {props.unitIcon && {props.unitIcon}} + )}
) diff --git a/src/components/commons/others/FullyRentedAPRDisclaimer.tsx b/src/components/commons/others/FullyRentedAPRDisclaimer.tsx index 8a977cd7..6722b5dd 100644 --- a/src/components/commons/others/FullyRentedAPRDisclaimer.tsx +++ b/src/components/commons/others/FullyRentedAPRDisclaimer.tsx @@ -6,7 +6,8 @@ const FullyRentedAPRDisclaimer = () => { const { t } = useTranslation('common', { keyPrefix: 'disclaimer' }) return ( - *{t('fullyRentedAPR')} + {'*'} + {t('fullyRentedAPR')} ) } diff --git a/src/components/layouts/SettingsMenu.tsx b/src/components/layouts/SettingsMenu.tsx index d086861e..5deba784 100644 --- a/src/components/layouts/SettingsMenu.tsx +++ b/src/components/layouts/SettingsMenu.tsx @@ -17,9 +17,16 @@ import { import { DatePickerInput } from '@mantine/dates' import { useDisclosure } from '@mantine/hooks' import { + IconBuildingBank, IconCash, + IconCircleOff, IconClock, - IconClockOff, + IconCoins, + IconCrystalBall, + IconCurrencyEthereum, + IconDatabase, + IconDatabaseOff, + IconHome, IconLanguage, IconMoon, IconSettings, @@ -33,6 +40,7 @@ import { selectUserCurrency, selectUserIncludesEth, selectUserIncludesLevinSwap, + selectUserIncludesOtherAssets, selectUserIncludesRmmV2, selectUserRentCalculation, selectVersion, @@ -41,6 +49,7 @@ import { userCurrencyChanged, userIncludesEthChanged, userIncludesLevinSwapChanged, + userIncludesOtherAssetsChanged, userIncludesRmmV2Changed, userRentCalculationChanged, } from 'src/store/features/settings/settingsSlice' @@ -58,6 +67,7 @@ const ColorSchemeMenuItem: FC = () => { return ( + {t('theme')} { return ( + {t('rents')} { value: RentCalculationState.Global, label: (
- + {t('global')}
), @@ -244,6 +255,7 @@ const FetchDataSettings: FC = () => { const userIncludesEth = useSelector(selectUserIncludesEth) const userIncludesLevinSwap = useSelector(selectUserIncludesLevinSwap) const userIncludesRmmV2 = useSelector(selectUserIncludesRmmV2) + const userIncludesOtherAssets = useSelector(selectUserIncludesOtherAssets) const setUserIncludesEth = (value: boolean) => dispatch(userIncludesEthChanged(value)) @@ -251,13 +263,18 @@ const FetchDataSettings: FC = () => { dispatch(userIncludesLevinSwapChanged(value)) const setUserIncludesRmmV2 = (value: boolean) => dispatch(userIncludesRmmV2Changed(value)) + const setUserIncludesOtherAssets = (value: boolean) => + dispatch(userIncludesOtherAssetsChanged(value)) return ( <> + {t('options')} setUserIncludesEth(event.currentTarget.checked)} label={t('includesEth')} + onLabel={} + offLabel={} style={{ margin: '4px 8px' }} /> { setUserIncludesLevinSwap(event.currentTarget.checked) } label={t('includesLevinSwap')} + onLabel={} + offLabel={} style={{ margin: '4px 8px' }} /> setUserIncludesRmmV2(event.currentTarget.checked)} + onLabel={} + offLabel={} label={t('includesRmmV2')} style={{ margin: '4px 8px' }} /> + + setUserIncludesOtherAssets(event.currentTarget.checked) + } + label={t('includesOtherAssets')} + onLabel={} + offLabel={} + style={{ margin: '4px 8px' }} + /> ) } diff --git a/src/hooks/useInitStore.ts b/src/hooks/useInitStore.ts index 1c51e48a..041019d4 100644 --- a/src/hooks/useInitStore.ts +++ b/src/hooks/useInitStore.ts @@ -15,7 +15,7 @@ import { setUserAddress, } from 'src/store/features/settings/settingsSlice' import { - fetchTransfers, + // fetchTransfers, // unused resetTransfers, } from 'src/store/features/transfers/transfersSlice' import { diff --git a/src/hooks/useREG.ts b/src/hooks/useREG.ts new file mode 100644 index 00000000..a8631b69 --- /dev/null +++ b/src/hooks/useREG.ts @@ -0,0 +1,177 @@ +import { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' + +import { Contract } from 'ethers' + +import { initializeProviders } from 'src/repositories/RpcProvider' +import { + selectCurrencyRates, + selectUserCurrency, +} from 'src/store/features/currencies/currenciesSelector' +import { + selectUserAddressList, + selectUserIncludesEth, +} from 'src/store/features/settings/settingsSelector' +import { REGRealtoken } from 'src/store/features/wallets/walletsSelector' +import { Currency } from 'src/types/Currencies' +import { ERC20ABI } from 'src/utils/blockchain/abi/ERC20ABI' +import { + DEFAULT_REG_PRICE, + DEFAULT_USDC_USD_RATE, + DEFAULT_XDAI_USD_RATE, + HoneySwapFactory_Address, + REG_ContractAddress, + REG_Vault_Gnosis_ContractAddress, + REG_asset_ID, + REGtokenDecimals, + USDConXdai_ContractAddress, + USDCtokenDecimals, + WXDAI_ContractAddress, + WXDAItokenDecimals, +} from 'src/utils/blockchain/consts/otherTokens' +import { getAddressesBalances } from 'src/utils/blockchain/erc20Infos' +import { + averageValues, + getUniV2AssetPrice, +} from 'src/utils/blockchain/poolPrice' +import { + getAddressesLockedBalances, + getRegVaultAbiGetUserGlobalStateOnly, +} from 'src/utils/blockchain/regVault' + +/** + * + * @param addressList : user addresses list + * @param userRate : user selected currency rate + * @param currenciesRates : currencies rates + * @param includeETH : include balances on ETH in the calculation + * @returns + */ +const getREG = async ( + addressList: string[], + userRate: number, + currenciesRates: Record, + includeETH = false, +): Promise => { + const { GnosisRpcProvider, EthereumRpcProvider } = await initializeProviders() + const providers = [GnosisRpcProvider] + if (includeETH) { + providers.push(EthereumRpcProvider) + } + const RegContract_Gnosis = new Contract( + REG_ContractAddress, + ERC20ABI, + GnosisRpcProvider, + ) + const availableBalance = await getAddressesBalances( + REG_ContractAddress, + addressList, + providers, + ) + + const regVaultAbiGetUserGlobalStateOnly = + getRegVaultAbiGetUserGlobalStateOnly() + + const lockedBalance = await getAddressesLockedBalances( + [ + // First provider + [ + // First vault + [ + REG_Vault_Gnosis_ContractAddress, // Contract address + regVaultAbiGetUserGlobalStateOnly, // Contract ABI + 'getUserGlobalState', // Contract method for getting balance + ], + // Second vault ... + ], + /* + // Second provider + [ + // First vault ... + [ + // Contract address + // Contract ABI + // Contract method for getting balance + ], + ], + // ... + */ + ], + addressList, + providers, + ) + + const totalAmount = availableBalance + lockedBalance + const contractRegTotalSupply = await RegContract_Gnosis.totalSupply() + const totalTokens = Number(contractRegTotalSupply) / 10 ** REGtokenDecimals + const amount = totalAmount / 10 ** REGtokenDecimals + + // Get REG token prices in USDC and WXDAI from LPs + const regPriceUsdc = await getUniV2AssetPrice( + HoneySwapFactory_Address, + REG_ContractAddress, + USDConXdai_ContractAddress, + REGtokenDecimals, + USDCtokenDecimals, + GnosisRpcProvider, + ) + const regPriceWxdai = await getUniV2AssetPrice( + HoneySwapFactory_Address, + REG_ContractAddress, + WXDAI_ContractAddress, + REGtokenDecimals, + WXDAItokenDecimals, + GnosisRpcProvider, + ) + + // Get rates for XDAI and USDC against USD + const rateXdaiUsd = currenciesRates?.XDAI + ? currenciesRates.XDAI + : DEFAULT_XDAI_USD_RATE + const rateUsdcUsd = currenciesRates?.USDC + ? currenciesRates.USDC + : DEFAULT_USDC_USD_RATE + // Convert token prices to USD + const assetPriceUsd1 = regPriceUsdc ? regPriceUsdc * rateUsdcUsd : null + const assetPriceUsd2 = regPriceWxdai ? regPriceWxdai * rateXdaiUsd : null + // Get average token prices in USD + const assetAveragePriceUSD = averageValues([assetPriceUsd1, assetPriceUsd2]) + // Convert prices in Currency by applying rate + const tokenPrice = assetAveragePriceUSD + ? assetAveragePriceUSD / userRate + : DEFAULT_REG_PRICE / userRate + const value = tokenPrice * amount + const totalInvestment = totalTokens * tokenPrice + + return { + id: `${REG_asset_ID}`, + fullName: 'RealToken Ecosystem Governance', + shortName: 'REG', + amount, + tokenPrice, + totalTokens, + imageLink: [ + 'https://static.debank.com/image/xdai_token/logo_url/0x0aa1e96d2a46ec6beb2923de1e61addf5f5f1dce/c56091d1d22e34e5e77aed0c64d19338.png', + ], + isRmmAvailable: false, + value, + totalInvestment, + unitPriceCost: tokenPrice, + } +} + +export const useREG = () => { + const [reg, setReg] = useState(null) + const addressList = useSelector(selectUserAddressList) + const { rate: userRate } = useSelector(selectUserCurrency) + const includeETH = useSelector(selectUserIncludesEth) + const currenciesRates = useSelector(selectCurrencyRates) + + useEffect(() => { + if (addressList.length) { + getREG(addressList, userRate, currenciesRates, includeETH).then(setReg) + } + }, [addressList, userRate, currenciesRates, includeETH]) + + return reg +} diff --git a/src/hooks/useREGVotingPower.ts b/src/hooks/useREGVotingPower.ts new file mode 100644 index 00000000..6f9a4098 --- /dev/null +++ b/src/hooks/useREGVotingPower.ts @@ -0,0 +1,72 @@ +import { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' + +import { Contract } from 'ethers' + +import { initializeProviders } from 'src/repositories/RpcProvider' +import { selectUserAddressList } from 'src/store/features/settings/settingsSelector' +import { REGVotingPowertoken } from 'src/store/features/wallets/walletsSelector' +import { ERC20ABI } from 'src/utils/blockchain/abi/ERC20ABI' +import { + DEFAULT_REGVotingPower_PRICE, + REGVotingPower_asset_ID, + REGVotingPowertokenDecimals, + RegVotingPower_Gnosis_ContractAddress, +} from 'src/utils/blockchain/consts/otherTokens' +import { getAddressesBalances } from 'src/utils/blockchain/erc20Infos' + +const getRegVotingPower = async ( + addressList: string[], +): Promise => { + const { GnosisRpcProvider } = await initializeProviders() + const providers = [GnosisRpcProvider] + const RegVotingPowerContract = new Contract( + RegVotingPower_Gnosis_ContractAddress, + ERC20ABI, + GnosisRpcProvider, + ) + const totalAmount = await getAddressesBalances( + RegVotingPower_Gnosis_ContractAddress, + addressList, + providers, + ) + + const contractRegVotePowerTotalSupply = + await RegVotingPowerContract.totalSupply() + const totalTokens = + Number(contractRegVotePowerTotalSupply) / 10 ** REGVotingPowertokenDecimals + const amount = totalAmount / 10 ** REGVotingPowertokenDecimals + const tokenPrice = DEFAULT_REGVotingPower_PRICE + const value = tokenPrice * amount + const totalInvestment = tokenPrice * totalTokens + + return { + id: `${REGVotingPower_asset_ID}`, + fullName: 'REG Voting Power Registry', + shortName: 'REG VOTING POWER', + amount, + tokenPrice, + totalTokens, + imageLink: [ + 'https://static.debank.com/image/xdai_token/logo_url/0x0aa1e96d2a46ec6beb2923de1e61addf5f5f1dce/c56091d1d22e34e5e77aed0c64d19338.png', + ], + isRmmAvailable: false, + value, + totalInvestment, + unitPriceCost: tokenPrice, + } +} + +export const useRegVotingPower = () => { + const [regVotingPower, setRegVotingPower] = + useState(null) + const addressList = useSelector(selectUserAddressList) + + useEffect(() => { + if (addressList.length) { + getRegVotingPower(addressList).then(setRegVotingPower) + } + }, [addressList]) + + return regVotingPower +} diff --git a/src/hooks/useRWA.ts b/src/hooks/useRWA.ts index 076d7101..16eef6d8 100644 --- a/src/hooks/useRWA.ts +++ b/src/hooks/useRWA.ts @@ -1,57 +1,103 @@ import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' -import { ethers } from 'ethers' +import { Contract } from 'ethers' import { initializeProviders } from 'src/repositories/RpcProvider' -import { selectUserCurrency } from 'src/store/features/currencies/currenciesSelector' import { - selectUserAddressList, - selectUserIncludesEth, + selectCurrencyRates, + selectUserCurrency, +} from 'src/store/features/currencies/currenciesSelector' +import { + selectUserAddressList, // selectUserIncludesEth, } from 'src/store/features/settings/settingsSelector' import { RWARealtoken } from 'src/store/features/wallets/walletsSelector' - -const tokenDecimals = 9 +import { Currency } from 'src/types/Currencies' +import { ERC20ABI } from 'src/utils/blockchain/abi/ERC20ABI' +import { + DEFAULT_RWA_PRICE, + DEFAULT_USDC_USD_RATE, + DEFAULT_XDAI_USD_RATE, + HoneySwapFactory_Address, + RWA_ContractAddress, + RWA_asset_ID, + RWAtokenDecimals, + USDConXdai_ContractAddress, + USDCtokenDecimals, + WXDAI_ContractAddress, + WXDAItokenDecimals, +} from 'src/utils/blockchain/consts/otherTokens' +import { getAddressesBalances } from 'src/utils/blockchain/erc20Infos' +import { + averageValues, + getUniV2AssetPrice, +} from 'src/utils/blockchain/poolPrice' const getRWA = async ( addressList: string[], - rate: number, - includeETH: boolean = false, + userRate: number, + currenciesRates: Record, + // includeETH = false, ): Promise => { - let totalAmount = 0 - const { GnosisRpcProvider, EthereumRpcProvider } = await initializeProviders() + const { GnosisRpcProvider /* , EthereumRpcProvider */ } = + await initializeProviders() + const providers = [GnosisRpcProvider] - let providers = [GnosisRpcProvider] + const contractRwa_Gnosis = new Contract( + RWA_ContractAddress, + ERC20ABI, + GnosisRpcProvider, + ) + const totalAmount = await getAddressesBalances( + RWA_ContractAddress, + addressList, + providers, + ) + const RwaContractTotalSupply = await contractRwa_Gnosis.totalSupply() + const totalTokens = Number(RwaContractTotalSupply) / 10 ** RWAtokenDecimals + const amount = totalAmount / 10 ** RWAtokenDecimals - if (includeETH) { - providers.push(EthereumRpcProvider) - } + // RWA token prices in USDC and WXDAI from LPs + const rwaPriceUsdc = await getUniV2AssetPrice( + HoneySwapFactory_Address, + RWA_ContractAddress, + USDConXdai_ContractAddress, + RWAtokenDecimals, + USDCtokenDecimals, + GnosisRpcProvider, + ) + const rwaPriceWxdai = await getUniV2AssetPrice( + HoneySwapFactory_Address, + RWA_ContractAddress, + WXDAI_ContractAddress, + RWAtokenDecimals, + WXDAItokenDecimals, + GnosisRpcProvider, + ) - for (let i = 0; i < addressList.length; i++) { - for (let j = 0; j < providers.length; j++) { - const RPCProvider = providers[j] - const RWAContract = new ethers.Contract( - '0x0675e8F4A52eA6c845CB6427Af03616a2af42170', - ['function balanceOf(address) view returns (uint)'], - RPCProvider, - ) - const RWAContractBalance = await RWAContract.balanceOf(addressList[i]) - totalAmount += Number(RWAContractBalance) - } - } - - const totalTokens = 100_000 - const amount = totalAmount / 10 ** tokenDecimals - const unitPriceCost = 50 / rate - - const value = unitPriceCost * amount - const totalInvestment = totalTokens * unitPriceCost + // Get rates for XDAI and USDC against USD + const rateXdaiUsd = currenciesRates?.XDAI + ? currenciesRates.XDAI + : DEFAULT_XDAI_USD_RATE + const rateUsdcUsd = currenciesRates?.USDC + ? currenciesRates.USDC + : DEFAULT_USDC_USD_RATE + // Convert token prices to USD + const assetPriceUsd1 = rwaPriceUsdc ? rwaPriceUsdc * rateUsdcUsd : null + const assetPriceUsd2 = rwaPriceWxdai ? rwaPriceWxdai * rateXdaiUsd : null + // Get average token price in USD + const assetAveragePriceUSD = averageValues([assetPriceUsd1, assetPriceUsd2]) + // Convert price in Currency by applying rate + const tokenPrice = (assetAveragePriceUSD ?? DEFAULT_RWA_PRICE) / userRate + const value = tokenPrice * amount + const totalInvestment = totalTokens * tokenPrice return { - id: '0', + id: `${RWA_asset_ID}`, fullName: 'RWA Holdings SA, Neuchatel, NE, Suisse', shortName: 'RWA', amount, + tokenPrice, totalTokens, imageLink: [ 'https://realt.co/wp-content/uploads/2024/02/Equity_FinalDesign-2000px-800x542.png', @@ -59,24 +105,26 @@ const getRWA = async ( isRmmAvailable: false, value, totalInvestment, - unitPriceCost, + unitPriceCost: tokenPrice, + initialLaunchDate: { + date: '2024-03-21 00:00:00.000000', + timezone_type: 3, + timezone: 'UTC', + }, } } export const useRWA = () => { const [rwa, setRwa] = useState(null) const addressList = useSelector(selectUserAddressList) - - const { rate } = useSelector(selectUserCurrency) - const includeETH = useSelector(selectUserIncludesEth) - + const { rate: userRate } = useSelector(selectUserCurrency) + const currenciesRates = useSelector(selectCurrencyRates) + // const includeETH = useSelector(selectUserIncludesEth) // useless: RWA does not (yet ?) exist on Ethereum useEffect(() => { - ;(async () => { - const rwa_ = await getRWA(addressList, rate, includeETH) - - setRwa(rwa_) - })() - }, [addressList]) + if (addressList.length) { + getRWA(addressList, userRate, currenciesRates).then(setRwa) + } + }, [addressList, userRate, currenciesRates]) return rwa } diff --git a/src/i18next/locales/en/common.json b/src/i18next/locales/en/common.json index 3f7f7554..6e34721e 100644 --- a/src/i18next/locales/en/common.json +++ b/src/i18next/locales/en/common.json @@ -19,16 +19,20 @@ "usd": "USD ($)", "eur": "Euro (€)", "chf": "Swiss franc (CHF)", + "theme": "Theme", "light": "Light", "dark": "Dark", "refreshDataButton": "Refresh data", + "rents": "Rents calculation", "realtime": "Realtime", "global": "Global", "date": "Date", "dateFormat": "MMMM DD, YYYY", + "options": "Options", "includesEth": "Includes Ethereum", "includesLevinSwap": "Includes LevinSwap", - "includesRmmV2": "Includes RMM V2" + "includesRmmV2": "Includes RMM V2", + "includesOtherAssets": "Includes other assets" }, "walletButton": { "connectWallet": "Connect wallet", @@ -71,7 +75,9 @@ "totalPriceCost": "Estimated price cost", "stableDeposit": "RMM deposit", "stableBorrow": "RMM borrow", - "rwa": "RWA" + "rwa": "RWA", + "reg": "REG", + "regVote": "REG Vote Power" }, "worthCard": { "title": "RealTokens", @@ -125,6 +131,10 @@ "viewOptions": { "table": "Table", "grid": "Grid" + }, + "paging": { + "placeholder": "Search value", + "all": "All" } }, "assetsViewFilterButton": { @@ -192,6 +202,7 @@ "weekly": "Weekly rents", "yearly": "Yearly rents", "rentedUnits": "Rented units", + "tokenPrice": "Token price", "propertyValue": "Property value", "rentStartDate": "Rent Start", "fullyRentedEstimation": "Fully rented APR", @@ -381,6 +392,13 @@ "yamStatisticsPage": { "home": "Home", "title": "Secondary market statistics (Yam)", + "columns": { + "token": "Token", + "tokenPrice": "RealT price", + "yamPrice": "Yam price (30 days)", + "yamDifference": "Difference", + "yamVolume": "Volume (30 days)" + }, "filter": { "field": "Filter", "all": "All", @@ -396,5 +414,8 @@ }, "disclaimer":{ "fullyRentedAPR": "This is a beta estimation done by RealT community. Please report any issues. Please note that is an indicative value and not a guarantee. RealT community or RealT does not take any responsibility for user actions based on this value." + }, + "errors" : { + "userNotFound": "User not found" } } diff --git a/src/i18next/locales/fr/common.json b/src/i18next/locales/fr/common.json index e05985a7..726f3c0b 100644 --- a/src/i18next/locales/fr/common.json +++ b/src/i18next/locales/fr/common.json @@ -19,16 +19,20 @@ "usd": "USD ($)", "eur": "Euro (€)", "chf": "Franc suisse (CHF)", + "theme": "Thème", "light": "Clair", "dark": "Sombre", "refreshDataButton": "Actualiser les données", + "rents": "Mode de calcul des loyers", "realtime": "Temps réel", "global": "Global", "date": "Date", "dateFormat": "DD MMMM YYYY", + "options": "Options", "includesEth": "Inclure Ethereum", "includesLevinSwap": "Inclure LevinSwap", - "includesRmmV2": "Inclure RMM V2" + "includesRmmV2": "Inclure RMM V2", + "includesOtherAssets": "Inclure d'autres actifs" }, "walletButton": { "connectWallet": "Connecter mon portefeuille", @@ -71,7 +75,9 @@ "totalPriceCost": "Prix d'achat estimé", "stableDeposit": "Dépôt RMM", "stableBorrow": "Emprunt RMM", - "rwa": "RWA" + "rwa": "RWA", + "reg": "REG", + "regVote": "Pouvoir de vote" }, "worthCard": { "title": "RealTokens", @@ -125,6 +131,10 @@ "viewOptions": { "table": "Tableau", "grid": "Carte" + }, + "paging": { + "placeholder": "Search value", + "all": "Tous" } }, "assetsViewFilterButton": { @@ -192,6 +202,7 @@ "weekly": "Loyers hebdomadaires", "yearly": "Loyers annuels", "rentedUnits": "Logements loués", + "tokenPrice":"Prix d'un token", "propertyValue": "Valeur de la propriété", "rentStartDate": "Date du premier loyer", "fullyRentedEstimation": "Rendement 100% loué", @@ -382,6 +393,13 @@ "yamStatisticsPage": { "home": "Accueil", "title": "Statistiques marché secondaire (Yam)", + "columns": { + "token": "Token", + "tokenPrice": "Prix RealT", + "yamPrice": "Prix Yam (30 jours)", + "yamDifference": "Différence", + "yamVolume": "Volume (30 jours)" + }, "filter": { "field": "Filtre", "all": "Toutes les propriétés", @@ -398,4 +416,8 @@ "disclaimer":{ "fullyRentedAPR": "Cette estimation est en phase bêta et a été développée par la communauté RealT. Nous vous invitons à signaler tout problème éventuel. Les informations fournies sont à titre indicatif uniquement. La communauté RealT ou RealT ne peut être tenue responsable en cas de décision prise à partir de données inexactes." } + , + "errors" : { + "userNotFound": "User not found" + } } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 512cd7b1..6ef493b7 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,3 +1,6 @@ +import { useMemo } from 'react' +import { useSelector } from 'react-redux' + import { NextPage } from 'next' import { Box, Flex, Grid } from '@mantine/core' @@ -9,13 +12,46 @@ import { SummaryCard, WorthCard, } from 'src/components/cards' +import { useREG } from 'src/hooks/useREG' +import { useRegVotingPower } from 'src/hooks/useREGVotingPower' +import { useRWA } from 'src/hooks/useRWA' +import { + OtherRealtoken, + UserRealtoken, + selectUserRealtokens, +} from 'src/store/features/wallets/walletsSelector' const HomePage: NextPage = () => { + const realtokens = useSelector(selectUserRealtokens) + const rwa = useRWA() + const reg = useREG() + const regVotingPower = useRegVotingPower() + + const allAssetsData = useMemo(() => { + // remove potential null/undefined values, return filtered value with the right type(s) + const assets: (UserRealtoken | OtherRealtoken | null)[] = [ + ...realtokens, + rwa, + reg, + regVotingPower, + ].filter((asset) => !!asset) + return assets as (UserRealtoken | OtherRealtoken)[] + }, [realtokens, rwa, reg, regVotingPower]) + + const otherAssetsData = useMemo(() => { + const assets = { + rwa, + reg, + regVotingPower, + } + return assets + }, [rwa, reg, regVotingPower]) + return ( - + @@ -28,7 +64,7 @@ const HomePage: NextPage = () => { - + ) diff --git a/src/pages/yamStatistics.tsx b/src/pages/yamStatistics.tsx index 5b6ce7a8..ad603a8c 100644 --- a/src/pages/yamStatistics.tsx +++ b/src/pages/yamStatistics.tsx @@ -46,14 +46,15 @@ const YamStatisticsRow: React.FC<{ {tokenPriceValue} {yamPriceValue} - {yamDifferenceValue} ( - {tNumbers('percent', { value: yamDifferencePercent })}) + {yamDifferenceValue} {'('} + {tNumbers('percent', { value: yamDifferencePercent })} + {')'} {volumeValue} - + @@ -122,7 +123,7 @@ const YamStatisticsPage = () => { }, [yamStatistics, page, pageSize]) if (isLoading) { - return
Loading...
+ return
{'Loading...'}
} return ( @@ -142,11 +143,11 @@ const YamStatisticsPage = () => {
- - - - - + + + + + {paginationYamStatistics.map((statistics, index) => ( data: string - }): null | ethers.LogDescription + }): null | LogDescription } } @@ -13,35 +25,191 @@ const GNOSIS_RPC_URLS = [ 'https://gnosis-rpc.publicnode.com', 'https://rpc.ankr.com/gnosis', 'https://gnosis.drpc.org', + 'https://rpc.gnosischain.com', + 'https://rpc.gnosis.gateway.fm', ] + const ETHEREUM_RPC_URLS = [ - 'https://rpc.ankr.com/eth', 'https://eth.llamarpc.com', + 'https://rpc.mevblocker.io', + 'https://eth.merkle.ioz', + 'https://rpc.ankr.com/eth', 'https://eth-pokt.nodies.app', ] -async function getWorkingRpcUrl(urls: string[]): Promise { +/** + * Test the RPC provider for finding the maximum number of concurrent requests it can handle + * using a dummy array of addresses for checking their balances + * @param provider RPC provider to test + * @param erc20ContractAddress ERC20 contract address + * @param concurrentRequestsMin Minimum number of concurrent requests + * @param concurrentRequestsMax Maximum number of concurrent requests + * @param requestsBatchSize Batch size + * @param waitDelayBetweenAttemptMs Delay between each test + * + * @returns + */ +async function testRpcThresholds( + provider: JsonRpcProvider, + erc20ContractAddress: string, + concurrentRequestsMin = 5, + concurrentRequestsMax = 10, + requestsBatchSize = 10, + waitDelayBetweenAttemptMs = 500, +): Promise { + let threshold = 0 + try { + if (!provider) { + throw new Error('provider is not defined') + } + if (!erc20ContractAddress) { + throw new Error('erc20ContractAddress is not defined') + } + if (concurrentRequestsMin < 1) { + throw new Error('concurrentRequestsMin cannot be less than 1') + } + if (concurrentRequestsMax < 1) { + throw new Error('concurrentRequestsMax cannot be less than 1') + } + if (requestsBatchSize < 1) { + throw new Error('requestsBatchSize cannot be less than 1') + } + if (waitDelayBetweenAttemptMs < 1) { + throw new Error('waitDelayBetweenAttemptMs cannot be less than 1') + } + if (concurrentRequestsMin > concurrentRequestsMax) { + throw new Error('concurrentMin cannot be greater than concurrentMax') + } + const erc20AbiBalanceOfOnly = getErc20AbiBalanceOfOnly() + if (!erc20AbiBalanceOfOnly) { + throw new Error('balanceOf ABI not found') + } + + // Create a dummy array of addresses filled with 'ZeroAddress' for fetching balances + const batchAddressesArray = Array(requestsBatchSize).fill([ZeroAddress]) + const contract = new Contract( + REG_ContractAddress, + erc20AbiBalanceOfOnly, + provider, + ) + // Loop from max to min concurrent requests + for ( + let currentThresold = concurrentRequestsMax; + currentThresold >= concurrentRequestsMin; + currentThresold-- + ) { + const balancesPromises = [] + // Loop for each batch request : send currentThresold requests simultaneously + for ( + let batchRequestsIdx = 0; + batchRequestsIdx < currentThresold; + batchRequestsIdx++ + ) { + const resBalancesPromise = + batchCallOneContractOneFunctionMultipleParams( + contract, + 'balanceOf', + batchAddressesArray, + requestsBatchSize, + requestsBatchSize, + 1, // only test once, no retry + false, // silence warnings/errors + ) + balancesPromises.push(resBalancesPromise) + } // Batch loop + const balances = await Promise.all(balancesPromises) + // check if any balances array are null/undefined: if so, the provider returned an error + const containsNull = balances.some((balance) => !balance) + if (!containsNull) { + threshold = currentThresold + break + } + // Else, continue to next threshold + await wait(waitDelayBetweenAttemptMs) + } // Threshold loop + } catch (error) { + console.error(error) + } + return threshold +} + +async function getWorkingRpc(urls: string[]): Promise { + let rpcConnectOk = false + let rpcThresholdValue = 0 + let failedRpcErrorCount = 0 for (const url of urls) { - const provider = new ethers.JsonRpcProvider(url) try { - await provider.getBlockNumber() - return url + rpcConnectOk = false + rpcThresholdValue = 0 + const provider = new JsonRpcProvider(url) + const network = provider.getNetwork() + const currentBlockNumber = provider.getBlockNumber() + await Promise.all([network, currentBlockNumber]) + rpcConnectOk = true + // Test for the maximum number of concurrent requests the provider can handle + rpcThresholdValue = await testRpcThresholds( + provider, + REG_ContractAddress, + 5, + 5, + 5, + 150, + ) + if (rpcThresholdValue < 1) { + // Throw error if the threshold is 0 + // Means the provider is not able to handle required concurrent requests number + // skip it and try next one + throw new Error('rpcThresholdValue returned 0') + } + // If any error has occurred before, log the successful connection + if (failedRpcErrorCount > 0) { + console.info( + `Successfully connected to ${url} after ${failedRpcErrorCount} failed attempts`, + ) + } + return provider } catch (error) { - console.error(`Failed to connect to ${url}, trying next one...`) + failedRpcErrorCount++ + if (!rpcConnectOk) { + // Connection error + console.error(`Failed to connect to ${url}, trying next one...`, error) + } else if (rpcThresholdValue < 1) { + // Threshold error + console.error( + `Successfull connection to ${url} BUT failed to test rpcThresholdValue, trying next one...`, + error, + ) + } else { + // General error + console.error(`Failed to connect to ${url}, trying next one...`, error) + } } } + throw new Error(`All RPC URLs (${urls?.length}) failed`) +} - throw new Error('All RPC URLs failed') +interface Providers { + GnosisRpcProvider: JsonRpcProvider + EthereumRpcProvider: JsonRpcProvider } +let initializeProvidersQueue: WaitingQueue | null = null +let providers: Providers | undefined = undefined + export const initializeProviders = async () => { - const gnosisRpcUrl = await getWorkingRpcUrl(GNOSIS_RPC_URLS) - const ethereumRpcUrl = await getWorkingRpcUrl(ETHEREUM_RPC_URLS) + if (initializeProvidersQueue) { + return initializeProvidersQueue.wait() + } + initializeProvidersQueue = new WaitingQueue() - const GnosisRpcProvider = new ethers.JsonRpcProvider(gnosisRpcUrl) - const EthereumRpcProvider = new ethers.JsonRpcProvider(ethereumRpcUrl) + const [GnosisRpcProvider, EthereumRpcProvider] = await Promise.all([ + getWorkingRpc(GNOSIS_RPC_URLS), + getWorkingRpc(ETHEREUM_RPC_URLS), + ]) - return { GnosisRpcProvider, EthereumRpcProvider } + providers = { GnosisRpcProvider, EthereumRpcProvider } + initializeProvidersQueue.resolve(providers) + return providers } /** @@ -53,9 +221,9 @@ export const initializeProviders = async () => { export async function getTransactionReceipt( transactionId: string, chainId: number, -): Promise { +): Promise { let attempt = 0 - let receipt: ethers.TransactionReceipt | null = null + let receipt: TransactionReceipt | null = null const { GnosisRpcProvider, EthereumRpcProvider } = await initializeProviders() diff --git a/src/repositories/currencies.repository.ts b/src/repositories/currencies.repository.ts index 1a3d2ed2..604aae11 100644 --- a/src/repositories/currencies.repository.ts +++ b/src/repositories/currencies.repository.ts @@ -8,6 +8,7 @@ export interface CurrencyRates { XdaiUsd: number EurUsd: number ChfUsd: number + UsdcUsd: number } function getChainlinkHandler(options: { @@ -39,6 +40,11 @@ const getXdaiUsd = getChainlinkHandler({ decimals: 8, }) +const getUsdcUsd = getChainlinkHandler({ + priceFeedContract: '0x26C31ac71010aF62E6B486D1132E266D6298857D', + decimals: 8, +}) + const getEurUsd = getChainlinkHandler({ priceFeedContract: '0xab70BCB260073d036d1660201e9d5405F5829b7a', decimals: 8, @@ -51,11 +57,12 @@ const getChfUsd = getChainlinkHandler({ export const CurrenciesRepository = { async getRates(): Promise { - const [XdaiUsd, EurUsd, ChfUsd] = await Promise.all([ + const [XdaiUsd, EurUsd, ChfUsd, UsdcUsd] = await Promise.all([ getXdaiUsd(), getEurUsd(), getChfUsd(), + getUsdcUsd(), ]) - return { XdaiUsd, EurUsd, ChfUsd } + return { XdaiUsd, EurUsd, ChfUsd, UsdcUsd } }, } diff --git a/src/repositories/subgraphs/queries/user.queries.ts b/src/repositories/subgraphs/queries/user.queries.ts index e676a24d..7e92fca7 100644 --- a/src/repositories/subgraphs/queries/user.queries.ts +++ b/src/repositories/subgraphs/queries/user.queries.ts @@ -33,8 +33,7 @@ const executeGetUserIdQuery = useCacheWithLocalStorage( query: GetUserIdQuery, variables: { address }, }) - - return response.data.account.userIds[0].userId ?? null + return response.data?.account?.userIds[0].userId ?? null }, { duration: 1000 * 60 * 60 * 24 * 7, // 7 days diff --git a/src/store/features/currencies/currenciesSlice.ts b/src/store/features/currencies/currenciesSlice.ts index ed5de5fa..32438fa6 100644 --- a/src/store/features/currencies/currenciesSlice.ts +++ b/src/store/features/currencies/currenciesSlice.ts @@ -15,6 +15,7 @@ const currenciesInitialState: CurrenciesInitialStateType = { [Currency.EUR]: 1, [Currency.CHF]: 1, [Currency.XDAI]: 1, + [Currency.USDC]: 1, }, isLoading: false, } @@ -40,8 +41,8 @@ export function fetchCurrenciesRates() { if (isLoading) return dispatch({ type: currenciesIsLoadingDispatchType, payload: true }) try { - const { ChfUsd, EurUsd, XdaiUsd } = await CurrenciesRepository.getRates() - + const { ChfUsd, EurUsd, XdaiUsd, UsdcUsd } = + await CurrenciesRepository.getRates() dispatch({ type: currenciesChangedDispatchType, payload: { @@ -49,6 +50,7 @@ export function fetchCurrenciesRates() { [Currency.EUR]: EurUsd, [Currency.CHF]: ChfUsd, [Currency.XDAI]: XdaiUsd, + [Currency.USDC]: UsdcUsd, }, }) } catch (error) { diff --git a/src/store/features/settings/settingsSelector.ts b/src/store/features/settings/settingsSelector.ts index df4d0c2a..e01b65c1 100644 --- a/src/store/features/settings/settingsSelector.ts +++ b/src/store/features/settings/settingsSelector.ts @@ -77,3 +77,8 @@ export const selectUserIncludesRmmV2 = createSelector( (state: RootState) => state.settings, (state) => state.includesRmmV2, ) + +export const selectUserIncludesOtherAssets = createSelector( + (state: RootState) => state.settings, + (state) => state.includesOtherAssets, +) diff --git a/src/store/features/settings/settingsSlice.ts b/src/store/features/settings/settingsSlice.ts index cac3b8af..1d35913b 100644 --- a/src/store/features/settings/settingsSlice.ts +++ b/src/store/features/settings/settingsSlice.ts @@ -19,6 +19,7 @@ const USER_RENT_CALCULATION_LS_KEY = 'store:settings/userRentCalculation' const USER_INCLUDES_ETH_LS_KEY = 'store:settings/includesEth' const USER_INCLUDES_LEVIN_SWAP_LS_KEY = 'store:settings/includesLevinSwap' const USER_INCLUDES_RMM_V2_LS_KEY = 'store:settings/includesRmmV2' +const USER_INCLUDES_OTHER_ASSETS_LS_KEY = 'store:settings/includesOtherAssets' export interface User { id: string @@ -37,6 +38,7 @@ interface SettingsInitialStateType { includesEth: boolean includesLevinSwap: boolean includesRmmV2: boolean + includesOtherAssets: boolean version?: string } @@ -51,6 +53,7 @@ const settingsInitialState: SettingsInitialStateType = { includesEth: false, includesLevinSwap: false, includesRmmV2: false, + includesOtherAssets: false, } // DISPATCH TYPE @@ -64,7 +67,8 @@ export const userIncludesLevinSwapChangedDispatchType = 'settings/includesLevinSwapChanged' export const userIncludesRmmV2ChangedDispatchType = 'settings/includesRmmV2Changed' - +export const userIncludesOtherAssetsDispatchType = + 'settings/includesOtherAssets' // ACTIONS export const initializeSettings = createAction(initializeSettingsDispatchType) export const userChanged = createAction(userChangedDispatchType) @@ -89,7 +93,9 @@ export const userIncludesLevinSwapChanged = createAction( export const userIncludesRmmV2Changed = createAction( userIncludesRmmV2ChangedDispatchType, ) - +export const userIncludesOtherAssetsChanged = createAction( + userIncludesOtherAssetsDispatchType, +) // THUNKS export function setUserAddress(address: string) { return async (dispatch: AppDispatch) => { @@ -214,6 +220,13 @@ export const settingsReducers = createReducer( action.payload.toString(), ) }) + .addCase(userIncludesOtherAssetsChanged, (state, action) => { + state.includesOtherAssets = action.payload + localStorage.setItem( + USER_INCLUDES_OTHER_ASSETS_LS_KEY, + action.payload.toString(), + ) + }) .addCase(initializeSettings, (state) => { const user = localStorage.getItem(USER_LS_KEY) const userCurrency = localStorage.getItem(USER_CURRENCY_LS_KEY) @@ -227,6 +240,9 @@ export const settingsReducers = createReducer( const userIncludesRmmV2 = localStorage.getItem( USER_INCLUDES_RMM_V2_LS_KEY, ) + const userIncludesOtherAssets = localStorage.getItem( + USER_INCLUDES_OTHER_ASSETS_LS_KEY, + ) state.user = user ? JSON.parse(user) : undefined state.userCurrency = userCurrency @@ -245,6 +261,7 @@ export const settingsReducers = createReducer( state.includesEth = userIncludesEth === 'true' state.includesLevinSwap = userIncludesLevinSwap === 'true' state.includesRmmV2 = userIncludesRmmV2 === 'true' + state.includesOtherAssets = userIncludesOtherAssets === 'true' const { publicRuntimeConfig } = getConfig() as { publicRuntimeConfig?: { version: string } diff --git a/src/store/features/wallets/walletsSelector.ts b/src/store/features/wallets/walletsSelector.ts index 31cd257a..34b185f3 100644 --- a/src/store/features/wallets/walletsSelector.ts +++ b/src/store/features/wallets/walletsSelector.ts @@ -8,6 +8,7 @@ import moment from 'moment' import { WalletBalances, WalletType } from 'src/repositories' import { UserRealTokenTransfer } from 'src/repositories/transfers/transfers.type' import { RootState } from 'src/store/store' +import { APIRealTokenDate } from 'src/types/APIRealToken' import { RealToken, RealTokenCanal } from 'src/types/RealToken' import { RentCalculation, @@ -37,12 +38,13 @@ export interface UserRealtoken extends RealToken { > } -export interface RWARealtoken { +export interface OtherRealtoken { id: string fullName: string shortName: string amount: number value: number + tokenPrice: number totalInvestment: number totalTokens: number imageLink: string[] @@ -50,6 +52,15 @@ export interface RWARealtoken { unitPriceCost: number } +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface RWARealtoken extends OtherRealtoken { + initialLaunchDate: APIRealTokenDate +} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface REGRealtoken extends OtherRealtoken {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface REGVotingPowertoken extends OtherRealtoken {} + const DAYS_PER_YEAR = 365 const MONTHS_PER_YEAR = 12 const AVG_DAYS_PER_MONTH = DAYS_PER_YEAR / MONTHS_PER_YEAR diff --git a/src/types/APIRealToken.ts b/src/types/APIRealToken.ts index b65af0b1..a25daca9 100644 --- a/src/types/APIRealToken.ts +++ b/src/types/APIRealToken.ts @@ -13,7 +13,7 @@ export enum APIRealTokenProductType { LoanIncome = 'loan_income', } -interface APIRealTokenDate { +export interface APIRealTokenDate { date: string timezone_type: number timezone: string diff --git a/src/types/Currencies.ts b/src/types/Currencies.ts index 900465e6..d12d9f39 100644 --- a/src/types/Currencies.ts +++ b/src/types/Currencies.ts @@ -3,6 +3,7 @@ export enum Currency { EUR = 'EUR', CHF = 'CHF', XDAI = 'XDAI', + USDC = 'USDC', } export enum CurrencySymbol { @@ -10,4 +11,5 @@ export enum CurrencySymbol { EUR = '€', CHF = 'CHF', XDAI = 'xDAI', + USDC = 'USDC', } diff --git a/src/utils/blockchain/ERC20.ts b/src/utils/blockchain/ERC20.ts index c799cdd9..e4dde0cb 100644 --- a/src/utils/blockchain/ERC20.ts +++ b/src/utils/blockchain/ERC20.ts @@ -44,3 +44,11 @@ export const ERC20 = { isTransferEvent, parseTransferEvent, } + +export const getErc20AbiBalanceOfOnly = (): object[] | null => { + const Erc20AbiBalanceOfOnly = ERC20ABI.find((abi) => abi.name === 'balanceOf') + if (!Erc20AbiBalanceOfOnly) { + throw new Error('balanceOf not found in ERC20 ABI') + } + return [Erc20AbiBalanceOfOnly] +} diff --git a/src/utils/blockchain/abi/ERC20ABI.ts b/src/utils/blockchain/abi/ERC20ABI.ts index d1342c3d..9bea18f6 100644 --- a/src/utils/blockchain/abi/ERC20ABI.ts +++ b/src/utils/blockchain/abi/ERC20ABI.ts @@ -1,27 +1,156 @@ export const ERC20ABI = [ { - anonymous: false, + type: 'function', + name: 'allowance', + inputs: [ + { name: 'owner', type: 'address', internalType: 'address' }, + { name: 'spender', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'approve', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { name: 'value', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'balanceOf', + inputs: [{ name: 'account', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'decimals', + inputs: [], + outputs: [{ name: '', type: 'uint8', internalType: 'uint8' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'name', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'symbol', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'totalSupply', + inputs: [], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'transfer', + inputs: [ + { name: 'to', type: 'address', internalType: 'address' }, + { name: 'value', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'transferFrom', + inputs: [ + { name: 'from', type: 'address', internalType: 'address' }, + { name: 'to', type: 'address', internalType: 'address' }, + { name: 'value', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'event', + name: 'Approval', inputs: [ { + name: 'owner', + type: 'address', indexed: true, internalType: 'address', - name: 'from', - type: 'address', }, { + name: 'spender', + type: 'address', indexed: true, internalType: 'address', - name: 'to', - type: 'address', }, { + name: 'value', + type: 'uint256', indexed: false, internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Transfer', + inputs: [ + { name: 'from', type: 'address', indexed: true, internalType: 'address' }, + { name: 'to', type: 'address', indexed: true, internalType: 'address' }, + { name: 'value', type: 'uint256', + indexed: false, + internalType: 'uint256', }, ], - name: 'Transfer', - type: 'event', + anonymous: false, + }, + { + type: 'error', + name: 'ERC20InsufficientAllowance', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { name: 'allowance', type: 'uint256', internalType: 'uint256' }, + { name: 'needed', type: 'uint256', internalType: 'uint256' }, + ], + }, + { + type: 'error', + name: 'ERC20InsufficientBalance', + inputs: [ + { name: 'sender', type: 'address', internalType: 'address' }, + { name: 'balance', type: 'uint256', internalType: 'uint256' }, + { name: 'needed', type: 'uint256', internalType: 'uint256' }, + ], + }, + { + type: 'error', + name: 'ERC20InvalidApprover', + inputs: [{ name: 'approver', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'ERC20InvalidReceiver', + inputs: [{ name: 'receiver', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'ERC20InvalidSender', + inputs: [{ name: 'sender', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'ERC20InvalidSpender', + inputs: [{ name: 'spender', type: 'address', internalType: 'address' }], }, ] diff --git a/src/utils/blockchain/abi/RegVaultABI.ts b/src/utils/blockchain/abi/RegVaultABI.ts new file mode 100644 index 00000000..710f0be8 --- /dev/null +++ b/src/utils/blockchain/abi/RegVaultABI.ts @@ -0,0 +1,670 @@ +export const RegVaultABI = [ + { inputs: [], stateMutability: 'nonpayable', type: 'constructor' }, + { inputs: [], name: 'AccessControlBadConfirmation', type: 'error' }, + { + inputs: [ + { internalType: 'address', name: 'account', type: 'address' }, + { internalType: 'bytes32', name: 'neededRole', type: 'bytes32' }, + ], + name: 'AccessControlUnauthorizedAccount', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'target', type: 'address' }], + name: 'AddressEmptyCode', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'AddressInsufficientBalance', + type: 'error', + }, + { + inputs: [ + { internalType: 'address', name: 'implementation', type: 'address' }, + ], + name: 'ERC1967InvalidImplementation', + type: 'error', + }, + { inputs: [], name: 'ERC1967NonPayable', type: 'error' }, + { inputs: [], name: 'EnforcedPause', type: 'error' }, + { inputs: [], name: 'ExpectedPause', type: 'error' }, + { inputs: [], name: 'FailedInnerCall', type: 'error' }, + { inputs: [], name: 'InvalidInitialization', type: 'error' }, + { inputs: [], name: 'InvalidTimestampForEpoch', type: 'error' }, + { inputs: [], name: 'LockPeriodNotEnded', type: 'error' }, + { inputs: [], name: 'NotInitializing', type: 'error' }, + { inputs: [], name: 'OnlyRegGovernorAllowed', type: 'error' }, + { + inputs: [{ internalType: 'address', name: 'token', type: 'address' }], + name: 'SafeERC20FailedOperation', + type: 'error', + }, + { inputs: [], name: 'SubscriptionPeriodEnded', type: 'error' }, + { inputs: [], name: 'SubscriptionPeriodNotStarted', type: 'error' }, + { inputs: [], name: 'UUPSUnauthorizedCallContext', type: 'error' }, + { + inputs: [{ internalType: 'bytes32', name: 'slot', type: 'bytes32' }], + name: 'UUPSUnsupportedProxiableUUID', + type: 'error', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { + indexed: true, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + indexed: true, + internalType: 'uint256', + name: 'epoch', + type: 'uint256', + }, + ], + name: 'ClaimBonus', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { + indexed: true, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + indexed: true, + internalType: 'uint256', + name: 'epoch', + type: 'uint256', + }, + ], + name: 'Deposit', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint64', + name: 'version', + type: 'uint64', + }, + ], + name: 'Initialized', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'Paused', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { + indexed: true, + internalType: 'uint256', + name: 'proposalId', + type: 'uint256', + }, + { + indexed: true, + internalType: 'uint256', + name: 'epoch', + type: 'uint256', + }, + ], + name: 'RecordVote', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { + indexed: true, + internalType: 'uint256', + name: 'proposalId', + type: 'uint256', + }, + { + indexed: true, + internalType: 'uint256', + name: 'epoch', + type: 'uint256', + }, + ], + name: 'RecordVoteNotActive', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { + indexed: true, + internalType: 'bytes32', + name: 'previousAdminRole', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'bytes32', + name: 'newAdminRole', + type: 'bytes32', + }, + ], + name: 'RoleAdminChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { + indexed: true, + internalType: 'address', + name: 'account', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + name: 'RoleGranted', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { + indexed: true, + internalType: 'address', + name: 'account', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + name: 'RoleRevoked', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'subscriptionStart', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'subscriptionEnd', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'lockPeriodEnd', + type: 'uint256', + }, + { + indexed: false, + internalType: 'address', + name: 'bonusToken', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'totalBonus', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'epoch', + type: 'uint256', + }, + ], + name: 'SetNewEpoch', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'regGovernor', + type: 'address', + }, + ], + name: 'SetRegGovernor', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'regToken', + type: 'address', + }, + ], + name: 'SetRegToken', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'Unpaused', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'implementation', + type: 'address', + }, + ], + name: 'Upgraded', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { + indexed: true, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + indexed: true, + internalType: 'uint256', + name: 'epoch', + type: 'uint256', + }, + ], + name: 'Withdraw', + type: 'event', + }, + { + inputs: [], + name: 'DEFAULT_ADMIN_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'PAUSER_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'UPGRADER_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'UPGRADE_INTERFACE_VERSION', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'user', type: 'address' }], + name: 'calculateBonus', + outputs: [ + { internalType: 'address[]', name: '', type: 'address[]' }, + { internalType: 'uint256[]', name: '', type: 'uint256[]' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'claimBonus', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], + name: 'deposit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'uint256', name: 'deadline', type: 'uint256' }, + { internalType: 'uint8', name: 'v', type: 'uint8' }, + { internalType: 'bytes32', name: 'r', type: 'bytes32' }, + { internalType: 'bytes32', name: 's', type: 'bytes32' }, + ], + name: 'depositWithPermit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'getCurrentEpoch', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getCurrentEpochState', + outputs: [ + { + components: [ + { + internalType: 'uint256', + name: 'subscriptionStart', + type: 'uint256', + }, + { internalType: 'uint256', name: 'subscriptionEnd', type: 'uint256' }, + { internalType: 'uint256', name: 'lockPeriodEnd', type: 'uint256' }, + { internalType: 'address', name: 'bonusToken', type: 'address' }, + { internalType: 'uint256', name: 'totalBonus', type: 'uint256' }, + { internalType: 'uint256', name: 'totalVotes', type: 'uint256' }, + { internalType: 'uint256', name: 'totalWeights', type: 'uint256' }, + ], + internalType: 'struct IREGIncentiveVault.EpochState', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getCurrentTotalDeposit', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'epoch', type: 'uint256' }], + name: 'getEpochState', + outputs: [ + { + components: [ + { + internalType: 'uint256', + name: 'subscriptionStart', + type: 'uint256', + }, + { internalType: 'uint256', name: 'subscriptionEnd', type: 'uint256' }, + { internalType: 'uint256', name: 'lockPeriodEnd', type: 'uint256' }, + { internalType: 'address', name: 'bonusToken', type: 'address' }, + { internalType: 'uint256', name: 'totalBonus', type: 'uint256' }, + { internalType: 'uint256', name: 'totalVotes', type: 'uint256' }, + { internalType: 'uint256', name: 'totalWeights', type: 'uint256' }, + ], + internalType: 'struct IREGIncentiveVault.EpochState', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRegGovernor', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRegToken', + outputs: [{ internalType: 'contract IERC20', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'role', type: 'bytes32' }], + name: 'getRoleAdmin', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'user', type: 'address' }, + { internalType: 'uint256', name: 'epoch', type: 'uint256' }, + ], + name: 'getUserEpochState', + outputs: [ + { + components: [ + { internalType: 'uint256', name: 'depositAmount', type: 'uint256' }, + { internalType: 'uint256', name: 'voteAmount', type: 'uint256' }, + { internalType: 'bool', name: 'claimed', type: 'bool' }, + ], + internalType: 'struct IREGIncentiveVault.UserEpochState', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'user', type: 'address' }], + name: 'getUserGlobalState', + outputs: [ + { + components: [ + { internalType: 'uint256', name: 'currentDeposit', type: 'uint256' }, + { + internalType: 'uint256', + name: 'lastClaimedEpoch', + type: 'uint256', + }, + ], + internalType: 'struct IREGIncentiveVault.UserGlobalState', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'grantRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'hasRole', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'regGovernor', type: 'address' }, + { internalType: 'address', name: 'regToken', type: 'address' }, + { internalType: 'address', name: 'defaultAdmin', type: 'address' }, + { internalType: 'address', name: 'pauser', type: 'address' }, + { internalType: 'address', name: 'upgrader', type: 'address' }, + ], + name: 'initialize', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'pause', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'paused', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'proxiableUUID', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'user', type: 'address' }, + { internalType: 'uint256', name: 'proposalId', type: 'uint256' }, + ], + name: 'recordVote', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'callerConfirmation', type: 'address' }, + ], + name: 'renounceRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'revokeRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'subscriptionStart', type: 'uint256' }, + { internalType: 'uint256', name: 'subscriptionEnd', type: 'uint256' }, + { internalType: 'uint256', name: 'lockPeriodEnd', type: 'uint256' }, + { internalType: 'address', name: 'bonusToken', type: 'address' }, + { internalType: 'uint256', name: 'totalBonus', type: 'uint256' }, + ], + name: 'setNewEpoch', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'regGovernor', type: 'address' }], + name: 'setRegGovernor', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'contract IERC20', name: 'regToken', type: 'address' }, + ], + name: 'setRegToken', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], + name: 'supportsInterface', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'unpause', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'newImplementation', type: 'address' }, + { internalType: 'bytes', name: 'data', type: 'bytes' }, + ], + name: 'upgradeToAndCall', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], + name: 'withdraw', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] diff --git a/src/utils/blockchain/abi/UniswapV2FactoryABI.ts b/src/utils/blockchain/abi/UniswapV2FactoryABI.ts new file mode 100644 index 00000000..2e91bf25 --- /dev/null +++ b/src/utils/blockchain/abi/UniswapV2FactoryABI.ts @@ -0,0 +1,106 @@ +export const UniswapV2FactoryABI = [ + { + type: 'constructor', + inputs: [ + { name: '_feeToSetter', type: 'address', internalType: 'address' }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'INIT_CODE_HASH', + inputs: [], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'allPairs', + inputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'allPairsLength', + inputs: [], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + // { + // type: 'function', + // name: 'createPair', + // inputs: [ + // { name: 'tokenA', type: 'address', internalType: 'address' }, + // { name: 'tokenB', type: 'address', internalType: 'address' }, + // ], + // outputs: [{ name: 'pair', type: 'address', internalType: 'address' }], + // stateMutability: 'nonpayable', + // }, + { + type: 'function', + name: 'feeTo', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'feeToSetter', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getPair', + inputs: [ + { name: '', type: 'address', internalType: 'address' }, + { name: '', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'setFeeTo', + inputs: [{ name: '_feeTo', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setFeeToSetter', + inputs: [ + { name: '_feeToSetter', type: 'address', internalType: 'address' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'event', + name: 'PairCreated', + inputs: [ + { + name: 'token0', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'token1', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'pair', + type: 'address', + indexed: false, + internalType: 'address', + }, + { name: '', type: 'uint256', indexed: false, internalType: 'uint256' }, + ], + anonymous: false, + }, +] diff --git a/src/utils/blockchain/consts/otherTokens.ts b/src/utils/blockchain/consts/otherTokens.ts new file mode 100644 index 00000000..992a99e1 --- /dev/null +++ b/src/utils/blockchain/consts/otherTokens.ts @@ -0,0 +1,57 @@ +// Each asset must have a different ID (used as KEY assets view) +const RWA_asset_ID = 0 +const REG_asset_ID = 1 +const REGVotingPower_asset_ID = 2 + +// Gnosis/xDai, Ethereum +const RWA_ContractAddress = '0x0675e8F4A52eA6c845CB6427Af03616a2af42170' +// Gnosis/xDai, Ethereum +const REG_ContractAddress = '0x0AA1e96D2a46Ec6beB2923dE1E61Addf5F5f1dce' +// Reg Vault only deployed on Gnosis/xDai +const REG_Vault_Gnosis_ContractAddress = + '0xe1877d33471e37fe0f62d20e60c469eff83fb4a0' +// Reg Voting Power only deployed on Gnosis/xDai +const RegVotingPower_Gnosis_ContractAddress = + '0x6382856a731Af535CA6aea8D364FCE67457da438' + +// Gnosis/xDai tokens for prices calculation +const WXDAI_ContractAddress = '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d' +const USDConXdai_ContractAddress = '0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83' + +const DEFAULT_RWA_PRICE = 50 // USD +const DEFAULT_REG_PRICE = 0 // USD +const DEFAULT_REGVotingPower_PRICE = 0 // USD + +const DEFAULT_XDAI_USD_RATE = 1 +const DEFAULT_USDC_USD_RATE = 1 + +const RWAtokenDecimals = 9 +const REGtokenDecimals = 18 +const REGVotingPowertokenDecimals = 18 +const USDCtokenDecimals = 6 +const WXDAItokenDecimals = 18 + +const HoneySwapFactory_Address = '0xA818b4F111Ccac7AA31D0BCc0806d64F2E0737D7' + +export { + RWA_ContractAddress, + REG_ContractAddress, + REG_Vault_Gnosis_ContractAddress, + RegVotingPower_Gnosis_ContractAddress, + WXDAI_ContractAddress, + USDConXdai_ContractAddress, + WXDAItokenDecimals, + RWAtokenDecimals, + USDCtokenDecimals, + REGtokenDecimals, + HoneySwapFactory_Address, + REGVotingPowertokenDecimals, + DEFAULT_RWA_PRICE, + DEFAULT_REG_PRICE, + DEFAULT_REGVotingPower_PRICE, + DEFAULT_XDAI_USD_RATE, + DEFAULT_USDC_USD_RATE, + RWA_asset_ID, + REG_asset_ID, + REGVotingPower_asset_ID, +} diff --git a/src/utils/blockchain/contract.ts b/src/utils/blockchain/contract.ts new file mode 100644 index 00000000..175b05a7 --- /dev/null +++ b/src/utils/blockchain/contract.ts @@ -0,0 +1,119 @@ +import { Contract } from 'ethers' + +import { wait } from '../general' + +const MAX_BATCH_CONTRACT_PER_CHUNK_DEFAULT = 100 +const MIN_BATCH_CONTRACT_PER_CHUNK_DEFAULT = 10 +const BATCH_MAX_ATTEMPTS_DEFAULT = 5 +const BATCH_WAIT_BETWEEN_ATTEMPTS_MS = 200 +const BATCH_WAIT_BETWEEN_CHUNKS_MS = 20 + +/** + * Batch call one contract one function with multiple parameters + * + * Required parameters + * @param _contract: Contract instance + * @param _methodName: string = contract method name + * @param _argsArray: object[n][m] // n: number of calls, m: number of parameters per call + * + * Optional parameters + * @param _initialBatchSize: number + * @param _minBatchSize: number + * @param _maxAttempts: number : max number of attempts, any value less than 1 will behave as a single attempt + * @param consoleWarnOnError: boolean : log error to console ; default: true ; set to false to suppress console error ; params error will still be logged + * + * @returns Promise :array of results + * + * @description Batch call the same function on the same contract with multiple parameters mutliple times + **/ +const batchCallOneContractOneFunctionMultipleParams = async ( + _contract: Contract, + _methodName: string, + _argsArray: object[][], + _initialBatchSize: number = MAX_BATCH_CONTRACT_PER_CHUNK_DEFAULT, + _minBatchSize: number = MIN_BATCH_CONTRACT_PER_CHUNK_DEFAULT, + _maxAttempts: number = BATCH_MAX_ATTEMPTS_DEFAULT, + consoleWarnOnError = true, +) => { + try { + let attempt = 0 + if (!_contract || !_methodName || !_argsArray) { + throw new Error( + 'batchCallOneContractOneFunctionMultipleParams Error:: Missing required parameters', + ) + } + if (_initialBatchSize < _minBatchSize || _initialBatchSize < 1) { + console.warn( + 'batchCallOneContractOneFunctionMultipleParams Warning:: _initialBatchSize cannot be less than _minBatchSize || _initialBatchSize < 1', + ) + // Set default values + _initialBatchSize = MAX_BATCH_CONTRACT_PER_CHUNK_DEFAULT + _minBatchSize = MIN_BATCH_CONTRACT_PER_CHUNK_DEFAULT + } + do { + // wait if attempt > 0 and grow wait time for each attempt + attempt && wait(BATCH_WAIT_BETWEEN_ATTEMPTS_MS * attempt) + attempt++ + try { + let results: object[] = [] + // Split the array into chunks + const chunks = [] + // Divide chunk size by attempt at each iteration (decrease chunk size for each attempt) + const currentBatchSize = _initialBatchSize / attempt + // Keep chunk size consistent + const chunkSize = + currentBatchSize < MIN_BATCH_CONTRACT_PER_CHUNK_DEFAULT + ? _minBatchSize + : currentBatchSize + for (let i = 0; i < _argsArray.length; i += chunkSize) { + chunks.push(_argsArray.slice(i, i + chunkSize)) + } + for (let i = 0; i < chunks.length; i++) { + const chunkPromises: object[] = [] + const _argsChunk = chunks[i] + _argsChunk.forEach(async (_args) => { + chunkPromises.push(contractCall(_contract, _methodName, _args)) + }) + const chunkResults = await Promise.all(chunkPromises) + results = results.concat(chunkResults) + // wait between remaining chunks + if (i < chunks.length - 1) { + wait(BATCH_WAIT_BETWEEN_CHUNKS_MS) + } + } + return results + } catch (error) { + if (consoleWarnOnError) { + const chainId = + (await _contract?.runner?.provider?.getNetwork())?.chainId ?? + 'unknown' + console.error( + `batchCallOneContractOneFunctionMultipleParams Error:: chainId: ${chainId} contract address: ${_contract?.target} methodName: ${_methodName} args: [${_argsArray}] initialBatchSize: ${_initialBatchSize} minBatchSize: ${_minBatchSize}`, + ) + } + } + } while (attempt < _maxAttempts) + } catch (error) { + console.error(error) + } +} + +const contractCall = async ( + _contract: Contract, + _methodName: string, + _args: object[], +): Promise => { + try { + return _contract[_methodName](..._args) + } catch (error) { + console.error(error) + } + return null +} + +export { + batchCallOneContractOneFunctionMultipleParams, + MAX_BATCH_CONTRACT_PER_CHUNK_DEFAULT, + MIN_BATCH_CONTRACT_PER_CHUNK_DEFAULT, + BATCH_MAX_ATTEMPTS_DEFAULT, +} diff --git a/src/utils/blockchain/erc20Infos.ts b/src/utils/blockchain/erc20Infos.ts new file mode 100644 index 00000000..4072610a --- /dev/null +++ b/src/utils/blockchain/erc20Infos.ts @@ -0,0 +1,62 @@ +import { Contract, JsonRpcProvider } from 'ethers' + +import { getErc20AbiBalanceOfOnly } from 'src/utils/blockchain/ERC20' + +import { batchCallOneContractOneFunctionMultipleParams } from './contract' + +const getAddressesBalances = async ( + contractAddress: string, + addressList: string[], + providers: JsonRpcProvider[], + consoleWarnOnError = false, +) => { + let totalAmount = 0 + try { + if (!contractAddress) { + consoleWarnOnError && console.error('Invalid contract address') + return totalAmount + } + if (!addressList?.length) { + consoleWarnOnError && console.error('Invalid address list') + return totalAmount + } + if (!providers?.length) { + consoleWarnOnError && console.error('Invalid providers') + return totalAmount + } + const erc20AbiBalanceOfOnly = getErc20AbiBalanceOfOnly() + if (!erc20AbiBalanceOfOnly) { + throw new Error('balanceOf ABI not found') + } + const balancesPromises = providers.map((provider: JsonRpcProvider) => { + const Erc20BalanceContract = new Contract( + contractAddress, + erc20AbiBalanceOfOnly, + provider, + ) + const balances = batchCallOneContractOneFunctionMultipleParams( + Erc20BalanceContract, + 'balanceOf', + addressList.map((address: string) => [address as unknown as object]), + ) + return balances + }) + + const balancesArray = await Promise.all(balancesPromises.flat()) + const balances = balancesArray.flat() + // Sum all valid balances + balances.forEach((balance: object | null | undefined) => { + try { + if (balance) { + totalAmount += Number(balance) + } + } catch (error) {} + }) + return totalAmount + } catch (error) { + console.warn('Failed to get balances', error) + } + return totalAmount +} + +export { getAddressesBalances } diff --git a/src/utils/blockchain/poolPrice.ts b/src/utils/blockchain/poolPrice.ts new file mode 100644 index 00000000..52fa70f1 --- /dev/null +++ b/src/utils/blockchain/poolPrice.ts @@ -0,0 +1,155 @@ +import { Contract, JsonRpcProvider, ethers } from 'ethers' + +import { LevinswapABI as UniswapV2PairABI } from './abi/LevinswapABI' +import { UniswapV2FactoryABI } from './abi/UniswapV2FactoryABI' + +const isAddressLowererThan = (address0: string, address1: string): boolean => { + if (!address0 || !address1) { + console.error(`Invalid address ${address0} or ${address1}`) + return false + } + return address0.toLowerCase() < address1.toLowerCase() +} + +const getUniV2PairAddress = async ( + factoryContract: Contract, + tokenAddress0: string, + tokenAddress1: string, +): Promise => { + try { + const pairAddress = await factoryContract.getPair( + tokenAddress0, + tokenAddress1, + ) + return pairAddress + } catch (error) { + console.error('Failed to get pair address', error) + } + return '' +} + +const getUniV2AssetPriceFromReserves = ( + reserve0: bigint, + reserve1: bigint, + token0Address: string, + token1Address: string, + token0Decimals: number, + token1Decimals: number, + whichAssetPrice = 0, // number +): number | null => { + let price: number | null = null + try { + const zeroIsLowerThanOne = isAddressLowererThan( + token0Address, + token1Address, + ) + let reserveNominator = reserve0 + let reserveNominatorMultiplierBI = BigInt(10 ** token1Decimals) + let reserveDenominator = reserve1 + let reserveDenominatorDecimalsDivider = token0Decimals + if ( + (whichAssetPrice && !zeroIsLowerThanOne) || + !(whichAssetPrice && zeroIsLowerThanOne) + ) { + reserveNominator = reserve1 + reserveNominatorMultiplierBI = BigInt(10 ** token0Decimals) + reserveDenominator = reserve0 + reserveDenominatorDecimalsDivider = token1Decimals + } + const priceBN = + (reserveNominator * reserveNominatorMultiplierBI) / reserveDenominator + price = Number(priceBN) + const priceDivider = 10 ** reserveDenominatorDecimalsDivider + + price = price / priceDivider + } catch (error) { + console.warn('Failed to compute price', error) + } + return price +} + +/** + * + * @param factoryAddress The address of the Uniswap V2 factory contract + * @param token0Address + * @param token1Address + * @param token0Decimals + * @param token1Decimals + * @param provider The provider to use to interact with the blockchain + * @param whichAssetPrice choose which asset price to get: 0 (default) = token0, any other value = token1 + * @returns + */ +const getUniV2AssetPrice = async ( + factoryAddress: string, + token0Address: string, + token1Address: string, + token0Decimals: number, + token1Decimals: number, + provider: JsonRpcProvider, + whichAssetPrice = 0, // : number +): Promise => { + let price: number | null = null + try { + const factoryContract = new ethers.Contract( + factoryAddress, + UniswapV2FactoryABI, + provider, + ) + const pairAddress = await getUniV2PairAddress( + factoryContract, + token0Address, + token1Address, + ) + if (!pairAddress) { + throw new Error('Failed to get pair address') + } + const pairContract = new ethers.Contract( + pairAddress, + UniswapV2PairABI, + provider, + ) + const reserves = await pairContract.getReserves() + const [reserve0, reserve1] = reserves + price = getUniV2AssetPriceFromReserves( + reserve0, + reserve1, + token0Address, + token1Address, + token0Decimals, + token1Decimals, + whichAssetPrice, + ) + } catch (error) { + console.warn( + `Failed to get asset price for factoryAddress ${factoryAddress} token0Address ${token0Address} token1Address ${token1Address}`, + error, + provider, + ) + } + return price +} + +const averageValues = ( + values: (number | null | undefined)[], +): number | null => { + let average: number | null = null + try { + if (values?.length) { + let sum = 0 + let count = 0 + values.forEach((value) => { + // Skip NaN / null / undefined values + if (value && isFinite(value)) { + sum += value + count++ + } + }) + average = sum / count + } + } catch (error) { + console.warn('Failed to get average values', error) + } + return average +} + +export { getUniV2AssetPrice, averageValues } diff --git a/src/utils/blockchain/regVault.ts b/src/utils/blockchain/regVault.ts new file mode 100644 index 00000000..caafbded --- /dev/null +++ b/src/utils/blockchain/regVault.ts @@ -0,0 +1,127 @@ +import { Contract, Interface, JsonRpcProvider } from 'ethers' + +import { RegVaultABI } from './abi/RegVaultABI' +import { batchCallOneContractOneFunctionMultipleParams } from './contract' + +export const getRegVaultAbiGetUserGlobalStateOnly = (): object[] | null => { + const RegVaultAbiGetUserGlobalStateOnly = RegVaultABI.find( + (abi) => abi.name === 'getUserGlobalState', + ) + if (!RegVaultAbiGetUserGlobalStateOnly) { + throw new Error('getUserGlobalState not found in RegVault ABI') + } + return [RegVaultAbiGetUserGlobalStateOnly] +} + +/** + * + * @param contractsAddressesByProvider : array of [array of contract addresses matching the providers] + * contractsAddressesByProvider must be consistet with providers array: for each provider, the corresponding array of contract addresses to query + * @param addressList + * @param providers : array of providers + * @param consoleWarnOnError + * @returns + */ +const getAddressesLockedBalances = async ( + contractsAddressesAbiFunctionnameByProvider: [ + string, + Interface | object[] | null, + string, + ][][], // [contractAddress, abi, functionName] + addressList: string[], + providers: JsonRpcProvider[], + consoleWarnOnError = false, +) => { + let totalAmount = 0 + try { + // Check parameters consistency + if (!contractsAddressesAbiFunctionnameByProvider?.length) { + consoleWarnOnError && console.warn('Invalid contracts addresses') + return totalAmount + } + // Sum all contractsAddressesByProvider lengths using reduce + // Only consider arrays with at least 1 element containing 3 elements (contractAddress, abi, functionName) + const contractAddressesSum = + contractsAddressesAbiFunctionnameByProvider.reduce( + (acc, val) => + acc + + (val.length + ? val.reduce((acc2, val2) => acc2 + (val2.length == 3 ? 1 : 0), 0) + : 0), + 0, + ) + // Nothing to do if not any contract addresse(s)/abi(s)/function name(s) provided + if (!contractAddressesSum) { + consoleWarnOnError && + console.error( + 'Invalid contracts addresses sum (no contract addresse(s)/abi(s)/function name(s))', + ) + return totalAmount + } + if (!addressList?.length) { + consoleWarnOnError && console.error('Invalid address list') + return totalAmount + } + if (!providers?.length) { + consoleWarnOnError && console.error('Invalid providers') + return totalAmount + } + // Convert addressList to object once for all providers + const addressListObj = addressList.map((address: string) => [ + address as unknown as object, + ]) + + const statesPromises = providers.map( + (provider: JsonRpcProvider, providerIdx) => { + if (!contractsAddressesAbiFunctionnameByProvider[providerIdx]?.length) { + // No contract(s) for this provider + consoleWarnOnError && console.warn('No contract(s) for this provider') + return [] + } + return contractsAddressesAbiFunctionnameByProvider[providerIdx].map( + ([contractAddress, abi, functionName]) => { + // Must have 3 elements: contractAddress, abi, functionName + if (!contractAddress || !abi || !functionName) { + consoleWarnOnError && + console.warn('ABI, contract address or function name missing') + return null + } + const RegVaultGetUserGlobalStateContract = new Contract( + contractAddress, + abi, + provider, + ) + const state = batchCallOneContractOneFunctionMultipleParams( + RegVaultGetUserGlobalStateContract, + functionName, + addressListObj, + ) + return state + }, + ) + }, + ) + + // Wait all promises to resolve and flatten the array + const statesArray = await Promise.all(statesPromises.flat()) + const states = statesArray.flat() + // Sum all valid balances + states.forEach((state: object | null | undefined) => { + try { + if (state) { + totalAmount += Number((state as { 0: string; 1: string })['0']) + } + } catch (error) { + if (consoleWarnOnError) { + console.warn('Failed to sum balances', error) + } + } + }) + return totalAmount + } catch (error) { + console.warn('Failed to get balances', error) + } + return totalAmount +} + +export { getAddressesLockedBalances } diff --git a/src/utils/general.ts b/src/utils/general.ts new file mode 100644 index 00000000..2053daf1 --- /dev/null +++ b/src/utils/general.ts @@ -0,0 +1,3 @@ +const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + +export { wait }
TokenToken PriceYam PriceYam Difference (30 days)Yam Volume (30 days){t('columns.token')}{t('columns.tokenPrice')}{t('columns.yamPrice')}{t('columns.yamDifference')}{t('columns.yamVolume')}