Skip to content

Commit 90b7791

Browse files
authored
Add Solana wallet balance endpoint and UI integration (#8338)
1 parent afe187d commit 90b7791

File tree

8 files changed

+259
-98
lines changed

8 files changed

+259
-98
lines changed

.changeset/quiet-streets-lie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@thirdweb-dev/api": patch
3+
---
4+
5+
added solana token balances endpoint

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/components/server-wallets-table.client.tsx

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ import {
4040
PaginationNext,
4141
PaginationPrevious,
4242
} from "@/components/ui/pagination";
43+
import {
44+
Select,
45+
SelectContent,
46+
SelectItem,
47+
SelectTrigger,
48+
SelectValue,
49+
} from "@/components/ui/select";
4350
import { Skeleton } from "@/components/ui/skeleton";
4451
import { Switch } from "@/components/ui/switch";
4552
import {
@@ -57,6 +64,7 @@ import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
5764
import { WalletProductIcon } from "@/icons/WalletProductIcon";
5865
import { useDashboardRouter } from "@/lib/DashboardRouter";
5966
import { cn } from "@/lib/utils";
67+
import { fetchSolanaBalance } from "../lib/getSolanaBalance";
6068
import { updateDefaultProjectWallet } from "../lib/vault.client";
6169
import { CreateServerWallet } from "../server-wallets/components/create-server-wallet.client";
6270
import type { Wallet as EVMWallet } from "../server-wallets/wallet-table/types";
@@ -79,6 +87,7 @@ interface ServerWalletsTableProps {
7987
teamSlug: string;
8088
client: ThirdwebClient;
8189
solanaPermissionError?: boolean;
90+
authToken: string;
8291
}
8392

8493
export function ServerWalletsTable(props: ServerWalletsTableProps) {
@@ -95,10 +104,14 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
95104
solanaTotalPages,
96105
client,
97106
solanaPermissionError,
107+
authToken,
98108
} = props;
99109

100110
const [activeChain, setActiveChain] = useState<WalletChain>("evm");
101111
const [selectedChainId, setSelectedChainId] = useState<number>(1);
112+
const [selectedSolanaChain, setSelectedSolanaChain] = useState<
113+
"solana:mainnet" | "solana:devnet"
114+
>("solana:mainnet");
102115
const [showSmartAccount, setShowSmartAccount] = useState(false);
103116
const queryClient = useQueryClient();
104117

@@ -145,11 +158,33 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
145158
</>
146159
)}
147160
{activeChain === "solana" && (
148-
<CreateSolanaWallet
149-
project={project}
150-
teamSlug={teamSlug}
151-
disabled={solanaPermissionError}
152-
/>
161+
<>
162+
<CreateSolanaWallet
163+
project={project}
164+
teamSlug={teamSlug}
165+
disabled={solanaPermissionError}
166+
/>
167+
<Select
168+
value={selectedSolanaChain}
169+
onValueChange={(value) =>
170+
setSelectedSolanaChain(
171+
value as "solana:mainnet" | "solana:devnet",
172+
)
173+
}
174+
>
175+
<SelectTrigger className="w-fit min-w-[180px] rounded-full bg-background hover:bg-accent/50">
176+
<SelectValue placeholder="Select network" />
177+
</SelectTrigger>
178+
<SelectContent className="rounded-xl">
179+
<SelectItem value="solana:mainnet" className="rounded-lg">
180+
Solana Mainnet
181+
</SelectItem>
182+
<SelectItem value="solana:devnet" className="rounded-lg">
183+
Solana Devnet
184+
</SelectItem>
185+
</SelectContent>
186+
</Select>
187+
</>
153188
)}
154189
</div>
155190

@@ -278,6 +313,8 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
278313
project={project}
279314
teamSlug={teamSlug}
280315
client={client}
316+
authToken={authToken}
317+
chainId={selectedSolanaChain}
281318
/>
282319
))}
283320
</TableBody>
@@ -507,11 +544,15 @@ function SolanaWalletRow({
507544
project,
508545
teamSlug,
509546
client,
547+
authToken,
548+
chainId,
510549
}: {
511550
wallet: SolanaWallet;
512551
project: Project;
513552
teamSlug: string;
514553
client: ThirdwebClient;
554+
authToken: string;
555+
chainId: "solana:mainnet" | "solana:devnet";
515556
}) {
516557
const engineService = project.services.find(
517558
(s) => s.name === "engineCloud",
@@ -547,7 +588,12 @@ function SolanaWalletRow({
547588
</TableCell>
548589

549590
<TableCell>
550-
<SolanaWalletBalance publicKey={wallet.publicKey} />
591+
<SolanaWalletBalance
592+
publicKey={wallet.publicKey}
593+
authToken={authToken}
594+
clientId={project.publishableKey}
595+
chainId={chainId}
596+
/>
551597
</TableCell>
552598

553599
<TableCell>
@@ -739,16 +785,27 @@ function WalletBalance({
739785
);
740786
}
741787

742-
function SolanaWalletBalance({ publicKey }: { publicKey: string }) {
788+
function SolanaWalletBalance({
789+
publicKey,
790+
authToken,
791+
clientId,
792+
chainId,
793+
}: {
794+
publicKey: string;
795+
authToken: string;
796+
clientId: string;
797+
chainId: "solana:mainnet" | "solana:devnet";
798+
}) {
743799
const balance = useQuery({
744800
queryFn: async () => {
745-
// TODO: Implement actual Solana balance fetching
746-
return {
747-
displayValue: "0",
748-
symbol: "SOL",
749-
};
801+
return await fetchSolanaBalance({
802+
publicKey,
803+
authToken,
804+
clientId,
805+
chainId,
806+
});
750807
},
751-
queryKey: ["solanaWalletBalance", publicKey],
808+
queryKey: ["solanaWalletBalance", publicKey, chainId],
752809
});
753810

754811
if (balance.isFetching) {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { configure, getSolanaWalletBalance } from "@thirdweb-dev/api";
2+
import { THIRDWEB_API_HOST } from "@/constants/urls";
3+
4+
// Configure the API client to use the correct base URL
5+
configure({
6+
override: {
7+
baseUrl: THIRDWEB_API_HOST,
8+
},
9+
});
10+
11+
export async function fetchSolanaBalance({
12+
publicKey,
13+
authToken,
14+
clientId,
15+
chainId = "solana:mainnet",
16+
}: {
17+
publicKey: string;
18+
authToken: string;
19+
clientId: string;
20+
chainId?: "solana:mainnet" | "solana:devnet";
21+
}): Promise<{
22+
displayValue: string;
23+
symbol: string;
24+
value: string;
25+
decimals: number;
26+
} | null> {
27+
try {
28+
const response = await getSolanaWalletBalance({
29+
path: {
30+
address: publicKey,
31+
},
32+
query: {
33+
chainId,
34+
},
35+
headers: {
36+
Authorization: `Bearer ${authToken}`,
37+
"Content-Type": "application/json",
38+
"x-client-id": clientId,
39+
},
40+
});
41+
42+
if (response.error || !response.data) {
43+
console.error(
44+
"Error fetching Solana balance:",
45+
response.error || "No data returned",
46+
);
47+
return null;
48+
}
49+
50+
return {
51+
displayValue: response.data.result.displayValue,
52+
symbol: "SOL",
53+
value: response.data.result.value,
54+
decimals: response.data.result.decimals,
55+
};
56+
} catch (error) {
57+
console.error("Error fetching Solana balance:", error);
58+
return null;
59+
}
60+
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ export default async function TransactionsAnalyticsPage(props: {
215215
solanaWallets={solanaAccounts.data.items}
216216
teamSlug={params.team_slug}
217217
solanaPermissionError={isSolanaPermissionError}
218+
authToken={authToken}
218219
/>
219220
)}
220221
</div>

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/solana-wallets/components/create-solana-wallet.client.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ export function CreateSolanaWallet(props: {
7070
<Dialog open={open} onOpenChange={setOpen}>
7171
<DialogTrigger asChild>
7272
<Button
73-
variant="default"
74-
className="gap-2 rounded-full bg-foreground hover:bg-foreground/90"
73+
variant="outline"
74+
className="gap-1.5 rounded-full bg-background text-foreground"
7575
disabled={props.disabled}
7676
>
7777
<PlusIcon className="size-4" />

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ export default async function Page(props: {
202202
solanaWallets={solanaAccounts.data.items}
203203
teamSlug={params.team_slug}
204204
solanaPermissionError={isSolanaPermissionError || false}
205+
authToken={authToken}
205206
/>
206207
)}
207208
</div>

packages/api/src/client/sdk.gen.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ import type {
6666
GetSolanaTransactionData,
6767
GetSolanaTransactionErrors,
6868
GetSolanaTransactionResponses,
69+
GetSolanaWalletBalanceData,
70+
GetSolanaWalletBalanceErrors,
71+
GetSolanaWalletBalanceResponses,
6972
GetTokenOwnersData,
7073
GetTokenOwnersErrors,
7174
GetTokenOwnersResponses,
@@ -1590,6 +1593,31 @@ export const createSolanaWallet = <ThrowOnError extends boolean = false>(
15901593
});
15911594
};
15921595

1596+
/**
1597+
* Get Solana Wallet Balance
1598+
* Get the SOL or SPL token balance for a Solana wallet on a specific Solana network.
1599+
*
1600+
* **Authentication**: Pass `x-client-id` for frontend usage from allowlisted origins or `x-secret-key` for backend usage.
1601+
*/
1602+
export const getSolanaWalletBalance = <ThrowOnError extends boolean = false>(
1603+
options: Options<GetSolanaWalletBalanceData, ThrowOnError>,
1604+
) => {
1605+
return (options.client ?? _heyApiClient).get<
1606+
GetSolanaWalletBalanceResponses,
1607+
GetSolanaWalletBalanceErrors,
1608+
ThrowOnError
1609+
>({
1610+
security: [
1611+
{
1612+
name: "x-client-id",
1613+
type: "apiKey",
1614+
},
1615+
],
1616+
url: "/v1/solana/wallets/{address}/balance",
1617+
...options,
1618+
});
1619+
};
1620+
15931621
/**
15941622
* Sign Solana Message
15951623
* Sign an arbitrary message with a Solana wallet. Supports both text and hexadecimal message formats with automatic format detection.

0 commit comments

Comments
 (0)