The Phantom Connect React Native SDK provides chain-specific hooks (useSolana and useEthereum) for signing and sending transactions optimized for mobile platforms.
Transaction security for embedded wallets: All transactions signed for embedded wallets pass through Phantom’s advanced simulation system before execution. This security layer automatically blocks malicious transactions and transactions from origins that have been reported as malicious, providing an additional layer of protection for your users’ assets.
Chain-specific transaction hooks
Solana transactions (useSolana)
import React from "react";
import { View, Button, Alert } from "react-native";
import { useSolana } from "@phantom/react-native-sdk";
function SolanaTransactions() {
const { solana } = useSolana();
const sendTransaction = async () => {
try {
// Sign and send transaction
const result = await solana.signAndSendTransaction(transaction);
Alert.alert("Success", `Transaction sent: ${result.hash}`);
} catch (error) {
Alert.alert("Error", `Transaction failed: ${error.message}`);
}
};
const signOnly = async () => {
try {
// Just sign (without sending)
const signedTx = await solana.signTransaction(transaction);
Alert.alert("Success", "Transaction signed!");
} catch (error) {
Alert.alert("Error", `Signing failed: ${error.message}`);
}
};
return (
<View style={{ padding: 20 }}>
<Button title="Send Transaction" onPress={sendTransaction} />
<Button title="Sign Only" onPress={signOnly} />
</View>
);
}
Ethereum transactions (useEthereum)
import React from "react";
import { View, Button, Alert } from "react-native";
import { useEthereum } from "@phantom/react-native-sdk";
function EthereumTransactions() {
const { ethereum } = useEthereum();
const sendTransaction = async () => {
try {
const result = await ethereum.sendTransaction({
to: "0x742d35Cc6634C0532925a3b8D4C8db86fB5C4A7E",
value: "1000000000000000000", // 1 ETH in wei
gas: "21000",
});
Alert.alert("Success", `ETH sent: ${result.hash}`);
} catch (error) {
Alert.alert("Error", `Transaction failed: ${error.message}`);
}
};
return (
<View style={{ padding: 20 }}>
<Button title="Send ETH" onPress={sendTransaction} />
</View>
);
}
Pass a presignTransaction callback to signAndSendTransaction for Solana transactions that need double signing, such as dapp fee payer flows. Calls without it proceed normally — it is never applied globally.
Phantom embedded wallets do not accept pre-signed transactions. If your use case requires a second signer (for example, your app as the fee payer), that signing must happen via this callback, after Phantom has constructed and validated the transaction.
presignTransaction only fires for Solana transactions via the embedded provider. EVM transactions are unaffected.
import React from "react";
import { View, Button, Alert } from "react-native";
import { useSolana, base64urlDecode, base64urlEncode } from "@phantom/react-native-sdk";
function SendWithFeeSponsor() {
const { solana } = useSolana();
const sendSponsored = async () => {
try {
const result = await solana.signAndSendTransaction(transaction, {
presignTransaction: async (tx, context) => {
// Send the transaction to your backend for fee payer signing
const response = await fetch("https://your-api.com/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
},
});
Alert.alert("Success", `Sponsored transaction sent: ${result.hash}`);
} catch (error) {
Alert.alert("Error", `Transaction failed: ${error.message}`);
}
};
const sendNormal = async () => {
try {
const result = await solana.signAndSendTransaction(transaction);
Alert.alert("Success", `Transaction sent: ${result.hash}`);
} catch (error) {
Alert.alert("Error", `Transaction failed: ${error.message}`);
}
};
return (
<View style={{ padding: 20, gap: 10 }}>
<Button title="Send (Dapp Pays Fees)" onPress={sendSponsored} />
<Button title="Send (User Pays Fees)" onPress={sendNormal} />
</View>
);
}
Never hold a fee payer keypair in frontend code. The presignTransaction callback runs on the device — use it to call your own backend, which holds the keypair securely and returns the partially-signed transaction.
Complete mobile examples
Solana transaction with mobile UI
import React, { useState } from "react";
import { View, Button, TextInput, Alert, Text, StyleSheet } from "react-native";
import { useSolana } from "@phantom/react-native-sdk";
import { Transaction, SystemProgram, PublicKey, LAMPORTS_PER_SOL, Connection } from "@solana/web3.js";
function SolanaMobileTransfer() {
const { solana } = useSolana();
const [recipient, setRecipient] = useState("");
const [amount, setAmount] = useState("0.001");
const [isLoading, setIsLoading] = useState(false);
const sendSOL = async () => {
if (!recipient || !amount) {
Alert.alert("Error", "Please fill in all fields");
return;
}
setIsLoading(true);
try {
// Get connection and recent blockhash
const connection = new Connection("https://api.mainnet-beta.solana.com");
const { blockhash } = await connection.getLatestBlockhash();
const fromAddress = await solana.getPublicKey();
const transferInstruction = SystemProgram.transfer({
fromPubkey: new PublicKey(fromAddress),
toPubkey: new PublicKey(recipient),
lamports: parseFloat(amount) * LAMPORTS_PER_SOL,
});
const transaction = new Transaction({
recentBlockhash: blockhash,
feePayer: new PublicKey(fromAddress),
}).add(transferInstruction);
const result = await solana.signAndSendTransaction(transaction);
Alert.alert(
"Success!",
`Sent ${amount} SOL\nTransaction: ${result.hash}`,
[{ text: "OK" }]
);
} catch (error) {
Alert.alert("Error", `Failed to send SOL: ${error.message}`);
} finally {
setIsLoading(false);
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>Send Solana</Text>
<TextInput
style={styles.input}
placeholder="Recipient Address"
value={recipient}
onChangeText={setRecipient}
multiline
/>
<TextInput
style={styles.input}
placeholder="Amount (SOL)"
value={amount}
onChangeText={setAmount}
keyboardType="decimal-pad"
/>
<Button
title={isLoading ? "Sending..." : "Send SOL"}
onPress={sendSOL}
disabled={isLoading}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
gap: 15,
},
title: {
fontSize: 20,
fontWeight: "bold",
marginBottom: 10,
},
input: {
borderWidth: 1,
borderColor: "#ccc",
borderRadius: 8,
padding: 12,
fontSize: 16,
},
});
By default, the user’s embedded wallet is the fee payer for all Solana transactions. The presignTransaction hook lets your app co-sign the transaction before the wallet signs it, enabling use cases like:
- Dapp-as-fee-payer — your app covers the transaction fee so users don’t need SOL
- Platform fees — add a fee instruction signed by your app’s keypair
- Multi-signer flows — any scenario where the app needs to sign alongside the user’s wallet
Phantom embedded wallets do not accept pre-signed transactions. If your use case requires a second signer, this hook is the only supported approach — your app’s signing must happen after Phantom has constructed and validated the transaction. This restriction does not apply to injected providers (e.g. the Phantom browser extension).
Pass presignTransaction directly to signAndSendTransaction for the specific calls that need it. Calls without it proceed normally — the function is never applied globally.
Example: app as fee payer
import { useSolana, base64urlDecode, base64urlEncode } from "@phantom/react-native-sdk";
import { Keypair, VersionedTransaction } from "@solana/web3.js";
// Your app's fee payer keypair (keep this on your backend in production)
const feePayerKeypair = Keypair.fromSecretKey(/* your fee payer secret key */);
function SendWithFeeSponsor() {
const { solana } = useSolana();
const sendSponsored = async () => {
const result = await solana.signAndSendTransaction(transaction, {
presignTransaction: async (tx, context) => {
// tx: base64url-encoded Solana transaction bytes
// context: { networkId: string, walletId: string }
// 1. Decode base64url → raw bytes
const txBytes = base64urlDecode(tx);
// 2. Deserialize
const versionedTx = VersionedTransaction.deserialize(txBytes);
// 3. Partially sign as fee payer — the user's wallet will sign next
versionedTx.sign([feePayerKeypair]);
// 4. Re-serialize → encode back to base64url
return base64urlEncode(versionedTx.serialize());
},
});
console.log("Transaction sent:", result.signature);
};
// This call has no presignTransaction — proceeds without any co-signing
const sendNormal = async () => {
const result = await solana.signAndSendTransaction(transaction);
console.log("Transaction sent:", result.signature);
};
}
The hook only fires for Solana transactions via the embedded provider. EVM transactions are unaffected.
Ethereum transaction with mobile UI
import React, { useState } from "react";
import { View, Button, TextInput, Alert, Text, StyleSheet } from "react-native";
import { useEthereum } from "@phantom/react-native-sdk";
function EthereumMobileTransfer() {
const { ethereum } = useEthereum();
const [recipient, setRecipient] = useState("");
const [amount, setAmount] = useState("0.001");
const [isLoading, setIsLoading] = useState(false);
const sendETH = async () => {
if (!recipient || !amount) {
Alert.alert("Error", "Please fill in all fields");
return;
}
setIsLoading(true);
try {
const weiAmount = (parseFloat(amount) * 1e18).toString(); // Convert ETH to wei
const result = await ethereum.sendTransaction({
to: recipient,
value: weiAmount,
gas: "21000",
});
Alert.alert(
"Success!",
`Sent ${amount} ETH\nTransaction: ${result.hash}`,
[{ text: "OK" }]
);
} catch (error) {
Alert.alert("Error", `Failed to send ETH: ${error.message}`);
} finally {
setIsLoading(false);
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>Send Ethereum</Text>
<TextInput
style={styles.input}
placeholder="Recipient Address (0x...)"
value={recipient}
onChangeText={setRecipient}
autoCapitalize="none"
/>
<TextInput
style={styles.input}
placeholder="Amount (ETH)"
value={amount}
onChangeText={setAmount}
keyboardType="decimal-pad"
/>
<Button
title={isLoading ? "Sending..." : "Send ETH"}
onPress={sendETH}
disabled={isLoading}
/>
</View>
);
}