diff --git a/cspell.json b/cspell.json index 3b35105fa48d..566f31e16a1b 100644 --- a/cspell.json +++ b/cspell.json @@ -243,7 +243,11 @@ "solana", "Görli", "resizer", - "Pageable" + "Pageable", + "metaverse", + "hexlify", + "contribs", + "contrib" ], "ignoreWords": [ "cryptopunks", @@ -346,7 +350,9 @@ "AICANFT", "CANFT", "Kodav", - "koda" + "koda", + "poap", + "irss" ], "ignoreRegExpList": ["/@servie/"], "overrides": [ diff --git a/package.json b/package.json index a988409f237d..9e8b39828041 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "preinstall": "npx only-allow pnpm", "postinstall": "patch-package", "prepare": "husky install", - "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js" + "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", + "spellcheck": "cspell lint --no-must-find-files" }, "lint-staged": { "*": "prettier --write --ignore-unknown", diff --git a/packages/mask/package.json b/packages/mask/package.json index dff57ae3c59b..869f88efcfea 100644 --- a/packages/mask/package.json +++ b/packages/mask/package.json @@ -54,6 +54,7 @@ "@uniswap/sdk-core": "^3.0.1", "@uniswap/v2-sdk": "3.0.0-alpha.2", "@uniswap/v3-sdk": "^3.3.2", + "@unstoppabledomains/resolution": "^5.0.1", "@walletconnect/client": "^1.2.2", "anchorme": "^2.1.2", "arweave": "1.10.13", diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index 1829ed2630cc..c143054eed52 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -18,7 +18,6 @@ "sign": "Sign", "reload": "Reload", "load": "Load", - "donation": "Donation", "load_all": "Load All", "supported_by": "Supported by", "no_data": "No Data", @@ -67,6 +66,7 @@ "unknown": "Unknown", "message": "Message", "dashboard_tab_collectibles": "Collectibles", + "dashboard_no_collection_found": "No collection found", "dashboard_no_collectible_found": "No collectible found", "days": "Every {{days}} days", "decrypted_postbox_add_recipients": "Append recipients", @@ -1010,5 +1010,15 @@ "plugin_profile_binding_rule3": "Twitter ID + {{suffix}} is ENS.", "plugin_profile_binding_rule4": "Address used to sign the avatar.", "plugin_profile_current_display_of": "Current display of {{type}}: ", + "plugin_profile_loading": "Loading...", + "plugin_profile_error_no_address": "Failed to find any valid address.", + "plugin_profile_address_name_ens": "ENS", + "plugin_profile_address_name_uns": "UNS", + "plugin_profile_address_name_dns": "DNS", + "plugin_profile_address_name_rns": "RNS", + "plugin_profile_address_name_address": "address", + "plugin_profile_rss3_inactive_project": "Inactive Project", + "plugin_profile_rss3_no_activity_time": "No activity time", + "plugin_profile_rss3_attended": "Attended", "plugin_vcent_last_offer_at": "LATEST OFFER at" } diff --git a/packages/mask/shared-ui/locales/qya-AA.json b/packages/mask/shared-ui/locales/qya-AA.json index d567a580cdb2..5a16b3fd3625 100644 --- a/packages/mask/shared-ui/locales/qya-AA.json +++ b/packages/mask/shared-ui/locales/qya-AA.json @@ -18,7 +18,6 @@ "sign": "crwdns10085:0crwdne10085:0", "reload": "crwdns10087:0crwdne10087:0", "load": "crwdns10089:0crwdne10089:0", - "donation": "crwdns10091:0crwdne10091:0", "load_all": "crwdns10093:0crwdne10093:0", "supported_by": "crwdns10095:0crwdne10095:0", "no_data": "crwdns10097:0crwdne10097:0", diff --git a/packages/mask/shared-ui/locales/zh-CN.json b/packages/mask/shared-ui/locales/zh-CN.json index c183b9a9141c..cb5d41212706 100644 --- a/packages/mask/shared-ui/locales/zh-CN.json +++ b/packages/mask/shared-ui/locales/zh-CN.json @@ -18,7 +18,6 @@ "sign": "签名", "reload": "重新加载", "load": "加载", - "donation": "捐赠", "load_all": "加载全部", "supported_by": "技术支持:", "no_data": "暂无数据", @@ -888,10 +887,10 @@ "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_rule4": "用于签署NFT 头像的地址。", - "plugin_profile_current_display_of": "当前显示 {{type}}: ", - "plugin_vcent_last_offer_at": "最新的OFFER为" + "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_rule4": "用于签署 NFT 头像的地址。", + "plugin_profile_current_display_of": "当前显示 {{type}}:", + "plugin_vcent_last_offer_at": "最新的 Offer 为" } diff --git a/packages/mask/src/database/Persona/types.ts b/packages/mask/src/database/Persona/types.ts index 94011842b08e..216b16cd2edd 100644 --- a/packages/mask/src/database/Persona/types.ts +++ b/packages/mask/src/database/Persona/types.ts @@ -6,6 +6,7 @@ export interface Profile extends Readonly { diff --git a/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/CollectibleCard.tsx b/packages/mask/src/extension/options-page/DashboardComponents/CollectionList/CollectibleCard.tsx similarity index 100% rename from packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/CollectibleCard.tsx rename to packages/mask/src/extension/options-page/DashboardComponents/CollectionList/CollectibleCard.tsx diff --git a/packages/mask/src/extension/options-page/DashboardComponents/CollectionList/CollectibleItem.tsx b/packages/mask/src/extension/options-page/DashboardComponents/CollectionList/CollectibleItem.tsx new file mode 100644 index 000000000000..2523db3dbd9e --- /dev/null +++ b/packages/mask/src/extension/options-page/DashboardComponents/CollectionList/CollectibleItem.tsx @@ -0,0 +1,47 @@ +import type { NonFungibleAssetProvider, ERC721TokenDetailed, Wallet } from '@masknet/web3-shared-evm' +import { Typography } from '@mui/material' +import { makeStyles } from '@masknet/theme' +import { CollectibleCard } from './CollectibleCard' + +const useStyles = makeStyles()((theme) => ({ + card: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + position: 'relative', + padding: theme.spacing(1), + }, + description: { + background: theme.palette.mode === 'light' ? '#F7F9FA' : '#17191D', + alignSelf: 'stretch', + }, + name: { + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden', + lineHeight: '36px', + paddingLeft: '8px', + }, +})) + +export interface CollectibleItemProps { + provider: NonFungibleAssetProvider + wallet?: Wallet + token: ERC721TokenDetailed + readonly?: boolean +} + +export function CollectibleItem(props: CollectibleItemProps) { + const { provider, wallet, token, readonly } = props + const { classes } = useStyles() + return ( + + + + + {token.info.name} + + + + ) +} diff --git a/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/index.tsx b/packages/mask/src/extension/options-page/DashboardComponents/CollectionList/CollectibleList.tsx similarity index 57% rename from packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/index.tsx rename to packages/mask/src/extension/options-page/DashboardComponents/CollectionList/CollectibleList.tsx index 3c5801889ba4..1e581d44c960 100644 --- a/packages/mask/src/extension/options-page/DashboardComponents/CollectibleList/index.tsx +++ b/packages/mask/src/extension/options-page/DashboardComponents/CollectionList/CollectibleList.tsx @@ -4,19 +4,16 @@ import { useValueRef } from '@masknet/shared' import { ChainId, NonFungibleAssetProvider, - ERC721TokenCollectionInfo, ERC721TokenDetailed, useCollectibles, - useCollections, Wallet, } from '@masknet/web3-shared-evm' import { Box, Button, Skeleton, Typography } from '@mui/material' import { makeStyles, useStylesExtends } from '@masknet/theme' import { currentNonFungibleAssetDataProviderSettings } from '../../../../plugins/Wallet/settings' import { useI18N } from '../../../../utils' -import { CollectibleCard } from './CollectibleCard' +import { CollectibleItem } from './CollectibleItem' import { WalletMessages } from '../../../../plugins/Wallet/messages' -import { Image } from '../../../../components/shared/Image' export const CollectibleContext = createContext<{ collectiblesRetry: () => void @@ -50,17 +47,6 @@ const useStyles = makeStyles()((theme) => ({ position: 'relative', padding: theme.spacing(1), }, - description: { - background: theme.palette.mode === 'light' ? '#F7F9FA' : '#17191D', - alignSelf: 'stretch', - }, - name: { - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - lineHeight: '36px', - paddingLeft: '8px', - }, loading: { position: 'absolute', bottom: 6, @@ -82,29 +68,6 @@ const useStyles = makeStyles()((theme) => ({ borderRadius: '50%', }, })) - -interface CollectibleItemProps { - provider: NonFungibleAssetProvider - wallet?: Wallet - token: ERC721TokenDetailed - readonly?: boolean -} - -function CollectibleItem(props: CollectibleItemProps) { - const { provider, wallet, token, readonly } = props - const { classes } = useStyles() - return ( - - - - - {token.info.name} - - - - ) -} - interface CollectibleListUIProps extends withClasses<'empty' | 'button' | 'text'> { provider: NonFungibleAssetProvider wallet?: Wallet @@ -115,12 +78,14 @@ interface CollectibleListUIProps extends withClasses<'empty' | 'button' | 'text' readonly?: boolean hasRetry?: boolean } + function CollectibleListUI(props: CollectibleListUIProps) { const { provider, wallet, collectibles, loading, collectiblesRetry, error, readonly, hasRetry = true } = props const classes = useStylesExtends(useStyles(), props) const { t } = useI18N() - WalletMessages.events.erc721TokensUpdated.on(collectiblesRetry) + useEffect(() => WalletMessages.events.erc721TokensUpdated.on(collectiblesRetry)) + if (loading) return ( @@ -157,11 +122,11 @@ function CollectibleListUI(props: CollectibleListUIProps) { {collectibles.map((x, i) => ( ))} @@ -171,16 +136,16 @@ function CollectibleListUI(props: CollectibleListUIProps) { ) } -export interface CollectibleListAddressProps extends withClasses<'empty' | 'button'> { +export interface CollectibleListProps extends withClasses<'empty' | 'button'> { + chainId: ChainId address: string collection?: string setCount: (count: number) => void } -export function CollectibleListAddress(props: CollectibleListAddressProps) { - const { address, collection, setCount } = props +export function CollectibleList(props: CollectibleListProps) { + const { chainId, address, collection, setCount } = props const provider = useValueRef(currentNonFungibleAssetDataProviderSettings) - const chainId = ChainId.Mainnet const [page, setPage] = useState(0) const classes = props.classes ?? {} @@ -189,7 +154,7 @@ export function CollectibleListAddress(props: CollectibleListAddressProps) { loading: collectiblesLoading, retry: collectiblesRetry, error: collectiblesError, - } = useCollectibles(address, chainId, provider, page, 50, collection) + } = useCollectibles(chainId, address, provider, page, 50, collection) const { collectibles = [], hasNextPage } = value const [rendCollectibles, setRendCollectibles] = useState([]) @@ -198,20 +163,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(() => { @@ -231,66 +191,3 @@ export function CollectibleListAddress(props: CollectibleListAddressProps) { /> ) } - -export function CollectionList({ address }: { address: string }) { - const provider = useValueRef(currentNonFungibleAssetDataProviderSettings) - const chainId = ChainId.Mainnet - const [page, setPage] = useState(0) - const { classes } = useStyles() - const [counts, setCounts] = useState([]) - const [rendCollections, setRendCollections] = useState([]) - - const { value = { collections: [], hasNextPage: false } } = useCollections(address, chainId, provider, page, 3) - const { collections = [], hasNextPage } = value - - useUpdateEffect(() => { - setPage(0) - }, [provider, address]) - - useEffect(() => { - if (collections.length) { - setRendCollections([...rendCollections, ...collections]) - if (hasNextPage) { - const timer = setTimeout(() => { - setPage(page + 1) - }, 3000) - - return () => { - clearTimeout(timer) - } - } - } - - return () => {} - }, [collections]) - - return ( - - {rendCollections.map((x, i) => ( - - - - {x.image ? : null} - - - {x.name} - {counts[i] ? `(${counts[i]})` : null} - - - { - counts[i] = count - setCounts(counts) - }} - /> - - ))} - - ) -} diff --git a/packages/mask/src/extension/options-page/DashboardComponents/CollectionList/index.tsx b/packages/mask/src/extension/options-page/DashboardComponents/CollectionList/index.tsx new file mode 100644 index 000000000000..5ed054b62d86 --- /dev/null +++ b/packages/mask/src/extension/options-page/DashboardComponents/CollectionList/index.tsx @@ -0,0 +1,151 @@ +import { createContext, useState, useEffect } from 'react' +import { useUpdateEffect } from 'react-use' +import { Box, Typography } from '@mui/material' +import { makeStyles } from '@masknet/theme' +import { ChainId, ERC721TokenCollectionInfo, NonFungibleAssetProvider, useCollections } from '@masknet/web3-shared-evm' +import { Image } from '../../../../components/shared/Image' +import { CollectibleList } from './CollectibleList' +import { useI18N } from '../../../../utils' + +export const CollectibleContext = createContext<{ + collectiblesRetry: () => void +}>(null!) + +const useStyles = makeStyles()((theme) => ({ + root: { + display: 'grid', + flexWrap: 'wrap', + gridTemplateColumns: 'repeat(auto-fill, minmax(172px, 1fr))', + gridGap: theme.spacing(1), + }, + text: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + }, + button: { + marginTop: theme.spacing(1), + }, + container: { + height: 'calc(100% - 52px)', + overflow: 'auto', + }, + card: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + position: 'relative', + padding: theme.spacing(1), + }, + description: { + background: theme.palette.mode === 'light' ? '#F7F9FA' : '#17191D', + alignSelf: 'stretch', + }, + name: { + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden', + lineHeight: '36px', + paddingLeft: '8px', + }, + loading: { + position: 'absolute', + bottom: 6, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + width: '100%', + }, + collectionWrap: { + width: '24px', + height: '24px', + borderRadius: '50%', + background: 'rgba(229,232,235,1)', + }, + collectionImg: { + objectFit: 'cover', + width: '100%', + height: '100%', + borderRadius: '50%', + }, +})) + +export interface CollectionListProps { + chainId: ChainId + address: string +} + +export function CollectionList({ address, chainId }: CollectionListProps) { + const { t } = useI18N() + const { classes } = useStyles() + const [page, setPage] = useState(0) + const [counts, setCounts] = useState([]) + const [rendCollections, setRendCollections] = useState([]) + + const { value = { collections: [], hasNextPage: false } } = useCollections( + chainId, + address, + NonFungibleAssetProvider.OPENSEA, + page, + 3, + ) + const { collections = [], hasNextPage } = value + + useUpdateEffect(() => { + setPage(0) + setRendCollections([]) + }, [address, chainId]) + + useEffect(() => { + if (!collections.length) return + setRendCollections([...rendCollections, ...collections]) + if (!hasNextPage) return + const timer = setTimeout(() => { + setPage(page + 1) + }, 3000) + return () => { + clearTimeout(timer) + } + }, [collections]) + + if (!collections.length) { + return ( + + {t('dashboard_no_collection_found')} + + ) + } + + return ( + + {rendCollections.map((x, i) => ( + + + + {x.image ? : null} + + + {x.name} + {counts[i] ? `(${counts[i]})` : null} + + + { + counts[i] = count + setCounts(counts) + }} + /> + + ))} + + ) +} diff --git a/packages/mask/src/extension/options-page/DashboardDialogs/Wallet/TransferNFT.tsx b/packages/mask/src/extension/options-page/DashboardDialogs/Wallet/TransferNFT.tsx index 36c3690b5672..4d79d03418d2 100644 --- a/packages/mask/src/extension/options-page/DashboardDialogs/Wallet/TransferNFT.tsx +++ b/packages/mask/src/extension/options-page/DashboardDialogs/Wallet/TransferNFT.tsx @@ -13,7 +13,7 @@ import { useRemoteControlledDialog } from '@masknet/shared' import { Image } from '../../../../components/shared/Image' import { WalletMessages } from '../../../../plugins/Wallet/messages' import { MaskIconOutlined } from '../../../../resources/MaskIcon' -import { CollectibleContext } from '../../DashboardComponents/CollectibleList' +import { CollectibleContext } from '../../DashboardComponents/CollectionList' // eslint-disable-next-line import/no-deprecated import { DashboardDialogCore, DashboardDialogWrapper, WrappedDialogProps } from '../Base' diff --git a/packages/mask/src/manifest.json b/packages/mask/src/manifest.json index d46e6a243549..f9fc4ff9aef6 100644 --- a/packages/mask/src/manifest.json +++ b/packages/mask/src/manifest.json @@ -10,6 +10,6 @@ "browser_action": { "default_popup": "popups.html" }, "homepage_url": "https://mask.io", "description": "The portal to the new & open Internet. Send encrypted message and decentralized Apps right on top of social networks.", - "web_accessible_resources": ["js/*", "*.png"], + "web_accessible_resources": ["js/*", "*.png", "*.css"], "content_security_policy": "script-src 'self' 'wasm-eval'; object-src 'self';" } diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatar.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatar.tsx index 20aef9b42ba6..db3ebf0f0897 100644 --- a/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatar.tsx +++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/NFTAvatar.tsx @@ -105,7 +105,7 @@ export function NFTAvatar(props: NFTAvatarProps) { loading, retry, error, - } = useCollectibles(account, ChainId.Mainnet, provider, page, 50) + } = useCollectibles(ChainId.Mainnet, account, provider, page, 50) const { collectibles, hasNextPage } = value const onClick = useCallback(async () => { diff --git a/packages/mask/src/plugins/Avatar/Services/index.ts b/packages/mask/src/plugins/Avatar/Services/index.ts index d5528e006f8f..de7511f874c1 100644 --- a/packages/mask/src/plugins/Avatar/Services/index.ts +++ b/packages/mask/src/plugins/Avatar/Services/index.ts @@ -28,7 +28,7 @@ export async function saveNFTAvatar(address: string, nft: AvatarMetaDB) { export async function getAddress(userId: string) { const address = await getUserAddress(userId) - return address + return (address ?? '') as string } export { getNFTContractVerifiedFromJSON } from './verified' diff --git a/packages/mask/src/plugins/Avatar/hooks/useUserOwnerAddress.ts b/packages/mask/src/plugins/Avatar/hooks/useUserOwnerAddress.ts index 9ced59fe96b5..6d0df27138a4 100644 --- a/packages/mask/src/plugins/Avatar/hooks/useUserOwnerAddress.ts +++ b/packages/mask/src/plugins/Avatar/hooks/useUserOwnerAddress.ts @@ -3,15 +3,13 @@ import type { AsyncState } from 'react-use/lib/useAsyncFn' import { PluginNFTAvatarRPC } from '../messages' const cache = new Map>() + export function useUserOwnerAddress(userId: string): AsyncState { return useAsync(async () => { if (!userId) return - let f = cache.get(userId) - if (!f) { - f = PluginNFTAvatarRPC.getAddress(userId) - cache.set(userId, f) + if (!cache.has(userId)) { + cache.set(userId, PluginNFTAvatarRPC.getAddress(userId)) } - const address = await f - return address - }, [userId, cache]) + return cache.get(userId) + }, [userId]) } diff --git a/packages/mask/src/plugins/EVM/UI/Web3State/getAssetsFn.ts b/packages/mask/src/plugins/EVM/UI/Web3State/getAssetsFn.ts index fd6a87929f46..a3c11f21e11f 100644 --- a/packages/mask/src/plugins/EVM/UI/Web3State/getAssetsFn.ts +++ b/packages/mask/src/plugins/EVM/UI/Web3State/getAssetsFn.ts @@ -139,8 +139,8 @@ export const getNonFungibleTokenFn = } const tokenFromProvider = await context.getAssetsListNFT( - address.toLowerCase(), network?.chainId ?? ChainId.Mainnet, + address.toLowerCase(), NonFungibleAssetProvider.OPENSEA, pagination?.page ?? 0, pagination?.size ?? 20, diff --git a/packages/mask/src/plugins/MaskBox/SNSAdaptor/components/TokenCard.tsx b/packages/mask/src/plugins/MaskBox/SNSAdaptor/components/TokenCard.tsx index 74a501d5f1b1..627ae800a029 100644 --- a/packages/mask/src/plugins/MaskBox/SNSAdaptor/components/TokenCard.tsx +++ b/packages/mask/src/plugins/MaskBox/SNSAdaptor/components/TokenCard.tsx @@ -1,8 +1,8 @@ +import { memo } from 'react' import { makeStyles } from '@masknet/theme' -import { ERC721ContractDetailed, NonFungibleAssetProvider, useERC721TokenDetailed } from '@masknet/web3-shared-evm' import { Typography } from '@mui/material' -import { memo } from 'react' -import { CollectibleCard } from '../../../../extension/options-page/DashboardComponents/CollectibleList/CollectibleCard' +import { ERC721ContractDetailed, NonFungibleAssetProvider, useERC721TokenDetailed } from '@masknet/web3-shared-evm' +import { CollectibleCard } from '../../../../extension/options-page/DashboardComponents/CollectionList/CollectibleCard' const useStyles = makeStyles()((theme) => ({ title: { diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/DAOPage.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/DAOPage.tsx index 6647a61e0e22..ffc81be94d1a 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/DAOPage.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/DAOPage.tsx @@ -1,8 +1,8 @@ +import { useState } from 'react' import IframeResizer from 'iframe-resizer-react' import { useTheme } from '@mui/material' -import type { Dao_Payload } from './hooks/useDao' -import { useState } from 'react' import type { ProfileIdentifier } from '@masknet/shared-base' +import type { Dao_Payload } from './hooks' interface DAOPageProps { payload: Dao_Payload | undefined diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/DonationsPage.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/DonationsPage.tsx index 02f1553eff8d..2393ada10c2e 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/DonationsPage.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/DonationsPage.tsx @@ -1,11 +1,68 @@ -import { Typography } from '@mui/material' +import { makeStyles } from '@masknet/theme' +import { CircularProgress, Link } from '@mui/material' +import urlcat from 'urlcat' +import type { GeneralAssetWithTags } from '../types' +import { RSS3_DEFAULT_IMAGE } from '../constants' +import { DonationCard } from './components' +import { useDonations } from './hooks' import { useI18N } from '../../../utils' -interface DonationPageProps {} + +const getDonationLink = (address: string, donation: GeneralAssetWithTags) => { + const { platform, identity, id, type } = donation + return urlcat('https://rss3.bio/:address/singlegitcoin/:platform/:identity/:id/:type', { + address, + platform, + identity, + id, + type, + }) +} + +const useStyles = makeStyles()((theme) => ({ + address: { + color: theme.palette.primary.main, + }, + link: { + '&:hover': { + textDecoration: 'none', + }, + }, +})) + +export interface DonationPageProps { + address: string +} + export function DonationPage(props: DonationPageProps) { + const { address } = props + const { classes } = useStyles() + const { donations, loading } = useDonations(address) const { t } = useI18N() + + if (loading) { + return ( + + + + ) + } return ( - - {t('donation')} - + + {donations.map((donation) => ( + + + + ))} + ) } diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfile.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfile.tsx index 9e96f2171a51..1bed7d12b73f 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfile.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfile.tsx @@ -1,18 +1,23 @@ import { useEffect, useMemo, useState } from 'react' -import { makeStyles, useStylesExtends } from '@masknet/theme' - -import { MaskMessages } from '../../../utils' +import { useUpdateEffect } from 'react-use' +import { Box, Typography } from '@mui/material' +import { unreachable } from '@dimensiondev/kit' +import { makeStyles } from '@masknet/theme' +import { AddressNameType, ChainId, useAddressNames } from '@masknet/web3-shared-evm' +import { MaskMessages, useI18N } from '../../../utils' import { useLocationChange } from '../../../utils/hooks/useLocationChange' -import { useCurrentVisitingIdentity } from '../../../components/DataSource/useActivatedUI' import { WalletsPage } from './WalletsPage' import { NFTPage } from './NFTPage' import { DonationPage } from './DonationsPage' +import { FootprintPage } from './FootprintPage' import { DAOPage } from './DAOPage' import { PageTags } from '../types' -import { unreachable } from '@dimensiondev/kit' import { PageTag } from './PageTag' -import { useDao } from './hooks/useDao' -import { useUpdateEffect } from 'react-use' +import { AddressViewer } from './components/AddressViewer' +import { NetworkSelector } from './components/NetworkSelector' +import { useCurrentVisitingIdentity } from '../../../components/DataSource/useActivatedUI' +import { useDao } from './hooks' +import type { Web3Plugin } from '@masknet/plugin-infra' const useStyles = makeStyles()((theme) => ({ root: { @@ -25,55 +30,132 @@ const useStyles = makeStyles()((theme) => ({ tags: { padding: theme.spacing(2), }, + metadata: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: theme.spacing(1), + }, content: { position: 'relative', padding: theme.spacing(1), }, })) -interface EnhancedProfilePageProps extends withClasses<'text' | 'button'> {} +export interface EnhancedProfilePageProps {} export function EnhancedProfilePage(props: EnhancedProfilePageProps) { - const [show, setShow] = useState(false) - const classes = useStylesExtends(useStyles(), props) + const { t } = useI18N() + const { classes } = useStyles() + + //#region identity const identity = useCurrentVisitingIdentity() + const { value: addressNames, loading: loadingAddressNames } = useAddressNames(identity) + //#endregion + + const [hidden, setHidden] = useState(true) const { value: daoPayload } = useDao(identity.identifier) const [currentTag, setCurrentTag] = useState(PageTags.NFTTag) + const [selectedNetwork, setSelectedNetwork] = useState(null) + useLocationChange(() => { + setCurrentTag(PageTags.NFTTag) + setSelectedNetwork(null) MaskMessages.events.profileNFTsTabUpdated.sendToLocal('reset') }) useEffect(() => { return MaskMessages.events.profileNFTsPageUpdated.on((data) => { - setShow(data.show) + setHidden(!data.show) }) - }, []) + }, [identity]) + + useUpdateEffect(() => { + setCurrentTag(PageTags.NFTTag) + }, [identity.identifier]) + + const addressName = useMemo(() => { + 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) + } + }, [currentTag, addressNames]) + + const address = addressName?.resolvedAddress ?? '' const content = useMemo(() => { switch (currentTag) { case PageTags.WalletTag: return case PageTags.NFTTag: - return + return case PageTags.DonationTag: - return + return + case PageTags.FootprintTag: + return case PageTags.DAOTag: return default: unreachable(currentTag) } - }, [currentTag, daoPayload, identity.identifier]) + }, [address, currentTag, daoPayload, identity.identifier, selectedNetwork]) - useUpdateEffect(() => { - setCurrentTag(PageTags.NFTTag) - }, [identity.identifier]) + if (hidden) return null - if (!show) return null + if (loadingAddressNames) + return ( + + + {t('plugin_profile_loading')} + + + ) + + if (!addressName) + return ( + + + {t('plugin_profile_error_no_address')} + + + ) return ( + - + + + + {currentTag === PageTags.NFTTag ? : } + {content} diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfileTab.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfileTab.tsx index b583f310388f..c366dca0c389 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfileTab.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/EnhancedProfileTab.tsx @@ -1,6 +1,6 @@ -import { Typography } from '@mui/material' -import classnames from 'classnames' import { ReactElement, useCallback, useEffect, useState } from 'react' +import classnames from 'classnames' +import { Typography } from '@mui/material' import { MaskMessages } from '../../../utils' export interface EnhancedProfileTabProps extends withClasses<'tab' | 'button' | 'selected'> { diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/FootprintPage.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/FootprintPage.tsx new file mode 100644 index 000000000000..21cf9d77e585 --- /dev/null +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/FootprintPage.tsx @@ -0,0 +1,73 @@ +import urlcat from 'urlcat' +import { makeStyles } from '@masknet/theme' +import { CircularProgress, Link } from '@mui/material' +import type { GeneralAssetWithTags } from '../types' +import { RSS3_DEFAULT_IMAGE } from '../constants' +import { FootprintCard } from './components' +import { useFootprints, useRss3Profile } from './hooks' + +const useStyles = makeStyles()((theme) => ({ + address: { + color: theme.palette.primary.main, + }, + link: { + '&:hover': { + textDecoration: 'none', + }, + }, +})) + +const getFootprintLink = (address: string, footprint: GeneralAssetWithTags) => { + const { platform, identity, id, type } = footprint + return urlcat('https://rss3.bio/:address/singlefootprint/:platform/:identity/:id/:type', { + address, + platform, + identity, + id, + type, + }) +} + +export interface FootprintPageProps { + address: string +} + +export function FootprintPage(props: FootprintPageProps) { + const { address } = props + const { classes } = useStyles() + const { value: profile } = useRss3Profile(address) + const username = profile?.name + + const { footprints, loading } = useFootprints(address) + + if (loading) { + return ( + + + + ) + } + + 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 65cd490a5cb5..63527eed759a 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/NFTPage.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/NFTPage.tsx @@ -1,26 +1,13 @@ -import { getMaskColor, makeStyles, useStylesExtends } from '@masknet/theme' -import { - formatEthereumAddress, - resolveAddressLinkOnExplorer, - ChainId, - EthereumNameType, - useEthereumAddress, -} from '@masknet/web3-shared-evm' -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined' -import { Box, Link, Typography, CircularProgress, Tooltip } from '@mui/material' -import { useCurrentVisitingIdentity } from '../../../components/DataSource/useActivatedUI' -import { CollectionList } from '../../../extension/options-page/DashboardComponents/CollectibleList' +import { getMaskColor, makeStyles } from '@masknet/theme' +import { Box, Typography } from '@mui/material' +import type { ChainId } from '@masknet/web3-shared-evm' import { useI18N } from '../../../utils' -import { useUserOwnerAddress } from '../../Avatar/hooks/useUserOwnerAddress' +import { CollectionList } from '../../../extension/options-page/DashboardComponents/CollectionList' const useStyles = makeStyles()((theme) => ({ root: { position: 'relative', }, - note: { - padding: `0 ${theme.spacing(1)}`, - textAlign: 'right', - }, text: { paddingTop: 36, paddingBottom: 36, @@ -28,51 +15,19 @@ const useStyles = makeStyles()((theme) => ({ color: getMaskColor(theme).textPrimary, }, }, - icon: { - color: getMaskColor(theme).textPrimary, - }, - iconContainer: { - display: 'inherit', - }, - tipList: { - listStyleType: 'decimal', - paddingLeft: 16, - }, })) -interface NFTPageProps extends withClasses<'text' | 'button'> {} +export interface NFTPageProps { + address: string + chainId: ChainId +} export function NFTPage(props: NFTPageProps) { - const classes = useStylesExtends(useStyles(), props) + const { address, chainId } = props const { t } = useI18N() - const identity = useCurrentVisitingIdentity() - const { loading: loadingENS, value } = useEthereumAddress( - identity.nickname ?? '', - identity.identifier.userId, - identity.bio ?? '', - ) - const { type, name, address } = value ?? {} - const { loading: loadingWalletGun, value: walletAddressGun } = useUserOwnerAddress(identity.identifier.userId) + const { classes } = useStyles() - const rulesTipMap = [ - t('plugin_profile_binding_rule1'), - t('plugin_profile_binding_rule2'), - t('plugin_profile_binding_rule3', { suffix: `".eth"` }), - t('plugin_profile_binding_rule4'), - ] - - const tooltipRender = ( - - {t('plugin_profile_binding_rules_title')} - - {rulesTipMap.map((item, index) => { - return {item} - })} - - - ) - - if (!address && !walletAddressGun) + if (!address) return ( @@ -80,58 +35,10 @@ export function NFTPage(props: NFTPageProps) { ) + return ( - {loadingWalletGun || loadingENS ? ( - - - - ) : ( - <> - - - - {t('plugin_profile_current_display_of', { type: type })} - - {type === EthereumNameType.DEFAULT - ? formatEthereumAddress( - (address?.length === 0 ? walletAddressGun : address) ?? '', - 4, - ) - : name} - - - - - - - - - - - > - )} + ) } diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/PageTag.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/PageTag.tsx index 17ee66423075..dcf77987e852 100644 --- a/packages/mask/src/plugins/Profile/SNSAdaptor/PageTag.tsx +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/PageTag.tsx @@ -1,16 +1,18 @@ +import classNames from 'classnames' import { makeStyles } from '@masknet/theme' import { Button } from '@mui/material' -import classNames from 'classnames' import { useI18N } from '../../../utils' import { PageTags } from '../types' -import type { Dao_Payload } from './hooks/useDao' +import { useDonations, useFootprints, Dao_Payload } from './hooks' const useStyles = makeStyles()((theme) => ({ root: { - '>:nth-child(2)': { - marginLeft: theme.spacing(2), - marginRight: theme.spacing(2), - }, + display: 'flex', + gap: '8px', + flexWrap: 'wrap', + }, + hidden: { + display: 'none', }, button: { border: `1px solid ${theme.palette.text.primary} !important`, @@ -22,21 +24,41 @@ const useStyles = makeStyles()((theme) => ({ color: theme.palette.primary.main, borderRadius: 9999, }, - hidden: { - display: 'none', + connectRSS3: { + position: 'absolute', + right: '16px', + boxSizing: 'border-box', + height: 36, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 9999, + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + border: '0px solid', + backgroundColor: theme.palette.primary.main, + color: 'rgb(255, 255, 255)', + cursor: 'pointer', }, })) -interface PageTagProps { +export interface PageTagProps { tag: PageTags + address: string daoPayload: Dao_Payload | undefined onChange: (tag: PageTags) => void } export function PageTag(props: PageTagProps) { const { classes } = useStyles() - const { onChange, tag, daoPayload } = props + const { onChange, tag, address, daoPayload } = props const { t } = useI18N() + + const { donations } = useDonations(address) + const { footprints } = useFootprints(address) + const hasDonations = donations.length > 0 + const hasFootprints = footprints.length > 0 + return ( {t('nfts')} - onChange(PageTags.DonationTag)} - size="medium"> - {t('donations')} - + {hasDonations ? ( + onChange(PageTags.DonationTag)} + size="medium"> + {t('donations')} + + ) : null} + {hasFootprints ? ( + onChange(PageTags.FootprintTag)} + size="medium"> + Footprints + + ) : null} {daoPayload ? ( ({ + root: { + padding: `0 ${theme.spacing(1)}`, + textAlign: 'right', + }, + text: { + paddingTop: 36, + paddingBottom: 36, + '& > p': { + color: getMaskColor(theme).textPrimary, + }, + }, + icon: { + color: getMaskColor(theme).textPrimary, + }, + iconContainer: { + display: 'inherit', + }, + tipList: { + listStyleType: 'decimal', + paddingLeft: 16, + }, +})) + +export interface AddressViewerProps { + addressName: AddressName +} + +export function AddressViewer({ addressName }: AddressViewerProps) { + const { t } = useI18N() + const { classes } = useStyles() + const { type, label, resolvedAddress } = addressName + + const typeMap = { + [AddressNameType.ADDRESS]: t('plugin_profile_address_name_address'), + [AddressNameType.ENS]: t('plugin_profile_address_name_ens'), + [AddressNameType.UNS]: t('plugin_profile_address_name_uns'), + [AddressNameType.DNS]: t('plugin_profile_address_name_dns'), + [AddressNameType.RSS3]: t('plugin_profile_address_name_rns'), + [AddressNameType.GUN]: t('plugin_profile_address_name_address'), + [AddressNameType.THE_GRAPH]: t('plugin_profile_address_name_address'), + } + + const rulesTipMap = [ + t('plugin_profile_binding_rule1'), + t('plugin_profile_binding_rule2'), + t('plugin_profile_binding_rule3', { suffix: `".eth"` }), + t('plugin_profile_binding_rule4'), + ] + + const tooltipRender = ( + + {t('plugin_profile_binding_rules_title')} + + {rulesTipMap.map((item, index) => ( + {item} + ))} + + + ) + + if (!resolvedAddress) return null + + return ( + + + + {t('plugin_profile_current_display_of', { type: typeMap[type] })} + + {formatEthereumAddress(label, 4)} + + + + + + + + + + ) +} diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/components/DonationCard.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/components/DonationCard.tsx new file mode 100644 index 000000000000..259d5d0871bb --- /dev/null +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/components/DonationCard.tsx @@ -0,0 +1,40 @@ +import { Typography } from '@mui/material' + +export interface DonationCardProps { + imageUrl: string + name: string + contribCount: number + contribDetails: { + token: string + amount: string + }[] +} + +export const DonationCard = ({ imageUrl, name, contribCount, contribDetails }: DonationCardProps) => { + return ( + + + + + {name} + + + + {contribCount} + Contrib + + {contribDetails.map((contrib, i) => ( + + {contrib.amount} + {contrib.token} + + ))} + + + + ) +} diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/components/FootprintCard.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/components/FootprintCard.tsx new file mode 100644 index 000000000000..149874ddec2a --- /dev/null +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/components/FootprintCard.tsx @@ -0,0 +1,66 @@ +import { ImageHolder } from './ImageHolder' +import { Typography } from '@mui/material' +import EventRoundedIcon from '@mui/icons-material/EventRounded' +import LocationOnRoundedIcon from '@mui/icons-material/LocationOnRounded' +import fromUnixTime from 'date-fns/fromUnixTime' +import { useI18N } from '../../../../utils' + +const formatDate = (ts: string): string => { + return fromUnixTime(Number.parseInt(ts, 16)).toLocaleDateString('en-US') +} +export interface FootprintProps { + imageUrl: string + startDate: string | undefined + endDate: string | undefined + city: string | undefined + country: string | undefined + username: string + activity: string +} + +export const FootprintCard = ({ imageUrl, startDate, endDate, city, country, username, activity }: FootprintProps) => { + const { t } = useI18N() + // Calc display date + let displayDate: string + if (startDate && endDate) { + displayDate = formatDate(startDate) + if (endDate !== startDate) { + displayDate += ` ~ ${formatDate(endDate)}` + } + } else { + displayDate = t('plugin_profile_rss3_no_activity_time') + } + + // Calc location + const location = city || country || 'Metaverse' + + return ( + + + + + + + + + {displayDate} + + + + + + {location} + + + + + {t('plugin_profile_rss3_attended')} + + + {activity} + + + + + ) +} diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/components/ImageHolder.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/components/ImageHolder.tsx new file mode 100644 index 000000000000..fcb3fd09a8db --- /dev/null +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/components/ImageHolder.tsx @@ -0,0 +1,20 @@ +export interface ImageProps { + url: string + title?: string + isFullRound: boolean + size: number +} + +export const ImageHolder = ({ url, title, isFullRound, size }: ImageProps) => { + const roundedClass = isFullRound ? 'rounded-full' : 'rounded' + const containerStyles = { + width: `${size}px`, + height: `${size}px`, + } + + return ( + + + + ) +} diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/components/NetworkSelector.tsx b/packages/mask/src/plugins/Profile/SNSAdaptor/components/NetworkSelector.tsx new file mode 100644 index 000000000000..4751cc03f083 --- /dev/null +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/components/NetworkSelector.tsx @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react' +import { MiniNetworkSelector } from '@masknet/shared' +import { getRegisteredWeb3Networks, useNetworkDescriptor, usePluginIDContext, Web3Plugin } from '@masknet/plugin-infra' + +export interface NetworkSelectorProps { + onSelectNetwork(network: Web3Plugin.NetworkDescriptor | null): void +} + +export function NetworkSelector({ onSelectNetwork }: NetworkSelectorProps) { + const pluginId = usePluginIDContext() + const networks = getRegisteredWeb3Networks() + const networkDescriptor = useNetworkDescriptor() + const [selectedNetwork, setSelectedNetwork] = useState( + networkDescriptor ?? null, + ) + + const [renderNetworks, setRenderNetworks] = useState([]) + + const onSelect = (network: Web3Plugin.NetworkDescriptor | null) => { + setSelectedNetwork(network) + onSelectNetwork(network) + } + + useEffect(() => { + setRenderNetworks(networks.filter((x) => pluginId === x.networkSupporterPluginID && x.isMainnet)) + }, [pluginId]) + + return ( + + ) +} diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/components/index.ts b/packages/mask/src/plugins/Profile/SNSAdaptor/components/index.ts new file mode 100644 index 000000000000..98b2b93bf9b3 --- /dev/null +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/components/index.ts @@ -0,0 +1,3 @@ +export * from './DonationCard' +export * from './FootprintCard' +export * from './ImageHolder' diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/hooks/index.ts b/packages/mask/src/plugins/Profile/SNSAdaptor/hooks/index.ts new file mode 100644 index 000000000000..d6be6abe2cf0 --- /dev/null +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/hooks/index.ts @@ -0,0 +1,4 @@ +export * from './useDao' +export * from './useDonations' +export * from './useFootprints' +export * from './useRss3Profile' diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/hooks/useDonations.ts b/packages/mask/src/plugins/Profile/SNSAdaptor/hooks/useDonations.ts new file mode 100644 index 000000000000..dc29236d2242 --- /dev/null +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/hooks/useDonations.ts @@ -0,0 +1,14 @@ +import { useAsync } from 'react-use' +import { PluginProfileRPC } from '../../messages' + +export function useDonations(address: string) { + const { value: donations = [], loading } = useAsync(async () => { + const rsp = await PluginProfileRPC.getDonations(address) + return rsp.status ? rsp.assets : [] + }, [address]) + + return { + donations, + loading, + } +} diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/hooks/useFootprints.ts b/packages/mask/src/plugins/Profile/SNSAdaptor/hooks/useFootprints.ts new file mode 100644 index 000000000000..5e81e2bbc370 --- /dev/null +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/hooks/useFootprints.ts @@ -0,0 +1,14 @@ +import { useAsync } from 'react-use' +import { PluginProfileRPC } from '../../messages' + +export function useFootprints(address: string) { + const { value: footprints = [], loading } = useAsync(async () => { + const rsp = await PluginProfileRPC.getFootprints(address) + return rsp.status ? rsp.assets : [] + }, [address]) + + return { + footprints, + loading, + } +} diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/hooks/useRss3Profile.ts b/packages/mask/src/plugins/Profile/SNSAdaptor/hooks/useRss3Profile.ts new file mode 100644 index 000000000000..5e7339c05c54 --- /dev/null +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/hooks/useRss3Profile.ts @@ -0,0 +1,9 @@ +import { useAsync } from 'react-use' +import { PluginProfileRPC } from '../../messages' + +export function useRss3Profile(address: string) { + return useAsync(async () => { + if (!address) return null + return PluginProfileRPC.getRSS3ProfileByAddress(address) + }, [address]) +} diff --git a/packages/mask/src/plugins/Profile/SNSAdaptor/styles/tailwind.css b/packages/mask/src/plugins/Profile/SNSAdaptor/styles/tailwind.css new file mode 100644 index 000000000000..e2981ca5fb16 --- /dev/null +++ b/packages/mask/src/plugins/Profile/SNSAdaptor/styles/tailwind.css @@ -0,0 +1,2058 @@ +/* cspell:disable */ +/*! tailwindcss v2.2.19 | MIT License | https://tailwindcss.com */ + +/*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */ + +/* +Document +======== +*/ + +/** +Use a better box model (opinionated). +*/ + +*, +::before, +::after { + box-sizing: border-box; +} + +/** +Use a more readable tab size (opinionated). +*/ + +html { + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; +} + +/** +1. Correct the line height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +*/ + +html { + line-height: 1.15; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ +} + +/* +Sections +======== +*/ + +/** +Remove the margin in all browsers. +*/ + +body { + margin: 0; +} + +/** +Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) +*/ + +body { + font-family: system-ui, -apple-system, /* Firefox supports this but not yet `system-ui` */ 'Segoe UI', Roboto, + Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; +} + +/* +Grouping content +================ +*/ + +/** +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ +} + +/* +Text-level semantics +==================== +*/ + +/** +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr[title] { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/** +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/** +1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) +2. Correct the odd 'em' font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/** +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/** +Prevent 'sub' and 'sup' elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +Tabular data +============ +*/ + +/** +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ +} + +/* +Forms +===== +*/ + +/** +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + line-height: 1.15; + /* 1 */ + margin: 0; + /* 2 */ +} + +/** +Remove the inheritance of text transform in Edge and Firefox. +1. Remove the inheritance of text transform in Firefox. +*/ + +button, +select { + /* 1 */ + text-transform: none; +} + +/** +Correct the inability to style clickable types in iOS and Safari. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; +} + +/** +Remove the inner border and padding in Firefox. +*/ + +::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** +Restore the focus styles unset by the previous rule. +*/ + +:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** +Remove the additional ':invalid' styles in Firefox. +See: https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737 +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/** +Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers. +*/ + +legend { + padding: 0; +} + +/** +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/** +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/** +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/** +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to 'inherit' in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Interactive +=========== +*/ + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/** + * Manually forked from SUIT CSS Base: https://github.com/suitcss/base + * A thin layer on top of normalize.css that provides a starting point more + * suitable for web applications. + */ + +/** + * Removes the default spacing and border for appropriate elements. + */ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +button { + background-color: transparent; + background-image: none; +} + +fieldset { + margin: 0; + padding: 0; +} + +ol, +ul { + list-style: none; + margin: 0; + padding: 0; +} + +/** + * Tailwind custom reset styles + */ + +/** + * 1. Use the user's configured `sans` font-family (with Tailwind's default + * sans-serif font stack as a fallback) as a sane default. + * 2. Use Tailwind's default "normal" line-height so the user isn't forced + * to override it to ensure consistency even when using the default theme. + */ + +html { + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', + Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + /* 1 */ + line-height: 1.5; + /* 2 */ +} + +/** + * Inherit font-family and line-height from `html` so users can set them as + * a class directly on the `html` element. + */ + +body { + font-family: inherit; + line-height: inherit; +} + +/** + * 1. Prevent padding and border from affecting element width. + * + * We used to set this in the html element and inherit from + * the parent element for everything else. This caused issues + * in shadow-dom-enhanced elements like where the content + * is wrapped by a div with box-sizing set to `content-box`. + * + * https://github.com/mozdevs/cssremedy/issues/4 + * + * + * 2. Allow adding a border to an element by just adding a border-width. + * + * By default, the way the browser specifies that an element should have no + * border is by setting it's border-style to `none` in the user-agent + * stylesheet. + * + * In order to easily add borders to elements by just setting the `border-width` + * property, we change the default border-style for all elements to `solid`, and + * use border-width to hide them instead. This way our `border` utilities only + * need to set the `border-width` property instead of the entire `border` + * shorthand, making our border utilities much more straightforward to compose. + * + * https://github.com/tailwindcss/tailwindcss/pull/116 + */ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: currentColor; + /* 2 */ +} + +/* + * Ensure horizontal rules are visible by default + */ + +hr { + border-top-width: 1px; +} + +/** + * Undo the `border-style: none` reset that Normalize applies to images so that + * our `border-{width}` utilities have the expected effect. + * + * The Normalize reset is unnecessary for us since we default the border-width + * to 0 on all elements. + * + * https://github.com/tailwindcss/tailwindcss/issues/362 + */ + +img { + border-style: solid; +} + +textarea { + resize: vertical; +} + +input::-moz-placeholder, +textarea::-moz-placeholder { + opacity: 1; + color: #9ca3af; +} + +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + opacity: 1; + color: #9ca3af; +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + color: #9ca3af; +} + +button, +[role='button'] { + cursor: pointer; +} + +/** + * Override legacy focus reset from Normalize with modern Firefox focus styles. + * + * This is actually an improvement over the new defaults in Firefox in our testing, + * as it triggers the better focus styles even for links, which still use a dotted + * outline in Firefox by default. + */ + +:-moz-focusring { + outline: auto; +} + +table { + border-collapse: collapse; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/** + * Reset links to optimize for opt-in styling instead of + * opt-out. + */ + +a { + color: inherit; + text-decoration: inherit; +} + +/** + * Reset form element properties that are easy to forget to + * style explicitly so you don't inadvertently introduce + * styles that deviate from your design system. These styles + * supplement a partial reset that is already applied by + * normalize.css. + */ + +button, +input, +optgroup, +select, +textarea { + padding: 0; + line-height: inherit; + color: inherit; +} + +/** + * Use the configured 'mono' font family for elements that + * are expected to be rendered with a monospace font, falling + * back to the system monospace stack if there is no configured + * 'mono' font family. + */ + +pre, +code, +kbd, +samp { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; +} + +/** + * 1. Make replaced elements `display: block` by default as that's + * the behavior you want almost all of the time. Inspired by + * CSS Remedy, with `svg` added as well. + * + * https://github.com/mozdevs/cssremedy/issues/14 + * + * 2. Add `vertical-align: middle` to align replaced elements more + * sensibly by default when overriding `display` by adding a + * utility like `inline`. + * + * This can trigger a poorly considered linting error in some + * tools but is included by design. + * + * https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210 + */ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/** + * Constrain images and videos to the parent width and preserve + * their intrinsic aspect ratio. + * + * https://github.com/mozdevs/cssremedy/issues/14 + */ + +img, +video { + max-width: 100%; + height: auto; +} + +/** + * Ensure the default browser behavior of the `hidden` attribute. + */ + +[hidden] { + display: none; +} + +*, +::before, +::after { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-transform: translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) + skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + --tw-border-opacity: 1; + border-color: rgba(229, 231, 235, var(--tw-border-opacity)); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-blur: var(--tw-empty, /*!*/ /*!*/); + --tw-brightness: var(--tw-empty, /*!*/ /*!*/); + --tw-contrast: var(--tw-empty, /*!*/ /*!*/); + --tw-grayscale: var(--tw-empty, /*!*/ /*!*/); + --tw-hue-rotate: var(--tw-empty, /*!*/ /*!*/); + --tw-invert: var(--tw-empty, /*!*/ /*!*/); + --tw-saturate: var(--tw-empty, /*!*/ /*!*/); + --tw-sepia: var(--tw-empty, /*!*/ /*!*/); + --tw-drop-shadow: var(--tw-empty, /*!*/ /*!*/); + --tw-filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) + var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.aspect-w-1, +.aspect-w-2, +.aspect-w-3, +.aspect-w-4, +.aspect-w-5, +.aspect-w-6, +.aspect-w-7, +.aspect-w-8, +.aspect-w-9, +.aspect-w-10, +.aspect-w-11, +.aspect-w-12, +.aspect-w-13, +.aspect-w-14, +.aspect-w-15, +.aspect-w-16 { + position: relative; + padding-bottom: calc(var(--tw-aspect-h) / var(--tw-aspect-w) * 100%); +} + +.aspect-w-1 > *, +.aspect-w-2 > *, +.aspect-w-3 > *, +.aspect-w-4 > *, +.aspect-w-5 > *, +.aspect-w-6 > *, +.aspect-w-7 > *, +.aspect-w-8 > *, +.aspect-w-9 > *, +.aspect-w-10 > *, +.aspect-w-11 > *, +.aspect-w-12 > *, +.aspect-w-13 > *, +.aspect-w-14 > *, +.aspect-w-15 > *, +.aspect-w-16 > * { + position: absolute; + height: 100%; + width: 100%; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.aspect-w-1 { + --tw-aspect-w: 1; +} + +.aspect-w-16 { + --tw-aspect-w: 16; +} + +.aspect-h-1 { + --tw-aspect-h: 1; +} + +.aspect-h-9 { + --tw-aspect-h: 9; +} + +.fixed { + position: fixed; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.top-0 { + top: 0px; +} + +.right-0 { + right: 0px; +} + +.right-2\.5 { + right: 0.625rem; +} + +.top-2\.5 { + top: 0.625rem; +} + +.right-2 { + right: 0.5rem; +} + +.top-2 { + top: 0.5rem; +} + +.left-2 { + left: 0.5rem; +} + +.z-30 { + z-index: 30; +} + +.z-50 { + z-index: 50; +} + +.m-auto { + margin: auto; +} + +.m-4 { + margin: 1rem; +} + +.m-0\.5 { + margin: 0.125rem; +} + +.m-0 { + margin: 0px; +} + +.my-8 { + margin-top: 2rem; + margin-bottom: 2rem; +} + +.mx-14 { + margin-left: 3.5rem; + margin-right: 3.5rem; +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.mx-2 { + margin-left: 0.5rem; + margin-right: 0.5rem; +} + +.mx-1 { + margin-left: 0.25rem; + margin-right: 0.25rem; +} + +.my-4 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.my-1 { + margin-top: 0.25rem; + margin-bottom: 0.25rem; +} + +.my-2 { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.ml-2 { + margin-left: 0.5rem; +} + +.mb-11 { + margin-bottom: 2.75rem; +} + +.mr-1 { + margin-right: 0.25rem; +} + +.ml-1 { + margin-left: 0.25rem; +} + +.mb-3 { + margin-bottom: 0.75rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-3 { + margin-top: 0.75rem; +} + +.-mt-3 { + margin-top: -0.75rem; +} + +.ml-0 { + margin-left: 0px; +} + +.mb-2\.5 { + margin-bottom: 0.625rem; +} + +.mt-2\.5 { + margin-top: 0.625rem; +} + +.box-border { + box-sizing: border-box; +} + +.inline { + display: inline; +} + +.flex { + display: flex; +} + +.inline-flex { + display: inline-flex; +} + +.grid { + display: grid; +} + +.hidden { + display: none; +} + +.h-8 { + height: 2rem; +} + +.h-14 { + height: 3.5rem; +} + +.h-4 { + height: 1rem; +} + +.h-full { + height: 100%; +} + +.h-0 { + height: 0px; +} + +.h-12 { + height: 3rem; +} + +.h-6 { + height: 1.5rem; +} + +.h-24 { + height: 6rem; +} + +.h-32 { + height: 8rem; +} + +.h-7 { + height: 1.75rem; +} + +.h-40 { + height: 10rem; +} + +.h-96 { + height: 24rem; +} + +.h-modal { + height: 60vh; +} + +.h-16 { + height: 4rem; +} + +.min-h-screen { + min-height: 100vh; +} + +.w-full { + width: 100%; +} + +.w-60 { + width: 15rem; +} + +.w-7\/11 { + width: 63.63636364%; +} + +.w-4\/11 { + width: 36.36363636%; +} + +.w-4\/5 { + width: 80%; +} + +.w-48 { + width: 12rem; +} + +.w-4 { + width: 1rem; +} + +.w-32 { + width: 8rem; +} + +.w-6 { + width: 1.5rem; +} + +.w-12 { + width: 3rem; +} + +.w-24 { + width: 6rem; +} + +.w-64 { + width: 16rem; +} + +.w-0 { + width: 0px; +} + +.w-max { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; +} + +.w-7 { + width: 1.75rem; +} + +.w-8 { + width: 2rem; +} + +.w-16 { + width: 4rem; +} + +.min-w-full { + min-width: 100%; +} + +.min-w-1\/4 { + min-width: 25%; +} + +.max-w-6xl { + max-width: 72rem; +} + +.max-w-5xl { + max-width: 64rem; +} + +.max-w-lg { + max-width: 32rem; +} + +.max-w-4xl { + max-width: 56rem; +} + +.max-w-screen-md { + max-width: 768px; +} + +.max-w-screen-sm { + max-width: 640px; +} + +.max-w-sm { + max-width: 24rem; +} + +.flex-1 { + flex: 1 1 0%; +} + +.flex-shrink-0 { + flex-shrink: 0; +} + +.flex-shrink { + flex-shrink: 1; +} + +.flex-grow { + flex-grow: 1; +} + +.transform { + transform: var(--tw-transform); +} + +@-webkit-keyframes spin { + to { + transform: rotate(360deg); + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.animate-spin { + -webkit-animation: spin 1s linear infinite; + animation: spin 1s linear infinite; +} + +.cursor-move { + cursor: move; +} + +.cursor-pointer { + cursor: pointer; +} + +.cursor-not-allowed { + cursor: not-allowed; +} + +.select-none { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.resize-none { + resize: none; +} + +.grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.flex-row { + flex-direction: row; +} + +.flex-col { + flex-direction: column; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.place-items-center { + place-items: center; +} + +.content-start { + align-content: flex-start; +} + +.items-start { + align-items: flex-start; +} + +.items-end { + align-items: flex-end; +} + +.items-center { + align-items: center; +} + +.justify-start { + justify-content: flex-start; +} + +.justify-end { + justify-content: flex-end; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.justify-around { + justify-content: space-around; +} + +.justify-items-center { + justify-items: center; +} + +.gap-4 { + gap: 1rem; +} + +.gap-3 { + gap: 0.75rem; +} + +.gap-6 { + gap: 1.5rem; +} + +.gap-2 { + gap: 0.5rem; +} + +.gap-5 { + gap: 1.25rem; +} + +.gap-1 { + gap: 0.25rem; +} + +.gap-2\.5 { + gap: 0.625rem; +} + +.gap-1\.5 { + gap: 0.375rem; +} + +.gap-x-8 { + -moz-column-gap: 2rem; + column-gap: 2rem; +} + +.gap-y-6 { + row-gap: 1.5rem; +} + +.gap-x-3 { + -moz-column-gap: 0.75rem; + column-gap: 0.75rem; +} + +.gap-y-5 { + row-gap: 1.25rem; +} + +.gap-x-5 { + -moz-column-gap: 1.25rem; + column-gap: 1.25rem; +} + +.gap-x-2 { + -moz-column-gap: 0.5rem; + column-gap: 0.5rem; +} + +.gap-y-4 { + row-gap: 1rem; +} + +.gap-y-2 { + row-gap: 0.5rem; +} + +.gap-x-12 { + -moz-column-gap: 3rem; + column-gap: 3rem; +} + +.gap-x-6 { + -moz-column-gap: 1.5rem; + column-gap: 1.5rem; +} + +.gap-x-1 { + -moz-column-gap: 0.25rem; + column-gap: 0.25rem; +} + +.gap-x-4 { + -moz-column-gap: 1rem; + column-gap: 1rem; +} + +.divide-y-2 > :not([hidden]) ~ :not([hidden]) { + --tw-divide-y-reverse: 0; + border-top-width: calc(2px * calc(1 - var(--tw-divide-y-reverse))); + border-bottom-width: calc(2px * var(--tw-divide-y-reverse)); +} + +.divide-y > :not([hidden]) ~ :not([hidden]) { + --tw-divide-y-reverse: 0; + border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); +} + +.divide-solid > :not([hidden]) ~ :not([hidden]) { + border-style: solid; +} + +.divide-primary > :not([hidden]) ~ :not([hidden]) { + --tw-divide-opacity: 1; + border-color: rgba(0, 114, 255, var(--tw-divide-opacity)); +} + +.divide-opacity-5 > :not([hidden]) ~ :not([hidden]) { + --tw-divide-opacity: 0.05; +} + +.divide-opacity-10 > :not([hidden]) ~ :not([hidden]) { + --tw-divide-opacity: 0.1; +} + +.overflow-auto { + overflow: auto; +} + +.overflow-hidden { + overflow: hidden; +} + +.overflow-y-auto { + overflow-y: auto; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.overflow-ellipsis { + text-overflow: ellipsis; +} + +.whitespace-pre-line { + white-space: pre-line; +} + +.break-all { + word-break: break-all; +} + +.rounded-full { + border-radius: 9999px; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.rounded { + border-radius: 5px; +} + +.border { + border-width: 1px; +} + +.border-2 { + border-width: 2px; +} + +.border-b { + border-bottom-width: 1px; +} + +.border-none { + border-style: none; +} + +.border-black { + --tw-border-opacity: 1; + border-color: rgba(0, 0, 0, var(--tw-border-opacity)); +} + +.border-primary { + --tw-border-opacity: 1; + border-color: rgba(0, 114, 255, var(--tw-border-opacity)); +} + +.border-account { + --tw-border-opacity: 1; + border-color: rgba(64, 210, 47, var(--tw-border-opacity)); +} + +.border-donation-bg { + --tw-border-opacity: 1; + border-color: rgba(240, 255, 250, var(--tw-border-opacity)); +} + +.border-nft { + --tw-border-opacity: 1; + border-color: rgba(117, 83, 255, var(--tw-border-opacity)); +} + +.border-donation { + --tw-border-opacity: 1; + border-color: rgba(17, 188, 146, var(--tw-border-opacity)); +} + +.border-footprint { + --tw-border-opacity: 1; + border-color: rgba(255, 180, 38, var(--tw-border-opacity)); +} + +.border-metamask-bg { + --tw-border-opacity: 1; + border-color: rgba(255, 244, 235, var(--tw-border-opacity)); +} + +.border-gray-300 { + --tw-border-opacity: 1; + border-color: rgba(209, 213, 219, var(--tw-border-opacity)); +} + +.border-opacity-70 { + --tw-border-opacity: 0.7; +} + +.border-opacity-40 { + --tw-border-opacity: 0.4; +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgba(255, 255, 255, var(--tw-bg-opacity)); +} + +.bg-account-bg { + --tw-bg-opacity: 1; + background-color: rgba(246, 255, 245, var(--tw-bg-opacity)); +} + +.bg-nft-bg { + --tw-bg-opacity: 1; + background-color: rgba(247, 245, 255, var(--tw-bg-opacity)); +} + +.bg-donation-bg { + --tw-bg-opacity: 1; + background-color: rgba(240, 255, 250, var(--tw-bg-opacity)); +} + +.bg-footprint-bg { + --tw-bg-opacity: 1; + background-color: rgba(255, 249, 240, var(--tw-bg-opacity)); +} + +.bg-secondary-bg { + --tw-bg-opacity: 1; + background-color: rgba(244, 247, 248, var(--tw-bg-opacity)); +} + +.bg-primary { + --tw-bg-opacity: 1; + background-color: rgba(0, 114, 255, var(--tw-bg-opacity)); +} + +.bg-account { + --tw-bg-opacity: 1; + background-color: rgba(64, 210, 47, var(--tw-bg-opacity)); +} + +.bg-nft { + --tw-bg-opacity: 1; + background-color: rgba(117, 83, 255, var(--tw-bg-opacity)); +} + +.bg-donation { + --tw-bg-opacity: 1; + background-color: rgba(17, 188, 146, var(--tw-bg-opacity)); +} + +.bg-footprint { + --tw-bg-opacity: 1; + background-color: rgba(255, 180, 38, var(--tw-bg-opacity)); +} + +.bg-metamask-bg { + --tw-bg-opacity: 1; + background-color: rgba(255, 244, 235, var(--tw-bg-opacity)); +} + +.bg-transparent { + background-color: transparent; +} + +.bg-black { + --tw-bg-opacity: 1; + background-color: rgba(0, 0, 0, var(--tw-bg-opacity)); +} + +.bg-opacity-5 { + --tw-bg-opacity: 0.05; +} + +.bg-opacity-50 { + --tw-bg-opacity: 0.5; +} + +.bg-cover { + background-size: cover; +} + +.bg-contain { + background-size: contain; +} + +.bg-center { + background-position: center; +} + +.bg-no-repeat { + background-repeat: no-repeat; +} + +.bg-origin-content { + background-origin: content-box; +} + +.object-contain { + -o-object-fit: contain; + object-fit: contain; +} + +.object-cover { + -o-object-fit: cover; + object-fit: cover; +} + +.p-4 { + padding: 1rem; +} + +.p-1 { + padding: 0.25rem; +} + +.p-0\.5 { + padding: 0.125rem; +} + +.p-0 { + padding: 0px; +} + +.p-2 { + padding: 0.5rem; +} + +.p-14 { + padding: 3.5rem; +} + +.p-3 { + padding: 0.75rem; +} + +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.px-12 { + padding-left: 3rem; + padding-right: 3rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.py-16 { + padding-top: 4rem; + padding-bottom: 4rem; +} + +.px-3\.5 { + padding-left: 0.875rem; + padding-right: 0.875rem; +} + +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.px-8 { + padding-left: 2rem; + padding-right: 2rem; +} + +.py-sm { + padding-top: 0.2rem; + padding-bottom: 0.2rem; +} + +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.py-2\.5 { + padding-top: 0.625rem; + padding-bottom: 0.625rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.px-5 { + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.py-12 { + padding-top: 3rem; + padding-bottom: 3rem; +} + +.py-1\.5 { + padding-top: 0.375rem; + padding-bottom: 0.375rem; +} + +.pt-16 { + padding-top: 4rem; +} + +.pb-16 { + padding-bottom: 4rem; +} + +.pt-12 { + padding-top: 3rem; +} + +.pt-10 { + padding-top: 2.5rem; +} + +.pb-5 { + padding-bottom: 1.25rem; +} + +.pl-14 { + padding-left: 3.5rem; +} + +.pt-2 { + padding-top: 0.5rem; +} + +.pl-40 { + padding-left: 10rem; +} + +.pb-8 { + padding-bottom: 2rem; +} + +.pl-2\.5 { + padding-left: 0.625rem; +} + +.pl-2 { + padding-left: 0.5rem; +} + +.pt-0 { + padding-top: 0px; +} + +.pr-2 { + padding-right: 0.5rem; +} + +.pl-4 { + padding-left: 1rem; +} + +.pr-5 { + padding-right: 1.25rem; +} + +.pt-5 { + padding-top: 1.25rem; +} + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.text-xs { + font-size: 0.75rem; + line-height: 1rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.font-bold { + font-weight: 700; +} + +.font-semibold { + font-weight: 600; +} + +.font-normal { + font-weight: 400; +} + +.font-medium { + font-weight: 500; +} + +.uppercase { + text-transform: uppercase; +} + +.capitalize { + text-transform: capitalize; +} + +.leading-normal { + line-height: 1.5; +} + +.leading-5 { + line-height: 1.25rem; +} + +.leading-none { + line-height: 1; +} + +.text-blue-500 { + --tw-text-opacity: 1; + color: rgba(59, 130, 246, var(--tw-text-opacity)); +} + +.text-primary { + --tw-text-opacity: 1; + color: rgba(0, 114, 255, var(--tw-text-opacity)); +} + +.text-nft { + --tw-text-opacity: 1; + color: rgba(117, 83, 255, var(--tw-text-opacity)); +} + +.text-account { + --tw-text-opacity: 1; + color: rgba(64, 210, 47, var(--tw-text-opacity)); +} + +.text-donation { + --tw-text-opacity: 1; + color: rgba(17, 188, 146, var(--tw-text-opacity)); +} + +.text-footprint { + --tw-text-opacity: 1; + color: rgba(255, 180, 38, var(--tw-text-opacity)); +} + +.text-secondary { + --tw-text-opacity: 1; + color: rgba(0, 114, 255, var(--tw-text-opacity)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgba(255, 255, 255, var(--tw-text-opacity)); +} + +.text-metamask-text { + --tw-text-opacity: 1; + color: rgba(148, 67, 0, var(--tw-text-opacity)); +} + +.text-opacity-70 { + --tw-text-opacity: 0.7; +} + +.text-opacity-40 { + --tw-text-opacity: 0.4; +} + +.placeholder-black::-moz-placeholder { + --tw-placeholder-opacity: 1; + color: rgba(0, 0, 0, var(--tw-placeholder-opacity)); +} + +.placeholder-black:-ms-input-placeholder { + --tw-placeholder-opacity: 1; + color: rgba(0, 0, 0, var(--tw-placeholder-opacity)); +} + +.placeholder-black::placeholder { + --tw-placeholder-opacity: 1; + color: rgba(0, 0, 0, var(--tw-placeholder-opacity)); +} + +.placeholder-opacity-20::-moz-placeholder { + --tw-placeholder-opacity: 0.2; +} + +.placeholder-opacity-20:-ms-input-placeholder { + --tw-placeholder-opacity: 0.2; +} + +.placeholder-opacity-20::placeholder { + --tw-placeholder-opacity: 0.2; +} + +.opacity-50 { + opacity: 0.5; +} + +.opacity-70 { + opacity: 0.7; +} + +.opacity-20 { + opacity: 0.2; +} + +.opacity-100 { + opacity: 1; +} + +.opacity-40 { + opacity: 0.4; +} + +.mix-blend-overlay { + mix-blend-mode: overlay; +} + +.shadow-lg { + --tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.outline-none { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.filter { + filter: var(--tw-filter); +} + +.transition { + transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, + -webkit-backdrop-filter; + transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, + backdrop-filter; + transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, + backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.duration-300 { + transition-duration: 300ms; +} + +.ease-in-out { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +.line-clamp-5 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 5; +} + +.hover\:border-primary:hover { + --tw-border-opacity: 1; + border-color: rgba(0, 114, 255, var(--tw-border-opacity)); +} + +.hover\:border-account:hover { + --tw-border-opacity: 1; + border-color: rgba(64, 210, 47, var(--tw-border-opacity)); +} + +.hover\:border-nft:hover { + --tw-border-opacity: 1; + border-color: rgba(117, 83, 255, var(--tw-border-opacity)); +} + +.hover\:border-donation:hover { + --tw-border-opacity: 1; + border-color: rgba(17, 188, 146, var(--tw-border-opacity)); +} + +.hover\:border-footprint:hover { + --tw-border-opacity: 1; + border-color: rgba(255, 180, 38, var(--tw-border-opacity)); +} + +.hover\:border-metamask-bg:hover { + --tw-border-opacity: 1; + border-color: rgba(255, 244, 235, var(--tw-border-opacity)); +} + +.hover\:border-opacity-40:hover { + --tw-border-opacity: 0.4; +} + +.hover\:text-white:hover { + --tw-text-opacity: 1; + color: rgba(255, 255, 255, var(--tw-text-opacity)); +} + +.hover\:text-metamask-text:hover { + --tw-text-opacity: 1; + color: rgba(148, 67, 0, var(--tw-text-opacity)); +} + +.hover\:text-opacity-80:hover { + --tw-text-opacity: 0.8; +} + +.hover\:opacity-90:hover { + opacity: 0.9; +} +.hover\:no-underline:hover { + text-decoration: none; +} + +@media (min-width: 640px) { + .sm\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (min-width: 768px) { + .md\:flex { + display: flex; + } + + .md\:w-64 { + width: 16rem; + } + + .md\:h-16 { + height: 4rem; + } + + .md\:flex-grow { + flex-grow: 1; + } + + .md\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .md\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .md\:grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + .md\:pt-16 { + padding-top: 4rem; + } +} + +@media (min-width: 1024px) { + .lg\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .lg\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .lg\:grid-cols-5 { + grid-template-columns: repeat(5, minmax(0, 1fr)); + } +} + +@media (min-width: 1280px) { + .xl\:max-w-6xl { + max-width: 72rem; + } + + .xl\:grid-cols-5 { + grid-template-columns: repeat(5, minmax(0, 1fr)); + } +} + +@media (min-width: 1536px) { + .\32xl\:grid { + display: grid; + } + + .\32xl\:max-w-7xl { + max-width: 80rem; + } +} diff --git a/packages/mask/src/plugins/Profile/Worker/index.ts b/packages/mask/src/plugins/Profile/Worker/index.ts new file mode 100644 index 000000000000..824d930b57e1 --- /dev/null +++ b/packages/mask/src/plugins/Profile/Worker/index.ts @@ -0,0 +1,9 @@ +import type { Plugin } from '@masknet/plugin-infra' +import { base } from '../base' +import '../messages' + +const worker: Plugin.Worker.Definition = { + ...base, + init(signal) {}, +} +export default worker diff --git a/packages/mask/src/plugins/Profile/constants.ts b/packages/mask/src/plugins/Profile/constants.ts index 4fbc840f84ba..40534c6c08f4 100644 --- a/packages/mask/src/plugins/Profile/constants.ts +++ b/packages/mask/src/plugins/Profile/constants.ts @@ -1,3 +1,5 @@ -export const PLUGIN_ID = 'com.maskbook.profile' +export const PLUGIN_ID = 'com.mask.profile' export const PLUGIN_NAME = 'Profile' export const PLUGIN_DESCRIPTION = 'Profile' + +export const RSS3_DEFAULT_IMAGE = 'https://rss3.mypinata.cloud/ipfs/QmVFq9qimnudPcs6QkQv8ZVEsvwD3aqETHWtS5yXgdbYY5' diff --git a/packages/mask/src/plugins/Profile/index.ts b/packages/mask/src/plugins/Profile/index.ts index 83cbdd00cbaf..eb51ba0afe25 100644 --- a/packages/mask/src/plugins/Profile/index.ts +++ b/packages/mask/src/plugins/Profile/index.ts @@ -8,4 +8,9 @@ registerPlugin({ hotModuleReload: (hot) => import.meta.webpackHot && import.meta.webpackHot.accept('./SNSAdaptor', () => hot(import('./SNSAdaptor'))), }, + Worker: { + load: () => import('./Worker'), + hotModuleReload: (hot) => + import.meta.webpackHot && import.meta.webpackHot.accept('./Worker', () => hot(import('./Worker'))), + }, }) diff --git a/packages/mask/src/plugins/Profile/messages.ts b/packages/mask/src/plugins/Profile/messages.ts new file mode 100644 index 000000000000..82acf8d7c337 --- /dev/null +++ b/packages/mask/src/plugins/Profile/messages.ts @@ -0,0 +1,9 @@ +import { createPluginMessage, PluginMessageEmitter, createPluginRPC } from '@masknet/plugin-infra' +import { PLUGIN_ID } from './constants' + +interface ProfileMessages { + rpc: unknown +} + +export const PluginProfileMessages: PluginMessageEmitter = createPluginMessage(PLUGIN_ID) +export const PluginProfileRPC = createPluginRPC(PLUGIN_ID, () => import('./services'), PluginProfileMessages.rpc) diff --git a/packages/mask/src/plugins/Profile/services.ts b/packages/mask/src/plugins/Profile/services.ts new file mode 100644 index 000000000000..1ddafbc5c78e --- /dev/null +++ b/packages/mask/src/plugins/Profile/services.ts @@ -0,0 +1,48 @@ +import urlcat from 'urlcat' +import { fetchJSON } from '../../extension/background-script/HelperService' +import { AssetType, GeneralAsset, RSS3Profile } from './types' + +interface Response { + status: boolean + assets: GeneralAsset[] +} + +interface NameInfo { + rnsName: string + ensName: string | null + address: string +} + +interface RSS3Info { + profile: RSS3Profile +} + +export function getDonations(account: string) { + const url = urlcat('https://hub.pass3.me/assets/list', { + personaID: account, + type: AssetType.GitcoinDonation, + }) + + return fetchJSON(url) +} + +export function getFootprints(account: string): Promise { + const url = urlcat('https://hub.pass3.me/assets/list', { + personaID: account, + type: AssetType.POAP, + }) + + return fetchJSON(url) +} + +export async function getRSS3AddressById(id: string) { + const url = urlcat('https://rss3.domains/name/:id', { id }) + const rsp = await fetchJSON(url) + return rsp.address +} + +export async function getRSS3ProfileByAddress(address: string) { + const url = urlcat('https://hub.pass3.me/:address', { address }) + const rsp = await fetchJSON(url) + return rsp?.profile +} diff --git a/packages/mask/src/plugins/Profile/types.ts b/packages/mask/src/plugins/Profile/types.ts index 4749e4687ad8..5a94d76053a5 100644 --- a/packages/mask/src/plugins/Profile/types.ts +++ b/packages/mask/src/plugins/Profile/types.ts @@ -2,5 +2,49 @@ export enum PageTags { WalletTag = 'Wallets', NFTTag = 'NFTs', DonationTag = 'Donations', + FootprintTag = 'Footprints', DAOTag = 'DAO', } + +export interface Asset { + chain: string +} + +export interface GeneralAsset { + platform: string + identity: string + id: string // contractAddress-id or admin_address + type: string + info: { + collection?: string + collection_icon?: string + image_preview_url?: string | null + animation_url?: string | null + animation_original_url?: string | null + title?: string + total_contribs?: number + token_contribs?: { + token: string + amount: string + }[] + start_date?: string + end_date?: string + country?: string + city?: string + } +} + +export interface GeneralAssetWithTags extends GeneralAsset { + tags?: string[] +} + +export interface RSS3Profile { + avatar: string[] + bio: string + name: string +} + +export enum AssetType { + GitcoinDonation = 'Gitcoin-Donation', + POAP = 'POAP', +} diff --git a/packages/mask/src/plugins/Wallet/services/assets.ts b/packages/mask/src/plugins/Wallet/services/assets.ts index 540820332686..ba50a1c8dcc8 100644 --- a/packages/mask/src/plugins/Wallet/services/assets.ts +++ b/packages/mask/src/plugins/Wallet/services/assets.ts @@ -1,3 +1,4 @@ +import { uniqBy } from 'lodash-es' import { unreachable } from '@dimensiondev/kit' import { leftShift, multipliedBy, rightShift } from '@masknet/web3-shared-base' import { @@ -23,6 +24,7 @@ import { import BigNumber from 'bignumber.js' import { values } from 'lodash-unified' import { EthereumAddress } from 'wallet.ts' +import { getTokens } from '.' import * as DebankAPI from '../apis/debank' import * as OpenSeaAPI from '../apis/opensea' import * as ZerionAPI from '../apis/zerion' @@ -37,14 +39,20 @@ import type { } from '../types' export async function getCollectionsNFT( - address: string, chainId: ChainId, + address: string, provider: NonFungibleAssetProvider, page?: number, size?: number, ): Promise<{ collections: ERC721TokenCollectionInfo[]; hasNextPage: boolean }> { if (provider === NonFungibleAssetProvider.OPENSEA) { - const { collections } = await OpenSeaAPI.getCollections(address, { chainId, page, size }) + const collectionsFromDB = (await getTokens(EthereumTokenType.ERC721)).map(x => ({ + name: x.contractDetailed.name, + image_url: x.contractDetailed.iconURL, + slug: x.contractDetailed.symbol, + })) + const { collections: collectionsFromProvider } = await OpenSeaAPI.getCollections(address, { chainId, page, size }) + const collections = uniqBy([...collectionsFromDB, ...collectionsFromProvider], (x) => x.slug) return { collections: collections.map((x) => ({ @@ -63,15 +71,17 @@ export async function getCollectionsNFT( } export async function getAssetsListNFT( - address: string, chainId: ChainId, + address: string, provider: NonFungibleAssetProvider, page?: number, size?: number, collection?: string, ): Promise<{ assets: ERC721TokenDetailed[]; hasNextPage: boolean }> { if (provider === NonFungibleAssetProvider.OPENSEA) { - const { assets } = await OpenSeaAPI.getAssetsList(address, { chainId, page, size, collection }) + const tokensFromDB = (await getTokens(EthereumTokenType.ERC721)).filter(x => x.contractDetailed.chainId === chainId && x.) + const { assets: tokensFromProvider } = await OpenSeaAPI.getAssetsList(address, { chainId, page, size, collection }) + const tokens = [...tokensFromDB, ...tokensFromProvider] return { assets: assets .filter( diff --git a/packages/mask/src/plugins/Wallet/services/domain.ts b/packages/mask/src/plugins/Wallet/services/domain.ts index ae90338a5140..72cf13fbfe5a 100644 --- a/packages/mask/src/plugins/Wallet/services/domain.ts +++ b/packages/mask/src/plugins/Wallet/services/domain.ts @@ -1,22 +1,142 @@ -import { memoizePromise, unreachable } from '@dimensiondev/kit' -import { AddressNameType, AddressName } from '@masknet/web3-shared-evm' +import { EthereumAddress } from 'wallet.ts' +import { memoizePromise } from '@dimensiondev/kit' +import { Resolution } from '@unstoppabledomains/resolution' +import { + AddressName, + AddressNameType, + ChainId, + isSameAddress, + ProviderType, + ZERO_ADDRESS, +} from '@masknet/web3-shared-evm' import * as ENS from '../apis/ens' +import { createWeb3 } from '../../../extension/background-script/EthereumServices/web3' +import { PluginProfileRPC } from '../../Profile/messages' +import { PluginNFTAvatarRPC } from '../../Avatar/messages' const fetchAddressNamesByTwitterIdCached = memoizePromise(ENS.fetchAddressNamesByTwitterId, (twitterId) => twitterId) +const fetchUserOwnerAddressByTwitterIdCached = memoizePromise(PluginNFTAvatarRPC.getAddress, (twitterId) => twitterId) -export async function getAddressNames(twitterId: string, addressNameType: AddressNameType): Promise { - switch (addressNameType) { - case AddressNameType.ENS: - return (await fetchAddressNamesByTwitterIdCached(twitterId.toLowerCase())).map((x) => ({ - label: '', - ownerAddress: x.owner, - resolvedAddress: x.owner, - })) - case AddressNameType.UNS: - throw new Error('To be implemented.') - case AddressNameType.DNS: - throw new Error('To be implemented.') - default: - unreachable(addressNameType) +const ENS_RE = /\S{1,256}\.(eth|kred|xyz|luxe)\b/ +const ADDRESS_FULL = /0x\w+/ +const RSS3_URL_RE = /https?:\/\/(?\w+)\.rss3\.bio/ +const RSS3_RNS_RE = /(?\w+)\.rss3/ + +function isValidAddress(address: string) { + return address && EthereumAddress.isValid(address) && !isSameAddress(address, ZERO_ADDRESS) +} + +function getEthereumName(twitterId: string, nickname: string, bio: string) { + const [matched] = nickname.match(ENS_RE) ?? bio.match(ENS_RE) ?? [] + if (matched) return matched + return twitterId && !twitterId.endsWith('.eth') ? `${twitterId}.eth` : '' +} + +function getRSS3Id(nickname: string, profileURL: string, bio: string) { + const matched = + nickname.match(RSS3_RNS_RE) || profileURL.match(RSS3_URL_RE) || bio.match(RSS3_URL_RE) || bio.match(RSS3_RNS_RE) + return matched?.groups?.name ?? '' +} + +function getAddress(text: string) { + const [matched] = text.match(ADDRESS_FULL) ?? [] + if (matched && EthereumAddress.isValid(matched)) return matched + return '' +} + +async function getResolvedENS(label: string) { + const web3 = await createWeb3({ + chainId: ChainId.Mainnet, + providerType: ProviderType.MaskWallet, + }) + return web3.eth.ens.getAddress(label) +} + +async function getResolvedUNS(label: string) { + const resolution = new Resolution() + const result = await resolution.records(label, ['crypto.ETH.address']) + return result['crypto.ETH.address'] +} + +export async function getAddressNames(identity: { + identifier: { + userId: string + network: string } + avatar?: string + bio?: string + nickname?: string + homepage?: string +}) { + const { identifier, bio = '', nickname = '', homepage = '' } = identity + const twitterId = identifier.network === 'twitter.com' ? identifier.userId : '' + + const address = getAddress(bio) + const ethereumName = getEthereumName(twitterId ?? '', nickname, bio) + const RSS3Id = getRSS3Id(nickname, homepage, bio) + + const allSettled = await Promise.allSettled([ + getResolvedENS(ethereumName), + getResolvedUNS(ethereumName), + PluginProfileRPC.getRSS3AddressById(RSS3Id), + fetchAddressNamesByTwitterIdCached(twitterId?.toLowerCase() ?? '').then( + (result) => result.find((x) => x.owner)?.owner ?? '', + ), + fetchUserOwnerAddressByTwitterIdCached(twitterId?.toLowerCase() ?? ''), + ]) + + const getSettledAddress = (result: PromiseSettledResult) => { + return result.status === 'fulfilled' ? result.value : '' + } + + const addressENS = getSettledAddress(allSettled[0]) + const addressUNS = getSettledAddress(allSettled[1]) + const addressRSS3 = getSettledAddress(allSettled[2]) + const addressTheGraph = getSettledAddress(allSettled[3]) + const addressGUN = getSettledAddress(allSettled[4]) + + return [ + isValidAddress(address) + ? { + type: AddressNameType.ADDRESS, + label: address, + resolvedAddress: address, + } + : null, + isValidAddress(addressENS) + ? { + type: AddressNameType.ENS, + label: ethereumName, + resolvedAddress: addressENS, + } + : null, + isValidAddress(addressUNS) + ? { + type: AddressNameType.UNS, + label: ethereumName, + resolvedAddress: addressUNS, + } + : null, + isValidAddress(addressRSS3) + ? { + type: AddressNameType.RSS3, + label: `${RSS3Id}.rss`, + resolvedAddress: addressRSS3, + } + : null, + isValidAddress(addressGUN) + ? { + type: AddressNameType.GUN, + label: addressGUN, + resolvedAddress: addressGUN, + } + : null, + isValidAddress(addressTheGraph) + ? { + type: AddressNameType.THE_GRAPH, + label: addressTheGraph, + resolvedAddress: addressTheGraph, + } + : null, + ].filter(Boolean) as AddressName[] } diff --git a/packages/mask/src/social-network-adaptor/twitter.com/collecting/identity.ts b/packages/mask/src/social-network-adaptor/twitter.com/collecting/identity.ts index 8a50d6192b39..ad78648aebc9 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/collecting/identity.ts +++ b/packages/mask/src/social-network-adaptor/twitter.com/collecting/identity.ts @@ -3,8 +3,9 @@ import { LiveSelector, MutationObserverWatcher } from '@dimensiondev/holoflows-k import { selfInfoSelectors, searchAvatarSelector, searchAvatarMetaSelector } from '../utils/selector' import { ProfileIdentifier } from '@masknet/shared-base' import { creator, SocialNetworkUI as Next } from '../../../social-network' +import Services from '../../../extension/service' import { twitterBase } from '../base' -import { getAvatar, getBioDescription, getNickname, getTwitterId } from '../utils/user' +import { getAvatar, getBioDescription, getNickname, getTwitterId, getPersonalHomepage } from '../utils/user' import { delay } from '@masknet/shared-base' function resolveLastRecognizedIdentityInner( @@ -43,6 +44,7 @@ function resolveCurrentVisitingIdentityInner( const assign = async () => { await delay(500) const bio = getBioDescription() + const homepage = getPersonalHomepage() const nickname = getNickname() const handle = getTwitterId() const avatar = getAvatar() @@ -53,6 +55,13 @@ function resolveCurrentVisitingIdentityInner( avatar, bio, } + Services.Helper.resolveTCOLink(homepage).then((link) => { + if (cancel?.aborted || !link) return + ref.value = { + ...ref.value, + homepage: link, + } + }) } const createWatcher = (selector: LiveSelector) => { const watcher = new MutationObserverWatcher(selector) diff --git a/packages/mask/src/social-network-adaptor/twitter.com/injection/EnhancedProfile.tsx b/packages/mask/src/social-network-adaptor/twitter.com/injection/EnhancedProfile.tsx index 4fe75913319b..00231ade0736 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/injection/EnhancedProfile.tsx +++ b/packages/mask/src/social-network-adaptor/twitter.com/injection/EnhancedProfile.tsx @@ -1,5 +1,4 @@ import { MutationObserverWatcher } from '@dimensiondev/holoflows-kit' -import { getMaskColor, makeStyles } from '@masknet/theme' import { EnhancedProfilePage } from '../../../plugins/Profile/SNSAdaptor/EnhancedProfile' import { createReactRootShadowed, startWatch } from '../../../utils' import { @@ -35,32 +34,6 @@ function getStyleProps() { } } -const useStyles = makeStyles()((theme) => { - const props = getStyleProps() - - return { - text: { - paddingTop: 29, - paddingBottom: 29, - '& > p': { - fontSize: 28, - fontFamily: props.fontFamily, - fontWeight: 700, - color: getMaskColor(theme).textPrimary, - }, - }, - button: { - backgroundColor: props.backgroundColor, - color: 'white', - marginTop: 18, - '&:hover': { - backgroundColor: props.backgroundColor, - }, - }, - } -}) - export function EnhancedProfilePageAtTwitter() { - const { classes } = useStyles() - return + return } 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 ba73bedacd5c..36fd4d9b525a 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 @@ -123,6 +123,8 @@ export const newPostButtonSelector = () => querySelector('[data-testid="SideN export const bioDescriptionSelector = () => querySelector('[data-testid="UserDescription"]') +export const personalHomepageSelector = () => querySelector('[data-testid="UserUrl"]') + export const bioPageUserNickNameSelector = () => querySelector('[data-testid="UserDescription"]') .map((x) => x.parentElement?.parentElement?.previousElementSibling) diff --git a/packages/mask/src/social-network-adaptor/twitter.com/utils/user.ts b/packages/mask/src/social-network-adaptor/twitter.com/utils/user.ts index 58ed870e5454..e0bcdce2f9d0 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/utils/user.ts +++ b/packages/mask/src/social-network-adaptor/twitter.com/utils/user.ts @@ -1,6 +1,11 @@ import { isNull } from 'lodash-unified' import type { SocialNetwork } from '../../../social-network' -import { bioDescriptionSelector, searchAvatarSelector, searchNickNameSelector } from './selector' +import { + bioDescriptionSelector, + searchAvatarSelector, + searchNickNameSelector, + personalHomepageSelector, +} from './selector' import { collectNodeText } from '../../../utils' /** @@ -42,6 +47,11 @@ export const getBioDescription = () => { return node ? collectNodeText(node) : '' } +export const getPersonalHomepage = () => { + const node = personalHomepageSelector().evaluate() + return node?.getAttribute('href') || '' +} + export const getAvatar = () => { const node = searchAvatarSelector().evaluate() as HTMLImageElement if (!node) return '' diff --git a/packages/mask/src/social-network/types.ts b/packages/mask/src/social-network/types.ts index 61fcb0456389..aee452c846e7 100644 --- a/packages/mask/src/social-network/types.ts +++ b/packages/mask/src/social-network/types.ts @@ -216,7 +216,7 @@ export namespace SocialNetworkUI { getSearchedKeyword?(): string } export type ProfileUI = { bioContent: string } - export type IdentityResolved = Pick + export type IdentityResolved = Pick /** Resolve the information of who am I on the current network. */ export interface IdentityResolveProvider { diff --git a/packages/mask/src/utils/collectNodeText.ts b/packages/mask/src/utils/collectNodeText.ts index ecfce7532e0d..bf4351a68c83 100644 --- a/packages/mask/src/utils/collectNodeText.ts +++ b/packages/mask/src/utils/collectNodeText.ts @@ -21,7 +21,7 @@ export function collectNodeText(node: HTMLElement | null | undefined, options: C const result = options.onHTMLAnchorElement?.(each) if (result?.some) return result.val const href = each.getAttribute('href') - return href || each.innerText + return [href, each.innerText].join(' ') } if (each instanceof HTMLImageElement) { const src = each.getAttribute('src') diff --git a/packages/shared/src/UI/components/MiniNetworkSelector/index.tsx b/packages/shared/src/UI/components/MiniNetworkSelector/index.tsx index f5d34c83835b..a74da5c81d88 100644 --- a/packages/shared/src/UI/components/MiniNetworkSelector/index.tsx +++ b/packages/shared/src/UI/components/MiniNetworkSelector/index.tsx @@ -77,6 +77,7 @@ export const MiniNetworkSelector = memo( width: size, height: size, minWidth: size, + minHeight: size, lineHeight: `${size}px`, }} onClick={() => onSelect(null)}> diff --git a/packages/web3-shared/evm/context/type.ts b/packages/web3-shared/evm/context/type.ts index a73865ff67f0..17245f2c789e 100644 --- a/packages/web3-shared/evm/context/type.ts +++ b/packages/web3-shared/evm/context/type.ts @@ -14,7 +14,6 @@ import type { NonFungibleAssetProvider, Transaction, AddressName, - AddressNameType, CryptoPrice, BalanceOfChains, ERC721TokenCollectionInfo, @@ -45,21 +44,30 @@ export interface Web3ProviderType { getAssetsList: (address: string, provider: FungibleAssetProvider, network?: NetworkType) => Promise getAssetsListNFT: ( - address: string, chainId: ChainId, + address: string, provider: NonFungibleAssetProvider, page?: number, size?: number, collection?: string, ) => Promise<{ assets: ERC721TokenDetailed[]; hasNextPage: boolean }> getCollectionsNFT: ( - address: string, chainId: ChainId, + address: string, provider: NonFungibleAssetProvider, page?: number, size?: number, ) => Promise<{ collections: ERC721TokenCollectionInfo[]; hasNextPage: boolean }> - getAddressNamesList: (twitterId: string, addressNameType: AddressNameType) => Promise + getAddressNamesList: (identity: { + identifier: { + userId: string + network: string + } + avatar?: string + bio?: string + nickname?: string + homepage?: string + }) => Promise getTransactionList: ( address: string, network: NetworkType, diff --git a/packages/web3-shared/evm/hooks/index.ts b/packages/web3-shared/evm/hooks/index.ts index 87be8ec990a7..035493c3d275 100644 --- a/packages/web3-shared/evm/hooks/index.ts +++ b/packages/web3-shared/evm/hooks/index.ts @@ -53,6 +53,7 @@ export * from './useAssetsMerged' export * from './useAssetsFromProvider' export * from './useAssets' export * from './useAssetsByTokenList' +export * from './useCollections' export * from './useCollectibles' export * from './useTransactions' export * from './useGetPastLogsParams' @@ -69,4 +70,3 @@ export * from './useERC20Tokens' export * from './useTrustedERC20Tokens' export * from './useTrustERC20TokenCallback' export * from './useAddERC20TokenCallback' -export * from './useEthereumName' diff --git a/packages/web3-shared/evm/hooks/useAddressNames.ts b/packages/web3-shared/evm/hooks/useAddressNames.ts index 6e4b731ee200..b1fb3d98e967 100644 --- a/packages/web3-shared/evm/hooks/useAddressNames.ts +++ b/packages/web3-shared/evm/hooks/useAddressNames.ts @@ -1,11 +1,19 @@ import { useAsyncRetry } from 'react-use' -import { AddressNameType } from '../types' import { useWeb3Context } from '../context' -export function useAddressNames(twitterId: string) { +export function useAddressNames(identity: { + identifier: { + userId: string + network: string + } + avatar?: string + bio?: string + nickname?: string + homepage?: string +}) { const { getAddressNamesList } = useWeb3Context() return useAsyncRetry(async () => { - return getAddressNamesList(twitterId, AddressNameType.ENS) - }, [twitterId]) + return getAddressNamesList(identity) + }, [identity, getAddressNamesList]) } diff --git a/packages/web3-shared/evm/hooks/useCollectibles.ts b/packages/web3-shared/evm/hooks/useCollectibles.ts index 7c22b67d2a9f..332c025a6e54 100644 --- a/packages/web3-shared/evm/hooks/useCollectibles.ts +++ b/packages/web3-shared/evm/hooks/useCollectibles.ts @@ -1,39 +1,13 @@ -import { ChainId, NonFungibleAssetProvider, ERC721TokenCollectionInfo, ERC721TokenDetailed } from '../types' import { useAsyncRetry } from 'react-use' -import { useWeb3Context } from '../context' import { uniqWith } from 'lodash-unified' +import { useWeb3Context } from '../context' +import type { ChainId, NonFungibleAssetProvider, ERC721TokenDetailed } from '../types' import { isSameAddress } from '../utils' import type { AsyncStateRetry } from 'react-use/lib/useAsyncRetry' -export function useCollections( - address: string, - chainId: ChainId | null, - provider: NonFungibleAssetProvider, - page: number, - size: number, -): AsyncStateRetry<{ - collections: ERC721TokenCollectionInfo[] - hasNextPage: boolean -}> { - const { getCollectionsNFT } = useWeb3Context() - - return useAsyncRetry(async () => { - if (!address) { - return { - collections: [], - hasNextPage: false, - } - } - - const result = await getCollectionsNFT(address.toLowerCase(), chainId ?? ChainId.Mainnet, provider, page, size) - - return result - }, [address, provider, page, chainId]) -} - export function useCollectibles( + chainId: ChainId, address: string, - chainId: ChainId | null, provider: NonFungibleAssetProvider, page: number, size: number, @@ -52,14 +26,7 @@ export function useCollectibles( } } - const result = await getAssetsListNFT( - address.toLowerCase(), - chainId ?? ChainId.Mainnet, - provider, - page, - size, - collection, - ) + const result = await getAssetsListNFT(chainId, address.toLowerCase(), provider, page, size, collection) return { collectibles: uniqWith( [ diff --git a/packages/web3-shared/evm/hooks/useCollections.ts b/packages/web3-shared/evm/hooks/useCollections.ts new file mode 100644 index 000000000000..a6bf3097b730 --- /dev/null +++ b/packages/web3-shared/evm/hooks/useCollections.ts @@ -0,0 +1,27 @@ +import { useAsyncRetry } from 'react-use' +import type { AsyncStateRetry } from 'react-use/lib/useAsyncRetry' +import type { ChainId, NonFungibleAssetProvider, ERC721TokenCollectionInfo } from '../types' +import { useWeb3Context } from '../context' + +export function useCollections( + chainId: ChainId, + address: string, + provider: NonFungibleAssetProvider, + page: number, + size: number, +): AsyncStateRetry<{ + collections: ERC721TokenCollectionInfo[] + hasNextPage: boolean +}> { + const { getCollectionsNFT } = useWeb3Context() + + return useAsyncRetry(async () => { + if (!address) { + return { + collections: [], + hasNextPage: false, + } + } + return getCollectionsNFT(chainId, address.toLowerCase(), provider, page, size) + }, [address, provider, page, chainId]) +} diff --git a/packages/web3-shared/evm/hooks/useEthereumName.ts b/packages/web3-shared/evm/hooks/useEthereumName.ts deleted file mode 100644 index fb0ec31587ae..000000000000 --- a/packages/web3-shared/evm/hooks/useEthereumName.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { useEffect, useMemo, useState } from 'react' -import type { AsyncState } from 'react-use/lib/useAsyncFn' -import { EthereumNameType } from '../constants' -import { useAddressNames } from './useAddressNames' -import { useResolveENS } from './useResolveENS' -import { useResolveUNS } from './useResolveUNS' - -const ENS_RE = /\S{1,256}\.(eth|kred|xyz|luxe)\b/ -const ENS_RE_FULL = new RegExp(`^${ENS_RE.source}$`) -const ADDRESS_FULL = /0x\w+/ -const ADDRESS = /^0x[\dA-Fa-f]{40}$/ - -export function useEthereumName(nickname: string, twitterId: string, bio: string) { - const [ethereumName, setEthereumName] = useState('') - - useEffect(() => { - const matched = bio.match(ENS_RE) - setEthereumName(matched ? matched[0] : '') - }, [bio]) - const name = useMemo(() => { - const matched = nickname.match(ENS_RE) - if (matched) { - return matched[0] - } - if (ethereumName) return ethereumName - return twitterId ? `${twitterId}.eth` : '' - }, [ethereumName, nickname, twitterId]) - - return name -} - -export function useEthereumAddress(nickname: string, twitterId: string, bio: string) { - const [address, setAddress] = useState() - const name = useEthereumName(nickname, twitterId, bio) - const { value: addressENS, loading: loadingAddressENS } = useResolveENS(name) - const { value: addressUNS, loading: loadingAddressUNS } = useResolveUNS(name) - const { value: names = [], loading: loadingAddressNames } = useAddressNames(twitterId) - - const ownerAddress = names.find((x) => x.ownerAddress)?.ownerAddress || address - - useEffect(() => { - setAddress('') - const matched = bio.match(ADDRESS_FULL) - if (matched && ADDRESS.test(matched[0])) setAddress(matched[0]) - }, [bio]) - - const isLoading = loadingAddressENS || loadingAddressUNS || loadingAddressNames - - return { - loading: isLoading, - value: isLoading - ? undefined - : { - type: addressENS - ? EthereumNameType.ENS_TYPE - : addressUNS - ? EthereumNameType.UNS_TYPE - : EthereumNameType.DEFAULT, - name: addressENS || addressUNS ? name : '', - address: isLoading ? '' : addressENS ?? addressUNS ?? ownerAddress ?? '', - }, - error: undefined, - } as AsyncState<{ - type: string - name: string - address: string - }> -} diff --git a/packages/web3-shared/evm/package.json b/packages/web3-shared/evm/package.json index 766d1738ad2b..090f9549d9ee 100644 --- a/packages/web3-shared/evm/package.json +++ b/packages/web3-shared/evm/package.json @@ -5,6 +5,7 @@ "types": "./dist/index.d.ts", "dependencies": { "@ensdomains/address-encoder": "^0.2.6", + "@masknet/shared-base": "workspace:*", "@masknet/web3-constants": "workspace:*", "@masknet/web3-contracts": "workspace:*", "@masknet/web3-shared-base": "workspace:*", diff --git a/packages/web3-shared/evm/types/index.ts b/packages/web3-shared/evm/types/index.ts index d7afb2cac29a..2e8783fc0c3a 100644 --- a/packages/web3-shared/evm/types/index.ts +++ b/packages/web3-shared/evm/types/index.ts @@ -609,15 +609,19 @@ export interface Transaction { //#region address name export enum AddressNameType { - ENS = 'ENS', - UNS = 'UNS', - DNS = 'DNS', + ADDRESS = 1, + ENS = 2, + UNS = 3, + DNS = 4, + RSS3 = 5, + GUN = 6, + THE_GRAPH = 7, } export interface AddressName { + type: AddressNameType label: string - ownerAddress: string - resolvedAddress?: string + resolvedAddress: string } //#endregion diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93fdb91640ef..e336a06acef1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -331,6 +331,7 @@ importers: '@uniswap/sdk-core': ^3.0.1 '@uniswap/v2-sdk': 3.0.0-alpha.2 '@uniswap/v3-sdk': ^3.3.2 + '@unstoppabledomains/resolution': ^5.0.1 '@walletconnect/client': ^1.2.2 anchorme: ^2.1.2 arweave: 1.10.13 @@ -459,6 +460,7 @@ importers: '@uniswap/sdk-core': 3.0.1 '@uniswap/v2-sdk': 3.0.0-alpha.2 '@uniswap/v3-sdk': 3.3.2 + '@unstoppabledomains/resolution': 5.0.1 '@walletconnect/client': 1.4.1 anchorme: 2.1.2 arweave: 1.10.13 @@ -876,6 +878,7 @@ importers: packages/web3-shared/evm: specifiers: '@ensdomains/address-encoder': ^0.2.6 + '@masknet/shared-base': workspace:* '@masknet/web3-constants': workspace:* '@masknet/web3-contracts': workspace:* '@masknet/web3-shared-base': workspace:* @@ -899,6 +902,7 @@ importers: web3-utils: 1.5.2 dependencies: '@ensdomains/address-encoder': 0.2.6 + '@masknet/shared-base': link:../../shared-base '@masknet/web3-constants': link:../../web3-constants '@masknet/web3-contracts': link:../../web3-contracts '@masknet/web3-shared-base': link:../base @@ -24764,6 +24768,26 @@ packages: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} dev: true + github.com/ProjectOpenSea/wyvern-js/1634ad0e2f9961accca62edbf4ac624effa18552: + resolution: {tarball: https://codeload.github.com/ProjectOpenSea/wyvern-js/tar.gz/1634ad0e2f9961accca62edbf4ac624effa18552} + name: wyvern-js + version: 3.2.0 + dependencies: + '@0xproject/abi-gen': 0.1.7 + '@0xproject/assert': 0.0.11 + '@0xproject/json-schemas': 0.7.24 + '@0xproject/utils': 0.1.3 + '@0xproject/web3-wrapper': 0.1.14 + 0x.js: 0.29.2 + bn.js: 4.12.0 + ethereumjs-abi: github.com/ProjectWyvern/ethereumjs-abi/3d2d89641a6ad5984929b6ca4b646452ec74f73d + ethereumjs-util: 5.2.1 + json-loader: 0.5.7 + jsonschema: 1.4.0 + lodash: 4.17.21 + web3: 0.20.7 + dev: false + github.com/ProjectOpenSea/wyvern-js/fabb7660f23f2252c141077e32193d281036299e: resolution: {tarball: https://codeload.github.com/ProjectOpenSea/wyvern-js/tar.gz/fabb7660f23f2252c141077e32193d281036299e} name: wyvern-js @@ -24806,7 +24830,7 @@ packages: bignumber.js: 6.0.0 typed-promisify: 0.4.0 web3-provider-engine: 13.8.0 - wyvern-js: github.com/ProjectOpenSea/wyvern-js/fabb7660f23f2252c141077e32193d281036299e + wyvern-js: github.com/ProjectOpenSea/wyvern-js/1634ad0e2f9961accca62edbf4ac624effa18552 transitivePeerDependencies: - debug dev: false