
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
@phantom/react-sdk
Advanced tools
React hooks for integrating Phantom wallet functionality into React applications with chain-specific operations.
React hooks for integrating Phantom wallet functionality into React applications with chain-specific operations.
npm install @phantom/react-sdk
Install additional dependencies based on the networks you want to support:
| Network Support | Required Dependencies |
|---|---|
| Solana | @solana/web3.js OR @solana/kit |
| Ethereum/EVM | viem |
Example for Solana + Ethereum support (using @solana/web3.js):
npm install @phantom/react-sdk @solana/web3.js viem
Example for Solana + Ethereum support (using @solana/kit):
npm install @phantom/react-sdk @solana/kit viem
import { PhantomProvider, useConnect, useSolana, useEthereum } from "@phantom/react-sdk";
import { AddressType } from "@phantom/browser-sdk";
function App() {
return (
<PhantomProvider
config={{
providers: ["injected"], // Only allow Phantom browser extension
addressTypes: [AddressType.solana, AddressType.ethereum],
}}
>
<WalletComponent />
</PhantomProvider>
);
}
function WalletComponent() {
const { connect, isConnecting } = useConnect();
const { solana } = useSolana();
const { ethereum } = useEthereum();
const handleConnect = async () => {
const { addresses } = await connect({ provider: "injected" });
console.log("Connected addresses:", addresses);
};
const signSolanaMessage = async () => {
const signature = await solana.signMessage("Hello Solana!");
console.log("Solana signature:", signature);
};
const signEthereumMessage = async () => {
const accounts = await ethereum.getAccounts();
const signature = await ethereum.signPersonalMessage("Hello Ethereum!", accounts[0]);
console.log("Ethereum signature:", signature);
};
return (
<div>
<button onClick={handleConnect} disabled={isConnecting}>
{isConnecting ? "Connecting..." : "Connect Wallet"}
</button>
<button onClick={signSolanaMessage}>Sign Solana Message</button>
<button onClick={signEthereumMessage}>Sign Ethereum Message</button>
</div>
);
}
import { PhantomProvider } from "@phantom/react-sdk";
import { AddressType } from "@phantom/browser-sdk";
function App() {
return (
<PhantomProvider
config={{
providers: ["google", "apple", "phantom", "injected"], // Allow all auth methods
appId: "your-app-id", // Get your app ID from phantom.com/portal (required for embedded providers)
addressTypes: [AddressType.solana, AddressType.ethereum],
}}
>
<YourApp />
</PhantomProvider>
);
}
The SDK includes a built-in connection modal UI that provides a user-friendly interface for connecting to Phantom. The modal supports multiple connection methods (Google, Apple, Phantom Login, browser extension) and handles all connection logic automatically.
To use the modal, pass a theme prop to PhantomProvider and use the useModal() hook to control visibility:
import { PhantomProvider, useModal, darkTheme, usePhantom } from "@phantom/react-sdk";
import { AddressType } from "@phantom/browser-sdk";
function App() {
return (
<PhantomProvider
config={{
providers: ["google", "apple", "phantom", "injected"],
appId: "your-app-id",
addressTypes: [AddressType.solana, AddressType.ethereum],
}}
theme={darkTheme}
appIcon="https://your-app.com/icon.png"
appName="Your App Name"
>
<WalletComponent />
</PhantomProvider>
);
}
function WalletComponent() {
const { open, close, isOpened } = useModal();
const { isConnected, user } = usePhantom();
if (isConnected) {
return (
<div>
<p>Connected as {user?.email || "Unknown"}</p>
</div>
);
}
return <button onClick={open}>Connect Wallet</button>;
}
Modal Features:
Control the connection modal visibility:
import { useModal } from "@phantom/react-sdk";
function ConnectButton() {
const { open, close, isOpened } = useModal();
return (
<div>
<button onClick={open}>Open Modal</button>
<button onClick={close}>Close Modal</button>
<p>Modal is {isOpened ? "open" : "closed"}</p>
</div>
);
}
Returns:
open() - Function to open the modalclose() - Function to close the modalisOpened - Boolean indicating if modal is currently visibleA ready-to-use button component that handles the complete connection flow. When disconnected, it shows a "Connect Wallet" button that opens the connection modal. When connected, it displays the truncated wallet address and opens the wallet management modal on click.
import { ConnectButton, AddressType } from "@phantom/react-sdk";
function Header() {
return (
<div>
{/* Default: Shows first available address */}
<ConnectButton />
{/* Show specific address type */}
<ConnectButton addressType={AddressType.solana} />
<ConnectButton addressType={AddressType.ethereum} />
{/* Full width button */}
<ConnectButton fullWidth />
</div>
);
}
Props:
addressType?: AddressType - Specify which address type to display when connected (e.g., AddressType.solana, AddressType.ethereum). If not specified, shows the first available address.fullWidth?: boolean - Whether the button should take full width of its container. Default: falseFeatures:
An inline embedded component that displays the connection UI directly in your page layout (without a modal backdrop). Perfect for auth callback pages or when you want a more integrated connection experience. The component automatically handles all connection states including loading, error, and success during the auth callback flow.
import { ConnectBox } from "@phantom/react-sdk";
function AuthCallbackPage() {
return (
<div>
<h1>Connecting to Phantom...</h1>
<ConnectBox />
</div>
);
}
Props:
maxWidth?: string | number - Maximum width of the box. Can be a string (e.g., "500px") or number (e.g., 500). Default: "350px"transparent?: boolean - When true, removes background, border, and shadow for a transparent appearance. Default: falseappIcon?: string - URL to your app icon (optional, can also be set via PhantomProvider)appName?: string - Your app name (optional, can also be set via PhantomProvider)Usage Examples:
import { ConnectBox } from "@phantom/react-sdk";
// Default usage
<ConnectBox />
// Custom width
<ConnectBox maxWidth="500px" />
// Transparent (no background/border)
<ConnectBox transparent />
// Custom width with transparent
<ConnectBox maxWidth={600} transparent />
Features:
Use Cases:
/auth/callback) where users return from OAuth providersWhen using embedded authentication providers (Google, Apple, Phantom Login, etc.), users are redirected to your app's callback URL after authentication. The SDK automatically handles the callback and completes the connection. Here's how to build a callback page if you're not using ConnectBox:
Basic Auth Callback Page:
import { usePhantom, useConnect, useAccounts } from "@phantom/react-sdk";
import { useNavigate } from "react-router-dom"; // or your router
function AuthCallbackPage() {
const navigate = useNavigate();
const { isConnected } = usePhantom();
const { isConnecting, error: connectError } = useConnect();
const addresses = useAccounts();
const handleGoHome = () => {
navigate("/");
};
// Loading state - SDK is processing the callback
if (isConnecting) {
return (
<div>
<h1>Connecting to wallet...</h1>
<p>Please wait while we complete your authentication.</p>
</div>
);
}
// Success state - connection completed
if (isConnected && addresses && addresses.length > 0) {
return (
<div>
<h1>Authentication Successful</h1>
<p>You are now connected to your wallet.</p>
<div>
<h2>Addresses:</h2>
{addresses.map((addr, index) => (
<div key={index}>
<strong>{addr.addressType}:</strong> {addr.address}
</div>
))}
</div>
<button onClick={handleGoHome}>Go to Main App</button>
</div>
);
}
// Error state - connection failed
if (connectError) {
return (
<div>
<h1>Authentication Failed</h1>
<p>{connectError.message || "An unknown error occurred during authentication."}</p>
<div>
<button onClick={handleGoHome}>Go to Main App</button>
</div>
</div>
);
}
// Default state (shouldn't normally reach here)
return (
<div>
<h1>Processing authentication...</h1>
</div>
);
}
Key Points:
autoConnect() automatically processes the callback URL parameters when the page loadsuseConnect() to access isConnecting and error states during the callback flowusePhantom() to check isConnected statususeAccounts() to get connected wallet addressesconnectError to handle authentication failuresRouter Setup:
Make sure your callback route is configured in your router:
import { Routes, Route } from "react-router-dom";
import { PhantomProvider } from "@phantom/react-sdk";
function App() {
return (
<PhantomProvider config={config} theme={darkTheme}>
<Routes>
<Route path="/auth/callback" element={<AuthCallbackPage />} />
<Route path="/" element={<MainApp />} />
</Routes>
</PhantomProvider>
);
}
Note: For a simpler implementation, consider using the ConnectBox component which handles all these states automatically.
Customize the modal appearance by passing a theme object to the PhantomProvider. The SDK includes two built-in themes: darkTheme (default) and lightTheme.
import { PhantomProvider, darkTheme, lightTheme } from "@phantom/react-sdk";
// Use dark theme (default)
<PhantomProvider config={config} theme={darkTheme}>
<App />
</PhantomProvider>
// Use light theme
<PhantomProvider config={config} theme={lightTheme}>
<App />
</PhantomProvider>
You can pass a partial theme object to customize specific properties:
import { PhantomProvider } from "@phantom/react-sdk";
const customTheme = {
background: "#1a1a1a",
text: "#ffffff",
secondary: "#98979C",
brand: "#ab9ff2",
error: "#ff4444",
success: "#00ff00",
borderRadius: "16px",
overlay: "rgba(0, 0, 0, 0.8)",
};
<PhantomProvider config={config} theme={customTheme} appIcon="https://your-app.com/icon.png" appName="Your App">
<App />
</PhantomProvider>;
| Property | Type | Description |
|---|---|---|
background | string | Background color for modal |
text | string | Primary text color |
secondary | string | Secondary color for text, borders, dividers (must be hex) |
brand | string | Brand/primary action color |
error | string | Error state color |
success | string | Success state color |
borderRadius | string | Border radius for buttons and modal |
overlay | string | Overlay background color (with opacity) |
Note: The secondary color must be a hex color value (e.g., #98979C) as it's used to derive auxiliary colors with opacity.
If you need to access the current theme in your own components, use the useTheme() hook:
import { useTheme } from "@phantom/react-sdk";
function CustomComponent() {
const theme = useTheme();
return <div style={{ backgroundColor: theme.background, color: theme.text }}>Themed content</div>;
}
The React SDK follows a clear connection pattern:
PhantomProvideruseConnect() to establish wallet connectionuseSolana(), useEthereum()) for transactions and signingfunction WalletExample() {
const { connect } = useConnect();
const { solana } = useSolana();
const { ethereum } = useEthereum();
// 1. Connect first (provider parameter is required)
const handleConnect = async () => {
await connect({ provider: "injected" });
};
// 2. Then use chain-specific operations
const sendSolanaTransaction = async () => {
const result = await solana.signAndSendTransaction(transaction);
};
const sendEthereumTransaction = async () => {
const result = await ethereum.sendTransaction(transaction);
};
}
The connect() method requires a provider parameter and automatically switches between providers based on the authentication method you specify:
const { connect } = useConnect();
// Connect with injected provider (Phantom extension)
// Automatically switches to injected provider if not already using it
await connect({
provider: "injected",
});
// Connect with Google authentication (embedded provider)
// Automatically switches to embedded provider if not already using it
await connect({
provider: "google",
});
// Connect with Apple authentication (embedded provider)
// Automatically switches to embedded provider if not already using it
await connect({
provider: "apple",
});
// Connect with Phantom authentication (embedded provider)
// Uses Phantom extension or mobile app for authentication
// Automatically switches to embedded provider if not already using it
await connect({
provider: "phantom",
});
// Connect with JWT authentication (embedded provider)
await connect({
provider: "jwt",
jwtToken: "your-jwt-token",
});
The SDK provides an isLoading state to track when initialization and autoconnect are in progress. This is useful for showing loading states before your app is ready.
import { useConnect, usePhantom } from "@phantom/react-sdk";
function App() {
const { isLoading } = usePhantom();
const { connect } = useConnect();
// Show loading state while SDK initializes
if (isLoading) {
return (
<div>
<h1>Initializing Phantom SDK...</h1>
<p>Please wait...</p>
</div>
);
}
// SDK is ready
return (
<div>
<h1>Welcome!</h1>
<button onClick={() => connect({ provider: "injected" })}>Connect Wallet</button>
</div>
);
}
The SDK supports multiple authentication providers that you configure via the providers array:
"injected" - Phantom browser extension"google" - Google OAuth"apple" - Apple ID"phantom" - Phantom Login"deeplink" - Deeplink to Phantom mobile app (only renders on mobile devices)Browser Extension Only
<PhantomProvider
config={{
providers: ["injected"], // Only allow browser extension
addressTypes: [AddressType.solana, AddressType.ethereum],
}}
>
<YourApp />
</PhantomProvider>
Multiple Authentication Methods
<PhantomProvider
config={{
providers: ["google", "apple", "phantom", "injected", "deeplink"], // Allow all methods
appId: "your-app-id", // Required for embedded providers
addressTypes: [AddressType.solana, AddressType.ethereum],
}}
>
<YourApp />
</PhantomProvider>
Mobile Deeplink Support
The "deeplink" provider enables a button that opens the Phantom mobile app on mobile devices. This button only appears on mobile devices when the Phantom browser extension is not installed. When clicked, it redirects users to the Phantom mobile app to complete authentication.
<PhantomProvider
config={{
providers: ["google", "apple", "phantom", "deeplink"], // Include deeplink for mobile support
appId: "your-app-id", // Required for deeplink
addressTypes: [AddressType.solana, AddressType.ethereum],
}}
>
<YourApp />
</PhantomProvider>
When using embedded providers (google, apple, phantom, etc.), you can specify the wallet type using embeddedWalletType. The default is "user-wallet":
<PhantomProvider
config={{
providers: ["google", "apple", "phantom"],
appId: "your-app-id",
addressTypes: [AddressType.solana, AddressType.ethereum],
embeddedWalletType: "user-wallet", // default, can be omitted
}}
>
<YourApp />
</PhantomProvider>
Connect to wallet:
import { useConnect } from "@phantom/react-sdk";
function ConnectButton() {
const { connect, isConnecting, isLoading, error } = useConnect();
const handleConnect = async () => {
try {
const { addresses } = await connect({ provider: "injected" });
console.log("Connected addresses:", addresses);
} catch (err) {
console.error("Failed to connect:", err);
}
};
// Wait for SDK to finish initializing before showing connect button
if (isLoading) {
return <div>Loading...</div>;
}
return (
<button onClick={handleConnect} disabled={isConnecting}>
{isConnecting ? "Connecting..." : "Connect Wallet"}
</button>
);
}
Get connected wallet addresses:
import { useAccounts } from "@phantom/react-sdk";
function WalletAddresses() {
const addresses = useAccounts();
if (!addresses) {
return <div>Not connected</div>;
}
return (
<div>
{addresses.map((addr, index) => (
<div key={index}>
<strong>{addr.addressType}:</strong> {addr.address}
</div>
))}
</div>
);
}
Disconnect from wallet:
import { useDisconnect } from "@phantom/react-sdk";
function DisconnectButton() {
const { disconnect, isDisconnecting } = useDisconnect();
return (
<button onClick={disconnect} disabled={isDisconnecting}>
{isDisconnecting ? "Disconnecting..." : "Disconnect"}
</button>
);
}
Check if the Phantom browser extension is installed (for injected provider):
import { useIsExtensionInstalled } from "@phantom/react-sdk";
function ExtensionStatus() {
const { isLoading, isInstalled } = useIsExtensionInstalled();
if (isLoading) {
return <div>Checking for Phantom extension...</div>;
}
return (
<div>
{isInstalled ? (
<p>✅ Phantom extension is installed!</p>
) : (
<p>
❌ Phantom extension not found.{" "}
<a href="https://phantom.app/download" target="_blank">
Install here
</a>
</p>
)}
</div>
);
}
Check if Phantom Login is available (requires extension installed and phantom_login feature support):
import { useIsPhantomLoginAvailable } from "@phantom/react-sdk";
function PhantomLoginButton() {
const { isLoading, isAvailable } = useIsPhantomLoginAvailable();
const { connect } = useConnect();
if (isLoading) {
return <div>Checking Phantom Login availability...</div>;
}
if (!isAvailable) {
return null; // Don't show button if Phantom Login is not available
}
return <button onClick={() => connect({ provider: "phantom" })}>Login with Phantom</button>;
}
Hook for Solana chain operations:
import { useSolana } from "@phantom/react-sdk";
import { VersionedTransaction, TransactionMessage, SystemProgram, PublicKey, Connection } from "@solana/web3.js";
function SolanaOperations() {
const { solana, isAvailable } = useSolana();
// Check if Solana is available before using it
if (!isAvailable) {
return <div>Solana is not available for the current wallet</div>;
}
const signMessage = async () => {
const signature = await solana.signMessage("Hello Solana!");
console.log("Signature:", signature);
};
const signAndSendTransaction = async () => {
// Create transaction
const connection = new Connection("https://api.mainnet-beta.solana.com");
const { blockhash } = await connection.getLatestBlockhash();
const fromAddress = solana.publicKey;
if (!fromAddress) throw new Error("Not connected");
const transferInstruction = SystemProgram.transfer({
fromPubkey: new PublicKey(fromAddress),
toPubkey: new PublicKey(toAddress),
lamports: 1000000, // 0.001 SOL
});
const messageV0 = new TransactionMessage({
payerKey: new PublicKey(fromAddress),
recentBlockhash: blockhash,
instructions: [transferInstruction],
}).compileToV0Message();
const transaction = new VersionedTransaction(messageV0);
// Sign and send
const result = await solana.signAndSendTransaction(transaction);
console.log("Transaction sent:", result.signature);
};
const switchNetwork = async () => {
await solana.switchNetwork("devnet");
};
return (
<div>
<button onClick={signMessage}>Sign Message</button>
<button onClick={signAndSendTransaction}>Send Transaction</button>
<button onClick={switchNetwork}>Switch to Devnet</button>
<p>Connected: {solana.isConnected ? "Yes" : "No"}</p>
</div>
);
}
Available methods:
signMessage(message) - Sign a messagesignTransaction(transaction) - Sign without sendingsignAndSendTransaction(transaction) - Sign and sendswitchNetwork(network) - Switch between mainnet/devnetpublicKey - Current public key (or null if disconnected)isConnected - Connection statusisAvailable - Provider availability (see note below)Note on isAvailable:
The isAvailable property indicates whether the Solana chain is available for the currently connected wallet:
For embedded wallets (Google, Apple, Phantom Login, etc.): isAvailable will be true for all networks configured in your addressTypes array, as embedded wallets support all configured networks.
For Phantom injected wallet: isAvailable will be true for all networks configured in your addressTypes array, as Phantom supports multiple networks.
For other injected wallets (discovered via Wallet Standard or EIP-6963): isAvailable depends on which networks the specific wallet supports. For example, if you connect to a wallet that only supports Ethereum, isAvailable will be false for Solana even if Solana is in your addressTypes configuration.
Always check isAvailable before attempting to use chain-specific methods when working with injected wallets that may not support all networks.
Hook for Ethereum chain operations:
import { useEthereum } from "@phantom/react-sdk";
function EthereumOperations() {
const { ethereum, isAvailable } = useEthereum();
// Check if Ethereum is available before using it
if (!isAvailable) {
return <div>Ethereum is not available for the current wallet</div>;
}
const signPersonalMessage = async () => {
const accounts = await ethereum.getAccounts();
const signature = await ethereum.signPersonalMessage("Hello Ethereum!", accounts[0]);
console.log("Signature:", signature);
};
const signTypedData = async () => {
const accounts = await ethereum.getAccounts();
const typedData = {
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
],
Mail: [
{ name: "from", type: "string" },
{ name: "to", type: "string" },
{ name: "contents", type: "string" },
],
},
primaryType: "Mail",
domain: {
name: "Ether Mail",
version: "1",
chainId: 1,
verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
},
message: {
from: "Alice",
to: "Bob",
contents: "Hello!",
},
};
const signature = await ethereum.signTypedData(typedData);
console.log("Typed data signature:", signature);
};
const signTransaction = async () => {
const signedTx = await ethereum.signTransaction({
to: "0x742d35Cc6634C0532925a3b8D4C8db86fB5C4A7E",
value: "1000000000000000000", // 1 ETH in wei
gas: "21000",
});
console.log("Transaction signed:", signedTx);
// Transaction is signed but not sent - you can broadcast it later
};
const sendTransaction = async () => {
const result = await ethereum.sendTransaction({
to: "0x742d35Cc6634C0532925a3b8D4C8db86fB5C4A7E",
value: "1000000000000000000", // 1 ETH in wei
gas: "21000",
});
console.log("Transaction sent:", result.hash);
};
const switchChain = async () => {
await ethereum.switchChain(137); // Switch to Polygon
};
return (
<div>
<button onClick={signPersonalMessage}>Sign Personal Message</button>
<button onClick={signTypedData}>Sign Typed Data</button>
<button onClick={signTransaction}>Sign Transaction</button>
<button onClick={sendTransaction}>Sign & Send Transaction</button>
<button onClick={switchChain}>Switch to Polygon</button>
<p>Connected: {ethereum.isConnected ? "Yes" : "No"}</p>
</div>
);
}
Available methods:
request(args) - EIP-1193 requestssignPersonalMessage(message, address) - Sign personal messagesignTypedData(typedData) - Sign EIP-712 typed datasignTransaction(transaction) - Sign transaction without sendingsendTransaction(transaction) - Sign and send transactionswitchChain(chainId) - Switch chains (accepts chain ID as number or a hex string)getChainId() - Get current chain IDgetAccounts() - Get connected accountsisConnected - Connection statusisAvailable - Provider availability (see note below)Note on isAvailable:
The isAvailable property indicates whether the Ethereum chain is available for the currently connected wallet:
For embedded wallets (Google, Apple, Phantom Login, etc.): isAvailable will be true for all networks configured in your addressTypes array, as embedded wallets support all configured networks.
For Phantom injected wallet: isAvailable will be true for all networks configured in your addressTypes array, as Phantom supports multiple networks.
For other injected wallets (discovered via Wallet Standard or EIP-6963): isAvailable depends on which networks the specific wallet supports. For example, if you connect to a wallet that only supports Solana, isAvailable will be false for Ethereum even if Ethereum is in your addressTypes configuration.
Always check isAvailable before attempting to use chain-specific methods when working with injected wallets that may not support all networks.
Supported EVM Networks:
| Network | Chain ID | Usage |
|---|---|---|
| Ethereum Mainnet | 1 | ethereum.switchChain(1) |
| Ethereum Sepolia | 11155111 | ethereum.switchChain(11155111) |
| Polygon Mainnet | 137 | ethereum.switchChain(137) |
| Polygon Amoy | 80002 | ethereum.switchChain(80002) |
| Base Mainnet | 8453 | ethereum.switchChain(8453) |
| Base Sepolia | 84532 | ethereum.switchChain(84532) |
| Arbitrum One | 42161 | ethereum.switchChain(42161) |
| Arbitrum Sepolia | 421614 | ethereum.switchChain(421614) |
| Monad Mainnet | 143 | ethereum.switchChain(143) |
| Monad Testnet | 10143 | ethereum.switchChain(10143) |
Hook to get discovered injected wallets with automatic loading and error states. Discovers wallets using Wallet Standard (Solana) and EIP-6963 (Ethereum) standards.
import { useDiscoveredWallets } from "@phantom/react-sdk";
function WalletSelector() {
const { wallets, isLoading, error, refetch } = useDiscoveredWallets();
// wallets: InjectedWalletInfo[] - Array of discovered wallets
// isLoading: boolean - Loading state during discovery
// error: Error | null - Error state if discovery fails
// refetch: () => Promise<void> - Function to manually trigger discovery
}
Returns:
wallets: InjectedWalletInfo[] - Array of discovered wallet informationisLoading: boolean - true while discovery is in progresserror: Error | null - Error object if discovery fails, null otherwiserefetch: () => Promise<void> - Async function to manually refresh the wallet listBehavior:
discoverWallets() to discover themaddressTypes configured in PhantomProviderHook for managing auto-confirm functionality with the Phantom extension. Auto-confirm allows transactions to be automatically approved without user interaction for enabled chains.
Note: This hook only works with the
injectedprovider type (Phantom browser extension). It will throw errors for embedded providers.
import { useAutoConfirm, NetworkId } from "@phantom/react-sdk";
function AutoConfirmControls() {
const { enable, disable, status, supportedChains, isLoading, error, refetch } = useAutoConfirm();
const handleEnable = async () => {
try {
// Enable auto-confirm for specific chains
const result = await enable({
chains: [NetworkId.SOLANA_DEVNET, NetworkId.ETHEREUM_MAINNET],
});
console.log("Auto-confirm enabled:", result);
} catch (err) {
console.error("Failed to enable auto-confirm:", err);
}
};
const handleDisable = async () => {
try {
await disable();
console.log("Auto-confirm disabled");
} catch (err) {
console.error("Failed to disable auto-confirm:", err);
}
};
if (isLoading) {
return <div>Loading auto-confirm settings...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
<h3>Auto-Confirm Settings</h3>
<div>
<strong>Status:</strong> {status?.enabled ? "Enabled" : "Disabled"}
{status?.chains && (
<div>
<strong>Active Chains:</strong>
<ul>
{status.chains.map(chain => (
<li key={chain}>{chain}</li>
))}
</ul>
</div>
)}
</div>
<div>
<strong>Supported Chains:</strong>
{supportedChains?.chains && (
<ul>
{supportedChains.chains.map(chain => (
<li key={chain}>{chain}</li>
))}
</ul>
)}
</div>
<div>
<button onClick={handleEnable} disabled={isLoading}>
Enable Auto-Confirm
</button>
<button onClick={handleDisable} disabled={isLoading}>
Disable Auto-Confirm
</button>
<button onClick={refetch} disabled={isLoading}>
Refresh Status
</button>
</div>
</div>
);
}
Hook Interface:
interface UseAutoConfirmResult {
enable: (params: AutoConfirmEnableParams) => Promise<AutoConfirmResult>;
disable: () => Promise<void>;
status: AutoConfirmResult | null;
supportedChains: AutoConfirmSupportedChainsResult | null;
isLoading: boolean;
error: Error | null;
refetch: () => Promise<void>;
}
interface AutoConfirmEnableParams {
chains?: NetworkId[]; // Optional array of chains to enable
}
interface AutoConfirmResult {
enabled: boolean;
chains: NetworkId[];
}
interface AutoConfirmSupportedChainsResult {
chains: NetworkId[];
}
Available Methods:
enable(params) - Enable auto-confirm for specific chainsdisable() - Disable auto-confirm completelyrefetch() - Refresh status and supported chains from extensionstatus - Current auto-confirm status (enabled/disabled and active chains)supportedChains - List of chains that support auto-confirmisLoading - Loading state for operationserror - Any errors from auto-confirm operationsUsage Notes:
refetch() to manually refresh data from the extensionimport { VersionedTransaction, TransactionMessage, SystemProgram, PublicKey, Connection } from "@solana/web3.js";
import { useSolana } from "@phantom/react-sdk";
function SolanaExample() {
const { solana } = useSolana();
const sendTransaction = async () => {
// Get recent blockhash
const connection = new Connection("https://api.mainnet-beta.solana.com");
const { blockhash } = await connection.getLatestBlockhash();
// Create transfer instruction
const fromAddress = solana.publicKey;
if (!fromAddress) throw new Error("Not connected");
const transferInstruction = SystemProgram.transfer({
fromPubkey: new PublicKey(fromAddress),
toPubkey: new PublicKey(toAddress),
lamports: 1000000, // 0.001 SOL
});
// Create VersionedTransaction
const messageV0 = new TransactionMessage({
payerKey: new PublicKey(fromAddress),
recentBlockhash: blockhash,
instructions: [transferInstruction],
}).compileToV0Message();
const transaction = new VersionedTransaction(messageV0);
// Sign and send using chain-specific hook
const result = await solana.signAndSendTransaction(transaction);
console.log("Transaction sent:", result.signature);
};
return <button onClick={sendTransaction}>Send SOL</button>;
}
import {
createSolanaRpc,
pipe,
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
address,
compileTransaction,
} from "@solana/kit";
import { useSolana } from "@phantom/react-sdk";
function SolanaKitExample() {
const { solana } = useSolana();
const sendTransaction = async () => {
const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const userPublicKey = solana.publicKey;
if (!userPublicKey) throw new Error("Not connected");
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayer(address(userPublicKey), tx),
tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
);
const transaction = compileTransaction(transactionMessage);
// Sign and send using chain-specific hook
const result = await solana.signAndSendTransaction(transaction);
console.log("Transaction sent:", result.signature);
};
return <button onClick={sendTransaction}>Send SOL</button>;
}
import { parseEther, parseGwei, encodeFunctionData } from "viem";
import { useEthereum } from "@phantom/react-sdk";
function EthereumExample() {
const { ethereum } = useEthereum();
const sendEth = async () => {
const result = await ethereum.sendTransaction({
to: "0x742d35Cc6634C0532925a3b8D4C8db86fB5C4A7E",
value: parseEther("1").toString(), // 1 ETH
gas: "21000",
gasPrice: parseGwei("20").toString(), // 20 gwei
});
console.log("ETH sent:", result.hash);
};
const sendToken = async () => {
const result = await ethereum.sendTransaction({
to: tokenContractAddress,
data: encodeFunctionData({
abi: erc20Abi,
functionName: "transfer",
args: [recipientAddress, parseEther("100")],
}),
gas: "50000",
maxFeePerGas: parseGwei("30").toString(),
maxPriorityFeePerGas: parseGwei("2").toString(),
});
console.log("Token sent:", result.hash);
};
return (
<div>
<button onClick={sendEth}>Send ETH</button>
<button onClick={sendToken}>Send Token</button>
</div>
);
}
Pass presignTransaction directly to signAndSendTransaction for calls that need co-signing. Calls without it proceed normally — it is never applied globally.
Important: Phantom embedded wallets do not accept pre-signed transactions. If your use case requires a second signer (e.g. your app as the fee payer), that signing must happen via this option, after Phantom has constructed and validated the transaction. This restriction does not apply to injected providers (e.g. the Phantom browser extension).
Note:
presignTransactiononly fires for Solana transactions via the embedded provider. EVM transactions and injected providers are unaffected.
import { useSolana, base64urlDecode, base64urlEncode } from "@phantom/react-sdk";
import { VersionedTransaction } from "@solana/web3.js";
function SendWithFeeSponsor() {
const { solana } = useSolana();
const sendSponsored = async () => {
// This call co-signs as fee payer.
// presignTransaction should call your backend — never hold a fee payer keypair in frontend code.
const result = await solana.signAndSendTransaction(transaction, {
presignTransaction: async (tx, context) => {
// Send the transaction to your backend for fee payer signing.
// Your backend deserializes, partially signs, and returns the modified transaction.
const response = await fetch("/api/presign", {
method: "POST",
body: JSON.stringify({ transaction: tx, networkId: context.networkId }),
headers: { "Content-Type": "application/json" },
});
const { transaction: signedTx } = await response.json();
return signedTx; // base64url-encoded, partially signed by the fee payer
},
});
};
const sendNormal = async () => {
// This call has no co-signer
const result = await solana.signAndSendTransaction(transaction);
};
}
Never hold a fee payer keypair in frontend code. The
presignTransactioncallback runs in the browser — use it to call your own backend, which holds the keypair securely and returns the partially-signed transaction.
Quick reference of all available hooks:
| Hook | Purpose | Returns |
|---|---|---|
useConnect | Connect to wallet | { connect, isConnecting, error } |
useModal | Control connection modal | { open, close, isOpened } |
useAccounts | Get wallet addresses | WalletAddress[] or null |
useIsExtensionInstalled | Check extension status | { isLoading, isInstalled } |
useIsPhantomLoginAvailable | Check Phantom Login availability | { isLoading, isAvailable } |
useDisconnect | Disconnect from wallet | { disconnect, isDisconnecting } |
useAutoConfirm | Auto-confirm management (injected only) | { enable, disable, status, supportedChains, ... } |
useDiscoveredWallets | Get discovered injected wallets | { wallets, isLoading, error, refetch } |
useSolana | Solana chain operations | { signMessage, signAndSendTransaction, ... } |
useEthereum | Ethereum chain operations | { signPersonalMessage, sendTransaction, ... } |
useTheme | Access current theme | PhantomTheme |
usePhantom | Get provider context | { isConnected, isReady } |
interface PhantomSDKConfig {
// List of allowed authentication providers (REQUIRED)
providers: AuthProviderType[]; // e.g., ["google", "apple", "phantom", "injected"]
addressTypes?: [AddressType, ...AddressType[]]; // Networks to enable (e.g., [AddressType.solana])
// Required when using embedded providers (google, apple, phantom)
appId?: string; // Your app ID from phantom.com/portal
// Optional configuration
apiBaseUrl?: string; // Phantom API base URL (optional, has default)
authOptions?: {
authUrl?: string; // Custom auth URL (optional, defaults to "https://connect.phantom.app/login")
redirectUrl?: string; // Custom redirect URL after authentication (optional)
};
embeddedWalletType?: "user-wallet"; // Wallet type (optional, defaults to "user-wallet")
autoConnect?: boolean; // Auto-connect to existing session (default: true when embedded providers used)
}
// Valid provider types
type AuthProviderType = "google" | "apple" | "phantom" | "injected";
The React SDK supports separate debug configuration that can be changed without reinstantiating the underlying SDK, providing better performance.
interface PhantomDebugConfig {
enabled?: boolean; // Enable debug logging
level?: DebugLevel; // Debug level (ERROR, WARN, INFO, DEBUG)
callback?: DebugCallback; // Custom debug message handler
}
Pass the debugConfig as a separate prop to PhantomProvider:
import { PhantomProvider, type PhantomSDKConfig, type PhantomDebugConfig, DebugLevel } from "@phantom/react-sdk";
function App() {
const [debugLevel, setDebugLevel] = useState(DebugLevel.INFO);
const [debugMessages, setDebugMessages] = useState([]);
// SDK configuration - static, won't change when debug settings change
const config: PhantomSDKConfig = {
providers: ["google", "apple", "phantom"],
appId: "your-app-id",
addressTypes: [AddressType.solana, AddressType.ethereum],
// ... other config
};
// Debug configuration - separate to avoid SDK reinstantiation
const debugConfig: PhantomDebugConfig = {
enabled: true,
level: debugLevel,
callback: (message) => {
setDebugMessages(prev => [...prev, message]);
}
};
return (
<PhantomProvider config={config} debugConfig={debugConfig}>
{/* Your app components */}
</PhantomProvider>
);
}
Debug callbacks receive a DebugMessage object:
interface DebugMessage {
timestamp: number; // Unix timestamp
level: DebugLevel; // Message level
category: string; // Component category
message: string; // Debug message text
data?: any; // Additional debug data (optional)
}
For more details and examples, see the @phantom/browser-sdk documentation.
FAQs
React hooks for integrating Phantom wallet functionality into React applications with chain-specific operations.
We found that @phantom/react-sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.