Hello! Welcome to our next lesson on building client-side applications for Solana.
In our last lesson, we saw how the Anchor client provides a powerful, streamlined way to build and send transactions. We used program.methods...rpc() to interact with an on-chain program from a TypeScript script. However, all our transactions were signed automatically by a local, file-based keypair. This is great for testing and backend scripts, but it's not how a decentralized application (dApp) works.
Today, we'll bridge that gap. We are moving from a script-based environment to a user-facing dApp. The goal is to have the user, through their own browser wallet like Phantom or Solflare, approve and sign the transactions our application builds.
This lesson will cover how to:
- Integrate the Solana Wallet Adapter into a React application.
- Use React hooks to get the connected user's wallet information.
- Use the connected wallet to sign and send transactions that we build with the Anchor client.
1. The Solana Wallet Adapter: Your dApp's Gateway to Wallets
To interact with a user's wallet, we can't just access it directly. We need a standard, secure way to request actions like signing. This is where the Solana Wallet Adapter comes in. It's a suite of libraries that provide a modular and consistent interface for dApps to communicate with many different wallets.
For a web developer like yourself, you can think of it as a plug-in system. You install the core adapter libraries, and then you add "plugins" for each wallet you want to support (Phantom, Solflare, Coinbase Wallet, etc.). The adapter handles the complex discovery and communication logic, exposing a simple API to your application.
Solana Smart Contract Tutorial: Using Phantom Wallet to create a DApp | React, Anchor
To start, let's get a high-level overview of the different libraries that make up the Wallet Adapter. This short clip from Josh's DevBox explains the three main pieces.
Watch from 05:20 to 07:04. Pay attention to the distinction between the core React adapter, the wallet-specific adapters, and the UI components.
2. Setting Up the dApp Environment
To make wallet state (like the connection status and user's public key) available throughout our entire React application, the Wallet Adapter uses React's Context API. This is a standard pattern in React for managing global state, which should be familiar to you.
We need to wrap our main application component with a few Provider components.
Getting started with Solana Wallet Adapter
This guide from the Coinbase developer documentation provides clear code examples for setting up the necessary providers. We'll use this as our reference.
Read 'Step 3: Configure wallets' and 'Step 5: Add wallet connection button'. Focus on how the WalletProvider is configured with a list of wallets and how the WalletModalProvider and WalletMultiButton are added to the component tree. This is the essential boilerplate for any dApp using the adapter.
Here’s a summary of the setup in a typical App.tsx file:
import { useMemo } from 'react';
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
import { WalletAdapterNetwork } from '@solana/wallet-adapter-base';
import { PhantomWalletAdapter, SolflareWalletAdapter } from '@solana/wallet-adapter-wallets';
import {
WalletModalProvider,
WalletMultiButton
} from '@solana/wallet-adapter-react-ui';
import { clusterApiUrl } from '@solana/web3.js';
// Default styles that can be overridden
require('@solana/wallet-adapter-react-ui/styles.css');
export const App = () => {
const network = WalletAdapterNetwork.Devnet;
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
const wallets = useMemo(
() => [
new PhantomWalletAdapter(),
new SolflareWalletAdapter(),
],
[network]
);
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>
{/* Your App's components go here */}
<div style={{ padding: '20px' }}>
<WalletMultiButton />
{/* We will add our transaction button here */}
</div>
</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
};
With this structure, any child component can now access the wallet's state. The WalletMultiButton is a pre-built component that handles the entire connect/disconnect UI flow for you.
3. Accessing the Wallet and Sending a Transaction
Once the providers are set up, we can use hooks provided by @solana/wallet-adapter-react to get the wallet's data and functions.
The two most important hooks are:
useConnection(): Returns a@solana/web3.jsConnectionobject for the currently configured RPC endpoint.useWallet(): Returns an object containing the wallet's state, including the user'spublicKey, a booleanconnectedflag, and functions to sign and send transactions.
The key to this lesson is understanding how these hooks connect back to the Anchor client we learned about previously.
In the last lesson, we created our AnchorProvider like this:
// Previous lesson: using a file-based keypair
const wallet = new Wallet(payerKeypair);
const provider = new AnchorProvider(connection, wallet, opts);
Now, in our dApp, the useWallet hook gives us an object that acts as our wallet. This object has the necessary methods (signTransaction, signAllTransactions) that AnchorProvider needs. When these methods are called, the Wallet Adapter routes the request to the user's connected browser wallet, triggering the pop-up for approval.
Here is the new flow:
- Inside your React component, get the
connectionfromuseConnection. - Get the
walletobject andpublicKeyfromuseWallet. - When a user clicks a button to perform an action:
a. Create anAnchorProviderusing theconnectionandwalletfrom the hooks.
b. Create yourProgramobject using this new provider.
c. Callprogram.methods.yourInstruction(...).rpc()exactly as before.
Because the provider is now configured with the user's wallet, the .rpc() call will automatically trigger the wallet pop-up for signing before sending the transaction.
Let's watch this entire flow in a detailed, practical example.
Solana Smart Contract Tutorial: Using Phantom Wallet to create a DApp | React, Anchor
This extended segment from Josh's DevBox builds a full dApp component that interacts with a counter program. It perfectly demonstrates how to wire up the wallet adapter hooks to the Anchor client.
Watch from 35:33 to 51:21. This is the core practical part of the lesson. As you watch, focus on these key steps inside the createCounter function: A provider is created from the connection and wallet hooks. The Anchor Program object is initialized with this provider. The program.rpc.initialize() call is made (this is older syntax for program.methods.initialize()...rpc()). A pop-up from Phantom appears, asking for transaction approval. After approval, the transaction is sent, and the code fetches the new account state to verify the result. Also, note the bug he finds around 49:00 related to variable scope—it's a very common issue in dApp development!
The user experience of this flow is the "magic" of a dApp. The frontend prepares a transaction, but the user remains in full control, using their private key (safely stored in their wallet) to authorize it.
Build and Deploy a Solana App in 10 Minutes | Full Frontend + Backend Tutorial
For a quick visual of this user experience, watch this short clip. It shows the wallet pop-up and confirmation flow from the user's perspective.
Watch from 05:12 to 05:54. Notice how clicking 'Create' on the web page triggers the wallet to pop up, requesting confirmation.
Test your understanding!
In the previous lesson, we used .signers([keypair]) when an instruction needed an extra signature (like for creating a new account). In a dApp, you don't have access to the user's private key. How does the useWallet hook solve the problem of signing? Does .rpc() sign with the user's key automatically?
Show answer
Yes, .rpc() automatically handles getting the user's signature. The wallet object from useWallet acts as a "signer". When AnchorProvider is created with this wallet object, any call to .rpc() will use it as the primary signer (the fee payer). The provider calls the wallet.signTransaction() method internally, which prompts the user for approval via the wallet extension. You don't need to do anything extra; the fee-paying signature is handled for you.
4. .rpc() vs. sendTransaction
It's important to clarify one point. The useWallet hook also returns a function called sendTransaction. This function takes a complete, pre-built Transaction object and asks the wallet to sign and send it.
So, why do we use Anchor's .rpc() instead?
Anchor's .rpc() is a higher-level convenience method that does several things for you:
- Builds the instruction from your
program.methodscall. - Fetches the latest blockhash.
- Assembles a
Transactionobject, setting the user's wallet as the fee payer. - Calls the provider's wallet to sign the transaction (triggering the pop-up).
- Sends the signed transaction to the network.
- Waits for the transaction to be confirmed based on the provider's commitment level.
You could achieve the same result manually by building the instruction with .instruction(), creating a Transaction, and then passing it to the sendTransaction function from useWallet. However, when using the Anchor client, letting .rpc() handle this entire lifecycle is much simpler and less error-prone.
Conclusion
You've now connected the dots between building transactions with Anchor and having a real user sign them in a dApp. This is a fundamental skill for any Solana developer.
Key Takeaways:
- The Solana Wallet Adapter is the essential middleware that connects your dApp to various user wallets.
- Setup involves wrapping your app in
ConnectionProviderandWalletProviderto make wallet state globally available. - The
useConnectionanduseWallethooks are the entry points for accessing the connection and user wallet within your components. - The
walletobject fromuseWalletcan be passed directly into anAnchorProvider, seamlessly integrating the user's wallet into the Anchor client workflow. - Calling
.rpc()on an Anchor program method will now automatically prompt the user to sign the transaction with their connected wallet.
In our next lesson, we will focus on what happens after you send a transaction. We'll explore how to confirm a transaction and handle different commitment levels (processed, confirmed, finalized), ensuring your dApp can reliably give feedback to the user about the status of their on-chain actions.
Can't find a good explanation? Sign up and we'll make it for you
Sign up