From 46a2dbd57aa4d88b81ea71995a3e5b70a386ea94 Mon Sep 17 00:00:00 2001 From: Kieran O'Neill Date: Fri, 22 Dec 2023 23:54:15 +0200 Subject: [PATCH] feat: add arc200 asset (#63) * refactor: rename assets to standard assets * refactor: use standard assets in the asset holdings in accounts * feat: add arc200 asset holdings to the account information * feat: add asa badge to standard assets and move to separate components * feat: add arc200 featire to redux store * feat: add arc200 asset tab item * feat: add add asset button to open an add asset modal * feat: add add assets feature to the redux store * build(deps): update algosdk package to 2.7.0 * feat: add query to fetch arc200 assets in a search * chore: squash * feat: abort query request when new request is made * feat: add arc200 add asset summary page * feat: add feature to add arc200 asset to the account holdings * feat: update arc200 asset information when updating the accounts --- package.json | 5 +- src/extension/apps/background/App.tsx | 6 +- src/extension/apps/main/App.tsx | 8 +- src/extension/apps/main/Root.tsx | 64 +--- .../AccountAssetsTab/AccountAssetsTab.tsx | 250 ------------- .../components/AccountAssetsTab/index.ts | 1 - .../AddAssetModal/AddAssetArc200AssetItem.tsx | 126 +++++++ .../AddAssetModal/AddAssetModal.tsx | 334 ++++++++++++++++++ .../AddAssetModalArc200SummaryContent.tsx | 186 ++++++++++ .../AddAssetStandardAssetItem.tsx | 130 +++++++ .../components/AddAssetModal/index.ts | 1 + .../components/AssetAvatar/AssetAvatar.tsx | 4 +- .../components/AssetBadge/AssetBadge.tsx | 44 +++ src/extension/components/AssetBadge/index.ts | 1 + .../components/AssetSelect/AssetSelect.tsx | 14 +- .../AssetSelect/AssetSelectOption.tsx | 7 +- .../AssetSelect/AssetSelectSingleValue.tsx | 7 +- .../components/AssetSelect/types/IOption.ts | 4 +- .../AssetsTab/AssetTabArc200AssetItem.tsx | 163 +++++++++ .../AssetsTab/AssetTabLoadingItem.tsx | 50 +++ .../AssetsTab/AssetTabStandardAssetItem.tsx | 163 +++++++++ .../components/AssetsTab/AssetsTab.tsx | 233 ++++++++++++ src/extension/components/AssetsTab/index.ts | 1 + ...setFreezeInnerTransactionAccordionItem.tsx | 12 +- ...tTransferInnerTransactionAccordionItem.tsx | 18 +- .../SendAssetModal/SendAmountInput.tsx | 4 +- .../SendAssetModal/SendAssetModal.tsx | 11 +- .../SendAssetModalSummaryContent.tsx | 4 +- .../AssetConfigTransactionContent.tsx | 9 +- .../AssetFreezeTransactionContent.tsx | 10 +- .../AssetTransferTransactionContent.tsx | 9 +- .../MultipleTransactionsContent.tsx | 15 +- .../SignTxnsModal/SignTxnsModalContent.tsx | 32 +- .../AssetTransferTransactionItemContent.tsx | 10 +- .../TransactionItem/TransactionItem.tsx | 8 +- src/extension/constants/Dimensions.ts | 1 + src/extension/constants/Keys.ts | 5 +- src/extension/enums/AccountsThunkEnum.ts | 1 + src/extension/enums/AddAssetThunkEnum.ts | 5 + src/extension/enums/Arc200AssetsThunkEnum.ts | 6 + src/extension/enums/AssetTypeEnum.ts | 6 + src/extension/enums/AssetsThunkEnum.ts | 7 - .../enums/StandardAssetsThunkEnum.ts | 7 + src/extension/enums/StoreNameEnum.ts | 4 +- src/extension/enums/index.ts | 5 +- src/extension/features/accounts/slice.ts | 56 ++- .../thunks/addArc200AssetHoldingThunk.ts | 132 +++++++ .../features/accounts/thunks/index.ts | 1 + .../types/IAddArc200AssetHoldingPayload.ts | 7 + .../features/accounts/types/index.ts | 1 + .../utils/fetchArc200AssetHoldingWithDelay.ts | 51 +++ .../features/accounts/utils/index.ts | 1 + .../utils/updateAccountInformation.ts | 19 +- .../features/{assets => add-asset}/index.ts | 0 src/extension/features/add-asset/slice.ts | 104 ++++++ .../features/add-asset/thunks/index.ts | 1 + .../add-asset/thunks/queryByIdThunk.ts | 148 ++++++++ .../add-asset/types/IAddAssetState.ts | 16 + .../add-asset/types/IAssetsWithNextToken.ts | 9 + .../types/IQueryByIdAsyncThunkConfig.ts | 11 + .../add-asset/types/IQueryByIdResult.ts | 12 + .../features/add-asset/types/index.ts | 4 + .../add-asset/utils/getInitialState.ts | 15 + .../features/add-asset/utils/index.ts | 2 + .../searchAlgorandApplicationsWithDelay.ts | 52 +++ src/extension/features/arc200-assets/index.ts | 4 + src/extension/features/arc200-assets/slice.ts | 86 +++++ .../fetchArc200AssetsFromStorageThunk.ts | 51 +++ .../features/arc200-assets/thunks/index.ts | 2 + .../updateArc200AssetInformationThunk.ts | 91 +++++ .../arc200-assets/types/IArc200AssetsState.ts | 18 + .../IUpdateArc200AssetInformationPayload.ts | 12 + .../IUpdateArc200AssetInformationResult.ts | 13 + .../features/arc200-assets/types/index.ts | 3 + .../arc200-assets/utils/getInitialState.ts | 11 + .../features/arc200-assets/utils/index.ts | 2 + .../utils/updateArc200AssetInformationById.ts | 63 ++++ src/extension/features/assets/slice.ts | 78 ---- .../assets/thunks/fetchAssetsThunk.ts | 64 ---- src/extension/features/assets/thunks/index.ts | 2 - .../thunks/updateAssetInformationThunk.ts | 93 ----- .../types/IUpdateAssetInformationPayload.ts | 12 - .../types/IUpdateAssetInformationResult.ts | 13 - src/extension/features/assets/types/index.ts | 3 - .../features/assets/utils/getInitialState.ts | 11 - src/extension/features/assets/utils/index.ts | 3 - src/extension/features/send-assets/slice.ts | 4 +- .../thunks/submitTransactionThunk.ts | 4 +- .../types/IInitializeSendAssetPayload.ts | 4 +- .../send-assets/types/ISendAssetsState.ts | 6 +- .../utils/createSendAssetTransaction.ts | 4 +- .../features/standard-assets/index.ts | 4 + .../features/standard-assets/slice.ts | 86 +++++ .../fetchStandardAssetsFromStorageThunk.ts | 48 +++ .../features/standard-assets/thunks/index.ts | 2 + .../updateStandardAssetInformationThunk.ts | 91 +++++ .../types/IStandardAssetsState.ts} | 12 +- .../IUpdateStandardAssetInformationPayload.ts | 12 + .../IUpdateStandardAssetInformationResult.ts | 13 + .../features/standard-assets/types/index.ts | 3 + ...fetchStandardAssetInformationWithDelay.ts} | 4 +- .../standard-assets/utils/getInitialState.ts | 11 + .../features/standard-assets/utils/index.ts | 3 + .../updateStandardAssetInformationById.ts} | 36 +- src/extension/hooks/useAsset/index.ts | 2 - .../hooks/useAsset/types/IUseAssetState.ts | 9 - src/extension/hooks/useAsset/types/index.ts | 1 - src/extension/hooks/useAsset/useAsset.ts | 56 --- src/extension/hooks/useAssets/index.ts | 1 - src/extension/hooks/useAssets/useAssets.ts | 33 -- src/extension/hooks/useOnNewAssets/index.ts | 1 + .../hooks/useOnNewAssets/useOnNewAssets.ts | 108 ++++++ .../hooks/useStandardAssetById/index.ts | 2 + .../types/IUseStandardAssetByIdState.ts | 9 + .../hooks/useStandardAssetById/types/index.ts | 1 + .../useStandardAssetById.ts | 58 +++ .../pages/AccountPage/AccountPage.tsx | 21 +- src/extension/pages/AssetPage/AssetPage.tsx | 6 +- .../useAssetPage/types/IUseAssetPageState.ts | 8 +- .../hooks/useAssetPage/useAssetPage.ts | 22 +- .../ApplicationTransactionContent.tsx | 7 +- .../AssetConfigTransactionContent.tsx | 16 +- .../AssetCreateTransactionContent.tsx | 7 +- .../AssetFreezeTransactionContent.tsx | 12 +- .../AssetTransferTransactionContent.tsx | 18 +- src/extension/selectors/index.ts | 16 +- .../selectors/useSelectAddAssetAccount.ts | 20 ++ .../useSelectAddAssetArc200Assets.ts | 10 + .../selectors/useSelectAddAssetError.ts | 13 + .../selectors/useSelectAddAssetFetching.ts | 10 + .../useSelectAddAssetSelectedArc200Asset.ts | 10 + .../useSelectArc200AssetsBySelectedNetwork.ts | 31 ++ src/extension/selectors/useSelectAssets.ts | 14 - .../selectors/useSelectAssetsByGenesisHash.ts | 23 -- .../useSelectFetchingArc200Assets.ts | 9 + .../selectors/useSelectFetchingAssets.ts | 7 - .../useSelectFetchingStandardAssets.ts | 9 + .../useSelectPreferredBlockExplorer.ts | 5 +- .../useSelectSendingAssetSelectedAsset.ts | 6 +- .../useSelectStandardAssetsByGenesisHash.ts | 27 ++ ...seSelectStandardAssetsBySelectedNetwork.ts | 31 ++ .../useSelectUpdatingArc200Assets.ts | 10 + .../selectors/useSelectUpdatingAssets.ts | 7 - .../useSelectUpdatingStandardAssets.ts | 10 + src/extension/services/AccountService.ts | 27 +- src/extension/services/Arc200AssetService.ts | 78 ++++ .../services/StandardAssetService.ts | 80 +++++ src/extension/services/index.ts | 2 + src/extension/translations/en.ts | 9 +- src/extension/types/IAccountInformation.ts | 9 +- src/extension/types/IAlgorandApplication.ts | 12 + .../types/IAlgorandApplicationParams.ts | 15 + .../IAlgorandSearchApplicationsResult.ts | 10 + .../types/IAppThunkDispatchReturn.ts | 17 + src/extension/types/IArc200Asset.ts | 20 ++ src/extension/types/IArc200AssetHolding.ts | 10 + .../types/IArc200AssetInformation.ts | 8 + src/extension/types/IAssetHolding.ts | 12 - src/extension/types/IBackgroundRootState.ts | 6 +- src/extension/types/IBaseAsyncThunkConfig.ts | 8 + src/extension/types/IMainRootState.ts | 8 +- .../types/{IAsset.ts => IStandardAsset.ts} | 4 +- src/extension/types/IStandardAssetHolding.ts | 12 + src/extension/types/IStorageItemTypes.ts | 6 +- src/extension/types/index.ts | 12 +- .../utils/calculateMaxTransactionAmount.ts | 9 +- .../utils/createNativeCurrencyAsset.ts | 8 +- .../fetchArc200AssetInformationWithDelay.ts | 49 +++ src/extension/utils/index.ts | 4 +- .../mapAlgorandAccountInformationToAccount.ts | 10 +- ...apArc200AssetFromArc200AssetInformation.ts | 25 ++ ...s => mapStandardAssetFromAlgorandAsset.ts} | 6 +- src/extension/utils/parseTransactionType.ts | 2 +- tsconfig.json | 8 +- webpack/utils/createCommonConfig.ts | 14 +- yarn.lock | 36 ++ 176 files changed, 3808 insertions(+), 1009 deletions(-) delete mode 100644 src/extension/components/AccountAssetsTab/AccountAssetsTab.tsx delete mode 100644 src/extension/components/AccountAssetsTab/index.ts create mode 100644 src/extension/components/AddAssetModal/AddAssetArc200AssetItem.tsx create mode 100644 src/extension/components/AddAssetModal/AddAssetModal.tsx create mode 100644 src/extension/components/AddAssetModal/AddAssetModalArc200SummaryContent.tsx create mode 100644 src/extension/components/AddAssetModal/AddAssetStandardAssetItem.tsx create mode 100644 src/extension/components/AddAssetModal/index.ts create mode 100644 src/extension/components/AssetBadge/AssetBadge.tsx create mode 100644 src/extension/components/AssetBadge/index.ts create mode 100644 src/extension/components/AssetsTab/AssetTabArc200AssetItem.tsx create mode 100644 src/extension/components/AssetsTab/AssetTabLoadingItem.tsx create mode 100644 src/extension/components/AssetsTab/AssetTabStandardAssetItem.tsx create mode 100644 src/extension/components/AssetsTab/AssetsTab.tsx create mode 100644 src/extension/components/AssetsTab/index.ts create mode 100644 src/extension/enums/AddAssetThunkEnum.ts create mode 100644 src/extension/enums/Arc200AssetsThunkEnum.ts create mode 100644 src/extension/enums/AssetTypeEnum.ts delete mode 100644 src/extension/enums/AssetsThunkEnum.ts create mode 100644 src/extension/enums/StandardAssetsThunkEnum.ts create mode 100644 src/extension/features/accounts/thunks/addArc200AssetHoldingThunk.ts create mode 100644 src/extension/features/accounts/types/IAddArc200AssetHoldingPayload.ts create mode 100644 src/extension/features/accounts/utils/fetchArc200AssetHoldingWithDelay.ts rename src/extension/features/{assets => add-asset}/index.ts (100%) create mode 100644 src/extension/features/add-asset/slice.ts create mode 100644 src/extension/features/add-asset/thunks/index.ts create mode 100644 src/extension/features/add-asset/thunks/queryByIdThunk.ts create mode 100644 src/extension/features/add-asset/types/IAddAssetState.ts create mode 100644 src/extension/features/add-asset/types/IAssetsWithNextToken.ts create mode 100644 src/extension/features/add-asset/types/IQueryByIdAsyncThunkConfig.ts create mode 100644 src/extension/features/add-asset/types/IQueryByIdResult.ts create mode 100644 src/extension/features/add-asset/types/index.ts create mode 100644 src/extension/features/add-asset/utils/getInitialState.ts create mode 100644 src/extension/features/add-asset/utils/index.ts create mode 100644 src/extension/features/add-asset/utils/searchAlgorandApplicationsWithDelay.ts create mode 100644 src/extension/features/arc200-assets/index.ts create mode 100644 src/extension/features/arc200-assets/slice.ts create mode 100644 src/extension/features/arc200-assets/thunks/fetchArc200AssetsFromStorageThunk.ts create mode 100644 src/extension/features/arc200-assets/thunks/index.ts create mode 100644 src/extension/features/arc200-assets/thunks/updateArc200AssetInformationThunk.ts create mode 100644 src/extension/features/arc200-assets/types/IArc200AssetsState.ts create mode 100644 src/extension/features/arc200-assets/types/IUpdateArc200AssetInformationPayload.ts create mode 100644 src/extension/features/arc200-assets/types/IUpdateArc200AssetInformationResult.ts create mode 100644 src/extension/features/arc200-assets/types/index.ts create mode 100644 src/extension/features/arc200-assets/utils/getInitialState.ts create mode 100644 src/extension/features/arc200-assets/utils/index.ts create mode 100644 src/extension/features/arc200-assets/utils/updateArc200AssetInformationById.ts delete mode 100644 src/extension/features/assets/slice.ts delete mode 100644 src/extension/features/assets/thunks/fetchAssetsThunk.ts delete mode 100644 src/extension/features/assets/thunks/index.ts delete mode 100644 src/extension/features/assets/thunks/updateAssetInformationThunk.ts delete mode 100644 src/extension/features/assets/types/IUpdateAssetInformationPayload.ts delete mode 100644 src/extension/features/assets/types/IUpdateAssetInformationResult.ts delete mode 100644 src/extension/features/assets/types/index.ts delete mode 100644 src/extension/features/assets/utils/getInitialState.ts delete mode 100644 src/extension/features/assets/utils/index.ts create mode 100644 src/extension/features/standard-assets/index.ts create mode 100644 src/extension/features/standard-assets/slice.ts create mode 100644 src/extension/features/standard-assets/thunks/fetchStandardAssetsFromStorageThunk.ts create mode 100644 src/extension/features/standard-assets/thunks/index.ts create mode 100644 src/extension/features/standard-assets/thunks/updateStandardAssetInformationThunk.ts rename src/extension/features/{assets/types/IAssetsState.ts => standard-assets/types/IStandardAssetsState.ts} (51%) create mode 100644 src/extension/features/standard-assets/types/IUpdateStandardAssetInformationPayload.ts create mode 100644 src/extension/features/standard-assets/types/IUpdateStandardAssetInformationResult.ts create mode 100644 src/extension/features/standard-assets/types/index.ts rename src/extension/features/{assets/utils/fetchAssetInformationWithDelay.ts => standard-assets/utils/fetchStandardAssetInformationWithDelay.ts} (85%) create mode 100644 src/extension/features/standard-assets/utils/getInitialState.ts create mode 100644 src/extension/features/standard-assets/utils/index.ts rename src/extension/features/{assets/utils/fetchAssetInformationById.ts => standard-assets/utils/updateStandardAssetInformationById.ts} (51%) delete mode 100644 src/extension/hooks/useAsset/index.ts delete mode 100644 src/extension/hooks/useAsset/types/IUseAssetState.ts delete mode 100644 src/extension/hooks/useAsset/types/index.ts delete mode 100644 src/extension/hooks/useAsset/useAsset.ts delete mode 100644 src/extension/hooks/useAssets/index.ts delete mode 100644 src/extension/hooks/useAssets/useAssets.ts create mode 100644 src/extension/hooks/useOnNewAssets/index.ts create mode 100644 src/extension/hooks/useOnNewAssets/useOnNewAssets.ts create mode 100644 src/extension/hooks/useStandardAssetById/index.ts create mode 100644 src/extension/hooks/useStandardAssetById/types/IUseStandardAssetByIdState.ts create mode 100644 src/extension/hooks/useStandardAssetById/types/index.ts create mode 100644 src/extension/hooks/useStandardAssetById/useStandardAssetById.ts create mode 100644 src/extension/selectors/useSelectAddAssetAccount.ts create mode 100644 src/extension/selectors/useSelectAddAssetArc200Assets.ts create mode 100644 src/extension/selectors/useSelectAddAssetError.ts create mode 100644 src/extension/selectors/useSelectAddAssetFetching.ts create mode 100644 src/extension/selectors/useSelectAddAssetSelectedArc200Asset.ts create mode 100644 src/extension/selectors/useSelectArc200AssetsBySelectedNetwork.ts delete mode 100644 src/extension/selectors/useSelectAssets.ts delete mode 100644 src/extension/selectors/useSelectAssetsByGenesisHash.ts create mode 100644 src/extension/selectors/useSelectFetchingArc200Assets.ts delete mode 100644 src/extension/selectors/useSelectFetchingAssets.ts create mode 100644 src/extension/selectors/useSelectFetchingStandardAssets.ts create mode 100644 src/extension/selectors/useSelectStandardAssetsByGenesisHash.ts create mode 100644 src/extension/selectors/useSelectStandardAssetsBySelectedNetwork.ts create mode 100644 src/extension/selectors/useSelectUpdatingArc200Assets.ts delete mode 100644 src/extension/selectors/useSelectUpdatingAssets.ts create mode 100644 src/extension/selectors/useSelectUpdatingStandardAssets.ts create mode 100644 src/extension/services/Arc200AssetService.ts create mode 100644 src/extension/services/StandardAssetService.ts create mode 100644 src/extension/types/IAlgorandApplication.ts create mode 100644 src/extension/types/IAlgorandApplicationParams.ts create mode 100644 src/extension/types/IAlgorandSearchApplicationsResult.ts create mode 100644 src/extension/types/IAppThunkDispatchReturn.ts create mode 100644 src/extension/types/IArc200Asset.ts create mode 100644 src/extension/types/IArc200AssetHolding.ts create mode 100644 src/extension/types/IArc200AssetInformation.ts delete mode 100644 src/extension/types/IAssetHolding.ts create mode 100644 src/extension/types/IBaseAsyncThunkConfig.ts rename src/extension/types/{IAsset.ts => IStandardAsset.ts} (97%) create mode 100644 src/extension/types/IStandardAssetHolding.ts create mode 100644 src/extension/utils/fetchArc200AssetInformationWithDelay.ts create mode 100644 src/extension/utils/mapArc200AssetFromArc200AssetInformation.ts rename src/extension/utils/{mapAssetFromAlgorandAsset.ts => mapStandardAssetFromAlgorandAsset.ts} (89%) diff --git a/package.json b/package.json index f3162fab..044a7b11 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ ], "private": true, "engines": { - "node": ">=15.0.0" + "node": ">=20.9.0" }, "scripts": { "analyze:chrome": "yarn build:chrome --analyze", @@ -116,7 +116,8 @@ "@walletconnect/core": "^2.8.0", "@walletconnect/utils": "^2.8.0", "@walletconnect/web3wallet": "^1.8.0", - "algosdk": "^2.1.0", + "algosdk": "^2.7.0", + "arc200js": "^2.3.1", "bignumber.js": "^9.1.1", "buffer": "^6.0.3", "chakra-ui-steps": "2.0.4", diff --git a/src/extension/apps/background/App.tsx b/src/extension/apps/background/App.tsx index a8fadad8..d023b816 100644 --- a/src/extension/apps/background/App.tsx +++ b/src/extension/apps/background/App.tsx @@ -9,12 +9,13 @@ import Root from './Root'; // features import { reducer as accountsReducer } from '@extension/features/accounts'; -import { reducer as assetsReducer } from '@extension/features/assets'; +import { reducer as arc200AssetsReducer } from '@extension/features/arc200-assets'; import { reducer as eventsReducer } from '@extension/features/events'; import { reducer as messagesReducer } from '@extension/features/messages'; import { reducer as networksReducer } from '@extension/features/networks'; import { reducer as sessionsReducer } from '@extension/features/sessions'; import { reducer as settingsReducer } from '@extension/features/settings'; +import { reducer as standardAssetsReducer } from '@extension/features/standard-assets'; import { reducer as systemReducer } from '@extension/features/system'; // types @@ -27,12 +28,13 @@ const App: FC = ({ i18next, initialColorMode }: IAppProps) => { const store: Store = makeStore( combineReducers({ accounts: accountsReducer, - assets: assetsReducer, + arc200Assets: arc200AssetsReducer, events: eventsReducer, messages: messagesReducer, networks: networksReducer, sessions: sessionsReducer, settings: settingsReducer, + standardAssets: standardAssetsReducer, system: systemReducer, }) ); diff --git a/src/extension/apps/main/App.tsx b/src/extension/apps/main/App.tsx index e76ff74f..476fe88a 100644 --- a/src/extension/apps/main/App.tsx +++ b/src/extension/apps/main/App.tsx @@ -17,7 +17,8 @@ import { // features import { reducer as accountsReducer } from '@extension/features/accounts'; -import { reducer as assetsReducer } from '@extension/features/assets'; +import { reducer as addAssetReducer } from '@extension/features/add-asset'; +import { reducer as arc200AssetsReducer } from '@extension/features/arc200-assets'; import { reducer as eventsReducer } from '@extension/features/events'; import { reducer as messagesReducer } from '@extension/features/messages'; import { reducer as networksReducer } from '@extension/features/networks'; @@ -25,6 +26,7 @@ import { reducer as notificationsReducer } from '@extension/features/notificatio import { reducer as sendAssetsReducer } from '@extension/features/send-assets'; import { reducer as sessionsReducer } from '@extension/features/sessions'; import { reducer as settingsReducer } from '@extension/features/settings'; +import { reducer as standardAssetsReducer } from '@extension/features/standard-assets'; import { reducer as systemReducer, setSideBar, @@ -86,7 +88,8 @@ const App: FC = ({ i18next, initialColorMode }: IAppProps) => { const store: Store = makeStore( combineReducers({ accounts: accountsReducer, - assets: assetsReducer, + addAsset: addAssetReducer, + arc200Assets: arc200AssetsReducer, events: eventsReducer, messages: messagesReducer, networks: networksReducer, @@ -94,6 +97,7 @@ const App: FC = ({ i18next, initialColorMode }: IAppProps) => { sendAssets: sendAssetsReducer, sessions: sessionsReducer, settings: settingsReducer, + standardAssets: standardAssetsReducer, system: systemReducer, }) ); diff --git a/src/extension/apps/main/Root.tsx b/src/extension/apps/main/Root.tsx index 03fbb1a3..bdb57ae9 100644 --- a/src/extension/apps/main/Root.tsx +++ b/src/extension/apps/main/Root.tsx @@ -3,6 +3,7 @@ import { useDispatch } from 'react-redux'; import { NavigateFunction, Outlet, useNavigate } from 'react-router-dom'; // components +import AddAssetModal from '@extension/components/AddAssetModal'; import ConfirmModal from '@extension/components/ConfirmModal'; import EnableModal from '@extension/components/EnableModal'; import ErrorModal from '@extension/components/ErrorModal'; @@ -13,14 +14,12 @@ import SignTxnsModal from '@extension/components/SignTxnsModal'; import WalletConnectModal from '@extension/components/WalletConnectModal'; // features +import { reset as resetAddAsset } from '@extension/features/add-asset'; import { fetchAccountsFromStorageThunk, startPollingForAccountsThunk, } from '@extension/features/accounts'; -import { - fetchAssetsThunk, - updateAssetInformationThunk, -} from '@extension/features/assets'; +import { fetchArc200AssetsFromStorageThunk } from '@extension/features/arc200-assets'; import { setEnableRequest, setSignBytesRequest, @@ -37,41 +36,32 @@ import { initializeWalletConnectThunk, } from '@extension/features/sessions'; import { fetchSettings } from '@extension/features/settings'; +import { fetchStandardAssetsFromStorageThunk } from '@extension/features/standard-assets'; import { setConfirm, setError, setNavigate } from '@extension/features/system'; // hooks import useOnMainAppMessage from '@extension/hooks/useOnMainAppMessage'; import useOnNetworkConnectivity from '@extension/hooks/useOnNetworkConnectivity'; +import useOnNewAssets from '@extension/hooks/useOnNewAssets'; import useNotifications from '@extension/hooks/useNotifications'; // selectors import { useSelectAccounts, - useSelectAssets, useSelectSelectedNetwork, } from '@extension/selectors'; // types -import { - IAccount, - IAccountInformation, - IAppThunkDispatch, - IAsset, - IAssetHolding, - INetwork, -} from '@extension/types'; - -// utils -import { convertGenesisHashToHex } from '@extension/utils'; +import { IAccount, IAppThunkDispatch, INetwork } from '@extension/types'; const Root: FC = () => { const dispatch: IAppThunkDispatch = useDispatch(); const navigate: NavigateFunction = useNavigate(); - // hooks + // selectors const accounts: IAccount[] = useSelectAccounts(); - const assets: Record | null = useSelectAssets(); const selectedNetwork: INetwork | null = useSelectSelectedNetwork(); // handlers + const handleAddAssetClose = () => dispatch(resetAddAsset()); const handleConfirmClose = () => dispatch(setConfirm(null)); const handleEnableModalClose = () => dispatch(setEnableRequest(null)); const handleErrorModalClose = () => dispatch(setError(null)); @@ -86,7 +76,8 @@ const Root: FC = () => { dispatch(setNavigate(navigate)); dispatch(fetchSettings()); dispatch(fetchSessionsThunk()); - dispatch(fetchAssetsThunk()); + dispatch(fetchStandardAssetsFromStorageThunk()); + dispatch(fetchArc200AssetsFromStorageThunk()); dispatch(initializeWalletConnectThunk()); dispatch(startPollingForAccountsThunk()); dispatch(startPollingForTransactionsParamsThunk()); @@ -108,39 +99,7 @@ const Root: FC = () => { dispatch(fetchTransactionParamsFromStorageThunk()); } }, [selectedNetwork]); - // whenever the accounts are updated, check if any new assets exist in the account - useEffect(() => { - if (accounts.length > 0 && assets && selectedNetwork) { - accounts.forEach((account) => { - const encodedGenesisHash: string = convertGenesisHashToHex( - selectedNetwork.genesisHash - ).toUpperCase(); - const accountInformation: IAccountInformation | null = - account.networkInformation[encodedGenesisHash] || null; - let newAssets: IAssetHolding[]; - - if (accountInformation) { - // filter out any new assets - newAssets = accountInformation.assetHoldings.filter( - (assetHolding) => - !assets[encodedGenesisHash].some( - (value) => value.id === assetHolding.id - ) - ); - - // if we have any new assets, update the information - if (newAssets.length > 0) { - dispatch( - updateAssetInformationThunk({ - ids: newAssets.map((value) => value.id), - network: selectedNetwork, - }) - ); - } - } - }); - } - }, [accounts]); + useOnNewAssets(); // handle new assets added useNotifications(); // handle notifications useOnNetworkConnectivity(); // listen to network connectivity useOnMainAppMessage(); // handle incoming messages @@ -152,6 +111,7 @@ const Root: FC = () => { + diff --git a/src/extension/components/AccountAssetsTab/AccountAssetsTab.tsx b/src/extension/components/AccountAssetsTab/AccountAssetsTab.tsx deleted file mode 100644 index 657d1e36..00000000 --- a/src/extension/components/AccountAssetsTab/AccountAssetsTab.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import { - Button, - HStack, - Icon, - Skeleton, - SkeletonCircle, - Spacer, - TabPanel, - Text, - Tooltip, - VStack, -} from '@chakra-ui/react'; -import { faker } from '@faker-js/faker'; -import BigNumber from 'bignumber.js'; -import React, { FC, ReactNode } from 'react'; -import { useTranslation } from 'react-i18next'; -import { IoAdd, IoChevronForward } from 'react-icons/io5'; -import { Link } from 'react-router-dom'; - -// components -import AssetAvatar from '@extension/components/AssetAvatar'; -import AssetIcon from '@extension/components/AssetIcon'; -import EmptyState from '@extension/components/EmptyState'; - -// constants -import { ACCOUNTS_ROUTE, ASSETS_ROUTE } from '@extension/constants'; - -// hooks -import useAccountInformation from '@extension/hooks/useAccountInformation'; -import useAssets from '@extension/hooks/useAssets'; -import useButtonHoverBackgroundColor from '@extension/hooks/useButtonHoverBackgroundColor'; -import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; -import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; -import useSubTextColor from '@extension/hooks/useSubTextColor'; - -// selectors -import { - useSelectFetchingAssets, - useSelectSelectedNetwork, - useSelectUpdatingAssets, -} from '@extension/selectors'; - -// services -import { AccountService } from '@extension/services'; - -// types -import { - IAccount, - IAccountInformation, - IAsset, - INetwork, -} from '@extension/types'; - -// utils -import { convertToStandardUnit, formatCurrencyUnit } from '@common/utils'; - -interface IProps { - account: IAccount; -} - -const AccountAssetsTab: FC = ({ account }: IProps) => { - const { t } = useTranslation(); - // selectors - const fetching: boolean = useSelectFetchingAssets(); - const selectedNetwork: INetwork | null = useSelectSelectedNetwork(); - const updating: boolean = useSelectUpdatingAssets(); - // hooks - const accountInformation: IAccountInformation | null = useAccountInformation( - account.id - ); - const assets: IAsset[] = useAssets(); - const buttonHoverBackgroundColor: string = useButtonHoverBackgroundColor(); - const defaultTextColor: string = useDefaultTextColor(); - const primaryButtonTextColor: string = usePrimaryButtonTextColor(); - const subTextColor: string = useSubTextColor(); - const handleAddAssetClick = () => console.log('add an asset!'); - const renderContent = () => { - let assetNodes: ReactNode[] = []; - - if (fetching || updating) { - return Array.from({ length: 3 }, (_, index) => ( - - )); - } - - if (accountInformation && accountInformation.assetHoldings.length > 0) { - assetNodes = accountInformation.assetHoldings.reduce( - (acc, assetHolding, currentIndex) => { - const asset: IAsset | null = - assets.find((value) => value.id === assetHolding.id) || null; - let standardUnitAmount: BigNumber; - - if (!asset) { - return acc; - } - - standardUnitAmount = convertToStandardUnit( - new BigNumber(assetHolding.amount), - asset.decimals - ); - - return [ - ...acc, - - - , - ]; - }, - [] - ); - } - - return assetNodes.length > 0 ? ( - assetNodes - ) : ( - <> - {/*empty state*/} - - ('buttons.addAsset'), - onClick: handleAddAssetClick, - }} - description={t('captions.noAssetsFound')} - text={t('headings.noAssetsFound')} - /> - - - ); - }; - - return ( - - - {renderContent()} - - - ); -}; - -export default AccountAssetsTab; diff --git a/src/extension/components/AccountAssetsTab/index.ts b/src/extension/components/AccountAssetsTab/index.ts deleted file mode 100644 index ccee276e..00000000 --- a/src/extension/components/AccountAssetsTab/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './AccountAssetsTab'; diff --git a/src/extension/components/AddAssetModal/AddAssetArc200AssetItem.tsx b/src/extension/components/AddAssetModal/AddAssetArc200AssetItem.tsx new file mode 100644 index 00000000..45d39ef1 --- /dev/null +++ b/src/extension/components/AddAssetModal/AddAssetArc200AssetItem.tsx @@ -0,0 +1,126 @@ +import { Button, HStack, Icon, Text, Tooltip, VStack } from '@chakra-ui/react'; +import React, { FC } from 'react'; +import { IoChevronForward } from 'react-icons/io5'; + +// components +import AssetAvatar from '@extension/components/AssetAvatar'; +import AssetBadge from '@extension/components/AssetBadge'; +import AssetIcon from '@extension/components/AssetIcon'; + +// constants +import { DEFAULT_GAP, TAB_ITEM_HEIGHT } from '@extension/constants'; + +// enums +import { AssetTypeEnum } from '@extension/enums'; + +// hooks +import useButtonHoverBackgroundColor from '@extension/hooks/useButtonHoverBackgroundColor'; +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; +import useSubTextColor from '@extension/hooks/useSubTextColor'; + +// types +import { IArc200Asset, INetwork } from '@extension/types'; + +interface IProps { + asset: IArc200Asset; + network: INetwork; + onClick: (asset: IArc200Asset) => void; +} + +const AddAssetArc200AssetItem: FC = ({ + asset, + network, + onClick, +}: IProps) => { + // hooks + const buttonHoverBackgroundColor: string = useButtonHoverBackgroundColor(); + const defaultTextColor: string = useDefaultTextColor(); + const primaryButtonTextColor: string = usePrimaryButtonTextColor(); + const subTextColor: string = useSubTextColor(); + // handlers + const handleOnClick = () => onClick(asset); + + return ( + + + + ); +}; + +export default AddAssetArc200AssetItem; diff --git a/src/extension/components/AddAssetModal/AddAssetModal.tsx b/src/extension/components/AddAssetModal/AddAssetModal.tsx new file mode 100644 index 00000000..a4b3f0c5 --- /dev/null +++ b/src/extension/components/AddAssetModal/AddAssetModal.tsx @@ -0,0 +1,334 @@ +import { + Heading, + HStack, + Input, + InputGroup, + InputRightElement, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Spinner, + Text, + VStack, +} from '@chakra-ui/react'; +import React, { ChangeEvent, FC, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { IoCloseOutline } from 'react-icons/io5'; +import { useDispatch } from 'react-redux'; + +// components +import Button from '@extension/components/Button'; +import IconButton from '@extension/components/IconButton'; +import AddAssetArc200AssetItem from './AddAssetArc200AssetItem'; +import AddAssetModalArc200SummaryContent from './AddAssetModalArc200SummaryContent'; + +// constants +import { DEFAULT_GAP } from '@extension/constants'; + +// enums +import { ErrorCodeEnum } from '@extension/enums'; + +// errors +import { BaseExtensionError } from '@extension/errors'; + +// features +import { addArc200AssetHoldingThunk } from '@extension/features/accounts'; +import { + clearAssets, + IQueryByIdAsyncThunkConfig, + IQueryByIdResult, + queryByIdThunk, + reset, + setSelectedArc200Asset, +} from '@extension/features/add-asset'; +import { create as createNotification } from '@extension/features/notifications'; + +// hooks +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import usePrimaryColor from '@extension/hooks/usePrimaryColor'; +import usePrimaryColorScheme from '@extension/hooks/usePrimaryColorScheme'; + +// selectors +import { + useSelectAddAssetAccount, + useSelectAddAssetArc200Assets, + useSelectAddAssetError, + useSelectAddAssetFetching, + useSelectAddAssetSelectedArc200Asset, + useSelectPreferredBlockExplorer, + useSelectSelectedNetwork, +} from '@extension/selectors'; + +// theme +import { theme } from '@extension/theme'; + +// types +import { + IAccount, + IAppThunkDispatch, + IAppThunkDispatchReturn, + IArc200Asset, + IExplorer, + INetworkWithTransactionParams, +} from '@extension/types'; + +interface IProps { + onClose: () => void; +} + +const AddAssetModal: FC = ({ onClose }: IProps) => { + const { t } = useTranslation(); + const dispatch: IAppThunkDispatch = useDispatch(); + // selectors + const account: IAccount | null = useSelectAddAssetAccount(); + const arc200Assets: IArc200Asset[] = useSelectAddAssetArc200Assets(); + const error: BaseExtensionError | null = useSelectAddAssetError(); + const explorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const fetching: boolean = useSelectAddAssetFetching(); + const selectedNetwork: INetworkWithTransactionParams | null = + useSelectSelectedNetwork(); + const selectedArc200Asset: IArc200Asset | null = + useSelectAddAssetSelectedArc200Asset(); + // hooks + const defaultTextColor: string = useDefaultTextColor(); + const primaryColor: string = usePrimaryColor(); + const primaryColorScheme: string = usePrimaryColorScheme(); + // state + const [query, setQuery] = useState(''); + const [queryByIdDispatch, setQueryByIdDispatch] = + useState | null>(null); + // misc + const isOpen: boolean = !!account; + // handlers + const handleAddAssetClick = async () => { + let updatedAccount: IAccount | null; + + if (!selectedNetwork || !account || !selectedArc200Asset) { + return; + } + + try { + updatedAccount = await dispatch( + addArc200AssetHoldingThunk({ + accountId: account.id, + appId: selectedArc200Asset.id, + genesisHash: selectedNetwork.genesisHash, + }) + ).unwrap(); + + if (updatedAccount && selectedArc200Asset) { + dispatch( + createNotification({ + title: t('headings.addedAsset', { + symbol: selectedArc200Asset.symbol, + }), + type: 'success', + }) + ); + } + + handleClose(); + } catch (error) { + dispatch( + createNotification({ + description: t('errors.descriptions.code', { + context: error.code, + }), + ephemeral: true, + title: t('errors.titles.code', { context: error.code }), + type: 'error', + }) + ); + } + }; + const handleCancelClick = () => handleClose(); + const handleClearQuery = () => { + setQuery(''); + dispatch(clearAssets()); + }; + const handleClose = () => { + setQuery(''); + setQueryByIdDispatch(null); + onClose(); + }; + const handleKeyUp = () => { + // if we have only numbers, we have an asset/app id + if (new RegExp(/^\d+$/).test(query)) { + // abort any previous request + if (queryByIdDispatch) { + queryByIdDispatch.abort(); + } + + setQueryByIdDispatch(dispatch(queryByIdThunk(query))); + + return; + } + }; + const handleOnQueryChange = (event: ChangeEvent) => { + setQuery(event.target.value); + }; + const handlePreviousClick = () => { + dispatch(setSelectedArc200Asset(null)); + }; + const handleSelectArc200AssetClick = (asset: IArc200Asset) => + dispatch(setSelectedArc200Asset(asset)); + // renders + const renderContent = () => { + if (selectedNetwork) { + if (selectedArc200Asset) { + return ( + + ); + } + } + + return ( + + + {t('captions.addAsset')} + + + + + + + {fetching && ( + + )} + {!fetching && query.length > 0 && ( + + )} + + + + + {selectedNetwork && + arc200Assets.map((value, index) => ( + + ))} + + + ); + }; + const renderFooter = () => { + if (selectedArc200Asset) { + return ( + + + + + + ); + } + + return ( + + ); + }; + + useEffect(() => { + if (error) { + switch (error.code) { + case ErrorCodeEnum.OfflineError: + dispatch( + createNotification({ + ephemeral: true, + title: t('headings.offline'), + type: 'error', + }) + ); + break; + default: + dispatch( + createNotification({ + description: `Please contact support with code "${error.code}" and describe what happened.`, + ephemeral: true, + title: t('errors.titles.code'), + type: 'error', + }) + ); + break; + } + } + }, [error]); + + return ( + + + + + {t('headings.addAsset')} + + + + + {renderContent()} + + + {renderFooter()} + + + ); +}; + +export default AddAssetModal; diff --git a/src/extension/components/AddAssetModal/AddAssetModalArc200SummaryContent.tsx b/src/extension/components/AddAssetModal/AddAssetModalArc200SummaryContent.tsx new file mode 100644 index 00000000..a6584d2e --- /dev/null +++ b/src/extension/components/AddAssetModal/AddAssetModalArc200SummaryContent.tsx @@ -0,0 +1,186 @@ +import { HStack, Text, Tooltip, useDisclosure, VStack } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; + +// components +import AssetAvatar from '@extension/components/AssetAvatar'; +import AssetBadge from '@extension/components/AssetBadge'; +import AssetIcon from '@extension/components/AssetIcon'; +import CopyIconButton from '@extension/components/CopyIconButton'; +import MoreInformationAccordion from '@extension/components/MoreInformationAccordion'; +import OpenTabIconButton from '@extension/components/OpenTabIconButton'; +import PageItem, { ITEM_HEIGHT } from '@extension/components/PageItem'; + +// constants +import { DEFAULT_GAP } from '@extension/constants'; + +// enums +import { AssetTypeEnum } from '@extension/enums'; + +// hooks +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; +import useSubTextColor from '@extension/hooks/useSubTextColor'; + +// types +import { + IArc200Asset, + IExplorer, + INetworkWithTransactionParams, +} from '@extension/types'; +import { convertToStandardUnit, formatCurrencyUnit } from '@common/utils'; + +interface IProps { + asset: IArc200Asset; + explorer: IExplorer | null; + network: INetworkWithTransactionParams; +} + +const AddAssetModalArc200SummaryContent: FC = ({ + asset, + explorer, + network, +}: IProps) => { + const { t } = useTranslation(); + const { isOpen, onOpen, onClose } = useDisclosure(); + // hooks + const defaultTextColor: string = useDefaultTextColor(); + const primaryButtonTextColor: string = usePrimaryButtonTextColor(); + const subTextColor: string = useSubTextColor(); + // handlers + const handleMoreInformationToggle = (value: boolean) => + value ? onOpen() : onClose(); + + return ( + + + {/*asset icon*/} + + } + size="md" + /> + + {/*symbol*/} + + + {asset.symbol} + + + + + {/*application id*/} + ('labels.applicationId')}> + + + {asset.id} + + + ( + 'captions.arc200ApplicationIdCopied' + )} + size="sm" + value={asset.id} + /> + + {explorer && ( + ('captions.openOn', { + name: explorer.canonicalName, + })} + url={`${explorer.baseUrl}${explorer.applicationPath}/${asset.id}`} + /> + )} + + + + {/*name*/} + ('labels.name')}> + + + {asset.name} + + + + + {/*type*/} + ('labels.type')}> + + + + + + {/*decimals*/} + ('labels.decimals')}> + + {asset.decimals.toString()} + + + + {/*total supply*/} + ('labels.totalSupply')}> + + + {formatCurrencyUnit( + convertToStandardUnit( + new BigNumber(asset.totalSupply), + asset.decimals + ), + asset.decimals + )} + + + + + + + + + ); +}; + +export default AddAssetModalArc200SummaryContent; diff --git a/src/extension/components/AddAssetModal/AddAssetStandardAssetItem.tsx b/src/extension/components/AddAssetModal/AddAssetStandardAssetItem.tsx new file mode 100644 index 00000000..1e4127e9 --- /dev/null +++ b/src/extension/components/AddAssetModal/AddAssetStandardAssetItem.tsx @@ -0,0 +1,130 @@ +import { + Button, + ColorMode, + HStack, + Icon, + Tag, + TagLabel, + Text, + Tooltip, + VStack, +} from '@chakra-ui/react'; +import React, { FC } from 'react'; +import { IoChevronForward } from 'react-icons/io5'; + +// components +import AssetAvatar from '@extension/components/AssetAvatar'; +import AssetIcon from '@extension/components/AssetIcon'; + +// constants +import { DEFAULT_GAP, TAB_ITEM_HEIGHT } from '@extension/constants'; + +// hooks +import useButtonHoverBackgroundColor from '@extension/hooks/useButtonHoverBackgroundColor'; +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; +import useSubTextColor from '@extension/hooks/useSubTextColor'; + +// selectors +import { useSelectColorMode } from '@extension/selectors'; + +// types +import { INetwork, IStandardAsset } from '@extension/types'; + +interface IProps { + asset: IStandardAsset; + network: INetwork; +} + +const AddAssetStandardAssetItem: FC = ({ asset, network }: IProps) => { + // selectors + const colorMode: ColorMode = useSelectColorMode(); + // hooks + const buttonHoverBackgroundColor: string = useButtonHoverBackgroundColor(); + const defaultTextColor: string = useDefaultTextColor(); + const primaryButtonTextColor: string = usePrimaryButtonTextColor(); + const subTextColor: string = useSubTextColor(); + + return ( + + + + ); +}; + +export default AddAssetStandardAssetItem; diff --git a/src/extension/components/AddAssetModal/index.ts b/src/extension/components/AddAssetModal/index.ts new file mode 100644 index 00000000..482ae344 --- /dev/null +++ b/src/extension/components/AddAssetModal/index.ts @@ -0,0 +1 @@ +export { default } from './AddAssetModal'; diff --git a/src/extension/components/AssetAvatar/AssetAvatar.tsx b/src/extension/components/AssetAvatar/AssetAvatar.tsx index 0f64f1ef..22395cd1 100644 --- a/src/extension/components/AssetAvatar/AssetAvatar.tsx +++ b/src/extension/components/AssetAvatar/AssetAvatar.tsx @@ -6,10 +6,10 @@ import { IoCheckmarkOutline } from 'react-icons/io5'; import usePrimaryColor from '@extension/hooks/usePrimaryColor'; // types -import { IAsset } from '@extension/types'; +import { IArc200Asset, IStandardAsset } from '@extension/types'; interface IProps extends AvatarProps { - asset: IAsset; + asset: IArc200Asset | IStandardAsset; fallbackIcon: ReactElement; } diff --git a/src/extension/components/AssetBadge/AssetBadge.tsx b/src/extension/components/AssetBadge/AssetBadge.tsx new file mode 100644 index 00000000..061946d6 --- /dev/null +++ b/src/extension/components/AssetBadge/AssetBadge.tsx @@ -0,0 +1,44 @@ +import { ColorMode, HStack, Tag, TagLabel } from '@chakra-ui/react'; +import React, { FC } from 'react'; + +// enums +import { AssetTypeEnum } from '@extension/enums'; + +// selectors +import { useSelectColorMode } from '@extension/selectors'; + +interface IProps { + size?: string; + type: AssetTypeEnum; +} + +const AssetBadge: FC = ({ size = 'sm', type }: IProps) => { + // hooks + const colorMode: ColorMode = useSelectColorMode(); + + switch (type) { + case AssetTypeEnum.Arc200: + return ( + + ARC200 + + ); + case AssetTypeEnum.Standard: + default: + return ( + + ASA + + ); + } +}; + +export default AssetBadge; diff --git a/src/extension/components/AssetBadge/index.ts b/src/extension/components/AssetBadge/index.ts new file mode 100644 index 00000000..196f5cf0 --- /dev/null +++ b/src/extension/components/AssetBadge/index.ts @@ -0,0 +1 @@ +export { default } from './AssetBadge'; diff --git a/src/extension/components/AssetSelect/AssetSelect.tsx b/src/extension/components/AssetSelect/AssetSelect.tsx index 12fc4c33..8aeaf4c2 100644 --- a/src/extension/components/AssetSelect/AssetSelect.tsx +++ b/src/extension/components/AssetSelect/AssetSelect.tsx @@ -19,7 +19,7 @@ import { theme } from '@extension/theme'; import { IAccount, IAccountInformation, - IAsset, + IStandardAsset, INetworkWithTransactionParams, } from '@extension/types'; import { IOption } from './types'; @@ -32,11 +32,11 @@ import { interface IProps { account: IAccount; - assets: IAsset[]; + assets: IStandardAsset[]; includeNativeCurrency?: boolean; network: INetworkWithTransactionParams; - onAssetChange: (value: IAsset) => void; - value: IAsset; + onAssetChange: (value: IStandardAsset) => void; + value: IStandardAsset; width?: string | number; } @@ -71,10 +71,10 @@ const AssetSelect: FC = ({ account.networkInformation[ convertGenesisHashToHex(network.genesisHash).toUpperCase() ] || null; - const selectableAssets: IAsset[] = - accountInformation?.assetHoldings.reduce( + const selectableAssets: IStandardAsset[] = + accountInformation?.standardAssetHoldings.reduce( (acc, assetHolding) => { - const asset: IAsset | null = + const asset: IStandardAsset | null = assets.find((value) => value.id === assetHolding.id) || null; if (!asset) { diff --git a/src/extension/components/AssetSelect/AssetSelectOption.tsx b/src/extension/components/AssetSelect/AssetSelectOption.tsx index 6d8f50fc..fd9509f0 100644 --- a/src/extension/components/AssetSelect/AssetSelectOption.tsx +++ b/src/extension/components/AssetSelect/AssetSelectOption.tsx @@ -20,10 +20,13 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; import { theme } from '@extension/theme'; // types -import { IAsset, INetworkWithTransactionParams } from '@extension/types'; +import { + IStandardAsset, + INetworkWithTransactionParams, +} from '@extension/types'; interface IProps { - asset: IAsset; + asset: IStandardAsset; isSelected: boolean; onClick?: ReactEventHandler; network: INetworkWithTransactionParams; diff --git a/src/extension/components/AssetSelect/AssetSelectSingleValue.tsx b/src/extension/components/AssetSelect/AssetSelectSingleValue.tsx index 9590c9a2..4069499b 100644 --- a/src/extension/components/AssetSelect/AssetSelectSingleValue.tsx +++ b/src/extension/components/AssetSelect/AssetSelectSingleValue.tsx @@ -13,11 +13,14 @@ import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; // types -import { IAsset, INetworkWithTransactionParams } from '@extension/types'; +import { + IStandardAsset, + INetworkWithTransactionParams, +} from '@extension/types'; import { DEFAULT_GAP } from '@extension/constants'; interface IProps { - asset: IAsset; + asset: IStandardAsset; network: INetworkWithTransactionParams; } diff --git a/src/extension/components/AssetSelect/types/IOption.ts b/src/extension/components/AssetSelect/types/IOption.ts index 43e0601e..3f93d334 100644 --- a/src/extension/components/AssetSelect/types/IOption.ts +++ b/src/extension/components/AssetSelect/types/IOption.ts @@ -1,7 +1,7 @@ -import { IAsset } from '@extension/types'; +import { IStandardAsset } from '@extension/types'; interface IOption { - asset: IAsset; + asset: IStandardAsset; value: string; } diff --git a/src/extension/components/AssetsTab/AssetTabArc200AssetItem.tsx b/src/extension/components/AssetsTab/AssetTabArc200AssetItem.tsx new file mode 100644 index 00000000..67d348d2 --- /dev/null +++ b/src/extension/components/AssetsTab/AssetTabArc200AssetItem.tsx @@ -0,0 +1,163 @@ +import { + Button, + ColorMode, + HStack, + Icon, + Text, + Tooltip, + VStack, +} from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React, { FC } from 'react'; +import { IoChevronForward } from 'react-icons/io5'; +import { Link } from 'react-router-dom'; + +// components +import AssetAvatar from '@extension/components/AssetAvatar'; +import AssetBadge from '@extension/components/AssetBadge'; +import AssetIcon from '@extension/components/AssetIcon'; + +// constants +import { + ACCOUNTS_ROUTE, + ASSETS_ROUTE, + DEFAULT_GAP, + TAB_ITEM_HEIGHT, +} from '@extension/constants'; + +// enums +import { AssetTypeEnum } from '@extension/enums'; + +// hooks +import useButtonHoverBackgroundColor from '@extension/hooks/useButtonHoverBackgroundColor'; +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; +import useSubTextColor from '@extension/hooks/useSubTextColor'; + +// selectors +import { useSelectColorMode } from '@extension/selectors'; + +// services +import { AccountService } from '@extension/services'; + +// types +import { IAccount, IArc200Asset, INetwork } from '@extension/types'; + +// utils +import { convertToStandardUnit, formatCurrencyUnit } from '@common/utils'; + +interface IProps { + account: IAccount; + amount: string; + arc200Asset: IArc200Asset; + network: INetwork; +} + +const AssetTabArc200AssetItem: FC = ({ + account, + amount, + arc200Asset, + network, +}: IProps) => { + // selectors + const colorMode: ColorMode = useSelectColorMode(); + // hooks + const buttonHoverBackgroundColor: string = useButtonHoverBackgroundColor(); + const defaultTextColor: string = useDefaultTextColor(); + const primaryButtonTextColor: string = usePrimaryButtonTextColor(); + const subTextColor: string = useSubTextColor(); + // misc + const standardUnitAmount: BigNumber = convertToStandardUnit( + new BigNumber(amount), + arc200Asset.decimals + ); + + return ( + + + + ); +}; + +export default AssetTabArc200AssetItem; diff --git a/src/extension/components/AssetsTab/AssetTabLoadingItem.tsx b/src/extension/components/AssetsTab/AssetTabLoadingItem.tsx new file mode 100644 index 00000000..d3c055f2 --- /dev/null +++ b/src/extension/components/AssetsTab/AssetTabLoadingItem.tsx @@ -0,0 +1,50 @@ +import { + Button, + HStack, + Skeleton, + SkeletonCircle, + Text, +} from '@chakra-ui/react'; +import { faker } from '@faker-js/faker'; +import React, { FC } from 'react'; + +// constants +import { DEFAULT_GAP, TAB_ITEM_HEIGHT } from '@extension/constants'; + +// hooks +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; + +const AssetTabLoadingItem: FC = () => { + // hooks + const defaultTextColor: string = useDefaultTextColor(); + + return ( + + ); +}; + +export default AssetTabLoadingItem; diff --git a/src/extension/components/AssetsTab/AssetTabStandardAssetItem.tsx b/src/extension/components/AssetsTab/AssetTabStandardAssetItem.tsx new file mode 100644 index 00000000..dcab575c --- /dev/null +++ b/src/extension/components/AssetsTab/AssetTabStandardAssetItem.tsx @@ -0,0 +1,163 @@ +import { + Button, + ColorMode, + HStack, + Icon, + Text, + Tooltip, + VStack, +} from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React, { FC } from 'react'; +import { IoChevronForward } from 'react-icons/io5'; +import { Link } from 'react-router-dom'; + +// components +import AssetAvatar from '@extension/components/AssetAvatar'; +import AssetBadge from '@extension/components/AssetBadge'; +import AssetIcon from '@extension/components/AssetIcon'; + +// constants +import { + ACCOUNTS_ROUTE, + ASSETS_ROUTE, + DEFAULT_GAP, + TAB_ITEM_HEIGHT, +} from '@extension/constants'; + +// enums +import { AssetTypeEnum } from '@extension/enums'; + +// hooks +import useButtonHoverBackgroundColor from '@extension/hooks/useButtonHoverBackgroundColor'; +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; +import useSubTextColor from '@extension/hooks/useSubTextColor'; + +// selectors +import { useSelectColorMode } from '@extension/selectors'; + +// services +import { AccountService } from '@extension/services'; + +// types +import { IAccount, INetwork, IStandardAsset } from '@extension/types'; + +// utils +import { convertToStandardUnit, formatCurrencyUnit } from '@common/utils'; + +interface IProps { + account: IAccount; + amount: string; + network: INetwork; + standardAsset: IStandardAsset; +} + +const AssetTabStandardAssetItem: FC = ({ + account, + amount, + network, + standardAsset, +}: IProps) => { + // selectors + const colorMode: ColorMode = useSelectColorMode(); + // hooks + const buttonHoverBackgroundColor: string = useButtonHoverBackgroundColor(); + const defaultTextColor: string = useDefaultTextColor(); + const primaryButtonTextColor: string = usePrimaryButtonTextColor(); + const subTextColor: string = useSubTextColor(); + // misc + const standardUnitAmount: BigNumber = convertToStandardUnit( + new BigNumber(amount), + standardAsset.decimals + ); + + return ( + + + + ); +}; + +export default AssetTabStandardAssetItem; diff --git a/src/extension/components/AssetsTab/AssetsTab.tsx b/src/extension/components/AssetsTab/AssetsTab.tsx new file mode 100644 index 00000000..89f91fbc --- /dev/null +++ b/src/extension/components/AssetsTab/AssetsTab.tsx @@ -0,0 +1,233 @@ +import { + HStack, + Spacer, + Spinner, + TabPanel, + Tooltip, + VStack, +} from '@chakra-ui/react'; +import React, { FC, ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; +import { IoAdd } from 'react-icons/io5'; +import { useDispatch } from 'react-redux'; + +// components +import Button from '@extension/components/Button'; +import EmptyState from '@extension/components/EmptyState'; +import AssetTabArc200AssetItem from './AssetTabArc200AssetItem'; +import AssetTabLoadingItem from './AssetTabLoadingItem'; +import AssetTabStandardAssetItem from './AssetTabStandardAssetItem'; + +// constants +import { DEFAULT_GAP } from '@extension/constants'; + +// features +import { setAccountId as setAddAssetAccountId } from '@extension/features/add-asset'; + +// hooks +import useAccountInformation from '@extension/hooks/useAccountInformation'; +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; + +// selectors +import { + useSelectArc200AssetsBySelectedNetwork, + useSelectFetchingArc200Assets, + useSelectFetchingStandardAssets, + useSelectStandardAssetsBySelectedNetwork, + useSelectSelectedNetwork, + useSelectUpdatingArc200Assets, + useSelectUpdatingStandardAssets, +} from '@extension/selectors'; + +// types +import { + IAccount, + IAccountInformation, + IStandardAsset, + INetwork, + IArc200Asset, + IAppThunkDispatch, +} from '@extension/types'; + +interface IAssetHolding { + amount: string; + id: string; + isArc200: boolean; +} +interface IProps { + account: IAccount; +} + +const AssetsTab: FC = ({ account }: IProps) => { + const { t } = useTranslation(); + const dispatch: IAppThunkDispatch = useDispatch(); + // selectors + const arc200Assets: IArc200Asset[] = useSelectArc200AssetsBySelectedNetwork(); + const fetchingArc200Assets: boolean = useSelectFetchingArc200Assets(); + const fetchingStandardAssets: boolean = useSelectFetchingStandardAssets(); + const selectedNetwork: INetwork | null = useSelectSelectedNetwork(); + const standardAssets: IStandardAsset[] = + useSelectStandardAssetsBySelectedNetwork(); + const updatingArc200Assets: boolean = useSelectUpdatingArc200Assets(); + const updatingStandardAssets: boolean = useSelectUpdatingStandardAssets(); + // hooks + const defaultTextColor: string = useDefaultTextColor(); + const accountInformation: IAccountInformation | null = useAccountInformation( + account.id + ); + // misc + const allAssetHoldings: IAssetHolding[] = accountInformation + ? [ + ...accountInformation.arc200AssetHoldings.map(({ amount, id }) => ({ + amount, + id, + isArc200: true, + })), + ...accountInformation.standardAssetHoldings.map(({ amount, id }) => ({ + amount, + id, + isArc200: false, + })), + ] + : []; + // handlers + const handleAddAssetClick = () => dispatch(setAddAssetAccountId(account.id)); + // renders + const renderContent = () => { + let assetNodes: ReactNode[] = []; + + if (fetchingArc200Assets || fetchingStandardAssets) { + return Array.from({ length: 3 }, (_, index) => ( + + )); + } + + if (selectedNetwork && accountInformation && allAssetHoldings.length > 0) { + assetNodes = allAssetHoldings.reduce( + (acc, { amount, id, isArc200 }, currentIndex) => { + const key: string = `asset-tab-item-${currentIndex}`; + let arc200Asset: IArc200Asset | null; + let standardAsset: IStandardAsset | null; + + // for standard assets + if (!isArc200) { + standardAsset = + standardAssets.find((value) => value.id === id) || null; + + if (!standardAsset) { + return acc; + } + + return [ + ...acc, + , + ]; + } + + arc200Asset = arc200Assets.find((value) => value.id === id) || null; + + if (!arc200Asset) { + return acc; + } + + return [ + ...acc, + , + ]; + }, + [] + ); + } + + return assetNodes.length > 0 ? ( + + {/*controls*/} + + {/*updating asset spinner*/} + {updatingArc200Assets || + (updatingStandardAssets && ( + ('captions.updatingAssetInformation')} + > + + + ))} + + + + {/*add asset*/} + + + + {/*asset list*/} + + {assetNodes} + + + ) : ( + + + + {/*empty state*/} + ('buttons.addAsset'), + onClick: handleAddAssetClick, + }} + description={t('captions.noAssetsFound')} + text={t('headings.noAssetsFound')} + /> + + + + ); + }; + + return ( + + {renderContent()} + + ); +}; + +export default AssetsTab; diff --git a/src/extension/components/AssetsTab/index.ts b/src/extension/components/AssetsTab/index.ts new file mode 100644 index 00000000..e5b35949 --- /dev/null +++ b/src/extension/components/AssetsTab/index.ts @@ -0,0 +1 @@ +export { default } from './AssetsTab'; diff --git a/src/extension/components/InnerTransactionAccordion/AssetFreezeInnerTransactionAccordionItem.tsx b/src/extension/components/InnerTransactionAccordion/AssetFreezeInnerTransactionAccordionItem.tsx index 9612bd9d..a605c139 100644 --- a/src/extension/components/InnerTransactionAccordion/AssetFreezeInnerTransactionAccordionItem.tsx +++ b/src/extension/components/InnerTransactionAccordion/AssetFreezeInnerTransactionAccordionItem.tsx @@ -24,7 +24,7 @@ import PageItem from '@extension/components/PageItem'; import { TransactionTypeEnum } from '@extension/enums'; // hooks -import useAsset from '@extension/hooks/useAsset'; +import useStandardAssetById from '@extension/hooks/useStandardAssetById'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import useSubTextColor from '@extension/hooks/useSubTextColor'; @@ -66,7 +66,7 @@ const AssetFreezeInnerTransactionAccordionItem: FC = ({ const accounts: IAccount[] = useSelectAccounts(); const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); // hooks - const { asset, updating } = useAsset(transaction.assetId); + const { standardAsset, updating } = useStandardAssetById(transaction.assetId); const defaultTextColor: string = useDefaultTextColor(); const subTextColor: string = useSubTextColor(); // misc @@ -88,7 +88,7 @@ const AssetFreezeInnerTransactionAccordionItem: FC = ({ {/*asset id*/} ('labels.assetId')}> - {!asset || updating ? ( + {!standardAsset || updating ? ( 12345678 @@ -97,13 +97,13 @@ const AssetFreezeInnerTransactionAccordionItem: FC = ({ ) : ( - {asset.id} + {standardAsset.id} ('captions.assetIdCopied')} size="xs" - value={asset.id} + value={standardAsset.id} /> {explorer && ( = ({ tooltipLabel={t('captions.openOn', { name: explorer.canonicalName, })} - url={`${explorer.baseUrl}${explorer.assetPath}/${asset.id}`} + url={`${explorer.baseUrl}${explorer.assetPath}/${standardAsset.id}`} /> )} diff --git a/src/extension/components/InnerTransactionAccordion/AssetTransferInnerTransactionAccordionItem.tsx b/src/extension/components/InnerTransactionAccordion/AssetTransferInnerTransactionAccordionItem.tsx index b0147acf..30205fb6 100644 --- a/src/extension/components/InnerTransactionAccordion/AssetTransferInnerTransactionAccordionItem.tsx +++ b/src/extension/components/InnerTransactionAccordion/AssetTransferInnerTransactionAccordionItem.tsx @@ -23,8 +23,8 @@ import OpenTabIconButton from '@extension/components/OpenTabIconButton'; import PageItem from '@extension/components/PageItem'; // hooks -import useAsset from '@extension/hooks/useAsset'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import useStandardAssetById from '@extension/hooks/useStandardAssetById'; import useSubTextColor from '@extension/hooks/useSubTextColor'; // selectors @@ -69,7 +69,7 @@ const AssetTransferInnerTransactionAccordionItem: FC = ({ const accounts: IAccount[] = useSelectAccounts(); const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); // hooks - const { asset, updating } = useAsset(transaction.assetId); + const { standardAsset, updating } = useStandardAssetById(transaction.assetId); const defaultTextColor: string = useDefaultTextColor(); const subTextColor: string = useSubTextColor(); // misc @@ -96,7 +96,7 @@ const AssetTransferInnerTransactionAccordionItem: FC = ({ {/*amount*/} - {!asset || updating ? ( + {!standardAsset || updating ? ( 0.001 @@ -112,7 +112,7 @@ const AssetTransferInnerTransactionAccordionItem: FC = ({ : 'red.500' } atomicUnitAmount={amount} - decimals={asset.decimals} + decimals={standardAsset.decimals} displayUnit={true} displayUnitColor={color || defaultTextColor} fontSize={fontSize} @@ -123,7 +123,7 @@ const AssetTransferInnerTransactionAccordionItem: FC = ({ ? '+' : '-' } - unit={asset.unitName || undefined} + unit={standardAsset.unitName || undefined} /> )} @@ -133,7 +133,7 @@ const AssetTransferInnerTransactionAccordionItem: FC = ({ {/*asset id*/} ('labels.assetId')}> - {!asset || updating ? ( + {!standardAsset || updating ? ( 12345678 @@ -142,13 +142,13 @@ const AssetTransferInnerTransactionAccordionItem: FC = ({ ) : ( - {asset.id} + {standardAsset.id} ('captions.assetIdCopied')} size="xs" - value={asset.id} + value={standardAsset.id} /> {explorer && ( = ({ tooltipLabel={t('captions.openOn', { name: explorer.canonicalName, })} - url={`${explorer.baseUrl}${explorer.assetPath}/${asset.id}`} + url={`${explorer.baseUrl}${explorer.assetPath}/${standardAsset.id}`} /> )} diff --git a/src/extension/components/SendAssetModal/SendAmountInput.tsx b/src/extension/components/SendAssetModal/SendAmountInput.tsx index 0245b4e2..178ecb4c 100644 --- a/src/extension/components/SendAssetModal/SendAmountInput.tsx +++ b/src/extension/components/SendAssetModal/SendAmountInput.tsx @@ -28,7 +28,7 @@ import { theme } from '@extension/theme'; // types import { IAccount, - IAsset, + IStandardAsset, INetworkWithTransactionParams, } from '@extension/types'; @@ -45,7 +45,7 @@ interface IProps { network: INetworkWithTransactionParams; maximumTransactionAmount: string; onValueChange: (value: string) => void; - selectedAsset: IAsset; + selectedAsset: IStandardAsset; value: string | null; } diff --git a/src/extension/components/SendAssetModal/SendAssetModal.tsx b/src/extension/components/SendAssetModal/SendAssetModal.tsx index 25c22ef5..e9dd9458 100644 --- a/src/extension/components/SendAssetModal/SendAssetModal.tsx +++ b/src/extension/components/SendAssetModal/SendAssetModal.tsx @@ -54,7 +54,6 @@ import { } from '@extension/features/send-assets'; // hooks -import useAssets from '@extension/hooks/useAssets'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import usePrimaryColor from '@extension/hooks/usePrimaryColor'; @@ -69,6 +68,7 @@ import { useSelectSendingAssetNote, useSelectSendingAssetSelectedAsset, useSelectSendingAssetTransactionId, + useSelectStandardAssetsBySelectedNetwork, } from '@extension/selectors'; // services @@ -81,7 +81,7 @@ import { theme } from '@extension/theme'; import { IAccount, IAppThunkDispatch, - IAsset, + IStandardAsset, INetworkWithTransactionParams, } from '@extension/types'; @@ -101,13 +101,15 @@ const SendAssetModal: FC = ({ onClose }: IProps) => { // selectors const accounts: IAccount[] = useSelectAccounts(); const amount: string = useSelectSendingAssetAmount(); + const assets: IStandardAsset[] = useSelectStandardAssetsBySelectedNetwork(); const confirming: boolean = useSelectSendingAssetConfirming(); const error: BaseExtensionError | null = useSelectSendingAssetError(); const fromAccount: IAccount | null = useSelectSendingAssetFromAccount(); const network: INetworkWithTransactionParams | null = useSelectSelectedNetwork(); const note: string | null = useSelectSendingAssetNote(); - const selectedAsset: IAsset | null = useSelectSendingAssetSelectedAsset(); + const selectedAsset: IStandardAsset | null = + useSelectSendingAssetSelectedAsset(); const transactionId: string | null = useSelectSendingAssetTransactionId(); // hooks const { @@ -118,7 +120,6 @@ const SendAssetModal: FC = ({ onClose }: IProps) => { validate: validateToAddress, value: toAddress, } = useAddressInput(); - const assets: IAsset[] = useAssets(); const defaultTextColor: string = useDefaultTextColor(); const { error: passwordError, @@ -137,7 +138,7 @@ const SendAssetModal: FC = ({ onClose }: IProps) => { const isOpen: boolean = !!selectedAsset; // handlers const handleAmountChange = (value: string) => dispatch(setAmount(value)); - const handleAssetChange = (value: IAsset) => + const handleAssetChange = (value: IStandardAsset) => dispatch(setSelectedAsset(value)); const handleCancelClick = () => onClose(); const handleFromAccountChange = (account: IAccount) => diff --git a/src/extension/components/SendAssetModal/SendAssetModalSummaryContent.tsx b/src/extension/components/SendAssetModal/SendAssetModalSummaryContent.tsx index 674dce22..6e2598fa 100644 --- a/src/extension/components/SendAssetModal/SendAssetModalSummaryContent.tsx +++ b/src/extension/components/SendAssetModal/SendAssetModalSummaryContent.tsx @@ -23,7 +23,7 @@ import { AccountService } from '@extension/services'; // types import { IAccount, - IAsset, + IStandardAsset, INetworkWithTransactionParams, } from '@extension/types'; @@ -32,7 +32,7 @@ import { createIconFromDataUri } from '@extension/utils'; interface IProps { amount: string; - asset: IAsset; + asset: IStandardAsset; fromAccount: IAccount; network: INetworkWithTransactionParams; note: string | null; diff --git a/src/extension/components/SignTxnsModal/AssetConfigTransactionContent.tsx b/src/extension/components/SignTxnsModal/AssetConfigTransactionContent.tsx index b701bdf6..03468c89 100644 --- a/src/extension/components/SignTxnsModal/AssetConfigTransactionContent.tsx +++ b/src/extension/components/SignTxnsModal/AssetConfigTransactionContent.tsx @@ -26,7 +26,12 @@ import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColo import useSubTextColor from '@extension/hooks/useSubTextColor'; // types -import { IAccount, IAsset, IExplorer, INetwork } from '@extension/types'; +import { + IAccount, + IStandardAsset, + IExplorer, + INetwork, +} from '@extension/types'; import { ICondensedProps } from './types'; // utils @@ -34,7 +39,7 @@ import { createIconFromDataUri, parseTransactionType } from '@extension/utils'; import Warning from '@extension/components/Warning'; interface IProps { - asset: IAsset | null; + asset: IStandardAsset | null; condensed?: ICondensedProps; explorer: IExplorer; fromAccount: IAccount | null; diff --git a/src/extension/components/SignTxnsModal/AssetFreezeTransactionContent.tsx b/src/extension/components/SignTxnsModal/AssetFreezeTransactionContent.tsx index 7a26bc93..26a80154 100644 --- a/src/extension/components/SignTxnsModal/AssetFreezeTransactionContent.tsx +++ b/src/extension/components/SignTxnsModal/AssetFreezeTransactionContent.tsx @@ -38,8 +38,8 @@ import { ILogger } from '@common/types'; import { IAccount, IAccountInformation, - IAsset, - IAssetHolding, + IStandardAsset, + IStandardAssetHolding, IExplorer, INetwork, } from '@extension/types'; @@ -53,7 +53,7 @@ import { } from '@extension/utils'; interface IProps { - asset: IAsset | null; + asset: IStandardAsset | null; condensed?: ICondensedProps; explorer: IExplorer; fromAccount: IAccount | null; @@ -215,7 +215,7 @@ const AssetFreezeTransactionContent: FC = ({ }, []); // once we have the freeze account information, check the asset balance useEffect(() => { - let assetHolding: IAssetHolding | null; + let assetHolding: IStandardAssetHolding | null; let freezeAccountInformation: IAccountInformation | null; if (asset && freezeAccount) { @@ -225,7 +225,7 @@ const AssetFreezeTransactionContent: FC = ({ network ); assetHolding = - freezeAccountInformation?.assetHoldings.find( + freezeAccountInformation?.standardAssetHoldings.find( (value) => value.id === asset.id ) || null; diff --git a/src/extension/components/SignTxnsModal/AssetTransferTransactionContent.tsx b/src/extension/components/SignTxnsModal/AssetTransferTransactionContent.tsx index f1ef2598..5a5b203d 100644 --- a/src/extension/components/SignTxnsModal/AssetTransferTransactionContent.tsx +++ b/src/extension/components/SignTxnsModal/AssetTransferTransactionContent.tsx @@ -21,7 +21,12 @@ import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColo import useSubTextColor from '@extension/hooks/useSubTextColor'; // types -import { IAccount, IAsset, IExplorer, INetwork } from '@extension/types'; +import { + IAccount, + IStandardAsset, + IExplorer, + INetwork, +} from '@extension/types'; import { ICondensedProps } from './types'; // utils @@ -29,7 +34,7 @@ import { convertToStandardUnit, formatCurrencyUnit } from '@common/utils'; import { createIconFromDataUri, parseTransactionType } from '@extension/utils'; interface IProps { - asset: IAsset | null; + asset: IStandardAsset | null; condensed?: ICondensedProps; explorer: IExplorer; fromAccount: IAccount | null; diff --git a/src/extension/components/SignTxnsModal/MultipleTransactionsContent.tsx b/src/extension/components/SignTxnsModal/MultipleTransactionsContent.tsx index 047d6f27..a9acaad0 100644 --- a/src/extension/components/SignTxnsModal/MultipleTransactionsContent.tsx +++ b/src/extension/components/SignTxnsModal/MultipleTransactionsContent.tsx @@ -21,10 +21,15 @@ import { TransactionTypeEnum } from '@extension/enums'; import useBorderColor from '@extension/hooks/useBorderColor'; // selectors -import { useSelectAssetsByGenesisHash } from '@extension/selectors'; +import { useSelectStandardAssetsByGenesisHash } from '@extension/selectors'; // types -import { IAccount, IAsset, IExplorer, INetwork } from '@extension/types'; +import { + IAccount, + IStandardAsset, + IExplorer, + INetwork, +} from '@extension/types'; // utils import { computeGroupId } from '@common/utils'; @@ -49,7 +54,9 @@ const MultipleTransactionsContent: FC = ({ }: IProps) => { const { t } = useTranslation(); // selectors - const assets: IAsset[] = useSelectAssetsByGenesisHash(network.genesisHash); + const assets: IStandardAsset[] = useSelectStandardAssetsByGenesisHash( + network.genesisHash + ); // hooks const borderColor: string = useBorderColor(); // state @@ -70,7 +77,7 @@ const MultipleTransactionsContent: FC = ({ transaction: Transaction, transactionIndex: number ) => { - const asset: IAsset | null = + const asset: IStandardAsset | null = assets.find((value) => value.id === String(transaction.assetIndex)) || null; const transactionType: TransactionTypeEnum = parseTransactionType( diff --git a/src/extension/components/SignTxnsModal/SignTxnsModalContent.tsx b/src/extension/components/SignTxnsModal/SignTxnsModalContent.tsx index 1467f55c..509404f9 100644 --- a/src/extension/components/SignTxnsModal/SignTxnsModalContent.tsx +++ b/src/extension/components/SignTxnsModal/SignTxnsModalContent.tsx @@ -20,15 +20,15 @@ import { TransactionTypeEnum } from '@extension/enums'; // features import { updateAccountInformation } from '@extension/features/accounts'; -import { updateAssetInformationThunk } from '@extension/features/assets'; +import { updateStandardAssetInformationThunk } from '@extension/features/standard-assets'; // selectors import { useSelectAccounts, - useSelectAssetsByGenesisHash, + useSelectStandardAssetsByGenesisHash, useSelectLogger, useSelectPreferredBlockExplorer, - useSelectUpdatingAssets, + useSelectUpdatingStandardAssets, } from '@extension/selectors'; // types @@ -37,7 +37,7 @@ import { IAccount, IAccountInformation, IAppThunkDispatch, - IAsset, + IStandardAsset, IExplorer, INetwork, } from '@extension/types'; @@ -61,9 +61,11 @@ const SignTxnsModalContent: FC = ({ const dispatch: IAppThunkDispatch = useDispatch(); // selectors const accounts: IAccount[] = useSelectAccounts(); - const assets: IAsset[] = useSelectAssetsByGenesisHash(network.genesisHash); const logger: ILogger = useSelectLogger(); - const updatingAssets: boolean = useSelectUpdatingAssets(); + const standardAssets: IStandardAsset[] = useSelectStandardAssetsByGenesisHash( + network.genesisHash + ); + const updatingStandardAssets: boolean = useSelectUpdatingStandardAssets(); const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); // state const [fetchingAccountInformation, setFetchingAccountInformation] = @@ -74,7 +76,7 @@ const SignTxnsModalContent: FC = ({ network.explorers[0] || null; // get the preferred explorer, if it exists in the networks, otherwise get the default one let singleTransaction: Transaction | null; - let singleTransactionAsset: IAsset | null; + let singleTransactionAsset: IStandardAsset | null; let singleTransactionFromAccount: IAccount | null; let singleTransactionType: TransactionTypeEnum; @@ -84,14 +86,16 @@ const SignTxnsModalContent: FC = ({ .filter((value) => value.type === 'axfer') .filter( (transaction) => - !assets.some((value) => value.id === String(transaction.assetIndex)) + !standardAssets.some( + (value) => value.id === String(transaction.assetIndex) + ) ) .map((value) => String(value.assetIndex)); // if we have some unknown assets, update the asset storage if (unknownAssetIds.length > 0) { dispatch( - updateAssetInformationThunk({ + updateStandardAssetInformationThunk({ ids: unknownAssetIds, network, }) @@ -165,7 +169,7 @@ const SignTxnsModalContent: FC = ({ explorer={explorer} fromAccounts={fromAccounts} loadingAccountInformation={fetchingAccountInformation} - loadingAssetInformation={updatingAssets} + loadingAssetInformation={updatingStandardAssets} network={network} transactions={transactions} /> @@ -176,7 +180,7 @@ const SignTxnsModalContent: FC = ({ if (singleTransaction) { singleTransactionAsset = - assets.find( + standardAssets.find( (value) => value.id === String(singleTransaction?.assetIndex) ) || null; singleTransactionFromAccount = fromAccounts[0] || null; @@ -206,7 +210,7 @@ const SignTxnsModalContent: FC = ({ asset={singleTransactionAsset} explorer={explorer} fromAccount={singleTransactionFromAccount} - loading={fetchingAccountInformation || updatingAssets} + loading={fetchingAccountInformation || updatingStandardAssets} network={network} transaction={singleTransaction} /> @@ -217,7 +221,7 @@ const SignTxnsModalContent: FC = ({ asset={singleTransactionAsset} explorer={explorer} fromAccount={singleTransactionFromAccount} - loading={fetchingAccountInformation || updatingAssets} + loading={fetchingAccountInformation || updatingStandardAssets} network={network} transaction={singleTransaction} /> @@ -236,7 +240,7 @@ const SignTxnsModalContent: FC = ({ asset={singleTransactionAsset} explorer={explorer} fromAccount={singleTransactionFromAccount} - loading={fetchingAccountInformation || updatingAssets} + loading={fetchingAccountInformation || updatingStandardAssets} network={network} transaction={singleTransaction} /> diff --git a/src/extension/components/TransactionItem/AssetTransferTransactionItemContent.tsx b/src/extension/components/TransactionItem/AssetTransferTransactionItemContent.tsx index 3cab4f3f..5ec64820 100644 --- a/src/extension/components/TransactionItem/AssetTransferTransactionItemContent.tsx +++ b/src/extension/components/TransactionItem/AssetTransferTransactionItemContent.tsx @@ -8,8 +8,8 @@ import AddressDisplay from '@extension/components/AddressDisplay'; import AssetDisplay from '@extension/components/AssetDisplay'; // hooks -import useAsset from '@extension/hooks/useAsset'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import useStandardAssetById from '@extension/hooks/useStandardAssetById'; import useSubTextColor from '@extension/hooks/useSubTextColor'; // services @@ -35,7 +35,7 @@ const AssetTransferTransactionItemContent: FC = ({ }: IProps) => { const { t } = useTranslation(); // hooks - const { asset, updating } = useAsset(transaction.assetId); + const { standardAsset, updating } = useStandardAssetById(transaction.assetId); const defaultTextColor: string = useDefaultTextColor(); const subTextColor: string = useSubTextColor(); const accountAddress: string = @@ -63,7 +63,7 @@ const AssetTransferTransactionItemContent: FC = ({ {/*amount*/} - {!asset || updating ? ( + {!standardAsset || updating ? ( @@ -81,7 +81,7 @@ const AssetTransferTransactionItemContent: FC = ({ ? 'green.500' : 'red.500' } - decimals={asset.decimals} + decimals={standardAsset.decimals} displayUnit={true} fontSize="sm" prefix={ @@ -91,7 +91,7 @@ const AssetTransferTransactionItemContent: FC = ({ ? '+' : '-' } - unit={asset.unitName || undefined} + unit={standardAsset.unitName || undefined} /> )} diff --git a/src/extension/components/TransactionItem/TransactionItem.tsx b/src/extension/components/TransactionItem/TransactionItem.tsx index c1fc20cd..5c3b2af7 100644 --- a/src/extension/components/TransactionItem/TransactionItem.tsx +++ b/src/extension/components/TransactionItem/TransactionItem.tsx @@ -10,7 +10,11 @@ import DefaultTransactionItemContent from './DefaultTransactionItemContent'; import PaymentTransactionItemContent from './PaymentTransactionItemContent'; // constants -import { ACCOUNTS_ROUTE, TRANSACTIONS_ROUTE } from '@extension/constants'; +import { + ACCOUNTS_ROUTE, + TAB_ITEM_HEIGHT, + TRANSACTIONS_ROUTE, +} from '@extension/constants'; // enums import { TransactionTypeEnum } from '@extension/enums'; @@ -88,7 +92,7 @@ const TransactionItem: FC = ({ as={Link} borderRadius={0} fontSize="md" - h={16} + h={TAB_ITEM_HEIGHT} justifyContent="start" pl={3} pr={1} diff --git a/src/extension/constants/Dimensions.ts b/src/extension/constants/Dimensions.ts index de9a5396..5bbb2efe 100644 --- a/src/extension/constants/Dimensions.ts +++ b/src/extension/constants/Dimensions.ts @@ -7,3 +7,4 @@ export const SIDEBAR_ITEM_HEIGHT: number = 12; export const SETTINGS_ITEM_HEIGHT: number = 16; export const SIDEBAR_MIN_WIDTH: number = 40; export const SIDEBAR_MAX_WIDTH: number = 250; +export const TAB_ITEM_HEIGHT: number = 16; diff --git a/src/extension/constants/Keys.ts b/src/extension/constants/Keys.ts index 97c2ddc5..b635bd38 100644 --- a/src/extension/constants/Keys.ts +++ b/src/extension/constants/Keys.ts @@ -1,5 +1,6 @@ export const ACCOUNTS_ITEM_KEY_PREFIX: string = 'accounts_'; -export const ASSETS_KEY_PREFIX: string = 'assets_'; +export const APP_WINDOW_KEY_PREFIX: string = 'app_window_'; +export const ARC200_ASSETS_KEY_PREFIX: string = 'arc200_assets_'; export const EVENT_QUEUE_ITEM_KEY: string = 'event_queue'; export const NETWORK_TRANSACTION_PARAMS_ITEM_KEY_PREFIX: string = 'network_transaction_params_'; @@ -9,4 +10,4 @@ export const SETTINGS_ADVANCED_KEY: string = 'settings_advanced'; export const SETTINGS_APPEARANCE_KEY: string = 'settings_appearance'; export const SETTINGS_GENERAL_KEY: string = 'settings_general'; export const SESSION_ITEM_KEY_PREFIX: string = 'session_'; -export const APP_WINDOW_KEY_PREFIX: string = 'app_window_'; +export const STANDARD_ASSETS_KEY_PREFIX: string = 'standard_assets_'; diff --git a/src/extension/enums/AccountsThunkEnum.ts b/src/extension/enums/AccountsThunkEnum.ts index ba14296c..815315d2 100644 --- a/src/extension/enums/AccountsThunkEnum.ts +++ b/src/extension/enums/AccountsThunkEnum.ts @@ -1,4 +1,5 @@ enum AccountsThunkEnum { + AddArc200AssetHolding = 'accounts/AddArc200AssetHolding', FetchAccountsFromStorage = 'accounts/fetchAccountsFromStorage', RemoveAccountById = 'accounts/removeAccountById', SaveNewAccount = 'accounts/saveNewAccount', diff --git a/src/extension/enums/AddAssetThunkEnum.ts b/src/extension/enums/AddAssetThunkEnum.ts new file mode 100644 index 00000000..fad7bd9d --- /dev/null +++ b/src/extension/enums/AddAssetThunkEnum.ts @@ -0,0 +1,5 @@ +enum AddAssetThunkEnum { + QueryById = 'addAsset/queryById', +} + +export default AddAssetThunkEnum; diff --git a/src/extension/enums/Arc200AssetsThunkEnum.ts b/src/extension/enums/Arc200AssetsThunkEnum.ts new file mode 100644 index 00000000..9484446b --- /dev/null +++ b/src/extension/enums/Arc200AssetsThunkEnum.ts @@ -0,0 +1,6 @@ +enum Arc200AssetsThunkEnum { + FetchArc200AssetsFromStorage = 'arc200-assets/fetchArc200AssetsFromStorage', + UpdateArc200AssetInformation = 'arc200-assets/updateArc200AssetInformation', +} + +export default Arc200AssetsThunkEnum; diff --git a/src/extension/enums/AssetTypeEnum.ts b/src/extension/enums/AssetTypeEnum.ts new file mode 100644 index 00000000..8d8d16b1 --- /dev/null +++ b/src/extension/enums/AssetTypeEnum.ts @@ -0,0 +1,6 @@ +enum AssetTypeEnum { + Arc200 = 'arc200', + Standard = 'standard', +} + +export default AssetTypeEnum; diff --git a/src/extension/enums/AssetsThunkEnum.ts b/src/extension/enums/AssetsThunkEnum.ts deleted file mode 100644 index 10479526..00000000 --- a/src/extension/enums/AssetsThunkEnum.ts +++ /dev/null @@ -1,7 +0,0 @@ -enum AssetsThunkEnum { - FetchAssets = 'assets/fetchAssets', - SaveNewAssets = 'assets/saveNewAssets', - UpdateAssetInformation = 'assets/updateAssetInformation', -} - -export default AssetsThunkEnum; diff --git a/src/extension/enums/StandardAssetsThunkEnum.ts b/src/extension/enums/StandardAssetsThunkEnum.ts new file mode 100644 index 00000000..4143ff3f --- /dev/null +++ b/src/extension/enums/StandardAssetsThunkEnum.ts @@ -0,0 +1,7 @@ +enum StandardAssetsThunkEnum { + FetchAssetsFromStorage = 'assets/fetchAssetsFromStorage', + SaveNewAssets = 'assets/saveNewAssets', + UpdateStandardAssetInformation = 'assets/updateStandardAssetInformation', +} + +export default StandardAssetsThunkEnum; diff --git a/src/extension/enums/StoreNameEnum.ts b/src/extension/enums/StoreNameEnum.ts index 647bbc50..8e96d28e 100644 --- a/src/extension/enums/StoreNameEnum.ts +++ b/src/extension/enums/StoreNameEnum.ts @@ -1,6 +1,7 @@ enum StoreNameEnum { Accounts = 'accounts', - Assets = 'assets', + AddAsset = 'add-asset', + Arc200Assets = 'arc200-assets', Events = 'events', Messages = 'messages', Networks = 'networks', @@ -9,6 +10,7 @@ enum StoreNameEnum { SendAssets = 'send-assets', Sessions = 'sessions', Settings = 'settings', + StandardAssets = 'standard-assets', System = 'system', } diff --git a/src/extension/enums/index.ts b/src/extension/enums/index.ts index b3d217d1..00eafc11 100644 --- a/src/extension/enums/index.ts +++ b/src/extension/enums/index.ts @@ -1,6 +1,8 @@ export { default as AccountsThunkEnum } from './AccountsThunkEnum'; +export { default as AddAssetThunkEnum } from './AddAssetThunkEnum'; export { default as AppTypeEnum } from './AppTypeEnum'; -export { default as AssetsThunkEnum } from './AssetsThunkEnum'; +export { default as Arc200AssetsThunkEnum } from './Arc200AssetsThunkEnum'; +export { default as AssetTypeEnum } from './AssetTypeEnum'; export { default as ErrorCodeEnum } from './ErrorCodeEnum'; export { default as EventsThunkEnum } from './EventsThunkEnum'; export { default as EventTypeEnum } from './EventTypeEnum'; @@ -11,6 +13,7 @@ export { default as RegisterThunkEnum } from './RegisterThunkEnum'; export { default as SendAssetsThunkEnum } from './SendAssetsThunkEnum'; export { default as SessionsThunkEnum } from './SessionsThunkEnum'; export { default as SettingsThunkEnum } from './SettingsThunkEnum'; +export { default as StandardAssetsThunkEnum } from './StandardAssetsThunkEnum'; export { default as StoreNameEnum } from './StoreNameEnum'; export { default as SystemThunkEnum } from './SystemThunkEnum'; export { default as TransactionTypeEnum } from './TransactionTypeEnum'; diff --git a/src/extension/features/accounts/slice.ts b/src/extension/features/accounts/slice.ts index 890a4cd2..9fe25a41 100644 --- a/src/extension/features/accounts/slice.ts +++ b/src/extension/features/accounts/slice.ts @@ -5,6 +5,7 @@ import { StoreNameEnum } from '@extension/enums'; // thunks import { + addArc200AssetHoldingThunk, fetchAccountsFromStorageThunk, removeAccountByIdThunk, saveNewAccountThunk, @@ -14,24 +15,57 @@ import { } from './thunks'; // types -import { - IAccount, - IPendingActionMeta, - IRejectedActionMeta, -} from '@extension/types'; -import { - IAccountsState, - IAccountUpdate, - IUpdateAccountsPayload, -} from './types'; +import { IAccount } from '@extension/types'; +import { IAccountsState } from './types'; // utils import { upsertItemsById } from '@extension/utils'; import { getInitialState } from './utils'; -import { stat } from 'copy-webpack-plugin/types/utils'; const slice = createSlice({ extraReducers: (builder) => { + /** add arc200 asset holdings **/ + builder.addCase( + addArc200AssetHoldingThunk.fulfilled, + (state: IAccountsState, action: PayloadAction) => { + if (action.payload) { + state.items = state.items.map((value) => + value.id === action.payload?.id ? action.payload : value + ); + } + + // remove updated account from the account update list + state.updatingAccounts = state.updatingAccounts.filter( + (value) => value.id !== action.payload?.id + ); + } + ); + builder.addCase( + addArc200AssetHoldingThunk.pending, + (state: IAccountsState, action) => { + state.updatingAccounts = [ + // filter the unrelated updating account ids + ...(state.updatingAccounts = state.updatingAccounts.filter( + (value) => value.id !== action.meta.arg.accountId + )), + // re-add the account being updated + { + id: action.meta.arg.accountId, + information: true, + transactions: false, + }, + ]; + } + ); + builder.addCase( + addArc200AssetHoldingThunk.rejected, + (state: IAccountsState, action) => { + // remove updated account from the account update list + state.updatingAccounts = state.updatingAccounts.filter( + (value) => value.id !== action.meta.arg.accountId + ); + } + ); /** fetch accounts from storage **/ builder.addCase( fetchAccountsFromStorageThunk.fulfilled, diff --git a/src/extension/features/accounts/thunks/addArc200AssetHoldingThunk.ts b/src/extension/features/accounts/thunks/addArc200AssetHoldingThunk.ts new file mode 100644 index 00000000..0ac1ce73 --- /dev/null +++ b/src/extension/features/accounts/thunks/addArc200AssetHoldingThunk.ts @@ -0,0 +1,132 @@ +import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; + +// constants +import { NODE_REQUEST_DELAY } from '@extension/constants'; + +// features +import { updateAccountInformation } from '@extension/features/accounts'; + +// enums +import { AccountsThunkEnum } from '@extension/enums'; + +// services +import { AccountService } from '@extension/services'; + +// types +import { ILogger } from '@common/types'; +import { + IAccount, + IAccountInformation, + IBaseAsyncThunkConfig, + INetwork, +} from '@extension/types'; +import { IAddArc200AssetHoldingPayload } from '../types'; + +// utils +import { convertGenesisHashToHex } from '@extension/utils'; + +const addArc200AssetHoldingThunk: AsyncThunk< + IAccount | null, // return + IAddArc200AssetHoldingPayload, // args + IBaseAsyncThunkConfig +> = createAsyncThunk< + IAccount | null, + IAddArc200AssetHoldingPayload, + IBaseAsyncThunkConfig +>( + AccountsThunkEnum.AddArc200AssetHolding, + async ({ accountId, appId, genesisHash }, { getState }) => { + const logger: ILogger = getState().system.logger; + const networks: INetwork[] = getState().networks.items; + const accounts: IAccount[] = getState().accounts.items; + let account: IAccount | null = + accounts.find((value) => value.id === accountId) || null; + let accountService: AccountService; + let currentAccountInformation: IAccountInformation; + let encodedGenesisHash: string; + let network: INetwork | null; + + if (!account) { + logger.debug( + `${AccountsThunkEnum.AddArc200AssetHolding}: no account for "${accountId}" found` + ); + + return null; + } + + network = + networks.find((value) => value.genesisHash === genesisHash) || null; + + if (!network) { + logger.debug( + `${AccountsThunkEnum.AddArc200AssetHolding}: no network found for "${genesisHash}" found` + ); + + return null; + } + + encodedGenesisHash = convertGenesisHashToHex( + network.genesisHash + ).toUpperCase(); + currentAccountInformation = + account.networkInformation[encodedGenesisHash] || + AccountService.initializeDefaultAccountInformation(); + + if ( + currentAccountInformation.arc200AssetHoldings.find( + (value) => value.id === appId + ) + ) { + logger.debug( + `${AccountsThunkEnum.AddArc200AssetHolding}: arc200 asset "${appId}" has already been added, ignoring` + ); + + return null; + } + + logger.debug( + `${AccountsThunkEnum.AddArc200AssetHolding}: adding arc200 asset "${appId}" to account "${account.id}"` + ); + + accountService = new AccountService({ + logger, + }); + account = { + ...account, + networkInformation: { + ...account.networkInformation, + [convertGenesisHashToHex(network.genesisHash).toUpperCase()]: + await updateAccountInformation({ + address: AccountService.convertPublicKeyToAlgorandAddress( + account.publicKey + ), + currentAccountInformation: { + ...currentAccountInformation, + arc200AssetHoldings: [ + ...currentAccountInformation.arc200AssetHoldings, + { + amount: '0', + id: appId, + }, + ], + }, + delay: NODE_REQUEST_DELAY, + forceUpdate: true, + logger, + network, + }), + }, + }; + + logger.debug( + `${AccountsThunkEnum.AddArc200AssetHolding}: saving account "${account.id}" to storage` + ); + + // save the account to storage + await accountService.saveAccounts([account]); + + return account; + } +); + +export default addArc200AssetHoldingThunk; diff --git a/src/extension/features/accounts/thunks/index.ts b/src/extension/features/accounts/thunks/index.ts index 231b1f55..f561ec77 100644 --- a/src/extension/features/accounts/thunks/index.ts +++ b/src/extension/features/accounts/thunks/index.ts @@ -1,3 +1,4 @@ +export { default as addArc200AssetHoldingThunk } from './addArc200AssetHoldingThunk'; export { default as fetchAccountsFromStorageThunk } from './fetchAccountsFromStorageThunk'; export { default as removeAccountByIdThunk } from './removeAccountByIdThunk'; export { default as saveNewAccountThunk } from './saveNewAccountThunk'; diff --git a/src/extension/features/accounts/types/IAddArc200AssetHoldingPayload.ts b/src/extension/features/accounts/types/IAddArc200AssetHoldingPayload.ts new file mode 100644 index 00000000..092be16d --- /dev/null +++ b/src/extension/features/accounts/types/IAddArc200AssetHoldingPayload.ts @@ -0,0 +1,7 @@ +interface IAddArc200AssetHoldingPayload { + accountId: string; + appId: string; + genesisHash: string; +} + +export default IAddArc200AssetHoldingPayload; diff --git a/src/extension/features/accounts/types/index.ts b/src/extension/features/accounts/types/index.ts index 5a31bb68..5f302de2 100644 --- a/src/extension/features/accounts/types/index.ts +++ b/src/extension/features/accounts/types/index.ts @@ -1,5 +1,6 @@ export type { default as IAccountsState } from './IAccountsState'; export type { default as IAccountUpdate } from './IAccountUpdate'; +export type { default as IAddArc200AssetHoldingPayload } from './IAddArc200AssetHoldingPayload'; export type { default as IFetchAccountsFromStoragePayload } from './IFetchAccountsFromStoragePayload'; export type { default as ISaveNewAccountPayload } from './ISaveNewAccountPayload'; export type { default as IUpdateAccountsPayload } from './IUpdateAccountsPayload'; diff --git a/src/extension/features/accounts/utils/fetchArc200AssetHoldingWithDelay.ts b/src/extension/features/accounts/utils/fetchArc200AssetHoldingWithDelay.ts new file mode 100644 index 00000000..59c391c7 --- /dev/null +++ b/src/extension/features/accounts/utils/fetchArc200AssetHoldingWithDelay.ts @@ -0,0 +1,51 @@ +import { Algodv2 } from 'algosdk'; +import Arc200Contract from 'arc200js'; + +// types +import { IArc200AssetHolding } from '@extension/types'; + +interface IOptions { + address: string; + arc200AppId: string; + client: Algodv2; + delay: number; +} + +/** + * Fetches ARC-200 asset holding from the node with a delay. + * @param {IOptions} options - options needed to send the request. + * @returns {IArc200AssetHolding} arc200 asset holding. + */ +export default async function fetchArc200AssetHoldingWithDelay({ + address, + arc200AppId, + client, + delay, +}: IOptions): Promise { + return new Promise((resolve, reject) => + setTimeout(async () => { + const contract: Arc200Contract = new Arc200Contract( + parseInt(arc200AppId), + client, + undefined + ); + let amount: string = '0'; + let result: { success: boolean; returnValue: bigint }; + + try { + result = await contract.arc200_balanceOf(address); + + if (result.success) { + amount = String(result.returnValue); + } + + resolve({ + id: arc200AppId, + amount, + }); + } catch (error) { + reject(error); + } + }, delay) + ); +} diff --git a/src/extension/features/accounts/utils/index.ts b/src/extension/features/accounts/utils/index.ts index b23f4169..b3364586 100644 --- a/src/extension/features/accounts/utils/index.ts +++ b/src/extension/features/accounts/utils/index.ts @@ -1,4 +1,5 @@ export { default as algorandAccountInformationWithDelay } from './algorandAccountInformationWithDelay'; +export { default as fetchArc200AssetHoldingWithDelay } from './fetchArc200AssetHoldingWithDelay'; export { default as getInitialState } from './getInitialState'; export { default as lookupAlgorandAccountTransactionsWithDelay } from './lookupAlgorandAccountTransactionsWithDelay'; export { default as refreshTransactions } from './refreshTransactions'; diff --git a/src/extension/features/accounts/utils/updateAccountInformation.ts b/src/extension/features/accounts/utils/updateAccountInformation.ts index 4b1e3753..758243c1 100644 --- a/src/extension/features/accounts/utils/updateAccountInformation.ts +++ b/src/extension/features/accounts/utils/updateAccountInformation.ts @@ -8,6 +8,7 @@ import { IBaseOptions } from '@common/types'; import { IAccountInformation, IAlgorandAccountInformation, + IArc200AssetHolding, INetwork, } from '@extension/types'; @@ -15,6 +16,7 @@ import { import { getAlgodClient } from '@common/utils'; import { mapAlgorandAccountInformationToAccount } from '@extension/utils'; import algorandAccountInformationWithDelay from './algorandAccountInformationWithDelay'; +import fetchArc200AssetHoldingWithDelay from './fetchArc200AssetHoldingWithDelay'; interface IOptions extends IBaseOptions { address: string; @@ -38,6 +40,7 @@ export default async function updateAccountInformation({ network, }: IOptions): Promise { let algorandAccountInformation: IAlgorandAccountInformation; + let arc200AssetHoldings: IArc200AssetHolding[]; let client: Algodv2; let updatedAt: Date; @@ -74,6 +77,17 @@ export default async function updateAccountInformation({ client, delay, }); + arc200AssetHoldings = await Promise.all( + currentAccountInformation.arc200AssetHoldings.map( + async (value) => + await fetchArc200AssetHoldingWithDelay({ + address, + arc200AppId: value.id, + client, + delay, + }) + ) + ); updatedAt = new Date(); logger && @@ -87,7 +101,10 @@ export default async function updateAccountInformation({ return mapAlgorandAccountInformationToAccount( algorandAccountInformation, - currentAccountInformation, + { + ...currentAccountInformation, + arc200AssetHoldings, + }, updatedAt.getTime() ); } catch (error) { diff --git a/src/extension/features/assets/index.ts b/src/extension/features/add-asset/index.ts similarity index 100% rename from src/extension/features/assets/index.ts rename to src/extension/features/add-asset/index.ts diff --git a/src/extension/features/add-asset/slice.ts b/src/extension/features/add-asset/slice.ts new file mode 100644 index 00000000..07ec2322 --- /dev/null +++ b/src/extension/features/add-asset/slice.ts @@ -0,0 +1,104 @@ +import { + createSlice, + Draft, + PayloadAction, + Reducer, + SerializedError, +} from '@reduxjs/toolkit'; + +// enums +import { StoreNameEnum } from '@extension/enums'; + +// errors +import { BaseExtensionError } from '@extension/errors'; + +// thunks +import { queryByIdThunk } from './thunks'; + +// types +import { IArc200Asset, IRejectedActionMeta } from '@extension/types'; +import { IAddAssetState, IQueryByIdResult } from './types'; + +// utils +import { getInitialState } from './utils'; + +const slice = createSlice({ + extraReducers: (builder) => { + /** query by id **/ + builder.addCase( + queryByIdThunk.fulfilled, + (state: IAddAssetState, action: PayloadAction) => { + state.arc200Assets = action.payload.arc200Assets; + state.fetching = false; + } + ); + builder.addCase(queryByIdThunk.pending, (state: IAddAssetState) => { + state.fetching = true; + }); + builder.addCase( + queryByIdThunk.rejected, + ( + state: IAddAssetState, + action: PayloadAction< + BaseExtensionError, + string, + IRejectedActionMeta, + SerializedError + > + ) => { + // if it is an abort error, ignore as it is a new request + if (action.error.name !== 'AbortError') { + state.error = action.payload; + state.fetching = false; + } + } + ); + }, + initialState: getInitialState(), + name: StoreNameEnum.AddAsset, + reducers: { + clearAssets: (state: Draft) => { + state.arc200Assets = { + items: [], + next: null, + }; + }, + reset: (state: Draft) => { + state.accountId = null; + state.arc200Assets = { + items: [], + next: null, + }; + state.error = null; + state.fetching = false; + state.selectedArc200Asset = null; + }, + setAccountId: ( + state: Draft, + action: PayloadAction + ) => { + state.accountId = action.payload; + }, + setError: ( + state: Draft, + action: PayloadAction + ) => { + state.error = action.payload; + }, + setSelectedArc200Asset: ( + state: Draft, + action: PayloadAction + ) => { + state.selectedArc200Asset = action.payload; + }, + }, +}); + +export const reducer: Reducer = slice.reducer; +export const { + clearAssets, + reset, + setAccountId, + setError, + setSelectedArc200Asset, +} = slice.actions; diff --git a/src/extension/features/add-asset/thunks/index.ts b/src/extension/features/add-asset/thunks/index.ts new file mode 100644 index 00000000..6cd115c0 --- /dev/null +++ b/src/extension/features/add-asset/thunks/index.ts @@ -0,0 +1 @@ +export { default as queryByIdThunk } from './queryByIdThunk'; diff --git a/src/extension/features/add-asset/thunks/queryByIdThunk.ts b/src/extension/features/add-asset/thunks/queryByIdThunk.ts new file mode 100644 index 00000000..60ac607e --- /dev/null +++ b/src/extension/features/add-asset/thunks/queryByIdThunk.ts @@ -0,0 +1,148 @@ +import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; +import { Algodv2, Indexer } from 'algosdk'; +import { BigNumber } from 'bignumber.js'; + +// constants +import { + DEFAULT_TRANSACTION_INDEXER_LIMIT, + NODE_REQUEST_DELAY, +} from '@extension/constants'; + +// enums +import { AddAssetThunkEnum } from '@extension/enums'; + +// errors +import { + BaseExtensionError, + NetworkNotSelectedError, + OfflineError, +} from '@extension/errors'; + +// types +import { ILogger } from '@common/types'; +import { + IAlgorandSearchApplicationsResult, + IArc200Asset, + IArc200AssetInformation, + IMainRootState, + INetworkWithTransactionParams, +} from '@extension/types'; +import { + IAssetsWithNextToken, + IQueryByIdAsyncThunkConfig, + IQueryByIdResult, +} from '../types'; + +// utils +import { getAlgodClient, getIndexerClient } from '@common/utils'; +import { + fetchArc200AssetInformationWithDelay, + mapArc200AssetFromArc200AssetInformation, + selectNetworkFromSettings, +} from '@extension/utils'; +import { searchAlgorandApplicationsWithDelay } from '../utils'; + +const queryByIdThunk: AsyncThunk< + IQueryByIdResult, // return + string, // args + IQueryByIdAsyncThunkConfig +> = createAsyncThunk( + AddAssetThunkEnum.QueryById, + async (query, { getState, rejectWithValue }) => { + const currentArc200Assets: IAssetsWithNextToken = + getState().addAsset.arc200Assets; + const logger: ILogger = getState().system.logger; + const online: boolean = getState().system.online; + const selectedNetwork: INetworkWithTransactionParams | null = + selectNetworkFromSettings(getState().networks.items, getState().settings); + let algorandSearchApplicationResult: IAlgorandSearchApplicationsResult; + let algodClient: Algodv2; + let indexerClient: Indexer; + let updatedArc200Assets: IArc200Asset[] = []; + + if (!online) { + logger.debug(`${AddAssetThunkEnum.QueryById}: extension offline`); + + return rejectWithValue( + new OfflineError('attempted to send transaction, but extension offline') + ); + } + + if (!selectedNetwork) { + logger.debug(`${AddAssetThunkEnum.QueryById}: no network selected`); + + return rejectWithValue( + new NetworkNotSelectedError( + 'attempted to send transaction, but no network selected' + ) + ); + } + + algodClient = getAlgodClient(selectedNetwork, { + logger, + }); + indexerClient = getIndexerClient(selectedNetwork, { + logger, + }); + + // if we have a next token, we are paginating + if (currentArc200Assets.next) { + updatedArc200Assets = currentArc200Assets.items; + } + + try { + algorandSearchApplicationResult = + await searchAlgorandApplicationsWithDelay({ + appId: query, + client: indexerClient, + delay: 0, + limit: DEFAULT_TRANSACTION_INDEXER_LIMIT, + next: currentArc200Assets.next, + }); + + for ( + let index = 0; + index < algorandSearchApplicationResult.applications.length; + index++ + ) { + const appId: string = new BigNumber( + String( + algorandSearchApplicationResult.applications[index].id as bigint + ) + ).toString(); + const arc200Asset: IArc200AssetInformation | null = + await fetchArc200AssetInformationWithDelay({ + algodClient, + id: appId, + indexerClient, + delay: index * NODE_REQUEST_DELAY, + }); + + // if the app is an arc200 app, add it to the list + if (arc200Asset) { + updatedArc200Assets.push( + mapArc200AssetFromArc200AssetInformation( + appId, + arc200Asset, + null, + false + ) + ); + } + } + + return { + arc200Assets: { + items: updatedArc200Assets, + next: algorandSearchApplicationResult['next-token'] || null, + }, + }; + } catch (error) { + logger.debug(`${AddAssetThunkEnum.QueryById}(): ${error.message}`); + + return rejectWithValue(error); + } + } +); + +export default queryByIdThunk; diff --git a/src/extension/features/add-asset/types/IAddAssetState.ts b/src/extension/features/add-asset/types/IAddAssetState.ts new file mode 100644 index 00000000..53eb30b7 --- /dev/null +++ b/src/extension/features/add-asset/types/IAddAssetState.ts @@ -0,0 +1,16 @@ +// errors +import { BaseExtensionError } from '@extension/errors'; + +// types +import { IArc200Asset } from '@extension/types'; +import IAssetsWithNextToken from './IAssetsWithNextToken'; + +interface IAddAssetState { + accountId: string | null; + arc200Assets: IAssetsWithNextToken; + error: BaseExtensionError | null; + fetching: boolean; + selectedArc200Asset: IArc200Asset | null; +} + +export default IAddAssetState; diff --git a/src/extension/features/add-asset/types/IAssetsWithNextToken.ts b/src/extension/features/add-asset/types/IAssetsWithNextToken.ts new file mode 100644 index 00000000..6fe4095c --- /dev/null +++ b/src/extension/features/add-asset/types/IAssetsWithNextToken.ts @@ -0,0 +1,9 @@ +// types +import { IArc200Asset, IStandardAsset } from '@extension/types'; + +interface IAssetsWithNextToken { + items: Asset[]; + next: string | null; +} + +export default IAssetsWithNextToken; diff --git a/src/extension/features/add-asset/types/IQueryByIdAsyncThunkConfig.ts b/src/extension/features/add-asset/types/IQueryByIdAsyncThunkConfig.ts new file mode 100644 index 00000000..07140dc0 --- /dev/null +++ b/src/extension/features/add-asset/types/IQueryByIdAsyncThunkConfig.ts @@ -0,0 +1,11 @@ +// errors +import { BaseExtensionError } from '@extension/errors'; + +// types +import { IBaseAsyncThunkConfig } from '@extension/types'; + +interface IQueryByIdAsyncThunkConfig extends IBaseAsyncThunkConfig { + rejectValue?: BaseExtensionError; +} + +export default IQueryByIdAsyncThunkConfig; diff --git a/src/extension/features/add-asset/types/IQueryByIdResult.ts b/src/extension/features/add-asset/types/IQueryByIdResult.ts new file mode 100644 index 00000000..e5fe95c2 --- /dev/null +++ b/src/extension/features/add-asset/types/IQueryByIdResult.ts @@ -0,0 +1,12 @@ +// types +import { IArc200Asset } from '@extension/types'; +import IAssetsWithNextToken from './IAssetsWithNextToken'; + +/** + * @property {IAssetsWithNextToken} arc200Assets - a list of ARC200 assets, which application IDs match the supplied ID. + */ +interface IQueryByIdResult { + arc200Assets: IAssetsWithNextToken; +} + +export default IQueryByIdResult; diff --git a/src/extension/features/add-asset/types/index.ts b/src/extension/features/add-asset/types/index.ts new file mode 100644 index 00000000..b1536c2a --- /dev/null +++ b/src/extension/features/add-asset/types/index.ts @@ -0,0 +1,4 @@ +export type { default as IAddAssetState } from './IAddAssetState'; +export type { default as IAssetsWithNextToken } from './IAssetsWithNextToken'; +export type { default as IQueryByIdResult } from './IQueryByIdResult'; +export type { default as IQueryByIdAsyncThunkConfig } from './IQueryByIdAsyncThunkConfig'; diff --git a/src/extension/features/add-asset/utils/getInitialState.ts b/src/extension/features/add-asset/utils/getInitialState.ts new file mode 100644 index 00000000..314a731d --- /dev/null +++ b/src/extension/features/add-asset/utils/getInitialState.ts @@ -0,0 +1,15 @@ +// types +import { IAddAssetState } from '../types'; + +export default function getInitialState(): IAddAssetState { + return { + accountId: null, + arc200Assets: { + items: [], + next: null, + }, + error: null, + fetching: false, + selectedArc200Asset: null, + }; +} diff --git a/src/extension/features/add-asset/utils/index.ts b/src/extension/features/add-asset/utils/index.ts new file mode 100644 index 00000000..89e5fe13 --- /dev/null +++ b/src/extension/features/add-asset/utils/index.ts @@ -0,0 +1,2 @@ +export { default as getInitialState } from './getInitialState'; +export { default as searchAlgorandApplicationsWithDelay } from './searchAlgorandApplicationsWithDelay'; diff --git a/src/extension/features/add-asset/utils/searchAlgorandApplicationsWithDelay.ts b/src/extension/features/add-asset/utils/searchAlgorandApplicationsWithDelay.ts new file mode 100644 index 00000000..db203054 --- /dev/null +++ b/src/extension/features/add-asset/utils/searchAlgorandApplicationsWithDelay.ts @@ -0,0 +1,52 @@ +import { Indexer, IntDecoding } from 'algosdk'; +import SearchForApplications from 'algosdk/dist/types/client/v2/indexer/searchForApplications'; + +// types +import { IAlgorandSearchApplicationsResult } from '@extension/types'; + +interface IOptions { + appId: string; + client: Indexer; + delay: number; + limit: number; + next: string | null; +} + +/** + * Searches for applications by the app ID from the node with a delay. + * @param {IOptions} options - options needed to send the request. + * @returns {IAlgorandSearchApplicationsResult} applications from the node. + */ +export default async function searchAlgorandApplicationsWithDelay({ + appId, + client, + delay, + limit, + next, +}: IOptions): Promise { + return new Promise((resolve, reject) => + setTimeout(async () => { + let result: IAlgorandSearchApplicationsResult; + let requestBuilder: SearchForApplications; + + try { + requestBuilder = client + .searchForApplications() + .index(parseInt(appId)) + .limit(limit); + + if (next) { + requestBuilder.nextToken(next); + } + + result = (await requestBuilder + .setIntDecoding(IntDecoding.BIGINT) + .do()) as IAlgorandSearchApplicationsResult; + + resolve(result); + } catch (error) { + reject(error); + } + }, delay) + ); +} diff --git a/src/extension/features/arc200-assets/index.ts b/src/extension/features/arc200-assets/index.ts new file mode 100644 index 00000000..0741f6b4 --- /dev/null +++ b/src/extension/features/arc200-assets/index.ts @@ -0,0 +1,4 @@ +export * from './slice'; +export * from './thunks'; +export * from './types'; +export * from './utils'; diff --git a/src/extension/features/arc200-assets/slice.ts b/src/extension/features/arc200-assets/slice.ts new file mode 100644 index 00000000..c857ba9d --- /dev/null +++ b/src/extension/features/arc200-assets/slice.ts @@ -0,0 +1,86 @@ +import { createSlice, PayloadAction, Reducer } from '@reduxjs/toolkit'; + +// enums +import { StoreNameEnum } from '@extension/enums'; + +// thunks +import { + fetchArc200AssetsFromStorageThunk, + updateArc200AssetInformationThunk, +} from './thunks'; + +// types +import { IArc200Asset } from '@extension/types'; +import { + IArc200AssetsState, + IUpdateArc200AssetInformationResult, +} from './types'; + +// utils +import { getInitialState } from './utils'; +import { convertGenesisHashToHex } from '@extension/utils'; + +const slice = createSlice({ + extraReducers: (builder) => { + /** fetch arc200 assets from storage **/ + builder.addCase( + fetchArc200AssetsFromStorageThunk.fulfilled, + ( + state: IArc200AssetsState, + action: PayloadAction> + ) => { + state.items = action.payload; + state.fetching = false; + } + ); + builder.addCase( + fetchArc200AssetsFromStorageThunk.pending, + (state: IArc200AssetsState) => { + state.fetching = true; + } + ); + builder.addCase( + fetchArc200AssetsFromStorageThunk.rejected, + (state: IArc200AssetsState) => { + state.fetching = false; + } + ); + /** update arc200 asset information **/ + builder.addCase( + updateArc200AssetInformationThunk.fulfilled, + ( + state: IArc200AssetsState, + action: PayloadAction + ) => { + state.items = { + ...state.items, + [convertGenesisHashToHex( + action.payload.network.genesisHash + ).toUpperCase()]: action.payload.arc200Assets, + }; + state.updating = false; + } + ); + builder.addCase( + updateArc200AssetInformationThunk.pending, + (state: IArc200AssetsState) => { + state.updating = true; + } + ); + builder.addCase( + updateArc200AssetInformationThunk.rejected, + (state: IArc200AssetsState) => { + state.updating = false; + } + ); + }, + initialState: getInitialState(), + name: StoreNameEnum.Arc200Assets, + reducers: { + noop: () => { + return; + }, + }, +}); + +export const reducer: Reducer = slice.reducer; diff --git a/src/extension/features/arc200-assets/thunks/fetchArc200AssetsFromStorageThunk.ts b/src/extension/features/arc200-assets/thunks/fetchArc200AssetsFromStorageThunk.ts new file mode 100644 index 00000000..cbeb93ad --- /dev/null +++ b/src/extension/features/arc200-assets/thunks/fetchArc200AssetsFromStorageThunk.ts @@ -0,0 +1,51 @@ +import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; + +// enums +import { Arc200AssetsThunkEnum } from '@extension/enums'; + +// services +import { Arc200AssetService } from '@extension/services'; + +// types +import { ILogger } from '@common/types'; +import { IArc200Asset, IMainRootState, INetwork } from '@extension/types'; + +// utils +import { convertGenesisHashToHex } from '@extension/utils'; + +const fetchArc200AssetsFromStorageThunk: AsyncThunk< + Record, // return + undefined, // args + Record +> = createAsyncThunk< + Record, + undefined, + { state: IMainRootState } +>( + Arc200AssetsThunkEnum.FetchArc200AssetsFromStorage, + async (_, { getState }) => { + const logger: ILogger = getState().system.logger; + const networks: INetwork[] = getState().networks.items; + const assetService: Arc200AssetService = new Arc200AssetService({ + logger, + }); + const assetItems: Record = {}; + + logger.debug( + `${Arc200AssetsThunkEnum.FetchArc200AssetsFromStorage}: fetching arc200 assets from storage` + ); + + await Promise.all( + networks.map( + async (network) => + (assetItems[ + convertGenesisHashToHex(network.genesisHash).toUpperCase() + ] = await assetService.getByGenesisHash(network.genesisHash)) + ) + ); + + return assetItems; + } +); + +export default fetchArc200AssetsFromStorageThunk; diff --git a/src/extension/features/arc200-assets/thunks/index.ts b/src/extension/features/arc200-assets/thunks/index.ts new file mode 100644 index 00000000..ca251314 --- /dev/null +++ b/src/extension/features/arc200-assets/thunks/index.ts @@ -0,0 +1,2 @@ +export { default as fetchArc200AssetsFromStorageThunk } from './fetchArc200AssetsFromStorageThunk'; +export { default as updateArc200AssetInformationThunk } from './updateArc200AssetInformationThunk'; diff --git a/src/extension/features/arc200-assets/thunks/updateArc200AssetInformationThunk.ts b/src/extension/features/arc200-assets/thunks/updateArc200AssetInformationThunk.ts new file mode 100644 index 00000000..6d6fa3c2 --- /dev/null +++ b/src/extension/features/arc200-assets/thunks/updateArc200AssetInformationThunk.ts @@ -0,0 +1,91 @@ +import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; + +// constants +import { NODE_REQUEST_DELAY } from '@extension/constants'; + +// enums +import { Arc200AssetsThunkEnum } from '@extension/enums'; + +// services +import { Arc200AssetService } from '@extension/services'; + +// types +import { ILogger } from '@common/types'; +import { IArc200Asset, IMainRootState } from '@extension/types'; +import { + IUpdateArc200AssetInformationPayload, + IUpdateArc200AssetInformationResult, +} from '../types'; + +// utils +import { upsertItemsById } from '@extension/utils'; +import { updateArc200AssetInformationById } from '../utils'; + +const updateArc200AssetInformationThunk: AsyncThunk< + IUpdateArc200AssetInformationResult, // return + IUpdateArc200AssetInformationPayload, // args + Record +> = createAsyncThunk< + IUpdateArc200AssetInformationResult, + IUpdateArc200AssetInformationPayload, + { state: IMainRootState } +>( + Arc200AssetsThunkEnum.UpdateArc200AssetInformation, + async ({ ids, network }, { getState }) => { + const logger: ILogger = getState().system.logger; + let asset: IArc200Asset | null; + let currentAssets: IArc200Asset[]; + let id: string; + let assetService: Arc200AssetService; + let updatedAssets: IArc200Asset[] = []; + + // get the information for each asset and add it to the array + for (let i: number = 0; i < ids.length; i++) { + id = ids[i]; + + try { + asset = await updateArc200AssetInformationById(id, { + delay: i * NODE_REQUEST_DELAY, // delay each request by 100ms from the last one, see https://algonode.io/api/#limits + logger, + network, + }); + + if (!asset) { + continue; + } + + logger.debug( + `${Arc200AssetsThunkEnum.UpdateArc200AssetInformation}: successfully updated asset information for arc200 asset "${id}" on "${network.genesisId}"` + ); + + updatedAssets.push(asset); + } catch (error) { + logger.error( + `${Arc200AssetsThunkEnum.UpdateArc200AssetInformation}: failed to get asset information for arc200 asset "${id}" on ${network.genesisId}: ${error.message}` + ); + } + } + + assetService = new Arc200AssetService({ + logger, + }); + currentAssets = await assetService.getByGenesisHash(network.genesisHash); + + logger.debug( + `${Arc200AssetsThunkEnum.UpdateArc200AssetInformation}: saving new asset information for network "${network.genesisId}" to storage` + ); + + // update the storage with the new asset information + currentAssets = await assetService.saveByGenesisHash( + network.genesisHash, + upsertItemsById(currentAssets, updatedAssets) + ); + + return { + network, + arc200Assets: currentAssets, + }; + } +); + +export default updateArc200AssetInformationThunk; diff --git a/src/extension/features/arc200-assets/types/IArc200AssetsState.ts b/src/extension/features/arc200-assets/types/IArc200AssetsState.ts new file mode 100644 index 00000000..ce9da1ad --- /dev/null +++ b/src/extension/features/arc200-assets/types/IArc200AssetsState.ts @@ -0,0 +1,18 @@ +// types +import { IArc200Asset } from '@extension/types'; + +/** + * @property {boolean} fetching - true when ARC200 assets are being fetched from storage. + * @property {Record | null} items - the ARC200 assets for each network, indexed by the + * hex encoded genesis hash. + * @property {boolean} saving - true when an asset is being saved to storage. + * @property {boolean} updating - true when remote ARC200 asset information is being updated. + */ +interface IArc200AssetsState { + fetching: boolean; + items: Record | null; + saving: boolean; + updating: boolean; +} + +export default IArc200AssetsState; diff --git a/src/extension/features/arc200-assets/types/IUpdateArc200AssetInformationPayload.ts b/src/extension/features/arc200-assets/types/IUpdateArc200AssetInformationPayload.ts new file mode 100644 index 00000000..728099a1 --- /dev/null +++ b/src/extension/features/arc200-assets/types/IUpdateArc200AssetInformationPayload.ts @@ -0,0 +1,12 @@ +import { INetwork } from '@extension/types'; + +/** + * @property {string[]} ids - the app IDs of the ARC200 assets to fetch information for. + * @property {INetwork} network - the network to fetch the ARC200 assets from. + */ +interface IUpdateArc200AssetInformationPayload { + ids: string[]; + network: INetwork; +} + +export default IUpdateArc200AssetInformationPayload; diff --git a/src/extension/features/arc200-assets/types/IUpdateArc200AssetInformationResult.ts b/src/extension/features/arc200-assets/types/IUpdateArc200AssetInformationResult.ts new file mode 100644 index 00000000..4c523bb3 --- /dev/null +++ b/src/extension/features/arc200-assets/types/IUpdateArc200AssetInformationResult.ts @@ -0,0 +1,13 @@ +// types +import { IArc200Asset, INetwork } from '@extension/types'; + +/** + * @property {IArc200Asset[]} arc200Assets - the updated ARC200 assets. + * @property {INetwork} network - the network. + */ +interface IUpdateArc200AssetInformationResult { + arc200Assets: IArc200Asset[]; + network: INetwork; +} + +export default IUpdateArc200AssetInformationResult; diff --git a/src/extension/features/arc200-assets/types/index.ts b/src/extension/features/arc200-assets/types/index.ts new file mode 100644 index 00000000..11a186c2 --- /dev/null +++ b/src/extension/features/arc200-assets/types/index.ts @@ -0,0 +1,3 @@ +export type { default as IArc200AssetsState } from './IArc200AssetsState'; +export type { default as IUpdateArc200AssetInformationPayload } from './IUpdateArc200AssetInformationPayload'; +export type { default as IUpdateArc200AssetInformationResult } from './IUpdateArc200AssetInformationResult'; diff --git a/src/extension/features/arc200-assets/utils/getInitialState.ts b/src/extension/features/arc200-assets/utils/getInitialState.ts new file mode 100644 index 00000000..dcc2db8a --- /dev/null +++ b/src/extension/features/arc200-assets/utils/getInitialState.ts @@ -0,0 +1,11 @@ +// types +import { IArc200AssetsState } from '../types'; + +export default function getInitialState(): IArc200AssetsState { + return { + fetching: false, + items: null, + saving: false, + updating: false, + }; +} diff --git a/src/extension/features/arc200-assets/utils/index.ts b/src/extension/features/arc200-assets/utils/index.ts new file mode 100644 index 00000000..5c227f1c --- /dev/null +++ b/src/extension/features/arc200-assets/utils/index.ts @@ -0,0 +1,2 @@ +export { default as getInitialState } from './getInitialState'; +export { default as updateArc200AssetInformationById } from './updateArc200AssetInformationById'; diff --git a/src/extension/features/arc200-assets/utils/updateArc200AssetInformationById.ts b/src/extension/features/arc200-assets/utils/updateArc200AssetInformationById.ts new file mode 100644 index 00000000..8a407ec4 --- /dev/null +++ b/src/extension/features/arc200-assets/utils/updateArc200AssetInformationById.ts @@ -0,0 +1,63 @@ +// types +import { IBaseOptions } from '@common/types'; +import { + IArc200AssetInformation, + IArc200Asset, + INetwork, +} from '@extension/types'; + +// utils +import { getAlgodClient, getIndexerClient } from '@common/utils'; +import { + fetchArc200AssetInformationWithDelay, + mapArc200AssetFromArc200AssetInformation, +} from '@extension/utils'; + +interface IOptions extends IBaseOptions { + delay?: number; + network: INetwork; +} + +/** + * Gets the ARC200 asset information. + * @param {string} id - the app ID of the ARC200 asset to fetch. + * @param {IOptions} options - options needed to fetch the ARC200 asset information. + * @returns {Promise} the ARC200 asset information, or null if there was an error. + */ +export default async function updateArc200AssetInformationById( + id: string, + { delay = 0, logger, network }: IOptions +): Promise { + let assetInformation: IArc200AssetInformation | null; + + try { + assetInformation = await fetchArc200AssetInformationWithDelay({ + algodClient: getAlgodClient(network, { + logger, + }), + delay, + id, + indexerClient: getIndexerClient(network, { + logger, + }), + }); + + if (!assetInformation) { + return null; + } + + return mapArc200AssetFromArc200AssetInformation( + id, + assetInformation, + null, + false + ); + } catch (error) { + logger && + logger.error( + `${updateArc200AssetInformationById.name}: failed to get arc200 asset information for arc200 asset "${id}" on ${network.genesisId}: ${error.message}` + ); + + return null; + } +} diff --git a/src/extension/features/assets/slice.ts b/src/extension/features/assets/slice.ts deleted file mode 100644 index 99ad07a3..00000000 --- a/src/extension/features/assets/slice.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { createSlice, PayloadAction, Reducer } from '@reduxjs/toolkit'; - -// enums -import { StoreNameEnum } from '@extension/enums'; - -// thunks -import { fetchAssetsThunk, updateAssetInformationThunk } from './thunks'; - -// types -import { IAsset } from '@extension/types'; -import { IAssetsState, IUpdateAssetInformationResult } from './types'; - -// utils -import { upsertItemsById } from '@extension/utils'; -import { getInitialState } from './utils'; - -const slice = createSlice({ - extraReducers: (builder) => { - /** Fetch assets **/ - builder.addCase( - fetchAssetsThunk.fulfilled, - ( - state: IAssetsState, - action: PayloadAction> - ) => { - state.items = action.payload; - state.fetching = false; - } - ); - builder.addCase(fetchAssetsThunk.pending, (state: IAssetsState) => { - state.fetching = true; - }); - builder.addCase(fetchAssetsThunk.rejected, (state: IAssetsState) => { - state.fetching = false; - }); - /** Update assets information **/ - builder.addCase( - updateAssetInformationThunk.fulfilled, - ( - state: IAssetsState, - action: PayloadAction - ) => { - if (action.payload) { - state.items = { - ...state.items, - [action.payload.encodedGenesisHash]: upsertItemsById( - state.items ? state.items[action.payload.encodedGenesisHash] : [], - action.payload.assets - ), - }; - } - - state.updating = false; - } - ); - builder.addCase( - updateAssetInformationThunk.pending, - (state: IAssetsState) => { - state.updating = true; - } - ); - builder.addCase( - updateAssetInformationThunk.rejected, - (state: IAssetsState) => { - state.updating = false; - } - ); - }, - initialState: getInitialState(), - name: StoreNameEnum.Assets, - reducers: { - noop: () => { - return; - }, - }, -}); - -export const reducer: Reducer = slice.reducer; diff --git a/src/extension/features/assets/thunks/fetchAssetsThunk.ts b/src/extension/features/assets/thunks/fetchAssetsThunk.ts deleted file mode 100644 index f08be700..00000000 --- a/src/extension/features/assets/thunks/fetchAssetsThunk.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; - -import { ASSETS_KEY_PREFIX } from '@extension/constants'; - -// enums -import { AssetsThunkEnum } from '@extension/enums'; - -// services -import { StorageManager } from '@extension/services'; - -// types -import { ILogger } from '@common/types'; -import { IAsset, IMainRootState, INetwork } from '@extension/types'; - -// utils -import { convertGenesisHashToHex } from '@extension/utils'; - -const fetchAssetsThunk: AsyncThunk< - Record, // return - undefined, // args - Record -> = createAsyncThunk< - Record, - undefined, - { state: IMainRootState } ->(AssetsThunkEnum.FetchAssets, async (_, { getState }) => { - const logger: ILogger = getState().system.logger; - const networks: INetwork[] = getState().networks.items; - const storageManager: StorageManager = new StorageManager(); - const assetItems: Record = {}; - - logger.debug(`${AssetsThunkEnum.FetchAssets}: fetching assets from storage`); - - await Promise.all( - networks.map(async (network) => { - const encodedGenesisHash: string = convertGenesisHashToHex( - network.genesisHash - ); // assets are stored by a hex encoded genesis hash - const assetsStorageKey: string = `${ASSETS_KEY_PREFIX}${encodedGenesisHash}`; - let assets: IAsset[] | null = await storageManager.getItem( - assetsStorageKey - ); - - // if we have no assets stored for this network, create a new entry - if (!assets) { - logger.debug( - `${AssetsThunkEnum.FetchAssets}: no asset entry found for "${network.genesisId}", creating an empty one` - ); - - assets = []; - - await storageManager.setItems({ - [assetsStorageKey]: assets, - }); - } - - assetItems[encodedGenesisHash] = assets; - }) - ); - - return assetItems; -}); - -export default fetchAssetsThunk; diff --git a/src/extension/features/assets/thunks/index.ts b/src/extension/features/assets/thunks/index.ts deleted file mode 100644 index f16518e2..00000000 --- a/src/extension/features/assets/thunks/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as fetchAssetsThunk } from './fetchAssetsThunk'; -export { default as updateAssetInformationThunk } from './updateAssetInformationThunk'; diff --git a/src/extension/features/assets/thunks/updateAssetInformationThunk.ts b/src/extension/features/assets/thunks/updateAssetInformationThunk.ts deleted file mode 100644 index 29992941..00000000 --- a/src/extension/features/assets/thunks/updateAssetInformationThunk.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; - -// constants -import { ASSETS_KEY_PREFIX, NODE_REQUEST_DELAY } from '@extension/constants'; - -// enums -import { AssetsThunkEnum } from '@extension/enums'; - -// services -import { StorageManager } from '@extension/services'; - -// types -import { ILogger } from '@common/types'; -import { IAsset, IMainRootState } from '@extension/types'; -import { - IUpdateAssetInformationPayload, - IUpdateAssetInformationResult, -} from '../types'; - -// utils -import { convertGenesisHashToHex, upsertItemsById } from '@extension/utils'; -import { fetchAssetInformationById } from '../utils'; - -const updateAssetInformationThunk: AsyncThunk< - IUpdateAssetInformationResult | null, // return - IUpdateAssetInformationPayload, // args - Record -> = createAsyncThunk< - IUpdateAssetInformationResult | null, - IUpdateAssetInformationPayload, - { state: IMainRootState } ->( - AssetsThunkEnum.UpdateAssetInformation, - async ({ ids, network }, { getState }) => { - const logger: ILogger = getState().system.logger; - const assets: IAsset[] = []; - let assetInformation: IAsset | null; - let assetsStorageKey: string; - let currentAssets: IAsset[]; - let encodedGenesisHash: string; - let id: string; - let storageManager: StorageManager; - - // get the information for each asset and add it to the array - for (let i: number = 0; i < ids.length; i++) { - id = ids[i]; - - try { - assetInformation = await fetchAssetInformationById(id, { - delay: i * NODE_REQUEST_DELAY, // delay each request by 100ms from the last one, see https://algonode.io/api/#limits - logger, - network, - }); - - if (!assetInformation) { - continue; - } - - logger.debug( - `${AssetsThunkEnum.UpdateAssetInformation}: successfully updated asset information for "${id}" on "${network.genesisId}"` - ); - - assets.push(assetInformation); - } catch (error) { - logger.error( - `${AssetsThunkEnum.UpdateAssetInformation}: failed to get asset information for asset "${id}" on ${network.genesisId}: ${error.message}` - ); - } - } - - storageManager = new StorageManager(); - encodedGenesisHash = convertGenesisHashToHex(network.genesisHash); - assetsStorageKey = `${ASSETS_KEY_PREFIX}${encodedGenesisHash}`; - currentAssets = - (await storageManager.getItem(assetsStorageKey)) || []; // get the current assets from storage - - logger.debug( - `${AssetsThunkEnum.UpdateAssetInformation}: saving new asset information for network "${network.genesisId}" to storage` - ); - - // update the storage with the new asset information - await storageManager.setItems({ - [assetsStorageKey]: upsertItemsById(currentAssets, assets), - }); - - return { - assets, - encodedGenesisHash, - }; - } -); - -export default updateAssetInformationThunk; diff --git a/src/extension/features/assets/types/IUpdateAssetInformationPayload.ts b/src/extension/features/assets/types/IUpdateAssetInformationPayload.ts deleted file mode 100644 index 265b505e..00000000 --- a/src/extension/features/assets/types/IUpdateAssetInformationPayload.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { INetwork } from '@extension/types'; - -/** - * @property {string[]} ids - the ID of the assets to fetch information for. - * @property {INetwork} network - the network to fetch assets from. - */ -interface IUpdateAssetInformationPayload { - ids: string[]; - network: INetwork; -} - -export default IUpdateAssetInformationPayload; diff --git a/src/extension/features/assets/types/IUpdateAssetInformationResult.ts b/src/extension/features/assets/types/IUpdateAssetInformationResult.ts deleted file mode 100644 index 95e3b4b7..00000000 --- a/src/extension/features/assets/types/IUpdateAssetInformationResult.ts +++ /dev/null @@ -1,13 +0,0 @@ -// types -import { IAsset } from '@extension/types'; - -/** - * @property {string} encodedGenesisHash - the hex encoded genesis hash of the network. - * @property {IAsset[]} assets - the updated assets. - */ -interface IUpdateAssetInformationResult { - assets: IAsset[]; - encodedGenesisHash: string; -} - -export default IUpdateAssetInformationResult; diff --git a/src/extension/features/assets/types/index.ts b/src/extension/features/assets/types/index.ts deleted file mode 100644 index 2998d7ec..00000000 --- a/src/extension/features/assets/types/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { default as IAssetsState } from './IAssetsState'; -export type { default as IUpdateAssetInformationPayload } from './IUpdateAssetInformationPayload'; -export type { default as IUpdateAssetInformationResult } from './IUpdateAssetInformationResult'; diff --git a/src/extension/features/assets/utils/getInitialState.ts b/src/extension/features/assets/utils/getInitialState.ts deleted file mode 100644 index e3ff2aad..00000000 --- a/src/extension/features/assets/utils/getInitialState.ts +++ /dev/null @@ -1,11 +0,0 @@ -// types -import { IAssetsState } from '../types'; - -export default function getInitialState(): IAssetsState { - return { - fetching: false, - items: null, - saving: false, - updating: false, - }; -} diff --git a/src/extension/features/assets/utils/index.ts b/src/extension/features/assets/utils/index.ts deleted file mode 100644 index 88e95f0a..00000000 --- a/src/extension/features/assets/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as fetchAssetInformationById } from './fetchAssetInformationById'; -export { default as fetchAssetInformationWithDelay } from './fetchAssetInformationWithDelay'; -export { default as getInitialState } from './getInitialState'; diff --git a/src/extension/features/send-assets/slice.ts b/src/extension/features/send-assets/slice.ts index 7c0262a4..90121b4d 100644 --- a/src/extension/features/send-assets/slice.ts +++ b/src/extension/features/send-assets/slice.ts @@ -16,7 +16,7 @@ import { StoreNameEnum } from '@extension/enums'; import { submitTransactionThunk } from './thunks'; // types -import { IAsset, IRejectedActionMeta } from '@extension/types'; +import { IStandardAsset, IRejectedActionMeta } from '@extension/types'; import { IInitializeSendAssetPayload, ISendAssetsState } from './types'; // utils @@ -100,7 +100,7 @@ const slice = createSlice({ }, setSelectedAsset: ( state: Draft, - action: PayloadAction + action: PayloadAction ) => { state.selectedAsset = action.payload; }, diff --git a/src/extension/features/send-assets/thunks/submitTransactionThunk.ts b/src/extension/features/send-assets/thunks/submitTransactionThunk.ts index 92a3d85a..27a16595 100644 --- a/src/extension/features/send-assets/thunks/submitTransactionThunk.ts +++ b/src/extension/features/send-assets/thunks/submitTransactionThunk.ts @@ -31,7 +31,7 @@ import { ILogger } from '@common/types'; import { IAccount, IAlgorandPendingTransactionResponse, - IAsset, + IStandardAsset, IMainRootState, INetworkWithTransactionParams, } from '@extension/types'; @@ -54,7 +54,7 @@ const submitTransactionThunk: AsyncThunk< SendAssetsThunkEnum.SubmitTransaction, async (password, { getState, rejectWithValue }) => { const amount: string | null = getState().sendAssets.amount; - const asset: IAsset | null = getState().sendAssets.selectedAsset; + const asset: IStandardAsset | null = getState().sendAssets.selectedAsset; const fromAddress: string | null = getState().sendAssets.fromAddress; const logger: ILogger = getState().system.logger; const networks: INetworkWithTransactionParams[] = getState().networks.items; diff --git a/src/extension/features/send-assets/types/IInitializeSendAssetPayload.ts b/src/extension/features/send-assets/types/IInitializeSendAssetPayload.ts index b080e12e..f6f31599 100644 --- a/src/extension/features/send-assets/types/IInitializeSendAssetPayload.ts +++ b/src/extension/features/send-assets/types/IInitializeSendAssetPayload.ts @@ -1,8 +1,8 @@ -import { IAsset } from '@extension/types'; +import { IStandardAsset } from '@extension/types'; interface IInitializeSendAssetPayload { fromAddress: string | null; - selectedAsset: IAsset; + selectedAsset: IStandardAsset; } export default IInitializeSendAssetPayload; diff --git a/src/extension/features/send-assets/types/ISendAssetsState.ts b/src/extension/features/send-assets/types/ISendAssetsState.ts index 57de0bd1..f4b47cf9 100644 --- a/src/extension/features/send-assets/types/ISendAssetsState.ts +++ b/src/extension/features/send-assets/types/ISendAssetsState.ts @@ -2,7 +2,7 @@ import { BaseExtensionError } from '@extension/errors'; // types -import { IAsset } from '@extension/types'; +import { IStandardAsset } from '@extension/types'; /** * @property {string} amount - the amount to send. Default is "0". @@ -10,7 +10,7 @@ import { IAsset } from '@extension/types'; * @property {BaseExtensionError | null} error - if an error occurred. * @property {string | null} fromAddress - the address to send from. * @property {string | null} note - the note to send. - * @property {IAsset | null} selectedAsset - the selected asset to send. + * @property {IStandardAsset | null} selectedAsset - the selected asset to send. * @property {string | null} toAddress - the address to send to. * @property {string | null} transactionId - the ID of the confirmed transaction. */ @@ -20,7 +20,7 @@ interface ISendAssetsState { error: BaseExtensionError | null; fromAddress: string | null; note: string | null; - selectedAsset: IAsset | null; + selectedAsset: IStandardAsset | null; toAddress: string | null; transactionId: string | null; } diff --git a/src/extension/features/send-assets/utils/createSendAssetTransaction.ts b/src/extension/features/send-assets/utils/createSendAssetTransaction.ts index db9a1b1f..5035b629 100644 --- a/src/extension/features/send-assets/utils/createSendAssetTransaction.ts +++ b/src/extension/features/send-assets/utils/createSendAssetTransaction.ts @@ -6,11 +6,11 @@ import { } from 'algosdk'; // types -import { IAsset } from '@extension/types'; +import { IStandardAsset } from '@extension/types'; interface IOptions { amount: string; - asset: IAsset; + asset: IStandardAsset; fromAddress: string; note: string | null; suggestedParams: SuggestedParams; diff --git a/src/extension/features/standard-assets/index.ts b/src/extension/features/standard-assets/index.ts new file mode 100644 index 00000000..0741f6b4 --- /dev/null +++ b/src/extension/features/standard-assets/index.ts @@ -0,0 +1,4 @@ +export * from './slice'; +export * from './thunks'; +export * from './types'; +export * from './utils'; diff --git a/src/extension/features/standard-assets/slice.ts b/src/extension/features/standard-assets/slice.ts new file mode 100644 index 00000000..e1f22ce9 --- /dev/null +++ b/src/extension/features/standard-assets/slice.ts @@ -0,0 +1,86 @@ +import { createSlice, PayloadAction, Reducer } from '@reduxjs/toolkit'; + +// enums +import { StoreNameEnum } from '@extension/enums'; + +// thunks +import { + fetchStandardAssetsFromStorageThunk, + updateStandardAssetInformationThunk, +} from './thunks'; + +// types +import { IStandardAsset } from '@extension/types'; +import { + IStandardAssetsState, + IUpdateStandardAssetInformationResult, +} from './types'; + +// utils +import { getInitialState } from './utils'; +import { convertGenesisHashToHex } from '@extension/utils'; + +const slice = createSlice({ + extraReducers: (builder) => { + /** fetch assets from storage **/ + builder.addCase( + fetchStandardAssetsFromStorageThunk.fulfilled, + ( + state: IStandardAssetsState, + action: PayloadAction> + ) => { + state.items = action.payload; + state.fetching = false; + } + ); + builder.addCase( + fetchStandardAssetsFromStorageThunk.pending, + (state: IStandardAssetsState) => { + state.fetching = true; + } + ); + builder.addCase( + fetchStandardAssetsFromStorageThunk.rejected, + (state: IStandardAssetsState) => { + state.fetching = false; + } + ); + /** update standard asset information **/ + builder.addCase( + updateStandardAssetInformationThunk.fulfilled, + ( + state: IStandardAssetsState, + action: PayloadAction + ) => { + state.items = { + ...state.items, + [convertGenesisHashToHex( + action.payload.network.genesisHash + ).toUpperCase()]: action.payload.standardAssets, + }; + state.updating = false; + } + ); + builder.addCase( + updateStandardAssetInformationThunk.pending, + (state: IStandardAssetsState) => { + state.updating = true; + } + ); + builder.addCase( + updateStandardAssetInformationThunk.rejected, + (state: IStandardAssetsState) => { + state.updating = false; + } + ); + }, + initialState: getInitialState(), + name: StoreNameEnum.StandardAssets, + reducers: { + noop: () => { + return; + }, + }, +}); + +export const reducer: Reducer = slice.reducer; diff --git a/src/extension/features/standard-assets/thunks/fetchStandardAssetsFromStorageThunk.ts b/src/extension/features/standard-assets/thunks/fetchStandardAssetsFromStorageThunk.ts new file mode 100644 index 00000000..1534b234 --- /dev/null +++ b/src/extension/features/standard-assets/thunks/fetchStandardAssetsFromStorageThunk.ts @@ -0,0 +1,48 @@ +import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; + +// enums +import { StandardAssetsThunkEnum } from '@extension/enums'; + +// services +import { StandardAssetService } from '@extension/services'; + +// types +import { ILogger } from '@common/types'; +import { IStandardAsset, IMainRootState, INetwork } from '@extension/types'; + +// utils +import { convertGenesisHashToHex } from '@extension/utils'; + +const fetchStandardAssetsFromStorageThunk: AsyncThunk< + Record, // return + undefined, // args + Record +> = createAsyncThunk< + Record, + undefined, + { state: IMainRootState } +>(StandardAssetsThunkEnum.FetchAssetsFromStorage, async (_, { getState }) => { + const logger: ILogger = getState().system.logger; + const networks: INetwork[] = getState().networks.items; + const assetService: StandardAssetService = new StandardAssetService({ + logger, + }); + const assetItems: Record = {}; + + logger.debug( + `${StandardAssetsThunkEnum.FetchAssetsFromStorage}: fetching assets from storage` + ); + + await Promise.all( + networks.map( + async (network) => + (assetItems[ + convertGenesisHashToHex(network.genesisHash).toUpperCase() + ] = await assetService.getByGenesisHash(network.genesisHash)) + ) + ); + + return assetItems; +}); + +export default fetchStandardAssetsFromStorageThunk; diff --git a/src/extension/features/standard-assets/thunks/index.ts b/src/extension/features/standard-assets/thunks/index.ts new file mode 100644 index 00000000..0637dbfa --- /dev/null +++ b/src/extension/features/standard-assets/thunks/index.ts @@ -0,0 +1,2 @@ +export { default as fetchStandardAssetsFromStorageThunk } from './fetchStandardAssetsFromStorageThunk'; +export { default as updateStandardAssetInformationThunk } from './updateStandardAssetInformationThunk'; diff --git a/src/extension/features/standard-assets/thunks/updateStandardAssetInformationThunk.ts b/src/extension/features/standard-assets/thunks/updateStandardAssetInformationThunk.ts new file mode 100644 index 00000000..5e96248c --- /dev/null +++ b/src/extension/features/standard-assets/thunks/updateStandardAssetInformationThunk.ts @@ -0,0 +1,91 @@ +import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; + +// constants +import { NODE_REQUEST_DELAY } from '@extension/constants'; + +// enums +import { StandardAssetsThunkEnum } from '@extension/enums'; + +// services +import { StandardAssetService } from '@extension/services'; + +// types +import { ILogger } from '@common/types'; +import { IMainRootState, IStandardAsset } from '@extension/types'; +import { + IUpdateStandardAssetInformationPayload, + IUpdateStandardAssetInformationResult, +} from '../types'; + +// utils +import { convertGenesisHashToHex, upsertItemsById } from '@extension/utils'; +import { updateStandardAssetInformationById } from '../utils'; + +const updateStandardAssetInformationThunk: AsyncThunk< + IUpdateStandardAssetInformationResult, // return + IUpdateStandardAssetInformationPayload, // args + Record +> = createAsyncThunk< + IUpdateStandardAssetInformationResult, + IUpdateStandardAssetInformationPayload, + { state: IMainRootState } +>( + StandardAssetsThunkEnum.UpdateStandardAssetInformation, + async ({ ids, network }, { getState }) => { + const logger: ILogger = getState().system.logger; + let asset: IStandardAsset | null; + let currentAssets: IStandardAsset[]; + let id: string; + let assetService: StandardAssetService; + let updatedAssets: IStandardAsset[] = []; + + // get the information for each asset and add it to the array + for (let i: number = 0; i < ids.length; i++) { + id = ids[i]; + + try { + asset = await updateStandardAssetInformationById(id, { + delay: i * NODE_REQUEST_DELAY, // delay each request by 100ms from the last one, see https://algonode.io/api/#limits + logger, + network, + }); + + if (!asset) { + continue; + } + + logger.debug( + `${StandardAssetsThunkEnum.UpdateStandardAssetInformation}: successfully updated asset information for standard asset "${id}" on "${network.genesisId}"` + ); + + updatedAssets.push(asset); + } catch (error) { + logger.error( + `${StandardAssetsThunkEnum.UpdateStandardAssetInformation}: failed to get asset information for standard asset "${id}" on ${network.genesisId}: ${error.message}` + ); + } + } + + assetService = new StandardAssetService({ + logger, + }); + currentAssets = await assetService.getByGenesisHash(network.genesisHash); + + logger.debug( + `${StandardAssetsThunkEnum.UpdateStandardAssetInformation}: saving new asset information for network "${network.genesisId}" to storage` + ); + + // update the storage with the new asset information + currentAssets = await assetService.saveByGenesisHash( + network.genesisHash, + upsertItemsById(currentAssets, updatedAssets) + ); + + return { + network, + standardAssets: currentAssets, + }; + } +); + +export default updateStandardAssetInformationThunk; diff --git a/src/extension/features/assets/types/IAssetsState.ts b/src/extension/features/standard-assets/types/IStandardAssetsState.ts similarity index 51% rename from src/extension/features/assets/types/IAssetsState.ts rename to src/extension/features/standard-assets/types/IStandardAssetsState.ts index 24c86817..188942f3 100644 --- a/src/extension/features/assets/types/IAssetsState.ts +++ b/src/extension/features/standard-assets/types/IStandardAssetsState.ts @@ -1,18 +1,18 @@ // types -import { IAsset } from '@extension/types'; +import { IStandardAsset } from '@extension/types'; /** * @property {boolean} fetching - true when assets are being fetched from storage. - * @property {Record | null} items - the assets for each network, indexed by their hex encoded genesis - * hash. + * @property {Record | null} items - the standard assets for each network, indexed by the + * hex encoded genesis hash. * @property {boolean} saving - true when an asset is being saved to storage. * @property {boolean} updating - true when remote asset information is being updated. */ -interface IAssetsState { +interface IStandardAssetsState { fetching: boolean; - items: Record | null; + items: Record | null; saving: boolean; updating: boolean; } -export default IAssetsState; +export default IStandardAssetsState; diff --git a/src/extension/features/standard-assets/types/IUpdateStandardAssetInformationPayload.ts b/src/extension/features/standard-assets/types/IUpdateStandardAssetInformationPayload.ts new file mode 100644 index 00000000..988abc35 --- /dev/null +++ b/src/extension/features/standard-assets/types/IUpdateStandardAssetInformationPayload.ts @@ -0,0 +1,12 @@ +import { INetwork } from '@extension/types'; + +/** + * @property {string[]} ids - the ID of the standard assets to fetch information for. + * @property {INetwork} network - the network to fetch assets from. + */ +interface IUpdateStandardAssetInformationPayload { + ids: string[]; + network: INetwork; +} + +export default IUpdateStandardAssetInformationPayload; diff --git a/src/extension/features/standard-assets/types/IUpdateStandardAssetInformationResult.ts b/src/extension/features/standard-assets/types/IUpdateStandardAssetInformationResult.ts new file mode 100644 index 00000000..12dc7b1e --- /dev/null +++ b/src/extension/features/standard-assets/types/IUpdateStandardAssetInformationResult.ts @@ -0,0 +1,13 @@ +// types +import { IStandardAsset, INetwork } from '@extension/types'; + +/** + * @property {IStandardAsset[]} standardAssets - the updated standard assets. + * @property {INetwork} network - the network. + */ +interface IUpdateStandardAssetInformationResult { + network: INetwork; + standardAssets: IStandardAsset[]; +} + +export default IUpdateStandardAssetInformationResult; diff --git a/src/extension/features/standard-assets/types/index.ts b/src/extension/features/standard-assets/types/index.ts new file mode 100644 index 00000000..9f4c07ba --- /dev/null +++ b/src/extension/features/standard-assets/types/index.ts @@ -0,0 +1,3 @@ +export type { default as IStandardAssetsState } from './IStandardAssetsState'; +export type { default as IUpdateStandardAssetInformationPayload } from './IUpdateStandardAssetInformationPayload'; +export type { default as IUpdateStandardAssetInformationResult } from './IUpdateStandardAssetInformationResult'; diff --git a/src/extension/features/assets/utils/fetchAssetInformationWithDelay.ts b/src/extension/features/standard-assets/utils/fetchStandardAssetInformationWithDelay.ts similarity index 85% rename from src/extension/features/assets/utils/fetchAssetInformationWithDelay.ts rename to src/extension/features/standard-assets/utils/fetchStandardAssetInformationWithDelay.ts index 30f07f47..6987a55b 100644 --- a/src/extension/features/assets/utils/fetchAssetInformationWithDelay.ts +++ b/src/extension/features/standard-assets/utils/fetchStandardAssetInformationWithDelay.ts @@ -10,11 +10,11 @@ interface IOptions { } /** - * Fetches asset information from the node with a delay. + * Fetches standard asset information from the node with a delay. * @param {IOptions} options - options needed to send the request. * @returns {IAlgorandAsset} asset information from the node. */ -export default async function fetchAssetInformationWithDelay({ +export default async function fetchStandardAssetInformationWithDelay({ client, delay, id, diff --git a/src/extension/features/standard-assets/utils/getInitialState.ts b/src/extension/features/standard-assets/utils/getInitialState.ts new file mode 100644 index 00000000..0ebce787 --- /dev/null +++ b/src/extension/features/standard-assets/utils/getInitialState.ts @@ -0,0 +1,11 @@ +// types +import { IStandardAssetsState } from '../types'; + +export default function getInitialState(): IStandardAssetsState { + return { + fetching: false, + items: null, + saving: false, + updating: false, + }; +} diff --git a/src/extension/features/standard-assets/utils/index.ts b/src/extension/features/standard-assets/utils/index.ts new file mode 100644 index 00000000..beaad8b4 --- /dev/null +++ b/src/extension/features/standard-assets/utils/index.ts @@ -0,0 +1,3 @@ +export { default as fetchAssetInformationWithDelay } from './fetchStandardAssetInformationWithDelay'; +export { default as updateStandardAssetInformationById } from './updateStandardAssetInformationById'; +export { default as getInitialState } from './getInitialState'; diff --git a/src/extension/features/assets/utils/fetchAssetInformationById.ts b/src/extension/features/standard-assets/utils/updateStandardAssetInformationById.ts similarity index 51% rename from src/extension/features/assets/utils/fetchAssetInformationById.ts rename to src/extension/features/standard-assets/utils/updateStandardAssetInformationById.ts index fe1f2b9e..c13cc590 100644 --- a/src/extension/features/assets/utils/fetchAssetInformationById.ts +++ b/src/extension/features/standard-assets/utils/updateStandardAssetInformationById.ts @@ -4,7 +4,7 @@ import { Algodv2 } from 'algosdk'; import { IBaseOptions } from '@common/types'; import { IAlgorandAsset, - IAsset, + IStandardAsset, INetwork, ITinyManAssetResponse, } from '@extension/types'; @@ -14,9 +14,9 @@ import { getAlgodClient } from '@common/utils'; import { fetchAssetList, fetchAssetVerification, - mapAssetFromAlgorandAsset, + mapStandardAssetFromAlgorandAsset, } from '@extension/utils'; -import fetchAssetInformationWithDelay from './fetchAssetInformationWithDelay'; +import fetchStandardAssetInformationWithDelay from './fetchStandardAssetInformationWithDelay'; interface IOptions extends IBaseOptions { delay?: number; @@ -24,23 +24,23 @@ interface IOptions extends IBaseOptions { } /** - * Gets the asset information. - * @param {string} id - the ID of the asset to fetch. - * @param {IOptions} options - options needed to fetch the asset information. - * @returns {Promise} the asset information of null if there was an error. + * Gets the standard asset information. + * @param {string} id - the ID of the standard asset to fetch. + * @param {IOptions} options - options needed to fetch the standard asset information. + * @returns {Promise} the standard asset information, or null if there was an error. */ -export default async function fetchAssetInformationById( +export default async function updateStandardAssetInformationById( id: string, { delay = 0, logger, network }: IOptions -): Promise { - let assetInformation: IAlgorandAsset; - let assetList: Record | null = null; +): Promise { + let standardAssetInformation: IAlgorandAsset; + let standardAssetList: Record | null = null; let client: Algodv2; let verified: boolean; // TODO: asset list only exists for algorand mainnet, move this url to config? if (network.genesisHash === 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=') { - assetList = await fetchAssetList({ + standardAssetList = await fetchAssetList({ logger, }); } @@ -51,7 +51,7 @@ export default async function fetchAssetInformationById( verified = false; try { - assetInformation = await fetchAssetInformationWithDelay({ + standardAssetInformation = await fetchStandardAssetInformationWithDelay({ client, delay, id, @@ -59,7 +59,7 @@ export default async function fetchAssetInformationById( logger && logger.debug( - `${fetchAssetInformationById.name}: getting verified status for "${id}" on "${network.genesisId}"` + `${updateStandardAssetInformationById.name}: getting verified status for "${id}" on "${network.genesisId}"` ); // TODO: asset list only exists for algorand mainnet, move this url to config? @@ -72,15 +72,15 @@ export default async function fetchAssetInformationById( }); } - return mapAssetFromAlgorandAsset( - assetInformation, - assetList ? assetList[id]?.logo.svg || null : null, + return mapStandardAssetFromAlgorandAsset( + standardAssetInformation, + standardAssetList ? standardAssetList[id]?.logo.svg || null : null, verified ); } catch (error) { logger && logger.error( - `${fetchAssetInformationById.name}: failed to get asset information for asset "${id}" on ${network.genesisId}: ${error.message}` + `${updateStandardAssetInformationById.name}: failed to get asset information for asset "${id}" on ${network.genesisId}: ${error.message}` ); return null; diff --git a/src/extension/hooks/useAsset/index.ts b/src/extension/hooks/useAsset/index.ts deleted file mode 100644 index cf9a3724..00000000 --- a/src/extension/hooks/useAsset/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './useAsset'; -export * from './types'; diff --git a/src/extension/hooks/useAsset/types/IUseAssetState.ts b/src/extension/hooks/useAsset/types/IUseAssetState.ts deleted file mode 100644 index f6a69104..00000000 --- a/src/extension/hooks/useAsset/types/IUseAssetState.ts +++ /dev/null @@ -1,9 +0,0 @@ -// types -import { IAsset } from '@extension/types'; - -interface IUseAssetState { - asset: IAsset | null; - updating: boolean; -} - -export default IUseAssetState; diff --git a/src/extension/hooks/useAsset/types/index.ts b/src/extension/hooks/useAsset/types/index.ts deleted file mode 100644 index 7383751f..00000000 --- a/src/extension/hooks/useAsset/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { default as IUseAssetState } from './IUseAssetState'; diff --git a/src/extension/hooks/useAsset/useAsset.ts b/src/extension/hooks/useAsset/useAsset.ts deleted file mode 100644 index 9d24c382..00000000 --- a/src/extension/hooks/useAsset/useAsset.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useDispatch } from 'react-redux'; - -// features -import { updateAssetInformationThunk } from '@extension/features/assets'; - -// hooks -import useAssets from '@extension/hooks/useAssets'; - -// selectors -import { - useSelectSelectedNetwork, - useSelectUpdatingAssets, -} from '@extension/selectors'; - -// types -import { IAppThunkDispatch, IAsset, INetwork } from '@extension/types'; -import { IUseAssetState } from './types'; - -export default function useAsset(assetId: string): IUseAssetState { - const dispatch: IAppThunkDispatch = useDispatch(); - // selectors - const selectedNetwork: INetwork | null = useSelectSelectedNetwork(); - const updatingAssets: boolean = useSelectUpdatingAssets(); - //hooks - const assets: IAsset[] = useAssets(); - // state - const [asset, setAsset] = useState(null); - - // fetch unknown asset information - useEffect(() => { - const selectedAsset: IAsset | null = - assets.find((value) => value.id === assetId) || null; - - if (selectedAsset) { - setAsset(selectedAsset); - - return; - } - - // if we don't have the asset, get the asset information - if (selectedNetwork) { - // dispatch( - // updateAssetInformationThunk({ - // ids: [assetId], - // network: selectedNetwork, - // }) - // ); - } - }, [assets]); - - return { - asset, - updating: updatingAssets, - }; -} diff --git a/src/extension/hooks/useAssets/index.ts b/src/extension/hooks/useAssets/index.ts deleted file mode 100644 index 6d755e9d..00000000 --- a/src/extension/hooks/useAssets/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './useAssets'; diff --git a/src/extension/hooks/useAssets/useAssets.ts b/src/extension/hooks/useAssets/useAssets.ts deleted file mode 100644 index d474e888..00000000 --- a/src/extension/hooks/useAssets/useAssets.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useEffect, useState } from 'react'; - -// selectors -import { - useSelectAssets, - useSelectSelectedNetwork, -} from '@extension/selectors'; - -// types -import { IAsset, INetwork } from '@extension/types'; - -// utils -import { convertGenesisHashToHex } from '@extension/utils'; - -export default function useAssets(): IAsset[] { - const selectedNetwork: INetwork | null = useSelectSelectedNetwork(); - const assetItems: Record | null = useSelectAssets(); - const [assets, setAssets] = useState([]); - - useEffect(() => { - let selectedAssets: IAsset[] = []; - - // if the assets have updated or the network has changed, get the new assets - if (assetItems && selectedNetwork) { - selectedAssets = - assetItems[convertGenesisHashToHex(selectedNetwork.genesisHash)] || []; - } - - setAssets(selectedAssets); - }, [assetItems, selectedNetwork]); - - return assets; -} diff --git a/src/extension/hooks/useOnNewAssets/index.ts b/src/extension/hooks/useOnNewAssets/index.ts new file mode 100644 index 00000000..4b56d577 --- /dev/null +++ b/src/extension/hooks/useOnNewAssets/index.ts @@ -0,0 +1 @@ +export { default } from './useOnNewAssets'; diff --git a/src/extension/hooks/useOnNewAssets/useOnNewAssets.ts b/src/extension/hooks/useOnNewAssets/useOnNewAssets.ts new file mode 100644 index 00000000..b73df8a5 --- /dev/null +++ b/src/extension/hooks/useOnNewAssets/useOnNewAssets.ts @@ -0,0 +1,108 @@ +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; + +// features +import { updateArc200AssetInformationThunk } from '@extension/features/arc200-assets'; +import { updateStandardAssetInformationThunk } from '@extension/features/standard-assets'; + +// selectors +import { + useSelectAccounts, + useSelectArc200AssetsBySelectedNetwork, + useSelectSelectedNetwork, + useSelectStandardAssetsBySelectedNetwork, +} from '@extension/selectors'; + +// types +import { + IAccount, + IAccountInformation, + IAppThunkDispatch, + IArc200Asset, + IArc200AssetHolding, + INetwork, + IStandardAsset, + IStandardAssetHolding, +} from '@extension/types'; + +// utils +import { convertGenesisHashToHex } from '@extension/utils'; + +/** + * Checks for changes in the accounts and gets new asset information if any new assets. + */ +export default function useOnNewAssets(): void { + const dispatch: IAppThunkDispatch = useDispatch(); + // selectors + const accounts: IAccount[] = useSelectAccounts(); + const arc200Assets: IArc200Asset[] = useSelectArc200AssetsBySelectedNetwork(); + const standardAssets: IStandardAsset[] = + useSelectStandardAssetsBySelectedNetwork(); + const selectedNetwork: INetwork | null = useSelectSelectedNetwork(); + + // check for any new arc200 assets + useEffect(() => { + if (accounts.length > 0 && arc200Assets && selectedNetwork) { + accounts.forEach((account) => { + const encodedGenesisHash: string = convertGenesisHashToHex( + selectedNetwork.genesisHash + ).toUpperCase(); + const accountInformation: IAccountInformation | null = + account.networkInformation[encodedGenesisHash] || null; + let newArc200AssetHoldings: IArc200AssetHolding[]; + + if (accountInformation) { + // filter out any new arc200 assets + newArc200AssetHoldings = + accountInformation.arc200AssetHoldings.filter( + (assetHolding) => + !arc200Assets.some((value) => value.id === assetHolding.id) + ); + + // if we have any new arc200 assets, update the information + if (newArc200AssetHoldings.length > 0) { + dispatch( + updateArc200AssetInformationThunk({ + ids: newArc200AssetHoldings.map((value) => value.id), + network: selectedNetwork, + }) + ); + } + } + }); + } + }, [accounts]); + + // check for any new standard assets + useEffect(() => { + if (accounts.length > 0 && standardAssets && selectedNetwork) { + accounts.forEach((account) => { + const encodedGenesisHash: string = convertGenesisHashToHex( + selectedNetwork.genesisHash + ).toUpperCase(); + const accountInformation: IAccountInformation | null = + account.networkInformation[encodedGenesisHash] || null; + let newStandardAssetHoldings: IStandardAssetHolding[]; + + if (accountInformation) { + // filter out any new standard assets + newStandardAssetHoldings = + accountInformation.standardAssetHoldings.filter( + (assetHolding) => + !standardAssets.some((value) => value.id === assetHolding.id) + ); + + // if we have any new standard assets, update the information + if (newStandardAssetHoldings.length > 0) { + dispatch( + updateStandardAssetInformationThunk({ + ids: newStandardAssetHoldings.map((value) => value.id), + network: selectedNetwork, + }) + ); + } + } + }); + } + }, [accounts]); +} diff --git a/src/extension/hooks/useStandardAssetById/index.ts b/src/extension/hooks/useStandardAssetById/index.ts new file mode 100644 index 00000000..c1b1d255 --- /dev/null +++ b/src/extension/hooks/useStandardAssetById/index.ts @@ -0,0 +1,2 @@ +export { default } from './useStandardAssetById'; +export * from './types'; diff --git a/src/extension/hooks/useStandardAssetById/types/IUseStandardAssetByIdState.ts b/src/extension/hooks/useStandardAssetById/types/IUseStandardAssetByIdState.ts new file mode 100644 index 00000000..d3af85b9 --- /dev/null +++ b/src/extension/hooks/useStandardAssetById/types/IUseStandardAssetByIdState.ts @@ -0,0 +1,9 @@ +// types +import { IStandardAsset } from '@extension/types'; + +interface IUseStandardAssetByIdState { + standardAsset: IStandardAsset | null; + updating: boolean; +} + +export default IUseStandardAssetByIdState; diff --git a/src/extension/hooks/useStandardAssetById/types/index.ts b/src/extension/hooks/useStandardAssetById/types/index.ts new file mode 100644 index 00000000..ea011bba --- /dev/null +++ b/src/extension/hooks/useStandardAssetById/types/index.ts @@ -0,0 +1 @@ +export type { default as IUseStandardAssetByIdState } from './IUseStandardAssetByIdState'; diff --git a/src/extension/hooks/useStandardAssetById/useStandardAssetById.ts b/src/extension/hooks/useStandardAssetById/useStandardAssetById.ts new file mode 100644 index 00000000..ba36aa7c --- /dev/null +++ b/src/extension/hooks/useStandardAssetById/useStandardAssetById.ts @@ -0,0 +1,58 @@ +import { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; + +// features +import { updateStandardAssetInformationThunk } from '@extension/features/standard-assets'; + +// selectors +import { + useSelectSelectedNetwork, + useSelectStandardAssetsBySelectedNetwork, + useSelectUpdatingStandardAssets, +} from '@extension/selectors'; + +// types +import { IAppThunkDispatch, IStandardAsset, INetwork } from '@extension/types'; +import { IUseStandardAssetByIdState } from './types'; + +export default function useStandardAssetById( + id: string +): IUseStandardAssetByIdState { + const dispatch: IAppThunkDispatch = useDispatch(); + // selectors + const selectedNetwork: INetwork | null = useSelectSelectedNetwork(); + const standardAssets: IStandardAsset[] = + useSelectStandardAssetsBySelectedNetwork(); + const updatingStandardAssets: boolean = useSelectUpdatingStandardAssets(); + // state + const [standardAsset, setStandardAsset] = useState( + null + ); + + // fetch unknown asset information + useEffect(() => { + const selectedAsset: IStandardAsset | null = + standardAssets.find((value) => value.id === id) || null; + + if (selectedAsset) { + setStandardAsset(selectedAsset); + + return; + } + + // if we don't have the asset, get the asset information + if (selectedNetwork) { + // dispatch( + // updateAssetInformationThunk({ + // ids: [assetId], + // network: selectedNetwork, + // }) + // ); + } + }, [standardAssets]); + + return { + standardAsset, + updating: updatingStandardAssets, + }; +} diff --git a/src/extension/pages/AccountPage/AccountPage.tsx b/src/extension/pages/AccountPage/AccountPage.tsx index e9203f03..065ebd1c 100644 --- a/src/extension/pages/AccountPage/AccountPage.tsx +++ b/src/extension/pages/AccountPage/AccountPage.tsx @@ -36,8 +36,9 @@ import { // components import ActivityTab from '@extension/components/ActivityTab'; -import AccountAssetsTab from '@extension/components/AccountAssetsTab'; +import AddAssetModal from '@extension/components/AddAssetModal'; import AccountNftsTab from '@extension/components/AccountNftsTab'; +import AssetsTab from '@extension/components/AssetsTab'; import CopyIconButton from '@extension/components/CopyIconButton'; import EmptyState from '@extension/components/EmptyState'; import IconButton from '@extension/components/IconButton'; @@ -355,7 +356,7 @@ const AccountPage: FC = () => { h="70dvh" sx={{ display: 'flex', flexDirection: 'column' }} > - + @@ -443,13 +444,15 @@ const AccountPage: FC = () => { return ( <> {account && ( - + <> + + )} { onOpen: onShareAddressModalOpen, } = useDisclosure(); // selectors - const fetchingAssets: boolean = useSelectFetchingAssets(); + const fetchingAssets: boolean = useSelectFetchingStandardAssets(); const explorer: IExplorer | null = useSelectPreferredBlockExplorer(); const selectedNetwork: INetwork | null = useSelectSelectedNetwork(); // hooks @@ -139,6 +139,7 @@ const AssetPage: FC = () => { onClose={onShareAddressModalClose} /> )} + { account.name || ellipseAddress(accountAddress, { end: 10, start: 10 }) } /> + (null); const [accountInformation, setAccountInformation] = useState(null); - const [asset, setAsset] = useState(null); - const [assetHolding, setAssetHolding] = useState(null); + const [asset, setAsset] = useState(null); + const [assetHolding, setAssetHolding] = + useState(null); const [standardUnitAmount, setStandardUnitAmount] = useState( new BigNumber('0') ); @@ -67,7 +65,7 @@ export default function useAssetPage({ }, [address, accounts]); // 1b. when we have the asset id and the assets, get the asset useEffect(() => { - let selectedAsset: IAsset | null; + let selectedAsset: IStandardAsset | null; if (assetId && assets.length > 0) { selectedAsset = assets.find((value) => value.id === assetId) || null; @@ -93,11 +91,11 @@ export default function useAssetPage({ }, [account, selectedNetwork]); // 3. when we have the account information, get the asset holding useEffect(() => { - let selectedAssetHolding: IAssetHolding | null; + let selectedAssetHolding: IStandardAssetHolding | null; if (accountInformation) { selectedAssetHolding = - accountInformation.assetHoldings.find( + accountInformation.standardAssetHoldings.find( (value) => value.id === assetId ) || null; diff --git a/src/extension/pages/TransactionPage/ApplicationTransactionContent.tsx b/src/extension/pages/TransactionPage/ApplicationTransactionContent.tsx index 3a969d95..25185ec1 100644 --- a/src/extension/pages/TransactionPage/ApplicationTransactionContent.tsx +++ b/src/extension/pages/TransactionPage/ApplicationTransactionContent.tsx @@ -59,7 +59,7 @@ const ApplicationTransactionContent: FC = ({ const { t } = useTranslation(); const { isOpen, onOpen, onClose } = useDisclosure(); // selectors - const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const explorer: IExplorer | null = useSelectPreferredBlockExplorer(); // hooks const defaultTextColor: string = useDefaultTextColor(); const primaryColorScheme: string = usePrimaryColorScheme(); @@ -72,11 +72,6 @@ const ApplicationTransactionContent: FC = ({ () => false ) ); - // misc - const explorer: IExplorer | null = - network.explorers.find((value) => value.id === preferredExplorer?.id) || - network.explorers[0] || - null; // get the preferred explorer, if it exists in the networks, otherwise get the default one // handlers const handleInnerTransactionsAccordionToggle = (accordionIndex: number) => (open: boolean) => { diff --git a/src/extension/pages/TransactionPage/AssetConfigTransactionContent.tsx b/src/extension/pages/TransactionPage/AssetConfigTransactionContent.tsx index bcaaab74..6f19bcf0 100644 --- a/src/extension/pages/TransactionPage/AssetConfigTransactionContent.tsx +++ b/src/extension/pages/TransactionPage/AssetConfigTransactionContent.tsx @@ -23,7 +23,7 @@ import PageItem, { ITEM_HEIGHT } from '@extension/components/PageItem'; import { DEFAULT_GAP } from '@extension/constants'; // hooks -import useAsset from '@extension/hooks/useAsset'; +import useStandardAssetById from '@extension/hooks/useStandardAssetById'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; import useSubTextColor from '@extension/hooks/useSubTextColor'; @@ -64,7 +64,7 @@ const AssetConfigTransactionContent: FC = ({ const accounts: IAccount[] = useSelectAccounts(); const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); // hooks - const { asset, updating } = useAsset(transaction.assetId); + const { standardAsset, updating } = useStandardAssetById(transaction.assetId); const defaultTextColor: string = useDefaultTextColor(); const primaryButtonTextColor: string = usePrimaryButtonTextColor(); const subTextColor: string = useSubTextColor(); @@ -376,7 +376,7 @@ const AssetConfigTransactionContent: FC = ({ w={3} /> } - unit={asset?.unitName || undefined} + unit={standardAsset?.unitName || undefined} /> @@ -397,7 +397,7 @@ const AssetConfigTransactionContent: FC = ({ {/*url*/} - {asset?.url && ( + {standardAsset?.url && ( ('labels.url')}> = ({ fontSize="sm" wordBreak="break-word" > - {asset.url} + {standardAsset.url} ('captions.openUrl')} - url={asset.url} + url={standardAsset.url} /> )} {/*unit name*/} - {asset?.unitName && ( + {standardAsset?.unitName && ( ('labels.unitName')}> - {asset.unitName} + {standardAsset.unitName} )} diff --git a/src/extension/pages/TransactionPage/AssetCreateTransactionContent.tsx b/src/extension/pages/TransactionPage/AssetCreateTransactionContent.tsx index 6a9240b0..dff39286 100644 --- a/src/extension/pages/TransactionPage/AssetCreateTransactionContent.tsx +++ b/src/extension/pages/TransactionPage/AssetCreateTransactionContent.tsx @@ -61,16 +61,11 @@ const AssetCreateTransactionContent: FC = ({ const { isOpen, onOpen, onClose } = useDisclosure(); // selectors const accounts: IAccount[] = useSelectAccounts(); - const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const explorer: IExplorer | null = useSelectPreferredBlockExplorer(); // hooks const defaultTextColor: string = useDefaultTextColor(); const primaryButtonTextColor: string = usePrimaryButtonTextColor(); const subTextColor: string = useSubTextColor(); - // misc - const explorer: IExplorer | null = - network.explorers.find((value) => value.id === preferredExplorer?.id) || - network.explorers[0] || - null; // get the preferred explorer, if it exists in the networks, otherwise get the default one // handlers const handleMoreInformationToggle = (value: boolean) => value ? onOpen() : onClose(); diff --git a/src/extension/pages/TransactionPage/AssetFreezeTransactionContent.tsx b/src/extension/pages/TransactionPage/AssetFreezeTransactionContent.tsx index 9754c04b..bfd39080 100644 --- a/src/extension/pages/TransactionPage/AssetFreezeTransactionContent.tsx +++ b/src/extension/pages/TransactionPage/AssetFreezeTransactionContent.tsx @@ -26,8 +26,8 @@ import { DEFAULT_GAP } from '@extension/constants'; import { TransactionTypeEnum } from '@extension/enums'; // hooks -import useAsset from '@extension/hooks/useAsset'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import useStandardAssetById from '@extension/hooks/useStandardAssetById'; import useSubTextColor from '@extension/hooks/useSubTextColor'; // selectors @@ -67,7 +67,7 @@ const AssetTransferTransactionContent: FC = ({ const accounts: IAccount[] = useSelectAccounts(); const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); // hooks - const { asset, updating } = useAsset(transaction.assetId); + const { standardAsset, updating } = useStandardAssetById(transaction.assetId); const defaultTextColor: string = useDefaultTextColor(); const subTextColor: string = useSubTextColor(); // misc @@ -79,7 +79,7 @@ const AssetTransferTransactionContent: FC = ({ const handleMoreInformationToggle = (value: boolean) => value ? onOpen() : onClose(); - if (!asset || updating) { + if (!standardAsset || updating) { return ; } @@ -95,13 +95,13 @@ const AssetTransferTransactionContent: FC = ({ ('labels.assetId')}> - {asset.id} + {standardAsset.id} ('captions.assetIdCopied')} size="sm" - value={asset.id} + value={standardAsset.id} /> {explorer && ( = ({ tooltipLabel={t('captions.openOn', { name: explorer.canonicalName, })} - url={`${explorer.baseUrl}${explorer.assetPath}/${asset.id}`} + url={`${explorer.baseUrl}${explorer.assetPath}/${standardAsset.id}`} /> )} diff --git a/src/extension/pages/TransactionPage/AssetTransferTransactionContent.tsx b/src/extension/pages/TransactionPage/AssetTransferTransactionContent.tsx index d7e78f6a..95f795d1 100644 --- a/src/extension/pages/TransactionPage/AssetTransferTransactionContent.tsx +++ b/src/extension/pages/TransactionPage/AssetTransferTransactionContent.tsx @@ -25,9 +25,9 @@ import PageItem, { ITEM_HEIGHT } from '@extension/components/PageItem'; import { DEFAULT_GAP } from '@extension/constants'; // hooks -import useAsset from '@extension/hooks/useAsset'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; +import useStandardAssetById from '@extension/hooks/useStandardAssetById'; import useSubTextColor from '@extension/hooks/useSubTextColor'; // selectors @@ -71,7 +71,7 @@ const AssetTransferTransactionContent: FC = ({ const accounts: IAccount[] = useSelectAccounts(); const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); // hooks - const { asset, updating } = useAsset(transaction.assetId); + const { standardAsset, updating } = useStandardAssetById(transaction.assetId); const defaultTextColor: string = useDefaultTextColor(); const primaryButtonTextColor: string = usePrimaryButtonTextColor(); const subTextColor: string = useSubTextColor(); @@ -87,7 +87,7 @@ const AssetTransferTransactionContent: FC = ({ const handleMoreInformationToggle = (value: boolean) => value ? onOpen() : onClose(); - if (!asset || updating) { + if (!standardAsset || updating) { return ; } @@ -110,13 +110,13 @@ const AssetTransferTransactionContent: FC = ({ : 'red.500' } atomicUnitAmount={amount} - decimals={asset.decimals} + decimals={standardAsset.decimals} displayUnit={true} displayUnitColor={subTextColor} fontSize="sm" icon={ = ({ ? '+' : '-' } - unit={asset.unitName || undefined} + unit={standardAsset.unitName || undefined} /> @@ -313,13 +313,13 @@ const AssetTransferTransactionContent: FC = ({ ('labels.assetId')}> - {asset.id} + {standardAsset.id} ('captions.assetIdCopied')} size="sm" - value={asset.id} + value={standardAsset.id} /> {explorer && ( = ({ tooltipLabel={t('captions.openOn', { name: explorer.canonicalName, })} - url={`${explorer.baseUrl}${explorer.assetPath}/${asset.id}`} + url={`${explorer.baseUrl}${explorer.assetPath}/${standardAsset.id}`} /> )} diff --git a/src/extension/selectors/index.ts b/src/extension/selectors/index.ts index 380e47e0..927ccf4a 100644 --- a/src/extension/selectors/index.ts +++ b/src/extension/selectors/index.ts @@ -3,16 +3,21 @@ export { default as useSelectAccountById } from './useSelectAccountById'; export { default as useSelectAccountInformationByAddress } from './useSelectAccountInformationByAddress'; export { default as useSelectAccounts } from './useSelectAccounts'; export { default as useSelectAccountTransactionsByAddress } from './useSelectAccountTransactionsByAddress'; -export { default as useSelectAssets } from './useSelectAssets'; -export { default as useSelectAssetsByGenesisHash } from './useSelectAssetsByGenesisHash'; +export { default as useSelectAddAssetArc200Assets } from './useSelectAddAssetArc200Assets'; +export { default as useSelectAddAssetAccount } from './useSelectAddAssetAccount'; +export { default as useSelectAddAssetError } from './useSelectAddAssetError'; +export { default as useSelectAddAssetFetching } from './useSelectAddAssetFetching'; +export { default as useSelectAddAssetSelectedArc200Asset } from './useSelectAddAssetSelectedArc200Asset'; +export { default as useSelectArc200AssetsBySelectedNetwork } from './useSelectArc200AssetsBySelectedNetwork'; export { default as useSelectColorMode } from './useSelectColorMode'; export { default as useSelectConfirm } from './useSelectConfirm'; export { default as useSelectEnableRequest } from './useSelectEnableRequest'; export { default as useSelectError } from './useSelectError'; export { default as useSelectFetchingAccounts } from './useSelectFetchingAccounts'; -export { default as useSelectFetchingAssets } from './useSelectFetchingAssets'; +export { default as useSelectFetchingArc200Assets } from './useSelectFetchingArc200Assets'; export { default as useSelectFetchingSessions } from './useSelectFetchingSessions'; export { default as useSelectFetchingSettings } from './useSelectFetchingSettings'; +export { default as useSelectFetchingStandardAssets } from './useSelectFetchingStandardAssets'; export { default as useSelectInitializingWalletConnect } from './useSelectInitializingWalletConnect'; export { default as useSelectIsOnline } from './useSelectIsOnline'; export { default as useSelectLogger } from './useSelectLogger'; @@ -38,6 +43,9 @@ export { default as useSelectSettings } from './useSelectSettings'; export { default as useSelectSideBar } from './useSelectSideBar'; export { default as useSelectSignBytesRequest } from './useSelectSignBytesRequest'; export { default as useSelectSignTxnsRequest } from './useSelectSignTxnsRequest'; -export { default as useSelectUpdatingAssets } from './useSelectUpdatingAssets'; +export { default as useSelectStandardAssetsByGenesisHash } from './useSelectStandardAssetsByGenesisHash'; +export { default as useSelectStandardAssetsBySelectedNetwork } from './useSelectStandardAssetsBySelectedNetwork'; +export { default as useSelectUpdatingArc200Assets } from './useSelectUpdatingArc200Assets'; +export { default as useSelectUpdatingStandardAssets } from './useSelectUpdatingStandardAssets'; export { default as useSelectWalletConnectModalOpen } from './useSelectWalletConnectModalOpen'; export { default as useSelectWeb3Wallet } from './useSelectWeb3Wallet'; diff --git a/src/extension/selectors/useSelectAddAssetAccount.ts b/src/extension/selectors/useSelectAddAssetAccount.ts new file mode 100644 index 00000000..9c3dfdba --- /dev/null +++ b/src/extension/selectors/useSelectAddAssetAccount.ts @@ -0,0 +1,20 @@ +import { useSelector } from 'react-redux'; + +// selectors +import useSelectAccounts from './useSelectAccounts'; + +// types +import { IAccount, IMainRootState } from '@extension/types'; + +export default function useSelectAddAssetAccount(): IAccount | null { + const accounts: IAccount[] = useSelectAccounts(); + const accountId: string | null = useSelector( + (state) => state.addAsset.accountId + ); + + if (!accountId) { + return null; + } + + return accounts.find((value) => value.id === accountId) || null; +} diff --git a/src/extension/selectors/useSelectAddAssetArc200Assets.ts b/src/extension/selectors/useSelectAddAssetArc200Assets.ts new file mode 100644 index 00000000..4c52fef2 --- /dev/null +++ b/src/extension/selectors/useSelectAddAssetArc200Assets.ts @@ -0,0 +1,10 @@ +import { useSelector } from 'react-redux'; + +// types +import { IArc200Asset, IMainRootState } from '@extension/types'; + +export default function useSelectAddAssetArc200Assets(): IArc200Asset[] { + return useSelector( + (state) => state.addAsset.arc200Assets.items + ); +} diff --git a/src/extension/selectors/useSelectAddAssetError.ts b/src/extension/selectors/useSelectAddAssetError.ts new file mode 100644 index 00000000..fd051a81 --- /dev/null +++ b/src/extension/selectors/useSelectAddAssetError.ts @@ -0,0 +1,13 @@ +import { useSelector } from 'react-redux'; + +// errors +import { BaseExtensionError } from '@extension/errors'; + +// types +import { IMainRootState } from '@extension/types'; + +export default function useSelectAddAssetError(): BaseExtensionError | null { + return useSelector( + (state) => state.addAsset.error + ); +} diff --git a/src/extension/selectors/useSelectAddAssetFetching.ts b/src/extension/selectors/useSelectAddAssetFetching.ts new file mode 100644 index 00000000..df63bb2d --- /dev/null +++ b/src/extension/selectors/useSelectAddAssetFetching.ts @@ -0,0 +1,10 @@ +import { useSelector } from 'react-redux'; + +// types +import { IMainRootState } from '@extension/types'; + +export default function useSelectAddAssetFetching(): boolean { + return useSelector( + (state) => state.addAsset.fetching + ); +} diff --git a/src/extension/selectors/useSelectAddAssetSelectedArc200Asset.ts b/src/extension/selectors/useSelectAddAssetSelectedArc200Asset.ts new file mode 100644 index 00000000..68631518 --- /dev/null +++ b/src/extension/selectors/useSelectAddAssetSelectedArc200Asset.ts @@ -0,0 +1,10 @@ +import { useSelector } from 'react-redux'; + +// types +import { IArc200Asset, IMainRootState } from '@extension/types'; + +export default function useSelectAddAssetSelectedArc200Asset(): IArc200Asset | null { + return useSelector( + (state) => state.addAsset.selectedArc200Asset + ); +} diff --git a/src/extension/selectors/useSelectArc200AssetsBySelectedNetwork.ts b/src/extension/selectors/useSelectArc200AssetsBySelectedNetwork.ts new file mode 100644 index 00000000..377633c5 --- /dev/null +++ b/src/extension/selectors/useSelectArc200AssetsBySelectedNetwork.ts @@ -0,0 +1,31 @@ +import { useSelector } from 'react-redux'; + +// types +import { IArc200Asset, IMainRootState, INetwork } from '@extension/types'; +import { + convertGenesisHashToHex, + selectNetworkFromSettings, +} from '@extension/utils'; + +/** + * Selects all the ARC200 assets for the selected network. + * @returns {IArc200Asset[]} all network ARC200 assets for the selected network. + */ +export default function useSelectArc200AssetsBySelectedNetwork(): IArc200Asset[] { + return useSelector((state) => { + const selectedNetwork: INetwork | null = selectNetworkFromSettings( + state.networks.items, + state.settings + ); + + if (!selectedNetwork) { + return []; + } + + return state.arc200Assets.items + ? state.arc200Assets.items[ + convertGenesisHashToHex(selectedNetwork.genesisHash).toUpperCase() + ] + : []; + }); +} diff --git a/src/extension/selectors/useSelectAssets.ts b/src/extension/selectors/useSelectAssets.ts deleted file mode 100644 index 9a934be7..00000000 --- a/src/extension/selectors/useSelectAssets.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useSelector } from 'react-redux'; - -// types -import { IAsset, IMainRootState } from '@extension/types'; - -/** - * Selects all the assets for each network or null. - * @returns {Record | null} all network assets, or null. - */ -export default function useSelectAssets(): Record | null { - return useSelector | null>( - (state) => state.assets.items - ); -} diff --git a/src/extension/selectors/useSelectAssetsByGenesisHash.ts b/src/extension/selectors/useSelectAssetsByGenesisHash.ts deleted file mode 100644 index dd43831a..00000000 --- a/src/extension/selectors/useSelectAssetsByGenesisHash.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useSelector } from 'react-redux'; - -// types -import { IAsset, IMainRootState } from '@extension/types'; - -// utils -import { convertGenesisHashToHex } from '@extension/utils'; - -/** - * Selects all the assets for a given genesis hash. - * @returns {IAsset[]} all network assets. - */ -export default function useSelectAssetsByGenesisHash( - genesisHash: string -): IAsset[] { - return useSelector((state) => { - if (!state.assets.items) { - return []; - } - - return state.assets.items[convertGenesisHashToHex(genesisHash)] || []; - }); -} diff --git a/src/extension/selectors/useSelectFetchingArc200Assets.ts b/src/extension/selectors/useSelectFetchingArc200Assets.ts new file mode 100644 index 00000000..7bb51e73 --- /dev/null +++ b/src/extension/selectors/useSelectFetchingArc200Assets.ts @@ -0,0 +1,9 @@ +import { useSelector } from 'react-redux'; + +// types +import { IMainRootState } from '@extension/types'; +export default function useSelectFetchingArc200Assets(): boolean { + return useSelector( + (state) => state.arc200Assets.fetching + ); +} diff --git a/src/extension/selectors/useSelectFetchingAssets.ts b/src/extension/selectors/useSelectFetchingAssets.ts deleted file mode 100644 index 445750e0..00000000 --- a/src/extension/selectors/useSelectFetchingAssets.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useSelector } from 'react-redux'; - -// types -import { IMainRootState } from '@extension/types'; -export default function useSelectFetchingAssets(): boolean { - return useSelector((state) => state.assets.fetching); -} diff --git a/src/extension/selectors/useSelectFetchingStandardAssets.ts b/src/extension/selectors/useSelectFetchingStandardAssets.ts new file mode 100644 index 00000000..db0ea1d3 --- /dev/null +++ b/src/extension/selectors/useSelectFetchingStandardAssets.ts @@ -0,0 +1,9 @@ +import { useSelector } from 'react-redux'; + +// types +import { IMainRootState } from '@extension/types'; +export default function useSelectFetchingStandardAssets(): boolean { + return useSelector( + (state) => state.standardAssets.fetching + ); +} diff --git a/src/extension/selectors/useSelectPreferredBlockExplorer.ts b/src/extension/selectors/useSelectPreferredBlockExplorer.ts index 9e5d55a3..71e4c44f 100644 --- a/src/extension/selectors/useSelectPreferredBlockExplorer.ts +++ b/src/extension/selectors/useSelectPreferredBlockExplorer.ts @@ -5,8 +5,9 @@ import { IExplorer, IMainRootState, INetwork } from '@extension/types'; import { convertGenesisHashToHex } from '@extension/utils'; /** - * Gets the currently preferred block explorer from the settings. - * @returns {IExplorer | null} the current preferred block explorer from settings. + * Gets the currently preferred block explorer from the settings. If the block explorer cannot be found, the default + * explorer is used (first index), or null. + * @returns {IExplorer | null} the current preferred block explorer from settings, or the default explorer, or null. */ export default function useSelectPreferredBlockExplorer(): IExplorer | null { return useSelector((state) => { diff --git a/src/extension/selectors/useSelectSendingAssetSelectedAsset.ts b/src/extension/selectors/useSelectSendingAssetSelectedAsset.ts index 05b6f56a..9c2836b4 100644 --- a/src/extension/selectors/useSelectSendingAssetSelectedAsset.ts +++ b/src/extension/selectors/useSelectSendingAssetSelectedAsset.ts @@ -1,10 +1,10 @@ import { useSelector } from 'react-redux'; // types -import { IAsset, IMainRootState } from '@extension/types'; +import { IStandardAsset, IMainRootState } from '@extension/types'; -export default function useSelectSendingAssetSelectedAsset(): IAsset | null { - return useSelector( +export default function useSelectSendingAssetSelectedAsset(): IStandardAsset | null { + return useSelector( (state) => state.sendAssets.selectedAsset ); } diff --git a/src/extension/selectors/useSelectStandardAssetsByGenesisHash.ts b/src/extension/selectors/useSelectStandardAssetsByGenesisHash.ts new file mode 100644 index 00000000..e66e0b49 --- /dev/null +++ b/src/extension/selectors/useSelectStandardAssetsByGenesisHash.ts @@ -0,0 +1,27 @@ +import { useSelector } from 'react-redux'; + +// types +import { IStandardAsset, IMainRootState } from '@extension/types'; + +// utils +import { convertGenesisHashToHex } from '@extension/utils'; + +/** + * Selects all the standard assets for a given genesis hash. + * @returns {IStandardAsset[]} all network standard assets. + */ +export default function useSelectStandardAssetsByGenesisHash( + genesisHash: string +): IStandardAsset[] { + return useSelector((state) => { + if (!state.standardAssets.items) { + return []; + } + + return ( + state.standardAssets.items[ + convertGenesisHashToHex(genesisHash).toUpperCase() + ] || [] + ); + }); +} diff --git a/src/extension/selectors/useSelectStandardAssetsBySelectedNetwork.ts b/src/extension/selectors/useSelectStandardAssetsBySelectedNetwork.ts new file mode 100644 index 00000000..f10aabd9 --- /dev/null +++ b/src/extension/selectors/useSelectStandardAssetsBySelectedNetwork.ts @@ -0,0 +1,31 @@ +import { useSelector } from 'react-redux'; + +// types +import { IStandardAsset, IMainRootState, INetwork } from '@extension/types'; +import { + convertGenesisHashToHex, + selectNetworkFromSettings, +} from '@extension/utils'; + +/** + * Selects all the standard assets for the selected network. + * @returns {IStandardAsset[]} all standard assets for the selected network. + */ +export default function useSelectStandardAssetsBySelectedNetwork(): IStandardAsset[] { + return useSelector((state) => { + const selectedNetwork: INetwork | null = selectNetworkFromSettings( + state.networks.items, + state.settings + ); + + if (!selectedNetwork) { + return []; + } + + return state.standardAssets.items + ? state.standardAssets.items[ + convertGenesisHashToHex(selectedNetwork.genesisHash).toUpperCase() + ] + : []; + }); +} diff --git a/src/extension/selectors/useSelectUpdatingArc200Assets.ts b/src/extension/selectors/useSelectUpdatingArc200Assets.ts new file mode 100644 index 00000000..4a6d8f7a --- /dev/null +++ b/src/extension/selectors/useSelectUpdatingArc200Assets.ts @@ -0,0 +1,10 @@ +import { useSelector } from 'react-redux'; + +// types +import { IMainRootState } from '@extension/types'; + +export default function useSelectUpdatingArc200Assets(): boolean { + return useSelector( + (state) => state.arc200Assets.updating + ); +} diff --git a/src/extension/selectors/useSelectUpdatingAssets.ts b/src/extension/selectors/useSelectUpdatingAssets.ts deleted file mode 100644 index 8ed5009c..00000000 --- a/src/extension/selectors/useSelectUpdatingAssets.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useSelector } from 'react-redux'; - -// types -import { IMainRootState } from '@extension/types'; -export default function useSelectUpdatingAssets(): boolean { - return useSelector((state) => state.assets.updating); -} diff --git a/src/extension/selectors/useSelectUpdatingStandardAssets.ts b/src/extension/selectors/useSelectUpdatingStandardAssets.ts new file mode 100644 index 00000000..4933616a --- /dev/null +++ b/src/extension/selectors/useSelectUpdatingStandardAssets.ts @@ -0,0 +1,10 @@ +import { useSelector } from 'react-redux'; + +// types +import { IMainRootState } from '@extension/types'; + +export default function useSelectUpdatingStandardAssets(): boolean { + return useSelector( + (state) => state.standardAssets.updating + ); +} diff --git a/src/extension/services/AccountService.ts b/src/extension/services/AccountService.ts index 5dc2bcd7..d0dd644f 100644 --- a/src/extension/services/AccountService.ts +++ b/src/extension/services/AccountService.ts @@ -136,10 +136,11 @@ export default class AccountService { public static initializeDefaultAccountInformation(): IAccountInformation { return { - assetHoldings: [], + arc200AssetHoldings: [], atomicBalance: '0', authAddress: null, minAtomicBalance: '0', + standardAssetHoldings: [], updatedAt: null, }; } @@ -218,14 +219,10 @@ export default class AccountService { return { ...acc, - ...(accountInformation - ? { - [encodedGenesisHash]: accountInformation, - } - : { - [encodedGenesisHash]: - AccountService.initializeDefaultAccountInformation(), - }), + [encodedGenesisHash]: { + ...AccountService.initializeDefaultAccountInformation(), + ...accountInformation, + }, }; }, {} @@ -240,14 +237,10 @@ export default class AccountService { return { ...acc, - ...(accountTransactions - ? { - [encodedGenesisHash]: accountTransactions, - } - : { - [encodedGenesisHash]: - AccountService.initializeDefaultAccountTransactions(), - }), + [encodedGenesisHash]: { + ...AccountService.initializeDefaultAccountTransactions(), + ...accountTransactions, + }, }; }, {}), })); diff --git a/src/extension/services/Arc200AssetService.ts b/src/extension/services/Arc200AssetService.ts new file mode 100644 index 00000000..6994dc77 --- /dev/null +++ b/src/extension/services/Arc200AssetService.ts @@ -0,0 +1,78 @@ +// constants +import { ARC200_ASSETS_KEY_PREFIX } from '@extension/constants'; + +// services +import StorageManager from './StorageManager'; + +// types +import { IBaseOptions, ILogger } from '@common/types'; +import { IArc200Asset, IStandardAsset } from '@extension/types'; + +// utils +import { convertGenesisHashToHex } from '@extension/utils'; + +export default class Arc200AssetService { + // private variables + private readonly logger: ILogger | null; + private readonly storageManager: StorageManager; + + constructor({ logger }: IBaseOptions) { + this.logger = logger || null; + this.storageManager = new StorageManager(); + } + + /** + * private functions + */ + + /** + * Convenience function that simply creates an item key by hex encoding the genesis hash. + * @param {string} genesisHash - the genesis hash to use to index. + * @returns {string} the arc200 asset item key. + */ + private createItemKey(genesisHash: string): string { + return `${ARC200_ASSETS_KEY_PREFIX}${convertGenesisHashToHex( + genesisHash + ).toUpperCase()}`; + } + + /** + * public functions + */ + + /** + * Gets the ARC200 assets for a given genesis hash. + * @param {string} genesisHash - genesis hash for a network. + * @returns {Promise} the list of standard assets. + */ + public async getByGenesisHash(genesisHash: string): Promise { + return ( + (await this.storageManager.getItem(this.createItemKey(genesisHash))) || [] + ); + } + + /** + * Removes all the ARC200 assets by the network's genesis hash. + * @param {string} genesisHash - genesis hash for a network. + */ + public async removeByGenesisHash(genesisHash: string): Promise { + await this.storageManager.remove(this.createItemKey(genesisHash)); + } + + /** + * Saves ARC200 assets to storage by the network's genesis hash. + * @param {string} genesisHash - genesis hash for a network. + * @param {IArc200Asset[]} assets - the ARC200 assets to save to storage. + * @returns {IArc200Asset[]} the saved ARC200 assets. + */ + public async saveByGenesisHash( + genesisHash: string, + assets: IArc200Asset[] + ): Promise { + await this.storageManager.setItems({ + [this.createItemKey(genesisHash)]: assets, + }); + + return assets; + } +} diff --git a/src/extension/services/StandardAssetService.ts b/src/extension/services/StandardAssetService.ts new file mode 100644 index 00000000..d109fa03 --- /dev/null +++ b/src/extension/services/StandardAssetService.ts @@ -0,0 +1,80 @@ +// constants +import { STANDARD_ASSETS_KEY_PREFIX } from '@extension/constants'; + +// services +import StorageManager from './StorageManager'; + +// types +import { IBaseOptions, ILogger } from '@common/types'; +import { IStandardAsset } from '@extension/types'; + +// utils +import { convertGenesisHashToHex } from '@extension/utils'; + +export default class StandardAssetService { + // private variables + private readonly logger: ILogger | null; + private readonly storageManager: StorageManager; + + constructor({ logger }: IBaseOptions) { + this.logger = logger || null; + this.storageManager = new StorageManager(); + } + + /** + * private functions + */ + + /** + * Convenience function that simply creates an item key by hex encoding the genesis hash. + * @param {string} genesisHash - the genesis hash to use to index. + * @returns {string} the asset item key. + */ + private createItemKey(genesisHash: string): string { + return `${STANDARD_ASSETS_KEY_PREFIX}${convertGenesisHashToHex( + genesisHash + ).toUpperCase()}`; + } + + /** + * public functions + */ + + /** + * Gets the standard assets for a given genesis hash. + * @param {string} genesisHash - genesis hash for a network. + * @returns {Promise} the list of standard assets. + */ + public async getByGenesisHash( + genesisHash: string + ): Promise { + return ( + (await this.storageManager.getItem(this.createItemKey(genesisHash))) || [] + ); + } + + /** + * Removes all the standard assets by the network's genesis hash. + * @param {string} genesisHash - genesis hash for a network. + */ + public async removeByGenesisHash(genesisHash: string): Promise { + await this.storageManager.remove(this.createItemKey(genesisHash)); + } + + /** + * Saves standard assets to storage by the network's genesis hash. + * @param {string} genesisHash - genesis hash for a network. + * @param {IStandardAsset[]} assets - the standard assets to save to storage. + * @returns {IStandardAsset[]} the saved standard assets. + */ + public async saveByGenesisHash( + genesisHash: string, + assets: IStandardAsset[] + ): Promise { + await this.storageManager.setItems({ + [this.createItemKey(genesisHash)]: assets, + }); + + return assets; + } +} diff --git a/src/extension/services/index.ts b/src/extension/services/index.ts index e3d7b37c..dc103059 100644 --- a/src/extension/services/index.ts +++ b/src/extension/services/index.ts @@ -1,5 +1,6 @@ export { default as AccountService } from './AccountService'; export { default as AppWindowManagerService } from './AppWindowManagerService'; +export { default as Arc200AssetService } from './Arc200AssetService'; export { default as BackgroundEventListener } from './BackgroundEventListener'; export { default as BackgroundMessageHandler } from './BackgroundMessageHandler'; export { default as ColorModeManager } from './ColorModeManager'; @@ -7,4 +8,5 @@ export { default as EventQueueService } from './EventQueueService'; export { default as ExternalMessageBroker } from './ExternalMessageBroker'; export { default as PrivateKeyService } from './PrivateKeyService'; export { default as SessionService } from './SessionService'; +export { default as StandardAssetService } from './StandardAssetService'; export { default as StorageManager } from './StorageManager'; diff --git a/src/extension/translations/en.ts b/src/extension/translations/en.ts index 05acdfa4..25efa52b 100644 --- a/src/extension/translations/en.ts +++ b/src/extension/translations/en.ts @@ -32,6 +32,8 @@ const translation: IResourceLanguage = { sign: 'Sign', }, captions: { + addAsset: + 'Enter an asset ID, name, symbol or application ID (for ARC-200).', addressCopied: 'Address copied!', addressDoesNotMatch: 'This address does not match the signer', allowBetaNet: 'Let BetaNet networks appear in the networks list.', @@ -52,6 +54,7 @@ const translation: IResourceLanguage = { [`appOnComplete_${TransactionTypeEnum.ApplicationOptIn}`]: `This transaction will opt the sender's account into the application by allocating some local state.`, [`appOnComplete_${TransactionTypeEnum.ApplicationUpdate}`]: 'This transaction will update the application, replacing the approval and clear programs. The application ID will not be changed.', + arc200ApplicationIdCopied: 'ARC200 application ID copied!', assetIdCopied: 'Asset ID copied!', audienceDoesNotMatch: 'The intended recipient of this token, does not match the host', @@ -104,8 +107,7 @@ const translation: IResourceLanguage = { 'We are almost done. Before we safely secure your new account on this device, we just need you to confirm you have copied your seed phrase.', noAccountsFound: 'You can create a new account or import an existing account.', - noAssetsFound: - 'You have not opt-ed into any assets. Try adding an asset now.', + noAssetsFound: 'You have not added any assets. Try adding one now.', noSessionsFound: 'Enabled dApps will appear here.', offline: 'It looks like you are offline, some features may not work', openOn: 'Open on {{name}}', @@ -136,6 +138,7 @@ const translation: IResourceLanguage = { transactionIdCopied: 'Transaction ID copied!', transactionSendSuccessful: 'Transaction "{{transactionId}}" was successfully sent.', + updatingAssetInformation: 'Updating asset information', }, errors: { descriptions: { @@ -159,6 +162,8 @@ const translation: IResourceLanguage = { }, }, headings: { + addAsset: 'Add Asset', + addedAsset: 'Added Asset {{symbol}}!', allowMainNetConfirm: 'Allow MainNet Networks', authentication: 'Authentication', beta: 'Beta', diff --git a/src/extension/types/IAccountInformation.ts b/src/extension/types/IAccountInformation.ts index db3a7e63..92df4a80 100644 --- a/src/extension/types/IAccountInformation.ts +++ b/src/extension/types/IAccountInformation.ts @@ -1,19 +1,22 @@ // types -import IAssetHolding from './IAssetHolding'; +import IArc200AssetHolding from './IArc200AssetHolding'; +import IStandardAssetHolding from './IStandardAssetHolding'; /** - * @property {IAssetHolding} assetHoldings - the assets this account holds. + * @property {IArc200AssetHolding} arc200AssetHoldings - the arc200 assets this account holds. * @property {string} atomicBalance - the atomic balance of this account as a string. * @property {string | null} authAddress - the address that this account has been rekeyed with. * @property {string} minAtomicBalance - the minimum balance for this account. + * @property {IStandardAssetHolding} standardAssetHoldings - the standard assets this account holds. * @property {number | null} updatedAt - a timestamp (in milliseconds) for when this account information was last * updated. */ interface IAccountInformation { - assetHoldings: IAssetHolding[]; + arc200AssetHoldings: IArc200AssetHolding[]; atomicBalance: string; authAddress: string | null; minAtomicBalance: string; + standardAssetHoldings: IStandardAssetHolding[]; updatedAt: number | null; } diff --git a/src/extension/types/IAlgorandApplication.ts b/src/extension/types/IAlgorandApplication.ts new file mode 100644 index 00000000..6fa60552 --- /dev/null +++ b/src/extension/types/IAlgorandApplication.ts @@ -0,0 +1,12 @@ +// types +import IAlgorandApplicationParams from './IAlgorandApplicationParams'; + +interface IAlgorandApplication { + ['created-at-round']?: bigint; + deleted?: boolean; + ['destroyed-at-round']: bigint; + id: bigint; + params: IAlgorandApplicationParams; +} + +export default IAlgorandApplication; diff --git a/src/extension/types/IAlgorandApplicationParams.ts b/src/extension/types/IAlgorandApplicationParams.ts new file mode 100644 index 00000000..8e563ba2 --- /dev/null +++ b/src/extension/types/IAlgorandApplicationParams.ts @@ -0,0 +1,15 @@ +// types +import IAlgorandStateSchema from './IAlgorandStateSchema'; +import IAlgorandTealKeyValue from './IAlgorandTealKeyValue'; + +interface IAlgorandApplicationParams { + ['approval-program']: string; + ['clear-state-program']: string; + creator?: string; + ['extra-program-pages']?: bigint; + ['global-state']?: IAlgorandTealKeyValue[]; + ['global-state-schema']?: IAlgorandStateSchema; + ['local-state-schema']?: IAlgorandStateSchema; +} + +export default IAlgorandApplicationParams; diff --git a/src/extension/types/IAlgorandSearchApplicationsResult.ts b/src/extension/types/IAlgorandSearchApplicationsResult.ts new file mode 100644 index 00000000..899bfd9f --- /dev/null +++ b/src/extension/types/IAlgorandSearchApplicationsResult.ts @@ -0,0 +1,10 @@ +// types +import IAlgorandApplication from './IAlgorandApplication'; + +interface IAlgorandSearchApplicationsResult { + applications: IAlgorandApplication[]; + ['current-round']: bigint; + ['next-token']?: string; +} + +export default IAlgorandSearchApplicationsResult; diff --git a/src/extension/types/IAppThunkDispatchReturn.ts b/src/extension/types/IAppThunkDispatchReturn.ts new file mode 100644 index 00000000..3254c70c --- /dev/null +++ b/src/extension/types/IAppThunkDispatchReturn.ts @@ -0,0 +1,17 @@ +// types +import { + AsyncThunkFulfilledActionCreator, + AsyncThunkRejectedActionCreator, +} from '@reduxjs/toolkit/dist/createAsyncThunk'; + +type IAppThunkDispatchReturn = Promise< + | ReturnType> + | ReturnType> +> & { + abort: (reason?: string) => void; + requestId: string; + arg: string; + unwrap: () => Promise; +}; + +export default IAppThunkDispatchReturn; diff --git a/src/extension/types/IArc200Asset.ts b/src/extension/types/IArc200Asset.ts new file mode 100644 index 00000000..b6f92385 --- /dev/null +++ b/src/extension/types/IArc200Asset.ts @@ -0,0 +1,20 @@ +/** + * @property {number} decimals - the number of digits to use after the decimal point when displaying this ARC-200 asset. + * @property {string} id - the app ID of the ARC-200 asset. + * @property {string | null} iconUrl - the URL of the asset icon. + * @property {string} name - the utf-8 name of the ARC-200 asset. + * @property {string} symbol - the utf-8 symbol of the ARC-200 asset. + * @property {string} totalSupply - the total supply of this ARC-200 asset. + * @property {boolean} verified - whether this ARC-200 asset is verified. + */ +interface IArc200Asset { + decimals: number; + iconUrl: string | null; + id: string; + name: string; + symbol: string; + totalSupply: string; + verified: boolean; +} + +export default IArc200Asset; diff --git a/src/extension/types/IArc200AssetHolding.ts b/src/extension/types/IArc200AssetHolding.ts new file mode 100644 index 00000000..30c5143e --- /dev/null +++ b/src/extension/types/IArc200AssetHolding.ts @@ -0,0 +1,10 @@ +/** + * @property {string} amount - the amount of the arc200 asset. + * @property {string} id - the arc200 app ID. + */ +interface IArc200AssetHolding { + amount: string; + id: string; +} + +export default IArc200AssetHolding; diff --git a/src/extension/types/IArc200AssetInformation.ts b/src/extension/types/IArc200AssetInformation.ts new file mode 100644 index 00000000..61268500 --- /dev/null +++ b/src/extension/types/IArc200AssetInformation.ts @@ -0,0 +1,8 @@ +interface IArc200AssetInformation { + name: string; + symbol: string; + totalSupply: bigint; + decimals: bigint; +} + +export default IArc200AssetInformation; diff --git a/src/extension/types/IAssetHolding.ts b/src/extension/types/IAssetHolding.ts deleted file mode 100644 index ae2b965c..00000000 --- a/src/extension/types/IAssetHolding.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @property {string} amount - the amount of the asset. - * @property {string} id - the asset ID. - * @property {boolean} isFrozen - whether this asset is frozen. - */ -interface IAssetHolding { - amount: string; - id: string; - isFrozen: boolean; -} - -export default IAssetHolding; diff --git a/src/extension/types/IBackgroundRootState.ts b/src/extension/types/IBackgroundRootState.ts index d8152ffa..2e228de0 100644 --- a/src/extension/types/IBackgroundRootState.ts +++ b/src/extension/types/IBackgroundRootState.ts @@ -1,21 +1,23 @@ // features import { IAccountsState } from '@extension/features/accounts'; -import { IAssetsState } from '@extension/features/assets'; +import { IArc200AssetsState } from '@extension/features/arc200-assets'; import { IEventsState } from '@extension/features/events'; import { INetworksState } from '@extension/features/networks'; import { ISessionsState } from '@extension/features/sessions'; import { ISettingsState } from '@extension/features/settings'; +import { IStandardAssetsState } from '@extension/features/standard-assets'; // types import IBaseRootState from './IBaseRootState'; interface IBackgroundRootState extends IBaseRootState { accounts: IAccountsState; - assets: IAssetsState; + arc200Assets: IArc200AssetsState; events: IEventsState; networks: INetworksState; sessions: ISessionsState; settings: ISettingsState; + standardAssets: IStandardAssetsState; } export default IBackgroundRootState; diff --git a/src/extension/types/IBaseAsyncThunkConfig.ts b/src/extension/types/IBaseAsyncThunkConfig.ts new file mode 100644 index 00000000..c3ce7dc3 --- /dev/null +++ b/src/extension/types/IBaseAsyncThunkConfig.ts @@ -0,0 +1,8 @@ +// types +import IMainRootState from './IMainRootState'; + +interface IBaseAsyncThunkConfig { + state: IMainRootState; +} + +export default IBaseAsyncThunkConfig; diff --git a/src/extension/types/IMainRootState.ts b/src/extension/types/IMainRootState.ts index e584f6f9..dcb243ce 100644 --- a/src/extension/types/IMainRootState.ts +++ b/src/extension/types/IMainRootState.ts @@ -1,25 +1,29 @@ // features import { IAccountsState } from '@extension/features/accounts'; -import { IAssetsState } from '@extension/features/assets'; +import { IAddAssetState } from '@extension/features/add-asset'; +import { IArc200AssetsState } from '@extension/features/arc200-assets'; import { IEventsState } from '@extension/features/events'; import { INetworksState } from '@extension/features/networks'; import { INotificationsState } from '@extension/features/notifications'; import { ISendAssetsState } from '@extension/features/send-assets'; import { ISessionsState } from '@extension/features/sessions'; import { ISettingsState } from '@extension/features/settings'; +import { IStandardAssetsState } from '@extension/features/standard-assets'; // types import IBaseRootState from './IBaseRootState'; interface IMainRootState extends IBaseRootState { accounts: IAccountsState; - assets: IAssetsState; + addAsset: IAddAssetState; + arc200Assets: IArc200AssetsState; events: IEventsState; networks: INetworksState; notifications: INotificationsState; sendAssets: ISendAssetsState; sessions: ISessionsState; settings: ISettingsState; + standardAssets: IStandardAssetsState; } export default IMainRootState; diff --git a/src/extension/types/IAsset.ts b/src/extension/types/IStandardAsset.ts similarity index 97% rename from src/extension/types/IAsset.ts rename to src/extension/types/IStandardAsset.ts index 95335d0f..9d7707a0 100644 --- a/src/extension/types/IAsset.ts +++ b/src/extension/types/IStandardAsset.ts @@ -22,7 +22,7 @@ * retrieved. * @property {boolean} verified - whether this asset is verified according to vestige.fi */ -interface IAsset { +interface IStandardAsset { clawbackAddress: string | null; creator: string; decimals: number; @@ -44,4 +44,4 @@ interface IAsset { verified: boolean; } -export default IAsset; +export default IStandardAsset; diff --git a/src/extension/types/IStandardAssetHolding.ts b/src/extension/types/IStandardAssetHolding.ts new file mode 100644 index 00000000..71804c65 --- /dev/null +++ b/src/extension/types/IStandardAssetHolding.ts @@ -0,0 +1,12 @@ +/** + * @property {string} amount - the amount of the standard asset. + * @property {string} id - the standard asset ID. + * @property {boolean} isFrozen - whether this standard asset is frozen. + */ +interface IStandardAssetHolding { + amount: string; + id: string; + isFrozen: boolean; +} + +export default IStandardAssetHolding; diff --git a/src/extension/types/IStorageItemTypes.ts b/src/extension/types/IStorageItemTypes.ts index 54ed16ee..4f7c8a53 100644 --- a/src/extension/types/IStorageItemTypes.ts +++ b/src/extension/types/IStorageItemTypes.ts @@ -3,12 +3,13 @@ import IAccount from './IAccount'; import IAdvancedSettings from './IAdvancedSettings'; import IAppearanceSettings from './IAppearanceSettings'; import IAppWindow from './IAppWindow'; -import IAsset from './IAsset'; +import IArc200Asset from './IArc200Asset'; import IGeneralSettings from './IGeneralSettings'; import IEvent from './IEvent'; import IPasswordTag from './IPasswordTag'; import IPrivateKey from './IPrivateKey'; import ISession from './ISession'; +import IStandardAsset from './IStandardAsset'; import ITransactionParams from './ITransactionParams'; type IStorageItemTypes = @@ -16,12 +17,13 @@ type IStorageItemTypes = | IAdvancedSettings | IAppearanceSettings | IAppWindow - | IAsset[] + | IArc200Asset[] | IGeneralSettings | IEvent[] | IPasswordTag | IPrivateKey | ISession + | IStandardAsset[] | ITransactionParams; export default IStorageItemTypes; diff --git a/src/extension/types/index.ts b/src/extension/types/index.ts index eeedd220..33be8207 100644 --- a/src/extension/types/index.ts +++ b/src/extension/types/index.ts @@ -6,6 +6,8 @@ export type { default as IAddAccountCompleteResult } from './IAddAccountComplete export type { default as IAdvancedSettings } from './IAdvancedSettings'; export type { default as IAlgorandAccountInformation } from './IAlgorandAccountInformation'; export type { default as IAlgorandAccountTransaction } from './IAlgorandAccountTransaction'; +export type { default as IAlgorandApplication } from './IAlgorandApplication'; +export type { default as IAlgorandApplicationParams } from './IAlgorandApplicationParams'; export type { default as IAlgorandApplicationTransaction } from './IAlgorandApplicationTransaction'; export type { default as IAlgorandAsset } from './IAlgorandAsset'; export type { default as IAlgorandAssetHolding } from './IAlgorandAssetHolding'; @@ -16,6 +18,7 @@ export type { default as IAlgorandAssetTransferTransaction } from './IAlgorandAs export type { default as IAlgorandKeyRegistrationTransaction } from './IAlgorandKeyRegistrationTransaction'; export type { default as IAlgorandPaymentTransaction } from './IAlgorandPaymentTransaction'; export type { default as IAlgorandPendingTransactionResponse } from './IAlgorandPendingTransactionResponse'; +export type { default as IAlgorandSearchApplicationsResult } from './IAlgorandSearchApplicationsResult'; export type { default as IAlgorandStateSchema } from './IAlgorandStateSchema'; export type { default as IAlgorandTealKeyValue } from './IAlgorandTealKeyValue'; export type { default as IAlgorandTealValue } from './IAlgorandTealValue'; @@ -24,20 +27,23 @@ export type { default as IAlgorandTransactionParams } from './IAlgorandTransacti export type { default as IAppearanceSettings } from './IAppearanceSettings'; export type { default as IAppProps } from './IAppProps'; export type { default as IAppThunkDispatch } from './IAppThunkDispatch'; +export type { default as IAppThunkDispatchReturn } from './IAppThunkDispatchReturn'; export type { default as IApplicationTransaction } from './IApplicationTransaction'; export type { default as IApplicationTransactionTypes } from './IApplicationTransactionTypes'; export type { default as IAppWindow } from './IAppWindow'; -export type { default as IAsset } from './IAsset'; +export type { default as IArc200Asset } from './IArc200Asset'; +export type { default as IArc200AssetHolding } from './IArc200AssetHolding'; +export type { default as IArc200AssetInformation } from './IArc200AssetInformation'; export type { default as IAssetConfigTransaction } from './IAssetConfigTransaction'; export type { default as IAssetCreateTransaction } from './IAssetCreateTransaction'; export type { default as IAssetDestroyTransaction } from './IAssetDestroyTransaction'; export type { default as IAssetFreezeTransaction } from './IAssetFreezeTransaction'; -export type { default as IAssetHolding } from './IAssetHolding'; export type { default as IAssetTransferTransaction } from './IAssetTransferTransaction'; export type { default as IAssetUnfreezeTransaction } from './IAssetUnfreezeTransaction'; export type { default as IBackgroundRootState } from './IBackgroundRootState'; export type { default as IBaseActionMeta } from './IBaseActionMeta'; export type { default as IBaseAssetFreezeTransaction } from './IBaseAssetFreezeTransaction'; +export type { default as IBaseAsyncThunkConfig } from './IBaseAsyncThunkConfig'; export type { default as IBaseRequest } from './IBaseRequest'; export type { default as IBaseRootState } from './IBaseRootState'; export type { default as IBaseTransaction } from './IBaseTransaction'; @@ -73,6 +79,8 @@ export type { default as ISignBytesEventPayload } from './ISignBytesEventPayload export type { default as ISignBytesRequest } from './ISignBytesRequest'; export type { default as ISignTxnsEventPayload } from './ISignTxnsEventPayload'; export type { default as ISignTxnsRequest } from './ISignTxnsRequest'; +export type { default as IStandardAsset } from './IStandardAsset'; +export type { default as IStandardAssetHolding } from './IStandardAssetHolding'; export type { default as IStorageItemTypes } from './IStorageItemTypes'; export type { default as ITinyManAssetResponse } from './ITinyManAssetResponse'; export type { default as ITransactionParams } from './ITransactionParams'; diff --git a/src/extension/utils/calculateMaxTransactionAmount.ts b/src/extension/utils/calculateMaxTransactionAmount.ts index 34b64b9e..b92cfeaa 100644 --- a/src/extension/utils/calculateMaxTransactionAmount.ts +++ b/src/extension/utils/calculateMaxTransactionAmount.ts @@ -4,7 +4,7 @@ import BigNumber from 'bignumber.js'; import { IAccount, IAccountInformation, - IAssetHolding, + IStandardAssetHolding, INetworkWithTransactionParams, } from '@extension/types'; @@ -36,7 +36,7 @@ export default function calculateMaxTransactionAmount({ convertGenesisHashToHex(network.genesisHash).toUpperCase() ] || null; let amount: BigNumber; - let assetHolding: IAssetHolding | null; + let assetHolding: IStandardAssetHolding | null; let balance: BigNumber; let minBalance: BigNumber; let minFee: BigNumber; @@ -48,8 +48,9 @@ export default function calculateMaxTransactionAmount({ // if the asset id is not 0 it is an asa, use the balance of the asset if (assetId != '0') { assetHolding = - accountInformation.assetHoldings.find((value) => value.id === assetId) || - null; + accountInformation.standardAssetHoldings.find( + (value) => value.id === assetId + ) || null; return new BigNumber(assetHolding?.amount || 0); } diff --git a/src/extension/utils/createNativeCurrencyAsset.ts b/src/extension/utils/createNativeCurrencyAsset.ts index 33256cd0..e2f5d02a 100644 --- a/src/extension/utils/createNativeCurrencyAsset.ts +++ b/src/extension/utils/createNativeCurrencyAsset.ts @@ -1,12 +1,14 @@ -import { IAsset, INetwork } from '@extension/types'; +import { IStandardAsset, INetwork } from '@extension/types'; /** * Creates a "dummy" asset for the native currency of the supplied network. The native "dummy" assets will always have * an ID of '0' as all other assets will have an ID > 0. * @param {INetwork} network - the network to create the native "dummy" asset from. - * @returns {IAsset} a "dummy" asset with an ID of '0'. + * @returns {IStandardAsset} a "dummy" asset with an ID of '0'. */ -export default function createNativeCurrencyAsset(network: INetwork): IAsset { +export default function createNativeCurrencyAsset( + network: INetwork +): IStandardAsset { return { clawbackAddress: null, creator: network.feeSunkAddress, // null address diff --git a/src/extension/utils/fetchArc200AssetInformationWithDelay.ts b/src/extension/utils/fetchArc200AssetInformationWithDelay.ts new file mode 100644 index 00000000..375e79aa --- /dev/null +++ b/src/extension/utils/fetchArc200AssetInformationWithDelay.ts @@ -0,0 +1,49 @@ +import { Algodv2, Indexer } from 'algosdk'; +import Arc200Contract from 'arc200js'; + +// types +import { IArc200AssetInformation } from '@extension/types'; + +interface IOptions { + algodClient: Algodv2; + delay: number; + id: string; + indexerClient: Indexer; +} + +/** + * Fetches ARC200 asset information from the node with a delay. + * @param {IOptions} options - options needed to send the request. + * @returns {IArc200AssetInformation | null} ARC200 asset information from the node. + */ +export default async function fetchArc200AssetInformationWithDelay({ + algodClient, + delay, + id, + indexerClient, +}: IOptions): Promise { + return new Promise((resolve, reject) => + setTimeout(async () => { + const contract: Arc200Contract = new Arc200Contract( + parseInt(id), + algodClient, + indexerClient + ); + let result: { returnValue: IArc200AssetInformation; success: boolean }; + + try { + result = await contract.getMetadata(); + + if (!result.success) { + resolve(null); + + return; + } + + resolve(result.returnValue); + } catch (error) { + reject(error); + } + }, delay) + ); +} diff --git a/src/extension/utils/index.ts b/src/extension/utils/index.ts index 5c8f2dee..68f5eb67 100644 --- a/src/extension/utils/index.ts +++ b/src/extension/utils/index.ts @@ -7,6 +7,7 @@ export { default as createNativeCurrencyAsset } from './createNativeCurrencyAsse export { default as decodeJwt } from './decodeJwt'; export { default as decodeURLSearchParam } from './decodeURLSearchParam'; export { default as ellipseAddress } from './ellipseAddress'; +export { default as fetchArc200AssetInformationWithDelay } from './fetchArc200AssetInformationWithDelay'; export { default as fetchAssetList } from './fetchAssetList'; export { default as fetchAssetVerification } from './fetchAssetVerification'; export { default as fetchWithDelay } from './fetchWithDelay'; @@ -17,11 +18,12 @@ export { default as isAccountKnown } from './isAccountKnown'; export { default as isHexString } from './isHexString'; export { default as isMnemonicValid } from './isMnemonicValid'; export { default as makeStore } from './makeStore'; -export { default as mapAssetFromAlgorandAsset } from './mapAssetFromAlgorandAsset'; +export { default as mapArc200AssetFromArc200AssetInformation } from './mapArc200AssetFromArc200AssetInformation'; export { default as mapAlgorandAccountInformationToAccount } from './mapAlgorandAccountInformationToAccount'; export { default as mapAlgorandTransactionToTransaction } from './mapAlgorandTransactionToTransaction'; export { default as mapSessionFromEnableRequest } from './mapSessionFromEnableRequest'; export { default as mapSessionFromWalletConnectSession } from './mapSessionFromWalletConnectSession'; +export { default as mapStandardAssetFromAlgorandAsset } from './mapStandardAssetFromAlgorandAsset'; export { default as parseTransactionType } from './parseTransactionType'; export { default as selectDefaultNetwork } from './selectDefaultNetwork'; export { default as selectNetworkFromSettings } from './selectNetworkFromSettings'; diff --git a/src/extension/utils/mapAlgorandAccountInformationToAccount.ts b/src/extension/utils/mapAlgorandAccountInformationToAccount.ts index b76e879b..1945b812 100644 --- a/src/extension/utils/mapAlgorandAccountInformationToAccount.ts +++ b/src/extension/utils/mapAlgorandAccountInformationToAccount.ts @@ -16,15 +16,15 @@ export default function mapAlgorandAccountInformationToAccountInformation( atomicBalance: new BigNumber( String(algorandAccountInformation.amount as bigint) ).toString(), - assetHoldings: algorandAccountInformation.assets.map((value) => ({ - amount: new BigNumber(String(value.amount as bigint)).toString(), - id: new BigNumber(String(value['asset-id'] as bigint)).toString(), - isFrozen: value['is-frozen'], - })), authAddress: algorandAccountInformation['auth-addr'] || null, minAtomicBalance: new BigNumber( String(algorandAccountInformation['min-balance'] as bigint) ).toString(), + standardAssetHoldings: algorandAccountInformation.assets.map((value) => ({ + amount: new BigNumber(String(value.amount as bigint)).toString(), + id: new BigNumber(String(value['asset-id'] as bigint)).toString(), + isFrozen: value['is-frozen'], + })), updatedAt: updatedAt || new Date().getTime(), }; } diff --git a/src/extension/utils/mapArc200AssetFromArc200AssetInformation.ts b/src/extension/utils/mapArc200AssetFromArc200AssetInformation.ts new file mode 100644 index 00000000..00eee2ac --- /dev/null +++ b/src/extension/utils/mapArc200AssetFromArc200AssetInformation.ts @@ -0,0 +1,25 @@ +import { BigNumber } from 'bignumber.js'; + +// types +import { IArc200Asset, IArc200AssetInformation } from '@extension/types'; + +export default function mapArc200AssetFromArc200AssetInformation( + appId: string, + assetInformation: IArc200AssetInformation, + iconUrl: string | null, + verified: boolean +): IArc200Asset { + return { + decimals: new BigNumber( + String(assetInformation.decimals as bigint) + ).toNumber(), + iconUrl, + id: appId, + name: assetInformation.name, + symbol: assetInformation.symbol, + totalSupply: new BigNumber( + String(assetInformation.totalSupply as bigint) + ).toString(), + verified, + }; +} diff --git a/src/extension/utils/mapAssetFromAlgorandAsset.ts b/src/extension/utils/mapStandardAssetFromAlgorandAsset.ts similarity index 89% rename from src/extension/utils/mapAssetFromAlgorandAsset.ts rename to src/extension/utils/mapStandardAssetFromAlgorandAsset.ts index 3a3cb4df..250f1529 100644 --- a/src/extension/utils/mapAssetFromAlgorandAsset.ts +++ b/src/extension/utils/mapStandardAssetFromAlgorandAsset.ts @@ -1,13 +1,13 @@ import { BigNumber } from 'bignumber.js'; // types -import { IAlgorandAsset, IAsset } from '@extension/types'; +import { IAlgorandAsset, IStandardAsset } from '@extension/types'; -export default function mapAssetFromAlgorandAsset( +export default function mapStandardAssetFromAlgorandAsset( algorandAsset: IAlgorandAsset, iconUrl: string | null, verified: boolean -): IAsset { +): IStandardAsset { return { clawbackAddress: algorandAsset.params.clawback || null, creator: algorandAsset.params.creator, diff --git a/src/extension/utils/parseTransactionType.ts b/src/extension/utils/parseTransactionType.ts index c8343e9f..12a816b2 100644 --- a/src/extension/utils/parseTransactionType.ts +++ b/src/extension/utils/parseTransactionType.ts @@ -109,7 +109,7 @@ export default function parseTransactionType( // to test if this an opt-in: if ( // if the sender does not hold the asset, and - !accountInformation?.assetHoldings.find( + !accountInformation?.standardAssetHoldings.find( (value) => value.id === String(encodedTransaction.xaid) ) && // if there is no amount, or the amount is zero (any amount will be a transfer, albeit a failed transfer), and diff --git a/tsconfig.json b/tsconfig.json index 1cb60f69..34681f4c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,7 +29,10 @@ "@extension/enums": ["src/extension/enums"], "@extension/errors": ["src/extension/errors"], "@extension/features/accounts": ["src/extension/features/accounts"], - "@extension/features/assets": ["src/extension/features/assets"], + "@extension/features/add-asset": ["src/extension/features/add-asset"], + "@extension/features/arc200-assets": [ + "src/extension/features/arc200-assets" + ], "@extension/features/events": ["src/extension/features/events"], "@extension/features/messages": ["src/extension/features/messages"], "@extension/features/networks": ["src/extension/features/networks"], @@ -42,6 +45,9 @@ "@extension/features/send-assets": ["src/extension/features/send-assets"], "@extension/features/sessions": ["src/extension/features/sessions"], "@extension/features/settings": ["src/extension/features/settings"], + "@extension/features/standard-assets": [ + "src/extension/features/standard-assets" + ], "@extension/features/system": ["src/extension/features/system"], "@extension/fonts/*": ["src/extension/fonts/*"], "@extension/hooks/*": ["src/extension/hooks/*"], diff --git a/webpack/utils/createCommonConfig.ts b/webpack/utils/createCommonConfig.ts index 569fdde8..06f39ea7 100644 --- a/webpack/utils/createCommonConfig.ts +++ b/webpack/utils/createCommonConfig.ts @@ -33,10 +33,15 @@ export default function createCommonConfig(): Configuration { 'features', 'accounts' ), - ['@extension/features/assets']: resolve( + ['@extension/features/add-asset']: resolve( extensionPath, 'features', - 'assets' + 'add-asset' + ), + ['@extension/features/arc200-assets']: resolve( + extensionPath, + 'features', + 'arc200-assets' ), ['@extension/features/events']: resolve( extensionPath, @@ -78,6 +83,11 @@ export default function createCommonConfig(): Configuration { 'features', 'settings' ), + ['@extension/features/standard-assets']: resolve( + extensionPath, + 'features', + 'standard-assets' + ), ['@extension/features/system']: resolve( extensionPath, 'features', diff --git a/yarn.lock b/yarn.lock index 2035218a..ae9f3d1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4187,6 +4187,21 @@ algosdk@^2.1.0: tweetnacl "^1.0.3" vlq "^2.0.4" +algosdk@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/algosdk/-/algosdk-2.7.0.tgz#6ebab130d25fc3cb4c74dce4d8753c7e86db1404" + integrity sha512-sBE9lpV7bup3rZ+q2j3JQaFAE9JwZvjWKX00vPlG8e9txctXbgLL56jZhSWZndqhDI9oI+0P4NldkuQIWdrUyg== + dependencies: + algo-msgpack-with-bigint "^2.1.1" + buffer "^6.0.3" + hi-base32 "^0.5.1" + js-sha256 "^0.9.0" + js-sha3 "^0.8.0" + js-sha512 "^0.8.0" + json-bigint "^1.0.0" + tweetnacl "^1.0.3" + vlq "^2.0.4" + ansi-align@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" @@ -4270,6 +4285,22 @@ anymatch@^3.0.3, anymatch@~3.1.2: resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== +arc200js@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/arc200js/-/arc200js-2.3.1.tgz#51a537ff0841a0a209b013cc53a435699a018be0" + integrity sha512-rBhoVVcHi2rYUTEteTvayGpI596N1uCPbQsQ9zNLmyJPkd2o5uTYyu4wBC87rWSGdDJejljylaC9NPalcei1cg== + dependencies: + arccjs "^2.3.1" + +arccjs@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/arccjs/-/arccjs-2.3.2.tgz#4177c332b70453fed040b8694241d0ec31abb676" + integrity sha512-RHdzIWU0UfYlnQP5f+RfVkPddsCyP9s0zwjOebIhdM1qnN21D7pAMBadg6JxDcawOVg9AktzQY+bghASb1uEJQ== + dependencies: + algosdk "^2.7.0" + buffer "^6.0.3" + sha512 "^0.0.1" + archy@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" @@ -11994,6 +12025,11 @@ sha.js@2.4.11: inherits "^2.0.1" safe-buffer "^5.0.1" +sha512@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/sha512/-/sha512-0.0.1.tgz#26e59022ef6abc75c7344f0a5ef3936caf2542d5" + integrity sha512-2t5rZu72ebMg2/OK44S/48UKxgBl7Kc3w/I0DkIzNld8NzDUxFBL2pOYAWEVknnuOqO62a3W+3x/xD7LrosGBg== + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"