Skip to content

Commit 0e159f7

Browse files
authored
[Dashboard] Add chain infrastructure deployment and management (#7456)
1 parent f1a965b commit 0e159f7

File tree

55 files changed

+1461
-262
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1461
-262
lines changed

apps/dashboard/src/@/actions/billing.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"use server";
2+
import "server-only";
23

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

69
export async function reSubscribePlan(options: {
710
teamId: string;
@@ -14,7 +17,10 @@ export async function reSubscribePlan(options: {
1417
}
1518

1619
const res = await fetch(
17-
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/checkout/resubscribe-plan`,
20+
new URL(
21+
`/v1/teams/${options.teamId}/checkout/resubscribe-plan`,
22+
NEXT_PUBLIC_THIRDWEB_API_HOST,
23+
),
1824
{
1925
body: JSON.stringify({}),
2026
headers: {
@@ -35,3 +41,83 @@ export async function reSubscribePlan(options: {
3541
status: 200,
3642
};
3743
}
44+
45+
export async function getChainInfraCheckoutURL(options: {
46+
teamSlug: string;
47+
skus: ChainInfraSKU[];
48+
chainId: number;
49+
annual: boolean;
50+
}) {
51+
const token = await getAuthToken();
52+
53+
if (!token) {
54+
return {
55+
error: "You are not logged in",
56+
status: "error",
57+
} as const;
58+
}
59+
60+
const res = await fetch(
61+
new URL(
62+
`/v1/teams/${options.teamSlug}/checkout/create-link`,
63+
NEXT_PUBLIC_THIRDWEB_API_HOST,
64+
),
65+
{
66+
body: JSON.stringify({
67+
annual: options.annual,
68+
baseUrl: getAbsoluteUrl(),
69+
chainId: options.chainId,
70+
skus: options.skus,
71+
}),
72+
headers: {
73+
Authorization: `Bearer ${token}`,
74+
"Content-Type": "application/json",
75+
},
76+
method: "POST",
77+
},
78+
);
79+
if (!res.ok) {
80+
const text = await res.text();
81+
console.error("Failed to create checkout link", text, res.status);
82+
switch (res.status) {
83+
case 402: {
84+
return {
85+
error:
86+
"You have outstanding invoices, please pay these first before re-subscribing.",
87+
status: "error",
88+
} as const;
89+
}
90+
case 429: {
91+
return {
92+
error: "Too many requests, please try again later.",
93+
status: "error",
94+
} as const;
95+
}
96+
case 403: {
97+
return {
98+
error: "You are not authorized to deploy infrastructure.",
99+
status: "error",
100+
} as const;
101+
}
102+
default: {
103+
return {
104+
error: "An unknown error occurred, please try again later.",
105+
status: "error",
106+
} as const;
107+
}
108+
}
109+
}
110+
111+
const json = await res.json();
112+
if (!json.result) {
113+
return {
114+
error: "An unknown error occurred, please try again later.",
115+
status: "error",
116+
} as const;
117+
}
118+
119+
return {
120+
data: json.result as string,
121+
status: "success",
122+
} as const;
123+
}
Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,34 @@
1-
export const authOptions = [
2-
"email",
3-
"phone",
4-
"passkey",
5-
"siwe",
6-
"guest",
7-
"google",
8-
"facebook",
9-
"x",
10-
"discord",
11-
"farcaster",
12-
"telegram",
13-
"github",
14-
"twitch",
15-
"steam",
16-
"apple",
17-
"coinbase",
18-
"line",
19-
] as const;
1+
import "server-only";
2+
3+
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
4+
import { getAuthToken } from "./auth-token";
5+
6+
export type AuthOption =
7+
| "email"
8+
| "phone"
9+
| "passkey"
10+
| "siwe"
11+
| "guest"
12+
| "google"
13+
| "facebook"
14+
| "x"
15+
| "discord"
16+
| "farcaster"
17+
| "telegram"
18+
| "github"
19+
| "twitch"
20+
| "steam"
21+
| "apple"
22+
| "coinbase"
23+
| "line";
2024

2125
export type Ecosystem = {
2226
name: string;
2327
imageUrl?: string;
2428
id: string;
2529
slug: string;
2630
permission: "PARTNER_WHITELIST" | "ANYONE";
27-
authOptions: (typeof authOptions)[number][];
31+
authOptions: AuthOption[];
2832
customAuthOptions?: {
2933
authEndpoint?: {
3034
url: string;
@@ -47,6 +51,54 @@ export type Ecosystem = {
4751
updatedAt: string;
4852
};
4953

54+
export async function fetchEcosystemList(teamIdOrSlug: string) {
55+
const token = await getAuthToken();
56+
57+
if (!token) {
58+
return [];
59+
}
60+
61+
const res = await fetch(
62+
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/ecosystem-wallet`,
63+
{
64+
headers: {
65+
Authorization: `Bearer ${token}`,
66+
},
67+
},
68+
);
69+
70+
if (!res.ok) {
71+
return [];
72+
}
73+
74+
return (await res.json()).result as Ecosystem[];
75+
}
76+
77+
export async function fetchEcosystem(slug: string, teamIdOrSlug: string) {
78+
const token = await getAuthToken();
79+
80+
if (!token) {
81+
return null;
82+
}
83+
84+
const res = await fetch(
85+
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/ecosystem-wallet/${slug}`,
86+
{
87+
headers: {
88+
Authorization: `Bearer ${token}`,
89+
},
90+
},
91+
);
92+
if (!res.ok) {
93+
const data = await res.json();
94+
console.error(data);
95+
return null;
96+
}
97+
98+
const data = (await res.json()) as { result: Ecosystem };
99+
return data.result;
100+
}
101+
50102
type PartnerPermission = "PROMPT_USER_V1" | "FULL_CONTROL_V1";
51103
export type Partner = {
52104
id: string;

apps/dashboard/src/@/api/team-subscription.ts

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getAuthToken } from "@/api/auth-token";
22
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
3-
import type { ProductSKU } from "@/types/billing";
3+
import type { ChainInfraSKU, ProductSKU } from "@/types/billing";
44

55
type InvoiceLine = {
66
// amount for this line item
@@ -22,7 +22,7 @@ type Invoice = {
2222

2323
export type TeamSubscription = {
2424
id: string;
25-
type: "PLAN" | "USAGE" | "PLAN_ADD_ON" | "PRODUCT";
25+
type: "PLAN" | "USAGE" | "PLAN_ADD_ON" | "PRODUCT" | "CHAIN";
2626
status:
2727
| "incomplete"
2828
| "incomplete_expired"
@@ -37,6 +37,13 @@ export type TeamSubscription = {
3737
trialStart: string | null;
3838
trialEnd: string | null;
3939
upcomingInvoice: Invoice;
40+
skus: (ProductSKU | ChainInfraSKU)[];
41+
};
42+
43+
type ChainTeamSubscription = Omit<TeamSubscription, "skus"> & {
44+
chainId: string;
45+
skus: ChainInfraSKU[];
46+
isLegacy: boolean;
4047
};
4148

4249
export async function getTeamSubscriptions(slug: string) {
@@ -60,3 +67,61 @@ export async function getTeamSubscriptions(slug: string) {
6067
}
6168
return null;
6269
}
70+
71+
const CHAIN_PLAN_TO_INFRA = {
72+
"chain:plan:gold": ["chain:infra:rpc", "chain:infra:account_abstraction"],
73+
"chain:plan:platinum": [
74+
"chain:infra:rpc",
75+
"chain:infra:insight",
76+
"chain:infra:account_abstraction",
77+
],
78+
"chain:plan:ultimate": [
79+
"chain:infra:rpc",
80+
"chain:infra:insight",
81+
"chain:infra:account_abstraction",
82+
],
83+
};
84+
85+
export async function getChainSubscriptions(slug: string) {
86+
const allSubscriptions = await getTeamSubscriptions(slug);
87+
if (!allSubscriptions) {
88+
return null;
89+
}
90+
91+
// first replace any sku that MIGHT match a chain plan
92+
const updatedSubscriptions = allSubscriptions
93+
.filter((s) => s.type === "CHAIN")
94+
.map((s) => {
95+
const skus = s.skus;
96+
const updatedSkus = skus.flatMap((sku) => {
97+
const plan =
98+
CHAIN_PLAN_TO_INFRA[sku as keyof typeof CHAIN_PLAN_TO_INFRA];
99+
return plan ? plan : sku;
100+
});
101+
return {
102+
...s,
103+
isLegacy: updatedSkus.length !== skus.length,
104+
skus: updatedSkus,
105+
};
106+
});
107+
108+
return updatedSubscriptions.filter(
109+
(s): s is ChainTeamSubscription =>
110+
"chainId" in s && typeof s.chainId === "string",
111+
);
112+
}
113+
114+
export async function getChainSubscriptionForChain(
115+
slug: string,
116+
chainId: number,
117+
) {
118+
const chainSubscriptions = await getChainSubscriptions(slug);
119+
120+
if (!chainSubscriptions) {
121+
return null;
122+
}
123+
124+
return (
125+
chainSubscriptions.find((s) => s.chainId === chainId.toString()) ?? null
126+
);
127+
}

apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export function SingleNetworkSelector(props: {
152152
disableChainId?: boolean;
153153
align?: "center" | "start" | "end";
154154
disableTestnets?: boolean;
155+
disableDeprecated?: boolean;
155156
placeholder?: string;
156157
client: ThirdwebClient;
157158
}) {
@@ -169,8 +170,17 @@ export function SingleNetworkSelector(props: {
169170
chains = chains.filter((chain) => chainIdSet.has(chain.chainId));
170171
}
171172

173+
if (props.disableDeprecated) {
174+
chains = chains.filter((chain) => chain.status !== "deprecated");
175+
}
176+
172177
return chains;
173-
}, [allChains, props.chainIds, props.disableTestnets]);
178+
}, [
179+
allChains,
180+
props.chainIds,
181+
props.disableTestnets,
182+
props.disableDeprecated,
183+
]);
174184

175185
const options = useMemo(() => {
176186
return chainsToShow.map((chain) => {

0 commit comments

Comments
 (0)