From 0ef7d1de97d44978834ce62f318191319c907a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nandy=20B=C3=A2?= Date: Wed, 18 Dec 2024 22:36:25 +0100 Subject: [PATCH] merge develop into master (#102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change NS for RealToken (#89) * chore: install required sharp dependency for production * chore(deps): bump body-parser from 1.20.2 to 1.20.3 (#87) Bumps [body-parser](https://github.com/expressjs/body-parser) from 1.20.2 to 1.20.3. - [Release notes](https://github.com/expressjs/body-parser/releases) - [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3) --- updated-dependencies: - dependency-name: body-parser dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: add new assets REG, REG Vote Power (#91) * first version : Assets as rows * 'other' assets display switch; 'other' assets shown in Summary 'other' assets = tokens others than RealTokens = RWA, REG, ... - Display switch for hiding 'other' assets in table rows - 'other' assets figures added to Summary section * contracts utils * use contract utils for fetching balances * Eth provider removed: REG Voting Power is not deployed on Eth * batchCallOneFunction not fully tested * error args * comments * wrong 'inheritance' * smol fix: properly throw 'User not found' error * style/comments * settings labels and icons * unitIcon: optional + alignment with other units * Filter 'other' assets in Grid view * Settings: switch icons, section names * fix: lint warning * fix: lint warning * fix: build error ? * fix: build error #2 ? * fix: build error #3 ? * fix: build error #4 ? * fix: build error #5 ? * Fix: existing filter error when filtering on "Owned on" * fix: avoid fetching others assets balances if user wallet was not set * feat: add other token price on cards and fix decimals rounds for total invest * fix: improve subsudy filter for exclude other realtokens * fix: fix last changes filter and use correct typing in filters for avoid futurs errors * feat: set initial launch date for RWA token * feat: initialize rpc provider only once * feat: include reg tokens locked inside the incentive vault * fix: fix eslint warnings and add missing translations * Update .gitignore exclude pnpm lockfile * Create .nvmrc add .nvmrc for switching between node versions * Update contract.ts optional parameters for customizing batch calls: batch max and min size, number of attempts before giving up * Regvotingpower: icon orange, size grows depending on amount * unused import * Usdc currency/rate * Voting power size & fill color depending on power amount * Voting power size & fill color depending on power amount * Fixes: token prices in USD, avoid fetching REG vault balances on Eth - Tokens values fix: Updated assets prices by using currenciesRates and user currency rate (previously considering usdc = usd and xdai = usd as well), now assets prices are converted from their respective currencies (e.g. usdc, xdai) to usd, then total value is priced in user choosen currency. - Smol fix: getAddressesLockedBalances was fetching vault balances on Eth, throwing errors as there is not such vault on Eth. Added parameters for fetching any number of vault by provider(s). * comment * Check Providers ability to handle requests - Check Providers ability to handle requests - Additionnal providers https://rpc.ankr.com/eth and https://eth-pokt.nodies.app currently fail to handle concurrent requests * Assets hooks moved in index for avoiding duplicate requests - Both SummaryCard and AssetsView use the same hooks for getting their data, leading to duplicate web3 requests Moving hooks in parent components prevents this form happening. * prettier * Avoid divide by zero in case asset.totalUnits is unknown/zero * "Clean" scripts * Assets filtering moved to the right place filter has to be in assetsView/AssetsView.tsx * Paging translation labels: placeholder, All * simplify pageSize handling * Assets filtering fix (causing pagination issues) OtherAssets filtering was improperly implemented into the map loops by skipping non "others assets" value, causing holes in display (missing cards). Filtering is now done in AssetsView where it should have been in the first place * Revert changes: realtime/global labels for rents calculation switch --------- Co-authored-by: Jycssu * fix: API URLs issue #97 (#98) * addresses issue #97 Moves api urls to (.)ENV set utls to new api.realtoken.community domain (previously api.realt.community) * Update .env.sample missing update of sample .env * Update .env.sample * NEXT_PUBLIC_ prefix removed for REALTOKENAPI ENV vars * feat: update yarn lock (#99) * feat: add new API urls to deployment workflow (#100) * feat: pass new env variables to docker env (#101) * fix: unchecked asset filters (#95) * merge master in develop (#103) * Revert "Master" * merge preprod <> master (#67) * add dropdown * simplify selector and add all token option * feat: change allPage value to Infinity * feat: estimate the fully rented rent * feat: add fully rented estimation to asset cards * refactor: move hook calls to top of the page * fix: propInfo definition * fix: last rent condition * feat: add estimation for property with not fully rented history * feat: get fully rented APR * apply max APR method * rename variable and functions * feat: add RWA token * feat: re-enable property onClick * feat: add rwa valuation on the rwa card * fix: missing property * feat: add rwa to summary card * define useRWA * take into account user currency * feat: add RWA value to net value calculation * remove comment * refactor: clean imports * feat: include RWA on Ethereum * fix: en communs * feat: update filter to support RWA token * fix: prettier * fix: other prettier errors * let prettier add strange semi-column * fix: imports * use hook * add fallback * switch for a useMemo * feat: add real time fully rented APR * feat: add gloabl metric fully rented APR * feat: add disclaimer * feat: add disclaimer * feat: update disclaimer message * fix: disclaimer message * improve message * feat: create yam statics stics page * feat: add yam statistics for all RealT Tokens on Gnosis (who have Gnosis chain contract prop) * feat: mask tokens with no volume * fix: add token name * feat: add pagination * feat: improve style * feat: change token per page to 100 * feat: add fully rented APR to asset grid * refactor: remove logs * feat: add fully rented APR to property details * fix: reset current page when tokens changed * fix: reset current page when user change page size * feat: add translation for YAM statistics hearder label * fix: yamStatistics: use selected currency for token price * feat: yamStatistics: add owned | all filter * feat: yamStatistics: add subsidized, fullySubsidized and notSubsidized filters * add additional fallbacks RPC URLs * fix: RPC initialization on currencies file --------- Co-authored-by: Nandy Bâ * merge preprod <> master (#77) * add dropdown * simplify selector and add all token option * feat: change allPage value to Infinity * feat: estimate the fully rented rent * feat: add fully rented estimation to asset cards * refactor: move hook calls to top of the page * fix: propInfo definition * fix: last rent condition * feat: add estimation for property with not fully rented history * feat: get fully rented APR * apply max APR method * rename variable and functions * feat: add RWA token * feat: re-enable property onClick * feat: add rwa valuation on the rwa card * fix: missing property * feat: add rwa to summary card * define useRWA * take into account user currency * feat: add RWA value to net value calculation * remove comment * refactor: clean imports * feat: include RWA on Ethereum * fix: en communs * feat: update filter to support RWA token * fix: prettier * fix: other prettier errors * let prettier add strange semi-column * fix: imports * use hook * add fallback * switch for a useMemo * feat: add real time fully rented APR * feat: add gloabl metric fully rented APR * feat: add disclaimer * feat: add disclaimer * feat: update disclaimer message * fix: disclaimer message * improve message * feat: create yam statics stics page * feat: add yam statistics for all RealT Tokens on Gnosis (who have Gnosis chain contract prop) * feat: mask tokens with no volume * fix: add token name * feat: add pagination * feat: improve style * feat: change token per page to 100 * feat: add fully rented APR to asset grid * refactor: remove logs * feat: add fully rented APR to property details * fix: reset current page when tokens changed * fix: reset current page when user change page size * feat: add translation for YAM statistics hearder label * fix: yamStatistics: use selected currency for token price * feat: yamStatistics: add owned | all filter * feat: yamStatistics: add subsidized, fullySubsidized and notSubsidized filters * add additional fallbacks RPC URLs * fix: RPC initialization on currencies file * feat: YamStatistic: add Yamp Volume's number of days * fix: fullyRentedAPR: fix french disclaimer text * fix: second disclaimer text small error * feat: fullyRentedAPR: manage VEFA properties * refactore: improve comment * feat: show VEFA properties forced fully rented APR only if property do not have tenants * fix: RWA table view * feat: add bridge link on header (#74) * fix: VEFA Realtime APR --------- Co-authored-by: Nandy Bâ Co-authored-by: Yohann Durand * Revert "merge preprod <> master (#77)" This reverts commit 68daad55434377c91e0224a580528e9a501e392f. * fix: APY fully rented for properties before start rent day (#85) Co-authored-by: alex <123092072+AlexRLT@users.noreply.github.com> * Change NS for RealToken (#89) (#90) --------- Co-authored-by: Sigri Co-authored-by: Kurtisone <104103601+Kurtisone@users.noreply.github.com> Co-authored-by: jycssu-com <110905167+jycssu-com@users.noreply.github.com> Co-authored-by: alex <123092072+AlexRLT@users.noreply.github.com> Co-authored-by: Jycssu Co-authored-by: Yohann Durand --------- Signed-off-by: dependabot[bot] Co-authored-by: Sigri Co-authored-by: Jycssu Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: BenoistP <75934369+BenoistP@users.noreply.github.com> Co-authored-by: Kurtisone <104103601+Kurtisone@users.noreply.github.com> Co-authored-by: jycssu-com <110905167+jycssu-com@users.noreply.github.com> Co-authored-by: alex <123092072+AlexRLT@users.noreply.github.com> Co-authored-by: Yohann Durand --- .env.sample | 2 + .github/workflows/branch.yml | 2 + .gitignore | 1 + .nvmrc | 1 + Dockerfile | 5 +- README.md | 3 + docker-compose-branch.yml | 2 + next.config.js | 2 +- package-lock.json | 288 +++++--- package.json | 7 +- src/components/assetsView/AssetsView.tsx | 44 +- .../assetsView/AssetsViewSearch.tsx | 8 +- .../filters/AssetsViewRentStatusFilter.tsx | 13 +- .../filters/AssetsViewRmmStatusFilter.tsx | 12 +- .../assetsView/filters/AssetsViewSort.tsx | 62 +- .../filters/AssetsViewSubsidyFilter.tsx | 39 +- .../filters/AssetsViewUserProtocolFilter.tsx | 19 +- .../filters/AssetsViewUserStatusFilter.tsx | 13 +- .../assetsView/filters/useFilters.ts | 4 +- src/components/assetsView/views/AssetGrid.tsx | 76 +- .../assetsView/views/AssetTable.tsx | 13 +- src/components/cards/AssetCard.tsx | 11 +- src/components/cards/RWACard.tsx | 11 +- src/components/cards/main/SummaryCard.tsx | 49 +- .../commons/fields/DecimalField.tsx | 19 +- src/components/commons/fields/StringField.tsx | 38 +- .../others/FullyRentedAPRDisclaimer.tsx | 3 +- src/components/layouts/SettingsMenu.tsx | 35 +- src/hooks/useInitStore.ts | 2 +- src/hooks/useREG.ts | 177 +++++ src/hooks/useREGVotingPower.ts | 72 ++ src/hooks/useRWA.ts | 138 ++-- src/i18next/locales/en/common.json | 25 +- src/i18next/locales/fr/common.json | 26 +- src/pages/api/history/index.ts | 6 +- src/pages/api/properties/index.ts | 6 +- src/pages/index.tsx | 40 +- src/pages/yamStatistics.tsx | 19 +- src/repositories/RpcProvider.ts | 200 +++++- src/repositories/currencies.repository.ts | 11 +- .../subgraphs/queries/user.queries.ts | 3 +- .../features/currencies/currenciesSlice.ts | 6 +- .../features/settings/settingsSelector.ts | 5 + src/store/features/settings/settingsSlice.ts | 21 +- src/store/features/wallets/walletsSelector.ts | 13 +- src/types/APIRealToken.ts | 2 +- src/types/Currencies.ts | 2 + src/utils/blockchain/ERC20.ts | 8 + src/utils/blockchain/abi/ERC20ABI.ts | 143 +++- src/utils/blockchain/abi/RegVaultABI.ts | 670 ++++++++++++++++++ .../blockchain/abi/UniswapV2FactoryABI.ts | 106 +++ src/utils/blockchain/consts/otherTokens.ts | 57 ++ src/utils/blockchain/contract.ts | 119 ++++ src/utils/blockchain/erc20Infos.ts | 62 ++ src/utils/blockchain/poolPrice.ts | 155 ++++ src/utils/blockchain/regVault.ts | 127 ++++ src/utils/general.ts | 3 + yarn.lock | 97 ++- 58 files changed, 2786 insertions(+), 317 deletions(-) create mode 100644 .nvmrc create mode 100644 src/hooks/useREG.ts create mode 100644 src/hooks/useREGVotingPower.ts create mode 100644 src/utils/blockchain/abi/RegVaultABI.ts create mode 100644 src/utils/blockchain/abi/UniswapV2FactoryABI.ts create mode 100644 src/utils/blockchain/consts/otherTokens.ts create mode 100644 src/utils/blockchain/contract.ts create mode 100644 src/utils/blockchain/erc20Infos.ts create mode 100644 src/utils/blockchain/poolPrice.ts create mode 100644 src/utils/blockchain/regVault.ts create mode 100644 src/utils/general.ts diff --git a/.env.sample b/.env.sample index 2307bc53..3dbc10d9 100644 --- a/.env.sample +++ b/.env.sample @@ -1,3 +1,5 @@ BUILD_ENV= COMMUNITY_API_KEY= THEGRAPH_API_KEY= +REALTOKENAPI='https://api.realtoken.community/v1/token' +REALTOKENAPI_HISTORY='https://history.api.realtoken.community/' \ No newline at end of file diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index 0fb470d4..a83044c2 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -78,5 +78,7 @@ jobs: docker login -u ${{ secrets.DOCKER_LOGIN }} -p ${{ secrets.DOCKER_PASSWD }} ${DOCKER_REGISTRY} THEGRAPH_API_KEY=${{ secrets.THEGRAPH_API_KEY }} \ COMMUNITY_API_KEY=${{ secrets.COMMUNITY_API_KEY }} \ + REALTOKENAPI=https://api.realtoken.community/v1/token \ + REALTOKENAPI_HISTORY=https://history.api.realtoken.community/ \ HOSTNAME=${{ github.ref_name == 'master' && 'dashboard.realtoken.community' || 'dashboard.${DOCKER_BRANCH}.realtoken.community' }} \ docker compose --project-name ${{ github.ref_name }}-dashboard --file docker-compose-branch.yml up -d' 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/Dockerfile b/Dockerfile index 3e5307fb..f62b12ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # 1. Install dependencies only when needed -FROM node:16-alpine AS deps +FROM node:18-alpine AS deps # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. RUN apk add --no-cache libc6-compat @@ -20,7 +20,7 @@ COPY . . RUN yarn build # 3. Production image, copy all the files and run next -FROM node:16-alpine AS runner +FROM node:18-alpine AS runner WORKDIR /app ENV NODE_ENV=production @@ -35,6 +35,7 @@ COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +RUN npm i sharp@0.32.6 --ignore-engines USER nextjs EXPOSE 3000 diff --git a/README.md b/README.md index 920d7d04..b7643d6a 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ To run the project you will need to set-up a `.env` file in the root folder: ``` COMMUNITY_API_KEY=XXXXXXXXXXXX THEGRAPH_API_KEY=XXXXXXXXXXXX + +REALTOKENAPI='https://api.realtoken.community/v1/token' +REALTOKENAPI_HISTORY='https://history.api.realtoken.community/' ``` To get a `COMMUNITY_API_KEY`, join the dedicated [telegram dev channel](https://t.me/+XQyoaFfmN61yk7X0) then ask for. diff --git a/docker-compose-branch.yml b/docker-compose-branch.yml index 31277d05..98d05a6d 100644 --- a/docker-compose-branch.yml +++ b/docker-compose-branch.yml @@ -5,6 +5,8 @@ services: environment: - THEGRAPH_API_KEY=$THEGRAPH_API_KEY - COMMUNITY_API_KEY=$COMMUNITY_API_KEY + - REALTOKENAPI=$REALTOKENAPI + - REALTOKENAPI_HISTORY=$REALTOKENAPI_HISTORY networks: - traefik-realt labels: 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-lock.json b/package-lock.json index c5ae3fde..8ecc5fb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "realtoken-dashboard-v2", - "version": "2.4.0", + "version": "2.4.1", "dependencies": { "@apollo/client": "^3.9.5", "@mantine/core": "^7.5.3", @@ -4708,9 +4708,9 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "peer": true, "dependencies": { "bytes": "3.1.2", @@ -4721,7 +4721,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -4746,21 +4746,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "peer": true }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "peer": true, - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/borsh": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", @@ -4897,13 +4882,44 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.2.tgz", + "integrity": "sha512-0lk0PHFe/uz0vl527fG9CgdE9WdafjDbCXvBbs+LUv000TVt2Jjhqbs4Jwm8gz070w8xXyEAxrPOMullsxXeGg==", + "dependencies": { + "call-bind": "^1.0.8", + "get-intrinsic": "^1.2.5" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5334,16 +5350,19 @@ "dev": true }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { @@ -5528,6 +5547,19 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/dunder-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", + "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexify": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", @@ -5737,13 +5769,20 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-abstract/node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" } }, "node_modules/es-iterator-helpers": { @@ -5768,6 +5807,17 @@ "safe-array-concat": "^1.0.1" } }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", @@ -6985,14 +7035,23 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", - "dependencies": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", + "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7114,11 +7173,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7204,11 +7263,11 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7218,6 +7277,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -7226,9 +7286,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -7260,9 +7320,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -8572,6 +8632,14 @@ "remove-accents": "0.5.0" } }, + "node_modules/math-intrinsics": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz", + "integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -8937,9 +9005,12 @@ } }, "node_modules/object-inspect": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", - "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9547,11 +9618,11 @@ } }, "node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -10212,14 +10283,16 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -10282,13 +10355,68 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" 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..d40b18db 100644 --- a/src/components/assetsView/filters/AssetsViewRentStatusFilter.tsx +++ b/src/components/assetsView/filters/AssetsViewRentStatusFilter.tsx @@ -3,8 +3,9 @@ import { useTranslation } from 'react-i18next' import { Select } from '@mantine/core' +import { assetsViewDefaultFilter } from 'src/states' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' @@ -50,7 +51,11 @@ export const AssetsViewRentStatusFilter: FC< data={viewOptions} value={filter.rentStatus} onChange={(value) => - onChange({ rentStatus: value as AssetRentStatusType }) + onChange({ + rentStatus: + (value as AssetRentStatusType) ?? + assetsViewDefaultFilter.rentStatus, + }) } classNames={inputClasses} /> @@ -61,7 +66,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..adc05b69 100644 --- a/src/components/assetsView/filters/AssetsViewRmmStatusFilter.tsx +++ b/src/components/assetsView/filters/AssetsViewRmmStatusFilter.tsx @@ -3,8 +3,9 @@ import { useTranslation } from 'react-i18next' import { Select } from '@mantine/core' +import { assetsViewDefaultFilter } from 'src/states' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' @@ -46,7 +47,12 @@ export const AssetsViewRmmStatusFilter: FC = ({ label={t('label')} data={viewOptions} value={filter.rmmStatus} - onChange={(value) => onChange({ rmmStatus: value as AssetRmmStatusType })} + onChange={(value) => + onChange({ + rmmStatus: + (value as AssetRmmStatusType) ?? assetsViewDefaultFilter.rmmStatus, + }) + } classNames={inputClasses} /> ) @@ -56,7 +62,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..c4e4e762 100644 --- a/src/components/assetsView/filters/AssetsViewSort.tsx +++ b/src/components/assetsView/filters/AssetsViewSort.tsx @@ -4,9 +4,10 @@ import { useSelector } from 'react-redux' import { Grid, Select, Switch } from '@mantine/core' +import { assetsViewDefaultFilter } from 'src/states' import { selectTransfersIsLoaded } from 'src/store/features/transfers/transfersSelector' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' @@ -73,7 +74,11 @@ export const AssetsViewSort: FC = ({ data={sortOptions} value={filter.sortBy} onChange={(value) => - onChange({ ...filter, sortBy: value as AssetSortType }) + onChange({ + ...filter, + sortBy: + (value as AssetSortType) ?? assetsViewDefaultFilter.sortBy, + }) } classNames={inputClasses} /> @@ -92,51 +97,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..1d2270cf 100644 --- a/src/components/assetsView/filters/AssetsViewSubsidyFilter.tsx +++ b/src/components/assetsView/filters/AssetsViewSubsidyFilter.tsx @@ -3,8 +3,9 @@ import { useTranslation } from 'react-i18next' import { Select } from '@mantine/core' +import { assetsViewDefaultFilter } from 'src/states' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' @@ -66,7 +67,12 @@ export const AssetsViewSubsidyFilter: FC = ({ label={t('label')} data={viewOptions} value={filter.subsidy} - onChange={(value) => onChange({ subsidy: value as AssetSubsidyType })} + onChange={(value) => + onChange({ + subsidy: + (value as AssetSubsidyType) ?? assetsViewDefaultFilter.subsidy, + }) + } classNames={inputClasses} /> ) @@ -76,23 +82,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..8c56e6d8 100644 --- a/src/components/assetsView/filters/AssetsViewUserProtocolFilter.tsx +++ b/src/components/assetsView/filters/AssetsViewUserProtocolFilter.tsx @@ -3,8 +3,9 @@ import { useTranslation } from 'react-i18next' import { Select } from '@mantine/core' +import { assetsViewDefaultFilter } from 'src/states' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' @@ -54,7 +55,11 @@ export const AssetsViewUserProtocolFilter: FC< data={viewOptions} value={filter.userProtocol} onChange={(value) => - onChange({ userProtocol: value as AssetUserProtocolType }) + onChange({ + userProtocol: + (value as AssetUserProtocolType) ?? + assetsViewDefaultFilter.userProtocol, + }) } classNames={inputClasses} /> @@ -66,20 +71,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..c85e7ae1 100644 --- a/src/components/assetsView/filters/AssetsViewUserStatusFilter.tsx +++ b/src/components/assetsView/filters/AssetsViewUserStatusFilter.tsx @@ -3,8 +3,9 @@ import { useTranslation } from 'react-i18next' import { Select } from '@mantine/core' +import { assetsViewDefaultFilter } from 'src/states' import { - RWARealtoken, + OtherRealtoken, UserRealtoken, } from 'src/store/features/wallets/walletsSelector' @@ -58,7 +59,11 @@ export const AssetsViewUserStatusFilter: FC< data={viewOptions} value={filter.userStatus} onChange={(value) => - onChange({ userStatus: value as AssetUserStatusType }) + onChange({ + userStatus: + (value as AssetUserStatusType) ?? + assetsViewDefaultFilter.userStatus, + }) } classNames={inputClasses} /> @@ -69,7 +74,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/api/history/index.ts b/src/pages/api/history/index.ts index 976dfc02..7305ad8e 100644 --- a/src/pages/api/history/index.ts +++ b/src/pages/api/history/index.ts @@ -8,8 +8,10 @@ const getRealTokenHistory = useCache( if (!process.env.COMMUNITY_API_KEY) { throw new Error('Missing COMMUNITY_API_KEY env variable') } - - const response = await fetch('https://history.api.realt.community/', { + if (!process.env.REALTOKENAPI_HISTORY) { + throw new Error('Missing REALTOKENAPI_HISTORY env variable') + } + const response = await fetch(process.env.REALTOKENAPI_HISTORY, { method: 'GET', headers: { 'X-AUTH-REALT-TOKEN': process.env.COMMUNITY_API_KEY }, }) diff --git a/src/pages/api/properties/index.ts b/src/pages/api/properties/index.ts index aba106e2..45f8c7be 100644 --- a/src/pages/api/properties/index.ts +++ b/src/pages/api/properties/index.ts @@ -8,8 +8,10 @@ const getRealTokenList = useCache( if (!process.env.COMMUNITY_API_KEY) { throw new Error('Missing COMMUNITY_API_KEY env variable') } - - const response = await fetch('https://api.realt.community/v1/token', { + if (!process.env.REALTOKENAPI) { + throw new Error('Missing REALTOKENAPI env variable') + } + const response = await fetch(process.env.REALTOKENAPI, { method: 'GET', headers: { 'X-AUTH-REALT-TOKEN': process.env.COMMUNITY_API_KEY }, }) 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 } diff --git a/yarn.lock b/yarn.lock index 1cbf2656..8d2a5ab7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1048,7 +1048,7 @@ "@next/swc-darwin-arm64@12.1.5": version "12.1.5" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.5.tgz#3d5b53211484c72074f4975ba0ec2b1107db300e" + resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.5.tgz" integrity sha512-y8mhldb/WFZ6lFeowkGfi0cO/lBdiBqDk4T4LZLvCpoQp4Or/NzUN6P5NzBQZ5/b4oUHM/wQICEM+1wKA4qIVw== "@next/swc-darwin-x64@12.1.5": @@ -1093,7 +1093,7 @@ "@next/swc-win32-x64-msvc@12.1.5": version "12.1.5" - resolved "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.5.tgz" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.5.tgz#02f377e4d41eaaacf265e34bab9bacd8efc4a351" integrity sha512-/SoXW1Ntpmpw3AXAzfDRaQidnd8kbZ2oSni8u5z0yw6t4RwJvmdZy1eOaAADRThWKV+2oU90++LSnXJIwBRWYQ== "@noble/curves@1.2.0", "@noble/curves@~1.2.0": @@ -1153,7 +1153,7 @@ "@parcel/watcher-darwin-arm64@2.3.0": version "2.3.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.3.0.tgz#c9cd03f8f233d512fcfc873d5b4e23f1569a82ad" + resolved "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.3.0.tgz" integrity sha512-mKY+oijI4ahBMc/GygVGvEdOq0L4DxhYgwQqYAz/7yPzuGi79oXrZG52WdpGA1wLBPrYb0T8uBaGFo7I6rvSKw== "@parcel/watcher-darwin-x64@2.3.0": @@ -1212,7 +1212,7 @@ "@parcel/watcher-win32-x64@2.3.0": version "2.3.0" - resolved "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.3.0.tgz" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.3.0.tgz#14e7246289861acc589fd608de39fe5d8b4bb0a7" integrity sha512-dLx+0XRdMnVI62kU3wbXvbIRhLck4aE28bIGKbRGS7BJNt54IIj9+c/Dkqb+7DJEbHUZAX1bwaoM8PqVlHJmCA== "@parcel/watcher@^2.3.0": @@ -1335,6 +1335,11 @@ "@noble/hashes" "~1.3.0" "@scure/base" "~1.1.0" +"@sindresorhus/merge-streams@^2.1.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz#719df7fb41766bc143369eaa0dd56d8dc87c9958" + integrity sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg== + "@solana/buffer-layout@^4.0.1": version "4.0.1" resolved "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz" @@ -2417,7 +2422,7 @@ brace-expansion@^1.1.7: braces@^3.0.2, braces@~3.0.2: version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: fill-range "^7.1.1" @@ -2780,6 +2785,26 @@ defu@^6.1.2, defu@^6.1.3: resolved "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz" integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== +del-cli@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/del-cli/-/del-cli-6.0.0.tgz#7822d0ffd5b73449a506a586d839711485bfb119" + integrity sha512-9nitGV2W6KLFyya4qYt4+9AKQFL+c0Ehj5K7V7IwlxTc6RMCfQUGY9E9pLG6e8TQjtwXpuiWIGGZb3mfVxyZkw== + dependencies: + del "^8.0.0" + meow "^13.2.0" + +del@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-8.0.0.tgz#f333a5673cfeb72e46084031714a7c30515e80aa" + integrity sha512-R6ep6JJ+eOBZsBr9esiNN1gxFbZE4Q2cULkUSFumGYecAiS6qodDvcPx/sFuWHMNul7DWmrtoEOpYSm7o6tbSA== + dependencies: + globby "^14.0.2" + is-glob "^4.0.3" + is-path-cwd "^3.0.0" + is-path-inside "^4.0.0" + p-map "^7.0.2" + slash "^5.1.0" + delay@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz" @@ -3401,9 +3426,9 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.11, fast-glob@^3.2.9: +fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.2: version "3.3.2" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" @@ -3458,7 +3483,7 @@ file-uri-to-path@1.0.0: fill-range@^7.1.1: version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -3517,7 +3542,7 @@ fs.realpath@^1.0.0: fsevents@~2.3.2: version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.1, function-bind@^1.1.2: @@ -3652,6 +3677,18 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +globby@^14.0.2: + version "14.0.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-14.0.2.tgz#06554a54ccfe9264e5a9ff8eded46aa1e306482f" + integrity sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw== + dependencies: + "@sindresorhus/merge-streams" "^2.1.0" + fast-glob "^3.3.2" + ignore "^5.2.4" + path-type "^5.0.0" + slash "^5.1.0" + unicorn-magic "^0.1.0" + gopd@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz" @@ -3824,6 +3861,11 @@ ignore@^5.2.0: resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz" integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== +ignore@^5.2.4: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + immer@^10.0.3: version "10.0.3" resolved "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz" @@ -4031,11 +4073,21 @@ is-number@^7.0.0: resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-path-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-3.0.0.tgz#889b41e55c8588b1eb2a96a61d05740a674521c7" + integrity sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA== + is-path-inside@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-path-inside@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-4.0.0.tgz#805aeb62c47c1b12fc3fd13bfb3ed1e7430071db" + integrity sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA== + is-regex@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" @@ -4423,7 +4475,7 @@ lodash@^1.0.0: lodash@^4.17.21: version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: @@ -4460,6 +4512,11 @@ match-sorter@^6.0.2: "@babel/runtime" "^7.23.8" remove-accents "0.5.0" +meow@^13.2.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f" + integrity sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" @@ -4828,6 +4885,11 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" +p-map@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-7.0.3.tgz#7ac210a2d36f81ec28b736134810f7ba4418cdb6" + integrity sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA== + p-try@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" @@ -4875,6 +4937,11 @@ path-type@^4.0.0: resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +path-type@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-5.0.0.tgz#14b01ed7aea7ddf9c7c3f46181d4d04f9c785bb8" + integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg== + pathe@^1.1.0, pathe@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz" @@ -5503,6 +5570,11 @@ slash@^3.0.0: resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slash@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce" + integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== + sonic-boom@^2.2.1: version "2.8.0" resolved "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz" @@ -5899,6 +5971,11 @@ unfetch@^4.2.0: resolved "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz" integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== +unicorn-magic@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz#1bb9a51c823aaf9d73a8bfcd3d1a23dde94b0ce4" + integrity sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ== + unload@2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz"
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')}