Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
fde39eb883 Version Packages (#7820)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-10 16:25:15 -07:00
c672a84fc3 Add chinese locale to connect button (#7807)
Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: yashjoisar <[email protected]>
2025-08-10 16:17:18 -07:00
Joaquim VergesandGitHub d52855dbda [SDK] Fix supportedTokens address comparison in usePaymentMethods (#7824) 2025-08-10 16:16:11 -07:00
MananTank a3909e1794 Add caching to ContractCard, speed up /explore pageload (#7822)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the `ContractRow`, `ContractPublisher`, and `ContractCard` components to streamline their props and improve caching mechanisms for fetching contract data.

### Detailed summary
- Simplified `ContractRow` props by removing the interface.
- Refactored `ContractPublisher` to use props directly instead of an interface.
- Updated `ContractPublisher` to utilize `Img` and `Blobbie` for avatar rendering.
- Introduced caching for `fetchPublishedContractVersion` and `resolvePublisherEnsAndAvatar`.
- Changed how `ContractPublisher` is rendered in `ContractCard` to use new props structure.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Improved performance and consistency for contract publisher information by introducing server-side caching for contract version data and publisher ENS details.
  * Enhanced publisher display with avatars and ENS names, including fallback visuals when no avatar is available.

* **Refactor**
  * Streamlined the contract publisher display by replacing the previous component with a simplified implementation accepting explicit props.
  * Updated type definitions for improved code clarity without affecting user-facing behavior.

* **Chores**
  * Minor import and type annotation adjustments for better maintainability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-09 15:59:46 +00:00
nischitpra f92201d353 webhook multiselect topic (#7818)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on modifying the handling of `sigHash` to support both single and multiple signature inputs across various components, improving user experience and functionality.

### Detailed summary
- Changed `sigHash` from a string to an array in multiple components.
- Updated validation and handling of `sigHash` in forms and utilities.
- Enhanced UI to reflect multiple signature selections.
- Adjusted props and methods to support multi-select functionality in `SignatureSelector` and related components.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Added support for selecting multiple signature hashes in webhook filters and selectors.
  * Enhanced signature selectors to allow both single-select and multi-select modes, with improved handling of custom signatures.

* **Improvements**
  * Updated UI labels, placeholders, and descriptions to reflect multi-selection capabilities and pluralization.
  * Review and summary steps now display multiple selected signatures with appropriate formatting.
  * Webhook payloads and validation schemas now support arrays of signature hashes for improved flexibility.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-08 20:18:28 +00:00
gregandGitHub 45ba811950 [SDK] Fix: Sort payment quotes in ascending order (#7819) 2025-08-08 12:42:53 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
7c458eaa49 Version Packages (#7814)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-08 11:04:39 -07:00
gregandGitHub f7ce6becd6 [SDK] Fix: Adds onExecuteTransaction to TransactionPayment (#7816) 2025-08-07 20:27:27 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
52d1ba32c4 Chore(deps): Bump viem from 2.28.1 to 2.33.2 (#7793)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-07 18:31:12 -07:00
saminacodes 1a7315c815 arb-docs (#7799)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the documentation for thirdweb's `Stylus` contracts, detailing the deployment and usage of `Stylus` contracts and `Airdrop` contracts on the `Arbitrum` network, including benefits, prerequisites, and step-by-step tutorials.

### Detailed summary
- Added a new section on `Contract security` in `page.mdx`.
- Updated `sidebar.tsx` to include collapsible sections for `Arbitrum Stylus` with links to contract deployment guides.
- Created detailed documentation for `Stylus Contracts` and `Stylus Airdrop Contracts` in their respective `page.mdx` files, including benefits, prerequisites, and step-by-step deployment instructions.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Documentation**
  * Added a new page detailing Stylus Airdrop Contract deployment and usage on Arbitrum, including multiple airdrop patterns and integration examples.
  * Introduced a guide for deploying Stylus contracts on Arbitrum, covering project setup, deployment, and NFT minting.
  * Added a contract security section highlighting third-party audits and linking to audit reports.
  * Updated the sidebar with a new "Arbitrum Stylus" section for easier navigation to Stylus contract guides.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-08 00:48:15 +00:00
gregandGitHub 50c6c193c6 [SDK] Fix: Bridge.tokens domain (#7815) 2025-08-07 16:55:42 -07:00
gregandGitHub e114b2c4f9 [SDK] Fix: Skip payment method selection when balance is sufficient (#7813) 2025-08-07 16:37:53 -07:00
MananTank d812cc370b [MNY-92] Dashboard: Add tokens section in explore (#7812)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving the project selection and token creation flow within the dashboard application. It includes the addition of new components, updates to existing ones, and enhancements to user notifications and alerts.

### Detailed summary
- Updated import paths for `project-selector` and `team-selector`.
- Added `TokenBanner` component for displaying token creation options.
- Enhanced `DismissibleAlert` to support conditional rendering based on visibility state.
- Introduced `GenericProjectSelector` for improved project selection.
- Modified `ExplorePage` layout and styling for better presentation.
- Added `TokensSection` component with links for launching tokens and NFTs.
- Updated `reportTokenUpsellClicked` function for event tracking.
- Improved UI elements for better user experience and responsiveness.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Introduced a "Tokens" section on the Explore page, showcasing options for creating ERC-20 tokens (Coins) and NFT Collections.
  * Added a promotional token banner on deploy pages for select published token contracts.
  * Launched a new project selection page within the team context, streamlining project navigation and selection.
  * Added a generic project selector component to enhance project selection flexibility.
  * Added tracking for clicks on token creation cards and token upsell banner interactions to improve analytics.

* **Improvements**
  * Enhanced layout and styling of the Explore page for better readability and organization.
  * Updated project and team selector components for more flexible descriptions and improved navigation.
  * Updated team page alert to preserve dismissal state across sessions.

* **Chores**
  * Refined import paths for improved code organization and maintainability.
  * Refactored dismissible alert component to support optional state persistence.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-07 20:45:31 +00:00
MananTank 7883127d51 Dashboard: Add caching to analytics charts (#7804)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the analytics functionality across various components by adding an `authToken` parameter to API calls, ensuring secure access to analytics data.

### Detailed summary
- Added `authToken` prop to multiple components for secure API access.
- Updated API functions to accept `authToken` for authentication.
- Modified analytics data fetching methods to include `authToken`.
- Changed default values and parameters in several functions for consistency.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * Analytics data fetching now uses caching for improved performance.
  * Authentication is enforced for all analytics-related pages and components, with users redirected to login if not authenticated.

* **Refactor**
  * All analytics API calls and related components now require an explicit authentication token, improving security and consistency.
  * Parameters for analytics functions and components are more consistently structured, often grouped into objects for clarity.

* **Style**
  * Default chart display updated from "totalVolume" to "activeUsers" in highlights cards for a more relevant initial view.

* **Bug Fixes**
  * Standardized error handling across analytics data requests.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-07 14:33:04 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
53d0d74d9b Version Packages (#7810)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-07 02:59:39 -07:00
Joaquim VergesandGitHub c0948fb9fe [SDK] Handle environments where process is not defined (#7809) 2025-08-07 02:48:44 -07:00
81 changed files with 3668 additions and 1402 deletions
+14
View File
@@ -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>
);
}
@@ -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}
@@ -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) {
@@ -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" &&
@@ -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
@@ -61,6 +61,7 @@ export default async function Page(props: {
return (
<ResponsiveSearchParamsProvider value={searchParams}>
<EcosystemAnalyticsPage
authToken={authToken}
ecosystemSlug={ecosystem.slug}
interval={interval}
defaultRange={defaultRange}
@@ -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>
);
@@ -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" />
@@ -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} />;
}
@@ -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} />;
}
@@ -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}
/>
);
}
@@ -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([
@@ -54,6 +54,7 @@ export default async function Page(props: {
projectId={project.id}
range={range}
teamId={project.teamId}
authToken={authToken}
/>
<div className="h-10" />
@@ -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} />
@@ -54,6 +54,7 @@ export default async function Page(props: {
projectId={project.id}
range={range}
teamId={project.teamId}
authToken={authToken}
/>
<div className="h-10" />
@@ -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 [];
});
@@ -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" />
@@ -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>
);
@@ -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 [];
});
@@ -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>
@@ -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 [];
@@ -65,7 +65,7 @@ export function CreateContractWebhookButton({
inputAbi: [],
name: "",
secret: "",
sigHash: "",
sigHash: [],
sigHashAbi: "",
toAddresses: "",
webhookUrl: "",
@@ -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>
@@ -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>
@@ -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>
@@ -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:
@@ -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: {
@@ -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<{
@@ -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
@@ -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 &amp; 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.
+14 -1
View File
@@ -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: [
+21
View File
@@ -1,5 +1,26 @@
# @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
- Updated dependencies [[`c0948fb`](https://github.com/thirdweb-dev/js/commit/c0948fb9fe71b6403ac08f84e4c6f30bdbea8e3a)]:
- thirdweb@5.105.29
## 0.2.28
### Patch Changes
+1 -1
View File
@@ -57,5 +57,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.2.28"
"version": "0.2.31"
}
+22
View File
@@ -1,5 +1,27 @@
# 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
- [#7809](https://github.com/thirdweb-dev/js/pull/7809) [`c0948fb`](https://github.com/thirdweb-dev/js/commit/c0948fb9fe71b6403ac08f84e4c6f30bdbea8e3a) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Handle environments where process is not defined
## 5.105.28
### Patch Changes
+2 -2
View File
@@ -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.28"
"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;
+2 -1
View File
@@ -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,
@@ -3,7 +3,9 @@ export const ZKSYNC_SINGLETON_FACTORY =
export const CONTRACT_DEPLOYER_ADDRESS =
"0x0000000000000000000000000000000000008006" as const;
export const KNOWN_CODES_STORAGE = "0x0000000000000000000000000000000000008004";
export const PUBLISHED_PRIVATE_KEY = process.env.ZKSYNC_PUBLISHED_PRIVATE_KEY;
export const PUBLISHED_PRIVATE_KEY = process
? process.env.ZKSYNC_PUBLISHED_PRIVATE_KEY
: undefined;
export const singletonFactoryAbi = [
"function deploy(bytes32,bytes32,bytes) external payable",
+3 -2
View File
@@ -1,4 +1,5 @@
export const IS_DEV =
process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
process &&
(process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test");
export const IS_TEST = process.env.NODE_ENV === "test";
export const IS_TEST = process && process.env.NODE_ENV === "test";
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/wagmi-adapter
## 0.2.128
## 0.2.127
## 0.2.126
## 0.2.125
## 0.2.124
+1 -1
View File
@@ -55,5 +55,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.2.125"
"version": "0.2.128"
}
+863 -468
View File
File diff suppressed because it is too large Load Diff