Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .changeset/quiet-streets-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@thirdweb-dev/api": patch
---

added solana token balances endpoint
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
import { WalletProductIcon } from "@/icons/WalletProductIcon";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
import { fetchSolanaBalance } from "../lib/getSolanaBalance";
import { updateDefaultProjectWallet } from "../lib/vault.client";
import { CreateServerWallet } from "../server-wallets/components/create-server-wallet.client";
import type { Wallet as EVMWallet } from "../server-wallets/wallet-table/types";
Expand All @@ -79,6 +80,7 @@ interface ServerWalletsTableProps {
teamSlug: string;
client: ThirdwebClient;
solanaPermissionError?: boolean;
authToken: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Auth token exposed to browser violates security guidelines.

The authToken is being passed through client component props and used directly in client-side API calls. This exposes the JWT to the browser, which violates the security guideline: "Keep tokens secret via internal routes or server actions."

Recommended architecture:

Create an internal API route that fetches Solana balance server-side:

// app/api/solana-balance/route.ts
import "server-only";
import { getAuthToken } from "@/lib/auth";
import { fetchSolanaBalance } from "...";

export async function GET(request: Request): Promise<Response> {
  const authToken = await getAuthToken();
  const { searchParams } = new URL(request.url);
  const publicKey = searchParams.get("publicKey");
  const clientId = searchParams.get("clientId");
  const chainId = searchParams.get("chainId") as "solana:mainnet" | "solana:devnet";
  
  const balance = await fetchSolanaBalance({ 
    publicKey, 
    authToken, 
    clientId, 
    chainId 
  });
  
  return Response.json(balance);
}

Then update the client component to call the internal route:

 function SolanaWalletBalance({
   publicKey,
-  authToken,
   clientId,
   chainId,
 }: {
   publicKey: string;
-  authToken: string;
   clientId: string;
   chainId: "solana:mainnet" | "solana:devnet";
 }) {
   const balance = useQuery({
     queryFn: async () => {
-      return await fetchSolanaBalance({
-        publicKey,
-        authToken,
-        clientId,
-        chainId,
-      });
+      const params = new URLSearchParams({ publicKey, clientId, chainId });
+      const response = await fetch(`/api/solana-balance?${params}`);
+      if (!response.ok) throw new Error("Failed to fetch balance");
+      return response.json();
     },
     queryKey: ["solanaWalletBalance", publicKey, chainId],
   });

As per coding guidelines.

Also applies to: 107-107, 316-317, 547-548, 554-555, 591-596, 801-806

🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/components/server-wallets-table.client.tsx
around lines 90, 107, 316-317, 547-548, 554-555, 591-596 and 801-806 the
authToken is passed into a client component and used for browser-side API calls
which exposes JWTs; remove authToken from client props and all client-side
usages, implement a server-only internal API route (or server action) that
obtains the token via getAuthToken/server-only and calls fetchSolanaBalance
(accepting publicKey, clientId, chainId as query params), return JSON from that
route, then update the client component to call this internal route with fetch
(no token in the browser) and consume the returned balance; update imports/types
and any call sites accordingly to ensure no JWT is ever sent to the client.

}

export function ServerWalletsTable(props: ServerWalletsTableProps) {
Expand All @@ -95,6 +97,7 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
solanaTotalPages,
client,
solanaPermissionError,
authToken,
} = props;

const [activeChain, setActiveChain] = useState<WalletChain>("evm");
Expand Down Expand Up @@ -278,6 +281,7 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
project={project}
teamSlug={teamSlug}
client={client}
authToken={authToken}
/>
))}
</TableBody>
Expand Down Expand Up @@ -507,11 +511,13 @@ function SolanaWalletRow({
project,
teamSlug,
client,
authToken,
}: {
wallet: SolanaWallet;
project: Project;
teamSlug: string;
client: ThirdwebClient;
authToken: string;
}) {
const engineService = project.services.find(
(s) => s.name === "engineCloud",
Expand Down Expand Up @@ -547,7 +553,11 @@ function SolanaWalletRow({
</TableCell>

<TableCell>
<SolanaWalletBalance publicKey={wallet.publicKey} />
<SolanaWalletBalance
publicKey={wallet.publicKey}
authToken={authToken}
clientId={project.publishableKey}
/>
</TableCell>

<TableCell>
Expand Down Expand Up @@ -739,14 +749,22 @@ function WalletBalance({
);
}

function SolanaWalletBalance({ publicKey }: { publicKey: string }) {
function SolanaWalletBalance({
publicKey,
authToken,
clientId,
}: {
publicKey: string;
authToken: string;
clientId: string;
}) {
const balance = useQuery({
queryFn: async () => {
// TODO: Implement actual Solana balance fetching
return {
displayValue: "0",
symbol: "SOL",
};
return await fetchSolanaBalance({
publicKey,
authToken,
clientId,
});
},
queryKey: ["solanaWalletBalance", publicKey],
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { configure, getSolanaWalletBalance } from "@thirdweb-dev/api";
import { THIRDWEB_API_HOST } from "@/constants/urls";

// Configure the API client to use the correct base URL
configure({
override: {
baseUrl: THIRDWEB_API_HOST,
},
});

export async function fetchSolanaBalance({
publicKey,
authToken,
clientId,
}: {
publicKey: string;
authToken: string;
clientId: string;
}): Promise<{
displayValue: string;
symbol: string;
value: string;
decimals: number;
} | null> {
try {
const response = await getSolanaWalletBalance({
path: {
address: publicKey,
},
query: {
chainId: "solana:mainnet",
},
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": clientId,
},
});

if (response.error || !response.data) {
console.error(
"Error fetching Solana balance:",
response.error || "No data returned",
);
return null;
}

return {
displayValue: response.data.result.displayValue,
symbol: "SOL",
value: response.data.result.value,
decimals: response.data.result.decimals,
};
} catch (error) {
console.error("Error fetching Solana balance:", error);
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ export default async function TransactionsAnalyticsPage(props: {
solanaWallets={solanaAccounts.data.items}
teamSlug={params.team_slug}
solanaPermissionError={isSolanaPermissionError}
authToken={authToken}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export default async function Page(props: {
solanaWallets={solanaAccounts.data.items}
teamSlug={params.team_slug}
solanaPermissionError={isSolanaPermissionError || false}
authToken={authToken}
/>
)}
</div>
Expand Down
28 changes: 28 additions & 0 deletions packages/api/src/client/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ import type {
GetSolanaTransactionData,
GetSolanaTransactionErrors,
GetSolanaTransactionResponses,
GetSolanaWalletBalanceData,
GetSolanaWalletBalanceErrors,
GetSolanaWalletBalanceResponses,
GetTokenOwnersData,
GetTokenOwnersErrors,
GetTokenOwnersResponses,
Expand Down Expand Up @@ -1590,6 +1593,31 @@ export const createSolanaWallet = <ThrowOnError extends boolean = false>(
});
};

/**
* Get Solana Wallet Balance
* Get the SOL or SPL token balance for a Solana wallet on a specific Solana network.
*
* **Authentication**: Pass `x-client-id` for frontend usage from allowlisted origins or `x-secret-key` for backend usage.
*/
export const getSolanaWalletBalance = <ThrowOnError extends boolean = false>(
options: Options<GetSolanaWalletBalanceData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetSolanaWalletBalanceResponses,
GetSolanaWalletBalanceErrors,
ThrowOnError
>({
security: [
{
name: "x-client-id",
type: "apiKey",
},
],
url: "/v1/solana/wallets/{address}/balance",
...options,
});
};

/**
* Sign Solana Message
* Sign an arbitrary message with a Solana wallet. Supports both text and hexadecimal message formats with automatic format detection.
Expand Down
Loading
Loading