Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fde39eb883 | ||
|
|
c672a84fc3 | ||
|
|
d52855dbda | ||
|
|
a3909e1794 | ||
|
|
f92201d353 | ||
|
|
45ba811950 | ||
|
|
7c458eaa49 | ||
|
|
f7ce6becd6 | ||
|
|
52d1ba32c4 | ||
|
|
1a7315c815 | ||
|
|
50c6c193c6 | ||
|
|
e114b2c4f9 | ||
|
|
d812cc370b | ||
|
|
7883127d51 |
@@ -503,3 +503,17 @@ export function reportFundWalletSuccessful() {
|
||||
export function reportFundWalletFailed(params: { errorMessage: string }) {
|
||||
posthog.capture("fund wallet failed", params);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track the conversion rate of the users choosing to create a token from new flow instead of the old flow
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*/
|
||||
export function reportTokenUpsellClicked(params: {
|
||||
assetType: "nft" | "coin";
|
||||
pageType: "explore" | "deploy-contract";
|
||||
}) {
|
||||
posthog.capture("token upsell clicked", params);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,13 +10,14 @@ interface SignatureOption {
|
||||
|
||||
interface SignatureSelectorProps {
|
||||
options: SignatureOption[];
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
value: string | string[];
|
||||
onChange: (val: string | string[]) => void;
|
||||
setAbi?: (abi: string) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
secondaryTextFormatter?: (sig: SignatureOption) => string;
|
||||
className?: string;
|
||||
multiSelect?: boolean;
|
||||
}
|
||||
|
||||
export function SignatureSelector({
|
||||
@@ -28,6 +29,7 @@ export function SignatureSelector({
|
||||
disabled,
|
||||
secondaryTextFormatter,
|
||||
className,
|
||||
multiSelect = false,
|
||||
}: SignatureSelectorProps) {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -42,38 +44,96 @@ export function SignatureSelector({
|
||||
}));
|
||||
}, [options, secondaryTextFormatter]);
|
||||
|
||||
// Check if the current value is a custom value (not in options)
|
||||
const isCustomValue = value && !options.some((opt) => opt.value === value);
|
||||
|
||||
// Add the custom value as an option if needed
|
||||
const allOptions = useMemo(() => {
|
||||
if (isCustomValue && value) {
|
||||
return [...formattedOptions, { label: value, value }];
|
||||
// Handle both single and multi-select values
|
||||
const currentValues = useMemo((): string[] => {
|
||||
if (multiSelect) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.filter(
|
||||
(val): val is string =>
|
||||
val !== undefined && val !== null && val !== "",
|
||||
);
|
||||
} else {
|
||||
return value ? [value] : [];
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length > 0 && value[0] ? [value[0]] : [];
|
||||
} else {
|
||||
return value ? [value] : [];
|
||||
}
|
||||
}
|
||||
return formattedOptions;
|
||||
}, [formattedOptions, isCustomValue, value]);
|
||||
}, [value, multiSelect]);
|
||||
|
||||
// Single-select MultiSelect wrapper
|
||||
// Check if the current values include custom values (not in options)
|
||||
const customValues = useMemo((): string[] => {
|
||||
return currentValues.filter(
|
||||
(val): val is string =>
|
||||
val !== undefined &&
|
||||
val !== null &&
|
||||
val !== "" &&
|
||||
!options.some((opt) => opt.value === val),
|
||||
);
|
||||
}, [currentValues, options]);
|
||||
|
||||
// Add the custom values as options if needed
|
||||
const allOptions = useMemo(() => {
|
||||
const customOptions = customValues.map((val) => ({
|
||||
label: val,
|
||||
value: val,
|
||||
}));
|
||||
return [...formattedOptions, ...customOptions];
|
||||
}, [formattedOptions, customValues]);
|
||||
|
||||
// Multi-select or single-select MultiSelect wrapper
|
||||
const handleSelectedValuesChange = useCallback(
|
||||
(selected: string[]) => {
|
||||
// Always use the last selected value for single-select behavior
|
||||
const selectedValue =
|
||||
selected.length > 0 ? (selected[selected.length - 1] ?? "") : "";
|
||||
onChange(selectedValue);
|
||||
const found = options.find((opt) => opt.value === selectedValue);
|
||||
if (setAbi) {
|
||||
setAbi(found?.abi || "");
|
||||
if (multiSelect) {
|
||||
// Multi-select behavior
|
||||
onChange(selected);
|
||||
// For multi-select, we'll use the ABI from the first selected option that has one
|
||||
const firstOptionWithAbi = selected.find((selectedValue) => {
|
||||
const found = options.find((opt) => opt.value === selectedValue);
|
||||
return found?.abi;
|
||||
});
|
||||
if (setAbi && firstOptionWithAbi) {
|
||||
const found = options.find((opt) => opt.value === firstOptionWithAbi);
|
||||
setAbi(found?.abi || "");
|
||||
}
|
||||
} else {
|
||||
// Single-select behavior (maintain backward compatibility)
|
||||
const selectedValue =
|
||||
selected.length > 0 ? (selected[selected.length - 1] ?? "") : "";
|
||||
onChange(selectedValue);
|
||||
const found = options.find((opt) => opt.value === selectedValue);
|
||||
if (setAbi) {
|
||||
setAbi(found?.abi || "");
|
||||
}
|
||||
}
|
||||
setSearchValue("");
|
||||
},
|
||||
[onChange, setAbi, options],
|
||||
[onChange, setAbi, options, multiSelect],
|
||||
);
|
||||
|
||||
// Handle custom value entry
|
||||
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "Enter" && searchValue.trim()) {
|
||||
if (!options.some((opt) => opt.value === searchValue.trim())) {
|
||||
onChange(searchValue.trim());
|
||||
if (multiSelect) {
|
||||
// Add to existing values for multi-select
|
||||
const currentArray = Array.isArray(value)
|
||||
? value
|
||||
: value
|
||||
? [value]
|
||||
: [];
|
||||
const filteredArray = currentArray.filter(
|
||||
(val): val is string => val !== undefined && val !== null,
|
||||
);
|
||||
const newValues = [...filteredArray, searchValue.trim()];
|
||||
onChange(newValues);
|
||||
} else {
|
||||
// Replace value for single-select
|
||||
onChange(searchValue.trim());
|
||||
}
|
||||
if (setAbi) setAbi("");
|
||||
setSearchValue("");
|
||||
// Optionally blur input
|
||||
@@ -106,7 +166,7 @@ export function SignatureSelector({
|
||||
customSearchInput={customSearchInput}
|
||||
customTrigger={null}
|
||||
disabled={disabled}
|
||||
maxCount={1}
|
||||
maxCount={multiSelect ? 100 : 1}
|
||||
onSelectedValuesChange={handleSelectedValuesChange}
|
||||
options={allOptions}
|
||||
overrideSearchFn={(option, searchTerm) =>
|
||||
@@ -116,11 +176,13 @@ export function SignatureSelector({
|
||||
placeholder={placeholder}
|
||||
renderOption={(option) => <span>{option.label}</span>}
|
||||
searchPlaceholder={placeholder}
|
||||
selectedValues={value ? [value] : []}
|
||||
selectedValues={currentValues}
|
||||
/>
|
||||
{isCustomValue && (
|
||||
{customValues.length > 0 && (
|
||||
<div className="mt-2 rounded border border-warning-200 bg-warning-50 px-2 py-1 text-warning-700 text-xs">
|
||||
You entered a custom signature. Please provide the ABI below.
|
||||
{multiSelect
|
||||
? `You entered ${customValues.length} custom signature${customValues.length > 1 ? "s" : ""}. Please provide the ABI below.`
|
||||
: "You entered a custom signature. Please provide the ABI below."}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { XIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
|
||||
export function DismissibleAlert(props: {
|
||||
export function DismissibleAlert(
|
||||
props: {
|
||||
title: React.ReactNode;
|
||||
header?: React.ReactNode;
|
||||
className?: string;
|
||||
description: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
} & (
|
||||
| {
|
||||
preserveState: true;
|
||||
localStorageId: string;
|
||||
}
|
||||
| {
|
||||
preserveState: false;
|
||||
}
|
||||
),
|
||||
) {
|
||||
if (props.preserveState) {
|
||||
return <DismissibleAlertWithLocalStorage {...props} />;
|
||||
}
|
||||
|
||||
return <DismissibleAlertWithoutLocalStorage {...props} />;
|
||||
}
|
||||
|
||||
function DismissibleAlertWithLocalStorage(props: {
|
||||
title: React.ReactNode;
|
||||
header?: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
localStorageId: string;
|
||||
}) {
|
||||
const [isVisible, setIsVisible] = useLocalStorage(
|
||||
@@ -17,19 +44,48 @@ export function DismissibleAlert(props: {
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return <AlertUI {...props} onClose={() => setIsVisible(false)} />;
|
||||
}
|
||||
|
||||
function DismissibleAlertWithoutLocalStorage(props: {
|
||||
title: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return <AlertUI {...props} onClose={() => setIsVisible(false)} />;
|
||||
}
|
||||
|
||||
function AlertUI(props: {
|
||||
title: React.ReactNode;
|
||||
header?: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative rounded-lg border border-border bg-card p-4">
|
||||
<Button
|
||||
aria-label="Close alert"
|
||||
className="absolute top-4 right-4 h-auto w-auto p-1 text-muted-foreground"
|
||||
onClick={() => setIsVisible(false)}
|
||||
variant="ghost"
|
||||
>
|
||||
<XIcon className="size-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h2 className="mb-0.5 font-semibold">{props.title} </h2>
|
||||
<div className="text-muted-foreground text-sm">{props.description}</div>
|
||||
<div className={props.className}>
|
||||
<div className="relative rounded-lg border border-border bg-card p-4 lg:p-6 overflow-hidden">
|
||||
<Button
|
||||
aria-label="Close alert"
|
||||
className="absolute top-4 right-4 h-auto w-auto p-1 text-muted-foreground"
|
||||
onClick={props.onClose}
|
||||
variant="ghost"
|
||||
>
|
||||
<XIcon className="size-5" />
|
||||
</Button>
|
||||
<div>
|
||||
{props.header}
|
||||
<h2 className="mb-0.5 font-semibold">{props.title} </h2>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{props.description}
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -64,6 +64,7 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
popoverContentClassName,
|
||||
showSelectedValuesInModal = false,
|
||||
customSearchInput,
|
||||
customTrigger,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -144,13 +145,18 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
// scroll to top when options change
|
||||
const popoverElRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Filter out customTrigger from props to avoid passing it to Button
|
||||
const buttonProps = Object.fromEntries(
|
||||
Object.entries(props).filter(([key]) => key !== "customTrigger"),
|
||||
) as React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
return (
|
||||
<Popover modal onOpenChange={setIsPopoverOpen} open={isPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
{props.customTrigger || (
|
||||
{customTrigger || (
|
||||
<Button
|
||||
ref={ref}
|
||||
{...props}
|
||||
{...buttonProps}
|
||||
className={cn(
|
||||
"flex h-auto min-h-10 w-full items-center justify-between rounded-md border border-border bg-inherit p-3 hover:bg-inherit",
|
||||
className,
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { moduleToBase64 } from "app/(app)/(dashboard)/published-contract/utils/module-base-64";
|
||||
import { RocketIcon, ShieldCheckIcon } from "lucide-react";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import Link from "next/link";
|
||||
import { resolveAvatar } from "thirdweb/extensions/ens";
|
||||
import { fetchPublishedContractVersion } from "@/api/contract/fetch-contracts-with-versions";
|
||||
import { ClientOnly } from "@/components/blocks/client-only";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { resolveEns } from "@/lib/ens";
|
||||
import { replaceDeployerAddress } from "@/lib/publisher-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { ContractPublisher } from "./contract-publisher";
|
||||
|
||||
interface ContractCardProps {
|
||||
type ContractCardProps = {
|
||||
publisher: string;
|
||||
contractId: string;
|
||||
titleOverride?: string;
|
||||
@@ -29,7 +30,7 @@ interface ContractCardProps {
|
||||
moduleId: string;
|
||||
version?: string;
|
||||
}[];
|
||||
}
|
||||
};
|
||||
|
||||
function getContractUrl(
|
||||
{
|
||||
@@ -74,6 +75,72 @@ function getContractUrl(
|
||||
return replaceDeployerAddress(pathName);
|
||||
}
|
||||
|
||||
const cached_fetchPublishedContractVersion = unstable_cache(
|
||||
async (publisher: string, contractId: string, version: string = "latest") => {
|
||||
const result = await fetchPublishedContractVersion(
|
||||
publisher,
|
||||
contractId,
|
||||
serverThirdwebClient,
|
||||
version,
|
||||
).catch(() => null);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const publisherEnsAndAvatar = result.publisher
|
||||
? await cached_resolvePublisherEnsAndAvatar(result.publisher)
|
||||
: undefined;
|
||||
|
||||
// Note: Do not return BigInt - it can't be serialized and cached by unstable_cache and will throw an error
|
||||
return {
|
||||
name: result.name,
|
||||
displayName: result.displayName,
|
||||
description: result.description,
|
||||
publisher: {
|
||||
address: result.publisher,
|
||||
ensName: publisherEnsAndAvatar?.ensName,
|
||||
ensAvatar: publisherEnsAndAvatar?.ensAvatar,
|
||||
},
|
||||
version: result.version,
|
||||
audit: result.audit,
|
||||
};
|
||||
},
|
||||
["fetchPublishedContractVersion"],
|
||||
{
|
||||
revalidate: 3600, // 1 hour
|
||||
},
|
||||
);
|
||||
|
||||
const cached_resolvePublisherEnsAndAvatar = unstable_cache(
|
||||
async (_addressOrEns: string) => {
|
||||
const addressOrEns = replaceDeployerAddress(_addressOrEns);
|
||||
const [ensNameInfo, ensAvatar] = await Promise.allSettled([
|
||||
resolveEns(addressOrEns, serverThirdwebClient),
|
||||
resolveAvatar({
|
||||
client: serverThirdwebClient,
|
||||
name: addressOrEns,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
ensName:
|
||||
ensNameInfo.status === "fulfilled"
|
||||
? ensNameInfo.value?.ensName
|
||||
: undefined,
|
||||
address:
|
||||
ensNameInfo.status === "fulfilled"
|
||||
? ensNameInfo.value?.address
|
||||
: undefined,
|
||||
ensAvatar: ensAvatar.status === "fulfilled" ? ensAvatar.value : undefined,
|
||||
};
|
||||
},
|
||||
["resolvePublisherEnsAndAvatar"],
|
||||
{
|
||||
revalidate: 3600, // 1 hour
|
||||
},
|
||||
);
|
||||
|
||||
export async function ContractCard({
|
||||
publisher,
|
||||
contractId,
|
||||
@@ -83,12 +150,11 @@ export async function ContractCard({
|
||||
modules = [],
|
||||
isBeta,
|
||||
}: ContractCardProps) {
|
||||
const publishedContractResult = await fetchPublishedContractVersion(
|
||||
const publishedContractResult = await cached_fetchPublishedContractVersion(
|
||||
publisher,
|
||||
contractId,
|
||||
serverThirdwebClient,
|
||||
version,
|
||||
).catch(() => null);
|
||||
);
|
||||
|
||||
if (!publishedContractResult) {
|
||||
return null;
|
||||
@@ -186,13 +252,12 @@ export async function ContractCard({
|
||||
!modules?.length && "mt-auto",
|
||||
)}
|
||||
>
|
||||
{publishedContractResult.publisher && (
|
||||
<ClientOnly ssr={<Skeleton className="size-5 rounded-full" />}>
|
||||
<ContractPublisher
|
||||
addressOrEns={publishedContractResult.publisher}
|
||||
client={getClientThirdwebClient()}
|
||||
/>
|
||||
</ClientOnly>
|
||||
{publishedContractResult.publisher.address && (
|
||||
<ContractPublisher
|
||||
address={publishedContractResult.publisher.address}
|
||||
ensName={publishedContractResult.publisher.ensName || undefined}
|
||||
ensAvatar={publishedContractResult.publisher.ensAvatar || undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -1,53 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import {
|
||||
AccountAddress,
|
||||
AccountAvatar,
|
||||
AccountBlobbie,
|
||||
AccountName,
|
||||
AccountProvider,
|
||||
} from "thirdweb/react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { replaceDeployerAddress } from "@/lib/publisher-utils";
|
||||
import { Blobbie } from "thirdweb/react";
|
||||
import { Img } from "@/components/blocks/Img";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { shortenIfAddress } from "@/utils/usedapp-external";
|
||||
|
||||
interface ContractPublisherProps {
|
||||
addressOrEns: string;
|
||||
client: ThirdwebClient;
|
||||
}
|
||||
|
||||
export const ContractPublisher: React.FC<ContractPublisherProps> = ({
|
||||
addressOrEns,
|
||||
client,
|
||||
}) => {
|
||||
export function ContractPublisher(props: {
|
||||
ensName: string | undefined;
|
||||
address: string;
|
||||
ensAvatar: string | undefined;
|
||||
}) {
|
||||
const displayName = props.ensName || props.address;
|
||||
return (
|
||||
<AccountProvider address={addressOrEns} client={client}>
|
||||
<Link
|
||||
className="flex shrink-0 items-center gap-1.5 hover:underline"
|
||||
href={replaceDeployerAddress(`/${addressOrEns}`)}
|
||||
>
|
||||
<AccountAvatar
|
||||
className="size-5 rounded-full object-cover"
|
||||
fallbackComponent={<AccountBlobbie className="size-5 rounded-full" />}
|
||||
loadingComponent={<Skeleton className="size-5 rounded-full" />}
|
||||
/>
|
||||
<Link
|
||||
className="flex shrink-0 items-center gap-1.5 hover:underline"
|
||||
href={`/${displayName}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Img
|
||||
className="size-5 rounded-full object-cover"
|
||||
src={
|
||||
resolveSchemeWithErrorHandler({
|
||||
client: getClientThirdwebClient(),
|
||||
uri: props.ensAvatar,
|
||||
}) || ""
|
||||
}
|
||||
fallback={<Blobbie address={props.address} />}
|
||||
/>
|
||||
|
||||
<AccountName
|
||||
className="text-xs"
|
||||
fallbackComponent={
|
||||
<AccountAddress
|
||||
className="text-xs"
|
||||
formatFn={(addr) =>
|
||||
shortenIfAddress(replaceDeployerAddress(addr))
|
||||
}
|
||||
/>
|
||||
}
|
||||
formatFn={(name) => replaceDeployerAddress(name)}
|
||||
loadingComponent={<Skeleton className="h-4 w-40" />}
|
||||
/>
|
||||
</Link>
|
||||
</AccountProvider>
|
||||
<span className="text-xs"> {shortenIfAddress(displayName)}</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,11 +7,7 @@ import {
|
||||
} from "@/components/contracts/contract-card/contract-card";
|
||||
import type { ExploreCategory } from "../../data";
|
||||
|
||||
interface ContractRowProps {
|
||||
category: ExploreCategory;
|
||||
}
|
||||
|
||||
export function ContractRow({ category }: ContractRowProps) {
|
||||
export function ContractRow({ category }: { category: ExploreCategory }) {
|
||||
return (
|
||||
<section>
|
||||
{/* Title, Description + View all link */}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ContractRow } from "./components/contract-row";
|
||||
import { DeployUpsellCard } from "./components/upsells/deploy-your-own";
|
||||
import { PublishUpsellCard } from "./components/upsells/publish-submit";
|
||||
import { EXPLORE_PAGE_DATA } from "./data";
|
||||
import { TokensSection } from "./tokens-section";
|
||||
|
||||
const title = "List of smart contracts for EVM Developers";
|
||||
const description =
|
||||
@@ -22,11 +23,11 @@ export default async function ExplorePage() {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="border-b py-10">
|
||||
<div className="container">
|
||||
<h1 className="mb-2 font-bold text-3xl lg:text-5xl tracking-tighter">
|
||||
<div className="container max-w-7xl">
|
||||
<h1 className="mb-2 font-bold text-4xl lg:text-5xl tracking-tight">
|
||||
Explore
|
||||
</h1>
|
||||
<p className="max-w-screen-sm text-base text-muted-foreground">
|
||||
<p className="max-w-screen-sm text-sm lg:text-base text-muted-foreground">
|
||||
The best place for web3 developers to explore smart contracts from
|
||||
world-class web3 protocols & engineers — all deployable with one
|
||||
click.
|
||||
@@ -34,7 +35,11 @@ export default async function ExplorePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container flex flex-col gap-4 py-10">
|
||||
<div className="container max-w-7xl pt-8 pb-4">
|
||||
<TokensSection />
|
||||
</div>
|
||||
|
||||
<div className="container max-w-7xl flex flex-col gap-8 py-10">
|
||||
<div className="flex flex-col gap-14">
|
||||
{EXPLORE_PAGE_DATA.map((category, idx) => (
|
||||
<Fragment key={category.id}>
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { ZapIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { reportTokenUpsellClicked } from "@/analytics/report";
|
||||
import { GridPattern } from "@/components/ui/background-patterns";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function TokensSection() {
|
||||
return (
|
||||
<div className="bg-card p-4 lg:py-8 lg:px-6 border rounded-xl relative w-full overflow-hidden">
|
||||
<GridPattern
|
||||
width={30}
|
||||
height={30}
|
||||
strokeDasharray={"4 2"}
|
||||
className="text-border dark:text-border/70 hidden lg:block translate-x-5"
|
||||
style={{
|
||||
maskImage:
|
||||
"linear-gradient(to bottom left,white,transparent,transparent)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-sm bg-background h-auto px-3 py-1 gap-2 mb-4"
|
||||
>
|
||||
<div className="rounded-full bg-primary size-2" />
|
||||
New
|
||||
</Badge>
|
||||
|
||||
<h2 className="font-semibold text-2xl tracking-tight mb-1">
|
||||
Launch Your Tokens Effortlessly
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-6 text-sm lg:text-base">
|
||||
Deploy contract and configure all settings you need to launch your token
|
||||
in one seamless flow
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-4xl">
|
||||
<CardLink
|
||||
assetType="coin"
|
||||
title="Launch Coin"
|
||||
description="Launch an ERC-20 token to create a cryptocurrency"
|
||||
href="/team/~/~project/tokens/create/token"
|
||||
bullets={[
|
||||
"Deploy Contract",
|
||||
"Configure Price and Supply",
|
||||
"Airdrop Tokens",
|
||||
]}
|
||||
/>
|
||||
|
||||
<CardLink
|
||||
assetType="nft"
|
||||
title="Launch NFT Collection"
|
||||
description="Launch an ERC-721 or ERC-1155 NFT collection"
|
||||
href="/team/~/~project/tokens/create/nft"
|
||||
bullets={[
|
||||
"Deploy Contract",
|
||||
"Upload NFTs",
|
||||
"Configure Price and Supply",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CardLink(props: {
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
assetType: "nft" | "coin";
|
||||
bullets: string[];
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex flex-col rounded-lg border p-4 cursor-pointer hover:border-active-border bg-background",
|
||||
)}
|
||||
>
|
||||
<div className="mb-3 flex">
|
||||
<div className="flex items-center justify-center rounded-full border p-2 bg-card">
|
||||
<ZapIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="mb-0.5 font-semibold text-lg tracking-tight">
|
||||
<Link
|
||||
className="before:absolute before:inset-0"
|
||||
href={props.href}
|
||||
onClick={() => {
|
||||
reportTokenUpsellClicked({
|
||||
assetType: props.assetType,
|
||||
pageType: "explore",
|
||||
});
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">{props.description}</p>
|
||||
|
||||
<ul className="mt-4 space-y-1 text-sm list-disc list-inside text-muted-foreground">
|
||||
{props.bullets.map((bullet) => (
|
||||
<li key={bullet}>{bullet}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+22
@@ -10,6 +10,7 @@ import { ZERO_FEE_VERSIONS } from "@/constants/fee-config";
|
||||
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { PublishedContractBreadcrumbs } from "../[publisher]/[contract_id]/components/breadcrumbs.client";
|
||||
import { DeployContractHeader } from "./contract-header";
|
||||
import { TokenBanner } from "./token-banner";
|
||||
import { DeployFormForUri } from "./uri-based-deploy";
|
||||
|
||||
type PublishBasedDeployProps = {
|
||||
@@ -95,6 +96,10 @@ export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) {
|
||||
),
|
||||
]);
|
||||
|
||||
const isTWPublisher =
|
||||
contractMetadata?.publisher ===
|
||||
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="border-border border-b border-dashed">
|
||||
@@ -111,6 +116,23 @@ export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isTWPublisher &&
|
||||
(contractMetadata.name === "DropERC20" ||
|
||||
contractMetadata.name === "TokenERC20") && (
|
||||
<TokenBanner type="erc20" />
|
||||
)}
|
||||
{isTWPublisher &&
|
||||
(contractMetadata.name === "DropERC721" ||
|
||||
contractMetadata.name === "TokenERC721") && (
|
||||
<TokenBanner type="erc721" />
|
||||
)}
|
||||
|
||||
{isTWPublisher &&
|
||||
(contractMetadata.name === "DropERC1155" ||
|
||||
contractMetadata.name === "TokenERC1155") && (
|
||||
<TokenBanner type="erc1155" />
|
||||
)}
|
||||
|
||||
<div className="container max-w-5xl py-8">
|
||||
<DeployFormForUri
|
||||
contractMetadata={contractMetadata}
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { reportTokenUpsellClicked } from "@/analytics/report";
|
||||
import { DismissibleAlert } from "@/components/blocks/dismissible-alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function TokenBanner(props: { type: "erc20" | "erc721" | "erc1155" }) {
|
||||
const title =
|
||||
props.type === "erc20"
|
||||
? "Launch Your Token Effortlessly"
|
||||
: "Launch Your NFT Collection Effortlessly";
|
||||
const description =
|
||||
props.type === "erc20"
|
||||
? "Deploy contract, set price and supply, airdrop tokens all in one seamless flow"
|
||||
: props.type === "erc721"
|
||||
? "Deploy contract, upload NFTs, set price all in one seamless flow"
|
||||
: "Deploy contract, upload NFTs, set prices and supply all in one seamless flow";
|
||||
const href =
|
||||
props.type === "erc20"
|
||||
? "/team/~/~project/tokens/create/token"
|
||||
: "/team/~/~project/tokens/create/nft";
|
||||
|
||||
return (
|
||||
<DismissibleAlert
|
||||
header={
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-background gap-1.5 py-1 px-2.5 h-auto mb-3"
|
||||
>
|
||||
<div className="rounded-full bg-primary size-1.5" />
|
||||
New
|
||||
</Badge>
|
||||
}
|
||||
title={title}
|
||||
className="container max-w-5xl mt-8"
|
||||
preserveState={false}
|
||||
description={description}
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="gap-2 rounded-full mt-4 px-6"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href={href}
|
||||
target="_blank"
|
||||
onClick={() => {
|
||||
reportTokenUpsellClicked({
|
||||
assetType: props.type === "erc20" ? "coin" : "nft",
|
||||
pageType: "deploy-contract",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Try Now <ArrowUpRightIcon className="size-3.5" />
|
||||
</Link>
|
||||
</Button>
|
||||
</DismissibleAlert>
|
||||
);
|
||||
}
|
||||
@@ -38,7 +38,10 @@ export default async function Page(props: {
|
||||
});
|
||||
|
||||
const projects = await getProjects(params.team_slug);
|
||||
const projectsWithTotalWallets = await getProjectsWithAnalytics(projects);
|
||||
const projectsWithTotalWallets = await getProjectsWithAnalytics(
|
||||
projects,
|
||||
authToken,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
@@ -64,6 +67,7 @@ export default async function Page(props: {
|
||||
<FreePlanUpsellBannerUI highlightPlan="growth" teamSlug={team.slug} />
|
||||
) : (
|
||||
<DismissibleAlert
|
||||
preserveState={true}
|
||||
description="Engines, contracts, project settings, and more are now managed within projects. Open or create a project to access them."
|
||||
localStorageId={`${team.id}-engines-alert`}
|
||||
title="Looking for Engines?"
|
||||
@@ -78,6 +82,7 @@ export default async function Page(props: {
|
||||
|
||||
async function getProjectsWithAnalytics(
|
||||
projects: Project[],
|
||||
authToken: string,
|
||||
): Promise<Array<ProjectWithAnalytics>> {
|
||||
return Promise.all(
|
||||
projects.map(async (project) => {
|
||||
@@ -85,13 +90,16 @@ async function getProjectsWithAnalytics(
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = subDays(today, 30);
|
||||
|
||||
const data = await getWalletConnections({
|
||||
from: thirtyDaysAgo,
|
||||
period: "all",
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
to: today,
|
||||
});
|
||||
const data = await getWalletConnections(
|
||||
{
|
||||
from: thirtyDaysAgo,
|
||||
period: "all",
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
to: today,
|
||||
},
|
||||
authToken,
|
||||
);
|
||||
|
||||
let uniqueWalletsConnected = 0;
|
||||
for (const d of data) {
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ export function TeamHighlightsCard({
|
||||
activeChart={
|
||||
selectedChart && selectedChart in chartConfig
|
||||
? (selectedChart as keyof AggregatedMetrics)
|
||||
: "totalVolume"
|
||||
: "activeUsers"
|
||||
}
|
||||
aggregateFn={(_data, key) => {
|
||||
if (key === "activeUsers") {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
getWalletConnections,
|
||||
getWalletUsers,
|
||||
} from "@/api/analytics";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getTeamBySlug } from "@/api/team/get-team";
|
||||
import type {
|
||||
DurationId,
|
||||
@@ -28,6 +29,7 @@ import type {
|
||||
UserOpStats,
|
||||
WalletStats,
|
||||
} from "@/types/analytics";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { PieChartCard } from "../../../../components/Analytics/PieChartCard";
|
||||
import { TotalSponsoredChartCardUI } from "../../_components/TotalSponsoredCard";
|
||||
import { TransactionsChartCardWithChainMapping } from "../../_components/transaction-card-with-chain-mapping";
|
||||
@@ -53,6 +55,12 @@ export default async function TeamOverviewPage(props: {
|
||||
props.searchParams,
|
||||
]);
|
||||
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (!authToken) {
|
||||
loginRedirect(`/team/${params.team_slug}/~/analytics`);
|
||||
}
|
||||
|
||||
const team = await getTeamBySlug(params.team_slug);
|
||||
|
||||
if (!team) {
|
||||
@@ -91,6 +99,7 @@ export default async function TeamOverviewPage(props: {
|
||||
fallback={<LoadingChartState className="h-[458px] border" />}
|
||||
>
|
||||
<AsyncTeamHighlightsCard
|
||||
authToken={authToken}
|
||||
selectedChartQueryParam="appHighlights"
|
||||
interval={interval}
|
||||
range={range}
|
||||
@@ -108,7 +117,11 @@ export default async function TeamOverviewPage(props: {
|
||||
fallback={<LoadingChartState className="h-[431px] border" />}
|
||||
searchParamsUsed={["from", "to"]}
|
||||
>
|
||||
<AsyncWalletDistributionCard range={range} teamId={team.id} />
|
||||
<AsyncWalletDistributionCard
|
||||
range={range}
|
||||
teamId={team.id}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
|
||||
<ResponsiveSuspense
|
||||
@@ -118,6 +131,7 @@ export default async function TeamOverviewPage(props: {
|
||||
<AsyncAuthMethodDistributionCard
|
||||
range={range}
|
||||
teamId={team.id}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
</div>
|
||||
@@ -133,6 +147,7 @@ export default async function TeamOverviewPage(props: {
|
||||
>
|
||||
<AsyncTransactionsChartCard
|
||||
selectedChartQueryParam="client_transactions"
|
||||
authToken={authToken}
|
||||
interval={interval}
|
||||
range={range}
|
||||
selectedChart={
|
||||
@@ -149,6 +164,7 @@ export default async function TeamOverviewPage(props: {
|
||||
searchParamsUsed={["from", "to", "interval", "userOpUsage"]}
|
||||
>
|
||||
<AsyncTotalSponsoredCard
|
||||
authToken={authToken}
|
||||
selectedChartQueryParam="userOpUsage"
|
||||
interval={interval}
|
||||
range={range}
|
||||
@@ -173,21 +189,28 @@ async function AsyncTeamHighlightsCard(props: {
|
||||
interval: "day" | "week";
|
||||
selectedChart: string | undefined;
|
||||
selectedChartQueryParam: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const [walletUserStatsTimeSeries, universalBridgeUsage] =
|
||||
await Promise.allSettled([
|
||||
getWalletUsers({
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
}),
|
||||
getUniversalBridgeUsage({
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
}),
|
||||
getWalletUsers(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
getUniversalBridgeUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
]);
|
||||
|
||||
if (
|
||||
@@ -216,13 +239,17 @@ async function AsyncTeamHighlightsCard(props: {
|
||||
async function AsyncWalletDistributionCard(props: {
|
||||
teamId: string;
|
||||
range: Range;
|
||||
authToken: string;
|
||||
}) {
|
||||
const walletConnections = await getWalletConnections({
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
}).catch(() => undefined);
|
||||
const walletConnections = await getWalletConnections(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch(() => undefined);
|
||||
|
||||
return walletConnections && walletConnections.length > 0 ? (
|
||||
<WalletDistributionCard data={walletConnections} />
|
||||
@@ -237,13 +264,17 @@ async function AsyncWalletDistributionCard(props: {
|
||||
async function AsyncAuthMethodDistributionCard(props: {
|
||||
teamId: string;
|
||||
range: Range;
|
||||
authToken: string;
|
||||
}) {
|
||||
const inAppWalletUsage = await getInAppWalletUsage({
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
}).catch(() => undefined);
|
||||
const inAppWalletUsage = await getInAppWalletUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch(() => undefined);
|
||||
|
||||
return inAppWalletUsage && inAppWalletUsage.length > 0 ? (
|
||||
<AuthMethodDistributionCard data={inAppWalletUsage} />
|
||||
@@ -261,21 +292,28 @@ async function AsyncTransactionsChartCard(props: {
|
||||
interval: "day" | "week";
|
||||
selectedChart: string | undefined;
|
||||
selectedChartQueryParam: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const [clientTransactionsTimeSeries, clientTransactions] =
|
||||
await Promise.allSettled([
|
||||
getClientTransactions({
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
}),
|
||||
getClientTransactions({
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
}),
|
||||
getClientTransactions(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
getClientTransactions(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
]);
|
||||
|
||||
return clientTransactionsTimeSeries.status === "fulfilled" &&
|
||||
@@ -301,20 +339,27 @@ async function AsyncTotalSponsoredCard(props: {
|
||||
interval: "day" | "week";
|
||||
selectedChart: string | undefined;
|
||||
selectedChartQueryParam: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const [userOpUsageTimeSeries, userOpUsage] = await Promise.allSettled([
|
||||
getUserOpUsage({
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
}),
|
||||
getUserOpUsage({
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
}),
|
||||
getUserOpUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
getUserOpUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
teamId: props.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
]);
|
||||
|
||||
return userOpUsageTimeSeries.status === "fulfilled" &&
|
||||
|
||||
+38
-23
@@ -18,6 +18,7 @@ export async function EcosystemAnalyticsPage({
|
||||
range,
|
||||
partners,
|
||||
defaultRange,
|
||||
authToken,
|
||||
}: {
|
||||
ecosystemSlug: string;
|
||||
teamId: string;
|
||||
@@ -25,6 +26,7 @@ export async function EcosystemAnalyticsPage({
|
||||
range: Range;
|
||||
defaultRange: DurationId;
|
||||
partners: Partner[];
|
||||
authToken: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
@@ -40,6 +42,7 @@ export async function EcosystemAnalyticsPage({
|
||||
<AsyncEcosystemWalletsSummary
|
||||
ecosystemSlug={ecosystemSlug}
|
||||
teamId={teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
@@ -66,6 +69,7 @@ export async function EcosystemAnalyticsPage({
|
||||
partners={partners}
|
||||
range={range}
|
||||
teamId={teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
</div>
|
||||
@@ -75,24 +79,31 @@ export async function EcosystemAnalyticsPage({
|
||||
async function AsyncEcosystemWalletsSummary(props: {
|
||||
ecosystemSlug: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const { ecosystemSlug, teamId } = props;
|
||||
const { ecosystemSlug, teamId, authToken } = props;
|
||||
|
||||
const allTimeStatsPromise = getEcosystemWalletUsage({
|
||||
ecosystemSlug,
|
||||
from: new Date(2022, 0, 1),
|
||||
period: "all",
|
||||
teamId,
|
||||
to: new Date(),
|
||||
});
|
||||
const allTimeStatsPromise = getEcosystemWalletUsage(
|
||||
{
|
||||
ecosystemSlug,
|
||||
from: new Date(2022, 0, 1),
|
||||
period: "all",
|
||||
teamId,
|
||||
to: new Date(),
|
||||
},
|
||||
authToken,
|
||||
);
|
||||
|
||||
const monthlyStatsPromise = getEcosystemWalletUsage({
|
||||
ecosystemSlug,
|
||||
from: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
period: "month",
|
||||
teamId,
|
||||
to: new Date(),
|
||||
});
|
||||
const monthlyStatsPromise = getEcosystemWalletUsage(
|
||||
{
|
||||
ecosystemSlug,
|
||||
from: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
period: "month",
|
||||
teamId,
|
||||
to: new Date(),
|
||||
},
|
||||
authToken,
|
||||
);
|
||||
|
||||
const [allTimeStats, monthlyStats] = await Promise.all([
|
||||
allTimeStatsPromise,
|
||||
@@ -114,16 +125,20 @@ async function AsyncEcosystemWalletUsersAnalytics(props: {
|
||||
interval: "day" | "week";
|
||||
range: Range;
|
||||
partners: Partner[];
|
||||
authToken: string;
|
||||
}) {
|
||||
const { ecosystemSlug, teamId, interval, range, partners } = props;
|
||||
const { ecosystemSlug, teamId, interval, range, partners, authToken } = props;
|
||||
|
||||
const stats = await getEcosystemWalletUsage({
|
||||
ecosystemSlug,
|
||||
from: range.from,
|
||||
period: interval,
|
||||
teamId,
|
||||
to: range.to,
|
||||
}).catch(() => null);
|
||||
const stats = await getEcosystemWalletUsage(
|
||||
{
|
||||
ecosystemSlug,
|
||||
from: range.from,
|
||||
period: interval,
|
||||
teamId,
|
||||
to: range.to,
|
||||
},
|
||||
authToken,
|
||||
).catch(() => null);
|
||||
|
||||
return (
|
||||
<EcosystemWalletUsersAnalyticsUI
|
||||
|
||||
+1
@@ -61,6 +61,7 @@ export default async function Page(props: {
|
||||
return (
|
||||
<ResponsiveSearchParamsProvider value={searchParams}>
|
||||
<EcosystemAnalyticsPage
|
||||
authToken={authToken}
|
||||
ecosystemSlug={ecosystem.slug}
|
||||
interval={interval}
|
||||
defaultRange={defaultRange}
|
||||
|
||||
+10
-4
@@ -35,11 +35,15 @@ function AccountAbstractionSummaryInner(props: {
|
||||
async function AsyncAccountAbstractionSummary(props: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const aggregateUserOpStats = await getAggregateUserOpUsage({
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
});
|
||||
const aggregateUserOpStats = await getAggregateUserOpUsage(
|
||||
{
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
},
|
||||
props.authToken,
|
||||
);
|
||||
|
||||
return (
|
||||
<AccountAbstractionSummaryInner
|
||||
@@ -52,6 +56,7 @@ async function AsyncAccountAbstractionSummary(props: {
|
||||
export function AccountAbstractionSummary(props: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
return (
|
||||
<Suspense
|
||||
@@ -65,6 +70,7 @@ export function AccountAbstractionSummary(props: {
|
||||
<AsyncAccountAbstractionSummary
|
||||
projectId={props.projectId}
|
||||
teamId={props.teamId}
|
||||
authToken={props.authToken}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
+11
-7
@@ -61,13 +61,16 @@ export default async function Page(props: {
|
||||
type: rangeType,
|
||||
};
|
||||
|
||||
const userOpStats = await getUserOpUsage({
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
to: range.to,
|
||||
});
|
||||
const userOpStats = await getUserOpUsage(
|
||||
{
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
to: range.to,
|
||||
},
|
||||
authToken,
|
||||
);
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
@@ -79,6 +82,7 @@ export default async function Page(props: {
|
||||
<AccountAbstractionSummary
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
|
||||
<div className="h-10" />
|
||||
|
||||
+8
-2
@@ -2,7 +2,13 @@ import { getEngineCloudMethodUsage } from "@/api/analytics";
|
||||
import type { AnalyticsQueryParams } from "@/types/analytics";
|
||||
import { EngineCloudBarChartCardUI } from "./EngineCloudBarChartCardUI";
|
||||
|
||||
export async function EngineCloudChartCardAsync(props: AnalyticsQueryParams) {
|
||||
const rawData = await getEngineCloudMethodUsage(props);
|
||||
export async function EngineCloudChartCardAsync(props: {
|
||||
params: AnalyticsQueryParams;
|
||||
authToken: string;
|
||||
}) {
|
||||
const rawData = await getEngineCloudMethodUsage(
|
||||
props.params,
|
||||
props.authToken,
|
||||
);
|
||||
return <EngineCloudBarChartCardUI rawData={rawData} />;
|
||||
}
|
||||
|
||||
+5
-3
@@ -2,8 +2,10 @@ import { getRpcMethodUsage } from "@/api/analytics";
|
||||
import type { AnalyticsQueryParams } from "@/types/analytics";
|
||||
import { RpcMethodBarChartCardUI } from "./RpcMethodBarChartCardUI";
|
||||
|
||||
export async function RpcMethodBarChartCardAsync(props: AnalyticsQueryParams) {
|
||||
const rawData = await getRpcMethodUsage(props);
|
||||
|
||||
export async function RpcMethodBarChartCardAsync(props: {
|
||||
params: AnalyticsQueryParams;
|
||||
authToken: string;
|
||||
}) {
|
||||
const rawData = await getRpcMethodUsage(props.params, props.authToken);
|
||||
return <RpcMethodBarChartCardUI rawData={rawData} />;
|
||||
}
|
||||
|
||||
+20
-15
@@ -3,19 +3,24 @@ import { getClientTransactions } from "@/api/analytics";
|
||||
import type { AnalyticsQueryParams } from "@/types/analytics";
|
||||
import { TransactionsChartsUI } from "./TransactionCharts";
|
||||
|
||||
export async function TransactionsChartCardAsync(
|
||||
props: AnalyticsQueryParams & {
|
||||
selectedChartQueryParam: string;
|
||||
selectedChart: string | undefined;
|
||||
client: ThirdwebClient;
|
||||
},
|
||||
) {
|
||||
export async function TransactionsChartCardAsync(props: {
|
||||
params: AnalyticsQueryParams;
|
||||
client: ThirdwebClient;
|
||||
selectedChartQueryParam: string;
|
||||
selectedChart: string | undefined;
|
||||
authToken: string;
|
||||
}) {
|
||||
const { params, authToken, client, selectedChart, selectedChartQueryParam } =
|
||||
props;
|
||||
const [data, aggregatedData] = await Promise.all([
|
||||
getClientTransactions(props),
|
||||
getClientTransactions({
|
||||
...props,
|
||||
period: "all",
|
||||
}),
|
||||
getClientTransactions(params, authToken),
|
||||
getClientTransactions(
|
||||
{
|
||||
...params,
|
||||
period: "all",
|
||||
},
|
||||
authToken,
|
||||
),
|
||||
]);
|
||||
|
||||
if (!aggregatedData.length) {
|
||||
@@ -25,10 +30,10 @@ export async function TransactionsChartCardAsync(
|
||||
return (
|
||||
<TransactionsChartsUI
|
||||
aggregatedData={aggregatedData}
|
||||
client={props.client}
|
||||
client={client}
|
||||
data={data}
|
||||
selectedChart={props.selectedChart}
|
||||
selectedChartQueryParam={props.selectedChartQueryParam}
|
||||
selectedChart={selectedChart}
|
||||
selectedChartQueryParam={selectedChartQueryParam}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+43
-30
@@ -51,39 +51,52 @@ export async function InsightAnalytics(props: {
|
||||
teamId: string;
|
||||
range: Range;
|
||||
interval: "day" | "week";
|
||||
authToken: string;
|
||||
}) {
|
||||
const { projectId, teamId, range, interval } = props;
|
||||
|
||||
const allTimeRequestsPromise = getInsightUsage({
|
||||
from: range.from,
|
||||
period: "all",
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
});
|
||||
const chainsDataPromise = getInsightChainUsage({
|
||||
from: range.from,
|
||||
limit: 10,
|
||||
period: "all",
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
});
|
||||
const statusCodesDataPromise = getInsightStatusCodeUsage({
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
});
|
||||
const endpointsDataPromise = getInsightEndpointUsage({
|
||||
from: range.from,
|
||||
limit: 10,
|
||||
period: "all",
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
});
|
||||
const allTimeRequestsPromise = getInsightUsage(
|
||||
{
|
||||
from: range.from,
|
||||
period: "all",
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
},
|
||||
props.authToken,
|
||||
);
|
||||
const chainsDataPromise = getInsightChainUsage(
|
||||
{
|
||||
from: range.from,
|
||||
limit: 10,
|
||||
period: "all",
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
},
|
||||
props.authToken,
|
||||
);
|
||||
const statusCodesDataPromise = getInsightStatusCodeUsage(
|
||||
{
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
},
|
||||
props.authToken,
|
||||
);
|
||||
const endpointsDataPromise = getInsightEndpointUsage(
|
||||
{
|
||||
from: range.from,
|
||||
limit: 10,
|
||||
period: "all",
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
},
|
||||
props.authToken,
|
||||
);
|
||||
|
||||
const [allTimeRequestsData, statusCodesData, endpointsData, chainsData] =
|
||||
await Promise.all([
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ export default async function Page(props: {
|
||||
projectId={project.id}
|
||||
range={range}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
|
||||
<div className="h-10" />
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ export function ProjectHighlightsCard(props: {
|
||||
activeChart={
|
||||
selectedChart && selectedChart in chartConfig
|
||||
? (selectedChart as keyof AggregatedMetrics)
|
||||
: "totalVolume"
|
||||
: "activeUsers"
|
||||
}
|
||||
aggregateFn={(_data, key) => {
|
||||
if (key === "activeUsers") {
|
||||
|
||||
@@ -91,6 +91,7 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
});
|
||||
|
||||
const activeStatus = await isProjectActive({
|
||||
authToken,
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
});
|
||||
@@ -124,6 +125,7 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
) : (
|
||||
<div className="container flex max-w-7xl grow flex-col">
|
||||
<ProjectAnalytics
|
||||
authToken={authToken}
|
||||
client={client}
|
||||
interval={interval}
|
||||
params={params}
|
||||
@@ -147,8 +149,10 @@ async function ProjectAnalytics(props: {
|
||||
interval: "day" | "week";
|
||||
searchParams: PageSearchParams;
|
||||
client: ThirdwebClient;
|
||||
authToken: string;
|
||||
}) {
|
||||
const { project, params, range, interval, searchParams, client } = props;
|
||||
const { project, params, range, interval, searchParams, client, authToken } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col gap-6">
|
||||
@@ -157,6 +161,7 @@ async function ProjectAnalytics(props: {
|
||||
searchParamsUsed={["from", "to", "interval", "appHighlights"]}
|
||||
>
|
||||
<AsyncAppHighlightsCard
|
||||
authToken={authToken}
|
||||
client={client}
|
||||
interval={interval}
|
||||
params={params}
|
||||
@@ -176,14 +181,22 @@ async function ProjectAnalytics(props: {
|
||||
fallback={<LoadingChartState className="h-[431px] border" />}
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
>
|
||||
<AsyncWalletDistributionCard project={project} range={range} />
|
||||
<AsyncWalletDistributionCard
|
||||
project={project}
|
||||
range={range}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
|
||||
<ResponsiveSuspense
|
||||
fallback={<LoadingChartState className="h-[431px] border" />}
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
>
|
||||
<AsyncAuthMethodDistributionCard project={project} range={range} />
|
||||
<AsyncAuthMethodDistributionCard
|
||||
project={project}
|
||||
range={range}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
</div>
|
||||
|
||||
@@ -193,16 +206,20 @@ async function ProjectAnalytics(props: {
|
||||
>
|
||||
<TransactionsChartCardAsync
|
||||
client={client}
|
||||
from={range.from}
|
||||
period={interval}
|
||||
params={{
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
to: range.to,
|
||||
}}
|
||||
authToken={authToken}
|
||||
selectedChart={
|
||||
typeof searchParams.client_transactions === "string"
|
||||
? searchParams.client_transactions
|
||||
: undefined
|
||||
}
|
||||
selectedChartQueryParam="client_transactions"
|
||||
teamId={project.teamId}
|
||||
to={range.to}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
|
||||
@@ -220,6 +237,7 @@ async function ProjectAnalytics(props: {
|
||||
: undefined
|
||||
}
|
||||
selectedChartQueryParam="totalSponsored"
|
||||
authToken={authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
|
||||
@@ -228,11 +246,14 @@ async function ProjectAnalytics(props: {
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
>
|
||||
<RpcMethodBarChartCardAsync
|
||||
from={range.from}
|
||||
period={interval}
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
to={range.to}
|
||||
params={{
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
to: range.to,
|
||||
}}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
|
||||
@@ -241,11 +262,14 @@ async function ProjectAnalytics(props: {
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
>
|
||||
<EngineCloudChartCardAsync
|
||||
from={range.from}
|
||||
period={interval}
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
to={range.to}
|
||||
params={{
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
to: range.to,
|
||||
}}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
</div>
|
||||
@@ -258,22 +282,29 @@ export async function AsyncTotalSponsoredCard(props: {
|
||||
interval: "day" | "week";
|
||||
selectedChart: string | undefined;
|
||||
selectedChartQueryParam: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const [userOpUsageTimeSeries, userOpUsage] = await Promise.allSettled([
|
||||
getUserOpUsage({
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
}),
|
||||
getUserOpUsage({
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
}),
|
||||
getUserOpUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
getUserOpUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
]);
|
||||
|
||||
return userOpUsageTimeSeries.status === "fulfilled" &&
|
||||
@@ -298,14 +329,18 @@ export async function AsyncTotalSponsoredCard(props: {
|
||||
async function AsyncAuthMethodDistributionCard(props: {
|
||||
project: Project;
|
||||
range: Range;
|
||||
authToken: string;
|
||||
}) {
|
||||
const inAppWalletUsage = await getInAppWalletUsage({
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
}).catch(() => undefined);
|
||||
const inAppWalletUsage = await getInAppWalletUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch(() => undefined);
|
||||
|
||||
return inAppWalletUsage && inAppWalletUsage.length > 0 ? (
|
||||
<AuthMethodDistributionCard data={inAppWalletUsage} />
|
||||
@@ -325,23 +360,30 @@ async function AsyncAppHighlightsCard(props: {
|
||||
selectedChart: string | undefined;
|
||||
client: ThirdwebClient;
|
||||
params: PageParams;
|
||||
authToken: string;
|
||||
}) {
|
||||
const [walletUserStatsTimeSeries, universalBridgeUsage] =
|
||||
await Promise.allSettled([
|
||||
getWalletUsers({
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
}),
|
||||
getUniversalBridgeUsage({
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
}),
|
||||
getWalletUsers(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
getUniversalBridgeUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
]);
|
||||
|
||||
if (
|
||||
@@ -380,14 +422,18 @@ async function AsyncAppHighlightsCard(props: {
|
||||
async function AsyncWalletDistributionCard(props: {
|
||||
project: Project;
|
||||
range: Range;
|
||||
authToken: string;
|
||||
}) {
|
||||
const walletConnections = await getWalletConnections({
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
}).catch(() => undefined);
|
||||
const walletConnections = await getWalletConnections(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch(() => undefined);
|
||||
|
||||
return walletConnections && walletConnections.length > 0 ? (
|
||||
<WalletDistributionCard data={walletConnections} />
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ export default async function Page(props: {
|
||||
projectId={project.id}
|
||||
range={range}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
|
||||
<div className="h-10" />
|
||||
|
||||
+21
-14
@@ -26,6 +26,7 @@ export async function PayAnalytics(props: {
|
||||
teamId: string;
|
||||
range: Range;
|
||||
interval: "day" | "week";
|
||||
authToken: string;
|
||||
}) {
|
||||
const { projectId, teamId, range, interval } = props;
|
||||
|
||||
@@ -37,23 +38,29 @@ export async function PayAnalytics(props: {
|
||||
month: "short" as const,
|
||||
};
|
||||
|
||||
const volumeDataPromise = getUniversalBridgeUsage({
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
}).catch((error) => {
|
||||
const volumeDataPromise = getUniversalBridgeUsage(
|
||||
{
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch((error) => {
|
||||
console.error(error);
|
||||
return [];
|
||||
});
|
||||
const walletDataPromise = getUniversalBridgeWalletUsage({
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
}).catch((error) => {
|
||||
const walletDataPromise = getUniversalBridgeWalletUsage(
|
||||
{
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch((error) => {
|
||||
console.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
+32
-22
@@ -17,31 +17,41 @@ export async function RPCAnalytics(props: {
|
||||
teamId: string;
|
||||
range: Range;
|
||||
interval: "day" | "week";
|
||||
authToken: string;
|
||||
}) {
|
||||
const { projectId, teamId, range, interval } = props;
|
||||
const { projectId, teamId, range, interval, authToken } = props;
|
||||
|
||||
// TODO: add requests by status code, but currently not performant enough
|
||||
const allRequestsByUsageTypePromise = getRpcUsageByType({
|
||||
from: range.from,
|
||||
period: "all",
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
});
|
||||
const requestsByUsageTypePromise = getRpcUsageByType({
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
});
|
||||
const evmMethodsPromise = getRpcMethodUsage({
|
||||
from: range.from,
|
||||
period: "all",
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
}).catch((error) => {
|
||||
const allRequestsByUsageTypePromise = getRpcUsageByType(
|
||||
{
|
||||
from: range.from,
|
||||
period: "all",
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
},
|
||||
authToken,
|
||||
);
|
||||
const requestsByUsageTypePromise = getRpcUsageByType(
|
||||
{
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
},
|
||||
authToken,
|
||||
);
|
||||
const evmMethodsPromise = getRpcMethodUsage(
|
||||
{
|
||||
from: range.from,
|
||||
period: "all",
|
||||
projectId: projectId,
|
||||
teamId: teamId,
|
||||
to: range.to,
|
||||
},
|
||||
authToken,
|
||||
).catch((error) => {
|
||||
console.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
@@ -52,6 +52,7 @@ export default async function Page(props: {
|
||||
projectClientId={project.publishableKey}
|
||||
projectId={project.id}
|
||||
range={range}
|
||||
authToken={authToken}
|
||||
teamId={project.teamId}
|
||||
/>
|
||||
<div className="h-10" />
|
||||
|
||||
+24
-15
@@ -51,23 +51,30 @@ function InAppWalletsSummaryInner(props: {
|
||||
async function AsyncInAppWalletsSummary(props: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const { teamId, projectId } = props;
|
||||
const allTimeStatsPromise = getInAppWalletUsage({
|
||||
from: new Date(2022, 0, 1),
|
||||
period: "all",
|
||||
projectId,
|
||||
teamId,
|
||||
to: new Date(),
|
||||
});
|
||||
const { teamId, projectId, authToken } = props;
|
||||
const allTimeStatsPromise = getInAppWalletUsage(
|
||||
{
|
||||
from: new Date(2022, 0, 1),
|
||||
period: "all",
|
||||
projectId,
|
||||
teamId,
|
||||
to: new Date(),
|
||||
},
|
||||
authToken,
|
||||
);
|
||||
|
||||
const monthlyStatsPromise = getInAppWalletUsage({
|
||||
from: subDays(new Date(), 30),
|
||||
period: "month",
|
||||
projectId,
|
||||
teamId,
|
||||
to: new Date(),
|
||||
});
|
||||
const monthlyStatsPromise = getInAppWalletUsage(
|
||||
{
|
||||
from: subDays(new Date(), 30),
|
||||
period: "month",
|
||||
projectId,
|
||||
teamId,
|
||||
to: new Date(),
|
||||
},
|
||||
authToken,
|
||||
);
|
||||
|
||||
const [allTimeStats, monthlyStats] = await Promise.all([
|
||||
allTimeStatsPromise,
|
||||
@@ -86,6 +93,7 @@ async function AsyncInAppWalletsSummary(props: {
|
||||
export function InAppWalletsSummary(props: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
return (
|
||||
<Suspense
|
||||
@@ -100,6 +108,7 @@ export function InAppWalletsSummary(props: {
|
||||
<AsyncInAppWalletsSummary
|
||||
projectId={props.projectId}
|
||||
teamId={props.teamId}
|
||||
authToken={props.authToken}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
+11
-7
@@ -34,6 +34,7 @@ type AsyncInAppWalletAnalyticsProps = Omit<
|
||||
> & {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
authToken: string;
|
||||
};
|
||||
|
||||
async function AsyncInAppWalletAnalytics(
|
||||
@@ -41,13 +42,16 @@ async function AsyncInAppWalletAnalytics(
|
||||
) {
|
||||
const range = props.range ?? getLastNDaysRange("last-120");
|
||||
|
||||
const stats = await getInAppWalletUsage({
|
||||
from: range.from,
|
||||
period: props.interval,
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
to: range.to,
|
||||
}).catch((error) => {
|
||||
const stats = await getInAppWalletUsage(
|
||||
{
|
||||
from: range.from,
|
||||
period: props.interval,
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
to: range.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch((error) => {
|
||||
console.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
+13
-1
@@ -1,9 +1,11 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getProject } from "@/api/project/projects";
|
||||
import type { DurationId } from "@/components/analytics/date-range-selector";
|
||||
import { ResponsiveTimeFilters } from "@/components/analytics/responsive-time-filters";
|
||||
import { getFiltersFromSearchParams } from "@/lib/time";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { InAppWalletAnalytics } from "./analytics/chart";
|
||||
import { InAppWalletsSummary } from "./analytics/chart/Summary";
|
||||
|
||||
@@ -21,6 +23,11 @@ export default async function Page(props: {
|
||||
props.params,
|
||||
]);
|
||||
|
||||
const authToken = await getAuthToken();
|
||||
if (!authToken) {
|
||||
loginRedirect(`/team/${params.team_slug}/${params.project_slug}/wallets`);
|
||||
}
|
||||
|
||||
const defaultRange: DurationId = "last-30";
|
||||
const { range, interval } = getFiltersFromSearchParams({
|
||||
defaultRange,
|
||||
@@ -38,7 +45,11 @@ export default async function Page(props: {
|
||||
return (
|
||||
<ResponsiveSearchParamsProvider value={searchParams}>
|
||||
<div>
|
||||
<InAppWalletsSummary projectId={project.id} teamId={project.teamId} />
|
||||
<InAppWalletsSummary
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
<div className="h-10" />
|
||||
<ResponsiveTimeFilters defaultRange={defaultRange} />
|
||||
<div className="h-6" />
|
||||
@@ -47,6 +58,7 @@ export default async function Page(props: {
|
||||
projectId={project.id}
|
||||
range={range}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</div>
|
||||
</ResponsiveSearchParamsProvider>
|
||||
|
||||
+22
-16
@@ -90,14 +90,17 @@ export default async function WebhooksAnalyticsPage(props: {
|
||||
const webhookId = selectedWebhookId === "all" ? undefined : selectedWebhookId;
|
||||
const [requestsData, latencyData] = await Promise.all([
|
||||
(async () => {
|
||||
const res = await getWebhookRequests({
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
to: range.to,
|
||||
webhookId,
|
||||
});
|
||||
const res = await getWebhookRequests(
|
||||
{
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
to: range.to,
|
||||
webhookId,
|
||||
},
|
||||
authToken,
|
||||
);
|
||||
if ("error" in res) {
|
||||
console.error("Failed to fetch webhook requests:", res.error);
|
||||
return [];
|
||||
@@ -105,14 +108,17 @@ export default async function WebhooksAnalyticsPage(props: {
|
||||
return res.data;
|
||||
})(),
|
||||
(async () => {
|
||||
const res = await getWebhookLatency({
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
to: range.to,
|
||||
webhookId,
|
||||
});
|
||||
const res = await getWebhookLatency(
|
||||
{
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
to: range.to,
|
||||
webhookId,
|
||||
},
|
||||
authToken,
|
||||
);
|
||||
if ("error" in res) {
|
||||
console.error("Failed to fetch webhook latency:", res.error);
|
||||
return [];
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ export function CreateContractWebhookButton({
|
||||
inputAbi: [],
|
||||
name: "",
|
||||
secret: "",
|
||||
sigHash: "",
|
||||
sigHash: [],
|
||||
sigHashAbi: "",
|
||||
toAddresses: "",
|
||||
webhookUrl: "",
|
||||
|
||||
+33
-16
@@ -74,13 +74,16 @@ export function FilterDetailsStep({
|
||||
const knownFunctionSignatures = functionSignatures.map(
|
||||
(sig) => sig.signature,
|
||||
);
|
||||
|
||||
// Handle both single and multiple signatures for custom signature detection
|
||||
const sigHashes = Array.isArray(sigHash) ? sigHash : sigHash ? [sigHash] : [];
|
||||
const isCustomSignature =
|
||||
(watchFilterType === "event" &&
|
||||
sigHash &&
|
||||
!knownEventSignatures.includes(sigHash)) ||
|
||||
sigHashes.some((hash) => hash && !knownEventSignatures.includes(hash))) ||
|
||||
(watchFilterType === "transaction" &&
|
||||
sigHash &&
|
||||
!knownFunctionSignatures.includes(sigHash));
|
||||
sigHashes.some(
|
||||
(hash) => hash && !knownFunctionSignatures.includes(hash),
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -140,7 +143,7 @@ export function FilterDetailsStep({
|
||||
</div>
|
||||
<FormControl>
|
||||
<div className="space-y-2">
|
||||
<Input placeholder="0x1234..." {...field} />
|
||||
<Input placeholder="0x1234...,0xabcd..." {...field} />
|
||||
|
||||
{/* ABI fetch status */}
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
@@ -300,13 +303,13 @@ export function FilterDetailsStep({
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<FormLabel>
|
||||
{watchFilterType === "event"
|
||||
? "Event Signature (optional)"
|
||||
: "Function Signature (optional)"}
|
||||
? "Event Signatures (optional)"
|
||||
: "Function Signatures (optional)"}
|
||||
</FormLabel>
|
||||
<p className="text-muted-foreground">
|
||||
{watchFilterType === "event"
|
||||
? "Select an event to monitor"
|
||||
: "Select a function to monitor"}
|
||||
? "Select events to monitor"
|
||||
: "Select functions to monitor"}
|
||||
</p>
|
||||
</div>
|
||||
<FormControl>
|
||||
@@ -315,11 +318,16 @@ export function FilterDetailsStep({
|
||||
eventSignatures.length > 0 ? (
|
||||
<SignatureSelector
|
||||
className="block w-full max-w-90 overflow-hidden text-ellipsis"
|
||||
multiSelect={true}
|
||||
onChange={(val) => {
|
||||
field.onChange(val);
|
||||
// If custom signature, clear ABI field
|
||||
const known = eventSignatures.map((sig) => sig.signature);
|
||||
if (val && !known.includes(val)) {
|
||||
const values = Array.isArray(val) ? val : [val];
|
||||
const hasCustomSignature = values.some(
|
||||
(v) => v && !known.includes(v),
|
||||
);
|
||||
if (hasCustomSignature) {
|
||||
form.setValue("abi", "");
|
||||
}
|
||||
}}
|
||||
@@ -328,21 +336,26 @@ export function FilterDetailsStep({
|
||||
label: truncateMiddle(sig.name, 30, 15),
|
||||
value: sig.signature,
|
||||
}))}
|
||||
placeholder="Select or enter an event signature"
|
||||
placeholder="Select or enter event signatures"
|
||||
setAbi={(abi) => form.setValue("sigHashAbi", abi)}
|
||||
value={field.value || ""}
|
||||
value={field.value || []}
|
||||
/>
|
||||
) : watchFilterType === "transaction" &&
|
||||
Object.keys(fetchedTxAbis).length > 0 &&
|
||||
functionSignatures.length > 0 ? (
|
||||
<SignatureSelector
|
||||
multiSelect={true}
|
||||
onChange={(val) => {
|
||||
field.onChange(val);
|
||||
// If custom signature, clear ABI field
|
||||
const known = functionSignatures.map(
|
||||
(sig) => sig.signature,
|
||||
);
|
||||
if (val && !known.includes(val)) {
|
||||
const values = Array.isArray(val) ? val : [val];
|
||||
const hasCustomSignature = values.some(
|
||||
(v) => v && !known.includes(v),
|
||||
);
|
||||
if (hasCustomSignature) {
|
||||
form.setValue("abi", "");
|
||||
}
|
||||
}}
|
||||
@@ -351,9 +364,9 @@ export function FilterDetailsStep({
|
||||
label: truncateMiddle(sig.name, 30, 15),
|
||||
value: sig.signature,
|
||||
}))}
|
||||
placeholder="Select or enter a function signature"
|
||||
placeholder="Select or enter function signatures"
|
||||
setAbi={(abi) => form.setValue("sigHashAbi", abi)}
|
||||
value={field.value || ""}
|
||||
value={field.value || []}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
@@ -367,7 +380,11 @@ export function FilterDetailsStep({
|
||||
? "Fetching event signatures..."
|
||||
: "Fetching function signatures..."
|
||||
}
|
||||
value={field.value}
|
||||
value={
|
||||
Array.isArray(field.value)
|
||||
? field.value.join(", ")
|
||||
: field.value || ""
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
+17
-2
@@ -194,12 +194,27 @@ export default function ReviewStep({
|
||||
|
||||
<li className="flex justify-between">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Signature Hash:
|
||||
Signature Hash
|
||||
{Array.isArray(form.watch("sigHash")) &&
|
||||
(form.watch("sigHash")?.length || 0) > 1
|
||||
? "es"
|
||||
: ""}
|
||||
:
|
||||
</span>
|
||||
<span className="font-medium text-sm">
|
||||
{(() => {
|
||||
const sigHash = form.watch("sigHash");
|
||||
return sigHash ? truncateMiddle(sigHash, 10, 6) : "None";
|
||||
if (!sigHash) return "None";
|
||||
|
||||
if (Array.isArray(sigHash)) {
|
||||
if (sigHash.length === 0) return "None";
|
||||
if (sigHash.length === 1) {
|
||||
return truncateMiddle(sigHash[0] || "", 10, 6);
|
||||
}
|
||||
return `${sigHash.length} signature${sigHash.length > 1 ? "s" : ""} selected`;
|
||||
} else {
|
||||
return truncateMiddle(sigHash, 10, 6);
|
||||
}
|
||||
})()}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
+80
-30
@@ -224,15 +224,18 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
|
||||
};
|
||||
|
||||
if (formData.sigHash) {
|
||||
filters.signatures = [
|
||||
{
|
||||
sig_hash: formData.sigHash.startsWith("0x")
|
||||
? formData.sigHash
|
||||
: keccak256(new TextEncoder().encode(formData.sigHash)),
|
||||
const sigHashes = Array.isArray(formData.sigHash)
|
||||
? formData.sigHash
|
||||
: [formData.sigHash];
|
||||
filters.signatures = sigHashes
|
||||
.filter((hash) => hash)
|
||||
.map((hash) => ({
|
||||
sig_hash: hash.startsWith("0x")
|
||||
? hash
|
||||
: keccak256(new TextEncoder().encode(hash)),
|
||||
abi: formData.sigHashAbi || formData.abi,
|
||||
params: {},
|
||||
},
|
||||
];
|
||||
}));
|
||||
}
|
||||
|
||||
return { ...topic, filters };
|
||||
@@ -271,15 +274,18 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
|
||||
}
|
||||
|
||||
if (formData.sigHash) {
|
||||
filters.signatures = [
|
||||
{
|
||||
sig_hash: formData.sigHash.startsWith("0x")
|
||||
? formData.sigHash
|
||||
: toFunctionSelector(formData.sigHash),
|
||||
const sigHashes = Array.isArray(formData.sigHash)
|
||||
? formData.sigHash
|
||||
: [formData.sigHash];
|
||||
filters.signatures = sigHashes
|
||||
.filter((hash) => hash)
|
||||
.map((hash) => ({
|
||||
sig_hash: hash.startsWith("0x")
|
||||
? hash
|
||||
: toFunctionSelector(hash),
|
||||
abi: formData.sigHashAbi || formData.abi,
|
||||
params: {},
|
||||
},
|
||||
];
|
||||
}));
|
||||
}
|
||||
|
||||
return { ...topic, filters };
|
||||
@@ -475,8 +481,8 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
|
||||
<FormLabel>
|
||||
{topic.id ===
|
||||
"contracts.event.confirmed"
|
||||
? "Event Signature"
|
||||
: "Function Signature"}
|
||||
? "Event Signatures"
|
||||
: "Function Signatures"}
|
||||
</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
@@ -489,6 +495,7 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
|
||||
0 ? (
|
||||
<SignatureSelector
|
||||
className="block w-full max-w-90 overflow-hidden text-ellipsis"
|
||||
multiSelect={true}
|
||||
onChange={(val) => {
|
||||
field.onChange(val);
|
||||
// If custom signature, clear ABI field
|
||||
@@ -497,9 +504,20 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
|
||||
(sig) =>
|
||||
sig.signature,
|
||||
);
|
||||
const values =
|
||||
Array.isArray(val)
|
||||
? val
|
||||
: [val];
|
||||
const hasCustomSignature =
|
||||
values.some(
|
||||
(v) =>
|
||||
v &&
|
||||
!known.includes(
|
||||
v,
|
||||
),
|
||||
);
|
||||
if (
|
||||
val &&
|
||||
!known.includes(val)
|
||||
hasCustomSignature
|
||||
) {
|
||||
eventFilterForm.setValue(
|
||||
"abi",
|
||||
@@ -518,14 +536,14 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
|
||||
value: sig.signature,
|
||||
}),
|
||||
)}
|
||||
placeholder="Select or enter an event signature"
|
||||
placeholder="Select or enter event signatures"
|
||||
setAbi={(abi) =>
|
||||
eventFilterForm.setValue(
|
||||
"sigHashAbi",
|
||||
abi,
|
||||
)
|
||||
}
|
||||
value={field.value || ""}
|
||||
value={field.value || []}
|
||||
/>
|
||||
) : topic.id ===
|
||||
"contracts.transaction.confirmed" &&
|
||||
@@ -535,6 +553,7 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
|
||||
txAbi.signatures.length >
|
||||
0 ? (
|
||||
<SignatureSelector
|
||||
multiSelect={true}
|
||||
onChange={(val) => {
|
||||
field.onChange(val);
|
||||
// If custom signature, clear ABI field
|
||||
@@ -543,9 +562,20 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
|
||||
(sig) =>
|
||||
sig.signature,
|
||||
);
|
||||
const values =
|
||||
Array.isArray(val)
|
||||
? val
|
||||
: [val];
|
||||
const hasCustomSignature =
|
||||
values.some(
|
||||
(v) =>
|
||||
v &&
|
||||
!known.includes(
|
||||
v,
|
||||
),
|
||||
);
|
||||
if (
|
||||
val &&
|
||||
!known.includes(val)
|
||||
hasCustomSignature
|
||||
) {
|
||||
transactionFilterForm.setValue(
|
||||
"abi",
|
||||
@@ -564,14 +594,14 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
|
||||
value: sig.signature,
|
||||
}),
|
||||
)}
|
||||
placeholder="Select or enter a function signature"
|
||||
placeholder="Select or enter function signatures"
|
||||
setAbi={(abi) =>
|
||||
transactionFilterForm.setValue(
|
||||
"sigHashAbi",
|
||||
abi,
|
||||
)
|
||||
}
|
||||
value={field.value || ""}
|
||||
value={field.value || []}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
@@ -714,7 +744,7 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
|
||||
<FormItem className="flex flex-col">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<FormLabel>
|
||||
Function Signature
|
||||
Function Signatures
|
||||
</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
@@ -724,6 +754,7 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
|
||||
txAbi.signatures.length >
|
||||
0 ? (
|
||||
<SignatureSelector
|
||||
multiSelect={true}
|
||||
onChange={(val) => {
|
||||
field.onChange(val);
|
||||
// If custom signature, clear ABI field
|
||||
@@ -732,9 +763,20 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
|
||||
(sig) =>
|
||||
sig.signature,
|
||||
);
|
||||
const values =
|
||||
Array.isArray(val)
|
||||
? val
|
||||
: [val];
|
||||
const hasCustomSignature =
|
||||
values.some(
|
||||
(v) =>
|
||||
v &&
|
||||
!known.includes(
|
||||
v,
|
||||
),
|
||||
);
|
||||
if (
|
||||
val &&
|
||||
!known.includes(val)
|
||||
hasCustomSignature
|
||||
) {
|
||||
transactionFilterForm.setValue(
|
||||
"abi",
|
||||
@@ -753,14 +795,14 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
|
||||
value: sig.signature,
|
||||
}),
|
||||
)}
|
||||
placeholder="Select or enter a function signature"
|
||||
placeholder="Select or enter function signatures"
|
||||
setAbi={(abi) =>
|
||||
transactionFilterForm.setValue(
|
||||
"sigHashAbi",
|
||||
abi,
|
||||
)
|
||||
}
|
||||
value={field.value || ""}
|
||||
value={field.value || []}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
@@ -769,7 +811,15 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
|
||||
}
|
||||
onChange={field.onChange}
|
||||
placeholder="Fetching function signatures..."
|
||||
value={field.value}
|
||||
value={
|
||||
Array.isArray(
|
||||
field.value,
|
||||
)
|
||||
? field.value.join(
|
||||
", ",
|
||||
)
|
||||
: field.value || ""
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
+11
-8
@@ -80,14 +80,17 @@ export default async function WebhooksPage(props: {
|
||||
// Fetch metrics for all webhooks in parallel
|
||||
const webhookMetrics = await Promise.all(
|
||||
webhookConfigs.map(async (config) => {
|
||||
const metricsResult = await getWebhookSummary({
|
||||
from: new Date(Date.now() - 24 * 60 * 60 * 1000),
|
||||
period: "day",
|
||||
projectId: project.id,
|
||||
teamId: project.teamId, // 24 hours ago
|
||||
to: new Date(),
|
||||
webhookId: config.id,
|
||||
});
|
||||
const metricsResult = await getWebhookSummary(
|
||||
{
|
||||
from: new Date(Date.now() - 24 * 60 * 60 * 1000),
|
||||
period: "day",
|
||||
projectId: project.id,
|
||||
teamId: project.teamId, // 24 hours ago
|
||||
to: new Date(),
|
||||
webhookId: config.id,
|
||||
},
|
||||
authToken,
|
||||
);
|
||||
|
||||
return {
|
||||
metrics:
|
||||
|
||||
+28
-14
@@ -33,13 +33,20 @@ export function buildEventWebhookPayload(
|
||||
): WebhookPayload {
|
||||
const eventSignatures = [];
|
||||
if (data.sigHash) {
|
||||
eventSignatures.push({
|
||||
abi: data.sigHashAbi || data.abi,
|
||||
params: {},
|
||||
sig_hash: data.sigHash.startsWith("0x")
|
||||
? data.sigHash
|
||||
: keccak256(new TextEncoder().encode(data.sigHash)),
|
||||
});
|
||||
const sigHashes = Array.isArray(data.sigHash)
|
||||
? data.sigHash
|
||||
: [data.sigHash];
|
||||
for (const sigHash of sigHashes) {
|
||||
if (sigHash) {
|
||||
eventSignatures.push({
|
||||
abi: data.sigHashAbi || data.abi,
|
||||
params: {},
|
||||
sig_hash: sigHash.startsWith("0x")
|
||||
? sigHash
|
||||
: keccak256(new TextEncoder().encode(sigHash)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
@@ -59,13 +66,20 @@ export function buildTransactionWebhookPayload(
|
||||
): WebhookPayload {
|
||||
const txSignatures = [];
|
||||
if (data.sigHash) {
|
||||
txSignatures.push({
|
||||
abi: data.sigHashAbi || data.abi,
|
||||
params: {},
|
||||
sig_hash: data.sigHash.startsWith("0x")
|
||||
? data.sigHash
|
||||
: toFunctionSelector(data.sigHash),
|
||||
});
|
||||
const sigHashes = Array.isArray(data.sigHash)
|
||||
? data.sigHash
|
||||
: [data.sigHash];
|
||||
for (const sigHash of sigHashes) {
|
||||
if (sigHash) {
|
||||
txSignatures.push({
|
||||
abi: data.sigHashAbi || data.abi,
|
||||
params: {},
|
||||
sig_hash: sigHash.startsWith("0x")
|
||||
? sigHash
|
||||
: toFunctionSelector(sigHash),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ export const webhookFormSchema = z
|
||||
.max(100, { message: "Name must be at most 100 characters long" }),
|
||||
params: z.record(z.unknown()).optional(),
|
||||
secret: z.string().optional(),
|
||||
sigHash: z.string().optional(),
|
||||
sigHash: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
sigHashAbi: z.string().optional(),
|
||||
toAddresses: z
|
||||
.string()
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AppFooter } from "@/components/footers/app-footer";
|
||||
import { DotsBackgroundPattern } from "@/components/ui/background-patterns";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { TeamHeader } from "../../components/TeamHeader/team-header";
|
||||
import { createTeamLink, TeamSelectorCard } from "./components/team-selector";
|
||||
import { createTeamLink, TeamSelectorCard } from "../_components/team-selector";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
|
||||
+9
-5
@@ -16,7 +16,7 @@ export function ProjectAndTeamSelectorCard(props: {
|
||||
projects: PartialProject[];
|
||||
}[];
|
||||
client: ThirdwebClient;
|
||||
description: React.ReactNode;
|
||||
description: React.ReactNode | undefined;
|
||||
onSelect: (selected: { team: Team; project: PartialProject }) => void;
|
||||
}) {
|
||||
const teamAndProjects = props.teamAndProjects.filter(
|
||||
@@ -31,10 +31,14 @@ export function ProjectAndTeamSelectorCard(props: {
|
||||
<div className="mb-2 self-start rounded-full border p-2">
|
||||
<BoxIcon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<h1 className="mb-0.5 font-semibold text-xl tracking-tight">
|
||||
Select a project
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">{props.description}</p>
|
||||
<div className="space-y-0.5">
|
||||
<h1 className="font-semibold text-xl tracking-tight">
|
||||
Select a project
|
||||
</h1>
|
||||
{props.description && (
|
||||
<p className="text-muted-foreground text-sm">{props.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
+25
-1
@@ -5,7 +5,7 @@ import type { PartialProject } from "@/api/project/getProjectContracts";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { useAddContractToProject } from "@/hooks/project-contracts";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { ProjectAndTeamSelectorCard } from "../../../../../components/TeamAndProjectSelectorCard";
|
||||
import { ProjectAndTeamSelectorCard } from "./TeamAndProjectSelectorCard";
|
||||
|
||||
export function SelectProjectForContract(props: {
|
||||
chainSlug: string;
|
||||
@@ -78,3 +78,27 @@ export function ImportAndSelectProjectForContract(props: {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GenericProjectSelector(props: {
|
||||
client: ThirdwebClient;
|
||||
paths: string[] | undefined;
|
||||
description: React.ReactNode | undefined;
|
||||
teamAndProjects: {
|
||||
team: Team;
|
||||
projects: PartialProject[];
|
||||
}[];
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
return (
|
||||
<ProjectAndTeamSelectorCard
|
||||
client={props.client}
|
||||
description={props.description}
|
||||
onSelect={(selection) => {
|
||||
router.push(
|
||||
`/team/${selection.team.slug}/${selection.project.slug}${props.paths?.length ? `/${props.paths.join("/")}` : ""}`,
|
||||
);
|
||||
}}
|
||||
teamAndProjects={props.teamAndProjects}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import { TeamHeader } from "../../../../../components/TeamHeader/team-header";
|
||||
import {
|
||||
ImportAndSelectProjectForContract,
|
||||
SelectProjectForContract,
|
||||
} from "./components/project-selector";
|
||||
} from "../../../../_components/project-selector";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { getValidAccount } from "@/api/account/get-account";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getProjects } from "@/api/project/projects";
|
||||
import { getTeams } from "@/api/team/get-team";
|
||||
import { AppFooter } from "@/components/footers/app-footer";
|
||||
import { DotsBackgroundPattern } from "@/components/ui/background-patterns";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { TeamHeader } from "../../../components/TeamHeader/team-header";
|
||||
import { GenericProjectSelector } from "../../_components/project-selector";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
paths?: string[];
|
||||
}>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const pagePath = `/team/~/~${params.paths?.length ? `/${params.paths.join("/")}` : ""}`;
|
||||
|
||||
const [authToken, account, teams] = await Promise.all([
|
||||
getAuthToken(),
|
||||
await getValidAccount(pagePath),
|
||||
getTeams(),
|
||||
]);
|
||||
|
||||
if (!authToken || !account || !teams) {
|
||||
loginRedirect(pagePath);
|
||||
}
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: undefined,
|
||||
});
|
||||
|
||||
const teamAndAllProjects = await Promise.all(
|
||||
teams.map(async (team) => {
|
||||
return {
|
||||
projects: await getProjects(team.slug).catch(() => []),
|
||||
team,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<ProjectSelectionLayout>
|
||||
<GenericProjectSelector
|
||||
client={client}
|
||||
paths={params.paths}
|
||||
description={
|
||||
params.paths?.[0] === "tokens"
|
||||
? "Select a project to continue with token creation"
|
||||
: "Select a project to continue"
|
||||
}
|
||||
teamAndProjects={teamAndAllProjects}
|
||||
/>
|
||||
</ProjectSelectionLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectSelectionLayout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="relative flex min-h-dvh flex-col">
|
||||
<div className="border-b bg-card">
|
||||
<TeamHeader />
|
||||
</div>
|
||||
|
||||
<div className="relative flex grow flex-col overflow-hidden">
|
||||
<DotsBackgroundPattern />
|
||||
<div className="container z-10 flex grow flex-col items-center justify-center py-20">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
<AppFooter className="relative z-10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -47,6 +47,10 @@ const locales: {
|
||||
id: "fr_FR",
|
||||
name: "French",
|
||||
},
|
||||
{
|
||||
id: "zh_CN",
|
||||
name: "Chinese (Simplified)",
|
||||
},
|
||||
];
|
||||
|
||||
export function LocaleFormControl(props: {
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import { Step, Steps, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Stylus Airdrop Contracts | thirdweb Documentation",
|
||||
description:
|
||||
"Use Stylus Airdrop contracts on Arbitrum for efficient token distribution at scale. WASM-powered smart contracts enable fast, low-cost airdrops to thousands of wallets.",
|
||||
image: {
|
||||
title: "Stylus Airdrop Contract",
|
||||
icon: "contracts",
|
||||
},
|
||||
});
|
||||
|
||||
# Stylus Airdrop Contract
|
||||
|
||||
|
||||
If you need to send tokens to thousands of wallets at once, you can leverage the Stylus Airdrop contracts.
|
||||
|
||||
Learn how to deploy Stylus Airdrop contracts through dashboard or CLI for inexpensive, WASM-powered distribution on [Arbitrum](https://thirdweb.com/arbitrum) chain.
|
||||
|
||||
### Benefits
|
||||
|
||||
- Ink, not gas: Stylus executes WASM thousands of times faster than the EVM, so sending 10 000 NFTs costs a fraction of normal gas.
|
||||
- Rust Tooling: Cargo tests, Clippy lints, and the full crates ecosystem for your drop logic.
|
||||
- Interoperability: Rust contracts call Solidity (and vice-versa) with zero wrappers.
|
||||
- Battle-tested Logic: Template ports the same claim, snapshot & signature pattern used by thirdweb’s Solidity pre-built contracts.
|
||||
|
||||
## Deploy through dashboard
|
||||
|
||||
Deploying a Stylus Airdrop contract is easy through the thirdweb dashboard and ideal when you don't want to modify any code on the contract.
|
||||
|
||||
<Steps>
|
||||
<Step title="Select Stylus Contract">
|
||||
|
||||
Navigate to the Stylus Contracts section on Explore and select any Airdrop contract for your project.
|
||||
</Step>
|
||||
|
||||
<Step title="Deploy Contract">
|
||||
Select Arbitrum Sepolia or any other Stylus-supported network, then select Deploy.
|
||||
</Step>
|
||||
|
||||
<Step title="Upload Recipients">
|
||||
You can either upload a CSV file with the recipient addresses and amounts and select `Run Airdrop`
|
||||
|
||||
Or set a claim condition for recipients to claim their tokens on their own.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Deploy through CLI
|
||||
|
||||
If you want to modify the contract code or deploy a custom Airdrop contract, you can use the thirdweb CLI.
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a new Stylus Airdrop project">
|
||||
|
||||
In your CLI, run the following command to create a new directory with an airdrop template contract.
|
||||
|
||||
```bash
|
||||
npx thirdweb create-stylus --template airdrop-erc721
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Modify contract logic">
|
||||
In the `src/lib.rs` file you can modify the contract logic such as adding fees, gating logic, analytics events, and more.
|
||||
</Step>
|
||||
|
||||
<Step title="Build & Test the Contract">
|
||||
|
||||
To build your project, run the following command:
|
||||
|
||||
```bash
|
||||
cargo stylus build
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Deploy or Publish Your Contract">
|
||||
You can publish and deploy your project to Arbitrum. Publishing stores your contract metadata in thirdweb’s on-chain registry so anyone (including you) can deploy that exact version later with a few clicks.
|
||||
|
||||
To publish your contract, ensure you have your thirdweb secret key from your created project, then run the following command:
|
||||
|
||||
```bash
|
||||
npx thirdweb publish-stylus -k YOUR_TW_SECRET_KEY
|
||||
```
|
||||
|
||||
If you'd prefer to just deploy a single instance of the contract without publishing, run the following command to deploy:
|
||||
|
||||
```bash
|
||||
npx thirdweb deploy-stylus -k YOUR_TW_SECRET_KEY
|
||||
```
|
||||
|
||||
Once the transaction confirms, the CLI will redirect you to the contract management dashboard where you can mint, transfer, view events, or pull ready-made SDK snippets.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Interacting with the Contract
|
||||
|
||||
Using the thirdweb SDKs, you can interact with your Stylus Airdrop contract to mint tokens, transfer ownership, and more.
|
||||
|
||||
The following includes three common patterns using thirdweb TypeScript SDK:
|
||||
|
||||
### Owner-executed batch airdrop
|
||||
|
||||
**When to use:** you already have every recipient’s address and want to send everything in a single transaction.
|
||||
|
||||
```javascript
|
||||
import { createThirdwebClient, getContract, sendTransaction} from "thirdweb";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
import { airdropERC721 } from "thirdweb/extensions/airdrop";
|
||||
import { createWallet, injectedProvider } from "thirdweb/wallets";
|
||||
|
||||
const client = createThirdwebClient({ clientId });
|
||||
|
||||
const wallet = createWallet("io.metamask"); // or any wallet id
|
||||
const account = await wallet.connect({ client });
|
||||
|
||||
const contract = getContract({
|
||||
client,
|
||||
chain: arbitrumSepolia,
|
||||
address: "<DEPLOYED_ADDRESS>",
|
||||
});
|
||||
|
||||
const contents = [
|
||||
{ recipient: "0xAbc…", tokenId: 1n },
|
||||
{ recipient: "0x123…", tokenId: 2n },
|
||||
];
|
||||
|
||||
const tx = airdropERC721({
|
||||
contract,
|
||||
tokenAddress: contract.address,
|
||||
contents,
|
||||
});
|
||||
|
||||
await sendTransaction({ transaction: tx, account });
|
||||
```
|
||||
|
||||
(Swap for airdropERC20 or airdropERC1155 helpers as needed.)
|
||||
|
||||
### Claim-based airdrop (Merkle snapshot)
|
||||
|
||||
**When to use:** you know the recipients but want them to claim the tokens at their convenience (gas paid by the claimer).
|
||||
|
||||
1. Generate snapshot off-chain
|
||||
|
||||
```javascript
|
||||
import { generateMerkleTreeInfoERC721 } from "thirdweb/extensions/airdrop";
|
||||
|
||||
const snapshot = [
|
||||
{ address: "0xAbc…", tokenId: 7n },
|
||||
{ address: "0x123…", tokenId: 8n },
|
||||
];
|
||||
|
||||
const { merkleRoot, contents } = await generateMerkleTreeInfoERC721({ snapshot });
|
||||
```
|
||||
|
||||
merkleRoot is a single 32-byte value representing the whole list.
|
||||
|
||||
2. Store root on-chain (_setMerkleRoot(root) in the template).
|
||||
|
||||
```javascript
|
||||
const tx = setMerkleRoot({
|
||||
contract,
|
||||
root: merkleRoot,
|
||||
});
|
||||
await sendTransaction({ transaction: tx, account }); // executed by admin
|
||||
```
|
||||
|
||||
3. Recipient claims:
|
||||
|
||||
```javascript
|
||||
import { claimERC721, fetchProofsERC721 } from "thirdweb/extensions/airdrop";
|
||||
|
||||
const proof = await fetchProofsERC721({
|
||||
contract,
|
||||
merkleRoot,
|
||||
recipient: "0xAbc…",
|
||||
});
|
||||
|
||||
const tx = claimERC721({
|
||||
contract,
|
||||
tokenAddress: contract.address,
|
||||
proof,
|
||||
});
|
||||
|
||||
await sendTransaction({ transaction: tx, account });
|
||||
```
|
||||
|
||||
### Signature Based Airdrop (Dynamic Authentication)
|
||||
|
||||
**When to use:** you don’t know the full list ahead of time (e.g., quest rewards). An authorised backend signs a payload per user; the user submits it on-chain.
|
||||
|
||||
1. Generate signed payload server-side:
|
||||
|
||||
```javascript
|
||||
import { generateAirdropSignatureERC721 } from "thirdweb/extensions/airdrop";
|
||||
|
||||
const { signature, payload } = await generateAirdropSignatureERC721({
|
||||
contract,
|
||||
signer: account, // ADMIN role or owner
|
||||
to: "0xRecipient",
|
||||
tokenId: 42n,
|
||||
});
|
||||
```
|
||||
|
||||
2. Recipient executes:
|
||||
|
||||
```javascript
|
||||
import { airdropERC721WithSignature } from "thirdweb/extensions/airdrop";
|
||||
|
||||
const tx = airdropERC721WithSignature({
|
||||
contract,
|
||||
signature,
|
||||
payload,
|
||||
});
|
||||
|
||||
await sendTransaction({ transaction: tx, account: recipientAccount });
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
- [ERC-721 Airdrop Template](https://github.com/thirdweb-example/stylus-airdrop-erc721-template)
|
||||
- [TypeScript SDK Documentation > Airdrops](https://portal.thirdweb.com/references/typescript/v5/airdrop/airdropERC721)
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { Steps, Step, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Stylus Contracts | thirdweb Documentation",
|
||||
description:
|
||||
"Deploy Stylus contracts on Arbitrum for efficient token distribution at scale. WASM-powered smart contracts enable fast, low-cost airdrops to thousands of wallets.",
|
||||
image: {
|
||||
title: "Deploy Stylus Contracts",
|
||||
icon: "contracts",
|
||||
},
|
||||
});
|
||||
|
||||
# Deploy Stylus Contracts on Arbitrum
|
||||
|
||||
Arbitrum Stylus brings a second, WebAssembly (WASM) virtual machine to every Arbitrum chain, so you can write contracts in Rust (or C/C++) while staying 100 % interoperable with existing Solidity code.
|
||||
|
||||
Under the hood, Stylus executes WASM thousands of times faster than the EVM, so transactions are metered in a new, much-smaller unit called ink rather than gas — translating to dramatic fee savings for compute- or memory-heavy logic. Learn more about Stylus on the [Arbitrum documentation](https://docs.arbitrum.io/stylus/concepts/how-it-works).
|
||||
|
||||
This tutorial will cover how to create a simple NFT template project with thirdweb CLI and deploy it to the Arbitrum network using Stylus. You can also follow this tutorial to deploy an ERC-20, ERC-1155, or Airdrop contract.
|
||||
|
||||
## Benefits
|
||||
|
||||
- Rust Tooling: Cargo, Clippy, unit tests, Rust crates, and the broader Rust ecosystem are at your disposal.
|
||||
- Ink-priced execution: Complex math, on-chain SVG generation, even lightweight ML models are suddenly affordable.
|
||||
- Opt-in re-entrancy protection: The Rust SDK disables re-entrancy by default; you only enable it when you really need it.
|
||||
- Multi-VM composability: Rust contract can call Solidity contracts (and vice-versa) without wrappers.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Create a project on your thirdweb account](https://thirdweb.com/dashboard)
|
||||
- Install thirdweb CLI by running `npm install -g thirdweb`
|
||||
- Install Rust tool chain by running `curl https://sh.rustup.rs -sSf | sh` or visit rust-lang.org
|
||||
- Install solc by running `npm install -g solc` or visit soliditylang.org
|
||||
- Install Node version 18 or higher
|
||||
|
||||
## Scaffold an ERC-721 Stylus project
|
||||
|
||||
<Steps>
|
||||
<Step title="Initialize a new Stylus project">
|
||||
In your CLI, create a new directory and run the following command to create a new stylus project:
|
||||
|
||||
```bash
|
||||
npx thirdweb create-stylus
|
||||
```
|
||||
|
||||
Select the ERC-721 template when prompted. This will scaffold a new Stylus project with a basic ERC-721 contract.
|
||||
</Step>
|
||||
|
||||
<Step title="Set Collection Name & Symbol">
|
||||
|
||||
Open src/lib.rs and set the collection name and symbol. You may also tweak any other minting logic as well such as supply cap, roles, and fees.
|
||||
|
||||
</Step>
|
||||
<Step title="Build & test the project">
|
||||
|
||||
Stylus compiles your Rust to WASM and generates an ABI JSON that thirdweb uses for the dashboard & SDK.
|
||||
|
||||
Run the following command to build your project:
|
||||
|
||||
```bash
|
||||
cargo stylus build
|
||||
```
|
||||
|
||||
You can also run the following command to test your project:
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Deploy or Publish Your Contract">
|
||||
|
||||
You can publish and deploy your project to Arbitrum. Publishing stores your contract metadata in thirdweb’s on-chain registry so anyone (including you) can deploy that exact version later with a few clicks.
|
||||
|
||||
To publish your contract, ensure you have your thirdweb secret key from your created project, then run the following command:
|
||||
|
||||
```bash
|
||||
npx thirdweb publish-stylus -k YOUR_TW_SECRET_KEY
|
||||
```
|
||||
|
||||
If you'd prefer to just deploy a single instance of the contract without publishing, run the following command to deploy:
|
||||
|
||||
```bash
|
||||
npx thirdweb deploy-stylus -k YOUR_TW_SECRET_KEY
|
||||
```
|
||||
|
||||
Once the transaction confirms, the CLI will redirect you to the contract management dashboard where you can mint, transfer, view events, or pull ready-made SDK snippets.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="(Optional) Mint NFTs Programmatically">
|
||||
|
||||
You can mint NFTs programmatically using the thirdweb SDKs. Use the following code snippet to mint NFTs:
|
||||
|
||||
```javascript
|
||||
import { createThirdwebClient, getContract, sendTransaction } from "thirdweb";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
import { mintTo } from "thirdweb/extensions/erc721";
|
||||
import { createWallet, injectedProvider } from "thirdweb/wallets";
|
||||
|
||||
const client = createThirdwebClient({ clientId });
|
||||
|
||||
const wallet = createWallet("io.metamask"); // or any wallet id
|
||||
const account = await wallet.connect({ client });
|
||||
|
||||
// 3 · Wrap the deployed contract
|
||||
const contract = getContract({
|
||||
client,
|
||||
chain: arbitrumSepolia,
|
||||
address: "<DEPLOYED_ADDRESS>",
|
||||
});
|
||||
|
||||
// 4 · Prepare the mint transaction
|
||||
const transaction = mintTo({
|
||||
contract,
|
||||
to: "0xRecipient",
|
||||
nft: {
|
||||
name: "Stylus NFT #1",
|
||||
// image, description, attributes, … are optional
|
||||
},
|
||||
});
|
||||
|
||||
// 5 · Send the transaction
|
||||
const result = await sendTransaction({ transaction, account });
|
||||
console.log("Minted token:", result);
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Learn more about Arbitrum Stylus through the official docs on architecture, gas/ink, and safety features. [View Arbitrum Documentation.](https://docs.arbitrum.io/stylus/concepts/how-it-works)
|
||||
@@ -0,0 +1,3 @@
|
||||
# Contract security
|
||||
|
||||
All thirdweb pre-built contracts are audited by third-party security firms. You can see the attached audit reports for each contract linked on the contract page on Explore.
|
||||
@@ -45,7 +45,20 @@ export const sidebar: SideBar = {
|
||||
],
|
||||
name: "Guides",
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
isCollapsible: true,
|
||||
links: [
|
||||
{
|
||||
href: `${slug}/arbitrum-stylus/stylus-contract`,
|
||||
name: "Deploy Stylus Contract",
|
||||
},
|
||||
{
|
||||
href: `${slug}/arbitrum-stylus/airdrop-contract`,
|
||||
name: "Stylus Airdrop Contract",
|
||||
},
|
||||
],
|
||||
name: "Arbitrum Stylus",
|
||||
},
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @thirdweb-dev/nebula
|
||||
|
||||
## 0.2.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`45ba811`](https://github.com/thirdweb-dev/js/commit/45ba811950fe9e85060a4c93c76c13f7f00f8292), [`c672a84`](https://github.com/thirdweb-dev/js/commit/c672a84fc398730e673b40537e03ef09615106c7), [`d52855d`](https://github.com/thirdweb-dev/js/commit/d52855dbdabf497dc72a5dbed56c55d345bac0e9)]:
|
||||
- thirdweb@5.105.31
|
||||
|
||||
## 0.2.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`e114b2c`](https://github.com/thirdweb-dev/js/commit/e114b2c4f98aa9470afffcd5f809953bb69c5c4f)]:
|
||||
- thirdweb@5.105.30
|
||||
|
||||
## 0.2.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -57,5 +57,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.29"
|
||||
"version": "0.2.31"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.105.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#7819](https://github.com/thirdweb-dev/js/pull/7819) [`45ba811`](https://github.com/thirdweb-dev/js/commit/45ba811950fe9e85060a4c93c76c13f7f00f8292) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Sort payment quotes in ascending order
|
||||
|
||||
- [#7807](https://github.com/thirdweb-dev/js/pull/7807) [`c672a84`](https://github.com/thirdweb-dev/js/commit/c672a84fc398730e673b40537e03ef09615106c7) Thanks [@Yash094](https://github.com/Yash094)! - Add chinese locale support
|
||||
|
||||
- [#7824](https://github.com/thirdweb-dev/js/pull/7824) [`d52855d`](https://github.com/thirdweb-dev/js/commit/d52855dbdabf497dc72a5dbed56c55d345bac0e9) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix supportedTokens address comparison
|
||||
|
||||
## 5.105.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#7813](https://github.com/thirdweb-dev/js/pull/7813) [`e114b2c`](https://github.com/thirdweb-dev/js/commit/e114b2c4f98aa9470afffcd5f809953bb69c5c4f) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Skips payment selection in the TransactionWidget if the user's balance is sufficient to complete the transaction.
|
||||
|
||||
## 5.105.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"qrcode": "1.5.3",
|
||||
"toml": "3.0.0",
|
||||
"uqr": "0.1.2",
|
||||
"viem": "2.28.1",
|
||||
"viem": "2.33.2",
|
||||
"zod": "3.25.75"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -409,5 +409,5 @@
|
||||
}
|
||||
},
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "5.105.29"
|
||||
"version": "5.105.31"
|
||||
}
|
||||
|
||||
@@ -93,31 +93,14 @@ export function usePaymentMethods(options: {
|
||||
type: "wallet" as const,
|
||||
quote: s.quote,
|
||||
}));
|
||||
const insufficientBalanceQuotes = validTokenQuotes
|
||||
.filter((s) => s.balance < s.quote.originAmount)
|
||||
.sort((a, b) => {
|
||||
return (
|
||||
Number.parseFloat(
|
||||
toTokens(a.quote.originAmount, a.originToken.decimals),
|
||||
) *
|
||||
(a.originToken.prices.USD || 1) -
|
||||
Number.parseFloat(
|
||||
toTokens(b.quote.originAmount, b.originToken.decimals),
|
||||
) *
|
||||
(b.originToken.prices.USD || 1)
|
||||
);
|
||||
});
|
||||
|
||||
const sufficientBalanceQuotes = validTokenQuotes
|
||||
.filter((s) => s.balance >= s.quote.originAmount)
|
||||
.filter((s) => !!s.originToken.prices.USD)
|
||||
.sort((a, b) => {
|
||||
return (
|
||||
Number.parseFloat(
|
||||
toTokens(b.quote.originAmount, b.originToken.decimals),
|
||||
) *
|
||||
Number.parseFloat(toTokens(b.balance, b.originToken.decimals)) *
|
||||
(b.originToken.prices.USD || 1) -
|
||||
Number.parseFloat(
|
||||
toTokens(a.quote.originAmount, a.originToken.decimals),
|
||||
) *
|
||||
Number.parseFloat(toTokens(a.balance, a.originToken.decimals)) *
|
||||
(a.originToken.prices.USD || 1)
|
||||
);
|
||||
});
|
||||
@@ -133,15 +116,14 @@ export function usePaymentMethods(options: {
|
||||
)
|
||||
: [];
|
||||
const finalQuotes = supportedTokens
|
||||
? [...sufficientBalanceQuotes, ...insufficientBalanceQuotes].filter(
|
||||
(q) =>
|
||||
tokensToInclude.find(
|
||||
(t) =>
|
||||
t.chainId === q.originToken.chainId &&
|
||||
t.address === q.originToken.address,
|
||||
),
|
||||
? sufficientBalanceQuotes.filter((q) =>
|
||||
tokensToInclude.find(
|
||||
(t) =>
|
||||
t.chainId === q.originToken.chainId &&
|
||||
t.address.toLowerCase() === q.originToken.address.toLowerCase(),
|
||||
),
|
||||
)
|
||||
: [...sufficientBalanceQuotes, ...insufficientBalanceQuotes];
|
||||
: sufficientBalanceQuotes;
|
||||
return finalQuotes;
|
||||
},
|
||||
queryKey: [
|
||||
|
||||
@@ -64,7 +64,6 @@ export function useTransactionDetails({
|
||||
return useQuery({
|
||||
enabled: !!transaction.to && !!chainMetadata.data,
|
||||
queryFn: async (): Promise<TransactionDetails> => {
|
||||
// Create contract instance for metadata fetching
|
||||
const contract = getContract({
|
||||
address: transaction.to as string,
|
||||
chain: transaction.chain,
|
||||
@@ -84,7 +83,7 @@ export function useTransactionDetails({
|
||||
client,
|
||||
erc20Value?.tokenAddress || NATIVE_TOKEN_ADDRESS,
|
||||
transaction.chain.id,
|
||||
).catch(() => null),
|
||||
),
|
||||
hasSponsoredTransactions
|
||||
? 0n
|
||||
: getTransactionGasCost(transaction).catch(() => null),
|
||||
|
||||
@@ -230,6 +230,8 @@ export function usePaymentMachine(
|
||||
if (event.type === "DESTINATION_CONFIRMED")
|
||||
return "methodSelection";
|
||||
if (event.type === "ERROR_OCCURRED") return "error";
|
||||
if (event.type === "CONTINUE_TO_TRANSACTION")
|
||||
return "post-buy-transaction";
|
||||
break;
|
||||
|
||||
case "methodSelection":
|
||||
|
||||
@@ -192,6 +192,7 @@ export function BridgeOrchestrator({
|
||||
// Handle errors
|
||||
const handleError = useCallback(
|
||||
(error: Error) => {
|
||||
console.error(error);
|
||||
onError?.(error);
|
||||
send({ error, type: "ERROR_OCCURRED" });
|
||||
},
|
||||
@@ -288,6 +289,7 @@ export function BridgeOrchestrator({
|
||||
client={client}
|
||||
connectOptions={modifiedConnectOptions}
|
||||
onContinue={handleRequirementsResolved}
|
||||
onExecuteTransaction={() => send({ type: "CONTINUE_TO_TRANSACTION" })}
|
||||
showThirdwebBranding={showThirdwebBranding}
|
||||
uiOptions={uiOptions}
|
||||
/>
|
||||
|
||||
@@ -33,6 +33,9 @@ import { BridgeOrchestrator, type UIOptions } from "./BridgeOrchestrator.js";
|
||||
import { UnsupportedTokenScreen } from "./UnsupportedTokenScreen.js";
|
||||
|
||||
export type BuyWidgetProps = {
|
||||
/**
|
||||
* Customize the supported tokens that users can pay with.
|
||||
*/
|
||||
supportedTokens?: SupportedTokens;
|
||||
/**
|
||||
* A client is the entry point to the thirdweb SDK.
|
||||
@@ -217,7 +220,7 @@ type UIOptionsResult =
|
||||
* <BuyWidget
|
||||
* client={client}
|
||||
* chain={ethereum}
|
||||
* amount="0.1"
|
||||
* amount="0.1" // in native tokens (ie. ETH)
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
@@ -229,11 +232,34 @@ type UIOptionsResult =
|
||||
* <BuyWidget
|
||||
* client={client}
|
||||
* chain={ethereum}
|
||||
* amount="100"
|
||||
* amount="100" // 100 USDC on mainnet
|
||||
* tokenAddress="0xA0b86a33E6417E4df2057B2d3C6d9F7cc11b0a70"
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* ### Customize the supported tokens
|
||||
*
|
||||
* You can customize the supported tokens that users can pay with by passing a `supportedTokens` object to the `BuyWidget` component.
|
||||
*
|
||||
* ```tsx
|
||||
* <BuyWidget
|
||||
* client={client}
|
||||
* chain={ethereum}
|
||||
* amount="0.1"
|
||||
* // user will only be able to pay with these tokens
|
||||
* supportedTokens={{
|
||||
* [8453]: [
|
||||
* {
|
||||
* address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
||||
* name: "USDC",
|
||||
* symbol: "USDC",
|
||||
* },
|
||||
* ],
|
||||
* }}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* ### Customize the UI
|
||||
*
|
||||
* You can customize the UI of the `BuyWidget` component by passing a custom theme object to the `theme` prop.
|
||||
|
||||
@@ -29,6 +29,9 @@ import { BridgeOrchestrator, type UIOptions } from "./BridgeOrchestrator.js";
|
||||
import { UnsupportedTokenScreen } from "./UnsupportedTokenScreen.js";
|
||||
|
||||
export type CheckoutWidgetProps = {
|
||||
/**
|
||||
* Customize the supported tokens that users can pay with.
|
||||
*/
|
||||
supportedTokens?: SupportedTokens;
|
||||
/**
|
||||
* A client is the entry point to the thirdweb SDK.
|
||||
@@ -215,11 +218,43 @@ type UIOptionsResult =
|
||||
* @example
|
||||
* ### Default configuration
|
||||
*
|
||||
* By default, the `CheckoutWidget` component will allows users to fund their wallets with crypto or fiat on any of the supported chains..
|
||||
* The `CheckoutWidget` component allows user to pay a given wallet for any product or service. You can register webhooks to get notified for every purchase done via the widget.
|
||||
*
|
||||
* ```tsx
|
||||
* <CheckoutWidget
|
||||
* client={client}
|
||||
* chain={base}
|
||||
* amount="0.01" // in native tokens (ETH), pass tokenAddress to charge in a specific token (USDC, USDT, etc.)
|
||||
* seller="0x123...abc" // the wallet address that will receive the payment
|
||||
* name="Premium Course"
|
||||
* description="Complete guide to web3 development"
|
||||
* image="/course-thumbnail.jpg"
|
||||
* onSuccess={() => {
|
||||
* alert("Purchase successful!");
|
||||
* }}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* ### Customize the supported tokens
|
||||
*
|
||||
* You can customize the supported tokens that users can pay with by passing a `supportedTokens` object to the `CheckoutWidget` component.
|
||||
*
|
||||
* ```tsx
|
||||
* <CheckoutWidget
|
||||
* client={client}
|
||||
* chain={arbitrum}
|
||||
* amount="0.01"
|
||||
* seller="0x123...abc"
|
||||
* // user will only be able to pay with these tokens
|
||||
* supportedTokens={{
|
||||
* [8453]: [
|
||||
* {
|
||||
* address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
||||
* name: "USDC",
|
||||
* symbol: "USDC",
|
||||
* },
|
||||
* ],
|
||||
* }}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
@@ -230,6 +265,9 @@ type UIOptionsResult =
|
||||
* ```tsx
|
||||
* <CheckoutWidget
|
||||
* client={client}
|
||||
* chain={arbitrum}
|
||||
* amount="0.01"
|
||||
* seller="0x123...abc"
|
||||
* theme={darkTheme({
|
||||
* colors: {
|
||||
* modalBg: "red",
|
||||
@@ -258,6 +296,9 @@ type UIOptionsResult =
|
||||
* ```tsx
|
||||
* <CheckoutWidget
|
||||
* client={client}
|
||||
* chain={arbitrum}
|
||||
* amount="0.01"
|
||||
* seller="0x123...abc"
|
||||
* connectOptions={{
|
||||
* connectModal: {
|
||||
* size: 'compact',
|
||||
|
||||
@@ -48,6 +48,11 @@ export interface TransactionPaymentProps {
|
||||
*/
|
||||
onContinue: (amount: string, token: Token, receiverAddress: Address) => void;
|
||||
|
||||
/**
|
||||
* Request to execute the transaction immediately (skips funding flow)
|
||||
*/
|
||||
onExecuteTransaction: () => void;
|
||||
|
||||
/**
|
||||
* Connect options for wallet connection
|
||||
*/
|
||||
@@ -64,6 +69,7 @@ export function TransactionPayment({
|
||||
uiOptions,
|
||||
client,
|
||||
onContinue,
|
||||
onExecuteTransaction,
|
||||
connectOptions,
|
||||
showThirdwebBranding = true,
|
||||
}: TransactionPaymentProps) {
|
||||
@@ -378,6 +384,16 @@ export function TransactionPayment({
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user has enough to pay, skip the payment step altogether
|
||||
if (
|
||||
userBalance &&
|
||||
Number(userBalance) >=
|
||||
Number(transactionDataQuery.data.totalCost)
|
||||
) {
|
||||
onExecuteTransaction();
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, use the full transaction cost
|
||||
onContinue(
|
||||
transactionDataQuery.data.totalCost,
|
||||
|
||||
@@ -37,6 +37,9 @@ import { BridgeOrchestrator, type UIOptions } from "./BridgeOrchestrator.js";
|
||||
import { UnsupportedTokenScreen } from "./UnsupportedTokenScreen.js";
|
||||
|
||||
export type TransactionWidgetProps = {
|
||||
/**
|
||||
* Customize the supported tokens that users can pay with.
|
||||
*/
|
||||
supportedTokens?: SupportedTokens;
|
||||
/**
|
||||
* A client is the entry point to the thirdweb SDK.
|
||||
@@ -218,7 +221,7 @@ type UIOptionsResult =
|
||||
* @example
|
||||
* ### Default configuration
|
||||
*
|
||||
* By default, the `TransactionWidget` component will allows users to fund their wallets with crypto or fiat on any of the supported chains..
|
||||
* By default, the `TransactionWidget` component allows users to fund their wallets with crypto or fiat on any of the supported chains.
|
||||
*
|
||||
* ```tsx
|
||||
* <TransactionWidget
|
||||
@@ -232,6 +235,30 @@ type UIOptionsResult =
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* ### Customize the supported tokens
|
||||
*
|
||||
* You can customize the supported tokens that users can pay with by passing a `supportedTokens` object to the `TransactionWidget` component.
|
||||
*
|
||||
* ```tsx
|
||||
* <TransactionWidget
|
||||
* client={client}
|
||||
* transaction={prepareTransaction({
|
||||
* to: "0x...",
|
||||
* chain: ethereum,
|
||||
* client: client,
|
||||
* })}
|
||||
* supportedTokens={{
|
||||
* [8453]: [
|
||||
* {
|
||||
* address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
||||
* name: "USDC",
|
||||
* symbol: "USDC",
|
||||
* },
|
||||
* ],
|
||||
* }}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* ### Customize the UI
|
||||
*
|
||||
* You can customize the UI of the `TransactionWidget` component by passing a custom theme object to the `theme` prop.
|
||||
|
||||
@@ -30,6 +30,9 @@ export async function getConnectLocale(localeId: LocaleId) {
|
||||
case "pt_BR": {
|
||||
return (await import("./br.js")).default;
|
||||
}
|
||||
case "zh_CN": {
|
||||
return (await import("./zh.js")).default;
|
||||
}
|
||||
default: {
|
||||
return (await import("./en.js")).default;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { ConnectLocale } from "./types.js";
|
||||
|
||||
const connectLocaleZh: ConnectLocale = {
|
||||
agreement: {
|
||||
and: "和",
|
||||
prefix: "连接即表示您同意",
|
||||
privacyPolicy: "隐私政策",
|
||||
termsOfService: "服务条款",
|
||||
},
|
||||
backupWallet: "备份钱包",
|
||||
buy: "购买",
|
||||
confirmInWallet: "在钱包中确认",
|
||||
connectAWallet: "连接钱包",
|
||||
connectedToSmartWallet: "智能钱包",
|
||||
connecting: "连接中",
|
||||
continueAsGuest: "以访客身份继续",
|
||||
copyAddress: "复制地址",
|
||||
currentNetwork: "当前网络",
|
||||
defaultButtonTitle: "连接",
|
||||
defaultModalTitle: "登录",
|
||||
disconnectWallet: "断开钱包连接",
|
||||
getStarted: "开始使用",
|
||||
goBackButton: "返回",
|
||||
guest: "访客",
|
||||
guestWalletWarning:
|
||||
"这是一个临时访客钱包。如果您不想失去访问权限,请备份钱包",
|
||||
id: "zh_CN",
|
||||
installed: "已安装",
|
||||
manageWallet: {
|
||||
connectAnApp: "连接应用",
|
||||
exportPrivateKey: "导出私钥",
|
||||
linkedProfiles: "已关联配置文件",
|
||||
linkProfile: "关联配置文件",
|
||||
title: "管理钱包",
|
||||
},
|
||||
networkSelector: {
|
||||
addCustomNetwork: "添加自定义网络",
|
||||
allNetworks: "全部",
|
||||
categoryLabel: {
|
||||
others: "所有网络",
|
||||
popular: "热门",
|
||||
recentlyUsed: "最近使用",
|
||||
},
|
||||
failedToSwitch: "网络切换失败",
|
||||
inputPlaceholder: "搜索网络或链 ID",
|
||||
loading: "加载中",
|
||||
mainnets: "主网",
|
||||
testnets: "测试网",
|
||||
title: "选择网络",
|
||||
},
|
||||
newToWallets: "刚接触钱包?",
|
||||
or: "或",
|
||||
passkeys: {
|
||||
linkPasskey: "添加通行密钥",
|
||||
title: "通行密钥",
|
||||
},
|
||||
payTransactions: "法币交易",
|
||||
personalWallet: "个人钱包",
|
||||
receive: "接收",
|
||||
receiveFundsScreen: {
|
||||
instruction: "复制地址以向此钱包发送资金",
|
||||
title: "接收资金",
|
||||
},
|
||||
recommended: "推荐",
|
||||
requestTestnetFunds: "申请测试网资金",
|
||||
send: "发送",
|
||||
sendFundsScreen: {
|
||||
amount: "金额",
|
||||
insufficientFunds: "余额不足",
|
||||
invalidAddress: "地址无效",
|
||||
noTokensFound: "未找到代币",
|
||||
searchToken: "搜索或粘贴代币地址",
|
||||
selectTokenTitle: "选择代币",
|
||||
sending: "发送中",
|
||||
sendTo: "发送到",
|
||||
submitButton: "发送",
|
||||
successMessage: "交易成功",
|
||||
title: "发送资金",
|
||||
token: "代币",
|
||||
transactionFailed: "交易失败",
|
||||
transactionRejected: "交易被拒绝",
|
||||
},
|
||||
signatureScreen: {
|
||||
instructionScreen: {
|
||||
disconnectWallet: "断开钱包连接",
|
||||
instruction: "请在您的钱包中签署消息请求以继续",
|
||||
signInButton: "登录",
|
||||
title: "登录",
|
||||
},
|
||||
signingScreen: {
|
||||
approveTransactionInSafe: "在 Safe 中批准交易",
|
||||
failedToSignIn: "登录失败",
|
||||
inProgress: "等待确认",
|
||||
prompt: "在您的钱包中签署签名请求",
|
||||
promptForSafe: "在您的钱包中签署签名请求并在 Safe 中批准交易",
|
||||
title: "登录中",
|
||||
tryAgain: "重试",
|
||||
},
|
||||
},
|
||||
signIn: "登录",
|
||||
smartWallet: "智能钱包",
|
||||
switchAccount: "切换账户",
|
||||
switchingNetwork: "网络切换中",
|
||||
switchNetwork: "切换网络",
|
||||
switchTo: "切换到",
|
||||
transactions: "交易",
|
||||
viewAllTransactions: "查看所有交易",
|
||||
viewFunds: {
|
||||
title: "查看资产",
|
||||
viewAssets: "查看资产",
|
||||
viewNFTs: "查看NFT资产",
|
||||
viewTokens: "查看代币",
|
||||
},
|
||||
walletTransactions: "钱包交易",
|
||||
welcomeScreen: {
|
||||
defaultSubtitle: "连接钱包以开始使用",
|
||||
defaultTitle: "您通往去中心化世界的门户",
|
||||
},
|
||||
};
|
||||
|
||||
export default connectLocaleZh;
|
||||
@@ -9,4 +9,5 @@ export type LocaleId =
|
||||
| "de_DE"
|
||||
| "fr_FR"
|
||||
| "ru_RU"
|
||||
| "pt_BR";
|
||||
| "pt_BR"
|
||||
| "zh_CN";
|
||||
|
||||
@@ -27,6 +27,8 @@ export async function getInjectedWalletLocale(
|
||||
case "pt_BR": {
|
||||
return (await import("./br.js")).default;
|
||||
}
|
||||
case "zh_CN":
|
||||
return (await import("./zh.js")).default;
|
||||
default:
|
||||
return (await import("./en.js")).default;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { InjectedWalletLocale } from "./types.js";
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
const injectedWalletLocaleZh = (wallet: string): InjectedWalletLocale => ({
|
||||
connectionScreen: {
|
||||
failed: "连接失败",
|
||||
inProgress: "等待用户确认",
|
||||
instruction: `请在 ${wallet} 中接受连接请求`,
|
||||
retry: "重试",
|
||||
},
|
||||
download: {
|
||||
android: "从 Google Play 下载",
|
||||
chrome: "下载 Chrome 扩展",
|
||||
iOS: "从 App Store 下载",
|
||||
},
|
||||
getStartedLink: `没有 ${wallet}?`,
|
||||
getStartedScreen: {
|
||||
instruction: `扫描二维码下载 ${wallet} 应用`,
|
||||
},
|
||||
scanScreen: {
|
||||
instruction: `用 ${wallet} 扫描二维码连接`,
|
||||
},
|
||||
});
|
||||
|
||||
export default injectedWalletLocaleZh;
|
||||
@@ -27,6 +27,8 @@ export async function getInAppWalletLocale(
|
||||
case "pt_BR": {
|
||||
return (await import("./br.js")).default;
|
||||
}
|
||||
case "zh_CN":
|
||||
return (await import("./zh.js")).default;
|
||||
default:
|
||||
return (await import("./en.js")).default;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { InAppWalletLocale } from "./types.js";
|
||||
|
||||
export default {
|
||||
createPassword: {
|
||||
confirmation: "我已保存密码",
|
||||
failedToSetPassword: "设置密码失败",
|
||||
inputPlaceholder: "输入密码",
|
||||
instruction: "为账户设置密码。使用新设备登录时需要此密码。",
|
||||
saveInstruction: "请妥善保存密码",
|
||||
submitButton: "设置密码",
|
||||
title: "创建密码",
|
||||
},
|
||||
emailLoginScreen: {
|
||||
enterCodeSendTo: "输入发送至您邮箱的验证码",
|
||||
enterRecoveryCode: "输入您首次注册时邮箱收到的恢复码",
|
||||
failedToSendCode: "发送验证码失败",
|
||||
invalidCode: "验证码无效",
|
||||
invalidCodeOrRecoveryCode: "验证码或恢复码无效",
|
||||
newDeviceDetected: "检测到新设备",
|
||||
resendCode: "重新发送验证码",
|
||||
sendingCode: "正在发送验证码",
|
||||
title: "登录",
|
||||
verify: "验证",
|
||||
},
|
||||
emailPlaceholder: "邮箱地址",
|
||||
emailRequired: "请输入邮箱地址",
|
||||
enterPassword: {
|
||||
inputPlaceholder: "输入您的密码",
|
||||
instruction: "输入您账户的密码",
|
||||
submitButton: "验证",
|
||||
title: "输入密码",
|
||||
wrongPassword: "密码错误",
|
||||
},
|
||||
invalidEmail: "邮箱地址无效",
|
||||
invalidPhone: "手机号码无效",
|
||||
linkWallet: "连接钱包",
|
||||
loginAsGuest: "以访客身份继续",
|
||||
maxAccountsExceeded: "账户数量已达上限。请联系应用开发者。",
|
||||
or: "或",
|
||||
passkey: "通行密钥",
|
||||
phonePlaceholder: "手机号码",
|
||||
phoneRequired: "手机号码为必填项",
|
||||
signIn: "登录",
|
||||
signInWithApple: "Apple",
|
||||
signInWithDiscord: "Discord",
|
||||
signInWithEmail: "邮箱登录",
|
||||
signInWithFacebook: "Facebook",
|
||||
signInWithGoogle: "Google",
|
||||
signInWithPhone: "使用手机号登录",
|
||||
signInWithWallet: "使用钱包登录",
|
||||
socialLoginScreen: {
|
||||
failed: "登录失败",
|
||||
instruction: "请在弹窗中登录账户",
|
||||
retry: "重试",
|
||||
title: "登录",
|
||||
},
|
||||
submitEmail: "继续",
|
||||
} satisfies InAppWalletLocale;
|
||||
@@ -27,6 +27,8 @@ export async function getSmartWalletLocale(
|
||||
case "pt_BR": {
|
||||
return (await import("./br.js")).default;
|
||||
}
|
||||
case "zh_CN":
|
||||
return (await import("./zh.js")).default;
|
||||
default:
|
||||
return (await import("./en.js")).default;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { SmartWalletLocale } from "./types.js";
|
||||
|
||||
const smartWalletLocalZh: SmartWalletLocale = {
|
||||
connecting: "正在连接智能账户",
|
||||
failedToConnect: "连接智能账户失败",
|
||||
wrongNetworkScreen: {
|
||||
failedToSwitch: "网络切换失败",
|
||||
subtitle: "钱包未连接至所需网络",
|
||||
title: "网络错误",
|
||||
},
|
||||
};
|
||||
|
||||
export default smartWalletLocalZh;
|
||||
@@ -29,6 +29,7 @@ const TransactionPaymentWithTheme = (
|
||||
const meta = {
|
||||
args: {
|
||||
client: storyClient,
|
||||
onExecuteTransaction: () => {},
|
||||
onContinue: (_amount, _token, _receiverAddress) => {},
|
||||
theme: "dark",
|
||||
uiOptions: TRANSACTION_UI_OPTIONS.ethTransfer,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# @thirdweb-dev/wagmi-adapter
|
||||
|
||||
## 0.2.128
|
||||
|
||||
## 0.2.127
|
||||
|
||||
## 0.2.126
|
||||
|
||||
## 0.2.125
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.126"
|
||||
"version": "0.2.128"
|
||||
}
|
||||
|
||||
Generated
+863
-468
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user