Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5158def993 | ||
|
|
d5a68c8511 | ||
|
|
dcd5822ee7 | ||
|
|
c55f02fa75 | ||
|
|
e702490f2f | ||
|
|
3b53732932 | ||
|
|
485dcc6020 | ||
|
|
a41e423a18 | ||
|
|
2d66242365 | ||
|
|
3268af61eb | ||
|
|
4ced3d26cd | ||
|
|
f69d1aad2d | ||
|
|
f680496ccb | ||
|
|
df734baf97 | ||
|
|
015293eb82 | ||
|
|
5fd51866c4 | ||
|
|
8234dbae8f | ||
|
|
43fbcac25e | ||
|
|
37aa1637de | ||
|
|
c48e0c9320 | ||
|
|
625febb37e |
@@ -0,0 +1,14 @@
|
||||
name: Auto Author Assign
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ opened, reopened ]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
assign-author:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: toshimaru/[email protected]
|
||||
@@ -48,7 +48,7 @@
|
||||
"@radix-ui/react-tooltip": "1.1.4",
|
||||
"@sentry/nextjs": "8.42.0",
|
||||
"@shazow/whatsabi": "^0.17.0",
|
||||
"@tanstack/react-query": "5.62.2",
|
||||
"@tanstack/react-query": "5.62.3",
|
||||
"@tanstack/react-table": "^8.17.3",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@vercel/functions": "^1.5.1",
|
||||
@@ -119,8 +119,8 @@
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "19.0.0",
|
||||
"@types/react-dom": "19.0.0",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/spdx-correct": "^3.1.3",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
|
||||
@@ -150,7 +150,7 @@ export const CustomConnectWallet = (props: {
|
||||
<Link
|
||||
href={`/login${pathname ? `?next=${encodeURIComponent(pathname)}` : ""}`}
|
||||
>
|
||||
Sign In
|
||||
Connect Wallet
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
|
||||
+1
-1
@@ -146,7 +146,7 @@ export function FaucetButton({
|
||||
<Link
|
||||
href={`/login${pathname ? `?next=${encodeURIComponent(pathname)}` : ""}`}
|
||||
>
|
||||
Sign In
|
||||
Connect Wallet
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
|
||||
+6
-11
@@ -1,4 +1,4 @@
|
||||
import { Card, Text } from "tw-components";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
interface NftPropertyProps {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
@@ -7,22 +7,17 @@ interface NftPropertyProps {
|
||||
|
||||
export const NftProperty: React.FC<NftPropertyProps> = ({ property }) => {
|
||||
return (
|
||||
<Card className="flex flex-col gap-2">
|
||||
<Card className="flex flex-col gap-2 p-3">
|
||||
{property?.trait_type && (
|
||||
<Text
|
||||
size="label.sm"
|
||||
color="primary.500"
|
||||
textAlign="center"
|
||||
lineHeight={1.2}
|
||||
>
|
||||
<p className="text-center text-link-foreground text-sm leading-[1.2]">
|
||||
{property?.trait_type}
|
||||
</Text>
|
||||
</p>
|
||||
)}
|
||||
<Text size="label.md" textAlign="center">
|
||||
<p className="text-center text-muted-foreground text-sm">
|
||||
{typeof property?.value === "object"
|
||||
? JSON.stringify(property?.value || {})
|
||||
: property?.value}
|
||||
</Text>
|
||||
</p>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
+17
-24
@@ -1,6 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import { Skeleton, Stat, StatLabel, StatNumber } from "@chakra-ui/react";
|
||||
import { useMemo } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import {
|
||||
@@ -9,7 +7,7 @@ import {
|
||||
totalSupply,
|
||||
} from "thirdweb/extensions/erc721";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { Card } from "tw-components";
|
||||
import { StatCard } from "../../overview/components/stat-card";
|
||||
|
||||
interface SupplyCardsProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -37,27 +35,22 @@ export const SupplyCards: React.FC<SupplyCardsProps> = ({ contract }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 md:flex-row md:gap-6">
|
||||
<Card as={Stat}>
|
||||
<StatLabel mb={{ base: 1, md: 0 }}>Total Supply</StatLabel>
|
||||
<Skeleton isLoaded={nextTokenIdQuery.isSuccess}>
|
||||
<StatNumber>{realTotalSupply.toString()}</StatNumber>
|
||||
</Skeleton>
|
||||
</Card>
|
||||
<Card as={Stat}>
|
||||
<StatLabel mb={{ base: 1, md: 0 }}>Claimed Supply</StatLabel>
|
||||
<Skeleton isLoaded={totalSupplyQuery.isSuccess}>
|
||||
<StatNumber>{totalSupplyQuery?.data?.toString()}</StatNumber>
|
||||
</Skeleton>
|
||||
</Card>
|
||||
<Card as={Stat}>
|
||||
<StatLabel mb={{ base: 1, md: 0 }}>Unclaimed Supply</StatLabel>
|
||||
<Skeleton
|
||||
isLoaded={totalSupplyQuery.isSuccess && nextTokenIdQuery.isSuccess}
|
||||
>
|
||||
<StatNumber>{unclaimedSupply}</StatNumber>
|
||||
</Skeleton>
|
||||
</Card>
|
||||
<div className="flex flex-row gap-3 md:gap-6 [&>*]:grow">
|
||||
<StatCard
|
||||
value={realTotalSupply.toString()}
|
||||
label="Total Supply"
|
||||
isPending={nextTokenIdQuery.isPending}
|
||||
/>
|
||||
<StatCard
|
||||
value={totalSupplyQuery?.data?.toString() || "N/A"}
|
||||
label="Claimed Supply"
|
||||
isPending={totalSupplyQuery.isPending}
|
||||
/>
|
||||
<StatCard
|
||||
value={unclaimedSupply}
|
||||
label="Unclaimed Supply"
|
||||
isPending={totalSupplyQuery.isPending || nextTokenIdQuery.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-13
@@ -41,7 +41,6 @@ import type { NFT, ThirdwebContract } from "thirdweb";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { Heading, Text } from "tw-components";
|
||||
|
||||
interface ContractOverviewNFTGetAllProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -61,11 +60,7 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
{
|
||||
Header: "Token Id",
|
||||
accessor: (row) => row.id?.toString(),
|
||||
Cell: (cell: CellProps<NFT, string>) => (
|
||||
<Text size="body.md" fontFamily="mono">
|
||||
{cell.value}
|
||||
</Text>
|
||||
),
|
||||
Cell: (cell: CellProps<NFT, string>) => <p>{cell.value}</p>,
|
||||
},
|
||||
{
|
||||
Header: "Media",
|
||||
@@ -140,9 +135,9 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
Cell: (cell: CellProps<NFT, number>) => {
|
||||
if (cell.row.original.type === "ERC1155") {
|
||||
return (
|
||||
<Text noOfLines={4} size="body.md" fontFamily="mono">
|
||||
<p className="line-clamp-4 font-mono text-base">
|
||||
{cell.row.original.supply.toString()}
|
||||
</Text>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -267,9 +262,9 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
|
||||
key={columnIndex}
|
||||
>
|
||||
<Text as="label" size="label.sm" color="faded">
|
||||
<p className="text-muted-foreground">
|
||||
{column.render("Header")}
|
||||
</Text>
|
||||
</p>
|
||||
</Th>
|
||||
))}
|
||||
{/* Need to add an empty header for the drawer button */}
|
||||
@@ -352,7 +347,7 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
>
|
||||
<Flex align="center" gap={4}>
|
||||
<Spinner size="sm" />
|
||||
<Heading size="label.lg">Fetching new page</Heading>
|
||||
<p className="text-lg">Fetching new page</p>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
@@ -373,12 +368,12 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
icon={<ChevronLeftIcon className="size-4" />}
|
||||
onClick={() => previousPage()}
|
||||
/>
|
||||
<Text whiteSpace="nowrap">
|
||||
<p className="whitespace-nowrap">
|
||||
Page <strong>{pageIndex + 1}</strong> of{" "}
|
||||
<Skeleton as="span" display="inline" isLoaded={querySuccess}>
|
||||
<strong>{pageCount}</strong>
|
||||
</Skeleton>
|
||||
</Text>
|
||||
</p>
|
||||
<IconButton
|
||||
isDisabled={!canNextPage || queryLoading}
|
||||
aria-label="next page"
|
||||
|
||||
+1
-18
@@ -1,8 +1,8 @@
|
||||
import { SkeletonContainer } from "@/components/ui/skeleton";
|
||||
import { useMemo } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { totalAuctions, totalListings } from "thirdweb/extensions/marketplace";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { StatCard } from "./stat-card";
|
||||
|
||||
const TotalListingsStat: React.FC<{ contract: ThirdwebContract }> = ({
|
||||
contract,
|
||||
@@ -82,20 +82,3 @@ export const ListingStatsV3: React.FC<ListingStatsV3Props> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function StatCard(props: {
|
||||
value: string;
|
||||
isPending: boolean;
|
||||
label: string;
|
||||
}) {
|
||||
return (
|
||||
<dl className="block rounded-lg border border-border bg-muted/50 p-4">
|
||||
<dt className="mb-1.5 text-sm md:text-base">{props.label}</dt>
|
||||
<SkeletonContainer
|
||||
loadedData={props.isPending ? undefined : props.value}
|
||||
skeletonData={"0000"}
|
||||
render={(v) => <dd className="truncate font-semibold text-xl">{v}</dd>}
|
||||
/>
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { SkeletonContainer } from "@/components/ui/skeleton";
|
||||
|
||||
export function StatCard(props: {
|
||||
value: string;
|
||||
isPending: boolean;
|
||||
label: string;
|
||||
}) {
|
||||
return (
|
||||
<dl className="block rounded-lg border border-border bg-muted/50 p-4">
|
||||
<dt className="mb-1.5 text-sm md:text-base">{props.label}</dt>
|
||||
<SkeletonContainer
|
||||
loadedData={props.isPending ? undefined : props.value}
|
||||
skeletonData={"0000"}
|
||||
render={(v) => <dd className="truncate font-semibold text-xl">{v}</dd>}
|
||||
/>
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
+17
-18
@@ -1,9 +1,9 @@
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { useIsAdmin } from "@3rdweb-sdk/react/hooks/useContractRoles";
|
||||
import { Flex, Select, Spinner } from "@chakra-ui/react";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
|
||||
import { Card, Heading, Text } from "tw-components";
|
||||
import { PermissionEditor } from "./permissions-editor";
|
||||
|
||||
interface ContractPermissionProps {
|
||||
@@ -17,7 +17,6 @@ export const ContractPermission: React.FC<ContractPermissionProps> = ({
|
||||
role,
|
||||
description,
|
||||
isPending,
|
||||
|
||||
contract,
|
||||
}) => {
|
||||
const {
|
||||
@@ -33,15 +32,15 @@ export const ContractPermission: React.FC<ContractPermissionProps> = ({
|
||||
const isAdmin = useIsAdmin(contract);
|
||||
|
||||
return (
|
||||
<Card position="relative">
|
||||
<Card className="relative p-4">
|
||||
<Flex direction="column" gap={3}>
|
||||
<div className="mb-3 flex flex-col gap-2">
|
||||
<div className="flex flex-row">
|
||||
<div className="flex grow flex-col gap-1">
|
||||
<Heading size="subtitle.sm" textTransform="capitalize">
|
||||
<p className="font-semibold text-base capitalize">
|
||||
{role === "minter" ? "Minter / Creator" : role}
|
||||
</Heading>
|
||||
<Text>{description}</Text>
|
||||
</p>
|
||||
<p className="text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
|
||||
{role === "transfer" && (
|
||||
@@ -49,7 +48,7 @@ export const ContractPermission: React.FC<ContractPermissionProps> = ({
|
||||
{isPending || isSubmitting ? (
|
||||
<Flex align="center" gap={2} px={2}>
|
||||
<Spinner size="sm" />
|
||||
<Text>{isPending ? "Loading ..." : "Updating ..."}</Text>
|
||||
<p>{isPending ? "Loading ..." : "Updating ..."}</p>
|
||||
</Flex>
|
||||
) : (
|
||||
<Select
|
||||
@@ -84,7 +83,7 @@ export const ContractPermission: React.FC<ContractPermissionProps> = ({
|
||||
{isPending || isSubmitting ? (
|
||||
<Flex align="center" gap={2} px={2}>
|
||||
<Spinner size="sm" />
|
||||
<Text>{isPending ? "Loading ..." : "Updating ..."}</Text>
|
||||
<p>{isPending ? "Loading ..." : "Updating ..."}</p>
|
||||
</Flex>
|
||||
) : (
|
||||
<Select
|
||||
@@ -119,7 +118,7 @@ export const ContractPermission: React.FC<ContractPermissionProps> = ({
|
||||
{isPending || isSubmitting ? (
|
||||
<Flex align="center" gap={2} px={2}>
|
||||
<Spinner size="sm" />
|
||||
<Text>{isPending ? "Loading ..." : "Updating ..."}</Text>
|
||||
<p>{isPending ? "Loading ..." : "Updating ..."}</p>
|
||||
</Flex>
|
||||
) : (
|
||||
<Select
|
||||
@@ -165,8 +164,8 @@ export const ContractPermission: React.FC<ContractPermissionProps> = ({
|
||||
padding="10px"
|
||||
gap={3}
|
||||
>
|
||||
<InfoIcon className="size-6 text-primary-foreground hover:opacity-10" />
|
||||
<Text color="primary.800" _dark={{ color: "primary.100" }}>
|
||||
<InfoIcon className="size-6 text-primary hover:opacity-10 dark:text-primary-foreground" />
|
||||
<p className="text-primary dark:text-primary-foreground">
|
||||
{isRestricted ? (
|
||||
<>
|
||||
The tokens in this contract are currently{" "}
|
||||
@@ -181,7 +180,7 @@ export const ContractPermission: React.FC<ContractPermissionProps> = ({
|
||||
transfer their tokens.
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</p>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
@@ -200,8 +199,8 @@ export const ContractPermission: React.FC<ContractPermissionProps> = ({
|
||||
padding="10px"
|
||||
gap={3}
|
||||
>
|
||||
<InfoIcon className="size-6 text-primary-foreground hover:opacity-10" />
|
||||
<Text color="primary.800" _dark={{ color: "primary.100" }}>
|
||||
<InfoIcon className="size-6 text-primary hover:opacity-10 dark:text-primary-foreground" />
|
||||
<p className="text-primary dark:text-primary-foreground">
|
||||
{isRestricted ? (
|
||||
<>
|
||||
Currently, only addresses specified below will be able to
|
||||
@@ -210,7 +209,7 @@ export const ContractPermission: React.FC<ContractPermissionProps> = ({
|
||||
) : (
|
||||
<>This marketplace is open for anyone to create listings.</>
|
||||
)}
|
||||
</Text>
|
||||
</p>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
@@ -229,8 +228,8 @@ export const ContractPermission: React.FC<ContractPermissionProps> = ({
|
||||
padding="10px"
|
||||
gap={3}
|
||||
>
|
||||
<InfoIcon className="size-6 text-primary-foreground hover:opacity-10" />
|
||||
<Text color="primary.800" _dark={{ color: "primary.100" }}>
|
||||
<InfoIcon className="size-6 text-primary hover:opacity-10 dark:text-primary-foreground" />
|
||||
<p className="text-primary dark:text-primary-foreground">
|
||||
{isRestricted ? (
|
||||
<>
|
||||
Currently, only assets from the contracts specified below
|
||||
@@ -242,7 +241,7 @@ export const ContractPermission: React.FC<ContractPermissionProps> = ({
|
||||
contract.
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</p>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
|
||||
@@ -17,14 +17,14 @@ export function DeployedContractsPageHeader() {
|
||||
setImportModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-4 md:pb-4 lg:flex-row lg:justify-between">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:justify-between">
|
||||
<div>
|
||||
<h1 className="mb-1.5 font-semibold text-3xl tracking-tight lg:text-4xl">
|
||||
Your contracts
|
||||
<h1 className="mb-1 font-semibold text-2xl tracking-tight lg:text-3xl">
|
||||
Deployed contracts
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
The list of contract instances that you have deployed or imported
|
||||
with thirdweb across all networks
|
||||
The list of contracts that you have deployed or imported with
|
||||
thirdweb across all networks
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2 [&>*]:grow">
|
||||
|
||||
@@ -8,12 +8,11 @@ import { getSortedDeployedContracts } from "./getSortedDeployedContracts";
|
||||
|
||||
export function DeployedContractsPage(props: {
|
||||
address: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<div className="flex grow flex-col">
|
||||
<DeployedContractsPageHeader />
|
||||
<div className="h-6" />
|
||||
<div className="h-8" />
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DeployedContractsPageAsync {...props} />
|
||||
</Suspense>
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
|
||||
export default function Layout(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <div className="container flex grow flex-col">{props.children}</div>;
|
||||
const layoutPath = "/account/contracts";
|
||||
|
||||
return (
|
||||
<SidebarLayout
|
||||
sidebarLinks={[
|
||||
{
|
||||
href: layoutPath,
|
||||
label: "Deployed contracts",
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
href: `${layoutPath}/published`,
|
||||
label: "Published contracts",
|
||||
exactMatch: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
</SidebarLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,5 @@ export default async function Page() {
|
||||
return redirect(`/login?next=${encodeURIComponent("/account/contracts")}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<DeployedContractsPage
|
||||
address={accountAddress}
|
||||
className="flex grow flex-col pt-10 pb-10"
|
||||
/>
|
||||
);
|
||||
return <DeployedContractsPage address={accountAddress} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Suspense } from "react";
|
||||
import { PublishedContracts } from "../../../(dashboard)/profile/[addressOrEns]/components/published-contracts";
|
||||
import { resolveAddressAndEns } from "../../../(dashboard)/profile/[addressOrEns]/resolveAddressAndEns";
|
||||
import { fetchPublishedContracts } from "../../../../components/contract-components/fetchPublishedContracts";
|
||||
|
||||
export async function PublishedContractsPage(props: {
|
||||
publisherAddress: string;
|
||||
}) {
|
||||
const resolvedInfo = await resolveAddressAndEns(props.publisherAddress);
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-start">
|
||||
<div>
|
||||
<h2 className="mb-1 font-semibold text-2xl tracking-tight lg:text-3xl">
|
||||
Published contracts
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground text-sm">
|
||||
The list of contracts published to thirdweb across all networks.{" "}
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/contracts/publish/overview"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more about publishing contracts
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button asChild>
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/contracts/publish/publish-contract"
|
||||
target="_blank"
|
||||
className="gap-2"
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
Publish Contract
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<Suspense fallback={<GenericLoadingPage />}>
|
||||
<AsyncPublishedContractsTable
|
||||
publisherAddress={props.publisherAddress}
|
||||
publisherEnsName={resolvedInfo?.ensName}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function AsyncPublishedContractsTable(props: {
|
||||
publisherAddress: string;
|
||||
publisherEnsName: string | undefined;
|
||||
}) {
|
||||
const publishedContracts = await fetchPublishedContracts(
|
||||
props.publisherAddress,
|
||||
);
|
||||
|
||||
if (publishedContracts.length === 0) {
|
||||
return (
|
||||
<div className="flex min-h-[300px] grow items-center justify-center rounded-lg border border-border">
|
||||
No published contracts found
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PublishedContracts
|
||||
publishedContracts={publishedContracts}
|
||||
publisherEnsName={props.publisherEnsName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { getAuthTokenWalletAddress } from "../../../api/lib/getAuthToken";
|
||||
import { loginRedirect } from "../../../login/loginRedirect";
|
||||
import { PublishedContractsPage } from "./PublishedContractsPage";
|
||||
|
||||
export default async function Page() {
|
||||
const accountAddress = await getAuthTokenWalletAddress();
|
||||
|
||||
if (!accountAddress) {
|
||||
loginRedirect("/account/contracts");
|
||||
}
|
||||
|
||||
return <PublishedContractsPage publisherAddress={accountAddress} />;
|
||||
}
|
||||
@@ -51,7 +51,6 @@ async function HeaderAndNav(props: {
|
||||
{
|
||||
path: "/account/contracts",
|
||||
name: "Contracts",
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
path: "/account/settings",
|
||||
|
||||
@@ -123,11 +123,7 @@ function PageContent(props: {
|
||||
| {
|
||||
id: "complete";
|
||||
}
|
||||
>(
|
||||
props.account
|
||||
? { id: "onboarding", account: props.account }
|
||||
: { id: "login" },
|
||||
);
|
||||
>({ id: "login" });
|
||||
|
||||
const router = useDashboardRouter();
|
||||
const connectionStatus = useActiveWalletConnectionStatus();
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../account/settings/getAccount";
|
||||
import { LoginAndOnboardingPage } from "./LoginPage";
|
||||
import { isOnboardingComplete } from "./onboarding/isOnboardingRequired";
|
||||
|
||||
export default async function Page(props: {
|
||||
searchParams: Promise<{
|
||||
@@ -11,13 +9,11 @@ export default async function Page(props: {
|
||||
const nextPath = (await props.searchParams).next;
|
||||
const account = await getRawAccount();
|
||||
|
||||
if (account && isOnboardingComplete(account)) {
|
||||
if (nextPath) {
|
||||
redirect(nextPath);
|
||||
} else {
|
||||
redirect("/team");
|
||||
}
|
||||
}
|
||||
// don't redirect away from login page if authToken is already present and onboarding is done
|
||||
// so that if user is stuck in a state where cookie is set, account onboarding is complete but the wallet is not connected, they can connect wallet, sign in and continue
|
||||
|
||||
// if the user is already logged in, wallet is connected and onboarding is complete
|
||||
// user will be redirected to the next path on the client side without having to do anything
|
||||
|
||||
return <LoginAndOnboardingPage account={account} nextPath={nextPath} />;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getProjects } from "@/api/projects";
|
||||
import { getTeamNebulaWaitList, getTeams } from "@/api/team";
|
||||
import { TabPathLinks } from "@/components/ui/tabs";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getValidAccount } from "../../../account/settings/getAccount";
|
||||
import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client";
|
||||
|
||||
export default async function TeamLayout(props: {
|
||||
@@ -9,7 +10,11 @@ export default async function TeamLayout(props: {
|
||||
params: Promise<{ team_slug: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const teams = await getTeams();
|
||||
|
||||
const [account, teams] = await Promise.all([
|
||||
getValidAccount(`/team/${params.team_slug}`),
|
||||
getTeams(),
|
||||
]);
|
||||
|
||||
if (!teams) {
|
||||
redirect("/login");
|
||||
@@ -37,6 +42,7 @@ export default async function TeamLayout(props: {
|
||||
currentTeam={team}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
currentProject={undefined}
|
||||
account={account}
|
||||
/>
|
||||
|
||||
<TabPathLinks
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
|
||||
export default async function Layout(props: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ team_slug: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const layoutPath = `/team/${params.team_slug}/~/contracts`;
|
||||
|
||||
return (
|
||||
<SidebarLayout
|
||||
sidebarLinks={[
|
||||
{
|
||||
href: layoutPath,
|
||||
label: "Deployed contracts",
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
href: `${layoutPath}/published`,
|
||||
label: "Published contracts",
|
||||
exactMatch: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
</SidebarLayout>
|
||||
);
|
||||
}
|
||||
@@ -14,12 +14,5 @@ export default async function Page(props: {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container flex grow flex-col">
|
||||
<DeployedContractsPage
|
||||
address={accountAddress}
|
||||
className="flex grow flex-col pt-10 pb-10"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return <DeployedContractsPage address={accountAddress} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { PublishedContractsPage } from "../../../../../../account/contracts/published/PublishedContractsPage";
|
||||
import { getAuthTokenWalletAddress } from "../../../../../../api/lib/getAuthToken";
|
||||
import { loginRedirect } from "../../../../../../login/loginRedirect";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ team_slug: string }>;
|
||||
}) {
|
||||
const accountAddress = await getAuthTokenWalletAddress();
|
||||
const params = await props.params;
|
||||
|
||||
if (!accountAddress) {
|
||||
loginRedirect(`/team/${params.team_slug}/~/contracts`);
|
||||
}
|
||||
|
||||
return <PublishedContractsPage publisherAddress={accountAddress} />;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
|
||||
export default async function Layout(props: {
|
||||
children?: React.ReactNode;
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const layoutPath = `/team/${params.team_slug}/${params.project_slug}/contracts`;
|
||||
|
||||
return (
|
||||
<SidebarLayout
|
||||
sidebarLinks={[
|
||||
{
|
||||
href: layoutPath,
|
||||
label: "Deployed contracts",
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
href: `${layoutPath}/published`,
|
||||
label: "Published contracts",
|
||||
exactMatch: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
</SidebarLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { DeployedContractsPage } from "../../../../account/contracts/_components/DeployedContractsPage";
|
||||
import { getAuthTokenWalletAddress } from "../../../../api/lib/getAuthToken";
|
||||
import { loginRedirect } from "../../../../login/loginRedirect";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
@@ -9,17 +9,8 @@ export default async function Page(props: {
|
||||
|
||||
if (!accountAddress) {
|
||||
const { team_slug, project_slug } = await props.params;
|
||||
return redirect(
|
||||
`/login?next=${encodeURIComponent(`/team/${team_slug}/${project_slug}/contracts`)}`,
|
||||
);
|
||||
loginRedirect(`/team/${team_slug}/${project_slug}/contracts`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container flex grow flex-col">
|
||||
<DeployedContractsPage
|
||||
address={accountAddress}
|
||||
className="flex grow flex-col pt-10 pb-10"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return <DeployedContractsPage address={accountAddress} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { PublishedContractsPage } from "../../../../../account/contracts/published/PublishedContractsPage";
|
||||
import { getAuthTokenWalletAddress } from "../../../../../api/lib/getAuthToken";
|
||||
import { loginRedirect } from "../../../../../login/loginRedirect";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
}) {
|
||||
const accountAddress = await getAuthTokenWalletAddress();
|
||||
const params = await props.params;
|
||||
|
||||
if (!accountAddress) {
|
||||
loginRedirect(`/team/${params.team_slug}/${params.project_slug}/contracts`);
|
||||
}
|
||||
|
||||
return <PublishedContractsPage publisherAddress={accountAddress} />;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { getProjects } from "@/api/projects";
|
||||
import { getTeamNebulaWaitList, getTeams } from "@/api/team";
|
||||
import { TabPathLinks } from "@/components/ui/tabs";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getValidAccount } from "../../../account/settings/getAccount";
|
||||
import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client";
|
||||
|
||||
export default async function TeamLayout(props: {
|
||||
@@ -10,7 +11,10 @@ export default async function TeamLayout(props: {
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const teams = await getTeams();
|
||||
const [teams, account] = await Promise.all([
|
||||
getTeams(),
|
||||
getValidAccount(`/team/${params.team_slug}/${params.project_slug}`),
|
||||
]);
|
||||
|
||||
if (!teams) {
|
||||
redirect("/login");
|
||||
@@ -49,6 +53,7 @@ export default async function TeamLayout(props: {
|
||||
currentProject={project}
|
||||
currentTeam={team}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
account={account}
|
||||
/>
|
||||
<TabPathLinks
|
||||
tabContainerClassName="px-4 lg:px-6"
|
||||
|
||||
@@ -35,7 +35,7 @@ export function TeamHeaderLoggedOutDesktopUI(props: {
|
||||
<Link
|
||||
href={`/login${pathname ? `?next=${encodeURIComponent(pathname)}` : ""}`}
|
||||
>
|
||||
Sign in
|
||||
Connect Wallet
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -64,7 +64,7 @@ export function TeamHeaderLoggedOutMobileUI(props: {
|
||||
<Link
|
||||
href={`/login${pathname ? `?next=${encodeURIComponent(pathname)}` : ""}`}
|
||||
>
|
||||
Sign in
|
||||
Connect Wallet
|
||||
</Link>
|
||||
</Button>
|
||||
<MobileBurgerMenuButton type="loggedOut" />
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Team } from "@/api/team";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import { useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useActiveWallet, useDisconnect } from "thirdweb/react";
|
||||
import { LazyCreateAPIKeyDialog } from "../../../../components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
|
||||
@@ -20,10 +20,10 @@ export function TeamHeaderLoggedIn(props: {
|
||||
currentTeam: Team;
|
||||
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
|
||||
currentProject: Project | undefined;
|
||||
account: Pick<Account, "email" | "id">;
|
||||
}) {
|
||||
const [isCreateProjectDialogOpen, setIsCreateProjectDialogOpen] =
|
||||
useState(false);
|
||||
const myAccountQuery = useAccount();
|
||||
const activeWallet = useActiveWallet();
|
||||
const { disconnect } = useDisconnect();
|
||||
const router = useDashboardRouter();
|
||||
@@ -45,7 +45,7 @@ export function TeamHeaderLoggedIn(props: {
|
||||
currentProject: props.currentProject,
|
||||
currentTeam: props.currentTeam,
|
||||
teamsAndProjects: props.teamsAndProjects,
|
||||
account: myAccountQuery.data,
|
||||
account: props.account,
|
||||
logout: logout,
|
||||
connectButton: <CustomConnectWallet />,
|
||||
createProject: () => setIsCreateProjectDialogOpen(true),
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { getProjects } from "@/api/projects";
|
||||
import { getTeams } from "@/api/team";
|
||||
import { getAuthToken } from "../../../api/lib/getAuthToken";
|
||||
import { getRawAccount } from "../../../account/settings/getAccount";
|
||||
import { TeamHeaderLoggedOut } from "./TeamHeaderLoggedOut";
|
||||
import { TeamHeaderLoggedIn } from "./team-header-logged-in.client";
|
||||
|
||||
export async function TeamHeader() {
|
||||
const authToken = await getAuthToken();
|
||||
const account = await getRawAccount();
|
||||
|
||||
if (!authToken) {
|
||||
if (!account) {
|
||||
return <TeamHeaderLoggedOut />;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export async function TeamHeader() {
|
||||
currentTeam={firstTeam}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
currentProject={undefined}
|
||||
account={account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ export const MismatchButton = forwardRef<
|
||||
<Link
|
||||
href={`/login${pathname ? `?next=${encodeURIComponent(pathname)}` : ""}`}
|
||||
>
|
||||
Sign In
|
||||
Connect Wallet
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { Flex, Skeleton } from "@chakra-ui/react";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { replaceDeployerAddress } from "lib/publisher-utils";
|
||||
import { ZERO_ADDRESS } from "thirdweb";
|
||||
import {
|
||||
AccountAddress,
|
||||
AccountAvatar,
|
||||
@@ -41,7 +42,11 @@ export const PublisherHeader: React.FC<PublisherHeaderProps> = ({
|
||||
Published by
|
||||
</Heading>
|
||||
|
||||
<AccountProvider address={ensQuery.data?.address || ""} client={client}>
|
||||
<AccountProvider
|
||||
// passing zero address during loading time to prevent the component from crashing
|
||||
address={ensQuery.data?.address || ZERO_ADDRESS}
|
||||
client={client}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href={replaceDeployerAddress(
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "1.1.4",
|
||||
"@tanstack/react-query": "5.62.2",
|
||||
"@tanstack/react-query": "5.62.3",
|
||||
"@thirdweb-dev/engine": "^0.0.16",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -47,8 +47,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.10.1",
|
||||
"@types/react": "19.0.0",
|
||||
"@types/react-dom": "19.0.0",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-next": "15.0.4",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
||||
|
||||
@@ -15,6 +15,7 @@ export default function Page() {
|
||||
address!
|
||||
</>
|
||||
}
|
||||
deployLink="https://thirdweb.com/team/~/~/engine/create?utm_source=playground"
|
||||
docsLink="https://thirdweb-engine.apidocumentation.com/reference#tag/erc20/POST/contract/{chain}/{contractAddress}/erc20/mint-batch-to?utm_source=playground"
|
||||
heroLink="/airdrop.avif"
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ export default function Page() {
|
||||
sponsor the gas so your users only need a wallet address!
|
||||
</>
|
||||
}
|
||||
deployLink="https://thirdweb.com/team/~/~/engine/create?utm_source=playground"
|
||||
docsLink="https://thirdweb-engine.apidocumentation.com/reference#tag/erc1155/POST/contract/{chain}/{contractAddress}/erc1155/mint-to?utm_source=playground"
|
||||
heroLink="/minting.avif"
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ export default function Page() {
|
||||
transaction or backend wallet events.
|
||||
</>
|
||||
}
|
||||
deployLink="https://thirdweb.com/team/~/~/engine/create?utm_source=playground"
|
||||
docsLink="https://portal.thirdweb.com/engine/features/webhooks?utm_source=playground"
|
||||
heroLink="/webhooks.avif"
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "../ui/button";
|
||||
export function EngineAPIHeader(props: {
|
||||
title: string;
|
||||
description: React.ReactNode;
|
||||
deployLink: string;
|
||||
docsLink: string;
|
||||
heroLink: string;
|
||||
}) {
|
||||
@@ -27,6 +28,11 @@ export function EngineAPIHeader(props: {
|
||||
|
||||
<div className="flex flex-col gap-3 md:flex-row">
|
||||
<Button asChild size="lg">
|
||||
<Link target="_blank" href={props.deployLink}>
|
||||
Deploy an Instance
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link target="_blank" href={props.docsLink}>
|
||||
View docs
|
||||
</Link>
|
||||
@@ -39,14 +45,6 @@ export function EngineAPIHeader(props: {
|
||||
Book a Demo
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://thirdweb.com/team/~/~/engine/create?utm_source=playground"
|
||||
>
|
||||
Deploy an Instance
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@tanstack/react-query": "5.62.2",
|
||||
"@tanstack/react-query": "5.62.3",
|
||||
"@tryghost/content-api": "^1.11.21",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -55,8 +55,8 @@
|
||||
"@types/flexsearch": "^0.7.6",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "22.10.1",
|
||||
"@types/react": "19.0.0",
|
||||
"@types/react-dom": "19.0.0",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"@types/react-html-parser": "^2.0.6",
|
||||
"@types/tryghost__content-api": "^1.3.16",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
|
||||
@@ -230,10 +230,10 @@ import { privateKeyToAccount } from "thirdweb/wallets";
|
||||
import { client } from "@/lib/client";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const privateKey = process.env.THIRDWEB_ADMIN_PRIVATE_KEY || "";
|
||||
const privateKey = process.env.AUTH_PRIVATE_KEY || "";
|
||||
|
||||
if (!privateKey) {
|
||||
throw new Error("Missing THIRDWEB_ADMIN_PRIVATE_KEY in .env file.");
|
||||
throw new Error("Missing AUTH_PRIVATE_KEY in .env file.");
|
||||
}
|
||||
|
||||
const thirdwebAuth = createAuth({
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@tanstack/react-query": "5.62.2",
|
||||
"@tanstack/react-query": "5.62.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.4",
|
||||
@@ -37,8 +37,8 @@
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "15.0.4",
|
||||
"@types/node": "22.10.1",
|
||||
"@types/react": "19.0.0",
|
||||
"@types/react-dom": "19.0.0",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"eslint": "8.57.0",
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"ws@>=8.0.0 <8.17.1": "8.17.1",
|
||||
"ws@>=7.0.0 <7.5.10": "7.5.10",
|
||||
"ws@>=6.0.0 <6.2.3": "6.2.3",
|
||||
"eslint": "8.57.0",
|
||||
"@typescript-eslint/typescript-estree": "^7.14.1",
|
||||
"axios@<1.7.4": "^1.7.4",
|
||||
"send@<0.19.0": "^0.19.0",
|
||||
"elliptic@<6.5.7": "^6.5.7",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @thirdweb-dev/service-utils
|
||||
|
||||
## 0.4.52
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5670](https://github.com/thirdweb-dev/js/pull/5670) [`e702490`](https://github.com/thirdweb-dev/js/commit/e702490f2ff5e2b7d6b28c06731af880583cf8bc) Thanks [@arcoraven](https://github.com/arcoraven)! - Add modelName, sessionId, requestId to usage
|
||||
|
||||
## 0.4.51
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/service-utils",
|
||||
"version": "0.4.51",
|
||||
"version": "0.4.52",
|
||||
"type": "module",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
|
||||
@@ -78,5 +78,8 @@ export const usageEventSchema = z.object({
|
||||
providerIp: z.string().optional(),
|
||||
promptTokens: z.number().int().nonnegative().optional(),
|
||||
completionTokens: z.number().int().nonnegative().optional(),
|
||||
modelName: z.string().optional(),
|
||||
sessionId: z.string().optional(),
|
||||
requestId: z.string().optional(),
|
||||
});
|
||||
export type UsageEvent = z.infer<typeof usageEventSchema>;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
"no-restricted-syntax": [
|
||||
"warn",
|
||||
{
|
||||
selector: "CallExpression[callee.name='useEffect']",
|
||||
message:
|
||||
'Are you *sure* you need to use "useEffect" here? If you loading any async function prefer using "useQuery".',
|
||||
},
|
||||
{
|
||||
selector: "CallExpression[callee.name='createContext']",
|
||||
message:
|
||||
'Are you *sure* you need to use a "Context"? In almost all cases you should prefer passing props directly.',
|
||||
},
|
||||
{
|
||||
selector: "CallExpression[callee.name='defineChain']",
|
||||
message: "Use getCachedChain instead for all internal usage",
|
||||
},
|
||||
],
|
||||
"no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
destructuredArrayIgnorePattern: "^_",
|
||||
ignoreRestSiblings: true,
|
||||
args: "none", // Ignore variables used in type definitions
|
||||
},
|
||||
],
|
||||
"tsdoc/syntax": "warn",
|
||||
},
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint", "eslint-plugin-tsdoc"],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2019,
|
||||
ecmaFeatures: {
|
||||
impliedStrict: true,
|
||||
jsx: true,
|
||||
},
|
||||
warnOnUnsupportedTypeScriptVersion: false,
|
||||
},
|
||||
overrides: [
|
||||
// THIS NEEDS TO GO LAST!
|
||||
{
|
||||
files: ["*.ts", "*.js", "*.tsx", "*.jsx"],
|
||||
extends: ["biome"],
|
||||
},
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,108 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.76.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#5533](https://github.com/thirdweb-dev/js/pull/5533) [`43fbcac`](https://github.com/thirdweb-dev/js/commit/43fbcac25e9383743f1f42af9da7fe1c1eae12b4) Thanks [@kien-ngo](https://github.com/kien-ngo)! - The Connected-details button now shows USD value next to the token balance.
|
||||
|
||||
### Breaking change to the AccountBalance
|
||||
|
||||
The formatFn props now takes in an object of type `AccountBalanceInfo`. The old `formatFn` was inflexible because it only allowed you to format the balance value.
|
||||
With this new version, you have access to both the balance and symbol.
|
||||
|
||||
```tsx
|
||||
import { AccountBalance, type AccountBalanceInfo } from "thirdweb/react";
|
||||
|
||||
<AccountBalance
|
||||
// Show the symbol in lowercase, before the balance
|
||||
formatFn={(props: AccountBalanceInfo) =>
|
||||
`${props.symbol.toLowerCase()} ${props.balance}`
|
||||
}
|
||||
/>;
|
||||
```
|
||||
|
||||
AccountBalance now supports showing the token balance in fiat value (only USD supported at the moment)
|
||||
|
||||
```tsx
|
||||
<AccountBalance showBalanceInFiat="USD" />
|
||||
```
|
||||
|
||||
The `formatFn` prop now takes in an object of type `AccountBalanceInfo` and outputs a string
|
||||
|
||||
```tsx
|
||||
import { AccountBalance, type AccountBalanceInfo } from "thirdweb/react";
|
||||
|
||||
<AccountBalance
|
||||
formatFn={(props: AccountBalanceInfo) =>
|
||||
`${props.balance}---${props.symbol.toLowerCase()}`
|
||||
}
|
||||
/>;
|
||||
|
||||
// Result: 11.12---eth
|
||||
```
|
||||
|
||||
### ConnectButton also supports displaying balance in fiat since it uses AccountBalance internally
|
||||
|
||||
```tsx
|
||||
<ConnectButton
|
||||
// Show USD value on the button
|
||||
detailsButton={{
|
||||
showBalanceInFiat: "USD",
|
||||
}}
|
||||
// Show USD value on the modal
|
||||
detailsModal={{
|
||||
showBalanceInFiat: "USD",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Export utils functions:
|
||||
|
||||
formatNumber: Round up a number to a certain decimal place
|
||||
|
||||
```tsx
|
||||
import { formatNumber } from "thirdweb/utils";
|
||||
const value = formatNumber(12.1214141, 1); // 12.1
|
||||
```
|
||||
|
||||
shortenLargeNumber: Shorten the string for large value. Mainly used for the AccountBalance's `formatFn`
|
||||
|
||||
```tsx
|
||||
import { shortenLargeNumber } from "thirdweb/utils";
|
||||
const numStr = shortenLargeNumber(1_000_000_000);
|
||||
```
|
||||
|
||||
### Fix to ConnectButton
|
||||
|
||||
The social image of the Details button now display correctly for non-square image.
|
||||
|
||||
### Massive test coverage improvement for the Connected-button components
|
||||
|
||||
- [#5655](https://github.com/thirdweb-dev/js/pull/5655) [`f69d1aa`](https://github.com/thirdweb-dev/js/commit/f69d1aad2d154a05ebc61a1d7545bd9e5aab17be) Thanks [@kien-ngo](https://github.com/kien-ngo)! - Improve NFT Components
|
||||
- Add custom resolver methods to NFTMedia, NFTName and NFTDescription
|
||||
- Add caching for the NFT-info-getter method to improve performance
|
||||
- Small fix to handle falsy values for NFT media src, name and description
|
||||
- Improve test coverage by extracting internal logics and testing them
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5660](https://github.com/thirdweb-dev/js/pull/5660) [`d5a68c8`](https://github.com/thirdweb-dev/js/commit/d5a68c85111809bf39e57f5fb39b5458c1f3fe9a) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Fix: Correctly cleans the "Custom Auth" profile type label
|
||||
|
||||
- [#5672](https://github.com/thirdweb-dev/js/pull/5672) [`3b53732`](https://github.com/thirdweb-dev/js/commit/3b5373293202b8ff13cc1502bef3cc9dffaa5afa) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Fix ox hardset version
|
||||
|
||||
- [#5653](https://github.com/thirdweb-dev/js/pull/5653) [`df734ba`](https://github.com/thirdweb-dev/js/commit/df734baf97ec2e976fedb124ecfbac7119c0bc5f) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - More helpful error messages for enclave and userop errors
|
||||
|
||||
- [#5656](https://github.com/thirdweb-dev/js/pull/5656) [`f680496`](https://github.com/thirdweb-dev/js/commit/f680496ccb1c639fab644fb54e9a962627cf3228) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix connecting to cb wallet browser extension when already on the same chain
|
||||
|
||||
- [#5671](https://github.com/thirdweb-dev/js/pull/5671) [`dcd5822`](https://github.com/thirdweb-dev/js/commit/dcd5822ee707fa52f1c3e02c75992485f83922f0) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Allow signature minting for LoyaltyCard contracts by passing the contractType
|
||||
|
||||
- [#5673](https://github.com/thirdweb-dev/js/pull/5673) [`c55f02f`](https://github.com/thirdweb-dev/js/commit/c55f02fa75a7f8959bbe69eac6cebfc7927ed865) Thanks [@ElasticBottle](https://github.com/ElasticBottle)! - always include gas price information even if it's 0 for enclave wallets
|
||||
|
||||
- [#5617](https://github.com/thirdweb-dev/js/pull/5617) [`c48e0c9`](https://github.com/thirdweb-dev/js/commit/c48e0c9320830aa69c0e9567d985ed8a94eeaaf1) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Fix: Disconnect smart account when account signer is disconnected
|
||||
|
||||
- [#5668](https://github.com/thirdweb-dev/js/pull/5668) [`485dcc6`](https://github.com/thirdweb-dev/js/commit/485dcc6020089a80d994c24882f389c24a0af039) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix add chain not triggering for certain wallets
|
||||
|
||||
## 5.75.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
+374
-373
@@ -1,375 +1,376 @@
|
||||
{
|
||||
"name": "thirdweb",
|
||||
"version": "5.75.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/thirdweb-dev/js/issues"
|
||||
},
|
||||
"author": "thirdweb eng <[email protected]>",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"thirdweb": "./dist/esm/cli/bin.js",
|
||||
"thirdweb-cli": "./dist/esm/cli/bin.js"
|
||||
},
|
||||
"main": "./dist/cjs/exports/thirdweb.js",
|
||||
"module": "./dist/esm/exports/thirdweb.js",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"import": "./dist/esm/exports/thirdweb.js",
|
||||
"default": "./dist/cjs/exports/thirdweb.js"
|
||||
},
|
||||
"./adapters/*": {
|
||||
"types": "./dist/types/exports/adapters/*.d.ts",
|
||||
"import": "./dist/esm/exports/adapters/*.js",
|
||||
"default": "./dist/cjs/exports/adapters/*.js"
|
||||
},
|
||||
"./auth": {
|
||||
"types": "./dist/types/exports/auth.d.ts",
|
||||
"import": "./dist/esm/exports/auth.js",
|
||||
"default": "./dist/cjs/exports/auth.js"
|
||||
},
|
||||
"./chains": {
|
||||
"types": "./dist/types/exports/chains.d.ts",
|
||||
"import": "./dist/esm/exports/chains.js",
|
||||
"default": "./dist/cjs/exports/chains.js"
|
||||
},
|
||||
"./contract": {
|
||||
"types": "./dist/types/exports/contract.d.ts",
|
||||
"import": "./dist/esm/exports/contract.js",
|
||||
"default": "./dist/cjs/exports/contract.js"
|
||||
},
|
||||
"./deploys": {
|
||||
"types": "./dist/types/exports/deploys.d.ts",
|
||||
"import": "./dist/esm/exports/deploys.js",
|
||||
"default": "./dist/cjs/exports/deploys.js"
|
||||
},
|
||||
"./event": {
|
||||
"types": "./dist/types/exports/event.d.ts",
|
||||
"import": "./dist/esm/exports/event.js",
|
||||
"default": "./dist/cjs/exports/event.js"
|
||||
},
|
||||
"./extensions/*": {
|
||||
"types": "./dist/types/exports/extensions/*.d.ts",
|
||||
"import": "./dist/esm/exports/extensions/*.js",
|
||||
"default": "./dist/cjs/exports/extensions/*.js"
|
||||
},
|
||||
"./pay": {
|
||||
"types": "./dist/types/exports/pay.d.ts",
|
||||
"import": "./dist/esm/exports/pay.js",
|
||||
"default": "./dist/cjs/exports/pay.js"
|
||||
},
|
||||
"./react": {
|
||||
"types": "./dist/types/exports/react.d.ts",
|
||||
"import": "./dist/esm/exports/react.js",
|
||||
"react-native": "./dist/esm/exports/react.native.js",
|
||||
"default": "./dist/cjs/exports/react.js"
|
||||
},
|
||||
"./react-native": {
|
||||
"types": "./dist/types/exports/react-native.d.ts",
|
||||
"import": "./dist/esm/exports/react-native.js",
|
||||
"default": "./dist/cjs/exports/react-native.js"
|
||||
},
|
||||
"./rpc": {
|
||||
"types": "./dist/types/exports/rpc.d.ts",
|
||||
"import": "./dist/esm/exports/rpc.js",
|
||||
"default": "./dist/cjs/exports/rpc.js"
|
||||
},
|
||||
"./storage": {
|
||||
"types": "./dist/types/exports/storage.d.ts",
|
||||
"import": "./dist/esm/exports/storage.js",
|
||||
"default": "./dist/cjs/exports/storage.js"
|
||||
},
|
||||
"./transaction": {
|
||||
"types": "./dist/types/exports/transaction.d.ts",
|
||||
"import": "./dist/esm/exports/transaction.js",
|
||||
"default": "./dist/cjs/exports/transaction.js"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./dist/types/exports/utils.d.ts",
|
||||
"import": "./dist/esm/exports/utils.js",
|
||||
"default": "./dist/cjs/exports/utils.js"
|
||||
},
|
||||
"./wallets": {
|
||||
"types": "./dist/types/exports/wallets.d.ts",
|
||||
"import": "./dist/esm/exports/wallets.js",
|
||||
"react-native": "./dist/esm/exports/wallets.native.js",
|
||||
"default": "./dist/cjs/exports/wallets.js"
|
||||
},
|
||||
"./wallets/in-app": {
|
||||
"types": "./dist/types/exports/wallets/in-app.d.ts",
|
||||
"import": "./dist/esm/exports/wallets/in-app.js",
|
||||
"react-native": "./dist/esm/exports/wallets/in-app.native.js",
|
||||
"default": "./dist/cjs/exports/wallets/in-app.js"
|
||||
},
|
||||
"./wallets/*": {
|
||||
"types": "./dist/types/exports/wallets/*.d.ts",
|
||||
"import": "./dist/esm/exports/wallets/*.js",
|
||||
"default": "./dist/cjs/exports/wallets/*.js"
|
||||
},
|
||||
"./modules": {
|
||||
"types": "./dist/types/exports/modules.d.ts",
|
||||
"import": "./dist/esm/exports/modules.js",
|
||||
"default": "./dist/cjs/exports/modules.js"
|
||||
},
|
||||
"./social": {
|
||||
"types": "./dist/types/exports/social.d.ts",
|
||||
"import": "./dist/esm/exports/social.js",
|
||||
"default": "./dist/cjs/exports/social.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"adapters/*": [
|
||||
"./dist/types/exports/adapters/*.d.ts"
|
||||
],
|
||||
"auth": [
|
||||
"./dist/types/exports/auth.d.ts"
|
||||
],
|
||||
"chains": [
|
||||
"./dist/types/exports/chains.d.ts"
|
||||
],
|
||||
"contract": [
|
||||
"./dist/types/exports/contract.d.ts"
|
||||
],
|
||||
"deploys": [
|
||||
"./dist/types/exports/deploys.d.ts"
|
||||
],
|
||||
"event": [
|
||||
"./dist/types/exports/event.d.ts"
|
||||
],
|
||||
"extensions/*": [
|
||||
"./dist/types/exports/extensions/*.d.ts"
|
||||
],
|
||||
"pay": [
|
||||
"./dist/types/exports/pay.d.ts"
|
||||
],
|
||||
"react": [
|
||||
"./dist/types/exports/react.d.ts"
|
||||
],
|
||||
"react-native": [
|
||||
"./dist/types/exports/react-native.d.ts"
|
||||
],
|
||||
"rpc": [
|
||||
"./dist/types/exports/rpc.d.ts"
|
||||
],
|
||||
"storage": [
|
||||
"./dist/types/exports/storage.d.ts"
|
||||
],
|
||||
"transaction": [
|
||||
"./dist/types/exports/transaction.d.ts"
|
||||
],
|
||||
"utils": [
|
||||
"./dist/types/exports/utils.d.ts"
|
||||
],
|
||||
"wallets": [
|
||||
"./dist/types/exports/wallets.d.ts"
|
||||
],
|
||||
"wallets/*": [
|
||||
"./dist/types/exports/wallets/*.d.ts"
|
||||
],
|
||||
"modules": [
|
||||
"./dist/types/exports/modules.d.ts"
|
||||
],
|
||||
"social": [
|
||||
"./dist/types/exports/social.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"browser": {
|
||||
"crypto": false
|
||||
},
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist/*",
|
||||
"src/*",
|
||||
"!**/*.tsbuildinfo",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.test.tsx",
|
||||
"!**/*.test.ts.snap",
|
||||
"!**/*.test-d.ts",
|
||||
"!**/*.bench.ts",
|
||||
"!tsconfig.build.json"
|
||||
],
|
||||
"dependencies": {
|
||||
"@coinbase/wallet-sdk": "4.2.4",
|
||||
"@emotion/react": "11.13.5",
|
||||
"@emotion/styled": "11.13.5",
|
||||
"@google/model-viewer": "2.1.1",
|
||||
"@noble/curves": "1.7.0",
|
||||
"@noble/hashes": "1.6.1",
|
||||
"@passwordless-id/webauthn": "^2.1.2",
|
||||
"@radix-ui/react-dialog": "1.1.2",
|
||||
"@radix-ui/react-focus-scope": "1.1.0",
|
||||
"@radix-ui/react-icons": "1.3.2",
|
||||
"@radix-ui/react-tooltip": "1.1.4",
|
||||
"@tanstack/react-query": "5.62.2",
|
||||
"@walletconnect/ethereum-provider": "2.17.2",
|
||||
"@walletconnect/sign-client": "2.17.2",
|
||||
"abitype": "1.0.7",
|
||||
"fuse.js": "7.0.0",
|
||||
"input-otp": "^1.4.1",
|
||||
"mipd": "0.0.7",
|
||||
"uqr": "0.1.2",
|
||||
"viem": "2.21.54"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/client-lambda": "^3",
|
||||
"@aws-sdk/credential-providers": "^3",
|
||||
"@coinbase/wallet-mobile-sdk": "^1",
|
||||
"@mobile-wallet-protocol/client": "0.1.1",
|
||||
"@react-native-async-storage/async-storage": "^1 || ^2",
|
||||
"ethers": "^5 || ^6",
|
||||
"expo-linking": "^6",
|
||||
"expo-web-browser": "^13 || ^14",
|
||||
"ox": "0.4.0",
|
||||
"react": "^18 || ^19",
|
||||
"react-native": "*",
|
||||
"react-native-aes-gcm-crypto": "^0.2",
|
||||
"react-native-passkey": "^3",
|
||||
"react-native-quick-crypto": ">=0.7.0-rc.6 || >=0.7",
|
||||
"react-native-svg": "^15",
|
||||
"typescript": ">=5.0.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"ethers": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native-aes-gcm-crypto": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-linking": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-web-browser": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native-quick-crypto": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native-passkey": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native-svg": {
|
||||
"optional": true
|
||||
},
|
||||
"@aws-sdk/client-lambda": {
|
||||
"optional": true
|
||||
},
|
||||
"@aws-sdk/client-kms": {
|
||||
"optional": true
|
||||
},
|
||||
"@aws-sdk/credential-providers": {
|
||||
"optional": true
|
||||
},
|
||||
"@react-native-async-storage/async-storage": {
|
||||
"optional": true
|
||||
},
|
||||
"@coinbase/wallet-mobile-sdk": {
|
||||
"optional": true
|
||||
},
|
||||
"@mobile-wallet-protocol/client": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"bench:compare": "bun run ./benchmarks/run.ts",
|
||||
"bench": "vitest -c ./test/vitest.config.ts bench",
|
||||
"format": "biome format ./src --write",
|
||||
"lint": "knip && biome check ./src && tsc --project ./tsconfig.build.json --module esnext --noEmit",
|
||||
"fix": "biome check ./src --fix",
|
||||
"knip": "knip",
|
||||
"build:generate": "bun scripts/generate/generate.ts",
|
||||
"build:generate-wallets": "bun scripts/wallets/generate.ts",
|
||||
"dev": "tsc --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm --watch",
|
||||
"dev:cjs": "printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json && tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false --watch",
|
||||
"dev:esm": "printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json && tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm --watch",
|
||||
"build": "pnpm clean && pnpm build:types && pnpm build:cjs && pnpm build:esm",
|
||||
"build:cjs": "tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false && printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json",
|
||||
"build:esm": "tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm && printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json",
|
||||
"build:types": "tsc --project ./tsconfig.build.json --module esnext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap",
|
||||
"clean": "rimraf dist",
|
||||
"size": "size-limit",
|
||||
"test:watch": "vitest -c ./test/vitest.config.ts dev",
|
||||
"test": "vitest run -c ./test/vitest.config.ts --coverage",
|
||||
"test:cov": "vitest dev -c ./test/vitest.config.ts --coverage",
|
||||
"test:dev": "vitest run -c ./test/vitest.config.ts",
|
||||
"test:react": "vitest run -c ./test/vitest.config.ts dev --ui src/react",
|
||||
"typedoc": "bun run scripts/typedoc.mjs",
|
||||
"update-version": "node scripts/version.mjs",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-kms": "3.699.0",
|
||||
"@aws-sdk/client-lambda": "3.699.0",
|
||||
"@aws-sdk/credential-providers": "3.699.0",
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@chromatic-com/storybook": "3.2.2",
|
||||
"@codspeed/vitest-plugin": "3.1.1",
|
||||
"@coinbase/wallet-mobile-sdk": "1.1.2",
|
||||
"@mobile-wallet-protocol/client": "0.1.2",
|
||||
"@react-native-async-storage/async-storage": "2.1.0",
|
||||
"@size-limit/preset-big-lib": "11.1.6",
|
||||
"@storybook/addon-essentials": "8.4.7",
|
||||
"@storybook/addon-interactions": "8.4.7",
|
||||
"@storybook/addon-links": "8.4.7",
|
||||
"@storybook/addon-onboarding": "8.4.7",
|
||||
"@storybook/react": "8.4.7",
|
||||
"@storybook/react-vite": "8.4.7",
|
||||
"@storybook/test": "8.4.7",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/cross-spawn": "^6.0.6",
|
||||
"@types/react": "19.0.0",
|
||||
"@viem/anvil": "0.0.10",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "2.1.8",
|
||||
"@vitest/ui": "2.1.8",
|
||||
"cross-spawn": "7.0.6",
|
||||
"dotenv-mono": "^1.3.14",
|
||||
"ethers5": "npm:ethers@5",
|
||||
"ethers6": "npm:ethers@6",
|
||||
"expo-linking": "7.0.3",
|
||||
"expo-web-browser": "14.0.1",
|
||||
"happy-dom": "15.11.7",
|
||||
"knip": "5.39.2",
|
||||
"msw": "2.6.6",
|
||||
"prettier": "3.3.3",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-native": "0.76.3",
|
||||
"react-native-aes-gcm-crypto": "0.2.2",
|
||||
"react-native-passkey": "3.0.0",
|
||||
"react-native-quick-crypto": "0.7.8",
|
||||
"react-native-svg": "15.9.0",
|
||||
"rimraf": "6.0.1",
|
||||
"sharp": "^0.33.5",
|
||||
"size-limit": "11.1.6",
|
||||
"storybook": "8.4.7",
|
||||
"typedoc": "0.27.3",
|
||||
"typescript": "5.7.2",
|
||||
"vite": "6.0.3",
|
||||
"vitest": "2.1.8"
|
||||
}
|
||||
"name": "thirdweb",
|
||||
"version": "5.76.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/thirdweb-dev/js/issues"
|
||||
},
|
||||
"author": "thirdweb eng <[email protected]>",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"thirdweb": "./dist/esm/cli/bin.js",
|
||||
"thirdweb-cli": "./dist/esm/cli/bin.js"
|
||||
},
|
||||
"main": "./dist/cjs/exports/thirdweb.js",
|
||||
"module": "./dist/esm/exports/thirdweb.js",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"import": "./dist/esm/exports/thirdweb.js",
|
||||
"default": "./dist/cjs/exports/thirdweb.js"
|
||||
},
|
||||
"./adapters/*": {
|
||||
"types": "./dist/types/exports/adapters/*.d.ts",
|
||||
"import": "./dist/esm/exports/adapters/*.js",
|
||||
"default": "./dist/cjs/exports/adapters/*.js"
|
||||
},
|
||||
"./auth": {
|
||||
"types": "./dist/types/exports/auth.d.ts",
|
||||
"import": "./dist/esm/exports/auth.js",
|
||||
"default": "./dist/cjs/exports/auth.js"
|
||||
},
|
||||
"./chains": {
|
||||
"types": "./dist/types/exports/chains.d.ts",
|
||||
"import": "./dist/esm/exports/chains.js",
|
||||
"default": "./dist/cjs/exports/chains.js"
|
||||
},
|
||||
"./contract": {
|
||||
"types": "./dist/types/exports/contract.d.ts",
|
||||
"import": "./dist/esm/exports/contract.js",
|
||||
"default": "./dist/cjs/exports/contract.js"
|
||||
},
|
||||
"./deploys": {
|
||||
"types": "./dist/types/exports/deploys.d.ts",
|
||||
"import": "./dist/esm/exports/deploys.js",
|
||||
"default": "./dist/cjs/exports/deploys.js"
|
||||
},
|
||||
"./event": {
|
||||
"types": "./dist/types/exports/event.d.ts",
|
||||
"import": "./dist/esm/exports/event.js",
|
||||
"default": "./dist/cjs/exports/event.js"
|
||||
},
|
||||
"./extensions/*": {
|
||||
"types": "./dist/types/exports/extensions/*.d.ts",
|
||||
"import": "./dist/esm/exports/extensions/*.js",
|
||||
"default": "./dist/cjs/exports/extensions/*.js"
|
||||
},
|
||||
"./pay": {
|
||||
"types": "./dist/types/exports/pay.d.ts",
|
||||
"import": "./dist/esm/exports/pay.js",
|
||||
"default": "./dist/cjs/exports/pay.js"
|
||||
},
|
||||
"./react": {
|
||||
"types": "./dist/types/exports/react.d.ts",
|
||||
"import": "./dist/esm/exports/react.js",
|
||||
"react-native": "./dist/esm/exports/react.native.js",
|
||||
"default": "./dist/cjs/exports/react.js"
|
||||
},
|
||||
"./react-native": {
|
||||
"types": "./dist/types/exports/react-native.d.ts",
|
||||
"import": "./dist/esm/exports/react-native.js",
|
||||
"default": "./dist/cjs/exports/react-native.js"
|
||||
},
|
||||
"./rpc": {
|
||||
"types": "./dist/types/exports/rpc.d.ts",
|
||||
"import": "./dist/esm/exports/rpc.js",
|
||||
"default": "./dist/cjs/exports/rpc.js"
|
||||
},
|
||||
"./storage": {
|
||||
"types": "./dist/types/exports/storage.d.ts",
|
||||
"import": "./dist/esm/exports/storage.js",
|
||||
"default": "./dist/cjs/exports/storage.js"
|
||||
},
|
||||
"./transaction": {
|
||||
"types": "./dist/types/exports/transaction.d.ts",
|
||||
"import": "./dist/esm/exports/transaction.js",
|
||||
"default": "./dist/cjs/exports/transaction.js"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./dist/types/exports/utils.d.ts",
|
||||
"import": "./dist/esm/exports/utils.js",
|
||||
"default": "./dist/cjs/exports/utils.js"
|
||||
},
|
||||
"./wallets": {
|
||||
"types": "./dist/types/exports/wallets.d.ts",
|
||||
"import": "./dist/esm/exports/wallets.js",
|
||||
"react-native": "./dist/esm/exports/wallets.native.js",
|
||||
"default": "./dist/cjs/exports/wallets.js"
|
||||
},
|
||||
"./wallets/in-app": {
|
||||
"types": "./dist/types/exports/wallets/in-app.d.ts",
|
||||
"import": "./dist/esm/exports/wallets/in-app.js",
|
||||
"react-native": "./dist/esm/exports/wallets/in-app.native.js",
|
||||
"default": "./dist/cjs/exports/wallets/in-app.js"
|
||||
},
|
||||
"./wallets/*": {
|
||||
"types": "./dist/types/exports/wallets/*.d.ts",
|
||||
"import": "./dist/esm/exports/wallets/*.js",
|
||||
"default": "./dist/cjs/exports/wallets/*.js"
|
||||
},
|
||||
"./modules": {
|
||||
"types": "./dist/types/exports/modules.d.ts",
|
||||
"import": "./dist/esm/exports/modules.js",
|
||||
"default": "./dist/cjs/exports/modules.js"
|
||||
},
|
||||
"./social": {
|
||||
"types": "./dist/types/exports/social.d.ts",
|
||||
"import": "./dist/esm/exports/social.js",
|
||||
"default": "./dist/cjs/exports/social.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"adapters/*": [
|
||||
"./dist/types/exports/adapters/*.d.ts"
|
||||
],
|
||||
"auth": [
|
||||
"./dist/types/exports/auth.d.ts"
|
||||
],
|
||||
"chains": [
|
||||
"./dist/types/exports/chains.d.ts"
|
||||
],
|
||||
"contract": [
|
||||
"./dist/types/exports/contract.d.ts"
|
||||
],
|
||||
"deploys": [
|
||||
"./dist/types/exports/deploys.d.ts"
|
||||
],
|
||||
"event": [
|
||||
"./dist/types/exports/event.d.ts"
|
||||
],
|
||||
"extensions/*": [
|
||||
"./dist/types/exports/extensions/*.d.ts"
|
||||
],
|
||||
"pay": [
|
||||
"./dist/types/exports/pay.d.ts"
|
||||
],
|
||||
"react": [
|
||||
"./dist/types/exports/react.d.ts"
|
||||
],
|
||||
"react-native": [
|
||||
"./dist/types/exports/react-native.d.ts"
|
||||
],
|
||||
"rpc": [
|
||||
"./dist/types/exports/rpc.d.ts"
|
||||
],
|
||||
"storage": [
|
||||
"./dist/types/exports/storage.d.ts"
|
||||
],
|
||||
"transaction": [
|
||||
"./dist/types/exports/transaction.d.ts"
|
||||
],
|
||||
"utils": [
|
||||
"./dist/types/exports/utils.d.ts"
|
||||
],
|
||||
"wallets": [
|
||||
"./dist/types/exports/wallets.d.ts"
|
||||
],
|
||||
"wallets/*": [
|
||||
"./dist/types/exports/wallets/*.d.ts"
|
||||
],
|
||||
"modules": [
|
||||
"./dist/types/exports/modules.d.ts"
|
||||
],
|
||||
"social": [
|
||||
"./dist/types/exports/social.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"browser": {
|
||||
"crypto": false
|
||||
},
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist/*",
|
||||
"src/*",
|
||||
"!**/*.tsbuildinfo",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.test.tsx",
|
||||
"!**/*.test.ts.snap",
|
||||
"!**/*.test-d.ts",
|
||||
"!**/*.bench.ts",
|
||||
"!tsconfig.build.json"
|
||||
],
|
||||
"dependencies": {
|
||||
"@coinbase/wallet-sdk": "4.2.4",
|
||||
"@emotion/react": "11.13.5",
|
||||
"@emotion/styled": "11.13.5",
|
||||
"@google/model-viewer": "2.1.1",
|
||||
"@noble/curves": "1.7.0",
|
||||
"@noble/hashes": "1.6.1",
|
||||
"@passwordless-id/webauthn": "^2.1.2",
|
||||
"@radix-ui/react-dialog": "1.1.2",
|
||||
"@radix-ui/react-focus-scope": "1.1.0",
|
||||
"@radix-ui/react-icons": "1.3.2",
|
||||
"@radix-ui/react-tooltip": "1.1.4",
|
||||
"@tanstack/react-query": "5.62.3",
|
||||
"@walletconnect/ethereum-provider": "2.17.2",
|
||||
"@walletconnect/sign-client": "2.17.2",
|
||||
"abitype": "1.0.7",
|
||||
"fuse.js": "7.0.0",
|
||||
"input-otp": "^1.4.1",
|
||||
"mipd": "0.0.7",
|
||||
"ox": "0.4.0",
|
||||
"uqr": "0.1.2",
|
||||
"viem": "2.21.54"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/client-lambda": "^3",
|
||||
"@aws-sdk/credential-providers": "^3",
|
||||
"@coinbase/wallet-mobile-sdk": "^1",
|
||||
"@mobile-wallet-protocol/client": "0.1.1",
|
||||
"@react-native-async-storage/async-storage": "^1 || ^2",
|
||||
"ethers": "^5 || ^6",
|
||||
"expo-linking": "^6",
|
||||
"expo-web-browser": "^13 || ^14",
|
||||
"react": "^18 || ^19",
|
||||
"react-native": "*",
|
||||
"react-native-aes-gcm-crypto": "^0.2",
|
||||
"react-native-passkey": "^3",
|
||||
"react-native-quick-crypto": ">=0.7.0-rc.6 || >=0.7",
|
||||
"react-native-svg": "^15",
|
||||
"typescript": ">=5.0.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"ethers": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native-aes-gcm-crypto": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-linking": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-web-browser": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native-quick-crypto": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native-passkey": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native-svg": {
|
||||
"optional": true
|
||||
},
|
||||
"@aws-sdk/client-lambda": {
|
||||
"optional": true
|
||||
},
|
||||
"@aws-sdk/client-kms": {
|
||||
"optional": true
|
||||
},
|
||||
"@aws-sdk/credential-providers": {
|
||||
"optional": true
|
||||
},
|
||||
"@react-native-async-storage/async-storage": {
|
||||
"optional": true
|
||||
},
|
||||
"@coinbase/wallet-mobile-sdk": {
|
||||
"optional": true
|
||||
},
|
||||
"@mobile-wallet-protocol/client": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"bench:compare": "bun run ./benchmarks/run.ts",
|
||||
"bench": "vitest -c ./test/vitest.config.ts bench",
|
||||
"format": "biome format ./src --write",
|
||||
"lint": "knip && biome check ./src && tsc --project ./tsconfig.build.json --module esnext --noEmit",
|
||||
"fix": "biome check ./src --fix",
|
||||
"knip": "knip",
|
||||
"build:generate": "bun scripts/generate/generate.ts",
|
||||
"build:generate-wallets": "bun scripts/wallets/generate.ts",
|
||||
"dev": "tsc --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm --watch",
|
||||
"dev:cjs": "printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json && tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false --watch",
|
||||
"dev:esm": "printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json && tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm --watch",
|
||||
"build": "pnpm clean && pnpm build:types && pnpm build:cjs && pnpm build:esm",
|
||||
"build:cjs": "tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false && printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json",
|
||||
"build:esm": "tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm && printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json",
|
||||
"build:types": "tsc --project ./tsconfig.build.json --module esnext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap",
|
||||
"clean": "rimraf dist",
|
||||
"size": "size-limit",
|
||||
"test:watch": "vitest -c ./test/vitest.config.ts dev",
|
||||
"test": "vitest run -c ./test/vitest.config.ts --coverage",
|
||||
"test:cov": "vitest dev -c ./test/vitest.config.ts --coverage",
|
||||
"test:ui": "vitest dev -c ./test/vitest.config.ts --coverage --ui",
|
||||
"test:dev": "vitest run -c ./test/vitest.config.ts",
|
||||
"test:react": "vitest run -c ./test/vitest.config.ts dev --ui src/react",
|
||||
"typedoc": "bun run scripts/typedoc.mjs",
|
||||
"update-version": "node scripts/version.mjs",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-kms": "3.699.0",
|
||||
"@aws-sdk/client-lambda": "3.699.0",
|
||||
"@aws-sdk/credential-providers": "3.699.0",
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@chromatic-com/storybook": "3.2.2",
|
||||
"@codspeed/vitest-plugin": "3.1.1",
|
||||
"@coinbase/wallet-mobile-sdk": "1.1.2",
|
||||
"@mobile-wallet-protocol/client": "0.1.2",
|
||||
"@react-native-async-storage/async-storage": "2.1.0",
|
||||
"@size-limit/preset-big-lib": "11.1.6",
|
||||
"@storybook/addon-essentials": "8.4.7",
|
||||
"@storybook/addon-interactions": "8.4.7",
|
||||
"@storybook/addon-links": "8.4.7",
|
||||
"@storybook/addon-onboarding": "8.4.7",
|
||||
"@storybook/react": "8.4.7",
|
||||
"@storybook/react-vite": "8.4.7",
|
||||
"@storybook/test": "8.4.7",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/cross-spawn": "^6.0.6",
|
||||
"@types/react": "19.0.1",
|
||||
"@viem/anvil": "0.0.10",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "2.1.8",
|
||||
"@vitest/ui": "2.1.8",
|
||||
"cross-spawn": "7.0.6",
|
||||
"dotenv-mono": "^1.3.14",
|
||||
"ethers5": "npm:ethers@5",
|
||||
"ethers6": "npm:ethers@6",
|
||||
"expo-linking": "7.0.3",
|
||||
"expo-web-browser": "14.0.1",
|
||||
"happy-dom": "15.11.7",
|
||||
"knip": "5.39.2",
|
||||
"msw": "2.6.7",
|
||||
"prettier": "3.3.3",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-native": "0.76.4",
|
||||
"react-native-aes-gcm-crypto": "0.2.2",
|
||||
"react-native-passkey": "3.0.0",
|
||||
"react-native-quick-crypto": "0.7.8",
|
||||
"react-native-svg": "15.10.1",
|
||||
"rimraf": "6.0.1",
|
||||
"sharp": "^0.33.5",
|
||||
"size-limit": "11.1.6",
|
||||
"storybook": "8.4.7",
|
||||
"typedoc": "0.27.3",
|
||||
"typescript": "5.7.2",
|
||||
"vite": "6.0.3",
|
||||
"vitest": "2.1.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
"function mintWithSignature((address to, address royaltyRecipient, uint256 royaltyBps, address primarySaleRecipient, string uri, uint256 quantity, uint256 pricePerToken, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) payload, bytes signature) payable returns (address signer)",
|
||||
"function verify((address to, address royaltyRecipient, uint256 royaltyBps, address primarySaleRecipient, string uri, uint256 quantity, uint256 pricePerToken, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) req, bytes signature) view returns (bool success, address signer)",
|
||||
"event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, (address to, address royaltyRecipient, uint256 royaltyBps, address primarySaleRecipient, string uri, uint256 quantity, uint256 pricePerToken, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) mintRequest)"
|
||||
]
|
||||
@@ -189,6 +189,7 @@ export {
|
||||
export {
|
||||
NFTMedia,
|
||||
type NFTMediaProps,
|
||||
type NFTMediaInfo,
|
||||
} from "../react/web/ui/prebuilt/NFT/media.js";
|
||||
|
||||
export { useConnectionManager } from "../react/core/providers/connection-manager.js";
|
||||
@@ -221,6 +222,7 @@ export {
|
||||
export {
|
||||
AccountBalance,
|
||||
type AccountBalanceProps,
|
||||
type AccountBalanceInfo,
|
||||
} from "../react/web/ui/prebuilt/Account/balance.js";
|
||||
export {
|
||||
AccountName,
|
||||
|
||||
@@ -204,3 +204,6 @@ export type {
|
||||
AbiConstructor,
|
||||
AbiFallback,
|
||||
} from "abitype";
|
||||
|
||||
export { shortenLargeNumber } from "../utils/shortenLargeNumber.js";
|
||||
export { formatNumber } from "../utils/formatNumber.js";
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { prepareEvent } from "../../../../../event/prepare-event.js";
|
||||
import type { AbiParameterToPrimitiveType } from "abitype";
|
||||
|
||||
/**
|
||||
* Represents the filters for the "TokensMintedWithSignature" event.
|
||||
*/
|
||||
export type TokensMintedWithSignatureEventFilters = Partial<{
|
||||
signer: AbiParameterToPrimitiveType<{
|
||||
type: "address";
|
||||
name: "signer";
|
||||
indexed: true;
|
||||
}>;
|
||||
mintedTo: AbiParameterToPrimitiveType<{
|
||||
type: "address";
|
||||
name: "mintedTo";
|
||||
indexed: true;
|
||||
}>;
|
||||
tokenIdMinted: AbiParameterToPrimitiveType<{
|
||||
type: "uint256";
|
||||
name: "tokenIdMinted";
|
||||
indexed: true;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Creates an event object for the TokensMintedWithSignature event.
|
||||
* @param filters - Optional filters to apply to the event.
|
||||
* @returns The prepared event object.
|
||||
* @extension ERC721
|
||||
* @example
|
||||
* ```ts
|
||||
* import { getContractEvents } from "thirdweb";
|
||||
* import { tokensMintedWithSignatureEvent } from "thirdweb/extensions/erc721";
|
||||
*
|
||||
* const events = await getContractEvents({
|
||||
* contract,
|
||||
* events: [
|
||||
* tokensMintedWithSignatureEvent({
|
||||
* signer: ...,
|
||||
* mintedTo: ...,
|
||||
* tokenIdMinted: ...,
|
||||
* })
|
||||
* ],
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function tokensMintedWithSignatureEvent(
|
||||
filters: TokensMintedWithSignatureEventFilters = {},
|
||||
) {
|
||||
return prepareEvent({
|
||||
signature:
|
||||
"event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, (address to, address royaltyRecipient, uint256 royaltyBps, address primarySaleRecipient, string uri, uint256 quantity, uint256 pricePerToken, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) mintRequest)",
|
||||
filters,
|
||||
});
|
||||
}
|
||||
Generated
+196
@@ -0,0 +1,196 @@
|
||||
import type { AbiParameterToPrimitiveType } from "abitype";
|
||||
import { readContract } from "../../../../../transaction/read-contract.js";
|
||||
import type { BaseTransactionOptions } from "../../../../../transaction/types.js";
|
||||
import { encodeAbiParameters } from "../../../../../utils/abi/encodeAbiParameters.js";
|
||||
import { decodeAbiParameters } from "viem";
|
||||
import type { Hex } from "../../../../../utils/encoding/hex.js";
|
||||
import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js";
|
||||
|
||||
/**
|
||||
* Represents the parameters for the "verify" function.
|
||||
*/
|
||||
export type VerifyParams = {
|
||||
req: AbiParameterToPrimitiveType<{
|
||||
type: "tuple";
|
||||
name: "req";
|
||||
components: [
|
||||
{ type: "address"; name: "to" },
|
||||
{ type: "address"; name: "royaltyRecipient" },
|
||||
{ type: "uint256"; name: "royaltyBps" },
|
||||
{ type: "address"; name: "primarySaleRecipient" },
|
||||
{ type: "string"; name: "uri" },
|
||||
{ type: "uint256"; name: "quantity" },
|
||||
{ type: "uint256"; name: "pricePerToken" },
|
||||
{ type: "address"; name: "currency" },
|
||||
{ type: "uint128"; name: "validityStartTimestamp" },
|
||||
{ type: "uint128"; name: "validityEndTimestamp" },
|
||||
{ type: "bytes32"; name: "uid" },
|
||||
];
|
||||
}>;
|
||||
signature: AbiParameterToPrimitiveType<{ type: "bytes"; name: "signature" }>;
|
||||
};
|
||||
|
||||
export const FN_SELECTOR = "0x252e82e8" as const;
|
||||
const FN_INPUTS = [
|
||||
{
|
||||
type: "tuple",
|
||||
name: "req",
|
||||
components: [
|
||||
{
|
||||
type: "address",
|
||||
name: "to",
|
||||
},
|
||||
{
|
||||
type: "address",
|
||||
name: "royaltyRecipient",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "royaltyBps",
|
||||
},
|
||||
{
|
||||
type: "address",
|
||||
name: "primarySaleRecipient",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
name: "uri",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "quantity",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "pricePerToken",
|
||||
},
|
||||
{
|
||||
type: "address",
|
||||
name: "currency",
|
||||
},
|
||||
{
|
||||
type: "uint128",
|
||||
name: "validityStartTimestamp",
|
||||
},
|
||||
{
|
||||
type: "uint128",
|
||||
name: "validityEndTimestamp",
|
||||
},
|
||||
{
|
||||
type: "bytes32",
|
||||
name: "uid",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "bytes",
|
||||
name: "signature",
|
||||
},
|
||||
] as const;
|
||||
const FN_OUTPUTS = [
|
||||
{
|
||||
type: "bool",
|
||||
name: "success",
|
||||
},
|
||||
{
|
||||
type: "address",
|
||||
name: "signer",
|
||||
},
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Checks if the `verify` method is supported by the given contract.
|
||||
* @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors.
|
||||
* @returns A boolean indicating if the `verify` method is supported.
|
||||
* @extension ERC721
|
||||
* @example
|
||||
* ```ts
|
||||
* import { isVerifySupported } from "thirdweb/extensions/erc721";
|
||||
* const supported = isVerifySupported(["0x..."]);
|
||||
* ```
|
||||
*/
|
||||
export function isVerifySupported(availableSelectors: string[]) {
|
||||
return detectMethod({
|
||||
availableSelectors,
|
||||
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the parameters for the "verify" function.
|
||||
* @param options - The options for the verify function.
|
||||
* @returns The encoded ABI parameters.
|
||||
* @extension ERC721
|
||||
* @example
|
||||
* ```ts
|
||||
* import { encodeVerifyParams } from "thirdweb/extensions/erc721";
|
||||
* const result = encodeVerifyParams({
|
||||
* req: ...,
|
||||
* signature: ...,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function encodeVerifyParams(options: VerifyParams) {
|
||||
return encodeAbiParameters(FN_INPUTS, [options.req, options.signature]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the "verify" function into a Hex string with its parameters.
|
||||
* @param options - The options for the verify function.
|
||||
* @returns The encoded hexadecimal string.
|
||||
* @extension ERC721
|
||||
* @example
|
||||
* ```ts
|
||||
* import { encodeVerify } from "thirdweb/extensions/erc721";
|
||||
* const result = encodeVerify({
|
||||
* req: ...,
|
||||
* signature: ...,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function encodeVerify(options: VerifyParams) {
|
||||
// we do a "manual" concat here to avoid the overhead of the "concatHex" function
|
||||
// we can do this because we know the specific formats of the values
|
||||
return (FN_SELECTOR +
|
||||
encodeVerifyParams(options).slice(2)) as `${typeof FN_SELECTOR}${string}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the result of the verify function call.
|
||||
* @param result - The hexadecimal result to decode.
|
||||
* @returns The decoded result as per the FN_OUTPUTS definition.
|
||||
* @extension ERC721
|
||||
* @example
|
||||
* ```ts
|
||||
* import { decodeVerifyResult } from "thirdweb/extensions/erc721";
|
||||
* const result = decodeVerifyResultResult("...");
|
||||
* ```
|
||||
*/
|
||||
export function decodeVerifyResult(result: Hex) {
|
||||
return decodeAbiParameters(FN_OUTPUTS, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the "verify" function on the contract.
|
||||
* @param options - The options for the verify function.
|
||||
* @returns The parsed result of the function call.
|
||||
* @extension ERC721
|
||||
* @example
|
||||
* ```ts
|
||||
* import { verify } from "thirdweb/extensions/erc721";
|
||||
*
|
||||
* const result = await verify({
|
||||
* contract,
|
||||
* req: ...,
|
||||
* signature: ...,
|
||||
* });
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
export async function verify(options: BaseTransactionOptions<VerifyParams>) {
|
||||
return readContract({
|
||||
contract: options.contract,
|
||||
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
|
||||
params: [options.req, options.signature],
|
||||
});
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
import type { AbiParameterToPrimitiveType } from "abitype";
|
||||
import type {
|
||||
BaseTransactionOptions,
|
||||
WithOverrides,
|
||||
} from "../../../../../transaction/types.js";
|
||||
import { prepareContractCall } from "../../../../../transaction/prepare-contract-call.js";
|
||||
import { encodeAbiParameters } from "../../../../../utils/abi/encodeAbiParameters.js";
|
||||
import { once } from "../../../../../utils/promise/once.js";
|
||||
import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js";
|
||||
|
||||
/**
|
||||
* Represents the parameters for the "mintWithSignature" function.
|
||||
*/
|
||||
export type MintWithSignatureParams = WithOverrides<{
|
||||
payload: AbiParameterToPrimitiveType<{
|
||||
type: "tuple";
|
||||
name: "payload";
|
||||
components: [
|
||||
{ type: "address"; name: "to" },
|
||||
{ type: "address"; name: "royaltyRecipient" },
|
||||
{ type: "uint256"; name: "royaltyBps" },
|
||||
{ type: "address"; name: "primarySaleRecipient" },
|
||||
{ type: "string"; name: "uri" },
|
||||
{ type: "uint256"; name: "quantity" },
|
||||
{ type: "uint256"; name: "pricePerToken" },
|
||||
{ type: "address"; name: "currency" },
|
||||
{ type: "uint128"; name: "validityStartTimestamp" },
|
||||
{ type: "uint128"; name: "validityEndTimestamp" },
|
||||
{ type: "bytes32"; name: "uid" },
|
||||
];
|
||||
}>;
|
||||
signature: AbiParameterToPrimitiveType<{ type: "bytes"; name: "signature" }>;
|
||||
}>;
|
||||
|
||||
export const FN_SELECTOR = "0x439c7be5" as const;
|
||||
const FN_INPUTS = [
|
||||
{
|
||||
type: "tuple",
|
||||
name: "payload",
|
||||
components: [
|
||||
{
|
||||
type: "address",
|
||||
name: "to",
|
||||
},
|
||||
{
|
||||
type: "address",
|
||||
name: "royaltyRecipient",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "royaltyBps",
|
||||
},
|
||||
{
|
||||
type: "address",
|
||||
name: "primarySaleRecipient",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
name: "uri",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "quantity",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "pricePerToken",
|
||||
},
|
||||
{
|
||||
type: "address",
|
||||
name: "currency",
|
||||
},
|
||||
{
|
||||
type: "uint128",
|
||||
name: "validityStartTimestamp",
|
||||
},
|
||||
{
|
||||
type: "uint128",
|
||||
name: "validityEndTimestamp",
|
||||
},
|
||||
{
|
||||
type: "bytes32",
|
||||
name: "uid",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "bytes",
|
||||
name: "signature",
|
||||
},
|
||||
] as const;
|
||||
const FN_OUTPUTS = [
|
||||
{
|
||||
type: "address",
|
||||
name: "signer",
|
||||
},
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Checks if the `mintWithSignature` method is supported by the given contract.
|
||||
* @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors.
|
||||
* @returns A boolean indicating if the `mintWithSignature` method is supported.
|
||||
* @extension ERC721
|
||||
* @example
|
||||
* ```ts
|
||||
* import { isMintWithSignatureSupported } from "thirdweb/extensions/erc721";
|
||||
*
|
||||
* const supported = isMintWithSignatureSupported(["0x..."]);
|
||||
* ```
|
||||
*/
|
||||
export function isMintWithSignatureSupported(availableSelectors: string[]) {
|
||||
return detectMethod({
|
||||
availableSelectors,
|
||||
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the parameters for the "mintWithSignature" function.
|
||||
* @param options - The options for the mintWithSignature function.
|
||||
* @returns The encoded ABI parameters.
|
||||
* @extension ERC721
|
||||
* @example
|
||||
* ```ts
|
||||
* import { encodeMintWithSignatureParams } from "thirdweb/extensions/erc721";
|
||||
* const result = encodeMintWithSignatureParams({
|
||||
* payload: ...,
|
||||
* signature: ...,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function encodeMintWithSignatureParams(
|
||||
options: MintWithSignatureParams,
|
||||
) {
|
||||
return encodeAbiParameters(FN_INPUTS, [options.payload, options.signature]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the "mintWithSignature" function into a Hex string with its parameters.
|
||||
* @param options - The options for the mintWithSignature function.
|
||||
* @returns The encoded hexadecimal string.
|
||||
* @extension ERC721
|
||||
* @example
|
||||
* ```ts
|
||||
* import { encodeMintWithSignature } from "thirdweb/extensions/erc721";
|
||||
* const result = encodeMintWithSignature({
|
||||
* payload: ...,
|
||||
* signature: ...,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function encodeMintWithSignature(options: MintWithSignatureParams) {
|
||||
// we do a "manual" concat here to avoid the overhead of the "concatHex" function
|
||||
// we can do this because we know the specific formats of the values
|
||||
return (FN_SELECTOR +
|
||||
encodeMintWithSignatureParams(options).slice(
|
||||
2,
|
||||
)) as `${typeof FN_SELECTOR}${string}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a transaction to call the "mintWithSignature" function on the contract.
|
||||
* @param options - The options for the "mintWithSignature" function.
|
||||
* @returns A prepared transaction object.
|
||||
* @extension ERC721
|
||||
* @example
|
||||
* ```ts
|
||||
* import { sendTransaction } from "thirdweb";
|
||||
* import { mintWithSignature } from "thirdweb/extensions/erc721";
|
||||
*
|
||||
* const transaction = mintWithSignature({
|
||||
* contract,
|
||||
* payload: ...,
|
||||
* signature: ...,
|
||||
* overrides: {
|
||||
* ...
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // Send the transaction
|
||||
* await sendTransaction({ transaction, account });
|
||||
* ```
|
||||
*/
|
||||
export function mintWithSignature(
|
||||
options: BaseTransactionOptions<
|
||||
| MintWithSignatureParams
|
||||
| {
|
||||
asyncParams: () => Promise<MintWithSignatureParams>;
|
||||
}
|
||||
>,
|
||||
) {
|
||||
const asyncOptions = once(async () => {
|
||||
return "asyncParams" in options ? await options.asyncParams() : options;
|
||||
});
|
||||
|
||||
return prepareContractCall({
|
||||
contract: options.contract,
|
||||
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
|
||||
params: async () => {
|
||||
const resolvedOptions = await asyncOptions();
|
||||
return [resolvedOptions.payload, resolvedOptions.signature] as const;
|
||||
},
|
||||
value: async () => (await asyncOptions()).overrides?.value,
|
||||
accessList: async () => (await asyncOptions()).overrides?.accessList,
|
||||
gas: async () => (await asyncOptions()).overrides?.gas,
|
||||
gasPrice: async () => (await asyncOptions()).overrides?.gasPrice,
|
||||
maxFeePerGas: async () => (await asyncOptions()).overrides?.maxFeePerGas,
|
||||
maxPriorityFeePerGas: async () =>
|
||||
(await asyncOptions()).overrides?.maxPriorityFeePerGas,
|
||||
nonce: async () => (await asyncOptions()).overrides?.nonce,
|
||||
extraGas: async () => (await asyncOptions()).overrides?.extraGas,
|
||||
erc20Value: async () => (await asyncOptions()).overrides?.erc20Value,
|
||||
});
|
||||
}
|
||||
@@ -15,6 +15,10 @@ import {
|
||||
type MintWithSignatureParams,
|
||||
mintWithSignature as generatedMintWithSignature,
|
||||
} from "../__generated__/ISignatureMintERC721/write/mintWithSignature.js";
|
||||
import {
|
||||
type MintWithSignatureParams as MintWithSignatureParamsV2,
|
||||
mintWithSignature as generatedMintWithSignatureV2,
|
||||
} from "../__generated__/ISignatureMintERC721_v2/write/mintWithSignature.js";
|
||||
|
||||
/**
|
||||
* Mints a new ERC721 token with the given minter signature
|
||||
@@ -38,6 +42,23 @@ import {
|
||||
* @returns A promise that resolves to the transaction result.
|
||||
*/
|
||||
export function mintWithSignature(
|
||||
options: BaseTransactionOptions<
|
||||
| { payload: PayloadTypeV2; signature: Hex }
|
||||
| { payload: PayloadType; signature: Hex }
|
||||
>,
|
||||
) {
|
||||
const { payload } = options;
|
||||
if ("quantity" in payload) {
|
||||
return mintWithSignatureV2(
|
||||
options as BaseTransactionOptions<MintWithSignatureParamsV2>,
|
||||
);
|
||||
}
|
||||
return mintWithSignatureV1(
|
||||
options as BaseTransactionOptions<MintWithSignatureParams>,
|
||||
);
|
||||
}
|
||||
|
||||
function mintWithSignatureV1(
|
||||
options: BaseTransactionOptions<MintWithSignatureParams>,
|
||||
) {
|
||||
const value = isNativeTokenAddress(options.payload.currency)
|
||||
@@ -51,10 +72,27 @@ export function mintWithSignature(
|
||||
});
|
||||
}
|
||||
|
||||
export type GenerateMintSignatureOptions = {
|
||||
function mintWithSignatureV2(
|
||||
options: BaseTransactionOptions<MintWithSignatureParamsV2>,
|
||||
) {
|
||||
const value = isNativeTokenAddress(options.payload.currency)
|
||||
? options.payload.pricePerToken * options.payload.quantity
|
||||
: 0n;
|
||||
return generatedMintWithSignatureV2({
|
||||
...options,
|
||||
overrides: {
|
||||
value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export type GenerateMintSignatureOptions<
|
||||
T extends "LoyaltyCard" | "TokenERC721" = "TokenERC721",
|
||||
> = {
|
||||
account: Account;
|
||||
contract: ThirdwebContract;
|
||||
mintRequest: GeneratePayloadInput;
|
||||
contractType?: T;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -87,10 +125,10 @@ export type GenerateMintSignatureOptions = {
|
||||
* @extension ERC721
|
||||
* @returns A promise that resolves to the payload and signature.
|
||||
*/
|
||||
export async function generateMintSignature(
|
||||
options: GenerateMintSignatureOptions,
|
||||
) {
|
||||
const { mintRequest, account, contract } = options;
|
||||
export async function generateMintSignature<
|
||||
T extends "LoyaltyCard" | "TokenERC721" = "TokenERC721",
|
||||
>(options: GenerateMintSignatureOptions<T>): Promise<SignPayloadResult<T>> {
|
||||
const { mintRequest, account, contract, contractType } = options;
|
||||
|
||||
const currency = mintRequest.currency || NATIVE_TOKEN_ADDRESS;
|
||||
const [price, uri, uid] = await Promise.all([
|
||||
@@ -175,15 +213,70 @@ export async function generateMintSignature(
|
||||
royaltyRecipient = mintRequest.royaltyRecipient;
|
||||
}
|
||||
|
||||
if (contractType === "LoyaltyCard") {
|
||||
return signPayloadV2({
|
||||
mintRequest,
|
||||
account,
|
||||
contract,
|
||||
uri,
|
||||
currency,
|
||||
uid,
|
||||
price,
|
||||
royaltyRecipient,
|
||||
primarySaleRecipient: saleRecipient,
|
||||
startTime,
|
||||
endTime,
|
||||
}) as Promise<SignPayloadResult<T>>;
|
||||
}
|
||||
return signPayloadV1({
|
||||
mintRequest,
|
||||
account,
|
||||
contract,
|
||||
uri,
|
||||
currency,
|
||||
uid,
|
||||
price,
|
||||
royaltyRecipient,
|
||||
primarySaleRecipient: saleRecipient,
|
||||
startTime,
|
||||
endTime,
|
||||
}) as Promise<SignPayloadResult<T>>;
|
||||
}
|
||||
|
||||
async function signPayloadV1({
|
||||
mintRequest,
|
||||
account,
|
||||
contract,
|
||||
uri,
|
||||
currency,
|
||||
uid,
|
||||
price,
|
||||
royaltyRecipient,
|
||||
primarySaleRecipient,
|
||||
startTime,
|
||||
endTime,
|
||||
}: {
|
||||
mintRequest: GeneratePayloadInput;
|
||||
account: Account;
|
||||
contract: ThirdwebContract;
|
||||
uri: string;
|
||||
currency: Address;
|
||||
uid: Hex;
|
||||
price: bigint;
|
||||
royaltyRecipient: Address;
|
||||
primarySaleRecipient: Address;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
}): Promise<{ payload: PayloadType; signature: Hex }> {
|
||||
const payload: PayloadType = {
|
||||
uri,
|
||||
currency,
|
||||
uid,
|
||||
price,
|
||||
to: mintRequest.to,
|
||||
royaltyRecipient: royaltyRecipient,
|
||||
royaltyRecipient,
|
||||
royaltyBps: toBigInt(mintRequest.royaltyBps || 0),
|
||||
primarySaleRecipient: saleRecipient,
|
||||
primarySaleRecipient,
|
||||
validityStartTimestamp: dateToSeconds(startTime),
|
||||
validityEndTimestamp: dateToSeconds(endTime),
|
||||
};
|
||||
@@ -195,19 +288,86 @@ export async function generateMintSignature(
|
||||
chainId: contract.chain.id,
|
||||
verifyingContract: contract.address,
|
||||
},
|
||||
types: { MintRequest: MintRequest721 },
|
||||
types: {
|
||||
MintRequest: MintRequest721,
|
||||
},
|
||||
primaryType: "MintRequest",
|
||||
message: payload,
|
||||
});
|
||||
return { payload, signature };
|
||||
}
|
||||
|
||||
async function signPayloadV2({
|
||||
mintRequest,
|
||||
account,
|
||||
contract,
|
||||
uri,
|
||||
currency,
|
||||
uid,
|
||||
price,
|
||||
royaltyRecipient,
|
||||
primarySaleRecipient,
|
||||
startTime,
|
||||
endTime,
|
||||
}: {
|
||||
mintRequest: GeneratePayloadInput;
|
||||
account: Account;
|
||||
contract: ThirdwebContract;
|
||||
uri: string;
|
||||
currency: Address;
|
||||
uid: Hex;
|
||||
price: bigint;
|
||||
royaltyRecipient: Address;
|
||||
primarySaleRecipient: Address;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
}): Promise<{ payload: PayloadTypeV2; signature: Hex }> {
|
||||
const payload: PayloadTypeV2 = {
|
||||
uri,
|
||||
currency,
|
||||
uid,
|
||||
quantity: toBigInt(1), // always 1 for 721 NFTs
|
||||
pricePerToken: price,
|
||||
to: mintRequest.to,
|
||||
royaltyRecipient,
|
||||
royaltyBps: toBigInt(mintRequest.royaltyBps || 0),
|
||||
primarySaleRecipient,
|
||||
validityStartTimestamp: dateToSeconds(startTime),
|
||||
validityEndTimestamp: dateToSeconds(endTime),
|
||||
};
|
||||
|
||||
const signature = await account.signTypedData({
|
||||
domain: {
|
||||
name: "SignatureMintERC721",
|
||||
version: "1",
|
||||
chainId: contract.chain.id,
|
||||
verifyingContract: contract.address,
|
||||
},
|
||||
types: {
|
||||
MintRequest: MintRequest721_V2,
|
||||
},
|
||||
primaryType: "MintRequest",
|
||||
message: payload,
|
||||
});
|
||||
return { payload, signature };
|
||||
}
|
||||
|
||||
type SignPayloadResult<T> = T extends "LoyaltyCard"
|
||||
? Awaited<ReturnType<typeof signPayloadV2>>
|
||||
: Awaited<ReturnType<typeof signPayloadV1>>;
|
||||
|
||||
type PayloadType = AbiParameterToPrimitiveType<{
|
||||
type: "tuple";
|
||||
name: "payload";
|
||||
components: typeof MintRequest721;
|
||||
}>;
|
||||
|
||||
type PayloadTypeV2 = AbiParameterToPrimitiveType<{
|
||||
type: "tuple";
|
||||
name: "payload";
|
||||
components: typeof MintRequest721_V2;
|
||||
}>;
|
||||
|
||||
type GeneratePayloadInput = {
|
||||
to: string;
|
||||
metadata: NFTInput | string;
|
||||
@@ -234,3 +394,19 @@ const MintRequest721 = [
|
||||
{ name: "validityEndTimestamp", type: "uint128" },
|
||||
{ name: "uid", type: "bytes32" },
|
||||
] as const;
|
||||
|
||||
// used for LoyaltyCard contract and base sigmint contracts
|
||||
// adds quantity to the payload so its the same as 1155
|
||||
const MintRequest721_V2 = [
|
||||
{ name: "to", type: "address" },
|
||||
{ name: "royaltyRecipient", type: "address" },
|
||||
{ name: "royaltyBps", type: "uint256" },
|
||||
{ name: "primarySaleRecipient", type: "address" },
|
||||
{ name: "uri", type: "string" },
|
||||
{ name: "quantity", type: "uint256" },
|
||||
{ name: "pricePerToken", type: "uint256" },
|
||||
{ name: "currency", type: "address" },
|
||||
{ name: "validityStartTimestamp", type: "uint128" },
|
||||
{ name: "validityEndTimestamp", type: "uint128" },
|
||||
{ name: "uid", type: "bytes32" },
|
||||
] as const;
|
||||
|
||||
@@ -60,6 +60,48 @@ describe.runIf(process.env.TW_SECRET_KEY)(
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
it("should generate a mint signature and mint an NFT for LoyaltyContract", async () => {
|
||||
const loyaltyContract = getContract({
|
||||
address: await deployERC721Contract({
|
||||
account: TEST_ACCOUNT_A,
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
params: {
|
||||
name: "Test",
|
||||
symbol: "TST",
|
||||
royaltyRecipient: TEST_ACCOUNT_C.address,
|
||||
saleRecipient: TEST_ACCOUNT_B.address,
|
||||
},
|
||||
type: "LoyaltyCard",
|
||||
}),
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
const { payload, signature } = await generateMintSignature({
|
||||
mintRequest: {
|
||||
to: TEST_ACCOUNT_B.address,
|
||||
metadata: {
|
||||
name: "My NFT",
|
||||
description: "This is my NFT",
|
||||
image: "https://example.com/image.png",
|
||||
},
|
||||
},
|
||||
account: TEST_ACCOUNT_A,
|
||||
contract: loyaltyContract,
|
||||
contractType: "LoyaltyCard",
|
||||
});
|
||||
const transaction = mintWithSignature({
|
||||
contract: loyaltyContract,
|
||||
payload,
|
||||
signature,
|
||||
});
|
||||
const { transactionHash } = await sendTransaction({
|
||||
transaction,
|
||||
account: TEST_ACCOUNT_A,
|
||||
});
|
||||
expect(transactionHash.length).toBe(66);
|
||||
});
|
||||
|
||||
it("should generate a mint signature and mint an NFT", async () => {
|
||||
const { payload, signature } = await generateMintSignature({
|
||||
mintRequest: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { TEST_CLIENT } from "~test/test-clients.js";
|
||||
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
|
||||
import { base } from "../../chains/chain-definitions/base.js";
|
||||
@@ -92,4 +92,24 @@ describe.runIf(process.env.TW_SECRET_KEY)("Pay: crypto-to-fiat", () => {
|
||||
`Error: ${TEST_ACCOUNT_A.address} on chainId: ${base.id} is not a valid contract address.`,
|
||||
);
|
||||
});
|
||||
it("should throw if response is not OK", async () => {
|
||||
global.fetch = vi.fn();
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 400,
|
||||
statusText: "Bad Request",
|
||||
});
|
||||
await expect(() =>
|
||||
convertCryptoToFiat({
|
||||
chain: base,
|
||||
fromTokenAddress: NATIVE_TOKEN_ADDRESS,
|
||||
fromAmount: 1,
|
||||
to: "USD",
|
||||
client: TEST_CLIENT,
|
||||
}),
|
||||
).rejects.toThrowError(
|
||||
`Failed to fetch USD value for token (${NATIVE_TOKEN_ADDRESS}) on chainId: ${base.id}`,
|
||||
);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getContract } from "../../contract/contract.js";
|
||||
import { isAddress } from "../../utils/address.js";
|
||||
import { getClientFetch } from "../../utils/fetch.js";
|
||||
import { getPayConvertCryptoToFiatEndpoint } from "../utils/definitions.js";
|
||||
import type { SupportedFiatCurrency } from "./type.js";
|
||||
|
||||
/**
|
||||
* Props for the `convertCryptoToFiat` function
|
||||
@@ -31,7 +32,7 @@ export type ConvertCryptoToFiatParams = {
|
||||
* The fiat symbol. e.g "USD"
|
||||
* Only USD is supported at the moment.
|
||||
*/
|
||||
to: "USD";
|
||||
to: SupportedFiatCurrency;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { TEST_CLIENT } from "~test/test-clients.js";
|
||||
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
|
||||
import { base } from "../../chains/chain-definitions/base.js";
|
||||
@@ -93,4 +93,24 @@ describe.runIf(process.env.TW_SECRET_KEY)("Pay: fiatToCrypto", () => {
|
||||
`Error: ${TEST_ACCOUNT_A.address} on chainId: ${base.id} is not a valid contract address.`,
|
||||
);
|
||||
});
|
||||
it("should throw if response is not OK", async () => {
|
||||
global.fetch = vi.fn();
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 400,
|
||||
statusText: "Bad Request",
|
||||
});
|
||||
await expect(() =>
|
||||
convertFiatToCrypto({
|
||||
chain: ethereum,
|
||||
to: NATIVE_TOKEN_ADDRESS,
|
||||
fromAmount: 1,
|
||||
from: "USD",
|
||||
client: TEST_CLIENT,
|
||||
}),
|
||||
).rejects.toThrowError(
|
||||
`Failed to convert USD value to token (${NATIVE_TOKEN_ADDRESS}) on chainId: 1`,
|
||||
);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getContract } from "../../contract/contract.js";
|
||||
import { isAddress } from "../../utils/address.js";
|
||||
import { getClientFetch } from "../../utils/fetch.js";
|
||||
import { getPayConvertFiatToCryptoEndpoint } from "../utils/definitions.js";
|
||||
import type { SupportedFiatCurrency } from "./type.js";
|
||||
|
||||
/**
|
||||
* Props for the `convertFiatToCrypto` function
|
||||
@@ -18,7 +19,7 @@ export type ConvertFiatToCryptoParams = {
|
||||
* The fiat symbol. e.g: "USD"
|
||||
* Currently only USD is supported.
|
||||
*/
|
||||
from: "USD";
|
||||
from: SupportedFiatCurrency;
|
||||
/**
|
||||
* The total amount of fiat to convert
|
||||
* e.g: If you want to convert 2 cents to USD, enter `0.02`
|
||||
@@ -101,7 +102,7 @@ export async function convertFiatToCrypto(
|
||||
const response = await getClientFetch(client)(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to convert ${to} value to token (${to}) on chainId: ${chain.id}`,
|
||||
`Failed to convert ${from} value to token (${to}) on chainId: ${chain.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const SUPPORTED_FIAT_CURRENCIES = ["USD"] as const;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type SupportedFiatCurrency = (typeof SUPPORTED_FIAT_CURRENCIES)[number];
|
||||
@@ -3,6 +3,7 @@ import type { Chain } from "../../../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../../../client/client.js";
|
||||
import type { BuyWithCryptoStatus } from "../../../../pay/buyWithCrypto/getStatus.js";
|
||||
import type { BuyWithFiatStatus } from "../../../../pay/buyWithFiat/getStatus.js";
|
||||
import type { SupportedFiatCurrency } from "../../../../pay/convert/type.js";
|
||||
import type { FiatProvider } from "../../../../pay/utils/commonTypes.js";
|
||||
import type { AssetTabs } from "../../../../react/web/ui/ConnectWallet/screens/ViewAssets.js";
|
||||
import type { PreparedTransaction } from "../../../../transaction/prepare-transaction.js";
|
||||
@@ -320,6 +321,12 @@ export type ConnectButton_detailsModalOptions = {
|
||||
* Note: If an empty array is passed, the [View Funds] button will be hidden
|
||||
*/
|
||||
assetTabs?: AssetTabs[];
|
||||
|
||||
/**
|
||||
* Show the token balance's value in fiat.
|
||||
* Note: Not all tokens are resolvable to a fiat value. In that case, nothing will be shown.
|
||||
*/
|
||||
showBalanceInFiat?: SupportedFiatCurrency;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -377,6 +384,12 @@ export type ConnectButton_detailsButtonOptions = {
|
||||
* Use custom avatar URL for the connected wallet image in the `ConnectButton` details button, overriding ENS avatar or Blobbie icon.
|
||||
*/
|
||||
connectedAccountAvatarUrl?: string;
|
||||
|
||||
/**
|
||||
* Show the token balance's value in fiat.
|
||||
* Note: Not all tokens are resolvable to a fiat value. In that case, nothing will be shown.
|
||||
*/
|
||||
showBalanceInFiat?: SupportedFiatCurrency;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -50,4 +50,19 @@ describe("ConnectButton", () => {
|
||||
);
|
||||
expect(screen.getByText("Sign In")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render with fiat balance props", () => {
|
||||
render(
|
||||
<ConnectButton
|
||||
client={TEST_CLIENT}
|
||||
detailsButton={{
|
||||
showBalanceInFiat: "USD",
|
||||
}}
|
||||
detailsModal={{
|
||||
showBalanceInFiat: "USD",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Connect Wallet")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,587 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { FC } from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { VITALIK_WALLET } from "~test/addresses.js";
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
renderHook,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "~test/react-render.js";
|
||||
import { TEST_CLIENT } from "~test/test-clients.js";
|
||||
import { base } from "../../../../chains/chain-definitions/base.js";
|
||||
import { ethereum } from "../../../../chains/chain-definitions/ethereum.js";
|
||||
import { useActiveAccount } from "../../../../react/core/hooks/wallets/useActiveAccount.js";
|
||||
import { useActiveWalletChain } from "../../../../react/core/hooks/wallets/useActiveWalletChain.js";
|
||||
import { ThirdwebProvider } from "../../providers/thirdweb-provider.js";
|
||||
import { AccountProvider } from "../prebuilt/Account/provider.js";
|
||||
import {
|
||||
ConnectedToSmartWallet,
|
||||
ConnectedWalletDetails,
|
||||
DetailsModal,
|
||||
InAppWalletUserInfo,
|
||||
SwitchNetworkButton,
|
||||
detailsBtn_formatFiatBalanceForButton,
|
||||
detailsBtn_formatTokenBalanceForButton,
|
||||
useWalletDetailsModal,
|
||||
} from "./Details.js";
|
||||
import en from "./locale/en.js";
|
||||
import { getConnectLocale } from "./locale/getConnectLocale.js";
|
||||
|
||||
/**
|
||||
* Tests for the Details button and Details Modal (parts of the ConnectButton component)
|
||||
*/
|
||||
const queryClient = new QueryClient();
|
||||
const client = TEST_CLIENT;
|
||||
vi.mock("../../../core/hooks/wallets/useActiveAccount.js", () => ({
|
||||
useActiveAccount: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockDetailsModalOptions = {};
|
||||
const mockSupportedTokens = {};
|
||||
const mockSupportedNFTs = {};
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mock
|
||||
const mockChains: any[] = [];
|
||||
const mockDisplayBalanceToken = {};
|
||||
const mockConnectOptions = {};
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mock
|
||||
const mockAssetTabs: any[] = [];
|
||||
const mockOnDisconnect = vi.fn();
|
||||
|
||||
describe("Details button", () => {
|
||||
it("should render (when a wallet is connected)", () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AccountProvider address={VITALIK_WALLET} client={client}>
|
||||
<ConnectedWalletDetails
|
||||
theme={"dark"}
|
||||
detailsButton={undefined}
|
||||
detailsModal={undefined}
|
||||
supportedTokens={undefined}
|
||||
supportedNFTs={undefined}
|
||||
onDisconnect={() => {}}
|
||||
chains={[]}
|
||||
switchButton={undefined}
|
||||
client={client}
|
||||
connectLocale={en}
|
||||
connectOptions={undefined}
|
||||
/>
|
||||
</AccountProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
const elements = container.getElementsByClassName("tw-connected-wallet");
|
||||
expect(!!elements.length).toBe(true);
|
||||
});
|
||||
|
||||
it("should render with showBalanceInFiat", () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AccountProvider address={VITALIK_WALLET} client={client}>
|
||||
<ConnectedWalletDetails
|
||||
theme={"dark"}
|
||||
detailsButton={{
|
||||
showBalanceInFiat: "USD",
|
||||
}}
|
||||
detailsModal={undefined}
|
||||
supportedTokens={undefined}
|
||||
supportedNFTs={undefined}
|
||||
onDisconnect={() => {}}
|
||||
chains={[]}
|
||||
switchButton={undefined}
|
||||
client={client}
|
||||
connectLocale={en}
|
||||
connectOptions={undefined}
|
||||
/>
|
||||
</AccountProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
const elements = container.getElementsByClassName("tw-connected-wallet");
|
||||
expect(!!elements.length).toBe(true);
|
||||
});
|
||||
|
||||
it("should render with custom UI from the `render` prop", () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AccountProvider address={VITALIK_WALLET} client={client}>
|
||||
<ConnectedWalletDetails
|
||||
theme={"dark"}
|
||||
detailsButton={{
|
||||
render: () => <p className="thirdweb_tw" />,
|
||||
}}
|
||||
detailsModal={undefined}
|
||||
supportedTokens={undefined}
|
||||
supportedNFTs={undefined}
|
||||
onDisconnect={() => {}}
|
||||
chains={[]}
|
||||
switchButton={undefined}
|
||||
client={client}
|
||||
connectLocale={en}
|
||||
connectOptions={undefined}
|
||||
/>
|
||||
</AccountProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
const element = container.querySelector("p.thirdweb_tw");
|
||||
expect(element).not.toBe(null);
|
||||
});
|
||||
|
||||
it("should render style properly", () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AccountProvider address={VITALIK_WALLET} client={client}>
|
||||
<ConnectedWalletDetails
|
||||
theme={"dark"}
|
||||
detailsButton={{
|
||||
style: {
|
||||
color: "red",
|
||||
width: "4444px",
|
||||
},
|
||||
}}
|
||||
detailsModal={undefined}
|
||||
supportedTokens={undefined}
|
||||
supportedNFTs={undefined}
|
||||
onDisconnect={() => {}}
|
||||
chains={[]}
|
||||
switchButton={undefined}
|
||||
client={client}
|
||||
connectLocale={en}
|
||||
connectOptions={undefined}
|
||||
/>
|
||||
</AccountProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
const element = container.querySelector(
|
||||
'[data-test="connected-wallet-details"]',
|
||||
);
|
||||
if (!element) {
|
||||
throw new Error("Details button not rendered properly");
|
||||
}
|
||||
const styles = window.getComputedStyle(element);
|
||||
expect(styles.color).toBe("red");
|
||||
expect(styles.width).toBe("4444px");
|
||||
});
|
||||
|
||||
it("should render the Balance section", () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AccountProvider address={VITALIK_WALLET} client={client}>
|
||||
<ConnectedWalletDetails
|
||||
theme={"dark"}
|
||||
detailsButton={undefined}
|
||||
detailsModal={undefined}
|
||||
supportedTokens={undefined}
|
||||
supportedNFTs={undefined}
|
||||
onDisconnect={() => {}}
|
||||
chains={[]}
|
||||
switchButton={undefined}
|
||||
client={client}
|
||||
connectLocale={en}
|
||||
connectOptions={undefined}
|
||||
/>
|
||||
</AccountProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
const elements = container.getElementsByClassName(
|
||||
"tw-connected-wallet__balance",
|
||||
);
|
||||
expect(!!elements.length).toBe(true);
|
||||
});
|
||||
|
||||
it("should render the Address section if detailsButton.connectedAccountName is not passed", () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AccountProvider address={VITALIK_WALLET} client={client}>
|
||||
<ConnectedWalletDetails
|
||||
theme={"dark"}
|
||||
detailsButton={undefined}
|
||||
detailsModal={undefined}
|
||||
supportedTokens={undefined}
|
||||
supportedNFTs={undefined}
|
||||
onDisconnect={() => {}}
|
||||
chains={[]}
|
||||
switchButton={undefined}
|
||||
client={client}
|
||||
connectLocale={en}
|
||||
connectOptions={undefined}
|
||||
/>
|
||||
</AccountProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
const elements = container.getElementsByClassName(
|
||||
"tw-connected-wallet__address",
|
||||
);
|
||||
expect(!!elements.length).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT render the Address section if detailsButton.connectedAccountName is passed", () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AccountProvider address={VITALIK_WALLET} client={client}>
|
||||
<ConnectedWalletDetails
|
||||
theme={"dark"}
|
||||
detailsButton={{
|
||||
connectedAccountName: "test name",
|
||||
}}
|
||||
detailsModal={undefined}
|
||||
supportedTokens={undefined}
|
||||
supportedNFTs={undefined}
|
||||
onDisconnect={() => {}}
|
||||
chains={[]}
|
||||
switchButton={undefined}
|
||||
client={client}
|
||||
connectLocale={en}
|
||||
connectOptions={undefined}
|
||||
/>
|
||||
</AccountProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
const elements = container.getElementsByClassName(
|
||||
"tw-connected-wallet__address",
|
||||
);
|
||||
expect(elements.length).toBe(1);
|
||||
expect(elements[0]?.innerHTML).toBe("test name");
|
||||
});
|
||||
|
||||
it("should render a custom img if detailsButton?.connectedAccountAvatarUrl is passed", () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AccountProvider address={VITALIK_WALLET} client={client}>
|
||||
<ConnectedWalletDetails
|
||||
theme={"dark"}
|
||||
detailsButton={{
|
||||
connectedAccountAvatarUrl: "https://thirdweb.com/cat.png",
|
||||
}}
|
||||
detailsModal={undefined}
|
||||
supportedTokens={undefined}
|
||||
supportedNFTs={undefined}
|
||||
onDisconnect={() => {}}
|
||||
chains={[]}
|
||||
switchButton={undefined}
|
||||
client={client}
|
||||
connectLocale={en}
|
||||
connectOptions={undefined}
|
||||
/>
|
||||
</AccountProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
const elements = container.getElementsByTagName("img");
|
||||
expect(elements.length).toBe(1);
|
||||
expect(elements[0]?.src).toBe("https://thirdweb.com/cat.png");
|
||||
});
|
||||
|
||||
it("should render AccountAvatar if no custom image is passed", () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AccountProvider address={VITALIK_WALLET} client={client}>
|
||||
<ConnectedWalletDetails
|
||||
theme={"dark"}
|
||||
detailsButton={{
|
||||
connectedAccountName: "test name",
|
||||
}}
|
||||
detailsModal={undefined}
|
||||
supportedTokens={undefined}
|
||||
supportedNFTs={undefined}
|
||||
onDisconnect={() => {}}
|
||||
chains={[]}
|
||||
switchButton={undefined}
|
||||
client={client}
|
||||
connectLocale={en}
|
||||
connectOptions={undefined}
|
||||
/>
|
||||
</AccountProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
const elements = container.getElementsByClassName(
|
||||
"tw-connected-wallet__account_avatar",
|
||||
);
|
||||
expect(elements.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should render the SwitchNetworkButton if chain is mismatched", () => {
|
||||
vi.mock(
|
||||
"../../../../react/core/hooks/wallets/useActiveWalletChain.js",
|
||||
() => ({
|
||||
useActiveWalletChain: vi.fn(),
|
||||
}),
|
||||
);
|
||||
vi.mocked(useActiveWalletChain).mockReturnValue(base);
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AccountProvider address={VITALIK_WALLET} client={client}>
|
||||
<ConnectedWalletDetails
|
||||
theme={"dark"}
|
||||
detailsButton={{
|
||||
connectedAccountName: "test name",
|
||||
}}
|
||||
detailsModal={undefined}
|
||||
supportedTokens={undefined}
|
||||
supportedNFTs={undefined}
|
||||
onDisconnect={() => {}}
|
||||
chains={[]}
|
||||
switchButton={{
|
||||
style: {
|
||||
color: "red",
|
||||
},
|
||||
className: "thirdwebSwitchBtn",
|
||||
label: "switchbtn",
|
||||
}}
|
||||
client={client}
|
||||
connectLocale={en}
|
||||
connectOptions={undefined}
|
||||
chain={ethereum}
|
||||
/>
|
||||
</AccountProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
const element = container.querySelector(
|
||||
"button.tw-connect-wallet--switch-network",
|
||||
);
|
||||
expect(element).not.toBe(null);
|
||||
const element2 = container.querySelector("button.thirdwebSwitchBtn");
|
||||
expect(element2).not.toBe(null);
|
||||
expect(element && element.innerHTML === "switchbtn").toBe(true);
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should render the fiat value properly", () => {
|
||||
expect(
|
||||
detailsBtn_formatFiatBalanceForButton({ balance: 12.9231, symbol: "$" }),
|
||||
).toBe(" ($13)");
|
||||
});
|
||||
|
||||
it("should render the token balance properly", () => {
|
||||
expect(
|
||||
detailsBtn_formatTokenBalanceForButton({
|
||||
balance: 12.923111,
|
||||
symbol: "ETH",
|
||||
}),
|
||||
).toBe("12.9231 ETH");
|
||||
});
|
||||
});
|
||||
|
||||
const thirdwebWrapper: FC = ({ children }: React.PropsWithChildren) => (
|
||||
<ThirdwebProvider>{children}</ThirdwebProvider>
|
||||
);
|
||||
|
||||
/**
|
||||
* useWalletDetailsModal
|
||||
*/
|
||||
describe("useWalletDetailsModal", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return an object with an open function", () => {
|
||||
const { result } = renderHook(() => useWalletDetailsModal(), {
|
||||
wrapper: thirdwebWrapper,
|
||||
});
|
||||
expect(result.current).toHaveProperty("open");
|
||||
expect(typeof result.current.open).toBe("function");
|
||||
});
|
||||
|
||||
it("should throw an error when opening modal without a connected wallet", () => {
|
||||
const { result } = renderHook(() => useWalletDetailsModal(), {
|
||||
wrapper: thirdwebWrapper,
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
result.current.open({
|
||||
client,
|
||||
}),
|
||||
).toThrow("Wallet is not connected.");
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* SwitchNetworkButton
|
||||
*/
|
||||
describe("SwitchNetworkButton", () => {
|
||||
it("should render a default button", () => {
|
||||
const { container } = render(
|
||||
<SwitchNetworkButton targetChain={ethereum} connectLocale={en} />,
|
||||
);
|
||||
const element = container.querySelector(
|
||||
"button.tw-connect-wallet--switch-network",
|
||||
);
|
||||
expect(element).not.toBe(null);
|
||||
});
|
||||
|
||||
it("should apply the style properly", () => {
|
||||
const { container } = render(
|
||||
<SwitchNetworkButton
|
||||
targetChain={ethereum}
|
||||
connectLocale={en}
|
||||
style={{ color: "red", width: "4444px" }}
|
||||
/>,
|
||||
);
|
||||
const element = container.querySelector(
|
||||
"button.tw-connect-wallet--switch-network",
|
||||
);
|
||||
if (!element) {
|
||||
throw new Error("Failed to render SwitchNetworkButton");
|
||||
}
|
||||
const styles = window.getComputedStyle(element);
|
||||
expect(styles.color).toBe("red");
|
||||
expect(styles.width).toBe("4444px");
|
||||
});
|
||||
|
||||
it("should apply the className properly", () => {
|
||||
const { container } = render(
|
||||
<SwitchNetworkButton
|
||||
targetChain={ethereum}
|
||||
connectLocale={en}
|
||||
className="thirdwebRocks"
|
||||
/>,
|
||||
);
|
||||
const element = container.querySelector("button.thirdwebRocks");
|
||||
expect(element).not.toBe(null);
|
||||
});
|
||||
|
||||
it("should render button's text with locale.switchNetwork by default", () => {
|
||||
const { container } = render(
|
||||
<SwitchNetworkButton targetChain={ethereum} connectLocale={en} />,
|
||||
);
|
||||
const element = container.querySelector(
|
||||
"button.tw-connect-wallet--switch-network",
|
||||
);
|
||||
if (!element) {
|
||||
throw new Error("Failed to render SwitchNetworkButton");
|
||||
}
|
||||
expect(element.innerHTML).toBe(en.switchNetwork);
|
||||
});
|
||||
|
||||
it("should render `switchNetworkBtnTitle` properly", () => {
|
||||
const { container } = render(
|
||||
<SwitchNetworkButton
|
||||
targetChain={ethereum}
|
||||
connectLocale={en}
|
||||
switchNetworkBtnTitle="cat"
|
||||
/>,
|
||||
);
|
||||
const element = container.querySelector(
|
||||
"button.tw-connect-wallet--switch-network",
|
||||
);
|
||||
if (!element) {
|
||||
throw new Error("Failed to render SwitchNetworkButton");
|
||||
}
|
||||
expect(element.innerHTML).toBe("cat");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ConnectedToSmartWallet", () => {
|
||||
it("should render nothing since no active wallet exists in default test env", () => {
|
||||
const { container } = render(
|
||||
<ThirdwebProvider>
|
||||
<ConnectedToSmartWallet client={client} connectLocale={en} />
|
||||
</ThirdwebProvider>,
|
||||
);
|
||||
// no smart wallet exists in this env so this component should render null
|
||||
const element = container.querySelector("span");
|
||||
expect(element).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("InAppWalletUserInfo", () => {
|
||||
it("should render a Skeleton since no active wallet exists in default test env", () => {
|
||||
const { container } = render(
|
||||
<ThirdwebProvider>
|
||||
<InAppWalletUserInfo client={client} locale={en} />
|
||||
</ThirdwebProvider>,
|
||||
);
|
||||
// no smart wallet exists in this env so this component should render null
|
||||
const element = container.querySelector(
|
||||
"div.InAppWalletUserInfo__skeleton",
|
||||
);
|
||||
expect(element).not.toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Details Modal", () => {
|
||||
beforeEach(() => {
|
||||
// Mock the animate method
|
||||
HTMLDivElement.prototype.animate = vi.fn().mockReturnValue({
|
||||
onfinish: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it("should close the modal when activeAccount is falsy", async () => {
|
||||
const closeModalMock = vi.fn();
|
||||
const locale = await getConnectLocale("en_US");
|
||||
|
||||
vi.mocked(useActiveAccount).mockReturnValue(undefined);
|
||||
|
||||
render(
|
||||
<DetailsModal
|
||||
client={TEST_CLIENT}
|
||||
locale={locale}
|
||||
detailsModal={mockDetailsModalOptions}
|
||||
theme="light"
|
||||
supportedTokens={mockSupportedTokens}
|
||||
supportedNFTs={mockSupportedNFTs}
|
||||
closeModal={closeModalMock}
|
||||
onDisconnect={mockOnDisconnect}
|
||||
chains={mockChains}
|
||||
displayBalanceToken={mockDisplayBalanceToken}
|
||||
connectOptions={mockConnectOptions}
|
||||
assetTabs={mockAssetTabs}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(closeModalMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the DetailsModal with default props", async () => {
|
||||
const closeModalMock = vi.fn();
|
||||
const locale = await getConnectLocale("en_US");
|
||||
|
||||
render(
|
||||
<DetailsModal
|
||||
client={TEST_CLIENT}
|
||||
locale={locale}
|
||||
theme="light"
|
||||
closeModal={closeModalMock}
|
||||
onDisconnect={mockOnDisconnect}
|
||||
chains={mockChains}
|
||||
connectOptions={mockConnectOptions}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Add assertions to check if the modal is rendered correctly
|
||||
expect(screen.getByText("Connect Modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call closeModal when the close button is clicked", async () => {
|
||||
const closeModalMock = vi.fn();
|
||||
const locale = await getConnectLocale("en_US");
|
||||
|
||||
render(
|
||||
<DetailsModal
|
||||
client={TEST_CLIENT}
|
||||
locale={locale}
|
||||
theme="light"
|
||||
closeModal={closeModalMock}
|
||||
onDisconnect={mockOnDisconnect}
|
||||
chains={mockChains}
|
||||
connectOptions={mockConnectOptions}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Simulate clicking the close button
|
||||
fireEvent.click(screen.getByRole("button", { name: /close/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(closeModalMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,15 +9,15 @@ import {
|
||||
TextAlignJustifyIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { type JSX, useContext, useEffect, useState } from "react";
|
||||
import { type JSX, useCallback, useContext, useEffect, useState } from "react";
|
||||
import { trackPayEvent } from "../../../../analytics/track/pay.js";
|
||||
import type { Chain } from "../../../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../../../client/client.js";
|
||||
import { getContract } from "../../../../contract/contract.js";
|
||||
import type { SupportedFiatCurrency } from "../../../../pay/convert/type.js";
|
||||
import { getLastAuthProvider } from "../../../../react/core/utils/storage.js";
|
||||
import { shortenAddress } from "../../../../utils/address.js";
|
||||
import { isContractDeployed } from "../../../../utils/bytecode/is-contract-deployed.js";
|
||||
import { formatNumber } from "../../../../utils/formatNumber.js";
|
||||
import { webLocalStorage } from "../../../../utils/storage/webStorage.js";
|
||||
import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js";
|
||||
import type { Ecosystem } from "../../../../wallets/in-app/core/wallet/types.js";
|
||||
@@ -86,7 +86,12 @@ import { fadeInAnimation } from "../design-system/animations.js";
|
||||
import { StyledButton } from "../design-system/elements.js";
|
||||
import { AccountAddress } from "../prebuilt/Account/address.js";
|
||||
import { AccountAvatar } from "../prebuilt/Account/avatar.js";
|
||||
import { AccountBalance } from "../prebuilt/Account/balance.js";
|
||||
import {
|
||||
AccountBalance,
|
||||
type AccountBalanceInfo,
|
||||
formatAccountFiatBalance,
|
||||
formatAccountTokenBalance,
|
||||
} from "../prebuilt/Account/balance.js";
|
||||
import { AccountBlobbie } from "../prebuilt/Account/blobbie.js";
|
||||
import { AccountName } from "../prebuilt/Account/name.js";
|
||||
import { AccountProvider } from "../prebuilt/Account/provider.js";
|
||||
@@ -144,7 +149,6 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
}> = (props) => {
|
||||
const { connectLocale: locale, client } = props;
|
||||
const setRootEl = useContext(SetRootElementContext);
|
||||
const activeAccount = useActiveAccount();
|
||||
const walletChain = useActiveWalletChain();
|
||||
|
||||
function closeModal() {
|
||||
@@ -196,6 +200,9 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
|
||||
const combinedClassName = `${TW_CONNECTED_WALLET} ${props.detailsButton?.className || ""}`;
|
||||
|
||||
const tokenAddress =
|
||||
props.detailsButton?.displayBalanceToken?.[Number(walletChain?.id)];
|
||||
|
||||
return (
|
||||
<WalletInfoButton
|
||||
type="button"
|
||||
@@ -223,16 +230,30 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
activeAccount && (
|
||||
<AccountAvatar
|
||||
loadingComponent={<AccountBlobbie size={35} />}
|
||||
fallbackComponent={<AccountBlobbie size={35} />}
|
||||
queryOptions={{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
<AccountAvatar
|
||||
className={`${TW_CONNECTED_WALLET}__account_avatar`}
|
||||
loadingComponent={
|
||||
<AccountBlobbie
|
||||
size={35}
|
||||
className={`${TW_CONNECTED_WALLET}__account_avatar`}
|
||||
/>
|
||||
}
|
||||
fallbackComponent={
|
||||
<AccountBlobbie
|
||||
size={35}
|
||||
className={`${TW_CONNECTED_WALLET}__account_avatar`}
|
||||
/>
|
||||
}
|
||||
queryOptions={{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
}}
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
<Container
|
||||
@@ -273,25 +294,71 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
size="xs"
|
||||
color="secondaryText"
|
||||
weight={400}
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "2px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<AccountBalance
|
||||
chain={walletChain}
|
||||
loadingComponent={<Skeleton height={fontSize.xs} width="70px" />}
|
||||
fallbackComponent={<Skeleton height={fontSize.xs} width="70px" />}
|
||||
formatFn={formatBalanceOnButton}
|
||||
tokenAddress={
|
||||
props.detailsButton?.displayBalanceToken?.[
|
||||
Number(walletChain?.id)
|
||||
]
|
||||
}
|
||||
/>
|
||||
{props.detailsButton?.showBalanceInFiat ? (
|
||||
<>
|
||||
<AccountBalance
|
||||
chain={walletChain}
|
||||
loadingComponent={
|
||||
<Skeleton height={fontSize.xs} width="50px" />
|
||||
}
|
||||
fallbackComponent={
|
||||
<Skeleton height={fontSize.xs} width="50px" />
|
||||
}
|
||||
tokenAddress={tokenAddress}
|
||||
/>
|
||||
<AccountBalance
|
||||
chain={walletChain}
|
||||
tokenAddress={tokenAddress}
|
||||
showBalanceInFiat="USD"
|
||||
formatFn={detailsBtn_formatFiatBalanceForButton}
|
||||
loadingComponent={
|
||||
<Skeleton height={fontSize.xs} width="20px" />
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<AccountBalance
|
||||
chain={walletChain}
|
||||
loadingComponent={<Skeleton height={fontSize.xs} width="70px" />}
|
||||
fallbackComponent={<Skeleton height={fontSize.xs} width="70px" />}
|
||||
formatFn={detailsBtn_formatTokenBalanceForButton}
|
||||
tokenAddress={tokenAddress}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
</Container>
|
||||
</WalletInfoButton>
|
||||
);
|
||||
};
|
||||
|
||||
function DetailsModal(props: {
|
||||
/**
|
||||
* @internal Exported for tests
|
||||
*/
|
||||
export function detailsBtn_formatFiatBalanceForButton(
|
||||
props: AccountBalanceInfo,
|
||||
) {
|
||||
return ` (${formatAccountFiatBalance({ ...props, decimals: 0 })})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Exported for test
|
||||
*/
|
||||
export function detailsBtn_formatTokenBalanceForButton(
|
||||
props: AccountBalanceInfo,
|
||||
) {
|
||||
return `${formatAccountTokenBalance({ ...props, decimals: props.balance < 1 ? 5 : 4 })}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Exported for tests only
|
||||
*/
|
||||
export function DetailsModal(props: {
|
||||
client: ThirdwebClient;
|
||||
locale: ConnectLocale;
|
||||
detailsModal?: ConnectButton_detailsModalOptions;
|
||||
@@ -307,6 +374,7 @@ function DetailsModal(props: {
|
||||
displayBalanceToken?: Record<number, string>;
|
||||
connectOptions: DetailsModalConnectOptions | undefined;
|
||||
assetTabs?: AssetTabs[];
|
||||
showBalanceInFiat?: SupportedFiatCurrency;
|
||||
}) {
|
||||
const [screen, setScreen] = useState<WalletDetailsModalScreen>("main");
|
||||
const { disconnect } = useDisconnect();
|
||||
@@ -330,12 +398,12 @@ function DetailsModal(props: {
|
||||
wallets: activeWallet ? [activeWallet] : [],
|
||||
});
|
||||
|
||||
function closeModal() {
|
||||
const closeModal = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
onModalUnmount(() => {
|
||||
props.closeModal();
|
||||
});
|
||||
}
|
||||
}, [props.closeModal]);
|
||||
|
||||
function handleDisconnect(info: { wallet: Wallet; account: Account }) {
|
||||
setIsOpen(false);
|
||||
@@ -343,6 +411,12 @@ function DetailsModal(props: {
|
||||
props.onDisconnect(info);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeAccount) {
|
||||
closeModal();
|
||||
}
|
||||
}, [activeAccount, closeModal]);
|
||||
|
||||
const networkSwitcherButton = (
|
||||
<MenuButton
|
||||
type="button"
|
||||
@@ -377,15 +451,44 @@ function DetailsModal(props: {
|
||||
<Text color="primaryText" size="md" multiline>
|
||||
{chainNameQuery.name || `Unknown chain #${walletChain?.id}`}
|
||||
<Text color="secondaryText" size="xs">
|
||||
<AccountBalance
|
||||
fallbackComponent={<Skeleton height="1em" width="100px" />}
|
||||
loadingComponent={<Skeleton height="1em" width="100px" />}
|
||||
formatFn={(num: number) => formatNumber(num, 9)}
|
||||
chain={walletChain}
|
||||
tokenAddress={
|
||||
props.displayBalanceToken?.[Number(walletChain?.id)]
|
||||
}
|
||||
/>
|
||||
{props.showBalanceInFiat ? (
|
||||
<>
|
||||
<AccountBalance
|
||||
fallbackComponent={<Skeleton height="1em" width="70px" />}
|
||||
loadingComponent={<Skeleton height="1em" width="70px" />}
|
||||
chain={walletChain}
|
||||
tokenAddress={
|
||||
props.displayBalanceToken?.[Number(walletChain?.id)]
|
||||
}
|
||||
formatFn={(props: AccountBalanceInfo) =>
|
||||
formatAccountTokenBalance({ ...props, decimals: 7 })
|
||||
}
|
||||
/>{" "}
|
||||
<AccountBalance
|
||||
loadingComponent={<Skeleton height="1em" width="30px" />}
|
||||
chain={walletChain}
|
||||
tokenAddress={
|
||||
props.displayBalanceToken?.[Number(walletChain?.id)]
|
||||
}
|
||||
formatFn={(props: AccountBalanceInfo) =>
|
||||
` (${formatAccountFiatBalance({ ...props, decimals: 3 })})`
|
||||
}
|
||||
showBalanceInFiat="USD"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<AccountBalance
|
||||
fallbackComponent={<Skeleton height="1em" width="100px" />}
|
||||
loadingComponent={<Skeleton height="1em" width="100px" />}
|
||||
formatFn={(props: AccountBalanceInfo) =>
|
||||
formatAccountTokenBalance({ ...props, decimals: 7 })
|
||||
}
|
||||
chain={walletChain}
|
||||
tokenAddress={
|
||||
props.displayBalanceToken?.[Number(walletChain?.id)]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
@@ -437,6 +540,11 @@ function DetailsModal(props: {
|
||||
<AccountAvatar
|
||||
loadingComponent={<AccountBlobbie size={Number(iconSize.xxl)} />}
|
||||
fallbackComponent={<AccountBlobbie size={Number(iconSize.xxl)} />}
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -470,7 +578,7 @@ function DetailsModal(props: {
|
||||
);
|
||||
|
||||
let content = (
|
||||
<div>
|
||||
<div className={`${TW_CONNECTED_WALLET}__default_modal_screen`}>
|
||||
<Spacer y="xs" />
|
||||
<Container
|
||||
px="lg"
|
||||
@@ -691,20 +799,6 @@ function DetailsModal(props: {
|
||||
<Text color="primaryText">{props.locale.manageWallet.title}</Text>
|
||||
</MenuButton>
|
||||
|
||||
{/* Switch to Personal Wallet */}
|
||||
{/* {personalWallet &&
|
||||
!props.detailsModal?.hideSwitchToPersonalWallet && (
|
||||
<AccountSwitcher
|
||||
wallet={personalWallet}
|
||||
name={locale.personalWallet}
|
||||
/>
|
||||
)} */}
|
||||
|
||||
{/* Switch to Smart Wallet */}
|
||||
{/* {smartWallet && (
|
||||
<AccountSwitcher name={locale.smartWallet} wallet={smartWallet} />
|
||||
)} */}
|
||||
|
||||
{/* Request Testnet funds */}
|
||||
{(props.detailsModal?.showTestnetFaucet ?? false) &&
|
||||
(chainFaucetsQuery.faucets.length > 0 ||
|
||||
@@ -993,12 +1087,11 @@ function DetailsModal(props: {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AccountProvider
|
||||
address={activeAccount?.address || ""}
|
||||
client={client}
|
||||
>
|
||||
{content}
|
||||
</AccountProvider>
|
||||
{activeAccount?.address && (
|
||||
<AccountProvider address={activeAccount.address} client={client}>
|
||||
{content}
|
||||
</AccountProvider>
|
||||
)}
|
||||
</Modal>
|
||||
</ScreenSetupContext.Provider>
|
||||
</WalletUIStatesProvider>
|
||||
@@ -1006,10 +1099,6 @@ function DetailsModal(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function formatBalanceOnButton(num: number) {
|
||||
return formatNumber(num, num < 1 ? 5 : 4);
|
||||
}
|
||||
|
||||
const WalletInfoButton = /* @__PURE__ */ StyledButton((_) => {
|
||||
const theme = useCustomTheme();
|
||||
return {
|
||||
@@ -1045,7 +1134,10 @@ const StyledChevronRightIcon = /* @__PURE__ */ styled(
|
||||
};
|
||||
});
|
||||
|
||||
function ConnectedToSmartWallet(props: {
|
||||
/**
|
||||
* @internal Exported for test
|
||||
*/
|
||||
export function ConnectedToSmartWallet(props: {
|
||||
client: ThirdwebClient;
|
||||
connectLocale: ConnectLocale;
|
||||
}) {
|
||||
@@ -1104,7 +1196,10 @@ function ConnectedToSmartWallet(props: {
|
||||
return null;
|
||||
}
|
||||
|
||||
function InAppWalletUserInfo(props: {
|
||||
/**
|
||||
* @internal Exported for tests
|
||||
*/
|
||||
export function InAppWalletUserInfo(props: {
|
||||
client: ThirdwebClient;
|
||||
locale: ConnectLocale;
|
||||
}) {
|
||||
@@ -1191,13 +1286,19 @@ function InAppWalletUserInfo(props: {
|
||||
);
|
||||
}
|
||||
|
||||
return <Skeleton width="50px" height="10px" />;
|
||||
return (
|
||||
<Skeleton
|
||||
width="50px"
|
||||
height="10px"
|
||||
className="InAppWalletUserInfo__skeleton"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @internal Exported for tests
|
||||
*/
|
||||
function SwitchNetworkButton(props: {
|
||||
export function SwitchNetworkButton(props: {
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
switchNetworkBtnTitle?: string;
|
||||
@@ -1507,6 +1608,12 @@ export type UseWalletDetailsModalOptions = {
|
||||
* Note: If an empty array is passed, the [View Funds] button will be hidden
|
||||
*/
|
||||
assetTabs?: AssetTabs[];
|
||||
|
||||
/**
|
||||
* Show the token balance's value in fiat.
|
||||
* Note: Not all tokens are resolvable to a fiat value. In that case, nothing will be shown.
|
||||
*/
|
||||
showBalanceInFiat?: SupportedFiatCurrency;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { render, screen } from "../../../../../../test/src/react-render.js";
|
||||
import { useSocialProfiles } from "../../../../core/social/useSocialProfiles.js";
|
||||
import { useProfiles } from "../../../hooks/wallets/useProfiles.js";
|
||||
import { LinkedProfilesScreen } from "./LinkedProfilesScreen.jsx";
|
||||
|
||||
// Mock the hooks
|
||||
vi.mock("../../../hooks/wallets/useProfiles");
|
||||
vi.mock("../../../../core/social/useSocialProfiles");
|
||||
vi.mock("../../components/Img", () => ({
|
||||
Img: () => <div data-testid="mock-img">Mock Image</div>,
|
||||
}));
|
||||
|
||||
describe("LinkedProfilesScreen", () => {
|
||||
const mockClient = {
|
||||
clientId: "test-client-id",
|
||||
secretKey: undefined,
|
||||
};
|
||||
|
||||
const mockProps = {
|
||||
onBack: vi.fn(),
|
||||
setScreen: vi.fn(),
|
||||
locale: {
|
||||
manageWallet: {
|
||||
linkedProfiles: "Linked Profiles",
|
||||
linkProfile: "Link Profile",
|
||||
},
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mocking data
|
||||
} as any,
|
||||
client: mockClient,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useSocialProfiles).mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mocking data
|
||||
} as any);
|
||||
});
|
||||
|
||||
describe("getProfileDisplayName", () => {
|
||||
it("should display email for email profile type", () => {
|
||||
vi.mocked(useProfiles).mockReturnValue({
|
||||
data: [{ type: "email", details: { email: "[email protected]" } }],
|
||||
isLoading: false,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mocking data
|
||||
} as any);
|
||||
|
||||
render(<LinkedProfilesScreen {...mockProps} />);
|
||||
expect(screen.getByText("[email protected]")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display email for google profile type", () => {
|
||||
vi.mocked(useProfiles).mockReturnValue({
|
||||
data: [{ type: "google", details: { email: "[email protected]" } }],
|
||||
isLoading: false,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mocking data
|
||||
} as any);
|
||||
|
||||
render(<LinkedProfilesScreen {...mockProps} />);
|
||||
expect(screen.getByText("[email protected]")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display phone number for phone profile type", () => {
|
||||
vi.mocked(useProfiles).mockReturnValue({
|
||||
data: [{ type: "phone", details: { phone: "+1234567890" } }],
|
||||
isLoading: false,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mocking data
|
||||
} as any);
|
||||
|
||||
render(<LinkedProfilesScreen {...mockProps} />);
|
||||
expect(screen.getByText("+1234567890")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display shortened address when address is present", () => {
|
||||
vi.mocked(useProfiles).mockReturnValue({
|
||||
data: [
|
||||
{
|
||||
type: "wallet",
|
||||
details: { address: "0x1234567890abcdef1234567890abcdef12345678" },
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mocking data
|
||||
} as any);
|
||||
|
||||
render(<LinkedProfilesScreen {...mockProps} />);
|
||||
expect(screen.getByText("0x123456...345678")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display email for cognito profile type", () => {
|
||||
vi.mocked(useProfiles).mockReturnValue({
|
||||
data: [{ type: "cognito", details: { email: "[email protected]" } }],
|
||||
isLoading: false,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mocking data
|
||||
} as any);
|
||||
|
||||
render(<LinkedProfilesScreen {...mockProps} />);
|
||||
expect(screen.getByText("[email protected]")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display Custom Profile for custom_auth_endpoint", () => {
|
||||
vi.mocked(useProfiles).mockReturnValue({
|
||||
data: [{ type: "Custom_auth_endpoint", details: {} }],
|
||||
isLoading: false,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mocking data
|
||||
} as any);
|
||||
|
||||
render(<LinkedProfilesScreen {...mockProps} />);
|
||||
expect(screen.getByText("Custom Profile")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should capitalize unknown profile types", () => {
|
||||
vi.mocked(useProfiles).mockReturnValue({
|
||||
data: [{ type: "unknown", details: {} }],
|
||||
isLoading: false,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mocking data
|
||||
} as any);
|
||||
|
||||
render(<LinkedProfilesScreen {...mockProps} />);
|
||||
expect(screen.getByText("Unknown")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not display guest profiles", () => {
|
||||
vi.mocked(useProfiles).mockReturnValue({
|
||||
data: [{ type: "guest", details: {} }],
|
||||
isLoading: false,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mocking data
|
||||
} as any);
|
||||
|
||||
render(<LinkedProfilesScreen {...mockProps} />);
|
||||
expect(screen.queryByText("Guest")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -33,6 +33,8 @@ function getProfileDisplayName(profile: Profile) {
|
||||
case (profile.type as string) === "cognito" &&
|
||||
profile.details.email !== undefined:
|
||||
return profile.details.email;
|
||||
case (profile.type as string).toLowerCase() === "custom_auth_endpoint":
|
||||
return "Custom Profile";
|
||||
default:
|
||||
return profile.type.slice(0, 1).toUpperCase() + profile.type.slice(1);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { render } from "~test/react-render.js";
|
||||
import { TEST_CLIENT } from "~test/test-clients.js";
|
||||
import { createWallet } from "../../../../../wallets/create-wallet.js";
|
||||
import en from "../locale/en.js";
|
||||
import { PrivateKey } from "./PrivateKey.js";
|
||||
|
||||
const client = TEST_CLIENT;
|
||||
|
||||
describe("PrivateKey screen", () => {
|
||||
it("should render the iframe", () => {
|
||||
const { container } = render(
|
||||
<PrivateKey
|
||||
onBack={() => {}}
|
||||
client={client}
|
||||
connectLocale={en}
|
||||
theme="dark"
|
||||
wallet={createWallet("io.metamask")}
|
||||
/>,
|
||||
);
|
||||
|
||||
const iframe = container.querySelector("iframe");
|
||||
expect(iframe).not.toBe(null);
|
||||
});
|
||||
|
||||
it("should throw an error if no wallet is specified", () => {
|
||||
expect(() =>
|
||||
render(
|
||||
<PrivateKey
|
||||
onBack={() => {}}
|
||||
client={client}
|
||||
connectLocale={en}
|
||||
theme="dark"
|
||||
/>,
|
||||
),
|
||||
).toThrowError("[PrivateKey] No wallet found");
|
||||
});
|
||||
|
||||
it("should render the modal title", () => {
|
||||
const { container } = render(
|
||||
<PrivateKey
|
||||
onBack={() => {}}
|
||||
client={client}
|
||||
connectLocale={en}
|
||||
theme="dark"
|
||||
wallet={createWallet("io.metamask")}
|
||||
/>,
|
||||
);
|
||||
|
||||
const element = container.querySelector("h2");
|
||||
expect(element).not.toBe(null);
|
||||
expect(element?.innerHTML).toBe(en.manageWallet.exportPrivateKey);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { render } from "~test/react-render.js";
|
||||
import { TEST_CLIENT } from "~test/test-clients.js";
|
||||
import en from "../locale/en.js";
|
||||
import { ReceiveFunds } from "./ReceiveFunds.js";
|
||||
|
||||
const client = TEST_CLIENT;
|
||||
|
||||
describe("ReceiveFunds screen", () => {
|
||||
it("should render a title with locale.title", () => {
|
||||
const { container } = render(
|
||||
<ReceiveFunds onBack={() => {}} client={client} connectLocale={en} />,
|
||||
);
|
||||
const element = container.querySelector("h2");
|
||||
expect(element).not.toBe(null);
|
||||
expect(element?.innerHTML).toBe(en.receiveFundsScreen.title);
|
||||
});
|
||||
|
||||
it("should render a span with locale.instruction", () => {
|
||||
const { container } = render(
|
||||
<ReceiveFunds onBack={() => {}} client={client} connectLocale={en} />,
|
||||
);
|
||||
const element = container.querySelector(
|
||||
"span.receive_fund_screen_instruction",
|
||||
);
|
||||
expect(element).not.toBe(null);
|
||||
expect(element?.innerHTML).toBe(en.receiveFundsScreen.instruction);
|
||||
});
|
||||
|
||||
it("should render the CopyIcon", () => {
|
||||
const { container } = render(
|
||||
<ReceiveFunds onBack={() => {}} client={client} connectLocale={en} />,
|
||||
);
|
||||
const element = container.querySelector("svg.tw-copy-icon");
|
||||
expect(element).not.toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -70,7 +70,12 @@ export function ReceiveFunds(props: {
|
||||
|
||||
<Spacer y="lg" />
|
||||
|
||||
<Text multiline center balance>
|
||||
<Text
|
||||
multiline
|
||||
center
|
||||
balance
|
||||
className="receive_fund_screen_instruction"
|
||||
>
|
||||
{locale.instruction}
|
||||
</Text>
|
||||
</Container>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { render } from "~test/react-render.js";
|
||||
import { TEST_CLIENT } from "~test/test-clients.js";
|
||||
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
|
||||
import { base } from "../../../../../chains/chain-definitions/base.js";
|
||||
import { useActiveWalletChain } from "../../../../../react/core/hooks/wallets/useActiveWalletChain.js";
|
||||
import en from "../locale/en.js";
|
||||
import { SendFundsForm } from "./SendFunds.js";
|
||||
|
||||
const client = TEST_CLIENT;
|
||||
|
||||
describe("SendFunds screen", () => {
|
||||
it("should render a title with locale.title", () => {
|
||||
vi.mock(
|
||||
"../../../../../react/core/hooks/wallets/useActiveWalletChain.js",
|
||||
() => ({
|
||||
useActiveWalletChain: vi.fn(),
|
||||
}),
|
||||
);
|
||||
vi.mocked(useActiveWalletChain).mockReturnValue(base);
|
||||
const { container } = render(
|
||||
<SendFundsForm
|
||||
token={{ nativeToken: true }}
|
||||
onTokenSelect={() => {}}
|
||||
receiverAddress={TEST_ACCOUNT_A.address}
|
||||
setReceiverAddress={() => {}}
|
||||
amount={"1"}
|
||||
setAmount={() => {}}
|
||||
onBack={() => {}}
|
||||
client={client}
|
||||
connectLocale={en}
|
||||
/>,
|
||||
);
|
||||
const element = container.querySelector("h2");
|
||||
expect(element).not.toBe(null);
|
||||
expect(element?.innerHTML).toBe(en.sendFundsScreen.title);
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("SendFundsForm should render the send button", () => {
|
||||
vi.mock(
|
||||
"../../../../../react/core/hooks/wallets/useActiveWalletChain.js",
|
||||
() => ({
|
||||
useActiveWalletChain: vi.fn(),
|
||||
}),
|
||||
);
|
||||
vi.mocked(useActiveWalletChain).mockReturnValue(base);
|
||||
const { container } = render(
|
||||
<SendFundsForm
|
||||
token={{ nativeToken: true }}
|
||||
onTokenSelect={() => {}}
|
||||
receiverAddress={TEST_ACCOUNT_A.address}
|
||||
setReceiverAddress={() => {}}
|
||||
amount={"1"}
|
||||
setAmount={() => {}}
|
||||
onBack={() => {}}
|
||||
client={client}
|
||||
connectLocale={en}
|
||||
/>,
|
||||
);
|
||||
const element = container.querySelector(
|
||||
"button.tw-sendfunds-screen-send-button",
|
||||
);
|
||||
expect(element?.innerHTML).toBe(en.sendFundsScreen.submitButton);
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
});
|
||||
@@ -107,9 +107,9 @@ export function SendFunds(props: {
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @internal Exported for tests
|
||||
*/
|
||||
function SendFundsForm(props: {
|
||||
export function SendFundsForm(props: {
|
||||
onTokenSelect: () => void;
|
||||
token: ERC20OrNativeToken;
|
||||
receiverAddress: string;
|
||||
@@ -321,6 +321,7 @@ function SendFundsForm(props: {
|
||||
fullWidth
|
||||
variant="accent"
|
||||
type="submit"
|
||||
className="tw-sendfunds-screen-send-button"
|
||||
onClick={async () => {
|
||||
if (!receiverAddress || !amount) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { render } from "~test/react-render.js";
|
||||
import { TEST_CLIENT } from "~test/test-clients.js";
|
||||
import en from "../locale/en.js";
|
||||
import { StartScreen } from "./StartScreen.js";
|
||||
|
||||
const client = TEST_CLIENT;
|
||||
|
||||
describe("StartScreen", () => {
|
||||
it("should render an image for the welcome screen", () => {
|
||||
const { container } = render(
|
||||
<StartScreen
|
||||
connectLocale={en}
|
||||
client={client}
|
||||
welcomeScreen={{
|
||||
img: {
|
||||
src: "https://cat.png",
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
}}
|
||||
meta={{
|
||||
showThirdwebBranding: false,
|
||||
termsOfServiceUrl: "https://cat.com",
|
||||
privacyPolicyUrl: "https://privacy.com",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const img = container.querySelector("img");
|
||||
expect(img).not.toBe(null);
|
||||
expect(img?.src).toBe("https://cat.png/");
|
||||
expect(img?.width).toBe(100);
|
||||
});
|
||||
|
||||
it("should render new-to-wallet link", () => {
|
||||
const { container } = render(
|
||||
<StartScreen
|
||||
connectLocale={en}
|
||||
client={client}
|
||||
welcomeScreen={undefined}
|
||||
meta={{
|
||||
showThirdwebBranding: false,
|
||||
termsOfServiceUrl: "https://cat.com",
|
||||
privacyPolicyUrl: "https://privacy.com",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const a = container.querySelector("a");
|
||||
expect(a).not.toBe(null);
|
||||
expect(a?.href).toBe("https://blog.thirdweb.com/web3-wallet/");
|
||||
});
|
||||
|
||||
it("should render an svg icon if a custom image is not passed", () => {
|
||||
const { container } = render(
|
||||
<StartScreen
|
||||
connectLocale={en}
|
||||
client={client}
|
||||
welcomeScreen={undefined}
|
||||
meta={{
|
||||
showThirdwebBranding: false,
|
||||
termsOfServiceUrl: "https://cat.com",
|
||||
privacyPolicyUrl: "https://privacy.com",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const svg = container.querySelector("svg");
|
||||
expect(svg).not.toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatTokenBalance } from "./formatTokenBalance.js";
|
||||
|
||||
describe("formatTokenBalance", () => {
|
||||
const mockBalanceData = {
|
||||
symbol: "ETH",
|
||||
name: "Ethereum",
|
||||
decimals: 18,
|
||||
displayValue: "1.23456789",
|
||||
};
|
||||
|
||||
it("formats balance with symbol by default", () => {
|
||||
const result = formatTokenBalance(mockBalanceData);
|
||||
expect(result).toBe("1.23457 ETH");
|
||||
});
|
||||
|
||||
it("formats balance without symbol when showSymbol is false", () => {
|
||||
const result = formatTokenBalance(mockBalanceData, false);
|
||||
expect(result).toBe("1.23457");
|
||||
});
|
||||
|
||||
it("respects custom decimal places", () => {
|
||||
const result = formatTokenBalance(mockBalanceData, true, 3);
|
||||
expect(result).toBe("1.235 ETH");
|
||||
});
|
||||
|
||||
it("handles zero balance", () => {
|
||||
const zeroBalance = { ...mockBalanceData, displayValue: "0" };
|
||||
const result = formatTokenBalance(zeroBalance);
|
||||
expect(result).toBe("0 ETH");
|
||||
});
|
||||
|
||||
it("handles very small numbers", () => {
|
||||
const smallBalance = { ...mockBalanceData, displayValue: "0.0000001" };
|
||||
const result = formatTokenBalance(smallBalance);
|
||||
expect(result).toBe("0.00001 ETH");
|
||||
});
|
||||
|
||||
it("handles large numbers", () => {
|
||||
const largeBalance = { ...mockBalanceData, displayValue: "1234567.89" };
|
||||
const result = formatTokenBalance(largeBalance);
|
||||
expect(result).toBe("1234567.89 ETH");
|
||||
});
|
||||
|
||||
it("rounds up for very small non-zero values", () => {
|
||||
const tinyBalance = { ...mockBalanceData, displayValue: "0.000000001" };
|
||||
const result = formatTokenBalance(tinyBalance, true, 8);
|
||||
expect(result).toBe("1e-8 ETH");
|
||||
});
|
||||
|
||||
it("handles different token symbols", () => {
|
||||
const usdcBalance = {
|
||||
...mockBalanceData,
|
||||
symbol: "USDC",
|
||||
displayValue: "100.5",
|
||||
};
|
||||
const result = formatTokenBalance(usdcBalance);
|
||||
expect(result).toBe("100.5 USDC");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
|
||||
import {
|
||||
NATIVE_TOKEN,
|
||||
type NativeToken,
|
||||
isNativeToken,
|
||||
} from "./nativeToken.js"; // Replace with the actual file name// Assuming this is defined in a constants file
|
||||
|
||||
describe("isNativeToken", () => {
|
||||
it("should return true for NATIVE_TOKEN", () => {
|
||||
expect(isNativeToken(NATIVE_TOKEN)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for an object with nativeToken property", () => {
|
||||
const token: NativeToken = { nativeToken: true };
|
||||
expect(isNativeToken(token)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for a token with the native token address", () => {
|
||||
const token = { address: NATIVE_TOKEN_ADDRESS };
|
||||
expect(isNativeToken(token)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for a token with the native token address in uppercase", () => {
|
||||
const token = { address: NATIVE_TOKEN_ADDRESS.toUpperCase() };
|
||||
expect(isNativeToken(token)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for a non-native token", () => {
|
||||
const token = { address: "0x1234567890123456789012345678901234567890" };
|
||||
expect(isNativeToken(token)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for an empty object", () => {
|
||||
expect(isNativeToken({})).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for a token with a similar but incorrect address", () => {
|
||||
const token = { address: `${NATIVE_TOKEN_ADDRESS.slice(0, -1)}0` };
|
||||
expect(isNativeToken(token)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -38,7 +38,11 @@ export const CopyIcon: React.FC<{
|
||||
flex="row"
|
||||
center="both"
|
||||
>
|
||||
{showCheckIcon ? <CheckIcon /> : <CopyIconSVG />}
|
||||
{showCheckIcon ? (
|
||||
<CheckIcon className="tw-check-icon" />
|
||||
) : (
|
||||
<CopyIconSVG className="tw-copy-icon" />
|
||||
)}
|
||||
</Container>
|
||||
</div>
|
||||
</ToolTip>
|
||||
|
||||
@@ -11,6 +11,7 @@ export const Skeleton: React.FC<{
|
||||
height: string;
|
||||
width?: string;
|
||||
color?: keyof Theme["colors"];
|
||||
className?: string;
|
||||
}> = (props) => {
|
||||
return (
|
||||
<SkeletonDiv
|
||||
@@ -19,6 +20,7 @@ export const Skeleton: React.FC<{
|
||||
height: props.height,
|
||||
width: props.width || "auto",
|
||||
}}
|
||||
className={props.className || ""}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,54 +1,185 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ANVIL_CHAIN } from "~test/chains.js";
|
||||
import { render, screen, waitFor } from "~test/react-render.js";
|
||||
import { VITALIK_WALLET } from "~test/addresses.js";
|
||||
import { render } from "~test/react-render.js";
|
||||
import { TEST_CLIENT } from "~test/test-clients.js";
|
||||
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
|
||||
import { getWalletBalance } from "../../../../../wallets/utils/getWalletBalance.js";
|
||||
import { AccountBalance } from "./balance.js";
|
||||
import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
|
||||
import { sepolia } from "../../../../../chains/chain-definitions/sepolia.js";
|
||||
import { defineChain } from "../../../../../chains/utils.js";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
|
||||
import {
|
||||
AccountBalance,
|
||||
formatAccountFiatBalance,
|
||||
formatAccountTokenBalance,
|
||||
loadAccountBalance,
|
||||
} from "./balance.js";
|
||||
import { AccountProvider } from "./provider.js";
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("AccountBalance component", () => {
|
||||
it("format the balance properly", async () => {
|
||||
const roundTo1Decimal = (num: number): number => Math.round(num * 10) / 10;
|
||||
const balance = await getWalletBalance({
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
address: TEST_ACCOUNT_A.address,
|
||||
});
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
render(
|
||||
<AccountProvider address={TEST_ACCOUNT_A.address} client={TEST_CLIENT}>
|
||||
<AccountBalance chain={ANVIL_CHAIN} formatFn={roundTo1Decimal} />
|
||||
</AccountProvider>,
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("AccountBalance component", () => {
|
||||
it("should render", async () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AccountProvider address={VITALIK_WALLET} client={TEST_CLIENT}>
|
||||
<AccountBalance
|
||||
chain={ethereum}
|
||||
loadingComponent={<span />}
|
||||
fallbackComponent={<span />}
|
||||
formatFn={() => "nope"}
|
||||
/>
|
||||
</AccountProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
waitFor(() =>
|
||||
expect(
|
||||
screen.getByText(roundTo1Decimal(Number(balance.displayValue)), {
|
||||
exact: true,
|
||||
selector: "span",
|
||||
}),
|
||||
).toBeInTheDocument(),
|
||||
const spans = container.getElementsByTagName("span");
|
||||
expect(spans).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("`loadAccountBalance` should fetch the native balance properly", async () => {
|
||||
const result = await loadAccountBalance({
|
||||
client: TEST_CLIENT,
|
||||
chain: ethereum,
|
||||
address: VITALIK_WALLET,
|
||||
});
|
||||
|
||||
expect(Number.isNaN(result.balance)).toBe(false);
|
||||
expect(result.symbol).toBe("ETH");
|
||||
});
|
||||
|
||||
it("`loadAccountBalance` should fetch the token balance properly", async () => {
|
||||
const result = await loadAccountBalance({
|
||||
client: TEST_CLIENT,
|
||||
chain: ethereum,
|
||||
address: VITALIK_WALLET,
|
||||
// USDC
|
||||
tokenAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
||||
});
|
||||
|
||||
expect(Number.isNaN(result.balance)).toBe(false);
|
||||
expect(result.symbol).toBe("USDC");
|
||||
});
|
||||
|
||||
it("`loadAccountBalance` should fetch the fiat balance properly", async () => {
|
||||
const result = await loadAccountBalance({
|
||||
client: TEST_CLIENT,
|
||||
chain: ethereum,
|
||||
address: VITALIK_WALLET,
|
||||
showBalanceInFiat: "USD",
|
||||
});
|
||||
|
||||
expect(Number.isNaN(result.balance)).toBe(false);
|
||||
expect(result.symbol).toBe("$");
|
||||
});
|
||||
|
||||
it("`loadAccountBalance` should throw if `chain` is not passed", async () => {
|
||||
await expect(() =>
|
||||
loadAccountBalance({ client: TEST_CLIENT, address: VITALIK_WALLET }),
|
||||
).rejects.toThrowError("chain is required");
|
||||
});
|
||||
|
||||
it("`loadAccountBalance` should throw if `tokenAddress` is mistakenly passed as native token", async () => {
|
||||
await expect(() =>
|
||||
loadAccountBalance({
|
||||
client: TEST_CLIENT,
|
||||
address: VITALIK_WALLET,
|
||||
tokenAddress: NATIVE_TOKEN_ADDRESS,
|
||||
chain: ethereum,
|
||||
}),
|
||||
).rejects.toThrowError(
|
||||
`Invalid tokenAddress - cannot be ${NATIVE_TOKEN_ADDRESS}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should fallback properly if failed to load", () => {
|
||||
render(
|
||||
<AccountProvider address={TEST_ACCOUNT_A.address} client={TEST_CLIENT}>
|
||||
<AccountBalance
|
||||
chain={undefined}
|
||||
fallbackComponent={<span>oops</span>}
|
||||
/>
|
||||
</AccountProvider>,
|
||||
);
|
||||
it("`loadAccountBalance` should throw if `address` is not a valid evm address", async () => {
|
||||
await expect(() =>
|
||||
loadAccountBalance({
|
||||
client: TEST_CLIENT,
|
||||
address: "haha",
|
||||
chain: ethereum,
|
||||
}),
|
||||
).rejects.toThrowError("Invalid wallet address. Expected an EVM address");
|
||||
});
|
||||
|
||||
waitFor(() =>
|
||||
expect(
|
||||
screen.getByText("oops", {
|
||||
exact: true,
|
||||
selector: "span",
|
||||
}),
|
||||
).toBeInTheDocument(),
|
||||
it("`loadAccountBalance` should throw if `tokenAddress` is passed but is not a valid evm address", async () => {
|
||||
await expect(() =>
|
||||
loadAccountBalance({
|
||||
client: TEST_CLIENT,
|
||||
address: VITALIK_WALLET,
|
||||
tokenAddress: "haha",
|
||||
chain: ethereum,
|
||||
}),
|
||||
).rejects.toThrowError(
|
||||
"Invalid tokenAddress. Expected an EVM contract address",
|
||||
);
|
||||
});
|
||||
|
||||
it("`formatAccountTokenBalance` should display a rounded-up value + symbol", () => {
|
||||
expect(
|
||||
formatAccountTokenBalance({
|
||||
balance: 1.1999,
|
||||
symbol: "ETH",
|
||||
decimals: 1,
|
||||
}),
|
||||
).toBe("1.2 ETH");
|
||||
});
|
||||
|
||||
it("`formatAccountFiatBalance` should display fiat symbol followed by a rounded-up fiat value", () => {
|
||||
expect(
|
||||
formatAccountFiatBalance({ balance: 55.001, symbol: "$", decimals: 0 }),
|
||||
).toBe("$55");
|
||||
});
|
||||
|
||||
it("`loadAccountBalance` should throw if failed to load tokenBalance (native token)", async () => {
|
||||
await expect(() =>
|
||||
loadAccountBalance({
|
||||
client: TEST_CLIENT,
|
||||
address: VITALIK_WALLET,
|
||||
chain: defineChain(-1),
|
||||
}),
|
||||
).rejects.toThrowError(
|
||||
`Failed to retrieve native token balance for address: ${VITALIK_WALLET} on chainId:-1`,
|
||||
);
|
||||
});
|
||||
|
||||
it("`loadAccountBalance` should throw if failed to load tokenBalance (erc20 token)", async () => {
|
||||
await expect(() =>
|
||||
loadAccountBalance({
|
||||
client: TEST_CLIENT,
|
||||
address: VITALIK_WALLET,
|
||||
chain: defineChain(-1),
|
||||
tokenAddress: "0xFfEBd97b29AD3b2BecF8E554e4a638A56C6Bbd59",
|
||||
}),
|
||||
).rejects.toThrowError(
|
||||
`Failed to retrieve token: 0xFfEBd97b29AD3b2BecF8E554e4a638A56C6Bbd59 balance for address: ${VITALIK_WALLET} on chainId:-1`,
|
||||
);
|
||||
});
|
||||
|
||||
it("if fetching fiat value then it should throw if failed to resolve (native token)", async () => {
|
||||
await expect(() =>
|
||||
loadAccountBalance({
|
||||
client: TEST_CLIENT,
|
||||
address: VITALIK_WALLET,
|
||||
chain: sepolia,
|
||||
showBalanceInFiat: "USD",
|
||||
}),
|
||||
).rejects.toThrowError(
|
||||
`Failed to resolve fiat value for native token on chainId: ${sepolia.id}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("if fetching fiat value then it should throw if failed to resolve (erc20 token)", async () => {
|
||||
await expect(() =>
|
||||
loadAccountBalance({
|
||||
client: TEST_CLIENT,
|
||||
address: VITALIK_WALLET,
|
||||
chain: sepolia,
|
||||
showBalanceInFiat: "USD",
|
||||
// this is a random erc20 token on sepolia that vitalik's wallet owns
|
||||
tokenAddress: "0xFfEBd97b29AD3b2BecF8E554e4a638A56C6Bbd59",
|
||||
}),
|
||||
).rejects.toThrowError(
|
||||
`Failed to resolve fiat value for token: 0xFfEBd97b29AD3b2BecF8E554e4a638A56C6Bbd59 on chainId: ${sepolia.id}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||
import type { Address } from "abitype";
|
||||
import type React from "react";
|
||||
import type { JSX } from "react";
|
||||
import type { Chain } from "../../../../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../../../../client/client.js";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
|
||||
import { convertCryptoToFiat } from "../../../../../exports/pay.js";
|
||||
import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js";
|
||||
import { useActiveWalletChain } from "../../../../../react/core/hooks/wallets/useActiveWalletChain.js";
|
||||
import {
|
||||
type GetWalletBalanceResult,
|
||||
getWalletBalance,
|
||||
} from "../../../../../wallets/utils/getWalletBalance.js";
|
||||
import { isAddress } from "../../../../../utils/address.js";
|
||||
import { formatNumber } from "../../../../../utils/formatNumber.js";
|
||||
import { shortenLargeNumber } from "../../../../../utils/shortenLargeNumber.js";
|
||||
import { getWalletBalance } from "../../../../../wallets/utils/getWalletBalance.js";
|
||||
import { useAccountContext } from "./provider.js";
|
||||
|
||||
/**
|
||||
* @component
|
||||
* @wallet
|
||||
*/
|
||||
export type AccountBalanceInfo = {
|
||||
/**
|
||||
* Represents either token balance or fiat balance.
|
||||
*/
|
||||
balance: number;
|
||||
/**
|
||||
* Represents either token symbol or fiat symbol
|
||||
*/
|
||||
symbol: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for the AccountBalance component
|
||||
* @component
|
||||
@@ -33,7 +53,7 @@ export interface AccountBalanceProps
|
||||
* use this function to transform the balance display value like round up the number
|
||||
* Particularly useful to avoid overflowing-UI issues
|
||||
*/
|
||||
formatFn?: (num: number) => number;
|
||||
formatFn?: (props: AccountBalanceInfo) => string;
|
||||
/**
|
||||
* This component will be shown while the balance of the account is being fetched
|
||||
* If not passed, the component will return `null`.
|
||||
@@ -67,9 +87,14 @@ export interface AccountBalanceProps
|
||||
* Optional `useQuery` params
|
||||
*/
|
||||
queryOptions?: Omit<
|
||||
UseQueryOptions<GetWalletBalanceResult>,
|
||||
UseQueryOptions<AccountBalanceInfo>,
|
||||
"queryFn" | "queryKey"
|
||||
>;
|
||||
|
||||
/**
|
||||
* Show the token balance in a supported fiat currency (e.g "USD")
|
||||
*/
|
||||
showBalanceInFiat?: SupportedFiatCurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,18 +119,21 @@ export interface AccountBalanceProps
|
||||
*
|
||||
*
|
||||
* ### Format the balance (round up, shorten etc.)
|
||||
* The AccountBalance component accepts a `formatFn` which takes in a number and outputs a number
|
||||
* The function is used to modify the display value of the wallet balance
|
||||
* The AccountBalance component accepts a `formatFn` which takes in an object of type `AccountBalanceInfo` and outputs a string
|
||||
* The function is used to modify the display value of the wallet balance (either in crypto or fiat)
|
||||
*
|
||||
* ```tsx
|
||||
* const roundTo1Decimal = (num: number):number => Math.round(num * 10) / 10;
|
||||
* import type { AccountBalanceInfo } from "thirdweb/react";
|
||||
* import { formatNumber } from "thirdweb/utils";
|
||||
*
|
||||
* <AccountBalance formatFn={roundTo1Decimal} />
|
||||
* const format = (props: AccountInfoBalance):string => `${formatNumber(props.balance, 1)} ${props.symbol.toLowerCase()}`
|
||||
*
|
||||
* <AccountBalance formatFn={format} />
|
||||
* ```
|
||||
*
|
||||
* Result:
|
||||
* ```html
|
||||
* <span>1.1 ETH</span>
|
||||
* <span>1.1 eth</span> // the balance is rounded up to 1 decimal and the symbol is lowercased
|
||||
* ```
|
||||
*
|
||||
* ### Show a loading sign when the balance is being fetched
|
||||
@@ -149,10 +177,11 @@ export interface AccountBalanceProps
|
||||
export function AccountBalance({
|
||||
chain,
|
||||
tokenAddress,
|
||||
formatFn,
|
||||
loadingComponent,
|
||||
fallbackComponent,
|
||||
queryOptions,
|
||||
formatFn,
|
||||
showBalanceInFiat,
|
||||
...restProps
|
||||
}: AccountBalanceProps) {
|
||||
const { address, client } = useAccountContext();
|
||||
@@ -160,25 +189,20 @@ export function AccountBalance({
|
||||
const chainToLoad = chain || walletChain;
|
||||
const balanceQuery = useQuery({
|
||||
queryKey: [
|
||||
"walletBalance",
|
||||
"internal_account_balance",
|
||||
chainToLoad?.id || -1,
|
||||
address || "0x0",
|
||||
address,
|
||||
{ tokenAddress },
|
||||
showBalanceInFiat,
|
||||
] as const,
|
||||
queryFn: async () => {
|
||||
if (!chainToLoad) {
|
||||
throw new Error("chain is required");
|
||||
}
|
||||
if (!client) {
|
||||
throw new Error("client is required");
|
||||
}
|
||||
return getWalletBalance({
|
||||
queryFn: async (): Promise<AccountBalanceInfo> =>
|
||||
loadAccountBalance({
|
||||
chain: chainToLoad,
|
||||
client,
|
||||
address,
|
||||
tokenAddress,
|
||||
});
|
||||
},
|
||||
showBalanceInFiat,
|
||||
}),
|
||||
retry: false,
|
||||
...queryOptions,
|
||||
});
|
||||
@@ -191,13 +215,129 @@ export function AccountBalance({
|
||||
return fallbackComponent || null;
|
||||
}
|
||||
|
||||
const displayValue = formatFn
|
||||
? formatFn(Number(balanceQuery.data.displayValue))
|
||||
: balanceQuery.data.displayValue;
|
||||
// Prioritize using the formatFn from users
|
||||
if (formatFn) {
|
||||
return <span {...restProps}>{formatFn(balanceQuery.data)}</span>;
|
||||
}
|
||||
|
||||
if (showBalanceInFiat) {
|
||||
return (
|
||||
<span {...restProps}>
|
||||
{formatAccountFiatBalance({ ...balanceQuery.data, decimals: 0 })}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span {...restProps}>
|
||||
{displayValue} {balanceQuery.data.symbol}
|
||||
{formatAccountTokenBalance({
|
||||
...balanceQuery.data,
|
||||
decimals: balanceQuery.data.balance < 1 ? 3 : 2,
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Exported for tests
|
||||
*/
|
||||
export async function loadAccountBalance(props: {
|
||||
chain?: Chain;
|
||||
client: ThirdwebClient;
|
||||
address: Address;
|
||||
tokenAddress?: Address;
|
||||
showBalanceInFiat?: SupportedFiatCurrency;
|
||||
}): Promise<AccountBalanceInfo> {
|
||||
const { chain, client, address, tokenAddress, showBalanceInFiat } = props;
|
||||
if (!chain) {
|
||||
throw new Error("chain is required");
|
||||
}
|
||||
|
||||
if (
|
||||
tokenAddress &&
|
||||
tokenAddress?.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()
|
||||
) {
|
||||
throw new Error(`Invalid tokenAddress - cannot be ${NATIVE_TOKEN_ADDRESS}`);
|
||||
}
|
||||
|
||||
if (!isAddress(address)) {
|
||||
throw new Error("Invalid wallet address. Expected an EVM address");
|
||||
}
|
||||
|
||||
if (tokenAddress && !isAddress(tokenAddress)) {
|
||||
throw new Error("Invalid tokenAddress. Expected an EVM contract address");
|
||||
}
|
||||
|
||||
const tokenBalanceData = await getWalletBalance({
|
||||
chain,
|
||||
client,
|
||||
address,
|
||||
tokenAddress,
|
||||
}).catch(() => undefined);
|
||||
|
||||
if (!tokenBalanceData) {
|
||||
throw new Error(
|
||||
`Failed to retrieve ${tokenAddress ? `token: ${tokenAddress}` : "native token"} balance for address: ${address} on chainId:${chain.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (showBalanceInFiat) {
|
||||
const fiatData = await convertCryptoToFiat({
|
||||
fromAmount: Number(tokenBalanceData.displayValue),
|
||||
fromTokenAddress: tokenAddress || NATIVE_TOKEN_ADDRESS,
|
||||
to: showBalanceInFiat,
|
||||
chain,
|
||||
client,
|
||||
}).catch(() => undefined);
|
||||
|
||||
if (fiatData === undefined) {
|
||||
throw new Error(
|
||||
`Failed to resolve fiat value for ${tokenAddress ? `token: ${tokenAddress}` : "native token"} on chainId: ${chain.id}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
balance: fiatData?.result,
|
||||
symbol:
|
||||
new Intl.NumberFormat("en", {
|
||||
style: "currency",
|
||||
currency: showBalanceInFiat,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
})
|
||||
.formatToParts(0)
|
||||
.find((p) => p.type === "currency")?.value ||
|
||||
showBalanceInFiat.toUpperCase(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
balance: Number(tokenBalanceData.displayValue),
|
||||
symbol: tokenBalanceData.symbol,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the display balance for both crypto and fiat, in the Details button and Modal
|
||||
* If both crypto balance and fiat balance exist, we have to keep the string very short to avoid UI issues.
|
||||
* @internal
|
||||
* Used internally for the Details button and the Details Modal
|
||||
*/
|
||||
export function formatAccountTokenBalance(
|
||||
props: AccountBalanceInfo & { decimals: number },
|
||||
): string {
|
||||
const formattedTokenBalance = formatNumber(props.balance, props.decimals);
|
||||
return `${formattedTokenBalance} ${props.symbol}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used internally for the Details button and Details Modal
|
||||
* @internal
|
||||
*/
|
||||
export function formatAccountFiatBalance(
|
||||
props: AccountBalanceInfo & { decimals: number },
|
||||
) {
|
||||
const num = formatNumber(props.balance, props.decimals);
|
||||
// Need to keep them short to avoid UI overflow issues
|
||||
const formattedFiatBalance = shortenLargeNumber(num);
|
||||
return `${props.symbol}${formattedFiatBalance}`;
|
||||
}
|
||||
|
||||
@@ -35,4 +35,20 @@ describe.runIf(process.env.TW_SECRET_KEY)("AccountProvider component", () => {
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should throw an error if no address is provided", () => {
|
||||
expect(() => {
|
||||
render(
|
||||
<AccountProvider
|
||||
// biome-ignore lint/suspicious/noExplicitAny: testing invalid input
|
||||
address={undefined as any}
|
||||
client={TEST_CLIENT}
|
||||
>
|
||||
<AccountAddress />
|
||||
</AccountProvider>,
|
||||
);
|
||||
}).toThrowError(
|
||||
"AccountProvider: No address passed. Ensure an address is always provided to the AccountProvider",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,11 @@ const AccountProviderContext = /* @__PURE__ */ createContext<
|
||||
export function AccountProvider(
|
||||
props: React.PropsWithChildren<AccountProviderProps>,
|
||||
) {
|
||||
if (!props.address) {
|
||||
throw new Error(
|
||||
"AccountProvider: No address passed. Ensure an address is always provided to the AccountProvider",
|
||||
);
|
||||
}
|
||||
return (
|
||||
<AccountProviderContext.Provider value={props}>
|
||||
{props.children}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DOODLES_CONTRACT,
|
||||
DROP1155_CONTRACT,
|
||||
UNISWAPV3_FACTORY_CONTRACT,
|
||||
} from "~test/test-contracts.js";
|
||||
import { fetchNftDescription } from "./description.js";
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("NFTDescription", () => {
|
||||
it("fetchNftDescription should work with ERC721", async () => {
|
||||
const desc = await fetchNftDescription({
|
||||
contract: DOODLES_CONTRACT,
|
||||
tokenId: 0n,
|
||||
});
|
||||
expect(desc).toBe(
|
||||
"A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.",
|
||||
);
|
||||
});
|
||||
|
||||
it("fetchNftDescription should work with ERC1155", async () => {
|
||||
const desc = await fetchNftDescription({
|
||||
contract: DROP1155_CONTRACT,
|
||||
tokenId: 0n,
|
||||
});
|
||||
expect(desc).toBe("");
|
||||
});
|
||||
|
||||
it("fetchNftDescription should respect descriptionResolver as a string", async () => {
|
||||
const desc = await fetchNftDescription({
|
||||
contract: DOODLES_CONTRACT,
|
||||
tokenId: 0n,
|
||||
descriptionResolver: "string",
|
||||
});
|
||||
expect(desc).toBe("string");
|
||||
});
|
||||
|
||||
it("fetchNftDescription should respect descriptionResolver as a non-async function", async () => {
|
||||
const desc = await fetchNftDescription({
|
||||
contract: DOODLES_CONTRACT,
|
||||
tokenId: 0n,
|
||||
descriptionResolver: () => "non-async",
|
||||
});
|
||||
expect(desc).toBe("non-async");
|
||||
});
|
||||
|
||||
it("fetchNftDescription should respect descriptionResolver as a async function", async () => {
|
||||
const desc = await fetchNftDescription({
|
||||
contract: DOODLES_CONTRACT,
|
||||
tokenId: 0n,
|
||||
descriptionResolver: async () => "async",
|
||||
});
|
||||
expect(desc).toBe("async");
|
||||
});
|
||||
|
||||
it("fetchNftDescription should throw error if failed to resolve nft info", async () => {
|
||||
await expect(() =>
|
||||
fetchNftDescription({
|
||||
contract: UNISWAPV3_FACTORY_CONTRACT,
|
||||
tokenId: 0n,
|
||||
}),
|
||||
).rejects.toThrowError("Failed to resolve NFT info");
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import type { UseQueryOptions } from "@tanstack/react-query";
|
||||
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||
import type { JSX } from "react";
|
||||
import type { NFT } from "../../../../../utils/nft/parseNft.js";
|
||||
import { useNftInfo } from "./hooks.js";
|
||||
import type { ThirdwebContract } from "../../../../../contract/contract.js";
|
||||
import { useNFTContext } from "./provider.js";
|
||||
import { getNFTInfo } from "./utils.js";
|
||||
|
||||
export interface NFTDescriptionProps
|
||||
extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
|
||||
@@ -13,7 +13,12 @@ export interface NFTDescriptionProps
|
||||
/**
|
||||
* Optional `useQuery` params
|
||||
*/
|
||||
queryOptions?: Omit<UseQueryOptions<NFT>, "queryFn" | "queryKey">;
|
||||
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
|
||||
/**
|
||||
* This prop can be a string or a (async) function that resolves to a string, representing the description of the NFT
|
||||
* This is particularly useful if you already have a way to fetch the data.
|
||||
*/
|
||||
descriptionResolver?: string | (() => string) | (() => Promise<string>);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,6 +63,21 @@ export interface NFTDescriptionProps
|
||||
* </NFTProvider>
|
||||
* ```
|
||||
*
|
||||
* ### Override the description with the `descriptionResolver` prop
|
||||
* If you already have the url, you can skip the network requests and pass it directly to the NFTDescription
|
||||
* ```tsx
|
||||
* <NFTDescription descriptionResolver="The desc of the NFT" />
|
||||
* ```
|
||||
*
|
||||
* You can also pass in your own custom (async) function that retrieves the description
|
||||
* ```tsx
|
||||
* const getDescription = async () => {
|
||||
* // ...
|
||||
* return description;
|
||||
* };
|
||||
*
|
||||
* <NFTDescription descriptionResolver={getDescription} />
|
||||
* ```
|
||||
* @component
|
||||
* @nft
|
||||
* @beta
|
||||
@@ -66,22 +86,61 @@ export function NFTDescription({
|
||||
loadingComponent,
|
||||
fallbackComponent,
|
||||
queryOptions,
|
||||
descriptionResolver,
|
||||
...restProps
|
||||
}: NFTDescriptionProps) {
|
||||
const { contract, tokenId } = useNFTContext();
|
||||
const nftQuery = useNftInfo({
|
||||
contract,
|
||||
tokenId,
|
||||
queryOptions,
|
||||
const descQuery = useQuery({
|
||||
queryKey: [
|
||||
"_internal_nft_description_",
|
||||
contract.chain.id,
|
||||
tokenId.toString(),
|
||||
{
|
||||
resolver:
|
||||
typeof descriptionResolver === "string"
|
||||
? descriptionResolver
|
||||
: typeof descriptionResolver === "function"
|
||||
? descriptionResolver.toString()
|
||||
: undefined,
|
||||
},
|
||||
],
|
||||
queryFn: async (): Promise<string> =>
|
||||
fetchNftDescription({ descriptionResolver, contract, tokenId }),
|
||||
...queryOptions,
|
||||
});
|
||||
|
||||
if (nftQuery.isLoading) {
|
||||
if (descQuery.isLoading) {
|
||||
return loadingComponent || null;
|
||||
}
|
||||
|
||||
if (!nftQuery.data?.metadata?.description) {
|
||||
if (!descQuery.data) {
|
||||
return fallbackComponent || null;
|
||||
}
|
||||
|
||||
return <span {...restProps}>{nftQuery.data.metadata.description}</span>;
|
||||
return <span {...restProps}>{descQuery.data}</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Exported for tests
|
||||
*/
|
||||
export async function fetchNftDescription(props: {
|
||||
descriptionResolver?: string | (() => string) | (() => Promise<string>);
|
||||
contract: ThirdwebContract;
|
||||
tokenId: bigint;
|
||||
}): Promise<string> {
|
||||
const { descriptionResolver, contract, tokenId } = props;
|
||||
if (typeof descriptionResolver === "string") {
|
||||
return descriptionResolver;
|
||||
}
|
||||
if (typeof descriptionResolver === "function") {
|
||||
return descriptionResolver();
|
||||
}
|
||||
const nft = await getNFTInfo({ contract, tokenId }).catch(() => undefined);
|
||||
if (!nft) {
|
||||
throw new Error("Failed to resolve NFT info");
|
||||
}
|
||||
if (typeof nft.metadata.description !== "string") {
|
||||
throw new Error("Failed to resolve NFT description");
|
||||
}
|
||||
return nft.metadata.description;
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||
import { getNFT as getNFT721 } from "../../../../../extensions/erc721/read/getNFT.js";
|
||||
import { getNFT as getNFT1155 } from "../../../../../extensions/erc1155/read/getNFT.js";
|
||||
import type { NFT } from "../../../../../utils/nft/parseNft.js";
|
||||
import type { NFTProviderProps } from "./provider.js";
|
||||
|
||||
/**
|
||||
* @internal Only used for the NFT prebuilt components
|
||||
*/
|
||||
export function useNftInfo(
|
||||
props: NFTProviderProps & {
|
||||
queryOptions?: Omit<UseQueryOptions<NFT>, "queryFn" | "queryKey">;
|
||||
},
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
"__nft_component_internal__",
|
||||
props.contract.chain.id,
|
||||
props.contract.address,
|
||||
props.tokenId.toString(),
|
||||
],
|
||||
queryFn: () =>
|
||||
getNFTInfo({ contract: props.contract, tokenId: props.tokenId }),
|
||||
...props.queryOptions,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export async function getNFTInfo(options: NFTProviderProps): Promise<NFT> {
|
||||
const nft = await Promise.allSettled([
|
||||
getNFT721(options),
|
||||
getNFT1155(options),
|
||||
]).then(([possibleNFT721, possibleNFT1155]) => {
|
||||
// getNFT extension always return an NFT object
|
||||
// so we need to check if the tokenURI exists
|
||||
if (
|
||||
possibleNFT721.status === "fulfilled" &&
|
||||
possibleNFT721.value.tokenURI
|
||||
) {
|
||||
return possibleNFT721.value;
|
||||
}
|
||||
if (
|
||||
possibleNFT1155.status === "fulfilled" &&
|
||||
possibleNFT1155.value.tokenURI
|
||||
) {
|
||||
return possibleNFT1155.value;
|
||||
}
|
||||
throw new Error("Failed to load NFT metadata");
|
||||
});
|
||||
return nft;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DOODLES_CONTRACT,
|
||||
DROP1155_CONTRACT,
|
||||
UNISWAPV3_FACTORY_CONTRACT,
|
||||
} from "~test/test-contracts.js";
|
||||
import { fetchNftMedia } from "./media.js";
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("NFTMedia", () => {
|
||||
it("fetchNftMedia should work with ERC721", async () => {
|
||||
const desc = await fetchNftMedia({
|
||||
contract: DOODLES_CONTRACT,
|
||||
tokenId: 0n,
|
||||
});
|
||||
expect(desc).toStrictEqual({
|
||||
src: "ipfs://QmUEfFfwAh4wyB5UfHCVPUxis4j4Q4kJXtm5x5p3g1fVUn",
|
||||
poster: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("fetchNftMedia should work with ERC1155", async () => {
|
||||
const desc = await fetchNftMedia({
|
||||
contract: DROP1155_CONTRACT,
|
||||
tokenId: 0n,
|
||||
});
|
||||
expect(desc).toStrictEqual({
|
||||
src: "ipfs://QmeGCqV1mSHTZrvuFzW1XZdCRRGXB6AmSotTqHoxA2xfDo/1.mp4",
|
||||
poster: "ipfs://QmeGCqV1mSHTZrvuFzW1XZdCRRGXB6AmSotTqHoxA2xfDo/0.png",
|
||||
});
|
||||
});
|
||||
|
||||
it("fetchNftMedia should respect mediaResolver as a string", async () => {
|
||||
const desc = await fetchNftMedia({
|
||||
contract: DOODLES_CONTRACT,
|
||||
tokenId: 0n,
|
||||
mediaResolver: {
|
||||
src: "string",
|
||||
poster: undefined,
|
||||
},
|
||||
});
|
||||
expect(desc).toStrictEqual({ src: "string", poster: undefined });
|
||||
});
|
||||
|
||||
it("fetchNftMedia should respect mediaResolver as a non-async function", async () => {
|
||||
const desc = await fetchNftMedia({
|
||||
contract: DOODLES_CONTRACT,
|
||||
tokenId: 0n,
|
||||
mediaResolver: () => ({
|
||||
src: "non-async",
|
||||
poster: undefined,
|
||||
}),
|
||||
});
|
||||
expect(desc).toStrictEqual({ src: "non-async", poster: undefined });
|
||||
});
|
||||
|
||||
it("fetchNftMedia should respect mediaResolver as a async function", async () => {
|
||||
const desc = await fetchNftMedia({
|
||||
contract: DOODLES_CONTRACT,
|
||||
tokenId: 0n,
|
||||
mediaResolver: async () =>
|
||||
await {
|
||||
src: "async",
|
||||
poster: undefined,
|
||||
},
|
||||
});
|
||||
expect(desc).toStrictEqual({ src: "async", poster: undefined });
|
||||
});
|
||||
|
||||
it("fetchNftMedia should throw error if failed to resolve nft info", async () => {
|
||||
await expect(() =>
|
||||
fetchNftMedia({
|
||||
contract: UNISWAPV3_FACTORY_CONTRACT,
|
||||
tokenId: 0n,
|
||||
}),
|
||||
).rejects.toThrowError("Failed to resolve NFT info");
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,25 @@
|
||||
import type { UseQueryOptions } from "@tanstack/react-query";
|
||||
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||
import type { JSX } from "react";
|
||||
import type { NFT } from "../../../../../utils/nft/parseNft.js";
|
||||
import type { ThirdwebContract } from "../../../../../contract/contract.js";
|
||||
import { MediaRenderer } from "../../MediaRenderer/MediaRenderer.js";
|
||||
import type { MediaRendererProps } from "../../MediaRenderer/types.js";
|
||||
import { useNftInfo } from "./hooks.js";
|
||||
import { useNFTContext } from "./provider.js";
|
||||
import { getNFTInfo } from "./utils.js";
|
||||
|
||||
/**
|
||||
* @component
|
||||
* @beta
|
||||
* @wallet
|
||||
*/
|
||||
export type NFTMediaInfo = {
|
||||
src: string;
|
||||
poster: string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* @component
|
||||
* @beta
|
||||
* @wallet
|
||||
* The props for the <NFTMedia /> component
|
||||
* It is similar to the [`MediaRendererProps`](https://portal.thirdweb.com/references/typescript/v5/MediaRendererProps)
|
||||
* (excluding `src`, `poster` and `client`) that you can
|
||||
@@ -22,7 +34,16 @@ export type NFTMediaProps = Omit<
|
||||
/**
|
||||
* Optional `useQuery` params
|
||||
*/
|
||||
queryOptions?: Omit<UseQueryOptions<NFT>, "queryFn" | "queryKey">;
|
||||
queryOptions?: Omit<UseQueryOptions<NFTMediaInfo>, "queryFn" | "queryKey">;
|
||||
/**
|
||||
* This prop can be a string or a (async) function that resolves to a string, representing the media url of the NFT
|
||||
* This is particularly useful if you already have a way to fetch the image.
|
||||
* In case of function, the function must resolve to an object of type `NFTMediaInfo`
|
||||
*/
|
||||
mediaResolver?:
|
||||
| NFTMediaInfo
|
||||
| (() => NFTMediaInfo)
|
||||
| (() => Promise<NFTMediaInfo>);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -76,6 +97,26 @@ export type NFTMediaProps = Omit<
|
||||
* ```tsx
|
||||
* <NFTMedia style={{ borderRadius: "8px" }} className="mx-auto" />
|
||||
* ```
|
||||
*
|
||||
* ### Override the media with the `mediaResolver` prop
|
||||
* If you already have the url, you can skip the network requests and pass it directly to the NFTMedia
|
||||
* ```tsx
|
||||
* <NFTMedia mediaResolver={{
|
||||
* src: "/cat_video.mp4",
|
||||
* // Poster is applicable to medias that are videos and audios
|
||||
* poster: "/cat-image.png",
|
||||
* }} />
|
||||
* ```
|
||||
*
|
||||
* You can also pass in your own custom (async) function that retrieves the media url
|
||||
* ```tsx
|
||||
* const getMedia = async () => {
|
||||
* const url = getNFTMedia(props);
|
||||
* return url;
|
||||
* };
|
||||
*
|
||||
* <NFTMedia mediaResolver={getMedia} />
|
||||
* ```
|
||||
* @nft
|
||||
* @beta
|
||||
*/
|
||||
@@ -83,37 +124,82 @@ export function NFTMedia({
|
||||
loadingComponent,
|
||||
fallbackComponent,
|
||||
queryOptions,
|
||||
mediaResolver,
|
||||
...mediaRendererProps
|
||||
}: NFTMediaProps) {
|
||||
const { contract, tokenId } = useNFTContext();
|
||||
const nftQuery = useNftInfo({
|
||||
contract,
|
||||
tokenId,
|
||||
queryOptions,
|
||||
const mediaQuery = useQuery({
|
||||
queryKey: [
|
||||
"_internal_nft_media_",
|
||||
contract.chain.id,
|
||||
tokenId.toString(),
|
||||
{
|
||||
resolver:
|
||||
typeof mediaResolver === "object"
|
||||
? mediaResolver
|
||||
: typeof mediaResolver === "function"
|
||||
? mediaResolver.toString()
|
||||
: undefined,
|
||||
},
|
||||
],
|
||||
queryFn: async (): Promise<NFTMediaInfo> =>
|
||||
fetchNftMedia({ mediaResolver, contract, tokenId }),
|
||||
...queryOptions,
|
||||
});
|
||||
|
||||
if (nftQuery.isLoading) {
|
||||
if (mediaQuery.isLoading) {
|
||||
return loadingComponent || null;
|
||||
}
|
||||
|
||||
if (!nftQuery.data) {
|
||||
return fallbackComponent || null;
|
||||
}
|
||||
|
||||
const animation_url = nftQuery.data.metadata.animation_url;
|
||||
const image =
|
||||
nftQuery.data.metadata.image || nftQuery.data.metadata.image_url;
|
||||
|
||||
if (!animation_url && !image) {
|
||||
if (!mediaQuery.data) {
|
||||
return fallbackComponent || null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MediaRenderer
|
||||
client={contract.client}
|
||||
src={animation_url || image}
|
||||
poster={image}
|
||||
src={mediaQuery.data.src}
|
||||
poster={mediaQuery.data.poster}
|
||||
{...mediaRendererProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Exported for tests only
|
||||
*/
|
||||
export async function fetchNftMedia(props: {
|
||||
mediaResolver?:
|
||||
| NFTMediaInfo
|
||||
| (() => NFTMediaInfo)
|
||||
| (() => Promise<NFTMediaInfo>);
|
||||
contract: ThirdwebContract;
|
||||
tokenId: bigint;
|
||||
}): Promise<{ src: string; poster: string | undefined }> {
|
||||
const { mediaResolver, contract, tokenId } = props;
|
||||
if (typeof mediaResolver === "object") {
|
||||
return mediaResolver;
|
||||
}
|
||||
if (typeof mediaResolver === "function") {
|
||||
return mediaResolver();
|
||||
}
|
||||
const nft = await getNFTInfo({ contract, tokenId }).catch(() => undefined);
|
||||
if (!nft) {
|
||||
throw new Error("Failed to resolve NFT info");
|
||||
}
|
||||
const animation_url = nft.metadata.animation_url;
|
||||
const image = nft.metadata.image || nft.metadata.image_url;
|
||||
if (animation_url) {
|
||||
return {
|
||||
src: animation_url,
|
||||
poster: image || undefined,
|
||||
};
|
||||
}
|
||||
if (image) {
|
||||
return {
|
||||
src: image,
|
||||
poster: undefined,
|
||||
};
|
||||
}
|
||||
throw new Error("Failed to resolve NFT media");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DOODLES_CONTRACT,
|
||||
DROP1155_CONTRACT,
|
||||
UNISWAPV3_FACTORY_CONTRACT,
|
||||
} from "~test/test-contracts.js";
|
||||
import { fetchNftName } from "./name.js";
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("NFTName", () => {
|
||||
it("fetchNftName should work with ERC721", async () => {
|
||||
const desc = await fetchNftName({
|
||||
contract: DOODLES_CONTRACT,
|
||||
tokenId: 0n,
|
||||
});
|
||||
expect(desc).toBe("Doodle #0");
|
||||
});
|
||||
|
||||
it("fetchNftName should work with ERC1155", async () => {
|
||||
const desc = await fetchNftName({
|
||||
contract: DROP1155_CONTRACT,
|
||||
tokenId: 0n,
|
||||
});
|
||||
expect(desc).toBe("Aura OG");
|
||||
});
|
||||
|
||||
it("fetchNftName should respect nameResolver as a string", async () => {
|
||||
const desc = await fetchNftName({
|
||||
contract: DOODLES_CONTRACT,
|
||||
tokenId: 0n,
|
||||
nameResolver: "string",
|
||||
});
|
||||
expect(desc).toBe("string");
|
||||
});
|
||||
|
||||
it("fetchNftName should respect nameResolver as a non-async function", async () => {
|
||||
const desc = await fetchNftName({
|
||||
contract: DOODLES_CONTRACT,
|
||||
tokenId: 0n,
|
||||
nameResolver: () => "non-async",
|
||||
});
|
||||
expect(desc).toBe("non-async");
|
||||
});
|
||||
|
||||
it("fetchNftName should respect nameResolver as a async function", async () => {
|
||||
const desc = await fetchNftName({
|
||||
contract: DOODLES_CONTRACT,
|
||||
tokenId: 0n,
|
||||
nameResolver: async () => "async",
|
||||
});
|
||||
expect(desc).toBe("async");
|
||||
});
|
||||
|
||||
it("fetchNftName should throw error if failed to resolve nft info", async () => {
|
||||
await expect(() =>
|
||||
fetchNftName({
|
||||
contract: UNISWAPV3_FACTORY_CONTRACT,
|
||||
tokenId: 0n,
|
||||
}),
|
||||
).rejects.toThrowError("Failed to resolve NFT info");
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { UseQueryOptions } from "@tanstack/react-query";
|
||||
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||
import type { JSX } from "react";
|
||||
import type { NFT } from "../../../../../utils/nft/parseNft.js";
|
||||
import { useNftInfo } from "./hooks.js";
|
||||
import type { ThirdwebContract } from "../../../../../contract/contract.js";
|
||||
import { useNFTContext } from "./provider.js";
|
||||
import { getNFTInfo } from "./utils.js";
|
||||
|
||||
export interface NFTNameProps
|
||||
extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
|
||||
@@ -11,7 +11,12 @@ export interface NFTNameProps
|
||||
/**
|
||||
* Optional `useQuery` params
|
||||
*/
|
||||
queryOptions?: Omit<UseQueryOptions<NFT>, "queryFn" | "queryKey">;
|
||||
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
|
||||
/**
|
||||
* This prop can be a string or a (async) function that resolves to a string, representing the name of the NFT
|
||||
* This is particularly useful if you already have a way to fetch the name of the NFT.
|
||||
*/
|
||||
nameResolver?: string | (() => string) | (() => Promise<string>);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +61,22 @@ export interface NFTNameProps
|
||||
* </NFTProvider>
|
||||
* ```
|
||||
*
|
||||
* ### Override the name with the `nameResolver` prop
|
||||
* If you already have the name, you can skip the network requests and pass it directly to the NFTName
|
||||
* ```tsx
|
||||
* <NFTName nameResolver="Doodles #1" />
|
||||
* ```
|
||||
*
|
||||
* You can also pass in your own custom (async) function that retrieves the name
|
||||
* ```tsx
|
||||
* const getName = async () => {
|
||||
* // ...
|
||||
* return name;
|
||||
* };
|
||||
*
|
||||
* <NFTName nameResolver={getName} />
|
||||
* ```
|
||||
*
|
||||
* @nft
|
||||
* @component
|
||||
* @beta
|
||||
@@ -64,22 +85,61 @@ export function NFTName({
|
||||
loadingComponent,
|
||||
fallbackComponent,
|
||||
queryOptions,
|
||||
nameResolver,
|
||||
...restProps
|
||||
}: NFTNameProps) {
|
||||
const { contract, tokenId } = useNFTContext();
|
||||
|
||||
const nftQuery = useNftInfo({
|
||||
contract,
|
||||
tokenId,
|
||||
queryOptions,
|
||||
const nameQuery = useQuery({
|
||||
queryKey: [
|
||||
"_internal_nft_name_",
|
||||
contract.chain.id,
|
||||
tokenId.toString(),
|
||||
{
|
||||
resolver:
|
||||
typeof nameResolver === "string"
|
||||
? nameResolver
|
||||
: typeof nameResolver === "function"
|
||||
? nameResolver.toString()
|
||||
: undefined,
|
||||
},
|
||||
],
|
||||
queryFn: async (): Promise<string> =>
|
||||
fetchNftName({ nameResolver, contract, tokenId }),
|
||||
...queryOptions,
|
||||
});
|
||||
|
||||
if (nftQuery.isLoading) {
|
||||
if (nameQuery.isLoading) {
|
||||
return loadingComponent || null;
|
||||
}
|
||||
|
||||
if (!nftQuery.data?.metadata?.name) {
|
||||
if (!nameQuery.data) {
|
||||
return fallbackComponent || null;
|
||||
}
|
||||
return <span {...restProps}>{nftQuery.data.metadata.name}</span>;
|
||||
return <span {...restProps}>{nameQuery.data}</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Exported for tests
|
||||
*/
|
||||
export async function fetchNftName(props: {
|
||||
nameResolver?: string | (() => string) | (() => Promise<string>);
|
||||
contract: ThirdwebContract;
|
||||
tokenId: bigint;
|
||||
}): Promise<string> {
|
||||
const { nameResolver, contract, tokenId } = props;
|
||||
if (typeof nameResolver === "string") {
|
||||
return nameResolver;
|
||||
}
|
||||
if (typeof nameResolver === "function") {
|
||||
return nameResolver();
|
||||
}
|
||||
const nft = await getNFTInfo({ contract, tokenId }).catch(() => undefined);
|
||||
if (!nft) {
|
||||
throw new Error("Failed to resolve NFT info");
|
||||
}
|
||||
if (typeof nft.metadata.name !== "string") {
|
||||
throw new Error("Failed to resolve NFT name");
|
||||
}
|
||||
return nft.metadata.name;
|
||||
}
|
||||
|
||||
+34
-42
@@ -1,48 +1,13 @@
|
||||
import { useContext } from "react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { render, screen, waitFor } from "~test/react-render.js";
|
||||
import { type FC, useContext } from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { render, renderHook, screen, waitFor } from "~test/react-render.js";
|
||||
import { DOODLES_CONTRACT } from "~test/test-contracts.js";
|
||||
import { getNFTInfo } from "./hooks.js";
|
||||
import { NFTDescription } from "./description.js";
|
||||
import { NFTMedia } from "./media.js";
|
||||
import { NFTName } from "./name.js";
|
||||
import { NFTProvider, NFTProviderContext } from "./provider.js";
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("NFT prebuilt component", () => {
|
||||
it("should fetch the NFT metadata", async () => {
|
||||
const nft = await getNFTInfo({
|
||||
contract: DOODLES_CONTRACT,
|
||||
tokenId: 1n,
|
||||
});
|
||||
expect(nft.metadata).toStrictEqual({
|
||||
attributes: [
|
||||
{
|
||||
trait_type: "face",
|
||||
value: "holographic beard",
|
||||
},
|
||||
{
|
||||
trait_type: "hair",
|
||||
value: "white bucket cap",
|
||||
},
|
||||
{
|
||||
trait_type: "body",
|
||||
value: "purple sweater with satchel",
|
||||
},
|
||||
{
|
||||
trait_type: "background",
|
||||
value: "grey",
|
||||
},
|
||||
{
|
||||
trait_type: "head",
|
||||
value: "gradient 2",
|
||||
},
|
||||
],
|
||||
description:
|
||||
"A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.",
|
||||
image: "ipfs://QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9",
|
||||
name: "Doodle #1",
|
||||
});
|
||||
});
|
||||
import { NFTProvider, NFTProviderContext, useNFTContext } from "./provider.js";
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("NFTProvider", () => {
|
||||
it("should render children correctly", () => {
|
||||
render(
|
||||
<NFTProvider contract={DOODLES_CONTRACT} tokenId={0n}>
|
||||
@@ -99,7 +64,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("NFT prebuilt component", () => {
|
||||
it("should render the NFT description", () => {
|
||||
render(
|
||||
<NFTProvider contract={DOODLES_CONTRACT} tokenId={1n}>
|
||||
<NFTName />
|
||||
<NFTDescription />
|
||||
</NFTProvider>,
|
||||
);
|
||||
|
||||
@@ -111,4 +76,31 @@ describe.runIf(process.env.TW_SECRET_KEY)("NFT prebuilt component", () => {
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it("useNFTContext should return the context value when used within NFTProvider", () => {
|
||||
const wrapper: FC = ({ children }: React.PropsWithChildren) => (
|
||||
<NFTProvider contract={DOODLES_CONTRACT} tokenId={0n}>
|
||||
{children}
|
||||
</NFTProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useNFTContext(), { wrapper });
|
||||
|
||||
expect(result.current.contract).toStrictEqual(DOODLES_CONTRACT);
|
||||
expect(result.current.tokenId).toBe(0n);
|
||||
});
|
||||
|
||||
it("useNFTContext should throw an error when used outside of NFTProvider", () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, "error")
|
||||
.mockImplementation(() => {});
|
||||
|
||||
expect(() => {
|
||||
renderHook(() => useNFTContext());
|
||||
}).toThrow(
|
||||
"NFTProviderContext not found. Make sure you are using NFTMedia, NFTDescription, etc. inside a <NFTProvider /> component",
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DOODLES_CONTRACT,
|
||||
DROP1155_CONTRACT,
|
||||
UNISWAPV3_FACTORY_CONTRACT,
|
||||
} from "~test/test-contracts.js";
|
||||
import { getNFTInfo } from "./utils.js";
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("getNFTInfo", () => {
|
||||
it("should work with ERC721", async () => {
|
||||
const nft = await getNFTInfo({
|
||||
contract: DOODLES_CONTRACT,
|
||||
tokenId: 0n,
|
||||
});
|
||||
expect(nft).toStrictEqual({
|
||||
id: 0n,
|
||||
metadata: {
|
||||
attributes: [
|
||||
{
|
||||
trait_type: "face",
|
||||
value: "mustache",
|
||||
},
|
||||
{
|
||||
trait_type: "hair",
|
||||
value: "purple long",
|
||||
},
|
||||
{
|
||||
trait_type: "body",
|
||||
value: "blue and yellow jacket",
|
||||
},
|
||||
{
|
||||
trait_type: "background",
|
||||
value: "green",
|
||||
},
|
||||
{
|
||||
trait_type: "head",
|
||||
value: "tan",
|
||||
},
|
||||
],
|
||||
description:
|
||||
"A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.",
|
||||
image: "ipfs://QmUEfFfwAh4wyB5UfHCVPUxis4j4Q4kJXtm5x5p3g1fVUn",
|
||||
name: "Doodle #0",
|
||||
},
|
||||
owner: null,
|
||||
tokenURI: "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/0",
|
||||
type: "ERC721",
|
||||
});
|
||||
});
|
||||
|
||||
it("should work with ERC1155", async () => {
|
||||
const nft = await getNFTInfo({
|
||||
contract: DROP1155_CONTRACT,
|
||||
tokenId: 0n,
|
||||
});
|
||||
expect(nft).toStrictEqual({
|
||||
id: 0n,
|
||||
metadata: {
|
||||
animation_url:
|
||||
"ipfs://QmeGCqV1mSHTZrvuFzW1XZdCRRGXB6AmSotTqHoxA2xfDo/1.mp4",
|
||||
attributes: [
|
||||
{
|
||||
trait_type: "Revenue Share",
|
||||
value: "40%",
|
||||
},
|
||||
{
|
||||
trait_type: "Max Supply",
|
||||
value: "50",
|
||||
},
|
||||
{
|
||||
trait_type: "Max Per Wallet",
|
||||
value: "1",
|
||||
},
|
||||
],
|
||||
background_color: "",
|
||||
description: "",
|
||||
external_url: "https://auraexchange.org",
|
||||
image: "ipfs://QmeGCqV1mSHTZrvuFzW1XZdCRRGXB6AmSotTqHoxA2xfDo/0.png",
|
||||
name: "Aura OG",
|
||||
},
|
||||
owner: null,
|
||||
supply: 33n,
|
||||
tokenURI: "ipfs://QmNgevzVNwJWJdErFY2B7KsuKdJz3gVuBraNKaSxPktLh5/0",
|
||||
type: "ERC1155",
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw error if failed to load nft info", async () => {
|
||||
await expect(() =>
|
||||
getNFTInfo({ contract: UNISWAPV3_FACTORY_CONTRACT, tokenId: 0n }),
|
||||
).rejects.toThrowError("Failed to load NFT metadata");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import {} from "@tanstack/react-query";
|
||||
import { getNFT as getNFT721 } from "../../../../../extensions/erc721/read/getNFT.js";
|
||||
import { getNFT as getNFT1155 } from "../../../../../extensions/erc1155/read/getNFT.js";
|
||||
import type { NFT } from "../../../../../utils/nft/parseNft.js";
|
||||
import { withCache } from "../../../../../utils/promise/withCache.js";
|
||||
import type { NFTProviderProps } from "./provider.js";
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export async function getNFTInfo(options: NFTProviderProps): Promise<NFT> {
|
||||
return withCache(
|
||||
async () => {
|
||||
const nft = await Promise.allSettled([
|
||||
getNFT721(options),
|
||||
getNFT1155(options),
|
||||
]).then(([possibleNFT721, possibleNFT1155]) => {
|
||||
// getNFT extension always return an NFT object
|
||||
// so we need to check if the tokenURI exists
|
||||
if (
|
||||
possibleNFT721.status === "fulfilled" &&
|
||||
possibleNFT721.value.tokenURI
|
||||
) {
|
||||
return possibleNFT721.value;
|
||||
}
|
||||
if (
|
||||
possibleNFT1155.status === "fulfilled" &&
|
||||
possibleNFT1155.value.tokenURI
|
||||
) {
|
||||
return possibleNFT1155.value;
|
||||
}
|
||||
throw new Error("Failed to load NFT metadata");
|
||||
});
|
||||
return nft;
|
||||
},
|
||||
{
|
||||
cacheKey: `nft_info:${options.contract.chain.id}:${options.contract.address}:${options.tokenId.toString()}`,
|
||||
cacheTime: 15 * 60 * 1000,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,11 @@ import { describe, expect, it } from "vitest";
|
||||
import { numberToHex } from "./hex.js";
|
||||
|
||||
describe("hex.ts", () => {
|
||||
it("should convert number with no padding", () => {
|
||||
const result = numberToHex(1);
|
||||
expect(result).toBe("0x1");
|
||||
});
|
||||
|
||||
it("should convert", () => {
|
||||
const result = numberToHex(100n, { size: 32, signed: false });
|
||||
expect(result).toBe(
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
/**
|
||||
* @internal
|
||||
* Round up a number to a certain decimal place
|
||||
* @example
|
||||
* ```ts
|
||||
* import { formatNumber } from "thirdweb/utils";
|
||||
* const value = formatNumber(12.1214141, 1); // 12.1
|
||||
* ```
|
||||
* @utils
|
||||
*/
|
||||
export function formatNumber(value: number, decimalPlaces: number) {
|
||||
if (value === 0) return 0;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { shortenLargeNumber } from "./shortenLargeNumber.js";
|
||||
|
||||
describe("shortenLargeNumber", () => {
|
||||
it("should not affect number below 999", () => {
|
||||
expect(shortenLargeNumber(999)).toBe("999");
|
||||
});
|
||||
it("should toLocaleString number below 10000", () => {
|
||||
expect(shortenLargeNumber(1000)).toBe("1,000");
|
||||
expect(shortenLargeNumber(9999)).toBe("9,999");
|
||||
});
|
||||
it("should shorten the number to `k`", () => {
|
||||
expect(shortenLargeNumber(10000)).toBe("10k");
|
||||
});
|
||||
it("should shorten the number to `M`", () => {
|
||||
expect(shortenLargeNumber(1_000_000)).toBe("1M");
|
||||
});
|
||||
it("should shorten the number to `B`", () => {
|
||||
expect(shortenLargeNumber(1_000_000_000)).toBe("1B");
|
||||
});
|
||||
it("should shorten the number to `k`", () => {
|
||||
expect(shortenLargeNumber(11100)).toBe("11.1k");
|
||||
});
|
||||
it("should shorten the number to `M`", () => {
|
||||
expect(shortenLargeNumber(1_100_000)).toBe("1.1M");
|
||||
});
|
||||
it("should shorten the number to `B`", () => {
|
||||
expect(shortenLargeNumber(1_100_000_001)).toBe("1.1B");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Shorten the string for large value
|
||||
* Mainly used for
|
||||
* Examples:
|
||||
* 10_000 -> 10k
|
||||
* 1_000_000 -> 1M
|
||||
* 1_000_000_000 -> 1B
|
||||
* @example
|
||||
* ```ts
|
||||
* import { shortenLargeNumber } from "thirdweb/utils";
|
||||
* const numStr = shortenLargeNumber(1_000_000_000, )
|
||||
* ```
|
||||
* @utils
|
||||
*/
|
||||
export function shortenLargeNumber(value: number) {
|
||||
if (value < 1000) {
|
||||
return value.toString();
|
||||
}
|
||||
if (value < 10_000) {
|
||||
return value.toLocaleString("en-US");
|
||||
}
|
||||
if (value < 1_000_000) {
|
||||
return formatLargeNumber(value, 1_000, "k");
|
||||
}
|
||||
if (value < 1_000_000_000) {
|
||||
return formatLargeNumber(value, 1_000_000, "M");
|
||||
}
|
||||
return formatLargeNumber(value, 1_000_000_000, "B");
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorten the string for large value (over 4 digits)
|
||||
* 1000 -> 1000
|
||||
* 10_000 -> 10k
|
||||
* 1_000_000 -> 1M
|
||||
* 1_000_000_000 -> 1B
|
||||
*/
|
||||
function formatLargeNumber(
|
||||
value: number,
|
||||
divisor: number,
|
||||
suffix: "k" | "M" | "B",
|
||||
) {
|
||||
const quotient = value / divisor;
|
||||
if (Number.isInteger(quotient)) {
|
||||
return Math.floor(quotient) + suffix;
|
||||
}
|
||||
return quotient.toFixed(1).replace(/\.0$/, "") + suffix;
|
||||
}
|
||||
@@ -463,11 +463,7 @@ export async function connectCoinbaseWalletSDK(
|
||||
? options.chain
|
||||
: getCachedChain(chainId);
|
||||
// Switch to chain if provided
|
||||
if (
|
||||
connectedChainId &&
|
||||
options?.chain &&
|
||||
connectedChainId !== options?.chain.id
|
||||
) {
|
||||
if (chainId && options?.chain && chainId !== options?.chain.id) {
|
||||
await switchChainCoinbaseWalletSDK(provider, options.chain);
|
||||
chain = options.chain;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@ export async function generateWallet({
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to generate wallet");
|
||||
throw new Error(
|
||||
`Failed to generate wallet - ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { wallet } = (await response.json()) as {
|
||||
|
||||
@@ -43,7 +43,9 @@ export async function signMessage({
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to sign message");
|
||||
throw new Error(
|
||||
`Failed to sign message - ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const signedMessage = (await response.json()) as {
|
||||
|
||||
@@ -38,7 +38,9 @@ export async function signTransaction({
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to sign transaction");
|
||||
throw new Error(
|
||||
`Failed to sign transaction - ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const signedTransaction = (await response.json()) as {
|
||||
|
||||
@@ -41,7 +41,9 @@ export async function signTypedData<
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to sign typed data");
|
||||
throw new Error(
|
||||
`Failed to sign typed data - ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const signedTypedData = (await response.json()) as {
|
||||
|
||||
@@ -162,7 +162,7 @@ export class EnclaveWallet implements IWebWallet {
|
||||
chainId: toHex(tx.chainId),
|
||||
};
|
||||
|
||||
if (tx.maxFeePerGas) {
|
||||
if (typeof tx.maxFeePerGas === "bigint") {
|
||||
transaction.maxFeePerGas = toHex(tx.maxFeePerGas);
|
||||
transaction.maxPriorityFeePerGas =
|
||||
typeof tx.maxPriorityFeePerGas === "bigint"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user