# About SolMeet

SolMeet hosts free in-person monthly workshops for Solana developers in Taipei, Taiwan. Learn how to build Dapps, DeFi protocols, NFT projects, games, and more on the blockchain. Join a community of professional developers, founders, and mentors!

You can learn more about us through this [deck](https://docs.google.com/presentation/d/1H6O4DLnR9R2bzIZBgyYlBBnpljPa08ra6es_pEESpgc/edit?usp=sharing).

## Our Motivation

* Train our own devs
* Create an open environment for learning blockchain technology, for **FREE**
* Enjoy sharing. Resolving problems and establishing a new culture **is FUN**

## Our Vision

* Build a free space where devs can learn and teams can find talents
* Help popularize Solana tech in Taipei, Asia, and even the world.
* Promote the open-source culture and cultivate talents for the next web3 community


# #1 - A Starter Kit for New Solana Developer

**Author:** [@ironaddicteddog](https://twitter.com/ironaddicteddog)

***\[Updated at 2022.3.31]***

## TL; DR

* What makes Solana so fast and efficient?
* What should I learn at minimum to understand how Solana program works?
* How can I setup my the environment quickly?
* What is the best practice to develop Solana programs?

## Before We Start

### Prerequisites

* [**Rust**](https://github.com/rust-lang/rust)
* [**Typescript**](https://github.com/microsoft/TypeScript)
* [Solidity](https://github.com/ethereum/solidity) (nice to have)

### Why Solana?

* It's a new blockchain paradigm evolved from Ethereum (more or less)
* It's blazing fast and efficient, meaning it will be cheap and affordable for average users
* It's a good investment on modern web technology (Rust / Typescript, etc)

### Why Rust?

* Rust is fast
* Rust is safer (compared to C++)
* **Rust can be compiled to WASM**
* (Almost) every programmable blockchain has a rust implementation
  * [OpenEthereum (Eth 1.0 Client, formerly Parity)](https://github.com/openethereum/openethereum)
  * [Lighthouse (Eth 2.0 Client)](https://github.com/sigp/lighthouse)
  * [Near protocol](https://github.com/near)
  * ...and of course, **Solana**

### Compare Solana to Ethereum, what are the pros and cons?

#### Pros

* **Parallelism is the secret sauce** making it blazing fast
* Efficient network synchronization using [PoH Clock](https://docs.solana.com/cluster/synchronization)
  * [VDF (in general definition)](https://github.com/solana-labs/solana/issues/388)
  * Verification can be parallelized
* Efficient PoS Consensus
  * [Tower BFT](https://docs.solana.com/implemented-proposals/tower-bft), a specialized PBFT designed for PoH

#### Cons

* Rather high [hardware requirements](https://docs.solana.com/running-validator/validator-reqs#hardware-recommendations) to run a validator
  * 12 cores CPU
  * N cores GPU
  * 128GB RAM

### Structure of this Kit

1. Hello world (for environment setup)
2. Escrow program using vanilla Rust (for learning core concepts)
3. Escrow program using Anchor (for learning the best practice)

## 1. Hello World!

### Goal

* Setup the development environment
* Get familiar with tools such as:
  * `cargo`
    * `cargo build-bpf`
  * `rustup`
  * `solana-cli`
    * `solana deploy`
  * `solana-test-validator`
  * `solana-web3.js`

### Install Rust and Solana Cli

* See [this doc](https://github.com/solana-labs/solana#1-install-rustc-cargo-and-rustfmt) for more details

#### Install `rustup`

```bash
$ curl https://sh.rustup.rs -sSf | sh
...

$ rustup component add rustfmt
...

$ rustup update
...

$ rustup install 1.59.0
...
```

#### Install `solana-cli`

* See [this doc](https://docs.solana.com/cli/install-solana-cli-tools) for more details

```bash
$ 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](https://github.com/solana-labs/solana#1-install-rustc-cargo-and-rustfmt) for more installation details.

#### Generate Keypair

```
$ 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
```

#### Config to Local Cluster

```bash
$ solana config set --url localhost
...
```

#### Install `rust-analyzer`, `Better TOML` and `crates` (Optional)

* See [this repo](https://github.com/rust-analyzer/rust-analyzer) for more details

`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.

#### Install Additional Dependencies (Optional)

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
...
```

### Build and Deploy

* See [this repo](https://github.com/solana-labs/example-helloworld) for full code base

First, let's clone the repo:

```bash
$ 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:

```bash
$ 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:

```bash
$ cd src/program-rust
$ cargo build-bpf
...
```

Deploy the program after compilation, :

```bash
$ solana program deploy target/deploy/helloworld.so
...
```

> If you encounter an insuffficient fund error, you may have to request for an aidrop:
>
> ```bash
> $ solana airdrop 1
> ```

### Say Hello World

First, we need to modify the `PROGRAM_PATH` in `src/client/hello_world.ts`:

```typescript
// 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:

```typescript
// 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],
  );
}
...
```

## 2. **Escrow Program (using vanilla Rust)**

### Goal

* Learn Solana account model and core concepts such as:
  * Account model
  * Program Architecture
  * Program Derived Address (PDA)
  * Cross-Program Invocation (CPI)
    * `invoke`
    * `invoke_signed`
* This section is extracted from this awesome tutorial: [Programming on Solana - An Introduction (by paulx)](https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction). Some of the explanations in this doc are more comprehensive and clearer in the original post. I strongly recommend you to read through the post at least once.
* **Program Architecture**
  * `lib.rs`: registering modules
  * `entrypoint.rs`: entrypoint to the program
  * `instruction.rs`: program API, (de)serializing instruction data
  * `processor.rs`: program logic
  * `state.rs`: program objects, (de)serializing state
  * `error.rs`: program specific errors
* See [this repo](https://github.com/paul-schaaf/solana-escrow) for full code base

### Core Concepts

#### Account

![](https://i.imgur.com/7kUb9di.png)

* Accounts are used to store state
* Accounts are owned by programs
* Only the account owner may debit an account and adjust its data
* All accounts to be written to or read must be passed into the entrypoint
* All internal Solana internal account information are saved into fields on the account (opens new window)but never into the data field which is solely meant for user space information
* Developers should use the data field to save data inside accounts

#### Program

* Solana programs are **stateless**
* Each program is processed by its **BPF Loader** and has an entrypoint whose structure depends on which BPF Loader is used
* In theory, programs have full autonomy over the accounts they own. It is up to the program's creator to limit this autonomy and up to the users of the program to verify the program's creator has really done so
* The flow of a program using this structure looks like this:
  * Someone calls the **entrypoint**
  * The **entrypoint** forwards the arguments to the **processor**
  * The **processor** asks **instruction** module to decode the instruction\_data argument from the entrypoint function.
  * Using the decoded data, the processor will now decide which processing function to use to process the request.
  * The processor may use **state** module to encode state into or decode the state of an account which has been passed into the entrypoint.
* When writing Solana programs, be mindful of the fact that any accounts may be passed into the entrypoint, including different ones than those defined in the API inside `instruction.rs`. It's the program's responsibility to check that received accounts == expected accounts

#### Instruction

* If you are familiar of Ethereum, think of Solana instructions as Ethereum transcations, while Solana transaction, which can wrap multiple instructions, is anologous to Ethereum [`multicall`](https://etherscan.io/address/0x5ba1e12693dc8f9c48aad8770482f4739beed696#code)

#### SPL `token` Program

* The token program owns token accounts which inside their data field hold relevant information
* the token program also owns token mint accounts with relevant data
* each token account holds a reference to their token mint account, thereby stating which token mint they belong to
* the token program allows the (user space) owner of a token account to transfer its ownership to another address
* All internal Solana internal account information are saved into fields on the account but never into the data field which is solely meant for user space information

#### PDA

* Program Derived Addresses do not lie on the ed25519 curve and therefore **have no private key associated with them.**

#### Cross-Program Invocation

* When including a signed account in a program call, in all CPIs including that account made by that program inside the current instruction, the account will also be signed, i.e. the signature is extended to the CPIs.
* when a program calls `invoke_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 true

> To spend Solana SPL, you don't need to approve. Why?

#### Rent

* Rent is deducted from an account's balance according to their space requirements (i.e. the space an account and its fields take up in memory) regularly. **An account can, however, be made rent-exempt** if its balance is higher than some threshold that depends on the space it's consuming
* If an account has no balance left, it will be purged from memory by the runtime after the transaction (you can see this when going navigating to an account that has been closed in the explorer)
* "closing" instructions must set the data field properly, even if the intent is to have the account be purged from memory after the transaction
* In any call to a program that is of the "close" kind, i.e. where you set an account's lamports to zero so it's removed from memory after the transaction, make sure to either clear the data field or leave the data in a state that would be OK to be recovered by a subsequent transaction.
* Solana has sysvars that are parameters of the Solana cluster you are on. These sysvars can be accessed through accounts and store parameters such as what the current fee or rent is. As of solana-program version 1.6.5, sysvars can also be accessed without being passed into the entrypoint as an account.

### Escrow Program Overview

#### Flow

#### Account Relations

![](https://i.imgur.com/0r1svM7.png)

### Part 1

Fisrt, let's create a new project `solana-escrow`:

```bash
$ cargo new solana-escrow --lib
    Created library `solana-escrow` package

$ cd solana-escrow
```

Next, we update the `Cargo.toml` manifest to as follows:

```toml=
# 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`:

```rust=
// 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`:

```rust=
// 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:

```toml=
# Cargo.toml

...
[dependencies]
...
thiserror = "1.0.24"
```

Update `error.rs`:

```rust=
// 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:

```toml=
# Cargo.toml

...
[dependencies]
...
spl-token = {version = "3.1.1", features = ["no-entrypoint"]}
```

Update `processor.rs`:

```rust=
// 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(())
    }
}
```

### Part 2

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:

```toml=
# Cargo.toml
...
[dependencies]
...
arrayref = "0.3.6"
```

Update `state.rs`:

```rust=
// 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`:

```rust=
// 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`:

```rust=
// 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:

```bash
$ cargo build-bpf
...
```

### Part 3

Next, we can implement another instruction `Exchange` and its corresponding function `process_exchange`.

Update `instruction.rs`:

```rust=
// 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`:

```rust=
// 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:

```rust=
// 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:

```bash
$ cargo build-bpf
...
```

### Interact with the escrow program

* See [this repo](https://github.com/paul-schaaf/solana-escrow/tree/master/scripts) for more details

#### Basic setup

Now, we can write some client side code to interact with the escrow program.

First, let's install dependencies:

```bash
$ 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
```

#### Generate Keypairs

We have to generate keypairs for `alice`, `bob`, and the transaction `payer`. This can be done via `solana-keygen`:

```bash
$ 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:

```bash
$ solana address -k keys/id.json
9q9XLUDjDKj2cahaN44X9Mid2HGJtUauFvjJG8qocY5a
```

```json=
// id_pub.json

"9q9XLUDjDKj2cahaN44X9Mid2HGJtUauFvjJG8qocY5a"
```

> Don't forget the double quotes

#### Add Code Base

Here we add the client code base. Copy and paste the following files to your local code base:

* [`ts/setup.ts`](https://github.com/paul-schaaf/solana-escrow/blob/master/scripts/src/setup.ts)
* [`ts/utils`](https://github.com/paul-schaaf/solana-escrow/blob/master/scripts/src/utils.ts)
* [`ts/alice.ts`](https://github.com/paul-schaaf/solana-escrow/blob/master/scripts/src/alice.ts)
* [`ts/bob.ts`](https://github.com/paul-schaaf/solana-escrow/blob/master/scripts/src/bob.ts)

> Again, I strongly recommend you to clone the original code base and run it

#### Compile, Depoly and Setup

First, let's start the validator:

```bash
$ solana-test-validator
...
```

Compile and deploy the program:

```bash
$ 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:

```json=
// program_pub.json

"EKnr6pssVnPmoGJH3NgtCByF9jMDRnyDQZxkHqz1GBS2"
```

Also, update the predefined `terms.json` as follows:

```json=
// terms.json

{
  "aliceExpectedAmount": 3,
  "bobExpectedAmount": 5
}
```

Fund the transaction `payer` in advance:

```bash
$ solana transfer 9q9XLUDjDKj2cahaN44X9Mid2HGJtUauFvjJG8qocY5a 100 --allow-unfunded-recipient
```

#### Run the Client Code

Finally, let's run the client code:

First, run `setup.ts` to mint the tokens to be exchanged:

```bash
$ ts-node ts/setup.ts
...
```

Next, run `alice.ts` to initialize the escrow program:

```bash
$ ts-node ts/alice.ts
...
```

You can see how an instruction is constructed. The interger `0` assined to the `Uint8Array` represents the instruction `InitEscrow`:

```typescript=
// 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:

```bash
$ ts-node ts/bob.ts
...
```

## 3. Escrow Program (using Anchor)

### Goal

* Learn the best practice
* Why use Anchor?
  * Remove Boilerplate
  * Make Solana program [safer](https://twitter.com/armaniferrante/status/1411589634228772870)
  * Clearer code structure
* A good framework reduces the mental pressure and keep the precious attention resource to important things
* **I actually wrote another post explaining the whole thing.** See [this doc](https://hackmd.io/@ironaddicteddog/solana-anchor-escrow) to learn more.

## More Advanced Topics

* Open-sourced Projects
  * [Serum](https://github.com/project-serum)
  * [Raydium](https://github.com/raydium-io)
  * [Saber](https://github.com/saber-hq)
  * ...
* Solana Program Library
  * [`token` Program](https://github.com/solana-labs/solana-program-library/tree/master/token)
  * [`token-swap` Program](https://github.com/solana-labs/solana-program-library/tree/master/token-swap)
  * [Associated Token Account](https://spl.solana.com/associated-token-account)
  * ...
* [Anchor AMM](https://github.com/ironaddicteddog/anchor-amm)

## References

### General

* <https://medium.com/@asmiller1989/solana-transactions-in-depth-1f7f7fe06ac2>
* <https://hackmd.io/@adamisrusty/HkVyZHBoO>
* <https://2501babe.github.io/posts/solana101.html>
* <https://github.com/paul-schaaf/awesome-solana>
* <https://github.com/project-serum/awesome-serum>
* <https://solana.com/developers>

### Front-End Development

* <https://github.com/yihau/full-stack-solana-development>
* <https://github.com/yihau/solana-web3-demo>
* <https://github.com/raydium-io/raydium-ui>
* <https://github.com/thuglabs/create-dapp-solana-nextjs>

### Program Development

* <https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/#instruction-rs-part-1-general-code-structure-and-the-beginning-of-the-escrow-program-flow>
* <https://github.com/jstarry/solana-workshop-tw>
* <https://jstarry.notion.site/Program-deploys-29780c48794c47308d5f138074dd9838>
* <https://jstarry.notion.site/Transaction-Fees-f09387e6a8d84287aa16a34ecb58e239>

### Anchor Tutorials

* <https://hackmd.io/@ironaddicteddog/anchor\\_example\\_escrow>
* <https://github.com/ironaddicteddog/anchor-escrow>
* <https://github.com/ironaddicteddog/anchor-amm>
* <https://dev.to/dabit3/the-complete-guide-to-full-stack-solana-development-with-react-anchor-rust-and-phantom-3291>
* <https://2501babe.github.io/posts/anchor101.html>
* <https://www.brianfriel.xyz/learning-how-to-build-on-solana/>
* <https://project-serum.github.io/anchor/tutorials/tutorial-0.html>

### Core Technology

* <https://medium.com/solana-labs/proof-of-history-explained-by-a-water-clock-e682183417b8>
* <https://medium.com/solana-labs/proof-of-history-a-clock-for-blockchain-cf47a61a9274>
* <https://medium.com/solana-labs/sealevel-parallel-processing-thousands-of-smart-contracts-d814b378192>
* <https://medium.com/solana-labs/solanas-network-architecture-8e913e1d5a40>
* <https://medium.com/solana-labs/7-innovations-that-make-solana-the-first-web-scale-blockchain-ddc50b1defda>
* <https://jito-labs.medium.com/solana-validator-101-transaction-processing-90bcdc271143>

### Twitters

* <https://twitter.com/ironaddicteddog>
* <https://twitter.com/armaniferrante>
* <https://twitter.com/therealchaseeb>
* <https://twitter.com/jstrry>


# #2 - Introduction to Anchor

**Author:** [@ironaddicteddog](https://twitter.com/ironaddicteddog)

***\[Updated at 2022.12.18]***

> You can find the full code base [here](https://github.com/ironaddicteddog/anchor-escrow)

## What is Anchor?

There is a comprehensive explanation on the [official website](https://project-serum.github.io/anchor/getting-started/introduction.html). Let me just quote relative paragraphs here:

> Anchor is a framework for Solana's Sealevel runtime providing several convenient developer tools.
>
> If you're familiar with developing in Ethereum's Solidity, Truffle, web3.js, then the experience will be familiar. Although the DSL syntax and semantics are targeted at Solana, the high level flow of writing RPC request handlers, emitting an IDL, and generating clients from IDL is the same.

In short, Anchor gives you the following handy tools for developing Solana programs:

* **Rust crates and eDSL for writing Solana programs**
* **IDL specification**
* **TypeScript package for generating clients from IDL**
* **CLI and workspace management for developing complete applications**

You can watch [this awesome talk](https://youtu.be/cvW8EwGHw8U) given by Armani Ferrante at Breakpoint 2021 to feel the power of Anchor.

### Workflow

![](https://i.imgur.com/jkObSKO.jpg)

1. Develop the **program** (Smart Contract)
2. Build the program and export the **IDL**
3. Generate the **client** representation of program from the IDL to interact with the program

### Why Anchor?

* Productivity
  * Make Solana program more intuitive to understand
  * More clear buisness Logic
  * Remove a ton of biolderplate code
* Security
  * Customized Account Validation
    * Singer
    * Mut
    * ...
  * Discriminator
    * Discriminator is generated and inserted into the first 8 bytes of account data. Ex: `sha256("account:<MyAccountName>")[..8] || borsh(account_struct)`
    * Used for more secure account validation and function dispatch
    * See [this Twitter thread](https://twitter.com/armaniferrante/status/1411589634228772870) for more details
    * See [here](https://github.com/project-serum/anchor/blob/master/ts/src/program/namespace/index.ts#L53) and [here](https://github.com/project-serum/anchor/blob/master/lang/syn/src/codegen/program/dispatch.rs#L146) for the actual implementation

## Before We Start

### Why Rust? Why Solana?

You can refer to this [doc](https://hackmd.io/@ironaddicteddog/solana-starter-kit#Before-We-Start) for the motivations.

### Prerequisites

* [Solana Helloworld](https://hackmd.io/@ironaddicteddog/solana-starter-kit#1-Hello-World)
* [**Solana Escrow Program (using vanilla Rust)**](https://hackmd.io/@ironaddicteddog/solana-starter-kit#2-Escrow-Program-using-vanilla-Rust)

## Installation

Install `avm`:

```bash
$ cargo install --git https://github.com/coral-xyz/anchor avm --locked --force
...
```

Install latest `anchor` version:

```bash
$ avm install 0.26.0
...
$ avm use 0.26.0
...
```

> If you haven't installed `cargo`, please refer to this [doc](https://book.solmeet.dev/notes/solana-starter-kit#install-rust-and-solana-cli) for installation steps.

### Extra Dependencies on Linux (Optional)

You may have to install some extra dependencies on Linux (ex. Ubuntu):

```bash
$ sudo apt-get update && sudo apt-get upgrade && sudo apt-get install -y pkg-config build-essential libudev-dev
...
```

### Verify the Installation

Check if Anchor is successfully installed:

```bash
$ anchor --version
anchor-cli 0.26.0
```

## Escrow Program

> Reminder: you can find the full code base for this example [here](https://github.com/ironaddicteddog/anchor-escrow). However, I would strongly recommend you to go through the copy-paste with me to get familiar with the flow.

Next, let's develop an escrow program using Anchor. I strongly recommend you to go through [this tutorial](https://hackmd.io/@ironaddicteddog/solana-starter-kit#2-Escrow-Program-using-vanilla-Rust) if you are not familiar with escrow program yet.

### Overview

Since this program is extended from the original [Escrow Program](https://github.com/paul-schaaf/solana-escrow), I assumed you have gone through the [original blog post](https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/#instruction-rs-part-1-general-code-structure-and-the-beginning-of-the-escrow-program-flow) at least once.

However, there is one major difference between this exmaple and the original Escrow program: Instead of letting initializer create a token account to be reset to a PDA authority, we create a token account `Vault` that has both a PDA key and a PDA authority.

#### Initialize

![](https://hackmd.io/_uploads/Hkn1gdtuj.png)

`Initializer` can send a transaction to the escrow program to initialize the Vault. In this transaction, two new accounts: `Vault` and `EscrowState`, will be created and tokens (Token A) to be exchanged will be transfered from `Initializer` to `Vault`.

#### Cancel

![](https://hackmd.io/_uploads/ry0GNdKdo.png)

`Initializer` can also send a transaction to the escrow program to cancel the demand of escrow. The tokens will be transfered back to the `Initialzer` and both `Vault` and `EscrowState` will be closed in this case.

#### Exchange

![](https://hackmd.io/_uploads/HkhNE_tdi.png)

`Taker` can send a transaction to the escrow to exchange Token B for Token A. First, tokens (Token B) will be transfered from `Taker` to `Initializer`. Afterward, the tokens (Token A) kept in the Vault will be transfered to `Taker`. Finally, both `Vault` and `EscrowState` will be closed.

### Initialize the Program

First, let's start a fresh Anchor project:

```bash
$ anchor init anchor-escrow
...
```

This handy command will populate a project folder including the following files:

* `Cargo.toml`
* `Anchor.toml`
* `package.json`
* `tsconfig.json`
* ...

### Program Architecture

There are 3 main parts in the program:

* **Processor**: Main buisiness logic locates in processor
* **Account Context (Instructions)**: Instruction data packing/unpacking and account constraints and access control locate in Instruction handling part
* **Account**: Declaration of account owned by program locates in account part

### Dependencies

Before we dive into the program, we need add the missing dependencies in `Cargo.toml`:

```toml
# Cargo.toml

...
[dependencies]
anchor-lang = "0.20.1"
anchor-spl = {version = "0.20.1"}
spl-token = {version = "3.3.0", features = ["no-entrypoint"]}
```

### Update `program_id` (Optional)

There is a default `program_id` defined by `declare_id!` macro in `lib.rs`:

```rust
// lib.rs

...

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

...
```

Although we can use the default value just fine, I would strongly recommend to replace this with the actual `program_id`, which is the public key of the deploy key.

Get the public key of the deploy key:

```bash
$ anchor keys list
anchor_escrow: Hfd7V12kj9AENQjLpTozaPW6aT2rhPm3LSyjXZ5AbWH
```

Replace the default value of `program_id` with this new value:

```toml
# Anchor.toml

[programs.localnet]
anchor_escrow = "Hfd7V12kj9AENQjLpTozaPW6aT2rhPm3LSyjXZ5AbWH"

...
```

```rust
// lib.rs

...

declare_id!("Hfd7V12kj9AENQjLpTozaPW6aT2rhPm3LSyjXZ5AbWH");

...
```

### Processor (Part 1)

Let's scaffold the processor first. There should be 3 functions corresponding 3 tasks listed above:

```rust=
// Processor (unimplemented)

#[program]
pub mod anchor_escrow {
    use super::*;

    pub fn initialize(
        ctx: Context<Initialize>,
        _vault_account_bump: u8,
        initializer_amount: u64,
        taker_amount: u64,
    ) -> ProgramResult {
        // TODO
        Ok(())
    }

    pub fn cancel(ctx: Context<Cancel>) -> ProgramResult {
        // TODO
        Ok(())
    }

    pub fn exchange(ctx: Context<Exchange>) -> ProgramResult {
        // TODO
        Ok(())
    }
}
```

The `#[program]` keyword is what makes the magic happen. In argument `ctx`, notice that we have to use a type `Initialize` for `Context<T>` generic. `Initialize` can be considered as a wrapper for instructions. This wrapper is enhanced by Anchor via derived macro (`#[derive(account)]`). We will see how it works real quick.

Each function has a corresponding instruction. As a result, there will be 3 instruction wrappers.

### Instructions (Part 1)

From the processor section, we know that each function defined needs a corresponding instruction. So let's define those in instruction section:

```rust=
// Instructions (unimplemented)

#[derive(Accounts)]
pub struct Initialize<'info> {
    // TODO
}

#[derive(Accounts)]
pub struct Exchange<'info> {
    // TODO
}

#[derive(Accounts)]
pub struct Cancel<'info> {
    // TODO
}
```

Depending on the program functions, the instructions should bring in the accounts that are needed for operations.

To see what are accounts needed for initializing escrow account, we have to consider what data stored in escrow account first.

### Program Account

Accounts that are owned and managed by the program are defined in the `#[account]` section.

#### `EscrowAccount`

| Field                               | Type     | Description                                                            |
| ----------------------------------- | -------- | ---------------------------------------------------------------------- |
| `initializer_key`                   | `Pubkey` | To authorize the actions properly                                      |
| `initializer_deposit_token_account` | `Pubkey` | To record the deposit account of initialzer                            |
| `initializer_receive_token_account` | `Pubkey` | To record the receiving account of initializer                         |
| `initializer_amount`                | `u64`    | To record how much token should the initializer transfer to taker      |
| `taker_amount`                      | `u64`    | To record how much token should the initializer receive from the taker |

As a result, we should design an account that stores the minimum information to validate the escrow state and keep the integrity of the program:

```rust=
// Program Account (fully implemented)

#[account]
pub struct EscrowAccount {
    pub initializer_key: Pubkey,
    pub initializer_deposit_token_account: Pubkey,
    pub initializer_receive_token_account: Pubkey,
    pub initializer_amount: u64,
    pub taker_amount: u64,
}
```

### Instructions (Part 2)

According to what we have in `EscrowAccount`, we need the following accounts to initialize it.

#### `Initialize`

| Field                                    | Type                          | Description                                                                                           |
| ---------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------- |
| **initializer**                          | `AccountInfo`                 | Signer of `InitialEscrow` instruction. To be stored in `EscrowAccount`                                |
| **initializer\_deposit\_token\_account** | `Account<TokenAccount>`       | The account of token account for token exchange. To be stored in `EscrowAccount`                      |
| **initializer\_receive\_token\_account** | `Account<TokenAccount>`       | The account of token account for token exchange. To be stored in `EscrowAccount`                      |
| **token\_program**                       | `AccountInfo`                 | The account of `TokenProgram`                                                                         |
| **escrow\_account**                      | `Box<Account<EscrowAccount>>` | The account of `EscrowAccount`                                                                        |
| **vault\_account**                       | `Account<TokenAccount>`       | The account of `Vault`, which is created by Anchor via **constraints**. (Will be explained in part 3) |
| **mint**                                 | `Account<Mint>`               | -                                                                                                     |
| **system\_program**                      | `AccountInfo`                 | -                                                                                                     |
| **rent**                                 | `Sysvar<Rent>`                | -                                                                                                     |

#### `Cancel`

| Field                                    | Type                          | Description                                                                                       |
| ---------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------- |
| **initializer**                          | `AccountInfo`                 | The initializer of `EscrowAccount`                                                                |
| **initializer\_deposit\_token\_account** | `Account<TokenAccount>`       | The address of token account for token exchange                                                   |
| **vault\_account**                       | `Account<TokenAccount>`       | The program derived address                                                                       |
| **vault\_authority**                     | `AccountInfo`                 | The program derived address                                                                       |
| **escrow\_account**                      | `Box<Account<EscrowAccount>>` | The address of `EscrowAccount`. Have to check if the `EscrowAccount` follows certain constraints. |
| **token\_program**                       | `AccountInfo`                 | The address of `TokenProgram`                                                                     |

#### `Exchange`

| Field                                    | Type                          | Description                                                                                       |
| ---------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------- |
| **taker**                                | `AccountInfo`                 | Singer of `Exchange` instruction                                                                  |
| **taker\_deposit\_token\_account**       | `Account<TokenAccount>`       | Token account for token exchange                                                                  |
| **taker\_receive\_token\_account**       | `Account<TokenAccount>`       | Token account for token exchange                                                                  |
| **initializer\_deposit\_token\_account** | `Account<TokenAccount>`       | Token account for token exchange                                                                  |
| **initializer\_receive\_token\_account** | `Account<TokenAccount>`       | Token account for token exchange                                                                  |
| **initializer**                          | `AccountInfo`                 | To be used in **constraints**. (Will explain in part 3)                                           |
| **escrow\_account**                      | `Box<Account<EscrowAccount>>` | The address of `EscrowAccount`. Have to check if the `EscrowAccount` follows certain constraints. |
| **vault\_account**                       | `Account<TokenAccount>`       | The program derived address                                                                       |
| **vault\_authority**                     | `AccountInfo`                 | The program derived address                                                                       |
| **token\_program**                       | `AccountInfo`                 | The address of `TokenProgram`                                                                     |

You can tell this is a very long list of inputs since Solana programs are **stateless**.

```rust=
// Instructions (partially implemented)

use anchor_spl::token::{self, CloseAccount, Mint, SetAuthority, TokenAccount, Transfer};
use spl_token::instruction::AuthorityType;
...

#[derive(Accounts)]
pub struct Initialize<'info> {
    pub initializer: AccountInfo<'info>,
    pub mint: Account<'info, Mint>,
    pub vault_account: Account<'info, TokenAccount>,
    pub initializer_deposit_token_account: Account<'info, TokenAccount>,
    pub initializer_receive_token_account: Account<'info, TokenAccount>,
    pub escrow_account: Box<Account<'info, EscrowAccount>>,
    pub system_program: AccountInfo<'info>,
    pub rent: Sysvar<'info, Rent>,
    pub token_program: AccountInfo<'info>,
}

#[derive(Accounts)]
pub struct Cancel<'info> {
    pub initializer: AccountInfo<'info>,
    pub initializer_deposit_token_account: Account<'info, TokenAccount>,
    pub vault_account: Account<'info, TokenAccount>,
    pub vault_authority: AccountInfo<'info>,
    pub escrow_account: Box<Account<'info, EscrowAccount>>,
    pub token_program: AccountInfo<'info>,
}

#[derive(Accounts)]
pub struct Exchange<'info> {
    pub taker: AccountInfo<'info>,
    pub taker_deposit_token_account: Account<'info, TokenAccount>,
    pub taker_receive_token_account: Account<'info, TokenAccount>,
    pub initializer_deposit_token_account: Account<'info, TokenAccount>,
    pub initializer_receive_token_account: Account<'info, TokenAccount>,
    pub initializer: AccountInfo<'info>,
    pub escrow_account: Box<Account<'info, EscrowAccount>>,
    pub vault_account: Account<'info, TokenAccount>,
    pub vault_authority: AccountInfo<'info>,
    pub token_program: AccountInfo<'info>,
}
```

> Notice the lifetime anotation used in generic

You can see there are 2 different types for account: `AccountInfo` and `Account`. So what is the difference? I suppose it's proper to use `Account` over `AccountInfo` when you want Anchor to deserialize the data for convenience. In that case, you can access the account data via a trivial method call. For example: `ctx.accounts.vault_account.mint`

### Processor (Part 2)

With necessary accounts, we can implement the business logic inside processor without bothering:

```rust=
// Processor (fully implenmented)

#[program]
pub mod anchor_escrow {
    use super::*;

    const ESCROW_PDA_SEED: &[u8] = b"escrow";

    pub fn initialize(
        ctx: Context<Initialize>,
        _vault_account_bump: u8,
        initializer_amount: u64,
        taker_amount: u64,
    ) -> ProgramResult {
        ctx.accounts.escrow_account.initializer_key = *ctx.accounts.initializer.key;
        ctx.accounts
            .escrow_account
            .initializer_deposit_token_account = *ctx
            .accounts
            .initializer_deposit_token_account
            .to_account_info()
            .key;
        ctx.accounts
            .escrow_account
            .initializer_receive_token_account = *ctx
            .accounts
            .initializer_receive_token_account
            .to_account_info()
            .key;
        ctx.accounts.escrow_account.initializer_amount = initializer_amount;
        ctx.accounts.escrow_account.taker_amount = taker_amount;

        let (vault_authority, _vault_authority_bump) =
            Pubkey::find_program_address(&[ESCROW_PDA_SEED], ctx.program_id);
        token::set_authority(
            ctx.accounts.into_set_authority_context(),
            AuthorityType::AccountOwner,
            Some(vault_authority),
        )?;

        token::transfer(
            ctx.accounts.into_transfer_to_pda_context(),
            ctx.accounts.escrow_account.initializer_amount,
        )?;

        Ok(())
    }

    pub fn cancel(ctx: Context<Cancel>) -> ProgramResult {
        let (_vault_authority, vault_authority_bump) =
            Pubkey::find_program_address(&[ESCROW_PDA_SEED], ctx.program_id);
        let authority_seeds = &[&ESCROW_PDA_SEED[..], &[vault_authority_bump]];

        token::transfer(
            ctx.accounts
                .into_transfer_to_initializer_context()
                .with_signer(&[&authority_seeds[..]]),
            ctx.accounts.escrow_account.initializer_amount,
        )?;

        token::close_account(
            ctx.accounts
                .into_close_context()
                .with_signer(&[&authority_seeds[..]]),
        )?;

        Ok(())
    }

    pub fn exchange(ctx: Context<Exchange>) -> ProgramResult {
        let (_vault_authority, vault_authority_bump) =
            Pubkey::find_program_address(&[ESCROW_PDA_SEED], ctx.program_id);
        let authority_seeds = &[&ESCROW_PDA_SEED[..], &[vault_authority_bump]];

        token::transfer(
            ctx.accounts.into_transfer_to_initializer_context(),
            ctx.accounts.escrow_account.taker_amount,
        )?;

        token::transfer(
            ctx.accounts
                .into_transfer_to_taker_context()
                .with_signer(&[&authority_seeds[..]]),
            ctx.accounts.escrow_account.initializer_amount,
        )?;

        token::close_account(
            ctx.accounts
                .into_close_context()
                .with_signer(&[&authority_seeds[..]]),
        )?;

        Ok(())
    }
}
```

Now the business logic is simple, straightforward, and clear to understand.

* In `initialize`, what happens is that the input accounts are assigned to `EscrowAccount` fields one by one. Then, a program derived address, or PDA, is derived to be going to become new authority of `initializer_deposit_token_account`.
* In `cancel`, it just simply reset the authority from PDA back to the initializer.
* In `exchange`, 3 things happen:
  * First, token A gets transfered from `pda_deposit_token_account` to `taker_receive_token_account`.
  * Next, token B gets transfered from `taker_deposit_token_account` to `initializer_receive_token_account`.
  * Finally, the authority of `pda_deposit_token_account` gets set back to the `initializer`.

### Utils

There are some util functions used for wrapping the data to be passed in `tokens::transfer`, `token::close_account` and `token::set_authority`. It might look a bit overwhelmed in the first place. However, the purpose behind these functions are clear and simple:

```rust=
// Utils (fully implemented)

impl<'info> Initialize<'info> {
    fn into_transfer_to_pda_context(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
        let cpi_accounts = Transfer {
            from: self
                .initializer_deposit_token_account
                .to_account_info()
                .clone(),
            to: self.vault_account.to_account_info().clone(),
            authority: self.initializer.clone(),
        };
        CpiContext::new(self.token_program.clone(), cpi_accounts)
    }

    fn into_set_authority_context(&self) -> CpiContext<'_, '_, '_, 'info, SetAuthority<'info>> {
        let cpi_accounts = SetAuthority {
            account_or_mint: self.vault_account.to_account_info().clone(),
            current_authority: self.initializer.clone(),
        };
        CpiContext::new(self.token_program.clone(), cpi_accounts)
    }
}

impl<'info> Cancel<'info> {
    fn into_transfer_to_initializer_context(
        &self,
    ) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
        let cpi_accounts = Transfer {
            from: self.vault_account.to_account_info().clone(),
            to: self
                .initializer_deposit_token_account
                .to_account_info()
                .clone(),
            authority: self.vault_authority.clone(),
        };
        CpiContext::new(self.token_program.clone(), cpi_accounts)
    }

    fn into_close_context(&self) -> CpiContext<'_, '_, '_, 'info, CloseAccount<'info>> {
        let cpi_accounts = CloseAccount {
            account: self.vault_account.to_account_info().clone(),
            destination: self.initializer.clone(),
            authority: self.vault_authority.clone(),
        };
        CpiContext::new(self.token_program.clone(), cpi_accounts)
    }
}

impl<'info> Exchange<'info> {
    fn into_transfer_to_initializer_context(
        &self,
    ) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
        let cpi_accounts = Transfer {
            from: self.taker_deposit_token_account.to_account_info().clone(),
            to: self
                .initializer_receive_token_account
                .to_account_info()
                .clone(),
            authority: self.taker.clone(),
        };
        CpiContext::new(self.token_program.clone(), cpi_accounts)
    }

    fn into_transfer_to_taker_context(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
        let cpi_accounts = Transfer {
            from: self.vault_account.to_account_info().clone(),
            to: self.taker_receive_token_account.to_account_info().clone(),
            authority: self.vault_authority.clone(),
        };
        CpiContext::new(self.token_program.clone(), cpi_accounts)
    }

    fn into_close_context(&self) -> CpiContext<'_, '_, '_, 'info, CloseAccount<'info>> {
        let cpi_accounts = CloseAccount {
            account: self.vault_account.to_account_info().clone(),
            destination: self.initializer.clone(),
            authority: self.vault_authority.clone(),
        };
        CpiContext::new(self.token_program.clone(), cpi_accounts)
    }
}
```

### Instructions (Part 3)

Finally, let's talk about the account constraints. Here comes a very handy funcionality that Anchor provides: Account Constraints.

Constraints are useful for basic checkings such as whether the initializer is the signer of instruction.

> If you are familiar of Solidity, you can map this concept to solidity modifier.

```rust=
// Instructions (fully implementated)

#[derive(Accounts)]
#[instruction(vault_account_bump: u8, initializer_amount: u64)]
pub struct Initialize<'info> {
    #[account(mut, signer)]
    pub initializer: AccountInfo<'info>,
    pub mint: Account<'info, Mint>,
    #[account(
        init,
        seeds = [b"token-seed".as_ref()],
        bump = vault_account_bump,
        payer = initializer,
        token::mint = mint,
        token::authority = initializer,
    )]
    pub vault_account: Account<'info, TokenAccount>,
    #[account(
        mut,
        constraint = initializer_deposit_token_account.amount >= initializer_amount
    )]
    pub initializer_deposit_token_account: Account<'info, TokenAccount>,
    pub initializer_receive_token_account: Account<'info, TokenAccount>,
    #[account(zero)]
    pub escrow_account: Box<Account<'info, EscrowAccount>>,
    pub system_program: AccountInfo<'info>,
    pub rent: Sysvar<'info, Rent>,
    pub token_program: AccountInfo<'info>,
}

#[derive(Accounts)]
pub struct Cancel<'info> {
    #[account(mut, signer)]
    pub initializer: AccountInfo<'info>,
    #[account(mut)]
    pub vault_account: Account<'info, TokenAccount>,
    pub vault_authority: AccountInfo<'info>,
    #[account(mut)]
    pub initializer_deposit_token_account: Account<'info, TokenAccount>,
    #[account(
        mut,
        constraint = escrow_account.initializer_key == *initializer.key,
        constraint = escrow_account.initializer_deposit_token_account == *initializer_deposit_token_account.to_account_info().key,
        close = initializer
    )]
    pub escrow_account: Box<Account<'info, EscrowAccount>>,
    pub token_program: AccountInfo<'info>,
}

#[derive(Accounts)]
pub struct Exchange<'info> {
    #[account(signer)]
    pub taker: AccountInfo<'info>,
    #[account(mut)]
    pub taker_deposit_token_account: Account<'info, TokenAccount>,
    #[account(mut)]
    pub taker_receive_token_account: Account<'info, TokenAccount>,
    #[account(mut)]
    pub initializer_deposit_token_account: Account<'info, TokenAccount>,
    #[account(mut)]
    pub initializer_receive_token_account: Account<'info, TokenAccount>,
    #[account(mut)]
    pub initializer: AccountInfo<'info>,
    #[account(
        mut,
        constraint = escrow_account.taker_amount <= taker_deposit_token_account.amount,
        constraint = escrow_account.initializer_deposit_token_account == *initializer_deposit_token_account.to_account_info().key,
        constraint = escrow_account.initializer_receive_token_account == *initializer_receive_token_account.to_account_info().key,
        constraint = escrow_account.initializer_key == *initializer.key,
        close = initializer
    )]
    pub escrow_account: Box<Account<'info, EscrowAccount>>,
    #[account(mut)]
    pub vault_account: Account<'info, TokenAccount>,
    pub vault_authority: AccountInfo<'info>,
    pub token_program: AccountInfo<'info>,
}
```

Here, we can see a few new attributes, such as:

| Attribute                                | Description                                                                                                                       |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `#[account(signer)]`                     | Checks the given account signed the transaction                                                                                   |
| `#[account(mut)]`                        | Marks the account as mutable and persists the state transition                                                                    |
| `#[account(constraint = <expression\>)]` | Executes the given code as a constraint. The expression should evaluate to a boolean                                              |
| `#[account(close = <target\>)]`          | Marks the account as being closed at the end of the instruction’s execution, sending the rent exemption lamports to the specified |

Notice that we used a rather complex constraint to create an token account that has a PDA key (See this [code snippet](https://github.com/project-serum/anchor/blob/master/tests/misc/programs/misc/src/context.rs#L10) for more details). Let's take a closer look of it:

```rust=
#[derive(Accounts)]
#[instruction(token_bump: u8)]
pub struct TestTokenSeedsInit<'info> {
    #[account(
        init,
        seeds = [b"my-token-seed".as_ref()],
        bump = token_bump,
        payer = authority,
        token::mint = mint,
        token::authority = authority,
    )]
    pub my_pda: Account<'info, TokenAccount>,
    pub mint: Account<'info, Mint>,
    pub authority: AccountInfo<'info>,
    pub system_program: AccountInfo<'info>,
    pub rent: Sysvar<'info, Rent>,
    pub token_program: AccountInfo<'info>,
}

```

Check the [official document](https://docs.rs/anchor-lang/0.18.2/anchor_lang/derive.Accounts.html) for more constraints.

Now, the program should compile again successfully:

```bash
$ anchor build
...
```

## Build and Test

So far we have only accomplished the first part of the workflow. Let's write some client side test for it.

### Interface Description Language (IDL)

First, you can access the IDL via the following path:

```bash
$ cat ./target/idl/anchor_escrow.json
...
```

This will print the full IDL on the terminal:

```json=
// anchor_escrow.json

{
  "version": "0.0.0",
  "name": "anchor_escrow",
  "instructions": [
    {
      "name": "initialize",
      "accounts": [
        {
          "name": "initializer",
          "isMut": true,
          "isSigner": true
        },
        {
          "name": "mint",
          "isMut": false,
          "isSigner": false
        },
        {
          "name": "vaultAccount",
          "isMut": true,
          "isSigner": false
        },
        {
          "name": "initializerDepositTokenAccount",
          "isMut": true,
          "isSigner": false
        },
        {
          "name": "initializerReceiveTokenAccount",
          "isMut": false,
          "isSigner": false
        },
        {
          "name": "escrowAccount",
          "isMut": true,
          "isSigner": false
        },
        {
          "name": "systemProgram",
          "isMut": false,
          "isSigner": false
        },
        {
          "name": "rent",
          "isMut": false,
          "isSigner": false
        },
        {
          "name": "tokenProgram",
          "isMut": false,
          "isSigner": false
        }
      ],
      "args": [
        {
          "name": "vaultAccountBump",
          "type": "u8"
        },
        {
          "name": "initializerAmount",
          "type": "u64"
        },
        {
          "name": "takerAmount",
          "type": "u64"
        }
      ]
    },
    {
      "name": "cancel",
      "accounts": [
        {
          "name": "initializer",
          "isMut": true,
          "isSigner": true
        },
        {
          "name": "vaultAccount",
          "isMut": true,
          "isSigner": false
        },
        {
          "name": "vaultAuthority",
          "isMut": false,
          "isSigner": false
        },
        {
          "name": "initializerDepositTokenAccount",
          "isMut": true,
          "isSigner": false
        },
        {
          "name": "escrowAccount",
          "isMut": true,
          "isSigner": false
        },
        {
          "name": "tokenProgram",
          "isMut": false,
          "isSigner": false
        }
      ],
      "args": []
    },
    {
      "name": "exchange",
      "accounts": [
        {
          "name": "taker",
          "isMut": false,
          "isSigner": true
        },
        {
          "name": "takerDepositTokenAccount",
          "isMut": true,
          "isSigner": false
        },
        {
          "name": "takerReceiveTokenAccount",
          "isMut": true,
          "isSigner": false
        },
        {
          "name": "initializerDepositTokenAccount",
          "isMut": true,
          "isSigner": false
        },
        {
          "name": "initializerReceiveTokenAccount",
          "isMut": true,
          "isSigner": false
        },
        {
          "name": "initializer",
          "isMut": true,
          "isSigner": false
        },
        {
          "name": "escrowAccount",
          "isMut": true,
          "isSigner": false
        },
        {
          "name": "vaultAccount",
          "isMut": true,
          "isSigner": false
        },
        {
          "name": "vaultAuthority",
          "isMut": false,
          "isSigner": false
        },
        {
          "name": "tokenProgram",
          "isMut": false,
          "isSigner": false
        }
      ],
      "args": []
    }
  ],
  "accounts": [
    {
      "name": "EscrowAccount",
      "type": {
        "kind": "struct",
        "fields": [
          {
            "name": "initializerKey",
            "type": "publicKey"
          },
          {
            "name": "initializerDepositTokenAccount",
            "type": "publicKey"
          },
          {
            "name": "initializerReceiveTokenAccount",
            "type": "publicKey"
          },
          {
            "name": "initializerAmount",
            "type": "u64"
          },
          {
            "name": "takerAmount",
            "type": "u64"
          }
        ]
      }
    }
  ]
}
```

As you can see, the IDL basically defines everything needed for a client representation.

> You can think of IDL as **ABI** if you are familiar with Ethereum and Solidity.

Next, lets move to `tests/anchor-escrow.ts` to implement the tests.

### Setup

Before we dive into the actual test cases, let's first setup the boilerlate for the tests:

```bash
$ npm install --save @solana/spl-token
```

```typescript=
// anchor-escrow.ts

import * as anchor from '@project-serum/anchor';
import { Program } from '@project-serum/anchor';
import { AnchorEscrow } from '../target/types/anchor_escrow';
import { PublicKey, SystemProgram, Transaction } from '@solana/web3.js';
import { TOKEN_PROGRAM_ID, Token } from "@solana/spl-token";
import { assert } from "chai";

describe('anchor-escrow', () => {

  // Configure the client to use the local cluster.
  const provider = anchor.Provider.env();
  anchor.setProvider(provider);

  const program = anchor.workspace.AnchorEscrow as Program<AnchorEscrow>;

  let mintA = null;
  let mintB = null;
  let initializerTokenAccountA = null;
  let initializerTokenAccountB = null;
  let takerTokenAccountA = null;
  let takerTokenAccountB = null;
  let vault_account_pda = null;
  let vault_account_bump = null;
  let vault_authority_pda = null;

  const takerAmount = 1000;
  const initializerAmount = 500;

  const escrowAccount = anchor.web3.Keypair.generate();
  const payer = anchor.web3.Keypair.generate();
  const mintAuthority = anchor.web3.Keypair.generate();
  const initializerMainAccount = anchor.web3.Keypair.generate();
  const takerMainAccount = anchor.web3.Keypair.generate();

  it("Initialize program state", async () => {
    // TODO
  });

  it("Initialize escrow", async () => {
    // TODO
  });

  it("Exchange escrow state", async () => {
    // TODO
  });

  it("Initialize escrow and cancel escrow", async () => {
    // TODO
  });
});
```

> Note: `target/types/anchor_escrow` is generated by running `anchor build`. Make sure you build the program first.

You can see there are 4 test cases to be completed. However, the first test case `Initialize program state` is used for program state setup such as minting tokens. As a result, there should be only 3 test cases corresponding to 3 functions of the program.

Let's finish the program state initialization:

```typescript=
// anchor-escrow.ts

...

describe('anchor-escrow', () => {
  it("Initialize program state", async () => {
    // Airdropping tokens to a payer.
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(payer.publicKey, 10000000000),
      "confirmed"
    );

    // Fund Main Accounts
    await provider.send(
      (() => {
        const tx = new Transaction();
        tx.add(
          SystemProgram.transfer({
            fromPubkey: payer.publicKey,
            toPubkey: initializerMainAccount.publicKey,
            lamports: 1000000000,
          }),
          SystemProgram.transfer({
            fromPubkey: payer.publicKey,
            toPubkey: takerMainAccount.publicKey,
            lamports: 1000000000,
          })
        );
        return tx;
      })(),
      [payer]
    );

    mintA = await Token.createMint(
      provider.connection,
      payer,
      mintAuthority.publicKey,
      null,
      0,
      TOKEN_PROGRAM_ID
    );

    mintB = await Token.createMint(
      provider.connection,
      payer,
      mintAuthority.publicKey,
      null,
      0,
      TOKEN_PROGRAM_ID
    );

    initializerTokenAccountA = await mintA.createAccount(initializerMainAccount.publicKey);
    takerTokenAccountA = await mintA.createAccount(takerMainAccount.publicKey);

    initializerTokenAccountB = await mintB.createAccount(initializerMainAccount.publicKey);
    takerTokenAccountB = await mintB.createAccount(takerMainAccount.publicKey);

    await mintA.mintTo(
      initializerTokenAccountA,
      mintAuthority.publicKey,
      [mintAuthority],
      initializerAmount
    );

    await mintB.mintTo(
      takerTokenAccountB,
      mintAuthority.publicKey,
      [mintAuthority],
      takerAmount
    );

    let _initializerTokenAccountA = await mintA.getAccountInfo(initializerTokenAccountA);
    let _takerTokenAccountB = await mintB.getAccountInfo(takerTokenAccountB);

    assert.ok(_initializerTokenAccountA.amount.toNumber() == initializerAmount);
    assert.ok(_takerTokenAccountB.amount.toNumber() == takerAmount);
  });
  ...

}
```

We should be able to pass the first test case at this point:

```bash
$ anchor test
...

  anchor-escrow
    ✔ Initialize program state (4814ms)
    ✔ Initialize escrow
    ✔ Exchange escrow state
    ✔ Initialize escrow and cancel escrow


  4 passing (5s)

✨  Done in 10.89s.
```

### Implement Tests for `initialize`, `exchange` and `cancel`

Next, we add the test case for `initialize`:

```typescript=
// anchor-escrow.ts

...

describe('anchor-escrow', () => {
  ...

  it("Initialize escrow", async () => {
    const [_vault_account_pda, _vault_account_bump] = await PublicKey.findProgramAddress(
      [Buffer.from(anchor.utils.bytes.utf8.encode("token-seed"))],
      program.programId
    );
    vault_account_pda = _vault_account_pda;
    vault_account_bump = _vault_account_bump;

    const [_vault_authority_pda, _vault_authority_bump] = await PublicKey.findProgramAddress(
      [Buffer.from(anchor.utils.bytes.utf8.encode("escrow"))],
      program.programId
    );
    vault_authority_pda = _vault_authority_pda;

    await program.rpc.initialize(
      vault_account_bump,
      new anchor.BN(initializerAmount),
      new anchor.BN(takerAmount),
      {
        accounts: {
          initializer: initializerMainAccount.publicKey,
          vaultAccount: vault_account_pda,
          mint: mintA.publicKey,
          initializerDepositTokenAccount: initializerTokenAccountA,
          initializerReceiveTokenAccount: initializerTokenAccountB,
          escrowAccount: escrowAccount.publicKey,
          systemProgram: anchor.web3.SystemProgram.programId,
          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
          tokenProgram: TOKEN_PROGRAM_ID,
        },
        instructions: [
          await program.account.escrowAccount.createInstruction(escrowAccount),
        ],
        signers: [escrowAccount, initializerMainAccount],
      }
    );

    let _vault = await mintA.getAccountInfo(vault_account_pda);

    let _escrowAccount = await program.account.escrowAccount.fetch(
      escrowAccount.publicKey
    );

    // Check that the new owner is the PDA.
    assert.ok(_vault.owner.equals(vault_authority_pda));

    // Check that the values in the escrow account match what we expect.
    assert.ok(_escrowAccount.initializerKey.equals(initializerMainAccount.publicKey));
    assert.ok(_escrowAccount.initializerAmount.toNumber() == initializerAmount);
    assert.ok(_escrowAccount.takerAmount.toNumber() == takerAmount);
    assert.ok(
      _escrowAccount.initializerDepositTokenAccount.equals(initializerTokenAccountA)
    );
    assert.ok(
      _escrowAccount.initializerReceiveTokenAccount.equals(initializerTokenAccountB)
    );
  });
  ...

}
```

We should see 2 implemented test cases passed at this point:

```bash
$ anchor test
...

  anchor-escrow
    ✔ Initialize program state (5035ms)
    ✔ Initialize escrow (499ms)
    ✔ Exchange escrow state
    ✔ Initialize escrow and cancel escrow


  4 passing (6s)

✨  Done in 11.39s.
```

Similarly, let's implement the rest of the tests real quick:

```typescript=
// anchor-escrow.ts

...

describe('anchor-escrow', () => {
  ...

  it("Exchange escrow state", async () => {
    await program.rpc.exchange({
      accounts: {
        taker: takerMainAccount.publicKey,
        takerDepositTokenAccount: takerTokenAccountB,
        takerReceiveTokenAccount: takerTokenAccountA,
        initializerDepositTokenAccount: initializerTokenAccountA,
        initializerReceiveTokenAccount: initializerTokenAccountB,
        initializer: initializerMainAccount.publicKey,
        escrowAccount: escrowAccount.publicKey,
        vaultAccount: vault_account_pda,
        vaultAuthority: vault_authority_pda,
        tokenProgram: TOKEN_PROGRAM_ID,
      },
      signers: [takerMainAccount]
    });

    let _takerTokenAccountA = await mintA.getAccountInfo(takerTokenAccountA);
    let _takerTokenAccountB = await mintB.getAccountInfo(takerTokenAccountB);
    let _initializerTokenAccountA = await mintA.getAccountInfo(initializerTokenAccountA);
    let _initializerTokenAccountB = await mintB.getAccountInfo(initializerTokenAccountB);

    assert.ok(_takerTokenAccountA.amount.toNumber() == initializerAmount);
    assert.ok(_initializerTokenAccountA.amount.toNumber() == 0);
    assert.ok(_initializerTokenAccountB.amount.toNumber() == takerAmount);
    assert.ok(_takerTokenAccountB.amount.toNumber() == 0);
  });

  it("Initialize escrow and cancel escrow", async () => {
    // Put back tokens into initializer token A account.
    await mintA.mintTo(
      initializerTokenAccountA,
      mintAuthority.publicKey,
      [mintAuthority],
      initializerAmount
    );

    await program.rpc.initialize(
      vault_account_bump,
      new anchor.BN(initializerAmount),
      new anchor.BN(takerAmount),
      {
        accounts: {
          initializer: initializerMainAccount.publicKey,
          vaultAccount: vault_account_pda,
          mint: mintA.publicKey,
          initializerDepositTokenAccount: initializerTokenAccountA,
          initializerReceiveTokenAccount: initializerTokenAccountB,
          escrowAccount: escrowAccount.publicKey,
          systemProgram: anchor.web3.SystemProgram.programId,
          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
          tokenProgram: TOKEN_PROGRAM_ID,
        },
        instructions: [
          await program.account.escrowAccount.createInstruction(escrowAccount),
        ],
        signers: [escrowAccount, initializerMainAccount],
      }
    );

    // Cancel the escrow.
    await program.rpc.cancel({
      accounts: {
        initializer: initializerMainAccount.publicKey,
        initializerDepositTokenAccount: initializerTokenAccountA,
        vaultAccount: vault_account_pda,
        vaultAuthority: vault_authority_pda,
        escrowAccount: escrowAccount.publicKey,
        tokenProgram: TOKEN_PROGRAM_ID,
      },
      signers: [initializerMainAccount]
    });

    // Check the final owner should be the provider public key.
    const _initializerTokenAccountA = await mintA.getAccountInfo(initializerTokenAccountA);
    assert.ok(_initializerTokenAccountA.owner.equals(initializerMainAccount.publicKey));

    // Check all the funds are still there.
    assert.ok(_initializerTokenAccountA.amount.toNumber() == initializerAmount);
  });
}
```

We should see all test cases passed at this moment:

```bash
$ anchor test
...

  anchor-escrow
    ✔ Initialize program state (4862ms)
    ✔ Initialize escrow (498ms)
    ✔ Exchange escrow state (503ms)
    ✔ Initialize escrow and cancel escrow (11043ms)


  4 passing (17s)

✨  Done in 22.90s.
```

And that's it!

## References

* <https://github.com/ironaddicteddog/anchor-escrow>
* <https://hackmd.io/@ironaddicteddog/solana-starter-kit>
* <https://www.youtube.com/watch?v=cvW8EwGHw8U>
* <https://github.com/project-serum/anchor/blob/master/CHANGELOG.md>
* <https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/>
* <https://project-serum.github.io/anchor/getting-started/introduction.html>
* <https://docs.rs/anchor-lang/0.18.2/anchor\\_lang/derive.Accounts.html>
* <https://anchor.projectserum.com/>
* <https://blog.soteria.dev/?p=ef42d944f086>


# #3 - A Complete Guide to Mint Solana NFTs with Metaplex

**Author:** [@ironaddicteddog](https://twitter.com/ironaddicteddog)

***\[Updated at 2022.4.27]***

> **See the example repo** [**here**](https://github.com/DappioWonderland/nft-mint-example)

## Overview

* Generate profile pictures (pfp) from trait materials with configurable weights
* Use Metaplex Standard
* Upload pfp and metadata to Arweave, which is a decentralized storage network
* Mint NFT on [`solana-mf`](https://github.com/DappioWonderland/solana), a mainnet-fork developed by Dappio
* Some handy tools
  * `hashlips_art_generator`
  * `arweave-image-uploader`
  * `metaboss`

## Introduction to Metaplex

* What does Metaplex do?
  * Metaplex Standard is adopted by popular wallets such as Phantom
* What is in Metaplex standard?
  * See [here](https://medium.com/metaplex/metaplex-metadata-standard-45af3d04b541) for more details

## Setup

### Structure

```
├── 📂 solmeet-3-sandbox
│
├── 📂 hashlips_art_engine
│   │
│   ├── 📂 layers
│   │
│   └── 📂 build
│       │
│       ├── 📂 images
|       |
|       └── 📄 _metadata.csv
│
├── 📂 arweave-image-uploader
│   │
│   └── 📂 public
│       |
│       ├── 📂 images
│       |
│       ├── 📄 data.csv
│       |
│       └── 📄 arweave-uris.json
│
└── 📂 mint
    │
    ├── 📄 mint.js
    │
    └── 📄 mints.json
```

### This Tutorial Only Works on x86\_64 Chip

> See [here](https://github.com/Automattic/node-canvas/issues/1733) for more discussion and work arounds

This tutorial only works on x86\_64 chip and **does not work on Apple Sillicon (M1 Chip)**. Some C++ libraries (ex: `cairo`) may fail. If you are on M1 chip, I strongly recommend you to use a Linux VPS. Here are some options:

* [DigitalOcean](https://www.digitalocean.com)
* [Linode](https://www.linode.com)
* [Vultr](https://www.vultr.com)

### Install `rust`

* See [this doc](https://hackmd.io/@ironaddicteddog/solana-starter-kit#Install-Rust-and-Solana-Cli) for more details

### Install `solana`

* See [this doc](https://hackmd.io/@ironaddicteddog/solana-starter-kit#Install-Rust-and-Solana-Cli) for more details

### Download `solmeet-3-sandbox`

#### Option 1: Use Google Drive Web

Folder link [here](https://drive.google.com/drive/folders/1tIXmkRq0cKLp6BZ_hP5_JjcdHa-WKpZV?usp=sharing)

#### Option 2: Use `gdown` (For Ubuntu User)

Install `gdown`:

```bash
$ sudo apt update
$ sudo apt install python3-pip
...

$ pip install gdown
...

```

Download `background`, `base`, `clothes`, `faces`, `hats` separately:

```bash
$ mkdir solmeet-3-sandbox
$ cd solmeet-3-sandbox
$ gdown --folder https://drive.google.com/drive/folders/1RLz4J7TTh9cnXKWJlUb6_SC5dSnDYiBL -O background
...

$ gdown --folder https://drive.google.com/drive/folders/1jj4V7GNvFqc2UROZhaEvoaF1t8vP53TF -O base
...

$ gdown --folder https://drive.google.com/drive/folders/1FXuztlvSfIsStFXu4_dInXwV9xz_b-gz -O clothes
...

$ gdown --folder https://drive.google.com/drive/folders/1TM5zK9pHm73oSO1U8hpg6G1An14cyagU -O faces
...

$ gdown --folder https://drive.google.com/drive/folders/1GKYw77k0gQRX-AbtTtNChzpGsCBNL1bJ -O hats
...

```

#### Option 3: Use `scp`

Download the folder to local machine first

```bash
$ scp -r [path of the local folder] user@host:[path to s]
```

### Install `hashlips_art_engine`

* <https://github.com/HashLips/hashlips\\_art\\_engine>

```bash
$ git clone https://github.com/HashLips/hashlips_art_engine.git
...

$ cd hashlips_art_engine
$ yarn
...

```

> Notice: Make sure your `node` version >= v16.13.0. See this [issue](https://github.com/HashLips/hashlips_art_engine/issues/375) for more details.

### Install `arweave-image-uploader`

* <https://github.com/thuglabs/arweave-image-uploader>

```bash
$ git clone https://github.com/thuglabs/arweave-image-uploader.git
...

$ cd arweave-image-uploader
$ yarn
...

```

### Install `metaboss`

* <https://github.com/samuelvanderwaal/metaboss>

```bash
$ sudo apt-get install pkg-config libssl-dev libudev-dev
...

$ cargo install metaboss --locked
...

```

### Install `proxyman`

* <https://proxyman.io/release/osx/Proxyman\\_latest.dmg>

**This should be installed on your local machine.**

> For Linux / Windows developers, you could choose whistle (open source) or postman.

### Setup Arweave Wallet

Follow this [doc](https://docs.arweave.org/info/wallets/arweave-web-extension-wallet) to setup your Arweave wallet and claim free AR token by completing assigned [task](https://faucet.arweave.net/).

**You should have a downloaded key file after the setup.** We will need the keyfile in the rest of the tutorial.

## Part 1: Generate Art Works

### Modify `hashlips_art_engine`

Additionally, we have to make a few small changes in the codebase to export the data with desired format.

Next, **replace the source code of `hashlips_art_engine/src/main.js` with the code from the** [**example**](https://raw.githubusercontent.com/DappioWonderland/nft-mint-example/master/hashlips_art_engine/src/main.js).

Here are the changes we made:

```javascript
// In src/main.js

...

// Line 33
let traits = layerConfigurations[0].layersOrder.map(o => o.name);
let metadataListCsv = [`Name,${traits.join(",")}`];
...

// Line 171
metadataListCsv.push(`${tempMetadata.name.split('#')[1]},${attributesList.map(o => o.value).join(",")}`);
...

// Line 317
const writeMetaDataCsv = (_data) => {
  fs.writeFileSync(`${buildDir}/_metadata.csv`, _data);
};
...

// Line 441
writeMetaDataCsv(metadataListCsv.join('\n'));
...

```

### Config `hashlips_art_engine`

**Replace the source code of `hashlips_art_engine/src/config.js` with the code from the** [**example**](https://raw.githubusercontent.com/DappioWonderland/nft-mint-example/master/hashlips_art_engine/src/config.js).

Here are the changes we made:

```javascript
// In src/config.js

...

// Line 5
const network = NETWORK.sol;
...

// Line 8
const namePrefix = "";
...

// Line 25
const layerConfigurations = [
  {
    growEditionSizeTo: 10,
    layersOrder: [
      { name: "background" },
      { name: "base" },
      { name: "clothes" },
      { name: "faces" },
      { name: "hats" },
    ],
  },
];
...

```

### Build Images

Copy `solmeet-3-sandbox` to `hashlips_art_engine`:

```bash
$ cd hashlips_art_engine
$ rm -rf ./layers
$ cp -r ../solmeet-3-sandbox ./layers
```

Set the rarity for each trait **by adding a weight number in filename**. In this tutorial, we will keep every value the same weight in a certain trait.

After updating the filenames, you should have the following results:

```bash
$ ls layers/background
bg1#1.png  bg2#1.png  bg3#1.png  bg4#1.png  bg5#1.png

$ ls layers/base
base1#1.png  base2#1.png

$ ls layers/clothes
clothes1#1.png  clothes2#1.png  clothes3#1.png  clothes4#1.png  clothes5#1.png

$ ls layers/faces
face1#1.png  face2#1.png  face3#1.png  face4#1.png  face5#1.png

$ ls layers/hats
hat1#1.png  hat2#1.png  hat3#1.png  hat4#1.png  hat5#1.png
```

Finally, build the images:

```bash
$ yarn run build
```

This will export the images to `images` folder and a `_metadata.csv` file, both under `build` folder.

> Note: You can compute the distribution of rarity by this command:
>
> ```bash
> $ yarn run rarity
> ```

## Part 2: Upload to Arweave

### Setup Arweave Wallet

Follow this [doc](https://docs.arweave.org/info/wallets/arweave-web-extension-wallet) to setup your Arweave wallet and claim free AR token by completing assigned task.

**You should have a downloaded key file after the setup.** We will need the keyfile in the rest of the tutorial.

### Modify `arweave-image-uploader`

Install `dotenv`:

```bash
$ yarn add dotenv
```

Copy the whole string from the downloaded key file and paste to new `.env` file:

```bash
$ touch .env
```

```
// In .env

KEY={"kty":"RSA","n":"tS1op66z_hQcHj5rKo_WZPvQp3nUP-auQCHqMr..."}
```

**Replace the source code of `arweave-image-uploader/uploader.js` with the code from the** [**example**](https://raw.githubusercontent.com/DappioWonderland/nft-mint-example/master/arweave-image-uploader/uploader.js).

Here are the changes we made:

```javascript
// In uploader.js

...

// Line 6
import dotenv from "dotenv";
dotenv.config();
...

// Line 21
const getNftName = (name) => `SolMeet-3 ART #${name}`;

const getMetadata = (name, imageUrl, attributes) => ({
  name: getNftName(name),
  symbol: "SMT",
  description:
    "SolMeet #3 Art Work",
  seller_fee_basis_points: 100,
  external_url: "https://solmeet.dev",
  attributes,
  collection: {
    name: "SolMeet",
    family: "Dev",
  },
  properties: {
    files: [
      {
        uri: imageUrl,
        type: "image/png",
      },
    ],
    category: "image",
    maxSupply: 0,
    creators: [
      {
        address: "DaPYbGagq3dFDZ1i2PWpSP27mg1ty7J3XfQQciQPLsUn",
        share: 100,
      },
    ],
  },
  image: imageUrl,
});
...

// Line 93
let key = JSON.parse(process.env.KEY);
...

// Line 123
let metadataUri = [];
let metadataCollectionUri = [];
...

// Line 181
metadataUri.push(metadataUrl);
...

// Line 192

// Collection
const collectionFilePath = folder + "logo.png";
const collectionLogo = fs.readFileSync(collectionFilePath);
const contentType = ["Content-Type", "image/png"];
const { id } = await runUpload(collectionLogo, contentType, true);
const imageUrl = id ? `https://arweave.net/${id}` : undefined;
const collectionName = "SolMeet NFT DAO";
const collectionFamily = "DAO";
const metadata = getCollectionMetadata(
  collectionName,
  collectionFamily,
  imageUrl
);
const metaContentType = ["Content-Type", "application/json"];
const metadataString = JSON.stringify(metadata);
const { id: metadataId } = await runUpload(metadataString, metaContentType);
const metadataUrl = id ? `https://arweave.net/${metadataId}` : undefined;

console.log("metadataUrl", metadataUrl);
const newItem = {
  collection: {
    name: collectionName,
    uri: metadataUrl,
  },
};
metadataCollectionUri.push(metadataUrl);

metadataCollection = { ...metadataCollection, ...newItem };
...

// Line 227
const uris = JSON.stringify(metadataUri);
fs.writeFileSync("./public/arweave-uris.json", uris);
const collectionUris = JSON.stringify(metadataCollectionUri);
fs.writeFileSync("./public/arweave-collection-uris.json", collectionUris);
...

```

**Notice: make sure that the address of `creators`** is the same as the mint transaction sender, which is the Solana cli wallet. You can double check via this command:

```bash
$ solana address
DaPYbGagq3dFDZ1i2PWpSP27mg1ty7J3XfQQciQPLsUn
```

### Upload Images

Copy the images and metadata to `arweave-image-uploader`:

```bash
$ cd arweave-image-uploader
$ rm -rf public/images
$ cp -r ../hashlips_art_engine/build/images public/images
$ cp ../hashlips_art_engine/build/_metadata.csv public/data.csv
```

Upload to Arweave:

```bash
$ yarn run upload
...

```

After uploading, you should see a output file `arweave-uris.json` under `public` folder. This is the uris of all the metadata. We will soon use it for minting in the next step.

Also, you can access your image and metadata by visiting the uri. For example:

* <https://arweave.net/hcekmHUHRlQhTHSh0wc0m7zL\\_EyUahr\\_IjGlBk-4EO8>
* <https://viewblock.io/arweave/tx/hcekmHUHRlQhTHSh0wc0m7zL\\_EyUahr\\_IjGlBk-4EO8>

## Part 3: Mint NFTs

### Config Solana

Here, we use `solana-mf` for deploying and testing:

```bash
$ solana config set --url https://rpc-mainnet-fork.dappio.xyz
...

$ solana config set --ws wss://rpc-mainnet-fork.dappio.xyz/ws
...
```

Don't forget to request for airdrop at the first place:

```bash
$ solana airdrop 1
...

```

### Mint

We will use `metaboss` for interacting with Metaplex. Here we have to do 3 things in order:

* Create Collection
* Mint NFTs
* Set and verify the Collection for NFTs

First, create a folder `mint` an empty file `mint.js`:

```bash
$ mkdir mint
$ cd mint
$ touch mint.js
```

Next, **replace the source code of `mint/mint.js` with the code from the** [**example**](https://raw.githubusercontent.com/DappioWonderland/nft-mint-example/master/mint/mint.js).

Then, run the minting script:

```bash
$ KEYPAIR=~/.config/solana/id.json RECEIVER=MY_ADDRESS AUTHORITY=MY_ADDRESS node mint.js 

collectionMint: 6NyVbJX1TB9HMYs5Wfx7qDuPcj8gX55DgsPh97Fv3M8T
nftMint: 6ofefmhENgvXwPznjJikZiAsbEKbxZb689gtkRcR4gUu
nftMint: HicQBruRwJHr7peuzAvZyhUrmwAAYXppjX1VQNVF15CC
nftMint: 2h7rHru4WRzBJnw3M7dN8TSfoUZPnzqD1Beo53gm6pC9
nftMint: 8vD2rNDNs6VZ7H4cRuRmi7uwZEff2MXxMA1Cbq9KdTm1
nftMint: 747BYudqdx9zdvwhvwvn5j4MDekpsuv9dEL5zNewcfvt
nftMint: C9pVfJRZR1NspdRAyPkow553aynNDpLnFYqBWZD5mWT9
nftMint: EYKc62HiRwKADAYe5fSTXXFfU5B5XCLg8nSsJACMEcGj
nftMint: 2U3pGSVSpVUYcaz3bu4ZMaDpHQjpXLVHnx6FbzzS5TJa
nftMint: EvT3voPJNZTTBTYvQYFmgboj9JxEnJiQ5zxr45tX5MKY
nftMint: 5H87TEVxeAK5kYiDP9UCzF3kCQ8L6rx263gqWBy4B1An
```

The results can be found in `mint/mints.json` as well.

### Display NFTs in Phantom

Config `proxyman` for redirecting the http requests that are sent to testnet. This is a hack for customizing Phantom RPC endpoint.

Open `proxyman` and press `option` + `command` + `r` to set the Map Remote rules:

![](https://hackmd.io/_uploads/HyYO-7kiK.png)

Here, we redirect every requests to `https://rpc-mainnet-fork.dappio.xyz`, which is a `solana-mf` RPC operated by Dappio.

> Note: make sure you are not running any VPN software other than Proxyman in order to make the mapping work.

Change the network to `testnet` in Phantom:

![](https://hackmd.io/_uploads/HkMFGmksK.png)

Now, the NFTs should all be displaying:

![](https://hackmd.io/_uploads/HyGizQJjK.png)

That's it!

## Reference

### General

* <https://github.com/ilmoi/awesome-solana-nfts>
* <https://hackmd.io/@levicook/HJcDneEWF>

### Metaplex

* <https://medium.com/metaplex/metaplex-metadata-standard-45af3d04b541>
* <https://medium.com/coinmonks/structure-of-metaplex-nft-c6ef4834a803>
* <https://github.com/metaplex-foundation/metaplex-program-library/tree/master/token-metadata>

### Arweave

* <https://pencilflip.medium.com/how-to-use-arweave-to-store-and-access-nft-metadata-823552293f62>
* <https://pencilflip.medium.com/how-to-use-arweave-to-store-and-access-nft-metadata-part-2-21cb87f4091e>


# #4 - BUIDL a Swap UI on Solana

**Authors:** [@SaiyanBs](https://twitter.com/SaiyanBs), [@ironaddicteddog](https://twitter.com/ironaddicteddog)

***\[Updated at 2022.3.31]***

> **See the example repo** [**here**](https://github.com/DappioWonderland/swap-ui-example)

> **See the demo DApp** [**here**](https://swap-ui-example.dappio.xyz)

## TL; DR

* Use Next.js + React.js + @solana/web3.js
* Raydium AMM swap
* Jupiter SDK swap

## Introduction

* What is Solana
  * Solana is a fast, low cost, decentralized blockchain with thousands of projects spanning DeFi, NFTs, Web3 and more.
* What is Raydium
  * Raydium is an Automated Market Maker (AMM) and liquidity provider built on the Solana blockchain.
* What is Jupiter
  * Jupiter is the key swap aggregator for Solana, offering the best route discovery between any token pair.

## Overview

### What does web3.js do?

* web3.js library
* Solana tx
* Solana ix

### How to find the program interface?

## Structure

```
├── 📂 pages
│   │
│   ├── 📂 api
│   │
│   ├── 📄 _app.tsx
│   │
│   ├── 📄 index.tsx
│   │
│   ├── 📄 jupiter.tsx
│   │
│   └── 📄 raydium.tsx
│
└── 📂 views
│   │
│   ├── 📂 commons
│   │
│   ├── 📂 jupiter
│   │
│   └── 📂 raydium
│
├── 📂 utils
│
├── 📂 styles
│
├── 📂 chakra
│   │
│   └── 📄 style.js
│
├── 📂 public
│
│── 📄 next.config.js
│
└── ...

```

## Setup

First, let's start a brand new next.js project:

```bash
$ npx create-next-app@latest solmeet-4-swap-ui --typescript
```

Remove `package-lock.json` since we will use `yarn` through this entire tutorial:

```bash
$ rm package-lock.json
```

### Install Dependencies

Next, let's install all the dependencies. This includes:

* Solana wallet adapter
* Solana web3.js
* Solana SPL token
* Serum
* Sass
* Jupiter SDK
* Next.js config plugins
* Chakra (UI lib)
* Lodash

Let's update `package.json` directly:

```json=
{
  "name": "solmeet-4-swap-ui",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@chakra-ui/icons": "^1.1.1",
    "@chakra-ui/react": "^1.7.4",
    "@emotion/react": "^11",
    "@emotion/styled": "^11",
    "@jup-ag/react-hook": "^1.0.0-beta.2",
    "@project-serum/borsh": "^0.2.3",
    "@project-serum/serum": "^0.13.61",
    "@solana/spl-token-registry": "^0.2.1733",
    "@solana/wallet-adapter-base": "^0.9.2",
    "@solana/wallet-adapter-react": "^0.15.2",
    "@solana/wallet-adapter-react-ui": "^0.9.4",
    "@solana/wallet-adapter-wallets": "^0.14.2",
    "@solana/web3.js": "^1.32.0",
    "framer-motion": "^5",
    "lodash-es": "^4.17.21",
    "next": "12.0.8",
    "next-compose-plugins": "^2.2.1",
    "next-transpile-modules": "^9.0.0",
    "react": "17.0.2",
    "react-dom": "17.0.2",
    "sass": "^1.49.0"
  },
  "devDependencies": {
    "@types/lodash-es": "^4.17.5",
    "@types/node": "17.0.10",
    "@types/react": "17.0.38",
    "eslint": "8.7.0",
    "eslint-config-next": "12.0.8",
    "typescript": "4.5.5"
  },
  "resolutions": {
    "@solana/buffer-layout": "^3.0.0"
  }
}
```

Then run the installation:

```
$ yarn
...
```

> Note: make sure the version of `buffer-layout` is locked at `^3.0.0`

### Scaffold

Populates folders and files for later update:

```bash
$ mkdir utils && touch utils/{ids.ts,layouts.ts,liquidity.ts,pools.ts,safe-math.ts,swap.ts,tokenList.ts,tokens.ts,web3.ts}
$ mkdir views && mkdir views/{commons,jupiter,raydium}
$ touch views/commons/{Navigator.tsx,WalletProvider.tsx,SplTokenList.tsx,Notify.tsx} && touch views/jupiter/{FeeInfo.tsx,JupiterForm.tsx,JupiterProvider.tsx} && touch views/raydium/{index.tsx,SlippageSetting.tsx,SwapOperateContainer.tsx,TokenList.tsx,TokenSelect.tsx,TitleRow.tsx}
$ touch styles/{swap.module.sass,color.module.sass,navigator.module.sass,jupiter.module.sass}
$ touch pages/{index.tsx,jupiter.tsx,raydium.tsx}
$ mkdir chakra && touch chakra/style.js
```

### Add Common Components

There are 4 common components:

* `Navigator`
* `Notify`
* `SplTokenList`
* `WalletProvider`

#### `Navigator`

Add the following code in `./views/commons/Navigator.tsx`:

```typescript=
import { FunctionComponent } from "react";
import Link from "next/link";
import {
  WalletModalProvider,
  WalletDisconnectButton,
  WalletMultiButton
} from "@solana/wallet-adapter-react-ui";
import { useWallet } from "@solana/wallet-adapter-react";
import style from "../../styles/navigator.module.sass";

const Navigator: FunctionComponent = () => {
  const wallet = useWallet();
  return (
    <div className={style.sidebar}>
      <div className={style.routesBlock}>
        <Link href="/" passHref>
          <a href="https://ibb.co/yP2vCNL">
            <img
              src="https://i.ibb.co/g9Yq8rs/logo-v4-horizontal-transparent.png"
              alt="logo-v4-horizontal-transparent"
              className={style.dappioLogo}
            />
          </a>
        </Link>
        <Link href="/jupiter">
          <a className={style.route}>Jupiter</a>
        </Link>
        <Link href="/raydium">
          <a className={style.route}>Raydium</a>
        </Link>
      </div>
      <WalletModalProvider>
        {wallet.connected ? <WalletDisconnectButton /> : <WalletMultiButton />}
      </WalletModalProvider>
    </div>
  );
};

export default Navigator;
```

#### `Notify`

Add the [following code](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/views/commons/Notify.tsx) in `./views/commons/Notify.tsx`:

```typescript=
import { FunctionComponent } from "react";
import {
  Alert,
  AlertIcon,
  AlertTitle,
  AlertDescription,
  AlertStatus
} from "@chakra-ui/react";
import style from "../../styles/swap.module.sass";

export interface INotify {
  status: AlertStatus;
  title: string;
  description: string;
  link?: string;
}
interface NotifyProps {
  message: {
    status: AlertStatus;
    title: string;
    description: string;
    link?: string;
  };
}

const Notify: FunctionComponent<NotifyProps> = props => {
  return (
    <Alert status={props.message.status} className={style.notifyContainer}>
      <div className={style.notifyTitleRow}>
        <AlertIcon boxSize="2rem" />
        <AlertTitle className={style.title}>{props.message.title}</AlertTitle>
      </div>
      <AlertDescription className={style.notifyDescription}>
        {props.message.description}
      </AlertDescription>
      {props.message.link ? (
        <a
          href={props.message.link}
          style={{ color: "#fbae21", textDecoration: "underline" }}
        >
          Check Explorer
        </a>
      ) : (
        ""
      )}
    </Alert>
  );
};

export default Notify;
```

#### `SplTokenList`

Add the [following code](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/views/commons/SplTokenList.tsx) in `./views/commons/SplTokenList.tsx`:

```typescript=
import { FunctionComponent } from "react";
import style from "../../styles/swap.module.sass";
import { TOKENS } from "../../utils/tokens";
import { ISplToken } from "../../utils/web3";

interface ISplTokenProps {
  splTokenData: ISplToken[];
}

interface SplTokenDisplayData {
  symbol: string;
  mint: string;
  pubkey: string;
  amount: number;
}

const SplTokenList: FunctionComponent<ISplTokenProps> = (
  props
): JSX.Element => {
  let tokenList: SplTokenDisplayData[] = [];
  if (props.splTokenData.length === 0) {
    return <></>;
  }

  for (const [_, value] of Object.entries(TOKENS)) {
    let spl: ISplToken | undefined = props.splTokenData.find(
      (t: ISplToken) => t.parsedInfo.mint === value.mintAddress
    );
    if (spl) {
      let token = {} as SplTokenDisplayData;
      token["symbol"] = value.symbol;
      token["mint"] = spl?.parsedInfo.mint;
      token["pubkey"] = spl?.pubkey;
      token["amount"] = spl?.amount;
      tokenList.push(token);
    }
  }

  let tokens = tokenList.map((item: SplTokenDisplayData) => {
    return (
      <div key={item.mint} className={style.splTokenItem}>
        <div>
          <span style={{ marginRight: "1rem", fontWeight: "600" }}>
            {item.symbol}
          </span>
          <span>- {item.amount}</span>
        </div>
        <div style={{ opacity: ".25" }}>
          <div>Mint: {item.mint}</div>
          <div>Pubkey: {item.pubkey}</div>
        </div>
      </div>
    );
  });

  return (
    <div className={style.splTokenContainer}>
      <div className={style.splTokenListTitle}>Your Tokens</div>
      {tokens}
    </div>
  );
};

export default SplTokenList;
```

#### `WalletProvider`

Add the [following code](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/views/commons/WalletProvider.tsx) in `./views/commons/WalletProvider.tsx`:

```typescript=
import React, { FunctionComponent, useMemo } from "react";
import {
  ConnectionProvider,
  WalletProvider
} from "@solana/wallet-adapter-react";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import {
  LedgerWalletAdapter,
  PhantomWalletAdapter,
  SlopeWalletAdapter,
  SolflareWalletAdapter,
  SolletExtensionWalletAdapter,
  SolletWalletAdapter,
  TorusWalletAdapter
} from "@solana/wallet-adapter-wallets";
import { clusterApiUrl } from "@solana/web3.js";

// Default styles that can be overridden by your app
require("@solana/wallet-adapter-react-ui/styles.css");

export const Wallet: FunctionComponent = props => {
  // // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
  const network = WalletAdapterNetwork.Mainnet;

  // // You can also provide a custom RPC endpoint.
  const endpoint = "https://rpc-mainnet-fork.dappio.xyz";

  // @solana/wallet-adapter-wallets includes all the adapters but supports tree shaking and lazy loading --
  // Only the wallets you configure here will be compiled into your application, and only the dependencies
  // of wallets that your users connect to will be loaded.
  const wallets = useMemo(
    () => [
      new PhantomWalletAdapter(),
      new SlopeWalletAdapter(),
      new SolflareWalletAdapter(),
      new TorusWalletAdapter(),
      new LedgerWalletAdapter(),
      new SolletWalletAdapter({ network }),
      new SolletExtensionWalletAdapter({ network })
    ],
    [network]
  );

  return (
    <ConnectionProvider endpoint={endpoint}>
      <WalletProvider wallets={wallets} autoConnect>
        {props.children}
      </WalletProvider>
    </ConnectionProvider>
  );
};
```

### Add Pages for `Raydium` and `Jupiter`

Add the following code in `./pages/raydium.tsx`:

```typescript=
import { FunctionComponent } from "react";

const RaydiumPage: FunctionComponent = () => {
  return <div>This is Raydium Page</div>;
};

export default RaydiumPage;
```

Add the following code in `./pages/jupiter.tsx`:

```typescript=
import { FunctionComponent } from "react";

const JupiterPage: FunctionComponent = () => {
  return <div>This is Jupiter Page</div>;
};

export default JupiterPage;
```

### Update Styles

Theere are 3 style sheets to be updated:

* `globals.css`
* `navigator.module.sass`
* `color.module.sass`

#### `globals.css`

Add the following code in `./styles/globals.css`:

```css=
html,
body {
  font-size: 10px;
  background-color: rgb(19, 27, 51);
  color: #eee
}

.wallet-adapter-modal-list-more {
  color: #eee
}
.wallet-adapter-button-trigger {
  background-color: #fbae21 !important;
  color: black !important
}
```

#### `navigator.module.sass`

Add the following code in `./styles/navigator.module.sass`:

```sass=
@import './color.module.sass'

.dappioLogo
  flex: 2
  text-align: center
  width: 12rem
  margin-right: 10rem
  cursor: pointer
.sidebar
  display: flex
  align-items: center
  font-size: 2rem
  height: 7rem
  border-bottom: 1px solid rgb(29, 40, 76)
  background-color: $main_blue
  padding: 0 4rem
  justify-content: space-between
  letter-spacing: .1rem
  font-weight: 500
.routesBlock
  display: flex
  align-items: center
  justify-content: space-around
  color: $white
  font-size: 1.5rem
.route
  margin-right: 5rem
```

#### `color.module.sass`

Add the following code in `./styles/color.module.sass`:

```sass=
$white: #eee
$main_blue: rgb(19, 27, 51)
$swap_card_bgc: #131a35
$coin_select_block_bgc: #000829
$placeholder_grey: #f1f1f2
$swap_btn_border_color: #5ac4be
$token_list_bgc: #1c274f
$slippage_setting_warning_red: #f5222d
```

### Update `app`

Replace `pages/_app.tsx` with following code:

```typescript=
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { Wallet } from "../views/commons/WalletProvider";
import Navigator from "../views/commons/Navigator";

function SwapUI({ Component, pageProps }: AppProps) {
  return (
    <>
      <Wallet>
        <Navigator />
        <Component {...pageProps} />
      </Wallet>
    </>
  );
}

export default SwapUI;
```

Start the dev server. For now you should see jupiter and raydium page with only plain text and one wallet connecting button:

```bash
$ yarn dev
```

## Part 1: Build a Swap on Raydium

### What we need to implement Raydium swap?

1. Token list
2. Slippage setting
3. Price out
4. Amm pools info
5. Interact with on-chain program

### Add Raydium Utils

Let's update each component one by one:

* [ids.ts](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/utils/ids.ts)
* [layouts.ts](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/utils/layouts.ts)
* [liquidity.ts](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/utils/liquidity.ts)
* [pools.ts](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/utils/pools.ts)
* [safe-math.ts](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/utils/safe-math.ts)
* [swap.ts](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/utils/swap.ts)
* [tokenList.ts](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/utils/tokenList.ts)
* [tokens.ts](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/utils/tokens.ts)
* [web3.ts](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/utils/web3.ts)

### Add Components

We will update the following components:

* `SlippageSetting`
* `SwapOperateContainer`
* `TitleRow`
* `TokenList`
* `TokenSelect`
* `index`

#### `TitleRow.tsx`

Add the [following code](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/views/raydium/TitleRow.tsx) in `./views/raydium/TitleRow.tsx`:

```typescript=
import style from "../../styles/swap.module.sass";
import {
  Tooltip,
  Popover,
  PopoverTrigger,
  PopoverContent,
  PopoverBody,
  PopoverArrow
} from "@chakra-ui/react";
import { SettingsIcon, InfoOutlineIcon } from "@chakra-ui/icons";
import { useState, useEffect, FunctionComponent } from "react";
import { TokenData, ITokenInfo } from ".";

interface ITitleProps {
  toggleSlippageSetting: Function;
  fromData: TokenData;
  toData: TokenData;
  updateSwapOutAmount: Function;
}

interface IAddressInfoProps {
  type: string;
}

const TitleRow: FunctionComponent<ITitleProps> = (props): JSX.Element => {
  const [second, setSecond] = useState<number>(0);
  const [percentage, setPercentage] = useState<number>(0);

  useEffect(() => {
    let id = setInterval(() => {
      setSecond(second + 1);
      setPercentage((second * 100) / 60);
      if (second === 60) {
        setSecond(0);
        props.updateSwapOutAmount();
      }
    }, 1000);
    return () => clearInterval(id);
  });

  const AddressInfo: FunctionComponent<IAddressInfoProps> = (
    addressProps
  ): JSX.Element => {
    let fromToData = {} as ITokenInfo;
    if (addressProps.type === "From") {
      fromToData = props.fromData.tokenInfo;
    } else {
      fromToData = props.toData.tokenInfo;
    }

    return (
      <>
        <span className={style.symbol}>{fromToData?.symbol}</span>
        <span className={style.address}>
          <span>{fromToData?.mintAddress.substring(0, 14)}</span>
          <span>{fromToData?.mintAddress ? "..." : ""}</span>
          {fromToData?.mintAddress.substr(-14)}
        </span>
      </>
    );
  };

  return (
    <div className={style.titleContainer}>
      <div className={style.title}>Swap</div>
      <div className={style.iconContainer}>
        <Tooltip
          hasArrow
          label={`Displayed data will auto-refresh after ${
            60 - second
          } seconds. Click this circle to update manually.`}
          color="white"
          bg="brand.100"
          padding="3"
        >
          <svg
            viewBox="0 0 36 36"
            className={`${style.percentageCircle} ${style.icon}`}
          >
            <path
              className={style.circleBg}
              d="M18 2.0845
              a 15.9155 15.9155 0 0 1 0 31.831
              a 15.9155 15.9155 0 0 1 0 -31.831"
            />
            <path
              d="M18 2.0845
              a 15.9155 15.9155 0 0 1 0 31.831
              a 15.9155 15.9155 0 0 1 0 -31.831"
              fill="none"
              stroke="rgb(20, 120, 227)"
              strokeWidth="3"
              // @ts-ignore
              strokeDasharray={[percentage, 100]}
            />
          </svg>
        </Tooltip>
        <Popover trigger="hover">
          <PopoverTrigger>
            <div className={style.icon}>
              <InfoOutlineIcon w={18} h={18} />
            </div>
          </PopoverTrigger>
          <PopoverContent
            color="white"
            bg="brand.100"
            border="none"
            w="auto"
            className={style.popover}
          >
            <PopoverArrow bg="brand.100" className={style.popover} />
            <PopoverBody>
              <div className={style.selectTokenAddressTitle}>
                Program Addresses (DO NOT DEPOSIT)
              </div>
              <div className={style.selectTokenAddress}>
                {props.fromData.tokenInfo?.symbol ? (
                  <AddressInfo type="From" />
                ) : (
                  ""
                )}
              </div>
              <div className={style.selectTokenAddress}>
                {props.toData.tokenInfo?.symbol ? (
                  <AddressInfo type="To" />
                ) : (
                  ""
                )}
              </div>
            </PopoverBody>
          </PopoverContent>
        </Popover>
        <div
          className={style.icon}
          onClick={() => props.toggleSlippageSetting()}
        >
          <SettingsIcon w={18} h={18} />
        </div>
      </div>
    </div>
  );
};

export default TitleRow;
```

#### `TokenList.tsx`

Add the [following code](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/views/raydium/TokenList.tsx) in `./views/raydium/TokenList.tsx`:

```typescript=
import { FunctionComponent, useEffect, useRef, useState } from "react";
import { CloseIcon } from "@chakra-ui/icons";
import SPLTokenRegistrySource from "../../utils/tokenList";
import { TOKENS } from "../../utils/tokens";
import { ITokenInfo } from ".";
import style from "../../styles/swap.module.sass";

interface TokenListProps {
  showTokenList: boolean;
  toggleTokenList: (event?: React.MouseEvent<HTMLDivElement>) => void;
  getTokenInfo: Function;
}

const TokenList: FunctionComponent<TokenListProps> = props => {
  const [initialList, setList] = useState<ITokenInfo[]>([]);
  const [searchedList, setSearchList] = useState<ITokenInfo[]>([]);
  const searchRef = useRef<any>();

  useEffect(() => {
    SPLTokenRegistrySource().then((res: any) => {
      let list: ITokenInfo[] = [];
      res.map((item: any) => {
        let token = {} as ITokenInfo;
        if (
          TOKENS[item.symbol] &&
          !list.find(
            (t: ITokenInfo) => t.mintAddress === TOKENS[item.symbol].mintAddress
          )
        ) {
          token = TOKENS[item.symbol];
          token["logoURI"] = item.logoURI;
          list.push(token);
        }
      });
      setList(() => list);
      props.getTokenInfo(
        list.find((item: ITokenInfo) => item.symbol === "SOL")
      );
    });
  }, []);

  useEffect(() => {
    setSearchList(() => initialList);
  }, [initialList]);

  const setTokenInfo = (item: ITokenInfo) => {
    props.getTokenInfo(item);
    props.toggleTokenList();
  };

  useEffect(() => {
    if (!props.showTokenList) {
      setSearchList(initialList);
      searchRef.current.value = "";
    }
  }, [props.showTokenList]);

  const listItems = (data: ITokenInfo[]) => {
    return data.map((item: ITokenInfo) => {
      return (
        <div
          className={style.tokenRow}
          key={item.mintAddress}
          onClick={() => setTokenInfo(item)}
        >
          <img src={item.logoURI} alt="" className={style.tokenLogo} />
          <div>{item.symbol}</div>
        </div>
      );
    });
  };

  const searchToken = (e: any) => {
    let key = e.target.value.toUpperCase();
    let newList: ITokenInfo[] = [];
    initialList.map((item: ITokenInfo) => {
      if (item.symbol.includes(key)) {
        newList.push(item);
      }
    });
    setSearchList(() => newList);
  };

  let tokeListComponentStyle;
  if (!props.showTokenList) {
    tokeListComponentStyle = {
      display: "none"
    };
  } else {
    tokeListComponentStyle = {
      display: "block"
    };
  }

  return (
    <div className={style.tokeListComponent} style={tokeListComponentStyle}>
      <div className={style.tokeListContainer}>
        <div className={style.header}>
          <div>Select a token</div>
          <div className={style.closeIcon} onClick={props.toggleTokenList}>
            <CloseIcon w={5} h={5} />
          </div>
        </div>
        <div className={style.inputBlock}>
          <input
            type="text"
            placeholder="Search name or mint address"
            ref={searchRef}
            className={style.searchTokenInput}
            onChange={searchToken}
          />
          <div className={style.tokenListTitleRow}>
            <div>Token name</div>
          </div>
        </div>
        <div className={style.list}>{listItems(searchedList)}</div>
        <div className={style.tokenListSetting}>View Token List</div>
      </div>
    </div>
  );
};

export default TokenList;
```

#### `SlippageSetting.tsx`

Add the [following code](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/views/raydium/SlippageSetting.tsx) in `./views/raydium/SlippageSetting.tsx`:

```typescript=
import { useState, useEffect, FunctionComponent } from "react";
import { CloseIcon } from "@chakra-ui/icons";
import style from "../../styles/swap.module.sass";

interface SlippageSettingProps {
  showSlippageSetting: boolean;
  toggleSlippageSetting: Function;
  getSlippageValue: Function;
  slippageValue: number;
}

const SlippageSetting: FunctionComponent<SlippageSettingProps> = props => {
  const rate = [0.1, 0.5, 1];
  const [warningText, setWarningText] = useState("");

  const setSlippageBtn = (item: number) => {
    props.getSlippageValue(item);
  };

  useEffect(() => {
    Options();

    if (props.slippageValue < 0) {
      setWarningText("Please enter a valid slippage percentage");
    } else if (props.slippageValue < 1) {
      setWarningText("Your transaction may fail");
    } else {
      setWarningText("");
    }
  }, [props.slippageValue]);

  const Options = (): JSX.Element => {
    return (
      <>
        {rate.map(item => {
          return (
            <button
              className={`${style.optionBtn} ${
                item === props.slippageValue
                  ? style.selectedSlippageRateBtn
                  : ""
              }`}
              key={item}
              onClick={() => setSlippageBtn(item)}
            >
              {item}%
            </button>
          );
        })}
      </>
    );
  };

  const updateInputRate = (e: React.FormEvent<HTMLInputElement>) => {
    props.getSlippageValue(e.currentTarget.value);
  };

  const close = () => {
    if (props.slippageValue < 0) {
      return;
    }
    props.toggleSlippageSetting();
  };

  if (!props.showSlippageSetting) {
    return null;
  }

  return (
    <div className={style.slippageSettingComponent}>
      <div className={style.slippageSettingContainer}>
        <div className={style.header}>
          <div>Setting</div>
          <div className={style.closeIcon} onClick={close}>
            <CloseIcon w={5} h={5} />
          </div>
        </div>
        <div className={style.settingSelectBlock}>
          <div className={style.title}>Slippage tolerance</div>
          <div className={style.optionsBlock}>
            <Options />
            <button className={`${style.optionBtn} ${style.inputBtn}`}>
              <input
                type="number"
                placeholder="0%"
                className={style.input}
                value={props.slippageValue}
                onChange={updateInputRate}
              />
              %
            </button>
          </div>
          <div className={style.warning}>{warningText}</div>
        </div>
      </div>
    </div>
  );
};

export default SlippageSetting;
```

#### `TokenSelect.tsx`

Add the [following code](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/views/raydium/TokenSelect.tsx) in `./views/raydium/TokenSelect.tsx`:

```typescript=
import { FunctionComponent, useEffect, useState } from "react";
import { ArrowDownIcon } from "@chakra-ui/icons";
import { useWallet } from "@solana/wallet-adapter-react";
import { TokenData } from "./index";
import { ISplToken } from "../../utils/web3";
import style from "../../styles/swap.module.sass";

interface TokenSelectProps {
  type: string;
  toggleTokenList: Function;
  tokenData: TokenData;
  updateAmount: Function;
  wallet: Object;
  splTokenData: ISplToken[];
}

export interface IUpdateAmountData {
  type: string;
  amount: number;
}

interface SelectTokenProps {
  propsData: {
    tokenData: TokenData;
  };
}

const TokenSelect: FunctionComponent<TokenSelectProps> = props => {
  let wallet = useWallet();
  const [tokenBalance, setTokenBalance] = useState<number | null>(null);

  const updateAmount = (e: any) => {
    e.preventDefault();

    const amountData: IUpdateAmountData = {
      amount: e.target.value,
      type: props.type
    };
    props.updateAmount(amountData);
  };

  const selectToken = () => {
    props.toggleTokenList(props.type);
  };

  useEffect(() => {
    const getTokenBalance = () => {
      let data: ISplToken | undefined = props.splTokenData.find(
        (t: ISplToken) =>
          t.parsedInfo.mint === props.tokenData.tokenInfo?.mintAddress
      );

      if (data) {
        //@ts-ignore
        setTokenBalance(data.amount);
      }
    };
    getTokenBalance();
  }, [props.splTokenData]);

  const SelectTokenBtn: FunctionComponent<
    SelectTokenProps
  > = selectTokenProps => {
    if (selectTokenProps.propsData.tokenData.tokenInfo?.symbol) {
      return (
        <>
          <img
            src={selectTokenProps.propsData.tokenData.tokenInfo?.logoURI}
            alt="logo"
            className={style.img}
          />
          <div className={style.coinNameBlock}>
            <span className={style.coinName}>
              {selectTokenProps.propsData.tokenData.tokenInfo?.symbol}
            </span>
            <ArrowDownIcon w={5} h={5} />
          </div>
        </>
      );
    }
    return (
      <>
        <span>Select a token</span>
        <ArrowDownIcon w={5} h={5} />
      </>
    );
  };

  return (
    <div className={style.coinSelect}>
      <div className={style.noteText}>
        <div>
          {props.type === "To" ? `${props.type} (Estimate)` : props.type}
        </div>
        <div>
          {wallet.connected && tokenBalance
            ? `Balance: ${tokenBalance.toFixed(4)}`
            : ""}
        </div>
      </div>
      <div className={style.coinAmountRow}>
        {props.type !== "From" ? (
          <div className={style.input}>
            {props.tokenData.amount ? props.tokenData.amount : "-"}
          </div>
        ) : (
          <input
            type="number"
            className={style.input}
            placeholder="0.00"
            onChange={updateAmount}
            disabled={props.type !== "From"}
          />
        )}

        <div className={style.selectTokenBtn} onClick={selectToken}>
          <SelectTokenBtn propsData={props} />
        </div>
      </div>
    </div>
  );
};

export default TokenSelect;
```

#### `SwapOperateContainer.tsx`

Add the [following code](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/views/raydium/SwapOperateContainer.tsx) in `./views/raydium/SwapOperateContainer.tsx`:

```typescript=
import { FunctionComponent } from "react";
import { ArrowUpDownIcon, QuestionOutlineIcon } from "@chakra-ui/icons";
import { Tooltip } from "@chakra-ui/react";
import { useWallet } from "@solana/wallet-adapter-react";
import {
  WalletModalProvider,
  WalletMultiButton
} from "@solana/wallet-adapter-react-ui";
import { TokenData } from ".";
import TokenSelect from "./TokenSelect";
import { ISplToken } from "../../utils/web3";
import style from "../../styles/swap.module.sass";

interface SwapOperateContainerProps {
  toggleTokenList: Function;
  fromData: TokenData;
  toData: TokenData;
  updateAmount: Function;
  switchFromAndTo: (event?: React.MouseEvent<HTMLDivElement>) => void;
  slippageValue: number;
  sendSwapTransaction: (event?: React.MouseEvent<HTMLButtonElement>) => void;
  splTokenData: ISplToken[];
}

interface SwapDetailProps {
  title: string;
  tooltipContent: string;
  value: string;
}

const SwapOperateContainer: FunctionComponent<
  SwapOperateContainerProps
> = props => {
  let wallet = useWallet();
  const SwapBtn = (swapProps: any) => {
    if (wallet.connected) {
      if (
        !swapProps.props.fromData.tokenInfo?.symbol ||
        !swapProps.props.toData.tokenInfo?.symbol
      ) {
        return (
          <button
            className={`${style.operateBtn} ${style.disabledBtn}`}
            disabled
          >
            Select a token
          </button>
        );
      }
      if (
        swapProps.props.fromData.tokenInfo?.symbol &&
        swapProps.props.toData.tokenInfo?.symbol
      ) {
        if (
          !swapProps.props.fromData.amount ||
          !swapProps.props.toData.amount
        ) {
          return (
            <button
              className={`${style.operateBtn} ${style.disabledBtn}`}
              disabled
            >
              Enter an amount
            </button>
          );
        }
      }

      return (
        <button
          className={style.operateBtn}
          onClick={props.sendSwapTransaction}
        >
          Swap
        </button>
      );
    } else {
      return (
        <div className={style.selectWallet}>
          <WalletModalProvider>
            <WalletMultiButton />
          </WalletModalProvider>
        </div>
      );
    }
  };

  const SwapDetailPreview: FunctionComponent<SwapDetailProps> = props => {
    return (
      <div className={style.slippageRow}>
        <div className={style.slippageTooltipBlock}>
          <div>{props.title}</div>
          <Tooltip
            hasArrow
            label={props.tooltipContent}
            color="white"
            bg="brand.100"
            padding="3"
          >
            <QuestionOutlineIcon
              w={5}
              h={5}
              className={`${style.icon} ${style.icon}`}
            />
          </Tooltip>
        </div>
        <div>{props.value}</div>
      </div>
    );
  };

  const SwapDetailPreviewList = (): JSX.Element => {
    return (
      <>
        <SwapDetailPreview
          title="Swapping Through"
          tooltipContent="This venue gave the best price for your trade"
          value={`${props.fromData.tokenInfo.symbol} > ${props.toData.tokenInfo.symbol}`}
        />
      </>
    );
  };

  return (
    <div className={style.swapCard}>
      <div className={style.cardBody}>
        <TokenSelect
          type="From"
          toggleTokenList={props.toggleTokenList}
          tokenData={props.fromData}
          updateAmount={props.updateAmount}
          wallet={wallet}
          splTokenData={props.splTokenData}
        />
        <div
          className={`${style.switchIcon} ${style.icon}`}
          onClick={props.switchFromAndTo}
        >
          <ArrowUpDownIcon w={5} h={5} />
        </div>
        <TokenSelect
          type="To"
          toggleTokenList={props.toggleTokenList}
          tokenData={props.toData}
          updateAmount={props.updateAmount}
          wallet={wallet}
          splTokenData={props.splTokenData}
        />
        <div className={style.slippageRow}>
          <div className={style.slippageTooltipBlock}>
            <div>Slippage Tolerance </div>
            <Tooltip
              hasArrow
              label="The maximum difference between your estimated price and execution price."
              color="white"
              bg="brand.100"
              padding="3"
            >
              <QuestionOutlineIcon
                w={5}
                h={5}
                className={`${style.icon} ${style.icon}`}
              />
            </Tooltip>
          </div>
          <div>{props.slippageValue}%</div>
        </div>
        {props.fromData.amount! > 0 &&
        props.fromData.tokenInfo.symbol &&
        props.toData.amount! > 0 &&
        props.toData.tokenInfo.symbol ? (
          <SwapDetailPreviewList />
        ) : (
          ""
        )}
        <SwapBtn props={props} />
      </div>
    </div>
  );
};

export default SwapOperateContainer;
```

#### `index`

Add the [following code](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/views/raydium/index.tsx) in `./views/raydium/index.tsx`:

```typescript=
import { useState, useEffect, FunctionComponent } from "react";
import TokenList from "./TokenList";
import TitleRow from "./TitleRow";
import SlippageSetting from "./SlippageSetting";
import SwapOperateContainer from "./SwapOperateContainer";
import { Connection } from "@solana/web3.js";
import { Spinner } from "@chakra-ui/react";
import { useWallet, WalletContextState } from "@solana/wallet-adapter-react";
import { getPoolByTokenMintAddresses } from "../../utils/pools";
import { swap, getSwapOutAmount, setupPools } from "../../utils/swap";
import { getSPLTokenData } from "../../utils/web3";
import Notify from "../commons/Notify";
import { INotify } from "../commons/Notify";
import SplTokenList from "../commons/SplTokenList";
import { ISplToken } from "../../utils/web3";
import { IUpdateAmountData } from "./TokenSelect";
import style from "../../styles/swap.module.sass";

export interface ITokenInfo {
  symbol: string;
  mintAddress: string;
  logoURI: string;
}
export interface TokenData {
  amount: number | null;
  tokenInfo: ITokenInfo;
}

const SwapPage: FunctionComponent = () => {
  const [showTokenList, setShowTokenList] = useState(false);
  const [showSlippageSetting, setShowSlippageSetting] = useState(false);
  const [selectType, setSelectType] = useState<string>("From");
  const [fromData, setFromData] = useState<TokenData>({} as TokenData);
  const [toData, setToData] = useState<TokenData>({} as TokenData);
  const [slippageValue, setSlippageValue] = useState(1);
  const [splTokenData, setSplTokenData] = useState<ISplToken[]>([]);
  const [liquidityPools, setLiquidityPools] = useState<any>("");
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [notify, setNotify] = useState<INotify>({
    status: "info",
    title: "",
    description: "",
    link: ""
  });
  const [showNotify, toggleNotify] = useState<Boolean>(false);

  let wallet: WalletContextState = useWallet();
  const connection = new Connection("https://rpc-mainnet-fork.dappio.xyz", {
    wsEndpoint: "wss://rpc-mainnet-fork.dappio.xyz/ws",
    commitment: "processed"
  });

  useEffect(() => {
    setIsLoading(true);
    setupPools(connection).then(data => {
      setLiquidityPools(data);
      setIsLoading(false);
    });
    return () => {
      setLiquidityPools("");
    };
  }, []);

  useEffect(() => {
    if (wallet.connected) {
      getSPLTokenData(wallet, connection).then((tokenList: ISplToken[]) => {
        if (tokenList) {
          setSplTokenData(() => tokenList.filter(t => t !== undefined));
        }
      });
    }
  }, [wallet.connected]);

  const updateAmount = (e: IUpdateAmountData) => {
    if (e.type === "From") {
      setFromData((old: TokenData) => ({
        ...old,
        amount: e.amount
      }));

      if (!e.amount) {
        setToData((old: TokenData) => ({
          ...old,
          amount: 0
        }));
      }
    }
  };

  const updateSwapOutAmount = () => {
    if (
      fromData.amount! > 0 &&
      fromData.tokenInfo?.symbol &&
      toData.tokenInfo?.symbol
    ) {
      let poolInfo = getPoolByTokenMintAddresses(
        fromData.tokenInfo.mintAddress,
        toData.tokenInfo.mintAddress
      );
      if (!poolInfo) {
        setNotify((old: INotify) => ({
          ...old,
          status: "error",
          title: "AMM error",
          description: "Current token pair pool not found"
        }));
        toggleNotify(true);
        return;
      }

      let parsedPoolsData = liquidityPools;
      let parsedPoolInfo = parsedPoolsData[poolInfo?.lp.mintAddress];

      // //@ts-ignore
      const { amountOutWithSlippage } = getSwapOutAmount(
        parsedPoolInfo,
        fromData.tokenInfo.mintAddress,
        toData.tokenInfo.mintAddress,
        fromData.amount!.toString(),
        slippageValue
      );

      setToData((old: TokenData) => ({
        ...old,
        amount: parseFloat(amountOutWithSlippage.fixed())
      }));
    }
  };

  useEffect(() => {
    updateSwapOutAmount();
  }, [fromData]);

  useEffect(() => {
    updateSwapOutAmount();
  }, [toData.tokenInfo?.symbol]);

  useEffect(() => {
    updateSwapOutAmount();
  }, [slippageValue]);

  const toggleTokenList = (e: any) => {
    setShowTokenList(() => !showTokenList);
    setSelectType(() => e);
  };

  const toggleSlippageSetting = () => {
    setShowSlippageSetting(() => !showSlippageSetting);
  };

  const getSlippageValue = (e: number) => {
    if (!e) {
      setSlippageValue(() => e);
    } else {
      setSlippageValue(() => e);
    }
  };

  const switchFromAndTo = () => {
    const fromToken = fromData.tokenInfo;
    const toToken = toData.tokenInfo;
    setFromData((old: TokenData) => ({
      ...old,
      tokenInfo: toToken,
      amount: null
    }));

    setToData((old: TokenData) => ({
      ...old,
      tokenInfo: fromToken,
      amount: null
    }));
  };

  const getTokenInfo = (e: any) => {
    if (selectType === "From") {
      if (toData.tokenInfo?.symbol === e?.symbol) {
        setToData((old: TokenData) => ({
          ...old,
          tokenInfo: {
            symbol: "",
            mintAddress: "",
            logoURI: ""
          }
        }));
      }

      setFromData((old: TokenData) => ({
        ...old,
        tokenInfo: e
      }));
    } else {
      if (fromData.tokenInfo?.symbol === e.symbol) {
        setFromData((old: TokenData) => ({
          ...old,
          tokenInfo: {
            symbol: "",
            mintAddress: "",
            logoURI: ""
          }
        }));
      }

      setToData((old: TokenData) => ({
        ...old,
        tokenInfo: e
      }));
    }
  };

  const sendSwapTransaction = async () => {
    let poolInfo = getPoolByTokenMintAddresses(
      fromData.tokenInfo.mintAddress,
      toData.tokenInfo.mintAddress
    );

    let fromTokenAccount: ISplToken | undefined | string = splTokenData.find(
      (token: ISplToken) =>
        token.parsedInfo.mint === fromData.tokenInfo.mintAddress
    );
    if (fromTokenAccount) {
      fromTokenAccount = fromTokenAccount.pubkey;
    } else {
      fromTokenAccount = "";
    }

    let toTokenAccount: ISplToken | undefined | string = splTokenData.find(
      (token: ISplToken) =>
        token.parsedInfo.mint === toData.tokenInfo.mintAddress
    );
    if (toTokenAccount) {
      toTokenAccount = toTokenAccount.pubkey;
    } else {
      toTokenAccount = "";
    }

    let wsol: ISplToken | undefined = splTokenData.find(
      (token: ISplToken) =>
        token.parsedInfo.mint === "So11111111111111111111111111111111111111112"
    );
    let wsolMint: string = "";
    if (wsol) {
      wsolMint = wsol.parsedInfo.mint;
    }

    if (poolInfo === undefined) {
      alert("Pool not exist");
      return;
    }

    swap(
      connection,
      wallet,
      poolInfo,
      fromData.tokenInfo.mintAddress,
      toData.tokenInfo.mintAddress,
      fromTokenAccount,
      toTokenAccount,
      fromData.amount!.toString(),
      toData.amount!.toString(),
      wsolMint
    ).then(async res => {
      toggleNotify(true);
      setNotify((old: INotify) => ({
        ...old,
        status: "success",
        title: "Transaction Send",
        description: "",
        link: `https://explorer.solana.com/address/${res}`
      }));

      let result = await connection.confirmTransaction(res);

      if (!result.value.err) {
        setNotify((old: INotify) => ({
          ...old,
          status: "success",
          title: "Transaction Success"
        }));
      } else {
        setNotify((old: INotify) => ({
          ...old,
          status: "success",
          title: "Fail",
          description: "Transaction fail, please check below link",
          link: `https://explorer.solana.com/address/${res}`
        }));
      }

      getSPLTokenData(wallet, connection).then((tokenList: ISplToken[]) => {
        if (tokenList) {
          setSplTokenData(() =>
            tokenList.filter((t: ISplToken) => t !== undefined)
          );
        }
      });
    });
  };

  useEffect(() => {
    const time = setTimeout(() => {
      toggleNotify(false);
    }, 8000);

    return () => clearTimeout(time);
  }, [notify]);

  useEffect(() => {
    if (wallet.connected) {
      setNotify((old: INotify) => ({
        ...old,
        status: "success",
        title: "Wallet connected",
        description: wallet.publicKey?.toBase58() as string
      }));
    } else {
      let description = wallet.publicKey?.toBase58();
      if (!description) {
        description = "Please try again";
      }
      setNotify((old: INotify) => ({
        ...old,
        status: "error",
        title: "Wallet disconnected",
        description: description as string
      }));
    }

    toggleNotify(true);
  }, [wallet.connected]);

  return (
    <div className={style.swapPage}>
      {isLoading ? (
        <div className={style.loading}>
          Loading raydium amm pool <Spinner />
        </div>
      ) : (
        ""
      )}
      <SplTokenList splTokenData={splTokenData} />
      <SlippageSetting
        showSlippageSetting={showSlippageSetting}
        toggleSlippageSetting={toggleSlippageSetting}
        getSlippageValue={getSlippageValue}
        slippageValue={slippageValue}
      />
      <TokenList
        showTokenList={showTokenList}
        toggleTokenList={toggleTokenList}
        getTokenInfo={getTokenInfo}
      />
      <div className={style.container}>
        {isLoading ? (
          ""
        ) : (
          <>
            <TitleRow
              toggleSlippageSetting={toggleSlippageSetting}
              fromData={fromData}
              toData={toData}
              updateSwapOutAmount={updateSwapOutAmount}
            />
            <SwapOperateContainer
              toggleTokenList={toggleTokenList}
              fromData={fromData}
              toData={toData}
              updateAmount={updateAmount}
              switchFromAndTo={switchFromAndTo}
              slippageValue={slippageValue}
              sendSwapTransaction={sendSwapTransaction}
              splTokenData={splTokenData}
            />
          </>
        )}
      </div>
      {showNotify ? <Notify message={notify} /> : null}
    </div>
  );
};

export default SwapPage;
```

### Update Style

Add code in`./styles/swap.module.sass` from [swap.module.sass](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/styles/swap.module.sass).

Add the following code in `./chakra/style.js`

```javascript=
import {
  extendTheme
} from "@chakra-ui/react"

const theme = extendTheme({
  colors: {
    brand: {
      100: "#1c274f"
    },
  },
})

export default theme
```

### Update Config

Replace `next.config.js` with following code:

```javascript=
/** @type {import('next').NextConfig} */
const withPlugins = require("next-compose-plugins");

/** eslint-disable @typescript-eslint/no-var-requires */
const withTM = require("next-transpile-modules")([
  "@solana/wallet-adapter-base",
  // Uncomment wallets you want to use
  // "@solana/wallet-adapter-bitpie",
  // "@solana/wallet-adapter-coin98",
  // "@solana/wallet-adapter-ledger",
  // "@solana/wallet-adapter-mathwallet",
  "@solana/wallet-adapter-phantom",
  "@solana/wallet-adapter-react",
  "@solana/wallet-adapter-solflare",
  "@solana/wallet-adapter-sollet",
  // "@solana/wallet-adapter-solong",
  // "@solana/wallet-adapter-torus",
  "@solana/wallet-adapter-wallets",
  // "@project-serum/sol-wallet-adapter",
  // "@solana/wallet-adapter-ant-design",
]);

const plugins = [
  [
    withTM,
    {
      webpack5: true,
      reactStrictMode: true,
    },
  ],
];

const nextConfig = {
  swcMinify: false,
  webpack: (config, {
    isServer
  }) => {
    if (!isServer) {
      config.resolve.fallback.fs = false;
    }
    return config;
  },
};

module.exports = withPlugins(plugins, nextConfig);
```

### Update `Raydium` Page

Finally, update `./pages/raydium.tsx`:

```typescript=
import { FunctionComponent } from "react";
import Swap from "../views/raydium/index";
import { ChakraProvider } from "@chakra-ui/react";
import theme from "../chakra/style";

const RaydiumPage: FunctionComponent = () => {
  return (
    <div>
      <ChakraProvider theme={theme}>
        <Swap />
      </ChakraProvider>
    </div>
  );
};

export default RaydiumPage;
```

Restart the dev server:

```
$ yarn dev
```

## Part 2: Build a Swap on Juipter Aggreggator

### How does Jupiter work?

### Add Components

We will update the following components:

* `FeeInfo`
* `JupiterForm`
* `JupiterProvider`

#### `JupiterProvider.tsx`

Add the [following code](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/views/jupiter/JupiterProvider.tsx) in `./views/jupiter/JupiterProvider.tsx`:

```typescript=
import { FunctionComponent } from "react";
import { JupiterProvider } from "@jup-ag/react-hook";
import { Connection } from "@solana/web3.js";
import { useWallet } from "@solana/wallet-adapter-react";
const connection = new Connection("https://rpc-mainnet-fork.dappio.xyz", {
  wsEndpoint: "wss://rpc-mainnet-fork.dappio.xyz/ws",
  commitment: "processed"
});

const Jupiter: FunctionComponent = ({ children }) => {
  const wallet = useWallet();
  return (
    <JupiterProvider
      connection={connection}
      cluster="mainnet-beta"
      userPublicKey={wallet.publicKey || undefined}
    >
      {children}
    </JupiterProvider>
  );
};

export default Jupiter;
```

#### `FeeInfo.tsx`

Add the [following code](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/views/jupiter/FeeInfo.tsx) in `./views/jupiter/FeeInfo.tsx`:

```typescript=
import React, { FunctionComponent, useEffect, useState } from "react";
import { RouteInfo, TransactionFeeInfo } from "@jup-ag/react-hook";

const FeeInfo: FunctionComponent<{ route: RouteInfo }> = ({
  route
}: {
  route: RouteInfo;
}) => {
  const [state, setState] = useState<TransactionFeeInfo>();
  useEffect(() => {
    setState(undefined);
    route.getDepositAndFee().then(setState);
  }, [route]);
  return (
    <div>
      {state && (
        <div>
          <br />
          Deposit For serum: {/* In lamports */}
          {state.openOrdersDeposits.reduce((total, i) => total + i, 0) /
            10 ** 9}{" "}
          SOL
          <br />
          Deposit For ATA: {/* In lamports */}
          {state.ataDeposit / 10 ** 9} SOL
          <br />
          Fee: {/* In lamports */}
          {state.signatureFee / 10 ** 9} SOL
          <br />
        </div>
      )}
    </div>
  );
};

export default FeeInfo;
```

#### `JupiterForm.tsx`

Add the [following code](https://raw.githubusercontent.com/DappioWonderland/swap-ui-example/master/views/jupiter/JupiterForm.tsx) in `./views/jupiter/JupiterForm.tsx`:

```typescript=
import React, { FunctionComponent, useEffect, useMemo, useState } from "react";
import { PublicKey } from "@solana/web3.js";
import { TokenListProvider, TokenInfo } from "@solana/spl-token-registry";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { useJupiter } from "@jup-ag/react-hook";
import { ENV as ENVChainId } from "@solana/spl-token-registry";
import FeeInfo from "./FeeInfo";
import { getSPLTokenData, ISplToken } from "../../utils/web3";
import SplTokenList from "../commons/SplTokenList";
import style from "../../styles/jupiter.module.sass";

const CHAIN_ID = ENVChainId.MainnetBeta;
interface IJupiterFormProps {}
interface IToken {
  mint: string;
  symbol: string;
}
type UseJupiterProps = Parameters<typeof useJupiter>[0];

const JupiterForm: FunctionComponent<IJupiterFormProps> = props => {
  const wallet = useWallet();
  const { connection } = useConnection();
  const [tokenMap, setTokenMap] = useState<Map<string, TokenInfo>>(new Map());

  const [formValue, setFormValue] = useState<UseJupiterProps>({
    amount: 1,
    inputMint: undefined,
    outputMint: undefined,
    slippage: 1 // 1%
  });

  const [inputTokenInfo, outputTokenInfo] = useMemo(() => {
    return [
      tokenMap.get(formValue.inputMint?.toBase58() || ""),
      tokenMap.get(formValue.outputMint?.toBase58() || "")
    ];
  }, [formValue.inputMint?.toBase58(), formValue.outputMint?.toBase58()]);
  const [splTokenData, setSplTokenData] = useState<ISplToken[]>([]);

  useEffect(() => {
    new TokenListProvider().resolve().then(tokens => {
      const tokenList = tokens.filterByChainId(CHAIN_ID).getList();
      setTokenMap(
        tokenList.reduce((map, item) => {
          map.set(item.address, item);
          return map;
        }, new Map())
      );
    });
  }, [setTokenMap]);

  const amountInDecimal = useMemo(() => {
    return formValue.amount * 10 ** (inputTokenInfo?.decimals || 1);
  }, [inputTokenInfo, formValue.amount]);

  const { routeMap, allTokenMints, routes, loading, exchange, error, refresh } =
    useJupiter({
      ...formValue,
      amount: amountInDecimal
    });

  const validOutputMints = useMemo(() => {
    return routeMap.get(formValue.inputMint?.toBase58() || "") || allTokenMints;
  }, [routeMap, formValue.inputMint?.toBase58()]);

  // ensure outputMint can be swapable to inputMint
  useEffect(() => {
    if (formValue.inputMint) {
      const possibleOutputs = routeMap.get(formValue.inputMint.toBase58());

      if (
        possibleOutputs &&
        !possibleOutputs?.includes(formValue.outputMint?.toBase58() || "")
      ) {
        setFormValue(val => ({
          ...val,
          outputMint: new PublicKey(possibleOutputs[0])
        }));
      }
    }
  }, [formValue.inputMint?.toBase58(), formValue.outputMint?.toBase58()]);

  const getSymbolByMint = (mintList: string[]) => {
    return mintList.map(t => {
      let tokenInfo: IToken = {
        mint: "",
        symbol: ""
      };
      tokenInfo["mint"] = t;
      tokenInfo["symbol"] = tokenMap.get(t)?.name || "unknown";
      return tokenInfo;
    });
  };

  const specificTokenOnly = (tokenList: IToken[]): (IToken | undefined)[] => {
    return tokenList.map((t: IToken) => {
      if (
        t.mint === "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB" ||
        t.mint === "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" ||
        t.mint === "So11111111111111111111111111111111111111112" ||
        t.mint === "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R" ||
        t.mint === "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt"
      ) {
        return t;
      }
    });
  };

  let inputList: IToken[] = specificTokenOnly(
    getSymbolByMint(allTokenMints).sort((a: any, b: any) =>
      a.symbol < b.symbol ? -1 : a.symbol > b.symbol ? 1 : 0
    )
  ).filter(t => t !== undefined) as IToken[];

  let outputList = specificTokenOnly(
    getSymbolByMint(validOutputMints).sort((a: any, b: any) =>
      a.symbol < b.symbol ? -1 : a.symbol > b.symbol ? 1 : 0
    )
  ).filter(t => t !== undefined) as IToken[];

  useEffect(() => {
    if (!wallet.connected) {
      return;
    }
    getSPLTokenData(wallet, connection).then((tokenList: ISplToken[]) => {
      if (tokenList) {
        setSplTokenData(() => tokenList.filter((t: any) => t !== undefined));
      }
    });
    return () => {};
  }, [wallet.connected]);

  return (
    <div style={{ display: "flex" }}>
      <div>
        <SplTokenList splTokenData={splTokenData} />
      </div>
      <div className={style.jupiterFormModal}>
        <div className={style.title}>Jupiter</div>
        <div className={style.selectBlock}>
          <label htmlFor="inputMint">Input token</label>
          <select
            className={style.select}
            id="inputMint"
            name="inputMint"
            value={formValue.inputMint?.toBase58()}
            onChange={e => {
              const pbKey = new PublicKey(e.currentTarget.value);
              if (pbKey) {
                setFormValue(val => ({
                  ...val,
                  inputMint: pbKey
                }));
              }
            }}
          >
            {inputList.map((t: IToken) => {
              return (
                <option key={t.mint} value={t.mint}>
                  {t.symbol}
                </option>
              );
            })}
          </select>
        </div>

        <div className={style.selectBlock}>
          <label htmlFor="outputMint">Output token</label>
          <select
            className={style.select}
            id="outputMint"
            name="outputMint"
            value={formValue.outputMint?.toBase58()}
            onChange={e => {
              const pbKey = new PublicKey(e.currentTarget.value);
              if (pbKey) {
                setFormValue(val => ({
                  ...val,
                  outputMint: pbKey
                }));
              }
            }}
          >
            {outputList.map((t: IToken) => {
              return (
                <option key={t.mint} value={t.mint}>
                  {t.symbol}
                </option>
              );
            })}
          </select>
        </div>

        <div>
          <label htmlFor="amount">
            Input Amount ({inputTokenInfo?.symbol})
          </label>
          <div>
            <input
              className={style.input}
              name="amount"
              id="amount"
              value={formValue.amount}
              type="text"
              pattern="[0-9]*"
              onInput={(e: any) => {
                let newValue = Number(e.target?.value || 0);
                newValue = Number.isNaN(newValue) ? 0 : newValue;
                setFormValue(val => ({
                  ...val,
                  amount: Math.max(newValue, 0)
                }));
              }}
            />
          </div>
        </div>
        <button
          className={style.operateBtn}
          type="button"
          onClick={refresh}
          disabled={loading}
        >
          {loading ? "Loading" : "Refresh rate"}
        </button>

        <div>Total routes: {routes?.length}</div>

        {routes?.[0] &&
          (() => {
            const route = routes[0];
            return (
              <div>
                <div>
                  Best route info :{" "}
                  {route.marketInfos.map(info => info.marketMeta.amm.label)}
                </div>
                <div>
                  Output:{" "}
                  {route.outAmount / 10 ** (outputTokenInfo?.decimals || 1)}{" "}
                  {outputTokenInfo?.symbol}
                </div>
                <FeeInfo route={route} />
              </div>
            );
          })()}

        {error && <div>Error in Jupiter, try changing your input</div>}

        <button
          className={`${style.operateBtn} ${style.swapBtn}`}
          type="button"
          disabled={loading}
          onClick={async () => {
            if (
              !loading &&
              routes?.[0] &&
              wallet.signAllTransactions &&
              wallet.signTransaction &&
              wallet.sendTransaction &&
              wallet.publicKey
            ) {
              await exchange({
                wallet: {
                  sendTransaction: wallet.sendTransaction,
                  publicKey: wallet.publicKey,
                  signAllTransactions: wallet.signAllTransactions,
                  signTransaction: wallet.signTransaction
                },
                route: routes[0],
                confirmationWaiterFactory: async txid => {
                  await connection.confirmTransaction(txid);
                  getSPLTokenData(wallet, connection).then(
                    (tokenList: ISplToken[]) => {
                      if (tokenList) {
                        setSplTokenData(() =>
                          tokenList.filter((t: ISplToken) => t !== undefined)
                        );
                      }
                    }
                  );
                  return await connection.getTransaction(txid, {
                    commitment: "confirmed"
                  });
                }
              });
            }
          }}
        >
          Swap Best Route
        </button>
      </div>
    </div>
  );
};

export default JupiterForm;
```

### Update Style

Add the following code in`./styles/jupiter.module.sass`

```sass=
.jupiterFormModal
  position: absolute
  top: 50%
  left: 50%
  transform: translate(-50%, -50%)
  padding: 4rem 8rem
  border-radius: 1rem
  background-color: rgba(0,0,0,.3)
  .title
    font-size: 2.5rem
    margin-bottom: 3rem
  .selectBlock
    display: flex
    justify-content: space-between
    align-items: center
    margin-bottom: 2rem
  .select
    border: none
    padding: .5rem 2rem
    outline: none
    border-radius: 1rem
    margin-left: 1rem
  .input
    padding: .2rem 1rem
    margin: 1rem 0 1rem 0
    outline: none
  .operateBtn
    padding: .8rem 1.2rem
    margin: 2rem 0
    border: none
    border-radius: 1rem
  .swapBtn
    background-color: #fbae21
    font-weight: 600
    padding: 1.2rem 2rem
```

### Update Config

Replace `next.config.js` with following code:

```typescript=
/** @type {import('next').NextConfig} */
const withPlugins = require("next-compose-plugins");

/** eslint-disable @typescript-eslint/no-var-requires */
const withTM = require("next-transpile-modules")([
  "@solana/wallet-adapter-base",
  // Uncomment wallets you want to use
  // "@solana/wallet-adapter-bitpie",
  // "@solana/wallet-adapter-coin98",
  // "@solana/wallet-adapter-ledger",
  // "@solana/wallet-adapter-mathwallet",
  "@solana/wallet-adapter-phantom",
  "@solana/wallet-adapter-react",
  "@solana/wallet-adapter-solflare",
  "@solana/wallet-adapter-sollet",
  // "@solana/wallet-adapter-solong",
  // "@solana/wallet-adapter-torus",
  "@solana/wallet-adapter-wallets",
  // "@project-serum/sol-wallet-adapter",
  // "@solana/wallet-adapter-ant-design",
]);

const plugins = [
  [
    withTM,
    {
      webpack5: true,
      reactStrictMode: true,
    },
  ],
];

const nextConfig = {
  swcMinify: false,
  webpack: (config, {
    isServer
  }) => {
    if (!isServer) {
      config.resolve.fallback.fs = false;
    }
    return config;
  },
};

module.exports = withPlugins(plugins, nextConfig);
```

### Update `Jupiter` Page

Add the following code in `./pages/jupiter.tsx`:

```typescript=
import { FunctionComponent } from "react";
import Jupiter from "../views/jupiter/JupiterProvider";
import JupiterForm from "../views/jupiter/JupiterForm";

const JupiterPage: FunctionComponent = () => {
  return (
    <>
      <Jupiter>
        <JupiterForm />
      </Jupiter>
    </>
  );
};

export default JupiterPage;
```

Restart the dev server:

```
$ yarn dev
```

## References

* <https://github.com/DappioWonderland/swap-ui-example>
* <https://solana-labs.github.io/solana-web3.js>
* <https://solana-labs.github.io/wallet-adapter>
* <https://docs.jup.ag>
* <https://github.com/raydium-io/raydium-ui>
* <https://github.com/yihau/full-stack-solana-development>
* <https://github.com/yihau/solana-web3-demo>
* <https://github.com/thuglabs/create-dapp-solana-nextjs>
* <https://www.udemy.com/course/typescript-the-complete-developers-guide>


# #5 - BUIDL an Auto-compounding Bot on Saber

**Authors:** [@wei\_sol\_](https://twitter.com/wei_sol_), [@ironaddicteddog](https://twitter.com/ironaddicteddog)

***\[Updated at 2022.3.31]***

> **See the example repo** [**here**](https://github.com/DappioWonderland/auto-compounding-bot)

## TL; DR

* Build SDKs only using @solana/web3.js
* Learn how to interact with Solana
* Builld an auto-compounding bot with SDK

## Introduction

Solana : Solana is a fast, low cost, decentralized blockchain with thousands of projects spanning DeFi, NFTs, Web3 and more.

Saber : Saber is a Curve-like AMM provider on Solana, support all kinds of stablecoin pair from various bridges

### Solana 101

Account : Everything on Solana is an account \* Accounts can only be owned by programs \* Every Account is like a file in a computer \* Accounts are used to store state \* Only the account owner may debit an account and adjust its data \* All accounts to be written to or read must be passed into `Insructions` \* Developers should use the data field to save data inside accounts [![](https://hackmd.io/_uploads/HkuU8iQx9.png)](https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction) >Image from <https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/>

![](https://hackmd.io/_uploads/HyG_Ee_Gq.jpg)

> Image from <https://explorer.solana.com/address/6ZRCB7AAqGre6c72PRz3MHLC73VMYvJ8bi9KHf1HFpNk>

Program : `Program` is just an account with excutable enable \* Solana programs are stateless \* Designed be upgradable (`BPF loader 2`)

![](https://hackmd.io/_uploads/HkZ2Hl_Mc.png)

> Image from <https://explorer.solana.com/address/TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA>

Program Account : An Account owned by a `Program` other than `System program` \* Data struct are very different from program to program

![](https://hackmd.io/_uploads/BytC5xuG9.png)

> Image from <https://explorer.solana.com/address/EzkjmZFzWccf2DRQ6uafahvfZh29ntwAmej8nDQ5tY1C>

Keyair : A byte array that contain `Public key` and `Private key` \* first 32 bytes are use as `Private key` \* last 32 bytes are generated by `Private key` and use as `Public key`

> \[26,151,18,191,115,212,220,144,52,66,74,133,251,235,69,161,254,121,70,227,171,227,17,170,154,227,32,151,40,125,37,158,0,94,253,210,209,242,208,122,162,84,158,36,211,63,243,252,104,36,58,243,120,134,127,132,193,186,63,50,0,230,93,200] 12T1dsupQBqwgQYsXWqhhmzQgreRtvkP95W8rW3Pk23R

Address : A `Public key` encoded in Base58

Rent : Pay for space to store data on Solana \* If `Rent` is paid for over 2 years, the account become `rent-exempt`

![](https://hackmd.io/_uploads/Hk9S9g_Gq.png)

> Image from <https://explorer.solana.com/address/8UviNr47S8eL6J3WfDxMRa3hvLta1VDJwNWqsDgtN3Cv>

RPC : The endpoint to read/write from/to Solana \* Free RPCs can be slow or congested when there is high demand.

PDA (Program Derived Address) : A account without a `Private key` that only can be signed by a `Program` \* PDA is generated by hashing a seed with the Program address

Instruction : The one and only way to interact with `Programs` \* All accounts used when processing should be write into the `Instruction`

![](https://hackmd.io/_uploads/B1j0deuM9.png)

> Raw instruction from <https://explorer.solana.com/tx/v2s26c5vBzrc7rTEyEsCreBDHaVd41iJ8F4j1gLFAy1QA5q8tqJex2Y2h3xsDmJpU1R9wAyRqziyzNB85cgxFjh>

![](https://hackmd.io/_uploads/H1QddedM5.png)

> Serialized instruction <https://explorer.solana.com/tx/37oPunF5tLw6DEqQUk4gMHeb6n1wdCAPWBF7e291znWwPXW7gCvPh9Srv5m5UMoxZoHSMR4BcQau1RnmU5yQeWKj>

Tx (Transaction) : A message send to `RPC` that contains one or more `Instructions`, Signatures and a Fee Payer \* Every Tx have a size limit of 1232 bytes \* A Tx can be signed by differents accounts \* Fee are determined by the amount of Signer at the moment

![](https://hackmd.io/_uploads/SkBRYldz5.png)

> Image from <https://explorer.solana.com/tx/v2s26c5vBzrc7rTEyEsCreBDHaVd41iJ8F4j1gLFAy1QA5q8tqJex2Y2h3xsDmJpU1R9wAyRqziyzNB85cgxFjh>

SPL ([Solana Program Library](https://github.com/solana-labs/solana-program-library)) : A example library organized by Solana lab \* SPL is not a token protocol on Solana, spl-token is.

ATA (Associated Token Account) : A Token Account which is a `PDA` created by [Associated Token Program](https://github.com/solana-labs/solana-program-library/tree/master/associated-token-account/) \* There is only one ATA with every wallet and a token mint

![](https://hackmd.io/_uploads/S1e-3OBgc.png)

> Image from [白上フブキ.eth](https://t.me/fakefubuki)

### Solana system model

![](https://hackmd.io/_uploads/HJ7vkYHl5.png)

## Overview

* Learn Solana basic from building a SDK
* Build a bot that collect, sell, reinvest the yield from LP farming

## Architecture

![](https://hackmd.io/_uploads/HkEhbHHgq.png)

### File Structure

```
├── 📂 raydium
│   │
│   ├── 📄 ids.ts
│   │
│   ├── 📄 index.ts
│   │
│   ├── 📄 infos.ts
│   │
│   ├── 📄 instructions.ts
│   │
│   ├── 📄 layouts.ts
│   │
│   └── 📄 transactions.ts
│
│
├── 📂 saber
│   │
│   ├── 📄 ids.ts
│   │
│   ├── 📄 index.ts
│   │
│   ├── 📄 infos.ts
│   │
│   ├── 📄 instructions.ts
│   │
│   ├── 📄 layouts.ts
│   │
│   └── 📄 transactions.ts
│
│
│── 📄 index.ts
│
│── 📄 utils.ts
│
│── 📄 package.json
│
│── 📄 tsconfig.json
│
└── ...

```

## Setup

### Install Rust and Solana

```bash
$ sh -c "$(curl -sSfL https://release.solana.com/v1.9.8/install)"
...
```

> See <https://hackmd.io/@ironaddicteddog/solana-starter-kit#Install-Rust-and-Solana-Cli> for more details.

### Recover your Wallet (Using Phantom)

![](https://hackmd.io/_uploads/H1IY6ASg5.png)

FIrst, click **Show Secret Reconvery Phrase** and **copy your recovery phrase at this point**.

Next, let's recover the wallet locally:

```bash=
$ solana-keygen recover 'prompt:?key=0/0' -o ~/.config/solana/solmeet-keypair-1.json
```

There should be a prompt asking for entering the recovery phrase in yout terminal. **Paste your recovery phrase at this point**.

* Set keypair

```bash=
$ solana config set --keypair ~/.config/solana/solmeet-keypair-1.json
```

### Config to `solana-mf`

```bash=
$ solana config set --url https://rpc-mainnet-fork.dappio.xyz
$ solana config set --ws wss://rpc-mainnet-fork.dappio.xyz/ws
$ solana config set --commitment processed
$ solana airdrop 1
```

### Scaffold

```bash=
$ mkdir solmeet-5-bot
$ cd solmeet-5-bot
$ tsc --init
```

```bash=
$ touch {index.ts,utils.ts}
$ mkdir saber && touch saber/{index.ts,ids.ts,layouts.ts,infos.ts,instructions.ts,transactions.ts}
$ mkdir raydium && touch raydium/{index.ts,ids.ts,layouts.ts,infos.ts,instructions.ts,transactions.ts}
```

#### Add `package.json`

```json=
{
  "name": "solmeet-5-bot",
  "version": "1.0.0",
  "description": "",
  "main": "./index.ts",
  "scripts": {
    "start": "ts-node ./index.ts"
  },
  "dependencies": {
    "@project-serum/borsh": "^0.2.5",
    "@project-serum/serum": "^0.13.61",
    "@solana/buffer-layout": "^4.0.0",
    "@solana/spl-token": "^0.2.0",
    "@solana/web3.js": "^1.35.0",
    "bignumber.js": "^9.0.1",
    "buffer-layout": "^1.2.2",
    "js-sha256": "^0.9.0"
  },
  "devDependencies": {
    "@project-serum/borsh": "^0.2.5",
    "@solana/web3.js": "^1.35.0",
    "@types/express": "^4.17.13",
    "@types/node": "^17.0.18",
    "buffer-layout": "^1.2.2",
    "ts-node": "^10.5.0",
    "typescript": "^4.5.5"
  }
}
```

#### Install Dependencies

```
$ yarn
```

## Part 1: Implement Common Modules

![](https://hackmd.io/_uploads/H1IXlGLgc.png)

In this part, we will implement the common modules for the bot:

* `ids.ts`
* `layouts.ts`
* `utils.ts`

### `ids.ts`

In Solana, **execution (programs) and states are decoupled**. As a result, we have to be very clear on the scope of the the programs and states:

#### `saber/ids.ts`

```typescript=
import { PublicKey } from "@solana/web3.js";

export const SBR_MINT = new PublicKey("Saber2gLauYim4Mvftnrasomsv6NvAuncvMEZwcLpD1");
export const USDC_UST_POOL = new PublicKey("KwnjUuZhTMTSGAaavkLEmSyfobY16JNH4poL9oeeEvE");
export const ADMIN_KEY = new PublicKey("H9XuKqszWYirDmXDQ12TZXGtxqUYYn4oi7FKzAm7RHGc");
export const SWAP_PROGRAM_ID = new PublicKey("SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ");
export const SABER_WRAP_PROGRAM_ID = new PublicKey("DecZY86MU5Gj7kppfUCEmd4LbXXuyZH1yHaP2NTqdiZB");
export const SABER_QUARRY_REWARDER = new PublicKey("rXhAofQCT7NN9TUqigyEAUzV1uLL4boeD8CRkNBSkYk");
export const QURARRY_MINE_PROGRAM_ID = new PublicKey("QMNeHCGYnLVDn1icRAfQZpjPLBNkfGbSKRB83G5d8KB");
export const SABER_MINT_WRAPPER = new PublicKey("EVVDA3ZiAjTizemLGXNUN3gb6cffQFEYkFjFZokPmUPz");
export const QURARRY_MINT_WRAPPER = new PublicKey("QMWoBmAyJLAsA1Lh9ugMTw2gciTihncciphzdNzdZYV");
export const SABER_FARM_MINTER = new PublicKey("GEoTC3gN12qHDniaDD7Zxvd5xtcZyEKkTPy42B44s82y");
export const IOU_TOKEN_MINT = new PublicKey("iouQcQBAiEXe6cKLS85zmZxUqaCqBdeHFpqKoSz615u");
export const CLAIM_FEE_TOKEN_ACCOUNT = new PublicKey("4Snkea6wv3K6qzDTdyJiF2VTiLPmCoyHJCzAdkdTStBK");
export const SABER_TOKEN_MINT = new PublicKey("Saber2gLauYim4Mvftnrasomsv6NvAuncvMEZwcLpD1");
export const MINTER_PROGRAM_ID = new PublicKey("RDM23yr8pr1kEAmhnFpaabPny6C9UVcEcok3Py5v86X");
export const DEPRECATED_POOLS = [
  new PublicKey("LeekqF2NMKiFNtYD6qXJHZaHx4hUdj4UiPu4t8sz7uK"),
  new PublicKey("2jQoGQRixdcfuRPt9Zui7pk6ivnrQv79mf8h13Tyoa9K"),
  new PublicKey("SPaiZAYyJBQHaSjtxFBKtLtQiCuG328r1mTfmvvydR5"),
  new PublicKey("HoNG9Z4jsA1qtkZhDRYBc67LF2cbusZahjyxXtXdKZgR"),
  new PublicKey("4Fss9Dy3vAUBuQ4SyEZz4vcLxeQqoFLZjdXhEUr3wqz3")
]
```

#### `raydium/ids.ts`

```typescript=
import { PublicKey } from "@solana/web3.js";

export const SBR_AMM_ID = new PublicKey("5cmAS6Mj4pG2Vp9hhyu3kpK9yvC7P6ejh9HiobpTE6Jc")
export const LIQUIDITY_POOL_PROGRAM_ID_V3 = new PublicKey('27haf8L6oxUeXrHrgEgsexjSY5hbVUWEmvv9Nyxg8vQv')
export const LIQUIDITY_POOL_PROGRAM_ID_V4 = new PublicKey('675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8')
export const STAKE_PROGRAM_ID = new PublicKey('EhhTKczWMGQt46ynNeRX1WfeagwwJd7ufHvCDjRxjo5Q')
export const STAKE_PROGRAM_ID_V5 = new PublicKey('9KEPoZmtHUrBbhWN1v1KWLMkkvwY6WLtAVUCPRtRjP4z')
export const AMM_AUTHORITY = new PublicKey("5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1")
```

### `layouts.ts`

`layouts` play an important role in both reading and writing on-chain data.

* **Reading**: It indicates how the stored bytes is arranged and what their types are.
* **Writing**: It indicates how the instruction data should be assembled to call a certain program.

#### `saber/layouts.ts`

```typescript=
import { publicKey, struct, u64, u128, u8, u16, i64, bool } from "@project-serum/borsh";

export const FARM_LAYOUT = struct([
  publicKey("rewarderKey"),
  publicKey("tokenMintKey"),
  u8("bump"),
  u16("index"),
  u8("tokenMintDecimals"),
  i64("famineTs"),
  i64("lastUpdateTs"),
  u128("rewardsPerTokenStored"),
  u64("annualRewardsRate"),
  u64("rewardsShare"),
  u64("totalTokensDeposited"),
  u64("numMiners"),
]);

export const MINER_LAYOUT = struct([
  publicKey("farmKey"),
  publicKey("owner"),
  u8("bump"),
  publicKey("vault"),
  u64("rewardsEarned"),
  u128("rewardsPerTokenPaid"),
  u64("balance"),
  u64("index"),
]);

export const SWAPINFO_LAYOUT = struct([
  bool("isInitialized"),
  bool("isPaused"),
  u8("nonce"),
  u64("initialAmpFactor"),
  u64("targetAmpFactor"),
  i64("startRampTs"),
  i64("stopRampTs"),
  i64("futureAdminDeadline"),
  publicKey("futureAdminKey"),
  publicKey("adminKey"),
  publicKey("tokenAccountA"),
  publicKey("tokenAccountB"),
  publicKey("poolMint"),
  publicKey("mintA"),
  publicKey("mintB"),
  publicKey("adminFeeAccountA"),
  publicKey("adminFeeAccountB"),
]);

export const WRAPINFO_LAYOUT = struct([
  u8("decimal"),
  u64("multiplyer"),
  publicKey("underlyingWrappedTokenMint"),
  publicKey("underlyingTokenAccount"),
  publicKey("wrappedTokenMint"),
]);

export const DEPOSIT_LAYPOUT = struct([
  u8('instruction'),
  u64('AtokenAmount'),
  u64('BtokenAmount'),
  u64('minimalRecieve'),
]);

export const WITHDRAW_LAYOUT = struct([
  u8('instruction'),
  u64('LPtokenAmount'),
  u64('minimalRecieve'),
]);

export const WRAP_LAYOUT = struct([
  u64('amount'),
]);

export const UNWRAP_LAYOUT = struct([
  u64('amount'),
]);

export const DEPOSIT_TO_FARM_LAYOUT = struct([
  u64('amount'),
]);

export const CREATE_MINER_LAYOUT = struct([
  u64('amount'),
]);

export const WITHDRAW_FROM_FARM_LAYOUT = struct([
  u64('amount'),
]);
```

#### `raydium/layouts.ts`

```typescript=
import { publicKey, struct, u8, u64, u128 } from "@project-serum/borsh";

export const SWAP_LAYOUT = struct([
  u8('instruction'),
  u64('amountIn'),
  u64('minAmountOut')
]);

export const ADD_LIQUIDITY_LAYOUT = struct([
  u8('instruction'),
  u64('maxCoinAmount'),
  u64('maxPcAmount'),
  u64('fixedFromCoin')
]);

export const REMOVE_LIQUIDITY_LAYOUT = struct([
  u8('instruction'),
  u64('amount')
]);

export const AMM_INFO_LAYOUT_V4 = struct([
  u64("status"),
  u64("nonce"),
  u64("orderNum"),
  u64("depth"),
  u64("coinDecimals"),
  u64("pcDecimals"),
  u64("state"),
  u64("resetFlag"),
  u64("minSize"),
  u64("volMaxCutRatio"),
  u64("amountWaveRatio"),
  u64("coinLotSize"),
  u64("pcLotSize"),
  u64("minPriceMultiplier"),
  u64("maxPriceMultiplier"),
  u64("systemDecimalsValue"),
  // Fees
  u64("minSeparateNumerator"),
  u64("minSeparateDenominator"),
  u64("tradeFeeNumerator"),
  u64("tradeFeeDenominator"),
  u64("pnlNumerator"),
  u64("pnlDenominator"),
  u64("swapFeeNumerator"),
  u64("swapFeeDenominator"),
  // OutPutData
  u64("needTakePnlCoin"),
  u64("needTakePnlPc"),
  u64("totalPnlPc"),
  u64("totalPnlCoin"),
  u128("poolTotalDepositPc"),
  u128("poolTotalDepositCoin"),
  u128("swapCoinInAmount"),
  u128("swapPcOutAmount"),
  u64("swapCoin2PcFee"),
  u128("swapPcInAmount"),
  u128("swapCoinOutAmount"),
  u64("swapPc2CoinFee"),
  publicKey("poolCoinTokenAccount"),
  publicKey("poolPcTokenAccount"),
  publicKey("coinMintAddress"),
  publicKey("pcMintAddress"),
  publicKey("lpMintAddress"),
  publicKey("ammOpenOrders"),
  publicKey("serumMarket"),
  publicKey("serumProgramId"),
  publicKey("ammTargetOrders"),
  publicKey("poolWithdrawQueue"),
  publicKey("poolTempLpTokenAccount"),
  publicKey("ammOwner"),
  publicKey("pnlOwner"),
]);
```

### `utils.ts`

Copy [this code snippet](https://raw.githubusercontent.com/DappioWonderland/auto-compounding-bot/main/utils.ts) to `utils.ts`.

#### `getAnchorInsByIdl`

Calculate **Anchor Identifier**:

```typescript=
// Example

function getAnchorInsByIdl(name: string): Buffer {
  const SIGHASH_GLOBAL_NAMESPACE = "global";
  const preimage = `${SIGHASH_GLOBAL_NAMESPACE}:${name}`;
  const hash = sha256.sha256.digest(preimage)
  const data = Buffer.from(hash).slice(0, 8)
  return data;
}
```

## Part 2: Implement "Read" Modules

![](https://hackmd.io/_uploads/HyymgM8x5.png)

* **`DataSizeFilter` and `MemCmpFilter`**

```typescript=
// Example

const adminIdMemcmp: MemcmpFilter = {
  memcmp: {
    offset: 8,
    bytes: rewarderKey.toString(),
  }
};

const sizeFilter: DataSizeFilter = {
  dataSize: 140
}
const filters = [adminIdMemcmp, sizeFilter];
const config: GetProgramAccountsConfig = { filters };
const allFarmAccount = await connection.getProgramAccounts(QURARRY_MINE_PROGRAM_ID, config);
```

### `infos.tx`

Copy [this code snippet](https://raw.githubusercontent.com/DappioWonderland/auto-compounding-bot/main/saber/infos.ts) to `saber/infos.ts` and [this code snippet](https://raw.githubusercontent.com/DappioWonderland/auto-compounding-bot/main/raydium/infos.ts) to `raydium/infos.ts`.

### `index.ts`

Copy [this code snippet](https://raw.githubusercontent.com/DappioWonderland/auto-compounding-bot/main/saber/index.ts) to `saber/index.ts` and [this code snippet](https://raw.githubusercontent.com/DappioWonderland/auto-compounding-bot/main/raydium/index.ts) to `raydium/index.ts`.

## Part 3: Implement "Write" Modules

![](https://hackmd.io/_uploads/S1mmxMIg9.png)

* Need to add Anchor identifier manually
* One single Solana tx includes multiple ixs
  * Solana ix = Ethereum tx
  * Solana tx = Ethereum multicall

### `instructions.ts`

Copy [this code snippet](https://raw.githubusercontent.com/DappioWonderland/auto-compounding-bot/main/saber/instructions.ts) to `saber/instructions.ts` and [this code snippet](https://raw.githubusercontent.com/DappioWonderland/auto-compounding-bot/main/raydium/instructions.ts) to `raydium/instructions.ts`.

### `transactions.ts`

Copy [this code snippet](https://raw.githubusercontent.com/DappioWonderland/auto-compounding-bot/main/saber/transactions.ts) to `saber/transactions.ts` and [this code snippet](https://raw.githubusercontent.com/DappioWonderland/auto-compounding-bot/main/raydium/transactions.ts) to `raydium/transactions.ts`.

## Part 4: Implement the Auto-compounding Bot

![](https://hackmd.io/_uploads/rksXxGLgq.png)

* Load Keypair
* Claim All mining rewards
* Swap all SBR to USDC
* Add all USDC swapped out to USDC-UST pool
* Deposit all LP to farming

### `index.ts`

```typescript=
import os from "os";
import fs from "fs";
import BN from "bn.js";
import { Connection, Keypair } from "@solana/web3.js";
import * as raydium from "./raydium";
import { SBR_AMM_ID } from "./raydium/ids";
import * as saber from "./saber";
import { SBR_MINT, USDC_UST_POOL } from "./saber/ids";
import * as utils from "./utils";

// Load keypair
const keyPairPath = `${os.homedir()}/.config/solana/solmeet-keypair-1.json`;
const privateKeyUint8Array = JSON.parse(fs.readFileSync(keyPairPath, "utf-8"));
const privateKey = Uint8Array.from(privateKeyUint8Array);
const wallet = Keypair.fromSecretKey(privateKey);

async function main() {
  const conn = new Connection("https://rpc-mainnet-fork.dappio.xyz", { wsEndpoint: "wss://rpc-mainnet-fork.dappio.xyz/ws", commitment: "processed", });
  // const connection = new Connection("https://solana-api.tt-prod.net", { commitment: "processed", });
  console.log("Fetching all Saber pools...");
  const swaps = await saber.getAllSwaps(conn);
  console.log("Fetching all Saber miners...");
  const miners = await saber.getAllMiners(conn, wallet.publicKey);
  console.log("Fetching Saber AMM pool on Raydium...");
  const sbrAmm = (await raydium.getAmmPool(SBR_AMM_ID, conn));

  // Claim All mining rewards
  console.log("Claiming all mining rewards...")
  for (const miner of miners) {
    for (const swap of swaps) {
      if (miner.farmKey.toString() === swap.farmingInfo?.infoPubkey.toString()) {
        if (miner.balance.toNumber() > 0) {
          // Create claimRewardTx
          const claimRewardTx = await saber.claimRewardTx(swap.farmingInfo as saber.FarmInfo, wallet.publicKey, conn)
          // Send Tx
          const result = await utils.signAndSendAll(claimRewardTx, conn, wallet)
          console.log(miner.getUnclaimedRewards(swap), "SBR reward claimed. Tx:", result);
        }
      }
    }
  }

  let tokenAccounts = await utils.getAllTokenAccount(wallet.publicKey, conn);
  let swapOutAmount = new BN(0);

  // Swap all SBR to USDC
  console.log("Swapping all SBR to USDC...")
  for (const token of tokenAccounts) {
    if (token.mint === SBR_MINT && token.amount.cmpn(0)) {
      swapOutAmount = await (await sbrAmm.calculateSwapOutAmount("coin", token.amount, conn)).divn(0.98);
      if (!swapOutAmount.cmpn(1)) {
        break;
      }
      const swapIx = await raydium.swap(sbrAmm, token.mint, sbrAmm.pcMintAddress, wallet.publicKey, token.amount, new BN(0), conn);
      const result = await utils.signAndSendAll(swapIx, conn, wallet);
      console.log(token.amount.toNumber() / 1000000, "SBR swapped. Tx:", result);
    }
  }

  // Add all USDC swapped out to USDC-UST pool
  console.log("Adding all USDC swapped out to USDC-UST pool...")
  for (const swap of swaps) {
    if (swap.infoPublicKey === USDC_UST_POOL) {
      const addLP = await saber.createDepositTx(swap, new BN(0), swapOutAmount, new BN(0), wallet.publicKey, conn)
      const result = await utils.signAndSendAll(addLP, conn, wallet)
      console.log("LP reinvested. Tx:", result);
    }
  }

  // Deposit all LP to farming
  console.log("Depositing all LP to farming...")
  tokenAccounts = await utils.getAllTokenAccount(wallet.publicKey, conn)
  for (const swap of swaps) {
    for (const token of tokenAccounts) {
      if (token.mint.toString() === swap.poolMint.toString() && token.amount.cmpn(0)) {
        // Create farmIx
        const farmIx = await saber.depositToFarm(swap.farmingInfo as saber.FarmInfo, wallet.publicKey, token.amount, conn)
        // Send Tx
        const result = await utils.signAndSendAll(farmIx, conn, wallet)
        console.log("Farm deposited. Tx:", result);
      }
    }
  }
}

async function run() {
  try {
    main();
  }
  catch (e) {
    console.error(e);
  }
}

run();
```

Finally, let's run the bot:

```
$ yarn start
Fetching all Saber pools...
Fetching all Saber miners...
Fetching Saber AMM pool on Raydium...
Claiming all mining rewards...
0.008659 SBR reward claimed. Tx: 5xCibLpPokio4YHTwVZ35VM8fG8cww8Ris52E1D1qr2F8VSgdJsrHoBJGRDE77p61kXU3UwTMYKtEP3VB2fj1tFM
0 SBR reward claimed. Tx: 2mwEVHKwQwG384JM9ys9iAoKjRnZVswyQRC4hip44nHPP2uJvkA1YUwLfrDn9bk84GxUpPyZjpjzkxZ2ENmfQPb4
Swapping all SBR to USDC...
Adding all USDC swapped out to USDC-UST pool...
Depositing all LP to farming...
✨  Done in 121.22s.
```

## References

* <https://github.com/DappioWonderland/auto-compounding-bot>
* <https://hackmd.io/@ironaddicteddog/solana-starter-kit>
* <https://hackmd.io/@ironaddicteddog/solana-anchor-escrow>


# #6 - A Starter Kit for Running Solana Validator

**Authors:** [@eefylin](https://twitter.com/eefylin), [@emersonliuuu](https://twitter.com/emersonliuuu), [@ironaddicteddog](https://twitter.com/ironaddicteddog)

***\[Updated at 2022.3.31]***

## Overview

### Validator Landscape

![](https://hackmd.io/_uploads/SJe4wKbM9.png)

**See** [**Solana Beach**](https://solanabeach.io/) **for more details**

* 1600+ Validators
* 1400+ RPC Nodes
* 3000+ TPS
* < 1s Block Time
* ...

### Economics

Solana’s crypto-economic system is designed to promote a healthy, long term self-sustaining economy with participant incentives aligned to the security and decentralization of the network. The main participants in this economy are validation-clients who secure solana network. At the early stage, just as many current blockchain economies (e.g. Bitcoin, Ethereum) does, rely on *protocol-based rewards* to support the economy, with the assumption that the revenue generated through *transaction fees* will support the economy in the long term, when the protocol derived rewards expire. see more [here](https://docs.solana.com/economics_overview)

**So, where do the protocol-based rewards come from?** The answer is ***Inflation Rate*** and transaction fees.

Initial Inflation Rate: 8% Dis-inflation Rate: −15% Long-term Inflation Rate: 1.5% ![](https://hackmd.io/_uploads/HJViJCVGc.png)

**How can we avoid being affected by inflation?** Stake your SOL and delegate to validator node.

## [Staking](https://solana.com/staking)

**Benefit**

1. Avoid token dilution acording to inflation of SOL Staking tokens, which will receive their proportional distribution of inflation issuance, should assuage any dilution concerns for staked token holders.
2. Make Solana network more secure As more token holders choose to stake their SOL tokens to different validators across the network, and the total amount of stake on the network increases, it becomes increasingly difficult for even a coordinated and well-funded attacker to amass enough stake to single-handedly alter the outcome of a consensus vote for their own benefit.

**Rewards** people who stake their token earns their share by the formula and the figure below (or you can see [here](https://docs.solana.com/inflation/terminology#staking-yield-) for more detail). You might notice there's a negative relation between staking yield and total SOL staked, which may be a factor that influences stake/unstake behavior.

```
Staking Yield = Inflation Rate × Validator Uptime ×
                (1 − Validator Fee) × (1 / % SOL Staked)
where:
% SOL Staked = Total SOL Staked / Total Current Supply 
```

for example: (Statistics are from [here](https://staking.staked.us/solana-staking))

```
Inflation Rate:    4.3%
Validator Uptime: 99.5%
Validator Fee:    10.0%
% SOL Staked:     77.1%

Staking Yield = 0.043 x 0.99.5 x (1 - 0.1) x (1 / 0.771)
              = 0.0499 (4.99 %)
```

![](https://hackmd.io/_uploads/SkzVBeUz5.png)

**Risk (Slashing)** "Slashing" is any process by which some portion of stake delegated to a validator is destroyed as a punitive measure for malicious actions undertaken by the validator. If you stake your stake to malicious validator, part of your stake portion might be slashed too.

malicious actions include inconsistant voting during lockout time, or nodes who cause some block fail to full finalization. See more slashing rules here\[[1](https://docs.solana.com/proposals/slashing), [2](https://docs.solana.com/proposals/optimistic-confirmation-and-slashing)]

**How to join**

1. [Stake and delegate to validator node](https://docs.solana.com/cli/delegate-stake)
2. [Join or create stake Pools](https://spl.solana.com/stake-pool)
3. Run a validator (talk more about this later)

## Validator

**Responsibility**

1. verified received block
2. sending [vote](https://docs.solana.com/terminology#ledger-vote) transaction (consensus mechanism)

**Minimum Requirement**

* minimum SOL: 0.02685864 SOL(vote account rent)
* hardware CPU 12 cores / 24 threads, RAM 128GB, Disk 1.5 TB (Accounts: 500GB, Ledger: 1TB) ... see more detail [here](https://docs.solana.com/running-validator/validator-reqs)

**Cost**

**1.1 SOL/day at most (vote transaction cost)**

**Rewards**

1. Protocol-based Rewards Issuances from a global, protocol-defined, inflation rate(short term). These rewards are delivered on top of earnings from transaction fees (long term)
2. Transaction Fee a fixed portion (initially 50%) of each transaction fee is destroyed, with the remaining fee going to the current leader processing the transaction.

## Terminology

![](https://hackmd.io/_uploads/ryruR6tM9.png)

* Validator vs RPC RPC node is a validator node who provide full functionality for public to query on-chain data and send transaction and also improved reliability, which means it needs higher hardware requirement than general validator node.
* [vote and stake account](https://docs.solana.com/cluster/stake-delegation-and-rewards#vote-and-stake-accounts) The rewards process is split into two on-chain programs. The Vote program solves the problem of making stakes slashable. The Stake program acts as custodian of the rewards pool and provides for passive delegation. The Stake program is responsible for paying rewards to staker and voter when shown that a staker's delegate has participated in validating the ledger. (Solana programs are stateless, thus we need accounts to store states)
* [identity](https://docs.solana.com/running-validator/validator-start#generate-identity) same as keypair, in blockchain world *address* represent your identity.
* [paper wallet](https://docs.solana.com/wallet-guide/paper-wallet) Solana commands can be run without ever saving a keypair to disk on a machine.
* [key rotation](https://docs.solana.com/running-validator/vote-accounts#key-rotation) Leaders and validators are expected to use ephemeral keys for operation. And also for security concern, key rotation allows validator rotate the vote account authority keys with no effect on the stake accounts that have been delegate to the vote account.

## Before We Start

***Labs are timed and you cannot pause them**. The timer, which starts when you click Start Lab, shows how long Google Cloud resources will be made available to you.*

### Create a Quiklabs Account

*Here are the necessary steps to enroll in the test environment*

* Visit <https://ce.qwiklabs.com>
* Create your own account
* Verify your email by checking your email box
* Login to <https://ce.qwiklabs.com>
* **Fill our** [**meetup form**](https://forms.gle/utvkviswqH8KNVNA8) **with the email address you just used to create your account**

### Requirements

* Access to a standard internet browser (Chrome browser recommended).
* Time to complete the lab.

> If you already have your own personal Google Cloud account or project, do not use it for this lab.

> If you are using a Chrome OS device, open an Incognito window to run this lab.

## Setup

*This hands-on lab lets you do the lab activities yourself in a real cloud environment, not in a simulation or demo environment.*

***It does so by giving you new, temporary credentials that you use to sign in and access Google Cloud for the duration of the lab.***

### How to start your lab and sign in to the Google Cloud Console

1. Click the Start Lab button. If you need to pay for the lab, a pop-up opens for you to select your payment method. On the left is a panel populated with the temporary credentials that you must use for this lab.
2. Copy the username, and then click Open Google Console. The lab spins up resources, and then opens another tab that shows the Sign in page.

* Open the tabs in separate windows, side-by-side.
* If you see the Choose an account page, click Use Another Account. Choose an account

3. In the Sign in page, paste the username that you copied from the left panel. Then copy and paste the password.

> **Important: You must use the credentials from the left panel. Do not use your Google Cloud Training credentials. If you have your own Google Cloud account, do not use it for this lab (avoids incurring charges).**

4. Click through the subsequent pages:

* Accept the terms and conditions.
* Do not add recovery options or two-factor authentication (because this is a temporary account).
* Do not sign up for free trials.
* After a few moments, the Cloud Console opens in this tab.
* Please check with the upper top selected the project assigned to you.

### Infrastructure Deployment

Create a VM (Navigation Menu -> Compute Engine -> VM instances -> Create Instance)

* Use the default name, region, zone.
* N2 series, Custom Machine type, CPU 24 cores, Ram 128 GB
* Change the "Boot Disk", "SSD persistent disk", Size 500GB
* Networking, Network tags, "solana"
* Leave the rest by default
* The above spec is based on [Solana document](https://docs.solana.com/running-validator/validator-reqs#hardware-recommendations)

Create 1 Firewall rule (Navigation Menu -> VPC network -> Firewall -> Create Firewall Rule)

* Name: solana-validator-ports
* Network: default
* Priority: 1000
* Diretion of traffic: Ingress
* Action on match: Allow
* Target tags: solana
* IPv4 ranges: 0.0.0.0/0
* tcp:8899, 8900, 11000
* udp:11000-11020

## Commands to start a Validator

### Connect to your Virtual Machine

* Please visit the VM page (Navigation Menu -> Compute Engine -> VM instances)
* Find the virtual machine you created, and click on the \[SSH] buttom.
* Now let's run some scripts!

### Key and Configuration

Run following command to Solana Command Line Tool:

```
sh -c "$(curl -sSfL https://release.solana.com/v1.9.13/install)"
```

Should get an output from previous command similiar to the following, please run it.

```
export PATH="/home/<studnet-00-ID>/.local/share/solana/install/active_release/bin:$PATH"
```

By default, CLI connect to Mainnet Beta, Let's connect to Devnet

```
solana config set --url http://api.devnet.solana.com
```

Leverage a System Tuner to update configuration automatically. For more detail please check [Solana document](https://docs.solana.com/running-validator/validator-start#system-tuning)

```
sudo $(command -v solana-sys-tuner) --user $(whoami) > sys-tuner.log 2>&1 &
```

Solana CLI support 2 ways to create key pairs, file wallet or paper wallt. Let's use file wallet in this case for convinience. Paper wallet detail please check [here](https://docs.solana.com/wallet-guide/paper-wallet)

```
solana-keygen new -o ~/.config/solana/validator-keypair.json
```

To show the pubkey again by running:

```
solana-keygen pubkey ~/.config/solana/validator-keypair.json
```

Set the solana configuration to use your validator keypair for all following commands:

```
solana config set --keypair ~/.config/solana/validator-keypair.json
```

You can use following command to check config status at any time:

```
solana config get
```

To start your Validator, we need some SOL in the wallet. With Devnet we can simply get a SOL by following:

```
solana airdrop 1
```

Let's check the balance

```
solana balance
```

Before we launch our Validator, we also need another key-pair for Vote Account:

```
solana-keygen new -o ~/.config/solana/vote-account-keypair.json
```

Also a key-pair for Authorized Withdrawer Account:

```
solana-keygen new -o ~/.config/solana/authorized-withdrawer-keypair.json
```

Run this command to create your Vote Account:

```
solana create-vote-account ~/.config/solana/vote-account-keypair.json ~/.config/solana/validator-keypair.json ~/.config/solana/authorized-withdrawer-keypair.json
```

### Create script for managed background running Validator

Create a executable file `validator.sh`

```
touch ~/.config/solana/validator.sh
```

Use your prefered editor to pasta following content to `validator.sh`

```
vim ~/.config/solana/validator.sh
```

```
solana-validator \
--identity ~/.config/solana/validator-keypair.json \
--vote-account ~/.config/solana/vote-account-keypair.json \
--rpc-port 8899 \
--entrypoint entrypoint.devnet.solana.com:8001 \
--limit-ledger-size \
--log ~/.config/solana/solana-validator.log \
--dynamic-port-range 11000-11020 &
```

Let's make it executable:

```
chmod 755 ~/.config/solana/validator.sh
```

Now it's time to create a file for Systemd, which is the Linux program we are going to use"

```
sudo touch /etc/systemd/system/sol.service
```

Paste following content to the file with your preferred editor: ( please change User, Environment, ExecStart with your own environment. )

```
sudo vim /etc/systemd/system/sol.service
```

```
[Unit]
Description=Solana Validator
After=network.target
Wants=solana-sys-tuner.service
StartLimitIntervalSec=0

[Service]
Type=simple
Restart=always
RestartSec=1
User=<student-00-ID>
LimitNOFILE=1000000
LogRateLimitIntervalSec=0
Environment="PATH=/bin:/usr/bin:/home/<student-00-ID>/.local/share/solana/install/active_release/bin"
ExecStart=/home/<student-00-ID>/.config/solana/validator.sh

[Install]
WantedBy=multi-user.target
```

Finally, let us run commands below to start our validator

```
sudo systemctl daemon-reload #let systemd to load our new service
```

Enable the systemd, so when the service stop will bring up agaiin

```
sudo systemctl enable --now sol #enable this service when VM restart
```

Now let's start the service

```
bash ~/.config/solana/validator.sh
```

### Check your validator status and logs

Let's check the log, once you see your validator catch up with other validator, can move to next step.

```
tail -f ~/.config/solana/solana-validator.log
```

Check our Validator from the Solana Devnet

```
solana gossip | grep <PUBKEY>
```

If you see your pubkey and the IP matching your VM external IP, Your Done!

## [Delegation Program](https://solana.foundation/delegation-program)

**Goal** Incentivize new validators to join to secure Solana network

**Get delegation from fundation**

* meet the Testnet Participation Criteria and all of the Baseline Criteria -> receive a “baseline” delegation from the Solana Foundation of 25,000 SOL

[Example: Baseline criteria for Epoch 252](https://solana.foundation/delegation-criteria/#vote-credits)

| BASELINE REQUIREMENT         | RESULT                                  |
| ---------------------------- | --------------------------------------- |
| Vote Credits                 | 227,047 or more                         |
| Maximum Commission           | 10% or under                            |
| Solana Release               | 1.7.14 or greater                       |
| Self Stake                   | 100 or more                             |
| Total Stake                  | 3,000,000 or less                       |
| Infrastructure Concentration | 10% or less                             |
| Infrastructure Concentration | Baseline in 5 of last 10 testnet epochs |

* meets all criteria to receive the baseline delegation and also meets all of the Bonus Criteria -> receive a “bonus” delegation (size is dynamic, this part depends on current participants)

**Get delegation from external**

1. starting a website and explaining why delegators should stake to you
2. starting a stake pool that promotes decentralization
3. joining a stake pool (<https://solana.foundation/stake-pools>) and receiving additional delegations from them.

([source](https://discord.com/channels/428295358100013066/849749936916267029/954445282572111902))

***

## References

* <https://github.com/DappioWonderland/solana>
* <https://docs.solana.com/running-validator/validator-start>
* <https://hackmd.io/@ironaddicteddog/solana-starter-kit>
* <https://github.com/DappioWonderland/solana>


# #7 - A Complete Guide to Create a NFT DAO on Solana

![](https://hackmd.io/_uploads/SycpRADrq.png)

**Authors:** [@emersonliuuu](https://twitter.com/emersonliuuu), [@ironaddicteddog](https://twitter.com/ironaddicteddog)

***\[Updated at 2022.4.28]***

## Overview

### DAO (Decentralized Autonomous Organization)

![](https://hackmd.io/_uploads/Syq6nvvSq.png)

> Image from [internet](https://www.google.com/url?sa=i\&url=https%3A%2F%2Funwire.pro%2F2022%2F01%2F29%2Fdao-explained%2Ffeature%2F\&psig=AOvVaw0VaigJjMLsCsYtvmlP7Zn_\&ust=1651195222406000\&source=images\&cd=vfe\&ved=0CA0QjhxqFwoTCJi359rLtfcCFQAAAAAdAAAAABA7)

Some definition here...

> An organization represented by rules encoded as a computer program that is transparent, controlled by the organization members and not influenced by a central government, in other words they are member-owned communities without centralized leadership. -- [WIKIPIDIA](https://en.wikipedia.org/wiki/Decentralized_autonomous_organization)

#### Featrues

* anyone who meets the requirement can participate decision making process.
* gather people with same goal together without geographic restrictions
* governance treasury (can be crytocurrencies or NFTs) through program

#### How DAOs work

If you own above certain amount of governance token, you are able to create a proposal. But normally people will discuss the topic first in the community for a while instead of creating a proposal directly, this is because people need some time to understand the scope and relative solutions. After a proposal been created, anyone who owns any amount of governance token can join the vote process. Once the voting period ended, user can excecute the program which already define in proposal.

#### Example 1: Curve DAO

![](https://hackmd.io/_uploads/Hytmv_wB5.png) ![](https://hackmd.io/_uploads/SJTmv_DH5.png)

Curve Finance is a DEX concentrate on stalbe coin swaping which provide low slippage rate.

#### Example 2: Mango DAO

![](https://hackmd.io/_uploads/SkBDhjPrc.png)

* Proposal example: <https://realms.today/dao/MNGO/proposal/GR3PFK68LqU4TZjTCTWUDYQsHZ9z4VDqa1ca75HbRCoy>
* Might discuss first: <https://forum.mango.markets/t/grant-for-mango-chinese-community/520>

#### Example 3: Serum

![](https://hackmd.io/_uploads/BJYqpowr9.png)

* <https://nation.io/dao/SERUM>

> learn more about DAO [here](https://consensys.net/blog/blockchain-explained/what-is-a-dao-and-how-do-they-work/)

### NFT DAO

![](https://hackmd.io/_uploads/SkkKIFPS5.png)

* [MonkeDAO](https://monkedao.io/)

NFT as a pass for joining governance process. More NFT you own, more voting prower you have.

### DAO tools

* <https://github.com/solana-labs/solana-program-library/tree/master/governance>
* <https://github.com/solana-labs/governance-ui>
* [Realms.today](https://realms.today/realms?cluster) (support NFT DAO)
* [Squads](https://app.squads.so/)

### Governance structure

![](https://hackmd.io/_uploads/Syii1OFN9.png)

1. when creating a DAO, governance program will create a PDA with your DAO's name in the seed.
2. if there's a program (called programA) going to be governed by DAO, governance program will generate a PDA as update authority of programA.
3. people can deposit their token to DAO and they will have token governanceaccount which allow them to create a proposal or voting.

### Proposal process

![](https://hackmd.io/_uploads/BJNRlOFVq.jpg)

> Image from [spl-governance](https://github.com/solana-labs/solana-program-library/blob/master/governance/README.md)

1. **\[ proposal: - ]&#x20;*****program owner*** create program governance
2. **\[ proposal: draft ]&#x20;*****proposal owner*** create a proposal
   * add/remove signatory
   * Insert/remove transaction
   * cancel proposal
3. **\[ proposal: signing ]&#x20;*****signatory*** agree this proposal or not
4. **\[ proposal: voting ]&#x20;*****voter*** vote "Yes" or "No" to the proposal, every vote will generate a vote record.
5. **\[ proposal: finalizing ]&#x20;*****user*** can finalize the proposal after voting period
   * if *Yes* > *No* **\[ proposal: succeeded ]**
   * else **\[ proposal: defeated ]**
6. **(if succeeded) \[ proposal: exicuting ]&#x20;*****user*** can exicute proposal transaction after hold up period.
7. **\[ proposal: completed ]**

## Part 1: Setup

### Mint NFTs

Before the creation of the DAO, let's mint some dummy NFTs and distribute them to different holders.

We will go through [SolMeet #3 note](https://book.solmeet.dev/notes/complete-guide-to-mint-solana-nft) to create and verify these NFTs and collection. Please refer to the note for more details.

> Please make sure **proxyman** is up and running with correct config to view the NFT on mainnet-fork in your browser wallet. See [here](https://book.solmeet.dev/notes/complete-guide-to-mint-solana-nft#display-nfts-in-phantom) for more details.

### Setup `governance-ui`

We will use a well-structured [front-end UI](https://github.com/solana-labs/governance-ui) maintained by Solana Lab through the entire example.

In the following sections, you will interact with the DAO via the UI that is running locally in your machine, with the correct RPC config

First, clone and build `governance-ui`:

```bash
$ git clone git@github.com:solana-labs/governance-ui.git
...

$ cd governance-ui
$ yarn
```

Before we start, we have to config the RPC to our [mainnet-fork](https://github.com/DappioWonderland/solana). Replace part of `utils/connection.ts` with the following code snippet:

```typescript
// In utils/connection.ts

...

// Line 6
  {
    name: 'mainnet',
    url: process.env.MAINNET_RPC || 'https://rpc-mainnet-fork.epochs.studio',
  },
...

// Line 30
export function getConnectionContext(cluster: string): ConnectionContext {
  const ENDPOINT = ENDPOINTS.find((e) => e.name === cluster) || ENDPOINTS[0]
  const commitment: Commitment = 'processed'
  return {
    cluster: ENDPOINT!.name as EndpointTypes,
    current: new Connection('https://rpc-mainnet-fork.epochs.studio', {
      commitment,
      wsEndpoint: 'wss://rpc-mainnet-fork.epochs.studio/ws',
    }),
    endpoint: ENDPOINT!.url,
  }
}
```

> You can also use the [explorer](https://explorer.solana.com/?cluster=custom\&customUrl=https%3A%2F%2Frpc-mainnet-fork.epochs.studio) thats points to `https://rpc-mainnet-fork.epochs.studio` to check the RPC status.

Finally, Let's run the DApp:

```
$ yarn run dev
```

You should see the DAO list by visiting `http://localhost:3000`:

![](https://hackmd.io/_uploads/SklJvcPHq.png)

## Part 2: Create a NFT DAO

### Create a DAO

First, click ***Create DAO*** and select ***I want to create a bespoke DAO***:

![](https://hackmd.io/_uploads/r1I4CiPS5.png)

In ***Create a new realm***:

* **Name**: Choose your favorite name for the DAO
* **Min community tokens to create governance**: Select `1` to aollow users who has single NFT can create governance

![](https://hackmd.io/_uploads/SyWTk2vr9.png)

In ***Council Settings***:

* **Approval quorum (%)**: Select your desired threshold for approval. Default is 60%.
* **Team wallets**: Council member of the DAO. Default is the creator.

![](https://hackmd.io/_uploads/HkD1ZhPHq.png)

In ***DAO summary***, confirm and click ***Create DAO***.

![](https://hackmd.io/_uploads/rkuc-2vBq.png)

Nice! You should have the DAO ready at this moment.

### Configure NFT Voting Plugin

These are 3 instructions to be done in this step:

* Create NFT plugin registrar
* Create NFT plugin max voter weight
* Configure NFT plugin collection

#### Create NFT plugin registrar

Click ***New Proposal***:

![](https://hackmd.io/_uploads/Sku5X3vHq.png)

In ***Add a proposal***:

* **Title**: Use your favored name for the title of proposal
* **Transaction**: Select ***Create NFT plugin registrar***
* **Governance**: Select the only acccount in the list

Then, click ***Add proposal***, you should be able to see this page when the transaction is executed:

![](https://hackmd.io/_uploads/S11tS2wrc.png)

Here, we need to perform two extra actions: **Vote** and **Execute** the proposal.

First, click ***Vote Yes***, you should be able to see this page when the transaction is executed:

![](https://hackmd.io/_uploads/rk-kPnwS5.png)

Here, pay attention to the upper right green state **`Succeeded: Yes`**, this means that this proposal is ready for execution.

Proposals will enter **Succeeded** state via satisfying one of the condition:

1. Votes pass the approval threshold when the voting deadline comes
2. 100% votes approve

**In this case, condition 2 is satisfied since there is only one vote can approve this proposal.**

Secondly, lets execute the proposal by cliking ***Execute***:

![](https://hackmd.io/_uploads/rJKw5hvH9.png)

Nice! This means that the first proposal is executed successfully.

#### Create NFT plugin max voter weight

Next, let's create another proposal:

![](https://hackmd.io/_uploads/BJt6onwS9.png)

In **Add a proposal**:

* **Title**: Use your favored name for the title of proposal
* **Transaction**: Select ***Create NFT plugin max voter weight***
* **Governance**: Select the only acccount in the list

Click ***Add proposal*** and then perform **Vote** and **Execute**:

![](https://hackmd.io/_uploads/SJM532wH9.png)

#### Configure NFT plugin collection

Next, let's create another proposal:

![](https://hackmd.io/_uploads/BkWzahDBq.png)

In ***Add a proposal***:

* **Title**: Use your favored name for the title of proposal
* **Transaction**: Select ***Configure NFT plugin collection***
* **Governance**: Select the only acccount in the list
* **Collection Size**: The total collection size of your NFT. *Here we set the size to a smaller number just for the demo purpose*
* **Collection Weight**: The weighting of each vote. Default is `1`.
* **Collection**: Use the key of the collection of your NFT

Click ***Add proposal*** and then perform **Vote** and **Execute**:

![](https://hackmd.io/_uploads/ryw5ChwB9.png)

### Enable NFT Voting Plugin

We only have one final config to enable the NFT DAO feature. Click ***Params*** and **Change config** to open the modal:

![](https://hackmd.io/_uploads/r12pJaPr5.png)

* In ***Change Realm Config***:
  * **Community voter weight addin**: Use NFT Voting Plugin Program Id `GnftV5kLjd67tvHpNGyodwWveEKivz3ZWvvE3Z4xi2iw`
  * **Community max voter weight addin**: Use NFT Voting Plugin Program Id `GnftV5kLjd67tvHpNGyodwWveEKivz3ZWvvE3Z4xi2iw`

Click ***Add proposal*** and then perform **Vote** and **Execute**, then refresh the DAO dashboard:

![](https://hackmd.io/_uploads/B14Kepvrc.png)

Whoa! Now you should see the NFTs from the configureed collection displaying on the dashboard.

Finally, click ***Register*** to use your holding NFTs.

## Part 3: Propose and Vote

In this section, we will go through the follwoing operations:

* Create treasury for SOL and NFT
* Send funds to treasury for SOL and NFT
* Propose to transfer funds
* Execute the proposal

> Be aware of the tx size limit. The proposal will fail if the proposer has more than **3** NFTs.

### Create Treasury

Click ***New Treasury Account***:

![](https://hackmd.io/_uploads/By9LDTPH9.png)

In ***Create new DAO wallet***:

* **Min community tokens to create proposal**: Set to `1` to allow NFT holder to propose

Click ***Create***, you can see a new SOL and NFT treasury appear:

![](https://hackmd.io/_uploads/B1_qEADH5.png)

### Send Funds to Treasury

#### SOL Treasury

Click ***View*** of SOL treasury:

![](https://hackmd.io/_uploads/By44HAPB9.png)

Click ***Copy Deposit Address*** to get the address of the treasury. Now, anyone can transfer funds to this address if they wish.

#### NFT Treasury

Click ***View*** of NFTs:

![](https://hackmd.io/_uploads/Skz-3APSc.png)

Next, click ***Deposit NFT*** and then ***Deposit NFT to Treasury account address***:

![](https://hackmd.io/_uploads/rJxKTRvBc.png)

Copy the ***Treasury account address*** (the same as the SOL treasury) to get the address of the treasury. Now, anyone can transfer funds to this address if they wish.

Let's check the funds once the transfer is done:

![](https://hackmd.io/_uploads/SycpRADrq.png)

### Propose to Transfer Funds

Click ***View*** of NFTs and then ***Send NFT***:

![](https://hackmd.io/_uploads/SkEL1yuSq.png)

In ***Send NFT***:

* **Destination account**: Receiver of NFT

Click **Propose**:

![](https://hackmd.io/_uploads/SybRxJ_B5.png)

### Execute the Proposal

Finally, let's perform **Vote** and **Execute**:

![](https://hackmd.io/_uploads/BkOLWyurq.png)

Here, you can see the NFT locked in the NFT treasury is gone. Let's check to the receiver's wallet:

![](https://hackmd.io/_uploads/rkujZyOr9.png)

Whoa! The NFT transfer is executed successfully!

## Reference

* <https://en.wikipedia.org/wiki/Decentralized\\_autonomous\\_organization>
* <https://docs.realms.today/>
* <https://github.com/solana-labs/solana-program-library/tree/master/governance>
* <https://github.com/solana-labs/governance-ui>
* <https://sinoglobalcap.medium.com/how-to-solana-chapter-5-daos-governance-e41a753ce72a>
* <https://app.squads.so/>
* <https://twitter.com/Sebastian\\_Bor>
* <https://github.com/solana-labs/governance-program-library/pull/37>
* <https://realms.today/dao/MonkeDAO>
* <https://docs.realms.today/DAO-Management/createing-DAOs/NFT-Community-DAO>
* <https://nation.io/>


# #8 - Deep Dive into Anchor by Implementing Token Management Program

**Author:** [@ironaddicteddog](https://twitter.com/ironaddicteddog), [@emersonliuuu](https://twitter.com/emersonliuuu)

***\[Updated at 2022.5.21]***

> You can find the full code base [here](https://github.com/ironaddicteddog/anchor-token-management)

## What is Anchor?

There is a comprehensive explanation on the [official website](https://project-serum.github.io/anchor/getting-started/introduction.html). Let me just quote relative paragraphs here:

> Anchor is a framework for Solana's Sealevel runtime providing several convenient developer tools.
>
> If you're familiar with developing in Ethereum's Solidity, Truffle, web3.js, then the experience will be familiar. Although the DSL syntax and semantics are targeted at Solana, the high level flow of writing RPC request handlers, emitting an IDL, and generating clients from IDL is the same.

In short, Anchor gives you the following handy tools for developing Solana programs:

* **Rust crates and eDSL for writing Solana programs**
* **IDL specification**
* **TypeScript package for generating clients from IDL**
* **CLI and workspace management for developing complete applications**

You can watch [this awesome talk](https://youtu.be/cvW8EwGHw8U) given by Armani Ferrante at Breakpoint 2021 to feel the power of Anchor.

### Workflow

![](https://i.imgur.com/jkObSKO.jpg)

1. Develop the **program** (Smart Contract)
2. Build the program and export the **IDL**
3. Generate the **client** representation of program from the IDL to interact with the program

### Why Anchor?

* **Anchor is the new standard**
  * [apr.dev](https://apr.dev)
  * [anchor.so](https://anchor.so)
  * [anchor.projectserum.com](https://anchor.projectserum.com)
* Productivity
  * Make Solana program more intuitive to understand
  * More clear buisness Logic
  * Remove a ton of biolderplate code
* Security
  * Use **discriminator**
    * Discriminator is generated and inserted into the first 8 bytes of account data. Ex: `sha256("account:<MyAccountName>")[..8] || borsh(account_struct)`
    * Used for more secure account validation and function dispatch
    * See [this Twitter thread](https://twitter.com/armaniferrante/status/1411589634228772870) for more details
    * See [here](https://github.com/project-serum/anchor/blob/master/ts/src/program/namespace/index.ts#L53) and [here](https://github.com/project-serum/anchor/blob/master/lang/syn/src/codegen/program/dispatch.rs#L146) for the actual implementation
  * Implement most of the [best practices](https://github.com/project-serum/sealevel-attacks) of secure program

### Where can I learn more about Anchor?

* [Anchor Book](https://project-serum.github.io/anchor/)
* [Anchor Examples](https://github.com/project-serum/anchor/tree/master/tests)
* [`anchor-escrow`](https://book.solmeet.dev/notes/intro-to-anchor)

## Overview of Token Management

**Note: These programs are originated from Serum's** [**stake program**](https://github.com/project-serum/stake) **developed by Armani Ferrante.** There are a few things evolved from the original version:

* Upgrade Anchor to latest version (Currently `0.24.2`)
* Optimize some function invocation to avoid stack frame limit
* Rename struct and module to make them better reveal its designed purpose

In short, token management consist of two modules:

* **Locker Manager, which manages the vesting of locked tokens**
* **Pool Manager, which manages the mining of rewarded tokens**

### Vesting and Mining

* **Token vesting and mining are two fundenmental components of DeFi tokenomics**
* There are existed solutions.
  * Bonfida vesting
  * Quarry
  * ...
* `anchor-token-management` intergates vesting and mining modules and has the following features:
  * Written in Anchor
  * Modular and customizable condition of releasing rewards
  * Control funds even if its locked in locker. For example: desposit to pool from locker. (Not covered in this tutorial)

### Architecture

![](https://hackmd.io/_uploads/HJP76GYwc.png)

### Interfaces

#### Locker

**Create Locker**

![](https://hackmd.io/_uploads/SJCFdv2wq.png)

**Withdraw**

![](https://hackmd.io/_uploads/HJW1KP2v5.png)

***

#### Pool (Rewarder)

**Create Pool**

![](https://hackmd.io/_uploads/SyLE5whv9.png)

**Drop Reward**

![](https://hackmd.io/_uploads/rkYMnwnvc.png)

**Expired Reward**

![](https://hackmd.io/_uploads/r1r53wnD9.png)

***

#### Pool (Staker)

**Create Staker**

![](https://hackmd.io/_uploads/S1su5D2Dc.png)

**Update Staker Vault**

![](https://hackmd.io/_uploads/BJM2cv2wc.png)

**Deposit**

![](https://hackmd.io/_uploads/HyFA5Pnv5.png)

**Withdraw**

![](https://hackmd.io/_uploads/r1gbk2vhPq.png)

**Stake**

![](https://hackmd.io/_uploads/r1qZoDnwc.png)

**Start Unstake**

![](https://hackmd.io/_uploads/SkYuoDnDc.png)

**End Unstake**

![](https://hackmd.io/_uploads/ryb3jD2wc.png)

**Claim Reward**

![](https://hackmd.io/_uploads/Hklrhwhvc.png)

**Claim Reward to Locker**

![](https://hackmd.io/_uploads/H1kO3DhDq.png)

## Implementing Token Management Program in Anchor

### Prerequisites

* Basic layouts of Anchor programs
  * program
  * context
  * state
  * error
  * ...
* Anchor Constraints
  * `[account(mut)]`
  * `[account(init)]`
  * ...
* Token Program
  * Token Account
  * Mint
  * Transfer
  * Burn
* [`anchor-escrow`](https://book.solmeet.dev/notes/intro-to-anchor)

### To Be Covered

* PDA account creation and derivation
* Access control
* CPI usage
* Custimized Error

### Step 1: Scaffolding

* Checkout to [`step-1`](https://github.com/ironaddicteddog/anchor-token-management/tree/step-1) branch to see the full code
* In this step, we scaffold the programs by following the interfaces explained above:
  * Implement `LockerManager`
  * Implement `PoolManager`
  * Implement `PoolRewardKeeper`

### Part 2: Implementing

* Checkout to [`step-2`](https://github.com/ironaddicteddog/anchor-token-management/tree/step-2) branch to see the full code
* In this step, we implement all the functions, context and states without concerning the security issues:
  * Add all contexts and states
  * Add utils
    * `calculator`
    * `RewardQueue`
  * Add [constraints](https://docs.rs/anchor-lang/0.24.2/anchor_lang/derive.Accounts.html)
    * PDA creation and derivation
    * `has_one`
    * Raw constraints
    * ...

> Tips: you can use `git diff` to see what changes have been made:
>
> ```bash
> $ git checkout step-2
> $ git diff step-1
> ```

### Part 3: Improving Security

* Checkout to [`step-3`](https://github.com/ironaddicteddog/anchor-token-management/tree/step-3) branch to see the full code
* In this step, we improve the security:
  * Implement access control and security check
  * Customize error

> Tips: you can use `git diff` to see what changes have been made:
>
> ```
> $ git checkout step-3
> $ git diff step-2
> ```

## More Advanced Topics

Control funds even if its locked in locker. For example: desposit to pool from locker. See the full [code base](https://github.com/ironaddicteddog/anchor-token-management) for details.

* `withdraw_to_whitelist`
* `deposit_from_locker`
* `withdraw_to_locker`

## Referneces

* <https://book.anchor-lang.com>
* <https://solanacookbook.com>
* <https://book.solmeet.dev/notes/intro-to-anchor>
* <https://github.com/project-serum/stake/blob/master/docs/staking.md>
* <https://docs.rs/anchor-lang/0.24.2/anchor\\_lang/derive.Accounts.html>


# #9 - Walk Through Solana SDK Design

**Author:** [@SaiyanBs](https://twitter.com/SaiyanBs), [@wei\_sol\_](https://twitter.com/wei_sol_), [@emersonliuuu](https://twitter.com/emersonliuuu)

***\[Updated at 2022.06.22]***

## TL; DR

1. Why is SDK design important?
   * Usage - frontend
   * Performance
   * As connector between client side and programs
2. How to design SDK under Solana architecture
3. Compare between Bad & Good design

## Overview

> Get data and show it on the client side.

### How to Get the Data

#### web 2

1. Get data by API.
2. Mostly data is from the same or partnership company.

#### web 3

1. Get data by SDK.
2. Mostly need to get the on-chain data or others project open sourced SDK.

**Example**

```typescript=
// SDK
export async function getStakedAmount(
  poolInfos: PublicKey[],
  provider: anchor.Provider
) {
    ...
}
```

```yaml=
 // API
 /api/v1/getStakedAmount:
   get:
      tags:
      - "pet"
      summary: "Get staked amount"
      description: "Get staked amount"
      produces:
      - "application/xml"
      - "application/json"
      parameters:
      - name: "poolInfos"
        in: "query"
        description: ""
        required: true
        type: "array"
        items:
          type: "PublicKey"
        default: "[]"
      - name: "anchorProvider"
        in: "query"
        description: ""
        required: true
        type: "Provider"
        default: "[]"

```

### How to Show the Data on the Client Side

Basically it's the same no matter it's web2 or web3. Nowadays most frontend developers choose frameworks to get the job done, and for now and especially on Solana, I believe Next.js is the best option.

#### Show it on the client side.

For instance, using Next.js, we need to split components like puzzles, split the whole page into pieces, and each piece may contain the content (layout and data).

And how we design the components **depends on**

1. layout (basically follows the design)
2. data flow
3. performance
4. maintenance (low coupling, readable)
5. how SDK or API design

Sometimes these points can against each other, so I feel like designing the components and data flow is more like an art, **may need to sacrifice some points to achieve another one**, so just choose a better way at the moment you did this.

#### For Instance, NFT staking page in [Dappio](https://app.dappio.xyz/nft-staking)

![](https://hackmd.io/_uploads/rks9rwOK5.png)

![](https://i.imgur.com/0iMhvPQ.jpg)

#### Discuss

1. What's the same part and different part between two pics ?
   * Different data by different project.
   * The pending reward card both exist in DappieGang and others project but with different layouts and position.
   * DappieGang has one utility row and filter part, but the others don't.
2. What's the similar funtion between two pics ?
   * Get staked info
   * Get pending reward
   * Claim pending reward
   * Stake / unstaked
3. What do we need to consider?

### Let's check the first part - Overview info

![](https://hackmd.io/_uploads/HkG_SEx5c.png)

```typescript=
// v1
export async function getStakedAmount(
  poolInfos: PublicKey[],
  provider: anchor.Provider
) {
    ...
}
```

As we can see, NFTs staked in pools which are accounts, so we need to know the address to get the account data.

How do we get the pool public key in v1 ? We hardcoded all of them.

```typescript=
const getAllStakedData = async () => {
      const allDappiePools = [
        ...nftStakingIDs.DAPG_COMMON_POOL_INFOS,
        ...nftStakingIDs.DAPG_LEGENDARY_PATTERN_POOL_INFOS,
        ...nftStakingIDs.DAPG_LEGENDARY_ROBOT_POOL_INFOS,
        ...nftStakingIDs.DAPG_LEGENDARY_ALIEN_POOL_INFOS,
        ...nftStakingIDs.DAPG_LEGENDARY_ZOMBIE_POOL_INFOS,
        ...nftStakingIDs.DAPG_GENESIS_POOL_INFOS,
      ];

      let res = 0;
      switch (props.checkingProject.projectName) {
        case ESupportedProjects.DAPPIEGANG:
          res = await getStakedAmount(allDappiePools, props.anchorProvider);
          break;
        case ESupportedProjects.SOVANA:
          res = await getStakedAmount(sovanaPools, props.anchorProvider);
          break;
        default:
          break;
      }
    
    ....
}
```

### Pending Reward

Then, it's the pending reward part which also exists in DappieGang's second row.

![](https://hackmd.io/_uploads/rk7EuNx99.png)

First thing we need to know is where's the reward from? The only reason we can get the `NFTU` is because we deposit our prove token into farm and farming/mining, so we need to know the farm's account to get the infos we need.

But where's the prove token from? The flow is

1. We stake our NFTs into pools get prove token.
2. We deposit our prove tokens to get farming token and mining NFTU.

So we need to know which pool to stake first, because differnt NFTs rarity stake to different pools, and this is defined by initialization, and all the rarity info also stores on chain.

```typescript=
// Get user staked data
// Notice! This is all user staked, includes different projects
export async function getStakedNFTMint(
  owner: PublicKey,
  provider: anchor.Provider,
  poolInfo?: PublicKey
) {
    ...
}
    
// Then we need to know staked NFTs' rarity and pool info
// And in SDK v1, we have two options to get this

// Option 1, pass in the poolInfos we hardcoded at first 
export async function getPoolInfo(poolInfos: PublicKey[], provider: anchor.Provider) {
    ...
    // And this will return rarity and all the mints stored in the pool
    // Of course we need to map with user staked ones again to know the exact one.
}
    
// Option 2, pass in the mints we wanna know
export async function rarityFilter(mintList: PublicKey[], provider: anchor.Provider) {
    ...
    // And this will return what pool and rarity are about these mints.
    // And inside this SDK, it'll fetch the on chain program to get these data.
    // It could be a multiple RPC requests!!
}

    
// Next we need to get farm info by pool info
export async function getFarmFromPool(poolInfo: PublicKey, provider: anchor.Provider, nonce = 0) {
    ...
    // And this one only accept single poolInfo one time, so if user got multiple NFTs and staked in different pools, we need to call this funciton multiple times.
    // It could be a multiple RPC requests!!
}

    
// Finally we get the farm public keys, so we can get the pending rewards we want
export async function getUnclaimedReward(
  owner: PublicKey,
  farmInfo: PublicKey,
  provider: anchor.Provider
) {
    ...
    // Also, only accept one farm at a time, so could be
    // Multiple RPC requests!!
}
```

As we can see, just only the pending rewards could make tons of RPC requests. And here we only go through the funtional parts, as a frontend developer, you need to deal with the component design to make it maintainable, readable, and make sure the performance won't destroy the user experience at the same time.

![](https://hackmd.io/_uploads/S1eXc-y59.png)

```typescript=
// And this is the SDK v1 stake, what should we do to make it happen ?
export async function stake(
  user: PublicKey,
  poolInfo: PublicKey,
  nftAccountList: PublicKey[],
  provider: anchor.Provider
) {
    ...
    // mint -> rarity -> pool -> stake
    // prove token -> farm -> deposit -> mining
}
```

#### Example - V2 version

```typescript=
export async function fetchAll(
  provider: anchor.Provider,
  adminKey?: PublicKey
) {
    ...
}
    
export function infoAndNftMatcher(
  allInfos: AllInfo[], 
  nftMint: PublicKey[]
) {
    ...
}
    
export function getStakedAmount(
  allInfos: AllInfo[],
  collection: string = "",
  rarity: string = ""
) {
  ...  
}
  
export async function stakeTxn(
  poolInfo: PoolInfo,
  user: PublicKey,
  userNftAccountList: PublicKey[],
  provider: anchor.AnchorProvider
) {
    ...
}
```

## Problems

1. **Hard code ID in SDK**

Which means once we add new category we will need to update SDK too.

```
export const ALL_POOL_INFOS = [new PublicKey("POOL_INFO_KEY")];
```

And here's only part of the DappieGang infos, so as we partnership more projects, it'll end up become a file that you don't want to involved.

![](https://hackmd.io/_uploads/ry2BVi0Yq.png)

2. **Too many RPC request** As we can see, in the pending rewards part, we need to call a tons of RPC requests to get the data we want, and it's just the pending reward part.
3. **Maintainance** Again, by the pending reward example we know, you have to call functions one by one and sometimes not very intuitive, especially from the client side.

From a frontend developer's view, we need to deal with a lot of for loop, sync/async issues, components design, state management.

From a team member's view, with a poor SDK design, we need to setup a clear workflow for different roles (frontend, SDK, program).

For example, the hardcoded pool and farm infos, which side to store all of these infos, and when to update the file after initializing a new one, how to maintain this file ..etc

### Goal: Design the Most Friendly SDK or API for Frontend

* Stateless, less parameters or arguments.
* Call anywhere we want, no need to consider the context.

## Good SDK Design

### Prerequisites

* Solana system model
* Account model

#### Solana system model

![](https://hackmd.io/_uploads/HJ7vkYHl5.png)

Reading from solana starts with sending a [http request](https://docs.solana.com/developing/clients/jsonrpc-api) to the RPC, and the data is sync from validator.

Writing data(sending transaction) to Solana is also first sent to RPC, the RPC will lookup the leader and pass the tx packet to it via a UDP request. The transaction will next be verified and processed.

Each instructions will be executed in the program, modifying account data. New block contained state changes will be sync across validators and voted.

#### Storing data in Solana

![](https://hackmd.io/_uploads/HkuU8iQx9.png)

> Image from <https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/>

### Rule of Thumb

#### 1. SDK Architecture

![](https://hackmd.io/_uploads/H1I-g3aY5.jpg)

This is an overview of a good SDK design. It can be separated into two part, read and write.

* The "read" part of the SDK is about fetching the data from RPC and deserialized into an object.
* The "write" part of the SDK is to create a transaction object to interact with programs.

Utility, layouts and ids is used across reading and writing.

#### 2. Reduce RPC request

* Each Account data only needs to fetch once.
* Reduce the usage of `Connection`.

#### 3. Stateless design

* SDK is for reading/writing data to Solana.
* To read form the chain, account needs to be fetched and deserialized.
* To write to the chain, a TX is built and send.
* SDK only handles data decoding and tx building.
* Data is not modified by any functions/methods.

## NFT Staking Program

![](https://hackmd.io/_uploads/S1OVTa0uq.png)

* A program that give out `Prove Token` by locking certain collections of NFT.
* NFT is stored separately in different Vaults.
* Mint list is manage by rarity program.

## NFT Staking Program (Implement)

> Find the full code base [here](https://github.com/Dappio-emerson/solmeet-9-nft-staking)

**Program Architecture**

```
solmeet-9-nft-staking
├── app
├── migrations
│   └── initialization
├── mintList
├── programs
│   ├── nft-rarity
│   └── nft-staking
├── target
│   ├── idl
│   └── types
├── tests
│   ├── v1
│   └── v2
└── ts
    ├── v1
    └── v2
```

### Setup Local Test Validator

Restart local validator, and clone a useful program for creating ATA from Mainnet. See more about the program [here](https://github.com/mercurial-finance/create-ata-if-missing-program)

```
$ solana-test-validator -r -c 9tiP8yZcekzfGzSBmp7n9LaDHRjxP2w7wJj8tpPJtfG -u https://api.mainnet-beta.solana.com
```

Configure RPC url to localnet and the wallet to the one for deploying. Make sure the wallet have enough balance (\~ 10 SOL)

```
$ solana config get

# config solana setting to target wallet and network
$ solana config set -k ~/.config/solana/id.json -u localhost

# check balance
$ solana balance

# request airdrop
$ solana airdrop 10
```

### Build And Deploy

Clone the code from github repo

```
$ git clone https://github.com/Dappio-emerson/solmeet-9-nft-staking.git
```

Install dependency and make sure you have replace program keys for all files below.

1. `programs/nft-rarity/src/lib.rs`
2. `programs/nft-staking/src/lib.rs`
3. `ts/v1/ids.ts`
4. `ts/v2/ids.ts`
5. `Anchor.toml`

```
#cd solmeet-9-nft-staking
$ yarn

# generate program key
$ anchor keys list
```

After replacing program keys, we are ready to build and deploy our program with command below.

```
$ anchor build
$ anchor deploy
```

**NOTICE**

Make sure the **`Program Id`** you get after deployed match with the one in program **`declare_id!("6Utx...QnKM")`**, otherwise transaction we send might failed.

### SDK v1

Before we start staking NFT to program will need to initialize the allowed mint list to `rarity info` in rairty program, then initialize `pool info` with the `rarity info` key we just initialized.

```
# run user defined scripts to initialize 
$ anchor run initializeState
```

**DEBUG**

If you have some error message like something below, run `yarn add ts-mocha` might solve. Thanks to [this post](https://stackoverflow.com/questions/71119753/solana-test-program-anchor-test-failing-tsconfig-json-needs-an-import-asserti).

```
TypeError: Module "YOUR_FILE_PATH/solmeet-9-nft-staking/tsconfig.json" needs an import assertion of type "json" 
  at new NodeError ...
```

#### Import library and declare variables

After initialization, we can implement staking part in `test/v1/1_nft-staking-v1.ts`, let's paste the code below to the file.

```typescript=
import * as anchor from "@project-serum/anchor";
import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet";
import { PublicKey } from "@solana/web3.js";
import * as fs from "fs";
import { findAssociatedTokenAddress } from "../../ts/v1/utils";
import * as nftFinanceSDK from "../../ts/v1";
import {
  COLLECTION_SEED,
  RARITY_SEED,
  MINT_LIST_PATH,
  connection,
} from "../0_setting";

describe("nft staking v1", () => {
  const wallet = NodeWallet.local();
  const options = anchor.AnchorProvider.defaultOptions();
  const provider = new anchor.AnchorProvider(connection, wallet, options);
  anchor.setProvider(provider);

  interface Classify {
    poolInfoKey: PublicKey;
    NftTokenAccountList: PublicKey[];
  }

  let poolInfoKey: PublicKey;
  let poolInfos: PublicKey[];
  let nftMintList: PublicKey[] = [];

  it("read nft mint", async () => {
    const rawData = fs.readFileSync(MINT_LIST_PATH, "utf-8");
    const data: string[] = JSON.parse(rawData);
    data.forEach((element) => {
      nftMintList.push(new PublicKey(element));
    });
  });

});

```

#### Add log before and after staking

```typescript=
  it("read nft mint", async () => {
    ...
  }
  
  it("staked status: before stake", async () => {
    console.log("staked status: before stake");

    const poolInfos = await nftFinanceSDK.getPoolInfo([poolInfoKey], provider);

    console.log(
      `staking rate: ${(poolInfos[0].totalLocked / nftMintList.length) * 100}%`
    );
    console.log(`# of nft staked: ${poolInfos[0].totalLocked}`);
  });

  it("stake nft", async () => {
    // TODO
  });

  it("staked status: after stake", async () => {
    console.log("staked status: after stake");

    const poolInfos = await nftFinanceSDK.getPoolInfo([poolInfoKey], provider);

    console.log(
      `staking rate: ${(poolInfos[0].totalLocked / nftMintList.length) * 100}%`
    );
    console.log(`# of nft staked: ${poolInfos[0].totalLocked}`);
  });

  it("unstake nft", async () => {
    // TODO
  });
```

#### Implement stake/unstake

Now, we are good to implement staking part! Let's think about what we need for building stake transaction.

![](https://hackmd.io/_uploads/BkqjHagc5.png)

Arguments we need for staking:

* staking pool info
  * **pool info key**
  * prove token mint
  * prove token authority
  * ...
* user info
  * user address
  * NFT mint
  * prove token ATA

![](https://hackmd.io/_uploads/B1QbV3g5c.png)

Pool info key is one of the argument we're going to use later, so we need to generate it first.

```typescript=
  ...

  let poolInfoKey: PublicKey;
  let poolInfos: PublicKey[];
  let nftMintList: PublicKey[] = [];

  it("general pool info key", async () => {
    // find poolInfoAccount
    poolInfoKey = await nftFinanceSDK.getPoolInfoKeyFromSeed(
      wallet.publicKey,
      COLLECTION_SEED,
      RARITY_SEED,
      0
    );
  });

  it("read nft mint", async () => {
    ...
  }
```

Add stake and unstake test.

```typescript=
  ...
  
  it("stake nft", async () => {
    const pairs = await nftFinanceSDK.rarityFilter(nftMintList, provider);

    const pairsClassify: Classify[] = [];
    for (let pair of pairs) {
      const nftTokenAccount = await findAssociatedTokenAddress(
        wallet.publicKey,
        pair.mint
      );
      const target = pairsClassify.filter((item) =>
        item.poolInfoKey.equals(pair.poolInfoKey)
      );
      if (target.length == 0) {
        pairsClassify.push({
          poolInfoKey: pair.poolInfoKey,
          NftTokenAccountList: [nftTokenAccount],
        });
      } else {
        target[0].NftTokenAccountList.push(nftTokenAccount);
      }
    }

    for (let classify of pairsClassify) {
      console.log(`poolInfo: ${classify.poolInfoKey.toString()}`);
      const stakeTxn = await nftFinanceSDK.stake(
        wallet.publicKey,
        classify.poolInfoKey,
        classify.NftTokenAccountList,
        provider
      );
      for (let txn of stakeTxn) {
        const result = await provider.sendAndConfirm(txn, [wallet.payer]);
        console.log("<Stake>", result);
      }
    }
  });

  ...

  it("unstake nft", async () => {
    const userStakedNft = await nftFinanceSDK.getStakedNFTMint(
      wallet.publicKey,
      provider
    );

    const pairsClassify: Classify[] = [];
    for (let pair of userStakedNft) {
      const target = pairsClassify.filter((item) =>
        item.poolInfoKey.equals(pair.poolInfoKey)
      );
      if (target.length == 0) {
        pairsClassify.push({
          poolInfoKey: pair.poolInfoKey,
          NftTokenAccountList: [pair.nftMint],
        });
      } else {
        target[0].NftTokenAccountList.push(pair.nftMint);
      }
    }

    for (let classify of pairsClassify) {
      const unstakeTxn = await nftFinanceSDK.unstake(
        wallet.publicKey,
        classify.poolInfoKey,
        classify.NftTokenAccountList,
        provider
      );
      for (let txn of unstakeTxn) {
        const result = await provider.sendAndConfirm(txn, [wallet.payer]);
        console.log("<Unstake>", result);
      }
    }
  });
```

Now, we can run command below to see all test works well or not.

```
$ anchor run testV1
```

#### Implement stake/unstake transaction in SDK

After running test, you might found we didn't actually stake our NFT to program since we didn't implememt the logic for stake and unstake in SDK yet. Let's add the logic in `ts/v1/transaction.ts` and run the test again.

**Stake**

```typescript=
export async function stake(
  user: PublicKey,
  poolInfo: PublicKey,
  nftAccountList: PublicKey[],
  provider: anchor.Provider
) {
  anchor.setProvider(provider);
  const NftStakingProgram = new anchor.Program(
    nftStakingIDL,
    NFT_STAKING_PROGRAM_ID,
    provider
  );

  // fetch poolInfo
  const poolInfoAccount = await NftStakingProgram.account.poolInfo.fetch(
    poolInfo
  );

  // create user prove token ATA
  const userProveTokenAccount = await findAssociatedTokenAddress(
    user,
    poolInfoAccount.proveTokenMint
  );
  const createProveTokenAtaIx = await createATAWithoutCheckIx(
    user,
    poolInfoAccount.proveTokenMint
  );

  const createAtaIxArr: anchor.web3.TransactionInstruction[] = [];
  createAtaIxArr.push(createProveTokenAtaIx);

  const stakeTxArr: Transaction[] = [];
  for (let userNftAccount of nftAccountList) {
    const nftAccount = await getAccount(provider.connection, userNftAccount);
    const nftMint = nftAccount.mint;

    const [nftVaultAccount, _] = await PublicKey.findProgramAddress(
      [nftMint.toBuffer(), poolInfo.toBuffer(), Buffer.from(NFT_VAULT_SEED)],
      NftStakingProgram.programId
    );

    // create nft vault ATA
    let nftVaultAta = await findAssociatedTokenAddress(
      nftVaultAccount,
      nftMint
    );
    const createAtaIx = await createATAWithoutCheckIx(
      nftVaultAccount,
      nftMint,
      user
    );

    createAtaIxArr.push(createAtaIx);

    const StakeTx = NftStakingProgram.transaction.stake({
      accounts: {
        user,
        poolInfo,
        nftMint,
        userNftAccount,
        nftVaultAta,
        userProveTokenAccount,
        nftVaultAccount,
        proveTokenMint: poolInfoAccount.proveTokenMint,
        rarityInfo: poolInfoAccount.rarityInfo,
        proveTokenAuthority: poolInfoAccount.proveTokenAuthority,
        proveTokenVault: poolInfoAccount.proveTokenVault,
        systemProgram: anchor.web3.SystemProgram.programId,
        tokenProgram: TOKEN_PROGRAM_ID,
      },
    });
    stakeTxArr.push(StakeTx);
  }

  const createAtaTxArr: Transaction[] = [];
  let tx = new Transaction();
  for (let [index, ix] of createAtaIxArr.entries()) {
    tx.add(ix);
    if (
      (index + 1) % ATA_TX_PER_BATCH == 0 ||
      index == createAtaIxArr.length - 1
    ) {
      createAtaTxArr.push(tx);
      tx = new Transaction();
    }
  }
  const allTx = createAtaTxArr.concat(stakeTxArr);

  return allTx;
}
```

**Unstake**

```typescript=
export async function unstake(
  user: PublicKey,
  poolInfo: PublicKey,
  nftList: PublicKey[],
  provider: anchor.Provider
) {
  anchor.setProvider(provider);
  const NftStakingProgram = new anchor.Program(
    nftStakingIDL,
    NFT_STAKING_PROGRAM_ID,
    provider
  );

  // fetch poolInfo
  const poolInfoAccount = await NftStakingProgram.account.poolInfo.fetch(
    poolInfo
  );

  // create user prove token ATA
  const userProveTokenAccount = await findAssociatedTokenAddress(
    user,
    poolInfoAccount.proveTokenMint
  );

  const unstakeTxPromises = nftList.map(async (nftMint) => {
    const [nftVaultAccount, _] = await PublicKey.findProgramAddress(
      [nftMint.toBuffer(), poolInfo.toBuffer(), Buffer.from(NFT_VAULT_SEED)],
      NftStakingProgram.programId
    );

    let userNftAccount = await findAssociatedTokenAddress(user, nftMint);
    const createAtaIx = await createATAWithoutCheckIx(user, nftMint);

    // create nft vault ATA
    let nftVaultAta = await findAssociatedTokenAddress(
      nftVaultAccount,
      nftMint
    );

    const UnstakeTx = NftStakingProgram.transaction.unstake({
      accounts: {
        user,
        poolInfo,
        nftMint,
        userNftAccount,
        nftVaultAta,
        userProveTokenAccount,
        nftVaultAccount,
        proveTokenMint: poolInfoAccount.proveTokenMint,
        rarityInfo: poolInfoAccount.rarityInfo,
        proveTokenAuthority: poolInfoAccount.proveTokenAuthority,
        proveTokenVault: poolInfoAccount.proveTokenVault,
        tokenProgram: TOKEN_PROGRAM_ID,
      },
      preInstructions: [createAtaIx],
    });

    return UnstakeTx;
  });

  return Promise.all(unstakeTxPromises);
}
```

Run command again, then you can stake all your NFT to the program now!

```
$ anchor run testV1
```

### SDK v2

You might notice there are some problems while we implementing stake/unstake.

1. We need to know corresponding pool info key before we use it, and there are two way to get the key. One is generate with the seed as we done previous, and another is hard coded in SDK. Someone who used this SDK won't know the seed, so pool info key might need to hard coded in SDK and this result in frequently update SDK due to new partner joined.
2. Since we only have pool info key, we must fetch account info when building transaction. You can imagine if user is going to stake lots of NFT in different pool that might be a disaster for just building transaction by sending tons of RPC request.

#### Refactor SDK

In order to solve the issues above, we will try to

* Make stake/unstake transaction/instruction stateless
* Remove hard coded stuff

Let's take a closer look at v1 SDK, the root cause of why we need to make multiple RPC request comes from the bad design of the interface. If we **only** had `pool info key` associated with `PoolInfo`, we would **force** fetching data in every function that requires `PoolInfo` data except the key. Actually, we can extract the fetching data from building transaction by implementing a fetch function, so called **`fetchAll()`**, to get all account we need for no matter matching NFT with pool or building transaction.

This is what `fetchAll()` function do, can see the `AllInfo` class definition in `ts/v2/poolInfos.ts`

```typescript=
export async function fetchAll(
  provider: anchor.Provider,
  adminKey?: PublicKey
): Promise<AllInfo[]> {
  const nftStakingProgram = new anchor.Program(
    nftStakingIDL,
    NFT_STAKING_PROGRAM_ID,
    provider
  );
  const nftRarityProgram = new anchor.Program(
    nftRarityIDL,
    NFT_RARITY_PROGRAM_ID,
    provider
  );

  let adminIdMemcmp: MemcmpFilter;
  if (adminKey != null && adminKey != undefined) {
    adminIdMemcmp = {
      memcmp: {
        offset: 8,
        bytes: adminKey.toString(),
      },
    };
  }

  const poolInfoSizeFilter: DataSizeFilter = {
    dataSize: nftStakingProgram.account.poolInfo.size,
  };
  let filters: (anchor.web3.MemcmpFilter | anchor.web3.DataSizeFilter)[] = [
    poolInfoSizeFilter,
  ];
  if (adminKey != null && adminKey != undefined) {
    filters = [poolInfoSizeFilter, adminIdMemcmp];
  }
  const allPoolInfos = await nftStakingProgram.account.poolInfo.all(filters);

  filters = [];
  if (adminKey != null && adminKey != undefined) {
    filters = [adminIdMemcmp];
  }
  const allRarityInfos = await nftRarityProgram.account.rarityInfo.all(filters);

  const allInfos: AllInfo[] = [];
  for (let currentRarityInfo of allRarityInfos) {
    for (let currentPoolInfo of allPoolInfos) {
      if (
        currentRarityInfo.publicKey.equals(currentPoolInfo.account.rarityInfo)
      ) {
        const rarityInfo = new RarityInfo(
          currentRarityInfo.publicKey,
          currentRarityInfo.account.admin,
          Buffer.from(currentRarityInfo.account.collection)
            .toString("utf-8")
            .split("\x00")[0],
          Buffer.from(currentRarityInfo.account.rarity)
            .toString("utf-8")
            .split("\x00")[0],
          currentRarityInfo.account.mintList
        );

        const poolInfo = new PoolInfo(
          currentPoolInfo.publicKey,
          currentPoolInfo.account.admin,
          currentPoolInfo.account.proveTokenMint,
          rarityInfo.key,
          currentPoolInfo.account.proveTokenAuthority,
          currentPoolInfo.account.proveTokenVault,
          Number(currentPoolInfo.account.totalLocked)
        );

        allInfos.push(new AllInfo(rarityInfo, poolInfo));
        break;
      }
    }
  }

  return allInfos;
}
```

After implementing `fetchAll()` with some class to store the data, now we only fetch data at the beginning by calling `fetchAll()`, then we can pass the data as an argument for building transaction.

![](https://hackmd.io/_uploads/BJrERYe5q.png)

#### Implement test with v2 (refactored) SDK

Open `tests/v2/1_nft-staking-v2.ts` you will see the code below (no need to do any modification). You can see that we don't need to generate pool info key first neither hard coded those keys in SDK now, we get all data with this line of code: **`allInfos = await nftFinanceSDK.fetchAll(provider);`**

```typescript=
import * as anchor from "@project-serum/anchor";
import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet";
import { PublicKey } from "@solana/web3.js";
import * as fs from "fs";
import { findAssociatedTokenAddress } from "../ts/utils";
import * as nftFinanceSDK from "../ts";
import { AllInfo } from "../ts/poolInfos";
import { UserInfo } from "../ts/userInfos";
import {
  COLLECTION_SEED,
  RARITY_SEED,
  MINT_LIST_PATH,
  connection,
} from "./0_setting";

describe("nft staking v2", () => {
  const wallet = NodeWallet.local();
  const options = anchor.AnchorProvider.defaultOptions();
  const provider = new anchor.AnchorProvider(connection, wallet, options);
  anchor.setProvider(provider);

  interface Classify {
    allInfo: AllInfo;
    NftTokenAccountList: PublicKey[];
  }

  let allInfos: AllInfo[];
  let nftMintList: PublicKey[] = [];

  it("read nft mint", async () => {
    const rawData = fs.readFileSync(MINT_LIST_PATH, "utf-8");
    const data: string[] = JSON.parse(rawData);
    data.forEach((element) => {
      nftMintList.push(new PublicKey(element));
    });
  });

  it("staked status: before stake", async () => {
    allInfos = await nftFinanceSDK.fetchAll(provider);
    console.log("staked status: before stake");

    const percentage = nftFinanceSDK.getStakedPercentage(
      allInfos,
      COLLECTION_SEED
    );
    console.log(`staking rate: ${percentage * 100}%`);
    const amount = nftFinanceSDK.getStakedAmount(allInfos, COLLECTION_SEED);
    console.log(`# of nft staked: ${amount}`);
  });

  it("stake nft", async () => {
    const pairs = nftFinanceSDK.infoAndNftMatcher(allInfos, nftMintList);

    const pairsClassify: Classify[] = [];
    for (let pair of pairs) {
      const nftTokenAccount = await findAssociatedTokenAddress(
        wallet.publicKey,
        pair.nftMint
      );
      const target = pairsClassify.filter((item) =>
        item.allInfo.poolInfo.key.equals(pair.allInfo.poolInfo.key)
      );
      if (target.length == 0) {
        pairsClassify.push({
          allInfo: pair.allInfo,
          NftTokenAccountList: [nftTokenAccount],
        });
      } else {
        target[0].NftTokenAccountList.push(nftTokenAccount);
      }
    }

    for (let classify of pairsClassify) {
      const stakeTxn = await nftFinanceSDK.txn.stakeTxn(
        classify.allInfo.poolInfo,
        wallet.publicKey,
        classify.NftTokenAccountList,
        provider
      );
      for (let txn of stakeTxn) {
        const result = await provider.sendAndConfirm(txn, [wallet.payer]);
        console.log("<Stake>", result);
      }
    }
  });

  it("staked status: after stake", async () => {
    console.log("staked status: after stake");
    allInfos = await nftFinanceSDK.fetchAll(provider);

    const percentage = nftFinanceSDK.getStakedPercentage(
      allInfos,
      COLLECTION_SEED
    );
    console.log(`staking rate: ${percentage * 100}%`);
    const amount = nftFinanceSDK.getStakedAmount(allInfos, COLLECTION_SEED);
    console.log(`# of nft staked: ${amount}`);
  });

  it("get user info", async () => {
    const userInfo = await nftFinanceSDK.fetchUser(wallet.publicKey, provider);

    console.log(`user address: ${userInfo.wallet.toString()}`);
    console.log(`# of user staked nft: ${userInfo.staked.length}`);
  });

  it("unstake nft", async () => {
    const userInfo = await nftFinanceSDK.fetchUser(wallet.publicKey, provider);
    const pairsClassify: Classify[] = [];
    for (let pair of userInfo.staked) {
      const target = pairsClassify.filter((item) =>
        item.allInfo.poolInfo.key.equals(pair.poolInfoKey)
      );
      if (target.length == 0) {
        pairsClassify.push({
          allInfo: nftFinanceSDK.getAllInfoFromPoolInfoKey(
            allInfos,
            pair.poolInfoKey
          ),
          NftTokenAccountList: [pair.nftMint],
        });
      } else {
        target[0].NftTokenAccountList.push(pair.nftMint);
      }
    }

    for (let classify of pairsClassify) {
      const unstakeTxn = await nftFinanceSDK.txn.unstakeTxn(
        classify.allInfo.poolInfo,
        wallet.publicKey,
        classify.NftTokenAccountList,
        provider
      );
      for (let txn of unstakeTxn) {
        const result = await provider.sendAndConfirm(txn, [wallet.payer]);
        console.log("<Unstake>", result);
      }
    }
  });
});
```

Run command below to make sure no issue occured.

```
$ anchor run testV2
```

#### Implement stake/unstake transaction in SDK v2

Once all test passed, let's add the logic for stake and unstake transaction in `ts/v2/transaction.ts`. Since we pass in full pool info data instead of only the key, we can remove all RPC request during transaction building.

**Stake**

```typescript=
export async function stakeTxn(
  poolInfo: PoolInfo,
  user: PublicKey,
  userNftAccountList: PublicKey[],
  provider: anchor.AnchorProvider
) {
  const ixArr: anchor.web3.TransactionInstruction[] = [];
  const createAtaTxnArr: Transaction[] = [];
  const stakeTxnArr: Transaction[] = [];
  for (let [index, userNftAccount] of userNftAccountList.entries()) {
    const StakeIxArr = await ix.stakeIx(
      poolInfo,
      user,
      userNftAccount,
      provider
    );
    if (index == 0) {
      ixArr.push(StakeIxArr[StakeIxStatus.createUserProveTokenAtaIx]);
    }
    ixArr.push(StakeIxArr[StakeIxStatus.createNFTVaultAtaIx]);
    const stakeTx = new Transaction();
    stakeTx.add(StakeIxArr[StakeIxStatus.stakeIx]);
    stakeTxnArr.push(stakeTx);
  }
  let txn = new Transaction();
  for (let [index, instruction] of ixArr.entries()) {
    txn.add(instruction);
    if ((index + 1) % ATA_TX_PER_BATCH == 0 || index == ixArr.length - 1) {
      createAtaTxnArr.push(txn);
      txn = new Transaction();
    }
  }

  const allTxn = createAtaTxnArr.concat(stakeTxnArr);

  return allTxn;
}
```

**Unstake**

```typescript=
export async function unstakeTxn(
  poolInfo: PoolInfo,
  user: PublicKey,
  nftMintList: PublicKey[],
  provider: anchor.AnchorProvider
) {
  const allTxn: Transaction[] = [];

  for (let nftMint of nftMintList) {
    const createAtaIx = await createATAWithoutCheckIx(user, nftMint);
    const unstakeIx = await ix.unstakeIx(poolInfo, user, nftMint, provider);
    const txn = new Transaction();
    txn.add(createAtaIx);
    txn.add(unstakeIx);
    allTxn.push(txn);
  }

  return allTxn;
}
```

Run command below again and now we successfully use refactored SDK to stake NFT!

```
$ anchor run testV2
```

### Difference Between v1 and v2 SDK

* **How we get account data**

  In v1 we fetch one account since we only get one pool info key at a time, but in v2 we done the fetching part at the beginning, and fetch all account with same structure in one RPC request.
* **Where we get account data**

  In v1 we hard coded the account address in SDK instead of storing data in client side by fetching all account we need at a time. In v2 we implement with the opposite way, which reduce the frequency of fetching same account in different function.
* **Maintainability**

  In v1 we'll need to update the hard coded stuff if new partner joined or we add new category which is tough to maintain. In v2, by replacing hard coded stuff with storing class in client side, there's no need to modify SDK due to new pool been created.

## Reference

* [book.solmeet.dev](https://book.solmeet.dev/)
* [BUIDL an Auto-compounding Bot on Saber](https://book.solmeet.dev/notes/buidl-auto-compounding-bot)
* [Dappio: NFT staking](https://app.dappio.xyz/nft-staking)
* [Anchor Book](https://book.anchor-lang.com/)
* [JSON RPC API](https://docs.solana.com/developing/clients/jsonrpc-api)


# #10 - Walk Through NFT Breeding Tokenomic and Program Design

**Author:** [@web3lovemore](https://twitter.com/web3lovemore), [@emersonliuuu](https://twitter.com/emersonliuuu), [@wei\_sol\_](https://twitter.com/wei_sol_)

***\[Updated at 2022.07.28]***

> **See the example repo** [**here**](https://github.com/DappioWonderland/nft-breeding)

## TL; DR

Deep diving on NFT application: How to do an breedable NFT and its application. For optimization of uility and playability.

(1) **Common Breeding Mechanisms**

The application for breedable NFT with common mechanisms and its tokenomics. This session will be set as a basic breeding approach with verifiable randomness. Not only use as gamefi application but also extension and flexibility for new standard on metaplex.

Note that the **randomness** as blockhash method, the developer can be customized for ones ex. chainlink VRF 、 switchboard and Pyth nework etc.

(2) **Case Study**

Introducing popular cases and scenarios with breeding uility projects on Solana ex. StepN or Solchick. or even with Fungible token expense to breed the new NFT ex. Stoneapecrew.

This is can be standardized as breeding on-chain protocol for developer as the customized the uilities.

## Overview

### NFT breeding mechanism and common design

note: A,B,C are NFT; $FT is FT.

(1) A+B->C \[Burn A,B]

(2) **A+B->A+B+C \[Most common]**

(3) A+$FT->C \[tokenomics extension]

(4) A+$FT->A+C

(5) A+B+$FT->A+B+C

### What is Breeding NFTs?

Breeding NFTs is a mechanism in which two **breedable NFTs** breed to form a new NFT offspring.

NFT project creators and startups employ NFT breeding to create long-term utility for collectors of their NFT. Rather than depending on the ever-shifting value of NFTs based on public sentiment, project creators go a step further to create real methods and ways in which collectors can benefit from buying their NFTs.

Breeding NFTs to get a new NFT (that's usually seen as more valuable and unique) is a significant way for collectors of an NFT to gain more from their initial investment. When a project uses NFT breeding to **create utility** for their NFTs, some or all of the NFTs in the collection of games are made to be breedable.

Generally, each NFT in a collection or game is unique and distinguishable from others. This is how they're perceived as valuable. When the NFTs are breedable and breed with another breedable NFT, what results is a new NFT that did not previously exist. This NFT will possess a combination of the unique features (usually the best traits) of the parent NFTs.

Because of these unique features, the NFT offspring is a rarer and more unique NFT, **increasing the collector's portfolio value**.

### History of Breeding NFTs

With the advent of blockchain, gaming came breeding NFTs. CryptoKitties, besides being the first widely recognized blockchain game, first imbibed the concept of NFT breeding.

In the game, each Kitty is an NFT that's unique and valuable. Players can own and trade these Kitties just like holding other NFTs. Players can also breed these Kitties to generate offspring.

Since the mechanism of NFT breeding became a successful and well-encouraged function in CryptoKitties, many other blockchain-based gaming projects have used NFT breeding to bring more utility to players who buy character NFTs in their games.

![](https://hackmd.io/_uploads/S1Nr_Py69.png)

Photo Resource: <https://www.youtube.com/watch?v=WbKVrhXfHaY>

### Stories of Axie infinity: (Inspired by cryptoKitties)

see [Storis on Crypto kitty and Axie infinity feat. Animoca Brands](https://hackmd.io/@Piercetw/Byv9l5kp9) for references

![](https://hackmd.io/_uploads/rk5JB6J65.png) ![](https://hackmd.io/_uploads/SkakS6yT5.jpg)

<https://whitepaper.axieinfinity.com/gameplay/breeding>

## Breeding New Tokens

With the advent of **NFT-based gaming** has come the breedable NFT. For anyone that likes online gaming, and creating new characters, breedable NFTs represent a tremendous opportunity. Every platform has its own rules for breeding, so it is hard to make blanket statements about how breedable NFTs work as a whole.

For example, **Roaring Leaders**, a new NFT collection, allows the Roaring Leaders NFTs to be bred, and produce Roaring Leaders cubs in a separate collection. The platform also created a novel, **Tinder-style dating system** that allows Roaring Leaders owners to **look for a 'mate' if they don't have both a male and female NFT to breed.**

Like other breedable NFTs, Roaring Leaders require payment in tokens to be bred. For this platform, the $ROAR token is used to pay for breeding.

![](https://hackmd.io/_uploads/HJU-dTypq.png)

## Breeding NFTs today

Apart from gaming projects, other NFT projects are now including NFT breeding as one of the utilities buyers of their NFTs will get.

This is irrespective of whether the NFTs are photos, videos, etc. Some NFT projects which utilize NFT breeding are **Rolling Leaders, Samurai Doge, and Fat Ape Club.**

Although the whole concept of breeding NFTs is universal among different NFT projects, the detailed workings of how it's implemented depend on the particular end goal of the project.

In some NFT projects, the parent NFTs will **be burned,** i.e., destroyed after breeding. In some other projects, the parent NFTs will not be burned. Instead, parent NFTs will be \*\*prevented from breeding again \*\*for some time after breeding.

In the same vein, for some projects, the resultant NFT is a combination of the parent NFTs only; In others, the consequent NFT possesses some unique features from each parent and some random features of its own, making it even rarer. **With NFTs, the rarer the features, the more unique the NFT is. And with NFT breeding, resultant NFTs are even rarer.**

For collectors of NFTs in a project, breeding NFTs serve to provide benefits beyond the aesthetic pleasure obtained from collecting NFTs.

#### DeRace\[polygon]: NFT horse breeding with chainlink randomness:

![](https://hackmd.io/_uploads/BJj21Aypq.png)

Resource: <https://deracing.org/learn/horses/derace-horse-breeding/>

\***More Tinder-Style example:**

ex. Crypto Punk and BAYC holder:RichBabies

![](https://hackmd.io/_uploads/Hyh8ITyp9.jpg)

Resource: <https://opensea.io/collection/rich-baby>

## Benefits of Breeding NFTs

For NFT collectors, being a part of projects that utilize breeding NFTs provides two main benefits.

One is the opportunity to own rarer and unique NFTs. In the NFT community, **the rarer the features of an NFT, the more valuable it's perceived to be**. Because of this, when a collector breeds two NFTs and gets a rarer offspring, he receives an NFT that's even **more valuable** than the parents he originally bought.

The second benefit is that collectors will be able to earn passive income through breeding NFTs. **Collectors can routinely breed NFTs to create rarer offspring and sell them while holding the parent NFTs they originally invested in.**

## Most Popular Breeding NFT Projects

1. CryptoKitties: (ETH)
2. Axie Infinity: Two Axies breed to create new offspring with unique traits such as body parts, class, cards, etc. (Ronin; ETH side chain)
3. StepN (Solana)

## Case Study

Popular Solana project with nft breeding features:

### (1) Solchicks

Only Gen0 NFTs, being the genesis collection, will have overall rarities (Common, Uncommon, Rare, etc.).

There are certain restrictions to breeding. Each NFT will be able to breed a maximum of 7 times, referred to as Breeding Count which will be shown in the NFT’s metadata. **NFT breeding will also be subject to the Family Rule where NFTs with the same parents (either one of the parents) cannot breed with each other.** Furthermore, an NFT cannot breed with either of its parents. The Gen of the new NFT will always be one higher than that of the parent with the higher Gen.

That means that if breeding occurs between a Gen 1 and another Gen 1, the resulting NFT will be Gen 2, but if the breeding occurs between a Gen 1 and a Gen 3, the resulting NFT will be Gen 4.

In terms of the cost for breeding, as mentioned above both $CHICKS and $SHARDS will be required to breed new SolChicks NFTs, and our initial principle of the breeding cost structure is as described below for the near-term, but we may modify this in the future. Rest assured that any modification to the core cost structure to the breeding system will be made in a transparent manner and will not be made lightly.

The cost of breeding will scale according to the parents’ generation (Gen) and Breeding Count. To explain further: Breeding with Gen1 NFTs will be more expensive than breeding with Gen0 NFTs, breeding with Gen2 NFTs will be more expensive than breeding with Gen1 NFTs, and so on

Breeding with NFTs with 1 Breeding Count will be more expensive than breeding with 0 Breeding Count (i.e., the NFT has never been used for breeding), breeding with NFTs with 2 Breeding Count will be more expensive than breeding with 1 Breeding Count, and so on

![](https://hackmd.io/_uploads/rJ-Q8IT35.png)

Below is a table showing the required number of $CHICKS and $SHARDS for breeding for each parent NFT for different generations and remaining breed counts.

![](https://hackmd.io/_uploads/H1DzjoJaq.png)

The utility of $CHICKS and $SHARDS will be as follows. Both tokens will be required for:

* Breeding new SolChicks NFTs
* Levelling up SolChicks NFTs
* Upgrading equipment and weapon NFTs
* Purchasing game items

Reference: <https://www.solchicks.io/> <https://whitepaper.solchicks.io/nft-breeding>

### (2) Stonedapecrew:

\*Retreats (V1)

We released the **first-ever on-chain NFT evolution process** in December 2021.

In version 1 of the NFT Evolution, the retreats, Chimpions can move up in the metaverse by going on retreat.

There they go on a deep reflection phase and with a bit of luck come back with a role.

Two retreat options are available:

* Basic retreat for 333 $PUFF => 60% chance of getting a role
* Advanced DMT retreat for 666 $PUFF => features extra trips & chilled parties, therefore the chance of 80% for adopting a role.
* Ayahuasca Retreat for 1420 $PUFF => 100% chance of getting a role

![](https://hackmd.io/_uploads/SJrlV8T2q.png)

To go on a rescue mission:

Have 2 Apes with different roles or rent one in our recruiting system for 5.25 SOL

Have enough $PUFF (start 1780 $PUFF) every 100 rescued, it'll increase by 4.2%

reusing two apes increases the total cost by 60% current cost (without reusing) can be viewed on <https://www.stonedapecrew.com/rescue>

![](https://hackmd.io/_uploads/BkuWsnkaq.png)

![](https://hackmd.io/_uploads/HJWwohJaq.png)

Reference: <https://www.stonedapecrew.com/> <https://docs.stonedapecrew.com/puff/utilities#awakening-v2>

### (3) StepN (shoes-minting)

Shoe-Minting Event (SME) is when users use 2 Sneakers they own as a blueprint to “**breed”**, producing a Shoebox in the process. For reference, the 2 Sneakers will be called Vintages (Parents). Both Vintages need to be in the user’s possession (not under lease) and have full durability to begin an SME.

Users can then select a Sneaker, by heading to the **Mint** tab, choosing the Sneaker to “**breed**” with, and pressing **Mint** to proceed. The user will instantly receive a Shoebox that can be opened immediately.

#### \*Dynamic Minting Costs

Minting cost = GST (A) + base GMT (B) + additional GMT (\[A+B]\*x)

1. If GST < $4, x = 0%;
2. If $4 < GST < $8, x = 50%;
3. If $8 < GST < $12, x = 100%;
4. If $12 < GST < $16, x = 200%;
5. If $16 < GST < $20, x = 400%;
6. If $20 < GST < $30, x = 800%;
7. If $30 < GST < $40, x = 1600%;
8. If $40 < GST < $50, x = 3200%;
9. If GST > $50, x = 6400%.

<https://whitepaper.stepn.com/game-fi-elements/shoe-minting>

Here are some **video demo (**[**1**](https://drive.google.com/drive/folders/1YtFbJBODT_cu0JTA5myfCgCpOXQxV1Ll?usp=sharing)**,** [**2**](https://www.youtube.com/watch?v=RixaU0eGlqk)**) for nft breeding**

### Tokenomics (Extension)

#### Star Atlas

![](https://hackmd.io/_uploads/SkeAdYBp9.png) ![](https://hackmd.io/_uploads/HywR_tBpc.png)

resources: <https://staratlas.com/>

#### Genopets

![](https://hackmd.io/_uploads/Hybwpakp5.png)

resources: <https://www.genopets.me/>

## Overview of NFT-Breeding Program

#### Prerequisite: What is Metaplex Standard?

![](https://i.imgur.com/yeO8vDC.png)

> image from [Metaplex docs](https://docs.metaplex.com/programs/token-metadata/accounts)

![](https://hackmd.io/_uploads/H1djPylTq.png)

### 1. Initialize

![](https://hackmd.io/_uploads/ry604xgpq.png)

* Store metaplex NFT attributes on chain
* Breeding Metadata
  * **Hash**
  * **Generation**
  * **Name**
  * **Metaplex Mint**
  * ParentA
  * ParentB
  * ...
  * AttributeA
  * AttributeB
  * ...
* Initialization is **only** for genesis (Generation 0)
* Change Upgrade Authority of NFT to Breeding PDA signer so that the used NFT can be burnt by Breeding program

### 2. Compute New Data

![](https://hackmd.io/_uploads/S1k-rex6q.png)

* Customizable Breeding Logic with `compute` interface
* **Write attributes to `BreedingMeta` (PDA)**

### 3. Mint Child NFT

![](https://hackmd.io/_uploads/SJA7rlgpc.png)

* **Write Metaplex metadata (without URI)**
* Transfer upgrade authority to Breeding program PDA Signer
* Burn Parent NFTs(optional)
* Read attributes (off-chain)
* Upload image to web3 storage (off-chain)

### 4. Update URI of child NFT

![](https://hackmd.io/_uploads/B1ONSgl6q.png)

* Update URI in Metaplex metadata

## Reference

* <https://solmeet.dev>
* <https://book.solmeet.dev>
* <https://t.me/joinchat/oDHbq6gwtZVkNjU1>
* <https://mirror.xyz/iamwgg.eth/sza6Vi5VyGtMweoQn-pja\\_hX9hEbqCdyOoJhJzM2qGI>
* <https://medium.com/cryptokitties/getting-started-with-cryptokitties-part-two-buying-and-breeding-792502e54a4d>
* <https://riseangle.com/nft-magazine/what-is-breeding-nfts-how-it-began-and-its-impact-on-nft-projects-today>
* <https://hackernoon.com/an-intro-to-breedable-nfts-and-social-connections-in-the-metaverse>
* <https://www.esports.net/crypto/breeding-games/>
* <https://hackernoon.com/breeding-nfts-like-pokemons-your-tokens-can-evolve>
* [Tokenomics](https://hackmd.io/@Piercetw/HyhNcK1pq)


# #11 - BUIDL an Orderbook-based DEX on Solana in 2 hours

**Author:** [@harry830622](https://twitter.com/harry830622), [@ironaddicteddog](https://twitter.com/ironaddicteddog)

***\[Updated at 2022.08.25]***

> **See the example repo** [**here**](https://github.com/harry830622/simple-serum)

## TL; DR

DEX is an essential infrastructure for any DeFi ecosystem. Currently almost all exchanges in TradFi and CEXs are orderbook-based while major DEXs on Ethereum are AMMs due to some of Ethereum’s undesirable nature such as low TPS and high transaction fee, etc.

However, on Solana, a blockchain fundamentally more advanced than Ethereum, orderbook-based DEXs become much more practical.

In this session, we are going to BUIDL an orderbook-based DEX on Solana.

## What is Central Limit Order Book (CLOB)

From Wikipedia:

* A central limit order book (or **CLOB**) is a trading method used by most exchanges globally.
* It is a transparent system that matches customer orders (e.g. bids and offers) on a price time priority basis.
* The highest (best) bid order and the lowest (cheapest) offer order constitutes the best market or the touch in a given security or swap contract.
* Customers can routinely cross the bid/ask spread to effect immediate execution.

### CLOB vs AMM

|                           | CLOB                                                                                                                                                                                                       | AMM                                                                                                                                                                               |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Depth Chart               | <p><img src="https://hackmd.io/_uploads/Bke21K34ko.png" alt=""><br><a href="https://support.bullish.com/hc/en-us/articles/4554641116057-Reading-the-Depth-Chart#01G2QKAM4KHJBHNBW944B1NKWD">Source</a></p> | <p><img src="https://hackmd.io/_uploads/ByueYhVki.png" alt=""><br><a href="https://medium.com/hackernoon/depth-chart-and-its-significance-in-trading-bdbfbbd23d33">Source</a></p> |
| Price Discovery Mechanism | Match Engine                                                                                                                                                                                               | Constant Product Formula                                                                                                                                                          |
| Capital Efficiency        | **High**                                                                                                                                                                                                   | Low                                                                                                                                                                               |
| Composability             | Low                                                                                                                                                                                                        | **High**                                                                                                                                                                          |

## Serum: The Most Crucial Financial Infrastructure of Solana

![](https://hackmd.io/_uploads/ryjlB54Js.png)

* Serum DEX is the matching engine powering Solana-based financial projects
* Serum can power:
  * Spot Market
  * Derivatives Market (Zeta Markets / PsyOptions / off-piste / HXRO)
  * Liquidations (Jet Protocol / Tulip / Parrot)
  * Asset Management (Solrise / Nova Finance)
  * AMMs (Raydium / Atrix / Cyclos)
  * In-game NFT markets (Star Atlas / DeFi Land / Aurory / OpenEra)

### Dominance of Serum

![](https://hackmd.io/_uploads/BkcYUtNJo.png)

* It takes **\~30%** of instruction volume on Solana
* Some analytics:
  * [Program Instruction Volume](https://analytics.solscan.io/public/dashboard/8d888828-baae-47b9-948b-d087e5de1411?select_report_period=past7days)
  * [TVL](https://solscan.io/amm/serum)

### Ecosystem

![](https://hackmd.io/_uploads/BJ2KgT7Jj.png)

## Raydium: AMM Built on Top of CLOB

### Problem

* High gas fees
* Speed
* High slippage on large orders
* Fragmented liquidity

### Solution

From Raydium lightpaper:

> Unlike other AMM platforms, Raydium provides on-chain liquidity to a central limit order book, meaning that Raydium’s users and liquidity pools have access to the order flow and liquidity of the entire Serum ecosystem, and vice versa.

* Raydium is a pure market maker which takes the tokens locked in it to create a series of orders at different price points and sizes to provide liquidity.
* It creates orders using the constant product invariant. This equation has the special property that it is stateless and given any two tokens, without any information about their relative prices or value, it can provide “infinite” liquidity to traders.
* Raydium utilizes this equation and prices orders on the orderbook according to the Fibonacci sequence to provide up to 20 orders at a variety of prices.

## Components of Serum

* [Instruction Set (Program)](https://github.com/project-serum/serum-dex/blob/master/dex/src/instruction.rs#L327)
* [Instruction Set (Client)](https://github.com/project-serum/serum-ts/blob/master/packages/serum/src/instructions.js#L161)
* [Processor](https://github.com/project-serum/serum-dex/blob/master/dex/src/matching.rs)
* Main actions
  * PlaceOrder
  * MatchOrder
  * CancelOrder
  * SettleFund
  * ConsumeEvent

## Architecture

### Overview

![](https://hackmd.io/_uploads/SkFWNREks.png)

* Market is like a trading pair in CEX
  * Coin: Base Asset
  * PC(Primary Currency): Quote Asset
* Open Orders is like your account in CEX
* Vaults are the token accounts for the matching engine
* Bids/Asks compose an order book
* Request Queue is like a job queue
* Event Queue records the events such as orders filled, orders cancelled, etc.

### Place Order

![](https://hackmd.io/_uploads/S1wFrANyo.png)

* Side
  * Bid
  * Ask
* Order Type
  * Limit
  * Post Only
  * IOC
* Init an open orders if there is none
* Transfer funds to the vaults if the current deposit is not enough

### Match Order

![](https://hackmd.io/_uploads/Hk75rC4ks.png)

* Match the order with orders on the order book

### Cancel Order

![](https://hackmd.io/_uploads/Hy9or0EJj.png)

* Cancel the order placed before

### Settle Fund

![](https://hackmd.io/_uploads/Skz2r0NJo.png)

* Transfer funds out of the vaults to your own wallets

### Consume Event

![](https://hackmd.io/_uploads/Skt5rANyj.png)

* To know what happened in the DEX

## References

* [A technical introduction to the Serum DEX](https://docs.google.com/document/d/1isGJES4jzQutI0GtQGuqtrBUqeHxl_xJNXdtOv4SdII/view)
* <https://uniswap.org/blog/uniswap-v3-dominance#comparison-of-uniswap-v3-versus-centralized-exchanges>
* <https://docs.projectserum.com/>
* <https://projectserum.medium.com/serum-srm-and-the-ecosystem-part-1-2742f6a24597>
* <https://projectserum.medium.com/serum-srm-and-an-ecosystem-for-the-future-part-2-91398e75ce27> <https://projectserum.medium.com/calling-all-devs-serum-srm-and-an-ecosystem-for-the-future-part-3-ae7adbf4466e>
* <https://raydium.io/Raydium-Litepaper.pdf>


# #12 - A Complete Guide to Build a Simple Aggregator with Universal Rabbit Hole

**Author:** [@ironaddicteddog](https://twitter.com/ironaddicteddog), [@wei\_sol\_](https://twitter.com/wei_sol_), [@emersonliuuu](https://twitter.com/emersonliuuu)

***\[Updated at 2022.9.29]***

> **See the example repo** [**here**](https://github.com/DappioWonderland/universal-rabbit-hole-example)

## TL; DR

In this example, we will demonstrate **how to implement a simple aggregator** by using Universal Rabbit Hole, step by step.

#### Things you will be doing

* Instantiate DeFi instances in 1 line of code
* Switch to different DeFi protocol in 1 line of code
* Compose different DeFi protocols in less than 5 lines of code

#### Things you will NOT be doing

* You won't touch the details of various SDK
* You won't have to encode / decode the on-chain account data
* You won't have to figure out how to assemble the instruction

## Overview

**We want developers to use DeFi elements as Lego blocks.** Universal Rabbit Hole enables developers to utilize various DeFi elements, such as Pool, Farm, Vault, to compose more complex operations without handling enormous amount of client SDK and inconsistent interfaces.

> See [this guide](https://guide.dappio.xyz/the-universal-rabbit-hole) and [this Medium Post](https://medium.com/dappio-wonderland/the-solution-to-composability-universal-rabbit-hole-28b817cc0fd4) for more details

### Architecture

There are two modules in Universal Rabbit Hole: **Navigator** and **Gateway**:

![](https://hackmd.io/_uploads/rJbWYMd-o.jpg)

#### Navigator (Reader)

Navigator is a Typescript client for instantiating various of kinds of DeFi protocols. You can use it as a standalone dependency in your own project or together with [Dappio Gateway](https://guide.dappio.xyz/the-universal-rabbit-hole).

#### Gateway (Writer)

Gateway is a **CaaS (Compasaility-as-a-Service)** that standardizes inter-protocol interaction on Solana to unlock the potential of composability. It has an universal interface for various Solana DeFi elements (Pool / Farm / MoneyMarket / Vault / Leveraged Farm / ...) and serves as a **common knowledge base** that helps Solana community learn and improve.

### Anatomy of Gateway

![](https://hackmd.io/_uploads/Skbcueoyi.jpg)

Let's take a closer look on Gateway module. There are 4 different components in order to make Gateway function:

* **Builder**: Off-chain component that helps composing different DeFi actions
* **Protocol(s)**: Off-chain component that packages the specific instruction set of each protocol
* **Gateway**: On-chain program that receives and dispatches all the transactions, manages state, distributes fees
* **Adapter(s)**: On-chain program that connects base program and Gateway program

While **Builder** and **Protocols** are packaged into a npm module, **Gateway (program)** and **Adapters** are Solana programs deployed by Dappio (sometimes third-party) to be invoked to interact with Base programs

### Workflow of Gateway

![](https://hackmd.io/_uploads/Bkvs9tU1i.png)

We can separate these series of operations into 2 distinct sections: **off-chain part** and **on-chain part**.

#### Off-chain Part

* User assembles actions by invoking action setter and providing proper parameters
* Builder composes transactions

#### On-chain Part

* User sends transactions directly to Gateway program
* Gateway dispatches transactions to their corresponding adapters through CPI (cross-program invocation)
* Adapter invokes Base program through another CPI

### Supported Protocols

Following are the current supported protocols:

| Program            | ID                                             | Type                |
| ------------------ | ---------------------------------------------- | ------------------- |
| Gateway            | `GATEp6AEtXtwHABNWHKH9qeh3uJDZtZJ7YBNYzHsX3FS` | -                   |
| Adapter Raydium    | `ADPT1q4xG8F9m64cQyjqGe11cCXQq6vL4beY5hJavhQ5` | Pool / Farm         |
| Adapter Orca       | `ADPTTyNqameXftbqsxwXhbs7v7XP8E82YMaUStPgjmU5` | Pool / Farm         |
| Adapter Saber      | `ADPT4GbWTs9DXxo91YGBjNntYwLpXxn4gEbxfnUPfQoB` | Pool / Farm         |
| Adapter Lifinity   | `ADPTF4WmNPebELw6UvnSVBdL7BAqs5ceg9tyrHsQfrJK` | Pool                |
| Adapter Solend     | `ADPTCXAFfJFVqcw73B4PWRZQjMNo7Q3Yj4g7p4zTiZnQ` | MoneyMarket         |
| Adapter Francium   | `ADPTax5HwQ2ZWVLmceCek8UrqMhwCy5q3SHwi8W71Kv2` | MoneyMarket         |
| Adapter Larix      | `ADPTLQQ1Bwgybb2qge7QKSW7woDrhEjcLWG642qP2X4`  | MoneyMarket / Farm  |
| Adapter Tulip      | `ADPT9nhC1asRcEB13FKymLTatqWGCuZHDznGgnakWKxW` | MoneyMarket / Vault |
| Adapter Friktion   | `ADPTzbsaBdXA3FqXoPHjaTjPfh9kadxxFKxonZihP1Ji` | Vault               |
| Adapter Katana     | `ADPTwDKJTizC3V8gZXDxt5uLjJv4pBnh1nTTf9dZJnS2` | Vault               |
| Adapter NFTFinance | `ADPTyBr92sBCE1hdYBRvXbMpF4hKs17xyDjFPxopcsrh` | NFTFinance          |

## Get Started

Follow the [offical documentation](https://docs.dappio.xyz/implemtation-guide-for-protocol-developers/example-1) of Universal Rabbit Hole to get started

## References

* <https://guide.dappio.xyz/the-universal-rabbit-hole>
* <https://medium.com/dappio-wonderland/the-solution-to-composability-universal-rabbit-hole-28b817cc0fd4>


# #13 - Solana Pay in Practice: The Challenge and Solution

**Author:** [@akirawuc](https://twitter.com/akirawuc)

***\[Updated at 2022.10.20]***

> **See the example repo:**
>
> * [solana-pay-escrow](https://github.com/akirawuc/solana-pay-escrow)
> * [solana-pay-merchant-side](https://github.com/akirawuc/solana-pay-escrow)

## TL; DR

* What is Solana Pay?
* What is the current challenge?
* How to improve it by a Escrow program?

## Overview of Solana Pay

* What is Solana-pay? What can it do?
* What is the mechanism?
* How we can make it better?

### Quick Introduction

* What is Solana-pay?

> A standard protocol to encode Solana transaction requests within URLs to enable payments and other use cases. -- from their GitHub repo

### How Does It Work?

Basic version We've mentioned that Solana pay is a protocol for payment, where the structure is as follow:

```
solana:<recipient>
       ?amount=<amount>
       &spl-token=<spl-token>
       &reference=<reference>
       &label=<label>
       &message=<message>
       &memo=<memo>
```

Web app to mobile wallet ![](https://hackmd.io/_uploads/Skmk5QLbi.png)

Web app to browser wallet ![](https://hackmd.io/_uploads/HJbb5XIZo.png)

Mobile app to mobile wallet ![](https://hackmd.io/_uploads/SyDbcQI-o.png)

### Executing Details

1. clone the repo, and run the example code in solana-pay/point-of-sales

```
#shell
git clone git@github.com:solana-labs/solana-pay.git
cd solana-pay/point-of-sales
```

2. directly demo how it works, and open up the console to show that how a transaction is confirmed.

```
npm install
npm run dev
npm run proxy
open "https://localhost:3001?recipient=Hw7QWUA98q8jUAgbHLGJsXycPESt6jiyF4cPFjgg5JVc&label=akirawu"
```

3. Create a transfer request

## Current Challenge

### Mechanism

Once a payment (a transfer request) on Solana pay is created, it will generate an address called `reference`, in order to check whether the payment is fulfilled.

System from the merchant side will continously check if there're transaction under the `reference` address, and will stop the checking if and only if the user fulfilled the payment. However, since the 'reference' is just a random address, it didn't have any preventing mechanism for customers to fulfilled it again.

### Problem: Repeat fulfilled, can use the same QR code to deposit as many times as you like

Essentially, the spirit of payment is a trade of value, where customer pay for merchant's product. Once the customer paid for the payment, the merchant will need to take the responsibility to either send product to the customer or refund. However, it faces difficulties in practice.

To analyze, we can separate a payment into 3 conditions, which are

1. complete (fulfilled exactly once)
2. repeated fulfilled (fulfilled multiple times)
3. incomplete (never fulfilled)

where we only need to discuss about the last 2:

* If the payment is repeated fulfilled:
  * Merchant can't prevent, and even have difficulty to notice it. By the mechanism currently, merchant's system will stop checking for the transaction under the 'reference' address once the payment is fulfilled.
  * The only possibility that the merchant can notice could only be either their system won't stop checking transaction under the 'reference' address or by checking every order manually themselves.
* If the payment is never fulfilled:
  * First, the check of the payment will continue forever, until being turned off manually.
  * More importantly, if the order of the payment included a limited edition product, how can the merchant be able to handle that all manually?
    * if check quantity before payment create: how can they released those unpaid orders and prevent the customer to fulfilled it?
    * if check quantity after payment create: how to stop customers from paying after the product is sold out?

![](https://hackmd.io/_uploads/rJue7n8Nj.png)

## Our Proposal to Improve Solana Pay

1. Time: will need to check if the transaction have been pending for a while
2. One time deposit: only the first time deposit should be success.

**The protocol have some time limit, and more specific is better, one payment only accept one time deposit.** We can use the new feature called **Transaction Request** on Solana Pay recently:

```
solana:<url>
```

Instead of using transfer request to do the transfer, switch to use the transaction request.

### Architecture

#### Off-chain

![](https://hackmd.io/_uploads/rysB3dUVj.png)

![](https://hackmd.io/_uploads/rJYjghAXo.png)

#### On-chain

> Use the escrow program!

Check out: <https://book.solmeet.dev/notes/intro-to-anchor> With the repo: <https://github.com/ironaddicteddog/anchor-escrow>

Notice that paying can just be consider as a single side escrow, while the initializer part won't need to transfer token into the vault, only the taker (buyer here) need to.

1. Initialize ![](https://hackmd.io/_uploads/S1srkgk4o.png)
2. Pay ![](https://hackmd.io/_uploads/B1-_1lyNs.png)
3. Cancel ![](https://hackmd.io/_uploads/rJ3tyeyVj.png)

## References

* <https://solmeet.dev/>
* <https://book.solmeet.dev/notes/intro-to-anchor>
* <https://github.com/ironaddicteddog/anchor-escrow>
* <https://github.com/solana-labs/solana-pay>
* <https://docs.solanapay.com/>
* <https://www.anchor-lang.com/>
* <https://coral-xyz.github.io/anchor/ts/index.html>
* <https://solana-labs.github.io/solana-program-library/token/js/>


# #14 - A Complete Guide to Implement an Adapter on Universal Rabbit Hole

**Author:** [@ironaddicteddog](https://twitter.com/ironaddicteddog), [@wei\_sol\_](https://twitter.com/wei_sol_), [@emersonliuuu](https://twitter.com/emersonliuuu)

***\[Updated at 2022.11.24]***

* **Check our Public Beta** [**here**](https://app.dappio.xyz)
* Uiversal Rabbit Hole
  * [Navigator](https://github.com/DappioWonderland/navigator)
  * [Gateway](https://github.com/DappioWonderland/gateway)
  * [Adapter Programs](https://github.com/DappioWonderland/adapter-programs)
* The PR of the Genopets Integration
  * [Navigator PR](https://github.com/DappioWonderland/navigator/pull/112)
  * [Gateway PR](https://github.com/DappioWonderland/gateway/pull/15)
  * [Adapter Programs PR](https://github.com/DappioWonderland/adapter-programs/pull/17)

## TL; DR

* What is Universal Rabbit Hole?
* Why is it necessary?
* How does it work?
* Demonstrate how to implement an adapter
  * Use Genopets farm as an example

## Overview

* Motivation
  * Open up the potential of composability
  * Develop higher level DeFi strategy without touching the implementation details
  * Attract more devs to build on top of your protocol
  * **NO NEED to re-invent the wheel (SDK)**
* Overview of Universal Rabbit Hole
  * Read part
    * Navigator
  * Writhe part
    * Gateway Program
    * Gateway Client
    * Adapter Program

#### Without URH

![](https://hackmd.io/_uploads/HykS0teHi.png)

* **Protocols are not composable**
* User has to deal with new SDK when trying to integrate new protocol

#### With URH

![](https://hackmd.io/_uploads/SJzgZC-Hj.png)

* **Protocols are composable**
* User only has to deal with builder (Gateway client) and Gateway program

### Workflow

![](https://hackmd.io/_uploads/rkZ6gRbSo.png)

### What is Genopets Staking

* Interfaces
  * Stake
  * Unstake
  * Harvest (With lockup period)

## Get Started

Follow the [offical documentation](https://docs.dappio.xyz/implemtation-guide-for-protocol-developers/example-2) of Universal Rabbit Hole to get started

## References

* <https://app.dappio.xyz>
* <https://github.com/DappioWonderland/navigator>
* <https://github.com/DappioWonderland/gateway>
* <https://github.com/DappioWonderland/adapter-programs>
* <https://guide.dappio.xyz/the-universal-rabbit-hole>
* <https://github.com/genopets-solana>


# #15 - A Complete Guide to Mint Solana NFTs through a Mobile App (Android)

Author: [@rockluckycat](https://twitter.com/RockLuckyCat)

> See the example repo [here](https://github.com/BoxInThePARK/mobile-nft-mint-example)

## Overview

* Creat candy machine through Metaplex js
* Creat a Pixel 4 Android virtual device
* Run react-native app on Android Emulator
  * Connect wallet through [@solana/mobile-wallet-adapter](https://github.com/solana-mobile/mobile-wallet-adapter)
  * Upload image and metadata to Arweave
  * Mint Solana NFT through candy machine

> The demo procedure is running on solana devnet

## Setup

### Structure

```
├── 📂 metaplex-candy-machine-example
│   │
│   └── 📄 creator.js
│
└── 📂 mobile-nft-mint-example
    │
    ├── 📂 andorid
    │
    ├── 📂 ios
    │
    ├── 📂 patch
    │   |
    │   └── 📂 arweave
    │
    ├── 📂 src
    │   │
    │   ├── 📂 components
    │   │
    │   ├── 📂 hooks
    │   │   │
    │   │   ├── 📄 useAuthorization.ts
    │   │   │
    │   │   ├── 📄 useGuardedCallback.ts
    │   │   │
    │   │   └── 📄 useUploader.ts
    │   │   │
    │   │   └── 📄 useMinter.ts
    │   │
    │   ├── 📄 App.tsx
    │   |
    │   └── 📄 MainScreen.tsx
    │
    └── 📂 types
```

### This Tutorial Only Works on Android OS

Since Solana Mobile SDK hasn't support iOS yet. You can't connect wallet with [@solana/mobile-wallet-adapter](https://github.com/solana-mobile/mobile-wallet-adapter) on iOS devices. As a result, this tutorial only works on Android right now.

### Setting up the development environment

There are two ways to develop React Native App.

* Expo Go
* React Native CLI

We don't talk about which one is better here. The point is I have tried to run this code with Expo Go. However it still has some issues when using [@solana/mobile-wallet-adapter](https://github.com/solana-mobile/mobile-wallet-adapter), but it works fine with React Native CLI.

**So I suggest you to use React Native CLI method to setting up your development environment.**

Follow this [doc](https://reactnative.dev/docs/environment-setup) to setup.

### Creat a Pixel 4 Android virtual device

Follow this [doc](https://developer.android.com/studio/run/managing-avds) to create a virtual device.

And remeber select Pixel 4 which is the newest verison has Play Store inside.

![Select Pixel 4](https://i.imgur.com/ozNIWz2.png)

System Image please select "S API Level 31"

![Select S API Level 31](https://i.imgur.com/Pd27QED.png)

After you run up your virtual Pixel 4, remember to install Phantom or Solflare.

### Install `metaplex-candy-machine-example`

* <https://github.com/BoxInThePARK/metaplex-candy-machine-example>

```bash
$ git clone https://github.com/BoxInThePARK/metaplex-candy-machine-example.git

$ cd metaplex-candy-machine-example
$ pnpm install
```

### Install `mobile-nft-mint-example`

* <https://github.com/BoxInThePARK/mobile-nft-mint-example>

```bash
$ git clone https://github.com/BoxInThePARK/mobile-nft-mint-example.git

$ cd mobile-nft-mint-example
$ yarn
```

* Set Arweave Package to Local Patch

  Right now, `arweave-js` hasn't completely supported react-native yet. Therefore, if you want to upload image or metadata to Arweave network through `arweave-js` package. You will address some issues occured by some needed packages are unable to resolve on react-native. Because react-native doesn't have them.

  To solve these problems, not only you should install other packages, you also need to do small modification on the package's sourcecode. This is the reason why we set `arweave-js` as a local patch,

  Here are the steps:

  * Install

    ```bash
        yarn add text-encoding
    ```
  * Modify source code

    ```javascript
    // In patch/arweave/node/lib/utils.js

    ...

    // Line 61
    const {TextEncoder} = require('text-encoding');

    ```

### Setup Arweave Wallet

Follow this [doc](https://docs.arweave.org/info/wallets/arweave-web-extension-wallet) to setup your Arweave wallet and claim free AR token by completing assigned [task](https://faucet.arweave.net/). **You should have a downloaded key file after the setup.** We will need the keyfile in the rest of the tutorial.

## Part 1: Create Candy Machine

### Create `.env` File

```Bash
//In metaplex-candy-machine-example

touch .env
```

Paste this to .env

```env
//In .env

ARWEAVE_KEY=[The key you get from "Set Arweave Wallet"]
METAPLEX_PRIVATE_KEY=[The secret key of your test wallet address]
```

### Create A New Candy Machine

```Bash
$ pnpm create-candy-machine

> metaplex-candy-machine-example@0.0.0 create-candy-machine ../metaplex-candy-machine-example
> node ./creator.js

publicKey [your wallet address]
Upload Collection Metadata
metadataUrl https://arweave.net/xxxxxxxxx
Initialize Metaplex
Create the Collection NFT
Create the Candy Machine
Done
candyMachine_address [new cm address]
```

Then you can get a new candy machine address.

## Part 2: Run The App on Android Emulator/Device

### Setup Arweave Wallet

Follow this [doc](https://docs.arweave.org/info/wallets/arweave-web-extension-wallet) to setup your Arweave wallet and claim free AR token by completing assigned task.

### Create `.env` File

```Bash
//In mobile-nft-mint-example

touch .env
```

Paste this to .env

```env
//In .env

REACT_APP_ARWEAVE_KEY=[The key you get from "Set Arweave Wallet"]
REACT_APP_METAPLEX_PRIVATE_KEY=[The secret key of your test wallet address]
REACT_APP_CANDY_MACHINE_ADDRESS=[The candy machine address you get from Part 1]
```

### Install App to Emulator

Recommend to have two terminal windows here.

Run the Metro

```Bash
$ yarn start --reset-cache
```

Install App

```Bash
$ yarn android
```

### Connect Wallet

> Set Wallet to devnet

#### Add uri in APP\_IDENTITY

```typescript
// In src/hooks/useAuthorization.ts

...

// Line 66
uri: 'https://book.solmeet.dev/',

```

#### Refresh app or rebuild. Then start testing!

![](https://i.imgur.com/ojVQjSu.png)

![](https://i.imgur.com/CdC5Nyv.png)

### Upload Image and Metadata

The procedure comes from SolMeet #3. You can take a detail look at [here](https://book.solmeet.dev/notes/complete-guide-to-mint-solana-nft#part-2-upload-to-arweave)

### Find Candy Machine

```typescript
//Initialize Metaplex
console.log('Initialize Metaplex');
const metaplex = Metaplex.make(connection).use(
keypairIdentity(metapleKeypair),
);
const treasury = metaplex.identity().publicKey;

//Find Candy Machine with Address
console.log('Fetch the Candy Machine');
let candyMachine = await metaplex.candyMachines().findByAddress({
address: new PublicKey(REACT_APP_CANDY_MACHINE_ADDRESS),
});

console.log('Update the Candy Machine');
await metaplex.candyMachines().update({
candyMachine,
guards: {
  botTax: {lamports: sol(0.01), lastInstruction: true},
  solPayment: {amount: sol(0.1), destination: treasury},
  startDate: {date: toDateTime('2022-10-17T16:00:00Z')},
  // All other guards are disabled...
},
});
```

### Mint NFT

```typescript
//Insert Item and Refresh Candy Machine
console.log('Insert Item to the Candy Machine');
await metaplex.candyMachines().insertItems({
candyMachine,
items: [{name: metaData.name, uri: metaData.id}],
});

candyMachine = await metaplex.candyMachines().refresh(candyMachine);

//Mint
console.log('Mint');
const {nft} = await metaplex.candyMachines().mint({
candyMachine,
collectionUpdateAuthority: metapleKeypair.publicKey,
owner: selectedAccount.publicKey,
});
```

### Find NFT in Your Wallet

![](https://i.imgur.com/2lkDNMg.jpg)

![](https://i.imgur.com/ZO0luKs.png)

## Reference

### General

* [SolMeet #3](https://book.solmeet.dev/notes/complete-guide-to-mint-solana-nft#setup-arweave-wallet)
* <https://reactnative.dev/docs>

### SMS

* <https://github.com/solana-mobile/mobile-wallet-adapter>
* <https://github.com/solana-mobile/mobile-wallet-adapter/tree/main/examples/example-react-native-app>

### Metaplex

* <https://github.com/metaplex-foundation/js-examples/tree/main/mint-ui-example>
* <https://docs.metaplex.com/programs/token-metadata/overview>

### Arweave

* <https://github.com/thuglabs/arweave-image-uploader>


