Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
881c7a96f7 | ||
|
|
ad68969559 | ||
|
|
915d955da1 | ||
|
|
7da16cff2f | ||
|
|
f0edc0f253 | ||
|
|
0a2d269782 | ||
|
|
3d996d1d23 | ||
|
|
40781b8170 | ||
|
|
d907017217 | ||
|
|
cf53c2d7f1 | ||
|
|
207cb9dbeb | ||
|
|
b4d365bdc0 | ||
|
|
ea4e2fe376 | ||
|
|
1d54cff7b4 | ||
|
|
fd04eefea9 | ||
|
|
4cc68310dc | ||
|
|
ccdce496e0 | ||
|
|
2725f337f8 | ||
|
|
2c6ea3e6dd | ||
|
|
219fc69d72 | ||
|
|
8d17685683 | ||
|
|
8ef418c543 | ||
|
|
b88f75b494 | ||
|
|
c83f1c6f7a | ||
|
|
bc0295487b | ||
|
|
90b779107d |
@@ -10,7 +10,7 @@ module.exports = [
|
||||
"/wallets",
|
||||
"/account-abstraction",
|
||||
"/payments",
|
||||
"/payments/x402",
|
||||
"/x402",
|
||||
"/nexus",
|
||||
"/auth",
|
||||
"/in-app-wallets",
|
||||
|
||||
@@ -479,6 +479,11 @@ async function redirects() {
|
||||
destination: "/tokens",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/payments/x402",
|
||||
destination: "/x402",
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ const customClaimAmounts: Record<number, number> = {
|
||||
531050104: 5,
|
||||
// Arc testnet
|
||||
5042002: 1,
|
||||
// Injective testnet
|
||||
1439: 1,
|
||||
};
|
||||
|
||||
const defaultClaimAmount = 0.01;
|
||||
|
||||
+8
@@ -14,6 +14,7 @@ type AggregatedMetrics = {
|
||||
|
||||
export function ProjectHighlightsCard(props: {
|
||||
selectedChart: string | undefined;
|
||||
aggregatedUserStats: InAppWalletStats[];
|
||||
userStats: InAppWalletStats[];
|
||||
volumeStats: UniversalBridgeStats[];
|
||||
teamSlug: string;
|
||||
@@ -22,6 +23,7 @@ export function ProjectHighlightsCard(props: {
|
||||
}) {
|
||||
const {
|
||||
selectedChart,
|
||||
aggregatedUserStats,
|
||||
userStats,
|
||||
volumeStats,
|
||||
teamSlug,
|
||||
@@ -70,6 +72,12 @@ export function ProjectHighlightsCard(props: {
|
||||
: "activeUsers"
|
||||
}
|
||||
aggregateFn={(_data, key) => {
|
||||
if (key === "activeUsers") {
|
||||
return aggregatedUserStats.reduce(
|
||||
(acc, curr) => acc + curr.uniqueWalletsConnected,
|
||||
0,
|
||||
);
|
||||
}
|
||||
return timeSeriesData.reduce((acc, curr) => acc + curr[key], 0);
|
||||
}}
|
||||
chartConfig={chartConfig}
|
||||
|
||||
@@ -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}
|
||||
|
||||
+70
-13
@@ -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) {
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { configure, getSolanaWalletBalance } from "@thirdweb-dev/api";
|
||||
import { THIRDWEB_API_HOST } from "@/constants/urls";
|
||||
|
||||
// Configure the API client to use the correct base URL
|
||||
configure({
|
||||
override: {
|
||||
baseUrl: THIRDWEB_API_HOST,
|
||||
},
|
||||
});
|
||||
|
||||
export async function fetchSolanaBalance({
|
||||
publicKey,
|
||||
authToken,
|
||||
clientId,
|
||||
chainId = "solana:mainnet",
|
||||
}: {
|
||||
publicKey: string;
|
||||
authToken: string;
|
||||
clientId: string;
|
||||
chainId?: "solana:mainnet" | "solana:devnet";
|
||||
}): Promise<{
|
||||
displayValue: string;
|
||||
symbol: string;
|
||||
value: string;
|
||||
decimals: number;
|
||||
} | null> {
|
||||
try {
|
||||
const response = await getSolanaWalletBalance({
|
||||
path: {
|
||||
address: publicKey,
|
||||
},
|
||||
query: {
|
||||
chainId,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": clientId,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.error || !response.data) {
|
||||
console.error(
|
||||
"Error fetching Solana balance:",
|
||||
response.error || "No data returned",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
displayValue: response.data.result.displayValue,
|
||||
symbol: "SOL",
|
||||
value: response.data.result.value,
|
||||
decimals: response.data.result.decimals,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching Solana balance:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+1
@@ -215,6 +215,7 @@ export default async function TransactionsAnalyticsPage(props: {
|
||||
solanaWallets={solanaAccounts.data.items}
|
||||
teamSlug={params.team_slug}
|
||||
solanaPermissionError={isSolanaPermissionError}
|
||||
authToken={authToken}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -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" />
|
||||
|
||||
+1
@@ -202,6 +202,7 @@ export default async function Page(props: {
|
||||
solanaWallets={solanaAccounts.data.items}
|
||||
teamSlug={params.team_slug}
|
||||
solanaPermissionError={isSolanaPermissionError || false}
|
||||
authToken={authToken}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
import {
|
||||
Callout,
|
||||
OpenSourceCard,
|
||||
createMetadata,
|
||||
InstallTabs,
|
||||
SDKCard,
|
||||
Grid,
|
||||
ArticleIconCard,
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
} from "@doc";
|
||||
import {
|
||||
ReactIcon,
|
||||
TypeScriptIcon,
|
||||
UnityIcon,
|
||||
DotNetIcon,
|
||||
UnrealEngineIcon,
|
||||
} from "@/icons";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "Find Routes",
|
||||
icon: "payments",
|
||||
},
|
||||
title: "Find Routes",
|
||||
description: "Find available routes for bridging, swapping, and paying.",
|
||||
});
|
||||
|
||||
# Find Routes
|
||||
|
||||
Use thirdweb's `Bridge.routes` utility to find all routes between tokens.
|
||||
|
||||
### Get All Routes
|
||||
|
||||
Retrieve all supported routes:
|
||||
|
||||
```typescript
|
||||
import { Bridge } from "thirdweb";
|
||||
|
||||
const routes = await Bridge.routes({
|
||||
client,
|
||||
});
|
||||
```
|
||||
|
||||
This returns an array of route objects with information about supported token pairs:
|
||||
|
||||
```typescript
|
||||
[
|
||||
{
|
||||
destinationToken: {
|
||||
address: "0x12c88a3C30A7AaBC1dd7f2c08a97145F5DCcD830",
|
||||
chainId: 1,
|
||||
decimals: 18,
|
||||
iconUri: "https://assets.coingecko.com/coins/images/37207/standard/G.jpg",
|
||||
name: "G7",
|
||||
symbol: "G7",
|
||||
},
|
||||
originToken: {
|
||||
address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
chainId: 480,
|
||||
decimals: 18,
|
||||
iconUri: "https://assets.relay.link/icons/1/light.png",
|
||||
name: "Ether",
|
||||
symbol: "ETH",
|
||||
}
|
||||
},
|
||||
// ... more routes
|
||||
]
|
||||
```
|
||||
|
||||
### Filter Routes by Origin
|
||||
|
||||
Get all routes starting from a specific chain and token:
|
||||
|
||||
```typescript
|
||||
import { Bridge } from "thirdweb";
|
||||
|
||||
// Get all routes starting from mainnet ETH
|
||||
const routes = await Bridge.routes({
|
||||
originChainId: 1,
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
client,
|
||||
});
|
||||
```
|
||||
|
||||
### Filter Routes by Destination
|
||||
|
||||
Find routes that end at a specific destination:
|
||||
|
||||
```typescript
|
||||
import { Bridge } from "thirdweb";
|
||||
|
||||
// Get all routes ending at USDC on Optimism
|
||||
const routes = await Bridge.routes({
|
||||
destinationChainId: 10,
|
||||
destinationTokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
|
||||
client,
|
||||
});
|
||||
```
|
||||
|
||||
### Filter Routes by Both Origin and Destination
|
||||
|
||||
Find direct routes between specific token pairs:
|
||||
|
||||
```typescript
|
||||
import { Bridge } from "thirdweb";
|
||||
|
||||
// Get routes from mainnet ETH to USDC on Optimism
|
||||
const routes = await Bridge.routes({
|
||||
originChainId: 1,
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
destinationChainId: 10,
|
||||
destinationTokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
|
||||
client,
|
||||
});
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
For large result sets, use pagination to retrieve routes in chunks:
|
||||
|
||||
```typescript
|
||||
import { Bridge } from "thirdweb";
|
||||
|
||||
// Get the first 10 routes starting from mainnet ETH
|
||||
const routes = await Bridge.routes({
|
||||
originChainId: 1,
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
client,
|
||||
});
|
||||
|
||||
// Get the next 10 routes
|
||||
const nextRoutes = await Bridge.routes({
|
||||
originChainId: 1,
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
limit: 10,
|
||||
offset: 10,
|
||||
client,
|
||||
});
|
||||
```
|
||||
|
||||
### Limit Route Complexity
|
||||
|
||||
Control the maximum number of steps in the returned routes:
|
||||
|
||||
```typescript
|
||||
import { Bridge } from "thirdweb";
|
||||
|
||||
// Get only direct routes (1 step)
|
||||
const directRoutes = await Bridge.routes({
|
||||
originChainId: 1,
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
maxSteps: 1,
|
||||
client,
|
||||
});
|
||||
```
|
||||
|
||||
## Going further
|
||||
|
||||
To connect with other auth strategies, use external wallets, or sponsor gas for users, check out the following guides:
|
||||
|
||||
- [Send a Payment](/payments/send)
|
||||
- [Get Token Prices](/payments/prices)
|
||||
|
||||
## Explore Full API References
|
||||
|
||||
- [Bridge.routes](/references/typescript/v5/routes)
|
||||
@@ -60,7 +60,6 @@ You can execute this quote exactly like a buy. See the [Send a Payment](/payment
|
||||
To connect with other auth strategies, use external wallets, or sponsor gas for users, check out the following guides:
|
||||
|
||||
- [Send a Payment](/payments/send)
|
||||
- [Get Routes](/payments/routes)
|
||||
|
||||
## Explore Full API References
|
||||
|
||||
|
||||
@@ -28,10 +28,6 @@ export const sidebar: SideBar = {
|
||||
href: `${bridgeSlug}/tokens`,
|
||||
name: "Get Token Prices",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/routes`,
|
||||
name: "Get Routes",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/bridge-widget-script`,
|
||||
name: "BridgeWidget Script",
|
||||
|
||||
@@ -255,23 +255,6 @@ export class SwapManager {
|
||||
|
||||
throw new Error("Transaction timeout - please check status manually");
|
||||
}
|
||||
|
||||
async getAvailableRoutes(params: {
|
||||
originChainId?: number;
|
||||
originTokenAddress?: string;
|
||||
destinationChainId?: number;
|
||||
destinationTokenAddress?: string;
|
||||
limit?: number;
|
||||
}) {
|
||||
try {
|
||||
return await Bridge.routes({
|
||||
...params,
|
||||
client: this.client,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get routes: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -319,92 +302,6 @@ async function swapEthToMatic(amount: string, userAccount: Account) {
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Price Comparison
|
||||
|
||||
Compare prices across multiple routes to find the best deal:
|
||||
|
||||
```typescript
|
||||
interface RouteComparison {
|
||||
route: any;
|
||||
quote: SwapQuote;
|
||||
priceImpact: number;
|
||||
gasEstimate: bigint;
|
||||
}
|
||||
|
||||
export class PriceComparator {
|
||||
private swapManager: SwapManager;
|
||||
|
||||
constructor(swapManager: SwapManager) {
|
||||
this.swapManager = swapManager;
|
||||
}
|
||||
|
||||
async compareRoutes(
|
||||
fromToken: { chainId: number; address: string },
|
||||
amount: bigint,
|
||||
userAddress: string
|
||||
): Promise<RouteComparison[]> {
|
||||
// Get available routes
|
||||
const routes = await this.swapManager.getAvailableRoutes({
|
||||
originChainId: fromToken.chainId,
|
||||
originTokenAddress: fromToken.address,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
// Get quotes for each route
|
||||
const comparisons: RouteComparison[] = [];
|
||||
|
||||
for (const route of routes) {
|
||||
try {
|
||||
const quote = await this.swapManager.getQuote({
|
||||
fromChainId: route.originToken.chainId,
|
||||
fromTokenAddress: route.originToken.address,
|
||||
toChainId: route.destinationToken.chainId,
|
||||
toTokenAddress: route.destinationToken.address,
|
||||
amount,
|
||||
userAddress,
|
||||
client: this.swapManager.client,
|
||||
});
|
||||
|
||||
const priceImpact = this.calculatePriceImpact(quote, amount);
|
||||
const gasEstimate = this.estimateGasCosts(quote);
|
||||
|
||||
comparisons.push({
|
||||
route,
|
||||
quote,
|
||||
priceImpact,
|
||||
gasEstimate,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(`Failed to get quote for route ${route.originToken.symbol} -> ${route.destinationToken.symbol}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by best output amount
|
||||
return comparisons.sort((a, b) =>
|
||||
Number(b.quote.destinationAmount) - Number(a.quote.destinationAmount)
|
||||
);
|
||||
}
|
||||
|
||||
private calculatePriceImpact(quote: SwapQuote, inputAmount: bigint): number {
|
||||
// Simplified price impact calculation
|
||||
const ratio = Number(quote.destinationAmount) / Number(quote.originAmount);
|
||||
return Math.abs(1 - ratio) * 100; // Percentage
|
||||
}
|
||||
|
||||
private estimateGasCosts(quote: SwapQuote): bigint {
|
||||
// Sum up gas estimates from all transactions
|
||||
return quote.steps.reduce((total, step) => {
|
||||
return total + step.transactions.reduce((stepTotal: bigint, tx: any) => {
|
||||
return stepTotal + (tx.gasLimit || 200000n); // Default estimate
|
||||
}, 0n);
|
||||
}, 0n);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Callout variant="info">
|
||||
**Gas Optimization Tip:** For frequently traded pairs, consider caching route data to reduce API calls and improve UI responsiveness.
|
||||
</Callout>
|
||||
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
@@ -415,20 +312,16 @@ export class PriceComparator {
|
||||
- **Cross-chain swaps** - Exchange tokens across 50+ supported chains
|
||||
- **Real-time quotes** - Get up-to-date pricing and execution estimates
|
||||
- **Route optimization** - Find the best path for any token pair
|
||||
- **Price comparison** - Compare multiple routes to maximize output
|
||||
- **Status tracking** - Monitor cross-chain transaction progress
|
||||
|
||||
|
||||
|
||||
## Going Further
|
||||
|
||||
- [Send a Payment](/payments/send) - Learn about peer-to-peer transfers
|
||||
- [Get Routes](/payments/routes) - Explore route discovery APIs
|
||||
- [Token Prices](/payments/tokens) - Access real-time token pricing
|
||||
- [Token Prices](/bridge/tokens) - Access real-time token pricing
|
||||
- [Webhooks](/payments/webhooks) - Get notified when swaps complete
|
||||
|
||||
## API Reference
|
||||
|
||||
- [Bridge.Buy.prepare](/references/typescript/v5/buy/prepare)
|
||||
- [Bridge.routes](/references/typescript/v5/routes)
|
||||
- [Bridge.status](/references/typescript/v5/status)
|
||||
@@ -119,7 +119,6 @@ const nextTokens = await Bridge.tokens({
|
||||
To connect with other auth strategies, use external wallets, or sponsor gas for users, check out the following guides:
|
||||
|
||||
- [Send a Payment](/payments/send)
|
||||
- [Get Routes](/payments/routes)
|
||||
|
||||
## Explore Full API References
|
||||
|
||||
|
||||
@@ -264,7 +264,6 @@ Once this code completes, your payment has been fully executed. Payments normall
|
||||
|
||||
- [Sell a Product](/payments/products)
|
||||
- [Token Prices](/payments/tokens)
|
||||
- [Routes](/payments/routes)
|
||||
|
||||
## Explore API References
|
||||
|
||||
|
||||
@@ -49,10 +49,6 @@ export const sidebar: SideBar = {
|
||||
href: `/bridge/tokens`,
|
||||
name: "Get Token Prices",
|
||||
},
|
||||
{
|
||||
href: `/bridge/routes`,
|
||||
name: "Get Routes",
|
||||
},
|
||||
{
|
||||
href: `${paymentsSlug}/webhooks`,
|
||||
name: "Webhooks",
|
||||
|
||||
@@ -222,7 +222,6 @@ async function verifyWebhook(
|
||||
To connect with other auth strategies, use external wallets, or sponsor gas for users, check out the following guides:
|
||||
|
||||
- [Send a Payment](/payments/send)
|
||||
- [Find Routes](/payments/routes)
|
||||
- [Get Token Prices](/payments/tokens)
|
||||
|
||||
## Explore Full API References
|
||||
|
||||
@@ -44,3 +44,7 @@ The admin key is temporarily stored in dashboard when performing actions that re
|
||||
Please note, you should never share your admin key with third parties.
|
||||
</Details>
|
||||
|
||||
<Details summary="Is Vault open source?">
|
||||
Vault is not yet open source, but we plan to release it soon.
|
||||
</Details>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export default async function Layout(props: { children: React.ReactNode }) {
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description:
|
||||
"Vault is an open-source non-custodial key management service, secured with TEE architecture (AWS Nitro Enclaves) and designed for blockchain applications.",
|
||||
"Vault is a non-custodial key management service secured with TEE architecture (AWS Nitro Enclaves), designed for blockchain applications.",
|
||||
title: "Vault",
|
||||
image: {
|
||||
icon: "vault",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ArrowLeftRightIcon, UserLockIcon, UsersIcon, WalletIcon } from "lucide-
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "thirdweb Vault",
|
||||
description: "An open-source, secure key management service for storing, managing, and protecting cryptographic keys and secrets.",
|
||||
description: "A secure key management service for storing, managing, and protecting cryptographic keys and secrets.",
|
||||
image: {
|
||||
icon: "vault",
|
||||
title: "thirdweb Vault",
|
||||
@@ -14,7 +14,7 @@ export const metadata = createMetadata({
|
||||
|
||||
# Vault
|
||||
|
||||
Vault is an open-source non-custodial key management service, secured with TEE architecture (AWS Nitro Enclaves) and designed for blockchain applications.
|
||||
Vault is a non-custodial key management service secured with TEE architecture (AWS Nitro Enclaves), designed for blockchain applications.
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -1,5 +1,61 @@
|
||||
# wagmi-inapp
|
||||
|
||||
## 0.0.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- thirdweb@5.111.6
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.181
|
||||
|
||||
## 0.0.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7da16cf`](https://github.com/thirdweb-dev/js/commit/7da16cff2ff3aaa80c7fe5614639d950d88b5bf2)]:
|
||||
- thirdweb@5.111.5
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.180
|
||||
|
||||
## 0.0.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`d907017`](https://github.com/thirdweb-dev/js/commit/d907017217a0927e7f69eab0470b1fe120b4d1a1), [`40781b8`](https://github.com/thirdweb-dev/js/commit/40781b8170a1014ef76e438ba3c8244137dba7ea)]:
|
||||
- thirdweb@5.111.4
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.179
|
||||
|
||||
## 0.0.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`207cb9d`](https://github.com/thirdweb-dev/js/commit/207cb9dbeb82411809bafa2a8e833b1f10414b92)]:
|
||||
- thirdweb@5.111.3
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.178
|
||||
|
||||
## 0.0.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1d54cff`](https://github.com/thirdweb-dev/js/commit/1d54cff7b442b8667ddd54bd783502c367ffc83e), [`fd04eef`](https://github.com/thirdweb-dev/js/commit/fd04eefea9a2a99eee3740d6746040c9f81cd43f)]:
|
||||
- thirdweb@5.111.2
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.177
|
||||
|
||||
## 0.0.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`ccdce49`](https://github.com/thirdweb-dev/js/commit/ccdce496e09bc9420b0e15854328651c01f36279)]:
|
||||
- thirdweb@5.111.1
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.176
|
||||
|
||||
## 0.0.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c83f1c6`](https://github.com/thirdweb-dev/js/commit/c83f1c6f7a38c8a9f424798e6e726e1d037ed042), [`219fc69`](https://github.com/thirdweb-dev/js/commit/219fc69d72b1ca861bf0228589039788008ea0c0)]:
|
||||
- thirdweb@5.111.0
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.175
|
||||
|
||||
## 0.0.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wagmi-inapp",
|
||||
"private": true,
|
||||
"version": "0.0.19",
|
||||
"version": "0.0.26",
|
||||
"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,54 @@
|
||||
# @thirdweb-dev/nebula
|
||||
|
||||
## 0.2.84
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- thirdweb@5.111.6
|
||||
|
||||
## 0.2.83
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7da16cf`](https://github.com/thirdweb-dev/js/commit/7da16cff2ff3aaa80c7fe5614639d950d88b5bf2)]:
|
||||
- thirdweb@5.111.5
|
||||
|
||||
## 0.2.82
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`d907017`](https://github.com/thirdweb-dev/js/commit/d907017217a0927e7f69eab0470b1fe120b4d1a1), [`40781b8`](https://github.com/thirdweb-dev/js/commit/40781b8170a1014ef76e438ba3c8244137dba7ea)]:
|
||||
- thirdweb@5.111.4
|
||||
|
||||
## 0.2.81
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`207cb9d`](https://github.com/thirdweb-dev/js/commit/207cb9dbeb82411809bafa2a8e833b1f10414b92)]:
|
||||
- thirdweb@5.111.3
|
||||
|
||||
## 0.2.80
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1d54cff`](https://github.com/thirdweb-dev/js/commit/1d54cff7b442b8667ddd54bd783502c367ffc83e), [`fd04eef`](https://github.com/thirdweb-dev/js/commit/fd04eefea9a2a99eee3740d6746040c9f81cd43f)]:
|
||||
- thirdweb@5.111.2
|
||||
|
||||
## 0.2.79
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`ccdce49`](https://github.com/thirdweb-dev/js/commit/ccdce496e09bc9420b0e15854328651c01f36279)]:
|
||||
- thirdweb@5.111.1
|
||||
|
||||
## 0.2.78
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c83f1c6`](https://github.com/thirdweb-dev/js/commit/c83f1c6f7a38c8a9f424798e6e726e1d037ed042), [`219fc69`](https://github.com/thirdweb-dev/js/commit/219fc69d72b1ca861bf0228589039788008ea0c0)]:
|
||||
- thirdweb@5.111.0
|
||||
|
||||
## 0.2.77
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -57,5 +57,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.77"
|
||||
"version": "0.2.84"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"import": "*",
|
||||
"limit": "1 kB",
|
||||
"name": "@thirdweb-dev/nexus (esm)",
|
||||
"path": "./dist/esm/exports/nexus.js"
|
||||
},
|
||||
{
|
||||
"limit": "1 kB",
|
||||
"name": "@thirdweb-dev/nexus (cjs)",
|
||||
"path": "./dist/cjs/exports/nexus.js"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
||||
"extends": "//",
|
||||
"overrides": [
|
||||
{
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"useSortedKeys": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"includes": ["package.json"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -1,5 +1,56 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.111.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`ad68969`](https://github.com/thirdweb-dev/js/commit/ad68969559a80a5f188a647ef0e1c04f0003a0ad)]:
|
||||
- @thirdweb-dev/engine@3.4.0
|
||||
|
||||
## 5.111.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8369](https://github.com/thirdweb-dev/js/pull/8369) [`7da16cf`](https://github.com/thirdweb-dev/js/commit/7da16cff2ff3aaa80c7fe5614639d950d88b5bf2) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Make maxAmount optional in wrapFetchWithPayment and loosen schema validation for payment payloads
|
||||
|
||||
## 5.111.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8363](https://github.com/thirdweb-dev/js/pull/8363) [`d907017`](https://github.com/thirdweb-dev/js/commit/d907017217a0927e7f69eab0470b1fe120b4d1a1) Thanks [@MananTank](https://github.com/MananTank)! - Fix BuyWidget autoconnect not working when receiverAddress prop is set
|
||||
|
||||
- [#8365](https://github.com/thirdweb-dev/js/pull/8365) [`40781b8`](https://github.com/thirdweb-dev/js/commit/40781b8170a1014ef76e438ba3c8244137dba7ea) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Respect passed chain when doing SIWE for in-app/ecosystem wallet
|
||||
|
||||
## 5.111.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8361](https://github.com/thirdweb-dev/js/pull/8361) [`207cb9d`](https://github.com/thirdweb-dev/js/commit/207cb9dbeb82411809bafa2a8e833b1f10414b92) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Add extra mobile detection for isMobile() function
|
||||
|
||||
## 5.111.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8358](https://github.com/thirdweb-dev/js/pull/8358) [`1d54cff`](https://github.com/thirdweb-dev/js/commit/1d54cff7b442b8667ddd54bd783502c367ffc83e) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Add support for extraMetadata in x402 payment requirements
|
||||
|
||||
- [#8356](https://github.com/thirdweb-dev/js/pull/8356) [`fd04eef`](https://github.com/thirdweb-dev/js/commit/fd04eefea9a2a99eee3740d6746040c9f81cd43f) Thanks [@kumaryash90](https://github.com/kumaryash90)! - Update implementations, fix deployment
|
||||
|
||||
## 5.111.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8354](https://github.com/thirdweb-dev/js/pull/8354) [`ccdce49`](https://github.com/thirdweb-dev/js/commit/ccdce496e09bc9420b0e15854328651c01f36279) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix hiddenWallets prop not applying to all wallets screen
|
||||
|
||||
## 5.111.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#8335](https://github.com/thirdweb-dev/js/pull/8335) [`c83f1c6`](https://github.com/thirdweb-dev/js/commit/c83f1c6f7a38c8a9f424798e6e726e1d037ed042) Thanks [@MananTank](https://github.com/MananTank)! - Remove `Bridge.routes` and `useBridgeRoutes`
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8345](https://github.com/thirdweb-dev/js/pull/8345) [`219fc69`](https://github.com/thirdweb-dev/js/commit/219fc69d72b1ca861bf0228589039788008ea0c0) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Update x402 flows to use remote /accepts endpoint
|
||||
|
||||
## 5.110.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -430,5 +430,5 @@
|
||||
}
|
||||
},
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "5.110.6"
|
||||
"version": "5.111.6"
|
||||
}
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
import { http, passthrough } from "msw";
|
||||
import { setupServer } from "msw/node";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import { TEST_CLIENT } from "~test/test-clients.js";
|
||||
import { routes } from "./Routes.js";
|
||||
|
||||
const server = setupServer(
|
||||
http.get("https://bridge.thirdweb.com/v1/routes", () => {
|
||||
passthrough();
|
||||
}),
|
||||
);
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("Bridge.routes", () => {
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
it("should get a valid list of routes", async () => {
|
||||
const allRoutes = await routes({
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
|
||||
expect(allRoutes).toBeDefined();
|
||||
expect(Array.isArray(allRoutes)).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter routes by origin chain", async () => {
|
||||
const filteredRoutes = await routes({
|
||||
client: TEST_CLIENT,
|
||||
originChainId: 1,
|
||||
});
|
||||
|
||||
expect(filteredRoutes).toBeDefined();
|
||||
expect(Array.isArray(filteredRoutes)).toBe(true);
|
||||
expect(
|
||||
filteredRoutes.every((route) => route.originToken.chainId === 1),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter routes by destination chain", async () => {
|
||||
const filteredRoutes = await routes({
|
||||
client: TEST_CLIENT,
|
||||
destinationChainId: 1,
|
||||
});
|
||||
|
||||
expect(filteredRoutes).toBeDefined();
|
||||
expect(Array.isArray(filteredRoutes)).toBe(true);
|
||||
expect(
|
||||
filteredRoutes.every((route) => route.destinationToken.chainId === 1),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter routes by origin token", async () => {
|
||||
const filteredRoutes = await routes({
|
||||
client: TEST_CLIENT,
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
});
|
||||
|
||||
expect(filteredRoutes).toBeDefined();
|
||||
expect(Array.isArray(filteredRoutes)).toBe(true);
|
||||
expect(
|
||||
filteredRoutes.every(
|
||||
(route) =>
|
||||
route.originToken.address ===
|
||||
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter routes by destination token", async () => {
|
||||
const filteredRoutes = await routes({
|
||||
client: TEST_CLIENT,
|
||||
destinationTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
});
|
||||
|
||||
expect(filteredRoutes).toBeDefined();
|
||||
expect(Array.isArray(filteredRoutes)).toBe(true);
|
||||
expect(
|
||||
filteredRoutes.every(
|
||||
(route) =>
|
||||
route.destinationToken.address ===
|
||||
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should combine filters", async () => {
|
||||
const filteredRoutes = await routes({
|
||||
client: TEST_CLIENT,
|
||||
destinationChainId: 10,
|
||||
destinationTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
originChainId: 1,
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
});
|
||||
|
||||
expect(filteredRoutes).toBeDefined();
|
||||
expect(Array.isArray(filteredRoutes)).toBe(true);
|
||||
expect(filteredRoutes.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
filteredRoutes.every(
|
||||
(route) =>
|
||||
route.originToken.chainId === 1 &&
|
||||
route.destinationToken.chainId === 10 &&
|
||||
route.originToken.address ===
|
||||
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" &&
|
||||
route.destinationToken.address ===
|
||||
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should respect limit and offset", async () => {
|
||||
const page1Routes = await routes({
|
||||
client: TEST_CLIENT,
|
||||
limit: 1,
|
||||
offset: 1,
|
||||
});
|
||||
|
||||
expect(page1Routes).toBeDefined();
|
||||
expect(Array.isArray(page1Routes)).toBe(true);
|
||||
expect(page1Routes.length).toBe(1);
|
||||
|
||||
const page2Routes = await routes({
|
||||
client: TEST_CLIENT,
|
||||
limit: 1,
|
||||
offset: 2,
|
||||
});
|
||||
|
||||
expect(page2Routes).toBeDefined();
|
||||
expect(Array.isArray(page2Routes)).toBe(true);
|
||||
expect(page2Routes.length).toBe(1);
|
||||
|
||||
expect(JSON.stringify(page1Routes)).not.toEqual(
|
||||
JSON.stringify(page2Routes),
|
||||
);
|
||||
});
|
||||
|
||||
it("should surface any errors", async () => {
|
||||
server.use(
|
||||
http.get("https://bridge.thirdweb.com/v1/routes", () => {
|
||||
return Response.json(
|
||||
{
|
||||
code: "InvalidRoutesRequest",
|
||||
message: "The provided request is invalid.",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
routes({
|
||||
client: TEST_CLIENT,
|
||||
limit: 1000,
|
||||
offset: 1000,
|
||||
}),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
"[Error: The provided request is invalid.]",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,228 +0,0 @@
|
||||
import type { Address as ox__Address, Hex as ox__Hex } from "ox";
|
||||
import type { ThirdwebClient } from "../client/client.js";
|
||||
import { getThirdwebBaseUrl } from "../utils/domains.js";
|
||||
import { getClientFetch } from "../utils/fetch.js";
|
||||
import { ApiError } from "./types/Errors.js";
|
||||
import type { Route } from "./types/Route.js";
|
||||
|
||||
/**
|
||||
* Retrieves supported Bridge routes based on the provided filters.
|
||||
*
|
||||
* When multiple filters are specified, a route must satisfy all filters to be included (it acts as an AND operator).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Bridge } from "thirdweb";
|
||||
*
|
||||
* const routes = await Bridge.routes({
|
||||
* client: thirdwebClient,
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Returned routes might look something like:
|
||||
* ```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",
|
||||
* }
|
||||
* },
|
||||
* {
|
||||
* destinationToken: {
|
||||
* address: "0x4d224452801ACEd8B2F0aebE155379bb5D594381",
|
||||
* chainId: 1,
|
||||
* decimals: 18,
|
||||
* iconUri: "https://coin-images.coingecko.com/coins/images/24383/large/apecoin.jpg?1696523566",
|
||||
* name: "ApeCoin",
|
||||
* symbol: "APE",
|
||||
* },
|
||||
* originToken: {
|
||||
* address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
* chainId: 480,
|
||||
* decimals: 18,
|
||||
* iconUri: "https://assets.relay.link/icons/1/light.png",
|
||||
* name: "Ether",
|
||||
* symbol: "ETH",
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* You can filter for specific chains or tokens:
|
||||
* ```typescript
|
||||
* import { Bridge } from "thirdweb";
|
||||
*
|
||||
* // Get all routes starting from mainnet ETH
|
||||
* const routes = await Bridge.routes({
|
||||
* originChainId: 1,
|
||||
* originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
* client: thirdwebClient,
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* The returned routes will be limited based on the API. You can paginate through the results using the `limit` and `offset` parameters:
|
||||
* ```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: thirdwebClient,
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* You can sort the returned routes by `popularity`:
|
||||
* ```ts
|
||||
* import { Bridge } from "thirdweb";
|
||||
*
|
||||
* // Get the 10 most popular routes starting from mainnet ETH
|
||||
* const routes = await Bridge.routes({
|
||||
* originChainId: 1,
|
||||
* originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
* limit: 10,
|
||||
* offset: 0,
|
||||
* sortBy: "popularity",
|
||||
* client: thirdwebClient,
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param options - The options for the quote.
|
||||
* @param options.client - Your thirdweb client.
|
||||
* @param options.originChainId - Filter by a specific origin chain ID.
|
||||
* @param options.originTokenAddress - Filter by a specific origin token address.
|
||||
* @param options.destinationChainId - Filter by a specific destination chain ID.
|
||||
* @param options.destinationTokenAddress - Filter by a specific destination token address.
|
||||
* @param options.transactionHash - Filter by a specific transaction hash.
|
||||
* @param options.maxSteps - Limit the number of steps returned.
|
||||
* @param options.sortBy - Sort the routes by various categories.
|
||||
* @param options.limit - Limit the number of routes returned.
|
||||
* @param options.offset - Offset the number of routes returned.
|
||||
*
|
||||
* @returns A promise that resolves to an array of routes.
|
||||
*
|
||||
* @throws Will throw an error if there is an issue fetching the routes.
|
||||
* @bridge
|
||||
* @beta
|
||||
*/
|
||||
export async function routes(options: routes.Options): Promise<routes.Result> {
|
||||
const {
|
||||
client,
|
||||
originChainId,
|
||||
originTokenAddress,
|
||||
destinationChainId,
|
||||
destinationTokenAddress,
|
||||
maxSteps,
|
||||
sortBy,
|
||||
limit,
|
||||
offset,
|
||||
includePrices,
|
||||
} = options;
|
||||
|
||||
const clientFetch = getClientFetch(client);
|
||||
const url = new URL(`${getThirdwebBaseUrl("bridge")}/v1/routes`);
|
||||
if (originChainId) {
|
||||
url.searchParams.set("originChainId", originChainId.toString());
|
||||
}
|
||||
if (originTokenAddress) {
|
||||
url.searchParams.set("originTokenAddress", originTokenAddress);
|
||||
}
|
||||
if (destinationChainId) {
|
||||
url.searchParams.set("destinationChainId", destinationChainId.toString());
|
||||
}
|
||||
if (destinationTokenAddress) {
|
||||
url.searchParams.set("destinationTokenAddress", destinationTokenAddress);
|
||||
}
|
||||
if (maxSteps) {
|
||||
url.searchParams.set("maxSteps", maxSteps.toString());
|
||||
}
|
||||
if (limit) {
|
||||
url.searchParams.set("limit", limit.toString());
|
||||
}
|
||||
if (offset) {
|
||||
url.searchParams.set("offset", offset.toString());
|
||||
}
|
||||
if (sortBy) {
|
||||
url.searchParams.set("sortBy", sortBy);
|
||||
}
|
||||
if (includePrices) {
|
||||
url.searchParams.set("includePrices", includePrices.toString());
|
||||
}
|
||||
|
||||
const response = await clientFetch(url.toString());
|
||||
if (!response.ok) {
|
||||
const errorJson = await response.json();
|
||||
throw new ApiError({
|
||||
code: errorJson.code || "UNKNOWN_ERROR",
|
||||
correlationId: errorJson.correlationId || undefined,
|
||||
message: errorJson.message || response.statusText,
|
||||
statusCode: response.status,
|
||||
});
|
||||
}
|
||||
|
||||
const { data }: { data: Route[] } = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Namespace containing types for the routes function.
|
||||
* @namespace routes
|
||||
* @bridge
|
||||
*/
|
||||
export declare namespace routes {
|
||||
/**
|
||||
* Options for fetching available bridge routes.
|
||||
* @interface Options
|
||||
* @bridge
|
||||
*/
|
||||
type Options = {
|
||||
/** Your thirdweb client */
|
||||
client: ThirdwebClient;
|
||||
/** The origin chain ID to filter routes by */
|
||||
originChainId?: number;
|
||||
/** The origin token address to filter routes by */
|
||||
originTokenAddress?: ox__Address.Address;
|
||||
/** The destination chain ID to filter routes by */
|
||||
destinationChainId?: number;
|
||||
/** The destination token address to filter routes by */
|
||||
destinationTokenAddress?: ox__Address.Address;
|
||||
/** Transaction hash to filter routes by */
|
||||
transactionHash?: ox__Hex.Hex;
|
||||
/**
|
||||
* Sort routes by popularity
|
||||
* @deprecated
|
||||
*/
|
||||
sortBy?: "popularity";
|
||||
/** Maximum number of steps in the route */
|
||||
maxSteps?: number;
|
||||
/** Whether to include price information in the response */
|
||||
includePrices?: boolean;
|
||||
/** Number of results to return (pagination) */
|
||||
limit?: number;
|
||||
/** Number of results to skip (pagination) */
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Result returned from fetching bridge routes.
|
||||
* Contains an array of available routes.
|
||||
* @interface Result
|
||||
* @bridge
|
||||
*/
|
||||
type Result = Route[];
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
export * as Buy from "./Buy.js";
|
||||
export { chains } from "./Chains.js";
|
||||
export * as Onramp from "./Onramp.js";
|
||||
export { routes } from "./Routes.js";
|
||||
export * as Sell from "./Sell.js";
|
||||
export { status } from "./Status.js";
|
||||
export { tokens } from "./Token.js";
|
||||
|
||||
@@ -9,44 +9,48 @@ import { getContract } from "../contract.js";
|
||||
import { deployCloneFactory } from "./utils/bootstrap.js";
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("deploy dynamic", () => {
|
||||
it.sequential("should deploy dynamic contract with extensions", async () => {
|
||||
await deployCloneFactory({
|
||||
account: TEST_ACCOUNT_A,
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
// TODO: Fix this test
|
||||
it.skip.sequential(
|
||||
"should deploy dynamic contract with extensions",
|
||||
async () => {
|
||||
await deployCloneFactory({
|
||||
account: TEST_ACCOUNT_A,
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
|
||||
const deployed = await deployPublishedContract({
|
||||
account: TEST_ACCOUNT_A,
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
contractId: "EvolvingNFT",
|
||||
contractParams: {
|
||||
contractURI: "",
|
||||
defaultAdmin: TEST_ACCOUNT_A.address,
|
||||
name: "Evolving nft",
|
||||
royaltyBps: 0n,
|
||||
royaltyRecipient: TEST_ACCOUNT_A.address,
|
||||
saleRecipient: TEST_ACCOUNT_A.address,
|
||||
symbol: "ENFT",
|
||||
trustedForwarders: [],
|
||||
},
|
||||
});
|
||||
const deployed = await deployPublishedContract({
|
||||
account: TEST_ACCOUNT_A,
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
contractId: "EvolvingNFT",
|
||||
contractParams: {
|
||||
contractURI: "",
|
||||
defaultAdmin: TEST_ACCOUNT_A.address,
|
||||
name: "Evolving nft",
|
||||
royaltyBps: 0n,
|
||||
royaltyRecipient: TEST_ACCOUNT_A.address,
|
||||
saleRecipient: TEST_ACCOUNT_A.address,
|
||||
symbol: "ENFT",
|
||||
trustedForwarders: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(deployed).toBeDefined();
|
||||
expect(deployed).toBeDefined();
|
||||
|
||||
const contract = getContract({
|
||||
address: deployed,
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
const contract = getContract({
|
||||
address: deployed,
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
|
||||
const extensions = await readContract({
|
||||
contract,
|
||||
method: resolveMethod("getAllExtensions"),
|
||||
params: [],
|
||||
});
|
||||
const extensions = await readContract({
|
||||
contract,
|
||||
method: resolveMethod("getAllExtensions"),
|
||||
params: [],
|
||||
});
|
||||
|
||||
expect(extensions.length).toEqual(3);
|
||||
});
|
||||
expect(extensions.length).toEqual(3);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const ZKSYNC_IMPLEMENTATIONS: Record<number, Record<string, string>> = {
|
||||
232: {
|
||||
MarketplaceV3: "0xB7387cF425eC8Da1b04b8E34De86245cb32bCA6D",
|
||||
MarketplaceV3: "0x4e0C3577335961Ff800FFDA24981EB2F38D94483",
|
||||
},
|
||||
300: {
|
||||
MarketplaceV3: "0x58e0F289C7dD2025eBd0696d913ECC0fdc1CC8bc",
|
||||
@@ -18,7 +18,7 @@ export const ZKSYNC_IMPLEMENTATIONS: Record<number, Record<string, string>> = {
|
||||
MarketplaceV3: "0x2dA4Dd326A6482679547071be21f74685d730504",
|
||||
},
|
||||
37111: {
|
||||
MarketplaceV3: "0xF8fd1016420c2B7832714241d36Efe55D41df126",
|
||||
MarketplaceV3: "0x12A305cc7168fa3b7B172fE53c57b9a22716F667",
|
||||
},
|
||||
50104: {
|
||||
MarketplaceV3: "0x292c324920d4120F928dA6f04548442fa10B8B0f",
|
||||
|
||||
@@ -171,7 +171,8 @@ describe.runIf(process.env.TW_SECRET_KEY)("getEvents", () => {
|
||||
expect(events.length).toBe(14);
|
||||
});
|
||||
|
||||
it("should get individual events with extension no filter using indexer", async () => {
|
||||
// TODO: re-enable this test once indexer is fixed
|
||||
it.skip("should get individual events with extension no filter using indexer", async () => {
|
||||
const events = await getContractEvents({
|
||||
contract: DOODLES_CONTRACT,
|
||||
events: [transferEvent()],
|
||||
@@ -182,7 +183,8 @@ describe.runIf(process.env.TW_SECRET_KEY)("getEvents", () => {
|
||||
expect(events.length).toBe(38);
|
||||
});
|
||||
|
||||
it("should get events for signature using indexer", async () => {
|
||||
// TODO: re-enable this test once indexer is fixed
|
||||
it.skip("should get events for signature using indexer", async () => {
|
||||
const events = await getContractEvents({
|
||||
contract: DOODLES_CONTRACT,
|
||||
events: [
|
||||
@@ -198,7 +200,8 @@ describe.runIf(process.env.TW_SECRET_KEY)("getEvents", () => {
|
||||
expect(events.length).toBe(2);
|
||||
});
|
||||
|
||||
it("should get specified events using indexer", async () => {
|
||||
// TODO: re-enable this test once indexer is fixed
|
||||
it.skip("should get specified events using indexer", async () => {
|
||||
const events = await getContractEvents({
|
||||
contract: USDT_CONTRACT,
|
||||
events: [
|
||||
|
||||
@@ -74,10 +74,6 @@ export type {
|
||||
} from "../react/core/hooks/transaction/useSendTransaction.js";
|
||||
export { useSimulateTransaction } from "../react/core/hooks/transaction/useSimulateTransaction.js";
|
||||
export type { BridgePrepareResult } from "../react/core/hooks/useBridgePrepare.js";
|
||||
export {
|
||||
type UseBridgeRoutesParams,
|
||||
useBridgeRoutes,
|
||||
} from "../react/core/hooks/useBridgeRoutes.js";
|
||||
export type { CompletedStatusResult } from "../react/core/hooks/useStepExecutor.js";
|
||||
export { useActiveAccount } from "../react/core/hooks/wallets/useActiveAccount.js";
|
||||
// wallet hooks
|
||||
|
||||
+44
-40
@@ -11,50 +11,54 @@ import { sendTransaction } from "../../../transaction/actions/send-transaction.j
|
||||
import { installPublishedExtension } from "./installPublishedExtension.js";
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("install extension", () => {
|
||||
it.sequential("should install extension to a dynamic contract", async () => {
|
||||
await deployCloneFactory({
|
||||
account: TEST_ACCOUNT_A,
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
// TODO: Fix this test
|
||||
it.skip.sequential(
|
||||
"should install extension to a dynamic contract",
|
||||
async () => {
|
||||
await deployCloneFactory({
|
||||
account: TEST_ACCOUNT_A,
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
|
||||
const deployed = await deployPublishedContract({
|
||||
account: TEST_ACCOUNT_A,
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
contractId: "EvolvingNFT",
|
||||
contractParams: {
|
||||
contractURI: "",
|
||||
defaultAdmin: TEST_ACCOUNT_A.address,
|
||||
name: "Evolving nft",
|
||||
royaltyBps: 0n,
|
||||
royaltyRecipient: TEST_ACCOUNT_A.address,
|
||||
saleRecipient: TEST_ACCOUNT_A.address,
|
||||
symbol: "ENFT",
|
||||
trustedForwarders: [],
|
||||
},
|
||||
});
|
||||
const deployed = await deployPublishedContract({
|
||||
account: TEST_ACCOUNT_A,
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
contractId: "EvolvingNFT",
|
||||
contractParams: {
|
||||
contractURI: "",
|
||||
defaultAdmin: TEST_ACCOUNT_A.address,
|
||||
name: "Evolving nft",
|
||||
royaltyBps: 0n,
|
||||
royaltyRecipient: TEST_ACCOUNT_A.address,
|
||||
saleRecipient: TEST_ACCOUNT_A.address,
|
||||
symbol: "ENFT",
|
||||
trustedForwarders: [],
|
||||
},
|
||||
});
|
||||
|
||||
const contract = getContract({
|
||||
address: deployed,
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
const contract = getContract({
|
||||
address: deployed,
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
|
||||
const transaction = installPublishedExtension({
|
||||
account: TEST_ACCOUNT_A,
|
||||
contract,
|
||||
extensionName: "DirectListingsLogic",
|
||||
});
|
||||
const transaction = installPublishedExtension({
|
||||
account: TEST_ACCOUNT_A,
|
||||
contract,
|
||||
extensionName: "DirectListingsLogic",
|
||||
});
|
||||
|
||||
await sendTransaction({ account: TEST_ACCOUNT_A, transaction });
|
||||
await sendTransaction({ account: TEST_ACCOUNT_A, transaction });
|
||||
|
||||
const extensions = await readContract({
|
||||
contract,
|
||||
method: resolveMethod("getAllExtensions"),
|
||||
params: [],
|
||||
});
|
||||
const extensions = await readContract({
|
||||
contract,
|
||||
method: resolveMethod("getAllExtensions"),
|
||||
params: [],
|
||||
});
|
||||
|
||||
expect(extensions.length).toEqual(4);
|
||||
});
|
||||
expect(extensions.length).toEqual(4);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,8 @@ import { deployPublishedContract } from "../../prebuilts/deploy-published.js";
|
||||
import { uninstallExtension } from "./uninstallExtension.js";
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("uninstall extension", () => {
|
||||
it.sequential(
|
||||
// TODO: Fix this test
|
||||
it.skip.sequential(
|
||||
"should uninstall extension from a dynamic contract",
|
||||
async () => {
|
||||
await deployCloneFactory({
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
fetchBytecodeFromCompilerMetadata,
|
||||
} from "../../utils/any-evm/deploy-metadata.js";
|
||||
import { encodeExtraDataWithUri } from "../../utils/any-evm/encode-extra-data-with-uri.js";
|
||||
import { isZkSyncChain } from "../../utils/any-evm/zksync/isZkSyncChain.js";
|
||||
import type { Hex } from "../../utils/encoding/hex.js";
|
||||
import type { Account } from "../../wallets/interfaces/wallet.js";
|
||||
import { getAllDefaultConstructorParamsForImplementation } from "./get-required-transactions.js";
|
||||
@@ -213,7 +214,8 @@ export async function deployContractfromDeployMetadata(
|
||||
|
||||
if (
|
||||
deployMetadata.routerType === "dynamic" &&
|
||||
deployMetadata.defaultExtensions
|
||||
deployMetadata.defaultExtensions &&
|
||||
!isZkSyncChain(chain)
|
||||
) {
|
||||
for (const e of deployMetadata.defaultExtensions) {
|
||||
await getOrDeployInfraForPublishedContract({
|
||||
|
||||
@@ -253,10 +253,10 @@ export function useSendTransactionCore(args: {
|
||||
(nativeCost > 0n && nativeBalance.value < nativeCost);
|
||||
|
||||
if (shouldShowModal) {
|
||||
const supportedDestinations = await Bridge.routes({
|
||||
const tokens = await Bridge.tokens({
|
||||
client: tx.client,
|
||||
destinationChainId: tx.chain.id,
|
||||
destinationTokenAddress: _erc20Value?.tokenAddress,
|
||||
chainId: tx.chain.id,
|
||||
tokenAddress: _erc20Value?.tokenAddress,
|
||||
}).catch((err) => {
|
||||
trackPayEvent({
|
||||
client: tx.client,
|
||||
@@ -269,11 +269,8 @@ export function useSendTransactionCore(args: {
|
||||
return null;
|
||||
});
|
||||
|
||||
if (
|
||||
!supportedDestinations ||
|
||||
supportedDestinations.length === 0
|
||||
) {
|
||||
// not a supported destination -> show deposit screen
|
||||
if (!tokens || tokens.length === 0) {
|
||||
// not a supported token -> show deposit screen
|
||||
trackPayEvent({
|
||||
client: tx.client,
|
||||
error: JSON.stringify({
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type MockedFunction,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import { routes } from "../../../bridge/Routes.js";
|
||||
import { ApiError } from "../../../bridge/types/Errors.js";
|
||||
import type { Route } from "../../../bridge/types/Route.js";
|
||||
import type { ThirdwebClient } from "../../../client/client.js";
|
||||
import type { UseBridgeRoutesParams } from "./useBridgeRoutes.js";
|
||||
|
||||
// Mock the Bridge routes function
|
||||
vi.mock("../../../bridge/Routes.js", () => ({
|
||||
routes: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockRoutes = routes as MockedFunction<typeof routes>;
|
||||
|
||||
// Mock client
|
||||
const mockClient = { clientId: "test" } as ThirdwebClient;
|
||||
|
||||
// Mock route data
|
||||
const mockRouteData: Route[] = [
|
||||
{
|
||||
destinationToken: {
|
||||
address: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
|
||||
chainId: 137,
|
||||
decimals: 18,
|
||||
name: "Wrapped Ethereum",
|
||||
priceUsd: 2000.0,
|
||||
symbol: "WETH",
|
||||
},
|
||||
originToken: {
|
||||
address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
chainId: 1,
|
||||
decimals: 18,
|
||||
name: "Ethereum",
|
||||
priceUsd: 2000.0,
|
||||
symbol: "ETH",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe("useBridgeRoutes", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should export correct hook parameters type", () => {
|
||||
// Type-only test to verify UseBridgeRoutesParams interface
|
||||
const params: UseBridgeRoutesParams = {
|
||||
client: mockClient,
|
||||
destinationChainId: 137,
|
||||
enabled: true,
|
||||
originChainId: 1,
|
||||
};
|
||||
|
||||
expect(params).toBeDefined();
|
||||
expect(params.client).toBe(mockClient);
|
||||
expect(params.originChainId).toBe(1);
|
||||
expect(params.destinationChainId).toBe(137);
|
||||
expect(params.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle different parameter combinations", () => {
|
||||
const fullParams: UseBridgeRoutesParams = {
|
||||
client: mockClient,
|
||||
destinationChainId: 137,
|
||||
destinationTokenAddress: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
|
||||
enabled: false,
|
||||
limit: 10,
|
||||
maxSteps: 3,
|
||||
offset: 0,
|
||||
originChainId: 1,
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
sortBy: "popularity",
|
||||
};
|
||||
|
||||
expect(fullParams).toBeDefined();
|
||||
expect(fullParams.sortBy).toBe("popularity");
|
||||
expect(fullParams.maxSteps).toBe(3);
|
||||
expect(fullParams.limit).toBe(10);
|
||||
expect(fullParams.offset).toBe(0);
|
||||
});
|
||||
|
||||
it("should have optional enabled parameter defaulting to true", () => {
|
||||
const paramsWithoutEnabled: UseBridgeRoutesParams = {
|
||||
client: mockClient,
|
||||
destinationChainId: 137,
|
||||
originChainId: 1,
|
||||
};
|
||||
|
||||
expect(paramsWithoutEnabled.enabled).toBeUndefined(); // Should be optional
|
||||
});
|
||||
|
||||
it("should validate that Bridge.routes would be called with correct parameters", async () => {
|
||||
const testParams = {
|
||||
client: mockClient,
|
||||
destinationChainId: 137,
|
||||
originChainId: 1,
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" as const,
|
||||
};
|
||||
|
||||
// Mock the routes function to return our test data
|
||||
mockRoutes.mockResolvedValue(mockRouteData);
|
||||
|
||||
// Directly call the routes function to verify it works with our parameters
|
||||
const result = await routes(testParams);
|
||||
|
||||
expect(mockRoutes).toHaveBeenCalledWith(testParams);
|
||||
expect(result).toEqual(mockRouteData);
|
||||
});
|
||||
|
||||
it("should handle API errors properly", async () => {
|
||||
const apiError = new ApiError({
|
||||
code: "INVALID_INPUT",
|
||||
message: "Invalid parameters",
|
||||
statusCode: 400,
|
||||
});
|
||||
|
||||
mockRoutes.mockRejectedValue(apiError);
|
||||
|
||||
try {
|
||||
await routes({
|
||||
client: mockClient,
|
||||
destinationChainId: 137,
|
||||
originChainId: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error).toBe(apiError);
|
||||
expect(error).toBeInstanceOf(ApiError);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,75 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { routes as RoutesTypes } from "../../../bridge/Routes.js";
|
||||
import { routes } from "../../../bridge/Routes.js";
|
||||
import { ApiError } from "../../../bridge/types/Errors.js";
|
||||
import { mapBridgeError } from "../errors/mapBridgeError.js";
|
||||
|
||||
/**
|
||||
* Parameters for the useBridgeRoutes hook
|
||||
*/
|
||||
export type UseBridgeRoutesParams = RoutesTypes.Options & {
|
||||
/**
|
||||
* Whether to enable the query. Useful for conditional fetching.
|
||||
* @default true
|
||||
*/
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook that fetches available bridge routes with caching and retry logic
|
||||
*
|
||||
* @param params - Parameters for fetching routes including client and filter options
|
||||
* @returns React Query result with routes data, loading state, and error handling
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { data: routes, isLoading, error } = useBridgeRoutes({
|
||||
* client: thirdwebClient,
|
||||
* originChainId: 1,
|
||||
* destinationChainId: 137,
|
||||
* originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function useBridgeRoutes(params: UseBridgeRoutesParams) {
|
||||
const { enabled = true, ...routeParams } = params;
|
||||
|
||||
return useQuery({
|
||||
enabled: enabled && !!routeParams.client,
|
||||
gcTime: 10 * 60 * 1000,
|
||||
queryFn: () => routes(routeParams),
|
||||
queryKey: [
|
||||
"bridge-routes",
|
||||
{
|
||||
destinationChainId: routeParams.destinationChainId,
|
||||
destinationTokenAddress: routeParams.destinationTokenAddress,
|
||||
limit: routeParams.limit,
|
||||
maxSteps: routeParams.maxSteps,
|
||||
offset: routeParams.offset,
|
||||
originChainId: routeParams.originChainId,
|
||||
originTokenAddress: routeParams.originTokenAddress,
|
||||
sortBy: routeParams.sortBy,
|
||||
},
|
||||
], // 5 minutes - routes are relatively stable
|
||||
retry: (failureCount, error) => {
|
||||
// Handle both ApiError and generic Error instances
|
||||
if (error instanceof ApiError) {
|
||||
const bridgeError = mapBridgeError(error);
|
||||
|
||||
// Don't retry on client-side errors (4xx)
|
||||
if (
|
||||
bridgeError.statusCode &&
|
||||
bridgeError.statusCode >= 400 &&
|
||||
bridgeError.statusCode < 500
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Retry up to 3 times for server errors or network issues
|
||||
return failureCount < 3;
|
||||
}, // 10 minutes garbage collection
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
||||
staleTime: 5 * 60 * 1000, // Exponential backoff, max 30s
|
||||
});
|
||||
}
|
||||
@@ -142,7 +142,9 @@ export function ConnectModal(
|
||||
const inAppWallet = wallets.find((wallet) => wallet.id === "inApp") as
|
||||
| Wallet<"inApp">
|
||||
| undefined;
|
||||
const externalWallets = wallets.filter((wallet) => wallet.id !== "inApp");
|
||||
const externalWallets = wallets
|
||||
.filter((wallet) => wallet.id !== "inApp")
|
||||
.filter((wallet) => !props.hiddenWallets?.includes(wallet.id));
|
||||
const showBranding = props.connectModal?.showThirdwebBranding !== false;
|
||||
const connectionManager = useConnectionManager();
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
import type { CompletedStatusResult } from "../../../core/hooks/useStepExecutor.js";
|
||||
import type { SupportedTokens } from "../../../core/utils/defaultTokens.js";
|
||||
import { webWindowAdapter } from "../../adapters/WindowAdapter.js";
|
||||
import { AutoConnect } from "../AutoConnect/AutoConnect.js";
|
||||
import connectLocaleEn from "../ConnectWallet/locale/en.js";
|
||||
import { EmbedContainer } from "../ConnectWallet/Modal/ConnectEmbed.js";
|
||||
import { DynamicHeight } from "../components/DynamicHeight.js";
|
||||
@@ -359,6 +360,21 @@ export function BuyWidget(props: BuyWidgetProps) {
|
||||
className={props.className}
|
||||
style={props.style}
|
||||
>
|
||||
{props.connectOptions?.autoConnect !== false && (
|
||||
<AutoConnect
|
||||
client={props.client}
|
||||
wallets={props.connectOptions?.wallets}
|
||||
timeout={
|
||||
typeof props.connectOptions?.autoConnect === "object"
|
||||
? props.connectOptions?.autoConnect?.timeout
|
||||
: undefined
|
||||
}
|
||||
appMetadata={props.connectOptions?.appMetadata}
|
||||
accountAbstraction={props.connectOptions?.accountAbstraction}
|
||||
chain={props.connectOptions?.chain}
|
||||
/>
|
||||
)}
|
||||
|
||||
<BridgeWidgetContent
|
||||
{...props}
|
||||
theme={props.theme || "dark"}
|
||||
|
||||
@@ -298,6 +298,7 @@ export function FundWallet(props: FundWalletProps) {
|
||||
}}
|
||||
theme={theme}
|
||||
{...props.connectOptions}
|
||||
autoConnect={false}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
|
||||
@@ -8,6 +8,7 @@ import walletInfos from "../../../../../wallets/__generated__/wallet-infos.js";
|
||||
import { NON_SEARCHABLE_WALLETS } from "../../../../../wallets/constants.js";
|
||||
import { createWallet } from "../../../../../wallets/create-wallet.js";
|
||||
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
|
||||
import type { WalletId } from "../../../../../wallets/wallet-types.js";
|
||||
import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js";
|
||||
import { iconSize, spacing } from "../../../../core/design-system/index.js";
|
||||
import { useSetSelectionData } from "../../../providers/wallet-ui-states-provider.js";
|
||||
@@ -34,15 +35,16 @@ function AllWalletsUI(props: {
|
||||
recommendedWallets: Wallet[] | undefined;
|
||||
connectLocale: ConnectLocale;
|
||||
disableSelectionDataReset?: boolean;
|
||||
walletIdsToHide?: WalletId[];
|
||||
}) {
|
||||
const { itemsToShow, lastItemRef } = useShowMore<HTMLLIElement>(10, 10);
|
||||
const setSelectionData = useSetSelectionData();
|
||||
|
||||
const walletList = useMemo(() => {
|
||||
return walletInfos.filter(
|
||||
(info) => !NON_SEARCHABLE_WALLETS.includes(info.id),
|
||||
);
|
||||
}, []);
|
||||
return walletInfos
|
||||
.filter((info) => !NON_SEARCHABLE_WALLETS.includes(info.id))
|
||||
.filter((info) => !props.walletIdsToHide?.includes(info.id));
|
||||
}, [props.walletIdsToHide]);
|
||||
|
||||
const fuseInstance = useMemo(() => {
|
||||
return new Fuse(walletList, {
|
||||
|
||||
@@ -172,6 +172,7 @@ export const ConnectModalContent = (props: {
|
||||
onBack={handleBack}
|
||||
onSelect={setScreen}
|
||||
recommendedWallets={props.recommendedWallets}
|
||||
walletIdsToHide={props.walletIdsToHide}
|
||||
size={props.size}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
@@ -138,6 +138,20 @@ export function LargeAmount() {
|
||||
);
|
||||
}
|
||||
|
||||
export function NoAutoConnect() {
|
||||
return (
|
||||
<Variant
|
||||
client={storyClient}
|
||||
chain={ethereum}
|
||||
tokenAddress="0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"
|
||||
amount="150000"
|
||||
connectOptions={{
|
||||
autoConnect: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Variant(props: BuyWidgetProps) {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -30,6 +30,45 @@ function isIOS(): boolean {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function isMobile(): boolean {
|
||||
return isAndroid() || isIOS();
|
||||
function hasTouchScreen(): boolean {
|
||||
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
"ontouchstart" in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
// @ts-expect-error - msMaxTouchPoints is IE specific
|
||||
navigator.msMaxTouchPoints > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
function hasMobileAPIs(): boolean {
|
||||
if (typeof window === "undefined") {
|
||||
return false;
|
||||
}
|
||||
return "orientation" in window || "onorientationchange" in window;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function isMobile(): boolean {
|
||||
// Primary signal: OS detection via user agent
|
||||
const isMobileOS = isAndroid() || isIOS();
|
||||
|
||||
if (isMobileOS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Secondary signal: catch edge cases like webviews with modified user agents
|
||||
// Both touch capability AND mobile-specific APIs must be present to avoid
|
||||
// false positives on touch-enabled desktops
|
||||
if (hasTouchScreen() && hasMobileAPIs()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { signLoginPayload } from "../../../../auth/core/sign-login-payload.js";
|
||||
import type { LoginPayload } from "../../../../auth/core/types.js";
|
||||
import type { Chain } from "../../../../chains/types.js";
|
||||
import { getCachedChain } from "../../../../chains/utils.js";
|
||||
import type { ThirdwebClient } from "../../../../client/client.js";
|
||||
import { getClientFetch } from "../../../../utils/fetch.js";
|
||||
@@ -15,10 +16,11 @@ import type { AuthStoredTokenWithCookieReturnType } from "./types.js";
|
||||
export async function siweAuthenticate(args: {
|
||||
wallet: Wallet;
|
||||
client: ThirdwebClient;
|
||||
chain?: Chain;
|
||||
ecosystem?: Ecosystem;
|
||||
}): Promise<AuthStoredTokenWithCookieReturnType> {
|
||||
const { wallet, client, ecosystem } = args;
|
||||
const siweChain = getCachedChain(1); // always use mainnet for SIWE for wide wallet compatibility
|
||||
const { wallet, client, ecosystem, chain } = args;
|
||||
const siweChain = chain || getCachedChain(1); // fallback to mainnet for SIWE for wide wallet compatibility
|
||||
// only connect if the wallet doesn't already have an account
|
||||
const account =
|
||||
wallet.getAccount() || (await wallet.connect({ chain: siweChain, client }));
|
||||
|
||||
@@ -69,7 +69,7 @@ export type SingleStepAuthArgsType =
|
||||
| {
|
||||
strategy: "wallet";
|
||||
wallet: Wallet;
|
||||
chain: Chain;
|
||||
chain?: Chain;
|
||||
}
|
||||
| {
|
||||
strategy: "guest";
|
||||
|
||||
@@ -186,6 +186,7 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
client: this.client,
|
||||
ecosystem: params.ecosystem,
|
||||
wallet: params.wallet,
|
||||
chain: params.chain,
|
||||
});
|
||||
}
|
||||
case "github":
|
||||
|
||||
@@ -107,6 +107,7 @@ describe("InAppWebConnector.connect", () => {
|
||||
client: TEST_CLIENT,
|
||||
ecosystem: undefined,
|
||||
wallet: mockWallet,
|
||||
chain: ethereum,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -368,6 +368,7 @@ export class InAppWebConnector implements InAppConnector {
|
||||
client: this.client,
|
||||
ecosystem: this.ecosystem,
|
||||
wallet: args.wallet,
|
||||
chain: args.chain,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import type { Abi } from "abitype";
|
||||
import { toFunctionSelector } from "viem/utils";
|
||||
import { ChainIdToNetwork, type Money, moneySchema } from "x402/types";
|
||||
import { getCachedChain } from "../chains/utils.js";
|
||||
import type { ThirdwebClient } from "../client/client.js";
|
||||
import { resolveContractAbi } from "../contract/actions/resolve-abi.js";
|
||||
import { getContract } from "../contract/contract.js";
|
||||
import { isPermitSupported } from "../extensions/erc20/__generated__/IERC20Permit/write/permit.js";
|
||||
import { isTransferWithAuthorizationSupported } from "../extensions/erc20/__generated__/USDC/write/transferWithAuthorization.js";
|
||||
import { getAddress } from "../utils/address.js";
|
||||
import { toUnits } from "../utils/units.js";
|
||||
import { decodePayment } from "./encode.js";
|
||||
import type { ThirdwebX402Facilitator } from "./facilitator.js";
|
||||
import {
|
||||
networkToChainId,
|
||||
networkToCaip2ChainId,
|
||||
type RequestedPaymentPayload,
|
||||
type RequestedPaymentRequirements,
|
||||
} from "./schemas.js";
|
||||
import {
|
||||
type DefaultAsset,
|
||||
type ERC20TokenAmount,
|
||||
type PaymentArgs,
|
||||
type PaymentRequiredResult,
|
||||
@@ -49,96 +44,27 @@ export async function decodePaymentRequest(
|
||||
routeConfig = {},
|
||||
method,
|
||||
paymentData,
|
||||
extraMetadata,
|
||||
} = args;
|
||||
const {
|
||||
description,
|
||||
mimeType,
|
||||
maxTimeoutSeconds,
|
||||
inputSchema,
|
||||
outputSchema,
|
||||
errorMessages,
|
||||
discoverable,
|
||||
} = routeConfig;
|
||||
const { errorMessages } = routeConfig;
|
||||
|
||||
let chainId: number;
|
||||
try {
|
||||
chainId = networkToChainId(network);
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 402,
|
||||
responseHeaders: { "Content-Type": "application/json" },
|
||||
responseBody: {
|
||||
x402Version,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: `Invalid network: ${network}`,
|
||||
accepts: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const atomicAmountForAsset = await processPriceToAtomicAmount(
|
||||
const paymentRequirementsResult = await facilitator.accepts({
|
||||
resourceUrl,
|
||||
method,
|
||||
network,
|
||||
price,
|
||||
chainId,
|
||||
facilitator,
|
||||
);
|
||||
if ("error" in atomicAmountForAsset) {
|
||||
return {
|
||||
status: 402,
|
||||
responseHeaders: { "Content-Type": "application/json" },
|
||||
responseBody: {
|
||||
x402Version,
|
||||
error: atomicAmountForAsset.error,
|
||||
accepts: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
const { maxAmountRequired, asset } = atomicAmountForAsset;
|
||||
|
||||
const paymentRequirements: RequestedPaymentRequirements[] = [];
|
||||
|
||||
const mappedNetwork = ChainIdToNetwork[chainId];
|
||||
paymentRequirements.push({
|
||||
scheme: "exact",
|
||||
network: mappedNetwork ? mappedNetwork : `eip155:${chainId}`,
|
||||
maxAmountRequired,
|
||||
resource: resourceUrl,
|
||||
description: description ?? "",
|
||||
mimeType: mimeType ?? "application/json",
|
||||
payTo: getAddress(facilitator.address), // always pay to the facilitator address first
|
||||
maxTimeoutSeconds: maxTimeoutSeconds ?? 86400,
|
||||
asset: getAddress(asset.address),
|
||||
outputSchema: {
|
||||
input: {
|
||||
type: "http",
|
||||
method,
|
||||
discoverable: discoverable ?? true,
|
||||
...inputSchema,
|
||||
},
|
||||
output: outputSchema,
|
||||
},
|
||||
extra: {
|
||||
recipientAddress: payTo, // input payTo is the final recipient address
|
||||
...((asset as ERC20TokenAmount["asset"]).eip712 ?? {}),
|
||||
},
|
||||
routeConfig,
|
||||
payTo,
|
||||
extraMetadata,
|
||||
});
|
||||
|
||||
// Check for payment header
|
||||
// Check for payment header, if none, return the payment requirements
|
||||
if (!paymentData) {
|
||||
return {
|
||||
status: 402,
|
||||
responseHeaders: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
responseBody: {
|
||||
x402Version,
|
||||
error: errorMessages?.paymentRequired || "X-PAYMENT header is required",
|
||||
accepts: paymentRequirements,
|
||||
},
|
||||
};
|
||||
return paymentRequirementsResult;
|
||||
}
|
||||
|
||||
const paymentRequirements = paymentRequirementsResult.responseBody.accepts;
|
||||
|
||||
// decode b64 payment
|
||||
let decodedPayment: RequestedPaymentPayload;
|
||||
try {
|
||||
@@ -163,8 +89,8 @@ export async function decodePaymentRequest(
|
||||
const selectedPaymentRequirements = paymentRequirements.find(
|
||||
(value) =>
|
||||
value.scheme === decodedPayment.scheme &&
|
||||
networkToChainId(value.network) ===
|
||||
networkToChainId(decodedPayment.network),
|
||||
networkToCaip2ChainId(value.network) ===
|
||||
networkToCaip2ChainId(decodedPayment.network),
|
||||
);
|
||||
if (!selectedPaymentRequirements) {
|
||||
return {
|
||||
@@ -190,86 +116,6 @@ export async function decodePaymentRequest(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the amount from the given price
|
||||
*
|
||||
* @param price - The price to parse
|
||||
* @param network - The network to get the default asset for
|
||||
* @returns The parsed amount or an error message
|
||||
*/
|
||||
async function processPriceToAtomicAmount(
|
||||
price: Money | ERC20TokenAmount,
|
||||
chainId: number,
|
||||
facilitator: ThirdwebX402Facilitator,
|
||||
): Promise<
|
||||
{ maxAmountRequired: string; asset: DefaultAsset } | { error: string }
|
||||
> {
|
||||
// Handle USDC amount (string) or token amount (ERC20TokenAmount)
|
||||
let maxAmountRequired: string;
|
||||
let asset: DefaultAsset;
|
||||
|
||||
if (typeof price === "string" || typeof price === "number") {
|
||||
// USDC amount in dollars
|
||||
const parsedAmount = moneySchema.safeParse(price);
|
||||
if (!parsedAmount.success) {
|
||||
return {
|
||||
error: `Invalid price (price: ${price}). Must be in the form "$3.10", 0.10, "0.001", ${parsedAmount.error}`,
|
||||
};
|
||||
}
|
||||
const parsedUsdAmount = parsedAmount.data;
|
||||
const defaultAsset = await getDefaultAsset(chainId, facilitator);
|
||||
if (!defaultAsset) {
|
||||
return {
|
||||
error: `Unable to get default asset on chain ${chainId}. Please specify an asset in the payment requirements.`,
|
||||
};
|
||||
}
|
||||
asset = defaultAsset;
|
||||
maxAmountRequired = toUnits(
|
||||
parsedUsdAmount.toString(),
|
||||
defaultAsset.decimals,
|
||||
).toString();
|
||||
} else {
|
||||
// Token amount in atomic units
|
||||
maxAmountRequired = price.amount;
|
||||
const tokenExtras = await getOrDetectTokenExtras({
|
||||
facilitator,
|
||||
partialAsset: price.asset,
|
||||
chainId,
|
||||
});
|
||||
if (!tokenExtras) {
|
||||
return {
|
||||
error: `Unable to find token information for ${price.asset.address} on chain ${chainId}. Please specify the asset decimals and eip712 information in the asset options.`,
|
||||
};
|
||||
}
|
||||
asset = {
|
||||
address: price.asset.address,
|
||||
decimals: tokenExtras.decimals,
|
||||
eip712: {
|
||||
name: tokenExtras.name,
|
||||
version: tokenExtras.version,
|
||||
primaryType: tokenExtras.primaryType,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
maxAmountRequired,
|
||||
asset,
|
||||
};
|
||||
}
|
||||
|
||||
async function getDefaultAsset(
|
||||
chainId: number,
|
||||
facilitator: ThirdwebX402Facilitator,
|
||||
): Promise<DefaultAsset | undefined> {
|
||||
const supportedAssets = await facilitator.supported();
|
||||
const matchingAsset = supportedAssets.kinds.find(
|
||||
(supported) => networkToChainId(supported.network) === chainId,
|
||||
);
|
||||
const assetConfig = matchingAsset?.extra?.defaultAsset as DefaultAsset;
|
||||
return assetConfig;
|
||||
}
|
||||
|
||||
export async function getSupportedSignatureType(args: {
|
||||
client: ThirdwebClient;
|
||||
asset: string;
|
||||
@@ -309,61 +155,3 @@ export async function getSupportedSignatureType(args: {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function getOrDetectTokenExtras(args: {
|
||||
facilitator: ThirdwebX402Facilitator;
|
||||
partialAsset: ERC20TokenAmount["asset"];
|
||||
chainId: number;
|
||||
}): Promise<
|
||||
| {
|
||||
name: string;
|
||||
version: string;
|
||||
decimals: number;
|
||||
primaryType: SupportedSignatureType;
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
const { facilitator, partialAsset, chainId } = args;
|
||||
if (
|
||||
partialAsset.eip712?.name &&
|
||||
partialAsset.eip712?.version &&
|
||||
partialAsset.decimals !== undefined
|
||||
) {
|
||||
return {
|
||||
name: partialAsset.eip712.name,
|
||||
version: partialAsset.eip712.version,
|
||||
decimals: partialAsset.decimals,
|
||||
primaryType: partialAsset.eip712.primaryType,
|
||||
};
|
||||
}
|
||||
// read from facilitator
|
||||
const response = await facilitator
|
||||
.supported({
|
||||
chainId,
|
||||
tokenAddress: partialAsset.address,
|
||||
})
|
||||
.catch(() => {
|
||||
return {
|
||||
kinds: [],
|
||||
};
|
||||
});
|
||||
|
||||
const exactScheme = response.kinds?.find((kind) => kind.scheme === "exact");
|
||||
if (!exactScheme) {
|
||||
return undefined;
|
||||
}
|
||||
const supportedAsset = exactScheme.extra?.supportedAssets?.find(
|
||||
(asset) =>
|
||||
asset.address.toLowerCase() === partialAsset.address.toLowerCase(),
|
||||
);
|
||||
if (!supportedAsset) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
name: supportedAsset.eip712.name,
|
||||
version: supportedAsset.eip712.version,
|
||||
decimals: supportedAsset.decimals,
|
||||
primaryType: supportedAsset.eip712.primaryType as SupportedSignatureType,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { ExactEvmPayload } from "x402/types";
|
||||
import {
|
||||
type RequestedPaymentPayload,
|
||||
RequestedPaymentPayloadSchema,
|
||||
} from "./schemas.js";
|
||||
import type { RequestedPaymentPayload } from "./schemas.js";
|
||||
|
||||
/**
|
||||
* Encodes a payment payload into a base64 string, ensuring bigint values are properly stringified
|
||||
@@ -44,8 +41,7 @@ export function decodePayment(payment: string): RequestedPaymentPayload {
|
||||
...parsed,
|
||||
payload: parsed.payload as ExactEvmPayload,
|
||||
};
|
||||
const validated = RequestedPaymentPayloadSchema.parse(obj);
|
||||
return validated;
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,13 +2,15 @@ import type { VerifyResponse } from "x402/types";
|
||||
import type { ThirdwebClient } from "../client/client.js";
|
||||
import { stringify } from "../utils/json.js";
|
||||
import { withCache } from "../utils/promise/withCache.js";
|
||||
import type {
|
||||
FacilitatorSettleResponse,
|
||||
FacilitatorSupportedResponse,
|
||||
FacilitatorVerifyResponse,
|
||||
RequestedPaymentPayload,
|
||||
RequestedPaymentRequirements,
|
||||
import {
|
||||
type FacilitatorSettleResponse,
|
||||
type FacilitatorSupportedResponse,
|
||||
type FacilitatorVerifyResponse,
|
||||
networkToCaip2ChainId,
|
||||
type RequestedPaymentPayload,
|
||||
type RequestedPaymentRequirements,
|
||||
} from "./schemas.js";
|
||||
import type { PaymentArgs, PaymentRequiredResult } from "./types.js";
|
||||
|
||||
export type WaitUntil = "simulated" | "submitted" | "confirmed";
|
||||
|
||||
@@ -46,6 +48,9 @@ export type ThirdwebX402Facilitator = {
|
||||
chainId: number;
|
||||
tokenAddress?: string;
|
||||
}) => Promise<FacilitatorSupportedResponse>;
|
||||
accepts: (
|
||||
args: Omit<PaymentArgs, "facilitator">,
|
||||
) => Promise<PaymentRequiredResult>;
|
||||
};
|
||||
|
||||
const DEFAULT_BASE_URL = "https://api.thirdweb.com/v1/payments/x402";
|
||||
@@ -256,6 +261,43 @@ export function facilitator(
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
async accepts(
|
||||
args: Omit<PaymentArgs, "facilitator">,
|
||||
): Promise<PaymentRequiredResult> {
|
||||
const url = config.baseUrl ?? DEFAULT_BASE_URL;
|
||||
let headers = { "Content-Type": "application/json" };
|
||||
const authHeaders = await facilitator.createAuthHeaders();
|
||||
headers = { ...headers, ...authHeaders.verify }; // same as verify
|
||||
const caip2ChainId = networkToCaip2ChainId(args.network);
|
||||
const res = await fetch(`${url}/accepts`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: stringify({
|
||||
resourceUrl: args.resourceUrl,
|
||||
method: args.method,
|
||||
network: caip2ChainId,
|
||||
price: args.price,
|
||||
routeConfig: args.routeConfig,
|
||||
serverWalletAddress: facilitator.address,
|
||||
recipientAddress: args.payTo,
|
||||
extraMetadata: args.extraMetadata,
|
||||
}),
|
||||
});
|
||||
if (res.status !== 402) {
|
||||
throw new Error(
|
||||
`Failed to construct payment requirements: ${res.statusText} - ${await res.text()}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
status: res.status as 402,
|
||||
responseBody:
|
||||
(await res.json()) as PaymentRequiredResult["responseBody"],
|
||||
responseHeaders: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return facilitator;
|
||||
|
||||
@@ -2,7 +2,8 @@ import { getCachedChain } from "../chains/utils.js";
|
||||
import type { ThirdwebClient } from "../client/client.js";
|
||||
import type { Wallet } from "../wallets/interfaces/wallet.js";
|
||||
import {
|
||||
networkToChainId,
|
||||
extractEvmChainId,
|
||||
networkToCaip2ChainId,
|
||||
type RequestedPaymentRequirements,
|
||||
RequestedPaymentRequirementsSchema,
|
||||
} from "./schemas.js";
|
||||
@@ -51,7 +52,7 @@ export function wrapFetchWithPayment(
|
||||
fetch: typeof globalThis.fetch,
|
||||
client: ThirdwebClient,
|
||||
wallet: Wallet,
|
||||
maxValue: bigint = BigInt(1 * 10 ** 6), // Default to 1 USDC
|
||||
maxValue?: bigint,
|
||||
) {
|
||||
return async (input: RequestInfo, init?: RequestInit) => {
|
||||
const response = await fetch(input, init);
|
||||
@@ -90,15 +91,25 @@ export function wrapFetchWithPayment(
|
||||
);
|
||||
}
|
||||
|
||||
if (BigInt(selectedPaymentRequirements.maxAmountRequired) > maxValue) {
|
||||
if (
|
||||
maxValue &&
|
||||
BigInt(selectedPaymentRequirements.maxAmountRequired) > maxValue
|
||||
) {
|
||||
throw new Error(
|
||||
`Payment amount exceeds maximum allowed (currently set to ${maxValue} in base units)`,
|
||||
);
|
||||
}
|
||||
|
||||
const paymentChainId = networkToChainId(
|
||||
const caip2ChainId = networkToCaip2ChainId(
|
||||
selectedPaymentRequirements.network,
|
||||
);
|
||||
const paymentChainId = extractEvmChainId(caip2ChainId);
|
||||
// TODO (402): support solana
|
||||
if (paymentChainId === null) {
|
||||
throw new Error(
|
||||
`Unsupported chain ID: ${selectedPaymentRequirements.network}`,
|
||||
);
|
||||
}
|
||||
|
||||
// switch to the payment chain if it's not the current chain
|
||||
if (paymentChainId !== chain.id) {
|
||||
@@ -150,7 +161,9 @@ function defaultPaymentRequirementsSelector(
|
||||
}
|
||||
// find the payment requirements matching the connected wallet chain
|
||||
const matchingPaymentRequirements = paymentRequirements.find(
|
||||
(x) => networkToChainId(x.network) === chainId && x.scheme === scheme,
|
||||
(x) =>
|
||||
extractEvmChainId(networkToCaip2ChainId(x.network)) === chainId &&
|
||||
x.scheme === scheme,
|
||||
);
|
||||
|
||||
if (matchingPaymentRequirements) {
|
||||
|
||||
@@ -15,7 +15,7 @@ const FacilitatorNetworkSchema = z.string();
|
||||
|
||||
export type FacilitatorNetwork = z.infer<typeof FacilitatorNetworkSchema>;
|
||||
|
||||
export const RequestedPaymentPayloadSchema = PaymentPayloadSchema.extend({
|
||||
const RequestedPaymentPayloadSchema = PaymentPayloadSchema.extend({
|
||||
network: FacilitatorNetworkSchema,
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ export const SupportedSignatureTypeSchema = z.enum([
|
||||
"Permit",
|
||||
]);
|
||||
|
||||
export const FacilitatorSupportedAssetSchema = z.object({
|
||||
const FacilitatorSupportedAssetSchema = z.object({
|
||||
address: z.string(),
|
||||
decimals: z.number(),
|
||||
eip712: z.object({
|
||||
@@ -92,25 +92,95 @@ export type FacilitatorSupportedResponse = z.infer<
|
||||
typeof FacilitatorSupportedResponseSchema
|
||||
>;
|
||||
|
||||
export function networkToChainId(network: string | Chain): number {
|
||||
if (typeof network === "object") {
|
||||
return network.id;
|
||||
}
|
||||
if (network.startsWith("eip155:")) {
|
||||
const chainId = parseInt(network.split(":")[1] ?? "0");
|
||||
if (!Number.isNaN(chainId) && chainId > 0) {
|
||||
return chainId;
|
||||
} else {
|
||||
throw new Error(`Invalid network: ${network}`);
|
||||
}
|
||||
}
|
||||
const mappedChainId = EvmNetworkToChainId.get(network as Network);
|
||||
if (!mappedChainId) {
|
||||
throw new Error(`Invalid network: ${network}`);
|
||||
}
|
||||
// TODO (402): support solana networks
|
||||
if (mappedChainId === 101 || mappedChainId === 103) {
|
||||
throw new Error("Solana networks not supported yet.");
|
||||
}
|
||||
return mappedChainId;
|
||||
function isEvmChain(caip2ChainId: Caip2ChainId): boolean {
|
||||
return caip2ChainId.startsWith("eip155:");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract numeric chain ID from CAIP-2 EVM chain (e.g., "eip155:1" -> 1)
|
||||
*/
|
||||
export function extractEvmChainId(caip2ChainId: Caip2ChainId): number | null {
|
||||
if (!isEvmChain(caip2ChainId)) {
|
||||
return null;
|
||||
}
|
||||
const parts = caip2ChainId.split(":");
|
||||
const chainId = Number(parts[1]);
|
||||
return Number.isNaN(chainId) ? null : chainId;
|
||||
}
|
||||
|
||||
/**
|
||||
* CAIP-2 compliant blockchain identifier
|
||||
* @see https://chainagnostic.org/CAIPs/caip-2
|
||||
*/
|
||||
const Caip2ChainIdSchema = z
|
||||
.union([z.string(), z.number().int().positive()])
|
||||
.transform((value, ctx) => {
|
||||
// Handle proper CAIP-2 format (already valid)
|
||||
if (typeof value === "string" && value.includes(":")) {
|
||||
const [namespace, reference] = value.split(":");
|
||||
|
||||
// Solana mainnet/devnet aliases
|
||||
if (namespace === "solana" && reference === "mainnet") {
|
||||
return "solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ" as const;
|
||||
}
|
||||
if (namespace === "solana" && reference === "devnet") {
|
||||
return "solana:8E9rvCKLFQia2Y35HXjjpWzj8weVo44K" as const;
|
||||
}
|
||||
|
||||
// Validate CAIP-2 format
|
||||
const namespaceRegex = /^[-a-z0-9]{3,8}$/;
|
||||
const referenceRegex = /^[-_a-zA-Z0-9]{1,32}$/;
|
||||
|
||||
if (!namespaceRegex.test(namespace ?? "")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Invalid CAIP-2 namespace: ${namespace}. Must match [-a-z0-9]{3,8}`,
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
|
||||
if (!referenceRegex.test(reference ?? "")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Invalid CAIP-2 reference: ${reference}. Must match [-_a-zA-Z0-9]{1,32}`,
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
|
||||
return value as `${string}:${string}`;
|
||||
}
|
||||
|
||||
// Handle number (EVM chain ID fallback)
|
||||
if (typeof value === "number") {
|
||||
return `eip155:${value}` as const;
|
||||
}
|
||||
|
||||
// Handle string number (EVM chain ID fallback)
|
||||
const numValue = Number(value);
|
||||
if (!Number.isNaN(numValue) && Number.isInteger(numValue) && numValue > 0) {
|
||||
return `eip155:${numValue}` as const;
|
||||
}
|
||||
|
||||
const mappedChainId = EvmNetworkToChainId.get(value as Network);
|
||||
if (mappedChainId) {
|
||||
return `eip155:${mappedChainId}` as const;
|
||||
}
|
||||
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Invalid chain ID: ${value}. Must be a CAIP-2 identifier (e.g., "eip155:1", "solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ"), a numeric chain ID for EVM, or "solana:mainnet"/"solana:devnet"`,
|
||||
});
|
||||
return z.NEVER;
|
||||
})
|
||||
.describe(
|
||||
"CAIP-2 blockchain identifier (e.g., 'eip155:1' for Ethereum, 'solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ' for Solana mainnet). Also accepts numeric EVM chain IDs (e.g., 1, 137) or aliases ('solana:mainnet', 'solana:devnet') for backward compatibility.",
|
||||
);
|
||||
|
||||
type Caip2ChainId = z.output<typeof Caip2ChainIdSchema>;
|
||||
|
||||
export function networkToCaip2ChainId(network: string | Chain): Caip2ChainId {
|
||||
if (typeof network === "object") {
|
||||
return `eip155:${network.id}` as const;
|
||||
}
|
||||
return Caip2ChainIdSchema.parse(network);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ import type { Account } from "../wallets/interfaces/wallet.js";
|
||||
import { getSupportedSignatureType } from "./common.js";
|
||||
import { encodePayment } from "./encode.js";
|
||||
import {
|
||||
networkToChainId,
|
||||
extractEvmChainId,
|
||||
networkToCaip2ChainId,
|
||||
type RequestedPaymentPayload,
|
||||
type RequestedPaymentRequirements,
|
||||
type UnsignedPaymentPayload,
|
||||
@@ -71,7 +72,14 @@ async function signPaymentHeader(
|
||||
x402Version: number,
|
||||
): Promise<RequestedPaymentPayload> {
|
||||
const from = getAddress(account.address);
|
||||
const chainId = networkToChainId(paymentRequirements.network);
|
||||
const caip2ChainId = networkToCaip2ChainId(paymentRequirements.network);
|
||||
const chainId = extractEvmChainId(caip2ChainId);
|
||||
|
||||
// TODO (402): support solana
|
||||
if (chainId === null) {
|
||||
throw new Error(`Unsupported chain ID: ${paymentRequirements.network}`);
|
||||
}
|
||||
|
||||
const supportedSignatureType = await getSupportedSignatureType({
|
||||
client,
|
||||
asset: paymentRequirements.asset,
|
||||
@@ -191,7 +199,10 @@ async function signERC3009Authorization(
|
||||
}: ExactEvmPayloadAuthorization,
|
||||
{ asset, network, extra }: RequestedPaymentRequirements,
|
||||
): Promise<{ signature: Hex }> {
|
||||
const chainId = networkToChainId(network);
|
||||
const chainId = extractEvmChainId(networkToCaip2ChainId(network));
|
||||
if (chainId === null) {
|
||||
throw new Error(`Unsupported chain ID: ${network}`);
|
||||
}
|
||||
const name = extra?.name;
|
||||
const version = extra?.version;
|
||||
|
||||
@@ -233,7 +244,10 @@ async function signERC2612Permit(
|
||||
{ from, to, value, validBefore, nonce }: ExactEvmPayloadAuthorization,
|
||||
{ asset, network, extra }: RequestedPaymentRequirements,
|
||||
): Promise<{ signature: Hex }> {
|
||||
const chainId = networkToChainId(network);
|
||||
const chainId = extractEvmChainId(networkToCaip2ChainId(network));
|
||||
if (chainId === null) {
|
||||
throw new Error(`Unsupported chain ID: ${network}`);
|
||||
}
|
||||
const name = extra?.name;
|
||||
const version = extra?.version;
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { ThirdwebX402Facilitator, WaitUntil } from "./facilitator.js";
|
||||
import type {
|
||||
FacilitatorNetwork,
|
||||
FacilitatorSettleResponse,
|
||||
FacilitatorSupportedAssetSchema,
|
||||
RequestedPaymentPayload,
|
||||
RequestedPaymentRequirements,
|
||||
SupportedSignatureTypeSchema,
|
||||
@@ -36,6 +35,8 @@ export type PaymentArgs = {
|
||||
routeConfig?: PaymentMiddlewareConfig;
|
||||
/** Optional recipient address to receive the payment if different from your facilitator address */
|
||||
payTo?: string;
|
||||
/** Optional extra data to be included in the payment request */
|
||||
extraMetadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type SettlePaymentArgs = PaymentArgs & {
|
||||
@@ -110,5 +111,3 @@ export type ERC20TokenAmount = {
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type DefaultAsset = z.infer<typeof FacilitatorSupportedAssetSchema>;
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @thirdweb-dev/wagmi-adapter
|
||||
|
||||
## 0.2.181
|
||||
|
||||
## 0.2.180
|
||||
|
||||
## 0.2.179
|
||||
|
||||
## 0.2.178
|
||||
|
||||
## 0.2.177
|
||||
|
||||
## 0.2.176
|
||||
|
||||
## 0.2.175
|
||||
|
||||
## 0.2.174
|
||||
|
||||
## 0.2.173
|
||||
|
||||
@@ -56,5 +56,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.174"
|
||||
"version": "0.2.181"
|
||||
}
|
||||
|
||||
Generated
+107
-111
Reference in New Issue
Block a user