Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
781a626461 | ||
|
|
f630912b9b | ||
|
|
163ab8afe1 | ||
|
|
62cfbb7441 | ||
|
|
d7104f69f4 | ||
|
|
23545cb999 | ||
|
|
f9283f389b | ||
|
|
9fbb29dc25 | ||
|
|
71a1e58eaf | ||
|
|
68b48f4faa | ||
|
|
1c660d4712 | ||
|
|
30b5c30ba0 | ||
|
|
86461f9a07 | ||
|
|
2ebba6139a | ||
|
|
f3bacc16cb | ||
|
|
968913d55b | ||
|
|
bd70db4a5d | ||
|
|
d9a138a90a | ||
|
|
777aed1e5f | ||
|
|
fbed4be959 | ||
|
|
5a26217a43 |
@@ -0,0 +1,70 @@
|
||||
"use server";
|
||||
|
||||
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
|
||||
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
|
||||
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
|
||||
|
||||
interface VaultWalletListItem {
|
||||
id: string;
|
||||
address: string;
|
||||
metadata?: {
|
||||
label?: string;
|
||||
projectId?: string;
|
||||
teamId?: string;
|
||||
type?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export async function listProjectServerWallets(params: {
|
||||
managementAccessToken: string;
|
||||
projectId: string;
|
||||
pageSize?: number;
|
||||
}): Promise<ProjectWalletSummary[]> {
|
||||
const { managementAccessToken, projectId, pageSize = 100 } = params;
|
||||
|
||||
if (!managementAccessToken || !NEXT_PUBLIC_THIRDWEB_VAULT_URL) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const vaultClient = await createVaultClient({
|
||||
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
|
||||
});
|
||||
|
||||
const response = await listEoas({
|
||||
client: vaultClient,
|
||||
request: {
|
||||
auth: {
|
||||
accessToken: managementAccessToken,
|
||||
},
|
||||
options: {
|
||||
page: 0,
|
||||
// @ts-expect-error - Vault SDK expects snake_case pagination fields
|
||||
page_size: pageSize,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success || !response.data?.items) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items = response.data.items as VaultWalletListItem[];
|
||||
|
||||
return items
|
||||
.filter((item) => {
|
||||
return (
|
||||
item.metadata?.projectId === projectId &&
|
||||
(!item.metadata?.type || item.metadata.type === "server-wallet")
|
||||
);
|
||||
})
|
||||
.map<ProjectWalletSummary>((item) => ({
|
||||
id: item.id,
|
||||
address: item.address,
|
||||
label: item.metadata?.label ?? undefined,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Failed to list project server wallets", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"use server";
|
||||
|
||||
import { configure, sendTokens } from "@thirdweb-dev/api";
|
||||
import { THIRDWEB_API_HOST } from "@/constants/urls";
|
||||
|
||||
configure({
|
||||
override: {
|
||||
baseUrl: THIRDWEB_API_HOST,
|
||||
},
|
||||
});
|
||||
|
||||
export async function sendProjectWalletTokens(options: {
|
||||
walletAddress: string;
|
||||
recipientAddress: string;
|
||||
chainId: number;
|
||||
quantityWei: string;
|
||||
publishableKey: string;
|
||||
teamId: string;
|
||||
tokenAddress?: string;
|
||||
secretKey: string;
|
||||
vaultAccessToken?: string;
|
||||
}) {
|
||||
const {
|
||||
walletAddress,
|
||||
recipientAddress,
|
||||
chainId,
|
||||
quantityWei,
|
||||
publishableKey,
|
||||
teamId,
|
||||
tokenAddress,
|
||||
secretKey,
|
||||
vaultAccessToken,
|
||||
} = options;
|
||||
|
||||
if (!secretKey) {
|
||||
return {
|
||||
error: "A project secret key is required to send funds.",
|
||||
ok: false,
|
||||
} as const;
|
||||
}
|
||||
|
||||
const response = await sendTokens({
|
||||
body: {
|
||||
chainId,
|
||||
from: walletAddress,
|
||||
recipients: [
|
||||
{
|
||||
address: recipientAddress,
|
||||
quantity: quantityWei,
|
||||
},
|
||||
],
|
||||
...(tokenAddress ? { tokenAddress } : {}),
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": publishableKey,
|
||||
"x-secret-key": secretKey,
|
||||
"x-team-id": teamId,
|
||||
...(vaultAccessToken ? { "x-vault-access-token": vaultAccessToken } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (response.error || !response.data) {
|
||||
return {
|
||||
error: response.error || "Failed to submit transfer request.",
|
||||
ok: false,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
transactionIds: response.data.result?.transactionIds ?? [],
|
||||
} as const;
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export function reportContractDeployed(properties: {
|
||||
publisher: string | undefined;
|
||||
contractName: string | undefined;
|
||||
deploymentType?: "asset";
|
||||
is_testnet: boolean | undefined;
|
||||
}) {
|
||||
posthog.capture("contract deployed", properties);
|
||||
}
|
||||
@@ -40,6 +41,7 @@ export function reportContractDeployed(properties: {
|
||||
export function reportContractDeployFailed(properties: {
|
||||
errorMessage: string;
|
||||
chainId: number;
|
||||
is_testnet: boolean | undefined;
|
||||
publisher: string | undefined;
|
||||
contractName: string | undefined;
|
||||
}) {
|
||||
@@ -171,9 +173,7 @@ export function reportOnboardingCompleted() {
|
||||
*
|
||||
*/
|
||||
export function reportFaucetUsed(properties: { chainId: number }) {
|
||||
posthog.capture("faucet used", {
|
||||
chainId: properties.chainId,
|
||||
});
|
||||
posthog.capture("faucet used", properties);
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
@@ -197,12 +197,7 @@ export function reportChainConfigurationAdded(properties: {
|
||||
decimals: number;
|
||||
};
|
||||
}) {
|
||||
posthog.capture("chain configuration added", {
|
||||
chainId: properties.chainId,
|
||||
chainName: properties.chainName,
|
||||
nativeCurrency: properties.nativeCurrency,
|
||||
rpcURLs: properties.rpcURLs,
|
||||
});
|
||||
posthog.capture("chain configuration added", properties);
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
@@ -227,12 +222,9 @@ export function reportAssetBuySuccessful(properties: {
|
||||
chainId: number;
|
||||
contractType: AssetContractType | undefined;
|
||||
assetType: "nft" | "coin";
|
||||
is_testnet: boolean | undefined;
|
||||
}) {
|
||||
posthog.capture("asset buy successful", {
|
||||
assetType: properties.assetType,
|
||||
chainId: properties.chainId,
|
||||
contractType: properties.contractType,
|
||||
});
|
||||
posthog.capture("asset buy successful", properties);
|
||||
}
|
||||
|
||||
type TokenSwapParams = {
|
||||
@@ -349,16 +341,12 @@ export function reportTokenSwapCancelled(properties: TokenSwapParams) {
|
||||
*/
|
||||
export function reportAssetBuyFailed(properties: {
|
||||
chainId: number;
|
||||
is_testnet: boolean | undefined;
|
||||
contractType: AssetContractType | undefined;
|
||||
assetType: "nft" | "coin";
|
||||
error: string;
|
||||
}) {
|
||||
posthog.capture("asset buy failed", {
|
||||
assetType: properties.assetType,
|
||||
chainId: properties.chainId,
|
||||
contractType: properties.contractType,
|
||||
error: properties.error,
|
||||
});
|
||||
posthog.capture("asset buy failed", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -371,14 +359,11 @@ export function reportAssetBuyFailed(properties: {
|
||||
*/
|
||||
export function reportAssetBuyCancelled(properties: {
|
||||
chainId: number;
|
||||
is_testnet: boolean | undefined;
|
||||
contractType: AssetContractType | undefined;
|
||||
assetType: "nft" | "coin";
|
||||
}) {
|
||||
posthog.capture("asset buy cancelled", {
|
||||
assetType: properties.assetType,
|
||||
chainId: properties.chainId,
|
||||
contractType: properties.contractType,
|
||||
});
|
||||
posthog.capture("asset buy cancelled", properties);
|
||||
}
|
||||
|
||||
// Assets Landing Page ----------------------------
|
||||
@@ -423,10 +408,7 @@ export function reportAssetCreationStepConfigured(
|
||||
step: "coin-info" | "token-distribution" | "launch-coin";
|
||||
},
|
||||
) {
|
||||
posthog.capture("asset creation step configured", {
|
||||
assetType: properties.assetType,
|
||||
step: properties.step,
|
||||
});
|
||||
posthog.capture("asset creation step configured", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -440,11 +422,10 @@ export function reportAssetCreationStepConfigured(
|
||||
export function reportAssetCreationSuccessful(properties: {
|
||||
assetType: "nft" | "coin";
|
||||
contractType: AssetContractType;
|
||||
chainId: number;
|
||||
is_testnet: boolean | undefined;
|
||||
}) {
|
||||
posthog.capture("asset creation successful", {
|
||||
assetType: properties.assetType,
|
||||
contractType: properties.contractType,
|
||||
});
|
||||
posthog.capture("asset creation successful", properties);
|
||||
}
|
||||
|
||||
type CoinCreationStep =
|
||||
@@ -466,7 +447,12 @@ type CoinCreationStep =
|
||||
* @MananTank
|
||||
*/
|
||||
export function reportAssetCreationFailed(
|
||||
properties: { contractType: AssetContractType; error: string } & (
|
||||
properties: {
|
||||
contractType: AssetContractType;
|
||||
error: string;
|
||||
is_testnet: boolean | undefined;
|
||||
chainId: number;
|
||||
} & (
|
||||
| {
|
||||
assetType: "nft";
|
||||
step:
|
||||
@@ -481,12 +467,7 @@ export function reportAssetCreationFailed(
|
||||
}
|
||||
),
|
||||
) {
|
||||
posthog.capture("asset creation failed", {
|
||||
assetType: properties.assetType,
|
||||
contractType: properties.contractType,
|
||||
error: properties.error,
|
||||
step: properties.step,
|
||||
});
|
||||
posthog.capture("asset creation failed", properties);
|
||||
}
|
||||
|
||||
type UpsellParams = {
|
||||
@@ -582,6 +563,7 @@ export function reportPaymentLinkBuyFailed(properties: {
|
||||
export function reportAssetPageview(properties: {
|
||||
assetType: "nft" | "coin";
|
||||
chainId: number;
|
||||
is_testnet: boolean | undefined;
|
||||
}) {
|
||||
posthog.capture("asset pageview", properties);
|
||||
}
|
||||
@@ -593,7 +575,10 @@ export function reportAssetPageview(properties: {
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*/
|
||||
export function reportChainPageview(properties: { chainId: number }) {
|
||||
export function reportChainPageview(properties: {
|
||||
chainId: number;
|
||||
is_testnet: boolean | undefined;
|
||||
}) {
|
||||
posthog.capture("chain pageview", properties);
|
||||
}
|
||||
|
||||
@@ -686,10 +671,7 @@ export function reportProductFeedback(properties: {
|
||||
feedback: string;
|
||||
source: "desktop" | "mobile";
|
||||
}) {
|
||||
posthog.capture("product feedback submitted", {
|
||||
feedback: properties.feedback,
|
||||
source: properties.source,
|
||||
});
|
||||
posthog.capture("product feedback submitted", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -448,6 +448,7 @@ type ActiveStatus = {
|
||||
pay: boolean;
|
||||
inAppWallet: boolean;
|
||||
ecosystemWallet: boolean;
|
||||
engineCloud: boolean;
|
||||
};
|
||||
|
||||
export const isProjectActive = unstable_cache(
|
||||
@@ -479,6 +480,7 @@ export const isProjectActive = unstable_cache(
|
||||
pay: false,
|
||||
rpc: false,
|
||||
sdk: false,
|
||||
engineCloud: false,
|
||||
storage: false,
|
||||
} as ActiveStatus;
|
||||
}
|
||||
@@ -488,7 +490,7 @@ export const isProjectActive = unstable_cache(
|
||||
},
|
||||
["isProjectActive"],
|
||||
{
|
||||
revalidate: 60 * 60, // 1 hour
|
||||
revalidate: 30, // 30 seconds
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ export function BuyAndSwapEmbed(props: {
|
||||
tokenAddress: string | undefined;
|
||||
buyAmount: string | undefined;
|
||||
pageType: PageType;
|
||||
isTestnet: boolean | undefined;
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
const [tab, setTab] = useState<"buy" | "swap">("swap");
|
||||
@@ -116,6 +117,7 @@ export function BuyAndSwapEmbed(props: {
|
||||
chainId: props.chain.id,
|
||||
error: errorMessage,
|
||||
contractType: undefined,
|
||||
is_testnet: props.isTestnet,
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -141,6 +143,7 @@ export function BuyAndSwapEmbed(props: {
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
contractType: undefined,
|
||||
is_testnet: props.isTestnet,
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -166,6 +169,7 @@ export function BuyAndSwapEmbed(props: {
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
contractType: undefined,
|
||||
is_testnet: props.isTestnet,
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
|
||||
// biome-ignore lint/correctness/noUnusedVariables: banner is toggled on-demand via API content changes
|
||||
function AnnouncementBannerUI(props: {
|
||||
href: string;
|
||||
label: string;
|
||||
|
||||
@@ -64,7 +64,7 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
|
||||
<CreateProjectDialogUI
|
||||
createProject={async (params) => {
|
||||
const res = await createProjectClient(props.teamId, params);
|
||||
await createVaultAccountAndAccessToken({
|
||||
const vaultTokens = await createVaultAccountAndAccessToken({
|
||||
project: res.project,
|
||||
projectSecretKey: res.secret,
|
||||
}).catch((error) => {
|
||||
@@ -74,6 +74,13 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
|
||||
);
|
||||
throw error;
|
||||
});
|
||||
|
||||
const managementAccessToken = vaultTokens.managementToken?.accessToken;
|
||||
|
||||
if (!managementAccessToken) {
|
||||
throw new Error("Missing management access token for project wallet");
|
||||
}
|
||||
|
||||
return {
|
||||
project: res.project,
|
||||
secret: res.secret,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
const PROJECT_WALLET_LABEL_SUFFIX = " Wallet";
|
||||
const PROJECT_WALLET_LABEL_MAX_LENGTH = 32;
|
||||
|
||||
/**
|
||||
* Builds the default label for a project's primary server wallet.
|
||||
* Ensures a stable naming convention so the wallet can be identified across flows.
|
||||
*/
|
||||
export function getProjectWalletLabel(projectName: string | undefined) {
|
||||
const baseName = projectName?.trim() || "Project";
|
||||
const maxBaseLength = Math.max(
|
||||
1,
|
||||
PROJECT_WALLET_LABEL_MAX_LENGTH - PROJECT_WALLET_LABEL_SUFFIX.length,
|
||||
);
|
||||
|
||||
const normalizedBase =
|
||||
baseName.length > maxBaseLength
|
||||
? baseName.slice(0, maxBaseLength).trimEnd()
|
||||
: baseName;
|
||||
|
||||
return `${normalizedBase}${PROJECT_WALLET_LABEL_SUFFIX}`;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import "server-only";
|
||||
|
||||
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
|
||||
import { getProjectWalletLabel } from "@/lib/project-wallet";
|
||||
|
||||
export type ProjectWalletSummary = {
|
||||
id: string;
|
||||
address: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
type VaultWalletListItem = {
|
||||
id: string;
|
||||
address: string;
|
||||
metadata?: {
|
||||
label?: string;
|
||||
projectId?: string;
|
||||
teamId?: string;
|
||||
type?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function getProjectWallet(
|
||||
project: Project,
|
||||
): Promise<ProjectWalletSummary | undefined> {
|
||||
const engineCloudService = project.services.find(
|
||||
(service) => service.name === "engineCloud",
|
||||
);
|
||||
|
||||
const managementAccessToken =
|
||||
engineCloudService?.managementAccessToken || undefined;
|
||||
const projectWalletAddress = (
|
||||
engineCloudService as { projectWalletAddress?: string } | undefined
|
||||
)?.projectWalletAddress;
|
||||
|
||||
if (
|
||||
!managementAccessToken ||
|
||||
!NEXT_PUBLIC_THIRDWEB_VAULT_URL ||
|
||||
!projectWalletAddress
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const vaultClient = await createVaultClient({
|
||||
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
|
||||
});
|
||||
|
||||
const response = await listEoas({
|
||||
client: vaultClient,
|
||||
request: {
|
||||
auth: {
|
||||
accessToken: managementAccessToken,
|
||||
},
|
||||
options: {
|
||||
page: 0,
|
||||
// @ts-expect-error - SDK expects snake_case for pagination arguments
|
||||
page_size: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const items = response.data.items as VaultWalletListItem[] | undefined;
|
||||
|
||||
if (!items?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const defaultLabel = getProjectWalletLabel(project.name);
|
||||
|
||||
const serverWallets = items.filter(
|
||||
(item) => item.metadata?.projectId === project.id,
|
||||
);
|
||||
|
||||
const defaultWallet = serverWallets.find(
|
||||
(item) =>
|
||||
item.address.toLowerCase() === projectWalletAddress.toLowerCase(),
|
||||
);
|
||||
|
||||
if (!defaultWallet) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
id: defaultWallet.id,
|
||||
address: defaultWallet.address,
|
||||
label: defaultWallet.metadata?.label ?? defaultLabel,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to load project wallet", error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
+1
@@ -8,6 +8,7 @@ export function BuyFundsSection(props: { chain: ChainMetadata }) {
|
||||
return (
|
||||
<GridPatternEmbedContainer>
|
||||
<BuyAndSwapEmbed
|
||||
isTestnet={props.chain.testnet}
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain={defineDashboardChain(props.chain.chainId, props.chain)}
|
||||
buyAmount={undefined}
|
||||
|
||||
+2
-1
@@ -2,10 +2,11 @@
|
||||
import { reportChainPageview } from "@/analytics/report";
|
||||
import { useEffectOnce } from "@/hooks/useEffectOnce";
|
||||
|
||||
export function ChainPageView(props: { chainId: number }) {
|
||||
export function ChainPageView(props: { chainId: number; is_testnet: boolean }) {
|
||||
useEffectOnce(() => {
|
||||
reportChainPageview({
|
||||
chainId: props.chainId,
|
||||
is_testnet: props.is_testnet,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ export default async function ChainPageLayout(props: {
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
<ChainPageView chainId={chain.chainId} />
|
||||
<ChainPageView chainId={chain.chainId} is_testnet={chain.testnet} />
|
||||
<div className="border-border border-b bg-card">
|
||||
<TeamHeader />
|
||||
</div>
|
||||
|
||||
+2
@@ -5,11 +5,13 @@ import { useEffectOnce } from "@/hooks/useEffectOnce";
|
||||
export function AssetPageView(props: {
|
||||
assetType: "nft" | "coin";
|
||||
chainId: number;
|
||||
is_testnet: boolean;
|
||||
}) {
|
||||
useEffectOnce(() => {
|
||||
reportAssetPageview({
|
||||
assetType: props.assetType,
|
||||
chainId: props.chainId,
|
||||
is_testnet: props.is_testnet,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+3
@@ -122,6 +122,7 @@ export function TokenDropClaim(props: {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC20",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
toast.error("Failed to approve spending", {
|
||||
@@ -160,6 +161,7 @@ export function TokenDropClaim(props: {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC20",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
toast.error("Failed to buy tokens", {
|
||||
@@ -172,6 +174,7 @@ export function TokenDropClaim(props: {
|
||||
assetType: "coin",
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC20",
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
setStepsUI({
|
||||
|
||||
+6
-1
@@ -76,7 +76,11 @@ export async function ERC20PublicPage(props: {
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
<AssetPageView assetType="coin" chainId={props.chainMetadata.chainId} />
|
||||
<AssetPageView
|
||||
assetType="coin"
|
||||
chainId={props.chainMetadata.chainId}
|
||||
is_testnet={props.chainMetadata.testnet}
|
||||
/>
|
||||
<PageHeader containerClassName="max-w-7xl" />
|
||||
|
||||
<div className="border-b border-dashed">
|
||||
@@ -199,6 +203,7 @@ function BuyEmbed(props: {
|
||||
tokenAddress={props.clientContract.address}
|
||||
buyAmount={undefined}
|
||||
pageType="asset"
|
||||
isTestnet={props.chainMetadata.testnet}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+5
-1
@@ -18,7 +18,11 @@ export function NFTPublicPageLayout(props: {
|
||||
}) {
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
<AssetPageView assetType="nft" chainId={props.chainMetadata.chainId} />
|
||||
<AssetPageView
|
||||
assetType="nft"
|
||||
chainId={props.chainMetadata.chainId}
|
||||
is_testnet={props.chainMetadata.testnet}
|
||||
/>
|
||||
<PageHeader containerClassName="max-w-8xl" />
|
||||
<div className="border-b">
|
||||
<div className="container max-w-8xl">
|
||||
|
||||
+4
@@ -120,6 +120,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC1155",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
console.error(errorMessage);
|
||||
@@ -147,6 +148,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
|
||||
assetType: "nft",
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC1155",
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
props.onSuccess?.();
|
||||
@@ -163,6 +165,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC1155",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -180,6 +183,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC1155",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+4
@@ -77,6 +77,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC721",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -103,6 +104,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
|
||||
assetType: "nft",
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC721",
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
props.onSuccess?.();
|
||||
@@ -115,6 +117,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC721",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -131,6 +134,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC721",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
+17
-5
@@ -190,25 +190,37 @@ export async function generateContractLayoutMetadata(params: {
|
||||
.replace("Testnet", "")
|
||||
.trim();
|
||||
|
||||
const title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
|
||||
let title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
|
||||
let description = "";
|
||||
|
||||
if (isERC721 || isERC1155) {
|
||||
description = `View tokens, source code, transactions, balances, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}.`;
|
||||
} else if (isERC20) {
|
||||
description = `View ERC20 tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
|
||||
title = `${contractMetadata.name} (${contractMetadata.symbol}) on ${cleanedChainName} | Buy, Swap, Bridge & Price`;
|
||||
description = `Buy, swap & bridge ${contractMetadata.name} (${contractMetadata.symbol}) on ${cleanedChainName} with thirdweb Bridge. View contract address, holders, analytics, transactions and live price.`;
|
||||
} else {
|
||||
description = `View tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
|
||||
description = `View tokens, transactions, balances, source code, and analytics for the ${contractMetadata.name} smart contract on ${cleanedChainName}`;
|
||||
}
|
||||
|
||||
return {
|
||||
description: description,
|
||||
title: title,
|
||||
openGraph: {
|
||||
description: description,
|
||||
title: title,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
const fallbackTitle = `${shortenIfAddress(params.contractAddress)} | ${params.chainIdOrSlug}`;
|
||||
const fallbackDescription = `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chainIdOrSlug}`;
|
||||
|
||||
return {
|
||||
description: `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chainIdOrSlug}`,
|
||||
title: `${shortenIfAddress(params.contractAddress)} | ${params.chainIdOrSlug}`,
|
||||
description: fallbackDescription,
|
||||
title: fallbackTitle,
|
||||
openGraph: {
|
||||
description: fallbackDescription,
|
||||
title: fallbackTitle,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -50,6 +50,7 @@ import {
|
||||
} from "@/constants/addresses";
|
||||
import { LAST_USED_PROJECT_ID, LAST_USED_TEAM_ID } from "@/constants/cookie";
|
||||
import { ZERO_FEE_CHAINS } from "@/constants/fee-config";
|
||||
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
import {
|
||||
useCustomFactoryAbi,
|
||||
useFunctionParamsFromABI,
|
||||
@@ -206,6 +207,8 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
|
||||
const activeAccount = useActiveAccount();
|
||||
const walletChain = useActiveWalletChain();
|
||||
const walletChainMetadata = useV5DashboardChain(walletChain?.id);
|
||||
|
||||
const { onError } = useTxNotifications(
|
||||
"Successfully deployed contract",
|
||||
"Failed to deploy contract",
|
||||
@@ -656,6 +659,7 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
chainId: walletChain.id,
|
||||
contractName: metadata.name,
|
||||
publisher: rewriteTwPublisher(metadata.publisher),
|
||||
is_testnet: walletChainMetadata?.testnet,
|
||||
});
|
||||
|
||||
deployStatusModal.nextStep();
|
||||
@@ -694,6 +698,7 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
contractName: metadata.name,
|
||||
errorMessage: parsedError,
|
||||
publisher: rewriteTwPublisher(metadata.publisher),
|
||||
is_testnet: walletChain.testnet,
|
||||
});
|
||||
|
||||
deployStatusModal.close();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BringToFrontIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import Link from "next/link";
|
||||
import { Bridge } from "thirdweb";
|
||||
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { PageHeader } from "./components/header";
|
||||
@@ -35,13 +34,10 @@ export default async function Page() {
|
||||
<h1 className="text-4xl font-bold tracking-tighter mb-1">
|
||||
Discover and swap any tokens on any chain, instantly
|
||||
</h1>
|
||||
<Link
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
href="https://thirdweb.com/monetize/bridge"
|
||||
target="_blank"
|
||||
>
|
||||
Powered by thirdweb bridge
|
||||
</Link>
|
||||
|
||||
<p className="text-muted-foreground">
|
||||
85+ chains, 3200+ tokens and 9+ million routes supported
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<TokenPage chains={chains} />
|
||||
|
||||
+11
-1
@@ -55,7 +55,7 @@ export function CreateProjectFormOnboarding(props: {
|
||||
<CreateProjectForm
|
||||
createProject={async (params) => {
|
||||
const res = await createProjectClient(props.teamId, params);
|
||||
await createVaultAccountAndAccessToken({
|
||||
const vaultTokens = await createVaultAccountAndAccessToken({
|
||||
project: res.project,
|
||||
projectSecretKey: res.secret,
|
||||
}).catch((error) => {
|
||||
@@ -65,6 +65,16 @@ export function CreateProjectFormOnboarding(props: {
|
||||
);
|
||||
throw error;
|
||||
});
|
||||
|
||||
const managementAccessToken =
|
||||
vaultTokens.managementToken?.accessToken;
|
||||
|
||||
if (!managementAccessToken) {
|
||||
throw new Error(
|
||||
"Missing management access token for project wallet",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
project: res.project,
|
||||
secret: res.secret,
|
||||
|
||||
+3
-3
@@ -28,11 +28,11 @@ export function QuickStartSection(props: {
|
||||
icon={ArrowRightLeftIcon}
|
||||
setupTime={5}
|
||||
id="swap_tokens"
|
||||
features={["Swap any token", "Cross-chain swaps"]}
|
||||
features={["Swap any token", "Cross-chain swap"]}
|
||||
description="Swap tokens cross-chain with dedicated swapping endpoints."
|
||||
link={{
|
||||
href: `https://portal.thirdweb.com/payments/swap`,
|
||||
label: "Setup Swaps",
|
||||
href: `https://portal.thirdweb.com/bridge/swap`,
|
||||
label: "Setup Swap",
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
+8
-58
@@ -69,70 +69,20 @@ export default async function Page(props: {
|
||||
},
|
||||
},
|
||||
links: [
|
||||
// TODO - add docs when bridge docs are added in portal
|
||||
// {
|
||||
// type: "docs",
|
||||
// href: "https://portal.thirdweb.com/payments",
|
||||
// },
|
||||
{
|
||||
type: "docs",
|
||||
href: "https://portal.thirdweb.com/bridge",
|
||||
},
|
||||
{
|
||||
type: "playground",
|
||||
href: "https://playground.thirdweb.com/bridge/swap-widget",
|
||||
},
|
||||
{
|
||||
type: "api",
|
||||
href: "https://api.thirdweb.com/reference#tag/bridge",
|
||||
},
|
||||
],
|
||||
}}
|
||||
footer={{
|
||||
center: {
|
||||
links: [
|
||||
{
|
||||
href: "https://playground.thirdweb.com/payments/ui-components",
|
||||
label: "UI Component",
|
||||
},
|
||||
{
|
||||
href: "https://playground.thirdweb.com/connect/payments/fund-wallet",
|
||||
label: "Buy Crypto",
|
||||
},
|
||||
{
|
||||
href: "https://playground.thirdweb.com/connect/payments/commerce",
|
||||
label: "Checkout",
|
||||
},
|
||||
{
|
||||
href: "https://playground.thirdweb.com/connect/payments/transactions",
|
||||
label: "Transactions",
|
||||
},
|
||||
],
|
||||
title: "Demos",
|
||||
},
|
||||
left: {
|
||||
links: [
|
||||
{
|
||||
href: "https://portal.thirdweb.com/payments",
|
||||
label: "Overview",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/typescript/v5/convertCryptoToFiat",
|
||||
label: "TypeScript",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/react/v5/pay/fund-wallets",
|
||||
label: "React",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/dotnet/universal-bridge/quickstart",
|
||||
label: ".NET",
|
||||
},
|
||||
],
|
||||
title: "Documentation",
|
||||
},
|
||||
right: {
|
||||
links: [
|
||||
{
|
||||
href: "https://www.youtube.com/watch?v=aBu175-VsNY",
|
||||
label: "Implement cross-chain payments in any app",
|
||||
},
|
||||
],
|
||||
title: "Tutorials",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-12">
|
||||
<ResponsiveSearchParamsProvider value={searchParams}>
|
||||
|
||||
+1
@@ -31,5 +31,6 @@ export const Default: Story = {
|
||||
],
|
||||
},
|
||||
teamSlug: "bar",
|
||||
managementAccessToken: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
+93
-1
@@ -7,6 +7,7 @@ import {
|
||||
import Link from "next/link";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { AlertDialogFooter } from "@/components/ui/alert-dialog";
|
||||
import { CodeServer } from "@/components/ui/code/code.server";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { DotNetIcon } from "@/icons/brand-icons/DotNetIcon";
|
||||
@@ -18,13 +19,34 @@ import { UnrealIcon } from "@/icons/brand-icons/UnrealIcon";
|
||||
import { ContractIcon } from "@/icons/ContractIcon";
|
||||
import { InsightIcon } from "@/icons/InsightIcon";
|
||||
import { PayIcon } from "@/icons/PayIcon";
|
||||
import { WalletProductIcon } from "@/icons/WalletProductIcon";
|
||||
import { getProjectWalletLabel } from "@/lib/project-wallet";
|
||||
import {
|
||||
getProjectWallet,
|
||||
type ProjectWalletSummary,
|
||||
} from "@/lib/server/project-wallet";
|
||||
import CreateServerWallet from "../../transactions/server-wallets/components/create-server-wallet.client";
|
||||
import { ClientIDSection } from "./ClientIDSection";
|
||||
import { IntegrateAPIKeyCodeTabs } from "./IntegrateAPIKeyCodeTabs";
|
||||
import { ProjectWalletControls } from "./ProjectWalletControls.client";
|
||||
import { SecretKeySection } from "./SecretKeySection";
|
||||
|
||||
export function ProjectFTUX(props: { project: Project; teamSlug: string }) {
|
||||
export async function ProjectFTUX(props: {
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
wallet?: ProjectWalletSummary | undefined;
|
||||
managementAccessToken: string | undefined;
|
||||
}) {
|
||||
const projectWallet = props.wallet ?? (await getProjectWallet(props.project));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
<ProjectWalletSection
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
wallet={projectWallet}
|
||||
managementAccessToken={props.managementAccessToken}
|
||||
/>
|
||||
<IntegrateAPIKeySection
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
@@ -39,6 +61,76 @@ export function ProjectFTUX(props: { project: Project; teamSlug: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ProjectWalletSection(props: {
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
wallet: ProjectWalletSummary | undefined;
|
||||
managementAccessToken: string | undefined;
|
||||
}) {
|
||||
const defaultLabel = getProjectWalletLabel(props.project.name);
|
||||
const walletAddress = props.wallet?.address;
|
||||
const label = props.wallet?.label ?? defaultLabel;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="rounded-lg border border-border bg-card p-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-full border border-border bg-background p-2">
|
||||
<WalletProductIcon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-lg tracking-tight">
|
||||
Project Wallet
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Default managed server wallet for this project. Use it for
|
||||
deployments, payments, and API integrations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{walletAddress ? (
|
||||
<>
|
||||
<ProjectWalletControls
|
||||
label={label}
|
||||
project={props.project}
|
||||
walletAddress={walletAddress}
|
||||
/>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end">
|
||||
<Link
|
||||
className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline"
|
||||
href={`/team/${props.teamSlug}/${props.project.slug}/transactions`}
|
||||
>
|
||||
View Transactions
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Alert variant="info">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>No default project wallet set</AlertTitle>
|
||||
<AlertDescription>
|
||||
Set a default project wallet to use for dashboard and API
|
||||
integrations.
|
||||
</AlertDescription>
|
||||
<AlertDialogFooter className="flex justify-start sm:justify-start pt-4">
|
||||
<CreateServerWallet
|
||||
managementAccessToken={props.managementAccessToken}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
setAsProjectWallet={true}
|
||||
/>
|
||||
</AlertDialogFooter>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Integrate API key section ------------------------------------------------------------
|
||||
|
||||
function IntegrateAPIKeySection({
|
||||
|
||||
+725
@@ -0,0 +1,725 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
EllipsisVerticalIcon,
|
||||
RefreshCcwIcon,
|
||||
SendIcon,
|
||||
ShuffleIcon,
|
||||
WalletIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { toWei } from "thirdweb";
|
||||
import { useWalletBalance } from "thirdweb/react";
|
||||
import { isAddress } from "thirdweb/utils";
|
||||
import { z } from "zod";
|
||||
import { listProjectServerWallets } from "@/actions/project-wallet/list-server-wallets";
|
||||
import { sendProjectWalletTokens } from "@/actions/project-wallet/send-tokens";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { FundWalletModal } from "@/components/blocks/fund-wallets-modal";
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";
|
||||
|
||||
type ProjectWalletControlsProps = {
|
||||
walletAddress: string;
|
||||
label: string;
|
||||
project: Pick<
|
||||
Project,
|
||||
"id" | "publishableKey" | "teamId" | "services" | "name"
|
||||
>;
|
||||
defaultChainId?: number;
|
||||
};
|
||||
|
||||
export function ProjectWalletControls(props: ProjectWalletControlsProps) {
|
||||
const { walletAddress, label, project, defaultChainId } = props;
|
||||
const [isSendOpen, setIsSendOpen] = useState(false);
|
||||
const [isReceiveOpen, setIsReceiveOpen] = useState(false);
|
||||
const [selectedChainId, setSelectedChainId] = useState(defaultChainId ?? 1);
|
||||
const [isChangeWalletOpen, setIsChangeWalletOpen] = useState(false);
|
||||
const [selectedWalletId, setSelectedWalletId] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const client = useMemo(() => getClientThirdwebClient(), []);
|
||||
const chain = useV5DashboardChain(selectedChainId);
|
||||
const queryClient = useQueryClient();
|
||||
const router = useDashboardRouter();
|
||||
|
||||
const engineCloudService = useMemo(
|
||||
() => project.services?.find((service) => service.name === "engineCloud"),
|
||||
[project.services],
|
||||
);
|
||||
const managementAccessToken =
|
||||
engineCloudService?.managementAccessToken ?? undefined;
|
||||
const isManagedVault = !!engineCloudService?.encryptedAdminKey;
|
||||
|
||||
const serverWalletsQuery = useQuery({
|
||||
enabled: !!managementAccessToken,
|
||||
queryFn: async () => {
|
||||
if (!managementAccessToken) {
|
||||
return [] as ProjectWalletSummary[];
|
||||
}
|
||||
|
||||
return listProjectServerWallets({
|
||||
managementAccessToken,
|
||||
projectId: project.id,
|
||||
});
|
||||
},
|
||||
queryKey: ["project", project.id, "server-wallets", managementAccessToken],
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
const serverWallets = serverWalletsQuery.data ?? [];
|
||||
const currentWalletAddressLower = walletAddress.toLowerCase();
|
||||
const otherWallets = useMemo(() => {
|
||||
return serverWallets.filter((wallet) => {
|
||||
return wallet.address.toLowerCase() !== currentWalletAddressLower;
|
||||
});
|
||||
}, [serverWallets, currentWalletAddressLower]);
|
||||
|
||||
const canChangeWallet = otherWallets.length > 0;
|
||||
|
||||
const handleOpenChangeWallet = () => {
|
||||
const currentWallet = serverWallets.find(
|
||||
(wallet) => wallet.address.toLowerCase() === currentWalletAddressLower,
|
||||
);
|
||||
|
||||
const nextWalletId =
|
||||
otherWallets[0]?.id ?? currentWallet?.id ?? serverWallets[0]?.id;
|
||||
|
||||
setSelectedWalletId(nextWalletId);
|
||||
setIsChangeWalletOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseChangeWallet = () => {
|
||||
setIsChangeWalletOpen(false);
|
||||
setSelectedWalletId(undefined);
|
||||
};
|
||||
|
||||
const selectedWallet = useMemo(() => {
|
||||
if (!selectedWalletId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return serverWallets.find((wallet) => wallet.id === selectedWalletId);
|
||||
}, [selectedWalletId, serverWallets]);
|
||||
|
||||
const isSelectionDifferent = Boolean(
|
||||
selectedWallet &&
|
||||
selectedWallet.address.toLowerCase() !== currentWalletAddressLower,
|
||||
);
|
||||
|
||||
const changeWalletMutation = useMutation({
|
||||
mutationFn: async (wallet: ProjectWalletSummary) => {
|
||||
await updateDefaultProjectWallet({
|
||||
project: project as Project,
|
||||
projectWalletAddress: wallet.address,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to update project wallet";
|
||||
toast.error(message);
|
||||
},
|
||||
onSuccess: async (_, wallet) => {
|
||||
const descriptionLabel = wallet.label ?? wallet.address;
|
||||
toast.success("Default project wallet updated", {
|
||||
description: `Now pointing to ${descriptionLabel}`,
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [
|
||||
"project",
|
||||
project.id,
|
||||
"server-wallets",
|
||||
managementAccessToken,
|
||||
],
|
||||
});
|
||||
handleCloseChangeWallet();
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
const balanceQuery = useWalletBalance({
|
||||
address: walletAddress,
|
||||
chain,
|
||||
client,
|
||||
});
|
||||
|
||||
const balanceDisplay = balanceQuery.data
|
||||
? `${balanceQuery.data.displayValue} ${balanceQuery.data.symbol}`
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="rounded-lg border border-border bg-background p-3">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3 lg:flex-row">
|
||||
<div className="flex-1">
|
||||
<p className="text-muted-foreground text-xs">Address</p>
|
||||
<WalletAddress address={walletAddress} client={client} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-muted-foreground text-xs">Label</p>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="font-mono text-sm break-all py-2 sm:py-0">
|
||||
{label}
|
||||
</p>
|
||||
{canChangeWallet && (
|
||||
<Button
|
||||
className="h-8 gap-1 sm:ml-4"
|
||||
onClick={handleOpenChangeWallet}
|
||||
size="sm"
|
||||
disabled={serverWalletsQuery.isLoading}
|
||||
variant="outline"
|
||||
>
|
||||
<ShuffleIcon className="size-3.5" />
|
||||
Change default
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex-1 flex-col">
|
||||
<p className="text-xs text-muted-foreground pb-2">Balance</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<WalletIcon className="size-4 text-muted-foreground" />
|
||||
{balanceQuery.isLoading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : balanceDisplay ? (
|
||||
<span className="text-md tracking-tight">
|
||||
{balanceDisplay}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">N/A</span>
|
||||
)}
|
||||
<Button
|
||||
aria-label="Refresh balance"
|
||||
className="h-8 w-8 p-0"
|
||||
disabled={balanceQuery.isFetching}
|
||||
onClick={() => balanceQuery.refetch()}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
>
|
||||
<RefreshCcwIcon
|
||||
className={cn(
|
||||
"size-4",
|
||||
balanceQuery.isFetching && "animate-spin",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-label="Open wallet actions"
|
||||
className="h-9 w-9"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
>
|
||||
<EllipsisVerticalIcon className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48 rounded-xl">
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2"
|
||||
onSelect={() => setIsSendOpen(true)}
|
||||
>
|
||||
<SendIcon className="size-4 text-muted-foreground" />
|
||||
Send funds
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2"
|
||||
onSelect={() => setIsReceiveOpen(true)}
|
||||
>
|
||||
<WalletIcon className="size-4 text-muted-foreground" />
|
||||
Receive funds
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-col flex-1">
|
||||
<p className="text-xs text-muted-foreground pb-2">Network</p>
|
||||
<SingleNetworkSelector
|
||||
chainId={selectedChainId}
|
||||
className="max-w-xs rounded-lg"
|
||||
client={client}
|
||||
disableDeprecated
|
||||
disableChainId
|
||||
onChange={setSelectedChainId}
|
||||
placeholder="Select network"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SendProjectWalletModal
|
||||
chainId={selectedChainId}
|
||||
client={client}
|
||||
isManagedVault={isManagedVault}
|
||||
label={label}
|
||||
onClose={() => setIsSendOpen(false)}
|
||||
onSuccess={() => balanceQuery.refetch()}
|
||||
open={isSendOpen}
|
||||
publishableKey={project.publishableKey}
|
||||
teamId={project.teamId}
|
||||
walletAddress={walletAddress}
|
||||
/>
|
||||
|
||||
<FundWalletModal
|
||||
checkoutWidgetTitle={`Fund ${label}`}
|
||||
client={client}
|
||||
defaultChainId={selectedChainId}
|
||||
description="Use your card or crypto to deposit into this server wallet"
|
||||
onOpenChange={setIsReceiveOpen}
|
||||
open={isReceiveOpen}
|
||||
recipientAddress={walletAddress}
|
||||
title="Fund project wallet"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen) {
|
||||
handleCloseChangeWallet();
|
||||
}
|
||||
}}
|
||||
open={isChangeWalletOpen}
|
||||
>
|
||||
<DialogContent className="gap-0 p-0">
|
||||
<DialogHeader className="p-4 lg:p-6">
|
||||
<DialogTitle>Change default wallet</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose another server wallet to use as the default for this
|
||||
project.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-4 pb-8 lg:px-6">
|
||||
{serverWalletsQuery.isLoading ? (
|
||||
<div className="flex justify-center py-6">
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
) : serverWallets.length <= 1 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You need at least two server wallets to pick a different
|
||||
default.
|
||||
</p>
|
||||
) : (
|
||||
<RadioGroup
|
||||
className="space-y-3"
|
||||
onValueChange={(value) => setSelectedWalletId(value)}
|
||||
value={selectedWalletId}
|
||||
>
|
||||
{serverWallets.map((wallet) => {
|
||||
const isCurrent =
|
||||
wallet.address.toLowerCase() === currentWalletAddressLower;
|
||||
const isSelected = wallet.id === selectedWalletId;
|
||||
|
||||
return (
|
||||
<Label
|
||||
key={wallet.id}
|
||||
htmlFor={`project-wallet-${wallet.id}`}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center gap-3 rounded-lg border border-border bg-card px-4 py-3 transition",
|
||||
isSelected && "border-primary ring-2 ring-primary/20",
|
||||
)}
|
||||
>
|
||||
<RadioGroupItem
|
||||
id={`project-wallet-${wallet.id}`}
|
||||
value={wallet.id}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{wallet.label ?? "Unnamed server wallet"}
|
||||
</span>
|
||||
{isCurrent && (
|
||||
<Badge variant="success" className="text-xs">
|
||||
current
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground break-all">
|
||||
{wallet.address}
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
);
|
||||
})}
|
||||
</RadioGroup>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex justify-end gap-3 border-t bg-card p-4 lg:p-6">
|
||||
<Button
|
||||
onClick={handleCloseChangeWallet}
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={changeWalletMutation.isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
!selectedWallet ||
|
||||
!isSelectionDifferent ||
|
||||
changeWalletMutation.isPending
|
||||
}
|
||||
onClick={() => {
|
||||
if (selectedWallet) {
|
||||
changeWalletMutation.mutate(selectedWallet);
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{changeWalletMutation.isPending && (
|
||||
<Spinner className="mr-2 size-4" />
|
||||
)}
|
||||
Save default
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const createSendFormSchema = (secretKeyLabel: string) =>
|
||||
z.object({
|
||||
chainId: z.number({
|
||||
required_error: "Select a network",
|
||||
}),
|
||||
toAddress: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Destination address is required")
|
||||
.refine((value) => Boolean(isAddress(value)), {
|
||||
message: "Enter a valid wallet address",
|
||||
}),
|
||||
amount: z.string().trim().min(1, "Amount is required"),
|
||||
secretKey: z.string().trim().min(1, `${secretKeyLabel} is required`),
|
||||
vaultAccessToken: z.string().trim(),
|
||||
});
|
||||
|
||||
const SECRET_KEY_LABEL = "Project secret key";
|
||||
|
||||
type SendFormValues = z.infer<ReturnType<typeof createSendFormSchema>>;
|
||||
|
||||
function SendProjectWalletModal(props: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
walletAddress: string;
|
||||
publishableKey: string;
|
||||
teamId: string;
|
||||
chainId: number;
|
||||
label: string;
|
||||
client: ReturnType<typeof getClientThirdwebClient>;
|
||||
isManagedVault: boolean;
|
||||
}) {
|
||||
const {
|
||||
open,
|
||||
onClose,
|
||||
onSuccess,
|
||||
walletAddress,
|
||||
publishableKey,
|
||||
teamId,
|
||||
chainId,
|
||||
label,
|
||||
client,
|
||||
isManagedVault,
|
||||
} = props;
|
||||
|
||||
const secretKeyLabel = SECRET_KEY_LABEL;
|
||||
const secretKeyPlaceholder = "Enter your project secret key";
|
||||
const secretKeyHelper =
|
||||
"Your project secret key was generated when you created your project. If you lost it, regenerate one from Project settings.";
|
||||
const vaultAccessTokenHelper =
|
||||
"Vault access tokens are optional credentials with server wallet permissions. Manage them in Vault settings.";
|
||||
|
||||
const formSchema = useMemo(() => createSendFormSchema(SECRET_KEY_LABEL), []);
|
||||
|
||||
const form = useForm<SendFormValues>({
|
||||
defaultValues: {
|
||||
amount: "0",
|
||||
chainId,
|
||||
secretKey: "",
|
||||
vaultAccessToken: "",
|
||||
toAddress: "",
|
||||
},
|
||||
mode: "onChange",
|
||||
resolver: zodResolver(formSchema),
|
||||
});
|
||||
|
||||
const selectedChain = useV5DashboardChain(form.watch("chainId"));
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax -- form submission chainId must track selector state
|
||||
useEffect(() => {
|
||||
form.setValue("chainId", chainId);
|
||||
}, [chainId, form]);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax -- reset cached inputs when modal closes to avoid leaking state
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
const currentValues = form.getValues();
|
||||
form.reset({
|
||||
amount: "0",
|
||||
chainId,
|
||||
secretKey: currentValues.secretKey ?? "",
|
||||
vaultAccessToken: currentValues.vaultAccessToken ?? "",
|
||||
toAddress: "",
|
||||
});
|
||||
}
|
||||
}, [open, chainId, form]);
|
||||
|
||||
const sendMutation = useMutation({
|
||||
mutationFn: async (values: SendFormValues) => {
|
||||
const quantityWei = toWei(values.amount).toString();
|
||||
const secretKeyValue = values.secretKey.trim();
|
||||
const vaultAccessTokenValue = values.vaultAccessToken.trim();
|
||||
|
||||
const result = await sendProjectWalletTokens({
|
||||
chainId: values.chainId,
|
||||
publishableKey,
|
||||
quantityWei,
|
||||
recipientAddress: values.toAddress,
|
||||
teamId,
|
||||
walletAddress,
|
||||
secretKey: secretKeyValue,
|
||||
...(vaultAccessTokenValue
|
||||
? { vaultAccessToken: vaultAccessTokenValue }
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const errorMessage =
|
||||
typeof result.error === "string"
|
||||
? result.error
|
||||
: "Failed to send funds";
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return result.transactionIds;
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
onSuccess: (transactionIds) => {
|
||||
toast.success("Transfer submitted", {
|
||||
description:
|
||||
transactionIds && transactionIds.length > 0
|
||||
? `Transaction ID: ${transactionIds[0]}`
|
||||
: undefined,
|
||||
});
|
||||
onSuccess();
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
open={open}
|
||||
>
|
||||
<DialogContent className="gap-0 p-0">
|
||||
<DialogHeader className="p-4 lg:p-6">
|
||||
<DialogTitle>Send from {label}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Execute a one-off transfer using your server wallet.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit((values) => {
|
||||
void sendMutation.mutateAsync(values);
|
||||
})}
|
||||
>
|
||||
<div className="space-y-4 px-4 pb-8 lg:px-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="chainId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Network</FormLabel>
|
||||
<FormControl>
|
||||
<SingleNetworkSelector
|
||||
chainId={field.value}
|
||||
className="bg-card"
|
||||
client={client}
|
||||
disableDeprecated
|
||||
onChange={(nextChainId) => {
|
||||
field.onChange(nextChainId);
|
||||
}}
|
||||
placeholder="Select network"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="toAddress"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Send to</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
placeholder="0x..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="amount"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Amount</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
inputMode="decimal"
|
||||
min="0"
|
||||
step="any"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Sending native token
|
||||
{selectedChain?.nativeCurrency?.symbol
|
||||
? ` (${selectedChain.nativeCurrency.symbol})`
|
||||
: ""}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="secretKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{secretKeyLabel}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
placeholder={secretKeyPlaceholder}
|
||||
spellCheck={false}
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{secretKeyHelper}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!isManagedVault && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="vaultAccessToken"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Vault access token
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
(optional)
|
||||
</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
placeholder="Enter a vault access token (optional)"
|
||||
spellCheck={false}
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{vaultAccessTokenHelper}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 border-t bg-card p-4 lg:p-6">
|
||||
<Button onClick={onClose} type="button" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={!form.formState.isValid || sendMutation.isPending}
|
||||
type="submit"
|
||||
>
|
||||
{sendMutation.isPending && <Spinner className="size-4" />}
|
||||
Submit transfer
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import { ResponsiveTimeFilters } from "@/components/analytics/responsive-time-fi
|
||||
import { ProjectAvatar } from "@/components/blocks/avatar/project-avatar";
|
||||
import { ProjectPage } from "@/components/blocks/project-page/project-page";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getProjectWallet } from "@/lib/server/project-wallet";
|
||||
import { getFiltersFromSearchParams } from "@/lib/time";
|
||||
import type {
|
||||
InAppWalletStats,
|
||||
@@ -38,7 +39,10 @@ import type {
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { PieChartCard } from "../../../components/Analytics/PieChartCard";
|
||||
import { EngineCloudChartCardAsync } from "./components/EngineCloudChartCard";
|
||||
import { ProjectFTUX } from "./components/ProjectFTUX/ProjectFTUX";
|
||||
import {
|
||||
ProjectFTUX,
|
||||
ProjectWalletSection,
|
||||
} from "./components/ProjectFTUX/ProjectFTUX";
|
||||
import { RpcMethodBarChartCardAsync } from "./components/RpcMethodBarChartCard";
|
||||
import { TransactionsChartCardAsync } from "./components/Transactions";
|
||||
import { ProjectHighlightsCard } from "./overview/highlights-card";
|
||||
@@ -64,6 +68,9 @@ type PageProps = {
|
||||
searchParams: Promise<PageSearchParams>;
|
||||
};
|
||||
|
||||
// Revalidate this page data every 30 seconds
|
||||
export const revalidate = 30;
|
||||
|
||||
export default async function ProjectOverviewPage(props: PageProps) {
|
||||
const [params, searchParams] = await Promise.all([
|
||||
props.params,
|
||||
@@ -104,6 +111,11 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
teamId: project.teamId,
|
||||
});
|
||||
|
||||
const projectWallet = await getProjectWallet(project);
|
||||
const managementAccessToken =
|
||||
project.services?.find((service) => service.name === "engineCloud")
|
||||
?.managementAccessToken ?? undefined;
|
||||
|
||||
return (
|
||||
<ResponsiveSearchParamsProvider value={searchParams}>
|
||||
<ProjectPage
|
||||
@@ -125,6 +137,12 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
>
|
||||
{isActive ? (
|
||||
<div className="flex flex-col gap-4 md:gap-6">
|
||||
<ProjectWalletSection
|
||||
project={project}
|
||||
teamSlug={params.team_slug}
|
||||
wallet={projectWallet}
|
||||
managementAccessToken={managementAccessToken}
|
||||
/>
|
||||
<ResponsiveTimeFilters defaultRange={defaultRange} />
|
||||
<ProjectAnalytics
|
||||
authToken={authToken}
|
||||
@@ -137,7 +155,12 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ProjectFTUX project={project} teamSlug={params.team_slug} />
|
||||
<ProjectFTUX
|
||||
project={project}
|
||||
teamSlug={params.team_slug}
|
||||
wallet={projectWallet}
|
||||
managementAccessToken={managementAccessToken}
|
||||
/>
|
||||
)}
|
||||
</ProjectPage>
|
||||
</ResponsiveSearchParamsProvider>
|
||||
|
||||
+2
@@ -15,6 +15,7 @@ export function QuickStartSection(props: {
|
||||
projectSlug: string;
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
projectWalletAddress?: string;
|
||||
}) {
|
||||
return (
|
||||
<section>
|
||||
@@ -43,6 +44,7 @@ export function QuickStartSection(props: {
|
||||
action={
|
||||
<CreatePaymentLinkButton
|
||||
clientId={props.clientId}
|
||||
projectWalletAddress={props.projectWalletAddress}
|
||||
teamId={props.teamId}
|
||||
>
|
||||
<Button
|
||||
|
||||
+42
-9
@@ -45,7 +45,11 @@ const formSchema = z.object({
|
||||
});
|
||||
|
||||
export function CreatePaymentLinkButton(
|
||||
props: PropsWithChildren<{ clientId: string; teamId: string }>,
|
||||
props: PropsWithChildren<{
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
projectWalletAddress?: string;
|
||||
}>,
|
||||
) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@@ -55,6 +59,7 @@ export function CreatePaymentLinkButton(
|
||||
<DialogContent className="p-0 !max-w-lg">
|
||||
<CreatePaymentLinkDialogContent
|
||||
clientId={props.clientId}
|
||||
projectWalletAddress={props.projectWalletAddress}
|
||||
setOpen={setOpen}
|
||||
teamId={props.teamId}
|
||||
/>
|
||||
@@ -67,6 +72,7 @@ function CreatePaymentLinkDialogContent(props: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
setOpen: (open: boolean) => void;
|
||||
projectWalletAddress?: string;
|
||||
}) {
|
||||
const client = getClientThirdwebClient();
|
||||
|
||||
@@ -182,14 +188,41 @@ function CreatePaymentLinkDialogContent(props: {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Recipient Address</FormLabel>
|
||||
<Input
|
||||
className="w-full bg-card"
|
||||
{...field}
|
||||
onChange={field.onChange}
|
||||
value={field.value}
|
||||
placeholder="Address or ENS"
|
||||
required
|
||||
/>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Input
|
||||
className="w-full bg-card sm:flex-1"
|
||||
{...field}
|
||||
onChange={field.onChange}
|
||||
value={field.value}
|
||||
placeholder="Address or ENS"
|
||||
required
|
||||
/>
|
||||
{props.projectWalletAddress && (
|
||||
<Button
|
||||
className="sm:w-auto"
|
||||
onClick={() => {
|
||||
if (!props.projectWalletAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setValue(
|
||||
"recipient",
|
||||
props.projectWalletAddress,
|
||||
{
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true,
|
||||
},
|
||||
);
|
||||
}}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Use Project Wallet
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
+16
-3
@@ -40,7 +40,11 @@ import { ErrorState } from "../../components/ErrorState";
|
||||
import { formatTokenAmount } from "../../components/format";
|
||||
import { CreatePaymentLinkButton } from "./CreatePaymentLinkButton.client";
|
||||
|
||||
export function PaymentLinksTable(props: { clientId: string; teamId: string }) {
|
||||
export function PaymentLinksTable(props: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
projectWalletAddress?: string;
|
||||
}) {
|
||||
return (
|
||||
<section>
|
||||
<div className="mb-4">
|
||||
@@ -49,12 +53,20 @@ export function PaymentLinksTable(props: { clientId: string; teamId: string }) {
|
||||
The payments you have created in this project
|
||||
</p>
|
||||
</div>
|
||||
<PaymentLinksTableInner clientId={props.clientId} teamId={props.teamId} />
|
||||
<PaymentLinksTableInner
|
||||
clientId={props.clientId}
|
||||
projectWalletAddress={props.projectWalletAddress}
|
||||
teamId={props.teamId}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function PaymentLinksTableInner(props: { clientId: string; teamId: string }) {
|
||||
function PaymentLinksTableInner(props: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
projectWalletAddress?: string;
|
||||
}) {
|
||||
const paymentLinksQuery = useQuery({
|
||||
queryFn: async () => {
|
||||
return getPaymentLinks({
|
||||
@@ -120,6 +132,7 @@ function PaymentLinksTableInner(props: { clientId: string; teamId: string }) {
|
||||
<CreatePaymentLinkButton
|
||||
key="create-payment-link"
|
||||
clientId={props.clientId}
|
||||
projectWalletAddress={props.projectWalletAddress}
|
||||
teamId={props.teamId}
|
||||
>
|
||||
<Button className="gap-2 rounded-full" variant="default" size="sm">
|
||||
|
||||
+6
@@ -6,6 +6,7 @@ import { getProject } from "@/api/project/projects";
|
||||
import { ProjectPage } from "@/components/blocks/project-page/project-page";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { PayIcon } from "@/icons/PayIcon";
|
||||
import { getProjectWallet } from "@/lib/server/project-wallet";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { AdvancedSection } from "./components/AdvancedSection.client";
|
||||
import { QuickStartSection } from "./components/QuickstartSection.client";
|
||||
@@ -29,6 +30,8 @@ export default async function Page(props: {
|
||||
redirect(`/team/${params.team_slug}`);
|
||||
}
|
||||
|
||||
const projectWallet = await getProjectWallet(project);
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: project.teamId,
|
||||
@@ -51,6 +54,7 @@ export default async function Page(props: {
|
||||
component: (
|
||||
<CreatePaymentLinkButton
|
||||
clientId={project.publishableKey}
|
||||
projectWalletAddress={projectWallet?.address}
|
||||
teamId={project.teamId}
|
||||
>
|
||||
<Button className="gap-1.5 rounded-full" size="sm">
|
||||
@@ -141,12 +145,14 @@ export default async function Page(props: {
|
||||
<div className="flex flex-col gap-12">
|
||||
<PaymentLinksTable
|
||||
clientId={project.publishableKey}
|
||||
projectWalletAddress={projectWallet?.address}
|
||||
teamId={project.teamId}
|
||||
/>
|
||||
<QuickStartSection
|
||||
projectSlug={params.project_slug}
|
||||
teamSlug={params.team_slug}
|
||||
clientId={project.publishableKey}
|
||||
projectWalletAddress={projectWallet?.address}
|
||||
teamId={project.teamId}
|
||||
/>
|
||||
|
||||
|
||||
+33
-1
@@ -7,6 +7,7 @@ import { toast } from "sonner";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { type Fee, updateFee } from "@/api/universal-bridge/developer";
|
||||
import { SettingsCard } from "@/components/blocks/SettingsCard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -25,6 +26,7 @@ interface PayConfigProps {
|
||||
teamId: string;
|
||||
teamSlug: string;
|
||||
fees: Fee;
|
||||
projectWalletAddress?: string;
|
||||
}
|
||||
|
||||
export const PayConfig: React.FC<PayConfigProps> = (props) => {
|
||||
@@ -103,7 +105,37 @@ export const PayConfig: React.FC<PayConfigProps> = (props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Recipient address</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="0x..." />
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Input
|
||||
{...field}
|
||||
className="sm:flex-1"
|
||||
placeholder="0x..."
|
||||
/>
|
||||
{props.projectWalletAddress && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!props.projectWalletAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setValue(
|
||||
"payoutAddress",
|
||||
props.projectWalletAddress,
|
||||
{
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true,
|
||||
},
|
||||
);
|
||||
}}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Use Project Wallet
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
+4
@@ -3,6 +3,7 @@ import { getAuthToken } from "@/api/auth-token";
|
||||
import { getProject } from "@/api/project/projects";
|
||||
import { getTeamBySlug } from "@/api/team/get-team";
|
||||
import { getFees } from "@/api/universal-bridge/developer";
|
||||
import { getProjectWallet } from "@/lib/server/project-wallet";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { ProjectSettingsBreadcrumb } from "../_components/project-settings-breadcrumb";
|
||||
import { PayConfig } from "./PayConfig";
|
||||
@@ -31,6 +32,8 @@ export default async function Page(props: {
|
||||
redirect(`/team/${team_slug}`);
|
||||
}
|
||||
|
||||
const projectWallet = await getProjectWallet(project);
|
||||
|
||||
let fees = await getFees({
|
||||
clientId: project.publishableKey,
|
||||
teamId: team.id,
|
||||
@@ -64,6 +67,7 @@ export default async function Page(props: {
|
||||
fees={fees}
|
||||
project={project}
|
||||
teamId={team.id}
|
||||
projectWalletAddress={projectWallet?.address}
|
||||
teamSlug={team_slug}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+1
@@ -143,6 +143,7 @@ export function CreateNFTPage(props: {
|
||||
contractName: contractType,
|
||||
deploymentType: "asset",
|
||||
publisher: "deployer.thirdweb.eth",
|
||||
is_testnet: chain.testnet,
|
||||
});
|
||||
|
||||
contractAddressRef.current = contractAddress;
|
||||
|
||||
+4
@@ -316,6 +316,8 @@ export function LaunchNFT(props: {
|
||||
contractType: ercType === "erc721" ? "DropERC721" : "DropERC1155",
|
||||
error: errorMessage,
|
||||
step: currentStep.id,
|
||||
is_testnet: chainMeta.testnet,
|
||||
chainId: Number(formValues.collectionInfo.chain),
|
||||
});
|
||||
|
||||
throw error;
|
||||
@@ -325,6 +327,8 @@ export function LaunchNFT(props: {
|
||||
reportAssetCreationSuccessful({
|
||||
assetType: "nft",
|
||||
contractType: ercType === "erc721" ? "DropERC721" : "DropERC1155",
|
||||
chainId: Number(formValues.collectionInfo.chain),
|
||||
is_testnet: chainMeta?.testnet ?? false,
|
||||
});
|
||||
|
||||
props.onLaunchSuccess();
|
||||
|
||||
+6
-1
@@ -178,12 +178,14 @@ export function CreateTokenAssetPage(props: {
|
||||
teamId: props.teamId,
|
||||
});
|
||||
|
||||
const chainMetadata = getChain(Number(params.values.chain));
|
||||
reportContractDeployed({
|
||||
address: contractAddress,
|
||||
chainId: Number(params.values.chain),
|
||||
contractName: "DropERC20",
|
||||
contractName: "ERC20Asset",
|
||||
deploymentType: "asset",
|
||||
publisher: account.address,
|
||||
is_testnet: chainMetadata.testnet,
|
||||
});
|
||||
|
||||
contractAddressRef.current = contractAddress;
|
||||
@@ -299,12 +301,15 @@ export function CreateTokenAssetPage(props: {
|
||||
teamId: props.teamId,
|
||||
});
|
||||
|
||||
const chainMetadata = getChain(Number(values.chain));
|
||||
|
||||
reportContractDeployed({
|
||||
address: contractAddress,
|
||||
chainId: Number(values.chain),
|
||||
contractName: "DropERC20",
|
||||
deploymentType: "asset",
|
||||
publisher: "deployer.thirdweb.eth",
|
||||
is_testnet: chainMetadata.testnet,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
+4
@@ -230,6 +230,8 @@ export function LaunchTokenStatus(props: {
|
||||
: "ERC20Asset",
|
||||
error: errorMessage,
|
||||
step: currentStep.id,
|
||||
is_testnet: chainMetadata?.testnet,
|
||||
chainId: Number(formValues.chain),
|
||||
});
|
||||
|
||||
updateStatus(i, {
|
||||
@@ -247,6 +249,8 @@ export function LaunchTokenStatus(props: {
|
||||
formValues.saleMode === "drop-erc20:token-drop"
|
||||
? "DropERC20"
|
||||
: "ERC20Asset",
|
||||
chainId: Number(formValues.chain),
|
||||
is_testnet: chainMetadata?.testnet,
|
||||
});
|
||||
|
||||
if (contractAddressRef.current) {
|
||||
|
||||
+104
-2
@@ -3,14 +3,17 @@
|
||||
import { encrypt } from "@thirdweb-dev/service-utils";
|
||||
import {
|
||||
createAccessToken,
|
||||
createEoa,
|
||||
createServiceAccount,
|
||||
createVaultClient,
|
||||
rotateServiceAccount,
|
||||
type VaultClient,
|
||||
} from "@thirdweb-dev/vault-sdk";
|
||||
import { engineCloudProxy } from "@/actions/proxies";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
|
||||
import { updateProjectClient } from "@/hooks/useApi";
|
||||
import { getProjectWalletLabel } from "@/lib/project-wallet";
|
||||
|
||||
const SERVER_WALLET_ACCESS_TOKEN_PURPOSE =
|
||||
"Access Token for All Server Wallets";
|
||||
@@ -126,6 +129,92 @@ export async function createVaultAccountAndAccessToken(props: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function createProjectServerWallet(props: {
|
||||
project: Project;
|
||||
managementAccessToken: string;
|
||||
label?: string;
|
||||
setAsProjectWallet?: boolean;
|
||||
}) {
|
||||
const vaultClient = await initVaultClient();
|
||||
|
||||
const walletLabel = props.label?.trim()
|
||||
? props.label.trim()
|
||||
: getProjectWalletLabel(props.project.name);
|
||||
|
||||
const eoa = await createEoa({
|
||||
client: vaultClient,
|
||||
request: {
|
||||
auth: {
|
||||
accessToken: props.managementAccessToken,
|
||||
},
|
||||
options: {
|
||||
metadata: {
|
||||
label: walletLabel,
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
type: "server-wallet",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!eoa.success) {
|
||||
throw new Error(eoa.error?.message || "Failed to create server wallet");
|
||||
}
|
||||
|
||||
engineCloudProxy({
|
||||
body: JSON.stringify({
|
||||
signerAddress: eoa.data.address,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.project.publishableKey,
|
||||
"x-team-id": props.project.teamId,
|
||||
},
|
||||
method: "POST",
|
||||
pathname: "/cache/smart-account",
|
||||
}).catch((err) => {
|
||||
console.warn("failed to cache server wallet", err);
|
||||
});
|
||||
|
||||
if (props.setAsProjectWallet) {
|
||||
await updateDefaultProjectWallet({
|
||||
project: props.project,
|
||||
projectWalletAddress: eoa.data.address,
|
||||
});
|
||||
}
|
||||
|
||||
return eoa.data;
|
||||
}
|
||||
|
||||
export async function updateDefaultProjectWallet(props: {
|
||||
project: Project;
|
||||
projectWalletAddress: string;
|
||||
}) {
|
||||
const services = props.project.services.filter(
|
||||
(service) => service.name !== "engineCloud",
|
||||
);
|
||||
const engineCloudService = props.project.services.find(
|
||||
(service) => service.name === "engineCloud",
|
||||
);
|
||||
if (engineCloudService) {
|
||||
const engineCloudServiceWithProjectWallet = {
|
||||
...engineCloudService,
|
||||
projectWalletAddress: props.projectWalletAddress,
|
||||
};
|
||||
|
||||
await updateProjectClient(
|
||||
{
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
},
|
||||
{
|
||||
services: [...services, engineCloudServiceWithProjectWallet],
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function createAndEncryptVaultAccessTokens(props: {
|
||||
project: Project;
|
||||
vaultClient: VaultClient;
|
||||
@@ -157,6 +246,13 @@ async function createAndEncryptVaultAccessTokens(props: {
|
||||
const managementToken = managementTokenResult.data;
|
||||
const walletToken = walletTokenResult.data;
|
||||
|
||||
// create a default project server wallet
|
||||
const defaultProjectServerWallet = await createProjectServerWallet({
|
||||
project,
|
||||
managementAccessToken: managementToken.accessToken,
|
||||
label: getProjectWalletLabel(project.name),
|
||||
});
|
||||
|
||||
if (projectSecretKey) {
|
||||
// verify that the project secret key is valid
|
||||
const projectSecretKeyHash = await hashSecretKey(projectSecretKey);
|
||||
@@ -182,7 +278,9 @@ async function createAndEncryptVaultAccessTokens(props: {
|
||||
},
|
||||
{
|
||||
services: [
|
||||
...props.project.services,
|
||||
...props.project.services.filter(
|
||||
(service) => service.name !== "engineCloud",
|
||||
),
|
||||
{
|
||||
name: "engineCloud",
|
||||
actions: [],
|
||||
@@ -191,6 +289,7 @@ async function createAndEncryptVaultAccessTokens(props: {
|
||||
encryptedAdminKey,
|
||||
encryptedWalletAccessToken,
|
||||
rotationCode: rotationCode,
|
||||
projectWalletAddress: defaultProjectServerWallet.address,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -204,7 +303,9 @@ async function createAndEncryptVaultAccessTokens(props: {
|
||||
},
|
||||
{
|
||||
services: [
|
||||
...props.project.services,
|
||||
...props.project.services.filter(
|
||||
(service) => service.name !== "engineCloud",
|
||||
),
|
||||
{
|
||||
name: "engineCloud",
|
||||
actions: [],
|
||||
@@ -213,6 +314,7 @@ async function createAndEncryptVaultAccessTokens(props: {
|
||||
encryptedAdminKey: null,
|
||||
encryptedWalletAccessToken: null,
|
||||
rotationCode: rotationCode,
|
||||
projectWalletAddress: defaultProjectServerWallet.address,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+9
-42
@@ -1,10 +1,8 @@
|
||||
"use client";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { createEoa } from "@thirdweb-dev/vault-sdk";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { engineCloudProxy } from "@/actions/proxies";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -17,12 +15,13 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { initVaultClient } from "../../lib/vault.client";
|
||||
import { createProjectServerWallet } from "../../lib/vault.client";
|
||||
|
||||
export default function CreateServerWallet(props: {
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
managementAccessToken: string | undefined;
|
||||
setAsProjectWallet?: boolean;
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
const [label, setLabel] = useState("");
|
||||
@@ -36,49 +35,17 @@ export default function CreateServerWallet(props: {
|
||||
managementAccessToken: string;
|
||||
label: string;
|
||||
}) => {
|
||||
const vaultClient = await initVaultClient();
|
||||
|
||||
const eoa = await createEoa({
|
||||
client: vaultClient,
|
||||
request: {
|
||||
auth: {
|
||||
accessToken: managementAccessToken,
|
||||
},
|
||||
options: {
|
||||
metadata: {
|
||||
label,
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
type: "server-wallet",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!eoa.success) {
|
||||
throw new Error("Failed to create eoa");
|
||||
}
|
||||
|
||||
// no need to await this, it's not blocking
|
||||
engineCloudProxy({
|
||||
body: JSON.stringify({
|
||||
signerAddress: eoa.data.address,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.project.publishableKey,
|
||||
"x-team-id": props.project.teamId,
|
||||
},
|
||||
method: "POST",
|
||||
pathname: "/cache/smart-account",
|
||||
}).catch((err) => {
|
||||
console.warn("failed to cache server wallet", err);
|
||||
const wallet = await createProjectServerWallet({
|
||||
label,
|
||||
managementAccessToken,
|
||||
project: props.project,
|
||||
setAsProjectWallet: props.setAsProjectWallet,
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
setModalOpen(false);
|
||||
|
||||
return eoa;
|
||||
setLabel("");
|
||||
return wallet;
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
|
||||
+56
-8
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { format, formatDistanceToNowStrict } from "date-fns";
|
||||
import {
|
||||
CheckIcon,
|
||||
MoreVerticalIcon,
|
||||
RefreshCcwIcon,
|
||||
SendIcon,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { useWalletBalance } from "thirdweb/react";
|
||||
import {
|
||||
@@ -20,6 +22,7 @@ import type { Project } from "@/api/project/projects";
|
||||
import { FundWalletModal } from "@/components/blocks/fund-wallets-modal";
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -50,7 +53,9 @@ import {
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
import { WalletProductIcon } from "@/icons/WalletProductIcon";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { updateDefaultProjectWallet } from "../../lib/vault.client";
|
||||
import CreateServerWallet from "../components/create-server-wallet.client";
|
||||
import type { Wallet } from "./types";
|
||||
|
||||
@@ -287,18 +292,34 @@ function ServerWalletTableRow(props: {
|
||||
queryKey: ["smart-account-address", wallet.address, chainId],
|
||||
});
|
||||
|
||||
// Get the default project wallet address
|
||||
const engineCloudService = project.services.find(
|
||||
(service) => service.name === "engineCloud",
|
||||
);
|
||||
const defaultWalletAddress = engineCloudService?.projectWalletAddress;
|
||||
const isDefaultWallet =
|
||||
defaultWalletAddress &&
|
||||
wallet.address.toLowerCase() === defaultWalletAddress.toLowerCase();
|
||||
|
||||
return (
|
||||
<TableRow key={wallet.id}>
|
||||
{/* Label */}
|
||||
<TableCell>
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm text-foreground",
|
||||
!wallet.metadata.label && "text-muted-foreground",
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm text-foreground",
|
||||
!wallet.metadata.label && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{wallet.metadata.label || "N/A"}
|
||||
</span>
|
||||
{isDefaultWallet && (
|
||||
<Badge variant="success" className="text-xs">
|
||||
default
|
||||
</Badge>
|
||||
)}
|
||||
>
|
||||
{wallet.metadata.label || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* Address */}
|
||||
@@ -391,6 +412,25 @@ function WalletActionsDropdown(props: {
|
||||
chainId: number;
|
||||
}) {
|
||||
const [showFundModal, setShowFundModal] = useState(false);
|
||||
const router = useDashboardRouter();
|
||||
|
||||
const setAsDefaultMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await updateDefaultProjectWallet({
|
||||
project: props.project,
|
||||
projectWalletAddress: props.wallet.address,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Wallet set as default project wallet");
|
||||
router.refresh();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Failed to set default wallet",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -420,6 +460,14 @@ function WalletActionsDropdown(props: {
|
||||
Fund wallet
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => setAsDefaultMutation.mutate()}
|
||||
disabled={setAsDefaultMutation.isPending}
|
||||
className="flex items-center gap-2 h-9 rounded-lg"
|
||||
>
|
||||
<CheckIcon className="size-4 text-muted-foreground" />
|
||||
Set as default
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export function UniversalBridgeEmbed({
|
||||
buyAmount={amount}
|
||||
tokenAddress={token?.address}
|
||||
pageType="bridge"
|
||||
isTestnet={chain.testnet}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { bridgeAppThirdwebClient } from "./constants";
|
||||
|
||||
const title = "thirdweb Bridge: Buy, Bridge & Swap Crypto on 85+ Chains";
|
||||
const description =
|
||||
"Bridge, swap, and on-ramp across 9M+ token routes on 85+ chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing and near-instant fast finality";
|
||||
"Bridge and swap 3,200+ tokens across 85+ chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing with near-instant finality";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description,
|
||||
@@ -105,8 +105,7 @@ export default async function BridgePage({
|
||||
href="https://portal.thirdweb.com/bridge"
|
||||
linkType="integrate-bridge"
|
||||
>
|
||||
Integrate Bridge in your apps in minutes, and start generating
|
||||
revenue
|
||||
Accept Payments in Any Token Right Inside Your App
|
||||
</PillLink>
|
||||
|
||||
<PillLink
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ShieldCheckIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { Bridge, defineChain, toTokens } from "thirdweb";
|
||||
import { getChainMetadata } from "thirdweb/chains";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
@@ -80,131 +81,137 @@ export default async function PayPage({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex z-10 flex-col lg:flex-row h-full w-full">
|
||||
<header className="min-w-full lg:min-w-[500px] border-b lg:border-r lg:h-full bg-card flex flex-col gap-4 items-start p-4 lg:p-8">
|
||||
<div>
|
||||
<div className="flex flex-row items-center justify-start gap-4">
|
||||
{projectMetadata.image && (
|
||||
<img
|
||||
src={
|
||||
resolveSchemeWithErrorHandler({
|
||||
uri: projectMetadata.image,
|
||||
client: payAppThirdwebClient,
|
||||
}) || ""
|
||||
}
|
||||
alt={projectMetadata.name}
|
||||
width={25}
|
||||
height={25}
|
||||
className="rounded-full overflow-hidden"
|
||||
/>
|
||||
<ThemeProvider
|
||||
forcedTheme={theme === "light" ? "light" : "dark"}
|
||||
attribute="class"
|
||||
disableTransitionOnChange
|
||||
enableSystem={false}
|
||||
>
|
||||
<div className="flex z-10 flex-col lg:flex-row h-full w-full">
|
||||
<header className="min-w-full lg:min-w-[500px] border-b lg:border-r lg:h-full bg-card flex flex-col gap-4 items-start p-4 lg:p-8">
|
||||
<div>
|
||||
<div className="flex flex-row items-center justify-start gap-4">
|
||||
{projectMetadata.image && (
|
||||
<img
|
||||
src={
|
||||
resolveSchemeWithErrorHandler({
|
||||
uri: projectMetadata.image,
|
||||
client: payAppThirdwebClient,
|
||||
}) || ""
|
||||
}
|
||||
alt={projectMetadata.name}
|
||||
width={25}
|
||||
height={25}
|
||||
className="rounded-full overflow-hidden"
|
||||
/>
|
||||
)}
|
||||
<h2 className="text-xl font-bold">{projectMetadata.name}</h2>
|
||||
</div>
|
||||
{projectMetadata.description && (
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{projectMetadata.description}
|
||||
</p>
|
||||
)}
|
||||
<h2 className="text-xl font-bold">{projectMetadata.name}</h2>
|
||||
</div>
|
||||
{projectMetadata.description && (
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{projectMetadata.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block my-4 w-full">
|
||||
{paymentLink.amount && (
|
||||
<div className="flex flex-col gap-1 w-full my-4">
|
||||
<span className="text-muted-foreground text-xs">Details</span>
|
||||
<div className="font-medium flex-row flex justify-between items-center w-full">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{token.iconUri && (
|
||||
<img
|
||||
src={resolveSchemeWithErrorHandler({
|
||||
uri: token.iconUri,
|
||||
client: getConfiguredThirdwebClient({
|
||||
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
teamId: undefined,
|
||||
}),
|
||||
})}
|
||||
alt={token.name}
|
||||
width={25}
|
||||
height={25}
|
||||
className="size-5 rounded-full overflow-hidden"
|
||||
/>
|
||||
)}
|
||||
{toTokens(BigInt(paymentLink.amount), token.decimals)}{" "}
|
||||
{token.symbol}
|
||||
</div>
|
||||
{token.prices.USD && (
|
||||
<span>
|
||||
$
|
||||
{(
|
||||
Number(token.prices.USD) *
|
||||
Number(
|
||||
toTokens(BigInt(paymentLink.amount), token.decimals),
|
||||
)
|
||||
).toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{chain && (
|
||||
<div className="flex flex-col gap-1 w-full my-4">
|
||||
<span className="text-muted-foreground text-xs">Network</span>
|
||||
<div className="font-medium flex-row flex justify-between items-center w-full">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{chain.icon?.url && (
|
||||
<img
|
||||
src={resolveSchemeWithErrorHandler({
|
||||
uri: chain.icon.url,
|
||||
client: getConfiguredThirdwebClient({
|
||||
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
teamId: undefined,
|
||||
}),
|
||||
})}
|
||||
alt={chain.name}
|
||||
width={chain.icon.width}
|
||||
height={chain.icon.height}
|
||||
className="size-5 rounded-full overflow-hidden"
|
||||
/>
|
||||
)}
|
||||
{chain.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{recipientEnsOrAddress.ensName ||
|
||||
(recipientEnsOrAddress.address && (
|
||||
<div className="hidden lg:block my-4 w-full">
|
||||
{paymentLink.amount && (
|
||||
<div className="flex flex-col gap-1 w-full my-4">
|
||||
<span className="text-muted-foreground text-xs">Seller</span>
|
||||
<span className="text-muted-foreground text-xs">Details</span>
|
||||
<div className="font-medium flex-row flex justify-between items-center w-full">
|
||||
{recipientEnsOrAddress.ensName ??
|
||||
shortenAddress(recipientEnsOrAddress.address)}
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{token.iconUri && (
|
||||
<img
|
||||
src={resolveSchemeWithErrorHandler({
|
||||
uri: token.iconUri,
|
||||
client: getConfiguredThirdwebClient({
|
||||
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
teamId: undefined,
|
||||
}),
|
||||
})}
|
||||
alt={token.name}
|
||||
width={25}
|
||||
height={25}
|
||||
className="size-5 rounded-full overflow-hidden"
|
||||
/>
|
||||
)}
|
||||
{toTokens(BigInt(paymentLink.amount), token.decimals)}{" "}
|
||||
{token.symbol}
|
||||
</div>
|
||||
{token.prices.USD && (
|
||||
<span>
|
||||
$
|
||||
{(
|
||||
Number(token.prices.USD) *
|
||||
Number(
|
||||
toTokens(BigInt(paymentLink.amount), token.decimals),
|
||||
)
|
||||
).toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{chain && (
|
||||
<div className="flex flex-col gap-1 w-full my-4">
|
||||
<span className="text-muted-foreground text-xs">Network</span>
|
||||
<div className="font-medium flex-row flex justify-between items-center w-full">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{chain.icon?.url && (
|
||||
<img
|
||||
src={resolveSchemeWithErrorHandler({
|
||||
uri: chain.icon.url,
|
||||
client: getConfiguredThirdwebClient({
|
||||
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
teamId: undefined,
|
||||
}),
|
||||
})}
|
||||
alt={chain.name}
|
||||
width={chain.icon.width}
|
||||
height={chain.icon.height}
|
||||
className="size-5 rounded-full overflow-hidden"
|
||||
/>
|
||||
)}
|
||||
{chain.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{recipientEnsOrAddress.ensName ||
|
||||
(recipientEnsOrAddress.address && (
|
||||
<div className="flex flex-col gap-1 w-full my-4">
|
||||
<span className="text-muted-foreground text-xs">Seller</span>
|
||||
<div className="font-medium flex-row flex justify-between items-center w-full">
|
||||
{recipientEnsOrAddress.ensName ??
|
||||
shortenAddress(recipientEnsOrAddress.address)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto hidden lg:block">
|
||||
<Badge className="flex items-center gap-1.5 bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-950 dark:text-purple-300 dark:border-purple-800">
|
||||
<ShieldCheckIcon className="size-3" />
|
||||
Secured by thirdweb
|
||||
</Badge>
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex justify-center p-12 w-full items-center">
|
||||
<PayPageWidget
|
||||
amount={paymentLink.amount ? BigInt(paymentLink.amount) : undefined}
|
||||
chainId={Number(paymentLink.destinationToken.chainId)}
|
||||
clientId={undefined} // Payment links don't need to use the same client ID to be executed
|
||||
image={paymentLink.imageUrl}
|
||||
name={paymentLink.title}
|
||||
paymentLinkId={id}
|
||||
purchaseData={paymentLink.purchaseData}
|
||||
recipientAddress={paymentLink.receiver}
|
||||
redirectUri={redirectUri}
|
||||
theme={theme}
|
||||
token={token}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
<div className="mt-auto hidden lg:block">
|
||||
<Badge className="flex items-center gap-1.5 bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-950 dark:text-purple-300 dark:border-purple-800">
|
||||
<ShieldCheckIcon className="size-3" />
|
||||
Secured by thirdweb
|
||||
</Badge>
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex justify-center p-12 w-full items-center">
|
||||
<PayPageWidget
|
||||
amount={paymentLink.amount ? BigInt(paymentLink.amount) : undefined}
|
||||
chainId={Number(paymentLink.destinationToken.chainId)}
|
||||
clientId={undefined} // Payment links don't need to use the same client ID to be executed
|
||||
image={paymentLink.imageUrl}
|
||||
name={paymentLink.title}
|
||||
paymentLinkId={id}
|
||||
purchaseData={paymentLink.purchaseData}
|
||||
recipientAddress={paymentLink.receiver}
|
||||
redirectUri={redirectUri}
|
||||
token={token}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
import { payAppThirdwebClient } from "app/pay/constants";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect } from "react";
|
||||
import { createThirdwebClient, NATIVE_TOKEN_ADDRESS, toTokens } from "thirdweb";
|
||||
import { AutoConnect, CheckoutWidget } from "thirdweb/react";
|
||||
import { checksumAddress } from "thirdweb/utils";
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
reportPaymentLinkBuySuccessful,
|
||||
} from "@/analytics/report";
|
||||
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
|
||||
export function PayPageWidget({
|
||||
chainId,
|
||||
@@ -21,7 +21,6 @@ export function PayPageWidget({
|
||||
name,
|
||||
image,
|
||||
redirectUri,
|
||||
theme,
|
||||
purchaseData,
|
||||
clientId,
|
||||
}: {
|
||||
@@ -34,17 +33,10 @@ export function PayPageWidget({
|
||||
image?: string;
|
||||
redirectUri?: string;
|
||||
clientId: string | undefined;
|
||||
theme?: "light" | "dark";
|
||||
purchaseData: Record<string, unknown> | undefined;
|
||||
}) {
|
||||
const { theme: browserTheme, setTheme } = useTheme();
|
||||
const { theme } = useTheme();
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (theme) {
|
||||
setTheme(theme);
|
||||
}
|
||||
}, [theme, setTheme]);
|
||||
const chain = useV5DashboardChain(chainId);
|
||||
|
||||
return (
|
||||
@@ -55,6 +47,7 @@ export function PayPageWidget({
|
||||
}
|
||||
/>
|
||||
<CheckoutWidget
|
||||
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
|
||||
connectOptions={{
|
||||
wallets: [
|
||||
createWallet("io.metamask"),
|
||||
@@ -87,7 +80,6 @@ export function PayPageWidget({
|
||||
paymentLinkId={paymentLinkId}
|
||||
purchaseData={purchaseData}
|
||||
seller={checksumAddress(recipientAddress)}
|
||||
theme={theme ?? (browserTheme === "light" ? "light" : "dark")}
|
||||
tokenAddress={
|
||||
token.address === NATIVE_TOKEN_ADDRESS
|
||||
? undefined
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import "@workspace/ui/global.css";
|
||||
import { Inter } from "next/font/google";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PayProviders } from "./components/client/Providers.client";
|
||||
|
||||
@@ -24,16 +23,9 @@ export default async function PayLayout({
|
||||
)}
|
||||
>
|
||||
<PayProviders>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
disableTransitionOnChange
|
||||
enableSystem={false}
|
||||
>
|
||||
<div className="relative mx-auto flex h-full w-full items-center justify-center overflow-x-hidden overflow-y-scroll">
|
||||
{children}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
<div className="relative mx-auto flex h-full w-full items-center justify-center overflow-x-hidden overflow-y-scroll">
|
||||
{children}
|
||||
</div>
|
||||
</PayProviders>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { createThirdwebClient, defineChain, getContract } from "thirdweb";
|
||||
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
|
||||
import { checksumAddress } from "thirdweb/utils";
|
||||
@@ -33,7 +34,16 @@ export default async function PayPage({
|
||||
!params.tokenAddress &&
|
||||
!params.amount
|
||||
) {
|
||||
return <PaymentLinkForm />;
|
||||
return (
|
||||
<ThemeProvider
|
||||
forcedTheme={params.theme === "light" ? "light" : "dark"}
|
||||
attribute="class"
|
||||
disableTransitionOnChange
|
||||
enableSystem={false}
|
||||
>
|
||||
<PaymentLinkForm />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Validate query parameters
|
||||
@@ -84,17 +94,23 @@ export default async function PayPage({
|
||||
};
|
||||
|
||||
return (
|
||||
<PayPageWidget
|
||||
amount={BigInt(params.amount)}
|
||||
chainId={Number(params.chainId)}
|
||||
clientId={client.clientId}
|
||||
image={params.image}
|
||||
name={params.name}
|
||||
purchaseData={undefined}
|
||||
recipientAddress={params.recipientAddress}
|
||||
redirectUri={params.redirectUri}
|
||||
theme={params.theme}
|
||||
token={token}
|
||||
/>
|
||||
<ThemeProvider
|
||||
forcedTheme={params.theme === "light" ? "light" : "dark"}
|
||||
attribute="class"
|
||||
disableTransitionOnChange
|
||||
enableSystem={false}
|
||||
>
|
||||
<PayPageWidget
|
||||
amount={BigInt(params.amount)}
|
||||
chainId={Number(params.chainId)}
|
||||
clientId={client.clientId}
|
||||
image={params.image}
|
||||
name={params.name}
|
||||
purchaseData={undefined}
|
||||
recipientAddress={params.recipientAddress}
|
||||
redirectUri={params.redirectUri}
|
||||
token={token}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export function ChatContainer() {
|
||||
return (
|
||||
<Reasoning
|
||||
key={`${message.id}-reasoning-${i}`}
|
||||
className="w-full"
|
||||
className="w-full max-w-md"
|
||||
isStreaming={status === "streaming"}
|
||||
>
|
||||
<ReasoningTrigger />
|
||||
|
||||
@@ -738,6 +738,10 @@ const paymentRedirects = {
|
||||
"/connect/pay/buy-with-fiat": "/payments",
|
||||
"/connect/pay/enable-test-mode": "/payments",
|
||||
"/connect/pay/guides/enable-test-mode": "/payments",
|
||||
"/payments/sell": "/bridge/sell",
|
||||
"/payments/swap": "/bridge/swap",
|
||||
"/payments/tokens": "/bridge/tokens",
|
||||
"/payments/routes": "/bridge/routes",
|
||||
};
|
||||
|
||||
const contractRedirects = {
|
||||
|
||||
@@ -17,6 +17,76 @@ https://api.thirdweb.com/mcp?secretKey=<your-project-secret-key>
|
||||
|
||||
Make sure to keep your secret key safe and never share it with anyone.
|
||||
|
||||
By default, all tools are available. You can filter the tools available by passing a comma-separated list of tools as a query parameter.
|
||||
|
||||
```http
|
||||
https://api.thirdweb.com/mcp?secretKey=<your-project-secret-key>&tools=fetchWithPayment,getWalletBalance
|
||||
```
|
||||
|
||||
You can find all available tools at the bottom of this page.
|
||||
|
||||
## Usage with agents
|
||||
|
||||
Use your favorite agent framework to plug in the MCP server as a collection of tools for your agent. Refer to your agent framework's documentation for more information.
|
||||
|
||||
#### Example usage with langchain:
|
||||
|
||||
<Tabs defaultValue="typescript">
|
||||
|
||||
<TabsList>
|
||||
<TabsTrigger value="typescript">TypeScript</TabsTrigger>
|
||||
<TabsTrigger value="python">Python</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="typescript">
|
||||
|
||||
```typescript
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
|
||||
|
||||
const mcpServers = {
|
||||
thirdweb: {
|
||||
url: `https://api.thirdweb.com/mcp?secretKey=<your-project-secret-key>`
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to MCP servers and get tools
|
||||
const client = new MultiServerMCPClient(mcpServers);
|
||||
const tools = await client.getTools();
|
||||
|
||||
const agent = createReactAgent({
|
||||
llm: model,
|
||||
tools: tools,
|
||||
});
|
||||
await agent.invoke({messages: [{ role: "user", content: "create a server wallet called 'my-wallet'" }]})
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="python">
|
||||
|
||||
```python
|
||||
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
client = MultiServerMCPClient(
|
||||
{
|
||||
"thirdweb": {
|
||||
"url": "https://api.thirdweb.com/mcp?secretKey=<your-project-secret-key>",
|
||||
}
|
||||
}
|
||||
)
|
||||
tools = await client.get_tools()
|
||||
agent = create_react_agent("openai:gpt-4.1", tools)
|
||||
response = await agent.invoke({"messages": "create a server wallet called 'my-wallet'"})
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
|
||||
</Tabs>
|
||||
|
||||
Once installed, you can use the entire thirdweb API with natural language.
|
||||
|
||||
## Usage with LLM clients
|
||||
|
||||
You can also use the MCP server on your own LLM client, like cursor, claude code and more. Refer to your LLM client's documentation for more information.
|
||||
@@ -112,31 +182,6 @@ claude mcp add --transport http "thirdweb-api" "https://api.thirdweb.com/mcp?sec
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Usage with agents
|
||||
|
||||
Use your favorite agent framework to plug in the MCP server as a collection of tools for your agent. Refer to your agent framework's documentation for more information.
|
||||
|
||||
#### Example usage with langchain:
|
||||
|
||||
```python
|
||||
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
client = MultiServerMCPClient(
|
||||
{
|
||||
"thirdweb-api": {
|
||||
"transport": "streamable_http",
|
||||
"url": "https://api.thirdweb.com/mcp?secretKey=<your-project-secret-key>",
|
||||
}
|
||||
}
|
||||
)
|
||||
tools = await client.get_tools()
|
||||
agent = create_react_agent("openai:gpt-4.1", tools)
|
||||
response = await agent.ainvoke({"messages": "create a server wallet called 'my-wallet'"})
|
||||
```
|
||||
|
||||
Once installed, you can use the entire thirdweb API with natural language.
|
||||
|
||||
## Example prompts
|
||||
|
||||
#### Managing server wallets
|
||||
@@ -164,3 +209,46 @@ List my contracts
|
||||
```
|
||||
approve 100 USDC from treasury wallet to executor wallet
|
||||
```
|
||||
|
||||
## Available tools
|
||||
|
||||
Currently, the following tools are available:
|
||||
|
||||
- `initiateAuthentication`
|
||||
- `completeAuthentication`
|
||||
- `linkAuthentication`
|
||||
- `unlinkAuthentication`
|
||||
- `getMyWallet`
|
||||
- `listUserWallets`
|
||||
- `createUserWallet`
|
||||
- `listServerWallets`
|
||||
- `createServerWallet`
|
||||
- `getWalletBalance`
|
||||
- `getWalletTransactions`
|
||||
- `getWalletTokens`
|
||||
- `getWalletNFTs`
|
||||
- `signMessage`
|
||||
- `signTypedData`
|
||||
- `sendTokens`
|
||||
- `listContracts`
|
||||
- `deployContract`
|
||||
- `readContract`
|
||||
- `writeContract`
|
||||
- `getContractTransactions`
|
||||
- `getContractEvents`
|
||||
- `getContractMetadata`
|
||||
- `getContractSignatures`
|
||||
- `getTransactionById`
|
||||
- `listTransactions`
|
||||
- `sendTransactions`
|
||||
- `createPayment`
|
||||
- `paymentsPurchase`
|
||||
- `getPaymentHistory`
|
||||
- `fetchWithPayment`
|
||||
- `createToken`
|
||||
- `listTokens`
|
||||
- `getTokenOwners`
|
||||
- `getBridgeChains`
|
||||
- `convertFiatToCrypto`
|
||||
- `bridgeSwap`
|
||||
- `chat`
|
||||
@@ -53,10 +53,6 @@ The BridgeWidget Script makes it easy to embed cross-chain swaps and fiat onramp
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
theme: "dark",
|
||||
buy: {
|
||||
chainId: 8453,
|
||||
amount: "0.1",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
```
|
||||
@@ -126,9 +122,9 @@ type Options = {
|
||||
};
|
||||
};
|
||||
};
|
||||
buy: {
|
||||
amount: string;
|
||||
chainId: number;
|
||||
buy?: {
|
||||
amount?: string;
|
||||
chainId?: number;
|
||||
tokenAddress?: string;
|
||||
buttonLabel?: string;
|
||||
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
|
||||
@@ -310,8 +306,8 @@ The options to configure the "Buy" tab UI.
|
||||
|
||||
```ts
|
||||
type Buy = {
|
||||
amount: string; // Required
|
||||
chainId: number; // Required
|
||||
amount?: string;
|
||||
chainId?: number;
|
||||
tokenAddress?: string;
|
||||
buttonLabel?: string;
|
||||
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
|
||||
@@ -327,21 +323,21 @@ type Buy = {
|
||||
```
|
||||
|
||||
|
||||
<Details summary="amount" tags={["Required"]}>
|
||||
<Details summary="amount">
|
||||
|
||||
Set the default token amount shown in the "Buy" tab.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="chainId" tags={["Required"]}>
|
||||
<Details summary="chainId">
|
||||
|
||||
Specify which chain should be selected in the "Buy" tab.
|
||||
Specify the default chain to be selected in the "Buy" tab.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="tokenAddress">
|
||||
|
||||
Specify which token should be selected in the "Buy" tab. If not specified, the native token of the selected chain will be selected.
|
||||
Specify the default token to be selected in the "Buy" tab. If not specified, the native token of the selected chain will be selected.
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@doc";
|
||||
|
||||
# Session Keys Guide
|
||||
|
||||
Session keys enable secure transaction execution on behalf of smart accounts without requiring direct access to the main account's private key. This guide will walk you through creating and using session keys with the thirdweb TypeScript SDK.
|
||||
Session keys enable secure transaction execution on behalf of accounts without requiring direct access to the main account's private key. This guide covers session keys for both ERC-4337 smart accounts and ERC-7702 smart EOAs (Externally Owned Accounts).
|
||||
|
||||
<Tabs defaultValue="erc4337">
|
||||
<TabsList>
|
||||
<TabsTrigger value="erc4337">ERC-4337 Smart Accounts</TabsTrigger>
|
||||
<TabsTrigger value="erc7702">ERC-7702 Smart EOAs</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="erc4337">
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -323,8 +333,409 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="erc7702">
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have:
|
||||
- A thirdweb client configured
|
||||
- An in-app wallet with ERC-7702 support
|
||||
- A session key account address (can be generated or an existing wallet)
|
||||
|
||||
## Setup
|
||||
|
||||
First, let's set up the necessary imports and configuration:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createThirdwebClient,
|
||||
getContract,
|
||||
sendTransaction,
|
||||
prepareTransaction
|
||||
} from "thirdweb";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
import { inAppWallet, createSessionKey } from "thirdweb/wallets/in-app";
|
||||
|
||||
// Configure your client
|
||||
const client = createThirdwebClient({
|
||||
clientId: "your-client-id",
|
||||
secretKey: "your-secret-key" // Only use in server environments
|
||||
});
|
||||
|
||||
// Your session key account address
|
||||
const sessionKeyAccountAddress = "0x..."; // Replace with your session key address
|
||||
```
|
||||
|
||||
## Step 1: Create ERC-7702 Smart EOA
|
||||
|
||||
Create an in-app wallet configured with ERC-7702 execution mode:
|
||||
|
||||
```typescript
|
||||
const wallet = inAppWallet({
|
||||
executionMode: {
|
||||
mode: "EIP7702",
|
||||
sponsorGas: true, // Enable gas sponsorship
|
||||
},
|
||||
});
|
||||
|
||||
// Connect the wallet (user authentication)
|
||||
const account = await wallet.connect({
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
strategy: "google", // or "email", "guest", etc.
|
||||
});
|
||||
|
||||
console.log("Smart EOA address:", account.address);
|
||||
```
|
||||
|
||||
### Execution Modes
|
||||
|
||||
The `executionMode` option allows you to configure the wallet behavior:
|
||||
|
||||
- `mode`: Set to `"EIP7702"` for ERC-7702 smart EOA functionality
|
||||
- `sponsorGas`: Enable gas sponsorship for gasless transactions
|
||||
|
||||
## Step 2: Deploy/Upgrade the Account
|
||||
|
||||
Send a transaction to trigger the account deployment or upgrade to ERC-7702:
|
||||
|
||||
```typescript
|
||||
await sendTransaction({
|
||||
account: account,
|
||||
transaction: prepareTransaction({
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
to: account.address,
|
||||
value: 0n, // Self-transfer to trigger deployment
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("Smart EOA deployed/upgraded successfully");
|
||||
```
|
||||
|
||||
## Step 3: Get Account Contract
|
||||
|
||||
Create a contract instance for the deployed smart EOA:
|
||||
|
||||
```typescript
|
||||
const accountContract = getContract({
|
||||
address: account.address,
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
// ABI will be automatically resolved since the contract is deployed
|
||||
});
|
||||
```
|
||||
|
||||
## Step 4: Create Session Key with Full Permissions
|
||||
|
||||
Add a session key with full execution permissions:
|
||||
|
||||
```typescript
|
||||
const tx = await sendTransaction({
|
||||
account: account,
|
||||
transaction: createSessionKey({
|
||||
account: account,
|
||||
contract: accountContract,
|
||||
sessionKeyAddress: sessionKeyAccountAddress,
|
||||
durationInSeconds: 86400, // 1 day
|
||||
grantFullPermissions: true, // Allow unrestricted access
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("Session key created:", tx.transactionHash);
|
||||
```
|
||||
|
||||
### Session Key Options
|
||||
|
||||
- `account`: The admin account that will create the session key
|
||||
- `contract`: The smart EOA contract instance
|
||||
- `sessionKeyAddress`: The address that will be granted session key permissions
|
||||
- `durationInSeconds`: How long the session key should be valid (in seconds)
|
||||
- `grantFullPermissions`: If `true`, grants unrestricted access. If `false`, you must specify policies.
|
||||
|
||||
## Step 5 (Advanced): Create Session Key with Granular Permissions
|
||||
|
||||
For more fine-grained control, you can specify call and transfer policies:
|
||||
|
||||
```typescript
|
||||
import { Condition, LimitType } from "thirdweb/wallets/in-app";
|
||||
|
||||
const tx = await sendTransaction({
|
||||
account: account,
|
||||
transaction: createSessionKey({
|
||||
account: account,
|
||||
contract: accountContract,
|
||||
sessionKeyAddress: sessionKeyAccountAddress,
|
||||
durationInSeconds: 86400, // 1 day
|
||||
grantFullPermissions: false, // Use granular permissions
|
||||
callPolicies: [
|
||||
{
|
||||
target: "0x123...", // Target contract address
|
||||
selector: "0xa9059cbb", // Function selector (e.g., ERC20 transfer)
|
||||
maxValuePerUse: 0n, // Max ETH value per call
|
||||
valueLimit: {
|
||||
limitType: LimitType.Lifetime, // Lifetime limit
|
||||
limit: 1000000n, // Max total value in wei
|
||||
period: 0n, // Not used for lifetime limits
|
||||
},
|
||||
constraints: [
|
||||
{
|
||||
condition: Condition.Equal, // Constraint type
|
||||
index: 0n, // Parameter index
|
||||
refValue: "0x000000000000000000000000abc...", // Expected value
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
transferPolicies: [
|
||||
{
|
||||
target: "0x456...", // Recipient address for transfers
|
||||
maxValuePerUse: 100000n, // Max wei per transfer
|
||||
valueLimit: {
|
||||
limitType: LimitType.Allowance, // Allowance-based limit
|
||||
limit: 1000000n, // Total allowance in wei
|
||||
period: 86400n, // Reset period in seconds
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("Session key with policies created:", tx.transactionHash);
|
||||
```
|
||||
|
||||
### Permission Types
|
||||
|
||||
#### Call Policies
|
||||
|
||||
Control which smart contract calls the session key can make:
|
||||
|
||||
- `target`: The contract address the session key can call
|
||||
- `selector`: The function selector (4-byte identifier) that can be called
|
||||
- `maxValuePerUse`: Maximum ETH value that can be sent per transaction
|
||||
- `valueLimit`: Total value limits over time
|
||||
- `constraints`: Parameter-level constraints for function calls
|
||||
|
||||
#### Transfer Policies
|
||||
|
||||
Control native token transfers the session key can make:
|
||||
|
||||
- `target`: The recipient address for transfers
|
||||
- `maxValuePerUse`: Maximum wei per individual transfer
|
||||
- `valueLimit`: Total transfer limits over time
|
||||
|
||||
#### Limit Types
|
||||
|
||||
```typescript
|
||||
enum LimitType {
|
||||
Unlimited = 0, // No limit
|
||||
Lifetime = 1, // One-time total limit
|
||||
Allowance = 2, // Renewable allowance based on period
|
||||
}
|
||||
```
|
||||
|
||||
#### Constraint Conditions
|
||||
|
||||
```typescript
|
||||
enum Condition {
|
||||
Unconstrained = 0, // No constraint
|
||||
Equal = 1, // Must equal reference value
|
||||
Greater = 2, // Must be greater than reference value
|
||||
Less = 3, // Must be less than reference value
|
||||
GreaterOrEqual = 4, // Must be >= reference value
|
||||
LessOrEqual = 5, // Must be <= reference value
|
||||
NotEqual = 6, // Must not equal reference value
|
||||
}
|
||||
```
|
||||
|
||||
## Step 6: Use the Session Key
|
||||
|
||||
Once created, the session key can be used with Engine or other server-side execution:
|
||||
|
||||
```typescript
|
||||
import { Engine } from "thirdweb/engine";
|
||||
|
||||
const serverWallet = Engine.serverWallet({
|
||||
address: sessionKeyAccountAddress,
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
vaultAccessToken: process.env.VAULT_TOKEN as string,
|
||||
});
|
||||
|
||||
// Execute transactions with the session key
|
||||
const tx = await sendTransaction({
|
||||
account: serverWallet,
|
||||
transaction: prepareTransaction({
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
to: "0x...", // Target address
|
||||
value: 0n,
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("Transaction executed:", tx.transactionHash);
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
Here's a complete example for setting up and using a session key with ERC-7702:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createThirdwebClient,
|
||||
getContract,
|
||||
sendTransaction,
|
||||
prepareTransaction
|
||||
} from "thirdweb";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
import { inAppWallet, createSessionKey } from "thirdweb/wallets/in-app";
|
||||
import { Engine } from "thirdweb/engine";
|
||||
|
||||
async function setupSessionKeyWith7702() {
|
||||
// Configuration
|
||||
const client = createThirdwebClient({
|
||||
clientId: "your-client-id",
|
||||
secretKey: "your-secret-key"
|
||||
});
|
||||
|
||||
const sessionKeyAccountAddress = "0x..."; // Your session key address
|
||||
|
||||
try {
|
||||
// Step 1: Create 7702 Smart EOA
|
||||
const wallet = inAppWallet({
|
||||
executionMode: {
|
||||
mode: "EIP7702",
|
||||
sponsorGas: true,
|
||||
},
|
||||
});
|
||||
|
||||
const account = await wallet.connect({
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
strategy: "google",
|
||||
});
|
||||
|
||||
console.log("Smart EOA created:", account.address);
|
||||
|
||||
// Step 2: Deploy/upgrade the account
|
||||
await sendTransaction({
|
||||
account: account,
|
||||
transaction: prepareTransaction({
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
to: account.address,
|
||||
value: 0n,
|
||||
}),
|
||||
});
|
||||
|
||||
// Step 3: Get account contract
|
||||
const accountContract = getContract({
|
||||
address: account.address,
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
});
|
||||
|
||||
// Step 4: Create session key
|
||||
const sessionTx = await sendTransaction({
|
||||
account: account,
|
||||
transaction: createSessionKey({
|
||||
account: account,
|
||||
contract: accountContract,
|
||||
sessionKeyAddress: sessionKeyAccountAddress,
|
||||
durationInSeconds: 86400, // 1 day
|
||||
grantFullPermissions: true,
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("Session key created:", sessionTx.transactionHash);
|
||||
|
||||
// Step 5: Use the session key with Engine
|
||||
const serverWallet = Engine.serverWallet({
|
||||
address: sessionKeyAccountAddress,
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
vaultAccessToken: process.env.VAULT_TOKEN as string,
|
||||
});
|
||||
|
||||
// Execute a transaction with the session key
|
||||
const tx = await sendTransaction({
|
||||
account: serverWallet,
|
||||
transaction: prepareTransaction({
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
to: "0x...", // Target address
|
||||
value: 0n,
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("Transaction successful:", tx.transactionHash);
|
||||
return tx;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the function
|
||||
setupSessionKeyWith7702()
|
||||
.then((tx) => console.log("Done!", tx.transactionHash))
|
||||
.catch((error) => console.error("Failed:", error));
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Session Key Storage**: Store session keys securely, preferably in a vault system
|
||||
- **Permission Scope**: Use granular permissions (`grantFullPermissions: false`) for production use cases
|
||||
- **Time Limits**: Set appropriate `durationInSeconds` values based on your use case
|
||||
- **Policy Design**: Carefully design call and transfer policies to minimize risk
|
||||
- **Key Rotation**: Regularly rotate session keys and revoke unused ones
|
||||
- **Monitoring**: Monitor session key usage for suspicious activity
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Account not deployed**: Ensure you've sent a transaction to trigger the account deployment/upgrade
|
||||
2. **Invalid signature**: Make sure the admin account is properly authenticated and can sign typed data
|
||||
3. **Permission denied**: Verify the session key has the necessary policies for the operation
|
||||
4. **Expired session key**: Check that the current time is within the session key's validity period
|
||||
|
||||
### Error Handling
|
||||
|
||||
Always wrap your session key operations in try-catch blocks:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const tx = await sendTransaction({
|
||||
account: account,
|
||||
transaction: createSessionKey({
|
||||
account: account,
|
||||
contract: accountContract,
|
||||
sessionKeyAddress: sessionKeyAccountAddress,
|
||||
durationInSeconds: 86400,
|
||||
grantFullPermissions: true,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes("signature")) {
|
||||
console.error("Failed to sign session key creation");
|
||||
} else if (error.message.includes("duration")) {
|
||||
console.error("Invalid duration specified");
|
||||
} else {
|
||||
console.error("Session key creation failed:", error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Learn more about [Smart Wallets](/engine/v3/configure-wallets/server-wallets)
|
||||
- Learn more about [In-App Wallets](/wallets/in-app-wallet/overview)
|
||||
- Explore [Engine API Reference](https://engine.thirdweb.com/reference)
|
||||
- Check out the [TypeScript SDK](/references/typescript/v5/serverWallet) documentation
|
||||
- Check out the [TypeScript SDK](/references/typescript/v5) documentation
|
||||
- Read about [ERC-7702](https://eips.ethereum.org/EIPS/eip-7702) specification
|
||||
|
||||
@@ -80,6 +80,10 @@ export const sidebar: SideBar = {
|
||||
href: `${paymentsSlug}/x402/server`,
|
||||
name: "Server Side",
|
||||
},
|
||||
{
|
||||
href: `${paymentsSlug}/x402/agents`,
|
||||
name: "Agents",
|
||||
},
|
||||
{
|
||||
href: `${paymentsSlug}/x402/facilitator`,
|
||||
name: "Facilitator",
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import { OpenApiEndpoint, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "x402 Agents",
|
||||
icon: "payments",
|
||||
},
|
||||
title: "x402 Agents",
|
||||
description: "Easily create AI agents that can pay for any x402-compatible API calls.",
|
||||
});
|
||||
|
||||
# x402 Agents
|
||||
|
||||
Easily create AI agents that can pay for any x402-compatible API calls.
|
||||
|
||||
## Using the MCP Server
|
||||
|
||||
The easiest way to equip your agents with the ability to pay for API calls is to use the [remote MCP server](/ai/mcp) and provide the tools to your agent.
|
||||
|
||||
The MCP server comes with all the tools by default, but you can filter the tools available by passing a comma-separated list of tools as a query parameter.
|
||||
|
||||
In this example, we create a ReAct agent using LangChain and filter the MCP tools to `fetchWithPayment` and `getWalletBalance` as the only 2 tools we give our agent. You can view the full list of available tools [here](/ai/mcp#available-tools).
|
||||
|
||||
<Tabs defaultValue="typescript">
|
||||
|
||||
<TabsList>
|
||||
<TabsTrigger value="typescript">TypeScript</TabsTrigger>
|
||||
<TabsTrigger value="python">Python</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="typescript">
|
||||
|
||||
```typescript
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
|
||||
const model = new ChatOpenAI({
|
||||
model: "gpt-4.1",
|
||||
});
|
||||
|
||||
const mcpServers = {
|
||||
thirdweb: {
|
||||
url: `https://api.thirdweb.com/mcp?secretKey=<your-project-secret-key>&tools=fetchWithPayment,getWalletBalance`
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to MCP servers and get tools
|
||||
const client = new MultiServerMCPClient(mcpServers);
|
||||
const tools = await client.getTools();
|
||||
|
||||
const agent = createReactAgent({
|
||||
llm: model,
|
||||
tools: tools,
|
||||
prompt: "Use the fetchWithPayment tool to fetch any endpoint. Always pay in {{tokenAddress}}. Your wallet address is {{walletAddress}}."
|
||||
});
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="python">
|
||||
|
||||
```python
|
||||
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
client = MultiServerMCPClient(
|
||||
{
|
||||
"thirdweb": {
|
||||
"url": "https://api.thirdweb.com/mcp?secretKey=<your-project-secret-key>&tools=fetchWithPayment,getWalletBalance",
|
||||
}
|
||||
}
|
||||
)
|
||||
tools = await client.get_tools()
|
||||
agent = create_react_agent("openai:gpt-4.1", tools, prompt="Use the fetchWithPayment tool to fetch any endpoint. Always pay in {{tokenAddress}}. Your wallet address is {{walletAddress}}.")
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
## Using the API directly
|
||||
|
||||
You can also create your own MCP tool or wrap all external calls with the `/v1/x402/fetchWithPayment` endpoint to automatically handle payment flows when APIs return a `402 Payment Required` response.
|
||||
|
||||
Pass the target `url`, `method` and optional `from` wallet address to the endpoint that will complete the payment. The address should be one of your [user wallets](/wallets/users) or [server wallets](/wallets/server).
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.thirdweb.com/v1/payments/x402/fetch?url=https://api.example.com/premium&method=GET&from=0x1234... \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-secret-key: <your-project-secret-key>" \
|
||||
-d '{ ... }' # optional request body passed through to the url called.
|
||||
```
|
||||
|
||||
<OpenApiEndpoint path="/v1/payments/x402/fetch" method="POST" />
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent, OpenApiEndpoint } from "@doc";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent, OpenApiEndpoint, createMetadata } from "@doc";
|
||||
import { TypeScriptIcon, EngineIcon } from "@/icons";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "x402 Client",
|
||||
icon: "payments",
|
||||
},
|
||||
title: "x402 Client",
|
||||
description: "Make requests to any x402-compatible backend and automatically handle payment flows when APIs return a `402 Payment Required` response.",
|
||||
});
|
||||
|
||||
# Client Side
|
||||
|
||||
Make requests to any x402-compatible backend by automatically handling payment flows when APIs return a `402 Payment Required` response.
|
||||
Make requests to any x402-compatible backend and automatically handle payment flows when APIs return a `402 Payment Required` response.
|
||||
|
||||
The client library wraps the native `fetch` API and handles:
|
||||
1. Initial request to the API
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent, OpenApiEndpoint, Callout } from "@doc";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent, OpenApiEndpoint, Callout, createMetadata } from "@doc";
|
||||
import { TypeScriptIcon, EngineIcon } from "@/icons";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "x402 Facilitator",
|
||||
icon: "payments",
|
||||
},
|
||||
title: "x402 Facilitator",
|
||||
description: "Verify and submit x402 payments using your own server wallet.",
|
||||
});
|
||||
|
||||
# Facilitator
|
||||
|
||||
The facilitator is a service that handles verifying and submitting x402 payments. It uses your own [server wallet](/wallets/server) and leverages EIP-7702 to submit transactions gaslessly.
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { ArticleIconCard } from "@doc";
|
||||
import { ArticleIconCard, createMetadata } from "@doc";
|
||||
import { ReactIcon, TypeScriptIcon, EngineIcon } from "@/icons";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "x402 Payments",
|
||||
icon: "payments",
|
||||
},
|
||||
title: "x402 Payments",
|
||||
description: "Instant payments for your APIs, websites, and autonomous agents.",
|
||||
});
|
||||
|
||||
# x402 Payments
|
||||
|
||||
x402 is an open-source protocol that turns the dormant HTTP 402 Payment Required status code into a fully-featured, on-chain payment layer for APIs, websites, and autonomous agents.
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent, DocImage } from "@doc";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent, DocImage, createMetadata } from "@doc";
|
||||
import { Steps, Step } from "@doc";
|
||||
import PaymentFlow from "./x402-protocol-flow.png";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "x402 Server",
|
||||
icon: "payments",
|
||||
},
|
||||
title: "x402 Server",
|
||||
description: "Accept x402 payments in your APIs from any x402-compatible client.",
|
||||
});
|
||||
|
||||
# Server Side
|
||||
|
||||
Accept x402 payments in your APIs using any x402-compatible client. Your server can verify and settle payments using thirdweb's facilitator service or any custom facilitator.
|
||||
|
||||
@@ -0,0 +1,663 @@
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@doc";
|
||||
|
||||
# Session Keys Guide
|
||||
|
||||
Session keys enable secure transaction execution on behalf of accounts without requiring direct access to the main account's private key. This guide covers session keys for both ERC-4337 smart accounts and ERC-7702 smart EOAs (Externally Owned Accounts).
|
||||
|
||||
<Tabs defaultValue="erc7702">
|
||||
<TabsList>
|
||||
<TabsTrigger value="erc7702">EIP-7702 Upgraded EOAs</TabsTrigger>
|
||||
<TabsTrigger value="erc4337">ERC-4337 Smart Accounts</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="erc4337">
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have:
|
||||
- A thirdweb client configured
|
||||
- Access to a session key account address
|
||||
|
||||
## Setup
|
||||
|
||||
First, let's set up the necessary imports and configuration:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
generateAccount,
|
||||
smartWallet,
|
||||
sendTransaction,
|
||||
getContract,
|
||||
createThirdwebClient
|
||||
} from "thirdweb";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
import { getAllActiveSigners } from "thirdweb/extensions/erc4337";
|
||||
import { Engine } from "thirdweb/engine";
|
||||
|
||||
// Configure your client
|
||||
const client = createThirdwebClient({
|
||||
clientId: "your-client-id",
|
||||
secretKey: "your-secret-key" // Only use in server environments
|
||||
});
|
||||
|
||||
// Your session key account address
|
||||
const sessionKeyAccountAddress = "0x..."; // Replace with your session key address (server wallet)
|
||||
|
||||
// Target address for transactions
|
||||
const targetAddress = "0x..."; // Replace with your target address
|
||||
```
|
||||
|
||||
## Step 1: Configure User Smart Wallet with Session Key
|
||||
|
||||
The first step is to add our session key address as a signer to the user's smart account. This is typically done on the client side since it needs explicit user approval. This can be done by configuring the smart wallet with the session key address and permissions.
|
||||
|
||||
In a React application, this can be done by using the `ConnectButton` or `ConnectEmbed` component. This will automatically configure the smart wallet with the session key address and permissions.
|
||||
|
||||
```typescript
|
||||
<ConnectButton
|
||||
accountAbstraction={{
|
||||
chain: sepolia,
|
||||
sessionKey: {
|
||||
address: sessionKeyAccountAddress,
|
||||
permissions: {
|
||||
// "*" allows all targets, or specify specific contract addresses
|
||||
approvedTargets: "*",
|
||||
},
|
||||
},
|
||||
sponsorGas: true,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
This can also be done in pure TypeScript by using the `smartWallet` function and connecting it to a personal account.
|
||||
|
||||
For this guide, we'll generate a random personal account that will be used to create the smart wallet:
|
||||
|
||||
```typescript
|
||||
// this would be the user's personal account
|
||||
const personalAccount = await generateAccount({
|
||||
client: client,
|
||||
});
|
||||
|
||||
// wrapped in a smart wallet with session key permissions
|
||||
const smart = smartWallet({
|
||||
chain: sepolia,
|
||||
sessionKey: {
|
||||
address: sessionKeyAccountAddress,
|
||||
permissions: {
|
||||
// "*" allows all targets, or specify specific contract addresses
|
||||
approvedTargets: "*",
|
||||
},
|
||||
},
|
||||
sponsorGas: true, // Enable gas sponsorship
|
||||
});
|
||||
|
||||
console.log("Personal account created:", personalAccount.address);
|
||||
```
|
||||
|
||||
### Session Key Permissions
|
||||
|
||||
The `permissions` object allows you to control what the session key can do:
|
||||
|
||||
- `approvedTargets`: Specify which contract addresses the session key can interact with
|
||||
- Use `"*"` for all targets
|
||||
- Use an array of addresses for specific contracts: `["0x123...", "0x456..."]`
|
||||
|
||||
## Step 2: Connect Smart Account
|
||||
|
||||
Connect the smart wallet using the personal account:
|
||||
|
||||
```typescript
|
||||
const smartAccount = await smart.connect({
|
||||
client: client,
|
||||
personalAccount: personalAccount,
|
||||
});
|
||||
|
||||
console.log("Smart account address:", smartAccount.address);
|
||||
```
|
||||
|
||||
Note that in a React application, this would be done automatically by the `ConnectButton` or `ConnectEmbed` component.
|
||||
|
||||
## Step 3 (Optional): Verify Session Key Registration
|
||||
|
||||
Check that the session key is properly registered as an active signer:
|
||||
|
||||
```typescript
|
||||
const signers = await getAllActiveSigners({
|
||||
contract: getContract({
|
||||
address: smartAccount.address,
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
}),
|
||||
});
|
||||
|
||||
// Verify the session key is in the list of active signers
|
||||
const isSessionKeyActive = signers
|
||||
.map((s) => s.signer)
|
||||
.includes(sessionKeyAccountAddress);
|
||||
|
||||
console.log("Session key is active:", isSessionKeyActive);
|
||||
console.log("All active signers:", signers.map((s) => s.signer));
|
||||
```
|
||||
|
||||
## Step 4: Create Engine Server Wallet
|
||||
|
||||
Set up an Engine server wallet using the session key for transaction execution:
|
||||
|
||||
```typescript
|
||||
const serverWallet = Engine.serverWallet({
|
||||
address: sessionKeyAccountAddress,
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
executionOptions: {
|
||||
entrypointVersion: "0.6", // ERC-4337 entrypoint version
|
||||
signerAddress: sessionKeyAccountAddress,
|
||||
smartAccountAddress: smartAccount.address,
|
||||
type: "ERC4337",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Execution Options
|
||||
|
||||
- `entrypointVersion`: The ERC-4337 entrypoint version to use
|
||||
- `signerAddress`: The session key address that will sign transactions
|
||||
- `smartAccountAddress`: The smart account address that will execute transactions
|
||||
- `type`: The account abstraction type (ERC4337)
|
||||
|
||||
## Step 5: Execute Transactions
|
||||
|
||||
Now you can execute transactions using the session key, here we are sending 0 ETH to the target address for testing purposes:
|
||||
|
||||
```typescript
|
||||
const tx = await sendTransaction({
|
||||
account: serverWallet,
|
||||
transaction: {
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
to: targetAddress,
|
||||
value: 0n, // Amount in wei (0 for no ETH transfer)
|
||||
// data: "0x...", // Optional: contract call data
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Transaction sent:", tx.transactionHash);
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
Here's a complete example putting it all together:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
generateAccount,
|
||||
smartWallet,
|
||||
sendTransaction,
|
||||
getContract,
|
||||
createThirdwebClient
|
||||
} from "thirdweb";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
import { getAllActiveSigners } from "thirdweb/extensions/erc4337";
|
||||
import { Engine } from "thirdweb/engine";
|
||||
|
||||
async function executeTransactionWithSessionKey() {
|
||||
// Configuration
|
||||
const client = createThirdwebClient({
|
||||
clientId: "your-client-id",
|
||||
secretKey: "your-secret-key"
|
||||
});
|
||||
|
||||
const sessionKeyAccountAddress = "0x..."; // Your session key address (server wallet)
|
||||
const targetAddress = "0x..."; // Target address for the final transaction
|
||||
|
||||
try {
|
||||
// Step 1: Create personal account
|
||||
const personalAccount = await generateAccount({ client });
|
||||
|
||||
// Step 2: Configure smart wallet
|
||||
const smart = smartWallet({
|
||||
chain: sepolia,
|
||||
sessionKey: {
|
||||
address: sessionKeyAccountAddress,
|
||||
permissions: {
|
||||
approvedTargets: "*",
|
||||
},
|
||||
},
|
||||
sponsorGas: true,
|
||||
});
|
||||
|
||||
// Step 3: Connect smart account
|
||||
const smartAccount = await smart.connect({
|
||||
client,
|
||||
personalAccount,
|
||||
});
|
||||
|
||||
// Step 4: Verify session key
|
||||
const signers = await getAllActiveSigners({
|
||||
contract: getContract({
|
||||
address: smartAccount.address,
|
||||
chain: sepolia,
|
||||
client,
|
||||
}),
|
||||
});
|
||||
|
||||
const isSessionKeyActive = signers
|
||||
.map((s) => s.signer)
|
||||
.includes(sessionKeyAccountAddress);
|
||||
|
||||
if (!isSessionKeyActive) {
|
||||
throw new Error("Session key is not active");
|
||||
}
|
||||
|
||||
// Step 5: Create server wallet
|
||||
const serverWallet = Engine.serverWallet({
|
||||
address: sessionKeyAccountAddress,
|
||||
chain: sepolia,
|
||||
client,
|
||||
executionOptions: {
|
||||
entrypointVersion: "0.6",
|
||||
signerAddress: sessionKeyAccountAddress,
|
||||
smartAccountAddress: smartAccount.address,
|
||||
type: "ERC4337",
|
||||
},
|
||||
});
|
||||
|
||||
// Step 6: Execute transaction
|
||||
const tx = await sendTransaction({
|
||||
account: serverWallet,
|
||||
transaction: {
|
||||
chain: sepolia,
|
||||
client,
|
||||
to: targetAddress,
|
||||
value: 0n,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Transaction successful:", tx.transactionHash);
|
||||
return tx;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error executing transaction:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the function
|
||||
executeTransactionWithSessionKey()
|
||||
.then((tx) => console.log("Done!", tx.transactionHash))
|
||||
.catch((error) => console.error("Failed:", error));
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Session Key Storage**: Store session keys securely, preferably in a vault system
|
||||
- **Permission Scope**: Limit session key permissions to only necessary targets
|
||||
- **Key Rotation**: Regularly rotate session keys for enhanced security
|
||||
- **Monitoring**: Monitor session key usage for suspicious activity
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Session key not active**: Ensure the session key is properly registered with the smart account
|
||||
2. **Permission denied**: Check that the target address is included in `approvedTargets`
|
||||
3. **Gas estimation failed**: Verify that gas sponsorship is properly configured
|
||||
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="erc7702">
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have:
|
||||
- A thirdweb client configured
|
||||
- An in-app wallet with ERC-7702 support
|
||||
- A session key account address (can be generated or an existing wallet)
|
||||
|
||||
## Setup
|
||||
|
||||
First, let's set up the necessary imports and configuration:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createThirdwebClient,
|
||||
getContract,
|
||||
sendTransaction,
|
||||
prepareTransaction
|
||||
} from "thirdweb";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
import { inAppWallet, createSessionKey } from "thirdweb/wallets/in-app";
|
||||
|
||||
// Configure your client
|
||||
const client = createThirdwebClient({
|
||||
clientId: "your-client-id",
|
||||
secretKey: "your-secret-key" // Only use in server environments
|
||||
});
|
||||
|
||||
// Your session key account address
|
||||
const sessionKeyAccountAddress = "0x..."; // Replace with your session key address (server wallet)
|
||||
```
|
||||
|
||||
## Step 1: Create ERC-7702 Smart EOA
|
||||
|
||||
Create an in-app wallet configured with ERC-7702 execution mode:
|
||||
|
||||
```typescript
|
||||
const wallet = inAppWallet({
|
||||
executionMode: {
|
||||
mode: "EIP7702",
|
||||
sponsorGas: true, // Enable gas sponsorship
|
||||
},
|
||||
});
|
||||
|
||||
// Connect the wallet (user authentication)
|
||||
const account = await wallet.connect({
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
strategy: "google", // or "email", "guest", etc.
|
||||
});
|
||||
|
||||
console.log("Upgraded EOA address:", account.address);
|
||||
```
|
||||
|
||||
### Execution Modes
|
||||
|
||||
The `executionMode` option allows you to configure the wallet behavior:
|
||||
|
||||
- `mode`: Set to `"EIP7702"` for EIP-7702 upgraded EOA functionality
|
||||
- `sponsorGas`: Enable gas sponsorship for gasless transactions
|
||||
|
||||
## Step 2: Get Account Contract
|
||||
|
||||
Create a contract instance for the deployed smart EOA:
|
||||
|
||||
```typescript
|
||||
const accountContract = getContract({
|
||||
address: account.address,
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
});
|
||||
```
|
||||
|
||||
## Step 3: Create Session Key with Full Permissions
|
||||
|
||||
Add a session key with full execution permissions:
|
||||
|
||||
```typescript
|
||||
const tx = await sendTransaction({
|
||||
account: account,
|
||||
transaction: createSessionKey({
|
||||
account: account,
|
||||
contract: accountContract,
|
||||
sessionKeyAddress: sessionKeyAccountAddress,
|
||||
durationInSeconds: 86400, // 1 day
|
||||
grantFullPermissions: true, // Allow unrestricted access
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("Session key created:", tx.transactionHash);
|
||||
```
|
||||
|
||||
### Session Key Options
|
||||
|
||||
- `account`: The admin account that will create the session key
|
||||
- `contract`: The smart EOA contract instance
|
||||
- `sessionKeyAddress`: The address that will be granted session key permissions
|
||||
- `durationInSeconds`: How long the session key should be valid (in seconds)
|
||||
- `grantFullPermissions`: If `true`, grants unrestricted access. If `false`, you must specify policies.
|
||||
|
||||
## Step 4 (Advanced): Create Session Key with Granular Permissions
|
||||
|
||||
For more fine-grained control, you can specify call and transfer policies:
|
||||
|
||||
```typescript
|
||||
import { Condition, LimitType } from "thirdweb/wallets/in-app";
|
||||
|
||||
const tx = await sendTransaction({
|
||||
account: account,
|
||||
transaction: createSessionKey({
|
||||
account: account,
|
||||
contract: accountContract,
|
||||
sessionKeyAddress: sessionKeyAccountAddress,
|
||||
durationInSeconds: 86400, // 1 day
|
||||
grantFullPermissions: false, // Use granular permissions
|
||||
callPolicies: [
|
||||
{
|
||||
target: "0x123...", // Target contract address
|
||||
selector: "0xa9059cbb", // Function selector (e.g., ERC20 transfer)
|
||||
maxValuePerUse: 0n, // Max ETH value per call
|
||||
valueLimit: {
|
||||
limitType: LimitType.Lifetime, // Lifetime limit
|
||||
limit: 1000000n, // Max total value in wei
|
||||
period: 0n, // Not used for lifetime limits
|
||||
},
|
||||
constraints: [
|
||||
{
|
||||
condition: Condition.Equal, // Constraint type
|
||||
index: 0n, // Parameter index
|
||||
refValue: "0x000000000000000000000000abc...", // Expected value
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
transferPolicies: [
|
||||
{
|
||||
target: "0x456...", // Recipient address for transfers
|
||||
maxValuePerUse: 100000n, // Max wei per transfer
|
||||
valueLimit: {
|
||||
limitType: LimitType.Allowance, // Allowance-based limit
|
||||
limit: 1000000n, // Total allowance in wei
|
||||
period: 86400n, // Reset period in seconds
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("Session key with policies created:", tx.transactionHash);
|
||||
```
|
||||
|
||||
### Permission Types
|
||||
|
||||
#### Call Policies
|
||||
|
||||
Control which smart contract calls the session key can make:
|
||||
|
||||
- `target`: The contract address the session key can call
|
||||
- `selector`: The function selector (4-byte identifier) that can be called
|
||||
- `maxValuePerUse`: Maximum ETH value that can be sent per transaction
|
||||
- `valueLimit`: Total value limits over time
|
||||
- `constraints`: Parameter-level constraints for function calls
|
||||
|
||||
#### Transfer Policies
|
||||
|
||||
Control native token transfers the session key can make:
|
||||
|
||||
- `target`: The recipient address for transfers
|
||||
- `maxValuePerUse`: Maximum wei per individual transfer
|
||||
- `valueLimit`: Total transfer limits over time
|
||||
|
||||
#### Limit Types
|
||||
|
||||
```typescript
|
||||
enum LimitType {
|
||||
Unlimited = 0, // No limit
|
||||
Lifetime = 1, // One-time total limit
|
||||
Allowance = 2, // Renewable allowance based on period
|
||||
}
|
||||
```
|
||||
|
||||
#### Constraint Conditions
|
||||
|
||||
```typescript
|
||||
enum Condition {
|
||||
Unconstrained = 0, // No constraint
|
||||
Equal = 1, // Must equal reference value
|
||||
Greater = 2, // Must be greater than reference value
|
||||
Less = 3, // Must be less than reference value
|
||||
GreaterOrEqual = 4, // Must be >= reference value
|
||||
LessOrEqual = 5, // Must be <= reference value
|
||||
NotEqual = 6, // Must not equal reference value
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: Use the Session Key
|
||||
|
||||
Once created, the session key can be used with Engine or other server-side execution:
|
||||
|
||||
```typescript
|
||||
import { Engine } from "thirdweb/engine";
|
||||
|
||||
const serverWallet = Engine.serverWallet({
|
||||
address: sessionKeyAccountAddress,
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
executionOptions: {
|
||||
type: "EIP7702",
|
||||
sessionKeyAddress: sessionKeyAccountAddress,
|
||||
accountAddress: account.address,
|
||||
},
|
||||
});
|
||||
|
||||
// Execute transactions with the session key
|
||||
const tx = await sendTransaction({
|
||||
account: serverWallet,
|
||||
transaction: prepareTransaction({
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
to: "0x...", // Target address
|
||||
value: 0n,
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("Transaction executed:", tx.transactionHash);
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
Here's a complete example for setting up and using a session key with ERC-7702:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createThirdwebClient,
|
||||
getContract,
|
||||
sendTransaction,
|
||||
prepareTransaction
|
||||
} from "thirdweb";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
import { inAppWallet, createSessionKey } from "thirdweb/wallets/in-app";
|
||||
import { Engine } from "thirdweb/engine";
|
||||
|
||||
async function setupSessionKeyWith7702() {
|
||||
// Configuration
|
||||
const client = createThirdwebClient({
|
||||
clientId: "your-client-id",
|
||||
secretKey: "your-secret-key"
|
||||
});
|
||||
|
||||
const sessionKeyAccountAddress = "0x..."; // Your session key address (server wallet)
|
||||
|
||||
try {
|
||||
// Step 1: Create 7702 Smart EOA
|
||||
const wallet = inAppWallet({
|
||||
executionMode: {
|
||||
mode: "EIP7702",
|
||||
sponsorGas: true,
|
||||
},
|
||||
});
|
||||
|
||||
const account = await wallet.connect({
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
strategy: "google",
|
||||
});
|
||||
|
||||
console.log("Upgraded EOA created:", account.address);
|
||||
|
||||
// Step 2: Get account contract
|
||||
const accountContract = getContract({
|
||||
address: account.address,
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
});
|
||||
|
||||
// Step 3: Create session key
|
||||
const sessionTx = await sendTransaction({
|
||||
account: account,
|
||||
transaction: createSessionKey({
|
||||
account: account,
|
||||
contract: accountContract,
|
||||
sessionKeyAddress: sessionKeyAccountAddress,
|
||||
durationInSeconds: 86400, // 1 day
|
||||
grantFullPermissions: true,
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("Session key created:", sessionTx.transactionHash);
|
||||
|
||||
// Step 4: Use the session key with Engine
|
||||
const serverWallet = Engine.serverWallet({
|
||||
address: sessionKeyAccountAddress,
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
executionOptions: {
|
||||
type: "EIP7702",
|
||||
sessionKeyAddress: sessionKeyAccountAddress,
|
||||
accountAddress: account.address,
|
||||
},
|
||||
});
|
||||
|
||||
// Execute a transaction with the session key
|
||||
const tx = await sendTransaction({
|
||||
account: serverWallet,
|
||||
transaction: prepareTransaction({
|
||||
chain: sepolia,
|
||||
client: client,
|
||||
to: "0x...", // Target address
|
||||
value: 0n,
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("Transaction successful:", tx.transactionHash);
|
||||
return tx;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the function
|
||||
setupSessionKeyWith7702()
|
||||
.then((tx) => console.log("Done!", tx.transactionHash))
|
||||
.catch((error) => console.error("Failed:", error));
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Session Key Storage**: Store session keys securely, preferably in a vault system
|
||||
- **Permission Scope**: Use granular permissions (`grantFullPermissions: false`) for production use cases
|
||||
- **Time Limits**: Set appropriate `durationInSeconds` values based on your use case
|
||||
- **Policy Design**: Carefully design call and transfer policies to minimize risk
|
||||
- **Key Rotation**: Regularly rotate session keys and revoke unused ones
|
||||
- **Monitoring**: Monitor session key usage for suspicious activity
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Account not deployed**: Ensure you've sent a transaction to trigger the account deployment/upgrade
|
||||
2. **Invalid signature**: Make sure the admin account is properly authenticated and can sign typed data
|
||||
3. **Permission denied**: Verify the session key has the necessary policies for the operation
|
||||
4. **Expired session key**: Check that the current time is within the session key's validity period
|
||||
|
||||
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Learn more about [In-App Wallets](/wallets/users)
|
||||
- Explore the [API Reference](/reference)
|
||||
- Check out the [TypeScript SDK](/references/typescript/v5) documentation
|
||||
@@ -65,6 +65,10 @@ export const sidebar: SideBar = {
|
||||
href: `${walletSlug}/export-private-key`,
|
||||
name: "Export Private Keys",
|
||||
},
|
||||
{
|
||||
href: `${walletSlug}/session-keys`,
|
||||
name: "Session Keys",
|
||||
},
|
||||
{
|
||||
href: `${walletSlug}/adapters`,
|
||||
name: "Usage with other libraries",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# wagmi-inapp
|
||||
|
||||
## 0.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`f630912`](https://github.com/thirdweb-dev/js/commit/f630912b9b0e1d8ebe22b0b52c650f5e40e12baf), [`62cfbb7`](https://github.com/thirdweb-dev/js/commit/62cfbb7441e66156c55c76d73f5b481f5ea576bc)]:
|
||||
- thirdweb@5.108.10
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.160
|
||||
|
||||
## 0.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`968913d`](https://github.com/thirdweb-dev/js/commit/968913d55b761c1e23062fdf6008904fa8c17050)]:
|
||||
- thirdweb@5.108.9
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.159
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wagmi-inapp",
|
||||
"private": true,
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @thirdweb-dev/ai-sdk-provider
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8211](https://github.com/thirdweb-dev/js/pull/8211) [`71a1e58`](https://github.com/thirdweb-dev/js/commit/71a1e58eafa6d51f0a3eb9c7a3fcc685465c72b5) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Default in-memory session store across thirdweb AI instances, option to pass your own
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -56,5 +56,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.1.3"
|
||||
"version": "0.1.4"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
export { createThirdwebAI, type ThirdwebProvider } from "../provider.js";
|
||||
export {
|
||||
createThirdwebAI,
|
||||
type SessionStore,
|
||||
type ThirdwebProvider,
|
||||
} from "../provider.js";
|
||||
export type {
|
||||
MonitorTransactionInput,
|
||||
MonitorTransactionOutput,
|
||||
|
||||
@@ -544,7 +544,7 @@ export class ThirdwebProvider implements ProviderV2 {
|
||||
|
||||
constructor(config: ThirdwebConfig = {}) {
|
||||
this.config = config;
|
||||
this.session = new SessionStore();
|
||||
this.session = config.sessionStore || memorySessionStore;
|
||||
}
|
||||
|
||||
chat(id?: string, settings: ThirdwebSettings = {}) {
|
||||
@@ -581,22 +581,29 @@ export class ThirdwebProvider implements ProviderV2 {
|
||||
}
|
||||
}
|
||||
|
||||
class SessionStore {
|
||||
export interface SessionStore {
|
||||
getSessionId(chatId: string): string | null;
|
||||
setSessionId(chatId: string, sessionId: string): void;
|
||||
clearSessionId(chatId: string): void;
|
||||
}
|
||||
|
||||
class InMemorySessionStore implements SessionStore {
|
||||
private sessionId: Map<string, string> = new Map();
|
||||
|
||||
getSessionId(chatId: string) {
|
||||
getSessionId(chatId: string): string | null {
|
||||
return this.sessionId.get(chatId) || null;
|
||||
}
|
||||
|
||||
setSessionId(chatId: string, sessionId: string) {
|
||||
setSessionId(chatId: string, sessionId: string): void {
|
||||
this.sessionId.set(chatId, sessionId);
|
||||
}
|
||||
|
||||
clearSessionId(chatId: string) {
|
||||
clearSessionId(chatId: string): void {
|
||||
this.sessionId.delete(chatId);
|
||||
}
|
||||
}
|
||||
|
||||
// singleton session store used as default if no session store is provided
|
||||
const memorySessionStore = new InMemorySessionStore();
|
||||
|
||||
// Factory function for easier usage
|
||||
export function createThirdwebAI(config: ThirdwebConfig = {}) {
|
||||
return new ThirdwebProvider(config);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { UIDataTypes, UIMessage } from "ai";
|
||||
import type { SessionStore } from "./provider.js";
|
||||
import type {
|
||||
MonitorTransactionInput,
|
||||
MonitorTransactionOutput,
|
||||
@@ -26,6 +27,11 @@ export interface ThirdwebConfig {
|
||||
* Optional: The user wallet auth token (JWT) for executing transactions
|
||||
*/
|
||||
walletAuthToken?: string;
|
||||
/**
|
||||
* Optional: The session store to use for storing and retrieving thirdweb AI session IDs from ai SDK chat ids.
|
||||
* If not provided, a default in-memory session store will be used.
|
||||
*/
|
||||
sessionStore?: SessionStore;
|
||||
}
|
||||
|
||||
export interface ThirdwebSettings {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @thirdweb-dev/engine
|
||||
|
||||
## 3.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8218](https://github.com/thirdweb-dev/js/pull/8218) [`f630912`](https://github.com/thirdweb-dev/js/commit/f630912b9b0e1d8ebe22b0b52c650f5e40e12baf) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Update to latest API spec
|
||||
|
||||
## 3.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/engine",
|
||||
"version": "3.2.1",
|
||||
"version": "3.2.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
|
||||
@@ -148,6 +148,9 @@ export type BatchResultItemEncodeResultSuccessItemEngineError =
|
||||
message: string;
|
||||
type: "THIRDWEB_ERROR";
|
||||
}
|
||||
| (SerialisableAwsSignerError & {
|
||||
type: "AWS_KMS_SIGNER_ERROR";
|
||||
})
|
||||
| {
|
||||
message: string;
|
||||
type: "INTERNAL_ERROR";
|
||||
@@ -235,6 +238,9 @@ export type BatchResultItemReadResultSuccessItemEngineError =
|
||||
message: string;
|
||||
type: "THIRDWEB_ERROR";
|
||||
}
|
||||
| (SerialisableAwsSignerError & {
|
||||
type: "AWS_KMS_SIGNER_ERROR";
|
||||
})
|
||||
| {
|
||||
message: string;
|
||||
type: "INTERNAL_ERROR";
|
||||
@@ -331,6 +337,9 @@ export type BatchResultItemSignResultDataEngineError =
|
||||
message: string;
|
||||
type: "THIRDWEB_ERROR";
|
||||
}
|
||||
| (SerialisableAwsSignerError & {
|
||||
type: "AWS_KMS_SIGNER_ERROR";
|
||||
})
|
||||
| {
|
||||
message: string;
|
||||
type: "INTERNAL_ERROR";
|
||||
@@ -472,13 +481,34 @@ export type ContractWrite = ContractCall & {
|
||||
/**
|
||||
* EIP-7702 Execution Options
|
||||
*/
|
||||
export type Eip7702ExecutionOptions = {
|
||||
export type Eip7702ExecutionOptions =
|
||||
| Eip7702OwnerExecution
|
||||
| Eip7702SessionKeyExecution;
|
||||
|
||||
/**
|
||||
* EIP-7702 Owner Execution
|
||||
*/
|
||||
export type Eip7702OwnerExecution = {
|
||||
/**
|
||||
* The EOA address that will sign the EIP-7702 transaction
|
||||
* The delegated EOA address
|
||||
*/
|
||||
from: AddressDef;
|
||||
};
|
||||
|
||||
/**
|
||||
* EIP-7702 Session Key Execution
|
||||
*/
|
||||
export type Eip7702SessionKeyExecution = {
|
||||
/**
|
||||
* The session key address is your server wallet, which has been granted a session key to the `account_address`
|
||||
*/
|
||||
sessionKeyAddress: AddressDef;
|
||||
/**
|
||||
* The account address is the address of a delegated account you want to execute the transaction on. This account has granted a session key to the `session_key_address`
|
||||
*/
|
||||
accountAddress: AddressDef;
|
||||
};
|
||||
|
||||
export type EmptyIdempotencySetResponse = {
|
||||
queueName: string;
|
||||
message: string;
|
||||
@@ -889,6 +919,64 @@ export type SendTransactionRequest = {
|
||||
webhookOptions?: Array<WebhookOptions>;
|
||||
};
|
||||
|
||||
export type SerialisableAwsSdkError =
|
||||
| {
|
||||
message: string;
|
||||
type: "CONSTRUCTION_FAILURE";
|
||||
}
|
||||
| {
|
||||
message: string;
|
||||
type: "TIMEOUT_ERROR";
|
||||
}
|
||||
| {
|
||||
message: string;
|
||||
type: "DISPATCH_FAILURE";
|
||||
}
|
||||
| {
|
||||
message: string;
|
||||
type: "RESPONSE_ERROR";
|
||||
}
|
||||
| {
|
||||
message: string;
|
||||
type: "SERVICE_ERROR";
|
||||
}
|
||||
| {
|
||||
message: string;
|
||||
type: "OTHER";
|
||||
};
|
||||
|
||||
export type SerialisableAwsSignerError =
|
||||
| {
|
||||
aws_sdk_error: SerialisableAwsSdkError;
|
||||
type: "SIGN";
|
||||
}
|
||||
| {
|
||||
aws_sdk_error: SerialisableAwsSdkError;
|
||||
type: "GET_PUBLIC_KEY";
|
||||
}
|
||||
| {
|
||||
message: string;
|
||||
type: "K256";
|
||||
}
|
||||
| {
|
||||
message: string;
|
||||
type: "SPKI";
|
||||
}
|
||||
| {
|
||||
message: string;
|
||||
type: "HEX";
|
||||
}
|
||||
| {
|
||||
type: "SIGNATURE_NOT_FOUND";
|
||||
}
|
||||
| {
|
||||
type: "PUBLIC_KEY_NOT_FOUND";
|
||||
}
|
||||
| {
|
||||
message: string;
|
||||
type: "UNKNOWN";
|
||||
};
|
||||
|
||||
export type SerializableReqwestError =
|
||||
| {
|
||||
Builder: {
|
||||
@@ -1552,6 +1640,7 @@ export type GetTransactionsResponses = {
|
||||
}
|
||||
| Array<unknown>;
|
||||
transactionHash: string | null;
|
||||
status: string | null;
|
||||
confirmedAt: string | null;
|
||||
confirmedAtBlockNumber: string | null;
|
||||
enrichedData:
|
||||
@@ -1712,6 +1801,7 @@ export type SearchTransactionsResponses = {
|
||||
}
|
||||
| Array<unknown>;
|
||||
transactionHash: string | null;
|
||||
status: string | null;
|
||||
confirmedAt: string | null;
|
||||
confirmedAtBlockNumber: string | null;
|
||||
enrichedData:
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @thirdweb-dev/nebula
|
||||
|
||||
## 0.2.63
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`f630912`](https://github.com/thirdweb-dev/js/commit/f630912b9b0e1d8ebe22b0b52c650f5e40e12baf), [`62cfbb7`](https://github.com/thirdweb-dev/js/commit/62cfbb7441e66156c55c76d73f5b481f5ea576bc)]:
|
||||
- thirdweb@5.108.10
|
||||
|
||||
## 0.2.62
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`968913d`](https://github.com/thirdweb-dev/js/commit/968913d55b761c1e23062fdf6008904fa8c17050)]:
|
||||
- thirdweb@5.108.9
|
||||
|
||||
## 0.2.61
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -57,5 +57,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.61"
|
||||
"version": "0.2.63"
|
||||
}
|
||||
|
||||
@@ -241,6 +241,7 @@ export type ProjectService =
|
||||
rotationCode?: string | null;
|
||||
encryptedAdminKey?: string | null;
|
||||
encryptedWalletAccessToken?: string | null;
|
||||
projectWalletAddress?: string | null;
|
||||
}
|
||||
| ProjectBundlerService
|
||||
| ProjectEmbeddedWalletsService;
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.108.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8218](https://github.com/thirdweb-dev/js/pull/8218) [`f630912`](https://github.com/thirdweb-dev/js/commit/f630912b9b0e1d8ebe22b0b52c650f5e40e12baf) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Export missing x402 types
|
||||
|
||||
- [#8215](https://github.com/thirdweb-dev/js/pull/8215) [`62cfbb7`](https://github.com/thirdweb-dev/js/commit/62cfbb7441e66156c55c76d73f5b481f5ea576bc) Thanks [@MananTank](https://github.com/MananTank)! - Show error in BuyWidget and SwapWidget UI if fetching token details fails
|
||||
|
||||
- Updated dependencies [[`f630912`](https://github.com/thirdweb-dev/js/commit/f630912b9b0e1d8ebe22b0b52c650f5e40e12baf)]:
|
||||
- @thirdweb-dev/engine@3.2.2
|
||||
|
||||
## 5.108.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8192](https://github.com/thirdweb-dev/js/pull/8192) [`968913d`](https://github.com/thirdweb-dev/js/commit/968913d55b761c1e23062fdf6008904fa8c17050) Thanks [@MananTank](https://github.com/MananTank)! - Fix TransactionWidget not updating when `currency` prop is changed after initial render
|
||||
|
||||
## 5.108.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -430,5 +430,5 @@
|
||||
}
|
||||
},
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "5.108.8"
|
||||
"version": "5.108.10"
|
||||
}
|
||||
|
||||
@@ -189,8 +189,8 @@ export function serverWallet(options: ServerWalletOptions): ServerWallet {
|
||||
};
|
||||
case "EIP7702":
|
||||
return {
|
||||
...executionOptions,
|
||||
chainId,
|
||||
from: address,
|
||||
type: "EIP7702",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export type { Money, PaymentMiddlewareConfig } from "x402/types";
|
||||
export { decodePayment, encodePayment } from "../x402/encode.js";
|
||||
export {
|
||||
facilitator,
|
||||
|
||||
@@ -9,7 +9,6 @@ import { getCompilerMetadata } from "../../../contract/actions/get-compiler-meta
|
||||
import { getContract } from "../../../contract/contract.js";
|
||||
import { decimals } from "../../../extensions/erc20/read/decimals.js";
|
||||
import { getToken } from "../../../pay/convert/get-token.js";
|
||||
import type { SupportedFiatCurrency } from "../../../pay/convert/type.js";
|
||||
import { encode } from "../../../transaction/actions/encode.js";
|
||||
import type { PreparedTransaction } from "../../../transaction/prepare-transaction.js";
|
||||
import { getTransactionGasCost } from "../../../transaction/utils.js";
|
||||
@@ -18,10 +17,7 @@ import { resolvePromisedValue } from "../../../utils/promise/resolve-promised-va
|
||||
import { toTokens } from "../../../utils/units.js";
|
||||
import type { Wallet } from "../../../wallets/interfaces/wallet.js";
|
||||
import { hasSponsoredTransactionsEnabled } from "../../../wallets/smart/is-smart-wallet.js";
|
||||
import {
|
||||
formatCurrencyAmount,
|
||||
formatTokenAmount,
|
||||
} from "../../web/ui/ConnectWallet/screens/formatTokenBalance.js";
|
||||
import { formatTokenAmount } from "../../web/ui/ConnectWallet/screens/formatTokenBalance.js";
|
||||
import { useChainMetadata } from "./others/useChainQuery.js";
|
||||
|
||||
interface TransactionDetails {
|
||||
@@ -31,7 +27,6 @@ interface TransactionDetails {
|
||||
selector: string;
|
||||
description?: string;
|
||||
};
|
||||
usdValueDisplay: string | null;
|
||||
txCostDisplay: string;
|
||||
gasCostDisplay: string | null;
|
||||
tokenInfo: TokenWithPrices | null;
|
||||
@@ -45,7 +40,6 @@ interface UseTransactionDetailsOptions {
|
||||
transaction: PreparedTransaction;
|
||||
client: ThirdwebClient;
|
||||
wallet: Wallet | undefined;
|
||||
currency: SupportedFiatCurrency | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +48,6 @@ interface UseTransactionDetailsOptions {
|
||||
*/
|
||||
export function useTransactionDetails({
|
||||
transaction,
|
||||
currency,
|
||||
client,
|
||||
wallet,
|
||||
}: UseTransactionDetailsOptions) {
|
||||
@@ -158,9 +151,6 @@ export function useTransactionDetails({
|
||||
: (value || 0n) + (gasCostWei || 0n);
|
||||
const totalCost = toTokens(totalCostWei, decimal);
|
||||
|
||||
const price = tokenInfo?.prices[currency || "USD"] || 0;
|
||||
const usdValue = price ? Number(totalCost) * price : null;
|
||||
|
||||
return {
|
||||
contractMetadata,
|
||||
costWei,
|
||||
@@ -173,9 +163,6 @@ export function useTransactionDetails({
|
||||
totalCost,
|
||||
totalCostWei,
|
||||
txCostDisplay: `${formatTokenAmount(costWei, decimal)} ${tokenSymbol}`,
|
||||
usdValueDisplay: usdValue
|
||||
? formatCurrencyAmount(currency || "USD", usdValue)
|
||||
: null,
|
||||
};
|
||||
},
|
||||
queryKey: [
|
||||
|
||||
@@ -251,11 +251,14 @@ export function FundWallet(props: FundWalletProps) {
|
||||
? tokenQuery.data.token
|
||||
: undefined,
|
||||
isFetching: tokenQuery.isFetching,
|
||||
isError:
|
||||
tokenQuery.isError ||
|
||||
tokenQuery.data?.type === "unsupported_token",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
balance={{
|
||||
data: tokenBalanceQuery.data?.value,
|
||||
data: tokenBalanceQuery.data,
|
||||
isFetching: tokenBalanceQuery.isFetching,
|
||||
}}
|
||||
client={props.client}
|
||||
@@ -279,6 +282,22 @@ export function FundWallet(props: FundWalletProps) {
|
||||
|
||||
<Spacer y="md" />
|
||||
|
||||
{(tokenQuery.isError ||
|
||||
tokenQuery.data?.type === "unsupported_token") && (
|
||||
<div
|
||||
style={{
|
||||
border: `1px solid ${theme.colors.borderColor}`,
|
||||
borderRadius: radius.full,
|
||||
padding: spacing.xs,
|
||||
marginBottom: spacing.md,
|
||||
}}
|
||||
>
|
||||
<Text size="sm" color="danger" center>
|
||||
Failed to fetch token details
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Continue Button */}
|
||||
{activeWalletInfo ? (
|
||||
<Button
|
||||
@@ -373,6 +392,7 @@ function TokenSection(props: {
|
||||
| {
|
||||
data: TokenWithPrices | undefined;
|
||||
isFetching: boolean;
|
||||
isError: boolean;
|
||||
}
|
||||
| undefined;
|
||||
currency: SupportedFiatCurrency;
|
||||
@@ -381,7 +401,14 @@ function TokenSection(props: {
|
||||
title: string;
|
||||
isConnected: boolean;
|
||||
balance: {
|
||||
data: bigint | undefined;
|
||||
data:
|
||||
| {
|
||||
value: bigint;
|
||||
decimals: number;
|
||||
symbol: string;
|
||||
name: string;
|
||||
}
|
||||
| undefined;
|
||||
isFetching: boolean;
|
||||
};
|
||||
onWalletClick: () => void;
|
||||
@@ -565,17 +592,16 @@ function TokenSection(props: {
|
||||
<Text size="xs" color="secondaryText">
|
||||
Current Balance
|
||||
</Text>
|
||||
{props.balance.data === undefined ||
|
||||
props.selectedToken.data === undefined ? (
|
||||
{props.balance.data === undefined ? (
|
||||
<Skeleton height={fontSize.xs} width="100px" />
|
||||
) : (
|
||||
<Text size="xs" color="primaryText">
|
||||
{formatTokenAmount(
|
||||
props.balance.data,
|
||||
props.selectedToken.data.decimals,
|
||||
props.balance.data.value,
|
||||
props.balance.data.decimals,
|
||||
5,
|
||||
)}{" "}
|
||||
{props.selectedToken.data.symbol}
|
||||
{props.balance.data.symbol}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useActiveAccount } from "../../../core/hooks/wallets/useActiveAccount.j
|
||||
import { useActiveWallet } from "../../../core/hooks/wallets/useActiveWallet.js";
|
||||
import { ConnectButton } from "../ConnectWallet/ConnectButton.js";
|
||||
import { PoweredByThirdweb } from "../ConnectWallet/PoweredByTW.js";
|
||||
import { formatCurrencyAmount } from "../ConnectWallet/screens/formatTokenBalance.js";
|
||||
import { Container, Line } from "../components/basic.js";
|
||||
import { Button } from "../components/buttons.js";
|
||||
import { ChainName } from "../components/ChainName.js";
|
||||
@@ -100,7 +101,6 @@ export function TransactionPayment({
|
||||
client,
|
||||
transaction: transaction,
|
||||
wallet,
|
||||
currency: currency,
|
||||
});
|
||||
|
||||
// We can't use useWalletBalance here because erc20Value is a possibly async value
|
||||
@@ -134,6 +134,19 @@ export function TransactionPayment({
|
||||
|
||||
const buttonLabel = _buttonLabel || `Execute ${functionName}`;
|
||||
|
||||
const tokenFiatPricePerToken =
|
||||
transactionDataQuery.data?.tokenInfo?.prices[currency] || undefined;
|
||||
|
||||
const totalFiatCost =
|
||||
tokenFiatPricePerToken && transactionDataQuery.data
|
||||
? tokenFiatPricePerToken * Number(transactionDataQuery.data.totalCost)
|
||||
: undefined;
|
||||
|
||||
const costToDisplay =
|
||||
totalFiatCost !== undefined
|
||||
? formatCurrencyAmount(currency, totalFiatCost)
|
||||
: transactionDataQuery.data?.txCostDisplay;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<WithHeader
|
||||
@@ -207,8 +220,7 @@ export function TransactionPayment({
|
||||
>
|
||||
{/* USD Value */}
|
||||
<Text color="primaryText" size="xl" weight={600}>
|
||||
{transactionDataQuery.data?.usdValueDisplay ||
|
||||
transactionDataQuery.data?.txCostDisplay}
|
||||
{costToDisplay}
|
||||
</Text>
|
||||
|
||||
{/* Function Name */}
|
||||
|
||||
@@ -770,6 +770,9 @@ function TransactionWidgetContent(
|
||||
if (screen.id === "execute-tx") {
|
||||
return (
|
||||
<ExecutingTxScreen
|
||||
onBack={() => {
|
||||
setScreen({ id: "init-ui" });
|
||||
}}
|
||||
closeModal={() => {
|
||||
setScreen({ id: "init-ui" });
|
||||
}}
|
||||
|
||||
@@ -21,6 +21,7 @@ export function SelectedTokenButton(props: {
|
||||
| {
|
||||
data: TokenWithPrices | undefined;
|
||||
isFetching: boolean;
|
||||
isError: boolean;
|
||||
}
|
||||
| undefined;
|
||||
client: ThirdwebClient;
|
||||
@@ -47,7 +48,7 @@ export function SelectedTokenButton(props: {
|
||||
{/* icons */}
|
||||
<Container relative color="secondaryText">
|
||||
{/* token icon */}
|
||||
{props.selectedToken ? (
|
||||
{props.selectedToken && !props.selectedToken.isError ? (
|
||||
<Img
|
||||
key={props.selectedToken?.data?.iconUri}
|
||||
src={
|
||||
@@ -112,7 +113,7 @@ export function SelectedTokenButton(props: {
|
||||
</Container>
|
||||
|
||||
{/* token symbol and chain name */}
|
||||
{props.selectedToken ? (
|
||||
{props.selectedToken && !props.selectedToken.isError ? (
|
||||
<Container flex="column" style={{ gap: "3px" }}>
|
||||
{props.selectedToken?.isFetching ? (
|
||||
<Skeleton width="60px" height={fontSize.md} />
|
||||
|
||||
@@ -199,7 +199,6 @@ export function PaymentOverview(props: {
|
||||
<TransactionOverViewCompact
|
||||
client={props.client}
|
||||
paymentMethod={props.paymentMethod}
|
||||
currency={props.currency}
|
||||
transaction={props.modeInfo.transaction}
|
||||
metadata={props.metadata}
|
||||
/>
|
||||
@@ -211,7 +210,6 @@ export function PaymentOverview(props: {
|
||||
|
||||
const TransactionOverViewCompact = (props: {
|
||||
transaction: PreparedTransaction;
|
||||
currency: SupportedFiatCurrency;
|
||||
paymentMethod: PaymentMethod;
|
||||
client: ThirdwebClient;
|
||||
metadata: {
|
||||
@@ -224,7 +222,6 @@ const TransactionOverViewCompact = (props: {
|
||||
client: props.client,
|
||||
transaction: props.transaction,
|
||||
wallet: props.paymentMethod.payerWallet,
|
||||
currency: props.currency,
|
||||
});
|
||||
|
||||
if (!txInfo.data) {
|
||||
|
||||
@@ -96,6 +96,7 @@ function useTokenPrice(options: {
|
||||
);
|
||||
},
|
||||
refetchOnMount: false,
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
@@ -320,6 +321,7 @@ export function SwapUI(props: SwapUIProps) {
|
||||
? {
|
||||
data: sellTokenQuery.data,
|
||||
isFetching: sellTokenQuery.isFetching,
|
||||
isError: sellTokenQuery.isError,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
@@ -372,6 +374,7 @@ export function SwapUI(props: SwapUIProps) {
|
||||
? {
|
||||
data: buyTokenQuery.data,
|
||||
isFetching: buyTokenQuery.isFetching,
|
||||
isError: buyTokenQuery.isError,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
@@ -389,7 +392,9 @@ export function SwapUI(props: SwapUIProps) {
|
||||
/>
|
||||
|
||||
{/* error message */}
|
||||
{preparedResultQuery.error ? (
|
||||
{preparedResultQuery.error ||
|
||||
buyTokenQuery.isError ||
|
||||
sellTokenQuery.isError ? (
|
||||
<Text
|
||||
size="sm"
|
||||
color="danger"
|
||||
@@ -398,7 +403,13 @@ export function SwapUI(props: SwapUIProps) {
|
||||
paddingBlock: spacing.md,
|
||||
}}
|
||||
>
|
||||
Failed to get a quote
|
||||
{preparedResultQuery.error
|
||||
? "Failed to get a quote"
|
||||
: buyTokenQuery.isError
|
||||
? "Failed to fetch buy token details"
|
||||
: sellTokenQuery.isError
|
||||
? "Failed to fetch sell token details"
|
||||
: "Failed to get a quote"}
|
||||
</Text>
|
||||
) : (
|
||||
<Spacer y="md" />
|
||||
@@ -626,6 +637,7 @@ function TokenSection(props: {
|
||||
| {
|
||||
data: TokenWithPrices | undefined;
|
||||
isFetching: boolean;
|
||||
isError: boolean;
|
||||
}
|
||||
| undefined;
|
||||
currency: SupportedFiatCurrency;
|
||||
|
||||
@@ -20,7 +20,7 @@ export function ExecutingTxScreen(props: {
|
||||
tx: PreparedTransaction;
|
||||
closeModal: () => void;
|
||||
onTxSent: (data: WaitForReceiptOptions) => void;
|
||||
onBack?: () => void;
|
||||
onBack: (() => void) | undefined;
|
||||
windowAdapter: WindowAdapter;
|
||||
}) {
|
||||
const sendTxCore = useSendTransaction({
|
||||
|
||||
@@ -133,6 +133,9 @@ function DepositAndExecuteTx(props: ModalProps) {
|
||||
if (screen === "execute-tx") {
|
||||
return (
|
||||
<ExecutingTxScreen
|
||||
onBack={() => {
|
||||
setScreen("deposit");
|
||||
}}
|
||||
closeModal={props.onClose}
|
||||
onTxSent={props.onTxSent}
|
||||
tx={props.tx}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# @thirdweb-dev/wagmi-adapter
|
||||
|
||||
## 0.2.160
|
||||
|
||||
## 0.2.159
|
||||
|
||||
## 0.2.158
|
||||
|
||||
## 0.2.157
|
||||
|
||||
@@ -56,5 +56,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.158"
|
||||
"version": "0.2.160"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user