diff --git a/cspell.json b/cspell.json index 8a4ca5c8a3ea..b01dfbf3f611 100644 --- a/cspell.json +++ b/cspell.json @@ -354,6 +354,7 @@ "CANFT", "Kodav", "koda", + "Enjin", "poap", "irss", "CELO" diff --git a/packages/mask/.webpack/config.ts b/packages/mask/.webpack/config.ts index 12d7dd88db53..7a6dd9c49095 100644 --- a/packages/mask/.webpack/config.ts +++ b/packages/mask/.webpack/config.ts @@ -71,7 +71,7 @@ export function createConfiguration(rawFlags: BuildFlags): Configuration { '@masknet/theme': join(__dirname, '../../theme/src/'), '@masknet/icons': join(__dirname, '../../icons/index.ts'), '@masknet/web3-kit': join(__dirname, '../../web3-kit/src/'), - '@masknet/web3-providers': join(__dirname, '../../web3-providers/src/'), + '@masknet/web3-providers': join(__dirname, '../../web3-providers/src'), '@masknet/web3-shared-evm': join(__dirname, '../../web3-shared/evm/'), '@masknet/web3-shared-flow': join(__dirname, '../../web3-shared/flow/'), '@masknet/web3-shared-solana': join(__dirname, '../../web3-shared/solana/'), diff --git a/packages/mask/package.json b/packages/mask/package.json index 9bf8bbe0fe30..2dcf205d186c 100644 --- a/packages/mask/package.json +++ b/packages/mask/package.json @@ -33,6 +33,7 @@ "@masknet/web3-contracts": "workspace:*", "@masknet/web3-shared-base": "workspace:*", "@masknet/web3-shared-evm": "workspace:*", + "@masknet/web3-providers": "workspace:*", "@msgpack/msgpack": "^2.7.1", "@servie/events": "^3.0.0", "@sinonjs/text-encoding": "^0.7.1", diff --git a/packages/mask/src/extension/popups/pages/Swap/SwapBox/index.tsx b/packages/mask/src/extension/popups/pages/Swap/SwapBox/index.tsx index 579f2670092d..b97e2a3def04 100644 --- a/packages/mask/src/extension/popups/pages/Swap/SwapBox/index.tsx +++ b/packages/mask/src/extension/popups/pages/Swap/SwapBox/index.tsx @@ -1,15 +1,11 @@ -import type { TradeProvider } from '@masknet/public-api' import { useChainId } from '@masknet/web3-shared-evm' -import { useCallback, useMemo } from 'react' +import { useMemo } from 'react' import { useHistory, useLocation } from 'react-router-dom' import { useUpdateEffect } from 'react-use' -import { currentTradeProviderSettings } from '../../../../../plugins/Trader/settings' -import type { FootnoteMenuOption } from '../../../../../plugins/Trader/SNSAdaptor/trader/FootnoteMenu' import { Trader } from '../../../../../plugins/Trader/SNSAdaptor/trader/Trader' import { TradeContext, useTradeContext } from '../../../../../plugins/Trader/trader/useTradeContext' -import { useAvailableTraderProviders } from '../../../../../plugins/Trader/trending/useAvailableTraderProviders' import { useCurrentTradeProvider } from '../../../../../plugins/Trader/trending/useCurrentTradeProvider' -import { Coin, TagType } from '../../../../../plugins/Trader/types' +import type { Coin } from '../../../../../plugins/Trader/types' import { PopupRoutes } from '@masknet/shared-base' export function SwapBox() { @@ -18,7 +14,6 @@ export function SwapBox() { const chainId = useChainId() const tradeProvider = useCurrentTradeProvider(chainId) const tradeContext = useTradeContext(tradeProvider) - const { value: tradeProviders = [] } = useAvailableTraderProviders(TagType.CASH, 'MASK') const coin = useMemo(() => { if (!location.search) return undefined @@ -36,10 +31,6 @@ export function SwapBox() { history.replace(PopupRoutes.Swap) }, [chainId]) - const onTradeProviderChange = useCallback((option: FootnoteMenuOption) => { - currentTradeProviderSettings.value = option.value as TradeProvider - }, []) - return ( diff --git a/packages/mask/src/plugins/Avatar/SNSAdaptor/AddNFT.tsx b/packages/mask/src/plugins/Avatar/SNSAdaptor/AddNFT.tsx index 22b08d71ba09..a01ed117f905 100644 --- a/packages/mask/src/plugins/Avatar/SNSAdaptor/AddNFT.tsx +++ b/packages/mask/src/plugins/Avatar/SNSAdaptor/AddNFT.tsx @@ -52,7 +52,7 @@ export function AddNFT(props: AddNFTProps) { createNFT(address, tokenId) .then((token) => { - if (!isSameAddress(token.info.owner, account)) { + if (!token || !isSameAddress(token?.info.owner, account)) { setMessage(t('nft_owner_hint')) return } diff --git a/packages/mask/src/plugins/Avatar/utils/index.ts b/packages/mask/src/plugins/Avatar/utils/index.ts index 8b3ce0308635..562e0c5fae7c 100644 --- a/packages/mask/src/plugins/Avatar/utils/index.ts +++ b/packages/mask/src/plugins/Avatar/utils/index.ts @@ -1,75 +1,34 @@ -import BigNumber from 'bignumber.js' -import { head, isNull } from 'lodash-unified' -import { PluginCollectibleRPC } from '../../Collectible/messages' -import { getLastSalePrice, getOrderUnitPrice, getOrderUSDPrice } from '../../Collectible/utils' -import { ChainId, createERC721Token, EthereumTokenType } from '@masknet/web3-shared-evm' -import type { AssetOrder } from '../../Collectible/types' +import { isNull } from 'lodash-unified' +import { ChainId, NonFungibleAssetProvider } from '@masknet/web3-shared-evm' +import { EVM_RPC } from '../../EVM/messages' import Services from '../../../extension/service' +import { getOrderUnitPrice } from '@masknet/web3-providers' +import { ONE } from '@masknet/web3-shared-base' export async function getNFT(address: string, tokenId: string) { - const asset = await PluginCollectibleRPC.getAsset(address, tokenId, ChainId.Mainnet) - - let orders: AssetOrder[] = [] - if (asset.sell_orders?.length) { - orders = asset.sell_orders - } else if (asset.orders?.length) { - orders = asset.orders - } else if (asset.buy_orders?.length) { - orders = asset.buy_orders - } - - const order = head( - orders.sort((a, b) => - new BigNumber( - getOrderUSDPrice( - b.current_price, - b.payment_token_contract?.usd_price, - b.payment_token_contract?.decimals, - ) ?? 0, - ) - .minus( - getOrderUSDPrice( - a.current_price, - a.payment_token_contract?.usd_price, - a.payment_token_contract?.decimals, - ) ?? 0, - ) - .toNumber(), - ), - ) - + const asset = await EVM_RPC.getAsset({ + address, + tokenId, + chainId: ChainId.Mainnet, + provider: NonFungibleAssetProvider.OPENSEA, + }) + const amount = + getOrderUnitPrice( + asset?.desktopOrder?.current_price, + asset?.desktopOrder?.payment_token_contract?.decimals ?? 0, + asset?.desktopOrder?.quantity ?? '1', + ) ?? ONE return { - amount: order - ? new BigNumber( - getOrderUnitPrice(order.current_price, order.payment_token_contract?.decimals, order.quantity) ?? 0, - ).toFixed() - : getLastSalePrice(asset.last_sale) ?? '0', - name: asset.name, - symbol: order?.payment_token_contract?.symbol ?? asset.last_sale?.payment_token?.symbol ?? 'ETH', - image: asset.image_url ?? asset.image_preview_url ?? '', - owner: asset.owner.address, + amount: amount.toFixed(), + name: asset?.name ?? '', + symbol: asset?.desktopOrder?.payment_token_contract?.symbol ?? 'ETH', + image: asset?.image_url ?? '', + owner: asset?.top_ownerships[0].owner.address ?? '', } } export async function createNFT(address: string, tokenId: string) { - const asset = await PluginCollectibleRPC.getAsset(address, tokenId, ChainId.Mainnet) - const token = createERC721Token( - { - chainId: ChainId.Mainnet, - type: EthereumTokenType.ERC721, - name: asset.asset_contract.name, - symbol: asset.asset_contract.token_symbol, - address: asset.asset_contract.address, - }, - { - name: asset.name, - description: asset.description, - mediaUrl: asset.image_url ?? asset.image_preview_url ?? '', - owner: asset.top_ownerships[0].owner.address, - }, - tokenId, - ) - return token + return EVM_RPC.getNFT({ address, tokenId, chainId: ChainId.Mainnet, provider: NonFungibleAssetProvider.OPENSEA }) } export async function getImage(image: string): Promise { diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/ActionBar.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/ActionBar.tsx index 32132acc06d3..a51866d7b24a 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/ActionBar.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/ActionBar.tsx @@ -26,7 +26,8 @@ export interface ActionBarProps {} export function ActionBar(props: ActionBarProps) { const { t } = useI18N() const { classes } = useStyles() - const { asset, token, assetOrder } = CollectibleState.useContainer() + const { asset, assetOrder } = CollectibleState.useContainer() + const assets = asset.value const { open: openCheckoutDialog, @@ -43,7 +44,7 @@ export function ActionBar(props: ActionBarProps) { if (!asset.value) return null return ( - {!asset.value.is_owner && asset.value.is_auction && assetOrder.value ? ( + {!asset.value.isOwner && asset.value.is_auction && assetOrder.value ? ( ) : null} - {!asset.value.is_owner && asset.value.is_auction ? ( + {!asset.value.isOwner && asset.value.is_auction ? ( ) : null} - {!asset.value.is_owner && !asset.value.is_auction ? ( + {!asset.value.isOwner && !asset.value.is_auction ? ( ) : null} - {asset.value.is_owner ? ( + {assets?.isOwner ? ( ) : null} diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/ArticleTab.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/ArticleTab.tsx index a71f6e132023..c395e666196d 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/ArticleTab.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/ArticleTab.tsx @@ -47,10 +47,10 @@ export function ArticleTab(props: ArticleTabProps) {
{asset.value.animation_url ? ( - + ) : ( - + )}
diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/CheckoutDialog.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/CheckoutDialog.tsx index 7f416c601a85..170d8ad16dcb 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/CheckoutDialog.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/CheckoutDialog.tsx @@ -19,10 +19,10 @@ import { useI18N } from '../../../utils' import { useRemoteControlledDialog } from '@masknet/shared' import ActionButton, { ActionButtonPromise } from '../../../extension/options-page/DashboardComponents/ActionButton' import { EthereumWalletConnectedBoundary } from '../../../web3/UI/EthereumWalletConnectedBoundary' -import type { useAsset } from '../hooks/useAsset' import { PluginCollectibleRPC } from '../messages' import { PluginTraderMessages } from '../../Trader/messages' import { CheckoutOrder } from './CheckoutOrder' +import type { useAsset } from '../../EVM/hooks/useAsset' import type { useAssetOrder } from '../hooks/useAssetOrder' const useStyles = makeStyles()((theme) => { @@ -55,16 +55,15 @@ const useStyles = makeStyles()((theme) => { export interface CheckoutDialogProps { asset?: ReturnType - assetOrder?: ReturnType + order: ReturnType open: boolean onClose: () => void } export function CheckoutDialog(props: CheckoutDialogProps) { - const { asset, open, onClose, assetOrder } = props + const { asset, open, onClose, order } = props const isAuction = asset?.value?.is_auction ?? false const isVerified = asset?.value?.is_verified ?? false - const { t } = useI18N() const { classes } = useStyles() @@ -76,13 +75,14 @@ export function CheckoutDialog(props: CheckoutDialogProps) { const onCheckout = useCallback(async () => { if (!asset?.value) return if (!asset.value.token_id || !asset.value.token_address) return - if (!assetOrder?.value) return + if (!order.value) return + await PluginCollectibleRPC.fulfillOrder({ - order: assetOrder.value, + order: order.value, accountAddress: account, recipientAddress: account, }) - }, [asset?.value, account, assetOrder?.value]) + }, [asset?.value, account, order?.value]) const { openDialog: openSwapDialog } = useRemoteControlledDialog(PluginTraderMessages.swapDialogUpdated) @@ -103,7 +103,7 @@ export function CheckoutDialog(props: CheckoutDialogProps) {
)} - + {isVerified ? null : ( <> - {asset?.value?.is_order_weth ? ( + {asset?.value?.isOrderWeth ? ( ({ @@ -25,8 +24,8 @@ export interface CheckoutOrderProps { export function CheckoutOrder(props: CheckoutOrderProps) { const { t } = useI18N() - const { asset, assetOrder } = props - const order = assetOrder?.value as Order | undefined + const { asset } = props + const order = asset?.value?.desktopOrder const { classes } = useStyles() const chainId = useChainId() @@ -45,7 +44,7 @@ export function CheckoutOrder(props: CheckoutOrderProps) {
- +
{asset.value.collection_name ?? ''} {asset.value.token_address && asset.value.token_id ? ( diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/Collectible.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/Collectible.tsx index dc474bf874d8..fd5b10e16a56 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/Collectible.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/Collectible.tsx @@ -29,16 +29,16 @@ import { HistoryTab } from './HistoryTab' import { CollectibleState } from '../hooks/useCollectibleState' import { CollectibleCard } from './CollectibleCard' import { CollectibleProviderIcon } from './CollectibleProviderIcon' -import { CollectibleProvider, CollectibleTab } from '../types' -import { currentCollectibleProviderSettings } from '../settings' +import { CollectibleTab } from '../types' import { MaskTextIcon } from '../../../resources/MaskIcon' import { resolveAssetLinkOnOpenSea, resolveCollectibleProviderName } from '../pipes' import { ActionBar } from './ActionBar' -import { useChainId } from '@masknet/web3-shared-evm' +import { NonFungibleAssetProvider, useChainId } from '@masknet/web3-shared-evm' import { getEnumAsArray } from '@dimensiondev/kit' import { FootnoteMenu, FootnoteMenuOption } from '../../Trader/SNSAdaptor/trader/FootnoteMenu' import { LoadingAnimation } from '@masknet/shared' import { Markdown } from '../../Snapshot/SNSAdaptor/Markdown' +import { currentNonFungibleAssetProviderSettings } from '../settings' const useStyles = makeStyles()((theme) => { return { @@ -156,16 +156,16 @@ export function Collectible(props: CollectibleProps) { const { asset, provider, tabIndex, setTabIndex } = CollectibleState.useContainer() //#region sync with settings - const collectibleProviderOptions = getEnumAsArray(CollectibleProvider) + const collectibleProviderOptions = getEnumAsArray(NonFungibleAssetProvider) const onDataProviderChange = useCallback((option: FootnoteMenuOption) => { - currentCollectibleProviderSettings.value = option.value as CollectibleProvider + currentNonFungibleAssetProviderSettings.value = option.value as NonFungibleAssetProvider }, []) //#endregion //#region provider switcher const CollectibleProviderSwitcher = useSettingsSwitcher( - currentCollectibleProviderSettings, - getEnumAsArray(CollectibleProvider).map((x) => x.value), + currentNonFungibleAssetProviderSettings, + getEnumAsArray(NonFungibleAssetProvider).map((x) => x.value), resolveCollectibleProviderName, ) //#endregion @@ -337,7 +337,7 @@ export function Collectible(props: CollectibleProps) { )} - {provider === CollectibleProvider.OPENSEA ? : null} + {provider === NonFungibleAssetProvider.OPENSEA ? : null} ) } diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/CollectibleProviderIcon.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/CollectibleProviderIcon.tsx index 8d44ff92dc3c..9098ddd5e17b 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/CollectibleProviderIcon.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/CollectibleProviderIcon.tsx @@ -2,7 +2,7 @@ import { makeStyles } from '@masknet/theme' import { OpenSeaIcon } from '../../../resources/OpenSeaIcon' import { RaribleIcon } from '../../../resources/RaribleIcon' import { unreachable } from '@dimensiondev/kit' -import { CollectibleProvider } from '../types' +import { NonFungibleAssetProvider } from '@masknet/web3-shared-evm' const useStyles = makeStyles()({ opensea: { @@ -18,15 +18,16 @@ const useStyles = makeStyles()({ }) export interface CollectibleProviderIconProps { - provider: CollectibleProvider + provider: NonFungibleAssetProvider } export function CollectibleProviderIcon(props: CollectibleProviderIconProps) { const { classes } = useStyles() switch (props.provider) { - case CollectibleProvider.OPENSEA: + case NonFungibleAssetProvider.NFTSCAN: + case NonFungibleAssetProvider.OPENSEA: return - case CollectibleProvider.RARIBLE: + case NonFungibleAssetProvider.RARIBLE: return default: unreachable(props.provider) diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/HistoryTab/Row.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/HistoryTab/Row.tsx index b4673b7baa1f..a22391604bb0 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/HistoryTab/Row.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/HistoryTab/Row.tsx @@ -2,14 +2,15 @@ import { Avatar, Link, TableCell, TableRow, Typography } from '@mui/material' import { makeStyles } from '@masknet/theme' import LinkIcon from '@mui/icons-material/Link' import { FormattedBalance } from '@masknet/shared' -import { formatBalance } from '@masknet/web3-shared-evm' +import { formatBalance, NonFungibleAssetProvider } from '@masknet/web3-shared-evm' import { formatElapsed } from '../../../Wallet/formatter' import { useMemo } from 'react' -import { CollectibleProvider, NFTHistory, OpenSeaAssetEventType, RaribleEventType } from '../../types' import { CollectibleState } from '../../hooks/useCollectibleState' import { resolveOpenSeaAssetEventType, resolveRaribleAssetEventType } from '../../pipes' import { Account } from '../Account' -import { getOrderUnitPrice } from '../../utils' +import { getOrderUnitPrice, NonFungibleTokenAPI } from '@masknet/web3-providers' +import type { OpenSeaAssetEventType } from '../../types/opensea' +import type { RaribleEventType } from '../../types' const useStyles = makeStyles()((theme) => { return { @@ -41,7 +42,7 @@ const useStyles = makeStyles()((theme) => { }) interface Props { - event: NFTHistory + event: NonFungibleTokenAPI.History isDifferenceToken?: boolean } @@ -50,16 +51,20 @@ export function Row({ event, isDifferenceToken }: Props) { const { provider } = CollectibleState.useContainer() const unitPrice = useMemo(() => { - if (provider === CollectibleProvider.RARIBLE || !isDifferenceToken || !event.price) return null + if (provider === NonFungibleAssetProvider.RARIBLE || !isDifferenceToken || !event.price) return null - return getOrderUnitPrice(event.price.price ?? 0, event.price.paymentToken?.decimals, event.price.quantity) + return getOrderUnitPrice( + event.price.price ?? 0, + event.price.paymentToken?.decimals, + event.price.quantity, + )?.toString() }, [event, isDifferenceToken, provider]) return ( - {provider === CollectibleProvider.OPENSEA + {provider === NonFungibleAssetProvider.OPENSEA ? resolveOpenSeaAssetEventType( event.eventType as OpenSeaAssetEventType, event.accountPair.from?.username, @@ -74,9 +79,9 @@ export function Row({ event, isDifferenceToken }: Props) { {event.price?.asset?.image_url && ( {event.price.paymentToken?.symbol} )} @@ -97,7 +102,7 @@ export function Row({ event, isDifferenceToken }: Props) { ) : ( - {event.price && provider === CollectibleProvider.OPENSEA + {event.price && provider === NonFungibleAssetProvider.OPENSEA ? formatBalance(event.price.quantity, event.price?.asset?.decimals ?? 0) : event.price?.quantity ?? ''} diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/HistoryTab/index.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/HistoryTab/index.tsx index 58df9742162f..3fa216c01999 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/HistoryTab/index.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/HistoryTab/index.tsx @@ -5,9 +5,9 @@ import { useI18N } from '../../../../utils' import { CollectibleTab } from '../CollectibleTab' import { CollectibleState } from '../../hooks/useCollectibleState' import { Row } from './Row' -import { CollectibleProvider } from '../../types' import { TableListPagination } from '../Pagination' import { LoadingAnimation } from '@masknet/shared' +import { NonFungibleAssetProvider } from '@masknet/web3-shared-evm' const useStyles = makeStyles()((theme) => { return { @@ -46,8 +46,8 @@ export function HistoryTab(props: HistoryTabProps) { //#region If there is a different asset, the unit price and quantity should be displayed const isDifferenceToken = useMemo(() => { - if (provider === CollectibleProvider.OPENSEA) - return events.value?.data.some((item) => item.price?.paymentToken?.symbol !== 'ETH') + if (provider === NonFungibleAssetProvider.OPENSEA) + return events.value?.data.some((item) => item.price?.asset?.asset_contract.symbol !== 'ETH') else return false }, [events.value, provider]) //#endregion @@ -103,7 +103,7 @@ export function HistoryTab(props: HistoryTabProps) { ))} - {(provider === CollectibleProvider.OPENSEA && events.value.data.length) || eventPage > 0 ? ( + {(provider === NonFungibleAssetProvider.OPENSEA && events.value.data.length) || eventPage > 0 ? ( setEventPage((prev) => prev - 1)} handleNextClick={() => setEventPage((prev) => prev + 1)} diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingByHighestBidCard.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingByHighestBidCard.tsx index 9e3f718c6412..a95a27a5861b 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingByHighestBidCard.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingByHighestBidCard.tsx @@ -15,10 +15,11 @@ import { ActionButtonPromise } from '../../../extension/options-page/DashboardCo import { SelectTokenAmountPanel } from '../../ITO/SNSAdaptor/SelectTokenAmountPanel' import { EthereumWalletConnectedBoundary } from '../../../web3/UI/EthereumWalletConnectedBoundary' import { DateTimePanel } from '../../../web3/UI/DateTimePanel' -import type { useAsset } from '../hooks/useAsset' import { PluginCollectibleRPC } from '../messages' import { toAsset } from '../helpers' import getUnixTime from 'date-fns/getUnixTime' +import type { useAsset } from '../../EVM/hooks' +import { isWyvernSchemaName } from '../utils' const useStyles = makeStyles()((theme) => ({ footer: { @@ -49,7 +50,6 @@ export interface ListingByHighestBidCardProps { export function ListingByHighestBidCard(props: ListingByHighestBidCardProps) { const { asset, tokenWatched, paymentTokens, open, onClose } = props const { amount, token, balance, setAmount, setToken } = tokenWatched - const { t } = useI18N() const { classes } = useStyles() @@ -71,11 +71,12 @@ export function ListingByHighestBidCard(props: ListingByHighestBidCardProps) { if (!asset.value.token_id || !asset.value.token_address) return if (!token?.value) return if (token.value.type !== EthereumTokenType.ERC20) return + const schemaName = asset.value.asset_contract?.schemaName await PluginCollectibleRPC.createSellOrder({ asset: toAsset({ tokenId: asset.value.token_id, tokenAddress: asset.value.token_address, - schemaName: asset.value.asset_contract.schema_name, + schemaName: isWyvernSchemaName(schemaName) ? schemaName : undefined, }), accountAddress: account, startAmount: Number.parseFloat(amount), diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingByPriceCard.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingByPriceCard.tsx index 9cf6635025ca..e144467b72b5 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingByPriceCard.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingByPriceCard.tsx @@ -19,8 +19,9 @@ import { EthereumWalletConnectedBoundary } from '../../../web3/UI/EthereumWallet import { DateTimePanel } from '../../../web3/UI/DateTimePanel' import { PluginCollectibleRPC } from '../messages' import { toAsset } from '../helpers' -import type { useAsset } from '../hooks/useAsset' import getUnixTime from 'date-fns/getUnixTime' +import type { useAsset } from '../../EVM/hooks' +import { isWyvernSchemaName } from '../utils' const useStyles = makeStyles()((theme) => { return { @@ -59,7 +60,6 @@ export interface ListingByPriceCardProps { export function ListingByPriceCard(props: ListingByPriceCardProps) { const { asset, tokenWatched, paymentTokens, open, onClose } = props const { amount, token, balance, setAmount, setToken } = tokenWatched - const { t } = useI18N() const { classes } = useStyles() @@ -101,11 +101,12 @@ export function ListingByPriceCard(props: ListingByPriceCardProps) { if (!asset.value.token_id || !asset.value.token_address) return if (!token?.value) return if (token.value.type !== EthereumTokenType.Native && token.value.type !== EthereumTokenType.ERC20) return + const schemaName = asset.value.asset_contract?.schemaName await PluginCollectibleRPC.createSellOrder({ asset: toAsset({ tokenId: asset.value.token_id, tokenAddress: asset.value.token_address, - schemaName: asset.value.asset_contract.schema_name, + schemaName: isWyvernSchemaName(schemaName) ? schemaName : undefined, }), accountAddress: account, startAmount: Number.parseFloat(amount), diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingTab.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingTab.tsx index 80b56018d904..010882d5743a 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingTab.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/ListingTab.tsx @@ -7,9 +7,8 @@ import { CollectibleState } from '../hooks/useCollectibleState' import { CollectibleTab } from './CollectibleTab' import { OrderRow } from './OrderRow' import { TableListPagination } from './Pagination' -import { CollectibleProvider } from '../types' import { LoadingTable } from './LoadingTable' -import { useAccount } from '@masknet/web3-shared-evm' +import { NonFungibleAssetProvider, useAccount } from '@masknet/web3-shared-evm' import { isZero } from '@masknet/web3-shared-base' const useStyles = makeStyles()((theme) => { @@ -45,14 +44,14 @@ export function ListingTab() { const { token, asset, provider, orders, orderPage, setOrderPage } = CollectibleState.useContainer() const isDifferenceToken = useMemo(() => { - if (provider === CollectibleProvider.OPENSEA) { + if (provider === NonFungibleAssetProvider.OPENSEA) { return ( - orders.some( + orders.value?.data.some( (item) => (item.payment_token_contract?.symbol !== 'WETH' && item.payment_token_contract?.symbol !== 'ETH') || (item.quantity && new BigNumber(item.quantity).toString() !== '1'), - ) && orders.filter((item) => isZero(item.expiration_time ?? 0)).length === 0 + ) && orders.value?.data.filter((item) => isZero(item.expiration_time ?? 0)).length === 0 ) } else { return false @@ -60,8 +59,8 @@ export function ListingTab() { }, [provider, orders]) const dataSource = useMemo(() => { - if (!asset.value || !orders.length) return [] - return orders.sort((a, b) => { + if (!orders.value || !orders.value?.data.length) return [] + return orders.value.data.sort((a, b) => { const current = new BigNumber(a.current_price ?? 0) const next = new BigNumber(b.current_price ?? 0) if (current.isLessThan(next)) return -1 @@ -108,7 +107,7 @@ export function ListingTab() { ) : ( <> {t('plugin_collectible_price')} - {provider === CollectibleProvider.OPENSEA ? ( + {provider === NonFungibleAssetProvider.OPENSEA ? ( {t('plugin_collectible_expiration')} ) : null} @@ -120,7 +119,7 @@ export function ListingTab() { ))} - {(provider === CollectibleProvider.OPENSEA && dataSource.length) || orderPage > 0 ? ( + {(provider === NonFungibleAssetProvider.OPENSEA && dataSource.length) || orderPage > 0 ? ( setOrderPage((prev) => prev - 1)} handleNextClick={() => setOrderPage((prev) => prev + 1)} diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/MakeOfferDialog.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/MakeOfferDialog.tsx index 8ae59c74ee91..bb554596441c 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/MakeOfferDialog.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/MakeOfferDialog.tsx @@ -28,15 +28,16 @@ import { UnreviewedWarning } from './UnreviewedWarning' import ActionButton, { ActionButtonPromise } from '../../../extension/options-page/DashboardComponents/ActionButton' import { SelectTokenAmountPanel } from '../../ITO/SNSAdaptor/SelectTokenAmountPanel' import { EthereumWalletConnectedBoundary } from '../../../web3/UI/EthereumWalletConnectedBoundary' -import type { useAsset } from '../hooks/useAsset' import { DateTimePanel } from '../../../web3/UI/DateTimePanel' import { PluginCollectibleRPC } from '../messages' import { toAsset } from '../helpers' import { PluginTraderMessages } from '../../Trader/messages' import { Trans } from 'react-i18next' import getUnixTime from 'date-fns/getUnixTime' +import type { useAsset } from '../../EVM/hooks' import { rightShift, ZERO } from '@masknet/web3-shared-base/utils/number' import type { Coin } from '../../Trader/types' +import { isWyvernSchemaName } from '../utils' const useStyles = makeStyles()((theme) => { return { @@ -74,14 +75,14 @@ export interface MakeOfferDialogProps { export function MakeOfferDialog(props: MakeOfferDialogProps) { const { asset, open, onClose } = props + const isAuction = asset?.value?.is_auction ?? false const isVerified = asset?.value?.is_verified ?? false const leastPrice = - asset?.value && asset.value.orders?.length ? new BigNumber(asset.value.orders[0].base_price ?? '0') : ZERO + asset?.value && asset.value.desktopOrder ? new BigNumber(asset.value.desktopOrder.current_price ?? '0') : ZERO const paymentTokens = (isAuction ? asset?.value?.offer_payment_tokens : asset?.value?.order_payment_tokens) ?? [] const selectedPaymentToken = first(paymentTokens) - const { t } = useI18N() const { classes } = useStyles() @@ -98,11 +99,12 @@ export function MakeOfferDialog(props: MakeOfferDialogProps) { if (!asset.value.token_id || !asset.value.token_address) return if (!token?.value) return if (token.value.type !== EthereumTokenType.Native && token.value.type !== EthereumTokenType.ERC20) return + const schemaName = asset.value.asset_contract?.schemaName await PluginCollectibleRPC.createBuyOrder({ asset: toAsset({ tokenId: asset.value.token_id, tokenAddress: asset.value.token_address, - schemaName: asset.value.asset_contract.schema_name, + schemaName: isWyvernSchemaName(schemaName) ? schemaName : undefined, }), accountAddress: account, startAmount: Number.parseFloat(amount), @@ -258,7 +260,7 @@ export function MakeOfferDialog(props: MakeOfferDialogProps) { completeOnClick={onClose} failedOnClick="use executor" /> - {(isAuction ? asset.value?.is_collection_weth : asset.value?.is_order_weth) ? ( + {(isAuction ? asset.value?.isCollectionWeth : asset.value?.isOrderWeth) ? ( { return { @@ -43,14 +43,14 @@ export function OfferTab() { const { asset, provider, offers, offerPage, setOfferPage } = CollectibleState.useContainer() const isDifferenceToken = useMemo(() => { - if (provider === CollectibleProvider.OPENSEA) { + if (provider === NonFungibleAssetProvider.OPENSEA) { return ( - offers.some( + offers.value?.data.some( (item) => (item.payment_token_contract?.symbol !== 'WETH' && item.payment_token_contract?.symbol !== 'ETH') || (item.quantity && new BigNumber(item.quantity).toString() !== '1'), - ) && offers.filter((item) => isZero(item.expiration_time ?? 0)).length === 0 + ) && offers.value.data.filter((item) => isZero(item.expiration_time ?? 0)).length === 0 ) } else { return false @@ -58,12 +58,12 @@ export function OfferTab() { }, [provider, offers]) const dataSource = useMemo(() => { - if (!offers.length) return [] - return offers + if (!offers.value?.data.length) return [] + return offers.value.data }, [offers]) if (asset.loading) return - if (!offers.length || asset.error || !dataSource.length) + if (!offers.value?.data.length || asset.error || !dataSource.length) return ( @@ -98,7 +98,7 @@ export function OfferTab() { ) : ( <> {t('plugin_collectible_price')} - {provider === CollectibleProvider.OPENSEA ? ( + {provider === NonFungibleAssetProvider.OPENSEA ? ( {t('plugin_collectible_expiration')} ) : null} @@ -110,7 +110,7 @@ export function OfferTab() { ))} - {(provider === CollectibleProvider.OPENSEA && dataSource.length) || offerPage > 0 ? ( + {(provider === NonFungibleAssetProvider.OPENSEA && dataSource.length) || offerPage > 0 ? ( setOfferPage((prev) => prev - 1)} handleNextClick={() => setOfferPage((prev) => prev + 1)} diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/OrderRow.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/OrderRow.tsx index fb9e5a28d7d8..dc0fb3b57681 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/OrderRow.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/OrderRow.tsx @@ -2,14 +2,20 @@ import { useMemo } from 'react' import BigNumber from 'bignumber.js' import { Avatar, Link, TableCell, TableRow, Typography } from '@mui/material' import { makeStyles } from '@masknet/theme' -import { AssetOrder, CollectibleProvider } from '../types' -import formatDistanceToNow from 'date-fns/formatDistanceToNow' -import { ChainId, formatBalance, resolveAddressLinkOnExplorer } from '@masknet/web3-shared-evm' + +import { + ChainId, + formatBalance, + NonFungibleAssetProvider, + resolveAddressLinkOnExplorer, +} from '@masknet/web3-shared-evm' import { isZero } from '@masknet/web3-shared-base' import { CollectibleState } from '../hooks/useCollectibleState' import { Account } from './Account' import { FormattedBalance } from '@masknet/shared' -import { getOrderUnitPrice } from '../utils' +import { getOrderUnitPrice, NonFungibleTokenAPI } from '@masknet/web3-providers' +import formatDistanceToNow from 'date-fns/formatDistanceToNow' +import urlcat from 'urlcat' const useStyles = makeStyles()((theme) => { return { @@ -50,7 +56,7 @@ const useStyles = makeStyles()((theme) => { }) interface IRowProps { - order: AssetOrder + order: NonFungibleTokenAPI.AssetOrder isDifferenceToken?: boolean acceptable?: boolean } @@ -61,10 +67,10 @@ export function OrderRow({ order, isDifferenceToken }: IRowProps) { const address = order.maker_account?.user?.username || order.maker_account?.address || '' const link = useMemo(() => { - return provider === CollectibleProvider.OPENSEA - ? `https://opensea.io/accounts/${address}` - : order.maker_account?.address - }, [order, provider]) + return provider === NonFungibleAssetProvider.OPENSEA + ? urlcat('https://opensea.io/accounts/:address', { address }) + : order.maker_account?.link + }, [order, provider, address]) return ( @@ -83,7 +89,7 @@ export function OrderRow({ order, isDifferenceToken }: IRowProps) { <> - {provider === CollectibleProvider.OPENSEA ? ( + {provider === NonFungibleAssetProvider.OPENSEA ? ( - {provider === CollectibleProvider.OPENSEA ? ( + {provider === NonFungibleAssetProvider.OPENSEA ? ( - {order.payment_token_contract?.image_url && ( + {order.payment_token_contract?.image_url ? ( {order.payment_token_contract?.symbol} - )} + ) : null} ) : null} {getOrderUnitPrice( order.current_price, order.payment_token_contract?.decimals, order.quantity, - )}{' '} - {provider === CollectibleProvider.OPENSEA + )?.toString()}{' '} + {provider === NonFungibleAssetProvider.OPENSEA ? order.payment_token_contract?.symbol ?? '' : 'ETH'} - {provider === CollectibleProvider.OPENSEA ? ( + {provider === NonFungibleAssetProvider.OPENSEA ? ( {order.expiration_time && diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/PostListingDialog.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/PostListingDialog.tsx index 6d7fd0ed3647..29ba310a63ed 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/PostListingDialog.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/PostListingDialog.tsx @@ -5,9 +5,9 @@ import { useI18N } from '../../../utils' import { InjectedDialog } from '../../../components/shared/InjectedDialog' import { ListingByPriceCard } from './ListingByPriceCard' import { ListingByHighestBidCard } from './ListingByHighestBidCard' -import type { useAsset } from '../hooks/useAsset' import { useFungibleTokenWatched } from '@masknet/web3-shared-evm' import { first } from 'lodash-unified' +import type { useAsset } from '../../EVM/hooks/useAsset' const useStyles = makeStyles()((theme) => { return { diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/index.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/index.tsx index 777dc7cb6bb1..37066975847a 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/index.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/index.tsx @@ -13,7 +13,6 @@ const sns: Plugin.SNSAdaptor.Definition = { init(signal) {}, PostInspector: function Component() { const links = usePostInfoDetails.postMetadataMentionedLinks().concat(usePostInfoDetails.postMentionedLinks()) - const link = uniq(links).find(checkUrl) const asset = getAssetInfoFromURL(link) diff --git a/packages/mask/src/plugins/Collectible/apis/index.ts b/packages/mask/src/plugins/Collectible/apis/index.ts index e21510b9c18c..da51c15f303e 100644 --- a/packages/mask/src/plugins/Collectible/apis/index.ts +++ b/packages/mask/src/plugins/Collectible/apis/index.ts @@ -1,2 +1 @@ -export * from './opensea' -export * from './rarible' +export * from './openseaFromSDK' diff --git a/packages/mask/src/plugins/Collectible/apis/opensea.ts b/packages/mask/src/plugins/Collectible/apis/opensea.ts deleted file mode 100644 index 69b363a0794d..000000000000 --- a/packages/mask/src/plugins/Collectible/apis/opensea.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { head, toString } from 'lodash-unified' -import { OpenSeaPort } from 'opensea-js' -import { ChainId } from '@masknet/web3-shared-evm' -import { request, requestSend } from '../../../extension/background-script/EthereumService' -import { resolveOpenSeaNetwork } from '../pipes' -import { OpenSeaAPI_Key, OpenSeaBaseURL, OpenSeaRinkebyBaseURL, ReferrerAddress } from '../constants' -import { currentChainIdSettings } from '../../Wallet/settings' -import urlcat from 'urlcat' -import type { OpenSeaAssetEvent, OpenSeaResponse } from '../types' - -function createExternalProvider() { - return { - isMetaMask: false, - isStatus: true, - host: '', - path: '', - sendAsync: requestSend, - send: requestSend, - request, - } -} - -async function createOpenSeaPort(chainId?: ChainId) { - return createOpenSeaPortChain(chainId ?? currentChainIdSettings.value) -} - -async function createOpenSeaPortChain(chainId: ChainId.Mainnet | ChainId.Rinkeby) { - return new OpenSeaPort( - createExternalProvider(), - { - networkName: resolveOpenSeaNetwork(chainId), - apiKey: OpenSeaAPI_Key, - }, - console.log, - ) -} - -async function createOpenSeaAPI(chainId: ChainId) { - if (chainId === ChainId.Rinkeby) return OpenSeaRinkebyBaseURL - return OpenSeaBaseURL -} - -export async function getAssetFromSDK(tokenAddress: string, tokenId: string, chainId?: ChainId) { - const _chainId = chainId ?? currentChainIdSettings.value - const sdkResponse = await (await createOpenSeaPort(_chainId)).api.getAsset({ tokenAddress, tokenId }) - return sdkResponse -} - -export async function getAsset(tokenAddress: string, tokenId: string, chainId?: ChainId) { - const _chainId = chainId ?? currentChainIdSettings.value - const fetchResponse = await ( - await fetch( - urlcat(await createOpenSeaAPI(_chainId), '/asset/:tokenAddress/:tokenId', { tokenAddress, tokenId }), - { - mode: 'cors', - headers: { - 'x-api-key': OpenSeaAPI_Key, - }, - }, - ) - ).json() - - const endTime = head<{ closing_date: Date }>( - fetchResponse.orders.filter((item: { side: number; closing_extendable: boolean }) => item.side === 1), - )?.closing_date - - return { - ...fetchResponse, - endTime, - } as OpenSeaResponse -} - -export async function getEvents(asset_contract_address: string, token_id: string, page?: number, size?: number) { - const params = new URLSearchParams() - params.append('asset_contract_address', asset_contract_address) - params.append('token_id', token_id) - params.append('offset', toString(page ?? 0)) - params.append('limit', toString(size ?? 10)) - - const fetchResponse = await ( - await fetch(urlcat(await createOpenSeaAPI(currentChainIdSettings.value), `events?${params.toString()}`), { - mode: 'cors', - headers: { 'x-api-key': OpenSeaAPI_Key }, - }) - ).json() - - const { asset_events }: { asset_events: OpenSeaAssetEvent[] } = fetchResponse - - return asset_events -} - -export async function createBuyOrder(payload: Parameters[0]) { - return (await createOpenSeaPort()).createBuyOrder({ - referrerAddress: ReferrerAddress, - ...payload, - }) -} - -export async function createSellOrder(payload: Parameters[0]) { - return (await createOpenSeaPort()).createSellOrder(payload) -} - -export async function fulfillOrder(payload: Parameters[0]) { - return (await createOpenSeaPort()).fulfillOrder({ - referrerAddress: ReferrerAddress, - ...payload, - }) -} diff --git a/packages/mask/src/plugins/Collectible/apis/openseaFromSDK.ts b/packages/mask/src/plugins/Collectible/apis/openseaFromSDK.ts new file mode 100644 index 000000000000..5b7108da4ed2 --- /dev/null +++ b/packages/mask/src/plugins/Collectible/apis/openseaFromSDK.ts @@ -0,0 +1,53 @@ +import { OpenSeaPort } from 'opensea-js' +import type { ChainId } from '@masknet/web3-shared-evm' +import { request, requestSend } from '../../../extension/background-script/EthereumService' +import { resolveOpenSeaNetwork } from '../pipes' +import { OpenSeaAPI_Key, ReferrerAddress } from '../constants' +import { currentChainIdSettings } from '../../Wallet/settings' + +function createExternalProvider() { + return { + isMetaMask: false, + isStatus: true, + host: '', + path: '', + sendAsync: requestSend, + send: requestSend, + request, + } +} + +function createOpenSeaPortChain(chainId: ChainId.Mainnet | ChainId.Rinkeby) { + const config = { + networkName: resolveOpenSeaNetwork(chainId), + apiKey: OpenSeaAPI_Key, + } + return new OpenSeaPort(createExternalProvider(), config, console.log) +} + +function createOpenSeaPort(chainId?: ChainId) { + return createOpenSeaPortChain(chainId ?? currentChainIdSettings.value) +} + +export async function getAssetFromSDK(tokenAddress: string, tokenId: string, chainId?: ChainId) { + chainId = chainId ?? currentChainIdSettings.value + return createOpenSeaPort(chainId).api.getAsset({ tokenAddress, tokenId }) +} + +export async function createBuyOrder(payload: Parameters[0]) { + return createOpenSeaPort().createBuyOrder({ + referrerAddress: ReferrerAddress, + ...payload, + }) +} + +export async function createSellOrder(payload: Parameters[0]) { + return createOpenSeaPort().createSellOrder(payload) +} + +export async function fulfillOrder(payload: Parameters[0]) { + return createOpenSeaPort().fulfillOrder({ + referrerAddress: ReferrerAddress, + ...payload, + }) +} diff --git a/packages/mask/src/plugins/Collectible/apis/rarible.ts b/packages/mask/src/plugins/Collectible/apis/rarible.ts deleted file mode 100644 index dfbfc4083160..000000000000 --- a/packages/mask/src/plugins/Collectible/apis/rarible.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { RaribleChainURL, RaribleMainnetURL } from '../constants' -import { compact } from 'lodash-unified' -import { OrderSide } from 'opensea-js/lib/types' -import stringify from 'json-stable-stringify' -import { - Ownership, - RaribleEventType, - RaribleHistory, - RaribleNFTItemMapResponse, - RaribleOfferResponse, - RaribleProfileResponse, -} from '../types' -import { toRaribleImage } from '../helpers' -import { resolveRaribleUserNetwork } from '../pipes' -import { currentChainIdSettings } from '../../Wallet/settings' -import urlcat from 'urlcat' - -async function fetchFromRarible(root: string, subPath: string, config = {} as RequestInit) { - const response = await ( - await fetch(urlcat(root, subPath), { - mode: 'cors', - ...config, - }) - ).json() - - return response as T -} - -export async function getProfilesFromRarible(addresses: (string | undefined)[]) { - return fetchFromRarible(RaribleMainnetURL, 'profiles/list', { - method: 'POST', - body: stringify(addresses), - headers: { - 'content-type': 'application/json', - }, - }) -} - -export async function getNFTItem(tokenAddress: string, tokenId: string) { - const assetResponse = await fetchFromRarible( - RaribleChainURL, - urlcat('/v0.1/nft/items/:tokenAddress::tokenId', { - includeMeta: true, - tokenAddress, - tokenId, - }), - { - method: 'GET', - headers: { - 'content-type': 'application/json', - }, - }, - ) - return assetResponse -} - -export async function getOffersFromRarible(tokenAddress: string, tokenId: string) { - const orders = await fetchFromRarible( - RaribleMainnetURL, - `items/${tokenAddress}:${tokenId}/offers`, - { - method: 'POST', - body: stringify({ size: 20 }), - headers: { - 'content-type': 'application/json', - }, - }, - ) - const profiles = await getProfilesFromRarible(orders.map((item) => item.maker)) - const chainId = currentChainIdSettings.value - return orders.map((order) => { - const ownerInfo = profiles.find((owner) => owner.id === order.maker) - return { - unitPrice: order.buyPriceEth, - hash: order.signature, - makerAccount: { - user: { - username: ownerInfo?.name, - }, - address: ownerInfo?.id, - profile_img_url: toRaribleImage(ownerInfo?.image), - link: `${resolveRaribleUserNetwork(chainId)}${ownerInfo?.id ?? ''}`, - }, - } - }) -} - -export async function getListingsFromRarible(tokenAddress: string, tokenId: string) { - const assets = await fetchFromRarible(RaribleMainnetURL, `items/${tokenAddress}:${tokenId}/ownerships`) - const listings = assets.filter((x) => x.selling) - const profiles = await getProfilesFromRarible(listings.map((x) => x.owner)) - const chainId = currentChainIdSettings.value - return listings.map((asset) => { - const ownerInfo = profiles.find((owner) => owner.id === asset.owner) - return { - unitPrice: asset.priceEth, - hash: asset.signature, - makerAccount: { - user: { - username: ownerInfo?.name, - }, - address: ownerInfo?.id, - profile_img_url: toRaribleImage(ownerInfo?.image), - link: `${resolveRaribleUserNetwork(chainId)}${ownerInfo?.id ?? ''}`, - }, - } - }) -} - -export async function getOrderFromRarible(tokenAddress: string, tokenId: string, side: OrderSide) { - switch (side) { - case OrderSide.Buy: - return getOffersFromRarible(tokenAddress, tokenId) - case OrderSide.Sell: - return getListingsFromRarible(tokenAddress, tokenId) - default: - return [] - } -} - -export async function getHistoryFromRarible(tokenAddress: string, tokenId: string) { - let histories = await fetchFromRarible(RaribleMainnetURL, `activity`, { - method: 'POST', - body: stringify({ - // types: ['BID', 'BURN', 'BUY', 'CANCEL', 'CANCEL_BID', 'ORDER', 'MINT', 'TRANSFER', 'SALE'], - filter: { - '@type': 'by_item', - address: tokenAddress, - tokenId, - }, - size: 100, - }), - headers: { - 'content-type': 'application/json', - }, - }) - - if (histories.length) { - histories = histories.filter((x) => Object.values(RaribleEventType).includes(x['@type'])) - } - - const profiles = await getProfilesFromRarible( - compact([ - ...histories.map((history) => history.owner), - ...histories.map((history) => history.buyer), - ...histories.map((history) => history.from), - ]), - ) - - return histories.map((history) => { - const ownerInfo = profiles.find((profile) => profile.id === history.owner) - const fromInfo = profiles.find((profile) => profile.id === history.buyer || profile.id === history.from) - return { - ...history, - ownerInfo, - fromInfo, - } - }) -} diff --git a/packages/mask/src/plugins/Collectible/helpers.ts b/packages/mask/src/plugins/Collectible/helpers.ts index b61908c01b99..45bb0d869b7f 100644 --- a/packages/mask/src/plugins/Collectible/helpers.ts +++ b/packages/mask/src/plugins/Collectible/helpers.ts @@ -1,7 +1,6 @@ -import type { Asset, OpenSeaFungibleToken, WyvernSchemaName } from 'opensea-js/lib/types' -import { ChainId, createERC20Token, createNativeToken } from '@masknet/web3-shared-evm' +import type { Asset, WyvernSchemaName } from 'opensea-js/lib/types' import { createRenderWithMetadata, createTypedMessageMetadataReader } from '@masknet/shared-base' -import { PLUGIN_META_KEY, RaribleIPFSURL } from './constants' +import { PLUGIN_META_KEY } from './constants' import type { CollectibleJSON_Payload, CollectibleToken } from './types' import schema from './schema.json' import type BigNumber from 'bignumber.js' @@ -21,11 +20,6 @@ export function toAsset(asset: { tokenId: string; tokenAddress: string; schemaNa } } -export function toTokenDetailed(chainId: ChainId, token: OpenSeaFungibleToken) { - if (token.symbol === 'ETH') return createNativeToken(chainId) - return createERC20Token(chainId, token.address, token.decimals, token.name, token.symbol) -} - export function toTokenIdentifier(token?: CollectibleToken) { if (!token) return '' return `${token.contractAddress}_${token.tokenId}` @@ -34,8 +28,3 @@ export function toTokenIdentifier(token?: CollectibleToken) { export function toDecimalAmount(weiAmount: BigNumber.Value, decimals: number) { return leftShift(weiAmount, decimals).toNumber() } - -export function toRaribleImage(url?: string) { - if (!url) return '' - return `${RaribleIPFSURL}${url.replace('ipfs://ipfs/', '')}` -} diff --git a/packages/mask/src/plugins/Collectible/hooks/useAsset.ts b/packages/mask/src/plugins/Collectible/hooks/useAsset.ts deleted file mode 100644 index 9344f7126bab..000000000000 --- a/packages/mask/src/plugins/Collectible/hooks/useAsset.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { unreachable } from '@dimensiondev/kit' -import { - currySameAddress, - EthereumTokenType, - FungibleTokenDetailed, - isSameAddress, - useAccount, - useChainId, - useTokenConstants, -} from '@masknet/web3-shared-evm' -import BigNumber from 'bignumber.js' -import fromUnixTime from 'date-fns/fromUnixTime' -import { first, head, uniqBy } from 'lodash-unified' -import { useAsyncRetry } from 'react-use' -import { OpenSeaAccountURL } from '../constants' -import { toTokenDetailed, toTokenIdentifier } from '../helpers' -import { PluginCollectibleRPC } from '../messages' -import { CollectibleProvider, CollectibleToken } from '../types' -import { getOrderUnitPrice } from '../utils' - -export function useAsset(provider: CollectibleProvider, token?: CollectibleToken) { - const account = useAccount() - const chainId = useChainId() - const { WNATIVE_ADDRESS } = useTokenConstants() - - return useAsyncRetry(async () => { - if (!token) return - switch (provider) { - case CollectibleProvider.OPENSEA: - const openSeaResponse = await PluginCollectibleRPC.getAsset(token.contractAddress, token.tokenId) - - const desktopOrder = head( - (openSeaResponse.sell_orders ?? []).sort( - (a, b) => - new BigNumber( - getOrderUnitPrice(a.current_price, a.payment_token_contract?.decimals, a.quantity) ?? 0, - ).toNumber() - - new BigNumber( - getOrderUnitPrice(b.current_price, b.payment_token_contract?.decimals, b.quantity) ?? 0, - ).toNumber(), - ), - ) - - return { - is_verified: ['approved', 'verified'].includes( - openSeaResponse.collection?.safelist_request_status ?? '', - ), - - is_order_weth: isSameAddress(desktopOrder?.payment_token ?? '', WNATIVE_ADDRESS), - is_collection_weth: openSeaResponse.collection.payment_tokens.some( - currySameAddress(WNATIVE_ADDRESS), - ), - is_owner: openSeaResponse.top_ownerships.some((item) => isSameAddress(item.owner.address, account)), - // it's an IOS string as my inspection - is_auction: Date.parse(`${openSeaResponse.endTime ?? ''}Z`) > Date.now(), - image_url: - openSeaResponse.image_url_original ?? - openSeaResponse.image_url ?? - openSeaResponse.image_preview_url ?? - '', - asset_contract: openSeaResponse.asset_contract, - current_price: desktopOrder - ? new BigNumber( - getOrderUnitPrice( - desktopOrder.current_price, - desktopOrder.payment_token_contract?.decimals, - desktopOrder.quantity, - ) ?? 0, - ).toNumber() - : null, - current_symbol: desktopOrder?.payment_token_contract?.symbol ?? 'ETH', - owner: { - ...openSeaResponse.owner, - link: `${OpenSeaAccountURL}${ - openSeaResponse.owner?.user?.username ?? openSeaResponse.owner.address ?? '' - }`, - }, - creator: { - ...openSeaResponse.creator, - link: `${OpenSeaAccountURL}${ - openSeaResponse.creator?.user?.username ?? openSeaResponse.creator?.address ?? '' - }`, - }, - token_id: openSeaResponse.token_id, - token_address: openSeaResponse.token_address ?? openSeaResponse.asset_contract.address, - traits: openSeaResponse.traits, - safelist_request_status: openSeaResponse.collection?.safelist_request_status ?? '', - description: openSeaResponse.description, - name: openSeaResponse.name ?? openSeaResponse.collection.name, - collection_name: openSeaResponse.collection.name, - animation_url: openSeaResponse.animation_url, - end_time: openSeaResponse.endTime - ? new Date(openSeaResponse.endTime) - : desktopOrder - ? fromUnixTime(desktopOrder.listing_time) - : null, - order_payment_tokens: desktopOrder?.payment_token_contract - ? [toTokenDetailed(chainId, desktopOrder.payment_token_contract)] - : [], - offer_payment_tokens: uniqBy( - openSeaResponse.collection.payment_tokens.map((x) => toTokenDetailed(chainId, x)), - (x) => x.address.toLowerCase(), - ).filter((x) => x.type === EthereumTokenType.ERC20), - slug: openSeaResponse.collection.slug, - orders: openSeaResponse.orders, - response_: openSeaResponse, - } - case CollectibleProvider.RARIBLE: - const raribleResponse = await PluginCollectibleRPC.getNFTItem(token.contractAddress, token.tokenId) - const owner = first(raribleResponse?.owners) - const creator = first(raribleResponse?.creators) - return { - is_order_weth: false, - is_collection_weth: false, - is_verified: false, - is_owner: false, - is_auction: false, - image_url: raribleResponse?.meta.image.url.PREVIEW, - asset_contract: null, - owner: owner - ? { - address: owner, - profile_img_url: '', - user: { username: owner }, - link: '', - } - : null, - creator: creator - ? { - address: creator.account, - profile_img_url: '', - user: { username: creator.account }, - link: '', - } - : null, - traits: raribleResponse?.meta.attributes.map(({ key, value }) => ({ trait_type: key, value })), - description: raribleResponse?.meta.description ?? '', - name: raribleResponse?.meta.name ?? 'Unknown', - collection_name: '', - animation_url: raribleResponse.meta.animation?.url.PREVIEW, - current_price: 0, - current_symbol: 'ETH', - end_time: null, - order_payment_tokens: [] as FungibleTokenDetailed[], - offer_payment_tokens: [] as FungibleTokenDetailed[], - - slug: '', - response_: raribleResponse, - } - default: - unreachable(provider) - } - }, [toTokenIdentifier(token), account, chainId, provider, WNATIVE_ADDRESS]) -} diff --git a/packages/mask/src/plugins/Collectible/hooks/useAssetOrder.ts b/packages/mask/src/plugins/Collectible/hooks/useAssetOrder.ts index 4edd2bd6a4b2..a46564562eab 100644 --- a/packages/mask/src/plugins/Collectible/hooks/useAssetOrder.ts +++ b/packages/mask/src/plugins/Collectible/hooks/useAssetOrder.ts @@ -1,40 +1,32 @@ import { unreachable } from '@dimensiondev/kit' -import { BigNumber } from 'bignumber.js' +import { getOrderUnitPrice } from '@masknet/web3-providers' +import { ONE } from '@masknet/web3-shared-base' +import { NonFungibleAssetProvider } from '@masknet/web3-shared-evm' import { head } from 'lodash-unified' +import type { Order } from 'opensea-js/lib/types' import { useAsyncRetry } from 'react-use' import { PluginCollectibleRPC } from '../messages' -import { CollectibleProvider, CollectibleToken } from '../types' -import { getOrderUnitPrice } from '../utils' +import type { CollectibleToken } from '../types' -export function useAssetOrder(provider: CollectibleProvider, token?: CollectibleToken) { +export function useAssetOrder(provider: NonFungibleAssetProvider, token?: CollectibleToken) { return useAsyncRetry(async () => { if (!token) return switch (provider) { - case CollectibleProvider.OPENSEA: + case NonFungibleAssetProvider.OPENSEA: const openSeaResponse = await PluginCollectibleRPC.getAssetFromSDK(token.contractAddress, token.tokenId) - - const desktopOrder = head( - (openSeaResponse.sellOrders ?? []).sort( - (a, b) => - new BigNumber( - getOrderUnitPrice( - a.currentPrice?.toFixed(), - a.paymentTokenContract?.decimals, - a.quantity.toFixed(), - ) ?? 0, - ).toNumber() - - new BigNumber( - getOrderUnitPrice( - b.currentPrice?.toFixed(), - b.paymentTokenContract?.decimals, - b.quantity.toFixed(), - ) ?? 0, - ).toNumber(), - ), - ) + const getPrice = (order: Order) => + getOrderUnitPrice( + order.currentPrice?.toFixed(), + order.paymentTokenContract?.decimals ?? 0, + order.quantity.toFixed(), + ) ?? ONE + const sellOrders = openSeaResponse.sellOrders ?? [] + const desktopOrder = head(sellOrders.sort((a, b) => getPrice(a).toNumber() - getPrice(b).toNumber())) return desktopOrder - case CollectibleProvider.RARIBLE: + case NonFungibleAssetProvider.RARIBLE: + return + case NonFungibleAssetProvider.NFTSCAN: return default: unreachable(provider) diff --git a/packages/mask/src/plugins/Collectible/hooks/useCollectibleState.ts b/packages/mask/src/plugins/Collectible/hooks/useCollectibleState.ts index c07d658f805d..c2292c6d95b7 100644 --- a/packages/mask/src/plugins/Collectible/hooks/useCollectibleState.ts +++ b/packages/mask/src/plugins/Collectible/hooks/useCollectibleState.ts @@ -1,47 +1,56 @@ -import { useMemo, useState } from 'react' +import { useState } from 'react' import { createContainer } from 'unstated-next' import { OrderSide } from 'opensea-js/lib/types' -import { useValueRef } from '@masknet/shared' -import { currentCollectibleProviderSettings } from '../settings' import { CollectibleTab, CollectibleToken } from '../types' -import { useAsset } from './useAsset' -import { useEvents } from './useEvents' +import { useAsset, useHistory, useOrders } from '../../EVM/hooks' import { useAssetOrder } from './useAssetOrder' +import { useValueRef } from '@masknet/shared' +import { currentNonFungibleAssetProviderSettings } from '../settings' function useCollectibleState(token?: CollectibleToken) { const [tabIndex, setTabIndex] = useState(CollectibleTab.ARTICLE) - const provider = useValueRef(currentCollectibleProviderSettings) - const asset = useAsset(provider, token) + const provider = useValueRef(currentNonFungibleAssetProviderSettings) + + const asset = useAsset(token?.contractAddress ?? '', token?.tokenId ?? '', provider) - //#region asset order + //#region asset order from sdk const assetOrder = useAssetOrder(provider, token) //#endregion //#region offers const [offerPage, setOfferPage] = useState(0) - const offers = useMemo(() => { - if (!asset.value?.orders) return [] - const _orders = asset.value?.orders?.filter((x) => x.side === OrderSide.Buy) - - return _orders.slice(offerPage * 10, (offerPage + 1) * 10) - }, [asset.value?.orders, offerPage]) + const offers = useOrders( + provider, + offerPage, + 50, + tabIndex === CollectibleTab.OFFER ? token?.contractAddress : undefined, + tabIndex === CollectibleTab.OFFER ? token?.tokenId : undefined, + OrderSide.Buy, + ) //#endregion //#region orders const [orderPage, setOrderPage] = useState(0) - const orders = useMemo(() => { - if (!asset.value?.orders) return [] - const _orders = asset.value?.orders?.filter((x) => x.side === OrderSide.Sell) - - return _orders.slice(orderPage * 10, (orderPage + 1) * 10) - }, [asset.value?.orders, orderPage]) + const orders = useOrders( + provider, + orderPage, + 50, + tabIndex === CollectibleTab.LISTING ? token?.contractAddress : undefined, + tabIndex === CollectibleTab.LISTING ? token?.tokenId : undefined, + OrderSide.Sell, + ) //#endregion //#region events const [eventPage, setEventPage] = useState(0) - const events = useEvents(provider, tabIndex === CollectibleTab.HISTORY ? token : undefined, eventPage, 10) - + const events = useHistory( + provider, + eventPage, + 50, + tabIndex === CollectibleTab.HISTORY ? token?.contractAddress : undefined, + tabIndex === CollectibleTab.HISTORY ? token?.tokenId : undefined, + ) //#endregion return { diff --git a/packages/mask/src/plugins/Collectible/hooks/useEvents.ts b/packages/mask/src/plugins/Collectible/hooks/useEvents.ts deleted file mode 100644 index 20b8a23c26b6..000000000000 --- a/packages/mask/src/plugins/Collectible/hooks/useEvents.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { useAsyncRetry } from 'react-use' -import { ChainId, useChainId, isZeroAddress } from '@masknet/web3-shared-evm' -import { CollectibleToken, NFTHistory, CollectibleProvider, OpenSeaAssetEventType } from '../types' -import { PluginCollectibleRPC } from '../messages' -import { NullAddress, OpenSeaAccountURL } from '../constants' -import { toRaribleImage, toTokenIdentifier } from '../helpers' -import { resolveRaribleUserNetwork } from '../pipes' - -export function useEvents(provider: CollectibleProvider, token?: CollectibleToken, page?: number, size?: number) { - const chainId = useChainId() - return useAsyncRetry<{ data: NFTHistory[]; hasNextPage: boolean }>(async () => { - if (!token || (chainId !== ChainId.Mainnet && chainId !== ChainId.Ropsten)) - return { - data: [] as NFTHistory[], - hasNextPage: false, - } as UnboxPromise - switch (provider) { - case CollectibleProvider.OPENSEA: - const openseaEvents = await PluginCollectibleRPC.getEvents( - token.contractAddress, - token.tokenId, - page, - size, - ) - - return { - data: openseaEvents.map((event) => { - const accountPair = - event.event_type === OpenSeaAssetEventType.SUCCESSFUL - ? { - from: { - username: event.seller?.user?.username, - address: event.seller?.address, - imageUrl: event.seller?.profile_img_url, - link: `${OpenSeaAccountURL}${ - event.seller?.user?.username ?? event.seller?.address - }`, - }, - to: { - username: event.winner_account?.user?.username, - address: event.winner_account?.address, - imageUrl: event.winner_account?.profile_img_url, - link: `${OpenSeaAccountURL}${ - event.winner_account?.user?.username ?? event.winner_account?.address - }`, - }, - } - : { - from: { - username: event.from_account?.user?.username, - address: event.from_account?.address, - imageUrl: event.from_account?.profile_img_url, - link: `${OpenSeaAccountURL}${ - event.from_account?.user?.username ?? event.from_account?.address - }`, - }, - to: { - username: event.to_account?.user?.username, - address: event.to_account?.address, - imageUrl: event.to_account?.profile_img_url, - link: `${OpenSeaAccountURL}${ - event.to_account?.user?.username ?? event.to_account?.address - }`, - }, - } - return { - id: event.id, - accountPair, - price: { - quantity: event.quantity, - asset: event.asset, - paymentToken: event.payment_token, - price: event.bid_amount ?? event.ending_price ?? event.starting_price, - }, - eventType: event.event_type, - transactionBlockExplorerLink: event.transaction?.blockExplorerLink, - timestamp: new Date(`${event.created_date}Z`).getTime(), - } as NFTHistory - }), - hasNextPage: openseaEvents.length === size, - } - - case CollectibleProvider.RARIBLE: - const raribleEvents = await PluginCollectibleRPC.getHistoryFromRarible( - token.contractAddress, - token.tokenId, - ) - return { - data: raribleEvents.map((event) => { - return { - id: event.id, - accountPair: { - from: event.fromInfo - ? { - username: isZeroAddress(event.fromInfo?.id) - ? NullAddress - : event.fromInfo?.name, - address: event.fromInfo?.id, - imageUrl: toRaribleImage(event.fromInfo?.image), - link: `${resolveRaribleUserNetwork(chainId)}${event.fromInfo?.id ?? ''}`, - } - : null, - to: event.ownerInfo - ? { - username: event.ownerInfo?.name, - address: event.ownerInfo?.id, - imageUrl: toRaribleImage(event.ownerInfo?.image), - link: `${resolveRaribleUserNetwork(chainId)}${event.ownerInfo?.id ?? ''}`, - } - : null, - }, - eventType: event['@type'], - timestamp: new Date(event.date).getTime(), - price: event.price - ? { - quantity: String(event.price), - } - : null, - } as NFTHistory - }), - - hasNextPage: false, - } - } - }, [chainId, toTokenIdentifier(token), page, size, provider]) -} diff --git a/packages/mask/src/plugins/Collectible/pipes/index.ts b/packages/mask/src/plugins/Collectible/pipes/index.ts index e2213e108e5d..999ce985d8fc 100644 --- a/packages/mask/src/plugins/Collectible/pipes/index.ts +++ b/packages/mask/src/plugins/Collectible/pipes/index.ts @@ -1,8 +1,8 @@ import { identity } from 'lodash-unified' import { Network } from 'opensea-js' -import { ChainId, createLookupTableResolver } from '@masknet/web3-shared-evm' +import { ChainId, createLookupTableResolver, NonFungibleAssetProvider } from '@masknet/web3-shared-evm' import { NullAddress, RaribleRopstenUserURL, RaribleUserURL } from '../constants' -import { CollectibleProvider, OpenSeaAssetEventType, RaribleEventType } from '../types' +import { OpenSeaAssetEventType, RaribleEventType } from '../types' import urlcat from 'urlcat' export function resolveOpenSeaAssetEventType(eventType: OpenSeaAssetEventType, fromUserName?: string) { @@ -44,10 +44,11 @@ export const resolveOpenSeaNetwork = createLookupTableResolver( +export const resolveCollectibleProviderName = createLookupTableResolver( { - [CollectibleProvider.OPENSEA]: 'OpenSea', - [CollectibleProvider.RARIBLE]: 'Rarible', + [NonFungibleAssetProvider.OPENSEA]: 'OpenSea', + [NonFungibleAssetProvider.RARIBLE]: 'Rarible', + [NonFungibleAssetProvider.NFTSCAN]: 'NFTScan', }, (providerType) => { throw new Error(`Unknown provider type: ${providerType}.`) diff --git a/packages/mask/src/plugins/Collectible/settings.ts b/packages/mask/src/plugins/Collectible/settings.ts index 1796a627ddc3..762851cd3360 100644 --- a/packages/mask/src/plugins/Collectible/settings.ts +++ b/packages/mask/src/plugins/Collectible/settings.ts @@ -1,10 +1,10 @@ +import { NonFungibleAssetProvider } from '@masknet/web3-shared-evm/types' import { createGlobalSettings } from '../../settings/createSettings' import { PLUGIN_IDENTIFIER } from './constants' -import { CollectibleProvider } from './types' -export const currentCollectibleProviderSettings = createGlobalSettings( - `${PLUGIN_IDENTIFIER}+tradeProvider`, - CollectibleProvider.OPENSEA, +export const currentNonFungibleAssetProviderSettings = createGlobalSettings( + `${PLUGIN_IDENTIFIER}+nonFungibleAssetProvider`, + NonFungibleAssetProvider.OPENSEA, { primary: () => '', }, diff --git a/packages/mask/src/plugins/Collectible/types/index.ts b/packages/mask/src/plugins/Collectible/types/index.ts index 667b063ae8cf..42749043aa32 100644 --- a/packages/mask/src/plugins/Collectible/types/index.ts +++ b/packages/mask/src/plugins/Collectible/types/index.ts @@ -1,7 +1,5 @@ import type { WyvernSchemaName } from 'opensea-js/lib/types' import type { ChainId } from '@masknet/web3-shared-evm' -import type { OpenSeaAssetEventType, OpenSeaFungibleToken } from './opensea' -import type { RaribleEventType } from './rarible' export * from './opensea' export * from './rarible' @@ -26,93 +24,3 @@ export interface CollectibleToken { contractAddress: string schemaName?: WyvernSchemaName } - -export enum CollectibleProvider { - OPENSEA = 0, - RARIBLE = 1, -} - -export interface NFTAsset { - imageUrl: string - assetContract: { - name: string - description: string - schemaName: WyvernSchemaName - } - currentPrice?: number - owner?: { - address: string - profile_img_url?: string - user?: { - username: string - } - } - creator?: { - address: string - profile_img_url?: string - user?: { - username: string - } - } - traits?: { trait_type: string; value: string }[] - safelist_request_status: string - description: string - name?: string - animation_url?: string -} - -export interface NFTOrder { - unitPrice: number - usdPrice?: number - makerAccount: { - user?: { - username?: string - } - address?: string - profile_img_url: string - link: string - } - hash?: string - quantity?: number - expirationTime?: string - paymentTokenContract?: OpenSeaFungibleToken - paymentToken?: string -} - -export interface NFTHistory { - id: string - accountPair: { - from?: { - username?: string - address?: string - imageUrl?: string - link: string - } - to?: { - username?: string - address?: string - imageUrl?: string - link: string - } - } - price?: { - quantity: string - price: string - asset?: { - id: string - decimals: number - image_url: string - image_original_url: string - image_preview_url: string - asset_contract: { - symbol: string - } - permalink: string - } - paymentToken?: OpenSeaFungibleToken - } - - eventType: OpenSeaAssetEventType | RaribleEventType - transactionBlockExplorerLink?: string - timestamp: number -} diff --git a/packages/mask/src/plugins/Collectible/utils.ts b/packages/mask/src/plugins/Collectible/utils.ts index 52f985fd3104..bbb9020c9a7f 100644 --- a/packages/mask/src/plugins/Collectible/utils.ts +++ b/packages/mask/src/plugins/Collectible/utils.ts @@ -1,4 +1,3 @@ -import BigNumber from 'bignumber.js' import { parseURL } from '@masknet/shared-base' import { openseaHostnames, @@ -6,9 +5,8 @@ import { raribleHostnames, rariblePathnameRegexMatcher, } from './constants' -import { ChainId, formatBalance } from '@masknet/web3-shared-evm' -import type { AssetEvent } from './types' -import { multipliedBy } from '@masknet/web3-shared-base' +import { ChainId } from '@masknet/web3-shared-evm' +import { WyvernSchemaName } from 'opensea-js/lib/types' export function checkUrl(url: string): boolean { const protocol = 'https://' @@ -59,23 +57,13 @@ export function getAssetInfoFromURL(url?: string) { return } -export function getOrderUnitPrice(currentPrice?: string, decimals?: number, quantity?: string) { - if (!currentPrice || !decimals || !quantity) return - const price = formatBalance(currentPrice, decimals) - const _quantity = formatBalance(quantity, new BigNumber(quantity).toString() !== '1' ? 8 : 0) - return new BigNumber(price).dividedBy(_quantity).toFixed(4, 1).toString() -} - -export function getOrderUSDPrice(currentPrice?: string, usdPrice?: string, decimals?: number) { - if (!currentPrice || !decimals) return - const price = formatBalance(usdPrice, 0) - const quantity = formatBalance(currentPrice, decimals) - - return multipliedBy(price, quantity).toFixed(2, 1).toString() -} - -export function getLastSalePrice(lastSale: AssetEvent | null) { - if (!lastSale?.total_price || !lastSale?.payment_token?.decimals) return - const price = formatBalance(lastSale.total_price, lastSale.payment_token.decimals) - return price +export function isWyvernSchemaName(name: unknown): name is WyvernSchemaName { + const schemas: unknown[] = [ + WyvernSchemaName.ERC20, + WyvernSchemaName.ERC721, + WyvernSchemaName.ERC1155, + WyvernSchemaName.LegacyEnjin, + WyvernSchemaName.ENSShortNameAuction, + ] + return schemas.includes(name) } diff --git a/packages/mask/src/plugins/CryptoartAI/utils.ts b/packages/mask/src/plugins/CryptoartAI/utils.ts index b62843c61b22..ea576212edf5 100644 --- a/packages/mask/src/plugins/CryptoartAI/utils.ts +++ b/packages/mask/src/plugins/CryptoartAI/utils.ts @@ -1,7 +1,5 @@ -import BigNumber from 'bignumber.js' -import type { AssetEvent, Order } from 'opensea-js/lib/types' import { parseURL } from '@masknet/shared-base' -import { ChainId, formatBalance } from '@masknet/web3-shared-evm' +import { ChainId } from '@masknet/web3-shared-evm' import { escapeRegExp } from 'lodash-unified' import { prefixPath, mainNetwork, testNetwork } from './constants' import type { Token } from './types' @@ -39,28 +37,6 @@ export function getAssetInfoFromURL(url?: string) { return { chain_id, creator, token_id, contractAddress } } -export function getOrderUnitPrice(order: Order) { - if (!order.currentPrice || !order.paymentTokenContract?.decimals) return - const price = formatBalance(order.currentPrice, order.paymentTokenContract.decimals) - const quantity = formatBalance(order.quantity, new BigNumber(order.quantity).toString() !== '1' ? 8 : 0) - - return new BigNumber(price).dividedBy(quantity).toFixed(4, 1) -} - -export function getOrderUSDPrice(order: Order) { - if (!order.currentPrice || !order.paymentTokenContract?.decimals) return - const price = formatBalance(order.paymentTokenContract.usdPrice, 0) - const quantity = formatBalance(order.currentPrice, order.paymentTokenContract.decimals) - - return new BigNumber(price).multipliedBy(quantity).toFixed(2, 1) -} - -export function getLastSalePrice(lastSale: AssetEvent | null) { - if (!lastSale?.totalPrice || !lastSale?.paymentToken?.decimals) return - const price = formatBalance(lastSale.totalPrice, lastSale.paymentToken.decimals) - return price -} - export function toTokenIdentifier(token?: Token) { if (!token) return '' return `${token.contractAddress}_${token.tokenId}` diff --git a/packages/mask/src/plugins/EVM/base.ts b/packages/mask/src/plugins/EVM/base.ts index d892984bd9c2..832117ad7d3d 100644 --- a/packages/mask/src/plugins/EVM/base.ts +++ b/packages/mask/src/plugins/EVM/base.ts @@ -11,6 +11,7 @@ import { export const base: Plugin.Shared.Definition = { ID: PLUGIN_ID, + icon: PLUGIN_ICON, name: { fallback: PLUGIN_NAME }, description: { fallback: PLUGIN_DESCRIPTION }, diff --git a/packages/mask/src/plugins/EVM/hooks/index.ts b/packages/mask/src/plugins/EVM/hooks/index.ts index 8da73f029d30..e494313e72d3 100644 --- a/packages/mask/src/plugins/EVM/hooks/index.ts +++ b/packages/mask/src/plugins/EVM/hooks/index.ts @@ -1,3 +1,7 @@ +export * from './useAsset' +export * from './useNFTBalance' +export * from './useHistory' +export * from './useOrders' export * from './useNetwork' export * from './useProvider' export * from './useBridgedProvider' diff --git a/packages/mask/src/plugins/EVM/hooks/useAsset.ts b/packages/mask/src/plugins/EVM/hooks/useAsset.ts new file mode 100644 index 000000000000..9d621953bce8 --- /dev/null +++ b/packages/mask/src/plugins/EVM/hooks/useAsset.ts @@ -0,0 +1,27 @@ +import { useAsyncRetry } from 'react-use' +import { + NonFungibleAssetProvider, + currySameAddress, + isSameAddress, + useAccount, + useChainId, + useTokenConstants, +} from '@masknet/web3-shared-evm' +import { EVM_RPC } from '../messages' + +export function useAsset(address: string, tokenId: string, provider: NonFungibleAssetProvider) { + const account = useAccount() + const chainId = useChainId() + const { WNATIVE_ADDRESS } = useTokenConstants() + + return useAsyncRetry(async () => { + const asset = await EVM_RPC.getAsset({ address, tokenId, chainId, provider }) + + return { + ...asset, + isOrderWeth: isSameAddress(asset?.desktopOrder?.payment_token ?? '', WNATIVE_ADDRESS) ?? false, + isCollectionWeth: asset?.collection?.payment_tokens?.some(currySameAddress(WNATIVE_ADDRESS)) ?? false, + isOwner: asset?.top_ownerships.some((item) => isSameAddress(item.owner.address, account)) ?? false, + } + }, [account, chainId, WNATIVE_ADDRESS, address, tokenId, provider]) +} diff --git a/packages/mask/src/plugins/EVM/hooks/useHistory.ts b/packages/mask/src/plugins/EVM/hooks/useHistory.ts new file mode 100644 index 000000000000..6764060c7704 --- /dev/null +++ b/packages/mask/src/plugins/EVM/hooks/useHistory.ts @@ -0,0 +1,29 @@ +import { useAsyncRetry } from 'react-use' +import type { AsyncStateRetry } from 'react-use/lib/useAsyncRetry' +import type { NonFungibleTokenAPI } from '@masknet/web3-providers' +import { NonFungibleAssetProvider, useChainId } from '@masknet/web3-shared-evm' +import { EVM_RPC } from '../messages' + +export function useHistory( + provider: NonFungibleAssetProvider, + page: number, + size: number, + address?: string, + tokenId?: string, +): AsyncStateRetry<{ data: NonFungibleTokenAPI.History[]; hasNextPage: boolean }> { + const chainId = useChainId() + return useAsyncRetry(async () => { + if (!address || !tokenId) + return { + data: [], + hasNextPage: false, + } + + const histories = await EVM_RPC.getHistory({ address, tokenId, chainId, provider, page, size }) + + return { + data: histories, + hasNextPage: histories.length === size, + } + }, [address, tokenId, chainId, provider, page, size]) +} diff --git a/packages/mask/src/plugins/EVM/hooks/useNFTBalance.ts b/packages/mask/src/plugins/EVM/hooks/useNFTBalance.ts new file mode 100644 index 000000000000..c388b1a0bf15 --- /dev/null +++ b/packages/mask/src/plugins/EVM/hooks/useNFTBalance.ts @@ -0,0 +1,11 @@ +import { useAsyncRetry } from 'react-use' +import { NonFungibleAssetProvider, useChainId } from '@masknet/web3-shared-evm' +import { EVM_RPC } from '../messages' + +export function useNFTBalance(address: string, disabled = false) { + const chainId = useChainId() + return useAsyncRetry(async () => { + if (!address || disabled) return null + return EVM_RPC.getNFTBalance({ address, chainId, provider: NonFungibleAssetProvider.NFTSCAN }) + }, [address, disabled, chainId]) +} diff --git a/packages/mask/src/plugins/EVM/hooks/useOrders.ts b/packages/mask/src/plugins/EVM/hooks/useOrders.ts new file mode 100644 index 000000000000..39e0b2588d8a --- /dev/null +++ b/packages/mask/src/plugins/EVM/hooks/useOrders.ts @@ -0,0 +1,37 @@ +import { useAsyncRetry } from 'react-use' +import type { AsyncStateRetry } from 'react-use/lib/useAsyncRetry' +import { NonFungibleTokenAPI } from '@masknet/web3-providers' +import { NonFungibleAssetProvider, useChainId } from '@masknet/web3-shared-evm' +import { EVM_RPC } from '../messages' + +export function useOrders( + provider: NonFungibleAssetProvider, + page: number, + size: number, + address?: string, + tokenId?: string, + side?: NonFungibleTokenAPI.OrderSide, +): AsyncStateRetry<{ data: NonFungibleTokenAPI.AssetOrder[]; hasNextPage: boolean }> { + const chainId = useChainId() + return useAsyncRetry(async () => { + if (!address || !tokenId) + return { + data: [], + hasNextPage: false, + } + const orders = await EVM_RPC.getOrders({ + address, + tokenId, + side: side ?? NonFungibleTokenAPI.OrderSide.Sell, + chainId, + provider, + page, + size, + }) + + return { + data: orders, + hasNextPage: orders.length === size, + } + }, [address, tokenId, side, chainId]) +} diff --git a/packages/mask/src/plugins/EVM/services.ts b/packages/mask/src/plugins/EVM/services.ts deleted file mode 100644 index 5914b704d67f..000000000000 --- a/packages/mask/src/plugins/EVM/services.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './apis' diff --git a/packages/mask/src/plugins/EVM/services/Asset/index.ts b/packages/mask/src/plugins/EVM/services/Asset/index.ts new file mode 100644 index 000000000000..04c0316b3a76 --- /dev/null +++ b/packages/mask/src/plugins/EVM/services/Asset/index.ts @@ -0,0 +1,117 @@ +import { unreachable } from '@dimensiondev/kit' +import { ChainId, NonFungibleAssetProvider } from '@masknet/web3-shared-evm' +import { NonFungibleTokenAPI, OpenSea, Rarible } from '@masknet/web3-providers' + +export interface AssetOption { + address: string + tokenId: string + chainId: ChainId + provider: NonFungibleAssetProvider +} + +export interface OrderOption { + address: string + tokenId: string + side: NonFungibleTokenAPI.OrderSide + chainId: ChainId + provider: NonFungibleAssetProvider + page: number + size: number +} + +export interface ListOption { + address: string + tokenId: string + chainId: ChainId + provider: NonFungibleAssetProvider +} + +export interface HistoryOption { + address: string + tokenId: string + chainId: ChainId + provider: NonFungibleAssetProvider + page: number + size: number +} + +export interface CollectionOption { + address: string + chainId: ChainId + provider: NonFungibleAssetProvider + page: number + size: number +} + +export async function getAsset(options: AssetOption) { + switch (options.provider) { + case NonFungibleAssetProvider.OPENSEA: + return OpenSea.getAsset(options.address, options.tokenId, { chainId: options.chainId }) + case NonFungibleAssetProvider.NFTSCAN: + return + case NonFungibleAssetProvider.RARIBLE: + return Rarible.getAsset(options.address, options.tokenId, { chainId: options.chainId }) + default: + unreachable(options.provider) + } +} + +export async function getOrders(options: OrderOption) { + const { provider, address, tokenId, side, chainId, page, size } = options + switch (provider) { + case NonFungibleAssetProvider.OPENSEA: + return OpenSea.getOrders(address, tokenId, side, { + chainId, + page, + size, + }) + case NonFungibleAssetProvider.NFTSCAN: + return [] + case NonFungibleAssetProvider.RARIBLE: + return Rarible.getOrders(address, tokenId, side, { chainId }) + default: + unreachable(provider) + } +} + +export async function getListings(options: ListOption) { + const { address, tokenId, chainId, provider } = options + switch (provider) { + case NonFungibleAssetProvider.OPENSEA: + return [] + case NonFungibleAssetProvider.NFTSCAN: + return [] + case NonFungibleAssetProvider.RARIBLE: + return Rarible.getListings(address, tokenId, { chainId }) + default: + unreachable(provider) + } +} + +export async function getHistory(options: HistoryOption) { + const { address, tokenId, chainId, provider, page, size } = options + switch (provider) { + case NonFungibleAssetProvider.OPENSEA: + return OpenSea.getHistory(address, tokenId, { chainId, page, size }) + case NonFungibleAssetProvider.NFTSCAN: + return [] + case NonFungibleAssetProvider.RARIBLE: + return Rarible.getHistory(address, tokenId) + default: + unreachable(provider) + } +} + +export async function getCollections(options: CollectionOption) { + const { address, chainId, provider, page, size } = options + switch (provider) { + case NonFungibleAssetProvider.OPENSEA: + return OpenSea.getCollections(address, { chainId, page, size }) + case NonFungibleAssetProvider.RARIBLE: + return [] + case NonFungibleAssetProvider.NFTSCAN: + return [] + default: + unreachable(provider) + } +} diff --git a/packages/mask/src/plugins/EVM/services/NFT/index.ts b/packages/mask/src/plugins/EVM/services/NFT/index.ts new file mode 100644 index 000000000000..29502742b4f9 --- /dev/null +++ b/packages/mask/src/plugins/EVM/services/NFT/index.ts @@ -0,0 +1,84 @@ +import { OpenSea, Rarible, NFTScan } from '@masknet/web3-providers' +import { unreachable } from '@dimensiondev/kit' +import { ChainId, NonFungibleAssetProvider } from '@masknet/web3-shared-evm' + +export interface NFTOption { + address: string + tokenId: string + chainId: ChainId + provider: NonFungibleAssetProvider +} + +export interface BalanceOption { + address: string + chainId: ChainId + provider: NonFungibleAssetProvider +} + +export interface ContractOption { + contractAddress: string + chainId: ChainId + provider: NonFungibleAssetProvider +} + +export interface NFTsByPaginationOption { + from: string + chainId: ChainId + provider: NonFungibleAssetProvider + page: number + size: number +} + +export async function getNFT(options: NFTOption) { + const { address, tokenId, chainId, provider } = options + switch (provider) { + case NonFungibleAssetProvider.OPENSEA: + return OpenSea.getToken(address, tokenId, chainId) + case NonFungibleAssetProvider.NFTSCAN: + return NFTScan.getToken(address, tokenId, chainId) + case NonFungibleAssetProvider.RARIBLE: + return Rarible.getToken(address, tokenId) + default: + unreachable(provider) + } +} + +export async function getNFTBalance(options: BalanceOption) { + const { address, chainId, provider } = options + switch (provider) { + case NonFungibleAssetProvider.RARIBLE: + case NonFungibleAssetProvider.OPENSEA: + case NonFungibleAssetProvider.NFTSCAN: + return NFTScan.getContractBalance(address) + + default: + unreachable(provider) + } +} + +export async function getNFTContract(options: ContractOption) { + const { contractAddress, chainId, provider } = options + switch (provider) { + case NonFungibleAssetProvider.OPENSEA: + case NonFungibleAssetProvider.NFTSCAN: + return OpenSea.getContract(contractAddress, chainId) + case NonFungibleAssetProvider.RARIBLE: + return + default: + unreachable(provider) + } +} + +export async function getNFTsByPagination(options: NFTsByPaginationOption) { + const { provider, from, chainId, page, size } = options + switch (provider) { + case NonFungibleAssetProvider.OPENSEA: + return OpenSea.getTokens(from, { chainId, page, size }) + case NonFungibleAssetProvider.NFTSCAN: + return NFTScan.getTokens(from, { chainId, page, size }) + case NonFungibleAssetProvider.RARIBLE: + return Rarible.getTokens(from, { chainId, page, size }) + default: + unreachable(provider) + } +} diff --git a/packages/mask/src/plugins/EVM/services/index.ts b/packages/mask/src/plugins/EVM/services/index.ts new file mode 100644 index 000000000000..01ad64779b05 --- /dev/null +++ b/packages/mask/src/plugins/EVM/services/index.ts @@ -0,0 +1,2 @@ +export * from './Asset' +export * from './NFT' diff --git a/packages/mask/src/plugins/EVM/apis/index.ts b/packages/mask/src/plugins/EVM/types/index.ts similarity index 100% rename from packages/mask/src/plugins/EVM/apis/index.ts rename to packages/mask/src/plugins/EVM/types/index.ts diff --git a/packages/web3-providers/src/rarible/pipes.ts b/packages/mask/src/plugins/EVM/utils/index.ts similarity index 100% rename from packages/web3-providers/src/rarible/pipes.ts rename to packages/mask/src/plugins/EVM/utils/index.ts diff --git a/packages/mask/src/plugins/Trader/SNSAdaptor/trader/FootnoteMenu.tsx b/packages/mask/src/plugins/Trader/SNSAdaptor/trader/FootnoteMenu.tsx index 6a224ba75763..2a59724c1d77 100644 --- a/packages/mask/src/plugins/Trader/SNSAdaptor/trader/FootnoteMenu.tsx +++ b/packages/mask/src/plugins/Trader/SNSAdaptor/trader/FootnoteMenu.tsx @@ -16,7 +16,7 @@ const useStyles = makeStyles()((theme) => ({ export interface FootnoteMenuOption { name: React.ReactNode - value: number + value: string | number } export interface FootnoteMenuProps { diff --git a/packages/mask/src/plugins/Wallet/SNSAdaptor/SelectNftContractDialog.tsx b/packages/mask/src/plugins/Wallet/SNSAdaptor/SelectNftContractDialog.tsx index a761177c4bc5..5616886b959b 100644 --- a/packages/mask/src/plugins/Wallet/SNSAdaptor/SelectNftContractDialog.tsx +++ b/packages/mask/src/plugins/Wallet/SNSAdaptor/SelectNftContractDialog.tsx @@ -20,8 +20,9 @@ import OpenInNewIcon from '@mui/icons-material/OpenInNew' import Fuse from 'fuse.js' import { useERC721ContractDetailed } from '@masknet/web3-shared-evm' import classNames from 'classnames' -import { useNFTscanFindAssets } from '../hooks/useNFTscanFindAssets' import { unionBy } from 'lodash-unified' +import { useNFTBalance } from '../../EVM/hooks' +import type { NonFungibleTokenAPI } from '@masknet/web3-providers' const useStyles = makeStyles()((theme) => ({ search: { @@ -106,11 +107,6 @@ const useStyles = makeStyles()((theme) => ({ }, })) -type NFTAsset = { - contractDetailed: ERC721ContractDetailed - balance?: number -} - export interface SelectNftContractDialogProps extends withClasses {} export function SelectNftContractDialog(props: SelectNftContractDialogProps) { @@ -151,7 +147,7 @@ export function SelectNftContractDialog(props: SelectNftContractDialogProps) { }, [id, setDialog]) //#endregion - const { value: assets } = useNFTscanFindAssets(account, !open) + const { value: assets } = useNFTBalance(account, !open) const erc721InDb = useERC721Tokens() const allContractsInDb = unionBy( @@ -207,8 +203,8 @@ export function SelectNftContractDialog(props: SelectNftContractDialogProps) { export interface SearchResultBoxProps extends withClasses { keyword: string - contractList: NFTAsset[] - searchedTokenList: NFTAsset[] + contractList: NonFungibleTokenAPI.ContractBalance[] + searchedTokenList: NonFungibleTokenAPI.ContractBalance[] onSubmit: (contract: ERC721ContractDetailed) => void } @@ -251,7 +247,7 @@ function SearchResultBox(props: SearchResultBoxProps) { } interface ContractListItemProps { - contract: NFTAsset + contract: NonFungibleTokenAPI.ContractBalance onSubmit: (contract: ERC721ContractDetailed) => void } diff --git a/packages/mask/src/plugins/Wallet/hooks/useNFTscanFindAssets.ts b/packages/mask/src/plugins/Wallet/hooks/useNFTscanFindAssets.ts deleted file mode 100644 index ddcbc1eefd5e..000000000000 --- a/packages/mask/src/plugins/Wallet/hooks/useNFTscanFindAssets.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { WalletRPC } from '../messages' -import { useAsyncRetry } from 'react-use' - -export function useNFTscanFindAssets(account: string, disabled = false) { - return useAsyncRetry(async () => { - if (!account || disabled) return null - return WalletRPC.nftscanFindAssets(account) - }, [account, disabled]) -} diff --git a/packages/mask/src/plugins/Wallet/services/assets.ts b/packages/mask/src/plugins/Wallet/services/assets.ts index 540820332686..9355ecc69789 100644 --- a/packages/mask/src/plugins/Wallet/services/assets.ts +++ b/packages/mask/src/plugins/Wallet/services/assets.ts @@ -5,7 +5,6 @@ import { ChainId, NonFungibleAssetProvider, createERC20Token, - createERC721Token, createNativeToken, CurrencyType, EthereumTokenType, @@ -23,8 +22,8 @@ import { import BigNumber from 'bignumber.js' import { values } from 'lodash-unified' import { EthereumAddress } from 'wallet.ts' +import { EVM_RPC } from '../../EVM/messages' import * as DebankAPI from '../apis/debank' -import * as OpenSeaAPI from '../apis/opensea' import * as ZerionAPI from '../apis/zerion' import { resolveChainByScope, resolveZerionAssetsScopeName } from '../pipes' import type { @@ -44,14 +43,16 @@ export async function getCollectionsNFT( size?: number, ): Promise<{ collections: ERC721TokenCollectionInfo[]; hasNextPage: boolean }> { if (provider === NonFungibleAssetProvider.OPENSEA) { - const { collections } = await OpenSeaAPI.getCollections(address, { chainId, page, size }) + const collections = await EVM_RPC.getCollections({ + address, + chainId, + provider, + page: page ?? 0, + size: size ?? 50, + }) return { - collections: collections.map((x) => ({ - name: x.name, - image: x.image_url || undefined, - slug: x.slug, - })), + collections, hasNextPage: collections.length === size, } } @@ -70,42 +71,26 @@ export async function getAssetsListNFT( size?: number, collection?: string, ): Promise<{ assets: ERC721TokenDetailed[]; hasNextPage: boolean }> { - if (provider === NonFungibleAssetProvider.OPENSEA) { - const { assets } = await OpenSeaAPI.getAssetsList(address, { chainId, page, size, collection }) + if (!EthereumAddress.isValid(address)) return { - assets: assets - .filter( - (x) => - ['non-fungible', 'semi-fungible'].includes(x.asset_contract.asset_contract_type) || - ['ERC721', 'ERC1155'].includes(x.asset_contract.schema_name), - ) - .map((x) => - createERC721Token( - { - chainId: ChainId.Mainnet, - type: EthereumTokenType.ERC721, - name: x.asset_contract.name, - symbol: x.asset_contract.symbol, - address: x.asset_contract.address, - }, - { - owner: address, - name: x.name || x.asset_contract.name, - description: x.description || x.asset_contract.symbol, - mediaUrl: x.image_url || x.image_preview_url || x.asset_contract.image_url || '', - }, - x.token_id, - ), - ), - hasNextPage: assets.length === size, + assets: [], + hasNextPage: false, } - } + const tokens = await EVM_RPC.getNFTsByPagination({ + from: address, + chainId, + provider, + page: page ?? 0, + size: size ?? 50, + }) return { - assets: [], - hasNextPage: false, + assets: tokens, + hasNextPage: tokens.length === size, } } +const filterAssetType = ['compound', 'trash', 'uniswap', 'uniswap-v2', 'nft'] + export async function getAssetsList( address: string, provider: FungibleAssetProvider, @@ -185,8 +170,6 @@ function formatAssetsFromDebank(data: WalletTokenRecord[], network?: NetworkType }) } -const filterAssetType = ['compound', 'trash', 'uniswap', 'uniswap-v2', 'nft'] - function formatAssetsFromZerion( data: ZerionAddressAsset[] | ZerionAddressCovalentAsset[], scope: SocketRequestAssetScope, diff --git a/packages/mask/src/plugins/Wallet/services/index.ts b/packages/mask/src/plugins/Wallet/services/index.ts index 3cb402518967..b24f93f2bd2c 100644 --- a/packages/mask/src/plugins/Wallet/services/index.ts +++ b/packages/mask/src/plugins/Wallet/services/index.ts @@ -11,6 +11,5 @@ export * from './domain' export * from './cloud' export * from './estimateGasFee' export * from './tokenPrice' -export * from './nftscan' -export * from './wallet' +export * from './wallet/index' export * from './legacyWallet' diff --git a/packages/mask/src/tsconfig.json b/packages/mask/src/tsconfig.json index 54e75d0b9e6c..28176677c278 100644 --- a/packages/mask/src/tsconfig.json +++ b/packages/mask/src/tsconfig.json @@ -18,6 +18,7 @@ { "path": "../../shared-base" }, { "path": "../../web3-shared/evm" }, { "path": "../../web3-shared/flow" }, + { "path": "../../web3-providers" }, { "path": "../../theme" }, { "path": "../../icons" }, { "path": "../../plugin-infra" }, diff --git a/packages/mask/src/utils/hooks/useSettingSwitcher.tsx b/packages/mask/src/utils/hooks/useSettingSwitcher.tsx index 4ed88865f9e8..df624d176e97 100644 --- a/packages/mask/src/utils/hooks/useSettingSwitcher.tsx +++ b/packages/mask/src/utils/hooks/useSettingSwitcher.tsx @@ -4,7 +4,7 @@ import { useValueRef } from '@masknet/shared' import ActionButton from '../../extension/options-page/DashboardComponents/ActionButton' import type { InternalSettings } from '../../settings/createSettings' -export function useSettingsSwitcher>( +export function useSettingsSwitcher>( settings: S, options: T[], resolver: (option: T) => string, diff --git a/packages/mask/src/web3/UI/ERC721ContractSelectPanel.tsx b/packages/mask/src/web3/UI/ERC721ContractSelectPanel.tsx index 4d91cf5c1bd2..19f823620c07 100644 --- a/packages/mask/src/web3/UI/ERC721ContractSelectPanel.tsx +++ b/packages/mask/src/web3/UI/ERC721ContractSelectPanel.tsx @@ -9,7 +9,7 @@ import { useRemoteControlledDialog } from '@masknet/shared' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { SelectNftContractDialogEvent, WalletMessages } from '../../plugins/Wallet/messages' import { useI18N } from '../../utils' -import { useNFTscanFindAssets } from '../../plugins/Wallet/hooks/useNFTscanFindAssets' +import { useNFTBalance } from '../../plugins/EVM/hooks/useNFTBalance' interface StyleProps { hasIcon: boolean @@ -72,7 +72,7 @@ export function ERC721ContractSelectPanel(props: ERC721TokenSelectPanelProps) { const account = useAccount() const { classes } = useStyles({ hasIcon: Boolean(contract?.iconURL) }) const { value: balanceFromChain, loading: loadingFromChain } = useERC721ContractBalance(contract?.address, account) - const { value: assets, loading: loadingFromNFTscan } = useNFTscanFindAssets(account, !contract) + const { value: assets, loading: loadingBalanceFromNFTscan } = useNFTBalance(account, !contract) const { t } = useI18N() @@ -84,7 +84,7 @@ export function ERC721ContractSelectPanel(props: ERC721TokenSelectPanelProps) { onBalanceChange(balance) - const loading = (loadingFromChain || loadingFromNFTscan) && !balance + const loading = (loadingFromChain || loadingBalanceFromNFTscan) && !balance //#region select contract const [id] = useState(uuid()) diff --git a/packages/plugins/Flow/src/apis/getFungibleAssets.ts b/packages/plugins/Flow/src/apis/getFungibleAssets.ts index d9f58c984f9b..bc895c1e7770 100644 --- a/packages/plugins/Flow/src/apis/getFungibleAssets.ts +++ b/packages/plugins/Flow/src/apis/getFungibleAssets.ts @@ -1,6 +1,6 @@ import { Pagination, Web3Plugin, CurrencyType } from '@masknet/plugin-infra' import { ChainId, createClient, getTokenConstants } from '@masknet/web3-shared-flow' -import { getTokenPrice } from '@masknet/web3-providers' +import { CoinGecko } from '@masknet/web3-providers' import { createFungibleAsset, createFungibleToken } from '../helpers' import { rightShift } from '@masknet/web3-shared-base' @@ -47,7 +47,7 @@ async function getTokenBalance( async function getAssetFUSD(chainId: ChainId, account: string) { const { FUSD_ADDRESS = '', FUNGIBLE_TOKEN_ADDRESS = '' } = getTokenConstants(chainId) - const price = await getTokenPrice('usd-coin', CurrencyType.USD) + const price = await CoinGecko.getTokenPrice('usd-coin', CurrencyType.USD) const balance = await getTokenBalance(chainId, account, 8, { fungibleTokenAddress: FUNGIBLE_TOKEN_ADDRESS, tokenAddress: FUSD_ADDRESS, @@ -64,7 +64,7 @@ async function getAssetFUSD(chainId: ChainId, account: string) { async function getAssetFLOW(chainId: ChainId, account: string) { const { FLOW_ADDRESS = '', FUNGIBLE_TOKEN_ADDRESS = '' } = getTokenConstants(chainId) - const price = await getTokenPrice('flow', CurrencyType.USD) + const price = await CoinGecko.getTokenPrice('flow', CurrencyType.USD) const balance = await getTokenBalance(chainId, account, 8, { fungibleTokenAddress: FUNGIBLE_TOKEN_ADDRESS, tokenAddress: FLOW_ADDRESS, @@ -82,7 +82,7 @@ async function getAssetFLOW(chainId: ChainId, account: string) { async function getAssetTether(chainId: ChainId, account: string) { const { TETHER_ADDRESS = '', FUNGIBLE_TOKEN_ADDRESS = '' } = getTokenConstants(chainId) - const price = await getTokenPrice('tether', CurrencyType.USD) + const price = await CoinGecko.getTokenPrice('tether', CurrencyType.USD) const balance = await getTokenBalance(chainId, account, 8, { fungibleTokenAddress: FUNGIBLE_TOKEN_ADDRESS, tokenAddress: TETHER_ADDRESS, diff --git a/packages/shared/src/UI/components/TokenIcon/index.tsx b/packages/shared/src/UI/components/TokenIcon/index.tsx index 42107cce722c..5e2752ac5023 100644 --- a/packages/shared/src/UI/components/TokenIcon/index.tsx +++ b/packages/shared/src/UI/components/TokenIcon/index.tsx @@ -44,16 +44,16 @@ export interface TokenIconProps extends withClasses<'icon'> { } export function TokenIcon(props: TokenIconProps) { - const { address, logoURI, name, chainId, AvatarProps, classes } = props - const _chainId = useChainId() + const currentChainId = useChainId() + const { address, logoURI, name, chainId = currentChainId, AvatarProps, classes } = props let _logoURI = logoURI if (!logoURI && isSameAddress(getTokenConstants().NATIVE_TOKEN_ADDRESS, formatEthereumAddress(address))) { - const nativeToken = getChainDetailed(chainId ?? _chainId) + const nativeToken = getChainDetailed(chainId) _logoURI = nativeToken?.nativeCurrency.logoURI } - const { TOKEN_ASSET_BASE_URI } = useTokenAssetBaseURLConstants(chainId ?? _chainId) + const { TOKEN_ASSET_BASE_URI } = useTokenAssetBaseURLConstants(chainId) const fallbackLogos = getFallbackIcons(address, TOKEN_ASSET_BASE_URI ?? []) const tokenBlockie = useBlockie(address) diff --git a/packages/web3-providers/package.json b/packages/web3-providers/package.json index 98353ba50045..fab7ba579e2e 100644 --- a/packages/web3-providers/package.json +++ b/packages/web3-providers/package.json @@ -5,6 +5,10 @@ "types": "./dist/index.d.ts", "dependencies": { "@masknet/plugin-infra": "workspace:*", + "@masknet/web3-shared-evm": "workspace:*", + "@masknet/web3-shared-base": "workspace:*", + "bignumber.js": "^9.0.1", + "date-fns": "^2.27.0", "json-stable-stringify": "^1.0.1", "lodash-unified": "1.0.1", "urlcat": "^2.0.4" diff --git a/packages/web3-providers/src/NFTScan/constants.ts b/packages/web3-providers/src/NFTScan/constants.ts new file mode 100644 index 000000000000..ee42f3ffdebe --- /dev/null +++ b/packages/web3-providers/src/NFTScan/constants.ts @@ -0,0 +1,10 @@ +import urlcat from 'urlcat' + +export const NFTSCAN_ID = 't9k2o5GC' +export const NFTSCAN_SECRET = '21da1d638ef5d0bf76e37aa5c2da7fd789ade9e3' +export const NFTSCAN_URL = 'https://restapi.nftscan.com' +export const NFTSCAN_BASE_API = urlcat(NFTSCAN_URL, '/api/v1') +export const NFTSCAN_ACCESS_TOKEN_URL = urlcat(NFTSCAN_URL, '/gw/token', { + apiKey: NFTSCAN_ID, + apiSecret: NFTSCAN_SECRET, +}) diff --git a/packages/web3-providers/src/NFTScan/index.ts b/packages/web3-providers/src/NFTScan/index.ts new file mode 100644 index 000000000000..04a1bd175994 --- /dev/null +++ b/packages/web3-providers/src/NFTScan/index.ts @@ -0,0 +1,106 @@ +import { ChainId, createERC721ContractDetailed, createERC721Token } from '@masknet/web3-shared-evm' +import addSeconds from 'date-fns/addSeconds' +import isBefore from 'date-fns/isBefore' +import urlcat from 'urlcat' +import type { NonFungibleTokenAPI } from '..' +import { NFTSCAN_ACCESS_TOKEN_URL, NFTSCAN_BASE_API } from './constants' +import type { NFTScanAsset, NFT_Assets } from './types' + +const tokenCache = new Map<'token', { token: string; expiration: Date }>() + +async function getToken() { + const token = tokenCache.get('token') + if (token && isBefore(Date.now(), token.expiration)) { + return token.token + } + const response = await fetch(NFTSCAN_ACCESS_TOKEN_URL, { mode: 'cors' }) + const { + data, + }: { + data: { accessToken: string; expiration: number } + } = await response.json() + tokenCache.set('token', { + token: data.accessToken, + expiration: addSeconds(Date.now(), data.expiration), + }) + return data.accessToken +} + +async function fetchAsset(path: string, body?: unknown) { + const response = await fetch(urlcat(NFTSCAN_BASE_API, path), { + method: 'POST', + headers: { 'Access-Token': await getToken() }, + body: JSON.stringify(body), + }) + if (!response.ok) return + return response.json() as Promise<{ data: T }> +} + +function createERC721ContractDetailedFromAssetContract(asset: NFTScanAsset) { + return createERC721ContractDetailed(ChainId.Mainnet, asset.trade_contract, undefined, asset.trade_symbol) +} + +function createERC721TokenAsset(asset: NFTScanAsset) { + const payload: { + name?: string + description?: string + image?: string + } = JSON.parse(asset.nft_json) + const detailed = createERC721ContractDetailedFromAssetContract(asset) + return createERC721Token( + detailed, + { + name: payload.name ?? '', + description: payload.description ?? '', + mediaUrl: payload.image ?? '', + owner: asset.nft_holder ?? '', + }, + asset.token_id, + ) +} + +export class NFTScanAPI implements NonFungibleTokenAPI.Provider { + async getContractBalance(address: string) { + const response = await fetchAsset('getGroupByNftContract', { + erc: 'erc721', + user_address: address, + }) + if (!response) return [] + return response.data + .map((x) => { + const contractDetailed = createERC721ContractDetailed( + ChainId.Mainnet, + x.nft_contract_address, + x.nft_platform_name, + ) + const balance = x.nft_asset.length + return { contractDetailed, balance } + }) + .sort((a, b) => a.balance - b.balance) + } + + async getToken(address: string, tokenId: string, chainId: ChainId) { + const response = await fetchAsset('getSingleNft', { + nft_address: address, + token_id: tokenId, + }) + if (!response) return + return createERC721TokenAsset(response.data) + } + + async getTokens(from: string, { chainId = ChainId.Mainnet, page = 0, size = 50 }: NonFungibleTokenAPI.Options) { + const response = await fetchAsset<{ + content: NFTScanAsset[] + page_index: number + page_size: number + total: number + }>('getAllNftByUserAddress', { + page_size: size, + page_index: page, + use_address: from, + erc: 'erc721', + }) + if (!response) return [] + return response.data.content.map(createERC721TokenAsset) + } +} diff --git a/packages/web3-providers/src/NFTScan/types.ts b/packages/web3-providers/src/NFTScan/types.ts new file mode 100644 index 000000000000..af287e07ce59 --- /dev/null +++ b/packages/web3-providers/src/NFTScan/types.ts @@ -0,0 +1,28 @@ +export interface NFTScanAsset { + last_price: string + nft_asset_id: string + nft_content_uri: string + nft_cover: string + nft_create_hash: string + nft_create_time: string + nft_creator: string + nft_detail: string + nft_holder: string + nft_json: string + nft_value: string + token_id: string + + trade_contract: string + trade_symbol: string + trade_decimal: string +} + +export type NFT_Assets = { + nft_asset: NFTScanAsset[] + nft_asset_count: number + nft_contract_address: string + nft_platform_count: number + nft_platform_describe: string + nft_platform_image: string + nft_platform_name: string +} diff --git a/packages/web3-providers/src/coingecko/index.ts b/packages/web3-providers/src/coingecko/index.ts index bde5313c332d..40f40657bfde 100644 --- a/packages/web3-providers/src/coingecko/index.ts +++ b/packages/web3-providers/src/coingecko/index.ts @@ -1,19 +1,16 @@ import type { CurrencyType } from '@masknet/plugin-infra' import { COINGECKO_URL_BASE } from './constants' +import type { PriceAPI } from '..' -export interface CryptoPrice { - [token: string]: { - [key in CurrencyType]?: string +export class CoinGeckoAPI implements PriceAPI.Provider { + async getTokenPrice(tokenId: string, currency: CurrencyType) { + const requestPath = `${COINGECKO_URL_BASE}/simple/price?ids=${tokenId}&vs_currencies=${currency}` + const price = await fetch(requestPath).then((r) => r.json() as Promise) + return price[tokenId] } -} - -export async function getTokenPrice(tokenId: string, currency: CurrencyType) { - const requestPath = `${COINGECKO_URL_BASE}/simple/price?ids=${tokenId}&vs_currencies=${currency}` - const price = await fetch(requestPath).then((r) => r.json() as Promise) - return price[tokenId] -} -export async function getTokensPrice(tokenIds: string[], currency: CurrencyType) { - const requestPath = `${COINGECKO_URL_BASE}/simple/price?ids=${tokenIds}&vs_currencies=${currency}` - return fetch(requestPath).then((r) => r.json() as Promise) + async getTokensPrice(tokenIds: string[], currency: CurrencyType) { + const requestPath = `${COINGECKO_URL_BASE}/simple/price?ids=${tokenIds}&vs_currencies=${currency}` + return fetch(requestPath).then((r) => r.json() as Promise) + } } diff --git a/packages/web3-providers/src/index.ts b/packages/web3-providers/src/index.ts index 598e380c7843..c33e07297f79 100644 --- a/packages/web3-providers/src/index.ts +++ b/packages/web3-providers/src/index.ts @@ -1,3 +1,12 @@ -export * from './opensea' -export * from './coingecko' -export * from './rarible' +import { CoinGeckoAPI } from './coingecko' +import { OpenSeaAPI } from './opensea' +import { RaribleAPI } from './rarible' +import { NFTScanAPI } from './NFTScan' + +export * from './types' +export * from './opensea/utils' + +export const OpenSea = new OpenSeaAPI() +export const Rarible = new RaribleAPI() +export const NFTScan = new NFTScanAPI() +export const CoinGecko = new CoinGeckoAPI() diff --git a/packages/web3-providers/src/opensea/constants.ts b/packages/web3-providers/src/opensea/constants.ts new file mode 100644 index 000000000000..3ee4d082d912 --- /dev/null +++ b/packages/web3-providers/src/opensea/constants.ts @@ -0,0 +1,3 @@ +export const OPENSEA_ACCOUNT_URL = 'https://opensea.io/accounts/:address' +export const OPENSEA_API_KEY = 'c38fe2446ee34f919436c32db480a2e3' +export const OPENSEA_API_URL = 'https://api.opensea.io' diff --git a/packages/web3-providers/src/opensea/index.ts b/packages/web3-providers/src/opensea/index.ts index 336ce12bb910..484f409ee77f 100644 --- a/packages/web3-providers/src/opensea/index.ts +++ b/packages/web3-providers/src/opensea/index.ts @@ -1 +1,320 @@ -export {} +import { + ChainId, + ERC721TokenDetailed, + EthereumTokenType, + createERC721Token, + ERC721ContractDetailed, + createERC721ContractDetailed, + createNativeToken, + createERC20Token, +} from '@masknet/web3-shared-evm' +import BigNumber from 'bignumber.js' +import fromUnixTime from 'date-fns/fromUnixTime' +import isAfter from 'date-fns/isAfter' +import { head, uniqBy } from 'lodash-unified' +import urlcat from 'urlcat' +import type { NonFungibleTokenAPI } from '../types' +import { getOrderUnitPrice, getOrderUSDPrice } from './utils' +import type { + OpenSeaAssetContract, + OpenSeaAssetEvent, + OpenSeaAssetOrder, + OpenSeaCollection, + OpenSeaCustomAccount, + OpenSeaResponse, +} from './types' +import { OPENSEA_ACCOUNT_URL, OPENSEA_API_KEY, OPENSEA_API_URL } from './constants' + +async function fetchFromOpenSea(url: string, chainId: ChainId) { + if (![ChainId.Mainnet, ChainId.Rinkeby].includes(chainId)) return + try { + const response = await fetch(urlcat(OPENSEA_API_URL, url), { + method: 'GET', + mode: 'cors', + headers: { 'x-api-key': OPENSEA_API_KEY, Accept: 'application/json' }, + }) + return response.json() as Promise + } catch { + return + } +} + +function createERC721ContractFromAssetContract( + address: string, + chainId: ChainId, + assetContract?: OpenSeaAssetContract, +): ERC721ContractDetailed { + return createERC721ContractDetailed(chainId, address, assetContract?.name, assetContract?.token_symbol) +} + +function createERC721TokenFromAsset( + address: string, + tokenId: string, + chainId: ChainId, + asset: OpenSeaResponse, +): ERC721TokenDetailed { + return createERC721Token( + createERC721ContractFromAssetContract(address, chainId, asset?.asset_contract), + { + name: asset?.name ?? asset?.asset_contract.name ?? '', + description: asset?.description ?? '', + mediaUrl: asset?.image_url_original ?? asset?.image_url ?? asset?.image_preview_url ?? '', + owner: asset?.owner.address ?? '', + }, + tokenId, + ) +} + +function createTokenDetailed( + chainId: ChainId, + token: { + address: string + decimals: number + name: string + symbol: string + }, +) { + if (token.symbol === 'ETH') return createNativeToken(chainId) + return createERC20Token(chainId, token.address, token.decimals, token.name, token.symbol) +} + +function createAssetLink(account: OpenSeaCustomAccount | undefined) { + if (!account) return '' + return urlcat(OPENSEA_ACCOUNT_URL, { + address: account?.user?.username ?? account?.address, + }) +} + +function createNFTAsset(asset: OpenSeaResponse, chainId: ChainId): NonFungibleTokenAPI.Asset { + const desktopOrder = head( + asset.sell_orders?.sort((a, b) => + new BigNumber(getOrderUSDPrice(a.current_price, a.payment_token_contract?.usd_price) ?? 0) + .minus(getOrderUSDPrice(a.current_price, a.payment_token_contract?.usd_price) ?? 0) + .toNumber(), + ), + ) + + return { + is_verified: ['approved', 'verified'].includes(asset.collection?.safelist_request_status ?? ''), + // it's an IOS string as my inspection + is_auction: isAfter(Date.parse(`${asset.endTime ?? ''}Z`), Date.now()), + image_url: asset.image_url_original ?? asset.image_url ?? asset.image_preview_url ?? '', + asset_contract: { + name: asset.asset_contract.name, + description: asset.asset_contract.description, + schemaName: asset.asset_contract.schema_name, + }, + current_price: desktopOrder + ? new BigNumber( + getOrderUnitPrice( + desktopOrder.current_price, + desktopOrder.payment_token_contract?.decimals, + desktopOrder.quantity, + ) ?? 0, + ).toNumber() + : null, + current_symbol: desktopOrder?.payment_token_contract?.symbol ?? 'ETH', + owner: { + ...asset.owner, + link: createAssetLink(asset.owner), + }, + creator: { + ...asset.creator, + link: createAssetLink(asset.creator), + }, + token_id: asset.token_id, + token_address: asset.token_address, + traits: asset.traits, + safelist_request_status: asset.collection?.safelist_request_status ?? '', + description: asset.description, + name: asset.name ?? asset.collection.name, + collection_name: asset.collection.name, + animation_url: asset.animation_url, + end_time: asset.endTime + ? new Date(asset.endTime) + : desktopOrder + ? fromUnixTime(desktopOrder.listing_time) + : null, + order_payment_tokens: desktopOrder?.payment_token_contract + ? [createTokenDetailed(chainId, desktopOrder.payment_token_contract)] + : [], + offer_payment_tokens: uniqBy( + asset.collection.payment_tokens.map((x) => createTokenDetailed(chainId, x)), + (x) => x.address.toLowerCase(), + ).filter((x) => x.type === EthereumTokenType.ERC20), + slug: asset.collection.slug, + desktopOrder, + top_ownerships: asset.top_ownerships.map((x): { owner: NonFungibleTokenAPI.AssetOwner } => ({ + owner: { + address: x.owner.address, + profile_img_url: x.owner.profile_img_url, + user: { username: x.owner.user?.username ?? '' }, + link: '', + }, + })), + collection: asset.collection as unknown as NonFungibleTokenAPI.AssetCollection, + response_: asset as any, + } +} + +function createNFTHistory(event: OpenSeaAssetEvent): NonFungibleTokenAPI.History { + const accountPair = + event.event_type === 'successful' + ? { + from: { + username: event.seller?.user?.username, + address: event.seller?.address, + imageUrl: event.seller?.profile_img_url, + link: createAssetLink(event.seller), + }, + to: { + username: event.winner_account?.user?.username, + address: event.winner_account?.address, + imageUrl: event.winner_account?.profile_img_url, + link: createAssetLink(event.winner_account), + }, + } + : { + from: { + username: event.from_account?.user?.username, + address: event.from_account?.address, + imageUrl: event.from_account?.profile_img_url, + link: createAssetLink(event.from_account), + }, + to: { + username: event.to_account?.user?.username, + address: event.to_account?.address, + imageUrl: event.to_account?.profile_img_url, + link: createAssetLink(event.to_account), + }, + } + return { + id: event.id, + accountPair, + price: { + quantity: event.quantity, + asset: event.asset, + paymentToken: event.payment_token, + price: event.bid_amount ?? event.ending_price ?? event.starting_price, + }, + eventType: event.event_type, + transactionBlockExplorerLink: event.transaction?.blockExplorerLink, + timestamp: new Date(`${event.created_date}Z`).getTime(), + } +} + +function createAssetOrder(order: OpenSeaAssetOrder): NonFungibleTokenAPI.AssetOrder { + return { + created_time: order.created_time, + current_price: order.current_price, + current_bounty: order.current_bounty, + maker_account: { ...order.maker, link: '' }, + taker_account: { ...order.taker, link: '' }, + payment_token: order.payment_token, + payment_token_contract: order.payment_token_contract, + fee_recipient_account: order.fee_recipient, + cancelled_or_finalized: order.cancelled || order.finalized, + marked_invalid: order.marked_invalid, + approved_on_chain: order.approved_on_chain, + listing_time: order.listing_time, + side: order.side, + quantity: order.quantity, + expiration_time: order.expiration_time, + order_hash: order.order_hash, + } +} + +export class OpenSeaAPI implements NonFungibleTokenAPI.Provider { + async getAsset(address: string, tokenId: string, { chainId = ChainId.Mainnet }: { chainId?: ChainId } = {}) { + const requestPath = urlcat('/api/v1/asset/:address/:tokenId', { address, tokenId }) + const response = await fetchFromOpenSea(requestPath, chainId) + if (!response) return + return createNFTAsset(response, chainId) + } + + async getContract(address: string, chainId: ChainId) { + const requestPath = urlcat('/api/v1/asset_contract/:address', { address }) + const assetContract = await fetchFromOpenSea(requestPath, chainId) + return createERC721ContractFromAssetContract(address, chainId, assetContract) + } + + async getToken(address: string, tokenId: string, chainId: ChainId) { + const requestPath = urlcat('/api/v1/asset/:address/:tokenId', { address, tokenId }) + const response = await fetchFromOpenSea(requestPath, chainId) + if (!response) return + return createERC721TokenFromAsset(address, tokenId, chainId, response) + } + + async getTokens(from: string, opts: NonFungibleTokenAPI.Options) { + const { chainId = ChainId.Mainnet, page = 0, size = 50 } = opts + const requestPath = urlcat('/api/v1/assets', { + owner: from, + offset: page, + limit: size, + }) + const response = await fetchFromOpenSea<{ assets: OpenSeaResponse[] }>(requestPath, chainId) + return ( + response?.assets + .filter( + (x: OpenSeaResponse) => + ['non-fungible', 'semi-fungible'].includes(x.asset_contract.type) || + ['ERC721', 'ERC1155'].includes(x.asset_contract.schema_name), + ) + .map((asset: OpenSeaResponse) => createERC721TokenFromAsset(from, asset.token_id, chainId, asset)) ?? [] + ) + } + + async getHistory( + address: string, + tokenId: string, + { chainId = ChainId.Mainnet, page, size }: NonFungibleTokenAPI.Options = {}, + ) { + const requestPath = urlcat('/api/v1/events', { + asset_contract_address: address, + token_id: tokenId, + offset: page, + limit: size, + }) + const response = await fetchFromOpenSea<{ + asset_events: OpenSeaAssetEvent[] + }>(requestPath, chainId) + return response?.asset_events.map(createNFTHistory) ?? [] + } + + async getOrders( + address: string, + tokenId: string, + side: NonFungibleTokenAPI.OrderSide, + { chainId = ChainId.Mainnet, page, size }: NonFungibleTokenAPI.Options = {}, + ) { + const requestPath = urlcat('/wyvern/v1/orders', { + asset_contract_address: address, + token_id: tokenId, + side, + offset: page, + limit: size, + }) + const response = await fetchFromOpenSea<{ + orders: OpenSeaAssetOrder[] + }>(requestPath, chainId) + return response?.orders?.map(createAssetOrder) ?? [] + } + + async getCollections(address: string, { chainId = ChainId.Mainnet, page, size }: NonFungibleTokenAPI.Options = {}) { + const requestPath = urlcat('/api/v1/collections', { + address, + offset: page, + limit: size, + }) + const response = await fetchFromOpenSea<{ + collections: OpenSeaCollection[] + }>(requestPath, chainId) + return ( + response?.collections.map((x) => ({ + name: x.name, + image: x.image_url || undefined, + slug: x.slug, + })) ?? [] + ) + } +} diff --git a/packages/web3-providers/src/opensea/types.ts b/packages/web3-providers/src/opensea/types.ts new file mode 100644 index 000000000000..64b57d83c97d --- /dev/null +++ b/packages/web3-providers/src/opensea/types.ts @@ -0,0 +1,200 @@ +import type { NonFungibleTokenAPI } from '..' + +export interface OpenSeaFees { + opensea_seller_fee_basis_points: number + opensea_buyer_fee_basis_points: number + dev_seller_fee_basis_points: number + dev_buyer_fee_basis_points: number +} + +export interface Asset { + token_id: string + token_address: string + schema_name?: string + version?: string + name?: string + decimals?: number +} + +export interface OpenSeaAssetContract extends OpenSeaFees { + name: string + address: string + type: string + schema_name: string + seller_fee_basis_points: number + buyer_fee_basis_points: number + description: string + token_symbol: string + image_url: string + stats?: object + traits?: object[] + external_link?: string + wiki_link?: string +} + +interface NumericalTraitStats { + min: number + max: number +} + +interface OpenSeaTraitStats { + [traitName: string]: NumericalTraitStats | Record +} + +interface OpenSeaFungibleToken { + image_url?: string + eth_price?: string + usd_price?: string + name: string + symbol: string + decimals: number + address: string +} + +export interface OpenSeaCustomAccount { + address: string + profile_img_url: string + user?: { + username: string + } +} + +export interface OpenSeaCollection extends OpenSeaFees { + name: string + slug: string + editors: string[] + hidden: boolean + featured: boolean + created_date: string + description: string + image_url: string + largeImage_url: string + featured_image_url: string + stats: object + display_data: object + payment_tokens: OpenSeaFungibleToken[] + payout_address?: string + trait_stats: OpenSeaTraitStats + external_link?: string + wiki_link?: string + safelist_request_status: string +} + +export interface OpenSeaResponse extends Asset { + animation_url: string + asset_contract: OpenSeaAssetContract + collection: OpenSeaCollection + name: string + description: string + owner: OpenSeaCustomAccount + orders: NonFungibleTokenAPI.AssetOrder[] | null + buy_orders: NonFungibleTokenAPI.AssetOrder[] | null + sell_orders: NonFungibleTokenAPI.AssetOrder[] | null + is_presale: boolean + image_url: string + image_preview_url: string + image_url_original: string + image_url_thumbnail: string + opensea_link: string + external_link: string + traits: { + trait_type: string + value: string + }[] + num_sales: number + last_sale: AssetEvent | null + background_color: string | null + transfer_fee: string | null + transfer_fee_payment_token: OpenSeaFungibleToken | null + top_ownerships: { + owner: OpenSeaCustomAccount + quantity: string + }[] + creator: OpenSeaCustomAccount + endTime: string +} + +interface AssetEvent { + event_type: string + event_timestamp: number + auction_type: string + total_price: string + transaction: Transaction | null + payment_token: OpenSeaFungibleToken | null +} + +interface Transaction { + from_account: OpenSeaCustomAccount + to_account: OpenSeaCustomAccount + created_date: string + modified_date: string + transaction_hash: string + transaction_index: string + block_number: string + block_hash: string + timestamp: number +} + +export interface OpenSeaAssetEvent { + id: string + event_type: string + from_account?: OpenSeaCustomAccount + to_account?: OpenSeaCustomAccount + seller?: OpenSeaCustomAccount + winner_account?: OpenSeaCustomAccount + asset: { + id: string + decimals: number + image_url: string + image_original_url: string + image_preview_url: string + asset_contract: { + symbol: string + } + permalink: string + } + payment_token: OpenSeaFungibleToken + quantity: string + ending_price: string + bid_amount: string + starting_price: string + transaction: { + blockExplorerLink: string + id: string + } + assetQuantity: { + asset: { + decimals?: number + id: string + } + quantity: string + id: string + } + created_date: string +} + +export interface OpenSeaAssetOrder { + approved_on_chain: boolean + asset: OpenSeaResponse + listing_time: number + created_time?: string + + current_price?: string + current_bounty?: string + maker: OpenSeaCustomAccount + taker: OpenSeaCustomAccount + + payment_token?: string + payment_token_contract?: NonFungibleTokenAPI.AssetToken + fee_recipient?: NonFungibleTokenAPI.AssetToken + + cancelled?: boolean + finalized?: boolean + + marked_invalid?: boolean + + side: number + quantity: string + expiration_time: number + order_hash: string +} diff --git a/packages/web3-providers/src/opensea/utils.ts b/packages/web3-providers/src/opensea/utils.ts new file mode 100644 index 000000000000..ff4529630aaf --- /dev/null +++ b/packages/web3-providers/src/opensea/utils.ts @@ -0,0 +1,15 @@ +import BigNumber from 'bignumber.js' +import { pow10 } from '@masknet/web3-shared-base' + +export function getOrderUnitPrice(currentPrice?: string, decimals?: number, quantity?: string) { + if (!currentPrice || !decimals || !quantity) return + const _currentPrice = new BigNumber(currentPrice).div(pow10(decimals)) + const _quantity = new BigNumber(quantity).div(pow10(new BigNumber(quantity).eq(1) ? 0 : 8)) + return _currentPrice.dividedBy(_quantity).decimalPlaces(4, 1) +} + +export function getOrderUSDPrice(currentPrice?: string, usdPrice?: string, decimals?: number) { + if (!currentPrice || !decimals) return + const quantity = new BigNumber(currentPrice).div(pow10(decimals)) + return new BigNumber(usdPrice ?? 0).multipliedBy(quantity).decimalPlaces(2, 1) +} diff --git a/packages/web3-providers/src/rarible/constants.ts b/packages/web3-providers/src/rarible/constants.ts index 29827b817df1..d6baed4a413d 100644 --- a/packages/web3-providers/src/rarible/constants.ts +++ b/packages/web3-providers/src/rarible/constants.ts @@ -4,3 +4,5 @@ export const RaribleIPFSURL = 'https://ipfs.rarible.com/ipfs/' export const RaribleUserURL = 'https://rarible.com/user/' export const RaribleRopstenUserURL = 'https://ropsten.rarible.com/user/' export const RaribleMainnetURL = 'https://api-mainnet.rarible.com/marketplace/api/v4/' +export const RaribleMainnetAPI_URL = 'https://api-staging.rarible.com/protocol/v0.1' +export const RaribleMainnetSearchURL = 'https://api-mainnet.rarible.com/marketplace/search/v1/' diff --git a/packages/web3-providers/src/rarible/index.ts b/packages/web3-providers/src/rarible/index.ts index e199d8d5bcea..7704f76847d3 100644 --- a/packages/web3-providers/src/rarible/index.ts +++ b/packages/web3-providers/src/rarible/index.ts @@ -1,7 +1,12 @@ import urlcat from 'urlcat' -import { compact } from 'lodash-unified' -import stringify from 'json-stable-stringify' -import { RaribleChainURL, RaribleMainnetURL, RaribleUserURL } from './constants' +import { compact, first } from 'lodash-unified' +import { + ChainId, + createLookupTableResolver, + ERC721TokenDetailed, + EthereumTokenType, + FungibleTokenDetailed, +} from '@masknet/web3-shared-evm' import { Ownership, RaribleEventType, @@ -10,134 +15,301 @@ import { RaribleOfferResponse, RaribleProfileResponse, } from './types' -import { toRaribleImage } from './helpers' +import { + RaribleUserURL, + RaribleRopstenUserURL, + RaribleMainnetURL, + RaribleChainURL, + RaribleMainnetAPI_URL, +} from './constants' +import { toRaribleImage } from './utils' +import { NonFungibleTokenAPI } from '..' -async function fetchFromRarible(root: string, subPath: string, config = {} as RequestInit) { - const response = await ( - await fetch(urlcat(root, subPath), { - mode: 'cors', - ...config, - }) - ).json() +const resolveRaribleUserNetwork = createLookupTableResolver( + { + [ChainId.Mainnet]: RaribleUserURL, + [ChainId.Ropsten]: RaribleRopstenUserURL, + }, + RaribleUserURL, +) - return response as T +async function fetchFromRarible(url: string, path: string, init?: RequestInit) { + const response = await fetch(urlcat(url, path), { + mode: 'cors', + ...init, + }) + return response.json() as Promise } -export async function getProfilesFromRarible(addresses: (string | undefined)[]) { - return fetchFromRarible(RaribleMainnetURL, 'profiles/list', { +function getProfilesFromRarible(addresses: (string | undefined)[]) { + return fetchFromRarible(RaribleMainnetURL, '/profiles/list', { method: 'POST', - body: stringify(addresses), + body: JSON.stringify(addresses), headers: { 'content-type': 'application/json', }, }) } -export async function getNFTItem(tokenAddress: string, tokenId: string) { - const assetResponse = await fetchFromRarible( - RaribleChainURL, - urlcat('/v0.1/nft/items/:tokenAddress::tokenId', { - includeMeta: true, - tokenAddress, - tokenId, - }), - { - method: 'GET', - headers: { - 'content-type': 'application/json', - }, +function createERC721TokenFromAsset( + tokenAddress: string, + tokenId: string, + asset?: RaribleNFTItemMapResponse, +): ERC721TokenDetailed { + return { + contractDetailed: { + type: EthereumTokenType.ERC721, + chainId: ChainId.Mainnet, + address: tokenAddress, + name: asset?.meta.name ?? '', + symbol: '', + }, + info: { + name: asset?.meta.name ?? '', + description: asset?.meta.description ?? '', + mediaUrl: toRaribleImage(asset?.meta.image.url.ORIGINAL ?? asset?.meta.image.url.PREVIEW ?? ''), + owner: asset?.owners[0], }, - ) - return assetResponse + tokenId: tokenId, + } } -export async function getOffersFromRarible(tokenAddress: string, tokenId: string) { - const orders = await fetchFromRarible( - RaribleMainnetURL, - `items/${tokenAddress}:${tokenId}/offers`, - { - method: 'POST', - body: stringify({ size: 20 }), - headers: { - 'content-type': 'application/json', - }, - }, - ) - const profiles = await getProfilesFromRarible(orders.map((item) => item.maker)) - return orders.map((order) => { - const ownerInfo = profiles.find((owner) => owner.id === order.maker) - return { - unitPrice: order.buyPriceEth, - hash: order.signature, - makerAccount: { - user: { - username: ownerInfo?.name, - }, - address: ownerInfo?.id, - profile_img_url: toRaribleImage(ownerInfo?.image), - link: `${RaribleUserURL}${ownerInfo?.id ?? ''}`, - }, - } - }) +function createNFTAsset(asset: RaribleNFTItemMapResponse, chainId: ChainId): NonFungibleTokenAPI.Asset { + const owner = first(asset?.owners) + const creator = first(asset?.creators) + return { + is_verified: false, + is_auction: false, + token_address: asset.contract, + image_url: toRaribleImage(asset?.meta.image.url.ORIGINAL), + asset_contract: null, + owner: owner + ? { + address: owner, + profile_img_url: '', + user: { username: owner }, + link: '', + } + : null, + creator: creator + ? { + address: creator.account, + profile_img_url: '', + user: { username: creator.account }, + link: '', + } + : null, + traits: asset?.meta.attributes.map(({ key, value }) => ({ trait_type: key, value })), + description: asset?.meta.description ?? '', + name: asset?.meta.name ?? 'Unknown', + collection_name: '', + animation_url: asset.meta.animation?.url.PREVIEW, + current_price: 0, + current_symbol: 'ETH', + end_time: null, + order_payment_tokens: [] as FungibleTokenDetailed[], + offer_payment_tokens: [] as FungibleTokenDetailed[], + top_ownerships: owner + ? [ + { + owner: { + address: owner, + profile_img_url: '', + user: { username: owner }, + link: '', + }, + }, + ] + : [], + slug: '', + response_: asset, + token_id: asset.tokenId, + safelist_request_status: '', + } } -export async function getListingsFromRarible(tokenAddress: string, tokenId: string) { - const assets = await fetchFromRarible(RaribleMainnetURL, `items/${tokenAddress}:${tokenId}/ownerships`) - const listings = assets.filter((x) => x.selling) - const profiles = await getProfilesFromRarible(listings.map((x) => x.owner)) - return listings.map((asset) => { - const ownerInfo = profiles.find((owner) => owner.id === asset.owner) - return { - unitPrice: asset.priceEth, - hash: asset.signature, - makerAccount: { - user: { - username: ownerInfo?.name, - }, - address: ownerInfo?.id, - profile_img_url: toRaribleImage(ownerInfo?.image), - link: `${RaribleUserURL}${ownerInfo?.id ?? ''}`, - }, - } +function _getAsset(address: string, tokenId: string) { + const requestPath = urlcat('/v0.1/nft/items/:address::tokenId', { + includeMeta: true, + address, + tokenId, + }) + return fetchFromRarible(RaribleChainURL, requestPath, { + method: 'GET', + mode: 'cors', + headers: { 'content-type': 'application/json' }, }) } -export async function getHistoryFromRarible(tokenAddress: string, tokenId: string) { - let histories = await fetchFromRarible(RaribleMainnetURL, `activity`, { - method: 'POST', - body: stringify({ - // types: ['BID', 'BURN', 'BUY', 'CANCEL', 'CANCEL_BID', 'ORDER', 'MINT', 'TRANSFER', 'SALE'], - filter: { - '@type': 'by_item', - address: tokenAddress, - tokenId, - }, - size: 100, - }), - headers: { - 'content-type': 'application/json', - }, - }) +export class RaribleAPI implements NonFungibleTokenAPI.Provider { + async getAsset(address: string, tokenId: string, { chainId = ChainId.Mainnet }: { chainId?: ChainId } = {}) { + const asset = await _getAsset(address, tokenId) + if (!asset) return + return createNFTAsset(asset, chainId) + } + + async getToken(tokenAddress: string, tokenId: string) { + const asset = await _getAsset(tokenAddress, tokenId) + return createERC721TokenFromAsset(tokenAddress, tokenId, asset) + } - if (histories.length) { - histories = histories.filter((x) => Object.values(RaribleEventType).includes(x['@type'])) + async getTokens(from: string, opts: NonFungibleTokenAPI.Options): Promise { + const requestPath = urlcat('/ethereum/nft/items/byOwner', { owner: from, size: opts.size }) + interface Payload { + total: number + continuation: string + items: RaribleNFTItemMapResponse[] + } + const asset = await fetchFromRarible(RaribleMainnetAPI_URL, requestPath) + if (!asset) return [] + return asset.items.map((asset) => createERC721TokenFromAsset(asset.contract, asset.tokenId, asset)) + } + + async getOffers( + tokenAddress: string, + tokenId: string, + { chainId = ChainId.Mainnet }: NonFungibleTokenAPI.Options = {}, + ): Promise { + const requestPath = urlcat('/items/:tokenAddress::tokenId/offers', { tokenAddress, tokenId }) + const orders = await fetchFromRarible(RaribleMainnetURL, requestPath, { + method: 'POST', + body: JSON.stringify({ size: 20 }), + headers: { 'content-type': 'application/json' }, + }) + const profiles = await getProfilesFromRarible(orders.map((item) => item.maker)) + return orders.map((order): NonFungibleTokenAPI.AssetOrder => { + const ownerInfo = profiles.find((owner) => owner.id === order.maker) + return { + created_time: order.updateDate, + current_price: order.buyPriceEth, + current_bounty: order.fee, + payment_token: order.token, + listing_time: 0, + side: NonFungibleTokenAPI.OrderSide.Buy, + quantity: '1', + expiration_time: 0, + order_hash: order.signature, + approved_on_chain: false, + maker_account: { + user: { username: ownerInfo?.name ?? '' }, + address: ownerInfo?.id ?? '', + profile_img_url: toRaribleImage(ownerInfo?.image), + link: `${resolveRaribleUserNetwork(chainId as number)}${ownerInfo?.id ?? ''}`, + }, + } + }) } - const profiles = await getProfilesFromRarible( - compact([ - ...histories.map((history) => history.owner), - ...histories.map((history) => history.buyer), - ...histories.map((history) => history.from), - ]), - ) + async getListings( + tokenAddress: string, + tokenId: string, + { chainId = ChainId.Mainnet }: NonFungibleTokenAPI.Options = {}, + ): Promise { + const requestPath = urlcat('/items/:tokenAddress::tokenId/ownerships', { tokenAddress, tokenId }) + const assets = await fetchFromRarible(RaribleMainnetURL, requestPath) + const listings = assets.filter((x) => x.selling) + const profiles = await getProfilesFromRarible(listings.map((x) => x.owner)) + return listings.map((asset): NonFungibleTokenAPI.AssetOrder => { + const ownerInfo = profiles.find((owner) => owner.id === asset.owner) + return { + created_time: asset.date, + approved_on_chain: false, + current_price: asset.priceEth, + payment_token: asset.token, + listing_time: 0, + side: NonFungibleTokenAPI.OrderSide.Buy, + quantity: '1', + expiration_time: 0, + order_hash: asset.signature, + maker_account: { + user: { username: ownerInfo?.name ?? '' }, + address: ownerInfo?.id ?? '', + profile_img_url: toRaribleImage(ownerInfo?.image), + link: `${resolveRaribleUserNetwork(chainId as number)}${ownerInfo?.id ?? ''}`, + }, + } + }) + } - return histories.map((history) => { - const ownerInfo = profiles.find((profile) => profile.id === history.owner) - const fromInfo = profiles.find((profile) => profile.id === history.buyer || profile.id === history.from) - return { - ...history, - ownerInfo, - fromInfo, + async getOrders( + tokenAddress: string, + tokenId: string, + side: NonFungibleTokenAPI.OrderSide, + opts: NonFungibleTokenAPI.Options = {}, + ) { + switch (side) { + case NonFungibleTokenAPI.OrderSide.Buy: + return this.getOffers(tokenAddress, tokenId, opts) + case NonFungibleTokenAPI.OrderSide.Sell: + return this.getListings(tokenAddress, tokenId, opts) + default: + return [] } - }) + } + + async getHistory(tokenAddress: string, tokenId: string): Promise { + const response = await fetchFromRarible(RaribleMainnetURL, '/activity', { + method: 'POST', + body: JSON.stringify({ + // types: ['BID', 'BURN', 'BUY', 'CANCEL', 'CANCEL_BID', 'ORDER', 'MINT', 'TRANSFER', 'SALE'], + filter: { + '@type': 'by_item', + address: tokenAddress, + tokenId, + }, + size: 100, + }), + headers: { + 'content-type': 'application/json', + }, + }) + + const histories = response.filter((x) => Object.values(RaribleEventType).includes(x['@type'])) + const profiles = await getProfilesFromRarible( + compact([ + ...histories.map((history) => history.owner), + ...histories.map((history) => history.buyer), + ...histories.map((history) => history.from), + ]), + ) + + return histories.map((history) => { + const ownerInfo = profiles.find((profile) => profile.id === history.owner) + const fromInfo = profiles.find((profile) => profile.id === history.buyer || profile.id === history.from) + return { + id: history.id, + eventType: history['@type'], + timestamp: history.date.getTime() ?? 0, + price: { + quantity: '1', + price: history.price, + asset: { + id: fromInfo?.id, + decimals: 0, + image_url: fromInfo?.image, + image_original_url: '', + image_preview_url: '', + asset_contract: { + symbol: fromInfo?.type, + }, + permalink: '', + }, + }, + accountPair: { + from: { + username: fromInfo?.name, + address: fromInfo?.id, + imageUrl: fromInfo?.image, + link: '', + }, + to: { + username: ownerInfo?.name, + address: ownerInfo?.id, + imageUrl: ownerInfo?.image, + link: '', + }, + }, + } as NonFungibleTokenAPI.History + }) + } } diff --git a/packages/web3-providers/src/rarible/types.ts b/packages/web3-providers/src/rarible/types.ts index 3efd9b42663a..672a023c8121 100644 --- a/packages/web3-providers/src/rarible/types.ts +++ b/packages/web3-providers/src/rarible/types.ts @@ -32,9 +32,9 @@ export interface Ownership { tokenId: string owner: string value: number - date: Date - price: number - priceEth: number + date: string + price: string + priceEth: string buyToken: string buyTokenId: string status: string @@ -167,15 +167,15 @@ export interface RaribleOfferResponse { buyAssetType: string value: number signature: string - updateDate: Date + updateDate: string importantUpdateDate: Date updateStateDate: Date contractVersion: number - fee: number + fee: string sold: number canceled: boolean pending: RaribleTransferItem[] - buyPriceEth: number + buyPriceEth: string version: number id: string active: boolean @@ -199,8 +199,8 @@ export interface RaribleHistory { '@type': RaribleEventType id: string owner: string - value: number - price: number + value: string + price: string buyToken: string buyTokenId: string buyer?: string diff --git a/packages/web3-providers/src/rarible/helpers.ts b/packages/web3-providers/src/rarible/utils.ts similarity index 51% rename from packages/web3-providers/src/rarible/helpers.ts rename to packages/web3-providers/src/rarible/utils.ts index 07bbe45ac8eb..d78d6ffad7d8 100644 --- a/packages/web3-providers/src/rarible/helpers.ts +++ b/packages/web3-providers/src/rarible/utils.ts @@ -2,5 +2,6 @@ import { RaribleIPFSURL } from './constants' export function toRaribleImage(url?: string) { if (!url) return '' - return `${RaribleIPFSURL}${url.replace('ipfs://ipfs/', '')}` + if (url.startsWith('ipfs://ipfs/')) return url.replace(/^ipfs:\/\/ipfs\//, RaribleIPFSURL) + return url } diff --git a/packages/web3-providers/src/types.ts b/packages/web3-providers/src/types.ts new file mode 100644 index 000000000000..9d61ba87748a --- /dev/null +++ b/packages/web3-providers/src/types.ts @@ -0,0 +1,199 @@ +import type { CurrencyType } from '@masknet/plugin-infra' +import type { + ChainId, + ERC20TokenDetailed, + ERC721ContractDetailed, + ERC721TokenDetailed, + NativeTokenDetailed, +} from '@masknet/web3-shared-evm' + +export namespace PriceAPI { + export interface CryptoPrice { + [token: string]: { + [key in CurrencyType]?: string + } + } + + export interface Provider { + getTokenPrice( + tokenId: string, + currency: CurrencyType, + ): Promise<{ + [key in CurrencyType]?: string + }> + getTokensPrice(tokenIds: string[], currency: CurrencyType): Promise + } +} + +export namespace NonFungibleTokenAPI { + export enum OrderSide { + Buy = 0, + Sell = 1, + } + export interface AssetOwner { + address: string + profile_img_url?: string + user?: { + username: string + } + link: string + } + + export interface AssetTrait { + trait_type: string + value: string + } + + export interface AssetToken { + image_url?: string + eth_price?: string + usd_price?: string + name: string + symbol: string + decimals: number + address: string + } + + export interface AssetOrder { + created_time?: string + current_price?: string + current_bounty?: string + maker_account?: AssetOwner + taker_account?: AssetOwner + payment_token?: string + payment_token_contract?: AssetToken + fee_recipient_account?: AssetToken + cancelled_or_finalized?: boolean + marked_invalid?: boolean + approved_on_chain: boolean + listing_time: number + side: number + quantity: string + expiration_time: number + order_hash: string + } + + export interface AssetCollection { + name: string + slug: string + editors: string[] + hidden: boolean + featured: boolean + created_date: string + description: string + image_url: string + largeImage_url: string + featured_image_url: string + stats: object + display_data: object + payment_tokens: AssetToken[] + payout_address?: string + trait_stats: AssetTrait + external_link?: string + wiki_link?: string + safelist_request_status: string + } + + export interface Asset { + is_verified: boolean + collection?: AssetCollection + is_auction: boolean + image_url: string + asset_contract: { name: string; description: string; schemaName: string } | null + current_price: number | null + current_symbol: string + owner: AssetOwner | null + creator: AssetOwner | null + token_id: string + token_address: string + traits: AssetTrait[] + safelist_request_status: string + description: string + name: string + collection_name: string + animation_url?: string + end_time: Date | null + order_payment_tokens: (ERC20TokenDetailed | NativeTokenDetailed)[] + offer_payment_tokens: (ERC20TokenDetailed | NativeTokenDetailed)[] + slug: string | null + desktopOrder?: AssetOrder + top_ownerships: { + owner: AssetOwner + }[] + + response_: any + } + + export interface History { + id: string + accountPair: { + from?: { + username?: string + address?: string + imageUrl?: string + link: string + } + to?: { + username?: string + address?: string + imageUrl?: string + link: string + } + } + price?: { + quantity: string + price: string + asset?: { + id: string + decimals: number + image_url: string + image_original_url: string + image_preview_url: string + asset_contract: { + symbol: string + } + permalink: string + } + paymentToken?: AssetToken + } + + eventType: string + transactionBlockExplorerLink?: string + timestamp: number + } + + export interface Collection { + name: string + image?: string + slug: string + } + + export interface ContractBalance { + contractDetailed: ERC721ContractDetailed + balance?: number + } + + export interface Options { + chainId?: ChainId + page?: number + size?: number + } + + export interface Provider { + getContract?: (address: string, chainId: ChainId) => Promise + getContractBalance?: (address: string) => Promise + getAsset?: (address: string, tokenId: string, opts?: { chainId?: ChainId }) => Promise + getToken?: (address: string, tokenId: string, chainId: ChainId) => Promise + getTokens?: (from: string, opts: Options) => Promise + getHistory?: (address: string, tokenId: string, opts?: Options) => Promise + getListings?: (address: string, tokenId: string, opts?: Options) => Promise + getOffers?: (address: string, tokenId: string, opts?: Options) => Promise + getOrders?: ( + address: string, + tokenId: string, + side: NonFungibleTokenAPI.OrderSide, + opts?: Options, + ) => Promise + getCollections?: (address: string, opts?: Options) => Promise + } +} diff --git a/packages/web3-providers/tsconfig.json b/packages/web3-providers/tsconfig.json index 7f399cce3bb0..e2eb901529ec 100644 --- a/packages/web3-providers/tsconfig.json +++ b/packages/web3-providers/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "rootDir": "./src/", + "rootDir": "./src", "outDir": "./dist/", "tsBuildInfoFile": "./dist/.tsbuildinfo" }, diff --git a/packages/web3-shared/base/utils/number.ts b/packages/web3-shared/base/utils/number.ts index acd76a62fdf1..b3b7b9570dc2 100644 --- a/packages/web3-shared/base/utils/number.ts +++ b/packages/web3-shared/base/utils/number.ts @@ -52,3 +52,8 @@ export function rightShift(n: BigNumber.Value, m: number | undefined | null) { export function leftShift(n: BigNumber.Value, m: number | undefined | null) { return new BigNumber(n).shiftedBy(-(m ?? 0)) } + +/** a / b */ +export function dividedBy(a: BigNumber.Value, b: BigNumber.Value) { + return new BigNumber(a).dividedBy(b) +} diff --git a/packages/web3-shared/evm/pipes/index.ts b/packages/web3-shared/evm/pipes/index.ts index 1f47d001aeab..16f2f6b9b2c4 100644 --- a/packages/web3-shared/evm/pipes/index.ts +++ b/packages/web3-shared/evm/pipes/index.ts @@ -146,6 +146,10 @@ export function resolveCollectibleProviderLink(chainId: ChainId, provider: NonFu case NonFungibleAssetProvider.OPENSEA: if (chainId === ChainId.Rinkeby) return `https://testnets.opensea.io` return `https://opensea.io` + case NonFungibleAssetProvider.RARIBLE: + return 'https://rarible.com' + case NonFungibleAssetProvider.NFTSCAN: + return 'https://nftscan.com' default: unreachable(provider) } @@ -157,6 +161,10 @@ export function resolveCollectibleAssetLink(chainId: ChainId, provider: NonFungi if (chainId === ChainId.Rinkeby) return `https://testnets.opensea.io/assets` if (chainId === ChainId.Matic) return `https://opensea.io/assets/matic` return `https://opensea.io/assets` + case NonFungibleAssetProvider.RARIBLE: + return '' + case NonFungibleAssetProvider.NFTSCAN: + return '' default: unreachable(provider) } @@ -173,6 +181,10 @@ export function resolveCollectibleLink( address, tokenId, }) + case NonFungibleAssetProvider.RARIBLE: + return '' + case NonFungibleAssetProvider.NFTSCAN: + return '' default: unreachable(provider) } diff --git a/packages/web3-shared/evm/types/index.ts b/packages/web3-shared/evm/types/index.ts index 4c763309ecdf..42facfc48c4c 100644 --- a/packages/web3-shared/evm/types/index.ts +++ b/packages/web3-shared/evm/types/index.ts @@ -550,6 +550,8 @@ export enum FungibleAssetProvider { export enum NonFungibleAssetProvider { OPENSEA = 'OpenSea', + RARIBLE = 'Rarible', + NFTSCAN = 'NFTScan', } export type UnboxTransactionObject = T extends NonPayableTransactionObject diff --git a/packages/web3-shared/evm/utils/token.ts b/packages/web3-shared/evm/utils/token.ts index c113aeed509e..4877a5aea7e5 100644 --- a/packages/web3-shared/evm/utils/token.ts +++ b/packages/web3-shared/evm/utils/token.ts @@ -125,7 +125,6 @@ export function createERC20Tokens( return accumulator }, base) } -//#endregion export function addGasMargin(value: BigNumber.Value, scale = 3000) { return new BigNumber(value).multipliedBy(new BigNumber(10000).plus(scale)).dividedToIntegerBy(10000) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dab1909e7c64..06573239a2a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -302,6 +302,7 @@ importers: '@masknet/shared-base': workspace:* '@masknet/theme': workspace:* '@masknet/web3-contracts': workspace:* + '@masknet/web3-providers': workspace:* '@masknet/web3-shared-base': workspace:* '@masknet/web3-shared-evm': workspace:* '@msgpack/msgpack': ^2.7.1 @@ -436,6 +437,7 @@ importers: '@masknet/shared-base': link:../shared-base '@masknet/theme': link:../theme '@masknet/web3-contracts': link:../web3-contracts + '@masknet/web3-providers': link:../web3-providers '@masknet/web3-shared-base': link:../web3-shared/base '@masknet/web3-shared-evm': link:../web3-shared/evm '@msgpack/msgpack': 2.7.1 @@ -888,11 +890,19 @@ importers: packages/web3-providers: specifiers: '@masknet/plugin-infra': workspace:* + '@masknet/web3-shared-base': workspace:* + '@masknet/web3-shared-evm': workspace:* + bignumber.js: ^9.0.1 + date-fns: ^2.27.0 json-stable-stringify: ^1.0.1 lodash-unified: 1.0.1 urlcat: ^2.0.4 dependencies: '@masknet/plugin-infra': link:../plugin-infra + '@masknet/web3-shared-base': link:../web3-shared/base + '@masknet/web3-shared-evm': link:../web3-shared/evm + bignumber.js: 9.0.1 + date-fns: 2.27.0 json-stable-stringify: 1.0.1 lodash-unified: 1.0.1_bd4845f8c1bf1d81a616b60758034d0a urlcat: 2.0.4