feat: replace nftscan with simplehash
This commit is contained in:
parent
d976965844
commit
034723fe8b
|
|
@ -17,7 +17,7 @@ NEXT_PUBLIC_WALLET_CONNECT_V2_PROJECT_ID=
|
|||
|
||||
# NFT
|
||||
|
||||
NFTSCAN_API_KEY=
|
||||
SIMPLEHASH_API_KEY=
|
||||
|
||||
# OPENAI
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
"MINTABLE",
|
||||
"MORALIS",
|
||||
"nextjs",
|
||||
"nftscan",
|
||||
"simplehash",
|
||||
"nocheck",
|
||||
"nprogress",
|
||||
"onclickoutside",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ apiVersion: v1
|
|||
stringData:
|
||||
REDIS_URL: ${REDIS_URL_DEV}
|
||||
DATABASE_URL: ${PG_CONN_STRING_DEV}
|
||||
NFTSCAN_API_KEY: ${NFTSCAN_API_KEY}
|
||||
SIMPLEHASH_API_KEY: ${SIMPLEHASH_API_KEY}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
ANONYMOUS_ACCOUNT_PRIVATEKEY: ${ANONYMOUS_ACCOUNT_PRIVATEKEY}
|
||||
PORTFOLIO_GITHUB_TOKEN: ${PORTFOLIO_GITHUB_TOKEN}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ apiVersion: v1
|
|||
stringData:
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
DATABASE_URL: ${PG_CONN_STRING}
|
||||
NFTSCAN_API_KEY: ${NFTSCAN_API_KEY}
|
||||
SIMPLEHASH_API_KEY: ${SIMPLEHASH_API_KEY}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
ANONYMOUS_ACCOUNT_PRIVATEKEY: ${ANONYMOUS_ACCOUNT_PRIVATEKEY}
|
||||
kind: Secret
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ module.exports = withBundleAnalyzer({
|
|||
ENV_REDIS_URL: process.env.REDIS_URL,
|
||||
ENV_REDIS_EXPIRE: process.env.REDIS_EXPIRE,
|
||||
ENV_REDIS_REFRESH: process.env.REDIS_REFRESH,
|
||||
ENV_NFTSCAN_API_KEY: process.env.NFTSCAN_API_KEY,
|
||||
ENV_SIMPLEHASH_API_KEY: process.env.SIMPLEHASH_API_KEY,
|
||||
ENV_OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
ENV_ANONYMOUS_ACCOUNT_PRIVATEKEY: process.env.ANONYMOUS_ACCOUNT_PRIVATEKEY,
|
||||
ENV_PORTFOLIO_GITHUB_TOKEN: process.env.PORTFOLIO_GITHUB_TOKEN,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import { Metadata } from "next"
|
||||
import Script from "next/script"
|
||||
|
||||
import { Image } from "~/components/ui/Image"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
import { UniMedia } from "~/components/ui/UniMedia"
|
||||
import { getTranslation } from "~/lib/i18n"
|
||||
import { toGateway } from "~/lib/ipfs-parser"
|
||||
import getQueryClient from "~/lib/query-client"
|
||||
import { fetchGetSite, getNFTs } from "~/queries/site.server"
|
||||
|
||||
|
|
@ -38,98 +36,50 @@ export default async function SiteNFTPage({
|
|||
const site = await fetchGetSite(params.site, queryClient)
|
||||
const { t } = await getTranslation("common")
|
||||
|
||||
let nfts = (await getNFTs(site?.owner)) ?? []
|
||||
|
||||
const names = new Map<string, boolean>()
|
||||
nfts.forEach((chain: any) => {
|
||||
if (!names.get(chain.chain)) {
|
||||
names.set(chain.chain, true)
|
||||
chain.assets = []
|
||||
chain.collection_assets.forEach((collection: any) => {
|
||||
chain.assets = chain.assets.concat(collection.assets)
|
||||
})
|
||||
chain.assets = chain.assets.filter((asset: any) => asset.content_uri)
|
||||
}
|
||||
})
|
||||
nfts = nfts
|
||||
.filter((chain: any) => chain.assets?.length)
|
||||
.sort((a: any, b: any) => b.assets.length - a.assets.length)
|
||||
|
||||
const displayNames: Record<string, string> = {
|
||||
eth: "Ethereum",
|
||||
bsc: "BNB Chain",
|
||||
pls: "Polygon",
|
||||
arbi: "Arbitrum One",
|
||||
opti: "Optimism",
|
||||
avax: "Avalanche",
|
||||
cro: "Cronos",
|
||||
platon: "PlatON",
|
||||
glmr: "Moonbeam",
|
||||
ftm: "Fantom",
|
||||
gnosis: "Gnosis",
|
||||
let nfts = (await getNFTs(site?.owner)) ?? {
|
||||
nfts: [],
|
||||
chains: [],
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
type="module"
|
||||
src="https://cdn.jsdelivr.net/npm/@google/model-viewer/dist/model-viewer.min.js"
|
||||
></Script>
|
||||
<h2 className="page-title">NFT {t("Showcase")}</h2>
|
||||
<div className="mt-8 text-zinc-500 text-sm">
|
||||
<div className="mt-8 text-zinc-500 text-sm mb-8">
|
||||
<span className="font-medium">Supported chains:</span>{" "}
|
||||
{Object.values(displayNames).join(", ")}
|
||||
{nfts.chains.join(", ")}
|
||||
</div>
|
||||
<div>
|
||||
{nfts.map((chain: any) => (
|
||||
<div
|
||||
key={chain.chain}
|
||||
className="flex flex-col"
|
||||
data-network={chain.chain}
|
||||
>
|
||||
<details open>
|
||||
<summary>
|
||||
<span className="inline-block text-2xl font-bold my-8 pl-2">
|
||||
{displayNames[chain.chain] || chain.chain} (
|
||||
{chain.assets.length})
|
||||
</span>
|
||||
</summary>
|
||||
<div className="grid grid-cols-3 md:grid-cols-4 gap-10">
|
||||
{chain.assets.map((nft: any) => (
|
||||
<UniLink
|
||||
key={nft.mint_transaction_hash}
|
||||
className="xlog-nft flex items-center flex-col"
|
||||
href={nft.external_link}
|
||||
data-collection={nft.contract_address}
|
||||
data-token-id={nft.token_id}
|
||||
data-name={nft.name}
|
||||
>
|
||||
<UniMedia
|
||||
src={
|
||||
nft.content_uri?.startsWith?.("http") ||
|
||||
nft.content_uri?.startsWith?.("data:")
|
||||
? nft.content_uri
|
||||
: nft.content_uri?.startsWith?.("<svg")
|
||||
? `data:image/svg+xml,${encodeURIComponent(
|
||||
nft.content_uri,
|
||||
)}` || ""
|
||||
: toGateway(
|
||||
nft.content_uri?.startsWith?.("ipfs://")
|
||||
? nft.content_uri
|
||||
: `ipfs://${nft.content_uri}`,
|
||||
) || ""
|
||||
}
|
||||
mime_type={nft.content_type}
|
||||
/>
|
||||
<div className="text-xs mt-2 text-center font-medium">
|
||||
{nft.name}
|
||||
</div>
|
||||
</UniLink>
|
||||
))}
|
||||
<div className="grid grid-cols-4 md:grid-cols-5 gap-8">
|
||||
{nfts.nfts.map((nft) => (
|
||||
<UniLink
|
||||
key={nft.nft_id}
|
||||
className="xlog-nft flex items-center flex-col"
|
||||
href={
|
||||
nft.external_url ||
|
||||
nft.collection?.external_url ||
|
||||
nft.collection?.marketplace_pages?.[0]?.nft_url ||
|
||||
nft.collection?.marketplace_pages?.[0]?.collection_url
|
||||
}
|
||||
data-collection={nft.contract_address.toLowerCase()}
|
||||
data-token-id={nft.token_id}
|
||||
data-name={nft.name}
|
||||
>
|
||||
<div className="w-full aspect-[1] relative rounded overflow-hidden">
|
||||
<div className="absolute top-0 bottom-0 left-0 right-0">
|
||||
<Image
|
||||
className="object-cover no-optimization w-full h-full rounded-2xl"
|
||||
alt={"nft"}
|
||||
src={nft.previews.image_medium_url}
|
||||
fill={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
))}
|
||||
<div className="text-xs mt-2 text-center font-medium">
|
||||
{nft.name}
|
||||
</div>
|
||||
</UniLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import React, { useState } from "react"
|
||||
|
||||
import { Image } from "~/components/ui/Image"
|
||||
import { IPFS_GATEWAY, IPFS_GATEWAY_FALLBACK } from "~/lib/env"
|
||||
|
||||
export const UniMedia = ({
|
||||
src,
|
||||
mime_type,
|
||||
}: {
|
||||
src: string
|
||||
mime_type?: string
|
||||
}) => {
|
||||
const [errorHandled, setErrorHandled] = useState(false)
|
||||
|
||||
const [type, setType] = useState(mime_type)
|
||||
|
||||
const handleError = async (e: any) => {
|
||||
const src = e.target.getAttribute("original-src")
|
||||
if (src && !errorHandled) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
src.replace(IPFS_GATEWAY, IPFS_GATEWAY_FALLBACK),
|
||||
{
|
||||
method: "HEAD",
|
||||
},
|
||||
)
|
||||
setType(response.headers.get("content-type") ?? "")
|
||||
setErrorHandled(true)
|
||||
} catch (error) {}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full aspect-[1] relative rounded overflow-hidden">
|
||||
<div className="absolute top-0 bottom-0 left-0 right-0">
|
||||
{(() => {
|
||||
switch (type?.split("/")[0]) {
|
||||
case "video":
|
||||
return (
|
||||
<video
|
||||
className="w-full h-full"
|
||||
src={src}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
/>
|
||||
)
|
||||
case "model":
|
||||
return (
|
||||
// @ts-ignore
|
||||
<model-viewer
|
||||
class="w-full h-full"
|
||||
src={src}
|
||||
ar
|
||||
ar-modes="webxr scene-viewer quick-look"
|
||||
seamless-poster
|
||||
shadow-intensity="1"
|
||||
camera-controls
|
||||
enable-pan
|
||||
/>
|
||||
)
|
||||
case "text":
|
||||
return <iframe className="w-full h-full" src={src}></iframe>
|
||||
default:
|
||||
return (
|
||||
<Image
|
||||
className="object-cover"
|
||||
alt={"nft"}
|
||||
src={src}
|
||||
original-src={src}
|
||||
fill={true}
|
||||
onError={handleError}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -10,8 +10,8 @@ export const REDIS_REFRESH =
|
|||
parseInt(config.ENV_REDIS_REFRESH || process.env.REDIS_REFRESH || "0") ||
|
||||
5 * 1000 // 5 seconds
|
||||
|
||||
export const NFTSCAN_API_KEY =
|
||||
config.ENV_NFTSCAN_API_KEY || process.env.NFTSCAN_API_KEY
|
||||
export const SIMPLEHASH_API_KEY =
|
||||
config.ENV_SIMPLEHASH_API_KEY || process.env.SIMPLEHASH_API_KEY
|
||||
export const OPENAI_API_KEY =
|
||||
config.ENV_OPENAI_API_KEY || process.env.OPENAI_API_KEY
|
||||
export const ANONYMOUS_ACCOUNT_PRIVATEKEY =
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { cacheGet } from "~/lib/redis.server"
|
|||
import { ExpandedCharacter } from "~/lib/types"
|
||||
import * as siteModel from "~/models/site.model"
|
||||
|
||||
import { NFTSCAN_API_KEY } from "../lib/env.server"
|
||||
import { SIMPLEHASH_API_KEY } from "../lib/env.server"
|
||||
|
||||
export const prefetchGetSite = async (
|
||||
input: string,
|
||||
|
|
@ -110,7 +110,7 @@ export const fetchGetComments = async (
|
|||
}
|
||||
|
||||
export const getNFTs = async (address?: string) => {
|
||||
if (!NFTSCAN_API_KEY || !address) {
|
||||
if (!SIMPLEHASH_API_KEY || !address) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -119,19 +119,84 @@ export const getNFTs = async (address?: string) => {
|
|||
noUpdate: true,
|
||||
expireTime: 60 * 60 * 24,
|
||||
getValueFun: async () => {
|
||||
const chains = (
|
||||
(await (
|
||||
await fetch("https://api.simplehash.com/api/v0/chains", {
|
||||
headers: {
|
||||
"X-API-KEY": SIMPLEHASH_API_KEY,
|
||||
accept: "application/json",
|
||||
},
|
||||
})
|
||||
).json()) as {
|
||||
chain: string
|
||||
is_testnet: boolean
|
||||
}[]
|
||||
)
|
||||
.filter((item) => !item.is_testnet)
|
||||
.map((item) => item.chain)
|
||||
.join(",")
|
||||
const result = await (
|
||||
await fetch(
|
||||
`https://restapi.nftscan.com/api/v2/assets/chain/${address}?erc_type=&chain=eth;bnb;polygon;arbitrum;optimism;avax;cronos;platon;moonbeam;fantom;gnosis`,
|
||||
`https://api.simplehash.com/api/v0/nfts/owners_v2?${new URLSearchParams(
|
||||
{
|
||||
chains: chains,
|
||||
wallet_addresses: address,
|
||||
filters: "spam_score__lte=90",
|
||||
order_by: "floor_price__desc",
|
||||
},
|
||||
).toString()}`,
|
||||
{
|
||||
headers: {
|
||||
"X-API-KEY": NFTSCAN_API_KEY,
|
||||
"X-API-KEY": SIMPLEHASH_API_KEY,
|
||||
accept: "application/json",
|
||||
},
|
||||
},
|
||||
)
|
||||
).json()
|
||||
return result.data
|
||||
while (result.next && result.nfts.length < 1000) {
|
||||
const next = await (
|
||||
await fetch(result.next, {
|
||||
headers: {
|
||||
"X-API-KEY": SIMPLEHASH_API_KEY,
|
||||
accept: "application/json",
|
||||
},
|
||||
})
|
||||
).json()
|
||||
result.nfts = result.nfts.concat(next.nfts)
|
||||
result.next = next.next
|
||||
}
|
||||
return {
|
||||
nfts: result.nfts,
|
||||
chains: chains.split(","),
|
||||
}
|
||||
},
|
||||
})
|
||||
}) as Promise<{
|
||||
nfts: {
|
||||
nft_id: string
|
||||
chain: string
|
||||
contract_address: string
|
||||
token_id: string
|
||||
name: string
|
||||
description: string
|
||||
previews: {
|
||||
image_small_url: string
|
||||
image_medium_url: string
|
||||
image_large_url: string
|
||||
image_opengraph_url: string
|
||||
blurhash: string
|
||||
predominant_color: string
|
||||
}
|
||||
external_url?: string
|
||||
collection?: {
|
||||
external_url?: string
|
||||
marketplace_pages?: {
|
||||
nft_url?: string
|
||||
collection_url?: string
|
||||
}[]
|
||||
}
|
||||
}[]
|
||||
chains: string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export const getCharacterColors = async (character?: ExpandedCharacter) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue