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: