Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0c2ac63a9 | ||
|
|
170f14fb54 | ||
|
|
da2a2a0e86 | ||
|
|
e62fd3fe40 | ||
|
|
99a7cce624 | ||
|
|
61152dd098 | ||
|
|
05519fcdde | ||
|
|
7257747520 | ||
|
|
e71e7d70a6 | ||
|
|
8d47864967 | ||
|
|
4a8adc143f | ||
|
|
db538473cf | ||
|
|
2057546934 | ||
|
|
ba52e68eab | ||
|
|
0eff64824b | ||
|
|
ae7407dca4 | ||
|
|
a443b309dc | ||
|
|
ae2ff743c0 | ||
|
|
dfa8603593 | ||
|
|
9f1ca54c26 | ||
|
|
7df81b7323 | ||
|
|
ab74fcdaf8 | ||
|
|
1191d5dad9 | ||
|
|
20b5ba943a |
@@ -14,7 +14,7 @@ We use [Turborepo](https://turbo.build/repo/docs) to manage the repository, and
|
||||
|
||||
We use [pnpm](https://pnpm.io) for package management across the repo. `pnpm` is similar to `npm` or `yarn` but with more efficient disk space usage.
|
||||
|
||||
**With the v5 SDK, we've consolidated everything into a single project at [/packages/thirdweb](./packages/thirdweb). You can still find the legacy packages at [/legacy_packages](./legacy_packages).**
|
||||
**With the v5 SDK, we've consolidated everything into a single project at [/packages/thirdweb](../packages/thirdweb). You can still find the legacy packages at [/legacy_packages](../legacy_packages).**
|
||||
|
||||
This single package provides a performant & lightweight SDK to interact with any EVM chain across Node, React, and React Native. Learn more about how to use the thirdweb SDK in our [documentation](https://portal.thirdweb.com/typescript/v5).
|
||||
|
||||
|
||||
@@ -48,15 +48,15 @@ async function fetchAnalytics(
|
||||
);
|
||||
}
|
||||
// client id DEBUG OVERRIDE
|
||||
// ANALYTICS_SERVICE_URL.searchParams.delete("projectId");
|
||||
// ANALYTICS_SERVICE_URL.searchParams.delete("teamId");
|
||||
// ANALYTICS_SERVICE_URL.searchParams.append(
|
||||
// analyticsServiceUrl.searchParams.delete("projectId");
|
||||
// analyticsServiceUrl.searchParams.delete("teamId");
|
||||
// analyticsServiceUrl.searchParams.append(
|
||||
// "teamId",
|
||||
// "team_clmb33q9w00gn1x0u2ri8z0k0",
|
||||
// "team_cm0lde33r02344w129k5hm2xz",
|
||||
// );
|
||||
// ANALYTICS_SERVICE_URL.searchParams.append(
|
||||
// analyticsServiceUrl.searchParams.append(
|
||||
// "projectId",
|
||||
// "prj_clyqwud5y00u1na7nzxnzlz7o",
|
||||
// "prj_cm4rqwx9b002qrnsnr37wqpo6",
|
||||
// );
|
||||
|
||||
return fetch(analyticsServiceUrl, {
|
||||
@@ -377,7 +377,7 @@ export async function getEcosystemWalletUsage(args: {
|
||||
|
||||
export async function getUniversalBridgeUsage(args: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
projectId?: string;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
period?: "day" | "week" | "month" | "year" | "all";
|
||||
@@ -395,11 +395,10 @@ export async function getUniversalBridgeUsage(args: {
|
||||
console.error(
|
||||
`Failed to fetch universal bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
return json.data as UniversalBridgeStats[];
|
||||
}
|
||||
|
||||
@@ -430,6 +429,5 @@ export async function getUniversalBridgeWalletUsage(args: {
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
return json.data as UniversalBridgeWalletStats[];
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
isCurrency?: boolean;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
|
||||
@@ -39,9 +39,10 @@ export function getFiltersFromSearchParams(params: {
|
||||
};
|
||||
|
||||
const defaultInterval =
|
||||
differenceInCalendarDays(range.to, range.from) > 30
|
||||
? "week"
|
||||
: ("day" as const);
|
||||
params.interval ??
|
||||
(differenceInCalendarDays(range.to, range.from) > 30
|
||||
? ("week" as const)
|
||||
: ("day" as const));
|
||||
|
||||
return {
|
||||
range,
|
||||
@@ -50,6 +51,6 @@ export function getFiltersFromSearchParams(params: {
|
||||
? ("day" as const)
|
||||
: params.interval === "week"
|
||||
? ("week" as const)
|
||||
: defaultInterval,
|
||||
: (defaultInterval as "day" | "week"),
|
||||
};
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -41,6 +41,8 @@ import creatorCTA from "./temp-assets/creatorCTA.png";
|
||||
import lumiaBanner from "./temp-assets/lumiaBanner.png";
|
||||
import somniaBanner from "./temp-assets/somniaBanner.png";
|
||||
|
||||
import NeoXBanner from "./temp-assets/NeoXBanner.jpg";
|
||||
import NeoXCTA from "./temp-assets/NeoXCTA.jpg";
|
||||
import flowBanner from "./temp-assets/flowBanner.png";
|
||||
import flowCTA from "./temp-assets/flowCTA.png";
|
||||
import mantleBanner from "./temp-assets/mantle.png";
|
||||
@@ -545,6 +547,30 @@ const chainMetaRecord = {
|
||||
about:
|
||||
"Metal L2 is a banking-focused Layer 2 blockchain built on the Optimism Superchain, designed to enable direct on-chain fiat deposits through its connection to The Digital Banking Network—an open-source blockchain banking protocol developed by Metallicus",
|
||||
},
|
||||
//NEO X
|
||||
47763: {
|
||||
headerImgUrl: NeoXBanner.src,
|
||||
about:
|
||||
"Neo X is an EVM-compatible sidechain built by Neo, incorporating Neo's distinctive dBFT consensus mechanism. Serving as a bridge between Neo N3 and the widely used EVM network, Neo X will play a crucial role in expanding the Neo ecosystem and offering developers more opportunities for innovation.",
|
||||
cta: {
|
||||
backgroundImageUrl: NeoXCTA.src,
|
||||
title: "Learn more about Neo X",
|
||||
buttonLink: "https://x.neo.org/",
|
||||
buttonText: "Learn More",
|
||||
},
|
||||
},
|
||||
//NEO X Testnet
|
||||
12227332: {
|
||||
headerImgUrl: NeoXBanner.src,
|
||||
about:
|
||||
"Neo X is an EVM-compatible sidechain built by Neo, incorporating Neo's distinctive dBFT consensus mechanism. Serving as a bridge between Neo N3 and the widely used EVM network, Neo X will play a crucial role in expanding the Neo ecosystem and offering developers more opportunities for innovation.",
|
||||
cta: {
|
||||
backgroundImageUrl: NeoXCTA.src,
|
||||
title: "Learn more about Neo X",
|
||||
buttonLink: "https://x.neo.org/",
|
||||
buttonText: "Learn More",
|
||||
},
|
||||
},
|
||||
//0G-Newton-Testnet
|
||||
16600: {
|
||||
headerImgUrl: zeroGBanner.src,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
getClientTransactions,
|
||||
getInAppWalletUsage,
|
||||
getUniversalBridgeUsage,
|
||||
getUserOpUsage,
|
||||
getWalletConnections,
|
||||
getWalletUsers,
|
||||
@@ -14,6 +15,7 @@ import { redirect } from "next/navigation";
|
||||
import { type WalletId, getWalletInfo } from "thirdweb/wallets";
|
||||
import type {
|
||||
InAppWalletStats,
|
||||
UniversalBridgeStats,
|
||||
WalletStats,
|
||||
WalletUserStats,
|
||||
} from "types/analytics";
|
||||
@@ -24,7 +26,10 @@ import { PieChartCard } from "../../../../components/Analytics/PieChartCard";
|
||||
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
import { EmptyStateCard } from "app/(app)/team/components/Analytics/EmptyStateCard";
|
||||
import {
|
||||
EmptyStateCard,
|
||||
EmptyStateContent,
|
||||
} from "app/(app)/team/components/Analytics/EmptyStateCard";
|
||||
import { Suspense } from "react";
|
||||
import { TotalSponsoredChartCardUI } from "../../_components/TotalSponsoredCard";
|
||||
import { TransactionsChartCardUI } from "../../_components/TransactionsCard";
|
||||
@@ -100,6 +105,7 @@ async function OverviewPageContent(props: {
|
||||
userOpUsage,
|
||||
clientTransactionsTimeSeries,
|
||||
clientTransactions,
|
||||
universalBridgeUsage,
|
||||
] = await Promise.all([
|
||||
// Aggregated wallet connections
|
||||
getWalletConnections({
|
||||
@@ -148,6 +154,13 @@ async function OverviewPageContent(props: {
|
||||
to: range.to,
|
||||
period: "all",
|
||||
}),
|
||||
// Universal Bridge
|
||||
getUniversalBridgeUsage({
|
||||
teamId: teamId,
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
period: interval,
|
||||
}),
|
||||
]);
|
||||
|
||||
const isEmpty =
|
||||
@@ -164,8 +177,9 @@ async function OverviewPageContent(props: {
|
||||
<div className="flex grow flex-col gap-6">
|
||||
{walletUserStatsTimeSeries.some((w) => w.totalUsers !== 0) ? (
|
||||
<div className="">
|
||||
<UsersChartCard
|
||||
<AppHighlightsCard
|
||||
userStats={walletUserStatsTimeSeries}
|
||||
volumeStats={universalBridgeUsage}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
</div>
|
||||
@@ -218,70 +232,96 @@ async function OverviewPageContent(props: {
|
||||
);
|
||||
}
|
||||
|
||||
type UserMetrics = {
|
||||
totalUsers: number;
|
||||
type AggregatedMetrics = {
|
||||
activeUsers: number;
|
||||
newUsers: number;
|
||||
returningUsers: number;
|
||||
totalVolume: number;
|
||||
feesCollected: number;
|
||||
};
|
||||
|
||||
type TimeSeriesMetrics = UserMetrics & {
|
||||
type TimeSeriesMetrics = AggregatedMetrics & {
|
||||
date: string;
|
||||
};
|
||||
|
||||
function processTimeSeriesData(
|
||||
userStats: WalletUserStats[],
|
||||
volumeStats: UniversalBridgeStats[],
|
||||
): TimeSeriesMetrics[] {
|
||||
const metrics: TimeSeriesMetrics[] = [];
|
||||
|
||||
let cumulativeUsers = 0;
|
||||
for (const stat of userStats) {
|
||||
cumulativeUsers += stat.newUsers ?? 0;
|
||||
const volume = volumeStats
|
||||
.filter((v) => v.date === stat.date && v.status === "completed")
|
||||
.reduce((acc, curr) => acc + curr.amountUsdCents / 100, 0);
|
||||
|
||||
const fees = volumeStats
|
||||
.filter((v) => v.date === stat.date && v.status === "completed")
|
||||
.reduce((acc, curr) => acc + curr.developerFeeUsdCents / 100, 0);
|
||||
|
||||
metrics.push({
|
||||
date: stat.date,
|
||||
activeUsers: stat.totalUsers ?? 0,
|
||||
returningUsers: stat.returningUsers ?? 0,
|
||||
newUsers: stat.newUsers ?? 0,
|
||||
totalUsers: cumulativeUsers,
|
||||
totalVolume: volume,
|
||||
feesCollected: fees,
|
||||
});
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
function UsersChartCard({
|
||||
function AppHighlightsCard({
|
||||
userStats,
|
||||
volumeStats,
|
||||
searchParams,
|
||||
}: {
|
||||
userStats: WalletUserStats[];
|
||||
volumeStats: UniversalBridgeStats[];
|
||||
searchParams?: { [key: string]: string | string[] | undefined };
|
||||
}) {
|
||||
const timeSeriesData = processTimeSeriesData(userStats);
|
||||
const timeSeriesData = processTimeSeriesData(userStats, volumeStats);
|
||||
|
||||
const chartConfig = {
|
||||
activeUsers: { label: "Active Users", color: "hsl(var(--chart-1))" },
|
||||
totalUsers: { label: "Total Users", color: "hsl(var(--chart-2))" },
|
||||
newUsers: { label: "New Users", color: "hsl(var(--chart-3))" },
|
||||
returningUsers: {
|
||||
label: "Returning Users",
|
||||
color: "hsl(var(--chart-4))",
|
||||
totalVolume: {
|
||||
label: "Total Volume",
|
||||
color: "hsl(var(--chart-2))",
|
||||
isCurrency: true,
|
||||
emptyContent: (
|
||||
<EmptyStateContent
|
||||
metric="Payments"
|
||||
description="Onramp, swap, and bridge with thirdweb's Universal Bridge."
|
||||
link="https://portal.thirdweb.com/connect/pay/overview"
|
||||
/>
|
||||
),
|
||||
},
|
||||
feesCollected: {
|
||||
label: "Fee Revenue",
|
||||
color: "hsl(var(--chart-4))",
|
||||
isCurrency: true,
|
||||
emptyContent: (
|
||||
<EmptyStateContent
|
||||
metric="Fees"
|
||||
description="Your apps haven't collected any fees yet."
|
||||
link={"https://portal.thirdweb.com/connect/pay/fees"}
|
||||
/>
|
||||
),
|
||||
},
|
||||
activeUsers: { label: "Active Users", color: "hsl(var(--chart-1))" },
|
||||
newUsers: { label: "New Users", color: "hsl(var(--chart-3))" },
|
||||
} as const;
|
||||
|
||||
return (
|
||||
<CombinedBarChartCard
|
||||
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
|
||||
title="Users"
|
||||
title="App Highlights"
|
||||
chartConfig={chartConfig}
|
||||
activeChart={
|
||||
(searchParams?.usersChart as keyof UserMetrics) ?? "activeUsers"
|
||||
(searchParams?.appHighlights as keyof AggregatedMetrics) ??
|
||||
"totalVolume"
|
||||
}
|
||||
data={timeSeriesData}
|
||||
aggregateFn={(_data, key) =>
|
||||
// If there is only one data point, use that one, otherwise use the previous
|
||||
timeSeriesData.filter((d) => (d[key] as number) > 0).length >= 2
|
||||
? timeSeriesData[timeSeriesData.length - 2]?.[key]
|
||||
: timeSeriesData[timeSeriesData.length - 1]?.[key]
|
||||
timeSeriesData.reduce((acc, curr) => acc + curr[key], 0)
|
||||
}
|
||||
// Get the trend from the last two COMPLETE periods
|
||||
trendFn={(data, key) =>
|
||||
@@ -291,7 +331,7 @@ function UsersChartCard({
|
||||
1
|
||||
: undefined
|
||||
}
|
||||
queryKey="usersChart"
|
||||
queryKey="appHighlights"
|
||||
existingQueryParams={searchParams}
|
||||
/>
|
||||
);
|
||||
|
||||
+3
-3
@@ -14,11 +14,11 @@ export default async function Page(props: {
|
||||
team_slug: string;
|
||||
project_slug: string;
|
||||
}>;
|
||||
searchParams: {
|
||||
searchParams: Promise<{
|
||||
from?: string | undefined | string[];
|
||||
to?: string | undefined | string[];
|
||||
interval?: string | undefined | string[];
|
||||
};
|
||||
}>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const project = await getProject(params.team_slug, params.project_slug);
|
||||
@@ -36,7 +36,7 @@ export default async function Page(props: {
|
||||
});
|
||||
|
||||
return (
|
||||
<ResponsiveSearchParamsProvider value={props.searchParams}>
|
||||
<ResponsiveSearchParamsProvider value={searchParams}>
|
||||
<div>
|
||||
<div className="mb-4 flex justify-start">
|
||||
<PayAnalyticsFilter />
|
||||
|
||||
+9
-32
@@ -19,15 +19,10 @@ import { Loader2Icon, LockIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
arbitrumSepolia,
|
||||
baseSepolia,
|
||||
optimismSepolia,
|
||||
sepolia,
|
||||
} from "thirdweb/chains";
|
||||
import * as z from "zod";
|
||||
import { CopyTextButton } from "../../../../../../../../@/components/ui/CopyTextButton";
|
||||
import { useTrack } from "../../../../../../../../hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "../../../../../../../../hooks/chains/allChains";
|
||||
import type { Wallet } from "../server-wallets/wallet-table/types";
|
||||
import { SmartAccountCell } from "../server-wallets/wallet-table/wallet-table-ui.client";
|
||||
import { deleteUserAccessToken, getUserAccessToken } from "./utils";
|
||||
@@ -53,6 +48,7 @@ export function SendTestTransaction(props: {
|
||||
const [hasSentTx, setHasSentTx] = useState(false);
|
||||
const router = useDashboardRouter();
|
||||
const trackEvent = useTrack();
|
||||
const chainsQuery = useAllChainsData();
|
||||
|
||||
const userAccessToken =
|
||||
props.userAccessToken ?? getUserAccessToken(props.project.id) ?? "";
|
||||
@@ -227,32 +223,13 @@ export function SendTestTransaction(props: {
|
||||
<p className="text-sm">Network</p>
|
||||
<SingleNetworkSelector
|
||||
className="bg-background"
|
||||
chainIds={[
|
||||
baseSepolia.id,
|
||||
optimismSepolia.id,
|
||||
arbitrumSepolia.id,
|
||||
sepolia.id,
|
||||
1301,
|
||||
1946,
|
||||
1993,
|
||||
919,
|
||||
6342,
|
||||
10143,
|
||||
44787,
|
||||
80002,
|
||||
80069,
|
||||
98985,
|
||||
128123,
|
||||
167009,
|
||||
168587773,
|
||||
37714555429,
|
||||
1952959480,
|
||||
97,
|
||||
296,
|
||||
1740,
|
||||
4202,
|
||||
10200,
|
||||
]}
|
||||
chainIds={chainsQuery.allChains
|
||||
.filter(
|
||||
(chain) =>
|
||||
chain.testnet === true &&
|
||||
chain.stackType !== "zksync_stack",
|
||||
)
|
||||
.map((chain) => chain.chainId)}
|
||||
client={thirdwebClient}
|
||||
chainId={form.watch("chainId")}
|
||||
onChange={(chainId) => {
|
||||
|
||||
+4
@@ -32,6 +32,10 @@ type ExecutionResult4337Serialized =
|
||||
| {
|
||||
status: "QUEUED";
|
||||
}
|
||||
| {
|
||||
status: "FAILED";
|
||||
error: string;
|
||||
}
|
||||
| {
|
||||
status: "SUBMITTED";
|
||||
monitoringStatus: "WILL_MONITOR" | "CANNOT_MONITOR";
|
||||
|
||||
+48
-2
@@ -183,6 +183,52 @@ export async function createWalletAccessToken(props: {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "eoa:read",
|
||||
metadataPatterns: [
|
||||
{
|
||||
key: "projectId",
|
||||
rule: {
|
||||
pattern: props.project.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "teamId",
|
||||
rule: {
|
||||
pattern: props.project.teamId,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
rule: {
|
||||
pattern: "server-wallet",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "eoa:create",
|
||||
requiredMetadataPatterns: [
|
||||
{
|
||||
key: "projectId",
|
||||
rule: {
|
||||
pattern: props.project.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "teamId",
|
||||
rule: {
|
||||
pattern: props.project.teamId,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
rule: {
|
||||
pattern: "server-wallet",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
projectId: props.project.id,
|
||||
@@ -289,7 +335,6 @@ export async function createManagementAccessToken(props: {
|
||||
});
|
||||
if (res.success) {
|
||||
const data = res.data;
|
||||
// store the management access token in the project
|
||||
await updateProjectClient(
|
||||
{
|
||||
projectId: props.project.id,
|
||||
@@ -308,8 +353,9 @@ export async function createManagementAccessToken(props: {
|
||||
],
|
||||
},
|
||||
);
|
||||
return res;
|
||||
}
|
||||
return res;
|
||||
throw new Error(`Failed to create management access token: ${res.error}`);
|
||||
}
|
||||
|
||||
export function maskSecret(secret: string) {
|
||||
|
||||
+13
-2
@@ -8,12 +8,14 @@ import { ServerWalletsTable } from "./wallet-table/wallet-table";
|
||||
|
||||
export default async function TransactionsServerWalletsPage(props: {
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
}) {
|
||||
const vaultClient = await createVaultClient({
|
||||
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
|
||||
});
|
||||
|
||||
const { team_slug, project_slug } = await props.params;
|
||||
const { page } = await props.searchParams;
|
||||
const [authToken, project] = await Promise.all([
|
||||
getAuthToken(),
|
||||
getProject(team_slug, project_slug),
|
||||
@@ -30,6 +32,8 @@ export default async function TransactionsServerWalletsPage(props: {
|
||||
const managementAccessToken =
|
||||
projectEngineCloudService?.managementAccessToken;
|
||||
|
||||
const pageSize = 10;
|
||||
const currentPage = Number.parseInt(page ?? "1");
|
||||
const eoas = managementAccessToken
|
||||
? await listEoas({
|
||||
client: vaultClient,
|
||||
@@ -37,10 +41,14 @@ export default async function TransactionsServerWalletsPage(props: {
|
||||
auth: {
|
||||
accessToken: managementAccessToken,
|
||||
},
|
||||
options: {},
|
||||
options: {
|
||||
page: currentPage - 1,
|
||||
// @ts-expect-error - TODO: fix this
|
||||
page_size: pageSize,
|
||||
},
|
||||
},
|
||||
})
|
||||
: { data: { items: [] }, error: null, success: true };
|
||||
: { data: { items: [], totalRecords: 0 }, error: null, success: true };
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -50,6 +58,9 @@ export default async function TransactionsServerWalletsPage(props: {
|
||||
<div className="flex flex-col gap-8">
|
||||
<ServerWalletsTable
|
||||
wallets={eoas.data.items as Wallet[]}
|
||||
totalRecords={eoas.data.totalRecords}
|
||||
currentPage={currentPage}
|
||||
totalPages={Math.ceil(eoas.data.totalRecords / pageSize)}
|
||||
project={project}
|
||||
teamSlug={team_slug}
|
||||
managementAccessToken={managementAccessToken ?? undefined}
|
||||
|
||||
+69
@@ -19,12 +19,21 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { formatDistanceToNowStrict } from "date-fns";
|
||||
import { format } from "date-fns/format";
|
||||
import { SendIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_FACTORY_V0_7,
|
||||
predictSmartAccountAddress,
|
||||
} from "thirdweb/wallets/smart";
|
||||
import { Button } from "../../../../../../../../../@/components/ui/button";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "../../../../../../../../../@/components/ui/pagination";
|
||||
import { useThirdwebClient } from "../../../../../../../../../@/constants/thirdweb.client";
|
||||
import { useDashboardRouter } from "../../../../../../../../../@/lib/DashboardRouter";
|
||||
import { useV5DashboardChain } from "../../../../../../../../../lib/v5-adapter";
|
||||
@@ -36,11 +45,17 @@ export function ServerWalletsTableUI({
|
||||
project,
|
||||
teamSlug,
|
||||
managementAccessToken,
|
||||
totalRecords,
|
||||
currentPage,
|
||||
totalPages,
|
||||
}: {
|
||||
wallets: Wallet[];
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
managementAccessToken: string | undefined;
|
||||
totalRecords: number;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
}) {
|
||||
const [showSigners, setShowSigners] = useState(false);
|
||||
return (
|
||||
@@ -121,6 +136,60 @@ export function ServerWalletsTableUI({
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<div className="flex flex-col items-center p-6">
|
||||
<div className="mb-4 text-muted-foreground text-sm">
|
||||
Found {totalRecords} server wallets
|
||||
</div>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<Link
|
||||
href={`/team/${teamSlug}/${project.slug}/engine/cloud/server-wallets?page=${
|
||||
currentPage > 1 ? currentPage - 1 : 1
|
||||
}`}
|
||||
passHref
|
||||
legacyBehavior
|
||||
>
|
||||
<PaginationPrevious
|
||||
className={
|
||||
currentPage <= 1 ? "pointer-events-none opacity-50" : ""
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
</PaginationItem>
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
|
||||
(pageNumber) => (
|
||||
<PaginationItem key={`page-${pageNumber}`}>
|
||||
<Link
|
||||
href={`/team/${teamSlug}/${project.slug}/engine/cloud/server-wallets?page=${pageNumber}`}
|
||||
passHref
|
||||
>
|
||||
<PaginationLink isActive={currentPage === pageNumber}>
|
||||
{pageNumber}
|
||||
</PaginationLink>
|
||||
</Link>
|
||||
</PaginationItem>
|
||||
),
|
||||
)}
|
||||
<PaginationItem>
|
||||
<Link
|
||||
href={`/team/${teamSlug}/${project.slug}/engine/cloud/server-wallets?page=${
|
||||
currentPage < totalPages ? currentPage + 1 : totalPages
|
||||
}`}
|
||||
passHref
|
||||
>
|
||||
<PaginationNext
|
||||
className={
|
||||
currentPage >= totalPages
|
||||
? "pointer-events-none opacity-50"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+9
@@ -6,16 +6,25 @@ export function ServerWalletsTable({
|
||||
wallets,
|
||||
project,
|
||||
teamSlug,
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalRecords,
|
||||
managementAccessToken,
|
||||
}: {
|
||||
wallets: Wallet[];
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
managementAccessToken: string | undefined;
|
||||
totalRecords: number;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
}) {
|
||||
return (
|
||||
<ServerWalletsTableUI
|
||||
wallets={wallets}
|
||||
totalRecords={totalRecords}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
project={project}
|
||||
teamSlug={teamSlug}
|
||||
managementAccessToken={managementAccessToken}
|
||||
|
||||
+6
-1
@@ -36,12 +36,17 @@ export function TransactionDetailsUI({
|
||||
transactionHash,
|
||||
confirmedAt,
|
||||
createdAt,
|
||||
errorMessage,
|
||||
executionParams,
|
||||
executionResult,
|
||||
} = transaction;
|
||||
|
||||
const status = executionResult?.status as keyof typeof statusDetails;
|
||||
const errorMessage =
|
||||
executionResult && "error" in executionResult
|
||||
? executionResult.error
|
||||
: executionResult && "revertData" in executionResult
|
||||
? executionResult.revertData?.revertReason
|
||||
: null;
|
||||
|
||||
const chain = chainId ? idToChain.get(Number.parseInt(chainId)) : undefined;
|
||||
const explorer = chain?.explorers?.[0];
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "components/analytics/date-range-selector";
|
||||
import type {
|
||||
InAppWalletStats,
|
||||
UniversalBridgeStats,
|
||||
UserOpStats,
|
||||
WalletStats,
|
||||
WalletUserStats,
|
||||
@@ -16,12 +17,16 @@ import type {
|
||||
|
||||
import {
|
||||
getInAppWalletUsage,
|
||||
getUniversalBridgeUsage,
|
||||
getUserOpUsage,
|
||||
getWalletConnections,
|
||||
getWalletUsers,
|
||||
isProjectActive,
|
||||
} from "@/api/analytics";
|
||||
import { EmptyStateCard } from "app/(app)/team/components/Analytics/EmptyStateCard";
|
||||
import {
|
||||
EmptyStateCard,
|
||||
EmptyStateContent,
|
||||
} from "app/(app)/team/components/Analytics/EmptyStateCard";
|
||||
import { RangeSelector } from "components/analytics/range-selector";
|
||||
import { Suspense } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
@@ -116,6 +121,7 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
<div className="container flex max-w-7xl grow flex-col">
|
||||
<Suspense fallback={<GenericLoadingPage />}>
|
||||
<ProjectAnalytics
|
||||
params={params}
|
||||
project={project}
|
||||
range={range}
|
||||
interval={interval}
|
||||
@@ -133,12 +139,13 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
|
||||
async function ProjectAnalytics(props: {
|
||||
project: Project;
|
||||
params: PageParams;
|
||||
range: Range;
|
||||
interval: "day" | "week";
|
||||
searchParams: PageSearchParams;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { project, range, interval, searchParams, client } = props;
|
||||
const { project, params, range, interval, searchParams, client } = props;
|
||||
|
||||
// Fetch all analytics data in parallel
|
||||
const [
|
||||
@@ -147,6 +154,7 @@ async function ProjectAnalytics(props: {
|
||||
inAppWalletUsage,
|
||||
userOpUsageTimeSeries,
|
||||
userOpUsage,
|
||||
universalBridgeUsage,
|
||||
] = await Promise.allSettled([
|
||||
// Aggregated wallet connections
|
||||
getWalletConnections({
|
||||
@@ -187,22 +195,30 @@ async function ProjectAnalytics(props: {
|
||||
to: range.to,
|
||||
period: "all",
|
||||
}),
|
||||
// Universal Bridge
|
||||
getUniversalBridgeUsage({
|
||||
teamId: project.teamId,
|
||||
projectId: project.id,
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
period: interval,
|
||||
}),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col gap-6">
|
||||
{walletUserStatsTimeSeries.status === "fulfilled" &&
|
||||
universalBridgeUsage.status === "fulfilled" &&
|
||||
walletUserStatsTimeSeries.value.some((w) => w.totalUsers !== 0) ? (
|
||||
<div className="">
|
||||
<UsersChartCard
|
||||
<AppHighlightsCard
|
||||
chartKey={
|
||||
(searchParams.usersChart as
|
||||
| "totalUsers"
|
||||
| "activeUsers"
|
||||
| "newUsers"
|
||||
| "returningUsers") ?? "activeUsers"
|
||||
(searchParams.appHighlights as keyof AggregatedMetrics) ??
|
||||
"totalVolume"
|
||||
}
|
||||
params={params}
|
||||
userStats={walletUserStatsTimeSeries.value}
|
||||
volumeStats={universalBridgeUsage.value}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
</div>
|
||||
@@ -267,14 +283,14 @@ async function ProjectAnalytics(props: {
|
||||
);
|
||||
}
|
||||
|
||||
type UserMetrics = {
|
||||
totalUsers: number;
|
||||
type AggregatedMetrics = {
|
||||
activeUsers: number;
|
||||
newUsers: number;
|
||||
returningUsers: number;
|
||||
totalVolume: number;
|
||||
feesCollected: number;
|
||||
};
|
||||
|
||||
type TimeSeriesMetrics = UserMetrics & {
|
||||
type TimeSeriesMetrics = AggregatedMetrics & {
|
||||
date: string;
|
||||
};
|
||||
|
||||
@@ -283,52 +299,82 @@ type TimeSeriesMetrics = UserMetrics & {
|
||||
*/
|
||||
function processTimeSeriesData(
|
||||
userStats: WalletUserStats[],
|
||||
volumeStats: UniversalBridgeStats[],
|
||||
): TimeSeriesMetrics[] {
|
||||
const metrics: TimeSeriesMetrics[] = [];
|
||||
|
||||
let cumulativeUsers = 0;
|
||||
for (const stat of userStats) {
|
||||
cumulativeUsers += stat.newUsers ?? 0;
|
||||
const volume = volumeStats
|
||||
.filter((v) => v.date === stat.date && v.status === "completed")
|
||||
.reduce((acc, curr) => acc + curr.amountUsdCents / 100, 0);
|
||||
|
||||
const fees = volumeStats
|
||||
.filter((v) => v.date === stat.date && v.status === "completed")
|
||||
.reduce((acc, curr) => acc + curr.developerFeeUsdCents / 100, 0);
|
||||
|
||||
metrics.push({
|
||||
date: stat.date,
|
||||
activeUsers: stat.totalUsers ?? 0,
|
||||
returningUsers: stat.returningUsers ?? 0,
|
||||
newUsers: stat.newUsers ?? 0,
|
||||
totalUsers: cumulativeUsers,
|
||||
totalVolume: volume,
|
||||
feesCollected: fees,
|
||||
});
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
function UsersChartCard({
|
||||
function AppHighlightsCard({
|
||||
chartKey,
|
||||
userStats,
|
||||
volumeStats,
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
chartKey: keyof UserMetrics;
|
||||
chartKey: keyof AggregatedMetrics;
|
||||
userStats: WalletUserStats[];
|
||||
volumeStats: UniversalBridgeStats[];
|
||||
params: PageParams;
|
||||
searchParams?: { [key: string]: string | string[] | undefined };
|
||||
}) {
|
||||
const timeSeriesData = processTimeSeriesData(userStats);
|
||||
const timeSeriesData = processTimeSeriesData(userStats, volumeStats);
|
||||
|
||||
const chartConfig = {
|
||||
activeUsers: { label: "Active Users", color: "hsl(var(--chart-1))" },
|
||||
totalUsers: { label: "Total Users", color: "hsl(var(--chart-2))" },
|
||||
newUsers: { label: "New Users", color: "hsl(var(--chart-3))" },
|
||||
returningUsers: {
|
||||
label: "Returning Users",
|
||||
color: "hsl(var(--chart-4))",
|
||||
totalVolume: {
|
||||
label: "Total Volume",
|
||||
color: "hsl(var(--chart-2))",
|
||||
isCurrency: true,
|
||||
emptyContent: (
|
||||
<EmptyStateContent
|
||||
metric="Payments"
|
||||
description="Onramp, swap, and bridge with thirdweb's Universal Bridge."
|
||||
link="https://portal.thirdweb.com/connect/pay/overview"
|
||||
/>
|
||||
),
|
||||
},
|
||||
feesCollected: {
|
||||
label: "Fee Revenue",
|
||||
color: "hsl(var(--chart-4))",
|
||||
isCurrency: true,
|
||||
emptyContent: (
|
||||
<EmptyStateContent
|
||||
metric="Fees"
|
||||
description="Your app hasn't collected any fees yet."
|
||||
link={`/team/${params.team_slug}/${params.project_slug}/connect/universal-bridge/settings`}
|
||||
/>
|
||||
),
|
||||
},
|
||||
activeUsers: { label: "Active Users", color: "hsl(var(--chart-1))" },
|
||||
newUsers: { label: "New Users", color: "hsl(var(--chart-3))" },
|
||||
} as const;
|
||||
|
||||
return (
|
||||
<CombinedBarChartCard
|
||||
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
|
||||
title="Users"
|
||||
title="App Highlights"
|
||||
chartConfig={chartConfig}
|
||||
activeChart={chartKey}
|
||||
queryKey="usersChart"
|
||||
queryKey="appHighlights"
|
||||
data={timeSeriesData}
|
||||
aggregateFn={(_data, key) =>
|
||||
// If there is only one data point, use that one, otherwise use the previous
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PosthogIdentifierServer } from "components/wallets/PosthogIdentifierServer";
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
import { EnsureValidConnectedWalletLoginServer } from "../../components/EnsureValidConnectedWalletLogin/EnsureValidConnectedWalletLoginServer";
|
||||
@@ -28,6 +31,10 @@ export default async function RootTeamLayout(props: {
|
||||
return (
|
||||
<div className="flex min-h-dvh flex-col">
|
||||
<div className="flex grow flex-col">
|
||||
{team.billingPlan === "starter_legacy" && (
|
||||
<StarterLegacyDiscontinuedBanner teamSlug={team_slug} />
|
||||
)}
|
||||
|
||||
{team.billingStatus === "pastDue" && (
|
||||
<PastDueBanner teamSlug={team_slug} />
|
||||
)}
|
||||
@@ -50,3 +57,31 @@ export default async function RootTeamLayout(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StarterLegacyDiscontinuedBanner(props: {
|
||||
teamSlug: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="border-red-600 border-b bg-red-50 px-4 py-6 text-red-800 dark:border-red-700 dark:bg-red-950 dark:text-red-100">
|
||||
<div className="text-center">
|
||||
<p>Starter legacy plans are being discontinued on May 31, 2025</p>
|
||||
<p>
|
||||
To prevent service interruptions and losing access to your current
|
||||
features select a new plan
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
className="mt-3 gap-2 border border-red-600 bg-red-100 text-red-800 hover:bg-red-200 dark:border-red-700 dark:bg-red-900 dark:text-red-100 dark:hover:bg-red-800"
|
||||
>
|
||||
<Link
|
||||
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true`}
|
||||
>
|
||||
Select a new plan
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export function BarChart({
|
||||
}}
|
||||
className="aspect-auto h-[250px] w-full pt-6"
|
||||
>
|
||||
{data.length === 0 ? (
|
||||
{data.length === 0 || data.every((d) => d[activeKey] === 0) ? (
|
||||
<EmptyChartState> {emptyChartContent} </EmptyChartState>
|
||||
) : (
|
||||
<RechartsBarChart
|
||||
@@ -87,7 +87,7 @@ export function BarChart({
|
||||
});
|
||||
}}
|
||||
valueFormatter={(v: unknown) =>
|
||||
isCurrency
|
||||
isCurrency || chartConfig[activeKey]?.isCurrency
|
||||
? toUSD(v as number)
|
||||
: formatTickerNumber(v as number)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,12 @@ import { BarChart } from "./BarChart";
|
||||
import { Stat } from "./Stat";
|
||||
|
||||
type CombinedBarChartConfig<K extends string> = {
|
||||
[key in K]: { label: string; color: string };
|
||||
[key in K]: {
|
||||
label: string;
|
||||
color: string;
|
||||
isCurrency?: boolean;
|
||||
emptyContent?: React.ReactNode;
|
||||
};
|
||||
};
|
||||
|
||||
// TODO - don't reload page on tab change -> make this a client component, load all the charts at once on server and switch between them on client
|
||||
@@ -72,7 +77,7 @@ export function CombinedBarChartCard<
|
||||
<Stat
|
||||
label={chartConfig[key].label}
|
||||
value={
|
||||
isCurrency
|
||||
isCurrency || chartConfig[key].isCurrency
|
||||
? toUSD(aggregateFn(data, key) ?? 0)
|
||||
: (aggregateFn(data, key) ?? 0)
|
||||
}
|
||||
@@ -95,6 +100,7 @@ export function CombinedBarChartCard<
|
||||
chartConfig={chartConfig}
|
||||
data={data}
|
||||
activeKey={activeChart}
|
||||
emptyChartContent={chartConfig[activeChart].emptyContent}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -6,25 +6,46 @@ import Link from "next/link";
|
||||
export function EmptyStateCard({
|
||||
metric,
|
||||
link,
|
||||
}: { metric: string; link?: string }) {
|
||||
description,
|
||||
}: { metric: string; link?: string; description?: string }) {
|
||||
return (
|
||||
<Card className="container h-[300px] p-2 md:h-[400px]">
|
||||
<div className="flex h-full flex-col items-center justify-center gap-2 rounded-md border text-center">
|
||||
<div className="flex size-8 items-center justify-center rounded-md border bg-card text-muted-foreground">
|
||||
<PlugIcon className="size-4" />
|
||||
</div>
|
||||
<div className="font-semibold text-lg">No data available</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Your app may not be configured to use {metric}.
|
||||
</div>
|
||||
{link && (
|
||||
<Button asChild variant="primary" size="sm" className="mt-4">
|
||||
<Link href={link} target="_blank" className="text-sm">
|
||||
Configure {metric}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<EmptyStateContent
|
||||
metric={metric}
|
||||
link={link}
|
||||
description={description}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmptyStateContent({
|
||||
metric,
|
||||
description,
|
||||
link,
|
||||
}: {
|
||||
metric: string;
|
||||
description?: string;
|
||||
link?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-2 text-foreground">
|
||||
<div className="flex size-8 items-center justify-center rounded-md border bg-card">
|
||||
<PlugIcon className="size-4" />
|
||||
</div>
|
||||
<div className="font-semibold text-lg">No data available</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{description ?? `Your app may not be configured to use ${metric}.`}
|
||||
</div>
|
||||
{link && (
|
||||
<Button asChild variant="primary" size="sm" className="mt-4">
|
||||
<Link href={link} target="_blank" className="text-sm">
|
||||
Configure {metric}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { storybookThirdwebClient } from "../../../../../stories/utils";
|
||||
import { type AssetBalance, AssetsSectionUI } from "./AssetsSection";
|
||||
|
||||
const meta = {
|
||||
title: "Nebula/AssetsSection",
|
||||
component: AssetsSectionUI,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="mx-auto h-dvh w-full max-w-[300px] bg-card p-2">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof AssetsSectionUI>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const tokensStub: AssetBalance[] = [
|
||||
{
|
||||
chain_id: 8453,
|
||||
token_address: "0xe8e55a847bb446d967ef92f4580162fb8f2d3f38",
|
||||
name: "Broge",
|
||||
symbol: "BROGE",
|
||||
decimals: 18,
|
||||
balance: "10000000000000000000000",
|
||||
},
|
||||
{
|
||||
chain_id: 8453,
|
||||
token_address: "0x8d2757ea27aabf172da4cca4e5474c76016e3dc5",
|
||||
name: "clBTC",
|
||||
symbol: "clBTC",
|
||||
decimals: 18,
|
||||
balance: "2",
|
||||
},
|
||||
{
|
||||
chain_id: 8453,
|
||||
token_address: "0xb56d0839998fd79efcd15c27cf966250aa58d6d3",
|
||||
name: "BASED USA",
|
||||
symbol: "USA",
|
||||
decimals: 18,
|
||||
balance: "1000000000000000000",
|
||||
},
|
||||
{
|
||||
chain_id: 8453,
|
||||
token_address: "0x600c9b69a65fb6d2551623a53ddef17b050233cd",
|
||||
name: "BearPaw",
|
||||
symbol: "PAW",
|
||||
decimals: 18,
|
||||
balance: "48888800000000000000",
|
||||
},
|
||||
{
|
||||
chain_id: 8453,
|
||||
token_address: "0x4c96a67b0577358894407af7bc3158fc1dffbeb5",
|
||||
name: "Degen Point Of View",
|
||||
symbol: "POV",
|
||||
decimals: 18,
|
||||
balance: "69000000000000000000",
|
||||
},
|
||||
{
|
||||
chain_id: 8453,
|
||||
token_address: "0x4200000000000000000000000000000000000006",
|
||||
name: "Wrapped Ether",
|
||||
symbol: "WETH",
|
||||
decimals: 18,
|
||||
balance: "6237535850425",
|
||||
},
|
||||
];
|
||||
|
||||
export const MultipleAssets: Story = {
|
||||
args: {
|
||||
data: tokensStub,
|
||||
isPending: false,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleAsset: Story = {
|
||||
args: {
|
||||
data: tokensStub.slice(0, 1),
|
||||
isPending: false,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyAssets: Story = {
|
||||
args: {
|
||||
data: [],
|
||||
isPending: false,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
data: [],
|
||||
isPending: true,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,257 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { isProd } from "@/constants/env-utils";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { XIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
NATIVE_TOKEN_ADDRESS,
|
||||
type ThirdwebClient,
|
||||
defineChain,
|
||||
getAddress,
|
||||
toTokens,
|
||||
} from "thirdweb";
|
||||
import {
|
||||
Blobbie,
|
||||
TokenIcon,
|
||||
TokenProvider,
|
||||
useActiveAccount,
|
||||
useActiveWalletChain,
|
||||
} from "thirdweb/react";
|
||||
import { getWalletBalance } from "thirdweb/wallets";
|
||||
import { ChainIconClient } from "../../../../../components/icons/ChainIcon";
|
||||
import { useAllChainsData } from "../../../../../hooks/chains/allChains";
|
||||
import { nebulaAppThirdwebClient } from "../../utils/nebulaThirdwebClient";
|
||||
|
||||
export type AssetBalance = {
|
||||
chain_id: number;
|
||||
token_address: string;
|
||||
balance: string;
|
||||
name: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
};
|
||||
|
||||
export function AssetsSectionUI(props: {
|
||||
data: AssetBalance[];
|
||||
isPending: boolean;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
if (props.data.length === 0 && !props.isPending) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 px-2 py-1">
|
||||
<div className="rounded-full border p-1">
|
||||
<XIcon className="size-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">No Assets </div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{!props.isPending &&
|
||||
props.data.map((asset) => (
|
||||
<AssetItem
|
||||
key={`${asset.chain_id}-${asset.token_address}`}
|
||||
asset={asset}
|
||||
client={props.client}
|
||||
/>
|
||||
))}
|
||||
|
||||
{props.isPending &&
|
||||
new Array(10).fill(null).map((_, index) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
<SkeletonAssetItem key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SkeletonAssetItem() {
|
||||
return (
|
||||
<div className="flex h-[48px] items-center gap-2 px-2 py-1">
|
||||
<Skeleton className="size-8 rounded-full" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<Skeleton className="h-3 w-32 bg-muted" />
|
||||
<Skeleton className="h-3 w-24 bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AssetItem(props: {
|
||||
asset: AssetBalance;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { idToChain } = useAllChainsData();
|
||||
const chainMeta = idToChain.get(props.asset.chain_id);
|
||||
const isNativeToken = props.asset.token_address === NATIVE_TOKEN_ADDRESS;
|
||||
|
||||
return (
|
||||
<TokenProvider
|
||||
address={props.asset.token_address}
|
||||
client={props.client}
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain={defineChain(props.asset.chain_id)}
|
||||
>
|
||||
<div className="relative flex h-[48px] items-center gap-2.5 rounded-lg px-2 py-1 hover:bg-accent">
|
||||
<div className="relative">
|
||||
<TokenIcon
|
||||
className="size-8 rounded-full border"
|
||||
loadingComponent={
|
||||
<Blobbie
|
||||
address={props.asset.token_address}
|
||||
className="size-8 rounded-full"
|
||||
/>
|
||||
}
|
||||
fallbackComponent={
|
||||
<Blobbie
|
||||
address={props.asset.token_address}
|
||||
className="size-8 rounded-full"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{!isNativeToken && (
|
||||
<div className="-right-0.5 -bottom-0.5 absolute rounded-full border bg-background p-0.5">
|
||||
<ChainIconClient
|
||||
client={props.client}
|
||||
className="size-3.5"
|
||||
src={chainMeta?.icon?.url || ""}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-col text-sm">
|
||||
<Link
|
||||
href={
|
||||
isNativeToken
|
||||
? `https://thirdweb.com/${props.asset.chain_id}`
|
||||
: `https://thirdweb.com/${props.asset.chain_id}/${props.asset.token_address}`
|
||||
}
|
||||
target="_blank"
|
||||
className="truncate font-medium before:absolute before:inset-0"
|
||||
>
|
||||
{props.asset.name}
|
||||
</Link>
|
||||
|
||||
<p className="truncate text-muted-foreground text-sm">
|
||||
{`${toTokens(BigInt(props.asset.balance), props.asset.decimals)} ${props.asset.symbol}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</TokenProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function AssetsSection(props: {
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const account = useActiveAccount();
|
||||
const activeChain = useActiveWalletChain();
|
||||
|
||||
const assetsQuery = useQuery({
|
||||
queryKey: ["v1/tokens/erc20", account?.address, activeChain?.id],
|
||||
queryFn: async () => {
|
||||
if (!account || !activeChain) {
|
||||
return [];
|
||||
}
|
||||
const chains = [...new Set([1, 8453, 10, 137, activeChain.id])];
|
||||
const url = new URL(
|
||||
`https://insight.${isProd ? "thirdweb" : "thirdweb-dev"}.com/v1/tokens/erc20/${account?.address}`,
|
||||
);
|
||||
url.searchParams.set("limit", "50");
|
||||
url.searchParams.set("metadata", "true");
|
||||
url.searchParams.set("include_spam", "false");
|
||||
url.searchParams.set("clientId", nebulaAppThirdwebClient.clientId);
|
||||
for (const chain of chains) {
|
||||
url.searchParams.append("chain", chain.toString());
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
const json = (await response.json()) as {
|
||||
data: AssetBalance[];
|
||||
};
|
||||
|
||||
const tokensToShowOnTop = new Set(
|
||||
[
|
||||
// base
|
||||
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", // usdc,
|
||||
"0x0555E30da8f98308EdB960aa94C0Db47230d2B9c", // wbtc
|
||||
"0x4200000000000000000000000000000000000006", // wrapped eth
|
||||
// ethereum
|
||||
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // usdc
|
||||
"0xdac17f958d2ee523a2206206994597c13d831ec7", // usdt
|
||||
"0xB8c77482e45F1F44dE1745F52C74426C631bDD52", // bnb
|
||||
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // weth
|
||||
"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", // wbtc
|
||||
// optimism
|
||||
"0x4200000000000000000000000000000000000042", // op token
|
||||
"0xdc6ff44d5d932cbd77b52e5612ba0529dc6226f1", // world coin
|
||||
"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58", // usdt
|
||||
"0x0b2c639c533813f4aa9d7837caf62653d097ff85", // usdc
|
||||
"0x4200000000000000000000000000000000000006", // wrapped eth
|
||||
// polygon
|
||||
"0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", // weth
|
||||
"0xc2132d05d31c914a87c6611c10748aeb04b58e8f", // usdt
|
||||
"0x3BA4c387f786bFEE076A58914F5Bd38d668B42c3", // bnb
|
||||
"0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // usdc
|
||||
"0x2791bca1f2de4661ed88a30c99a7a9449aa84174", // usdc.e
|
||||
].map((x) => getAddress(x)),
|
||||
);
|
||||
|
||||
return json.data.sort((a, b) => {
|
||||
if (tokensToShowOnTop.has(getAddress(a.token_address))) {
|
||||
return -1;
|
||||
}
|
||||
if (tokensToShowOnTop.has(getAddress(b.token_address))) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
enabled: !!account && !!activeChain,
|
||||
});
|
||||
|
||||
const nativeBalances = useQuery({
|
||||
queryKey: ["getWalletBalance", account?.address, activeChain?.id],
|
||||
queryFn: async () => {
|
||||
if (!account || !activeChain) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const chains = [...new Set([1, 8453, 10, 137, activeChain.id])];
|
||||
|
||||
const result = await Promise.allSettled(
|
||||
chains.map((chain) =>
|
||||
getWalletBalance({
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(chain),
|
||||
client: props.client,
|
||||
address: account.address,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return result
|
||||
.filter((r) => r.status === "fulfilled")
|
||||
.map((r) => ({
|
||||
chain_id: r.value.chainId,
|
||||
token_address: r.value.tokenAddress,
|
||||
balance: r.value.value.toString(),
|
||||
name: r.value.name,
|
||||
symbol: r.value.symbol,
|
||||
decimals: r.value.decimals,
|
||||
}))
|
||||
.filter((x) => x.balance !== "0") as AssetBalance[];
|
||||
},
|
||||
});
|
||||
|
||||
const isPending = assetsQuery.isPending || nativeBalances.isPending;
|
||||
|
||||
const data = [...(nativeBalances.data ?? []), ...(assetsQuery.data ?? [])];
|
||||
|
||||
return (
|
||||
<AssetsSectionUI data={data} isPending={isPending} client={props.client} />
|
||||
);
|
||||
}
|
||||
@@ -36,8 +36,10 @@ import {
|
||||
AccountBlobbie,
|
||||
AccountName,
|
||||
AccountProvider,
|
||||
WalletIcon,
|
||||
WalletName,
|
||||
WalletProvider,
|
||||
useActiveWallet,
|
||||
} from "thirdweb/react";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import type { Wallet } from "thirdweb/wallets";
|
||||
@@ -451,8 +453,18 @@ function WalletSelector(props: {
|
||||
selectedAddress: string | undefined;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const activeWallet = useActiveWallet();
|
||||
|
||||
if (!props.selectedAddress) {
|
||||
const accountBlobbie = <AccountBlobbie className="size-3 rounded-full" />;
|
||||
const accountAvatarFallback = (
|
||||
<WalletIcon
|
||||
className="size-3 rounded-lg"
|
||||
fallbackComponent={accountBlobbie}
|
||||
loadingComponent={accountBlobbie}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!props.selectedAddress || !activeWallet) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -471,34 +483,32 @@ function WalletSelector(props: {
|
||||
aria-expanded={open}
|
||||
className="flex h-auto items-center gap-1 rounded-full px-2 py-1.5 text-xs"
|
||||
>
|
||||
<AccountProvider
|
||||
address={props.selectedAddress}
|
||||
client={props.client}
|
||||
>
|
||||
<AccountAvatar
|
||||
className="size-4 rounded-full"
|
||||
loadingComponent={
|
||||
<Skeleton className="size-3 rounded-full border" />
|
||||
}
|
||||
fallbackComponent={
|
||||
<AccountBlobbie className="size-3 rounded-full" />
|
||||
}
|
||||
/>
|
||||
<AccountName
|
||||
className="text-xs"
|
||||
loadingComponent={
|
||||
<span className="text-xs">
|
||||
{shortenAddress(props.selectedAddress)}
|
||||
</span>
|
||||
}
|
||||
fallbackComponent={
|
||||
<span className="text-xs">
|
||||
{shortenAddress(props.selectedAddress)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<ChevronDownIcon className="size-3 text-muted-foreground/70" />
|
||||
</AccountProvider>
|
||||
<WalletProvider id={activeWallet.id}>
|
||||
<AccountProvider
|
||||
address={props.selectedAddress}
|
||||
client={props.client}
|
||||
>
|
||||
<AccountAvatar
|
||||
className="size-3 rounded-full"
|
||||
loadingComponent={accountAvatarFallback}
|
||||
fallbackComponent={accountAvatarFallback}
|
||||
/>
|
||||
<AccountName
|
||||
className="text-xs"
|
||||
loadingComponent={
|
||||
<span className="text-xs">
|
||||
{shortenAddress(props.selectedAddress)}
|
||||
</span>
|
||||
}
|
||||
fallbackComponent={
|
||||
<span className="text-xs">
|
||||
{shortenAddress(props.selectedAddress)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<ChevronDownIcon className="size-3 text-muted-foreground/70" />
|
||||
</AccountProvider>
|
||||
</WalletProvider>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -513,81 +523,107 @@ function WalletSelector(props: {
|
||||
</div>
|
||||
|
||||
<div className="[&>*:not(:last-child)]:border-b">
|
||||
{sortedWallets.map((wallet) => (
|
||||
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
|
||||
<div
|
||||
key={wallet.address}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center justify-between px-3 py-4 hover:bg-accent/50",
|
||||
props.selectedAddress === wallet.address && "bg-accent/50",
|
||||
)}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
props.onClick(wallet);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<AccountProvider address={wallet.address} client={props.client}>
|
||||
<WalletProvider id={wallet.walletId}>
|
||||
<div className="flex items-center gap-2">
|
||||
<AccountAvatar
|
||||
className="size-8 rounded-full"
|
||||
loadingComponent={
|
||||
<Skeleton className="size-8 rounded-full border" />
|
||||
}
|
||||
fallbackComponent={
|
||||
<AccountBlobbie className="size-8 rounded-full" />
|
||||
}
|
||||
/>
|
||||
{sortedWallets.map((wallet) => {
|
||||
const accountBlobbie = (
|
||||
<AccountBlobbie className="size-8 rounded-full" />
|
||||
);
|
||||
const accountAvatarFallback = (
|
||||
<WalletIcon
|
||||
className="size-8 rounded-lg"
|
||||
fallbackComponent={accountBlobbie}
|
||||
loadingComponent={accountBlobbie}
|
||||
/>
|
||||
);
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<AccountName
|
||||
className="text-sm"
|
||||
loadingComponent={
|
||||
<span className="text-sm">
|
||||
{shortenAddress(wallet.address)}
|
||||
</span>
|
||||
}
|
||||
fallbackComponent={
|
||||
<span className="text-sm">
|
||||
{shortenAddress(wallet.address)}
|
||||
</span>
|
||||
}
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
key={wallet.address}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center justify-between px-3 py-4 hover:bg-accent/50",
|
||||
props.selectedAddress === wallet.address && "bg-accent/50",
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
setOpen(false);
|
||||
props.onClick(wallet);
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
props.onClick(wallet);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<WalletProvider id={wallet.walletId}>
|
||||
<AccountProvider
|
||||
address={wallet.address}
|
||||
client={props.client}
|
||||
>
|
||||
<WalletProvider id={wallet.walletId}>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<AccountAvatar
|
||||
className="size-8 rounded-full"
|
||||
loadingComponent={accountAvatarFallback}
|
||||
fallbackComponent={accountAvatarFallback}
|
||||
/>
|
||||
|
||||
<CopyButton address={wallet.address} />
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-1">
|
||||
<AccountName
|
||||
className="text-sm"
|
||||
loadingComponent={
|
||||
<span className="text-sm">
|
||||
{shortenAddress(wallet.address)}
|
||||
</span>
|
||||
}
|
||||
fallbackComponent={
|
||||
<span className="text-sm">
|
||||
{shortenAddress(wallet.address)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
||||
{wallet.walletId === "smart" && (
|
||||
<Badge variant="outline" className="bg-card px-2">
|
||||
Gasless
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<CopyButton address={wallet.address} />
|
||||
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{wallet.walletId === "smart" ? (
|
||||
"Smart Account"
|
||||
) : (
|
||||
<WalletName
|
||||
fallbackComponent={<span> Your Account </span>}
|
||||
loadingComponent={
|
||||
<Skeleton className="h-3.5 w-40" />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{wallet.walletId === "smart" && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-card px-2"
|
||||
>
|
||||
Gasless
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{wallet.walletId === "smart" ? (
|
||||
"Smart Account"
|
||||
) : (
|
||||
<WalletName
|
||||
fallbackComponent={
|
||||
<span> Your Account </span>
|
||||
}
|
||||
loadingComponent={
|
||||
<Skeleton className="h-3.5 w-40" />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WalletProvider>
|
||||
</AccountProvider>
|
||||
</WalletProvider>
|
||||
</AccountProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{props.selectedAddress === wallet.address && (
|
||||
<CheckIcon className="size-4 text-foreground" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{props.selectedAddress === wallet.address && (
|
||||
<CheckIcon className="size-4 text-foreground" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -17,7 +17,7 @@ export function ChatPageLayout(props: {
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<aside className="hidden w-[280px] shrink-0 border-border border-r bg-card lg:block">
|
||||
<aside className="hidden w-[300px] shrink-0 border-border border-r bg-card lg:block">
|
||||
<ChatSidebar
|
||||
sessions={props.sessions}
|
||||
authToken={props.authToken}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
"use client";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TabButtons } from "@/components/ui/tabs";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
FileCode2Icon,
|
||||
LogOutIcon,
|
||||
@@ -14,15 +19,27 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
AccountAvatar,
|
||||
AccountBlobbie,
|
||||
AccountProvider,
|
||||
useActiveWallet,
|
||||
} from "thirdweb/react";
|
||||
import { WalletIcon, WalletName, WalletProvider } from "thirdweb/react";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import { useIsClientMounted } from "../../../../components/ClientOnly/ClientOnly";
|
||||
import { doNebulaLogout } from "../../login/auth-actions";
|
||||
import type { TruncatedSessionInfo } from "../api/types";
|
||||
import { useNewChatPageLink } from "../hooks/useNewChatPageLink";
|
||||
import { useSessionsWithLocalOverrides } from "../hooks/useSessionsWithLocalOverrides";
|
||||
import { NebulaIcon } from "../icons/NebulaIcon";
|
||||
import { nebulaAppThirdwebClient } from "../utils/nebulaThirdwebClient";
|
||||
import { AssetsSection } from "./AssetsSection/AssetsSection";
|
||||
import { ChatSidebarLink } from "./ChatSidebarLink";
|
||||
import { NebulaConnectWallet } from "./NebulaConnectButton";
|
||||
import { TransactionsSection } from "./TransactionsSection/TransactionsSection";
|
||||
|
||||
export function ChatSidebar(props: {
|
||||
sessions: TruncatedSessionInfo[];
|
||||
@@ -97,7 +114,11 @@ export function ChatSidebar(props: {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-auto space-y-1 border-y border-dashed px-2 py-4">
|
||||
<div className="mt-auto">
|
||||
<WalletDetails />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-2 border-t px-4 py-3">
|
||||
<SidebarIconLink
|
||||
href="https://portal.thirdweb.com/nebula"
|
||||
icon={FileCode2Icon}
|
||||
@@ -120,10 +141,122 @@ export function ChatSidebar(props: {
|
||||
label="Log Out"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div className="[&>*]:!w-full p-4">
|
||||
<NebulaConnectWallet detailsButtonClassName="!bg-background hover:!border-active-border" />
|
||||
function WalletDetails() {
|
||||
const [tab, setTab] = useState<"assets" | "transactions">("assets");
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
return (
|
||||
<DynamicHeight transition="height 220ms ease">
|
||||
<div className="flex items-center justify-between gap-2 border-t px-2 py-1.5">
|
||||
<CustomConnectButton />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-auto w-auto p-1.5 transition-transform duration-300",
|
||||
isExpanded ? "rotate-180" : "",
|
||||
)}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="border-t border-dashed">
|
||||
<TabButtons
|
||||
tabContainerClassName="px-2 pt-2"
|
||||
tabClassName="!text-sm py-1.5"
|
||||
tabs={[
|
||||
{
|
||||
name: "Assets",
|
||||
onClick: () => setTab("assets"),
|
||||
isActive: tab === "assets",
|
||||
},
|
||||
{
|
||||
name: "Recent Activity",
|
||||
onClick: () => setTab("transactions"),
|
||||
isActive: tab === "transactions",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="h-[230px] overflow-y-auto py-4 pr-1 pl-2">
|
||||
{tab === "assets" && (
|
||||
<AssetsSection client={nebulaAppThirdwebClient} />
|
||||
)}
|
||||
|
||||
{tab === "transactions" && (
|
||||
<TransactionsSection client={nebulaAppThirdwebClient} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DynamicHeight>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomConnectButton() {
|
||||
const activeWallet = useActiveWallet();
|
||||
const accountBlobbie = <AccountBlobbie className="size-8 rounded-full" />;
|
||||
const accountAvatarFallback = (
|
||||
<WalletIcon
|
||||
className="size-8 rounded-lg"
|
||||
fallbackComponent={accountBlobbie}
|
||||
loadingComponent={accountBlobbie}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="[&>*]:!w-full grow">
|
||||
<NebulaConnectWallet
|
||||
detailsButtonClassName="!bg-background hover:!border-active-border"
|
||||
customDetailsButton={
|
||||
activeWallet
|
||||
? (address) => {
|
||||
return (
|
||||
<WalletProvider id={activeWallet.id}>
|
||||
<AccountProvider
|
||||
address={address}
|
||||
client={nebulaAppThirdwebClient}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex h-auto w-full items-center justify-start gap-2.5 rounded-lg px-2 hover:bg-accent"
|
||||
>
|
||||
<AccountAvatar
|
||||
className="size-8 rounded-full"
|
||||
fallbackComponent={accountAvatarFallback}
|
||||
loadingComponent={accountAvatarFallback}
|
||||
/>
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<div className="text-sm">
|
||||
{shortenAddress(address)}
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{activeWallet.id === "smart" ? (
|
||||
<span> Smart Wallet </span>
|
||||
) : (
|
||||
<WalletName
|
||||
fallbackComponent={<span>Wallet</span>}
|
||||
loadingComponent={<span>Wallet</span>}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</AccountProvider>
|
||||
</WalletProvider>
|
||||
);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -140,7 +273,7 @@ function ToggleThemeButton() {
|
||||
icon={
|
||||
isClientMounted ? (theme === "light" ? SunIcon : MoonIcon) : Spinner
|
||||
}
|
||||
label="Theme"
|
||||
label="Toggle Theme"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -152,16 +285,22 @@ function SidebarIconLink(props: {
|
||||
href: string;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
className="h-auto w-full justify-start gap-2.5 rounded-lg px-2 py-2 text-left text-sm"
|
||||
>
|
||||
<Link href={props.href} target={props.target} prefetch={false}>
|
||||
<props.icon className="size-4" />
|
||||
{props.label}
|
||||
</Link>
|
||||
</Button>
|
||||
<ToolTipLabel label={props.label}>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
className="size-10 rounded-full p-0 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Link
|
||||
href={props.href}
|
||||
target={props.target}
|
||||
prefetch={false}
|
||||
aria-label={props.label}
|
||||
>
|
||||
<props.icon className="size-5" />
|
||||
</Link>
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -171,13 +310,15 @@ function SidebarIconButton(props: {
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-auto w-full justify-start gap-2.5 rounded-lg px-2 py-2 text-left text-sm"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<props.icon className="size-4" />
|
||||
{props.label}
|
||||
</Button>
|
||||
<ToolTipLabel label={props.label}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={props.onClick}
|
||||
aria-label={props.label}
|
||||
className="size-10 rounded-full p-0 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<props.icon className="size-5" />
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export const NebulaConnectWallet = (props: {
|
||||
connectButtonClassName?: string;
|
||||
signInLinkButtonClassName?: string;
|
||||
detailsButtonClassName?: string;
|
||||
customDetailsButton?: (address: string) => React.ReactElement;
|
||||
}) => {
|
||||
const router = useDashboardRouter();
|
||||
const { theme } = useTheme();
|
||||
@@ -63,6 +64,7 @@ export const NebulaConnectWallet = (props: {
|
||||
);
|
||||
}
|
||||
|
||||
const { customDetailsButton } = props;
|
||||
return (
|
||||
<ConnectButton
|
||||
theme={getSDKTheme(t)}
|
||||
@@ -90,6 +92,9 @@ export const NebulaConnectWallet = (props: {
|
||||
}}
|
||||
detailsButton={{
|
||||
className: props.detailsButtonClassName,
|
||||
render: customDetailsButton
|
||||
? () => customDetailsButton(account.address)
|
||||
: undefined,
|
||||
}}
|
||||
chains={allChainsV5}
|
||||
// we have an AutoConnect already added in root layout with AA configuration
|
||||
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { storybookThirdwebClient } from "../../../../../stories/utils";
|
||||
import {
|
||||
TransactionSectionUI,
|
||||
type WalletTransaction,
|
||||
} from "./TransactionsSection";
|
||||
|
||||
const meta = {
|
||||
title: "Nebula/TransactionsSection",
|
||||
component: TransactionSectionUI,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="mx-auto h-dvh w-full max-w-[300px] bg-card p-2">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof TransactionSectionUI>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const transactionsStub: WalletTransaction[] = [
|
||||
{
|
||||
chain_id: "8453",
|
||||
hash: "0x2a098695dcfa32a67ec115af7c8da1ef6f443ea72baf7e49525204dd521a985e",
|
||||
from_address: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
|
||||
to_address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
value: "0",
|
||||
decoded: {
|
||||
name: "transfer",
|
||||
signature: "transfer(address,uint256)",
|
||||
inputs: {
|
||||
to: "0x83dd93fa5d8343094f850f90b3fb90088c1bb425",
|
||||
value: "10000",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
chain_id: "8453",
|
||||
hash: "0xc521bfa0ba3e68fa1a52c67f93a8e215d3ade0b45956ba215390bcc0576202f1",
|
||||
value: "1000000000000000",
|
||||
from_address: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
|
||||
to_address: "0x83dd93fa5d8343094f850f90b3fb90088c1bb425",
|
||||
decoded: {
|
||||
name: "",
|
||||
signature: "",
|
||||
inputs: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
chain_id: "8453",
|
||||
hash: "0xf2d92059c9ea425ccf7568bfe2589b3c7e45b108b5af658ec79c2f2d3723e410",
|
||||
from_address: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
|
||||
to_address: "0x83dd93fa5d8343094f850f90b3fb90088c1bb425",
|
||||
value: "1000000000000000",
|
||||
decoded: {
|
||||
name: "",
|
||||
signature: "",
|
||||
inputs: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
chain_id: "8453",
|
||||
hash: "0xea3da430876c09acfa665450a91edb99fe8dc018864c5dfa3ac53bf265ce8d66",
|
||||
from_address: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
|
||||
to_address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
value: "0",
|
||||
decoded: {
|
||||
name: "transfer",
|
||||
signature: "transfer(address,uint256)",
|
||||
inputs: {
|
||||
to: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
|
||||
value: "100000",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
chain_id: "8453",
|
||||
hash: "0xad03e5b350645f2e4cdd066c30b2f6b708aa34bd4d1c5ca20fd81ecfe1656164",
|
||||
from_address: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
|
||||
to_address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
value: "0",
|
||||
decoded: {
|
||||
name: "approve",
|
||||
signature: "approve(address,uint256)",
|
||||
inputs: {
|
||||
spender: "0xf8ab2dbe6c43bf1a856471182290f91d621ba76d",
|
||||
value: "10000000",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
chain_id: "8453",
|
||||
hash: "0xd3106aaa4b7e530ac7530c8ea4984eec52670aabaf5969ae6bc8d8246e74c3c0",
|
||||
from_address: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
|
||||
to_address: "0xf8ab2dbe6c43bf1a856471182290f91d621ba76d",
|
||||
value: "0",
|
||||
decoded: {
|
||||
name: "initiateTransaction",
|
||||
signature:
|
||||
"initiateTransaction((bytes32,address,uint256,address,address,uint256,address,uint256,bytes,bytes),bytes)",
|
||||
inputs: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
chain_id: "8453",
|
||||
hash: "0xd284b6e0dd938b4610ff1877c1d4692a8d10d83ec6be24789dc87e1ef4aa4756",
|
||||
from_address: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
|
||||
to_address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
value: "0",
|
||||
decoded: {
|
||||
name: "approve",
|
||||
signature: "approve(address,uint256)",
|
||||
inputs: {
|
||||
spender: "0xf8ab2dbe6c43bf1a856471182290f91d621ba76d",
|
||||
value: "1000000",
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const MultipleAssets: Story = {
|
||||
args: {
|
||||
data: transactionsStub,
|
||||
isPending: false,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleAsset: Story = {
|
||||
args: {
|
||||
data: transactionsStub.slice(0, 1),
|
||||
isPending: false,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyAssets: Story = {
|
||||
args: {
|
||||
data: [],
|
||||
isPending: false,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
data: [],
|
||||
isPending: true,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { isProd } from "@/constants/env-utils";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { XIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import { ChainIconClient } from "../../../../../components/icons/ChainIcon";
|
||||
import { useAllChainsData } from "../../../../../hooks/chains/allChains";
|
||||
import { nebulaAppThirdwebClient } from "../../utils/nebulaThirdwebClient";
|
||||
|
||||
// Note: this is not the full object type returned from insight API, it only includes fields we care about
|
||||
export type WalletTransaction = {
|
||||
chain_id: string;
|
||||
value: string;
|
||||
hash: string;
|
||||
from_address: string;
|
||||
to_address: string;
|
||||
decoded: {
|
||||
name: string;
|
||||
signature: string;
|
||||
inputs: null | object;
|
||||
};
|
||||
};
|
||||
|
||||
export function TransactionSectionUI(props: {
|
||||
data: WalletTransaction[];
|
||||
isPending: boolean;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
if (props.data.length === 0 && !props.isPending) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 px-2 py-1">
|
||||
<div className="rounded-full border p-1">
|
||||
<XIcon className="size-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">No Recent Activity</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{!props.isPending &&
|
||||
props.data.map((asset) => (
|
||||
<TransactionInfo
|
||||
key={asset.hash}
|
||||
transaction={asset}
|
||||
client={props.client}
|
||||
/>
|
||||
))}
|
||||
|
||||
{props.isPending &&
|
||||
new Array(10).fill(null).map((_, index) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
<SkeletonAssetItem key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SkeletonAssetItem() {
|
||||
return (
|
||||
<div className="flex h-[48px] items-center gap-2 px-2 py-1">
|
||||
<Skeleton className="size-8 rounded-full" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<Skeleton className="h-3 w-32 bg-muted" />
|
||||
<Skeleton className="h-3 w-24 bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TransactionInfo(props: {
|
||||
transaction: WalletTransaction;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { idToChain } = useAllChainsData();
|
||||
const chainMeta = idToChain.get(Number(props.transaction.chain_id));
|
||||
const title = getTransactionTitle(props.transaction);
|
||||
const description = getTransactionDescription(props.transaction);
|
||||
const explorer =
|
||||
chainMeta?.explorers?.[0]?.url ||
|
||||
`https://thirdweb.com/${props.transaction.chain_id}`;
|
||||
|
||||
return (
|
||||
<div className="relative flex h-[48px] items-center gap-2.5 rounded-lg px-2 py-1 hover:bg-accent">
|
||||
<ChainIconClient
|
||||
client={props.client}
|
||||
className="size-8 rounded-full border"
|
||||
src={chainMeta?.icon?.url || ""}
|
||||
/>
|
||||
|
||||
<div className="flex min-w-0 flex-col text-sm">
|
||||
<Link
|
||||
href={`${explorer}/tx/${props.transaction.hash}`}
|
||||
target="_blank"
|
||||
className="truncate font-medium capitalize before:absolute before:inset-0"
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
|
||||
{description && (
|
||||
<p className="text-muted-foreground text-sm">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getTransactionTitle(transaction: WalletTransaction) {
|
||||
if (transaction.decoded.name) {
|
||||
return transaction.decoded.name;
|
||||
}
|
||||
|
||||
if (transaction.decoded.signature) {
|
||||
const nameFromSignature = transaction.decoded.signature.split("(")[0];
|
||||
if (nameFromSignature) {
|
||||
return nameFromSignature;
|
||||
}
|
||||
}
|
||||
|
||||
if (transaction.value !== "0") {
|
||||
return "Transfer";
|
||||
}
|
||||
|
||||
return "Transaction Sent";
|
||||
}
|
||||
|
||||
function getTransactionDescription(transaction: WalletTransaction) {
|
||||
if (
|
||||
typeof transaction.decoded.inputs === "object" &&
|
||||
transaction.decoded.inputs !== null
|
||||
) {
|
||||
if (
|
||||
"to" in transaction.decoded.inputs &&
|
||||
typeof transaction.decoded.inputs.to === "string"
|
||||
) {
|
||||
return `To: ${shortenAddress(transaction.decoded.inputs.to)}`;
|
||||
}
|
||||
|
||||
if (
|
||||
"spender" in transaction.decoded.inputs &&
|
||||
typeof transaction.decoded.inputs.spender === "string"
|
||||
) {
|
||||
return `Spender: ${shortenAddress(transaction.decoded.inputs.spender)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `To: ${shortenAddress(transaction.to_address)}`;
|
||||
}
|
||||
|
||||
export function TransactionsSection(props: {
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const account = useActiveAccount();
|
||||
const activeChain = useActiveWalletChain();
|
||||
|
||||
const txQuery = useQuery({
|
||||
queryKey: ["v1/wallets/transactions", account?.address, activeChain?.id],
|
||||
queryFn: async () => {
|
||||
if (!account || !activeChain) {
|
||||
return [];
|
||||
}
|
||||
const chains = [...new Set([1, 8453, 10, 137, activeChain.id])];
|
||||
const url = new URL(
|
||||
`https://insight.${isProd ? "thirdweb" : "thirdweb-dev"}.com/v1/wallets/${account.address}/transactions`,
|
||||
);
|
||||
url.searchParams.set("limit", "20");
|
||||
url.searchParams.set("decode", "true");
|
||||
url.searchParams.set("clientId", nebulaAppThirdwebClient.clientId);
|
||||
|
||||
const threeMonthsAgoUnixTime = Math.floor(
|
||||
(Date.now() - 3 * 30 * 24 * 60 * 60 * 1000) / 1000,
|
||||
);
|
||||
|
||||
url.searchParams.set(
|
||||
"filter_block_timestamp_gte",
|
||||
`${threeMonthsAgoUnixTime}`,
|
||||
);
|
||||
|
||||
for (const chain of chains) {
|
||||
url.searchParams.append("chain", chain.toString());
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
const json = (await response.json()) as {
|
||||
data?: WalletTransaction[];
|
||||
};
|
||||
|
||||
return json.data ?? [];
|
||||
},
|
||||
retry: false,
|
||||
enabled: !!account && !!activeChain,
|
||||
});
|
||||
|
||||
return (
|
||||
<TransactionSectionUI
|
||||
data={txQuery.data ?? []}
|
||||
isPending={txQuery.isPending}
|
||||
client={props.client}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export function useSessionsWithLocalOverrides(
|
||||
if (index !== -1) {
|
||||
mergedSessions[index] = session;
|
||||
} else {
|
||||
mergedSessions.push(session);
|
||||
mergedSessions.unshift(session);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
const STORAGE_KEY = "thirdweb:ub-analytics-range";
|
||||
|
||||
type SavedRange = {
|
||||
rangeType: string;
|
||||
rangeType: "custom" | DurationId | undefined;
|
||||
interval: "day" | "week";
|
||||
};
|
||||
|
||||
@@ -47,7 +47,10 @@ export function PayAnalyticsFilter() {
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
interval: savedRange.interval,
|
||||
defaultRange: (savedRange.rangeType || "last-30") as DurationId,
|
||||
defaultRange:
|
||||
savedRange.rangeType === "custom"
|
||||
? "last-30"
|
||||
: savedRange.rangeType || "last-30",
|
||||
});
|
||||
|
||||
setResponsiveSearchParams((v) => ({
|
||||
@@ -57,7 +60,6 @@ export function PayAnalyticsFilter() {
|
||||
interval: savedRange.interval,
|
||||
}));
|
||||
} catch (e) {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
console.error("Failed to parse saved range:", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SkeletonContainer } from "@/components/ui/skeleton";
|
||||
import { useId, useMemo } from "react";
|
||||
import { Area, AreaChart, ResponsiveContainer, Tooltip, XAxis } from "recharts";
|
||||
import type { UniversalBridgeWalletStats } from "types/analytics";
|
||||
import { CardHeading, ChangeBadge, NoDataOverlay, chartHeight } from "./common";
|
||||
import { CardHeading, NoDataOverlay, chartHeight } from "./common";
|
||||
|
||||
type GraphDataItem = {
|
||||
date: string;
|
||||
@@ -20,7 +20,7 @@ export function PayNewCustomers(props: {
|
||||
/**
|
||||
* For each date, compute the total number of wallets that have never existed before in the time series
|
||||
*/
|
||||
const { graphData, trend } = useMemo(() => {
|
||||
const { graphData } = useMemo(() => {
|
||||
const dates = new Set<string>();
|
||||
for (const item of props.data) {
|
||||
if (!dates.has(item.date)) {
|
||||
@@ -47,16 +47,7 @@ export function PayNewCustomers(props: {
|
||||
value: newUsers,
|
||||
});
|
||||
}
|
||||
const lastPeriod = newUsersData[newUsersData.length - 2];
|
||||
const currentPeriod = newUsersData[newUsersData.length - 1];
|
||||
// Calculate the percent change from last period to current period
|
||||
const trend =
|
||||
lastPeriod && currentPeriod && lastPeriod.value > 0
|
||||
? (currentPeriod.value - lastPeriod.value) / lastPeriod.value
|
||||
: lastPeriod?.value === 0
|
||||
? 100
|
||||
: 0;
|
||||
return { graphData: newUsersData, trend };
|
||||
return { graphData: newUsersData };
|
||||
}, [props.data, props.dateFormat]);
|
||||
const isEmpty = useMemo(
|
||||
() => graphData.length === 0 || graphData.every((x) => x.value === 0),
|
||||
@@ -86,17 +77,6 @@ export function PayNewCustomers(props: {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isEmpty && (
|
||||
<SkeletonContainer
|
||||
loadedData={trend}
|
||||
className="rounded-2xl"
|
||||
skeletonData={1}
|
||||
render={(v) => {
|
||||
return <ChangeBadge percent={v} />;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,7 +102,7 @@ export function PayNewCustomers(props: {
|
||||
{payload?.date}
|
||||
</p>
|
||||
<p className="text-base text-medium">
|
||||
Customers: {payload?.value}
|
||||
New Customers: {payload?.value}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -43,6 +43,7 @@ export function PaymentHistory(props: {
|
||||
});
|
||||
return res;
|
||||
},
|
||||
refetchInterval: 10_000,
|
||||
});
|
||||
const isEmpty = useMemo(
|
||||
() => !payPurchaseData?.data.length,
|
||||
@@ -68,8 +69,8 @@ export function PaymentHistory(props: {
|
||||
<table className="w-full selection:bg-inverted selection:text-inverted-foreground ">
|
||||
<thead>
|
||||
<TableHeadingRow>
|
||||
<TableHeading> Paid </TableHeading>
|
||||
<TableHeading> Bought </TableHeading>
|
||||
<TableHeading> Sent </TableHeading>
|
||||
<TableHeading> Received </TableHeading>
|
||||
<TableHeading>Type</TableHeading>
|
||||
<TableHeading>Status</TableHeading>
|
||||
<TableHeading>Recipient</TableHeading>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useMemo } from "react";
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis } from "recharts";
|
||||
import type { UniversalBridgeStats } from "types/analytics";
|
||||
import { toUSD } from "../../../../utils/number";
|
||||
import { CardHeading, ChangeBadge, NoDataOverlay, chartHeight } from "./common";
|
||||
import { CardHeading, NoDataOverlay, chartHeight } from "./common";
|
||||
|
||||
type GraphData = {
|
||||
date: string;
|
||||
@@ -24,7 +24,7 @@ export function Payouts(props: {
|
||||
props.data.every((x) => x.developerFeeUsdCents === 0);
|
||||
|
||||
const barColor = isEmpty ? "hsl(var(--accent))" : "hsl(var(--chart-1))";
|
||||
const { graphData, totalPayoutsUSD, trend } = useMemo(() => {
|
||||
const { graphData, totalPayoutsUSD } = useMemo(() => {
|
||||
const dates = new Set<string>();
|
||||
for (const item of props.data) {
|
||||
if (!dates.has(item.date)) {
|
||||
@@ -49,18 +49,9 @@ export function Payouts(props: {
|
||||
value: total / 100,
|
||||
});
|
||||
}
|
||||
const lastPeriod = cleanedData[cleanedData.length - 2];
|
||||
const currentPeriod = cleanedData[cleanedData.length - 1];
|
||||
const trend =
|
||||
lastPeriod && currentPeriod && lastPeriod.value > 0
|
||||
? (currentPeriod.value - lastPeriod.value) / lastPeriod.value
|
||||
: lastPeriod?.value === 0
|
||||
? 100
|
||||
: 0;
|
||||
return {
|
||||
graphData: cleanedData,
|
||||
totalPayoutsUSD: totalPayouts / 100,
|
||||
trend,
|
||||
};
|
||||
}, [props.data, props.dateFormat]);
|
||||
|
||||
@@ -90,17 +81,6 @@ export function Payouts(props: {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isEmpty && (
|
||||
<SkeletonContainer
|
||||
className="rounded-2xl"
|
||||
loadedData={trend}
|
||||
skeletonData={1}
|
||||
render={(percent) => {
|
||||
return <ChangeBadge percent={percent} />;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative flex w-full justify-center">
|
||||
|
||||
@@ -24,11 +24,10 @@ export function TotalPayVolume(props: {
|
||||
};
|
||||
}) {
|
||||
const uniqueId = useId();
|
||||
const [successType, setSuccessType] = useState<"success" | "fail">("success");
|
||||
const [type, setType] = useState<"all" | "crypto" | "fiat">("all");
|
||||
|
||||
const graphData: GraphData[] | undefined = useMemo(() => {
|
||||
let data = (() => {
|
||||
const data = (() => {
|
||||
switch (type) {
|
||||
case "crypto": {
|
||||
return props.data?.filter((x) => x.type === "onchain");
|
||||
@@ -45,13 +44,6 @@ export function TotalPayVolume(props: {
|
||||
}
|
||||
})();
|
||||
|
||||
data = (() => {
|
||||
if (successType === "fail") {
|
||||
return data.filter((x) => x.status === "failed");
|
||||
}
|
||||
return data.filter((x) => x.status === "completed");
|
||||
})();
|
||||
|
||||
const dates = new Set<string>();
|
||||
for (const item of data) {
|
||||
if (!dates.has(item.date)) {
|
||||
@@ -72,15 +64,13 @@ export function TotalPayVolume(props: {
|
||||
});
|
||||
}
|
||||
return cleanedData;
|
||||
}, [props.data, type, successType, props.dateFormat]);
|
||||
}, [props.data, type, props.dateFormat]);
|
||||
|
||||
const isEmpty =
|
||||
graphData.length === 0 || graphData.every((x) => x.value === 0);
|
||||
const chartColor = isEmpty
|
||||
? "hsl(var(--muted-foreground))"
|
||||
: successType === "success"
|
||||
? "hsl(var(--chart-1))"
|
||||
: "hsl(var(--chart-3))";
|
||||
: "hsl(var(--chart-1))";
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
@@ -104,21 +94,6 @@ export function TotalPayVolume(props: {
|
||||
<SelectItem value="fiat">Fiat</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={successType}
|
||||
onValueChange={(value: "success" | "fail") => {
|
||||
setSuccessType(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="bg-transparent">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem value="success">Successful</SelectItem>
|
||||
<SelectItem value="fail">Failed</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { ArrowDownIcon, ArrowUpIcon } from "lucide-react";
|
||||
|
||||
export function NoDataOverlay() {
|
||||
return (
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-card/50 text-muted-foreground text-sm backdrop-blur-sm">
|
||||
@@ -14,39 +10,6 @@ export function CardHeading(props: { children: React.ReactNode }) {
|
||||
return <h3 className="font-medium text-base">{props.children}</h3>;
|
||||
}
|
||||
|
||||
export function ChangeBadge(props: { percent: number }) {
|
||||
const percentValue = `${props.percent.toFixed(0)}%`;
|
||||
let label = "No change compared to prior range";
|
||||
if (props.percent !== 0) {
|
||||
label = `
|
||||
${props.percent >= 0 ? "Increase" : "Decrease"} of ${percentValue} compared to prior range
|
||||
`;
|
||||
}
|
||||
return (
|
||||
<ToolTipLabel label={label}>
|
||||
<div>
|
||||
<Badge
|
||||
variant={props.percent >= 0 ? "success" : "destructive"}
|
||||
className="gap-1 px-2 py-1.5 text-sm"
|
||||
>
|
||||
{props.percent >= 0 ? (
|
||||
<ArrowUpIcon className="size-4 " />
|
||||
) : (
|
||||
<ArrowDownIcon className="size-4" />
|
||||
)}
|
||||
|
||||
{new Intl.NumberFormat("en-US", {
|
||||
style: "percent",
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
signDisplay: "never",
|
||||
}).format(props.percent)}
|
||||
</Badge>
|
||||
</div>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableData({ children }: { children: React.ReactNode }) {
|
||||
return <td className="px-3 py-2 text-sm">{children}</td>;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"@radix-ui/react-switch": "^1.2.2",
|
||||
"@radix-ui/react-tooltip": "1.2.3",
|
||||
"@tanstack/react-query": "5.74.4",
|
||||
"@thirdweb-dev/engine": "0.0.19",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
|
||||
@@ -9,16 +9,64 @@ export function useEngineTxStatus(queueId: string | undefined) {
|
||||
if (!queueId) throw new Error("No queue ID provided");
|
||||
const res = await get_engine_tx_status(queueId);
|
||||
|
||||
const txStatus: EngineTxStatus = {
|
||||
queueId: queueId,
|
||||
status: res.result.status,
|
||||
chainId: res.result.chainId,
|
||||
transactionHash: res.result.transactionHash,
|
||||
queuedAt: res.result.queuedAt,
|
||||
sentAt: res.result.sentAt,
|
||||
minedAt: res.result.minedAt,
|
||||
cancelledAt: res.result.cancelledAt,
|
||||
};
|
||||
let txStatus: EngineTxStatus;
|
||||
switch (res.status) {
|
||||
case "QUEUED": {
|
||||
txStatus = {
|
||||
queueId: queueId,
|
||||
status: "queued",
|
||||
chainId: res.chain.id.toString(),
|
||||
transactionHash: null,
|
||||
queuedAt: res.createdAt,
|
||||
sentAt: null,
|
||||
minedAt: null,
|
||||
cancelledAt: null,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "SUBMITTED": {
|
||||
txStatus = {
|
||||
queueId: queueId,
|
||||
status: "sent",
|
||||
chainId: res.chain.id.toString(),
|
||||
transactionHash: null,
|
||||
queuedAt: res.createdAt,
|
||||
sentAt: res.createdAt,
|
||||
minedAt: null,
|
||||
cancelledAt: null,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "CONFIRMED": {
|
||||
txStatus = {
|
||||
queueId: queueId,
|
||||
status: "mined",
|
||||
chainId: res.chain.id.toString(),
|
||||
transactionHash: res.transactionHash,
|
||||
queuedAt: res.createdAt,
|
||||
sentAt: res.confirmedAt,
|
||||
minedAt: res.confirmedAt,
|
||||
cancelledAt: null,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "FAILED": {
|
||||
txStatus = {
|
||||
queueId: queueId,
|
||||
status: "errored",
|
||||
chainId: res.chain.id.toString(),
|
||||
transactionHash: null,
|
||||
queuedAt: res.createdAt,
|
||||
sentAt: null,
|
||||
minedAt: null,
|
||||
cancelledAt: res.cancelledAt,
|
||||
};
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown engine tx status: ${res}`);
|
||||
}
|
||||
}
|
||||
|
||||
return txStatus;
|
||||
},
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
"use server";
|
||||
|
||||
import { Engine } from "@thirdweb-dev/engine";
|
||||
|
||||
import { Engine, defineChain, encode, getContract } from "thirdweb";
|
||||
import { multicall } from "thirdweb/extensions/common";
|
||||
import * as ERC20 from "thirdweb/extensions/erc20";
|
||||
import * as ERC1155 from "thirdweb/extensions/erc1155";
|
||||
import { THIRDWEB_CLIENT } from "../../lib/client";
|
||||
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string;
|
||||
const ENGINE_VAULT_ACCESS_TOKEN = process.env
|
||||
.ENGINE_VAULT_ACCESS_TOKEN as string;
|
||||
|
||||
const engine = new Engine({
|
||||
url: process.env.ENGINE_URL as string,
|
||||
accessToken: process.env.ENGINE_ACCESS_TOKEN as string,
|
||||
const serverWallet = Engine.serverWallet({
|
||||
address: BACKEND_WALLET_ADDRESS,
|
||||
client: THIRDWEB_CLIENT,
|
||||
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
export async function airdrop_tokens_with_engine(params: {
|
||||
@@ -17,20 +23,37 @@ export async function airdrop_tokens_with_engine(params: {
|
||||
amount: string;
|
||||
}[];
|
||||
}) {
|
||||
const res = await engine.erc20.mintBatchTo(
|
||||
params.chainId.toString(),
|
||||
params.contractAddress,
|
||||
BACKEND_WALLET_ADDRESS,
|
||||
{
|
||||
data: params.receivers,
|
||||
},
|
||||
const contract = getContract({
|
||||
address: params.contractAddress,
|
||||
chain: defineChain(params.chainId),
|
||||
client: THIRDWEB_CLIENT,
|
||||
});
|
||||
const data = await Promise.all(
|
||||
params.receivers.map((receiver) =>
|
||||
encode(
|
||||
ERC20.mintTo({
|
||||
contract,
|
||||
to: receiver.toAddress,
|
||||
amount: receiver.amount,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
const tx = multicall({
|
||||
contract,
|
||||
data,
|
||||
});
|
||||
|
||||
return res.result;
|
||||
const res = await serverWallet.enqueueTransaction({ transaction: tx });
|
||||
|
||||
return res.transactionId;
|
||||
}
|
||||
|
||||
export async function get_engine_tx_status(queueId: string) {
|
||||
const status = await engine.transaction.status(queueId);
|
||||
const status = await Engine.getTransactionStatus({
|
||||
client: THIRDWEB_CLIENT,
|
||||
transactionId: queueId,
|
||||
});
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -49,17 +72,19 @@ type MintNFTParams = {
|
||||
};
|
||||
|
||||
export async function mint_erc1155_nft_with_engine(params: MintNFTParams) {
|
||||
const res = await engine.erc1155.mintTo(
|
||||
params.chainId.toString(),
|
||||
params.contractAddress,
|
||||
BACKEND_WALLET_ADDRESS,
|
||||
{
|
||||
receiver: params.toAddress,
|
||||
metadataWithSupply: params.metadataWithSupply,
|
||||
},
|
||||
);
|
||||
const tx = ERC1155.mintTo({
|
||||
contract: getContract({
|
||||
address: params.contractAddress,
|
||||
chain: defineChain(params.chainId),
|
||||
client: THIRDWEB_CLIENT,
|
||||
}),
|
||||
nft: params.metadataWithSupply.metadata,
|
||||
to: params.toAddress,
|
||||
supply: BigInt(params.metadataWithSupply.supply),
|
||||
});
|
||||
const res = await serverWallet.enqueueTransaction({ transaction: tx });
|
||||
|
||||
return res.result;
|
||||
return res.transactionId;
|
||||
}
|
||||
|
||||
type ClaimNFTParams = {
|
||||
@@ -71,16 +96,17 @@ type ClaimNFTParams = {
|
||||
};
|
||||
|
||||
export async function claim_erc1155_nft_with_engine(params: ClaimNFTParams) {
|
||||
const res = await engine.erc1155.claimTo(
|
||||
params.chainId.toString(),
|
||||
params.contractAddress,
|
||||
BACKEND_WALLET_ADDRESS,
|
||||
{
|
||||
receiver: params.receiverAddress,
|
||||
quantity: params.quantity.toString(),
|
||||
tokenId: params.tokenId,
|
||||
},
|
||||
);
|
||||
const tx = ERC1155.claimTo({
|
||||
contract: getContract({
|
||||
address: params.contractAddress,
|
||||
chain: defineChain(params.chainId),
|
||||
client: THIRDWEB_CLIENT,
|
||||
}),
|
||||
to: params.receiverAddress,
|
||||
tokenId: BigInt(params.tokenId),
|
||||
quantity: BigInt(params.quantity),
|
||||
});
|
||||
const res = await serverWallet.enqueueTransaction({ transaction: tx });
|
||||
|
||||
return res.result;
|
||||
return res.transactionId;
|
||||
}
|
||||
|
||||
@@ -34,49 +34,49 @@ export function AirdropCode() {
|
||||
}
|
||||
|
||||
const engineAirdropSendCode = `\
|
||||
const chainId = ${airdropExample.chainId};
|
||||
const contractAddress = "${airdropExample.contractAddress}";
|
||||
const addresses = ${JSON.stringify(
|
||||
airdropExample.receivers.map((x) => ({
|
||||
address: x.toAddress,
|
||||
quantity: x.amount,
|
||||
recipient: x.toAddress,
|
||||
amount: x.amount,
|
||||
})),
|
||||
null,
|
||||
2,
|
||||
)};
|
||||
|
||||
const url = \`\${YOUR_ENGINE_URL}\/contract/\${chainId}/\${contractAddress}\/erc1155\/airdrop\`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": "Bearer YOUR_SECRET_TOKEN",
|
||||
"Content-Type": "application/json",
|
||||
"X-Backend-Wallet-Address": "YOUR_BACKEND_WALLET_ADDRESS",
|
||||
},
|
||||
body: JSON.stringify({ addresses }),
|
||||
const contract = getContract({
|
||||
address: ${airdropExample.contractAddress},
|
||||
chain: defineChain(${airdropExample.chainId}),
|
||||
client: THIRDWEB_CLIENT,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const queueId = data.queueId;
|
||||
const transaction = airdropERC20({
|
||||
contract,
|
||||
tokenAddress: ${airdropExample.contractAddress},
|
||||
contents: addresses,
|
||||
});
|
||||
|
||||
const serverWallet = Engine.serverWallet({
|
||||
address: BACKEND_WALLET_ADDRESS,
|
||||
client: THIRDWEB_CLIENT,
|
||||
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
const { transactionId } = await serverWallet.enqueueTransaction({ transaction });
|
||||
`;
|
||||
|
||||
const engineAirdropGetStatus = `\
|
||||
function getEngineTxStatus(queueId: string) {
|
||||
const url = \`\${YOUR_ENGINE_URL}\/transaction/\${queueId}\`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": "Bearer YOUR_SECRET_TOKEN",
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return data.result;
|
||||
}
|
||||
|
||||
// you can keep polling for the status until you get a status of either "mined" or "errored" or "cancelled"
|
||||
const result = await getEngineTxStatus(queueId);
|
||||
const result = await Engine.getTransactionStatus({
|
||||
client: THIRDWEB_CLIENT,
|
||||
transactionId: transactionId,
|
||||
});
|
||||
|
||||
console.log(result.status);
|
||||
|
||||
// or wait for the transaction to be mined (polls status until it's mined)
|
||||
const result = await Engine.waitForTransactionHash({
|
||||
client: THIRDWEB_CLIENT,
|
||||
transactionId: transactionId,
|
||||
});
|
||||
|
||||
console.log(result.transactionHash);
|
||||
`;
|
||||
|
||||
@@ -32,10 +32,10 @@ export function EngineAirdropPreview() {
|
||||
const res = await airdropMutation.mutateAsync();
|
||||
updateEngineTxStatus({
|
||||
chainId: airdropExample.chainId,
|
||||
queueId: res.queueId,
|
||||
queueId: res,
|
||||
});
|
||||
|
||||
setQueueId(res.queueId);
|
||||
setQueueId(res);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const airdropExample = {
|
||||
contractAddress: "0xcB30dB8FB977e8b27ae34c86aF16C4F5E428c0bE",
|
||||
contractAddress: "0x6E238275023A2575136CF60f655B6B2C0C58b4ac",
|
||||
chainId: 84532,
|
||||
chainName: "Base Sepolia",
|
||||
chainExplorer: "https://base-sepolia.blockscout.com",
|
||||
|
||||
@@ -13,7 +13,7 @@ export function MintCode() {
|
||||
|
||||
<div className="h-4" />
|
||||
<h3 className="mb-2 font-semibold text-xl tracking-tight">
|
||||
Send Transaction Request to Mint NFTs
|
||||
Send Transaction Request to Mint Dynamic NFTs
|
||||
</h3>
|
||||
<Code code={engineMintCode} lang="typescript" />
|
||||
|
||||
@@ -36,48 +36,45 @@ export function MintCode() {
|
||||
const engineMintCode = `\
|
||||
const chainId = ${mintExample.chainId};
|
||||
const contractAddress = "${mintExample.contractAddress}";
|
||||
const url = \`\${YOUR_ENGINE_URL}\/contract/\${chainId}/\${contractAddress}\/erc1155\/mint-to\`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": "Bearer YOUR_SECRET_TOKEN",
|
||||
"Content-Type": "application/json",
|
||||
"X-Backend-Wallet-Address": "YOUR_BACKEND_WALLET_ADDRESS",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
receiver: "0x....",
|
||||
metadataWithSupply: {
|
||||
metadata: {
|
||||
name: "...",
|
||||
description: "...",
|
||||
image: "...", // ipfs or https link to your asset
|
||||
},
|
||||
supply: "1",
|
||||
},
|
||||
})
|
||||
const contract = getContract({
|
||||
address: ${mintExample.contractAddress},
|
||||
chain: defineChain(${mintExample.chainId}),
|
||||
client: THIRDWEB_CLIENT,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data.queueId);
|
||||
const transaction = mintTo({
|
||||
contract,
|
||||
to: "0x....",
|
||||
nft: {
|
||||
name: "...",
|
||||
description: "...",
|
||||
image: "...", // ipfs or https link to your asset
|
||||
},
|
||||
supply: "1",
|
||||
});
|
||||
|
||||
const serverWallet = Engine.serverWallet({
|
||||
address: BACKEND_WALLET_ADDRESS,
|
||||
client: THIRDWEB_CLIENT,
|
||||
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
const { transactionId } = await serverWallet.enqueueTransaction({ transaction });
|
||||
`;
|
||||
|
||||
const getEngineStatusCode = `\
|
||||
function getEngineTxStatus(queueId: string) {
|
||||
const url = \`\${YOUR_ENGINE_URL}\/transaction/\${queueId}\`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": "Bearer YOUR_SECRET_TOKEN",
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return data.result;
|
||||
}
|
||||
|
||||
// you can keep polling for the status until you get a status of either "mined" or "errored" or "cancelled"
|
||||
const result = await getEngineTxStatus(queueId);
|
||||
const result = await Engine.getTransactionStatus({
|
||||
client: THIRDWEB_CLIENT,
|
||||
transactionId: transactionId,
|
||||
});
|
||||
|
||||
console.log(result.status);
|
||||
|
||||
// or wait for the transaction to be mined (polls status until it's mined)
|
||||
const result = await Engine.waitForTransactionHash({
|
||||
client: THIRDWEB_CLIENT,
|
||||
transactionId: transactionId,
|
||||
});
|
||||
|
||||
console.log(result.transactionHash);
|
||||
`;
|
||||
|
||||
@@ -231,10 +231,10 @@ export function EngineMintPreview() {
|
||||
});
|
||||
|
||||
// optimistic update
|
||||
queryClient.setQueryData(["engineTxStatus", result.queueId], {
|
||||
queryClient.setQueryData(["engineTxStatus", result], {
|
||||
status: "queued",
|
||||
chainId: mintExample.chainId.toString(),
|
||||
queueId: result.queueId,
|
||||
queueId: result,
|
||||
transactionHash: null,
|
||||
queuedAt: new Date().toISOString(),
|
||||
sentAt: null,
|
||||
@@ -242,7 +242,7 @@ export function EngineMintPreview() {
|
||||
cancelledAt: null,
|
||||
} satisfies EngineTxStatus);
|
||||
|
||||
setQueueId(result.queueId);
|
||||
setQueueId(result);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const mintExample = {
|
||||
contractAddress: "0x8CD193648f5D4E8CD9fD0f8d3865052790A680f6",
|
||||
contractAddress: "0x8a4E14591088bBce4a4Dcfa677C1af78d336d6FB",
|
||||
chainId: 84532,
|
||||
chainName: "Base Sepolia",
|
||||
chainExplorer: "https://base-sepolia.blockscout.com",
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
title="Mint NFTs"
|
||||
title="Mint Dynamic NFTs"
|
||||
description={
|
||||
<>
|
||||
Allow your users to mint new tokens into any given contract. You
|
||||
|
||||
@@ -71,7 +71,7 @@ export function EngineWebhooksPreview() {
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
const res = await claimMutation.mutateAsync(values.receiverAddress);
|
||||
setQueueId(res.queueId);
|
||||
setQueueId(res);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -117,6 +117,21 @@ export const connectLinks: Array<{
|
||||
},
|
||||
] as const;
|
||||
|
||||
const apisLinks = [
|
||||
{
|
||||
name: "Insight",
|
||||
href: "https://insight-api.thirdweb.com/reference",
|
||||
},
|
||||
{
|
||||
name: "Engine Cloud",
|
||||
href: "https://engine.thirdweb.com/reference#tag/write",
|
||||
},
|
||||
{
|
||||
name: "Universal Bridge",
|
||||
href: "https://bridge.thirdweb.com/reference",
|
||||
},
|
||||
];
|
||||
|
||||
const supportLinks = [
|
||||
{
|
||||
name: "Get thirdweb support",
|
||||
@@ -221,7 +236,15 @@ export function Header() {
|
||||
<DocSearch variant="search" />
|
||||
</div>
|
||||
|
||||
<div className="xl:px-2">
|
||||
<div className="xl:px-1">
|
||||
<DropdownLinks
|
||||
links={apisLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
category="APIs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="xl:px-1">
|
||||
<DropdownLinks
|
||||
links={supportLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
|
||||
@@ -95,7 +95,7 @@ You can specify which onramp provider to present to your users. By default, we c
|
||||
client={client}
|
||||
detailsModal={{
|
||||
payOptions: {
|
||||
preferredProvider: "STRIPE" | "KADO" | "TRANSAK",
|
||||
preferredProvider: "STRIPE" | "TRANSAK" | "COINBASE",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -29,6 +29,15 @@ This can include intents, native bridges, third party bridges, cross-chain messa
|
||||
health, price, and speed to guarantee the best path every time.
|
||||
</Details>
|
||||
|
||||
<Details summary="Am I able to use Universal Bridge with other wallet infrastructure providers?">
|
||||
Yes, Universal Bridge is designed to be agnostic to wallet infrastructure. You can use it with any wallet service.
|
||||
</Details>
|
||||
|
||||
|
||||
<Details summary="Does Universal Bridge support Apple Pay or Google Pay?">
|
||||
Apple Pay and Google Pay are supported through the onramp providers depending on region. Coinbase, Stripe, and Transak currently support Apple Pay. Transak supports Google Pay. [Learn more about the supported onramp providers.](/connect/pay/onramp-providers).
|
||||
</Details>
|
||||
|
||||
<Details summary="What are the KYC requirements?">
|
||||
Onramp options all require minimal KYC. This is done to comply with local regulations and to ensure the safety of our users.
|
||||
Please note: onramping with Coinbase does not require KYC for a purchase below $500 with debit card.
|
||||
@@ -41,7 +50,7 @@ Our transaction maximum starts from $1500 per week for new users and can increas
|
||||
</Details>
|
||||
|
||||
<Details summary="What countries does Buy With Fiat support?">
|
||||
**Buy With Fiat** is available 130+ countries. The following countries are _UNSUPPORTED_:
|
||||
**Buy With Fiat** is available 160+ countries. The following countries are _UNSUPPORTED_:
|
||||
|
||||
- Afghanistan
|
||||
- Africa (All Countries)
|
||||
|
||||
@@ -12,14 +12,30 @@ export const metadata = createMetadata({
|
||||
|
||||
# Onramp Providers
|
||||
|
||||
thirdweb Universal Bridge integrates with Stripe, Kado and Transak to power our onramp. By default, we choose a recommended provider based on the location of the user, KYC status, and currency.
|
||||
**Onramp Providers** are services that allow users to buy crypto with fiat. You can set preferred providers for your application by using the [`preferredProvider`](/connect/pay/customization/payembed#preferred-provider) parameter in the `PayEmbed` component.
|
||||
Universal Bridge supports the following onramp providers:
|
||||
|
||||
| Provider | Supported Countries |
|
||||
| -------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| Transak | https://transak.notion.site/On-Ramp-Payment-Methods-Fees-Other-Details-b0761634feed4b338a69f4f186d906a5 |
|
||||
| Kado | https://www.kado.money/supported-countries |
|
||||
| Stripe | https://docs.stripe.com/crypto/onramp |
|
||||
- **Coinbase**
|
||||
- **Transak**
|
||||
- **Stripe**
|
||||
|
||||
## Choosing a Preferred Provider
|
||||
## Provider Fees
|
||||
|
||||
It is possible to specify a preferred onramp provider by setting the `preferredProvider` parameter in the ConnectButton, PayEmbed, or Headless Integration. Go to [customizations](../pay/customization/connectbutton#preferredProvider) for more detail, or checkout the [API Reference for preferredProvider](https://portal.thirdweb.com/references/typescript/v5/GetBuyWithFiatQuoteParams).
|
||||
Each provider has different fees associated with their services which often depend on various factors. Onramp provider fees are set by the provider and are not controlled by thirdweb.
|
||||
|
||||
| Provider | Fees |
|
||||
| -------- | ---- |
|
||||
| Coinbase | [Learn more about Coinbase Fees](https://help.coinbase.com/en/coinbase/trading-and-funding/pricing-and-fees/fees) |
|
||||
| Transak | [Learn more about Transak Fees](https://support.transak.com/en/articles/7845942-how-does-transak-calculate-prices-and-fees)|
|
||||
| Stripe | [Learn more about Stripe Fees](https://stripe.com/legal/crypto-onramp). |
|
||||
|
||||
|
||||
## Provider Regions
|
||||
|
||||
Each provider has different regions they support. You can learn more about the supported regions for each provider in the links below.
|
||||
|
||||
| Provider | Regions |
|
||||
| -------- | ------- |
|
||||
| Coinbase | [View Regions](https://docs.cdp.coinbase.com/onramp/docs/api-configurations) |
|
||||
| Transak | [View Regions](https://transak.com/global-coverage) |
|
||||
| Stripe | [View Regions](https://support.stripe.com/questions/crypto-supportability-and-availability-by-region) |
|
||||
@@ -241,6 +241,10 @@ export const sidebar: SideBar = {
|
||||
name: "Supported Routes",
|
||||
href: "https://thirdweb.com/routes",
|
||||
},
|
||||
{
|
||||
name: "Onramp Providers",
|
||||
href: `${paySlug}/onramp-providers`,
|
||||
},
|
||||
{
|
||||
name: "Service Fees",
|
||||
href: `${paySlug}/fees`,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 175 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
@@ -0,0 +1,66 @@
|
||||
import { DocImage } from "@doc";
|
||||
import { Tabs, TabsList, TabsContent } from "@/components/ui/tabs";
|
||||
import stylusPublishPage from "./assets/stylus-publish-page.png";
|
||||
import stylusDeployPage from "./assets/stylus-deploy-page.png";
|
||||
|
||||
# Arbitrum Stylus
|
||||
|
||||
Integrate thirdweb tools in Stylus contract (Rust) development workflow.
|
||||
|
||||
Use thirdweb CLI to create, publish, and deploy contracts written with Stylus. The deployed contracts can be used via thirdweb dashboard or SDK.
|
||||
|
||||
## Prerequisites
|
||||
- Rust toolchain should be installed (https://www.rust-lang.org/tools/install)
|
||||
- Solidity (solc) should be installed (https://docs.soliditylang.org/en/latest/installing-solidity.html)
|
||||
|
||||
You can also use the Stylus quickstart guide for reference: https://docs.arbitrum.io/stylus/quickstart
|
||||
|
||||
## Getting started
|
||||
|
||||
To get started, setup a starter repo (can be skipped if you already have a Stylus project):
|
||||
|
||||
<Tabs defaultValue="forge">
|
||||
<TabsContent value="forge">
|
||||
```bash
|
||||
npx thirdweb create-stylus
|
||||
```
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
This will setup a project containing a template contract.
|
||||
|
||||
## Publishing and Deploying
|
||||
|
||||
Use one of these commands to publish or deploy your Stylus contract.
|
||||
|
||||
<Tabs defaultValue="forge">
|
||||
<TabsContent value="forge">
|
||||
```bash
|
||||
npx thirdweb publish-stylus -k <YOUR SECRET KEY>
|
||||
```
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
OR
|
||||
|
||||
<Tabs defaultValue="forge">
|
||||
<TabsContent value="forge">
|
||||
```bash
|
||||
npx thirdweb deploy-stylus -k <YOUR SECRET KEY>
|
||||
```
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
Publishing a contract saves the contract metadata to an onchain registry, and creates a contract page from where you can deploy a specific version of this contract multiple times.
|
||||
|
||||
This is what you should see:
|
||||
|
||||
<DocImage src={stylusPublishPage} />
|
||||
|
||||
|
||||
|
||||
After deployment, the contract will be available on thirdweb dashboard. You can interact with it via dashboard or integrate it in your app using the code snippets as shown below:
|
||||
|
||||
<DocImage src={stylusDeployPage} />
|
||||
|
||||
More templates for Stylus Rust contracts and use-case specific installable modules are coming soon.
|
||||
@@ -799,6 +799,11 @@ export const sidebar: SideBar = {
|
||||
},
|
||||
],
|
||||
},
|
||||
// stylus
|
||||
{
|
||||
name: "Arbitrum Stylus",
|
||||
href: `${buildSlug}/stylus`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ separator: true },
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 335 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 347 KiB |
@@ -2,4 +2,12 @@ import { Callout, Details } from "@doc";
|
||||
|
||||
# Engine FAQs
|
||||
|
||||
More information coming soon.
|
||||
<Details summary="How is pricing calculated for Engine Cloud?">
|
||||
Pricing is calculated through the number of write requests (ex: /v1/write/contract) made through the Engine API. Requests cost $1 per 1,000 requests. Read requests made through Engine API are free, within the RPC limits/plan.
|
||||
|
||||
For transactions through server wallets paid through the user's thirdweb account, users will pay a 5% premium on the gas fee of each transaction completed on any mainnet as part of account abstraction fees. There are no gas charges for testnets.
|
||||
|
||||
**Please note: Legacy pricing plans may be subject to the 10% fee instead of 5%. Please update your pricing plan to avoid being subject to the 10% fee.**
|
||||
|
||||
The breakdown for usage and transaction fees can be found in your usage dashboard under the team overview.
|
||||
</Details>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Callout, Step, Steps, Tabs, TabsContent, TabsTrigger, TabsList } from "@doc";
|
||||
import { Callout, Step, Steps, Tabs, TabsContent, TabsTrigger, TabsList, DocImage } from "@doc";
|
||||
import CreateVault from '../assets/create-vault.png';
|
||||
import SendTransaction from '../assets/send-transaction.png';
|
||||
|
||||
# Get started
|
||||
|
||||
@@ -19,7 +21,8 @@ Learn how to get started with thirdweb Engine. This guide will walk you through
|
||||
Create a vault to manage your Engine's server wallets. After setup, you can manage your Vault to create additional access tokens or rotate admin keys.
|
||||
|
||||
<Callout variant="info" title="Vault">
|
||||
Vault is thirdweb's key management service designed to store smart server wallets non-custodially. Learn more about [Vault].
|
||||
Vault is thirdweb's key management service designed to store smart server wallets non-custodially. [Learn more about Vault.](/vault)
|
||||
<DocImage src={CreateVault} alt="Create Vault"/>
|
||||
</Callout>
|
||||
</Step>
|
||||
|
||||
@@ -35,6 +38,7 @@ Learn how to get started with thirdweb Engine. This guide will walk you through
|
||||
To verify your server wallet setup and see how transactions work, you can send a test transaction in the next step. This sends a no-op transaction—a transaction with zero value—to your own wallet.
|
||||
|
||||
You can send additional test transactions or proceed with the full setup whenever you're ready.
|
||||
<DocImage src={SendTransaction} alt="Send Test Transaction"/>
|
||||
</Step>
|
||||
|
||||
<Step title="Integrate with your app">
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Callout } from "@doc";
|
||||
|
||||
# Migrating from Engine v2 to v3
|
||||
|
||||
This guide outlines the key changes required to migrate your applications from Engine v2 to Engine v3. We'll cover common operations and highlight the differences in API requests and authentication.
|
||||
Learn how to migrate your applications from Engine v2 to Engine v3. We'll cover common operations and highlight the differences in API requests and authentication.
|
||||
|
||||
<Callout variant="info" title="Migrating Wallets">
|
||||
If you'd like to migrate your wallets from v2 to v3 to also be secured by [Vault](/vault) or thirdweb's key management service, please [contact us for assistance.](https://thirdweb.com/support)
|
||||
</Callout>
|
||||
|
||||
## Writing to a contract
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ Looking for the previous version? [Head back to the Engine V2 documentation.](/e
|
||||
| Wallets | Server Wallets | Server Wallets, AWS, GCP, Circle |
|
||||
| Database Backups | ❌ | 30 Day Backups |
|
||||
| Infrastructure | Shared | Isolated |
|
||||
| Rate Limits | [Limited by RPC Plan](https://thirdweb.com/pricing) | Unlimited |
|
||||
| Rate Limits | Unlimited | Unlimited |
|
||||
| Pricing | [See pricing page](https://thirdweb.com/pricing) | [See pricing page](https://thirdweb.com/pricing) |
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @thirdweb-dev/insight
|
||||
|
||||
## 3.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#7050](https://github.com/thirdweb-dev/js/pull/7050) [`ae2ff74`](https://github.com/thirdweb-dev/js/commit/ae2ff743c05be7267e904ece9098601794b10dd9) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Update openAPI spec
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
@@ -5,23 +5,38 @@ This package is a thin openAPI wrapper for insight, our in-house indexer.
|
||||
## Configuration
|
||||
|
||||
```ts
|
||||
import { configure } from "@thirdweb-dev/insight";
|
||||
import { configure } from "@thirdweb-dev/engine";
|
||||
|
||||
// call this once at the startup of your application
|
||||
configure({
|
||||
clientId: "<YOUR_CLIENT_ID>",
|
||||
secretKey: "<PROJECT_SECRET_KEY>",
|
||||
});
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
```ts
|
||||
import { getV1Events } from "@thirdweb-dev/insight";
|
||||
import { writeContract } from "@thirdweb-dev/engine";
|
||||
|
||||
const events = await getV1Events({
|
||||
query: {
|
||||
chain: [1, 137],
|
||||
filter_address: "0x1234567890123456789012345678901234567890",
|
||||
const result = await writeContract({
|
||||
headers: {
|
||||
"x-vault-access-token": "<VAULT_ACCESS_TOKEN>",
|
||||
},
|
||||
body: {
|
||||
executionOptions: {
|
||||
from: "0x1234567891234567891234567891234567891234",
|
||||
chainId: "1",
|
||||
},
|
||||
params: [
|
||||
{
|
||||
contractAddress: "0x1234567890123456789012345678901234567890",
|
||||
method: "function transfer(address to, uint256 amount)",
|
||||
params: [
|
||||
"0x1234567890123456789012345678901234567890",
|
||||
"1000000000000000000",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineConfig } from "@hey-api/openapi-ts";
|
||||
|
||||
export default defineConfig({
|
||||
input: "http://localhost:3009/openapi", // TODO: update to prod
|
||||
input: "https://engine.thirdweb.com/openapi",
|
||||
output: { path: "src/client", lint: "biome", format: "biome" },
|
||||
plugins: ["@hey-api/client-fetch"],
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/engine",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
@@ -23,7 +23,10 @@
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": ["dist/*", "src/*"],
|
||||
"files": [
|
||||
"dist/*",
|
||||
"src/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@hey-api/client-fetch": "0.10.0"
|
||||
},
|
||||
|
||||
@@ -23,6 +23,6 @@ export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> =
|
||||
|
||||
export const client = createClient(
|
||||
createConfig<ClientOptions>({
|
||||
baseUrl: "http://localhost:3009",
|
||||
baseUrl: "https://engine.thirdweb.com",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -144,6 +144,7 @@ export type WriteContractResponses = {
|
||||
transactions: Array<{
|
||||
id: string;
|
||||
batchIndex: number;
|
||||
clientId: string;
|
||||
chainId: string;
|
||||
from: string | null;
|
||||
transactionParams:
|
||||
@@ -188,6 +189,7 @@ export type WriteContractResponses = {
|
||||
transactions: Array<{
|
||||
id: string;
|
||||
batchIndex: number;
|
||||
clientId: string;
|
||||
chainId: string;
|
||||
from: string | null;
|
||||
transactionParams:
|
||||
@@ -273,6 +275,7 @@ export type SendTransactionResponses = {
|
||||
transactions: Array<{
|
||||
id: string;
|
||||
batchIndex: number;
|
||||
clientId: string;
|
||||
chainId: string;
|
||||
from: string | null;
|
||||
transactionParams:
|
||||
@@ -317,6 +320,7 @@ export type SendTransactionResponses = {
|
||||
transactions: Array<{
|
||||
id: string;
|
||||
batchIndex: number;
|
||||
clientId: string;
|
||||
chainId: string;
|
||||
from: string | null;
|
||||
transactionParams:
|
||||
@@ -876,6 +880,7 @@ export type SearchTransactionsResponses = {
|
||||
transactions: Array<{
|
||||
id: string;
|
||||
batchIndex: number;
|
||||
clientId: string;
|
||||
chainId: string;
|
||||
from: string | null;
|
||||
transactionParams:
|
||||
@@ -1013,5 +1018,5 @@ export type GetTransactionAnalyticsSummaryResponse =
|
||||
GetTransactionAnalyticsSummaryResponses[keyof GetTransactionAnalyticsSummaryResponses];
|
||||
|
||||
export type ClientOptions = {
|
||||
baseUrl: "http://localhost:3009" | (string & {});
|
||||
baseUrl: "https://engine.thirdweb.com" | (string & {});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.99.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#7065](https://github.com/thirdweb-dev/js/pull/7065) [`61152dd`](https://github.com/thirdweb-dev/js/commit/61152dd0984adc36fdcd722e1382c716c6bf2368) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Return timestamps in Engine.getTransactionStatus()
|
||||
|
||||
- [#7069](https://github.com/thirdweb-dev/js/pull/7069) [`da2a2a0`](https://github.com/thirdweb-dev/js/commit/da2a2a0e86f6ab5b919667964e001f16e20326ad) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix buyWithCrypto false not respected when going back from quote
|
||||
|
||||
- [#7040](https://github.com/thirdweb-dev/js/pull/7040) [`20b5ba9`](https://github.com/thirdweb-dev/js/commit/20b5ba943a3c22633c7c49ba82104a6057be10b0) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Propagate 401 errors when connecting in-app wallet
|
||||
|
||||
- [#7061](https://github.com/thirdweb-dev/js/pull/7061) [`8d47864`](https://github.com/thirdweb-dev/js/commit/8d478649678fa2016bd6a6b72873dd1aaa2f5e05) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Expose prepareUserOp utility function
|
||||
|
||||
- Updated dependencies [[`ae2ff74`](https://github.com/thirdweb-dev/js/commit/ae2ff743c05be7267e904ece9098601794b10dd9)]:
|
||||
- @thirdweb-dev/engine@3.0.1
|
||||
|
||||
## 5.99.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "thirdweb",
|
||||
"version": "5.99.1",
|
||||
"version": "5.99.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
|
||||
@@ -49,6 +49,9 @@ export type ExecutionResult = Prettify<
|
||||
chain: Chain;
|
||||
from: string | undefined;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
confirmedAt: string | null;
|
||||
cancelledAt: string | null;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -105,6 +108,9 @@ export async function getTransactionStatus(args: {
|
||||
const executionResult = data.executionResult as ExecutionResult4337Serialized;
|
||||
return {
|
||||
...executionResult,
|
||||
createdAt: data.createdAt,
|
||||
confirmedAt: data.confirmedAt,
|
||||
cancelledAt: data.cancelledAt,
|
||||
chain: getCachedChain(Number(data.chainId)),
|
||||
from: data.from ?? undefined,
|
||||
id: data.id,
|
||||
|
||||
@@ -5,6 +5,7 @@ export {
|
||||
createUnsignedUserOp,
|
||||
signUserOp,
|
||||
createAndSignUserOp,
|
||||
prepareUserOp,
|
||||
getUserOpHash,
|
||||
} from "../../wallets/smart/lib/userop.js";
|
||||
|
||||
|
||||
@@ -510,8 +510,9 @@ function BuyScreenContent(props: BuyScreenContentProps) {
|
||||
client={client}
|
||||
onBack={() => {
|
||||
if (
|
||||
screen.id === "buy-with-crypto" ||
|
||||
screen.id === "buy-with-fiat"
|
||||
(screen.id === "buy-with-crypto" ||
|
||||
screen.id === "buy-with-fiat") &&
|
||||
enabledPaymentMethods.buyWithCryptoEnabled
|
||||
) {
|
||||
setScreen({
|
||||
id: "select-from-token",
|
||||
|
||||
@@ -3137,6 +3137,79 @@ export async function getWalletInfo<TImage extends boolean>(
|
||||
: import("./wallet/app.imem/index.js").then((w) => w.wallet)
|
||||
) as Promise<[TImage] extends [true] ? string : any>;
|
||||
}
|
||||
case "com.socios.app": {
|
||||
return (
|
||||
image
|
||||
? import("./wallet/com.socios.app/image.js").then(
|
||||
(img) => img.default,
|
||||
)
|
||||
: import("./wallet/com.socios.app/index.js").then((w) => w.wallet)
|
||||
) as Promise<[TImage] extends [true] ? string : any>;
|
||||
}
|
||||
case "io.bimwallet": {
|
||||
return (
|
||||
image
|
||||
? import("./wallet/io.bimwallet/image.js").then((img) => img.default)
|
||||
: import("./wallet/io.bimwallet/index.js").then((w) => w.wallet)
|
||||
) as Promise<[TImage] extends [true] ? string : any>;
|
||||
}
|
||||
case "com.adftechnology": {
|
||||
return (
|
||||
image
|
||||
? import("./wallet/com.adftechnology/image.js").then(
|
||||
(img) => img.default,
|
||||
)
|
||||
: import("./wallet/com.adftechnology/index.js").then((w) => w.wallet)
|
||||
) as Promise<[TImage] extends [true] ? string : any>;
|
||||
}
|
||||
case "com.reown.appkit-lab": {
|
||||
return (
|
||||
image
|
||||
? import("./wallet/com.reown.appkit-lab/image.js").then(
|
||||
(img) => img.default,
|
||||
)
|
||||
: import("./wallet/com.reown.appkit-lab/index.js").then(
|
||||
(w) => w.wallet,
|
||||
)
|
||||
) as Promise<[TImage] extends [true] ? string : any>;
|
||||
}
|
||||
case "com.reown": {
|
||||
return (
|
||||
image
|
||||
? import("./wallet/com.reown/image.js").then((img) => img.default)
|
||||
: import("./wallet/com.reown/index.js").then((w) => w.wallet)
|
||||
) as Promise<[TImage] extends [true] ? string : any>;
|
||||
}
|
||||
case "com.reown.docs": {
|
||||
return (
|
||||
image
|
||||
? import("./wallet/com.reown.docs/image.js").then(
|
||||
(img) => img.default,
|
||||
)
|
||||
: import("./wallet/com.reown.docs/index.js").then((w) => w.wallet)
|
||||
) as Promise<[TImage] extends [true] ? string : any>;
|
||||
}
|
||||
case "io.yowallet": {
|
||||
return (
|
||||
image
|
||||
? import("./wallet/io.yowallet/image.js").then((img) => img.default)
|
||||
: import("./wallet/io.yowallet/index.js").then((w) => w.wallet)
|
||||
) as Promise<[TImage] extends [true] ? string : any>;
|
||||
}
|
||||
case "org.hot-labs": {
|
||||
return (
|
||||
image
|
||||
? import("./wallet/org.hot-labs/image.js").then((img) => img.default)
|
||||
: import("./wallet/org.hot-labs/index.js").then((w) => w.wallet)
|
||||
) as Promise<[TImage] extends [true] ? string : any>;
|
||||
}
|
||||
case "ai.purewallet": {
|
||||
return (
|
||||
image
|
||||
? import("./wallet/ai.purewallet/image.js").then((img) => img.default)
|
||||
: import("./wallet/ai.purewallet/index.js").then((w) => w.wallet)
|
||||
) as Promise<[TImage] extends [true] ? string : any>;
|
||||
}
|
||||
case "io.walletverse": {
|
||||
return (
|
||||
image
|
||||
|
||||
+11
-1
@@ -1,7 +1,7 @@
|
||||
// This file is auto-generated by the `scripts/wallets/generate.ts` script.
|
||||
// Do not modify this file manually.
|
||||
|
||||
// 403 wallets
|
||||
// 413 wallets
|
||||
export type WCSupportedWalletIds =
|
||||
| "io.metamask"
|
||||
| "com.trustwallet.app"
|
||||
@@ -404,6 +404,16 @@ export type WCSupportedWalletIds =
|
||||
| "network.trustkeys"
|
||||
| "finance.voltage"
|
||||
| "app.imem"
|
||||
| "com.socios.app"
|
||||
| "io.bimwallet"
|
||||
| "com.adftechnology"
|
||||
| "com.reown.appkit-lab"
|
||||
| "com.reown"
|
||||
| "com.reown.docs"
|
||||
| "com.reown"
|
||||
| "io.yowallet"
|
||||
| "org.hot-labs"
|
||||
| "ai.purewallet"
|
||||
| "io.walletverse"
|
||||
| "com.berasig";
|
||||
|
||||
|
||||
+46
-1
@@ -201,7 +201,7 @@ const ALL_MINIMAL_WALLET_INFOS = (<const>[
|
||||
},
|
||||
{
|
||||
id: "com.fastex.wallet",
|
||||
name: "Fastex Wallet",
|
||||
name: "Yo Wallet",
|
||||
hasMobileSupport: true,
|
||||
},
|
||||
{
|
||||
@@ -1974,6 +1974,51 @@ const ALL_MINIMAL_WALLET_INFOS = (<const>[
|
||||
name: "iMe",
|
||||
hasMobileSupport: true,
|
||||
},
|
||||
{
|
||||
id: "com.socios.app",
|
||||
name: "Socios.com - Wallet & Tokens",
|
||||
hasMobileSupport: true,
|
||||
},
|
||||
{
|
||||
id: "io.bimwallet",
|
||||
name: "BIM Wallet",
|
||||
hasMobileSupport: true,
|
||||
},
|
||||
{
|
||||
id: "com.adftechnology",
|
||||
name: "ADF Wallet",
|
||||
hasMobileSupport: true,
|
||||
},
|
||||
{
|
||||
id: "com.reown.appkit-lab",
|
||||
name: "Flutter Sample Wallet",
|
||||
hasMobileSupport: true,
|
||||
},
|
||||
{
|
||||
id: "com.reown",
|
||||
name: "React Native Sample Wallet",
|
||||
hasMobileSupport: true,
|
||||
},
|
||||
{
|
||||
id: "com.reown.docs",
|
||||
name: "Kotlin Sample Internal Wallet",
|
||||
hasMobileSupport: true,
|
||||
},
|
||||
{
|
||||
id: "io.yowallet",
|
||||
name: "YoWallet",
|
||||
hasMobileSupport: true,
|
||||
},
|
||||
{
|
||||
id: "org.hot-labs",
|
||||
name: "HOT Wallet",
|
||||
hasMobileSupport: true,
|
||||
},
|
||||
{
|
||||
id: "ai.purewallet",
|
||||
name: "PureWallet app",
|
||||
hasMobileSupport: true,
|
||||
},
|
||||
{
|
||||
id: "io.walletverse",
|
||||
name: "Walletverse",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// This file is auto-generated by the `scripts/wallets/generate.ts` script.
|
||||
// Do not modify this file manually.
|
||||
|
||||
const image =
|
||||
"data:image/webp;base64,UklGRgQFAABXRUJQVlA4IPgEAAAwIACdASqAAIAAPm0ylEakIyIhrhJJGIANiWIAz9RWEa/HcsjzP4bxBaed5gHOo8xXm3/2P1Rfrt+M3yAfsB1i/oFftN1tH7l/srYd69Tf/mL/QMZfNJ80EKEGulnbAUcBnTCirNK/1Ubo5TRGY/cp41APZRCzbWT2WvcyhYb6chB3YziLFGYRI+xb+iHU0fshtIBMoseO5v//Q8+W7fcLDKmjr7QxAZ1pq2faN//9XO80B0xVraVlJk52BeRI3ka/DHx65tWO4OzJU3yHjPYT6qR6MX41Gq0QyrvEU0s5IlQP5qweywIJAADiwLwtPy85pYcE1qRNTnBAikdJPCe4Qr/16x/3/PELgAD+/KgPJb+mk+tPXLY04UQnLyKAaX9mzAZworFvQfVX7/5Opy/7rv8vTN71BG77JKCYGBk6XWok8T9RVfgAbbRY8479+6Sl1Ij1VrzFpD+u/qZkx2EkIpH74jkeOnl9BUvcYui0HQHATy3PN20vP0QqkekGeNJ7jHPD98Q3Qen6sZeOE8NhdCwWmmsAs6ZRlQxJzwzWf76IsYo8GpasuDYRco6eUgZeFDuPpmcu+Yy8pI57y0InWYv05Jw4P7VMc/+CLZKJh3OJblL2vIP8RqH4a/ivVS/0vzeN+nE6XNYRcHpTIF2H0hQLafGmcQzLBwbS7jXVrT8ksE23UMP75O92c/o1WEE+ada6R6PgoWPtnzH/zqMbgKTb1nS1K1YYS/OBVSkg1bp4LsKKR13SLxjMHYZ7CeMi7OuztBhmqHVYm0NQPp/RXIxwkYYb2gij3fZCPYk7txETutPjXyhq3JpvscHCmrVaSOvOSFEF6hpYHXlJtDe1V6gsIxVdYobhUflfP+naF+J08LdEypc5l6layhVLUtFqtNM65hkLQpZbl6FWJf7PWf6P2FFXklOquhAf9QjNddtpXUPoGWWEYcbGBlzr98vjnKfbD4QtMG0loff1cfog7ce9SYKlLjfJiNJT4HKe/ZQqeMQ5VaJoQPGhrM3p87fq3BrcEukDX4V2fb7PM8zVDKuC+2vnT8U3CmgX0+XEZoto/SKF16O9wYUcN16VKN4gfXcZ4fKflWkSye3jN/yZWKYwPAON5vP1DfMEq4wBhkN1Hwq1VgzVQeBCVwFqcGNacc/ipVr44aA0M7n2/HQo1LKvrEg2ceWFByhlGYd7TwCWsPrl3Jnqd4t1+/ihc+vltUR8D42H+FO/K8/q56EscLiqN+BJffyQQPhvVtxT1E8UmJIO3BPbYwlYfnpV/6BYuJkZSr/jz7bKJZuAOMwSu2cPp3U14Ko46VVy6oAnhpRIMI2ERdey0+Q+5jETJawhy6VnYycH1W5a5GFuvS62aJFxxgRvhT/C0I+b1gpz1fZoPz8QgqK5JZxwK97fbTF7XoAAkxOHiUHcjluKSEyIIuGyStPKtFk7JKx783eP+UHNXot4w61O1HguL5vBgQ3ehKd9Eg5+yfdWIHs25GnXwfYDuhsXKbv1JigmxT48QzPfkR5AAAJoXi9wmOxQI8dug241k/XRlUaJ3tEUg31G856xaXEo4kloiLq/v+vSrBHNyrXX31umKq8UgMnzVQOJY0Uri8fznGX0X2MmMr4IkBh1risIAUfTE/SIIwgnWkuEjUGcucXpVSRbQh7I7QHs1Y0WH5Qk3eFp596qVZju2SGV9IAAAAA=";
|
||||
|
||||
export default image;
|
||||
@@ -0,0 +1,32 @@
|
||||
// This file is auto-generated by the `scripts/wallets/generate.ts` script.
|
||||
// Do not modify this file manually.
|
||||
|
||||
export const wallet = {
|
||||
id: "ai.purewallet",
|
||||
name: "PureWallet app",
|
||||
homepage: "https://purewallet.ai",
|
||||
image_id: "4e97b9d6-60eb-42cd-6256-5cd7205e3f00",
|
||||
app: {
|
||||
browser: null,
|
||||
ios: "https://apps.apple.com/us/app/purewallet/id6738949168?platform=iphone",
|
||||
android:
|
||||
"https://play.google.com/store/apps/details?id=com.nslab.purewalletp&pli=1",
|
||||
mac: null,
|
||||
windows: null,
|
||||
linux: null,
|
||||
chrome: null,
|
||||
firefox: null,
|
||||
safari: null,
|
||||
edge: null,
|
||||
opera: null,
|
||||
},
|
||||
rdns: null,
|
||||
mobile: {
|
||||
native: "purewalletp://",
|
||||
universal: null,
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
},
|
||||
} as const;
|
||||
@@ -27,6 +27,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://wallet.catecoin.club/",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -26,6 +26,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://clot.app",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -26,6 +26,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://everspace.app/",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -27,6 +27,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: "gamic://",
|
||||
universal: null,
|
||||
universal: "https://gamic.app/",
|
||||
},
|
||||
} as const;
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal:
|
||||
"https://chrome.google.com/webstore/detail/keeper-wallet/lpilbniiabackdjcionkobglmddfbcjo",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -28,6 +28,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://wallet.keplr.app",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -26,6 +26,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://keyring.app/",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -26,6 +26,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://defi.krystal.app/",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -26,6 +26,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://m1nty.app",
|
||||
},
|
||||
} as const;
|
||||
|
||||
+1
-1
@@ -26,6 +26,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://wallet.qubic.app",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -27,6 +27,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://app.sinum.io",
|
||||
},
|
||||
} as const;
|
||||
|
||||
+1
-1
@@ -27,6 +27,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://docs.localtrade.cc/products/defi-wallet-mvp-for-ios",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -26,6 +26,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://3swallet.com/",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -26,6 +26,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://abra.com",
|
||||
},
|
||||
} as const;
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// This file is auto-generated by the `scripts/wallets/generate.ts` script.
|
||||
// Do not modify this file manually.
|
||||
|
||||
const image =
|
||||
"data:image/webp;base64,UklGRjoEAABXRUJQVlA4IC4EAABQGACdASqAAIAAPm02l0ikIyIhJJVY4IANiWMKFrc0fYPMdrD9w3TEqfYhkG9Rnic9JPzD/9T0h/9B6n/QA/ZL1bP7N7BP7AewB5bXshf32aSfi+F+s3wn7T29QwH0qE1DyyHEDgVF/jLsP/AqI0mIQZ1yxtgJ75iNJnb6GBKAbP2gXIolxJarFiDNkKhqUPbISLwx/nh7kz0UY8bdBOzrzeVWldx5pNcxiwuru/hXwKrUUrncjixCU6Dd1/lmfM5baYHYLFvQS2aSYAD+/5WUYIl0GoWT7gAgOrOfUt6dED+qbPjSyJGiygVlGYWdl3DW2Ozn3jU0YcwCXWXvAxFEhC0Gx+Y/vPjzU4DptHhjTQx/UgRuNlrn785CyaD9WsPVCYY24FU6DReEUGbTE89WWwvDQ/65hd4rDuLHDYCoveJ1Otwv2GkTFSMZp8lWCX70S5LSZk5TYRE35W7PxUJ6808xCnllC+WP+QAEdiWC5c/e0McrsB1gDGbvvg+edL+YelfR5q5pfb5UclqZWVjnqh3ZYDuCnwaNyJONDdovv+jH4ujfxugRnuQ3YT6TwH6SZ+vv9LWmqZRv0NrPsQIh/Gx6NP4o70O0JBa8O/FvAPUTcmuOAuv50Y0G+UHwvrVXZiJy4dP5qhpejqXpULybjMC31nkl3p/yCb2P1UCuvmnnAWaetHmluA7/He948JlnT/NDsgl2dibN/YY50xmp4v0axOB9DnzBr6vbIEZfxlG4zWX/gKtm06NVWw+CAMvCc/aAW7Bvg6hL78Se6p7blXBTKFQuDnn5XHO67ypYz5sgjbh9JjVV23jkN8FH5bV5UqulJOwNjMUlvlRd/S9+g4/6rnEgn6rz6Yitb/kQ+BUfga7i3EgztNuil5WE+FpMOWj0pUWxlT3aRihD+LrZPdhBMoIqgwf39clbBWGP86Lu/ZSXQszPy2Acln7OSReL6q8Sa4vh8U/4kSEc2k7H2XLpWeF8eMRaqjmiBkFzO35iNfFtq3fh6lloOz68D10s8aJ8LEm0r3sKWyiI12omYjvMCQ/d3eb34XwYSfHKNHIyJCay4yJvp+QmbYDw09QLI9EOQg6up53BtftO2Xwuz9vUzpYXo81z5v+Mo63RUltrakimoCdd/Kbib6oXgenrafY9G3cCMCKP0ES0wQfC/9m5ygTimTCtf+bhn/FHZI7nd2XH/H+kkyo4p74d5hUo2iyRgt8TNxwPdana162o+GGJUKsIZ+w5+kjrkharJT90wO+ttCEsfR5pp1PxrBV056so9RwsHtjvxWKDCPartUHuEJLh1lT/IN61Ov+MKitBq0BTd9K4uvsTw6hggsNhaJs6O/Tq/Cb/+RUdu7tfYpsSabweMcyE0wldxrsUB2+cWEXH/swGYXutO/IiuFUy8I4N8mjD98AAAAAAAA==";
|
||||
|
||||
export default image;
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// This file is auto-generated by the `scripts/wallets/generate.ts` script.
|
||||
// Do not modify this file manually.
|
||||
|
||||
export const wallet = {
|
||||
id: "com.adftechnology",
|
||||
name: "ADF Wallet",
|
||||
homepage: "https://www.adftechnology.com/",
|
||||
image_id: "28acc8ed-a0e7-4004-1968-c54869fa2100",
|
||||
app: {
|
||||
browser: null,
|
||||
ios: null,
|
||||
android: "https://play.google.com/store/apps/details?id=com.adf.wallet",
|
||||
mac: null,
|
||||
windows: null,
|
||||
linux: null,
|
||||
chrome: null,
|
||||
firefox: null,
|
||||
safari: null,
|
||||
edge: null,
|
||||
opera: null,
|
||||
},
|
||||
rdns: null,
|
||||
mobile: {
|
||||
native: "adf://",
|
||||
universal: null,
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
},
|
||||
} as const;
|
||||
+1
-1
@@ -27,6 +27,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://apps.apple.com/us/app/bettatrade/id6474153866",
|
||||
},
|
||||
} as const;
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ export const wallet = {
|
||||
mac: null,
|
||||
windows: null,
|
||||
linux: null,
|
||||
chrome: null,
|
||||
chrome: "https://www.binance.com/en/download",
|
||||
firefox: null,
|
||||
safari: null,
|
||||
edge: null,
|
||||
@@ -26,6 +26,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://www.binance.com/en/web3wallet",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -27,6 +27,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://wallet.bitcoin.com/",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -26,6 +26,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://bitso.com/web3-wallet",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -27,6 +27,6 @@ export const wallet = {
|
||||
},
|
||||
desktop: {
|
||||
native: null,
|
||||
universal: null,
|
||||
universal: "https://www.broearn.com",
|
||||
},
|
||||
} as const;
|
||||
|
||||
+1
-1
@@ -2,6 +2,6 @@
|
||||
// Do not modify this file manually.
|
||||
|
||||
const image =
|
||||
"data:image/webp;base64,UklGRrIHAABXRUJQVlA4IKYHAACwJQCdASqAAIAAPm0wlUYkIyIhKxdLmIANiWwA0MouX75h/dfyP9oGsf1T8Of1Tnip68tbx39C/5n3je+71C/nf/e+4B+ov7DdYnzAftL+0/vYejf++f4/2Hf7J/rusz9FPy1P24+HT9p/209nS8b/GXPr2Syu4e7JZWKuHfh5+An9R4qPPNowqmleJDBvAHYmuGhkt+Kob17LzVz5lNH1BIbFuVOVGofXXE5rvrWDwiCqUU7sUiovoLMjMT4otjkYmo33SSfAYoiofY/d/dRHaMtaWYNofT7FSfpmXkWrKfcfdwdAiXkVRuvVVHd8XLs/0W1NKPD2vQiRNPCfnPWL9aYbPTkWyALa5kQp6vPeE0mLPHq1vy4WktOyqABHUfQGLvpEhiLdmbRJ0RR3C6fnE5f8xm6AAP7+VdBNDnj/vKppmyPzYp6Cafn0x6b7Eb1JBnPH0Dg3aprWMgUoRG6COVNywDZ9NZd+i1AaYfrR3h6oYoPtiLTWJKDj9kbUFt27ji+ppIlFxnlxf5T7yLrmJABeauNx6rBu01sYXrXq7nJtJk6ST1+6NbpHnl0ewElGFATD6Ks44FEhOGF8GBa74QWH5mYjv5G7FNA/jszv6BEPPsRe4fTzbypdv6eYRzMSWeJW7J36fNwTKA47R4BeZ3VOKzqHqqhr384gAXX+UlmweGXvFbKl3lyfkkyHNiPwyYlyeCMM6ITydtXMBDcdySlhGdV3oHN3Y27F11CQXFwBPJgCb/gAMkreB1r0AG0x3lwIspnBqKyAgPApll2fkxE/+fp347V+fVG0rmUymy248B2+oAAnsPgbAOmaLSIora0nkKUj9horOv1VsnyAs/PDI+7Z6pIIObECJERPYaVAajDZlBbCWtCh3ruKCDq0M/Y1H7VjCd+2kQxY5HookGjtSiQwAAuEOp2GEFwXwyAW8hyywT8yHBLITMhbKes86TplAZ/N0t4eTpt2pwXMEQNs2kAQ4lf8XjewZkqzqwEpqZIHtaIxg0mud2Hys8UGmhY+5oGVphx6+/U1y1vECRgjbAB3uwZtd5Q80/DCZ/2IsdvwsBXwn9OJEoRApzUGAp7jHNw+WSo1zG6mvgDx6lvuT8zbOTxrq0Jd+PmnIyn29NsXILVlpnde9uhCB9IbEXJSZPjViM1okL8jEO4SegaewCtOJcUiS0sVGqq2P1cjadOXJEBDWhrdHFORe6uuOA8qyjzOFM7HX7gLXM/o4LRindoKrRom3Wsk/YYul/b9cR+J47wPo8wiRthXh2uHwQ/+emCIgvBy7XiEoeVdzX3+zwIYlR6SjPCpr7sdkd+lINy8Ua5tp9gRNDToz9a4RJ/jeCe6D/FZijI/R9v2wiqOL/g7XhGSe2uqKE+cqcyOnrCdtLTeFRNXeF4x4oUl86PfwWlL4RMxdiCGmhZlut921H9oxJXNtnILmqqW/Ip06bmkMZB+gOAE8uBhC0GfikaE6cqBzpLIhadhpB50ql7pYaJix5xo6sGvfvE98cKhVBO3ehv57FCcAw1eulKk/b82xEjKl8PcO4wAewwWJhtQxyiZQ9ZlrnrFSgoFptdkVQFZD4VBqcRAFhS0O8LEKihEK2UWgKpJdtTwk6VKGvVVkIv30xJMS9v371X0tfhkdx8XKARND2k/7iXRjRo2Y+DNTFIS/XhaxRE5+X7GweWxvzcLbIipXHbSKcF2mvNKk7LvyvCbc0ka0vRspMNXrc7u69Q3BNSgSOwya/SwIUNz3yC0GD9xWJY0Xzb6Y1wndMQeEr+bi3ZHuuZ0QKmFLvDi/Ua3e5PKjZSPxBo4W/z2jVvgz8CL/nA06GRvyk1DnjSMyPhRxWmmtEqOqto1/t8X3laM+IR+S2vmGUu0hXjWhnXdBIj8igNO+pssXcqcGqG/kbevkXj5CtRwGbhv5wLNndGTbuvdRxvLzg6H3GU2jKRYdnYdfJC5euS8s1R0pE7NcOGZabHtYkfSdUXRFnRjpfcwlYRGFToMYSBKR43FkFIaR4lXX9pEW+NivKqosv/G07CEJK6YAhGVY2iAthWtgZnJBwc/TG7AbU9CbXbdciiLoqEZRUUAzUhGezqMxKlCVi9UzEh15uJCoAK8gSOs1GNlFURmvOK9CnxC3M6lftTXR1+H4ZutECldRGyhPQLfcrH7Dn7h0Whj6alGMtwJRnC6gxJuT4IhUEbhMK6FEKiYBfR0aFcgFtMLysx7A2WEnBITbqoOArdp0KcSyFrbQiLuRX0pQr7jvgI+qSDIZnCMSUlndVfwnb+iESnh5kxqoM2CipjnKBw3KOqQ2QhBtSqeUgTQhWjAJ5l8Q78n4ZIycUwP3rX7UK+//t7Tf20Znn+plDSDWX7IDs4K5x3HRsJoBvWcFVwPvXnBYBvhsh2+53aeLwOdcm7kZP/LlwZghrXSoDJeaOIs13H10trMrw0caxhC+9IRzgKIPTIM1v3mcsZ3N6KhiPzQotczw743tnhMNX7/XkAW4pAxzUbWADSZatfPb7D65ti6FiTs/S+SC+TSacK30tbiSZTn6siw4kzukIQJ8+t9rbvOuSte6uvr+bFfV3h2Zi8G4pMIkuxJ7aDNlaYc9LbGToLUIAAAAA==";
|
||||
"data:image/webp;base64,UklGRpYEAABXRUJQVlA4IIoEAAAwGgCdASqAAIAAPm00lUgkIqIhJJcamIANiUAa6gxaIObPyz766oDzNfBfzD/O/zDdAP0d/yf6q9hTzAeAB7gPQA/ln+l6xj0AP2A9Mv2LP7v/qv169kC5q5EXslpS7KdqL/Cb0AR5kBKve8ZZLwKezayJxwY0I5an7xvHGw/247hMtmC5OIHyrL1y9vHXzveltLH25GF5CRwR4OiFRxs8aibfH6aDJ1HuFCZT26iRhuPMbSkN0BvOvVqvAnSO0DewpND2MbTvMa9prt++gQbhWaAWbyfezXRVgAD+/KgAALi8oshSjnWh7CViWT2tNMBnV0uw+unZuSvnYSD5UUlJcPJyugPbpF34x+uYSOQyY3RggtLovpT4r6LoW7tf/wOaOY0f4qS1xqFuko2FKY/pq48LpLoYspBQknF+CdlZbygeKy6cFA9SbZHazOpIBdMov88HN7gnTpN9bJOyCf73f1YmaMYzt/lWifxOyKpGVquqnqgb84adSXymhLp1Tdpm9SvQeNWEMk/gnxrkdfZrbn6IgCpLVN2deMQ/8NUgLVDBy604j7637RpfcoHnYXHyvyt5X0Vu3j1CVB5C8tWujefwNmooOqeigxeST8ngG8gBismcfqaoxRPKrjFFvpULxCvml0yz+khU1+uWbB7vu7fvuCn6VasOP7++koNJNPGI1UTyRBHY5dIvAoPAUOAmrOsjaKmDLXYJbPdJuUv5jmmHi0EQ1BAetoo4vz5SjXpswF5wS1gJ5Y0WQyNKevFpyXnky3KUo2w1qZ4LvqpQygy3+70P3XQQrtheXrhJ09NazZT1ZUMVK0T1yeMT7AEDR2IT1UYWpVwZHzk2HqxkoPXxCZVRK/5DZbCSza6dGgQhrVcR8tgcsujsE3yXsW8Ikca4d13uLR5Wfh00i2PI1Dpr8TSytQxr731rMIF2hrQ2hMUiT2OMyTJsNgyXoLPqIVjhSz15C+SSbLwxOgdhSnPZ9WkGrx6gb0wR3zqAq96frXGr8HBI5UMfQEFS5fBbhF0rG9VjpLI62X6m8R8j03bEA4pAFOG8HWju2W8FjN1ijkCLMlyiUpNU7EEyKPmTxlcK2mJpOIdhcMyOf5cfA41obQmURPGLIE9RVy7UkLuc86tTndeKDpPK2d31NJtSzLpviX5A9LOeDX+x/PA6xzckcejJqt0SxnBwsbGyF9RaRNJjvHHCKrSJMYXEBP1mW7vUZV1F3aGtKwGtcnml39Bw42e/h8e0tUWnA+uEAgk7awr1Xb/CVVbVGujWsIqTUMLOE8k92Nm+fQF9lR/3mpWETUZV3h6QNOSWnghIzK6tnTDwT1OuXHcZB/EGtouXNO+CXU5PEnzf9wgZNl/oZ9YnBIaPa1aQ64+QXMUCdib9oGLKhsA+X1C5a8ROWTwGxWUg7EFbjhGCAHhuGKG5FwkPS874fM9geCYInQChd+lJx5WAsiTAew9AW3Us7ZZn4C1i89wglJXP2I/FD77flEyiMM6bkveoRK5UVs7StpoM0pNkoKJxPzLRDc+2UA9KnLG1awOgAAAA";
|
||||
|
||||
export default image;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user