diff --git a/app/[contractAddress]/[tokenId]/[chainId]/Panel.tsx b/app/[contractAddress]/[tokenId]/[chainId]/Panel.tsx
index 4f01079..c1a231e 100644
--- a/app/[contractAddress]/[tokenId]/[chainId]/Panel.tsx
+++ b/app/[contractAddress]/[tokenId]/[chainId]/Panel.tsx
@@ -1,8 +1,15 @@
/* eslint-disable @next/next/no-img-element */
import clsx from "clsx";
import { useState } from "react";
-import { Check, Exclamation } from "@/components/icon";
-import { Tabs, TabPanel, MediaViewer, ExternalLink } from "@/components/ui";
+import { Check } from "@/components/icon";
+import {
+ Tabs,
+ TabPanel,
+ MediaViewer,
+ ExternalLink,
+ DropdownMenu,
+ Disclaimer,
+} from "@/components/ui";
import { TbaOwnedNft } from "@/lib/types";
import useSWR from "swr";
import { getAlchemy } from "@/lib/clients";
@@ -16,10 +23,55 @@ export const TABS = {
ASSETS: "Assets",
};
+interface CopyAddressProps {
+ account: string;
+ displayedAddress: string;
+}
+
+const CopyAddress = ({ account, displayedAddress }: CopyAddressProps) => {
+ const [copied, setCopied] = useState(false);
+
+ return (
+
{
+ const textarea = document.createElement("textarea");
+ textarea.textContent = account;
+ textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in MS Edge.
+ document.body.appendChild(textarea);
+ textarea.select();
+
+ try {
+ document.execCommand("copy"); // Security exception may be thrown by some browsers.
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1000);
+
+ return;
+ } catch (ex) {
+ console.warn("Copy to clipboard failed.", ex);
+ return false;
+ } finally {
+ document.body.removeChild(textarea);
+ }
+ }}
+ >
+ {copied ? (
+
+
+
+ ) : (
+ shortenAddress(displayedAddress)
+ )}
+
+ );
+};
+
interface Props {
className?: string;
approvalTokensCount?: number;
account?: string;
+ accounts?: string[];
+ handleAccountChange?: (account: string) => void;
tokens: TbaOwnedNft[];
title: string;
chainId: number;
@@ -29,6 +81,8 @@ export const Panel = ({
className,
approvalTokensCount,
account,
+ accounts,
+ handleAccountChange,
tokens,
title,
chainId,
@@ -58,53 +112,26 @@ export const Panel = ({
{title}
-
{account && displayedAddress && (
- {
- const textarea = document.createElement("textarea");
- textarea.textContent = account;
- textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in MS Edge.
- document.body.appendChild(textarea);
- textarea.select();
-
- try {
- document.execCommand("copy"); // Security exception may be thrown by some browsers.
- setCopied(true);
- setTimeout(() => setCopied(false), 1000);
-
- return;
- } catch (ex) {
- console.warn("Copy to clipboard failed.", ex);
- return false;
- } finally {
- document.body.removeChild(textarea);
- }
- }}
+
- {copied ? (
-
-
-
- ) : (
- shortenAddress(displayedAddress)
- )}
-
+
+
)}
{approvalTokensCount ? (
-
-
-
-
-
- {`There are existing approvals on (${approvalTokensCount}) tokens owned by this account. Check approval status on tokenbound.org before purchasing.`}
-
-
+
) : null}
+ {typeof account === "string" && accounts && account === accounts[1] && (
+
+ )}
void;
approvalTokensCount?: number;
account?: string;
+ accounts?: string[];
+ handleAccountChange: (arg0: string) => void;
tokens: TbaOwnedNft[];
title: string;
chainId: number;
@@ -48,6 +50,8 @@ export const TokenDetail = ({
handleOpenClose,
approvalTokensCount,
account,
+ accounts,
+ handleAccountChange,
tokens,
title,
chainId,
@@ -80,6 +84,8 @@ export const TokenDetail = ({
tokens={tokens}
title={title}
chainId={chainId}
+ accounts={accounts}
+ handleAccountChange={handleAccountChange}
/>
)}
diff --git a/app/[contractAddress]/[tokenId]/[chainId]/page.tsx b/app/[contractAddress]/[tokenId]/[chainId]/page.tsx
index 36cff6d..82f6b0e 100644
--- a/app/[contractAddress]/[tokenId]/[chainId]/page.tsx
+++ b/app/[contractAddress]/[tokenId]/[chainId]/page.tsx
@@ -4,13 +4,13 @@ import { useEffect, useState } from "react";
import useSWR from "swr";
import { isNil } from "lodash";
import { TokenboundClient } from "@tokenbound/sdk";
-import { getAccount, getAccountStatus, getLensNfts, getNfts } from "@/lib/utils";
+import { getAccountStatus } from "@/lib/utils";
import { TbLogo } from "@/components/icon";
-import { useGetApprovals, useNft } from "@/lib/hooks";
+import { useGetApprovals, useNft, useTBADetails } from "@/lib/hooks";
import { TbaOwnedNft } from "@/lib/types";
import { getAddress } from "viem";
import { TokenDetail } from "./TokenDetail";
-import { HAS_CUSTOM_IMPLEMENTATION, alchemyApiKey } from "@/lib/constants";
+import { HAS_CUSTOM_IMPLEMENTATION } from "@/lib/constants";
interface TokenParams {
params: {
@@ -26,12 +26,11 @@ interface TokenParams {
export default function Token({ params, searchParams }: TokenParams) {
const [imagesLoaded, setImagesLoaded] = useState(false);
- const [nfts, setNfts] = useState([]);
- const [lensNfts, setLensNfts] = useState([]);
const { tokenId, contractAddress, chainId } = params;
const { disableloading, logo } = searchParams;
const [showTokenDetail, setShowTokenDetail] = useState(false);
const chainIdNumber = parseInt(chainId);
+
const tokenboundClient = new TokenboundClient({ chainId: chainIdNumber });
const {
@@ -67,9 +66,11 @@ export default function Token({ params, searchParams }: TokenParams) {
}, [nftImages, nftMetadataLoading]);
// Fetch nft's TBA
- const { data: account } = useSWR(tokenId ? `/account/${tokenId}` : null, async () => {
- const result = await getAccount(Number(tokenId), contractAddress, chainIdNumber);
- return result.data;
+ const { account, nfts, handleAccountChange, tba, tbaV2 } = useTBADetails({
+ tokenboundClient,
+ tokenId,
+ tokenContract: contractAddress as `0x${string}`,
+ chainId: chainIdNumber,
});
// Get nft's TBA account bytecode to check if account is deployed or not
@@ -88,33 +89,12 @@ export default function Token({ params, searchParams }: TokenParams) {
return data ?? false;
});
- // fetch nfts inside TBA
- useEffect(() => {
- async function fetchNfts(account: string) {
- const [data, lensData] = await Promise.all([
- getNfts(chainIdNumber, account),
- getLensNfts(account),
- ]);
- if (data) {
- setNfts(data);
- }
- if (lensData) {
- setLensNfts(lensData);
- }
- }
-
- if (account) {
- fetchNfts(account);
- }
- }, [account, accountIsDeployed, chainIdNumber]);
-
const [tokens, setTokens] = useState([]);
- const allNfts = [...nfts, ...lensNfts];
- const { data: approvalData } = useGetApprovals(allNfts, account, chainIdNumber);
+ const { data: approvalData } = useGetApprovals(nfts, account, chainIdNumber);
useEffect(() => {
- if (nfts !== undefined && nfts.length) {
+ if (nfts !== undefined) {
nfts.map((token) => {
const foundApproval = approvalData?.find((item) => {
const contract = item.contract.address;
@@ -129,11 +109,8 @@ export default function Token({ params, searchParams }: TokenParams) {
token.hasApprovals = foundApproval?.hasApprovals || false;
});
setTokens(nfts);
- if (lensNfts) {
- setTokens([...nfts, ...lensNfts]);
- }
}
- }, [nfts, approvalData, lensNfts]);
+ }, [nfts, approvalData, account]);
const showLoading = disableloading !== "true" && nftMetadataLoading;
@@ -151,6 +128,8 @@ export default function Token({ params, searchParams }: TokenParams) {
title={nftMetadata.title}
chainId={chainIdNumber}
logo={logo}
+ accounts={[tba, tbaV2 as string]}
+ handleAccountChange={handleAccountChange}
/>
)}
diff --git a/components/ui/Disclaimer.tsx b/components/ui/Disclaimer.tsx
new file mode 100644
index 0000000..9b21c46
--- /dev/null
+++ b/components/ui/Disclaimer.tsx
@@ -0,0 +1,24 @@
+import clsx from "clsx";
+import { Exclamation } from "../icon";
+
+interface Props
+ extends React.DetailedHTMLProps
, HTMLDivElement> {
+ message: string;
+}
+
+export const Disclaimer = ({ message, className, ...rest }: Props) => {
+ return (
+
+ );
+};
diff --git a/components/ui/DropDownMenu.tsx b/components/ui/DropDownMenu.tsx
new file mode 100644
index 0000000..b7e56c7
--- /dev/null
+++ b/components/ui/DropDownMenu.tsx
@@ -0,0 +1,100 @@
+import { shortenAddress } from "@/lib/utils";
+import clsx from "clsx";
+import { useState } from "react";
+
+interface Props
+ extends React.DetailedHTMLProps, HTMLDivElement> {
+ options?: string[];
+ currentOption?: string;
+ setCurrentOption?: (arg0: string) => void;
+}
+
+export const DropdownMenu = ({
+ options,
+ currentOption,
+ setCurrentOption,
+ className,
+ children,
+ ...rest
+}: Props) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const handleOptionClick = (option: string) => {
+ if (setCurrentOption) setCurrentOption(option);
+ setIsOpen(false);
+ };
+
+ if (!options || options.length <= 1) {
+ return null;
+ }
+
+ return (
+
+
+ {children ||
{currentOption}
}
+ {options?.length > 1 && (
+
+
+
+ )}
+
+ {isOpen && (
+
+
+ {options?.map((option, index) => (
+
handleOptionClick(option)}
+ >
+
+ {shortenAddress(option)}
+
+ {index === 0 ? "V3" : "V2"}
+
+
+
+ ))}
+
+
+ )}
+
+ );
+};
diff --git a/components/ui/index.ts b/components/ui/index.ts
index 9acd71c..08c416c 100644
--- a/components/ui/index.ts
+++ b/components/ui/index.ts
@@ -3,3 +3,5 @@ export * from "./Tabs";
export * from "./TabPanel";
export * from "./MediaViewer";
export * from "./ExternalLink";
+export * from "./DropDownMenu";
+export * from "./Disclaimer";
diff --git a/lib/hooks/index.ts b/lib/hooks/index.ts
index 6bfa118..c103bf3 100644
--- a/lib/hooks/index.ts
+++ b/lib/hooks/index.ts
@@ -1,3 +1,4 @@
export * from "./useNft";
export * from "./useGetApprovals";
export * from "./useGetTokenBalances";
+export * from "./useTBADetails";
diff --git a/lib/hooks/useTBADetails.ts b/lib/hooks/useTBADetails.ts
new file mode 100644
index 0000000..d131e34
--- /dev/null
+++ b/lib/hooks/useTBADetails.ts
@@ -0,0 +1,90 @@
+import { getAccount, getLensNfts, getNfts } from "@/lib/utils";
+import { TokenboundClient } from "@tokenbound/sdk";
+import { useEffect, useState } from "react";
+import useSWR from "swr";
+import { TbaOwnedNft } from "../types";
+
+type TBADetailsParams = {
+ tokenboundClient: TokenboundClient;
+ tokenId: string;
+ tokenContract: `0x${string}`;
+ chainId: number;
+};
+
+export const useTBADetails = ({
+ tokenboundClient,
+ tokenId,
+ tokenContract,
+ chainId,
+}: TBADetailsParams) => {
+ const [account, setAccount] = useState("");
+ const [nfts, setNfts] = useState([]);
+
+ const handleAccountChange = (account: string) => {
+ setAccount(account);
+
+ if (account === tba) {
+ setNfts(tbaNFTs || []);
+ } else {
+ setNfts(tbaV2NFTs || []);
+ }
+ };
+
+ const { data: tbaV2 } = useSWR(`tbaV2-${tokenId}-${tokenContract}`, () =>
+ getAccount(Number(tokenId), tokenContract, chainId)
+ );
+
+ const tba = tokenboundClient.getAccount({ tokenId, tokenContract });
+
+ const { data: isTbaDeployed } = useSWR(`tba-${tokenId}-${tokenContract}`, () =>
+ tokenboundClient.checkAccountDeployment({ accountAddress: tba })
+ );
+
+ const {
+ data: tbaV2NFTs,
+ isLoading,
+ error,
+ } = useSWR(`tbaV2NFTs-${tbaV2?.data}}`, async () => {
+ if (tbaV2) {
+ console.log("Inside fetching the nfts for v2: ", tbaV2);
+ const [nfts, lensNFT] = await Promise.all([
+ getNfts(chainId, tbaV2.data as `0x${string}`),
+ getLensNfts(tbaV2.data as `0x${string}`),
+ ]);
+
+ return [...nfts, ...lensNFT];
+ }
+
+ return [];
+ });
+
+ const { data: tbaNFTs } = useSWR(`tbaNFTs-${tba}`, async () => {
+ const [nfts, lensNFT] = await Promise.all([getNfts(chainId, tba), getLensNfts(tba)]);
+
+ return [...nfts, ...lensNFT];
+ });
+
+ useEffect(() => {
+ // If there are nfts in v3 by default show those
+ if (tbaNFTs?.length) {
+ setAccount(tba);
+ setNfts(tbaNFTs);
+ // If there are no nfts in v3 but there are in v2 show those
+ } else if (tbaV2NFTs?.length && !tbaNFTs?.length) {
+ setAccount(tbaV2?.data as `0x${string}`);
+ setNfts(tbaV2NFTs);
+ // Default to v3
+ } else {
+ setAccount(tba);
+ setNfts(tbaNFTs || []);
+ }
+ }, [isTbaDeployed, tbaV2NFTs, tbaNFTs, tba, tbaV2?.data]);
+
+ return {
+ tba,
+ tbaV2: tbaV2?.data,
+ account,
+ nfts,
+ handleAccountChange,
+ };
+};
diff --git a/lib/utils/shortenAddress.ts b/lib/utils/shortenAddress.ts
index 2508b5b..b8c396f 100644
--- a/lib/utils/shortenAddress.ts
+++ b/lib/utils/shortenAddress.ts
@@ -1,4 +1,5 @@
-export function shortenAddress(address: string): string {
+export function shortenAddress(address?: string): string {
+ if (!address) return "";
const start = address.slice(0, 4);
const end = address.slice(-3);
return `${start}...${end}`;
diff --git a/package.json b/package.json
index c2d4a9e..48c6dbf 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
},
"dependencies": {
"@sentry/nextjs": "^7.55.2",
- "@tokenbound/sdk": "^0.3.5",
+ "@tokenbound/sdk": "^0.4.4",
"@types/node": "20.3.1",
"@types/react": "18.2.12",
"@types/react-dom": "18.2.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index db1d205..2abbc0c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -9,8 +9,8 @@ dependencies:
specifier: ^7.55.2
version: 7.55.2(encoding@0.1.13)(next@13.4.5)(react@18.2.0)
'@tokenbound/sdk':
- specifier: ^0.3.5
- version: 0.3.5(typescript@5.1.3)
+ specifier: ^0.4.4
+ version: 0.4.4(typescript@5.1.3)
'@types/node':
specifier: 20.3.1
version: 20.3.1
@@ -97,12 +97,12 @@ devDependencies:
packages:
- /@adraffy/ens-normalize@1.9.0:
- resolution: {integrity: sha512-iowxq3U30sghZotgl4s/oJRci6WPBfNO5YYgk2cIOMCHr3LeGPcsZjCEr+33Q4N+oV3OABDAtA+pyvWjbvBifQ==}
+ /@adraffy/ens-normalize@1.10.0:
+ resolution: {integrity: sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==}
dev: false
- /@adraffy/ens-normalize@1.9.4:
- resolution: {integrity: sha512-UK0bHA7hh9cR39V+4gl2/NnBBjoXIxkuWAPCaY4X7fbH4L/azIi7ilWOCjMUYfpJgraLUAqkRi2BqrjME8Rynw==}
+ /@adraffy/ens-normalize@1.9.0:
+ resolution: {integrity: sha512-iowxq3U30sghZotgl4s/oJRci6WPBfNO5YYgk2cIOMCHr3LeGPcsZjCEr+33Q4N+oV3OABDAtA+pyvWjbvBifQ==}
dev: false
/@alloc/quick-lru@5.2.0:
@@ -766,10 +766,6 @@ packages:
resolution: {integrity: sha512-RkmuBcqiNioeeBKbgzMlOdreUkJfYaSjwgx9XDgGGpjvWgyaxWvDmZVSN9CS6LjEASadhgPv2BcFp+SeouWXXA==}
dev: false
- /@scure/base@1.1.1:
- resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==}
- dev: false
-
/@scure/base@1.1.3:
resolution: {integrity: sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==}
dev: false
@@ -801,7 +797,7 @@ packages:
resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==}
dependencies:
'@noble/hashes': 1.3.2
- '@scure/base': 1.1.1
+ '@scure/base': 1.1.3
dev: false
/@sentry-internal/tracing@7.55.2:
@@ -961,10 +957,10 @@ packages:
tslib: 2.5.3
dev: false
- /@tokenbound/sdk@0.3.5(typescript@5.1.3):
- resolution: {integrity: sha512-THgeAkY+l/OpUdWsAh/HyP5x9X0pLIyLCSuNPMmmDRUwuSoBC3J65/bZWffX4CXXUXr92IzHrw0PM+Jy7nUBKQ==}
+ /@tokenbound/sdk@0.4.4(typescript@5.1.3):
+ resolution: {integrity: sha512-BUjEEfzpq/6j2EP/xuwOVbFUyisHUf15PoJj/0y4+QQKYci2c6uoY7Im0CnfIakh0iiGTtYgrnwWTnDMefuPyg==}
dependencies:
- viem: 1.10.7(typescript@5.1.3)
+ viem: 1.19.3(typescript@5.1.3)
transitivePeerDependencies:
- bufferutil
- typescript
@@ -1010,12 +1006,6 @@ packages:
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
dev: false
- /@types/ws@8.5.5:
- resolution: {integrity: sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==}
- dependencies:
- '@types/node': 20.3.1
- dev: false
-
/@typescript-eslint/parser@5.59.11(eslint@8.42.0)(typescript@5.1.3):
resolution: {integrity: sha512-s9ZF3M+Nym6CAZEkJJeO2TFHHDsKAM3ecNkLuH4i4s8/RCPnF5JRip2GyviYkeEAcwGMJxkqG9h2dAsnA1nZpA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -2811,8 +2801,8 @@ packages:
ws: 8.12.0
dev: false
- /isomorphic-ws@5.0.0(ws@8.13.0):
- resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==}
+ /isows@1.0.3(ws@8.13.0):
+ resolution: {integrity: sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg==}
peerDependencies:
ws: '*'
dependencies:
@@ -4141,22 +4131,21 @@ packages:
- zod
dev: false
- /viem@1.10.7(typescript@5.1.3):
- resolution: {integrity: sha512-yuaYSHgV1g794nfxhn+V89qgK5ziFTLBSNqSDt4KW8YpjLu0Ah6LLZTtpOj3+MRWKKDwJ1YL2rENb8cuXstUzg==}
+ /viem@1.19.3(typescript@5.1.3):
+ resolution: {integrity: sha512-SymIbCO0nIq2ucna8R3XV4f/lEshKGLuhYU2f6O+OAbmE2ugBxBKx2axMKQrQML87nE0jb2qqqtRJka5SO/7MA==}
peerDependencies:
typescript: '>=5.0.4'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
- '@adraffy/ens-normalize': 1.9.4
+ '@adraffy/ens-normalize': 1.10.0
'@noble/curves': 1.2.0
'@noble/hashes': 1.3.2
'@scure/bip32': 1.3.2
'@scure/bip39': 1.2.1
- '@types/ws': 8.5.5
abitype: 0.9.8(typescript@5.1.3)
- isomorphic-ws: 5.0.0(ws@8.13.0)
+ isows: 1.0.3(ws@8.13.0)
typescript: 5.1.3
ws: 8.13.0
transitivePeerDependencies:
diff --git a/tailwind.config.js b/tailwind.config.js
index 425a21e..b4dcfc2 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -17,6 +17,8 @@ module.exports = {
"tb-warning-primary": "#FF8A00",
"tb-warning-secondary": "rgba(255, 138, 0, 0.1)",
"tb-text-gray": "#666D74",
+ purple: "#644CF7",
+ "purple-fade": "rgba(100, 76, 247, 0.10)",
},
transitionProperty: {
width: "width",