This document is not maintained anymore. Please visit our new book for the latest update.
cargo
cargo build-bpf
rustup
solana-cli
solana deploy
solana-test-validator
solana-web3.js
rustup
$ curl https://sh.rustup.rs -sSf | sh
...
$ rustup component add rustfmt
...
$ rustup update
...
$ rustup install 1.59.0
...
solana-cli
$ sh -c "$(curl -sSfL https://release.solana.com/v1.10.5/install)"
...
$ solana --version
solana-cli 1.10.5 (src:devbuild; feat:2037512494)
Note: You may have to build from source if you are using Mac M1 machine. See this doc for more installation details.
$ solana-keygen new
...
Or, you can recover your key from your existing key phrase:
$ solana-keygen recover 'prompt:?key=0/0' -o ~/.config/solana/solmeet-keypair-1.json
...
Config the keypath:
$ solana config set --keypair ~/.config/solana/solmeet-keypair-1.json
$ solana config set --url localhost
...
rust-analyzer
, Better TOML
and crates
(Optional)rust-analyzer
can be very handy if you are using Visual Studio Code. For example, the analyzer can help download the missing dependencies for you automatically.
If you are using Linux, you may need to install these tools as well:
$ sudo apt-get update
...
$ sudo apt-get install libssl-dev libudev-dev pkg-config zlib1g-dev llvm clang make
...
First, let's clone the repo:
$ git clone https://github.com/solana-labs/example-helloworld.git
...
$ cd example-helloworld
$ npm install
...
$ npm install -g ts-node
...
Run solana-test-validator
in another terminal session:
$ solana-test-validator
Ledger location: test-ledger
Log: test-ledger/validator.log
⠤ Initializing...
Identity: D4kA7VzHnmVa9HqfL1gQzTgHBGYdcsaADFxZnJfLxnxz
Genesis Hash: AvyN2Hka7q3aBUFcNbEKERQxEPiKs1B3kVVUiXnbdCk
Version: 1.8.0
Shred Version: 59947
Gossip Address: 127.0.0.1:1024
TPU Address: 127.0.0.1:1027
JSON RPC URL: http://127.0.0.1:8899
...
Next, let's compile the hello world program:
$ cd src/program-rust
$ cargo build-bpf
...
Deploy the program after compilation, :
$ solana program deploy target/deploy/helloworld.so
...
If you encounter an insuffficient fund error, you may have to request for an aidrop:
$ solana airdrop 1
First, we need to modify the PROGRAM_PATH
in src/client/hello_world.ts
:
// In src/client/hello_world.ts
// Modify PROGRAM_PATH at Line 43
...
// const PROGRAM_PATH = path.resolve(__dirname, '../../dist/program');
const PROGRAM_PATH = path.resolve(__dirname, '../program-rust/target/deploy');
...
Finally, let's make the program say Hello by sending a transaction:
$ npm install -g ts-node
...
$ ts-node ../client/main.ts
Let's say hello to a Solana account...
Connection to cluster established: http://localhost:8899 { 'feature-set': 2037512494, 'solana-core': '1.8.0' }
Using account 4h8EgjxFHnTLshhGWb91MgyN2PXJZ8dmbc8UiTsfatLf containing 499999999.14836293 SOL to pay for fees
Using program 7hV1hUKgY4ZF3J2UYAhvZFhNtr4PB4MLufsuXFU5Usa2
Saying hello to f5nadW1a9e86aaigWfuKPhAKTYCNYUeZ9xm9i3HjS8P
f5nadW1a9e86aaigWfuKPhAKTYCNYUeZ9xm9i3HjS8P has been greeted 2 time(s)
Success
If we take a closer look to function sayHello
, we can see how a solana transaction is constructed and sent:
// In hello_world.ts
...
export async function sayHello(): Promise<void> {
console.log('Saying hello to', greetedPubkey.toBase58());
const instruction = new TransactionInstruction({
keys: [{pubkey: greetedPubkey, isSigner: false, isWritable: true}],
programId,
data: Buffer.alloc(0), // All instructions are hellos
});
await sendAndConfirmTransaction(
connection,
new Transaction().add(instruction),
[payer],
);
}
...
invoke
invoke_signed
lib.rs
: registering modulesentrypoint.rs
: entrypoint to the programinstruction.rs
: program API, (de)serializing instruction dataprocessor.rs
: program logicstate.rs
: program objects, (de)serializing stateerror.rs
: program specific errorsinstruction.rs
. It's the program's responsibility to check that received accounts == expected accountsmulticall
token
Programinvoke_signed
, the runtime uses the given seeds and the program id of the calling program to recreate the PDA and if it matches one of the given accounts inside invoke_signed's arguments, that account's signed property will be set to trueTo spend Solana SPL, you don't need to approve. Why?
Fisrt, let's create a new project solana-escrow
:
$ cargo new solana-escrow --lib
Created library `solana-escrow` package
$ cd solana-escrow
Next, we update the Cargo.toml
manifest to as follows:
# Cargo.toml
[package]
name = "solana-escrow"
version = "0.1.0"
edition = "2018"
license = "WTFPL"
publish = false
[dependencies]
solana-program = "1.6.9"
[lib]
crate-type = ["cdylib", "lib"]
According to the program architecture, we will have five modules in the end. Let's create all these files at once before we start implementing them.
$ touch src/entrypoint.rs
$ touch src/processor.rs
$ touch src/instruction.rs
$ touch src/state.rs
$ touch src/error.rs
Next, define these modules in lib.rs
:
// lib.rs
pub mod entrypoint;
pub mod error;
pub mod instruction;
pub mod processor;
pub mod state;
Let's begin to implement these modules. First, we define instructions. Instructions are the APIs of program. Copy and paste the following snippet into your local instuction.rs
:
// instruction.rs (partially implemented)
use std::convert::TryInto;
use solana_program::program_error::ProgramError;
use crate::error::EscrowError::InvalidInstruction;
pub enum EscrowInstruction {
/// Starts the trade by creating and populating an escrow account and transferring ownership of the given temp token account to the PDA
///
///
/// Accounts expected:
///
/// 0. `[signer]` The account of the person initializing the escrow
/// 1. `[writable]` Temporary token account that should be created prior to this instruction and owned by the initializer
/// 2. `[]` The initializer's token account for the token they will receive should the trade go through
/// 3. `[writable]` The escrow account, it will hold all necessary info about the trade.
/// 4. `[]` The rent sysvar
/// 5. `[]` The token program
InitEscrow {
/// The amount party A expects to receive of token Y
amount: u64
}
}
impl EscrowInstruction {
/// Unpacks a byte buffer into a [EscrowInstruction](enum.EscrowInstruction.html).
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
let (tag, rest) = input.split_first().ok_or(InvalidInstruction)?;
Ok(match tag {
0 => Self::InitEscrow {
amount: Self::unpack_amount(rest)?,
},
_ => return Err(InvalidInstruction.into()),
})
}
fn unpack_amount(input: &[u8]) -> Result<u64, ProgramError> {
let amount = input
.get(..8)
.and_then(|slice| slice.try_into().ok())
.map(u64::from_le_bytes)
.ok_or(InvalidInstruction)?;
Ok(amount)
}
}
You may notice that there are a few compile warning telling you InvalidInstruction
is not resolved. Let's implement it in error.rs
.
Update dependencies:
# Cargo.toml
...
[dependencies]
...
thiserror = "1.0.24"
Update error.rs
:
// error.rs (partially implemented)
use thiserror::Error;
use solana_program::program_error::ProgramError;
#[derive(Error, Debug, Copy, Clone)]
pub enum EscrowError {
/// Invalid instruction
#[error("Invalid Instruction")]
InvalidInstruction,
/// Not Rent Exempt
#[error("Not Rent Exempt")]
NotRentExempt,
}
impl From<EscrowError> for ProgramError {
fn from(e: EscrowError) -> Self {
ProgramError::Custom(e as u32)
}
}
The main business logic locates in processor.rs
. There will be two functions corresponding two instructions. Let's implement those one by one. Here we implement the process_init_escrow
function which matches EscrowInstruction::InitEscrow
case:
Update dependencies:
# Cargo.toml
...
[dependencies]
...
spl-token = {version = "3.1.1", features = ["no-entrypoint"]}
Update processor.rs
:
// processor.rs (partially implemented)
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program_error::ProgramError,
msg,
pubkey::Pubkey,
program_pack::{Pack, IsInitialized},
sysvar::{rent::Rent, Sysvar},
program::invoke
};
use crate::{instruction::EscrowInstruction, error::EscrowError, state::Escrow};
pub struct Processor;
impl Processor {
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
let instruction = EscrowInstruction::unpack(instruction_data)?;
match instruction {
EscrowInstruction::InitEscrow { amount } => {
msg!("Instruction: InitEscrow");
Self::process_init_escrow(accounts, amount, program_id)
}
}
}
fn process_init_escrow(
accounts: &[AccountInfo],
amount: u64,
program_id: &Pubkey,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let initializer = next_account_info(account_info_iter)?;
if !initializer.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
let temp_token_account = next_account_info(account_info_iter)?;
let token_to_receive_account = next_account_info(account_info_iter)?;
if *token_to_receive_account.owner != spl_token::id() {
return Err(ProgramError::IncorrectProgramId);
}
let escrow_account = next_account_info(account_info_iter)?;
let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?;
if !rent.is_exempt(escrow_account.lamports(), escrow_account.data_len()) {
return Err(EscrowError::NotRentExempt.into());
}
let mut escrow_info = Escrow::unpack_unchecked(&escrow_account.data.borrow())?;
if escrow_info.is_initialized() {
return Err(ProgramError::AccountAlreadyInitialized);
}
Ok(())
}
}
You will notice a warning raised due to unresolved state::Escrow
.
What does state.rs
do? It basically represents the data structure stored in the account owned by Escrow program. Also, it has the pack/unpack utils to convert the data format.
Update dependencies:
# Cargo.toml
...
[dependencies]
...
arrayref = "0.3.6"
Update state.rs
:
// state.rs
use solana_program::{
program_pack::{IsInitialized, Pack, Sealed},
program_error::ProgramError,
pubkey::Pubkey,
};
use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
pub struct Escrow {
pub is_initialized: bool,
pub initializer_pubkey: Pubkey,
pub temp_token_account_pubkey: Pubkey,
pub initializer_token_to_receive_account_pubkey: Pubkey,
pub expected_amount: u64,
}
impl Sealed for Escrow {}
impl IsInitialized for Escrow {
fn is_initialized(&self) -> bool {
self.is_initialized
}
}
impl Pack for Escrow {
const LEN: usize = 105;
fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
let src = array_ref![src, 0, Escrow::LEN];
let (
is_initialized,
initializer_pubkey,
temp_token_account_pubkey,
initializer_token_to_receive_account_pubkey,
expected_amount,
) = array_refs![src, 1, 32, 32, 32, 8];
let is_initialized = match is_initialized {
[0] => false,
[1] => true,
_ => return Err(ProgramError::InvalidAccountData),
};
Ok(Escrow {
is_initialized,
initializer_pubkey: Pubkey::new_from_array(*initializer_pubkey),
temp_token_account_pubkey: Pubkey::new_from_array(*temp_token_account_pubkey),
initializer_token_to_receive_account_pubkey: Pubkey::new_from_array(*initializer_token_to_receive_account_pubkey),
expected_amount: u64::from_le_bytes(*expected_amount),
})
}
fn pack_into_slice(&self, dst: &mut [u8]) {
let dst = array_mut_ref![dst, 0, Escrow::LEN];
let (
is_initialized_dst,
initializer_pubkey_dst,
temp_token_account_pubkey_dst,
initializer_token_to_receive_account_pubkey_dst,
expected_amount_dst,
) = mut_array_refs![dst, 1, 32, 32, 32, 8];
let Escrow {
is_initialized,
initializer_pubkey,
temp_token_account_pubkey,
initializer_token_to_receive_account_pubkey,
expected_amount,
} = self;
is_initialized_dst[0] = *is_initialized as u8;
initializer_pubkey_dst.copy_from_slice(initializer_pubkey.as_ref());
temp_token_account_pubkey_dst.copy_from_slice(temp_token_account_pubkey.as_ref());
initializer_token_to_receive_account_pubkey_dst.copy_from_slice(initializer_token_to_receive_account_pubkey.as_ref());
*expected_amount_dst = expected_amount.to_le_bytes();
}
}
Let's further extend the business logic of process_init_escrow
in processor.rs
:
// processor.rs (partially implemented)
...
impl Processor {
fn process_init_escrow(
accounts: &[AccountInfo],
amount: u64,
program_id: &Pubkey,
) -> ProgramResult {
...
escrow_info.is_initialized = true;
escrow_info.initializer_pubkey = *initializer.key;
escrow_info.temp_token_account_pubkey = *temp_token_account.key;
escrow_info.initializer_token_to_receive_account_pubkey = *token_to_receive_account.key;
escrow_info.expected_amount = amount;
Escrow::pack(escrow_info, &mut escrow_account.data.borrow_mut())?;
let (pda, _bump_seed) = Pubkey::find_program_address(&[b"escrow"], program_id);
let token_program = next_account_info(account_info_iter)?;
let owner_change_ix = spl_token::instruction::set_authority(
token_program.key,
temp_token_account.key,
Some(&pda),
spl_token::instruction::AuthorityType::AccountOwner,
initializer.key,
&[&initializer.key],
)?;
msg!("Calling the token program to transfer token account ownership...");
invoke(
&owner_change_ix,
&[
temp_token_account.clone(),
initializer.clone(),
token_program.clone(),
],
)?;
Ok(())
}
}
Here, we can see invoke
is called to perform a CPI.
To make the first function process_init_escrow
callable, let's put it in the entrypoint.rs
:
// entrypoint.rs (partially implemented)
use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey
};
use crate::processor::Processor;
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
Processor::process(program_id, accounts, instruction_data)
}
Check if we can compile it successfully:
$ cargo build-bpf
...
Next, we can implement another instruction Exchange
and its corresponding function process_exchange
.
Update instruction.rs
:
// instructions.rs (fully implemented)
pub enum EscrowInstruction {
...
/// Accepts a trade
///
///
/// Accounts expected:
///
/// 0. `[signer]` The account of the person taking the trade
/// 1. `[writable]` The taker's token account for the token they send
/// 2. `[writable]` The taker's token account for the token they will receive should the trade go through
/// 3. `[writable]` The PDA's temp token account to get tokens from and eventually close
/// 4. `[writable]` The initializer's main account to send their rent fees to
/// 5. `[writable]` The initializer's token account that will receive tokens
/// 6. `[writable]` The escrow account holding the escrow info
/// 7. `[]` The token program
/// 8. `[]` The PDA account
Exchange {
/// the amount the taker expects to be paid in the other token, as a u64 because that's the max possible supply of a token
amount: u64,
}
}
impl EscrowInstruction {
...
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
...
Ok(match tag {
...
1 => Self::Exchange {
amount: Self::unpack_amount(rest)?
},
...
})
}
}
Also in processor.rs
:
// processor.rs (fully implemented)
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program::{invoke, invoke_signed},
program_error::ProgramError,
program_pack::{IsInitialized, Pack},
pubkey::Pubkey,
sysvar::{rent::Rent, Sysvar},
};
use spl_token::state::Account as TokenAccount;
...
impl Processor {
pub fn process(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
...
match instruction {
...
EscrowInstruction::Exchange { amount } => {
msg!("Instruction: Exchange");
Self::process_exchange(accounts, amount, program_id)
}
}
}
fn process_exchange(
accounts: &[AccountInfo],
amount_expected_by_taker: u64,
program_id: &Pubkey,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let taker = next_account_info(account_info_iter)?;
if !taker.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
let takers_sending_token_account = next_account_info(account_info_iter)?;
let takers_token_to_receive_account = next_account_info(account_info_iter)?;
let pdas_temp_token_account = next_account_info(account_info_iter)?;
let pdas_temp_token_account_info =
TokenAccount::unpack(&pdas_temp_token_account.data.borrow())?;
let (pda, bump_seed) = Pubkey::find_program_address(&[b"escrow"], program_id);
if amount_expected_by_taker != pdas_temp_token_account_info.amount {
return Err(EscrowError::ExpectedAmountMismatch.into());
}
let initializers_main_account = next_account_info(account_info_iter)?;
let initializers_token_to_receive_account = next_account_info(account_info_iter)?;
let escrow_account = next_account_info(account_info_iter)?;
let escrow_info = Escrow::unpack(&escrow_account.data.borrow())?;
if escrow_info.temp_token_account_pubkey != *pdas_temp_token_account.key {
return Err(ProgramError::InvalidAccountData);
}
if escrow_info.initializer_pubkey != *initializers_main_account.key {
return Err(ProgramError::InvalidAccountData);
}
if escrow_info.initializer_token_to_receive_account_pubkey != *initializers_token_to_receive_account.key {
return Err(ProgramError::InvalidAccountData);
}
let token_program = next_account_info(account_info_iter)?;
let transfer_to_initializer_ix = spl_token::instruction::transfer(
token_program.key,
takers_sending_token_account.key,
initializers_token_to_receive_account.key,
taker.key,
&[&taker.key],
escrow_info.expected_amount,
)?;
msg!("Calling the token program to transfer tokens to the escrow's initializer...");
invoke(
&transfer_to_initializer_ix,
&[
takers_sending_token_account.clone(),
initializers_token_to_receive_account.clone(),
taker.clone(),
token_program.clone(),
],
)?;
let pda_account = next_account_info(account_info_iter)?;
let transfer_to_taker_ix = spl_token::instruction::transfer(
token_program.key,
pdas_temp_token_account.key,
takers_token_to_receive_account.key,
&pda,
&[&pda],
pdas_temp_token_account_info.amount,
)?;
msg!("Calling the token program to transfer tokens to the taker...");
invoke_signed(
&transfer_to_taker_ix,
&[
pdas_temp_token_account.clone(),
takers_token_to_receive_account.clone(),
pda_account.clone(),
token_program.clone(),
],
&[&[&b"escrow"[..], &[bump_seed]]],
)?;
let close_pdas_temp_acc_ix = spl_token::instruction::close_account(
token_program.key,
pdas_temp_token_account.key,
initializers_main_account.key,
&pda,
&[&pda]
)?;
msg!("Calling the token program to close pda's temp account...");
invoke_signed(
&close_pdas_temp_acc_ix,
&[
pdas_temp_token_account.clone(),
initializers_main_account.clone(),
pda_account.clone(),
token_program.clone(),
],
&[&[&b"escrow"[..], &[bump_seed]]],
)?;
msg!("Closing the escrow account...");
**initializers_main_account.lamports.borrow_mut() = initializers_main_account.lamports()
.checked_add(escrow_account.lamports())
.ok_or(EscrowError::AmountOverflow)?;
**escrow_account.lamports.borrow_mut() = 0;
*escrow_account.data.borrow_mut() = &mut [];
Ok(())
}
}
Here we can see that invoke_signed
is called with seeds since the owner of escrow account is a PDA.
Finally, implement the missing error enums:
// error.rs (fully implemented)
...
pub enum EscrowError {
...
/// Expected Amount Mismatch
#[error("Expected Amount Mismatch")]
ExpectedAmountMismatch,
/// Amount Overflow
#[error("Amount Overflow")]
AmountOverflow,
}
Check if we can compile successfully:
$ cargo build-bpf
...
Now, we can write some client side code to interact with the escrow program.
First, let's install dependencies:
$ npm init -y
...
$ npm install --save @solana/spl-token @solana/web3.js bn.js
...
$ tsc --init
...
Next, let's generate the files to be filled in necessary code and data:
$ mkdir keys
$ touch keys/id_pub.json
$ touch keys/alice_pub.json
$ touch keys/bob_pub.json
$ touch keys/program_pub.json
$ mkdir ts
$ touch ts/setup.ts
$ touch ts/utils.ts
$ touch ts/alice.ts
$ touch ts/bob.ts
$ touch terms.json
We have to generate keypairs for alice
, bob
, and the transaction payer
. This can be done via solana-keygen
:
$ solana-keygen new -o keys/id.json
...
$ solana-keygen new -o keys/alice.json
...
$ solana-keygen new -o keys/bob.json
...
Next, we need to manually update the public keys for each. Retrieve the address for all of them and paste it to the *_pub.json
files accordingly. For example:
$ solana address -k keys/id.json
9q9XLUDjDKj2cahaN44X9Mid2HGJtUauFvjJG8qocY5a
// id_pub.json
"9q9XLUDjDKj2cahaN44X9Mid2HGJtUauFvjJG8qocY5a"
Don't forget the double quotes
Here we add the client code base. Copy and paste the following files to your local code base:
Again, I strongly recommend you to clone the original code base and run it
First, let's start the validator:
$ solana-test-validator
...
Compile and deploy the program:
$ cargo build-bpf
...
$ solana program deploy target/deploy/solana_escrow.so
Program Id: EKnr6pssVnPmoGJH3NgtCByF9jMDRnyDQZxkHqz1GBS2
Before we execute the client code, we need to update the programId
to be looked up:
// program_pub.json
"EKnr6pssVnPmoGJH3NgtCByF9jMDRnyDQZxkHqz1GBS2"
Also, update the predefined terms.json
as follows:
// terms.json
{
"aliceExpectedAmount": 3,
"bobExpectedAmount": 5
}
Fund the transaction payer
in advance:
$ solana transfer 9q9XLUDjDKj2cahaN44X9Mid2HGJtUauFvjJG8qocY5a 100 --allow-unfunded-recipient
Finally, let's run the client code:
First, run setup.ts
to mint the tokens to be exchanged:
$ ts-node ts/setup.ts
...
Next, run alice.ts
to initialize the escrow program:
$ ts-node ts/alice.ts
...
You can see how an instruction is constructed. The interger 0
assined to the Uint8Array
represents the instruction InitEscrow
:
// alice.ts
...
const alice = async () => {
const initEscrowIx = new TransactionInstruction({
programId: escrowProgramId,
keys: [
{ pubkey: aliceKeypair.publicKey, isSigner: true, isWritable: false },
{
pubkey: tempXTokenAccountKeypair.publicKey,
isSigner: false,
isWritable: true,
},
{
pubkey: aliceYTokenAccountPubkey,
isSigner: false,
isWritable: false,
},
{ pubkey: escrowKeypair.publicKey, isSigner: false, isWritable: true },
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
],
data: Buffer.from(
Uint8Array.of(0, ...new BN(terms.aliceExpectedAmount).toArray("le", 8))
),
});
...
}
Then, run bob.ts
to exchange and close the escrow account:
$ ts-node ts/bob.ts
...
solana