diff --git a/packages/extension-base/src/services/chain-service/utils/index.ts b/packages/extension-base/src/services/chain-service/utils/index.ts index d42d0cb7b35..ecf633c5420 100644 --- a/packages/extension-base/src/services/chain-service/utils/index.ts +++ b/packages/extension-base/src/services/chain-service/utils/index.ts @@ -339,6 +339,11 @@ export function _getChainNativeTokenBasicInfo (chainInfo: _ChainInfo): BasicToke symbol: chainInfo.cardanoInfo.symbol, decimals: chainInfo.cardanoInfo.decimals }; + } else if (chainInfo.bitcoinInfo) { + return { + symbol: chainInfo.bitcoinInfo.symbol, + decimals: chainInfo.bitcoinInfo.decimals + }; } return defaultTokenInfo; diff --git a/packages/extension-base/src/types/balance/index.ts b/packages/extension-base/src/types/balance/index.ts index 161cb1bed89..d5c204584f7 100644 --- a/packages/extension-base/src/types/balance/index.ts +++ b/packages/extension-base/src/types/balance/index.ts @@ -37,11 +37,6 @@ export interface BalanceItem { // substrate fields metadata?: _BalanceMetadata; } - -export interface BalanceItemWithAddressType extends BalanceItem { - addressTypeLabel?: string -} - /** Balance info of all tokens on an address */ export type BalanceInfo = Record; // Key is tokenSlug /** Balance info of all addresses */ diff --git a/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx b/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx index 783e1562415..7219bf991c4 100644 --- a/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Account/AccountDetail/index.tsx @@ -3,7 +3,7 @@ import { NotificationType } from '@subwallet/extension-base/background/KoniTypes'; import { AccountActions, AccountProxy, AccountProxyType } from '@subwallet/extension-base/types'; -import { AccountProxyTypeTag, CloseIcon, Layout, PageWrapper } from '@subwallet/extension-koni-ui/components'; +import { AccountChainTypeLogos, AccountProxyTypeTag, CloseIcon, Layout, PageWrapper } from '@subwallet/extension-koni-ui/components'; import { FilterTabItemType, FilterTabs } from '@subwallet/extension-koni-ui/components/FilterTabs'; import { WalletModalContext } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; import { useDefaultNavigate, useGetAccountProxyById, useNotification } from '@subwallet/extension-koni-ui/hooks'; @@ -14,7 +14,7 @@ import { FormCallbacks, FormFieldData } from '@subwallet/extension-koni-ui/types import { convertFieldToObject } from '@subwallet/extension-koni-ui/utils/form/form'; import { Button, Form, Icon, Input } from '@subwallet/react-ui'; import CN from 'classnames'; -import { CircleNotch, Export, FloppyDiskBack, GitMerge, Trash } from 'phosphor-react'; +import { Export, GitMerge, Trash } from 'phosphor-react'; import { RuleObject } from 'rc-field-form/lib/interface'; import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -95,7 +95,6 @@ const Component: React.FC = ({ accountProxy, onBack, requestView const [deleting, setDeleting] = useState(false); // @ts-ignore const [deriving, setDeriving] = useState(false); - const [saving, setSaving] = useState(false); const filterTabItems = useMemo(() => { const result = [ @@ -211,7 +210,6 @@ const Component: React.FC = ({ accountProxy, onBack, requestView if (changeMap[FormFieldName.NAME]) { clearTimeout(saveTimeOutRef.current); - setSaving(true); const isValidForm = form.getFieldsError().every((field) => !field.errors.length); @@ -219,8 +217,6 @@ const Component: React.FC = ({ accountProxy, onBack, requestView saveTimeOutRef.current = setTimeout(() => { form.submit(); }, 1000); - } else { - setSaving(false); } } }, [form]); @@ -230,25 +226,18 @@ const Component: React.FC = ({ accountProxy, onBack, requestView const name = values[FormFieldName.NAME]; if (name === accountProxy.name) { - setSaving(false); - return; } const accountProxyId = accountProxy.id; if (!accountProxyId) { - setSaving(false); - return; } editAccount(accountProxyId, name.trim()) .catch((error: Error) => { form.setFields([{ name: FormFieldName.NAME, errors: [error.message] }]); - }) - .finally(() => { - setSaving(false); }); }, [accountProxy.id, accountProxy.name, form]); @@ -427,10 +416,9 @@ const Component: React.FC = ({ accountProxy, onBack, requestView onBlur={form.submit} placeholder={t('Account name')} suffix={( - )} /> @@ -526,6 +514,12 @@ const AccountDetail = styled(Wrapper)(({ theme: { token } }: Props) => { gap: token.sizeSM }, + '.__account-item-chain-type-logos': { + minHeight: 20, + marginRight: 12, + marginLeft: 12 + }, + '.account-detail-form, .derivation-info-form': { paddingTop: token.padding, paddingLeft: token.padding, diff --git a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx index 3645f09b221..7489d3b6764 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Tokens/DetailModal.tsx @@ -3,14 +3,13 @@ import { APIItemState } from '@subwallet/extension-base/background/KoniTypes'; import { _isChainBitcoinCompatible } from '@subwallet/extension-base/services/chain-service/utils'; -import { BalanceItemWithAddressType } from '@subwallet/extension-base/types'; import { AccountTokenBalanceItem, EmptyList, RadioGroup } from '@subwallet/extension-koni-ui/components'; import { useSelector } from '@subwallet/extension-koni-ui/hooks'; import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; import { RootState } from '@subwallet/extension-koni-ui/stores'; -import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { BalanceItemWithAddressType, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { TokenBalanceItemType } from '@subwallet/extension-koni-ui/types/balance'; -import { getBitcoinLabelByKeypair, isAccountAll } from '@subwallet/extension-koni-ui/utils'; +import { getBitcoinAccountDetails, getBitcoinKeypairAttributes, isAccountAll } from '@subwallet/extension-koni-ui/utils'; import { getKeypairTypeByAddress, isBitcoinAddress } from '@subwallet/keyring'; import { Form, Icon, ModalContext, Number, SwModal } from '@subwallet/react-ui'; import BigN from 'bignumber.js'; @@ -161,19 +160,39 @@ function Component ({ className = '', currentTokenInfo, id, onCancel, tokenBalan if (isBitcoinAddress(item.address)) { const keyPairType = getKeypairTypeByAddress(item.address); - resultItem.addressTypeLabel = getBitcoinLabelByKeypair(keyPairType); + const attributes = getBitcoinKeypairAttributes(keyPairType); + + resultItem.addressTypeLabel = attributes.label; + resultItem.schema = attributes.schema; } result.push(resultItem); } // Sort by total balance in descending order - return result.sort((a, b) => { - const aTotal = new BigN(a.free).plus(BigN(a.locked)); - const bTotal = new BigN(b.free).plus(BigN(b.locked)); + return result + .sort((a, b) => { + const _isABitcoin = isBitcoinAddress(a.address); + const _isBBitcoin = isBitcoinAddress(b.address); - return bTotal.minus(aTotal).toNumber(); - }); + if (_isABitcoin && _isBBitcoin) { + const aKeyPairType = getKeypairTypeByAddress(a.address); + const bKeyPairType = getKeypairTypeByAddress(b.address); + + const aDetails = getBitcoinAccountDetails(aKeyPairType); + const bDetails = getBitcoinAccountDetails(bKeyPairType); + + return aDetails.order - bDetails.order; + } + + return 0; + }) + .sort((a, b) => { + const aTotal = new BigN(a.free).plus(BigN(a.locked)); + const bTotal = new BigN(b.free).plus(BigN(b.locked)); + + return bTotal.minus(aTotal).toNumber(); + }); }, [accounts, balanceMap, currentAccountProxy, currentTokenInfo?.slug, isAllAccount, isBitcoinChain]); const symbol = currentTokenInfo?.symbol || ''; diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx index 8c88f1de6f3..c515a93bbe9 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainAddressItem.tsx @@ -16,10 +16,12 @@ type Props = ThemeProps & { onClickQrButton?: VoidFunction; onClickInfoButton?: VoidFunction; isShowInfoButton?: boolean; + infoButtonTooltip?: string; } function Component (props: Props): React.ReactElement { - const { className, isShowInfoButton, + const { className, infoButtonTooltip, + isShowInfoButton, item, onClick, onClickCopyButton, onClickInfoButton, onClickQrButton } = props; const _onClickCopyButton: React.MouseEventHandler = React.useCallback((event) => { @@ -84,7 +86,7 @@ function Component (props: Props): React.ReactElement { } onClick={_onClickInfoButton} size='xs' - tooltip={'This network has two address formats'} + tooltip={infoButtonTooltip} tooltipPlacement={'topLeft'} type='ghost' /> @@ -129,7 +131,7 @@ const AccountChainAddressItem = styled(Component)(({ theme: { token } }: 'white-space': 'nowrap', gap: token.sizeXXS, flex: 1, - alignItems: 'flex-end' + alignItems: 'baseline' }, '.__item-chain-name': { diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainTypeLogos.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainTypeLogos.tsx index c7631481a39..154dc322abe 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AccountChainTypeLogos.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AccountChainTypeLogos.tsx @@ -57,7 +57,7 @@ const AccountChainTypeLogos = styled(Component)(({ theme: { token } }: Pr }, '.__chain-type-logo + .__chain-type-logo': { - marginLeft: -token.marginXXS + marginLeft: -6 } }; }); diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx index 49b9e350146..d6d88f2a90c 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AccountProxySelectorItem.tsx @@ -161,6 +161,10 @@ function Component (props: Props): React.ReactElement {
{accountProxy.suri || ''}
+ ) : ( @@ -343,13 +347,17 @@ const AccountProxySelectorItem = styled(Component)(({ theme }) => { '.__item-derived-path': { display: 'flex', - gap: token.sizeXS - 2, + gap: 4, alignItems: 'center', '.__derive-account-path': { fontSize: token.fontSizeSM, color: token.colorTextLight4, - lineHeight: token.lineHeightSM + lineHeight: token.lineHeightSM, + maxWidth: 103, + overflow: 'hidden', + 'white-space': 'nowrap', + textOverflow: 'ellipsis' } }, diff --git a/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx b/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx index cbfc9f6125c..5376efa0c14 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/AddressSelectorItem.tsx @@ -2,11 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { toShort } from '@subwallet/extension-koni-ui/utils'; +import { getBitcoinKeypairAttributes, toShort } from '@subwallet/extension-koni-ui/utils'; +import { getKeypairTypeByAddress, isBitcoinAddress } from '@subwallet/keyring'; import { Icon } from '@subwallet/react-ui'; import CN from 'classnames'; import { CheckCircle } from 'phosphor-react'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; import AccountProxyAvatar from './AccountProxyAvatar'; @@ -25,6 +26,16 @@ function Component (props: Props): React.ReactElement { avatarValue, className, isSelected, name, onClick, showUnselectIcon } = props; + const bitcoinAttributes = useMemo(() => { + if (isBitcoinAddress(address)) { + const keyPairType = getKeypairTypeByAddress(address); + + return getBitcoinKeypairAttributes(keyPairType); + } + + return undefined; + }, [address]); + return (
{
- { - !!name && ( -
- {name} -
+ {name + ? ( + <> +
+
+ {name} +
+ {!!bitcoinAttributes && !!bitcoinAttributes.schema + ? ( + <> +
 - 
+
+ {bitcoinAttributes.label} +
+ + ) + : null} +
+
+ {toShort(address, 9, 10)} +
+ ) - } + : ( +
+
+ {toShort(address, 9, 10)} +
+ {!!bitcoinAttributes && !!bitcoinAttributes.schema + ? ( + <> +
+ {bitcoinAttributes.label} +
+ + ) + : null} +
-
- {name ? `(${toShort(address, 4, 5)})` : toShort(address, 9, 10)} -
+ )}
@@ -87,19 +127,33 @@ const AddressSelectorItem = styled(Component)(({ theme: { token } }: Prop minHeight: 52, '.__avatar': { - marginRight: token.marginSM + marginRight: token.marginXS }, '.__item-center-part': { display: 'flex', + flexDirection: 'column', overflowX: 'hidden', 'white-space': 'nowrap', - gap: token.sizeXXS, flex: 1, fontSize: token.fontSize, lineHeight: token.lineHeight }, + '.__item-name-wrapper': { + display: 'flex', + alignItems: 'baseline' + }, + + '.__item-address-wrapper': { + display: 'flex', + gap: 12, + alignItems: 'baseline', + '.__address': { + fontSize: token.fontSize + } + }, + '.__item-right-part': { display: 'flex' }, @@ -119,16 +173,40 @@ const AddressSelectorItem = styled(Component)(({ theme: { token } }: Prop '.__name': { color: token.colorTextLight1, overflow: 'hidden', - textOverflow: 'ellipsis' + textOverflow: 'ellipsis', + fontWeight: token.fontWeightStrong }, '.__address': { - color: token.colorTextLight4 + color: token.colorTextLight4, + fontSize: token.fontSizeSM, + fontWeight: token.bodyFontWeight, + lineHeight: token.lineHeightSM }, '&:hover': { background: token.colorBgInput + }, + + '.__label, .__name-label-divider': { + fontSize: token.fontSizeXS, + lineHeight: token.lineHeightXS, + fontWeight: 700, + '&.-schema-orange-7': { + color: token['orange-7'] + }, + '&.-schema-lime-7': { + color: token['lime-7'] + }, + '&.-schema-cyan-7': { + color: token['cyan-7'] + } + }, + + '.__name-label-divider': { + color: token.colorTextTertiary } + }; }); diff --git a/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx b/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx index ade66889377..fa65021d303 100644 --- a/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx +++ b/packages/extension-koni-ui/src/components/AccountProxy/list/AccountChainAddressList.tsx @@ -1,15 +1,19 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; import { TON_CHAINS } from '@subwallet/extension-base/services/earning-service/constants'; import { AccountProxy } from '@subwallet/extension-base/types'; import { AccountChainAddressItem, GeneralEmptyList } from '@subwallet/extension-koni-ui/components'; import { WalletModalContext } from '@subwallet/extension-koni-ui/contexts/WalletModalContextProvider'; -import { useGetAccountChainAddresses, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain, useNotification, useTranslation } from '@subwallet/extension-koni-ui/hooks'; -import { AccountChainAddress, ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { useGetAccountChainAddresses, useGetBitcoinAccounts, useHandleLedgerGenericAccountWarning, useHandleTonAccountWarning, useIsPolkadotUnifiedChain, useNotification, useSelector, useTranslation } from '@subwallet/extension-koni-ui/hooks'; +import { AccountChainAddress, AccountInfoType, AccountTokenAddress, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { copyToClipboard } from '@subwallet/extension-koni-ui/utils'; +import { isBitcoinAddress } from '@subwallet/keyring'; +import { BitcoinAddressType } from '@subwallet/keyring/types'; +import { getBitcoinAddressInfo } from '@subwallet/keyring/utils/address/validate'; import { SwList } from '@subwallet/react-ui'; -import React, { useCallback, useContext, useEffect } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo } from 'react'; import styled from 'styled-components'; type Props = ThemeProps & { @@ -20,14 +24,92 @@ type Props = ThemeProps & { } }; +interface BitcoinAccountsByNetwork { + mainnet: AccountInfoType[]; + testnet: AccountInfoType[]; +} + function Component ({ accountProxy, className, isInModal, modalProps }: Props) { const { t } = useTranslation(); const items: AccountChainAddress[] = useGetAccountChainAddresses(accountProxy); + const getBitcoinAccounts = useGetBitcoinAccounts(); const notify = useNotification(); const onHandleTonAccountWarning = useHandleTonAccountWarning(); const onHandleLedgerGenericAccountWarning = useHandleLedgerGenericAccountWarning(); - const { addressQrModal, selectAddressFormatModal } = useContext(WalletModalContext); + const { accountTokenAddressModal, addressQrModal, selectAddressFormatModal } = useContext(WalletModalContext); const checkIsPolkadotUnifiedChain = useIsPolkadotUnifiedChain(); + const chainInfoMap = useSelector((state) => state.chainStore.chainInfoMap); + + const bitcoinAccountList: AccountInfoType[] = useMemo(() => { + if (!items) { + return []; + } + + return items + .filter((item) => isBitcoinAddress(item.address)) + .map((item) => ({ + address: item.address, + type: item.accountType + })); + }, [items]); + + const soloBitcoinAccount = useMemo((): BitcoinAccountsByNetwork => { + if (!bitcoinAccountList || bitcoinAccountList.length === 0) { + return { mainnet: [], testnet: [] }; + } + + const mainnet: AccountInfoType[] = []; + const testnet: AccountInfoType[] = []; + + bitcoinAccountList.forEach((account) => { + const bitcoinAddressInfo = getBitcoinAddressInfo(account.address); + + if (bitcoinAddressInfo.network === 'mainnet') { + mainnet.push(account); + } else { + testnet.push(account); + } + }); + + return { mainnet, testnet }; + }, [bitcoinAccountList]); + + const filteredItems = useMemo(() => { + if (!items) { + return []; + } + + return items.filter((item) => { + if (isBitcoinAddress(item.address)) { + const addressInfo = getBitcoinAddressInfo(item.address); + + if (addressInfo.network === 'mainnet' && soloBitcoinAccount.mainnet.length > 1) { + return [BitcoinAddressType.p2wpkh, BitcoinAddressType.p2wsh].includes(addressInfo.type); + } else if (addressInfo.network === 'testnet' && soloBitcoinAccount.testnet.length > 1) { + return [BitcoinAddressType.p2wpkh, BitcoinAddressType.p2wsh].includes(addressInfo.type); + } + + return true; + } + + return true; + }); + }, [items, soloBitcoinAccount.mainnet.length, soloBitcoinAccount.testnet.length]); + + const getBitcoinTokenAddresses = useCallback( + (slug: string, bitcoinAccounts: AccountInfoType[]): AccountTokenAddress[] => { + const chainInfo = chainInfoMap[slug]; + + if (!chainInfo) { + return []; + } + + const nativeTokenSlug = _getChainNativeTokenSlug(chainInfo); + + return getBitcoinAccounts(slug, nativeTokenSlug, chainInfo, bitcoinAccounts); + }, + [chainInfoMap, getBitcoinAccounts] + ); const openSelectAddressFormatModal = useCallback((item: AccountChainAddress) => { selectAddressFormatModal.open({ @@ -45,9 +127,25 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { }); }, [isInModal, modalProps, selectAddressFormatModal]); + const openAccountTokenAddressModal = useCallback((accounts: AccountTokenAddress[], closeCallback?: VoidCallback) => { + const processFunction = () => { + accountTokenAddressModal.open({ + items: accounts, + onBack: accountTokenAddressModal.close, + onCancel: () => { + accountTokenAddressModal.close(); + closeCallback?.(); + } + }); + }; + + processFunction(); + }, [accountTokenAddressModal]); + const onShowQr = useCallback((item: AccountChainAddress) => { return () => { const isPolkadotUnifiedChain = checkIsPolkadotUnifiedChain(item.slug); + const isBitcoinChain = isBitcoinAddress(item.address); const processFunction = () => { addressQrModal.open({ @@ -66,20 +164,34 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { if (isPolkadotUnifiedChain) { openSelectAddressFormatModal(item); - } else { - onHandleTonAccountWarning(item.accountType, () => { - onHandleLedgerGenericAccountWarning({ - accountProxy: accountProxy, - chainSlug: item.slug - }, processFunction); - }); + + return; } + + if (isBitcoinChain) { + // TODO: Currently, only supports Bitcoin native token. + const accountTokenAddressList = getBitcoinTokenAddresses(item.slug, bitcoinAccountList); + + if (accountTokenAddressList.length > 1) { + openAccountTokenAddressModal(accountTokenAddressList); + + return; + } + } + + onHandleTonAccountWarning(item.accountType, () => { + onHandleLedgerGenericAccountWarning({ + accountProxy: accountProxy, + chainSlug: item.slug + }, processFunction); + }); }; - }, [accountProxy, addressQrModal, checkIsPolkadotUnifiedChain, isInModal, modalProps, onHandleLedgerGenericAccountWarning, onHandleTonAccountWarning, openSelectAddressFormatModal]); + }, [accountProxy, addressQrModal, bitcoinAccountList, checkIsPolkadotUnifiedChain, getBitcoinTokenAddresses, isInModal, modalProps, onHandleLedgerGenericAccountWarning, onHandleTonAccountWarning, openAccountTokenAddressModal, openSelectAddressFormatModal]); const onCopyAddress = useCallback((item: AccountChainAddress) => { return () => { const isPolkadotUnifiedChain = checkIsPolkadotUnifiedChain(item.slug); + const isBitcoinChain = isBitcoinAddress(item.address); const processFunction = () => { copyToClipboard(item.address || ''); @@ -90,31 +202,75 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { if (isPolkadotUnifiedChain) { openSelectAddressFormatModal(item); - } else { - onHandleTonAccountWarning(item.accountType, () => { - onHandleLedgerGenericAccountWarning({ - accountProxy: accountProxy, - chainSlug: item.slug - }, processFunction); - }); + + return; + } + + if (isBitcoinChain) { + // TODO: Currently, only supports Bitcoin native token. + + const accountTokenAddressList = getBitcoinTokenAddresses(item.slug, bitcoinAccountList); + + if (accountTokenAddressList.length > 1) { + openAccountTokenAddressModal(accountTokenAddressList); + + return; + } } + + onHandleTonAccountWarning(item.accountType, () => { + onHandleLedgerGenericAccountWarning({ + accountProxy: accountProxy, + chainSlug: item.slug + }, processFunction); + }); }; - }, [accountProxy, checkIsPolkadotUnifiedChain, notify, onHandleLedgerGenericAccountWarning, onHandleTonAccountWarning, openSelectAddressFormatModal, t]); + }, [accountProxy, bitcoinAccountList, checkIsPolkadotUnifiedChain, getBitcoinTokenAddresses, notify, onHandleLedgerGenericAccountWarning, onHandleTonAccountWarning, openAccountTokenAddressModal, openSelectAddressFormatModal, t]); const onClickInfoButton = useCallback((item: AccountChainAddress) => { return () => { + const isBitcoinChain = isBitcoinAddress(item.address); + + if (isBitcoinChain) { + // TODO: Currently, only supports Bitcoin native token. + const accountTokenAddressList = getBitcoinTokenAddresses(item.slug, bitcoinAccountList); + + if (accountTokenAddressList.length > 1) { + openAccountTokenAddressModal(accountTokenAddressList); + + return; + } + } + openSelectAddressFormatModal(item); }; - }, [openSelectAddressFormatModal]); + }, [bitcoinAccountList, getBitcoinTokenAddresses, openAccountTokenAddressModal, openSelectAddressFormatModal]); const renderItem = useCallback( (item: AccountChainAddress) => { const isPolkadotUnifiedChain = checkIsPolkadotUnifiedChain(item.slug); + const isBitcoinChain = isBitcoinAddress(item.address); + let tooltip = ''; + + if (isPolkadotUnifiedChain) { + tooltip = 'This network has two address formats'; + } else if (isBitcoinChain) { + tooltip = 'This network has three address types'; + } + + let isShowBitcoinInfoButton = false; + + if (isBitcoinChain) { + const accountTokenAddressList = getBitcoinTokenAddresses(item.slug, bitcoinAccountList); + + isShowBitcoinInfoButton = accountTokenAddressList.length > 1; + } return ( ); }, - [checkIsPolkadotUnifiedChain, onClickInfoButton, onCopyAddress, onShowQr] + [bitcoinAccountList, checkIsPolkadotUnifiedChain, getBitcoinTokenAddresses, onClickInfoButton, onCopyAddress, onShowQr] ); const emptyList = useCallback(() => { @@ -145,7 +301,7 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { return prev; } - const targetAddress = items.find((i) => i.slug === prev.chainSlug)?.address; + const targetAddress = filteredItems.find((i) => i.slug === prev.chainSlug)?.address; if (!targetAddress) { return prev; @@ -157,13 +313,13 @@ function Component ({ accountProxy, className, isInModal, modalProps }: Props) { }; }); } - }, [addressQrModal, items]); + }, [addressQrModal, filteredItems]); return ( (({ theme: { token } }: Props) => { color: token.blue }, + '.__value.-schema-cyan-7': { + color: token['cyan-7'] + }, + + '.__value.-schema-lime-7': { + color: token['lime-7'] + }, + + '.__value.-schema-orange-7': { + color: token['orange-7'] + }, + '.__value.-schema-even-odd': { color: token.colorTextLight2, diff --git a/packages/extension-koni-ui/src/components/MetaInfo/parts/types.ts b/packages/extension-koni-ui/src/components/MetaInfo/parts/types.ts index 720742f8ac1..42f16416eff 100644 --- a/packages/extension-koni-ui/src/components/MetaInfo/parts/types.ts +++ b/packages/extension-koni-ui/src/components/MetaInfo/parts/types.ts @@ -6,5 +6,5 @@ import React from 'react'; export interface InfoItemBase extends ThemeProps { label?: React.ReactNode, - valueColorSchema?: 'default' | 'light' | 'gray' | 'success' | 'gold' | 'danger' | 'warning' | 'magenta' | 'green' | 'blue' + valueColorSchema?: 'default' | 'light' | 'gray' | 'success' | 'gold' | 'danger' | 'warning' | 'magenta' | 'green' | 'blue' | 'orange-7' | 'lime-7' | 'cyan-7' } diff --git a/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx b/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx index cd2a15aaad2..02f0d996b78 100644 --- a/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/AddressBook/AddressBookModal.tsx @@ -6,7 +6,8 @@ import { _reformatAddressWithChain, getAccountChainTypeForAddress } from '@subwa import { AddressSelectorItem, BackIcon } from '@subwallet/extension-koni-ui/components'; import { useChainInfo, useFilterModal, useReformatAddress, useSelector } from '@subwallet/extension-koni-ui/hooks'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { isAccountAll, isChainInfoAccordantAccountChainType } from '@subwallet/extension-koni-ui/utils'; +import { getBitcoinAccountDetails, isAccountAll, isChainInfoAccordantAccountChainType } from '@subwallet/extension-koni-ui/utils'; +import { getKeypairTypeByAddress, isBitcoinAddress } from '@subwallet/keyring'; import { Badge, Icon, ModalContext, SwList, SwModal } from '@subwallet/react-ui'; import { SwListSectionRef } from '@subwallet/react-ui/es/sw-list'; import CN from 'classnames'; @@ -137,6 +138,20 @@ const Component: React.FC = (props: Props) => { return result .sort((a: AnalyzeAddress, b: AnalyzeAddress) => { + const _isABitcoin = isBitcoinAddress(a.address); + const _isBBitcoin = isBitcoinAddress(b.address); + const _isSameProxyId = a.proxyId === b.proxyId; + + if (_isABitcoin && _isBBitcoin && _isSameProxyId) { + const aKeyPairType = getKeypairTypeByAddress(a.address); + const bKeyPairType = getKeypairTypeByAddress(b.address); + + const aDetails = getBitcoinAccountDetails(aKeyPairType); + const bDetails = getBitcoinAccountDetails(bKeyPairType); + + return aDetails.order - bDetails.order; + } + return ((a?.displayName || '').toLowerCase() > (b?.displayName || '').toLowerCase()) ? 1 : -1; }) .sort((a, b) => getGroupPriority(b) - getGroupPriority(a)); diff --git a/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx b/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx index 66d49273425..294994cb791 100644 --- a/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Global/AccountTokenAddressModal.tsx @@ -25,16 +25,19 @@ type Props = ThemeProps & AccountTokenAddressModalProps & { }; const modalId = ADDRESS_GROUP_MODAL; +const LEARN_MORE_DOCS_URL = 'https://docs.subwallet.app/main/extension-user-guide/receive-and-transfer-assets/receive-tokens-and-nfts#select-your-preferred-bitcoin-address'; const Component: React.FC = ({ className, items, onBack, onCancel }: Props) => { const { t } = useTranslation(); const notify = useNotification(); const { addressQrModal } = useContext(WalletModalContext); + // Note: This component only supports Bitcoin addresses. Please review it if you want to use it for other use cases. const onShowQr = useCallback((item: AccountTokenAddress) => { return () => { const processFunction = () => { addressQrModal.open({ + accountTokenAddresses: items, address: item.accountInfo.address, chainSlug: item.chainSlug, onBack: addressQrModal.close, @@ -47,7 +50,7 @@ const Component: React.FC = ({ className, items, onBack, onCancel }: Prop processFunction(); }; - }, [addressQrModal, onCancel]); + }, [addressQrModal, items, onCancel]); const onCopyAddress = useCallback((item: AccountTokenAddress) => { return () => { @@ -104,16 +107,26 @@ const Component: React.FC = ({ className, items, onBack, onCancel }: Prop onClick: onCancel } : undefined} - title={t('Select address')} + title={t('Select address type')} > - } - className={'address-group-list'} - list={items} - renderItem={renderItem} - renderWhenEmpty={renderEmpty} - /> -
+
+
+ {t('SubWallet supports three Bitcoin address types for receiving and transferring assets. Make sure you choose the correct address type to avoid risks of fund loss. ')} + Learn more +
+ } + className={'address-group-list'} + list={items} + renderItem={renderItem} + renderWhenEmpty={renderEmpty} + /> +
); }; @@ -129,6 +142,15 @@ const AccountTokenAddressModal = styled(Component)(({ theme: { token } }: marginTop: 8 }, + '.sub-title': { + paddingBottom: token.padding, + fontSize: token.fontSizeSM, + fontWeight: token.bodyFontWeight, + lineHeight: token.lineHeightSM, + textAlign: 'center', + color: token.colorTextTertiary + }, + '.ant-sw-list-search-input': { paddingBottom: token.paddingXS }, diff --git a/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx b/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx index e1e8cd2c2e1..535931b5c56 100644 --- a/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Global/AddressQrModal.tsx @@ -10,17 +10,19 @@ import { ADDRESS_QR_MODAL, TON_WALLET_CONTRACT_SELECTOR_MODAL } from '@subwallet import { useDefaultNavigate, useFetchChainInfo, useGetAccountByAddress } from '@subwallet/extension-koni-ui/hooks'; import useNotification from '@subwallet/extension-koni-ui/hooks/common/useNotification'; import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; -import { ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { toShort } from '@subwallet/extension-koni-ui/utils'; +import { AccountTokenAddress, ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { getBitcoinKeypairAttributes, toShort } from '@subwallet/extension-koni-ui/utils'; +import { getKeypairTypeByAddress, isBitcoinAddress } from '@subwallet/keyring'; import { Button, Icon, Logo, ModalContext, SwModal, SwQRCode, Tag } from '@subwallet/react-ui'; import CN from 'classnames'; -import { ArrowSquareOut, CaretLeft, CopySimple, Gear, House } from 'phosphor-react'; -import React, { useCallback, useContext, useMemo } from 'react'; +import { ArrowSquareOut, CaretLeft, CaretRight, CopySimple, Gear, House } from 'phosphor-react'; +import React, { useCallback, useContext, useMemo, useState } from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; import styled from 'styled-components'; export interface AddressQrModalProps { address: string; + accountTokenAddresses?: AccountTokenAddress[]; chainSlug: string; onBack?: VoidFunction; onCancel?: VoidFunction; @@ -34,18 +36,54 @@ type Props = ThemeProps & AddressQrModalProps & { const modalId = ADDRESS_QR_MODAL; const tonWalletContractSelectorModalId = TON_WALLET_CONTRACT_SELECTOR_MODAL; -const Component: React.FC = ({ address, chainSlug, className, isNewFormat, onBack, onCancel }: Props) => { +const Component: React.FC = ({ accountTokenAddresses = [], address: initialAddress, chainSlug, className, isNewFormat, onBack, onCancel }: Props) => { const { t } = useTranslation(); const { activeModal, checkActive, inactiveModal } = useContext(ModalContext); const notify = useNotification(); const chainInfo = useFetchChainInfo(chainSlug); - const accountInfo = useGetAccountByAddress(address); + const accountInfo = useGetAccountByAddress(initialAddress); const isTonWalletContactSelectorModalActive = checkActive(tonWalletContractSelectorModalId); const goHome = useDefaultNavigate().goHome; + const showNavigationButtons = useMemo(() => { + return accountTokenAddresses.length > 1; + }, [accountTokenAddresses]); + + const [currentIndex, setCurrentIndex] = useState(() => { + if (!showNavigationButtons) { + return 0; + } + + const index = accountTokenAddresses?.findIndex((item) => item.accountInfo.address === initialAddress); + + return index !== -1 ? index : 0; + }); + + const currentAddress = showNavigationButtons ? accountTokenAddresses[currentIndex]?.accountInfo.address || initialAddress : initialAddress; + const scanExplorerAddressUrl = useMemo(() => { - return getExplorerLink(chainInfo, address, 'account'); - }, [address, chainInfo]); + return getExplorerLink(chainInfo, currentAddress, 'account'); + }, [currentAddress, chainInfo]); + + const bitcoinAttributes = useMemo(() => { + if (isBitcoinAddress(currentAddress)) { + const keyPairType = getKeypairTypeByAddress(currentAddress); + + return getBitcoinKeypairAttributes(keyPairType); + } + + return undefined; + }, [currentAddress]); + + const handlePrevious = useCallback(() => { + setCurrentIndex((prev) => Math.max(0, prev - 1)); + }, []); + + const handleNext = useCallback(() => { + if (accountTokenAddresses) { + setCurrentIndex((prev) => Math.min(accountTokenAddresses.length - 1, prev + 1)); + } + }, [accountTokenAddresses]); const onGoHome = useCallback(() => { goHome(); @@ -130,17 +168,49 @@ const Component: React.FC = ({ address, chainSlug, className, isNewFormat > <>
+ {showNavigationButtons && ( +
+ {!!bitcoinAttributes && !!bitcoinAttributes.label + ? ( +
+
{bitcoinAttributes.label}
+
+ ) + : null}
= ({ address, chainSlug, className, isNewFormat />
- {toShort(address || '', 7, 7)} + {toShort(currentAddress || '', 7, 7)}
{isNewFormat !== undefined &&
@@ -163,7 +233,7 @@ const Component: React.FC = ({ address, chainSlug, className, isNewFormat
} - +