From 525727c840eea34ee6698e38a0d13f787d53f4dc Mon Sep 17 00:00:00 2001 From: Kieran O'Neill Date: Tue, 5 Dec 2023 15:32:35 +0200 Subject: [PATCH] feat: send native currency and asas to another account (#37) * feat: add button to open send asset modal * feat: add network transaction params to storage * feat: add send input to calculate maximum transaction aount * feat: add asset select * feat: add share address modal when clicking the receive button * feat: add to address and note fields * feat: add summary page * feat: add thunk to send transaction * feat: add toast when error or completion of trnasaction submission * feat: update account information when confirmation of asset transafer * fix: account tab selection persists through account selection * fix: sidebar account address aligns left * feat: use toast from hook and handle errors in send asset modal * refactor: account information and account transaction thunk use lists of account ids * feat: add transaction to send asset toast messages * feat: shrink inputs to medium size * build: update manifest version script to omit pre-release suffixes --- bin/update_manifest_version.sh | 9 +- package.json | 1 + src/common/utils/formatCurrencyUnit.test.ts | 27 +- src/common/utils/formatCurrencyUnit.ts | 11 +- src/common/utils/getAlgodClient.ts | 6 +- src/common/utils/getIndexerClient.ts | 6 +- src/common/utils/getRandomNode.ts | 11 + src/common/utils/index.ts | 1 + src/extension/apps/background/App.tsx | 2 + src/extension/apps/main/App.tsx | 2 + src/extension/apps/main/Root.tsx | 60 +-- src/extension/apps/registration/Root.tsx | 23 +- .../AccountAssetsTab/AccountAssetsTab.tsx | 2 +- .../AccountSelect/AccountSelect.tsx | 10 +- .../components/AddressInput/AddressInput.tsx | 61 +++ .../components/AddressInput/hooks/index.ts | 1 + .../AddressInput/hooks/useAddressInput.ts | 43 ++ .../components/AddressInput/index.ts | 4 + .../types/IUseAddressInputState.ts | 13 + .../components/AddressInput/types/index.ts | 1 + .../components/AddressInput/utils/index.ts | 1 + .../components/AddressInput/utils/validate.ts | 22 + .../components/AssetDisplay/AssetDisplay.tsx | 3 +- .../components/AssetSelect/AssetSelect.tsx | 168 ++++++ .../AssetSelect/AssetSelectOption.tsx | 181 +++++++ .../AssetSelect/AssetSelectSingleValue.tsx | 75 +++ .../AssetSelect/constants/Dimensions.ts | 1 + .../components/AssetSelect/constants/index.ts | 1 + src/extension/components/AssetSelect/index.ts | 2 + .../components/AssetSelect/types/IOption.ts | 8 + .../components/AssetSelect/types/index.ts | 1 + .../CreatePasswordInput.tsx | 7 +- .../components/EnableModal/EnableModal.tsx | 2 + .../NativeBalance/NativeBalance.tsx | 10 +- .../components/PageHeader/PageHeader.tsx | 5 +- .../components/PasswordInput/hooks/index.ts | 1 + .../PasswordInput/hooks/usePassword.ts | 43 ++ .../components/PasswordInput/index.ts | 3 + .../PasswordInput/types/IUsePasswordState.ts | 13 + .../components/PasswordInput/types/index.ts | 1 + .../components/PasswordInput/utils/index.ts | 1 + .../PasswordInput/utils/validate.ts | 17 + .../SendAssetModal/SendAmountInput.tsx | 240 +++++++++ .../SendAssetModal/SendAssetModal.tsx | 489 ++++++++++++++++++ .../SendAssetModalContentSkeleton.tsx | 14 + .../SendAssetModalSummaryContent.tsx | 168 ++++++ .../SendAssetModal/SendAssetSummaryItem.tsx | 62 +++ .../components/SendAssetModal/index.ts | 1 + src/extension/components/SideBar/SideBar.tsx | 52 +- .../components/SideBar/SideBarAccountItem.tsx | 18 +- .../SignBytesModal/SignBytesModal.tsx | 2 + .../AssetTransferTransactionContent.tsx | 4 +- .../PaymentTransactionContent.tsx | 7 +- src/extension/constants/Durations.ts | 2 + src/extension/constants/Keys.ts | 2 + src/extension/enums/AccountsThunkEnum.ts | 2 +- src/extension/enums/ErrorCodeEnum.ts | 7 + src/extension/enums/NetworksThunkEnum.ts | 8 + src/extension/enums/SendAssetsThunkEnum.ts | 5 + src/extension/enums/StoreNameEnum.ts | 1 + src/extension/enums/index.ts | 2 + .../errors/FailedToSendTransactionError.ts | 11 + .../errors/NetworkNotSelectedError.ts | 10 + src/extension/errors/OfflineError.ts | 10 + src/extension/errors/index.ts | 3 + src/extension/features/accounts/slice.ts | 27 +- .../thunks/fetchAccountsFromStorageThunk.ts | 14 +- .../features/accounts/thunks/index.ts | 2 +- .../accounts/thunks/removeAccountByIdThunk.ts | 2 +- .../startPollingForAccountInformationThunk.ts | 2 +- .../stopPollingForAccountInformationThunk.ts | 2 +- .../thunks/updateAccountInformationThunk.ts | 42 +- ...pdateAccountTransactionsForAccountThunk.ts | 77 --- .../thunks/updateAccountTransactionsThunk.ts | 84 +++ .../types/IUpdateAccountInformationPayload.ts | 11 + .../IUpdateAccountTransactionsPayload.ts | 11 + .../features/accounts/types/index.ts | 2 + ...tchAlgorandAccountTransactionsWithDelay.ts | 49 ++ .../features/accounts/utils/index.ts | 1 + .../utils/updateAccountInformation.ts | 6 +- .../utils/updateAccountTransactions.ts | 55 +- .../assets/thunks/fetchAssetsThunk.ts | 4 +- .../thunks/updateAssetInformationThunk.ts | 6 +- src/extension/features/networks/index.ts | 1 + src/extension/features/networks/slice.ts | 84 ++- .../fetchTransactionParamsFromStorageThunk.ts | 98 ++++ .../features/networks/thunks/index.ts | 4 + .../startPollingForTransactionsParamsThunk.ts | 36 ++ .../stopPollingForTransactionsParamsThunk.ts | 30 ++ ...ransactionParamsForSelectedNetworkThunk.ts | 84 +++ .../features/networks/types/INetworksState.ts | 14 +- .../networks/utils/getInitialState.ts | 12 +- .../features/networks/utils/index.ts | 1 + .../networks/utils/updateTransactionParams.ts | 98 ++++ src/extension/features/send-assets/index.ts | 4 + src/extension/features/send-assets/slice.ts | 126 +++++ .../features/send-assets/thunks/index.ts | 1 + .../thunks/submitTransactionThunk.ts | 204 ++++++++ .../types/IInitializeSendAssetPayload.ts | 8 + .../send-assets/types/ISendAssetsState.ts | 28 + .../features/send-assets/types/index.ts | 2 + .../utils/createSendAssetTransaction.ts | 64 +++ .../send-assets/utils/getInitialState.ts | 15 + .../features/send-assets/utils/index.ts | 2 + .../features/settings/thunks/fetchSettings.ts | 4 +- .../features/settings/thunks/setSettings.ts | 20 +- src/extension/features/system/slice.ts | 17 +- .../features/system/types/ISystemState.ts | 2 - .../features/system/utils/getInitialState.ts | 1 - .../hooks/useToastWithDefaultOptions/index.ts | 1 + .../useToastWithDefaultOptions.ts | 15 + .../pages/AccountPage/AccountPage.tsx | 69 +-- src/extension/pages/AssetPage/AssetPage.tsx | 49 +- .../ChangePasswordPage/ChangePasswordPage.tsx | 8 +- src/extension/selectors/index.ts | 13 +- src/extension/selectors/useSelectAccount.ts | 33 -- .../selectors/useSelectAccountByAddress.ts | 33 ++ .../useSelectAccountInformationByAddress.ts | 32 ++ .../useSelectAccountTransactionsByAddress.ts | 32 ++ .../useSelectNetworkByGenesisHash.ts | 11 +- src/extension/selectors/useSelectNetworks.ts | 34 +- .../selectors/useSelectSelectedNetwork.ts | 29 +- .../selectors/useSelectSendingAssetAmount.ts | 10 + .../useSelectSendingAssetConfirming.ts | 10 + .../selectors/useSelectSendingAssetError.ts | 13 + .../useSelectSendingAssetFromAccount.ts | 29 ++ .../selectors/useSelectSendingAssetNote.ts | 10 + .../useSelectSendingAssetSelectedAsset.ts | 10 + .../useSelectSendingAssetToAddress.ts | 10 + .../useSelectSendingAssetTransactionId.ts | 10 + src/extension/selectors/useSelectToast.ts | 11 - src/extension/services/PrivateKeyService.ts | 5 +- src/extension/translations/en.ts | 16 + .../IAlgorandPendingTransactionResponse.ts | 17 + .../types/IAlgorandTransactionParams.ts | 10 + src/extension/types/IBaseActionMeta.ts | 9 + src/extension/types/IFulfilledActionMeta.ts | 6 + src/extension/types/IMainRootState.ts | 2 + .../types/INetworkWithTransactionParams.ts | 7 + src/extension/types/IPendingActionMeta.ts | 6 + src/extension/types/IRejectedActionMeta.ts | 10 + src/extension/types/IStorageItemTypes.ts | 4 +- src/extension/types/ITransactionParams.ts | 13 + src/extension/types/index.ts | 8 + .../utils/calculateMaxTransactionAmount.ts | 64 +++ .../utils/createNativeCurrencyAsset.ts | 31 ++ src/extension/utils/index.ts | 2 + src/extension/utils/makeStore.ts | 13 +- src/extension/utils/selectDefaultNetwork.ts | 10 +- .../utils/selectNetworkFromSettings.ts | 17 +- src/manifest.common.json | 10 +- tsconfig.json | 4 +- webpack/utils/createCommonConfig.ts | 10 +- yarn.lock | 177 ++++++- 154 files changed, 3833 insertions(+), 427 deletions(-) create mode 100644 src/common/utils/getRandomNode.ts create mode 100644 src/extension/components/AddressInput/AddressInput.tsx create mode 100644 src/extension/components/AddressInput/hooks/index.ts create mode 100644 src/extension/components/AddressInput/hooks/useAddressInput.ts create mode 100644 src/extension/components/AddressInput/index.ts create mode 100644 src/extension/components/AddressInput/types/IUseAddressInputState.ts create mode 100644 src/extension/components/AddressInput/types/index.ts create mode 100644 src/extension/components/AddressInput/utils/index.ts create mode 100644 src/extension/components/AddressInput/utils/validate.ts create mode 100644 src/extension/components/AssetSelect/AssetSelect.tsx create mode 100644 src/extension/components/AssetSelect/AssetSelectOption.tsx create mode 100644 src/extension/components/AssetSelect/AssetSelectSingleValue.tsx create mode 100644 src/extension/components/AssetSelect/constants/Dimensions.ts create mode 100644 src/extension/components/AssetSelect/constants/index.ts create mode 100644 src/extension/components/AssetSelect/index.ts create mode 100644 src/extension/components/AssetSelect/types/IOption.ts create mode 100644 src/extension/components/AssetSelect/types/index.ts create mode 100644 src/extension/components/PasswordInput/hooks/index.ts create mode 100644 src/extension/components/PasswordInput/hooks/usePassword.ts create mode 100644 src/extension/components/PasswordInput/types/IUsePasswordState.ts create mode 100644 src/extension/components/PasswordInput/types/index.ts create mode 100644 src/extension/components/PasswordInput/utils/index.ts create mode 100644 src/extension/components/PasswordInput/utils/validate.ts create mode 100644 src/extension/components/SendAssetModal/SendAmountInput.tsx create mode 100644 src/extension/components/SendAssetModal/SendAssetModal.tsx create mode 100644 src/extension/components/SendAssetModal/SendAssetModalContentSkeleton.tsx create mode 100644 src/extension/components/SendAssetModal/SendAssetModalSummaryContent.tsx create mode 100644 src/extension/components/SendAssetModal/SendAssetSummaryItem.tsx create mode 100644 src/extension/components/SendAssetModal/index.ts create mode 100644 src/extension/enums/NetworksThunkEnum.ts create mode 100644 src/extension/enums/SendAssetsThunkEnum.ts create mode 100644 src/extension/errors/FailedToSendTransactionError.ts create mode 100644 src/extension/errors/NetworkNotSelectedError.ts create mode 100644 src/extension/errors/OfflineError.ts delete mode 100644 src/extension/features/accounts/thunks/updateAccountTransactionsForAccountThunk.ts create mode 100644 src/extension/features/accounts/thunks/updateAccountTransactionsThunk.ts create mode 100644 src/extension/features/accounts/types/IUpdateAccountInformationPayload.ts create mode 100644 src/extension/features/accounts/types/IUpdateAccountTransactionsPayload.ts create mode 100644 src/extension/features/accounts/utils/fetchAlgorandAccountTransactionsWithDelay.ts create mode 100644 src/extension/features/networks/thunks/fetchTransactionParamsFromStorageThunk.ts create mode 100644 src/extension/features/networks/thunks/index.ts create mode 100644 src/extension/features/networks/thunks/startPollingForTransactionsParamsThunk.ts create mode 100644 src/extension/features/networks/thunks/stopPollingForTransactionsParamsThunk.ts create mode 100644 src/extension/features/networks/thunks/updateTransactionParamsForSelectedNetworkThunk.ts create mode 100644 src/extension/features/networks/utils/updateTransactionParams.ts create mode 100644 src/extension/features/send-assets/index.ts create mode 100644 src/extension/features/send-assets/slice.ts create mode 100644 src/extension/features/send-assets/thunks/index.ts create mode 100644 src/extension/features/send-assets/thunks/submitTransactionThunk.ts create mode 100644 src/extension/features/send-assets/types/IInitializeSendAssetPayload.ts create mode 100644 src/extension/features/send-assets/types/ISendAssetsState.ts create mode 100644 src/extension/features/send-assets/types/index.ts create mode 100644 src/extension/features/send-assets/utils/createSendAssetTransaction.ts create mode 100644 src/extension/features/send-assets/utils/getInitialState.ts create mode 100644 src/extension/features/send-assets/utils/index.ts create mode 100644 src/extension/hooks/useToastWithDefaultOptions/index.ts create mode 100644 src/extension/hooks/useToastWithDefaultOptions/useToastWithDefaultOptions.ts delete mode 100644 src/extension/selectors/useSelectAccount.ts create mode 100644 src/extension/selectors/useSelectAccountByAddress.ts create mode 100644 src/extension/selectors/useSelectAccountInformationByAddress.ts create mode 100644 src/extension/selectors/useSelectAccountTransactionsByAddress.ts create mode 100644 src/extension/selectors/useSelectSendingAssetAmount.ts create mode 100644 src/extension/selectors/useSelectSendingAssetConfirming.ts create mode 100644 src/extension/selectors/useSelectSendingAssetError.ts create mode 100644 src/extension/selectors/useSelectSendingAssetFromAccount.ts create mode 100644 src/extension/selectors/useSelectSendingAssetNote.ts create mode 100644 src/extension/selectors/useSelectSendingAssetSelectedAsset.ts create mode 100644 src/extension/selectors/useSelectSendingAssetToAddress.ts create mode 100644 src/extension/selectors/useSelectSendingAssetTransactionId.ts delete mode 100644 src/extension/selectors/useSelectToast.ts create mode 100644 src/extension/types/IAlgorandPendingTransactionResponse.ts create mode 100644 src/extension/types/IAlgorandTransactionParams.ts create mode 100644 src/extension/types/IBaseActionMeta.ts create mode 100644 src/extension/types/IFulfilledActionMeta.ts create mode 100644 src/extension/types/INetworkWithTransactionParams.ts create mode 100644 src/extension/types/IPendingActionMeta.ts create mode 100644 src/extension/types/IRejectedActionMeta.ts create mode 100644 src/extension/types/ITransactionParams.ts create mode 100644 src/extension/utils/calculateMaxTransactionAmount.ts create mode 100644 src/extension/utils/createNativeCurrencyAsset.ts diff --git a/bin/update_manifest_version.sh b/bin/update_manifest_version.sh index 8a8a0de5..9161764f 100755 --- a/bin/update_manifest_version.sh +++ b/bin/update_manifest_version.sh @@ -14,6 +14,8 @@ source "${SCRIPT_DIR}/set_vars.sh" # # Returns exit code 0 if successful, or 1 if the semantic version is incorrectly formatted. function main { + local version + set_vars if [ -z "${1}" ]; then @@ -27,8 +29,11 @@ function main { exit 1 fi - printf "%b updating manifest.common.json#version to version '%s' \n" "${INFO_PREFIX}" "${1}" - cat <<< $(jq --arg version "${1}" '.version = $version' "${PWD}/src/manifest.common.json") > "${PWD}/src/manifest.common.json" + # for pre release versions, be sure to remove the *-beta.xx that is suffixed to the end of the semantic version + version=${1%-*} + + printf "%b updating manifest.common.json#version to version '%s' \n" "${INFO_PREFIX}" "${version}" + cat <<< $(jq --arg version "${version}" '.version = $version' "${PWD}/src/manifest.common.json") > "${PWD}/src/manifest.common.json" exit 0 } diff --git a/package.json b/package.json index f38aace0..51fb5b98 100644 --- a/package.json +++ b/package.json @@ -136,6 +136,7 @@ "react-markdown": "^8.0.7", "react-redux": "^8.0.5", "react-router-dom": "^6.8.2", + "react-select": "^5.8.0", "scrypt-async": "^2.0.1", "tweetnacl": "^1.0.3", "uuid": "^9.0.0", diff --git a/src/common/utils/formatCurrencyUnit.test.ts b/src/common/utils/formatCurrencyUnit.test.ts index 34d1e859..3eda8a84 100644 --- a/src/common/utils/formatCurrencyUnit.test.ts +++ b/src/common/utils/formatCurrencyUnit.test.ts @@ -5,6 +5,7 @@ import formatCurrencyUnit from './formatCurrencyUnit'; interface ITestParams { input: BigNumber; + decimals?: number; expected: string; } @@ -42,6 +43,16 @@ describe('formatCurrencyUnit()', () => { input: new BigNumber('612290.716271'), expected: '612,290.72', }, + { + input: new BigNumber('612290.716271'), + decimals: 5, + expected: '612,290.71627', + }, + { + input: new BigNumber('612290.716271'), + decimals: 4, + expected: '612,290.7163', + }, { input: new BigNumber('12290.716271'), expected: '12,290.72', @@ -72,16 +83,28 @@ describe('formatCurrencyUnit()', () => { }, { input: new BigNumber('0.16500'), + decimals: 3, expected: '0.165', }, + { + input: new BigNumber('0.23100'), + decimals: 2, + expected: '0.23', + }, + { + input: new BigNumber('0.23500'), + decimals: 2, + expected: '0.24', + }, { input: new BigNumber('0.716271'), + decimals: 6, expected: '0.716271', }, ])( `should format the unit of $input to $expected`, - ({ input, expected }: ITestParams) => { - expect(formatCurrencyUnit(input)).toBe(expected); + ({ input, decimals, expected }: ITestParams) => { + expect(formatCurrencyUnit(input, decimals)).toBe(expected); } ); }); diff --git a/src/common/utils/formatCurrencyUnit.ts b/src/common/utils/formatCurrencyUnit.ts index ab8600ec..0438b5c7 100644 --- a/src/common/utils/formatCurrencyUnit.ts +++ b/src/common/utils/formatCurrencyUnit.ts @@ -6,10 +6,13 @@ import numbro from 'numbro'; * @param {BigNumber} input - the unit as a BigNumber. * @returns {string} the formatted unit. */ -export default function formatCurrencyUnit(input: BigNumber): string { +export default function formatCurrencyUnit( + input: BigNumber, + decimals: number = 2 +): string { if (input.gte(1)) { // numbers >= 1m+ - if (input.decimalPlaces(2).gte(new BigNumber(1000000))) { + if (input.decimalPlaces(decimals).gte(new BigNumber(1000000))) { return numbro(input.toString()).format({ average: true, totalLength: 6, @@ -19,7 +22,7 @@ export default function formatCurrencyUnit(input: BigNumber): string { // numbers <= 999,999.99 return numbro(input.toString()).format({ - mantissa: 2, + mantissa: decimals, thousandSeparated: true, trimMantissa: true, }); @@ -27,7 +30,7 @@ export default function formatCurrencyUnit(input: BigNumber): string { // numbers < 1 return numbro(input.toString()).format({ - mantissa: 6, + mantissa: decimals, trimMantissa: true, }); } diff --git a/src/common/utils/getAlgodClient.ts b/src/common/utils/getAlgodClient.ts index 30c08911..7aa0210a 100644 --- a/src/common/utils/getAlgodClient.ts +++ b/src/common/utils/getAlgodClient.ts @@ -4,6 +4,9 @@ import { Algodv2 } from 'algosdk'; import { IBaseOptions } from '@common/types'; import { INetwork, INode } from '@extension/types'; +// utils +import getRandomNode from './getRandomNode'; + /** * Gets a random algod node from the given network. * @param {INetwork} network - the network to choose the node from. @@ -14,8 +17,7 @@ export default function getAlgodClient( network: INetwork, { logger }: IBaseOptions = { logger: undefined } ): Algodv2 { - const algod: INode = - network.algods[Math.floor(Math.random() * network.algods.length)]; + const algod: INode = getRandomNode(network.algods); logger && logger.debug( diff --git a/src/common/utils/getIndexerClient.ts b/src/common/utils/getIndexerClient.ts index f196dfcd..2c3b63b0 100644 --- a/src/common/utils/getIndexerClient.ts +++ b/src/common/utils/getIndexerClient.ts @@ -4,6 +4,9 @@ import { Indexer } from 'algosdk'; import { IBaseOptions } from '@common/types'; import { INetwork, INode } from '@extension/types'; +// utils +import getRandomNode from './getRandomNode'; + /** * Gets a random indexer node from the given network. * @param {INetwork} network - the network to choose the node from. @@ -14,8 +17,7 @@ export default function getIndexerClient( network: INetwork, { logger }: IBaseOptions = { logger: undefined } ): Indexer { - const indexer: INode = - network.indexers[Math.floor(Math.random() * network.indexers.length)]; + const indexer: INode = getRandomNode(network.indexers); logger && logger.debug( diff --git a/src/common/utils/getRandomNode.ts b/src/common/utils/getRandomNode.ts new file mode 100644 index 00000000..7b05ff96 --- /dev/null +++ b/src/common/utils/getRandomNode.ts @@ -0,0 +1,11 @@ +// types +import { INode } from '@extension/types'; + +/** + * Convenience function that randomly picks a node from a list. + * @param {INode[]} nodes - a list of nodes. + * @returns {INode} a random node from the list. + */ +export default function getRandomNode(nodes: INode[]): INode { + return nodes[Math.floor(Math.random() * nodes.length)]; +} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 91916d71..1d2cf66e 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -5,4 +5,5 @@ export { default as createLogger } from './createLogger'; export { default as formatCurrencyUnit } from './formatCurrencyUnit'; export { default as getAlgodClient } from './getAlgodClient'; export { default as getIndexerClient } from './getIndexerClient'; +export { default as getRandomNode } from './getRandomNode'; export { default as mapSerializableErrors } from './mapSerializableErrors'; diff --git a/src/extension/apps/background/App.tsx b/src/extension/apps/background/App.tsx index 479b5389..fbdccd4c 100644 --- a/src/extension/apps/background/App.tsx +++ b/src/extension/apps/background/App.tsx @@ -12,6 +12,7 @@ import { reducer as accountsReducer } from '@extension/features/accounts'; import { reducer as assetsReducer } from '@extension/features/assets'; import { reducer as messagesReducer } from '@extension/features/messages'; import { reducer as networksReducer } from '@extension/features/networks'; +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 systemReducer } from '@extension/features/system'; @@ -29,6 +30,7 @@ const App: FC = ({ i18next, initialColorMode }: IAppProps) => { assets: assetsReducer, messages: messagesReducer, networks: networksReducer, + sendAssets: sendAssetsReducer, sessions: sessionsReducer, settings: settingsReducer, system: systemReducer, diff --git a/src/extension/apps/main/App.tsx b/src/extension/apps/main/App.tsx index 553fe587..05fe3566 100644 --- a/src/extension/apps/main/App.tsx +++ b/src/extension/apps/main/App.tsx @@ -20,6 +20,7 @@ import { reducer as accountsReducer } from '@extension/features/accounts'; import { reducer as assetsReducer } from '@extension/features/assets'; import { reducer as messagesReducer } from '@extension/features/messages'; import { reducer as networksReducer } from '@extension/features/networks'; +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 { @@ -86,6 +87,7 @@ const App: FC = ({ i18next, initialColorMode }: IAppProps) => { assets: assetsReducer, messages: messagesReducer, networks: networksReducer, + sendAssets: sendAssetsReducer, sessions: sessionsReducer, settings: settingsReducer, system: systemReducer, diff --git a/src/extension/apps/main/Root.tsx b/src/extension/apps/main/Root.tsx index 519debff..d5a67424 100644 --- a/src/extension/apps/main/Root.tsx +++ b/src/extension/apps/main/Root.tsx @@ -1,4 +1,3 @@ -import { createStandaloneToast } from '@chakra-ui/react'; import React, { FC, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { NavigateFunction, Outlet, useNavigate } from 'react-router-dom'; @@ -8,6 +7,7 @@ import ConfirmModal from '@extension/components/ConfirmModal'; import EnableModal from '@extension/components/EnableModal'; import ErrorModal from '@extension/components/ErrorModal'; import MainLayout from '@extension/components/MainLayout'; +import SendAssetModal from '@extension/components/SendAssetModal'; import SignBytesModal from '@extension/components/SignBytesModal'; import SignTxnsModal from '@extension/components/SignTxnsModal'; import WalletConnectModal from '@extension/components/WalletConnectModal'; @@ -17,12 +17,6 @@ import { fetchAccountsFromStorageThunk, startPollingForAccountInformationThunk, } from '@extension/features/accounts'; -import { - setConfirm, - setError, - setNavigate, - setToast, -} from '@extension/features/system'; import { fetchAssetsThunk, updateAssetInformationThunk, @@ -32,12 +26,18 @@ import { setSignBytesRequest, setSignTxnsRequest, } from '@extension/features/messages'; +import { + fetchTransactionParamsFromStorageThunk, + startPollingForTransactionsParamsThunk, +} from '@extension/features/networks'; +import { reset as resetSendAsset } from '@extension/features/send-assets'; import { closeWalletConnectModal, fetchSessionsThunk, initializeWalletConnectThunk, } from '@extension/features/sessions'; import { fetchSettings } from '@extension/features/settings'; +import { setConfirm, setError, setNavigate } from '@extension/features/system'; // hooks import useOnMainAppMessage from '@extension/hooks/useOnMainAppMessage'; @@ -50,9 +50,6 @@ import { useSelectSelectedNetwork, } from '@extension/selectors'; -// theme -import { theme } from '@extension/theme'; - // types import { IAccount, @@ -73,48 +70,41 @@ const Root: FC = () => { const accounts: IAccount[] = useSelectAccounts(); const assets: Record | null = useSelectAssets(); const selectedNetwork: INetwork | null = useSelectSelectedNetwork(); - // misc - const { toast, ToastContainer } = createStandaloneToast({ - defaultOptions: { - containerStyle: { - margin: '0', - maxWidth: '100%', - minWidth: '100%', - padding: '0.5rem', - width: '100%', - }, - duration: 6000, - position: 'top', - }, - theme, - }); // handlers const handleConfirmClose = () => dispatch(setConfirm(null)); const handleEnableModalClose = () => dispatch(setEnableRequest(null)); const handleErrorModalClose = () => dispatch(setError(null)); + const handleSendAssetModalClose = () => dispatch(resetSendAsset()); const handleSignBytesModalClose = () => dispatch(setSignBytesRequest(null)); const handleSignTxnsModalClose = () => dispatch(setSignTxnsRequest(null)); const handleWalletConnectModalClose = () => dispatch(closeWalletConnectModal()); + // 1. fetched required data from storage useEffect(() => { dispatch(setNavigate(navigate)); - dispatch(setToast(toast)); dispatch(fetchSettings()); dispatch(fetchSessionsThunk()); dispatch(fetchAssetsThunk()); dispatch(initializeWalletConnectThunk()); dispatch(startPollingForAccountInformationThunk()); + dispatch(startPollingForTransactionsParamsThunk()); }, []); - // fetch accounts when the selected network has been found and no accounts exist + // 2. when the selected network has been fetched from storage useEffect(() => { - if (selectedNetwork && accounts.length < 1) { - dispatch( - fetchAccountsFromStorageThunk({ - updateAccountInformation: true, - updateAccountTransactions: true, - }) - ); + if (selectedNetwork) { + // fetch accounts when no accounts exist + if (accounts.length < 1) { + dispatch( + fetchAccountsFromStorageThunk({ + updateAccountInformation: true, + updateAccountTransactions: true, + }) + ); + } + + // fetch the most recent transaction params for the selected network + dispatch(fetchTransactionParamsFromStorageThunk()); } }, [selectedNetwork]); // whenever the accounts are updated, check if any new assets exist in the account @@ -160,8 +150,8 @@ const Root: FC = () => { + - diff --git a/src/extension/apps/registration/Root.tsx b/src/extension/apps/registration/Root.tsx index 9ceda7cc..c6b0d9de 100644 --- a/src/extension/apps/registration/Root.tsx +++ b/src/extension/apps/registration/Root.tsx @@ -1,4 +1,4 @@ -import { Center, createStandaloneToast, Flex } from '@chakra-ui/react'; +import { Center, Flex } from '@chakra-ui/react'; import React, { FC, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { NavigateFunction, Outlet, useNavigate } from 'react-router-dom'; @@ -7,46 +7,27 @@ import { NavigateFunction, Outlet, useNavigate } from 'react-router-dom'; import ErrorModal from '@extension/components/ErrorModal'; // features -import { setError, setNavigate, setToast } from '@extension/features/system'; +import { setError, setNavigate } from '@extension/features/system'; import { fetchSettings } from '@extension/features/settings'; -// theme -import { theme } from '@extension/theme'; - // types import { IAppThunkDispatch } from '@extension/types'; const Root: FC = () => { const dispatch: IAppThunkDispatch = useDispatch(); const navigate: NavigateFunction = useNavigate(); - const { toast, ToastContainer } = createStandaloneToast({ - defaultOptions: { - containerStyle: { - margin: '0', - maxWidth: '100%', - minWidth: '100%', - padding: '0.5rem', - width: '100%', - }, - duration: 6000, - position: 'top', - }, - theme, - }); const handleErrorModalClose = () => { dispatch(setError(null)); }; useEffect(() => { dispatch(setNavigate(navigate)); - dispatch(setToast(toast)); dispatch(fetchSettings()); }, []); return ( <> -
= ({ account }: IProps) => { {/*amount*/} - {formatCurrencyUnit(standardUnitAmount)} + {formatCurrencyUnit(standardUnitAmount, asset.decimals)} diff --git a/src/extension/components/AccountSelect/AccountSelect.tsx b/src/extension/components/AccountSelect/AccountSelect.tsx index ca76face..f5ee3aaa 100644 --- a/src/extension/components/AccountSelect/AccountSelect.tsx +++ b/src/extension/components/AccountSelect/AccountSelect.tsx @@ -18,6 +18,7 @@ import useColorModeValue from '@extension/hooks/useColorModeValue'; // types import { IAccount } from '@extension/types'; +import { DEFAULT_GAP } from '@extension/constants'; interface IProps { accounts: IAccount[]; @@ -35,7 +36,7 @@ const AccountSelect: FC = ({ accounts, onSelect, value }: IProps) => { const minimumHeight: number = 48; return ( - + = ({ accounts, onSelect, value }: IProps) => { borderRadius="md" borderWidth="1px" minH={`${minimumHeight}px`} - px={4} - py={2} + px={DEFAULT_GAP - 2} + py={DEFAULT_GAP / 3} transition="all 0.2s" w="full" > @@ -53,7 +54,8 @@ const AccountSelect: FC = ({ accounts, onSelect, value }: IProps) => { - + + {accounts.map((account, index) => ( ) => void; + onChange: (event: ChangeEvent) => void; + value: string; +} + +const AddressInput: FC = ({ + disabled, + error, + label, + onBlur, + onChange, + value, +}: IProps) => { + const { t } = useTranslation(); + // hooks + const defaultTextColor: string = useDefaultTextColor(); + const primaryColor: string = usePrimaryColor(); + + return ( + + + {label || t('labels.address')} + + + + ('placeholders.enterAddress')} + type="text" + value={value} + /> + + + + {error} + + + ); +}; + +export default AddressInput; diff --git a/src/extension/components/AddressInput/hooks/index.ts b/src/extension/components/AddressInput/hooks/index.ts new file mode 100644 index 00000000..94da7366 --- /dev/null +++ b/src/extension/components/AddressInput/hooks/index.ts @@ -0,0 +1 @@ +export { default as useAddressInput } from './useAddressInput'; diff --git a/src/extension/components/AddressInput/hooks/useAddressInput.ts b/src/extension/components/AddressInput/hooks/useAddressInput.ts new file mode 100644 index 00000000..35c64085 --- /dev/null +++ b/src/extension/components/AddressInput/hooks/useAddressInput.ts @@ -0,0 +1,43 @@ +import { ChangeEvent, FocusEvent, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +// types +import { IUseAddressInputState } from '../types'; + +// utils +import { validate as validateAddress } from '../utils'; + +export default function useAddressInput(): IUseAddressInputState { + const { t } = useTranslation(); + // state + const [error, setError] = useState(null); + const [value, setValue] = useState(''); + // actions + const onBlur = (event: FocusEvent) => + setError(validateAddress(event.target.value, t)); + const onChange = (event: ChangeEvent) => { + setError(null); + setValue(event.target.value); + }; + const reset: () => void = () => { + setError(null); + setValue(''); + }; + const validate: () => string | null = () => { + const newError: string | null = validateAddress(value, t); + + setError(newError); + + return newError; + }; + + return { + error, + onBlur, + onChange, + reset, + setError, + validate, + value, + }; +} diff --git a/src/extension/components/AddressInput/index.ts b/src/extension/components/AddressInput/index.ts new file mode 100644 index 00000000..db8bc21c --- /dev/null +++ b/src/extension/components/AddressInput/index.ts @@ -0,0 +1,4 @@ +export { default } from './AddressInput'; +export * from './hooks'; +export * from './types'; +export * from './utils'; diff --git a/src/extension/components/AddressInput/types/IUseAddressInputState.ts b/src/extension/components/AddressInput/types/IUseAddressInputState.ts new file mode 100644 index 00000000..408f40e8 --- /dev/null +++ b/src/extension/components/AddressInput/types/IUseAddressInputState.ts @@ -0,0 +1,13 @@ +import { ChangeEvent, FocusEvent } from 'react'; + +interface IUseAddressInputState { + error: string | null; + onBlur: (event: FocusEvent) => void; + onChange: (event: ChangeEvent) => void; + reset: () => void; + setError: (value: string | null) => void; + validate: () => string | null; + value: string; +} + +export default IUseAddressInputState; diff --git a/src/extension/components/AddressInput/types/index.ts b/src/extension/components/AddressInput/types/index.ts new file mode 100644 index 00000000..4b514d25 --- /dev/null +++ b/src/extension/components/AddressInput/types/index.ts @@ -0,0 +1 @@ +export type { default as IUseAddressInputState } from './IUseAddressInputState'; diff --git a/src/extension/components/AddressInput/utils/index.ts b/src/extension/components/AddressInput/utils/index.ts new file mode 100644 index 00000000..99cccdcf --- /dev/null +++ b/src/extension/components/AddressInput/utils/index.ts @@ -0,0 +1 @@ +export { default as validate } from './validate'; diff --git a/src/extension/components/AddressInput/utils/validate.ts b/src/extension/components/AddressInput/utils/validate.ts new file mode 100644 index 00000000..306909af --- /dev/null +++ b/src/extension/components/AddressInput/utils/validate.ts @@ -0,0 +1,22 @@ +import { isValidAddress } from 'algosdk'; +import { TFunction } from 'i18next'; + +/** + * Validates the address. The address is valid if null is returned, otherwise the string will contain a human-readable + * error. + * @param {string} input - the input address. + * @param {TFunction} t - translate function from i18next. + * @returns {string | null} null if the address is valid, or if it is invalid, a string detailing a human-readable + * error. + */ +export default function validate(input: string, t: TFunction): string | null { + if (input.length <= 0) { + return t('errors.inputs.required', { name: 'Address' }); + } + + if (!isValidAddress(input)) { + return t('errors.inputs.invalidAddress'); + } + + return null; +} diff --git a/src/extension/components/AssetDisplay/AssetDisplay.tsx b/src/extension/components/AssetDisplay/AssetDisplay.tsx index 66338935..7d41fd09 100644 --- a/src/extension/components/AssetDisplay/AssetDisplay.tsx +++ b/src/extension/components/AssetDisplay/AssetDisplay.tsx @@ -45,7 +45,8 @@ const AssetDisplay: FC = ({ {`${prefix || ''}${formatCurrencyUnit( - convertToStandardUnit(atomicUnitAmount, decimals) + convertToStandardUnit(atomicUnitAmount, decimals), + decimals )}`} {icon} diff --git a/src/extension/components/AssetSelect/AssetSelect.tsx b/src/extension/components/AssetSelect/AssetSelect.tsx new file mode 100644 index 00000000..12fc4c33 --- /dev/null +++ b/src/extension/components/AssetSelect/AssetSelect.tsx @@ -0,0 +1,168 @@ +import React, { FC } from 'react'; +import Select, { GroupBase, OptionProps, SingleValueProps } from 'react-select'; +import { FilterOptionOption } from 'react-select/dist/declarations/src/filters'; + +// components +import AssetSelectOption from './AssetSelectOption'; +import AssetSelectSingleValue from './AssetSelectSingleValue'; + +// constants +import { OPTION_HEIGHT } from './constants'; + +// hooks +import useColorModeValue from '@extension/hooks/useColorModeValue'; + +// theme +import { theme } from '@extension/theme'; + +// types +import { + IAccount, + IAccountInformation, + IAsset, + INetworkWithTransactionParams, +} from '@extension/types'; +import { IOption } from './types'; + +// utils +import { + convertGenesisHashToHex, + createNativeCurrencyAsset, +} from '@extension/utils'; + +interface IProps { + account: IAccount; + assets: IAsset[]; + includeNativeCurrency?: boolean; + network: INetworkWithTransactionParams; + onAssetChange: (value: IAsset) => void; + value: IAsset; + width?: string | number; +} + +const AssetSelect: FC = ({ + account, + assets, + includeNativeCurrency = false, + network, + onAssetChange, + value, + width, +}: IProps) => { + // hooks + const primaryColor: string = useColorModeValue( + theme.colors.primaryLight['500'], + theme.colors.primaryDark['500'] + ); + const primaryColor25: string = useColorModeValue( + theme.colors.primaryLight['200'], + theme.colors.primaryDark['200'] + ); + const primaryColor50: string = useColorModeValue( + theme.colors.primaryLight['300'], + theme.colors.primaryDark['300'] + ); + const primaryColor75: string = useColorModeValue( + theme.colors.primaryLight['400'], + theme.colors.primaryDark['400'] + ); + // misc + const accountInformation: IAccountInformation | null = + account.networkInformation[ + convertGenesisHashToHex(network.genesisHash).toUpperCase() + ] || null; + const selectableAssets: IAsset[] = + accountInformation?.assetHoldings.reduce( + (acc, assetHolding) => { + const asset: IAsset | null = + assets.find((value) => value.id === assetHolding.id) || null; + + if (!asset) { + return acc; + } + + return [...acc, asset]; + }, + includeNativeCurrency ? [createNativeCurrencyAsset(network)] : [] + ) || []; + // handlers + const handleAssetChange = (value: IOption) => onAssetChange(value.asset); + const handleSearchFilter = ( + { data }: FilterOptionOption, + inputValue: string + ) => { + return !!( + data.asset.id.toUpperCase().includes(inputValue.toUpperCase()) || + (data.asset.unitName && + data.asset.unitName.toUpperCase().includes(inputValue.toUpperCase())) + ); + }; + + return ( +