Skip to content

Commit c11fe95

Browse files
[Dashboard] Add x402 fee configuration (#8434)
1 parent 851fee1 commit c11fe95

File tree

5 files changed

+241
-13
lines changed

5 files changed

+241
-13
lines changed

.changeset/tidy-paws-feel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@thirdweb-dev/service-utils": patch
3+
---
4+
5+
Update engineCloud service config
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Project } from "@/api/project/projects";
2+
3+
type X402Fee = {
4+
feeRecipient: string;
5+
feeBps: number;
6+
};
7+
8+
/**
9+
* Extract x402 fee configuration from project's engineCloud service
10+
*/
11+
export function getX402Fees(project: Project): X402Fee {
12+
const engineCloudService = project.services.find(
13+
(service) => service.name === "engineCloud",
14+
);
15+
16+
if (!engineCloudService) {
17+
return {
18+
feeRecipient: "",
19+
feeBps: 0,
20+
};
21+
}
22+
23+
return {
24+
feeRecipient: engineCloudService.x402FeeRecipient || "",
25+
feeBps: engineCloudService.x402FeeBPS || 0,
26+
};
27+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
"use client";
2+
3+
import { zodResolver } from "@hookform/resolvers/zod";
4+
import { useMutation } from "@tanstack/react-query";
5+
import { useForm } from "react-hook-form";
6+
import { toast } from "sonner";
7+
import type { Project } from "@/api/project/projects";
8+
import { SettingsCard } from "@/components/blocks/SettingsCard";
9+
import { Button } from "@/components/ui/button";
10+
import {
11+
Form,
12+
FormControl,
13+
FormField,
14+
FormItem,
15+
FormLabel,
16+
} from "@/components/ui/form";
17+
import { Input } from "@/components/ui/input";
18+
import { updateProjectClient } from "@/hooks/useApi";
19+
import {
20+
type ApiKeyPayConfigValidationSchema,
21+
apiKeyPayConfigValidationSchema,
22+
} from "@/schema/validations";
23+
24+
interface X402FeeConfigProps {
25+
project: Project;
26+
fees: {
27+
feeRecipient: string;
28+
feeBps: number;
29+
};
30+
projectWalletAddress?: string;
31+
}
32+
33+
export const X402FeeConfig: React.FC<X402FeeConfigProps> = (props) => {
34+
const form = useForm<ApiKeyPayConfigValidationSchema>({
35+
resolver: zodResolver(apiKeyPayConfigValidationSchema),
36+
values: {
37+
developerFeeBPS: props.fees.feeBps ? props.fees.feeBps / 100 : 0,
38+
payoutAddress: props.fees.feeRecipient ?? "",
39+
},
40+
});
41+
42+
const updateFeeMutation = useMutation({
43+
mutationFn: async (values: {
44+
payoutAddress: string;
45+
developerFeeBPS: number;
46+
}) => {
47+
// Find and update the engineCloud service
48+
const newServices = props.project.services.map((service) => {
49+
if (service.name === "engineCloud") {
50+
return {
51+
...service,
52+
x402FeeBPS: values.developerFeeBPS,
53+
x402FeeRecipient: values.payoutAddress,
54+
};
55+
}
56+
return service;
57+
});
58+
59+
// Update the project with the new services configuration
60+
await updateProjectClient(
61+
{
62+
projectId: props.project.id,
63+
teamId: props.project.teamId,
64+
},
65+
{
66+
services: newServices,
67+
},
68+
);
69+
},
70+
});
71+
72+
const handleSubmit = form.handleSubmit(
73+
({ payoutAddress, developerFeeBPS }) => {
74+
updateFeeMutation.mutate(
75+
{
76+
developerFeeBPS: developerFeeBPS ? developerFeeBPS * 100 : 0,
77+
payoutAddress,
78+
},
79+
{
80+
onError: (err) => {
81+
toast.error("Failed to update fee configuration");
82+
console.error(err);
83+
},
84+
onSuccess: () => {
85+
toast.success("Fee configuration updated");
86+
},
87+
},
88+
);
89+
},
90+
(errors) => {
91+
console.log(errors);
92+
},
93+
);
94+
95+
return (
96+
<Form {...form}>
97+
<form autoComplete="off" onSubmit={handleSubmit}>
98+
<SettingsCard
99+
bottomText="Fees are sent to recipient address"
100+
errorText={form.getFieldState("payoutAddress").error?.message}
101+
noPermissionText={undefined}
102+
saveButton={{
103+
disabled: !form.formState.isDirty,
104+
isPending: updateFeeMutation.isPending,
105+
type: "submit",
106+
}}
107+
>
108+
<div>
109+
<h3 className="font-semibold text-xl tracking-tight">
110+
Fee Sharing
111+
</h3>
112+
<p className="mt-1.5 mb-4 text-foreground text-sm">
113+
thirdweb collects a 0.3% service fee on x402 transactions. You may
114+
set your own developer fee in addition to this fee.
115+
</p>
116+
117+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
118+
<FormField
119+
control={form.control}
120+
name="payoutAddress"
121+
render={({ field }) => (
122+
<FormItem>
123+
<FormLabel>Recipient address</FormLabel>
124+
<FormControl>
125+
<div className="flex flex-col gap-2 sm:flex-row">
126+
<Input
127+
{...field}
128+
className="sm:flex-1"
129+
placeholder="0x..."
130+
/>
131+
{props.projectWalletAddress && (
132+
<Button
133+
onClick={() => {
134+
if (!props.projectWalletAddress) {
135+
return;
136+
}
137+
138+
form.setValue(
139+
"payoutAddress",
140+
props.projectWalletAddress,
141+
{
142+
shouldDirty: true,
143+
shouldTouch: true,
144+
shouldValidate: true,
145+
},
146+
);
147+
}}
148+
size="sm"
149+
type="button"
150+
variant="outline"
151+
>
152+
Use Project Wallet
153+
</Button>
154+
)}
155+
</div>
156+
</FormControl>
157+
</FormItem>
158+
)}
159+
/>
160+
<FormField
161+
control={form.control}
162+
name="developerFeeBPS"
163+
render={({ field }) => (
164+
<FormItem>
165+
<FormLabel>Fee amount</FormLabel>
166+
<FormControl>
167+
<div className="flex items-center gap-2">
168+
<Input {...field} placeholder="0.5" type="number" />
169+
<span className="text-muted-foreground text-sm">%</span>
170+
</div>
171+
</FormControl>
172+
</FormItem>
173+
)}
174+
/>
175+
</div>
176+
</div>
177+
</SettingsCard>
178+
</form>
179+
</Form>
180+
);
181+
};
Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,46 @@
11
import { redirect } from "next/navigation";
22
import { getAuthToken } from "@/api/auth-token";
33
import { getProject } from "@/api/project/projects";
4+
import { getTeamBySlug } from "@/api/team/get-team";
5+
import { getX402Fees } from "@/api/x402/config";
6+
import { getProjectWallet } from "@/lib/server/project-wallet";
47
import { loginRedirect } from "@/utils/redirects";
8+
import { X402FeeConfig } from "./X402FeeConfig";
59

610
export default async function Page(props: {
711
params: Promise<{ team_slug: string; project_slug: string }>;
812
}) {
9-
const params = await props.params;
13+
const { team_slug, project_slug } = await props.params;
14+
15+
const [team, project, authToken] = await Promise.all([
16+
getTeamBySlug(team_slug),
17+
getProject(team_slug, project_slug),
18+
getAuthToken(),
19+
]);
1020

11-
const authToken = await getAuthToken();
1221
if (!authToken) {
13-
loginRedirect(
14-
`/team/${params.team_slug}/${params.project_slug}/x402/configuration`,
15-
);
22+
loginRedirect(`/team/${team_slug}/${project_slug}/x402/configuration`);
23+
}
24+
25+
if (!team) {
26+
redirect("/team");
1627
}
1728

18-
const project = await getProject(params.team_slug, params.project_slug);
1929
if (!project) {
20-
redirect(`/team/${params.team_slug}`);
30+
redirect(`/team/${team_slug}`);
2131
}
2232

33+
const projectWallet = await getProjectWallet(project);
34+
35+
const fees = getX402Fees(project);
36+
2337
return (
2438
<div className="flex flex-col gap-6">
25-
<div className="rounded-lg border border-border bg-card p-8 text-center">
26-
<h2 className="text-2xl font-semibold">Coming Soon</h2>
27-
<p className="mt-2 text-muted-foreground">
28-
x402 payments configuration will be available soon.
29-
</p>
30-
</div>
39+
<X402FeeConfig
40+
fees={fees}
41+
project={project}
42+
projectWalletAddress={projectWallet?.address}
43+
/>
3144
</div>
3245
);
3346
}

packages/service-utils/src/core/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ export type ProjectService =
242242
encryptedAdminKey?: string | null;
243243
encryptedWalletAccessToken?: string | null;
244244
projectWalletAddress?: string | null;
245+
x402FeeBPS?: number | null;
246+
x402FeeRecipient?: string | null;
245247
}
246248
| ProjectBundlerService
247249
| ProjectEmbeddedWalletsService;

0 commit comments

Comments
 (0)