[BLD-231] Playground: Add API reference, Remove Insight and Payments API playgrounds (#7986)

This commit is contained in:
Manan Tank
2025-09-05 04:35:43 +07:00
committed by GitHub
parent 769019dd0b
commit 6429a238c1
29 changed files with 92 additions and 2260 deletions
@@ -39,10 +39,6 @@ export function InsightFTUX(props: { clientId: string }) {
},
]}
ctas={[
{
href: "https://playground.thirdweb.com/insight",
label: "Try on Playground",
},
{
href: "https://portal.thirdweb.com/insight",
label: "View Docs",
@@ -60,10 +60,6 @@ export default async function Page(props: {
type: "docs",
href: "https://portal.thirdweb.com/insight",
},
{
type: "playground",
href: "https://playground.thirdweb.com/insight",
},
{
type: "api",
href: "https://insight-api.thirdweb.com/reference",
@@ -74,44 +70,6 @@ export default async function Page(props: {
},
],
}}
footer={{
center: {
links: [
{
href: "https://www.youtube.com/watch?v=U2aW7YIUJVw",
label:
"Blockchain Data on Any EVM - Quick and Easy REST APIs for Onchain Data",
},
{
href: "https://www.youtube.com/watch?v=HvqewXLVRig",
label: "Build a Whale Alerts Telegram Bot with Insight",
},
],
title: "Tutorials",
},
left: {
links: [
{
href: "https://portal.thirdweb.com/insight",
label: "Overview",
},
{
href: "https://insight-api.thirdweb.com/reference",
label: "API Reference",
},
],
title: "Documentation",
},
right: {
links: [
{
href: "https://playground.thirdweb.com/insight",
label: "API Playground",
},
],
title: "Demos",
},
}}
>
<ResponsiveSearchParamsProvider value={searchParams}>
<div>
+10
View File
@@ -147,6 +147,16 @@ const nextConfig = {
destination: "/wallets/ecosystem-wallet",
permanent: false,
},
{
source: "/insight",
destination: "https://insight.thirdweb.com/reference",
permanent: false,
},
{
source: "/payments/backend",
destination: "/reference#tag/payments",
permanent: false,
},
];
},
async rewrites() {
+2 -2
View File
@@ -17,6 +17,7 @@
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "1.2.7",
"@radix-ui/react-use-controllable-state": "^1.2.2",
"@scalar/api-reference-react": "^0.7.42",
"@tanstack/react-query": "5.81.5",
"@thirdweb-dev/ai-sdk-provider": "workspace:*",
"@workspace/ui": "workspace:*",
@@ -77,8 +78,7 @@
"prefix": "biome check ./src --fix",
"prelint": "biome check ./src",
"start": "next start",
"typecheck": "tsc --noEmit",
"update-insight-blueprints": "bun scripts/updateInsightBlueprints.ts"
"typecheck": "tsc --noEmit"
},
"version": "0.1.1"
}
@@ -1,20 +0,0 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { fetchAllBlueprints } from "./utils";
const blueprints = await fetchAllBlueprints();
const fileContent = `\
// This file is auto-generated by the update-insight-blueprints script
import type { MinimalBlueprintSpec } from "./utils";
export const insightBlueprints: MinimalBlueprintSpec[] = ${JSON.stringify(blueprints, null, 2)}`;
fs.writeFileSync(
path.join(__dirname, "../src/app/insight/insightBlueprints.ts"),
fileContent,
);
console.log(
"Updated insightBlueprints.ts - Please format this file and commit it",
);
-34
View File
@@ -1,34 +0,0 @@
import type {
BlueprintListItem,
MinimalBlueprintSpec,
} from "../src/app/insight/utils";
async function fetchBlueprintList() {
const res = await fetch("https://insight.thirdweb.com/v1/blueprints");
if (!res.ok) {
const text = await res.text();
throw new Error(`Failed to fetch blueprints: ${text}`);
}
const json = (await res.json()) as { data: BlueprintListItem[] };
return json.data;
}
export const fetchAllBlueprints = async (): Promise<MinimalBlueprintSpec[]> => {
try {
const blueprints = await fetchBlueprintList();
return blueprints.map((blueprint) => {
return {
id: blueprint.id,
name: blueprint.name,
paths: blueprint.paths.map(({ method, ...path }) => path),
};
});
} catch (error) {
console.error(error);
return [];
}
};
@@ -3,7 +3,6 @@ import {
BlocksIcon,
BotIcon,
BoxIcon,
BracesIcon,
CircleUserIcon,
CreditCardIcon,
DollarSignIcon,
@@ -24,7 +23,6 @@ import {
UserIcon,
WalletCardsIcon,
} from "lucide-react";
import { InsightIcon } from "../../icons/InsightIcon";
export type FeatureCardMetadata = {
icon: React.FC<{ className?: string }>;
@@ -187,52 +185,6 @@ export const paymentsFeatureCards: FeatureCardMetadata[] = [
description:
"Enable users to pay for onchain transactions with fiat or crypto",
},
{
icon: BracesIcon,
title: "Payments API",
link: "/payments/backend",
description: "Create customizable UIs or backend flows using the HTTP API",
},
];
export const insightFeatureCards: FeatureCardMetadata[] = [
{
icon: InsightIcon,
title: "Events",
link: "/insight",
description: "Query contract events on any supported EVM chain",
},
{
icon: InsightIcon,
title: "Transactions",
link: "/insight",
description: "Query transactions to and from specified recipients",
},
{
icon: InsightIcon,
title: "Tokens",
link: "/insight",
description: "Query token owners, transfers, prices, and more",
},
{
icon: InsightIcon,
title: "NFTs",
link: "/insight",
description:
"Query NFT balances, collections, transfers, metadata, and more.",
},
{
icon: InsightIcon,
title: "Wallets",
link: "/insight",
description: "Query transactions to and from specific wallets",
},
{
icon: InsightIcon,
title: "Contracts",
link: "/insight",
description: "Query a contract's ABI or metadata",
},
];
export const accountAbstractionsFeatureCards: FeatureCardMetadata[] = [
@@ -1,358 +0,0 @@
"use client";
import { InfoIcon, SearchIcon } from "lucide-react";
import { useMemo, useState } from "react";
import type { ControllerRenderProps } from "react-hook-form";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Textarea } from "@/components/ui/textarea";
interface Preset {
id: string;
label: string;
value: string;
}
const DEFAULT_AGGREGATE_PRESETS: Preset[] = [
{
id: "count-all",
label: "Count All Items",
value: "count() AS count_all",
},
{
id: "sum-gas",
label: "Sum (gas_used)",
value: "sum(gas_used) AS total_gas_used",
},
{
id: "avg-gas",
label: "Average (gas_used)",
value: "avg(gas_used) AS avg_gas_used",
},
{
id: "min-gas",
label: "Min (gas_used)",
value: "min(gas_used) AS min_gas_used",
},
{
id: "max-gas",
label: "Max (gas_used)",
value: "max(gas_used) AS max_gas_used",
},
{
id: "count-distinct",
label: "Count Distinct",
value: "countDistinct(column_name) AS unique_count",
},
];
const GENERAL_TRANSACTIONS_PRESETS: Preset[] = [
{
id: "tx-count",
label: "Transaction Count",
value: "count() AS transaction_count",
},
{
id: "total-value",
label: "Total Value (Wei)",
value: "sum(value) AS total_value_wei",
},
{
id: "total-gas",
label: "Total Gas Used",
value: "sum(gas_used) AS total_gas_used",
},
{
id: "avg-value",
label: "Avg Value (Wei)",
value: "avg(value) AS avg_value_wei",
},
{
id: "avg-gas",
label: "Avg Gas Used",
value: "avg(gas_used) AS avg_gas_used",
},
{
id: "unique-senders",
label: "Unique Senders",
value: "countDistinct(from_address) AS unique_senders",
},
{
id: "unique-receivers",
label: "Unique Receivers",
value: "countDistinct(to_address) AS unique_receivers",
},
];
const WALLET_TRANSACTIONS_PRESETS: Preset[] = [
{
id: "wallet-tx-count",
label: "Transaction Count",
value: "count() AS transaction_count",
},
{
id: "wallet-total-value",
label: "Total Value (Wei)",
value: "sum(value) AS total_value_wei",
},
{
id: "wallet-avg-value",
label: "Avg Value (Wei)",
value: "avg(value) AS avg_value_wei",
},
{
id: "wallet-total-fees",
label: "Total Fees (Wei)",
value: "sum(gas_used * gas_price) AS total_fees_wei",
},
];
const EVENTS_PRESETS: Preset[] = [
{
id: "event-count",
label: "Event Count",
value: "count() AS event_count",
},
{
id: "unique-addresses",
label: "Unique Addresses",
value: "countDistinct(address) AS unique_addresses",
},
{
id: "min-block",
label: "Min Block Number",
value: "min(block_number) AS min_block",
},
{
id: "max-block",
label: "Max Block Number",
value: "max(block_number) AS max_block",
},
{
id: "unique-topics",
label: "Unique Topics",
value: "countDistinct(topic0) AS unique_topics",
},
];
const BLOCKS_PRESETS: Preset[] = [
{
id: "block-count",
label: "Block Count",
value: "count() AS block_count",
},
{
id: "total-transactions",
label: "Total Transactions",
value: "sum(transaction_count) AS total_transactions",
},
{
id: "avg-transactions",
label: "Avg Transactions/Block",
value: "avg(transaction_count) AS avg_transactions_per_block",
},
{
id: "total-gas-used",
label: "Total Gas Used",
value: "sum(gas_used) AS total_gas_used",
},
{
id: "min-block-number",
label: "Min Block Number",
value: "min(number) AS min_block_number",
},
{
id: "max-block-number",
label: "Max Block Number",
value: "max(number) AS max_block_number",
},
];
const ENDPOINT_SPECIFIC_PRESETS: Record<string, Preset[]> = {
"/v1/blocks": BLOCKS_PRESETS,
"/v1/events": EVENTS_PRESETS,
"/v1/transactions": GENERAL_TRANSACTIONS_PRESETS,
"/v1/wallets/{wallet_address}/transactions": WALLET_TRANSACTIONS_PRESETS,
// Add more endpoint paths and their specific presets here
};
function getAggregatePresets(endpointPath: string): Preset[] {
return ENDPOINT_SPECIFIC_PRESETS[endpointPath] || DEFAULT_AGGREGATE_PRESETS;
}
interface AggregateParameterInputProps {
field: ControllerRenderProps<{ [x: string]: string | number }, string>;
showTip?: boolean;
hasError?: boolean;
placeholder?: string;
endpointPath: string;
}
export function AggregateParameterInput({
field,
placeholder = "Enter aggregation query...",
endpointPath,
showTip = true,
}: AggregateParameterInputProps) {
const { value, onChange } = field;
const [searchTerm, setSearchTerm] = useState("");
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const presets = useMemo(
() => getAggregatePresets(endpointPath),
[endpointPath],
);
const selectedValues = useMemo(() => {
if (!value) return [];
return String(value)
.split(",")
.map((v) => v.trim())
.filter(Boolean);
}, [value]);
const filteredPresets = useMemo(() => {
if (!searchTerm) return presets;
const query = searchTerm.toLowerCase();
return presets.filter(
(preset) =>
preset.label.toLowerCase().includes(query) ||
preset.value.toLowerCase().includes(query),
);
}, [presets, searchTerm]);
const selectedPresets = useMemo(() => {
return presets
.filter((preset) => selectedValues.includes(preset.value.trim()))
.map((preset) => preset.id);
}, [presets, selectedValues]);
const handlePresetToggle = (presetId: string) => {
const preset = presets.find((p) => p.id === presetId);
if (!preset) return;
const isSelected = selectedValues.includes(preset.value.trim());
let newValues: string[];
if (isSelected) {
newValues = selectedValues.filter((v) => v !== preset.value.trim());
} else {
newValues = [...selectedValues, preset.value.trim()];
}
onChange({ target: { value: newValues.join(",") } });
};
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange({ target: { value: e.target.value } });
};
const getButtonText = () => {
if (selectedValues.length === 0) return "aggregate";
if (selectedValues.length === 1)
return `${selectedValues.length} aggregation added`;
return `${selectedValues.length} aggregations added`;
};
return (
<div className="relative w-full">
<Popover onOpenChange={setIsPopoverOpen} open={isPopoverOpen}>
<PopoverTrigger asChild>
<Button
className="-ml-1 w-full justify-start border-0 border-none bg-transparent font-mono text-muted-foreground shadow-none hover:bg-transparent hover:text-muted-foreground focus:ring-0 focus:ring-offset-0"
variant="ghost"
>
<span className="truncate">{getButtonText()}</span>
</Button>
</PopoverTrigger>
<PopoverContent
align="center"
className="w-[calc(100vw-2rem)] p-0 sm:w-[32rem]"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<div className="space-y-4 p-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
{showTip && (
<div className="ml-auto flex items-center text-muted-foreground text-xs">
<InfoIcon className="mr-1.5 h-3.5 w-3.5" />
<span>Separate multiple with commas</span>
</div>
)}
</div>
<Textarea
className="min-h-[100px] font-mono text-sm"
onChange={handleTextareaChange}
placeholder={placeholder}
spellCheck={false}
value={(value as string) || ""}
/>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="relative w-full">
<SearchIcon className="-translate-y-1/2 absolute top-1/2 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
className="h-8 pl-8 text-sm"
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search presets..."
type="search"
value={searchTerm}
/>
</div>
</div>
<div className="max-h-60 overflow-y-auto rounded-md border">
{filteredPresets.length > 0 ? (
<div className="divide-y">
{filteredPresets.map((preset) => (
<div className="p-3 hover:bg-accent/50" key={preset.id}>
<div className="flex items-start space-x-3">
<Checkbox
checked={selectedPresets.includes(preset.id)}
className="mt-1 h-4 w-4 rounded"
id={preset.id}
onCheckedChange={() =>
handlePresetToggle(preset.id)
}
/>
<div className="flex-1 space-y-1">
<div className="flex items-center">
<Label
className="cursor-pointer font-medium text-sm leading-none"
htmlFor={preset.id}
>
{preset.label}
</Label>
</div>
<div className="flex justify-between">
<code className="break-all font-mono text-muted-foreground text-xs">
{preset.value}
</code>
</div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="p-4 text-center text-muted-foreground text-sm">
No presets found. Try a different search term.
</div>
)}
</div>
</div>
</div>
</PopoverContent>
</Popover>
</div>
);
}
@@ -1,940 +0,0 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import {
ArrowDownLeftIcon,
ArrowUpRightIcon,
CheckIcon,
CopyIcon,
InfoIcon,
PlayIcon,
} from "lucide-react";
import type { OpenAPIV3 } from "openapi-types";
import { useEffect, useMemo, useState } from "react";
import {
type ControllerRenderProps,
type UseFormReturn,
useForm,
} from "react-hook-form";
import { z } from "zod";
import { CodeClient } from "@/components/code/code.client";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ScrollShadow } from "@/components/ui/ScrollShadow";
import { Spinner } from "@/components/ui/Spinner";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { MultiNetworkSelector } from "../../../components/blocks/NetworkSelectors";
import type { BlueprintParameter, BlueprintPathMetadata } from "../utils";
import { AggregateParameterInput } from "./aggregate-parameter-input.client";
export function BlueprintPlayground(props: {
metadata: BlueprintPathMetadata;
backLink: string;
clientId: string;
path: string;
supportedChainIds: number[];
domain: string;
}) {
const [abortController, setAbortController] =
useState<AbortController | null>(null);
const requestMutation = useMutation({
mutationFn: async (url: string) => {
const controller = new AbortController();
setAbortController(controller);
const start = performance.now();
try {
const res = await fetch(url, {
headers: {
"x-client-id": props.clientId,
},
signal: controller.signal,
});
return {
data: await res.text(),
status: res.status,
time: performance.now() - start,
};
} catch (e) {
const time = performance.now() - start;
if (e instanceof Error) {
return {
data: e.message,
time: time,
};
}
return {
data: "Failed to fetch",
time: time,
};
}
},
});
return (
<BlueprintPlaygroundUI
abortRequest={() => {
if (abortController) {
// just abort it - don't set a new controller
abortController.abort();
}
}}
backLink={props.backLink}
clientId={props.clientId}
domain={props.domain}
isPending={requestMutation.isPending}
metadata={props.metadata}
onRun={(url) => {
requestMutation.mutate(url);
}}
path={props.path}
response={
abortController?.signal.aborted ? undefined : requestMutation.data
}
supportedChainIds={props.supportedChainIds}
/>
);
}
function modifyParametersForPlayground(_parameters: BlueprintParameter[]) {
const parameters = [..._parameters];
// make chain query param required - its not required in open api spec - because it either has to be set in subdomain or as a query param
const chainIdParameter = parameters.find((p) => p.name === "chain_id");
if (chainIdParameter) {
chainIdParameter.required = true;
}
// remove the client id parameter if it is present - we will always replace the parameter with project's client id
const clientIdParameterIndex = parameters.findIndex(
(p) => p.name === "clientId",
);
if (clientIdParameterIndex !== -1) {
parameters.splice(clientIdParameterIndex, 1);
}
// remove the chain parameter if it is present
const chainParameterIndex = parameters.findIndex((p) => p.name === "chain");
if (chainParameterIndex !== -1) {
parameters.splice(chainParameterIndex, 1);
}
return parameters;
}
function BlueprintPlaygroundUI(props: {
backLink: string;
isPending: boolean;
onRun: (url: string) => void;
response:
| {
time: number;
data: undefined | string;
status?: number;
}
| undefined;
clientId: string;
abortRequest: () => void;
domain: string;
path: string;
metadata: BlueprintPathMetadata;
supportedChainIds: number[];
}) {
const parameters = useMemo(() => {
const filteredParams = props.metadata.parameters?.filter(
isOpenAPIV3ParameterObject,
);
return modifyParametersForPlayground(filteredParams || []);
}, [props.metadata.parameters]);
const formSchema = useMemo(() => {
return createParametersFormSchema(parameters);
}, [parameters]);
const defaultValues = useMemo(() => {
const values: Record<string, string | number | string[] | number[]> = {};
for (const param of parameters) {
if (param.schema && "type" in param.schema && param.schema.default) {
values[param.name] = param.schema.default;
} else if (param.name === "filter_block_timestamp_gte") {
values[param.name] = Math.floor(
(Date.now() - 3 * 30 * 24 * 60 * 60 * 1000) / 1000,
);
} else if (param.name === "chain_id") {
values[param.name] = [];
} else {
values[param.name] = "";
}
}
return values;
}, [parameters]);
const form = useForm<z.infer<typeof formSchema>>({
defaultValues: defaultValues,
resolver: zodResolver(formSchema),
});
function onSubmit(values: z.infer<typeof formSchema>) {
const url = createBlueprintUrl({
clientId: props.clientId,
domain: props.domain,
intent: "run",
parameters: parameters,
path: props.path,
values: values,
});
props.onRun(url);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex grow flex-col">
<div className="flex grow flex-col">
<div className="flex grow flex-col overflow-hidden rounded-xl border bg-card">
<PlaygroundHeader
clientId={props.clientId}
domain={props.domain}
getFormValues={() => form.getValues()}
isPending={props.isPending}
parameters={parameters}
path={props.path}
/>
<div className="grid grow grid-cols-1 lg:grid-cols-2">
<div className="flex max-h-[500px] grow flex-col max-sm:border-b lg:max-h-[740px] lg:border-r">
<RequestConfigSection
domain={props.domain}
form={form}
parameters={parameters}
path={props.path}
supportedChainIds={props.supportedChainIds}
/>
</div>
<div className="flex h-[500px] grow flex-col lg:h-[740px]">
<ResponseSection
abortRequest={props.abortRequest}
isPending={props.isPending}
response={props.response}
/>
</div>
</div>
</div>
</div>
</div>
</form>
</Form>
);
}
function PlaygroundHeader(props: {
parameters: BlueprintParameter[];
isPending: boolean;
getFormValues: () => Record<string, string>;
clientId: string;
domain: string;
path: string;
}) {
const [hasCopied, setHasCopied] = useState(false);
return (
<div className="border-b px-4 py-4 lg:flex lg:justify-center lg:py-3">
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2 overflow-hidden rounded-xl border bg-muted/50 p-2 lg:justify-center">
{/* copy url */}
<Button
className="hover:accent h-auto w-auto p-1"
onClick={() => {
setHasCopied(true);
const url = createBlueprintUrl({
clientId: props.clientId,
domain: props.domain,
intent: "copy",
parameters: props.parameters,
path: props.path,
values: props.getFormValues(),
});
setTimeout(() => {
setHasCopied(false);
}, 500);
navigator.clipboard.writeText(url);
}}
variant="ghost"
>
{hasCopied ? (
<CheckIcon className="size-4 text-green-500" />
) : (
<CopyIcon className="size-4" />
)}
</Button>
{/* vertical line */}
<div className="h-6 w-[1px] bg-border" />
{/* domain + path */}
<div className="flex items-center gap-1.5 overflow-hidden">
<div className="rounded-xl bg-muted-foreground/10 px-2.5 py-1 font-mono text-xs lg:text-sm">
<span className="hidden lg:inline">{props.domain}</span>
<span className="lg:hidden">...</span>
</div>
<div className="truncate font-mono text-xs lg:text-sm">
{props.path}
</div>
</div>
{/* Run */}
<Button
className="ml-2 hidden h-auto w-auto gap-1.5 p-1 px-2 lg:flex"
disabled={props.isPending}
type="submit"
>
{props.isPending ? (
<Spinner className="size-4" />
) : (
<PlayIcon className="size-4" />
)}
Run
</Button>
</div>
<Button
className="gap-2 rounded-lg lg:hidden"
disabled={props.isPending}
size="sm"
type="submit"
>
{props.isPending ? (
<Spinner className="size-4" />
) : (
<PlayIcon className="size-4" />
)}
Run
</Button>
</div>
</div>
);
}
function RequestConfigSection(props: {
parameters: BlueprintParameter[];
form: ParametersForm;
domain: string;
path: string;
supportedChainIds: number[];
}) {
const { pathVariables, queryParams, filterQueryParams } = useMemo(() => {
const pathVariables: OpenAPIV3.ParameterObject[] = [];
const queryParams: OpenAPIV3.ParameterObject[] = [];
const filterQueryParams: OpenAPIV3.ParameterObject[] = [];
for (const param of props.parameters) {
if (param.in === "path") {
pathVariables.push(param);
}
if (param.in === "query") {
if (param.name.startsWith("filter_")) {
filterQueryParams.push(param);
} else {
queryParams.push(param);
}
}
}
return {
filterQueryParams,
pathVariables,
queryParams,
};
}, [props.parameters]);
const showError =
!props.form.formState.isValid &&
props.form.formState.isDirty &&
props.form.formState.isSubmitted;
return (
<div className="flex grow flex-col overflow-hidden">
<div className="flex min-h-[60px] items-center justify-between gap-2 border-b p-4 text-sm">
<div className="flex items-center gap-2">
<ArrowUpRightIcon className="size-5" />
Request
</div>
{showError && <Badge variant="destructive">Invalid Request</Badge>}
</div>
<ScrollShadow className="flex-1" scrollableClassName="max-h-full">
{pathVariables.length > 0 && (
<ParameterSection
className="border-b"
domain={props.domain}
form={props.form}
parameters={pathVariables}
path={props.path}
supportedChainIds={props.supportedChainIds}
title="Path Variables"
/>
)}
{queryParams.length > 0 && (
<ParameterSection
className="border-b"
domain={props.domain}
form={props.form}
parameters={queryParams}
path={props.path}
supportedChainIds={props.supportedChainIds}
title="Query Parameters"
/>
)}
{filterQueryParams.length > 0 && (
<ParameterSection
domain={props.domain}
form={props.form}
parameters={filterQueryParams}
path={props.path}
supportedChainIds={props.supportedChainIds}
title="Filter Query Parameters"
/>
)}
</ScrollShadow>
</div>
);
}
type ParametersForm = UseFormReturn<{
[x: string]: string | number | string[] | number[];
}>;
function ParameterSection(props: {
parameters: BlueprintParameter[];
title: string;
form: ParametersForm;
domain: string;
path: string;
supportedChainIds: number[];
className?: string;
}) {
return (
<div className={cn("p-4 py-6", props.className)}>
<h3 className="mb-3 font-medium text-sm"> {props.title} </h3>
<div className="overflow-hidden rounded-lg border">
{props.parameters.map((param, i) => {
const description =
param.schema && "type" in param.schema
? param.schema.description
: undefined;
const example =
param.schema && "type" in param.schema
? param.schema.example
: undefined;
const exampleToShow =
typeof example === "string" || typeof example === "number"
? example
: undefined;
const showTip = description !== undefined || example !== undefined;
const hasError = !!props.form.formState.errors[param.name];
return (
<FormField
control={props.form.control}
key={param.name}
name={param.name}
render={({ field }) => (
<FormItem
className={cn(
"space-y-0",
i !== props.parameters.length - 1 && "border-b",
)}
>
<div
className={cn(
"grid items-center",
param.name === "chain_id"
? "grid-cols-1 lg:grid-cols-2"
: "grid-cols-2",
)}
key={param.name}
>
<div className="flex h-full flex-row flex-wrap items-center justify-between gap-1 border-r px-3 py-2">
<div className="font-medium font-mono text-sm">
{param.name}
</div>
{param.required && (
<Badge
className="px-2 text-muted-foreground"
variant="secondary"
>
Required
</Badge>
)}
</div>
<div className="relative">
{param.name === "chain_id" ? (
<MultiNetworkSelector
chainIds={
props.supportedChainIds.length > 0
? props.supportedChainIds
: undefined
}
className="rounded-none border-0 border-t lg:border-none"
onChange={(chainIds) => {
props.form.setValue("chain_id", chainIds, {
shouldDirty: true,
shouldValidate: true,
});
}}
popoverContentClassName="min-w-[calc(100vw-20px)] lg:min-w-[500px]"
selectedBadgeClassName="bg-background"
selectedChainIds={
props.form.watch("chain_id") as number[]
}
/>
) : !Array.isArray(field.value) ? (
<>
<ParameterInput
endpointPath={props.path}
field={{
...field,
value: field.value,
}}
hasError={hasError}
param={param}
placeholder={param.description || param.name}
showTip={showTip}
/>
{showTip && (
<ToolTipLabel
contentClassName="max-w-[100vw] break-all"
hoverable
label={
<div className="flex flex-col gap-2">
{description && (
<p className="text-foreground">
{param.name === "aggregate"
? "Aggregation(s). You can type in multiple, separated by a comma, or select from the presets"
: description}
</p>
)}
{exampleToShow !== undefined && (
<div>
<p className="mb-1 text-muted-foreground">
Example:{" "}
<span className="font-mono">
{param.name === "aggregate"
? "count() AS count_all, countDistinct(address) AS unique_addresses"
: exampleToShow}
</span>
</p>
</div>
)}
</div>
}
>
<Button
asChild
className="-translate-y-1/2 absolute top-1/2 right-2 hidden h-auto w-auto p-1.5 text-muted-foreground opacity-50 hover:opacity-100 lg:flex"
variant="ghost"
>
<div>
<InfoIcon className="size-4" />
</div>
</Button>
</ToolTipLabel>
)}
</>
) : null}
</div>
</div>
<FormMessage className="mt-0 border-destructive-text border-t px-3 py-2" />
</FormItem>
)}
/>
);
})}
</div>
</div>
);
}
function ParameterInput(props: {
param: OpenAPIV3.ParameterObject;
field: ControllerRenderProps<
{
[x: string]: string | number;
},
string
>;
showTip: boolean;
hasError: boolean;
placeholder: string;
endpointPath: string;
}) {
const { param, field, showTip, hasError, placeholder } = props;
if (param.schema && "type" in param.schema && param.schema.enum) {
const { value, onChange, ...restField } = field;
return (
<Select
{...restField}
onValueChange={(v) => {
onChange({ target: { value: v } });
}}
value={value.toString()}
>
<SelectTrigger
chevronClassName="hidden"
className={cn(
"border-none bg-transparent pr-10 font-mono focus:ring-0 focus:ring-offset-0",
value === "" && "text-muted-foreground",
)}
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent className="font-mono">
{param.schema.enum.map((val) => {
return (
<SelectItem key={val} value={val}>
{val}
</SelectItem>
);
})}
</SelectContent>
</Select>
);
}
if (param.name === "aggregate") {
return (
<AggregateParameterInput
endpointPath={props.endpointPath}
field={field}
placeholder={placeholder}
showTip={showTip}
/>
);
}
return (
<Input
{...field}
className={cn(
"h-auto truncate rounded-none border-0 bg-transparent py-3 font-mono text-sm focus-visible:ring-0 focus-visible:ring-offset-0",
showTip && "lg:pr-10",
hasError && "text-destructive-text",
)}
placeholder={placeholder}
/>
);
}
function formatMilliseconds(ms: number) {
if (ms < 1000) {
return `${Math.round(ms)}ms`;
}
return `${(ms / 1000).toFixed(2)}s`;
}
function ResponseSection(props: {
isPending: boolean;
response:
| { data: undefined | string; status?: number; time: number }
| undefined;
abortRequest: () => void;
}) {
const formattedData = useMemo(() => {
if (!props.response?.data) return undefined;
try {
return JSON.stringify(JSON.parse(props.response.data), null, 2);
} catch {
return props.response.data;
}
}, [props.response]);
return (
<div className="flex h-full grow flex-col">
<div className="flex min-h-[60px] items-center justify-between gap-2 border-b p-4 text-sm">
<div className="flex items-center gap-2">
<ArrowDownLeftIcon className="size-5" />
Response
{props.isPending && <ElapsedTimeCounter />}
{props.response?.time && !props.isPending && (
<span className="text-muted-foreground">
{formatMilliseconds(props.response.time)}
</span>
)}
</div>
{!props.isPending && props.response?.status && (
<Badge
variant={
props.response.status >= 200 && props.response.status < 300
? "success"
: "destructive"
}
>
{props.response.status}
</Badge>
)}
</div>
{props.isPending && (
<div className="flex grow flex-col items-center justify-center gap-4">
<Spinner className="size-14 text-muted-foreground" />
<Button onClick={props.abortRequest} size="sm" variant="ghost">
Cancel
</Button>
</div>
)}
{!props.isPending && !props.response && (
<div className="flex grow flex-col items-center justify-center p-4">
<div>
<div className="flex justify-center">
<div className="rounded-xl border p-1">
<div className="rounded-lg border bg-card p-1.5">
<PlayIcon className="size-5 text-muted-foreground" />
</div>
</div>
</div>
<p className="mt-3 text-center">Click Run to start a request</p>
</div>
</div>
)}
{!props.isPending && props.response && (
<CodeClient
className="rounded-none border-none bg-transparent"
code={formattedData || ""}
lang="json"
scrollableClassName="h-full"
scrollableContainerClassName="h-full"
// shadowColor="hsl(var(--muted)/50%)"
/>
)}
</div>
);
}
function openAPIV3ParamToZodFormSchema(
schema: BlueprintParameter["schema"],
isRequired: boolean,
): z.ZodTypeAny | undefined {
if (!schema) {
return;
}
if ("anyOf" in schema) {
const anyOf = schema.anyOf;
if (!anyOf) {
return;
}
const anySchemas = anyOf
.map((s) => openAPIV3ParamToZodFormSchema(s, isRequired))
.filter((x) => !!x);
// @ts-expect-error - Its ok, z.union is expecting tuple type but we have array
return z.union(anySchemas);
}
if (!("type" in schema)) {
return;
}
// if enum values
const enumValues = schema.enum;
if (enumValues) {
const enumSchema = z.enum(
// @ts-expect-error - Its correct
enumValues,
);
if (isRequired) {
return enumSchema;
}
return enumSchema.or(z.literal(""));
}
switch (schema.type) {
case "integer": {
const intSchema = z.coerce
.number({
message: "Must be an integer",
})
.int({
message: "Must be an integer",
});
return isRequired
? intSchema.min(1, {
message: "Required",
})
: intSchema.optional();
}
case "number": {
const numberSchema = z.coerce.number();
return isRequired
? numberSchema.min(1, {
message: "Required",
})
: numberSchema.optional();
}
case "boolean": {
const booleanSchema = z.coerce.boolean();
return isRequired ? booleanSchema : booleanSchema.optional();
}
case "array": {
if ("type" in schema.items) {
let itemSchema: z.ZodTypeAny | undefined;
if (schema.items.type === "number") {
itemSchema = z.number();
} else if (schema.items.type === "integer") {
itemSchema = z.number().int();
} else if (schema.items.type === "string") {
itemSchema = z.string();
}
if (itemSchema) {
const arraySchema = isRequired
? z.array(itemSchema).min(1, { message: "Required" })
: z.array(itemSchema);
const arrayOrSingleItemSchema = z.union([arraySchema, itemSchema]);
return isRequired
? arrayOrSingleItemSchema
: arrayOrSingleItemSchema.optional();
}
}
break;
}
// everything else - just accept it as a string;
default: {
const stringSchema = z.string();
return isRequired
? stringSchema.min(1, {
message: "Required",
})
: stringSchema.optional();
}
}
}
function createParametersFormSchema(parameters: BlueprintParameter[]) {
const shape: z.ZodRawShape = {};
for (const param of parameters) {
if (param.deprecated) {
continue;
}
const paramSchema = openAPIV3ParamToZodFormSchema(
param.schema,
!!param.required,
);
if (paramSchema) {
shape[param.name] = paramSchema;
} else {
shape[param.name] = param.required
? z.string().min(1, { message: "Required" })
: z.string();
}
}
return z.object(shape);
}
function createBlueprintUrl(options: {
parameters: BlueprintParameter[];
values: Record<string, string>;
clientId: string;
domain: string;
path: string;
intent: "copy" | "run";
}) {
const { parameters, domain, path, values, clientId } = options;
let url = `${domain}${path}`;
// loop over the values and replace {x} or :x with the actual values for paths
// and add query parameters
const pathVariables = parameters.filter((param) => param.in === "path");
const queryParams = parameters.filter((param) => param.in === "query");
for (const parameter of pathVariables) {
const value = values[parameter.name];
if (value) {
url = url.replace(`{${parameter.name}}`, value);
url = url.replace(`:${parameter.name}`, value);
}
}
const searchParams = new URLSearchParams();
for (const parameter of queryParams) {
const value = values[parameter.name];
if (value) {
if (Array.isArray(value)) {
for (const v of value) {
searchParams.append(parameter.name, v);
}
} else {
searchParams.append(parameter.name, value);
}
}
}
// add client Id search param
if (options.intent === "run") {
searchParams.append("clientId", clientId);
} else {
searchParams.append("clientId", "YOUR_THIRDWEB_CLIENT_ID");
}
if (searchParams.toString()) {
url = `${url}?${searchParams.toString()}`;
}
return url;
}
function ElapsedTimeCounter() {
const [ms, setMs] = useState(0);
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
const internal = 100;
const id = setInterval(() => {
setMs((prev) => prev + internal);
}, internal);
return () => clearInterval(id);
}, []);
return (
<span className="fade-in-0 animate-in text-muted-foreground text-sm duration-300">
{formatMilliseconds(ms)}
</span>
);
}
function isOpenAPIV3ParameterObject(
x: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject,
): x is OpenAPIV3.ParameterObject {
return !("$ref" in x);
}
@@ -1,104 +0,0 @@
import { redirect } from "next/navigation";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { cn } from "@/lib/utils";
import { THIRDWEB_CLIENT } from "../../../lib/client";
import { isProd } from "../../../lib/env";
import { fetchBlueprintSpec } from "../utils";
import { BlueprintPlayground } from "./blueprint-playground.client";
export default async function Page(props: {
params: Promise<{
blueprint_slug: string;
}>;
searchParams: Promise<{ path: string }>;
}) {
const [params, searchParams] = await Promise.all([
props.params,
props.searchParams,
]);
// invalid url
if (!searchParams.path) {
redirect("/insight");
}
const thirdwebDomain = !isProd ? "thirdweb-dev" : "thirdweb";
const domain = `https://insight.${thirdwebDomain}.com`;
const [blueprintSpec] = await Promise.all([
fetchBlueprintSpec({
blueprintId: params.blueprint_slug,
}),
]);
const pathMetadata = blueprintSpec.openapiJson.paths[searchParams.path]?.get;
// invalid url
if (!pathMetadata) {
redirect("/insight");
}
const supportedChainIds =
blueprintSpec.openapiJson.servers?.[0]?.variables?.chainId?.enum?.map(
Number,
) || [];
const title = pathMetadata.summary || "";
return (
<div>
<Breadcrumbs />
<h1 className="mt-3 mb-6 font-semibold text-2xl tracking-tight">
<div className="flex items-center gap-2">
<span
className={cn(
pathMetadata.deprecated && "text-muted-foreground line-through",
)}
>
{title}
</span>
{pathMetadata.deprecated && (
<Badge className="text-xs" variant="destructive">
Deprecated
</Badge>
)}
</div>
</h1>
{pathMetadata.deprecated && (
<div className="mt-3 mb-6">
<div className="flex items-center gap-2">
<span>{pathMetadata.description}</span>
</div>
</div>
)}
<BlueprintPlayground
backLink={"/insight"}
clientId={THIRDWEB_CLIENT.clientId}
domain={domain}
key={searchParams.path}
metadata={pathMetadata}
path={searchParams.path}
supportedChainIds={supportedChainIds}
/>
</div>
);
}
function Breadcrumbs() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/insight">Insight Blueprints</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
</BreadcrumbList>
</Breadcrumb>
);
}
@@ -1,239 +0,0 @@
// This file is auto-generated by the update-insight-blueprints script
import type { MinimalBlueprintSpec } from "./utils";
export const insightBlueprints: MinimalBlueprintSpec[] = [
{
id: "events",
name: "Events",
paths: [
{
deprecated: false,
name: "Get events",
path: "/v1/events",
},
{
deprecated: false,
name: "Get contract events",
path: "/v1/events/{contractAddress}",
},
{
deprecated: false,
name: "Get contract events with specific signature",
path: "/v1/events/{contractAddress}/{signature}",
},
],
},
{
id: "transactions",
name: "Transactions",
paths: [
{
deprecated: false,
name: "Get transactions",
path: "/v1/transactions",
},
{
deprecated: false,
name: "Get contract transactions",
path: "/v1/transactions/{contractAddress}",
},
{
deprecated: false,
name: "Get contract transactions with specific signature",
path: "/v1/transactions/{contractAddress}/{signature}",
},
],
},
{
id: "blocks",
name: "Blocks",
paths: [
{
deprecated: false,
name: "Get blocks",
path: "/v1/blocks",
},
],
},
{
id: "tokens",
name: "Tokens",
paths: [
{
deprecated: false,
name: "Get token owners by contract",
path: "/v1/tokens/owners",
},
{
deprecated: false,
name: "Get token transfers by transaction",
path: "/v1/tokens/transfers/transaction/{transaction_hash}",
},
{
deprecated: false,
name: "Get token transfers by contract",
path: "/v1/tokens/transfers/{contract_address}",
},
{
deprecated: false,
name: "Get token transfers",
path: "/v1/tokens/transfers",
},
{
deprecated: true,
name: "Get ERC-20 balances by address",
path: "/v1/tokens/erc20/{ownerAddress}",
},
{
deprecated: false,
name: "Get tokens",
path: "/v1/tokens",
},
{
deprecated: true,
name: "Get ERC-721 balances by address",
path: "/v1/tokens/erc721/{ownerAddress}",
},
{
deprecated: true,
name: "Get ERC-1155 balances by address",
path: "/v1/tokens/erc1155/{ownerAddress}",
},
{
deprecated: false,
name: "Get supported tokens for price data",
path: "/v1/tokens/price/supported",
},
{
deprecated: false,
name: "Get token price",
path: "/v1/tokens/price",
},
{
deprecated: false,
name: "Token lookup",
path: "/v1/tokens/lookup",
},
],
},
{
id: "nfts",
name: "Nfts",
paths: [
{
deprecated: false,
name: "Get NFT balances by address",
path: "/v1/nfts/balance/{ownerAddress}",
},
{
deprecated: false,
name: "Get collection",
path: "/v1/nfts/collections/{contract_address}",
},
{
deprecated: false,
name: "Get NFTs",
path: "/v1/nfts",
},
{
deprecated: false,
name: "Get NFT owners by contract",
path: "/v1/nfts/owners/{contract_address}",
},
{
deprecated: false,
name: "Get NFT owners by token",
path: "/v1/nfts/owners/{contract_address}/{token_id}",
},
{
deprecated: false,
name: "Get NFT transfers",
path: "/v1/nfts/transfers",
},
{
deprecated: false,
name: "Get NFT transfers by transaction",
path: "/v1/nfts/transfers/transaction/{transaction_hash}",
},
{
deprecated: false,
name: "Get NFT transfers by contract",
path: "/v1/nfts/transfers/{contract_address}",
},
{
deprecated: false,
name: "Get NFTs by contract",
path: "/v1/nfts/{contract_address}",
},
{
deprecated: false,
name: "Get NFT transfers by token",
path: "/v1/nfts/transfers/{contract_address}/{token_id}",
},
{
deprecated: false,
name: "Get NFT by token ID",
path: "/v1/nfts/{contract_address}/{token_id}",
},
{
deprecated: false,
name: "Force refresh collection metadata",
path: "/v1/nfts/metadata/refresh/{contract_address}",
},
{
deprecated: false,
name: "Force refresh token metadata",
path: "/v1/nfts/metadata/refresh/{contract_address}/{token_id}",
},
],
},
{
id: "wallets",
name: "Wallets",
paths: [
{
deprecated: false,
name: "Get wallet transactions",
path: "/v1/wallets/{wallet_address}/transactions",
},
],
},
{
id: "contracts",
name: "Contracts",
paths: [
{
deprecated: false,
name: "Get contract ABI​",
path: "/v1/contracts/abi/{contractAddress}",
},
{
deprecated: false,
name: "Get contract metadata​",
path: "/v1/contracts/metadata/{contractAddress}",
},
],
},
{
id: "decode",
name: "Decode",
paths: [
{
deprecated: false,
name: "Decode logs and transactions​",
path: "/v1/decode/{contractAddress}",
},
],
},
{
id: "resolve",
name: "Resolve",
paths: [
{
deprecated: false,
name: "Resolve",
path: "/v1/resolve/{input}",
},
],
},
];
@@ -1,31 +0,0 @@
import type React from "react";
import { createMetadata } from "@/lib/metadata";
import { PageLayout } from "../../components/blocks/APIHeader";
import { InsightIcon } from "../../icons/InsightIcon";
const title = "Insight";
const description =
"Simple & customizable endpoints for querying rich blockchain data";
export const metadata = createMetadata({
title,
description,
image: {
icon: "insight",
title,
},
});
export default function Layout(props: { children: React.ReactNode }) {
return (
<PageLayout
icon={InsightIcon}
containerClassName="pb-16"
description={description}
docsLink="https://portal.thirdweb.com/insight?utm_source=playground"
title={title}
>
{props.children}
</PageLayout>
);
}
@@ -1,85 +0,0 @@
import Link from "next/link";
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
import { insightBlueprints } from "./insightBlueprints";
export default function Page() {
return (
<div>
<h2 className="mb-2 font-semibold text-2xl tracking-tight">Blueprints</h2>
<p className="mb-5 text-muted-foreground">
A blueprint is an API that provides access to on-chain data in a
user-friendly format. <br /> There is no need for ABIs, decoding, RPC,
or web3 knowledge to fetch blockchain data.{" "}
<Link
className="underline decoration-muted-foreground/50 decoration-dotted underline-offset-[5px] hover:decoration-solid"
href="https://portal.thirdweb.com/insight/blueprints?utm_source=playground"
target="_blank"
>
Learn more about Insight Blueprints{" "}
</Link>
</p>
<div className="flex flex-col gap-8">
{insightBlueprints.map((blueprint) => {
return (
<BlueprintSection
blueprints={blueprint.paths.map((pathInfo) => {
return {
deprecated: pathInfo.deprecated,
link: `/insight/${blueprint.id}?path=${pathInfo.path}`,
name: pathInfo.name,
};
})}
key={blueprint.id}
title={blueprint.name}
/>
);
})}
</div>
</div>
);
}
function BlueprintSection(props: {
title: string;
blueprints: { name: string; link: string; deprecated?: boolean }[];
}) {
return (
<div className="overflow-hidden rounded-lg border bg-card">
<div className="flex items-center gap-2 border-b bg-accent/20 px-6 py-4">
<h2 className="font-semibold text-lg tracking-tight">{props.title}</h2>
</div>
<Table>
<TableBody>
{props.blueprints.map((item) => (
<TableRow
className="group hover:bg-accent/50"
key={item.link}
linkBox
>
<TableCell>
<span className="flex items-center gap-3">
<Link
className={`${
item.deprecated
? "line-through before:absolute before:inset-0"
: ""
}`}
href={item.link}
>
{item.name}
</Link>
{item.deprecated && (
<span className="rounded bg-destructive/10 px-2 py-0.5 font-medium text-destructive text-xs">
Deprecated
</span>
)}
</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
@@ -1,57 +0,0 @@
// Note: This file is also used in the update-insight-blueprints script
// Do not use Next.js/Vercel specific APIs or envs here
import type { OpenAPIV3 } from "openapi-types";
export type BlueprintParameter = OpenAPIV3.ParameterObject;
export type BlueprintPathMetadata = OpenAPIV3.PathItemObject;
const THIRDWEB_INSIGHT_API_DOMAIN =
process.env.NEXT_PUBLIC_INSIGHT_URL || "insight.thirdweb.com";
export type BlueprintListItem = {
id: string;
name: string;
description: string;
slug: string;
openapiJson: OpenAPIV3.Document;
openapiUrl: string;
paths: {
name: string;
path: string;
method: string;
deprecated: boolean;
}[];
};
export type BlueprintSpec = {
id: string;
name: string;
description: string;
openapiJson: OpenAPIV3.Document;
};
export type MinimalBlueprintSpec = {
id: string;
name: string;
paths: {
name: string;
path: string;
deprecated: boolean;
}[];
};
export async function fetchBlueprintSpec(params: { blueprintId: string }) {
const res = await fetch(
`https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/blueprints/${params.blueprintId}`,
);
if (!res.ok) {
const text = await res.text();
throw new Error(`Failed to fetch blueprint: ${text}`);
}
const json = (await res.json()) as { data: BlueprintSpec };
return json.data;
}
+6 -38
View File
@@ -1,14 +1,12 @@
"use client";
import { ArrowLeftRightIcon, BotIcon } from "lucide-react";
import { ArrowLeftRightIcon, BotIcon, Code2Icon } from "lucide-react";
import type { ShadcnSidebarLink } from "@/components/blocks/full-width-sidebar-layout";
import { ContractIcon } from "../icons/ContractIcon";
import { InsightIcon } from "../icons/InsightIcon";
import { PayIcon } from "../icons/PayIcon";
import { SmartAccountIcon } from "../icons/SmartAccountIcon";
import { TokenIcon } from "../icons/TokenIcon";
import { WalletProductIcon } from "../icons/WalletProductIcon";
import { insightBlueprints } from "./insight/insightBlueprints";
const ai: ShadcnSidebarLink = {
subMenu: {
@@ -207,10 +205,6 @@ const payments: ShadcnSidebarLink = {
href: "/payments/transactions",
label: "Onchain Transaction",
},
{
href: "/payments/backend",
label: "Payments API",
},
],
};
@@ -240,36 +234,6 @@ const transactions: ShadcnSidebarLink = {
],
};
const insightLinks: ShadcnSidebarLink[] = insightBlueprints.map((blueprint) => {
return {
links: blueprint.paths.map((pathInfo) => {
return {
href: `/insight/${blueprint.id}?path=${pathInfo.path}`,
label: pathInfo.name,
exactMatch: true,
};
}),
subMenu: {
label: blueprint.name,
},
};
});
const insight: ShadcnSidebarLink = {
links: [
{
href: "/insight",
label: "Overview",
exactMatch: true,
},
...insightLinks,
],
subMenu: {
label: "Insight",
icon: InsightIcon,
},
};
export const sidebarLinks: ShadcnSidebarLink[] = [
ai,
wallets,
@@ -277,6 +241,10 @@ export const sidebarLinks: ShadcnSidebarLink[] = [
contracts,
payments,
tokens,
insight,
accountAbstractions,
{
href: "/reference",
label: "API Reference",
icon: Code2Icon,
},
];
-2
View File
@@ -5,7 +5,6 @@ import {
aiFeatureCards,
contractsFeatureCards,
type FeatureCardMetadata,
insightFeatureCards,
paymentsFeatureCards,
transactionsFeatureCards,
walletsFeatureCards,
@@ -48,7 +47,6 @@ export default function Page() {
title="Contracts"
/>
<FeatureSection featureCards={paymentsFeatureCards} title="Payments" />
<FeatureSection featureCards={insightFeatureCards} title="Insight" />
<FeatureSection
featureCards={accountAbstractionsFeatureCards}
title="Account Abstraction"
@@ -1,35 +0,0 @@
import { BracesIcon } from "lucide-react";
import type React from "react";
import { PageHeader } from "@/components/blocks/APIHeader";
import { createMetadata } from "@/lib/metadata";
const title = "Payments API";
const description =
"Create customizable components or backend flows with an HTTP API to onramp, swap, and bridge to and from different cryptocurrencies";
const ogDescription =
"Bridge, swap, and onramp any currency with thirdweb’s Payments REST API. Use simple HTTP requests to integrate from your backend.";
export const metadata = createMetadata({
description: ogDescription,
title,
image: {
icon: "payments",
title,
},
});
export default function Layout(props: { children: React.ReactNode }) {
return (
<div>
<PageHeader
description={description}
docsLink="https://portal.thirdweb.com/payments?utm_source=playground"
icon={BracesIcon}
title={title}
containerClassName="max-w-[1400px]"
/>
{props.children}
</div>
);
}
@@ -1,100 +0,0 @@
import { ArrowUpRightIcon } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
import { getBridgePaths } from "./utils";
export default async function Page() {
try {
const paths = await getBridgePaths();
return (
<div className="container max-w-[1400px] pb-20">
<div className="flex flex-col justify-between md:flex-row md:gap-8">
<div>
<h2 className="mb-0.5 font-semibold text-xl tracking-tight">
thirdweb Payments REST API
</h2>
<p className="mb-5 text-muted-foreground">
Directly interact with the thirdweb Payments API from your
backend, using standard REST APIs.
</p>
</div>
<Link href="https://bridge.thirdweb.com/reference" target="_blank">
<Button
className="max-md:w-full rounded-full gap-2"
variant="outline"
size="sm"
>
View all endpoints
<ArrowUpRightIcon className="size-4" />
</Button>
</Link>
</div>
<div className="flex flex-col gap-8">
<BlueprintSection
blueprints={paths
.filter(
([_pathName, pathObj]) => typeof pathObj?.get !== "undefined",
)
.map(([pathName, pathObj]) => {
if (!pathObj) {
throw new Error(`Path not found: ${pathName}`);
}
return {
description: pathObj.get?.description || "",
link: `/payments/backend/reference?route=${pathName}`,
name: pathName,
};
})}
title="Available GET endpoints"
/>
</div>
</div>
);
} catch (error) {
console.error(error);
return <div>Error fetching API spec</div>;
}
}
function BlueprintSection(props: {
title: string;
blueprints: { name: string; description: string; link: string }[];
}) {
return (
<div className="overflow-hidden rounded-lg border bg-card">
<div className="flex items-center gap-2 border-b bg-accent/20 px-6 py-4">
<h2 className="font-semibold text-lg tracking-tight">{props.title}</h2>
</div>
<Table>
<TableBody>
{props.blueprints.map((item) => (
<TableRow
className="group hover:bg-accent/50"
key={item.link}
linkBox
>
<TableCell>
<span className="flex items-center gap-3">
<Link
className="before:absolute before:inset-0"
href={item.link}
>
<div className="flex flex-col">
<p className="font-semibold text-base">{item.name}</p>
<p className="text-muted-foreground text-sm">
{item.description}
</p>
</div>
</Link>
</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
@@ -1,72 +0,0 @@
import { redirect } from "next/navigation";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { THIRDWEB_CLIENT } from "../../../../lib/client";
import { isProd } from "../../../../lib/env";
import { BlueprintPlayground } from "../../../insight/[blueprint_slug]/blueprint-playground.client";
import { getBridgePaths } from "../utils";
export default async function Page(props: {
searchParams: Promise<{
route: string;
}>;
}) {
const params = await props.searchParams;
// invalid url
if (!params.route) {
redirect("/payments/backend");
}
const thirdwebDomain = !isProd ? "thirdweb-dev" : "thirdweb";
const domain = `https://bridge.${thirdwebDomain}.com`;
const paths = await getBridgePaths();
const pathMetadata = paths.find(([path]) => path === params.route)?.[1]?.get;
// invalid url
if (!pathMetadata) {
redirect("/payments/backend");
}
const title = pathMetadata.summary || "";
return (
<div className="container max-w-[1400px] pb-8">
<Breadcrumbs />
<div className="h-3" />
{title && (
<h1 className="mb-6 font-semibold text-2xl tracking-tight lg:text-3xl">
{title}
</h1>
)}
<BlueprintPlayground
backLink={"/payments/backend"}
clientId={THIRDWEB_CLIENT.clientId}
domain={domain}
key={params.route}
metadata={pathMetadata}
path={params.route}
supportedChainIds={[]}
/>
</div>
);
}
function Breadcrumbs() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/payments/backend">Payments API</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
</BreadcrumbList>
</Breadcrumb>
);
}
@@ -1,13 +0,0 @@
import type { OpenAPIV3 } from "openapi-types";
import { isProd } from "../../../lib/env";
export async function getBridgePaths() {
const thirdwebDomain = !isProd ? "thirdweb-dev" : "thirdweb";
const res = await fetch(`https://bridge.${thirdwebDomain}.com/openapi.json`);
const openapiJson = (await res.json()) as OpenAPIV3.Document;
return Object.entries(openapiJson.paths).filter(
([, pathObj]) =>
pathObj?.get?.deprecated === undefined ||
pathObj?.get?.deprecated === false,
);
}
@@ -0,0 +1,7 @@
import { ScalarApiReference } from "../../components/blocks/scalar-api/ScalarClient";
export default async function ReferencePage() {
const responses = await fetch("https://api.thirdweb.com/openapi.json");
const spec = await responses.json();
return <ScalarApiReference spec={spec} />;
}
@@ -0,0 +1,43 @@
"use client";
import { ApiReferenceReact } from "@scalar/api-reference-react";
import "@scalar/api-reference-react/style.css";
import "./scalar.css";
import { useTheme } from "next-themes";
import type { OpenAPIV3 } from "openapi-types";
import { useEffect } from "react";
export function ScalarApiReference(props: { spec: OpenAPIV3.Document }) {
const { theme } = useTheme();
// scaler is using light-mode and dark-mode classes for theming
useEffect(() => {
if (theme === "dark") {
document.body.classList.remove("light-mode");
document.body.classList.add("dark-mode");
} else {
document.body.classList.remove("dark-mode");
document.body.classList.add("light-mode");
}
}, [theme]);
return (
<ApiReferenceReact
configuration={{
theme: "deepSpace",
sources: [
{
content: props.spec,
},
],
layout: "modern",
withDefaultFonts: false,
showSidebar: true,
hideModels: false,
hideDarkModeToggle: true,
hideDownloadButton: false,
hideTestRequestButton: false,
}}
/>
);
}
@@ -0,0 +1,4 @@
.scalar-app {
--scalar-background-1: hsl(var(--background));
--scalar-background-2: hsl(var(--card));
}
@@ -1,21 +0,0 @@
export function InsightIcon(props: { className?: string }) {
return (
<svg
className={props.className}
fill="none"
height="24"
role="presentation"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20.5 7.27783L12 12.0001M12 12.0001L3.49997 7.27783M12 12.0001L12 21.5001M21 12.0001V7.94153C21 7.59889 21 7.42757 20.9495 7.27477C20.9049 7.13959 20.8318 7.01551 20.7354 6.91082C20.6263 6.79248 20.4766 6.70928 20.177 6.54288L12.777 2.43177C12.4934 2.27421 12.3516 2.19543 12.2015 2.16454C12.0685 2.13721 11.9315 2.13721 11.7986 2.16454C11.6484 2.19543 11.5066 2.27421 11.223 2.43177L3.82297 6.54288C3.52345 6.70928 3.37368 6.79248 3.26463 6.91082C3.16816 7.01551 3.09515 7.13959 3.05048 7.27477C3 7.42757 3 7.59889 3 7.94153V16.0586C3 16.4013 3 16.5726 3.05048 16.7254C3.09515 16.8606 3.16816 16.9847 3.26463 17.0893C3.37368 17.2077 3.52346 17.2909 3.82297 17.4573L11.223 21.5684C11.5066 21.726 11.6484 21.8047 11.7986 21.8356C11.9315 21.863 12.0685 21.863 12.2015 21.8356C12.3516 21.8047 12.4934 21.726 12.777 21.5684L13 21.4445M7.5 4.50008L16.5 9.50008M22 21.5001L21 20.5001M22 18.0001C22 19.6569 20.6569 21.0001 19 21.0001C17.3431 21.0001 16 19.6569 16 18.0001C16 16.3432 17.3431 15.0001 19 15.0001C20.6569 15.0001 22 16.3432 22 18.0001Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg>
);
}
+3 -3
View File
@@ -1,6 +1,6 @@
export const isProd =
(process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV) ===
"production";
// export const isProd =
// (process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV) ===
// "production";
function getVercelEnv(): "production" | "preview" | "development" {
const onVercel = process.env.VERCEL || process.env.NEXT_PUBLIC_VERCEL_ENV;
-6
View File
@@ -3,7 +3,6 @@ import {
BoxIcon,
BracesIcon,
BrainIcon,
ExternalLinkIcon,
MessageCircleQuestionIcon,
NetworkIcon,
RocketIcon,
@@ -27,11 +26,6 @@ export const sidebar: SideBar = {
icon: <StickyNoteIcon />,
name: "Use Cases",
},
{
href: "https://playground.thirdweb.com/insight",
icon: <ExternalLinkIcon />,
name: "Playground",
},
{
href: `${insightSlug}/get-started`,
icon: <RocketIcon />,
@@ -4,9 +4,10 @@ import { ApiReferenceReact } from "@scalar/api-reference-react";
import "@scalar/api-reference-react/style.css";
import "./scalar.css";
import { useTheme } from "next-themes";
import type { OpenAPIV3 } from "openapi-types";
import { useEffect } from "react";
export function ScalarApiReference() {
export function ScalarApiReference(props: { spec: OpenAPIV3.Document }) {
const { theme } = useTheme();
// scaler is using light-mode and dark-mode classes for theming
@@ -23,12 +24,17 @@ export function ScalarApiReference() {
return (
<ApiReferenceReact
configuration={{
url: "https://api.thirdweb.com/openapi.json",
theme: "deepSpace",
sources: [
{
content: props.spec,
},
],
layout: "modern",
withDefaultFonts: false,
showSidebar: true,
hideModels: false,
hideDarkModeToggle: true, // Hide since portal controls theme
hideDarkModeToggle: true,
hideDownloadButton: false,
hideTestRequestButton: false,
}}
+4 -2
View File
@@ -10,10 +10,12 @@ export const metadata = createMetadata({
description: "Complete REST API reference for all thirdweb services",
});
export default function ApiReferencePage() {
export default async function ApiReferencePage() {
const responses = await fetch("https://api.thirdweb.com/openapi.json");
const spec = await responses.json();
return (
<div className="container max-sm:px-0">
<ScalarApiReference />
<ScalarApiReference spec={spec} />
</div>
);
}
+4 -1
View File
@@ -615,6 +615,9 @@ importers:
'@radix-ui/react-use-controllable-state':
specifier: ^1.2.2
version: 1.2.2(@types/[email protected])([email protected])
'@scalar/api-reference-react':
specifier: ^0.7.42
version: 0.7.42([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
'@tanstack/react-query':
specifier: 5.81.5
version: 5.81.5([email protected])
@@ -18079,7 +18082,7 @@ snapshots:
'@babel/code-frame': 7.27.1
'@babel/generator': 7.28.0
'@babel/helper-globals': 7.28.0
'@babel/parser': 7.28.0
'@babel/parser': 7.28.3
'@babel/template': 7.27.2
'@babel/types': 7.28.2
debug: 4.4.1([email protected])