diff --git a/packages/extension-base/src/koni/api/nft/config.ts b/packages/extension-base/src/koni/api/nft/config.ts index 2056c62246b..27861fa7e61 100644 --- a/packages/extension-base/src/koni/api/nft/config.ts +++ b/packages/extension-base/src/koni/api/nft/config.ts @@ -141,42 +141,47 @@ if (isFirefox) { }); } -if (!RuntimeInfo?.protocol || - (!RuntimeInfo?.protocol.startsWith('http') || RuntimeInfo?.protocol.startsWith('https'))) { +if (RuntimeInfo.protocol && RuntimeInfo.protocol.startsWith('http')) { + // This is for https + if (RuntimeInfo.protocol.startsWith('https')) { + RANDOM_IPFS_GATEWAY_SETTING.push({ + provider: IPFS_FLEEK, + weight: 4 + }, + { + provider: IPFS_GATEWAY_4EVERLAND, + weight: 2 + }, + { + provider: IPFS_W3S_LINK, + weight: 1 + }, + { + provider: CF_IPFS_GATEWAY, + weight: 4 + }, + { + provider: PINATA_IPFS_GATEWAY, + weight: 1 // Rate limit too low + }, + { + provider: IPFS_IO, + weight: 5 + } + ); + } +} else { + // This is for extension env or other RANDOM_IPFS_GATEWAY_SETTING.push({ - provider: IPFS_FLEEK, - weight: 4 - }, - { - provider: IPFS_GATEWAY_4EVERLAND, - weight: 2 - }, - { - provider: IPFS_W3S_LINK, - weight: 1 - }, - { - provider: CF_IPFS_GATEWAY, - weight: 4 - }, - { - provider: PINATA_IPFS_GATEWAY, - weight: 1 // Rate limit too low - }, - { provider: NFT_STORAGE_GATEWAY, weight: 50 }, - { - provider: GATEWAY_IPFS_IO, - weight: 5 - }, { provider: DWEB_LINK, weight: 5 }, { - provider: IPFS_IO, + provider: GATEWAY_IPFS_IO, weight: 5 } ); diff --git a/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollectionDetail.tsx b/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollectionDetail.tsx index b6f7bc550b0..d2efdefc72b 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollectionDetail.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollectionDetail.tsx @@ -48,18 +48,22 @@ function Component ({ className = '' }: Props): React.ReactElement { const routingParams = { collectionInfo, nftItem } as INftItemDetail; if (nftItem.description) { - const ordinalNftItem = JSON.parse(nftItem.description) as OrdinalRemarkData; - - if ('p' in ordinalNftItem && 'op' in ordinalNftItem && 'tick' in ordinalNftItem && 'amt' in ordinalNftItem) { - return ( - - ); + try { + const ordinalNftItem = JSON.parse(nftItem.description) as OrdinalRemarkData; + + if ('p' in ordinalNftItem && 'op' in ordinalNftItem && 'tick' in ordinalNftItem && 'amt' in ordinalNftItem) { + return ( + + ); + } + } catch (e) { + } } diff --git a/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollections.tsx b/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollections.tsx index 344d67642e3..0bcfec8102a 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollections.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Nfts/NftCollections.tsx @@ -11,18 +11,28 @@ import { INftCollectionDetail } from '@subwallet/extension-koni-ui/Popup/Home/Nf import { ThemeProps } from '@subwallet/extension-koni-ui/types'; import { ActivityIndicator, ButtonProps, Icon, SwList } from '@subwallet/react-ui'; import CN from 'classnames'; -import { ArrowClockwise, Image } from 'phosphor-react'; -import React, { useCallback, useContext } from 'react'; +import { ArrowClockwise, Image, Plus, PlusCircle } from 'phosphor-react'; +import React, { useCallback, useContext, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; type Props = ThemeProps -const reloadIcon = ; +const reloadIcon = ( + +); + +const rightIcon = ( + +); function Component ({ className = '' }: Props): React.ReactElement { useSetCurrentPage('/home/nfts/collections'); @@ -55,6 +65,12 @@ function Component ({ className = '' }: Props): React.ReactElement { }) .catch(console.error); } + }, + { + icon: rightIcon, + onClick: () => { + navigate('/settings/tokens/import-nft', { state: { isExternalRequest: false } }); + } } ]; @@ -110,15 +126,33 @@ function Component ({ className = '' }: Props): React.ReactElement { ); }, [getNftsByCollection, handleOnClickCollection]); + const emptyButtonProps = useMemo((): ButtonProps => { + return { + icon: ( + + ), + children: t('Add collectible'), + shape: 'circle', + size: 'xs', + onClick: () => { + navigate('/settings/tokens/import-nft', { state: { isExternalRequest: false } }); + } + }; + }, [navigate, t]); + const emptyNft = useCallback(() => { return ( ); - }, [t]); + }, [emptyButtonProps, t]); return ( { displayGrid={true} enableSearchInput={true} gridGap={'14px'} + key={nftCollections.length} // fix render issue of flat-list list={nftCollections} minColumnWidth={'160px'} renderItem={renderNftCollection} diff --git a/packages/extension-koni-ui/src/Popup/Home/Nfts/NftImport.tsx b/packages/extension-koni-ui/src/Popup/Home/Nfts/NftImport.tsx index 3d97e878aed..de7c4515597 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Nfts/NftImport.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Nfts/NftImport.tsx @@ -143,7 +143,7 @@ function Component ({ className = '' }: Props): React.ReactElement { .then((result) => { if (result) { showNotification({ - message: t('Imported NFT successfully') + message: t('Imported collectible successfully') }); goBack(); } else { @@ -195,7 +195,7 @@ function Component ({ className = '' }: Props): React.ReactElement { setLoading(false); if (validationResult.isExist) { - reject(t('Existed NFT')); + reject(t('Existed collectible')); } if (validationResult.contractError) { @@ -242,7 +242,7 @@ function Component ({ className = '' }: Props): React.ReactElement { onClick: form.submit, children: t('Import') }} - title={t('Import NFT')} + title={t('Import collectible')} >
{ disabled={!selectedChain} items={nftTypeOptions} label={t('Type')} - placeholder={t('Select NFT type')} - title={t('Select NFT type')} + placeholder={t('Select collectible type')} + title={t('Select collectible type')} /> @@ -304,7 +304,7 @@ function Component ({ className = '' }: Props): React.ReactElement { > ('NFT collection name')} + label={t('Collection name')} /> diff --git a/packages/extension-koni-ui/src/Popup/Home/Nfts/NftItemDetail.tsx b/packages/extension-koni-ui/src/Popup/Home/Nfts/NftItemDetail.tsx index bd9f5557b71..19a666f4b09 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Nfts/NftItemDetail.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Nfts/NftItemDetail.tsx @@ -1,13 +1,14 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { _isPureEvmChain } from '@subwallet/extension-base/services/chain-service/utils'; import { getExplorerLink } from '@subwallet/extension-base/services/transaction-service/utils'; import { OrdinalRemarkData } from '@subwallet/extension-base/types'; import DefaultLogosMap from '@subwallet/extension-koni-ui/assets/logo'; import { AccountProxyAvatar, Layout, PageWrapper } from '@subwallet/extension-koni-ui/components'; -import { CAMERA_CONTROLS_MODEL_VIEWER_PROPS, DEFAULT_MODEL_VIEWER_PROPS, SHOW_3D_MODELS_CHAIN } from '@subwallet/extension-koni-ui/constants'; +import { CAMERA_CONTROLS_MODEL_VIEWER_PROPS, DEFAULT_MODEL_VIEWER_PROPS, DEFAULT_NFT_PARAMS, DEFAULT_TRANSACTION_PARAMS, NFT_TRANSACTION, SHOW_3D_MODELS_CHAIN } from '@subwallet/extension-koni-ui/constants'; import { DataContext } from '@subwallet/extension-koni-ui/contexts/DataContext'; -import { useNavigateOnChangeAccount, useSelector } from '@subwallet/extension-koni-ui/hooks'; +import { useNavigateOnChangeAccount, useNotification, useSelector } from '@subwallet/extension-koni-ui/hooks'; import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; import useDefaultNavigate from '@subwallet/extension-koni-ui/hooks/router/useDefaultNavigate'; import useGetChainInfo from '@subwallet/extension-koni-ui/hooks/screen/common/useFetchChainInfo'; @@ -15,44 +16,85 @@ import useGetAccountInfoByAddress from '@subwallet/extension-koni-ui/hooks/scree import InscriptionImage from '@subwallet/extension-koni-ui/Popup/Home/Nfts/component/InscriptionImage'; import { INftItemDetail } from '@subwallet/extension-koni-ui/Popup/Home/Nfts/utils'; import { RootState } from '@subwallet/extension-koni-ui/stores'; -import { Theme, ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { BackgroundIcon, Field, Icon, Image, Logo, ModalContext, SwModal } from '@subwallet/react-ui'; +import { SendNftParams, Theme, ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { BackgroundIcon, Button, ButtonProps, Field, Icon, Image, Logo, ModalContext, SwModal } from '@subwallet/react-ui'; import { getAlphaColor } from '@subwallet/react-ui/lib/theme/themes/default/colorAlgorithm'; import CN from 'classnames'; -import { CaretLeft, Info } from 'phosphor-react'; +import { CaretLeft, Info, PaperPlaneTilt } from 'phosphor-react'; import React, { useCallback, useContext, useMemo } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import styled, { useTheme } from 'styled-components'; +import { useLocalStorage } from 'usehooks-ts'; type Props = ThemeProps const NFT_DESCRIPTION_MAX_LENGTH = 70; -const modalCloseButton = ; +const modalCloseButton = ( + +); function Component ({ className = '' }: Props): React.ReactElement { const location = useLocation(); const { collectionInfo, nftItem } = location.state as INftItemDetail; const { t } = useTranslation(); + const notify = useNotification(); + + const navigate = useNavigate(); const { goBack } = useDefaultNavigate(); const { token } = useTheme() as Theme; const dataContext = useContext(DataContext); const { activeModal, inactiveModal } = useContext(ModalContext); const currentAccountProxy = useSelector((state: RootState) => state.accountState.currentAccountProxy); + const accounts = useSelector((state: RootState) => state.accountState.accounts); const originChainInfo = useGetChainInfo(nftItem.chain); const ownerAccountInfo = useGetAccountInfoByAddress(nftItem.owner || ''); const accountExternalUrl = getExplorerLink(originChainInfo, nftItem.owner, 'account'); + const [, setStorage] = useLocalStorage(NFT_TRANSACTION, DEFAULT_NFT_PARAMS); useNavigateOnChangeAccount('/home/nfts/collections'); + const onClickSend = useCallback(() => { + if (nftItem && nftItem.owner) { + const owner = accounts.find((a) => a.address === nftItem.owner); + + if (owner?.isReadOnly) { + notify({ + message: t('The NFT owner is a watch-only account, you cannot send the NFT with it'), + type: 'info', + duration: 3 + }); + + return; + } + } + + setStorage({ + ...DEFAULT_TRANSACTION_PARAMS, + collectionId: nftItem.collectionId, + from: nftItem.owner, + itemId: nftItem.id, + to: '', + chain: nftItem.chain + }); + navigate('/transaction/send-nft'); + }, [accounts, navigate, nftItem, notify, setStorage, t]); + + const subHeaderRightButton: ButtonProps[] = [ + { + children: t('Send'), + onClick: onClickSend + } + ]; + const ownerPrefix = useCallback(() => { if (nftItem.owner) { return ( @@ -137,14 +179,16 @@ function Component ({ className = '' }: Props): React.ReactElement { }, [nftItem.externalUrl]); const show3DModel = SHOW_3D_MODELS_CHAIN.includes(nftItem.chain); - const ordinalNftItem = nftItem.description && JSON.parse(nftItem.description) as OrdinalRemarkData; + const isInscription = useMemo(() => { - if (ordinalNftItem && 'p' in ordinalNftItem && 'op' in ordinalNftItem && 'tick' in ordinalNftItem && 'amt' in ordinalNftItem) { - return true; - } + try { + const ordinalNftItem = nftItem.description && JSON.parse(nftItem.description) as OrdinalRemarkData; - return false; - }, [ordinalNftItem]); + return !!(ordinalNftItem && 'p' in ordinalNftItem && 'op' in ordinalNftItem && 'tick' in ordinalNftItem && 'amt' in ordinalNftItem); + } catch (e) { + return false; + } + }, [nftItem.description]); return ( { showSubHeader={true} subHeaderBackground={'transparent'} subHeaderCenter={false} + subHeaderIcons={subHeaderRightButton} subHeaderPaddingVertical={true} title={nftItem.name || nftItem.id} > @@ -171,6 +216,7 @@ function Component ({ className = '' }: Props): React.ReactElement { {!isInscription && ( { }
+ + { + _isPureEvmChain(originChainInfo) && ( + + ) + } (({ theme: { token } }: Props) => '.nft_item_detail__send_text': { fontSize: token.fontSizeLG, - lineHeight: token.lineHeightLG, - color: token.colorTextLight1 + lineHeight: token.lineHeightLG }, '.nft_item_detail__prop_section': { diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendNFT.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendNFT.tsx index c7ba23616cb..ab3322b2628 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendNFT.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendNFT.tsx @@ -2,8 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 import { ExtrinsicType, NftCollection, NftItem } from '@subwallet/extension-base/background/KoniTypes'; +import { AbstractAddressJson } from '@subwallet/extension-base/background/types'; +import { _isPureEvmChain } from '@subwallet/extension-base/services/chain-service/utils'; import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; import { isSameAddress } from '@subwallet/extension-base/utils'; +import DefaultLogosMap from '@subwallet/extension-koni-ui/assets/logo'; import { AddressInput, ChainSelector, HiddenInput, PageWrapper } from '@subwallet/extension-koni-ui/components'; import { DEFAULT_MODEL_VIEWER_PROPS, SHOW_3D_MODELS_CHAIN } from '@subwallet/extension-koni-ui/constants'; import { DataContext } from '@subwallet/extension-koni-ui/contexts/DataContext'; @@ -11,6 +14,7 @@ import { useFocusFormItem, useGetChainPrefixBySlug, useHandleSubmitTransaction, import { evmNftSubmitTransaction, substrateNftSubmitTransaction } from '@subwallet/extension-koni-ui/messaging'; import { FormCallbacks, FormFieldData, FormInstance, FormRule, SendNftParams, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { findAccountByAddress, noop, reformatAddress, simpleCheckForm } from '@subwallet/extension-koni-ui/utils'; +import { getKeypairTypeByAddress } from '@subwallet/keyring'; import { Button, Form, Icon, Image, Typography } from '@subwallet/react-ui'; import CN from 'classnames'; import { ArrowCircleRight } from 'phosphor-react'; @@ -198,6 +202,22 @@ const Component: React.FC = () => { const checkAction = usePreCheckAction(from); + const addressBookFilter = useCallback((addressJson: AbstractAddressJson): boolean => { + const addressType = getKeypairTypeByAddress(addressJson.address); + + const chainInfo = chainInfoMap[chain]; + + if (chain === 'bitcoin') { + return 'bitcoin-84'.includes(addressType) && addressJson.address !== from; + } else if (chain === 'bitcoinTestnet') { + return 'bittest-84'.includes(addressType) && addressJson.address !== from; + } else if (!!chainInfo && _isPureEvmChain(chainInfo)) { + return 'ethereum'.includes(addressType) && addressJson.address !== from; + } + + return false; + }, [chain, chainInfoMap, from]); + useEffect(() => { if (nftItem === DEFAULT_ITEM || collectionInfo === DEFAULT_COLLECTION) { navigate('/home/nfts/collections'); @@ -226,9 +246,10 @@ const Component: React.FC = () => {
@@ -252,6 +273,7 @@ const Component: React.FC = () => { statusHelpAsTooltip={true} > import('@subwall const NftItemDetail = new LazyLoader('NftItemDetail', () => import('@subwallet/extension-koni-ui/Popup/Home/Nfts/NftItemDetail')); const NftCollections = new LazyLoader('NftCollections', () => import('@subwallet/extension-koni-ui/Popup/Home/Nfts/NftCollections')); const NftCollectionDetail = new LazyLoader('NftCollectionDetail', () => import('@subwallet/extension-koni-ui/Popup/Home/Nfts/NftCollectionDetail')); -// const NftImport = new LazyLoader('NftImport', () => import('@subwallet/extension-koni-ui/Popup/Home/Nfts/NftImport')); +const NftImport = new LazyLoader('NftImport', () => import('@subwallet/extension-koni-ui/Popup/Home/Nfts/NftImport')); const History = new LazyLoader('History', () => import('@subwallet/extension-koni-ui/Popup/Home/History')); const Home = new LazyLoader('Home', () => import('@subwallet/extension-koni-ui/Popup/Home')); @@ -229,8 +229,8 @@ export const router = createHashRouter([ children: [ ManageTokens.generateRouterObject('manage'), FungibleTokenImport.generateRouterObject('import-token'), - TokenDetail.generateRouterObject('detail') - // NftImport.generateRouterObject('import-nft') + TokenDetail.generateRouterObject('detail'), + NftImport.generateRouterObject('import-nft') ] } ] diff --git a/patches/@subwallet+react-ui+5.1.2-b74.patch b/patches/@subwallet+react-ui+5.1.2-b74.patch index 87edd32453a..3e550d29ccb 100644 --- a/patches/@subwallet+react-ui+5.1.2-b74.patch +++ b/patches/@subwallet+react-ui+5.1.2-b74.patch @@ -1,3 +1,45 @@ +diff --git a/node_modules/@subwallet/react-ui/es/image/index.d.ts b/node_modules/@subwallet/react-ui/es/image/index.d.ts +index 6daf2a3..c12be26 100644 +--- a/node_modules/@subwallet/react-ui/es/image/index.d.ts ++++ b/node_modules/@subwallet/react-ui/es/image/index.d.ts +@@ -13,6 +13,7 @@ export interface SwImageProps extends ImageProps { + isLoading?: boolean; + activityIndicatorSize?: number | string; + modelViewerProps?: ModelViewerProps; ++ fallbackSrc?: string; + } + export interface CompositionImage

extends React.FC

{ + PreviewGroup: typeof PreviewGroup; +diff --git a/node_modules/@subwallet/react-ui/es/image/index.js b/node_modules/@subwallet/react-ui/es/image/index.js +index 2b8275d..25a0579 100644 +--- a/node_modules/@subwallet/react-ui/es/image/index.js ++++ b/node_modules/@subwallet/react-ui/es/image/index.js +@@ -36,9 +36,10 @@ const Image = _a => { + modelViewerProps, + onLoad, + onError, +- src ++ src, ++ fallbackSrc + } = _a, +- otherProps = __rest(_a, ["prefixCls", "preview", "rootClassName", "shape", "width", "height", "responsive", "isLoading", "activityIndicatorSize", "modelViewerProps", "onLoad", "onError", "src"]); ++ otherProps = __rest(_a, ["prefixCls", "preview", "rootClassName", "shape", "width", "height", "responsive", "isLoading", "activityIndicatorSize", "modelViewerProps", "onLoad", "onError", "src", "fallbackSrc"]); + const { + getPrefixCls, + locale: contextLocale = defaultLocale, +@@ -165,10 +166,10 @@ const Image = _a => { + preview: false, + prefixCls: `${prefixCls}`, + rootClassName: mergedRootClassName, +- fallback: FAULT_TOLERANT, ++ fallback: fallbackSrc || FAULT_TOLERANT, + onLoad: handleOnLoad, + onError: handleImageError, +- src: FAULT_TOLERANT ++ src: fallbackSrc || FAULT_TOLERANT + }, otherProps)); + }; + if (shape === 'squircle') { diff --git a/node_modules/@subwallet/react-ui/es/number/index.js b/node_modules/@subwallet/react-ui/es/number/index.js index 895129b..0081551 100644 --- a/node_modules/@subwallet/react-ui/es/number/index.js