Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c37af4f191 | ||
|
|
f964b8aaff | ||
|
|
424e6b8f9b | ||
|
|
763d3de5a8 | ||
|
|
f6f138ccbc | ||
|
|
f5d5e73e57 | ||
|
|
673e51b11c | ||
|
|
282edf7ccb | ||
|
|
a87e91b99c | ||
|
|
0e630c4550 | ||
|
|
11c7bf0955 | ||
|
|
c9e430d092 | ||
|
|
1f440d0c97 | ||
|
|
3a0aac4393 | ||
|
|
7b13015053 | ||
|
|
7f081618e1 | ||
|
|
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 |
@@ -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,7 @@ module.exports = [
|
||||
"/wallets",
|
||||
"/account-abstraction",
|
||||
"/payments",
|
||||
"/payments/x402",
|
||||
"/x402",
|
||||
"/nexus",
|
||||
"/auth",
|
||||
"/in-app-wallets",
|
||||
|
||||
|
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,8 @@ export type AuthOption =
|
||||
| "steam"
|
||||
| "apple"
|
||||
| "coinbase"
|
||||
| "line";
|
||||
| "line"
|
||||
| "epic";
|
||||
|
||||
export type Ecosystem = {
|
||||
name: string;
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -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))",
|
||||
},
|
||||
|
||||
@@ -225,6 +225,10 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
|
||||
moduleInfo: {
|
||||
bytecodeUri: selectedModule.metadata.bytecodeUri,
|
||||
},
|
||||
isStylus:
|
||||
(selectedModule.metadata.compilers?.stylus &&
|
||||
selectedModule.metadata.compilers?.stylus?.length > 0) ||
|
||||
false,
|
||||
});
|
||||
},
|
||||
queryKey: [
|
||||
@@ -432,6 +436,7 @@ async function isModuleCompatible(options: {
|
||||
moduleInfo: {
|
||||
bytecodeUri: string;
|
||||
};
|
||||
isStylus: boolean;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
// 1. get module's bytecode
|
||||
@@ -444,15 +449,17 @@ async function isModuleCompatible(options: {
|
||||
|
||||
// 2. check compatibility with core and installed modules
|
||||
try {
|
||||
const isCompatible = await checkModulesCompatibility({
|
||||
chain: options.contractInfo.chain,
|
||||
client: options.client,
|
||||
coreBytecode: options.contractInfo.bytecode,
|
||||
moduleBytecodes: [
|
||||
moduleBytecode,
|
||||
...options.contractInfo.installedModuleBytecodes,
|
||||
],
|
||||
});
|
||||
const isCompatible =
|
||||
options.isStylus ||
|
||||
(await checkModulesCompatibility({
|
||||
chain: options.contractInfo.chain,
|
||||
client: options.client,
|
||||
coreBytecode: options.contractInfo.bytecode,
|
||||
moduleBytecodes: [
|
||||
moduleBytecode,
|
||||
...options.contractInfo.installedModuleBytecodes,
|
||||
],
|
||||
}));
|
||||
|
||||
return isCompatible;
|
||||
} catch (e) {
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
CircleXIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { NATIVE_TOKEN_ADDRESS, type ThirdwebClient } from "thirdweb";
|
||||
import {
|
||||
getAddress,
|
||||
NATIVE_TOKEN_ADDRESS,
|
||||
type ThirdwebClient,
|
||||
} from "thirdweb";
|
||||
import type { Status, Token } from "thirdweb/bridge";
|
||||
import { status } from "thirdweb/bridge";
|
||||
import { toTokens } from "thirdweb/utils";
|
||||
@@ -123,6 +127,8 @@ function TokenInfo(props: {
|
||||
const isNativeToken =
|
||||
props.token.address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase();
|
||||
|
||||
const tokenAddress = getAddress(props.token.address);
|
||||
|
||||
return (
|
||||
<div className="flex-1 pt-10 pb-9 px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -206,7 +212,7 @@ function TokenInfo(props: {
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground ">{props.addressLabel}</p>
|
||||
<WalletAddress
|
||||
address={props.walletAddress}
|
||||
address={getAddress(props.walletAddress)}
|
||||
client={props.client}
|
||||
className="py-0.5 h-auto text-sm [&>*]:!font-sans tabular-nums [&>*]:font-normal text-muted-foreground hover:text-foreground"
|
||||
iconClassName="size-3"
|
||||
@@ -222,8 +228,8 @@ function TokenInfo(props: {
|
||||
? `/${chainQuery.data?.slug || props.token.chainId}`
|
||||
: `/${chainQuery.data?.slug || props.token.chainId}/${props.token.address}`
|
||||
}
|
||||
textToShow={props.token.address}
|
||||
textToCopy={props.token.address}
|
||||
textToShow={tokenAddress}
|
||||
textToCopy={tokenAddress}
|
||||
copyTooltip="Copy Token Address"
|
||||
className="-translate-x-1"
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Clock4Icon,
|
||||
InfoIcon,
|
||||
} from "lucide-react";
|
||||
import { toTokens } from "thirdweb";
|
||||
import { getAddress, toTokens } from "thirdweb";
|
||||
import { status } from "thirdweb/bridge";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import {
|
||||
@@ -153,6 +153,9 @@ function GeneericTxDetails(props: {
|
||||
|
||||
const timestamp = getDatefromTimestamp(block.timestamp);
|
||||
|
||||
const fromAddress = getAddress(transaction.from);
|
||||
const toAddress = transaction.to ? getAddress(transaction.to) : undefined;
|
||||
|
||||
return (
|
||||
<div className="border rounded-xl p-4 lg:p-6 bg-card">
|
||||
{/* section 1 */}
|
||||
@@ -216,8 +219,8 @@ function GeneericTxDetails(props: {
|
||||
<section className="border-b py-6 space-y-5 lg:space-y-3">
|
||||
<GridItem label="From" tooltip="The sending party of the transaction.">
|
||||
<CopyTextButton
|
||||
textToCopy={transaction.from}
|
||||
textToShow={transaction.from}
|
||||
textToCopy={fromAddress}
|
||||
textToShow={fromAddress}
|
||||
tooltip="Copy from address"
|
||||
variant="ghost"
|
||||
copyIconPosition="right"
|
||||
@@ -225,14 +228,14 @@ function GeneericTxDetails(props: {
|
||||
/>
|
||||
</GridItem>
|
||||
|
||||
{transaction.to && (
|
||||
{toAddress && (
|
||||
<GridItem
|
||||
label="Interacted with (To)"
|
||||
tooltip="The receiving party of the transaction (could be a contract address)."
|
||||
>
|
||||
<CopyTextButton
|
||||
textToCopy={transaction.to}
|
||||
textToShow={transaction.to}
|
||||
textToCopy={toAddress}
|
||||
textToShow={toAddress}
|
||||
tooltip="Copy to address"
|
||||
variant="ghost"
|
||||
copyIconPosition="right"
|
||||
|
||||
@@ -183,13 +183,64 @@ const STYLUS = {
|
||||
],
|
||||
description:
|
||||
"Airdrop your NFTs or tokens to a large number of recipients. Built with Arbitrum Stylus.",
|
||||
displayName: "Arbitrum Stylus Contracts",
|
||||
displayName: "Airdrop - Arbitrum Stylus",
|
||||
id: "stylus",
|
||||
name: "Stylus",
|
||||
showInExplore: true,
|
||||
isBeta: true,
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const MODULAR_CONTRACTS_STYLUS = {
|
||||
contracts: [
|
||||
// erc721 token
|
||||
[
|
||||
"thirdweb.eth/ERC721CoreInitializable",
|
||||
[
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusMintableERC721",
|
||||
"deployer.thirdweb.eth/BatchMetadataERC721",
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusTransferableERC721",
|
||||
],
|
||||
{
|
||||
description: "ERC721 NFTs that only owners can mint.",
|
||||
title: "Modular NFT Collection",
|
||||
},
|
||||
],
|
||||
// erc1155 token
|
||||
[
|
||||
"thirdweb.eth/ERC1155CoreInitializable",
|
||||
[
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusMintableERC1155",
|
||||
"deployer.thirdweb.eth/BatchMetadataERC1155",
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusTransferableERC1155",
|
||||
"deployer.thirdweb.eth/SequentialTokenIdERC1155",
|
||||
],
|
||||
{
|
||||
description: "ERC1155 NFTs that only owners can mint.",
|
||||
title: "Modular Edition",
|
||||
},
|
||||
],
|
||||
// erc20 token
|
||||
[
|
||||
"thirdweb.eth/ERC20CoreInitializable",
|
||||
[
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusMintableERC20",
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusTransferableERC20",
|
||||
],
|
||||
{
|
||||
description: "ERC20 Tokens that only owners can mint.",
|
||||
title: "Modular Token",
|
||||
},
|
||||
],
|
||||
],
|
||||
description:
|
||||
"Collection of highly customizable and upgradeable smart contracts built with the modular contracts framework.",
|
||||
displayName: "Modular Contracts - Arbitrum Stylus",
|
||||
id: "modular-contracts",
|
||||
name: "modular",
|
||||
showInExplore: true,
|
||||
isBeta: true,
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const CATEGORIES: Record<string, ExploreCategory> = {
|
||||
[POPULAR.id]: POPULAR,
|
||||
[NFTS.id]: NFTS,
|
||||
@@ -203,6 +254,7 @@ const CATEGORIES: Record<string, ExploreCategory> = {
|
||||
[COMMERCE.id]: COMMERCE,
|
||||
[STAKING.id]: STAKING,
|
||||
[GOVERNANCE.id]: GOVERNANCE,
|
||||
[MODULAR_CONTRACTS_STYLUS.id]: MODULAR_CONTRACTS_STYLUS,
|
||||
};
|
||||
|
||||
export function getCategory(id: string) {
|
||||
|
||||
@@ -58,6 +58,7 @@ const authOptions = [
|
||||
"apple",
|
||||
"coinbase",
|
||||
"line",
|
||||
"epic",
|
||||
] as const satisfies AuthOption[];
|
||||
|
||||
type AuthOptionsFormData = {
|
||||
|
||||
@@ -59,7 +59,6 @@ export function ErrorRate({ datapoints }: { datapoints: ResultItem[] }) {
|
||||
key={tag}
|
||||
stackId="a"
|
||||
stroke={`var(--color-${tag})`}
|
||||
type="natural"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -378,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,
|
||||
@@ -408,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}
|
||||
|
||||
@@ -1,45 +1,95 @@
|
||||
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
|
||||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Spinner } from "@workspace/ui/components/spinner";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { UnifiedTransactionsTable } from "../components/transactions-table.client";
|
||||
import { getTransactionAnalyticsSummary } from "../lib/analytics-summary.client";
|
||||
import type { Wallet } from "../server-wallets/wallet-table/types";
|
||||
import { TransactionAnalyticsFilter } from "./filter";
|
||||
import { TransactionsChartCard } from "./tx-chart/tx-chart";
|
||||
import type { SolanaWallet } from "../solana-wallets/wallet-table/types";
|
||||
import { EngineChecklist } from "./ftux.client";
|
||||
import { TransactionAnalyticsSummary } from "./summary";
|
||||
import { TransactionsAnalytics } from "./tx-chart/tx-chart";
|
||||
|
||||
export function TransactionsAnalyticsPageContent(props: {
|
||||
searchParams: {
|
||||
from?: string | undefined | string[];
|
||||
to?: string | undefined | string[];
|
||||
interval?: string | undefined | string[];
|
||||
};
|
||||
project: Project;
|
||||
showAnalytics: boolean;
|
||||
wallets?: Wallet[];
|
||||
wallets: Wallet[];
|
||||
teamSlug: string;
|
||||
client: ThirdwebClient;
|
||||
authToken: string;
|
||||
teamId: string;
|
||||
isManagedVault: boolean;
|
||||
testTxWithWallet: string | undefined;
|
||||
testSolanaTxWithWallet: string | undefined;
|
||||
solanaWallets: SolanaWallet[];
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveSearchParamsProvider value={props.searchParams}>
|
||||
<div className="flex grow flex-col gap-6">
|
||||
{props.showAnalytics && (
|
||||
<>
|
||||
<div className="flex justify-end">
|
||||
<TransactionAnalyticsFilter />
|
||||
</div>
|
||||
<TransactionsChartCard
|
||||
project={props.project}
|
||||
searchParams={props.searchParams}
|
||||
teamSlug={props.teamSlug}
|
||||
wallets={props.wallets ?? []}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<UnifiedTransactionsTable
|
||||
client={props.client}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
/>
|
||||
const engineTxSummaryQuery = useQuery({
|
||||
queryKey: [
|
||||
"engine-tx-analytics-summary",
|
||||
props.teamId,
|
||||
props.project.publishableKey,
|
||||
props.authToken,
|
||||
],
|
||||
queryFn: async () => {
|
||||
const data = await getTransactionAnalyticsSummary({
|
||||
clientId: props.project.publishableKey,
|
||||
teamId: props.teamId,
|
||||
authToken: props.authToken,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
if (engineTxSummaryQuery.isPending) {
|
||||
return (
|
||||
<div className="flex h-[642px] grow items-center justify-center bg-card rounded-xl border">
|
||||
<Spinner className="size-10" />
|
||||
</div>
|
||||
</ResponsiveSearchParamsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const hasTransactions = engineTxSummaryQuery.data
|
||||
? engineTxSummaryQuery.data.totalCount > 0
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col gap-10">
|
||||
<EngineChecklist
|
||||
isManagedVault={props.isManagedVault}
|
||||
client={props.client}
|
||||
hasTransactions={hasTransactions}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
testTxWithWallet={props.testTxWithWallet}
|
||||
testSolanaTxWithWallet={props.testSolanaTxWithWallet}
|
||||
wallets={props.wallets}
|
||||
solanaWallets={props.solanaWallets}
|
||||
/>
|
||||
|
||||
{props.showAnalytics && hasTransactions && (
|
||||
<div className="flex flex-col gap-6">
|
||||
<TransactionAnalyticsSummary
|
||||
clientId={props.project.publishableKey}
|
||||
teamId={props.project.teamId}
|
||||
initialData={engineTxSummaryQuery.data}
|
||||
/>
|
||||
<TransactionsAnalytics
|
||||
project={props.project}
|
||||
authToken={props.authToken}
|
||||
teamSlug={props.teamSlug}
|
||||
wallets={props.wallets ?? []}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<UnifiedTransactionsTable
|
||||
client={props.client}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useResponsiveSearchParams,
|
||||
useSetResponsiveSearchParams,
|
||||
} from "responsive-rsc";
|
||||
import { DateRangeSelector } from "@/components/analytics/date-range-selector";
|
||||
import { IntervalSelector } from "@/components/analytics/interval-selector";
|
||||
import { normalizeTimeISOString } from "@/lib/time";
|
||||
import { getTxAnalyticsFiltersFromSearchParams } from "../lib/utils";
|
||||
|
||||
export function TransactionAnalyticsFilter() {
|
||||
const responsiveSearchParams = useResponsiveSearchParams();
|
||||
const setResponsiveSearchParams = useSetResponsiveSearchParams();
|
||||
|
||||
const { range, interval } = getTxAnalyticsFiltersFromSearchParams({
|
||||
from: responsiveSearchParams.from,
|
||||
interval: responsiveSearchParams.interval,
|
||||
to: responsiveSearchParams.to,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="no-scrollbar flex items-center gap-3 max-sm:overflow-auto">
|
||||
<DateRangeSelector
|
||||
popoverAlign="end"
|
||||
range={range}
|
||||
setRange={(newRange) => {
|
||||
setResponsiveSearchParams((v) => {
|
||||
return {
|
||||
...v,
|
||||
from: normalizeTimeISOString(newRange.from),
|
||||
to: normalizeTimeISOString(newRange.to),
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<IntervalSelector
|
||||
intervalType={interval}
|
||||
setIntervalType={(newInterval) => {
|
||||
setResponsiveSearchParams((v) => {
|
||||
return {
|
||||
...v,
|
||||
interval: newInterval,
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ActivityIcon, CoinsIcon } from "lucide-react";
|
||||
import { toEther } from "thirdweb/utils";
|
||||
import { StatCard } from "@/components/analytics/stat"; // Assuming correct path
|
||||
import type { TransactionSummaryData } from "../lib/analytics";
|
||||
import type { TransactionSummaryData } from "../lib/analytics-summary.client";
|
||||
|
||||
// Renders the UI based on fetched data or pending state
|
||||
function TransactionAnalyticsSummaryUI(props: {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
import { NEXT_PUBLIC_ENGINE_CLOUD_URL } from "@/constants/public-envs";
|
||||
import type { TransactionStats } from "@/types/analytics";
|
||||
|
||||
export async function getTransactionsChartData({
|
||||
teamId,
|
||||
clientId,
|
||||
from,
|
||||
to,
|
||||
interval,
|
||||
authToken,
|
||||
}: {
|
||||
teamId: string;
|
||||
clientId: string;
|
||||
from: string;
|
||||
to: string;
|
||||
interval: "day" | "week";
|
||||
authToken: string;
|
||||
}): Promise<TransactionStats[]> {
|
||||
const filters = {
|
||||
endDate: to,
|
||||
resolution: interval,
|
||||
startDate: from,
|
||||
};
|
||||
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_ENGINE_CLOUD_URL}/v1/transactions/analytics`,
|
||||
{
|
||||
body: JSON.stringify(filters),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": clientId,
|
||||
"x-team-id": teamId,
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 || response.status === 400) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// TODO - need to handle this error state, like we do with the connect charts
|
||||
throw new Error(
|
||||
`Error fetching transactions chart data: ${response.status} ${
|
||||
response.statusText
|
||||
} - ${await response.text().catch(() => "Unknown error")}`,
|
||||
);
|
||||
}
|
||||
|
||||
type TransactionsChartResponse = {
|
||||
result: {
|
||||
analytics: Array<{
|
||||
timeBucket: string;
|
||||
chainId: string;
|
||||
count: number;
|
||||
}>;
|
||||
metadata: {
|
||||
resolution: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const data = (await response.json()) as TransactionsChartResponse;
|
||||
|
||||
return data.result.analytics.map((stat) => ({
|
||||
chainId: Number(stat.chainId),
|
||||
count: stat.count,
|
||||
date: stat.timeBucket,
|
||||
}));
|
||||
}
|
||||
@@ -108,10 +108,10 @@ export function TransactionsChartCardUI(props: {
|
||||
customHeader={
|
||||
<div className="relative px-6 pt-6">
|
||||
<h3 className="mb-0.5 font-semibold text-xl tracking-tight">
|
||||
Daily Transactions
|
||||
Transactions
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Amount of daily transactions by chain.
|
||||
transactions broken down by chain
|
||||
</p>
|
||||
|
||||
<div className="top-6 right-6 mb-8 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
|
||||
@@ -194,13 +194,7 @@ function EmptyChartContent(props: {
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="flex items-center gap-2 rounded-full border bg-background px-3.5 py-1.5 text-sm">
|
||||
<span className="!pointer-events-auto relative flex size-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75" />
|
||||
<span className="relative inline-flex size-2 rounded-full bg-primary" />
|
||||
</span>
|
||||
Waiting for transactions...
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">No transactions found</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,73 +1,68 @@
|
||||
import { ResponsiveSuspense } from "responsive-rsc";
|
||||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { getTransactionsChart } from "../../lib/analytics";
|
||||
import { getTxAnalyticsFiltersFromSearchParams } from "../../lib/utils";
|
||||
import {
|
||||
DateRangeSelector,
|
||||
getLastNDaysRange,
|
||||
} from "@/components/analytics/date-range-selector";
|
||||
import { IntervalSelector } from "@/components/analytics/interval-selector";
|
||||
import { normalizeTimeISOString } from "@/lib/time";
|
||||
import type { Wallet } from "../../server-wallets/wallet-table/types";
|
||||
import { getTransactionsChartData } from "./data";
|
||||
import { TransactionsChartCardUI } from "./tx-chart-ui";
|
||||
|
||||
async function AsyncTransactionsChartCard(props: {
|
||||
from: string;
|
||||
to: string;
|
||||
interval: "day" | "week";
|
||||
export function TransactionsAnalytics(props: {
|
||||
project: Project;
|
||||
wallets: Wallet[];
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
}) {
|
||||
const data = await getTransactionsChart({
|
||||
const [range, setRange] = useState(() => getLastNDaysRange("last-30"));
|
||||
const [interval, setInterval] = useState<"day" | "week">("day");
|
||||
|
||||
const params = {
|
||||
clientId: props.project.publishableKey,
|
||||
from: props.from,
|
||||
interval: props.interval,
|
||||
from: normalizeTimeISOString(range.from),
|
||||
interval: interval,
|
||||
teamId: props.project.teamId,
|
||||
to: props.to,
|
||||
to: normalizeTimeISOString(range.to),
|
||||
authToken: props.authToken,
|
||||
};
|
||||
|
||||
const engineTxAnalytics = useQuery({
|
||||
queryKey: ["engine-tx-analytics", params],
|
||||
queryFn: async () => {
|
||||
const data = await getTransactionsChartData(params);
|
||||
return data;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<TransactionsChartCardUI
|
||||
isPending={false}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
userOpStats={data}
|
||||
wallets={props.wallets}
|
||||
/>
|
||||
);
|
||||
}
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex justify-end">
|
||||
<div className="no-scrollbar flex items-center gap-3 max-sm:overflow-auto">
|
||||
<DateRangeSelector
|
||||
popoverAlign="end"
|
||||
range={range}
|
||||
setRange={setRange}
|
||||
/>
|
||||
|
||||
export function TransactionsChartCard(props: {
|
||||
searchParams: {
|
||||
from?: string | undefined | string[];
|
||||
to?: string | undefined | string[];
|
||||
interval?: string | undefined | string[];
|
||||
};
|
||||
project: Project;
|
||||
wallets: Wallet[];
|
||||
teamSlug: string;
|
||||
}) {
|
||||
const { range, interval } = getTxAnalyticsFiltersFromSearchParams(
|
||||
props.searchParams,
|
||||
);
|
||||
|
||||
return (
|
||||
<ResponsiveSuspense
|
||||
// TODO - change this if this component does not end up using these params
|
||||
fallback={
|
||||
<TransactionsChartCardUI
|
||||
isPending={true}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
userOpStats={[]}
|
||||
wallets={[]}
|
||||
/>
|
||||
}
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
>
|
||||
<AsyncTransactionsChartCard
|
||||
from={range.from.toISOString()}
|
||||
interval={interval}
|
||||
<IntervalSelector
|
||||
intervalType={interval}
|
||||
setIntervalType={setInterval}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TransactionsChartCardUI
|
||||
isPending={engineTxAnalytics.isPending}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
to={range.to.toISOString()}
|
||||
userOpStats={engineTxAnalytics.data ?? []}
|
||||
wallets={props.wallets}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,6 +64,7 @@ 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 type { Wallet as EVMWallet } from "../server-wallets/wallet-table/types";
|
||||
@@ -79,6 +87,7 @@ interface ServerWalletsTableProps {
|
||||
teamSlug: string;
|
||||
client: ThirdwebClient;
|
||||
solanaPermissionError?: boolean;
|
||||
authToken: string;
|
||||
}
|
||||
|
||||
export function ServerWalletsTable(props: ServerWalletsTableProps) {
|
||||
@@ -95,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();
|
||||
|
||||
@@ -145,11 +158,33 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
|
||||
</>
|
||||
)}
|
||||
{activeChain === "solana" && (
|
||||
<CreateSolanaWallet
|
||||
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>
|
||||
|
||||
@@ -278,6 +313,8 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
|
||||
project={project}
|
||||
teamSlug={teamSlug}
|
||||
client={client}
|
||||
authToken={authToken}
|
||||
chainId={selectedSolanaChain}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -507,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",
|
||||
@@ -547,7 +588,12 @@ function SolanaWalletRow({
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<SolanaWalletBalance publicKey={wallet.publicKey} />
|
||||
<SolanaWalletBalance
|
||||
publicKey={wallet.publicKey}
|
||||
authToken={authToken}
|
||||
clientId={project.publishableKey}
|
||||
chainId={chainId}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
@@ -739,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) {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { NEXT_PUBLIC_ENGINE_CLOUD_URL } from "@/constants/public-envs";
|
||||
|
||||
export type TransactionSummaryData = {
|
||||
totalCount: number;
|
||||
totalGasCostWei: string;
|
||||
totalGasUnitsUsed: string; // Keep fetched data structure
|
||||
};
|
||||
|
||||
type AnalyticsSummaryApiResponse = {
|
||||
result: {
|
||||
summary: {
|
||||
totalCount: number;
|
||||
totalGasCostWei: string;
|
||||
totalGasUnitsUsed: string;
|
||||
};
|
||||
metadata: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// Fetches data from the /analytics-summary endpoint
|
||||
export async function getTransactionAnalyticsSummary(props: {
|
||||
teamId: string;
|
||||
clientId: string;
|
||||
authToken: string;
|
||||
}): Promise<TransactionSummaryData> {
|
||||
const body = {};
|
||||
const defaultData: TransactionSummaryData = {
|
||||
totalCount: 0,
|
||||
totalGasCostWei: "0",
|
||||
totalGasUnitsUsed: "0",
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_ENGINE_CLOUD_URL}/v1/transactions/analytics-summary`,
|
||||
{
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
Authorization: `Bearer ${props.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 || response.status === 400) {
|
||||
console.error("Unauthorized fetching transaction summary");
|
||||
return defaultData;
|
||||
}
|
||||
const errorText = await response.text();
|
||||
throw new Error(
|
||||
`Error fetching transaction summary: ${response.status} ${response.statusText} - ${errorText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as AnalyticsSummaryApiResponse;
|
||||
|
||||
return {
|
||||
totalCount: data.result.summary.totalCount ?? 0,
|
||||
totalGasCostWei: data.result.summary.totalGasCostWei ?? "0",
|
||||
totalGasUnitsUsed: data.result.summary.totalGasUnitsUsed ?? "0",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch transaction summary:", error);
|
||||
return defaultData;
|
||||
}
|
||||
}
|
||||
@@ -1,157 +1,10 @@
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { NEXT_PUBLIC_ENGINE_CLOUD_URL } from "@/constants/public-envs";
|
||||
import type { TransactionStats } from "@/types/analytics";
|
||||
import type {
|
||||
Transaction,
|
||||
TransactionsResponse,
|
||||
} from "../analytics/tx-table/types";
|
||||
|
||||
// Define the structure of the data we expect back from our fetch function
|
||||
export type TransactionSummaryData = {
|
||||
totalCount: number;
|
||||
totalGasCostWei: string;
|
||||
totalGasUnitsUsed: string; // Keep fetched data structure
|
||||
};
|
||||
|
||||
// Define the structure of the API response
|
||||
type AnalyticsSummaryApiResponse = {
|
||||
result: {
|
||||
summary: {
|
||||
totalCount: number;
|
||||
totalGasCostWei: string;
|
||||
totalGasUnitsUsed: string;
|
||||
};
|
||||
metadata: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// Fetches data from the /analytics-summary endpoint
|
||||
export async function getTransactionAnalyticsSummary(props: {
|
||||
teamId: string;
|
||||
clientId: string;
|
||||
}): Promise<TransactionSummaryData> {
|
||||
const authToken = await getAuthToken();
|
||||
const body = {};
|
||||
const defaultData: TransactionSummaryData = {
|
||||
totalCount: 0,
|
||||
totalGasCostWei: "0",
|
||||
totalGasUnitsUsed: "0",
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_ENGINE_CLOUD_URL}/v1/transactions/analytics-summary`,
|
||||
{
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 || response.status === 400) {
|
||||
console.error("Unauthorized fetching transaction summary");
|
||||
return defaultData;
|
||||
}
|
||||
const errorText = await response.text();
|
||||
throw new Error(
|
||||
`Error fetching transaction summary: ${response.status} ${response.statusText} - ${errorText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as AnalyticsSummaryApiResponse;
|
||||
|
||||
return {
|
||||
totalCount: data.result.summary.totalCount ?? 0,
|
||||
totalGasCostWei: data.result.summary.totalGasCostWei ?? "0",
|
||||
totalGasUnitsUsed: data.result.summary.totalGasUnitsUsed ?? "0",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch transaction summary:", error);
|
||||
return defaultData;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTransactionsChart({
|
||||
teamId,
|
||||
clientId,
|
||||
from,
|
||||
to,
|
||||
interval,
|
||||
}: {
|
||||
teamId: string;
|
||||
clientId: string;
|
||||
from: string;
|
||||
to: string;
|
||||
interval: "day" | "week";
|
||||
}): Promise<TransactionStats[]> {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
const filters = {
|
||||
endDate: to,
|
||||
resolution: interval,
|
||||
startDate: from,
|
||||
};
|
||||
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_ENGINE_CLOUD_URL}/v1/transactions/analytics`,
|
||||
{
|
||||
body: JSON.stringify(filters),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": clientId,
|
||||
"x-team-id": teamId,
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 || response.status === 400) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// TODO - need to handle this error state, like we do with the connect charts
|
||||
throw new Error(
|
||||
`Error fetching transactions chart data: ${response.status} ${
|
||||
response.statusText
|
||||
} - ${await response.text().catch(() => "Unknown error")}`,
|
||||
);
|
||||
}
|
||||
|
||||
type TransactionsChartResponse = {
|
||||
result: {
|
||||
analytics: Array<{
|
||||
timeBucket: string;
|
||||
chainId: string;
|
||||
count: number;
|
||||
}>;
|
||||
metadata: {
|
||||
resolution: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const data = (await response.json()) as TransactionsChartResponse;
|
||||
|
||||
return data.result.analytics.map((stat) => ({
|
||||
chainId: Number(stat.chainId),
|
||||
count: stat.count,
|
||||
date: stat.timeBucket,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getSingleTransaction({
|
||||
teamId,
|
||||
clientId,
|
||||
|
||||
@@ -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,14 +0,0 @@
|
||||
import { getFiltersFromSearchParams } from "@/lib/time";
|
||||
|
||||
export function getTxAnalyticsFiltersFromSearchParams(params: {
|
||||
from?: string | undefined | string[];
|
||||
to?: string | undefined | string[];
|
||||
interval?: string | undefined | string[];
|
||||
}) {
|
||||
return getFiltersFromSearchParams({
|
||||
defaultRange: "last-30",
|
||||
from: params.from,
|
||||
interval: params.interval,
|
||||
to: params.to,
|
||||
});
|
||||
}
|
||||
@@ -7,10 +7,7 @@ import { ProjectPage } from "@/components/blocks/project-page/project-page";
|
||||
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { TransactionsAnalyticsPageContent } from "./analytics/analytics-page";
|
||||
import { EngineChecklist } from "./analytics/ftux.client";
|
||||
import { TransactionAnalyticsSummary } from "./analytics/summary";
|
||||
import { ServerWalletsTable } from "./components/server-wallets-table.client";
|
||||
import { getTransactionAnalyticsSummary } from "./lib/analytics";
|
||||
import type { Wallet } from "./server-wallets/wallet-table/types";
|
||||
import { listSolanaAccounts } from "./solana-wallets/lib/vault.client";
|
||||
import type { SolanaWallet } from "./solana-wallets/wallet-table/types";
|
||||
@@ -110,12 +107,6 @@ export default async function TransactionsAnalyticsPage(props: {
|
||||
const isSolanaPermissionError =
|
||||
solanaAccounts.error?.message.includes("AUTH_INSUFFICIENT_SCOPE") ?? false;
|
||||
|
||||
const initialData = await getTransactionAnalyticsSummary({
|
||||
clientId: project.publishableKey,
|
||||
teamId: project.teamId,
|
||||
}).catch(() => undefined);
|
||||
const hasTransactions = initialData ? initialData.totalCount > 0 : false;
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: project.teamId,
|
||||
@@ -152,41 +143,30 @@ export default async function TransactionsAnalyticsPage(props: {
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-10">
|
||||
<EngineChecklist
|
||||
isManagedVault={isManagedVault}
|
||||
client={client}
|
||||
hasTransactions={hasTransactions}
|
||||
project={project}
|
||||
teamSlug={params.team_slug}
|
||||
testTxWithWallet={searchParams.testTxWithWallet as string | undefined}
|
||||
testSolanaTxWithWallet={
|
||||
searchParams.testSolanaTxWithWallet as string | undefined
|
||||
}
|
||||
wallets={wallets ?? []}
|
||||
solanaWallets={solanaAccounts.data.items}
|
||||
/>
|
||||
{hasTransactions &&
|
||||
!searchParams.testTxWithWallet &&
|
||||
!searchParams.testSolanaTxWithWallet && (
|
||||
<TransactionAnalyticsSummary
|
||||
clientId={project.publishableKey}
|
||||
initialData={initialData}
|
||||
teamId={project.teamId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* transactions */}
|
||||
<TransactionsAnalyticsPageContent
|
||||
client={client}
|
||||
authToken={authToken}
|
||||
project={project}
|
||||
searchParams={searchParams}
|
||||
showAnalytics={
|
||||
hasTransactions &&
|
||||
!searchParams.testTxWithWallet &&
|
||||
!searchParams.testSolanaTxWithWallet
|
||||
}
|
||||
teamSlug={params.team_slug}
|
||||
wallets={wallets}
|
||||
wallets={wallets ?? []}
|
||||
solanaWallets={solanaAccounts.data.items}
|
||||
teamId={project.teamId}
|
||||
isManagedVault={isManagedVault}
|
||||
testTxWithWallet={
|
||||
typeof searchParams.testTxWithWallet === "string"
|
||||
? searchParams.testTxWithWallet
|
||||
: undefined
|
||||
}
|
||||
testSolanaTxWithWallet={
|
||||
typeof searchParams.testSolanaTxWithWallet === "string"
|
||||
? searchParams.testSolanaTxWithWallet
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Server Wallets (EVM + Solana) */}
|
||||
@@ -215,6 +195,7 @@ export default async function TransactionsAnalyticsPage(props: {
|
||||
solanaWallets={solanaAccounts.data.items}
|
||||
teamSlug={params.team_slug}
|
||||
solanaPermissionError={isSolanaPermissionError}
|
||||
authToken={authToken}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -70,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" />
|
||||
|
||||
@@ -87,6 +87,7 @@ const authMethodsToPickFrom: InAppWalletAuth[] = [
|
||||
"line",
|
||||
"x",
|
||||
"tiktok",
|
||||
"epic",
|
||||
"coinbase",
|
||||
"farcaster",
|
||||
"telegram",
|
||||
|
||||
@@ -202,6 +202,7 @@ export default async function Page(props: {
|
||||
solanaWallets={solanaAccounts.data.items}
|
||||
teamSlug={params.team_slug}
|
||||
solanaPermissionError={isSolanaPermissionError || false}
|
||||
authToken={authToken}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -20,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>
|
||||
|
||||
@@ -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,144 +60,33 @@ export default async function PayPage({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-dvh w-full items-center justify-center">
|
||||
<div className="relative flex flex-col min-h-dvh w-full">
|
||||
<ThemeProvider
|
||||
forcedTheme={theme === "light" ? "light" : "dark"}
|
||||
defaultTheme={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>
|
||||
<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 py-12 w-full items-center grow">
|
||||
<PayPageWidget
|
||||
amount={
|
||||
paymentLink.amount ? BigInt(paymentLink.amount) : undefined
|
||||
}
|
||||
chainId={Number(paymentLink.destinationToken.chainId)}
|
||||
clientId={undefined} // Payment links don't need to use the same client ID to be executed
|
||||
image={paymentLink.imageUrl}
|
||||
name={paymentLink.title}
|
||||
paymentLinkId={id}
|
||||
purchaseData={paymentLink.purchaseData}
|
||||
recipientAddress={paymentLink.receiver}
|
||||
redirectUri={redirectUri}
|
||||
token={token}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
<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)}
|
||||
clientId={undefined} // Payment links don't need to use the same client ID to be executed
|
||||
image={paymentLink.imageUrl}
|
||||
name={paymentLink.title}
|
||||
paymentLinkId={id}
|
||||
purchaseData={paymentLink.purchaseData}
|
||||
recipientAddress={paymentLink.receiver}
|
||||
redirectUri={redirectUri}
|
||||
token={token}
|
||||
/>
|
||||
</main>
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
);
|
||||
@@ -237,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();
|
||||
|
||||
@@ -9,7 +9,12 @@ import { payLandingWallets } from "./wallets";
|
||||
const client = getClientThirdwebClient();
|
||||
|
||||
export function StyledBuyWidget(
|
||||
props: Omit<BuyWidgetProps, "client" | "theme">,
|
||||
props: Omit<
|
||||
BuyWidgetProps,
|
||||
"client" | "theme" | "onSuccess" | "onError" | "onCancel"
|
||||
> & {
|
||||
successUrl?: string;
|
||||
},
|
||||
) {
|
||||
const { theme } = useTheme();
|
||||
|
||||
@@ -22,6 +27,22 @@ export function StyledBuyWidget(
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,19 +21,44 @@ 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 searchParams = await props.searchParams;
|
||||
|
||||
const onlyAddress = (v: string) => (isAddress(v) ? v : undefined);
|
||||
const onlyNumber = (v: string) =>
|
||||
Number.isNaN(Number(v)) ? undefined : Number(v);
|
||||
|
||||
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
|
||||
@@ -51,6 +76,7 @@ export default async function PayPage(props: {
|
||||
tokenAddress={token}
|
||||
receiverAddress={receiver}
|
||||
amount={amount ? amount.toString() : undefined}
|
||||
successUrl={successUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -43,7 +43,6 @@ export function X402RightSection(props: { options: X402PlaygroundOptions }) {
|
||||
fetch,
|
||||
THIRDWEB_CLIENT,
|
||||
activeWallet,
|
||||
BigInt(1 * 10 ** 18),
|
||||
);
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set("chainId", props.options.chain.id.toString());
|
||||
|
||||
@@ -20,6 +20,7 @@ const allInAppWalletLoginMethods: InAppWalletAuth[] = [
|
||||
"line",
|
||||
"x",
|
||||
"tiktok",
|
||||
"epic",
|
||||
"facebook",
|
||||
"apple",
|
||||
"coinbase",
|
||||
|
||||
@@ -742,6 +742,14 @@ const paymentRedirects = {
|
||||
"/payments/swap": "/bridge/swap",
|
||||
"/payments/tokens": "/bridge/tokens",
|
||||
"/payments/routes": "/bridge/routes",
|
||||
"/payments/x402": "/x402",
|
||||
"/payments/x402/:path*": "/x402/:path*",
|
||||
"/payments/fund": "/bridge/fund",
|
||||
"/payments/products": "/bridge/products",
|
||||
"/payments/transactions": "/bridge/transactions",
|
||||
"/payments/send": "/bridge/send",
|
||||
"/payments/webhooks": "/bridge/webhooks",
|
||||
"/payments/custom-data": "/bridge/custom-data",
|
||||
};
|
||||
|
||||
const contractRedirects = {
|
||||
|
||||
@@ -40,8 +40,8 @@ const links = [
|
||||
name: "Contracts",
|
||||
},
|
||||
{
|
||||
href: "/payments",
|
||||
name: "Payments",
|
||||
href: "/x402",
|
||||
name: "x402",
|
||||
},
|
||||
{
|
||||
href: "/bridge",
|
||||
|
||||
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 310 KiB |
@@ -188,8 +188,8 @@ Any purchase data you provide to a payment will be included on all webhooks, sta
|
||||
|
||||
To connect with other auth strategies, use external wallets, or sponsor gas for users, check out the following guides:
|
||||
|
||||
- [Send a Payment](/payments/send)
|
||||
- [Webhooks](/payments/webhooks)
|
||||
- [Send a Payment](/bridge/send)
|
||||
- [Webhooks](/bridge/webhooks)
|
||||
|
||||
## Explore Full API References
|
||||
|
||||
@@ -74,8 +74,8 @@ The BuyWidget handles the complete payment flow, supporting both crypto and fiat
|
||||
|
||||
## Going Further
|
||||
|
||||
- [Custom Payment Data](/payments/custom-data) - Attach metadata to payments
|
||||
- [Webhooks](/payments/webhooks) - Get notified when payments complete
|
||||
- [Custom Payment Data](/bridge/custom-data) - Attach metadata to payments
|
||||
- [Webhooks](/bridge/webhooks) - Get notified when payments complete
|
||||
|
||||
## API Reference
|
||||
|
||||
@@ -97,7 +97,7 @@ const preparedQuote = await Bridge.Buy.prepare({
|
||||
|
||||
The prepared quote will contain details about the payment, including the transactions needed to execute it.
|
||||
|
||||
You can execute the included transactions using the wallet of your choice. To learn how to send it using thirdweb Wallets, checkout the [Send a Payment](/payments/send) guide.
|
||||
You can execute the included transactions using the wallet of your choice. To learn how to send it using thirdweb Wallets, checkout the [Send a Payment](/bridge/send) guide.
|
||||
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -77,8 +77,8 @@ The CheckoutWidget handles the complete payment flow, supporting both crypto and
|
||||
|
||||
## Going Further
|
||||
|
||||
- [Custom Payment Data](/payments/custom-data) - Attach metadata to payments
|
||||
- [Webhooks](/payments/webhooks) - Get notified when payments complete
|
||||
- [Custom Payment Data](/bridge/custom-data) - Attach metadata to payments
|
||||
- [Webhooks](/bridge/webhooks) - Get notified when payments complete
|
||||
|
||||
## API Reference
|
||||
|
||||
@@ -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)
|
||||
@@ -52,15 +52,14 @@ client,
|
||||
});
|
||||
```
|
||||
|
||||
You can execute this quote exactly like a buy. See the [Send a Payment](/payments/send) guide for more details on this process.
|
||||
You can execute this quote exactly like a buy. See the [Send a Payment](/bridge/send) guide for more details on this process.
|
||||
|
||||
|
||||
## 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 Routes](/payments/routes)
|
||||
- [Send a Payment](/bridge/send)
|
||||
|
||||
## Explore Full API References
|
||||
|
||||
|
||||
@@ -262,9 +262,8 @@ Once this code completes, your payment has been fully executed. Payments normall
|
||||
|
||||
## Going further
|
||||
|
||||
- [Sell a Product](/payments/products)
|
||||
- [Token Prices](/payments/tokens)
|
||||
- [Routes](/payments/routes)
|
||||
- [Sell a Product](/bridge/products)
|
||||
- [Token Prices](/bridge/tokens)
|
||||
|
||||
## Explore API References
|
||||
|
||||
@@ -16,25 +16,45 @@ export const sidebar: SideBar = {
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
href: `${bridgeSlug}/sell`,
|
||||
name: "Sell Tokens",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/swap`,
|
||||
name: "Swap Tokens",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/bridge-widget-script`,
|
||||
name: "BridgeWidget Script",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/fund`,
|
||||
name: "Fund Wallets",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/products`,
|
||||
name: "Sell a Product",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/transactions`,
|
||||
name: "Pay for Transactions",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/send`,
|
||||
name: "Send a Payment",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/sell`,
|
||||
name: "Sell Tokens",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/tokens`,
|
||||
name: "Get Token Prices",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/routes`,
|
||||
name: "Get Routes",
|
||||
href: `${bridgeSlug}/webhooks`,
|
||||
name: "Webhooks",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/bridge-widget-script`,
|
||||
name: "BridgeWidget Script",
|
||||
href: `${bridgeSlug}/custom-data`,
|
||||
name: "Custom Data",
|
||||
},
|
||||
],
|
||||
name: "Guides",
|
||||
|
||||
@@ -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
|
||||
- [Webhooks](/payments/webhooks) - Get notified when swaps complete
|
||||
- [Send a Payment](/bridge/send) - Learn about peer-to-peer transfers
|
||||
- [Token Prices](/bridge/tokens) - Access real-time token pricing
|
||||
- [Webhooks](/bridge/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)
|
||||
@@ -59,7 +59,17 @@ This returns token information with current price:
|
||||
symbol: "USDC",
|
||||
name: "USD Coin",
|
||||
iconUri: "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png",
|
||||
priceUsd: 1.00
|
||||
prices: {
|
||||
USD: 1.00,
|
||||
EUR: 0.86,
|
||||
GBP: 0.74,
|
||||
JPY: 148.85,
|
||||
KRW: 1393.14,
|
||||
CNY: 7.18,
|
||||
INR: 86.20,
|
||||
NOK: 10.18,
|
||||
SEK: 9.68,
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -118,8 +128,7 @@ 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)
|
||||
- [Send a Payment](/bridge/send)
|
||||
|
||||
## Explore Full API References
|
||||
|
||||
|
||||
@@ -86,8 +86,8 @@ The TransactionWidget handles the complete payment flow, supporting both crypto
|
||||
|
||||
## Going Further
|
||||
|
||||
- [Custom Payment Data](/payments/custom-data) - Attach metadata to payments
|
||||
- [Webhooks](/payments/webhooks) - Get notified when payments complete
|
||||
- [Custom Payment Data](/bridge/custom-data) - Attach metadata to payments
|
||||
- [Webhooks](/bridge/webhooks) - Get notified when payments complete
|
||||
|
||||
## API Reference
|
||||
|
||||
@@ -221,9 +221,8 @@ 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)
|
||||
- [Send a Payment](/bridge/send)
|
||||
- [Get Token Prices](/bridge/tokens)
|
||||
|
||||
## Explore Full API References
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { Step, Steps, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Stylus Minting Modules | thirdweb Documentation",
|
||||
description:
|
||||
"Customizable rust modules written for thirdweb's modular contracts",
|
||||
image: {
|
||||
title: "Stylus Minting Modules",
|
||||
icon: "contracts",
|
||||
},
|
||||
});
|
||||
|
||||
# Stylus Minting Modules
|
||||
|
||||
You can now install rust based modules into a solidity based [modular contract](https://github.com/thirdweb-dev/modular-contracts/blob/dev/design-document.md).
|
||||
|
||||
This plug-n-play model also allows users to write their own custom minting logic in rust, taking advantage of Stylus' interoperability and gas savings. These modules work with existing router contracts and modules written in solidity.
|
||||
|
||||
## Deploy through dashboard
|
||||
|
||||
Deploying a Modular Contract and installing modules is easy through the thirdweb dashboard and ideal when you don't want to modify any code on the contract.
|
||||
|
||||
<Steps>
|
||||
<Step title="Select Modular Contract for Stylus">
|
||||
|
||||
Navigate to the Modular Contracts section on Explore and select any token contract for your project.
|
||||
</Step>
|
||||
|
||||
<Step title="Deploy Contract">
|
||||
Select Arbitrum Sepolia or any other Stylus-supported network, then select Deploy. Once you have deployed one of these contracts, the modules come pre-installed, both solidity and rust ones.
|
||||
</Step>
|
||||
|
||||
<Step title="Edit Modules">
|
||||
If you wish to install a custom module, navigate to modules tab on your contract page. Enter the publisher address and select module from the dropdown, and click install.
|
||||
|
||||
You can uninstall a module from the same page.
|
||||
|
||||
Here's a list of prebuilt Rust / Stylus specific modules provided by thirdweb:
|
||||
- [Mintable ERC20](https://thirdweb.com/0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusMintableERC20)
|
||||
- [Transferable ERC20](https://thirdweb.com/0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusTransferableERC20)
|
||||
- [Mintable ERC721](https://thirdweb.com/0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusMintableERC721)
|
||||
- [Transferable ERC721](https://thirdweb.com/0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusTransferableERC721)
|
||||
- [Mintable ERC1155](https://thirdweb.com/0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusMintableERC1155)
|
||||
- [Transferable ERC1155](https://thirdweb.com/0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusTransferableERC1155)
|
||||
|
||||
You can customize these modules via thirdweb CLI as described below.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Build with CLI
|
||||
|
||||
If you want to modify the contract code or publish a custom module, you can use the thirdweb CLI.
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a new Stylus module">
|
||||
|
||||
In your CLI, run the following command to create a new directory with an template module. Select the module template from the list shown.
|
||||
|
||||
```bash
|
||||
npx thirdweb create-stylus
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Modify logic">
|
||||
In the `src/lib.rs` file you can modify the contract logic such as adding fees, gating logic, analytics events, and more.
|
||||
</Step>
|
||||
|
||||
<Step title="Build & Test">
|
||||
|
||||
To build your project, run the following command:
|
||||
|
||||
```bash
|
||||
cargo stylus build
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Deploy or Publish Your Module">
|
||||
You can publish your module and install it to your modular contract. Publishing stores your module metadata in thirdweb’s on-chain registry so anyone (including you) can use that exact version later with a few clicks.
|
||||
|
||||
To publish your module, ensure you have your thirdweb secret key from your created project, then run the following command:
|
||||
|
||||
```bash
|
||||
npx thirdweb publish-stylus -k YOUR_TW_SECRET_KEY
|
||||
```
|
||||
|
||||
Once published, the module is now available to install via thirdweb dashboard from the modules tab of your contract.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Interacting with the Contract
|
||||
|
||||
Using the thirdweb SDKs, you can interact with your Stylus Modular contracts / modules to mint tokens, transfer ownership, and more.
|
||||
|
||||
The following describes how to interact with a ERC721 modular contract using thirdweb SDK. (Swap for ERC20 or ERC1155 variants as needed.)
|
||||
|
||||
### Upload metadata
|
||||
|
||||
```ts
|
||||
import { BatchMetadataERC721 } from "thirdweb/modules";
|
||||
|
||||
const transaction = BatchMetadataERC721.uploadMetadata({
|
||||
contract,
|
||||
metadatas: [
|
||||
{ name: "My NFT", description: "This is my NFT" },
|
||||
],
|
||||
});
|
||||
|
||||
await sendTransaction({
|
||||
transaction,
|
||||
account,
|
||||
});
|
||||
```
|
||||
|
||||
### Mint with role
|
||||
|
||||
```typescript
|
||||
import { MintableERC721 } from "thirdweb/modules";
|
||||
|
||||
const transaction = MintableERC721.mintWithRole({
|
||||
contract,
|
||||
to: "0x...", // Address to mint tokens to
|
||||
nfts: [
|
||||
{
|
||||
name: "My NFT",
|
||||
description: "This is my NFT",
|
||||
image: "ipfs://...",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Send the transaction
|
||||
await sendTransaction({ transaction, account });
|
||||
```
|
||||
|
||||
### Set transfer rules
|
||||
|
||||
```ts
|
||||
import { sendTransaction } from "thirdweb";
|
||||
import { TransferableERC20 } from "thirdweb/modules";
|
||||
|
||||
const transaction = TransferableERC20.setTransferable({
|
||||
contract,
|
||||
enableTransfer: ...,
|
||||
overrides: {
|
||||
...
|
||||
}
|
||||
});
|
||||
|
||||
// Send the transaction
|
||||
await sendTransaction({ transaction, account });
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
- [Modular Contracts Design Doc](https://github.com/thirdweb-dev/modular-contracts/blob/dev/design-document.md)
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import { Step, Steps, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Stylus Zk Mint Contract | thirdweb Documentation",
|
||||
description:
|
||||
"Stylus Zk Minting contract and app template",
|
||||
image: {
|
||||
title: "Stylus Zk Mint",
|
||||
icon: "contracts",
|
||||
},
|
||||
});
|
||||
|
||||
# ZK Minting with Stylus
|
||||
|
||||
You can now create and deploy ZK Proof based token contracts with Stylus. The templates provide you with Stylus contracts, ZK circuit files, and a ready-to-deploy Next.js app + API for proof generation and minting.
|
||||
|
||||
Follow the steps below to learn how to build a privacy-preserving ERC721 token system using Zero-Knowledge proofs on Arbitrum Stylus. Users can mint tokens by proving they own a minimum amount of ETH without revealing their exact balance. However, you can customize the circuits and the contracts to add your own checks for minting.
|
||||
|
||||
## What you'll build
|
||||
|
||||
- ZK Circuit: Proves token ownership without revealing exact balances
|
||||
- Stylus Contract: Rust-based ERC721 contract that verifies ZK proofs
|
||||
- Frontend: Next.js app for generating proofs and minting tokens
|
||||
- Oracle System: Secure balance verification mechanism
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Create a project on your thirdweb account](https://thirdweb.com/dashboard)
|
||||
- Install thirdweb CLI by running `npm install -g thirdweb`
|
||||
- Install Rust tool chain by running `curl https://sh.rustup.rs -sSf | sh` or visit rust-lang.org
|
||||
- Install solc by running `npm install -g solc` or visit soliditylang.org
|
||||
- Install Node version 18 or higher
|
||||
|
||||
<Steps>
|
||||
<Step title="Project Setup">
|
||||
|
||||
In your CLI, run the following command to create a new directory with an template module. Select the module template from the list shown.
|
||||
|
||||
```bash
|
||||
npx thirdweb create-stylus
|
||||
```
|
||||
|
||||
Select **"Stylus ZK ERC721"** from the dropdown menu. This will:
|
||||
- Clone the repository to your machine
|
||||
- Set up the project structure
|
||||
- Install basic dependencies
|
||||
</Step>
|
||||
|
||||
<Step title="Install Dependencies">
|
||||
Install dependencies for all components:
|
||||
|
||||
```bash
|
||||
# Install root dependencies
|
||||
pnpm install
|
||||
|
||||
# Install circuit dependencies
|
||||
cd circuits
|
||||
pnpm install
|
||||
cd ..
|
||||
|
||||
# Install frontend dependencies
|
||||
cd app
|
||||
pnpm install
|
||||
cd ..
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Generate Cryptographic Keys">
|
||||
Run the setup script to generate oracle keys and build the ZK circuit:
|
||||
|
||||
```bash
|
||||
chmod +x setup.sh
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
This script will:
|
||||
- Generate a random oracle secret key
|
||||
- Inject the secret into the ZK circuit
|
||||
- Compile the circuit with circom
|
||||
- Generate proving and verification keys
|
||||
- Create the trusted setup for Groth16
|
||||
|
||||
**⚠️ Important**: The oracle secret is critical for security. Keep it private!
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Deploy the Contract">
|
||||
### Using thirdweb CLI
|
||||
|
||||
|
||||
```bash
|
||||
cd contracts
|
||||
npx thirdweb@latest deploy-stylus -k <THIRDWEB_SECRET_KEY>
|
||||
```
|
||||
|
||||
### Using Stylus CLI (Alternative)
|
||||
|
||||
```bash
|
||||
cd contracts
|
||||
cargo stylus deploy --endpoint arbitrum-sepolia
|
||||
```
|
||||
|
||||
Copy the deployed contract address - you'll need it for the frontend.
|
||||
</Step>
|
||||
|
||||
<Step title="Configure the Frontend">
|
||||
Update the contract address in your frontend:
|
||||
|
||||
```bash
|
||||
cd app
|
||||
# Edit pages/index.tsx or lib/config.ts
|
||||
# Update ZK_MINT_CONTRACT_ADDRESS with your deployed address
|
||||
```
|
||||
|
||||
Create environment file:
|
||||
|
||||
```bash
|
||||
# app/.env.local
|
||||
RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
|
||||
ORACLE_SECRET_KEY=<your_oracle_secret_from_setup>
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Run the Application">
|
||||
```bash
|
||||
cd app
|
||||
pnpm dev
|
||||
# Visit http://localhost:3000
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Test the System">
|
||||
1. **Connect Wallet**: Connect to Arbitrum Sepolia testnet
|
||||
2. **Generate Proof**: Click "Generate ZK Proof" - this proves you have sufficient balance
|
||||
3. **Mint Tokens**: Use the proof to mint ERC721 tokens
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Customizing the ZK Logic
|
||||
|
||||
The above described logic for ZK verification of user balance is provided as a template. You can modify the logic and write your own custom ZK verification code and circuits as described below.
|
||||
|
||||
### Understanding the Circuit
|
||||
|
||||
The core circuit (`circuits/token_ownership.circom`) has these components:
|
||||
|
||||
```txt
|
||||
template TokenOwnership(oracle_secret) {
|
||||
// Private inputs (hidden from public)
|
||||
signal input actual_balance; // Real balance from oracle
|
||||
signal input salt; // Randomness for uniqueness
|
||||
|
||||
// Public inputs (visible on-chain)
|
||||
signal input min_required_balance; // Minimum threshold
|
||||
signal input token_contract_hash; // Which token to check
|
||||
signal input user_address_hash; // User's address hash
|
||||
signal input timestamp; // When oracle signed data
|
||||
signal input oracle_commitment; // Oracle's commitment
|
||||
|
||||
// Output
|
||||
signal output nullifier; // Prevents replay attacks
|
||||
}
|
||||
```
|
||||
|
||||
### Customization Examples
|
||||
|
||||
#### 1. Change Balance Threshold Logic
|
||||
|
||||
Replace the balance check with custom logic:
|
||||
|
||||
```txt
|
||||
// Original: Simple greater-than check
|
||||
component gte = GreaterEqThan(64);
|
||||
gte.in[0] <== actual_balance;
|
||||
gte.in[1] <== min_required_balance;
|
||||
gte.out === 1;
|
||||
|
||||
// Custom: Logarithmic scaling
|
||||
component log_check = LogarithmicCheck();
|
||||
log_check.balance <== actual_balance;
|
||||
log_check.threshold <== min_required_balance;
|
||||
log_check.out === 1;
|
||||
```
|
||||
|
||||
#### 2. Add Multiple Token Support
|
||||
|
||||
Extend the circuit to verify multiple token balances:
|
||||
|
||||
```txt
|
||||
template MultiTokenOwnership(oracle_secret, num_tokens) {
|
||||
signal input actual_balances[num_tokens];
|
||||
signal input min_required_balances[num_tokens];
|
||||
signal input token_addresses[num_tokens];
|
||||
|
||||
// Verify each token separately
|
||||
component checks[num_tokens];
|
||||
for (var i = 0; i < num_tokens; i++) {
|
||||
checks[i] = GreaterEqThan(64);
|
||||
checks[i].in[0] <== actual_balances[i];
|
||||
checks[i].in[1] <== min_required_balances[i];
|
||||
checks[i].out === 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Add Time-Based Constraints
|
||||
|
||||
Add expiration logic to proofs:
|
||||
|
||||
```txt
|
||||
// Add time validation
|
||||
component time_check = LessThan(64);
|
||||
time_check.in[0] <== timestamp;
|
||||
time_check.in[1] <== max_timestamp; // New public input
|
||||
time_check.out === 1;
|
||||
```
|
||||
|
||||
### Rebuilding After Changes
|
||||
|
||||
After modifying the circuit:
|
||||
|
||||
```bash
|
||||
cd circuits
|
||||
pnpm run build
|
||||
|
||||
# Re-inject verification keys
|
||||
cd ..
|
||||
node scripts/inject-vk.js
|
||||
|
||||
# Recompile contract
|
||||
cd contracts
|
||||
cargo build
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
- [thirdweb Arbitrum Documentation](https://portal.thirdweb.com/contracts/arbitrum-stylus/stylus-contract)
|
||||
- [Arbitrum Stylus Documentation](https://docs.arbitrum.io/stylus/concepts/how-it-works)
|
||||
|
||||
@@ -56,6 +56,14 @@ export const sidebar: SideBar = {
|
||||
href: `${slug}/arbitrum-stylus/airdrop-contract`,
|
||||
name: "Stylus Airdrop Contract",
|
||||
},
|
||||
{
|
||||
href: `${slug}/arbitrum-stylus/zk-mint`,
|
||||
name: "Stylus Zk Mint",
|
||||
},
|
||||
{
|
||||
href: `${slug}/arbitrum-stylus/minting-modules`,
|
||||
name: "Stylus Minting Modules",
|
||||
},
|
||||
],
|
||||
name: "Arbitrum Stylus",
|
||||
},
|
||||
|
||||
@@ -41,14 +41,14 @@ export default function Page() {
|
||||
|
||||
function Hero() {
|
||||
return (
|
||||
<section className="grid gap-4 py-14 lg:grid-cols-[1fr_420px] lg:py-0">
|
||||
<section className="grid gap-4 pt-14 pb-6 lg:grid-cols-[1fr_420px] lg:py-0">
|
||||
{/* Left */}
|
||||
<div className="flex flex-col justify-center">
|
||||
<div>
|
||||
<h1 className="mb-3 font-bold text-4xl tracking-tighter lg:text-6xl">
|
||||
thirdweb Documentation
|
||||
</h1>
|
||||
<p className="mb-8 max-w-lg text-lg text-muted-foreground leading-normal text-pretty">
|
||||
<p className="mb-6 max-w-lg text-base 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">
|
||||
@@ -178,10 +178,10 @@ function LearningResourcesSection() {
|
||||
title="Wallets"
|
||||
/>
|
||||
<ArticleCardIndex
|
||||
description="Create onchain payments and sell products"
|
||||
href="/payments"
|
||||
icon={PaymentsIcon}
|
||||
title="Payments"
|
||||
description="Instant payments for APIs, websites, and agents"
|
||||
href="/x402"
|
||||
icon={ZapIcon}
|
||||
title="x402"
|
||||
/>
|
||||
<ArticleCardIndex
|
||||
description="Bridge, swap, and onramp tokens on any chain"
|
||||
|
||||
@@ -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",
|
||||
@@ -65,33 +61,6 @@ export const sidebar: SideBar = {
|
||||
name: "Guides",
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
href: `${paymentsSlug}/x402`,
|
||||
name: "Get Started",
|
||||
},
|
||||
{
|
||||
href: `${paymentsSlug}/x402/client`,
|
||||
name: "Client Side",
|
||||
},
|
||||
{
|
||||
href: `${paymentsSlug}/x402/server`,
|
||||
name: "Server Side",
|
||||
},
|
||||
{
|
||||
href: `${paymentsSlug}/x402/agents`,
|
||||
name: "Agents",
|
||||
},
|
||||
{
|
||||
href: `${paymentsSlug}/x402/facilitator`,
|
||||
name: "Facilitator",
|
||||
},
|
||||
],
|
||||
name: "x402",
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ The client library wraps the native `fetch` API and handles:
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="http">
|
||||
## Prepare Payment
|
||||
## Fetch with Payment
|
||||
|
||||
Fetch any x402-compatible API and automatically handle payment flows when APIs return a `402 Payment Required` response using the authenticated wallet.
|
||||
|
||||
@@ -27,7 +27,9 @@ You can view all transactions processed by your facilitator in your project dash
|
||||
|
||||
## Chain and token support
|
||||
|
||||
Our facilitator supports payments on any EVM chain, as long as the payment token supports either:
|
||||
The thirdweb x402 client/server stack supports payments on [170+ EVM chains](https://thirdweb.com/chainlist?service=account-abstraction).
|
||||
|
||||
The payment token must support either:
|
||||
|
||||
- ERC-2612 permit (most ERC20 tokens)
|
||||
- ERC-3009 sign with authorization (USDC on all chains)
|
||||
@@ -0,0 +1,21 @@
|
||||
import { createMetadata } from "@doc";
|
||||
import { DocLayout } from "@/components/Layouts/DocLayout";
|
||||
import { sidebar } from "./sidebar";
|
||||
|
||||
export default async function Layout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<DocLayout editPageButton={true} sideBar={sidebar}>
|
||||
{props.children}
|
||||
</DocLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description:
|
||||
"Instant payments for your APIs, websites, and autonomous agents. x402 is an open-source protocol that turns the HTTP 402 status code into a fully-featured, on-chain payment layer.",
|
||||
image: {
|
||||
icon: "payments",
|
||||
title: "x402 Payments Docs",
|
||||
},
|
||||
title: "x402 Payments Documentation",
|
||||
});
|
||||
@@ -12,9 +12,8 @@ export const metadata = createMetadata({
|
||||
|
||||
# x402 Payments
|
||||
|
||||
x402 is an open-source protocol that turns the dormant HTTP 402 Payment Required status code into a fully-featured, on-chain payment layer for APIs, websites, and autonomous agents.
|
||||
[x402](https://x402.org) is an open-source protocol that turns the dormant HTTP 402 Payment Required status code into a fully-featured, on-chain payment layer for APIs, websites, and autonomous agents.
|
||||
|
||||
Learn more about the protocol at [x402.org](https://x402.org).
|
||||
|
||||
<ArticleIconCard
|
||||
title="x402 Playground"
|
||||
@@ -23,6 +22,15 @@ Learn more about the protocol at [x402.org](https://x402.org).
|
||||
href="https://playground.thirdweb.com/payments/x402"
|
||||
/>
|
||||
|
||||
## Chain and token support
|
||||
|
||||
The thirdweb x402 client/server stack supports payments on [170+ EVM chains](https://thirdweb.com/chainlist?service=account-abstraction).
|
||||
|
||||
The payment token must support either:
|
||||
|
||||
- ERC-2612 permit (most ERC20 tokens)
|
||||
- ERC-3009 sign with authorization (USDC on all chains)
|
||||
|
||||
## Client Side
|
||||
|
||||
`wrapFetchWithPayment` wraps the native fetch API to automatically handle `402 Payment Required` responses from any x402-compatible API:
|
||||
@@ -42,7 +50,7 @@ const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
|
||||
const response = await fetchWithPay('https://api.example.com/paid-endpoint');
|
||||
```
|
||||
|
||||
You can also use the thirdweb API to fetch any x402 compatible endpoint and pay for it with the authenticated wallet. See the [client side docs](/payments/x402/client) for more details.
|
||||
You can also use the thirdweb API to fetch any x402 compatible endpoint and pay for it with the authenticated wallet. See the [client side docs](/x402/client) for more details.
|
||||
|
||||
## Server Side
|
||||
|
||||
@@ -88,7 +96,7 @@ export async function GET(request: Request) {
|
||||
}
|
||||
```
|
||||
|
||||
You can also create middlewares to handle payment for multiple endpoints, see the [server side docs](/payments/x402/server) for more details. The [facilitator](/payments/x402/facilitator) handles settling the payment onchain using your own server wallet.
|
||||
You can also create middlewares to handle payment for multiple endpoints, see the [server side docs](/x402/server) for more details. The [facilitator](/x402/facilitator) handles settling the payment onchain using your own server wallet.
|
||||
|
||||
## Going Further
|
||||
|
||||
@@ -97,18 +105,18 @@ You can also create middlewares to handle payment for multiple endpoints, see th
|
||||
title="Client Side"
|
||||
description="Learn how to interact with x402-compatible APIs from your client applications"
|
||||
icon={TypeScriptIcon}
|
||||
href="/payments/x402/client"
|
||||
href="/x402/client"
|
||||
/>
|
||||
<ArticleIconCard
|
||||
title="Server Side"
|
||||
description="Accept x402 payments in your APIs with middleware or dedicated endpoints"
|
||||
icon={EngineIcon}
|
||||
href="/payments/x402/server"
|
||||
href="/x402/server"
|
||||
/>
|
||||
<ArticleIconCard
|
||||
title="Facilitator API"
|
||||
description="Configure the facilitator service to verify and settle payments"
|
||||
icon={EngineIcon}
|
||||
href="/payments/x402/facilitator"
|
||||
href="/x402/facilitator"
|
||||
/>
|
||||
</div>
|
||||
@@ -421,23 +421,31 @@ Protect multiple endpoints with a shared middleware:
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
## Price Configuration
|
||||
## Price and Token Configuration
|
||||
|
||||
You can specify prices in multiple ways:
|
||||
|
||||
### USD String
|
||||
|
||||
This will default to using USDC on the specified network.
|
||||
|
||||
```typescript
|
||||
price: "$0.10" // 10 cents in USD
|
||||
network: polygon, // or any other EVM chain
|
||||
price: "$0.10" // 10 cents in USDC
|
||||
```
|
||||
|
||||
### ERC20 Token
|
||||
|
||||
You can use any ERC20 token that supports the ERC-2612 permit or ERC-3009 sign with authorization.
|
||||
|
||||
Simply pass the amount in base units and the token address.
|
||||
|
||||
```typescript
|
||||
network: arbitrum,
|
||||
price: {
|
||||
amount: "100000", // Amount in base units (100000 = 0.1 USDC with 6 decimals)
|
||||
amount: "1000000000000000", // Amount in base units (0.001 tokens with 18 decimals)
|
||||
asset: {
|
||||
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Token address
|
||||
address: "0xf01E52B0BAC3E147f6CAf956a64586865A0aA928", // Token address
|
||||
}
|
||||
}
|
||||
```
|
||||
|
Before Width: | Height: | Size: 376 KiB After Width: | Height: | Size: 376 KiB |
@@ -0,0 +1,77 @@
|
||||
import { ExternalLinkIcon, ZapIcon } from "lucide-react";
|
||||
import type { SideBar } from "@/components/Layouts/DocLayout";
|
||||
import { EngineIcon, ReactIcon, TypeScriptIcon, UnityIcon } from "@/icons";
|
||||
import { UnrealEngineIcon } from "../../icons/sdks/UnrealEngineIcon";
|
||||
|
||||
const x402Slug = "/x402";
|
||||
|
||||
export const sidebar: SideBar = {
|
||||
links: [
|
||||
{
|
||||
href: x402Slug,
|
||||
name: "Get Started",
|
||||
icon: <ZapIcon />,
|
||||
},
|
||||
{
|
||||
href: "https://playground.thirdweb.com/payments/x402",
|
||||
icon: <ExternalLinkIcon />,
|
||||
name: "Playground",
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
href: `${x402Slug}/client`,
|
||||
name: "Client Side",
|
||||
},
|
||||
{
|
||||
href: `${x402Slug}/server`,
|
||||
name: "Server Side",
|
||||
},
|
||||
{
|
||||
href: `${x402Slug}/agents`,
|
||||
name: "Agents",
|
||||
},
|
||||
{
|
||||
href: `${x402Slug}/facilitator`,
|
||||
name: "Facilitator",
|
||||
},
|
||||
],
|
||||
name: "Guides",
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
href: `https://payments.thirdweb.com/reference`,
|
||||
icon: <EngineIcon />,
|
||||
name: "REST API",
|
||||
},
|
||||
{
|
||||
href: "/references/typescript/v5",
|
||||
icon: <TypeScriptIcon />,
|
||||
name: "TypeScript",
|
||||
},
|
||||
{
|
||||
href: "/references/typescript/v5",
|
||||
icon: <ReactIcon />,
|
||||
name: "React",
|
||||
},
|
||||
{
|
||||
href: "/unity",
|
||||
icon: <UnityIcon />,
|
||||
name: "Unity",
|
||||
},
|
||||
{
|
||||
href: "/unreal-engine",
|
||||
icon: <UnrealEngineIcon />,
|
||||
name: "Unreal Engine",
|
||||
},
|
||||
],
|
||||
name: "API References",
|
||||
},
|
||||
],
|
||||
name: "x402",
|
||||
};
|
||||
@@ -15,6 +15,7 @@ const authOptions: InAppWalletAuth[] = [
|
||||
"facebook",
|
||||
"x",
|
||||
"tiktok",
|
||||
"epic",
|
||||
"discord",
|
||||
"telegram",
|
||||
"twitch",
|
||||
|
||||
@@ -29,6 +29,9 @@ type AuthMethod =
|
||||
| "telegram"
|
||||
| "farcaster"
|
||||
| "github"
|
||||
| "line"
|
||||
| "tiktok"
|
||||
| "epic"
|
||||
| "passkey"
|
||||
| "wallet"
|
||||
| "guest"
|
||||
@@ -55,6 +58,9 @@ const authMethods: { id: AuthMethod; label: string; description: string }[] = [
|
||||
{ id: "telegram", label: "Telegram", description: "Telegram OAuth" },
|
||||
{ id: "farcaster", label: "Farcaster", description: "Farcaster OAuth" },
|
||||
{ id: "github", label: "GitHub", description: "GitHub OAuth" },
|
||||
{ id: "line", label: "Line", description: "Line OAuth" },
|
||||
{ id: "tiktok", label: "TikTok", description: "TikTok OAuth" },
|
||||
{ id: "epic", label: "Epic Games", description: "Epic Games OAuth" },
|
||||
{ id: "passkey", label: "Passkey", description: "WebAuthn passkey" },
|
||||
{
|
||||
id: "wallet",
|
||||
@@ -244,6 +250,9 @@ const account = await wallet.connect({
|
||||
case "telegram":
|
||||
case "farcaster":
|
||||
case "github":
|
||||
case "line":
|
||||
case "tiktok":
|
||||
case "epic":
|
||||
return (
|
||||
baseSetup +
|
||||
`// ${authMethod.charAt(0).toUpperCase() + authMethod.slice(1)} OAuth
|
||||
@@ -561,6 +570,9 @@ function PrebuiltUIExample() {
|
||||
case "telegram":
|
||||
case "farcaster":
|
||||
case "github":
|
||||
case "line":
|
||||
case "tiktok":
|
||||
case "epic":
|
||||
return (
|
||||
baseSetup +
|
||||
` await wallet.connect({\n client,\n strategy: "${authMethod}",\n });\n` +
|
||||
@@ -639,7 +651,10 @@ var address = await wallet.LoginWithOtp("123456");`
|
||||
case "x":
|
||||
case "telegram":
|
||||
case "farcaster":
|
||||
case "github": {
|
||||
case "github":
|
||||
case "line":
|
||||
case "tiktok":
|
||||
case "epic": {
|
||||
const providerMap: Record<string, string> = {
|
||||
google: "Google",
|
||||
apple: "Apple",
|
||||
@@ -649,6 +664,9 @@ var address = await wallet.LoginWithOtp("123456");`
|
||||
telegram: "Telegram",
|
||||
farcaster: "Farcaster",
|
||||
github: "Github",
|
||||
line: "Line",
|
||||
tiktok: "TikTok",
|
||||
epic: "Epic",
|
||||
};
|
||||
return (
|
||||
baseSetup +
|
||||
@@ -746,7 +764,10 @@ var wallet = await ThirdwebManager.Instance.ConnectWallet(options);`
|
||||
case "x":
|
||||
case "telegram":
|
||||
case "farcaster":
|
||||
case "github": {
|
||||
case "github":
|
||||
case "line":
|
||||
case "tiktok":
|
||||
case "epic": {
|
||||
const providerMap: Record<string, string> = {
|
||||
google: "Google",
|
||||
apple: "Apple",
|
||||
@@ -756,6 +777,9 @@ var wallet = await ThirdwebManager.Instance.ConnectWallet(options);`
|
||||
telegram: "Telegram",
|
||||
farcaster: "Farcaster",
|
||||
github: "Github",
|
||||
line: "Line",
|
||||
tiktok: "TikTok",
|
||||
epic: "Epic",
|
||||
};
|
||||
return (
|
||||
baseSetup +
|
||||
|
||||
@@ -1,5 +1,133 @@
|
||||
# wagmi-inapp
|
||||
|
||||
## 0.0.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`f964b8a`](https://github.com/thirdweb-dev/js/commit/f964b8aaff22d4078ff4c4c5e09c6ebb5fcb182c), [`424e6b8`](https://github.com/thirdweb-dev/js/commit/424e6b8f9b071d3629ff0291ae6875fa4e4c882e), [`763d3de`](https://github.com/thirdweb-dev/js/commit/763d3de5a8db19a8022b2eda420f13a6787de100)]:
|
||||
- thirdweb@5.111.10
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.185
|
||||
|
||||
## 0.0.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`673e51b`](https://github.com/thirdweb-dev/js/commit/673e51b11cf2d06f00ab92bd4cbb45c51d95ec1f)]:
|
||||
- thirdweb@5.111.9
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.184
|
||||
|
||||
## 0.0.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1f440d0`](https://github.com/thirdweb-dev/js/commit/1f440d0c97adf14d26d55666257b57541b2ef01a)]:
|
||||
- thirdweb@5.111.8
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.183
|
||||
|
||||
## 0.0.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7f08161`](https://github.com/thirdweb-dev/js/commit/7f081618e16e7d5754a09780421e35b244186ce8), [`7f08161`](https://github.com/thirdweb-dev/js/commit/7f081618e16e7d5754a09780421e35b244186ce8)]:
|
||||
- thirdweb@5.111.7
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.182
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wagmi-inapp",
|
||||
"private": true,
|
||||
"version": "0.0.14",
|
||||
"version": "0.0.30",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/api",
|
||||
"version": "0.1.1",
|
||||
"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,
|
||||
@@ -1590,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.
|
||||
|
||||
@@ -2135,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
|
||||
*/
|
||||
@@ -2171,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";
|
||||
};
|
||||
@@ -3234,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
|
||||
*/
|
||||
@@ -4314,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
|
||||
*/
|
||||
@@ -4725,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
|
||||
*/
|
||||
@@ -4914,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;
|
||||
@@ -4943,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;
|
||||
@@ -5015,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"
|
||||
@@ -5049,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;
|
||||
@@ -5078,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;
|
||||
@@ -5154,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"
|
||||
@@ -5183,7 +5141,10 @@ export type SettleX402PaymentResponses = {
|
||||
| "solana-devnet"
|
||||
| "solana"
|
||||
| "sei"
|
||||
| "sei-testnet";
|
||||
| "sei-testnet"
|
||||
| "polygon"
|
||||
| "polygon-amoy"
|
||||
| "peaq";
|
||||
errorMessage?: string;
|
||||
};
|
||||
};
|
||||
@@ -5226,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?: {
|
||||
/**
|
||||
@@ -5340,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
|
||||
*/
|
||||
@@ -5552,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;
|
||||
@@ -6298,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
|
||||
*/
|
||||
@@ -6640,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,117 @@
|
||||
# @thirdweb-dev/nebula
|
||||
|
||||
## 0.2.88
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`f964b8a`](https://github.com/thirdweb-dev/js/commit/f964b8aaff22d4078ff4c4c5e09c6ebb5fcb182c), [`424e6b8`](https://github.com/thirdweb-dev/js/commit/424e6b8f9b071d3629ff0291ae6875fa4e4c882e), [`763d3de`](https://github.com/thirdweb-dev/js/commit/763d3de5a8db19a8022b2eda420f13a6787de100)]:
|
||||
- thirdweb@5.111.10
|
||||
|
||||
## 0.2.87
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`673e51b`](https://github.com/thirdweb-dev/js/commit/673e51b11cf2d06f00ab92bd4cbb45c51d95ec1f)]:
|
||||
- thirdweb@5.111.9
|
||||
|
||||
## 0.2.86
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1f440d0`](https://github.com/thirdweb-dev/js/commit/1f440d0c97adf14d26d55666257b57541b2ef01a)]:
|
||||
- thirdweb@5.111.8
|
||||
|
||||
## 0.2.85
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7f08161`](https://github.com/thirdweb-dev/js/commit/7f081618e16e7d5754a09780421e35b244186ce8), [`7f08161`](https://github.com/thirdweb-dev/js/commit/7f081618e16e7d5754a09780421e35b244186ce8)]:
|
||||
- thirdweb@5.111.7
|
||||
|
||||
## 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
|
||||
|
||||
@@ -57,5 +57,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.72"
|
||||
"version": "0.2.88"
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"entry": ["src/exports/**"],
|
||||
"ignore": ["src/**/__generated__/**", "**/*.bench.ts"],
|
||||
"ignoreBinaries": ["printf"],
|
||||
"ignoreDependencies": ["tslib"],
|
||||
"project": ["src/**/*.{ts,tsx}"],
|
||||
"rules": {
|
||||
"enumMembers": "off",
|
||||
"optionalPeerDependencies": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"author": "thirdweb eng <[email protected]>",
|
||||
"browser": {
|
||||
"crypto": false
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/thirdweb-dev/js/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.6",
|
||||
"@size-limit/preset-small-lib": "11.2.0",
|
||||
"knip": "5.60.2",
|
||||
"rimraf": "6.0.1",
|
||||
"size-limit": "11.2.0",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/types/exports/nexus-fetch.d.ts",
|
||||
"import": "./dist/esm/exports/nexus-fetch.js",
|
||||
"default": "./dist/cjs/exports/nexus-fetch.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist/*",
|
||||
"src/*",
|
||||
"!**/*.tsbuildinfo",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.test.tsx",
|
||||
"!**/*.test.ts.snap",
|
||||
"!**/*.test-d.ts",
|
||||
"!**/*.bench.ts",
|
||||
"!tsconfig.build.json"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "./dist/cjs/exports/nexus-fetch.js",
|
||||
"module": "./dist/esm/exports/nexus-fetch.js",
|
||||
"name": "@thirdweb-dev/fetch",
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm clean && pnpm build:types && pnpm build:cjs && pnpm build:esm",
|
||||
"build:cjs": "tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false && printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json",
|
||||
"build:esm": "tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm && printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json",
|
||||
"build:types": "tsc --project ./tsconfig.build.json --module nodenext --moduleResolution nodenext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap",
|
||||
"clean": "rimraf dist",
|
||||
"dev": "tsc --project ./tsconfig.build.json --module nodenext --moduleResolution nodenext --outDir ./dist/esm --watch",
|
||||
"dev:cjs": "printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json && tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false --watch",
|
||||
"dev:esm": "printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json && tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm --watch",
|
||||
"fix": "biome check ./src --fix",
|
||||
"format": "biome format ./src --write",
|
||||
"knip": "knip",
|
||||
"lint": "knip && biome check ./src && tsc --project ./tsconfig.build.json --module nodenext --moduleResolution nodenext --noEmit",
|
||||
"size": "size-limit",
|
||||
"typecheck": "tsc --project ./tsconfig.build.json --module nodenext --moduleResolution nodenext --noEmit",
|
||||
"test:ui": "bun ./testing/test.html --watch"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/nexus-fetch.d.ts",
|
||||
"typings": "./dist/types/exports/nexus-fetch.d.ts",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { wrap } from "../fetch.js";
|
||||
@@ -0,0 +1,459 @@
|
||||
//---
|
||||
// Constants
|
||||
//---
|
||||
const TOKEN_DATA_KEY = "tw:nexus:td";
|
||||
const STATE_KEY = "tw:nexus:state";
|
||||
const VERIFIER_KEY = "tw:nexus:verifier";
|
||||
const NEXUS_API_BASE_URL = "https://nexus-api.thirdweb-dev.com";
|
||||
const OAUTH_CLIENT_ID = "9ad56e84-5abe-4ea5-a6c2-9834b9d31245";
|
||||
// Token expiry buffer: consider token expired 5 minutes before actual expiry
|
||||
const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
||||
|
||||
//---
|
||||
// Types
|
||||
//---
|
||||
type TokenData = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
type OAuthTokenResponse = {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
token_type: "Bearer";
|
||||
};
|
||||
|
||||
//---
|
||||
// Token Encoding/Decoding
|
||||
//---
|
||||
function encodeTokenData(data: TokenData): string {
|
||||
return btoa(JSON.stringify(data));
|
||||
}
|
||||
|
||||
function decodeTokenData(encoded: string): TokenData | null {
|
||||
try {
|
||||
const decoded = atob(encoded);
|
||||
return JSON.parse(decoded) as TokenData;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//---
|
||||
// Initialization
|
||||
// Automatically wrap global fetch and complete OAuth flow if returning from authorization
|
||||
//---
|
||||
(() => {
|
||||
if (hasWindow()) {
|
||||
completeOauthFlow();
|
||||
const originalFetch = globalThis.fetch;
|
||||
try {
|
||||
globalThis.fetch = wrap(originalFetch);
|
||||
} catch (error) {
|
||||
console.error("[thirdweb nexus] Error wrapping fetch", error);
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
//---
|
||||
// Main Fetch Wrapper
|
||||
//---
|
||||
/**
|
||||
* Wraps a fetch function to automatically handle HTTP 402 (Payment Required) responses.
|
||||
* When a 402 is encountered, it uses the Nexus API to process the payment and retry the request.
|
||||
*
|
||||
* @param fetchFn - The fetch function to wrap (typically globalThis.fetch)
|
||||
* @param options - Optional configuration
|
||||
* @param options.maxValue - Maximum payment value allowed in wei
|
||||
* @returns A wrapped fetch function that handles 402 responses
|
||||
*/
|
||||
export function wrap(
|
||||
fetchFn: typeof globalThis.fetch,
|
||||
options?: {
|
||||
maxValue?: bigint;
|
||||
},
|
||||
) {
|
||||
return async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const response = await fetchFn(input, init);
|
||||
|
||||
if (response.status !== 402) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const AT = await getAuthToken();
|
||||
if (!AT) {
|
||||
// open login page in a new tab or throw an error if opening fails
|
||||
startOauthFlow();
|
||||
return new Response("Payment required", { status: 402 });
|
||||
}
|
||||
|
||||
// create the proxy request
|
||||
const proxyUrl = new URL("/fetch", NEXUS_API_BASE_URL);
|
||||
proxyUrl.searchParams.set(
|
||||
"data",
|
||||
btoa(
|
||||
JSON.stringify({
|
||||
// original request related information
|
||||
request: {
|
||||
url: new URL(
|
||||
typeof input === "string"
|
||||
? input
|
||||
: input instanceof Request
|
||||
? input.url
|
||||
: input,
|
||||
).toString(),
|
||||
method:
|
||||
(typeof input === "string"
|
||||
? init?.method
|
||||
: input instanceof Request
|
||||
? input.method
|
||||
: "GET") ?? "GET",
|
||||
headers:
|
||||
typeof input === "string"
|
||||
? init?.headers
|
||||
: input instanceof Request
|
||||
? input.headers
|
||||
: {},
|
||||
},
|
||||
// x402 options
|
||||
x402Options: {
|
||||
...(options?.maxValue
|
||||
? { maxValue: options.maxValue.toString() }
|
||||
: {}),
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
let proxyRes = await fetchFn(proxyUrl, {
|
||||
method: "POST",
|
||||
body:
|
||||
(typeof input === "string"
|
||||
? init?.body
|
||||
: input instanceof Request
|
||||
? input.body
|
||||
: init?.body) ?? undefined,
|
||||
headers: {
|
||||
Authorization: `Bearer ${AT}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Handle 401 (unauthorized) - try to refresh token and retry
|
||||
if (proxyRes.status === 401) {
|
||||
const refreshedToken = await refreshAccessToken();
|
||||
if (refreshedToken) {
|
||||
// Retry with refreshed token
|
||||
proxyRes = await fetchFn(proxyUrl, {
|
||||
method: "POST",
|
||||
body:
|
||||
(typeof input === "string"
|
||||
? init?.body
|
||||
: input instanceof Request
|
||||
? input.body
|
||||
: init?.body) ?? undefined,
|
||||
headers: {
|
||||
Authorization: `Bearer ${refreshedToken}`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Refresh failed, start OAuth flow
|
||||
startOauthFlow();
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
if (proxyRes.status === 402) {
|
||||
// means the wallet doesn't have funds and needs to onramp, open the funding link
|
||||
const data = (await proxyRes.json()) as {
|
||||
result: { link: string; message: string };
|
||||
};
|
||||
// TODO: we can await this and then retry the call after by setting up
|
||||
// window.postMessage communication and opening this in a new tab
|
||||
const paymentUrl = new URL(data.result.link);
|
||||
paymentUrl.searchParams.set("successUrl", window.location.href);
|
||||
window.location.href = paymentUrl.toString();
|
||||
return new Response("Payment required", { status: 402 });
|
||||
}
|
||||
return proxyRes;
|
||||
};
|
||||
}
|
||||
|
||||
//---
|
||||
// Browser/Environment Utilities
|
||||
//---
|
||||
function hasWindow(): boolean {
|
||||
return typeof window !== "undefined";
|
||||
}
|
||||
|
||||
function getLocalStorage(): Storage | null {
|
||||
try {
|
||||
return hasWindow() ? window.localStorage : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//---
|
||||
// Token Data Management
|
||||
//---
|
||||
let inMemoryTokenData: TokenData | null = null;
|
||||
|
||||
function getTokenData(): TokenData | null {
|
||||
if (inMemoryTokenData !== null) {
|
||||
return inMemoryTokenData;
|
||||
}
|
||||
const encoded = getLocalStorage()?.getItem(TOKEN_DATA_KEY);
|
||||
if (encoded) {
|
||||
const data = decodeTokenData(encoded);
|
||||
if (data) {
|
||||
inMemoryTokenData = data;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setTokenData(
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
expiresIn: number,
|
||||
): void {
|
||||
if (!accessToken || accessToken === "undefined") {
|
||||
return;
|
||||
}
|
||||
if (!refreshToken || refreshToken === "undefined") {
|
||||
return;
|
||||
}
|
||||
// Calculate expiry timestamp (current time + expires_in seconds)
|
||||
const expiresAt = Date.now() + expiresIn * 1000;
|
||||
const tokenData: TokenData = { accessToken, refreshToken, expiresAt };
|
||||
const encoded = encodeTokenData(tokenData);
|
||||
getLocalStorage()?.setItem(TOKEN_DATA_KEY, encoded);
|
||||
inMemoryTokenData = tokenData;
|
||||
}
|
||||
|
||||
function deleteTokenData(): void {
|
||||
getLocalStorage()?.removeItem(TOKEN_DATA_KEY);
|
||||
inMemoryTokenData = null;
|
||||
}
|
||||
|
||||
function isTokenExpired(tokenData: TokenData | null): boolean {
|
||||
if (!tokenData) {
|
||||
return true;
|
||||
}
|
||||
return Date.now() >= tokenData.expiresAt - TOKEN_EXPIRY_BUFFER_MS;
|
||||
}
|
||||
|
||||
//---
|
||||
// Token Validation & Refresh
|
||||
//---
|
||||
/**
|
||||
* Gets a valid access token, refreshing it if necessary.
|
||||
* @returns A valid access token or null if unavailable
|
||||
*/
|
||||
async function getAuthToken(): Promise<string | null> {
|
||||
const tokenData = getTokenData();
|
||||
|
||||
// If we have a token but it's expired, try to refresh
|
||||
if (tokenData && isTokenExpired(tokenData)) {
|
||||
const refreshedToken = await refreshAccessToken();
|
||||
return refreshedToken;
|
||||
}
|
||||
|
||||
return tokenData?.accessToken ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes an expired access token using the refresh token.
|
||||
* @returns A new access token or null if refresh fails
|
||||
*/
|
||||
async function refreshAccessToken(): Promise<string | null> {
|
||||
const tokenData = getTokenData();
|
||||
if (!tokenData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${NEXUS_API_BASE_URL}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: tokenData.refreshToken,
|
||||
client_id: OAUTH_CLIENT_ID,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Refresh failed, clear all tokens
|
||||
deleteTokenData();
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as OAuthTokenResponse;
|
||||
|
||||
// Store new tokens
|
||||
setTokenData(data.access_token, data.refresh_token, data.expires_in);
|
||||
|
||||
return data.access_token;
|
||||
} catch (error) {
|
||||
console.error("[thirdweb nexus] Error refreshing access token", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//---
|
||||
// OAuth PKCE (Proof Key for Code Exchange) Utilities
|
||||
//---
|
||||
function storeState(code: string): void {
|
||||
getLocalStorage()?.setItem(STATE_KEY, code);
|
||||
}
|
||||
|
||||
function getState(): string | null | undefined {
|
||||
return getLocalStorage()?.getItem(STATE_KEY);
|
||||
}
|
||||
|
||||
function deleteState(): void {
|
||||
getLocalStorage()?.removeItem(STATE_KEY);
|
||||
}
|
||||
|
||||
function storeVerifier(verifier: string): void {
|
||||
getLocalStorage()?.setItem(VERIFIER_KEY, verifier);
|
||||
}
|
||||
|
||||
function getVerifier(): string | null | undefined {
|
||||
return getLocalStorage()?.getItem(VERIFIER_KEY);
|
||||
}
|
||||
|
||||
function deleteVerifier(): void {
|
||||
getLocalStorage()?.removeItem(VERIFIER_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cryptographically random code verifier for PKCE.
|
||||
*/
|
||||
function generateCodeVerifier(): string {
|
||||
const array = new Uint8Array(32);
|
||||
crypto.getRandomValues(array);
|
||||
return base64UrlEncode(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a code challenge from a verifier for PKCE using SHA-256.
|
||||
*/
|
||||
async function generateCodeChallenge(verifier: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(verifier);
|
||||
const hash = await crypto.subtle.digest("SHA-256", data);
|
||||
return base64UrlEncode(new Uint8Array(hash));
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 URL-safe encoding (without padding).
|
||||
*/
|
||||
function base64UrlEncode(array: Uint8Array): string {
|
||||
const base64 = btoa(String.fromCharCode(...array));
|
||||
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||
}
|
||||
|
||||
//---
|
||||
// OAuth Flow
|
||||
//---
|
||||
/**
|
||||
* Initiates the OAuth authorization flow by redirecting to the Nexus authorization endpoint.
|
||||
*/
|
||||
async function startOauthFlow(): Promise<void> {
|
||||
const state = crypto.randomUUID();
|
||||
const verifier = generateCodeVerifier();
|
||||
const challenge = await generateCodeChallenge(verifier);
|
||||
|
||||
const redirectUri = new URL(window.location.href);
|
||||
const url = new URL("/oauth/authorize", NEXUS_API_BASE_URL);
|
||||
url.searchParams.set("response_type", "code");
|
||||
url.searchParams.set("client_id", OAUTH_CLIENT_ID);
|
||||
url.searchParams.set("code_challenge", challenge);
|
||||
url.searchParams.set("code_challenge_method", "S256");
|
||||
url.searchParams.set("state", state);
|
||||
url.searchParams.set(
|
||||
"redirect_uri",
|
||||
`${redirectUri.origin}${redirectUri.pathname}`,
|
||||
);
|
||||
storeState(state);
|
||||
storeVerifier(verifier);
|
||||
|
||||
// just open the page directly
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the OAuth flow by exchanging the authorization code for tokens.
|
||||
* Called automatically on page load if code parameter is present in URL.
|
||||
*/
|
||||
async function completeOauthFlow(): Promise<void> {
|
||||
try {
|
||||
const searchParams = new URL(window.location.href).searchParams;
|
||||
const code = searchParams.get("code");
|
||||
const state = searchParams.get("state");
|
||||
const error = searchParams.get("error");
|
||||
|
||||
if (error) {
|
||||
console.error("[thirdweb nexus] OAuth error:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!code || !state) {
|
||||
// No code or state found, nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
if (state !== getState()) {
|
||||
// State mismatch - possible CSRF attack
|
||||
console.warn("[thirdweb nexus] OAuth state mismatch");
|
||||
return;
|
||||
}
|
||||
|
||||
const verifier = getVerifier();
|
||||
if (!verifier) {
|
||||
// No verifier found, nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete the state and verifier so they can't be reused
|
||||
deleteState();
|
||||
deleteVerifier();
|
||||
|
||||
const redirectUri = new URL(window.location.href);
|
||||
const response = await fetch(`${NEXUS_API_BASE_URL}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
code,
|
||||
code_verifier: verifier,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: `${redirectUri.origin}${redirectUri.pathname}`,
|
||||
client_id: OAUTH_CLIENT_ID,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(
|
||||
"[thirdweb nexus] Failed to exchange authorization code for tokens",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as OAuthTokenResponse;
|
||||
|
||||
// Store all token data
|
||||
setTokenData(data.access_token, data.refresh_token, data.expires_in);
|
||||
} catch (error) {
|
||||
console.error("[thirdweb nexus] Error completing OAuth flow:", error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Nexus Fetch Testing</title>
|
||||
<script src="../src/exports/nexus-fetch.ts"></script>
|
||||
</head>
|
||||
<body>
|
||||
<button id="test-button">Test</button>
|
||||
<script>
|
||||
const testButton = document.getElementById("test-button");
|
||||
testButton.addEventListener("click", () => {
|
||||
fetch("https://q1qkkomp.nx.link/kindle", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
recipient: "0x0890C23024089675D072E984f28A93bb391a35Ab",
|
||||
})
|
||||
});
|
||||
});
|
||||
testButton.click();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
// This tsconfig file contains the shared config for the build (tsconfig.build.json) and type checking (tsconfig.json) config.
|
||||
"compilerOptions": {
|
||||
// Incremental builds
|
||||
// NOTE: Enabling incremental builds speeds up `tsc`. Keep in mind though that it does not reliably bust the cache when the `tsconfig.json` file changes.
|
||||
"allowJs": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"checkJs": false,
|
||||
|
||||
// Interop constraints
|
||||
"esModuleInterop": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"importHelpers": true,
|
||||
// Incremental builds
|
||||
// NOTE: Enabling incremental builds speeds up `tsc`. Keep in mind though that it does not reliably bust the cache when the `tsconfig.json` file changes.
|
||||
"incremental": false,
|
||||
|
||||
// jsx for "/react" portion
|
||||
"jsx": "react-jsx",
|
||||
"lib": [
|
||||
"ES2022", // By using ES2022 we get access to the `.cause` property on `Error` instances.
|
||||
"DOM" // We are adding `DOM` here to get the `fetch`, etc. types. This should be removed once these types are available via DefinitelyTyped.
|
||||
],
|
||||
"module": "NodeNext",
|
||||
|
||||
// Language and environment
|
||||
"moduleResolution": "NodeNext",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
|
||||
// Skip type checking for node modules
|
||||
"skipLibCheck": true,
|
||||
|
||||
// Type checking
|
||||
"strict": true,
|
||||
"target": "ES2021",
|
||||
"useDefineForClassFields": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
// This tsconfig file contains the shared config for the build (tsconfig.build.json) and type checking (tsconfig.json) config.
|
||||
"include": []
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"rootDir": "./src",
|
||||
"sourceMap": true
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.test-d.ts",
|
||||
"src/**/*.bench.ts",
|
||||
"src/**/*.macro.ts"
|
||||
],
|
||||
"extends": "./tsconfig.base.json",
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// This configuration is used for local development and type checking.
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "./dist",
|
||||
"paths": {
|
||||
"~test/*": ["./test/src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": [],
|
||||
"extends": "./tsconfig.base.json",
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"import": "*",
|
||||
"limit": "110 kB",
|
||||
"name": "@thirdweb-dev/nexus (esm)",
|
||||
"path": "./dist/esm/exports/nexus.js"
|
||||
},
|
||||
{
|
||||
"limit": "350 kB",
|
||||
"name": "@thirdweb-dev/nexus (cjs)",
|
||||
"path": "./dist/cjs/exports/nexus.js"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
# @thirdweb-dev/nexus
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#8332](https://github.com/thirdweb-dev/js/pull/8332) [`8e357b3`](https://github.com/thirdweb-dev/js/commit/8e357b3cb39dcc7a8595a83224098a9c44585389) Thanks [@jnsdls](https://github.com/jnsdls)! - initial release
|
||||