From 3656a867e69cd9d685e752dc1b363b75f9b2acbc Mon Sep 17 00:00:00 2001 From: "Yohan @ ScreenshotLabs" Date: Fri, 5 Apr 2024 15:18:37 +0200 Subject: [PATCH] fix: images not showing properly (#179) * image fallback * fixed lounge * refactor: extracted starknet nft api calls to its own helper * refactor: endpoints * fixed ark project logo link not working when both wallets connected --- apps/web/next.config.mjs | 10 +- .../bridge/_components/TransferNftsList.tsx | 2 +- .../_hooks/useEthereumCollectionApproval.tsx | 23 +-- .../lounge/_components/NftTransferItem.tsx | 10 +- apps/web/src/app/(routes)/page.tsx | 15 -- .../portfolio/_components/NftTabsList.tsx | 2 +- apps/web/src/app/(routes)/providers.tsx | 25 +-- .../app/_components/ConnectWalletsButton.tsx | 2 +- apps/web/src/app/_components/Media.tsx | 5 +- .../WalletModals/WalletModalsContext.tsx | 7 +- apps/web/src/server/api/helpers/l1nfts.ts | 16 ++ apps/web/src/server/api/helpers/l2nfts.ts | 124 ++++++++++++++ .../src/server/api/routers/bridgeRequest.ts | 8 +- apps/web/src/server/api/routers/l1Nfts.ts | 32 ++-- apps/web/src/server/api/routers/l2Nfts.ts | 158 ++++-------------- turbo.json | 3 +- 16 files changed, 238 insertions(+), 204 deletions(-) create mode 100644 apps/web/src/server/api/helpers/l1nfts.ts create mode 100644 apps/web/src/server/api/helpers/l2nfts.ts diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 5480200b..11250c1a 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -20,7 +20,15 @@ const config = { defaultLocale: "en", }, images: { - remotePatterns: [{ protocol: "https", hostname: "**" }], + // remotePatterns: [{ protocol: "https", hostname: "**" }], + remotePatterns: [ + { + protocol: "https", + hostname: "**", + port: "", + pathname: "**", + }, + ], }, webpack: (config) => { config.externals.push("pino-pretty", "lokijs", "encoding"); diff --git a/apps/web/src/app/(routes)/bridge/_components/TransferNftsList.tsx b/apps/web/src/app/(routes)/bridge/_components/TransferNftsList.tsx index aee6eafc..4cae7d6b 100644 --- a/apps/web/src/app/(routes)/bridge/_components/TransferNftsList.tsx +++ b/apps/web/src/app/(routes)/bridge/_components/TransferNftsList.tsx @@ -57,7 +57,7 @@ export default function TransferNftsList() { media={selectedNft.media} width={52} /> -
+
{selectedNft.collectionName} diff --git a/apps/web/src/app/(routes)/bridge/_hooks/useEthereumCollectionApproval.tsx b/apps/web/src/app/(routes)/bridge/_hooks/useEthereumCollectionApproval.tsx index 478c0248..5f4b5b22 100644 --- a/apps/web/src/app/(routes)/bridge/_hooks/useEthereumCollectionApproval.tsx +++ b/apps/web/src/app/(routes)/bridge/_hooks/useEthereumCollectionApproval.tsx @@ -32,28 +32,17 @@ export default function useEthereumCollectionApproval() { const { data: approveHash, - error, - failureReason, isLoading: isSigning, writeContract: writeContractApprove, } = useWriteContract(); - console.error("error: ", error); - console.error("failureReason: ", failureReason); function approveForAll() { - try { - writeContractApprove({ - abi: erc721Abi, - address: selectedCollectionAddress as `0x${string}`, - args: [ - process.env.NEXT_PUBLIC_L1_BRIDGE_ADDRESS as `0x${string}`, - true, - ], - functionName: "setApprovalForAll", - }); - } catch (e) { - console.error(e); - } + writeContractApprove({ + abi: erc721Abi, + address: selectedCollectionAddress as `0x${string}`, + args: [process.env.NEXT_PUBLIC_L1_BRIDGE_ADDRESS as `0x${string}`, true], + functionName: "setApprovalForAll", + }); } const { isLoading: isApproveLoading } = useWaitForTransactionReceipt({ diff --git a/apps/web/src/app/(routes)/lounge/_components/NftTransferItem.tsx b/apps/web/src/app/(routes)/lounge/_components/NftTransferItem.tsx index 3e0ca45a..8f1800dc 100644 --- a/apps/web/src/app/(routes)/lounge/_components/NftTransferItem.tsx +++ b/apps/web/src/app/(routes)/lounge/_components/NftTransferItem.tsx @@ -41,9 +41,13 @@ function getDisplayedDate(timestamp?: number) { const date = new Date(timestamp * 1000); - return `${ - date.getMonth() + 1 - }/${date.getDate()}/${date.getFullYear()} - ${date.getHours()}:${date.getMinutes()}`; + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + const year = date.getFullYear(); + const hours = date.getHours().toString().padStart(2, "0"); + const minutes = date.getMinutes().toString().padStart(2, "0"); + + return `${month}/${day}/${year} - ${hours}:${minutes}`; } export default function NftTransferItem({ diff --git a/apps/web/src/app/(routes)/page.tsx b/apps/web/src/app/(routes)/page.tsx index f762a3f8..1ea9b7d0 100644 --- a/apps/web/src/app/(routes)/page.tsx +++ b/apps/web/src/app/(routes)/page.tsx @@ -1,27 +1,12 @@ "use client"; -import { useAccount as useStarknetAccount } from "@starknet-react/core"; import { Typography } from "design-system"; import Image from "next/image"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; -import { useAccount as useEthereumAccount } from "wagmi"; import ConnectWalletsButton from "../_components/ConnectWalletsButton"; import Footer from "../_components/Footer"; export default function Page() { - const { address: starknetAddress } = useStarknetAccount(); - const { address: ethereumAddress } = useEthereumAccount(); - - const router = useRouter(); - - useEffect(() => { - if (starknetAddress !== undefined && ethereumAddress !== undefined) { - void router.push("/bridge"); - } - }, [starknetAddress, ethereumAddress, router]); - return ( <>
diff --git a/apps/web/src/app/(routes)/portfolio/_components/NftTabsList.tsx b/apps/web/src/app/(routes)/portfolio/_components/NftTabsList.tsx index 631be9e0..4baa77b1 100644 --- a/apps/web/src/app/(routes)/portfolio/_components/NftTabsList.tsx +++ b/apps/web/src/app/(routes)/portfolio/_components/NftTabsList.tsx @@ -25,7 +25,7 @@ export default function NftTabsList() { } = useInfiniteStarknetCollections(); return ( - + diff --git a/apps/web/src/app/_components/ConnectWalletsButton.tsx b/apps/web/src/app/_components/ConnectWalletsButton.tsx index 3ec334b4..fc8ab091 100644 --- a/apps/web/src/app/_components/ConnectWalletsButton.tsx +++ b/apps/web/src/app/_components/ConnectWalletsButton.tsx @@ -28,7 +28,7 @@ export default function ConnectWalletsButton() { toggleConnectEthereumWalletModal(); return; } - toggleConnectWalletsModal; + toggleConnectWalletsModal(); } return ( diff --git a/apps/web/src/app/_components/Media.tsx b/apps/web/src/app/_components/Media.tsx index c063d817..ccdc7339 100644 --- a/apps/web/src/app/_components/Media.tsx +++ b/apps/web/src/app/_components/Media.tsx @@ -1,5 +1,6 @@ import Image from "next/image"; import { useTheme } from "next-themes"; +import { useState } from "react"; import { type NftMedia } from "~/server/api/types"; /* eslint-disable @next/next/no-img-element */ @@ -20,8 +21,9 @@ export default function Media({ width, }: MediaProps) { const { resolvedTheme } = useTheme(); + const [hasFailedToLoad, setHasFailedToLoad] = useState(false); - if (media.src === undefined || media.src.length === 0) { + if (media.src === undefined || media.src.length === 0 || hasFailedToLoad) { return ( {alt} setHasFailedToLoad(true)} src={media.src} width={width} /> diff --git a/apps/web/src/app/_components/WalletModals/WalletModalsContext.tsx b/apps/web/src/app/_components/WalletModals/WalletModalsContext.tsx index 17540064..b330aa93 100644 --- a/apps/web/src/app/_components/WalletModals/WalletModalsContext.tsx +++ b/apps/web/src/app/_components/WalletModals/WalletModalsContext.tsx @@ -3,7 +3,7 @@ import { useNetwork as useStarknetNetwork, } from "@starknet-react/core"; import { SideDialog } from "design-system"; -import { usePathname } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { type PropsWithChildren, createContext, @@ -38,6 +38,7 @@ export function WalletModalsProvider({ children }: PropsWithChildren) { >(null); const pathname = usePathname(); + const router = useRouter(); const toggleConnectEthereumWalletModal = useCallback(() => { if (userOpenedModal === "ethereumWallet") { @@ -88,6 +89,7 @@ export function WalletModalsProvider({ children }: PropsWithChildren) { ethereumAddress !== undefined ) { setUserOpenedModal(null); + void router.push("/bridge"); return; } }, [starknetAddress, ethereumAddress, userOpenedModal]); @@ -111,8 +113,7 @@ export function WalletModalsProvider({ children }: PropsWithChildren) { return; } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pathname]); + }, [pathname, starknetAddress, ethereumAddress]); return ( ; + total_count: number; +}; +export async function getL2ContractsForOwner(address: string) { + const url = `${nftApiUrl}/v1/owners/${validateAndParseAddress( + address + )}/contracts`; + + const contractsResponse = await fetch(url, { + headers: requestsHeader, + }); + const contracts = + (await contractsResponse.json()) as ArkCollectionsApiResponse; + + return contracts; +} + +type ArkBatchNftsApiResponse = { + result: Array<{ + contract_address: string; + contract_name: string; + metadata?: { normalized: { image?: string; name?: string } }; + owner: string; + token_id: string; + }>; +}; +export async function getL2NftsMetadataBatch( + tokens: Array<{ contract_address: string; token_id: string }> +) { + const url = `${nftApiUrl}/v1/tokens/batch`; + + const nftsResponse = await fetch(url, { + body: JSON.stringify({ + tokens: tokens.map((token) => ({ + contract_address: validateAndParseAddress(token.contract_address), + token_id: token.token_id, + })), + }), + headers: requestsHeader, + method: "POST", + }); + + const nfts = (await nftsResponse.json()) as ArkBatchNftsApiResponse; + + return nfts; +} + +type ArkNftsApiResponse = { + result: Array<{ + contract_address: string; + metadata: { + normalized: { image: null | string; name: null | string }; + } | null; + owner: string; + token_id: string; + }>; + total_count: number; +}; +export async function getL2NftsForOwner( + userAddress: string, + contractAddress: string | undefined +) { + const url = `${nftApiUrl}/v1/owners/${validateAndParseAddress( + userAddress + )}/tokens${ + contractAddress !== undefined + ? `?contract_address=${validateAndParseAddress(contractAddress)}` + : "" + }`; + + const nftsResponse = await fetch(url, { + headers: requestsHeader, + }); + + const nfts = (await nftsResponse.json()) as ArkNftsApiResponse; + + return nfts; +} + +type ArkCollectionInfoApiResponse = { + result: { contract_address: string; name: string; symbol: string }; +}; +export async function getL2ContractMetadata(contractAddress: string) { + const url = `${nftApiUrl}/v1/contracts/${validateAndParseAddress( + contractAddress + )}`; + + const contractInfoResponse = await fetch(url, { + headers: requestsHeader, + }); + + const contractInfo = + (await contractInfoResponse.json()) as ArkCollectionInfoApiResponse; + + return contractInfo; +} + +export function getMediaObjectFromUrl(image: string | undefined): NftMedia { + if (image === undefined) { + return { format: "image", src: undefined }; + } + const mediaSrc = image.replace("ipfs://", process.env.IPFS_GATEWAY ?? ""); + const mediaFormat = mediaSrc?.split(".").pop() === "mp4" ? "video" : "image"; + + return { format: mediaFormat, src: mediaSrc }; +} diff --git a/apps/web/src/server/api/routers/bridgeRequest.ts b/apps/web/src/server/api/routers/bridgeRequest.ts index 89f25818..727ed8ef 100644 --- a/apps/web/src/server/api/routers/bridgeRequest.ts +++ b/apps/web/src/server/api/routers/bridgeRequest.ts @@ -8,8 +8,8 @@ import { type NftMedia } from "../types"; const alchemy = new Alchemy({ apiKey: process.env.ALCHEMY_API_KEY, - // network: Network.ETH_MAINNET, - network: Network.ETH_GOERLI, + network: Network.ETH_MAINNET, + // network: Network.ETH_GOERLI, }); export type BridgeRequestEventStatus = @@ -104,9 +104,6 @@ export const bridgeRequestRouter = createTRPCRouter({ tokenId: bridgeRequest.token_ids[0], })) ); - } else { - try { - } catch (error) {} } const bridgeRequestsWithMetadata = bridgeRequests.map( @@ -136,7 +133,6 @@ export const bridgeRequestRouter = createTRPCRouter({ requestContent: JSON.parse( bridgeRequest.req.content ) as Array, - // requestHash: bridgeRequest.req.hash, status: lastBridgeRequestEvent?.label ?? "error", statusTimestamp: lastBridgeRequestEvent?.block_timestamp ?? 0, tokenIds: bridgeRequest.token_ids, diff --git a/apps/web/src/server/api/routers/l1Nfts.ts b/apps/web/src/server/api/routers/l1Nfts.ts index 51aebd25..6e4b8d67 100644 --- a/apps/web/src/server/api/routers/l1Nfts.ts +++ b/apps/web/src/server/api/routers/l1Nfts.ts @@ -6,6 +6,7 @@ import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; import l1_bridge_contract from "../../../app/_abis/bridge_l1_abi.json"; +import { getMediaObjectFromAlchemyMedia } from "../helpers/l1nfts"; import { type Collection, type Nft } from "../types"; const viemClient = createClient({ @@ -59,9 +60,7 @@ export const l1NftsRouter = createTRPCRouter({ | undefined; const collections: Array = contracts.map((contract) => { - const media = contract.media[0]; - const mediaSrc = media?.gateway ?? media?.thumbnail ?? media?.raw; - const mediaFormat = media?.format === "mp4" ? "video" : "image"; + const media = getMediaObjectFromAlchemyMedia(contract.media[0]); const isBridgeable = whitelistedCollections !== undefined && whitelistedCollections.find( @@ -73,7 +72,7 @@ export const l1NftsRouter = createTRPCRouter({ return { contractAddress: contract.address, isBridgeable, - media: { format: mediaFormat, src: mediaSrc }, + media, name: contract.name ?? contract.symbol ?? "Unknown", totalBalance: contract.totalBalance, }; @@ -100,15 +99,14 @@ export const l1NftsRouter = createTRPCRouter({ ); return response.map((nft) => { - const media = nft.media[0]; - const mediaSrc = media?.gateway ?? media?.thumbnail ?? media?.raw; - const mediaFormat = media?.format === "mp4" ? "video" : "image"; + const media = getMediaObjectFromAlchemyMedia(nft.media[0]); + const tokenName = nft.title.length > 0 ? nft.title : `#${nft.tokenId}`; return { collectionName: nft.contract.name, - media: { format: mediaFormat, src: mediaSrc }, + media, tokenId: nft.tokenId, - tokenName: nft.title.length > 0 ? nft.title : `#${nft.tokenId}`, + tokenName, }; }); }), @@ -139,19 +137,17 @@ export const l1NftsRouter = createTRPCRouter({ pageSize, }); - // TODO @YohanTz: Handle videos const ownedNfts: Array = nfts.map((nft) => { - const media = nft.media[0]; - const mediaSrc = media?.gateway ?? media?.thumbnail ?? media?.raw; - const mediaFormat = media?.format === "mp4" ? "video" : "image"; + const media = getMediaObjectFromAlchemyMedia(nft.media[0]); + const name = + nft.title.length > 0 + ? nft.title + : `${nft.title ?? nft.contract.name} #${nft.tokenId}`; return { contractAddress: nft.contract.address, - media: { format: mediaFormat, src: mediaSrc }, - name: - nft.title.length > 0 - ? nft.title - : `${nft.title ?? nft.contract.name} #${nft.tokenId}`, + media, + name, tokenId: nft.tokenId, }; }); diff --git a/apps/web/src/server/api/routers/l2Nfts.ts b/apps/web/src/server/api/routers/l2Nfts.ts index 3449925b..ec1ffc27 100644 --- a/apps/web/src/server/api/routers/l2Nfts.ts +++ b/apps/web/src/server/api/routers/l2Nfts.ts @@ -3,45 +3,15 @@ import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; +import { + getL2ContractMetadata, + getL2ContractsForOwner, + getL2NftsForOwner, + getL2NftsMetadataBatch, + getMediaObjectFromUrl, +} from "../helpers/l2nfts"; import { type Collection, type Nft } from "../types"; -type ArkNftsApiResponse = { - result: Array<{ - contract_address: string; - metadata: { - normalized: { image: null | string; name: null | string }; - } | null; - owner: string; - token_id: string; - }>; - total_count: number; -}; - -type ArkCollectionsApiResponse = { - result: Array<{ - contract_address: string; - contract_type: string; - image?: string; - name: string; - symbol: string; - tokens_count: number; - }>; - total_count: number; -}; - -type ArkBatchNftsApiResponse = { - result: Array<{ - contract_address: string; - metadata?: { normalized: { image?: string; name?: string } }; - owner: string; - token_id: string; - }>; -}; - -type ArkCollectionInfoApiResponse = { - result: { contract_address: string; name: string; symbol: string }; -}; - export const l2NftsRouter = createTRPCRouter({ getCollectionInfo: publicProcedure .input(z.object({ contractAddress: z.string() })) @@ -49,19 +19,7 @@ export const l2NftsRouter = createTRPCRouter({ const { contractAddress } = input; try { - const url = `${ - process.env.NEXT_PUBLIC_ARK_API_DOMAIN ?? "" - }/v1/contracts/${validateAndParseAddress(contractAddress)}`; - - const contractInfoResponse = await fetch(url, { - headers: { - "Content-Type": "application/json", - "X-API-KEY": process.env.ARK_API_KEY ?? "", - }, - }); - - const contractInfo = - (await contractInfoResponse.json()) as ArkCollectionInfoApiResponse; + const contractInfo = await getL2ContractMetadata(contractAddress); return { name: contractInfo.result.name }; } catch (error) { @@ -77,26 +35,11 @@ export const l2NftsRouter = createTRPCRouter({ } = input; try { - const url = `${ - process.env.NEXT_PUBLIC_ARK_API_DOMAIN ?? "" - }/v1/owners/${validateAndParseAddress(address)}/contracts`; - - const contractsResponse = await fetch(url, { - headers: { - "Content-Type": "application/json", - "X-API-KEY": process.env.ARK_API_KEY ?? "", - }, - }); - const contracts = - (await contractsResponse.json()) as ArkCollectionsApiResponse; - const collections: Array = contracts.result.map( + const contractsForOwner = await getL2ContractsForOwner(address); + + const collections: Array = contractsForOwner.result.map( (contract) => { - const mediaSrc = contract.image?.replace( - "ipfs://", - process.env.IPFS_GATEWAY ?? "" - ); - const mediaFormat = - mediaSrc?.split(".").pop() === "mp4" ? "video" : "image"; + const media = getMediaObjectFromUrl(contract.image); return { contractAddress: contract.contract_address, @@ -104,14 +47,14 @@ export const l2NftsRouter = createTRPCRouter({ // isBridgeable: // contract.contract_address === - media: { format: mediaFormat, src: mediaSrc }, - name: contract.name ?? contract.symbol, + media, + name: contract.name ?? contract.symbol ?? "Unknown", totalBalance: contract.tokens_count, }; } ); - return { collections, totalCount: contracts.total_count }; + return { collections, totalCount: contractsForOwner.total_count }; } catch (error) { console.error("getL2NftCollectionsByWallet error: ", error); return { collections: [], totalCount: 0 }; @@ -133,25 +76,12 @@ export const l2NftsRouter = createTRPCRouter({ } try { - const url = `${ - process.env.NEXT_PUBLIC_ARK_API_DOMAIN ?? "" - }/v1/tokens/batch`; - - const nftsResponse = await fetch(url, { - body: JSON.stringify({ - tokens: tokenIds.map((tokenId) => ({ - contract_address: validateAndParseAddress(contractAddress), - token_id: tokenId, - })), - }), - headers: { - "Content-Type": "application/json", - "X-API-KEY": process.env.ARK_API_KEY ?? "", - }, - method: "POST", - }); - - const nfts = (await nftsResponse.json()) as ArkBatchNftsApiResponse; + const nfts = await getL2NftsMetadataBatch( + tokenIds.map((tokenId) => ({ + contract_address: contractAddress, + token_id: tokenId, + })) + ); return nfts.result .filter( @@ -161,16 +91,11 @@ export const l2NftsRouter = createTRPCRouter({ validateAndParseAddress(ownerAddress) ) .map((nft) => { - const mediaSrc = nft.metadata?.normalized.image?.replace( - "ipfs://", - process.env.IPFS_GATEWAY ?? "" - ); - const mediaFormat = - mediaSrc?.split(".").pop() === "mp4" ? "video" : "image"; + const media = getMediaObjectFromUrl(nft.metadata?.normalized.image); return { - collectionName: "EveraiDuo", - media: { format: mediaFormat, src: mediaSrc }, + collectionName: nft.contract_name, + media, tokenId: nft.token_id, tokenName: nft.metadata?.normalized.name ?? `#${nft.token_id}`, }; @@ -201,38 +126,21 @@ export const l2NftsRouter = createTRPCRouter({ } = input; try { - const url = `${ - process.env.NEXT_PUBLIC_ARK_API_DOMAIN ?? "" - }/v1/owners/${validateAndParseAddress(userAddress)}/tokens${ - contractAddress !== undefined - ? `?contract_address=${validateAndParseAddress(contractAddress)}` - : "" - }`; - - const nftsResponse = await fetch(url, { - headers: { - "Content-Type": "application/json", - "X-API-KEY": process.env.ARK_API_KEY ?? "", - }, - }); - - const nfts = (await nftsResponse.json()) as ArkNftsApiResponse; + const nfts = await getL2NftsForOwner(userAddress, contractAddress); const ownedNfts: Array = nfts.result.map((nft) => { - const mediaSrc = nft.metadata?.normalized.image?.replace( - "ipfs://", - process.env.IPFS_GATEWAY ?? "" + const media = getMediaObjectFromUrl( + nft.metadata?.normalized.image ?? undefined ); - const mediaFormat = - mediaSrc?.split(".").pop() === "mp4" ? "video" : "image"; + const name = + nft.metadata?.normalized.name?.length ?? 0 > 0 + ? nft.metadata?.normalized?.name ?? "" + : `${nft.token_id}`; return { contractAddress: nft.contract_address, - media: { format: mediaFormat, src: mediaSrc }, - name: - nft.metadata?.normalized.name?.length ?? 0 > 0 - ? nft.metadata?.normalized?.name ?? "" - : `${nft.token_id}`, + media, + name, tokenId: nft.token_id, }; }); diff --git a/turbo.json b/turbo.json index 4db019c2..1aa5d542 100644 --- a/turbo.json +++ b/turbo.json @@ -29,7 +29,8 @@ "VERCEL_URL", "NODE_ENV", "PORT", - "IPFS_GATEWAY" + "IPFS_GATEWAY", + "NEXT_PUBLIC_ALCHEMY_STARKNET_RPC_ENDPOINT" ], "pipeline": { "build:l1": {