Konten ini sedang diterjemahkan dan akan tersedia di sini ketika siap.
Beyond Standard Operations
Token transfers and NFT operations use well-known programs with standardized instructions. But Solana's power comes from custom programs: DeFi protocols, gaming systems, social networks, all running on-chain.
Every Solana program follows the same pattern:
Receive an instruction with accounts and data
Validate the accounts
Process the data
Modify account state
Your mobile app builds these instructions, bundles them into transactions, and submits them. The program executes atomically on-chain.
Instruction Anatomy
An instruction has three parts:
import { TransactionInstruction, PublicKey } from "@solana/web3.js";
const instruction = new TransactionInstruction({
// 1. Which program to call
programId: new PublicKey("YourProgramAddress111111111111111111111111"),
// 2. Which accounts the program needs
keys: [
{ pubkey: userWallet, isSigner: true, isWritable: true },
{ pubkey: dataAccount, isSigner: false, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
],
// 3. What to do (program-specific binary data)
data: Buffer.from([/* instruction data */])
});Account Keys
Each account in keys has two flags:
isSigner: Must this account sign the transaction?isWritable: Will this account's data change?
Get these wrong and the transaction fails. The program validates them.
Instruction Data
The data field is program-specific. It typically contains:
A discriminator (which function to call)
Serialized arguments
Anchor programs use an 8-byte discriminator. Native programs vary.
Program Derived Addresses
PDAs are addresses derived from seeds; no private key exists. Programs use them for data storage and authority.
import { PublicKey } from "@solana/web3.js";
// Derive a PDA
const [pda, bump] = PublicKey.findProgramAddressSync(
[
Buffer.from("user-profile"), // A fixed string seed
userWallet.toBuffer(), // The user's public key
],
programId
);
// The bump ensures the address is off the curve (not a valid keypair)
console.log("PDA:", pda.toBase58());
console.log("Bump:", bump);Common PDA Patterns
// User-specific data
const [userProfile] = PublicKey.findProgramAddressSync(
[Buffer.from("profile"), userWallet.toBuffer()],
programId
);
// Global config
const [globalConfig] = PublicKey.findProgramAddressSync(
[Buffer.from("config")],
programId
);
// Item in a collection
const [item] = PublicKey.findProgramAddressSync(
[Buffer.from("item"), Buffer.from(itemId)],
programId
);Working with Anchor Programs
Most Solana programs use Anchor. The IDL (Interface Definition Language) describes available instructions.
Generating Client Code
From an IDL, you can generate TypeScript types:
# If you have the IDL
anchor idl init --filepath idl.json program_addressOr fetch it directly:
import { Program, AnchorProvider, Idl } from "@coral-xyz/anchor";
// Fetch IDL from chain
const idl = await Program.fetchIdl(programId, provider);
// Create program instance
const program = new Program(idl as Idl, programId, provider);
// Now you have typed methods
const tx = await program.methods
.initialize(arg1, arg2)
.accounts({
user: userWallet,
dataAccount: dataAccount,
systemProgram: SystemProgram.programId,
})
.rpc();Mobile-Friendly Pattern
On mobile with MWA, you typically can't use Anchor's .rpc() directly. Build the instruction instead:
// Build instruction without sending
const ix = await program.methods
.yourMethod(arg1, arg2)
.accounts({
user: userWallet,
// ... other accounts
})
.instruction();
// Now use the instruction in a transaction
const tx = new VersionedTransaction(
new TransactionMessage({
payerKey: userWallet,
recentBlockhash: blockhash,
instructions: [ix],
}).compileToV0Message()
);
// Sign with MWA
const signedTx = await transact(async (wallet) => {
const [signed] = await wallet.signTransactions({
transactions: [tx]
});
return signed;
});
// Send
await connection.sendTransaction(signedTx);Instruction Encoding
When you don't have Anchor, you encode instructions manually.
Borsh Serialization
Most programs use Borsh. Here's how to encode:
import * as borsh from "borsh";
// Define the schema
class MyInstruction {
instruction: number;
amount: bigint;
constructor(fields: { instruction: number; amount: bigint }) {
this.instruction = fields.instruction;
this.amount = fields.amount;
}
}
const schema = new Map([
[MyInstruction, {
kind: "struct",
fields: [
["instruction", "u8"],
["amount", "u64"]
]
}]
]);
// Encode
const instruction = new MyInstruction({
instruction: 0, // First instruction in the program
amount: BigInt(1_000_000_000) // 1 SOL in lamports
});
const data = borsh.serialize(schema, instruction);Anchor Discriminators
Anchor instructions start with an 8-byte discriminator derived from the instruction name:
import { sha256 } from "@noble/hashes/sha256";
function getAnchorDiscriminator(instructionName: string): Buffer {
const hash = sha256(`global:${instructionName}`);
return Buffer.from(hash.slice(0, 8));
}
// Usage
const discriminator = getAnchorDiscriminator("initialize");
const fullData = Buffer.concat([
discriminator,
borsh.serialize(argsSchema, args)
]);Multi-Instruction Transactions
Real operations often need multiple instructions:
async function createAndInitialize(
connection: Connection,
userWallet: PublicKey
): Promise<string> {
const newAccount = Keypair.generate();
const instructions = [
// 1. Create the account
SystemProgram.createAccount({
fromPubkey: userWallet,
newAccountPubkey: newAccount.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(dataSize),
space: dataSize,
programId: myProgramId,
}),
// 2. Initialize it with our program
new TransactionInstruction({
programId: myProgramId,
keys: [
{ pubkey: newAccount.publicKey, isSigner: false, isWritable: true },
{ pubkey: userWallet, isSigner: true, isWritable: false },
],
data: initializeInstructionData,
}),
];
const { blockhash } = await connection.getLatestBlockhash();
const message = new TransactionMessage({
payerKey: userWallet,
recentBlockhash: blockhash,
instructions,
}).compileToV0Message();
const tx = new VersionedTransaction(message);
// The new account must also sign
tx.sign([newAccount]);
// User signs with MWA
const signedTx = await transact(async (wallet) => {
const [signed] = await wallet.signTransactions({
transactions: [tx]
});
return signed;
});
return await connection.sendTransaction(signedTx);
}Address Lookup Tables
Complex transactions can exceed size limits. Address Lookup Tables (ALTs) compress account addresses:
import { AddressLookupTableAccount } from "@solana/web3.js";
// Fetch the lookup table
const lookupTableAddress = new PublicKey("YourLookupTable...");
const lookupTableAccount = await connection
.getAddressLookupTable(lookupTableAddress)
.then(res => res.value);
if (!lookupTableAccount) {
throw new Error("Lookup table not found");
}
// Use it in your transaction
const message = new TransactionMessage({
payerKey: userWallet,
recentBlockhash: blockhash,
instructions,
}).compileToV0Message([lookupTableAccount]); // Pass ALTs here
const tx = new VersionedTransaction(message);ALTs are especially useful for:
DeFi protocols with many token accounts
NFT operations across collections
Any transaction touching many accounts
Simulating Before Sending
Always simulate complex transactions:
async function simulateAndSend(
connection: Connection,
transaction: VersionedTransaction
): Promise<{ signature: string; logs: string[] }> {
// Simulate first
const simulation = await connection.simulateTransaction(transaction, {
sigVerify: false,
commitment: "confirmed",
});
if (simulation.value.err) {
console.error("Simulation logs:", simulation.value.logs);
throw new Error(`Simulation failed: ${JSON.stringify(simulation.value.err)}`);
}
console.log("Simulation successful. Logs:", simulation.value.logs);
// If simulation passed, send it
const signature = await connection.sendTransaction(transaction);
return {
signature,
logs: simulation.value.logs || [],
};
}Interpreting Simulation Logs
function parseSimulationLogs(logs: string[]): {
programCalls: string[];
errors: string[];
computeUnits: number | null;
} {
const programCalls: string[] = [];
const errors: string[] = [];
let computeUnits: number | null = null;
for (const log of logs) {
if (log.includes("invoke")) {
programCalls.push(log);
}
if (log.toLowerCase().includes("error")) {
errors.push(log);
}
const cuMatch = log.match(/consumed (\d+) of \d+ compute units/);
if (cuMatch) {
computeUnits = parseInt(cuMatch[1]);
}
}
return { programCalls, errors, computeUnits };
}Common Program Patterns
Initialize User Account
async function initializeUserAccount(
program: Program,
userWallet: PublicKey
): Promise<string> {
const [userAccount] = PublicKey.findProgramAddressSync(
[Buffer.from("user"), userWallet.toBuffer()],
program.programId
);
const ix = await program.methods
.initializeUser()
.accounts({
user: userWallet,
userAccount,
systemProgram: SystemProgram.programId,
})
.instruction();
// ... build transaction and sign with MWA
}Deposit to Vault
async function depositToVault(
program: Program,
userWallet: PublicKey,
amount: number
): Promise<string> {
const [vault] = PublicKey.findProgramAddressSync(
[Buffer.from("vault")],
program.programId
);
const userTokenAccount = await getAssociatedTokenAddress(
tokenMint,
userWallet
);
const vaultTokenAccount = await getAssociatedTokenAddress(
tokenMint,
vault,
true // allowOwnerOffCurve - vault is a PDA
);
const ix = await program.methods
.deposit(new BN(amount))
.accounts({
user: userWallet,
userTokenAccount,
vault,
vaultTokenAccount,
tokenProgram: TOKEN_PROGRAM_ID,
})
.instruction();
// ... build transaction and sign with MWA
}Error Handling
Program errors come in different forms:
try {
await connection.sendTransaction(signedTx);
} catch (error) {
// Transaction-level error
if (error.message.includes("Transaction simulation failed")) {
// Parse the logs for the actual error
const logs = error.logs || [];
const programError = logs.find(log =>
log.includes("Error") || log.includes("error")
);
console.log("Program error:", programError);
}
// Anchor error codes
if (error.error?.errorCode) {
console.log("Anchor error:", error.error.errorCode.name);
console.log("Message:", error.error.errorMessage);
}
// Custom program error
if (error.message.includes("custom program error")) {
const codeMatch = error.message.match(/custom program error: 0x([0-9a-f]+)/i);
if (codeMatch) {
const errorCode = parseInt(codeMatch[1], 16);
console.log("Custom error code:", errorCode);
}
}
}Key Takeaways
Instructions = program + accounts + data: Get all three right
PDAs don't have private keys: Programs derive them from seeds
Use Anchor's
.instruction()method to get instructions for MWA signingEncode data correctly: Borsh serialization with proper discriminators
Simulate first: Catch errors before spending fees
ALTs compress large transactions: Use them when you hit size limits
With these patterns, you can interact with any Solana program from mobile. The key is understanding the program's expected accounts and data format, usually documented in the IDL or program source.