From c481a8a25b60d3fca6165d362e5fcae892cde88d Mon Sep 17 00:00:00 2001 From: Kieran O'Neill Date: Wed, 27 Dec 2023 15:16:11 +0000 Subject: [PATCH] feat: send arc200 asset (#71) * feat: add type to asset holdings * feat: add arc200 support to asset page * feat: add arc200 support to the asset select * fix: arc200 explorer links now go to applications * feat: update modal to handle response from sumbit transaction response * feat: implement sending arc200 tokens * fix: encode uri when opening links * fix: liniting errors --- .../components/AccountItem/AccountItem.tsx | 6 +- .../components/AssetSelect/AssetSelect.tsx | 126 +++++++-- .../AssetSelectArc200AssetOption.tsx | 138 ++++++++++ .../AssetSelectArc200AssetSingleValue.tsx | 70 +++++ ...tsx => AssetSelectStandardAssetOption.tsx} | 131 ++++++--- ...> AssetSelectStandardAssetSingleValue.tsx} | 30 ++- src/extension/components/AssetSelect/index.ts | 2 +- .../components/AssetSelect/types/IOption.ts | 4 +- .../OpenTabIconButton/OpenTabIconButton.tsx | 4 +- .../thunks/addArc200AssetHoldingThunk.ts | 3 +- .../utils/fetchArc200AssetHoldingWithDelay.ts | 4 + src/extension/features/send-assets/slice.ts | 38 +-- .../thunks/submitTransactionThunk.ts | 113 ++++---- .../types/IInitializeSendAssetPayload.ts | 4 +- .../send-assets/types/ISendAssetsState.ts | 13 +- .../utils/createSendAssetTransaction.ts | 64 ----- .../send-assets/utils/getInitialState.ts | 2 - .../features/send-assets/utils/index.ts | 3 +- .../sendArc200AssetTransferTransaction.ts | 67 +++++ .../sendStandardAssetTransferTransaction.ts | 111 ++++++++ .../modals/SendAssetModal/SendAmountInput.tsx | 24 +- .../modals/SendAssetModal/SendAssetModal.tsx | 248 ++++++++++-------- .../SendAssetModalConfirmingContent.tsx | 41 +++ .../SendAssetModalSummaryContent.tsx | 113 +++++--- src/extension/pages/AssetPage/AssetPage.tsx | 85 ++++-- .../useAssetPage/types/IUseAssetPageState.ts | 8 +- .../hooks/useAssetPage/useAssetPage.ts | 77 ++++-- src/extension/selectors/index.ts | 2 - .../selectors/useSelectSendingAssetError.ts | 13 - .../useSelectSendingAssetSelectedAsset.ts | 9 +- .../useSelectSendingAssetTransactionId.ts | 10 - src/extension/services/AccountService.ts | 18 +- src/extension/types/IArc200AssetHolding.ts | 15 +- src/extension/types/IBaseAssetHolding.ts | 14 + src/extension/types/IStandardAssetHolding.ts | 13 +- src/extension/types/index.ts | 1 + .../utils/calculateMaxTransactionAmount.ts | 60 +++-- .../mapAlgorandAccountInformationToAccount.ts | 4 + 38 files changed, 1163 insertions(+), 525 deletions(-) create mode 100644 src/extension/components/AssetSelect/AssetSelectArc200AssetOption.tsx create mode 100644 src/extension/components/AssetSelect/AssetSelectArc200AssetSingleValue.tsx rename src/extension/components/AssetSelect/{AssetSelectOption.tsx => AssetSelectStandardAssetOption.tsx} (69%) rename src/extension/components/AssetSelect/{AssetSelectSingleValue.tsx => AssetSelectStandardAssetSingleValue.tsx} (72%) delete mode 100644 src/extension/features/send-assets/utils/createSendAssetTransaction.ts create mode 100644 src/extension/features/send-assets/utils/sendArc200AssetTransferTransaction.ts create mode 100644 src/extension/features/send-assets/utils/sendStandardAssetTransferTransaction.ts create mode 100644 src/extension/modals/SendAssetModal/SendAssetModalConfirmingContent.tsx delete mode 100644 src/extension/selectors/useSelectSendingAssetError.ts delete mode 100644 src/extension/selectors/useSelectSendingAssetTransactionId.ts create mode 100644 src/extension/types/IBaseAssetHolding.ts diff --git a/src/extension/components/AccountItem/AccountItem.tsx b/src/extension/components/AccountItem/AccountItem.tsx index cca9a821..5bfad8a1 100644 --- a/src/extension/components/AccountItem/AccountItem.tsx +++ b/src/extension/components/AccountItem/AccountItem.tsx @@ -3,7 +3,6 @@ import React, { FC } from 'react'; import { IoWalletOutline } from 'react-icons/io5'; // hooks -import useAccountInformation from '@extension/hooks/useAccountInformation'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; import usePrimaryColor from '@extension/hooks/usePrimaryColor'; @@ -13,7 +12,7 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; import { AccountService } from '@extension/services'; // types -import { IAccount, IAccountInformation } from '@extension/types'; +import { IAccount } from '@extension/types'; // utils import { ellipseAddress } from '@extension/utils'; @@ -29,9 +28,6 @@ const AccountItem: FC = ({ subTextColor, textColor, }: IProps) => { - const accountInformation: IAccountInformation | null = useAccountInformation( - account.id - ); const defaultSubTextColor: string = useSubTextColor(); const defaultTextColor: string = useDefaultTextColor(); const primaryButtonTextColor: string = usePrimaryButtonTextColor(); diff --git a/src/extension/components/AssetSelect/AssetSelect.tsx b/src/extension/components/AssetSelect/AssetSelect.tsx index 8aeaf4c2..4cd7067b 100644 --- a/src/extension/components/AssetSelect/AssetSelect.tsx +++ b/src/extension/components/AssetSelect/AssetSelect.tsx @@ -3,12 +3,17 @@ 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'; +import AssetSelectArc200AssetOption from './AssetSelectArc200AssetOption'; +import AssetSelectArc200AssetSingleValue from './AssetSelectArc200AssetSingleValue'; +import AssetSelectStandardAssetOption from './AssetSelectStandardAssetOption'; +import AssetSelectStandardAssetSingleValue from './AssetSelectStandardAssetSingleValue'; // constants import { OPTION_HEIGHT } from './constants'; +// enums +import { AssetTypeEnum } from '@extension/enums'; + // hooks import useColorModeValue from '@extension/hooks/useColorModeValue'; @@ -21,6 +26,7 @@ import { IAccountInformation, IStandardAsset, INetworkWithTransactionParams, + IArc200Asset, } from '@extension/types'; import { IOption } from './types'; @@ -32,11 +38,11 @@ import { interface IProps { account: IAccount; - assets: IStandardAsset[]; + assets: (IArc200Asset | IStandardAsset)[]; includeNativeCurrency?: boolean; network: INetworkWithTransactionParams; - onAssetChange: (value: IStandardAsset) => void; - value: IStandardAsset; + onAssetChange: (value: IArc200Asset | IStandardAsset) => void; + value: IArc200Asset | IStandardAsset; width?: string | number; } @@ -71,17 +77,36 @@ const AssetSelect: FC = ({ account.networkInformation[ convertGenesisHashToHex(network.genesisHash).toUpperCase() ] || null; - const selectableAssets: IStandardAsset[] = - accountInformation?.standardAssetHoldings.reduce( - (acc, assetHolding) => { - const asset: IStandardAsset | null = - assets.find((value) => value.id === assetHolding.id) || null; + const selectableAssets: (IArc200Asset | IStandardAsset)[] = + assets.reduce<(IArc200Asset | IStandardAsset)[]>( + (acc, asset) => { + let selectedAsset: IArc200Asset | IStandardAsset | null; + + // check if the asset exists in the asset holdings of the account; has it been "added" + switch (asset.type) { + case AssetTypeEnum.Arc200: + selectedAsset = accountInformation?.arc200AssetHoldings.find( + (value) => value.id === asset.id + ) + ? asset + : null; + break; + case AssetTypeEnum.Standard: + selectedAsset = accountInformation?.standardAssetHoldings.find( + (value) => value.id === asset.id + ) + ? asset + : null; + break; + default: + selectedAsset = null; + } - if (!asset) { + if (!selectedAsset) { return acc; } - return [...acc, asset]; + return [...acc, selectedAsset]; }, includeNativeCurrency ? [createNativeCurrencyAsset(network)] : [] ) || []; @@ -91,11 +116,23 @@ const AssetSelect: FC = ({ { data }: FilterOptionOption, inputValue: string ) => { - return !!( - data.asset.id.toUpperCase().includes(inputValue.toUpperCase()) || - (data.asset.unitName && - data.asset.unitName.toUpperCase().includes(inputValue.toUpperCase())) - ); + switch (data.asset.type) { + case AssetTypeEnum.Arc200: + return ( + data.asset.id.toUpperCase().includes(inputValue.toUpperCase()) || + data.asset.symbol.toUpperCase().includes(inputValue.toUpperCase()) + ); + case AssetTypeEnum.Standard: + return !!( + data.asset.id.toUpperCase().includes(inputValue.toUpperCase()) || + (data.asset.unitName && + data.asset.unitName + .toUpperCase() + .includes(inputValue.toUpperCase())) + ); + default: + return false; + } }; return ( @@ -105,19 +142,52 @@ const AssetSelect: FC = ({ data, innerProps, isSelected, - }: OptionProps>) => ( - - ), + }: OptionProps>) => { + switch (data.asset.type) { + case AssetTypeEnum.Arc200: + return ( + + ); + case AssetTypeEnum.Standard: + return ( + + ); + default: + return null; + } + }, SingleValue: ({ data, - }: SingleValueProps>) => ( - - ), + }: SingleValueProps>) => { + switch (data.asset.type) { + case AssetTypeEnum.Arc200: + return ( + + ); + case AssetTypeEnum.Standard: + return ( + + ); + default: + return null; + } + }, }} filterOption={handleSearchFilter} onChange={handleAssetChange} diff --git a/src/extension/components/AssetSelect/AssetSelectArc200AssetOption.tsx b/src/extension/components/AssetSelect/AssetSelectArc200AssetOption.tsx new file mode 100644 index 00000000..a7042ca8 --- /dev/null +++ b/src/extension/components/AssetSelect/AssetSelectArc200AssetOption.tsx @@ -0,0 +1,138 @@ +import { HStack, Spacer, Text, VStack } from '@chakra-ui/react'; +import React, { FC, ReactEventHandler, useState } from 'react'; + +// components +import AssetAvatar from '@extension/components/AssetAvatar'; +import AssetBadge from '@extension/components/AssetBadge'; +import AssetIcon from '@extension/components/AssetIcon'; + +// constants +import { DEFAULT_GAP } from '@extension/constants'; +import { OPTION_HEIGHT } from './constants'; + +// hooks +import useButtonHoverBackgroundColor from '@extension/hooks/useButtonHoverBackgroundColor'; +import useColorModeValue from '@extension/hooks/useColorModeValue'; +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; +import useSubTextColor from '@extension/hooks/useSubTextColor'; + +// theme +import { theme } from '@extension/theme'; + +// types +import { IArc200Asset, INetworkWithTransactionParams } from '@extension/types'; + +interface IProps { + asset: IArc200Asset; + isSelected: boolean; + onClick?: ReactEventHandler; + network: INetworkWithTransactionParams; +} + +const AssetSelectOption: FC = ({ + asset, + isSelected, + onClick, + network, +}: IProps) => { + // hooks + const buttonHoverBackgroundColor: string = useButtonHoverBackgroundColor(); + const defaultTextColor: string = useDefaultTextColor(); + const primaryButtonTextColor: string = usePrimaryButtonTextColor(); + const primaryColor: string = useColorModeValue( + theme.colors.primaryLight['500'], + theme.colors.primaryDark['500'] + ); + const subTextColor: string = useSubTextColor(); + // state + const [backgroundColor, setBackgroundColor] = useState( + isSelected ? primaryColor : 'var(--chakra-colors-chakra-body-bg)' + ); + // misc + const formattedDefaultTextColor: string = isSelected + ? primaryButtonTextColor + : defaultTextColor; + const formattedSubTextColor: string = isSelected + ? primaryButtonTextColor + : subTextColor; + // handlers + const handleMouseEnter = () => { + if (!isSelected) { + setBackgroundColor(buttonHoverBackgroundColor); + } + }; + const handleMouseLeave = () => { + if (!isSelected) { + setBackgroundColor('var(--chakra-colors-chakra-body-bg)'); + } + }; + + return ( + + {/*icon*/} + + } + size="xs" + /> + + {/*name/symbol*/} + + + {asset.name} + + + + {asset.symbol} + + + + + + {/*id/type*/} + + + + + {asset.id} + + + + ); +}; + +export default AssetSelectOption; diff --git a/src/extension/components/AssetSelect/AssetSelectArc200AssetSingleValue.tsx b/src/extension/components/AssetSelect/AssetSelectArc200AssetSingleValue.tsx new file mode 100644 index 00000000..f8962d2e --- /dev/null +++ b/src/extension/components/AssetSelect/AssetSelectArc200AssetSingleValue.tsx @@ -0,0 +1,70 @@ +import { HStack, Spacer, Text } from '@chakra-ui/react'; +import React, { FC } from 'react'; + +// components +import AssetAvatar from '@extension/components/AssetAvatar'; +import AssetBadge from '@extension/components/AssetBadge'; +import AssetIcon from '@extension/components/AssetIcon'; + +// constants +import { DEFAULT_GAP } from '@extension/constants'; +import { OPTION_HEIGHT } from './constants'; + +// hooks +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; + +// types +import { IArc200Asset, INetworkWithTransactionParams } from '@extension/types'; + +interface IProps { + asset: IArc200Asset; + network: INetworkWithTransactionParams; +} + +const AssetSelectArc200AssetSingleValue: FC = ({ + asset, + network, +}: IProps) => { + // hooks + const defaultTextColor: string = useDefaultTextColor(); + const primaryButtonTextColor: string = usePrimaryButtonTextColor(); + + return ( + + {/*icon*/} + + } + size="xs" + /> + + {/*symbol*/} + + {asset.symbol} + + + + + {/*type*/} + + + ); +}; + +export default AssetSelectArc200AssetSingleValue; diff --git a/src/extension/components/AssetSelect/AssetSelectOption.tsx b/src/extension/components/AssetSelect/AssetSelectStandardAssetOption.tsx similarity index 69% rename from src/extension/components/AssetSelect/AssetSelectOption.tsx rename to src/extension/components/AssetSelect/AssetSelectStandardAssetOption.tsx index fd9509f0..bd7994d1 100644 --- a/src/extension/components/AssetSelect/AssetSelectOption.tsx +++ b/src/extension/components/AssetSelect/AssetSelectStandardAssetOption.tsx @@ -1,8 +1,15 @@ -import { HStack, Text, VStack } from '@chakra-ui/react'; -import React, { FC, ReactEventHandler, SyntheticEvent, useState } from 'react'; +import { HStack, Spacer, Text, VStack } from '@chakra-ui/react'; +import React, { + FC, + ReactEventHandler, + ReactNode, + SyntheticEvent, + useState, +} from 'react'; // components import AssetAvatar from '@extension/components/AssetAvatar'; +import AssetBadge from '@extension/components/AssetBadge'; import AssetIcon from '@extension/components/AssetIcon'; // constants @@ -32,7 +39,7 @@ interface IProps { network: INetworkWithTransactionParams; } -const AssetSelectOption: FC = ({ +const AssetSelectStandardAssetOption: FC = ({ asset, isSelected, onClick, @@ -70,7 +77,9 @@ const AssetSelectOption: FC = ({ } }; // renders - const renderUnitName = () => { + const renderContent = () => { + let idAndTypeNode: ReactNode; + if (asset.id === '0') { return ( @@ -79,15 +88,26 @@ const AssetSelectOption: FC = ({ ); } + idAndTypeNode = ( + + + + + {asset.id} + + + ); + if (!asset.unitName) { if (asset.name) { return ( - + <> + {/*name*/} = ({ {asset.name} - - {asset.id} - - + + + {/*id/type*/} + {idAndTypeNode} + ); } return ( - - {asset.id} - + <> + {/*id*/} + + {asset.id} + + + + + {/*type*/} + + ); } - return ( - - - + // if there is a unit, but no name + if (!asset.name) { + return ( + <> + {/*unit*/} + {asset.unitName} - - {asset.id} - - + + + {/*id/type*/} + {idAndTypeNode} + + ); + } - {asset.name && ( + // if there is a unit and name + return ( + <> + {/*name/unit*/} + {asset.name} - )} - + + + {asset.unitName} + + + + + + {/*id/type*/} + {idAndTypeNode} + ); }; @@ -175,10 +221,9 @@ const AssetSelectOption: FC = ({ size="xs" /> - {/*name/unit*/} - {renderUnitName()} + {renderContent()} ); }; -export default AssetSelectOption; +export default AssetSelectStandardAssetOption; diff --git a/src/extension/components/AssetSelect/AssetSelectSingleValue.tsx b/src/extension/components/AssetSelect/AssetSelectStandardAssetSingleValue.tsx similarity index 72% rename from src/extension/components/AssetSelect/AssetSelectSingleValue.tsx rename to src/extension/components/AssetSelect/AssetSelectStandardAssetSingleValue.tsx index 4069499b..bbda3328 100644 --- a/src/extension/components/AssetSelect/AssetSelectSingleValue.tsx +++ b/src/extension/components/AssetSelect/AssetSelectStandardAssetSingleValue.tsx @@ -1,11 +1,13 @@ -import { HStack, Text } from '@chakra-ui/react'; +import { HStack, Spacer, Text } from '@chakra-ui/react'; import React, { FC } from 'react'; // components import AssetAvatar from '@extension/components/AssetAvatar'; +import AssetBadge from '@extension/components/AssetBadge'; import AssetIcon from '@extension/components/AssetIcon'; // constants +import { DEFAULT_GAP } from '@extension/constants'; import { OPTION_HEIGHT } from './constants'; // hooks @@ -17,19 +19,21 @@ import { IStandardAsset, INetworkWithTransactionParams, } from '@extension/types'; -import { DEFAULT_GAP } from '@extension/constants'; interface IProps { asset: IStandardAsset; network: INetworkWithTransactionParams; } -const AssetSelectSingleValue: FC = ({ asset, network }: IProps) => { +const AssetSelectStandardAssetSingleValue: FC = ({ + asset, + network, +}: IProps) => { // hooks const defaultTextColor: string = useDefaultTextColor(); const primaryButtonTextColor: string = usePrimaryButtonTextColor(); // renders - const renderUnitName = () => { + const renderContent = () => { if (asset.id === '0') { return ( @@ -39,9 +43,16 @@ const AssetSelectSingleValue: FC = ({ asset, network }: IProps) => { } return ( - - {asset.unitName || asset.id} - + <> + + {asset.unitName || asset.id} + + + + + {/*type*/} + + ); }; @@ -69,10 +80,9 @@ const AssetSelectSingleValue: FC = ({ asset, network }: IProps) => { size="xs" /> - {/*name/unit*/} - {renderUnitName()} + {renderContent()} ); }; -export default AssetSelectSingleValue; +export default AssetSelectStandardAssetSingleValue; diff --git a/src/extension/components/AssetSelect/index.ts b/src/extension/components/AssetSelect/index.ts index be8fdffa..aa1f266e 100644 --- a/src/extension/components/AssetSelect/index.ts +++ b/src/extension/components/AssetSelect/index.ts @@ -1,2 +1,2 @@ export { default } from './AssetSelect'; -export { default as AssetSelectOption } from './AssetSelectOption'; +export { default as AssetSelectOption } from './AssetSelectStandardAssetOption'; diff --git a/src/extension/components/AssetSelect/types/IOption.ts b/src/extension/components/AssetSelect/types/IOption.ts index 3f93d334..de55e5e1 100644 --- a/src/extension/components/AssetSelect/types/IOption.ts +++ b/src/extension/components/AssetSelect/types/IOption.ts @@ -1,7 +1,7 @@ -import { IStandardAsset } from '@extension/types'; +import { IArc200Asset, IStandardAsset } from '@extension/types'; interface IOption { - asset: IStandardAsset; + asset: IArc200Asset | IStandardAsset; value: string; } diff --git a/src/extension/components/OpenTabIconButton/OpenTabIconButton.tsx b/src/extension/components/OpenTabIconButton/OpenTabIconButton.tsx index 1252de79..cc013f85 100644 --- a/src/extension/components/OpenTabIconButton/OpenTabIconButton.tsx +++ b/src/extension/components/OpenTabIconButton/OpenTabIconButton.tsx @@ -18,11 +18,13 @@ const OpenTabIconButton: FC = ({ tooltipLabel, url, }: IProps) => { + // hooks const buttonHoverBackgroundColor: string = useButtonHoverBackgroundColor(); const defaultTextColor: string = useDefaultTextColor(); + // handlers const handleOpenClick = () => browser.tabs.create({ - url, + url: encodeURI(url), }); return ( diff --git a/src/extension/features/accounts/thunks/addArc200AssetHoldingThunk.ts b/src/extension/features/accounts/thunks/addArc200AssetHoldingThunk.ts index 0ac1ce73..52ae7074 100644 --- a/src/extension/features/accounts/thunks/addArc200AssetHoldingThunk.ts +++ b/src/extension/features/accounts/thunks/addArc200AssetHoldingThunk.ts @@ -7,7 +7,7 @@ import { NODE_REQUEST_DELAY } from '@extension/constants'; import { updateAccountInformation } from '@extension/features/accounts'; // enums -import { AccountsThunkEnum } from '@extension/enums'; +import { AccountsThunkEnum, AssetTypeEnum } from '@extension/enums'; // services import { AccountService } from '@extension/services'; @@ -107,6 +107,7 @@ const addArc200AssetHoldingThunk: AsyncThunk< { amount: '0', id: appId, + type: AssetTypeEnum.Arc200, }, ], }, diff --git a/src/extension/features/accounts/utils/fetchArc200AssetHoldingWithDelay.ts b/src/extension/features/accounts/utils/fetchArc200AssetHoldingWithDelay.ts index 59c391c7..508728a8 100644 --- a/src/extension/features/accounts/utils/fetchArc200AssetHoldingWithDelay.ts +++ b/src/extension/features/accounts/utils/fetchArc200AssetHoldingWithDelay.ts @@ -1,6 +1,9 @@ import { Algodv2 } from 'algosdk'; import Arc200Contract from 'arc200js'; +// enums +import { AssetTypeEnum } from '@extension/enums'; + // types import { IArc200AssetHolding } from '@extension/types'; @@ -42,6 +45,7 @@ export default async function fetchArc200AssetHoldingWithDelay({ resolve({ id: arc200AppId, amount, + type: AssetTypeEnum.Arc200, }); } catch (error) { reject(error); diff --git a/src/extension/features/send-assets/slice.ts b/src/extension/features/send-assets/slice.ts index f5a2c8bd..99a2cebc 100644 --- a/src/extension/features/send-assets/slice.ts +++ b/src/extension/features/send-assets/slice.ts @@ -1,13 +1,4 @@ -import { - createSlice, - Draft, - PayloadAction, - Reducer, - SerializedError, -} from '@reduxjs/toolkit'; - -// errors -import { BaseExtensionError } from '@extension/errors'; +import { createSlice, Draft, PayloadAction, Reducer } from '@reduxjs/toolkit'; // enums import { StoreNameEnum } from '@extension/enums'; @@ -16,7 +7,7 @@ import { StoreNameEnum } from '@extension/enums'; import { submitTransactionThunk } from './thunks'; // types -import { IStandardAsset, IRejectedActionMeta } from '@extension/types'; +import { IStandardAsset, IArc200Asset } from '@extension/types'; import { IInitializeSendAssetPayload, ISendAssetsState } from './types'; // utils @@ -27,8 +18,7 @@ const slice = createSlice({ /** submit transaction **/ builder.addCase( submitTransactionThunk.fulfilled, - (state: ISendAssetsState, action: PayloadAction) => { - state.transactionId = action.payload; + (state: ISendAssetsState) => { state.confirming = false; } ); @@ -40,16 +30,7 @@ const slice = createSlice({ ); builder.addCase( submitTransactionThunk.rejected, - ( - state: ISendAssetsState, - action: PayloadAction< - BaseExtensionError, - string, - IRejectedActionMeta, - SerializedError - > - ) => { - state.error = action.payload; + (state: ISendAssetsState) => { state.confirming = false; } ); @@ -67,12 +48,10 @@ const slice = createSlice({ reset: (state: Draft) => { state.amountInStandardUnits = '0'; state.confirming = false; - state.error = null; state.fromAddress = null; state.note = null; state.selectedAsset = null; state.toAddress = null; - state.transactionId = null; }, setAmount: ( state: Draft, @@ -80,12 +59,6 @@ const slice = createSlice({ ) => { state.amountInStandardUnits = action.payload; }, - setError: ( - state: Draft, - action: PayloadAction - ) => { - state.error = action.payload; - }, setFromAddress: ( state: Draft, action: PayloadAction @@ -100,7 +73,7 @@ const slice = createSlice({ }, setSelectedAsset: ( state: Draft, - action: PayloadAction + action: PayloadAction ) => { state.selectedAsset = action.payload; }, @@ -118,7 +91,6 @@ export const { initializeSendAsset, reset, setAmount, - setError, setFromAddress, setNote, setSelectedAsset, diff --git a/src/extension/features/send-assets/thunks/submitTransactionThunk.ts b/src/extension/features/send-assets/thunks/submitTransactionThunk.ts index 8543abb8..28d7aa85 100644 --- a/src/extension/features/send-assets/thunks/submitTransactionThunk.ts +++ b/src/extension/features/send-assets/thunks/submitTransactionThunk.ts @@ -1,17 +1,10 @@ import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; -import { - Address, - Algodv2, - decodeAddress, - IntDecoding, - SuggestedParams, - Transaction, - waitForConfirmation, -} from 'algosdk'; +import { Address, Algodv2, decodeAddress, SuggestedParams } from 'algosdk'; +import BigNumber from 'bignumber.js'; import browser from 'webextension-polyfill'; // enums -import { SendAssetsThunkEnum } from '@extension/enums'; +import { AssetTypeEnum, SendAssetsThunkEnum } from '@extension/enums'; // errors import { @@ -30,17 +23,23 @@ import { AccountService, PrivateKeyService } from '@extension/services'; import { ILogger } from '@common/types'; import { IAccount, - IAlgorandPendingTransactionResponse, IStandardAsset, IMainRootState, INetworkWithTransactionParams, + IArc200Asset, } from '@extension/types'; // utils -import { convertToAtomicUnit, getAlgodClient } from '@common/utils'; +import { + convertToAtomicUnit, + getAlgodClient, + getIndexerClient, +} from '@common/utils'; import { selectNetworkFromSettings } from '@extension/utils'; -import { createSendAssetTransaction } from '../utils'; -import BigNumber from 'bignumber.js'; +import { + sendArc200AssetTransferTransaction, + sendStandardAssetTransferTransaction, +} from '../utils'; interface AsyncThunkConfig { state: IMainRootState; @@ -56,25 +55,22 @@ const submitTransactionThunk: AsyncThunk< async (password, { getState, rejectWithValue }) => { const amountInStandardUnits: string | null = getState().sendAssets.amountInStandardUnits; - const asset: IStandardAsset | null = getState().sendAssets.selectedAsset; + const asset: IArc200Asset | IStandardAsset | null = + getState().sendAssets.selectedAsset; const fromAddress: string | null = getState().sendAssets.fromAddress; const logger: ILogger = getState().system.logger; const networks: INetworkWithTransactionParams[] = getState().networks.items; const online: boolean = getState().system.online; - const note: string | null = getState().sendAssets.note; - const selectedNetwork: INetworkWithTransactionParams | null = + const network: INetworkWithTransactionParams | null = selectNetworkFromSettings(networks, getState().settings); + const note: string | null = getState().sendAssets.note; const toAddress: string | null = getState().sendAssets.toAddress; let fromAccount: IAccount | null; let algodClient: Algodv2; let decodedAddress: Address; let privateKey: Uint8Array | null; let privateKeyService: PrivateKeyService; - let sentRawTransaction: { txId: string }; - let signedTransactionData: Uint8Array; let suggestedParams: SuggestedParams; - let transactionResponse: IAlgorandPendingTransactionResponse; - let unsignedTransaction: Transaction; if (!amountInStandardUnits || !asset || !fromAddress || !toAddress) { logger.debug( @@ -113,7 +109,7 @@ const submitTransactionThunk: AsyncThunk< ); } - if (!selectedNetwork) { + if (!network) { logger.debug( `${SendAssetsThunkEnum.SubmitTransaction}: no network selected` ); @@ -152,50 +148,47 @@ const submitTransactionThunk: AsyncThunk< return rejectWithValue(error); } - algodClient = getAlgodClient(selectedNetwork, { + algodClient = getAlgodClient(network, { logger, }); try { suggestedParams = await algodClient.getTransactionParams().do(); - unsignedTransaction = createSendAssetTransaction({ - amount: convertToAtomicUnit( - new BigNumber(amountInStandardUnits), - asset.decimals - ).toString(), // convert to atomic units - asset, - fromAddress, - note, - suggestedParams, - toAddress, - }); - signedTransactionData = unsignedTransaction.signTxn(privateKey); - - logger.debug( - `${SendAssetsThunkEnum.SubmitTransaction}: sending transaction to the network` - ); - sentRawTransaction = await algodClient - .sendRawTransaction(signedTransactionData) - .setIntDecoding(IntDecoding.BIGINT) - .do(); - - logger.debug( - `${SendAssetsThunkEnum.SubmitTransaction}: transaction "${sentRawTransaction.txId}" sent to the network, confirming` - ); - - transactionResponse = (await waitForConfirmation( - algodClient, - sentRawTransaction.txId, - 4 - )) as IAlgorandPendingTransactionResponse; - - logger.debug( - `${SendAssetsThunkEnum.SubmitTransaction}: transaction "${sentRawTransaction.txId}" confirmed in round "${transactionResponse['confirmed-round']}"` - ); - - // on success, return the transaction id - return sentRawTransaction.txId; + switch (asset.type) { + case AssetTypeEnum.Arc200: + return await sendArc200AssetTransferTransaction({ + algodClient, + amount: convertToAtomicUnit( + new BigNumber(amountInStandardUnits), + asset.decimals + ).toString(), + asset, + fromAddress, + indexerClient: getIndexerClient(network, { logger }), + logger, + note, + privateKey, + toAddress, + }); + case AssetTypeEnum.Standard: + return sendStandardAssetTransferTransaction({ + algodClient, + amount: convertToAtomicUnit( + new BigNumber(amountInStandardUnits), + asset.decimals + ).toString(), // convert to atomic units + asset, + fromAddress, + logger, + note, + privateKey, + suggestedParams, + toAddress, + }); + default: + throw new Error('unknown asset'); + } } catch (error) { logger.debug( `${SendAssetsThunkEnum.SubmitTransaction}(): ${error.message}` diff --git a/src/extension/features/send-assets/types/IInitializeSendAssetPayload.ts b/src/extension/features/send-assets/types/IInitializeSendAssetPayload.ts index f6f31599..c5d5b740 100644 --- a/src/extension/features/send-assets/types/IInitializeSendAssetPayload.ts +++ b/src/extension/features/send-assets/types/IInitializeSendAssetPayload.ts @@ -1,8 +1,8 @@ -import { IStandardAsset } from '@extension/types'; +import { IArc200Asset, IStandardAsset } from '@extension/types'; interface IInitializeSendAssetPayload { fromAddress: string | null; - selectedAsset: IStandardAsset; + selectedAsset: IArc200Asset | 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 79153d77..ddb7a76c 100644 --- a/src/extension/features/send-assets/types/ISendAssetsState.ts +++ b/src/extension/features/send-assets/types/ISendAssetsState.ts @@ -1,28 +1,21 @@ -// errors -import { BaseExtensionError } from '@extension/errors'; - // types -import { IStandardAsset } from '@extension/types'; +import { IArc200Asset, IStandardAsset } from '@extension/types'; /** * @property {string} amountInStandardUnits - the amount, in standard units, to send. Defaults to "0". * @property {boolean} confirming - confirming the transaction to the network. - * @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 {IStandardAsset | null} selectedAsset - the selected asset to send. + * @property {IArc200Asset | 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. */ interface ISendAssetsState { amountInStandardUnits: string; confirming: boolean; - error: BaseExtensionError | null; fromAddress: string | null; note: string | null; - selectedAsset: IStandardAsset | null; + selectedAsset: IArc200Asset | IStandardAsset | null; toAddress: string | null; - transactionId: string | null; } export default ISendAssetsState; diff --git a/src/extension/features/send-assets/utils/createSendAssetTransaction.ts b/src/extension/features/send-assets/utils/createSendAssetTransaction.ts deleted file mode 100644 index 5035b629..00000000 --- a/src/extension/features/send-assets/utils/createSendAssetTransaction.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - makePaymentTxnWithSuggestedParams, - makeAssetTransferTxnWithSuggestedParams, - SuggestedParams, - Transaction, -} from 'algosdk'; - -// types -import { IStandardAsset } from '@extension/types'; - -interface IOptions { - amount: string; - asset: IStandardAsset; - fromAddress: string; - note: string | null; - suggestedParams: SuggestedParams; - toAddress: string; -} - -/** - * Convenience function that creates a payment transaction or an asset transfer transaction based on the asset ID. - * If the asset ID is 0, we can assume this the native currency, so we use a payment transaction. - * @param {IOptions} options - the fields needed to create a transaction - * @returns {Transaction} an Algorand transaction ready to be signed. - */ -export default function createSendAssetTransaction({ - amount, - asset, - fromAddress, - note, - suggestedParams, - toAddress, -}: IOptions): Transaction { - let encodedNote: Uint8Array | undefined; - let encoder: TextEncoder; - - if (note) { - encoder = new TextEncoder(); - encodedNote = encoder.encode(note); - } - - // assets with an id of 0, are native currency, so we use a payment transaction - if (asset.id === '0') { - return makePaymentTxnWithSuggestedParams( - fromAddress, - toAddress, - BigInt(amount), - undefined, - encodedNote, - suggestedParams - ); - } - - return makeAssetTransferTxnWithSuggestedParams( - fromAddress, - toAddress, - undefined, - undefined, - BigInt(amount), - encodedNote, - parseInt(asset.id), - suggestedParams - ); -} diff --git a/src/extension/features/send-assets/utils/getInitialState.ts b/src/extension/features/send-assets/utils/getInitialState.ts index b4b9791a..d38a32fa 100644 --- a/src/extension/features/send-assets/utils/getInitialState.ts +++ b/src/extension/features/send-assets/utils/getInitialState.ts @@ -5,11 +5,9 @@ export default function getInitialState(): ISendAssetsState { return { amountInStandardUnits: '0', confirming: false, - error: null, fromAddress: null, note: null, selectedAsset: null, toAddress: null, - transactionId: null, }; } diff --git a/src/extension/features/send-assets/utils/index.ts b/src/extension/features/send-assets/utils/index.ts index 89e3a017..d79b6188 100644 --- a/src/extension/features/send-assets/utils/index.ts +++ b/src/extension/features/send-assets/utils/index.ts @@ -1,2 +1,3 @@ -export { default as createSendAssetTransaction } from './createSendAssetTransaction'; export { default as getInitialState } from './getInitialState'; +export { default as sendArc200AssetTransferTransaction } from './sendArc200AssetTransferTransaction'; +export { default as sendStandardAssetTransferTransaction } from './sendStandardAssetTransferTransaction'; diff --git a/src/extension/features/send-assets/utils/sendArc200AssetTransferTransaction.ts b/src/extension/features/send-assets/utils/sendArc200AssetTransferTransaction.ts new file mode 100644 index 00000000..00922cc0 --- /dev/null +++ b/src/extension/features/send-assets/utils/sendArc200AssetTransferTransaction.ts @@ -0,0 +1,67 @@ +import { Algodv2, Indexer } from 'algosdk'; +import Arc200Contract from 'arc200js'; + +// errors +import { FailedToSendTransactionError } from '@extension/errors'; + +// types +import { IBaseOptions } from '@common/types'; +import { IArc200Asset } from '@extension/types'; + +interface IOptions extends IBaseOptions { + algodClient: Algodv2; + amount: string; + asset: IArc200Asset; + fromAddress: string; + indexerClient: Indexer; + note: string | null; + privateKey: Uint8Array; + toAddress: string; +} + +interface IResult { + success: boolean; + txId?: string; + txns?: string[]; +} + +/** + * Convenience function that calls the ARC-200 contract to transfer the tokens. + * @param {IOptions} options - the fields needed to create a transaction + * @returns {IResult} the result of the transaction. + */ +export default async function sendArc200AssetTransferTransaction({ + algodClient, + amount, + asset, + fromAddress, + indexerClient, + privateKey, + toAddress, +}: IOptions): Promise { + const contract: Arc200Contract = new Arc200Contract( + parseInt(asset.id), + algodClient, + indexerClient, + { + acc: { + addr: fromAddress, + sk: privateKey, + }, + } + ); + const result: IResult = await contract.arc200_transfer( + toAddress, + BigInt(amount), + false, + true + ); + + if (result.success && result.txId) { + return result.txId; + } + + throw new FailedToSendTransactionError( + 'arc200 transaction was not successful' + ); +} diff --git a/src/extension/features/send-assets/utils/sendStandardAssetTransferTransaction.ts b/src/extension/features/send-assets/utils/sendStandardAssetTransferTransaction.ts new file mode 100644 index 00000000..99ca7586 --- /dev/null +++ b/src/extension/features/send-assets/utils/sendStandardAssetTransferTransaction.ts @@ -0,0 +1,111 @@ +import { + Algodv2, + makePaymentTxnWithSuggestedParams, + makeAssetTransferTxnWithSuggestedParams, + SuggestedParams, + Transaction, + IntDecoding, + waitForConfirmation, +} from 'algosdk'; + +// types +import { IBaseOptions } from '@common/types'; +import { + IAlgorandPendingTransactionResponse, + IStandardAsset, +} from '@extension/types'; + +interface IOptions extends IBaseOptions { + algodClient: Algodv2; + amount: string; + asset: IStandardAsset; + fromAddress: string; + note: string | null; + privateKey: Uint8Array; + suggestedParams: SuggestedParams; + toAddress: string; +} + +/** + * Convenience function that creates a payment transaction or an asset transfer transaction based on the asset ID. + * If the asset ID is 0, we can assume this the native currency, so we use a payment transaction. + * @param {IOptions} options - the fields needed to create a transaction + * @returns {Transaction} an Algorand transaction ready to be signed. + */ +export default async function sendStandardAssetTransferTransaction({ + algodClient, + amount, + asset, + fromAddress, + logger, + note, + privateKey, + suggestedParams, + toAddress, +}: IOptions): Promise { + let encodedNote: Uint8Array | undefined; + let encoder: TextEncoder; + let sentRawTransaction: { txId: string }; + let signedTransactionData: Uint8Array; + let transactionResponse: IAlgorandPendingTransactionResponse; + let unsignedTransaction: Transaction; + + if (note) { + encoder = new TextEncoder(); + encodedNote = encoder.encode(note); + } + + // assets with an id of 0, are native currency, so we use a payment transaction + if (asset.id === '0') { + unsignedTransaction = makePaymentTxnWithSuggestedParams( + fromAddress, + toAddress, + BigInt(amount), + undefined, + encodedNote, + suggestedParams + ); + } else { + unsignedTransaction = makeAssetTransferTxnWithSuggestedParams( + fromAddress, + toAddress, + undefined, + undefined, + BigInt(amount), + encodedNote, + parseInt(asset.id), + suggestedParams + ); + } + + signedTransactionData = unsignedTransaction.signTxn(privateKey); + + logger && + logger.debug( + `${sendStandardAssetTransferTransaction.name}: sending asset "${asset.type}" transfer transaction to network` + ); + + sentRawTransaction = await algodClient + .sendRawTransaction(signedTransactionData) + .setIntDecoding(IntDecoding.BIGINT) + .do(); + + logger && + logger.debug( + `${sendStandardAssetTransferTransaction.name}: transaction "${sentRawTransaction.txId}" sent to the network, confirming` + ); + + transactionResponse = (await waitForConfirmation( + algodClient, + sentRawTransaction.txId, + 4 + )) as IAlgorandPendingTransactionResponse; + + logger && + logger.debug( + `${sendStandardAssetTransferTransaction.name}: transaction "${sentRawTransaction.txId}" confirmed in round "${transactionResponse['confirmed-round']}"` + ); + + // on success, return the transaction id + return sentRawTransaction.txId; +} diff --git a/src/extension/modals/SendAssetModal/SendAmountInput.tsx b/src/extension/modals/SendAssetModal/SendAmountInput.tsx index 169f1a20..ef7339b3 100644 --- a/src/extension/modals/SendAssetModal/SendAmountInput.tsx +++ b/src/extension/modals/SendAssetModal/SendAmountInput.tsx @@ -18,6 +18,9 @@ import { IoInformationCircleOutline } from 'react-icons/io5'; // constants import { DEFAULT_GAP } from '@extension/constants'; +// enums +import { AssetTypeEnum } from '@extension/enums'; + // hooks import useButtonHoverBackgroundColor from '@extension/hooks/useButtonHoverBackgroundColor'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; @@ -34,6 +37,7 @@ import { IAccount, IStandardAsset, INetworkWithTransactionParams, + IArc200Asset, } from '@extension/types'; // utils @@ -45,7 +49,7 @@ interface IProps { network: INetworkWithTransactionParams; maximumTransactionAmount: string; onValueChange: (value: string) => void; - selectedAsset: IStandardAsset; + selectedAsset: IArc200Asset | IStandardAsset; value: string | null; } @@ -110,7 +114,21 @@ const SendAmountInput: FC = ({ onValueChange(valueAsString || '0'); // renders const renderMaximumTransactionAmountLabel = () => { - const maximumTransactionAmountLabel: ReactElement = ( + let symbol: string = ''; + let maximumTransactionAmountLabel: ReactElement; + + switch (selectedAsset.type) { + case AssetTypeEnum.Arc200: + symbol = selectedAsset.symbol; + break; + case AssetTypeEnum.Standard: + symbol = selectedAsset.unitName || ''; + break; + default: + break; + } + + maximumTransactionAmountLabel = ( = ({ {`${t('labels.max')}: ${formatCurrencyUnit( maximumTransactionAmountInStandardUnit, selectedAsset.decimals - )} ${selectedAsset.unitName}`} + )} ${symbol}`} diff --git a/src/extension/modals/SendAssetModal/SendAssetModal.tsx b/src/extension/modals/SendAssetModal/SendAssetModal.tsx index 93404ba0..764e499d 100644 --- a/src/extension/modals/SendAssetModal/SendAssetModal.tsx +++ b/src/extension/modals/SendAssetModal/SendAssetModal.tsx @@ -6,7 +6,6 @@ import { ModalContent, ModalFooter, ModalHeader, - Spinner, Text, Textarea, VStack, @@ -27,6 +26,7 @@ import PasswordInput, { usePassword, } from '@extension/components/PasswordInput'; import SendAmountInput from './SendAmountInput'; +import SendAssetModalConfirmingContent from './SendAssetModalConfirmingContent'; import SendAssetModalContentSkeleton from './SendAssetModalContentSkeleton'; import SendAssetModalSummaryContent from './SendAssetModalSummaryContent'; @@ -34,22 +34,18 @@ import SendAssetModalSummaryContent from './SendAssetModalSummaryContent'; import { DEFAULT_GAP } from '@extension/constants'; // enums -import { ErrorCodeEnum } from '@extension/enums'; - -// errors -import { BaseExtensionError } from '@extension/errors'; +import { AssetTypeEnum, ErrorCodeEnum } from '@extension/enums'; // features import { updateAccountsThunk } from '@extension/features/accounts'; import { create as createNotification } from '@extension/features/notifications'; import { + reset as resetSendAssets, setAmount, - setError, setFromAddress, setNote, setSelectedAsset, setToAddress, - reset as resetSendAssets, submitTransactionThunk, } from '@extension/features/send-assets'; @@ -60,14 +56,13 @@ import usePrimaryColor from '@extension/hooks/usePrimaryColor'; // selectors import { useSelectAccounts, + useSelectArc200AssetsBySelectedNetwork, useSelectSelectedNetwork, useSelectSendingAssetAmountInStandardUnits, useSelectSendingAssetConfirming, - useSelectSendingAssetError, useSelectSendingAssetFromAccount, useSelectSendingAssetNote, useSelectSendingAssetSelectedAsset, - useSelectSendingAssetTransactionId, useSelectStandardAssetsBySelectedNetwork, } from '@extension/selectors'; @@ -81,8 +76,9 @@ import { theme } from '@extension/theme'; import { IAccount, IAppThunkDispatch, - IStandardAsset, + IArc200Asset, INetworkWithTransactionParams, + IStandardAsset, } from '@extension/types'; // utils @@ -100,18 +96,18 @@ const SendAssetModal: FC = ({ onClose }: IProps) => { const dispatch: IAppThunkDispatch = useDispatch(); // selectors const accounts: IAccount[] = useSelectAccounts(); + const arc200Assets: IArc200Asset[] = useSelectArc200AssetsBySelectedNetwork(); const amountInStandardUnits: string = useSelectSendingAssetAmountInStandardUnits(); - const assets: IStandardAsset[] = useSelectStandardAssetsBySelectedNetwork(); + const standardAssets: 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: IStandardAsset | null = + const selectedAsset: IArc200Asset | IStandardAsset | null = useSelectSendingAssetSelectedAsset(); - const transactionId: string | null = useSelectSendingAssetTransactionId(); // hooks const { error: toAddressError, @@ -136,12 +132,34 @@ const SendAssetModal: FC = ({ onClose }: IProps) => { useState('0'); const [showSummary, setShowSummary] = useState(false); // misc + const allAssets: (IArc200Asset | IStandardAsset)[] = [ + ...arc200Assets, + ...standardAssets, + ] + .sort((a, b) => { + const aName: string = a.name?.toUpperCase() || ''; + const bName: string = b.name?.toUpperCase() || ''; + + return aName < bName ? -1 : aName > bName ? 1 : 0; + }) // sort each alphabetically by name + .sort((a, b) => (a.verified === b.verified ? 0 : a.verified ? -1 : 1)); // then sort to bring the verified to the front const isOpen: boolean = !!selectedAsset; // handlers const handleAmountChange = (value: string) => dispatch(setAmount(value)); - const handleAssetChange = (value: IStandardAsset) => + const handleAssetChange = (value: IArc200Asset | IStandardAsset) => dispatch(setSelectedAsset(value)); - const handleCancelClick = () => onClose(); + const handleCancelClick = () => handleClose(); + const handleClose = () => { + // reset modal store - should close modal + dispatch(resetSendAssets()); + + // reset modal input + setShowSummary(false); + resetToAddress(); + resetPassword(); + + onClose(); + }; const handleFromAccountChange = (account: IAccount) => dispatch( setFromAddress( @@ -164,9 +182,73 @@ const SendAssetModal: FC = ({ onClose }: IProps) => { setShowSummary(false); }; const handleSendClick = async () => { - if (!validatePassword()) { - dispatch(setError(null)); - dispatch(submitTransactionThunk(password)); + let transactionId: string; + let toAccount: IAccount | null; + + if (validatePassword() || !fromAccount || !network) { + return; + } + + try { + transactionId = await dispatch(submitTransactionThunk(password)).unwrap(); + toAccount = + accounts.find( + (value) => + AccountService.convertPublicKeyToAlgorandAddress( + value.publicKey + ) === toAddress + ) || null; + + // send a success transaction + dispatch( + createNotification({ + description: t('captions.transactionSendSuccessful', { + transactionId: ellipseAddress(transactionId, { end: 4, start: 4 }), + }), + title: t('headings.transactionSuccessful'), + type: 'success', + }) + ); + + // force update the account information as we spent fees and refresh all the new transactions + dispatch( + updateAccountsThunk({ + accountIds: toAccount + ? [fromAccount.id, toAccount.id] + : [fromAccount.id], + forceInformationUpdate: true, + refreshTransactions: true, + }) + ); + + // clean up + handleClose(); + } catch (error) { + switch (error.code) { + case ErrorCodeEnum.InvalidPasswordError: + setPasswordError(t('errors.inputs.invalidPassword')); + + break; + 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; + } } }; const handleToAddressChange = (event: ChangeEvent) => { @@ -178,32 +260,7 @@ const SendAssetModal: FC = ({ onClose }: IProps) => { // renders const renderContent = () => { if (confirming) { - return ( - - - - - {t('captions.confirmingTransaction')} - - - ); + return ; } if (fromAccount && network && selectedAsset) { @@ -246,7 +303,7 @@ const SendAssetModal: FC = ({ onClose }: IProps) => { = ({ onClose }: IProps) => { ); }; + const renderHeader = () => { + switch (selectedAsset?.type) { + case AssetTypeEnum.Arc200: + return ( + + {t('headings.sendAsset', { + asset: selectedAsset.symbol, + })} + + ); + case AssetTypeEnum.Standard: + return ( + + {t('headings.sendAsset', { + asset: selectedAsset?.unitName || 'Asset', + })} + + ); + default: + return ( + + {t('headings.sendAsset', { + asset: 'Asset', + })} + + ); + } + }; useEffect(() => { let newMaximumTransactionAmount: BigNumber; @@ -371,7 +456,7 @@ const SendAssetModal: FC = ({ onClose }: IProps) => { if (fromAccount && network && selectedAsset) { newMaximumTransactionAmount = calculateMaxTransactionAmount({ account: fromAccount, - assetId: selectedAsset.id, // native currency should have an asset id of 0 + asset: selectedAsset, network, }); @@ -390,69 +475,6 @@ const SendAssetModal: FC = ({ onClose }: IProps) => { setMaximumTransactionAmount('0'); }, [fromAccount, network, selectedAsset]); - useEffect(() => { - if (transactionId) { - // send a success transaction - dispatch( - createNotification({ - description: t('captions.transactionSendSuccessful', { - transactionId: ellipseAddress(transactionId), - }), - title: t('headings.transactionSuccessful'), - type: 'success', - }) - ); - - // refresh the account transactions - if (fromAccount) { - // force update the account information as we spent fees and refresh all the new transactions - dispatch( - updateAccountsThunk({ - accountIds: [fromAccount.id], - forceInformationUpdate: true, - refreshTransactions: true, - }) - ); - } - - // reset modal store - should close modal - dispatch(resetSendAssets()); - - // reset modal input - setShowSummary(false); - resetToAddress(); - resetPassword(); - } - }, [transactionId]); - useEffect(() => { - if (error) { - switch (error.code) { - case ErrorCodeEnum.InvalidPasswordError: - setPasswordError(t('errors.inputs.invalidPassword')); - - break; - 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 ( = ({ onClose }: IProps) => { borderBottomRadius={0} > - - {t('headings.sendAsset', { - asset: selectedAsset?.unitName || 'Asset', - })} - + {renderHeader()} diff --git a/src/extension/modals/SendAssetModal/SendAssetModalConfirmingContent.tsx b/src/extension/modals/SendAssetModal/SendAssetModalConfirmingContent.tsx new file mode 100644 index 00000000..3d2798c6 --- /dev/null +++ b/src/extension/modals/SendAssetModal/SendAssetModalConfirmingContent.tsx @@ -0,0 +1,41 @@ +import { Spinner, Text, VStack } from '@chakra-ui/react'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; + +// constants +import { DEFAULT_GAP } from '@extension/constants'; + +// hooks +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import usePrimaryColor from '@extension/hooks/usePrimaryColor'; + +const SendAssetModalConfirmingContent: FC = () => { + const { t } = useTranslation(); + // hooks + const defaultTextColor: string = useDefaultTextColor(); + const primaryColor: string = usePrimaryColor(); + + return ( + + + + + {t('captions.confirmingTransaction')} + + + ); +}; + +export default SendAssetModalConfirmingContent; diff --git a/src/extension/modals/SendAssetModal/SendAssetModalSummaryContent.tsx b/src/extension/modals/SendAssetModal/SendAssetModalSummaryContent.tsx index 43d2addd..db7e0142 100644 --- a/src/extension/modals/SendAssetModal/SendAssetModalSummaryContent.tsx +++ b/src/extension/modals/SendAssetModal/SendAssetModalSummaryContent.tsx @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'; // components import AddressDisplay from '@extension/components/AddressDisplay'; import AssetAvatar from '@extension/components/AssetAvatar'; +import AssetBadge from '@extension/components/AssetBadge'; import AssetDisplay from '@extension/components/AssetDisplay'; import AssetIcon from '@extension/components/AssetIcon'; import SendAssetSummaryItem from './SendAssetSummaryItem'; @@ -13,6 +14,9 @@ import SendAssetSummaryItem from './SendAssetSummaryItem'; // constants import { DEFAULT_GAP } from '@extension/constants'; +// enums +import { AssetTypeEnum } from '@extension/enums'; + // hooks import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; import useSubTextColor from '@extension/hooks/useSubTextColor'; @@ -23,8 +27,9 @@ import { AccountService } from '@extension/services'; // types import { IAccount, - IStandardAsset, + IArc200Asset, INetworkWithTransactionParams, + IStandardAsset, } from '@extension/types'; // utils @@ -33,7 +38,7 @@ import { convertToAtomicUnit } from '@common/utils'; interface IProps { amountInStandardUnits: string; - asset: IStandardAsset; + asset: IArc200Asset | IStandardAsset; fromAccount: IAccount; network: INetworkWithTransactionParams; note: string | null; @@ -52,18 +57,39 @@ const SendAssetModalSummaryContent: FC = ({ // hooks const primaryButtonTextColor: string = usePrimaryButtonTextColor(); const subTextColor: string = useSubTextColor(); - - return ( - - {/*amount/asset*/} - { + switch (asset.type) { + case AssetTypeEnum.Arc200: + return ( + + } + size="2xs" + /> + } + unit={asset.symbol} + /> + ); + case AssetTypeEnum.Standard: + return ( = ({ } unit={asset.unitName || undefined} /> - } + ); + default: + return null; + } + }; + + return ( + + {/*amount/asset*/} + ('labels.amount')} /> @@ -133,26 +175,35 @@ const SendAssetModalSummaryContent: FC = ({ label={t('labels.to')} /> - {/*fee*/} + {/*type*/} - } - label={t('labels.fee')} + item={} + label={t('labels.type')} /> + {/*fee*/} + {asset.type === AssetTypeEnum.Standard && ( + + } + label={t('labels.fee')} + /> + )} + {/*note*/} {note && note.length > 0 && ( { const { t } = useTranslation(); @@ -78,7 +83,7 @@ const AssetPage: FC = () => { accountInformation, asset, assetHolding, - standardUnitAmount, + amountInStandardUnits, } = useAssetPage({ address: address || null, assetId: assetId || null, @@ -156,7 +161,7 @@ const AssetPage: FC = () => { @@ -166,7 +171,7 @@ const AssetPage: FC = () => { spacing={1} w="full" > - {/*asset icon*/} + {/*icon*/} { size="md" /> - {/*asset name*/} + + {/*amount*/} + + + {formatCurrencyUnit(amountInStandardUnits, asset.decimals)} + + + + {/*symbol/unit*/} + {asset.type === AssetTypeEnum.Arc200 && ( + + {asset.symbol} + + )} + {asset.type === AssetTypeEnum.Standard && asset.unitName && ( + + {asset.unitName} + + )} + + + {/*name*/} {asset.name && ( { spacing={2} w="full" > - {/*asset unit name*/} - {asset.unitName && ( - <> - - {asset.unitName} - - - | - - - )} + {/*type*/} + + + + | + - {/*asset id*/} + {/*id*/} { tooltipLabel={t('captions.openOn', { name: explorer.canonicalName, })} - url={`${explorer.baseUrl}${explorer.assetPath}/${asset.id}`} + url={`${explorer.baseUrl}${ + asset.type === AssetTypeEnum.Standard + ? explorer.assetPath + : explorer.applicationPath + }/${asset.id}`} /> )} - - {/*amount*/} - - - {formatCurrencyUnit(standardUnitAmount, asset.decimals)} - - {/*send/receive buttons*/} diff --git a/src/extension/pages/AssetPage/hooks/useAssetPage/types/IUseAssetPageState.ts b/src/extension/pages/AssetPage/hooks/useAssetPage/types/IUseAssetPageState.ts index 50f1ae04..7ebd8bc8 100644 --- a/src/extension/pages/AssetPage/hooks/useAssetPage/types/IUseAssetPageState.ts +++ b/src/extension/pages/AssetPage/hooks/useAssetPage/types/IUseAssetPageState.ts @@ -4,6 +4,8 @@ import BigNumber from 'bignumber.js'; import { IAccount, IAccountInformation, + IArc200Asset, + IArc200AssetHolding, IStandardAsset, IStandardAssetHolding, } from '@extension/types'; @@ -11,9 +13,9 @@ import { interface IUseAssetPageState { account: IAccount | null; accountInformation: IAccountInformation | null; - asset: IStandardAsset | null; - assetHolding: IStandardAssetHolding | null; - standardUnitAmount: BigNumber; + amountInStandardUnits: BigNumber; + asset: IArc200Asset | IStandardAsset | null; + assetHolding: IArc200AssetHolding | IStandardAssetHolding | null; } export default IUseAssetPageState; diff --git a/src/extension/pages/AssetPage/hooks/useAssetPage/useAssetPage.ts b/src/extension/pages/AssetPage/hooks/useAssetPage/useAssetPage.ts index 88fffda1..d3a6e1ab 100644 --- a/src/extension/pages/AssetPage/hooks/useAssetPage/useAssetPage.ts +++ b/src/extension/pages/AssetPage/hooks/useAssetPage/useAssetPage.ts @@ -1,8 +1,13 @@ +import BigNumber from 'bignumber.js'; import { useEffect, useState } from 'react'; +// enums +import { AssetTypeEnum } from '@extension/enums'; + // selectors import { useSelectAccounts, + useSelectArc200AssetsBySelectedNetwork, useSelectSelectedNetwork, useSelectStandardAssetsBySelectedNetwork, } from '@extension/selectors'; @@ -14,12 +19,15 @@ import { AccountService } from '@extension/services'; import { IAccount, IAccountInformation, + IArc200Asset, + IArc200AssetHolding, IStandardAsset, IStandardAssetHolding, INetwork, } from '@extension/types'; import { IUseAssetPageOptions, IUseAssetPageState } from './types'; -import BigNumber from 'bignumber.js'; + +// utils import { convertToStandardUnit } from '@common/utils'; export default function useAssetPage({ @@ -30,15 +38,20 @@ export default function useAssetPage({ // selectors const accounts: IAccount[] = useSelectAccounts(); const selectedNetwork: INetwork | null = useSelectSelectedNetwork(); - const assets: IStandardAsset[] = useSelectStandardAssetsBySelectedNetwork(); + const arc200Assets: IArc200Asset[] = useSelectArc200AssetsBySelectedNetwork(); + const standardAssets: IStandardAsset[] = + useSelectStandardAssetsBySelectedNetwork(); // state const [account, setAccount] = useState(null); const [accountInformation, setAccountInformation] = useState(null); - const [asset, setAsset] = useState(null); - const [assetHolding, setAssetHolding] = - useState(null); - const [standardUnitAmount, setStandardUnitAmount] = useState( + const [asset, setAsset] = useState( + null + ); + const [assetHolding, setAssetHolding] = useState< + IArc200AssetHolding | IStandardAssetHolding | null + >(null); + const [amountInStandardUnits, setAmountInStandardUnits] = useState( new BigNumber('0') ); @@ -65,10 +78,18 @@ export default function useAssetPage({ }, [address, accounts]); // 1b. when we have the asset id and the assets, get the asset useEffect(() => { - let selectedAsset: IStandardAsset | null; + let selectedAsset: IArc200Asset | IStandardAsset | null; - if (assetId && assets.length > 0) { - selectedAsset = assets.find((value) => value.id === assetId) || null; + if (assetId) { + // first, find amongst the arc200 assets + selectedAsset = + arc200Assets.find((value) => value.id === assetId) || null; + + // if there is not an arc200 asset, it maybe a standard asset + if (!selectedAsset) { + selectedAsset = + standardAssets.find((value) => value.id === assetId) || null; + } // if there is no asset, we have an error if (!selectedAsset) { @@ -77,7 +98,7 @@ export default function useAssetPage({ setAsset(selectedAsset); } - }, [assetId, assets]); + }, [arc200Assets, assetId, standardAssets]); // 2. when the account has been found and we have the selected network, get the account information useEffect(() => { if (account && selectedNetwork) { @@ -91,26 +112,42 @@ export default function useAssetPage({ }, [account, selectedNetwork]); // 3. when we have the account information, get the asset holding useEffect(() => { - let selectedAssetHolding: IStandardAssetHolding | null; + let selectedAssetHolding: + | IArc200AssetHolding + | IStandardAssetHolding + | null; - if (accountInformation) { - selectedAssetHolding = - accountInformation.standardAssetHoldings.find( - (value) => value.id === assetId - ) || null; + if (asset && accountInformation) { + switch (asset.type) { + case AssetTypeEnum.Arc200: + selectedAssetHolding = + accountInformation.arc200AssetHoldings.find( + (value) => value.id === assetId + ) || null; + break; + case AssetTypeEnum.Standard: + selectedAssetHolding = + accountInformation.standardAssetHoldings.find( + (value) => value.id === assetId + ) || null; + break; + default: + selectedAssetHolding = null; + break; + } - // if teh account does not have the asset holding, we have an error + // if the account does not have the asset holding, we have an error if (!selectedAssetHolding) { return onError(); } setAssetHolding(selectedAssetHolding); } - }, [accountInformation]); + }, [asset, accountInformation]); // 4. when we have the asset and asset holding, update the standard amount useEffect(() => { if (asset && assetHolding) { - setStandardUnitAmount( + setAmountInStandardUnits( convertToStandardUnit( new BigNumber(assetHolding.amount), asset.decimals @@ -122,8 +159,8 @@ export default function useAssetPage({ return { account, accountInformation, + amountInStandardUnits, asset, assetHolding, - standardUnitAmount, }; } diff --git a/src/extension/selectors/index.ts b/src/extension/selectors/index.ts index 79174963..27943703 100644 --- a/src/extension/selectors/index.ts +++ b/src/extension/selectors/index.ts @@ -33,12 +33,10 @@ export { default as useSelectSavingSettings } from './useSelectSavingSettings'; export { default as useSelectSelectedNetwork } from './useSelectSelectedNetwork'; export { default as useSelectSendingAssetAmountInStandardUnits } from './useSelectSendingAssetAmountInStandardUnits'; export { default as useSelectSendingAssetConfirming } from './useSelectSendingAssetConfirming'; -export { default as useSelectSendingAssetError } from './useSelectSendingAssetError'; export { default as useSelectSendingAssetFromAccount } from './useSelectSendingAssetFromAccount'; export { default as useSelectSendingAssetNote } from './useSelectSendingAssetNote'; export { default as useSelectSendingAssetSelectedAsset } from './useSelectSendingAssetSelectedAsset'; export { default as useSelectSendingAssetToAddress } from './useSelectSendingAssetToAddress'; -export { default as useSelectSendingAssetTransactionId } from './useSelectSendingAssetTransactionId'; export { default as useSelectSessions } from './useSelectSessions'; export { default as useSelectSettings } from './useSelectSettings'; export { default as useSelectSideBar } from './useSelectSideBar'; diff --git a/src/extension/selectors/useSelectSendingAssetError.ts b/src/extension/selectors/useSelectSendingAssetError.ts deleted file mode 100644 index ed57538d..00000000 --- a/src/extension/selectors/useSelectSendingAssetError.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useSelector } from 'react-redux'; - -// errors -import { BaseExtensionError } from '@extension/errors'; - -// types -import { IMainRootState } from '@extension/types'; - -export default function useSelectSendingAssetError(): BaseExtensionError | null { - return useSelector( - (state) => state.sendAssets.error - ); -} diff --git a/src/extension/selectors/useSelectSendingAssetSelectedAsset.ts b/src/extension/selectors/useSelectSendingAssetSelectedAsset.ts index 9c2836b4..ba4f90b3 100644 --- a/src/extension/selectors/useSelectSendingAssetSelectedAsset.ts +++ b/src/extension/selectors/useSelectSendingAssetSelectedAsset.ts @@ -1,10 +1,13 @@ import { useSelector } from 'react-redux'; // types -import { IStandardAsset, IMainRootState } from '@extension/types'; +import { IStandardAsset, IMainRootState, IArc200Asset } from '@extension/types'; -export default function useSelectSendingAssetSelectedAsset(): IStandardAsset | null { - return useSelector( +export default function useSelectSendingAssetSelectedAsset(): + | IArc200Asset + | IStandardAsset + | null { + return useSelector( (state) => state.sendAssets.selectedAsset ); } diff --git a/src/extension/selectors/useSelectSendingAssetTransactionId.ts b/src/extension/selectors/useSelectSendingAssetTransactionId.ts deleted file mode 100644 index 9c941f79..00000000 --- a/src/extension/selectors/useSelectSendingAssetTransactionId.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useSelector } from 'react-redux'; - -// types -import { IMainRootState } from '@extension/types'; - -export default function useSelectSendingAssetTransactionId(): string | null { - return useSelector( - (state) => state.sendAssets.transactionId - ); -} diff --git a/src/extension/services/AccountService.ts b/src/extension/services/AccountService.ts index d0dd644f..0f2f423b 100644 --- a/src/extension/services/AccountService.ts +++ b/src/extension/services/AccountService.ts @@ -8,6 +8,9 @@ import { networks } from '@extension/config'; // constants import { ACCOUNTS_ITEM_KEY_PREFIX } from '@extension/constants'; +// enums +import { AssetTypeEnum } from '@extension/enums'; + // services import StorageManager from './StorageManager'; @@ -221,7 +224,20 @@ export default class AccountService { ...acc, [encodedGenesisHash]: { ...AccountService.initializeDefaultAccountInformation(), - ...accountInformation, + ...(accountInformation && { + ...accountInformation, + arc200AssetHoldings: accountInformation.arc200AssetHoldings.map( + (value) => ({ + ...value, + type: AssetTypeEnum.Arc200, + }) + ), + standardAssetHoldings: + accountInformation.standardAssetHoldings.map((value) => ({ + ...value, + type: AssetTypeEnum.Standard, + })), + }), }, }; }, diff --git a/src/extension/types/IArc200AssetHolding.ts b/src/extension/types/IArc200AssetHolding.ts index 30c5143e..6db263f6 100644 --- a/src/extension/types/IArc200AssetHolding.ts +++ b/src/extension/types/IArc200AssetHolding.ts @@ -1,10 +1,11 @@ -/** - * @property {string} amount - the amount of the arc200 asset. - * @property {string} id - the arc200 app ID. - */ -interface IArc200AssetHolding { - amount: string; - id: string; +// enums +import { AssetTypeEnum } from '@extension/enums'; + +// types +import { IBaseAssetHolding } from '@extension/types'; + +interface IArc200AssetHolding extends IBaseAssetHolding { + type: AssetTypeEnum.Arc200; } export default IArc200AssetHolding; diff --git a/src/extension/types/IBaseAssetHolding.ts b/src/extension/types/IBaseAssetHolding.ts new file mode 100644 index 00000000..1e474913 --- /dev/null +++ b/src/extension/types/IBaseAssetHolding.ts @@ -0,0 +1,14 @@ +import { AssetTypeEnum } from '@extension/enums'; + +/** + * @property {string} amount - the amount, in atomic units, the asset. + * @property {string} id - the asset ID. + * @property {AssetTypeEnum} type - the type of asset. + */ +interface IBaseAssetHolding { + amount: string; + id: string; + type: AssetTypeEnum; +} + +export default IBaseAssetHolding; diff --git a/src/extension/types/IStandardAssetHolding.ts b/src/extension/types/IStandardAssetHolding.ts index 71804c65..bcd7284b 100644 --- a/src/extension/types/IStandardAssetHolding.ts +++ b/src/extension/types/IStandardAssetHolding.ts @@ -1,12 +1,15 @@ +// enums +import { AssetTypeEnum } from '@extension/enums'; + +// types +import { IBaseAssetHolding } from '@extension/types'; + /** - * @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; +interface IStandardAssetHolding extends IBaseAssetHolding { isFrozen: boolean; + type: AssetTypeEnum.Standard; } export default IStandardAssetHolding; diff --git a/src/extension/types/index.ts b/src/extension/types/index.ts index f55a96ae..5d488c9e 100644 --- a/src/extension/types/index.ts +++ b/src/extension/types/index.ts @@ -45,6 +45,7 @@ export type { default as IBackgroundRootState } from './IBackgroundRootState'; export type { default as IBaseActionMeta } from './IBaseActionMeta'; export type { default as IBaseAsset } from './IBaseAsset'; export type { default as IBaseAssetFreezeTransaction } from './IBaseAssetFreezeTransaction'; +export type { default as IBaseAssetHolding } from './IBaseAssetHolding'; export type { default as IBaseAsyncThunkConfig } from './IBaseAsyncThunkConfig'; export type { default as IBaseRequest } from './IBaseRequest'; export type { default as IBaseRootState } from './IBaseRootState'; diff --git a/src/extension/utils/calculateMaxTransactionAmount.ts b/src/extension/utils/calculateMaxTransactionAmount.ts index b92cfeaa..7dc2f536 100644 --- a/src/extension/utils/calculateMaxTransactionAmount.ts +++ b/src/extension/utils/calculateMaxTransactionAmount.ts @@ -1,11 +1,17 @@ import BigNumber from 'bignumber.js'; +// enums +import { AssetTypeEnum } from '@extension/enums'; + // types import { IAccount, IAccountInformation, IStandardAssetHolding, INetworkWithTransactionParams, + IArc200Asset, + IStandardAsset, + IArc200AssetHolding, } from '@extension/types'; // utils @@ -13,22 +19,24 @@ import convertGenesisHashToHex from './convertGenesisHashToHex'; interface IOptions { account: IAccount; - assetId: string; + asset: IArc200Asset | IStandardAsset; network: INetworkWithTransactionParams; } /** * Convenience function that calculates the maximum transaction amount. - * - If the `assetId` is not zero, then the transaction amount for that asset is calculated. Zero is returned if the + * - For ARC-200 and standard assets, when the asset ID is NOT "0", then the transaction amount for that asset is + * calculated. Zero is returned if the * account does not hold any asset holding for the supplied asset. - * - If the `assetId` is '0', this will be the native currency and is the balance - min balance to keep account open - - * the minimum transaction fee. If the balance is calculated to fall below zero, zero is returned. + * - If the asset is a standard asset with an ID of "0", this will be the native currency and is + * (the balance - min balance to keep account open - the minimum transaction fee). If the balance is calculated falls + * below zero, zero is returned. * @param {IOptions} options - the account, assetId and network. * @returns {BigNumber} the maximum transaction amount for the given asset or the native currency. */ export default function calculateMaxTransactionAmount({ account, - assetId, + asset, network, }: IOptions): BigNumber { const accountInformation: IAccountInformation | null = @@ -36,7 +44,7 @@ export default function calculateMaxTransactionAmount({ convertGenesisHashToHex(network.genesisHash).toUpperCase() ] || null; let amount: BigNumber; - let assetHolding: IStandardAssetHolding | null; + let assetHolding: IArc200AssetHolding | IStandardAssetHolding | null; let balance: BigNumber; let minBalance: BigNumber; let minFee: BigNumber; @@ -45,21 +53,33 @@ export default function calculateMaxTransactionAmount({ return new BigNumber('0'); } - // if the asset id is not 0 it is an asa, use the balance of the asset - if (assetId != '0') { - assetHolding = - accountInformation.standardAssetHoldings.find( - (value) => value.id === assetId - ) || null; + switch (asset.type) { + case AssetTypeEnum.Arc200: + assetHolding = + accountInformation.arc200AssetHoldings.find( + (value) => value.id === asset.id + ) || null; - return new BigNumber(assetHolding?.amount || 0); - } + return new BigNumber(assetHolding?.amount || 0); + case AssetTypeEnum.Standard: + // if the asset id is not 0 it is an asa, use the balance of the asset + if (asset.id !== '0') { + assetHolding = + accountInformation.standardAssetHoldings.find( + (value) => value.id === asset.id + ) || null; - balance = new BigNumber(accountInformation.atomicBalance); - minBalance = new BigNumber(accountInformation.minAtomicBalance); - minFee = new BigNumber(network.minFee); - amount = balance.minus(minBalance).minus(minFee); // balance - min balance to keep account open - the minimum transaction fee = amount + return new BigNumber(assetHolding?.amount || 0); + } - // if the amount falls below zero, just return zero - return amount.lt(new BigNumber(0)) ? new BigNumber(0) : amount; + balance = new BigNumber(accountInformation.atomicBalance); + minBalance = new BigNumber(accountInformation.minAtomicBalance); + minFee = new BigNumber(network.minFee); + amount = balance.minus(minBalance).minus(minFee); // balance - min balance to keep account open - the minimum transaction fee = amount + + // if the amount falls below zero, just return zero + return amount.lt(new BigNumber(0)) ? new BigNumber(0) : amount; + default: + return new BigNumber('0'); + } } diff --git a/src/extension/utils/mapAlgorandAccountInformationToAccount.ts b/src/extension/utils/mapAlgorandAccountInformationToAccount.ts index 1945b812..c785155b 100644 --- a/src/extension/utils/mapAlgorandAccountInformationToAccount.ts +++ b/src/extension/utils/mapAlgorandAccountInformationToAccount.ts @@ -1,5 +1,8 @@ import { BigNumber } from 'bignumber.js'; +// enums +import { AssetTypeEnum } from '@extension/enums'; + // types import { IAccountInformation, @@ -24,6 +27,7 @@ export default function mapAlgorandAccountInformationToAccountInformation( amount: new BigNumber(String(value.amount as bigint)).toString(), id: new BigNumber(String(value['asset-id'] as bigint)).toString(), isFrozen: value['is-frozen'], + type: AssetTypeEnum.Standard, })), updatedAt: updatedAt || new Date().getTime(), };