diff --git a/src/common/utils/createAlgodClient/createAlgodClient.ts b/src/common/utils/createAlgodClient/createAlgodClient.ts index 0b3b301c..d3300658 100644 --- a/src/common/utils/createAlgodClient/createAlgodClient.ts +++ b/src/common/utils/createAlgodClient/createAlgodClient.ts @@ -5,7 +5,7 @@ import type { IBaseOptions } from '@common/types'; import type { INetwork, INode } from '@extension/types'; // utils -import getRandomNode from '../getRandomNode'; +import getRandomItem from '@common/utils/getRandomItem'; /** * Gets a random algod node from the given network. @@ -18,7 +18,7 @@ export default function createAlgodClient( { logger }: IBaseOptions = { logger: undefined } ): Algodv2 { const _functionName: string = 'createAlgodClient'; - const algod: INode = getRandomNode(network.algods); + const algod: INode = getRandomItem(network.algods); logger?.debug( `${_functionName}: selected algod node "${algod.canonicalName}"` diff --git a/src/common/utils/getIndexerClient/getIndexerClient.ts b/src/common/utils/getIndexerClient/getIndexerClient.ts index 2ac455d2..88e73e3f 100644 --- a/src/common/utils/getIndexerClient/getIndexerClient.ts +++ b/src/common/utils/getIndexerClient/getIndexerClient.ts @@ -5,7 +5,7 @@ import { IBaseOptions } from '@common/types'; import { INetwork, INode } from '@extension/types'; // utils -import getRandomNode from '../getRandomNode'; +import getRandomItem from '@common/utils/getRandomItem'; /** * Gets a random indexer node from the given network. @@ -17,7 +17,7 @@ export default function getIndexerClient( network: INetwork, { logger }: IBaseOptions = { logger: undefined } ): Indexer { - const indexer: INode = getRandomNode(network.indexers); + const indexer: INode = getRandomItem(network.indexers); logger && logger.debug( diff --git a/src/common/utils/getRandomItem/getRandomItem.ts b/src/common/utils/getRandomItem/getRandomItem.ts new file mode 100644 index 00000000..b6a5a7fd --- /dev/null +++ b/src/common/utils/getRandomItem/getRandomItem.ts @@ -0,0 +1,8 @@ +/** + * Convenience function that randomly picks an item from a list. + * @param {Item[]} list - a list of items. + * @returns {Item} a random item from the list. + */ +export default function getRandomItem(list: Item[]): Item { + return list[Math.floor(Math.random() * list.length)]; +} diff --git a/src/common/utils/getRandomItem/index.ts b/src/common/utils/getRandomItem/index.ts new file mode 100644 index 00000000..766ab838 --- /dev/null +++ b/src/common/utils/getRandomItem/index.ts @@ -0,0 +1 @@ +export { default } from './getRandomItem'; diff --git a/src/common/utils/getRandomNode/getRandomNode.ts b/src/common/utils/getRandomNode/getRandomNode.ts deleted file mode 100644 index 7b05ff96..00000000 --- a/src/common/utils/getRandomNode/getRandomNode.ts +++ /dev/null @@ -1,11 +0,0 @@ -// 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/getRandomNode/index.ts b/src/common/utils/getRandomNode/index.ts deleted file mode 100644 index 3a4673d0..00000000 --- a/src/common/utils/getRandomNode/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './getRandomNode'; diff --git a/src/extension/apps/background/App.tsx b/src/extension/apps/background/App.tsx index 6eba96dc..6195d4bc 100644 --- a/src/extension/apps/background/App.tsx +++ b/src/extension/apps/background/App.tsx @@ -9,7 +9,7 @@ import Root from './Root'; // features import { reducer as accountsReducer } from '@extension/features/accounts'; -import { reducer as arc200AssetsReducer } from '@extension/features/arc200-assets'; +import { reducer as arc200AssetsReducer } from '@extension/features/arc0200-assets'; import { reducer as eventsReducer } from '@extension/features/events'; import { reducer as messagesReducer } from '@extension/features/messages'; import { reducer as networksReducer } from '@extension/features/networks'; @@ -28,7 +28,7 @@ const App: FC = ({ i18next, initialColorMode }: IAppProps) => { const store: Store = makeStore( combineReducers({ accounts: accountsReducer, - arc200Assets: arc200AssetsReducer, + arc0200Assets: arc200AssetsReducer, events: eventsReducer, messages: messagesReducer, networks: networksReducer, diff --git a/src/extension/apps/main/App.tsx b/src/extension/apps/main/App.tsx index e9136935..1c64ccd1 100644 --- a/src/extension/apps/main/App.tsx +++ b/src/extension/apps/main/App.tsx @@ -18,6 +18,7 @@ import { ACCOUNTS_ROUTE, ADD_ACCOUNT_ROUTE, ASSETS_ROUTE, + NFTS_ROUTE, SETTINGS_ROUTE, PASSWORD_LOCK_ROUTE, TRANSACTIONS_ROUTE, @@ -26,7 +27,8 @@ import { // features import { reducer as accountsReducer } from '@extension/features/accounts'; import { reducer as addAssetReducer } from '@extension/features/add-asset'; -import { reducer as arc200AssetsReducer } from '@extension/features/arc200-assets'; +import { reducer as arc0072AssetsReducer } from '@extension/features/arc0072-assets'; +import { reducer as arc200AssetsReducer } from '@extension/features/arc0200-assets'; import { reducer as eventsReducer } from '@extension/features/events'; import { reducer as messagesReducer } from '@extension/features/messages'; import { reducer as networksReducer } from '@extension/features/networks'; @@ -48,6 +50,7 @@ import { import AccountPage from '@extension/pages/AccountPage'; import AssetPage from '@extension/pages/AssetPage'; import LoadingPage from '@extension/pages/LoadingPage'; +import NFTPage from '@extension/pages/NFTPage'; import PasswordLockPage from '@extension/pages/PasswordLockPage'; import TransactionPage from '@extension/pages/TransactionPage'; @@ -107,6 +110,15 @@ const createRouter = ({ dispatch, getState }: Store) => { }, path: `${ASSETS_ROUTE}/:assetId`, }, + { + element: , + loader: () => { + dispatch(setSideBar(true)); + + return null; + }, + path: `${NFTS_ROUTE}/:appId/:tokenId`, + }, { element: , loader: () => { @@ -164,7 +176,8 @@ const App: FC = ({ i18next, initialColorMode }: IAppProps) => { combineReducers({ accounts: accountsReducer, addAsset: addAssetReducer, - arc200Assets: arc200AssetsReducer, + arc0072Assets: arc0072AssetsReducer, + arc0200Assets: arc200AssetsReducer, events: eventsReducer, messages: messagesReducer, networks: networksReducer, diff --git a/src/extension/apps/main/Root.tsx b/src/extension/apps/main/Root.tsx index 777634d8..80832952 100644 --- a/src/extension/apps/main/Root.tsx +++ b/src/extension/apps/main/Root.tsx @@ -19,7 +19,8 @@ import { fetchAccountsFromStorageThunk, startPollingForAccountsThunk, } from '@extension/features/accounts'; -import { fetchARC0200AssetsFromStorageThunk } from '@extension/features/arc200-assets'; +import { fetchARC0072AssetsFromStorageThunk } from '@extension/features/arc0072-assets'; +import { fetchARC0200AssetsFromStorageThunk } from '@extension/features/arc0200-assets'; import { setEnableRequest, setSignBytesRequest, @@ -99,6 +100,7 @@ const Root: FC = () => { dispatch(fetchSettingsFromStorageThunk()); dispatch(fetchSessionsThunk()); dispatch(fetchStandardAssetsFromStorageThunk()); + dispatch(fetchARC0072AssetsFromStorageThunk()); dispatch(fetchARC0200AssetsFromStorageThunk()); dispatch(initializeWalletConnectThunk()); dispatch(startPollingForAccountsThunk()); diff --git a/src/extension/apps/registration/App.tsx b/src/extension/apps/registration/App.tsx index 393786de..5ef365e3 100644 --- a/src/extension/apps/registration/App.tsx +++ b/src/extension/apps/registration/App.tsx @@ -16,7 +16,7 @@ import { } from '@extension/constants'; // features -import { reducer as arc200AssetsReducer } from '@extension/features/arc200-assets'; +import { reducer as arc200AssetsReducer } from '@extension/features/arc0200-assets'; import { reducer as networksReducer } from '@extension/features/networks'; import { reducer as notificationsReducer } from '@extension/features/notifications'; import { reducer as settingsReducer } from '@extension/features/settings'; @@ -66,7 +66,7 @@ const App: FC = ({ i18next, initialColorMode }: IAppProps) => { const store: Store = makeStore( combineReducers({ - arc200Assets: arc200AssetsReducer, + arc0200Assets: arc200AssetsReducer, networks: networksReducer, notifications: notificationsReducer, settings: settingsReducer, diff --git a/src/extension/apps/registration/Root.tsx b/src/extension/apps/registration/Root.tsx index 21522bce..c55bc577 100644 --- a/src/extension/apps/registration/Root.tsx +++ b/src/extension/apps/registration/Root.tsx @@ -7,7 +7,7 @@ import { Outlet } from 'react-router-dom'; import { BODY_BACKGROUND_COLOR } from '@extension/constants'; // features -import { fetchARC0200AssetsFromStorageThunk } from '@extension/features/arc200-assets'; +import { fetchARC0200AssetsFromStorageThunk } from '@extension/features/arc0200-assets'; import { fetchSettingsFromStorageThunk } from '@extension/features/settings'; // types diff --git a/src/extension/components/AccountNftsTab/AccountNftsTab.tsx b/src/extension/components/AccountNftsTab/AccountNftsTab.tsx deleted file mode 100644 index 5eae5f8e..00000000 --- a/src/extension/components/AccountNftsTab/AccountNftsTab.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Heading, Spacer, TabPanel, VStack } from '@chakra-ui/react'; -import React, { FC } from 'react'; -import { useTranslation } from 'react-i18next'; - -// hooks -import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; - -const AccountNftsTab: FC = () => { - const { t } = useTranslation(); - const defaultTextColor: string = useDefaultTextColor(); - - return ( - - - - - - {t('headings.comingSoon')} - - - - - - ); -}; - -export default AccountNftsTab; diff --git a/src/extension/components/AccountNftsTab/index.ts b/src/extension/components/AccountNftsTab/index.ts deleted file mode 100644 index f8cbdf14..00000000 --- a/src/extension/components/AccountNftsTab/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './AccountNftsTab'; diff --git a/src/extension/components/AssetAvatar/AssetAvatar.tsx b/src/extension/components/AssetAvatar/AssetAvatar.tsx index 88769596..b8840e6f 100644 --- a/src/extension/components/AssetAvatar/AssetAvatar.tsx +++ b/src/extension/components/AssetAvatar/AssetAvatar.tsx @@ -1,5 +1,5 @@ import { Avatar, AvatarBadge, AvatarProps, Icon } from '@chakra-ui/react'; -import React, { FC, ReactElement } from 'react'; +import React, { FC } from 'react'; import { IoCheckmarkOutline } from 'react-icons/io5'; // enums @@ -9,18 +9,9 @@ import { AssetTypeEnum } from '@extension/enums'; import usePrimaryColor from '@extension/hooks/usePrimaryColor'; // types -import { IAssetTypes, INativeCurrency } from '@extension/types'; +import type { IProps } from './types'; -interface IProps extends AvatarProps { - asset: IAssetTypes | INativeCurrency; - fallbackIcon?: ReactElement; -} - -const AssetAvatar: FC = ({ - asset, - fallbackIcon, - ...avatarProps -}: IProps) => { +const AssetAvatar: FC = ({ asset, fallbackIcon, ...avatarProps }) => { // hooks const primaryColor: string = usePrimaryColor(); // misc diff --git a/src/extension/components/AssetAvatar/types/IProps.ts b/src/extension/components/AssetAvatar/types/IProps.ts new file mode 100644 index 00000000..61d9aa9a --- /dev/null +++ b/src/extension/components/AssetAvatar/types/IProps.ts @@ -0,0 +1,12 @@ +import { AvatarProps } from '@chakra-ui/react'; +import { ReactElement } from 'react'; + +// types +import type { IAssetTypes, INativeCurrency } from '@extension/types'; + +interface IProps extends AvatarProps { + asset: IAssetTypes | INativeCurrency; + fallbackIcon?: ReactElement; +} + +export default IProps; diff --git a/src/extension/components/AssetAvatar/types/index.ts b/src/extension/components/AssetAvatar/types/index.ts new file mode 100644 index 00000000..f404deed --- /dev/null +++ b/src/extension/components/AssetAvatar/types/index.ts @@ -0,0 +1 @@ +export type { default as IProps } from './IProps'; diff --git a/src/extension/components/AssetBadge/AssetBadge.tsx b/src/extension/components/AssetBadge/AssetBadge.tsx index a605fdaa..b2c575f4 100644 --- a/src/extension/components/AssetBadge/AssetBadge.tsx +++ b/src/extension/components/AssetBadge/AssetBadge.tsx @@ -1,4 +1,4 @@ -import { ColorMode, HStack, Tag, TagLabel } from '@chakra-ui/react'; +import { ColorMode, Tag, TagLabel } from '@chakra-ui/react'; import React, { FC } from 'react'; // enums @@ -7,16 +7,24 @@ import { AssetTypeEnum } from '@extension/enums'; // selectors import { useSelectColorMode } from '@extension/selectors'; -interface IProps { - size?: string; - type: AssetTypeEnum; -} +// types +import type { IProps } from './types'; const AssetBadge: FC = ({ size = 'sm', type }: IProps) => { // hooks const colorMode: ColorMode = useSelectColorMode(); switch (type) { + case AssetTypeEnum.ARC0072: + return ( + + ARC0072 + + ); case AssetTypeEnum.ARC0200: return ( = ({ size = 'sm', type }: IProps) => { size={size} variant={colorMode === 'dark' ? 'solid' : 'subtle'} > - ARC200 + ARC0200 ); case AssetTypeEnum.Native: diff --git a/src/extension/components/AssetBadge/types/IProps.ts b/src/extension/components/AssetBadge/types/IProps.ts new file mode 100644 index 00000000..5623d76b --- /dev/null +++ b/src/extension/components/AssetBadge/types/IProps.ts @@ -0,0 +1,9 @@ +// enums +import { AssetTypeEnum } from '@extension/enums'; + +interface IProps { + size?: string; + type: AssetTypeEnum; +} + +export default IProps; diff --git a/src/extension/components/AssetBadge/types/index.ts b/src/extension/components/AssetBadge/types/index.ts new file mode 100644 index 00000000..f404deed --- /dev/null +++ b/src/extension/components/AssetBadge/types/index.ts @@ -0,0 +1 @@ +export type { default as IProps } from './IProps'; diff --git a/src/extension/components/AssetsTab/AssetTabLoadingItem.tsx b/src/extension/components/AssetTabLoadingItem/AssetTabLoadingItem.tsx similarity index 99% rename from src/extension/components/AssetsTab/AssetTabLoadingItem.tsx rename to src/extension/components/AssetTabLoadingItem/AssetTabLoadingItem.tsx index d3c055f2..4e4abaa0 100644 --- a/src/extension/components/AssetsTab/AssetTabLoadingItem.tsx +++ b/src/extension/components/AssetTabLoadingItem/AssetTabLoadingItem.tsx @@ -32,11 +32,13 @@ const AssetTabLoadingItem: FC = () => { > + {faker.company.bsBuzz()} + {faker.random.numeric(3)} diff --git a/src/extension/components/AssetTabLoadingItem/index.ts b/src/extension/components/AssetTabLoadingItem/index.ts new file mode 100644 index 00000000..8f72a023 --- /dev/null +++ b/src/extension/components/AssetTabLoadingItem/index.ts @@ -0,0 +1 @@ +export { default } from './AssetTabLoadingItem'; diff --git a/src/extension/components/AssetsTab/AssetTabArc200AssetItem.tsx b/src/extension/components/AssetsTab/AssetTabARC0200AssetItem.tsx similarity index 78% rename from src/extension/components/AssetsTab/AssetTabArc200AssetItem.tsx rename to src/extension/components/AssetsTab/AssetTabARC0200AssetItem.tsx index 66268715..8a8fb80c 100644 --- a/src/extension/components/AssetsTab/AssetTabArc200AssetItem.tsx +++ b/src/extension/components/AssetsTab/AssetTabARC0200AssetItem.tsx @@ -1,12 +1,4 @@ -import { - Button, - ColorMode, - HStack, - Icon, - Text, - Tooltip, - VStack, -} from '@chakra-ui/react'; +import { Button, HStack, Icon, Text, Tooltip, VStack } from '@chakra-ui/react'; import BigNumber from 'bignumber.js'; import React, { FC } from 'react'; import { IoChevronForward } from 'react-icons/io5'; @@ -19,7 +11,6 @@ import AssetIcon from '@extension/components/AssetIcon'; // constants import { - ACCOUNTS_ROUTE, ASSETS_ROUTE, DEFAULT_GAP, TAB_ITEM_HEIGHT, @@ -34,34 +25,24 @@ import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; import useSubTextColor from '@extension/hooks/useSubTextColor'; -// selectors -import { useSelectColorMode } from '@extension/selectors'; - -// services -import AccountService from '@extension/services/AccountService'; - // types -import { IAccount, IARC0200Asset, INetwork } from '@extension/types'; +import type { IARC0200Asset, INetwork } from '@extension/types'; // utils import convertToStandardUnit from '@common/utils/convertToStandardUnit'; import formatCurrencyUnit from '@common/utils/formatCurrencyUnit'; interface IProps { - account: IAccount; amount: string; - arc200Asset: IARC0200Asset; + arc0200Asset: IARC0200Asset; network: INetwork; } -const AssetTabArc200AssetItem: FC = ({ - account, +const AssetTabARC0200AssetItem: FC = ({ amount, - arc200Asset, + arc0200Asset, network, }: IProps) => { - // selectors - const colorMode: ColorMode = useSelectColorMode(); // hooks const buttonHoverBackgroundColor: string = useButtonHoverBackgroundColor(); const defaultTextColor: string = useDefaultTextColor(); @@ -70,13 +51,13 @@ const AssetTabArc200AssetItem: FC = ({ // misc const standardUnitAmount: BigNumber = convertToStandardUnit( new BigNumber(amount), - arc200Asset.decimals + arc0200Asset.decimals ); return ( + + ); +}; + +export default NFTsTabARC0072AssetItem; diff --git a/src/extension/components/NFTsTab/index.ts b/src/extension/components/NFTsTab/index.ts new file mode 100644 index 00000000..a9b17d41 --- /dev/null +++ b/src/extension/components/NFTsTab/index.ts @@ -0,0 +1 @@ +export { default } from './NFTsTab'; diff --git a/src/extension/components/NFTsTab/types/INFTsTabARC0072AssetItemProps.ts b/src/extension/components/NFTsTab/types/INFTsTabARC0072AssetItemProps.ts new file mode 100644 index 00000000..fa72e2f8 --- /dev/null +++ b/src/extension/components/NFTsTab/types/INFTsTabARC0072AssetItemProps.ts @@ -0,0 +1,9 @@ +// types +import type { IARC0072AssetHolding, INetwork } from '@extension/types'; + +interface INFTsTabARC0072AssetItemProps { + arc0072AssetHolding: IARC0072AssetHolding; + network: INetwork; +} + +export default INFTsTabARC0072AssetItemProps; diff --git a/src/extension/components/NFTsTab/types/INFTsTabProps.ts b/src/extension/components/NFTsTab/types/INFTsTabProps.ts new file mode 100644 index 00000000..6c394238 --- /dev/null +++ b/src/extension/components/NFTsTab/types/INFTsTabProps.ts @@ -0,0 +1,8 @@ +// types +import type { IAccount } from '@extension/types'; + +interface INFTsTabProps { + account: IAccount; +} + +export default INFTsTabProps; diff --git a/src/extension/components/NFTsTab/types/index.ts b/src/extension/components/NFTsTab/types/index.ts new file mode 100644 index 00000000..9bc77b3f --- /dev/null +++ b/src/extension/components/NFTsTab/types/index.ts @@ -0,0 +1,2 @@ +export type { default as INFTsTabARC0072AssetItemProps } from './INFTsTabARC0072AssetItemProps'; +export type { default as INFTsTabProps } from './INFTsTabProps'; diff --git a/src/extension/config/networks.ts b/src/extension/config/networks.ts index 304c3cf7..5ad41272 100644 --- a/src/extension/config/networks.ts +++ b/src/extension/config/networks.ts @@ -1,16 +1,19 @@ +// constants +import { + ALGORAND_ICON_URI, + ALGORAND_LISTING_ICON_URI, + VOI_ICON_URI, + VOI_LISTING_ICON_URI, +} from '@extension/constants'; + +// enums import { AssetTypeEnum, NetworkTypeEnum } from '@extension/enums'; // types -import { INetwork } from '@extension/types'; +import type { INetwork } from '@extension/types'; -const algorandIconUri: string = - 'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTIwMCAxMjAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgoJPHBhdGgKCSAgICAgIGQ9Ik0gMy43OTI0ODAxLDExOTYuNzU0NyBDIDcuMzg4OTI2LDExOTAuNzg2OCAxNzIuNTQ4ODcsOTA0LjUzNjYzIDI2Mi43MTM5Miw3NDggMzM5LjQwNzIzLDYxNC44NTE4MyA0MDIuMzI1MzUsNTA1Ljg0MDM4IDY1MC4yODM0OCw3Ni41IDY3Mi44MzYxMywzNy40NSA2OTIuMTU0MDgsNC4yNjI1IDY5My4yMTIyNSwyLjc1IEwgNjk1LjEzNjE5LDAgaCA5MC4zMDAxMyA5MC4zMDAxMyBsIDEuNTgzMTksNS43NSBjIDAuODcwNzUsMy4xNjI1IDE0LjkwOTk2LDU1LjcgMzEuMTk4MjUsMTE2Ljc1IDMzLjM0NTM2LDEyNC45ODE0OCA0NC43MjU4NiwxNjcuMTc1MzkgNDYuNjYxNDMsMTczIGwgMS4zMjkyMyw0IDkzLjczNDE1LDAuNSA5My43MzQxLDAuNSAtNjMuNjM4OSwxMTAgYyAtMzUuMDAxNCw2MC41IC02My44MjA2LDExMC45IC02NC4wNDI2LDExMiAtMC4yMjIsMS4xIDMuODc3NiwxNy41MzgxNCA5LjExMDIsMzYuNTI5MjEgQyAxMDQyLjUwNDksNjIxLjA4OTQzIDExOTcsMTE5Ny4yNzQ4IDExOTcsMTE5OC45ODY0IGMgMCwwLjY3NzUgLTMwLjc1OTIsMS4wMTM2IC05Mi43NjgzLDEuMDEzNiBoIC05Mi43Njg0IEwgOTc0LjA4NDA4LDEwNjAuMjUgQyA5NDMuMjg5Niw5NDUuMTE4NTQgODkxLjY2MjM3LDc1My45ODQ5OSA4ODkuMzc0MjMsNzQ2LjYzODQ5IGMgLTAuNDg0ODIsLTEuNTU2NTkgLTAuNzg3MDYsLTEuNjM4NDkgLTEuODQ1MiwtMC41IC0wLjY5NTk3LDAuNzQ4ODMgLTMzLjIyMzg2LDU2LjcxMTUxIC03Mi4yODQxOSwxMjQuMzYxNTEgLTM5LjA2MDMzLDY3LjY1IC05Ny42NTA3MSwxNjkuMTI1IC0xMzAuMjAwODUsMjI1LjUgLTMyLjU1MDE0LDU2LjM3NSAtNTkuNTEzMTIsMTAyLjgzOTYgLTU5LjkxNzczLDEwMy4yNTQ3IC0wLjQwNDYxLDAuNDE1MSAtNDcuMTUzMTUsMC42NDAxIC0xMDMuODg1NjUsMC41IEwgNDE4LjA5MDYzLDExOTkuNSA1MDcuMzYxNjUsMTA0NCBDIDU1Ni40NjA3MSw5NTguNDc1IDYzNi45MjIzNCw4MTguOTc1IDY4Ni4xNjUyNiw3MzQgNzc2LjYwMjU3LDU3Ny45Mzg4MSA4MTkuNzMxNzcsNTAzLjM0MDc3IDgyMS44NjcwOCw0OTkuMjg0NTkgYyAwLjk5MDY2LC0xLjg4MTgzIC0zLjUxMjA1LC0xOS43MjUwMiAtMjkuOTAzMzIsLTExOC41IEMgNzU0Ljg4MDcxLDI0MS45OTMyOSA3NTcuODg0MjUsMjUzIDc1Ny4wOTM1MiwyNTMgNzU1Ljc5NTAzLDI1MyA3MjMuMjI4NTcsMzA4Ljg3MTg4IDY0MS45NzUyLDQ1MC41IDU5Ni4yMjE4OCw1MzAuMjUgNTE3LjQ1ODcsNjY2LjgyNSA0NjYuOTQ1OSw3NTQgNDE2LjQzMzEsODQxLjE3NSAzMzcuODE3ODksOTc3LjA3NSAyOTIuMjQ1NDMsMTA1NiBsIC04Mi44NTkwMSwxNDMuNSAtMTAzLjc3NzY1LDAuMjU0NyAtMTAzLjc3NzY1MDcsMC4yNTQ2IHoiIC8+Cjwvc3ZnPgo='; -const algorandListingUri: string = - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAxnpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjabVBBDsQgCLz7in2CAlV8jt3aZH+wz99RaNM2O4kDMmZAQv9+9vAaoCRBlqK55hwBqVKpIdFoaJNTlMkT4hLut3o4BUKJEdmumv39UU+ngYWGbLkY6duF9S5U70D6MPJGPCYiJJsbVTdiMiG5QbNvxVy1XL+w9niH2gmD9mMnxcLzLgXb2xb0YaLOiSOYOdsAPA4HbhAIHLngYeIyKwoWJp8EC/m3pwPhBxlBWYcVT7MZAAABhGlDQ1BJQ0MgcHJvZmlsZQAAeJx9kT1Iw0AcxV9TpSIVUQuKOGSoThZERRy1CkWoEGqFVh1MLv0QmjQkKS6OgmvBwY/FqoOLs64OroIg+AHi6uKk6CIl/i8ptIjx4Lgf7+497t4BQq3ENKttDNB020wl4mImuyKGXhFGCD3oRb/MLGNWkpLwHV/3CPD1Lsaz/M/9ObrUnMWAgEg8wwzTJl4nntq0Dc77xBFWlFXic+JRky5I/Mh1xeM3zgWXBZ4ZMdOpOeIIsVhoYaWFWdHUiCeJo6qmU76Q8VjlvMVZK1VY4578heGcvrzEdZpDSGABi5AgQkEFGyjBRoxWnRQLKdqP+/gHXb9ELoVcG2DkmEcZGmTXD/4Hv7u18hPjXlI4DrS/OM7HMBDaBepVx/k+dpz6CRB8Bq70pr9cA6Y/Sa82tegR0L0NXFw3NWUPuNwBBp4M2ZRdKUhTyOeB9zP6pizQdwt0rnq9NfZx+gCkqavkDXBwCIwUKHvN590drb39e6bR3w8yP3KNgU0c7AAADltpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDQuNC4wLUV4aXYyIj4KIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgIHhtbG5zOkdJTVA9Imh0dHA6Ly93d3cuZ2ltcC5vcmcveG1wLyIKICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICB4bXBNTTpEb2N1bWVudElEPSJnaW1wOmRvY2lkOmdpbXA6ODMzMTg2MDItNGViMS00NzYwLWJjZmUtZGUwMGY5NWYyY2RmIgogICB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjdmNzYzMDA0LTgwYzMtNDZiNS05NzE2LWFmNjU4ZjE0MzMyMSIKICAgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjc2NGYzMzM5LWZiNzQtNGY4NS05MDNkLWZlODc2ZTkwZjVjNSIKICAgZGM6Rm9ybWF0PSJpbWFnZS9wbmciCiAgIEdJTVA6QVBJPSIyLjAiCiAgIEdJTVA6UGxhdGZvcm09IkxpbnV4IgogICBHSU1QOlRpbWVTdGFtcD0iMTcwMTk2OTUyMzQ0NTUyMyIKICAgR0lNUDpWZXJzaW9uPSIyLjEwLjM2IgogICB0aWZmOk9yaWVudGF0aW9uPSIxIgogICB4bXA6Q3JlYXRvclRvb2w9IkdJTVAgMi4xMCIKICAgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMzoxMjowN1QxNzoxODo0MiswMDowMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjM6MTI6MDdUMTc6MTg6NDIrMDA6MDAiPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6Y2hhbmdlZD0iLyIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo0MWNhMDhjMC1hYmYwLTRlYjItYjY4OS05N2JmODUwZjRhMmYiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkdpbXAgMi4xMCAoTGludXgpIgogICAgICBzdEV2dDp3aGVuPSIyMDIzLTEyLTA3VDE3OjExOjI3KzAwOjAwIi8+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InNhdmVkIgogICAgICBzdEV2dDpjaGFuZ2VkPSIvIgogICAgICBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmU1YWE1NTJhLTFkOWUtNDE5Ny1hMzQxLTA0ODliMTc2ZmVhNSIKICAgICAgc3RFdnQ6c29mdHdhcmVBZ2VudD0iR2ltcCAyLjEwIChMaW51eCkiCiAgICAgIHN0RXZ0OndoZW49IjIwMjMtMTItMDdUMTc6MTg6NDMrMDA6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgIAo8P3hwYWNrZXQgZW5kPSJ3Ij8+W5rQBgAAAAZiS0dEAG8AKgDimf8bgQAAAAlwSFlzAAAN1wAADdcBQiibeAAAAAd0SU1FB+cMBxESK1YeFPYAAAfjSURBVHja3VttTFTZGX7uMMMOgvMhoDM267IxqTW2EcHQak3sR1I1cVOdH9hq69YmLiIGx8RINGkss9b6pzbEGE2jtt00BLUhBaQ1NfJD1InF1brLLrNMlwDZTOw44DAzMMA9H/3RK5m59zI6zL3DwElOSCaH957nPe/3eY8A/UcBgG8BqACwBsDbdrt9NaXUEYlEggBYUVFRidlsDoZCoS8AfAXAD+DfAHoBxLHAhgBgJYBDADoAvATAAPA0JwMwKtE4JNEUchm4CcBOAO3SqXGNZ1xixnvSt3IK+M8AfDrHk56LZPQC+Pl8M0IA8AMAT7IEXI0RTwD8cD5UYxmAawDIPACXTyLtZVm2wG8B8GUOAJfPLwF8V2+Rr9HJwGk1JwB8oLlKHDlyxOZwOH4HgOYw+FeTWiyW3x48eNCsFXjr5s2buwRBYAsA/IyBrKio+Gt9fb0tY7F3Op2/FwSBLyDwHAAXBIGtWrXqfKbqULdAxH5WdQBwcK7gvwdgcgGDTzSMaXuHUgCDiwB8ootclo67+8siAv9qXlWzB2oGYjuATgCGbERVDoeD7927l1ssFgMAUErR39/Prl+/rvX3KYAfAehKtegtKanJ2sncunWLMMZ44hgeHiaFhYV6fO/J6xKoX2YT/P79++nk5GQyes65KIps+/btengfJmWuqiMfgC9b4AsKCvizZ88In2VcvHhRr0SrdzYp2JXNtPbMmTMK0U8cXq9XLwYwqXCjMIad2QK/evVqFggEZj19zjkPhUJk+fLleu2hXe4AvpbNoKelpSUleM45Z4zxAwcO6CUFcanGODM+yBb4HTt20FgsRlUAK/ShublZz4LLoUQG/C0b4A0GA+/u7iZqVr+trW1a/nskEmGDg4NscHCQDQ0NUb/fT8rLy7WyU7cSrX8wGwyor6+noigqTvrOnTtiZWUli8fjNJVa+P1+YrFYtNrPS+nOAuuykfHZ7Xbe39+vOP3R0VG6adMmZjAYeF9f36y2gRDCjh8/TjT2BlUA8NNsnH5TU5PC7THG+IULF2ZA3bx5k6Ryi3l5ebrYgV/rDX79+vXsxYsXCnA+n4/YbLaZdYcPH1aNDeLxOHO5XHpIaZMBwDt6JzyNjY2spKQkL/E3URR5U1OTEA6HE38TOOdc/v/379+nra2teiRn76C4uPihnqfvcrnoxMSEwrjdvXtXTCy1LVmyhPf29qqqwJUrV3Rxhzab7VNYrdav9AJvNBr5o0ePRDmgcDhMt27dmuTOzp49O2to7PV6RT32V1RU9F8A+FgvBpw4cYIQQhSorl27RtIJjYPBICkuLtZjj5/rxoCSkhI+MDCgADU0NEScTmfS2ubm5pShMaWU7dmzRw81+ARLly4d0oMBly5dUog0pZSdOnUqCci2bdtoNBqlr8sNrl69SnRQgWGUlpY+0ppwZWUlGxkZUYDq6ekhJpMpKTS+d+/eaxMjzjl//Pgx0UFK/wUAf9KacEdHB1Hx5bS6ujrJl9fV1amGxl1dXdNy2zE6OqpQHQ3mHwHAoyXR6upqqhbTt7e3J52g1WrlPp9PwahAIEDWrl3LhoeHiTxqrKmp0VoKGiF1WmhC0GQy8Z6eHqJS3KAbNmxIcnvnz59XtREej4cA4Ldv31bQaWlp0ZoB+wCgXKtkqKGhgVBKWap4HwBft24dCwaDCil5+vQpMZvNHAA/ffq0gkG9vb1Ew3tKCmA9AJil1FAXt+f3+4ndbk9a29raqlg3OTnJ9u3bRxOZJI8gY7EYXbNmDdMwHZ65Qv+HHm6PEMLcbneSdO3atUs1NO7s7CTy4ok8PWaM8ZMnT2qlBrcTk4L6TIhVVFSour0HDx4kpbBGo5F7vV7VmkBVVZXiZNXSY7kxzWC6ExnwLoDpuRJra2tTxPuxWIzu3Lkz6fSPHTtG5e6NMcYvX76sCqq2tlYhVX6/nxiNxkzBT0uYk8rid+dCbPfu3aoifePGDSKLurjP51MwamBggJSWlqrSdjgcfGRkhMhsBS0vL8/UaP9T7V70J+kSysvL4w8fPlSI6fPnz4ncWHk8HqpmIxoaGlKKtBr9xsbGTNSAAXCpFQfM6ba/uVwuGg6Hp6LR6NTY2BiNRCI8HA6zc+fOKTbo9XqnJyYmJsbHx8cjkchUJBKZ6u7uFl8nzh6Ph4iiKBLy/z/T09NMLl1pzj6pEKx6PV4D4PKbllPy8/OxYsUKSFUcQRAEMMYQCAQgL+w4HA7k5+dzzjkYY+CcIxqNCtFoNHUfrsmEsrIyKvl/gXNuGBsbE4LB4FwqQBzALwB8NNsCM4DPFmFzxBtfjwPAjgXeGJWqpfb7b9oi8+dFyIA/pNMyVwzgP4sI/BcA0m6a/A6A8UUAPvbqBmgu4/0caYnPRO/fz6hV1mKx/GqeHkRk3Cq7cuXKDzPuHK+trX1r48aNHy2kZmlBENiWLVs6jh49atHk/sjtdlvLysp+s0DUgTidzg/dbrdVjwcT+yWjkqvgx6VIT9c3RFUA+nMQvB/At5GlYZMCi1x5NHUFgB1ZHoLUUv/xPHqJp5inZ3NJCZtUWv8M2Xs42Sfpej5yaJgA/BjA3wFM6QB8Sire7s414Gqq8TaAI1LldQxzfzz9UmJonURT0GOzejNjCYBvAtgA4BsA3rXZbF8nhKyIxWIjAGhhYaGtoKDgeSgU+hzAsJS8fCLd3+v6fP5/AqTo36pmHHEAAAAASUVORK5CYII='; -const voiIconUri: string = - 'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTIwMCAxMjAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgoJPHBhdGggZD0ibSA1MzYuNjE1MDcsMTE0MS4xMzUzIGMgLTM3LjIxNzU0LC0xMC41OTQ5IC02OS4zODk1LC0yOS4zOTggLTEwMC44MTkzOCwtNTguOTI0OCAtMjEuNDc1OTMsLTIwLjE3NTQgLTMzLjkyMDQ1LC0zNy43NDA2IC02MS4yMDAwMiwtODYuMzgyNzUgLTIuOTI4NDMsLTUuMjIxNjYgLTI1Ljc5ODgsLTQ0Ljc0MDA4IC01MC44MjMwNiwtODcuODE4NzEgQyAyOTguNzQ4MzYsODY0LjkzMDQxIDI2MS41MjI1Nyw4MDAuODQ2NDkgMjQxLjA0ODY0LDc2NS42MDAzNCAyMjAuNTc0NzEsNzMwLjM1NDE4IDE3NS44NTY3NCw2NTMuNDUzNDkgMTQxLjY3NTM4LDU5NC43MDk5IC00Ljc5MDM3MjgsMzQyLjk5NTk4IC0zLjM3OTcyNzEsMzQ2LjI1NDg0IDEuNDQ1MzQ4NywyNzAuNzUxNTkgMTIuNDk4ODA1LDk3Ljc4NjAyMSAxOTAuNDIwMTgsLTAuMDA1OTQyMTggMzQ4LjgzMjEzLDc5LjgxNTM2MyBjIDQzLjI3MzA4LDIxLjgwNDYyNyA3My43MTAyNiw1Ny44NzI3NjcgMTI4LjM5NDA2LDE1Mi4xNDczMDcgMjcuMTMzMjgsNDYuNzc3NjMgNjMuMDQ1MDksMTA4LjU0NzY4IDc5LjgwNCwxMzcuMjY2NzcgMTYuNzU4OTUsMjguNzE5MDkgMzIuMTczNjEsNTUuMjkzNTkgMzQuMjU0NzUsNTkuMDU0NDIgNy43NzgwMSwxNC4wNTU0OSAxMy44NDExOCw4LjkxNjI1IDM3LjM1NTQyLC0zMS42NjMwNSBDIDc4NC4yNTIzLDEyOC4wNzU5NSA3NzUuMTEyOTQsMTQyLjY2NTY0IDgwNC45ODE2NCwxMTUuMTE1OTIgOTU4LjIzOTYsLTI2LjI0MzEwNCAxMTk5Ljk1NTEsNzguNDMwODc5IDExOTkuOTU1MSwyODYuMTU3NjkgYyAwLDYxLjQ4NzAzIC02LjQxOCw3OS42NTIwMyAtNjEuMjE3MywxNzMuMjYzOTQgLTIxLjM5NjcsMzYuNTUxNTQgLTU2Ljg0MzYsOTcuNDMxMjYgLTc4Ljc3MDUsMTM1LjI4ODI3IC0yMS45MjcsMzcuODU2OTYgLTQ5Ljk0NzUsODUuOTE5OSAtNjIuMjY3NzcsMTA2LjgwNjUyIC0xMi4zMjAzMiwyMC44ODY1OSAtNTMuMTQxNTUsOTEuMzc4ODkgLTkwLjcxMzgzLDE1Ni42NDk1NyAtMTIxLjkzNDExLDIxMS44MjQyMSAtMTI3LjQyODM2LDIxOS43OTc0MSAtMTcyLjk0MDA2LDI1MC45NjU5MSAtNTUuOTg4ODUsMzguMzQzOCAtMTMxLjkyMDYzLDUwLjY1MjMgLTE5Ny40MzA1NywzMi4wMDM0IHoiIC8+Cjwvc3ZnPgo='; -const voiListingUri: string = - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAxXpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjabVBBEsMgCLzzij5BwCg8xzTpTH/Q53dVkkk63RlXYHVFaP+8X/ToEM6Ul2rFS0lA9uzSEFiaaIM55cEDOSTktzqdgqCk2HWmVuL8UefTYG4N0XIxsmcI613weEHsxyge0t6RINjCyMNIZQocBm1+KxW3ev3Cuqc7bC7qpHV4nya/ea6Y3ragqCK7siawapkNaF9K2iAIGNdxkLWOig/m6AQD+TenA/QF5AZZHCIdZIIAAAGEaUNDUElDQyBwcm9maWxlAAB4nH2RPUjDQBzFX1OlIhVRO4g4ZKhOFkSLOGoVilAh1AqtOphc+iE0aUhSXBwF14KDH4tVBxdnXR1cBUHwA8TVxUnRRUr8X1JoEePBcT/e3XvcvQOEeplpVsc4oOm2mU4mxGxuRQy9IowQ+tCPuMwsY1aSUvAdX/cI8PUuxrP8z/05etS8xYCASDzDDNMmXiee2rQNzvvEEVaSVeJz4jGTLkj8yHXF4zfORZcFnhkxM+k54gixWGxjpY1ZydSI48RRVdMpX8h6rHLe4qyVq6x5T/7CcF5fXuI6zWEksYBFSBChoIoNlGEjRqtOioU07Sd8/EOuXyKXQq4NMHLMowINsusH/4Pf3VqFyQkvKZwAOl8c52MECO0CjZrjfB87TuMECD4DV3rLX6kD05+k11pa9Ajo3QYurluasgdc7gCDT4Zsyq4UpCkUCsD7GX1TDhi4BbpXvd6a+zh9ADLUVeoGODgERouUvebz7q723v490+zvB3pocqqkgdnbAAAPPmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgeG1sbnM6R0lNUD0iaHR0cDovL3d3dy5naW1wLm9yZy94bXAvIgogICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDo5ZDhkNTMwNi05MjMxLTQ4YmYtYjI2ZS1hMzZlZWQyODEyYTkiCiAgIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OGZmMWY2MjUtOGNmZC00ODNhLTg0NTQtYTg4YmI0NThlMWZjIgogICB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6ZGMzZDg3YTMtN2QzMC00OWZlLWFiNTMtZTdkOGEzMTY4ZmRiIgogICBkYzpGb3JtYXQ9ImltYWdlL3BuZyIKICAgR0lNUDpBUEk9IjIuMCIKICAgR0lNUDpQbGF0Zm9ybT0iTGludXgiCiAgIEdJTVA6VGltZVN0YW1wPSIxNzAxOTY5NTUyNzgxMDExIgogICBHSU1QOlZlcnNpb249IjIuMTAuMzYiCiAgIHRpZmY6T3JpZW50YXRpb249IjEiCiAgIHhtcDpDcmVhdG9yVG9vbD0iR0lNUCAyLjEwIgogICB4bXA6TWV0YWRhdGFEYXRlPSIyMDIzOjEyOjA3VDE3OjE5OjExKzAwOjAwIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAyMzoxMjowN1QxNzoxOToxMSswMDowMCI+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InNhdmVkIgogICAgICBzdEV2dDpjaGFuZ2VkPSIvIgogICAgICBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjAxMzM0YThmLWVjNzgtNDE0NC1hNjRlLTFhZDI3NWM2NWFhMiIKICAgICAgc3RFdnQ6c29mdHdhcmVBZ2VudD0iR2ltcCAyLjEwIChMaW51eCkiCiAgICAgIHN0RXZ0OndoZW49IjIwMjMtMTItMDdUMTc6MDY6MDQrMDA6MDAiLz4KICAgICA8cmRmOmxpCiAgICAgIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiCiAgICAgIHN0RXZ0OmNoYW5nZWQ9Ii8iCiAgICAgIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6NjYwYmFjODYtMWU1YS00M2MxLTkyN2QtYTVjNTUxMjVhOWM4IgogICAgICBzdEV2dDpzb2Z0d2FyZUFnZW50PSJHaW1wIDIuMTAgKExpbnV4KSIKICAgICAgc3RFdnQ6d2hlbj0iMjAyMy0xMi0wN1QxNzoxMjowNCswMDowMCIvPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6Y2hhbmdlZD0iLyIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiNmYyOWM0NS05MDNiLTRjNmUtODg1NS01YzE1YWIyODlkZTEiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkdpbXAgMi4xMCAoTGludXgpIgogICAgICBzdEV2dDp3aGVuPSIyMDIzLTEyLTA3VDE3OjE5OjEyKzAwOjAwIi8+CiAgICA8L3JkZjpTZXE+CiAgIDwveG1wTU06SGlzdG9yeT4KICA8L3JkZjpEZXNjcmlwdGlvbj4KIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg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'; +// utils +import fetchVoiARC0072TokensByOwner from '@extension/utils/fetchVoiARC0072TokensByOwner'; const networks: INetwork[] = [ /** @@ -24,9 +27,16 @@ const networks: INetwork[] = [ url: 'https://testnet-api.voi.nodly.io', }, ], + arc0072Indexers: [ + { + canonicalName: 'NFT Navigator', + fetchTokensByOwner: fetchVoiARC0072TokensByOwner, + id: 'nft-navigator', + }, + ], canonicalName: 'Voi', chakraTheme: 'voi', - explorers: [ + blockExplorers: [ { accountPath: '/account', applicationPath: '/application', @@ -57,12 +67,22 @@ const networks: INetwork[] = [ }, nativeCurrency: { decimals: 6, - iconUrl: voiIconUri, - listingUri: voiListingUri, + iconUrl: VOI_ICON_URI, + listingUri: VOI_LISTING_ICON_URI, symbol: 'VOI', type: AssetTypeEnum.Native, verified: true, }, + nftExplorers: [ + { + baseURL: 'https://nftnavigator.xyz', + canonicalName: 'NFT Navigator', + collectionPath: (appId: string) => `/collection/${appId}`, + id: 'nft-navigator', + tokenPath: (appId: string, tokenId: string) => + `/collection/${appId}/token/${tokenId}`, + }, + ], type: NetworkTypeEnum.Test, }, /** @@ -76,9 +96,10 @@ const networks: INetwork[] = [ url: 'https://mainnet-api.algonode.cloud', }, ], + arc0072Indexers: [], canonicalName: 'Algorand', chakraTheme: 'algorand', - explorers: [ + blockExplorers: [ { accountPath: '/address', applicationPath: '/application', @@ -120,12 +141,13 @@ const networks: INetwork[] = [ }, nativeCurrency: { decimals: 6, - iconUrl: algorandIconUri, - listingUri: algorandListingUri, + iconUrl: ALGORAND_ICON_URI, + listingUri: ALGORAND_LISTING_ICON_URI, symbol: 'ALGO', type: AssetTypeEnum.Native, verified: true, }, + nftExplorers: [], type: NetworkTypeEnum.Stable, }, { @@ -136,9 +158,10 @@ const networks: INetwork[] = [ url: 'https://betanet-api.algonode.cloud', }, ], + arc0072Indexers: [], canonicalName: 'Algorand', chakraTheme: 'algorand', - explorers: [], + blockExplorers: [], feeSunkAddress: 'A7NMWS3NT3IUDMLVO26ULGXGIIOUQ3ND2TXSER6EBGRZNOBOUIQXHIBGDE', genesisId: 'betanet-v1.0', @@ -157,12 +180,13 @@ const networks: INetwork[] = [ }, nativeCurrency: { decimals: 6, - iconUrl: algorandIconUri, - listingUri: algorandListingUri, + iconUrl: ALGORAND_ICON_URI, + listingUri: ALGORAND_LISTING_ICON_URI, symbol: 'ALGO', type: AssetTypeEnum.Native, verified: true, }, + nftExplorers: [], type: NetworkTypeEnum.Beta, }, { @@ -173,9 +197,10 @@ const networks: INetwork[] = [ url: 'https://testnet-api.algonode.cloud', }, ], + arc0072Indexers: [], canonicalName: 'Algorand', chakraTheme: 'algorand', - explorers: [ + blockExplorers: [ { accountPath: '/address', applicationPath: '/application', @@ -206,12 +231,13 @@ const networks: INetwork[] = [ }, nativeCurrency: { decimals: 6, - iconUrl: algorandIconUri, - listingUri: algorandListingUri, + iconUrl: ALGORAND_ICON_URI, + listingUri: ALGORAND_LISTING_ICON_URI, symbol: 'ALGO', type: AssetTypeEnum.Native, verified: true, }, + nftExplorers: [], type: NetworkTypeEnum.Test, }, ]; diff --git a/src/extension/constants/Keys.ts b/src/extension/constants/Keys.ts index e07743dc..a87c4f66 100644 --- a/src/extension/constants/Keys.ts +++ b/src/extension/constants/Keys.ts @@ -1,6 +1,7 @@ export const ACCOUNTS_ITEM_KEY_PREFIX: string = 'accounts_'; export const ACTIVE_ACCOUNT_DETAILS_KEY: string = 'active_account_details'; export const APP_WINDOW_KEY_PREFIX: string = 'app_window_'; +export const ARC0072_ASSETS_KEY_PREFIX: string = 'arc0072_assets_'; export const ARC0200_ASSETS_KEY_PREFIX: string = 'arc200_assets_'; export const EVENT_QUEUE_ITEM_KEY: string = 'event_queue'; export const NETWORK_TRANSACTION_PARAMS_ITEM_KEY_PREFIX: string = diff --git a/src/extension/constants/Routes.ts b/src/extension/constants/Routes.ts index 73e8ad9c..4453eab4 100644 --- a/src/extension/constants/Routes.ts +++ b/src/extension/constants/Routes.ts @@ -12,6 +12,7 @@ export const GENERAL_ROUTE: string = '/general'; export const GET_STARTED_ROUTE: string = '/get-started'; export const IMPORT_ACCOUNT_VIA_SEED_PHRASE_ROUTE: string = '/import-account-via-seed-phrase'; +export const NFTS_ROUTE: string = '/nfts'; export const PASSWORD_LOCK_ROUTE: string = '/password-lock'; export const SECURITY_ROUTE: string = '/security'; export const SESSIONS_ROUTE: string = '/sessions'; diff --git a/src/extension/constants/URIs.ts b/src/extension/constants/URIs.ts new file mode 100644 index 00000000..9c7302d1 --- /dev/null +++ b/src/extension/constants/URIs.ts @@ -0,0 +1,8 @@ +export const ALGORAND_ICON_URI: string = + 'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTIwMCAxMjAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgoJPHBhdGgKCSAgICAgIGQ9Ik0gMy43OTI0ODAxLDExOTYuNzU0NyBDIDcuMzg4OTI2LDExOTAuNzg2OCAxNzIuNTQ4ODcsOTA0LjUzNjYzIDI2Mi43MTM5Miw3NDggMzM5LjQwNzIzLDYxNC44NTE4MyA0MDIuMzI1MzUsNTA1Ljg0MDM4IDY1MC4yODM0OCw3Ni41IDY3Mi44MzYxMywzNy40NSA2OTIuMTU0MDgsNC4yNjI1IDY5My4yMTIyNSwyLjc1IEwgNjk1LjEzNjE5LDAgaCA5MC4zMDAxMyA5MC4zMDAxMyBsIDEuNTgzMTksNS43NSBjIDAuODcwNzUsMy4xNjI1IDE0LjkwOTk2LDU1LjcgMzEuMTk4MjUsMTE2Ljc1IDMzLjM0NTM2LDEyNC45ODE0OCA0NC43MjU4NiwxNjcuMTc1MzkgNDYuNjYxNDMsMTczIGwgMS4zMjkyMyw0IDkzLjczNDE1LDAuNSA5My43MzQxLDAuNSAtNjMuNjM4OSwxMTAgYyAtMzUuMDAxNCw2MC41IC02My44MjA2LDExMC45IC02NC4wNDI2LDExMiAtMC4yMjIsMS4xIDMuODc3NiwxNy41MzgxNCA5LjExMDIsMzYuNTI5MjEgQyAxMDQyLjUwNDksNjIxLjA4OTQzIDExOTcsMTE5Ny4yNzQ4IDExOTcsMTE5OC45ODY0IGMgMCwwLjY3NzUgLTMwLjc1OTIsMS4wMTM2IC05Mi43NjgzLDEuMDEzNiBoIC05Mi43Njg0IEwgOTc0LjA4NDA4LDEwNjAuMjUgQyA5NDMuMjg5Niw5NDUuMTE4NTQgODkxLjY2MjM3LDc1My45ODQ5OSA4ODkuMzc0MjMsNzQ2LjYzODQ5IGMgLTAuNDg0ODIsLTEuNTU2NTkgLTAuNzg3MDYsLTEuNjM4NDkgLTEuODQ1MiwtMC41IC0wLjY5NTk3LDAuNzQ4ODMgLTMzLjIyMzg2LDU2LjcxMTUxIC03Mi4yODQxOSwxMjQuMzYxNTEgLTM5LjA2MDMzLDY3LjY1IC05Ny42NTA3MSwxNjkuMTI1IC0xMzAuMjAwODUsMjI1LjUgLTMyLjU1MDE0LDU2LjM3NSAtNTkuNTEzMTIsMTAyLjgzOTYgLTU5LjkxNzczLDEwMy4yNTQ3IC0wLjQwNDYxLDAuNDE1MSAtNDcuMTUzMTUsMC42NDAxIC0xMDMuODg1NjUsMC41IEwgNDE4LjA5MDYzLDExOTkuNSA1MDcuMzYxNjUsMTA0NCBDIDU1Ni40NjA3MSw5NTguNDc1IDYzNi45MjIzNCw4MTguOTc1IDY4Ni4xNjUyNiw3MzQgNzc2LjYwMjU3LDU3Ny45Mzg4MSA4MTkuNzMxNzcsNTAzLjM0MDc3IDgyMS44NjcwOCw0OTkuMjg0NTkgYyAwLjk5MDY2LC0xLjg4MTgzIC0zLjUxMjA1LC0xOS43MjUwMiAtMjkuOTAzMzIsLTExOC41IEMgNzU0Ljg4MDcxLDI0MS45OTMyOSA3NTcuODg0MjUsMjUzIDc1Ny4wOTM1MiwyNTMgNzU1Ljc5NTAzLDI1MyA3MjMuMjI4NTcsMzA4Ljg3MTg4IDY0MS45NzUyLDQ1MC41IDU5Ni4yMjE4OCw1MzAuMjUgNTE3LjQ1ODcsNjY2LjgyNSA0NjYuOTQ1OSw3NTQgNDE2LjQzMzEsODQxLjE3NSAzMzcuODE3ODksOTc3LjA3NSAyOTIuMjQ1NDMsMTA1NiBsIC04Mi44NTkwMSwxNDMuNSAtMTAzLjc3NzY1LDAuMjU0NyAtMTAzLjc3NzY1MDcsMC4yNTQ2IHoiIC8+Cjwvc3ZnPgo='; +export const ALGORAND_LISTING_ICON_URI: string = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAxnpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjabVBBDsQgCLz7in2CAlV8jt3aZH+wz99RaNM2O4kDMmZAQv9+9vAaoCRBlqK55hwBqVKpIdFoaJNTlMkT4hLut3o4BUKJEdmumv39UU+ngYWGbLkY6duF9S5U70D6MPJGPCYiJJsbVTdiMiG5QbNvxVy1XL+w9niH2gmD9mMnxcLzLgXb2xb0YaLOiSOYOdsAPA4HbhAIHLngYeIyKwoWJp8EC/m3pwPhBxlBWYcVT7MZAAABhGlDQ1BJQ0MgcHJvZmlsZQAAeJx9kT1Iw0AcxV9TpSIVUQuKOGSoThZERRy1CkWoEGqFVh1MLv0QmjQkKS6OgmvBwY/FqoOLs64OroIg+AHi6uKk6CIl/i8ptIjx4Lgf7+497t4BQq3ENKttDNB020wl4mImuyKGXhFGCD3oRb/MLGNWkpLwHV/3CPD1Lsaz/M/9ObrUnMWAgEg8wwzTJl4nntq0Dc77xBFWlFXic+JRky5I/Mh1xeM3zgWXBZ4ZMdOpOeIIsVhoYaWFWdHUiCeJo6qmU76Q8VjlvMVZK1VY4578heGcvrzEdZpDSGABi5AgQkEFGyjBRoxWnRQLKdqP+/gHXb9ELoVcG2DkmEcZGmTXD/4Hv7u18hPjXlI4DrS/OM7HMBDaBepVx/k+dpz6CRB8Bq70pr9cA6Y/Sa82tegR0L0NXFw3NWUPuNwBBp4M2ZRdKUhTyOeB9zP6pizQdwt0rnq9NfZx+gCkqavkDXBwCIwUKHvN590drb39e6bR3w8yP3KNgU0c7AAADltpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDQuNC4wLUV4aXYyIj4KIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgIHhtbG5zOkdJTVA9Imh0dHA6Ly93d3cuZ2ltcC5vcmcveG1wLyIKICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICB4bXBNTTpEb2N1bWVudElEPSJnaW1wOmRvY2lkOmdpbXA6ODMzMTg2MDItNGViMS00NzYwLWJjZmUtZGUwMGY5NWYyY2RmIgogICB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjdmNzYzMDA0LTgwYzMtNDZiNS05NzE2LWFmNjU4ZjE0MzMyMSIKICAgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjc2NGYzMzM5LWZiNzQtNGY4NS05MDNkLWZlODc2ZTkwZjVjNSIKICAgZGM6Rm9ybWF0PSJpbWFnZS9wbmciCiAgIEdJTVA6QVBJPSIyLjAiCiAgIEdJTVA6UGxhdGZvcm09IkxpbnV4IgogICBHSU1QOlRpbWVTdGFtcD0iMTcwMTk2OTUyMzQ0NTUyMyIKICAgR0lNUDpWZXJzaW9uPSIyLjEwLjM2IgogICB0aWZmOk9yaWVudGF0aW9uPSIxIgogICB4bXA6Q3JlYXRvclRvb2w9IkdJTVAgMi4xMCIKICAgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMzoxMjowN1QxNzoxODo0MiswMDowMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjM6MTI6MDdUMTc6MTg6NDIrMDA6MDAiPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6Y2hhbmdlZD0iLyIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo0MWNhMDhjMC1hYmYwLTRlYjItYjY4OS05N2JmODUwZjRhMmYiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkdpbXAgMi4xMCAoTGludXgpIgogICAgICBzdEV2dDp3aGVuPSIyMDIzLTEyLTA3VDE3OjExOjI3KzAwOjAwIi8+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InNhdmVkIgogICAgICBzdEV2dDpjaGFuZ2VkPSIvIgogICAgICBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmU1YWE1NTJhLTFkOWUtNDE5Ny1hMzQxLTA0ODliMTc2ZmVhNSIKICAgICAgc3RFdnQ6c29mdHdhcmVBZ2VudD0iR2ltcCAyLjEwIChMaW51eCkiCiAgICAgIHN0RXZ0OndoZW49IjIwMjMtMTItMDdUMTc6MTg6NDMrMDA6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgIAo8P3hwYWNrZXQgZW5kPSJ3Ij8+W5rQBgAAAAZiS0dEAG8AKgDimf8bgQAAAAlwSFlzAAAN1wAADdcBQiibeAAAAAd0SU1FB+cMBxESK1YeFPYAAAfjSURBVHja3VttTFTZGX7uMMMOgvMhoDM267IxqTW2EcHQak3sR1I1cVOdH9hq69YmLiIGx8RINGkss9b6pzbEGE2jtt00BLUhBaQ1NfJD1InF1brLLrNMlwDZTOw44DAzMMA9H/3RK5m59zI6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'; +export const VOI_ICON_URI: string = + 'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTIwMCAxMjAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgoJPHBhdGggZD0ibSA1MzYuNjE1MDcsMTE0MS4xMzUzIGMgLTM3LjIxNzU0LC0xMC41OTQ5IC02OS4zODk1LC0yOS4zOTggLTEwMC44MTkzOCwtNTguOTI0OCAtMjEuNDc1OTMsLTIwLjE3NTQgLTMzLjkyMDQ1LC0zNy43NDA2IC02MS4yMDAwMiwtODYuMzgyNzUgLTIuOTI4NDMsLTUuMjIxNjYgLTI1Ljc5ODgsLTQ0Ljc0MDA4IC01MC44MjMwNiwtODcuODE4NzEgQyAyOTguNzQ4MzYsODY0LjkzMDQxIDI2MS41MjI1Nyw4MDAuODQ2NDkgMjQxLjA0ODY0LDc2NS42MDAzNCAyMjAuNTc0NzEsNzMwLjM1NDE4IDE3NS44NTY3NCw2NTMuNDUzNDkgMTQxLjY3NTM4LDU5NC43MDk5IC00Ljc5MDM3MjgsMzQyLjk5NTk4IC0zLjM3OTcyNzEsMzQ2LjI1NDg0IDEuNDQ1MzQ4NywyNzAuNzUxNTkgMTIuNDk4ODA1LDk3Ljc4NjAyMSAxOTAuNDIwMTgsLTAuMDA1OTQyMTggMzQ4LjgzMjEzLDc5LjgxNTM2MyBjIDQzLjI3MzA4LDIxLjgwNDYyNyA3My43MTAyNiw1Ny44NzI3NjcgMTI4LjM5NDA2LDE1Mi4xNDczMDcgMjcuMTMzMjgsNDYuNzc3NjMgNjMuMDQ1MDksMTA4LjU0NzY4IDc5LjgwNCwxMzcuMjY2NzcgMTYuNzU4OTUsMjguNzE5MDkgMzIuMTczNjEsNTUuMjkzNTkgMzQuMjU0NzUsNTkuMDU0NDIgNy43NzgwMSwxNC4wNTU0OSAxMy44NDExOCw4LjkxNjI1IDM3LjM1NTQyLC0zMS42NjMwNSBDIDc4NC4yNTIzLDEyOC4wNzU5NSA3NzUuMTEyOTQsMTQyLjY2NTY0IDgwNC45ODE2NCwxMTUuMTE1OTIgOTU4LjIzOTYsLTI2LjI0MzEwNCAxMTk5Ljk1NTEsNzguNDMwODc5IDExOTkuOTU1MSwyODYuMTU3NjkgYyAwLDYxLjQ4NzAzIC02LjQxOCw3OS42NTIwMyAtNjEuMjE3MywxNzMuMjYzOTQgLTIxLjM5NjcsMzYuNTUxNTQgLTU2Ljg0MzYsOTcuNDMxMjYgLTc4Ljc3MDUsMTM1LjI4ODI3IC0yMS45MjcsMzcuODU2OTYgLTQ5Ljk0NzUsODUuOTE5OSAtNjIuMjY3NzcsMTA2LjgwNjUyIC0xMi4zMjAzMiwyMC44ODY1OSAtNTMuMTQxNTUsOTEuMzc4ODkgLTkwLjcxMzgzLDE1Ni42NDk1NyAtMTIxLjkzNDExLDIxMS44MjQyMSAtMTI3LjQyODM2LDIxOS43OTc0MSAtMTcyLjk0MDA2LDI1MC45NjU5MSAtNTUuOTg4ODUsMzguMzQzOCAtMTMxLjkyMDYzLDUwLjY1MjMgLTE5Ny40MzA1NywzMi4wMDM0IHoiIC8+Cjwvc3ZnPgo='; +export const VOI_LISTING_ICON_URI: string = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAxXpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjabVBBEsMgCLzzij5BwCg8xzTpTH/Q53dVkkk63RlXYHVFaP+8X/ToEM6Ul2rFS0lA9uzSEFiaaIM55cEDOSTktzqdgqCk2HWmVuL8UefTYG4N0XIxsmcI613weEHsxyge0t6RINjCyMNIZQocBm1+KxW3ev3Cuqc7bC7qpHV4nya/ea6Y3ragqCK7siawapkNaF9K2iAIGNdxkLWOig/m6AQD+TenA/QF5AZZHCIdZIIAAAGEaUNDUElDQyBwcm9maWxlAAB4nH2RPUjDQBzFX1OlIhVRO4g4ZKhOFkSLOGoVilAh1AqtOphc+iE0aUhSXBwF14KDH4tVBxdnXR1cBUHwA8TVxUnRRUr8X1JoEePBcT/e3XvcvQOEeplpVsc4oOm2mU4mxGxuRQy9IowQ+tCPuMwsY1aSUvAdX/cI8PUuxrP8z/05etS8xYCASDzDDNMmXiee2rQNzvvEEVaSVeJz4jGTLkj8yHXF4zfORZcFnhkxM+k54gixWGxjpY1ZydSI48RRVdMpX8h6rHLe4qyVq6x5T/7CcF5fXuI6zWEksYBFSBChoIoNlGEjRqtOioU07Sd8/EOuXyKXQq4NMHLMowINsusH/4Pf3VqFyQkvKZwAOl8c52MECO0CjZrjfB87TuMECD4DV3rLX6kD05+k11pa9Ajo3QYurluasgdc7gCDT4Zsyq4UpCkUCsD7GX1TDhi4BbpXvd6a+zh9ADLUVeoGODgERouUvebz7q723v490+zvB3pocqqkgdnbAAAPPmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgeG1sbnM6R0lNUD0iaHR0cDovL3d3dy5naW1wLm9yZy94bXAvIgogICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDo5ZDhkNTMwNi05MjMxLTQ4YmYtYjI2ZS1hMzZlZWQyODEyYTkiCiAgIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OGZmMWY2MjUtOGNmZC00ODNhLTg0NTQtYTg4YmI0NThlMWZjIgogICB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6ZGMzZDg3YTMtN2QzMC00OWZlLWFiNTMtZTdkOGEzMTY4ZmRiIgogICBkYzpGb3JtYXQ9ImltYWdlL3BuZyIKICAgR0lNUDpBUEk9IjIuMCIKICAgR0lNUDpQbGF0Zm9ybT0iTGludXgiCiAgIEdJTVA6VGltZVN0YW1wPSIxNzAxOTY5NTUyNzgxMDExIgogICBHSU1QOlZlcnNpb249IjIuMTAuMzYiCiAgIHRpZmY6T3JpZW50YXRpb249IjEiCiAgIHhtcDpDcmVhdG9yVG9vbD0iR0lNUCAyLjEwIgogICB4bXA6TWV0YWRhdGFEYXRlPSIyMDIzOjEyOjA3VDE3OjE5OjExKzAwOjAwIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAyMzoxMjowN1QxNzoxOToxMSswMDowMCI+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InNhdmVkIgogICAgICBzdEV2dDpjaGFuZ2VkPSIvIgogICAgICBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjAxMzM0YThmLWVjNzgtNDE0NC1hNjRlLTFhZDI3NWM2NWFhMiIKICAgICAgc3RFdnQ6c29mdHdhcmVBZ2VudD0iR2ltcCAyLjEwIChMaW51eCkiCiAgICAgIHN0RXZ0OndoZW49IjIwMjMtMTItMDdUMTc6MDY6MDQrMDA6MDAiLz4KICAgICA8cmRmOmxpCiAgICAgIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiCiAgICAgIHN0RXZ0OmNoYW5nZWQ9Ii8iCiAgICAgIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6NjYwYmFjODYtMWU1YS00M2MxLTkyN2QtYTVjNTUxMjVhOWM4IgogICAgICBzdEV2dDpzb2Z0d2FyZUFnZW50PSJHaW1wIDIuMTAgKExpbnV4KSIKICAgICAgc3RFdnQ6d2hlbj0iMjAyMy0xMi0wN1QxNzoxMjowNCswMDowMCIvPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6Y2hhbmdlZD0iLyIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiNmYyOWM0NS05MDNiLTRjNmUtODg1NS01YzE1YWIyODlkZTEiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkdpbXAgMi4xMCAoTGludXgpIgogICAgICBzdEV2dDp3aGVuPSIyMDIzLTEyLTA3VDE3OjE5OjEyKzAwOjAwIi8+CiAgICA8L3JkZjpTZXE+CiAgIDwveG1wTU06SGlzdG9yeT4KICA8L3JkZjpEZXNjcmlwdGlvbj4KIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVuZD0idyI/Psr3E3MAAAAGYktHRABvACoA4pn/G4EAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfnDAcREwzqD5DcAAAHHElEQVR42uWbf2xT1xXHP+/aDrEDJFnKb+GyarCSMuikCVjpH+kqTRGVyh+UTcqklrYg0aHWWqdKlbZpmjRpWqUlc9kYU2CjWce0dUxqgDZsZcpW0IC0hZbWWZMpahNKFUOIf8Rx7Ge/tz/eTQrESXyf30sT8v0zee/4fM8799xzzzlXw0WEansFsBLYCNwLrAVWAUuAKsAvH00DMaAf+AjoBC4C54G+cCRouKWj5gJpL7AJ2A7UA6sBr01xOaAbaAOOAufCkWBuRhogVNt7B7AT2AWsccG4JtAFHAQOhyPBazPCAJL4s8AeoJrpwSBwAGgs1RBaCcR9wG7gx8BiPh9EgZ8AzeFIUJ82A4Rqe9cCzcAWZgbOALvDkWCnqwYI1fZqwGNAGFjIzEICCAEvhSNBs9iXhAL5MqAJODQDySN1OgQ0SV2d84BQbW8AaJFb22zAUeDRcCQ4XLIBJPlXgK3MLrwG7JjKCKIIt2+ZheSROrdMtRzEFAHvhVnk9oWwHXhBclH2gMeAp5n9eFpyKT4GyH3+7AyN9na3yM2F8gQxQYbXfBuRH90imyW3m1DolLZbNcPTPLD6IR8b6vzULPUhBMSv5+m+MMKFP2fQE6YtrTUv3P1wGRvq/NyxbFRujq53Mrx1ZAQ9riR3i+S2f8IlIA82H6jk9ou/5uHbz1dz15f9aAUiyvWrOidaBnn7dxkl8mNy7/ajFVioA1GdV5sHee+IktwocM+NByjPjf/dvOh7P5Rn+KKwfIuHp36xmGUr5xVUEsBf4eGejQGSWpbLbxV3lL/zG172/HxyuYEKD1/5uppcoALIn7vadGpcDJBff0+xkjx++M4PaqisnrrW4fVqbHuihhX3T/1sWbVGw/M1LCxGrk9j25M1LN3sUfGCPZLruCC4U+U8v+kJPyvunFf0r5YHBI88W4XwTf5c/XPzWbKirHi5fkH9LqV4XS25fmYAWcbapSJlw/0B5aC2ao2fLU/5J133931TffNZvc6Pd77SK7sk5zEP2CTLWMUdIX2waKlPPapr8OCOKvzLtYIp2bZnKin3C2W5gQoPi9YpLYM1kvOYAbar1AY0H5SVqysKUPUFLw99f8G4v697pIy1Xw3YkokG5Qs1xTesFF/I0nW9ytumAfmcvb0dYGPdApbf57kpoG59vAoh7JcoM0PK+tSHanvFaN1+tcqbxgj09WRsK1tWLnh4b+VY3lD3TIDlwXm25aUSeaKX8sqhA1gpsJoWynX7N/+WxDDse8Ga9RWsb5jH/C8K6rZVlpTnXjqfIpdUfs0LbBRYHRtldB3T6bqUtq20ELD10Sq+9aNKFlR67X/9oTxtv0naff1egdWuUoZpwPEDcfSsfS9YsqKM9Zvml/T1T7+WIP6h7c7ZWoHVq7OFy//O8c7pJJ8Xoley/KNpqBQRqwRWo9I2jjclScbz007eNOD1P8TIJUoSs0RgdWltI9lj0N4an3YDdL0/zMU/ZkoVUyX4rEVtG+0vpvi0NzNt5LMZg1d/FcMs3fH8wgmF8sNw4nCspG1RBR3tSa6ccWbZCazhhJLx/itZOi8Mu04+PpjjRKNjgTctsCYzSocBrfviZNKuDXNgmnDqrzGGLzvmaTGBNZbiCPo78pw9lXDNAH09I5zen3ZSZL/AmslxDK83DnH9qu44+XzO5PjBGEbWUbEfCayBJMcw0m/yxl/imA7Hw0sdKbqOOW7YToE1jeUo/tOc5uNu51w1nTI49mLcmhJyFhcF1iiao5NXpg6tv42T053R+MzJOAPvOR5cc8B5AfRhjaI5ip6TOu+eHSpZzkC/zt8bU27E1G6gT8ghxDbn9yxobUqQStpPWEwTTv4pRnbQlQSrLRwJGqOZ4FFcWGHxDw3ePGH/nNDz3zQdvx9xJaWQnMeKouewhhAdxxu/TNH/ifrepWdNWvfHMHNuaEWX5GwZQI6fHnTjl3JJaHs5hqEYwzraE3z8T3fYAwdHR25vPAwdxprAdH6veTnDu2eLz9+jV7K0/sy1Qsug5MpNBpAd0wNuFS+OPBfng7dTUyZIg9d0Wn46wEjUtZPlgRu7w7cehxuxWsiOQ4+ZHHryOsdbBhi8po8zRHrYoKM9QePjUS7/yzXXj0qOYxjXiQjV9n4X+LWbR1pPAO560MeyL/nweODaJzn+166T/tT1esLecCR404BEoXp0M9CAi3PA+WHoPqbTjc404ozkxqQeIL1g7g5JyYDYiTV4bNwG5A0gNNEk+WQ1wZeAfbeBAfZJLgUxaTtWjpkeYfZOix4FGsKRYNaWAaQR5u6wtIwHw8CO0cPDLPryO4oZly+qLyAFNWDdFJnJgdGQOjYUQ76oJXDLcrjtrszM+UtTtlpj8oceAPa6dXZQyO33Ag/YIW/bA27xhrl5cXICQ+xkrl2dLWCIuXl5egJjzPjr8/8HYKadv86Sx1sAAAAASUVORK5CYII='; diff --git a/src/extension/constants/Urls.ts b/src/extension/constants/URLs.ts similarity index 100% rename from src/extension/constants/Urls.ts rename to src/extension/constants/URLs.ts diff --git a/src/extension/constants/index.ts b/src/extension/constants/index.ts index ace0f5db..e0a3221a 100644 --- a/src/extension/constants/index.ts +++ b/src/extension/constants/index.ts @@ -9,5 +9,6 @@ export * from './Links'; export * from './Limits'; export * from './Routes'; export * from './Styles'; -export * from './Urls'; +export * from './URIs'; +export * from './URLs'; export * from './Validation'; diff --git a/src/extension/contracts/ARC0072Contract/ARC0072Contract.test.ts b/src/extension/contracts/ARC0072Contract/ARC0072Contract.test.ts new file mode 100644 index 00000000..de653d23 --- /dev/null +++ b/src/extension/contracts/ARC0072Contract/ARC0072Contract.test.ts @@ -0,0 +1,86 @@ +import { Account, generateAccount } from 'algosdk'; +import BigNumber from 'bignumber.js'; + +// config +import { networks } from '@extension/config'; + +// contracts +import ARC0072Contract from './ARC0072Contract'; + +// types +import type { IBaseOptions } from '@common/types'; +import type { IARC0200AssetInformation, INetwork } from '@extension/types'; + +// utils +import createLogger from '@common/utils/createLogger'; + +describe.skip(`${__dirname}#ARC0072Contract`, () => { + const appId: BigNumber = new BigNumber('28385598'); + const options: IBaseOptions = { + logger: createLogger('debug'), + }; + const totalSupply: BigNumber = new BigNumber('14'); + let contract: ARC0072Contract; + + beforeAll(() => { + const network: INetwork | null = + networks.find((value) => value.genesisId === 'voitest-v1') || null; + + if (!network) { + throw new Error('unable to find voi testnet network'); + } + + contract = new ARC0072Contract({ + appId, + network, + ...options, + }); + }); + + describe('when getting the balance for an owner', () => { + it('should return 0 for a new account', async () => { + // arrange + const account: Account = generateAccount(); + // act + const result: BigNumber = await contract.balanceOf(account.addr); + + // assert + expect(result.toNumber()).toBe(0); + }); + }); + + describe('when getting the owner of a token', () => { + it('should return null for a token that does not exist', async () => { + // arrange + // act + const result: string | null = await contract.ownerOf( + new BigNumber('9999999') + ); + + // assert + expect(result).toBeNull(); + }); + }); + + describe('when getting the token URI', () => { + it('should return 0 for a new account', async () => { + // arrange + // act + const result: string = await contract.tokenURI(new BigNumber('100')); + + // assert + expect(result).toBe(''); + }); + }); + + describe('when getting the total supply', () => { + it('should return the total supply', async () => { + // arrange + // act + const result: BigNumber = await contract.totalSupply(); + + // assert + expect(result.eq(totalSupply)).toBe(true); + }); + }); +}); diff --git a/src/extension/contracts/ARC0072Contract/ARC0072Contract.ts b/src/extension/contracts/ARC0072Contract/ARC0072Contract.ts new file mode 100644 index 00000000..18fee064 --- /dev/null +++ b/src/extension/contracts/ARC0072Contract/ARC0072Contract.ts @@ -0,0 +1,238 @@ +import { ABIContract, ABIMethod, ABIType } from 'algosdk'; +import BigNumber from 'bignumber.js'; + +// abi +import abi from './abi.json'; + +// contracts +import BaseContract, { INewBaseContractOptions } from '../BaseContract'; + +// enums +import { ARC0072MethodEnum } from './enums'; + +// errors +import { InvalidABIContractError } from '@extension/errors'; + +// utils +import isZeroAddress from '@extension/utils/isZeroAddress'; + +export default class ARC0072Contract extends BaseContract { + constructor(options: INewBaseContractOptions) { + super(options); + + this.abi = new ABIContract(abi); + } + + /** + * public functions + */ + + /** + * Gets the balance of the ARC-0072 asset for a given address. + * @param {string} owner - the address of the account to check. + * @returns {Promise} the balance of the account. + * @throws {InvalidABIContractError} if the application ID is not an ARC-0072 asset. + * @throws {ReadABIContractError} if there was a problem reading the data. + */ + public async balanceOf(owner: string): Promise { + const _functionName: string = 'balanceOf'; + let abiAddressArgType: ABIType | null; + let abiMethod: ABIMethod; + let result: BigNumber | null; + + try { + abiMethod = this.abi.getMethodByName(ARC0072MethodEnum.BalanceOf); + abiAddressArgType = abiMethod.args[0]?.type as ABIType; + + // if the first arg, owner, is not an address + if (!abiAddressArgType || abiAddressArgType.toString() !== 'address') { + throw new InvalidABIContractError( + this.appId.toString(), + `application "${this.appId.toString()}" not valid as method "${ + ARC0072MethodEnum.BalanceOf + }" has an invalid "owner" type` + ); + } + + result = (await this.readByMethod({ + abiMethod, + appArgs: [abiAddressArgType.encode(owner)], + })) as BigNumber | null; + } catch (error) { + this.logger.debug( + `${ARC0072Contract.name}#${_functionName}: ${error.message}` + ); + + throw error; + } + + if (!result) { + throw new InvalidABIContractError( + this.appId.toString(), + `application "${this.appId.toString()}" not valid because the result returned "null"` + ); + } + + return result; + } + + /** + * Gets the owner of a token for a given ID. + * @param {BigNumber} tokenId - the token ID to check. + * @returns {Promise} returns the address or null if the ID has not been assigned. + * @throws {InvalidABIContractError} if the application ID is not an ARC-0072 asset. + * @throws {ReadABIContractError} if there was a problem reading the data. + */ + public async ownerOf(tokenId: BigNumber): Promise { + const _functionName: string = 'ownerOf'; + let abiMethod: ABIMethod; + let address: string; + let encodedTokenId: Uint8Array; + let result: string | null; + + try { + abiMethod = this.abi.getMethodByName(ARC0072MethodEnum.OwnerOf); + encodedTokenId = (abiMethod.args[0].type as ABIType).encode( + BigInt(tokenId.toString()) + ); + result = (await this.readByMethod({ + abiMethod, + appArgs: [encodedTokenId], + })) as string | null; + } catch (error) { + this.logger.debug( + `${ARC0072Contract.name}#${_functionName}: ${error.message}` + ); + + throw error; + } + + if (!result) { + throw new InvalidABIContractError( + this.appId.toString(), + `application "${this.appId.toString()}" not valid because the result returned "null"` + ); + } + + address = this.trimNullBytes(result); + + // a zero address indicates the token ID has not been assigned, so return null + if (isZeroAddress(address)) { + return null; + } + + return address; + } + + /** + * Gets the token ID of the token with the given index. + * @param {BigNumber} index - the index. + * @returns {BigNumber} the token ID for a given index. + */ + public async tokenByIndex(index: BigNumber): Promise { + const _functionName: string = 'tokenByIndex'; + let abiMethod: ABIMethod; + let encodedIndex: Uint8Array; + let result: BigNumber | null; + + try { + abiMethod = this.abi.getMethodByName(ARC0072MethodEnum.TokenByIndex); + encodedIndex = (abiMethod.args[0].type as ABIType).encode( + BigInt(index.toString()) + ); + result = (await this.readByMethod({ + abiMethod, + appArgs: [encodedIndex], + })) as BigNumber | null; + } catch (error) { + this.logger.debug( + `${ARC0072Contract.name}#${_functionName}: ${error.message}` + ); + + throw error; + } + + if (!result) { + throw new InvalidABIContractError( + this.appId.toString(), + `application "${this.appId.toString()}" not valid because the result returned "null"` + ); + } + + return result; + } + + /** + * Gets the token URI, the token metadata, for a given token ID. + * @param {string} tokenId - the token ID to check. + * @returns {string} the token URI of the ARC-0072 asset. + * @throws {InvalidABIContractError} if the application ID is not an ARC-0072 asset. + * @throws {ReadABIContractError} if there was a problem reading the data. + */ + public async tokenURI(tokenId: BigNumber): Promise { + const _functionName: string = 'tokenURI'; + let abiMethod: ABIMethod; + let encodedTokenId: Uint8Array; + let result: string | null; + + try { + abiMethod = this.abi.getMethodByName(ARC0072MethodEnum.TokenURI); + encodedTokenId = (abiMethod.args[0].type as ABIType).encode( + BigInt(tokenId.toString()) + ); + result = (await this.readByMethod({ + abiMethod, + appArgs: [encodedTokenId], + })) as string | null; + } catch (error) { + this.logger.debug( + `${ARC0072Contract.name}#${_functionName}: ${error.message}` + ); + + throw error; + } + + if (!result) { + throw new InvalidABIContractError( + this.appId.toString(), + `application "${this.appId.toString()}" not valid because the result returned "null"` + ); + } + + return this.trimNullBytes(result); + } + + /** + * Gets the total supply of the ARC-0072 asset. + * @returns {BigNumber} the total supply of the ARC-0072 asset. + * @throws {InvalidABIContractError} if the supplied application ID is not an ARC-0072 asset. + * @throws {ReadABIContractError} if there was a problem reading the data. + */ + public async totalSupply(): Promise { + const _functionName: string = 'totalSupply'; + let abiMethod: ABIMethod; + let result: BigNumber | null; + + try { + abiMethod = this.abi.getMethodByName(ARC0072MethodEnum.TotalSupply); + result = (await this.readByMethod({ + abiMethod, + })) as BigNumber | null; + } catch (error) { + this.logger.debug( + `${ARC0072Contract.name}#${_functionName}: ${error.message}` + ); + + throw error; + } + + if (!result) { + throw new InvalidABIContractError( + this.appId.toString(), + `application "${this.appId.toString()}" not valid because the result returned "null"` + ); + } + + return result; + } +} diff --git a/src/extension/contracts/ARC0072Contract/abi.json b/src/extension/contracts/ARC0072Contract/abi.json new file mode 100644 index 00000000..f057c835 --- /dev/null +++ b/src/extension/contracts/ARC0072Contract/abi.json @@ -0,0 +1,95 @@ +{ + "name": "ARC-72", + "description": "Smart Contract NFT Interface", + "methods": [ + { + "name": "arc72_balanceOf", + "args": [{ "type": "address" }], + "returns": { "type": "uint256" }, + "readonly": true + }, + { + "name": "arc72_getApproved", + "args": [{ "type": "uint256" }], + "returns": { "type": "address" }, + "readonly": true + }, + { + "name": "arc72_isApprovedForAll", + "args": [{ "type": "address" }, { "type": "address" }], + "returns": { "type": "bool" }, + "readonly": true + }, + { + "name": "arc72_ownerOf", + "args": [{ "type": "uint256" }], + "returns": { "type": "address" }, + "readonly": true + }, + { + "name": "arc72_tokenByIndex", + "args": [{ "type": "uint256" }], + "returns": { "type": "uint256" }, + "readonly": true + }, + { + "name": "arc72_totalSupply", + "args": [], + "returns": { "type": "uint256" }, + "readonly": true + }, + { + "name": "arc72_tokenURI", + "args": [{ "type": "uint256" }], + "returns": { "type": "byte[256]" }, + "readonly": true + }, + { + "name": "supportsInterface", + "args": [{ "type": "byte[4]" }], + "returns": { "type": "bool" }, + "readonly": true + }, + { + "name": "arc72_approve", + "args": [{ "type": "address" }, { "type": "uint256" }], + "returns": { "type": "void" } + }, + { + "name": "arc72_setApprovalForAll", + "args": [{ "type": "address" }, { "type": "bool" }], + "returns": { "type": "void" } + }, + { + "name": "arc72_transferFrom", + "args": [ + { "type": "address" }, + { "type": "address" }, + { "type": "uint256" } + ], + "returns": { "type": "void" } + } + ], + "events": [ + { + "name": "arc72_Approval", + "args": [ + { "type": "address" }, + { "type": "address" }, + { "type": "uint256" } + ] + }, + { + "name": "arc72_ApprovalForAll", + "args": [{ "type": "address" }, { "type": "address" }, { "type": "bool" }] + }, + { + "name": "arc72_Transfer", + "args": [ + { "type": "address" }, + { "type": "address" }, + { "type": "uint256" } + ] + } + ] +} diff --git a/src/extension/contracts/ARC0072Contract/enums/ARC0072MethodEnum.ts b/src/extension/contracts/ARC0072Contract/enums/ARC0072MethodEnum.ts new file mode 100644 index 00000000..13afa01b --- /dev/null +++ b/src/extension/contracts/ARC0072Contract/enums/ARC0072MethodEnum.ts @@ -0,0 +1,10 @@ +enum ARC0072MethodEnum { + BalanceOf = 'arc72_balanceOf', + OwnerOf = 'arc72_ownerOf', + TokenByIndex = 'arc72_tokenByIndex', + TokenURI = 'arc72_tokenURI', + TotalSupply = 'arc72_totalSupply', + TransferFrom = 'arc72_transferFrom', +} + +export default ARC0072MethodEnum; diff --git a/src/extension/contracts/ARC0072Contract/enums/index.ts b/src/extension/contracts/ARC0072Contract/enums/index.ts new file mode 100644 index 00000000..6ec1d802 --- /dev/null +++ b/src/extension/contracts/ARC0072Contract/enums/index.ts @@ -0,0 +1 @@ +export { default as ARC0072MethodEnum } from './ARC0072MethodEnum'; diff --git a/src/extension/contracts/ARC0072Contract/index.ts b/src/extension/contracts/ARC0072Contract/index.ts new file mode 100644 index 00000000..23a96022 --- /dev/null +++ b/src/extension/contracts/ARC0072Contract/index.ts @@ -0,0 +1,2 @@ +export { default } from './ARC0072Contract'; +export * from './enums'; diff --git a/src/extension/contracts/BaseContract/BaseContract.ts b/src/extension/contracts/BaseContract/BaseContract.ts index 3aa48bec..9115b688 100644 --- a/src/extension/contracts/BaseContract/BaseContract.ts +++ b/src/extension/contracts/BaseContract/BaseContract.ts @@ -180,6 +180,11 @@ export default class BaseContract { type = (abiMethod.returns.type as algosdk.ABIType).toString(); + // if we have an address, return as a string + if (type.includes('address')) { + return abiMethod.returns.type.decode(trimmedLog) as string; + } + // if we have bytes, return as a string if (type.includes('byte')) { return new TextDecoder().decode(trimmedLog); diff --git a/src/extension/enums/ARC0072AssetsThunkEnum.ts b/src/extension/enums/ARC0072AssetsThunkEnum.ts new file mode 100644 index 00000000..4ed3f9b1 --- /dev/null +++ b/src/extension/enums/ARC0072AssetsThunkEnum.ts @@ -0,0 +1,6 @@ +enum ARC0072AssetsThunkEnum { + FetchARC0072AssetsFromStorage = 'arc0072-assets/fetchARC0072AssetsFromStorage', + UpdateARC0072AssetInformation = 'arc0072-assets/updateARC0072AssetInformation', +} + +export default ARC0072AssetsThunkEnum; diff --git a/src/extension/enums/AssetTypeEnum.ts b/src/extension/enums/AssetTypeEnum.ts index db1fdcf1..a4d6ba80 100644 --- a/src/extension/enums/AssetTypeEnum.ts +++ b/src/extension/enums/AssetTypeEnum.ts @@ -1,4 +1,5 @@ enum AssetTypeEnum { + ARC0072 = 'arc0072', ARC0200 = 'arc200', Native = 'native', Standard = 'standard', diff --git a/src/extension/enums/ErrorCodeEnum.ts b/src/extension/enums/ErrorCodeEnum.ts index 0abe90c6..b0b2a935 100644 --- a/src/extension/enums/ErrorCodeEnum.ts +++ b/src/extension/enums/ErrorCodeEnum.ts @@ -4,6 +4,7 @@ enum ErrorCodeEnum { MalformedDataError = 1001, ParsingError = 1002, NetworkNotSelectedError = 1003, + NetworkConnectionError = 1004, // private key InvalidPasswordError = 2000, diff --git a/src/extension/enums/StoreNameEnum.ts b/src/extension/enums/StoreNameEnum.ts index 87c03959..2f2ba439 100644 --- a/src/extension/enums/StoreNameEnum.ts +++ b/src/extension/enums/StoreNameEnum.ts @@ -1,7 +1,8 @@ enum StoreNameEnum { Accounts = 'accounts', AddAsset = 'add-asset', - Arc200Assets = 'arc200-assets', + ARC0072Assets = 'arc0072-assets', + ARC0200Assets = 'arc0200-assets', Events = 'events', Messages = 'messages', Networks = 'networks', diff --git a/src/extension/enums/index.ts b/src/extension/enums/index.ts index 8ea9078b..4a8d68e3 100644 --- a/src/extension/enums/index.ts +++ b/src/extension/enums/index.ts @@ -2,8 +2,9 @@ export { default as AccountsThunkEnum } from './AccountsThunkEnum'; export { default as AccountTabEnum } from './AccountTabEnum'; export { default as AddAssetThunkEnum } from './AddAssetThunkEnum'; export { default as AppTypeEnum } from './AppTypeEnum'; -export { default as ARC0300AssetTypeEnum } from './ARC0300AssetTypeEnum'; +export { default as ARC0072AssetsThunkEnum } from './ARC0072AssetsThunkEnum'; export { default as ARC0200AssetsThunkEnum } from './ARC0200AssetsThunkEnum'; +export { default as ARC0300AssetTypeEnum } from './ARC0300AssetTypeEnum'; export { default as ARC0300AuthorityEnum } from './ARC0300AuthorityEnum'; export { default as ARC0300EncodingEnum } from './ARC0300EncodingEnum'; export { default as ARC0300PathEnum } from './ARC0300PathEnum'; diff --git a/src/extension/errors/NetworkConnectionError.ts b/src/extension/errors/NetworkConnectionError.ts new file mode 100644 index 00000000..af76d121 --- /dev/null +++ b/src/extension/errors/NetworkConnectionError.ts @@ -0,0 +1,10 @@ +// enums +import { ErrorCodeEnum } from '../enums'; + +// errors +import BaseExtensionError from './BaseExtensionError'; + +export default class NetworkConnectionError extends BaseExtensionError { + public readonly code: ErrorCodeEnum = ErrorCodeEnum.NetworkConnectionError; + public readonly name: string = 'NetworkConnectionError'; +} diff --git a/src/extension/errors/index.ts b/src/extension/errors/index.ts index 5250d958..ae2cca57 100644 --- a/src/extension/errors/index.ts +++ b/src/extension/errors/index.ts @@ -8,6 +8,7 @@ export { default as FailedToSendTransactionError } from './FailedToSendTransacti export { default as InvalidABIContractError } from './InvalidABIContractError'; export { default as InvalidPasswordError } from './InvalidPasswordError'; export { default as MalformedDataError } from './MalformedDataError'; +export { default as NetworkConnectionError } from './NetworkConnectionError'; export { default as NetworkNotSelectedError } from './NetworkNotSelectedError'; export { default as OfflineError } from './OfflineError'; export { default as ParsingError } from './ParsingError'; diff --git a/src/extension/features/accounts/utils/index.ts b/src/extension/features/accounts/utils/index.ts index 66a46038..0612828f 100644 --- a/src/extension/features/accounts/utils/index.ts +++ b/src/extension/features/accounts/utils/index.ts @@ -1,6 +1,3 @@ -export { default as fetchARC0200AssetHoldingWithDelay } from './fetchARC0200AssetHoldingWithDelay'; export { default as getInitialState } from './getInitialState'; -export { default as lookupAlgorandAccountTransactionsWithDelay } from './lookupAlgorandAccountTransactionsWithDelay'; -export { default as refreshTransactions } from './refreshTransactions'; export { default as updateAccountInformation } from './updateAccountInformation'; export { default as updateAccountTransactions } from './updateAccountTransactions'; diff --git a/src/extension/features/accounts/utils/updateAccountInformation.ts b/src/extension/features/accounts/utils/updateAccountInformation.ts index cf8993f3..de26796f 100644 --- a/src/extension/features/accounts/utils/updateAccountInformation.ts +++ b/src/extension/features/accounts/utils/updateAccountInformation.ts @@ -8,6 +8,7 @@ import type { IBaseOptions } from '@common/types'; import type { IAccountInformation, IAlgorandAccountInformation, + IARC0072AssetHolding, IARC0200AssetHolding, INetwork, } from '@extension/types'; @@ -15,8 +16,9 @@ import type { // utils import createAlgodClient from '@common/utils/createAlgodClient'; import algorandAccountInformationWithDelay from '@extension/utils/algorandAccountInformationWithDelay'; +import fetchARC0072AssetHoldingsWithDelay from '@extension/utils/fetchARC0072AssetHoldingsWithDelay'; +import fetchARC0200AssetHoldingWithDelay from '@extension/utils/fetchARC0200AssetHoldingWithDelay'; import mapAlgorandAccountInformationToAccount from '@extension/utils/mapAlgorandAccountInformationToAccount'; -import fetchARC0200AssetHoldingWithDelay from './fetchARC0200AssetHoldingWithDelay'; interface IOptions extends IBaseOptions { address: string; @@ -41,6 +43,7 @@ export default async function updateAccountInformation({ }: IOptions): Promise { const _functionName: string = 'updateAccountInformation'; let algorandAccountInformation: IAlgorandAccountInformation; + let arc0072AssetHoldings: IARC0072AssetHolding[]; let arc200AssetHoldings: IARC0200AssetHolding[]; let client: Algodv2; let updatedAt: Date; @@ -76,6 +79,12 @@ export default async function updateAccountInformation({ client, delay, }); + arc0072AssetHoldings = await fetchARC0072AssetHoldingsWithDelay({ + address, + delay, + logger, + network, + }); arc200AssetHoldings = await Promise.all( currentAccountInformation.arc200AssetHoldings.map( async (value) => @@ -100,6 +109,7 @@ export default async function updateAccountInformation({ algorandAccountInformation, { ...currentAccountInformation, + arc0072AssetHoldings, arc200AssetHoldings, }, updatedAt.getTime() diff --git a/src/extension/features/accounts/utils/updateAccountTransactions.ts b/src/extension/features/accounts/utils/updateAccountTransactions.ts index 990657fa..8eb89628 100644 --- a/src/extension/features/accounts/utils/updateAccountTransactions.ts +++ b/src/extension/features/accounts/utils/updateAccountTransactions.ts @@ -13,9 +13,9 @@ import { // utils import getIndexerClient from '@common/utils/getIndexerClient'; +import lookupAlgorandAccountTransactionsWithDelay from '@extension/utils/lookupAlgorandAccountTransactionsWithDelay'; import mapAlgorandTransactionToTransaction from '@extension/utils/mapAlgorandTransactionToTransaction'; -import lookupAlgorandAccountTransactionsWithDelay from './lookupAlgorandAccountTransactionsWithDelay'; -import refreshTransactions from './refreshTransactions'; +import refreshTransactions from '@extension/utils/refreshTransactions'; interface IOptions extends IBaseOptions { address: string; diff --git a/src/extension/features/add-asset/thunks/queryARC0200AssetThunk.ts b/src/extension/features/add-asset/thunks/queryARC0200AssetThunk.ts index bb4d29c2..c4ae696c 100644 --- a/src/extension/features/add-asset/thunks/queryARC0200AssetThunk.ts +++ b/src/extension/features/add-asset/thunks/queryARC0200AssetThunk.ts @@ -102,7 +102,7 @@ const queryARC0200AssetThunk: AsyncThunk< } arc200Assets = selectAssetsForNetwork( - getState().arc200Assets.items, + getState().arc0200Assets.items, selectedNetwork.genesisHash ); arc200AssetHoldings = diff --git a/src/extension/features/arc200-assets/index.ts b/src/extension/features/arc0072-assets/index.ts similarity index 100% rename from src/extension/features/arc200-assets/index.ts rename to src/extension/features/arc0072-assets/index.ts diff --git a/src/extension/features/arc0072-assets/slice.ts b/src/extension/features/arc0072-assets/slice.ts new file mode 100644 index 00000000..a45fd5be --- /dev/null +++ b/src/extension/features/arc0072-assets/slice.ts @@ -0,0 +1,92 @@ +import { createSlice, PayloadAction, Reducer } from '@reduxjs/toolkit'; + +// enums +import { StoreNameEnum } from '@extension/enums'; + +// thunks +import { + fetchARC0072AssetsFromStorageThunk, + updateARC0072AssetInformationThunk, +} from './thunks'; + +// types +import type { IARC0072Asset } from '@extension/types'; +import type { IState, IUpdateARC0072AssetInformationResult } from './types'; + +// utils +import { getInitialState } from './utils'; +import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; +import upsertItemsById from '@extension/utils/upsertItemsById'; + +const slice = createSlice({ + extraReducers: (builder) => { + /** fetch arc-0072 assets from storage **/ + builder.addCase( + fetchARC0072AssetsFromStorageThunk.fulfilled, + ( + state: IState, + action: PayloadAction> + ) => { + state.items = action.payload; + state.fetching = false; + } + ); + builder.addCase( + fetchARC0072AssetsFromStorageThunk.pending, + (state: IState) => { + state.fetching = true; + } + ); + builder.addCase( + fetchARC0072AssetsFromStorageThunk.rejected, + (state: IState) => { + state.fetching = false; + } + ); + /** update arc-0072 asset information **/ + builder.addCase( + updateARC0072AssetInformationThunk.fulfilled, + ( + state: IState, + action: PayloadAction + ) => { + const encodedGenesisHash: string = convertGenesisHashToHex( + action.payload.network.genesisHash + ).toUpperCase(); + const currentARC0072Assets: IARC0072Asset[] = state.items + ? state.items[encodedGenesisHash] + : []; + + state.items = { + ...state.items, + [encodedGenesisHash]: upsertItemsById( + currentARC0072Assets, + action.payload.arc0072Assets + ), + }; + state.updating = false; + } + ); + builder.addCase( + updateARC0072AssetInformationThunk.pending, + (state: IState) => { + state.updating = true; + } + ); + builder.addCase( + updateARC0072AssetInformationThunk.rejected, + (state: IState) => { + state.updating = false; + } + ); + }, + initialState: getInitialState(), + name: StoreNameEnum.ARC0072Assets, + reducers: { + noop: () => { + return; + }, + }, +}); + +export const reducer: Reducer = slice.reducer; diff --git a/src/extension/features/arc0072-assets/thunks/fetchARC0072AssetsFromStorageThunk.ts b/src/extension/features/arc0072-assets/thunks/fetchARC0072AssetsFromStorageThunk.ts new file mode 100644 index 00000000..e80cc8f2 --- /dev/null +++ b/src/extension/features/arc0072-assets/thunks/fetchARC0072AssetsFromStorageThunk.ts @@ -0,0 +1,55 @@ +import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; + +// enums +import { ARC0072AssetsThunkEnum } from '@extension/enums'; + +// services +import ARC0072AssetService from '@extension/services/ARC0072AssetService'; + +// types +import type { ILogger } from '@common/types'; +import type { + IARC0072Asset, + IBaseAsyncThunkConfig, + INetwork, +} from '@extension/types'; + +// utils +import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; + +const fetchARC0072AssetsFromStorageThunk: AsyncThunk< + Record, // return + undefined, // args + IBaseAsyncThunkConfig +> = createAsyncThunk< + Record, + undefined, + IBaseAsyncThunkConfig +>( + ARC0072AssetsThunkEnum.FetchARC0072AssetsFromStorage, + async (_, { getState }) => { + const logger: ILogger = getState().system.logger; + const networks: INetwork[] = getState().networks.items; + const assetService: ARC0072AssetService = new ARC0072AssetService({ + logger, + }); + const assetItems: Record = {}; + + logger.debug( + `${ARC0072AssetsThunkEnum.FetchARC0072AssetsFromStorage}: fetching arc-0072 assets from storage` + ); + + await Promise.all( + networks.map( + async (network) => + (assetItems[ + convertGenesisHashToHex(network.genesisHash).toUpperCase() + ] = await assetService.getByGenesisHash(network.genesisHash)) + ) + ); + + return assetItems; + } +); + +export default fetchARC0072AssetsFromStorageThunk; diff --git a/src/extension/features/arc0072-assets/thunks/index.ts b/src/extension/features/arc0072-assets/thunks/index.ts new file mode 100644 index 00000000..d77a6cd3 --- /dev/null +++ b/src/extension/features/arc0072-assets/thunks/index.ts @@ -0,0 +1,2 @@ +export { default as fetchARC0072AssetsFromStorageThunk } from './fetchARC0072AssetsFromStorageThunk'; +export { default as updateARC0072AssetInformationThunk } from './updateARC0072AssetInformationThunk'; diff --git a/src/extension/features/arc0072-assets/thunks/updateARC0072AssetInformationThunk.ts b/src/extension/features/arc0072-assets/thunks/updateARC0072AssetInformationThunk.ts new file mode 100644 index 00000000..8197a8d7 --- /dev/null +++ b/src/extension/features/arc0072-assets/thunks/updateARC0072AssetInformationThunk.ts @@ -0,0 +1,91 @@ +import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; + +// constants +import { NODE_REQUEST_DELAY } from '@extension/constants'; + +// enums +import { ARC0072AssetsThunkEnum } from '@extension/enums'; + +// services +import ARC0072AssetService from '@extension/services/ARC0072AssetService'; + +// types +import type { ILogger } from '@common/types'; +import type { IARC0072Asset, IBaseAsyncThunkConfig } from '@extension/types'; +import type { + IUpdateARC0072AssetInformationPayload, + IUpdateARC0072AssetInformationResult, +} from '../types'; + +// utils +import updateARC0072AssetInformationById from '@extension/utils/updateARC0072AssetInformationById'; +import upsertItemsById from '@extension/utils/upsertItemsById'; + +const updateARC0072AssetInformationThunk: AsyncThunk< + IUpdateARC0072AssetInformationResult, // return + IUpdateARC0072AssetInformationPayload, // args + IBaseAsyncThunkConfig +> = createAsyncThunk< + IUpdateARC0072AssetInformationResult, + IUpdateARC0072AssetInformationPayload, + IBaseAsyncThunkConfig +>( + ARC0072AssetsThunkEnum.UpdateARC0072AssetInformation, + async ({ ids, network }, { getState }) => { + const logger: ILogger = getState().system.logger; + let asset: IARC0072Asset | null; + let currentAssets: IARC0072Asset[]; + let id: string; + let assetService: ARC0072AssetService; + let updatedAssets: IARC0072Asset[] = []; + + // get the information for each asset and add it to the array + for (let i: number = 0; i < ids.length; i++) { + id = ids[i]; + + try { + asset = await updateARC0072AssetInformationById(id, { + delay: i * NODE_REQUEST_DELAY, // delay each request by 100ms from the last one, see https://algonode.io/api/#limits + logger, + network, + }); + + if (!asset) { + continue; + } + + logger.debug( + `${ARC0072AssetsThunkEnum.UpdateARC0072AssetInformation}: successfully updated asset information for arc-0072 asset "${id}" on "${network.genesisId}"` + ); + + updatedAssets.push(asset); + } catch (error) { + logger.error( + `${ARC0072AssetsThunkEnum.UpdateARC0072AssetInformation}: failed to get asset information for arc-0072 asset "${id}" on ${network.genesisId}: ${error.message}` + ); + } + } + + assetService = new ARC0072AssetService({ + logger, + }); + currentAssets = await assetService.getByGenesisHash(network.genesisHash); + + logger.debug( + `${ARC0072AssetsThunkEnum.UpdateARC0072AssetInformation}: saving new asset information for network "${network.genesisId}" to storage` + ); + + // update the storage with the new asset information + await assetService.saveByGenesisHash( + network.genesisHash, + upsertItemsById(currentAssets, updatedAssets) + ); + + return { + arc0072Assets: updatedAssets, + network, + }; + } +); + +export default updateARC0072AssetInformationThunk; diff --git a/src/extension/features/arc0072-assets/types/IState.ts b/src/extension/features/arc0072-assets/types/IState.ts new file mode 100644 index 00000000..3d4909c2 --- /dev/null +++ b/src/extension/features/arc0072-assets/types/IState.ts @@ -0,0 +1,18 @@ +// types +import type { IARC0072Asset } from '@extension/types'; + +/** + * @property {boolean} fetching - true when ARC-0072 assets are being fetched from storage. + * @property {Record | null} items - the ARC-0072 assets for each network, indexed by the + * hex encoded genesis hash. + * @property {boolean} saving - true when an asset is being saved to storage. + * @property {boolean} updating - true when remote ARC-0072 asset information is being updated. + */ +interface IState { + fetching: boolean; + items: Record | null; + saving: boolean; + updating: boolean; +} + +export default IState; diff --git a/src/extension/features/arc0072-assets/types/IUpdateARC0072AssetInformationPayload.ts b/src/extension/features/arc0072-assets/types/IUpdateARC0072AssetInformationPayload.ts new file mode 100644 index 00000000..47175dda --- /dev/null +++ b/src/extension/features/arc0072-assets/types/IUpdateARC0072AssetInformationPayload.ts @@ -0,0 +1,13 @@ +// types +import type { INetwork } from '@extension/types'; + +/** + * @property {string[]} ids - the app IDs of the ARC-0072 assets to fetch information for. + * @property {INetwork} network - the network to fetch the ARC-0072 assets from. + */ +interface IUpdateARC0072AssetInformationPayload { + ids: string[]; + network: INetwork; +} + +export default IUpdateARC0072AssetInformationPayload; diff --git a/src/extension/features/arc0072-assets/types/IUpdateARC0072AssetInformationResult.ts b/src/extension/features/arc0072-assets/types/IUpdateARC0072AssetInformationResult.ts new file mode 100644 index 00000000..dd341e1d --- /dev/null +++ b/src/extension/features/arc0072-assets/types/IUpdateARC0072AssetInformationResult.ts @@ -0,0 +1,13 @@ +// types +import type { IARC0072Asset, INetwork } from '@extension/types'; + +/** + * @property {IARC0072Asset[]} arc0072Assets - the updated ARC-0072 assets. + * @property {INetwork} network - the network. + */ +interface IUpdateARC0072AssetInformationResult { + arc0072Assets: IARC0072Asset[]; + network: INetwork; +} + +export default IUpdateARC0072AssetInformationResult; diff --git a/src/extension/features/arc0072-assets/types/index.ts b/src/extension/features/arc0072-assets/types/index.ts new file mode 100644 index 00000000..65b453d4 --- /dev/null +++ b/src/extension/features/arc0072-assets/types/index.ts @@ -0,0 +1,3 @@ +export type { default as IState } from './IState'; +export type { default as IUpdateARC0072AssetInformationPayload } from './IUpdateARC0072AssetInformationPayload'; +export type { default as IUpdateARC0072AssetInformationResult } from './IUpdateARC0072AssetInformationResult'; diff --git a/src/extension/features/arc0072-assets/utils/getInitialState.ts b/src/extension/features/arc0072-assets/utils/getInitialState.ts new file mode 100644 index 00000000..c1eb46f1 --- /dev/null +++ b/src/extension/features/arc0072-assets/utils/getInitialState.ts @@ -0,0 +1,11 @@ +// types +import type { IState } from '../types'; + +export default function getInitialState(): IState { + return { + fetching: false, + items: null, + saving: false, + updating: false, + }; +} diff --git a/src/extension/features/arc200-assets/utils/index.ts b/src/extension/features/arc0072-assets/utils/index.ts similarity index 100% rename from src/extension/features/arc200-assets/utils/index.ts rename to src/extension/features/arc0072-assets/utils/index.ts diff --git a/src/extension/features/arc0200-assets/index.ts b/src/extension/features/arc0200-assets/index.ts new file mode 100644 index 00000000..0741f6b4 --- /dev/null +++ b/src/extension/features/arc0200-assets/index.ts @@ -0,0 +1,4 @@ +export * from './slice'; +export * from './thunks'; +export * from './types'; +export * from './utils'; diff --git a/src/extension/features/arc200-assets/slice.ts b/src/extension/features/arc0200-assets/slice.ts similarity index 98% rename from src/extension/features/arc200-assets/slice.ts rename to src/extension/features/arc0200-assets/slice.ts index 23633f7c..319d37a8 100644 --- a/src/extension/features/arc200-assets/slice.ts +++ b/src/extension/features/arc0200-assets/slice.ts @@ -84,7 +84,7 @@ const slice = createSlice({ ); }, initialState: getInitialState(), - name: StoreNameEnum.Arc200Assets, + name: StoreNameEnum.ARC0200Assets, reducers: { noop: () => { return; diff --git a/src/extension/features/arc200-assets/thunks/fetchARC0200AssetsFromStorageThunk.ts b/src/extension/features/arc0200-assets/thunks/fetchARC0200AssetsFromStorageThunk.ts similarity index 100% rename from src/extension/features/arc200-assets/thunks/fetchARC0200AssetsFromStorageThunk.ts rename to src/extension/features/arc0200-assets/thunks/fetchARC0200AssetsFromStorageThunk.ts diff --git a/src/extension/features/arc200-assets/thunks/index.ts b/src/extension/features/arc0200-assets/thunks/index.ts similarity index 100% rename from src/extension/features/arc200-assets/thunks/index.ts rename to src/extension/features/arc0200-assets/thunks/index.ts diff --git a/src/extension/features/arc200-assets/thunks/updateARC0200AssetInformationThunk.ts b/src/extension/features/arc0200-assets/thunks/updateARC0200AssetInformationThunk.ts similarity index 100% rename from src/extension/features/arc200-assets/thunks/updateARC0200AssetInformationThunk.ts rename to src/extension/features/arc0200-assets/thunks/updateARC0200AssetInformationThunk.ts diff --git a/src/extension/features/arc200-assets/types/IARC0200AssetsState.ts b/src/extension/features/arc0200-assets/types/IARC0200AssetsState.ts similarity index 74% rename from src/extension/features/arc200-assets/types/IARC0200AssetsState.ts rename to src/extension/features/arc0200-assets/types/IARC0200AssetsState.ts index 7f1fecb1..4feb2d5e 100644 --- a/src/extension/features/arc200-assets/types/IARC0200AssetsState.ts +++ b/src/extension/features/arc0200-assets/types/IARC0200AssetsState.ts @@ -1,9 +1,9 @@ // types -import { IARC0200Asset } from '@extension/types'; +import type { IARC0200Asset } from '@extension/types'; /** * @property {boolean} fetching - true when ARC200 assets are being fetched from storage. - * @property {Record | null} items - the ARC200 assets for each network, indexed by the + * @property {Record | null} items - the ARC200 assets for each network, indexed by the * hex encoded genesis hash. * @property {boolean} saving - true when an asset is being saved to storage. * @property {boolean} updating - true when remote ARC200 asset information is being updated. diff --git a/src/extension/features/arc200-assets/types/IUpdateARC0200AssetInformationPayload.ts b/src/extension/features/arc0200-assets/types/IUpdateARC0200AssetInformationPayload.ts similarity index 100% rename from src/extension/features/arc200-assets/types/IUpdateARC0200AssetInformationPayload.ts rename to src/extension/features/arc0200-assets/types/IUpdateARC0200AssetInformationPayload.ts diff --git a/src/extension/features/arc200-assets/types/IUpdateARC0200AssetInformationResult.ts b/src/extension/features/arc0200-assets/types/IUpdateARC0200AssetInformationResult.ts similarity index 100% rename from src/extension/features/arc200-assets/types/IUpdateARC0200AssetInformationResult.ts rename to src/extension/features/arc0200-assets/types/IUpdateARC0200AssetInformationResult.ts diff --git a/src/extension/features/arc200-assets/types/index.ts b/src/extension/features/arc0200-assets/types/index.ts similarity index 100% rename from src/extension/features/arc200-assets/types/index.ts rename to src/extension/features/arc0200-assets/types/index.ts diff --git a/src/extension/features/arc200-assets/utils/getInitialState.ts b/src/extension/features/arc0200-assets/utils/getInitialState.ts similarity index 100% rename from src/extension/features/arc200-assets/utils/getInitialState.ts rename to src/extension/features/arc0200-assets/utils/getInitialState.ts diff --git a/src/extension/features/arc0200-assets/utils/index.ts b/src/extension/features/arc0200-assets/utils/index.ts new file mode 100644 index 00000000..85e2c689 --- /dev/null +++ b/src/extension/features/arc0200-assets/utils/index.ts @@ -0,0 +1 @@ +export { default as getInitialState } from './getInitialState'; diff --git a/src/extension/features/networks/utils/updateTransactionParams.ts b/src/extension/features/networks/utils/updateTransactionParams.ts index d0e432b2..02cd0aae 100644 --- a/src/extension/features/networks/utils/updateTransactionParams.ts +++ b/src/extension/features/networks/utils/updateTransactionParams.ts @@ -2,15 +2,15 @@ import { NETWORK_TRANSACTION_PARAMS_ANTIQUATED_TIMEOUT } from '@extension/constants'; // types -import { IBaseOptions } from '@common/types'; -import { +import type { IBaseOptions } from '@common/types'; +import type { IAlgorandTransactionParams, INetworkWithTransactionParams, INode, } from '@extension/types'; // utils -import getRandomNode from '@common/utils/getRandomNode'; +import getRandomItem from '@common/utils/getRandomItem'; /** * Fetches the transaction params for a give network. @@ -22,6 +22,7 @@ export default async function updateTransactionParams( network: INetworkWithTransactionParams, { logger }: IBaseOptions ): Promise { + const _functionName: string = 'updateTransactionParams'; let algorandTransactionParams: IAlgorandTransactionParams; let algod: INode; let response: Response; @@ -33,22 +34,20 @@ export default async function updateTransactionParams( network.updatedAt + NETWORK_TRANSACTION_PARAMS_ANTIQUATED_TIMEOUT > new Date().getTime() ) { - logger && - logger.debug( - `${updateTransactionParams.name}: last updated "${new Date( - network.updatedAt - ).toString()}", skipping` - ); + logger?.debug( + `${_functionName}: last updated "${new Date( + network.updatedAt + ).toString()}", skipping` + ); return network; } - algod = getRandomNode(network.algods); + algod = getRandomItem(network.algods); - logger && - logger.debug( - `${updateTransactionParams.name}: updating transaction params for network "${network.genesisId}"` - ); + logger?.debug( + `${_functionName}: updating transaction params for network "${network.genesisId}"` + ); try { // use rest api as the @@ -62,24 +61,20 @@ export default async function updateTransactionParams( // check if the genesis hashes match if (algorandTransactionParams['genesis-hash'] !== network.genesisHash) { - logger && - logger.debug( - `${updateTransactionParams.name}: requested network genesis hash "${network.genesisHash}" does not match the returned genesis hash "${algorandTransactionParams['genesis-hash']}", ignoring` - ); + logger?.debug( + `${_functionName}: requested network genesis hash "${network.genesisHash}" does not match the returned genesis hash "${algorandTransactionParams['genesis-hash']}", ignoring` + ); return network; } updatedAt = new Date(); - logger && - logger.debug( - `${ - updateTransactionParams.name - }: successfully updated transaction params for network "${ - network.genesisId - }" at "${updatedAt.toString()}"` - ); + logger?.debug( + `${_functionName}: successfully updated transaction params for network "${ + network.genesisId + }" at "${updatedAt.toString()}"` + ); return { ...network, @@ -88,10 +83,9 @@ export default async function updateTransactionParams( updatedAt: updatedAt.getTime(), }; } catch (error) { - logger && - logger.error( - `${updateTransactionParams.name}: failed to get transaction params for network "${network.genesisId}": ${error.message}` - ); + logger?.error( + `${_functionName}: failed to get transaction params for network "${network.genesisId}": ${error.message}` + ); return network; } diff --git a/src/extension/features/settings/thunks/saveSettingsToStorageThunk.ts b/src/extension/features/settings/thunks/saveSettingsToStorageThunk.ts index 634c2525..82188f01 100644 --- a/src/extension/features/settings/thunks/saveSettingsToStorageThunk.ts +++ b/src/extension/features/settings/thunks/saveSettingsToStorageThunk.ts @@ -33,6 +33,7 @@ const saveSettingsToStorageThunk: AsyncThunk< }); let selectedNetwork: INetworkWithTransactionParams | null = selectNetworkFromSettings(networks, settings); + let encodedGenesisHash: string; // if the beta/main-net has been disallowed and the selected network is one of the disallowed, set it to a test one if ( @@ -43,10 +44,14 @@ const saveSettingsToStorageThunk: AsyncThunk< selectedNetwork.type === NetworkTypeEnum.Stable) ) { selectedNetwork = selectDefaultNetwork(networks); - - settings.general.preferredBlockExplorerIds[ - convertGenesisHashToHex(selectedNetwork.genesisHash).toUpperCase() - ] = selectedNetwork.explorers[0]?.id || null; + encodedGenesisHash = convertGenesisHashToHex( + selectedNetwork.genesisHash + ).toUpperCase(); + + settings.general.preferredBlockExplorerIds[encodedGenesisHash] = + selectedNetwork.blockExplorers[0]?.id || null; + settings.general.preferredNFTExplorerIds[encodedGenesisHash] = + selectedNetwork.nftExplorers[0]?.id || null; settings.general.selectedNetworkGenesisHash = selectedNetwork.genesisHash; } diff --git a/src/extension/features/settings/utils/getInitialState.ts b/src/extension/features/settings/utils/getInitialState.ts index 3a577edd..561856b0 100644 --- a/src/extension/features/settings/utils/getInitialState.ts +++ b/src/extension/features/settings/utils/getInitialState.ts @@ -18,6 +18,7 @@ export default function getInitialState(): ISettingsState { fetching: false, general: { preferredBlockExplorerIds: {}, + preferredNFTExplorerIds: {}, selectedNetworkGenesisHash: null, }, saving: false, diff --git a/src/extension/hooks/useOnNewAssets/useOnNewAssets.ts b/src/extension/hooks/useOnNewAssets/useOnNewAssets.ts index 6f05b74f..27defdfa 100644 --- a/src/extension/hooks/useOnNewAssets/useOnNewAssets.ts +++ b/src/extension/hooks/useOnNewAssets/useOnNewAssets.ts @@ -2,22 +2,26 @@ import { useEffect } from 'react'; import { useDispatch } from 'react-redux'; // features -import { updateARC0200AssetInformationThunk } from '@extension/features/arc200-assets'; +import { updateARC0072AssetInformationThunk } from '@extension/features/arc0072-assets'; +import { updateARC0200AssetInformationThunk } from '@extension/features/arc0200-assets'; import { updateStandardAssetInformationThunk } from '@extension/features/standard-assets'; // selectors import { useSelectAccounts, + useSelectARC0072AssetsBySelectedNetwork, useSelectARC0200AssetsBySelectedNetwork, useSelectSelectedNetwork, useSelectStandardAssetsBySelectedNetwork, } from '@extension/selectors'; // types -import { +import type { IAccount, IAccountInformation, IAppThunkDispatch, + IARC0072Asset, + IARC0072AssetHolding, IARC0200Asset, IARC0200AssetHolding, INetwork, @@ -35,15 +39,50 @@ export default function useOnNewAssets(): void { const dispatch: IAppThunkDispatch = useDispatch(); // selectors const accounts: IAccount[] = useSelectAccounts(); - const arc200Assets: IARC0200Asset[] = + const arc0072Assets: IARC0072Asset[] = + useSelectARC0072AssetsBySelectedNetwork(); + const arc0200Assets: IARC0200Asset[] = useSelectARC0200AssetsBySelectedNetwork(); const standardAssets: IStandardAsset[] = useSelectStandardAssetsBySelectedNetwork(); const selectedNetwork: INetwork | null = useSelectSelectedNetwork(); - // check for any new arc200 assets + // check for any new arc-0072 assets + useEffect(() => { + if (accounts.length > 0 && arc0072Assets && selectedNetwork) { + accounts.forEach((account) => { + const encodedGenesisHash: string = convertGenesisHashToHex( + selectedNetwork.genesisHash + ).toUpperCase(); + const accountInformation: IAccountInformation | null = + account.networkInformation[encodedGenesisHash] || null; + let newARC0072AssetHoldings: IARC0072AssetHolding[]; + + if (accountInformation) { + // filter out any new arc-0072 assets + newARC0072AssetHoldings = + accountInformation.arc0072AssetHoldings.filter( + (assetHolding) => + !arc0072Assets.some((value) => value.id === assetHolding.id) + ); + + // if we have any new arc-0072 assets, update the information + if (newARC0072AssetHoldings.length > 0) { + dispatch( + updateARC0072AssetInformationThunk({ + ids: newARC0072AssetHoldings.map((value) => value.id), + network: selectedNetwork, + }) + ); + } + } + }); + } + }, [accounts]); + + // check for any new arc-0200 assets useEffect(() => { - if (accounts.length > 0 && arc200Assets && selectedNetwork) { + if (accounts.length > 0 && arc0200Assets && selectedNetwork) { accounts.forEach((account) => { const encodedGenesisHash: string = convertGenesisHashToHex( selectedNetwork.genesisHash @@ -53,14 +92,14 @@ export default function useOnNewAssets(): void { let newARC0200AssetHoldings: IARC0200AssetHolding[]; if (accountInformation) { - // filter out any new arc200 assets + // filter out any new arc-0200 assets newARC0200AssetHoldings = accountInformation.arc200AssetHoldings.filter( (assetHolding) => - !arc200Assets.some((value) => value.id === assetHolding.id) + !arc0200Assets.some((value) => value.id === assetHolding.id) ); - // if we have any new arc200 assets, update the information + // if we have any new arc-0200 assets, update the information if (newARC0200AssetHoldings.length > 0) { dispatch( updateARC0200AssetInformationThunk({ diff --git a/src/extension/hooks/useUpdateARC0200Assets/useUpdateARC0200Assets.ts b/src/extension/hooks/useUpdateARC0200Assets/useUpdateARC0200Assets.ts index a60f6134..dad147cd 100644 --- a/src/extension/hooks/useUpdateARC0200Assets/useUpdateARC0200Assets.ts +++ b/src/extension/hooks/useUpdateARC0200Assets/useUpdateARC0200Assets.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; // features -import { updateARC0200AssetInformationThunk } from '@extension/features/arc200-assets'; +import { updateARC0200AssetInformationThunk } from '@extension/features/arc0200-assets'; // selectors import { diff --git a/src/extension/images/placeholder_nft.png b/src/extension/images/placeholder_nft.png new file mode 100644 index 00000000..b46ac4ce Binary files /dev/null and b/src/extension/images/placeholder_nft.png differ diff --git a/src/extension/modals/AddAssetModal/AddAssetModal.tsx b/src/extension/modals/AddAssetModal/AddAssetModal.tsx index 0edf02b0..c4885299 100644 --- a/src/extension/modals/AddAssetModal/AddAssetModal.tsx +++ b/src/extension/modals/AddAssetModal/AddAssetModal.tsx @@ -79,7 +79,7 @@ import { useSelectAddAssetStandardAssets, useSelectLogger, useSelectPasswordLockPassword, - useSelectPreferredBlockExplorer, + useSelectSettingsPreferredBlockExplorer, useSelectSelectedNetwork, useSelectSettings, } from '@extension/selectors'; @@ -95,7 +95,7 @@ import type { IAppThunkDispatchReturn, IARC0200Asset, IAssetTypes, - IExplorer, + IBlockExplorer, INetworkWithTransactionParams, ISettings, IStandardAsset, @@ -120,7 +120,8 @@ const AddAssetModal: FC = ({ onClose }: IProps) => { const accounts: IAccount[] = useSelectAccounts(); const arc200Assets: IARC0200Asset[] = useSelectAddAssetARC0200Assets(); const confirming: boolean = useSelectAddAssetConfirming(); - const explorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const explorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); const fetching: boolean = useSelectAddAssetFetching(); const logger: ILogger = useSelectLogger(); const passwordLockPassword: string | null = useSelectPasswordLockPassword(); diff --git a/src/extension/modals/AddAssetModal/AddAssetModalARC0200AssetSummaryContent.tsx b/src/extension/modals/AddAssetModal/AddAssetModalARC0200AssetSummaryContent.tsx index cbb6ca00..ff342a38 100644 --- a/src/extension/modals/AddAssetModal/AddAssetModalARC0200AssetSummaryContent.tsx +++ b/src/extension/modals/AddAssetModal/AddAssetModalARC0200AssetSummaryContent.tsx @@ -26,7 +26,7 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; // types import { IARC0200Asset, - IExplorer, + IBlockExplorer, INetworkWithTransactionParams, } from '@extension/types'; @@ -36,7 +36,7 @@ import formatCurrencyUnit from '@common/utils/formatCurrencyUnit'; interface IProps { asset: IARC0200Asset; - explorer: IExplorer | null; + explorer: IBlockExplorer | null; network: INetworkWithTransactionParams; } diff --git a/src/extension/modals/AddAssetModal/AddAssetModalStandardAssetSummaryContent.tsx b/src/extension/modals/AddAssetModal/AddAssetModalStandardAssetSummaryContent.tsx index a69b8045..9e9ee36e 100644 --- a/src/extension/modals/AddAssetModal/AddAssetModalStandardAssetSummaryContent.tsx +++ b/src/extension/modals/AddAssetModal/AddAssetModalStandardAssetSummaryContent.tsx @@ -36,7 +36,7 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; // types import { IAccount, - IExplorer, + IBlockExplorer, INetworkWithTransactionParams, IStandardAsset, } from '@extension/types'; @@ -50,7 +50,7 @@ import isAccountKnown from '@extension/utils/isAccountKnown'; interface IProps { accounts: IAccount[]; asset: IStandardAsset; - explorer: IExplorer | null; + explorer: IBlockExplorer | null; network: INetworkWithTransactionParams; } diff --git a/src/extension/modals/SignTxnsModal/ApplicationTransactionContent.tsx b/src/extension/modals/SignTxnsModal/ApplicationTransactionContent.tsx index a6cbee36..2c2657e2 100644 --- a/src/extension/modals/SignTxnsModal/ApplicationTransactionContent.tsx +++ b/src/extension/modals/SignTxnsModal/ApplicationTransactionContent.tsx @@ -22,7 +22,7 @@ import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import useSubTextColor from '@extension/hooks/useSubTextColor'; // types -import type { IExplorer, INetwork } from '@extension/types'; +import type { IBlockExplorer, INetwork } from '@extension/types'; import type { ICondensedProps } from './types'; // utils @@ -31,7 +31,7 @@ import parseTransactionType from '@extension/utils/parseTransactionType'; interface IProps { condensed?: ICondensedProps; - explorer: IExplorer | null; + explorer: IBlockExplorer | null; network: INetwork; transaction: Transaction; } diff --git a/src/extension/modals/SignTxnsModal/AssetConfigTransactionContent.tsx b/src/extension/modals/SignTxnsModal/AssetConfigTransactionContent.tsx index 209f4830..51376d61 100644 --- a/src/extension/modals/SignTxnsModal/AssetConfigTransactionContent.tsx +++ b/src/extension/modals/SignTxnsModal/AssetConfigTransactionContent.tsx @@ -30,7 +30,7 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; import type { IAccount, IStandardAsset, - IExplorer, + IBlockExplorer, INetwork, } from '@extension/types'; import type { ICondensedProps } from './types'; @@ -42,7 +42,7 @@ import parseTransactionType from '@extension/utils/parseTransactionType'; interface IProps { asset: IStandardAsset | null; condensed?: ICondensedProps; - explorer: IExplorer | null; + explorer: IBlockExplorer | null; fromAccount: IAccount | null; loading?: boolean; network: INetwork; diff --git a/src/extension/modals/SignTxnsModal/AssetFreezeTransactionContent.tsx b/src/extension/modals/SignTxnsModal/AssetFreezeTransactionContent.tsx index c9ba5508..a9fb208b 100644 --- a/src/extension/modals/SignTxnsModal/AssetFreezeTransactionContent.tsx +++ b/src/extension/modals/SignTxnsModal/AssetFreezeTransactionContent.tsx @@ -40,7 +40,7 @@ import type { IAccountInformation, IStandardAsset, IStandardAssetHolding, - IExplorer, + IBlockExplorer, INetwork, } from '@extension/types'; import type { ICondensedProps } from './types'; @@ -53,7 +53,7 @@ import parseTransactionType from '@extension/utils/parseTransactionType'; interface IProps { asset: IStandardAsset | null; condensed?: ICondensedProps; - explorer: IExplorer | null; + explorer: IBlockExplorer | null; fromAccount: IAccount | null; loading?: boolean; network: INetwork; diff --git a/src/extension/modals/SignTxnsModal/AssetTransferTransactionContent.tsx b/src/extension/modals/SignTxnsModal/AssetTransferTransactionContent.tsx index 3205691b..c3ab32ff 100644 --- a/src/extension/modals/SignTxnsModal/AssetTransferTransactionContent.tsx +++ b/src/extension/modals/SignTxnsModal/AssetTransferTransactionContent.tsx @@ -24,7 +24,7 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; import type { IAccount, IStandardAsset, - IExplorer, + IBlockExplorer, INetwork, } from '@extension/types'; import type { ICondensedProps } from './types'; @@ -38,7 +38,7 @@ import parseTransactionType from '@extension/utils/parseTransactionType'; interface IProps { asset: IStandardAsset | null; condensed?: ICondensedProps; - explorer: IExplorer | null; + explorer: IBlockExplorer | null; fromAccount: IAccount | null; loading?: boolean; network: INetwork; diff --git a/src/extension/modals/SignTxnsModal/AtomicTransactionsContent.tsx b/src/extension/modals/SignTxnsModal/AtomicTransactionsContent.tsx index 13289f03..9bbcc3cb 100644 --- a/src/extension/modals/SignTxnsModal/AtomicTransactionsContent.tsx +++ b/src/extension/modals/SignTxnsModal/AtomicTransactionsContent.tsx @@ -34,7 +34,7 @@ import { useSelectAccounts, useSelectLogger, useSelectNetworks, - useSelectPreferredBlockExplorer, + useSelectSettingsPreferredBlockExplorer, useSelectStandardAssetsByGenesisHash, useSelectUpdatingStandardAssets, } from '@extension/selectors'; @@ -47,7 +47,7 @@ import type { ILogger } from '@common/types'; import type { IAccount, IAccountInformation, - IExplorer, + IBlockExplorer, INetwork, IStandardAsset, } from '@extension/types'; @@ -72,7 +72,8 @@ const AtomicTransactionsContent: FC = ({ transactions }: IProps) => { const accounts: IAccount[] = useSelectAccounts(); const logger: ILogger = useSelectLogger(); const networks: INetwork[] = useSelectNetworks(); - const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const preferredExplorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); const standardAssets: IStandardAsset[] = useSelectStandardAssetsByGenesisHash(genesisHash); const updatingStandardAssets: boolean = useSelectUpdatingStandardAssets(); @@ -89,9 +90,11 @@ const AtomicTransactionsContent: FC = ({ transactions }: IProps) => { const computedGroupId: string = encodeBase64(computeGroupId(transactions)); const network: INetwork | null = networks.find((value) => value.genesisHash === genesisHash) || null; - const explorer: IExplorer | null = - network?.explorers.find((value) => value.id === preferredExplorer?.id) || - network?.explorers[0] || + const explorer: IBlockExplorer | null = + network?.blockExplorers.find( + (value) => value.id === preferredExplorer?.id + ) || + network?.blockExplorers[0] || null; // get the preferred explorer, if it exists in the network, otherwise get the default one // handlers const handleToggleAccordion = (accordionIndex: number) => (open: boolean) => { diff --git a/src/extension/modals/SignTxnsModal/SignTxnsAddressItem.tsx b/src/extension/modals/SignTxnsModal/SignTxnsAddressItem.tsx index 9d882444..07626a3a 100644 --- a/src/extension/modals/SignTxnsModal/SignTxnsAddressItem.tsx +++ b/src/extension/modals/SignTxnsModal/SignTxnsAddressItem.tsx @@ -16,14 +16,14 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; // selectors import { useSelectAccounts, - useSelectPreferredBlockExplorer, + useSelectSettingsPreferredBlockExplorer, } from '@extension/selectors'; // services import AccountService from '@extension/services/AccountService'; // types -import { IAccount, IExplorer, INetwork } from '@extension/types'; +import { IAccount, IBlockExplorer, INetwork } from '@extension/types'; interface IProps extends StackProps { address: string; @@ -42,13 +42,16 @@ const SignTxnsAddressItem: FC = ({ const { t } = useTranslation(); // selectors const accounts: IAccount[] = useSelectAccounts(); - const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const preferredExplorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); // hooks const defaultTextColor: string = useDefaultTextColor(); const subTextColor: string = useSubTextColor(); - const explorer: IExplorer | null = - network.explorers.find((value) => value.id === preferredExplorer?.id) || - network.explorers[0] || + const explorer: IBlockExplorer | null = + network.blockExplorers.find( + (value) => value.id === preferredExplorer?.id + ) || + network.blockExplorers[0] || null; // get the preferred explorer, if it exists in the networks, otherwise get the default one const account: IAccount | null = accounts.find( diff --git a/src/extension/modals/SignTxnsModal/SignTxnsChangeAddressItem.tsx b/src/extension/modals/SignTxnsModal/SignTxnsChangeAddressItem.tsx index 7adb9611..16ff94fc 100644 --- a/src/extension/modals/SignTxnsModal/SignTxnsChangeAddressItem.tsx +++ b/src/extension/modals/SignTxnsModal/SignTxnsChangeAddressItem.tsx @@ -17,7 +17,7 @@ import useTextBackgroundColor from '@extension/hooks/useTextBackgroundColor'; // selectors import { useSelectAccounts, - useSelectPreferredBlockExplorer, + useSelectSettingsPreferredBlockExplorer, } from '@extension/selectors'; // services @@ -27,7 +27,7 @@ import AccountService from '@extension/services/AccountService'; import { theme } from '@extension/theme'; // types -import { IAccount, IExplorer, INetwork } from '@extension/types'; +import { IAccount, IBlockExplorer, INetwork } from '@extension/types'; // utils import ellipseAddress from '@extension/utils/ellipseAddress'; @@ -51,7 +51,8 @@ const SignTxnsChangeAddressItem: FC = ({ const { t } = useTranslation(); // selectors const accounts: IAccount[] = useSelectAccounts(); - const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const preferredExplorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); // hooks const defaultTextColor: string = useDefaultTextColor(); const subTextColor: string = useSubTextColor(); @@ -68,9 +69,11 @@ const SignTxnsChangeAddressItem: FC = ({ AccountService.convertPublicKeyToAlgorandAddress(value.publicKey) === newAddress ) || null; - const explorer: IExplorer | null = - network.explorers.find((value) => value.id === preferredExplorer?.id) || - network.explorers[0] || + const explorer: IBlockExplorer | null = + network.blockExplorers.find( + (value) => value.id === preferredExplorer?.id + ) || + network.blockExplorers[0] || null; // get the preferred explorer, if it exists in the networks, otherwise get the default one const renderAccount = (address: string, account: IAccount | null) => { return ( diff --git a/src/extension/modals/SignTxnsModal/SingleTransactionContent.tsx b/src/extension/modals/SignTxnsModal/SingleTransactionContent.tsx index 8bbbfef2..612083e5 100644 --- a/src/extension/modals/SignTxnsModal/SingleTransactionContent.tsx +++ b/src/extension/modals/SignTxnsModal/SingleTransactionContent.tsx @@ -23,7 +23,7 @@ import { useSelectAccounts, useSelectLogger, useSelectNetworks, - useSelectPreferredBlockExplorer, + useSelectSettingsPreferredBlockExplorer, useSelectStandardAssetsByGenesisHash, useSelectUpdatingStandardAssets, } from '@extension/selectors'; @@ -36,7 +36,7 @@ import type { ILogger } from '@common/types'; import type { IAccount, IAccountInformation, - IExplorer, + IBlockExplorer, INetwork, IStandardAsset, } from '@extension/types'; @@ -55,7 +55,8 @@ const SingleTransactionContent: FC = ({ transaction }: IProps) => { const accounts: IAccount[] = useSelectAccounts(); const logger: ILogger = useSelectLogger(); const networks: INetwork[] = useSelectNetworks(); - const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const preferredExplorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); const standardAssets: IStandardAsset[] = useSelectStandardAssetsByGenesisHash(encodedGenesisHash); const updatingStandardAssets: boolean = useSelectUpdatingStandardAssets(); @@ -66,9 +67,11 @@ const SingleTransactionContent: FC = ({ transaction }: IProps) => { // misc const network: INetwork | null = networks.find((value) => value.genesisHash === encodedGenesisHash) || null; - const explorer: IExplorer | null = - network?.explorers.find((value) => value.id === preferredExplorer?.id) || - network?.explorers[0] || + const explorer: IBlockExplorer | null = + network?.blockExplorers.find( + (value) => value.id === preferredExplorer?.id + ) || + network?.blockExplorers[0] || null; // get the preferred explorer, if it exists in the network, otherwise get the default one const standardAsset: IStandardAsset | null = standardAssets.find( diff --git a/src/extension/pages/AccountPage/AccountPage.tsx b/src/extension/pages/AccountPage/AccountPage.tsx index c77d69eb..a4becbee 100644 --- a/src/extension/pages/AccountPage/AccountPage.tsx +++ b/src/extension/pages/AccountPage/AccountPage.tsx @@ -27,15 +27,15 @@ import { NavigateFunction, useNavigate } from 'react-router-dom'; // components import ActivityTab from '@extension/components/ActivityTab'; -import AccountNftsTab from '@extension/components/AccountNftsTab'; import AssetsTab from '@extension/components/AssetsTab'; import CopyIconButton from '@extension/components/CopyIconButton'; import EditableAccountNameField from '@extension/components/EditableAccountNameField'; import EmptyState from '@extension/components/EmptyState'; import IconButton from '@extension/components/IconButton'; import OpenTabIconButton from '@extension/components/OpenTabIconButton'; -import NetworkSelect from '@extension/components/NetworkSelect'; import NativeBalance from '@extension/components/NativeBalance'; +import NetworkSelect from '@extension/components/NetworkSelect'; +import NFTsTab from '@extension/components/NFTsTab'; import AccountPageSkeletonContent from './AccountPageSkeletonContent'; // constants @@ -72,10 +72,10 @@ import { useSelectActiveAccountInformation, useSelectActiveAccountTransactions, useSelectFetchingAccounts, - useSelectFetchingSettings, + useSelectSettingsFetching, useSelectIsOnline, useSelectNetworks, - useSelectPreferredBlockExplorer, + useSelectSettingsPreferredBlockExplorer, useSelectSavingAccounts, useSelectSelectedNetwork, useSelectSettings, @@ -91,7 +91,7 @@ import type { IAccountTransactions, IActiveAccountDetails, IAppThunkDispatch, - IExplorer, + IBlockExplorer, INetwork, ISettings, } from '@extension/types'; @@ -117,10 +117,11 @@ const AccountPage: FC = () => { const activeAccountDetails: IActiveAccountDetails | null = useSelectActiveAccountDetails(); const fetchingAccounts: boolean = useSelectFetchingAccounts(); - const fetchingSettings: boolean = useSelectFetchingSettings(); + const fetchingSettings: boolean = useSelectSettingsFetching(); const online: boolean = useSelectIsOnline(); const networks: INetwork[] = useSelectNetworks(); - const explorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const explorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); const savingAccounts: boolean = useSelectSavingAccounts(); const selectedNetwork: INetwork | null = useSelectSelectedNetwork(); const settings: ISettings = useSelectSettings(); @@ -374,7 +375,7 @@ const AccountPage: FC = () => { > - + { } = useDisclosure(); // selectors const fetchingAssets: boolean = useSelectFetchingStandardAssets(); - const explorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const explorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); const selectedNetwork: INetwork | null = useSelectSelectedNetwork(); // hooks const { diff --git a/src/extension/pages/GeneralSettingsPage/GeneralSettingsPage.tsx b/src/extension/pages/GeneralSettingsPage/GeneralSettingsPage.tsx index a5666244..4bbd619a 100644 --- a/src/extension/pages/GeneralSettingsPage/GeneralSettingsPage.tsx +++ b/src/extension/pages/GeneralSettingsPage/GeneralSettingsPage.tsx @@ -18,16 +18,18 @@ import { setConfirmModal } from '@extension/features/system'; // selectors import { - useSelectPreferredBlockExplorer, useSelectSelectedNetwork, useSelectSettings, + useSelectSettingsPreferredBlockExplorer, + useSelectSettingsPreferredNFTExplorer, } from '@extension/selectors'; // types import type { IAppThunkDispatch, - IExplorer, + IBlockExplorer, INetwork, + INFTExplorer, ISettings, } from '@extension/types'; @@ -38,13 +40,20 @@ const GeneralSettingsPage: FC = () => { const { t } = useTranslation(); const dispatch: IAppThunkDispatch = useDispatch(); // selectors - const preferredBlockExplorer: IExplorer | null = - useSelectPreferredBlockExplorer(); + const preferredBlockExplorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); + const preferredNFTExplorer: INFTExplorer | null = + useSelectSettingsPreferredNFTExplorer(); const selectedNetwork: INetwork | null = useSelectSelectedNetwork(); const settings: ISettings = useSelectSettings(); // misc const blockExplorerOptions: IOption[] = - selectedNetwork?.explorers.map((value) => ({ + selectedNetwork?.blockExplorers.map((value) => ({ + label: value.canonicalName, + value: value.id, + })) || []; + const nftExplorerOptions: IOption[] = + selectedNetwork?.nftExplorers.map((value) => ({ label: value.canonicalName, value: value.id, })) || []; @@ -59,12 +68,13 @@ const GeneralSettingsPage: FC = () => { }) ); const handlePreferredBlockExplorerChange = (option: IOption) => { - let explorer: IExplorer | null; + let explorer: IBlockExplorer | null; if (selectedNetwork) { explorer = - selectedNetwork.explorers.find((value) => value.id === option.value) || - null; + selectedNetwork.blockExplorers.find( + (value) => value.id === option.value + ) || null; if (explorer) { dispatch( @@ -84,6 +94,33 @@ const GeneralSettingsPage: FC = () => { } } }; + const handlePreferredNFTExplorerChange = (option: IOption) => { + let explorer: INFTExplorer | null; + + if (selectedNetwork) { + explorer = + selectedNetwork.nftExplorers.find( + (value) => value.id === option.value + ) || null; + + if (explorer) { + dispatch( + saveSettingsToStorageThunk({ + ...settings, + general: { + ...settings.general, + preferredNFTExplorerIds: { + ...settings.general.preferredNFTExplorerIds, + [convertGenesisHashToHex( + selectedNetwork.genesisHash + ).toUpperCase()]: explorer.id, + }, + }, + }) + ); + } + } + }; return ( <> @@ -109,6 +146,20 @@ const GeneralSettingsPage: FC = () => { ) || blockExplorerOptions[0] } /> + + {/* preferred nft explorer */} + ('captions.preferredNFTExplorer')} + emptyOptionLabel={t('captions.noNFTExplorersAvailable')} + label={t('labels.preferredNFTExplorer')} + onChange={handlePreferredNFTExplorerChange} + options={nftExplorerOptions} + value={ + nftExplorerOptions.find( + (value) => value.value === preferredNFTExplorer?.id + ) || nftExplorerOptions[0] + } + /> {/* danger zone */} diff --git a/src/extension/pages/NFTPage/NFTPage.tsx b/src/extension/pages/NFTPage/NFTPage.tsx new file mode 100644 index 00000000..18efbfd6 --- /dev/null +++ b/src/extension/pages/NFTPage/NFTPage.tsx @@ -0,0 +1,256 @@ +import { + Code, + Heading, + HStack, + Image, + Link, + Skeleton, + Text, + Tooltip, + VStack, +} from '@chakra-ui/react'; +import React, { FC, ReactElement, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { NavigateFunction, useNavigate, useParams } from 'react-router-dom'; + +// components +import AssetBadge from '@extension/components/AssetBadge'; +import CopyIconButton from '@extension/components/CopyIconButton'; +import LoadingPage from '@extension/components/LoadingPage'; +import OpenTabIconButton from '@extension/components/OpenTabIconButton'; +import PageHeader from '@extension/components/PageHeader'; +import PageItem from '@extension/components/PageItem'; + +// constants +import { ACCOUNTS_ROUTE, DEFAULT_GAP } from '@extension/constants'; + +// hooks +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import useSubTextColor from '@extension/hooks/useSubTextColor'; +import useNFTPage from './hooks/useNFTPage'; + +// images +import nftPlaceholderImage from '@extension/images/placeholder_nft.png'; + +// selectors +import { + useSelectSettingsPreferredBlockExplorer, + useSelectSettingsPreferredNFTExplorer, +} from '@extension/selectors'; + +// services +import AccountService from '@extension/services/AccountService'; + +// types +import type { IBlockExplorer, INFTExplorer } from '@extension/types'; + +// utils +import ellipseAddress from '@extension/utils/ellipseAddress'; + +const NFTPage: FC = () => { + const { t } = useTranslation(); + const navigate: NavigateFunction = useNavigate(); + const { appId, tokenId } = useParams(); + // selectors + const blockExplorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); + const nftExplorer: INFTExplorer | null = + useSelectSettingsPreferredNFTExplorer(); + // hooks + const { account, accountInformation, asset, assetHolding } = useNFTPage({ + appId: appId || null, + onError: () => + navigate(ACCOUNTS_ROUTE, { + replace: true, + }), + tokenId: tokenId || null, + }); + const defaultTextColor: string = useDefaultTextColor(); + const subTextColor: string = useSubTextColor(); + // handlers + const reset = () => + navigate(ACCOUNTS_ROUTE, { + replace: true, + }); + // renders + const renderImage = () => { + let imageElement: ReactElement; + + if (!assetHolding) { + return ( + + NFT image loading + + ); + } + + imageElement = ( + NFT image + ); + + // if we have an nft explorer, wrap the nft in a link + if (nftExplorer) { + return ( + + {imageElement} + + ); + } + + return imageElement; + }; + let accountAddress: string; + + // if we don't have the params, return to accounts page + useEffect(() => { + if (!appId || !tokenId) { + return reset(); + } + }, []); + + if (!account || !accountInformation || !assetHolding) { + return ; + } + + accountAddress = AccountService.convertPublicKeyToAlgorandAddress( + account.publicKey + ); + + return ( + <> + + + + + {/*image*/} + {renderImage()} + + {/*name (or id)*/} + {assetHolding.metadata.name ? ( + + + {assetHolding.metadata.name} + + + ) : ( + + {`#${assetHolding.tokenId}`} + + )} + + {/*description*/} + {assetHolding.metadata.description && ( + + + {assetHolding.metadata.description} + + + )} + + {/*token id/total supply*/} + ('labels.tokenId')}> + + {asset + ? `${assetHolding.tokenId}/${asset.totalSupply}` + : assetHolding.tokenId} + + + + {/*application id*/} + ('labels.applicationId')}> + + + {assetHolding.id} + + + {/*copy app id button*/} + ('labels.copyApplicationId')} + tooltipLabel={t('labels.copyApplicationId')} + value={assetHolding.id} + /> + + {/*open app on explorer*/} + {blockExplorer && ( + ('captions.openOn', { + name: blockExplorer.canonicalName, + })} + url={`${blockExplorer.baseUrl}${blockExplorer.applicationPath}/${assetHolding.id}`} + /> + )} + + + + {/*type*/} + ('labels.type')}> + + + + + + ); +}; + +export default NFTPage; diff --git a/src/extension/pages/NFTPage/hooks/useNFTPage/index.ts b/src/extension/pages/NFTPage/hooks/useNFTPage/index.ts new file mode 100644 index 00000000..05a19109 --- /dev/null +++ b/src/extension/pages/NFTPage/hooks/useNFTPage/index.ts @@ -0,0 +1,2 @@ +export { default } from './useNFTPage'; +export * from './types'; diff --git a/src/extension/pages/NFTPage/hooks/useNFTPage/types/IUseNFTPageOptions.ts b/src/extension/pages/NFTPage/hooks/useNFTPage/types/IUseNFTPageOptions.ts new file mode 100644 index 00000000..73b8de4f --- /dev/null +++ b/src/extension/pages/NFTPage/hooks/useNFTPage/types/IUseNFTPageOptions.ts @@ -0,0 +1,7 @@ +interface IUseNFTPageOptions { + appId: string | null; + onError: () => void; + tokenId: string | null; +} + +export default IUseNFTPageOptions; diff --git a/src/extension/pages/NFTPage/hooks/useNFTPage/types/IUseNFTPageState.ts b/src/extension/pages/NFTPage/hooks/useNFTPage/types/IUseNFTPageState.ts new file mode 100644 index 00000000..37bc2816 --- /dev/null +++ b/src/extension/pages/NFTPage/hooks/useNFTPage/types/IUseNFTPageState.ts @@ -0,0 +1,16 @@ +// types +import type { + IAccount, + IAccountInformation, + IARC0072Asset, + IARC0072AssetHolding, +} from '@extension/types'; + +interface IUseNFTPageState { + account: IAccount | null; + accountInformation: IAccountInformation | null; + asset: IARC0072Asset | null; + assetHolding: IARC0072AssetHolding | null; +} + +export default IUseNFTPageState; diff --git a/src/extension/pages/NFTPage/hooks/useNFTPage/types/index.ts b/src/extension/pages/NFTPage/hooks/useNFTPage/types/index.ts new file mode 100644 index 00000000..d19fc9ac --- /dev/null +++ b/src/extension/pages/NFTPage/hooks/useNFTPage/types/index.ts @@ -0,0 +1,2 @@ +export type { default as IUseNFTPageOptions } from './IUseNFTPageOptions'; +export type { default as IUseNFTPageState } from './IUseNFTPageState'; diff --git a/src/extension/pages/NFTPage/hooks/useNFTPage/useNFTPage.ts b/src/extension/pages/NFTPage/hooks/useNFTPage/useNFTPage.ts new file mode 100644 index 00000000..67854c43 --- /dev/null +++ b/src/extension/pages/NFTPage/hooks/useNFTPage/useNFTPage.ts @@ -0,0 +1,71 @@ +import { useEffect, useState } from 'react'; + +// selectors +import { + useSelectActiveAccount, + useSelectActiveAccountInformation, + useSelectARC0072AssetsBySelectedNetwork, +} from '@extension/selectors'; + +// types +import type { + IAccount, + IAccountInformation, + IARC0072Asset, + IARC0072AssetHolding, +} from '@extension/types'; +import type { IUseNFTPageOptions, IUseNFTPageState } from './types'; + +export default function useNFTPage({ + appId, + onError, + tokenId, +}: IUseNFTPageOptions): IUseNFTPageState { + // selectors + const account: IAccount | null = useSelectActiveAccount(); + const accountInformation: IAccountInformation | null = + useSelectActiveAccountInformation(); + const arc0072Assets: IARC0072Asset[] = + useSelectARC0072AssetsBySelectedNetwork(); + // state + const [asset, setAsset] = useState(null); + const [assetHolding, setAssetHolding] = useState( + null + ); + + useEffect(() => { + let selectedAsset: IARC0072Asset | null; + + if (appId) { + // first, find amongst the arc-0072 assets + selectedAsset = arc0072Assets.find((value) => value.id === appId) || null; + + setAsset(selectedAsset); + } + }, [arc0072Assets, appId]); + // when we have the asset information and the account information, get the asset holding + useEffect(() => { + let selectedAssetHolding: IARC0072AssetHolding | null; + + if (accountInformation && appId && tokenId) { + selectedAssetHolding = + accountInformation.arc0072AssetHoldings.find( + (value) => value.id === appId && value.tokenId === tokenId + ) || null; + + // if the account does not have the asset holding, we have an error + if (!selectedAssetHolding) { + return onError(); + } + + setAssetHolding(selectedAssetHolding); + } + }, [appId, accountInformation, tokenId]); + + return { + account, + accountInformation, + asset, + assetHolding, + }; +} diff --git a/src/extension/pages/NFTPage/index.ts b/src/extension/pages/NFTPage/index.ts new file mode 100644 index 00000000..8e5bcee0 --- /dev/null +++ b/src/extension/pages/NFTPage/index.ts @@ -0,0 +1 @@ +export { default } from './NFTPage'; diff --git a/src/extension/pages/TransactionPage/ApplicationTransactionContent.tsx b/src/extension/pages/TransactionPage/ApplicationTransactionContent.tsx index e129c591..c72f1502 100644 --- a/src/extension/pages/TransactionPage/ApplicationTransactionContent.tsx +++ b/src/extension/pages/TransactionPage/ApplicationTransactionContent.tsx @@ -32,13 +32,13 @@ import usePrimaryColorScheme from '@extension/hooks/usePrimaryColorScheme'; import useSubTextColor from '@extension/hooks/useSubTextColor'; // selectors -import { useSelectPreferredBlockExplorer } from '@extension/selectors'; +import { useSelectSettingsPreferredBlockExplorer } from '@extension/selectors'; // types import { IAccount, IApplicationTransaction, - IExplorer, + IBlockExplorer, INetwork, } from '@extension/types'; @@ -60,7 +60,8 @@ const ApplicationTransactionContent: FC = ({ const { t } = useTranslation(); const { isOpen, onOpen, onClose } = useDisclosure(); // selectors - const explorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const explorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); // hooks const defaultTextColor: string = useDefaultTextColor(); const primaryColorScheme: string = usePrimaryColorScheme(); diff --git a/src/extension/pages/TransactionPage/AssetConfigTransactionContent.tsx b/src/extension/pages/TransactionPage/AssetConfigTransactionContent.tsx index 59fec46d..dd5093b6 100644 --- a/src/extension/pages/TransactionPage/AssetConfigTransactionContent.tsx +++ b/src/extension/pages/TransactionPage/AssetConfigTransactionContent.tsx @@ -31,14 +31,14 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; // selectors import { useSelectAccounts, - useSelectPreferredBlockExplorer, + useSelectSettingsPreferredBlockExplorer, } from '@extension/selectors'; // types import { IAccount, IAssetConfigTransaction, - IExplorer, + IBlockExplorer, INetwork, } from '@extension/types'; @@ -60,16 +60,19 @@ const AssetConfigTransactionContent: FC = ({ const { isOpen, onOpen, onClose } = useDisclosure(); // selectors const accounts: IAccount[] = useSelectAccounts(); - const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const preferredExplorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); // hooks const { standardAsset, updating } = useStandardAssetById(transaction.assetId); const defaultTextColor: string = useDefaultTextColor(); const primaryButtonTextColor: string = usePrimaryButtonTextColor(); const subTextColor: string = useSubTextColor(); // misc - const explorer: IExplorer | null = - network.explorers.find((value) => value.id === preferredExplorer?.id) || - network.explorers[0] || + const explorer: IBlockExplorer | null = + network.blockExplorers.find( + (value) => value.id === preferredExplorer?.id + ) || + network.blockExplorers[0] || null; // get the preferred explorer, if it exists in the networks, otherwise get the default one // handlers const handleMoreInformationToggle = (value: boolean) => diff --git a/src/extension/pages/TransactionPage/AssetCreateTransactionContent.tsx b/src/extension/pages/TransactionPage/AssetCreateTransactionContent.tsx index 564c1cd0..1ce564bc 100644 --- a/src/extension/pages/TransactionPage/AssetCreateTransactionContent.tsx +++ b/src/extension/pages/TransactionPage/AssetCreateTransactionContent.tsx @@ -30,14 +30,14 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; // selectors import { useSelectAccounts, - useSelectPreferredBlockExplorer, + useSelectSettingsPreferredBlockExplorer, } from '@extension/selectors'; // types import { IAccount, IAssetCreateTransaction, - IExplorer, + IBlockExplorer, INetwork, } from '@extension/types'; @@ -59,7 +59,8 @@ const AssetCreateTransactionContent: FC = ({ const { isOpen, onOpen, onClose } = useDisclosure(); // selectors const accounts: IAccount[] = useSelectAccounts(); - const explorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const explorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); // hooks const defaultTextColor: string = useDefaultTextColor(); const primaryButtonTextColor: string = usePrimaryButtonTextColor(); diff --git a/src/extension/pages/TransactionPage/AssetDestroyTransactionContent.tsx b/src/extension/pages/TransactionPage/AssetDestroyTransactionContent.tsx index fd414cfa..733d6810 100644 --- a/src/extension/pages/TransactionPage/AssetDestroyTransactionContent.tsx +++ b/src/extension/pages/TransactionPage/AssetDestroyTransactionContent.tsx @@ -28,14 +28,14 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; // selectors import { useSelectAccounts, - useSelectPreferredBlockExplorer, + useSelectSettingsPreferredBlockExplorer, } from '@extension/selectors'; // types import { IAccount, IAssetDestroyTransaction, - IExplorer, + IBlockExplorer, INetwork, } from '@extension/types'; @@ -57,14 +57,17 @@ const AssetDestroyTransactionContent: FC = ({ const { isOpen, onOpen, onClose } = useDisclosure(); // selectors const accounts: IAccount[] = useSelectAccounts(); - const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const preferredExplorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); // hooks const defaultTextColor: string = useDefaultTextColor(); const subTextColor: string = useSubTextColor(); // misc - const explorer: IExplorer | null = - network.explorers.find((value) => value.id === preferredExplorer?.id) || - network.explorers[0] || + const explorer: IBlockExplorer | null = + network.blockExplorers.find( + (value) => value.id === preferredExplorer?.id + ) || + network.blockExplorers[0] || null; // get the preferred explorer, if it exists in the networks, otherwise get the default one // handlers const handleMoreInformationToggle = (value: boolean) => diff --git a/src/extension/pages/TransactionPage/AssetFreezeTransactionContent.tsx b/src/extension/pages/TransactionPage/AssetFreezeTransactionContent.tsx index 717c19b8..d6ed07fb 100644 --- a/src/extension/pages/TransactionPage/AssetFreezeTransactionContent.tsx +++ b/src/extension/pages/TransactionPage/AssetFreezeTransactionContent.tsx @@ -33,7 +33,7 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; // selectors import { useSelectAccounts, - useSelectPreferredBlockExplorer, + useSelectSettingsPreferredBlockExplorer, } from '@extension/selectors'; // types @@ -41,7 +41,7 @@ import { IAccount, IAssetFreezeTransaction, IAssetUnfreezeTransaction, - IExplorer, + IBlockExplorer, INetwork, } from '@extension/types'; @@ -63,15 +63,18 @@ const AssetTransferTransactionContent: FC = ({ const { isOpen, onOpen, onClose } = useDisclosure(); // selectors const accounts: IAccount[] = useSelectAccounts(); - const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const preferredExplorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); // hooks const { standardAsset, updating } = useStandardAssetById(transaction.assetId); const defaultTextColor: string = useDefaultTextColor(); const subTextColor: string = useSubTextColor(); // misc - const explorer: IExplorer | null = - network.explorers.find((value) => value.id === preferredExplorer?.id) || - network.explorers[0] || + const explorer: IBlockExplorer | null = + network.blockExplorers.find( + (value) => value.id === preferredExplorer?.id + ) || + network.blockExplorers[0] || null; // get the preferred explorer, if it exists in the networks, otherwise get the default one // handlers const handleMoreInformationToggle = (value: boolean) => diff --git a/src/extension/pages/TransactionPage/AssetTransferTransactionContent.tsx b/src/extension/pages/TransactionPage/AssetTransferTransactionContent.tsx index 829eb309..74a9bd0a 100644 --- a/src/extension/pages/TransactionPage/AssetTransferTransactionContent.tsx +++ b/src/extension/pages/TransactionPage/AssetTransferTransactionContent.tsx @@ -33,7 +33,7 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; // selectors import { useSelectAccounts, - useSelectPreferredBlockExplorer, + useSelectSettingsPreferredBlockExplorer, } from '@extension/selectors'; // services @@ -43,7 +43,7 @@ import AccountService from '@extension/services/AccountService'; import { IAccount, IAssetTransferTransaction, - IExplorer, + IBlockExplorer, INetwork, } from '@extension/types'; @@ -67,7 +67,8 @@ const AssetTransferTransactionContent: FC = ({ const { isOpen, onOpen, onClose } = useDisclosure(); // selectors const accounts: IAccount[] = useSelectAccounts(); - const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const preferredExplorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); // hooks const { standardAsset, updating } = useStandardAssetById(transaction.assetId); const defaultTextColor: string = useDefaultTextColor(); @@ -77,9 +78,11 @@ const AssetTransferTransactionContent: FC = ({ const accountAddress: string = AccountService.convertPublicKeyToAlgorandAddress(account.publicKey); const amount: BigNumber = new BigNumber(transaction.amount); - const explorer: IExplorer | null = - network.explorers.find((value) => value.id === preferredExplorer?.id) || - network.explorers[0] || + const explorer: IBlockExplorer | null = + network.blockExplorers.find( + (value) => value.id === preferredExplorer?.id + ) || + network.blockExplorers[0] || null; // get the preferred explorer, if it exists in the networks, otherwise get the default one // handlers const handleMoreInformationToggle = (value: boolean) => diff --git a/src/extension/pages/TransactionPage/PaymentTransactionContent.tsx b/src/extension/pages/TransactionPage/PaymentTransactionContent.tsx index 7cfad1ce..24690a25 100644 --- a/src/extension/pages/TransactionPage/PaymentTransactionContent.tsx +++ b/src/extension/pages/TransactionPage/PaymentTransactionContent.tsx @@ -28,7 +28,7 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; // selectors import { useSelectAccounts, - useSelectPreferredBlockExplorer, + useSelectSettingsPreferredBlockExplorer, } from '@extension/selectors'; // services @@ -37,7 +37,7 @@ import AccountService from '@extension/services/AccountService'; // types import { IAccount, - IExplorer, + IBlockExplorer, INetwork, IPaymentTransaction, } from '@extension/types'; @@ -61,7 +61,8 @@ const PaymentTransactionContent: FC = ({ const { isOpen, onOpen, onClose } = useDisclosure(); // selectors const accounts: IAccount[] = useSelectAccounts(); - const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const preferredExplorer: IBlockExplorer | null = + useSelectSettingsPreferredBlockExplorer(); // hooks const defaultTextColor: string = useDefaultTextColor(); const subTextColor: string = useSubTextColor(); @@ -69,9 +70,11 @@ const PaymentTransactionContent: FC = ({ const accountAddress: string = AccountService.convertPublicKeyToAlgorandAddress(account.publicKey); const amount: BigNumber = new BigNumber(transaction.amount); - const explorer: IExplorer | null = - network.explorers.find((value) => value.id === preferredExplorer?.id) || - network.explorers[0] || + const explorer: IBlockExplorer | null = + network.blockExplorers.find( + (value) => value.id === preferredExplorer?.id + ) || + network.blockExplorers[0] || null; // get the preferred explorer, if it exists in the networks, otherwise get the default one const isReceiverKnown: boolean = accounts.findIndex( diff --git a/src/extension/selectors/arc-0072-assets/index.ts b/src/extension/selectors/arc-0072-assets/index.ts new file mode 100644 index 00000000..fee2df8c --- /dev/null +++ b/src/extension/selectors/arc-0072-assets/index.ts @@ -0,0 +1,3 @@ +export { default as useSelectARC0072AssetsBySelectedNetwork } from './useSelectARC0072AssetsBySelectedNetwork'; +export { default as useSelectARC0072AssetsFetching } from './useSelectARC0072AssetsFetching'; +export { default as useSelectARC0072AssetsUpdating } from './useSelectARC0072AssetsUpdating'; diff --git a/src/extension/selectors/arc-0072-assets/useSelectARC0072AssetsBySelectedNetwork.ts b/src/extension/selectors/arc-0072-assets/useSelectARC0072AssetsBySelectedNetwork.ts new file mode 100644 index 00000000..37e81285 --- /dev/null +++ b/src/extension/selectors/arc-0072-assets/useSelectARC0072AssetsBySelectedNetwork.ts @@ -0,0 +1,31 @@ +import { useSelector } from 'react-redux'; + +// types +import type { IARC0072Asset, IMainRootState, INetwork } from '@extension/types'; + +// utils +import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; +import selectNetworkFromSettings from '@extension/utils/selectNetworkFromSettings'; + +/** + * Selects all the ARC-0072 assets for the selected network. + * @returns {IARC0072Asset[]} all network ARC-0072 assets for the selected network. + */ +export default function useSelectARC0072AssetsBySelectedNetwork(): IARC0072Asset[] { + return useSelector((state) => { + const selectedNetwork: INetwork | null = selectNetworkFromSettings( + state.networks.items, + state.settings + ); + + if (!selectedNetwork) { + return []; + } + + return state.arc0072Assets.items + ? state.arc0072Assets.items[ + convertGenesisHashToHex(selectedNetwork.genesisHash).toUpperCase() + ] + : []; + }); +} diff --git a/src/extension/selectors/arc-0072-assets/useSelectARC0072AssetsFetching.ts b/src/extension/selectors/arc-0072-assets/useSelectARC0072AssetsFetching.ts new file mode 100644 index 00000000..7612447e --- /dev/null +++ b/src/extension/selectors/arc-0072-assets/useSelectARC0072AssetsFetching.ts @@ -0,0 +1,10 @@ +import { useSelector } from 'react-redux'; + +// types +import type { IMainRootState } from '@extension/types'; + +export default function useSelectARC0072AssetsFetching(): boolean { + return useSelector( + (state) => state.arc0072Assets.fetching + ); +} diff --git a/src/extension/selectors/arc-0072-assets/useSelectARC0072AssetsUpdating.ts b/src/extension/selectors/arc-0072-assets/useSelectARC0072AssetsUpdating.ts new file mode 100644 index 00000000..59e30043 --- /dev/null +++ b/src/extension/selectors/arc-0072-assets/useSelectARC0072AssetsUpdating.ts @@ -0,0 +1,10 @@ +import { useSelector } from 'react-redux'; + +// types +import type { IMainRootState } from '@extension/types'; + +export default function useSelectARC0072AssetsUpdating(): boolean { + return useSelector( + (state) => state.arc0072Assets.updating + ); +} diff --git a/src/extension/selectors/arc-0200-assets/index.ts b/src/extension/selectors/arc-0200-assets/index.ts index 4297402d..b89cff37 100644 --- a/src/extension/selectors/arc-0200-assets/index.ts +++ b/src/extension/selectors/arc-0200-assets/index.ts @@ -1 +1,3 @@ export { default as useSelectARC0200AssetsBySelectedNetwork } from './useSelectARC0200AssetsBySelectedNetwork'; +export { default as useSelectARC0200AssetsFetching } from './useSelectARC0200AssetsFetching'; +export { default as useSelectARC0200AssetsUpdating } from './useSelectARC0200AssetsUpdating'; diff --git a/src/extension/selectors/arc-0200-assets/useSelectARC0200AssetsBySelectedNetwork.ts b/src/extension/selectors/arc-0200-assets/useSelectARC0200AssetsBySelectedNetwork.ts index 922634af..ed1c3a7c 100644 --- a/src/extension/selectors/arc-0200-assets/useSelectARC0200AssetsBySelectedNetwork.ts +++ b/src/extension/selectors/arc-0200-assets/useSelectARC0200AssetsBySelectedNetwork.ts @@ -22,8 +22,8 @@ export default function useSelectARC0200AssetsBySelectedNetwork(): IARC0200Asset return []; } - return state.arc200Assets.items - ? state.arc200Assets.items[ + return state.arc0200Assets.items + ? state.arc0200Assets.items[ convertGenesisHashToHex(selectedNetwork.genesisHash).toUpperCase() ] : []; diff --git a/src/extension/selectors/arc-0200-assets/useSelectARC0200AssetsFetching.ts b/src/extension/selectors/arc-0200-assets/useSelectARC0200AssetsFetching.ts new file mode 100644 index 00000000..3dd4ffe1 --- /dev/null +++ b/src/extension/selectors/arc-0200-assets/useSelectARC0200AssetsFetching.ts @@ -0,0 +1,10 @@ +import { useSelector } from 'react-redux'; + +// types +import type { IMainRootState } from '@extension/types'; + +export default function useSelectARC0200AssetsFetching(): boolean { + return useSelector( + (state) => state.arc0200Assets.fetching + ); +} diff --git a/src/extension/selectors/arc-0200-assets/useSelectARC0200AssetsUpdating.ts b/src/extension/selectors/arc-0200-assets/useSelectARC0200AssetsUpdating.ts new file mode 100644 index 00000000..38ae3bc3 --- /dev/null +++ b/src/extension/selectors/arc-0200-assets/useSelectARC0200AssetsUpdating.ts @@ -0,0 +1,10 @@ +import { useSelector } from 'react-redux'; + +// types +import type { IMainRootState } from '@extension/types'; + +export default function useSelectARC0200AssetsUpdating(): boolean { + return useSelector( + (state) => state.arc0200Assets.updating + ); +} diff --git a/src/extension/selectors/index.ts b/src/extension/selectors/index.ts index 3ac3c8b6..71600e40 100644 --- a/src/extension/selectors/index.ts +++ b/src/extension/selectors/index.ts @@ -9,9 +9,7 @@ export { default as useSelectColorMode } from './useSelectColorMode'; export { default as useSelectDebugLogging } from './useSelectDebugLogging'; export { default as useSelectEnableRequest } from './useSelectEnableRequest'; export { default as useSelectFetchingAccounts } from './useSelectFetchingAccounts'; -export { default as useSelectFetchingARC0200Assets } from './useSelectFetchingARC0200Assets'; export { default as useSelectFetchingSessions } from './useSelectFetchingSessions'; -export { default as useSelectFetchingSettings } from './useSelectFetchingSettings'; export { default as useSelectFetchingStandardAssets } from './useSelectFetchingStandardAssets'; export { default as useSelectInitializingWalletConnect } from './useSelectInitializingWalletConnect'; export { default as useSelectIsOnline } from './useSelectIsOnline'; @@ -20,26 +18,24 @@ export { default as useSelectNetworkByGenesisHash } from './useSelectNetworkByGe export { default as useSelectNetworks } from './useSelectNetworks'; export { default as useSelectNotShowingNotifications } from './useSelectNotShowingNotifications'; export { default as useSelectPasswordLockPassword } from './useSelectPasswordLockPassword'; -export { default as useSelectPreferredBlockExplorer } from './useSelectPreferredBlockExplorer'; export { default as useSelectSavingAccounts } from './useSelectSavingAccounts'; export { default as useSelectSavingPasswordLock } from './useSelectSavingPasswordLock'; export { default as useSelectSavingSessions } from './useSelectSavingSessions'; -export { default as useSelectSavingSettings } from './useSelectSavingSettings'; export { default as useSelectSelectedNetwork } from './useSelectSelectedNetwork'; export { default as useSelectSessions } from './useSelectSessions'; -export { default as useSelectSettings } from './useSelectSettings'; export { default as useSelectSideBar } from './useSelectSideBar'; export { default as useSelectSignBytesRequest } from './useSelectSignBytesRequest'; export { default as useSelectSignTxnsRequest } from './useSelectSignTxnsRequest'; export { default as useSelectStandardAssets } from './useSelectStandardAssets'; export { default as useSelectStandardAssetsByGenesisHash } from './useSelectStandardAssetsByGenesisHash'; export { default as useSelectStandardAssetsBySelectedNetwork } from './useSelectStandardAssetsBySelectedNetwork'; -export { default as useSelectUpdatingArc200Assets } from './useSelectUpdatingArc200Assets'; export { default as useSelectUpdatingStandardAssets } from './useSelectUpdatingStandardAssets'; export { default as useSelectWalletConnectModalOpen } from './useSelectWalletConnectModalOpen'; export { default as useSelectWeb3Wallet } from './useSelectWeb3Wallet'; export * from './accounts'; +export * from './arc-0072-assets'; export * from './arc-0200-assets'; export * from './registration'; export * from './send-assets'; +export * from './settings'; export * from './system'; diff --git a/src/extension/selectors/settings/index.ts b/src/extension/selectors/settings/index.ts new file mode 100644 index 00000000..a6103e72 --- /dev/null +++ b/src/extension/selectors/settings/index.ts @@ -0,0 +1,5 @@ +export { default as useSelectSettings } from './useSelectSettings'; +export { default as useSelectSettingsFetching } from './useSelectSettingsFetching'; +export { default as useSelectSettingsPreferredBlockExplorer } from './useSelectSettingsPreferredBlockExplorer'; +export { default as useSelectSettingsPreferredNFTExplorer } from './useSelectSettingsPreferredNFTExplorer'; +export { default as useSelectSettingsSaving } from './useSelectSettingsSaving'; diff --git a/src/extension/selectors/useSelectSettings.ts b/src/extension/selectors/settings/useSelectSettings.ts similarity index 82% rename from src/extension/selectors/useSelectSettings.ts rename to src/extension/selectors/settings/useSelectSettings.ts index 1d728604..d0f2654f 100644 --- a/src/extension/selectors/useSelectSettings.ts +++ b/src/extension/selectors/settings/useSelectSettings.ts @@ -4,7 +4,7 @@ import { useSelector } from 'react-redux'; import { filterSettingsFromState } from '@extension/features/settings'; // types -import { IMainRootState, ISettings } from '@extension/types'; +import type { IMainRootState, ISettings } from '@extension/types'; export default function useSelectSettings(): ISettings { return useSelector((state) => diff --git a/src/extension/selectors/useSelectFetchingSettings.ts b/src/extension/selectors/settings/useSelectSettingsFetching.ts similarity index 54% rename from src/extension/selectors/useSelectFetchingSettings.ts rename to src/extension/selectors/settings/useSelectSettingsFetching.ts index 4a6ac0e5..70008f30 100644 --- a/src/extension/selectors/useSelectFetchingSettings.ts +++ b/src/extension/selectors/settings/useSelectSettingsFetching.ts @@ -1,8 +1,9 @@ import { useSelector } from 'react-redux'; // types -import { IMainRootState } from '@extension/types'; -export default function useSelectFetchingSettings(): boolean { +import type { IMainRootState } from '@extension/types'; + +export default function useSelectSettingsFetching(): boolean { return useSelector( (state) => state.settings.fetching ); diff --git a/src/extension/selectors/useSelectPreferredBlockExplorer.ts b/src/extension/selectors/settings/useSelectSettingsPreferredBlockExplorer.ts similarity index 63% rename from src/extension/selectors/useSelectPreferredBlockExplorer.ts rename to src/extension/selectors/settings/useSelectSettingsPreferredBlockExplorer.ts index d6ab747f..1d121185 100644 --- a/src/extension/selectors/useSelectPreferredBlockExplorer.ts +++ b/src/extension/selectors/settings/useSelectSettingsPreferredBlockExplorer.ts @@ -1,16 +1,22 @@ import { useSelector } from 'react-redux'; // types -import { IExplorer, IMainRootState, INetwork } from '@extension/types'; +import type { + IBlockExplorer, + IMainRootState, + INetwork, +} from '@extension/types'; + +// utils import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; /** * Gets the currently preferred block explorer from the settings. If the block explorer cannot be found, the default * explorer is used (first index), or null. - * @returns {IExplorer | null} the current preferred block explorer from settings, or the default explorer, or null. + * @returns {IBlockExplorer | null} the current preferred block explorer from settings, or the default explorer, or null. */ -export default function useSelectPreferredBlockExplorer(): IExplorer | null { - return useSelector((state) => { +export default function useSelectPreferredBlockExplorer(): IBlockExplorer | null { + return useSelector((state) => { const selectedNetwork: INetwork | null = state.networks.items.find( (value) => @@ -23,14 +29,14 @@ export default function useSelectPreferredBlockExplorer(): IExplorer | null { } return ( - selectedNetwork.explorers.find( + selectedNetwork.blockExplorers.find( (value) => value.id === state.settings.general.preferredBlockExplorerIds[ convertGenesisHashToHex(selectedNetwork.genesisHash).toUpperCase() ] ) || - selectedNetwork.explorers[0] || + selectedNetwork.blockExplorers[0] || null ); }); diff --git a/src/extension/selectors/settings/useSelectSettingsPreferredNFTExplorer.ts b/src/extension/selectors/settings/useSelectSettingsPreferredNFTExplorer.ts new file mode 100644 index 00000000..bfc12286 --- /dev/null +++ b/src/extension/selectors/settings/useSelectSettingsPreferredNFTExplorer.ts @@ -0,0 +1,39 @@ +import { useSelector } from 'react-redux'; + +// types +import type { IMainRootState, INetwork, INFTExplorer } from '@extension/types'; + +// utils +import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; + +/** + * Gets the currently preferred NFT explorer from the settings. If the NFT explorer cannot be found, the default + * explorer is used (first index), or null. + * @returns {INFTExplorer | null} the current preferred NFT explorer from settings, or the default explorer, or null. + */ +export default function useSelectPreferredNFTExplorer(): INFTExplorer | null { + return useSelector((state) => { + const selectedNetwork: INetwork | null = + state.networks.items.find( + (value) => + value.genesisHash === + state.settings.general.selectedNetworkGenesisHash + ) || null; + + if (!selectedNetwork) { + return null; + } + + return ( + selectedNetwork.nftExplorers.find( + (value) => + value.id === + state.settings.general.preferredNFTExplorerIds[ + convertGenesisHashToHex(selectedNetwork.genesisHash).toUpperCase() + ] + ) || + selectedNetwork.nftExplorers[0] || + null + ); + }); +} diff --git a/src/extension/selectors/useSelectSavingSettings.ts b/src/extension/selectors/settings/useSelectSettingsSaving.ts similarity index 53% rename from src/extension/selectors/useSelectSavingSettings.ts rename to src/extension/selectors/settings/useSelectSettingsSaving.ts index 7f50b359..960af9e7 100644 --- a/src/extension/selectors/useSelectSavingSettings.ts +++ b/src/extension/selectors/settings/useSelectSettingsSaving.ts @@ -1,7 +1,8 @@ import { useSelector } from 'react-redux'; // types -import { IMainRootState } from '@extension/types'; -export default function useSelectSavingSettings(): boolean { +import type { IMainRootState } from '@extension/types'; + +export default function useSelectSettingsSaving(): boolean { return useSelector((state) => state.settings.saving); } diff --git a/src/extension/selectors/useSelectFetchingARC0200Assets.ts b/src/extension/selectors/useSelectFetchingARC0200Assets.ts deleted file mode 100644 index cce9e559..00000000 --- a/src/extension/selectors/useSelectFetchingARC0200Assets.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useSelector } from 'react-redux'; - -// types -import { IMainRootState } from '@extension/types'; -export default function useSelectFetchingARC0200Assets(): boolean { - return useSelector( - (state) => state.arc200Assets.fetching - ); -} diff --git a/src/extension/selectors/useSelectUpdatingArc200Assets.ts b/src/extension/selectors/useSelectUpdatingArc200Assets.ts deleted file mode 100644 index 4a6d8f7a..00000000 --- a/src/extension/selectors/useSelectUpdatingArc200Assets.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useSelector } from 'react-redux'; - -// types -import { IMainRootState } from '@extension/types'; - -export default function useSelectUpdatingArc200Assets(): boolean { - return useSelector( - (state) => state.arc200Assets.updating - ); -} diff --git a/src/extension/services/ARC0072AssetService/ARC0072AssetService.ts b/src/extension/services/ARC0072AssetService/ARC0072AssetService.ts new file mode 100644 index 00000000..6b0318ea --- /dev/null +++ b/src/extension/services/ARC0072AssetService/ARC0072AssetService.ts @@ -0,0 +1,103 @@ +// constants +import { ARC0072_ASSETS_KEY_PREFIX } from '@extension/constants'; + +// enums +import { AssetTypeEnum } from '@extension/enums'; + +// services +import StorageManager from '../StorageManager'; + +// types +import type { IBaseOptions, ILogger } from '@common/types'; +import type { IARC0072Asset } from '@extension/types'; + +// utils +import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; + +export default class ARC0072AssetService { + // private variables + private readonly logger: ILogger | null; + private readonly storageManager: StorageManager; + + constructor({ logger }: IBaseOptions) { + this.logger = logger || null; + this.storageManager = new StorageManager(); + } + + /** + * public static functions + */ + + public static initializeDefaultARC0072Asset(): IARC0072Asset { + return { + id: '0', + totalSupply: '0', + type: AssetTypeEnum.ARC0072, + verified: false, + }; + } + + /** + * private functions + */ + + /** + * Convenience function that simply creates an item key by hex encoding the genesis hash. + * @param {string} genesisHash - the genesis hash to use to index. + * @returns {string} the ARC-0072 asset item key. + */ + private createItemKey(genesisHash: string): string { + return `${ARC0072_ASSETS_KEY_PREFIX}${convertGenesisHashToHex( + genesisHash + ).toUpperCase()}`; + } + + /** + * public functions + */ + + /** + * Gets the ARC-0072 assets for a given genesis hash. + * @param {string} genesisHash - genesis hash for a network. + * @returns {Promise} the list of standard assets. + */ + public async getByGenesisHash(genesisHash: string): Promise { + const assets: IARC0072Asset[] | null = await this.storageManager.getItem( + this.createItemKey(genesisHash) + ); + + if (!assets) { + return []; + } + + return assets.map((value) => ({ + ...ARC0072AssetService.initializeDefaultARC0072Asset(), // add any new properties + ...value, + })); + } + + /** + * Removes all the ARC-0072 assets by the network's genesis hash. + * @param {string} genesisHash - genesis hash for a network. + */ + public async removeByGenesisHash(genesisHash: string): Promise { + await this.storageManager.remove(this.createItemKey(genesisHash)); + } + + /** + * Saves ARC-0072 assets to storage by the network's genesis hash. + * @param {string} genesisHash - genesis hash for a network. + * @param {IARC0072Asset[]} assets - the ARC-0072 assets to save to storage. + * @returns {IARC0072Asset[]} the saved ARC-0072 assets. + */ + public async saveByGenesisHash( + genesisHash: string, + assets: IARC0072Asset[] + ): Promise { + await this.storageManager.setItems({ + [this.createItemKey(genesisHash)]: assets, + }); + + return assets; + } +} diff --git a/src/extension/services/ARC0072AssetService/index.ts b/src/extension/services/ARC0072AssetService/index.ts new file mode 100644 index 00000000..8776919f --- /dev/null +++ b/src/extension/services/ARC0072AssetService/index.ts @@ -0,0 +1 @@ +export { default } from './ARC0072AssetService'; diff --git a/src/extension/services/ARC0200AssetService/ARC0200AssetService.ts b/src/extension/services/ARC0200AssetService/ARC0200AssetService.ts index 0ced7a39..85fc525c 100644 --- a/src/extension/services/ARC0200AssetService/ARC0200AssetService.ts +++ b/src/extension/services/ARC0200AssetService/ARC0200AssetService.ts @@ -28,7 +28,7 @@ export default class ARC0200AssetService { * public static functions */ - public static initializeDefaultArc200Asset(): IARC0200Asset { + public static initializeDefaultARC0200Asset(): IARC0200Asset { return { decimals: 0, iconUrl: null, @@ -75,7 +75,7 @@ export default class ARC0200AssetService { } return assets.map((value) => ({ - ...ARC0200AssetService.initializeDefaultArc200Asset(), // add any new properties + ...ARC0200AssetService.initializeDefaultARC0200Asset(), // add any new properties ...value, })); } diff --git a/src/extension/services/AccountService/AccountService.ts b/src/extension/services/AccountService/AccountService.ts index e0f25205..d3ee4275 100644 --- a/src/extension/services/AccountService/AccountService.ts +++ b/src/extension/services/AccountService/AccountService.ts @@ -147,6 +147,7 @@ export default class AccountService { public static initializeDefaultAccountInformation(): IAccountInformation { return { + arc0072AssetHoldings: [], arc200AssetHoldings: [], atomicBalance: '0', authAddress: null, @@ -224,6 +225,7 @@ export default class AccountService { accountInformation: IAccountInformation ): IAccountInformation { return { + arc0072AssetHoldings: accountInformation.arc0072AssetHoldings, arc200AssetHoldings: accountInformation.arc200AssetHoldings, atomicBalance: accountInformation.atomicBalance, authAddress: accountInformation.authAddress, diff --git a/src/extension/services/SettingsService/SettingsService.ts b/src/extension/services/SettingsService/SettingsService.ts index c5296362..b2aa9ff2 100644 --- a/src/extension/services/SettingsService/SettingsService.ts +++ b/src/extension/services/SettingsService/SettingsService.ts @@ -57,6 +57,7 @@ export default class SettingsService { }, general: { preferredBlockExplorerIds: {}, + preferredNFTExplorerIds: {}, selectedNetworkGenesisHash: defaultNetwork.genesisHash, }, security: { diff --git a/src/extension/translations/en.ts b/src/extension/translations/en.ts index f6199df4..ec3fa6d0 100644 --- a/src/extension/translations/en.ts +++ b/src/extension/translations/en.ts @@ -143,6 +143,8 @@ const translation: IResourceLanguage = { 'You can create a new account or import an existing account.', noAssetsFound: 'You have not added any assets. Try adding one now.', noBlockExplorersAvailable: 'No block explorers available', + noNFTExplorersAvailable: 'No NFT explorers available', + noNFTsFound: `You don't have any NFTs.`, noSessionsFound: 'Enabled dApps will appear here.', noThemesAvailable: 'No themes available', offline: 'It looks like you are offline, some features may not work', @@ -154,6 +156,7 @@ const translation: IResourceLanguage = { passwordScoreInfo: 'To conform with our <2>Strong Password Policy, you are required to use a sufficiently strong password. Password must be at least 8 characters.', preferredBlockExplorer: 'Used when opening chain information in new tabs.', + preferredNFTExplorer: 'Used when opening NFTs.', removeAccount: 'Are you sure you want to remove account "{{address}}"?', removeAccountWarning: 'To add this account back you will need the seed phrase', @@ -240,6 +243,7 @@ const translation: IResourceLanguage = { newAccountComplete: 'Almost There...', noAccountsFound: 'No accounts found', noAssetsFound: 'No assets found', + noNFTsFound: 'No NFTs found', noTransactionsFound: 'No transactions found', noSessionsFound: 'No sessions found', numberOfTransactions: '{{number}} transaction', @@ -368,6 +372,7 @@ const translation: IResourceLanguage = { passwordLockDuration_1800000: '30 minutes', passwordLockTimeout: 'Password lock timeout', preferredBlockExplorer: 'Preferred Block Explorer', + preferredNFTExplorer: 'Preferred NFT Explorer', removeAccount: 'Remove Account', removeSession: 'Remove Session', reserveAccount: 'Reserve Account', @@ -383,6 +388,7 @@ const translation: IResourceLanguage = { symbol: 'Symbol', theme: 'Theme', to: 'To', + tokenId: 'Token ID', totalSupply: 'Total Supply', type: 'Type', unitName: 'Unit Name', diff --git a/src/extension/types/IARC0003TokenMetadata.ts b/src/extension/types/IARC0003TokenMetadata.ts new file mode 100644 index 00000000..9c8124b6 --- /dev/null +++ b/src/extension/types/IARC0003TokenMetadata.ts @@ -0,0 +1,41 @@ +// types +import IARC0003TokenMetadataLocalization from './IARC0003TokenMetadataLocalization'; + +/** + * @property {string} animation_url - [optional] A URI pointing to a multi-media file representing the asset. + * @property {string} animation_url_integrity - [optional] The SHA-256 digest of the file pointed by the URI external_url. The field value is a single SHA-256 integrity metadata as defined in the W3C subresource integrity specification (https://w3c.github.io/webappsec-subresource-integrity). + * @property {string} animation_url_mimetype - [optional] The MIME type of the file pointed by the URI animation_url. If the MIME type is not specified, clients MAY guess the MIME type from the file extension or MAY decide not to display the asset at all. It is STRONGLY RECOMMENDED to include the MIME type. + * @property {string} background_color - [optional] Background color do display the asset. MUST be a six-character hexadecimal without a pre-pended #. + * @property {number} decimals - [optional] The number of decimal places that the token amount should display - e.g. 18, means to divide the token amount by 1000000000000000000 to get its user representation. + * @property {string} description - [optional] Describes the asset to which this token represents. + * @property {string} external_url - [optional] A URI pointing to an external website presenting the asset. + * @property {string} external_url_integrity - [optional] The SHA-256 digest of the file pointed by the URI external_url. The field value is a single SHA-256 integrity metadata as defined in the W3C subresource integrity specification (https://w3c.github.io/webappsec-subresource-integrity). + * @property {string} external_url_mimetype - [optional] The MIME type of the file pointed by the URI external_url. It is expected to be 'text/html' in almost all cases. + * @property {string} extra_metadata - [optional] Extra metadata in base64. If the field is specified (even if it is an empty string) the asset metadata (am) of the ASA is computed differently than if it is not specified. + * @property {string} image - [optional] A URI pointing to a file with MIME type image/* representing the asset to which this token represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive. + * @property {string} image_integrity - [optional] The SHA-256 digest of the file pointed by the URI image. The field value is a single SHA-256 integrity metadata as defined in the W3C subresource integrity specification (https://w3c.github.io/webappsec-subresource-integrity). + * @property {string} image_mimetype - [optional] The MIME type of the file pointed by the URI external_url. It is expected to be 'text/html' in almost all cases. + * @property {IARC0003TokenMetadataLocalization} localization - [optional] Localization metadata. + * @property {string} name - [optional] Identifies the asset to which this token represents. + * @property {Properties} properties - [optional] Arbitrary properties (also called attributes). Values may be strings, numbers, object or arrays. + */ +interface IARC0003TokenMetadata { + animation_url?: string; + animation_url_integrity?: string; + animation_url_mimetype?: string; + background_color?: string; + decimals?: number; + description?: string; + external_url?: string; + external_url_integrity?: string; + external_url_mimetype?: string; + extra_metadata?: string; + image?: string; + image_integrity?: string; + image_mimetype?: string; + localization?: IARC0003TokenMetadataLocalization; + name?: string; + properties?: Properties; +} + +export default IARC0003TokenMetadata; diff --git a/src/extension/types/IARC0003TokenMetadataLocalization.ts b/src/extension/types/IARC0003TokenMetadataLocalization.ts new file mode 100644 index 00000000..efea9afe --- /dev/null +++ b/src/extension/types/IARC0003TokenMetadataLocalization.ts @@ -0,0 +1,14 @@ +/** + * @property {string} default - The locale of the default data within the base JSON. + * @property {string} locales - The list of locales for which data is available. These locales should conform to those defined in the Unicode Common Locale Data Repository (http://cldr.unicode.org/). + * @property {Record} integrity - [optional] The SHA-256 digests of the localized JSON files (except the default one). The field name is the locale. The field value is a single SHA-256 integrity metadata as defined in the W3C subresource integrity specification (https://w3c.github.io/webappsec-subresource-integrity). + * @property {string} uri - The URI pattern to fetch localized data from. This URI should contain the substring `{locale}` which will be replaced with the appropriate locale value before sending the request. + */ +interface IARC0003TokenMetadataLocalization { + default: string; + locales: string[]; + integrity?: Record; + uri: string; +} + +export default IARC0003TokenMetadataLocalization; diff --git a/src/extension/types/IARC0072Asset.ts b/src/extension/types/IARC0072Asset.ts new file mode 100644 index 00000000..5bd59df4 --- /dev/null +++ b/src/extension/types/IARC0072Asset.ts @@ -0,0 +1,19 @@ +// enums +import { AssetTypeEnum } from '@extension/enums'; + +// types +import IBaseAsset from './IBaseAsset'; + +/** + * @property {string} id - the app ID of the ARC-0072 asset. + * @property {number} decimals - the number of digits to use after the decimal point when displaying this ARC-200 asset. + * @property {string} totalSupply - the total supply of the ARC-0072 asset. + * @property {AssetTypeEnum.ARC0072} type - indicates the asset type is of "arc0072". + */ +interface IARC0072Asset extends IBaseAsset { + id: string; + totalSupply: string; + type: AssetTypeEnum.ARC0072; +} + +export default IARC0072Asset; diff --git a/src/extension/types/IARC0072AssetHolding.ts b/src/extension/types/IARC0072AssetHolding.ts new file mode 100644 index 00000000..d15e6a43 --- /dev/null +++ b/src/extension/types/IARC0072AssetHolding.ts @@ -0,0 +1,13 @@ +// enums +import { AssetTypeEnum } from '@extension/enums'; + +// types +import { IARC0003TokenMetadata, IBaseAssetHolding } from '@extension/types'; + +interface IARC0072AssetHolding extends IBaseAssetHolding { + metadata: IARC0003TokenMetadata; + tokenId: string; + type: AssetTypeEnum.ARC0072; +} + +export default IARC0072AssetHolding; diff --git a/src/extension/types/IARC0072AssetInformation.ts b/src/extension/types/IARC0072AssetInformation.ts new file mode 100644 index 00000000..f13e6d64 --- /dev/null +++ b/src/extension/types/IARC0072AssetInformation.ts @@ -0,0 +1,5 @@ +interface IARC0072AssetInformation { + totalSupply: bigint; +} + +export default IARC0072AssetInformation; diff --git a/src/extension/types/IARC0072Indexer.ts b/src/extension/types/IARC0072Indexer.ts new file mode 100644 index 00000000..a87b51fd --- /dev/null +++ b/src/extension/types/IARC0072Indexer.ts @@ -0,0 +1,13 @@ +// types +import IARC0072AssetHolding from './IARC0072AssetHolding'; +import IARC0072IndexerFetchTokensByOwnerOptions from './IARC0072IndexerFetchTokensByOwnerOptions'; + +interface IARC0072Indexer { + canonicalName: string; + fetchTokensByOwner: ( + options: IARC0072IndexerFetchTokensByOwnerOptions + ) => Promise; + id: string; +} + +export default IARC0072Indexer; diff --git a/src/extension/types/IARC0072IndexerFetchTokensByOwnerOptions.ts b/src/extension/types/IARC0072IndexerFetchTokensByOwnerOptions.ts new file mode 100644 index 00000000..fee829f0 --- /dev/null +++ b/src/extension/types/IARC0072IndexerFetchTokensByOwnerOptions.ts @@ -0,0 +1,8 @@ +// types +import type { IBaseOptions } from '@common/types'; + +interface IARC0072IndexerFetchTokensByOwnerOptions extends IBaseOptions { + address: string; +} + +export default IARC0072IndexerFetchTokensByOwnerOptions; diff --git a/src/extension/types/IARC0200Asset.ts b/src/extension/types/IARC0200Asset.ts index 70748eb1..d0aea07c 100644 --- a/src/extension/types/IARC0200Asset.ts +++ b/src/extension/types/IARC0200Asset.ts @@ -5,15 +5,17 @@ import { AssetTypeEnum } from '@extension/enums'; import IBaseAsset from './IBaseAsset'; /** - * @property {number} decimals - the number of digits to use after the decimal point when displaying this ARC-200 asset. - * @property {string} id - the app ID of the ARC-200 asset. - * @property {string} name - the utf-8 name of the ARC-200 asset. - * @property {string} symbol - the utf-8 symbol of the ARC-200 asset. - * @property {string} totalSupply - the total supply of this ARC-200 asset. - * @property {AssetTypeEnum.Standard} type - indicates the asset type is of "arc200". + * @property {number} decimals - the number of digits to use after the decimal point when displaying this ARC-0200 asset. + * @property {string | null} iconUrl - the URL of the asset icon. + * @property {string} id - the app ID of the ARC-0200 asset. + * @property {string} name - the utf-8 name of the ARC-0200 asset. + * @property {string} symbol - the utf-8 symbol of the ARC-0200 asset. + * @property {string} totalSupply - the total supply of this ARC-0200 asset. + * @property {AssetTypeEnum.ARC0200} type - indicates the asset type is of "arc200". */ interface IARC0200Asset extends IBaseAsset { decimals: number; + iconUrl: string | null; id: string; name: string; symbol: string; diff --git a/src/extension/types/IAccountInformation.ts b/src/extension/types/IAccountInformation.ts index fdeb9483..2b7e2731 100644 --- a/src/extension/types/IAccountInformation.ts +++ b/src/extension/types/IAccountInformation.ts @@ -1,9 +1,11 @@ // types +import IARC0072AssetHolding from './IARC0072AssetHolding'; import IARC0200AssetHolding from './IARC0200AssetHolding'; import IStandardAssetHolding from './IStandardAssetHolding'; /** - * @property {IARC0200AssetHolding} arc200AssetHoldings - the arc200 assets this account holds. + * @property {IARC0072AssetHolding} arc0072AssetHoldings - the ARC-0072 assets this account holds. + * @property {IARC0200AssetHolding} arc200AssetHoldings - the ARC-0200 assets this account holds. * @property {string} atomicBalance - the atomic balance of this account as a string. * @property {string | null} authAddress - the address that this account has been rekeyed with. * @property {string} minAtomicBalance - the minimum balance for this account. @@ -12,6 +14,7 @@ import IStandardAssetHolding from './IStandardAssetHolding'; * updated. */ interface IAccountInformation { + arc0072AssetHoldings: IARC0072AssetHolding[]; arc200AssetHoldings: IARC0200AssetHolding[]; atomicBalance: string; authAddress: string | null; diff --git a/src/extension/types/IAccountTransactions.ts b/src/extension/types/IAccountTransactions.ts index 3f690785..3c5c6b9d 100644 --- a/src/extension/types/IAccountTransactions.ts +++ b/src/extension/types/IAccountTransactions.ts @@ -1,5 +1,5 @@ // types -import { ITransactions } from '@extension/types'; +import type { ITransactions } from '@extension/types'; /** * @property {string | null} next - the token for the next page of results. diff --git a/src/extension/types/IBackgroundRootState.ts b/src/extension/types/IBackgroundRootState.ts index fc310a15..307d78a9 100644 --- a/src/extension/types/IBackgroundRootState.ts +++ b/src/extension/types/IBackgroundRootState.ts @@ -1,18 +1,16 @@ // features -import { IAccountsState } from '@extension/features/accounts'; -import { IARC0200AssetsState } from '@extension/features/arc200-assets'; -import { IEventsState } from '@extension/features/events'; -import { INetworksState } from '@extension/features/networks'; -import { ISessionsState } from '@extension/features/sessions'; -import { ISettingsState } from '@extension/features/settings'; -import { IStandardAssetsState } from '@extension/features/standard-assets'; +import type { IAccountsState } from '@extension/features/accounts'; +import type { IEventsState } from '@extension/features/events'; +import type { INetworksState } from '@extension/features/networks'; +import type { ISessionsState } from '@extension/features/sessions'; +import type { ISettingsState } from '@extension/features/settings'; +import type { IStandardAssetsState } from '@extension/features/standard-assets'; // types -import IBaseRootState from './IBaseRootState'; +import type IBaseRootState from './IBaseRootState'; interface IBackgroundRootState extends IBaseRootState { accounts: IAccountsState; - arc200Assets: IARC0200AssetsState; events: IEventsState; networks: INetworksState; sessions: ISessionsState; diff --git a/src/extension/types/IBaseAsset.ts b/src/extension/types/IBaseAsset.ts index a45b3f17..71d841b0 100644 --- a/src/extension/types/IBaseAsset.ts +++ b/src/extension/types/IBaseAsset.ts @@ -2,12 +2,12 @@ import { AssetTypeEnum } from '@extension/enums'; /** - * @property {string | null} iconUrl - the URL of the asset icon. + * @property {string} id - the id of the asset. For app-based assets, this represents the application ID, for standard + * assets, this represents the asset index. * @property {AssetTypeEnum} type - indicates the type of asset. * @property {boolean} verified - whether this asset is verified according to vestige.fi */ interface IBaseAsset { - iconUrl: string | null; type: AssetTypeEnum; verified: boolean; } diff --git a/src/extension/types/IBaseRootState.ts b/src/extension/types/IBaseRootState.ts index 582738bc..d2e05952 100644 --- a/src/extension/types/IBaseRootState.ts +++ b/src/extension/types/IBaseRootState.ts @@ -1,7 +1,9 @@ // features -import { ISystemState } from '@extension/features/system'; +import type { IARC0200AssetsState } from '@extension/features/arc0200-assets'; +import type { ISystemState } from '@extension/features/system'; interface IBaseRootState { + arc0200Assets: IARC0200AssetsState; system: ISystemState; } diff --git a/src/extension/types/IExplorer.ts b/src/extension/types/IBlockExplorer.ts similarity index 78% rename from src/extension/types/IExplorer.ts rename to src/extension/types/IBlockExplorer.ts index dd0746ab..348532ab 100644 --- a/src/extension/types/IExplorer.ts +++ b/src/extension/types/IBlockExplorer.ts @@ -1,4 +1,4 @@ -interface IExplorer { +interface IBlockExplorer { accountPath: string; applicationPath: string; assetPath: string; @@ -10,4 +10,4 @@ interface IExplorer { transactionPath: string; } -export default IExplorer; +export default IBlockExplorer; diff --git a/src/extension/types/IFetchAssetWithDelayOptions.ts b/src/extension/types/IFetchAssetWithDelayOptions.ts new file mode 100644 index 00000000..46db056a --- /dev/null +++ b/src/extension/types/IFetchAssetWithDelayOptions.ts @@ -0,0 +1,11 @@ +// types +import type { IBaseOptions } from '@common/types'; +import INetwork from './INetwork'; + +interface IFetchAssetWithDelayOptions extends IBaseOptions { + delay: number; + id: string; + network: INetwork; +} + +export default IFetchAssetWithDelayOptions; diff --git a/src/extension/types/IGeneralSettings.ts b/src/extension/types/IGeneralSettings.ts index 04aa593a..29a9b869 100644 --- a/src/extension/types/IGeneralSettings.ts +++ b/src/extension/types/IGeneralSettings.ts @@ -1,5 +1,6 @@ interface IGeneralSettings { preferredBlockExplorerIds: Record; + preferredNFTExplorerIds: Record; selectedNetworkGenesisHash: string | null; } diff --git a/src/extension/types/IMainRootState.ts b/src/extension/types/IMainRootState.ts index ddf9f604..8648d4c3 100644 --- a/src/extension/types/IMainRootState.ts +++ b/src/extension/types/IMainRootState.ts @@ -1,15 +1,16 @@ // features -import { IAccountsState } from '@extension/features/accounts'; -import { IAddAssetState } from '@extension/features/add-asset'; -import { IARC0200AssetsState } from '@extension/features/arc200-assets'; -import { IEventsState } from '@extension/features/events'; -import { INetworksState } from '@extension/features/networks'; -import { INotificationsState } from '@extension/features/notifications'; -import { IPasswordLockState } from '@extension/features/password-lock'; -import { ISendAssetsState } from '@extension/features/send-assets'; -import { ISessionsState } from '@extension/features/sessions'; -import { ISettingsState } from '@extension/features/settings'; -import { IStandardAssetsState } from '@extension/features/standard-assets'; +import type { IAccountsState } from '@extension/features/accounts'; +import type { IAddAssetState } from '@extension/features/add-asset'; +import type { IARC0200AssetsState } from '@extension/features/arc0200-assets'; +import type { IState as IARC0072AssetsState } from '@extension/features/arc0072-assets'; +import type { IEventsState } from '@extension/features/events'; +import type { INetworksState } from '@extension/features/networks'; +import type { INotificationsState } from '@extension/features/notifications'; +import type { IPasswordLockState } from '@extension/features/password-lock'; +import type { ISendAssetsState } from '@extension/features/send-assets'; +import type { ISessionsState } from '@extension/features/sessions'; +import type { ISettingsState } from '@extension/features/settings'; +import type { IStandardAssetsState } from '@extension/features/standard-assets'; // types import IBaseRootState from './IBaseRootState'; @@ -17,7 +18,7 @@ import IBaseRootState from './IBaseRootState'; interface IMainRootState extends IBaseRootState { accounts: IAccountsState; addAsset: IAddAssetState; - arc200Assets: IARC0200AssetsState; + arc0072Assets: IARC0072AssetsState; events: IEventsState; networks: INetworksState; notifications: INotificationsState; diff --git a/src/extension/types/INFTExplorer.ts b/src/extension/types/INFTExplorer.ts new file mode 100644 index 00000000..08d7d191 --- /dev/null +++ b/src/extension/types/INFTExplorer.ts @@ -0,0 +1,9 @@ +interface INFTExplorer { + baseURL: string; + canonicalName: string; + collectionPath: (appId: string) => string; + id: string; + tokenPath: (appId: string, tokenId: string) => string; +} + +export default INFTExplorer; diff --git a/src/extension/types/INetwork.ts b/src/extension/types/INetwork.ts index 326b511a..57450eec 100644 --- a/src/extension/types/INetwork.ts +++ b/src/extension/types/INetwork.ts @@ -2,22 +2,26 @@ import { NetworkTypeEnum } from '../enums'; // types -import IChainNamespace from './IChainNamespace'; -import IExplorer from './IExplorer'; -import INativeCurrency from './INativeCurrency'; -import INode from './INode'; +import type IARC0072Indexer from './IARC0072Indexer'; +import type IChainNamespace from './IChainNamespace'; +import type IBlockExplorer from './IBlockExplorer'; +import type INativeCurrency from './INativeCurrency'; +import type INFTExplorer from './INFTExplorer'; +import type INode from './INode'; interface INetwork { + algods: INode[]; + arc0072Indexers: IARC0072Indexer[]; + blockExplorers: IBlockExplorer[]; canonicalName: string; chakraTheme: string; - explorers: IExplorer[]; feeSunkAddress: string; genesisHash: string; genesisId: string; + indexers: INode[]; namespace: IChainNamespace; nativeCurrency: INativeCurrency; - indexers: INode[]; - algods: INode[]; + nftExplorers: INFTExplorer[]; type: NetworkTypeEnum; } diff --git a/src/extension/types/IRegistrationRootState.ts b/src/extension/types/IRegistrationRootState.ts index 8f1eefe9..0a3ee672 100644 --- a/src/extension/types/IRegistrationRootState.ts +++ b/src/extension/types/IRegistrationRootState.ts @@ -1,5 +1,4 @@ // features -import type { IARC0200AssetsState } from '@extension/features/arc200-assets'; import type { INetworksState } from '@extension/features/networks'; import type { INotificationsState } from '@extension/features/notifications'; import type { IRegistrationState } from '@extension/features/registration'; @@ -7,10 +6,9 @@ import type { ISettingsState } from '@extension/features/settings'; import type { ISystemState } from '@extension/features/system'; // types -import IBaseRootState from './IBaseRootState'; +import type IBaseRootState from './IBaseRootState'; interface IRegistrationRootState extends IBaseRootState { - arc200Assets: IARC0200AssetsState; networks: INetworksState; notifications: INotificationsState; registration: IRegistrationState; diff --git a/src/extension/types/IStandardAsset.ts b/src/extension/types/IStandardAsset.ts index cc6d22fa..210de4d4 100644 --- a/src/extension/types/IStandardAsset.ts +++ b/src/extension/types/IStandardAsset.ts @@ -11,6 +11,7 @@ import IBaseAsset from './IBaseAsset'; * @property {boolean} defaultFrozen - whether holdings of this asset are frozen by default. * @property {boolean} deleted - whether this asset is deleted or not. * @property {string | null} freezeAddress - the address of the account that can freeze this asset. + * @property {string | null} iconUrl - the URL of the asset icon. * @property {string} id - the ID of this asset. * @property {string | null} managerAddress - the address of the account that can manage the keys of this asset and to * destroy it. @@ -34,6 +35,7 @@ interface IStandardAsset extends IBaseAsset { defaultFrozen: boolean; deleted: boolean; freezeAddress: string | null; + iconUrl: string | null; id: string; managerAddress: string | null; metadataHash: string | null; diff --git a/src/extension/types/IStorageItemTypes.ts b/src/extension/types/IStorageItemTypes.ts index 1e307549..805ef193 100644 --- a/src/extension/types/IStorageItemTypes.ts +++ b/src/extension/types/IStorageItemTypes.ts @@ -4,6 +4,7 @@ import IActiveAccountDetails from './IActiveAccountDetails'; import IAdvancedSettings from './IAdvancedSettings'; import IAppearanceSettings from './IAppearanceSettings'; import IAppWindow from './IAppWindow'; +import IARC0072Asset from './IARC0072Asset'; import IARC0200Asset from './IARC0200Asset'; import IEvent from './IEvent'; import IGeneralSettings from './IGeneralSettings'; @@ -21,6 +22,7 @@ type IStorageItemTypes = | IAdvancedSettings | IAppearanceSettings | IAppWindow + | IARC0072Asset[] | IARC0200Asset[] | IGeneralSettings | IEvent[] diff --git a/src/extension/types/IUpdateAssetInformationByIdOptions.ts b/src/extension/types/IUpdateAssetInformationByIdOptions.ts new file mode 100644 index 00000000..5f7b7b0b --- /dev/null +++ b/src/extension/types/IUpdateAssetInformationByIdOptions.ts @@ -0,0 +1,10 @@ +// types +import type { IBaseOptions } from '@common/types'; +import INetwork from './INetwork'; + +interface IUpdateAssetInformationByIdOptions extends IBaseOptions { + delay?: number; + network: INetwork; +} + +export default IUpdateAssetInformationByIdOptions; diff --git a/src/extension/types/index.ts b/src/extension/types/index.ts index 788e62df..9075e4ab 100644 --- a/src/extension/types/index.ts +++ b/src/extension/types/index.ts @@ -33,6 +33,13 @@ export type { default as IAppThunkDispatchReturn } from './IAppThunkDispatchRetu export type { default as IApplicationTransaction } from './IApplicationTransaction'; export type { default as IApplicationTransactionTypes } from './IApplicationTransactionTypes'; export type { default as IAppWindow } from './IAppWindow'; +export type { default as IARC0003TokenMetadata } from './IARC0003TokenMetadata'; +export type { default as IARC0003TokenMetadataLocalization } from './IARC0003TokenMetadataLocalization'; +export type { default as IARC0072Asset } from './IARC0072Asset'; +export type { default as IARC0072AssetHolding } from './IARC0072AssetHolding'; +export type { default as IARC0072AssetInformation } from './IARC0072AssetInformation'; +export type { default as IARC0072Indexer } from './IARC0072Indexer'; +export type { default as IARC0072IndexerFetchTokensByOwnerOptions } from './IARC0072IndexerFetchTokensByOwnerOptions'; export type { default as IARC0200Asset } from './IARC0200Asset'; export type { default as IARC0200AssetHolding } from './IARC0200AssetHolding'; export type { default as IARC0200AssetInformation } from './IARC0200AssetInformation'; @@ -55,13 +62,15 @@ export type { default as IBaseAssetHolding } from './IBaseAssetHolding'; export type { default as IBaseAsyncThunkConfig } from './IBaseAsyncThunkConfig'; export type { default as IBaseRootState } from './IBaseRootState'; export type { default as IBaseTransaction } from './IBaseTransaction'; +export type { default as IBlockExplorer } from './IBlockExplorer'; +export type { default as IChainNamespace } from './IChainNamespace'; export type { default as IClientEventPayload } from './IClientEventPayload'; export type { default as IClientRequest } from './IClientRequest'; export type { default as IDecodedJwt } from './IDecodedJwt'; export type { default as IDecodedJwtHeader } from './IDecodedJwtHeader'; export type { default as IDecodedJwtPayload } from './IDecodedJwtPayload'; -export type { default as IExplorer } from './IExplorer'; export type { default as IEvent } from './IEvent'; +export type { default as IFetchAssetWithDelayOptions } from './IFetchAssetWithDelayOptions'; export type { default as IFulfilledActionMeta } from './IFulfilledActionMeta'; export type { default as IGeneralSettings } from './IGeneralSettings'; export type { default as IInitializeAccountOptions } from './IInitializeAccountOptions'; @@ -72,6 +81,7 @@ export type { default as IMainRootState } from './IMainRootState'; export type { default as INativeCurrency } from './INativeCurrency'; export type { default as INetwork } from './INetwork'; export type { default as INetworkWithTransactionParams } from './INetworkWithTransactionParams'; +export type { default as INFTExplorer } from './INFTExplorer'; export type { default as INode } from './INode'; export type { default as INotification } from './INotification'; export type { default as INotificationType } from './INotificationType'; @@ -94,7 +104,7 @@ export type { default as IStorageItemTypes } from './IStorageItemTypes'; export type { default as ITinyManAssetResponse } from './ITinyManAssetResponse'; export type { default as ITransactionParams } from './ITransactionParams'; export type { default as ITransactions } from './ITransactions'; -export type { default as IVestigeFiAssetResponse } from './IVestigeFiAssetResponse'; export type { default as IUnknownTransaction } from './IUnknownTransaction'; -export type { default as IChainNamespace } from './IChainNamespace'; +export type { default as IUpdateAssetInformationByIdOptions } from './IUpdateAssetInformationByIdOptions'; +export type { default as IVestigeFiAssetResponse } from './IVestigeFiAssetResponse'; export type { default as IWalletConnectSessionMetadata } from './IWalletConnectSessionMetadata'; diff --git a/src/extension/utils/fetchARC0072AssetHoldingsWithDelay/fetchARC0072AssetHoldingWithDelay.ts b/src/extension/utils/fetchARC0072AssetHoldingsWithDelay/fetchARC0072AssetHoldingWithDelay.ts new file mode 100644 index 00000000..3534a873 --- /dev/null +++ b/src/extension/utils/fetchARC0072AssetHoldingsWithDelay/fetchARC0072AssetHoldingWithDelay.ts @@ -0,0 +1,51 @@ +// types +import type { IARC0072AssetHolding, IARC0072Indexer } from '@extension/types'; +import type { IOptions } from './types'; + +// utils +import getRandomItem from '@common/utils/getRandomItem'; + +/** + * Fetches ARC-0072 asset holdings from the node ARC-0072 indexer with a delay. + * @param {IOptions} options - options needed to send the request. + * @returns {IARC0072AssetHolding[]} the ARC-0072 asset holdings. + */ +export default async function fetchARC0072AssetHoldingsWithDelay({ + address, + delay, + logger, + network, +}: IOptions): Promise { + const _functionName: string = 'fetchARC0072AssetHoldingsWithDelay'; + const arc0072Indexer: IARC0072Indexer | null = + getRandomItem(network.arc0072Indexers) || null; + + if (!arc0072Indexer) { + logger?.debug( + `${_functionName}: no arc-0072 indexers found for network "${network.genesisId}"` + ); + + return []; + } + + return new Promise((resolve, reject) => + setTimeout(async () => { + let result: IARC0072AssetHolding[]; + + try { + result = await arc0072Indexer.fetchTokensByOwner({ + address, + logger, + }); + + resolve(result); + } catch (error) { + reject(error); + } + return await arc0072Indexer.fetchTokensByOwner({ + address, + logger, + }); + }, delay) + ); +} diff --git a/src/extension/utils/fetchARC0072AssetHoldingsWithDelay/index.ts b/src/extension/utils/fetchARC0072AssetHoldingsWithDelay/index.ts new file mode 100644 index 00000000..79a82b66 --- /dev/null +++ b/src/extension/utils/fetchARC0072AssetHoldingsWithDelay/index.ts @@ -0,0 +1 @@ +export { default } from './fetchARC0072AssetHoldingWithDelay'; diff --git a/src/extension/utils/fetchARC0200AssetInformationWithDelay/types/IOptions.ts b/src/extension/utils/fetchARC0072AssetHoldingsWithDelay/types/IOptions.ts similarity index 92% rename from src/extension/utils/fetchARC0200AssetInformationWithDelay/types/IOptions.ts rename to src/extension/utils/fetchARC0072AssetHoldingsWithDelay/types/IOptions.ts index 8f07de77..2b06409c 100644 --- a/src/extension/utils/fetchARC0200AssetInformationWithDelay/types/IOptions.ts +++ b/src/extension/utils/fetchARC0072AssetHoldingsWithDelay/types/IOptions.ts @@ -3,9 +3,9 @@ import type { IBaseOptions } from '@common/types'; import type { INetwork } from '@extension/types'; interface IOptions extends IBaseOptions { + address: string; network: INetwork; delay: number; - id: string; } export default IOptions; diff --git a/src/extension/utils/fetchARC0200AssetInformationWithDelay/types/index.ts b/src/extension/utils/fetchARC0072AssetHoldingsWithDelay/types/index.ts similarity index 100% rename from src/extension/utils/fetchARC0200AssetInformationWithDelay/types/index.ts rename to src/extension/utils/fetchARC0072AssetHoldingsWithDelay/types/index.ts diff --git a/src/extension/utils/fetchARC0072AssetInformationWithDelay/fetchARC0072AssetInformationWithDelay.ts b/src/extension/utils/fetchARC0072AssetInformationWithDelay/fetchARC0072AssetInformationWithDelay.ts new file mode 100644 index 00000000..bb082d0a --- /dev/null +++ b/src/extension/utils/fetchARC0072AssetInformationWithDelay/fetchARC0072AssetInformationWithDelay.ts @@ -0,0 +1,54 @@ +import BigNumber from 'bignumber.js'; + +// enums +import { ErrorCodeEnum } from '@extension/enums'; + +// contracts +import ARC0072Contract from '@extension/contracts/ARC0072Contract'; + +// types +import type { + IARC0072AssetInformation, + IFetchAssetWithDelayOptions, +} from '@extension/types'; + +/** + * Fetches ARC-0072 asset information from the node with a delay. + * @param {IFetchAssetWithDelayOptions} options - options needed to send the request. + * @returns {IARC0072AssetInformation | null} ARC-0072 asset information or null if the asset is not an ARC-0072 asset. + */ +export default async function fetchARC0072AssetInformationWithDelay({ + delay, + id, + logger, + network, +}: IFetchAssetWithDelayOptions): Promise { + return new Promise((resolve, reject) => + setTimeout(async () => { + let contract: ARC0072Contract; + let totalSupply: BigNumber; + + try { + contract = new ARC0072Contract({ + appId: new BigNumber(id), + network, + logger, + }); + totalSupply = await contract.totalSupply(); + + resolve({ + totalSupply: BigInt(totalSupply.toString()), + }); + } catch (error) { + switch (error.code) { + case ErrorCodeEnum.InvalidABIContractError: + return resolve(null); + default: + break; + } + + reject(error); + } + }, delay) + ); +} diff --git a/src/extension/utils/fetchARC0072AssetInformationWithDelay/index.ts b/src/extension/utils/fetchARC0072AssetInformationWithDelay/index.ts new file mode 100644 index 00000000..3efa8178 --- /dev/null +++ b/src/extension/utils/fetchARC0072AssetInformationWithDelay/index.ts @@ -0,0 +1 @@ +export { default } from './fetchARC0072AssetInformationWithDelay'; diff --git a/src/extension/features/accounts/utils/fetchARC0200AssetHoldingWithDelay.ts b/src/extension/utils/fetchARC0200AssetHoldingWithDelay/fetchARC0200AssetHoldingWithDelay.ts similarity index 81% rename from src/extension/features/accounts/utils/fetchARC0200AssetHoldingWithDelay.ts rename to src/extension/utils/fetchARC0200AssetHoldingWithDelay/fetchARC0200AssetHoldingWithDelay.ts index fc342af1..3bbe39e9 100644 --- a/src/extension/features/accounts/utils/fetchARC0200AssetHoldingWithDelay.ts +++ b/src/extension/utils/fetchARC0200AssetHoldingWithDelay/fetchARC0200AssetHoldingWithDelay.ts @@ -7,15 +7,8 @@ import ARC0200Contract from '@extension/contracts/ARC0200Contract'; import { AssetTypeEnum } from '@extension/enums'; // types -import type { IBaseOptions } from '@common/types'; -import type { IARC0200AssetHolding, INetwork } from '@extension/types'; - -interface IOptions extends IBaseOptions { - address: string; - appId: string; - network: INetwork; - delay: number; -} +import type { IARC0200AssetHolding } from '@extension/types'; +import type { IOptions } from './types'; /** * Fetches ARC-0200 asset holding from the node with a delay. diff --git a/src/extension/utils/fetchARC0200AssetHoldingWithDelay/index.ts b/src/extension/utils/fetchARC0200AssetHoldingWithDelay/index.ts new file mode 100644 index 00000000..64dbedcd --- /dev/null +++ b/src/extension/utils/fetchARC0200AssetHoldingWithDelay/index.ts @@ -0,0 +1 @@ +export { default } from './fetchARC0200AssetHoldingWithDelay'; diff --git a/src/extension/utils/fetchARC0200AssetHoldingWithDelay/types/IOptions.ts b/src/extension/utils/fetchARC0200AssetHoldingWithDelay/types/IOptions.ts new file mode 100644 index 00000000..4f798b5c --- /dev/null +++ b/src/extension/utils/fetchARC0200AssetHoldingWithDelay/types/IOptions.ts @@ -0,0 +1,12 @@ +// types +import type { IBaseOptions } from '@common/types'; +import type { INetwork } from '@extension/types'; + +interface IOptions extends IBaseOptions { + address: string; + appId: string; + network: INetwork; + delay: number; +} + +export default IOptions; diff --git a/src/extension/utils/fetchARC0200AssetHoldingWithDelay/types/index.ts b/src/extension/utils/fetchARC0200AssetHoldingWithDelay/types/index.ts new file mode 100644 index 00000000..68e70016 --- /dev/null +++ b/src/extension/utils/fetchARC0200AssetHoldingWithDelay/types/index.ts @@ -0,0 +1 @@ +export type { default as IOptions } from './IOptions'; diff --git a/src/extension/utils/fetchARC0200AssetInformationWithDelay/fetchARC0200AssetInformationWithDelay.ts b/src/extension/utils/fetchARC0200AssetInformationWithDelay/fetchARC0200AssetInformationWithDelay.ts index 09139843..f48bc8cb 100644 --- a/src/extension/utils/fetchARC0200AssetInformationWithDelay/fetchARC0200AssetInformationWithDelay.ts +++ b/src/extension/utils/fetchARC0200AssetInformationWithDelay/fetchARC0200AssetInformationWithDelay.ts @@ -7,12 +7,14 @@ import { ErrorCodeEnum } from '@extension/enums'; import ARC0200Contract from '@extension/contracts/ARC0200Contract'; // types -import type { IARC0200AssetInformation } from '@extension/types'; -import type { IOptions } from './types'; +import type { + IARC0200AssetInformation, + IFetchAssetWithDelayOptions, +} from '@extension/types'; /** * Fetches ARC-0200 asset information from the node with a delay. - * @param {IOptions} options - options needed to send the request. + * @param {IFetchAssetWithDelayOptions} options - options needed to send the request. * @returns {IARC0200AssetInformation | null} ARC-0200 asset information or null if the asset is not an ARC-0200 asset. */ export default async function fetchARC0200AssetInformationWithDelay({ @@ -20,7 +22,7 @@ export default async function fetchARC0200AssetInformationWithDelay({ id, logger, network, -}: IOptions): Promise { +}: IFetchAssetWithDelayOptions): Promise { return new Promise((resolve, reject) => setTimeout(async () => { let contract: ARC0200Contract; diff --git a/src/extension/utils/fetchARC0200AssetInformationWithDelay/index.ts b/src/extension/utils/fetchARC0200AssetInformationWithDelay/index.ts index d68243ef..b566b0fe 100644 --- a/src/extension/utils/fetchARC0200AssetInformationWithDelay/index.ts +++ b/src/extension/utils/fetchARC0200AssetInformationWithDelay/index.ts @@ -1,2 +1 @@ export { default } from './fetchARC0200AssetInformationWithDelay'; -export * from './types'; diff --git a/src/extension/utils/fetchVoiARC0072TokensByOwner/fetchVoiARC0072TokensByOwner.ts b/src/extension/utils/fetchVoiARC0072TokensByOwner/fetchVoiARC0072TokensByOwner.ts new file mode 100644 index 00000000..e74c5397 --- /dev/null +++ b/src/extension/utils/fetchVoiARC0072TokensByOwner/fetchVoiARC0072TokensByOwner.ts @@ -0,0 +1,60 @@ +// errors +import { + MalformedDataError, + NetworkConnectionError, + UnknownError, +} from '@extension/errors'; + +// types +import type { + IARC0072AssetHolding, + IARC0072IndexerFetchTokensByOwnerOptions, +} from '@extension/types'; +import type { IResponse } from './types'; +import { AssetTypeEnum } from '@extension/enums'; + +export default async function fetchVoiARC0072TokensByOwner({ + address, + logger, +}: IARC0072IndexerFetchTokensByOwnerOptions): Promise { + const _functionName: string = 'fetchVoiARC0072TokensByOwner'; + let errorMessage: string; + let response: Response; + let result: IResponse; + + try { + response = await fetch( + `https://arc72-idx.nftnavigator.xyz/nft-indexer/v1/tokens?owner=${address}` + ); + } catch (error) { + logger?.error(`${_functionName}:`, error); + + throw new NetworkConnectionError(error.message); + } + + if (!response.ok) { + errorMessage = await response.text(); + + logger?.error(`${_functionName}: ${errorMessage}`); + + throw new UnknownError(errorMessage); + } + + result = await response.json(); + + if (!result.tokens) { + errorMessage = 'result does not contain the "tokens" property'; + + logger?.error(`${_functionName}: ${errorMessage}`); + + throw new MalformedDataError(errorMessage); + } + + return result.tokens.map(({ contractId, metadata, tokenId }) => ({ + amount: '0', + id: contractId.toString(), + metadata: JSON.parse(metadata), + tokenId: tokenId.toString(), + type: AssetTypeEnum.ARC0072, + })); +} diff --git a/src/extension/utils/fetchVoiARC0072TokensByOwner/index.ts b/src/extension/utils/fetchVoiARC0072TokensByOwner/index.ts new file mode 100644 index 00000000..922174b0 --- /dev/null +++ b/src/extension/utils/fetchVoiARC0072TokensByOwner/index.ts @@ -0,0 +1,2 @@ +export { default } from './fetchVoiARC0072TokensByOwner'; +export * from './types'; diff --git a/src/extension/utils/fetchVoiARC0072TokensByOwner/types/IResponse.ts b/src/extension/utils/fetchVoiARC0072TokensByOwner/types/IResponse.ts new file mode 100644 index 00000000..34487c45 --- /dev/null +++ b/src/extension/utils/fetchVoiARC0072TokensByOwner/types/IResponse.ts @@ -0,0 +1,10 @@ +// types +import type ITokenResponse from './ITokenResponse'; + +interface IResponse { + currentRound: number; + ['next-token']: number; + tokens: ITokenResponse[]; +} + +export default IResponse; diff --git a/src/extension/utils/fetchVoiARC0072TokensByOwner/types/ITokenResponse.ts b/src/extension/utils/fetchVoiARC0072TokensByOwner/types/ITokenResponse.ts new file mode 100644 index 00000000..d0649e27 --- /dev/null +++ b/src/extension/utils/fetchVoiARC0072TokensByOwner/types/ITokenResponse.ts @@ -0,0 +1,11 @@ +interface ITokenResponse { + approved: string; + contractId: number; + metadataURI: string; + metadata: string; + ['mint-round']: number; + owner: string; + tokenId: number; +} + +export default ITokenResponse; diff --git a/src/extension/utils/fetchVoiARC0072TokensByOwner/types/index.ts b/src/extension/utils/fetchVoiARC0072TokensByOwner/types/index.ts new file mode 100644 index 00000000..3db16472 --- /dev/null +++ b/src/extension/utils/fetchVoiARC0072TokensByOwner/types/index.ts @@ -0,0 +1,2 @@ +export type { default as IResponse } from './IResponse'; +export type { default as ITokenResponse } from './ITokenResponse'; diff --git a/src/extension/utils/isZeroAddress/index.ts b/src/extension/utils/isZeroAddress/index.ts new file mode 100644 index 00000000..2ac3d49d --- /dev/null +++ b/src/extension/utils/isZeroAddress/index.ts @@ -0,0 +1 @@ +export { default } from './isZeroAddress'; diff --git a/src/extension/utils/isZeroAddress/isZeroAddress.ts b/src/extension/utils/isZeroAddress/isZeroAddress.ts new file mode 100644 index 00000000..770d6af0 --- /dev/null +++ b/src/extension/utils/isZeroAddress/isZeroAddress.ts @@ -0,0 +1,11 @@ +/** + * Convenience function that just checks if an address is a zero address, i.e. the address is: + * "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ" + * @param {string} address - the address to check. + * @returns {boolean} true if the address is a zero address, false otherwise. + */ +export default function isZeroAddress(address: string): boolean { + return ( + address === 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ' + ); +} diff --git a/src/extension/utils/lookupAlgorandAccountTransactionsWithDelay/index.ts b/src/extension/utils/lookupAlgorandAccountTransactionsWithDelay/index.ts new file mode 100644 index 00000000..148b5f5d --- /dev/null +++ b/src/extension/utils/lookupAlgorandAccountTransactionsWithDelay/index.ts @@ -0,0 +1 @@ +export { default } from './lookupAlgorandAccountTransactionsWithDelay'; diff --git a/src/extension/features/accounts/utils/lookupAlgorandAccountTransactionsWithDelay.ts b/src/extension/utils/lookupAlgorandAccountTransactionsWithDelay/lookupAlgorandAccountTransactionsWithDelay.ts similarity index 86% rename from src/extension/features/accounts/utils/lookupAlgorandAccountTransactionsWithDelay.ts rename to src/extension/utils/lookupAlgorandAccountTransactionsWithDelay/lookupAlgorandAccountTransactionsWithDelay.ts index f3235fa3..928fd753 100644 --- a/src/extension/features/accounts/utils/lookupAlgorandAccountTransactionsWithDelay.ts +++ b/src/extension/utils/lookupAlgorandAccountTransactionsWithDelay/lookupAlgorandAccountTransactionsWithDelay.ts @@ -2,16 +2,8 @@ import { Indexer, IntDecoding } from 'algosdk'; import LookupAccountTransactions from 'algosdk/dist/types/client/v2/indexer/lookupAccountTransactions'; // types -import { IAlgorandAccountTransaction } from '@extension/types'; - -interface IOptions { - address: string; - afterTime: number | null; - client: Indexer; - delay: number; - limit: number; - next: string | null; -} +import type { IAlgorandAccountTransaction } from '@extension/types'; +import type { IOptions } from './types'; /** * Looks up the account transactions for a given address with a delay. diff --git a/src/extension/utils/lookupAlgorandAccountTransactionsWithDelay/types/IOptions.ts b/src/extension/utils/lookupAlgorandAccountTransactionsWithDelay/types/IOptions.ts new file mode 100644 index 00000000..d3ea81c1 --- /dev/null +++ b/src/extension/utils/lookupAlgorandAccountTransactionsWithDelay/types/IOptions.ts @@ -0,0 +1,15 @@ +import { Indexer } from 'algosdk'; + +// types +import type { IBaseOptions } from '@common/types'; + +interface IOptions extends IBaseOptions { + address: string; + afterTime: number | null; + client: Indexer; + delay: number; + limit: number; + next: string | null; +} + +export default IOptions; diff --git a/src/extension/utils/lookupAlgorandAccountTransactionsWithDelay/types/index.ts b/src/extension/utils/lookupAlgorandAccountTransactionsWithDelay/types/index.ts new file mode 100644 index 00000000..68e70016 --- /dev/null +++ b/src/extension/utils/lookupAlgorandAccountTransactionsWithDelay/types/index.ts @@ -0,0 +1 @@ +export type { default as IOptions } from './IOptions'; diff --git a/src/extension/utils/mapARC0072AssetFromARC0072AssetInformation/index.ts b/src/extension/utils/mapARC0072AssetFromARC0072AssetInformation/index.ts new file mode 100644 index 00000000..e6446e48 --- /dev/null +++ b/src/extension/utils/mapARC0072AssetFromARC0072AssetInformation/index.ts @@ -0,0 +1 @@ +export { default } from './mapARC0072AssetFromARC0072AssetInformation'; diff --git a/src/extension/utils/mapARC0072AssetFromARC0072AssetInformation/mapARC0072AssetFromARC0072AssetInformation.ts b/src/extension/utils/mapARC0072AssetFromARC0072AssetInformation/mapARC0072AssetFromARC0072AssetInformation.ts new file mode 100644 index 00000000..36f5fa8e --- /dev/null +++ b/src/extension/utils/mapARC0072AssetFromARC0072AssetInformation/mapARC0072AssetFromARC0072AssetInformation.ts @@ -0,0 +1,22 @@ +import { BigNumber } from 'bignumber.js'; + +// types +import { AssetTypeEnum } from '@extension/enums'; + +// types +import type { IARC0072Asset, IARC0072AssetInformation } from '@extension/types'; + +export default function mapARC0072AssetFromARC0072AssetInformation( + appId: string, + assetInformation: IARC0072AssetInformation, + verified: boolean +): IARC0072Asset { + return { + id: appId, + totalSupply: new BigNumber( + String(assetInformation.totalSupply as bigint) + ).toString(), + type: AssetTypeEnum.ARC0072, + verified, + }; +} diff --git a/src/extension/utils/refreshTransactions/index.ts b/src/extension/utils/refreshTransactions/index.ts new file mode 100644 index 00000000..9cdf3b8c --- /dev/null +++ b/src/extension/utils/refreshTransactions/index.ts @@ -0,0 +1 @@ +export { default } from './refreshTransactions'; diff --git a/src/extension/features/accounts/utils/refreshTransactions.ts b/src/extension/utils/refreshTransactions/refreshTransactions.ts similarity index 80% rename from src/extension/features/accounts/utils/refreshTransactions.ts rename to src/extension/utils/refreshTransactions/refreshTransactions.ts index a41f9baf..760ffff7 100644 --- a/src/extension/features/accounts/utils/refreshTransactions.ts +++ b/src/extension/utils/refreshTransactions/refreshTransactions.ts @@ -1,5 +1,3 @@ -import { Indexer } from 'algosdk'; - // constants import { DEFAULT_TRANSACTION_INDEXER_LIMIT, @@ -7,25 +5,15 @@ import { } from '@extension/constants'; // types -import { IBaseOptions } from '@common/types'; -import { +import type { IAlgorandAccountTransaction, - INetwork, ITransactions, } from '@extension/types'; +import type { IOptions } from './types'; // utils -import mapAlgorandTransactionToTransaction from '@extension/utils/mapAlgorandTransactionToTransaction'; -import lookupAlgorandAccountTransactionsWithDelay from './lookupAlgorandAccountTransactionsWithDelay'; - -interface IOptions extends IBaseOptions { - address: string; - afterTime: number; - client: Indexer; - delay?: number; - next: string | null; - network: INetwork; -} +import lookupAlgorandAccountTransactionsWithDelay from '../lookupAlgorandAccountTransactionsWithDelay'; +import mapAlgorandTransactionToTransaction from '../mapAlgorandTransactionToTransaction'; /** * Fetches all latest transactions from a given time. This function runs recursively until the 'next-token' is diff --git a/src/extension/utils/refreshTransactions/types/IOptions.ts b/src/extension/utils/refreshTransactions/types/IOptions.ts new file mode 100644 index 00000000..57ba5fd0 --- /dev/null +++ b/src/extension/utils/refreshTransactions/types/IOptions.ts @@ -0,0 +1,16 @@ +import { Indexer } from 'algosdk'; + +// types +import type { IBaseOptions } from '@common/types'; +import type { INetwork } from '@extension/types'; + +interface IOptions extends IBaseOptions { + address: string; + afterTime: number; + client: Indexer; + delay?: number; + next: string | null; + network: INetwork; +} + +export default IOptions; diff --git a/src/extension/utils/refreshTransactions/types/index.ts b/src/extension/utils/refreshTransactions/types/index.ts new file mode 100644 index 00000000..68e70016 --- /dev/null +++ b/src/extension/utils/refreshTransactions/types/index.ts @@ -0,0 +1 @@ +export type { default as IOptions } from './IOptions'; diff --git a/src/extension/utils/updateARC0072AssetInformationById/index.ts b/src/extension/utils/updateARC0072AssetInformationById/index.ts new file mode 100644 index 00000000..bb985d8c --- /dev/null +++ b/src/extension/utils/updateARC0072AssetInformationById/index.ts @@ -0,0 +1 @@ +export { default } from './updateARC0072AssetInformationById'; diff --git a/src/extension/utils/updateARC0072AssetInformationById/updateARC0072AssetInformationById.ts b/src/extension/utils/updateARC0072AssetInformationById/updateARC0072AssetInformationById.ts new file mode 100644 index 00000000..19139104 --- /dev/null +++ b/src/extension/utils/updateARC0072AssetInformationById/updateARC0072AssetInformationById.ts @@ -0,0 +1,49 @@ +// types +import type { + IARC0072Asset, + IARC0072AssetInformation, + IUpdateAssetInformationByIdOptions, +} from '@extension/types'; + +// utils +import fetchARC0072AssetInformationWithDelay from '../fetchARC0072AssetInformationWithDelay'; +import mapARC0072AssetFromARC0072AssetInformation from '../mapARC0072AssetFromARC0072AssetInformation'; + +/** + * Gets the ARC-0072 asset information. + * @param {string} id - the app ID of the ARC-0072 asset to fetch. + * @param {IOptions} options - options needed to fetch the ARC200 asset information. + * @returns {Promise} the ARC-0072 asset information, or null if there was an error. + */ +export default async function updateARC0072AssetInformationById( + id: string, + { delay = 0, logger, network }: IUpdateAssetInformationByIdOptions +): Promise { + const _functionName: string = 'updateARC0072AssetInformationById'; + let assetInformation: IARC0072AssetInformation | null; + + try { + assetInformation = await fetchARC0072AssetInformationWithDelay({ + delay, + id, + logger, + network, + }); + + if (!assetInformation) { + return null; + } + + return mapARC0072AssetFromARC0072AssetInformation( + id, + assetInformation, + false + ); + } catch (error) { + logger?.error( + `${_functionName}: failed to get arc-0072 asset information for arc-0072 asset "${id}" on ${network.genesisId}: ${error.message}` + ); + + return null; + } +} diff --git a/src/extension/utils/updateARC0200AssetInformationById/updateARC0200AssetInformationById.ts b/src/extension/utils/updateARC0200AssetInformationById/updateARC0200AssetInformationById.ts index b66bb4a4..2feb788c 100644 --- a/src/extension/utils/updateARC0200AssetInformationById/updateARC0200AssetInformationById.ts +++ b/src/extension/utils/updateARC0200AssetInformationById/updateARC0200AssetInformationById.ts @@ -1,20 +1,14 @@ // types -import type { IBaseOptions } from '@common/types'; import type { IARC0200AssetInformation, IARC0200Asset, - INetwork, + IUpdateAssetInformationByIdOptions, } from '@extension/types'; // utils import fetchARC0200AssetInformationWithDelay from '../fetchARC0200AssetInformationWithDelay'; import mapARC0200AssetFromARC0200AssetInformation from '../mapARC0200AssetFromARC0200AssetInformation'; -interface IOptions extends IBaseOptions { - delay?: number; - network: INetwork; -} - /** * Gets the ARC-0200 asset information. * @param {string} id - the app ID of the ARC200 asset to fetch. @@ -23,7 +17,7 @@ interface IOptions extends IBaseOptions { */ export default async function updateARC0200AssetInformationById( id: string, - { delay = 0, logger, network }: IOptions + { delay = 0, logger, network }: IUpdateAssetInformationByIdOptions ): Promise { const _functionName: string = 'updateARC0200AssetInformationById'; let assetInformation: IARC0200AssetInformation | null; @@ -47,10 +41,9 @@ export default async function updateARC0200AssetInformationById( false ); } catch (error) { - logger && - logger.error( - `${_functionName}: failed to get arc-0200 asset information for arc-0200 asset "${id}" on ${network.genesisId}: ${error.message}` - ); + logger?.error( + `${_functionName}: failed to get arc-0200 asset information for arc-0200 asset "${id}" on ${network.genesisId}: ${error.message}` + ); return null; } diff --git a/tsconfig.json b/tsconfig.json index 418f387c..9a858107 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,8 +32,11 @@ "@extension/errors": ["src/extension/errors"], "@extension/features/accounts": ["src/extension/features/accounts"], "@extension/features/add-asset": ["src/extension/features/add-asset"], - "@extension/features/arc200-assets": [ - "src/extension/features/arc200-assets" + "@extension/features/arc0072-assets": [ + "src/extension/features/arc0072-assets" + ], + "@extension/features/arc0200-assets": [ + "src/extension/features/arc0200-assets" ], "@extension/features/events": ["src/extension/features/events"], "@extension/features/messages": ["src/extension/features/messages"], diff --git a/webpack/utils/createCommonConfig.ts b/webpack/utils/createCommonConfig.ts index 4f5045e6..0480176a 100644 --- a/webpack/utils/createCommonConfig.ts +++ b/webpack/utils/createCommonConfig.ts @@ -41,10 +41,15 @@ export default function createCommonConfig(): Configuration { 'features', 'add-asset' ), - ['@extension/features/arc200-assets']: resolve( + ['@extension/features/arc0072-assets']: resolve( extensionPath, 'features', - 'arc200-assets' + 'arc0072-assets' + ), + ['@extension/features/arc0200-assets']: resolve( + extensionPath, + 'features', + 'arc0200-assets' ), ['@extension/features/events']: resolve( extensionPath,