Hello! Let's dive into our next lesson.
In our last session, we successfully built the user interface for our dApp, creating a form with controlled components to capture input for our Solana program. We now have the "what" – the data the user wants to send. Today, we'll focus on the "how" – taking that data, constructing a transaction, and submitting it to the blockchain. This is where your dApp truly comes to life, moving from a static front-end to an interactive application that can change on-chain state.
This lesson directly addresses the learning outcome: Construct and submit a program transaction based on user input from the UI. We'll see how the Anchor framework makes this process remarkably straightforward, connecting our React component's state directly to our on-chain Rust logic.
From UI State to Blockchain Instruction
Recall the update function from our last lesson's example. We had an <input> element bound to a state variable, let's call it input. The goal is to pass the value of input to our on-chain program.
With Anchor, this is primarily handled by the program.rpc object. Each function you define in your Rust #[program] block becomes a method on program.rpc that you can call from your client.
Let's look at the full-stack "message board" dApp from the guide we've been following. This will serve as our primary reference for this lesson.
The Complete Guide to Full Stack Solana Development ...
This guide by Nader Dabit provides a perfect, self-contained example of a React UI that captures user input and uses it to call a program instruction. We'll be focusing on the second example in the guide.
Please read the section titled 'Hello World part 2'. Pay close attention to both the Rust program and the React App.js code. Specifically, compare the update function in Rust with the async function update() in the React code. This comparison is the key to understanding today's lesson.
The Anatomy of an Anchor RPC Call
After reviewing the code, let's break down the most important line in the client-side update function:
await program.rpc.update(input, {
accounts: {
baseAccount: baseAccount.publicKey
}
});
This single call is doing a lot of work. Let's dissect it:
-
program.rpc.update(...): This is the Remote Procedure Call (RPC).updatedirectly corresponds to the public functionupdatein the Rust#[program]block. Anchor generates this client-side binding for you automatically. -
input: This is the first argument. It's theinputstate variable from our React component. Notice how it maps directly to thedata: Stringparameter in the Rust function signature:pub fn update(ctx: Context<Update>, data: String) -> ProgramResult. Anchor handles the serialization of this JavaScript string into the format the Rust program expects. -
{ accounts: { ... } }: This is the Context object. It corresponds to thectx: Context<Update>parameter in the Rust function. Theaccountsproperty within this object is where you provide the public keys for all the accounts defined in yourUpdatestruct in Rust.- In Rust:
pub base_account: Account<'info, BaseAccount> - In TypeScript:
baseAccount: baseAccount.publicKey
This mapping tells the Solana runtime which on-chain account it needs to load and pass into your program instruction.
- In Rust:
Your experience with function calls and passing arguments in JavaScript is directly applicable here. The main difference is that in addition to the data arguments (input), you must also provide the accounts context so the blockchain knows which state to operate on.
The User's Role: Signing the Transaction
When the await program.rpc.update(...) line is executed, something magical happens. Anchor constructs the transaction, but it can't send it alone. A transaction that changes data or transfers assets requires a signature from the fee payer and any other required signers.
Since our Provider was configured with the user's connected wallet, the Anchor library will automatically hand this constructed transaction over to the wallet. The wallet (e.g., Phantom) will then display a pop-up asking the user to review and approve the transaction.
This video provides a great visual for this exact moment.
Solana Tutorial: Writing a dApp to Work With PDA's | React, Next.js, Anchor
This video from Josh's DevBox demonstrates building a dApp. While the program is different, the client-side interaction pattern is the same. We'll watch the part where a transaction is actually sent.
Please watch the 'Live Demonstration of Transaction Submission and Confirmation' (timestamp 00:24:19 to 00:26:45). Observe what happens after the 'Create Transaction' button is clicked. You'll see the Phantom wallet pop up, asking for approval. This is the crucial step where the user signs the transaction your dApp has just built.
This wallet prompt is a cornerstone of Web3 security. Your dApp can only propose actions; the user always has the final say and must authorize any state change or expenditure of funds with their private key.
Test your understanding!
Imagine your Anchor program has the following instruction for creating a user profile:
// In the #[program] block
pub fn create_profile(ctx: Context<CreateProfile>, username: String, age: u8) -> ProgramResult { ... }
// The Accounts struct
#[derive(Accounts)]
pub struct CreateProfile<'info> {
#[account(init, payer = user, space = 96)]
pub profile_account: Account<'info, Profile>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program <'info, System>,
}
On your React client, you have the user's input in a state object: const [formData, setFormData] = useState({ username: 'solana-dev', age: 30 });. You also have the keypair for the new profile_account stored in const profileAccount = Keypair.generate();.
How would you write the program.rpc call to execute this instruction?
Show answer
The call would look like this:
await program.rpc.createProfile(formData.username, formData.age, {
accounts: {
profileAccount: profileAccount.publicKey,
user: provider.wallet.publicKey,
systemProgram: SystemProgram.programId,
},
signers: [profileAccount],
});
Explanation:
createProfile: Matches the Rust function name.formData.username,formData.age: The instruction arguments are passed in order.accounts: Maps the names from theCreateProfilestruct (profile_account,user,system_program) to their corresponding public keys on the client.signers: Because we are creating (init) a new account, the keypair for that new account must sign the transaction to authorize its creation. Theuser(fee payer) will be asked to sign automatically via the wallet provider.
Handling Success and Failure
The await keyword means your code will pause until the transaction is processed. But what happens then? The call can either succeed or fail. As a developer, you need to handle both cases to provide a good user experience.
This is where a try...catch block becomes essential.
Look again at the createCounter function in the first "Hello World" example from the Nader Dabit guide.
The Complete Guide to Full Stack Solana Development ...
Let's revisit the first 'Hello World' example from the same guide to see a clear implementation of error handling and UI updates.
Scroll to the section 'Building the React app' and examine the createCounter function inside the App.js code block. Notice the entire await program.rpc.create(...) call is wrapped in a try...catch block.
In the try block:
- The transaction is sent via
program.rpc. - After it succeeds, the code proceeds to
const account = await program.account.baseAccount.fetch(...). This re-fetches the account's data from the blockchain. - The UI state is then updated with the new data (
setValue(account.count.toString())), giving the user instant feedback that their action was successful.
In the catch (err) block:
console.log("Transaction error: ", err);is used to log the error. In a real application, you would use this block to display a user-friendly error message (e.g., "Transaction failed. Please try again.").
This try...catch pattern is fundamental for robust dApp development. A transaction can fail for many reasons: the user rejecting it in their wallet, insufficient funds for fees, or a logic error in your on-chain program. Gracefully handling these failures is critical.
Conclusion
Congratulations! You have now connected all the dots from user interface to on-chain execution. You can build a form, capture user input, and use that input to construct and submit a transaction that permanently alters the state of the Solana blockchain.
Key Takeaways:
program.rpcis the Bridge: Anchor'sprogram.rpc.instructionName()method is the primary tool for calling your on-chain program from the client.- Arguments and Context: An RPC call takes the instruction's data arguments first, followed by a context object containing the
accountsand any additionalsigners. - Automatic Signing: The
Providerobject automatically triggers the user's wallet to prompt for a signature, ensuring user control and security. - Update the UI on Success: After a successful transaction, you should re-fetch the on-chain data to reflect the new state in your dApp's UI.
- Handle Errors Gracefully: Always wrap transaction submissions in a
try...catchblock to manage potential failures and provide clear feedback to the user.
Preview of the Next Lesson:
When we await a transaction, what are we actually waiting for? The Solana network has different stages of transaction confirmation: processed, confirmed, and finalized. Understanding these "commitment levels" is crucial for building responsive and reliable applications. In our next lesson, we will dive into what these levels mean and how to use them to give your users precise and timely feedback on the status of their transactions.
Can't find a good explanation? Sign up and we'll make it for you
Sign up