From 48636f804af1cb2491fa4cab97eb8c8db89be33f Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Fri, 24 Dec 2021 10:21:35 +0800 Subject: [PATCH 01/31] chore: bump version to 2.3.2 --- package.json | 2 +- packages/mask/src/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 27100283b6b2..6523aaed3062 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mask-network", "packageManager": "pnpm@6.23.1", - "version": "2.3.1", + "version": "2.3.2", "private": true, "license": "AGPL-3.0-or-later", "scripts": { diff --git a/packages/mask/src/manifest.json b/packages/mask/src/manifest.json index 14451f2b9e7c..72f143cc868f 100644 --- a/packages/mask/src/manifest.json +++ b/packages/mask/src/manifest.json @@ -1,6 +1,6 @@ { "name": "Mask Network", - "version": "2.3.1", + "version": "2.3.2", "manifest_version": 2, "permissions": ["storage", "downloads", "webNavigation", "activeTab"], "optional_permissions": ["", "notifications", "clipboardRead"], From e018418d6ebd0aa3a8bc6af2495183397d2ef137 Mon Sep 17 00:00:00 2001 From: guanbinrui <52657989+guanbinrui@users.noreply.github.com> Date: Fri, 24 Dec 2021 15:09:53 +0800 Subject: [PATCH 02/31] fix: the dao tab (#5281) * fix: DAO tab * fix: rss3 links * fix: the wallet discovery rules * fix: hide nfts tab if non-address found * refactor: the empty indicator * fix: the link path of footprint * fix: the redirect bug --- packages/mask/shared-ui/locales/en-US.json | 12 +- packages/mask/shared-ui/locales/qya-AA.json | 2 +- packages/mask/shared-ui/locales/zh-CN.json | 10 +- .../CollectibleList/index.tsx | 65 +++++------ .../Profile/SNSAdaptor/DonationsPage.tsx | 32 +++--- .../Profile/SNSAdaptor/EnhancedProfile.tsx | 107 ++++++++++++------ .../Profile/SNSAdaptor/FootprintPage.tsx | 33 +++--- .../plugins/Profile/SNSAdaptor/NFTPage.tsx | 12 -- .../plugins/Profile/SNSAdaptor/PageTag.tsx | 43 +++---- .../SNSAdaptor/components/AddressViewer.tsx | 2 +- .../Profile/SNSAdaptor/hooks/useDonations.ts | 11 +- .../Profile/SNSAdaptor/hooks/useFootprints.ts | 11 +- .../src/plugins/Wallet/services/domain.ts | 2 +- 13 files changed, 172 insertions(+), 170 deletions(-) diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index cce5f66cecc2..efcfcef6dda3 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -66,7 +66,7 @@ "unknown": "Unknown", "message": "Message", "dashboard_tab_collectibles": "Collectibles", - "dashboard_no_collectible_found": "No collectible found", + "dashboard_no_collectible_found": "No collectible found.", "days": "Every {{days}} days", "decrypted_postbox_add_recipients": "Append recipients", "decrypted_postbox_decrypting": "Mask decrypting…", @@ -997,11 +997,11 @@ "nft_button_add_collectible": "Add Collectible", "nft_avatar": "NFT Avatar", "plugin_profile_no_wallets": "Connect your wallet here.
This section will be shown to your encrypted friends.
The display content includes digital art, donations, badges
and other public information on chain.", - "plugin_profile_binding_rules_title": "The NFT gallary will show NFTs as below rules:", - "plugin_profile_binding_rule1": "Twitter nickname is ENS.", - "plugin_profile_binding_rule2": "Twitter bio contains ENS(e.g. vitalik.eth) or valid address.", - "plugin_profile_binding_rule3": "Twitter ID + {{suffix}} is ENS.", - "plugin_profile_binding_rule4": "Address used to sign the avatar.", + "plugin_profile_binding_rules_title": "The wallet address discovery rules:", + "plugin_profile_binding_rule1": "The nickname is ENS or RNS.", + "plugin_profile_binding_rule2": "The bio contains ENS, RNS or a valid address.", + "plugin_profile_binding_rule3": "The ID + suffix is ENS or RNS.", + "plugin_profile_binding_rule4": "The address used to sign the PFP NFTs.", "plugin_profile_current_display_of": "Current display of {{type}}: ", "plugin_profile_loading": "Loading...", "plugin_profile_error_no_address": "Failed to find any valid address.", diff --git a/packages/mask/shared-ui/locales/qya-AA.json b/packages/mask/shared-ui/locales/qya-AA.json index a04e5344b4ce..67ac406c9872 100644 --- a/packages/mask/shared-ui/locales/qya-AA.json +++ b/packages/mask/shared-ui/locales/qya-AA.json @@ -999,7 +999,7 @@ "plugin_profile_binding_rules_title": "crwdns10417:0crwdne10417:0", "plugin_profile_binding_rule1": "crwdns10419:0crwdne10419:0", "plugin_profile_binding_rule2": "crwdns10421:0crwdne10421:0", - "plugin_profile_binding_rule3": "crwdns10423:0{{suffix}}crwdne10423:0", + "plugin_profile_binding_rule3": "crwdns10423:0crwdne10423:0", "plugin_profile_binding_rule4": "crwdns10425:0crwdne10425:0", "plugin_profile_current_display_of": "crwdns10427:0{{type}}crwdne10427:0", "plugin_vcent_last_offer_at": "crwdns10295:0crwdne10295:0" diff --git a/packages/mask/shared-ui/locales/zh-CN.json b/packages/mask/shared-ui/locales/zh-CN.json index af53d4eac92f..a4956305aeca 100644 --- a/packages/mask/shared-ui/locales/zh-CN.json +++ b/packages/mask/shared-ui/locales/zh-CN.json @@ -865,11 +865,11 @@ "nft_button_add_collectible": "添加收藏品", "nft_avatar": "NFT 头像", "plugin_profile_no_wallets": "在此连接您的钱包。
此部分将显示给您的加密朋友。
显示内容包括数字艺术、捐赠、徽章
和其他链上的公开信息。", - "plugin_profile_binding_rules_title": "Web3 按如下规则展示NFTs:", - "plugin_profile_binding_rule1": "您的 Twitter 昵称为ENS。", - "plugin_profile_binding_rule2": "您的 Twitter bio 包含 ENS(例如,vitalik.eth) 或有效地址。", - "plugin_profile_binding_rule3": "您的 Twitter ID + {{suffix}} 是ENS。", + "plugin_profile_binding_rules_title": "Web3 按如下规则发现钱包地址:", + "plugin_profile_binding_rule1": "您的昵称为 ENS 或者 RNS。", + "plugin_profile_binding_rule2": "您的个人介绍中包含 ENS、RNS 或有效地址。", + "plugin_profile_binding_rule3": "您的 ID + 词缀是 ENS 或者 RNS。", "plugin_profile_binding_rule4": "用于签署 NFT 头像的地址。", - "plugin_profile_current_display_of": "当前显示 {{type}}:", + "plugin_profile_current_display_of": "当前显示 {{type}}: ", "plugin_vcent_last_offer_at": "最新的 Offer 为" } diff --git a/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/index.tsx b/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/index.tsx index 3c5801889ba4..afbb423d9a09 100644 --- a/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/index.tsx +++ b/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/index.tsx @@ -117,10 +117,11 @@ interface CollectibleListUIProps extends withClasses<'empty' | 'button' | 'text' } function CollectibleListUI(props: CollectibleListUIProps) { const { provider, wallet, collectibles, loading, collectiblesRetry, error, readonly, hasRetry = true } = props - const classes = useStylesExtends(useStyles(), props) const { t } = useI18N() + const classes = useStylesExtends(useStyles(), props) + + useEffect(() => WalletMessages.events.erc721TokensUpdated.on(collectiblesRetry)) - WalletMessages.events.erc721TokensUpdated.on(collectiblesRetry) if (loading) return ( @@ -171,13 +172,13 @@ function CollectibleListUI(props: CollectibleListUIProps) { ) } -export interface CollectibleListAddressProps extends withClasses<'empty' | 'button'> { +export interface CollectibleListProps extends withClasses<'empty' | 'button'> { address: string collection?: string setCount: (count: number) => void } -export function CollectibleListAddress(props: CollectibleListAddressProps) { +export function CollectibleList(props: CollectibleListProps) { const { address, collection, setCount } = props const provider = useValueRef(currentNonFungibleAssetDataProviderSettings) const chainId = ChainId.Mainnet @@ -198,20 +199,15 @@ export function CollectibleListAddress(props: CollectibleListAddressProps) { }, [provider, address]) useEffect(() => { - if (collectibles.length) { - setRendCollectibles([...rendCollectibles, ...collectibles]) - if (hasNextPage) { - const timer = setTimeout(() => { - setPage(page + 1) - }, 1000) - - return () => { - clearTimeout(timer) - } - } + if (!collectibles.length) return + setRendCollectibles([...rendCollectibles, ...collectibles]) + if (!hasNextPage) return + const timer = setTimeout(() => { + setPage(page + 1) + }, 1000) + return () => { + clearTimeout(timer) } - - return () => {} }, [collectibles]) useEffect(() => { @@ -233,8 +229,9 @@ export function CollectibleListAddress(props: CollectibleListAddressProps) { } export function CollectionList({ address }: { address: string }) { - const provider = useValueRef(currentNonFungibleAssetDataProviderSettings) const chainId = ChainId.Mainnet + const provider = useValueRef(currentNonFungibleAssetDataProviderSettings) + const { t } = useI18N() const [page, setPage] = useState(0) const { classes } = useStyles() const [counts, setCounts] = useState([]) @@ -248,22 +245,26 @@ export function CollectionList({ address }: { address: string }) { }, [provider, address]) useEffect(() => { - if (collections.length) { - setRendCollections([...rendCollections, ...collections]) - if (hasNextPage) { - const timer = setTimeout(() => { - setPage(page + 1) - }, 3000) - - return () => { - clearTimeout(timer) - } - } + if (!collections.length) return + setRendCollections([...rendCollections, ...collections]) + if (!hasNextPage) return + const timer = setTimeout(() => { + setPage(page + 1) + }, 3000) + return () => { + clearTimeout(timer) } - - return () => {} }, [collections]) + if (!rendCollections.length) + return ( + + + {t('dashboard_no_collectible_found')} + + + ) + return ( {rendCollections.map((x, i) => ( @@ -281,7 +282,7 @@ export function CollectionList({ address }: { address: string }) { {counts[i] ? `(${counts[i]})` : null} - { diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/DonationsPage.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/DonationsPage.tsx index a0f1c057f706..04bd28ed30ae 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/DonationsPage.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/DonationsPage.tsx @@ -1,16 +1,15 @@ -import { makeStyles } from '@masknet/theme' -import { CircularProgress, Link } from '@mui/material' import urlcat from 'urlcat' -import type { GeneralAssetWithTags } from '../types' +import { makeStyles } from '@masknet/theme' +import { Link } from '@mui/material' +import type { AddressName } from '@masknet/web3-shared-evm' +import type { GeneralAsset, GeneralAssetWithTags } from '../types' import { RSS3_DEFAULT_IMAGE } from '../constants' import { DonationCard } from './components' -import { useDonations } from './hooks' import { useI18N } from '../../../utils' -const getDonationLink = (address: string, donation: GeneralAssetWithTags) => { +const getDonationLink = (label: string, donation: GeneralAssetWithTags) => { const { platform, identity, id, type } = donation - return urlcat('https://rss3.bio/:address/singlegitcoin/:platform/:identity/:id/:type', { - address, + return urlcat(`https://${label}.bio/singlegitcoin/:platform/:identity/:id/:type`, { platform, identity, id, @@ -30,28 +29,23 @@ const useStyles = makeStyles()((theme) => ({ })) export interface DonationPageProps { - address: string + addressName?: AddressName + donations?: GeneralAsset[] } export function DonationPage(props: DonationPageProps) { - const { address } = props - const { classes } = useStyles() - const { donations, loading } = useDonations(address) + const { addressName, donations = [] } = props const { t } = useI18N() + const { classes } = useStyles() + + if (!addressName) return null - if (loading) { - return ( -
- -
- ) - } return (
{donations.map((donation) => ( diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfile.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfile.tsx index 8af942239e14..f3d530424382 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfile.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfile.tsx @@ -3,7 +3,7 @@ import { useUpdateEffect } from 'react-use' import { Box, Typography } from '@mui/material' import { unreachable } from '@dimensiondev/kit' import { makeStyles, useStylesExtends } from '@masknet/theme' -import { AddressNameType, useAddressNames } from '@masknet/web3-shared-evm' +import { AddressName, AddressNameType, useAddressNames } from '@masknet/web3-shared-evm' import { MaskMessages, useI18N } from '../../../utils' import { useLocationChange } from '../../../utils/hooks/useLocationChange' import { WalletsPage } from './WalletsPage' @@ -15,7 +15,33 @@ import { PageTags } from '../types' import { PageTag } from './PageTag' import { AddressViewer } from './components/AddressViewer' import { useCurrentVisitingIdentity } from '../../../components/DataSource/useActivatedUI' -import { useDao } from './hooks' +import { useDao, useDonations, useFootprints } from './hooks' + +function getAddressName(currentTag: PageTags, addressNames: AddressName[]) { + const getAddressByType = (type: AddressNameType) => addressNames?.find((x) => x.type === type) + + const addressLiteral = getAddressByType(AddressNameType.ADDRESS) + const addressENS = getAddressByType(AddressNameType.ENS) + const addressUNS = getAddressByType(AddressNameType.UNS) + const addressGUN = getAddressByType(AddressNameType.GUN) + const addressRSS3 = getAddressByType(AddressNameType.RSS3) + const addressTheGraph = getAddressByType(AddressNameType.THE_GRAPH) + + switch (currentTag) { + case PageTags.WalletTag: + return + case PageTags.NFTTag: + return addressENS || addressUNS || addressRSS3 || addressLiteral || addressGUN || addressTheGraph + case PageTags.DonationTag: + return addressRSS3 || addressENS || addressUNS || addressLiteral || addressGUN || addressTheGraph + case PageTags.FootprintTag: + return addressRSS3 || addressENS || addressUNS || addressLiteral || addressGUN || addressTheGraph + case PageTags.DAOTag: + return + default: + unreachable(currentTag) + } +} const useStyles = makeStyles()((theme) => ({ root: { @@ -41,17 +67,14 @@ export function EnhancedProfilePage(props: EnhancedProfilePageProps) { const { t } = useI18N() const classes = useStylesExtends(useStyles(), props) - //#region identity const identity = useCurrentVisitingIdentity() - const { value: addressNames, loading: loadingAddressNames } = useAddressNames(identity) - //#endregion + const { value: addressNames = [], loading: loadingAddressNames } = useAddressNames(identity) const [hidden, setHidden] = useState(true) - const { value: daoPayload } = useDao(identity.identifier) - const [currentTag, setCurrentTag] = useState(PageTags.NFTTag) + const [currentTag, setCurrentTag] = useState() useLocationChange(() => { - setCurrentTag(PageTags.NFTTag) + setCurrentTag(undefined) MaskMessages.events.profileNFTsTabUpdated.sendToLocal('reset') }) @@ -62,57 +85,57 @@ export function EnhancedProfilePage(props: EnhancedProfilePageProps) { }, [identity]) useUpdateEffect(() => { - setCurrentTag(PageTags.NFTTag) + setCurrentTag(undefined) }, [identity.identifier]) - const addressName = useMemo(() => { - const getAddressByType = (type: AddressNameType) => addressNames?.find((x) => x.type === type) + const addressNFTs = getAddressName(PageTags.NFTTag, addressNames) + const addressRSS3 = getAddressName(PageTags.DonationTag, addressNames) + + const { value: daoPayload, loading: loadingDAO } = useDao(identity.identifier) + const { value: donations, loading: loadingDonations } = useDonations(addressRSS3?.resolvedAddress ?? '') + const { value: footprints, loading: loadingFootprints } = useFootprints(addressRSS3?.resolvedAddress ?? '') - const addressLiteral = getAddressByType(AddressNameType.ADDRESS) - const addressENS = getAddressByType(AddressNameType.ENS) - const addressUNS = getAddressByType(AddressNameType.UNS) - const addressGUN = getAddressByType(AddressNameType.GUN) - const addressRSS3 = getAddressByType(AddressNameType.RSS3) - const addressTheGraph = getAddressByType(AddressNameType.THE_GRAPH) + const currentTagComputed = currentTag ?? (addressNFTs ? PageTags.NFTTag : daoPayload ? PageTags.DAOTag : undefined) - switch (currentTag) { + const addressName = useMemo(() => { + if (!currentTagComputed) return + switch (currentTagComputed) { case PageTags.WalletTag: return case PageTags.NFTTag: - return addressENS || addressUNS || addressRSS3 || addressLiteral || addressGUN || addressTheGraph + return addressNFTs case PageTags.DonationTag: - return addressRSS3 || addressENS || addressUNS || addressLiteral || addressGUN || addressTheGraph + return addressRSS3 case PageTags.FootprintTag: - return addressRSS3 || addressENS || addressUNS || addressLiteral || addressGUN || addressTheGraph + return addressRSS3 case PageTags.DAOTag: return default: - unreachable(currentTag) + unreachable(currentTagComputed) } - }, [currentTag, addressNames]) - - const address = addressName?.resolvedAddress ?? '' + }, [currentTagComputed, addressNFTs, addressRSS3]) const content = useMemo(() => { - switch (currentTag) { + if (!currentTagComputed) return null + switch (currentTagComputed) { case PageTags.WalletTag: return case PageTags.NFTTag: - return + return case PageTags.DonationTag: - return + return case PageTags.FootprintTag: - return + return case PageTags.DAOTag: return default: - unreachable(currentTag) + unreachable(currentTagComputed) } - }, [address, currentTag, daoPayload, identity.identifier]) + }, [addressNFTs, addressRSS3, currentTagComputed, daoPayload, donations, footprints, identity.identifier]) if (hidden) return null - if (loadingAddressNames) + if (loadingAddressNames || loadingDAO) return (
) - if (!addressName) + if (!addressNFTs && !addressRSS3 && !daoPayload) return (
- -
-
- +
+ {addressName ? ( +
+ +
+ ) : null}
{content}
) diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/FootprintPage.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/FootprintPage.tsx index 21cf9d77e585..d3c0ab8dd04b 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/FootprintPage.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/FootprintPage.tsx @@ -1,10 +1,11 @@ import urlcat from 'urlcat' import { makeStyles } from '@masknet/theme' -import { CircularProgress, Link } from '@mui/material' -import type { GeneralAssetWithTags } from '../types' +import { Link } from '@mui/material' +import type { AddressName } from '@masknet/web3-shared-evm' +import type { GeneralAsset, GeneralAssetWithTags } from '../types' import { RSS3_DEFAULT_IMAGE } from '../constants' import { FootprintCard } from './components' -import { useFootprints, useRss3Profile } from './hooks' +import { useRss3Profile } from './hooks' const useStyles = makeStyles()((theme) => ({ address: { @@ -17,43 +18,35 @@ const useStyles = makeStyles()((theme) => ({ }, })) -const getFootprintLink = (address: string, footprint: GeneralAssetWithTags) => { +const getFootprintLink = (label: string, footprint: GeneralAssetWithTags) => { const { platform, identity, id, type } = footprint - return urlcat('https://rss3.bio/:address/singlefootprint/:platform/:identity/:id/:type', { - address, + return urlcat(`https://${label}.bio/singlefootprint/:platform/:identity/:id/:type`, { platform, identity, id, - type, + type: type.replaceAll('-', '.'), }) } export interface FootprintPageProps { - address: string + addressName?: AddressName + footprints?: GeneralAsset[] } export function FootprintPage(props: FootprintPageProps) { - const { address } = props + const { addressName, footprints = [] } = props const { classes } = useStyles() - const { value: profile } = useRss3Profile(address) + const { value: profile } = useRss3Profile(addressName?.resolvedAddress ?? '') const username = profile?.name - const { footprints, loading } = useFootprints(address) - - if (loading) { - return ( -
- -
- ) - } + if (!addressName) return null return (
{footprints.map((footprint) => ( diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/NFTPage.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/NFTPage.tsx index 03bf9848f2ae..8f5f64b7d9d1 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/NFTPage.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/NFTPage.tsx @@ -1,7 +1,5 @@ import { getMaskColor, makeStyles, useStylesExtends } from '@masknet/theme' -import { Box, Typography } from '@mui/material' import { CollectionList } from '../../../extension/options-page/DashboardComponents/CollectibleList' -import { useI18N } from '../../../utils' const useStyles = makeStyles()((theme) => ({ root: { @@ -22,18 +20,8 @@ export interface NFTPageProps extends withClasses<'text' | 'button'> { export function NFTPage(props: NFTPageProps) { const { address } = props - const { t } = useI18N() const classes = useStylesExtends(useStyles(), props) - if (!address) - return ( -
- - {t('dashboard_no_collectible_found')} - -
- ) - return (
diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/PageTag.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/PageTag.tsx index dcf77987e852..2cdfa68795c2 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/PageTag.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/PageTag.tsx @@ -2,8 +2,9 @@ import classNames from 'classnames' import { makeStyles } from '@masknet/theme' import { Button } from '@mui/material' import { useI18N } from '../../../utils' -import { PageTags } from '../types' -import { useDonations, useFootprints, Dao_Payload } from './hooks' +import { GeneralAsset, PageTags } from '../types' +import type { AddressName } from '@masknet/web3-shared-evm' +import type { Dao_Payload } from './hooks' const useStyles = makeStyles()((theme) => ({ root: { @@ -43,21 +44,21 @@ const useStyles = makeStyles()((theme) => ({ })) export interface PageTagProps { - tag: PageTags - address: string - daoPayload: Dao_Payload | undefined + tag?: PageTags + daoPayload?: Dao_Payload + donations?: GeneralAsset[] + footprints?: GeneralAsset[] + addressNFTs?: AddressName + addressRSS3?: AddressName onChange: (tag: PageTags) => void } export function PageTag(props: PageTagProps) { - const { classes } = useStyles() - const { onChange, tag, address, daoPayload } = props + const { tag, daoPayload, donations, footprints, addressNFTs, addressRSS3, onChange } = props const { t } = useI18N() + const { classes } = useStyles() - const { donations } = useDonations(address) - const { footprints } = useFootprints(address) - const hasDonations = donations.length > 0 - const hasFootprints = footprints.length > 0 + if (!tag) return null return (
@@ -68,14 +69,16 @@ export function PageTag(props: PageTagProps) { size="medium"> {t('wallets')} - - {hasDonations ? ( + {addressNFTs ? ( + + ) : null} + {donations?.length ? (
) } - const info = result.tokenDetailed.info + return (
- {info.name} - {info.name} + + {result.tokenDetailed.info.name} {claimed && {t('plugin_red_packet_claimed')}}
) diff --git a/packages/mask/src/plugins/RedPacket/SNSAdaptor/NftRedPacketHistoryItem.tsx b/packages/mask/src/plugins/RedPacket/SNSAdaptor/NftRedPacketHistoryItem.tsx index f29a2232bd0b..56d0efe93b65 100644 --- a/packages/mask/src/plugins/RedPacket/SNSAdaptor/NftRedPacketHistoryItem.tsx +++ b/packages/mask/src/plugins/RedPacket/SNSAdaptor/NftRedPacketHistoryItem.tsx @@ -131,6 +131,12 @@ const useStyles = makeStyles()((theme) => { color: theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.26)' : 'rgba(255, 255, 255, 0.3)', }, }, + ellipsis: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + maxWidth: 360, + }, } }) @@ -179,12 +185,17 @@ export const NftRedPacketHistoryItem: FC = memo( classes={{ icon: classes.icon }} address={contractDetailed?.address ?? ''} name={contractDetailed?.name ?? '-'} - logoURI={contractDetailed?.iconURL ?? ''} + logoURI={ + contractDetailed?.iconURL ?? + new URL('../../../resources/maskFilledIcon.png', import.meta.url).toString() + } />
- + {history.message === '' ? t('plugin_red_packet_best_wishes') : history.message} diff --git a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacket/index.tsx b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacket/index.tsx index eca7b922266e..23f27fb06aae 100644 --- a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacket/index.tsx +++ b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacket/index.tsx @@ -55,9 +55,11 @@ export function RedPacket(props: RedPacketProps) { const { NATIVE_TOKEN_ADDRESS } = useTokenConstants() const { value: tokenDetailed } = useFungibleTokenDetailed( - payload.token?.type ?? payload.token_type ?? isSameAddress(NATIVE_TOKEN_ADDRESS, payload.token_address) - ? EthereumTokenType.Native - : EthereumTokenType.ERC20, + payload.token?.type ?? + payload.token_type ?? + (isSameAddress(NATIVE_TOKEN_ADDRESS, payload.token_address) + ? EthereumTokenType.Native + : EthereumTokenType.ERC20), payload.token?.address ?? payload.token_address ?? '', ) const token = payload.token ?? tokenDetailed @@ -105,7 +107,7 @@ export function RedPacket(props: RedPacketProps) { const state = canClaim ? claimState : refundState if (state.type === TransactionStateType.UNKNOWN) return if (!availability || !token) return - if (state.type === TransactionStateType.HASH) { + if (state.type === TransactionStateType.CONFIRMED) { setTransactionDialog({ open: true, shareLink: shareLink!.toString(), @@ -119,7 +121,6 @@ export function RedPacket(props: RedPacketProps) { }) : '', }) - } else if (state.type === TransactionStateType.CONFIRMED) { resetClaimCallback() resetRefundCallback() revalidateAvailability() @@ -211,7 +212,7 @@ export function RedPacket(props: RedPacketProps) { ) : null}
-
+
{payload.sender.message} @@ -227,14 +228,16 @@ export function RedPacket(props: RedPacketProps) {
- + {listOfStatus.includes(RedPacketStatus.expired) || listOfStatus.includes(RedPacketStatus.empty) ? null : ( + + )} ) } diff --git a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacket/useStyles.ts b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacket/useStyles.ts index 704407003403..c1981af2d161 100644 --- a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacket/useStyles.ts +++ b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacket/useStyles.ts @@ -158,5 +158,13 @@ to { connectWallet: { marginTop: 16, }, + fullWidthBox: { + width: '100%', + }, + loadingText: { + textAlign: 'center', + fontSize: 24, + marginTop: 210, + }, } }) diff --git a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketConfirmDialog.tsx b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketConfirmDialog.tsx index 1f681e671715..786618659e96 100644 --- a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketConfirmDialog.tsx +++ b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketConfirmDialog.tsx @@ -59,6 +59,10 @@ const useStyles = makeStyles()((theme) => ({ color: theme.palette.mode === 'light' ? '#7B8192' : '#6F767C', }, }, + ellipsis: { + textOverflow: 'ellipsis', + overflow: 'hidden', + }, })) export interface ConfirmRedPacketFormProps { @@ -81,7 +85,7 @@ export function RedPacketConfirmDialog(props: ConfirmRedPacketFormProps) { return ( - + {settings?.message} diff --git a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketERC20Form.tsx b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketERC20Form.tsx index 0cd8ba2ddfa4..c6dcf9ba10b6 100644 --- a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketERC20Form.tsx +++ b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketERC20Form.tsx @@ -7,6 +7,7 @@ import { useNativeTokenDetailed, useRedPacketConstants, useFungibleTokenBalance, + useChainId, } from '@masknet/web3-shared-evm' import { isGreaterThan, isZero, multipliedBy, rightShift } from '@masknet/web3-shared-base' import { omit } from 'lodash-unified' @@ -89,6 +90,7 @@ export function RedPacketERC20Form(props: RedPacketFormProps) { const { onChange, onNext, origin } = props // context const account = useAccount() + const chainId = useChainId() const { HAPPY_RED_PACKET_ADDRESS_V4 } = useRedPacketConstants() //#region select token @@ -148,6 +150,10 @@ export function RedPacketERC20Form(props: RedPacketFormProps) { const totalAmount = useMemo(() => multipliedBy(amount, isRandom ? 1 : shares ?? '0'), [amount, shares]) const isDivisible = !totalAmount.dividedBy(shares).isLessThan(1) + useEffect(() => { + setToken(nativeTokenDetailed) + }, [chainId, nativeTokenDetailed]) + useEffect(() => { setRawAmount('0') }, [token]) diff --git a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketInHistoryList.tsx b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketInHistoryList.tsx index 654a37834c79..7acdb3a09324 100644 --- a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketInHistoryList.tsx +++ b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketInHistoryList.tsx @@ -93,13 +93,14 @@ const useStyles = makeStyles()((theme) => { flexWrap: 'wrap', }, }, - div: {}, + div: { + maxWidth: 350, + }, icon: { width: 27, height: 27, }, title: { - whiteSpace: 'break-spaces', fontWeight: 500, fontSize: 16, }, @@ -112,10 +113,17 @@ const useStyles = makeStyles()((theme) => { }, actionButton: { height: 26, + background: theme.palette.mode === 'light' ? '#000' : '#fff', + color: theme.palette.mode === 'light' ? '#fff' : '#000', minHeight: 'auto', [smallQuery]: { marginTop: theme.spacing(1), }, + '&:hover': { + background: theme.palette.mode === 'light' ? '#000' : '#fff', + color: theme.palette.mode === 'light' ? '#fff' : '#000', + opacity: 0.8, + }, }, footer: { width: '100%', @@ -165,6 +173,9 @@ const useStyles = makeStyles()((theme) => { color: theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.26)' : 'rgba(255, 255, 255, 0.3)', }, }, + fullWidthBox: { + width: '100%', + }, } }) @@ -257,11 +268,13 @@ export function RedPacketInHistoryList(props: RedPacketInHistoryListProps) {
- - {history.sender.message === '' - ? t('plugin_red_packet_best_wishes') - : history.sender.message} - +
+ + {history.sender.message === '' + ? t('plugin_red_packet_best_wishes') + : history.sender.message} + +
{t('plugin_red_packet_history_duration', { startTime: dateTimeFormat(new Date(history.creation_time)), @@ -330,8 +343,6 @@ export function RedPacketInHistoryList(props: RedPacketInHistoryListProps) { ) : null}
diff --git a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketNft.tsx b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketNft.tsx index 97121ec708ae..bba4b0699001 100644 --- a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketNft.tsx +++ b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketNft.tsx @@ -216,6 +216,13 @@ const useStyles = makeStyles()((theme) => ({ borderColor: 'white', }, }, + ellipsis: { + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden', + maxWidth: 400, + fontSize: '1.5rem', + }, })) export interface RedPacketNftProps { payload: RedPacketNftJSONPayload @@ -354,7 +361,7 @@ export function RedPacketNft({ payload }: RedPacketNftProps) { {payload.message}} subheader={ - Mask.io + Mask.io From: @{payload.senderName}
@@ -418,7 +425,7 @@ export function RedPacketNft({ payload }: RedPacketNftProps) {
- ) : ( + ) : availability.isClaimedAll || availability.isCompleted ? null : ( ) - const tabs = [ , , @@ -225,7 +230,10 @@ export function Collectible(props: CollectibleProps) { {asset.value.description ? ( - + ) : null} diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/HistoryTab/index.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/HistoryTab/index.tsx index 168a2c5a6e36..7a8349907354 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/HistoryTab/index.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/HistoryTab/index.tsx @@ -14,6 +14,9 @@ const useStyles = makeStyles()((theme) => { root: { overflow: 'auto', }, + head: { + wordBreak: 'keep-all', + }, content: { padding: '0 !important', }, @@ -74,7 +77,7 @@ export function HistoryTab(props: HistoryTabProps) { return ( - + {t('plugin_collectible_event')} {isDifferenceToken ? ( diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingTab.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingTab.tsx index 8f38ed8aa79c..80b56018d904 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingTab.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingTab.tsx @@ -30,6 +30,7 @@ const useStyles = makeStyles()((theme) => { }, emptyCell: { borderStyle: 'none', + textAlign: 'center', }, button: { marginLeft: theme.spacing(1), diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/OfferTab.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/OfferTab.tsx index 3d916aef78ae..baf5185c23a9 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/OfferTab.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/OfferTab.tsx @@ -29,6 +29,7 @@ const useStyles = makeStyles()((theme) => { }, emptyCell: { borderStyle: 'none', + textAlign: 'center', }, button: { marginLeft: theme.spacing(1), diff --git a/packages/mask/src/plugins/ITO/SNSAdaptor/ConfirmDialog.tsx b/packages/mask/src/plugins/ITO/SNSAdaptor/ConfirmDialog.tsx index d654c305fca6..88afd11f5837 100644 --- a/packages/mask/src/plugins/ITO/SNSAdaptor/ConfirmDialog.tsx +++ b/packages/mask/src/plugins/ITO/SNSAdaptor/ConfirmDialog.tsx @@ -83,6 +83,7 @@ const useStyles = makeStyles()((theme) => ({ padding: theme.spacing(1), textAlign: 'left', color: theme.palette.text.secondary, + wordBreak: 'keep-all', }, button: { color: '#fff', From 447b151a4238e8d49268ab7c9941b8ddcf5d80e2 Mon Sep 17 00:00:00 2001 From: lelenei <72531217+lelenei@users.noreply.github.com> Date: Thu, 23 Dec 2021 13:30:49 +0800 Subject: [PATCH 13/31] fix: incorrect inject behavior when open message dialog on Twitter main page (#5275) --- .../twitter.com/injection/NFT/ProfileNFTAvatar.tsx | 4 ++-- .../social-network-adaptor/twitter.com/utils/selector.ts | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/ProfileNFTAvatar.tsx b/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/ProfileNFTAvatar.tsx index 6258830fa961..0cee95ba5f46 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/ProfileNFTAvatar.tsx +++ b/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/ProfileNFTAvatar.tsx @@ -6,8 +6,8 @@ import { createReactRootShadowed, MaskMessages, NFTAvatarEvent, startWatch } fro import { searchAvatarOpenFileSelector, searchProfessionalButtonSelector, - searchProfileAvatarSelector, searchProfileSaveSelector, + searchProfileSetAvatarSelector, } from '../../utils/selector' import { useCurrentVisitingIdentity } from '../../../../components/DataSource/useActivatedUI' import { getAvatarId } from '../../utils/user' @@ -17,7 +17,7 @@ import { NFTAvatar } from '../../../../plugins/Avatar/SNSAdaptor/NFTAvatar' import { hookInputUploadOnce } from '@masknet/injected-script' export async function injectProfileNFTAvatarInTwitter(signal: AbortSignal) { - const watcher = new MutationObserverWatcher(searchProfileAvatarSelector()) + const watcher = new MutationObserverWatcher(searchProfileSetAvatarSelector()) startWatch(watcher, signal) createReactRootShadowed(watcher.firstDOMProxy.afterShadow, { signal }).render() } diff --git a/packages/mask/src/social-network-adaptor/twitter.com/utils/selector.ts b/packages/mask/src/social-network-adaptor/twitter.com/utils/selector.ts index f8305af1cd78..fc14d4627254 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/utils/selector.ts +++ b/packages/mask/src/social-network-adaptor/twitter.com/utils/selector.ts @@ -202,7 +202,9 @@ export const selfInfoSelectors = () => ({ }) //#region nft avatar -export const searchProfileAvatarSelector = () => querySelectorAll('[data-testid="fileInput"]').at(1).closest(4) +export const searchProfileAvatarSelector = () => { + return querySelectorAll('[data-testid="fileInput"]').at(1).closest(4) +} export const searchProfileAvatarParentSelector = () => querySelectorAll('[data-testid="fileInput"]').at(1).closest(2).evaluate()[0] @@ -224,6 +226,9 @@ export const searchProfileSaveSelector = () => querySelector('[data-testid="P export const searchProfessionalButtonSelector = () => querySelector('[data-testid="ProfessionalButton_Switch_To_Professional"]') + +export const searchProfileSetAvatarSelector = () => + searchProfessionalButtonSelector().closest(3).querySelector('div > div:nth-child(2) > div:nth-child(2)') //#endregion //#region avatar selector From b76e67cb6a2badad7055fb0e109e55f3cd85f2e5 Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Thu, 30 Dec 2021 19:11:23 +0800 Subject: [PATCH 14/31] chore: update ito abi --- .../mask/src/plugins/ITO/SNSAdaptor/ITO.tsx | 20 ++++++------------- .../hooks/useAvailabilityComputed.ts | 3 ++- .../ITO/SNSAdaptor/hooks/useClaimablePools.ts | 6 ++++-- .../ITO/Worker/apis/checkAvailability.ts | 2 +- packages/mask/src/plugins/ITO/types.ts | 2 +- packages/web3-contracts/abis/ITO2.json | 2 +- packages/web3-contracts/types/ITO2.d.ts | 2 +- 7 files changed, 16 insertions(+), 21 deletions(-) diff --git a/packages/mask/src/plugins/ITO/SNSAdaptor/ITO.tsx b/packages/mask/src/plugins/ITO/SNSAdaptor/ITO.tsx index fc361bc0b702..e0bb2e83eae8 100644 --- a/packages/mask/src/plugins/ITO/SNSAdaptor/ITO.tsx +++ b/packages/mask/src/plugins/ITO/SNSAdaptor/ITO.tsx @@ -11,6 +11,7 @@ import { useChainId, useChainIdValid, useTokenConstants, + isNativeTokenAddress, } from '@masknet/web3-shared-evm' import { isZero, ZERO, isGreaterThan } from '@masknet/web3-shared-base' import { Box, Card, Grid, Link, Typography } from '@mui/material' @@ -316,10 +317,9 @@ export function ITO(props: ITO_Props) { .toString() const canWithdraw = useMemo( () => + !availability?.destructed && isAccountSeller && - !tradeInfo?.destructInfo && - !loadingTradeInfo && - !availability?.exchanged_tokens.every((t) => t === '0') && + !availability?.exchanged_tokens.every(isZero) && (listOfStatus.includes(ITO_Status.expired) || noRemain), [tradeInfo, listOfStatus, isAccountSeller, noRemain, loadingTradeInfo], ) @@ -727,7 +727,8 @@ export function ITO(props: ITO_Props) { className={classes.actionButton}> {t('plugin_ito_withdraw')} - ) : !ifQualified || !(ifQualified as Qual_V2).qualified ? ( + ) : (!ifQualified || !(ifQualified as Qual_V2).qualified) && + !isNativeTokenAddress(qualificationAddress) ? ( - ) : listOfStatus.includes(ITO_Status.expired) ? ( - undefined} - variant="contained" - size="large" - className={classes.actionButton}> - {t('plugin_ito_expired')} - - ) : listOfStatus.includes(ITO_Status.waited) ? ( + ) : listOfStatus.includes(ITO_Status.expired) ? null : listOfStatus.includes(ITO_Status.waited) ? ( { + const unlockTime = Number(raw.availability.unlock_time) * 1000 return { pids: [raw.pid], amount: Number(raw.availability.swapped), token: raw.token, - isClaimable: raw.availability.unlocked, - unlockTime: new Date(Number(raw.availability.unlock_time) * 1000), + isClaimable: isAfter(Date.now(), unlockTime), + unlockTime: new Date(unlockTime), } }) .reduce((acc: SwappedToken[], cur) => { diff --git a/packages/mask/src/plugins/ITO/Worker/apis/checkAvailability.ts b/packages/mask/src/plugins/ITO/Worker/apis/checkAvailability.ts index 3927e00a8ef4..b2d4491c5ae5 100644 --- a/packages/mask/src/plugins/ITO/Worker/apis/checkAvailability.ts +++ b/packages/mask/src/plugins/ITO/Worker/apis/checkAvailability.ts @@ -23,7 +23,7 @@ function decodeResult(data: string, isV1: boolean): Availability { remaining: +parseHexToInt(results[1]), started: results[2], expired: results[3], - unlocked: results[4], + destructed: results[4], unlock_time: parseHexToInt(results[5]), swapped: parseHexToInt(results[6]), exchanged_tokens: parse(results[7]).map(parseHexToInt), diff --git a/packages/mask/src/plugins/ITO/types.ts b/packages/mask/src/plugins/ITO/types.ts index 6b9f99dd6e48..4b12672df00f 100644 --- a/packages/mask/src/plugins/ITO/types.ts +++ b/packages/mask/src/plugins/ITO/types.ts @@ -81,7 +81,7 @@ export interface Availability { remaining: number started: boolean expired: boolean - unlocked: boolean + destructed: boolean unlock_time: string swapped: string exchanged_tokens: string[] diff --git a/packages/web3-contracts/abis/ITO2.json b/packages/web3-contracts/abis/ITO2.json index 4f1fe29ec979..823ddf58d724 100644 --- a/packages/web3-contracts/abis/ITO2.json +++ b/packages/web3-contracts/abis/ITO2.json @@ -271,7 +271,7 @@ }, { "internalType": "bool", - "name": "unlocked", + "name": "destructed", "type": "bool" }, { diff --git a/packages/web3-contracts/types/ITO2.d.ts b/packages/web3-contracts/types/ITO2.d.ts index f0cdf8c45370..1e6d72768ea5 100644 --- a/packages/web3-contracts/types/ITO2.d.ts +++ b/packages/web3-contracts/types/ITO2.d.ts @@ -107,7 +107,7 @@ export interface ITO2 extends BaseContract { remaining: string started: boolean expired: boolean - unlocked: boolean + destructed: boolean unlock_time: string swapped: string exchanged_tokens: string[] From 0edd24919c3fda3a1370fdcc9a801b3dd2e4e5b2 Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Fri, 31 Dec 2021 11:11:34 +0800 Subject: [PATCH 15/31] fix: type --- packages/mask/src/plugins/ITO/SNSAdaptor/ITO.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/mask/src/plugins/ITO/SNSAdaptor/ITO.tsx b/packages/mask/src/plugins/ITO/SNSAdaptor/ITO.tsx index e0bb2e83eae8..057cc8d18ba8 100644 --- a/packages/mask/src/plugins/ITO/SNSAdaptor/ITO.tsx +++ b/packages/mask/src/plugins/ITO/SNSAdaptor/ITO.tsx @@ -11,7 +11,7 @@ import { useChainId, useChainIdValid, useTokenConstants, - isNativeTokenAddress, + isNative, } from '@masknet/web3-shared-evm' import { isZero, ZERO, isGreaterThan } from '@masknet/web3-shared-base' import { Box, Card, Grid, Link, Typography } from '@mui/material' @@ -727,8 +727,7 @@ export function ITO(props: ITO_Props) { className={classes.actionButton}> {t('plugin_ito_withdraw')} - ) : (!ifQualified || !(ifQualified as Qual_V2).qualified) && - !isNativeTokenAddress(qualificationAddress) ? ( + ) : (!ifQualified || !(ifQualified as Qual_V2).qualified) && !isNative(qualificationAddress) ? ( Date: Fri, 31 Dec 2021 15:23:58 +0800 Subject: [PATCH 16/31] fix: inject web3 tab after load (#5366) --- .../twitter.com/injection/EnhancedProfileTab.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/mask/src/social-network-adaptor/twitter.com/injection/EnhancedProfileTab.tsx b/packages/mask/src/social-network-adaptor/twitter.com/injection/EnhancedProfileTab.tsx index 3a0120641158..4ddfa7c40a6d 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/injection/EnhancedProfileTab.tsx +++ b/packages/mask/src/social-network-adaptor/twitter.com/injection/EnhancedProfileTab.tsx @@ -15,8 +15,19 @@ import { EnhancedProfileTab } from '../../../plugins/Profile/SNSAdaptor/Enhanced export function injectEnhancedProfileTabAtTwitter(signal: AbortSignal) { const watcher = new MutationObserverWatcher(searchProfileTabListLastChildSelector()) - startWatch(watcher, signal) - createReactRootShadowed(watcher.firstDOMProxy.afterShadow, { signal }).render() + let tabInjected = false + const contentWatcher = new MutationObserverWatcher(searchProfileTabPageSelector()).useForeach((node, key, meta) => { + const elePage = searchProfileTabPageSelector().evaluate() + if (elePage && !tabInjected) { + startWatch(watcher, signal) + createReactRootShadowed(watcher.firstDOMProxy.afterShadow, { signal }).render( + , + ) + tabInjected = true + } + }) + + startWatch(contentWatcher, signal) } function getStyleProps() { From 867c4fafcbae83a22999c96df7576e043996a1fe Mon Sep 17 00:00:00 2001 From: cformcn Date: Fri, 31 Dec 2021 18:53:55 +0800 Subject: [PATCH 17/31] feat: add openocean (#5198) * feat: add openocean * fix: use leftShift * fix: add openocean to ignoreWords * fix: remove annotation * fix: clear openoceanIcon properties * fix: conflict * fix: conflict * chore: qa * chore: qa * chore: qa Co-authored-by: guanbinrui <52657989+guanbinrui@users.noreply.github.com> Co-authored-by: zhouhanseng --- cspell.json | 2 + .../Labs/components/SwapSettingDialog.tsx | 17 +++- .../SNSAdaptor/trader/TradeProviderIcon.tsx | 3 + .../mask/src/plugins/Trader/apis/index.ts | 1 + .../plugins/Trader/apis/openocean/index.ts | 39 ++++++++ .../src/plugins/Trader/apis/trader/index.ts | 21 ++++- .../src/plugins/Trader/constants/openocean.ts | 13 +++ packages/mask/src/plugins/Trader/pipes.ts | 6 ++ packages/mask/src/plugins/Trader/settings.ts | 3 + .../Trader/trader/dodo/useTradeGasLimit.ts | 3 +- .../trader/openocean/useSlippageTolerance.ts | 9 ++ .../Trader/trader/openocean/useTrade.ts | 65 ++++++++++++++ .../trader/openocean/useTradeCallback.ts | 89 +++++++++++++++++++ .../trader/openocean/useTradeComputed.ts | 38 ++++++++ .../trader/openocean/useTradeGasLimit.ts | 27 ++++++ .../Trader/trader/useAllTradeComputed.ts | 14 +++ .../Trader/trader/useGetTradeContext.ts | 5 ++ .../Trader/trader/useTradeApproveComputed.ts | 2 + .../plugins/Trader/trader/useTradeCallback.ts | 12 ++- .../plugins/Trader/trader/useTradeContext.ts | 5 ++ .../mask/src/plugins/Trader/types/index.ts | 1 + .../src/plugins/Trader/types/openocean.ts | 45 ++++++++++ packages/mask/src/resources/OpenOceanIcon.tsx | 16 ++++ packages/public-api/src/web.ts | 1 + packages/web3-constants/evm/trader.json | 28 ++++++ 25 files changed, 456 insertions(+), 9 deletions(-) create mode 100644 packages/mask/src/plugins/Trader/apis/openocean/index.ts create mode 100644 packages/mask/src/plugins/Trader/constants/openocean.ts create mode 100644 packages/mask/src/plugins/Trader/trader/openocean/useSlippageTolerance.ts create mode 100644 packages/mask/src/plugins/Trader/trader/openocean/useTrade.ts create mode 100644 packages/mask/src/plugins/Trader/trader/openocean/useTradeCallback.ts create mode 100644 packages/mask/src/plugins/Trader/trader/openocean/useTradeComputed.ts create mode 100644 packages/mask/src/plugins/Trader/trader/openocean/useTradeGasLimit.ts create mode 100644 packages/mask/src/plugins/Trader/types/openocean.ts create mode 100644 packages/mask/src/resources/OpenOceanIcon.tsx diff --git a/cspell.json b/cspell.json index 566f31e16a1b..35dd5e382b48 100644 --- a/cspell.json +++ b/cspell.json @@ -285,6 +285,8 @@ "crwdns", "dodoapi", "getdodoroute", + "openocean", + "OPENOCEAN", "getx", "hookform", "idiv", diff --git a/packages/dashboard/src/pages/Labs/components/SwapSettingDialog.tsx b/packages/dashboard/src/pages/Labs/components/SwapSettingDialog.tsx index 62099f1d293a..bd5e36d8c5da 100644 --- a/packages/dashboard/src/pages/Labs/components/SwapSettingDialog.tsx +++ b/packages/dashboard/src/pages/Labs/components/SwapSettingDialog.tsx @@ -27,6 +27,7 @@ export default function SwapSettingDialog({ open, onClose }: SettingDialogProps) { label: 'Balancer', value: TradeProvider.BALANCER }, { label: 'DODO', value: TradeProvider.DODO }, { label: 'Bancor', value: TradeProvider.BANCOR }, + { label: 'OpenOcean', value: TradeProvider.OPENOCEAN }, ] const polygonOptions = [ @@ -34,6 +35,7 @@ export default function SwapSettingDialog({ open, onClose }: SettingDialogProps) { label: 'SushiSwap', value: TradeProvider.SUSHISWAP }, { label: '0x', value: TradeProvider.ZRX }, { label: 'DODO', value: TradeProvider.DODO }, + { label: 'OpenOcean', value: TradeProvider.OPENOCEAN }, ] const bscOptions = [ @@ -41,11 +43,22 @@ export default function SwapSettingDialog({ open, onClose }: SettingDialogProps) { label: 'SushiSwap', value: TradeProvider.SUSHISWAP }, { label: '0x', value: TradeProvider.ZRX }, { label: 'DODO', value: TradeProvider.DODO }, + { label: 'OpenOcean', value: TradeProvider.OPENOCEAN }, ] - const arbitrumOptions = [{ label: 'UniSwap V3', value: TradeProvider.UNISWAP_V3 }] + const arbitrumOptions = [ + { label: 'UniSwap V3', value: TradeProvider.UNISWAP_V3 }, + { label: 'OpenOcean', value: TradeProvider.OPENOCEAN }, + ] + + const xDaiOptions = [ + { label: 'SushiSwap', value: TradeProvider.SUSHISWAP }, + { label: 'OpenOcean', value: TradeProvider.OPENOCEAN }, + ] + + const fantomOptions = xDaiOptions - const xDaiOptions = [{ label: 'SushiSwap', value: TradeProvider.SUSHISWAP }] + const celoOptions = [{ label: 'SushiSwap', value: TradeProvider.SUSHISWAP }] const t = useDashboardI18N() diff --git a/packages/mask/src/plugins/Trader/SNSAdaptor/trader/TradeProviderIcon.tsx b/packages/mask/src/plugins/Trader/SNSAdaptor/trader/TradeProviderIcon.tsx index 1edffc16c22f..422b3e44e89d 100644 --- a/packages/mask/src/plugins/Trader/SNSAdaptor/trader/TradeProviderIcon.tsx +++ b/packages/mask/src/plugins/Trader/SNSAdaptor/trader/TradeProviderIcon.tsx @@ -8,6 +8,7 @@ import { SushiSwapIcon } from '../../../../resources/SushiSwapIcon' import { UniswapIcon } from '../../../../resources/UniswapIcon' import { ZRXIcon } from '../../../../resources/ZRXIcon' import { DODOIcon } from '../../../../resources/DODOIcon' +import { OpenOceanIcon } from '../../../../resources/OpenOceanIcon' import { resolveTradeProviderName } from '../../pipes' const quickswapIcon = new URL('../../../../resources/quickswap.png', import.meta.url).toString() @@ -59,6 +60,8 @@ export function TradeProviderIcon(props: TradeProviderIconProps) { return case TradeProvider.BANCOR: return + case TradeProvider.OPENOCEAN: + return default: unreachable(props.provider) } diff --git a/packages/mask/src/plugins/Trader/apis/index.ts b/packages/mask/src/plugins/Trader/apis/index.ts index 686bb84b62f4..ed4fa7e0fd63 100644 --- a/packages/mask/src/plugins/Trader/apis/index.ts +++ b/packages/mask/src/plugins/Trader/apis/index.ts @@ -6,3 +6,4 @@ export * from './blocks' export * from './LBP' export * from './dodo' export * from './bancor' +export * from './openocean' diff --git a/packages/mask/src/plugins/Trader/apis/openocean/index.ts b/packages/mask/src/plugins/Trader/apis/openocean/index.ts new file mode 100644 index 000000000000..92b8443b25eb --- /dev/null +++ b/packages/mask/src/plugins/Trader/apis/openocean/index.ts @@ -0,0 +1,39 @@ +import type { SwapOOData, SwapOORequest } from '../../types/openocean' +import { OPENOCEAN_BASE_URL } from '../../constants/openocean' +import { leftShift } from '@masknet/web3-shared-base' +import urlcat from 'urlcat' + +export async function swapOO(request: SwapOORequest): Promise { + const response = await fetch( + urlcat(OPENOCEAN_BASE_URL, `/${request.chainId}/swap`, { + inTokenSymbol: request.toToken?.symbol, + inTokenAddress: request.fromToken?.address, + outTokenSymbol: request.toToken?.symbol, + outTokenAddress: request.toToken?.address, + amount: request.fromAmount, + gasPrice: 5000000000, + slippage: request.slippage, + disabledDexIds: '', + account: request.userAddr, + }), + ) + const payload = await response.json() + const { data, outAmount, minOutAmount, to, value } = payload + const _resAmount = leftShift(outAmount, request.toToken.decimals).toNumber() + const _fromAmount = leftShift(request.fromAmount, request.fromToken.decimals).toNumber() + return { + data, + targetApproveAddr: request.fromToken.address, + targetDecimals: request.fromToken.decimals, + resAmount: _resAmount, + fromAmount: _fromAmount, + resPricePerFromToken: +(_fromAmount / _resAmount).toFixed(8), + resPricePerToToken: +(_resAmount / _fromAmount).toFixed(8), + to, + value, + slippage: request.slippage, + fromTokenSymbol: request.fromToken.symbol, + toTokenSymbol: request.toToken.symbol, + minOutAmount, + } as SwapOOData +} diff --git a/packages/mask/src/plugins/Trader/apis/trader/index.ts b/packages/mask/src/plugins/Trader/apis/trader/index.ts index b9ba608d5120..7e76a5a39655 100644 --- a/packages/mask/src/plugins/Trader/apis/trader/index.ts +++ b/packages/mask/src/plugins/Trader/apis/trader/index.ts @@ -16,15 +16,28 @@ export async function getAvailableTraderProviders(chainId: ChainId) { TradeProvider.BALANCER, TradeProvider.DODO, TradeProvider.BANCOR, + TradeProvider.OPENOCEAN, ] case NetworkType.Polygon: - return [TradeProvider.QUICKSWAP, TradeProvider.SUSHISWAP, TradeProvider.DODO, TradeProvider.ZRX] + return [ + TradeProvider.QUICKSWAP, + TradeProvider.SUSHISWAP, + TradeProvider.DODO, + TradeProvider.ZRX, + TradeProvider.OPENOCEAN, + ] case NetworkType.Binance: - return [TradeProvider.PANCAKESWAP, TradeProvider.SUSHISWAP, TradeProvider.DODO, TradeProvider.ZRX] + return [ + TradeProvider.PANCAKESWAP, + TradeProvider.SUSHISWAP, + TradeProvider.DODO, + TradeProvider.ZRX, + TradeProvider.OPENOCEAN, + ] case NetworkType.Arbitrum: - return [TradeProvider.UNISWAP_V3] + return [TradeProvider.UNISWAP_V3, TradeProvider.OPENOCEAN] case NetworkType.xDai: - return [TradeProvider.SUSHISWAP] + return [TradeProvider.SUSHISWAP, TradeProvider.OPENOCEAN] default: safeUnreachable(networkType) return [] diff --git a/packages/mask/src/plugins/Trader/constants/openocean.ts b/packages/mask/src/plugins/Trader/constants/openocean.ts new file mode 100644 index 000000000000..e3412435e2bc --- /dev/null +++ b/packages/mask/src/plugins/Trader/constants/openocean.ts @@ -0,0 +1,13 @@ +import { NetworkType } from '@masknet/web3-shared-evm' + +export const OPENOCEAN_BASE_URL = 'https://ethapi.openocean.finance/v2/' + +export const networkNames: Record = { + [NetworkType.Ethereum]: 'eth', + [NetworkType.Binance]: 'bsc', + [NetworkType.Polygon]: 'polygon', + [NetworkType.Arbitrum]: 'arbitrum', + [NetworkType.xDai]: 'xdai', + [NetworkType.Celo]: 'celo', + [NetworkType.Fantom]: 'fantom', +} diff --git a/packages/mask/src/plugins/Trader/pipes.ts b/packages/mask/src/plugins/Trader/pipes.ts index 432b56a9b5b8..96703d73f8a4 100644 --- a/packages/mask/src/plugins/Trader/pipes.ts +++ b/packages/mask/src/plugins/Trader/pipes.ts @@ -56,6 +56,7 @@ export const resolveTradeProviderName = createLookupTableResolver { throw new Error(`Unknown provider type: ${tradeProvider}`) @@ -98,6 +99,8 @@ export function resolveTradeProviderLink(tradeProvider: TradeProvider, networkTy return 'https://app.dodoex.io' case TradeProvider.BANCOR: return 'https://app.bancor.network/eth/swap' + case TradeProvider.OPENOCEAN: + return 'https://openocean.finance/classic' default: unreachable(tradeProvider) } @@ -149,6 +152,9 @@ export function resolveTradePairLink(tradeProvider: TradeProvider, address: stri case TradeProvider.BANCOR: // TODO - Bancor analytics should be available with V3 return `` + case TradeProvider.OPENOCEAN: + // TODO - OpenOcean + return `` default: unreachable(tradeProvider) } diff --git a/packages/mask/src/plugins/Trader/settings.ts b/packages/mask/src/plugins/Trader/settings.ts index 1373bd750078..fc9804c62a7b 100644 --- a/packages/mask/src/plugins/Trader/settings.ts +++ b/packages/mask/src/plugins/Trader/settings.ts @@ -92,6 +92,7 @@ const pancakeswapSettings = createInternalSettings(`${PLUGIN_IDENTIFIER} const balancerSettings = createInternalSettings(`${PLUGIN_IDENTIFIER}+tradeProvider+balancer`, '') const dodoSettings = createInternalSettings(`${PLUGIN_IDENTIFIER}+tradeProvider+dodo`, '') const bancorSettings = createInternalSettings(`${PLUGIN_IDENTIFIER}+tradeProvider+bancor`, '') +const openoceanSettings = createInternalSettings(`${PLUGIN_IDENTIFIER}+tradeProvider+openocean`, '') /** * The general settings of specific tarde provider @@ -118,6 +119,8 @@ export function getCurrentTradeProviderGeneralSettings(tradeProvider: TradeProvi return dodoSettings case TradeProvider.BANCOR: return bancorSettings + case TradeProvider.OPENOCEAN: + return openoceanSettings default: unreachable(tradeProvider) } diff --git a/packages/mask/src/plugins/Trader/trader/dodo/useTradeGasLimit.ts b/packages/mask/src/plugins/Trader/trader/dodo/useTradeGasLimit.ts index 6173ce860c80..fbd80ff11a33 100644 --- a/packages/mask/src/plugins/Trader/trader/dodo/useTradeGasLimit.ts +++ b/packages/mask/src/plugins/Trader/trader/dodo/useTradeGasLimit.ts @@ -9,8 +9,7 @@ import type { AsyncState } from 'react-use/lib/useAsyncFn' export function useTradeGasLimit(tradeComputed: TradeComputed | null): AsyncState { const { targetChainId } = TargetChainIdContext.useContainer() - - const web3 = useWeb3(false, targetChainId) + const web3 = useWeb3(true, targetChainId) const account = useAccount() const config = useMemo(() => { if (!account || !tradeComputed?.trade_) return null diff --git a/packages/mask/src/plugins/Trader/trader/openocean/useSlippageTolerance.ts b/packages/mask/src/plugins/Trader/trader/openocean/useSlippageTolerance.ts new file mode 100644 index 000000000000..f54692e3703f --- /dev/null +++ b/packages/mask/src/plugins/Trader/trader/openocean/useSlippageTolerance.ts @@ -0,0 +1,9 @@ +import { clamp } from 'lodash-unified' +import { useValueRef } from '@masknet/shared' +import { SLIPPAGE_MAX, SLIPPAGE_MIN } from '../../constants' +import { currentSlippageSettings } from '../../settings' + +export function useSlippageTolerance() { + const slippage_ = useValueRef(currentSlippageSettings) + return clamp(slippage_, SLIPPAGE_MIN, SLIPPAGE_MAX) +} diff --git a/packages/mask/src/plugins/Trader/trader/openocean/useTrade.ts b/packages/mask/src/plugins/Trader/trader/openocean/useTrade.ts new file mode 100644 index 000000000000..960a788ce0eb --- /dev/null +++ b/packages/mask/src/plugins/Trader/trader/openocean/useTrade.ts @@ -0,0 +1,65 @@ +import { + FungibleTokenDetailed, + isNativeTokenAddress, + useAccount, + useBlockNumber, + useRPCConstants, + useTokenConstants, + useTraderConstants, +} from '@masknet/web3-shared-evm' +import { useAsyncRetry } from 'react-use' +import { PluginTraderRPC } from '../../messages' +import type { TradeStrategy } from '../../types' +import { TargetChainIdContext } from '../useTargetChainIdContext' +import { useSlippageTolerance } from './useSlippageTolerance' +import { first } from 'lodash-unified' + +export function useTrade( + strategy: TradeStrategy, + inputAmount: string, + outputAmount: string, + inputToken?: FungibleTokenDetailed, + outputToken?: FungibleTokenDetailed, +) { + const { NATIVE_TOKEN_ADDRESS } = useTokenConstants() + const blockNumber = useBlockNumber() + const slippage = useSlippageTolerance() + const { targetChainId } = TargetChainIdContext.useContainer() + const { RPC } = useRPCConstants(targetChainId) + const providerURL = first(RPC) + const { OPENOCEAN_ETH_ADDRESS } = useTraderConstants(targetChainId) + const account = useAccount() + + return useAsyncRetry(async () => { + if (!inputToken || !outputToken) return null + if (inputAmount === '0') return null + const sellToken = isNativeTokenAddress(inputToken.address) + ? { ...inputToken, address: OPENOCEAN_ETH_ADDRESS ?? '' } + : inputToken + const buyToken = isNativeTokenAddress(outputToken.address) + ? { ...outputToken, address: OPENOCEAN_ETH_ADDRESS ?? '' } + : outputToken + return PluginTraderRPC.swapOO({ + isNativeSellToken: isNativeTokenAddress(inputToken.address), + fromToken: sellToken, + toToken: buyToken, + fromAmount: inputAmount, + slippage, + userAddr: account, + rpc: providerURL, + chainId: targetChainId, + }) + }, [ + NATIVE_TOKEN_ADDRESS, + strategy, + inputAmount, + outputAmount, + inputToken?.address, + outputToken?.address, + slippage, + blockNumber, // refresh api each block + account, + providerURL, + targetChainId, + ]) +} diff --git a/packages/mask/src/plugins/Trader/trader/openocean/useTradeCallback.ts b/packages/mask/src/plugins/Trader/trader/openocean/useTradeCallback.ts new file mode 100644 index 000000000000..38d729d0a6bd --- /dev/null +++ b/packages/mask/src/plugins/Trader/trader/openocean/useTradeCallback.ts @@ -0,0 +1,89 @@ +import { useCallback, useMemo, useState } from 'react' +import stringify from 'json-stable-stringify' +import { pick } from 'lodash-unified' +import type { TransactionConfig } from 'web3-core' +import { + TransactionState, + TransactionStateType, + useAccount, + useChainId, + useWeb3, + GasOptionConfig, +} from '@masknet/web3-shared-evm' +import type { SwapOOSuccessResponse, TradeComputed } from '../../types' + +export function useTradeCallback( + tradeComputed: TradeComputed | null, + gasConfig?: GasOptionConfig, +) { + const web3 = useWeb3() + const account = useAccount() + const chainId = useChainId() + const [tradeState, setTradeState] = useState({ + type: TransactionStateType.UNKNOWN, + }) + + // compose transaction config + const config = useMemo(() => { + if (!account || !tradeComputed?.trade_) return null + return { + from: account, + ...pick(tradeComputed.trade_, ['to', 'data', 'value']), + } as TransactionConfig + }, [account, tradeComputed]) + + const tradeCallback = useCallback(async () => { + // validate config + if (!account || !config) { + setTradeState({ + type: TransactionStateType.UNKNOWN, + }) + return + } + + // start waiting for provider to confirm tx + setTradeState({ + type: TransactionStateType.WAIT_FOR_CONFIRMING, + }) + + // compose transaction config + const config_ = { + ...config, + gas: await web3.eth.estimateGas(config).catch((error) => { + setTradeState({ + type: TransactionStateType.FAILED, + error, + }) + throw error + }), + ...gasConfig, + } + + // send transaction and wait for hash + return new Promise((resolve, reject) => { + web3.eth.sendTransaction(config_, (error, hash) => { + if (error) { + setTradeState({ + type: TransactionStateType.FAILED, + error, + }) + reject(error) + } else { + setTradeState({ + type: TransactionStateType.HASH, + hash, + }) + resolve(hash) + } + }) + }) + }, [web3, account, chainId, stringify(config)]) + + const resetCallback = useCallback(() => { + setTradeState({ + type: TransactionStateType.UNKNOWN, + }) + }, []) + + return [tradeState, tradeCallback, resetCallback] as const +} diff --git a/packages/mask/src/plugins/Trader/trader/openocean/useTradeComputed.ts b/packages/mask/src/plugins/Trader/trader/openocean/useTradeComputed.ts new file mode 100644 index 000000000000..f91027c64583 --- /dev/null +++ b/packages/mask/src/plugins/Trader/trader/openocean/useTradeComputed.ts @@ -0,0 +1,38 @@ +import BigNumber from 'bignumber.js' +import { useMemo } from 'react' +import type { FungibleTokenDetailed } from '@masknet/web3-shared-evm' +import { ZERO, pow10 } from '@masknet/web3-shared-base' +import { SwapOOData, TradeComputed, TradeStrategy } from '../../types' + +export function useTradeComputed( + trade: SwapOOData | null, + strategy: TradeStrategy, + inputToken?: FungibleTokenDetailed, + outputToken?: FungibleTokenDetailed, +) { + return useMemo(() => { + if (!trade) return null + if (!inputToken || !outputToken) return null + const isExactIn = strategy === TradeStrategy.ExactIn + if (!isExactIn) return null + const inputAmount = new BigNumber(trade.fromAmount).multipliedBy(pow10(inputToken.decimals)).integerValue() + const executionPrice = new BigNumber(trade.resPricePerToToken) + const outputAmount = new BigNumber(trade.resAmount).multipliedBy(pow10(outputToken.decimals)).integerValue() + const priceImpact = new BigNumber(trade.priceImpact) + return { + strategy, + inputToken, + outputToken, + inputAmount, + outputAmount, + executionPrice, + fee: ZERO, + maximumSold: inputAmount, + minimumReceived: new BigNumber(trade.minOutAmount), + // minimumProtocolFee + priceImpact, + + trade_: { ...trade }, + } as TradeComputed + }, [trade, strategy, inputToken, outputToken]) +} diff --git a/packages/mask/src/plugins/Trader/trader/openocean/useTradeGasLimit.ts b/packages/mask/src/plugins/Trader/trader/openocean/useTradeGasLimit.ts new file mode 100644 index 000000000000..6173ce860c80 --- /dev/null +++ b/packages/mask/src/plugins/Trader/trader/openocean/useTradeGasLimit.ts @@ -0,0 +1,27 @@ +import type { SwapRouteData, TradeComputed } from '../../types' +import { useMemo } from 'react' +import { useAccount, useWeb3 } from '@masknet/web3-shared-evm' +import { TargetChainIdContext } from '../useTargetChainIdContext' +import { pick } from 'lodash-unified' +import type { TransactionConfig } from 'web3-core' +import { useAsync } from 'react-use' +import type { AsyncState } from 'react-use/lib/useAsyncFn' + +export function useTradeGasLimit(tradeComputed: TradeComputed | null): AsyncState { + const { targetChainId } = TargetChainIdContext.useContainer() + + const web3 = useWeb3(false, targetChainId) + const account = useAccount() + const config = useMemo(() => { + if (!account || !tradeComputed?.trade_) return null + return { + from: account, + ...pick(tradeComputed.trade_, ['to', 'data', 'value']), + } as TransactionConfig + }, [account, tradeComputed]) + + return useAsync(async () => { + if (!config) return 0 + return web3.eth.estimateGas(config) + }, [config, web3]) +} diff --git a/packages/mask/src/plugins/Trader/trader/useAllTradeComputed.ts b/packages/mask/src/plugins/Trader/trader/useAllTradeComputed.ts index bb42bb019a60..4f2612cedf48 100644 --- a/packages/mask/src/plugins/Trader/trader/useAllTradeComputed.ts +++ b/packages/mask/src/plugins/Trader/trader/useAllTradeComputed.ts @@ -18,10 +18,13 @@ import { useTradeGasLimit as useDODOTradeGasLimit } from './dodo/useTradeGasLimi import { useTrade as useBancorTrade } from './bancor/useTrade' import { useTradeComputed as useBancorTradeComputed } from './bancor/useTradeComputed' import { useTradeGasLimit as useBancorTradeGasLimit } from './bancor/useTradeGasLimit' +import { useTradeComputed as useOpenOceanTradeComputed } from './openocean/useTradeComputed' +import { useTrade as useOpenOceanTrade } from './openocean/useTrade' import { TradeProvider } from '@masknet/public-api' import { useAvailableTraderProviders } from '../trending/useAvailableTraderProviders' import { useNativeTradeGasLimit } from './useNativeTradeGasLimit' import { TargetChainIdContext } from './useTargetChainIdContext' +import type { TradeComputed, SwapRouteData } from '../types' export function useAllTradeComputed( inputAmount: string, @@ -176,6 +179,16 @@ export function useAllTradeComputed( const bancorSwapEstimateGas = useBancorTradeGasLimit(bancor) + // openocean + const openocean_ = useOpenOceanTrade(TradeStrategy.ExactIn, inputAmount_, '0', inputToken, outputToken) + const openocean = useOpenOceanTradeComputed( + openocean_.value ?? null, + TradeStrategy.ExactIn, + inputToken, + outputToken, + ) + const openoceanSwapEstimateGas = useDODOTradeGasLimit(openocean as TradeComputed | null) + const allTradeResult = [ { provider: TradeProvider.UNISWAP_V2, ...uniswapV2_, value: uniswapV2, gas: uniswapEstimateGas }, { provider: TradeProvider.SUSHISWAP, ...sushiSwap_, value: sushiSwap, gas: sushiSwapEstimateGas }, @@ -187,6 +200,7 @@ export function useAllTradeComputed( { provider: TradeProvider.BALANCER, ...balancer_, value: balancer, gas: balancerSwapEstimateGas }, { provider: TradeProvider.DODO, ...dodo_, value: dodo, gas: dodoSwapEstimateGas }, { provider: TradeProvider.BANCOR, ...bancor_, value: bancor, gas: bancorSwapEstimateGas }, + { provider: TradeProvider.OPENOCEAN, ...openocean_, value: openocean, gas: openoceanSwapEstimateGas }, ] return nativeToken_.value diff --git a/packages/mask/src/plugins/Trader/trader/useGetTradeContext.ts b/packages/mask/src/plugins/Trader/trader/useGetTradeContext.ts index c0eb7e4df671..b9eea9226d02 100644 --- a/packages/mask/src/plugins/Trader/trader/useGetTradeContext.ts +++ b/packages/mask/src/plugins/Trader/trader/useGetTradeContext.ts @@ -113,6 +113,11 @@ export function useGetTradeContext(tradeProvider?: TradeProvider) { TYPE: tradeProvider, ROUTER_CONTRACT_ADDRESS: getTraderConstants(chainId).BANCOR_EXCHANGE_PROXY_ADDRESS, } + case TradeProvider.OPENOCEAN: + return { + TYPE: tradeProvider, + ROUTER_CONTRACT_ADDRESS: getTraderConstants(chainId).BANCOR_EXCHANGE_PROXY_ADDRESS, + } default: if (tradeProvider) unreachable(tradeProvider) return null diff --git a/packages/mask/src/plugins/Trader/trader/useTradeApproveComputed.ts b/packages/mask/src/plugins/Trader/trader/useTradeApproveComputed.ts index 3a64c6fb201f..8130abcb3e97 100644 --- a/packages/mask/src/plugins/Trader/trader/useTradeApproveComputed.ts +++ b/packages/mask/src/plugins/Trader/trader/useTradeApproveComputed.ts @@ -24,6 +24,8 @@ export function useTradeApproveComputed( approveAddress: (() => { if (context?.TYPE === TradeProvider.ZRX) return trade?.trade_ ? (trade.trade_ as SwapQuoteResponse).allowanceTarget : '' + if (context?.TYPE === TradeProvider.OPENOCEAN) + return trade?.trade_ ? (trade?.trade_ as SwapQuoteResponse).to : '' return context?.ROUTER_CONTRACT_ADDRESS ?? '' })(), } diff --git a/packages/mask/src/plugins/Trader/trader/useTradeCallback.ts b/packages/mask/src/plugins/Trader/trader/useTradeCallback.ts index 5994a0906bcd..3065bf857b6f 100644 --- a/packages/mask/src/plugins/Trader/trader/useTradeCallback.ts +++ b/packages/mask/src/plugins/Trader/trader/useTradeCallback.ts @@ -9,6 +9,7 @@ import type { SwapBancorRequest, SwapRouteSuccessResponse, TradeComputed, + SwapOOSuccessResponse, } from '../types' import { useTradeCallback as useNativeTokenWrapperCallback } from './native/useTradeCallback' import { useTradeCallback as useZrxCallback } from './0x/useTradeCallback' @@ -16,6 +17,7 @@ import { useTradeCallback as useUniswapCallback } from './uniswap/useTradeCallba import { useTradeCallback as useBalancerCallback } from './balancer/useTradeCallback' import { useTradeCallback as useDODOCallback } from './dodo/useTradeCallback' import { useTradeCallback as useBancorCallback } from './bancor/useTradeCallback' +import { useTradeCallback as useOpenOceanCallback } from './openocean/useTradeCallback' import { useExchangeProxyContract } from '../contracts/balancer/useExchangeProxyContract' import type { NativeTokenWrapper } from './native/useTradeComputed' import { isNativeTokenWrapper } from '../helpers' @@ -48,7 +50,9 @@ export function useTradeCallback( ? (tradeComputed as TradeComputed) : null const tradeComputedForBancor = !isNativeTokenWrapper_ ? (tradeComputed as TradeComputed) : null - + const tradeComputedForOpenOcean = !isNativeTokenWrapper_ + ? (tradeComputed as TradeComputed) + : null // uniswap like providers const uniswapV2Like = useUniswapCallback(tradeComputedForUniswapV2Like, provider, gasConfig) const uniswapV3Like = useUniswapCallback(tradeComputedForUniswapV3Like, provider, gasConfig) @@ -66,6 +70,10 @@ export function useTradeCallback( const zrx = useZrxCallback(provider === TradeProvider.ZRX ? tradeComputedForZRX : null, gasConfig) const dodo = useDODOCallback(provider === TradeProvider.DODO ? tradeComputedForDODO : null, gasConfig) const bancor = useBancorCallback(provider === TradeProvider.BANCOR ? tradeComputedForBancor : null, gasConfig) + const openocean = useOpenOceanCallback( + provider === TradeProvider.OPENOCEAN ? tradeComputedForOpenOcean : null, + gasConfig, + ) // the trade is an ETH-WETH pair const nativeTokenWrapper = useNativeTokenWrapperCallback( @@ -96,6 +104,8 @@ export function useTradeCallback( return dodo case TradeProvider.BANCOR: return bancor + case TradeProvider.OPENOCEAN: + return openocean default: if (provider) unreachable(provider) return [] diff --git a/packages/mask/src/plugins/Trader/trader/useTradeContext.ts b/packages/mask/src/plugins/Trader/trader/useTradeContext.ts index b5b6dfc7f350..6aa3cdfb1df1 100644 --- a/packages/mask/src/plugins/Trader/trader/useTradeContext.ts +++ b/packages/mask/src/plugins/Trader/trader/useTradeContext.ts @@ -114,6 +114,11 @@ export function useTradeContext(tradeProvider: TradeProvider) { TYPE: tradeProvider, ROUTER_CONTRACT_ADDRESS: getTraderConstants(chainId).BANCOR_EXCHANGE_PROXY_ADDRESS, } + case TradeProvider.OPENOCEAN: + return { + TYPE: tradeProvider, + ROUTER_CONTRACT_ADDRESS: getTraderConstants(chainId).OPENOCEAN_EXCHANGE_PROXY_ADDRESS, + } default: unreachable(tradeProvider) } diff --git a/packages/mask/src/plugins/Trader/types/index.ts b/packages/mask/src/plugins/Trader/types/index.ts index 776dc3316315..bcb516e55046 100644 --- a/packages/mask/src/plugins/Trader/types/index.ts +++ b/packages/mask/src/plugins/Trader/types/index.ts @@ -5,3 +5,4 @@ export * from './dodo' export * from './trader' export * from './trending' export * from './bancor' +export * from './openocean' diff --git a/packages/mask/src/plugins/Trader/types/openocean.ts b/packages/mask/src/plugins/Trader/types/openocean.ts new file mode 100644 index 000000000000..5875abdf78f8 --- /dev/null +++ b/packages/mask/src/plugins/Trader/types/openocean.ts @@ -0,0 +1,45 @@ +import type { FungibleTokenDetailed } from '@masknet/web3-shared-evm' + +export interface SwapOORequest { + isNativeSellToken: boolean + fromToken: FungibleTokenDetailed + toToken: FungibleTokenDetailed + fromAmount: string + slippage: number + userAddr?: string + rpc?: string + chainId: number +} + +export type SwapOOResponse = SwapOOSuccessResponse | SwapOOErrorResponse + +export interface SwapOOSuccessResponse { + code: number + status: 200 + data: SwapOOData +} + +export interface SwapOOErrorResponse { + code: number + status: number + data: string | undefined +} + +export interface SwapOOData { + data: string + priceImpact: number + resAmount: number + resCostGas: number + resPricePerFromToken: number + resPricePerToToken: number + targetApproveAddr: string + targetDecimals: number + to: string + useSource: string + fromAmount: number + value: string + slippage: number + fromTokenSymbol: string + toTokenSymbol: string + minOutAmount: number +} diff --git a/packages/mask/src/resources/OpenOceanIcon.tsx b/packages/mask/src/resources/OpenOceanIcon.tsx new file mode 100644 index 000000000000..8af955de587e --- /dev/null +++ b/packages/mask/src/resources/OpenOceanIcon.tsx @@ -0,0 +1,16 @@ +import { SvgIconProps, SvgIcon } from '@mui/material' + +const svg = ( + + + + + +) + +export function OpenOceanIcon(props: SvgIconProps) { + return {svg} +} diff --git a/packages/public-api/src/web.ts b/packages/public-api/src/web.ts index fb9aaeb03303..1cd77bcc010b 100644 --- a/packages/public-api/src/web.ts +++ b/packages/public-api/src/web.ts @@ -210,6 +210,7 @@ export enum TradeProvider { DODO = 7, UNISWAP_V3 = 8, BANCOR = 9, + OPENOCEAN = 10, } /** Supported language settings */ export enum LanguageOptions { diff --git a/packages/web3-constants/evm/trader.json b/packages/web3-constants/evm/trader.json index f9b466a0d1dc..35d64c975900 100644 --- a/packages/web3-constants/evm/trader.json +++ b/packages/web3-constants/evm/trader.json @@ -446,5 +446,33 @@ "Arbitrum": "", "Arbitrum_Rinkeby": "", "xDai": "" + }, + "OPENOCEAN_ETH_ADDRESS": { + "Mainnet": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + "Ropsten": "", + "Rinkeby": "", + "Kovan": "", + "Gorli": "", + "BSC": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + "BSCT": "", + "Matic": "0x0000000000000000000000000000000000001010", + "Mumbai": "", + "Arbitrum": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + "Arbitrum_Rinkeby": "", + "xDai": "0x0000000000000000000000000000000000000000" + }, + "OPENOCEAN_EXCHANGE_PROXY_ADDRESS": { + "Mainnet": "0x6352a56caadC4F1E25CD6c75970Fa768A3304e64", + "Ropsten": "", + "Rinkeby": "", + "Kovan": "", + "Gorli": "", + "BSC": "0x6352a56caadC4F1E25CD6c75970Fa768A3304e64", + "BSCT": "", + "Matic": "0x6352a56caadc4f1e25cd6c75970fa768a3304e64", + "Mumbai": "", + "Arbitrum": "0x6352a56caadc4f1e25cd6c75970fa768a3304e64", + "Arbitrum_Rinkeby": "", + "xDai": "0x6352a56caadC4F1E25CD6c75970Fa768A3304e64" } } From 2110700923cf8791bf7925650773efeb454e9215 Mon Sep 17 00:00:00 2001 From: Hancheng Zhou Date: Fri, 31 Dec 2021 18:45:26 +0800 Subject: [PATCH 18/31] feat: nft lucky drop select all (#5243) * refactor: lazy query tokenURI * chore: rpc proxy * chore: rm lazy loading nft list * chore: shift select * chore: filter by token id input * fix: mask 524 * chore: select all * chore: select option * chore: max share * fix: select option * fix: option select loading * chore: add loading * chore: add loading * chore: text * chore: non-enumerable * fix: load panel * fix: non-enumerable disable button * chore: revert rpc config * chore: reply code review * chore: reply code review * chore: reply code review * chore: reply code review * chore: reply code review * chore: little fix --- packages/mask/shared-ui/locales/en-US.json | 8 + .../RedPacket/SNSAdaptor/RedPacketDialog.tsx | 2 +- .../SNSAdaptor/RedPacketERC721Form.tsx | 124 ++++- .../RedPacket/SNSAdaptor/RedPacketNft.tsx | 7 +- .../SNSAdaptor/SelectNftTokenDialog.tsx | 449 +++++++++++++++--- .../hooks/useClaimNftRedpacketCallback.ts | 15 +- .../mask/src/plugins/RedPacket/constants.ts | 1 + packages/mask/src/plugins/RedPacket/types.ts | 5 + .../hooks/useERC721TokenDetailedCallback.ts | 10 +- 9 files changed, 540 insertions(+), 81 deletions(-) diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index 022f2a482cf2..43c889da5e97 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -361,6 +361,14 @@ "plugin_red_packet_nft_account_name": "Wallet account", "plugin_red_packet_nft_attached_message": "Attached Message", "plugin_red_packet_nft_total_amount": "Total Amount", + "plugin_red_packet_nft_select_collection": "Choose your collection", + "plugin_red_packet_nft_max_shares": "The maximum number of NFTs to be sold in NFT lucky drop contract is {{amount}}.", + "plugin_red_packet_nft_max_shares_tip": "The collection lucky drop contract supports up to {{amount}} NFTs for sale.", + "plugin_red_packet_nft_shift_select_tip": "You can also use {{text}} to select multiple NFTs.", + "plugin_red_packet_nft_non_existed_tip": "Token ID does not exist or belong to you.", + "plugin_red_packet_nft_select_all_option": "ALL ({{total}} NFT)", + "plugin_red_packet_nft_select_partially_option": "Select partially", + "plugin_red_packet_nft_approve_all_tip": "Note: When selecting approve all, all NFTs in the contract will be authorized for sale by default, including the NFTs transfered later.", "plugin_red_packet_completed": "Completed", "plugin_red_packet_expired": "Expired", "plugin_red_packet_indivisible": "The minimum amount for each share is {{amount}} {{symbol}}", diff --git a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketDialog.tsx b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketDialog.tsx index 666d6716f4f4..4d68eb9e9884 100644 --- a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketDialog.tsx +++ b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketDialog.tsx @@ -231,7 +231,7 @@ export default function RedPacketDialog(props: RedPacketDialogProps) { const tokenState = useState(RpTypeTabs.ERC20) - const dialogContentHeight = state[0] === DialogTabs.past ? 600 : tokenState[0] === RpTypeTabs.ERC20 ? 350 : 540 + const dialogContentHeight = state[0] === DialogTabs.past ? 600 : tokenState[0] === RpTypeTabs.ERC20 ? 350 : 640 const tabProps: AbstractTabProps = { tabs: [ diff --git a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketERC721Form.tsx b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketERC721Form.tsx index c72a4213107c..841ccc93fa19 100644 --- a/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketERC721Form.tsx +++ b/packages/mask/src/plugins/RedPacket/SNSAdaptor/RedPacketERC721Form.tsx @@ -1,4 +1,4 @@ -import { Box, Typography, List, ListItem } from '@mui/material' +import { Box, Typography, List, ListItem, CircularProgress } from '@mui/material' import { makeStyles } from '@masknet/theme' import { useState, useCallback, useEffect, useMemo } from 'react' import { useI18N } from '../../../utils' @@ -15,12 +15,15 @@ import { useChainId, useNftRedPacketConstants, } from '@masknet/web3-shared-evm' +import CheckIcon from '@mui/icons-material/Check' import CloseIcon from '@mui/icons-material/Close' import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline' import { RedpacketMessagePanel } from './RedpacketMessagePanel' -import { SelectNftTokenDialog } from './SelectNftTokenDialog' +import { SelectNftTokenDialog, OrderedERC721Token } from './SelectNftTokenDialog' import { RedpacketNftConfirmDialog } from './RedpacketNftConfirmDialog' import { NftImage } from './NftImage' +import { NFTSelectOption } from '../types' +import { NFT_RED_PACKET_MAX_SHARES } from '../constants' const useStyles = makeStyles()((theme) => { return { @@ -135,6 +138,51 @@ const useStyles = makeStyles()((theme) => { width: 64, height: 64, }, + selectWrapper: { + display: 'flex', + alignItems: 'center', + margin: '16px 0 8px 0', + }, + option: { + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + }, + optionLeft: { + marginRight: '16px', + }, + checkIcon: { + width: 15, + height: 15, + color: '#fff', + }, + checkIconWrapper: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + cursor: 'pointer', + width: 17, + height: 17, + borderRadius: 999, + marginRight: 5, + border: '1px solid #6E748E', + backgroundColor: 'white', + }, + checked: { + borderColor: '#1D9BF0 !important', + background: '#1D9BF0 !important', + }, + approveAllTip: { + color: '#FF5F5F', + margin: '0px 4px 24px 4px', + }, + disabledSelector: { + opacity: 0.5, + pointerEvents: 'none', + }, + loadingOwnerList: { + margin: '24px auto 16px', + }, } }) interface RedPacketERC721FormProps { @@ -146,22 +194,25 @@ export function RedPacketERC721Form(props: RedPacketERC721FormProps) { const { classes } = useStyles() const [open, setOpen] = useState(false) const [balance, setBalance] = useState(0) + const [selectOption, setSelectOption] = useState(undefined) const [openConfirmDialog, setOpenConfirmDialog] = useState(false) const account = useAccount() const chainId = useChainId() const [contract, setContract] = useState() - const [existTokenDetailedList, setExistTokenDetailedList] = useState([]) + const [existTokenDetailedList, setExistTokenDetailedList] = useState([]) const [message, setMessage] = useState('Best Wishes!') const { asyncRetry: { loading: loadingOwnerList }, - tokenDetailedOwnerList = [], + tokenDetailedOwnerList: _tokenDetailedOwnerList = [], clearTokenDetailedOwnerList, } = useERC721TokenDetailedOwnerList(contract, account) - + const tokenDetailedOwnerList = _tokenDetailedOwnerList.map((v, index) => ({ ...v, index } as OrderedERC721Token)) const removeToken = useCallback((token: ERC721TokenDetailed) => { setExistTokenDetailedList((list) => list.filter((t) => t.tokenId !== token.tokenId)) }, []) + const maxSelectShares = Math.min(NFT_RED_PACKET_MAX_SHARES, tokenDetailedOwnerList.length) + const clearToken = useCallback(() => { setExistTokenDetailedList([]) clearTokenDetailedOwnerList() @@ -172,6 +223,14 @@ export function RedPacketERC721Form(props: RedPacketERC721FormProps) { setContract(undefined) }, []) + useEffect(() => { + if (loadingOwnerList) { + setSelectOption(undefined) + } else if (!selectOption) { + setSelectOption(NFTSelectOption.Partial) + } + }, [tokenDetailedOwnerList, selectOption, loadingOwnerList]) + useEffect(() => { clearToken() setOpen(false) @@ -199,6 +258,56 @@ export function RedPacketERC721Form(props: RedPacketERC721FormProps) { onBalanceChange={setBalance} /> {contract && balance ? ( + loadingOwnerList ? ( + + ) : ( + +
{ + setSelectOption(NFTSelectOption.All) + setExistTokenDetailedList(tokenDetailedOwnerList.slice(0, maxSelectShares)) + }}> +
+ +
+ + {tokenDetailedOwnerList.length === 0 + ? 'All' + : t('plugin_red_packet_nft_select_all_option', { + total: Math.min(NFT_RED_PACKET_MAX_SHARES, tokenDetailedOwnerList.length), + })} + +
+
{ + setSelectOption(NFTSelectOption.Partial) + setExistTokenDetailedList([]) + }}> +
+ +
+ + {t('plugin_red_packet_nft_select_partially_option')} + +
+
+ ) + ) : null} + {contract && balance && selectOption === NFTSelectOption.Partial && !loadingOwnerList ? (
{existTokenDetailedList.map((value, i) => ( @@ -233,6 +342,11 @@ export function RedPacketERC721Form(props: RedPacketERC721FormProps) {
setMessage(val)} message={message} />
+ {contract && balance && !loadingOwnerList ? ( + + {t('plugin_red_packet_nft_approve_all_tip')} + + ) : null} { + retryAvailability() + resetCallback() + }, [account]) const previewNftImg = new URL('./assets/nft-preview.png', import.meta.url).toString() const rpNftImg = new URL('./assets/redpacket.nft.png', import.meta.url).toString() diff --git a/packages/mask/src/plugins/RedPacket/SNSAdaptor/SelectNftTokenDialog.tsx b/packages/mask/src/plugins/RedPacket/SNSAdaptor/SelectNftTokenDialog.tsx index 593a1897e9b4..6fd555cf206e 100644 --- a/packages/mask/src/plugins/RedPacket/SNSAdaptor/SelectNftTokenDialog.tsx +++ b/packages/mask/src/plugins/RedPacket/SNSAdaptor/SelectNftTokenDialog.tsx @@ -7,21 +7,29 @@ import { useAccount, isSameAddress, } from '@masknet/web3-shared-evm' -import { useI18N } from '../../../utils' +import { useI18N, ShadowRootTooltip } from '../../../utils' import { DialogContent, Box, InputBase, Paper, Button, Typography, ListItem, CircularProgress } from '@mui/material' +import QuestionMarkIcon from '@mui/icons-material/QuestionMark' import { makeStyles } from '@masknet/theme' import { useCallback, useState, useEffect } from 'react' import { SearchIcon } from '@masknet/icons' import CheckIcon from '@mui/icons-material/Check' +import { Trans } from 'react-i18next' import { useUpdate } from 'react-use' import { NftImage } from './NftImage' +import { findLastIndex, uniqBy } from 'lodash-unified' +import { NFT_RED_PACKET_MAX_SHARES } from '../constants' -const useStyles = makeStyles()((theme) => ({ +interface StyleProps { + isSelectSharesExceed: boolean +} + +const useStyles = makeStyles()((theme, props) => ({ dialogContent: { minHeight: 380, }, dialogContentFixedHeight: { - height: 550, + height: 600, }, tokenBox: { background: theme.palette.mode === 'light' ? '#F7F9FA' : '#17191D', @@ -34,9 +42,9 @@ const useStyles = makeStyles()((theme) => ({ ownerTokenBox: { background: theme.palette.mode === 'light' ? '#F7F9FA' : '#17191D', width: '96%', - height: 450, + height: 480, borderRadius: 12, - margin: '16px auto', + margin: '14px auto', padding: 10, }, ownerList: { @@ -119,6 +127,8 @@ const useStyles = makeStyles()((theme) => ({ }, confirmButton: { width: '100%', + backgroundColor: theme.palette.mode === 'dark' ? '#fff' : '#000', + color: theme.palette.mode === 'dark' ? '#000' : '#fff', }, tokenSelector: { display: 'grid', @@ -141,8 +151,12 @@ const useStyles = makeStyles()((theme) => ({ flexDirection: 'column', borderRadius: 6, height: 180, + userSelect: 'none', width: 120, }, + hide: { + display: 'none !important', + }, loadingWrapper: { display: 'flex', justifyContent: 'center', @@ -178,7 +192,7 @@ const useStyles = makeStyles()((theme) => ({ backgroundColor: 'white', }, checked: { - borderColor: '#1C68F3', + borderColor: '#1C68F3 !important', }, checkIcon: { width: 15, @@ -192,20 +206,101 @@ const useStyles = makeStyles()((theme) => ({ width: 64, height: 64, }, + selectedTokenAmount: { + color: props.isSelectSharesExceed ? '#FF5F5F' : '#1C68F3', + }, + totalAmount: { + paddingLeft: 1, + }, + selectAmountBox: { + display: 'flex', + flexDirection: 'row-reverse', + alignItems: 'center', + }, + questionMarkIcon: { + padding: 2, + width: 12, + border: `1px solid ${theme.palette.mode === 'light' ? '#0F1419' : '#D9D9D9'}`, + borderRadius: 999, + transform: 'translateY(-1px)', + height: 12, + marginLeft: 5, + cursor: 'pointer', + }, + selectBar: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: 12, + padding: '0 8px', + }, + selectAll: { + display: 'flex', + alignItems: 'center', + fontSize: 14, + }, + selectAllCheckBox: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + cursor: 'pointer', + width: 17, + height: 17, + borderRadius: 6, + marginRight: 5, + border: '2px solid #6E748E', + backgroundColor: 'white', + }, + selectAllCheckBoxText: { + pointerEvents: 'none', + }, + selectSharesExceed: { + color: '#FF5F5F', + }, + selectSharesExceedBox: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + margin: '14px 4px', + }, + arrow: { + color: theme.palette.mode === 'dark' ? '#fff' : '#111418', + transform: 'translateX(260px) !important', + }, + tooltip: { + transform: 'translateX(20px) !important', + padding: '10px 20px', + width: 256, + backgroundColor: theme.palette.mode === 'dark' ? '#fff' : '#111418', + }, + tooltipText: { + color: theme.palette.mode === 'dark' ? '#000' : '#fff', + }, + tokenId: { + color: '#1D9BF0', + }, + nonExistedTokenBox: { + height: 388, + width: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, })) +export type OrderedERC721Token = ERC721TokenDetailed & { index: number } + export interface SelectNftTokenDialogProps extends withClasses { open: boolean loadingOwnerList: boolean onClose: () => void contract: ERC721ContractDetailed | undefined - existTokenDetailedList: ERC721TokenDetailed[] - tokenDetailedOwnerList: ERC721TokenDetailed[] - setExistTokenDetailedList: React.Dispatch> + existTokenDetailedList: OrderedERC721Token[] + tokenDetailedOwnerList: OrderedERC721Token[] + setExistTokenDetailedList: React.Dispatch> } export function SelectNftTokenDialog(props: SelectNftTokenDialogProps) { - const { classes } = useStyles() const { open, contract, @@ -217,12 +312,37 @@ export function SelectNftTokenDialog(props: SelectNftTokenDialogProps) { } = props const { t } = useI18N() const account = useAccount() - const [tokenDetailed, setTokenDetailed] = useState() + const [tokenDetailed, setTokenDetailed] = useState() const [searched, setSearched] = useState(false) const [tokenDetailedSelectedList, setTokenDetailedSelectedList] = - useState(existTokenDetailedList) + useState(existTokenDetailedList) const [loadingToken, setLoadingToken] = useState(false) const [tokenId, setTokenId, erc721TokenDetailedCallback] = useERC721TokenDetailedCallback(contract) + const [tokenIdListInput, setTokenIdListInput] = useState('') + const [tokenIdFilterList, setTokenIdFilterList] = useState([]) + const [nonExistedTokenIdList, setNonExistedTokenIdList] = useState([]) + const maxSelectShares = Math.min(NFT_RED_PACKET_MAX_SHARES, tokenDetailedOwnerList.length) + const isSelectSharesExceed = + (tokenDetailedOwnerList.length === 0 ? NFT_RED_PACKET_MAX_SHARES - 1 : NFT_RED_PACKET_MAX_SHARES) < + tokenDetailedSelectedList.length + const { classes } = useStyles({ isSelectSharesExceed }) + const selectAll = maxSelectShares === tokenDetailedSelectedList.length + const selectAllHandler = useCallback(() => { + setTokenDetailedSelectedList( + tokenIdFilterList.length === 0 + ? [] + : selectAll + ? tokenDetailedSelectedList.filter((t) => !tokenIdFilterList.includes(t.tokenId)) + : tokenDetailedSelectedList, + ) + + if (!selectAll) { + const newlyAdded = tokenDetailedOwnerList + .filter((t) => tokenIdFilterList.length === 0 || tokenIdFilterList.includes(t.tokenId)) + .slice(0, maxSelectShares) + setTokenDetailedSelectedList(uniqBy(newlyAdded.concat(tokenDetailedSelectedList), 'tokenId')) + } + }, [selectAll, maxSelectShares, tokenIdFilterList, tokenDetailedSelectedList]) useEffect(() => { setTokenDetailed(undefined) @@ -231,26 +351,61 @@ export function SelectNftTokenDialog(props: SelectNftTokenDialogProps) { setSearched(false) }, [contract]) - const update = useUpdate() useEffect(() => { - update() - }, [tokenDetailedOwnerList]) + setTokenIdFilterList([]) + setNonExistedTokenIdList([]) + }, [tokenIdListInput]) + + const update = useUpdate() + useEffect(update, [tokenDetailedOwnerList]) const selectToken = useCallback( - (token: ERC721TokenDetailed, findToken: ERC721TokenDetailed | undefined) => { - if (findToken) { - setTokenDetailedSelectedList(tokenDetailedSelectedList.filter((v) => v.tokenId !== findToken.tokenId)) + (token: OrderedERC721Token, findToken: OrderedERC721Token | undefined, shiftKey: boolean, index: number) => { + if (!shiftKey || tokenIdFilterList.length > 0) { + if (findToken) { + setTokenDetailedSelectedList( + tokenDetailedSelectedList.filter((v) => v.tokenId !== findToken.tokenId), + ) + } else { + setTokenDetailedSelectedList(tokenDetailedSelectedList.concat({ ...token, index })) + } } else { - setTokenDetailedSelectedList(tokenDetailedSelectedList.concat(token)) + const tokenDetailedSelectedListSorted = tokenDetailedSelectedList.sort( + (a, b) => (b.index ?? 0) - (a.index ?? 0), + ) + if (findToken) { + const unselectedTokenIdList: string[] = [] + let nextToken: OrderedERC721Token | undefined = findToken + while (nextToken) { + unselectedTokenIdList.push(nextToken.tokenId) + const nextTokenIndex: number = (nextToken?.index ?? 0) + 1 + nextToken = tokenDetailedSelectedListSorted.find((v) => (v.index ?? 0) === nextTokenIndex) + } + setTokenDetailedSelectedList( + tokenDetailedSelectedList.filter((v) => !unselectedTokenIdList.includes(v.tokenId)), + ) + } else { + const lastSelectedToken = tokenDetailedSelectedListSorted.filter((v) => (v?.index ?? 0) < index)[0] + + const lastSelectedTokenIndex = findLastIndex( + tokenDetailedOwnerList, + (v, i) => v.tokenId === lastSelectedToken?.tokenId && i < index, + ) + setTokenDetailedSelectedList( + tokenDetailedSelectedList.concat( + tokenDetailedOwnerList.slice(lastSelectedTokenIndex + 1, index + 1), + ), + ) + } } }, - [tokenDetailedSelectedList, setTokenDetailedSelectedList], + [tokenDetailedSelectedList, setTokenDetailedSelectedList, tokenIdFilterList], ) //#region fetch token detail const onSearch = useCallback(async () => { setLoadingToken(true) const _tokenDetailed = await erc721TokenDetailedCallback() - setTokenDetailed(_tokenDetailed?.info.owner ? _tokenDetailed : undefined) + setTokenDetailed(_tokenDetailed?.info.owner ? { ..._tokenDetailed, index: 0 } : undefined) setSearched(true) setLoadingToken(false) }, [erc721TokenDetailedCallback]) @@ -260,10 +415,21 @@ export function SelectNftTokenDialog(props: SelectNftTokenDialogProps) { setSearched(false) }, [tokenId]) + useEffect(() => { + if (tokenDetailedOwnerList.length > 0) setTokenDetailed(undefined) + }, [tokenDetailedOwnerList.length]) + const isOwner = isSameAddress(account, tokenDetailed?.info.owner) || tokenDetailedSelectedList.length > 0 const isAdded = existTokenDetailedList.map((t) => t.tokenId).includes(tokenDetailed?.tokenId ?? '') //#endregion + const onFilter = useCallback(() => { + if (!/^(\s?(\d+)?\s?,?)+$/.test(tokenIdListInput)) return + const list = tokenIdListInput.split(',').map((v) => Number(v).toString()) + setTokenIdFilterList(list) + setNonExistedTokenIdList(list.filter((v) => !tokenDetailedOwnerList.map((t) => t.tokenId).includes(v))) + }, [tokenIdListInput]) + const onSubmit = useCallback(() => { setExistTokenDetailedList( tokenDetailed ? [tokenDetailed, ...existTokenDetailedList] : tokenDetailedSelectedList, @@ -271,8 +437,30 @@ export function SelectNftTokenDialog(props: SelectNftTokenDialogProps) { onClose() }, [tokenDetailed, tokenDetailedSelectedList, setExistTokenDetailedList, onClose]) + const NonExistedTokenList = () => ( + + {nonExistedTokenIdList.map((id, i) => ( + + {' #' + id} + {i < nonExistedTokenIdList.length - 1 ? ', ' : ' '} + + ))} + + ), + }} + /> + ) + return ( - + {tokenDetailedOwnerList.length === 0 ? ( @@ -306,17 +494,49 @@ export function SelectNftTokenDialog(props: SelectNftTokenDialogProps) { ) : ( - -
- - {tokenDetailed.info.name} + +
+ + {tokenDetailed?.info.name}
)} +
+ + {isSelectSharesExceed + ? t('plugin_red_packet_nft_max_shares_tip', { amount: NFT_RED_PACKET_MAX_SHARES }) + : null} + + + + {t('plugin_red_packet_nft_max_shares', { + amount: NFT_RED_PACKET_MAX_SHARES, + })} + + } + placement="top-end" + classes={{ tooltip: classes.tooltip, arrow: classes.arrow }} + arrow> + + + + {tokenDetailedSelectedList.length}{' '} + NFTs + + +
@@ -364,53 +588,144 @@ export function SelectNftTokenDialog(props: SelectNftTokenDialogProps) {
) : ( -
- {tokenDetailedOwnerList.map((token, i) => { - const findToken = tokenDetailedSelectedList.find((t) => t.tokenId === token.tokenId) - - return ( - - -
- - {token?.info.name} - -
+ <> + {tokenIdFilterList.length === 0 ? ( +
+
selectToken(token, findToken)}> - {findToken ? : null} + onClick={selectAllHandler}> + {selectAll ? : null}
- - ) - })} - {loadingOwnerList ? ( - - - + + {t('select_all')} + +
+ + , + }} + values={{ + text: 'Shift', + }} + /> + +
) : null} -
+ {nonExistedTokenIdList.length > 0 && + nonExistedTokenIdList.length === tokenIdFilterList.length ? ( +
+ + + +
+ ) : ( +
+ {tokenDetailedOwnerList.map((token, i) => { + const findToken = tokenDetailedSelectedList.find( + (t) => t.tokenId === token.tokenId, + ) + + return ( + 0 && + !tokenIdFilterList.includes(token.tokenId) + ? classes.hide + : '', + )} + key={i.toString()}> + +
+ + {token?.info.name} + +
+ +
+ selectToken(token, findToken, event.shiftKey, token.index) + }> + {findToken ? : null} +
+
+ ) + })} + {loadingOwnerList ? ( + + + + ) : null} +
+ )} + )} +
+
+ + {nonExistedTokenIdList.length > 0 && + nonExistedTokenIdList.length !== tokenIdFilterList.length ? ( + + ) : null} + + + {isSelectSharesExceed + ? t('plugin_red_packet_nft_max_shares_tip', { amount: NFT_RED_PACKET_MAX_SHARES }) + : null} + +
+ + + + {t('plugin_red_packet_nft_max_shares', { + amount: NFT_RED_PACKET_MAX_SHARES, + })} + + } + placement="top-end" + classes={{ tooltip: classes.tooltip, arrow: classes.arrow }} + arrow> + + + + + {tokenDetailedSelectedList.length + ' '} + + /{tokenDetailedOwnerList.length} + + +