Instructions and Messages
Instructions and messages are the core building blocks for interacting with programs on the Arch Network. They define what operations to perform and how to execute them atomically.
Overview
- Instructions define individual operations to be performed by a program
- Messages group instructions together for atomic execution
- Transactions contain messages with signatures for network submission
Instructions
An instruction specifies a program to call, accounts to use, and data to pass.
Structure
#![allow(unused)] fn main() { pub struct Instruction { pub program_id: Pubkey, // Program to execute pub accounts: Vec<AccountMeta>, // Required accounts pub data: Vec<u8>, // Instruction data } }
Creating Instructions
import { Instruction, AccountMeta } from '@saturnbtcio/arch-sdk';
// Create a basic instruction
const instruction = new Instruction({
programId: myProgramId,
accounts: [
{
pubkey: userAccount,
isSigner: true,
isWritable: true
},
{
pubkey: dataAccount,
isSigner: false,
isWritable: true
}
],
data: Buffer.from([1, 2, 3, 4]) // Serialized instruction data
});
Account Metadata
Each instruction must specify how accounts will be used:
interface AccountMeta {
pubkey: Pubkey; // Account's public key
isSigner: boolean; // Must the account sign the transaction?
isWritable: boolean; // Can the account's data be modified?
}
Permission Examples
// Signer and writable (user's main account)
{
pubkey: userPublicKey,
isSigner: true,
isWritable: true
}
// Read-only reference (program or config account)
{
pubkey: configAccount,
isSigner: false,
isWritable: false
}
// Writable but not signer (data account owned by program)
{
pubkey: dataAccount,
isSigner: false,
isWritable: true
}
Instruction Data
Instruction data is typically serialized using Borsh or similar formats:
// Define instruction enum
enum MyProgramInstruction {
Initialize = 0,
UpdateValue = 1,
Close = 2
}
// Serialize instruction data
function createUpdateInstruction(newValue: number): Buffer {
const data = Buffer.alloc(9); // 1 byte for instruction + 8 bytes for u64
data.writeUInt8(MyProgramInstruction.UpdateValue, 0);
data.writeBigUInt64LE(BigInt(newValue), 1);
return data;
}
// Use in instruction
const instruction = new Instruction({
programId: myProgramId,
accounts: [/* ... */],
data: createUpdateInstruction(42)
});
Messages
Messages group instructions for atomic execution and include transaction metadata.
Structure
#![allow(unused)] fn main() { pub struct Message { pub signers: Vec<Pubkey>, // Required signers pub instructions: Vec<Instruction>, // Instructions to execute } }
Creating Messages
import { Message, Transaction } from '@saturnbtcio/arch-sdk';
// Create transaction with multiple instructions
const transaction = new Transaction()
.add(instruction1)
.add(instruction2)
.add(instruction3);
// The transaction internally creates a message
const message = transaction.compileMessage();
Atomic Execution
All instructions in a message execute atomically - if any instruction fails, the entire transaction fails:
// These instructions will all succeed or all fail together
const transaction = new Transaction()
.add(transferInstruction) // Transfer tokens
.add(updateAccountInstruction) // Update account data
.add(logInstruction); // Log the operation
Building Transactions
Simple Transaction
// Single instruction transaction
const transaction = new Transaction()
.add(instruction);
// Sign and send
const signature = await connection.sendAndConfirmTransaction(
transaction,
[keypair]
);
Multi-Instruction Transaction
// Complex transaction with multiple operations
const transaction = new Transaction()
.add(createAccountInstruction)
.add(initializeAccountInstruction)
.add(transferInstruction);
// Sign with multiple keypairs if needed
const signature = await connection.sendAndConfirmTransaction(
transaction,
[payer, newAccount, authority]
);
Transaction Limits
// Check transaction size before sending
const messageSize = transaction.compileMessage().serialize().length;
const maxSize = 1232; // Current limit
if (messageSize > maxSize) {
throw new Error(`Transaction too large: ${messageSize} bytes`);
}
Common Patterns
System Program Operations
import { SystemProgram } from '@saturnbtcio/arch-sdk';
// Create account
const createInstruction = SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: newAccount.publicKey,
lamports: 1000000,
space: 256,
programId: myProgramId
});
// Transfer lamports
const transferInstruction = SystemProgram.transfer({
fromPubkey: sender.publicKey,
toPubkey: recipient.publicKey,
lamports: 1000000
});
Program-Specific Instructions
// Create helper functions for your program
class MyProgram {
static initialize(
account: Pubkey,
authority: Pubkey,
initialValue: number
): Instruction {
return new Instruction({
programId: MY_PROGRAM_ID,
accounts: [
{ pubkey: account, isSigner: false, isWritable: true },
{ pubkey: authority, isSigner: true, isWritable: false }
],
data: Buffer.from([0, ...new BN(initialValue).toArray('le', 8)])
});
}
static updateValue(
account: Pubkey,
authority: Pubkey,
newValue: number
): Instruction {
return new Instruction({
programId: MY_PROGRAM_ID,
accounts: [
{ pubkey: account, isSigner: false, isWritable: true },
{ pubkey: authority, isSigner: true, isWritable: false }
],
data: Buffer.from([1, ...new BN(newValue).toArray('le', 8)])
});
}
}
// Use the helper functions
const initInstruction = MyProgram.initialize(
dataAccount.publicKey,
authority.publicKey,
100
);
Cross-Program Invocation (CPI)
Programs can call other programs using CPI:
#![allow(unused)] fn main() { // In your program use arch_sdk::{invoke, invoke_signed}; // Invoke another program invoke( &instruction, &[account1, account2, program_account] )?; // Invoke with program-derived address invoke_signed( &instruction, &[account1, account2, program_account], &[&[b"seed", &[bump]]] )?; }
Error Handling
Client-Side Validation
// Validate instruction before sending
function validateInstruction(instruction: Instruction): void {
if (!instruction.programId) {
throw new Error('Program ID is required');
}
if (instruction.accounts.length === 0) {
throw new Error('At least one account is required');
}
// Check for required signers
const hasRequiredSigner = instruction.accounts.some(
account => account.isSigner
);
if (!hasRequiredSigner) {
throw new Error('At least one signer is required');
}
}
Transaction Errors
try {
const signature = await connection.sendAndConfirmTransaction(
transaction,
[keypair]
);
} catch (error) {
if (error instanceof TransactionError) {
console.error('Transaction failed:', error.message);
console.error('Logs:', error.logs);
} else {
console.error('Unexpected error:', error);
}
}
Best Practices
Security
- Validate all accounts: Ensure accounts have correct permissions
- Check program ownership: Verify accounts are owned by expected programs
- Sanitize instruction data: Validate all input parameters
- Use type-safe serialization: Prefer Borsh or similar libraries
Performance
- Batch operations: Group related instructions in single transactions
- Minimize account usage: Only include necessary accounts
- Cache program IDs: Store frequently used program IDs as constants
- Optimize instruction data: Use efficient serialization formats
Development
- Create instruction builders: Build helper functions for common operations
- Document instruction formats: Clearly document expected data formats
- Test edge cases: Test with invalid accounts, insufficient funds, etc.
- Use TypeScript: Take advantage of type safety
Advanced Patterns
Conditional Instructions
// Build instructions based on conditions
const instructions: Instruction[] = [];
// Always initialize
instructions.push(initializeInstruction);
// Conditionally add operations
if (shouldTransfer) {
instructions.push(transferInstruction);
}
if (shouldClose) {
instructions.push(closeInstruction);
}
// Build transaction
const transaction = new Transaction();
instructions.forEach(ix => transaction.add(ix));
Instruction Factories
// Create reusable instruction factories
interface TokenTransferParams {
source: Pubkey;
destination: Pubkey;
authority: Pubkey;
amount: number;
}
function createTokenTransferInstruction(
params: TokenTransferParams
): Instruction {
return new Instruction({
programId: TOKEN_PROGRAM_ID,
accounts: [
{ pubkey: params.source, isSigner: false, isWritable: true },
{ pubkey: params.destination, isSigner: false, isWritable: true },
{ pubkey: params.authority, isSigner: true, isWritable: false }
],
data: encodeTokenTransferData(params.amount)
});
}
Examples
For complete examples using instructions and messages, see:
- Hello World - Basic instruction usage
- Counter - State management instructions
- Token Program - Complex instruction patterns
- Escrow - Multi-party transaction coordination
Source Code
The instruction and message implementations are available in the Arch Examples Repository.