1+ import { createVaultClient , listEoas } from "@thirdweb-dev/vault-sdk" ;
12import {
23 ArrowLeftRightIcon ,
34 ChevronRightIcon ,
@@ -7,8 +8,10 @@ import {
78import Link from "next/link" ;
89import type { Project } from "@/api/project/projects" ;
910import { Alert , AlertDescription , AlertTitle } from "@/components/ui/alert" ;
11+ import { CopyTextButton } from "@/components/ui/CopyTextButton" ;
1012import { CodeServer } from "@/components/ui/code/code.server" ;
1113import { UnderlineLink } from "@/components/ui/UnderlineLink" ;
14+ import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs" ;
1215import { DotNetIcon } from "@/icons/brand-icons/DotNetIcon" ;
1316import { GithubIcon } from "@/icons/brand-icons/GithubIcon" ;
1417import { ReactIcon } from "@/icons/brand-icons/ReactIcon" ;
@@ -18,13 +21,31 @@ import { UnrealIcon } from "@/icons/brand-icons/UnrealIcon";
1821import { ContractIcon } from "@/icons/ContractIcon" ;
1922import { InsightIcon } from "@/icons/InsightIcon" ;
2023import { PayIcon } from "@/icons/PayIcon" ;
24+ import { WalletProductIcon } from "@/icons/WalletProductIcon" ;
25+ import { getProjectWalletLabel } from "@/lib/project-wallet" ;
2126import { ClientIDSection } from "./ClientIDSection" ;
2227import { IntegrateAPIKeyCodeTabs } from "./IntegrateAPIKeyCodeTabs" ;
2328import { SecretKeySection } from "./SecretKeySection" ;
2429
25- export function ProjectFTUX ( props : { project : Project ; teamSlug : string } ) {
30+ type ProjectWalletSummary = {
31+ id : string ;
32+ address : string ;
33+ label ?: string ;
34+ } ;
35+
36+ export async function ProjectFTUX ( props : {
37+ project : Project ;
38+ teamSlug : string ;
39+ } ) {
40+ const projectWallet = await fetchProjectWallet ( props . project ) ;
41+
2642 return (
2743 < div className = "flex flex-col gap-10" >
44+ < ProjectWalletSection
45+ project = { props . project }
46+ teamSlug = { props . teamSlug }
47+ wallet = { projectWallet }
48+ />
2849 < IntegrateAPIKeySection
2950 project = { props . project }
3051 teamSlug = { props . teamSlug }
@@ -39,6 +60,165 @@ export function ProjectFTUX(props: { project: Project; teamSlug: string }) {
3960 ) ;
4061}
4162
63+ function ProjectWalletSection ( props : {
64+ project : Project ;
65+ teamSlug : string ;
66+ wallet : ProjectWalletSummary | undefined ;
67+ } ) {
68+ const defaultLabel = getProjectWalletLabel ( props . project . name ) ;
69+ const walletAddress = props . wallet ?. address ;
70+ const label = props . wallet ?. label ?? defaultLabel ;
71+ const shortenedAddress = walletAddress
72+ ? `${ walletAddress . slice ( 0 , 6 ) } ...${ walletAddress . slice ( - 4 ) } `
73+ : undefined ;
74+
75+ return (
76+ < section >
77+ < h2 className = "mb-3 font-semibold text-xl tracking-tight" >
78+ Project Wallet
79+ </ h2 >
80+
81+ < div className = "rounded-lg border border-border bg-card p-4" >
82+ < div className = "flex flex-col gap-4" >
83+ < div className = "flex items-center gap-3" >
84+ < div className = "rounded-full border border-border bg-background p-2" >
85+ < WalletProductIcon className = "size-5 text-muted-foreground" />
86+ </ div >
87+ < div >
88+ < p className = "font-semibold text-lg tracking-tight" > { label } </ p >
89+ < p className = "text-muted-foreground text-sm" >
90+ Default managed server wallet for this project. Use it for
91+ deployments, payments, and other automated flows.
92+ </ p >
93+ </ div >
94+ </ div >
95+
96+ { walletAddress ? (
97+ < div className = "flex flex-col gap-3 rounded-lg border border-dashed border-border/60 bg-background p-3 md:flex-row md:items-center md:justify-between" >
98+ < div >
99+ < p className = "text-muted-foreground text-xs uppercase" >
100+ Wallet address
101+ </ p >
102+ < p className = "font-mono text-sm break-all" > { walletAddress } </ p >
103+ </ div >
104+ < CopyTextButton
105+ className = "w-full md:w-auto"
106+ copyIconPosition = "right"
107+ textToCopy = { walletAddress }
108+ textToShow = { shortenedAddress ?? walletAddress }
109+ tooltip = "Copy wallet address"
110+ />
111+ </ div >
112+ ) : (
113+ < Alert variant = "info" >
114+ < CircleAlertIcon className = "size-5" />
115+ < AlertTitle > Project wallet unavailable</ AlertTitle >
116+ < AlertDescription >
117+ We could not load the default wallet for this project. Visit the
118+ Transactions page to create or refresh your server wallets.
119+ </ AlertDescription >
120+ </ Alert >
121+ ) }
122+
123+ < div className = "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between" >
124+ < p className = "text-muted-foreground text-sm" >
125+ Manage balances, gas sponsorship, and smart account settings in
126+ Transactions.
127+ </ p >
128+ < Link
129+ className = "inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline"
130+ href = { `/team/${ props . teamSlug } /${ props . project . slug } /transactions` }
131+ >
132+ Open Transactions
133+ < ChevronRightIcon className = "size-4" />
134+ </ Link >
135+ </ div >
136+ </ div >
137+ </ div >
138+ </ section >
139+ ) ;
140+ }
141+
142+ type VaultWalletListItem = {
143+ id : string ;
144+ address : string ;
145+ metadata ?: {
146+ label ?: string ;
147+ projectId ?: string ;
148+ teamId ?: string ;
149+ type ?: string ;
150+ } ;
151+ } ;
152+
153+ async function fetchProjectWallet (
154+ project : Project ,
155+ ) : Promise < ProjectWalletSummary | undefined > {
156+ const engineCloudService = project . services . find (
157+ ( service ) => service . name === "engineCloud" ,
158+ ) ;
159+
160+ const managementAccessToken =
161+ engineCloudService ?. managementAccessToken || undefined ;
162+
163+ if ( ! managementAccessToken || ! NEXT_PUBLIC_THIRDWEB_VAULT_URL ) {
164+ return undefined ;
165+ }
166+
167+ try {
168+ const vaultClient = await createVaultClient ( {
169+ baseUrl : NEXT_PUBLIC_THIRDWEB_VAULT_URL ,
170+ } ) ;
171+
172+ const response = await listEoas ( {
173+ client : vaultClient ,
174+ request : {
175+ auth : {
176+ accessToken : managementAccessToken ,
177+ } ,
178+ options : {
179+ page : 0 ,
180+ // @ts -expect-error - SDK expects snake_case for pagination arguments
181+ page_size : 25 ,
182+ } ,
183+ } ,
184+ } ) ;
185+
186+ if ( ! response . success || ! response . data ) {
187+ return undefined ;
188+ }
189+
190+ const items = response . data . items as VaultWalletListItem [ ] | undefined ;
191+
192+ if ( ! items ?. length ) {
193+ return undefined ;
194+ }
195+
196+ const expectedLabel = getProjectWalletLabel ( project . name ) ;
197+
198+ const serverWallets = items . filter (
199+ ( item ) => item . metadata ?. projectId === project . id ,
200+ ) ;
201+
202+ const defaultWallet =
203+ serverWallets . find ( ( item ) => item . metadata ?. label === expectedLabel ) ||
204+ serverWallets . find ( ( item ) => item . metadata ?. type === "server-wallet" ) ||
205+ serverWallets [ 0 ] ;
206+
207+ if ( ! defaultWallet ) {
208+ return undefined ;
209+ }
210+
211+ return {
212+ id : defaultWallet . id ,
213+ address : defaultWallet . address ,
214+ label : defaultWallet . metadata ?. label ,
215+ } ;
216+ } catch ( error ) {
217+ console . error ( "Failed to load project wallet" , error ) ;
218+ return undefined ;
219+ }
220+ }
221+
42222// Integrate API key section ------------------------------------------------------------
43223
44224function IntegrateAPIKeySection ( {
0 commit comments