Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
221f95b4f8 | ||
|
|
223c497f49 | ||
|
|
541bee5b34 | ||
|
|
df128bfc17 | ||
|
|
f2d10853f1 | ||
|
|
a68830ca67 | ||
|
|
5da500edca | ||
|
|
4542a48933 | ||
|
|
17127d7763 | ||
|
|
0dbb174e50 |
@@ -119,13 +119,18 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
if (filteredOptions.length >= itemsToShow) {
|
||||
break;
|
||||
}
|
||||
const option = options[i];
|
||||
if (!option) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (overrideSearchFn) {
|
||||
if (overrideSearchFn(options[i], searchValLowercase)) {
|
||||
filteredOptions.push(options[i]);
|
||||
if (overrideSearchFn(option, searchValLowercase)) {
|
||||
filteredOptions.push(option);
|
||||
}
|
||||
} else {
|
||||
if (options[i].label.toLowerCase().includes(searchValLowercase)) {
|
||||
filteredOptions.push(options[i]);
|
||||
if (option.label.toLowerCase().includes(searchValLowercase)) {
|
||||
filteredOptions.push(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,14 +63,18 @@ export const SelectWithSearch = React.forwardRef<
|
||||
if (filteredOptions.length >= itemsToShow) {
|
||||
break;
|
||||
}
|
||||
const option = options[i];
|
||||
if (!option) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (overrideSearchFn) {
|
||||
if (overrideSearchFn(options[i], searchValLowercase)) {
|
||||
filteredOptions.push(options[i]);
|
||||
if (overrideSearchFn(option, searchValLowercase)) {
|
||||
filteredOptions.push(option);
|
||||
}
|
||||
} else {
|
||||
if (options[i].label.toLowerCase().includes(searchValLowercase)) {
|
||||
filteredOptions.push(options[i]);
|
||||
if (option.label.toLowerCase().includes(searchValLowercase)) {
|
||||
filteredOptions.push(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ const ChartTooltipContent = React.forwardRef<
|
||||
}
|
||||
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item.dataKey || item.name || "value"}`;
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
|
||||
@@ -16,7 +16,9 @@ const ImageUpload = React.forwardRef<HTMLInputElement, ImageUploadProps>(
|
||||
const [activeFile, setActiveFile] = React.useState<File | null>(null);
|
||||
const onDrop = React.useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
setActiveFile(acceptedFiles[0]);
|
||||
if (acceptedFiles[0]) {
|
||||
setActiveFile(acceptedFiles[0]);
|
||||
}
|
||||
onUpload?.(acceptedFiles);
|
||||
},
|
||||
[onUpload],
|
||||
|
||||
@@ -35,7 +35,8 @@ const InputOTPSlot = React.forwardRef<
|
||||
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
||||
>(({ index, className, ...props }, ref) => {
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
|
||||
// biome-ignore lint/style/noNonNullAssertion: pure shadcn component - it works
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]!;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -62,7 +63,7 @@ const InputOTPSeparator = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ ...props }, ref) => (
|
||||
// biome-ignore lint/a11y/useFocusableInteractive: <explanation>
|
||||
// biome-ignore lint/a11y/useFocusableInteractive: pure shadcn component - it works
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<Dot />
|
||||
</div>
|
||||
|
||||
@@ -44,13 +44,12 @@ export function useActivity(contract: ThirdwebContract, autoUpdate?: boolean) {
|
||||
}
|
||||
const obj = eventsQuery.data.slice(0, 100).reduce(
|
||||
(acc, curr) => {
|
||||
if (acc[curr.transactionHash]) {
|
||||
acc[curr.transactionHash].events.push(curr);
|
||||
acc[curr.transactionHash].events.sort(
|
||||
(a, b) => b.logIndex - a.logIndex,
|
||||
);
|
||||
if (acc[curr.transactionHash].blockNumber > curr.blockNumber) {
|
||||
acc[curr.transactionHash].blockNumber = curr.blockNumber;
|
||||
const internalTx = acc[curr.transactionHash];
|
||||
if (internalTx) {
|
||||
internalTx.events.push(curr);
|
||||
internalTx.events.sort((a, b) => b.logIndex - a.logIndex);
|
||||
if (internalTx.blockNumber > curr.blockNumber) {
|
||||
internalTx.blockNumber = curr.blockNumber;
|
||||
}
|
||||
} else {
|
||||
acc[curr.transactionHash] = {
|
||||
|
||||
@@ -48,7 +48,7 @@ export async function fetchDashboardContractMetadata(
|
||||
if (compilerMetadata?.name.includes(":")) {
|
||||
const _name = compilerMetadata.name.split(":")[1];
|
||||
if (_name) {
|
||||
compilerMetadata.name = compilerMetadata.name.split(":")[1];
|
||||
compilerMetadata.name = _name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -171,7 +171,7 @@ function minimizeChain(
|
||||
return {
|
||||
name: chain.name,
|
||||
chain: chain.chain,
|
||||
rpc: [firstRpc],
|
||||
rpc: [firstRpc || ""],
|
||||
nativeCurrency: chain.nativeCurrency,
|
||||
shortName: chain.shortName,
|
||||
chainId: chain.chainId,
|
||||
@@ -509,7 +509,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
)}
|
||||
{colorOptions.map((color) => (
|
||||
<option key={color} value={color}>
|
||||
{color[0].toUpperCase() + color.substring(1)}
|
||||
{color[0]?.toUpperCase() + color.substring(1)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
@@ -524,7 +524,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
<Select {...register("secondaryColor")}>
|
||||
{colorOptions.map((color) => (
|
||||
<option key={color} value={color}>
|
||||
{color[0].toUpperCase() + color.substring(1)}
|
||||
{color[0]?.toUpperCase() + color.substring(1)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
+110
-74
@@ -1,101 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import { FormControl, Input } from "@chakra-ui/react";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { toast } from "sonner";
|
||||
import { type ThirdwebContract, isAddress } from "thirdweb";
|
||||
import { mintAdditionalSupplyTo } from "thirdweb/extensions/erc1155";
|
||||
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
|
||||
import { z } from "zod";
|
||||
|
||||
interface MintSupplyTabProps {
|
||||
contract: ThirdwebContract;
|
||||
tokenId: string;
|
||||
}
|
||||
|
||||
const mintAdditionalSupplyFormSchema = z.object({
|
||||
to: z.string().refine((value) => isAddress(value), {
|
||||
message: "Invalid Ethereum address",
|
||||
}),
|
||||
amount: z.number().int().min(1, "Amount must be at least 1"),
|
||||
});
|
||||
|
||||
const MintSupplyTab: React.FC<MintSupplyTabProps> = ({ contract, tokenId }) => {
|
||||
const trackEvent = useTrack();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
reset,
|
||||
} = useForm<{ to: string; amount: string }>({
|
||||
defaultValues: { amount: "1" },
|
||||
const address = useActiveAccount()?.address;
|
||||
|
||||
const form = useForm<z.input<typeof mintAdditionalSupplyFormSchema>>({
|
||||
resolver: zodResolver(mintAdditionalSupplyFormSchema),
|
||||
defaultValues: {
|
||||
amount: 1,
|
||||
to: address || "",
|
||||
},
|
||||
});
|
||||
|
||||
const address = useActiveAccount()?.address;
|
||||
const { mutate, isPending } = useSendAndConfirmTransaction();
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"Mint successful",
|
||||
"Error minting additional supply",
|
||||
contract,
|
||||
);
|
||||
const sendAndConfirmTx = useSendAndConfirmTransaction();
|
||||
|
||||
function onSubmit(values: z.input<typeof mintAdditionalSupplyFormSchema>) {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "mint-supply",
|
||||
label: "attempt",
|
||||
});
|
||||
const transaction = mintAdditionalSupplyTo({
|
||||
contract,
|
||||
to: values.to,
|
||||
tokenId: BigInt(tokenId),
|
||||
supply: BigInt(values.amount),
|
||||
});
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "mint-supply",
|
||||
label: "success",
|
||||
});
|
||||
form.reset();
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "mint-supply",
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
},
|
||||
});
|
||||
toast.promise(promise, {
|
||||
loading: "Minting NFT",
|
||||
success: "Minted successfully",
|
||||
error: "Failed to mint",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={handleSubmit((data) => {
|
||||
if (address) {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "mint-supply",
|
||||
label: "attempt",
|
||||
});
|
||||
const transaction = mintAdditionalSupplyTo({
|
||||
contract,
|
||||
to: address,
|
||||
tokenId: BigInt(tokenId),
|
||||
supply: BigInt(data.amount),
|
||||
});
|
||||
mutate(transaction, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "mint-supply",
|
||||
label: "success",
|
||||
});
|
||||
onSuccess();
|
||||
reset();
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "mint-supply",
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
onError(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
})}
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex w-full flex-col gap-2"
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex w-full flex-col gap-6 md:flex-row">
|
||||
<FormControl isRequired isInvalid={!!errors.to}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="amount"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Amount</FormLabel>
|
||||
<Input placeholder="1" {...register("amount")} />
|
||||
<FormHelperText>How many would you like to mint?</FormHelperText>
|
||||
<FormErrorMessage>{errors.to?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
How many would you like to mint?
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
transactionCount={1}
|
||||
isLoading={isPending}
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
alignSelf="flex-end"
|
||||
>
|
||||
Mint
|
||||
</TransactionButton>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="to"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Recipient</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
transactionCount={1}
|
||||
isLoading={sendAndConfirmTx.isPending}
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
alignSelf="flex-end"
|
||||
>
|
||||
Mint
|
||||
</TransactionButton>
|
||||
</form>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+35
-6
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import {
|
||||
Box,
|
||||
@@ -15,15 +17,17 @@ import {
|
||||
useBreakpointValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { useChainSlug } from "hooks/chains/chainSlug";
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import { ChevronLeftIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { getNFT as getErc721NFT } from "thirdweb/extensions/erc721";
|
||||
import { getNFT as getErc1155NFT } from "thirdweb/extensions/erc1155";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { resolveScheme } from "thirdweb/storage";
|
||||
import { Badge, Button, Card, CodeBlock, Heading, Text } from "tw-components";
|
||||
import { AddressCopyButton } from "tw-components/AddressCopyButton";
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import { shortenString } from "utils/usedapp-external";
|
||||
import { NftProperty } from "../components/nft-property";
|
||||
import { useNFTDrawerTabs } from "./useNftDrawerTabs";
|
||||
|
||||
@@ -65,6 +69,8 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
tokenId,
|
||||
});
|
||||
|
||||
const client = useThirdwebClient();
|
||||
|
||||
const { data: nft, isPending } = useReadContract(
|
||||
isErc721 ? getErc721NFT : getErc1155NFT,
|
||||
{
|
||||
@@ -203,10 +209,11 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
<Heading size="label.md">Token ID</Heading>
|
||||
</GridItem>
|
||||
<GridItem colSpan={8}>
|
||||
<AddressCopyButton
|
||||
size="xs"
|
||||
address={nft.id?.toString()}
|
||||
title="Token ID"
|
||||
<CopyTextButton
|
||||
textToCopy={nft.id?.toString()}
|
||||
textToShow={nft.id?.toString()}
|
||||
tooltip="Token ID"
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
</GridItem>
|
||||
|
||||
@@ -240,6 +247,28 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
</GridItem>
|
||||
</>
|
||||
)}
|
||||
<GridItem colSpan={4}>
|
||||
<Heading size="label.md">Token URI</Heading>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
colSpan={8}
|
||||
className="flex flex-row items-center gap-1"
|
||||
>
|
||||
<CopyTextButton
|
||||
textToCopy={nft.tokenURI}
|
||||
textToShow={shortenString(nft.tokenURI)}
|
||||
tooltip="The URI of this NFT"
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
<Button variant="ghost">
|
||||
<Link
|
||||
href={resolveScheme({ client, uri: nft.tokenURI })}
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</GridItem>
|
||||
</SimpleGrid>
|
||||
</Card>
|
||||
{properties ? (
|
||||
|
||||
+3
-1
@@ -41,7 +41,9 @@ export const PermissionsTable: React.FC<PermissionsTableProps> = ({
|
||||
roleMembers.forEach((member) => {
|
||||
return !acc.find((m) => m.member === member)
|
||||
? acc.push({ member, roles: [role] })
|
||||
: acc[acc.findIndex((m) => m.member === member)].roles.push(role);
|
||||
: acc[acc.findIndex((m) => m.member === member)]?.roles.push(
|
||||
role,
|
||||
);
|
||||
});
|
||||
|
||||
return acc;
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ export const DistributeButton: React.FC<DistributeButtonProps> = ({
|
||||
const numTransactions = useMemo(() => {
|
||||
if (
|
||||
validBalances.length === 1 &&
|
||||
validBalances[0].name === "Native Token"
|
||||
validBalances[0]?.name === "Native Token"
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ export async function EcosystemLandingPage(props: {
|
||||
console.error("failed to fetch ecosystems", err);
|
||||
return [];
|
||||
});
|
||||
if (ecosystems.length > 0) {
|
||||
if (ecosystems[0]) {
|
||||
redirect(`${props.ecosystemLayoutPath}/${ecosystems[0].slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -105,8 +105,10 @@ export function CreateEcosystemForm(props: {
|
||||
<ImageUpload
|
||||
accept="image/png, image/jpeg"
|
||||
onUpload={(files) => {
|
||||
form.setValue("logo", files[0]);
|
||||
form.clearErrors("logo");
|
||||
if (files[0]) {
|
||||
form.setValue("logo", files[0]);
|
||||
form.clearErrors("logo");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -80,10 +80,14 @@ export default async function ExploreCategoryPage(
|
||||
<div className="h-10" />
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-3">
|
||||
{category.contracts.map((publishedContractId, idx) => {
|
||||
const publisher: string = Array.isArray(publishedContractId)
|
||||
const publisher: string | undefined = Array.isArray(
|
||||
publishedContractId,
|
||||
)
|
||||
? publishedContractId[0].split("/")[0]
|
||||
: publishedContractId.split("/")[0];
|
||||
const contractId: string = Array.isArray(publishedContractId)
|
||||
const contractId: string | undefined = Array.isArray(
|
||||
publishedContractId,
|
||||
)
|
||||
? publishedContractId[0].split("/")[1]
|
||||
: publishedContractId.split("/")[1];
|
||||
const modules = Array.isArray(publishedContractId)
|
||||
@@ -92,6 +96,10 @@ export default async function ExploreCategoryPage(
|
||||
const overrides = Array.isArray(publishedContractId)
|
||||
? publishedContractId[2]
|
||||
: undefined;
|
||||
if (!publisher || !contractId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractCard
|
||||
key={publisher + contractId + overrides?.title}
|
||||
@@ -107,8 +115,8 @@ export default async function ExploreCategoryPage(
|
||||
modules={
|
||||
modules?.length
|
||||
? modules.map((m) => ({
|
||||
publisher: m.split("/")[0],
|
||||
moduleId: m.split("/")[1],
|
||||
publisher: m.split("/")[0] || "",
|
||||
moduleId: m.split("/")[1] || "",
|
||||
}))
|
||||
: undefined
|
||||
}
|
||||
|
||||
+4
@@ -32,6 +32,10 @@ export default async function Image(props: {
|
||||
publishedContracts.find((p) => p.version === props.params.version) ||
|
||||
publishedContracts[0];
|
||||
|
||||
if (!publishedContract) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const publishedContractName =
|
||||
publishedContract?.displayName || publishedContract?.name;
|
||||
|
||||
|
||||
+5
@@ -4,6 +4,7 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { SimpleGrid } from "@chakra-ui/react";
|
||||
import { fetchPublishedContractVersions } from "components/contract-components/fetch-contracts-with-versions";
|
||||
import { PublishedContract } from "components/contract-components/published-contract";
|
||||
import { notFound } from "next/navigation";
|
||||
import { isAddress } from "thirdweb";
|
||||
import { resolveAddress } from "thirdweb/extensions/ens";
|
||||
import { PublishedActions } from "../../../components/contract-actions-published.client";
|
||||
@@ -46,6 +47,10 @@ export default async function PublishedContractPage(
|
||||
publishedContractVersions.find((v) => v.version === props.params.version) ||
|
||||
publishedContractVersions[0];
|
||||
|
||||
if (!publishedContract) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeployContractHeader
|
||||
|
||||
+4
@@ -29,6 +29,10 @@ export default async function Image(props: {
|
||||
|
||||
const publishedContract = publishedContracts[0];
|
||||
|
||||
if (!publishedContract) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const publishedContractName =
|
||||
publishedContract?.displayName || publishedContract?.name;
|
||||
|
||||
|
||||
+5
@@ -1,5 +1,6 @@
|
||||
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { notFound } from "next/navigation";
|
||||
import { PublishedContract } from "../../../../../components/contract-components/published-contract";
|
||||
import { PublishedActions } from "../../components/contract-actions-published.client";
|
||||
import { DeployContractHeader } from "../../components/contract-header";
|
||||
@@ -23,6 +24,10 @@ export default async function PublishedContractPage(
|
||||
|
||||
const publishedContract = publishedContractVersions[0];
|
||||
|
||||
if (!publishedContract) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeployContractHeader
|
||||
|
||||
+3
-2
@@ -118,10 +118,11 @@ function descriptionShortener(description: string) {
|
||||
words.push(word);
|
||||
currentLength += word.length + 1;
|
||||
}
|
||||
if (words[words.length - 1].length < 4) {
|
||||
const lastWord = words[words.length - 1];
|
||||
if (lastWord && lastWord.length < 4) {
|
||||
words = words.slice(0, -1);
|
||||
}
|
||||
if (words[words.length - 1].endsWith(".")) {
|
||||
if (words[words.length - 1]?.endsWith(".")) {
|
||||
return words.join(" ");
|
||||
}
|
||||
if (!shortened) {
|
||||
|
||||
+5
-1
@@ -48,8 +48,12 @@ export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) {
|
||||
publishedContractVersions.find((v) => v.version === props.version) ||
|
||||
publishedContractVersions[0];
|
||||
|
||||
if (!publishedContract) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const moduleUris = modules
|
||||
.filter((m) => m !== null)
|
||||
.filter((m) => m !== null && m !== undefined)
|
||||
.map((m) => m.publishMetadataUri);
|
||||
const [contractMetadata, ...fetchedModules] = await Promise.all([
|
||||
fetchDeployMetadata({
|
||||
|
||||
@@ -116,7 +116,7 @@ export async function createTicketAction(
|
||||
|
||||
const markdown = prepareEmailBody({
|
||||
product,
|
||||
markdownInput: keyVal.markdown,
|
||||
markdownInput: keyVal.markdown || "",
|
||||
email: account.data.email,
|
||||
name: account.data.name,
|
||||
extraInfoInput: keyVal,
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ export function TeamSettingsMobileNav(props: {
|
||||
return (
|
||||
<div className="flex items-center gap-2 border-border border-b px-4 py-4 text-muted-foreground">
|
||||
<Link
|
||||
href={teamLinks[0].href}
|
||||
href={teamLinks[0]?.href || "#"}
|
||||
className="inline-flex items-center gap-1"
|
||||
onClick={() => {
|
||||
setShowFull(true);
|
||||
|
||||
+1
-1
@@ -191,7 +191,7 @@ export function WalletConnectorsChartCard(props: {
|
||||
<Bar
|
||||
key={walletType}
|
||||
dataKey={walletType}
|
||||
fill={chartConfig[walletType].color}
|
||||
fill={chartConfig[walletType]?.color}
|
||||
radius={4}
|
||||
stackId="a"
|
||||
strokeWidth={1.5}
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@ export function WalletDistributionChartCard(props: {
|
||||
return {
|
||||
walletType,
|
||||
value: data,
|
||||
fill: _chartConfig[walletType].color || "transparent",
|
||||
fill: _chartConfig[walletType]?.color || "transparent",
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
+4
-3
@@ -24,9 +24,10 @@ const walletsToPickFrom: WalletId[] = [
|
||||
];
|
||||
|
||||
const pickRandomWallet = () => {
|
||||
return walletsToPickFrom[
|
||||
Math.floor(Math.random() * walletsToPickFrom.length)
|
||||
];
|
||||
return (
|
||||
walletsToPickFrom[Math.floor(Math.random() * walletsToPickFrom.length)] ||
|
||||
"io.metamask"
|
||||
);
|
||||
};
|
||||
|
||||
export function createWalletStatsStub(days: number): WalletStats[] {
|
||||
|
||||
+5
-4
@@ -37,12 +37,14 @@ type ContentItem = {
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type TabId = "explore" | "import" | "build" | "deploy";
|
||||
|
||||
const DeployOptions = () => {
|
||||
const [showImportModal, setShowImportModal] = useState(false);
|
||||
const router = useDashboardRouter();
|
||||
const trackEvent = useTrack();
|
||||
|
||||
const content: Record<string, ContentItem> = useMemo(
|
||||
const content: Record<TabId, ContentItem> = useMemo(
|
||||
() => ({
|
||||
explore: {
|
||||
title: "Ready-to-deploy",
|
||||
@@ -72,8 +74,7 @@ const DeployOptions = () => {
|
||||
[],
|
||||
);
|
||||
|
||||
const contentKeys = Object.keys(content);
|
||||
const [activeTab, setActiveTab] = useState(contentKeys[0]);
|
||||
const [activeTab, setActiveTab] = useState<TabId>("explore");
|
||||
const activeTabContent = content[activeTab];
|
||||
|
||||
return (
|
||||
@@ -91,7 +92,7 @@ const DeployOptions = () => {
|
||||
name: value.title,
|
||||
isActive: activeTab === key,
|
||||
isEnabled: true,
|
||||
onClick: () => setActiveTab(key),
|
||||
onClick: () => setActiveTab(key as TabId),
|
||||
}))}
|
||||
tabClassName="font-medium !text-sm"
|
||||
/>
|
||||
|
||||
+6
-1
@@ -29,7 +29,12 @@ export function TeamAndProjectSelectorPopoverButton(props: TeamSwitcherProps) {
|
||||
const { currentTeam, teamsAndProjects } = props;
|
||||
const [hoveredTeam, setHoveredTeam] = useState<Team>();
|
||||
const projectsToShowOfTeam =
|
||||
hoveredTeam || currentTeam || teamsAndProjects[0].team;
|
||||
hoveredTeam || currentTeam || teamsAndProjects[0]?.team;
|
||||
|
||||
// if we can't find a single team associated with this user - something is really wrong
|
||||
if (!projectsToShowOfTeam) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const projectsToShow = teamsAndProjects.find(
|
||||
(x) => x.team.slug === projectsToShowOfTeam.slug,
|
||||
|
||||
@@ -39,13 +39,21 @@ function Variants(props: {
|
||||
const Comp =
|
||||
props.type === "mobile" ? TeamHeaderMobileUI : TeamHeaderDesktopUI;
|
||||
|
||||
const team1 = teamsAndProjectsStub[0]?.team;
|
||||
const team2 = teamsAndProjectsStub[1]?.team;
|
||||
const team3 = teamsAndProjectsStub[2]?.team;
|
||||
const team3Project = teamsAndProjectsStub[2]?.projects[0];
|
||||
|
||||
if (!team1 || !team2 || !team3 || !team3Project) {
|
||||
return <div> failed to get team and project stubs </div>;
|
||||
}
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<div className="flex min-h-screen flex-col gap-6 bg-gray-700 px-4 py-10">
|
||||
<BadgeContainer label="Team Free, Account Loaded">
|
||||
<Comp
|
||||
teamsAndProjects={teamsAndProjectsStub}
|
||||
currentTeam={teamsAndProjectsStub[0].team}
|
||||
currentTeam={team1}
|
||||
currentProject={undefined}
|
||||
logout={() => {}}
|
||||
connectButton={<ConnectButtonStub />}
|
||||
@@ -60,7 +68,7 @@ function Variants(props: {
|
||||
<BadgeContainer label="Team Growth, Account Loading">
|
||||
<Comp
|
||||
teamsAndProjects={teamsAndProjectsStub}
|
||||
currentTeam={teamsAndProjectsStub[1].team}
|
||||
currentTeam={team2}
|
||||
currentProject={undefined}
|
||||
account={undefined}
|
||||
logout={() => {}}
|
||||
@@ -72,7 +80,7 @@ function Variants(props: {
|
||||
<BadgeContainer label="Team, Pro">
|
||||
<Comp
|
||||
teamsAndProjects={teamsAndProjectsStub}
|
||||
currentTeam={teamsAndProjectsStub[2].team}
|
||||
currentTeam={team3}
|
||||
currentProject={undefined}
|
||||
account={{
|
||||
email: "[email protected]",
|
||||
@@ -87,8 +95,8 @@ function Variants(props: {
|
||||
<BadgeContainer label="Team + Project, Pro">
|
||||
<Comp
|
||||
teamsAndProjects={teamsAndProjectsStub}
|
||||
currentTeam={teamsAndProjectsStub[2].team}
|
||||
currentProject={teamsAndProjectsStub[2].projects[0]}
|
||||
currentTeam={team3}
|
||||
currentProject={team3Project}
|
||||
account={{
|
||||
email: "[email protected]",
|
||||
id: "foo",
|
||||
|
||||
@@ -72,7 +72,7 @@ export class ConnectFrame {
|
||||
action: "post_redirect",
|
||||
},
|
||||
],
|
||||
image: connectFrames["7"].imageUrl,
|
||||
image: connectFrames[7].imageUrl,
|
||||
// hardcode to "7"
|
||||
post_url: `${getAbsoluteUrl()}/api/frame/connect?step=7`,
|
||||
});
|
||||
@@ -81,7 +81,8 @@ export class ConnectFrame {
|
||||
static getFrameHtmlResponse = (step: Step, direction: "next" | "back") => {
|
||||
const readyStepNum =
|
||||
direction === "next" ? Number(step) + 1 : Number(step) - 1;
|
||||
const frameImg = connectFrames[readyStepNum]?.imageUrl;
|
||||
const frameImg =
|
||||
connectFrames[readyStepNum as keyof typeof connectFrames]?.imageUrl;
|
||||
|
||||
if (!frameImg) {
|
||||
throw new Error(`Image frame not found for step: ${step}`);
|
||||
|
||||
@@ -58,6 +58,10 @@ const AreaChart = <
|
||||
}
|
||||
|
||||
const indexType = index.type || "date";
|
||||
const firstData = data[0];
|
||||
const lastData = data[data.length - 1];
|
||||
const firstDataAtIndexId = firstData?.[index.id];
|
||||
const lastDataAtIndexId = lastData?.[index.id];
|
||||
|
||||
return (
|
||||
<div className={cn("h-full w-full", className)}>
|
||||
@@ -149,8 +153,8 @@ const AreaChart = <
|
||||
type="number"
|
||||
tick={{ transform: "translate(0, 6)" }}
|
||||
ticks={
|
||||
startEndOnly
|
||||
? [data[0][index.id], data[data.length - 1][index.id]]
|
||||
startEndOnly && firstDataAtIndexId && lastDataAtIndexId
|
||||
? [firstDataAtIndexId, lastDataAtIndexId]
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -22,7 +22,11 @@ export const IconUpload: React.FC<{ onUpload: (url: string) => void }> = ({
|
||||
|
||||
storageUpload.mutate([file], {
|
||||
onSuccess([uri]) {
|
||||
onUpload(uri);
|
||||
if (uri) {
|
||||
onUpload(uri);
|
||||
} else {
|
||||
toast.error("Something went wrong uploading icon");
|
||||
}
|
||||
// also refetch the files list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [PINNED_FILES_QUERY_KEY_ROOT],
|
||||
@@ -30,7 +34,7 @@ export const IconUpload: React.FC<{ onUpload: (url: string) => void }> = ({
|
||||
},
|
||||
onError(error) {
|
||||
console.error(error);
|
||||
toast.error("Failed to upload file");
|
||||
toast.error("Failed to upload icon");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -193,12 +193,12 @@ const CodePlayground = ({
|
||||
<Button
|
||||
size="sm"
|
||||
fontWeight={
|
||||
tab === "write" && write.signature === fn.signature
|
||||
tab === "write" && write?.signature === fn.signature
|
||||
? 600
|
||||
: 400
|
||||
}
|
||||
opacity={
|
||||
tab === "write" && write.signature === fn.signature
|
||||
tab === "write" && write?.signature === fn.signature
|
||||
? 1
|
||||
: 0.65
|
||||
}
|
||||
@@ -225,12 +225,12 @@ const CodePlayground = ({
|
||||
<Button
|
||||
size="sm"
|
||||
fontWeight={
|
||||
tab === "read" && read.signature === fn.signature
|
||||
tab === "read" && read?.signature === fn.signature
|
||||
? 600
|
||||
: 400
|
||||
}
|
||||
opacity={
|
||||
tab === "read" && read.signature === fn.signature
|
||||
tab === "read" && read?.signature === fn.signature
|
||||
? 1
|
||||
: 0.65
|
||||
}
|
||||
@@ -257,10 +257,10 @@ const CodePlayground = ({
|
||||
<Button
|
||||
size="sm"
|
||||
fontWeight={
|
||||
tab === "events" && event.name === ev.name ? 600 : 400
|
||||
tab === "events" && event?.name === ev.name ? 600 : 400
|
||||
}
|
||||
opacity={
|
||||
tab === "events" && event.name === ev.name ? 1 : 0.65
|
||||
tab === "events" && event?.name === ev.name ? 1 : 0.65
|
||||
}
|
||||
onClick={() => {
|
||||
setTab("events");
|
||||
|
||||
+3
-3
@@ -416,9 +416,9 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
defaultAdmin: params.deployParams._defaultAdmin,
|
||||
platformFeeBps: Number(params.deployParams._platformFeeBps),
|
||||
platformFeeRecipient: params.deployParams._platformFeeRecipient,
|
||||
trustedForwarders: JSON.parse(
|
||||
params.deployParams._trustedForwarders,
|
||||
),
|
||||
trustedForwarders: params.deployParams._trustedForwarders
|
||||
? JSON.parse(params.deployParams._trustedForwarders)
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+3
-3
@@ -146,7 +146,7 @@ export const ContractParamsFieldset: React.FC<ContractParamsFieldsetProps> = ({
|
||||
bgColor="backgroundHighlight"
|
||||
>
|
||||
<Text>
|
||||
{paramTemplateValues[0].helperText} Click to
|
||||
{paramTemplateValues[0]?.helperText} Click to
|
||||
apply.
|
||||
</Text>
|
||||
</Card>
|
||||
@@ -161,14 +161,14 @@ export const ContractParamsFieldset: React.FC<ContractParamsFieldsetProps> = ({
|
||||
`constructorParams.${
|
||||
param.name ? param.name : "*"
|
||||
}.defaultValue`,
|
||||
paramTemplateValues[0].value,
|
||||
paramTemplateValues[0]?.value,
|
||||
{
|
||||
shouldDirty: true,
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
{paramTemplateValues[0].value}
|
||||
{paramTemplateValues[0]?.value}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</InputRightElement>
|
||||
|
||||
@@ -324,7 +324,7 @@ export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
|
||||
: undefined;
|
||||
|
||||
const [selectedFunction, setSelectedFunction] = useState<
|
||||
AbiFunction | AbiEvent
|
||||
AbiFunction | AbiEvent | undefined
|
||||
>(_item ?? fnsOrEvents[0]);
|
||||
// Set the active tab to Write or Read depends on the `_item`
|
||||
const _defaultTabIndex =
|
||||
@@ -370,14 +370,15 @@ export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
|
||||
<Divider my={2} />
|
||||
</>
|
||||
)}
|
||||
{filteredFunctions.map((fn) => (
|
||||
<FunctionsOrEventsListItem
|
||||
key={`${fn.name}_${fn.type}_${fn.inputs.length}`}
|
||||
fn={fn}
|
||||
selectedFunction={selectedFunction}
|
||||
setSelectedFunction={setSelectedFunction}
|
||||
/>
|
||||
))}
|
||||
{selectedFunction &&
|
||||
filteredFunctions.map((fn) => (
|
||||
<FunctionsOrEventsListItem
|
||||
key={`${fn.name}_${fn.type}_${fn.inputs.length}`}
|
||||
fn={fn}
|
||||
selectedFunction={selectedFunction}
|
||||
setSelectedFunction={setSelectedFunction}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -447,7 +448,7 @@ export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
{events.length > 0 && (
|
||||
{events.length > 0 && selectedFunction && (
|
||||
<Box px={4} pt={2} overflowX="hidden">
|
||||
{events.map((fn) => (
|
||||
<FunctionsOrEventsListItem
|
||||
@@ -467,7 +468,9 @@ export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
|
||||
overflow="auto"
|
||||
colSpan={{ base: 12, md: 8 }}
|
||||
>
|
||||
<ContractFunction fn={selectedFunction} contract={contract} />
|
||||
{selectedFunction && (
|
||||
<ContractFunction fn={selectedFunction} contract={contract} />
|
||||
)}
|
||||
</GridItem>
|
||||
</SimpleGrid>
|
||||
);
|
||||
@@ -476,7 +479,9 @@ export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
|
||||
interface FunctionsOrEventsListItemProps {
|
||||
fn: AbiFunction | AbiEvent;
|
||||
selectedFunction: AbiFunction | AbiEvent;
|
||||
setSelectedFunction: Dispatch<SetStateAction<AbiFunction | AbiEvent>>;
|
||||
setSelectedFunction: Dispatch<
|
||||
SetStateAction<AbiFunction | AbiEvent | undefined>
|
||||
>;
|
||||
}
|
||||
|
||||
const FunctionsOrEventsListItem: React.FC<FunctionsOrEventsListItemProps> = ({
|
||||
|
||||
@@ -182,13 +182,16 @@ export const InAppWalletSettingsUI: React.FC<
|
||||
// FIXME: This must match components/settings/ApiKeys/Edit/index.tsx
|
||||
// Make it more generic w/o me thinking of values
|
||||
const newServices = [...services];
|
||||
newServices[serviceIdx] = {
|
||||
...services[serviceIdx],
|
||||
customAuthentication,
|
||||
customAuthEndpoint,
|
||||
applicationImageUrl: branding?.applicationImageUrl,
|
||||
applicationName: branding?.applicationName || apiKey.name,
|
||||
};
|
||||
|
||||
if (services[serviceIdx]) {
|
||||
newServices[serviceIdx] = {
|
||||
...services[serviceIdx],
|
||||
customAuthentication,
|
||||
customAuthEndpoint,
|
||||
applicationImageUrl: branding?.applicationImageUrl,
|
||||
applicationName: branding?.applicationName || apiKey.name,
|
||||
};
|
||||
}
|
||||
|
||||
props.updateApiKey(
|
||||
{
|
||||
@@ -573,8 +576,8 @@ function NativeAppsFieldset(props: {
|
||||
Enter redirect URIs separated by commas or new lines. This is
|
||||
often your application's deep link.
|
||||
<br className="max-sm:hidden" />
|
||||
Currently only used in Unity and React Native platform when users
|
||||
authenticate through social logins.
|
||||
Currently only used in Unity, Unreal Engine and React Native
|
||||
platform when users authenticate through social logins.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -97,7 +97,7 @@ export const InAppWalletUsersPageContent = (props: {
|
||||
}
|
||||
const csv = Papa.unparse(
|
||||
theWalletsWeWant.map((row) => ({
|
||||
email: row.ews_authed_user[0].email,
|
||||
email: row.ews_authed_user[0]?.email,
|
||||
address: row.embedded_wallet?.[0]?.address || "",
|
||||
created: format(new Date(row.created_at), "MMM dd, yyyy"),
|
||||
last_login: format(new Date(row.last_accessed_at), "MMM dd, yyyy"),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import {
|
||||
type EngineWebhook,
|
||||
useEngineRevokeWebhook,
|
||||
@@ -23,7 +24,7 @@ import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { Trash2Icon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button, Card, FormLabel, Text } from "tw-components";
|
||||
import { AddressCopyButton } from "tw-components/AddressCopyButton";
|
||||
import { shortenString } from "utils/usedapp-external";
|
||||
|
||||
export function beautifyString(str: string): string {
|
||||
return str
|
||||
@@ -58,10 +59,11 @@ const columns = [
|
||||
header: "Secret",
|
||||
cell: (cell) => {
|
||||
return (
|
||||
<AddressCopyButton
|
||||
address={cell.getValue() || ""}
|
||||
title="secret"
|
||||
size="xs"
|
||||
<CopyTextButton
|
||||
textToCopy={cell.getValue() || ""}
|
||||
textToShow={shortenString(cell.getValue() || "")}
|
||||
tooltip="Secret"
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
+3
-3
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import {
|
||||
type EngineContractSubscription,
|
||||
@@ -34,7 +35,6 @@ import { useState } from "react";
|
||||
import { eth_getBlockByNumber, getRpcClient } from "thirdweb";
|
||||
import { shortenAddress as shortenAddresThrows } from "thirdweb/utils";
|
||||
import { Button, Card, FormLabel, LinkButton, Text } from "tw-components";
|
||||
import { AddressCopyButton } from "../../../tw-components/AddressCopyButton";
|
||||
|
||||
function shortenAddress(address: string) {
|
||||
if (!address) {
|
||||
@@ -93,9 +93,9 @@ export const ContractSubscriptionTable: React.FC<
|
||||
const explorer = chain?.explorers?.[0];
|
||||
if (!explorer) {
|
||||
return (
|
||||
<AddressCopyButton
|
||||
<CopyAddressButton
|
||||
address={cell.getValue()}
|
||||
title="Contract address"
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
|
||||
import type { Transaction } from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import {
|
||||
@@ -22,7 +23,6 @@ import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { useRef } from "react";
|
||||
import { Button, FormLabel, Text } from "tw-components";
|
||||
import { AddressCopyButton } from "tw-components/AddressCopyButton";
|
||||
|
||||
interface TimelineStep {
|
||||
step: string;
|
||||
@@ -242,10 +242,9 @@ const CancelTransactionButton = ({
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>To</FormLabel>
|
||||
<AddressCopyButton
|
||||
<CopyAddressButton
|
||||
address={transaction.toAddress ?? ""}
|
||||
size="xs"
|
||||
shortenAddress={false}
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
|
||||
@@ -279,12 +279,12 @@ export const TransactionsTable: React.FC<TransactionsTableProps> = ({
|
||||
disclosure={transactionDisclosure}
|
||||
onClickPrevious={
|
||||
idx > 0
|
||||
? () => setSelectedTransaction(transactions[idx - 1])
|
||||
? () => setSelectedTransaction(transactions[idx - 1] || null)
|
||||
: undefined
|
||||
}
|
||||
onClickNext={
|
||||
idx < transactions.length - 1
|
||||
? () => setSelectedTransaction(transactions[idx + 1])
|
||||
? () => setSelectedTransaction(transactions[idx + 1] || null)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { moduleToBase64 } from "app/(dashboard)/published-contract/utils/module-
|
||||
import { RocketIcon, ShieldCheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { resolveScheme } from "thirdweb/storage";
|
||||
import invariant from "tiny-invariant";
|
||||
import { fetchPublishedContractVersion } from "../../contract-components/fetch-contracts-with-versions";
|
||||
import { ContractPublisher, replaceDeployerAddress } from "../publisher";
|
||||
|
||||
@@ -255,8 +256,11 @@ function usePublishedContract(publishedContractId: PublishedContractId) {
|
||||
const [publisher, contractId, version] = publishedContractId.split("/");
|
||||
return useQuery({
|
||||
queryKey: ["published-contract", { publishedContractId }],
|
||||
queryFn: () =>
|
||||
fetchPublishedContractVersion(publisher, contractId, version),
|
||||
queryFn: () => {
|
||||
invariant(publisher, "publisher is required");
|
||||
invariant(contractId, "contractId is required");
|
||||
return fetchPublishedContractVersion(publisher, contractId, version);
|
||||
},
|
||||
enabled: !!publisher || !!contractId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,10 +48,14 @@ export function ContractRow({ category }: ContractRowProps) {
|
||||
|
||||
<div className="relative z-0 grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{category.contracts.slice(0, 6).map((publishedContractId, idx) => {
|
||||
const publisher: string = Array.isArray(publishedContractId)
|
||||
const publisher: string | undefined = Array.isArray(
|
||||
publishedContractId,
|
||||
)
|
||||
? publishedContractId[0].split("/")[0]
|
||||
: publishedContractId.split("/")[0];
|
||||
const contractId: string = Array.isArray(publishedContractId)
|
||||
const contractId: string | undefined = Array.isArray(
|
||||
publishedContractId,
|
||||
)
|
||||
? publishedContractId[0].split("/")[1]
|
||||
: publishedContractId.split("/")[1];
|
||||
const modules = Array.isArray(publishedContractId)
|
||||
@@ -60,6 +64,10 @@ export function ContractRow({ category }: ContractRowProps) {
|
||||
const overrides = Array.isArray(publishedContractId)
|
||||
? publishedContractId[2]
|
||||
: undefined;
|
||||
|
||||
if (!publisher || !contractId) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ContractCard
|
||||
key={publisher + contractId + overrides?.title}
|
||||
@@ -75,8 +83,8 @@ export function ContractRow({ category }: ContractRowProps) {
|
||||
modules={
|
||||
modules?.length
|
||||
? modules.map((m) => ({
|
||||
publisher: m.split("/")[0],
|
||||
moduleId: m.split("/")[1],
|
||||
publisher: m.split("/")[0] || "",
|
||||
moduleId: m.split("/")[1] || "",
|
||||
}))
|
||||
: undefined
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { UseMutationResult } from "@tanstack/react-query";
|
||||
import { FileInput } from "components/shared/FileInput";
|
||||
import { useErrorHandler } from "contexts/error-handler";
|
||||
import { UploadIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button, type ButtonProps } from "tw-components";
|
||||
import type { ComponentWithChildren } from "types/component-with-children";
|
||||
|
||||
@@ -20,7 +21,13 @@ export const IpfsUploadButton: ComponentWithChildren<IpfsUploadButtonProps> = ({
|
||||
const { onError } = useErrorHandler();
|
||||
const handleUpload = (file: File) => {
|
||||
storageUpload.mutate([file], {
|
||||
onSuccess: ([uri]) => onUpload(uri),
|
||||
onSuccess: ([uri]) => {
|
||||
if (uri) {
|
||||
onUpload(uri);
|
||||
} else {
|
||||
toast.error("File Upload but no URI returned");
|
||||
}
|
||||
},
|
||||
onError: (error) => onError(error, "Failed to upload file"),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -151,14 +151,15 @@ const FileUpload: React.FC<FileUploadProps> = ({ files, updateFiles }) => {
|
||||
// const progressPercent = (progress.progress / progress.total) * 100;
|
||||
|
||||
const mainIpfsUri = useMemo(() => {
|
||||
if (ipfsHashes.length === 0) {
|
||||
const firstHash = ipfsHashes[0];
|
||||
if (firstHash === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (ipfsHashes.length === 1) {
|
||||
return replaceIpfsUrl(ipfsHashes[0]);
|
||||
return replaceIpfsUrl(firstHash);
|
||||
}
|
||||
// get the folder
|
||||
const uri = ipfsHashes[0].split("ipfs://")[1];
|
||||
const uri = firstHash.split("ipfs://")[1] || "";
|
||||
const folderPath = uri.split("/")[0]; // "Qma.../image.png" -> "Qma..."
|
||||
return `https://ipfs.io/ipfs/${folderPath}`;
|
||||
}, [ipfsHashes]);
|
||||
@@ -183,6 +184,9 @@ const FileUpload: React.FC<FileUploadProps> = ({ files, updateFiles }) => {
|
||||
>
|
||||
{filesToShow.map((file, index) => {
|
||||
const ipfsHash = ipfsHashes[index];
|
||||
if (!ipfsHash) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<GridItem colSpan={1} key={`${file.name}_${index}`}>
|
||||
<SimpleGrid
|
||||
@@ -492,7 +496,7 @@ const FileUpload: React.FC<FileUploadProps> = ({ files, updateFiles }) => {
|
||||
uri:
|
||||
uris.length === 1
|
||||
? uris[0]
|
||||
: uris[0].split("/").slice(0, -1).join("/"),
|
||||
: (uris[0] || "").split("/").slice(0, -1).join("/"),
|
||||
});
|
||||
setIpfsHashes(uris);
|
||||
// also refetch the files list
|
||||
|
||||
@@ -35,11 +35,16 @@ export const LandingDynamicSelector: React.FC<LandingDynamicSelectorProps> = ({
|
||||
lineHeight,
|
||||
margin = "",
|
||||
}) => {
|
||||
const [selectedItemTitle, setSelectedItemTitle] = useState(items[0].title);
|
||||
const firstItem = items[0];
|
||||
const [selectedItemTitle, setSelectedItemTitle] = useState(firstItem?.title);
|
||||
const trackEvent = useTrack();
|
||||
|
||||
if (!firstItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedItem =
|
||||
items.find((item) => item.title === selectedItemTitle) || items[0];
|
||||
items.find((item) => item.title === selectedItemTitle) || firstItem;
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap={8}>
|
||||
|
||||
@@ -32,11 +32,17 @@ export const LandingOptionSelector: React.FC<LandingOptionSelectorProps> = ({
|
||||
TRACKING_CATEGORY,
|
||||
blackToWhiteTitle,
|
||||
}) => {
|
||||
const [selectedItemTitle, setSelectedItemTitle] = useState(items[0].title);
|
||||
const firstItem = items[0];
|
||||
|
||||
const [selectedItemTitle, setSelectedItemTitle] = useState(firstItem?.title);
|
||||
const trackEvent = useTrack();
|
||||
|
||||
if (!firstItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedItem =
|
||||
items.find((item) => item.title === selectedItemTitle) || items[0];
|
||||
items.find((item) => item.title === selectedItemTitle) || firstItem;
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap={8}>
|
||||
|
||||
@@ -16,12 +16,24 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { format, fromUnixTime } from "date-fns";
|
||||
import { TagIcon } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const LazyShareFreeWalletsModal = dynamic(
|
||||
() =>
|
||||
import("./share-free-wallets-modal.client").then(
|
||||
(mod) => mod.ShareFreeWalletsModal,
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => null,
|
||||
},
|
||||
);
|
||||
|
||||
export type ActiveCouponResponse = {
|
||||
id: string;
|
||||
start: number;
|
||||
@@ -36,7 +48,10 @@ export type ActiveCouponResponse = {
|
||||
|
||||
function ApplyCouponCard(props: {
|
||||
teamId: string | undefined;
|
||||
onCouponApplied: (data: ActiveCouponResponse) => void;
|
||||
onCouponApplied: (
|
||||
data: ActiveCouponResponse,
|
||||
isFreeWalletsCoupon: boolean,
|
||||
) => void;
|
||||
isPaymentSetup: boolean;
|
||||
onAddPayment: () => void;
|
||||
}) {
|
||||
@@ -87,7 +102,9 @@ export function ApplyCouponCardUI(props: {
|
||||
status: number;
|
||||
data: null | ActiveCouponResponse;
|
||||
}>;
|
||||
onCouponApplied: ((data: ActiveCouponResponse) => void) | undefined;
|
||||
onCouponApplied:
|
||||
| ((data: ActiveCouponResponse, isFreeWalletsCoupon: boolean) => void)
|
||||
| undefined;
|
||||
prefillPromoCode?: string;
|
||||
scrollIntoView?: boolean;
|
||||
isPaymentSetup: boolean;
|
||||
@@ -130,7 +147,12 @@ export function ApplyCouponCardUI(props: {
|
||||
case 200: {
|
||||
toast.success("Coupon applied successfully");
|
||||
if (res.data) {
|
||||
props.onCouponApplied?.(res.data);
|
||||
props.onCouponApplied?.(
|
||||
res.data,
|
||||
// prod & dev
|
||||
values.promoCode === "FREEWALLETS" ||
|
||||
values.promoCode === "TESTFREEWALLETS",
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -258,6 +280,7 @@ export function CouponSection(props: {
|
||||
isPaymentSetup: boolean;
|
||||
onAddPayment: () => void;
|
||||
}) {
|
||||
const [showShareModal, setShowShareModal] = useState(false);
|
||||
const loggedInUser = useLoggedInUser();
|
||||
const [optimisticCouponData, setOptimisticCouponData] = useState<
|
||||
| {
|
||||
@@ -319,35 +342,46 @@ export function CouponSection(props: {
|
||||
? optimisticCouponData.data
|
||||
: activeCoupon.data;
|
||||
|
||||
if (couponData) {
|
||||
return (
|
||||
<CouponDetailsCardUI
|
||||
activeCoupon={couponData}
|
||||
deleteCoupon={{
|
||||
mutateAsync: deleteActiveCoupon.mutateAsync,
|
||||
isPending: deleteActiveCoupon.isPending,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<LoadingCouponSection />}>
|
||||
<ApplyCouponCard
|
||||
teamId={props.teamId}
|
||||
onCouponApplied={(coupon) => {
|
||||
setOptimisticCouponData({
|
||||
type: "added",
|
||||
data: coupon,
|
||||
});
|
||||
activeCoupon.refetch().then(() => {
|
||||
setOptimisticCouponData(undefined);
|
||||
});
|
||||
}}
|
||||
isPaymentSetup={props.isPaymentSetup}
|
||||
onAddPayment={props.onAddPayment}
|
||||
/>
|
||||
</Suspense>
|
||||
<>
|
||||
{couponData ? (
|
||||
<CouponDetailsCardUI
|
||||
activeCoupon={couponData}
|
||||
deleteCoupon={{
|
||||
mutateAsync: deleteActiveCoupon.mutateAsync,
|
||||
isPending: deleteActiveCoupon.isPending,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Suspense fallback={<LoadingCouponSection />}>
|
||||
<ApplyCouponCard
|
||||
teamId={props.teamId}
|
||||
onCouponApplied={(coupon, isFreeWalletsCoupon) => {
|
||||
setOptimisticCouponData({
|
||||
type: "added",
|
||||
data: coupon,
|
||||
});
|
||||
|
||||
if (isFreeWalletsCoupon) {
|
||||
setShowShareModal(true);
|
||||
}
|
||||
activeCoupon.refetch().then(() => {
|
||||
setOptimisticCouponData(undefined);
|
||||
});
|
||||
}}
|
||||
isPaymentSetup={props.isPaymentSetup}
|
||||
onAddPayment={props.onAddPayment}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{showShareModal && (
|
||||
<LazyShareFreeWalletsModal
|
||||
isOpen={showShareModal}
|
||||
onOpenChange={setShowShareModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+404
@@ -0,0 +1,404 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ChevronLeftIcon, MailIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { DynamicHeight } from "../../../../@/components/ui/DynamicHeight";
|
||||
import { XIcon } from "../../../icons/brand-icons/XIcon";
|
||||
|
||||
export function ShareFreeWalletsModal(props: {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
const [screen, setScreen] = useState<"base" | "email">("base");
|
||||
|
||||
return (
|
||||
<Dialog open={props.isOpen} onOpenChange={props.onOpenChange}>
|
||||
<DialogContent
|
||||
className="z-[10001] p-0 md:max-w-[380px]"
|
||||
dialogOverlayClassName="z-[10000]"
|
||||
>
|
||||
<DynamicHeight>
|
||||
{screen === "base" && (
|
||||
<div className="fade-in-0 animate-in duration-300">
|
||||
<DialogHeader className="p-6 text-center">
|
||||
<div className="mb-4 flex justify-center gap-2">
|
||||
<VibrantSmileIcon className="size-16" />
|
||||
</div>
|
||||
<DialogTitle className="mb-1 text-lg">
|
||||
Congratulations!
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-balance text-base text-muted-foreground">
|
||||
You are getting free wallets for the next 12 months
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-2 gap-3 border-border border-t p-6">
|
||||
<Button variant="outline" className="gap-2" asChild>
|
||||
<TrackedLinkTW
|
||||
href={`https://x.com/intent/post?text=${encodeURIComponent(twitterPostBody)}`}
|
||||
target="_blank"
|
||||
category="share-free-wallets"
|
||||
label="twitter-share"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
Share on X
|
||||
</TrackedLinkTW>
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="gap-2"
|
||||
onClick={() => setScreen("email")}
|
||||
>
|
||||
<MailIcon className="size-4 shrink-0" />
|
||||
Share via Email
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{screen === "email" && (
|
||||
<SendEmailScreen goBack={() => setScreen("base")} />
|
||||
)}
|
||||
</DynamicHeight>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
const emailFormSchema = z.object({
|
||||
email: z.string().email(),
|
||||
app: z.string(),
|
||||
});
|
||||
|
||||
function SendEmailScreen(props: {
|
||||
goBack: () => void;
|
||||
}) {
|
||||
const form = useForm<z.infer<typeof emailFormSchema>>({
|
||||
resolver: zodResolver(emailFormSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
app: "default",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="fade-in-0 animate-in duration-300">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(() => {})}>
|
||||
<div className="p-6">
|
||||
<DialogHeader className="text-center">
|
||||
<div className="mb-4 flex justify-center gap-2">
|
||||
<VibrantEmailIcon className="size-16" />
|
||||
</div>
|
||||
<DialogTitle className="mb-1 text-lg">
|
||||
Share via Email
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-balance text-base text-muted-foreground">
|
||||
Give the gift of unlimited wallets to another dev
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="h-4" />
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="[email protected]"
|
||||
{...field}
|
||||
type="email"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="h-4" />
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="app"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Share with</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(value) => form.setValue("app", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select Email App" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[10001]">
|
||||
<SelectGroup>
|
||||
<SelectItem value="default">
|
||||
Default Email App
|
||||
</SelectItem>
|
||||
<SelectItem value="gmail">Gmail</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-3 border-border border-t p-6">
|
||||
<Button variant="outline" className="gap-2" onClick={props.goBack}>
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
{!form.formState.isValid ? (
|
||||
<Button variant="primary" className="gap-2" type="submit">
|
||||
<MailIcon className="size-4 shrink-0" />
|
||||
Share via Email
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="primary" className="gap-2" asChild>
|
||||
<TrackedLinkTW
|
||||
href={
|
||||
form.watch("app") === "gmail"
|
||||
? openGmailEmailApp(form.watch("email"))
|
||||
: openDefaultEmailApp(form.watch("email"))
|
||||
}
|
||||
target="_blank"
|
||||
category="share-free-wallets"
|
||||
label="email-share"
|
||||
>
|
||||
<MailIcon className="size-4 shrink-0" />
|
||||
Share via Email
|
||||
</TrackedLinkTW>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function openDefaultEmailApp(email: string) {
|
||||
return `mailto:${email}?subject=${encodeURIComponent(emailSubject)}&body=${encodeURIComponent(emailPostBody)}`;
|
||||
}
|
||||
|
||||
function openGmailEmailApp(email: string) {
|
||||
return `https://mail.google.com/mail/?view=cm&fs=1&to=${email}&su=${encodeURIComponent(emailSubject)}&body=${encodeURIComponent(emailPostBody)}`;
|
||||
}
|
||||
|
||||
const twitterPostBody = `\
|
||||
Just claimed the free, unlimited wallets offer from @thirdweb! 🚀
|
||||
|
||||
No more per-wallet pricing, full support for web, mobile, and console.
|
||||
|
||||
Here's to building without constraints ✨
|
||||
|
||||
👉 https://thirdweb.com/unlimited-wallets 👈`;
|
||||
|
||||
const emailSubject = "You've unlocked Unlimited Wallets!";
|
||||
|
||||
const emailPostBody = `\
|
||||
Hey there,
|
||||
|
||||
I just came across something that might interest you. thirdweb is offering free, unlimited wallets for developers.
|
||||
I've claimed it and thought you might want to take a look too.
|
||||
|
||||
Here's the lowdown:
|
||||
* It's completely free
|
||||
* No more per-wallet pricing constraints
|
||||
* Full support for web, mobile, and console development
|
||||
|
||||
I'm pretty excited about the possibilities this opens up for building without limitations. You can check it out here: https://thirdweb.com/unlimited-wallets
|
||||
|
||||
Let me know what you think if you decide to give it a try!`;
|
||||
|
||||
function VibrantSmileIcon(props: {
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
role="presentation"
|
||||
>
|
||||
<path
|
||||
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
|
||||
fill="url(#paint0_linear_754_5630)"
|
||||
fillOpacity="0.08"
|
||||
/>
|
||||
<path
|
||||
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
|
||||
stroke="url(#paint1_linear_754_5630)"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M26.2857 34.8567C26.2857 34.8567 28.4286 37.7139 32 37.7139C35.5714 37.7139 37.7143 34.8567 37.7143 34.8567M36.2857 27.7139H36.3M27.7143 27.7139H27.7286M46.2857 31.9996C46.2857 39.8894 39.8898 46.2853 32 46.2853C24.1102 46.2853 17.7143 39.8894 17.7143 31.9996C17.7143 24.1098 24.1102 17.7139 32 17.7139C39.8898 17.7139 46.2857 24.1098 46.2857 31.9996ZM37 27.7139C37 28.1084 36.6802 28.4282 36.2857 28.4282C35.8912 28.4282 35.5714 28.1084 35.5714 27.7139C35.5714 27.3194 35.8912 26.9996 36.2857 26.9996C36.6802 26.9996 37 27.3194 37 27.7139ZM28.4286 27.7139C28.4286 28.1084 28.1088 28.4282 27.7143 28.4282C27.3198 28.4282 27 28.1084 27 27.7139C27 27.3194 27.3198 26.9996 27.7143 26.9996C28.1088 26.9996 28.4286 27.3194 28.4286 27.7139Z"
|
||||
stroke="url(#paint2_linear_754_5630)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_754_5630"
|
||||
x1="-4.34726"
|
||||
y1="20.5361"
|
||||
x2="61.0409"
|
||||
y2="66.8729"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_754_5630"
|
||||
x1="-4.34726"
|
||||
y1="20.5361"
|
||||
x2="61.0409"
|
||||
y2="66.8729"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_754_5630"
|
||||
x1="15.7735"
|
||||
y1="26.8818"
|
||||
x2="44.9647"
|
||||
y2="47.5678"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function VibrantEmailIcon(props: {
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
role="presentation"
|
||||
>
|
||||
<path
|
||||
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
|
||||
fill="url(#paint0_linear_754_9605)"
|
||||
fillOpacity="0.08"
|
||||
/>
|
||||
<path
|
||||
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
|
||||
stroke="url(#paint1_linear_754_9605)"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M17.7144 24.857L29.3785 33.0219C30.3231 33.6831 30.7953 34.0137 31.309 34.1417C31.7628 34.2548 32.2374 34.2548 32.6911 34.1417C33.2048 34.0137 33.6771 33.6831 34.6216 33.0219L46.2858 24.857M24.5715 43.4284H39.4286C41.8289 43.4284 43.029 43.4284 43.9457 42.9613C44.7522 42.5504 45.4078 41.8948 45.8187 41.0884C46.2858 40.1716 46.2858 38.9715 46.2858 36.5713V27.4284C46.2858 25.0282 46.2858 23.8281 45.8187 22.9113C45.4078 22.1049 44.7522 21.4493 43.9457 21.0384C43.029 20.5713 41.8289 20.5713 39.4286 20.5713H24.5715C22.1713 20.5713 20.9712 20.5713 20.0544 21.0384C19.248 21.4493 18.5924 22.1049 18.1815 22.9113C17.7144 23.8281 17.7144 25.0282 17.7144 27.4284V36.5713C17.7144 38.9715 17.7144 40.1716 18.1815 41.0884C18.5924 41.8948 19.248 42.5504 20.0544 42.9613C20.9712 43.4284 22.1713 43.4284 24.5715 43.4284Z"
|
||||
stroke="url(#paint2_linear_754_9605)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_754_9605"
|
||||
x1="-4.34726"
|
||||
y1="20.5361"
|
||||
x2="61.0409"
|
||||
y2="66.8729"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_754_9605"
|
||||
x1="-4.34726"
|
||||
y1="20.5361"
|
||||
x2="61.0409"
|
||||
y2="66.8729"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_754_9605"
|
||||
x1="15.7736"
|
||||
y1="27.9056"
|
||||
x2="40.3444"
|
||||
y2="49.6705"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { mobileViewport } from "../../../../stories/utils";
|
||||
import { ShareFreeWalletsModal } from "./share-free-wallets-modal.client";
|
||||
|
||||
const meta = {
|
||||
title: "Billing/coupons/ShareFreeWalletsModal",
|
||||
component: Variants,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Variants>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Variants() {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
return (
|
||||
<div className="flex grow items-center justify-center p-6">
|
||||
<Button onClick={() => setIsOpen(true)}>Open Again</Button>
|
||||
<ShareFreeWalletsModal isOpen={isOpen} onOpenChange={setIsOpen} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,11 +8,8 @@ import {
|
||||
ethereum,
|
||||
fantom,
|
||||
fantomTestnet,
|
||||
hardhat,
|
||||
localhost,
|
||||
optimism,
|
||||
polygon,
|
||||
sepolia,
|
||||
} from "thirdweb/chains";
|
||||
|
||||
export interface CurrencyMetadata {
|
||||
@@ -21,160 +18,126 @@ export interface CurrencyMetadata {
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
const NATIVE_TOKENS: Record<
|
||||
number,
|
||||
{
|
||||
name: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
wrapped: CurrencyMetadata;
|
||||
}
|
||||
> = /* @__PURE__ */ {
|
||||
[ethereum.id]: {
|
||||
name: "Ether",
|
||||
symbol: "ETH",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
name: "Wrapped Ether",
|
||||
symbol: "WETH",
|
||||
},
|
||||
type NativeTokenInfo = {
|
||||
name: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
wrapped: CurrencyMetadata;
|
||||
};
|
||||
|
||||
const ethereumNativeToken: NativeTokenInfo = {
|
||||
name: "Ether",
|
||||
symbol: "ETH",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
name: "Wrapped Ether",
|
||||
symbol: "WETH",
|
||||
},
|
||||
[sepolia.id]: {
|
||||
name: "Sepolia Ether",
|
||||
symbol: "SEP",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9",
|
||||
name: "Wrapped Ether",
|
||||
symbol: "WETH",
|
||||
},
|
||||
};
|
||||
|
||||
const polygonNativeToken: NativeTokenInfo = {
|
||||
name: "Matic",
|
||||
symbol: "MATIC",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
|
||||
name: "Wrapped Matic",
|
||||
symbol: "WMATIC",
|
||||
},
|
||||
[polygon.id]: {
|
||||
name: "Matic",
|
||||
symbol: "MATIC",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
|
||||
name: "Wrapped Matic",
|
||||
symbol: "WMATIC",
|
||||
},
|
||||
};
|
||||
|
||||
const avalancheNativeToken: NativeTokenInfo = {
|
||||
name: "Avalanche",
|
||||
symbol: "AVAX",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7",
|
||||
name: "Wrapped AVAX",
|
||||
symbol: "WAVAX",
|
||||
},
|
||||
[avalanche.id]: {
|
||||
name: "Avalanche",
|
||||
symbol: "AVAX",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7",
|
||||
name: "Wrapped AVAX",
|
||||
symbol: "WAVAX",
|
||||
},
|
||||
};
|
||||
|
||||
const avalancheFujiNativeToken: NativeTokenInfo = {
|
||||
name: "Avalanche",
|
||||
symbol: "AVAX",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0xd00ae08403B9bbb9124bB305C09058E32C39A48c",
|
||||
name: "Wrapped AVAX",
|
||||
symbol: "WAVAX",
|
||||
},
|
||||
[avalancheFuji.id]: {
|
||||
name: "Avalanche",
|
||||
symbol: "AVAX",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0xd00ae08403B9bbb9124bB305C09058E32C39A48c",
|
||||
name: "Wrapped AVAX",
|
||||
symbol: "WAVAX",
|
||||
},
|
||||
};
|
||||
|
||||
const fantomNativeToken: NativeTokenInfo = {
|
||||
name: "Fantom",
|
||||
symbol: "FTM",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83",
|
||||
name: "Wrapped Fantom",
|
||||
symbol: "WFTM",
|
||||
},
|
||||
[fantom.id]: {
|
||||
name: "Fantom",
|
||||
symbol: "FTM",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83",
|
||||
name: "Wrapped Fantom",
|
||||
symbol: "WFTM",
|
||||
},
|
||||
};
|
||||
|
||||
const fantomTestnetNativeToken: NativeTokenInfo = {
|
||||
name: "Fantom",
|
||||
symbol: "FTM",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0xf1277d1Ed8AD466beddF92ef448A132661956621",
|
||||
name: "Wrapped Fantom",
|
||||
symbol: "WFTM",
|
||||
},
|
||||
[fantomTestnet.id]: {
|
||||
name: "Fantom",
|
||||
symbol: "FTM",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0xf1277d1Ed8AD466beddF92ef448A132661956621",
|
||||
name: "Wrapped Fantom",
|
||||
symbol: "WFTM",
|
||||
},
|
||||
};
|
||||
|
||||
const arbitrumNativeToken: NativeTokenInfo = {
|
||||
name: "Ether",
|
||||
symbol: "ETH",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1",
|
||||
name: "Wrapped Ether",
|
||||
symbol: "WETH",
|
||||
},
|
||||
[arbitrum.id]: {
|
||||
name: "Ether",
|
||||
symbol: "ETH",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1",
|
||||
name: "Wrapped Ether",
|
||||
symbol: "WETH",
|
||||
},
|
||||
};
|
||||
|
||||
const optimismNativeToken: NativeTokenInfo = {
|
||||
name: "Ether",
|
||||
symbol: "ETH",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0x4200000000000000000000000000000000000006",
|
||||
name: "Wrapped Ether",
|
||||
symbol: "WETH",
|
||||
},
|
||||
[optimism.id]: {
|
||||
name: "Ether",
|
||||
symbol: "ETH",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0x4200000000000000000000000000000000000006",
|
||||
name: "Wrapped Ether",
|
||||
symbol: "WETH",
|
||||
},
|
||||
};
|
||||
|
||||
const bscNativeToken: NativeTokenInfo = {
|
||||
name: "Binance Chain Native Token",
|
||||
symbol: "BNB",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
|
||||
name: "Wrapped Binance Chain Token",
|
||||
symbol: "WBNB",
|
||||
},
|
||||
[bsc.id]: {
|
||||
name: "Binance Chain Native Token",
|
||||
symbol: "BNB",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
|
||||
name: "Wrapped Binance Chain Token",
|
||||
symbol: "WBNB",
|
||||
},
|
||||
},
|
||||
[bscTestnet.id]: {
|
||||
name: "Binance Chain Native Token",
|
||||
symbol: "TBNB",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd",
|
||||
name: "Wrapped Binance Chain Testnet Token",
|
||||
symbol: "WBNB",
|
||||
},
|
||||
},
|
||||
[hardhat.id]: {
|
||||
name: "Ether",
|
||||
symbol: "ETH",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0x5FbDB2315678afecb367f032d93F642f64180aa3",
|
||||
name: "Wrapped Ether",
|
||||
symbol: "WETH",
|
||||
},
|
||||
},
|
||||
[localhost.id]: {
|
||||
name: "Ether",
|
||||
symbol: "ETH",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0x5FbDB2315678afecb367f032d93F642f64180aa3",
|
||||
name: "Wrapped Ether",
|
||||
symbol: "WETH",
|
||||
},
|
||||
},
|
||||
[280]: {
|
||||
name: "zkSync Era Testnet",
|
||||
symbol: "ETH",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91",
|
||||
name: "Wrapped Ether",
|
||||
symbol: "WETH",
|
||||
},
|
||||
};
|
||||
|
||||
const bscTestnetNativeToken: NativeTokenInfo = {
|
||||
name: "Binance Chain Native Token",
|
||||
symbol: "TBNB",
|
||||
decimals: 18,
|
||||
wrapped: {
|
||||
address: "0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd",
|
||||
name: "Wrapped Binance Chain Testnet Token",
|
||||
symbol: "WBNB",
|
||||
},
|
||||
};
|
||||
|
||||
const Ethereum: CurrencyMetadata[] = [
|
||||
{
|
||||
...NATIVE_TOKENS[ethereum.id].wrapped,
|
||||
...ethereumNativeToken.wrapped,
|
||||
},
|
||||
{
|
||||
address: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
@@ -200,7 +163,7 @@ const Ethereum: CurrencyMetadata[] = [
|
||||
|
||||
const Polygon: CurrencyMetadata[] = [
|
||||
{
|
||||
...NATIVE_TOKENS[polygon.id].wrapped,
|
||||
...polygonNativeToken.wrapped,
|
||||
},
|
||||
{
|
||||
address: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
|
||||
@@ -233,7 +196,7 @@ const Polygon: CurrencyMetadata[] = [
|
||||
|
||||
const Fantom: CurrencyMetadata[] = [
|
||||
{
|
||||
...NATIVE_TOKENS[fantom.id].wrapped,
|
||||
...fantomNativeToken.wrapped,
|
||||
},
|
||||
{
|
||||
name: "Wrapped Ether",
|
||||
@@ -254,13 +217,13 @@ const Fantom: CurrencyMetadata[] = [
|
||||
|
||||
const FantomTestnet: CurrencyMetadata[] = [
|
||||
{
|
||||
...NATIVE_TOKENS[fantomTestnet.id].wrapped,
|
||||
...fantomTestnetNativeToken.wrapped,
|
||||
},
|
||||
];
|
||||
|
||||
const Avalanche: CurrencyMetadata[] = [
|
||||
{
|
||||
...NATIVE_TOKENS[avalanche.id].wrapped,
|
||||
...avalancheNativeToken.wrapped,
|
||||
},
|
||||
{
|
||||
address: "0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB",
|
||||
@@ -291,7 +254,7 @@ const Avalanche: CurrencyMetadata[] = [
|
||||
|
||||
const AvalancheFujiTestnet: CurrencyMetadata[] = [
|
||||
{
|
||||
...NATIVE_TOKENS[avalancheFuji.id].wrapped,
|
||||
...avalancheFujiNativeToken.wrapped,
|
||||
},
|
||||
// Source: https://developers.circle.com/stablecoins/docs/usdc-on-testing-networks#usdc-on-avalanche-testnet
|
||||
{
|
||||
@@ -303,7 +266,7 @@ const AvalancheFujiTestnet: CurrencyMetadata[] = [
|
||||
|
||||
const Optimism: CurrencyMetadata[] = [
|
||||
{
|
||||
...NATIVE_TOKENS[optimism.id].wrapped,
|
||||
...optimismNativeToken.wrapped,
|
||||
},
|
||||
// Source: https://www.circle.com/blog/now-available-usdc-on-op-mainnet
|
||||
{
|
||||
@@ -321,7 +284,7 @@ const Optimism: CurrencyMetadata[] = [
|
||||
|
||||
const Arbitrum: CurrencyMetadata[] = [
|
||||
{
|
||||
...NATIVE_TOKENS[arbitrum.id].wrapped,
|
||||
...arbitrumNativeToken.wrapped,
|
||||
},
|
||||
// Source: https://www.circle.com/blog/arbitrum-usdc-now-available
|
||||
{
|
||||
@@ -339,7 +302,7 @@ const Arbitrum: CurrencyMetadata[] = [
|
||||
|
||||
const BinanceMainnet: CurrencyMetadata[] = [
|
||||
{
|
||||
...NATIVE_TOKENS[bsc.id].wrapped,
|
||||
...bscNativeToken.wrapped,
|
||||
},
|
||||
{
|
||||
address: "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56",
|
||||
@@ -360,7 +323,7 @@ const BinanceMainnet: CurrencyMetadata[] = [
|
||||
|
||||
const BinanceTestnet: CurrencyMetadata[] = [
|
||||
{
|
||||
...NATIVE_TOKENS[bscTestnet.id].wrapped,
|
||||
...bscTestnetNativeToken.wrapped,
|
||||
},
|
||||
{
|
||||
address: "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { Chain } from "thirdweb";
|
||||
import { type Chain, defineChain } from "thirdweb";
|
||||
import type { StoredChain } from "../stores/chainStores";
|
||||
|
||||
export function mapV4ChainToV5Chain(v4Chain: StoredChain) {
|
||||
const chain: Chain = {
|
||||
id: v4Chain.chainId,
|
||||
rpc: v4Chain.rpc[0],
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
rpc: v4Chain.rpc[0] || defineChain(v4Chain.chainId).rpc,
|
||||
// TypeScript shenanigans, just avoiding as string assertion here
|
||||
blockExplorers: v4Chain.explorers?.map((x) => x),
|
||||
// TypeScript shenanigans, just avoiding as string assertion here
|
||||
|
||||
@@ -48,6 +48,9 @@ export function createSetAllRoleMembersTx(options: {
|
||||
const sortedRoles = roles.sort((role) => (role === "admin" ? 1 : -1));
|
||||
for (let i = 0; i < sortedRoles.length; i++) {
|
||||
const role = sortedRoles[i];
|
||||
if (!role) {
|
||||
continue;
|
||||
}
|
||||
const [addresses, currentAddresses] = await Promise.all([
|
||||
Promise.all(
|
||||
options.roleMemberMap[role]?.map((addressOrEns) =>
|
||||
|
||||
@@ -469,10 +469,16 @@ export function formatSnippet(
|
||||
fn?.name &&
|
||||
extensionNamespace &&
|
||||
extensionNamespace in EXTENSION_NAMESPACE_FUNCTION_MAPPING &&
|
||||
EXTENSION_NAMESPACE_FUNCTION_MAPPING[extensionNamespace] &&
|
||||
fn.name in EXTENSION_NAMESPACE_FUNCTION_MAPPING[extensionNamespace]
|
||||
) {
|
||||
const extensionConfig =
|
||||
EXTENSION_NAMESPACE_FUNCTION_MAPPING[extensionNamespace][fn.name];
|
||||
|
||||
if (!extensionConfig) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (env) {
|
||||
case "javascript":
|
||||
codeForEnv = buildJavascriptSnippet({
|
||||
|
||||
@@ -53,6 +53,9 @@ function createAllChainsStore() {
|
||||
|
||||
for (let i = 0; i < originalAllChains.length; i++) {
|
||||
let _chain = originalAllChains[i];
|
||||
if (!_chain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// for dev env, replace the rpc urls to dev domain
|
||||
if (!isProd) {
|
||||
|
||||
@@ -4,7 +4,7 @@ interface Frame {
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
export const connectFrames: Record<number, Frame> = {
|
||||
export const connectFrames: Record<1 | 2 | 3 | 4 | 5 | 6 | 7, Frame> = {
|
||||
1: {
|
||||
imageUrl: `${getAbsoluteUrl()}/assets/connect/frames/frame-1.png`,
|
||||
},
|
||||
|
||||
@@ -180,7 +180,7 @@ async function fetchContractName(chainId: number, contractAddress: string) {
|
||||
}
|
||||
const compilationTarget = json.settings.compilationTarget;
|
||||
const targets = Object.keys(compilationTarget);
|
||||
const name = compilationTarget[targets[0]];
|
||||
const name = targets[0] ? compilationTarget[targets[0]] : "";
|
||||
return name;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ export function generateSimpleHashUrl({ chainId, owner }: GenerateURLParams) {
|
||||
const url = new URL("https://api.simplehash.com/api/v0/nfts/owners");
|
||||
|
||||
url.searchParams.append("wallet_addresses", owner);
|
||||
url.searchParams.append("chains", simpleHashSupportedChainIdsMap[chainId]);
|
||||
if (simpleHashSupportedChainIdsMap[chainId]) {
|
||||
url.searchParams.append("chains", simpleHashSupportedChainIdsMap[chainId]);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export function handleArbitraryTokenURI(rawUri: string): string {
|
||||
}
|
||||
const uriSplit = `ipfs://${rawUri.split("/ipfs/")}`;
|
||||
|
||||
return uriSplit[uriSplit.length - 1];
|
||||
return uriSplit[uriSplit.length - 1] || "";
|
||||
}
|
||||
|
||||
export function shouldDownloadURI(rawUri: string): boolean {
|
||||
|
||||
@@ -73,7 +73,7 @@ export async function middleware(request: NextRequest) {
|
||||
// DIFFERENT DYNAMIC ROUTING CASES
|
||||
|
||||
// /<address>/... case
|
||||
if (isPossibleEVMAddress(paths[0])) {
|
||||
if (paths[0] && isPossibleEVMAddress(paths[0])) {
|
||||
// special case for "deployer.thirdweb.eth"
|
||||
// we want to always redirect this to "thirdweb.eth/..."
|
||||
if (paths[0] === "deployer.thirdweb.eth") {
|
||||
|
||||
@@ -87,10 +87,12 @@ function descriptionShortener(description: string) {
|
||||
words.push(word);
|
||||
currentLength += word.length + 1;
|
||||
}
|
||||
if (words[words.length - 1].length < 4) {
|
||||
|
||||
const lastWord = words[words.length - 1];
|
||||
if (lastWord && lastWord.length < 4) {
|
||||
words = words.slice(0, -1);
|
||||
}
|
||||
if (words[words.length - 1].endsWith(".")) {
|
||||
if (lastWord?.endsWith(".")) {
|
||||
return words.join(" ");
|
||||
}
|
||||
if (!shortened) {
|
||||
|
||||
@@ -132,7 +132,10 @@ const ConnectLanding: ThirdwebNextPage = () => {
|
||||
{/* Farcaster frames headers */}
|
||||
<Head>
|
||||
<meta property="fc:frame" content="vNext" />
|
||||
<meta property="fc:frame:image" content={connectFrames["1"].imageUrl} />
|
||||
<meta
|
||||
property="fc:frame:image"
|
||||
content={connectFrames["1"]?.imageUrl}
|
||||
/>
|
||||
<meta
|
||||
property="fc:frame:post_url"
|
||||
content={`${getAbsoluteUrl()}/api/frame/connect?step=1`}
|
||||
|
||||
@@ -106,7 +106,7 @@ const InteractLanding: ThirdwebNextPage = () => {
|
||||
<LandingIconSectionItem
|
||||
icon={require("../../public/assets/product-pages-icons/contracts/icon-marketplace-1.svg")}
|
||||
title="Read & write to contracts"
|
||||
description="Integrate smart contracts into your app, call any & all functions, and have code snippets auto-generated based on your contract's supported extensions — for React, React Native, TypeScript, Python, Unity, and Go — on any chain."
|
||||
description="Integrate smart contracts into your app, call any & all functions, and have code snippets auto-generated based on your contract's supported extensions — for React, React Native, TypeScript, Python, Unity, Unreal Engine and Go — on any chain."
|
||||
/>
|
||||
</Card>
|
||||
<Card p={8}>
|
||||
|
||||
@@ -59,7 +59,7 @@ export function getCookie(name: string) {
|
||||
|
||||
for (const c of cookies) {
|
||||
const [_name, _val] = c.trim().split("=");
|
||||
if (_name === name) {
|
||||
if (_name === name && _val) {
|
||||
return decodeURIComponent(_val);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { Tooltip } from "@chakra-ui/react";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { CopyIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Button,
|
||||
type ButtonProps,
|
||||
type PossibleButtonSize,
|
||||
buttonSizesMap,
|
||||
} from "./button";
|
||||
import { Card } from "./card";
|
||||
import { Text } from "./text";
|
||||
|
||||
interface AddressCopyButtonProps extends Omit<ButtonProps, "onClick" | "size"> {
|
||||
address?: string;
|
||||
noIcon?: boolean;
|
||||
size?: PossibleButtonSize;
|
||||
title?: string;
|
||||
shortenAddress?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* shorten the string to 13 characters with the format of 6 chars + ... + 4 chars
|
||||
* does not shorten if string is less than 13 characters
|
||||
*
|
||||
* @param str string to shorten
|
||||
* @returns shortened string to length 13
|
||||
*/
|
||||
const shorten = (str: string) => {
|
||||
if (str.length > 13) {
|
||||
return `${str.substring(0, 6)}...${str.substring(str.length - 4)}`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
export const AddressCopyButton: React.FC<AddressCopyButtonProps> = ({
|
||||
address,
|
||||
noIcon,
|
||||
flexGrow = 0,
|
||||
size = "sm",
|
||||
borderRadius = "md",
|
||||
variant = "outline",
|
||||
shortenAddress = true,
|
||||
title = "address",
|
||||
...restButtonProps
|
||||
}) => {
|
||||
const { onCopy } = useClipboard(address || "");
|
||||
|
||||
const trackEvent = useTrack();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
p={0}
|
||||
bg="transparent"
|
||||
boxShadow="none"
|
||||
label={
|
||||
<Card py={2} px={4} bgColor="backgroundHighlight">
|
||||
<Text size="label.sm">Copy {title} to clipboard</Text>
|
||||
</Card>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
flexGrow={flexGrow}
|
||||
size={size}
|
||||
borderRadius={borderRadius}
|
||||
variant={variant}
|
||||
{...restButtonProps}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onCopy();
|
||||
toast.info(
|
||||
`${title.charAt(0).toUpperCase() + title.slice(1)} copied.`,
|
||||
);
|
||||
if (title === "Token ID") {
|
||||
trackEvent({
|
||||
category: "tokenid_button",
|
||||
action: "copy",
|
||||
tokenId: address,
|
||||
});
|
||||
} else {
|
||||
trackEvent({
|
||||
category: "address_button",
|
||||
action: "copy",
|
||||
address,
|
||||
});
|
||||
}
|
||||
}}
|
||||
leftIcon={noIcon ? undefined : <CopyIcon className="size-3" />}
|
||||
fontFamily="mono"
|
||||
>
|
||||
<Text color="inherit" size={`label.${buttonSizesMap[size]}`}>
|
||||
{address && (shortenAddress ? shorten(address) : address)}
|
||||
</Text>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -10,9 +10,3 @@ export * from "./link";
|
||||
export * from "./menu";
|
||||
export * from "./table-container";
|
||||
export * from "./text";
|
||||
|
||||
/**
|
||||
* ❗️❗️❗️ @DANGER
|
||||
* Do not export `AddressCopyButton` from here
|
||||
* It will result in a HUGE bundle size
|
||||
*/
|
||||
|
||||
@@ -214,8 +214,9 @@ export async function processInputData(
|
||||
};
|
||||
for (let i = 0; i < results.data.length; i++) {
|
||||
if (!results.errors.find((e) => e.row === i)) {
|
||||
if (results.data[i].name) {
|
||||
validResults.data.push(results.data[i]);
|
||||
const result = results.data[i];
|
||||
if (result?.name) {
|
||||
validResults.data.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"downlevelIteration": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"paths": {
|
||||
"tw-components": ["tw-components"],
|
||||
"tw-components/*": ["tw-components/*"]
|
||||
|
||||
+157
-110
@@ -18,8 +18,13 @@ Install Forge from Foundry and add the modular contract framework:
|
||||
|
||||
```bash
|
||||
forge init
|
||||
forge install https://github.com/thirdweb-dev/modular-contracts.git
|
||||
forge remappings > remappings.txt
|
||||
forge install thirdweb-dev/modular-contracts --no-commit
|
||||
```
|
||||
|
||||
Add the Thirdweb modular contracts to `foundry.toml` under `remappings`:
|
||||
|
||||
```toml
|
||||
remappings = ['@thirdweb-dev=lib/modular-contracts/']
|
||||
```
|
||||
|
||||
### Setup Core Contract
|
||||
@@ -29,152 +34,196 @@ forge remappings > remappings.txt
|
||||
<Step title="Create a Core Contract">
|
||||
Create a new file in the `src` folder called `CounterCore.sol`, and inherit the `Core` contract.
|
||||
|
||||
```solidity
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
```solidity
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Core} from "modular-contracts/src/Core.sol";
|
||||
import {Core} from "@thirdweb-dev/src/Core.sol";
|
||||
import {BeforeIncrementCallback} from "./interface/BeforeIncrementCallback.sol";
|
||||
|
||||
contract CounterCore is Core {
|
||||
contract CounterCore is Core {
|
||||
constructor(address owner) {
|
||||
_initializeOwner(owner);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(address owner) {
|
||||
_initializeOwner(owner);
|
||||
```
|
||||
|
||||
> **Note**
|
||||
> The `Core` contract is the base contract that needs to be inherited for this contract to be recognized as a core contract.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Set Get Supported Callback Function">
|
||||
Implement the `getSupportedCallbackFunctions` function. The Core contract is abstract because this function is not implemented. To avoid compilation errors, declare the function with an empty body for now.
|
||||
|
||||
```solidity
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Core} from "@thirdweb-dev/src/Core.sol";
|
||||
|
||||
contract CounterCore is Core {
|
||||
constructor(address owner) {
|
||||
_initializeOwner(owner);
|
||||
}
|
||||
|
||||
function getSupportedCallbackFunctions()
|
||||
public
|
||||
pure
|
||||
override
|
||||
returns (SupportedCallbackFunction[] memory supportedCallbackFunctions)
|
||||
{}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**
|
||||
> The `Core` contract is the base contract that needs to be inherited for this contract to be recognized as a core contract.
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Set Up Increment Function">
|
||||
Define a function to increment a counter.
|
||||
|
||||
```solidity
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
```solidity
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Core} from "modular-contracts/src/Core.sol";
|
||||
import {Core} from "@thirdweb-dev/src/Core.sol";
|
||||
|
||||
contract CounterCore is Core {
|
||||
uint256 public count;
|
||||
contract CounterCore is Core {
|
||||
uint256 public count;
|
||||
|
||||
constructor(address owner) {
|
||||
_initializeOwner(owner);
|
||||
}
|
||||
constructor(address owner) {
|
||||
_initializeOwner(owner);
|
||||
}
|
||||
|
||||
function increment() public {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
function getSupportedCallbackFunctions()
|
||||
public
|
||||
pure
|
||||
override
|
||||
returns (SupportedCallbackFunction[] memory supportedCallbackFunctions)
|
||||
{}
|
||||
|
||||
// 👇👇👇👇👇👇👇👇👇
|
||||
function increment() public {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Add a Callback Function">
|
||||
Introduce the `_beforeIncrement` function to use the `beforeIncrement` callback from a module.
|
||||
Introduce the `_beforeIncrement` function to use the `beforeIncrement` callback from a module to achieve this, we'll introduce the interface `BeforeIncrementCallback`
|
||||
|
||||
> **Note**
|
||||
> Callback functions are hook-like functionalities that can be used before or after the main functionality of a core contract.
|
||||
> In this example, the `beforeIncrement` callback is executed before the main increment functionality.
|
||||
> **Note**
|
||||
> Callback functions are hook-like functionalities that can be used before or after the main functionality of a core contract.
|
||||
> In this example, the `beforeIncrement` callback is executed before the main increment functionality.
|
||||
|
||||
```solidity
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
```solidity
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Core} from "modular-contracts/src/Core.sol";
|
||||
import {Core} from "@thirdweb-dev/src/Core.sol";
|
||||
|
||||
interface BeforeIncrementCallback {
|
||||
function beforeIncrement(uint256 count) external returns (uint256);
|
||||
}
|
||||
// 👇👇👇👇👇👇👇👇👇
|
||||
|
||||
contract CounterCore is Core {
|
||||
uint256 public count;
|
||||
interface BeforeIncrementCallback {
|
||||
function beforeIncrement(uint256 count) external returns (uint256);
|
||||
}
|
||||
|
||||
constructor(address owner) {
|
||||
_initializeOwner(owner);
|
||||
}
|
||||
contract CounterCore is Core {
|
||||
uint256 public count;
|
||||
|
||||
function increment() public {
|
||||
uint256 newCount = _beforeIncrement(count);
|
||||
count = newCount;
|
||||
}
|
||||
constructor(address owner) {
|
||||
_initializeOwner(owner);
|
||||
}
|
||||
|
||||
function _beforeIncrement(
|
||||
uint256 count
|
||||
) internal returns (uint256 newCount) {
|
||||
(, bytes memory returndata) = _executeCallbackFunction(
|
||||
BeforeIncrementCallback.beforeIncrement.selector,
|
||||
abi.encodeCall(BeforeIncrementCallback.beforeIncrement, (count))
|
||||
);
|
||||
newCount = abi.decode(returndata, (uint256));
|
||||
}
|
||||
}
|
||||
```
|
||||
function getSupportedCallbackFunctions()
|
||||
public
|
||||
pure
|
||||
override
|
||||
returns (SupportedCallbackFunction[] memory supportedCallbackFunctions)
|
||||
{}
|
||||
|
||||
function increment() public {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// 👇👇👇👇👇👇👇👇👇
|
||||
|
||||
function _beforeIncrement(
|
||||
uint256 _count
|
||||
) internal returns (uint256 newCount) {
|
||||
(, bytes memory returnData) = _executeCallbackFunction(
|
||||
BeforeIncrementCallback.beforeIncrement.selector,
|
||||
abi.encodeCall(BeforeIncrementCallback.beforeIncrement, (_count))
|
||||
);
|
||||
|
||||
newCount = abi.decode(returnData, (uint256));
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Implement Supported Functions">
|
||||
Implement the `getSupportedCallbackFunctions` and `supportsInterface` functions to expose which callback functions and interfaces this core contract supports.
|
||||
|
||||
```solidity
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
```solidity
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Core} from "modular-contracts/src/Core.sol";
|
||||
import {Core} from "@thirdweb-dev/src/Core.sol";
|
||||
|
||||
interface BeforeIncrementCallback {
|
||||
function beforeIncrement(uint256 count) external returns (uint256);
|
||||
}
|
||||
interface BeforeIncrementCallback {
|
||||
function beforeIncrement(uint256 count) external returns (uint256);
|
||||
}
|
||||
|
||||
contract CounterCore is Core {
|
||||
uint256 public count;
|
||||
contract CounterCore is Core {
|
||||
uint256 public count;
|
||||
|
||||
constructor(address owner) {
|
||||
_initializeOwner(owner);
|
||||
}
|
||||
constructor(address owner) {
|
||||
_initializeOwner(owner);
|
||||
}
|
||||
|
||||
function increment() public {
|
||||
uint256 newCount = _beforeIncrement(count);
|
||||
count = newCount;
|
||||
}
|
||||
// 👇👇👇👇👇👇👇👇👇
|
||||
function getSupportedCallbackFunctions()
|
||||
public
|
||||
pure
|
||||
override
|
||||
returns (SupportedCallbackFunction[] memory supportedCallbackFunctions)
|
||||
{
|
||||
supportedCallbackFunctions = new SupportedCallbackFunction[](1);
|
||||
supportedCallbackFunctions[0] = SupportedCallbackFunction({
|
||||
selector: BeforeIncrementCallback.beforeIncrement.selector,
|
||||
mode: CallbackMode.OPTIONAL
|
||||
});
|
||||
}
|
||||
|
||||
function getSupportedCallbackFunctions()
|
||||
public
|
||||
pure
|
||||
override
|
||||
returns (SupportedCallbackFunction[] memory supportedCallbackFunctions)
|
||||
{
|
||||
supportedCallbackFunctions = new SupportedCallbackFunction ;
|
||||
supportedCallbackFunctions[0] = SupportedCallbackFunction({
|
||||
selector: BeforeIncrementCallback.beforeIncrement.selector,
|
||||
mode: CallbackMode.REQUIRED
|
||||
});
|
||||
}
|
||||
function increment() public {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
function supportsInterface(bytes4 interfaceId)
|
||||
public
|
||||
view
|
||||
override
|
||||
returns (bool)
|
||||
{
|
||||
return
|
||||
interfaceId == 0x00000001 || super.supportsInterface(interfaceId);
|
||||
}
|
||||
function _beforeIncrement(
|
||||
uint256 _count
|
||||
) internal returns (uint256 newCount) {
|
||||
(, bytes memory returnData) = _executeCallbackFunction(
|
||||
BeforeIncrementCallback.beforeIncrement.selector,
|
||||
abi.encodeCall(BeforeIncrementCallback.beforeIncrement, (_count))
|
||||
);
|
||||
|
||||
function _beforeIncrement(
|
||||
uint256 count
|
||||
) internal returns (uint256 newCount) {
|
||||
(bool success, bytes memory returndata) = _executeCallbackFunction(
|
||||
BeforeIncrementCallback.beforeIncrement.selector,
|
||||
abi.encodeCall(BeforeIncrementCallback.beforeIncrement, (count))
|
||||
);
|
||||
newCount = abi.decode(returndata, (uint256));
|
||||
}
|
||||
}
|
||||
```
|
||||
newCount = abi.decode(returnData, (uint256));
|
||||
}
|
||||
|
||||
// 👇👇👇👇👇👇👇👇👇
|
||||
function supportsInterface(
|
||||
bytes4 interfaceId
|
||||
) public view override returns (bool) {
|
||||
return interfaceId == 0x00000001 || super.supportsInterface(interfaceId);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -183,5 +232,3 @@ forge remappings > remappings.txt
|
||||
---
|
||||
|
||||
This guide will help you create a core contract that can increment a counter with optional callback functions for additional modular functionality.
|
||||
|
||||
|
||||
|
||||
+268
-308
@@ -21,11 +21,17 @@ export const metadata = createMetadata({
|
||||
<Step title="Install Forge from Foundry">
|
||||
For assistance, refer to the [Foundry installation guide](https://book.getfoundry.sh/getting-started/installation).
|
||||
|
||||
```bash
|
||||
forge init
|
||||
forge install https://github.com/thirdweb-dev/modular-contracts.git
|
||||
forge remappings > remappings.txt
|
||||
```
|
||||
```bash
|
||||
forge init
|
||||
forge install thirdweb-dev/modular-contracts --no-commit
|
||||
```
|
||||
|
||||
Add the Thirdweb modular contracts to `foundry.toml` under `remappings`:
|
||||
|
||||
```toml
|
||||
remappings = ['@thirdweb-dev=lib/modular-contracts/']
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
### Create Module
|
||||
@@ -33,388 +39,342 @@ export const metadata = createMetadata({
|
||||
<Step title="Create a New Module File">
|
||||
Create a new file called `CounterModule.sol` in the `src` folder and start with the following code:
|
||||
|
||||
```solidity
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
```solidity
|
||||
//SPDX-License-Identifier: MIT
|
||||
|
||||
import {Module} from "modular-contracts/src/Module.sol";
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
contract counterModule is Module {}
|
||||
```
|
||||
import {Module} from "@thirdweb-dev/src/Module.sol";
|
||||
|
||||
contract CounterModule is Module {
|
||||
function getModuleConfig()
|
||||
public
|
||||
pure
|
||||
override
|
||||
returns (ModuleConfig memory config)
|
||||
{}
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**
|
||||
> The `Module` contract is the base contract that needs to be inherited for this contract to be recognized as a Module Contract, and we need to implement the `getModuleConfig` function to prevent the contract to be marked as abstract.
|
||||
|
||||
> **Note**
|
||||
> The `Module` contract is the base contract that needs to be inherited for this contract to be recognized as a Module Contract.
|
||||
</Step>
|
||||
|
||||
<Step title="Create a Storage Library">
|
||||
Create a library called `CounterStorage` responsible for holding the state of the Module Contract:
|
||||
|
||||
```solidity
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
```solidity
|
||||
//SPDX-License-Identifier: MIT
|
||||
|
||||
import {Module} from "modular-contracts/src/Module.sol";
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
library CounterStorage {
|
||||
/// @custom:storage-location erc7201:token.minting.counter
|
||||
bytes32 public constant COUNTER_STORAGE_POSITION =
|
||||
keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
|
||||
~bytes32(uint256(0xff));
|
||||
import {Module} from "@thirdweb-dev/src/Module.sol";
|
||||
|
||||
struct Data {
|
||||
uint256 step;
|
||||
}
|
||||
contract CounterModule is Module {
|
||||
function getModuleConfig()
|
||||
public
|
||||
pure
|
||||
override
|
||||
returns (ModuleConfig memory config)
|
||||
{}
|
||||
}
|
||||
|
||||
function data() internal pure returns (Data storage data_) {
|
||||
bytes32 position = COUNTER_STORAGE_POSITION;
|
||||
assembly {
|
||||
data_.slot := position;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 👇👇👇👇👇👇👇👇👇
|
||||
library CounterStorage {
|
||||
/// @custom:storage-location erc7201:token.minting.counter
|
||||
bytes32 public constant COUNTER_STORAGE_POSITION =
|
||||
keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
|
||||
~bytes32(uint256(0xff));
|
||||
|
||||
contract counterModule is Module {}
|
||||
```
|
||||
struct Data {
|
||||
uint256 step;
|
||||
}
|
||||
|
||||
function data() internal pure returns (Data storage data_) {
|
||||
bytes32 position = COUNTER_STORAGE_POSITION;
|
||||
assembly {
|
||||
data_.slot := position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
> **Note**
|
||||
> The library `CounterStorage` uses the ERC-7201: Namespace storage layout to store the data. Learn more about [ERC-7201](https://eips.ethereum.org/EIPS/eip-7201).
|
||||
|
||||
> **Note**
|
||||
> The library `CounterStorage` uses the ERC-7201: Namespace storage layout to store the data. Learn more about [ERC-7201](https://eips.ethereum.org/EIPS/eip-7201).
|
||||
</Step>
|
||||
|
||||
<Step title="Set Up Storage Access Function">
|
||||
Set up the function `_counterStorage` to access the storage from the `CounterStorage` library:
|
||||
|
||||
```solidity
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
```solidity
|
||||
//SPDX-License-Identifier: MIT
|
||||
|
||||
import {Module} from "modular-contracts/src/Module.sol";
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
library CounterStorage {
|
||||
/// @custom:storage-location erc7201:token.minting.counter
|
||||
bytes32 public constant COUNTER_STORAGE_POSITION =
|
||||
keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
|
||||
~bytes32(uint256(0xff));
|
||||
import {Module} from "@thirdweb-dev/src/Module.sol";
|
||||
|
||||
struct Data {
|
||||
uint256 step;
|
||||
}
|
||||
contract CounterModule is Module {
|
||||
function getModuleConfig()
|
||||
public
|
||||
pure
|
||||
override
|
||||
returns (ModuleConfig memory config)
|
||||
{}
|
||||
|
||||
function data() internal pure returns (Data storage data_) {
|
||||
bytes32 position = COUNTER_STORAGE_POSITION;
|
||||
assembly {
|
||||
data_.slot := position;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 👇👇👇👇👇👇👇👇👇
|
||||
|
||||
contract counterModule is Module {
|
||||
/*//////////////////////////////////////////////////////////////
|
||||
Internal Functions
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
function _counterStorage()
|
||||
internal
|
||||
pure
|
||||
returns (CounterStorage.Data storage)
|
||||
{
|
||||
return CounterStorage.data();
|
||||
}
|
||||
}
|
||||
|
||||
library CounterStorage {
|
||||
/// @custom:storage-location erc7201:token.minting.counter
|
||||
bytes32 public constant COUNTER_STORAGE_POSITION =
|
||||
keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
|
||||
~bytes32(uint256(0xff));
|
||||
|
||||
struct Data {
|
||||
uint256 step;
|
||||
}
|
||||
|
||||
fun2ction data() internal pure returns (Data storage data_) {
|
||||
bytes32 position = COUNTER_STORAGE_POSITION;
|
||||
assembly {
|
||||
data_.slot := position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
function _counterStorage()
|
||||
internal
|
||||
pure
|
||||
returns (CounterStorage.Data storage)
|
||||
{
|
||||
return CounterStorage.data();
|
||||
}
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Set Up Fallback Functions">
|
||||
Set up fallback functions that act as the setters and getters for `step`:
|
||||
|
||||
```solidity
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
```solidity
|
||||
//SPDX-License-Identifier: MIT
|
||||
|
||||
import {Module} from "modular-contracts/src/Module.sol";
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
library CounterStorage {
|
||||
/// @custom:storage-location erc7201:token.minting.counter
|
||||
bytes32 public constant COUNTER_STORAGE_POSITION =
|
||||
keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
|
||||
~bytes32(uint256(0xff));
|
||||
import {Module} from "@thirdweb-dev/src/Module.sol";
|
||||
|
||||
struct Data {
|
||||
uint256 step;
|
||||
}
|
||||
contract CounterModule is Module {
|
||||
function getModuleConfig()
|
||||
public
|
||||
pure
|
||||
override
|
||||
returns (ModuleConfig memory config)
|
||||
{}
|
||||
|
||||
function data() internal pure returns (Data storage data_) {
|
||||
bytes32 position = COUNTER_STORAGE_POSITION;
|
||||
assembly {
|
||||
data_.slot := position;
|
||||
}
|
||||
}
|
||||
}
|
||||
function _counterStorage()
|
||||
internal
|
||||
pure
|
||||
returns (CounterStorage.Data storage)
|
||||
{
|
||||
return CounterStorage.data();
|
||||
}
|
||||
|
||||
contract counterModule is Module {
|
||||
/*//////////////////////////////////////////////////////////////
|
||||
Callback & Fallback Functions
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
// 👇👇👇👇👇👇👇👇👇
|
||||
// Fallback Functions
|
||||
function getStep() external view returns (uint256) {
|
||||
return _counterStorage().step;
|
||||
}
|
||||
|
||||
function getStep() external view returns (uint256) {
|
||||
return _counterStorage().step;
|
||||
}
|
||||
function setStep(uint256 _step) external {
|
||||
_counterStorage().step = _step;
|
||||
}
|
||||
}
|
||||
|
||||
function setStep(uint256 step) external {
|
||||
_counterStorage().step = step;
|
||||
}
|
||||
library CounterStorage {
|
||||
/// @custom:storage-location erc7201:token.minting.counter
|
||||
bytes32 public constant COUNTER_STORAGE_POSITION =
|
||||
keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
|
||||
~bytes32(uint256(0xff));
|
||||
|
||||
/*//////////////////////////////////////////////////////////////
|
||||
Internal Functions
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
struct Data {
|
||||
uint256 step;
|
||||
}
|
||||
|
||||
function _counterStorage()
|
||||
internal
|
||||
pure
|
||||
returns (CounterStorage.Data storage)
|
||||
{
|
||||
return CounterStorage.data();
|
||||
}
|
||||
}
|
||||
```
|
||||
function data() internal pure returns (Data storage data_) {
|
||||
bytes32 position = COUNTER_STORAGE_POSITION;
|
||||
assembly {
|
||||
data_.slot := position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
> **Note**
|
||||
> Fallback functions are extra functionalities that a core contract can use via the Solidity `fallback` function.
|
||||
|
||||
> **Note**
|
||||
> Fallback functions are extra functionalities that a core contract can use via the Solidity `fallback` function.
|
||||
</Step>
|
||||
|
||||
<Step title="Set Up Callback Function">
|
||||
Set up a callback function `beforeIncrement` that increases the given count by `step`:
|
||||
|
||||
```solidity
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
```solidity
|
||||
//SPDX-License-Identifier: MIT
|
||||
|
||||
import {Module} from "modular-contracts/src/Module.sol";
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
library CounterStorage {
|
||||
/// @custom:storage-location erc7201:token.minting.counter
|
||||
bytes32 public constant COUNTER_STORAGE_POSITION =
|
||||
keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
|
||||
~bytes32(uint256(0xff));
|
||||
import {Module} from "@thirdweb-dev/src/Module.sol";
|
||||
|
||||
struct Data {
|
||||
uint256 step;
|
||||
}
|
||||
contract CounterModule is Module {
|
||||
function getModuleConfig()
|
||||
public
|
||||
pure
|
||||
override
|
||||
returns (ModuleConfig memory config)
|
||||
{}
|
||||
|
||||
function data() internal pure returns (Data storage data_) {
|
||||
bytes32 position = COUNTER_STORAGE_POSITION;
|
||||
assembly {
|
||||
data_.slot := position;
|
||||
}
|
||||
}
|
||||
}
|
||||
function _counterStorage()
|
||||
internal
|
||||
pure
|
||||
returns (CounterStorage.Data storage)
|
||||
{
|
||||
return CounterStorage.data();
|
||||
}
|
||||
|
||||
contract counterModule is Module {
|
||||
/*//////////////////////////////////////////////////////////////
|
||||
Callback & Fallback Functions
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
// Fallback Functions
|
||||
function getStep() external view returns (uint256) {
|
||||
return _counterStorage().step;
|
||||
}
|
||||
|
||||
function beforeIncrement(uint256 count) external view returns (uint256) {
|
||||
return count + _counterStorage().step;
|
||||
}
|
||||
function setStep(uint256 _step) external {
|
||||
_counterStorage().step = _step;
|
||||
}
|
||||
|
||||
function getStep() external view returns (uint256) {
|
||||
return _counterStorage().step;
|
||||
}
|
||||
|
||||
function setStep(uint256 step) external {
|
||||
_counterStorage().step = step;
|
||||
}
|
||||
// 👇👇👇👇👇👇👇👇👇
|
||||
function beforeIncrement(uint256 count) external view returns (uint256) {
|
||||
return count + _counterStorage().step;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////
|
||||
Internal Functions
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
library CounterStorage {
|
||||
/// @custom:storage-location erc7201:token.minting.counter
|
||||
bytes32 public constant COUNTER_STORAGE_POSITION =
|
||||
keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
|
||||
~bytes32(uint256(0xff));
|
||||
|
||||
function _counterStorage()
|
||||
internal
|
||||
pure
|
||||
returns (CounterStorage.Data storage)
|
||||
{
|
||||
return CounterStorage.data();
|
||||
}
|
||||
}
|
||||
```
|
||||
struct Data {
|
||||
uint256 step;
|
||||
}
|
||||
|
||||
> **Note**
|
||||
> Callback functions are hook-like functionalities that can be used before or after the main functionality of a core contract. In this snippet, the `beforeIncrement` callback is used before the main increment functionality.
|
||||
</Step>
|
||||
function data() internal pure returns (Data storage data_) {
|
||||
bytes32 position = COUNTER_STORAGE_POSITION;
|
||||
assembly {
|
||||
data_.slot := position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<Step title="Set Up Install and Uninstall Functions">
|
||||
Add the `onInstall` and `onUninstall` functions along with helper functions `encodeBytesOnInstall` and `encodeBytesOnUninstall`:
|
||||
```
|
||||
|
||||
```solidity
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
> **Note**
|
||||
> Callback functions are hook-like functionalities that can be used before or after the main functionality of a core contract. In this snippet, the `beforeIncrement` callback is used before the main increment functionality.
|
||||
|
||||
import {Module} from "modular-contracts/src/Module.sol";
|
||||
|
||||
library CounterStorage {
|
||||
/// @custom:storage-location erc7201:token.minting.counter
|
||||
bytes32 public constant COUNTER_STORAGE_POSITION =
|
||||
keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
|
||||
~bytes32(uint256(0xff));
|
||||
|
||||
struct Data {
|
||||
uint256 step;
|
||||
}
|
||||
|
||||
function data() internal pure returns (Data storage data_) {
|
||||
bytes32 position = COUNTER_STORAGE_POSITION;
|
||||
assembly {
|
||||
data_.slot := position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contract counterModule is Module {
|
||||
/*//////////////////////////////////////////////////////////////
|
||||
Install / Uninstall
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
|
||||
function onInstall(bytes calldata data) external {
|
||||
uint256 step = abi.decode(data, (uint256));
|
||||
_counterStorage().step = step;
|
||||
}
|
||||
|
||||
function onUninstall(bytes calldata data) external {}
|
||||
|
||||
function encodeBytesOnInstall(
|
||||
uint256 step
|
||||
) external pure returns (bytes memory) {
|
||||
return abi.encode(step);
|
||||
}
|
||||
|
||||
function encodeBytesOnUninstall() external pure returns (bytes memory) {
|
||||
return "";
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////
|
||||
Callback & Fallback Functions
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
|
||||
function beforeIncrement(uint256 count) external view returns (uint256) {
|
||||
return count + _counterStorage().step;
|
||||
}
|
||||
|
||||
function getStep() external view returns (uint256) {
|
||||
return _counterStorage().step;
|
||||
}
|
||||
|
||||
function setStep(uint256 step) external {
|
||||
_counterStorage().step = step;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////
|
||||
Internal Functions
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
|
||||
function _counterStorage()
|
||||
internal
|
||||
pure
|
||||
returns (CounterStorage.Data storage)
|
||||
{
|
||||
return CounterStorage.data();
|
||||
}
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Set Up Module Config">
|
||||
Lastly, set up the `getModuleConfig` function which is responsible for communicating to the core contract:
|
||||
Lastly, set up the `getModuleConfig` functionallity as this is the one which is responsible for communicating to the core contract:
|
||||
|
||||
```solidity
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
```solidity
|
||||
//SPDX-License-Identifier: MIT
|
||||
|
||||
import {Module} from "modular-contracts/src/Module.sol";
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
library CounterStorage {
|
||||
/// @custom:storage-location erc7201:token.minting.counter
|
||||
bytes32 public constant COUNTER_STORAGE_POSITION =
|
||||
keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
|
||||
~bytes32(uint256(
|
||||
import {Module} from "@thirdweb-dev/src/Module.sol";
|
||||
// 👇👇👇👇👇👇👇👇👇
|
||||
import {Role} from "@thirdweb-dev/src/Role.sol";
|
||||
|
||||
0xff));
|
||||
contract CounterModule is Module {
|
||||
// 👇👇👇👇👇👇👇👇👇
|
||||
function getModuleConfig()
|
||||
public
|
||||
pure
|
||||
override
|
||||
returns (ModuleConfig memory config)
|
||||
{
|
||||
// Callback Function array of one element
|
||||
config.callbackFunctions = new CallbackFunction[](1);
|
||||
// Fallback Function array of two elements
|
||||
config.fallbackFunctions = new FallbackFunction[](2);
|
||||
|
||||
struct Data {
|
||||
uint256 step;
|
||||
}
|
||||
//adding the functions to the arrays
|
||||
config.callbackFunctions[0] = CallbackFunction(
|
||||
this.beforeIncrement.selector
|
||||
);
|
||||
|
||||
function data() internal pure returns (Data storage data_) {
|
||||
bytes32 position = COUNTER_STORAGE_POSITION;
|
||||
assembly {
|
||||
data_.slot := position;
|
||||
}
|
||||
}
|
||||
}
|
||||
config.fallbackFunctions[0] = FallbackFunction({
|
||||
selector: this.getStep.selector,
|
||||
permissionBits: 0
|
||||
});
|
||||
|
||||
contract counterModule is Module {
|
||||
/*//////////////////////////////////////////////////////////////
|
||||
Module Config
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
config.fallbackFunctions[1] = FallbackFunction({
|
||||
selector: this.setStep.selector,
|
||||
permissionBits: Role._MANAGER_ROLE
|
||||
});
|
||||
|
||||
function getModuleConfig()
|
||||
public
|
||||
pure
|
||||
override
|
||||
returns (ModuleConfig memory config)
|
||||
{
|
||||
config.callbackFunctions = new CallbackFunction[](1);
|
||||
config.fallbackFunctions = new FallbackFunction[](2);
|
||||
// Required interfaces for the Module
|
||||
config.requiredInterfaces = new bytes4[](1);
|
||||
config.requiredInterfaces[0] = 0x00000001;
|
||||
|
||||
config.callbackFunctions[0] = CallbackFunction(
|
||||
this.beforeIncrement.selector
|
||||
);
|
||||
// register the intallation callback
|
||||
config.registerInstallationCallback = true;
|
||||
}
|
||||
|
||||
config.fallbackFunctions[0] = FallbackFunction({
|
||||
selector: this.getStep.selector,
|
||||
permissionBits: 0
|
||||
});
|
||||
function _counterStorage()
|
||||
internal
|
||||
pure
|
||||
returns (CounterStorage.Data storage)
|
||||
{
|
||||
return CounterStorage.data();
|
||||
}
|
||||
|
||||
config.fallbackFunctions[1] = FallbackFunction({
|
||||
selector: this.setStep.selector,
|
||||
permissionBits: Role._MANAGER_ROLE
|
||||
});
|
||||
// Fallback Functions
|
||||
function getStep() external view returns (uint256) {
|
||||
return _counterStorage().step;
|
||||
}
|
||||
|
||||
config.requiredInterfaces = new bytes4 ;
|
||||
config.requiredInterfaces[0] = 0x00000001;
|
||||
function setStep(uint256 _step) external {
|
||||
_counterStorage().step = _step;
|
||||
}
|
||||
|
||||
config.registerInstallationCallback = true;
|
||||
}
|
||||
function beforeIncrement(uint256 count) external view returns (uint256) {
|
||||
return count + _counterStorage().step;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////
|
||||
Callback & Fallback Functions
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
library CounterStorage {
|
||||
/// @custom:storage-location erc7201:token.minting.counter
|
||||
bytes32 public constant COUNTER_STORAGE_POSITION =
|
||||
keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
|
||||
~bytes32(uint256(0xff));
|
||||
|
||||
function beforeIncrement(uint256 count) external view returns (uint256) {
|
||||
return count + _counterStorage().step;
|
||||
}
|
||||
struct Data {
|
||||
uint256 step;
|
||||
}
|
||||
|
||||
function getStep() external view returns (uint256) {
|
||||
return _counterStorage().step;
|
||||
}
|
||||
function data() internal pure returns (Data storage data_) {
|
||||
bytes32 position = COUNTER_STORAGE_POSITION;
|
||||
assembly {
|
||||
data_.slot := position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setStep(uint256 step) external {
|
||||
_counterStorage().step = step;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////
|
||||
Internal Functions
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
|
||||
function _counterStorage()
|
||||
internal
|
||||
pure
|
||||
returns (CounterStorage.Data storage)
|
||||
{
|
||||
return CounterStorage.data();
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.63.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5092](https://github.com/thirdweb-dev/js/pull/5092) [`223c497`](https://github.com/thirdweb-dev/js/commit/223c497f491de83078b455090d0e191bec255169) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Ensure smart accounts are deployed before validating signatures
|
||||
|
||||
- [#5089](https://github.com/thirdweb-dev/js/pull/5089) [`541bee5`](https://github.com/thirdweb-dev/js/commit/541bee5b34d36a6abc7edab9c143dc93fa71d558) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix useProfiles not updating when connecting to a different account
|
||||
|
||||
## 5.63.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "thirdweb",
|
||||
"version": "5.63.1",
|
||||
"version": "5.63.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import {} from "viem";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ANVIL_CHAIN } from "~test/chains.js";
|
||||
import { TEST_CONTRACT_URI } from "~test/ipfs-uris.js";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { BaseTransactionOptions } from "../../../transaction/types.js";
|
||||
import {} from "../../../utils/ipfs.js";
|
||||
import {
|
||||
type UploadMetadataParams as CommonUploadMetadataParams,
|
||||
uploadMetadata as commonUploadMetadata,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { BaseTransactionOptions } from "../../../transaction/types.js";
|
||||
import { getAddress } from "../../../utils/address.js";
|
||||
import {} from "../../../utils/date.js";
|
||||
import { mint as generatedMint } from "../__generated__/ERC20Core/write/mint.js";
|
||||
|
||||
// TODO (modular) - this should be its own module
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { BaseTransactionOptions } from "../../../transaction/types.js";
|
||||
import { getAddress } from "../../../utils/address.js";
|
||||
import {} from "../../../utils/date.js";
|
||||
import {
|
||||
getBaseUriFromBatch,
|
||||
uploadOrExtractURIs,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import {} from "abitype";
|
||||
import type { Chain } from "../../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../../client/client.js";
|
||||
import { eth_call } from "../../../rpc/actions/eth_call.js";
|
||||
|
||||
@@ -31,7 +31,7 @@ export function useProfiles(args: {
|
||||
}): UseQueryResult<Profile[]> {
|
||||
const wallet = useAdminWallet();
|
||||
return useQuery({
|
||||
queryKey: ["profiles", wallet?.id],
|
||||
queryKey: ["profiles", wallet?.id, wallet?.getAccount()?.address],
|
||||
enabled: !!wallet && (wallet.id === "inApp" || isEcosystemWallet(wallet)),
|
||||
queryFn: async () => {
|
||||
const ecosystem: Ecosystem | undefined =
|
||||
|
||||
@@ -31,7 +31,7 @@ export function useProfiles(args: {
|
||||
}): UseQueryResult<Profile[]> {
|
||||
const wallet = useAdminWallet();
|
||||
return useQuery({
|
||||
queryKey: ["profiles", wallet?.id],
|
||||
queryKey: ["profiles", wallet?.id, wallet?.getAccount()?.address],
|
||||
enabled: !!wallet && (wallet.id === "inApp" || isEcosystemWallet(wallet)),
|
||||
queryFn: async () => {
|
||||
const ecosystem: Ecosystem | undefined =
|
||||
|
||||
@@ -256,6 +256,11 @@ async function createSmartAccount(
|
||||
account,
|
||||
accountContract,
|
||||
});
|
||||
// the bundler and rpc might not be in sync, so while the bundler has a transaction hash for the deployment,
|
||||
// the rpc might not have it yet, so we wait until the rpc confirms the contract is deployed
|
||||
await confirmContractDeployment({
|
||||
accountContract,
|
||||
});
|
||||
}
|
||||
|
||||
const originalMsgHash = hashMessage(message);
|
||||
@@ -344,6 +349,11 @@ async function createSmartAccount(
|
||||
account,
|
||||
accountContract,
|
||||
});
|
||||
// the bundler and rpc might not be in sync, so while the bundler has a transaction hash for the deployment,
|
||||
// the rpc might not have it yet, so we wait until the rpc confirms the contract is deployed
|
||||
await confirmContractDeployment({
|
||||
accountContract,
|
||||
});
|
||||
}
|
||||
|
||||
const originalMsgHash = hashTypedData(typedData);
|
||||
@@ -607,3 +617,24 @@ async function _sendUserOp(args: {
|
||||
transactionHash: receipt.transactionHash,
|
||||
};
|
||||
}
|
||||
|
||||
async function confirmContractDeployment(args: {
|
||||
accountContract: ThirdwebContract;
|
||||
}) {
|
||||
const { accountContract } = args;
|
||||
const startTime = Date.now();
|
||||
const timeout = 60000; // wait 1 minute max
|
||||
const { isContractDeployed } = await import(
|
||||
"../../utils/bytecode/is-contract-deployed.js"
|
||||
);
|
||||
let isDeployed = await isContractDeployed(accountContract);
|
||||
while (!isDeployed) {
|
||||
if (Date.now() - startTime > timeout) {
|
||||
throw new Error(
|
||||
"Timeout: Smart account deployment not confirmed after 1 minute",
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
isDeployed = await isContractDeployed(accountContract);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
|
||||
import { arbitrumSepolia } from "../../chains/chain-definitions/arbitrum-sepolia.js";
|
||||
import { type ThirdwebContract, getContract } from "../../contract/contract.js";
|
||||
|
||||
import { balanceOf } from "../../extensions/erc1155/__generated__/IERC1155/read/balanceOf.js";
|
||||
import { claimTo } from "../../extensions/erc1155/drops/write/claimTo.js";
|
||||
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
|
||||
@@ -62,6 +61,12 @@ describe.runIf(process.env.TW_SECRET_KEY).skip.sequential(
|
||||
expect(smartWalletAddress).toHaveLength(42);
|
||||
});
|
||||
|
||||
it("can sign a msg", async () => {
|
||||
await smartAccount.signMessage({ message: "hello world" });
|
||||
const isDeployed = await isContractDeployed(accountContract);
|
||||
expect(isDeployed).toEqual(true);
|
||||
});
|
||||
|
||||
it("can execute a tx", async () => {
|
||||
const tx = await sendAndConfirmTransaction({
|
||||
transaction: claimTo({
|
||||
|
||||
Reference in New Issue
Block a user