Introduction

Poseidon is a transpiler that helps you to convert your TypeScript Solana programs into Anchor, which is especially convenient for people who are just getting started with Solana!

Now you can write Solana programs in Typescript and transpile them into Rust using Poseidon!

Installation

Make sure you have Rust and Cargo installed

Installing with Cargo

cargo install --git  https://github.com/Turbin3/poseidon

That's it, you're done!

Installing from source

git clone https://github.com/Turbin3/poseidon

Navigate to the project directory:

cd poseidon

Build poseidon:

cargo build --release

This will create a binary named poseidon in the target/release directory. You can copy the binary to a location in your $PATH for easier access.

Usage

poseidon compile --input "input.ts" --output "output.rs"

Check out examples in the repo to learn how to write Poseidon Typescript which can be transpiled to Anchor programs.

Examples

  1. Vote (Rust, TypeScript)

  2. Vault (Rust, TypeScript)

  3. Escrow (Rust, TypeScript)

  4. Favorites (Rust, TypeScript)

Tutorial

Overview

This tutorial is for people with no experience in Rust who want to quickly write a Solana program in TypeScript. Poseidon will help you transpile your TypeScript code into Anchor (a Solana framework), allowing you to understand how Solana works through practical examples.

Please note that if your goal is to become a protocol engineer on Solana, you'll eventually need to learn Anchor and Rust to understand how Solana works at a lower level.

Without further ado, let’s get your hands dirty!

Environment Setup

Prerequisites

If you’ve already install Solana and Anchor, feel free to skip prerequisites part

During this tutorial, we will be using the following tools:

$ rustup --version
rustup 1.27.1 (54dd3d00f 2024-04-24)
$ solana --version
solana-cli 1.18.17 (src:b685182a; feat:4215500110, client:SolanaLabs)
$ yarn --version
1.22.19
$ anchor --version
anchor-cli 0.30.1

If you haven't installed all of them yet, go to Solana Anchor Installation Guide

Install Poseidon

git clone [email protected]:Turbin3/poseidon.git
cd poseidon
# Build poseidon binary file
cargo build --release

You can copy poseidon from the target/release folder to your PATH or update your PATH in your profile file (~/.bash_profile, ~/.zshrc, ~/.profile, or ~/.bashrc) for future use.

To finish this tutorial, you can simply create an alias:

$ pwd
/path/to/poseidon/project
$ alias poseidon='/path/to/poseidon/project/target/release/poseidon'
# Check poseidon command works as expected
$ poseidon --help

Congratulations! You’ve completed the most challenging part! Setting up the environment can be a hassle, but once it's done, the rest will be much simpler and easier.

Your First Solana Program with TypeScript

We’ll build a simple vote program with three instructions: initialize, upvote, and downvote.

Remember what Poseidon does for you? Here’s a quick recap:

Poseidon helps by transpiling your TypeScript code into Anchor.

Let’s use poseidon init to set up a scaffold, and then we can start writing our program in TypeScript.

# Feel free to switch to whereever you preferred.
$ mkdir tutorial
$ cd tutorial
$ poseidon init vote-program

Open vote-program/ts-programs/voteProgram.ts in VS Code (or any IDE you prefer) and add the initial pieces of code (without the logic).

import { Account, Pubkey, type Result, i64, u8, Signer } from "@solanaturbine/poseidon";

export default class VoteProgram {
  static PROGRAM_ID = new Pubkey("11111111111111111111111111111111");

  initialize(): Result {}
  upvote(): Result {}
  downvote(): Result {}
}

As we mentioned at the beginning, this program will contain only three simple instructions (initialize, upvote, downvote). Here’s how it looks when using Poseidon. In Solana, programs are stateless, meaning the functions above are “pure functions”—you get the same output from the same input. But something is missing in the code, what it is?

State!

Ultimately, we need a place to store our voting results, just like storing data in a database in Web2. In Solana, we called it “Account.” Let’s add the account at the end of our program.

// ...

export interface VoteState extends Account {
  vote: i64; // This field store the voting result
  bump: u8; // bump is for PDA (program derieved account, a special type of account which controlled by program on Solana)
}

i64 stands for signed integer with 64 bit and u8 stands for unsigned integer with 8 bit in Rust.

We’ll use the vote field to store the voting result, and we can ignore the bump field for now. You can find more information about it in the reference section after completing this tutorial.

We’ve defined the VoteState account as our data structure, and now we're ready to implement the logic inside each instruction. Let’s start with the initialize instruction:

// Pass all the accounts we need as the parameters
initialize(state: VoteState, user: Signer): Result {

    // Use `.derive([seed])` to define the PDA and chain the `.init(payer)` at the end for creating the account and pass the payer argument
    state.derive(["vote"])
         .init(user);

    // Set the initial value to the `vote` field of the account
    state.vote = new i64(0);
}

If a user wants to store anything on Solana, such as VoteState in this case, they’ll need to pay rent for the space they’re using, as validators need to store the data on their hardware. To cover this rent, we add user with the Signer type as a parameter, allowing the user to transfer their SOL to the VoteState account to pay for the rent.

We’ve mentioned PDA several times, but what is it? PDA (Program Derived Address) is an important concept on Solana. It allows an account to be controlled by a specified program. To construct a PDA, you need a seed—a byte array that can be derived from a string, public key, integer, or even combinations of these! In this case, we use the string “vote” as the seed. You can find more examples of different seed combinations in the provided examples.

After the state account is initialized, we can assign an initial value, new i64(0), to it.

We’re almost done. Let’s update the upvote and downvote instructions:

upvote(state: VoteState): Result {
    state.derive(["vote"]);
    state.vote = state.vote.add(1);
}

downvote(state: VoteState): Result {
    state.derive(["vote"]);
    state.vote = state.vote.sub(1);
}

Every time you use a PDA, you’ll need to specify its seed, but only when creating the account do you need to chain the init() at the end. When you're initializing account, Poseidon automatically adds the SystemProgram account to the account struct. Similarly in examples given in the repo, we can see that it also automatically adds Token Program and Associated Token Program accounts.

The logic for upvote and downvote is quite simple—just add or subtract by 1. The only thing to be aware of is that you need to assign the result back to where it’s stored, e.g. state.vote. Otherwise, the value won’t be updated after the instruction is executed.

The final step to complete this program is to run the command below to get your correct program ID and replace, if the program ID is not synced yet.

$ poseidon sync

Test Your Program!

It’s time to verify that the program works as expected! Let’s use the Poseidon command with Anchor to make the magic happen 😉 If you type poseidon --help in your terminal, you’ll see:

poseidon --help
Usage: poseidon <COMMAND>

Commands:
  build    Build Typescript programs in workspace
  test     Run anchor tests in the workspace
  sync     Sync anchor keys in poseidon programs
  compile  Transpile a Typescript program to a Rust program
  init     Initializes a new workspace
  help     Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version

Obviously, we’ll use the TypeScript code to generate and replace the Rust code that Anchor generated for us. If you’ve followed this tutorial step-by-step, your program structure (under the tutorial/vote_program folder) should look like this:

.
├── Anchor.toml
├── Cargo.toml
├── app
├── migrations
│   └── deploy.ts
├── package.json
├── programs
│   └── vote_program
│       ├── Cargo.toml
│       ├── Xargo.toml
│       └── src
│           └── lib.rs      <--------- Output Rust file
├── target
│   └── deploy
│       └── vote_program-keypair.json
├── tests
│   └── vote_program.ts
├── ts-programs
│   ├── package.json
│   └── src
│       └── voteProgram.ts  <--------- Input Typescript file
├── tsconfig.json
└── yarn.lock

If you’re in the root directory of the program, use the following command:

poseidon build

And if you're not in the root directory or just want to compile by specifying the location, use the following command:

poseidon compile -i ts-programs/src/voteProgram.ts -o programs/vote-program/src/lib.rs

Once the code is transpiled to lib.rs

anchor build

Let’s replace the contents of tests/vote-program.ts with the code below:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { VoteProgram } from "../target/types/vote_program";
import { assert } from "chai";

describe("vote program", () => {
  // Configure the client to use the local cluster.
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);
  const program = anchor.workspace.VoteProgram as Program<VoteProgram>;
  const voteState = anchor.web3.PublicKey.findProgramAddressSync(
    [anchor.utils.bytes.utf8.encode("vote")],
    program.programId
  )[0];

  it("Create and initialize vote state", async () => {
    const txid = await program.methods
      .initialize()
      .accounts({
        user: provider.wallet.publicKey,
      })
      .rpc();
    console.log("Initialize tx:", txid);

    const voteStateAccount = await program.account.voteState.fetch(voteState);
    assert.ok(voteStateAccount.vote.eq(new anchor.BN(0)));
  });

  it("Upvote", async () => {
    const txid = await program.methods.upvote().accounts({}).rpc();

    console.log("upvote tx:", txid);

    const voteStateAccount = await program.account.voteState.fetch(voteState);
    assert.ok(voteStateAccount.vote.eq(new anchor.BN(1)));
  });

  it("Downvote", async () => {
    const txid = await program.methods.downvote().accounts({}).rpc();

    console.log("downvote tx:", txid);

    const voteStateAccount = await program.account.voteState.fetch(voteState);
    assert.ok(voteStateAccount.vote.eq(new anchor.BN(0)));
  });
});

For testing it locally, we can run

poseidon test

This command will build the program, start a local validator with the program deployed, and run all the tests in the tests folder. This is a quick way to check if your program works correctly. Ideally, you should see all your tests pass like this:

  vote program
Initialize tx: 4uNEPU1dTXnNDgs3thgbkqQhN11xscbgcV1362Wv2nXRJSCfsra6B1AP24y6qjCXGLWrqjrrzFrtCf7S1YF6tRkZ
    ✔ Create and initialize vote state (426ms)
upvote tx: 2j7FypJmk5yyiugYVxPcgmWQkG7YYCUXdBEpzACJAv2UPXQj6b3tS47S3pN1dTr8JsCt3czYDMo62DuxjUjLNe78
    ✔ Upvote (471ms)
downvote tx: pTKwbkU9NTFdLaRFRTZCwuYaAHrYX44dkLAHau7GsBWvaEjsV5U6gYX59Ku6DKrXENsyQd5cirtSwBtBC9zN9Ut
    ✔ Downvote (466ms)

  3 passing (1s)

If you want to verify it on the Solana Devnet (a network for developers testing their programs), use this command:

anchor test --provider.cluster devnet

After all the tests have passed, you can copy the transaction IDs and verify them on Solana’s blockchain explorer.

Here’s the example of the transaction ID (ApCnLHqiAm...amxDb439jg) might look like in the explorer on Devnet.

Thoughts & Takeaway

Congratulations! 🎉 You've completed your first Solana program in TypeScript!

Poseidon helps by transpiling your TypeScript program into Rust using the Anchor framework format. You can check out examples/vote/rust/vote.rs to see what the code looks like in Rust. This will help you better understand Rust syntax and Solana’s design principles.

After finishing this tutorial, we highly recommend going through all the resources in the reference section one-by-one. This will give you a more comprehensive understanding of how Solana works and help clarify some common jargon, such as account, PDA, rent, and more.

We hope you enjoyed this tutorial, and we look forward to seeing you in the wild but exciting Solana space!

Reference

Mapping into Anchor

In this section, we will dive deep into how TypeScript code is mapped to the Anchor framework in Rust. We will use the escrow program as the example to illustrate the differences and similarities between the two.

Comparison Table

Solana TermTypeScript (Poseidon)Rust (Anchor)
ProgramClassModule
InstructionMethodFunction
Account (State)InterfaceStruct

Escrow Program

You can find the code for the escrow program in the examples/escrow directory of the Poseidon repository.

TypeScript (Poseidon)

Let's start with the TypeScript code for the escrow program. This code defines the structure and logic of the program using Poseidon.

import {
  Account,
  AssociatedTokenAccount,
  Mint,
  Pubkey,
  Seeds,
  Signer,
  SystemAccount,
  TokenAccount,
  TokenProgram,
  UncheckedAccount,
  u64,
  u8,
} from "@solanaturbine/poseidon";

export default class EscrowProgram {
  static PROGRAM_ID = new Pubkey("11111111111111111111111111111111");

  make(
    maker: Signer,
    makerMint: Mint,
    takerMint: Mint,
    makerAta: AssociatedTokenAccount,
    auth: UncheckedAccount,
    vault: TokenAccount,
    escrow: EscrowState,
    depositAmount: u64,
    offerAmount: u64,
    seed: u64
  ) {
    makerAta.derive(makerMint, maker.key);
    auth.derive(["auth"]);
    vault.derive(["vault", escrow.key], makerMint, auth.key).init();
    escrow.derive(["escrow", maker.key, seed.toBytes()]).init();

    escrow.authBump = auth.getBump();
    escrow.vaultBump = vault.getBump();
    escrow.escrowBump = escrow.getBump();

    escrow.maker = maker.key;
    escrow.makerMint = makerMint.key;
    escrow.takerMint = takerMint.key;
    escrow.amount = offerAmount;
    escrow.seed = seed;

    TokenProgram.transfer(
      makerAta, // from
      vault, // to
      maker, // authority
      depositAmount // amount to transferred
    );
  }

  refund(
    maker: Signer,
    makerMint: Mint,
    makerAta: AssociatedTokenAccount,
    auth: UncheckedAccount,
    vault: TokenAccount,
    escrow: EscrowState
  ) {
    makerAta.derive(makerMint, maker.key);
    auth.derive(["auth"]);
    vault.derive(["vault", escrow.key], makerMint, auth.key);
    escrow
      .derive(["escrow", maker.key, escrow.seed.toBytes()])
      .has([maker])
      .close(maker);

    TokenProgram.transfer(
      vault,
      makerAta,
      auth,
      vault.amount,
      ["auth", escrow.authBump.toBytes()] // Seeds for the PDA signing
    );
  }

  take(
    taker: Signer,
    maker: SystemAccount,
    takerMint: Mint,
    makerMint: Mint,
    takerAta: AssociatedTokenAccount,
    takerReceiveAta: AssociatedTokenAccount,
    makerReceiveAta: AssociatedTokenAccount,
    auth: UncheckedAccount,
    vault: TokenAccount,
    escrow: EscrowState
  ) {
    takerAta.derive(takerMint, taker.key);
    takerReceiveAta.derive(makerMint, taker.key).initIfNeeded();
    makerReceiveAta.derive(takerMint, maker.key).initIfNeeded();
    auth.derive(["auth"]);
    vault.derive(["vault", escrow.key], makerMint, auth.key);
    escrow
      .derive(["escrow", maker.key, escrow.seed.toBytes()])
      .has([maker, makerMint, takerMint])
      .close(maker);

    TokenProgram.transfer(takerAta, makerReceiveAta, taker, escrow.amount);

    // Explicitly define the seeds for the PDA signing
    let seeds: Seeds = ["auth", escrow.authBump.toBytes()];
    TokenProgram.transfer(vault, takerReceiveAta, auth, vault.amount, seeds);
  }
}

export interface EscrowState extends Account {
  maker: Pubkey;
  makerMint: Pubkey;
  takerMint: Pubkey;
  amount: u64;
  seed: u64;
  authBump: u8;
  escrowBump: u8;
  vaultBump: u8;
}

Rust (Anchor)

Now, let's look at the equivalent Rust code using the Anchor framework.

use anchor_lang::prelude::*;
use anchor_spl::{
    token::{
        TokenAccount, Mint, Token, transfer as transfer_spl, Transfer as TransferSPL,
    },
    associated_token::AssociatedToken,
};
declare_id!("11111111111111111111111111111111");
#[program]
pub mod escrow_program {
    use super::*;
    pub fn make(
        ctx: Context<MakeContext>,
        deposit_amount: u64,
        offer_amount: u64,
        seed: u64,
    ) -> Result<()> {
        ctx.accounts.escrow.auth_bump = ctx.bumps.auth;
        ctx.accounts.escrow.vault_bump = ctx.bumps.vault;
        ctx.accounts.escrow.escrow_bump = ctx.bumps.escrow;
        ctx.accounts.escrow.maker = ctx.accounts.maker.key();
        ctx.accounts.escrow.maker_mint = ctx.accounts.maker_mint.key();
        ctx.accounts.escrow.taker_mint = ctx.accounts.taker_mint.key();
        ctx.accounts.escrow.amount = offer_amount;
        ctx.accounts.escrow.seed = seed;
        let cpi_accounts = TransferSPL {
            from: ctx.accounts.maker_ata.to_account_info(),
            to: ctx.accounts.vault.to_account_info(),
            authority: ctx.accounts.maker.to_account_info(),
        };
        let cpi_ctx = CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            cpi_accounts,
        );
        transfer_spl(cpi_ctx, deposit_amount)?;
        Ok(())
    }
    pub fn refund(ctx: Context<RefundContext>) -> Result<()> {
        let cpi_accounts = TransferSPL {
            from: ctx.accounts.vault.to_account_info(),
            to: ctx.accounts.maker_ata.to_account_info(),
            authority: ctx.accounts.auth.to_account_info(),
        };
        let signer_seeds = &[&b"auth"[..], &[ctx.accounts.escrow.auth_bump]];
        let binding = [&signer_seeds[..]];
        let cpi_ctx = CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            cpi_accounts,
            &binding,
        );
        transfer_spl(cpi_ctx, ctx.accounts.vault.amount)?;
        Ok(())
    }
    pub fn take(ctx: Context<TakeContext>) -> Result<()> {
        let cpi_accounts = TransferSPL {
            from: ctx.accounts.taker_ata.to_account_info(),
            to: ctx.accounts.maker_receive_ata.to_account_info(),
            authority: ctx.accounts.taker.to_account_info(),
        };
        let cpi_ctx = CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            cpi_accounts,
        );
        transfer_spl(cpi_ctx, ctx.accounts.escrow.amount)?;
        let cpi_accounts = TransferSPL {
            from: ctx.accounts.vault.to_account_info(),
            to: ctx.accounts.taker_receive_ata.to_account_info(),
            authority: ctx.accounts.auth.to_account_info(),
        };
        let signer_seeds = &[&b"auth"[..], &[ctx.accounts.escrow.auth_bump]];
        let binding = [&signer_seeds[..]];
        let cpi_ctx = CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            cpi_accounts,
            &binding,
        );
        transfer_spl(cpi_ctx, ctx.accounts.vault.amount)?;
        Ok(())
    }
}
#[derive(Accounts)]
#[instruction(seed:u64)]
pub struct MakeContext<'info> {
    #[account(
        mut,
        associated_token::mint = maker_mint,
        associated_token::authority = maker,
    )]
    pub maker_ata: Account<'info, TokenAccount>,
    #[account(seeds = [b"auth"], bump)]
    /// CHECK: This acc is safe
    pub auth: UncheckedAccount<'info>,
    #[account(mut)]
    pub maker: Signer<'info>,
    #[account()]
    pub maker_mint: Account<'info, Mint>,
    #[account(
        init,
        payer = maker,
        seeds = [b"vault",
        escrow.key().as_ref()],
        token::mint = maker_mint,
        token::authority = auth,
        bump,
    )]
    pub vault: Account<'info, TokenAccount>,
    #[account()]
    pub taker_mint: Account<'info, Mint>,
    #[account(
        init,
        payer = maker,
        space = 123,
        seeds = [b"escrow",
        maker.key().as_ref(),
        seed.to_le_bytes().as_ref()],
        bump,
    )]
    pub escrow: Account<'info, EscrowState>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct RefundContext<'info> {
    #[account(
        mut,
        associated_token::mint = maker_mint,
        associated_token::authority = maker,
    )]
    pub maker_ata: Account<'info, TokenAccount>,
    #[account(
        mut,
        seeds = [b"vault",
        escrow.key().as_ref()],
        token::mint = maker_mint,
        token::authority = auth,
        bump,
    )]
    pub vault: Account<'info, TokenAccount>,
    #[account()]
    pub maker_mint: Account<'info, Mint>,
    #[account(mut)]
    pub maker: Signer<'info>,
    #[account(
        mut,
        seeds = [b"escrow",
        maker.key().as_ref(),
        escrow.seed.to_le_bytes().as_ref()],
        has_one = maker,
        bump,
        close = maker,
    )]
    pub escrow: Account<'info, EscrowState>,
    #[account(seeds = [b"auth"], bump)]
    /// CHECK: This acc is safe
    pub auth: UncheckedAccount<'info>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct TakeContext<'info> {
    #[account(
        init_if_needed,
        payer = taker,
        associated_token::mint = maker_mint,
        associated_token::authority = taker,
    )]
    pub taker_receive_ata: Account<'info, TokenAccount>,
    #[account(mut)]
    pub taker: Signer<'info>,
    #[account(
        mut,
        seeds = [b"vault",
        escrow.key().as_ref()],
        token::mint = maker_mint,
        token::authority = auth,
        bump,
    )]
    pub vault: Account<'info, TokenAccount>,
    #[account(
        mut,
        associated_token::mint = taker_mint,
        associated_token::authority = taker,
    )]
    pub taker_ata: Account<'info, TokenAccount>,
    #[account(seeds = [b"auth"], bump)]
    /// CHECK: This acc is safe
    pub auth: UncheckedAccount<'info>,
    #[account(mut)]
    pub maker: SystemAccount<'info>,
    #[account(
        mut,
        seeds = [b"escrow",
        maker.key().as_ref(),
        escrow.seed.to_le_bytes().as_ref()],
        has_one = maker,
        has_one = maker_mint,
        has_one = taker_mint,
        bump,
        close = maker,
    )]
    pub escrow: Account<'info, EscrowState>,
    #[account()]
    pub taker_mint: Account<'info, Mint>,
    #[account()]
    pub maker_mint: Account<'info, Mint>,
    #[account(
        init_if_needed,
        payer = taker,
        associated_token::mint = taker_mint,
        associated_token::authority = maker,
    )]
    pub maker_receive_ata: Account<'info, TokenAccount>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
}
#[account]
pub struct EscrowState {
    pub maker: Pubkey,
    pub maker_mint: Pubkey,
    pub taker_mint: Pubkey,
    pub amount: u64,
    pub seed: u64,
    pub auth_bump: u8,
    pub escrow_bump: u8,
    pub vault_bump: u8,
}

Program

In TypeScript, the program is defined as a class with a static PROGRAM_ID to specify the program ID.

import { Pubkey } from "@solanaturbine/poseidon";
export default class EscrowProgram {
  static PROGRAM_ID = new Pubkey("11111111111111111111111111111111");
}

And poseidon will transpile it into the following Rust code.

use anchor_lang::prelude::*;
declare_id!("11111111111111111111111111111111");
#[program]
pub mod escrow_program {
    use super::*;
}

Notice that Anchor will generate the program ID for you.

Get your program IDs with this command inside your Anchor project.

$ anchor keys list
# Output
# <program_name>: <program_id>

Instruction

To define instructions in TypeScript, you would typically define methods inside the program class.

import { Pubkey } from "@solanaturbine/poseidon";

export default class EscrowProgram {
  static PROGRAM_ID = new Pubkey("11111111111111111111111111111111");

  make() {}
  refund() {}
  take() {}
}

And the context for each instruction is implicit in the method parameters.

import {
  Account,
  AssociatedTokenAccount,
  Mint,
  Pubkey,
  Seeds,
  Signer,
  SystemAccount,
  TokenAccount,
  TokenProgram,
  UncheckedAccount,
  u64,
  u8,
} from "@solanaturbine/poseidon";

export default class EscrowProgram {
  static PROGRAM_ID = new Pubkey("11111111111111111111111111111111");

  make(
    maker: Signer,
    makerMint: Mint,
    takerMint: Mint,
    makerAta: AssociatedTokenAccount,
    auth: UncheckedAccount,
    vault: TokenAccount,
    escrow: EscrowState, // custom state account, will explain in the next section
    depositAmount: u64,
    offerAmount: u64,
    seed: u64
  ) {}
  refund(
    maker: Signer,
    makerMint: Mint,
    makerAta: AssociatedTokenAccount,
    auth: UncheckedAccount,
    vault: TokenAccount,
    escrow: EscrowState // custom state account, will explain in the next section
  ) {}
  take(
    taker: Signer,
    maker: SystemAccount,
    takerMint: Mint,
    makerMint: Mint,
    takerAta: AssociatedTokenAccount,
    takerReceiveAta: AssociatedTokenAccount,
    makerReceiveAta: AssociatedTokenAccount,
    auth: UncheckedAccount,
    vault: TokenAccount,
    escrow: EscrowState // custom state account, will explain in the next section
  ) {}
}

@solanaturbine/poseidon package provides the necessary types for defining instructions in TypeScript, such as Rust types (u8, u64, i8, i128, boolean, string), SPL types (Pubkey, AssociatedTokenAccount, Mint, TokenAccount, TokenProgram), Anchor account types (Signer, UncheckedAccount, SystemAccount), etc.

It will transpile the TypeScript code into the following Rust code.

use anchor_lang::prelude::*;
use anchor_spl::{
    token::{TokenAccount, Mint, Token},
    associated_token::AssociatedToken,
};
declare_id!("11111111111111111111111111111111");
#[program]
pub mod escrow_program {
    use super::*;
    pub fn make(
        ctx: Context<MakeContext>,
        deposit_amount: u64,
        offer_amount: u64,
        seed: u64,
    ) -> Result<()> {
        Ok(())
    }
    pub fn refund(ctx: Context<RefundContext>) -> Result<()> {
        Ok(())
    }
    pub fn take(ctx: Context<TakeContext>) -> Result<()> {
        Ok(())
    }
}
#[derive(Accounts)]
pub struct MakeContext<'info> {
    pub escrow: Account<'info, EscrowState>,
    pub taker_mint: Account<'info, Mint>,
    #[account(mut)]
    pub maker: Signer<'info>,
    /// CHECK: This acc is safe
    pub auth: UncheckedAccount<'info>,
    pub maker_ata: Account<'info, TokenAccount>,
    pub vault: Account<'info, TokenAccount>,
    pub maker_mint: Account<'info, Mint>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct RefundContext<'info> {
    pub escrow: Account<'info, EscrowState>,
    pub maker_ata: Account<'info, TokenAccount>,
    #[account(mut)]
    pub maker: Signer<'info>,
    pub maker_mint: Account<'info, Mint>,
    /// CHECK: This acc is safe
    pub auth: UncheckedAccount<'info>,
    pub vault: Account<'info, TokenAccount>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct TakeContext<'info> {
    #[account(mut)]
    pub maker: SystemAccount<'info>,
    pub taker_ata: Account<'info, TokenAccount>,
    pub vault: Account<'info, TokenAccount>,
    pub escrow: Account<'info, EscrowState>,
    #[account(mut)]
    pub taker: Signer<'info>,
    pub maker_mint: Account<'info, Mint>,
    pub taker_mint: Account<'info, Mint>,
    pub taker_receive_ata: Account<'info, TokenAccount>,
    pub maker_receive_ata: Account<'info, TokenAccount>,
    /// CHECK: This acc is safe
    pub auth: UncheckedAccount<'info>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
}

You might notice that the accounts defined in TypeScript are automatically transpiled into the Rust account struct, which is how the instruction context is typically organized.

If you have additional parameters that are not accounts, you can pass them as arguments after the accounts. Like make instruction, it has depositAmount, offerAmount, and seed as additional parameters.

State

State is the data stored on Solana accounts.

Notice that the term state is used to describe the data stored on Solana accounts, while the term account is used to describe the Solana account itself.

Define Custom State Accounts

In TypeScript, custom state accounts are defined as an Interface that extends Account.

import { Account, Pubkey, u64, u8 } from "@solanaturbine/poseidon";

export interface EscrowState extends Account {
  maker: Pubkey;
  makerMint: Pubkey;
  takerMint: Pubkey;
  amount: u64;
  seed: u64;
  authBump: u8;
  escrowBump: u8;
  vaultBump: u8;
}

You can use types from the @solanaturbine/poseidon package to define the fields of the custom state account.

After transpiling, the custom state account will be defined as a struct in Rust.

#[account]
pub struct EscrowState {
    pub maker: Pubkey,
    pub maker_mint: Pubkey,
    pub taker_mint: Pubkey,
    pub amount: u64,
    pub seed: u64,
    pub auth_bump: u8,
    pub escrow_bump: u8,
    pub vault_bump: u8,
}

State Manipulation

To set the state of an account, you can simply assign the values to the fields of the account.

// ...

export default class EscrowProgram {
  static PROGRAM_ID = new Pubkey("11111111111111111111111111111111");
  make(
    maker: Signer,
    escrow: EscrowState,
    makerAta: AssociatedTokenAccount,
    makerMint: Mint,
    takerMint: Mint,
    auth: UncheckedAccount,
    vault: TokenAccount,
    depositAmount: u64,
    offerAmount: u64,
    seed: u64
  ) {
    escrow.maker = maker.key;
    escrow.makerMint = makerMint.key;
    escrow.takerMint = takerMint.key;

    escrow.amount = offerAmount;
    escrow.seed = seed;
  }
}

The corresponding Rust code will be generated as follows.

// ...

declare_id!("11111111111111111111111111111111");
#[program]
pub mod escrow_program {
    use super::*;
    pub fn make(
        ctx: Context<MakeContext>,
        deposit_amount: u64,
        offer_amount: u64,
        seed: u64,
    ) -> Result<()> {
        ctx.accounts.escrow.maker = ctx.accounts.maker.key();
        ctx.accounts.escrow.maker_mint = ctx.accounts.maker_mint.key();
        ctx.accounts.escrow.taker_mint = ctx.accounts.taker_mint.key();

        ctx.accounts.escrow.amount = offer_amount;
        ctx.accounts.escrow.seed = seed;
    }
}

Also if you want to do some arithmetic operations, @solanaturbine/poseidon package provides the necessary types for that.

Check out vote example to see how to use them. Here's a snippet from the example:

// initialize the state
state.vote = new i64(0);
// increment the state
state.vote = state.vote.add(1);
// decrement the state
state.vote = state.vote.sub(1);

Poseidon Type Reference

TypeAnchorPoseidon
BooleanboolBoolean
Integeru8/u16/u32/i8/i16/i32u8/u16/u32/i8/i16/i32
StringStringString<N>
VectorVec<T>Vec<T, N>

where N is the max length of the type.

Program Derived Address (PDA)

Program Derived Addresses (PDAs) are a way to derive a new address from a seed and a program id. This is useful for creating new accounts that are tied to a specific program.

For example, in the escrow program, the escrow account is created as a PDA. This ensures that the escrow account is tied to the escrow program and cannot be controlled by any other program or entity.

To define an account as a PDA with @solanaturbine/poseidon, you can use the derive method for every account by specifying your seed within an array ([]) as the first parameter.

seed can be a string, a number, a Pubkey, or even the combination of them.

// ...
export default class EscrowProgram {
  static PROGRAM_ID = new Pubkey("11111111111111111111111111111111");

  make() {
    // Wrap the seed with an array
    auth.derive(["auth"]); // seed: string("auth")
    vault.derive(["vault", escrow.key]); // seed: string("vault") + Pubkey(escrow.key)
    escrow.derive(["escrow", maker.key, seed.toBytes()]); // seed: string("escrow") + Pubkey(maker.key) + number(seed.toBytes())

    escrow.authBump = auth.getBump();
    escrow.vaultBump = vault.getBump();
    escrow.escrowBump = escrow.getBump();
  }
}

The magic behind PDA is that it uses the program id as the base address and the seed(as we created above) with a bump(a number between 0 to 255) as the offset to derive a new address, which is unique and off the Ed25519 curve, without a corresponding private key. This technique guarantees that the derived address is only controllable by the program that created it.

Normally, we'll store the bump value in the state account to ensure that the program can always derive the same address and save the cost of bump calculation during the runtime. You can use the getBump method to get the bump value for the account.

The corresponding Rust code will be generated as follows.

// ...

declare_id!("11111111111111111111111111111111");

#[program]
pub mod escrow_program {
    use super::*;
    pub fn make(
        ctx: Context<MakeContext>,
    ) -> Result<()> {
        ctx.accounts.escrow.auth_bump = ctx.bumps.auth;
        ctx.accounts.escrow.vault_bump = ctx.bumps.vault;
        ctx.accounts.escrow.escrow_bump = ctx.bumps.escrow;
        Ok(())
    }
}

#[derive(Accounts)]
#[instruction(seed:u64)]
pub struct MakeContext<'info> {
    #[account(seeds = [b"auth"], bump)]
    /// CHECK: This acc is safe
    pub auth: UncheckedAccount<'info>,
    #[account(
        seeds = [b"vault", escrow.key().as_ref()],
        bump,
    )]
    pub vault: Account<'info, TokenAccount>,
    #[account(
        seeds = [b"escrow", maker.key().as_ref(), seed.to_le_bytes().as_ref()],
        bump,
    )]
    pub escrow: Account<'info, EscrowState>,
}

If you're creating a PDA with a given bump, you can use the deriveWithBump method with the bump following the seed instead. See the example below or the vault example for more details:

auth.deriveWithBump(["auth", state.key], state.authBump);

We highly recommend you to go through the official documentation to understand the concept of PDAs in Solana.

Account Constraints

We have seen an example of how to define account constraints in previous sections while we're creating PDAs, e.g. #[account(seeds = [b"auth"], bump)]. In this section, we will discuss the constraints in more detail.

Anchor provides a way to define constraints on accounts that are passed to the program by using the #[account(..)] attribute. These constraints are used to ensure that the account passed to the program is the correct account. This is done by checking the account's address and the account's data.

Here are some commonly used constraints if you want to define them in TypeScript:

export default class EscrowProgram {
  static PROGRAM_ID = new Pubkey("11111111111111111111111111111111");

  make(
    escrow: EscrowState,
    makerMint: Mint,
    auth: UncheckedAccount,
    vault: TokenAccount
  ) {
    // `init` constraint: create a new account
    vault.derive(["vault", escrow.key], makerMint, auth.key).init();
  }

  refund(maker: Signer, escrow: EscrowState) {
    escrow
      .derive(["escrow", maker.key, escrow.seed.toBytes()])
      .has([maker]) // `has_one` constraint: check if the data stored inside the `escrow.maker` is the same as the `maker` account
      .close(maker); // `close` constraint: close the account after the instruction is executed, transfer the remaining SOL to the `maker` account
  }

  take(
    taker: Signer,
    maker: SystemAccount,
    takerAta: AssociatedTokenAccount,
    makerMint: Mint,
    takerMint: Mint,
    escrow: EscrowState
  ) {
    takerAta
      .derive(makerMint, taker.key) // SPL constraints: check if the `taker` account has the same mint as the `makerMint` account and the authority is the `taker` account
      .initIfNeeded(); // `init_if_needed` constraint: initialize the account if it doesn't exist
    escrow
      .derive(["escrow", maker.key, escrow.seed.toBytes()])
      .has([maker, makerMint, takerMint]) // `has_one` constraint: can specify multiple accounts to check
      .close(maker);
  }
}

export interface EscrowState extends Account {
  maker: Pubkey;
  makerMint: Pubkey;
  takerMint: Pubkey;
  amount: u64;
  seed: u64;
  authBump: u8;
  escrowBump: u8;
  vaultBump: u8;
}

You can simply define the constraints by chaining the constraints methods after the account you want to check and make sure the .derive() method is called before the other constraints methods.

Normal Constraints

init (and space)

.init() method is used to create a new account. It is used to create a new account with the given data. Poseidon will automatically calculate the space required for the account based on the how you define the account in the state interface and specify the space in Rust with space constraint.

initIfNeeded

Exact same functionality as the init constraint but only runs if the account does not exist yet1.

If you're using .initIfNeeded() method, you should add additional feature flags inside your Cargo.toml file under your program's directory:

[features]
anchor-lang = { version = "xxx", features = ["init-if-needed"]}

seed (and bump)

This is the constraint we use to define PDAs.

The seed constraint is used to derive a new address from the base address. The bump value is a number between 0 to 255 that is used to derive a new address.

Use the .derive([seed]) method to derive a new address and use the .getBump() method to get the bump value for the account.

Use the .deriveWithBump([seed], bump) method to derive a new address with a bump value if you're creating a PDA with a bump.

The seed and bump constraints are required to use together to derive a new address.

close

.close(rentReceiver) method is used to close the account after the instruction is executed. It will transfer the remaining SOL to the account(rentReceiver) passed to the method.

has (or has_one in Anchor)

.has([]) in TypeScript (or has_one constraint in Anchor) is used to check if the data stored inside the account is the same as the data passed to the method. Like in the refund method, we're checking if the maker account's Pubkey is the same as the one stored inside escrow.maker.

And has constraint allows you to check multiple accounts at once. Like in the take method, you can check if the maker, makerMint, and takerMint accounts are the same as the ones stored inside the escrow account.

SPL Constraints

mint and authority

If the account is a TokenAccount, .derive([seed], mint, authority) method is used to check if the account has the same mint as the mint account and the authority is the authority account.

You can use it with the init constraint to derive and initialize a new TokenAccount, like vault account in the make method.

vault.derive(["vault", escrow.key], makerMint, auth.key).init();

For accounts of type AssociatedTokenAccount, .derive(mint, authority) is used instead.

vault.derive(makerMint, auth.key).initIfNeeded();
1

Check the Anchor documentation for more information on constraints.

Cross Program Invocation (CPI)

When a program invokes another program, it is called a cross program invocation (CPI). This is a powerful feature of Solana that allows programs to interact with each other. This is useful when you want to separate the logic of your program into multiple programs, or when you want to reuse the logic of another program.

@solanaturbine/poseidon provides a few commonly used program like TokenProgram and SystemProgram for you to invoke the corresponding instructions.

Invoking Token Program

To transfer tokens inside your program, you can use the transfer method from the TokenProgram. Here's an example of how to transfer tokens from token accounts which controlled by different types of owner, one is a user's (associated) token account and the other is a PDA's token account:

// ...

export default class EscrowProgram {
  static PROGRAM_ID = new Pubkey("11111111111111111111111111111111");

  take(
    taker: Signer,
    maker: SystemAccount,
    makerReceiveAta: AssociatedTokenAccount,
    takerAta: AssociatedTokenAccount,
    takerReceiveAta: AssociatedTokenAccount,
    makerMint: Mint,
    takerMint: Mint,
    auth: UncheckedAccount,
    vault: TokenAccount,
    escrow: EscrowState
  ) {
    makerReceiveAta.derive(takerMint, maker.key).initIfNeeded(); // Check if the associated token account is initialized
    takerAta.derive(takerMint, taker.key); // Don't need to check if the ATA is initialized, because if it's not, the transfer will fail
    takerReceiveAta.derive(makerMint, taker.key).initIfNeeded(); // Check if the associated token account is initialized
    auth.derive(["auth"]);
    vault.derive(["vault", escrow.key], makerMint, auth.key);
    escrow
      .derive(["escrow", maker.key, escrow.seed.toBytes()])
      .has([maker, makerMint, takerMint]) // Check if the expected accounts are the same as the provided accounts
      .close(maker);

    // Cross program invocation
    // Transfer tokens from taker's ATA to maker's ATA
    TokenProgram.transfer(
      takerAta, // from
      makerReceiveAta, // to
      taker, // authority
      escrow.amount // amount to be sent
    );

    // Cross program invocation
    // Transfer tokens from `vault` account to taker's ATA
    // Seeds are used for signing the transaction since the `vault` account is owned by the `auth` PDA under the escrow program
    let seeds: Seeds = ["auth", escrow.authBump.toBytes()];
    TokenProgram.transfer(
      vault, // from
      takerReceiveAta, // to
      auth, // authority
      vault.amount, // amount to be sent
      seeds // seeds will be at the last arguments if needed
    );
  }
}
export interface EscrowState extends Account {
  maker: Pubkey;
  makerMint: Pubkey;
  takerMint: Pubkey;
  amount: u64;
  seed: u64;
  authBump: u8;
  escrowBump: u8;
  vaultBump: u8;
}

In the example above, we transfer tokens from the taker's ATA to the maker's ATA and from the vault to the taker's ATA. We use the TokenProgram to transfer the tokens.

Here's the corresponding Rust code for the transfer CPI:

declare_id!("11111111111111111111111111111111");
#[program]
pub mod escrow_program {
    use super::*;
    pub fn make(
        ctx: Context<MakeContext>,
        deposit_amount: u64,
        offer_amount: u64,
        seed: u64,
    ) -> Result<()> {
        let cpi_accounts = TransferSPL {
            from: ctx.accounts.maker_ata.to_account_info(),
            to: ctx.accounts.vault.to_account_info(),
            authority: ctx.accounts.maker.to_account_info(),
        };
        let cpi_ctx = CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            cpi_accounts,
        );
        transfer_spl(cpi_ctx, deposit_amount)?;
        Ok(())
    }
    pub fn refund(ctx: Context<RefundContext>) -> Result<()> {
        let cpi_accounts = TransferSPL {
            from: ctx.accounts.vault.to_account_info(),
            to: ctx.accounts.maker_ata.to_account_info(),
            authority: ctx.accounts.auth.to_account_info(),
        };
        let signer_seeds = &[&b"auth"[..], &[ctx.accounts.escrow.auth_bump]];
        let binding = [&signer_seeds[..]];
        let cpi_ctx = CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            cpi_accounts,
            &binding,
        );
        transfer_spl(cpi_ctx, ctx.accounts.vault.amount)?;
        Ok(())
    }
}

Invoking System Program

It's quite similar to how you invoke the TokenProgram. Here's an example of how to invoke transfer instruction in SystemProgram:

// Invoke by normal account
SystemProgram.transfer(
  owner, // from
  vault, // to
  amount // amount to be sent
);

// Invoke by PDA
SystemProgram.transfer(
  vault, // from
  owner, // to
  amount, // amount to be sent
  ["vault", state.key, state.authBump] // seeds will be at the last arguments if needed
);

Can check the full codebase in the vault example.

Other Resources