Skip to content

[Dashboard] Add chain infrastructure deployment and management #7456

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion apps/dashboard/src/@/actions/billing.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"use server";
import "server-only";

import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import type { ChainInfraSKU } from "@/types/billing";
import { getAbsoluteUrl } from "@/utils/vercel";

export async function reSubscribePlan(options: {
teamId: string;
Expand All @@ -14,7 +17,10 @@ export async function reSubscribePlan(options: {
}

const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/checkout/resubscribe-plan`,
new URL(
`/v1/teams/${options.teamId}/checkout/resubscribe-plan`,
NEXT_PUBLIC_THIRDWEB_API_HOST,
),
{
body: JSON.stringify({}),
headers: {
Expand All @@ -35,3 +41,83 @@ export async function reSubscribePlan(options: {
status: 200,
};
}

export async function getChainInfraCheckoutURL(options: {
teamSlug: string;
skus: ChainInfraSKU[];
chainId: number;
annual: boolean;
}) {
const token = await getAuthToken();

if (!token) {
return {
error: "You are not logged in",
status: "error",
} as const;
}

const res = await fetch(
new URL(
`/v1/teams/${options.teamSlug}/checkout/create-link`,
NEXT_PUBLIC_THIRDWEB_API_HOST,
),
{
body: JSON.stringify({
annual: options.annual,
baseUrl: getAbsoluteUrl(),
chainId: options.chainId,
skus: options.skus,
}),
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
method: "POST",
},
);
if (!res.ok) {
const text = await res.text();
console.error("Failed to create checkout link", text, res.status);
switch (res.status) {
case 402: {
return {
error:
"You have outstanding invoices, please pay these first before re-subscribing.",
status: "error",
} as const;
}
case 429: {
return {
error: "Too many requests, please try again later.",
status: "error",
} as const;
}
case 403: {
return {
error: "You are not authorized to deploy infrastructure.",
status: "error",
} as const;
}
default: {
return {
error: "An unknown error occurred, please try again later.",
status: "error",
} as const;
}
}
}

const json = await res.json();
if (!json.result) {
return {
error: "An unknown error occurred, please try again later.",
status: "error",
} as const;
}

return {
data: json.result as string,
status: "success",
} as const;
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
export const authOptions = [
"email",
"phone",
"passkey",
"siwe",
"guest",
"google",
"facebook",
"x",
"discord",
"farcaster",
"telegram",
"github",
"twitch",
"steam",
"apple",
"coinbase",
"line",
] as const;
import "server-only";

import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { getAuthToken } from "./auth-token";

export type AuthOption =
| "email"
| "phone"
| "passkey"
| "siwe"
| "guest"
| "google"
| "facebook"
| "x"
| "discord"
| "farcaster"
| "telegram"
| "github"
| "twitch"
| "steam"
| "apple"
| "coinbase"
| "line";

export type Ecosystem = {
name: string;
imageUrl?: string;
id: string;
slug: string;
permission: "PARTNER_WHITELIST" | "ANYONE";
authOptions: (typeof authOptions)[number][];
authOptions: AuthOption[];
customAuthOptions?: {
authEndpoint?: {
url: string;
Expand All @@ -47,6 +51,54 @@ export type Ecosystem = {
updatedAt: string;
};

export async function fetchEcosystemList(teamIdOrSlug: string) {
const token = await getAuthToken();

if (!token) {
return [];
}

const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/ecosystem-wallet`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);

if (!res.ok) {
return [];
}

return (await res.json()).result as Ecosystem[];
}

export async function fetchEcosystem(slug: string, teamIdOrSlug: string) {
const token = await getAuthToken();

if (!token) {
return null;
}

const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/ecosystem-wallet/${slug}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
if (!res.ok) {
const data = await res.json();
console.error(data);
return null;
}

const data = (await res.json()) as { result: Ecosystem };
return data.result;
}

type PartnerPermission = "PROMPT_USER_V1" | "FULL_CONTROL_V1";
export type Partner = {
id: string;
Expand Down
69 changes: 67 additions & 2 deletions apps/dashboard/src/@/api/team-subscription.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import type { ProductSKU } from "@/types/billing";
import type { ChainInfraSKU, ProductSKU } from "@/types/billing";

type InvoiceLine = {
// amount for this line item
Expand All @@ -22,7 +22,7 @@ type Invoice = {

export type TeamSubscription = {
id: string;
type: "PLAN" | "USAGE" | "PLAN_ADD_ON" | "PRODUCT";
type: "PLAN" | "USAGE" | "PLAN_ADD_ON" | "PRODUCT" | "CHAIN";
status:
| "incomplete"
| "incomplete_expired"
Expand All @@ -37,6 +37,13 @@ export type TeamSubscription = {
trialStart: string | null;
trialEnd: string | null;
upcomingInvoice: Invoice;
skus: (ProductSKU | ChainInfraSKU)[];
};

type ChainTeamSubscription = Omit<TeamSubscription, "skus"> & {
chainId: string;
skus: ChainInfraSKU[];
isLegacy: boolean;
};

export async function getTeamSubscriptions(slug: string) {
Expand All @@ -60,3 +67,61 @@ export async function getTeamSubscriptions(slug: string) {
}
return null;
}

const CHAIN_PLAN_TO_INFRA = {
"chain:plan:gold": ["chain:infra:rpc", "chain:infra:account_abstraction"],
"chain:plan:platinum": [
"chain:infra:rpc",
"chain:infra:insight",
"chain:infra:account_abstraction",
],
"chain:plan:ultimate": [
"chain:infra:rpc",
"chain:infra:insight",
"chain:infra:account_abstraction",
],
};

export async function getChainSubscriptions(slug: string) {
const allSubscriptions = await getTeamSubscriptions(slug);
if (!allSubscriptions) {
return null;
}

// first replace any sku that MIGHT match a chain plan
const updatedSubscriptions = allSubscriptions
.filter((s) => s.type === "CHAIN")
.map((s) => {
const skus = s.skus;
const updatedSkus = skus.flatMap((sku) => {
const plan =
CHAIN_PLAN_TO_INFRA[sku as keyof typeof CHAIN_PLAN_TO_INFRA];
return plan ? plan : sku;
});
return {
...s,
isLegacy: updatedSkus.length !== skus.length,
skus: updatedSkus,
};
});

return updatedSubscriptions.filter(
(s): s is ChainTeamSubscription =>
"chainId" in s && typeof s.chainId === "string",
);
}

export async function getChainSubscriptionForChain(
slug: string,
chainId: number,
) {
const chainSubscriptions = await getChainSubscriptions(slug);

if (!chainSubscriptions) {
return null;
}

return (
chainSubscriptions.find((s) => s.chainId === chainId.toString()) ?? null
);
}
12 changes: 11 additions & 1 deletion apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export function SingleNetworkSelector(props: {
disableChainId?: boolean;
align?: "center" | "start" | "end";
disableTestnets?: boolean;
disableDeprecated?: boolean;
placeholder?: string;
client: ThirdwebClient;
}) {
Expand All @@ -169,8 +170,17 @@ export function SingleNetworkSelector(props: {
chains = chains.filter((chain) => chainIdSet.has(chain.chainId));
}

if (props.disableDeprecated) {
chains = chains.filter((chain) => chain.status !== "deprecated");
}

return chains;
}, [allChains, props.chainIds, props.disableTestnets]);
}, [
allChains,
props.chainIds,
props.disableTestnets,
props.disableDeprecated,
]);

const options = useMemo(() => {
return chainsToShow.map((chain) => {
Expand Down
Loading
Loading