Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0ace736a8 | ||
|
|
b30369f3cc | ||
|
|
f2121e9519 | ||
|
|
29d05a50bf | ||
|
|
f00f559a12 | ||
|
|
ceba6835dd | ||
|
|
b33c1a30ac | ||
|
|
72fbb1550f | ||
|
|
1e7df5ad3d | ||
|
|
ca16ec783b | ||
|
|
b1a780832e | ||
|
|
9bb65ad5b2 | ||
|
|
db291d2d38 | ||
|
|
9dd0b17f15 | ||
|
|
12082fd5fd | ||
|
|
5ff6063961 | ||
|
|
3db3982fab | ||
|
|
bf90104330 |
@@ -1,30 +0,0 @@
|
||||
import { defineConfig } from "checkly";
|
||||
import { Frequency } from "checkly/constructs";
|
||||
|
||||
export default defineConfig({
|
||||
checks: {
|
||||
activated: true,
|
||||
browserChecks: {
|
||||
frequency: Frequency.EVERY_24H,
|
||||
testMatch: "./tests/**/*.spec.ts",
|
||||
},
|
||||
checkMatch: "./**/*.check.ts",
|
||||
frequency: Frequency.EVERY_24H,
|
||||
ignoreDirectoriesMatch: [],
|
||||
locations: ["us-east-1", "eu-west-1"],
|
||||
muted: false,
|
||||
playwrightConfig: {
|
||||
use: {
|
||||
baseURL: process.env.ENVIRONMENT_URL || "https://thirdweb.com",
|
||||
},
|
||||
},
|
||||
runtimeId: "2023.09",
|
||||
tags: ["website"],
|
||||
},
|
||||
cli: {
|
||||
runLocation: "eu-west-1",
|
||||
},
|
||||
logicalId: "thirdweb-www",
|
||||
projectName: "thirdweb.com",
|
||||
repoUrl: "https://github.com/thirdweb-dev/dashboard",
|
||||
});
|
||||
@@ -11,6 +11,7 @@ module.exports = [
|
||||
"/account-abstraction",
|
||||
"/payments",
|
||||
"/payments/x402",
|
||||
"/nexus",
|
||||
"/auth",
|
||||
"/in-app-wallets",
|
||||
"/transactions",
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"@chromatic-com/storybook": "4.0.1",
|
||||
"@next/bundle-analyzer": "15.3.5",
|
||||
"@next/eslint-plugin-next": "15.3.5",
|
||||
"@playwright/test": "1.53.2",
|
||||
"@storybook/addon-docs": "9.0.15",
|
||||
"@storybook/addon-links": "9.0.15",
|
||||
"@storybook/addon-onboarding": "9.0.15",
|
||||
@@ -99,7 +98,6 @@
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"checkly": "6.0.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||
@@ -122,13 +120,11 @@
|
||||
"format": "biome format ./src --write",
|
||||
"knip": "knip",
|
||||
"lint": "biome check ./src && knip && eslint ./src",
|
||||
"playwright": "playwright test",
|
||||
"postbuild": "next-sitemap",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"start": "next start",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"update-checkly": "npx checkly deploy"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"version": "3.0.0"
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
|
||||
{
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
|
||||
{
|
||||
name: "webkit",
|
||||
use: { ...devices["Desktop Safari"] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
testDir: "./tests",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: process.env.ENVIRONMENT_URL || "http://127.0.0.1:3000",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: process.env.CI
|
||||
? undefined
|
||||
: {
|
||||
command: "pnpm run start",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
url: "http://127.0.0.1:3000",
|
||||
},
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
});
|
||||
@@ -683,7 +683,7 @@ export function reportProductFeedback(properties: {
|
||||
*
|
||||
*/
|
||||
export function reportBridgePageLinkClick(params: {
|
||||
linkType: "integrate-bridge" | "trending-tokens";
|
||||
linkType: "bridge-docs" | "trending-tokens";
|
||||
}) {
|
||||
posthog.capture("bridge page link clicked", params);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ const CHAIN_PLAN_TO_INFRA = {
|
||||
],
|
||||
};
|
||||
|
||||
export async function getChainSubscriptions(slug: string) {
|
||||
async function getChainSubscriptions(slug: string) {
|
||||
const allSubscriptions = await getTeamSubscriptions(slug);
|
||||
if (!allSubscriptions) {
|
||||
return null;
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function getUniversalBridgeTokens(props: {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
|
||||
} as Record<string, string>,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function addUniversalBridgeTokenRoute(props: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.project.publishableKey,
|
||||
} as Record<string, string>,
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { Chain } from "thirdweb";
|
||||
import { defineChain } from "thirdweb";
|
||||
import { BuyWidget, SwapWidget } from "thirdweb/react";
|
||||
import type { Wallet } from "thirdweb/wallets";
|
||||
import {
|
||||
reportAssetBuyCancelled,
|
||||
reportAssetBuyFailed,
|
||||
@@ -25,17 +27,46 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { parseError } from "@/utils/errorParser";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
import { appMetadata } from "../../constants/connect";
|
||||
import { getConfiguredThirdwebClient } from "../../constants/thirdweb.server";
|
||||
|
||||
type PageType = "asset" | "bridge" | "chain";
|
||||
|
||||
export function BuyAndSwapEmbed(props: {
|
||||
chain: Chain;
|
||||
tokenAddress: string | undefined;
|
||||
buyAmount: string | undefined;
|
||||
export type BuyAndSwapEmbedProps = {
|
||||
buyTab:
|
||||
| {
|
||||
buyToken:
|
||||
| {
|
||||
tokenAddress: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
| undefined;
|
||||
swapTab:
|
||||
| {
|
||||
sellToken:
|
||||
| {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
amount?: string;
|
||||
}
|
||||
| undefined;
|
||||
buyToken:
|
||||
| {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
amount?: string;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
| undefined;
|
||||
pageType: PageType;
|
||||
isTestnet: boolean | undefined;
|
||||
}) {
|
||||
wallets?: Wallet[];
|
||||
};
|
||||
|
||||
export function BuyAndSwapEmbed(props: BuyAndSwapEmbedProps) {
|
||||
const { theme } = useTheme();
|
||||
const [tab, setTab] = useState<"buy" | "swap">("swap");
|
||||
const themeObj = getSDKTheme(theme === "light" ? "light" : "dark");
|
||||
@@ -84,24 +115,39 @@ export function BuyAndSwapEmbed(props: {
|
||||
|
||||
{tab === "buy" && (
|
||||
<BuyWidget
|
||||
amount={props.buyAmount || "1"}
|
||||
chain={props.chain}
|
||||
amount={props.buyTab?.buyToken?.amount || "1"}
|
||||
chain={
|
||||
props.buyTab?.buyToken?.chainId
|
||||
? defineChain(props.buyTab.buyToken.chainId)
|
||||
: undefined
|
||||
}
|
||||
tokenAddress={
|
||||
props.buyTab?.buyToken?.tokenAddress as `0x${string}` | undefined
|
||||
}
|
||||
className="!rounded-2xl !border-none"
|
||||
title=""
|
||||
client={client}
|
||||
connectOptions={{
|
||||
autoConnect: false,
|
||||
wallets: props.wallets,
|
||||
appMetadata: appMetadata,
|
||||
}}
|
||||
onError={(e, quote) => {
|
||||
const errorMessage = parseError(e);
|
||||
|
||||
const buyChainId =
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined;
|
||||
|
||||
if (!buyChainId) {
|
||||
return;
|
||||
}
|
||||
|
||||
reportTokenBuyFailed({
|
||||
buyTokenChainId:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined,
|
||||
buyTokenChainId: buyChainId,
|
||||
buyTokenAddress:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationTokenAddress
|
||||
@@ -114,21 +160,27 @@ export function BuyAndSwapEmbed(props: {
|
||||
if (props.pageType === "asset") {
|
||||
reportAssetBuyFailed({
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
chainId: buyChainId,
|
||||
error: errorMessage,
|
||||
contractType: undefined,
|
||||
is_testnet: props.isTestnet,
|
||||
is_testnet: false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onCancel={(quote) => {
|
||||
const buyChainId =
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined;
|
||||
|
||||
if (!buyChainId) {
|
||||
return;
|
||||
}
|
||||
|
||||
reportTokenBuyCancelled({
|
||||
buyTokenChainId:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined,
|
||||
buyTokenChainId: buyChainId,
|
||||
buyTokenAddress:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationTokenAddress
|
||||
@@ -141,24 +193,30 @@ export function BuyAndSwapEmbed(props: {
|
||||
if (props.pageType === "asset") {
|
||||
reportAssetBuyCancelled({
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
chainId: buyChainId,
|
||||
contractType: undefined,
|
||||
is_testnet: props.isTestnet,
|
||||
is_testnet: false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onSuccess={({ quote }) => {
|
||||
const buyChainId =
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined;
|
||||
|
||||
if (!buyChainId) {
|
||||
return;
|
||||
}
|
||||
|
||||
reportTokenBuySuccessful({
|
||||
buyTokenChainId:
|
||||
quote.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined,
|
||||
buyTokenChainId: buyChainId,
|
||||
buyTokenAddress:
|
||||
quote.type === "buy"
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationTokenAddress
|
||||
: quote.type === "onramp"
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.tokenAddress
|
||||
: undefined,
|
||||
pageType: props.pageType,
|
||||
@@ -167,14 +225,13 @@ export function BuyAndSwapEmbed(props: {
|
||||
if (props.pageType === "asset") {
|
||||
reportAssetBuySuccessful({
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
chainId: buyChainId,
|
||||
contractType: undefined,
|
||||
is_testnet: props.isTestnet,
|
||||
is_testnet: false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
theme={themeObj}
|
||||
tokenAddress={props.tokenAddress as `0x${string}`}
|
||||
paymentMethods={["card"]}
|
||||
/>
|
||||
)}
|
||||
@@ -184,18 +241,14 @@ export function BuyAndSwapEmbed(props: {
|
||||
client={client}
|
||||
theme={themeObj}
|
||||
className="!rounded-2xl !border-none"
|
||||
connectOptions={{
|
||||
autoConnect: false,
|
||||
wallets: props.wallets,
|
||||
appMetadata: appMetadata,
|
||||
}}
|
||||
prefill={{
|
||||
// buy this token by default
|
||||
buyToken: {
|
||||
chainId: props.chain.id,
|
||||
tokenAddress: props.tokenAddress,
|
||||
},
|
||||
// sell the native token by default (but if buytoken is a native token, don't set)
|
||||
sellToken: props.tokenAddress
|
||||
? {
|
||||
chainId: props.chain.id,
|
||||
}
|
||||
: undefined,
|
||||
buyToken: props.swapTab?.buyToken,
|
||||
sellToken: props.swapTab?.sellToken,
|
||||
}}
|
||||
onError={(error, quote) => {
|
||||
const errorMessage = parseError(error);
|
||||
|
||||
@@ -5,25 +5,20 @@ import { Button } from "@/components/ui/button";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function FaqSection(props: {
|
||||
export function FaqAccordion(props: {
|
||||
faqs: Array<{ title: string; description: string }>;
|
||||
}) {
|
||||
return (
|
||||
<section>
|
||||
<h2 className="text-2xl md:text-3xl font-semibold mb-4 tracking-tight">
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<div className="flex flex-col">
|
||||
{props.faqs.map((faq, faqIndex) => (
|
||||
<FaqItem
|
||||
key={faq.title}
|
||||
title={faq.title}
|
||||
description={faq.description}
|
||||
className={cn(faqIndex === props.faqs.length - 1 && "border-b-0")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<div className="flex flex-col">
|
||||
{props.faqs.map((faq, faqIndex) => (
|
||||
<FaqItem
|
||||
key={faq.title}
|
||||
title={faq.title}
|
||||
description={faq.description}
|
||||
className={cn(faqIndex === props.faqs.length - 1 && "border-b-0")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
import type { SocialProfile } from "thirdweb/react";
|
||||
import { BadgeContainer, storybookThirdwebClient } from "@/storybook/utils";
|
||||
import { WalletAddress, WalletAddressUI } from "./wallet-address";
|
||||
|
||||
const meta = {
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
title: "blocks/WalletAddress",
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
const client = storybookThirdwebClient as unknown as ThirdwebClient;
|
||||
|
||||
return (
|
||||
<div className="container flex max-w-4xl flex-col gap-8 py-10">
|
||||
<BadgeContainer label="Social Profiles Loaded">
|
||||
<WalletAddressUI
|
||||
address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
|
||||
client={client}
|
||||
profiles={{
|
||||
data: vitalikEth,
|
||||
isPending: false,
|
||||
}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No Social Profiles">
|
||||
<WalletAddressUI
|
||||
address="0x83Dd93fA5D8343094f850f90B3fb90088C1bB425"
|
||||
client={client}
|
||||
shortenAddress
|
||||
profiles={{
|
||||
data: [],
|
||||
isPending: false,
|
||||
}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Loading Social Profiles">
|
||||
<WalletAddressUI
|
||||
address="0x83Dd93fA5D8343094f850f90B3fb90088C1bB425"
|
||||
client={client}
|
||||
shortenAddress
|
||||
profiles={{
|
||||
data: [],
|
||||
isPending: true,
|
||||
}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Zero address">
|
||||
<WalletAddressUI
|
||||
address={ZERO_ADDRESS}
|
||||
client={client}
|
||||
profiles={{
|
||||
data: [],
|
||||
isPending: false,
|
||||
}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Real Component - Invalid Address">
|
||||
<WalletAddress address="not-an-address" client={client} />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Real Component - vitalik.eth">
|
||||
<WalletAddress
|
||||
address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const vitalikEth: SocialProfile[] = [
|
||||
{
|
||||
type: "ens",
|
||||
name: "vitalik.eth",
|
||||
bio: "mi pinxe lo crino tcati",
|
||||
avatar: "https://euc.li/vitalik.eth",
|
||||
metadata: {
|
||||
name: "vitalik.eth",
|
||||
address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
|
||||
avatar: "https://euc.li/vitalik.eth",
|
||||
description: "mi pinxe lo crino tcati",
|
||||
url: "https://vitalik.ca",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "farcaster",
|
||||
name: "vitalik.eth",
|
||||
bio: undefined,
|
||||
avatar:
|
||||
"https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b663cd63-fecf-4d0f-7f87-0e0b6fd42800/original",
|
||||
metadata: {
|
||||
fid: 5650,
|
||||
bio: undefined,
|
||||
pfp: "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b663cd63-fecf-4d0f-7f87-0e0b6fd42800/original",
|
||||
username: "vitalik.eth",
|
||||
addresses: [
|
||||
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
|
||||
"0x96B6bB2bd2Eba3b4Fbefd7DbAC448ad7B6CBf279",
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,11 +1,10 @@
|
||||
"use client";
|
||||
import { CheckIcon, CircleSlashIcon, CopyIcon, XIcon } from "lucide-react";
|
||||
import { CircleSlashIcon, XIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
import { Blobbie, type SocialProfile, useSocialProfiles } from "thirdweb/react";
|
||||
import { Img } from "@/components/blocks/Img";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
HoverCard,
|
||||
@@ -13,22 +12,49 @@ import {
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useClipboard } from "@/hooks/useClipboard";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { CopyTextButton } from "../ui/CopyTextButton";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
|
||||
export function WalletAddress(props: {
|
||||
type WalletAddressProps = {
|
||||
address: string | undefined;
|
||||
shortenAddress?: boolean;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
client: ThirdwebClient;
|
||||
fallbackIcon?: React.ReactNode;
|
||||
}) {
|
||||
};
|
||||
|
||||
export function WalletAddress(props: WalletAddressProps) {
|
||||
const profiles = useSocialProfiles({
|
||||
address: props.address,
|
||||
client: props.client,
|
||||
});
|
||||
|
||||
return (
|
||||
<WalletAddressUI
|
||||
{...props}
|
||||
profiles={{
|
||||
data: profiles.data || [],
|
||||
isPending: profiles.isPending,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function WalletAddressUI(
|
||||
props: WalletAddressProps & {
|
||||
profiles: {
|
||||
data: SocialProfile[];
|
||||
isPending: boolean;
|
||||
};
|
||||
},
|
||||
) {
|
||||
// default back to zero address if no address provided
|
||||
const address = useMemo(() => props.address || ZERO_ADDRESS, [props.address]);
|
||||
|
||||
const [shortenedAddress, lessShortenedAddress] = useMemo(() => {
|
||||
const [shortenedAddress, _lessShortenedAddress] = useMemo(() => {
|
||||
return [
|
||||
props.shortenAddress !== false
|
||||
? `${address.slice(0, 6)}...${address.slice(-4)}`
|
||||
@@ -37,17 +63,10 @@ export function WalletAddress(props: {
|
||||
];
|
||||
}, [address, props.shortenAddress]);
|
||||
|
||||
const profiles = useSocialProfiles({
|
||||
address: address,
|
||||
client: props.client,
|
||||
});
|
||||
|
||||
const { onCopy, hasCopied } = useClipboard(address, 2000);
|
||||
|
||||
if (!isAddress(address)) {
|
||||
return (
|
||||
<ToolTipLabel hoverable label={address}>
|
||||
<span className="flex items-center gap-2 underline-offset-4 hover:underline">
|
||||
<span className="flex items-center gap-2 underline-offset-4 hover:underline w-fit">
|
||||
<div className="flex size-5 items-center justify-center rounded-full border bg-background">
|
||||
<XIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
@@ -78,7 +97,7 @@ export function WalletAddress(props: {
|
||||
<HoverCardTrigger asChild tabIndex={-1}>
|
||||
<Button
|
||||
className={cn(
|
||||
"flex flex-row items-center gap-2 px-0",
|
||||
"flex flex-row items-center gap-2 px-0 max-w-full truncate",
|
||||
props.className,
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -88,89 +107,82 @@ export function WalletAddress(props: {
|
||||
<WalletAvatar
|
||||
address={address}
|
||||
iconClassName={props.iconClassName}
|
||||
profiles={profiles.data || []}
|
||||
profiles={props.profiles.data}
|
||||
thirdwebClient={props.client}
|
||||
fallbackIcon={props.fallbackIcon}
|
||||
/>
|
||||
)}
|
||||
<span className="cursor-pointer font-mono">
|
||||
{profiles.data?.[0]?.name || shortenedAddress}
|
||||
<span className="cursor-pointer font-mono max-w-full truncate">
|
||||
{props.profiles.data?.[0]?.name || shortenedAddress}
|
||||
</span>
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
className="w-80 border-border"
|
||||
className="w-[calc(100vw-2rem)] lg:w-[450px] border-border rounded-xl p-4 lg:p-6"
|
||||
onClick={(e) => {
|
||||
// do not close the hover card when clicking anywhere in the content
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-lg">Wallet Address</h3>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={onCopy}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
)}
|
||||
{hasCopied ? "Copied!" : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="rounded bg-muted p-2 text-center font-mono text-sm">
|
||||
{lessShortenedAddress}
|
||||
</p>
|
||||
<h3 className="font-semibold text-lg">Social Profiles</h3>
|
||||
{profiles.isPending ? (
|
||||
<p className="text-muted-foreground text-sm">Loading profiles...</p>
|
||||
) : !profiles.data?.length ? (
|
||||
<p className="text-muted-foreground text-sm">No profiles found</p>
|
||||
) : (
|
||||
profiles.data?.map((profile) => {
|
||||
const walletAvatarLink = resolveSchemeWithErrorHandler({
|
||||
client: props.client,
|
||||
uri: profile.avatar,
|
||||
});
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-medium text-sm">Wallet Address</h3>
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row items-center gap-2"
|
||||
key={profile.type + profile.name}
|
||||
>
|
||||
{walletAvatarLink && (
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
alt={profile.name}
|
||||
className="object-cover"
|
||||
src={walletAvatarLink}
|
||||
/>
|
||||
{profile.name && (
|
||||
<AvatarFallback>
|
||||
{profile.name.slice(0, 2)}
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
)}
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<div className="flex w-full flex-row items-center justify-between gap-4">
|
||||
<h4 className="font-semibold text-md">{profile.name}</h4>
|
||||
<Badge variant="outline">{profile.type}</Badge>
|
||||
<CopyTextButton
|
||||
textToShow={address}
|
||||
textToCopy={address}
|
||||
tooltip="Copy address"
|
||||
copyIconPosition="right"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground -translate-x-1.5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 border-t pt-5 border-dashed">
|
||||
<h3 className="font-medium text-sm">Social Profiles</h3>
|
||||
|
||||
{props.profiles.isPending ? (
|
||||
<Skeleton className="h-4 w-[50%]" />
|
||||
) : !props.profiles.data?.length ? (
|
||||
<p className="text-muted-foreground text-sm">No profiles found</p>
|
||||
) : (
|
||||
<div className="!mt-2">
|
||||
{props.profiles.data?.map((profile) => {
|
||||
const walletAvatarLink = resolveSchemeWithErrorHandler({
|
||||
client: props.client,
|
||||
uri: profile.avatar,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row items-center gap-3 py-2"
|
||||
key={profile.type + profile.name}
|
||||
>
|
||||
<Avatar className="size-9">
|
||||
<AvatarImage
|
||||
alt={profile.name}
|
||||
className="object-cover"
|
||||
src={walletAvatarLink}
|
||||
/>
|
||||
{profile.name && (
|
||||
<AvatarFallback>
|
||||
{profile.name.slice(0, 2)}
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<h4 className="text-sm leading-none">{profile.name}</h4>
|
||||
<span className="text-muted-foreground text-xs leading-none capitalize">
|
||||
{profile.type === "ens" ? "ENS" : profile.type}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{profile.bio && (
|
||||
<p className="line-clamp-1 whitespace-normal text-muted-foreground text-sm">
|
||||
{profile.bio}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { resetAnalytics } from "@/analytics/reset";
|
||||
import { CustomChainRenderer } from "@/components/misc/CustomChainRenderer";
|
||||
import { LazyConfigureNetworkModal } from "@/components/misc/configure-networks/LazyConfigureNetworkModal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { appMetadata } from "@/constants/connect";
|
||||
import { popularChains } from "@/constants/popularChains";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { useFavoriteChainIds } from "@/hooks/favorite-chains";
|
||||
@@ -138,11 +139,7 @@ export const CustomConnectWallet = (props: {
|
||||
return (
|
||||
<>
|
||||
<ConnectButton
|
||||
appMetadata={{
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
name: "thirdweb",
|
||||
url: "https://thirdweb.com",
|
||||
}}
|
||||
appMetadata={appMetadata}
|
||||
chain={props.chain}
|
||||
chains={allChainsV5}
|
||||
client={client}
|
||||
@@ -280,11 +277,7 @@ export function useCustomConnectModal() {
|
||||
return useCallback(
|
||||
(options: { chain?: Chain; client: ThirdwebClient }) => {
|
||||
return connect({
|
||||
appMetadata: {
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
name: "thirdweb",
|
||||
url: "https://thirdweb.com",
|
||||
},
|
||||
appMetadata,
|
||||
chain: options?.chain,
|
||||
client: options.client,
|
||||
privacyPolicyUrl: "/privacy-policy",
|
||||
|
||||
+12
-2
@@ -14,6 +14,15 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import type { SearchType } from "./types";
|
||||
|
||||
const searchTypeLabels: Record<SearchType, string> = {
|
||||
email: "Email",
|
||||
phone: "Phone",
|
||||
id: "Auth Identifier",
|
||||
address: "Address",
|
||||
externalWallet: "External Wallet",
|
||||
userId: "User Identifier",
|
||||
};
|
||||
|
||||
export function AdvancedSearchInput(props: {
|
||||
onSearch: (searchType: SearchType, query: string) => void;
|
||||
onClear: () => void;
|
||||
@@ -52,7 +61,8 @@ export function AdvancedSearchInput(props: {
|
||||
<SelectContent>
|
||||
<SelectItem value="email">Email</SelectItem>
|
||||
<SelectItem value="phone">Phone</SelectItem>
|
||||
<SelectItem value="id">ID</SelectItem>
|
||||
<SelectItem value="id">Auth Identifier</SelectItem>
|
||||
<SelectItem value="userId">User Identifier</SelectItem>
|
||||
<SelectItem value="address">Address</SelectItem>
|
||||
<SelectItem value="externalWallet">External Wallet</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -62,7 +72,7 @@ export function AdvancedSearchInput(props: {
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
className="bg-background pl-9 border-r-0 rounded-r-none rounded-l-full"
|
||||
placeholder={`Search by ${searchType}...`}
|
||||
placeholder={`Search by ${searchTypeLabels[searchType]}...`}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -12,18 +12,25 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { CopyTextButton } from "../ui/CopyTextButton";
|
||||
|
||||
const getUserIdentifier = (user: WalletUser) => {
|
||||
const getAuthIdentifier = (user: WalletUser) => {
|
||||
const mainDetail = user.linkedAccounts[0]?.details;
|
||||
return (
|
||||
mainDetail?.id ??
|
||||
mainDetail?.email ??
|
||||
mainDetail?.phone ??
|
||||
mainDetail?.address ??
|
||||
mainDetail?.id ??
|
||||
user.id
|
||||
mainDetail?.address
|
||||
);
|
||||
};
|
||||
|
||||
const truncateIdentifier = (value: string) => {
|
||||
if (value.length <= 18) {
|
||||
return value;
|
||||
}
|
||||
return `${value.slice(0, 8)}...${value.slice(-4)}`;
|
||||
};
|
||||
|
||||
export function SearchResults(props: {
|
||||
results: WalletUser[];
|
||||
client: ThirdwebClient;
|
||||
@@ -51,6 +58,8 @@ export function SearchResults(props: {
|
||||
const mainDetail = user.linkedAccounts?.[0]?.details;
|
||||
const email = mainDetail?.email as string | undefined;
|
||||
const phone = mainDetail?.phone as string | undefined;
|
||||
const authIdentifier = getAuthIdentifier(user);
|
||||
const userIdentifier = user.id;
|
||||
|
||||
// Get external wallet addresses from linkedAccounts where type is 'siwe'
|
||||
const externalWalletAccounts =
|
||||
@@ -68,7 +77,34 @@ export function SearchResults(props: {
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
User Identifier
|
||||
</p>
|
||||
<p className="text-sm">{getUserIdentifier(user)}</p>
|
||||
{userIdentifier ? (
|
||||
<CopyTextButton
|
||||
textToShow={truncateIdentifier(userIdentifier)}
|
||||
textToCopy={userIdentifier}
|
||||
tooltip="Copy User Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm">N/A</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Auth Identifier
|
||||
</p>
|
||||
{authIdentifier ? (
|
||||
<CopyTextButton
|
||||
textToShow={truncateIdentifier(authIdentifier)}
|
||||
textToCopy={authIdentifier}
|
||||
tooltip="Copy Auth Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm">N/A</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{walletAddress && (
|
||||
|
||||
+69
-44
@@ -28,22 +28,42 @@ import { SearchResults } from "./SearchResults";
|
||||
import { searchUsers } from "./searchUsers";
|
||||
import type { SearchType } from "./types";
|
||||
|
||||
const getUserIdentifier = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
const getAuthIdentifier = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
const mainDetail = accounts[0]?.details;
|
||||
return (
|
||||
mainDetail?.id ??
|
||||
mainDetail?.email ??
|
||||
mainDetail?.phone ??
|
||||
mainDetail?.address ??
|
||||
mainDetail?.id
|
||||
mainDetail?.address
|
||||
);
|
||||
};
|
||||
|
||||
const getExternalWallets = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
return accounts?.filter((account) => account.type === "siwe") || [];
|
||||
const getPrimaryEmail = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
const emailFromPrimary = accounts[0]?.details?.email;
|
||||
if (emailFromPrimary) {
|
||||
return emailFromPrimary;
|
||||
}
|
||||
|
||||
const emailAccount = accounts.find((account) => {
|
||||
return typeof account.details?.email === "string" && account.details.email;
|
||||
});
|
||||
|
||||
if (emailAccount && typeof emailAccount.details.email === "string") {
|
||||
return emailAccount.details.email;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const columnHelper = createColumnHelper<WalletUser>();
|
||||
|
||||
const truncateIdentifier = (value: string) => {
|
||||
if (value.length <= 18) {
|
||||
return value;
|
||||
}
|
||||
return `${value.slice(0, 8)}...${value.slice(-4)}`;
|
||||
};
|
||||
|
||||
export function InAppWalletUsersPageContent(
|
||||
props: {
|
||||
authToken: string;
|
||||
@@ -56,9 +76,30 @@ export function InAppWalletUsersPageContent(
|
||||
) {
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
columnHelper.accessor("id", {
|
||||
cell: (cell) => {
|
||||
const userId = cell.getValue();
|
||||
|
||||
if (!userId) {
|
||||
return "N/A";
|
||||
}
|
||||
|
||||
return (
|
||||
<CopyTextButton
|
||||
textToShow={truncateIdentifier(userId)}
|
||||
textToCopy={userId}
|
||||
tooltip="Copy User Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
);
|
||||
},
|
||||
header: "User Identifier",
|
||||
id: "user_identifier",
|
||||
}),
|
||||
columnHelper.accessor("linkedAccounts", {
|
||||
cell: (cell) => {
|
||||
const identifier = getUserIdentifier(cell.getValue());
|
||||
const identifier = getAuthIdentifier(cell.getValue());
|
||||
|
||||
if (!identifier) {
|
||||
return "N/A";
|
||||
@@ -66,21 +107,17 @@ export function InAppWalletUsersPageContent(
|
||||
|
||||
return (
|
||||
<CopyTextButton
|
||||
textToShow={
|
||||
identifier.length > 30
|
||||
? `${identifier.slice(0, 30)}...`
|
||||
: identifier
|
||||
}
|
||||
textToShow={truncateIdentifier(identifier)}
|
||||
textToCopy={identifier}
|
||||
tooltip="Copy User Identifier"
|
||||
tooltip="Copy Auth Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableColumnFilter: true,
|
||||
header: "User Identifier",
|
||||
id: "user_identifier",
|
||||
header: "Auth Identifier",
|
||||
id: "auth_identifier",
|
||||
}),
|
||||
columnHelper.accessor("wallets", {
|
||||
cell: (cell) => {
|
||||
@@ -94,33 +131,24 @@ export function InAppWalletUsersPageContent(
|
||||
}),
|
||||
columnHelper.accessor("linkedAccounts", {
|
||||
cell: (cell) => {
|
||||
const externalWallets = getExternalWallets(cell.getValue());
|
||||
if (externalWallets.length === 0) {
|
||||
return <span className="text-muted-foreground text-sm">None</span>;
|
||||
const email = getPrimaryEmail(cell.getValue());
|
||||
|
||||
if (!email) {
|
||||
return <span className="text-muted-foreground text-sm">N/A</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{externalWallets.slice(0, 2).map((account) => {
|
||||
const address = account.details?.address as string | undefined;
|
||||
return address ? (
|
||||
<div
|
||||
key={`external-${address}-${account.details?.id}`}
|
||||
className="text-xs"
|
||||
>
|
||||
<WalletAddress address={address} client={props.client} />
|
||||
</div>
|
||||
) : null;
|
||||
})}
|
||||
{externalWallets.length > 2 && (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
+{externalWallets.length - 2} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<CopyTextButton
|
||||
textToShow={email}
|
||||
textToCopy={email}
|
||||
tooltip="Copy Email"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
);
|
||||
},
|
||||
header: "External Wallets",
|
||||
id: "external_wallets",
|
||||
header: "Email",
|
||||
id: "email",
|
||||
}),
|
||||
columnHelper.accessor("wallets", {
|
||||
cell: (cell) => {
|
||||
@@ -230,20 +258,17 @@ export function InAppWalletUsersPageContent(
|
||||
});
|
||||
const csv = Papa.unparse(
|
||||
usersWallets.map((row) => {
|
||||
const externalWallets = getExternalWallets(row.linkedAccounts);
|
||||
const externalWalletAddresses = externalWallets
|
||||
.map((account) => account.details?.address)
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
const email = getPrimaryEmail(row.linkedAccounts);
|
||||
|
||||
return {
|
||||
address: row.wallets[0]?.address || "Uninitialized",
|
||||
created: row.wallets[0]?.createdAt
|
||||
? new Date(row.wallets[0].createdAt).toISOString()
|
||||
: "Wallet not created yet",
|
||||
external_wallets: externalWalletAddresses || "None",
|
||||
email: email || "N/A",
|
||||
login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "),
|
||||
user_identifier: getUserIdentifier(row.linkedAccounts),
|
||||
auth_identifier: getAuthIdentifier(row.linkedAccounts) || "N/A",
|
||||
user_identifier: row.id || "N/A",
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -18,10 +18,10 @@ configure({
|
||||
type APIWallet = ListUserWalletsResponses[200]["result"]["wallets"][0];
|
||||
type APIProfile = APIWallet["profiles"][0];
|
||||
|
||||
// Transform API response to match existing WalletUser format
|
||||
// Transform API response to match wallet user format
|
||||
function transformToWalletUser(apiWallet: APIWallet): WalletUser {
|
||||
return {
|
||||
id: getProfileId(apiWallet.profiles[0]) || "",
|
||||
id: apiWallet.userId || getProfileId(apiWallet.profiles[0]) || "",
|
||||
linkedAccounts: apiWallet.profiles.map((profile) => {
|
||||
// Create details object based on the profile data
|
||||
let details:
|
||||
@@ -105,6 +105,9 @@ export async function searchUsers(
|
||||
case "externalWallet":
|
||||
queryParams.externalWalletAddress = query;
|
||||
break;
|
||||
case "userId":
|
||||
queryParams.userId = query;
|
||||
break;
|
||||
}
|
||||
|
||||
// Use the generated API function with Bearer authentication
|
||||
|
||||
@@ -3,4 +3,5 @@ export type SearchType =
|
||||
| "phone"
|
||||
| "id"
|
||||
| "address"
|
||||
| "externalWallet";
|
||||
| "externalWallet"
|
||||
| "userId";
|
||||
|
||||
@@ -41,7 +41,7 @@ export function CopyTextButton(props: {
|
||||
<Button
|
||||
aria-label={props.tooltip}
|
||||
className={cn(
|
||||
"flex h-auto w-auto gap-2 rounded-lg px-1.5 py-0.5 font-normal text-foreground",
|
||||
"flex h-auto w-auto gap-2 rounded-lg px-1.5 py-0.5 font-normal text-foreground max-w-full truncate",
|
||||
props.className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
import { ArrowUpRightIcon, CheckIcon, CopyIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "./button";
|
||||
import { ToolTipLabel } from "./tooltip";
|
||||
|
||||
export function LinkWithCopyButton(props: {
|
||||
href: string;
|
||||
textToShow: string;
|
||||
textToCopy: string;
|
||||
copyTooltip: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const Icon = isCopied ? CheckIcon : CopyIcon;
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1", props.className)}>
|
||||
<ToolTipLabel label={props.copyTooltip}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="p-1 h-auto shrink-0 opacity-70 hover:opacity-100 text-muted-foreground"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(props.textToCopy);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 1000);
|
||||
}}
|
||||
>
|
||||
<Icon className="size-3" />
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
<Link
|
||||
href={props.href}
|
||||
target="_blank"
|
||||
className="text-sm text-muted-foreground hover:underline flex items-center gap-1 tabular-nums flex-1 truncate hover:text-foreground group"
|
||||
>
|
||||
<span className="max-w-full truncate">{props.textToShow}</span>
|
||||
<ArrowUpRightIcon className="size-3.5 opacity-70 shrink-0 group-hover:opacity-100" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const appMetadata = {
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
name: "thirdweb",
|
||||
url: "https://thirdweb.com",
|
||||
};
|
||||
@@ -59,7 +59,10 @@ export async function fetchDashboardContractMetadata(
|
||||
|
||||
return {
|
||||
contractType: compilerMetadata?.name || "",
|
||||
image: contractMetadata.image || "",
|
||||
image:
|
||||
contractMetadata.image && typeof contractMetadata.image === "string"
|
||||
? contractMetadata.image
|
||||
: "",
|
||||
name: contractName,
|
||||
symbol: contractSymbol,
|
||||
};
|
||||
|
||||
@@ -76,10 +76,10 @@ const fetchAccountList = ({
|
||||
};
|
||||
};
|
||||
|
||||
// Transform API response to match existing WalletUser format
|
||||
// Transform API response to match the wallet user format
|
||||
function transformToWalletUser(apiWallet: APIWallet): WalletUser {
|
||||
return {
|
||||
id: getProfileId(apiWallet.profiles[0]) || "",
|
||||
id: apiWallet.userId || getProfileId(apiWallet.profiles[0]) || "",
|
||||
linkedAccounts: apiWallet.profiles.map((profile) => {
|
||||
// Create details object based on the profile data
|
||||
let details:
|
||||
|
||||
+14
-6
@@ -1,18 +1,26 @@
|
||||
"use client";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { BuyAndSwapEmbed } from "@/components/blocks/BuyAndSwapEmbed";
|
||||
import { GridPatternEmbedContainer } from "@/components/blocks/grid-pattern-embed-container";
|
||||
import { defineDashboardChain } from "@/lib/defineDashboardChain";
|
||||
|
||||
export function BuyFundsSection(props: { chain: ChainMetadata }) {
|
||||
return (
|
||||
<GridPatternEmbedContainer>
|
||||
<BuyAndSwapEmbed
|
||||
isTestnet={props.chain.testnet}
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain={defineDashboardChain(props.chain.chainId, props.chain)}
|
||||
buyAmount={undefined}
|
||||
tokenAddress={undefined}
|
||||
swapTab={{
|
||||
sellToken: {
|
||||
chainId: props.chain.chainId,
|
||||
tokenAddress: NATIVE_TOKEN_ADDRESS,
|
||||
},
|
||||
buyToken: undefined,
|
||||
}}
|
||||
buyTab={{
|
||||
buyToken: {
|
||||
chainId: props.chain.chainId,
|
||||
tokenAddress: NATIVE_TOKEN_ADDRESS,
|
||||
},
|
||||
}}
|
||||
pageType="chain"
|
||||
/>
|
||||
</GridPatternEmbedContainer>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { getRawAccount } from "@/api/account/get-account";
|
||||
import { FaqSection } from "@/components/blocks/faq-section";
|
||||
import { FaqAccordion } from "@/components/blocks/faq-section";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getChain, getCustomChainMetadata } from "../../utils";
|
||||
import { fetchChainSeo } from "./apis/chain-seo";
|
||||
@@ -67,10 +67,21 @@ export default async function Page(props: Props) {
|
||||
)}
|
||||
|
||||
{chainSeo?.faqs && chainSeo.faqs.length > 0 && (
|
||||
<div className="py-10">
|
||||
<FaqSection faqs={chainSeo.faqs} />
|
||||
</div>
|
||||
<FaqSection faqs={chainSeo.faqs} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FaqSection(props: {
|
||||
faqs: Array<{ title: string; description: string }>;
|
||||
}) {
|
||||
return (
|
||||
<section className="py-10">
|
||||
<h2 className="text-2xl md:text-3xl font-semibold mb-4 tracking-tight">
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<FaqAccordion faqs={props.faqs} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
+2
-3
@@ -10,8 +10,7 @@ import * as ERC20Ext from "thirdweb/extensions/erc20";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import { download } from "thirdweb/storage";
|
||||
import type { OverrideEntry } from "thirdweb/utils";
|
||||
import { maxUint256 } from "thirdweb/utils";
|
||||
import { isRecord, maxUint256, type OverrideEntry } from "thirdweb/utils";
|
||||
import type { z } from "zod";
|
||||
import type {
|
||||
ClaimCondition as LegacyClaimCondition,
|
||||
@@ -86,7 +85,7 @@ export async function getClaimPhasesInLegacyFormat(
|
||||
]);
|
||||
const snapshot = await fetchSnapshot(
|
||||
condition.merkleRoot,
|
||||
contractMetadata.merkle,
|
||||
isRecord(contractMetadata.merkle) ? contractMetadata.merkle : {},
|
||||
options.contract.client,
|
||||
);
|
||||
return {
|
||||
|
||||
+4
-1
@@ -47,7 +47,10 @@ export default async function Image({
|
||||
chainName: info.chainMetadata.name,
|
||||
contractAddress: info.serverContract.address,
|
||||
displayName: contractDisplayName,
|
||||
logo: contractMetadata.image,
|
||||
logo:
|
||||
contractMetadata.image && typeof contractMetadata.image === "string"
|
||||
? contractMetadata.image
|
||||
: undefined,
|
||||
});
|
||||
} catch {
|
||||
return contractOGImageTemplate({
|
||||
|
||||
+5
-5
@@ -2,6 +2,8 @@
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { ConnectButton } from "thirdweb/react";
|
||||
import type { Wallet } from "thirdweb/wallets";
|
||||
import { appMetadata } from "@/constants/connect";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
@@ -10,6 +12,7 @@ const client = getClientThirdwebClient();
|
||||
|
||||
export function PublicPageConnectButton(props: {
|
||||
connectButtonClassName?: string;
|
||||
wallets?: Wallet[];
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
const t = theme === "light" ? "light" : "dark";
|
||||
@@ -17,11 +20,7 @@ export function PublicPageConnectButton(props: {
|
||||
|
||||
return (
|
||||
<ConnectButton
|
||||
appMetadata={{
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
name: "thirdweb",
|
||||
url: "https://thirdweb.com",
|
||||
}}
|
||||
appMetadata={appMetadata}
|
||||
autoConnect={false}
|
||||
chains={allChainsV5}
|
||||
client={client}
|
||||
@@ -38,6 +37,7 @@ export function PublicPageConnectButton(props: {
|
||||
}}
|
||||
// we have an AutoConnect already added in root layout with AA configuration
|
||||
theme={getSDKTheme(t)}
|
||||
wallets={props.wallets}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+60
-28
@@ -89,16 +89,31 @@ export async function ERC20PublicPage(props: {
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
contractCreator={contractCreator}
|
||||
image={contractMetadata.image}
|
||||
image={
|
||||
contractMetadata.image &&
|
||||
typeof contractMetadata.image === "string"
|
||||
? contractMetadata.image
|
||||
: undefined
|
||||
}
|
||||
isDashboardUser={isDashboardUser}
|
||||
name={contractMetadata.name}
|
||||
name={
|
||||
contractMetadata.name && typeof contractMetadata.name === "string"
|
||||
? contractMetadata.name
|
||||
: // if we do not have a contract name fall back to the address
|
||||
props.clientContract.address
|
||||
}
|
||||
socialUrls={
|
||||
typeof contractMetadata.social_urls === "object" &&
|
||||
contractMetadata.social_urls !== null
|
||||
? contractMetadata.social_urls
|
||||
: {}
|
||||
}
|
||||
symbol={contractMetadata.symbol}
|
||||
symbol={
|
||||
contractMetadata.symbol &&
|
||||
typeof contractMetadata.symbol === "string"
|
||||
? contractMetadata.symbol
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,21 +158,25 @@ export async function ERC20PublicPage(props: {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showBuyEmbed && (
|
||||
<div className="container max-w-7xl pb-10">
|
||||
<GridPatternEmbedContainer>
|
||||
<BuyEmbed
|
||||
chainMetadata={props.chainMetadata}
|
||||
claimConditionMeta={claimConditionMeta}
|
||||
clientContract={props.clientContract}
|
||||
tokenAddress={props.clientContract.address}
|
||||
tokenDecimals={tokenDecimals}
|
||||
tokenName={contractMetadata.name}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
</GridPatternEmbedContainer>
|
||||
</div>
|
||||
)}
|
||||
{showBuyEmbed &&
|
||||
contractMetadata.name &&
|
||||
typeof contractMetadata.name === "string" &&
|
||||
contractMetadata.symbol &&
|
||||
typeof contractMetadata.symbol === "string" && (
|
||||
<div className="container max-w-7xl pb-10">
|
||||
<GridPatternEmbedContainer>
|
||||
<BuyEmbed
|
||||
chainMetadata={props.chainMetadata}
|
||||
claimConditionMeta={claimConditionMeta}
|
||||
clientContract={props.clientContract}
|
||||
tokenAddress={props.clientContract.address}
|
||||
tokenDecimals={tokenDecimals}
|
||||
tokenName={contractMetadata.name}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
</GridPatternEmbedContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="container flex max-w-7xl grow flex-col pb-10">
|
||||
<div className="flex grow flex-col gap-8">
|
||||
@@ -167,12 +186,15 @@ export async function ERC20PublicPage(props: {
|
||||
contractAddress={props.clientContract.address}
|
||||
/>
|
||||
|
||||
<RecentTransfers
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
decimals={tokenDecimals}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
{contractMetadata.symbol &&
|
||||
typeof contractMetadata.symbol === "string" && (
|
||||
<RecentTransfers
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
decimals={tokenDecimals}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,11 +221,21 @@ function BuyEmbed(props: {
|
||||
if (!props.claimConditionMeta) {
|
||||
return (
|
||||
<BuyAndSwapEmbed
|
||||
chain={props.clientContract.chain}
|
||||
tokenAddress={props.clientContract.address}
|
||||
buyAmount={undefined}
|
||||
// chain={props.clientContract.chain}
|
||||
swapTab={{
|
||||
sellToken: {
|
||||
chainId: props.clientContract.chain.id,
|
||||
tokenAddress: props.clientContract.address,
|
||||
},
|
||||
buyToken: undefined,
|
||||
}}
|
||||
buyTab={{
|
||||
buyToken: {
|
||||
chainId: props.clientContract.chain.id,
|
||||
tokenAddress: props.clientContract.address,
|
||||
},
|
||||
}}
|
||||
pageType="asset"
|
||||
isTestnet={props.chainMetadata.testnet}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+10
-3
@@ -53,6 +53,13 @@ export async function NFTPublicPage(props: {
|
||||
const _isTokenByIndexSupported =
|
||||
props.type === "erc721" && isTokenByIndexSupported(functionSelectors);
|
||||
|
||||
// FIXME: this is technically a bad fallback but we gotta do what we gotta do
|
||||
const contractMetadataWithNameAndSymbolFallback = {
|
||||
...contractMetadata,
|
||||
// fall back to the contract address if the name is not set
|
||||
name: contractMetadata.name || props.clientContract.address,
|
||||
symbol: contractMetadata.symbol || "",
|
||||
};
|
||||
const buyNFTDropCard = nftDropClaimParams ? (
|
||||
<BuyNFTDropCardServer
|
||||
chainMetadata={props.chainMetadata}
|
||||
@@ -73,7 +80,7 @@ export async function NFTPublicPage(props: {
|
||||
<NFTsGrid
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
collectionMetadata={contractMetadata}
|
||||
collectionMetadata={contractMetadataWithNameAndSymbolFallback}
|
||||
gridClassName={
|
||||
buyNFTDropCard
|
||||
? "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||
@@ -110,7 +117,7 @@ export async function NFTPublicPage(props: {
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
contractCreator={contractCreator}
|
||||
contractMetadata={contractMetadata}
|
||||
contractMetadata={contractMetadataWithNameAndSymbolFallback}
|
||||
isDashboardUser={isDashboardUser}
|
||||
>
|
||||
<ResponsiveLayout
|
||||
@@ -138,7 +145,7 @@ export async function NFTPublicPage(props: {
|
||||
<PageLoadTokenViewerSheet
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
collectionMetadata={contractMetadata}
|
||||
collectionMetadata={contractMetadataWithNameAndSymbolFallback}
|
||||
tokenByIndexSupported={_isTokenByIndexSupported}
|
||||
tokenId={BigInt(props.tokenId)}
|
||||
type={props.type}
|
||||
|
||||
+8
-3
@@ -37,7 +37,7 @@ export function TokenViewerSheet(
|
||||
clientContract: ThirdwebContract;
|
||||
chainMetadata: ChainMetadata;
|
||||
collectionMetadata: {
|
||||
name: string;
|
||||
name: string | null;
|
||||
image?: string;
|
||||
};
|
||||
type: "erc1155" | "erc721";
|
||||
@@ -57,6 +57,11 @@ export function TokenViewerSheet(
|
||||
) {
|
||||
const tokenId = props.variant === "fetch-data" ? props.tokenId : props.nft.id;
|
||||
|
||||
const collectionMetadataWithNameFallback = {
|
||||
...props.collectionMetadata,
|
||||
name: props.collectionMetadata.name || props.clientContract.address,
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onOpenChange={(open) => {
|
||||
@@ -78,7 +83,7 @@ export function TokenViewerSheet(
|
||||
<FetchAndRenderTokenInfo
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
collectionMetadata={props.collectionMetadata}
|
||||
collectionMetadata={collectionMetadataWithNameFallback}
|
||||
tokenByIndexSupported={props.tokenByIndexSupported}
|
||||
tokenId={props.tokenId}
|
||||
type={props.type}
|
||||
@@ -86,7 +91,7 @@ export function TokenViewerSheet(
|
||||
) : (
|
||||
<TokenInfoUI
|
||||
chainMetadata={props.chainMetadata}
|
||||
collectionMetadata={props.collectionMetadata}
|
||||
collectionMetadata={collectionMetadataWithNameFallback}
|
||||
contract={props.clientContract}
|
||||
data={props.nft}
|
||||
tokenId={tokenId}
|
||||
|
||||
+4
-1
@@ -84,7 +84,10 @@ export const SettingsMetadata = ({
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
let image: string | undefined = metadata.data?.image;
|
||||
let image: string | undefined =
|
||||
metadata.data?.image && typeof metadata.data.image === "string"
|
||||
? metadata.data.image
|
||||
: undefined;
|
||||
try {
|
||||
image = image
|
||||
? // eslint-disable-next-line no-restricted-syntax
|
||||
|
||||
+59
-50
@@ -3,7 +3,12 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { CodeClient } from "@workspace/ui/components/code/code.client";
|
||||
import { Img } from "@workspace/ui/components/img";
|
||||
import { Spinner } from "@workspace/ui/components/spinner";
|
||||
import { ArrowRightIcon, CircleCheckIcon, CircleXIcon } from "lucide-react";
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
ArrowUpRightIcon,
|
||||
CircleCheckIcon,
|
||||
CircleXIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { NATIVE_TOKEN_ADDRESS, type ThirdwebClient } from "thirdweb";
|
||||
import type { Status, Token } from "thirdweb/bridge";
|
||||
@@ -11,6 +16,7 @@ import { status } from "thirdweb/bridge";
|
||||
import { toTokens } from "thirdweb/utils";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { LinkWithCopyButton } from "@/components/ui/link-with-copy-button";
|
||||
import { SkeletonContainer } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { fetchChain } from "@/utils/fetchChain";
|
||||
@@ -46,9 +52,9 @@ export function BridgeStatus(props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="px-6 lg:px-8 py-7 space-y-1.5">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-sm text-muted-foreground "> Status </p>
|
||||
<div className="px-6 lg:px-8 py-7 space-y-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground "> Status </p>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
@@ -75,14 +81,15 @@ export function BridgeStatus(props: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-sm text-muted-foreground ">Payment ID</p>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground ">Payment ID</p>
|
||||
<CopyTextButton
|
||||
textToCopy={bridgeStatus.paymentId}
|
||||
textToShow={`${bridgeStatus.paymentId.slice(0, 6)}...${bridgeStatus.paymentId.slice(-4)}`}
|
||||
textToShow={bridgeStatus.paymentId}
|
||||
tooltip="Payment ID"
|
||||
className="text-sm translate-x-1.5"
|
||||
className="text-sm -translate-x-1.5 tabular-nums text-muted-foreground"
|
||||
copyIconPosition="left"
|
||||
iconClassName="text-muted-foreground/70"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
@@ -90,7 +97,7 @@ export function BridgeStatus(props: {
|
||||
|
||||
{purchaseDataString && (
|
||||
<div className="px-6 lg:px-8 py-7 space-y-2 border-t border-dashed">
|
||||
<p className="text-sm text-muted-foreground ">Purchase Data</p>
|
||||
<p className="text-sm text-foreground ">Purchase Data</p>
|
||||
<CodeClient
|
||||
code={purchaseDataString}
|
||||
lang="json"
|
||||
@@ -113,6 +120,8 @@ function TokenInfo(props: {
|
||||
txHash: string | undefined;
|
||||
}) {
|
||||
const chainQuery = useChainQuery(props.token.chainId);
|
||||
const isNativeToken =
|
||||
props.token.address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase();
|
||||
|
||||
return (
|
||||
<div className="flex-1 pt-10 pb-9 px-6 lg:px-8">
|
||||
@@ -121,8 +130,9 @@ function TokenInfo(props: {
|
||||
{props.label}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="h-6" />
|
||||
|
||||
<div className="pb-7 pt-6 border-b border-dashed">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative hover:ring-2 hover:ring-offset-2 hover:ring-offset-card hover:ring-foreground/30 rounded-full">
|
||||
<Link
|
||||
@@ -178,10 +188,11 @@ function TokenInfo(props: {
|
||||
render={(data) => (
|
||||
<Link
|
||||
href={`/${chainQuery.data?.slug || props.token.chainId}`}
|
||||
className="text-sm text-muted-foreground leading-none hover:underline"
|
||||
className="text-sm text-muted-foreground leading-none hover:underline flex items-center gap-1"
|
||||
target="_blank"
|
||||
>
|
||||
{data}
|
||||
{data}{" "}
|
||||
<ArrowUpRightIcon className="size-3.5 text-muted-foreground/70 shrink-0" />
|
||||
</Link>
|
||||
)}
|
||||
/>
|
||||
@@ -189,41 +200,44 @@ function TokenInfo(props: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-6" />
|
||||
<div className="h-8" />
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex gap-2.5 justify-between">
|
||||
<p className="text-sm text-muted-foreground ">{props.addressLabel}</p>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground ">{props.addressLabel}</p>
|
||||
<WalletAddress
|
||||
address={props.walletAddress}
|
||||
client={props.client}
|
||||
className="py-0.5 h-auto text-sm [&>*]:!font-sans"
|
||||
iconClassName="size-3.5"
|
||||
className="py-0.5 h-auto text-sm [&>*]:!font-sans tabular-nums [&>*]:font-normal text-muted-foreground hover:text-foreground"
|
||||
iconClassName="size-3"
|
||||
shortenAddress={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-between">
|
||||
<p className="text-sm text-muted-foreground ">Token Address</p>
|
||||
<CopyTextButton
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground ">Token Address</p>
|
||||
<LinkWithCopyButton
|
||||
href={
|
||||
isNativeToken
|
||||
? `/${chainQuery.data?.slug || props.token.chainId}`
|
||||
: `/${chainQuery.data?.slug || props.token.chainId}/${props.token.address}`
|
||||
}
|
||||
textToShow={props.token.address}
|
||||
textToCopy={props.token.address}
|
||||
textToShow={`${props.token.address.slice(0, 6)}...${props.token.address.slice(-4)}`}
|
||||
tooltip="Token Address"
|
||||
className="text-sm translate-x-1.5"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
copyTooltip="Copy Token Address"
|
||||
className="-translate-x-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-between">
|
||||
<p className="text-sm text-muted-foreground ">Transaction Hash</p>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground ">Transaction Hash</p>
|
||||
{props.txHash ? (
|
||||
<CopyTextButton
|
||||
<LinkWithCopyButton
|
||||
className="-translate-x-1"
|
||||
href={`/${chainQuery.data?.slug || props.token.chainId}/tx/${props.txHash}`}
|
||||
textToShow={`${props.txHash.slice(0, 12)}...${props.txHash.slice(-4)}`}
|
||||
textToCopy={props.txHash}
|
||||
textToShow={`${props.txHash.slice(0, 6)}...${props.txHash.slice(-4)}`}
|
||||
tooltip="Transaction Hash"
|
||||
className="text-sm translate-x-1.5"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
copyTooltip="Copy Transaction Hash"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground ">N/A</p>
|
||||
@@ -288,10 +302,8 @@ function FailedBridgeStatusContent(props: {
|
||||
}) {
|
||||
return (
|
||||
<div className="px-6 lg:px-8 py-7 space-y-1.5 border-b border-dashed">
|
||||
<h3 className="text-base font-medium tracking-tight mb-3">
|
||||
Transactions
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="text-lg font-medium tracking-tight mb-4">Transactions</h3>
|
||||
<div className="space-y-4">
|
||||
{props.transactions.map((tx) => {
|
||||
return (
|
||||
<TxHashRow
|
||||
@@ -328,7 +340,7 @@ function TxHashRow(props: {
|
||||
const chainQuery = useChainQuery(props.chainId);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Img
|
||||
src={resolveSchemeWithErrorHandler({
|
||||
@@ -336,26 +348,23 @@ function TxHashRow(props: {
|
||||
uri: chainQuery.data?.icon?.url,
|
||||
})}
|
||||
alt={chainQuery.data?.name}
|
||||
className="size-4 rounded-full"
|
||||
className="size-3.5 rounded-full"
|
||||
fallback={
|
||||
<div className="size-4 from-blue-500 to-foreground rounded-full bg-gradient-to-b" />
|
||||
<div className="size-3.5 from-blue-500 to-foreground rounded-full bg-gradient-to-b" />
|
||||
}
|
||||
/>
|
||||
<SkeletonContainer
|
||||
skeletonData={`Chain ${props.chainId}`}
|
||||
loadedData={chainQuery.data?.name}
|
||||
render={(data) => (
|
||||
<p className="text-sm text-muted-foreground">{data}</p>
|
||||
)}
|
||||
render={(data) => <p className="text-sm text-foreground">{data}</p>}
|
||||
/>
|
||||
</div>
|
||||
<CopyTextButton
|
||||
<LinkWithCopyButton
|
||||
href={`/${chainQuery.data?.slug || props.chainId}/tx/${props.txHash}`}
|
||||
textToShow={props.txHash}
|
||||
textToCopy={props.txHash}
|
||||
textToShow={`${props.txHash.slice(0, 6)}...${props.txHash.slice(-4)}`}
|
||||
tooltip="Copy Transaction Hash"
|
||||
className="text-sm"
|
||||
copyIconPosition="right"
|
||||
variant="ghost"
|
||||
copyTooltip="Copy Transaction Hash"
|
||||
className="-translate-x-0.5"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
"use client";
|
||||
import { BookOpenIcon, MoonIcon, SunIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ToggleThemeButton } from "@/components/blocks/color-mode-toggle";
|
||||
import { useTheme } from "next-themes";
|
||||
import { ClientOnly } from "@/components/blocks/client-only";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThirdwebMiniLogo } from "../../../components/ThirdwebMiniLogo";
|
||||
import { PublicPageConnectButton } from "../../(chain)/[chain_id]/[contractAddress]/public-pages/_components/PublicPageConnectButton";
|
||||
@@ -9,7 +14,7 @@ export function PageHeader(props: { containerClassName?: string }) {
|
||||
<div className="border-b">
|
||||
<header
|
||||
className={cn(
|
||||
"container flex max-w-7xl justify-between py-3",
|
||||
"container flex max-w-7xl justify-between py-4",
|
||||
props.containerClassName,
|
||||
)}
|
||||
>
|
||||
@@ -22,18 +27,49 @@ export function PageHeader(props: { containerClassName?: string }) {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-5">
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/bridge"
|
||||
target="_blank"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
<ToggleThemeButton />
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-1 lg:gap-2">
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/bridge"
|
||||
target="_blank"
|
||||
aria-label="View Documentation"
|
||||
className="text-sm text-muted-foreground hover:text-foreground p-2 hover:bg-accent rounded-full transition-all duration-100"
|
||||
>
|
||||
<BookOpenIcon className="size-5" />
|
||||
</Link>
|
||||
<ToggleThemeButton />
|
||||
</div>
|
||||
<PublicPageConnectButton connectButtonClassName="!rounded-full" />
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleThemeButton(props: { className?: string }) {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
return (
|
||||
<ClientOnly
|
||||
ssr={<Skeleton className="size-[36px] rounded-full border bg-accent" />}
|
||||
>
|
||||
<Button
|
||||
aria-label="Toggle theme"
|
||||
className={cn(
|
||||
"h-auto w-auto rounded-full p-2 text-muted-foreground hover:text-foreground",
|
||||
props.className,
|
||||
)}
|
||||
onClick={() => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
}}
|
||||
variant="ghost"
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<SunIcon className="size-5 " />
|
||||
) : (
|
||||
<MoonIcon className="size-5 " />
|
||||
)}
|
||||
</Button>
|
||||
</ClientOnly>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { redirect } from "next/navigation";
|
||||
import { StripeRedirectErrorPage } from "../../_components/StripeRedirectErrorPage";
|
||||
import { getPlanCancelUrl } from "../../utils/billing";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function CancelPlanPage(props: {
|
||||
params: Promise<{
|
||||
team_id: string;
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
getInvoicePaymentUrl,
|
||||
} from "../../../utils/billing";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function CheckoutPage(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
|
||||
@@ -2,6 +2,8 @@ import { redirect } from "next/navigation";
|
||||
import { StripeRedirectErrorPage } from "../../_components/StripeRedirectErrorPage";
|
||||
import { getBillingPortalUrl } from "../../utils/billing";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function ManageBillingPage(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
"use client";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { AutoConnect } from "thirdweb/react";
|
||||
import type { SmartWalletOptions } from "thirdweb/wallets";
|
||||
|
||||
export function TWAutoConnect(props: {
|
||||
accountAbstraction?: SmartWalletOptions;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
return (
|
||||
<AutoConnect
|
||||
accountAbstraction={props.accountAbstraction}
|
||||
client={props.client}
|
||||
/>
|
||||
);
|
||||
export function TWAutoConnect(props: { client: ThirdwebClient }) {
|
||||
return <AutoConnect client={props.client} />;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,9 @@ export default async function DropPage({
|
||||
]);
|
||||
|
||||
const thumbnail =
|
||||
project.thumbnail || nft.metadata.image || contractMetadata.image || "";
|
||||
project.thumbnail ||
|
||||
nft.metadata.image ||
|
||||
(typeof contractMetadata.image === "string" ? contractMetadata.image : "");
|
||||
|
||||
const displayName = contractMetadata.name || nft.metadata.name || "";
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
HelpCircleIcon,
|
||||
HomeIcon,
|
||||
SettingsIcon,
|
||||
WalletCardsIcon,
|
||||
} from "lucide-react";
|
||||
import { FullWidthSidebarLayout } from "@/components/blocks/full-width-sidebar-layout";
|
||||
|
||||
@@ -18,11 +17,6 @@ export function TeamSidebarLayout(props: {
|
||||
layoutPath: string;
|
||||
children: React.ReactNode;
|
||||
ecosystems: Array<{ name: string; slug: string }>;
|
||||
chainSubscriptions: Array<{
|
||||
chainId: number;
|
||||
chainName: string;
|
||||
slug: string;
|
||||
}>;
|
||||
}) {
|
||||
const { layoutPath, children } = props;
|
||||
|
||||
@@ -72,24 +66,6 @@ export function TeamSidebarLayout(props: {
|
||||
{
|
||||
separator: true,
|
||||
} as const,
|
||||
// infrastructure below here
|
||||
// TODO: make this one link to an overview page that has a list of all the chains currently deployed
|
||||
{
|
||||
links: [
|
||||
...props.chainSubscriptions.map((chainSubscription) => ({
|
||||
href: `${layoutPath}/~/infrastructure/${chainSubscription.slug}`,
|
||||
label: chainSubscription.chainName,
|
||||
})),
|
||||
{
|
||||
href: `${layoutPath}/~/infrastructure/deploy`,
|
||||
label: "Deploy Infrastructure",
|
||||
},
|
||||
],
|
||||
subMenu: {
|
||||
icon: WalletCardsIcon,
|
||||
label: "Chain Infrastucture",
|
||||
},
|
||||
},
|
||||
]}
|
||||
footerSidebarLinks={[
|
||||
{
|
||||
|
||||
@@ -4,13 +4,11 @@ import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token";
|
||||
import { getProjects } from "@/api/project/projects";
|
||||
import { fetchEcosystemList } from "@/api/team/ecosystems";
|
||||
import { getTeamBySlug, getTeams } from "@/api/team/get-team";
|
||||
import { getChainSubscriptions } from "@/api/team/team-subscription";
|
||||
import { CustomChatButton } from "@/components/chat/CustomChatButton";
|
||||
import { AnnouncementBanner } from "@/components/misc/AnnouncementBanner";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { siwaExamplePrompts } from "@/constants/siwa-example-prompts";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getChain } from "../../../(dashboard)/(chain)/utils";
|
||||
import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client";
|
||||
import { StaffModeNotice } from "./_components/StaffModeNotice";
|
||||
import { TeamSidebarLayout } from "./TeamSidebarLayout";
|
||||
@@ -21,52 +19,26 @@ export default async function TeamLayout(props: {
|
||||
}) {
|
||||
const params = await props.params;
|
||||
|
||||
const [
|
||||
accountAddress,
|
||||
account,
|
||||
teams,
|
||||
authToken,
|
||||
team,
|
||||
ecosystems,
|
||||
chainSubscriptions,
|
||||
] = await Promise.all([
|
||||
getAuthTokenWalletAddress(),
|
||||
getValidAccount(`/team/${params.team_slug}`),
|
||||
getTeams(),
|
||||
getAuthToken(),
|
||||
getTeamBySlug(params.team_slug),
|
||||
fetchEcosystemList(params.team_slug),
|
||||
getChainSubscriptions(params.team_slug),
|
||||
]);
|
||||
const [accountAddress, account, teams, authToken, team, ecosystems] =
|
||||
await Promise.all([
|
||||
getAuthTokenWalletAddress(),
|
||||
getValidAccount(`/team/${params.team_slug}`),
|
||||
getTeams(),
|
||||
getAuthToken(),
|
||||
getTeamBySlug(params.team_slug),
|
||||
fetchEcosystemList(params.team_slug),
|
||||
]);
|
||||
|
||||
if (!teams || !accountAddress || !authToken || !team) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const [teamsAndProjects, chainSidebarLinks] = await Promise.all([
|
||||
Promise.all(
|
||||
teams.map(async (team) => ({
|
||||
projects: await getProjects(team.slug),
|
||||
team,
|
||||
})),
|
||||
),
|
||||
chainSubscriptions
|
||||
? await Promise.all(
|
||||
chainSubscriptions.map(async (chainSubscription) => {
|
||||
if (!chainSubscription.chainId) {
|
||||
throw new Error("Chain ID is required");
|
||||
}
|
||||
const chain = await getChain(chainSubscription.chainId);
|
||||
|
||||
return {
|
||||
chainId: chain.chainId,
|
||||
chainName: chain.name,
|
||||
slug: chain.slug,
|
||||
};
|
||||
}),
|
||||
).catch(() => [])
|
||||
: [],
|
||||
]);
|
||||
const teamsAndProjects = await Promise.all(
|
||||
teams.map(async (team) => ({
|
||||
projects: await getProjects(team.slug),
|
||||
team,
|
||||
})),
|
||||
);
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
@@ -92,9 +64,6 @@ export default async function TeamLayout(props: {
|
||||
</div>
|
||||
|
||||
<TeamSidebarLayout
|
||||
chainSubscriptions={chainSidebarLinks.sort(
|
||||
(a, b) => a.chainId - b.chainId,
|
||||
)}
|
||||
ecosystems={ecosystems.map((ecosystem) => ({
|
||||
name: ecosystem.name,
|
||||
slug: ecosystem.slug,
|
||||
|
||||
+2
-5
@@ -11,6 +11,7 @@ import {
|
||||
useActiveWalletConnectionStatus,
|
||||
} from "thirdweb/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { appMetadata } from "@/constants/connect";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
@@ -65,11 +66,7 @@ export const NebulaConnectWallet = (props: {
|
||||
const { customDetailsButton } = props;
|
||||
return (
|
||||
<ConnectButton
|
||||
appMetadata={{
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
name: "thirdweb",
|
||||
url: "https://thirdweb.com",
|
||||
}}
|
||||
appMetadata={appMetadata}
|
||||
autoConnect={false}
|
||||
chains={allChainsV5}
|
||||
client={props.client}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import type { BuyAndSwapEmbedProps } from "@/components/blocks/BuyAndSwapEmbed";
|
||||
import { FaqAccordion } from "@/components/blocks/faq-section";
|
||||
import { UniversalBridgeEmbed } from "./client/UniversalBridgeEmbed";
|
||||
import { BridgePageHeader } from "./header";
|
||||
|
||||
export function BridgePageUI(props: {
|
||||
title: React.ReactNode;
|
||||
buyTab: BuyAndSwapEmbedProps["buyTab"];
|
||||
swapTab: BuyAndSwapEmbedProps["swapTab"];
|
||||
}) {
|
||||
return (
|
||||
<div className="grow flex flex-col">
|
||||
<BridgePageHeader />
|
||||
|
||||
<div className="flex grow items-center justify-center px-4 relative pt-12 pb-20 lg:py-28 min-h-[calc(100dvh-60px)]">
|
||||
<DotsBackgroundPattern />
|
||||
<UniversalBridgeEmbed buyTab={props.buyTab} swapTab={props.swapTab} />
|
||||
</div>
|
||||
|
||||
<HeadingSection title={props.title} />
|
||||
|
||||
<div className="h-20 lg:h-40" />
|
||||
|
||||
<BridgeFaqSection />
|
||||
|
||||
<div className="h-32" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeadingSection(props: { title: React.ReactNode }) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="mb-3 lg:mb-6">{props.title}</div>
|
||||
|
||||
<p className="text-muted-foreground text-sm text-pretty text-center lg:text-lg mb-6 lg:mb-8">
|
||||
Seamlessly move your assets across 85+ chains with the best rates and
|
||||
fastest execution
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-3 lg:gap-2 items-center justify-center">
|
||||
<DataPill>85+ Chains Supported</DataPill>
|
||||
<DataPill>4500+ Tokens Supported</DataPill>
|
||||
<DataPill>9+ Million Routes Available</DataPill>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DataPill(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<p className="bg-card flex items-center text-xs lg:text-sm gap-1.5 text-foreground border rounded-full px-8 lg:px-3 py-1.5 hover:text-foreground transition-colors duration-300">
|
||||
{props.children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function DotsBackgroundPattern(props: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute -inset-x-36 -inset-y-24 text-foreground/20 dark:text-muted-foreground/20 hidden lg:block",
|
||||
props.className,
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: "radial-gradient(currentColor 1px, transparent 1px)",
|
||||
backgroundSize: "24px 24px",
|
||||
maskImage:
|
||||
"radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 50%)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const bridgeFaqs: Array<{ title: string; description: string }> = [
|
||||
{
|
||||
title: "What is bridging in crypto?",
|
||||
description:
|
||||
"Crypto bridging (cross-chain bridging) moves tokens between blockchains so you can use assets across networks. In thirdweb Bridge, connect your wallet, choose the source token/network and destination token/network, review the route and price, then confirm. Assets arrive after finality, often under ~10 seconds on fast routes, though timing depends on networks and congestion.",
|
||||
},
|
||||
{
|
||||
title: "How does crypto bridging work?",
|
||||
description:
|
||||
"Bridge smart contracts lock or burn tokens on the source chain and mint or release equivalents on the destination via verified cross-chain providers. thirdweb Bridge automatically finds the fastest, lowest-cost route and may use different mechanisms based on networks and liquidity. Arrival can range from seconds to minutes depending on finality; many routes complete in ~10 seconds",
|
||||
},
|
||||
{
|
||||
title: "What is a crypto asset swap?",
|
||||
description:
|
||||
"A crypto swap exchanges one token for another via a DEX or aggregator. thirdweb Bridge lets you bridge + swap in one step. For example, ETH on Ethereum to USDC on Base, by selecting your start and end tokens/networks and confirming.",
|
||||
},
|
||||
{
|
||||
title: "How can I get stablecoins like USDC or USDT?",
|
||||
description:
|
||||
"Use thirdweb Bridge to convert assets you hold into USDC or USDT on your chosen network: select your current token/network, pick the stablecoin (USDC, USDT, etc) on the destination, and confirm. You can also buy stablecoins with fiat in the Buy flow and bridge if needed. Always verify official token contract addresses.",
|
||||
},
|
||||
{
|
||||
title: "What is the cost of bridging and swapping?",
|
||||
description:
|
||||
"Costs include gas on each chain, bridge/liquidity provider fees, and any DEX swap fees or price impact. thirdweb Bridge compares routes and selects the best price route. Save by using lower-gas times or combining bridge + swap in one flow.",
|
||||
},
|
||||
];
|
||||
|
||||
function BridgeFaqSection() {
|
||||
return (
|
||||
<section className="container max-w-2xl">
|
||||
<h2 className="text-2xl md:text-3xl font-semibold mb-4 lg:mb-8 tracking-tight text-center">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
<FaqAccordion faqs={bridgeFaqs} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -2,10 +2,15 @@
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { ThirdwebProvider } from "thirdweb/react";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { TWAutoConnect } from "../../../(app)/components/autoconnect";
|
||||
|
||||
const thirdwebClient = getClientThirdwebClient();
|
||||
|
||||
export function BridgeProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<TWAutoConnect client={thirdwebClient} />
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import type { TokenInfo } from "thirdweb/react";
|
||||
import { BuyAndSwapEmbed } from "@/components/blocks/BuyAndSwapEmbed";
|
||||
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
import { createWallet } from "thirdweb/wallets";
|
||||
import {
|
||||
BuyAndSwapEmbed,
|
||||
type BuyAndSwapEmbedProps,
|
||||
} from "@/components/blocks/BuyAndSwapEmbed";
|
||||
import { appMetadata } from "@/constants/connect";
|
||||
|
||||
export function UniversalBridgeEmbed({
|
||||
chainId,
|
||||
token,
|
||||
amount,
|
||||
}: {
|
||||
chainId?: number;
|
||||
token: TokenInfo | undefined;
|
||||
amount?: string;
|
||||
export const bridgeWallets = [
|
||||
createWallet("io.metamask"),
|
||||
createWallet("com.coinbase.wallet", {
|
||||
appMetadata,
|
||||
}),
|
||||
createWallet("me.rainbow"),
|
||||
createWallet("io.rabby"),
|
||||
createWallet("io.zerion.wallet"),
|
||||
];
|
||||
|
||||
export function UniversalBridgeEmbed(props: {
|
||||
buyTab: BuyAndSwapEmbedProps["buyTab"];
|
||||
swapTab: BuyAndSwapEmbedProps["swapTab"];
|
||||
}) {
|
||||
const chain = useV5DashboardChain(chainId || 1);
|
||||
|
||||
return (
|
||||
<BuyAndSwapEmbed
|
||||
chain={chain}
|
||||
buyAmount={amount}
|
||||
tokenAddress={token?.address}
|
||||
buyTab={props.buyTab}
|
||||
swapTab={props.swapTab}
|
||||
pageType="bridge"
|
||||
isTestnet={chain.testnet}
|
||||
wallets={bridgeWallets}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
import { ChevronRightIcon, DollarSignIcon, TrendingUpIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { reportBridgePageLinkClick } from "@/analytics/report";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function PillLink(props: {
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
linkType: "integrate-bridge" | "trending-tokens";
|
||||
}) {
|
||||
const Icon =
|
||||
props.linkType === "integrate-bridge" ? DollarSignIcon : TrendingUpIcon;
|
||||
return (
|
||||
<Link
|
||||
href={props.href}
|
||||
target="_blank"
|
||||
onClick={() => reportBridgePageLinkClick({ linkType: props.linkType })}
|
||||
className={cn(
|
||||
"shadow-sm text-center justify-center inline-flex items-center gap-4 text-foreground group",
|
||||
"text-sm bg-card/50 backdrop-blur-lg border border-border/70",
|
||||
"rounded-full px-4 py-2.5 transition-colors duration-300 text-pretty leading-5",
|
||||
"hover:bg-pink-300/20 dark:hover:bg-pink-950/30 hover:text-foreground hover:border-pink-400 dark:hover:border-pink-800",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Icon className="size-4 shrink-0 text-pink-600" />
|
||||
{props.children}
|
||||
</div>
|
||||
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground/50 group-hover:translate-x-1 transition-all duration-300 group-hover:text-foreground" />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,27 @@
|
||||
"use client";
|
||||
import { BookOpenIcon, CoinsIcon, MoonIcon, SunIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ToggleThemeButton } from "@/components/blocks/color-mode-toggle";
|
||||
import { useTheme } from "next-themes";
|
||||
import { reportBridgePageLinkClick } from "@/analytics/report";
|
||||
import { ClientOnly } from "@/components/blocks/client-only";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PublicPageConnectButton } from "../../(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/_components/PublicPageConnectButton";
|
||||
import { ThirdwebMiniLogo } from "../../(app)/components/ThirdwebMiniLogo";
|
||||
import { bridgeWallets } from "./client/UniversalBridgeEmbed";
|
||||
|
||||
export function PageHeader(props: { containerClassName?: string }) {
|
||||
export function BridgePageHeader(props: { containerClassName?: string }) {
|
||||
return (
|
||||
<div className="border-b border-dashed">
|
||||
<div className="border-b border-border/70">
|
||||
<header
|
||||
className={cn(
|
||||
"container flex max-w-7xl justify-between py-4",
|
||||
"container flex max-w-7xl justify-between py-3 lg:py-4",
|
||||
props.containerClassName,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link className="flex items-center gap-2" href="/team">
|
||||
<div className="flex items-center gap-6">
|
||||
<Link className="flex items-center gap-2" href="/home">
|
||||
<ThirdwebMiniLogo className="h-5" />
|
||||
<span className="hidden font-bold text-2xl tracking-tight lg:block">
|
||||
thirdweb
|
||||
@@ -22,18 +29,68 @@ export function PageHeader(props: { containerClassName?: string }) {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-5">
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/bridge"
|
||||
target="_blank"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
<ToggleThemeButton className="bg-transparent" />
|
||||
<PublicPageConnectButton connectButtonClassName="!rounded-full" />
|
||||
<div className="flex items-center gap-3 lg:gap-5">
|
||||
<div className="flex items-center gap-1 lg:gap-2">
|
||||
<Link
|
||||
onClick={() => {
|
||||
reportBridgePageLinkClick({ linkType: "trending-tokens" });
|
||||
}}
|
||||
target="_blank"
|
||||
href="/tokens"
|
||||
className="text-sm text-muted-foreground hover:text-foreground p-2 hover:bg-accent rounded-full transition-all duration-100"
|
||||
>
|
||||
<span className="text-sm sr-only md:not-sr-only">
|
||||
Trending Tokens
|
||||
</span>
|
||||
<CoinsIcon className="size-5 md:hidden" />
|
||||
</Link>
|
||||
<Link
|
||||
onClick={() => {
|
||||
reportBridgePageLinkClick({ linkType: "bridge-docs" });
|
||||
}}
|
||||
href="https://portal.thirdweb.com/bridge"
|
||||
target="_blank"
|
||||
aria-label="View Documentation"
|
||||
className="text-sm text-muted-foreground hover:text-foreground p-2 hover:bg-accent rounded-full transition-all duration-100"
|
||||
>
|
||||
<BookOpenIcon className="size-5" />
|
||||
</Link>
|
||||
<ToggleThemeButton />
|
||||
</div>
|
||||
<PublicPageConnectButton
|
||||
connectButtonClassName="!rounded-full"
|
||||
wallets={bridgeWallets}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleThemeButton(props: { className?: string }) {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
return (
|
||||
<ClientOnly
|
||||
ssr={<Skeleton className="size-[36px] rounded-full border bg-accent" />}
|
||||
>
|
||||
<Button
|
||||
aria-label="Toggle theme"
|
||||
className={cn(
|
||||
"h-auto w-auto rounded-full p-2 text-muted-foreground hover:text-foreground",
|
||||
props.className,
|
||||
)}
|
||||
onClick={() => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
}}
|
||||
variant="ghost"
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<SunIcon className="size-5 " />
|
||||
) : (
|
||||
<MoonIcon className="size-5 " />
|
||||
)}
|
||||
</Button>
|
||||
</ClientOnly>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { setThirdwebDomains } from "thirdweb/utils";
|
||||
import {
|
||||
NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
|
||||
NEXT_PUBLIC_IPFS_GATEWAY_URL,
|
||||
} from "@/constants/public-envs";
|
||||
import {
|
||||
THIRDWEB_BRIDGE_URL,
|
||||
THIRDWEB_BUNDLER_DOMAIN,
|
||||
THIRDWEB_INAPP_WALLET_DOMAIN,
|
||||
THIRDWEB_INSIGHT_API_DOMAIN,
|
||||
THIRDWEB_PAY_DOMAIN,
|
||||
THIRDWEB_RPC_DOMAIN,
|
||||
THIRDWEB_SOCIAL_API_DOMAIN,
|
||||
THIRDWEB_STORAGE_DOMAIN,
|
||||
} from "@/constants/urls";
|
||||
import { getVercelEnv } from "@/utils/vercel";
|
||||
|
||||
function getBridgeThirdwebClient() {
|
||||
if (getVercelEnv() !== "production") {
|
||||
// if not on production: run this when creating a client to set the domains
|
||||
setThirdwebDomains({
|
||||
bridge: THIRDWEB_BRIDGE_URL,
|
||||
bundler: THIRDWEB_BUNDLER_DOMAIN,
|
||||
inAppWallet: THIRDWEB_INAPP_WALLET_DOMAIN,
|
||||
insight: THIRDWEB_INSIGHT_API_DOMAIN,
|
||||
pay: THIRDWEB_PAY_DOMAIN,
|
||||
rpc: THIRDWEB_RPC_DOMAIN,
|
||||
social: THIRDWEB_SOCIAL_API_DOMAIN,
|
||||
storage: THIRDWEB_STORAGE_DOMAIN,
|
||||
});
|
||||
}
|
||||
|
||||
// During build time, client ID might not be available
|
||||
if (!NEXT_PUBLIC_DASHBOARD_CLIENT_ID) {
|
||||
// Return a minimal client that will fail gracefully at runtime if needed
|
||||
return createThirdwebClient({
|
||||
clientId: "dummy-build-time-client",
|
||||
config: {
|
||||
storage: {
|
||||
gatewayUrl: NEXT_PUBLIC_IPFS_GATEWAY_URL,
|
||||
},
|
||||
},
|
||||
secretKey: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return createThirdwebClient({
|
||||
clientId: NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
|
||||
config: {
|
||||
storage: {
|
||||
gatewayUrl: NEXT_PUBLIC_IPFS_GATEWAY_URL,
|
||||
},
|
||||
},
|
||||
secretKey: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export const bridgeAppThirdwebClient = getBridgeThirdwebClient();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
@@ -0,0 +1,181 @@
|
||||
import type { Metadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Bridge, NATIVE_TOKEN_ADDRESS } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { fetchChain } from "@/utils/fetchChain";
|
||||
import { BridgePageUI } from "../../components/bridge-page";
|
||||
import { generateTokenPairSlugs, getTokenPairData } from "./slug-map";
|
||||
|
||||
type Params = {
|
||||
"token-pair": string;
|
||||
};
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<Params>;
|
||||
};
|
||||
|
||||
export async function generateStaticParams(): Promise<Params[]> {
|
||||
return generateTokenPairSlugs().map((slug) => ({
|
||||
"token-pair": slug,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function generateMetadata(
|
||||
props: PageProps,
|
||||
): Promise<Metadata | undefined> {
|
||||
const params = await props.params;
|
||||
const tokenPair = params["token-pair"];
|
||||
const tokenMetadata = await getTokenMetadata(tokenPair).catch(
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
if (!tokenMetadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const title = getTitle(tokenMetadata);
|
||||
const description =
|
||||
"Bridge and swap 4500+ tokens across 85+ chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing with near-instant finality";
|
||||
|
||||
const result: Metadata = {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default async function Page(props: { params: Promise<Params> }) {
|
||||
const params = await props.params;
|
||||
|
||||
const tokenMetadata = await getTokenMetadata(params["token-pair"]);
|
||||
if (!tokenMetadata) {
|
||||
redirect("/bridge");
|
||||
}
|
||||
|
||||
const { fromToken, toToken, fromChain, toChain } = tokenMetadata;
|
||||
|
||||
return (
|
||||
<BridgePageUI
|
||||
buyTab={{
|
||||
buyToken: {
|
||||
tokenAddress: toToken.address,
|
||||
chainId: toToken.chainId,
|
||||
},
|
||||
}}
|
||||
swapTab={{
|
||||
sellToken: {
|
||||
tokenAddress: fromToken.address,
|
||||
chainId: fromToken.chainId,
|
||||
},
|
||||
buyToken: {
|
||||
tokenAddress: toToken.address,
|
||||
chainId: toToken.chainId,
|
||||
},
|
||||
}}
|
||||
title={
|
||||
<h1 className="text-center text-2xl md:text-5xl font-semibold tracking-tighter text-balance max-w-5xl block mx-auto">
|
||||
{getTitle({
|
||||
fromToken,
|
||||
toToken,
|
||||
fromChain,
|
||||
toChain,
|
||||
})}
|
||||
</h1>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function cleanedChainName(chainName: string) {
|
||||
return chainName.replaceAll("mainnet", "").replaceAll("Mainnet", "").trim();
|
||||
}
|
||||
|
||||
function cleanTokenName(tokenName: string) {
|
||||
return tokenName.replaceAll("Token", "").replaceAll("token", "").trim();
|
||||
}
|
||||
|
||||
async function getTokenMetadata(slug: string) {
|
||||
const tokenPairData = getTokenPairData(slug);
|
||||
if (!tokenPairData) {
|
||||
redirect("/bridge");
|
||||
}
|
||||
|
||||
const [fromTokenTokens, toTokenTokens, fromChain, toChain] =
|
||||
await Promise.all([
|
||||
Bridge.tokens({
|
||||
client: serverThirdwebClient,
|
||||
chainId: tokenPairData.from.chainId,
|
||||
tokenAddress: tokenPairData.from.tokenAddress,
|
||||
}).catch(() => undefined),
|
||||
Bridge.tokens({
|
||||
client: serverThirdwebClient,
|
||||
chainId: tokenPairData.to.chainId,
|
||||
tokenAddress: tokenPairData.to.tokenAddress,
|
||||
}).catch(() => undefined),
|
||||
fetchChain(tokenPairData.from.chainId),
|
||||
fetchChain(tokenPairData.to.chainId),
|
||||
]);
|
||||
|
||||
const fromToken = fromTokenTokens?.[0];
|
||||
const toToken = toTokenTokens?.[0];
|
||||
|
||||
if (!fromToken || !toToken || !fromChain || !toChain) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
fromToken,
|
||||
toToken,
|
||||
fromChain,
|
||||
toChain,
|
||||
};
|
||||
}
|
||||
|
||||
function getTitle(data: {
|
||||
fromToken: {
|
||||
name: string;
|
||||
symbol: string;
|
||||
address: string;
|
||||
};
|
||||
toToken: {
|
||||
name: string;
|
||||
symbol: string;
|
||||
address: string;
|
||||
};
|
||||
fromChain: ChainMetadata;
|
||||
toChain: ChainMetadata;
|
||||
}) {
|
||||
const isSameChain = data.fromChain.chainId === data.toChain.chainId;
|
||||
const fromTokenName = cleanTokenName(data.fromToken.name);
|
||||
const toTokenName = cleanTokenName(data.toToken.name);
|
||||
|
||||
const fromTokenFullName =
|
||||
fromTokenName === data.fromToken.symbol ||
|
||||
data.fromToken.address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()
|
||||
? data.fromToken.symbol
|
||||
: `${fromTokenName} (${data.fromToken.symbol})`;
|
||||
|
||||
const toTokenFullName =
|
||||
toTokenName === data.toToken.symbol ||
|
||||
data.toToken.address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()
|
||||
? data.toToken.symbol
|
||||
: `${toTokenName} (${data.toToken.symbol})`;
|
||||
|
||||
// if both symbol are same: Swap {symbol} from {fromChain} to {toChain}
|
||||
if (fromTokenFullName === toTokenFullName) {
|
||||
return `Swap ${fromTokenFullName} from ${cleanedChainName(data.fromChain.name)} to ${cleanedChainName(data.toChain.name)}`;
|
||||
}
|
||||
|
||||
const line1 = `Swap ${fromTokenFullName} to ${toTokenFullName}`;
|
||||
const line2 = isSameChain
|
||||
? `on ${cleanedChainName(data.fromChain.name)}`
|
||||
: `cross-chain from ${cleanedChainName(data.fromChain.name)} to ${cleanedChainName(data.toChain.name)}`;
|
||||
|
||||
return `${line1} ${line2}`;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { NATIVE_TOKEN_ADDRESS } from "thirdweb";
|
||||
|
||||
type Tokens = Record<
|
||||
string,
|
||||
| {
|
||||
chainId: number;
|
||||
tokens: Record<string, string | undefined>;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
|
||||
const tokens: Tokens = {
|
||||
eth: {
|
||||
chainId: 1,
|
||||
tokens: {
|
||||
usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
usdt: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
shib: "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE",
|
||||
},
|
||||
},
|
||||
bnb: {
|
||||
chainId: 56,
|
||||
tokens: {
|
||||
ada: "0x3EE2200Efb3400fAbB9AacF31297cBdD1d435D47",
|
||||
},
|
||||
},
|
||||
avax: {
|
||||
chainId: 43114,
|
||||
tokens: {},
|
||||
},
|
||||
doge: {
|
||||
chainId: 2000,
|
||||
tokens: {},
|
||||
},
|
||||
base: {
|
||||
chainId: 8453,
|
||||
tokens: {
|
||||
usdc: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
weth: "0x4200000000000000000000000000000000000006",
|
||||
},
|
||||
},
|
||||
pol: {
|
||||
chainId: 137,
|
||||
tokens: {
|
||||
usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
||||
bnb: "0x3BA4c387f786bFEE076A58914F5Bd38d668B42c3",
|
||||
shib: "0x6f8a06447ff6fcf75d803135a7de15ce88c1d4ec",
|
||||
weth: "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619",
|
||||
avax: "0x2C89bbc92BD86F8075d1DEcc58C7F4E0107f286b",
|
||||
},
|
||||
},
|
||||
arb: {
|
||||
chainId: 42161,
|
||||
tokens: {
|
||||
usdc: "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type TokenPairData = {
|
||||
from: {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
};
|
||||
to: {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
};
|
||||
};
|
||||
|
||||
// examples: eth-to-eth-usdc, eth-usdc-to-pol, base-to-eth
|
||||
export function getTokenPairData(slug: string): TokenPairData | undefined {
|
||||
const [fromSlug, toSlug] = slug.split("-to-");
|
||||
if (!fromSlug || !toSlug) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const fromTokenInfo = getTokenInfo(fromSlug);
|
||||
const toTokenInfo = getTokenInfo(toSlug);
|
||||
|
||||
if (!fromTokenInfo || !toTokenInfo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
from: fromTokenInfo,
|
||||
to: toTokenInfo,
|
||||
};
|
||||
}
|
||||
|
||||
function getTokenInfo(slug: string):
|
||||
| {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
}
|
||||
| undefined {
|
||||
const [chainSlug, tokenSlug] = slug.split("-");
|
||||
|
||||
if (!chainSlug) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const chainObject = tokens[chainSlug];
|
||||
|
||||
if (!chainObject) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const tokenAddress = tokenSlug
|
||||
? chainObject.tokens[tokenSlug]
|
||||
: NATIVE_TOKEN_ADDRESS;
|
||||
|
||||
if (!tokenAddress) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
chainId: chainObject.chainId,
|
||||
tokenAddress,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateTokenPairSlugs() {
|
||||
const tokenIds = Object.keys(tokens);
|
||||
|
||||
// generate all possible token pair slugs
|
||||
const tokenSlugs: string[] = [];
|
||||
for (const chainSlug of tokenIds) {
|
||||
// erc20s and native token
|
||||
if (tokens[chainSlug]) {
|
||||
tokenSlugs.push(chainSlug); // native token
|
||||
for (const tokenSlug in tokens[chainSlug].tokens) {
|
||||
tokenSlugs.push(`${chainSlug}-${tokenSlug}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const routeSlugs: string[] = [];
|
||||
for (const fromSlug of tokenSlugs) {
|
||||
for (const toSlug of tokenSlugs) {
|
||||
if (fromSlug === toSlug) {
|
||||
continue;
|
||||
}
|
||||
routeSlugs.push(`${fromSlug}-to-${toSlug}`);
|
||||
}
|
||||
}
|
||||
|
||||
return routeSlugs;
|
||||
}
|
||||
@@ -1,16 +1,5 @@
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { DotIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import type { Address } from "thirdweb";
|
||||
import { defineChain } from "thirdweb/chains";
|
||||
import { getContract } from "thirdweb/contract";
|
||||
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
|
||||
import { FaqSection } from "@/components/blocks/faq-section";
|
||||
import { AppFooter } from "@/components/footers/app-footer";
|
||||
import { PillLink } from "./components/client/pill-link";
|
||||
import { UniversalBridgeEmbed } from "./components/client/UniversalBridgeEmbed";
|
||||
import { PageHeader } from "./components/header";
|
||||
import { bridgeAppThirdwebClient } from "./constants";
|
||||
import { BridgePageUI } from "./components/bridge-page";
|
||||
|
||||
const title = "thirdweb Bridge: Buy, Bridge & Swap Crypto on 85+ Chains";
|
||||
const description =
|
||||
@@ -25,163 +14,17 @@ export const metadata: Metadata = {
|
||||
title,
|
||||
};
|
||||
|
||||
export default async function BridgePage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<Record<string, string | string[]>>;
|
||||
}) {
|
||||
const { chainId, tokenAddress, amount } = await searchParams;
|
||||
|
||||
let symbol: string | undefined;
|
||||
let decimals: number | undefined;
|
||||
let tokenName: string | undefined;
|
||||
|
||||
if (chainId && tokenAddress) {
|
||||
try {
|
||||
const metadata = await getCurrencyMetadata({
|
||||
contract: getContract({
|
||||
address: tokenAddress as Address,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(Number(chainId)),
|
||||
client: bridgeAppThirdwebClient,
|
||||
}),
|
||||
});
|
||||
({ symbol, decimals, name: tokenName } = metadata);
|
||||
} catch (error) {
|
||||
console.warn("Failed to fetch token metadata:", error);
|
||||
// Continue with undefined values; the component should handle gracefully
|
||||
}
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="grow flex flex-col relative overflow-hidden">
|
||||
<div className="relative z-10">
|
||||
<PageHeader />
|
||||
|
||||
<div className="h-12 lg:h-20" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="container">
|
||||
<h1 className="text-3xl md:text-6xl font-bold mb-6 tracking-tighter text-center leading-8 lg:leading-none text-balance">
|
||||
Bridge and Swap tokens <br className="max-sm:hidden" /> across any
|
||||
chain, instantly
|
||||
</h1>
|
||||
|
||||
<div className="flex flex-wrap gap-2 items-center justify-center">
|
||||
<DataPill>85+ Chains Supported</DataPill>
|
||||
<DotIcon className="size-3 text-muted-foreground lg:hidden" />
|
||||
<DataPill>4500+ Tokens Supported</DataPill>
|
||||
<DataPill>9+ Million Routes Available</DataPill>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-12 lg:h-16" />
|
||||
|
||||
<div className="flex grow items-center justify-center px-4 relative">
|
||||
<DotsBackgroundPattern />
|
||||
<UniversalBridgeEmbed
|
||||
amount={amount as string}
|
||||
chainId={chainId ? Number(chainId) : undefined}
|
||||
token={
|
||||
symbol && decimals && tokenName
|
||||
? {
|
||||
address: tokenAddress as Address,
|
||||
name: tokenName,
|
||||
symbol,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-32" />
|
||||
|
||||
<div className="flex flex-col gap-4 items-center container">
|
||||
<PillLink
|
||||
href="https://portal.thirdweb.com/bridge"
|
||||
linkType="integrate-bridge"
|
||||
>
|
||||
Accept Payments in Any Token Right Inside Your App
|
||||
</PillLink>
|
||||
|
||||
<PillLink
|
||||
href="https://thirdweb.com/tokens"
|
||||
linkType="trending-tokens"
|
||||
>
|
||||
Discover Trending Tokens
|
||||
</PillLink>
|
||||
</div>
|
||||
|
||||
<div className="h-32" />
|
||||
|
||||
<div className="container max-w-2xl">
|
||||
<BridgeFaqSection />
|
||||
</div>
|
||||
|
||||
<div className="h-32" />
|
||||
|
||||
<div className="relative">
|
||||
<AppFooter />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DataPill(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<p className="lg:bg-card flex items-center gap-1.5 text-muted-foreground text-xs lg:backdrop-blur-lg lg:border lg:border-border/70 rounded-full lg:px-3 lg:py-1.5 hover:text-foreground transition-colors duration-300">
|
||||
{props.children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function DotsBackgroundPattern(props: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute -inset-x-36 -inset-y-24 text-foreground/20 dark:text-muted-foreground/20 hidden lg:block",
|
||||
props.className,
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: "radial-gradient(currentColor 1px, transparent 1px)",
|
||||
backgroundSize: "24px 24px",
|
||||
maskImage:
|
||||
"radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 50%)",
|
||||
}}
|
||||
<BridgePageUI
|
||||
buyTab={undefined}
|
||||
swapTab={undefined}
|
||||
title={
|
||||
<h1 className="text-3xl md:text-6xl font-semibold tracking-tighter text-balance text-center">
|
||||
Bridge and Swap tokens <br className="max-sm:hidden" /> across any
|
||||
chain, instantly
|
||||
</h1>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const bridgeFaqs: Array<{ title: string; description: string }> = [
|
||||
{
|
||||
title: "What is bridging in crypto?",
|
||||
description:
|
||||
"Crypto bridging (cross-chain bridging) moves tokens between blockchains so you can use assets across networks. In thirdweb Bridge, connect your wallet, choose the source token/network and destination token/network, review the route and price, then confirm. Assets arrive after finality, often under ~10 seconds on fast routes, though timing depends on networks and congestion.",
|
||||
},
|
||||
{
|
||||
title: "How does crypto bridging work?",
|
||||
description:
|
||||
"Bridge smart contracts lock or burn tokens on the source chain and mint or release equivalents on the destination via verified cross-chain providers. thirdweb Bridge automatically finds the fastest, lowest-cost route and may use different mechanisms based on networks and liquidity. Arrival can range from seconds to minutes depending on finality; many routes complete in ~10 seconds",
|
||||
},
|
||||
{
|
||||
title: "What is a crypto asset swap?",
|
||||
description:
|
||||
"A crypto swap exchanges one token for another via a DEX or aggregator. thirdweb Bridge lets you bridge + swap in one step. For example, ETH on Ethereum to USDC on Base, by selecting your start and end tokens/networks and confirming.",
|
||||
},
|
||||
{
|
||||
title: "How can I get stablecoins like USDC or USDT?",
|
||||
description:
|
||||
"Use thirdweb Bridge to convert assets you hold into USDC or USDT on your chosen network: select your current token/network, pick the stablecoin (USDC, USDT, etc) on the destination, and confirm. You can also buy stablecoins with fiat in the Buy flow and bridge if needed. Always verify official token contract addresses.",
|
||||
},
|
||||
{
|
||||
title: "What is the cost of bridging and swapping?",
|
||||
description:
|
||||
"Costs include gas on each chain, bridge/liquidity provider fees, and any DEX swap fees or price impact. thirdweb Bridge compares routes and selects the best price route. Save by using lower-gas times or combining bridge + swap in one flow.",
|
||||
},
|
||||
];
|
||||
|
||||
function BridgeFaqSection() {
|
||||
return <FaqSection faqs={bridgeFaqs} />;
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# E2E testing setup
|
||||
|
||||
This directory contains the end-to-end (E2E) testing setup for the project.
|
||||
|
||||
## Adding a test
|
||||
|
||||
1. create a new file in this directory with the `.spec.ts` extension
|
||||
2. copy the paterns from the existing tests
|
||||
|
||||
## Running the tests locally
|
||||
|
||||
To run the tests locally, you can use the following command:
|
||||
|
||||
```bash
|
||||
pnpm playwright
|
||||
```
|
||||
|
||||
## CI (vercel + checkly)
|
||||
|
||||
These tests are run automatically on every pull request as part of the vercel build (after the build completes we check if the tests pass and then we deploy the changes to the preview environment).
|
||||
|
||||
Your PR will be _blocked_ from deploying if these tests fail.
|
||||
|
||||
### Updating the tests in checkly
|
||||
|
||||
1. Create the tests
|
||||
2. make sure they run locally
|
||||
3. push the changes to the repository and run `pnpm update-checkly` to update the checkly tests. (Requires checkly cli being signed in, meaning you need to have checkly access.)
|
||||
@@ -1,14 +0,0 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { getBaseURL } from "./setup";
|
||||
|
||||
test.beforeEach(async ({ page, baseURL }) => {
|
||||
await page.goto(`${getBaseURL(baseURL)}/1`);
|
||||
});
|
||||
|
||||
test.describe("Chain Page", () => {
|
||||
test("redirects chain-id to correct slug", async ({ page }) => {
|
||||
// Expect the page to have the correct title.
|
||||
const pageUrl = page.url();
|
||||
expect(pageUrl).toContain("/ethereum");
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { getBaseURL, waitForPageLoad } from "./setup";
|
||||
|
||||
test.beforeEach(async ({ page, baseURL }) => {
|
||||
await page.goto(
|
||||
`${getBaseURL(baseURL)}/1/0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D`,
|
||||
);
|
||||
});
|
||||
|
||||
test.describe("Contract Page", () => {
|
||||
test("BAYC", async ({ page }) => {
|
||||
// give it some time to load
|
||||
await waitForPageLoad(page, {
|
||||
loadTimeout: 10000,
|
||||
waitAfterLoad: 15000,
|
||||
});
|
||||
// Expect the page to have the correct title.
|
||||
expect(await page.title()).toBe(
|
||||
"BoredApeYachtClub (BAYC) | Ethereum Smart Contract | thirdweb",
|
||||
);
|
||||
// expect the page to have the correct h1 tag.
|
||||
expect(await page.locator("h1").textContent()).toBe("BoredApeYachtClub");
|
||||
|
||||
// find the extensions header
|
||||
const nftTabLinkEl = page.locator("a", { hasText: "NFTs" }).first();
|
||||
expect(await nftTabLinkEl.textContent()).toBe("NFTs");
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { getBaseURL } from "./setup";
|
||||
|
||||
test.beforeEach(async ({ page, baseURL }) => {
|
||||
await page.goto(getBaseURL(baseURL));
|
||||
});
|
||||
|
||||
test.describe("Homepage", () => {
|
||||
test("has correct title", async ({ page }) => {
|
||||
// Expect a title to be correct.
|
||||
await expect(page).toHaveTitle(
|
||||
"thirdweb: The complete web3 development platform",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { getBaseURL } from "./setup";
|
||||
|
||||
test.beforeEach(async ({ page, baseURL }) => {
|
||||
await page.goto(`${getBaseURL(baseURL)}/thirdweb.eth`);
|
||||
});
|
||||
|
||||
test.describe("Publisher Page", () => {
|
||||
test("thirdweb.eth", async ({ page }) => {
|
||||
// Expect the page to have the correct title.
|
||||
expect(await page.locator("h1").textContent()).toBe("thirdweb.eth");
|
||||
expect(await page.locator("h2").nth(1).textContent()).toBe(
|
||||
"Published contracts",
|
||||
);
|
||||
// first h2 header, then 10 elements in first table -> should be 12th
|
||||
expect(await page.locator("h2").nth(12).textContent()).toBe(
|
||||
"Deployed contracts",
|
||||
);
|
||||
|
||||
// 13 h2 headers to start
|
||||
expect((await page.locator("h2").all()).length).toBe(13);
|
||||
|
||||
const publishedContractsShowMoreButton = page
|
||||
.locator("text=Show more")
|
||||
.first();
|
||||
await publishedContractsShowMoreButton.click();
|
||||
// should load 10 more elements
|
||||
expect((await page.locator("h2").all()).length).toBe(23);
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
export function getBaseURL(url: string | undefined) {
|
||||
if (process.env.ENVIRONMENT_URL) {
|
||||
url = process.env.ENVIRONMENT_URL;
|
||||
}
|
||||
if (!url) {
|
||||
url = "https://thirdweb.com/";
|
||||
}
|
||||
return url.endsWith("/") ? url.slice(0, -1) : url;
|
||||
}
|
||||
|
||||
async function wait(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export async function waitForPageLoad(
|
||||
page: Page,
|
||||
options: { loadTimeout: number; waitAfterLoad?: number },
|
||||
) {
|
||||
await Promise.race([
|
||||
page.waitForLoadState("load"),
|
||||
wait(options.loadTimeout),
|
||||
]);
|
||||
|
||||
if (options.waitAfterLoad) {
|
||||
await wait(options.waitAfterLoad);
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,6 @@ function App() {
|
||||
>
|
||||
<TokenIcon
|
||||
className="h-auto w-20 rounded-full"
|
||||
iconResolver="/usdc.svg"
|
||||
/>
|
||||
</TokenProvider>
|
||||
)
|
||||
|
||||
@@ -31,10 +31,7 @@ export function TokenImageOverridePreview() {
|
||||
chain={ethereum}
|
||||
client={THIRDWEB_CLIENT}
|
||||
>
|
||||
<TokenIcon
|
||||
className="h-auto w-20 rounded-full"
|
||||
iconResolver="/usdc.svg"
|
||||
/>
|
||||
<TokenIcon className="h-auto w-20 rounded-full" />
|
||||
</TokenProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# wagmi-inapp
|
||||
|
||||
## 0.0.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`ceba683`](https://github.com/thirdweb-dev/js/commit/ceba6835dd60896efc34fe1495a1812c0cc39db7), [`b30369f`](https://github.com/thirdweb-dev/js/commit/b30369f3cc0bbceed3470b6c905551a5c930f08f)]:
|
||||
- thirdweb@5.110.0
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.168
|
||||
|
||||
## 0.0.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`5ff6063`](https://github.com/thirdweb-dev/js/commit/5ff6063961c5b58e6a49cae77c803155072f30fc), [`72fbb15`](https://github.com/thirdweb-dev/js/commit/72fbb1550f957c93cfde4e63d7bb6458fb192c47)]:
|
||||
- thirdweb@5.109.1
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.167
|
||||
|
||||
## 0.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wagmi-inapp",
|
||||
"private": true,
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.13",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @thirdweb-dev/api
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8290](https://github.com/thirdweb-dev/js/pull/8290) [`f2121e9`](https://github.com/thirdweb-dev/js/commit/f2121e9519719ef809986c919acc5069b5b7ce74) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - added support for userId across wallet apis
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#8279](https://github.com/thirdweb-dev/js/pull/8279) [`72fbb15`](https://github.com/thirdweb-dev/js/commit/72fbb1550f957c93cfde4e63d7bb6458fb192c47) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Update to latest API
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/api",
|
||||
"version": "0.0.3",
|
||||
"version": "0.1.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
|
||||
@@ -15,12 +15,18 @@ import type {
|
||||
CompleteAuthenticationData,
|
||||
CompleteAuthenticationErrors,
|
||||
CompleteAuthenticationResponses,
|
||||
ConvertFiatToCryptoData,
|
||||
ConvertFiatToCryptoErrors,
|
||||
ConvertFiatToCryptoResponses,
|
||||
CreatePaymentData,
|
||||
CreatePaymentErrors,
|
||||
CreatePaymentResponses,
|
||||
CreateServerWalletData,
|
||||
CreateServerWalletErrors,
|
||||
CreateServerWalletResponses,
|
||||
CreateSolanaWalletData,
|
||||
CreateSolanaWalletErrors,
|
||||
CreateSolanaWalletResponses,
|
||||
CreateTokenData,
|
||||
CreateTokenErrors,
|
||||
CreateTokenResponses,
|
||||
@@ -30,6 +36,15 @@ import type {
|
||||
DeployContractData,
|
||||
DeployContractErrors,
|
||||
DeployContractResponses,
|
||||
FetchWithPaymentData,
|
||||
FetchWithPaymentErrors,
|
||||
FetchWithPaymentResponses,
|
||||
GetBridgeChainsData,
|
||||
GetBridgeChainsErrors,
|
||||
GetBridgeChainsResponses,
|
||||
GetBridgeSupportedRoutesData,
|
||||
GetBridgeSupportedRoutesErrors,
|
||||
GetBridgeSupportedRoutesResponses,
|
||||
GetContractEventsData,
|
||||
GetContractEventsErrors,
|
||||
GetContractEventsResponses,
|
||||
@@ -48,6 +63,9 @@ import type {
|
||||
GetPaymentHistoryData,
|
||||
GetPaymentHistoryErrors,
|
||||
GetPaymentHistoryResponses,
|
||||
GetSolanaTransactionData,
|
||||
GetSolanaTransactionErrors,
|
||||
GetSolanaTransactionResponses,
|
||||
GetTokenOwnersData,
|
||||
GetTokenOwnersErrors,
|
||||
GetTokenOwnersResponses,
|
||||
@@ -69,12 +87,21 @@ import type {
|
||||
InitiateAuthenticationData,
|
||||
InitiateAuthenticationErrors,
|
||||
InitiateAuthenticationResponses,
|
||||
LinkAuthenticationData,
|
||||
LinkAuthenticationErrors,
|
||||
LinkAuthenticationResponses,
|
||||
ListContractsData,
|
||||
ListContractsErrors,
|
||||
ListContractsResponses,
|
||||
ListPayableServicesData,
|
||||
ListPayableServicesErrors,
|
||||
ListPayableServicesResponses,
|
||||
ListServerWalletsData,
|
||||
ListServerWalletsErrors,
|
||||
ListServerWalletsResponses,
|
||||
ListSolanaWalletsData,
|
||||
ListSolanaWalletsErrors,
|
||||
ListSolanaWalletsResponses,
|
||||
ListTokensData,
|
||||
ListTokensErrors,
|
||||
ListTokensResponses,
|
||||
@@ -94,20 +121,41 @@ import type {
|
||||
ReadContractData,
|
||||
ReadContractErrors,
|
||||
ReadContractResponses,
|
||||
SendSolanaTokensData,
|
||||
SendSolanaTokensErrors,
|
||||
SendSolanaTokensResponses,
|
||||
SendSolanaTransactionData,
|
||||
SendSolanaTransactionErrors,
|
||||
SendSolanaTransactionResponses,
|
||||
SendTokensData,
|
||||
SendTokensErrors,
|
||||
SendTokensResponses,
|
||||
SendTransactionsData,
|
||||
SendTransactionsErrors,
|
||||
SendTransactionsResponses,
|
||||
SettleX402PaymentData,
|
||||
SettleX402PaymentErrors,
|
||||
SettleX402PaymentResponses,
|
||||
SignMessageData,
|
||||
SignMessageErrors,
|
||||
SignMessageResponses,
|
||||
SignSolanaMessageData,
|
||||
SignSolanaMessageErrors,
|
||||
SignSolanaMessageResponses,
|
||||
SignTypedDataData,
|
||||
SignTypedDataErrors,
|
||||
SignTypedDataResponses,
|
||||
SocialAuthenticationData,
|
||||
SocialAuthenticationErrors,
|
||||
SupportedX402PaymentsData,
|
||||
SupportedX402PaymentsErrors,
|
||||
SupportedX402PaymentsResponses,
|
||||
UnlinkAuthenticationData,
|
||||
UnlinkAuthenticationErrors,
|
||||
UnlinkAuthenticationResponses,
|
||||
VerifyX402PaymentData,
|
||||
VerifyX402PaymentErrors,
|
||||
VerifyX402PaymentResponses,
|
||||
WriteContractData,
|
||||
WriteContractErrors,
|
||||
WriteContractResponses,
|
||||
@@ -137,22 +185,24 @@ export type Options<
|
||||
* **Supported Methods:**
|
||||
* - **SMS** - Send verification code to phone number
|
||||
* - **Email** - Send verification code to email address
|
||||
* - **OAuth** - ⚠️ **DEPRECATED**: Use `/auth/social` instead for OAuth flows
|
||||
* - **Passkey** - Generate WebAuthn challenge for biometric authentication
|
||||
* - **SIWE** - Generate Sign-In with Ethereum payload
|
||||
*
|
||||
* **OAuth Migration:**
|
||||
* The OAuth method in this endpoint is deprecated. Please use the new `/auth/social` endpoint instead:
|
||||
* - **Old**: `POST /auth/initiate` with `{"method": "oauth", "provider": "google", "redirectUrl": "..."}`
|
||||
* - **New**: `GET /auth/social?provider=google&redirectUrl=...`
|
||||
*
|
||||
* **Flow:**
|
||||
* 1. Choose your authentication method
|
||||
* 2. Provide method-specific parameters
|
||||
* 3. Receive challenge data to complete authentication
|
||||
* 4. Use the /complete endpoint to finish the process
|
||||
*
|
||||
* NOTE: for custom authentication (JWT, auth-payload) and for guest authentication, you can skip this step and use the `/complete` endpoint directly.
|
||||
* **OAuth:**
|
||||
*
|
||||
* The OAuth method uses a dedicated `/auth/social` endpoint instead of this one:
|
||||
*
|
||||
* `GET /auth/social?provider=google&redirectUrl=...`
|
||||
*
|
||||
* **Custom (JWT, auth-payload) and Guest:**
|
||||
*
|
||||
* For custom authentication (JWT, auth-payload) and for guest authentication, you can skip this step and use the `/auth/complete` endpoint directly.
|
||||
*
|
||||
* **Authentication:** Requires `x-client-id` header for frontend usage or `x-secret-key` for backend usage.
|
||||
*/
|
||||
@@ -194,6 +244,7 @@ export const initiateAuthentication = <ThrowOnError extends boolean = false>(
|
||||
* - `isNewUser` - Whether this is a new wallet creation
|
||||
* - `token` - JWT token for authenticated API requests
|
||||
* - `type` - The authentication method used
|
||||
* - `userId` - Unique identifier for the authenticated user
|
||||
* - `walletAddress` - Your new or existing wallet address
|
||||
*
|
||||
* **Next step - Verify your token:**
|
||||
@@ -237,6 +288,82 @@ export const completeAuthentication = <ThrowOnError extends boolean = false>(
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Link Auth
|
||||
* Link an additional authentication method or external wallet to the currently authenticated user. Provide the authentication token from another completed login (for example, a SIWE wallet or OAuth account) and this endpoint will associate it with the user's existing wallet.
|
||||
*
|
||||
* **Usage:**
|
||||
* 1. Complete an authentication flow using `/auth/complete` to obtain the new authentication token
|
||||
* 2. Call this endpoint with the token you want to link
|
||||
* 3. Receive the full list of linked authentication profiles for the wallet
|
||||
*
|
||||
* **Authentication:** Requires both client authentication (`x-client-id` or `x-secret-key`) and a wallet access token via `Authorization: Bearer <jwt>`.
|
||||
*/
|
||||
export const linkAuthentication = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<LinkAuthenticationData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).post<
|
||||
LinkAuthenticationResponses,
|
||||
LinkAuthenticationErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-client-id",
|
||||
type: "apiKey",
|
||||
},
|
||||
{
|
||||
scheme: "bearer",
|
||||
type: "http",
|
||||
},
|
||||
],
|
||||
url: "/v1/auth/link",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Unlink Auth
|
||||
* Disconnect an authentication method or external wallet from the currently authenticated user. Provide the identifiers for the provider you want to remove (for example, an email address or wallet address) and this endpoint will detach it from the user's account.
|
||||
*
|
||||
* **Usage:**
|
||||
* 1. Choose the provider type you want to disconnect (email, phone, siwe, oauth, etc.)
|
||||
* 2. Supply the provider-specific identifiers in the request body
|
||||
* 3. Receive the updated list of linked authentication profiles
|
||||
*
|
||||
* **Authentication:** Requires both client authentication (`x-client-id` or `x-secret-key`) and a wallet access token via `Authorization: Bearer <jwt>`.
|
||||
*/
|
||||
export const unlinkAuthentication = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<UnlinkAuthenticationData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).post<
|
||||
UnlinkAuthenticationResponses,
|
||||
UnlinkAuthenticationErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-client-id",
|
||||
type: "apiKey",
|
||||
},
|
||||
{
|
||||
scheme: "bearer",
|
||||
type: "http",
|
||||
},
|
||||
],
|
||||
url: "/v1/auth/unlink",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Social Auth
|
||||
* Complete OAuth authentication with social providers in a single step. Unlike other auth methods that require separate initiate/complete calls, OAuth is handled entirely through redirects.
|
||||
@@ -316,6 +443,7 @@ export const socialAuthentication = <ThrowOnError extends boolean = false>(
|
||||
* Retrieve the authenticated user's wallet information including wallet addresses and linked authentication wallets. This endpoint provides comprehensive user data for the currently authenticated session.
|
||||
*
|
||||
* **Returns:**
|
||||
* - userId - Unique identifier for this wallet in thirdweb auth
|
||||
* - Primary wallet address
|
||||
* - Smart wallet address (if available)
|
||||
* - Wallet creation timestamp
|
||||
@@ -507,7 +635,7 @@ export const getWalletTransactions = <ThrowOnError extends boolean = false>(
|
||||
|
||||
/**
|
||||
* Get Tokens
|
||||
* Retrieves token balances for a specific wallet address across one or more blockchain networks. This endpoint provides comprehensive token data including ERC-20 tokens with their balances, metadata, and price information. Results can be filtered by chain and paginated to meet specific requirements.
|
||||
* Retrieves token balances for a specific wallet address across one or more blockchain networks. This endpoint provides comprehensive token data including ERC-20 tokens with their balances, metadata, and price information. Results can be filtered by chain, sorted by balance or USD value, and customized to include/exclude spam tokens, native tokens, and tokens without price data. Supports pagination and metadata resolution options.
|
||||
*
|
||||
* **Authentication**: Pass `x-client-id` header for frontend usage from allowlisted origins or `x-secret-key` for backend usage.
|
||||
*/
|
||||
@@ -1058,6 +1186,151 @@ export const paymentsPurchase = <ThrowOnError extends boolean = false>(
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* x402 - Verify payment
|
||||
* Verify an x402 payment payload against the provided payment requirements. Compatible with any standard x402 middleware.
|
||||
*/
|
||||
export const verifyX402Payment = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<VerifyX402PaymentData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).post<
|
||||
VerifyX402PaymentResponses,
|
||||
VerifyX402PaymentErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-secret-key",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/payments/x402/verify",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* x402 - Settle payment
|
||||
* Settle an x402 payment. Compatible with any standard x402 middleware.
|
||||
*/
|
||||
export const settleX402Payment = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<SettleX402PaymentData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).post<
|
||||
SettleX402PaymentResponses,
|
||||
SettleX402PaymentErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-secret-key",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/payments/x402/settle",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* x402 - Supported payment methods
|
||||
* List supported x402 payment methods, optionally filtered by token address and chainId. Compatible with any standard x402 middleware.
|
||||
*/
|
||||
export const supportedX402Payments = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<SupportedX402PaymentsData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).get<
|
||||
SupportedX402PaymentsResponses,
|
||||
SupportedX402PaymentsErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-secret-key",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/payments/x402/supported",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* x402 - Fetch with payment
|
||||
* Fetch any given url. If the url returns HTTP 402 payment required, this endpoint handles payment with the authenticated wallet.
|
||||
*
|
||||
* Optionally pass a 'from' query parameter with the authenticated wallet address (server or user wallet) to complete the payment.
|
||||
*
|
||||
* If no 'from' parameter is passed, the default project wallet address will be used.
|
||||
*
|
||||
* - Works with any x402 compatible endpoint.
|
||||
* - Automatically selects a compatible payment method.
|
||||
* - Signs the appropriate payment payload.
|
||||
* - Sends the payment to the url.
|
||||
* - Returns the final result from the url called.
|
||||
*
|
||||
* Request body and headers are always passed through to the url called.
|
||||
*
|
||||
* **Authentication**: This endpoint requires wallet authentication for the payment. For frontend usage, include `x-client-id` and `Authorization: Bearer <jwt>` headers. For backend usage, include `x-secret-key` header.
|
||||
*/
|
||||
export const fetchWithPayment = <ThrowOnError extends boolean = false>(
|
||||
options: Options<FetchWithPaymentData, ThrowOnError>,
|
||||
) => {
|
||||
return (options.client ?? _heyApiClient).post<
|
||||
FetchWithPaymentResponses,
|
||||
FetchWithPaymentErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
scheme: "bearer",
|
||||
type: "http",
|
||||
},
|
||||
{
|
||||
name: "x-client-id",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/payments/x402/fetch",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "*/*",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* x402 - Discover resources
|
||||
* Discover payable x402 compatible services and HTTP endpoints that can be paid for using the fetchWithPayment tool. Use this tool to browse services, APIs and endpoints to find what you need for your tasks. Each item has a resource url that you can call with the fetchWithPayment tool.Price is in the base units of the asset. For example, if the price is 1000000 and the asset is USDC (which is the default and has 6 decimals), the price is 1 USDC.Examples: if network is eip155:8453, asset is 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, max amount required is 10000, resource is https://api.example.com/paid-api, then you should interpret that as "the api.example.com/paid-api service costs 0.01 USDC per call".
|
||||
*/
|
||||
export const listPayableServices = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<ListPayableServicesData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).get<
|
||||
ListPayableServicesResponses,
|
||||
ListPayableServicesErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-client-id",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/payments/x402/discovery/resources",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* List Tokens
|
||||
* Lists or search existing tokens based on the provided filters. Supports querying by chain ID, token address, symbol, and/or name.
|
||||
@@ -1124,7 +1397,13 @@ export const createToken = <ThrowOnError extends boolean = false>(
|
||||
|
||||
/**
|
||||
* Get Owners
|
||||
* Retrieves a paginated list of owners for a given ERC-20 token contract on a specific chain.
|
||||
* Retrieves a paginated list of owners for a given token contract on a specific chain. Supports ERC-20 tokens, ERC-721 NFTs, and ERC-1155 tokens:
|
||||
*
|
||||
* - **ERC-20**: No `tokenId` provided - returns token holders with balances
|
||||
* - **NFT Collection**: No `tokenId` provided - returns all owners of any token in the collection
|
||||
* - **Specific NFT**: `tokenId` provided - returns owner(s) of that specific token ID
|
||||
*
|
||||
* The token standard is automatically detected using ERC165 interface detection when needed.
|
||||
*
|
||||
* **Authentication**: Pass `x-client-id` header for frontend usage from allowlisted origins or `x-secret-key` for backend usage.
|
||||
*/
|
||||
@@ -1147,6 +1426,83 @@ export const getTokenOwners = <ThrowOnError extends boolean = false>(
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* List Supported Chains
|
||||
* List all blockchain networks available for cross-chain bridging. Each chain includes metadata and native currency details.
|
||||
*
|
||||
* **Authentication**: Pass `x-client-id` header for frontend usage from allowlisted origins or `x-secret-key` for backend usage.
|
||||
*/
|
||||
export const getBridgeChains = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<GetBridgeChainsData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).get<
|
||||
GetBridgeChainsResponses,
|
||||
GetBridgeChainsErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-client-id",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/bridge/chains",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* List Supported Routes
|
||||
* List supported bridge routes with simple pagination and optional chain or token filters.
|
||||
*
|
||||
* **Authentication**: Pass `x-client-id` header for frontend usage from allowlisted origins or `x-secret-key` for backend usage.
|
||||
*/
|
||||
export const getBridgeSupportedRoutes = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<GetBridgeSupportedRoutesData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).get<
|
||||
GetBridgeSupportedRoutesResponses,
|
||||
GetBridgeSupportedRoutesErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-client-id",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/bridge/routes",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert Fiat to Crypto
|
||||
* Convert fiat currency amount to cryptocurrency token amount. Supports multiple fiat currencies based on available price data for the specific token. Returns the equivalent amount of crypto tokens for the specified fiat amount based on current market prices. If price data is not available for the requested currency, the API will return a 404 error.
|
||||
*
|
||||
* **Native Tokens**: To get the price of native tokens (like ETH on Ethereum), use the address `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`. For example, to get the price of ETH on Ethereum Mainnet (chainId: 1), pass `to=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`.
|
||||
*
|
||||
* **Authentication**: Pass `x-client-id` header for frontend usage from allowlisted origins or `x-secret-key` for backend usage.
|
||||
*/
|
||||
export const convertFiatToCrypto = <ThrowOnError extends boolean = false>(
|
||||
options: Options<ConvertFiatToCryptoData, ThrowOnError>,
|
||||
) => {
|
||||
return (options.client ?? _heyApiClient).get<
|
||||
ConvertFiatToCryptoResponses,
|
||||
ConvertFiatToCryptoErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-client-id",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/bridge/convert",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Swap or Bridge Tokens
|
||||
* Swap one token for another using the optimal route available. You can specify a tokenIn amount (if exact='input') or tokenOut amount (if exact='output'), but not both. The corresponding output or input amount will be returned as the quote.
|
||||
@@ -1180,6 +1536,172 @@ export const bridgeSwap = <ThrowOnError extends boolean = false>(
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* List Solana Wallets
|
||||
* List all Solana wallets created for your project. Supports pagination with page and limit parameters.
|
||||
*
|
||||
* **Authentication**: This endpoint requires backend authentication using the x-secret-key header. The secret key should never be exposed publicly.
|
||||
*/
|
||||
export const listSolanaWallets = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<ListSolanaWalletsData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).get<
|
||||
ListSolanaWalletsResponses,
|
||||
ListSolanaWalletsErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-secret-key",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/solana/wallets",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create Solana Wallet
|
||||
* Create a new Solana wallet or return the existing wallet for a given label. Labels must be unique within your project.
|
||||
*
|
||||
* **Authentication**: This endpoint requires backend authentication using the x-secret-key header. The secret key should never be exposed publicly.
|
||||
*/
|
||||
export const createSolanaWallet = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<CreateSolanaWalletData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).post<
|
||||
CreateSolanaWalletResponses,
|
||||
CreateSolanaWalletErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-secret-key",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/solana/wallets",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sign Solana Message
|
||||
* Sign an arbitrary message with a Solana wallet. Supports both text and hexadecimal message formats with automatic format detection.
|
||||
*
|
||||
* **Authentication**: This endpoint requires backend authentication using the x-secret-key header. The secret key should never be exposed publicly.
|
||||
*/
|
||||
export const signSolanaMessage = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<SignSolanaMessageData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).post<
|
||||
SignSolanaMessageResponses,
|
||||
SignSolanaMessageErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-secret-key",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/solana/sign-message",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Send Solana Tokens
|
||||
* Transfer native SOL or SPL tokens on Solana. Automatically handles token account creation for SPL tokens if needed.
|
||||
*
|
||||
* **Authentication**: This endpoint requires backend authentication using the x-secret-key header. The secret key should never be exposed publicly.
|
||||
*/
|
||||
export const sendSolanaTokens = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<SendSolanaTokensData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).post<
|
||||
SendSolanaTokensResponses,
|
||||
SendSolanaTokensErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-secret-key",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/solana/send",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Send Solana Transaction
|
||||
* Submit a Solana transaction composed of one or more instructions. Transactions are queued and processed asynchronously.
|
||||
*
|
||||
* **Authentication**: This endpoint requires backend authentication using the x-secret-key header. The secret key should never be exposed publicly.
|
||||
*/
|
||||
export const sendSolanaTransaction = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<SendSolanaTransactionData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).post<
|
||||
SendSolanaTransactionResponses,
|
||||
SendSolanaTransactionErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-secret-key",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/solana/transactions",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Solana Transaction
|
||||
* Retrieve the status and details of a queued Solana transaction using the identifier returned when the transaction was submitted.
|
||||
*
|
||||
* **Authentication**: This endpoint requires backend authentication using the x-secret-key header. The secret key should never be exposed publicly.
|
||||
*/
|
||||
export const getSolanaTransaction = <ThrowOnError extends boolean = false>(
|
||||
options: Options<GetSolanaTransactionData, ThrowOnError>,
|
||||
) => {
|
||||
return (options.client ?? _heyApiClient).get<
|
||||
GetSolanaTransactionResponses,
|
||||
GetSolanaTransactionErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
security: [
|
||||
{
|
||||
name: "x-secret-key",
|
||||
type: "apiKey",
|
||||
},
|
||||
],
|
||||
url: "/v1/solana/transactions/:transactionId",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Chat
|
||||
* Thirdweb AI chat completion API (BETA).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,19 @@
|
||||
# @thirdweb-dev/nebula
|
||||
|
||||
## 0.2.71
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`ceba683`](https://github.com/thirdweb-dev/js/commit/ceba6835dd60896efc34fe1495a1812c0cc39db7), [`b30369f`](https://github.com/thirdweb-dev/js/commit/b30369f3cc0bbceed3470b6c905551a5c930f08f)]:
|
||||
- thirdweb@5.110.0
|
||||
|
||||
## 0.2.70
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`5ff6063`](https://github.com/thirdweb-dev/js/commit/5ff6063961c5b58e6a49cae77c803155072f30fc), [`72fbb15`](https://github.com/thirdweb-dev/js/commit/72fbb1550f957c93cfde4e63d7bb6458fb192c47)]:
|
||||
- thirdweb@5.109.1
|
||||
|
||||
## 0.2.69
|
||||
|
||||
### 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.69"
|
||||
"version": "0.2.71"
|
||||
}
|
||||
|
||||
@@ -18,19 +18,19 @@ export function getAuthHeaders(
|
||||
case !!secretKey:
|
||||
return {
|
||||
"x-secret-key": secretKey,
|
||||
} as Record<string, string>;
|
||||
};
|
||||
|
||||
// 2. if we have a JWT AND either a teamId or clientId, we'll use the JWT for auth
|
||||
case !!(jwt && (teamId || clientId)):
|
||||
return {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
} as Record<string, string>;
|
||||
};
|
||||
|
||||
// 3. if we have an incoming service api key, we'll use it
|
||||
case !!incomingServiceApiKey: {
|
||||
return {
|
||||
"x-service-api-key": incomingServiceApiKey,
|
||||
} as Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
// 4. if nothing else is present, we'll use the service api key
|
||||
|
||||
@@ -1,5 +1,63 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.110.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#8289](https://github.com/thirdweb-dev/js/pull/8289) [`b30369f`](https://github.com/thirdweb-dev/js/commit/b30369f3cc0bbceed3470b6c905551a5c930f08f) Thanks [@jnsdls](https://github.com/jnsdls)! - ### `getContractMetadata()` now returns a record with `unknown` values instead of `any`.
|
||||
|
||||
before:
|
||||
|
||||
```ts
|
||||
const metadata = await getContractMetadata({ contract });
|
||||
metadata; // Record<string, any>
|
||||
metadata.name; // string
|
||||
metadata.symbol; // string
|
||||
```
|
||||
|
||||
after:
|
||||
|
||||
```ts
|
||||
const metadata = await getContractMetadata({ contract });
|
||||
metadata; // Record<string, unknown>
|
||||
metadata.name; // string | null
|
||||
metadata.symbol; // string | null
|
||||
```
|
||||
|
||||
Metadata is not (and was never) strictly defined outside of `name` and `symbol` and may contain any type of data in the record.
|
||||
This is not a runtime change but it may break type inference in existing apps that relied on the previous return type.
|
||||
|
||||
**Recommended fix:**
|
||||
You _should_ type-guard any key you access from "metadata".
|
||||
|
||||
```ts
|
||||
const metadata = await getContractMetadata({ contract });
|
||||
if ("foo" in metadata && typeof metadata.foo === "string") {
|
||||
metadata.foo; // string
|
||||
}
|
||||
```
|
||||
|
||||
**Quick fix:**
|
||||
If adding type assertions is not something you can do in the short term you can also assert the type directly.
|
||||
_This is as "unsafe" as the type was before._
|
||||
|
||||
```ts
|
||||
const metadata = await getContractMetadata({ contract });
|
||||
const foo = metadata.foo as string;
|
||||
```
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8280](https://github.com/thirdweb-dev/js/pull/8280) [`ceba683`](https://github.com/thirdweb-dev/js/commit/ceba6835dd60896efc34fe1495a1812c0cc39db7) Thanks [@MananTank](https://github.com/MananTank)! - Fix process not defined error when using "thirdweb/contract" import in Vite
|
||||
|
||||
## 5.109.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8266](https://github.com/thirdweb-dev/js/pull/8266) [`5ff6063`](https://github.com/thirdweb-dev/js/commit/5ff6063961c5b58e6a49cae77c803155072f30fc) Thanks [@MananTank](https://github.com/MananTank)! - autofocus token search input when token selector modal opens in `BuyWidget`, `SwapWidget` and `BridgeWidget` components
|
||||
|
||||
- [#8279](https://github.com/thirdweb-dev/js/pull/8279) [`72fbb15`](https://github.com/thirdweb-dev/js/commit/72fbb1550f957c93cfde4e63d7bb6458fb192c47) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Resolve TokenIcon automatically when available
|
||||
|
||||
## 5.109.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -430,5 +430,5 @@
|
||||
}
|
||||
},
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "5.109.0"
|
||||
"version": "5.110.0"
|
||||
}
|
||||
|
||||
@@ -197,3 +197,8 @@ export type { JWTPayload } from "../utils/jwt/types.js";
|
||||
export type { NFTInput, NFTMetadata } from "../utils/nft/parseNft.js";
|
||||
export { resolvePromisedValue } from "../utils/promise/resolve-promised-value.js";
|
||||
export { shortenLargeNumber } from "../utils/shortenLargeNumber.js";
|
||||
|
||||
// ------------------------------------------------
|
||||
// type guards
|
||||
// ------------------------------------------------
|
||||
export * from "../utils/type-guards.js";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { upload } from "../../../storage/upload.js";
|
||||
import type { BaseTransactionOptions } from "../../../transaction/types.js";
|
||||
import { isRecord } from "../../../utils/type-guards.js";
|
||||
import { setContractURI } from "../../common/__generated__/IContractMetadata/write/setContractURI.js";
|
||||
|
||||
/**
|
||||
@@ -80,7 +81,12 @@ export function saveSnapshot(
|
||||
|
||||
// keep the old merkle roots from other tokenIds
|
||||
for (const key of Object.keys(metadata.merkle || {})) {
|
||||
merkleInfos[key] = metadata.merkle[key];
|
||||
const merkleInfo = isRecord(metadata.merkle)
|
||||
? metadata.merkle[key]
|
||||
: undefined;
|
||||
if (merkleInfo) {
|
||||
merkleInfos[key] = merkleInfo;
|
||||
}
|
||||
}
|
||||
const mergedMetadata = {
|
||||
...metadata,
|
||||
|
||||
@@ -20,10 +20,9 @@ export { isContractURISupported as isGetContractMetadataSupported } from "../__g
|
||||
export async function getContractMetadata(
|
||||
options: BaseTransactionOptions,
|
||||
): Promise<{
|
||||
name: string;
|
||||
symbol: string;
|
||||
// biome-ignore lint/suspicious/noExplicitAny: TODO: fix any
|
||||
[key: string]: any;
|
||||
name: string | null;
|
||||
symbol: string | null;
|
||||
[key: string]: unknown;
|
||||
}> {
|
||||
const [resolvedMetadata, resolvedName, resolvedSymbol] = await Promise.all([
|
||||
contractURI(options)
|
||||
@@ -43,8 +42,14 @@ export async function getContractMetadata(
|
||||
|
||||
// TODO: basic parsing?
|
||||
return {
|
||||
...resolvedMetadata,
|
||||
name: resolvedMetadata?.name ?? resolvedName,
|
||||
symbol: resolvedMetadata?.symbol ?? resolvedSymbol,
|
||||
...(resolvedMetadata ?? {}),
|
||||
name:
|
||||
resolvedMetadata?.name && typeof resolvedMetadata.name === "string"
|
||||
? resolvedMetadata.name
|
||||
: resolvedName,
|
||||
symbol:
|
||||
resolvedMetadata?.symbol && typeof resolvedMetadata.symbol === "string"
|
||||
? resolvedMetadata.symbol
|
||||
: resolvedSymbol,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,6 +37,6 @@ export function useSocialProfiles(options: {
|
||||
return await getSocialProfiles({ address, client });
|
||||
},
|
||||
queryKey: ["social-profiles", address],
|
||||
retry: 3,
|
||||
retry: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -205,6 +205,7 @@ export function FundWallet(props: FundWalletProps) {
|
||||
transform: "none",
|
||||
}}
|
||||
setOpen={(v) => setIsTokenSelectionOpen(v)}
|
||||
autoFocusCrossIcon={false}
|
||||
>
|
||||
<SelectToken
|
||||
activeWalletInfo={activeWalletInfo}
|
||||
|
||||
@@ -11,6 +11,7 @@ export function SearchInput(props: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder: string;
|
||||
autoFocus?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@@ -41,6 +42,7 @@ export function SearchInput(props: {
|
||||
paddingLeft: "44px",
|
||||
}}
|
||||
onChange={(e) => props.onChange(e.target.value)}
|
||||
autoFocus={props.autoFocus}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -496,6 +496,7 @@ function TokenSelectionScreen(props: {
|
||||
value={props.search}
|
||||
onChange={props.setSearch}
|
||||
placeholder="Search by token or address"
|
||||
autoFocus={!props.isMobile}
|
||||
/>
|
||||
</Container>
|
||||
|
||||
|
||||
@@ -204,6 +204,7 @@ export function SwapUI(props: SwapUIProps) {
|
||||
<Container p="md">
|
||||
<Modal
|
||||
hide={false}
|
||||
autoFocusCrossIcon={false}
|
||||
className="tw-modal__swap-widget"
|
||||
size={isMobile ? "compact" : "wide"}
|
||||
title="Select Token"
|
||||
@@ -398,13 +399,14 @@ export function SwapUI(props: SwapUIProps) {
|
||||
<Text
|
||||
size="sm"
|
||||
color="danger"
|
||||
multiline
|
||||
center
|
||||
style={{
|
||||
paddingBlock: spacing.md,
|
||||
}}
|
||||
>
|
||||
{preparedResultQuery.error
|
||||
? "Failed to get a quote"
|
||||
? preparedResultQuery.error.message || "Failed to get a quote"
|
||||
: buyTokenQuery.isError
|
||||
? "Failed to fetch buy token details"
|
||||
: sellTokenQuery.isError
|
||||
|
||||
@@ -41,6 +41,7 @@ export const Modal: React.FC<{
|
||||
title: string;
|
||||
hide?: boolean;
|
||||
crossContainerStyles?: React.CSSProperties;
|
||||
autoFocusCrossIcon?: boolean;
|
||||
}> = (props) => {
|
||||
const [open, setOpen] = useState(props.open);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
@@ -143,7 +144,15 @@ export const Modal: React.FC<{
|
||||
}}
|
||||
>
|
||||
<Dialog.Close asChild>
|
||||
<IconButton aria-label="Close" autoFocus type="button">
|
||||
<IconButton
|
||||
aria-label="Close"
|
||||
autoFocus={
|
||||
props.autoFocusCrossIcon === undefined
|
||||
? true
|
||||
: props.autoFocusCrossIcon
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<Cross2Icon
|
||||
height={iconSize.md}
|
||||
style={{
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getChainMetadata } from "../../../../../chains/utils.js";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
|
||||
import { getContract } from "../../../../../contract/contract.js";
|
||||
import { getContractMetadata } from "../../../../../extensions/common/read/getContractMetadata.js";
|
||||
import { getToken } from "../../../../../pay/convert/get-token.js";
|
||||
import { getFunctionId } from "../../../../../utils/function-id.js";
|
||||
import { resolveScheme } from "../../../../../utils/ipfs.js";
|
||||
import { useTokenContext } from "./provider.js";
|
||||
@@ -135,6 +136,14 @@ export function TokenIcon({
|
||||
return resolveScheme({ client, uri: possibleUrl });
|
||||
}
|
||||
|
||||
const bridgeToken = await getToken(client, address, chain.id).catch(
|
||||
() => null,
|
||||
);
|
||||
|
||||
if (bridgeToken?.iconUri) {
|
||||
return bridgeToken.iconUri;
|
||||
}
|
||||
|
||||
// Try to get the icon from the contractURI
|
||||
const contractMetadata = await getContractMetadata({
|
||||
contract: getContract({
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getContractMetadata } from "../../../../../../extensions/common/read/ge
|
||||
import { getNFT } from "../../../../../../extensions/erc1155/read/getNFT.js";
|
||||
import type { PreparedTransaction } from "../../../../../../transaction/prepare-transaction.js";
|
||||
import type { BaseTransactionOptions } from "../../../../../../transaction/types.js";
|
||||
import { isString } from "../../../../../../utils/type-guards.js";
|
||||
import type { Account } from "../../../../../../wallets/interfaces/wallet.js";
|
||||
import { useReadContract } from "../../../../../core/hooks/contract/useReadContract.js";
|
||||
import { useActiveAccount } from "../../../../../core/hooks/wallets/useActiveAccount.js";
|
||||
@@ -191,8 +192,10 @@ async function getPayMetadata(
|
||||
};
|
||||
}
|
||||
return {
|
||||
image: contractMetadata?.image,
|
||||
name: contractMetadata?.name,
|
||||
image: isString(contractMetadata?.image)
|
||||
? contractMetadata.image
|
||||
: undefined,
|
||||
name: isString(contractMetadata?.name) ? contractMetadata.name : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -11,6 +11,7 @@ import {
|
||||
createListing,
|
||||
} from "../../../../../../extensions/marketplace/direct-listings/write/createListing.js";
|
||||
import type { BaseTransactionOptions } from "../../../../../../transaction/types.js";
|
||||
import { isString } from "../../../../../../utils/type-guards.js";
|
||||
import { useReadContract } from "../../../../../core/hooks/contract/useReadContract.js";
|
||||
import type { TransactionButtonProps } from "../../../../../core/hooks/transaction/transaction-button-utils.js";
|
||||
import { useActiveAccount } from "../../../../../core/hooks/wallets/useActiveAccount.js";
|
||||
@@ -228,7 +229,9 @@ async function getPayMetadata(
|
||||
}
|
||||
|
||||
return {
|
||||
image: contractMetadata?.image,
|
||||
name: contractMetadata?.name,
|
||||
image: isString(contractMetadata?.image)
|
||||
? contractMetadata.image
|
||||
: undefined,
|
||||
name: isString(contractMetadata?.name) ? contractMetadata.name : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
+25
-18
@@ -1,27 +1,18 @@
|
||||
import type { Meta } from "@storybook/react";
|
||||
import { BridgeWidgetScript } from "../../../script-exports/bridge-widget-script.js";
|
||||
import {
|
||||
BridgeWidgetScript,
|
||||
type BridgeWidgetScriptProps,
|
||||
} from "../../../script-exports/bridge-widget-script.js";
|
||||
import { storyClient } from "../../utils.js";
|
||||
|
||||
const meta: Meta<typeof BridgeWidgetScript> = {
|
||||
title: "Bridge/BridgeWidgetScript",
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
decorators: [
|
||||
(Story) => {
|
||||
return (
|
||||
<div>
|
||||
<Story />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
export default meta;
|
||||
|
||||
export function BasicUsage() {
|
||||
return (
|
||||
<BridgeWidgetScript
|
||||
<Variant
|
||||
clientId={storyClient.clientId}
|
||||
buy={{ chainId: 8453, amount: "0.1" }}
|
||||
/>
|
||||
@@ -30,7 +21,7 @@ export function BasicUsage() {
|
||||
|
||||
export function LightTheme() {
|
||||
return (
|
||||
<BridgeWidgetScript
|
||||
<Variant
|
||||
clientId={storyClient.clientId}
|
||||
theme="light"
|
||||
buy={{ chainId: 8453, amount: "0.1" }}
|
||||
@@ -40,7 +31,7 @@ export function LightTheme() {
|
||||
|
||||
export function CurrencySet() {
|
||||
return (
|
||||
<BridgeWidgetScript
|
||||
<Variant
|
||||
clientId={storyClient.clientId}
|
||||
currency="JPY"
|
||||
buy={{ chainId: 8453, amount: "0.1" }}
|
||||
@@ -50,7 +41,7 @@ export function CurrencySet() {
|
||||
|
||||
export function NoThirdwebBranding() {
|
||||
return (
|
||||
<BridgeWidgetScript
|
||||
<Variant
|
||||
clientId={storyClient.clientId}
|
||||
theme="light"
|
||||
buy={{ chainId: 8453, amount: "0.1" }}
|
||||
@@ -61,7 +52,7 @@ export function NoThirdwebBranding() {
|
||||
|
||||
export function CustomTheme() {
|
||||
return (
|
||||
<BridgeWidgetScript
|
||||
<Variant
|
||||
clientId={storyClient.clientId}
|
||||
buy={{ chainId: 8453, amount: "0.1" }}
|
||||
theme={{
|
||||
@@ -77,3 +68,19 @@ export function CustomTheme() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Variant(props: BridgeWidgetScriptProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "40px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<BridgeWidgetScript {...props} theme="dark" />
|
||||
<BridgeWidgetScript {...props} theme="light" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { Meta } from "@storybook/react-vite";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
|
||||
import { SwapWidget } from "../../../react/web/ui/Bridge/swap-widget/SwapWidget.js";
|
||||
import {
|
||||
SwapWidget,
|
||||
type SwapWidgetProps,
|
||||
} from "../../../react/web/ui/Bridge/swap-widget/SwapWidget.js";
|
||||
import { storyClient } from "../../utils.js";
|
||||
|
||||
const meta = {
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
title: "Bridge/Swap/SwapWidget/Prefill",
|
||||
} satisfies Meta<typeof SwapWidget>;
|
||||
export default meta;
|
||||
|
||||
export function Buy_NativeToken() {
|
||||
return (
|
||||
<SwapWidget
|
||||
<Variant
|
||||
client={storyClient}
|
||||
prefill={{
|
||||
buyToken: {
|
||||
@@ -26,7 +26,7 @@ export function Buy_NativeToken() {
|
||||
|
||||
export function Buy_Base_USDC() {
|
||||
return (
|
||||
<SwapWidget
|
||||
<Variant
|
||||
client={storyClient}
|
||||
prefill={{
|
||||
buyToken: {
|
||||
@@ -40,7 +40,7 @@ export function Buy_Base_USDC() {
|
||||
|
||||
export function Buy_NativeToken_With_Amount() {
|
||||
return (
|
||||
<SwapWidget
|
||||
<Variant
|
||||
client={storyClient}
|
||||
prefill={{
|
||||
buyToken: {
|
||||
@@ -55,7 +55,7 @@ export function Buy_NativeToken_With_Amount() {
|
||||
|
||||
export function Sell_NativeToken() {
|
||||
return (
|
||||
<SwapWidget
|
||||
<Variant
|
||||
client={storyClient}
|
||||
prefill={{
|
||||
sellToken: {
|
||||
@@ -82,7 +82,7 @@ export function Sell_Base_USDC() {
|
||||
|
||||
export function Sell_NativeToken_With_Amount() {
|
||||
return (
|
||||
<SwapWidget
|
||||
<Variant
|
||||
client={storyClient}
|
||||
prefill={{
|
||||
sellToken: {
|
||||
@@ -97,7 +97,7 @@ export function Sell_NativeToken_With_Amount() {
|
||||
|
||||
export function Buy_And_Sell_NativeToken() {
|
||||
return (
|
||||
<SwapWidget
|
||||
<Variant
|
||||
client={storyClient}
|
||||
prefill={{
|
||||
// base native token
|
||||
@@ -114,3 +114,19 @@ export function Buy_And_Sell_NativeToken() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Variant(props: SwapWidgetProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "40px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<SwapWidget {...props} theme="dark" />
|
||||
<SwapWidget {...props} theme="light" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import type { Meta } from "@storybook/react";
|
||||
import { lightTheme } from "../../../react/core/design-system/index.js";
|
||||
import { SwapWidget } from "../../../react/web/ui/Bridge/swap-widget/SwapWidget.js";
|
||||
import {
|
||||
SwapWidget,
|
||||
type SwapWidgetProps,
|
||||
} from "../../../react/web/ui/Bridge/swap-widget/SwapWidget.js";
|
||||
import { storyClient } from "../../utils.js";
|
||||
|
||||
const meta: Meta<typeof SwapWidget> = {
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
title: "Bridge/Swap/SwapWidget",
|
||||
decorators: [
|
||||
(Story) => {
|
||||
return (
|
||||
<div>
|
||||
<Story />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
export default meta;
|
||||
|
||||
@@ -26,7 +17,7 @@ export function BasicUsage() {
|
||||
|
||||
export function CurrencySet() {
|
||||
return (
|
||||
<SwapWidget
|
||||
<Variant
|
||||
client={storyClient}
|
||||
currency="JPY"
|
||||
persistTokenSelections={false}
|
||||
@@ -36,7 +27,7 @@ export function CurrencySet() {
|
||||
|
||||
export function LightMode() {
|
||||
return (
|
||||
<SwapWidget
|
||||
<Variant
|
||||
client={storyClient}
|
||||
currency="JPY"
|
||||
theme="light"
|
||||
@@ -47,7 +38,7 @@ export function LightMode() {
|
||||
|
||||
export function NoThirdwebBranding() {
|
||||
return (
|
||||
<SwapWidget
|
||||
<Variant
|
||||
client={storyClient}
|
||||
currency="JPY"
|
||||
showThirdwebBranding={false}
|
||||
@@ -58,7 +49,7 @@ export function NoThirdwebBranding() {
|
||||
|
||||
export function CustomTheme() {
|
||||
return (
|
||||
<SwapWidget
|
||||
<Variant
|
||||
client={storyClient}
|
||||
currency="JPY"
|
||||
persistTokenSelections={false}
|
||||
@@ -74,3 +65,19 @@ export function CustomTheme() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Variant(props: SwapWidgetProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "40px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<SwapWidget {...props} theme="dark" />
|
||||
<SwapWidget {...props} theme="light" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,6 @@ import {
|
||||
import { storyClient } from "./utils.js";
|
||||
|
||||
const meta = {
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
title: "Bridge/Buy/BuyWidget",
|
||||
} satisfies Meta<typeof BuyWidget>;
|
||||
export default meta;
|
||||
@@ -143,7 +140,14 @@ export function LargeAmount() {
|
||||
|
||||
function Variant(props: BuyWidgetProps) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "40px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "40px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<BuyWidget {...props} theme="dark" />
|
||||
<BuyWidget {...props} theme="light" />
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,10 @@ export const ZKSYNC_SINGLETON_FACTORY =
|
||||
export const CONTRACT_DEPLOYER_ADDRESS =
|
||||
"0x0000000000000000000000000000000000008006" as const;
|
||||
export const KNOWN_CODES_STORAGE = "0x0000000000000000000000000000000000008004";
|
||||
export const PUBLISHED_PRIVATE_KEY = process
|
||||
? process.env.ZKSYNC_PUBLISHED_PRIVATE_KEY
|
||||
: undefined;
|
||||
export const PUBLISHED_PRIVATE_KEY =
|
||||
typeof process !== "undefined" && process && process.env
|
||||
? process.env.ZKSYNC_PUBLISHED_PRIVATE_KEY
|
||||
: undefined;
|
||||
|
||||
export const singletonFactoryAbi = [
|
||||
"function deploy(bytes32,bytes32,bytes) external payable",
|
||||
|
||||
@@ -18,8 +18,7 @@ export type FetchContractMetadata = {
|
||||
*/
|
||||
export async function fetchContractMetadata(
|
||||
options: FetchContractMetadata,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: TODO: fix any
|
||||
): Promise<{ [key: string]: any } | undefined> {
|
||||
): Promise<{ [key: string]: unknown } | undefined> {
|
||||
const { client, uri } = options;
|
||||
|
||||
// handle case where the URI is a base64 encoded JSON
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getContractMetadata } from "../../../extensions/common/read/getContract
|
||||
import { MerkleTree } from "../../../merkletree/MerkleTree.js";
|
||||
import { download } from "../../../storage/download.js";
|
||||
import type { Address } from "../../address.js";
|
||||
import { isRecord } from "../../type-guards.js";
|
||||
import { hashEntryERC1155 } from "./hash-entry-erc1155.js";
|
||||
import type {
|
||||
ClaimProofERC1155,
|
||||
@@ -54,7 +55,8 @@ export async function fetchProofsERC1155(options: {
|
||||
const metadata = await getContractMetadata({
|
||||
contract,
|
||||
});
|
||||
const merkleData: Record<string, string> = metadata.merkle || {};
|
||||
|
||||
const merkleData = isRecord(metadata.merkle) ? metadata.merkle : {};
|
||||
const snapshotUri = merkleData[merkleRoot];
|
||||
|
||||
if (!snapshotUri) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getContractMetadata } from "../../../extensions/common/read/getContract
|
||||
import { MerkleTree } from "../../../merkletree/MerkleTree.js";
|
||||
import { download } from "../../../storage/download.js";
|
||||
import type { Address } from "../../address.js";
|
||||
import { isRecord } from "../../type-guards.js";
|
||||
import { convertQuantity } from "../drops/convert-quantity.js";
|
||||
import { hashEntryERC20 } from "./hash-entry-erc20.js";
|
||||
import type {
|
||||
@@ -56,7 +57,7 @@ export async function fetchProofsERC20(options: {
|
||||
const metadata = await getContractMetadata({
|
||||
contract,
|
||||
});
|
||||
const merkleData: Record<string, string> = metadata.merkle || {};
|
||||
const merkleData = isRecord(metadata.merkle) ? metadata.merkle : {};
|
||||
const snapshotUri = merkleData[merkleRoot];
|
||||
|
||||
if (!snapshotUri) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getContractMetadata } from "../../../extensions/common/read/getContract
|
||||
import { MerkleTree } from "../../../merkletree/MerkleTree.js";
|
||||
import { download } from "../../../storage/download.js";
|
||||
import type { Address } from "../../address.js";
|
||||
import { isRecord } from "../../type-guards.js";
|
||||
import { hashEntryERC721 } from "./hash-entry-erc721.js";
|
||||
import type {
|
||||
ClaimProofERC721,
|
||||
@@ -54,7 +55,8 @@ export async function fetchProofsERC721(options: {
|
||||
const metadata = await getContractMetadata({
|
||||
contract,
|
||||
});
|
||||
const merkleData: Record<string, string> = metadata.merkle || {};
|
||||
|
||||
const merkleData = isRecord(metadata.merkle) ? metadata.merkle : {};
|
||||
const snapshotUri = merkleData[merkleRoot];
|
||||
|
||||
if (!snapshotUri) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { ThirdwebContract } from "../../../contract/contract.js";
|
||||
import { getContractMetadata } from "../../../extensions/common/read/getContractMetadata.js";
|
||||
import type { Hex } from "../../encoding/hex.js";
|
||||
import { isRecord } from "../../type-guards.js";
|
||||
import type { ClaimCondition, OverrideProof } from "./types.js";
|
||||
|
||||
export type GetClaimParamsOptions = {
|
||||
@@ -118,7 +119,8 @@ export async function getClaimParams(options: GetClaimParamsOptions) {
|
||||
const metadata = await getContractMetadata({
|
||||
contract: options.contract,
|
||||
});
|
||||
const merkleData: Record<string, string> = metadata.merkle || {};
|
||||
|
||||
const merkleData = isRecord(metadata.merkle) ? metadata.merkle : {};
|
||||
const snapshotUri = merkleData[cc.merkleRoot];
|
||||
|
||||
if (!snapshotUri) {
|
||||
|
||||
+7
-1
@@ -5,6 +5,7 @@ import type { SetClaimConditionsParams as GeneratedParams } from "../../../exten
|
||||
import { upload } from "../../../storage/upload.js";
|
||||
import { dateToSeconds } from "../../date.js";
|
||||
import { type Hex, toHex } from "../../encoding/hex.js";
|
||||
import { isRecord } from "../../type-guards.js";
|
||||
import { convertErc20Amount } from "../convert-erc20-amount.js";
|
||||
import { processOverrideList } from "./process-override-list.js";
|
||||
import type { ClaimConditionsInput } from "./types.js";
|
||||
@@ -74,7 +75,12 @@ export async function getMulticallSetClaimConditionTransactions(options: {
|
||||
});
|
||||
// keep the old merkle roots from other tokenIds
|
||||
for (const key of Object.keys(metadata.merkle || {})) {
|
||||
merkleInfos[key] = metadata.merkle[key];
|
||||
const merkleInfo = isRecord(metadata.merkle)
|
||||
? metadata.merkle[key]
|
||||
: undefined;
|
||||
if (merkleInfo) {
|
||||
merkleInfos[key] = merkleInfo;
|
||||
}
|
||||
}
|
||||
const mergedMetadata = {
|
||||
...metadata,
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
isObject,
|
||||
isObjectWithKeys,
|
||||
isRecord,
|
||||
isString,
|
||||
} from "./type-guards.js";
|
||||
|
||||
describe("isObject", () => {
|
||||
it("returns true for plain objects", () => {
|
||||
expect(isObject({})).toBe(true);
|
||||
expect(isObject({ a: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for non-null object-like values (arrays, dates, maps)", () => {
|
||||
expect(isObject([])).toBe(true);
|
||||
expect(isObject(new Date())).toBe(true);
|
||||
expect(isObject(new Map())).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for primitives, functions and null/undefined", () => {
|
||||
expect(isObject(null)).toBe(false);
|
||||
expect(isObject(undefined)).toBe(false);
|
||||
expect(isObject(123)).toBe(false);
|
||||
expect(isObject("str")).toBe(false);
|
||||
expect(isObject(true)).toBe(false);
|
||||
expect(isObject(() => {})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isString", () => {
|
||||
it("returns true for string primitives", () => {
|
||||
expect(isString("" as unknown)).toBe(true);
|
||||
expect(isString("hello" as unknown)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for non-strings and String objects", () => {
|
||||
// String objects are typeof "object"
|
||||
// eslint-disable-next-line no-new-wrappers
|
||||
expect(isString(new String("x") as unknown)).toBe(false);
|
||||
expect(isString(1 as unknown)).toBe(false);
|
||||
expect(isString({} as unknown)).toBe(false);
|
||||
expect(isString([] as unknown)).toBe(false);
|
||||
expect(isString(null as unknown)).toBe(false);
|
||||
expect(isString(undefined as unknown)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isObjectWithKeys", () => {
|
||||
it("returns true when object has all specified keys", () => {
|
||||
const value = { a: 1, b: 2 } as const;
|
||||
expect(isObjectWithKeys(value, ["a"])).toBe(true);
|
||||
expect(isObjectWithKeys(value, ["a", "b"])).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when any specified key is missing", () => {
|
||||
const value = { a: 1 } as const;
|
||||
expect(isObjectWithKeys(value, ["a", "b"])).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults to just checking object-ness when no keys are provided", () => {
|
||||
expect(isObjectWithKeys({})).toBe(true);
|
||||
expect(isObjectWithKeys([])).toBe(true);
|
||||
expect(isObjectWithKeys(123 as unknown)).toBe(false);
|
||||
});
|
||||
|
||||
it("works with arrays when keys exist on the array", () => {
|
||||
const arr = ["x"];
|
||||
expect(isObjectWithKeys(arr, ["0"])).toBe(true);
|
||||
expect(isObjectWithKeys(arr, ["length"])).toBe(true);
|
||||
expect(isObjectWithKeys([], ["0"])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isRecord", () => {
|
||||
it("returns true for plain object with string values (default)", () => {
|
||||
expect(isRecord({})).toBe(true);
|
||||
expect(isRecord({ a: "x", b: "y" })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for arrays and non-objects", () => {
|
||||
expect(isRecord([] as unknown)).toBe(false);
|
||||
expect(isRecord(1 as unknown)).toBe(false);
|
||||
expect(isRecord(null as unknown)).toBe(false);
|
||||
expect(isRecord(undefined as unknown)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when any value is not a string (default guards)", () => {
|
||||
expect(isRecord({ a: 1 })).toBe(false);
|
||||
expect(isRecord({ a: "x", b: 2 as unknown as string })).toBe(false);
|
||||
});
|
||||
|
||||
it("respects a custom value guard (numbers)", () => {
|
||||
const numberGuard = (v: unknown): v is number => typeof v === "number";
|
||||
expect(isRecord({ a: 1, b: 2 }, { value: numberGuard })).toBe(true);
|
||||
expect(isRecord({ a: 1, b: "x" }, { value: numberGuard })).toBe(false);
|
||||
});
|
||||
|
||||
it("applies a custom key guard", () => {
|
||||
const keyStartsWithA = (k: unknown): k is string =>
|
||||
typeof k === "string" && k.startsWith("a");
|
||||
expect(
|
||||
isRecord({ apple: "x", aardvark: "y" }, { key: keyStartsWithA }),
|
||||
).toBe(true);
|
||||
expect(isRecord({ banana: "x" }, { key: keyStartsWithA })).toBe(false);
|
||||
});
|
||||
|
||||
it("objects with only symbol keys are considered records (no enumerable string keys)", () => {
|
||||
const s = Symbol("s");
|
||||
const obj = { [s]: "x" } as Record<symbol, string>;
|
||||
// Object.entries ignores symbol keys; guard will see zero entries
|
||||
expect(isRecord(obj)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -4,10 +4,20 @@
|
||||
* @returns True if the value is an object, false otherwise.
|
||||
* @internal
|
||||
*/
|
||||
function isObject(value: unknown): value is object {
|
||||
export function isObject(value: unknown): value is object {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value is a string.
|
||||
* @param value - The value to check.
|
||||
* @returns True if the value is a string, false otherwise.
|
||||
* @internal
|
||||
*/
|
||||
export function isString(value: unknown): value is string {
|
||||
return typeof value === "string";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value is an object with specified keys.
|
||||
* @param value - The value to check.
|
||||
@@ -21,3 +31,29 @@ export function isObjectWithKeys<key extends string>(
|
||||
): value is Record<key, unknown> {
|
||||
return isObject(value) && keys.every((key) => key in value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value is a record with string values.
|
||||
* @param value - The value to check.
|
||||
* @returns True if the value is a record with string values, false otherwise.
|
||||
* @internal
|
||||
*/
|
||||
export function isRecord<
|
||||
K extends string | number | symbol = string,
|
||||
V = string,
|
||||
T extends Record<K, V> = Record<K, V>,
|
||||
>(
|
||||
value: unknown,
|
||||
guards?: {
|
||||
key?: (k: unknown) => k is K;
|
||||
value?: (v: unknown) => v is V;
|
||||
},
|
||||
): value is T {
|
||||
const keyGuard = guards?.key ?? isString;
|
||||
const valueGuard = guards?.value ?? isString;
|
||||
return (
|
||||
isObject(value) &&
|
||||
!Array.isArray(value) &&
|
||||
Object.entries(value).every(([k, v]) => keyGuard(k) && valueGuard(v))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# @thirdweb-dev/wagmi-adapter
|
||||
|
||||
## 0.2.168
|
||||
|
||||
## 0.2.167
|
||||
|
||||
## 0.2.166
|
||||
|
||||
## 0.2.165
|
||||
|
||||
@@ -56,5 +56,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.166"
|
||||
"version": "0.2.168"
|
||||
}
|
||||
|
||||
Generated
+65
-1109
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user