Client Side Development
Most dApps use TypeScript to interact with deployed Solana programs. Understanding how to integrate your program client-side is essential for building functional applications.
Anchor Client SDK
Anchor simplifies client interaction with Solana programs through an Interface Description Language (IDL) file that mirrors your program's structure.
When combined with Anchor's TypeScript library (@coral-xyz/anchor), the IDL provides a streamlined approach to building instructions and transactions.
Setup
The @coral-xyz/anchor
package installs automatically when creating an Anchor program. After running anchor build, Anchor generates:
- An IDL at
target/idl/<program-name>.json
- A TypeScript SDK at
target/types/<program-name>.ts
These files abstract away much of the underlying complexity. Transfer them to your TypeScript client using this structure:
src
├── anchor
│ ├── <program-name>.json
│ └── <program-name>.ts
└── integration.ts
To use the wallet adapter with Anchor's TypeScript SDK, create a Provider object that combines the Connection (localhost, devnet, or mainnet) and the Wallet (the address that pays for and signs transactions).
Set up the Wallet and Connection:
import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react";
const { connection } = useConnection();
const wallet = useAnchorWallet();
Create the Provider object and set it as the default:
import { AnchorProvider, setProvider } from "@coral-xyz/anchor";
const provider = new AnchorProvider(connection, wallet, {
commitment: "confirmed",
});
setProvider(provider);
Program
Anchor's Program object creates a custom API for interacting with Solana programs. This API serves as the central interface for all onchain program communication:
- Send transactions,
- Fetch deserialized accounts,
- Decode instruction data,
- Subscribe to account changes,
- Listen to events
Create the Program object by importing the types and IDL:
import <program-name> from "./<program-name>.json";
import type { <Program-Type> } from "./<program-name>.ts";
import { Program, Idl } from "@coral-xyz/anchor";
const program = new Program(<program-name> as <Program-Type>);
If you haven't set a default provider, specify it explicitly:
const program = new Program(<program-name> as <Program-Type>, provider);
Once configured, use the Anchor Methods Builder to construct instructions and transactions. The MethodsBuilder
uses the IDL to provide a streamlined format for building transactions that invoke program instructions.
The basic MethodsBuilder
pattern:
await program.methods
.instructionName(instructionDataInputs)
.accounts({})
.signers([])
.rpc();
Pass additional signers beyond the provider using .signers()
.
Accounts
Use dot syntax to call .accounts
on the MethodsBuilder
, passing an object with each account the instruction expects based on the IDL.
Transactions
The default method for sending transactions through Anchor is .rpc()
, which sends the transaction directly to the blockchain.
For scenarios requiring backend signing (like creating a transaction on the frontend with the user's wallet, then securely signing with a backend keypair), use .transaction()
:
const transaction = await program.methods
.instructionName(instructionDataInputs)
.accounts({})
.transaction();
//... Sign the transaction in the backend
// Send the transaction to the chain
await sendTransaction(transaction, connection);
To bundle multiple Anchor instructions, use .instruction()
to get instruction objects:
// Create first instruction
const instructionOne = await program.methods
.instructionOneName(instructionOneDataInputs)
.accounts({})
.instruction();
// Create second instruction
const instructionTwo = await program.methods
.instructionTwoName(instructionTwoDataInputs)
.accounts({})
.instruction();
// Add both instructions to one transaction
const transaction = new Transaction().add(instructionOne, instructionTwo);
// Send transaction
await sendTransaction(transaction, connection);
Fetch and Filter Accounts
When your program creates hundreds of accounts, tracking them becomes challenging. The Program object provides methods to efficiently fetch and filter program accounts.
Fetch all addresses of a specific account type:
const accounts = await program.account.counter.all();
Filter specific accounts using the memcmp
flag:
const accounts = await program.account.counter.all([
{
memcmp: {
offset: 8,
bytes: bs58.encode(new BN(0, "le").toArray()),
},
},
]);
For checking if account data changed, fetch deserialized account data for a specific account using fetch:
const account = await program.account.counter.fetch(ACCOUNT_ADDRESS);
Fetch multiple accounts simultaneously:
const accounts = await program.account.counter.fetchMultiple([
ACCOUNT_ADDRESS_ONE,
ACCOUNT_ADDRESS_TWO,
]);
Events and Webhooks
Rather than fetching onchain data every time users connect their wallets, set up systems that listen to the blockchain and store relevant data in a database.
Two main approaches exist for listening to onchain events:
- Polling: The client repeatedly checks for new data at intervals. The server responds with the latest data regardless of changes, potentially returning duplicate information.
- Streaming: The server pushes data to the client only when updates occur. This provides more efficient, real-time data transfer since only relevant changes are transmitted.
For streaming Anchor instructions, use webhooks that listen to events and send them to your server when they occur. For example, update a database entry whenever an NFT sale happens on your marketplace.
Anchor provides two macros for emitting events:
emit!()
: Emits events directly to program logs using thesol_log_data()
syscall, encoding event data as base64 strings prefixed with "Program Data"emit_cpi!()
: Emits events through Cross Program Invocations (CPIs). Event data is encoded and included in the CPI's instruction data instead of program logs
emit!()
macro
Program implementation:
use anchor_lang::prelude::*;
declare_id!("8T7MsCZyzxboviPJg5Rc7d8iqEcDReYR2pkQKrmbg7dy");
#[program]
pub mod event {
use super::*;
pub fn emit_event(_ctx: Context<EmitEvent>, input: String) -> Result<()> {
emit!(CustomEvent { message: input });
Ok(())
}
}
#[derive(Accounts)]
pub struct EmitEvent {}
#[event]
pub struct CustomEvent {
pub message: String,
}
Client-side event listening with Anchor SDK helpers for base64 decoding:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Event } from "../target/types/event";
describe("event", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Event as Program<Event>;
it("Emits custom event", async () => {
// Set up listener before sending transaction
const listenerId = program.addEventListener("customEvent", event => {
// Process the event data
console.log("Event Data:", event);
});
});
});
emit_cpi!()
macro
Program implementation:
use anchor_lang::prelude::*;
declare_id!("2cDQ2LxKwQ8fnFUz4LLrZ157QzBnhPNeQrTSmWcpVin1");
#[program]
pub mod event_cpi {
use super::*;
pub fn emit_event(ctx: Context<EmitEvent>, input: String) -> Result<()> {
emit_cpi!(CustomEvent { message: input });
Ok(())
}
}
#[event_cpi]
#[derive(Accounts)]
pub struct EmitEvent {}
#[event]
pub struct CustomEvent {
pub message: String,
}
Client-side event decoding:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { EventCpi } from "../target/types/event_cpi";
describe("event-cpi", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.EventCpi as Program<EventCpi>;
it("Emits custom event", async () => {
// Fetch the transaction data
const transactionData = await program.provider.connection.getTransaction(
transactionSignature,
{ commitment: "confirmed" },
);
// Decode the event data from the CPI instruction data
const eventIx = transactionData.meta.innerInstructions[0].instructions[0];
const rawData = anchor.utils.bytes.bs58.decode(eventIx.data);
const base64Data = anchor.utils.bytes.base64.encode(rawData.subarray(8));
const event = program.coder.events.decode(base64Data);
console.log(event);
});
});