diff --git a/cspell.json b/cspell.json index aa4c82a87783..1bc112a68844 100644 --- a/cspell.json +++ b/cspell.json @@ -264,6 +264,7 @@ "xlarge", "xlink", "zerion", + "zora", "zubin" ], "ignoreWords": [ diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/CollectibleProviderIcon.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/CollectibleProviderIcon.tsx index 0b7815f6f68b..370d5441737d 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/CollectibleProviderIcon.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/CollectibleProviderIcon.tsx @@ -1,6 +1,7 @@ import { makeStyles } from '@masknet/theme' import { OpenSeaIcon } from '../../../resources/OpenSeaIcon' import { RaribleIcon } from '../../../resources/RaribleIcon' +import { ZoraIcon } from '../../../resources/ZoraIcon' import { unreachable } from '@dimensiondev/kit' import { NonFungibleAssetProvider } from '@masknet/web3-shared-evm' @@ -37,6 +38,8 @@ export function CollectibleProviderIcon(props: CollectibleProviderIconProps) { return case NonFungibleAssetProvider.RARIBLE: return + case NonFungibleAssetProvider.ZORA: + return default: unreachable(props.provider) } diff --git a/packages/mask/src/plugins/Collectible/base.ts b/packages/mask/src/plugins/Collectible/base.ts index a63f7ded5c20..0501d77e94db 100644 --- a/packages/mask/src/plugins/Collectible/base.ts +++ b/packages/mask/src/plugins/Collectible/base.ts @@ -16,6 +16,7 @@ export const base: Plugin.Shared.Definition = { postContent: new Set([ /opensea.io\/assets\/(0x[\dA-Fa-f]{40})\/(\d+)/, /rarible.com\/token\/(0x[\dA-Fa-f]{40}):(\d+)/, + /zora.co\/collections\/(0x[\dA-Fa-f]{40})\/\d+$/, ]), }, } diff --git a/packages/mask/src/plugins/Collectible/constants.ts b/packages/mask/src/plugins/Collectible/constants.ts index b87ff123d1a0..d1f392a8b4a9 100644 --- a/packages/mask/src/plugins/Collectible/constants.ts +++ b/packages/mask/src/plugins/Collectible/constants.ts @@ -12,6 +12,9 @@ export const openseaPathnameRegexMatcher = /^\/assets\/(0x[\dA-Fa-f]{40})\/(\d+) export const raribleHostnames = ['rarible.com', 'app.rarible.com', 'ropsten.rarible.com'] export const rariblePathnameRegexMatcher = /^\/token\/(0x[\dA-Fa-f]{40}):(\d+)/ +export const zoraHostnames = ['zora.co'] +export const zoraPathnameRegexMatcher = /^\/collections\/(0x[\dA-Fa-f]{40})\/(\d+)$/ + export const OpenSeaAPI_Key = 'c38fe2446ee34f919436c32db480a2e3' export const OpenSeaBaseURL = 'https://api.opensea.io/api/v1/' export const OpenSeaRinkebyBaseURL = 'https://rinkeby-api.opensea.io/api/v1/' diff --git a/packages/mask/src/plugins/Collectible/hooks/useAssetOrder.ts b/packages/mask/src/plugins/Collectible/hooks/useAssetOrder.ts index 1b4f2ca7f4b4..ae211e1ea6cd 100644 --- a/packages/mask/src/plugins/Collectible/hooks/useAssetOrder.ts +++ b/packages/mask/src/plugins/Collectible/hooks/useAssetOrder.ts @@ -32,6 +32,8 @@ export function useAssetOrder(provider: NonFungibleAssetProvider, token?: Collec return case NonFungibleAssetProvider.NFTSCAN: return + case NonFungibleAssetProvider.ZORA: + return default: unreachable(provider) } diff --git a/packages/mask/src/plugins/Collectible/pipes/index.ts b/packages/mask/src/plugins/Collectible/pipes/index.ts index b8c9dabd7f80..5f91ab391b21 100644 --- a/packages/mask/src/plugins/Collectible/pipes/index.ts +++ b/packages/mask/src/plugins/Collectible/pipes/index.ts @@ -56,6 +56,7 @@ export const resolveCollectibleProviderName = createLookupTableResolver { throw new Error(`Unknown provider type: ${providerType}.`) @@ -88,6 +89,13 @@ export const resolveLinkOnRarible = createLookupTableResolver( 'https://rarible.com', ) +export const resolveLinkOnZora = createLookupTableResolver( + { + [ChainId.Mainnet]: 'https://zora.co', + }, + 'https://zora.co', +) + export function resolveTraitLinkOnOpenSea(chainId: ChainId, slug: string, search: string, value: string) { if (chainId === ChainId.Rinkeby) { return `https://testnets.opensea.io/assets/${slug}?search[stringTraits][0][name]=${search}&search[stringTraits][0][values][0]=${value}` @@ -109,6 +117,8 @@ export function resolveAssetLinkOnCurrentProvider( return urlcat(resolveLinkOnRarible(chainId), '/token/:address/:id', { address, id }) case NonFungibleAssetProvider.NFTSCAN: return '' + case NonFungibleAssetProvider.ZORA: + return urlcat(resolveLinkOnZora(chainId), '/collections/:address/:id', { address, id }) default: return '' } @@ -127,6 +137,8 @@ export function resolveUserUrlOnCurrentProvider( return urlcat(resolveLinkOnOpenSea(chainId), `/${username ?? ''}`) case NonFungibleAssetProvider.NFTSCAN: return '' + case NonFungibleAssetProvider.ZORA: + return urlcat(resolveLinkOnZora(chainId), `/${address}`) default: return '' } @@ -140,6 +152,8 @@ export function resolveAvatarLinkOnCurrentProvider(chainId: ChainId, asset: any, return urlcat(resolveLinkOnRarible(chainId), `/collection/${asset.token_address ?? ''}`) case NonFungibleAssetProvider.NFTSCAN: return '' + case NonFungibleAssetProvider.ZORA: + return '' default: return '' } diff --git a/packages/mask/src/plugins/Collectible/utils.ts b/packages/mask/src/plugins/Collectible/utils.ts index d46e1fe977fe..c71f218d9360 100644 --- a/packages/mask/src/plugins/Collectible/utils.ts +++ b/packages/mask/src/plugins/Collectible/utils.ts @@ -4,6 +4,8 @@ import { openseaPathnameRegexMatcher, raribleHostnames, rariblePathnameRegexMatcher, + zoraHostnames, + zoraPathnameRegexMatcher, } from './constants' import { ChainId } from '@masknet/web3-shared-evm' import { WyvernSchemaName } from 'opensea-js/lib/types' @@ -14,7 +16,8 @@ export function checkUrl(url: string): boolean { return ( (openseaHostnames.includes(_url.hostname) && openseaPathnameRegexMatcher.test(_url.pathname)) || - (raribleHostnames.includes(_url.hostname) && rariblePathnameRegexMatcher.test(_url.pathname)) + (raribleHostnames.includes(_url.hostname) && rariblePathnameRegexMatcher.test(_url.pathname)) || + (zoraHostnames.includes(_url.hostname) && zoraPathnameRegexMatcher.test(_url.pathname)) ) } @@ -53,6 +56,17 @@ export function getAssetInfoFromURL(url?: string) { } // #endregion + // #region zora + const zoraMatched = _url.pathname.match(zoraPathnameRegexMatcher) + if (zoraMatched) { + return { + chain_id: _url.host.includes('rinkeby') ? ChainId.Rinkeby : ChainId.Mainnet, + address: zoraMatched[1], + token_id: zoraMatched[2], + } + } + // #endregion + // nothing matched return } diff --git a/packages/mask/src/plugins/EVM/services/Asset/index.ts b/packages/mask/src/plugins/EVM/services/Asset/index.ts index 272066f1ba89..63fbd5adebe6 100644 --- a/packages/mask/src/plugins/EVM/services/Asset/index.ts +++ b/packages/mask/src/plugins/EVM/services/Asset/index.ts @@ -1,6 +1,6 @@ import { unreachable } from '@dimensiondev/kit' import { ChainId, NonFungibleAssetProvider } from '@masknet/web3-shared-evm' -import { NonFungibleTokenAPI, OpenSea, Rarible } from '@masknet/web3-providers' +import { NonFungibleTokenAPI, OpenSea, Rarible, Zora } from '@masknet/web3-providers' export interface AssetOption { address: string @@ -56,6 +56,8 @@ export async function getAsset(options: AssetOption) { return case NonFungibleAssetProvider.RARIBLE: return Rarible.getAsset(options.address, options.tokenId, { chainId: options.chainId }) + case NonFungibleAssetProvider.ZORA: + return Zora.getAsset(options.address, options.tokenId) default: unreachable(options.provider) } @@ -74,6 +76,8 @@ export async function getOrders(options: OrderOption) { return [] case NonFungibleAssetProvider.RARIBLE: return Rarible.getOrders(address, tokenId, side, { chainId }) + case NonFungibleAssetProvider.ZORA: + return Zora.getOrders(address, tokenId, side) default: unreachable(provider) } @@ -88,6 +92,8 @@ export async function getListings(options: ListOption) { return [] case NonFungibleAssetProvider.RARIBLE: return Rarible.getListings(address, tokenId, { chainId }) + case NonFungibleAssetProvider.ZORA: + return Zora.getListings(address, tokenId) default: unreachable(provider) } @@ -102,6 +108,8 @@ export async function getHistory(options: HistoryOption) { return [] case NonFungibleAssetProvider.RARIBLE: return Rarible.getHistory(address, tokenId) + case NonFungibleAssetProvider.ZORA: + return Zora.getHistory(address, tokenId) default: unreachable(provider) } @@ -116,6 +124,8 @@ export async function getCollections(options: CollectionOption) { return defaultPageableData case NonFungibleAssetProvider.NFTSCAN: return defaultPageableData + case NonFungibleAssetProvider.ZORA: + return defaultPageableData 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 index 11bf09b1d6dc..d4fb2dcc7005 100644 --- a/packages/mask/src/plugins/EVM/services/NFT/index.ts +++ b/packages/mask/src/plugins/EVM/services/NFT/index.ts @@ -1,4 +1,4 @@ -import { OpenSea, Rarible, NFTScan } from '@masknet/web3-providers' +import { OpenSea, Rarible, NFTScan, Zora } from '@masknet/web3-providers' import { unreachable } from '@dimensiondev/kit' import { ChainId, NonFungibleAssetProvider } from '@masknet/web3-shared-evm' @@ -39,6 +39,8 @@ export async function getNFT(options: NFTOption) { return NFTScan.getToken(address, tokenId) case NonFungibleAssetProvider.RARIBLE: return Rarible.getToken(address, tokenId) + case NonFungibleAssetProvider.ZORA: + return Zora.getToken(address, tokenId) default: unreachable(provider) } @@ -50,6 +52,7 @@ export async function getNFTBalance(options: BalanceOption) { case NonFungibleAssetProvider.RARIBLE: case NonFungibleAssetProvider.OPENSEA: case NonFungibleAssetProvider.NFTSCAN: + case NonFungibleAssetProvider.ZORA: return NFTScan.getContractBalance(address) default: @@ -65,6 +68,8 @@ export async function getNFTContract(options: ContractOption) { return OpenSea.getContract(contractAddress, chainId) case NonFungibleAssetProvider.RARIBLE: return + case NonFungibleAssetProvider.ZORA: + return default: unreachable(provider) } @@ -79,6 +84,8 @@ export async function getNFTsByPagination(options: NFTsByPaginationOption) { return NFTScan.getTokens(from, options) case NonFungibleAssetProvider.RARIBLE: return Rarible.getTokens(from, options) + case NonFungibleAssetProvider.ZORA: + return Rarible.getTokens(from, options) default: unreachable(provider) } diff --git a/packages/mask/src/resources/ZoraIcon.tsx b/packages/mask/src/resources/ZoraIcon.tsx new file mode 100644 index 000000000000..2d0098a19a84 --- /dev/null +++ b/packages/mask/src/resources/ZoraIcon.tsx @@ -0,0 +1,63 @@ +import { SvgIconProps, SvgIcon } from '@mui/material' + +const svg = ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) + +export function ZoraIcon(props: SvgIconProps) { + return {svg} +} diff --git a/packages/web3-providers/package.json b/packages/web3-providers/package.json index a3dac9739e31..039f9fcdf057 100644 --- a/packages/web3-providers/package.json +++ b/packages/web3-providers/package.json @@ -11,6 +11,7 @@ "@masknet/web3-contracts": "workspace:*", "react-use": "^17.3.2", "wallet.ts": "^1.0.1", + "graphql-request": "^3.4.0", "bignumber.js": "9.0.2", "date-fns": "^2.28.0", "json-stable-stringify": "^1.0.1", diff --git a/packages/web3-providers/src/index.ts b/packages/web3-providers/src/index.ts index 5f6928a75970..113f736adb2e 100644 --- a/packages/web3-providers/src/index.ts +++ b/packages/web3-providers/src/index.ts @@ -2,6 +2,7 @@ import { CoinGeckoAPI } from './coingecko' import { OpenSeaAPI } from './opensea' import { RaribleAPI } from './rarible' import { NFTScanAPI } from './NFTScan' +import { ZoraAPI } from './zora' import { NativeExplorerAPI } from './explorer' import { RSS3API } from './rss3' import { KeyValueAPI } from './kv' @@ -17,6 +18,7 @@ export * from './NextID' export const OpenSea = new OpenSeaAPI() export const Rarible = new RaribleAPI() export const NFTScan = new NFTScanAPI() +export const Zora = new ZoraAPI() export const CoinGecko = new CoinGeckoAPI() export const Explorer = new NativeExplorerAPI() export const RSS3 = new RSS3API() diff --git a/packages/web3-providers/src/zora/constants.ts b/packages/web3-providers/src/zora/constants.ts new file mode 100644 index 000000000000..2dd58bc8a746 --- /dev/null +++ b/packages/web3-providers/src/zora/constants.ts @@ -0,0 +1 @@ +export const ZORA_MAINNET_GRAPHQL_URL = 'https://cors.r2d2.to/?https://indexer-prod-mainnet.zora.co/v1/graphql' diff --git a/packages/web3-providers/src/zora/index.ts b/packages/web3-providers/src/zora/index.ts new file mode 100644 index 000000000000..00f041ef36cd --- /dev/null +++ b/packages/web3-providers/src/zora/index.ts @@ -0,0 +1,360 @@ +import { GraphQLClient } from 'graphql-request' +import { first } from 'lodash-unified' +import { + formatWeiToEther, + ChainId, + FungibleTokenDetailed, + ERC721TokenDetailed, + EthereumTokenType, +} from '@masknet/web3-shared-evm' + +import { NonFungibleTokenAPI } from '..' +import type { ZoraToken, ZoraHistory, ZoraBid, ZoraAsk } from './types' +import { getAssetQuery, getTokenHistoryQuery, getBidsQuery, getAsksQuery } from './queries' +import { ZORA_MAINNET_GRAPHQL_URL } from './constants' + +function createNFTAsset(asset: ZoraToken): NonFungibleTokenAPI.Asset { + return { + is_verified: false, + is_auction: asset.currentAuction !== null, + image_url: + asset.metadata.json.animation_url ?? asset.metadata.json.image_url ?? asset.metadata.json.image ?? '', + asset_contract: { + name: asset.tokenContract.name, + description: '', + schemaName: '', + }, + current_price: asset.v3Ask ? formatWeiToEther(asset.v3Ask.askPrice).toNumber() : null, + current_symbol: asset.symbol ?? 'ETH', + owner: asset.owner + ? { + address: asset.owner, + profile_img_url: '', + user: { username: asset.owner }, + link: `https://zora.co/${asset?.owner}`, + } + : null, + creator: asset.metadata.json.created_by + ? { + address: asset.metadata.json.created_by, + profile_img_url: '', + user: { username: asset.metadata.json.created_by }, + link: `https://zora.co/${asset?.metadata.json.created_by}`, + } + : null, + token_id: asset.tokenId, + token_address: asset.address, + traits: asset.metadata.json.attributes, + safelist_request_status: '', + description: asset.metadata.json.description, + name: asset.name ?? asset.metadata.json.name, + collection_name: '', + animation_url: asset.metadata.json.animation_url, + end_time: asset.currentAuction ? new Date(asset.currentAuction.expiresAt) : null, + order_payment_tokens: [] as FungibleTokenDetailed[], + offer_payment_tokens: [] as FungibleTokenDetailed[], + slug: '', + top_ownerships: asset.owner + ? [ + { + owner: { + address: asset.owner, + profile_img_url: '', + user: { username: asset.owner }, + link: `https://zora.co/${asset.owner}`, + }, + }, + ] + : [], + response_: asset, + last_sale: null, + } +} + +function createERC721TokenFromAsset(tokenAddress: string, tokenId: string, asset?: ZoraToken): ERC721TokenDetailed { + return { + contractDetailed: { + type: EthereumTokenType.ERC721, + chainId: ChainId.Mainnet, + address: tokenAddress, + name: asset?.tokenContract.name ?? '', + symbol: '', + }, + info: { + name: asset?.metadata.json.name ?? '', + description: asset?.metadata.json.description ?? '', + mediaUrl: asset?.metadata.json.animation_url ?? asset?.metadata.json.image_url ?? '', + owner: asset?.owner, + }, + tokenId: tokenId, + } +} + +export class ZoraAPI { + private client + constructor() { + this.client = new GraphQLClient(ZORA_MAINNET_GRAPHQL_URL) + } + + async getAsset(address: string, tokenId: string) { + const variables = { + address, + tokenId, + } + const assetData = await this.client.request(getAssetQuery, variables) + if (!assetData) return + return createNFTAsset(assetData.Token[0]) + } + + async getToken(address: string, tokenId: string) { + const variables = { + address, + tokenId, + } + const asset = await this.client.request(getAssetQuery, variables) + if (!asset) return + return createERC721TokenFromAsset(address, tokenId, asset.Token[0]) + } + + async getHistory(address: string, tokenId: string): Promise { + const variables = { + address, + tokenId, + } + + const nftEventHistory = await this.client.request(getTokenHistoryQuery, variables) + + const history: NonFungibleTokenAPI.History[] = nftEventHistory.Token[0].transferEvents.map( + (event: ZoraHistory): NonFungibleTokenAPI.History | undefined => { + if (event.transaction.mediaMints?.length !== 0) { + const mint = first(event.transaction.mediaMints) + return { + id: mint?.id || '', + eventType: 'mint', + timestamp: new Date(`${event.blockTimestamp}Z`).getTime(), + price: { + quantity: '0', + price: '0', + paymentToken: { + name: 'Ether', + symbol: 'ETH', + decimals: 18, + address: '0x0000000000000000000000000000000000000000', + }, + }, + accountPair: { + from: { + username: '', + address: mint?.address, + imageUrl: '', + link: `https://zora.co/${mint?.address}`, + }, + to: { + username: '', + address: mint?.creator, + imageUrl: '', + link: `https://zora.co/${mint?.creator}`, + }, + }, + } + } + if (event.transaction.auctionCreatedEvents?.length !== 0) { + const list = first(event.transaction.auctionCreatedEvents) + return { + id: list?.id || '', + eventType: 'List', + timestamp: new Date(`${event.blockTimestamp}Z`).getTime(), + price: { + quantity: list?.reservePrice ? formatWeiToEther(list?.reservePrice).toFixed(4) : '', + price: '1', + paymentToken: { + name: 'Ether', + symbol: 'ETH', + decimals: 18, + address: list?.auctionCurrency || '', + }, + }, + accountPair: { + from: { + username: list?.tokenOwner, + address: list?.tokenOwner, + imageUrl: '', + link: `https://zora.co/${list?.tokenOwner}`, + }, + to: { + username: '', + address: '', + imageUrl: '', + link: '', + }, + }, + } + } + if (event.transaction.marketBidEvents?.length !== 0) { + const bid = first(event.transaction.marketBidEvents) + return { + id: bid?.id || '', + eventType: 'Bid', + timestamp: new Date(`${event.blockTimestamp}Z`).getTime(), + price: { + quantity: bid?.amount ? formatWeiToEther(bid?.amount).toFixed(4) : '', + price: '1', + paymentToken: { + name: 'Ether', + symbol: 'ETH', + decimals: 18, + address: bid?.currencyAddress || '', + }, + }, + accountPair: { + from: { + username: bid?.bidder, + address: bid?.bidder, + imageUrl: '', + link: `https://zora.co/${bid?.bidder}`, + }, + to: { + username: bid?.recipient, + address: bid?.recipient, + imageUrl: '', + link: `https://zora.co/${bid?.recipient}`, + }, + }, + } + } + if (event.transaction.auctionEndedEvents?.length !== 0) { + const buy = first(event.transaction.auctionEndedEvents) + return { + id: buy?.id || '', + eventType: 'Buy', + timestamp: new Date(`${event.blockTimestamp}Z`).getTime(), + price: { + quantity: buy?.auction.lastBidAmount + ? formatWeiToEther(buy?.auction.lastBidAmount).toFixed(4) + : '', + price: '1', + paymentToken: { + name: 'Ether', + symbol: 'ETH', + decimals: 18, + address: buy?.auction.auctionCurrency || '', + }, + }, + accountPair: { + from: { + username: buy?.tokenOwner, + address: buy?.tokenOwner, + imageUrl: '', + link: `https://zora.co/${buy?.tokenOwner}`, + }, + to: { + username: buy?.winner, + address: buy?.winner, + imageUrl: '', + link: `https://zora.co/${buy?.winner}`, + }, + }, + } + } + return + }, + ) + + return history.filter((event) => event !== undefined) + } + async getOffers(tokenAddress: string, tokenId: string): Promise { + const variables = { + tokenAddress, + tokenId, + } + const nftBids = await this.client.request(getBidsQuery, variables) + + const offers: NonFungibleTokenAPI.AssetOrder[] = nftBids.Token[0].transferEvents.map( + (event: ZoraBid): NonFungibleTokenAPI.AssetOrder | undefined => { + if (event.transaction.marketBidEvents?.length !== 0) { + const bid = first(event.transaction.marketBidEvents) + return { + created_time: new Date(`${bid?.blockTimestamp}Z`).getTime().toString(), + current_price: bid?.amount, + payment_token: bid?.currencyAddress || '', + payment_token_contract: { + name: 'Ether', + symbol: 'ETH', + decimals: 18, + address: bid?.currencyAddress || '', + }, + listing_time: 0, + side: NonFungibleTokenAPI.OrderSide.Buy, + quantity: '1', + expiration_time: 0, + order_hash: bid?.transactionHash || '', + approved_on_chain: false, + maker_account: { + user: { username: bid?.recipient || '' }, + address: bid?.recipient || '', + profile_img_url: '', + link: `https://zora.co/${bid?.recipient}`, + }, + } + } + return + }, + ) + + return offers.filter((offer) => offer !== undefined) + } + + async getListings(tokenAddress: string, tokenId: string): Promise { + const variables = { + tokenAddress, + tokenId, + } + + const nftAsks = await this.client.request(getAsksQuery, variables) + + const orders: NonFungibleTokenAPI.AssetOrder[] = nftAsks.Token[0].transferEvents.map( + (event: ZoraAsk): NonFungibleTokenAPI.AssetOrder | undefined => { + if (event.transaction.auctionCreatedEvents?.length !== 0) { + const ask = first(event.transaction.auctionCreatedEvents) + return { + created_time: new Date(`${ask?.blockTimestamp}Z`).getTime().toString(), + approved_on_chain: false, + current_price: formatWeiToEther(ask?.reservePrice || 0).toString(), + payment_token: ask?.auctionCurrency, + payment_token_contract: { + name: 'Ether', + symbol: 'ETH', + decimals: 18, + address: ask?.auctionCurrency || '', + }, + listing_time: 0, + side: NonFungibleTokenAPI.OrderSide.Sell, + quantity: '1', + expiration_time: 0, + order_hash: ask?.transactionHash || '', + maker_account: { + user: { username: ask?.tokenOwner || '' }, + address: ask?.tokenOwner || '', + profile_img_url: '', + link: `https://zora.co/${ask?.tokenOwner}`, + }, + } + } + return + }, + ) + + return orders.filter((order) => order !== undefined) + } + + async getOrders(tokenAddress: string, tokenId: string, side: NonFungibleTokenAPI.OrderSide) { + switch (side) { + case NonFungibleTokenAPI.OrderSide.Buy: + return this.getOffers(tokenAddress, tokenId) + case NonFungibleTokenAPI.OrderSide.Sell: + return this.getListings(tokenAddress, tokenId) + default: + return [] + } + } +} diff --git a/packages/web3-providers/src/zora/queries.ts b/packages/web3-providers/src/zora/queries.ts new file mode 100644 index 000000000000..be1d774c2824 --- /dev/null +++ b/packages/web3-providers/src/zora/queries.ts @@ -0,0 +1,106 @@ +import { gql } from 'graphql-request' + +export const getAssetQuery = gql` + query getAsset($address: String!, $tokenId: String!) { + Token(where: { address: { _eq: $address }, tokenId: { _eq: $tokenId } }) { + tokenId + name + address + owner + metadata { + json + } + v3Ask { + askPrice + } + currentAuction { + expiresAt + } + symbol + tokenContract { + name + } + } + } +` + +export const getTokenHistoryQuery = gql` + query getTokenHistory($address: String!, $tokenId: String!) { + Token(where: { address: { _eq: $address }, tokenId: { _eq: $tokenId } }) { + transferEvents { + blockTimestamp + transaction { + mediaMints { + id + blockTimestamp + creator + address + } + auctionCreatedEvents { + id + reservePrice + tokenOwner + blockTimestamp + auctionCurrency + } + marketBidEvents(where: { status: { _eq: "FINALIZED" } }) { + id + blockTimestamp + amount + currencyAddress + bidder + recipient + } + auctionEndedEvents { + id + transaction { + blockTimestamp + } + tokenOwner + winner + auction { + lastBidAmount + auctionCurrency + } + } + } + } + } + } +` + +export const getBidsQuery = gql` + query getBidEvents($tokenAddress: String!, $tokenId: String!) { + Token(where: { address: { _eq: $tokenAddress }, tokenId: { _eq: $tokenId } }) { + transferEvents { + transaction { + marketBidEvents(where: { status: { _eq: "FINALIZED" } }) { + blockTimestamp + amount + currencyAddress + transactionHash + recipient + } + } + } + } + } +` + +export const getAsksQuery = gql` + query getAskEvents($tokenAddress: String!, $tokenId: String!) { + Token(where: { address: { _eq: $tokenAddress }, tokenId: { _eq: $tokenId } }) { + transferEvents { + transaction { + auctionCreatedEvents { + auctionCurrency + reservePrice + tokenOwner + blockTimestamp + transactionHash + } + } + } + } + } +` diff --git a/packages/web3-providers/src/zora/types.ts b/packages/web3-providers/src/zora/types.ts new file mode 100644 index 000000000000..93571f67a937 --- /dev/null +++ b/packages/web3-providers/src/zora/types.ts @@ -0,0 +1,98 @@ +export interface ZoraHistory { + blockTimestamp: string + transaction: ZoraTransactionEvent +} + +export interface ZoraTransactionEvent { + mediaMints?: MediaMint[] + auctionCreatedEvents?: AuctionCreatedEvent[] + marketBidEvents?: MarketBidEvent[] + auctionEndedEvents?: AuctionEndedEvent[] +} + +export interface MediaMint { + id: string + blockTimestamp: string + creator: string + address: string +} + +export interface AuctionCreatedEvent { + id: string + blockTimestamp: string + reservePrice: string + auctionCurrency: string + tokenOwner: string + transactionHash: string +} + +export interface MarketBidEvent { + id: string + blockTimestamp: string + amount: string + currencyAddress: string + bidder: string + recipient: string + transactionHash: string +} + +export interface AuctionEndedEvent { + id: string + tokenOwner: string + winner: string + blockTimestamp: string + auction: AuctionInfo +} + +export interface AuctionInfo { + lastBidAmount: string + auctionCurrency: string + expiresAt: string +} + +export interface ZoraBid { + transaction: ZoraTransactionEvent +} + +export interface ZoraAsk { + transaction: ZoraTransactionEvent +} + +export interface ZoraToken { + tokenId: string + name: string + address: string + currentAuction: AuctionInfo + metadata: TokenMetadata + tokenContract: TokenContract + v3Ask: V3TokenAsk + symbol: string + owner: string +} + +export interface TokenMetadata { + json: MetadataInfo +} + +export interface MetadataInfo { + animation_url: string + image_url: string + created_by: string + attributes: TokenAttributes[] + name: string + description: string + image: string +} + +export interface TokenContract { + name: string +} + +export interface V3TokenAsk { + askPrice: string +} + +export interface TokenAttributes { + trait_type: string + value: string +} diff --git a/packages/web3-shared/evm/pipes/index.ts b/packages/web3-shared/evm/pipes/index.ts index 34a54a1a422b..818cfb540e1f 100644 --- a/packages/web3-shared/evm/pipes/index.ts +++ b/packages/web3-shared/evm/pipes/index.ts @@ -197,6 +197,8 @@ export function resolveCollectibleProviderLink(chainId: ChainId, provider: NonFu return 'https://rarible.com' case NonFungibleAssetProvider.NFTSCAN: return 'https://nftscan.com' + case NonFungibleAssetProvider.ZORA: + return 'https://zora.co' default: unreachable(provider) } @@ -212,6 +214,8 @@ export function resolveCollectibleAssetLink(chainId: ChainId, provider: NonFungi return '' case NonFungibleAssetProvider.NFTSCAN: return '' + case NonFungibleAssetProvider.ZORA: + return '' default: unreachable(provider) } @@ -232,6 +236,8 @@ export function resolveCollectibleLink( return '' case NonFungibleAssetProvider.NFTSCAN: return '' + case NonFungibleAssetProvider.ZORA: + return '' default: unreachable(provider) } diff --git a/packages/web3-shared/evm/types/index.ts b/packages/web3-shared/evm/types/index.ts index 7840867421cc..8855d3da1d73 100644 --- a/packages/web3-shared/evm/types/index.ts +++ b/packages/web3-shared/evm/types/index.ts @@ -583,6 +583,7 @@ export enum NonFungibleAssetProvider { OPENSEA = 'OpenSea', RARIBLE = 'Rarible', NFTSCAN = 'NFTScan', + ZORA = 'Zora', } export type UnboxTransactionObject = T extends NonPayableTransactionObject diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 857aed4cd2b6..a86db554380e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1149,6 +1149,7 @@ importers: '@masknet/web3-shared-evm': workspace:* bignumber.js: 9.0.2 date-fns: ^2.28.0 + graphql-request: ^3.4.0 json-stable-stringify: ^1.0.1 lodash-unified: 1.0.2 react-use: ^17.3.2 @@ -1165,6 +1166,7 @@ importers: '@masknet/web3-shared-evm': link:../web3-shared/evm bignumber.js: 9.0.2 date-fns: 2.28.0 + graphql-request: 3.5.0 json-stable-stringify: 1.0.1 lodash-unified: 1.0.2_da03a4540fbd16bbaafbb96724306afd react-use: 17.3.2_757a802188413a36d4f24237d13b8e90 @@ -16035,6 +16037,16 @@ packages: /graceful-fs/4.2.9: resolution: {integrity: sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==} + /graphql-request/3.5.0: + resolution: {integrity: sha512-Io89QpfU4rqiMbqM/KwMBzKaDLOppi8FU8sEccCE4JqCgz95W9Q8bvxQ4NfPALLSMvg9nafgg8AkYRmgKSlukA==} + peerDependencies: + graphql: 14.x || 15.x + dependencies: + cross-fetch: 3.1.4 + extract-files: 9.0.0 + form-data: 3.0.1 + dev: false + /graphql-request/3.5.0_graphql@15.5.3: resolution: {integrity: sha512-Io89QpfU4rqiMbqM/KwMBzKaDLOppi8FU8sEccCE4JqCgz95W9Q8bvxQ4NfPALLSMvg9nafgg8AkYRmgKSlukA==} peerDependencies: