Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
bc0295487b Version Packages (#8339)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-30 07:07:28 +07:00
FirekeeperandGitHub 90b779107d Add Solana wallet balance endpoint and UI integration (#8338) 2025-10-30 06:56:00 +07:00
9 changed files with 261 additions and 99 deletions
@@ -40,6 +40,13 @@ import {
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
import {
@@ -57,6 +64,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";
@@ -79,6 +87,7 @@ interface ServerWalletsTableProps {
teamSlug: string;
client: ThirdwebClient;
solanaPermissionError?: boolean;
authToken: string;
}
export function ServerWalletsTable(props: ServerWalletsTableProps) {
@@ -95,10 +104,14 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
solanaTotalPages,
client,
solanaPermissionError,
authToken,
} = props;
const [activeChain, setActiveChain] = useState<WalletChain>("evm");
const [selectedChainId, setSelectedChainId] = useState<number>(1);
const [selectedSolanaChain, setSelectedSolanaChain] = useState<
"solana:mainnet" | "solana:devnet"
>("solana:mainnet");
const [showSmartAccount, setShowSmartAccount] = useState(false);
const queryClient = useQueryClient();
@@ -145,11 +158,33 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
</>
)}
{activeChain === "solana" && (
<CreateSolanaWallet
project={project}
teamSlug={teamSlug}
disabled={solanaPermissionError}
/>
<>
<CreateSolanaWallet
project={project}
teamSlug={teamSlug}
disabled={solanaPermissionError}
/>
<Select
value={selectedSolanaChain}
onValueChange={(value) =>
setSelectedSolanaChain(
value as "solana:mainnet" | "solana:devnet",
)
}
>
<SelectTrigger className="w-fit min-w-[180px] rounded-full bg-background hover:bg-accent/50">
<SelectValue placeholder="Select network" />
</SelectTrigger>
<SelectContent className="rounded-xl">
<SelectItem value="solana:mainnet" className="rounded-lg">
Solana Mainnet
</SelectItem>
<SelectItem value="solana:devnet" className="rounded-lg">
Solana Devnet
</SelectItem>
</SelectContent>
</Select>
</>
)}
</div>
@@ -278,6 +313,8 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
project={project}
teamSlug={teamSlug}
client={client}
authToken={authToken}
chainId={selectedSolanaChain}
/>
))}
</TableBody>
@@ -507,11 +544,15 @@ function SolanaWalletRow({
project,
teamSlug,
client,
authToken,
chainId,
}: {
wallet: SolanaWallet;
project: Project;
teamSlug: string;
client: ThirdwebClient;
authToken: string;
chainId: "solana:mainnet" | "solana:devnet";
}) {
const engineService = project.services.find(
(s) => s.name === "engineCloud",
@@ -547,7 +588,12 @@ function SolanaWalletRow({
</TableCell>
<TableCell>
<SolanaWalletBalance publicKey={wallet.publicKey} />
<SolanaWalletBalance
publicKey={wallet.publicKey}
authToken={authToken}
clientId={project.publishableKey}
chainId={chainId}
/>
</TableCell>
<TableCell>
@@ -739,16 +785,27 @@ function WalletBalance({
);
}
function SolanaWalletBalance({ publicKey }: { publicKey: string }) {
function SolanaWalletBalance({
publicKey,
authToken,
clientId,
chainId,
}: {
publicKey: string;
authToken: string;
clientId: string;
chainId: "solana:mainnet" | "solana:devnet";
}) {
const balance = useQuery({
queryFn: async () => {
// TODO: Implement actual Solana balance fetching
return {
displayValue: "0",
symbol: "SOL",
};
return await fetchSolanaBalance({
publicKey,
authToken,
clientId,
chainId,
});
},
queryKey: ["solanaWalletBalance", publicKey],
queryKey: ["solanaWalletBalance", publicKey, chainId],
});
if (balance.isFetching) {
@@ -0,0 +1,60 @@
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,
chainId = "solana:mainnet",
}: {
publicKey: string;
authToken: string;
clientId: string;
chainId?: "solana:mainnet" | "solana:devnet";
}): Promise<{
displayValue: string;
symbol: string;
value: string;
decimals: number;
} | null> {
try {
const response = await getSolanaWalletBalance({
path: {
address: publicKey,
},
query: {
chainId,
},
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;
}
}
@@ -215,6 +215,7 @@ export default async function TransactionsAnalyticsPage(props: {
solanaWallets={solanaAccounts.data.items}
teamSlug={params.team_slug}
solanaPermissionError={isSolanaPermissionError}
authToken={authToken}
/>
)}
</div>
@@ -70,8 +70,8 @@ export function CreateSolanaWallet(props: {
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="default"
className="gap-2 rounded-full bg-foreground hover:bg-foreground/90"
variant="outline"
className="gap-1.5 rounded-full bg-background text-foreground"
disabled={props.disabled}
>
<PlusIcon className="size-4" />
@@ -202,6 +202,7 @@ export default async function Page(props: {
solanaWallets={solanaAccounts.data.items}
teamSlug={params.team_slug}
solanaPermissionError={isSolanaPermissionError || false}
authToken={authToken}
/>
)}
</div>
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/api
## 0.1.2
### Patch Changes
- [#8338](https://github.com/thirdweb-dev/js/pull/8338) [`90b7791`](https://github.com/thirdweb-dev/js/commit/90b779107d3cb726adeca6a3f885129c12599c6a) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - added solana token balances endpoint
## 0.1.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/api",
"version": "0.1.1",
"version": "0.1.2",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
+28
View File
@@ -66,6 +66,9 @@ import type {
GetSolanaTransactionData,
GetSolanaTransactionErrors,
GetSolanaTransactionResponses,
GetSolanaWalletBalanceData,
GetSolanaWalletBalanceErrors,
GetSolanaWalletBalanceResponses,
GetTokenOwnersData,
GetTokenOwnersErrors,
GetTokenOwnersResponses,
@@ -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.
+92 -83
View File
@@ -2135,6 +2135,10 @@ export type GetWalletTransactionsData = {
address: string;
};
query: {
/**
* Chain ID(s) to request transaction data for. You can specify multiple chain IDs by repeating the parameter, up to a maximum of 50. Example: ?chainId=1&chainId=137
*/
chainId: Array<number>;
/**
* Filter by block timestamp (Unix timestamp) greater than or equal to this value
*/
@@ -2171,10 +2175,6 @@ export type GetWalletTransactionsData = {
* Sort order: 'asc' for ascending, 'desc' for descending
*/
sortOrder?: "asc" | "desc";
/**
* Chain ID(s) to request transaction data for. You can specify multiple chain IDs by repeating the parameter, up to a maximum of 50. Example: ?chainId=1&chainId=137
*/
chainId: Array<number>;
};
url: "/v1/wallets/{address}/transactions";
};
@@ -3234,11 +3234,11 @@ export type WriteContractErrors = {
/**
* Payment ID
*/
id: string;
id?: string;
/**
* Bridge quote for completing the payment
*/
quote: {
quote?: {
/**
* Block number when quote was generated
*/
@@ -4314,11 +4314,11 @@ export type SendTransactionsErrors = {
/**
* Payment ID
*/
id: string;
id?: string;
/**
* Bridge quote for completing the payment
*/
quote: {
quote?: {
/**
* Block number when quote was generated
*/
@@ -4725,11 +4725,11 @@ export type PaymentsPurchaseErrors = {
/**
* Payment ID
*/
id: string;
id?: string;
/**
* Bridge quote for completing the payment
*/
quote: {
quote?: {
/**
* Block number when quote was generated
*/
@@ -4914,17 +4914,7 @@ export type VerifyX402PaymentData = {
paymentPayload: {
x402Version: number;
scheme: "exact";
network:
| "base-sepolia"
| "base"
| "avalanche-fuji"
| "avalanche"
| "iotex"
| "solana-devnet"
| "solana"
| "sei"
| "sei-testnet"
| string;
network: string;
payload:
| {
signature: string;
@@ -4943,17 +4933,7 @@ export type VerifyX402PaymentData = {
};
paymentRequirements: {
scheme: "exact";
network:
| "base-sepolia"
| "base"
| "avalanche-fuji"
| "avalanche"
| "iotex"
| "solana-devnet"
| "solana"
| "sei"
| "sei-testnet"
| string;
network: string;
maxAmountRequired: string;
resource: string;
description: string;
@@ -5015,7 +4995,6 @@ export type VerifyX402PaymentResponses = {
| "invalid_exact_svm_payload_transaction_instruction_not_spl_token_transfer_checked"
| "invalid_exact_svm_payload_transaction_instruction_not_token_2022_transfer_checked"
| "invalid_exact_svm_payload_transaction_not_a_transfer_instruction"
| "invalid_exact_svm_payload_transaction_cannot_derive_receiver_ata"
| "invalid_exact_svm_payload_transaction_receiver_ata_not_found"
| "invalid_exact_svm_payload_transaction_sender_ata_not_found"
| "invalid_exact_svm_payload_transaction_simulation_failed"
@@ -5049,17 +5028,7 @@ export type SettleX402PaymentData = {
paymentPayload: {
x402Version: number;
scheme: "exact";
network:
| "base-sepolia"
| "base"
| "avalanche-fuji"
| "avalanche"
| "iotex"
| "solana-devnet"
| "solana"
| "sei"
| "sei-testnet"
| string;
network: string;
payload:
| {
signature: string;
@@ -5078,17 +5047,7 @@ export type SettleX402PaymentData = {
};
paymentRequirements: {
scheme: "exact";
network:
| "base-sepolia"
| "base"
| "avalanche-fuji"
| "avalanche"
| "iotex"
| "solana-devnet"
| "solana"
| "sei"
| "sei-testnet"
| string;
network: string;
maxAmountRequired: string;
resource: string;
description: string;
@@ -5154,7 +5113,6 @@ export type SettleX402PaymentResponses = {
| "invalid_exact_svm_payload_transaction_instruction_not_spl_token_transfer_checked"
| "invalid_exact_svm_payload_transaction_instruction_not_token_2022_transfer_checked"
| "invalid_exact_svm_payload_transaction_not_a_transfer_instruction"
| "invalid_exact_svm_payload_transaction_cannot_derive_receiver_ata"
| "invalid_exact_svm_payload_transaction_receiver_ata_not_found"
| "invalid_exact_svm_payload_transaction_sender_ata_not_found"
| "invalid_exact_svm_payload_transaction_simulation_failed"
@@ -5183,7 +5141,10 @@ export type SettleX402PaymentResponses = {
| "solana-devnet"
| "solana"
| "sei"
| "sei-testnet";
| "sei-testnet"
| "polygon"
| "polygon-amoy"
| "peaq";
errorMessage?: string;
};
};
@@ -5226,17 +5187,7 @@ export type SupportedX402PaymentsResponses = {
kinds: Array<{
x402Version: 1;
scheme: "exact";
network:
| "base-sepolia"
| "base"
| "avalanche-fuji"
| "avalanche"
| "iotex"
| "solana-devnet"
| "solana"
| "sei"
| "sei-testnet"
| string;
network: string;
extra?: {
defaultAsset?: {
/**
@@ -5340,11 +5291,11 @@ export type FetchWithPaymentErrors = {
/**
* Payment ID
*/
id: string;
id?: string;
/**
* Bridge quote for completing the payment
*/
quote: {
quote?: {
/**
* Block number when quote was generated
*/
@@ -5552,17 +5503,7 @@ export type ListPayableServicesResponses = {
x402Version: number;
accepts: Array<{
scheme: "exact";
network:
| "base-sepolia"
| "base"
| "avalanche-fuji"
| "avalanche"
| "iotex"
| "solana-devnet"
| "solana"
| "sei"
| "sei-testnet"
| string;
network: string;
maxAmountRequired: string;
resource: string;
description: string;
@@ -6298,11 +6239,11 @@ export type BridgeSwapErrors = {
/**
* Payment ID
*/
id: string;
id?: string;
/**
* Bridge quote for completing the payment
*/
quote: {
quote?: {
/**
* Block number when quote was generated
*/
@@ -6640,6 +6581,74 @@ export type CreateSolanaWalletResponses = {
export type CreateSolanaWalletResponse =
CreateSolanaWalletResponses[keyof CreateSolanaWalletResponses];
export type GetSolanaWalletBalanceData = {
body?: never;
path: {
/**
* Public key of the Solana wallet.
*/
address: string;
};
query: {
/**
* Solana network to query. Choose either solana:mainnet or solana:devnet.
*/
chainId: "solana:mainnet" | "solana:devnet";
/**
* SPL token mint address. Omit to retrieve native SOL balance.
*/
tokenAddress?: string;
};
url: "/v1/solana/wallets/{address}/balance";
};
export type GetSolanaWalletBalanceErrors = {
/**
* Invalid request parameters. This occurs when the wallet address or token mint is invalid, or the request mixes a token mint with multiple networks.
*/
400: unknown;
/**
* Authentication required. Include `x-client-id` or `x-secret-key` headers.
*/
401: unknown;
/**
* Internal server error. This may occur due to RPC connectivity issues or unexpected failures.
*/
500: unknown;
};
export type GetSolanaWalletBalanceResponses = {
/**
* Wallet balance retrieved successfully for the requested Solana network.
*/
200: {
/**
* Balance data for the requested Solana network.
*/
result: {
/**
* Requested Solana network.
*/
chainId: "solana:mainnet" | "solana:devnet";
/**
* Number of decimals used by the token.
*/
decimals: number;
/**
* Human-readable balance formatted using token decimals.
*/
displayValue: string;
/**
* Raw balance value expressed in base units (lamports).
*/
value: string;
};
};
};
export type GetSolanaWalletBalanceResponse =
GetSolanaWalletBalanceResponses[keyof GetSolanaWalletBalanceResponses];
export type SignSolanaMessageData = {
/**
* Request payload for signing an arbitrary Solana message.