From 32e66c87f9d17f12f2eead36777e0f9aa0a316bc Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Thu, 5 Sep 2024 12:31:45 -0400 Subject: [PATCH] Initial support for managing token family key pattern (#194) * Initial support for managing token family key pattern * remove debug log entry * lazy load crypto images * fix z-index issues * default image and fallback + token styling * Handle ETH multichain * handle ETH and other legacy keys as multichain if necessary * lint * mock unit tests for token family response * Support Base chain address record verification * handle legacy / mapped multichain display * sort versions in CryptoIcon UX --- packages/config/src/env/default.ts | 10 +- .../ui-components/src/actions/pav3Actions.ts | 18 +- .../src/components/CopyToClipboard.tsx | 1 - .../CryptoAddresses/CryptoAddress.tsx | 84 +- .../src/components/DropDownMenu.tsx | 1 + .../src/components/Image/CryptoIcon.tsx | 802 +++--------------- .../src/components/Manage/DomainProfile.tsx | 4 +- .../src/components/Manage/Tabs/Crypto.tsx | 90 +- .../Manage/common/AddCurrencyModal.tsx | 35 +- .../Manage/common/CurrencyInput.tsx | 38 +- .../Manage/common/MultiChainInput.tsx | 30 +- .../Manage/common/currencyRecords.ts | 276 +++--- .../Manage/common/verification/provider.tsx | 1 + .../src/components/TokenGallery/NftCard.tsx | 2 +- .../src/components/TokenGallery/NftModal.tsx | 2 +- .../Wallet/DomainWalletTransactions.tsx | 1 + .../src/components/Wallet/Token.tsx | 1 + .../src/hooks/useResolverKeys.ts | 23 +- .../src/lib/domain/getImageUrl.ts | 4 + .../ui-components/src/lib/domain/records.ts | 56 +- .../ui-components/src/lib/types/blockchain.ts | 13 +- packages/ui-components/src/lib/types/pav3.ts | 44 +- .../ui-components/src/lib/types/records.ts | 5 +- .../src/lib/types/resolverKeys.ts | 54 ++ server/pages/[domain]/index.page.test.tsx | 15 + server/pages/[domain]/index.page.tsx | 4 +- 26 files changed, 658 insertions(+), 956 deletions(-) diff --git a/packages/config/src/env/default.ts b/packages/config/src/env/default.ts index 5bfce004..67d19599 100644 --- a/packages/config/src/env/default.ts +++ b/packages/config/src/env/default.ts @@ -60,7 +60,15 @@ export default function getDefaultConfig(): Config { UNSTOPPABLE_CONTRACT_ADDRESS: '0xa9a6a3626993d487d2dbda3173cf58ca1a9d9e9f', UNSTOPPABLE_METADATA_ENDPOINT: 'https://api.ud-staging.com/metadata', IPFS_BASE_URL: 'https://ipfs.io', - VERIFICATION_SUPPORTED: ['SOL', 'ETH', 'MATIC', 'FTM', 'AVAX', 'BTC'], + VERIFICATION_SUPPORTED: [ + 'SOL', + 'ETH', + 'BASE', + 'MATIC', + 'FTM', + 'AVAX', + 'BTC', + ], LOGIN_WITH_UNSTOPPABLE: { CLIENT_ID: '115148ec-364d-4e19-b7d8-2807e8f1b525', REDIRECT_URI: diff --git a/packages/ui-components/src/actions/pav3Actions.ts b/packages/ui-components/src/actions/pav3Actions.ts index 49262b8c..150b697b 100644 --- a/packages/ui-components/src/actions/pav3Actions.ts +++ b/packages/ui-components/src/actions/pav3Actions.ts @@ -1,7 +1,7 @@ import config from '@unstoppabledomains/config'; import {fetchApi} from '../lib'; -import type {RecordUpdateResponse} from '../lib/types/pav3'; +import type {MappedResolverKey, RecordUpdateResponse} from '../lib/types/pav3'; // confirmRecordUpdate submits a transaction signature to allow a domain record // update to be processed on the blockchain @@ -38,6 +38,22 @@ export const confirmRecordUpdate = async ( }); }; +export const getAllResolverKeys = async (): Promise => { + const keys = await fetchApi(`/resolve/keys`, { + method: 'GET', + mode: 'cors', + host: config.PROFILE.HOST_URL, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }); + if (keys && Array.isArray(keys)) { + return keys; + } + return []; +}; + // getRegistrationMessage retrieve a message that must be signed before on-chain // record operations can be initiated for the given domain. Returns undefined if // a message is not required. diff --git a/packages/ui-components/src/components/CopyToClipboard.tsx b/packages/ui-components/src/components/CopyToClipboard.tsx index 385d6ea6..1f97831c 100644 --- a/packages/ui-components/src/components/CopyToClipboard.tsx +++ b/packages/ui-components/src/components/CopyToClipboard.tsx @@ -43,7 +43,6 @@ const CopyToClipboard = ({ display="inline" aria-label={tooltip || t('common.copy')} onClick={handleCopyClick} - zIndex={10000} > {children} diff --git a/packages/ui-components/src/components/CryptoAddresses/CryptoAddress.tsx b/packages/ui-components/src/components/CryptoAddresses/CryptoAddress.tsx index e6c7c4c4..f5f5040a 100644 --- a/packages/ui-components/src/components/CryptoAddresses/CryptoAddress.tsx +++ b/packages/ui-components/src/components/CryptoAddresses/CryptoAddress.tsx @@ -115,6 +115,8 @@ const useStyles = makeStyles()((theme: Theme) => ({ }, menuActionIcon: { marginRight: theme.spacing(1), + height: '14px', + width: '14px', }, })); @@ -216,6 +218,7 @@ const CryptoAddress: React.FC = ({ case 'ETH': case 'FTM': case 'AVAX': + case 'BASE': return isEthAddress(addr) ? `https://www.oklink.com/${symbol.toLowerCase()}/address/${addr}?channelId=uns001` : ''; @@ -254,10 +257,7 @@ const CryptoAddress: React.FC = ({ placement="bottom" arrow > - + {chain && {chain}} @@ -331,46 +331,52 @@ const CryptoAddress: React.FC = ({ anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose} + transformOrigin={{horizontal: 'right', vertical: 'top'}} + anchorOrigin={{horizontal: 'right', vertical: 'bottom'}} classes={{list: classes.menuList}} > - {Object.keys(filteredVersions).map(version => ( - - {getBlockScanUrl(currency, filteredVersions[version]) ? ( - - handleSingleAddressClick(filteredVersions[version]) - } - > - - {version} - - ) : ( - - - a.localeCompare(b)) + .map(version => ( + + {getBlockScanUrl(currency, filteredVersions[version]) ? ( + + handleSingleAddressClick(filteredVersions[version]) + } + > + - {version} + {version} - - )} - - ))} + ) : ( + + + + {version} + + + )} + + ))} ) : isSingleAddressWithLink ? ( diff --git a/packages/ui-components/src/components/DropDownMenu.tsx b/packages/ui-components/src/components/DropDownMenu.tsx index 9d66dc6a..00ceed71 100644 --- a/packages/ui-components/src/components/DropDownMenu.tsx +++ b/packages/ui-components/src/components/DropDownMenu.tsx @@ -37,6 +37,7 @@ const useStyles = makeStyles<{marginTop?: number}>()( position: 'absolute', top: `${marginTop || '44'}px`, right: '0px', + zIndex: 100, }, container: { display: 'flex', diff --git a/packages/ui-components/src/components/Image/CryptoIcon.tsx b/packages/ui-components/src/components/Image/CryptoIcon.tsx index ea9b2448..6d1a2a61 100644 --- a/packages/ui-components/src/components/Image/CryptoIcon.tsx +++ b/packages/ui-components/src/components/Image/CryptoIcon.tsx @@ -1,714 +1,102 @@ -import type {SvgIconProps} from '@mui/material/SvgIcon'; -import dynamic from 'next/dynamic'; -import React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import type {Theme} from '@mui/material/styles'; +import React, {useEffect, useState} from 'react'; +import {LazyLoadImage} from 'react-lazy-load-image-component'; -import type {CurrenciesType} from '../../lib/types/blockchain'; -import {Currencies} from '../../lib/types/blockchain'; +import {makeStyles} from '@unstoppabledomains/ui-kit/styles'; -const Base = dynamic(() => import('./BaseIcon')); -const Bitcoin = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Bitcoin'), -); -const Ethereum = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Ethereum'), -); -const Litecoin = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Litecoin'), -); -const Ripple = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Ripple'), -); -const Zilliqa = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Zilliqa'), -); -const EthereumClassic = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/EthereumClassic'), -); -const Chainlink = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Chainlink'), -); -const USDCoin = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/USDCoin'), -); -const BasicAttentionToken = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/BasicAttentionToken'), -); -const Augur = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Augur'), -); -const ZRX = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/ZRX'), -); -const Dai = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Dai'), -); -const BitcoinCash = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/BitcoinCash'), -); -const Monero = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Monero'), -); -const Dash = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Dash'), -); -const Neo = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Neo'), -); -const Doge = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Doge'), -); -const Zcash = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Zcash'), -); -const Cardano = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Cardano'), -); -const EOS = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/EOS'), -); -const StellarLumens = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/StellarLumens'), -); -const BinanceCoin = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/BinanceCoin'), -); -const BitcoinGold = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/BitcoinGold'), -); -const Nano = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Nano'), -); -const WavesTech = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/WavesTech'), -); -const Komodo = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Komodo'), -); -const Aeternity = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Aeternity'), -); -const Wanchain = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Wanchain'), -); -const Ubiq = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Ubiq'), -); -const Tezos = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Tezos'), -); -const Iota = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Iota'), -); -const VeChain = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/VeChain'), -); -const Qtum = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Qtum'), -); -const ICX = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/ICX'), -); -const DigiByte = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/DigiByte'), -); -const Zcoin = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Zcoin'), -); -const Burst = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Burst'), -); -const Decred = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Decred'), -); -const NEM = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/NEM'), -); -const Lisk = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Lisk'), -); -const Cosmos = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Cosmos'), -); -const Ontology = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Ontology'), -); -const SmartCash = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/SmartCash'), -); -const TokenPay = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/TokenPay'), -); -const GroestIcoin = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/GroestIcoin'), -); -const Gas = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Gas'), -); -const TRON = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/TRON'), -); -const VeThorToken = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/VeThorToken'), -); -const BitcoinDiamond = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/BitcoinDiamond'), -); -const BitTorrent = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/BitTorrent'), -); -const Kin = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Kin'), -); -const Ravencoin = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Ravencoin'), -); -const Ark = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Ark'), -); -const Verge = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Verge'), -); -const Algorand = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Algorand'), -); -const Neblio = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Neblio'), -); -const Bounty = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Bounty'), -); -const Harmony = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Harmony'), -); -const HuobiToken = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/HuobiToken'), -); -const EnjinCoin = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/EnjinCoin'), -); -const YearnFinance = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/YearnFinance'), -); -const Compound = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Compound'), -); -const Balancer = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Balancer'), -); -const Ampleforth = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Ampleforth'), -); -const Tether = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Tether'), -); -const Lend = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Lend'), -); -const BitcoinSV = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/BitcoinSV'), -); -const XinFin = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/XinFin'), -); -const CRO = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/CRO'), -); -const Fantom = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Fantom'), -); -const Stratis = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Stratis'), -); -const Switcheo = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Switcheo'), -); -const FuseNetwork = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/FuseNetwork'), -); -const Arweave = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Arweave'), -); -const Nimiq = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Nimiq'), -); -const Polygon = dynamic(() => - import('@unstoppabledomains/ui-kit/icons/Polygon').then( - mod => mod.Polygon36x36, - ), -); -const Solana = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Solana'), -); -const CompoundTether = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/CompoundTether'), -); -const Avalanche = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Avalanche'), -); -const Polkadot = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Polkadot'), -); -const BinanceUSD = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/BinanceUSD'), -); -const SHIB = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/SHIB'), -); -const Terra = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Terra'), -); -const PancakeSwap = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/PancakeSwap'), -); -const MANA = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/MANA'), -); -const Elrond = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Elrond'), -); -const SAND = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/SAND'), -); -const Hedera = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Hedera'), -); -const WAXP = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/WAXP'), -); -const ONEINCH = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/ONEINCH'), -); -const BLOCKS = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Blocks'), -); -const THETA = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/THETA'), -); -const Helium = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Helium'), -); -const SafeMoon = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/SafeMoon'), -); -const NEARProtocol = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/NEARProtocol'), -); -const Filecoin = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Filecoin'), -); -const AxieInfinity = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/AxieInfinity'), -); -const Amp = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Amp'), -); -const Celo = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Celo'), -); -const Kusama = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Kusama'), -); -const Casper = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Casper'), -); -const Uniswap = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Uniswap'), -); -const Celsius = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Celsius'), -); -const Ergo = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Ergo'), -); -const Kava = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Kava'), -); -const Loopring = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Loopring'), -); -const Polymath = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Polymath'), -); -const ThetaFuel = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/ThetaFuel'), -); -const Nexo = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Nexo'), -); -const Flow = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Flow'), -); -const InternetComputer = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/InternetComputer'), -); -const TrueUSD = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/TrueUSD'), -); -const Klever = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Klever'), -); -const YieldApp = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/YieldApp'), -); -const OKT = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/OKT'), -); -const Bit2Me = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Bit2Me'), -); -const TheDogeNFT = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/TheDogeNFT'), -); -const Gala = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Gala'), -); -const Mobix = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Mobix'), -); -const Fab = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Fabric'), -); -const Firo = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Firo'), -); -const Fet = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Fet'), -); -const Beam = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Beam'), -); -const RailgunIcon = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/RailgunIcon'), -); -const Sui = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Sui'), -); -const Moon = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/MOON'), -); -const Sweat = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Sweat'), -); -const Deso = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Deso'), -); -const FLR = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/FLR'), -); -const SGB = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/SGB'), -); -const POKT = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/PocketNetwork'), -); -const XLA = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Scala'), -); -const KAI = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/KardiaChain'), -); -const APT = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Aptos'), -); -const GTH = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Gather'), -); -const HI = dynamic(() => import('@unstoppabledomains/ui-kit/icons/crypto/HI')); -const Verse = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/Verse'), -); +import useResolverKeys from '../../hooks/useResolverKeys'; +import {getDefaultCryptoIconUrl} from '../../lib'; +import {getMappedResolverKey} from '../../lib/types/resolverKeys'; -const MContent = dynamic( - () => import('@unstoppabledomains/ui-kit/icons/crypto/MContent'), -); +const useStyles = makeStyles()((theme: Theme) => ({ + innerImage: { + width: '100%', + height: '100%', + }, + fallbackContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: theme.palette.neutralShades[400], + width: '100%', + height: '100%', + }, + fallbackText: { + color: theme.palette.neutralShades[800], + fontWeight: 'bold', + }, +})); type Props = { - currency: CurrenciesType; -} & SvgIconProps; + currency: string; + className?: string; + lazyLoad?: boolean; +}; -export const CryptoIcon = React.forwardRef( - ({currency, ...rest}, ref) => { - switch (currency) { - case Currencies.BASE: - return ; - case Currencies.BTC: - return ; - case Currencies.ETH: - return ; - case Currencies.LTC: - return ; - case Currencies.XRP: - return ; - case Currencies.ZIL: - return ; - case Currencies.ETC: - return ; - case Currencies.LINK: - return ; - case Currencies.USDC: - return ; - case Currencies.BAT: - return ; - case Currencies.REP: - return ; - case Currencies.ZRX: - return ; - case Currencies.DAI: - return ; - case Currencies.BCH: - return ; - case Currencies.XMR: - return ; - case Currencies.DASH: - return ; - case Currencies.NEO: - return ; - case Currencies.DOGE: - return ; - case Currencies.ZEC: - return ; - case Currencies.ADA: - return ; - case Currencies.EOS: - return ; - case Currencies.XLM: - return ; - case Currencies.BNB: - return ; - case Currencies.BTG: - return ; - case Currencies.NANO: - return ; - case Currencies.WAVES: - return ; - case Currencies.KMD: - return ; - case Currencies.AE: - return ; - case Currencies.WAN: - return ; - case Currencies.UBQ: - return ; - case Currencies.XTZ: - return ; - case Currencies.MIOTA: - return ; - case Currencies.VET: - return ; - case Currencies.QTUM: - return ; - case Currencies.ICX: - return ; - case Currencies.DGB: - return ; - case Currencies.XZC: - return ; - case Currencies.BURST: - return ; - case Currencies.DCR: - return ; - case Currencies.XEM: - return ; - case Currencies.LSK: - return ; - case Currencies.ATOM: - return ; - case Currencies.ONT: - return ; - case Currencies.SMART: - return ; - case Currencies.TPAY: - return ; - case Currencies.GRS: - return ; - case Currencies.GAS: - return ; - case Currencies.TRX: - return ; - case Currencies.VTHO: - return ; - case Currencies.BCD: - return ; - case Currencies.BTT: - return ; - case Currencies.KIN: - return ; - case Currencies.RVN: - return ; - case Currencies.ARK: - return ; - case Currencies.XVG: - return ; - case Currencies.ALGO: - return ; - case Currencies.NEBL: - return ; - case Currencies.BNTY: - return ; - case Currencies.ONE: - return ; - case Currencies.HT: - return ; - case Currencies.ENJ: - return ; - case Currencies.YFI: - return ; - case Currencies.COMP: - return ; - case Currencies.BAL: - return ; - case Currencies.AMPL: - return ; - case Currencies.USDT: - return ; - case Currencies.LEND: - return ; - case Currencies.BSV: - return ; - case Currencies.XDC: - return ; - case Currencies.CRO: - return ; - case Currencies.FTM: - return ; - case Currencies.STRAT: - return ; - case Currencies.SWTH: - return ; - case Currencies.FUSE: - return ; - case Currencies.AR: - return ; - case Currencies.NIM: - return ; - case Currencies.MATIC: - return ; - case Currencies.SOL: - return ; - case Currencies.CUSDT: - return ; - case Currencies.AVAX: - return ; - case Currencies.DOT: - return ; - case Currencies.BUSD: - return ; - case Currencies.SHIB: - return ; - case Currencies.LUNA: - return ; - case Currencies.CAKE: - return ; - case Currencies.MANA: - return ; - case Currencies.EGLD: - return ; - case Currencies.SAND: - return ; - case Currencies.HBAR: - return ; - case Currencies.WAXP: - return ; - case Currencies['1INCH']: - return ; - case Currencies.BLOCKS: - return ; - case Currencies.THETA: - return ; - case Currencies.HNT: - return ; - case Currencies.SAFEMOON: - return ; - case Currencies.NEAR: - return ; - case Currencies.FIL: - return ; - case Currencies.AXS: - return ; - case Currencies.AMP: - return ; - case Currencies.CELO: - return ; - case Currencies.KSM: - return ; - case Currencies.CSPR: - return ; - case Currencies.UNI: - return ; - case Currencies.CEL: - return ; - case Currencies.ERG: - return ; - case Currencies.KAVA: - return ; - case Currencies.LRC: - return ; - case Currencies.POLY: - return ; - case Currencies.TFUEL: - return ; - case Currencies.NEXO: - return ; - case Currencies.FLOW: - return ; - case Currencies.ICP: - return ; - case Currencies.TUSD: - return ; - case Currencies.KLV: - return ; - case Currencies.YLD: - return ; - case Currencies.OKT: - return ; - case Currencies.B2M: - return ; - case Currencies.DOG: - return ; - case Currencies.GALA: - return ; - case Currencies.MOBX: - return ; - case Currencies.FAB: - return ; - case Currencies.FIRO: - return ; - case Currencies.FET: - return ; - case Currencies.BEAM: - return ; - case Currencies['0ZK']: - return ; - case Currencies.SUI: - return ; - case Currencies.MOON: - return ; - case Currencies.SWEAT: - return ; - case Currencies.DESO: - return ; - case Currencies.FLR: - return ; - case Currencies.SGB: - return ; - case Currencies.POKT: - return ; - case Currencies.XLA: - return ; - case Currencies.KAI: - return ; - case Currencies.APT: - return ; - case Currencies.GTH: - return ; - case Currencies.HI: - return ; - case Currencies.VERSE: - return ; - case Currencies.MCONTENT: - return ; - default: - return null; +export const CryptoIcon: React.FC = ({ + currency, + className, + lazyLoad, +}) => { + const {classes} = useStyles(); + const {mappedResolverKeys, loading} = useResolverKeys(); + const [iconUrl, setIconUrl] = useState( + getDefaultCryptoIconUrl(currency), + ); + const [iconLoaded, setIconLoaded] = useState(false); + const [title, setTitle] = useState(currency); + const [isBroken, setIsBroken] = useState(false); + + useEffect(() => { + // wait for resolver keys to load + if (loading || !mappedResolverKeys) { + return; } - }, -); + + // find the currency icon URL + const mappedKey = getMappedResolverKey(currency, mappedResolverKeys); + const mappedResolverUrl = + mappedKey?.info?.iconUrl || mappedKey?.info?.logoUrl; + if (mappedResolverUrl && mappedResolverUrl !== iconUrl) { + setIconUrl(mappedResolverUrl); + } + + setTitle(mappedKey?.name || currency); + }, [loading]); + + const handleError = () => { + setIsBroken(true); + }; + + const renderPlaceholder = () => ( + + + + {currency.slice(0, 1)} + + + + ); + + return ( + + {isBroken || !iconUrl || !lazyLoad ? ( + renderPlaceholder() + ) : ( + setIconLoaded(true)} + /> + )} + + ); +}; diff --git a/packages/ui-components/src/components/Manage/DomainProfile.tsx b/packages/ui-components/src/components/Manage/DomainProfile.tsx index 1fc61e49..389b6804 100644 --- a/packages/ui-components/src/components/Manage/DomainProfile.tsx +++ b/packages/ui-components/src/components/Manage/DomainProfile.tsx @@ -563,7 +563,9 @@ export const DomainProfile: React.FC = ({ address={address} onUpdate={onUpdateWrapper} setButtonComponent={setButtonComponent} - filterFn={(k: string) => k.startsWith('crypto.')} + filterFn={(k: string) => + k.startsWith('crypto.') || k.startsWith('token.') + } /> = ({ }) => { const {classes} = useStyles(); const {web3Deps, setWeb3Deps} = useWeb3Context(); - const {unsResolverKeys: resolverKeys, loading: resolverKeysLoading} = - useResolverKeys(); + const { + unsResolverKeys: legacyResolverKeys, + mappedResolverKeys, + loading: resolverKeysLoading, + } = useResolverKeys(); const [saveClicked, setSaveClicked] = useState(false); const [isSignatureSuccess, setIsSignatureSuccess] = useState(false); const [isLoading, setIsLoading] = useState(true); @@ -125,16 +128,17 @@ export const Crypto: React.FC = ({ } setButtonComponent( - + {t('manage.addCurrency')} test + = ({ , ); - }, [isPendingTx, isSaving, isLoading, records]); + }, [isPendingTx, isSaving, isLoading, isModalOpened, records]); const loadRecords = async () => { const data = await getProfileData(domain, [ @@ -224,29 +228,52 @@ export const Crypto: React.FC = ({ }; const handleInputChange = (id: string, value: string) => { - records[id] = value; - setFilteredRecords({ - ...records, + // require mapped resolver keys to be initialized + if (!mappedResolverKeys) { + return; + } + + // find the associated mapped resolver key for the provided ID + const keys = getMappedRecordKeysForUpdate(id, mappedResolverKeys); + + keys.map(k => { + records[k] = value; + setFilteredRecords({ + ...records, + }); }); }; const handleInputDelete = (ids: string[]) => { + // require mapped resolver keys to be initialized + if (!mappedResolverKeys) { + return; + } + + // iterate the deleted IDs ids.map(id => { - deletedRecords.push(id); - handleInputChange(id, ''); + // find the associated mapped resolver key for the provided ID + const keys = getMappedRecordKeysForUpdate(id, mappedResolverKeys); + keys.map(k => { + deletedRecords.push(k); + handleInputChange(k, ''); + }); }); }; const handleAddNewAddress = ({versions}: NewAddressRecord) => { const newValidAddresses = versions.filter(v => !v.deprecated); - const newValidKeys = newValidAddresses.map(v => v.key); + const newValidKeys = newValidAddresses + .map(v => v.key) + // don't add keys that are already included in records + .filter(k => !Object.keys(records).includes(k)); const newAddressRecords = newValidKeys.reduce( (acc, key) => ({...acc, [key]: ''}), // Adding new address records with empty values {}, ); setFilteredRecords({ - ...records, ...newAddressRecords, + ...records, }); }; @@ -326,13 +353,18 @@ export const Crypto: React.FC = ({ }; const renderMultiChainAddresses = () => { - const recordsToRender = getMultichainAddressRecords(records, resolverKeys); + const recordsToRender = getMultichainAddressRecords( + records, + legacyResolverKeys, + mappedResolverKeys, + ); return recordsToRender.map(multiChainRecord => ( = ({ }; const renderSingleChainAddresses = () => { - const recordsToRender = getSingleChainAddressRecords(records, resolverKeys); + const recordsToRender = getSingleChainAddressRecords( + records, + legacyResolverKeys, + mappedResolverKeys, + ); return recordsToRender.map(singleChainAddressRecord => { - const {currency, key, value} = singleChainAddressRecord; + const {currency, key, value, mappedResolverKey} = + singleChainAddressRecord; + if (!mappedResolverKey) { + return; + } return ( = ({ domain={domain} ownerAddress={address} value={value} - recordKey={key} + mappedResolverKey={mappedResolverKey} onDelete={handleInputDelete} onChange={handleInputChange} uiDisabled={!!isPendingTx} @@ -412,14 +452,12 @@ export const Crypto: React.FC = ({ onSignature={handleRecordUpdate} forceWalletConnected={true} /> - {isModalOpened && ( - - )} + )} diff --git a/packages/ui-components/src/components/Manage/common/AddCurrencyModal.tsx b/packages/ui-components/src/components/Manage/common/AddCurrencyModal.tsx index 773a6f96..2875e1bf 100644 --- a/packages/ui-components/src/components/Manage/common/AddCurrencyModal.tsx +++ b/packages/ui-components/src/components/Manage/common/AddCurrencyModal.tsx @@ -12,11 +12,7 @@ import {makeStyles} from '@unstoppabledomains/ui-kit/styles'; import useResolverKeys from '../../../hooks/useResolverKeys'; import type {CurrenciesType, NewAddressRecord} from '../../../lib'; -import { - AllInitialCurrenciesEnum, - CurrencyToName, - useTranslationContext, -} from '../../../lib'; +import {useTranslationContext} from '../../../lib'; import {CryptoIcon} from '../../Image'; import FormError from './FormError'; import {getAllAddressRecords} from './currencyRecords'; @@ -131,13 +127,10 @@ const AddCurrencyModal: React.FC = ({ open, onClose, onAddNewAddress, - isEns, }) => { - const {unsResolverKeys: resolverKeys} = useResolverKeys(); - const validCoins = getAllAddressRecords(resolverKeys).filter( - key => - !Object.keys(AllInitialCurrenciesEnum).includes(key.currency) && - key.versions.every(v => !v.deprecated), + const {mappedResolverKeys} = useResolverKeys(); + const validCoins = getAllAddressRecords(mappedResolverKeys).filter(key => + key.versions.every(v => !v.deprecated), ); const [t] = useTranslationContext(); const [searchQuery, setSearchQuery] = useState(''); @@ -168,34 +161,38 @@ const AddCurrencyModal: React.FC = ({ } setFilteredCoins( - validCoins.filter(({currency}) => { + validCoins.filter(({shortName: currency, name, versions}) => { const searchValue = searchQuery.toLowerCase(); return ( currency.toLowerCase().includes(searchValue) || - CurrencyToName[currency]?.toLowerCase().includes(searchValue) + name?.toLowerCase().includes(searchValue) || + versions.find(v => v.key.toLowerCase().includes(searchValue)) ); }), ); }, [searchQuery]); - const renderCoin = ({currency, versions}: NewAddressRecord) => ( + const renderCoin = ({ + shortName: currency, + name, + versions, + }: NewAddressRecord) => (
v.key).join()} className={classes.currencyItem} - onClick={() => handleSelectCoin({currency, versions})} + onClick={() => handleSelectCoin({shortName: currency, name, versions})} >
-
- {CurrencyToName[currency] || currency} -
+
{name}
{currency} {versions.length > 0 && versions.every(v => v.deprecated) && ( diff --git a/packages/ui-components/src/components/Manage/common/CurrencyInput.tsx b/packages/ui-components/src/components/Manage/common/CurrencyInput.tsx index ee2ef88d..2ad99b88 100644 --- a/packages/ui-components/src/components/Manage/common/CurrencyInput.tsx +++ b/packages/ui-components/src/components/Manage/common/CurrencyInput.tsx @@ -11,25 +11,26 @@ import type { SerializedPublicDomainProfileData, Web3Dependencies, } from '../../../lib'; -import { - AllInitialCurrenciesEnum, - CurrencyToName, - useTranslationContext, -} from '../../../lib'; +import {AllInitialCurrenciesEnum, useTranslationContext} from '../../../lib'; +import type {MappedResolverKey} from '../../../lib/types/pav3'; import type {ResolverKeyName} from '../../../lib/types/resolverKeys'; import {CryptoIcon} from '../../Image'; import ManageInput from './ManageInput'; -import {isTokenDeprecated, isValidRecordKeyValue} from './currencyRecords'; +import { + getParentNetworkSymbol, + isTokenDeprecated, + isValidMappedResolverKeyValue, +} from './currencyRecords'; import VerifyAdornment from './verification/VerifyAdornment'; export interface Props { currency: string; domain: string; ownerAddress: string; - onChange: (key: ResolverKeyName, value: string) => void; - onDelete: (keys: ResolverKeyName[]) => void; + onChange: (key: string, value: string) => void; + onDelete: (keys: string[]) => void; value: string; - recordKey: ResolverKeyName; + mappedResolverKey: MappedResolverKey; uiDisabled: boolean; profileData?: SerializedPublicDomainProfileData; setWeb3Deps: (value: Web3Dependencies | undefined) => void; @@ -227,7 +228,7 @@ const CurrencyInput: React.FC = ({ onChange, onDelete, value, - recordKey, + mappedResolverKey, uiDisabled, profileData, setWeb3Deps, @@ -239,15 +240,20 @@ const CurrencyInput: React.FC = ({ const [address, setAddress] = useState(value); const [isError, setIsError] = useState(false); const initial = Boolean(AllInitialCurrenciesEnum[currency]); - const {unsResolverKeys, ensResolverKeys} = useResolverKeys(); + const {unsResolverKeys} = useResolverKeys(); const resolverKeys = unsResolverKeys; - const isDeprecated = isTokenDeprecated(recordKey, resolverKeys); - const currencyName = CurrencyToName[currency] || currency; + const recordKey = mappedResolverKey.mapping?.to || mappedResolverKey.key; + const isDeprecated = isTokenDeprecated( + recordKey as ResolverKeyName, + resolverKeys, + ); + const currencyName = mappedResolverKey.name || currency; + const currencySymbol = getParentNetworkSymbol(mappedResolverKey) || currency; const placeholder = t('manage.enterYourAddress', {currency: currencyName}); const handleChange = (_key: string, newValue: string) => { const isValid = - !newValue || isValidRecordKeyValue(recordKey, newValue, resolverKeys); + !newValue || isValidMappedResolverKeyValue(newValue, mappedResolverKey); setAddress(newValue); setIsError(!isValid); // Allowing empty values to delete the record @@ -279,7 +285,7 @@ const CurrencyInput: React.FC = ({
@@ -322,7 +328,7 @@ const CurrencyInput: React.FC = ({ domain={domain} ownerAddress={ownerAddress} profileData={profileData} - currency={currency} + currency={currencySymbol} setWeb3Deps={setWeb3Deps} uiDisabled={false} saveClicked={saveClicked} diff --git a/packages/ui-components/src/components/Manage/common/MultiChainInput.tsx b/packages/ui-components/src/components/Manage/common/MultiChainInput.tsx index 4a94d58f..38a812e7 100644 --- a/packages/ui-components/src/components/Manage/common/MultiChainInput.tsx +++ b/packages/ui-components/src/components/Manage/common/MultiChainInput.tsx @@ -13,17 +13,22 @@ import type { SerializedPublicDomainProfileData, Web3Dependencies, } from '../../../lib'; -import {CurrencyToName, useTranslationContext} from '../../../lib'; +import {useTranslationContext} from '../../../lib'; import type {ResolverKeyName} from '../../../lib/types/resolverKeys'; import {MultichainKeyToLocaleKey} from '../../../lib/types/resolverKeys'; import {CryptoIcon} from '../../Image'; import {useStyles} from './CurrencyInput'; import FormError from './FormError'; -import {isTokenDeprecated, isValidRecordKeyValue} from './currencyRecords'; +import { + getParentNetworkSymbol, + isTokenDeprecated, + isValidMappedResolverKeyValue, +} from './currencyRecords'; import VerifyAdornment from './verification/VerifyAdornment'; type Props = { currency: CurrenciesType; + name: string; domain: string; ownerAddress: string; versions: MultiChainAddressVersion[]; @@ -38,6 +43,7 @@ type Props = { const MultiChainInput: React.FC = ({ currency, + name, domain, ownerAddress, onChange, @@ -81,12 +87,10 @@ const MultiChainInput: React.FC = ({
- - {CurrencyToName[currency] || currency} - + {name || currency}
@@ -107,7 +111,11 @@ const MultiChainInput: React.FC = ({
- {versions.map(({key, version, value = ''}) => { + {versions.map(({key, version, value = '', mappedResolverKey}) => { + if (!mappedResolverKey) { + return; + } + const isDeprecated = isTokenDeprecated(key, unsResolverKeys); const handleChange = ({ @@ -116,7 +124,7 @@ const MultiChainInput: React.FC = ({ const newValue = target.value.trim(); const isValid = !newValue || - isValidRecordKeyValue(key, newValue, unsResolverKeys); + isValidMappedResolverKeyValue(newValue, mappedResolverKey); setValues({...values, [key]: newValue}); setErrors({...errors, [key]: !isValid}); @@ -126,11 +134,13 @@ const MultiChainInput: React.FC = ({ } }; + const currencySymbol = + getParentNetworkSymbol(mappedResolverKey) || currency; const placeholder = t('manage.enterYourAddress', { currency: (MultichainKeyToLocaleKey[key] && t(MultichainKeyToLocaleKey[key])) || - CurrencyToName[currency] || + mappedResolverKey.name || currency, }); @@ -158,7 +168,7 @@ const MultiChainInput: React.FC = ({ domain={domain} ownerAddress={ownerAddress} profileData={profileData} - currency={currency} + currency={currencySymbol} setWeb3Deps={setWeb3Deps} uiDisabled={false} saveClicked={saveClicked} diff --git a/packages/ui-components/src/components/Manage/common/currencyRecords.ts b/packages/ui-components/src/components/Manage/common/currencyRecords.ts index 05ffd27a..57d302c6 100644 --- a/packages/ui-components/src/components/Manage/common/currencyRecords.ts +++ b/packages/ui-components/src/components/Manage/common/currencyRecords.ts @@ -12,10 +12,7 @@ import { AllInitialCurrenciesEnum, Registry, } from '../../../lib/types/blockchain'; -import { - ADDRESS_REGEX, - MULTI_CHAIN_ADDRESS_REGEX, -} from '../../../lib/types/records'; +import type {MappedResolverKey} from '../../../lib/types/pav3'; import type { ResolverKeyName, ResolverKeys, @@ -45,34 +42,6 @@ export const EMPTY_DOMAIN_RECORDS: DomainRecords = { whois: {}, }; -/** - * Extracts currency and version from a resolver key of any registry - */ -const extractCurrencyAndVersion = ( - key: ResolverKeyName, - resolverKeys: ResolverKeys, -): {currency: string; version: string} | null => { - // UNS single-chain address: "crypto.BTC.address" - if (key.match(ADDRESS_REGEX)) { - const [_crypto, currency, _address] = key.split('.'); - if (!currency) { - return null; - } - return {currency, version: currency}; - } - - // UNS multi-chain address: "crypto.MATIC.version.ERC20.address" - if (key.match(MULTI_CHAIN_ADDRESS_REGEX)) { - const [_crypto, currency, _version, version] = key.split('.'); - if (!currency || !version) { - return null; - } - return {currency, version}; - } - - return null; -}; - /** * Maps UNS and ENS resolver keys to initial currencies */ @@ -95,29 +64,38 @@ export const InitialCurrencyToResolverKeys: Record< * Works with any registry. */ export const getAllAddressRecords = ( - resolverKeys: ResolverKeys, + mappedResolverKeys?: MappedResolverKey[], ): NewAddressRecord[] => { - const {ResolverKeys, ResolverKey} = resolverKeys; const result: NewAddressRecord[] = []; + mappedResolverKeys + ?.filter(k => k.subType === 'CRYPTO_TOKEN' && k.shortName) + .sort((a, b) => b.shortName.localeCompare(a.shortName)) + .forEach(mappedResolverKey => { + // define resolver key values + const key = mappedResolverKey.mapping?.to || mappedResolverKey.key; + const name = mappedResolverKey.name; + const shortName = mappedResolverKey.shortName; + const deprecated = false; - ResolverKeys.forEach(key => { - const {currency, version} = - extractCurrencyAndVersion(key, resolverKeys) ?? {}; - const deprecated = ResolverKey[key].deprecated; - - if (!currency || !version) { - return; - } + // update result + const record = result.find( + r => (r.name && r.name === name) || r.shortName === shortName, + ); + if (record && Array.isArray(record.versions)) { + record.versions.push({ + key, + deprecated, + }); + } else { + result.push({ + shortName, + name: mappedResolverKey.name, + versions: [{key, deprecated}], + }); + } + }); - const record = result.find(r => r.currency === currency); - if (record && Array.isArray(record.versions)) { - record.versions.push({key, deprecated}); - } else { - result.push({currency, versions: [{key, deprecated}]}); - } - }); - - return result.sort((a, b) => a.currency.localeCompare(b.currency)); + return result.sort((a, b) => a.shortName.localeCompare(b.shortName)); }; /** @@ -138,108 +116,175 @@ export const getInitialAddressRecordKeys = ( }; /** - * Only UNS resolver keys can be multi-chain. We're groupping them by currency + * Only UNS resolver keys can be multi-chain. We're grouping them by currency * to make it easier to display them in the UI. */ export const getMultichainAddressRecords = ( records: DomainRawRecords, - resolverKeys: ResolverKeys, + legacyResolverKeys: ResolverKeys, + mappedResolverKeys?: MappedResolverKey[], ): MultiChainAddressRecord[] => { - const {initialKeys, allKeys} = getInitialAddressRecordKeys(resolverKeys); + const {initialKeys} = getInitialAddressRecordKeys(legacyResolverKeys); const recordKeys = Object.keys(records) as ResolverKeyName[]; const recordKeysWithInitial = uniq([...initialKeys, ...recordKeys]); const result: MultiChainAddressRecord[] = []; + mappedResolverKeys + ?.filter( + k => + // is a token + k.subType === 'CRYPTO_TOKEN' && + // is included in requested keys + (recordKeysWithInitial.includes(k.key as ResolverKeyName) || + recordKeysWithInitial.includes(k.mapping?.to as ResolverKeyName)), + ) + .forEach(mappedResolverKey => { + // define output values + const currency = mappedResolverKey.name || mappedResolverKey.shortName; + const directValue = records[mappedResolverKey.key as ResolverKeyName]; + const mappedRecordValue = mappedResolverKey?.mapping?.to + ? records[mappedResolverKey.mapping.to as ResolverKeyName] + : ''; + const value = directValue || mappedRecordValue || ''; + const key = (mappedResolverKey?.mapping?.to || + mappedResolverKey.key) as ResolverKeyName; - recordKeysWithInitial.forEach(key => { - if (!allKeys.includes(key)) { - return; - } - if (!key.match(MULTI_CHAIN_ADDRESS_REGEX)) { - return; - } + // filter out missing currency value + if (!currency) { + return; + } - // "crypto.MATIC.version.ERC20.address" - const [_crypto, currency, _version, version] = key.split('.'); - if (!currency || !version) { - return; - } - const record = result.find(r => r.currency === currency); - const value = records[key] ?? ''; + // filter out mapped keys without related entries + if ( + !mappedResolverKey.related || + mappedResolverKey.related.length === 0 + ) { + return; + } - if (record) { - record.versions.push({version, value, key}); - } else { - result.push({currency, versions: [{version, value, key}]}); - } - }); + // filter out records without parent + if (!mappedResolverKey.parents) { + return; + } + + // find the parent network version type + const version = getParentNetworkSymbol(mappedResolverKey); + if (!version) { + return; + } + + // update result list with the new version + const record = result.find(r => r.currency === currency); + if (record) { + // filter mapped resolver already in the version list + if ( + record.versions.find( + v => v.mappedResolverKey?.key === mappedResolverKey.key, + ) + ) { + return; + } + + record.versions.push({version, value, key, mappedResolverKey}); + } else { + result.push({ + currency, + name: mappedResolverKey.name, + versions: [{version, value, key, mappedResolverKey}], + }); + } + }); return result; }; +export const getParentNetworkSymbol = ( + mappedResolverKey: MappedResolverKey, +): string | undefined => { + return mappedResolverKey.parents?.find(p => p.subType === 'CRYPTO_NETWORK') + ?.shortName; +}; + export const getSingleChainAddressRecords = ( records: DomainRawRecords, - resolverKeys: ResolverKeys, + legacyResolverKeys: ResolverKeys, + mappedResolverKeys?: MappedResolverKey[], ): SingleChainAddressRecord[] => { - const {ResolverKey} = resolverKeys; - const {initialKeys, allKeys} = getInitialAddressRecordKeys(resolverKeys); + const {initialKeys} = getInitialAddressRecordKeys(legacyResolverKeys); const recordKeys = Object.keys(records) as ResolverKeyName[]; const recordKeysWithInitial = uniq([...initialKeys, ...recordKeys]); const result: SingleChainAddressRecord[] = []; + mappedResolverKeys + ?.filter( + k => + // is a token + k.subType === 'CRYPTO_TOKEN' && + // is included in requested keys + (recordKeysWithInitial.includes(k.key as ResolverKeyName) || + recordKeysWithInitial.includes(k.mapping?.to as ResolverKeyName)), + ) + .forEach(mappedResolverKey => { + // define output values + const currency = mappedResolverKey.shortName; + const directValue = records[mappedResolverKey.key as ResolverKeyName]; + const mappedRecordValue = mappedResolverKey?.mapping?.to + ? records[mappedResolverKey.mapping.to as ResolverKeyName] + : ''; + const value = directValue || mappedRecordValue || ''; + const key = (mappedResolverKey?.mapping?.to || + mappedResolverKey.key) as ResolverKeyName; - recordKeysWithInitial.forEach(key => { - if (!allKeys.includes(key)) { - return; - } - if (!key.match(ADDRESS_REGEX)) { - return; - } + // filter out chains with related entries + if (mappedResolverKey.related && mappedResolverKey.related.length > 0) { + return; + } - const currency = ResolverKey[key].symbol; - if (!currency) { - return; - } + // filter mapped resolver already in list + if ( + result.find(r => r.mappedResolverKey?.key === mappedResolverKey.key) + ) { + return; + } - result.push({currency, value: records[key] ?? '', key}); - }); + // add to result list + result.push({currency, value, key, mappedResolverKey}); + }); + // return single record addresses return result; }; -export const getTotalCurrenciesCount = ( - unsResolverKeys: ResolverKeys, -): number => { - const addressRecords = getAllAddressRecords(unsResolverKeys); - return addressRecords.length; -}; - -export const hasErrors = ( - newRecords: DomainRawRecords, - resolverKeys: ResolverKeys, -): boolean => { - const newRecordKeys = Object.keys(newRecords) as ResolverKeyName[]; - return newRecordKeys.some(key => { - const value = newRecords[key]; - return !isValidRecordKeyValue(key, value, resolverKeys); - }); -}; - export const isTokenDeprecated = ( key: ResolverKeyName, - resolverKeys: ResolverKeys, + legacyResolverKeys: ResolverKeys, ) => { - const {ResolverKey} = resolverKeys; + const {ResolverKey} = legacyResolverKeys; return ResolverKey[key]?.deprecated ?? false; }; +export const isValidMappedResolverKeyValue = ( + value: string = '', + key: MappedResolverKey, +): boolean => { + if (!key.validation?.regexes) { + return true; + } + for (const regex of key.validation.regexes) { + if (new RegExp(regex.pattern).test(value)) { + return true; + } + } + return false; +}; + /** * Validates a record value based on the resolver key. Works with all resolver keys. */ export const isValidRecordKeyValue = ( key: ResolverKeyName, value: string = '', - resolverKeys: ResolverKeys, + legacyResolverKeys: ResolverKeys, ) => { - const {ResolverKey} = resolverKeys; + const {ResolverKey} = legacyResolverKeys; // If the key is not recognized, it's invalid. if (!ResolverKey[key]) { @@ -255,10 +300,3 @@ export const isValidRecordKeyValue = ( const {validationRegex} = ResolverKey[key]; return !validationRegex || new RegExp(validationRegex).test(value); }; - -export const validEthAddress = ( - value: string, - unsResolverKeys: ResolverKeys, -) => { - return isValidRecordKeyValue('crypto.ETH.address', value, unsResolverKeys); -}; diff --git a/packages/ui-components/src/components/Manage/common/verification/provider.tsx b/packages/ui-components/src/components/Manage/common/verification/provider.tsx index 7c230c60..86c1c661 100644 --- a/packages/ui-components/src/components/Manage/common/verification/provider.tsx +++ b/packages/ui-components/src/components/Manage/common/verification/provider.tsx @@ -22,6 +22,7 @@ export const getVerificationProvider = ( > ); case 'ETH': + case 'BASE': case 'MATIC': case 'FTM': case 'AVAX': diff --git a/packages/ui-components/src/components/TokenGallery/NftCard.tsx b/packages/ui-components/src/components/TokenGallery/NftCard.tsx index 1056bcda..7dd02d26 100644 --- a/packages/ui-components/src/components/TokenGallery/NftCard.tsx +++ b/packages/ui-components/src/components/TokenGallery/NftCard.tsx @@ -317,7 +317,7 @@ const NftCard = ({domain, address, nft, compact, placeholder}: Props) => { {nft.symbol && ( )} {nft.name ? ( diff --git a/packages/ui-components/src/components/TokenGallery/NftModal.tsx b/packages/ui-components/src/components/TokenGallery/NftModal.tsx index 052e1680..4c875cef 100644 --- a/packages/ui-components/src/components/TokenGallery/NftModal.tsx +++ b/packages/ui-components/src/components/TokenGallery/NftModal.tsx @@ -363,7 +363,7 @@ const NftModal: React.FC = ({ )} diff --git a/packages/ui-components/src/components/Wallet/DomainWalletTransactions.tsx b/packages/ui-components/src/components/Wallet/DomainWalletTransactions.tsx index 2a5a0612..5f54ca18 100644 --- a/packages/ui-components/src/components/Wallet/DomainWalletTransactions.tsx +++ b/packages/ui-components/src/components/Wallet/DomainWalletTransactions.tsx @@ -98,6 +98,7 @@ const useStyles = makeStyles()((theme: Theme, {palletteShade}) => ({ currencyIcon: { width: 35, height: 35, + backgroundColor: theme.palette.neutralShades[bgNeutralShade], }, noActivity: { marginTop: theme.spacing(2), diff --git a/packages/ui-components/src/components/Wallet/Token.tsx b/packages/ui-components/src/components/Wallet/Token.tsx index c51d85fb..da5a4ffd 100644 --- a/packages/ui-components/src/components/Wallet/Token.tsx +++ b/packages/ui-components/src/components/Wallet/Token.tsx @@ -64,6 +64,7 @@ const useStyles = makeStyles()((theme: Theme, {palletteShade}) => ({ borderRadius: '50%', width: '40px', height: '40px', + backgroundColor: palletteShade[bgNeutralShade], }, tokenIconDefault: { borderRadius: '50%', diff --git a/packages/ui-components/src/hooks/useResolverKeys.ts b/packages/ui-components/src/hooks/useResolverKeys.ts index 98eb1fac..66edbadc 100644 --- a/packages/ui-components/src/hooks/useResolverKeys.ts +++ b/packages/ui-components/src/hooks/useResolverKeys.ts @@ -1,5 +1,8 @@ import {useEffect, useState} from 'react'; +import {useQuery} from 'react-query'; +import {getAllResolverKeys} from '../actions/pav3Actions'; +import type {MappedResolverKey} from '../lib/types/pav3'; import type {ResolverKeys} from '../lib/types/resolverKeys'; import { EMPTY_RESOLVER_KEYS, @@ -10,16 +13,23 @@ import { export type UseResolverKeys = { unsResolverKeys: ResolverKeys; ensResolverKeys: ResolverKeys; + mappedResolverKeys?: MappedResolverKey[]; loading: boolean; }; /** - * Fetches UNS and ENS resolver keys + * Fetches mapped and legacy (UNS, ENS) resolver keys */ const useResolverKeys = (): UseResolverKeys => { const [unsResolverKeys, setUnsResolverKeys] = useState(EMPTY_RESOLVER_KEYS); const [ensResolverKeys, setEnsResolverKeys] = useState(EMPTY_RESOLVER_KEYS); - const [loading, setLoading] = useState(true); + const [legacyResolverKeysLoading, setLegacyResolverKeysLoading] = + useState(true); + const {data: mappedResolverKeys, isLoading: mappedResolverKeysLoading} = + useQuery(['all-resolver-keys'], getAllResolverKeys, { + cacheTime: Infinity, // Cache indefinitely + staleTime: Infinity, // Prevent automatic refetching of the data + }); const loadResolverKeys = async () => { const [newUnsResolverKeys, newEnsResolverKeys] = await Promise.all([ @@ -28,14 +38,19 @@ const useResolverKeys = (): UseResolverKeys => { ]); setUnsResolverKeys(newUnsResolverKeys); setEnsResolverKeys(newEnsResolverKeys); - setLoading(false); + setLegacyResolverKeysLoading(false); }; useEffect(() => { void loadResolverKeys(); }, []); - return {unsResolverKeys, ensResolverKeys, loading}; + return { + unsResolverKeys, + ensResolverKeys, + mappedResolverKeys, + loading: legacyResolverKeysLoading || mappedResolverKeysLoading, + }; }; export default useResolverKeys; diff --git a/packages/ui-components/src/lib/domain/getImageUrl.ts b/packages/ui-components/src/lib/domain/getImageUrl.ts index fc7220d8..e06435b0 100644 --- a/packages/ui-components/src/lib/domain/getImageUrl.ts +++ b/packages/ui-components/src/lib/domain/getImageUrl.ts @@ -13,6 +13,10 @@ const normalizeImagePath = (path: string): string => { } }; +export const getDefaultCryptoIconUrl = (symbol: string) => { + return `https://images.unstoppabledomains.com/images/icons/${symbol}/icon.svg`; +}; + export const getUnoptimizedImageUrl = (path: string): string => { if (isDataUri(path)) { return path; diff --git a/packages/ui-components/src/lib/domain/records.ts b/packages/ui-components/src/lib/domain/records.ts index 05dd0677..b4049475 100644 --- a/packages/ui-components/src/lib/domain/records.ts +++ b/packages/ui-components/src/lib/domain/records.ts @@ -4,6 +4,7 @@ import reduce from 'lodash/reduce'; import {NullAddress} from '@unstoppabledomains/resolution/build/types'; +import {getParentNetworkSymbol} from '../../components/Manage/common/currencyRecords'; import type { MulticoinAddresses, MulticoinVersions, @@ -12,7 +13,10 @@ import type { import { ADDRESS_REGEX, MULTI_CHAIN_ADDRESS_REGEX, + TOKEN_FAMILY_REGEX, } from '../../lib/types/records'; +import type {MappedResolverKey} from '../types/pav3'; +import {getMappedResolverKey} from '../types/resolverKeys'; export const mapMultiCoinAddresses = ( records: Record | null, @@ -36,20 +40,70 @@ export const mapMultiCoinAddresses = ( export const parseRecords = ( records: Record, + mappedResolverKeys?: MappedResolverKey[], ): ParsedRecords => { + // initial set of multichain addresses + const multicoinAddresses = mapMultiCoinAddresses(records); + + // initial set of single chain addresses const addresses = mapKeys( pickBy(records, (v, k) => Boolean(v) && k.match(ADDRESS_REGEX)), (_v, k) => k.split('.')[1], ); + const tokenFamilyEntries = mapKeys( + pickBy(records, (v, k) => Boolean(v) && k.match(TOKEN_FAMILY_REGEX)), + (_v, k) => k, + ); + // Remove null and empty addresses for (const key in addresses) { if (addresses[key] === '0x' || addresses[key] === NullAddress) { delete addresses[key]; } } + + // special handling for mapped resolver keys + if (mappedResolverKeys) { + // Remove duplicate token entries already present in + // address dictionary + for (const token in tokenFamilyEntries) { + const mappedToken = getMappedResolverKey(token, mappedResolverKeys); + if (mappedToken?.mapping?.to && records[mappedToken.mapping.to]) { + continue; + } + addresses[token] = tokenFamilyEntries[token]; + } + + // remove multichain addresses from single chain list and augment the + // multicoinAddresses list + for (const addressKey of Object.keys(addresses)) { + const mappedKey = getMappedResolverKey(addressKey, mappedResolverKeys); + if (mappedKey?.related && mappedKey.related.length > 0) { + // determine parent network + const parentNetwork = addressKey.includes('.') + ? getParentNetworkSymbol(mappedKey) + : addressKey; + if (!parentNetwork) { + continue; + } + + // remove single chain + const addressValue = addresses[addressKey]; + delete addresses[addressKey]; + + // add multichain + const multicoinKey = mappedKey.shortName; + if (!multicoinAddresses[multicoinKey]) { + multicoinAddresses[multicoinKey] = {}; + } + multicoinAddresses[multicoinKey][parentNetwork] = addressValue; + } + } + } + return { addresses, - multicoinAddresses: mapMultiCoinAddresses(records), + multicoinAddresses, }; }; diff --git a/packages/ui-components/src/lib/types/blockchain.ts b/packages/ui-components/src/lib/types/blockchain.ts index f7e54738..51e358bd 100644 --- a/packages/ui-components/src/lib/types/blockchain.ts +++ b/packages/ui-components/src/lib/types/blockchain.ts @@ -1,5 +1,6 @@ import type {DnsRecordType} from '@unstoppabledomains/resolution'; +import type {MappedResolverKey} from './pav3'; import type {ResolverKeyName} from './resolverKeys'; export enum AdditionalCurrenciesEnum { @@ -380,14 +381,19 @@ export enum Mirror { */ export type MultiChainAddressRecord = { currency: string; + name: string; versions: MultiChainAddressVersion[]; }; -export type MultiChainAddressVersion = DomainAddressRecord & {version: string}; +export type MultiChainAddressVersion = DomainAddressRecord & { + version: string; + mappedResolverKey?: MappedResolverKey; +}; export type NewAddressRecord = { - currency: string; - versions: {key: ResolverKeyName; deprecated: boolean}[]; + name: string; + shortName: string; + versions: {key: string; deprecated: boolean}[]; }; export enum Registry { @@ -401,4 +407,5 @@ export enum Registry { */ export type SingleChainAddressRecord = DomainAddressRecord & { currency: string; + mappedResolverKey?: MappedResolverKey; }; diff --git a/packages/ui-components/src/lib/types/pav3.ts b/packages/ui-components/src/lib/types/pav3.ts index bb2ee99f..2bcfa062 100644 --- a/packages/ui-components/src/lib/types/pav3.ts +++ b/packages/ui-components/src/lib/types/pav3.ts @@ -1,4 +1,44 @@ -import type UnsResolverKeysJson from 'uns/resolver-keys.json'; +export interface MappedResolverKey { + type: string; + subType: string; + name: string; + shortName: string; + key: string; + validation?: Validation; + info?: Info; + related?: string[]; + mapping?: Mapping; + parents?: Parent[]; +} + +export interface Parent { + type: string; + subType: string; + name: string; + key: string; + shortName?: string; +} + +interface Validation { + regexes: Regex[]; +} + +interface Regex { + name: string; + pattern: string; +} + +interface Info { + logoUrl: string; + iconUrl: string; + description: string; +} + +interface Mapping { + isPreferred: boolean; + from: string[]; + to: string; +} export type RecordUpdateResponse = { operationId: string; @@ -10,5 +50,3 @@ export type RecordUpdateResponse = { value?: string; }; }; - -export type UnsResolverKey = keyof typeof UnsResolverKeysJson.keys; diff --git a/packages/ui-components/src/lib/types/records.ts b/packages/ui-components/src/lib/types/records.ts index cc92638c..e0112c2b 100644 --- a/packages/ui-components/src/lib/types/records.ts +++ b/packages/ui-components/src/lib/types/records.ts @@ -6,15 +6,16 @@ export const MULTI_CHAIN_ADDRESS_REGEX = new RegExp( `crypto\\.${TICKER_REGEX}\\.version\\.${TICKER_REGEX}\\.address`, ); export type MetadataDomainRecords = Record; - export type MetadataDomainsRecordsResponse = { data: {domain: string; records: MetadataDomainRecords}[]; }; -export type MulticoinAddresses = Record; +export type MulticoinAddresses = Record; export type MulticoinVersions = Record; export type ParsedRecords = { addresses: Addresses; multicoinAddresses: MulticoinAddresses; }; + +export const TOKEN_FAMILY_REGEX = new RegExp('^token\\..*'); diff --git a/packages/ui-components/src/lib/types/resolverKeys.ts b/packages/ui-components/src/lib/types/resolverKeys.ts index f79207cc..56537cbf 100644 --- a/packages/ui-components/src/lib/types/resolverKeys.ts +++ b/packages/ui-components/src/lib/types/resolverKeys.ts @@ -2,6 +2,7 @@ import cloneDeep from 'lodash/cloneDeep'; import type EnsResolverKeysJson from 'uns/ens-resolver-keys.json'; import type UnsResolverKeysJson from 'uns/resolver-keys.json'; +import type {MappedResolverKey} from './pav3'; import {ADDRESS_REGEX, MULTI_CHAIN_ADDRESS_REGEX} from './records'; /** @@ -64,6 +65,59 @@ export type ResolverKeys = { export type UnsResolverKey = keyof typeof UnsResolverKeysJson.keys; +export const getMappedRecordKeysForUpdate = ( + id: string, + keys: MappedResolverKey[], +): string[] => { + // find the associated mapped resolver key for the provided ID + const mappedResolverKey = getMappedResolverKey(id, keys); + if (!mappedResolverKey) { + return [id]; + } + + // build list of keys to update + const expandedKeys = [mappedResolverKey.key]; + if (mappedResolverKey.mapping?.to) { + expandedKeys.push(mappedResolverKey.mapping.to); + } + return expandedKeys; +}; + +export const getMappedResolverKey = ( + id: string, + keys: MappedResolverKey[], +): MappedResolverKey | undefined => { + // search for matching keys + return ( + // find by exact match + keys.find(k => k.key.toLowerCase() === id.toLowerCase()) || + // find by mapping "to" match + keys.find(k => k.mapping?.to.toLowerCase() === id.toLowerCase()) || + // find by mapping "from" match + keys.find(k => + k.mapping?.from?.find(f => f.toLowerCase() === id.toLowerCase()), + ) || + // find by matching network + keys + .filter(k => k.subType === 'CRYPTO_NETWORK') + .find( + k => + // matches the shortname + k.shortName.toLowerCase() === id.toLowerCase() || + (k.name && k.name.toLowerCase() === id.toLowerCase()), + ) || + // find by matching token + keys + .filter(k => k.subType === 'CRYPTO_TOKEN') + .find( + k => + // matches the shortname + k.shortName.toLowerCase() === id.toLowerCase() || + (k.name && k.name.toLowerCase() === id.toLowerCase()), + ) + ); +}; + export const loadEnsResolverKeys = async (): Promise => { if (!cachedEnsResolverKeys) { cachedEnsResolverKeys = await import('uns/ens-resolver-keys.json'); diff --git a/server/pages/[domain]/index.page.test.tsx b/server/pages/[domain]/index.page.test.tsx index 2c724036..a0b1fdce 100644 --- a/server/pages/[domain]/index.page.test.tsx +++ b/server/pages/[domain]/index.page.test.tsx @@ -16,6 +16,7 @@ import * as domainActions from '@unstoppabledomains/ui-components/src/actions/do import * as domainProfileActions from '@unstoppabledomains/ui-components/src/actions/domainProfileActions'; import * as featureFlagActions from '@unstoppabledomains/ui-components/src/actions/featureFlagActions'; import * as identityActions from '@unstoppabledomains/ui-components/src/actions/identityActions'; +import * as pav3Actions from '@unstoppabledomains/ui-components/src/actions/pav3Actions'; import * as push from '@unstoppabledomains/ui-components/src/components/Chat/protocol/push'; import * as chatStorage from '@unstoppabledomains/ui-components/src/components/Chat/storage'; import * as nftImage from '@unstoppabledomains/ui-components/src/components/TokenGallery/NftImage'; @@ -212,6 +213,20 @@ describe('', () => { status: PersonaInquiryStatus.COMPLETED, }); jest.spyOn(Storage.prototype, 'getItem').mockReturnValue(null); + jest.spyOn(pav3Actions, 'getAllResolverKeys').mockResolvedValue([ + { + type: 'CRYPTO', + subType: 'CRYPTO_TOKEN', + name: 'Ether', + shortName: 'ETH', + key: 'token.EVM.ETH.ETH.address', + mapping: { + isPreferred: true, + from: ['crypto.ETH.address'], + to: 'crypto.ETH.address', + }, + }, + ]); }); it('should display a basic domain profile page', async () => { diff --git a/server/pages/[domain]/index.page.tsx b/server/pages/[domain]/index.page.tsx index 3898c9d4..e95b0cec 100644 --- a/server/pages/[domain]/index.page.tsx +++ b/server/pages/[domain]/index.page.tsx @@ -114,6 +114,7 @@ import { getDomainConnections, getOwnerDomains, } from '@unstoppabledomains/ui-components/src/actions/domainProfileActions'; +import useResolverKeys from '@unstoppabledomains/ui-components/src/hooks/useResolverKeys'; import {notifyEvent} from '@unstoppabledomains/ui-components/src/lib/error'; import CopyContentIcon from '@unstoppabledomains/ui-kit/icons/CopyContent'; @@ -143,6 +144,7 @@ const DomainProfile = ({ const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const [imagePath, setImagePath] = useState(); const {enqueueSnackbar} = useSnackbar(); + const {mappedResolverKeys} = useResolverKeys(); const {isChatOpen, setOpenChat} = useUnstoppableMessaging(); const {nfts, nftSymbolVisible, expanded: nftShowAll} = useTokenGallery(); const [isLoaded, setIsLoaded] = useState(false); @@ -227,7 +229,7 @@ const DomainProfile = ({ ); // retrieve on-chain record data - const addressRecords = parseRecords(records || {}); + const addressRecords = parseRecords(records || {}, mappedResolverKeys); const domainSellerEmail = profileData?.profile?.publicDomainSellerEmail; const isForSale = Boolean(domainSellerEmail); const ipfsHash = records['ipfs.html.value'];