This package provides React hooks for conveniently accessing embedded wallet functionality.
Built on top of @coinbase/cdp-core
, it offers a React-friendly interface for end user authentication
and embedded wallet operations.
This guide will help you get started with @coinbase/cdp-hooks. You'll learn how to install the package, set up the provider, and use the hooks in both web and React Native applications.
For web applications, add the package to your project using your preferred package manager:
# With npm
npm install @coinbase/cdp-core @coinbase/cdp-hooks
# With pnpm
pnpm add @coinbase/cdp-core @coinbase/cdp-hooks
# With yarn
yarn add @coinbase/cdp-core @coinbase/cdp-hooks
For React Native applications, you'll need additional crypto polyfills and dependencies:
# Core packages
npm install @coinbase/cdp-core @coinbase/cdp-hooks
# Install this polyfill with expo for better compatibility
npx expo install react-native-quick-crypto
# Required crypto polyfills for React Native
npm install react-native-get-random-values @ungap/structured-clone
# AsyncStorage for React Native storage
npm install @react-native-async-storage/async-storage
React Native Setup Code
You'll need to initialize the crypto polyfills before importing your app. Create or update your entry point file (typically index.js
or index.ts
):
import structuredClone from "@ungap/structured-clone";
import { install } from "react-native-quick-crypto";
import "react-native-get-random-values";
// Install crypto polyfills
if (!("structuredClone" in globalThis)) {
globalThis.structuredClone = structuredClone as any;
}
install(); // Install react-native-quick-crypto
// Import your app after polyfills are installed
import App from "./App";
// Register your app component
import { registerRootComponent } from "expo"; // For Expo apps
registerRootComponent(App);
Why these dependencies?
react-native-quick-crypto
: Provides Web Crypto API compatibility for asymmetric key generation (ECDSA, RSA) required for JWT signing and encryptionreact-native-get-random-values
: Provides secure random number generation via crypto.getRandomValues()
@ungap/structured-clone
: Polyfills structuredClone
for object cloning compatibility@react-native-async-storage/async-storage
: Provides persistent storage for auth tokens and secretshttp://localhost:3000
Next, you need to wrap your application with the CDPHooksProvider, which provides the necessary context for hooks to work correctly.
Update your main application file (e.g., main.tsx) to include the provider:
import React from "react";
import { CDPHooksProvider } from "@coinbase/cdp-hooks";
import { App } from './App'; // Your main App component
function App() {
return (
<CDPHooksProvider
config={{
// Copy and paste your project ID here.
projectId: "your-project-id",
}}
>
<App />
</CDPHooksProvider>
);
}
For React Native, the setup is identical.
import React from "react";
import { CDPHooksProvider } from "@coinbase/cdp-hooks";
import { App } from "./App";
export default function App() {
return (
<CDPHooksProvider config={{
projectId: "your-project-id",
}}>
<App />
</CDPHooksProvider>
);
}
You can configure the provider to automatically create Smart Accounts for new users:
function App() {
return (
<CDPHooksProvider
config={{
projectId: "your-project-id",
createAccountOnLogin: "evm-smart", // Creates Smart Accounts instead of EOAs
}}
>
<App />
</CDPHooksProvider>
);
}
createAccountOnLogin
is set to "evm-smart"
, new users will automatically get both an EOA and a Smart Account.End user authentication proceeds in two steps:
flowId
flowId
, after which the user will be authenticated, returning a User object.import { useSignInWithEmail, useVerifyEmailOTP } from "@coinbase/cdp-hooks";
function SignIn() {
const { signInWithEmail } = useSignInWithEmail();
const { verifyEmailOTP } = useVerifyEmailOTP();
const handleSignIn = async (email: string) => {
try {
// Start sign in flow
const { flowId } = await signInWithEmail({ email });
// In a real application, you would prompt the user for the OTP they received
// in their email. Here, we hardcode it for convenience.
const otp = "123456";
// Complete sign in
const { user, isNewUser } = await verifyEmailOTP({
flowId,
otp
});
console.log("Signed in user:", user);
console.log("User EVM address (EOA):", user.evmAccounts[0]);
console.log("User Smart Account:", user.evmSmartAccounts?.[0]);
} catch (error) {
console.error("Sign in failed:", error);
}
};
return <button onClick={() => handleSignIn("user@example.com")}>Sign In</button>;
}
For React Native, you'll use native UI components and handle the sign-in flow similarly:
import React, { useState } from "react";
import { View, Text, TextInput, TouchableOpacity, Alert } from "react-native";
import { useSignInWithEmail, useVerifyEmailOTP } from "@coinbase/cdp-hooks";
function SignInScreen() {
const { signInWithEmail } = useSignInWithEmail();
const { verifyEmailOTP } = useVerifyEmailOTP();
const [email, setEmail] = useState("");
const [otp, setOtp] = useState("");
const [flowId, setFlowId] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleSignIn = async () => {
if (!email) {
Alert.alert("Error", "Please enter an email address");
return;
}
setIsLoading(true);
try {
const result = await signInWithEmail({ email });
setFlowId(result.flowId);
Alert.alert("Success", "OTP sent to your email!");
} catch (error) {
Alert.alert("Error", error instanceof Error ? error.message : "Failed to sign in");
} finally {
setIsLoading(false);
}
};
const handleVerifyOTP = async () => {
if (!otp || !flowId) {
Alert.alert("Error", "Please enter the OTP");
return;
}
setIsLoading(true);
try {
const { user } = await verifyEmailOTP({ flowId, otp });
Alert.alert("Success", "Successfully signed in!");
console.log("Signed in user:", user);
} catch (error) {
Alert.alert("Error", error instanceof Error ? error.message : "Failed to verify OTP");
} finally {
setIsLoading(false);
}
};
return (
<View style={{ padding: 20 }}>
<Text>Email:</Text>
<TextInput
value={email}
onChangeText={setEmail}
placeholder="Enter your email"
keyboardType="email-address"
autoCapitalize="none"
editable={!isLoading}
style={{ borderWidth: 1, borderColor: "#ddd", padding: 12, marginBottom: 16 }}
/>
<TouchableOpacity
onPress={handleSignIn}
disabled={isLoading}
style={{
backgroundColor: "#007AFF",
padding: 15,
borderRadius: 8,
alignItems: "center",
marginBottom: 12,
opacity: isLoading ? 0.6 : 1,
}}
>
<Text style={{ color: "white", fontSize: 16, fontWeight: "600" }}>
{isLoading ? "Sending..." : "Sign In with Email"}
</Text>
</TouchableOpacity>
{flowId && (
<>
<Text>Enter OTP from email:</Text>
<TextInput
value={otp}
onChangeText={setOtp}
placeholder="Enter 6-digit OTP"
keyboardType="number-pad"
maxLength={6}
editable={!isLoading}
style={{ borderWidth: 1, borderColor: "#ddd", padding: 12, marginBottom: 16 }}
/>
<TouchableOpacity
onPress={handleVerifyOTP}
disabled={isLoading}
style={{
backgroundColor: "#007AFF",
padding: 15,
borderRadius: 8,
alignItems: "center",
opacity: isLoading ? 0.6 : 1,
}}
>
<Text style={{ color: "white", fontSize: 16, fontWeight: "600" }}>
{isLoading ? "Verifying..." : "Verify OTP"}
</Text>
</TouchableOpacity>
</>
)}
</View>
);
}
Once the end user has signed in, you can display their information in your application:
import { useCurrentUser, useEvmAddress } from "@coinbase/cdp-hooks";
function UserInformation() {
const { currentUser: user } = useCurrentUser();
const { evmAddress } = useEvmAddress();
if (!user) {
return <div>Please sign in</div>;
}
const emailAddress = user.authenticationMethods.email?.email;
return (
<div>
<h2>User Information</h2>
<p>User ID: {user.userId}</p>
<p>EVM Address (EOA): {evmAddress}</p>
{user.evmSmartAccounts?.[0] && (
<p>Smart Account: {user.evmSmartAccounts[0]}</p>
)}
{ email && <p>EmailAddress: {emailAddress}</p>}
</div>
);
}
We support signing and sending a Blockchain transaction in a single action on the following networks:
import { useSendEvmTransaction, useEvmAddress } from "@coinbase/cdp-hooks";
function SendTransaction() {
const { sendEvmTransaction: sendTransaction } = useSendEvmTransaction();
const { evmAddress } = useEvmAddress();
const handleSend = async () => {
if (!evmAddress) return;
try {
const result = await sendTransaction({
evmAccount: evmAddress,
transaction: {
to: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
value: 100000000000000n, // 0.0001 ETH in wei
nonce: 0,
gas: 21000n,
maxFeePerGas: 30000000000n,
maxPriorityFeePerGas: 1000000000n,
chainId: 84532, // Base Sepolia
type: "eip1559",
}
});
console.log("Transaction hash:", result.transactionHash);
} catch (error) {
console.error("Transaction failed:", error);
}
};
return <button onClick={handleSend}>Send Transaction</button>;
}
For networks other than those supported by the CDP APIs, your end user must sign the transaction, and then
you must broadcast the transaction yourself. This example uses the public client from viem
to broadcast the transaction.
import { useSignEvmTransaction, useEvmAddress } from "@coinbase/cdp-hooks";
import { http, createPublicClient } from "viem";
import { tron } from "viem/chains";
function CrossChainTransaction() {
const { signEvmTransaction: signTransaction } = useSignEvmTransaction();
const { evmAddress } = useEvmAddress();
const handleSend = async () => {
if (!evmAddress) return;
try {
// Sign the transaction
const { signedTransaction } = await signTransaction({
evmAccount: evmAddress,
transaction: {
to: "0x...",
value: 100000000000000n,
nonce: 0,
gas: 21000n,
maxFeePerGas: 30000000000n,
maxPriorityFeePerGas: 1000000000n,
chainId: 728126428, // Tron
type: "eip1559",
}
});
// Broadcast using a different client
const client = createPublicClient({
chain: tron,
transport: http()
});
const hash = await client.sendRawTransaction({
serializedTransaction: signedTransaction
});
console.log("Transaction hash:", hash);
} catch (error) {
console.error("Transaction failed:", error);
}
};
return <button onClick={handleSend}>Send Transaction</button>;
}
End users can sign EVM messages, hashes, and typed data to generate signatures for various onchain applications.
import { useSignEvmMessage, useSignEvmTypedData, useEvmAddress } from "@coinbase/cdp-hooks";
function SignData() {
const { signEvmMessage: signMessage } = useSignEvmMessage();
const { signEvmTypedData: signTypedData } = useSignEvmTypedData();
const { signEvmHash: signHash } = useSignEvmHash();
const { evmAddress } = useEvmAddress();
const handleSignHash = async () => {
if (!evmAddress) return;
const result = await signMessage({
evmAccount: evmAddress,
message: "Hello World"
});
console.log("Message signature:", result.signature);
}
const handleSignMessage = async () => {
if (!evmAddress) return;
const result = await signMessage({
evmAccount: evmAddress,
message: "Hello World"
});
console.log("Message signature:", result.signature);
};
const handleSignTypedData = async () => {
if (!evmAddress) return;
const result = await signTypedData({
evmAccount: evmAddress,
typedData: {
domain: {
name: "Example DApp",
version: "1",
chainId: 84532,
},
types: {
Person: [
{ name: "name", type: "string" },
{ name: "wallet", type: "address" }
]
},
primaryType: "Person",
message: {
name: "Bob",
wallet: evmAddress
}
}
});
console.log("Typed data signature:", result.signature);
};
return (
<div>
<button onClick={handleSignMessage}>Sign Message</button>
<button onClick={handleSignTypedData}>Sign Typed Data</button>
<button onClick={handleSignHash}>Sign Hash</button>
</div>
);
}
End users can export their private keys from their embedded wallet, allowing them to import it into an EVM-compatible wallet of their choice.
import { useExportEvmAccount, useEvmAddress } from "@coinbase/cdp-hooks";
function ExportKey() {
const { exportEvmAccount: exportAccount } = useExportEvmAccount();
const { evmAddress } = useEvmAddress();
const handleExport = async () => {
if (!evmAddress) return;
try {
const { privateKey } = await exportAccount({
evmAccount: evmAddress
});
console.log("Private Key:", privateKey);
// Warning: Handle private keys with extreme care!
} catch (error) {
console.error("Export failed:", error);
}
};
return <button onClick={handleExport}>Export Private Key</button>;
}
Smart Accounts provide advanced account abstraction features with React hooks.
Send user operations from Smart Accounts with support for multiple calls and paymaster sponsorship. The hook returns a method to execute the user operation and status
, data
, and error
properties to read the result of the user operation:
import { useSendUserOperation, useCurrentUser } from "@coinbase/cdp-hooks";
function SendUserOperation() {
const { sendUserOperation, status, data, error } = useSendUserOperation();
const { currentUser } = useCurrentUser();
const handleSendUserOperation = async () => {
const smartAccount = currentUser?.evmSmartAccounts?.[0];
if (!smartAccount) return;
try {
// This will automatically start tracking the user operation status
const result = await sendUserOperation({
evmSmartAccount: smartAccount,
network: "base-sepolia",
calls: [{
to: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
value: 1000000000000000000n,
data: "0x",
}],
});
console.log("User Operation Hash:", result.userOperationHash);
} catch (error) {
console.error("Failed to send user operation:", error);
}
};
return (
<div>
{status === "idle" && <p>Ready to send user operation</p>}
{status === "pending" && (
<div>
<p>User operation pending...</p>
{data && <p>User Op Hash: {data.userOpHash}</p>}
</div>
)}
{status === "success" && data && (
<div>
<p>User operation successful!</p>
<p>Transaction Hash: {data.transactionHash}</p>
<p>Status: {data.status}</p>
</div>
)}
{status === "error" && (
<div>
<p>User operation failed</p>
<p>Error: {error?.message}</p>
</div>
)}
<button onClick={handleSendUserOperation} disabled={status === "pending"}>
{status === "pending" ? "Sending..." : "Send User Operation"}
</button>
</div>
);
}
Use the useWaitForUserOperation
hook to poll for user operation status and provide real-time updates. This hook immediately fires off a query to get the result of the user operation:
import { useWaitForUserOperation, useState } from "react";
function WaitForUserOperation() {
const { status, data, error } = useWaitForUserOperation({
userOperationHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
evmSmartAccount: "0x1234567890123456789012345678901234567890",
network: "base-sepolia"
});
return (
<div>
{status === "idle" && <p>No user operation being tracked</p>}
{status === "pending" && (
<div>
<p>User operation pending...</p>
{data && <p>User Op Hash: {data.userOpHash}</p>}
</div>
)}
{status === "success" && data && (
<div>
<p>User operation successful!</p>
<p>Transaction Hash: {data.transactionHash}</p>
<p>Status: {data.status}</p>
</div>
)}
{status === "error" && (
<div>
<p>User operation failed</p>
<p>Error: {error?.message}</p>
</div>
)}
</div>
);
}
You can control when the useWaitForUserOperation
hook should start polling using the enabled
parameter:
function ConditionalWaitForUserOperation() {
const [shouldPoll, setShouldPoll] = useState(false);
const { status, data, error } = useWaitForUserOperation({
userOperationHash: "0x1234...",
evmSmartAccount: "0x5678...",
network: "base-sepolia",
enabled: shouldPoll // Only poll when this is true
});
return (
<div>
<button onClick={() => setShouldPoll(true)}>
Start Polling
</button>
<button onClick={() => setShouldPoll(false)}>
Stop Polling
</button>
<p>Status: {status}</p>
{data && <p>User Operation Status: {data.status}</p>}
{error && <p>Error: {error.message}</p>}
</div>
);
}