+
Back to Transactions
-
{children}
+
{children}
);
}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/page.tsx
index 536a350e77b..3fcf0b5aab3 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/page.tsx
@@ -15,9 +15,12 @@ import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { loginRedirect } from "@/utils/redirects";
import type { Transaction } from "../../analytics/tx-table/types";
import {
+ getSingleSolanaTransaction,
getSingleTransaction,
+ getSolanaTransactionActivityLogs,
getTransactionActivityLogs,
} from "../../lib/analytics";
+import { SolanaTransactionDetailsUI } from "./solana-transaction-details-ui";
import { TransactionDetailsUI } from "./transaction-details-ui";
type AbiItem =
@@ -195,17 +198,18 @@ export default async function TransactionPage({
redirect(`/team/${team_slug}`);
}
- const [transactionData, activityLogs] = await Promise.all([
+ // Try fetching both EVM and Solana transactions
+ const [evmTransactionData, solanaTransactionData] = await Promise.all([
getSingleTransaction({
clientId: project.publishableKey,
teamId: project.teamId,
transactionId: id,
- }),
- getTransactionActivityLogs({
+ }).catch(() => null),
+ getSingleSolanaTransaction({
clientId: project.publishableKey,
teamId: project.teamId,
transactionId: id,
- }),
+ }).catch(() => null),
]);
const client = getClientThirdwebClient({
@@ -213,23 +217,50 @@ export default async function TransactionPage({
teamId: project.teamId,
});
- if (!transactionData) {
- notFound();
+ // Determine which transaction type we have and fetch appropriate activity logs
+ if (solanaTransactionData) {
+ const activityLogs = await getSolanaTransactionActivityLogs({
+ clientId: project.publishableKey,
+ teamId: project.teamId,
+ transactionId: id,
+ });
+
+ // Render Solana transaction details
+ return (
+
+ );
}
- // Decode transaction data on the server
- const decodedTransactionData = await decodeTransactionData(transactionData);
+ if (evmTransactionData) {
+ const activityLogs = await getTransactionActivityLogs({
+ clientId: project.publishableKey,
+ teamId: project.teamId,
+ transactionId: id,
+ });
- return (
-
+ // Decode transaction data on the server for EVM
+ const decodedTransactionData =
+ await decodeTransactionData(evmTransactionData);
+
+ // Render EVM transaction details
+ return (
-
- );
+ );
+ }
+
+ // No transaction found
+ notFound();
}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/solana-transaction-details-ui.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/solana-transaction-details-ui.tsx
new file mode 100644
index 00000000000..534073947b6
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/solana-transaction-details-ui.tsx
@@ -0,0 +1,459 @@
+"use client";
+
+import { format, formatDistanceToNowStrict } from "date-fns";
+import {
+ ChevronDownIcon,
+ ChevronRightIcon,
+ ExternalLinkIcon,
+} from "lucide-react";
+import Link from "next/link";
+import { useState } from "react";
+import type { ThirdwebClient } from "thirdweb";
+import { stringify } from "thirdweb/utils";
+import type { Project } from "@/api/project/projects";
+import { SolanaAddress } from "@/components/blocks/solana-address";
+import { Badge } from "@/components/ui/badge";
+import { CopyTextButton } from "@/components/ui/CopyTextButton";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { CodeClient } from "@/components/ui/code/code.client";
+import { TabButtons } from "@/components/ui/tabs";
+import { ToolTipLabel } from "@/components/ui/tooltip";
+import type { SolanaTransaction } from "../../analytics/solana-tx-table/types";
+import type { ActivityLogEntry } from "../../lib/analytics";
+import { getSolanaNetworkName, getSolscanUrl } from "../../lib/solana-utils";
+
+const solanaStatusDetails = {
+ CONFIRMED: { name: "Confirmed", variant: "success" as const },
+ FAILED: { name: "Failed", variant: "destructive" as const },
+ QUEUED: { name: "Queued", variant: "warning" as const },
+ SUBMITTED: { name: "Submitted", variant: "warning" as const },
+};
+
+export function SolanaTransactionDetailsUI({
+ transaction,
+ activityLogs,
+}: {
+ transaction: SolanaTransaction;
+ teamSlug: string;
+ client: ThirdwebClient;
+ project: Project;
+ activityLogs: ActivityLogEntry[];
+}) {
+ const [activeTab, setActiveTab] = useState<"overview" | "logs" | "raw">(
+ "overview",
+ );
+
+ // Extract relevant data from transaction
+ const {
+ id,
+ chainId,
+ signerAddress,
+ signature,
+ confirmedAt,
+ createdAt,
+ executionResult,
+ errorMessage,
+ } = transaction;
+
+ const status = (executionResult?.status ||
+ transaction.status ||
+ "QUEUED") as keyof typeof solanaStatusDetails;
+
+ // Parse network from chainId
+ const network = getSolanaNetworkName(chainId);
+ const networkDisplay = network.charAt(0).toUpperCase() + network.slice(1);
+
+ // Calculate time difference between creation and confirmation
+ const confirmationTime =
+ confirmedAt && createdAt
+ ? new Date(confirmedAt).getTime() -
+ (createdAt instanceof Date ? createdAt : new Date(createdAt)).getTime()
+ : null;
+
+ // Get signature from executionResult if available
+ const txSignature =
+ (executionResult && "signature" in executionResult
+ ? executionResult.signature
+ : signature) || null;
+
+ // Get slot and blockTime
+ const slot =
+ executionResult && "slot" in executionResult
+ ? executionResult.slot
+ : transaction.confirmedAtSlot;
+ const blockTime =
+ executionResult && "blockTime" in executionResult
+ ? executionResult.blockTime
+ : transaction.blockTime;
+
+ return (
+ <>
+ {/* Transaction ID Header */}
+
+
+
+ Transaction Details
+
+
+ Queue ID:
+
+
+
+
+ {solanaStatusDetails[status].name}
+
+
+
+ {/* Tabs */}
+
setActiveTab("overview"),
+ isActive: activeTab === "overview",
+ },
+ {
+ name: "Activity Logs",
+ onClick: () => setActiveTab("logs"),
+ isActive: activeTab === "logs",
+ },
+ {
+ name: "Raw Data",
+ onClick: () => setActiveTab("raw"),
+ isActive: activeTab === "raw",
+ },
+ ]}
+ />
+
+ {/* Overview Tab */}
+ {activeTab === "overview" && (
+
+ {/* Transaction Information */}
+
+
+ Transaction Information
+
+
+
+
+
+
+
+ {txSignature && (
+
+
+
+ {txSignature.slice(0, 12)}...{txSignature.slice(-12)}
+
+
+
+
+ )}
+ {slot && }
+ {errorMessage && (
+
+
+ {errorMessage}
+
+
+ )}
+
+
+
+ {/* Timing Information */}
+
+
+ Timing Information
+
+
+
+
+
+ {formatDistanceToNowStrict(
+ createdAt instanceof Date
+ ? createdAt
+ : new Date(createdAt),
+ { addSuffix: true },
+ )}
+
+
+
+ {confirmedAt && (
+ <>
+
+
+
+ {formatDistanceToNowStrict(new Date(confirmedAt), {
+ addSuffix: true,
+ })}
+
+
+
+ {confirmationTime && (
+
+ )}
+ >
+ )}
+ {blockTime && (
+
+ )}
+
+
+
+ {/* Transaction Instructions */}
+ {transaction.transactionParams &&
+ transaction.transactionParams.instructions.length > 0 && (
+
+
+ Instructions
+
+
+
+ {transaction.transactionParams.instructions.map(
+ (instruction, index) => (
+
+ ),
+ )}
+
+
+
+ )}
+
+ {/* Execution Details */}
+ {executionResult && (
+
+
+ Execution Details
+
+
+
+
+
+ )}
+
+ )}
+
+ {/* Activity Logs Tab */}
+ {activeTab === "logs" && (
+
+
+ Activity Logs
+
+
+ {activityLogs.length === 0 ? (
+
+ No activity logs available
+
+ ) : (
+
+ {activityLogs.map((log) => (
+
+ ))}
+
+ )}
+
+
+ )}
+
+ {/* Raw Data Tab */}
+ {activeTab === "raw" && (
+
+
+ Raw Transaction Data
+
+
+
+
+
+ )}
+ >
+ );
+}
+
+function InfoRow({
+ label,
+ value,
+ children,
+}: {
+ label: string;
+ value?: string;
+ children?: React.ReactNode;
+}) {
+ return (
+
+
{label}
+
+ {children || {value || "N/A"} }
+
+
+ );
+}
+
+function InstructionCard({
+ instruction,
+ index,
+}: {
+ instruction: {
+ programId: string;
+ keys: Array<{
+ pubkey: string;
+ isSigner: boolean;
+ isWritable: boolean;
+ }>;
+ data: string;
+ };
+ index: number;
+}) {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ return (
+
+
setIsExpanded(!isExpanded)}
+ >
+ Instruction {index + 1}
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
+ {isExpanded && (
+
+
+
+ Program ID
+
+
+ {instruction.programId}
+
+
+
+
+
+ Accounts ({instruction.keys.length})
+
+
+ {instruction.keys.map((key, idx) => (
+
+
+ {key.pubkey}
+
+
+ {key.isSigner && (
+
+ Signer
+
+ )}
+ {key.isWritable && (
+
+ Writable
+
+ )}
+
+
+ ))}
+
+
+
+
+
+ Data
+
+
+ {instruction.data || "No data"}
+
+
+
+ )}
+
+ );
+}
+
+function ActivityLogItem({ log }: { log: ActivityLogEntry }) {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ return (
+
+
setIsExpanded(!isExpanded)}
+ >
+
+
+ {log.eventType}
+
+ {log.stageName}
+
+
+
+ {format(new Date(log.timestamp), "PP pp")}
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isExpanded && log.payload && (
+
+
+
+ )}
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx
index c6e4324820b..0d8f03639c7 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx
@@ -22,8 +22,8 @@ import { TabButtons } from "@/components/ui/tabs";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useAllChainsData } from "@/hooks/chains/allChains";
import { ChainIconClient } from "@/icons/ChainIcon";
-import { statusDetails } from "../../analytics/tx-table/tx-table-ui";
import type { Transaction } from "../../analytics/tx-table/types";
+import { statusDetails } from "../../analytics/tx-table/types";
import type { ActivityLogEntry } from "../../lib/analytics";
import type { DecodedTransactionData, DecodedTransactionResult } from "./page";
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/page.tsx
index 523b38c5332..4019cd88dda 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/page.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/page.tsx
@@ -12,11 +12,15 @@ import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { WalletProductIcon } from "@/icons/WalletProductIcon";
import { getFiltersFromSearchParams } from "@/lib/time";
import { loginRedirect } from "@/utils/redirects";
+import { ServerWalletsTable } from "../transactions/components/server-wallets-table.client";
import type { Wallet } from "../transactions/server-wallets/wallet-table/types";
-import { ServerWalletsTable } from "../transactions/server-wallets/wallet-table/wallet-table";
+import { listSolanaAccounts } from "../transactions/solana-wallets/lib/vault.client";
+import type { SolanaWallet } from "../transactions/solana-wallets/wallet-table/types";
import { InAppWalletAnalytics } from "./analytics/chart";
import { InAppWalletsSummary } from "./analytics/chart/Summary";
+export const dynamic = "force-dynamic";
+
export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
searchParams: Promise<{
@@ -25,6 +29,7 @@ export default async function Page(props: {
type?: string;
interval?: string;
page?: string;
+ solana_page?: string;
}>;
}) {
const [searchParams, params] = await Promise.all([
@@ -66,7 +71,9 @@ export default async function Page(props: {
// Fetch server wallets with pagination (5 per page)
const pageSize = 5;
const currentPage = Number.parseInt(searchParams.page ?? "1");
+ const solanCurrentPage = Number.parseInt(searchParams.solana_page ?? "1");
+ // Fetch EVM wallets
const eoas =
vaultClient && managementAccessToken
? await listEoas({
@@ -84,7 +91,32 @@ export default async function Page(props: {
})
: { data: { items: [], totalRecords: 0 }, error: null, success: true };
- const serverWallets = eoas.data?.items as Wallet[] | undefined;
+ // Fetch Solana wallets
+ let solanaAccounts: {
+ data: { items: SolanaWallet[]; totalRecords: number };
+ error: Error | null;
+ success: boolean;
+ };
+
+ if (managementAccessToken) {
+ solanaAccounts = await listSolanaAccounts({
+ managementAccessToken,
+ page: solanCurrentPage,
+ limit: pageSize,
+ projectId: project.id,
+ });
+ } else {
+ solanaAccounts = {
+ data: { items: [], totalRecords: 0 },
+ error: null,
+ success: true,
+ };
+ }
+
+ // Check for Solana permission errors
+ const isSolanaPermissionError = solanaAccounts.error?.message?.includes(
+ "AUTH_INSUFFICIENT_SCOPE",
+ );
const client = getClientThirdwebClient({
jwt: authToken,
@@ -143,18 +175,34 @@ export default async function Page(props: {
authToken={authToken}
/>
- {/* Server Wallets Section */}
+ {/* Server Wallets Section (EVM + Solana) */}
- {eoas.error ? null : (
+ {eoas.error ? (
+
+
+ EVM Wallet Error
+
+
+ {eoas.error.message || "Failed to load EVM wallets"}
+
+
+ ) : (
)}
diff --git a/packages/vault-sdk/src/types.ts b/packages/vault-sdk/src/types.ts
index 4d1b07a7128..00cfc805dff 100644
--- a/packages/vault-sdk/src/types.ts
+++ b/packages/vault-sdk/src/types.ts
@@ -503,7 +503,7 @@ type CreateSolanaAccountData = {
};
type GetSolanaAccountsData = {
- accounts: CreateSolanaAccountData[];
+ items: CreateSolanaAccountData[];
totalCount: number;
page: number;
pageSize: number;