articleAug 1, 2022
Solana programs and Web3 API
Walkthrough of Solana web3.js patterns: connect, keypair, airdrop, transfer, deploy a hello-world program, create greeting accounts, and read/write on-chain state.

Solana programs and Web3 API
import type {NextApiRequest, NextApiResponse} from 'next';
import {getNodeURL} from '@figment-solana/lib';
import {Connection} from '@solana/web3.js';
export default async function connect(
req: NextApiRequest,
res: NextApiResponse<string>,
) {
try {
const {network} = req.body;
const url = getNodeURL(network);
const connection = new Connection(url);
const version = await connection.getVersion();
res.status(200).json(version['solana-core']);
} catch (error) {
let errorMessage = error instanceof Error ? error.message : 'Unknown Error';
res.status(500).json(errorMessage);
}
}- Third-party JSON-RPC endpoints act as the Web3 provider.
getNodeURL(network)returns the RPC URL;@solana/web3.jsConnectiontalks to it and can reportsolana-coreversion.
Generate a keypair
import type {NextApiRequest, NextApiResponse} from 'next';
import {Keypair} from '@solana/web3.js';
type ResponseT = {
secret: string;
address: string;
};
export default function keypair(
_req: NextApiRequest,
res: NextApiResponse<string | ResponseT>,
) {
try {
const keypair = new Keypair();
const address = keypair.publicKey.toString();
const secret = JSON.stringify(Array.from(keypair.secretKey));
res.status(200).json({
secret,
address,
});
} catch (error) {
let errorMessage = error instanceof Error ? error.message : 'Unknown Error';
res.status(500).json(errorMessage);
}
}
Solana keypairs use Ed25519. The public key is the address; the secret key is the signing material.
SOL balance and airdrop
Token names and faucet behavior differ by network (mainnet vs testnet/devnet).
1 SOL = 1,000,000,000 lamportsimport {Connection, PublicKey, LAMPORTS_PER_SOL} from '@solana/web3.js';
import type {NextApiRequest, NextApiResponse} from 'next';
import {getNodeURL} from '@figment-solana/lib';
export default async function fund(
req: NextApiRequest,
res: NextApiResponse<string>,
) {
try {
const {network, address} = req.body;
const url = getNodeURL(network);
const connection = new Connection(url, 'confirmed');
const publicKey = new PublicKey(address);
const hash = await connection.requestAirdrop(publicKey, LAMPORTS_PER_SOL);
await connection.confirmTransaction(hash);
res.status(200).json(hash);
} catch (error) {
let errorMessage = error instanceof Error ? error.message : 'Unknown Error';
res.status(500).json(errorMessage);
}
}- Build a
PublicKeyfrom the address. Connection.requestAirdrop(pubkey, LAMPORTS_PER_SOL)requests 1 SOL.- Await
confirmTransactionbefore treating the airdrop as settled.
Check balance
import type {NextApiRequest, NextApiResponse} from 'next';
import {Connection, PublicKey} from '@solana/web3.js';
import {getNodeURL} from '@figment-solana/lib';
export default async function balance(
req: NextApiRequest,
res: NextApiResponse<string | number>,
) {
try {
const {network, address} = req.body;
const url = getNodeURL(network);
const connection = new Connection(url, 'confirmed');
const publicKey = new PublicKey(address);
const balance = await connection.getBalance(publicKey);
if (balance === 0 || balance === undefined) {
throw new Error('Account not funded');
}
res.status(200).json(balance);
} catch (error) {
let errorMessage = error instanceof Error ? error.message : 'Unknown Error';
res.status(500).json(errorMessage);
}
}- Convert the address string to a
PublicKey. getBalance(pubkey)returns lamports.
Transfer SOL
Move lamports from a signed account to another address on the same cluster.
import type {NextApiRequest, NextApiResponse} from 'next';
import {getNodeURL} from '@figment-solana/lib';
import {
Connection,
PublicKey,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
} from '@solana/web3.js';
export default async function transfer(
req: NextApiRequest,
res: NextApiResponse<string>,
) {
try {
const {address, secret, recipient, lamports, network} = req.body;
const url = getNodeURL(network);
const connection = new Connection(url, 'confirmed');
const fromPubkey = new PublicKey(address);
const toPubkey = new PublicKey(recipient);
const secretKey = Uint8Array.from(JSON.parse(secret as string));
const instructions = SystemProgram.transfer({
fromPubkey,
toPubkey,
lamports,
});
const signers = [
{
publicKey: fromPubkey,
secretKey,
},
];
const transaction = new Transaction().add(instructions);
const hash = await sendAndConfirmTransaction(
connection,
transaction,
signers,
);
res.status(200).json(hash);
} catch (error) {
let errorMessage = error instanceof Error ? error.message : 'Unknown Error';
res.status(500).json(errorMessage);
}
}SystemProgram
SystemProgram in @solana/web3.js is the factory for instructions that talk to the native system program.
SystemProgram.transfer
Builds a transfer instruction:
const instructions = SystemProgram.transfer({
fromPubkey,
toPubkey,
lamports,
});Build a signers array with the payer public and secret keys:
const signers = [
{
publicKey: fromPubkey,
secretKey,
},
];
Wrap the instruction in a Transaction:
const transaction = new Transaction().add(instructions);Send and confirm
const hash = await sendAndConfirmTransaction(
connection,
transaction,
signers,
);Deploy a simple program (lib.rs)
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};- borsh — Binary Object Representation Serializer for Hashing
- solana_program crates:
account_info,entrypoint,ProgramResult,msg!,ProgramError,Pubkey
GreetingAccount
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct GreetingAccount {
pub counter: u32,
}Derive macros wire Borsh (de)serialization and Debug at compile time. The only field is counter: u32.
Program entrypoint
// Declare and export the program's entrypoint
entrypoint!(process_instruction);process_instruction
// Program entrypoint's implementation
pub fn process_instruction(
program_id: &Pubkey, // Public key of the account the hello world program was loaded into
accounts: &[AccountInfo], // The account to say hello to
_instruction_data: &[u8], // Ignored, all helloworld instructions are hellos
) -> ProgramResult {
msg!("Hello World Rust program entrypoint");
// Iterating accounts is safer than indexing
let accounts_iter = &mut accounts.iter();
// Get the account to say hello to
let account = next_account_info(accounts_iter)?;
// The account must be owned by the program in order to modify its data
if account.owner != program_id {
msg!("Greeted account does not have the correct program id");
return Err(ProgramError::IncorrectProgramId);
}
// Increment and store the number of times the account has been greeted
let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;
greeting_account.counter += 1;
greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?;
msg!("Greeted {} time(s)!", greeting_account.counter);
Ok(())
}- entrypoint’s implementation
- retrun value ⇒ process_instruction entrypoint (ProgramResult {})
- parms
program_id: &Pubkey,
- reference ← Account Pubkey
accounts: &[AccountInfo],
- Iterator Reference (accoutns_iter)
- next_account_info(account_iter)? ⇒ AccountInfo or NotEnoughAccountKeys error
// Iterating accounts is safer than indexing
let accounts_iter = &mut accounts.iter();
// Get the account to say hello to
let account = next_account_info(accounts_iter)?;_instruction_data: &[u8]
Ignored here — every call is a hello.
// The account must be owned by the program in order to modify its data
if account.owner != program_id {
msg!("Greeted account does not have the correct program id");
return Err(ProgramError::IncorrectProgramId);
}Owner check: the greeting account must be owned by this program.
// Increment and store the number of times the account has been greeted
let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;
greeting_account.counter += 1;
greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?;
msg!("Greeted {} time(s)!", greeting_account.counter);
Ok(())Deserialize the account data into GreetingAccount, bump counter, then serialize it back into the account buffer.
Solana CLI: keypair and airdrop
☁ app [main] ⚡ solana-keygen new --outfile solana-wallet/keypair.json
Generating a new keypair
For added security, enter a BIP39 passphrase
NOTE! This passphrase improves security of the recovery seed phrase NOT the
keypair file itself, which is stored as insecure plain text
BIP39 Passphrase (empty for none):
Wrote new keypair to solana-wallet/keypair.json
=====================================================================
pubkey: <PUB-KEY>
=====================================================================
Save this seed phrase and your BIP39 passphrase to recover your new keypair:
jelly panel immense gadget task cat ship paper knock gap prevent just
=====================================================================
☁ app [main] ⚡ solana airdrop 1 $(solana-keygen pubkey solana-wallet/keypair.json)
Requesting airdrop of 1 SOL
Signature: <Signature>Build and deploy
Building the program
yarn run solana:build:program
"solana:build:program": "cargo build-bpf --manifest-path=contracts/solana/program/Cargo.toml --bpf-out-dir=dist/solana/program",The build emits a .so shared object.
$ solana program deploy /Users/zer0luck/project/app/dist/solana/program/helloworld.soDeploying the program
☁ Contract [main] ⚡ solana deploy -v --keypair solana-wallet/keypair.json dist/solana/program/helloworld.so
RPC URL: <https://api.devnet.solana.com>
Default Signer Path: solana-wallet/keypair.json
Commitment: confirmed
Program Id: <ProgramID>import type {NextApiRequest, NextApiResponse} from 'next';
import {Connection, PublicKey} from '@solana/web3.js';
import {getNodeURL} from '@figment-solana/lib';
import path from 'path';
import fs from 'mz/fs';
const PROGRAM_PATH = path.resolve('dist/solana/program');
const PROGRAM_SO_PATH = path.join(PROGRAM_PATH, 'helloworld.so');
export default async function deploy(
req: NextApiRequest,
res: NextApiResponse<string | boolean>,
) {
try {
const {network, programId} = req.body;
const url = getNodeURL(network);
const connection = new Connection(url, 'confirmed');
// Re-create publicKeys from params
const publicKey = new PublicKey(programId);
const programInfo = await connection.getAccountInfo(publicKey);
if (programInfo === null) {
if (fs.existsSync(PROGRAM_SO_PATH)) {
throw new Error(
'Program needs to be deployed with `solana program deploy`',
);
} else {
throw new Error('Program needs to be built and deployed');
}
} else if (!programInfo.executable) {
throw new Error(`Program is not executable`);
}
res.status(200).json(true);
} catch (error) {
let errorMessage = error instanceof Error ? error.message : 'Unknown Error';
res.status(500).json(errorMessage);
}
}getAccountInfo(programId) must return an executable account after a successful deploy.
Create program storage
Solana programs do not hold app state in the program account itself. Allocate a separate data account owned by the program:
import {
Connection,
PublicKey,
Keypair,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
} from '@solana/web3.js';
import type {NextApiRequest, NextApiResponse} from 'next';
import {getNodeURL} from '@figment-solana/lib';
import * as borsh from 'borsh';
// The state of a greeting account managed by the hello world program
class GreetingAccount {
counter = 0;
constructor(fields: {counter: number} | undefined = undefined) {
if (fields) {
this.counter = fields.counter;
}
}
}
// Borsh schema definition for greeting accounts
const GreetingSchema = new Map([
[GreetingAccount, {kind: 'struct', fields: [['counter', 'u32']]}],
]);
// The expected size of each greeting account.
const GREETING_SIZE = borsh.serialize(
GreetingSchema,
new GreetingAccount(),
).length;
type ResponseT = {
hash: string;
greeter: string;
};
export default async function greeter(
req: NextApiRequest,
res: NextApiResponse<string | ResponseT>,
) {
try {
const {network, secret, programId: programAddress} = req.body;
const url = getNodeURL(network);
const connection = new Connection(url, 'confirmed');
const programId = new PublicKey(programAddress);
const payer = Keypair.fromSecretKey(new Uint8Array(JSON.parse(secret)));
const GREETING_SEED = 'hello';
// Are there any methods from PublicKey to derive a public key from a seed?
const greetedPubkey = await PublicKey.createWithSeed(
payer.publicKey,
GREETING_SEED,
programId,
);
// This function calculates the fees we have to pay to keep the newly
// created account alive on the blockchain. We're naming it lamports because
// that is the denomination of the amount being returned by the function.
const lamports = await connection.getMinimumBalanceForRentExemption(
GREETING_SIZE,
);
// Find which instructions are expected and complete SystemProgram with
// the required arguments.
const transaction = new Transaction().add(
SystemProgram.createAccountWithSeed({
fromPubkey: payer.publicKey,
basePubkey: payer.publicKey,
seed: GREETING_SEED,
newAccountPubkey: greetedPubkey,
lamports: lamports,
space: GREETING_SIZE,
programId,
}),
);
// Complete this function call with the expected arguments.
const hash = await sendAndConfirmTransaction(connection, transaction, [
payer,
]);
res.status(200).json({
hash: hash,
greeter: greetedPubkey.toBase58(),
});
} catch (error) {
let errorMessage = error instanceof Error ? error.message : 'Unknown Error';
res.status(500).json(errorMessage);
}
}Derive the greeting pubkey
const programId = new PublicKey(programAddress);
const payer = Keypair.fromSecretKey(new Uint8Array(JSON.parse(secret)));
const GREETING_SEED = 'hello';
// Are there any methods from PublicKey to derive a public key from a seed?
const greetedPubkey = await PublicKey.createWithSeed(
payer.publicKey,
GREETING_SEED,
programId,
);PublicKey.createWithSeed(base, seed, programId) derives the new account address.
const lamports = await connection.getMinimumBalanceForRentExemption(
GREETING_SIZE,
);
// Find which instructions are expected and complete SystemProgram with
// the required arguments.
const transaction = new Transaction().add(
SystemProgram.createAccountWithSeed({
fromPubkey: payer.publicKey,
basePubkey: payer.publicKey,
seed: GREETING_SEED,
newAccountPubkey: greetedPubkey,
lamports: lamports,
space: GREETING_SIZE,
programId,
}),
);fromPubkey— pays for the createbasePubkey/seed— inputs to the derived addresslamports— rent-exempt minimum forGREETING_SIZEspace— allocated byte lengthprogramId— owner of the new accountnewAccountPubkey— result ofcreateWithSeed
Read program data
Deserialize the on-chain buffer with the same Borsh schema:
import type {NextApiRequest, NextApiResponse} from 'next';
import {Connection, PublicKey} from '@solana/web3.js';
import {getNodeURL} from '@figment-solana/lib';
import * as borsh from 'borsh';
// The state of a greeting account managed by the hello world program
class GreetingAccount {
counter = 0;
constructor(fields: {counter: number} | undefined = undefined) {
if (fields) {
this.counter = fields.counter;
}
}
}
// Borsh schema definition for greeting accounts
const GreetingSchema = new Map([
[GreetingAccount, {kind: 'struct', fields: [['counter', 'u32']]}],
]);
export default async function getter(
req: NextApiRequest,
res: NextApiResponse<string | number>,
) {
try {
const {network, greeter} = req.body;
const url = getNodeURL(network);
const connection = new Connection(url, 'confirmed');
const greeterPublicKey = new PublicKey(greeter);
const accountInfo = await connection.getAccountInfo(greeterPublicKey);
if (accountInfo === null) {
throw new Error('Error: cannot find the greeted account');
}
// Find the expected parameters.
// const value = new Test({ x: 255, y: 20, z: '123', q: [1, 2, 3] });
const greeting = borsh.deserialize(
GreetingSchema,
GreetingAccount,
accountInfo.data,
);
// A little helper
console.log(greeting);
// Pass the counter to the client-side as JSON
res.status(200).json(greeting.counter);
} catch (error) {
let errorMessage = error instanceof Error ? error.message : 'Unknown Error';
console.log(errorMessage);
res.status(500).json(errorMessage);
}
}borsh.deserialize(schema, class, buffer) turns account bytes into a GreetingAccount.
Call the program (increment)
import {
Connection,
PublicKey,
Keypair,
TransactionInstruction,
Transaction,
sendAndConfirmTransaction,
} from '@solana/web3.js';
import type {NextApiRequest, NextApiResponse} from 'next';
import {getNodeURL} from '@figment-solana/lib';
export default async function setter(
req: NextApiRequest,
res: NextApiResponse<string>,
) {
try {
const {greeter, secret, programId, network} = req.body;
const url = getNodeURL(network);
const connection = new Connection(url, 'confirmed');
const greeterPublicKey = new PublicKey(greeter);
const programKey = new PublicKey(programId);
const payerSecretKey = new Uint8Array(JSON.parse(secret));
const payerKeypair = Keypair.fromSecretKey(payerSecretKey);
// this your turn to figure out
// how to create this instruction
const instruction = new TransactionInstruction({
keys: [{pubkey: greeterPublicKey, isSigner: false, isWritable: true}],
programId: programKey,
data: Buffer.alloc(0), // All instructions are hellos
});
const hash = await sendAndConfirmTransaction(
connection,
new Transaction().add(instruction),
[payerKeypair],
);
res.status(200).json(hash);
} catch (error) {
console.error(error);
res.status(500).json('Get balance failed');
}
}const instruction = new TransactionInstruction({
keys: [{pubkey: greeterPublicKey, isSigner: false, isWritable: true}],
programId: programKey,
data: Buffer.alloc(0), // All instructions are hellos
});
const hash = await sendAndConfirmTransaction(
connection,
new Transaction().add(instruction),
[payerKeypair],
);The greeting account is writable but not a signer. Instruction data is empty because every call is a hello.