Hello! Welcome to the next step in your journey to becoming a Solana developer.
In our last lesson, we focused on the "read" part of the equation: fetching account data from the blockchain and deserializing it into a usable format. You now have the skills to inspect and understand the state of any account on Solana.
Today, we move to the "write" side. You will learn how to construct a Solana transaction in TypeScript, which is the fundamental way to request a state change on the blockchain. Our goal is to assemble a transaction containing one or more instructions, sign it, and prepare it to be sent to the network. This is the core mechanism behind every action, from transferring SOL to minting an NFT or interacting with a DeFi protocol.
1. The Anatomy of a Solana Transaction
Before we write any code, let's establish a clear mental model. A transaction is not a single action; it's a package that bundles everything the Solana network needs to process your request.
Solana Explained: Accounts, PDAs, CPIs & Anchor CRUD Demo
First, let's watch a very brief conceptual overview of what a transaction is. This clip from Francesco Ciulla's video 'Solana Explained' provides a perfect, concise definition.
Watch from 01:36 to 02:05 to understand how transactions relate to instructions and programs.
As the video explained, a transaction is an atomic container for one or more instructions. Let's break down its essential components.
Batch Transactions on Solana for Improved Efficiency
The article 'Batch Transactions on Solana for Improved Efficiency' provides an excellent breakdown of these components. We'll use it to understand the theory before moving to the modern code implementation.
Please read the sections 'What Are Batch Transactions?' and 'Key Concepts in Solana Transaction Batching'. This will define the core ideas of atomicity, instructions, accounts, signers, and transaction size limits.
To summarize the key points from the resources:
- Instructions: The specific actions you want to perform. Each instruction targets a program and includes the accounts it will read from or write to, along with any necessary data.
- Atomicity: This is a crucial feature. If a transaction contains three instructions and the third one fails, the entire transaction is reverted. The first two instructions will have no effect. It's all or nothing.
- Fee Payer: One account must be designated to pay the network fees for the transaction. This account's signature is always required.
- Signatures: Any instruction that debits an account or modifies data owned by an account requires a signature from that account's keypair. A transaction bundles all required signatures.
- Recent Blockhash: To prevent a transaction from being processed multiple times, it must include a recent blockhash. This effectively gives the transaction a short lifespan.
- Size Limit: A transaction has a maximum size of 1232 bytes. This limits how many instructions you can bundle together.
With this theory in place, let's start building.
2. Constructing a Transaction with @solana/web3.js v2
Many older tutorials and guides (including the code examples in the Oodles Blockchain article you just read) use the v1 syntax of @solana/web3.js. This typically looks like new Transaction().add(...).
We are using the modern, recommended v2 SDK, which adopts a more modular and functional approach. The core idea is to first build a TransactionMessage and then create a Transaction from that message.
The best way to see this in action is to watch an expert. We'll be referencing a fantastic tutorial that walks through this exact process.
Solana kit (forermly web3.js v2.0.0) officially released [Solana Tutorial] - Nov 12th '24
The video 'Solana kit (formerly web3.js v2.0.0) officially released' by Solandy is an in-depth guide to the new SDK. We will watch specific segments that demonstrate how to create instructions and assemble them into a transaction.
First, watch the segment from 24:17 to 27:30. This part shows how to create a basic SOL transfer instruction using the new transferSoulInstruction function from the modular @solana/program-system package. Pay close attention to how the instruction parameters are provided as a structured object. Next, watch from 36:42 to 41:05. This is the most critical part for our lesson. It demonstrates how to use the pipe function to assemble a TransactionMessage by adding instructions, setting a fee payer, and attaching a blockhash lifetime.
3. A Practical Step-by-Step Implementation
Let's translate what you just saw in the video into a complete, working script. We will construct a transaction that contains two separate SOL transfer instructions.
Step 1: Project Setup
Continue in the project you set up in the previous lessons. We need to install the system program package, which contains helpers for creating system instructions like transfers.
In your terminal, run:npm install @solana/program-system
Now, let's set up our src/index.ts file. Replace its contents with the following initial setup:
// src/index.ts
import {
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
createTransaction,
signTransaction,
sendAndConfirmTransaction,
address,
lamports,
} from '@solana/web3.js';
import { createKeypairFromSecretKey } from '@solana/keys';
import { getTransferSolInstruction } from '@solana/program-system';
import { base58 } from '@metaplex-foundation/umi/serializers'; // For readable signatures
// Use a local secret key for the payer/sender
// REPLACE WITH YOUR OWN SECRET KEY (e.g., from id.json or a new keypair)
const secret = new Uint8Array([
/* PASTE YOUR 64-BYTE SECRET KEY ARRAY HERE */
]);
const payer = createKeypairFromSecretKey(secret);
// The recipients of our transfers
const recipient1 = address('ENY9m5225zB1Vb721e5M41T2n81VruPj28oHeXG6y4Rk');
const recipient2 = address('3i5yq9z2v3dA7bXBpzvQxTzm3bWpLcyYgyk6i5aJ2Q2T');
const RPC_URL = 'https://api.devnet.solana.com';
async function main() {
// Setup connections
const rpc = createSolanaRpc(RPC_URL);
const rpcSubscriptions = createSolanaRpcSubscriptions(RPC_URL);
console.log(`Payer address: ${payer.address}`);
console.log(`Attempting to send SOL to:`);
console.log(` -> ${recipient1}`);
console.log(` -> ${recipient2}`);
// We will build our transaction here
try {
// 1. Airdrop SOL to the payer if needed
const balance = await rpc.getBalance(payer.address).send();
if (balance.value < lamports('0.5')) {
console.log('Airdropping 1 SOL to payer...');
await rpc.requestAirdrop(payer.address, lamports('1.0')).send();
}
console.log(`Payer balance: ${balance.value} lamports`);
// The rest of the logic will go here...
} catch (error) {
console.error('❌ An error occurred:', error);
}
}
main();
Action Required: Replace the empty secret array with your own secret key (a Uint8Array of 64 numbers). You can get this from your ~/.config/solana/id.json file or generate a new keypair. Your experience with front-end development means you're likely familiar with handling environment variables; for a real application, you would load this secret securely, not hardcode it.
Step 2: Create Instructions
Inside the try block, let's create two distinct transfer instructions. Each is a self-contained object.
// (Inside the `try` block of main function)
// 2. Create the transfer instructions
const transferToRecipient1 = getTransferSolInstruction({
source: payer.address,
destination: recipient1,
amount: lamports('0.1'), // Send 0.1 SOL
});
const transferToRecipient2 = getTransferSolInstruction({
source: payer.address,
destination: recipient2,
amount: lamports('0.2'), // Send 0.2 SOL
});
Notice how clear this is. The getTransferSolInstruction function takes a configuration object, making the code readable and less error-prone than passing ordered arguments.
Step 3: Build the Transaction Message
Now we assemble these instructions into a single transaction message using the pipe approach shown in the video. This is the core of the v2 construction process.
// (Continuing inside the `try` block)
// 3. Get a recent blockhash
const { value: blockhash } = await rpc.getLatestBlockhash().send();
// 4. Build the transaction message
const message = createTransactionMessage({
payerAddress: payer.address,
instructions: [transferToRecipient1, transferToRecipient2],
recentBlockhash: blockhash,
});
Here, we've simplified the pipe from the video. The createTransactionMessage function is a convenient helper that bundles the payer, instructions, and blockhash in one step. This is a common pattern for basic transactions.
Step 4: Create, Sign, and Send the Transaction
The message is just the content. Now we create the final Transaction object and sign it with the payer's keypair.
// (Continuing inside the `try` block)
// 5. Create a transaction from the message
let transaction = createTransaction({
message,
});
// 6. Sign the transaction with the payer
const signedTransaction = await signTransaction([payer], transaction);
// 7. Create a transaction sender
const send = createTransactionSender(rpc, rpcSubscriptions);
// 8. Send and confirm the transaction
const signature = await send(signedTransaction, {
commitment: 'confirmed',
});
// The signature is a byte array, so we encode it to base58 for readability
const [signatureBase58] = base58.serialize(signature);
console.log(`✅ Transaction successful!`);
console.log(` Signature: ${signatureBase58}`);
console.log(` View on Explorer: https://explorer.solana.com/tx/${signatureBase58}?cluster=devnet`);
Note: I've noticed a small inconsistency in the video regarding how sendAndConfirmTransaction is created. The code above uses a createTransactionSender factory, which is a clean way to prepare the sending function with your RPC connections. This is a robust pattern for v2.
Now, run your complete script: npx esrun src/index.ts.
If all goes well, you will see a success message with a transaction signature. You can copy this signature and paste it into the Solana Explorer to see the details. You will find that a single transaction resulted in two separate SOL transfers, demonstrating the power of atomicity!
Test your understanding!
Modify the script to perform a third action in the same transaction: send 50000 lamports to a newly generated keypair.
Hint: You will need to:
- Generate a new keypair inside the
mainfunction. - Create a third
getTransferSolInstruction. - Add this new instruction to the
instructionsarray when creating the transaction message.
Show answer
Here's how you could modify the main function:
// (Inside main function)
import { generateKeypair } from '@solana/keys'; // Add this to your imports
async function main() {
// ... (setup code)
try {
// ... (airdrop logic)
// Generate a new keypair on the fly
const newRecipient = generateKeypair();
console.log(` -> ${newRecipient.address} (newly generated)`);
// Create the transfer instructions
const transferToRecipient1 = getTransferSolInstruction({
source: payer.address,
destination: recipient1,
amount: lamports('0.1'),
});
const transferToRecipient2 = getTransferSolInstruction({
source: payer.address,
destination: recipient2,
amount: lamports('0.2'),
});
// The new instruction
const transferToNewRecipient = getTransferSolInstruction({
source: payer.address,
destination: newRecipient.address,
amount: 50000, // lamports
});
// Get a recent blockhash
const { value: blockhash } = await rpc.getLatestBlockhash().send();
// Build the transaction message with all three instructions
const message = createTransactionMessage({
payerAddress: payer.address,
instructions: [
transferToRecipient1,
transferToRecipient2,
transferToNewRecipient, // Add the third instruction
],
recentBlockhash: blockhash,
});
// ... (signing and sending logic remains the same)
} catch (error) {
console.error('❌ An error occurred:', error);
}
}
Conclusion
Congratulations! You have successfully composed, signed, and sent your first multi-instruction transaction on Solana using the modern @solana/web3.js SDK. This is a huge milestone.
Let's recap the key takeaways from this lesson:
- A Solana transaction is an atomic bundle of one or more instructions.
- The essential components are instructions, a fee payer, a recent blockhash, and the required signatures.
- The modern
@solana/web3.jsv2 SDK uses a functional, modular approach. Key packages include@solana/web3.js,@solana/keys, and program-specific helpers like@solana/program-system. - The core construction process involves creating individual instructions, building a
TransactionMessage, creating aTransactionfrom the message, and then signing it.
You now understand the manual process of building transactions from scratch. This gives you a deep appreciation for what happens under the hood.
In our next lesson, we will see how this process can be simplified. You will learn how to use an Anchor-generated client to build program instructions and transactions. Anchor abstracts away much of this manual construction, allowing you to interact with your own custom programs much more easily.
Can't find a good explanation? Sign up and we'll make it for you
Sign up