Compare commits
69
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
881c7a96f7 | ||
|
|
ad68969559 | ||
|
|
915d955da1 | ||
|
|
7da16cff2f | ||
|
|
f0edc0f253 | ||
|
|
0a2d269782 | ||
|
|
3d996d1d23 | ||
|
|
40781b8170 | ||
|
|
d907017217 | ||
|
|
cf53c2d7f1 | ||
|
|
207cb9dbeb | ||
|
|
b4d365bdc0 | ||
|
|
ea4e2fe376 | ||
|
|
1d54cff7b4 | ||
|
|
fd04eefea9 | ||
|
|
4cc68310dc | ||
|
|
ccdce496e0 | ||
|
|
2725f337f8 | ||
|
|
2c6ea3e6dd | ||
|
|
219fc69d72 | ||
|
|
8d17685683 | ||
|
|
8ef418c543 | ||
|
|
b88f75b494 | ||
|
|
c83f1c6f7a | ||
|
|
bc0295487b | ||
|
|
90b779107d | ||
|
|
afe187de95 | ||
|
|
759b93beff | ||
|
|
8e357b3cb3 | ||
|
|
6c318f83d6 | ||
|
|
f790c9fae9 | ||
|
|
bcbb3f540a | ||
|
|
931c730a08 | ||
|
|
e0c83121c2 | ||
|
|
a82a31e5ad | ||
|
|
3d235e218a | ||
|
|
c08fb1356e | ||
|
|
68cca6f992 | ||
|
|
e0abf2b273 | ||
|
|
abf0bbaf35 | ||
|
|
23cb3bd98c | ||
|
|
dcef3f4fe7 | ||
|
|
bf46520a6b | ||
|
|
7defce7264 | ||
|
|
ae3a122cf1 | ||
|
|
2412a6958c | ||
|
|
bfd3be764c | ||
|
|
46b9d2dd82 | ||
|
|
3e9eb49787 | ||
|
|
630edadcec | ||
|
|
f757e60846 | ||
|
|
d5d593956e | ||
|
|
9a4b1cb147 | ||
|
|
85fe9491cd | ||
|
|
899051f788 | ||
|
|
5828b9bd45 | ||
|
|
2b68a32eaa | ||
|
|
66ae33ee8b | ||
|
|
e9788096d6 | ||
|
|
706d696434 | ||
|
|
c36cfbb5c7 | ||
|
|
93a9ba9121 | ||
|
|
6a7b556d75 | ||
|
|
e0ace736a8 | ||
|
|
b30369f3cc | ||
|
|
f2121e9519 | ||
|
|
29d05a50bf | ||
|
|
f00f559a12 | ||
|
|
ceba6835dd |
@@ -188,9 +188,16 @@ jobs:
|
||||
- name: Build Packages
|
||||
run: pnpm build
|
||||
|
||||
- name: Report bundle size
|
||||
- name: Report bundle size (thirdweb)
|
||||
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
package_manager: pnpm
|
||||
directory: packages/thirdweb
|
||||
directory: packages/thirdweb
|
||||
|
||||
- name: Report bundle size (nexus)
|
||||
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
package_manager: pnpm
|
||||
directory: packages/nexus
|
||||
@@ -10,7 +10,8 @@ module.exports = [
|
||||
"/wallets",
|
||||
"/account-abstraction",
|
||||
"/payments",
|
||||
"/payments/x402",
|
||||
"/x402",
|
||||
"/nexus",
|
||||
"/auth",
|
||||
"/in-app-wallets",
|
||||
"/transactions",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@number-flow/react": "^0.5.10",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
@@ -479,6 +479,11 @@ async function redirects() {
|
||||
destination: "/tokens",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/payments/x402",
|
||||
destination: "/x402",
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function getUniversalBridgeTokens(props: {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
|
||||
} as Record<string, string>,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function addUniversalBridgeTokenRoute(props: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.project.publishableKey,
|
||||
} as Record<string, string>,
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ function SkeletonBarChart(props: {
|
||||
fill={`url(#${fillAreaSkeletonId})`}
|
||||
radius={8}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
type="natural"
|
||||
type="monotone"
|
||||
/>
|
||||
</AreaChart>
|
||||
)}
|
||||
|
||||
@@ -172,7 +172,7 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
key={key}
|
||||
stackId={props.variant !== "stacked" ? undefined : "a"}
|
||||
stroke={`var(--color-${key})`}
|
||||
type="natural"
|
||||
type="monotone"
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -51,17 +51,12 @@ export function WalletAddressUI(
|
||||
};
|
||||
},
|
||||
) {
|
||||
// default back to zero address if no address provided
|
||||
const address = useMemo(() => props.address || ZERO_ADDRESS, [props.address]);
|
||||
const address = props.address || ZERO_ADDRESS;
|
||||
|
||||
const [shortenedAddress, _lessShortenedAddress] = useMemo(() => {
|
||||
return [
|
||||
props.shortenAddress !== false
|
||||
? `${address.slice(0, 6)}...${address.slice(-4)}`
|
||||
: address,
|
||||
`${address.slice(0, 14)}...${address.slice(-12)}`,
|
||||
];
|
||||
}, [address, props.shortenAddress]);
|
||||
const shortenedAddress =
|
||||
props.shortenAddress !== false
|
||||
? `${address.slice(0, 6)}...${address.slice(-4)}`
|
||||
: address;
|
||||
|
||||
if (!isAddress(address)) {
|
||||
return (
|
||||
|
||||
+12
-2
@@ -14,6 +14,15 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import type { SearchType } from "./types";
|
||||
|
||||
const searchTypeLabels: Record<SearchType, string> = {
|
||||
email: "Email",
|
||||
phone: "Phone",
|
||||
id: "Auth Identifier",
|
||||
address: "Address",
|
||||
externalWallet: "External Wallet",
|
||||
userId: "User Identifier",
|
||||
};
|
||||
|
||||
export function AdvancedSearchInput(props: {
|
||||
onSearch: (searchType: SearchType, query: string) => void;
|
||||
onClear: () => void;
|
||||
@@ -52,7 +61,8 @@ export function AdvancedSearchInput(props: {
|
||||
<SelectContent>
|
||||
<SelectItem value="email">Email</SelectItem>
|
||||
<SelectItem value="phone">Phone</SelectItem>
|
||||
<SelectItem value="id">ID</SelectItem>
|
||||
<SelectItem value="id">Auth Identifier</SelectItem>
|
||||
<SelectItem value="userId">User Identifier</SelectItem>
|
||||
<SelectItem value="address">Address</SelectItem>
|
||||
<SelectItem value="externalWallet">External Wallet</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -62,7 +72,7 @@ export function AdvancedSearchInput(props: {
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
className="bg-background pl-9 border-r-0 rounded-r-none rounded-l-full"
|
||||
placeholder={`Search by ${searchType}...`}
|
||||
placeholder={`Search by ${searchTypeLabels[searchType]}...`}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -12,18 +12,25 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { CopyTextButton } from "../ui/CopyTextButton";
|
||||
|
||||
const getUserIdentifier = (user: WalletUser) => {
|
||||
const getAuthIdentifier = (user: WalletUser) => {
|
||||
const mainDetail = user.linkedAccounts[0]?.details;
|
||||
return (
|
||||
mainDetail?.id ??
|
||||
mainDetail?.email ??
|
||||
mainDetail?.phone ??
|
||||
mainDetail?.address ??
|
||||
mainDetail?.id ??
|
||||
user.id
|
||||
mainDetail?.address
|
||||
);
|
||||
};
|
||||
|
||||
const truncateIdentifier = (value: string) => {
|
||||
if (value.length <= 18) {
|
||||
return value;
|
||||
}
|
||||
return `${value.slice(0, 8)}...${value.slice(-4)}`;
|
||||
};
|
||||
|
||||
export function SearchResults(props: {
|
||||
results: WalletUser[];
|
||||
client: ThirdwebClient;
|
||||
@@ -51,6 +58,8 @@ export function SearchResults(props: {
|
||||
const mainDetail = user.linkedAccounts?.[0]?.details;
|
||||
const email = mainDetail?.email as string | undefined;
|
||||
const phone = mainDetail?.phone as string | undefined;
|
||||
const authIdentifier = getAuthIdentifier(user);
|
||||
const userIdentifier = user.id;
|
||||
|
||||
// Get external wallet addresses from linkedAccounts where type is 'siwe'
|
||||
const externalWalletAccounts =
|
||||
@@ -68,7 +77,34 @@ export function SearchResults(props: {
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
User Identifier
|
||||
</p>
|
||||
<p className="text-sm">{getUserIdentifier(user)}</p>
|
||||
{userIdentifier ? (
|
||||
<CopyTextButton
|
||||
textToShow={truncateIdentifier(userIdentifier)}
|
||||
textToCopy={userIdentifier}
|
||||
tooltip="Copy User Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm">N/A</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Auth Identifier
|
||||
</p>
|
||||
{authIdentifier ? (
|
||||
<CopyTextButton
|
||||
textToShow={truncateIdentifier(authIdentifier)}
|
||||
textToCopy={authIdentifier}
|
||||
tooltip="Copy Auth Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm">N/A</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{walletAddress && (
|
||||
|
||||
+69
-44
@@ -28,22 +28,42 @@ import { SearchResults } from "./SearchResults";
|
||||
import { searchUsers } from "./searchUsers";
|
||||
import type { SearchType } from "./types";
|
||||
|
||||
const getUserIdentifier = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
const getAuthIdentifier = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
const mainDetail = accounts[0]?.details;
|
||||
return (
|
||||
mainDetail?.id ??
|
||||
mainDetail?.email ??
|
||||
mainDetail?.phone ??
|
||||
mainDetail?.address ??
|
||||
mainDetail?.id
|
||||
mainDetail?.address
|
||||
);
|
||||
};
|
||||
|
||||
const getExternalWallets = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
return accounts?.filter((account) => account.type === "siwe") || [];
|
||||
const getPrimaryEmail = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
const emailFromPrimary = accounts[0]?.details?.email;
|
||||
if (emailFromPrimary) {
|
||||
return emailFromPrimary;
|
||||
}
|
||||
|
||||
const emailAccount = accounts.find((account) => {
|
||||
return typeof account.details?.email === "string" && account.details.email;
|
||||
});
|
||||
|
||||
if (emailAccount && typeof emailAccount.details.email === "string") {
|
||||
return emailAccount.details.email;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const columnHelper = createColumnHelper<WalletUser>();
|
||||
|
||||
const truncateIdentifier = (value: string) => {
|
||||
if (value.length <= 18) {
|
||||
return value;
|
||||
}
|
||||
return `${value.slice(0, 8)}...${value.slice(-4)}`;
|
||||
};
|
||||
|
||||
export function InAppWalletUsersPageContent(
|
||||
props: {
|
||||
authToken: string;
|
||||
@@ -56,9 +76,30 @@ export function InAppWalletUsersPageContent(
|
||||
) {
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
columnHelper.accessor("id", {
|
||||
cell: (cell) => {
|
||||
const userId = cell.getValue();
|
||||
|
||||
if (!userId) {
|
||||
return "N/A";
|
||||
}
|
||||
|
||||
return (
|
||||
<CopyTextButton
|
||||
textToShow={truncateIdentifier(userId)}
|
||||
textToCopy={userId}
|
||||
tooltip="Copy User Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
);
|
||||
},
|
||||
header: "User Identifier",
|
||||
id: "user_identifier",
|
||||
}),
|
||||
columnHelper.accessor("linkedAccounts", {
|
||||
cell: (cell) => {
|
||||
const identifier = getUserIdentifier(cell.getValue());
|
||||
const identifier = getAuthIdentifier(cell.getValue());
|
||||
|
||||
if (!identifier) {
|
||||
return "N/A";
|
||||
@@ -66,21 +107,17 @@ export function InAppWalletUsersPageContent(
|
||||
|
||||
return (
|
||||
<CopyTextButton
|
||||
textToShow={
|
||||
identifier.length > 30
|
||||
? `${identifier.slice(0, 30)}...`
|
||||
: identifier
|
||||
}
|
||||
textToShow={truncateIdentifier(identifier)}
|
||||
textToCopy={identifier}
|
||||
tooltip="Copy User Identifier"
|
||||
tooltip="Copy Auth Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableColumnFilter: true,
|
||||
header: "User Identifier",
|
||||
id: "user_identifier",
|
||||
header: "Auth Identifier",
|
||||
id: "auth_identifier",
|
||||
}),
|
||||
columnHelper.accessor("wallets", {
|
||||
cell: (cell) => {
|
||||
@@ -94,33 +131,24 @@ export function InAppWalletUsersPageContent(
|
||||
}),
|
||||
columnHelper.accessor("linkedAccounts", {
|
||||
cell: (cell) => {
|
||||
const externalWallets = getExternalWallets(cell.getValue());
|
||||
if (externalWallets.length === 0) {
|
||||
return <span className="text-muted-foreground text-sm">None</span>;
|
||||
const email = getPrimaryEmail(cell.getValue());
|
||||
|
||||
if (!email) {
|
||||
return <span className="text-muted-foreground text-sm">N/A</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{externalWallets.slice(0, 2).map((account) => {
|
||||
const address = account.details?.address as string | undefined;
|
||||
return address ? (
|
||||
<div
|
||||
key={`external-${address}-${account.details?.id}`}
|
||||
className="text-xs"
|
||||
>
|
||||
<WalletAddress address={address} client={props.client} />
|
||||
</div>
|
||||
) : null;
|
||||
})}
|
||||
{externalWallets.length > 2 && (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
+{externalWallets.length - 2} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<CopyTextButton
|
||||
textToShow={email}
|
||||
textToCopy={email}
|
||||
tooltip="Copy Email"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
);
|
||||
},
|
||||
header: "External Wallets",
|
||||
id: "external_wallets",
|
||||
header: "Email",
|
||||
id: "email",
|
||||
}),
|
||||
columnHelper.accessor("wallets", {
|
||||
cell: (cell) => {
|
||||
@@ -230,20 +258,17 @@ export function InAppWalletUsersPageContent(
|
||||
});
|
||||
const csv = Papa.unparse(
|
||||
usersWallets.map((row) => {
|
||||
const externalWallets = getExternalWallets(row.linkedAccounts);
|
||||
const externalWalletAddresses = externalWallets
|
||||
.map((account) => account.details?.address)
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
const email = getPrimaryEmail(row.linkedAccounts);
|
||||
|
||||
return {
|
||||
address: row.wallets[0]?.address || "Uninitialized",
|
||||
created: row.wallets[0]?.createdAt
|
||||
? new Date(row.wallets[0].createdAt).toISOString()
|
||||
: "Wallet not created yet",
|
||||
external_wallets: externalWalletAddresses || "None",
|
||||
email: email || "N/A",
|
||||
login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "),
|
||||
user_identifier: getUserIdentifier(row.linkedAccounts),
|
||||
auth_identifier: getAuthIdentifier(row.linkedAccounts) || "N/A",
|
||||
user_identifier: row.id || "N/A",
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -18,10 +18,10 @@ configure({
|
||||
type APIWallet = ListUserWalletsResponses[200]["result"]["wallets"][0];
|
||||
type APIProfile = APIWallet["profiles"][0];
|
||||
|
||||
// Transform API response to match existing WalletUser format
|
||||
// Transform API response to match wallet user format
|
||||
function transformToWalletUser(apiWallet: APIWallet): WalletUser {
|
||||
return {
|
||||
id: getProfileId(apiWallet.profiles[0]) || "",
|
||||
id: apiWallet.userId || getProfileId(apiWallet.profiles[0]) || "",
|
||||
linkedAccounts: apiWallet.profiles.map((profile) => {
|
||||
// Create details object based on the profile data
|
||||
let details:
|
||||
@@ -105,6 +105,9 @@ export async function searchUsers(
|
||||
case "externalWallet":
|
||||
queryParams.externalWalletAddress = query;
|
||||
break;
|
||||
case "userId":
|
||||
queryParams.userId = query;
|
||||
break;
|
||||
}
|
||||
|
||||
// Use the generated API function with Bearer authentication
|
||||
|
||||
@@ -3,4 +3,5 @@ export type SearchType =
|
||||
| "phone"
|
||||
| "id"
|
||||
| "address"
|
||||
| "externalWallet";
|
||||
| "externalWallet"
|
||||
| "userId";
|
||||
|
||||
@@ -59,7 +59,10 @@ export async function fetchDashboardContractMetadata(
|
||||
|
||||
return {
|
||||
contractType: compilerMetadata?.name || "",
|
||||
image: contractMetadata.image || "",
|
||||
image:
|
||||
contractMetadata.image && typeof contractMetadata.image === "string"
|
||||
? contractMetadata.image
|
||||
: "",
|
||||
name: contractName,
|
||||
symbol: contractSymbol,
|
||||
};
|
||||
|
||||
@@ -76,10 +76,10 @@ const fetchAccountList = ({
|
||||
};
|
||||
};
|
||||
|
||||
// Transform API response to match existing WalletUser format
|
||||
// Transform API response to match the wallet user format
|
||||
function transformToWalletUser(apiWallet: APIWallet): WalletUser {
|
||||
return {
|
||||
id: getProfileId(apiWallet.profiles[0]) || "",
|
||||
id: apiWallet.userId || getProfileId(apiWallet.profiles[0]) || "",
|
||||
linkedAccounts: apiWallet.profiles.map((profile) => {
|
||||
// Create details object based on the profile data
|
||||
let details:
|
||||
|
||||
@@ -5,6 +5,10 @@ const customClaimAmounts: Record<number, number> = {
|
||||
631571: 0.1,
|
||||
// Sophon testnet
|
||||
531050104: 5,
|
||||
// Arc testnet
|
||||
5042002: 1,
|
||||
// Injective testnet
|
||||
1439: 1,
|
||||
};
|
||||
|
||||
const defaultClaimAmount = 0.01;
|
||||
|
||||
@@ -21,8 +21,8 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
|
||||
primaryButtonText: "hsl(var(--inverted-foreground))",
|
||||
primaryText: "hsl(var(--foreground))",
|
||||
scrollbarBg: "hsl(var(--muted))",
|
||||
secondaryButtonBg: "hsl(var(--secondary))",
|
||||
secondaryButtonHoverBg: "hsl(var(--secondary)/80%)",
|
||||
secondaryButtonBg: "hsl(var(--secondary)/70%)",
|
||||
secondaryButtonHoverBg: "hsl(var(--secondary))",
|
||||
secondaryButtonText: "hsl(var(--secondary-foreground))",
|
||||
secondaryIconColor: "hsl(var(--secondary-foreground))",
|
||||
secondaryIconHoverBg: "hsl(var(--accent))",
|
||||
@@ -33,7 +33,7 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
|
||||
separatorLine: "hsl(var(--border))",
|
||||
skeletonBg: "hsl(var(--secondary-foreground)/15%)",
|
||||
success: "hsl(var(--success-text))",
|
||||
tertiaryBg: "hsl(var(--muted)/30%)",
|
||||
tertiaryBg: "hsl(var(--muted)/50%)",
|
||||
tooltipBg: "hsl(var(--popover))",
|
||||
tooltipText: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
|
||||
+2
-3
@@ -10,8 +10,7 @@ import * as ERC20Ext from "thirdweb/extensions/erc20";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import { download } from "thirdweb/storage";
|
||||
import type { OverrideEntry } from "thirdweb/utils";
|
||||
import { maxUint256 } from "thirdweb/utils";
|
||||
import { isRecord, maxUint256, type OverrideEntry } from "thirdweb/utils";
|
||||
import type { z } from "zod";
|
||||
import type {
|
||||
ClaimCondition as LegacyClaimCondition,
|
||||
@@ -86,7 +85,7 @@ export async function getClaimPhasesInLegacyFormat(
|
||||
]);
|
||||
const snapshot = await fetchSnapshot(
|
||||
condition.merkleRoot,
|
||||
contractMetadata.merkle,
|
||||
isRecord(contractMetadata.merkle) ? contractMetadata.merkle : {},
|
||||
options.contract.client,
|
||||
);
|
||||
return {
|
||||
|
||||
+4
-1
@@ -47,7 +47,10 @@ export default async function Image({
|
||||
chainName: info.chainMetadata.name,
|
||||
contractAddress: info.serverContract.address,
|
||||
displayName: contractDisplayName,
|
||||
logo: contractMetadata.image,
|
||||
logo:
|
||||
contractMetadata.image && typeof contractMetadata.image === "string"
|
||||
? contractMetadata.image
|
||||
: undefined,
|
||||
});
|
||||
} catch {
|
||||
return contractOGImageTemplate({
|
||||
|
||||
+46
-24
@@ -89,16 +89,31 @@ export async function ERC20PublicPage(props: {
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
contractCreator={contractCreator}
|
||||
image={contractMetadata.image}
|
||||
image={
|
||||
contractMetadata.image &&
|
||||
typeof contractMetadata.image === "string"
|
||||
? contractMetadata.image
|
||||
: undefined
|
||||
}
|
||||
isDashboardUser={isDashboardUser}
|
||||
name={contractMetadata.name}
|
||||
name={
|
||||
contractMetadata.name && typeof contractMetadata.name === "string"
|
||||
? contractMetadata.name
|
||||
: // if we do not have a contract name fall back to the address
|
||||
props.clientContract.address
|
||||
}
|
||||
socialUrls={
|
||||
typeof contractMetadata.social_urls === "object" &&
|
||||
contractMetadata.social_urls !== null
|
||||
? contractMetadata.social_urls
|
||||
: {}
|
||||
}
|
||||
symbol={contractMetadata.symbol}
|
||||
symbol={
|
||||
contractMetadata.symbol &&
|
||||
typeof contractMetadata.symbol === "string"
|
||||
? contractMetadata.symbol
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,21 +158,25 @@ export async function ERC20PublicPage(props: {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showBuyEmbed && (
|
||||
<div className="container max-w-7xl pb-10">
|
||||
<GridPatternEmbedContainer>
|
||||
<BuyEmbed
|
||||
chainMetadata={props.chainMetadata}
|
||||
claimConditionMeta={claimConditionMeta}
|
||||
clientContract={props.clientContract}
|
||||
tokenAddress={props.clientContract.address}
|
||||
tokenDecimals={tokenDecimals}
|
||||
tokenName={contractMetadata.name}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
</GridPatternEmbedContainer>
|
||||
</div>
|
||||
)}
|
||||
{showBuyEmbed &&
|
||||
contractMetadata.name &&
|
||||
typeof contractMetadata.name === "string" &&
|
||||
contractMetadata.symbol &&
|
||||
typeof contractMetadata.symbol === "string" && (
|
||||
<div className="container max-w-7xl pb-10">
|
||||
<GridPatternEmbedContainer>
|
||||
<BuyEmbed
|
||||
chainMetadata={props.chainMetadata}
|
||||
claimConditionMeta={claimConditionMeta}
|
||||
clientContract={props.clientContract}
|
||||
tokenAddress={props.clientContract.address}
|
||||
tokenDecimals={tokenDecimals}
|
||||
tokenName={contractMetadata.name}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
</GridPatternEmbedContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="container flex max-w-7xl grow flex-col pb-10">
|
||||
<div className="flex grow flex-col gap-8">
|
||||
@@ -167,12 +186,15 @@ export async function ERC20PublicPage(props: {
|
||||
contractAddress={props.clientContract.address}
|
||||
/>
|
||||
|
||||
<RecentTransfers
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
decimals={tokenDecimals}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
{contractMetadata.symbol &&
|
||||
typeof contractMetadata.symbol === "string" && (
|
||||
<RecentTransfers
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
decimals={tokenDecimals}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+10
-3
@@ -53,6 +53,13 @@ export async function NFTPublicPage(props: {
|
||||
const _isTokenByIndexSupported =
|
||||
props.type === "erc721" && isTokenByIndexSupported(functionSelectors);
|
||||
|
||||
// FIXME: this is technically a bad fallback but we gotta do what we gotta do
|
||||
const contractMetadataWithNameAndSymbolFallback = {
|
||||
...contractMetadata,
|
||||
// fall back to the contract address if the name is not set
|
||||
name: contractMetadata.name || props.clientContract.address,
|
||||
symbol: contractMetadata.symbol || "",
|
||||
};
|
||||
const buyNFTDropCard = nftDropClaimParams ? (
|
||||
<BuyNFTDropCardServer
|
||||
chainMetadata={props.chainMetadata}
|
||||
@@ -73,7 +80,7 @@ export async function NFTPublicPage(props: {
|
||||
<NFTsGrid
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
collectionMetadata={contractMetadata}
|
||||
collectionMetadata={contractMetadataWithNameAndSymbolFallback}
|
||||
gridClassName={
|
||||
buyNFTDropCard
|
||||
? "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||
@@ -110,7 +117,7 @@ export async function NFTPublicPage(props: {
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
contractCreator={contractCreator}
|
||||
contractMetadata={contractMetadata}
|
||||
contractMetadata={contractMetadataWithNameAndSymbolFallback}
|
||||
isDashboardUser={isDashboardUser}
|
||||
>
|
||||
<ResponsiveLayout
|
||||
@@ -138,7 +145,7 @@ export async function NFTPublicPage(props: {
|
||||
<PageLoadTokenViewerSheet
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
collectionMetadata={contractMetadata}
|
||||
collectionMetadata={contractMetadataWithNameAndSymbolFallback}
|
||||
tokenByIndexSupported={_isTokenByIndexSupported}
|
||||
tokenId={BigInt(props.tokenId)}
|
||||
type={props.type}
|
||||
|
||||
+8
-3
@@ -37,7 +37,7 @@ export function TokenViewerSheet(
|
||||
clientContract: ThirdwebContract;
|
||||
chainMetadata: ChainMetadata;
|
||||
collectionMetadata: {
|
||||
name: string;
|
||||
name: string | null;
|
||||
image?: string;
|
||||
};
|
||||
type: "erc1155" | "erc721";
|
||||
@@ -57,6 +57,11 @@ export function TokenViewerSheet(
|
||||
) {
|
||||
const tokenId = props.variant === "fetch-data" ? props.tokenId : props.nft.id;
|
||||
|
||||
const collectionMetadataWithNameFallback = {
|
||||
...props.collectionMetadata,
|
||||
name: props.collectionMetadata.name || props.clientContract.address,
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onOpenChange={(open) => {
|
||||
@@ -78,7 +83,7 @@ export function TokenViewerSheet(
|
||||
<FetchAndRenderTokenInfo
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
collectionMetadata={props.collectionMetadata}
|
||||
collectionMetadata={collectionMetadataWithNameFallback}
|
||||
tokenByIndexSupported={props.tokenByIndexSupported}
|
||||
tokenId={props.tokenId}
|
||||
type={props.type}
|
||||
@@ -86,7 +91,7 @@ export function TokenViewerSheet(
|
||||
) : (
|
||||
<TokenInfoUI
|
||||
chainMetadata={props.chainMetadata}
|
||||
collectionMetadata={props.collectionMetadata}
|
||||
collectionMetadata={collectionMetadataWithNameFallback}
|
||||
contract={props.clientContract}
|
||||
data={props.nft}
|
||||
tokenId={tokenId}
|
||||
|
||||
+4
-1
@@ -84,7 +84,10 @@ export const SettingsMetadata = ({
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
let image: string | undefined = metadata.data?.image;
|
||||
let image: string | undefined =
|
||||
metadata.data?.image && typeof metadata.data.image === "string"
|
||||
? metadata.data.image
|
||||
: undefined;
|
||||
try {
|
||||
image = image
|
||||
? // eslint-disable-next-line no-restricted-syntax
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Metadata } from "next";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { Bridge } from "thirdweb";
|
||||
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { bridgeStats } from "../../../bridge/data";
|
||||
import { PageHeader } from "./components/header";
|
||||
import { TokenPage } from "./components/token-page";
|
||||
|
||||
@@ -36,7 +37,8 @@ export default async function Page() {
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground">
|
||||
85+ chains, 4500+ tokens and 9+ million routes supported
|
||||
{bridgeStats.supportedChains} chains, {bridgeStats.supportedTokens}{" "}
|
||||
tokens and {bridgeStats.supportedRoutes} routes supported
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { redirect } from "next/navigation";
|
||||
import { StripeRedirectErrorPage } from "../../_components/StripeRedirectErrorPage";
|
||||
import { getPlanCancelUrl } from "../../utils/billing";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function CancelPlanPage(props: {
|
||||
params: Promise<{
|
||||
team_id: string;
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
getInvoicePaymentUrl,
|
||||
} from "../../../utils/billing";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function CheckoutPage(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
|
||||
@@ -2,6 +2,8 @@ import { redirect } from "next/navigation";
|
||||
import { StripeRedirectErrorPage } from "../../_components/StripeRedirectErrorPage";
|
||||
import { getBillingPortalUrl } from "../../utils/billing";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function ManageBillingPage(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
|
||||
@@ -76,7 +76,9 @@ export default async function DropPage({
|
||||
]);
|
||||
|
||||
const thumbnail =
|
||||
project.thumbnail || nft.metadata.image || contractMetadata.image || "";
|
||||
project.thumbnail ||
|
||||
nft.metadata.image ||
|
||||
(typeof contractMetadata.image === "string" ? contractMetadata.image : "");
|
||||
|
||||
const displayName = contractMetadata.name || nft.metadata.name || "";
|
||||
|
||||
|
||||
+6
-2
@@ -4,9 +4,14 @@ import { ProjectFTUX } from "./ProjectFTUX";
|
||||
|
||||
const meta = {
|
||||
component: ProjectFTUX,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="container py-8 pb-20">
|
||||
<div className="container py-8 pb-20 max-w-7xl">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
@@ -31,6 +36,5 @@ export const Default: Story = {
|
||||
],
|
||||
},
|
||||
teamSlug: "bar",
|
||||
managementAccessToken: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
+1
-80
@@ -15,33 +15,12 @@ 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 { ClientIDSection } from "./ClientIDSection";
|
||||
import { ProjectWalletControls } from "./ProjectWalletControls.client";
|
||||
import { ProjectWalletSetup } from "./ProjectWalletSetup.client";
|
||||
import { SecretKeySection } from "./SecretKeySection";
|
||||
|
||||
export async function ProjectFTUX(props: {
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
wallet?: ProjectWalletSummary | undefined;
|
||||
managementAccessToken: string | undefined;
|
||||
}) {
|
||||
const projectWallet = props.wallet ?? (await getProjectWallet(props.project));
|
||||
|
||||
export function ProjectFTUX(props: { project: Project; teamSlug: string }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
<ProjectWalletSection
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
wallet={projectWallet}
|
||||
managementAccessToken={props.managementAccessToken}
|
||||
/>
|
||||
<IntegrateAPIKeySection project={props.project} />
|
||||
<ProductsSection
|
||||
projectSlug={props.project.slug}
|
||||
@@ -53,64 +32,6 @@ export async function ProjectFTUX(props: {
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
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>
|
||||
</>
|
||||
) : (
|
||||
<ProjectWalletSetup
|
||||
managementAccessToken={props.managementAccessToken}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Integrate API key section ------------------------------------------------------------
|
||||
|
||||
function IntegrateAPIKeySection({ project }: { project: Project }) {
|
||||
|
||||
-747
@@ -1,747 +0,0 @@
|
||||
"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 { Spinner } from "@/components/ui/Spinner";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
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("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>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Select
|
||||
onValueChange={(value) => setSelectedWalletId(value)}
|
||||
value={selectedWalletId ?? ""}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select server wallet">
|
||||
{selectedWallet ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<WalletAddress
|
||||
address={selectedWallet.address}
|
||||
client={client}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-foreground">
|
||||
{selectedWallet.label ?? "Unnamed server wallet"}
|
||||
</span>
|
||||
{selectedWallet.address.toLowerCase() ===
|
||||
currentWalletAddressLower && (
|
||||
<Badge variant="success" className="text-xs">
|
||||
current
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
"Select server wallet"
|
||||
)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{serverWallets.map((wallet) => {
|
||||
const isCurrent =
|
||||
wallet.address.toLowerCase() ===
|
||||
currentWalletAddressLower;
|
||||
|
||||
return (
|
||||
<SelectItem key={wallet.id} value={wallet.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<WalletAddress
|
||||
address={wallet.address}
|
||||
client={client}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-foreground">
|
||||
{wallet.label ?? "Unnamed server wallet"}
|
||||
</span>
|
||||
{isCurrent && (
|
||||
<Badge variant="success" className="text-xs">
|
||||
current
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
-215
@@ -1,215 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { listProjectServerWallets } from "@/actions/project-wallet/list-server-wallets";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
|
||||
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";
|
||||
import CreateServerWallet from "../../transactions/server-wallets/components/create-server-wallet.client";
|
||||
|
||||
export function ProjectWalletSetup(props: {
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
managementAccessToken: string | undefined;
|
||||
}) {
|
||||
const { project, teamSlug, managementAccessToken } = props;
|
||||
const router = useDashboardRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const client = useMemo(() => getClientThirdwebClient(), []);
|
||||
const [selectedWalletId, setSelectedWalletId] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const serverWalletsQuery = useQuery({
|
||||
enabled: Boolean(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 effectiveSelectedWalletId = useMemo(() => {
|
||||
if (
|
||||
selectedWalletId &&
|
||||
serverWallets.some((wallet) => wallet.id === selectedWalletId)
|
||||
) {
|
||||
return selectedWalletId;
|
||||
}
|
||||
|
||||
return serverWallets[0]?.id;
|
||||
}, [selectedWalletId, serverWallets]);
|
||||
|
||||
const selectedWallet = useMemo(() => {
|
||||
if (!effectiveSelectedWalletId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return serverWallets.find(
|
||||
(wallet) => wallet.id === effectiveSelectedWalletId,
|
||||
);
|
||||
}, [effectiveSelectedWalletId, serverWallets]);
|
||||
|
||||
const setDefaultWalletMutation = useMutation({
|
||||
mutationFn: async (wallet: ProjectWalletSummary) => {
|
||||
await updateDefaultProjectWallet({
|
||||
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("Project wallet updated", {
|
||||
description: `Now pointing to ${descriptionLabel}`,
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [
|
||||
"project",
|
||||
project.id,
|
||||
"server-wallets",
|
||||
managementAccessToken,
|
||||
],
|
||||
});
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
const canSelectExisting =
|
||||
Boolean(managementAccessToken) && serverWallets.length > 0;
|
||||
|
||||
return (
|
||||
<Alert variant="info">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>No project wallet set</AlertTitle>
|
||||
<AlertDescription>
|
||||
Set a project wallet to use for dashboard and API integrations.
|
||||
</AlertDescription>
|
||||
|
||||
<div className="mt-4 space-y-5">
|
||||
{serverWalletsQuery.isLoading ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Spinner className="size-4" />
|
||||
<span>Loading existing server wallets…</span>
|
||||
</div>
|
||||
) : canSelectExisting ? (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
Choose an existing server wallet
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
These wallets were already created for this project. Pick one to
|
||||
set as the default.
|
||||
</p>
|
||||
<Select
|
||||
onValueChange={(value) => setSelectedWalletId(value)}
|
||||
value={effectiveSelectedWalletId ?? ""}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select server wallet">
|
||||
{selectedWallet ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<WalletAddress
|
||||
address={selectedWallet.address}
|
||||
client={client}
|
||||
/>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{selectedWallet.label ?? "Unnamed server wallet"}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
"Select server wallet"
|
||||
)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{serverWallets.map((wallet) => (
|
||||
<SelectItem key={wallet.id} value={wallet.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<WalletAddress
|
||||
address={wallet.address}
|
||||
client={client}
|
||||
/>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{wallet.label ?? "Unnamed server wallet"}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button
|
||||
disabled={!selectedWallet || setDefaultWalletMutation.isPending}
|
||||
onClick={() => {
|
||||
if (selectedWallet) {
|
||||
setDefaultWalletMutation.mutate(selectedWallet);
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{setDefaultWalletMutation.isPending && (
|
||||
<Spinner className="mr-2 size-4" />
|
||||
)}
|
||||
Set as project wallet
|
||||
</Button>
|
||||
<CreateServerWallet
|
||||
managementAccessToken={managementAccessToken}
|
||||
project={project}
|
||||
teamSlug={teamSlug}
|
||||
setAsProjectWallet
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 text-sm text-muted-foreground">
|
||||
<p>
|
||||
Create a server wallet to start using transactions and other
|
||||
project features.
|
||||
</p>
|
||||
<CreateServerWallet
|
||||
managementAccessToken={managementAccessToken}
|
||||
project={project}
|
||||
teamSlug={teamSlug}
|
||||
setAsProjectWallet
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
+736
@@ -0,0 +1,736 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowLeftRightIcon,
|
||||
EllipsisVerticalIcon,
|
||||
RefreshCcwIcon,
|
||||
SendIcon,
|
||||
ShuffleIcon,
|
||||
WalletIcon,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { type ThirdwebClient, toWei } from "thirdweb";
|
||||
import { useWalletBalance } from "thirdweb/react";
|
||||
import { isAddress } from "thirdweb/utils";
|
||||
import { z } from "zod";
|
||||
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 { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
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 { Spinner } from "@/components/ui/Spinner";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { 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 GetProjectServerWallets = (params: {
|
||||
managementAccessToken: string;
|
||||
projectId: string;
|
||||
}) => Promise<ProjectWalletSummary[]>;
|
||||
|
||||
type ProjectWalletControlsProps = {
|
||||
projectWallet: ProjectWalletSummary;
|
||||
project: Project;
|
||||
defaultChainId?: number;
|
||||
teamSlug: string;
|
||||
getProjectServerWallets: GetProjectServerWallets;
|
||||
client: ThirdwebClient;
|
||||
};
|
||||
|
||||
export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) {
|
||||
const { projectWallet, 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 chain = useV5DashboardChain(selectedChainId);
|
||||
|
||||
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 props.getProjectServerWallets({
|
||||
managementAccessToken,
|
||||
projectId: project.id,
|
||||
});
|
||||
},
|
||||
queryKey: ["project", project.id, "server-wallets", managementAccessToken],
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
const balanceQuery = useWalletBalance({
|
||||
address: projectWallet.address,
|
||||
chain,
|
||||
client: props.client,
|
||||
});
|
||||
|
||||
const canChangeWallet =
|
||||
serverWalletsQuery.data && serverWalletsQuery.data.length > 1;
|
||||
|
||||
const router = useDashboardRouter();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-xl border border-border bg-card relative">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-label="Open wallet actions"
|
||||
className="p-2 h-auto w-auto absolute right-4 top-4 z-10"
|
||||
variant="ghost"
|
||||
>
|
||||
<EllipsisVerticalIcon className="size-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64 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>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2"
|
||||
onSelect={() =>
|
||||
router.push(
|
||||
`/team/${props.teamSlug}/${props.project.slug}/transactions`,
|
||||
)
|
||||
}
|
||||
>
|
||||
<ArrowLeftRightIcon className="size-4 text-muted-foreground" />
|
||||
View transactions
|
||||
</DropdownMenuItem>
|
||||
|
||||
{canChangeWallet && (
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2"
|
||||
onSelect={() => setIsChangeWalletOpen(true)}
|
||||
>
|
||||
<ShuffleIcon className="size-4 text-muted-foreground" />
|
||||
Change project wallet
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<div className="p-4 relative flex flex-col gap-5">
|
||||
<div>
|
||||
<p className="text-sm text-foreground mb-1">Wallet Address</p>
|
||||
<CopyTextButton
|
||||
textToShow={projectWallet.address}
|
||||
textToCopy={projectWallet.address}
|
||||
tooltip="Copy wallet address"
|
||||
copyIconPosition="right"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground -translate-x-1.5 py-0 h-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-foreground mb-1">Wallet Label</p>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="text-sm break-all py-2 sm:py-0 text-muted-foreground">
|
||||
{projectWallet.label || "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-dashed flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-sm text-foreground mb-1">Balance</p>
|
||||
<div className="flex items-center gap-1">
|
||||
{balanceQuery.isFetching || !balanceQuery.data ? (
|
||||
<Skeleton className="h-5 w-[100px]" />
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{balanceQuery.data.displayValue} {balanceQuery.data.symbol}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<Button
|
||||
aria-label="Refresh balance"
|
||||
className="p-1 h-auto w-auto"
|
||||
disabled={balanceQuery.isFetching}
|
||||
onClick={() => balanceQuery.refetch()}
|
||||
variant="ghost"
|
||||
>
|
||||
<RefreshCcwIcon
|
||||
className={cn(
|
||||
"size-3 text-muted-foreground",
|
||||
balanceQuery.isFetching && "animate-spin",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SingleNetworkSelector
|
||||
chainId={selectedChainId}
|
||||
className="w-fit rounded-full bg-background hover:bg-accent/50"
|
||||
client={props.client}
|
||||
disableDeprecated
|
||||
disableChainId
|
||||
onChange={setSelectedChainId}
|
||||
placeholder="Select network"
|
||||
popoverContentClassName="!w-[320px] rounded-xl overflow-hidden"
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SendProjectWalletModal
|
||||
chainId={selectedChainId}
|
||||
client={props.client}
|
||||
isManagedVault={isManagedVault}
|
||||
label={projectWallet.label || ""}
|
||||
onClose={() => setIsSendOpen(false)}
|
||||
onSuccess={() => balanceQuery.refetch()}
|
||||
open={isSendOpen}
|
||||
publishableKey={project.publishableKey}
|
||||
teamId={project.teamId}
|
||||
walletAddress={projectWallet.address}
|
||||
/>
|
||||
|
||||
<FundWalletModal
|
||||
checkoutWidgetTitle="Fund Project Wallet"
|
||||
client={props.client}
|
||||
defaultChainId={selectedChainId}
|
||||
description="Use your card or crypto to deposit into this server wallet"
|
||||
onOpenChange={setIsReceiveOpen}
|
||||
open={isReceiveOpen}
|
||||
recipientAddress={projectWallet.address}
|
||||
title="Fund project wallet"
|
||||
/>
|
||||
|
||||
<Dialog onOpenChange={setIsChangeWalletOpen} open={isChangeWalletOpen}>
|
||||
<DialogContent className="gap-0 p-0 overflow-hidden">
|
||||
<ChangeProjectWalletDialogContent
|
||||
isOpen={isChangeWalletOpen}
|
||||
setIsOpen={setIsChangeWalletOpen}
|
||||
serverWallets={{
|
||||
data: serverWalletsQuery.data ?? [],
|
||||
isPending: serverWalletsQuery.isLoading,
|
||||
}}
|
||||
projectWallet={projectWallet}
|
||||
client={props.client}
|
||||
project={project}
|
||||
managementAccessToken={managementAccessToken}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChangeProjectWalletDialogContent(props: {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
serverWallets: {
|
||||
data: ProjectWalletSummary[];
|
||||
isPending: boolean;
|
||||
};
|
||||
projectWallet: ProjectWalletSummary;
|
||||
managementAccessToken: string | undefined;
|
||||
project: Project;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const router = useDashboardRouter();
|
||||
|
||||
const [selectedWalletId, setSelectedWalletId] = useState<string | undefined>(
|
||||
props.projectWallet.id,
|
||||
);
|
||||
|
||||
const serverWallets = props.serverWallets.data;
|
||||
|
||||
const currentWalletAddressLower = props.projectWallet.address.toLowerCase();
|
||||
|
||||
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: props.project,
|
||||
projectWalletAddress: wallet.address,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to update project wallet";
|
||||
toast.error(message);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
toast.success("Project wallet updated");
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [
|
||||
"project",
|
||||
props.project.id,
|
||||
"server-wallets",
|
||||
props.managementAccessToken,
|
||||
],
|
||||
});
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DialogHeader className="p-4 lg:p-6">
|
||||
<DialogTitle>Change Project Wallet</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose a server wallet to use as project wallet
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-4 pb-8 lg:px-6">
|
||||
{props.serverWallets.isPending ? (
|
||||
<div className="flex justify-center py-6">
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
setSelectedWalletId(value);
|
||||
}}
|
||||
value={selectedWalletId ?? ""}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select server wallet">
|
||||
{selectedWallet ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<WalletAddress
|
||||
address={selectedWallet.address}
|
||||
client={props.client}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-foreground">
|
||||
{selectedWallet.label ?? "Unnamed server wallet"}
|
||||
</span>
|
||||
{selectedWallet.address.toLowerCase() ===
|
||||
currentWalletAddressLower && (
|
||||
<Badge variant="success" className="text-xs">
|
||||
current
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
"Select server wallet"
|
||||
)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{serverWallets.map((wallet) => {
|
||||
const isCurrent =
|
||||
wallet.address.toLowerCase() === currentWalletAddressLower;
|
||||
|
||||
return (
|
||||
<SelectItem key={wallet.id} value={wallet.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<WalletAddress
|
||||
address={wallet.address}
|
||||
client={props.client}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-foreground">
|
||||
{wallet.label ?? "Unnamed server wallet"}
|
||||
</span>
|
||||
{isCurrent && (
|
||||
<Badge variant="success" className="text-xs">
|
||||
current
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 border-t bg-card p-4 lg:p-6">
|
||||
<Button
|
||||
onClick={() => props.setIsOpen(false)}
|
||||
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" />
|
||||
)}
|
||||
Set as project wallet
|
||||
</Button>
|
||||
</div>
|
||||
</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>>;
|
||||
|
||||
type SendProjectWalletModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
walletAddress: string;
|
||||
publishableKey: string;
|
||||
teamId: string;
|
||||
chainId: number;
|
||||
label: string;
|
||||
client: ReturnType<typeof getClientThirdwebClient>;
|
||||
isManagedVault: boolean;
|
||||
};
|
||||
|
||||
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.";
|
||||
|
||||
function SendProjectWalletModal(props: SendProjectWalletModalProps) {
|
||||
return (
|
||||
<Dialog
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen) {
|
||||
props.onClose();
|
||||
}
|
||||
}}
|
||||
open={props.open}
|
||||
>
|
||||
<DialogContent className="gap-0 p-0 overflow-hidden">
|
||||
<SendProjectWalletModalContent {...props} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function SendProjectWalletModalContent(props: SendProjectWalletModalProps) {
|
||||
const {
|
||||
onClose,
|
||||
onSuccess,
|
||||
walletAddress,
|
||||
publishableKey,
|
||||
teamId,
|
||||
chainId,
|
||||
label,
|
||||
client,
|
||||
isManagedVault,
|
||||
} = props;
|
||||
|
||||
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"));
|
||||
|
||||
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 (
|
||||
<div 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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
|
||||
import { projectStub } from "@/storybook/stubs";
|
||||
import { storybookThirdwebClient } from "@/storybook/utils";
|
||||
import { ProjectWalletSectionUI } from "./project-wallet";
|
||||
|
||||
const meta = {
|
||||
component: ProjectWalletSectionUI,
|
||||
title: "Project/ProjectWalletSection",
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="container py-8 pb-20 max-w-7xl">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof ProjectWalletSectionUI>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const projectWithManagedAccessToken = projectStub("foo", "bar");
|
||||
projectWithManagedAccessToken.services = [
|
||||
{
|
||||
name: "engineCloud",
|
||||
actions: [],
|
||||
managementAccessToken: "managed-access-token",
|
||||
},
|
||||
];
|
||||
|
||||
const projectWithoutManagedAccessToken = projectStub("foo", "bar");
|
||||
|
||||
const projectWallet1: ProjectWalletSummary = {
|
||||
id: "server-wallet-id",
|
||||
address: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
|
||||
label: "Project Wallet 1",
|
||||
};
|
||||
|
||||
const projectWallet2: ProjectWalletSummary = {
|
||||
id: "server-wallet-id-2",
|
||||
address: "0x83Dd93fA5D8343094f850f90B3fb90088C1bB425",
|
||||
label: "Project Wallet 2",
|
||||
};
|
||||
|
||||
export const NoProjectWalletSetNoManagedAccessToken: Story = {
|
||||
args: {
|
||||
project: projectWithoutManagedAccessToken,
|
||||
client: storybookThirdwebClient,
|
||||
teamSlug: "bar",
|
||||
projectWallet: undefined,
|
||||
getProjectServerWallets: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return [];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoProjectWalletSetWithManagedAccessToken: Story = {
|
||||
args: {
|
||||
project: projectWithManagedAccessToken,
|
||||
client: storybookThirdwebClient,
|
||||
teamSlug: "bar",
|
||||
projectWallet: undefined,
|
||||
getProjectServerWallets: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return [];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoProjectWalletSetWithManagedAccessTokenAndServerWallets: Story = {
|
||||
args: {
|
||||
project: projectWithManagedAccessToken,
|
||||
teamSlug: "bar",
|
||||
client: storybookThirdwebClient,
|
||||
projectWallet: undefined,
|
||||
getProjectServerWallets: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return [projectWallet1, projectWallet2];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoProjectWalletSetLoading: Story = {
|
||||
args: {
|
||||
project: projectWithManagedAccessToken,
|
||||
teamSlug: "bar",
|
||||
client: storybookThirdwebClient,
|
||||
projectWallet: undefined,
|
||||
getProjectServerWallets: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100000));
|
||||
return [projectWallet1, projectWallet2];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ProjectWalletSetMultipleServerWallets: Story = {
|
||||
args: {
|
||||
project: projectWithManagedAccessToken,
|
||||
teamSlug: "bar",
|
||||
client: storybookThirdwebClient,
|
||||
projectWallet: projectWallet1,
|
||||
getProjectServerWallets: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return [projectWallet1, projectWallet2];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ProjectWalletSetSingleServerWallet: Story = {
|
||||
args: {
|
||||
project: projectWithManagedAccessToken,
|
||||
teamSlug: "bar",
|
||||
projectWallet: projectWallet1,
|
||||
client: storybookThirdwebClient,
|
||||
getProjectServerWallets: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return [projectWallet1];
|
||||
},
|
||||
},
|
||||
};
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { UnderlineLink } from "@workspace/ui/components/UnderlineLink";
|
||||
import { ChevronDownIcon, XIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { listProjectServerWallets } from "@/actions/project-wallet/list-server-wallets";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
|
||||
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";
|
||||
import { CreateServerWallet } from "../../transactions/server-wallets/components/create-server-wallet.client";
|
||||
import { ProjectWalletDetailsSection } from "./project-wallet-details";
|
||||
|
||||
type GetProjectServerWallets = (params: {
|
||||
managementAccessToken: string;
|
||||
projectId: string;
|
||||
}) => Promise<ProjectWalletSummary[]>;
|
||||
|
||||
function CreateProjectWalletSection(props: {
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
getProjectServerWallets: GetProjectServerWallets;
|
||||
}) {
|
||||
const { project, teamSlug } = props;
|
||||
const router = useDashboardRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const client = useMemo(() => getClientThirdwebClient(), []);
|
||||
const [selectedWalletId, setSelectedWalletId] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [isSelectDialogOpen, setIsSelectDialogOpen] = useState(false);
|
||||
|
||||
const managementAccessToken =
|
||||
props.project.services?.find((service) => service.name === "engineCloud")
|
||||
?.managementAccessToken ?? undefined;
|
||||
|
||||
const serverWalletsQuery = useQuery({
|
||||
enabled: Boolean(managementAccessToken),
|
||||
queryFn: async () => {
|
||||
if (!managementAccessToken) {
|
||||
return [] as ProjectWalletSummary[];
|
||||
}
|
||||
|
||||
return props.getProjectServerWallets({
|
||||
managementAccessToken,
|
||||
projectId: project.id,
|
||||
});
|
||||
},
|
||||
queryKey: ["project", project.id, "server-wallets", managementAccessToken],
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const serverWallets = serverWalletsQuery.data ?? [];
|
||||
|
||||
const effectiveSelectedWalletId = useMemo(() => {
|
||||
if (
|
||||
selectedWalletId &&
|
||||
serverWallets.some((wallet) => wallet.id === selectedWalletId)
|
||||
) {
|
||||
return selectedWalletId;
|
||||
}
|
||||
|
||||
return serverWallets[0]?.id;
|
||||
}, [selectedWalletId, serverWallets]);
|
||||
|
||||
const selectedWallet = useMemo(() => {
|
||||
if (!effectiveSelectedWalletId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return serverWallets.find(
|
||||
(wallet) => wallet.id === effectiveSelectedWalletId,
|
||||
);
|
||||
}, [effectiveSelectedWalletId, serverWallets]);
|
||||
|
||||
const setDefaultWalletMutation = useMutation({
|
||||
mutationFn: async (wallet: ProjectWalletSummary) => {
|
||||
await updateDefaultProjectWallet({
|
||||
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("Project wallet updated", {
|
||||
description: `Now pointing to ${descriptionLabel}`,
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [
|
||||
"project",
|
||||
project.id,
|
||||
"server-wallets",
|
||||
managementAccessToken,
|
||||
],
|
||||
});
|
||||
setIsSelectDialogOpen(false);
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
const canSelectExisting =
|
||||
Boolean(managementAccessToken) && serverWallets.length > 0;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border p-4 bg-card">
|
||||
<div className="rounded-full p-2 border bg-background inline-flex mb-4">
|
||||
<XIcon className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<h2 className="text-sm font-medium text-foreground mb-0.5">
|
||||
No Project Wallet set
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Set a project wallet to set the default sender in thirdweb API.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 space-y-5 text-foreground">
|
||||
{serverWalletsQuery.isLoading ? (
|
||||
<Skeleton className="h-[36px] max-w-lg rounded-full" />
|
||||
) : canSelectExisting ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
className="bg-background rounded-full gap-2"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedWalletId(effectiveSelectedWalletId);
|
||||
setIsSelectDialogOpen(true);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<ChevronDownIcon className="size-4 text-muted-foreground" />
|
||||
Select a server wallet
|
||||
</Button>
|
||||
|
||||
<CreateServerWallet
|
||||
project={project}
|
||||
teamSlug={teamSlug}
|
||||
setAsProjectWallet
|
||||
button={{ size: "sm", iconClassName: "size-3.5" }}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen) {
|
||||
setIsSelectDialogOpen(false);
|
||||
}
|
||||
}}
|
||||
open={isSelectDialogOpen}
|
||||
>
|
||||
<DialogContent className="gap-0 p-0 overflow-hidden">
|
||||
<DialogHeader className="p-4 lg:p-6">
|
||||
<DialogTitle>Select project wallet</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose a 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 === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No server wallets found.
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Select
|
||||
onValueChange={(value) => setSelectedWalletId(value)}
|
||||
value={selectedWalletId ?? ""}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select server wallet" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{serverWallets.map((wallet) => (
|
||||
<SelectItem key={wallet.id} value={wallet.id}>
|
||||
<div className="flex items-center gap-3">
|
||||
<WalletAddress
|
||||
address={wallet.address}
|
||||
client={client}
|
||||
className="h-auto py-1"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{wallet.label}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 border-t bg-card p-4 lg:p-6">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsSelectDialogOpen(false);
|
||||
}}
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={setDefaultWalletMutation.isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
!selectedWallet || setDefaultWalletMutation.isPending
|
||||
}
|
||||
onClick={() => {
|
||||
if (selectedWallet) {
|
||||
setDefaultWalletMutation.mutate(selectedWallet);
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{setDefaultWalletMutation.isPending && (
|
||||
<Spinner className="mr-2 size-4" />
|
||||
)}
|
||||
Set as project wallet
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 text-sm text-muted-foreground">
|
||||
<CreateServerWallet
|
||||
project={project}
|
||||
teamSlug={teamSlug}
|
||||
setAsProjectWallet
|
||||
button={{ size: "sm", iconClassName: "size-3.5" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProjectWalletSectionUI(props: {
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
projectWallet: ProjectWalletSummary | undefined;
|
||||
getProjectServerWallets: GetProjectServerWallets;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3">
|
||||
<h2 className="mb-1 font-semibold text-xl tracking-tight">
|
||||
Project Wallet
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
The server wallet to be used as the default sender in{" "}
|
||||
<UnderlineLink
|
||||
href="https://portal.thirdweb.com/reference"
|
||||
target="_blank"
|
||||
>
|
||||
thirdweb API
|
||||
</UnderlineLink>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{props.projectWallet ? (
|
||||
<ProjectWalletDetailsSection
|
||||
project={props.project}
|
||||
projectWallet={props.projectWallet}
|
||||
client={props.client}
|
||||
teamSlug={props.teamSlug}
|
||||
getProjectServerWallets={props.getProjectServerWallets}
|
||||
/>
|
||||
) : (
|
||||
<CreateProjectWalletSection
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
getProjectServerWallets={props.getProjectServerWallets}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProjectWalletSection(props: {
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
projectWallet: ProjectWalletSummary | undefined;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
return (
|
||||
<ProjectWalletSectionUI
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
client={props.client}
|
||||
projectWallet={props.projectWallet}
|
||||
getProjectServerWallets={listProjectServerWallets}
|
||||
/>
|
||||
);
|
||||
}
|
||||
-1
@@ -59,7 +59,6 @@ export function ErrorRate({ datapoints }: { datapoints: ResultItem[] }) {
|
||||
key={tag}
|
||||
stackId="a"
|
||||
stroke={`var(--color-${tag})`}
|
||||
type="natural"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
+8
@@ -14,6 +14,7 @@ type AggregatedMetrics = {
|
||||
|
||||
export function ProjectHighlightsCard(props: {
|
||||
selectedChart: string | undefined;
|
||||
aggregatedUserStats: InAppWalletStats[];
|
||||
userStats: InAppWalletStats[];
|
||||
volumeStats: UniversalBridgeStats[];
|
||||
teamSlug: string;
|
||||
@@ -22,6 +23,7 @@ export function ProjectHighlightsCard(props: {
|
||||
}) {
|
||||
const {
|
||||
selectedChart,
|
||||
aggregatedUserStats,
|
||||
userStats,
|
||||
volumeStats,
|
||||
teamSlug,
|
||||
@@ -70,6 +72,12 @@ export function ProjectHighlightsCard(props: {
|
||||
: "activeUsers"
|
||||
}
|
||||
aggregateFn={(_data, key) => {
|
||||
if (key === "activeUsers") {
|
||||
return aggregatedUserStats.reduce(
|
||||
(acc, curr) => acc + curr.uniqueWalletsConnected,
|
||||
0,
|
||||
);
|
||||
}
|
||||
return timeSeriesData.reduce((acc, curr) => acc + curr[key], 0);
|
||||
}}
|
||||
chartConfig={chartConfig}
|
||||
|
||||
@@ -39,10 +39,8 @@ import type {
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { PieChartCard } from "../../../components/Analytics/PieChartCard";
|
||||
import { EngineCloudChartCardAsync } from "./components/EngineCloudChartCard";
|
||||
import {
|
||||
ProjectFTUX,
|
||||
ProjectWalletSection,
|
||||
} from "./components/ProjectFTUX/ProjectFTUX";
|
||||
import { ProjectFTUX } from "./components/ProjectFTUX/ProjectFTUX";
|
||||
import { ProjectWalletSection } from "./components/project-wallet/project-wallet";
|
||||
import { RpcMethodBarChartCardAsync } from "./components/RpcMethodBarChartCard";
|
||||
import { TransactionsChartCardAsync } from "./components/Transactions";
|
||||
import { ProjectHighlightsCard } from "./overview/highlights-card";
|
||||
@@ -112,9 +110,6 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
});
|
||||
|
||||
const projectWallet = await getProjectWallet(project);
|
||||
const managementAccessToken =
|
||||
project.services?.find((service) => service.name === "engineCloud")
|
||||
?.managementAccessToken ?? undefined;
|
||||
|
||||
return (
|
||||
<ResponsiveSearchParamsProvider value={searchParams}>
|
||||
@@ -135,14 +130,17 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
actions: null,
|
||||
}}
|
||||
>
|
||||
<ProjectWalletSection
|
||||
project={project}
|
||||
teamSlug={params.team_slug}
|
||||
projectWallet={projectWallet}
|
||||
client={client}
|
||||
/>
|
||||
|
||||
<div className="h-10" />
|
||||
|
||||
{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}
|
||||
@@ -155,12 +153,7 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ProjectFTUX
|
||||
project={project}
|
||||
teamSlug={params.team_slug}
|
||||
wallet={projectWallet}
|
||||
managementAccessToken={managementAccessToken}
|
||||
/>
|
||||
<ProjectFTUX project={project} teamSlug={params.team_slug} />
|
||||
)}
|
||||
</ProjectPage>
|
||||
</ResponsiveSearchParamsProvider>
|
||||
@@ -385,8 +378,18 @@ async function AsyncAppHighlightsCard(props: {
|
||||
params: PageParams;
|
||||
authToken: string;
|
||||
}) {
|
||||
const [walletUserStatsTimeSeries, universalBridgeUsage] =
|
||||
const [aggregatedUserStats, walletUserStatsTimeSeries, universalBridgeUsage] =
|
||||
await Promise.allSettled([
|
||||
getInAppWalletUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
getInAppWalletUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
@@ -415,6 +418,11 @@ async function AsyncAppHighlightsCard(props: {
|
||||
) {
|
||||
return (
|
||||
<ProjectHighlightsCard
|
||||
aggregatedUserStats={
|
||||
aggregatedUserStats.status === "fulfilled"
|
||||
? aggregatedUserStats.value
|
||||
: []
|
||||
}
|
||||
selectedChart={props.selectedChart}
|
||||
selectedChartQueryParam={props.selectedChartQueryParam}
|
||||
teamSlug={props.params.team_slug}
|
||||
|
||||
+7
-24
@@ -3,14 +3,13 @@ import { useMemo } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { type Step, StepsCard } from "@/components/blocks/StepsCard";
|
||||
import CreateServerWallet from "../server-wallets/components/create-server-wallet.client";
|
||||
import { CreateServerWallet } from "../server-wallets/components/create-server-wallet.client";
|
||||
import type { Wallet } from "../server-wallets/wallet-table/types";
|
||||
import type { SolanaWallet } from "../solana-wallets/wallet-table/types";
|
||||
import { SendTestSolanaTransaction } from "./send-test-solana-tx.client";
|
||||
import { SendTestTransaction } from "./send-test-tx.client";
|
||||
|
||||
interface Props {
|
||||
managementAccessToken: string | undefined;
|
||||
project: Project;
|
||||
wallets: Wallet[];
|
||||
solanaWallets: SolanaWallet[];
|
||||
@@ -29,11 +28,12 @@ export const EngineChecklist: React.FC<Props> = (props) => {
|
||||
const steps: Step[] = [];
|
||||
steps.push({
|
||||
children: (
|
||||
<CreateServerWalletStep
|
||||
managementAccessToken={props.managementAccessToken}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
/>
|
||||
<div className="mt-4 flex flex-row gap-4">
|
||||
<CreateServerWallet
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
completed: props.wallets.length > 0 || props.hasTransactions,
|
||||
description:
|
||||
@@ -61,7 +61,6 @@ export const EngineChecklist: React.FC<Props> = (props) => {
|
||||
});
|
||||
return steps;
|
||||
}, [
|
||||
props.managementAccessToken,
|
||||
props.project,
|
||||
props.wallets,
|
||||
props.hasTransactions,
|
||||
@@ -113,19 +112,3 @@ export const EngineChecklist: React.FC<Props> = (props) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function CreateServerWalletStep(props: {
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
managementAccessToken: string | undefined;
|
||||
}) {
|
||||
return (
|
||||
<div className="mt-4 flex flex-row gap-4">
|
||||
<CreateServerWallet
|
||||
managementAccessToken={props.managementAccessToken}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+72
-22
@@ -40,6 +40,13 @@ import {
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
@@ -57,8 +64,9 @@ import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
import { WalletProductIcon } from "@/icons/WalletProductIcon";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { fetchSolanaBalance } from "../lib/getSolanaBalance";
|
||||
import { updateDefaultProjectWallet } from "../lib/vault.client";
|
||||
import CreateServerWallet from "../server-wallets/components/create-server-wallet.client";
|
||||
import { CreateServerWallet } from "../server-wallets/components/create-server-wallet.client";
|
||||
import type { Wallet as EVMWallet } from "../server-wallets/wallet-table/types";
|
||||
import { CreateSolanaWallet } from "../solana-wallets/components/create-solana-wallet.client";
|
||||
import { UpgradeSolanaPermissions } from "../solana-wallets/components/upgrade-solana-permissions.client";
|
||||
@@ -77,9 +85,9 @@ interface ServerWalletsTableProps {
|
||||
solanaTotalPages: number;
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
managementAccessToken: string | undefined;
|
||||
client: ThirdwebClient;
|
||||
solanaPermissionError?: boolean;
|
||||
authToken: string;
|
||||
}
|
||||
|
||||
export function ServerWalletsTable(props: ServerWalletsTableProps) {
|
||||
@@ -88,7 +96,6 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
|
||||
solanaWallets,
|
||||
project,
|
||||
teamSlug,
|
||||
managementAccessToken,
|
||||
evmTotalRecords,
|
||||
evmCurrentPage,
|
||||
evmTotalPages,
|
||||
@@ -97,10 +104,14 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
|
||||
solanaTotalPages,
|
||||
client,
|
||||
solanaPermissionError,
|
||||
authToken,
|
||||
} = props;
|
||||
|
||||
const [activeChain, setActiveChain] = useState<WalletChain>("evm");
|
||||
const [selectedChainId, setSelectedChainId] = useState<number>(1);
|
||||
const [selectedSolanaChain, setSelectedSolanaChain] = useState<
|
||||
"solana:mainnet" | "solana:devnet"
|
||||
>("solana:mainnet");
|
||||
const [showSmartAccount, setShowSmartAccount] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -134,11 +145,7 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
|
||||
<div className="flex flex-row gap-2.5">
|
||||
{activeChain === "evm" && (
|
||||
<>
|
||||
<CreateServerWallet
|
||||
managementAccessToken={managementAccessToken}
|
||||
project={project}
|
||||
teamSlug={teamSlug}
|
||||
/>
|
||||
<CreateServerWallet project={project} teamSlug={teamSlug} />
|
||||
<SingleNetworkSelector
|
||||
chainId={selectedChainId}
|
||||
onChange={setSelectedChainId}
|
||||
@@ -151,12 +158,33 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
|
||||
</>
|
||||
)}
|
||||
{activeChain === "solana" && (
|
||||
<CreateSolanaWallet
|
||||
managementAccessToken={managementAccessToken}
|
||||
project={project}
|
||||
teamSlug={teamSlug}
|
||||
disabled={solanaPermissionError}
|
||||
/>
|
||||
<>
|
||||
<CreateSolanaWallet
|
||||
project={project}
|
||||
teamSlug={teamSlug}
|
||||
disabled={solanaPermissionError}
|
||||
/>
|
||||
<Select
|
||||
value={selectedSolanaChain}
|
||||
onValueChange={(value) =>
|
||||
setSelectedSolanaChain(
|
||||
value as "solana:mainnet" | "solana:devnet",
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-fit min-w-[180px] rounded-full bg-background hover:bg-accent/50">
|
||||
<SelectValue placeholder="Select network" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl">
|
||||
<SelectItem value="solana:mainnet" className="rounded-lg">
|
||||
Solana Mainnet
|
||||
</SelectItem>
|
||||
<SelectItem value="solana:devnet" className="rounded-lg">
|
||||
Solana Devnet
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -285,6 +313,8 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
|
||||
project={project}
|
||||
teamSlug={teamSlug}
|
||||
client={client}
|
||||
authToken={authToken}
|
||||
chainId={selectedSolanaChain}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -514,11 +544,15 @@ function SolanaWalletRow({
|
||||
project,
|
||||
teamSlug,
|
||||
client,
|
||||
authToken,
|
||||
chainId,
|
||||
}: {
|
||||
wallet: SolanaWallet;
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
client: ThirdwebClient;
|
||||
authToken: string;
|
||||
chainId: "solana:mainnet" | "solana:devnet";
|
||||
}) {
|
||||
const engineService = project.services.find(
|
||||
(s) => s.name === "engineCloud",
|
||||
@@ -554,7 +588,12 @@ function SolanaWalletRow({
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<SolanaWalletBalance publicKey={wallet.publicKey} />
|
||||
<SolanaWalletBalance
|
||||
publicKey={wallet.publicKey}
|
||||
authToken={authToken}
|
||||
clientId={project.publishableKey}
|
||||
chainId={chainId}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
@@ -746,16 +785,27 @@ function WalletBalance({
|
||||
);
|
||||
}
|
||||
|
||||
function SolanaWalletBalance({ publicKey }: { publicKey: string }) {
|
||||
function SolanaWalletBalance({
|
||||
publicKey,
|
||||
authToken,
|
||||
clientId,
|
||||
chainId,
|
||||
}: {
|
||||
publicKey: string;
|
||||
authToken: string;
|
||||
clientId: string;
|
||||
chainId: "solana:mainnet" | "solana:devnet";
|
||||
}) {
|
||||
const balance = useQuery({
|
||||
queryFn: async () => {
|
||||
// TODO: Implement actual Solana balance fetching
|
||||
return {
|
||||
displayValue: "0",
|
||||
symbol: "SOL",
|
||||
};
|
||||
return await fetchSolanaBalance({
|
||||
publicKey,
|
||||
authToken,
|
||||
clientId,
|
||||
chainId,
|
||||
});
|
||||
},
|
||||
queryKey: ["solanaWalletBalance", publicKey],
|
||||
queryKey: ["solanaWalletBalance", publicKey, chainId],
|
||||
});
|
||||
|
||||
if (balance.isFetching) {
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { configure, getSolanaWalletBalance } from "@thirdweb-dev/api";
|
||||
import { THIRDWEB_API_HOST } from "@/constants/urls";
|
||||
|
||||
// Configure the API client to use the correct base URL
|
||||
configure({
|
||||
override: {
|
||||
baseUrl: THIRDWEB_API_HOST,
|
||||
},
|
||||
});
|
||||
|
||||
export async function fetchSolanaBalance({
|
||||
publicKey,
|
||||
authToken,
|
||||
clientId,
|
||||
chainId = "solana:mainnet",
|
||||
}: {
|
||||
publicKey: string;
|
||||
authToken: string;
|
||||
clientId: string;
|
||||
chainId?: "solana:mainnet" | "solana:devnet";
|
||||
}): Promise<{
|
||||
displayValue: string;
|
||||
symbol: string;
|
||||
value: string;
|
||||
decimals: number;
|
||||
} | null> {
|
||||
try {
|
||||
const response = await getSolanaWalletBalance({
|
||||
path: {
|
||||
address: publicKey,
|
||||
},
|
||||
query: {
|
||||
chainId,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": clientId,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.error || !response.data) {
|
||||
console.error(
|
||||
"Error fetching Solana balance:",
|
||||
response.error || "No data returned",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
displayValue: response.data.result.displayValue,
|
||||
symbol: "SOL",
|
||||
value: response.data.result.value,
|
||||
decimals: response.data.result.decimals,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching Solana balance:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -156,7 +156,6 @@ export default async function TransactionsAnalyticsPage(props: {
|
||||
isManagedVault={isManagedVault}
|
||||
client={client}
|
||||
hasTransactions={hasTransactions}
|
||||
managementAccessToken={managementAccessToken ?? undefined}
|
||||
project={project}
|
||||
teamSlug={params.team_slug}
|
||||
testTxWithWallet={searchParams.testTxWithWallet as string | undefined}
|
||||
@@ -207,7 +206,6 @@ export default async function TransactionsAnalyticsPage(props: {
|
||||
evmTotalPages={Math.ceil(eoas.data.totalRecords / pageSize)}
|
||||
evmTotalRecords={eoas.data.totalRecords}
|
||||
evmWallets={eoas.data.items as Wallet[]}
|
||||
managementAccessToken={managementAccessToken ?? undefined}
|
||||
project={project}
|
||||
solanaCurrentPage={solanCurrentPage}
|
||||
solanaTotalPages={Math.ceil(
|
||||
@@ -217,6 +215,7 @@ export default async function TransactionsAnalyticsPage(props: {
|
||||
solanaWallets={solanaAccounts.data.items}
|
||||
teamSlug={params.team_slug}
|
||||
solanaPermissionError={isSolanaPermissionError}
|
||||
authToken={authToken}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+28
-9
@@ -15,18 +15,26 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { createProjectServerWallet } from "../../lib/vault.client";
|
||||
|
||||
export default function CreateServerWallet(props: {
|
||||
export function CreateServerWallet(props: {
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
managementAccessToken: string | undefined;
|
||||
setAsProjectWallet?: boolean;
|
||||
button?: {
|
||||
size?: "default" | "sm" | "lg";
|
||||
iconClassName?: string;
|
||||
};
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
const [label, setLabel] = useState("");
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
const managementAccessToken =
|
||||
props.project.services?.find((service) => service.name === "engineCloud")
|
||||
?.managementAccessToken ?? undefined;
|
||||
|
||||
const createEoaMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
managementAccessToken,
|
||||
@@ -53,12 +61,12 @@ export default function CreateServerWallet(props: {
|
||||
});
|
||||
|
||||
const handleCreateServerWallet = async () => {
|
||||
if (!props.managementAccessToken) {
|
||||
if (!managementAccessToken) {
|
||||
router.push(`/team/${props.teamSlug}/${props.project.slug}/vault`);
|
||||
} else {
|
||||
await createEoaMutation.mutateAsync({
|
||||
label,
|
||||
managementAccessToken: props.managementAccessToken,
|
||||
managementAccessToken: managementAccessToken,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -68,21 +76,32 @@ export default function CreateServerWallet(props: {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className="gap-1.5 rounded-full bg-background"
|
||||
className="gap-1.5 rounded-full bg-background text-foreground"
|
||||
variant="outline"
|
||||
size={props.button?.size}
|
||||
onClick={() =>
|
||||
props.managementAccessToken
|
||||
managementAccessToken
|
||||
? setModalOpen(true)
|
||||
: router.push(`/team/${props.teamSlug}/${props.project.slug}/vault`)
|
||||
}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Spinner className="size-4" />
|
||||
<Spinner
|
||||
className={cn(
|
||||
"size-4 text-muted-foreground",
|
||||
props.button?.iconClassName,
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<PlusIcon className="size-4 text-muted-foreground" />
|
||||
<PlusIcon
|
||||
className={cn(
|
||||
"size-4 text-muted-foreground",
|
||||
props.button?.iconClassName,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{props.managementAccessToken
|
||||
{managementAccessToken
|
||||
? isLoading
|
||||
? "Creating..."
|
||||
: "Create Server Wallet"
|
||||
|
||||
+8
-5
@@ -20,7 +20,6 @@ import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { createSolanaAccount } from "../lib/vault.client";
|
||||
|
||||
export function CreateSolanaWallet(props: {
|
||||
managementAccessToken: string | undefined;
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
disabled?: boolean;
|
||||
@@ -29,14 +28,18 @@ export function CreateSolanaWallet(props: {
|
||||
const [label, setLabel] = useState("");
|
||||
const router = useDashboardRouter();
|
||||
|
||||
const managementAccessToken =
|
||||
props.project.services?.find((service) => service.name === "engineCloud")
|
||||
?.managementAccessToken ?? undefined;
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!props.managementAccessToken) {
|
||||
if (!managementAccessToken) {
|
||||
throw new Error("No management access token");
|
||||
}
|
||||
|
||||
const result = await createSolanaAccount({
|
||||
managementAccessToken: props.managementAccessToken,
|
||||
managementAccessToken: managementAccessToken,
|
||||
label: label.trim(),
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
@@ -67,8 +70,8 @@ export function CreateSolanaWallet(props: {
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="gap-2 rounded-full bg-foreground hover:bg-foreground/90"
|
||||
variant="outline"
|
||||
className="gap-1.5 rounded-full bg-background text-foreground"
|
||||
disabled={props.disabled}
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
|
||||
+1
-1
@@ -193,7 +193,6 @@ export default async function Page(props: {
|
||||
evmTotalPages={Math.ceil(eoas.data.totalRecords / pageSize)}
|
||||
evmTotalRecords={eoas.data.totalRecords}
|
||||
evmWallets={eoas.data.items as Wallet[]}
|
||||
managementAccessToken={managementAccessToken ?? undefined}
|
||||
project={project}
|
||||
solanaCurrentPage={solanCurrentPage}
|
||||
solanaTotalPages={Math.ceil(
|
||||
@@ -203,6 +202,7 @@ export default async function Page(props: {
|
||||
solanaWallets={solanaAccounts.data.items}
|
||||
teamSlug={params.team_slug}
|
||||
solanaPermissionError={isSolanaPermissionError || false}
|
||||
authToken={authToken}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -1,6 +1,13 @@
|
||||
import type { Format } from "@number-flow/react";
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import Image, { type StaticImageData } from "next/image";
|
||||
import type { BuyAndSwapEmbedProps } from "@/components/blocks/BuyAndSwapEmbed";
|
||||
import { FaqAccordion } from "@/components/blocks/faq-section";
|
||||
import ChainsImage from "../assets/chains.png";
|
||||
import RoutesImage from "../assets/routes.png";
|
||||
import TokensImage from "../assets/tokens.png";
|
||||
import { bridgeStats } from "../data";
|
||||
import { AnimatedNumbers } from "./client/animated-numbers";
|
||||
import { UniversalBridgeEmbed } from "./client/UniversalBridgeEmbed";
|
||||
import { BridgePageHeader } from "./header";
|
||||
|
||||
@@ -13,7 +20,7 @@ export function BridgePageUI(props: {
|
||||
<div className="grow flex flex-col">
|
||||
<BridgePageHeader />
|
||||
|
||||
<div className="flex grow items-center justify-center px-4 relative pt-12 pb-20 lg:py-28 min-h-[calc(100dvh-60px)]">
|
||||
<div className="flex grow items-center justify-center px-4 relative pt-12 pb-20 lg:py-28 min-h-[calc(100dvh-60px)] overflow-hidden">
|
||||
<DotsBackgroundPattern />
|
||||
<UniversalBridgeEmbed buyTab={props.buyTab} swapTab={props.swapTab} />
|
||||
</div>
|
||||
@@ -35,24 +42,58 @@ function HeadingSection(props: { title: React.ReactNode }) {
|
||||
<div className="mb-3 lg:mb-6">{props.title}</div>
|
||||
|
||||
<p className="text-muted-foreground text-sm text-pretty text-center lg:text-lg mb-6 lg:mb-8">
|
||||
Seamlessly move your assets across 85+ chains with the best rates and
|
||||
fastest execution
|
||||
Seamlessly move your assets across {bridgeStats.supportedChains} chains
|
||||
with the best rates and fastest execution
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-3 lg:gap-2 items-center justify-center">
|
||||
<DataPill>85+ Chains Supported</DataPill>
|
||||
<DataPill>4500+ Tokens Supported</DataPill>
|
||||
<DataPill>9+ Million Routes Available</DataPill>
|
||||
<div className="flex gap-3 items-center lg:justify-center flex-wrap">
|
||||
<DataSquare data={90} label="Chains Supported" src={ChainsImage} />
|
||||
<DataSquare
|
||||
data={6700}
|
||||
label="Tokens Supported"
|
||||
src={TokensImage}
|
||||
format={{ notation: "compact" }}
|
||||
/>
|
||||
<DataSquare
|
||||
data={14000000}
|
||||
format={{ notation: "compact" }}
|
||||
label="Routes Available"
|
||||
src={RoutesImage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DataPill(props: { children: React.ReactNode }) {
|
||||
function DataSquare(props: {
|
||||
data: number;
|
||||
label: string;
|
||||
src: StaticImageData;
|
||||
format?: Format;
|
||||
imageClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<p className="bg-card flex items-center text-xs lg:text-sm gap-1.5 text-foreground border rounded-full px-8 lg:px-3 py-1.5 hover:text-foreground transition-colors duration-300">
|
||||
{props.children}
|
||||
</p>
|
||||
<div className="py-2 lg:py-0 size-full lg:size-[220px] rounded-xl border hover:bg-card bg-card/50 relative shrink-0 overflow-hidden">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-1 text-3xl lg:text-5xl font-medium tracking-tight font-mono mb-1 h-[45px] lg:h-[56px]">
|
||||
<AnimatedNumbers
|
||||
value={props.data}
|
||||
format={props.format}
|
||||
suffix="+"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-foreground">{props.label}</p>
|
||||
</div>
|
||||
<Image
|
||||
draggable={false}
|
||||
src={props.src}
|
||||
className={cn(
|
||||
"object-cover h-full lg:h-auto absolute right-0 top-0 bottom-0 left-1/2 lg:left-0 lg:top-auto invert dark:invert-0 opacity-50 lg:opacity-100",
|
||||
props.imageClassName,
|
||||
)}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export const bridgeWallets = [
|
||||
createWallet("me.rainbow"),
|
||||
createWallet("io.rabby"),
|
||||
createWallet("io.zerion.wallet"),
|
||||
createWallet("com.okex.wallet"),
|
||||
];
|
||||
|
||||
export function UniversalBridgeEmbed(props: {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import NumberFlow, { type Format } from "@number-flow/react";
|
||||
import { useLayoutEffect, useRef, useState } from "react";
|
||||
|
||||
export function AnimatedNumbers(props: {
|
||||
value: number;
|
||||
format?: Format;
|
||||
suffix?: string;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [value, setValue] = useState(props.value);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (ref.current) {
|
||||
// when the div becomes visible, set the value to the props value
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setValue(props.value);
|
||||
} else {
|
||||
setValue(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(ref.current);
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
}, [props.value]);
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<NumberFlow
|
||||
willChange
|
||||
value={value}
|
||||
format={props.format}
|
||||
suffix={props.suffix}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const bridgeStats = {
|
||||
supportedChains: "90+",
|
||||
supportedTokens: "6700+",
|
||||
supportedRoutes: "14M+",
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { fetchChain } from "@/utils/fetchChain";
|
||||
import { BridgePageUI } from "../../components/bridge-page";
|
||||
import { bridgeStats } from "../../data";
|
||||
import { generateTokenPairSlugs, getTokenPairData } from "./slug-map";
|
||||
|
||||
type Params = {
|
||||
@@ -35,8 +36,7 @@ export async function generateMetadata(
|
||||
}
|
||||
|
||||
const title = getTitle(tokenMetadata);
|
||||
const description =
|
||||
"Bridge and swap 4500+ tokens across 85+ chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing with near-instant finality";
|
||||
const description = `Bridge and swap ${bridgeStats.supportedTokens} tokens across ${bridgeStats.supportedChains} chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing with near-instant finality`;
|
||||
|
||||
const result: Metadata = {
|
||||
title,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
import { isAddress, NATIVE_TOKEN_ADDRESS } from "thirdweb";
|
||||
import { BridgePageUI } from "./components/bridge-page";
|
||||
import { bridgeStats } from "./data";
|
||||
|
||||
const title = "thirdweb Bridge: Buy, Bridge & Swap Crypto on 85+ Chains";
|
||||
const description =
|
||||
"Bridge and swap 4500+ tokens across 85+ chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing with near-instant finality";
|
||||
const title = `thirdweb Bridge: Buy, Bridge & Swap Crypto on ${bridgeStats.supportedChains} Chains`;
|
||||
const description = `Bridge and swap ${bridgeStats.supportedTokens} tokens across ${bridgeStats.supportedChains} chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing with near-instant finality`;
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description,
|
||||
@@ -14,11 +15,50 @@ export const metadata: Metadata = {
|
||||
title,
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
type SearchParams = {
|
||||
[key: string]: string | string[] | undefined;
|
||||
};
|
||||
|
||||
export default async function Page(props: {
|
||||
searchParams: Promise<SearchParams>;
|
||||
}) {
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
const onlyAddress = (v: string) => (isAddress(v) ? v : undefined);
|
||||
const onlyNumber = (v: string) =>
|
||||
Number.isNaN(Number(v)) ? undefined : Number(v);
|
||||
|
||||
// output is buy, input is sell
|
||||
const sellChain = parse(searchParams.inputChain, onlyNumber);
|
||||
const sellCurrency = parse(searchParams.inputCurrency, onlyAddress);
|
||||
|
||||
const buyChain = parse(searchParams.outputChain, onlyNumber);
|
||||
const buyCurrency = parse(searchParams.outputCurrency, onlyAddress);
|
||||
|
||||
return (
|
||||
<BridgePageUI
|
||||
buyTab={undefined}
|
||||
swapTab={undefined}
|
||||
buyTab={{
|
||||
buyToken: buyChain
|
||||
? {
|
||||
chainId: buyChain,
|
||||
tokenAddress: buyCurrency || NATIVE_TOKEN_ADDRESS,
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
swapTab={{
|
||||
sellToken: sellChain
|
||||
? {
|
||||
chainId: sellChain,
|
||||
tokenAddress: sellCurrency || NATIVE_TOKEN_ADDRESS,
|
||||
}
|
||||
: undefined,
|
||||
buyToken: buyChain
|
||||
? {
|
||||
chainId: buyChain,
|
||||
tokenAddress: buyCurrency || NATIVE_TOKEN_ADDRESS,
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
title={
|
||||
<h1 className="text-3xl md:text-6xl font-semibold tracking-tighter text-balance text-center">
|
||||
Bridge and Swap tokens <br className="max-sm:hidden" /> across any
|
||||
@@ -28,3 +68,13 @@ export default function Page() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function parse<T>(
|
||||
value: string | string[] | undefined,
|
||||
fn: (value: string) => T | undefined,
|
||||
): T | undefined {
|
||||
if (typeof value === "string") {
|
||||
return fn(value);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ const loginOptions = [
|
||||
createWallet("io.rabby"),
|
||||
createWallet("me.rainbow"),
|
||||
createWallet("io.zerion.wallet"),
|
||||
createWallet("com.okex.wallet"),
|
||||
];
|
||||
|
||||
const inAppWalletLoginOptions = [
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
import { Img } from "@workspace/ui/components/img";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { ClientOnly } from "@/components/blocks/client-only";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { payAppThirdwebClient } from "../constants";
|
||||
|
||||
export function PayIdPageHeader(props: {
|
||||
projectName: string;
|
||||
projectIcon: string | undefined;
|
||||
}) {
|
||||
return (
|
||||
<div className="border-b border-border/70">
|
||||
<header className="container flex max-w-7xl justify-between py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{props.projectIcon && (
|
||||
<Img
|
||||
src={
|
||||
resolveSchemeWithErrorHandler({
|
||||
uri: props.projectIcon,
|
||||
client: payAppThirdwebClient,
|
||||
}) || ""
|
||||
}
|
||||
alt=""
|
||||
className="rounded-full size-6 object-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
<h2 className="text-xl font-semibold tracking-tight">
|
||||
{props.projectName}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 lg:gap-5">
|
||||
<ToggleThemeButton />
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleThemeButton(props: { className?: string }) {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
return (
|
||||
<ClientOnly
|
||||
ssr={<Skeleton className="size-[36px] rounded-full border bg-accent" />}
|
||||
>
|
||||
<Button
|
||||
aria-label="Toggle theme"
|
||||
className={cn(
|
||||
"h-auto w-auto rounded-full p-2 text-muted-foreground hover:text-foreground",
|
||||
props.className,
|
||||
)}
|
||||
onClick={() => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
}}
|
||||
variant="ghost"
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<SunIcon className="size-5 " />
|
||||
) : (
|
||||
<MoonIcon className="size-5 " />
|
||||
)}
|
||||
</Button>
|
||||
</ClientOnly>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,16 @@
|
||||
import { ShieldCheckIcon } from "lucide-react";
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
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";
|
||||
import { Bridge } from "thirdweb";
|
||||
import { getPaymentLink } from "@/api/universal-bridge/links";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import {
|
||||
API_SERVER_SECRET,
|
||||
DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
} from "@/constants/server-envs";
|
||||
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { resolveEns } from "@/lib/ens";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { PayPageWidget } from "../components/client/PayPageWidget.client";
|
||||
import { payAppThirdwebClient } from "../constants";
|
||||
import { PayIdPageHeader } from "./header";
|
||||
|
||||
const title = "thirdweb Pay";
|
||||
const description = "Fast, secure, and simple payments.";
|
||||
@@ -54,26 +49,10 @@ export default async function PayPage({
|
||||
tokenAddress: paymentLink.destinationToken.address,
|
||||
});
|
||||
|
||||
const chainPromise = getChainMetadata(
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
defineChain(Number(paymentLink.destinationToken.chainId)),
|
||||
);
|
||||
|
||||
const recipientPromise = resolveEns(
|
||||
paymentLink.receiver,
|
||||
getConfiguredThirdwebClient({
|
||||
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
teamId: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
const [tokens, projectMetadata, chain, recipientEnsOrAddress] =
|
||||
await Promise.all([
|
||||
tokensPromise,
|
||||
projectMetadataPromise,
|
||||
chainPromise,
|
||||
recipientPromise,
|
||||
]);
|
||||
const [tokens, projectMetadata] = await Promise.all([
|
||||
tokensPromise,
|
||||
projectMetadataPromise,
|
||||
]);
|
||||
|
||||
const token = tokens[0];
|
||||
if (!token) {
|
||||
@@ -81,122 +60,20 @@ export default async function PayPage({
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative flex flex-col min-h-dvh w-full">
|
||||
<ThemeProvider
|
||||
defaultTheme={theme === "light" ? "light" : "dark"}
|
||||
attribute="class"
|
||||
disableTransitionOnChange
|
||||
enableSystem={false}
|
||||
>
|
||||
<PayIdPageHeader
|
||||
projectIcon={projectMetadata.image || undefined}
|
||||
projectName={projectMetadata.name}
|
||||
/>
|
||||
|
||||
<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="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">
|
||||
<main className="flex justify-center py-12 w-full items-center grow overflow-hidden relative">
|
||||
<DotsBackgroundPattern />
|
||||
<PayPageWidget
|
||||
amount={paymentLink.amount ? BigInt(paymentLink.amount) : undefined}
|
||||
chainId={Number(paymentLink.destinationToken.chainId)}
|
||||
@@ -210,8 +87,8 @@ export default async function PayPage({
|
||||
token={token}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -228,7 +105,24 @@ async function getProjectMetadata(clientId: string) {
|
||||
}
|
||||
|
||||
const { data } = (await response.json()) as {
|
||||
data: { name: string; image: string | null; description: string | null };
|
||||
data: { name: string; image: string | null };
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
function DotsBackgroundPattern(props: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute -inset-x-36 -inset-y-24 text-foreground/20 dark:text-muted-foreground/15 hidden lg:block",
|
||||
props.className,
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: "radial-gradient(currentColor 1px, transparent 1px)",
|
||||
backgroundSize: "24px 24px",
|
||||
maskImage:
|
||||
"radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 50%)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { payAppThirdwebClient } from "app/pay/constants";
|
||||
import { useTheme } from "next-themes";
|
||||
import { createThirdwebClient, NATIVE_TOKEN_ADDRESS, toTokens } from "thirdweb";
|
||||
@@ -63,7 +64,12 @@ export function PayPageWidget({
|
||||
client={
|
||||
clientId ? createThirdwebClient({ clientId }) : payAppThirdwebClient
|
||||
}
|
||||
image={image}
|
||||
className={cn(
|
||||
"shadow-xl",
|
||||
!image &&
|
||||
"[&_.tw-header-image]:invert dark:[&_.tw-header-image]:invert-0",
|
||||
)}
|
||||
image={image || "/assets/pay/general-pay.png"}
|
||||
name={name}
|
||||
onSuccess={() => {
|
||||
reportPaymentLinkBuySuccessful();
|
||||
|
||||
@@ -1,399 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { payAppThirdwebClient } from "app/pay/constants";
|
||||
import { ChevronDownIcon, CreditCardIcon } from "lucide-react";
|
||||
import { useCallback, useId, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
createThirdwebClient,
|
||||
defineChain,
|
||||
getContract,
|
||||
type ThirdwebClient,
|
||||
toUnits,
|
||||
} from "thirdweb";
|
||||
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
|
||||
import { resolveScheme, upload } from "thirdweb/storage";
|
||||
import { FileInput } from "@/components/blocks/FileInput";
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { TokenSelector } from "@/components/blocks/TokenSelector";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { resolveEns } from "@/lib/ens";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function PaymentLinkForm() {
|
||||
const [chainId, setChainId] = useState<number>();
|
||||
const [recipientAddress, setRecipientAddress] = useState("");
|
||||
// TODO - clean this up later
|
||||
const [tokenAddressWithChain, setTokenAddressWithChain] = useState("");
|
||||
const [amount, setAmount] = useState("");
|
||||
const [title, setTitle] = useState("");
|
||||
const [image, setImage] = useState<File | null>(null);
|
||||
const [imageUri, setImageUri] = useState<string>("");
|
||||
const [uploadingImage, setUploadingImage] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [paymentUrl, setPaymentUrl] = useState<string>("");
|
||||
|
||||
const isFormComplete = useMemo(() => {
|
||||
return chainId && recipientAddress && tokenAddressWithChain && amount;
|
||||
}, [chainId, recipientAddress, tokenAddressWithChain, amount]);
|
||||
|
||||
const handleImageUpload = useCallback(async (file: File) => {
|
||||
try {
|
||||
setImage(file);
|
||||
setUploadingImage(true);
|
||||
|
||||
const uploadClient = createThirdwebClient({
|
||||
clientId: "7ae789153cf9ecde8f35649f2d8a4333",
|
||||
});
|
||||
const uri = await upload({
|
||||
client: uploadClient,
|
||||
files: [file],
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const resolvedUrl = resolveScheme({
|
||||
client: uploadClient,
|
||||
uri,
|
||||
});
|
||||
|
||||
setImageUri(resolvedUrl);
|
||||
toast.success("Image uploaded successfully");
|
||||
} catch (error) {
|
||||
console.error("Error uploading image:", error);
|
||||
toast.error("Failed to upload image");
|
||||
setImage(null);
|
||||
} finally {
|
||||
setUploadingImage(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
if (
|
||||
!chainId ||
|
||||
!recipientAddress ||
|
||||
!tokenAddressWithChain ||
|
||||
!amount
|
||||
) {
|
||||
throw new Error("All fields are required");
|
||||
}
|
||||
|
||||
const inputs = await parseInputs(
|
||||
payAppThirdwebClient,
|
||||
chainId,
|
||||
tokenAddressWithChain,
|
||||
recipientAddress,
|
||||
amount,
|
||||
);
|
||||
|
||||
// Build payment URL
|
||||
const params = new URLSearchParams({
|
||||
amount: inputs.amount.toString(),
|
||||
chainId: inputs.chainId.toString(),
|
||||
clientId: payAppThirdwebClient.clientId,
|
||||
recipientAddress: inputs.recipientAddress,
|
||||
tokenAddress: inputs.tokenAddress,
|
||||
});
|
||||
|
||||
// Add title as name parameter if provided
|
||||
if (title) {
|
||||
params.set("name", title);
|
||||
}
|
||||
|
||||
// Add image URI if available
|
||||
if (imageUri) {
|
||||
params.set("image", imageUri);
|
||||
}
|
||||
|
||||
const url = `${window.location.origin}/pay?${params.toString()}`;
|
||||
setPaymentUrl(url);
|
||||
toast.success("Payment created!");
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "An error occurred");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[amount, chainId, imageUri, recipientAddress, title, tokenAddressWithChain],
|
||||
);
|
||||
|
||||
const handlePreview = useCallback(async () => {
|
||||
if (!chainId || !recipientAddress || !tokenAddressWithChain || !amount) {
|
||||
toast.error("Please fill in all fields first");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const inputs = await parseInputs(
|
||||
payAppThirdwebClient,
|
||||
chainId,
|
||||
tokenAddressWithChain,
|
||||
recipientAddress,
|
||||
amount,
|
||||
);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
amount: inputs.amount.toString(),
|
||||
chainId: inputs.chainId.toString(),
|
||||
clientId: payAppThirdwebClient.clientId,
|
||||
recipientAddress: inputs.recipientAddress,
|
||||
tokenAddress: inputs.tokenAddress,
|
||||
});
|
||||
|
||||
// Add title as name parameter if provided
|
||||
if (title) {
|
||||
params.set("name", title);
|
||||
}
|
||||
|
||||
// Add image URI if available
|
||||
if (imageUri) {
|
||||
params.set("image", imageUri);
|
||||
}
|
||||
|
||||
window.open(`/pay?${params.toString()}`, "_blank");
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "An error occurred");
|
||||
}
|
||||
}, [
|
||||
amount,
|
||||
chainId,
|
||||
imageUri,
|
||||
recipientAddress,
|
||||
title,
|
||||
tokenAddressWithChain,
|
||||
]);
|
||||
|
||||
const [selectedChainId, selectedTokenAddress] = tokenAddressWithChain
|
||||
? tokenAddressWithChain.split(":")
|
||||
: [];
|
||||
|
||||
const recipientId = useId();
|
||||
const amountId = useId();
|
||||
const titleId = useId();
|
||||
|
||||
return (
|
||||
<Card className="mx-auto w-full max-w-[500px]">
|
||||
<CardHeader>
|
||||
<div className="flex flex-col items-center gap-2 sm:flex-row sm:gap-2">
|
||||
<div className="rounded-lg border border-muted p-1.5 sm:p-2">
|
||||
<CreditCardIcon className="size-5 sm:size-6" />
|
||||
</div>
|
||||
<CardTitle className="text-center sm:text-left">
|
||||
Create a Payment
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">Network</Label>
|
||||
<SingleNetworkSelector
|
||||
chainId={chainId}
|
||||
className="w-full"
|
||||
client={payAppThirdwebClient}
|
||||
disableTestnets
|
||||
onChange={setChainId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">Token</Label>
|
||||
<TokenSelector
|
||||
addNativeTokenIfMissing={false}
|
||||
chainId={chainId ?? undefined}
|
||||
className="w-full"
|
||||
client={payAppThirdwebClient}
|
||||
disabled={!chainId}
|
||||
enabled={!!chainId}
|
||||
onChange={(value) => {
|
||||
setTokenAddressWithChain(`${value.chainId}:${value.address}`);
|
||||
}}
|
||||
selectedToken={
|
||||
selectedChainId && selectedTokenAddress
|
||||
? {
|
||||
address: selectedTokenAddress,
|
||||
chainId: Number(selectedChainId),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
showCheck={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm" htmlFor={recipientId}>
|
||||
Recipient Address
|
||||
</Label>
|
||||
<Input
|
||||
className="w-full"
|
||||
id={recipientId}
|
||||
onChange={(e) => setRecipientAddress(e.target.value)}
|
||||
placeholder="Address or ENS"
|
||||
required
|
||||
value={recipientAddress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm" htmlFor={amountId}>
|
||||
Amount
|
||||
</Label>
|
||||
<Input
|
||||
className="w-full"
|
||||
id={amountId}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
placeholder="0.0"
|
||||
required
|
||||
step="any"
|
||||
type="number"
|
||||
value={amount}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between px-0 text-muted-foreground hover:bg-transparent",
|
||||
)}
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<span>Advanced Options</span>
|
||||
<ChevronDownIcon
|
||||
className={`size-4 transition-transform duration-200 ease-in-out ${
|
||||
showAdvanced ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"grid transition-all duration-200 ease-in-out",
|
||||
showAdvanced
|
||||
? "grid-rows-[1fr] opacity-100"
|
||||
: "grid-rows-[0fr] opacity-0",
|
||||
)}
|
||||
>
|
||||
<div className={cn(showAdvanced ? "" : "overflow-hidden")}>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm" htmlFor={titleId}>
|
||||
Title
|
||||
</Label>
|
||||
<Input
|
||||
className="w-full"
|
||||
id={titleId}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Payment for..."
|
||||
value={title}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">Image</Label>
|
||||
<div className="w-full">
|
||||
<FileInput
|
||||
accept={{ "image/*": [] }}
|
||||
className="!rounded-md aspect-square h-24 w-full"
|
||||
client={payAppThirdwebClient}
|
||||
helperText="image"
|
||||
isDisabled={uploadingImage}
|
||||
isDisabledText="Uploading..."
|
||||
selectOrUpload="Upload"
|
||||
setValue={handleImageUpload}
|
||||
value={image || imageUri}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-4">
|
||||
{paymentUrl && (
|
||||
<CopyTextButton
|
||||
className="w-full justify-between truncate bg-background px-3 py-2"
|
||||
copyIconPosition="right"
|
||||
textToCopy={paymentUrl}
|
||||
textToShow={paymentUrl}
|
||||
tooltip="Copy Payment Link"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
className="flex-1"
|
||||
disabled={isLoading || !isFormComplete}
|
||||
onClick={handlePreview}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
disabled={isLoading || !isFormComplete}
|
||||
type="submit"
|
||||
>
|
||||
{isLoading ? "Creating..." : "Create"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
async function parseInputs(
|
||||
client: ThirdwebClient,
|
||||
chainId: number,
|
||||
tokenAddressWithChain: string,
|
||||
recipientAddressOrEns: string,
|
||||
decimalAmount: string,
|
||||
) {
|
||||
const [_chainId, tokenAddress] = tokenAddressWithChain.split(":");
|
||||
if (Number(_chainId) !== chainId) {
|
||||
throw new Error("Chain ID does not match token chain");
|
||||
}
|
||||
if (!tokenAddress) {
|
||||
throw new Error("Missing token address");
|
||||
}
|
||||
|
||||
const ensPromise = resolveEns(recipientAddressOrEns, client);
|
||||
const currencyPromise = getCurrencyMetadata({
|
||||
contract: getContract({
|
||||
address: tokenAddress,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(chainId),
|
||||
client,
|
||||
}),
|
||||
});
|
||||
const [ens, currencyMetadata] = await Promise.all([
|
||||
ensPromise,
|
||||
currencyPromise,
|
||||
]);
|
||||
if (!ens.address) {
|
||||
throw new Error("Invalid recipient address");
|
||||
}
|
||||
|
||||
const amountInWei = toUnits(decimalAmount, currencyMetadata.decimals);
|
||||
|
||||
return {
|
||||
amount: amountInWei,
|
||||
chainId,
|
||||
recipientAddress: ens.address,
|
||||
tokenAddress,
|
||||
};
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export type PayParams = {
|
||||
chainId: string;
|
||||
recipientAddress: string;
|
||||
tokenAddress: string;
|
||||
amount: string;
|
||||
name?: string;
|
||||
image?: string;
|
||||
theme?: "light" | "dark";
|
||||
redirectUri?: string;
|
||||
clientId?: string;
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTheme } from "next-themes";
|
||||
import { ClientOnly } from "@/components/blocks/client-only";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThirdwebMiniLogo } from "../../(app)/components/ThirdwebMiniLogo";
|
||||
|
||||
export function PayLandingHeader(props: { containerClassName?: string }) {
|
||||
return (
|
||||
<div className="border-b border-border/70">
|
||||
<header
|
||||
className={cn(
|
||||
"container flex max-w-7xl justify-between py-4",
|
||||
props.containerClassName,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-6">
|
||||
<Link className="flex items-center gap-2" href="/home">
|
||||
<ThirdwebMiniLogo className="h-5" />
|
||||
<span className="font-bold text-2xl tracking-tight">thirdweb</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 lg:gap-5">
|
||||
<ToggleThemeButton />
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleThemeButton(props: { className?: string }) {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
return (
|
||||
<ClientOnly
|
||||
ssr={<Skeleton className="size-[36px] rounded-full border bg-accent" />}
|
||||
>
|
||||
<Button
|
||||
aria-label="Toggle theme"
|
||||
className={cn(
|
||||
"h-auto w-auto rounded-full p-2 text-muted-foreground hover:text-foreground",
|
||||
props.className,
|
||||
)}
|
||||
onClick={() => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
}}
|
||||
variant="ghost"
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<SunIcon className="size-5 " />
|
||||
) : (
|
||||
<MoonIcon className="size-5 " />
|
||||
)}
|
||||
</Button>
|
||||
</ClientOnly>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { BuyWidget, type BuyWidgetProps } from "thirdweb/react";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
import { payLandingWallets } from "./wallets";
|
||||
|
||||
const client = getClientThirdwebClient();
|
||||
|
||||
export function StyledBuyWidget(
|
||||
props: Omit<
|
||||
BuyWidgetProps,
|
||||
"client" | "theme" | "onSuccess" | "onError" | "onCancel"
|
||||
> & {
|
||||
successUrl?: string;
|
||||
},
|
||||
) {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<BuyWidget
|
||||
{...props}
|
||||
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
|
||||
client={client}
|
||||
className="shadow-xl"
|
||||
connectOptions={{
|
||||
wallets: payLandingWallets,
|
||||
}}
|
||||
onSuccess={() => {
|
||||
if (props.successUrl) {
|
||||
try {
|
||||
const url = new URL(props.successUrl);
|
||||
url.searchParams.set("success", "true");
|
||||
window.location.href = url.toString();
|
||||
} catch (error) {
|
||||
// Log URL construction error for debugging
|
||||
console.error(
|
||||
"Failed to construct redirect URL:",
|
||||
props.successUrl,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createWallet } from "thirdweb/wallets";
|
||||
import { appMetadata } from "@/constants/connect";
|
||||
|
||||
export const payLandingWallets = [
|
||||
createWallet("io.metamask"),
|
||||
createWallet("com.coinbase.wallet", {
|
||||
appMetadata,
|
||||
}),
|
||||
createWallet("me.rainbow"),
|
||||
createWallet("io.rabby"),
|
||||
createWallet("io.zerion.wallet"),
|
||||
createWallet("com.okex.wallet"),
|
||||
];
|
||||
@@ -18,15 +18,11 @@ export default async function PayLayout({
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={cn(
|
||||
"h-screen w-screen bg-background font-sans antialiased",
|
||||
"min-h-dvh w-screen bg-background font-sans antialiased flex flex-col",
|
||||
fontSans.variable,
|
||||
)}
|
||||
>
|
||||
<PayProviders>
|
||||
<div className="relative mx-auto flex h-full w-full items-center justify-center overflow-x-hidden overflow-y-scroll">
|
||||
{children}
|
||||
</div>
|
||||
</PayProviders>
|
||||
<PayProviders>{children}</PayProviders>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
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";
|
||||
import { PaymentLinkForm } from "./components/client/PaymentLinkForm.client";
|
||||
import { PayPageWidget } from "./components/client/PayPageWidget.client";
|
||||
import type { PayParams } from "./components/types";
|
||||
import { payAppThirdwebClient } from "./constants";
|
||||
import { defineChain, isAddress } from "thirdweb";
|
||||
import { PayLandingHeader } from "./landing/header";
|
||||
import { StyledBuyWidget } from "./landing/styled-buy-widget";
|
||||
|
||||
const title = "thirdweb Payments";
|
||||
const description = "Fast, secure, and simple payments.";
|
||||
@@ -20,97 +17,97 @@ export const metadata: Metadata = {
|
||||
title,
|
||||
};
|
||||
|
||||
export default async function PayPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<PayParams>;
|
||||
type SearchParams = {
|
||||
[key: string]: string | string[] | undefined;
|
||||
};
|
||||
|
||||
const onlyAddress = (v: string) => (isAddress(v) ? v : undefined);
|
||||
const onlyNumber = (v: string) =>
|
||||
Number.isNaN(Number(v)) ? undefined : Number(v);
|
||||
|
||||
/**
|
||||
* Validates and normalizes a URL string.
|
||||
* Only allows http: and https: protocols with a valid hostname.
|
||||
* @returns normalized URL string on success, undefined on failure
|
||||
*/
|
||||
const onlyUrl = (v: string): string | undefined => {
|
||||
try {
|
||||
const url = new URL(v);
|
||||
// Only allow http or https protocols
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||
return undefined;
|
||||
}
|
||||
// Ensure hostname is non-empty
|
||||
if (!url.hostname) {
|
||||
return undefined;
|
||||
}
|
||||
// Return normalized URL
|
||||
return url.toString();
|
||||
} catch {
|
||||
// Invalid URL format
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export default async function PayPage(props: {
|
||||
searchParams: Promise<SearchParams>;
|
||||
}) {
|
||||
const params = await searchParams;
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
// If no query parameters are provided, show the form
|
||||
if (
|
||||
!params.chainId &&
|
||||
!params.recipientAddress &&
|
||||
!params.tokenAddress &&
|
||||
!params.amount
|
||||
) {
|
||||
return (
|
||||
<ThemeProvider
|
||||
forcedTheme={params.theme === "light" ? "light" : "dark"}
|
||||
attribute="class"
|
||||
disableTransitionOnChange
|
||||
enableSystem={false}
|
||||
>
|
||||
<PaymentLinkForm />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Validate query parameters
|
||||
if (Array.isArray(params.chainId)) {
|
||||
throw new Error("A single chainId parameter is required.");
|
||||
}
|
||||
if (Array.isArray(params.recipientAddress)) {
|
||||
throw new Error("A single recipientAddress parameter is required.");
|
||||
}
|
||||
if (Array.isArray(params.tokenAddress)) {
|
||||
throw new Error("A single tokenAddress parameter is required.");
|
||||
}
|
||||
if (Array.isArray(params.amount)) {
|
||||
throw new Error("A single amount parameter is required.");
|
||||
}
|
||||
if (Array.isArray(params.clientId)) {
|
||||
throw new Error("A single clientId parameter is required.");
|
||||
}
|
||||
if (Array.isArray(params.redirectUri)) {
|
||||
throw new Error("A single redirectUri parameter is required.");
|
||||
}
|
||||
|
||||
// Use any provided clientId or use the dashboard client
|
||||
const client =
|
||||
params.clientId && !Array.isArray(params.clientId)
|
||||
? createThirdwebClient({ clientId: params.clientId })
|
||||
: payAppThirdwebClient;
|
||||
|
||||
const tokenContract = getContract({
|
||||
address: params.tokenAddress,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(Number(params.chainId)),
|
||||
client: payAppThirdwebClient,
|
||||
});
|
||||
const {
|
||||
symbol,
|
||||
decimals,
|
||||
name: tokenName,
|
||||
} = await getCurrencyMetadata({
|
||||
contract: tokenContract,
|
||||
});
|
||||
const token = {
|
||||
address: checksumAddress(params.tokenAddress),
|
||||
chainId: Number(params.chainId),
|
||||
decimals,
|
||||
name: tokenName,
|
||||
symbol,
|
||||
};
|
||||
const receiver = parse(searchParams.receiver, onlyAddress);
|
||||
const token = parse(searchParams.token, onlyAddress);
|
||||
const chain = parse(searchParams.chain, onlyNumber);
|
||||
const amount = parse(searchParams.amount, onlyNumber);
|
||||
const successUrl = parse(searchParams.successUrl, onlyUrl);
|
||||
|
||||
return (
|
||||
<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}
|
||||
/>
|
||||
<div className="flex flex-col grow">
|
||||
<PayLandingHeader />
|
||||
<div className="flex-1 flex items-center justify-center py-20 relative overflow-hidden">
|
||||
<DotsBackgroundPattern />
|
||||
<StyledBuyWidget
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain={chain ? defineChain(chain) : undefined}
|
||||
tokenAddress={token}
|
||||
receiverAddress={receiver}
|
||||
amount={amount ? amount.toString() : undefined}
|
||||
successUrl={successUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function parse<T>(
|
||||
value: string | string[] | undefined,
|
||||
fn: (value: string) => T | undefined,
|
||||
): T | undefined {
|
||||
if (typeof value === "string") {
|
||||
return fn(value);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function DotsBackgroundPattern(props: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute -inset-x-36 -inset-y-24 text-foreground/20 dark:text-muted-foreground/20 hidden lg:block",
|
||||
props.className,
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: "radial-gradient(currentColor 1px, transparent 1px)",
|
||||
backgroundSize: "24px 24px",
|
||||
maskImage:
|
||||
"radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 50%)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@abstract-foundation/agw-react": "^1.6.4",
|
||||
"@abstract-foundation/agw-react": "^1.10.0",
|
||||
"@ai-sdk/react": "^2.0.25",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
|
||||
@@ -18,8 +18,7 @@ export async function GET(request: NextRequest) {
|
||||
// const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_SMART_WALLET as string;
|
||||
const ENGINE_VAULT_ACCESS_TOKEN = process.env
|
||||
.ENGINE_VAULT_ACCESS_TOKEN as string;
|
||||
// const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
|
||||
const API_URL = "http://localhost:3030";
|
||||
const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
|
||||
|
||||
const twFacilitator = facilitator({
|
||||
baseUrl: `${API_URL}/v1/payments/x402`,
|
||||
@@ -42,7 +41,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const amount = queryParams.get("amount") || "0.01";
|
||||
const payTo = queryParams.get("payTo") ?? undefined;
|
||||
const tokenAddress = queryParams.get("tokenAddress") || token.address;
|
||||
const tokenAddress = queryParams.get("tokenAddress");
|
||||
const decimals = queryParams.get("decimals") || token.decimals.toString();
|
||||
const waitUntil =
|
||||
(queryParams.get("waitUntil") as "simulated" | "submitted" | "confirmed") ||
|
||||
@@ -54,13 +53,15 @@ export async function GET(request: NextRequest) {
|
||||
paymentData,
|
||||
network: defineChain(Number(chainId)),
|
||||
payTo,
|
||||
price: {
|
||||
amount: toUnits(amount, parseInt(decimals)).toString(),
|
||||
asset: {
|
||||
address: tokenAddress as `0x${string}`,
|
||||
decimals: decimals ? parseInt(decimals) : token.decimals,
|
||||
},
|
||||
},
|
||||
price: tokenAddress
|
||||
? {
|
||||
amount: toUnits(amount, parseInt(decimals)).toString(),
|
||||
asset: {
|
||||
address: tokenAddress as `0x${string}`,
|
||||
decimals: decimals ? parseInt(decimals) : token.decimals,
|
||||
},
|
||||
}
|
||||
: amount,
|
||||
routeConfig: {
|
||||
description: "Access to paid content",
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function Page() {
|
||||
<PageLayout
|
||||
icon={PlaneIcon}
|
||||
description={description}
|
||||
docsLink="https://thirdweb-engine.apidocumentation.com/reference#tag/erc20/POST/contract/{chain}/{contractAddress}/erc20/mint-batch-to?utm_source=playground"
|
||||
docsLink="https://engine.thirdweb.com/reference"
|
||||
title={title}
|
||||
>
|
||||
<EngineAirdropPreview />
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function Page() {
|
||||
<PageLayout
|
||||
icon={StampIcon}
|
||||
description={description}
|
||||
docsLink="https://thirdweb-engine.apidocumentation.com/reference#tag/erc1155/POST/contract/{chain}/{contractAddress}/erc1155/mint-to?utm_source=playground"
|
||||
docsLink="https://engine.thirdweb.com/reference"
|
||||
title={title}
|
||||
>
|
||||
<EngineMintPreview />
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
import {
|
||||
Callout,
|
||||
OpenSourceCard,
|
||||
createMetadata,
|
||||
InstallTabs,
|
||||
SDKCard,
|
||||
Grid,
|
||||
ArticleIconCard,
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
} from "@doc";
|
||||
import {
|
||||
ReactIcon,
|
||||
TypeScriptIcon,
|
||||
UnityIcon,
|
||||
DotNetIcon,
|
||||
UnrealEngineIcon,
|
||||
} from "@/icons";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "Find Routes",
|
||||
icon: "payments",
|
||||
},
|
||||
title: "Find Routes",
|
||||
description: "Find available routes for bridging, swapping, and paying.",
|
||||
});
|
||||
|
||||
# Find Routes
|
||||
|
||||
Use thirdweb's `Bridge.routes` utility to find all routes between tokens.
|
||||
|
||||
### Get All Routes
|
||||
|
||||
Retrieve all supported routes:
|
||||
|
||||
```typescript
|
||||
import { Bridge } from "thirdweb";
|
||||
|
||||
const routes = await Bridge.routes({
|
||||
client,
|
||||
});
|
||||
```
|
||||
|
||||
This returns an array of route objects with information about supported token pairs:
|
||||
|
||||
```typescript
|
||||
[
|
||||
{
|
||||
destinationToken: {
|
||||
address: "0x12c88a3C30A7AaBC1dd7f2c08a97145F5DCcD830",
|
||||
chainId: 1,
|
||||
decimals: 18,
|
||||
iconUri: "https://assets.coingecko.com/coins/images/37207/standard/G.jpg",
|
||||
name: "G7",
|
||||
symbol: "G7",
|
||||
},
|
||||
originToken: {
|
||||
address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
chainId: 480,
|
||||
decimals: 18,
|
||||
iconUri: "https://assets.relay.link/icons/1/light.png",
|
||||
name: "Ether",
|
||||
symbol: "ETH",
|
||||
}
|
||||
},
|
||||
// ... more routes
|
||||
]
|
||||
```
|
||||
|
||||
### Filter Routes by Origin
|
||||
|
||||
Get all routes starting from a specific chain and token:
|
||||
|
||||
```typescript
|
||||
import { Bridge } from "thirdweb";
|
||||
|
||||
// Get all routes starting from mainnet ETH
|
||||
const routes = await Bridge.routes({
|
||||
originChainId: 1,
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
client,
|
||||
});
|
||||
```
|
||||
|
||||
### Filter Routes by Destination
|
||||
|
||||
Find routes that end at a specific destination:
|
||||
|
||||
```typescript
|
||||
import { Bridge } from "thirdweb";
|
||||
|
||||
// Get all routes ending at USDC on Optimism
|
||||
const routes = await Bridge.routes({
|
||||
destinationChainId: 10,
|
||||
destinationTokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
|
||||
client,
|
||||
});
|
||||
```
|
||||
|
||||
### Filter Routes by Both Origin and Destination
|
||||
|
||||
Find direct routes between specific token pairs:
|
||||
|
||||
```typescript
|
||||
import { Bridge } from "thirdweb";
|
||||
|
||||
// Get routes from mainnet ETH to USDC on Optimism
|
||||
const routes = await Bridge.routes({
|
||||
originChainId: 1,
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
destinationChainId: 10,
|
||||
destinationTokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
|
||||
client,
|
||||
});
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
For large result sets, use pagination to retrieve routes in chunks:
|
||||
|
||||
```typescript
|
||||
import { Bridge } from "thirdweb";
|
||||
|
||||
// Get the first 10 routes starting from mainnet ETH
|
||||
const routes = await Bridge.routes({
|
||||
originChainId: 1,
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
client,
|
||||
});
|
||||
|
||||
// Get the next 10 routes
|
||||
const nextRoutes = await Bridge.routes({
|
||||
originChainId: 1,
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
limit: 10,
|
||||
offset: 10,
|
||||
client,
|
||||
});
|
||||
```
|
||||
|
||||
### Limit Route Complexity
|
||||
|
||||
Control the maximum number of steps in the returned routes:
|
||||
|
||||
```typescript
|
||||
import { Bridge } from "thirdweb";
|
||||
|
||||
// Get only direct routes (1 step)
|
||||
const directRoutes = await Bridge.routes({
|
||||
originChainId: 1,
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
maxSteps: 1,
|
||||
client,
|
||||
});
|
||||
```
|
||||
|
||||
## Going further
|
||||
|
||||
To connect with other auth strategies, use external wallets, or sponsor gas for users, check out the following guides:
|
||||
|
||||
- [Send a Payment](/payments/send)
|
||||
- [Get Token Prices](/payments/prices)
|
||||
|
||||
## Explore Full API References
|
||||
|
||||
- [Bridge.routes](/references/typescript/v5/routes)
|
||||
@@ -60,7 +60,6 @@ You can execute this quote exactly like a buy. See the [Send a Payment](/payment
|
||||
To connect with other auth strategies, use external wallets, or sponsor gas for users, check out the following guides:
|
||||
|
||||
- [Send a Payment](/payments/send)
|
||||
- [Get Routes](/payments/routes)
|
||||
|
||||
## Explore Full API References
|
||||
|
||||
|
||||
@@ -28,10 +28,6 @@ export const sidebar: SideBar = {
|
||||
href: `${bridgeSlug}/tokens`,
|
||||
name: "Get Token Prices",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/routes`,
|
||||
name: "Get Routes",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/bridge-widget-script`,
|
||||
name: "BridgeWidget Script",
|
||||
|
||||
@@ -255,23 +255,6 @@ export class SwapManager {
|
||||
|
||||
throw new Error("Transaction timeout - please check status manually");
|
||||
}
|
||||
|
||||
async getAvailableRoutes(params: {
|
||||
originChainId?: number;
|
||||
originTokenAddress?: string;
|
||||
destinationChainId?: number;
|
||||
destinationTokenAddress?: string;
|
||||
limit?: number;
|
||||
}) {
|
||||
try {
|
||||
return await Bridge.routes({
|
||||
...params,
|
||||
client: this.client,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get routes: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -319,92 +302,6 @@ async function swapEthToMatic(amount: string, userAccount: Account) {
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Price Comparison
|
||||
|
||||
Compare prices across multiple routes to find the best deal:
|
||||
|
||||
```typescript
|
||||
interface RouteComparison {
|
||||
route: any;
|
||||
quote: SwapQuote;
|
||||
priceImpact: number;
|
||||
gasEstimate: bigint;
|
||||
}
|
||||
|
||||
export class PriceComparator {
|
||||
private swapManager: SwapManager;
|
||||
|
||||
constructor(swapManager: SwapManager) {
|
||||
this.swapManager = swapManager;
|
||||
}
|
||||
|
||||
async compareRoutes(
|
||||
fromToken: { chainId: number; address: string },
|
||||
amount: bigint,
|
||||
userAddress: string
|
||||
): Promise<RouteComparison[]> {
|
||||
// Get available routes
|
||||
const routes = await this.swapManager.getAvailableRoutes({
|
||||
originChainId: fromToken.chainId,
|
||||
originTokenAddress: fromToken.address,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
// Get quotes for each route
|
||||
const comparisons: RouteComparison[] = [];
|
||||
|
||||
for (const route of routes) {
|
||||
try {
|
||||
const quote = await this.swapManager.getQuote({
|
||||
fromChainId: route.originToken.chainId,
|
||||
fromTokenAddress: route.originToken.address,
|
||||
toChainId: route.destinationToken.chainId,
|
||||
toTokenAddress: route.destinationToken.address,
|
||||
amount,
|
||||
userAddress,
|
||||
client: this.swapManager.client,
|
||||
});
|
||||
|
||||
const priceImpact = this.calculatePriceImpact(quote, amount);
|
||||
const gasEstimate = this.estimateGasCosts(quote);
|
||||
|
||||
comparisons.push({
|
||||
route,
|
||||
quote,
|
||||
priceImpact,
|
||||
gasEstimate,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(`Failed to get quote for route ${route.originToken.symbol} -> ${route.destinationToken.symbol}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by best output amount
|
||||
return comparisons.sort((a, b) =>
|
||||
Number(b.quote.destinationAmount) - Number(a.quote.destinationAmount)
|
||||
);
|
||||
}
|
||||
|
||||
private calculatePriceImpact(quote: SwapQuote, inputAmount: bigint): number {
|
||||
// Simplified price impact calculation
|
||||
const ratio = Number(quote.destinationAmount) / Number(quote.originAmount);
|
||||
return Math.abs(1 - ratio) * 100; // Percentage
|
||||
}
|
||||
|
||||
private estimateGasCosts(quote: SwapQuote): bigint {
|
||||
// Sum up gas estimates from all transactions
|
||||
return quote.steps.reduce((total, step) => {
|
||||
return total + step.transactions.reduce((stepTotal: bigint, tx: any) => {
|
||||
return stepTotal + (tx.gasLimit || 200000n); // Default estimate
|
||||
}, 0n);
|
||||
}, 0n);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Callout variant="info">
|
||||
**Gas Optimization Tip:** For frequently traded pairs, consider caching route data to reduce API calls and improve UI responsiveness.
|
||||
</Callout>
|
||||
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
@@ -415,20 +312,16 @@ export class PriceComparator {
|
||||
- **Cross-chain swaps** - Exchange tokens across 50+ supported chains
|
||||
- **Real-time quotes** - Get up-to-date pricing and execution estimates
|
||||
- **Route optimization** - Find the best path for any token pair
|
||||
- **Price comparison** - Compare multiple routes to maximize output
|
||||
- **Status tracking** - Monitor cross-chain transaction progress
|
||||
|
||||
|
||||
|
||||
## Going Further
|
||||
|
||||
- [Send a Payment](/payments/send) - Learn about peer-to-peer transfers
|
||||
- [Get Routes](/payments/routes) - Explore route discovery APIs
|
||||
- [Token Prices](/payments/tokens) - Access real-time token pricing
|
||||
- [Token Prices](/bridge/tokens) - Access real-time token pricing
|
||||
- [Webhooks](/payments/webhooks) - Get notified when swaps complete
|
||||
|
||||
## API Reference
|
||||
|
||||
- [Bridge.Buy.prepare](/references/typescript/v5/buy/prepare)
|
||||
- [Bridge.routes](/references/typescript/v5/routes)
|
||||
- [Bridge.status](/references/typescript/v5/status)
|
||||
@@ -119,7 +119,6 @@ const nextTokens = await Bridge.tokens({
|
||||
To connect with other auth strategies, use external wallets, or sponsor gas for users, check out the following guides:
|
||||
|
||||
- [Send a Payment](/payments/send)
|
||||
- [Get Routes](/payments/routes)
|
||||
|
||||
## Explore Full API References
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ function Hero() {
|
||||
<h1 className="mb-3 font-bold text-4xl tracking-tighter lg:text-6xl">
|
||||
thirdweb Documentation
|
||||
</h1>
|
||||
<p className="mb-8 max-w-md text-lg text-muted-foreground leading-normal">
|
||||
Development framework for building onchain apps, games, and agents.
|
||||
<p className="mb-8 max-w-lg text-lg text-muted-foreground leading-normal text-pretty">
|
||||
Platform for building the next generation of internet products
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<ChatButton />
|
||||
|
||||
@@ -264,7 +264,6 @@ Once this code completes, your payment has been fully executed. Payments normall
|
||||
|
||||
- [Sell a Product](/payments/products)
|
||||
- [Token Prices](/payments/tokens)
|
||||
- [Routes](/payments/routes)
|
||||
|
||||
## Explore API References
|
||||
|
||||
|
||||
@@ -49,10 +49,6 @@ export const sidebar: SideBar = {
|
||||
href: `/bridge/tokens`,
|
||||
name: "Get Token Prices",
|
||||
},
|
||||
{
|
||||
href: `/bridge/routes`,
|
||||
name: "Get Routes",
|
||||
},
|
||||
{
|
||||
href: `${paymentsSlug}/webhooks`,
|
||||
name: "Webhooks",
|
||||
|
||||
@@ -222,7 +222,6 @@ async function verifyWebhook(
|
||||
To connect with other auth strategies, use external wallets, or sponsor gas for users, check out the following guides:
|
||||
|
||||
- [Send a Payment](/payments/send)
|
||||
- [Find Routes](/payments/routes)
|
||||
- [Get Token Prices](/payments/tokens)
|
||||
|
||||
## Explore Full API References
|
||||
|
||||
@@ -44,3 +44,7 @@ The admin key is temporarily stored in dashboard when performing actions that re
|
||||
Please note, you should never share your admin key with third parties.
|
||||
</Details>
|
||||
|
||||
<Details summary="Is Vault open source?">
|
||||
Vault is not yet open source, but we plan to release it soon.
|
||||
</Details>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export default async function Layout(props: { children: React.ReactNode }) {
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description:
|
||||
"Vault is an open-source non-custodial key management service, secured with TEE architecture (AWS Nitro Enclaves) and designed for blockchain applications.",
|
||||
"Vault is a non-custodial key management service secured with TEE architecture (AWS Nitro Enclaves), designed for blockchain applications.",
|
||||
title: "Vault",
|
||||
image: {
|
||||
icon: "vault",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ArrowLeftRightIcon, UserLockIcon, UsersIcon, WalletIcon } from "lucide-
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "thirdweb Vault",
|
||||
description: "An open-source, secure key management service for storing, managing, and protecting cryptographic keys and secrets.",
|
||||
description: "A secure key management service for storing, managing, and protecting cryptographic keys and secrets.",
|
||||
image: {
|
||||
icon: "vault",
|
||||
title: "thirdweb Vault",
|
||||
@@ -14,7 +14,7 @@ export const metadata = createMetadata({
|
||||
|
||||
# Vault
|
||||
|
||||
Vault is an open-source non-custodial key management service, secured with TEE architecture (AWS Nitro Enclaves) and designed for blockchain applications.
|
||||
Vault is a non-custodial key management service secured with TEE architecture (AWS Nitro Enclaves), designed for blockchain applications.
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -63,10 +63,48 @@ export const config = createConfig({
|
||||
});
|
||||
```
|
||||
|
||||
Then in your application, you can use the connector to trigger the in-app wallet connection.
|
||||
### Using the ConnectButton or ConnectEmbed with a wagmi application
|
||||
|
||||
You can use the thirdweb react connection components and hooks like ConnectButton, ConnectEmbed, useConnectModal, etc, but still keep using wagmi for the rest of the application.
|
||||
|
||||
To do so, you will need to connect the `inAppWalletConnector` connector from the `@thirdweb-dev/wagmi-adapter` package with the connected thirdweb wallet.
|
||||
|
||||
You can do this at any time after connecting the thirdweb wallet, a convenient place to do this is in the `onConnect` callback of the ConnectButton or ConnectEmbed.
|
||||
|
||||
```tsx
|
||||
const { connectors, connect } = useConnect(); // from wagmi
|
||||
|
||||
<ConnectButton
|
||||
client={client}
|
||||
onConnect={(wallet) => {
|
||||
// connect the wagmi connector with the connected thirdweb wallet
|
||||
const twConnector = connectors.find(
|
||||
(c) => c.id === "in-app-wallet",
|
||||
);
|
||||
if (twConnector) {
|
||||
const options = {
|
||||
wallet, // pass the connected wallet
|
||||
} satisfies ConnectionOptions; // for type safety
|
||||
connect({
|
||||
connector: twConnector,
|
||||
chainId: chain.id,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
Make sure your app is wrapped in a `<ThirdwebProvider>` as well as a `<WagmiProvider>` since both contexts will be used here. From there, the wallet state will be in sync between the 2 libraries.
|
||||
|
||||
**Note:** the ConnectButton and ConnectEmbed handle reconnecting automatically on page reload. If not using those components (ie. useConnectModal or useConnect hooks directly), you will need to handle reconnecting by explicitely calling `useAutoConnect()`, and doing the same wagmi connection from the `onConnect` callback.
|
||||
|
||||
### Using the connector directly (headless mode)
|
||||
|
||||
You can also use the `inAppWalletConnector` connector directly in your application to connect to the thirdweb wallet without needing a `<ThirdwebProvider>` at all.
|
||||
|
||||
```ts
|
||||
const { connect, connectors } = useConnect();
|
||||
const { connect, connectors } = useConnect(); // from wagmi
|
||||
|
||||
const onClick = () => {
|
||||
// grab the connector
|
||||
@@ -79,9 +117,10 @@ const onClick = () => {
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
### Converting a wagmi wallet client to a thirdweb wallet
|
||||
|
||||
You can use the thirdweb SDK within a wagmi application by setting the wagmi connected account as the thirdweb 'active wallet'. After that, you can use all of the react components and hooks normally, including `BuyWidget`, `TransactionButton`, etc.
|
||||
You can also use the thirdweb SDK within a wagmi application by setting the wagmi connected account as the thirdweb 'active wallet'. After that, you can use all of the react components and hooks normally, including `BuyWidget`, `TransactionButton`, etc.
|
||||
|
||||
```ts
|
||||
// Assumes you've wrapped your application in a `<ThirdwebProvider>`
|
||||
|
||||
@@ -1,5 +1,117 @@
|
||||
# wagmi-inapp
|
||||
|
||||
## 0.0.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- thirdweb@5.111.6
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.181
|
||||
|
||||
## 0.0.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7da16cf`](https://github.com/thirdweb-dev/js/commit/7da16cff2ff3aaa80c7fe5614639d950d88b5bf2)]:
|
||||
- thirdweb@5.111.5
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.180
|
||||
|
||||
## 0.0.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`d907017`](https://github.com/thirdweb-dev/js/commit/d907017217a0927e7f69eab0470b1fe120b4d1a1), [`40781b8`](https://github.com/thirdweb-dev/js/commit/40781b8170a1014ef76e438ba3c8244137dba7ea)]:
|
||||
- thirdweb@5.111.4
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.179
|
||||
|
||||
## 0.0.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`207cb9d`](https://github.com/thirdweb-dev/js/commit/207cb9dbeb82411809bafa2a8e833b1f10414b92)]:
|
||||
- thirdweb@5.111.3
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.178
|
||||
|
||||
## 0.0.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1d54cff`](https://github.com/thirdweb-dev/js/commit/1d54cff7b442b8667ddd54bd783502c367ffc83e), [`fd04eef`](https://github.com/thirdweb-dev/js/commit/fd04eefea9a2a99eee3740d6746040c9f81cd43f)]:
|
||||
- thirdweb@5.111.2
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.177
|
||||
|
||||
## 0.0.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`ccdce49`](https://github.com/thirdweb-dev/js/commit/ccdce496e09bc9420b0e15854328651c01f36279)]:
|
||||
- thirdweb@5.111.1
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.176
|
||||
|
||||
## 0.0.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c83f1c6`](https://github.com/thirdweb-dev/js/commit/c83f1c6f7a38c8a9f424798e6e726e1d037ed042), [`219fc69`](https://github.com/thirdweb-dev/js/commit/219fc69d72b1ca861bf0228589039788008ea0c0)]:
|
||||
- thirdweb@5.111.0
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.175
|
||||
|
||||
## 0.0.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`6c318f8`](https://github.com/thirdweb-dev/js/commit/6c318f83d63aa8acce46da666e43638e410f799c), [`759b93b`](https://github.com/thirdweb-dev/js/commit/759b93beff7ab10a3a2fa3b9dd25ca628efb9267), [`8e357b3`](https://github.com/thirdweb-dev/js/commit/8e357b3cb39dcc7a8595a83224098a9c44585389)]:
|
||||
- thirdweb@5.110.6
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.174
|
||||
|
||||
## 0.0.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c08fb13`](https://github.com/thirdweb-dev/js/commit/c08fb1356e77be9eeb0569905fcb2ab76a516dc3), [`bcbb3f5`](https://github.com/thirdweb-dev/js/commit/bcbb3f540a8eab2b02f9261b779bd2aad3464598), [`e0c8312`](https://github.com/thirdweb-dev/js/commit/e0c83121c201eb2dbdfa6a6b1bbb637f5ad2a21d)]:
|
||||
- thirdweb@5.110.5
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.173
|
||||
|
||||
## 0.0.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`23cb3bd`](https://github.com/thirdweb-dev/js/commit/23cb3bd98c5390e64f45da5a222042cdc76bd7f4), [`abf0bba`](https://github.com/thirdweb-dev/js/commit/abf0bbaf359c8de5b0ed0d481e3791c4b7cbf92b), [`e0abf2b`](https://github.com/thirdweb-dev/js/commit/e0abf2b2738661fda3c69551d78231cfac4b0767)]:
|
||||
- thirdweb@5.110.4
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.172
|
||||
|
||||
## 0.0.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`bf46520`](https://github.com/thirdweb-dev/js/commit/bf46520a6b183c4beb873de5e63853cd3a23e685)]:
|
||||
- thirdweb@5.110.3
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.171
|
||||
|
||||
## 0.0.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`3e9eb49`](https://github.com/thirdweb-dev/js/commit/3e9eb49787e00c09c3286aeb4ad73826a2856e65), [`bfd3be7`](https://github.com/thirdweb-dev/js/commit/bfd3be764c461c420f1f8520fe4d5d2cc83e42f7), [`ae3a122`](https://github.com/thirdweb-dev/js/commit/ae3a122cf1628f0fc997dd240d4b23842ac30f66), [`d5d5939`](https://github.com/thirdweb-dev/js/commit/d5d593956e12b2a15393ed32d1d742bc43575bdd), [`f757e60`](https://github.com/thirdweb-dev/js/commit/f757e6084648b6b6e5879abeab30954753a1020d)]:
|
||||
- thirdweb@5.110.2
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.170
|
||||
|
||||
## 0.0.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`85fe949`](https://github.com/thirdweb-dev/js/commit/85fe9491cd6d2270847e9941ddb7df76aefd8b31), [`e978809`](https://github.com/thirdweb-dev/js/commit/e9788096d665afef7c2a933cea5d6f522fc67273), [`899051f`](https://github.com/thirdweb-dev/js/commit/899051f788b0c2521a022846782892a8c1405a66)]:
|
||||
- thirdweb@5.110.1
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.169
|
||||
|
||||
## 0.0.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`ceba683`](https://github.com/thirdweb-dev/js/commit/ceba6835dd60896efc34fe1495a1812c0cc39db7), [`b30369f`](https://github.com/thirdweb-dev/js/commit/b30369f3cc0bbceed3470b6c905551a5c930f08f)]:
|
||||
- thirdweb@5.110.0
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.168
|
||||
|
||||
## 0.0.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wagmi-inapp",
|
||||
"private": true,
|
||||
"version": "0.0.12",
|
||||
"version": "0.0.26",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -2,11 +2,13 @@ import type { ConnectionOptions } from "@thirdweb-dev/wagmi-adapter";
|
||||
import { ConnectButton } from "thirdweb/react";
|
||||
import {
|
||||
useAccount,
|
||||
useCallsStatus,
|
||||
useConnect,
|
||||
useDisconnect,
|
||||
useSendCalls,
|
||||
useSendTransaction,
|
||||
} from "wagmi";
|
||||
import { chain, client } from "./wagmi.js";
|
||||
import { chain, client, thirdwebChainForWallet, wallet } from "./wagmi.js";
|
||||
|
||||
function App() {
|
||||
const account = useAccount();
|
||||
@@ -20,6 +22,17 @@ function App() {
|
||||
error: sendTxError,
|
||||
data: sendTxData,
|
||||
} = useSendTransaction();
|
||||
const { sendCalls, data, isPending: isPendingSendCalls } = useSendCalls();
|
||||
const {
|
||||
data: callStatus,
|
||||
isLoading: isLoadingCallStatus,
|
||||
error: callStatusError,
|
||||
} = useCallsStatus({
|
||||
id: data?.id || "",
|
||||
query: {
|
||||
enabled: !!data?.id,
|
||||
},
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
@@ -44,6 +57,8 @@ function App() {
|
||||
<h2>Connect</h2>
|
||||
<ConnectButton
|
||||
client={client}
|
||||
chain={thirdwebChainForWallet}
|
||||
wallets={[wallet]}
|
||||
onConnect={(wallet) => {
|
||||
// auto connect to wagmi on tw connect
|
||||
const twConnector = connectors.find(
|
||||
@@ -95,7 +110,26 @@ function App() {
|
||||
>
|
||||
Send Tx
|
||||
</button>
|
||||
<div>{isPending ? "Sending..." : ""}</div>
|
||||
<button
|
||||
onClick={() =>
|
||||
sendCalls({
|
||||
calls: [
|
||||
{
|
||||
to: "0x70997970c51812dc3a010c7d01b50e0d17dc79c8",
|
||||
value: 0n,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
Send Calls
|
||||
</button>
|
||||
<div>
|
||||
{isPending || isPendingSendCalls || isLoadingCallStatus
|
||||
? "Sending..."
|
||||
: ""}
|
||||
</div>
|
||||
<div>
|
||||
{isSuccess
|
||||
? `Success: ${sendTxData}`
|
||||
@@ -103,6 +137,13 @@ function App() {
|
||||
? sendTxError?.message
|
||||
: ""}
|
||||
</div>
|
||||
<div>
|
||||
{callStatus
|
||||
? `Success: ${JSON.stringify(callStatus, null, 2)}`
|
||||
: callStatusError
|
||||
? callStatusError?.message
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { inAppWalletConnector } from "@thirdweb-dev/wagmi-adapter";
|
||||
import { createThirdwebClient, defineChain as thirdwebChain } from "thirdweb";
|
||||
import { inAppWallet } from "thirdweb/wallets/in-app";
|
||||
import { createConfig, http } from "wagmi";
|
||||
import { baseSepolia } from "wagmi/chains";
|
||||
|
||||
@@ -15,6 +16,13 @@ export const client = createThirdwebClient({
|
||||
clientId,
|
||||
});
|
||||
|
||||
export const wallet = inAppWallet({
|
||||
executionMode: {
|
||||
mode: "EIP7702",
|
||||
sponsorGas: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const chain = baseSepolia;
|
||||
export const thirdwebChainForWallet = thirdwebChain(baseSepolia.id);
|
||||
|
||||
@@ -23,8 +31,8 @@ export const config = createConfig({
|
||||
connectors: [
|
||||
inAppWalletConnector({
|
||||
client,
|
||||
smartAccount: {
|
||||
chain: thirdwebChain(chain.id),
|
||||
executionMode: {
|
||||
mode: "EIP7702",
|
||||
sponsorGas: true,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @thirdweb-dev/api
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8338](https://github.com/thirdweb-dev/js/pull/8338) [`90b7791`](https://github.com/thirdweb-dev/js/commit/90b779107d3cb726adeca6a3f885129c12599c6a) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - added solana token balances endpoint
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8290](https://github.com/thirdweb-dev/js/pull/8290) [`f2121e9`](https://github.com/thirdweb-dev/js/commit/f2121e9519719ef809986c919acc5069b5b7ce74) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - added support for userId across wallet apis
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/api",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
|
||||
@@ -66,6 +66,9 @@ import type {
|
||||
GetSolanaTransactionData,
|
||||
GetSolanaTransactionErrors,
|
||||
GetSolanaTransactionResponses,
|
||||
GetSolanaWalletBalanceData,
|
||||
GetSolanaWalletBalanceErrors,
|
||||
GetSolanaWalletBalanceResponses,
|
||||
GetTokenOwnersData,
|
||||
GetTokenOwnersErrors,
|
||||
GetTokenOwnersResponses,
|
||||
@@ -244,6 +247,7 @@ export const initiateAuthentication = <ThrowOnError extends boolean = false>(
|
||||
* - `isNewUser` - Whether this is a new wallet creation
|
||||
* - `token` - JWT token for authenticated API requests
|
||||
* - `type` - The authentication method used
|
||||
* - `userId` - Unique identifier for the authenticated user
|
||||
* - `walletAddress` - Your new or existing wallet address
|
||||
*
|
||||
* **Next step - Verify your token:**
|
||||
@@ -442,6 +446,7 @@ export const socialAuthentication = <ThrowOnError extends boolean = false>(
|
||||
* Retrieve the authenticated user's wallet information including wallet addresses and linked authentication wallets. This endpoint provides comprehensive user data for the currently authenticated session.
|
||||
*
|
||||
* **Returns:**
|
||||
* - userId - Unique identifier for this wallet in thirdweb auth
|
||||
* - Primary wallet address
|
||||
* - Smart wallet address (if available)
|
||||
* - Wallet creation timestamp
|
||||
@@ -1588,6 +1593,31 @@ export const createSolanaWallet = <ThrowOnError extends boolean = false>(
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Solana Wallet Balance
|
||||
* Get the SOL or SPL token balance for a Solana wallet on a specific Solana network.
|
||||
*
|
||||
* **Authentication**: Pass `x-client-id` for frontend usage from allowlisted origins or `x-secret-key` for backend usage.
|
||||
*/
|
||||
export const getSolanaWalletBalance = <ThrowOnError extends boolean = false>(
|
||||
options: Options<GetSolanaWalletBalanceData, ThrowOnError>,
|
||||
) => {
|
||||
return (options.client ?? _heyApiClient).get<
|
||||
GetSolanaWalletBalanceResponses,
|
||||
GetSolanaWalletBalanceErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-client-id",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/solana/wallets/{address}/balance",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sign Solana Message
|
||||
* Sign an arbitrary message with a Solana wallet. Supports both text and hexadecimal message formats with automatic format detection.
|
||||
|
||||
@@ -330,6 +330,10 @@ export type CompleteAuthenticationResponses = {
|
||||
* Type of authentication completed
|
||||
*/
|
||||
type: string;
|
||||
/**
|
||||
* Unique identifier for the authenticated user
|
||||
*/
|
||||
userId: string;
|
||||
/**
|
||||
* The wallet address
|
||||
*/
|
||||
@@ -875,6 +879,10 @@ export type GetMyWalletResponses = {
|
||||
*/
|
||||
200: {
|
||||
result: {
|
||||
/**
|
||||
* Unique identifier for the user wallet within the thirdweb auth system.
|
||||
*/
|
||||
userId?: string;
|
||||
/**
|
||||
* The EOA (Externally Owned Wallet) address of the wallet. This is the traditional wallet address.
|
||||
*/
|
||||
@@ -1076,6 +1084,10 @@ export type ListUserWalletsData = {
|
||||
address?: string;
|
||||
externalWalletAddress?: string;
|
||||
id?: string;
|
||||
/**
|
||||
* Filter results by the unique user identifier returned by auth flows.
|
||||
*/
|
||||
userId?: string;
|
||||
};
|
||||
url: "/v1/wallets/user";
|
||||
};
|
||||
@@ -1122,6 +1134,10 @@ export type ListUserWalletsResponses = {
|
||||
* Array of user wallets
|
||||
*/
|
||||
wallets: Array<{
|
||||
/**
|
||||
* Unique identifier for the user wallet within the thirdweb auth system.
|
||||
*/
|
||||
userId?: string;
|
||||
/**
|
||||
* The EOA (Externally Owned Wallet) address of the wallet. This is the traditional wallet address.
|
||||
*/
|
||||
@@ -1362,6 +1378,10 @@ export type CreateUserWalletResponses = {
|
||||
*/
|
||||
200: {
|
||||
result: {
|
||||
/**
|
||||
* Unique identifier for the user wallet within the thirdweb auth system.
|
||||
*/
|
||||
userId?: string;
|
||||
/**
|
||||
* The EOA (Externally Owned Wallet) address of the wallet. This is the traditional wallet address.
|
||||
*/
|
||||
@@ -1604,6 +1624,10 @@ export type ListServerWalletsResponses = {
|
||||
* Array of server wallets
|
||||
*/
|
||||
wallets: Array<{
|
||||
/**
|
||||
* Unique identifier for the user wallet within the thirdweb auth system.
|
||||
*/
|
||||
userId?: string;
|
||||
/**
|
||||
* The EOA (Externally Owned Wallet) address of the wallet. This is the traditional wallet address.
|
||||
*/
|
||||
@@ -1831,6 +1855,10 @@ export type CreateServerWalletResponses = {
|
||||
*/
|
||||
200: {
|
||||
result: {
|
||||
/**
|
||||
* Unique identifier for the user wallet within the thirdweb auth system.
|
||||
*/
|
||||
userId?: string;
|
||||
/**
|
||||
* The EOA (Externally Owned Wallet) address of the wallet. This is the traditional wallet address.
|
||||
*/
|
||||
@@ -2107,6 +2135,10 @@ export type GetWalletTransactionsData = {
|
||||
address: string;
|
||||
};
|
||||
query: {
|
||||
/**
|
||||
* Chain ID(s) to request transaction data for. You can specify multiple chain IDs by repeating the parameter, up to a maximum of 50. Example: ?chainId=1&chainId=137
|
||||
*/
|
||||
chainId: Array<number>;
|
||||
/**
|
||||
* Filter by block timestamp (Unix timestamp) greater than or equal to this value
|
||||
*/
|
||||
@@ -2143,10 +2175,6 @@ export type GetWalletTransactionsData = {
|
||||
* Sort order: 'asc' for ascending, 'desc' for descending
|
||||
*/
|
||||
sortOrder?: "asc" | "desc";
|
||||
/**
|
||||
* Chain ID(s) to request transaction data for. You can specify multiple chain IDs by repeating the parameter, up to a maximum of 50. Example: ?chainId=1&chainId=137
|
||||
*/
|
||||
chainId: Array<number>;
|
||||
};
|
||||
url: "/v1/wallets/{address}/transactions";
|
||||
};
|
||||
@@ -3206,11 +3234,11 @@ export type WriteContractErrors = {
|
||||
/**
|
||||
* Payment ID
|
||||
*/
|
||||
id: string;
|
||||
id?: string;
|
||||
/**
|
||||
* Bridge quote for completing the payment
|
||||
*/
|
||||
quote: {
|
||||
quote?: {
|
||||
/**
|
||||
* Block number when quote was generated
|
||||
*/
|
||||
@@ -4286,11 +4314,11 @@ export type SendTransactionsErrors = {
|
||||
/**
|
||||
* Payment ID
|
||||
*/
|
||||
id: string;
|
||||
id?: string;
|
||||
/**
|
||||
* Bridge quote for completing the payment
|
||||
*/
|
||||
quote: {
|
||||
quote?: {
|
||||
/**
|
||||
* Block number when quote was generated
|
||||
*/
|
||||
@@ -4697,11 +4725,11 @@ export type PaymentsPurchaseErrors = {
|
||||
/**
|
||||
* Payment ID
|
||||
*/
|
||||
id: string;
|
||||
id?: string;
|
||||
/**
|
||||
* Bridge quote for completing the payment
|
||||
*/
|
||||
quote: {
|
||||
quote?: {
|
||||
/**
|
||||
* Block number when quote was generated
|
||||
*/
|
||||
@@ -4886,17 +4914,7 @@ export type VerifyX402PaymentData = {
|
||||
paymentPayload: {
|
||||
x402Version: number;
|
||||
scheme: "exact";
|
||||
network:
|
||||
| "base-sepolia"
|
||||
| "base"
|
||||
| "avalanche-fuji"
|
||||
| "avalanche"
|
||||
| "iotex"
|
||||
| "solana-devnet"
|
||||
| "solana"
|
||||
| "sei"
|
||||
| "sei-testnet"
|
||||
| string;
|
||||
network: string;
|
||||
payload:
|
||||
| {
|
||||
signature: string;
|
||||
@@ -4915,17 +4933,7 @@ export type VerifyX402PaymentData = {
|
||||
};
|
||||
paymentRequirements: {
|
||||
scheme: "exact";
|
||||
network:
|
||||
| "base-sepolia"
|
||||
| "base"
|
||||
| "avalanche-fuji"
|
||||
| "avalanche"
|
||||
| "iotex"
|
||||
| "solana-devnet"
|
||||
| "solana"
|
||||
| "sei"
|
||||
| "sei-testnet"
|
||||
| string;
|
||||
network: string;
|
||||
maxAmountRequired: string;
|
||||
resource: string;
|
||||
description: string;
|
||||
@@ -4987,7 +4995,6 @@ export type VerifyX402PaymentResponses = {
|
||||
| "invalid_exact_svm_payload_transaction_instruction_not_spl_token_transfer_checked"
|
||||
| "invalid_exact_svm_payload_transaction_instruction_not_token_2022_transfer_checked"
|
||||
| "invalid_exact_svm_payload_transaction_not_a_transfer_instruction"
|
||||
| "invalid_exact_svm_payload_transaction_cannot_derive_receiver_ata"
|
||||
| "invalid_exact_svm_payload_transaction_receiver_ata_not_found"
|
||||
| "invalid_exact_svm_payload_transaction_sender_ata_not_found"
|
||||
| "invalid_exact_svm_payload_transaction_simulation_failed"
|
||||
@@ -5021,17 +5028,7 @@ export type SettleX402PaymentData = {
|
||||
paymentPayload: {
|
||||
x402Version: number;
|
||||
scheme: "exact";
|
||||
network:
|
||||
| "base-sepolia"
|
||||
| "base"
|
||||
| "avalanche-fuji"
|
||||
| "avalanche"
|
||||
| "iotex"
|
||||
| "solana-devnet"
|
||||
| "solana"
|
||||
| "sei"
|
||||
| "sei-testnet"
|
||||
| string;
|
||||
network: string;
|
||||
payload:
|
||||
| {
|
||||
signature: string;
|
||||
@@ -5050,17 +5047,7 @@ export type SettleX402PaymentData = {
|
||||
};
|
||||
paymentRequirements: {
|
||||
scheme: "exact";
|
||||
network:
|
||||
| "base-sepolia"
|
||||
| "base"
|
||||
| "avalanche-fuji"
|
||||
| "avalanche"
|
||||
| "iotex"
|
||||
| "solana-devnet"
|
||||
| "solana"
|
||||
| "sei"
|
||||
| "sei-testnet"
|
||||
| string;
|
||||
network: string;
|
||||
maxAmountRequired: string;
|
||||
resource: string;
|
||||
description: string;
|
||||
@@ -5126,7 +5113,6 @@ export type SettleX402PaymentResponses = {
|
||||
| "invalid_exact_svm_payload_transaction_instruction_not_spl_token_transfer_checked"
|
||||
| "invalid_exact_svm_payload_transaction_instruction_not_token_2022_transfer_checked"
|
||||
| "invalid_exact_svm_payload_transaction_not_a_transfer_instruction"
|
||||
| "invalid_exact_svm_payload_transaction_cannot_derive_receiver_ata"
|
||||
| "invalid_exact_svm_payload_transaction_receiver_ata_not_found"
|
||||
| "invalid_exact_svm_payload_transaction_sender_ata_not_found"
|
||||
| "invalid_exact_svm_payload_transaction_simulation_failed"
|
||||
@@ -5155,7 +5141,10 @@ export type SettleX402PaymentResponses = {
|
||||
| "solana-devnet"
|
||||
| "solana"
|
||||
| "sei"
|
||||
| "sei-testnet";
|
||||
| "sei-testnet"
|
||||
| "polygon"
|
||||
| "polygon-amoy"
|
||||
| "peaq";
|
||||
errorMessage?: string;
|
||||
};
|
||||
};
|
||||
@@ -5198,17 +5187,7 @@ export type SupportedX402PaymentsResponses = {
|
||||
kinds: Array<{
|
||||
x402Version: 1;
|
||||
scheme: "exact";
|
||||
network:
|
||||
| "base-sepolia"
|
||||
| "base"
|
||||
| "avalanche-fuji"
|
||||
| "avalanche"
|
||||
| "iotex"
|
||||
| "solana-devnet"
|
||||
| "solana"
|
||||
| "sei"
|
||||
| "sei-testnet"
|
||||
| string;
|
||||
network: string;
|
||||
extra?: {
|
||||
defaultAsset?: {
|
||||
/**
|
||||
@@ -5312,11 +5291,11 @@ export type FetchWithPaymentErrors = {
|
||||
/**
|
||||
* Payment ID
|
||||
*/
|
||||
id: string;
|
||||
id?: string;
|
||||
/**
|
||||
* Bridge quote for completing the payment
|
||||
*/
|
||||
quote: {
|
||||
quote?: {
|
||||
/**
|
||||
* Block number when quote was generated
|
||||
*/
|
||||
@@ -5524,17 +5503,7 @@ export type ListPayableServicesResponses = {
|
||||
x402Version: number;
|
||||
accepts: Array<{
|
||||
scheme: "exact";
|
||||
network:
|
||||
| "base-sepolia"
|
||||
| "base"
|
||||
| "avalanche-fuji"
|
||||
| "avalanche"
|
||||
| "iotex"
|
||||
| "solana-devnet"
|
||||
| "solana"
|
||||
| "sei"
|
||||
| "sei-testnet"
|
||||
| string;
|
||||
network: string;
|
||||
maxAmountRequired: string;
|
||||
resource: string;
|
||||
description: string;
|
||||
@@ -6270,11 +6239,11 @@ export type BridgeSwapErrors = {
|
||||
/**
|
||||
* Payment ID
|
||||
*/
|
||||
id: string;
|
||||
id?: string;
|
||||
/**
|
||||
* Bridge quote for completing the payment
|
||||
*/
|
||||
quote: {
|
||||
quote?: {
|
||||
/**
|
||||
* Block number when quote was generated
|
||||
*/
|
||||
@@ -6612,6 +6581,74 @@ export type CreateSolanaWalletResponses = {
|
||||
export type CreateSolanaWalletResponse =
|
||||
CreateSolanaWalletResponses[keyof CreateSolanaWalletResponses];
|
||||
|
||||
export type GetSolanaWalletBalanceData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Public key of the Solana wallet.
|
||||
*/
|
||||
address: string;
|
||||
};
|
||||
query: {
|
||||
/**
|
||||
* Solana network to query. Choose either solana:mainnet or solana:devnet.
|
||||
*/
|
||||
chainId: "solana:mainnet" | "solana:devnet";
|
||||
/**
|
||||
* SPL token mint address. Omit to retrieve native SOL balance.
|
||||
*/
|
||||
tokenAddress?: string;
|
||||
};
|
||||
url: "/v1/solana/wallets/{address}/balance";
|
||||
};
|
||||
|
||||
export type GetSolanaWalletBalanceErrors = {
|
||||
/**
|
||||
* Invalid request parameters. This occurs when the wallet address or token mint is invalid, or the request mixes a token mint with multiple networks.
|
||||
*/
|
||||
400: unknown;
|
||||
/**
|
||||
* Authentication required. Include `x-client-id` or `x-secret-key` headers.
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Internal server error. This may occur due to RPC connectivity issues or unexpected failures.
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type GetSolanaWalletBalanceResponses = {
|
||||
/**
|
||||
* Wallet balance retrieved successfully for the requested Solana network.
|
||||
*/
|
||||
200: {
|
||||
/**
|
||||
* Balance data for the requested Solana network.
|
||||
*/
|
||||
result: {
|
||||
/**
|
||||
* Requested Solana network.
|
||||
*/
|
||||
chainId: "solana:mainnet" | "solana:devnet";
|
||||
/**
|
||||
* Number of decimals used by the token.
|
||||
*/
|
||||
decimals: number;
|
||||
/**
|
||||
* Human-readable balance formatted using token decimals.
|
||||
*/
|
||||
displayValue: string;
|
||||
/**
|
||||
* Raw balance value expressed in base units (lamports).
|
||||
*/
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type GetSolanaWalletBalanceResponse =
|
||||
GetSolanaWalletBalanceResponses[keyof GetSolanaWalletBalanceResponses];
|
||||
|
||||
export type SignSolanaMessageData = {
|
||||
/**
|
||||
* Request payload for signing an arbitrary Solana message.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @thirdweb-dev/engine
|
||||
|
||||
## 3.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#8371](https://github.com/thirdweb-dev/js/pull/8371) [`ad68969`](https://github.com/thirdweb-dev/js/commit/ad68969559a80a5f188a647ef0e1c04f0003a0ad) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - Reworked Solana Sign Transaction API that accepts more flexible inputs
|
||||
|
||||
## 3.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/engine",
|
||||
"version": "3.3.0",
|
||||
"version": "3.4.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
|
||||
@@ -274,6 +274,33 @@ export const sendSolanaTransaction = <ThrowOnError extends boolean = false>(
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sign Solana Transaction
|
||||
* Sign a Solana transaction without broadcasting it
|
||||
*/
|
||||
export const signSolanaTransaction = <ThrowOnError extends boolean = false>(
|
||||
options: Options<SignSolanaTransactionData, ThrowOnError>,
|
||||
) => {
|
||||
return (options.client ?? _heyApiClient).post<
|
||||
SignSolanaTransactionResponses,
|
||||
unknown,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-secret-key",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/solana/sign/transaction",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel Transaction
|
||||
* Attempt to cancel a queued transaction. Transactions that have been sent and are waiting for mine cannot be cancelled.
|
||||
@@ -424,33 +451,6 @@ export const signSolanaMessage = <ThrowOnError extends boolean = false>(
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sign Solana Transaction
|
||||
* Sign a serialized Solana transaction.
|
||||
*/
|
||||
export const signSolanaTransaction = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<SignSolanaTransactionData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).post<
|
||||
SignSolanaTransactionResponses,
|
||||
unknown,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-secret-key",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/solana/sign-transaction",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Transactions
|
||||
* Search transactions with various filters and pagination
|
||||
|
||||
@@ -556,6 +556,10 @@ export type Eip7702OwnerExecution = {
|
||||
* The delegated EOA address
|
||||
*/
|
||||
from: AddressDef;
|
||||
/**
|
||||
* Optional fleet ID to route the transaction to a specific bundler fleet
|
||||
*/
|
||||
fleetId?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -570,6 +574,10 @@ export type Eip7702SessionKeyExecution = {
|
||||
* 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;
|
||||
/**
|
||||
* Optional fleet ID to route the transaction to a specific bundler fleet
|
||||
*/
|
||||
fleetId?: string | null;
|
||||
};
|
||||
|
||||
export type EmptyIdempotencySetResponse = {
|
||||
@@ -1000,15 +1008,11 @@ export type RpcErrorResponse = {
|
||||
/**
|
||||
* Request to send a Solana transaction
|
||||
*/
|
||||
export type SendSolanaTransactionRequest = {
|
||||
export type SendSolanaTransactionRequest = SolanaTransactionInput & {
|
||||
/**
|
||||
* Idempotency key for this transaction (defaults to random UUID)
|
||||
*/
|
||||
idempotencyKey?: string;
|
||||
/**
|
||||
* List of Solana instructions to execute
|
||||
*/
|
||||
instructions: Array<SolanaInstructionData>;
|
||||
/**
|
||||
* Solana execution options
|
||||
*/
|
||||
@@ -1188,6 +1192,30 @@ export type SignResultData = {
|
||||
signedData: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Request to sign a Solana transaction
|
||||
*/
|
||||
export type SignSolanaTransactionRequest = SolanaTransactionInput & {
|
||||
/**
|
||||
* Solana execution options
|
||||
*/
|
||||
executionOptions: SolanaExecutionOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data returned from successful signing
|
||||
*/
|
||||
export type SignSolanaTransactionResponse = {
|
||||
/**
|
||||
* The signature (base58-encoded)
|
||||
*/
|
||||
signature: string;
|
||||
/**
|
||||
* The signed serialized transaction (base64-encoded)
|
||||
*/
|
||||
signedTransaction: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Request to sign typed data
|
||||
*/
|
||||
@@ -1452,6 +1480,21 @@ export type SolanaRpcResponseErrorData =
|
||||
type: "NODE_UNHEALTHY";
|
||||
};
|
||||
|
||||
/**
|
||||
* Input for Solana transaction - either build from instructions or use pre-built
|
||||
*/
|
||||
export type SolanaTransactionInput =
|
||||
| SolanaTransactionInputInstructions
|
||||
| SolanaTransactionInputSerialized;
|
||||
|
||||
export type SolanaTransactionInputInstructions = {
|
||||
instructions: Array<SolanaInstructionData>;
|
||||
};
|
||||
|
||||
export type SolanaTransactionInputSerialized = {
|
||||
transaction: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Execution Option Variants
|
||||
* All supported specific execution options are contained here
|
||||
@@ -1503,6 +1546,22 @@ export type SuccessResponseQueuedTransactionsResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
export type SuccessResponseSignSolanaTransactionResponse = {
|
||||
/**
|
||||
* Data returned from successful signing
|
||||
*/
|
||||
result: {
|
||||
/**
|
||||
* The signature (base58-encoded)
|
||||
*/
|
||||
signature: string;
|
||||
/**
|
||||
* The signed serialized transaction (base64-encoded)
|
||||
*/
|
||||
signedTransaction: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ThirdwebError =
|
||||
| {
|
||||
error: ThirdwebSerializationError;
|
||||
@@ -1847,6 +1906,32 @@ export type SendSolanaTransactionResponses = {
|
||||
export type SendSolanaTransactionResponse =
|
||||
SendSolanaTransactionResponses[keyof SendSolanaTransactionResponses];
|
||||
|
||||
export type SignSolanaTransactionData = {
|
||||
/**
|
||||
* Sign Solana transaction request
|
||||
*/
|
||||
body: SignSolanaTransactionRequest;
|
||||
headers?: {
|
||||
/**
|
||||
* Vault access token
|
||||
*/
|
||||
"x-vault-access-token"?: string | null;
|
||||
};
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: "/v1/solana/sign/transaction";
|
||||
};
|
||||
|
||||
export type SignSolanaTransactionResponses = {
|
||||
/**
|
||||
* Successfully signed Solana transaction
|
||||
*/
|
||||
200: SuccessResponseSignSolanaTransactionResponse;
|
||||
};
|
||||
|
||||
export type SignSolanaTransactionResponse2 =
|
||||
SignSolanaTransactionResponses[keyof SignSolanaTransactionResponses];
|
||||
|
||||
export type CancelTransactionData = {
|
||||
body?: never;
|
||||
path: {
|
||||
@@ -2076,49 +2161,6 @@ export type SignSolanaMessageResponses = {
|
||||
export type SignSolanaMessageResponse =
|
||||
SignSolanaMessageResponses[keyof SignSolanaMessageResponses];
|
||||
|
||||
export type SignSolanaTransactionData = {
|
||||
body?: {
|
||||
/**
|
||||
* Base58 encoded Solana public key
|
||||
*/
|
||||
from: string;
|
||||
/**
|
||||
* Base64 encoded Solana transaction
|
||||
*/
|
||||
transaction: string;
|
||||
};
|
||||
headers?: {
|
||||
/**
|
||||
* Vault Access Token used to access your EOA
|
||||
*/
|
||||
"x-vault-access-token"?: string;
|
||||
};
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: "/v1/solana/sign-transaction";
|
||||
};
|
||||
|
||||
export type SignSolanaTransactionResponses = {
|
||||
/**
|
||||
* Transaction signed
|
||||
*/
|
||||
200: {
|
||||
result: {
|
||||
/**
|
||||
* Base58 encoded signature
|
||||
*/
|
||||
signature: string;
|
||||
/**
|
||||
* Base58 encoded Solana public key
|
||||
*/
|
||||
signerPublicKey: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type SignSolanaTransactionResponse =
|
||||
SignSolanaTransactionResponses[keyof SignSolanaTransactionResponses];
|
||||
|
||||
export type GetTransactionsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
|
||||
@@ -1,5 +1,103 @@
|
||||
# @thirdweb-dev/nebula
|
||||
|
||||
## 0.2.84
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- thirdweb@5.111.6
|
||||
|
||||
## 0.2.83
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7da16cf`](https://github.com/thirdweb-dev/js/commit/7da16cff2ff3aaa80c7fe5614639d950d88b5bf2)]:
|
||||
- thirdweb@5.111.5
|
||||
|
||||
## 0.2.82
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`d907017`](https://github.com/thirdweb-dev/js/commit/d907017217a0927e7f69eab0470b1fe120b4d1a1), [`40781b8`](https://github.com/thirdweb-dev/js/commit/40781b8170a1014ef76e438ba3c8244137dba7ea)]:
|
||||
- thirdweb@5.111.4
|
||||
|
||||
## 0.2.81
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`207cb9d`](https://github.com/thirdweb-dev/js/commit/207cb9dbeb82411809bafa2a8e833b1f10414b92)]:
|
||||
- thirdweb@5.111.3
|
||||
|
||||
## 0.2.80
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1d54cff`](https://github.com/thirdweb-dev/js/commit/1d54cff7b442b8667ddd54bd783502c367ffc83e), [`fd04eef`](https://github.com/thirdweb-dev/js/commit/fd04eefea9a2a99eee3740d6746040c9f81cd43f)]:
|
||||
- thirdweb@5.111.2
|
||||
|
||||
## 0.2.79
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`ccdce49`](https://github.com/thirdweb-dev/js/commit/ccdce496e09bc9420b0e15854328651c01f36279)]:
|
||||
- thirdweb@5.111.1
|
||||
|
||||
## 0.2.78
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c83f1c6`](https://github.com/thirdweb-dev/js/commit/c83f1c6f7a38c8a9f424798e6e726e1d037ed042), [`219fc69`](https://github.com/thirdweb-dev/js/commit/219fc69d72b1ca861bf0228589039788008ea0c0)]:
|
||||
- thirdweb@5.111.0
|
||||
|
||||
## 0.2.77
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`6c318f8`](https://github.com/thirdweb-dev/js/commit/6c318f83d63aa8acce46da666e43638e410f799c), [`759b93b`](https://github.com/thirdweb-dev/js/commit/759b93beff7ab10a3a2fa3b9dd25ca628efb9267), [`8e357b3`](https://github.com/thirdweb-dev/js/commit/8e357b3cb39dcc7a8595a83224098a9c44585389)]:
|
||||
- thirdweb@5.110.6
|
||||
|
||||
## 0.2.76
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c08fb13`](https://github.com/thirdweb-dev/js/commit/c08fb1356e77be9eeb0569905fcb2ab76a516dc3), [`bcbb3f5`](https://github.com/thirdweb-dev/js/commit/bcbb3f540a8eab2b02f9261b779bd2aad3464598), [`e0c8312`](https://github.com/thirdweb-dev/js/commit/e0c83121c201eb2dbdfa6a6b1bbb637f5ad2a21d)]:
|
||||
- thirdweb@5.110.5
|
||||
|
||||
## 0.2.75
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`23cb3bd`](https://github.com/thirdweb-dev/js/commit/23cb3bd98c5390e64f45da5a222042cdc76bd7f4), [`abf0bba`](https://github.com/thirdweb-dev/js/commit/abf0bbaf359c8de5b0ed0d481e3791c4b7cbf92b), [`e0abf2b`](https://github.com/thirdweb-dev/js/commit/e0abf2b2738661fda3c69551d78231cfac4b0767)]:
|
||||
- thirdweb@5.110.4
|
||||
|
||||
## 0.2.74
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`bf46520`](https://github.com/thirdweb-dev/js/commit/bf46520a6b183c4beb873de5e63853cd3a23e685)]:
|
||||
- thirdweb@5.110.3
|
||||
|
||||
## 0.2.73
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`3e9eb49`](https://github.com/thirdweb-dev/js/commit/3e9eb49787e00c09c3286aeb4ad73826a2856e65), [`bfd3be7`](https://github.com/thirdweb-dev/js/commit/bfd3be764c461c420f1f8520fe4d5d2cc83e42f7), [`ae3a122`](https://github.com/thirdweb-dev/js/commit/ae3a122cf1628f0fc997dd240d4b23842ac30f66), [`d5d5939`](https://github.com/thirdweb-dev/js/commit/d5d593956e12b2a15393ed32d1d742bc43575bdd), [`f757e60`](https://github.com/thirdweb-dev/js/commit/f757e6084648b6b6e5879abeab30954753a1020d)]:
|
||||
- thirdweb@5.110.2
|
||||
|
||||
## 0.2.72
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`85fe949`](https://github.com/thirdweb-dev/js/commit/85fe9491cd6d2270847e9941ddb7df76aefd8b31), [`e978809`](https://github.com/thirdweb-dev/js/commit/e9788096d665afef7c2a933cea5d6f522fc67273), [`899051f`](https://github.com/thirdweb-dev/js/commit/899051f788b0c2521a022846782892a8c1405a66)]:
|
||||
- thirdweb@5.110.1
|
||||
|
||||
## 0.2.71
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`ceba683`](https://github.com/thirdweb-dev/js/commit/ceba6835dd60896efc34fe1495a1812c0cc39db7), [`b30369f`](https://github.com/thirdweb-dev/js/commit/b30369f3cc0bbceed3470b6c905551a5c930f08f)]:
|
||||
- thirdweb@5.110.0
|
||||
|
||||
## 0.2.70
|
||||
|
||||
### 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.70"
|
||||
"version": "0.2.84"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"import": "*",
|
||||
"limit": "1 kB",
|
||||
"name": "@thirdweb-dev/nexus (esm)",
|
||||
"path": "./dist/esm/exports/nexus.js"
|
||||
},
|
||||
{
|
||||
"limit": "1 kB",
|
||||
"name": "@thirdweb-dev/nexus (cjs)",
|
||||
"path": "./dist/cjs/exports/nexus.js"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
||||
"extends": "//",
|
||||
"overrides": [
|
||||
{
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"useSortedKeys": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"includes": ["package.json"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user