瀏覽代碼

add tokens/token-swap/steel (#267)

Leo Phạm 9 月之前
父節點
當前提交
5bdafb467a

+ 23 - 0
tokens/token-swap/steel/Cargo.toml

@@ -0,0 +1,23 @@
+[workspace]
+resolver = "2"
+members = ["api", "program"]
+
+[workspace.package]
+version = "0.1.0"
+edition = "2021"
+license = "Apache-2.0"
+homepage = ""
+documentation = ""
+repository = ""
+readme = "./README.md"
+keywords = ["solana"]
+
+[workspace.dependencies]
+token-swap-api = { path = "./api", version = "0.1.0" }
+bytemuck = "1.14"
+num_enum = "0.7"
+solana-program = "1.18"
+steel = { version = "2.0", features = ["spl"] }
+thiserror = "1.0"
+spl-token = "^4"
+spl-math = { version = "0.3.0", features = ["no-entrypoint"] }

+ 45 - 0
tokens/token-swap/steel/README.md

@@ -0,0 +1,45 @@
+# Token swap example amm in Steel
+
+**TokenSwap** - Your Gateway to Effortless Trading! Welcome to the world of Automated Market Makers (AMM), where seamless trading is made possible with the power of automation. The primary goal of AMMs is to act as automatic buyers and sellers, readily available whenever users wish to trade their assets.
+        
+## API
+- [`Consts`](api/src/consts.rs) – Program constants.
+- [`Error`](api/src/error.rs) – Custom program errors.
+- [`Instruction`](api/src/instruction.rs) – Declared instructions.
+
+## Instructions
+- [`CreateAmm`](program/src/create_amm.rs) – Create amm ...
+- [`CreatePool`](program/src/create_pool.rs) – Create liquidity pool
+- [`DepositLiquidity`](program/src/deposit_liquidity.rs) – Desposit liquidity to pool
+- [`WithdrawLiquidity`](program/src/withdraw_liquidity.rs) – Withdraw liquidity from pool
+- [`Swap`](program/src/swap.rs) – Swap exact token amount
+
+## State
+- [`Amm`](api/src/state/amm.rs) – Amm state
+- [`Pool`](api/src/state/pool.rs) – Pool state
+
+## How to run
+
+Compile your program:
+
+```sh
+pnpm build
+```
+
+Run unit and integration tests:
+
+```sh
+pnpm test
+```
+
+Run build and test
+
+```sh
+pnpm build-and-test
+```
+
+Deploy your program:
+
+```sh
+pnpm deploy
+```

+ 20 - 0
tokens/token-swap/steel/api/Cargo.toml

@@ -0,0 +1,20 @@
+[package]
+name = "token-swap-api"
+description = "API for interacting with the TokenSwap program"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+homepage.workspace = true
+documentation.workspace = true
+repository.workspace = true
+readme.workspace = true
+keywords.workspace = true
+
+[dependencies]
+bytemuck.workspace = true
+num_enum.workspace = true
+solana-program.workspace = true
+steel.workspace = true
+thiserror.workspace = true
+spl-token.workspace = true
+spl-math.workspace = true

+ 11 - 0
tokens/token-swap/steel/api/src/consts.rs

@@ -0,0 +1,11 @@
+use solana_program::pubkey;
+use steel::Pubkey;
+
+pub const MINIMUM_LIQUIDITY: u64 = 100;
+
+pub const AUTHORITY_SEED: &[u8] = b"authority";
+
+pub const LIQUIDITY_SEED: &[u8] = b"liquidity";
+
+pub const ASSOCIATED_TOKEN_PROGRAM_ID: Pubkey =
+    pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");

+ 20 - 0
tokens/token-swap/steel/api/src/error.rs

@@ -0,0 +1,20 @@
+use steel::*;
+
+#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)]
+#[repr(u32)]
+pub enum TokenSwapError {
+    #[error("Invalid fee, must be between 0 and 10000")]
+    InvalidFee = 0,
+    #[error("Account is not existed")]
+    AccountIsNotExisted = 1,
+    #[error("Invalid account")]
+    InvalidAccount = 2,
+    #[error("Deposit too small")]
+    DepositTooSmall = 3,
+    #[error("Withdrawal too small")]
+    OutputTooSmall,
+    #[error("Invariant violated")]
+    InvariantViolated,
+}
+
+error!(TokenSwapError);

+ 49 - 0
tokens/token-swap/steel/api/src/instruction.rs

@@ -0,0 +1,49 @@
+use steel::*;
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
+pub enum TokenSwapInstruction {
+    CreateAmm = 0,
+    CreatePool = 1,
+    DepositLiquidity = 2,
+    WithdrawLiquidity = 3,
+    Swap = 4,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Pod, Zeroable)]
+pub struct CreateAmm {
+    pub id: Pubkey,
+    pub fee: [u8; 2],
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Pod, Zeroable)]
+pub struct CreatePool {}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Pod, Zeroable)]
+pub struct DepositLiquidity {
+    pub amount_a: [u8; 8],
+    pub amount_b: [u8; 8],
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Pod, Zeroable)]
+pub struct WithdrawLiquidity {
+    pub amount: [u8; 8],
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Pod, Zeroable)]
+pub struct Swap {
+    pub swap_a: u8,
+    pub input_amount: [u8; 8],
+    pub min_output_amount: [u8; 8],
+}
+
+instruction!(TokenSwapInstruction, CreateAmm);
+instruction!(TokenSwapInstruction, CreatePool);
+instruction!(TokenSwapInstruction, DepositLiquidity);
+instruction!(TokenSwapInstruction, WithdrawLiquidity);
+instruction!(TokenSwapInstruction, Swap);

+ 18 - 0
tokens/token-swap/steel/api/src/lib.rs

@@ -0,0 +1,18 @@
+pub mod consts;
+pub mod error;
+pub mod instruction;
+pub mod sdk;
+pub mod state;
+
+pub mod prelude {
+    pub use crate::consts::*;
+    pub use crate::error::*;
+    pub use crate::instruction::*;
+    pub use crate::sdk::*;
+    pub use crate::state::*; 
+}
+
+use steel::*;
+
+// TODO Set program id
+declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35"); 

+ 277 - 0
tokens/token-swap/steel/api/src/sdk.rs

@@ -0,0 +1,277 @@
+use steel::*;
+
+use crate::prelude::*;
+
+pub fn create_amm(payer: Pubkey, admin: Pubkey, id: Pubkey, fee: u16) -> Instruction {
+    Instruction {
+        program_id: crate::ID,
+        accounts: vec![
+            AccountMeta::new(payer, true),
+            AccountMeta::new_readonly(admin, false),
+            AccountMeta::new(amm_pda(id).0, false),
+            AccountMeta::new_readonly(system_program::ID, false),
+        ],
+        data: CreateAmm {
+            id,
+            fee: fee.to_le_bytes(),
+        }
+        .to_bytes(),
+    }
+}
+
+pub fn create_pool(payer: Pubkey, amm: Pubkey, mint_a: Pubkey, mint_b: Pubkey) -> Instruction {
+    let pool_authority = pool_authority_pda(amm, mint_a, mint_b).0;
+    let (pool_account_a, _) = Pubkey::find_program_address(
+        &[
+            pool_authority.as_ref(),
+            spl_token::ID.as_ref(),
+            mint_a.as_ref(),
+        ],
+        &spl_token::ID,
+    );
+
+    let (pool_account_b, _) = Pubkey::find_program_address(
+        &[
+            pool_authority.as_ref(),
+            spl_token::ID.as_ref(),
+            mint_b.as_ref(),
+        ],
+        &spl_token::ID,
+    );
+
+    Instruction {
+        program_id: crate::ID,
+        accounts: vec![
+            AccountMeta::new(payer, true),
+            AccountMeta::new_readonly(amm, false),
+            AccountMeta::new(pool_pda(amm, mint_a, mint_b).0, false),
+            AccountMeta::new_readonly(pool_authority, false),
+            AccountMeta::new(mint_liquidity_pda(amm, mint_a, mint_b).0, false),
+            AccountMeta::new(mint_a, false),
+            AccountMeta::new(mint_b, false),
+            AccountMeta::new(pool_account_a, false),
+            AccountMeta::new(pool_account_b, false),
+            AccountMeta::new_readonly(spl_token::ID, false),
+            AccountMeta::new_readonly(system_program::ID, false),
+            AccountMeta::new_readonly(ASSOCIATED_TOKEN_PROGRAM_ID, false),
+            AccountMeta::new_readonly(sysvar::rent::ID, false),
+        ],
+        data: CreatePool {}.to_bytes(),
+    }
+}
+
+pub fn deposit_liquidity(
+    payer: Pubkey,
+    depositor: Pubkey,
+    pool: Pubkey,
+    pool_authority: Pubkey,
+    amm: Pubkey,
+    mint_a: Pubkey,
+    mint_b: Pubkey,
+    amount_a: u64,
+    amount_b: u64,
+) -> Instruction {
+    let (pool_account_a, _) = Pubkey::find_program_address(
+        &[
+            pool_authority.as_ref(),
+            spl_token::ID.as_ref(),
+            mint_a.as_ref(),
+        ],
+        &spl_token::ID,
+    );
+
+    let (pool_account_b, _) = Pubkey::find_program_address(
+        &[
+            pool_authority.as_ref(),
+            spl_token::ID.as_ref(),
+            mint_b.as_ref(),
+        ],
+        &spl_token::ID,
+    );
+
+    let mint_liquidity = mint_liquidity_pda(amm, mint_a, mint_b).0;
+
+    let (depositor_account_liquidity, _) = Pubkey::find_program_address(
+        &[
+            depositor.as_ref(),
+            spl_token::ID.as_ref(),
+            mint_liquidity.as_ref(),
+        ],
+        &spl_token::ID,
+    );
+
+    let (depositor_account_a, _) = Pubkey::find_program_address(
+        &[depositor.as_ref(), spl_token::ID.as_ref(), mint_a.as_ref()],
+        &spl_token::ID,
+    );
+
+    let (depositor_account_b, _) = Pubkey::find_program_address(
+        &[depositor.as_ref(), spl_token::ID.as_ref(), mint_b.as_ref()],
+        &spl_token::ID,
+    );
+    Instruction {
+        program_id: crate::ID,
+        accounts: vec![
+            AccountMeta::new(payer, true),
+            AccountMeta::new(depositor, true),
+            AccountMeta::new_readonly(pool, false),
+            AccountMeta::new_readonly(pool_authority, false),
+            AccountMeta::new(mint_liquidity, false),
+            AccountMeta::new(mint_a, false),
+            AccountMeta::new(mint_b, false),
+            AccountMeta::new(pool_account_a, false),
+            AccountMeta::new(pool_account_b, false),
+            AccountMeta::new(depositor_account_liquidity, false),
+            AccountMeta::new(depositor_account_a, false),
+            AccountMeta::new(depositor_account_b, false),
+            AccountMeta::new_readonly(spl_token::ID, false),
+            AccountMeta::new_readonly(system_program::ID, false),
+            AccountMeta::new_readonly(ASSOCIATED_TOKEN_PROGRAM_ID, false),
+        ],
+        data: DepositLiquidity {
+            amount_a: amount_a.to_le_bytes(),
+            amount_b: amount_b.to_le_bytes(),
+        }
+        .to_bytes(),
+    }
+}
+
+pub fn withdraw_liquidity(
+    payer: Pubkey,
+    depositor: Pubkey,
+    pool: Pubkey,
+    pool_authority: Pubkey,
+    amm: Pubkey,
+    mint_a: Pubkey,
+    mint_b: Pubkey,
+    amount: u64,
+) -> Instruction {
+    let (pool_account_a, _) = Pubkey::find_program_address(
+        &[
+            pool_authority.as_ref(),
+            spl_token::ID.as_ref(),
+            mint_a.as_ref(),
+        ],
+        &spl_token::ID,
+    );
+
+    let (pool_account_b, _) = Pubkey::find_program_address(
+        &[
+            pool_authority.as_ref(),
+            spl_token::ID.as_ref(),
+            mint_b.as_ref(),
+        ],
+        &spl_token::ID,
+    );
+
+    let mint_liquidity = mint_liquidity_pda(amm, mint_a, mint_b).0;
+
+    let (depositor_account_liquidity, _) = Pubkey::find_program_address(
+        &[
+            depositor.as_ref(),
+            spl_token::ID.as_ref(),
+            mint_liquidity.as_ref(),
+        ],
+        &spl_token::ID,
+    );
+
+    let (depositor_account_a, _) = Pubkey::find_program_address(
+        &[depositor.as_ref(), spl_token::ID.as_ref(), mint_a.as_ref()],
+        &spl_token::ID,
+    );
+
+    let (depositor_account_b, _) = Pubkey::find_program_address(
+        &[depositor.as_ref(), spl_token::ID.as_ref(), mint_b.as_ref()],
+        &spl_token::ID,
+    );
+    Instruction {
+        program_id: crate::ID,
+        accounts: vec![
+            AccountMeta::new(payer, true),
+            AccountMeta::new(depositor, true),
+            AccountMeta::new_readonly(pool, false),
+            AccountMeta::new_readonly(pool_authority, false),
+            AccountMeta::new(mint_liquidity, false),
+            AccountMeta::new(mint_a, false),
+            AccountMeta::new(mint_b, false),
+            AccountMeta::new(pool_account_a, false),
+            AccountMeta::new(pool_account_b, false),
+            AccountMeta::new(depositor_account_liquidity, false),
+            AccountMeta::new(depositor_account_a, false),
+            AccountMeta::new(depositor_account_b, false),
+            AccountMeta::new_readonly(spl_token::ID, false),
+            AccountMeta::new_readonly(system_program::ID, false),
+            AccountMeta::new_readonly(ASSOCIATED_TOKEN_PROGRAM_ID, false),
+        ],
+        data: WithdrawLiquidity {
+            amount: amount.to_le_bytes(),
+        }
+        .to_bytes(),
+    }
+}
+
+pub fn swap(
+    payer: Pubkey,
+    trader: Pubkey,
+    pool: Pubkey,
+    pool_authority: Pubkey,
+    amm: Pubkey,
+    mint_a: Pubkey,
+    mint_b: Pubkey,
+    swap_a: bool,
+    input_amount: u64,
+    min_output_amount: u64,
+) -> Instruction {
+    let (pool_account_a, _) = Pubkey::find_program_address(
+        &[
+            pool_authority.as_ref(),
+            spl_token::ID.as_ref(),
+            mint_a.as_ref(),
+        ],
+        &spl_token::ID,
+    );
+
+    let (pool_account_b, _) = Pubkey::find_program_address(
+        &[
+            pool_authority.as_ref(),
+            spl_token::ID.as_ref(),
+            mint_b.as_ref(),
+        ],
+        &spl_token::ID,
+    );
+
+    let (trader_account_a, _) = Pubkey::find_program_address(
+        &[trader.as_ref(), spl_token::ID.as_ref(), mint_a.as_ref()],
+        &spl_token::ID,
+    );
+
+    let (trader_account_b, _) = Pubkey::find_program_address(
+        &[trader.as_ref(), spl_token::ID.as_ref(), mint_b.as_ref()],
+        &spl_token::ID,
+    );
+    Instruction {
+        program_id: crate::ID,
+        accounts: vec![
+            AccountMeta::new(payer, true),
+            AccountMeta::new(trader, true),
+            AccountMeta::new_readonly(amm, false),
+            AccountMeta::new_readonly(pool, false),
+            AccountMeta::new_readonly(pool_authority, false),
+            AccountMeta::new(mint_a, false),
+            AccountMeta::new(mint_b, false),
+            AccountMeta::new(pool_account_a, false),
+            AccountMeta::new(pool_account_b, false),
+            AccountMeta::new(trader_account_a, false),
+            AccountMeta::new(trader_account_b, false),
+            AccountMeta::new_readonly(spl_token::ID, false),
+            AccountMeta::new_readonly(system_program::ID, false),
+            AccountMeta::new_readonly(ASSOCIATED_TOKEN_PROGRAM_ID, false),
+        ],
+        data: Swap {
+            swap_a: swap_a as u8,
+            input_amount: input_amount.to_le_bytes(),
+            min_output_amount: min_output_amount.to_le_bytes(),
+        }
+        .to_bytes(),
+    }
+}

+ 23 - 0
tokens/token-swap/steel/api/src/state/amm.rs

@@ -0,0 +1,23 @@
+use steel::*;
+
+use super::TokenSwapAccount;
+
+/// Fetch PDA of the amm account.
+pub fn amm_pda(id: Pubkey) -> (Pubkey, u8) {
+    Pubkey::find_program_address(&[id.as_ref()], &crate::id())
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
+pub struct Amm {
+    /// The primary key of the AMM
+    pub id: Pubkey,
+
+    /// Account that has admin authority over the AMM
+    pub admin: Pubkey,
+
+    /// The LP fee taken on each trade, in basis points
+    pub fee: [u8; 2],
+}
+
+account!(TokenSwapAccount, Amm);

+ 13 - 0
tokens/token-swap/steel/api/src/state/mod.rs

@@ -0,0 +1,13 @@
+mod amm;
+mod pool;
+pub use amm::*;
+pub use pool::*;
+
+use steel::*;
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)]
+pub enum TokenSwapAccount {
+    Amm = 0,
+    Pool = 1,
+}

+ 116 - 0
tokens/token-swap/steel/api/src/state/pool.rs

@@ -0,0 +1,116 @@
+use steel::*;
+
+use crate::{
+    consts::{AUTHORITY_SEED, LIQUIDITY_SEED},
+    error::TokenSwapError,
+};
+
+use super::TokenSwapAccount;
+
+/// Fetch PDA of the pool account.
+pub fn pool_pda(amm: Pubkey, mint_a: Pubkey, mint_b: Pubkey) -> (Pubkey, u8) {
+    Pubkey::find_program_address(
+        &[amm.as_ref(), mint_a.as_ref(), mint_b.as_ref()],
+        &crate::id(),
+    )
+}
+
+pub fn validate_pool_account(pool: &AccountInfo, mint_a: Pubkey, mint_b: Pubkey) -> ProgramResult {
+    let pool_info_data = pool.as_account::<Pool>(&crate::id())?;
+    pool.has_owner(&crate::id())?.has_seeds(
+        &[
+            pool_info_data.amm.as_ref(),
+            pool_info_data.mint_a.as_ref(),
+            pool_info_data.mint_b.as_ref(),
+        ],
+        &crate::id(),
+    )?;
+
+    if pool_info_data.mint_a != mint_a || pool_info_data.mint_b != mint_b {
+        return Err(TokenSwapError::InvalidAccount.into());
+    }
+
+    Ok(())
+}
+
+pub fn pool_authority_pda(amm: Pubkey, mint_a: Pubkey, mint_b: Pubkey) -> (Pubkey, u8) {
+    Pubkey::find_program_address(
+        &[
+            amm.as_ref(),
+            mint_a.as_ref(),
+            mint_b.as_ref(),
+            AUTHORITY_SEED,
+        ],
+        &crate::id(),
+    )
+}
+
+pub fn validate_pool_authority(
+    pool: &Pool,
+    pool_authority: &AccountInfo,
+    mint_a: Pubkey,
+    mint_b: Pubkey,
+) -> ProgramResult {
+    pool_authority.has_seeds(
+        &[
+            pool.amm.as_ref(),
+            mint_a.as_ref(),
+            mint_b.as_ref(),
+            AUTHORITY_SEED,
+        ],
+        &crate::id(),
+    )?;
+
+    Ok(())
+}
+
+pub fn mint_liquidity_pda(amm: Pubkey, mint_a: Pubkey, mint_b: Pubkey) -> (Pubkey, u8) {
+    Pubkey::find_program_address(
+        &[
+            amm.as_ref(),
+            mint_a.as_ref(),
+            mint_b.as_ref(),
+            LIQUIDITY_SEED,
+        ],
+        &crate::id(),
+    )
+}
+
+pub fn validate_mint_liquidity(
+    pool: &Pool,
+    mint_liquidity: &AccountInfo,
+    mint_a: Pubkey,
+    mint_b: Pubkey,
+) -> ProgramResult {
+    mint_liquidity
+        .is_writable()?
+        .has_seeds(
+            &[
+                pool.amm.as_ref(),
+                mint_a.as_ref(),
+                mint_b.as_ref(),
+                LIQUIDITY_SEED,
+            ],
+            &crate::id(),
+        )?
+        .as_mint()?;
+
+    Ok(())
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
+pub struct Pool {
+    /// Primary key of the AMM
+    pub amm: Pubkey,
+
+    /// Mint of token A
+    pub mint_a: Pubkey,
+
+    /// Mint of token B
+    pub mint_b: Pubkey,
+
+    pub pool_authority_bump: u8,
+}
+
+account!(TokenSwapAccount, Pool);

+ 31 - 0
tokens/token-swap/steel/package.json

@@ -0,0 +1,31 @@
+{
+  "name": "token-swap",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "pnpm ts-mocha -p ./tsconfig.json -t 1000000 ./tests/*.test.ts",
+    "build-and-test": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./tests/fixtures && pnpm test",
+    "build": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./program/target/so",
+    "deploy": "solana program deploy ./program/target/so/account_data_program.so"
+  },
+  "keywords": [],
+  "author": "Leo Pham <hongthaipro@gmail.com>",
+  "license": "ISC",
+  "dependencies": {
+    "@solana/spl-token": "^0.4.9",
+    "@solana/web3.js": "^1.95.4",
+    "bs58": "^6.0.0"
+  },
+  "devDependencies": {
+    "@types/chai": "^4.3.7",
+    "@types/mocha": "^10.0.9",
+    "@types/node": "^22.7.9",
+    "borsh": "^2.0.0",
+    "chai": "^4.3.7",
+    "mocha": "^10.7.3",
+    "solana-bankrun": "^0.4.0",
+    "ts-mocha": "^10.0.0",
+    "typescript": "^5.6.3"
+  }
+}

+ 1463 - 0
tokens/token-swap/steel/pnpm-lock.yaml

@@ -0,0 +1,1463 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    dependencies:
+      '@solana/spl-token':
+        specifier: ^0.4.9
+        version: 0.4.9(@solana/web3.js@1.95.4(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)(utf-8-validate@5.0.10)
+      '@solana/web3.js':
+        specifier: ^1.95.4
+        version: 1.95.4(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      bs58:
+        specifier: ^6.0.0
+        version: 6.0.0
+    devDependencies:
+      '@types/chai':
+        specifier: ^4.3.7
+        version: 4.3.20
+      '@types/mocha':
+        specifier: ^10.0.9
+        version: 10.0.9
+      '@types/node':
+        specifier: ^22.7.9
+        version: 22.8.0
+      borsh:
+        specifier: ^2.0.0
+        version: 2.0.0
+      chai:
+        specifier: ^4.3.7
+        version: 4.5.0
+      mocha:
+        specifier: ^10.7.3
+        version: 10.7.3
+      solana-bankrun:
+        specifier: ^0.4.0
+        version: 0.4.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      ts-mocha:
+        specifier: ^10.0.0
+        version: 10.0.0(mocha@10.7.3)
+      typescript:
+        specifier: ^5.6.3
+        version: 5.6.3
+
+packages:
+
+  '@babel/runtime@7.26.0':
+    resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==}
+    engines: {node: '>=6.9.0'}
+
+  '@noble/curves@1.6.0':
+    resolution: {integrity: sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==}
+    engines: {node: ^14.21.3 || >=16}
+
+  '@noble/hashes@1.5.0':
+    resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==}
+    engines: {node: ^14.21.3 || >=16}
+
+  '@solana/buffer-layout-utils@0.2.0':
+    resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==}
+    engines: {node: '>= 10'}
+
+  '@solana/buffer-layout@4.0.1':
+    resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==}
+    engines: {node: '>=5.10'}
+
+  '@solana/codecs-core@2.0.0-rc.1':
+    resolution: {integrity: sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==}
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/codecs-data-structures@2.0.0-rc.1':
+    resolution: {integrity: sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==}
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/codecs-numbers@2.0.0-rc.1':
+    resolution: {integrity: sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==}
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/codecs-strings@2.0.0-rc.1':
+    resolution: {integrity: sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==}
+    peerDependencies:
+      fastestsmallesttextencoderdecoder: ^1.0.22
+      typescript: '>=5'
+
+  '@solana/codecs@2.0.0-rc.1':
+    resolution: {integrity: sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==}
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/errors@2.0.0-rc.1':
+    resolution: {integrity: sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==}
+    hasBin: true
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/options@2.0.0-rc.1':
+    resolution: {integrity: sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==}
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/spl-token-group@0.0.7':
+    resolution: {integrity: sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==}
+    engines: {node: '>=16'}
+    peerDependencies:
+      '@solana/web3.js': ^1.95.3
+
+  '@solana/spl-token-metadata@0.1.6':
+    resolution: {integrity: sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==}
+    engines: {node: '>=16'}
+    peerDependencies:
+      '@solana/web3.js': ^1.95.3
+
+  '@solana/spl-token@0.4.9':
+    resolution: {integrity: sha512-g3wbj4F4gq82YQlwqhPB0gHFXfgsC6UmyGMxtSLf/BozT/oKd59465DbnlUK8L8EcimKMavxsVAMoLcEdeCicg==}
+    engines: {node: '>=16'}
+    peerDependencies:
+      '@solana/web3.js': ^1.95.3
+
+  '@solana/web3.js@1.95.4':
+    resolution: {integrity: sha512-sdewnNEA42ZSMxqkzdwEWi6fDgzwtJHaQa5ndUGEJYtoOnM6X5cvPmjoTUp7/k7bRrVAxfBgDnvQQHD6yhlLYw==}
+
+  '@swc/helpers@0.5.13':
+    resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==}
+
+  '@types/chai@4.3.20':
+    resolution: {integrity: sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==}
+
+  '@types/connect@3.4.38':
+    resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
+
+  '@types/json5@0.0.29':
+    resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
+
+  '@types/mocha@10.0.9':
+    resolution: {integrity: sha512-sicdRoWtYevwxjOHNMPTl3vSfJM6oyW8o1wXeI7uww6b6xHg8eBznQDNSGBCDJmsE8UMxP05JgZRtsKbTqt//Q==}
+
+  '@types/node@12.20.55':
+    resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
+
+  '@types/node@22.8.0':
+    resolution: {integrity: sha512-84rafSBHC/z1i1E3p0cJwKA+CfYDNSXX9WSZBRopjIzLET8oNt6ht2tei4C7izwDeEiLLfdeSVBv1egOH916hg==}
+
+  '@types/uuid@8.3.4':
+    resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
+
+  '@types/ws@7.4.7':
+    resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==}
+
+  '@types/ws@8.5.12':
+    resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==}
+
+  JSONStream@1.3.5:
+    resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
+    hasBin: true
+
+  agentkeepalive@4.5.0:
+    resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
+    engines: {node: '>= 8.0.0'}
+
+  ansi-colors@4.1.3:
+    resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
+    engines: {node: '>=6'}
+
+  ansi-regex@5.0.1:
+    resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+    engines: {node: '>=8'}
+
+  ansi-styles@4.3.0:
+    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+    engines: {node: '>=8'}
+
+  anymatch@3.1.3:
+    resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+    engines: {node: '>= 8'}
+
+  argparse@2.0.1:
+    resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+  arrify@1.0.1:
+    resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
+    engines: {node: '>=0.10.0'}
+
+  assertion-error@1.1.0:
+    resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
+
+  balanced-match@1.0.2:
+    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+  base-x@3.0.10:
+    resolution: {integrity: sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==}
+
+  base-x@5.0.0:
+    resolution: {integrity: sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==}
+
+  base64-js@1.5.1:
+    resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
+  bigint-buffer@1.1.5:
+    resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==}
+    engines: {node: '>= 10.0.0'}
+
+  bignumber.js@9.1.2:
+    resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==}
+
+  binary-extensions@2.3.0:
+    resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+    engines: {node: '>=8'}
+
+  bindings@1.5.0:
+    resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
+
+  bn.js@5.2.1:
+    resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==}
+
+  borsh@0.7.0:
+    resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==}
+
+  borsh@2.0.0:
+    resolution: {integrity: sha512-kc9+BgR3zz9+cjbwM8ODoUB4fs3X3I5A/HtX7LZKxCLaMrEeDFoBpnhZY//DTS1VZBSs6S5v46RZRbZjRFspEg==}
+
+  brace-expansion@2.0.1:
+    resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+
+  braces@3.0.3:
+    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+    engines: {node: '>=8'}
+
+  browser-stdout@1.3.1:
+    resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==}
+
+  bs58@4.0.1:
+    resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==}
+
+  bs58@6.0.0:
+    resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==}
+
+  buffer-from@1.1.2:
+    resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+
+  buffer@6.0.3:
+    resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+
+  bufferutil@4.0.8:
+    resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==}
+    engines: {node: '>=6.14.2'}
+
+  camelcase@6.3.0:
+    resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
+    engines: {node: '>=10'}
+
+  chai@4.5.0:
+    resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==}
+    engines: {node: '>=4'}
+
+  chalk@4.1.2:
+    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+    engines: {node: '>=10'}
+
+  chalk@5.3.0:
+    resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
+    engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
+  check-error@1.0.3:
+    resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
+
+  chokidar@3.6.0:
+    resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
+    engines: {node: '>= 8.10.0'}
+
+  cliui@7.0.4:
+    resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
+
+  color-convert@2.0.1:
+    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+    engines: {node: '>=7.0.0'}
+
+  color-name@1.1.4:
+    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+  commander@12.1.0:
+    resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
+    engines: {node: '>=18'}
+
+  commander@2.20.3:
+    resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
+
+  debug@4.3.7:
+    resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
+    engines: {node: '>=6.0'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
+  decamelize@4.0.0:
+    resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==}
+    engines: {node: '>=10'}
+
+  deep-eql@4.1.4:
+    resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==}
+    engines: {node: '>=6'}
+
+  delay@5.0.0:
+    resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==}
+    engines: {node: '>=10'}
+
+  diff@3.5.0:
+    resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==}
+    engines: {node: '>=0.3.1'}
+
+  diff@5.2.0:
+    resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
+    engines: {node: '>=0.3.1'}
+
+  emoji-regex@8.0.0:
+    resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+  es6-promise@4.2.8:
+    resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
+
+  es6-promisify@5.0.0:
+    resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==}
+
+  escalade@3.2.0:
+    resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+    engines: {node: '>=6'}
+
+  escape-string-regexp@4.0.0:
+    resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+    engines: {node: '>=10'}
+
+  eventemitter3@5.0.1:
+    resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
+
+  eyes@0.1.8:
+    resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==}
+    engines: {node: '> 0.1.90'}
+
+  fast-stable-stringify@1.0.0:
+    resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==}
+
+  fastestsmallesttextencoderdecoder@1.0.22:
+    resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==}
+
+  file-uri-to-path@1.0.0:
+    resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
+
+  fill-range@7.1.1:
+    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+    engines: {node: '>=8'}
+
+  find-up@5.0.0:
+    resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+    engines: {node: '>=10'}
+
+  flat@5.0.2:
+    resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
+    hasBin: true
+
+  fs.realpath@1.0.0:
+    resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+
+  fsevents@2.3.3:
+    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+    os: [darwin]
+
+  get-caller-file@2.0.5:
+    resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+    engines: {node: 6.* || 8.* || >= 10.*}
+
+  get-func-name@2.0.2:
+    resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
+
+  glob-parent@5.1.2:
+    resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+    engines: {node: '>= 6'}
+
+  glob@8.1.0:
+    resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
+    engines: {node: '>=12'}
+    deprecated: Glob versions prior to v9 are no longer supported
+
+  has-flag@4.0.0:
+    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+    engines: {node: '>=8'}
+
+  he@1.2.0:
+    resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
+    hasBin: true
+
+  humanize-ms@1.2.1:
+    resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
+
+  ieee754@1.2.1:
+    resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
+  inflight@1.0.6:
+    resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+    deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
+
+  inherits@2.0.4:
+    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+  is-binary-path@2.1.0:
+    resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+    engines: {node: '>=8'}
+
+  is-extglob@2.1.1:
+    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+    engines: {node: '>=0.10.0'}
+
+  is-fullwidth-code-point@3.0.0:
+    resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+    engines: {node: '>=8'}
+
+  is-glob@4.0.3:
+    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+    engines: {node: '>=0.10.0'}
+
+  is-number@7.0.0:
+    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+    engines: {node: '>=0.12.0'}
+
+  is-plain-obj@2.1.0:
+    resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}
+    engines: {node: '>=8'}
+
+  is-unicode-supported@0.1.0:
+    resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
+    engines: {node: '>=10'}
+
+  isomorphic-ws@4.0.1:
+    resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==}
+    peerDependencies:
+      ws: '*'
+
+  jayson@4.1.2:
+    resolution: {integrity: sha512-5nzMWDHy6f+koZOuYsArh2AXs73NfWYVlFyJJuCedr93GpY+Ku8qq10ropSXVfHK+H0T6paA88ww+/dV+1fBNA==}
+    engines: {node: '>=8'}
+    hasBin: true
+
+  js-yaml@4.1.0:
+    resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+    hasBin: true
+
+  json-stringify-safe@5.0.1:
+    resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
+
+  json5@1.0.2:
+    resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
+    hasBin: true
+
+  jsonparse@1.3.1:
+    resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
+    engines: {'0': node >= 0.2.0}
+
+  locate-path@6.0.0:
+    resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+    engines: {node: '>=10'}
+
+  log-symbols@4.1.0:
+    resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
+    engines: {node: '>=10'}
+
+  loupe@2.3.7:
+    resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
+
+  make-error@1.3.6:
+    resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
+
+  minimatch@5.1.6:
+    resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
+    engines: {node: '>=10'}
+
+  minimist@1.2.8:
+    resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+  mkdirp@0.5.6:
+    resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
+    hasBin: true
+
+  mocha@10.7.3:
+    resolution: {integrity: sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==}
+    engines: {node: '>= 14.0.0'}
+    hasBin: true
+
+  ms@2.1.3:
+    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+  node-fetch@2.7.0:
+    resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+    engines: {node: 4.x || >=6.0.0}
+    peerDependencies:
+      encoding: ^0.1.0
+    peerDependenciesMeta:
+      encoding:
+        optional: true
+
+  node-gyp-build@4.8.2:
+    resolution: {integrity: sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==}
+    hasBin: true
+
+  normalize-path@3.0.0:
+    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+    engines: {node: '>=0.10.0'}
+
+  once@1.4.0:
+    resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+  p-limit@3.1.0:
+    resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+    engines: {node: '>=10'}
+
+  p-locate@5.0.0:
+    resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+    engines: {node: '>=10'}
+
+  path-exists@4.0.0:
+    resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+    engines: {node: '>=8'}
+
+  pathval@1.1.1:
+    resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
+
+  picomatch@2.3.1:
+    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+    engines: {node: '>=8.6'}
+
+  randombytes@2.1.0:
+    resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
+
+  readdirp@3.6.0:
+    resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+    engines: {node: '>=8.10.0'}
+
+  regenerator-runtime@0.14.1:
+    resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
+
+  require-directory@2.1.1:
+    resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+    engines: {node: '>=0.10.0'}
+
+  rpc-websockets@9.0.4:
+    resolution: {integrity: sha512-yWZWN0M+bivtoNLnaDbtny4XchdAIF5Q4g/ZsC5UC61Ckbp0QczwO8fg44rV3uYmY4WHd+EZQbn90W1d8ojzqQ==}
+
+  safe-buffer@5.2.1:
+    resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
+  serialize-javascript@6.0.2:
+    resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
+
+  solana-bankrun-darwin-arm64@0.4.0:
+    resolution: {integrity: sha512-6dz78Teoz7ez/3lpRLDjktYLJb79FcmJk2me4/YaB8WiO6W43OdExU4h+d2FyuAryO2DgBPXaBoBNY/8J1HJmw==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [darwin]
+
+  solana-bankrun-darwin-universal@0.4.0:
+    resolution: {integrity: sha512-zSSw/Jx3KNU42pPMmrEWABd0nOwGJfsj7nm9chVZ3ae7WQg3Uty0hHAkn5NSDCj3OOiN0py9Dr1l9vmRJpOOxg==}
+    engines: {node: '>= 10'}
+    os: [darwin]
+
+  solana-bankrun-darwin-x64@0.4.0:
+    resolution: {integrity: sha512-LWjs5fsgHFtyr7YdJR6r0Ho5zrtzI6CY4wvwPXr8H2m3b4pZe6RLIZjQtabCav4cguc14G0K8yQB2PTMuGub8w==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [darwin]
+
+  solana-bankrun-linux-x64-gnu@0.4.0:
+    resolution: {integrity: sha512-SrlVrb82UIxt21Zr/XZFHVV/h9zd2/nP25PMpLJVLD7Pgl2yhkhfi82xj3OjxoQqWe+zkBJ+uszA0EEKr67yNw==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [linux]
+
+  solana-bankrun-linux-x64-musl@0.4.0:
+    resolution: {integrity: sha512-Nv328ZanmURdYfcLL+jwB1oMzX4ZzK57NwIcuJjGlf0XSNLq96EoaO5buEiUTo4Ls7MqqMyLbClHcrPE7/aKyA==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [linux]
+
+  solana-bankrun@0.4.0:
+    resolution: {integrity: sha512-NMmXUipPBkt8NgnyNO3SCnPERP6xT/AMNMBooljGA3+rG6NN8lmXJsKeLqQTiFsDeWD74U++QM/DgcueSWvrIg==}
+    engines: {node: '>= 10'}
+
+  source-map-support@0.5.21:
+    resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+
+  source-map@0.6.1:
+    resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+    engines: {node: '>=0.10.0'}
+
+  string-width@4.2.3:
+    resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+    engines: {node: '>=8'}
+
+  strip-ansi@6.0.1:
+    resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+    engines: {node: '>=8'}
+
+  strip-bom@3.0.0:
+    resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
+    engines: {node: '>=4'}
+
+  strip-json-comments@3.1.1:
+    resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+    engines: {node: '>=8'}
+
+  superstruct@2.0.2:
+    resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==}
+    engines: {node: '>=14.0.0'}
+
+  supports-color@7.2.0:
+    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+    engines: {node: '>=8'}
+
+  supports-color@8.1.1:
+    resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
+    engines: {node: '>=10'}
+
+  text-encoding-utf-8@1.0.2:
+    resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==}
+
+  through@2.3.8:
+    resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
+
+  to-regex-range@5.0.1:
+    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+    engines: {node: '>=8.0'}
+
+  tr46@0.0.3:
+    resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
+  ts-mocha@10.0.0:
+    resolution: {integrity: sha512-VRfgDO+iiuJFlNB18tzOfypJ21xn2xbuZyDvJvqpTbWgkAgD17ONGr8t+Tl8rcBtOBdjXp5e/Rk+d39f7XBHRw==}
+    engines: {node: '>= 6.X.X'}
+    hasBin: true
+    peerDependencies:
+      mocha: ^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X || ^9.X.X || ^10.X.X
+
+  ts-node@7.0.1:
+    resolution: {integrity: sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==}
+    engines: {node: '>=4.2.0'}
+    hasBin: true
+
+  tsconfig-paths@3.15.0:
+    resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
+
+  tslib@2.8.0:
+    resolution: {integrity: sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==}
+
+  type-detect@4.1.0:
+    resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==}
+    engines: {node: '>=4'}
+
+  typescript@5.6.3:
+    resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+  undici-types@6.19.8:
+    resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
+
+  utf-8-validate@5.0.10:
+    resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==}
+    engines: {node: '>=6.14.2'}
+
+  uuid@8.3.2:
+    resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
+    hasBin: true
+
+  webidl-conversions@3.0.1:
+    resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
+  whatwg-url@5.0.0:
+    resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
+  workerpool@6.5.1:
+    resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==}
+
+  wrap-ansi@7.0.0:
+    resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+    engines: {node: '>=10'}
+
+  wrappy@1.0.2:
+    resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+  ws@7.5.10:
+    resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}
+    engines: {node: '>=8.3.0'}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: ^5.0.2
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+
+  ws@8.18.0:
+    resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
+    engines: {node: '>=10.0.0'}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: '>=5.0.2'
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+
+  y18n@5.0.8:
+    resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+    engines: {node: '>=10'}
+
+  yargs-parser@20.2.9:
+    resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
+    engines: {node: '>=10'}
+
+  yargs-unparser@2.0.0:
+    resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==}
+    engines: {node: '>=10'}
+
+  yargs@16.2.0:
+    resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
+    engines: {node: '>=10'}
+
+  yn@2.0.0:
+    resolution: {integrity: sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ==}
+    engines: {node: '>=4'}
+
+  yocto-queue@0.1.0:
+    resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+    engines: {node: '>=10'}
+
+snapshots:
+
+  '@babel/runtime@7.26.0':
+    dependencies:
+      regenerator-runtime: 0.14.1
+
+  '@noble/curves@1.6.0':
+    dependencies:
+      '@noble/hashes': 1.5.0
+
+  '@noble/hashes@1.5.0': {}
+
+  '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)':
+    dependencies:
+      '@solana/buffer-layout': 4.0.1
+      '@solana/web3.js': 1.95.4(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      bigint-buffer: 1.1.5
+      bignumber.js: 9.1.2
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - utf-8-validate
+
+  '@solana/buffer-layout@4.0.1':
+    dependencies:
+      buffer: 6.0.3
+
+  '@solana/codecs-core@2.0.0-rc.1(typescript@5.6.3)':
+    dependencies:
+      '@solana/errors': 2.0.0-rc.1(typescript@5.6.3)
+      typescript: 5.6.3
+
+  '@solana/codecs-data-structures@2.0.0-rc.1(typescript@5.6.3)':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-rc.1(typescript@5.6.3)
+      '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.6.3)
+      '@solana/errors': 2.0.0-rc.1(typescript@5.6.3)
+      typescript: 5.6.3
+
+  '@solana/codecs-numbers@2.0.0-rc.1(typescript@5.6.3)':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-rc.1(typescript@5.6.3)
+      '@solana/errors': 2.0.0-rc.1(typescript@5.6.3)
+      typescript: 5.6.3
+
+  '@solana/codecs-strings@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-rc.1(typescript@5.6.3)
+      '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.6.3)
+      '@solana/errors': 2.0.0-rc.1(typescript@5.6.3)
+      fastestsmallesttextencoderdecoder: 1.0.22
+      typescript: 5.6.3
+
+  '@solana/codecs@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-rc.1(typescript@5.6.3)
+      '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.6.3)
+      '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.6.3)
+      '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)
+      '@solana/options': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)
+      typescript: 5.6.3
+    transitivePeerDependencies:
+      - fastestsmallesttextencoderdecoder
+
+  '@solana/errors@2.0.0-rc.1(typescript@5.6.3)':
+    dependencies:
+      chalk: 5.3.0
+      commander: 12.1.0
+      typescript: 5.6.3
+
+  '@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-rc.1(typescript@5.6.3)
+      '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.6.3)
+      '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.6.3)
+      '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)
+      '@solana/errors': 2.0.0-rc.1(typescript@5.6.3)
+      typescript: 5.6.3
+    transitivePeerDependencies:
+      - fastestsmallesttextencoderdecoder
+
+  '@solana/spl-token-group@0.0.7(@solana/web3.js@1.95.4(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)':
+    dependencies:
+      '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)
+      '@solana/web3.js': 1.95.4(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+    transitivePeerDependencies:
+      - fastestsmallesttextencoderdecoder
+      - typescript
+
+  '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.95.4(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)':
+    dependencies:
+      '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)
+      '@solana/web3.js': 1.95.4(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+    transitivePeerDependencies:
+      - fastestsmallesttextencoderdecoder
+      - typescript
+
+  '@solana/spl-token@0.4.9(@solana/web3.js@1.95.4(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)(utf-8-validate@5.0.10)':
+    dependencies:
+      '@solana/buffer-layout': 4.0.1
+      '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      '@solana/spl-token-group': 0.0.7(@solana/web3.js@1.95.4(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)
+      '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.95.4(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)
+      '@solana/web3.js': 1.95.4(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      buffer: 6.0.3
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - fastestsmallesttextencoderdecoder
+      - typescript
+      - utf-8-validate
+
+  '@solana/web3.js@1.95.4(bufferutil@4.0.8)(utf-8-validate@5.0.10)':
+    dependencies:
+      '@babel/runtime': 7.26.0
+      '@noble/curves': 1.6.0
+      '@noble/hashes': 1.5.0
+      '@solana/buffer-layout': 4.0.1
+      agentkeepalive: 4.5.0
+      bigint-buffer: 1.1.5
+      bn.js: 5.2.1
+      borsh: 0.7.0
+      bs58: 4.0.1
+      buffer: 6.0.3
+      fast-stable-stringify: 1.0.0
+      jayson: 4.1.2(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      node-fetch: 2.7.0
+      rpc-websockets: 9.0.4
+      superstruct: 2.0.2
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - utf-8-validate
+
+  '@swc/helpers@0.5.13':
+    dependencies:
+      tslib: 2.8.0
+
+  '@types/chai@4.3.20': {}
+
+  '@types/connect@3.4.38':
+    dependencies:
+      '@types/node': 22.8.0
+
+  '@types/json5@0.0.29':
+    optional: true
+
+  '@types/mocha@10.0.9': {}
+
+  '@types/node@12.20.55': {}
+
+  '@types/node@22.8.0':
+    dependencies:
+      undici-types: 6.19.8
+
+  '@types/uuid@8.3.4': {}
+
+  '@types/ws@7.4.7':
+    dependencies:
+      '@types/node': 22.8.0
+
+  '@types/ws@8.5.12':
+    dependencies:
+      '@types/node': 22.8.0
+
+  JSONStream@1.3.5:
+    dependencies:
+      jsonparse: 1.3.1
+      through: 2.3.8
+
+  agentkeepalive@4.5.0:
+    dependencies:
+      humanize-ms: 1.2.1
+
+  ansi-colors@4.1.3: {}
+
+  ansi-regex@5.0.1: {}
+
+  ansi-styles@4.3.0:
+    dependencies:
+      color-convert: 2.0.1
+
+  anymatch@3.1.3:
+    dependencies:
+      normalize-path: 3.0.0
+      picomatch: 2.3.1
+
+  argparse@2.0.1: {}
+
+  arrify@1.0.1: {}
+
+  assertion-error@1.1.0: {}
+
+  balanced-match@1.0.2: {}
+
+  base-x@3.0.10:
+    dependencies:
+      safe-buffer: 5.2.1
+
+  base-x@5.0.0: {}
+
+  base64-js@1.5.1: {}
+
+  bigint-buffer@1.1.5:
+    dependencies:
+      bindings: 1.5.0
+
+  bignumber.js@9.1.2: {}
+
+  binary-extensions@2.3.0: {}
+
+  bindings@1.5.0:
+    dependencies:
+      file-uri-to-path: 1.0.0
+
+  bn.js@5.2.1: {}
+
+  borsh@0.7.0:
+    dependencies:
+      bn.js: 5.2.1
+      bs58: 4.0.1
+      text-encoding-utf-8: 1.0.2
+
+  borsh@2.0.0: {}
+
+  brace-expansion@2.0.1:
+    dependencies:
+      balanced-match: 1.0.2
+
+  braces@3.0.3:
+    dependencies:
+      fill-range: 7.1.1
+
+  browser-stdout@1.3.1: {}
+
+  bs58@4.0.1:
+    dependencies:
+      base-x: 3.0.10
+
+  bs58@6.0.0:
+    dependencies:
+      base-x: 5.0.0
+
+  buffer-from@1.1.2: {}
+
+  buffer@6.0.3:
+    dependencies:
+      base64-js: 1.5.1
+      ieee754: 1.2.1
+
+  bufferutil@4.0.8:
+    dependencies:
+      node-gyp-build: 4.8.2
+    optional: true
+
+  camelcase@6.3.0: {}
+
+  chai@4.5.0:
+    dependencies:
+      assertion-error: 1.1.0
+      check-error: 1.0.3
+      deep-eql: 4.1.4
+      get-func-name: 2.0.2
+      loupe: 2.3.7
+      pathval: 1.1.1
+      type-detect: 4.1.0
+
+  chalk@4.1.2:
+    dependencies:
+      ansi-styles: 4.3.0
+      supports-color: 7.2.0
+
+  chalk@5.3.0: {}
+
+  check-error@1.0.3:
+    dependencies:
+      get-func-name: 2.0.2
+
+  chokidar@3.6.0:
+    dependencies:
+      anymatch: 3.1.3
+      braces: 3.0.3
+      glob-parent: 5.1.2
+      is-binary-path: 2.1.0
+      is-glob: 4.0.3
+      normalize-path: 3.0.0
+      readdirp: 3.6.0
+    optionalDependencies:
+      fsevents: 2.3.3
+
+  cliui@7.0.4:
+    dependencies:
+      string-width: 4.2.3
+      strip-ansi: 6.0.1
+      wrap-ansi: 7.0.0
+
+  color-convert@2.0.1:
+    dependencies:
+      color-name: 1.1.4
+
+  color-name@1.1.4: {}
+
+  commander@12.1.0: {}
+
+  commander@2.20.3: {}
+
+  debug@4.3.7(supports-color@8.1.1):
+    dependencies:
+      ms: 2.1.3
+    optionalDependencies:
+      supports-color: 8.1.1
+
+  decamelize@4.0.0: {}
+
+  deep-eql@4.1.4:
+    dependencies:
+      type-detect: 4.1.0
+
+  delay@5.0.0: {}
+
+  diff@3.5.0: {}
+
+  diff@5.2.0: {}
+
+  emoji-regex@8.0.0: {}
+
+  es6-promise@4.2.8: {}
+
+  es6-promisify@5.0.0:
+    dependencies:
+      es6-promise: 4.2.8
+
+  escalade@3.2.0: {}
+
+  escape-string-regexp@4.0.0: {}
+
+  eventemitter3@5.0.1: {}
+
+  eyes@0.1.8: {}
+
+  fast-stable-stringify@1.0.0: {}
+
+  fastestsmallesttextencoderdecoder@1.0.22: {}
+
+  file-uri-to-path@1.0.0: {}
+
+  fill-range@7.1.1:
+    dependencies:
+      to-regex-range: 5.0.1
+
+  find-up@5.0.0:
+    dependencies:
+      locate-path: 6.0.0
+      path-exists: 4.0.0
+
+  flat@5.0.2: {}
+
+  fs.realpath@1.0.0: {}
+
+  fsevents@2.3.3:
+    optional: true
+
+  get-caller-file@2.0.5: {}
+
+  get-func-name@2.0.2: {}
+
+  glob-parent@5.1.2:
+    dependencies:
+      is-glob: 4.0.3
+
+  glob@8.1.0:
+    dependencies:
+      fs.realpath: 1.0.0
+      inflight: 1.0.6
+      inherits: 2.0.4
+      minimatch: 5.1.6
+      once: 1.4.0
+
+  has-flag@4.0.0: {}
+
+  he@1.2.0: {}
+
+  humanize-ms@1.2.1:
+    dependencies:
+      ms: 2.1.3
+
+  ieee754@1.2.1: {}
+
+  inflight@1.0.6:
+    dependencies:
+      once: 1.4.0
+      wrappy: 1.0.2
+
+  inherits@2.0.4: {}
+
+  is-binary-path@2.1.0:
+    dependencies:
+      binary-extensions: 2.3.0
+
+  is-extglob@2.1.1: {}
+
+  is-fullwidth-code-point@3.0.0: {}
+
+  is-glob@4.0.3:
+    dependencies:
+      is-extglob: 2.1.1
+
+  is-number@7.0.0: {}
+
+  is-plain-obj@2.1.0: {}
+
+  is-unicode-supported@0.1.0: {}
+
+  isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)):
+    dependencies:
+      ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+
+  jayson@4.1.2(bufferutil@4.0.8)(utf-8-validate@5.0.10):
+    dependencies:
+      '@types/connect': 3.4.38
+      '@types/node': 12.20.55
+      '@types/ws': 7.4.7
+      JSONStream: 1.3.5
+      commander: 2.20.3
+      delay: 5.0.0
+      es6-promisify: 5.0.0
+      eyes: 0.1.8
+      isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10))
+      json-stringify-safe: 5.0.1
+      uuid: 8.3.2
+      ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
+  js-yaml@4.1.0:
+    dependencies:
+      argparse: 2.0.1
+
+  json-stringify-safe@5.0.1: {}
+
+  json5@1.0.2:
+    dependencies:
+      minimist: 1.2.8
+    optional: true
+
+  jsonparse@1.3.1: {}
+
+  locate-path@6.0.0:
+    dependencies:
+      p-locate: 5.0.0
+
+  log-symbols@4.1.0:
+    dependencies:
+      chalk: 4.1.2
+      is-unicode-supported: 0.1.0
+
+  loupe@2.3.7:
+    dependencies:
+      get-func-name: 2.0.2
+
+  make-error@1.3.6: {}
+
+  minimatch@5.1.6:
+    dependencies:
+      brace-expansion: 2.0.1
+
+  minimist@1.2.8: {}
+
+  mkdirp@0.5.6:
+    dependencies:
+      minimist: 1.2.8
+
+  mocha@10.7.3:
+    dependencies:
+      ansi-colors: 4.1.3
+      browser-stdout: 1.3.1
+      chokidar: 3.6.0
+      debug: 4.3.7(supports-color@8.1.1)
+      diff: 5.2.0
+      escape-string-regexp: 4.0.0
+      find-up: 5.0.0
+      glob: 8.1.0
+      he: 1.2.0
+      js-yaml: 4.1.0
+      log-symbols: 4.1.0
+      minimatch: 5.1.6
+      ms: 2.1.3
+      serialize-javascript: 6.0.2
+      strip-json-comments: 3.1.1
+      supports-color: 8.1.1
+      workerpool: 6.5.1
+      yargs: 16.2.0
+      yargs-parser: 20.2.9
+      yargs-unparser: 2.0.0
+
+  ms@2.1.3: {}
+
+  node-fetch@2.7.0:
+    dependencies:
+      whatwg-url: 5.0.0
+
+  node-gyp-build@4.8.2:
+    optional: true
+
+  normalize-path@3.0.0: {}
+
+  once@1.4.0:
+    dependencies:
+      wrappy: 1.0.2
+
+  p-limit@3.1.0:
+    dependencies:
+      yocto-queue: 0.1.0
+
+  p-locate@5.0.0:
+    dependencies:
+      p-limit: 3.1.0
+
+  path-exists@4.0.0: {}
+
+  pathval@1.1.1: {}
+
+  picomatch@2.3.1: {}
+
+  randombytes@2.1.0:
+    dependencies:
+      safe-buffer: 5.2.1
+
+  readdirp@3.6.0:
+    dependencies:
+      picomatch: 2.3.1
+
+  regenerator-runtime@0.14.1: {}
+
+  require-directory@2.1.1: {}
+
+  rpc-websockets@9.0.4:
+    dependencies:
+      '@swc/helpers': 0.5.13
+      '@types/uuid': 8.3.4
+      '@types/ws': 8.5.12
+      buffer: 6.0.3
+      eventemitter3: 5.0.1
+      uuid: 8.3.2
+      ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+    optionalDependencies:
+      bufferutil: 4.0.8
+      utf-8-validate: 5.0.10
+
+  safe-buffer@5.2.1: {}
+
+  serialize-javascript@6.0.2:
+    dependencies:
+      randombytes: 2.1.0
+
+  solana-bankrun-darwin-arm64@0.4.0:
+    optional: true
+
+  solana-bankrun-darwin-universal@0.4.0:
+    optional: true
+
+  solana-bankrun-darwin-x64@0.4.0:
+    optional: true
+
+  solana-bankrun-linux-x64-gnu@0.4.0:
+    optional: true
+
+  solana-bankrun-linux-x64-musl@0.4.0:
+    optional: true
+
+  solana-bankrun@0.4.0(bufferutil@4.0.8)(utf-8-validate@5.0.10):
+    dependencies:
+      '@solana/web3.js': 1.95.4(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      bs58: 4.0.1
+    optionalDependencies:
+      solana-bankrun-darwin-arm64: 0.4.0
+      solana-bankrun-darwin-universal: 0.4.0
+      solana-bankrun-darwin-x64: 0.4.0
+      solana-bankrun-linux-x64-gnu: 0.4.0
+      solana-bankrun-linux-x64-musl: 0.4.0
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - utf-8-validate
+
+  source-map-support@0.5.21:
+    dependencies:
+      buffer-from: 1.1.2
+      source-map: 0.6.1
+
+  source-map@0.6.1: {}
+
+  string-width@4.2.3:
+    dependencies:
+      emoji-regex: 8.0.0
+      is-fullwidth-code-point: 3.0.0
+      strip-ansi: 6.0.1
+
+  strip-ansi@6.0.1:
+    dependencies:
+      ansi-regex: 5.0.1
+
+  strip-bom@3.0.0:
+    optional: true
+
+  strip-json-comments@3.1.1: {}
+
+  superstruct@2.0.2: {}
+
+  supports-color@7.2.0:
+    dependencies:
+      has-flag: 4.0.0
+
+  supports-color@8.1.1:
+    dependencies:
+      has-flag: 4.0.0
+
+  text-encoding-utf-8@1.0.2: {}
+
+  through@2.3.8: {}
+
+  to-regex-range@5.0.1:
+    dependencies:
+      is-number: 7.0.0
+
+  tr46@0.0.3: {}
+
+  ts-mocha@10.0.0(mocha@10.7.3):
+    dependencies:
+      mocha: 10.7.3
+      ts-node: 7.0.1
+    optionalDependencies:
+      tsconfig-paths: 3.15.0
+
+  ts-node@7.0.1:
+    dependencies:
+      arrify: 1.0.1
+      buffer-from: 1.1.2
+      diff: 3.5.0
+      make-error: 1.3.6
+      minimist: 1.2.8
+      mkdirp: 0.5.6
+      source-map-support: 0.5.21
+      yn: 2.0.0
+
+  tsconfig-paths@3.15.0:
+    dependencies:
+      '@types/json5': 0.0.29
+      json5: 1.0.2
+      minimist: 1.2.8
+      strip-bom: 3.0.0
+    optional: true
+
+  tslib@2.8.0: {}
+
+  type-detect@4.1.0: {}
+
+  typescript@5.6.3: {}
+
+  undici-types@6.19.8: {}
+
+  utf-8-validate@5.0.10:
+    dependencies:
+      node-gyp-build: 4.8.2
+    optional: true
+
+  uuid@8.3.2: {}
+
+  webidl-conversions@3.0.1: {}
+
+  whatwg-url@5.0.0:
+    dependencies:
+      tr46: 0.0.3
+      webidl-conversions: 3.0.1
+
+  workerpool@6.5.1: {}
+
+  wrap-ansi@7.0.0:
+    dependencies:
+      ansi-styles: 4.3.0
+      string-width: 4.2.3
+      strip-ansi: 6.0.1
+
+  wrappy@1.0.2: {}
+
+  ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10):
+    optionalDependencies:
+      bufferutil: 4.0.8
+      utf-8-validate: 5.0.10
+
+  ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10):
+    optionalDependencies:
+      bufferutil: 4.0.8
+      utf-8-validate: 5.0.10
+
+  y18n@5.0.8: {}
+
+  yargs-parser@20.2.9: {}
+
+  yargs-unparser@2.0.0:
+    dependencies:
+      camelcase: 6.3.0
+      decamelize: 4.0.0
+      flat: 5.0.2
+      is-plain-obj: 2.1.0
+
+  yargs@16.2.0:
+    dependencies:
+      cliui: 7.0.4
+      escalade: 3.2.0
+      get-caller-file: 2.0.5
+      require-directory: 2.1.1
+      string-width: 4.2.3
+      y18n: 5.0.8
+      yargs-parser: 20.2.9
+
+  yn@2.0.0: {}
+
+  yocto-queue@0.1.0: {}

+ 28 - 0
tokens/token-swap/steel/program/Cargo.toml

@@ -0,0 +1,28 @@
+[package]
+name = "token-swap-program"
+description = ""
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+homepage.workspace = true
+documentation.workspace = true
+repository.workspace = true
+readme.workspace = true
+keywords.workspace = true
+
+[lib]
+crate-type = ["cdylib", "lib"]
+
+[dependencies]
+token-swap-api.workspace = true
+solana-program.workspace = true
+steel.workspace = true
+spl-token.workspace = true
+spl-math.workspace = true
+
+[dev-dependencies]
+bs64 = "0.1.2"
+rand = "0.8.5"
+solana-program-test = "1.18"
+solana-sdk = "1.18"
+tokio = { version = "1.35", features = ["full"] }

+ 40 - 0
tokens/token-swap/steel/program/src/create_amm.rs

@@ -0,0 +1,40 @@
+use steel::*;
+use token_swap_api::prelude::*;
+
+pub fn process_create_amm(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
+    // Load accounts.
+    let [payer_info, admin_info, amm_info, system_program] = accounts else {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    };
+
+    let args = CreateAmm::try_from_bytes(data)?;
+    let id = args.id;
+    let fee = args.fee;
+
+    // Check fee is valid.
+    if u16::from_le_bytes(fee) > 10000 {
+        return Err(TokenSwapError::InvalidFee.into());
+    }
+
+    // check payer is signer of the transaction
+    payer_info.is_signer()?;
+    amm_info
+        .is_empty()?
+        .is_writable()?
+        .has_seeds(&[id.as_ref()], &token_swap_api::ID)?;
+    system_program.is_program(&system_program::ID)?;
+
+    // Initialize amm account.
+    create_account::<Amm>(
+        amm_info,
+        system_program,
+        payer_info,
+        &token_swap_api::ID,
+        &[id.as_ref()],
+    )?;
+    let amm = amm_info.as_account_mut::<Amm>(&token_swap_api::ID)?;
+    amm.admin = *admin_info.key;
+    amm.id = id;
+    amm.fee = fee;
+    Ok(())
+}

+ 141 - 0
tokens/token-swap/steel/program/src/create_pool.rs

@@ -0,0 +1,141 @@
+use solana_program::program_pack::Pack;
+use spl_token::state::Mint;
+use steel::*;
+use token_swap_api::prelude::*;
+
+pub fn process_create_pool(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult {
+    // Load accounts.
+    let [payer_info, amm_info, pool_info, pool_authority_info, mint_liquidity_info, mint_a_info, mint_b_info, pool_account_a_info, pool_account_b_info, token_program, system_program, associated_token_program, rent_sysvar] =
+        accounts
+    else {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    };
+
+    // Check payer account is signer.
+    payer_info.is_signer()?;
+    token_program.is_program(&spl_token::ID)?;
+    system_program.is_program(&system_program::ID)?;
+    associated_token_program.is_program(&ASSOCIATED_TOKEN_PROGRAM_ID)?;
+
+    // Check amm account is owned by token_swap_api::ID.
+    amm_info.has_owner(&token_swap_api::ID)?;
+
+    // Check pool account is owned by token_swap_api::ID.
+    pool_info.is_empty()?.is_writable()?.has_seeds(
+        &[
+            amm_info.key.as_ref(),
+            mint_a_info.key.as_ref(),
+            mint_b_info.key.as_ref(),
+        ],
+        &token_swap_api::ID,
+    )?;
+
+    // Check pool_authority account
+    pool_authority_info.is_empty()?.has_seeds(
+        &[
+            amm_info.key.as_ref(),
+            mint_a_info.key.as_ref(),
+            mint_b_info.key.as_ref(),
+            AUTHORITY_SEED,
+        ],
+        &token_swap_api::ID,
+    )?;
+
+    // Check mint_liquidity account
+    mint_liquidity_info.is_empty()?.is_writable()?.has_seeds(
+        &[
+            amm_info.key.as_ref(),
+            mint_a_info.key.as_ref(),
+            mint_b_info.key.as_ref(),
+            LIQUIDITY_SEED,
+        ],
+        &token_swap_api::ID,
+    )?;
+
+    // Verify mint_a and mint_b is a mint account.
+    let _mint_a = mint_a_info.as_mint()?;
+    let _mint_b = mint_b_info.as_mint()?;
+
+    // Verify pool_account_a and pool_account_b is
+    pool_account_a_info.is_empty()?.is_writable()?;
+
+    pool_account_b_info.is_empty()?.is_writable()?;
+
+    // init pool account
+    create_account::<Pool>(
+        pool_info,
+        system_program,
+        payer_info,
+        &token_swap_api::ID,
+        &[
+            amm_info.key.as_ref(),
+            mint_a_info.key.as_ref(),
+            mint_b_info.key.as_ref(),
+        ],
+    )?;
+
+    // get mint_liquidity_info bump to save
+    let (_, bump) = pool_authority_pda(*amm_info.key, *mint_a_info.key, *mint_b_info.key);
+
+    let pool_info_data = pool_info.as_account_mut::<Pool>(&token_swap_api::ID)?;
+    pool_info_data.amm = *amm_info.key;
+    pool_info_data.mint_a = *mint_a_info.key;
+    pool_info_data.mint_b = *mint_b_info.key;
+    pool_info_data.pool_authority_bump = bump;
+
+    let (_, bump) = mint_liquidity_pda(*amm_info.key, *mint_a_info.key, *mint_b_info.key);
+    // allocate mint_liquidity account
+    allocate_account_with_bump(
+        mint_liquidity_info,
+        system_program,
+        payer_info,
+        Mint::LEN,
+        &spl_token::ID,
+        &[
+            amm_info.key.as_ref(),
+            mint_a_info.key.as_ref(),
+            mint_b_info.key.as_ref(),
+            LIQUIDITY_SEED,
+        ],
+        bump,
+    )?;
+
+    // init mint_liquidity account
+    solana_program::program::invoke(
+        &spl_token::instruction::initialize_mint(
+            &spl_token::ID,
+            mint_liquidity_info.key,
+            pool_authority_info.key,
+            Some(pool_authority_info.key),
+            9,
+        )?,
+        &[
+            token_program.clone(),
+            mint_liquidity_info.clone(),
+            pool_authority_info.clone(),
+            rent_sysvar.clone(),
+        ],
+    )?;
+
+    create_associated_token_account(
+        payer_info,
+        pool_authority_info,
+        pool_account_a_info,
+        mint_a_info,
+        system_program,
+        token_program,
+        associated_token_program,
+    )?;
+
+    create_associated_token_account(
+        payer_info,
+        pool_authority_info,
+        pool_account_b_info,
+        mint_b_info,
+        system_program,
+        token_program,
+        associated_token_program,
+    )?;
+
+    Ok(())
+}

+ 179 - 0
tokens/token-swap/steel/program/src/deposit_liquidity.rs

@@ -0,0 +1,179 @@
+use spl_math::uint::U256;
+use steel::*;
+use token_swap_api::prelude::*;
+pub fn process_deposit_liquidity(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
+    // Load accounts.
+    let [payer_info, depositor_info, pool_info, pool_authority_info, mint_liquidity_info, mint_a_info, mint_b_info, pool_account_a_info, pool_account_b_info, depositor_account_liquidity_info, depositor_account_a_info, depositor_account_b_info, token_program, system_program, associated_token_program] =
+        accounts
+    else {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    };
+
+    let args = DepositLiquidity::try_from_bytes(data)?;
+    let amount_a = u64::from_le_bytes(args.amount_a);
+    let amount_b = u64::from_le_bytes(args.amount_b);
+
+    // Check payer account is signer and program is the correct program.
+    payer_info.is_signer()?;
+    token_program.is_program(&spl_token::ID)?;
+    system_program.is_program(&system_program::ID)?;
+    associated_token_program.is_program(&ASSOCIATED_TOKEN_PROGRAM_ID)?;
+
+    // check if depositor is signer
+    depositor_info.is_signer()?;
+
+    // Verify mint_a and mint_b is a mint account.
+    let _mint_a = mint_a_info.as_mint()?;
+    let _mint_b = mint_b_info.as_mint()?;
+
+    // validate pool account
+
+    if pool_info.data_is_empty() {
+        return Err(TokenSwapError::AccountIsNotExisted.into());
+    }
+    validate_pool_account(pool_info, *mint_a_info.key, *mint_b_info.key)?;
+
+    let pool_info_data = pool_info.as_account_mut::<Pool>(&token_swap_api::ID)?;
+
+    // validate pool authority
+    validate_pool_authority(
+        pool_info_data,
+        pool_authority_info,
+        *mint_a_info.key,
+        *mint_b_info.key,
+    )?;
+
+    // validate mint liquidity
+    validate_mint_liquidity(
+        pool_info_data,
+        mint_liquidity_info,
+        *mint_a_info.key,
+        *mint_b_info.key,
+    )?;
+
+    // // validate pool_account_a_info, pool_account_b_info
+    let pool_account_a = pool_account_a_info
+        .is_writable()?
+        .as_associated_token_account(pool_authority_info.key, mint_a_info.key)?;
+    let pool_account_b = pool_account_b_info
+        .is_writable()?
+        .as_associated_token_account(pool_authority_info.key, mint_b_info.key)?;
+
+    // validate depositor_account_a_info and depositor_account_b_info
+    let depositor_account_a = depositor_account_a_info
+        .is_writable()?
+        .as_associated_token_account(depositor_info.key, mint_a_info.key)?;
+    let depositor_account_b = depositor_account_b_info
+        .is_writable()?
+        .as_associated_token_account(depositor_info.key, mint_b_info.key)?;
+
+    // Prevent depositing assets the depositor does not own
+    let mut amount_a = if amount_a > depositor_account_a.amount {
+        depositor_account_a.amount
+    } else {
+        amount_a
+    };
+    let mut amount_b = if amount_b > depositor_account_b.amount {
+        depositor_account_b.amount
+    } else {
+        amount_b
+    };
+
+    // Defining pool creation like this allows attackers to frontrun pool creation with bad ratios
+    let pool_creation = pool_account_a.amount == 0 && pool_account_b.amount == 0;
+
+    (amount_a, amount_b) = if pool_creation {
+        // Add as is if there is no liquidity
+        (amount_a, amount_b)
+    } else {
+        let ratio = U256::from(pool_account_a.amount)
+            .checked_mul(U256::from(pool_account_b.amount))
+            .unwrap();
+        if pool_account_a.amount > pool_account_b.amount {
+            (
+                U256::from(amount_b).checked_mul(ratio).unwrap().as_u64(),
+                amount_b,
+            )
+        } else {
+            (
+                amount_a,
+                U256::from(amount_a).checked_div(ratio).unwrap().as_u64(),
+            )
+        }
+    };
+
+    // Transfer tokens to the pool
+    transfer(
+        depositor_info,
+        depositor_account_a_info,
+        pool_account_a_info,
+        token_program,
+        amount_a,
+    )?;
+
+    transfer(
+        depositor_info,
+        depositor_account_b_info,
+        pool_account_b_info,
+        token_program,
+        amount_b,
+    )?;
+
+    // Computing the amount of liquidity about to be deposited
+    let mut liquidity = U256::from(amount_a)
+        .checked_mul(U256::from(amount_b))
+        .unwrap()
+        .integer_sqrt()
+        .as_u64();
+
+    // Lock some minimum liquidity on the first deposit
+    if pool_creation {
+        if liquidity < MINIMUM_LIQUIDITY {
+            return Err(TokenSwapError::DepositTooSmall.into());
+        }
+
+        liquidity -= MINIMUM_LIQUIDITY;
+    }
+
+    if depositor_account_liquidity_info.data_is_empty() {
+        // Create the depositor's liquidity account if it does not exist
+        create_associated_token_account(
+            payer_info,
+            depositor_info,
+            depositor_account_liquidity_info,
+            mint_liquidity_info,
+            system_program,
+            token_program,
+            associated_token_program,
+        )?;
+    }
+
+    // Mint the liquidity to user
+    let seeds = &[
+        pool_info_data.amm.as_ref(),
+        pool_info_data.mint_a.as_ref(),
+        pool_info_data.mint_b.as_ref(),
+        AUTHORITY_SEED,
+        &[pool_info_data.pool_authority_bump],
+    ];
+    let signer_seeds = &[&seeds[..]];
+    solana_program::program::invoke_signed(
+        &spl_token::instruction::mint_to(
+            &spl_token::id(),
+            mint_liquidity_info.key,
+            depositor_account_liquidity_info.key,
+            pool_authority_info.key,
+            &[pool_authority_info.key],
+            liquidity,
+        )?,
+        &[
+            token_program.clone(),
+            mint_liquidity_info.clone(),
+            depositor_account_liquidity_info.clone(),
+            pool_authority_info.clone(),
+        ],
+        signer_seeds,
+    )?;
+
+    Ok(())
+}

+ 33 - 0
tokens/token-swap/steel/program/src/lib.rs

@@ -0,0 +1,33 @@
+mod create_amm;
+mod create_pool;
+mod deposit_liquidity;
+mod swap;
+mod withdraw_liquidity;
+use create_amm::*;
+use create_pool::*;
+use deposit_liquidity::*;
+use swap::*;
+use withdraw_liquidity::*;
+
+use steel::*;
+use token_swap_api::prelude::*;
+
+pub fn process_instruction(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    data: &[u8],
+) -> ProgramResult {
+    let (ix, data) = parse_instruction(&token_swap_api::ID, program_id, data)?;
+
+    match ix {
+        TokenSwapInstruction::CreateAmm => process_create_amm(accounts, data)?,
+        TokenSwapInstruction::CreatePool => process_create_pool(accounts, data)?,
+        TokenSwapInstruction::DepositLiquidity => process_deposit_liquidity(accounts, data)?,
+        TokenSwapInstruction::WithdrawLiquidity => process_withdraw_liquidity(accounts, data)?,
+        TokenSwapInstruction::Swap => process_swap(accounts, data)?,
+    }
+
+    Ok(())
+}
+
+entrypoint!(process_instruction);

+ 188 - 0
tokens/token-swap/steel/program/src/swap.rs

@@ -0,0 +1,188 @@
+use spl_math::uint::U256;
+use steel::*;
+use token_swap_api::prelude::*;
+pub fn process_swap(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
+    // Load accounts.
+    let [payer_info, trader_info, amm_info, pool_info, pool_authority_info, mint_a_info, mint_b_info, pool_account_a_info, pool_account_b_info, trader_account_a_info, trader_account_b_info, token_program, system_program, associated_token_program] =
+        accounts
+    else {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    };
+
+    let args = Swap::try_from_bytes(data)?;
+    let swap_a: bool = args.swap_a == 1;
+    let input_amount = u64::from_le_bytes(args.input_amount);
+    let min_output_amount = u64::from_le_bytes(args.min_output_amount);
+
+    // Check payer account is signer and program is the correct program.
+    payer_info.is_signer()?;
+    token_program.is_program(&spl_token::ID)?;
+    system_program.is_program(&system_program::ID)?;
+    associated_token_program.is_program(&ASSOCIATED_TOKEN_PROGRAM_ID)?;
+
+    // check if depositor is signer
+    trader_info.is_signer()?;
+
+    // Verify mint_a and mint_b is a mint account.
+    let _mint_a = mint_a_info.as_mint()?;
+    let _mint_b = mint_b_info.as_mint()?;
+
+    // validate pool account
+
+    if pool_info.data_is_empty() {
+        return Err(TokenSwapError::AccountIsNotExisted.into());
+    }
+    validate_pool_account(pool_info, *mint_a_info.key, *mint_b_info.key)?;
+
+    let pool_info_data = pool_info.as_account_mut::<Pool>(&token_swap_api::ID)?;
+
+    // validate pool authority
+    validate_pool_authority(
+        pool_info_data,
+        pool_authority_info,
+        *mint_a_info.key,
+        *mint_b_info.key,
+    )?;
+
+    // Check amm account is owned by token_swap_api::ID.
+    amm_info.has_owner(&token_swap_api::ID)?;
+
+    // // validate pool_account_a_info, pool_account_b_info
+    let pool_account_a = pool_account_a_info
+        .is_writable()?
+        .as_associated_token_account(pool_authority_info.key, mint_a_info.key)?;
+    let pool_account_b = pool_account_b_info
+        .is_writable()?
+        .as_associated_token_account(pool_authority_info.key, mint_b_info.key)?;
+
+    // check if trader_account_a and trader_account_b is exists
+
+    if trader_account_a_info.data_is_empty() {
+        // Create the depositor's liquidity account if it does not exist
+        create_associated_token_account(
+            payer_info,
+            trader_info,
+            trader_account_a_info,
+            mint_a_info,
+            system_program,
+            token_program,
+            associated_token_program,
+        )?;
+    }
+
+    if trader_account_b_info.data_is_empty() {
+        // Create the depositor's liquidity account if it does not exist
+        create_associated_token_account(
+            payer_info,
+            trader_info,
+            trader_account_b_info,
+            mint_b_info,
+            system_program,
+            token_program,
+            associated_token_program,
+        )?;
+    }
+
+    let trader_account_a =
+        trader_account_a_info.as_associated_token_account(trader_info.key, mint_a_info.key)?;
+    let trader_account_b =
+        trader_account_b_info.as_associated_token_account(trader_info.key, mint_b_info.key)?;
+
+    // Prevent depositing assets the depositor does not own
+    let input = if swap_a && input_amount > trader_account_a.amount {
+        trader_account_a.amount
+    } else if !swap_a && input_amount > trader_account_b.amount {
+        trader_account_b.amount
+    } else {
+        input_amount
+    };
+
+    // Apply trading fee, used to compute the output
+    let amm = amm_info.as_account_mut::<Amm>(&token_swap_api::ID)?;
+    let fee = u16::from_le_bytes(amm.fee);
+    let taxed_input = input - input * (fee as u64) / 10000;
+
+    let output = if swap_a {
+        U256::from(taxed_input)
+            .checked_mul(U256::from(pool_account_b.amount))
+            .unwrap()
+            .checked_div(
+                U256::from(pool_account_a.amount)
+                    .checked_add(U256::from(taxed_input))
+                    .unwrap(),
+            )
+            .unwrap()
+    } else {
+        U256::from(taxed_input)
+            .checked_mul(U256::from(pool_account_a.amount))
+            .unwrap()
+            .checked_div(
+                U256::from(pool_account_b.amount)
+                    .checked_add(U256::from(taxed_input))
+                    .unwrap(),
+            )
+            .unwrap()
+    }
+    .as_u64();
+
+    if output < min_output_amount {
+        return Err(TokenSwapError::OutputTooSmall.into());
+    }
+
+    let pool_authority_seeds = &[
+        pool_info_data.amm.as_ref(),
+        pool_info_data.mint_a.as_ref(),
+        pool_info_data.mint_b.as_ref(),
+        AUTHORITY_SEED,
+    ];
+
+    // // Compute the invariant before the trade
+    let invariant = pool_account_a.amount * pool_account_b.amount;
+
+    if swap_a {
+        transfer(
+            trader_info,
+            trader_account_a_info,
+            pool_account_a_info,
+            token_program,
+            input,
+        )?;
+        transfer_signed_with_bump(
+            pool_authority_info,
+            pool_account_b_info,
+            trader_account_b_info,
+            token_program,
+            output,
+            pool_authority_seeds,
+            pool_info_data.pool_authority_bump,
+        )?;
+    } else {
+        transfer_signed_with_bump(
+            pool_authority_info,
+            pool_account_a_info,
+            trader_account_a_info,
+            token_program,
+            input,
+            pool_authority_seeds,
+            pool_info_data.pool_authority_bump,
+        )?;
+        transfer(
+            trader_info,
+            trader_account_b_info,
+            pool_account_b_info,
+            token_program,
+            output,
+        )?;
+    }
+
+    let pool_account_a = pool_account_a_info
+        .as_associated_token_account(pool_authority_info.key, mint_a_info.key)?;
+    let pool_account_b = pool_account_b_info
+        .as_associated_token_account(pool_authority_info.key, mint_b_info.key)?;
+
+    if invariant > pool_account_a.amount * pool_account_b.amount {
+        return Err(TokenSwapError::InvariantViolated.into());
+    }
+
+    Ok(())
+}

+ 123 - 0
tokens/token-swap/steel/program/src/withdraw_liquidity.rs

@@ -0,0 +1,123 @@
+use spl_math::uint::U256;
+use steel::*;
+use token_swap_api::prelude::*;
+pub fn process_withdraw_liquidity(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
+    // Load accounts.
+    let [payer_info, depositor_info, pool_info, pool_authority_info, mint_liquidity_info, mint_a_info, mint_b_info, pool_account_a_info, pool_account_b_info, depositor_account_liquidity_info, depositor_account_a_info, depositor_account_b_info, token_program, system_program, associated_token_program] =
+        accounts
+    else {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    };
+
+    let args = WithdrawLiquidity::try_from_bytes(data)?;
+    let amount = u64::from_le_bytes(args.amount);
+
+    // Check payer account is signer and program is the correct program.
+    payer_info.is_signer()?;
+    token_program.is_program(&spl_token::ID)?;
+    system_program.is_program(&system_program::ID)?;
+    associated_token_program.is_program(&ASSOCIATED_TOKEN_PROGRAM_ID)?;
+
+    // check if depositor is signer
+    depositor_info.is_signer()?;
+
+    // Verify mint_a and mint_b is a mint account.
+    let _mint_a = mint_a_info.as_mint()?;
+    let _mint_b = mint_b_info.as_mint()?;
+
+    // validate pool account
+
+    if pool_info.data_is_empty() {
+        return Err(TokenSwapError::AccountIsNotExisted.into());
+    }
+    validate_pool_account(pool_info, *mint_a_info.key, *mint_b_info.key)?;
+
+    let pool_info_data = pool_info.as_account_mut::<Pool>(&token_swap_api::ID)?;
+
+    // validate pool authority
+    validate_pool_authority(
+        pool_info_data,
+        pool_authority_info,
+        *mint_a_info.key,
+        *mint_b_info.key,
+    )?;
+
+    // validate mint liquidity
+    validate_mint_liquidity(
+        pool_info_data,
+        mint_liquidity_info,
+        *mint_a_info.key,
+        *mint_b_info.key,
+    )?;
+
+    // // validate pool_account_a_info, pool_account_b_info
+    let pool_account_a = pool_account_a_info
+        .is_writable()?
+        .as_associated_token_account(pool_authority_info.key, mint_a_info.key)?;
+    let pool_account_b = pool_account_b_info
+        .is_writable()?
+        .as_associated_token_account(pool_authority_info.key, mint_b_info.key)?;
+
+    // validate depositor_account_a_info and depositor_account_b_info
+    let _depositor_account_a = depositor_account_a_info
+        .is_writable()?
+        .as_associated_token_account(depositor_info.key, mint_a_info.key)?;
+    let _depositor_account_b = depositor_account_b_info
+        .is_writable()?
+        .as_associated_token_account(depositor_info.key, mint_b_info.key)?;
+
+    let pool_authority_seeds = &[
+        pool_info_data.amm.as_ref(),
+        pool_info_data.mint_a.as_ref(),
+        pool_info_data.mint_b.as_ref(),
+        AUTHORITY_SEED,
+    ];
+
+    // Transfer tokens from the pool
+    let mint_liquidity = mint_liquidity_info.as_mint()?;
+    let amount_a = U256::from(amount)
+        .checked_mul(U256::from(pool_account_a.amount))
+        .unwrap()
+        .checked_div(U256::from(mint_liquidity.supply + MINIMUM_LIQUIDITY))
+        .unwrap()
+        .as_u64();
+
+    transfer_signed_with_bump(
+        pool_authority_info,
+        pool_account_a_info,
+        depositor_account_a_info,
+        token_program,
+        amount_a,
+        pool_authority_seeds,
+        pool_info_data.pool_authority_bump,
+    )?;
+
+    let amount_b = U256::from(amount)
+        .checked_mul(U256::from(pool_account_b.amount))
+        .unwrap()
+        .checked_div(U256::from(mint_liquidity.supply + MINIMUM_LIQUIDITY))
+        .unwrap()
+        .as_u64();
+
+    transfer_signed_with_bump(
+        pool_authority_info,
+        pool_account_b_info,
+        depositor_account_b_info,
+        token_program,
+        amount_b,
+        pool_authority_seeds,
+        pool_info_data.pool_authority_bump,
+    )?;
+
+    // Burn the liquidity tokens
+    // It will fail if the amount is invalid
+    burn(
+        depositor_account_liquidity_info,
+        mint_liquidity_info,
+        depositor_info,
+        token_program,
+        amount,
+    )?;
+
+    Ok(())
+}

+ 60 - 0
tokens/token-swap/steel/program/tests/test.rs

@@ -0,0 +1,60 @@
+use solana_program::hash::Hash;
+use solana_program_test::{processor, BanksClient, ProgramTest};
+use solana_sdk::{
+    native_token::LAMPORTS_PER_SOL, signature::Keypair, signer::Signer, system_instruction,
+    transaction::Transaction,
+};
+use steel::*;
+use token_swap_api::prelude::*;
+
+async fn setup() -> (BanksClient, Keypair, Hash) {
+    let mut program_test = ProgramTest::new(
+        "token_swap_program",
+        token_swap_api::ID,
+        processor!(token_swap_program::process_instruction),
+    );
+    program_test.prefer_bpf(true);
+    program_test.start().await
+}
+
+#[tokio::test]
+async fn run_test() {
+    // Setup test
+    let (mut banks, payer, blockhash) = setup().await;
+
+    let admin = Keypair::new();
+    let id = Keypair::new();
+    let fee = 1000; // 10%
+
+    // // create admin account
+    // let tx = Transaction::new_signed_with_payer(
+    //     &[system_instruction::create_account(
+    //         &payer.pubkey(),
+    //         &admin.pubkey(),
+    //         1 * LAMPORTS_PER_SOL,
+    //         0,
+    //         &token_swap_api::ID,
+    //     )],
+    //     Some(&payer.pubkey()),
+    //     &[&payer, &admin],
+    //     blockhash,
+    // );
+
+    // let res = banks.process_transaction(tx).await;
+    // assert!(res.is_ok());
+
+    // Submit initialize transaction.
+    let ix = create_amm(payer.pubkey(), admin.pubkey(), id.pubkey(), fee);
+    let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash);
+    let res = banks.process_transaction(tx).await;
+
+    assert!(res.is_ok());
+
+    let amm_address = amm_pda(id.pubkey()).0;
+    let amm_account = banks.get_account(amm_address).await.unwrap().unwrap();
+    let amm = Amm::try_from_bytes(&amm_account.data).unwrap();
+    assert_eq!(amm_account.owner, token_swap_api::ID);
+    assert_eq!(amm.id, id.pubkey());
+    assert_eq!(amm.admin, admin.pubkey());
+    assert_eq!(u16::from_le_bytes(amm.fee), fee);
+}

+ 294 - 0
tokens/token-swap/steel/tests/create_pool_and_swap.test.ts

@@ -0,0 +1,294 @@
+import { Connection, Keypair, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY, Transaction, TransactionInstruction } from '@solana/web3.js';
+import { AccountLayout, ASSOCIATED_TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, MintLayout, TOKEN_PROGRAM_ID } from '@solana/spl-token';
+import { assert } from 'chai';
+import { describe, it } from 'mocha';
+import { BanksClient, ProgramTestContext, start } from 'solana-bankrun';
+import {
+  createAMint,
+  deserializeAmmAccount,
+  deserializePoolAccount,
+  getCreateAmmInstructionData,
+  getCreatePoolInstructionData,
+  getDepositLiquidityInstructionData,
+  getSwapInstructionData,
+  mintTo,
+} from './utils';
+
+const PROGRAM_ID = new PublicKey('z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35');
+
+describe('Token Swap Program: Create and swap', () => {
+  let context: ProgramTestContext;
+  let client: BanksClient;
+  let payer: Keypair;
+  const mint_a = Keypair.generate();
+  const mint_b = Keypair.generate();
+  const admin = Keypair.generate();
+  const trader = Keypair.generate();
+  const fee = 500; // 5%
+
+  const id = Keypair.generate();
+  const [ammPda] = PublicKey.findProgramAddressSync([id.publicKey.toBuffer()], PROGRAM_ID);
+
+  const [poolPda] = PublicKey.findProgramAddressSync([ammPda.toBuffer(), mint_a.publicKey.toBuffer(), mint_b.publicKey.toBuffer()], PROGRAM_ID);
+
+  const [poolAuthorityPda] = PublicKey.findProgramAddressSync(
+    [ammPda.toBuffer(), mint_a.publicKey.toBuffer(), mint_b.publicKey.toBuffer(), Buffer.from('authority')],
+    PROGRAM_ID,
+  );
+
+  const [mintLiquidityPda] = PublicKey.findProgramAddressSync(
+    [ammPda.toBuffer(), mint_a.publicKey.toBuffer(), mint_b.publicKey.toBuffer(), Buffer.from('liquidity')],
+    PROGRAM_ID,
+  );
+
+  const poolAccountA = getAssociatedTokenAddressSync(mint_a.publicKey, poolAuthorityPda, true);
+
+  const poolAccountB = getAssociatedTokenAddressSync(mint_b.publicKey, poolAuthorityPda, true);
+
+  let depositorAccountLp: PublicKey;
+  let depositorAccountA: PublicKey;
+  let depositorAccountB: PublicKey;
+  let traderAccountA: PublicKey;
+  let traderAccountB: PublicKey;
+
+  const MINIMUM_LIQUIDITY = 100;
+
+  const amountA = BigInt(4 * 10 ** 9);
+  const amountB = BigInt(1 * 10 ** 9);
+  const amountLp = BigInt(Math.sqrt(Number(amountA) * Number(amountB)) - MINIMUM_LIQUIDITY);
+
+  before(async () => {
+    context = await start([{ name: 'token_swap_program', programId: PROGRAM_ID }], []);
+    client = context.banksClient;
+    payer = context.payer;
+    console.log(mint_a.publicKey.toBase58(), payer.publicKey.toBase58());
+    await createAMint(context, payer, mint_a);
+    await createAMint(context, payer, mint_b);
+
+    depositorAccountLp = getAssociatedTokenAddressSync(mintLiquidityPda, payer.publicKey, false);
+
+    depositorAccountA = getAssociatedTokenAddressSync(mint_a.publicKey, payer.publicKey, false);
+
+    depositorAccountB = getAssociatedTokenAddressSync(mint_b.publicKey, payer.publicKey, false);
+
+    traderAccountA = getAssociatedTokenAddressSync(mint_a.publicKey, trader.publicKey, false);
+
+    traderAccountB = getAssociatedTokenAddressSync(mint_b.publicKey, trader.publicKey, false);
+
+    await mintTo(context, payer, payer.publicKey, mint_a.publicKey);
+    await mintTo(context, payer, payer.publicKey, mint_b.publicKey);
+    await mintTo(context, payer, trader.publicKey, mint_a.publicKey);
+    await mintTo(context, payer, trader.publicKey, mint_b.publicKey);
+  });
+
+  it('Should create a new amm successfully', async () => {
+    const tx = new Transaction();
+    tx.add(
+      new TransactionInstruction({
+        programId: PROGRAM_ID,
+        keys: [
+          { pubkey: payer.publicKey, isSigner: true, isWritable: true },
+          { pubkey: admin.publicKey, isSigner: false, isWritable: false },
+          { pubkey: ammPda, isSigner: false, isWritable: true },
+          {
+            pubkey: SystemProgram.programId,
+            isSigner: false,
+            isWritable: false,
+          },
+        ],
+        data: getCreateAmmInstructionData(id.publicKey, fee),
+      }),
+    );
+    tx.recentBlockhash = context.lastBlockhash;
+    tx.sign(payer);
+
+    // process the transaction
+    await client.processTransaction(tx);
+
+    const ammAccount = await client.getAccount(ammPda);
+    assert.isNotNull(ammAccount);
+    assert.equal(ammAccount?.owner.toBase58(), PROGRAM_ID.toBase58());
+    const ammAccountData = deserializeAmmAccount(ammAccount.data);
+
+    assert.equal(ammAccountData.id.toBase58(), id.publicKey.toBase58());
+    assert.equal(ammAccountData.admin.toBase58(), admin.publicKey.toBase58());
+    assert.equal(ammAccountData.fee, fee);
+  });
+
+  it('Should create a new pool successfully', async () => {
+    const tx = new Transaction();
+    tx.add(
+      new TransactionInstruction({
+        programId: PROGRAM_ID,
+        keys: [
+          { pubkey: payer.publicKey, isSigner: true, isWritable: true },
+          { pubkey: ammPda, isSigner: false, isWritable: false },
+          { pubkey: poolPda, isSigner: false, isWritable: true },
+          { pubkey: poolAuthorityPda, isSigner: false, isWritable: false },
+          { pubkey: mintLiquidityPda, isSigner: false, isWritable: true },
+          { pubkey: mint_a.publicKey, isSigner: false, isWritable: false },
+          { pubkey: mint_b.publicKey, isSigner: false, isWritable: false },
+          { pubkey: poolAccountA, isSigner: false, isWritable: true },
+          { pubkey: poolAccountB, isSigner: false, isWritable: true },
+          {
+            pubkey: TOKEN_PROGRAM_ID,
+            isSigner: false,
+            isWritable: false,
+          },
+          {
+            pubkey: SystemProgram.programId,
+            isSigner: false,
+            isWritable: false,
+          },
+          {
+            pubkey: ASSOCIATED_TOKEN_PROGRAM_ID,
+            isSigner: false,
+            isWritable: false,
+          },
+          {
+            pubkey: SYSVAR_RENT_PUBKEY,
+            isSigner: false,
+            isWritable: false,
+          },
+        ],
+        data: getCreatePoolInstructionData(),
+      }),
+    );
+    tx.recentBlockhash = context.lastBlockhash;
+    tx.sign(payer);
+
+    // process the transaction
+    await client.processTransaction(tx);
+
+    const poolPdaAccount = await client.getAccount(poolPda);
+    assert.isNotNull(poolPdaAccount);
+    assert.equal(poolPdaAccount?.owner.toBase58(), PROGRAM_ID.toBase58());
+
+    const data = deserializePoolAccount(poolPdaAccount.data);
+    assert.equal(data.amm.toBase58(), ammPda.toBase58());
+    assert.equal(data.mintA.toBase58(), mint_a.publicKey.toBase58());
+    assert.equal(data.mintB.toBase58(), mint_b.publicKey.toBase58());
+  });
+
+  it('Should deposit liquidity successfully', async () => {
+    const tx = new Transaction();
+    tx.add(
+      new TransactionInstruction({
+        programId: PROGRAM_ID,
+        keys: [
+          { pubkey: payer.publicKey, isSigner: true, isWritable: true },
+          { pubkey: payer.publicKey, isSigner: true, isWritable: true },
+          { pubkey: poolPda, isSigner: false, isWritable: true },
+          { pubkey: poolAuthorityPda, isSigner: false, isWritable: false },
+          { pubkey: mintLiquidityPda, isSigner: false, isWritable: true },
+          { pubkey: mint_a.publicKey, isSigner: false, isWritable: false },
+          { pubkey: mint_b.publicKey, isSigner: false, isWritable: false },
+          { pubkey: poolAccountA, isSigner: false, isWritable: true },
+          { pubkey: poolAccountB, isSigner: false, isWritable: true },
+          { pubkey: depositorAccountLp, isSigner: false, isWritable: true },
+          { pubkey: depositorAccountA, isSigner: false, isWritable: true },
+          { pubkey: depositorAccountB, isSigner: false, isWritable: true },
+          {
+            pubkey: TOKEN_PROGRAM_ID,
+            isSigner: false,
+            isWritable: false,
+          },
+          {
+            pubkey: SystemProgram.programId,
+            isSigner: false,
+            isWritable: false,
+          },
+          {
+            pubkey: ASSOCIATED_TOKEN_PROGRAM_ID,
+            isSigner: false,
+            isWritable: false,
+          },
+        ],
+        data: getDepositLiquidityInstructionData(amountA, amountB),
+      }),
+    );
+    tx.recentBlockhash = context.lastBlockhash;
+    tx.sign(payer);
+
+    // process the transaction
+    await client.processTransaction(tx);
+
+    const rawDepositorAccountLp = await client.getAccount(depositorAccountLp);
+    assert.isNotNull(rawDepositorAccountLp);
+    const decodedDepositorAccountLp = AccountLayout.decode(rawDepositorAccountLp?.data);
+    assert.equal(decodedDepositorAccountLp.amount, amountLp);
+
+    const expectedLpAmount = Math.sqrt(Number(amountA) * Number(amountB)) - MINIMUM_LIQUIDITY;
+
+    const rawLiquidityMint = await client.getAccount(mintLiquidityPda);
+    assert.isNotNull(rawLiquidityMint);
+    const decodedLiquidityMint = MintLayout.decode(rawLiquidityMint.data);
+    assert.equal(decodedLiquidityMint.supply, BigInt(expectedLpAmount));
+
+    const rawPoolAccountA = await client.getAccount(poolAccountA);
+    assert.isNotNull(rawPoolAccountA);
+    const decodedPoolAccountA = AccountLayout.decode(rawPoolAccountA?.data);
+    assert.equal(decodedPoolAccountA.amount, amountA);
+
+    const rawPoolAccountB = await client.getAccount(poolAccountB);
+    assert.isNotNull(rawPoolAccountB);
+    const decodedPoolAccountB = AccountLayout.decode(rawPoolAccountB?.data);
+    assert.equal(decodedPoolAccountB.amount, amountB);
+  });
+
+  it('Should swap successfully', async () => {
+    const swapAmount = BigInt(10 ** 9);
+    const minimunAmountOut = BigInt(100);
+    const tx = new Transaction();
+    tx.add(
+      new TransactionInstruction({
+        programId: PROGRAM_ID,
+        keys: [
+          { pubkey: payer.publicKey, isSigner: true, isWritable: true },
+          { pubkey: trader.publicKey, isSigner: true, isWritable: true },
+          { pubkey: ammPda, isSigner: false, isWritable: true },
+          { pubkey: poolPda, isSigner: false, isWritable: true },
+          { pubkey: poolAuthorityPda, isSigner: false, isWritable: false },
+          { pubkey: mint_a.publicKey, isSigner: false, isWritable: false },
+          { pubkey: mint_b.publicKey, isSigner: false, isWritable: false },
+          { pubkey: poolAccountA, isSigner: false, isWritable: true },
+          { pubkey: poolAccountB, isSigner: false, isWritable: true },
+          { pubkey: traderAccountA, isSigner: false, isWritable: true },
+          { pubkey: traderAccountB, isSigner: false, isWritable: true },
+          {
+            pubkey: TOKEN_PROGRAM_ID,
+            isSigner: false,
+            isWritable: false,
+          },
+          {
+            pubkey: SystemProgram.programId,
+            isSigner: false,
+            isWritable: false,
+          },
+          {
+            pubkey: ASSOCIATED_TOKEN_PROGRAM_ID,
+            isSigner: false,
+            isWritable: false,
+          },
+        ],
+        data: getSwapInstructionData(true, swapAmount, minimunAmountOut),
+      }),
+    );
+    tx.recentBlockhash = context.lastBlockhash;
+    tx.sign(payer, trader);
+
+    // process the transaction
+    await client.processTransaction(tx);
+
+    const rawTraderAccountA = await client.getAccount(traderAccountA);
+    assert.isNotNull(rawTraderAccountA);
+    const decodedTraderAccountA = AccountLayout.decode(rawTraderAccountA?.data);
+
+    assert.equal(decodedTraderAccountA.amount, BigInt(999000000000));
+
+    const rawTraderAccountB = await client.getAccount(traderAccountB);
+    assert.isNotNull(rawTraderAccountB);
+    const decodedTraderAccountB = AccountLayout.decode(rawTraderAccountB?.data);
+    assert.equal(decodedTraderAccountB.amount, BigInt(1000191919191));
+  });
+});

+ 313 - 0
tokens/token-swap/steel/tests/create_pool_and_withdraw_all_liquid.test.ts

@@ -0,0 +1,313 @@
+import { Connection, Keypair, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY, Transaction, TransactionInstruction } from '@solana/web3.js';
+import { AccountLayout, ASSOCIATED_TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, MintLayout, TOKEN_PROGRAM_ID } from '@solana/spl-token';
+import { assert } from 'chai';
+import { describe, it } from 'mocha';
+import { BanksClient, ProgramTestContext, start } from 'solana-bankrun';
+import {
+  createAMint,
+  deserializeAmmAccount,
+  deserializePoolAccount,
+  getCreateAmmInstructionData,
+  getCreatePoolInstructionData,
+  getDepositLiquidityInstructionData,
+  getWithdrawLiquidityInstructionData,
+  mintTo,
+} from './utils';
+
+const PROGRAM_ID = new PublicKey('z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35');
+
+describe('Token Swap Program: Create and withdraw all liquidity', () => {
+  let context: ProgramTestContext;
+  let client: BanksClient;
+  let payer: Keypair;
+  const mint_a = Keypair.generate();
+  const mint_b = Keypair.generate();
+  const admin = Keypair.generate();
+  const trader = Keypair.generate();
+  const fee = 500; // 5%
+
+  const id = Keypair.generate();
+  const [ammPda] = PublicKey.findProgramAddressSync([id.publicKey.toBuffer()], PROGRAM_ID);
+
+  const [poolPda] = PublicKey.findProgramAddressSync([ammPda.toBuffer(), mint_a.publicKey.toBuffer(), mint_b.publicKey.toBuffer()], PROGRAM_ID);
+
+  const [poolAuthorityPda] = PublicKey.findProgramAddressSync(
+    [ammPda.toBuffer(), mint_a.publicKey.toBuffer(), mint_b.publicKey.toBuffer(), Buffer.from('authority')],
+    PROGRAM_ID,
+  );
+
+  const [mintLiquidityPda] = PublicKey.findProgramAddressSync(
+    [ammPda.toBuffer(), mint_a.publicKey.toBuffer(), mint_b.publicKey.toBuffer(), Buffer.from('liquidity')],
+    PROGRAM_ID,
+  );
+
+  const poolAccountA = getAssociatedTokenAddressSync(mint_a.publicKey, poolAuthorityPda, true);
+
+  const poolAccountB = getAssociatedTokenAddressSync(mint_b.publicKey, poolAuthorityPda, true);
+
+  let depositorAccountLp: PublicKey;
+  let depositorAccountA: PublicKey;
+  let depositorAccountB: PublicKey;
+  let traderAccountA: PublicKey;
+  let traderAccountB: PublicKey;
+
+  const MINIMUM_LIQUIDITY = 100;
+
+  const amountA = BigInt(4 * 10 ** 9);
+  const amountB = BigInt(1 * 10 ** 9);
+  const amountLp = BigInt(Math.sqrt(Number(amountA) * Number(amountB)) - MINIMUM_LIQUIDITY);
+
+  before(async () => {
+    context = await start([{ name: 'token_swap_program', programId: PROGRAM_ID }], []);
+    client = context.banksClient;
+    payer = context.payer;
+    console.log(mint_a.publicKey.toBase58(), payer.publicKey.toBase58());
+    await createAMint(context, payer, mint_a);
+    await createAMint(context, payer, mint_b);
+
+    depositorAccountLp = getAssociatedTokenAddressSync(mintLiquidityPda, payer.publicKey, false);
+
+    depositorAccountA = getAssociatedTokenAddressSync(mint_a.publicKey, payer.publicKey, false);
+
+    depositorAccountB = getAssociatedTokenAddressSync(mint_b.publicKey, payer.publicKey, false);
+
+    traderAccountA = getAssociatedTokenAddressSync(mint_a.publicKey, trader.publicKey, false);
+
+    traderAccountB = getAssociatedTokenAddressSync(mint_b.publicKey, trader.publicKey, false);
+
+    await mintTo(context, payer, payer.publicKey, mint_a.publicKey);
+    await mintTo(context, payer, payer.publicKey, mint_b.publicKey);
+    await mintTo(context, payer, trader.publicKey, mint_a.publicKey);
+    await mintTo(context, payer, trader.publicKey, mint_b.publicKey);
+  });
+
+  it('Should create a new amm successfully', async () => {
+    const tx = new Transaction();
+    tx.add(
+      new TransactionInstruction({
+        programId: PROGRAM_ID,
+        keys: [
+          { pubkey: payer.publicKey, isSigner: true, isWritable: true },
+          { pubkey: admin.publicKey, isSigner: false, isWritable: false },
+          { pubkey: ammPda, isSigner: false, isWritable: true },
+          {
+            pubkey: SystemProgram.programId,
+            isSigner: false,
+            isWritable: false,
+          },
+        ],
+        data: getCreateAmmInstructionData(id.publicKey, fee),
+      }),
+    );
+    tx.recentBlockhash = context.lastBlockhash;
+    tx.sign(payer);
+
+    // process the transaction
+    await client.processTransaction(tx);
+
+    const ammAccount = await client.getAccount(ammPda);
+    assert.isNotNull(ammAccount);
+    assert.equal(ammAccount?.owner.toBase58(), PROGRAM_ID.toBase58());
+    const ammAccountData = deserializeAmmAccount(ammAccount.data);
+
+    assert.equal(ammAccountData.id.toBase58(), id.publicKey.toBase58());
+    assert.equal(ammAccountData.admin.toBase58(), admin.publicKey.toBase58());
+    assert.equal(ammAccountData.fee, fee);
+  });
+
+  it('Should create a new pool successfully', async () => {
+    const tx = new Transaction();
+    tx.add(
+      new TransactionInstruction({
+        programId: PROGRAM_ID,
+        keys: [
+          { pubkey: payer.publicKey, isSigner: true, isWritable: true },
+          { pubkey: ammPda, isSigner: false, isWritable: false },
+          { pubkey: poolPda, isSigner: false, isWritable: true },
+          { pubkey: poolAuthorityPda, isSigner: false, isWritable: false },
+          { pubkey: mintLiquidityPda, isSigner: false, isWritable: true },
+          { pubkey: mint_a.publicKey, isSigner: false, isWritable: false },
+          { pubkey: mint_b.publicKey, isSigner: false, isWritable: false },
+          { pubkey: poolAccountA, isSigner: false, isWritable: true },
+          { pubkey: poolAccountB, isSigner: false, isWritable: true },
+          {
+            pubkey: TOKEN_PROGRAM_ID,
+            isSigner: false,
+            isWritable: false,
+          },
+          {
+            pubkey: SystemProgram.programId,
+            isSigner: false,
+            isWritable: false,
+          },
+          {
+            pubkey: ASSOCIATED_TOKEN_PROGRAM_ID,
+            isSigner: false,
+            isWritable: false,
+          },
+          {
+            pubkey: SYSVAR_RENT_PUBKEY,
+            isSigner: false,
+            isWritable: false,
+          },
+        ],
+        data: getCreatePoolInstructionData(),
+      }),
+    );
+    tx.recentBlockhash = context.lastBlockhash;
+    tx.sign(payer);
+
+    // process the transaction
+    await client.processTransaction(tx);
+
+    const poolPdaAccount = await client.getAccount(poolPda);
+    assert.isNotNull(poolPdaAccount);
+    assert.equal(poolPdaAccount?.owner.toBase58(), PROGRAM_ID.toBase58());
+
+    const data = deserializePoolAccount(poolPdaAccount.data);
+    assert.equal(data.amm.toBase58(), ammPda.toBase58());
+    assert.equal(data.mintA.toBase58(), mint_a.publicKey.toBase58());
+    assert.equal(data.mintB.toBase58(), mint_b.publicKey.toBase58());
+  });
+
+  it('Should deposit liquidity successfully', async () => {
+    const tx = new Transaction();
+    tx.add(
+      new TransactionInstruction({
+        programId: PROGRAM_ID,
+        keys: [
+          { pubkey: payer.publicKey, isSigner: true, isWritable: true },
+          { pubkey: payer.publicKey, isSigner: true, isWritable: true },
+          { pubkey: poolPda, isSigner: false, isWritable: true },
+          { pubkey: poolAuthorityPda, isSigner: false, isWritable: false },
+          { pubkey: mintLiquidityPda, isSigner: false, isWritable: true },
+          { pubkey: mint_a.publicKey, isSigner: false, isWritable: false },
+          { pubkey: mint_b.publicKey, isSigner: false, isWritable: false },
+          { pubkey: poolAccountA, isSigner: false, isWritable: true },
+          { pubkey: poolAccountB, isSigner: false, isWritable: true },
+          { pubkey: depositorAccountLp, isSigner: false, isWritable: true },
+          { pubkey: depositorAccountA, isSigner: false, isWritable: true },
+          { pubkey: depositorAccountB, isSigner: false, isWritable: true },
+          {
+            pubkey: TOKEN_PROGRAM_ID,
+            isSigner: false,
+            isWritable: false,
+          },
+          {
+            pubkey: SystemProgram.programId,
+            isSigner: false,
+            isWritable: false,
+          },
+          {
+            pubkey: ASSOCIATED_TOKEN_PROGRAM_ID,
+            isSigner: false,
+            isWritable: false,
+          },
+        ],
+        data: getDepositLiquidityInstructionData(amountA, amountB),
+      }),
+    );
+    tx.recentBlockhash = context.lastBlockhash;
+    tx.sign(payer);
+
+    // process the transaction
+    await client.processTransaction(tx);
+
+    const rawDepositorAccountLp = await client.getAccount(depositorAccountLp);
+    assert.isNotNull(rawDepositorAccountLp);
+    const decodedDepositorAccountLp = AccountLayout.decode(rawDepositorAccountLp?.data);
+    assert.equal(decodedDepositorAccountLp.amount, amountLp);
+
+    const expectedLpAmount = Math.sqrt(Number(amountA) * Number(amountB)) - MINIMUM_LIQUIDITY;
+
+    const rawLiquidityMint = await client.getAccount(mintLiquidityPda);
+    assert.isNotNull(rawLiquidityMint);
+    const decodedLiquidityMint = MintLayout.decode(rawLiquidityMint.data);
+    assert.equal(decodedLiquidityMint.supply, BigInt(expectedLpAmount));
+
+    const rawPoolAccountA = await client.getAccount(poolAccountA);
+    assert.isNotNull(rawPoolAccountA);
+    const decodedPoolAccountA = AccountLayout.decode(rawPoolAccountA?.data);
+    assert.equal(decodedPoolAccountA.amount, amountA);
+
+    const rawPoolAccountB = await client.getAccount(poolAccountB);
+    assert.isNotNull(rawPoolAccountB);
+    const decodedPoolAccountB = AccountLayout.decode(rawPoolAccountB?.data);
+    assert.equal(decodedPoolAccountB.amount, amountB);
+  });
+
+  it('Should withdraw all liquidity successfully', async () => {
+    const pre_rawLiquidityMint = await client.getAccount(mintLiquidityPda);
+    assert.isNotNull(pre_rawLiquidityMint);
+    const decodedPreLiquidityMint = MintLayout.decode(pre_rawLiquidityMint.data);
+    const expectedPostPoolAccountAAmount = BigInt(
+      Number(amountA) - (Number(amountLp) * Number(amountA)) / (Number(decodedPreLiquidityMint.supply) + MINIMUM_LIQUIDITY),
+    );
+
+    const expectedPostPoolAccountBAmount = BigInt(
+      Number(amountB) - (Number(amountLp) * Number(amountB)) / (Number(decodedPreLiquidityMint.supply) + MINIMUM_LIQUIDITY),
+    );
+
+    const tx = new Transaction();
+    tx.add(
+      new TransactionInstruction({
+        programId: PROGRAM_ID,
+        keys: [
+          { pubkey: payer.publicKey, isSigner: true, isWritable: true },
+          { pubkey: payer.publicKey, isSigner: true, isWritable: true },
+          { pubkey: poolPda, isSigner: false, isWritable: true },
+          { pubkey: poolAuthorityPda, isSigner: false, isWritable: false },
+          { pubkey: mintLiquidityPda, isSigner: false, isWritable: true },
+          { pubkey: mint_a.publicKey, isSigner: false, isWritable: false },
+          { pubkey: mint_b.publicKey, isSigner: false, isWritable: false },
+          { pubkey: poolAccountA, isSigner: false, isWritable: true },
+          { pubkey: poolAccountB, isSigner: false, isWritable: true },
+          { pubkey: depositorAccountLp, isSigner: false, isWritable: true },
+          { pubkey: depositorAccountA, isSigner: false, isWritable: true },
+          { pubkey: depositorAccountB, isSigner: false, isWritable: true },
+          {
+            pubkey: TOKEN_PROGRAM_ID,
+            isSigner: false,
+            isWritable: false,
+          },
+          {
+            pubkey: SystemProgram.programId,
+            isSigner: false,
+            isWritable: false,
+          },
+          {
+            pubkey: ASSOCIATED_TOKEN_PROGRAM_ID,
+            isSigner: false,
+            isWritable: false,
+          },
+        ],
+        data: getWithdrawLiquidityInstructionData(amountLp),
+      }),
+    );
+    tx.recentBlockhash = context.lastBlockhash;
+    tx.sign(payer);
+
+    // process the transaction
+    await client.processTransaction(tx);
+
+    const rawDepositorAccountLp = await client.getAccount(depositorAccountLp);
+    assert.isNotNull(rawDepositorAccountLp);
+    const decodedDepositorAccountLp = AccountLayout.decode(rawDepositorAccountLp?.data);
+    assert.equal(decodedDepositorAccountLp.amount, BigInt(0));
+
+    const rawLiquidityMint = await client.getAccount(mintLiquidityPda);
+    assert.isNotNull(rawLiquidityMint);
+    const decodedLiquidityMint = MintLayout.decode(rawLiquidityMint.data);
+    assert.equal(decodedLiquidityMint.supply, BigInt(0));
+
+    const rawPoolAccountA = await client.getAccount(poolAccountA);
+    assert.isNotNull(rawPoolAccountA);
+    const decodedPoolAccountA = AccountLayout.decode(rawPoolAccountA?.data);
+    assert.equal(decodedPoolAccountA.amount, expectedPostPoolAccountAAmount);
+
+    const rawPoolAccountB = await client.getAccount(poolAccountB);
+    assert.isNotNull(rawPoolAccountB);
+    const decodedPoolAccountB = AccountLayout.decode(rawPoolAccountB?.data);
+    assert.equal(decodedPoolAccountB.amount, expectedPostPoolAccountBAmount);
+  });
+});

+ 149 - 0
tokens/token-swap/steel/tests/utils.ts

@@ -0,0 +1,149 @@
+import {
+  MINT_SIZE,
+  TOKEN_PROGRAM_ID,
+  createAssociatedTokenAccountInstruction,
+  createInitializeMint2Instruction,
+  createMintToInstruction,
+  getAssociatedTokenAddressSync,
+} from '@solana/spl-token';
+import { Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram, Transaction } from '@solana/web3.js';
+import * as borsh from 'borsh';
+import { ProgramTestContext } from 'solana-bankrun';
+
+export const instructionDiscriminators = {
+  CreateAmm: Buffer.from([0]),
+  CreatePool: Buffer.from([1]),
+  DepositLiquidity: Buffer.from([2]),
+  WithdrawLiquidity: Buffer.from([3]),
+  Swap: Buffer.from([4]),
+};
+export const getCreatePoolInstructionData = () => {
+  return Buffer.concat([instructionDiscriminators.CreatePool]);
+};
+
+export const encodeBigint = (value: bigint) => {
+  const buffer = Buffer.alloc(8);
+  buffer.writeBigUInt64LE(value);
+  return Uint8Array.from(buffer);
+};
+
+export const getDepositLiquidityInstructionData = (amountA: bigint, amountB: bigint) => {
+  return Buffer.concat([instructionDiscriminators.DepositLiquidity, encodeBigint(amountA), encodeBigint(amountB)]);
+};
+
+export const getWithdrawLiquidityInstructionData = (amountLp: bigint) => {
+  return Buffer.concat([instructionDiscriminators.WithdrawLiquidity, encodeBigint(amountLp)]);
+};
+
+export const getSwapInstructionData = (swapA: boolean, inputAmount: bigint, minimunAmountOut: bigint) => {
+  return Buffer.concat([instructionDiscriminators.Swap, Buffer.from([swapA ? 1 : 0]), encodeBigint(inputAmount), encodeBigint(minimunAmountOut)]);
+};
+
+export const getCreateAmmInstructionData = (id: PublicKey, fee: number) => {
+  const buffer = Buffer.alloc(2);
+  buffer.writeUint16LE(fee, 0);
+  return Buffer.concat([instructionDiscriminators.CreateAmm, id.toBuffer(), Buffer.from(buffer)]);
+};
+
+export const createAMint = async (context: ProgramTestContext, payer: Keypair, mint: Keypair) => {
+  const tx = new Transaction();
+  tx.add(
+    SystemProgram.createAccount({
+      fromPubkey: payer.publicKey,
+      newAccountPubkey: mint.publicKey,
+      // the `space` required for a token mint is accessible in the `@solana/spl-token` sdk
+      space: MINT_SIZE,
+      // store enough lamports needed for our `space` to be rent exempt
+      lamports: Number((await context.banksClient.getRent()).minimumBalance(BigInt(MINT_SIZE))),
+      // tokens are owned by the "token program"
+      programId: TOKEN_PROGRAM_ID,
+    }),
+    createInitializeMint2Instruction(mint.publicKey, 9, payer.publicKey, payer.publicKey),
+  );
+  tx.recentBlockhash = context.lastBlockhash;
+  tx.sign(payer, mint);
+
+  // process the transaction
+  await context.banksClient.processTransaction(tx);
+};
+
+export const mintTo = async (context: ProgramTestContext, payer: Keypair, owner: PublicKey, mint: PublicKey) => {
+  const tokenAccount = getAssociatedTokenAddressSync(mint, owner, false);
+  const tx = new Transaction();
+  tx.add(
+    createAssociatedTokenAccountInstruction(payer.publicKey, tokenAccount, owner, mint),
+    createMintToInstruction(mint, tokenAccount, payer.publicKey, 1_000 * LAMPORTS_PER_SOL),
+  );
+  tx.recentBlockhash = context.lastBlockhash;
+  tx.sign(payer);
+
+  // process the transaction
+  await context.banksClient.processTransaction(tx);
+  return tokenAccount;
+};
+
+// Define AmmAccount type
+export type AmmAccount = {
+  id: PublicKey;
+  admin: PublicKey;
+  fee: number;
+};
+
+// Define DataAccountRaw type for deserialization
+export type AmmAccountRaw = {
+  id: Uint8Array;
+  admin: Uint8Array;
+  fee: number;
+};
+
+// Define the schema for the account data
+export const ammAccountSchema: borsh.Schema = {
+  struct: {
+    discriminator: 'u64',
+    id: { array: { type: 'u8', len: 32 } },
+    admin: { array: { type: 'u8', len: 32 } },
+    fee: 'u16',
+  },
+};
+
+export const deserializeAmmAccount = (data: Uint8Array): AmmAccount => {
+  const account = borsh.deserialize(ammAccountSchema, data) as AmmAccountRaw;
+  return {
+    id: new PublicKey(account.id),
+    admin: new PublicKey(account.admin),
+    fee: account.fee,
+  };
+};
+
+// Define AmmAccount type
+export type PoolAccount = {
+  amm: PublicKey;
+  mintA: PublicKey;
+  mintB: PublicKey;
+};
+
+// Define DataAccountRaw type for deserialization
+export type PoolAccountRaw = {
+  amm: Uint8Array;
+  mint_a: Uint8Array;
+  mint_b: Uint8Array;
+};
+
+// Define the schema for the account data
+export const poolAccountSchema: borsh.Schema = {
+  struct: {
+    discriminator: 'u64',
+    amm: { array: { type: 'u8', len: 32 } },
+    mint_a: { array: { type: 'u8', len: 32 } },
+    mint_b: { array: { type: 'u8', len: 32 } },
+  },
+};
+
+export const deserializePoolAccount = (data: Uint8Array): PoolAccount => {
+  const account = borsh.deserialize(poolAccountSchema, data) as PoolAccountRaw;
+  return {
+    amm: new PublicKey(account.amm),
+    mintA: new PublicKey(account.mint_a),
+    mintB: new PublicKey(account.mint_b),
+  };
+};

+ 10 - 0
tokens/token-swap/steel/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "types": ["mocha", "chai", "node"],
+    "typeRoots": ["./node_modules/@types"],
+    "lib": ["es2015"],
+    "module": "commonjs",
+    "target": "es6",
+    "esModuleInterop": true
+  }
+}