Explorar el Código

examples: Swap on the serum orderbook (#224)

Armani Ferrante hace 4 años
padre
commit
a1464d14d5

+ 3 - 0
.gitmodules

@@ -0,0 +1,3 @@
+[submodule "examples/swap/deps/serum-dex"]
+	path = examples/swap/deps/serum-dex
+	url = https://github.com/project-serum/serum-dex

+ 3 - 0
.travis.yml

@@ -6,6 +6,8 @@ cache: cargo
 env:
   global:
     - NODE_VERSION="14.7.0"
+git:
+  submodules: true
 
 _defaults: &defaults
   before_install:
@@ -65,6 +67,7 @@ jobs:
       script:
         - pushd examples/chat && yarn && anchor test && popd
         - pushd examples/ido-pool && yarn && anchor test && popd
+        - pushd examples/swap/deps/serum-dex/dex && cargo build-bpf && cd ../../../ && anchor test && popd
         - pushd examples/tutorial/basic-0 && anchor test && popd
         - pushd examples/tutorial/basic-1 && anchor test && popd
         - pushd examples/tutorial/basic-2 && anchor test && popd

+ 102 - 11
Cargo.lock

@@ -42,6 +42,12 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "alloc-traits"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b2d54853319fd101b8dd81de382bcbf3e03410a64d8928bbee85a3e7dcde483"
+
 [[package]]
 name = "anchor-attribute-access-control"
 version = "0.4.4"
@@ -191,6 +197,8 @@ name = "anchor-spl"
 version = "0.4.4"
 dependencies = [
  "anchor-lang",
+ "lazy_static",
+ "serum_dex",
  "solana-program",
  "spl-token 3.1.0",
 ]
@@ -987,6 +995,26 @@ dependencies = [
  "cfg-if 1.0.0",
 ]
 
+[[package]]
+name = "enumflags2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83c8d82922337cd23a15f88b70d8e4ef5f11da38dd7cdb55e84dd5de99695da0"
+dependencies = [
+ "enumflags2_derive",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "946ee94e3dbf58fdd324f9ce245c7b238d46a66f00e86a020b71996349e46cce"
+dependencies = [
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.67",
+]
+
 [[package]]
 name = "env_logger"
 version = "0.8.3"
@@ -1012,6 +1040,16 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da"
 
+[[package]]
+name = "field-offset"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf539fba70056b50f40a22e0da30639518a12ee18c35807858a63b158cb6dde7"
+dependencies = [
+ "memoffset 0.6.1",
+ "rustc_version 0.3.3",
+]
+
 [[package]]
 name = "filetime"
 version = "0.2.14"
@@ -2008,7 +2046,7 @@ checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252"
 dependencies = [
  "lock_api 0.3.4",
  "parking_lot_core 0.6.2",
- "rustc_version",
+ "rustc_version 0.2.3",
 ]
 
 [[package]]
@@ -2042,7 +2080,7 @@ dependencies = [
  "cloudabi",
  "libc",
  "redox_syscall 0.1.57",
- "rustc_version",
+ "rustc_version 0.2.3",
  "smallvec 0.6.14",
  "winapi 0.3.9",
 ]
@@ -2514,6 +2552,15 @@ dependencies = [
  "semver 0.9.0",
 ]
 
+[[package]]
+name = "rustc_version"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
+dependencies = [
+ "semver 0.11.0",
+]
+
 [[package]]
 name = "rustls"
 version = "0.19.0"
@@ -2539,6 +2586,12 @@ version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
 
+[[package]]
+name = "safe-transmute"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d95e7284b4bd97e24af76023904cd0157c9cc9da0310beb4139a1e88a748d47"
+
 [[package]]
 name = "same-file"
 version = "1.0.6"
@@ -2708,7 +2761,7 @@ dependencies = [
 [[package]]
 name = "serum-common"
 version = "0.1.0"
-source = "git+https://github.com/project-serum/serum-dex#480cfefdbd7789c1fa2ac4474c6456b507f9a78f"
+source = "git+https://github.com/project-serum/serum-dex#e264db2c9cc326f246a8fb108becb7d71bba3e72"
 dependencies = [
  "anyhow",
  "arrayref",
@@ -2723,6 +2776,29 @@ dependencies = [
  "spl-token 2.0.8",
 ]
 
+[[package]]
+name = "serum_dex"
+version = "0.2.0"
+source = "git+https://github.com/project-serum/serum-dex#e264db2c9cc326f246a8fb108becb7d71bba3e72"
+dependencies = [
+ "arrayref",
+ "bincode",
+ "bytemuck",
+ "byteorder",
+ "enumflags2",
+ "field-offset",
+ "itertools",
+ "num-traits",
+ "num_enum",
+ "safe-transmute",
+ "serde",
+ "solana-program",
+ "spl-token 3.1.0",
+ "static_assertions",
+ "thiserror",
+ "without-alloc",
+]
+
 [[package]]
 name = "sha-1"
 version = "0.8.2"
@@ -3000,7 +3076,7 @@ dependencies = [
  "generic-array 0.14.4",
  "log",
  "memmap2",
- "rustc_version",
+ "rustc_version 0.2.3",
  "serde",
  "serde_derive",
  "sha2 0.9.3",
@@ -3018,7 +3094,7 @@ dependencies = [
  "lazy_static",
  "proc-macro2 1.0.24",
  "quote 1.0.9",
- "rustc_version",
+ "rustc_version 0.2.3",
  "syn 1.0.67",
 ]
 
@@ -3101,7 +3177,7 @@ dependencies = [
  "num-derive",
  "num-traits",
  "rand 0.7.3",
- "rustc_version",
+ "rustc_version 0.2.3",
  "rustversion",
  "serde",
  "serde_bytes",
@@ -3174,7 +3250,7 @@ dependencies = [
  "rand 0.7.3",
  "rayon",
  "regex",
- "rustc_version",
+ "rustc_version 0.2.3",
  "serde",
  "serde_derive",
  "solana-config-program",
@@ -3223,7 +3299,7 @@ dependencies = [
  "rand 0.7.3",
  "rand_chacha 0.2.2",
  "rand_core 0.6.2",
- "rustc_version",
+ "rustc_version 0.2.3",
  "rustversion",
  "serde",
  "serde_bytes",
@@ -3278,7 +3354,7 @@ dependencies = [
  "log",
  "num-derive",
  "num-traits",
- "rustc_version",
+ "rustc_version 0.2.3",
  "serde",
  "serde_derive",
  "solana-config-program",
@@ -3322,7 +3398,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d85d6da83a490f9e2a889828fd6b3014e306ee1747429e01b3fe6d78da1dec43"
 dependencies = [
  "log",
- "rustc_version",
+ "rustc_version 0.2.3",
  "serde",
  "serde_derive",
  "solana-frozen-abi",
@@ -3341,7 +3417,7 @@ dependencies = [
  "log",
  "num-derive",
  "num-traits",
- "rustc_version",
+ "rustc_version 0.2.3",
  "serde",
  "serde_derive",
  "solana-frozen-abi",
@@ -3412,6 +3488,12 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
 
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
 [[package]]
 name = "strsim"
 version = "0.8.0"
@@ -4197,6 +4279,15 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "without-alloc"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e34736feff52a0b3e5680927e947a4d8fac1f0b80dc8120b080dd8de24d75e2"
+dependencies = [
+ "alloc-traits",
+]
+
 [[package]]
 name = "ws2_32-sys"
 version = "0.2.1"

+ 3 - 0
Cargo.toml

@@ -8,3 +8,6 @@ members = [
     "lang/syn",
     "spl",
 ]
+exclude = [
+    "examples/swap/deps/serum-dex"
+]

+ 6 - 0
examples/swap/Anchor.toml

@@ -0,0 +1,6 @@
+cluster = "localnet"
+wallet = "~/.config/solana/id.json"
+
+[[test.genesis]]
+address = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
+program = "./deps/serum-dex/dex/target/deploy/serum_dex.so"

+ 7 - 0
examples/swap/Cargo.toml

@@ -0,0 +1,7 @@
+[workspace]
+members = [
+    "programs/*"
+]
+exclude = [
+    "deps/serum-dex"
+]

+ 34 - 0
examples/swap/README.md

@@ -0,0 +1,34 @@
+# Swap
+
+An example swap program that provides a convenient API to the Serum orderbook
+for performing instantly settled token swaps.
+
+## Usage
+
+This example requires building the Serum DEX from source, which is done using
+git submodules.
+
+### Install Submodules
+
+Pull the source
+
+```
+git submodule init
+git submodule update
+```
+
+### Build the DEX
+
+Build it
+
+```
+cd deps/serum-dex/dex/ && cargo build-bpf && cd ../../../
+```
+
+### Run the Test
+
+Run the test
+
+```
+anchor test
+```

+ 1 - 0
examples/swap/deps/serum-dex

@@ -0,0 +1 @@
+Subproject commit 19c8e37bf41d044a084b21e58182a50d119d46a2

+ 12 - 0
examples/swap/migrations/deploy.js

@@ -0,0 +1,12 @@
+// Migrations are an early feature. Currently, they're nothing more than this
+// single deploy script that's invoked from the CLI, injecting a provider
+// configured from the workspace's Anchor.toml.
+
+const anchor = require("@project-serum/anchor");
+
+module.exports = async function (provider) {
+  // Configure client to use the provider.
+  anchor.setProvider(provider);
+
+  // Add your deploy script here.
+}

+ 19 - 0
examples/swap/programs/swap/Cargo.toml

@@ -0,0 +1,19 @@
+[package]
+name = "swap"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "swap"
+
+[features]
+no-entrypoint = []
+no-idl = []
+cpi = ["no-entrypoint"]
+default = []
+
+[dependencies]
+anchor-lang = { path = "../../../../lang" }
+anchor-spl = { path = "../../../../spl" }

+ 2 - 0
examples/swap/programs/swap/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 493 - 0
examples/swap/programs/swap/src/lib.rs

@@ -0,0 +1,493 @@
+//! Program to perform instantly settled token swaps on the Serum DEX.
+//!
+//! Before using any instruction here, a user must first create an open orders
+//! account on all markets being used. This only needs to be done once. As a
+//! convention established by the DEX, this should be done via the system
+//! program create account instruction in the same transaction as the user's
+//! first trade. Then, the DEX will lazily initialize the open orders account.
+
+use anchor_lang::prelude::*;
+use anchor_spl::dex;
+use anchor_spl::dex::serum_dex::instruction::SelfTradeBehavior;
+use anchor_spl::dex::serum_dex::matching::{OrderType, Side as SerumSide};
+use anchor_spl::dex::serum_dex::state::MarketState;
+use anchor_spl::token;
+use std::num::NonZeroU64;
+
+#[program]
+pub mod swap {
+    use super::*;
+
+    /// Swaps two tokens on a single A/B market, where A is the base currency
+    /// and B is the quote currency. This is just a direct IOC trade that
+    /// instantly settles.
+    ///
+    /// When side is "bid", then swaps B for A. When side is "ask", then swaps
+    /// A for B.
+    ///
+    /// Arguments:
+    ///
+    /// * `side`                     - The direction to swap.
+    /// * `amount`                   - The amount to swap *from*
+    /// * `min_expected_swap_amount` - The minimum amount of the *to* token the
+    ///    client expects to receive from the swap. The instruction fails if
+    ///    execution would result in less.
+    #[access_control(is_valid_swap(&ctx))]
+    pub fn swap<'info>(
+        ctx: Context<'_, '_, '_, 'info, Swap<'info>>,
+        side: Side,
+        amount: u64,
+        min_expected_swap_amount: u64,
+    ) -> Result<()> {
+        // Optional referral account (earns a referral fee).
+        let referral = ctx.remaining_accounts.iter().next().map(Clone::clone);
+
+        // Side determines swap direction.
+        let (from_token, to_token) = match side {
+            Side::Bid => (&ctx.accounts.pc_wallet, &ctx.accounts.market.coin_wallet),
+            Side::Ask => (&ctx.accounts.market.coin_wallet, &ctx.accounts.pc_wallet),
+        };
+
+        // Token balances before the trade.
+        let from_amount_before = token::accessor::amount(from_token)?;
+        let to_amount_before = token::accessor::amount(to_token)?;
+
+        // Execute trade.
+        let orderbook: OrderbookClient<'info> = (&*ctx.accounts).into();
+        match side {
+            Side::Bid => orderbook.buy(amount, referral.clone())?,
+            Side::Ask => orderbook.sell(amount, referral.clone())?,
+        };
+        orderbook.settle(referral)?;
+
+        // Token balances after the trade.
+        let from_amount_after = token::accessor::amount(from_token)?;
+        let to_amount_after = token::accessor::amount(to_token)?;
+
+        //  Calculate the delta, i.e. the amount swapped.
+        let from_amount = from_amount_before.checked_sub(from_amount_after).unwrap();
+        let to_amount = to_amount_after.checked_sub(to_amount_before).unwrap();
+
+        // Safety checks.
+        apply_risk_checks(DidSwap {
+            authority: *ctx.accounts.authority.key,
+            given_amount: amount,
+            min_expected_swap_amount,
+            from_amount,
+            to_amount,
+            spill_amount: 0,
+            from_mint: token::accessor::mint(from_token)?,
+            to_mint: token::accessor::mint(to_token)?,
+            quote_mint: match side {
+                Side::Bid => token::accessor::mint(from_token)?,
+                Side::Ask => token::accessor::mint(to_token)?,
+            },
+        })?;
+
+        Ok(())
+    }
+
+    /// Swaps two base currencies across two different markets.
+    ///
+    /// That is, suppose there are two markets, A/USD(x) and B/USD(x).
+    /// Then swaps token A for token B via
+    ///
+    /// * IOC (immediate or cancel) sell order on A/USD(x) market.
+    /// * Settle open orders to get USD(x).
+    /// * IOC buy order on B/USD(x) market to convert USD(x) to token B.
+    /// * Settle open orders to get token B.
+    ///
+    /// Arguments:
+    ///
+    /// * `amount`                  - The amount to swap *from*.
+    /// * `min_expected_swap_amount - The minimum amount of the *to* token the
+    ///    client expects to receive from the swap. The instruction fails if
+    ///    execution would result in less.
+    #[access_control(is_valid_swap_transitive(&ctx))]
+    pub fn swap_transitive<'info>(
+        ctx: Context<'_, '_, '_, 'info, SwapTransitive<'info>>,
+        amount: u64,
+        min_expected_swap_amount: u64,
+    ) -> Result<()> {
+        // Optional referral account (earns a referral fee).
+        let referral = ctx.remaining_accounts.iter().next().map(Clone::clone);
+
+        // Leg 1: Sell Token A for USD(x) (or whatever quote currency is used).
+        let (from_amount, sell_proceeds) = {
+            // Token balances before the trade.
+            let base_before = token::accessor::amount(&ctx.accounts.from.coin_wallet)?;
+            let quote_before = token::accessor::amount(&ctx.accounts.pc_wallet)?;
+
+            // Execute the trade.
+            let orderbook = ctx.accounts.orderbook_from();
+            orderbook.sell(amount, referral.clone())?;
+            orderbook.settle(referral.clone())?;
+
+            // Token balances after the trade.
+            let base_after = token::accessor::amount(&ctx.accounts.from.coin_wallet)?;
+            let quote_after = token::accessor::amount(&ctx.accounts.pc_wallet)?;
+
+            // Report the delta.
+            (
+                base_before.checked_sub(base_after).unwrap(),
+                quote_after.checked_sub(quote_before).unwrap(),
+            )
+        };
+
+        // Leg 2: Buy Token B with USD(x) (or whatever quote currency is used).
+        let (to_amount, spill_amount) = {
+            // Token balances before the trade.
+            let base_before = token::accessor::amount(&ctx.accounts.to.coin_wallet)?;
+            let quote_before = token::accessor::amount(&ctx.accounts.pc_wallet)?;
+
+            // Execute the trade.
+            let orderbook = ctx.accounts.orderbook_to();
+            orderbook.buy(sell_proceeds, referral.clone())?;
+            orderbook.settle(referral)?;
+
+            // Token balances after the trade.
+            let base_after = token::accessor::amount(&ctx.accounts.to.coin_wallet)?;
+            let quote_after = token::accessor::amount(&ctx.accounts.pc_wallet)?;
+
+            // Report the delta.
+            (
+                base_after.checked_sub(base_before).unwrap(),
+                quote_before.checked_sub(quote_after).unwrap(),
+            )
+        };
+
+        // Safety checks.
+        apply_risk_checks(DidSwap {
+            given_amount: amount,
+            min_expected_swap_amount,
+            from_amount,
+            to_amount,
+            spill_amount,
+            from_mint: token::accessor::mint(&ctx.accounts.from.coin_wallet)?,
+            to_mint: token::accessor::mint(&ctx.accounts.to.coin_wallet)?,
+            quote_mint: token::accessor::mint(&ctx.accounts.pc_wallet)?,
+            authority: *ctx.accounts.authority.key,
+        })?;
+
+        Ok(())
+    }
+}
+
+// Asserts the swap event is valid.
+fn apply_risk_checks(event: DidSwap) -> Result<()> {
+    // Reject if the resulting amount is less than the client's expectation.
+    if event.to_amount < event.min_expected_swap_amount {
+        return Err(ErrorCode::SlippageExceeded.into());
+    }
+    emit!(event);
+    Ok(())
+}
+
+// The only constraint imposed on these accounts is that the market's base
+// currency mint is not equal to the quote currency's. All other checks are
+// done by the DEX on CPI.
+#[derive(Accounts)]
+pub struct Swap<'info> {
+    market: MarketAccounts<'info>,
+    #[account(signer)]
+    authority: AccountInfo<'info>,
+    #[account(mut)]
+    pc_wallet: AccountInfo<'info>,
+    // Programs.
+    dex_program: AccountInfo<'info>,
+    token_program: AccountInfo<'info>,
+    // Sysvars.
+    rent: AccountInfo<'info>,
+}
+
+impl<'info> From<&Swap<'info>> for OrderbookClient<'info> {
+    fn from(accounts: &Swap<'info>) -> OrderbookClient<'info> {
+        OrderbookClient {
+            market: accounts.market.clone(),
+            authority: accounts.authority.clone(),
+            pc_wallet: accounts.pc_wallet.clone(),
+            dex_program: accounts.dex_program.clone(),
+            token_program: accounts.token_program.clone(),
+            rent: accounts.rent.clone(),
+        }
+    }
+}
+
+// The only constraint imposed on these accounts is that the from market's
+// base currency's is not equal to the to market's base currency. All other
+// checks are done by the DEX on CPI (and the quote currency is ensured to be
+// the same on both markets since there's only one account field for it).
+#[derive(Accounts)]
+pub struct SwapTransitive<'info> {
+    from: MarketAccounts<'info>,
+    to: MarketAccounts<'info>,
+    // Must be the authority over all open orders accounts used.
+    #[account(signer)]
+    authority: AccountInfo<'info>,
+    #[account(mut)]
+    pc_wallet: AccountInfo<'info>,
+    // Programs.
+    dex_program: AccountInfo<'info>,
+    token_program: AccountInfo<'info>,
+    // Sysvars.
+    rent: AccountInfo<'info>,
+}
+
+impl<'info> SwapTransitive<'info> {
+    fn orderbook_from(&self) -> OrderbookClient<'info> {
+        OrderbookClient {
+            market: self.from.clone(),
+            authority: self.authority.clone(),
+            pc_wallet: self.pc_wallet.clone(),
+            dex_program: self.dex_program.clone(),
+            token_program: self.token_program.clone(),
+            rent: self.rent.clone(),
+        }
+    }
+    fn orderbook_to(&self) -> OrderbookClient<'info> {
+        OrderbookClient {
+            market: self.to.clone(),
+            authority: self.authority.clone(),
+            pc_wallet: self.pc_wallet.clone(),
+            dex_program: self.dex_program.clone(),
+            token_program: self.token_program.clone(),
+            rent: self.rent.clone(),
+        }
+    }
+}
+
+// Client for sending orders to the Serum DEX.
+struct OrderbookClient<'info> {
+    market: MarketAccounts<'info>,
+    authority: AccountInfo<'info>,
+    pc_wallet: AccountInfo<'info>,
+    dex_program: AccountInfo<'info>,
+    token_program: AccountInfo<'info>,
+    rent: AccountInfo<'info>,
+}
+
+impl<'info> OrderbookClient<'info> {
+    // Executes the sell order portion of the swap, purchasing as much of the
+    // quote currency as possible for the given `base_amount`.
+    //
+    // `base_amount` is the "native" amount of the base currency, i.e., token
+    // amount including decimals.
+    fn sell(&self, base_amount: u64, referral: Option<AccountInfo<'info>>) -> ProgramResult {
+        let limit_price = 1;
+        let max_coin_qty = {
+            // The loaded market must be dropped before CPI.
+            let market = MarketState::load(&self.market.market, &dex::ID)?;
+            coin_lots(&market, base_amount)
+        };
+        let max_native_pc_qty = u64::MAX;
+        self.order_cpi(
+            limit_price,
+            max_coin_qty,
+            max_native_pc_qty,
+            Side::Ask,
+            referral,
+        )
+    }
+
+    // Executes the buy order portion of the swap, purchasing as much of the
+    // base currency as possible, for the given `quote_amount`.
+    //
+    // `quote_amount` is the "native" amount of the quote currency, i.e., token
+    // amount including decimals.
+    fn buy(&self, quote_amount: u64, referral: Option<AccountInfo<'info>>) -> ProgramResult {
+        let limit_price = u64::MAX;
+        let max_coin_qty = u64::MAX;
+        let max_native_pc_qty = quote_amount;
+        self.order_cpi(
+            limit_price,
+            max_coin_qty,
+            max_native_pc_qty,
+            Side::Bid,
+            referral,
+        )
+    }
+
+    // Executes a new order on the serum dex via CPI.
+    //
+    // * `limit_price` - the limit order price in lot units.
+    // * `max_coin_qty`- the max number of the base currency lot units.
+    // * `max_native_pc_qty` - the max number of quote currency in native token
+    //                         units (includes decimals).
+    // * `side` - bid or ask, i.e. the type of order.
+    // * `referral` - referral account, earning a fee.
+    fn order_cpi(
+        &self,
+        limit_price: u64,
+        max_coin_qty: u64,
+        max_native_pc_qty: u64,
+        side: Side,
+        referral: Option<AccountInfo<'info>>,
+    ) -> ProgramResult {
+        // Client order id is only used for cancels. Not used here so hardcode.
+        let client_order_id = 0;
+        // Limit is the dex's custom compute budge parameter, setting an upper
+        // bound on the number of matching cycles the program can perform
+        // before giving up and posting the remaining unmatched order.
+        let limit = 65535;
+
+        let dex_accs = dex::NewOrderV3 {
+            market: self.market.market.clone(),
+            open_orders: self.market.open_orders.clone(),
+            request_queue: self.market.request_queue.clone(),
+            event_queue: self.market.event_queue.clone(),
+            market_bids: self.market.bids.clone(),
+            market_asks: self.market.asks.clone(),
+            order_payer_token_account: self.market.order_payer_token_account.clone(),
+            open_orders_authority: self.authority.clone(),
+            coin_vault: self.market.coin_vault.clone(),
+            pc_vault: self.market.pc_vault.clone(),
+            token_program: self.token_program.clone(),
+            rent: self.rent.clone(),
+        };
+        let mut ctx = CpiContext::new(self.dex_program.clone(), dex_accs);
+        if let Some(referral) = referral {
+            ctx = ctx.with_remaining_accounts(vec![referral]);
+        }
+        dex::new_order_v3(
+            ctx,
+            side.into(),
+            NonZeroU64::new(limit_price).unwrap(),
+            NonZeroU64::new(max_coin_qty).unwrap(),
+            NonZeroU64::new(max_native_pc_qty).unwrap(),
+            SelfTradeBehavior::DecrementTake,
+            OrderType::ImmediateOrCancel,
+            client_order_id,
+            limit,
+        )
+    }
+
+    fn settle(&self, referral: Option<AccountInfo<'info>>) -> ProgramResult {
+        let settle_accs = dex::SettleFunds {
+            market: self.market.market.clone(),
+            open_orders: self.market.open_orders.clone(),
+            open_orders_authority: self.authority.clone(),
+            coin_vault: self.market.coin_vault.clone(),
+            pc_vault: self.market.pc_vault.clone(),
+            coin_wallet: self.market.coin_wallet.clone(),
+            pc_wallet: self.pc_wallet.clone(),
+            vault_signer: self.market.vault_signer.clone(),
+            token_program: self.token_program.clone(),
+        };
+        let mut ctx = CpiContext::new(self.dex_program.clone(), settle_accs);
+        if let Some(referral) = referral {
+            ctx = ctx.with_remaining_accounts(vec![referral]);
+        }
+        dex::settle_funds(ctx)
+    }
+}
+
+// Returns the amount of lots for the base currency of a trade with `size`.
+fn coin_lots(market: &MarketState, size: u64) -> u64 {
+    size.checked_div(market.coin_lot_size).unwrap()
+}
+
+// Market accounts are the accounts used to place orders against the dex minus
+// common accounts, i.e., program ids, sysvars, and the `pc_wallet`.
+#[derive(Accounts, Clone)]
+pub struct MarketAccounts<'info> {
+    #[account(mut)]
+    market: AccountInfo<'info>,
+    #[account(mut)]
+    open_orders: AccountInfo<'info>,
+    #[account(mut)]
+    request_queue: AccountInfo<'info>,
+    #[account(mut)]
+    event_queue: AccountInfo<'info>,
+    #[account(mut)]
+    bids: AccountInfo<'info>,
+    #[account(mut)]
+    asks: AccountInfo<'info>,
+    // The `spl_token::Account` that funds will be taken from, i.e., transferred
+    // from the user into the market's vault.
+    //
+    // For bids, this is the base currency. For asks, the quote.
+    #[account(mut)]
+    order_payer_token_account: AccountInfo<'info>,
+    // Also known as the "base" currency. For a given A/B market,
+    // this is the vault for the A mint.
+    #[account(mut)]
+    coin_vault: AccountInfo<'info>,
+    // Also known as the "quote" currency. For a given A/B market,
+    // this is the vault for the B mint.
+    #[account(mut)]
+    pc_vault: AccountInfo<'info>,
+    // PDA owner of the DEX's token accounts for base + quote currencies.
+    vault_signer: AccountInfo<'info>,
+    // User wallets.
+    #[account(mut)]
+    coin_wallet: AccountInfo<'info>,
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize)]
+pub enum Side {
+    Bid,
+    Ask,
+}
+
+impl From<Side> for SerumSide {
+    fn from(side: Side) -> SerumSide {
+        match side {
+            Side::Bid => SerumSide::Bid,
+            Side::Ask => SerumSide::Ask,
+        }
+    }
+}
+
+// Access control modifiers.
+
+fn is_valid_swap(ctx: &Context<Swap>) -> Result<()> {
+    _is_valid_swap(&ctx.accounts.market.coin_wallet, &ctx.accounts.pc_wallet)
+}
+
+fn is_valid_swap_transitive(ctx: &Context<SwapTransitive>) -> Result<()> {
+    _is_valid_swap(&ctx.accounts.from.coin_wallet, &ctx.accounts.to.coin_wallet)
+}
+
+// Validates the tokens being swapped are of different mints.
+fn _is_valid_swap<'info>(from: &AccountInfo<'info>, to: &AccountInfo<'info>) -> Result<()> {
+    let from_token_mint = token::accessor::mint(from)?;
+    let to_token_mint = token::accessor::mint(to)?;
+    if from_token_mint == to_token_mint {
+        return Err(ErrorCode::SwapTokensCannotMatch.into());
+    }
+    Ok(())
+}
+
+// Event emitted when a swap occurs for two base currencies on two different
+// markets (quoted in the same token).
+#[event]
+pub struct DidSwap {
+    // User given (max) amount to swap.
+    pub given_amount: u64,
+    // The minimum amount of the *to* token expected to be received from
+    // executing the swap.
+    pub min_expected_swap_amount: u64,
+    // Amount of the `from` token sold.
+    pub from_amount: u64,
+    // Amount of the `to` token purchased.
+    pub to_amount: u64,
+    // Amount of the quote currency accumulated from the swap.
+    pub spill_amount: u64,
+    // Mint sold.
+    pub from_mint: Pubkey,
+    // Mint purchased.
+    pub to_mint: Pubkey,
+    // Mint of the token used as the quote currency in the two markets used
+    // for swapping.
+    pub quote_mint: Pubkey,
+    // User that signed the transaction.
+    pub authority: Pubkey,
+}
+
+#[error]
+pub enum ErrorCode {
+    #[msg("The tokens being swapped must have different mints")]
+    SwapTokensCannotMatch,
+    #[msg("Slippage tolerance exceeded")]
+    SlippageExceeded,
+}

+ 311 - 0
examples/swap/tests/swap.js

@@ -0,0 +1,311 @@
+const assert = require("assert");
+const anchor = require("@project-serum/anchor");
+const BN = anchor.BN;
+const OpenOrders = require("@project-serum/serum").OpenOrders;
+const TOKEN_PROGRAM_ID = require("@solana/spl-token").TOKEN_PROGRAM_ID;
+const serumCmn = require("@project-serum/common");
+const utils = require("./utils");
+
+// Taker fee rate (bps).
+const TAKER_FEE = 0.0022;
+
+describe("swap", () => {
+  // Configure the client to use the local cluster.
+  anchor.setProvider(anchor.Provider.env());
+
+  // Swap program client.
+  const program = anchor.workspace.Swap;
+
+  // Accounts used to setup the orderbook.
+  let ORDERBOOK_ENV,
+    // Accounts used for A -> USDC swap transactions.
+    SWAP_A_USDC_ACCOUNTS,
+    // Accounts used for  USDC -> A swap transactions.
+    SWAP_USDC_A_ACCOUNTS,
+    // Serum DEX vault PDA for market A/USDC.
+    marketAVaultSigner,
+    // Serum DEX vault PDA for market B/USDC.
+    marketBVaultSigner;
+
+  // Open orders accounts on the two markets for the provider.
+  const openOrdersA = new anchor.web3.Account();
+  const openOrdersB = new anchor.web3.Account();
+
+  it("BOILERPLATE: Sets up two markets with resting orders", async () => {
+    ORDERBOOK_ENV = await utils.setupTwoMarkets({
+      provider: program.provider,
+    });
+  });
+
+  it("BOILERPLATE: Sets up reusable accounts", async () => {
+    const marketA = ORDERBOOK_ENV.marketA;
+    const marketB = ORDERBOOK_ENV.marketB;
+
+    const [vaultSignerA] = await utils.getVaultOwnerAndNonce(
+      marketA._decoded.ownAddress
+    );
+    const [vaultSignerB] = await utils.getVaultOwnerAndNonce(
+      marketB._decoded.ownAddress
+    );
+    marketAVaultSigner = vaultSignerA;
+    marketBVaultSigner = vaultSignerB;
+
+    SWAP_USDC_A_ACCOUNTS = {
+      market: {
+        market: marketA._decoded.ownAddress,
+        requestQueue: marketA._decoded.requestQueue,
+        eventQueue: marketA._decoded.eventQueue,
+        bids: marketA._decoded.bids,
+        asks: marketA._decoded.asks,
+        coinVault: marketA._decoded.baseVault,
+        pcVault: marketA._decoded.quoteVault,
+        vaultSigner: marketAVaultSigner,
+        // User params.
+        openOrders: openOrdersA.publicKey,
+        orderPayerTokenAccount: ORDERBOOK_ENV.godUsdc,
+        coinWallet: ORDERBOOK_ENV.godA,
+      },
+      pcWallet: ORDERBOOK_ENV.godUsdc,
+      authority: program.provider.wallet.publicKey,
+      dexProgram: utils.DEX_PID,
+      tokenProgram: TOKEN_PROGRAM_ID,
+      rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+    };
+    SWAP_A_USDC_ACCOUNTS = {
+      ...SWAP_USDC_A_ACCOUNTS,
+      market: {
+        ...SWAP_USDC_A_ACCOUNTS.market,
+        orderPayerTokenAccount: ORDERBOOK_ENV.godA,
+      },
+    };
+  });
+
+  it("Swaps from USDC to Token A", async () => {
+    const marketA = ORDERBOOK_ENV.marketA;
+
+    // Swap exactly enough USDC to get 1.2 A tokens (best offer price is 6.041 USDC).
+    const expectedResultantAmount = 7.2;
+    const bestOfferPrice = 6.041;
+    const amountToSpend = expectedResultantAmount * bestOfferPrice;
+    const swapAmount = new BN((amountToSpend / (1 - TAKER_FEE)) * 10 ** 6);
+
+    const [tokenAChange, usdcChange] = await withBalanceChange(
+      program.provider,
+      [ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godUsdc],
+      async () => {
+        await program.rpc.swap(Side.Bid, swapAmount, new BN(1.0), {
+          accounts: SWAP_USDC_A_ACCOUNTS,
+          instructions: [
+            // First order to this market so one must create the open orders account.
+            await OpenOrders.makeCreateAccountTransaction(
+              program.provider.connection,
+              marketA._decoded.ownAddress,
+              program.provider.wallet.publicKey,
+              openOrdersA.publicKey,
+              utils.DEX_PID
+            ),
+            // Might as well create the second open orders account while we're here.
+            // In prod, this should actually be done within the same tx as an
+            // order to market B.
+            await OpenOrders.makeCreateAccountTransaction(
+              program.provider.connection,
+              ORDERBOOK_ENV.marketB._decoded.ownAddress,
+              program.provider.wallet.publicKey,
+              openOrdersB.publicKey,
+              utils.DEX_PID
+            ),
+          ],
+          signers: [openOrdersA, openOrdersB],
+        });
+      }
+    );
+
+    assert.ok(tokenAChange === expectedResultantAmount);
+    assert.ok(usdcChange === -swapAmount.toNumber() / 10 ** 6);
+  });
+
+  it("Swaps from Token A to USDC", async () => {
+    const marketA = ORDERBOOK_ENV.marketA;
+
+    // Swap out A tokens for USDC.
+    const swapAmount = 8.1;
+    const bestBidPrice = 6.004;
+    const amountToFill = swapAmount * bestBidPrice;
+    const takerFee = 0.0022;
+    const resultantAmount = new BN(amountToFill * (1 - TAKER_FEE) * 10 ** 6);
+
+    const [tokenAChange, usdcChange] = await withBalanceChange(
+      program.provider,
+      [ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godUsdc],
+      async () => {
+        await program.rpc.swap(
+          Side.Ask,
+          new BN(swapAmount * 10 ** 6),
+          new BN(swapAmount),
+          {
+            accounts: SWAP_A_USDC_ACCOUNTS,
+          }
+        );
+      }
+    );
+
+    assert.ok(tokenAChange === -swapAmount);
+    assert.ok(usdcChange === resultantAmount.toNumber() / 10 ** 6);
+  });
+
+  it("Swaps from Token A to Token B", async () => {
+    const marketA = ORDERBOOK_ENV.marketA;
+    const marketB = ORDERBOOK_ENV.marketB;
+    const swapAmount = 10;
+    const [tokenAChange, tokenBChange, usdcChange] = await withBalanceChange(
+      program.provider,
+      [ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godB, ORDERBOOK_ENV.godUsdc],
+      async () => {
+        // Perform the actual swap.
+        await program.rpc.swapTransitive(
+          new BN(swapAmount * 10 ** 6),
+          new BN(swapAmount - 1),
+          {
+            accounts: {
+              from: {
+                market: marketA._decoded.ownAddress,
+                requestQueue: marketA._decoded.requestQueue,
+                eventQueue: marketA._decoded.eventQueue,
+                bids: marketA._decoded.bids,
+                asks: marketA._decoded.asks,
+                coinVault: marketA._decoded.baseVault,
+                pcVault: marketA._decoded.quoteVault,
+                vaultSigner: marketAVaultSigner,
+                // User params.
+                openOrders: openOrdersA.publicKey,
+                // Swapping from A -> USDC.
+                orderPayerTokenAccount: ORDERBOOK_ENV.godA,
+                coinWallet: ORDERBOOK_ENV.godA,
+              },
+              to: {
+                market: marketB._decoded.ownAddress,
+                requestQueue: marketB._decoded.requestQueue,
+                eventQueue: marketB._decoded.eventQueue,
+                bids: marketB._decoded.bids,
+                asks: marketB._decoded.asks,
+                coinVault: marketB._decoded.baseVault,
+                pcVault: marketB._decoded.quoteVault,
+                vaultSigner: marketBVaultSigner,
+                // User params.
+                openOrders: openOrdersB.publicKey,
+                // Swapping from USDC -> B.
+                orderPayerTokenAccount: ORDERBOOK_ENV.godUsdc,
+                coinWallet: ORDERBOOK_ENV.godB,
+              },
+              pcWallet: ORDERBOOK_ENV.godUsdc,
+              authority: program.provider.wallet.publicKey,
+              dexProgram: utils.DEX_PID,
+              tokenProgram: TOKEN_PROGRAM_ID,
+              rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+            },
+          }
+        );
+      }
+    );
+
+    assert.ok(tokenAChange === -swapAmount);
+    // TODO: calculate this dynamically from the swap amount.
+    assert.ok(tokenBChange === 9.8);
+    assert.ok(usdcChange === 0);
+  });
+
+  it("Swaps from Token B to Token A", async () => {
+    const marketA = ORDERBOOK_ENV.marketA;
+    const marketB = ORDERBOOK_ENV.marketB;
+    const swapAmount = 23;
+    const [tokenAChange, tokenBChange, usdcChange] = await withBalanceChange(
+      program.provider,
+      [ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godB, ORDERBOOK_ENV.godUsdc],
+      async () => {
+        // Perform the actual swap.
+        await program.rpc.swapTransitive(
+          new BN(swapAmount * 10 ** 6),
+          new BN(swapAmount - 1),
+          {
+            accounts: {
+              from: {
+                market: marketB._decoded.ownAddress,
+                requestQueue: marketB._decoded.requestQueue,
+                eventQueue: marketB._decoded.eventQueue,
+                bids: marketB._decoded.bids,
+                asks: marketB._decoded.asks,
+                coinVault: marketB._decoded.baseVault,
+                pcVault: marketB._decoded.quoteVault,
+                vaultSigner: marketBVaultSigner,
+                // User params.
+                openOrders: openOrdersB.publicKey,
+                // Swapping from B -> USDC.
+                orderPayerTokenAccount: ORDERBOOK_ENV.godB,
+                coinWallet: ORDERBOOK_ENV.godB,
+              },
+              to: {
+                market: marketA._decoded.ownAddress,
+                requestQueue: marketA._decoded.requestQueue,
+                eventQueue: marketA._decoded.eventQueue,
+                bids: marketA._decoded.bids,
+                asks: marketA._decoded.asks,
+                coinVault: marketA._decoded.baseVault,
+                pcVault: marketA._decoded.quoteVault,
+                vaultSigner: marketAVaultSigner,
+                // User params.
+                openOrders: openOrdersA.publicKey,
+                // Swapping from USDC -> A.
+                orderPayerTokenAccount: ORDERBOOK_ENV.godUsdc,
+                coinWallet: ORDERBOOK_ENV.godA,
+              },
+              pcWallet: ORDERBOOK_ENV.godUsdc,
+              authority: program.provider.wallet.publicKey,
+              dexProgram: utils.DEX_PID,
+              tokenProgram: TOKEN_PROGRAM_ID,
+              rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+            },
+          }
+        );
+      }
+    );
+
+    // TODO: calculate this dynamically from the swap amount.
+    assert.ok(tokenAChange === 22.6);
+    assert.ok(tokenBChange === -swapAmount);
+    assert.ok(usdcChange === 0);
+  });
+});
+
+// Side rust enum used for the program's RPC API.
+const Side = {
+  Bid: { bid: {} },
+  Ask: { ask: {} },
+};
+
+// Executes a closure. Returning the change in balances from before and after
+// its execution.
+async function withBalanceChange(provider, addrs, fn) {
+  const beforeBalances = [];
+  for (let k = 0; k < addrs.length; k += 1) {
+    beforeBalances.push(
+      (await serumCmn.getTokenAccount(provider, addrs[k])).amount
+    );
+  }
+
+  await fn();
+
+  const afterBalances = [];
+  for (let k = 0; k < addrs.length; k += 1) {
+    afterBalances.push(
+      (await serumCmn.getTokenAccount(provider, addrs[k])).amount
+    );
+  }
+
+  const deltas = [];
+  for (let k = 0; k < addrs.length; k += 1) {
+    deltas.push(
+      (afterBalances[k].toNumber() - beforeBalances[k].toNumber()) / 10 ** 6
+    );
+  }
+  return deltas;
+}

+ 510 - 0
examples/swap/tests/utils/index.js

@@ -0,0 +1,510 @@
+// Boilerplate utils to bootstrap an orderbook for testing on a localnet.
+// not super relevant to the point of the example, though may be useful to
+// include into your own workspace for testing.
+//
+// TODO: Modernize all these apis. This is all quite clunky.
+
+const Token = require("@solana/spl-token").Token;
+const TOKEN_PROGRAM_ID = require("@solana/spl-token").TOKEN_PROGRAM_ID;
+const TokenInstructions = require("@project-serum/serum").TokenInstructions;
+const Market = require("@project-serum/serum").Market;
+const DexInstructions = require("@project-serum/serum").DexInstructions;
+const web3 = require("@project-serum/anchor").web3;
+const Connection = web3.Connection;
+const BN = require("@project-serum/anchor").BN;
+const serumCmn = require("@project-serum/common");
+const Account = web3.Account;
+const Transaction = web3.Transaction;
+const PublicKey = web3.PublicKey;
+const SystemProgram = web3.SystemProgram;
+const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
+
+async function setupTwoMarkets({ provider }) {
+  // Setup mints with initial tokens owned by the provider.
+  const decimals = 6;
+  const [MINT_A, GOD_A] = await serumCmn.createMintAndVault(
+    provider,
+    new BN(1000000000000000),
+    undefined,
+    decimals
+  );
+  const [MINT_B, GOD_B] = await serumCmn.createMintAndVault(
+    provider,
+    new BN(1000000000000000),
+    undefined,
+    decimals
+  );
+  const [USDC, GOD_USDC] = await serumCmn.createMintAndVault(
+    provider,
+    new BN(1000000000000000),
+    undefined,
+    decimals
+  );
+
+  // Create a funded account to act as market maker.
+  const amount = 100000 * 10 ** decimals;
+  const marketMaker = await fundAccount({
+    provider,
+    mints: [
+      { god: GOD_A, mint: MINT_A, amount, decimals },
+      { god: GOD_B, mint: MINT_B, amount, decimals },
+      { god: GOD_USDC, mint: USDC, amount, decimals },
+    ],
+  });
+
+  // Setup A/USDC and B/USDC markets with resting orders.
+  const asks = [
+    [6.041, 7.8],
+    [6.051, 72.3],
+    [6.055, 5.4],
+    [6.067, 15.7],
+    [6.077, 390.0],
+    [6.09, 24.0],
+    [6.11, 36.3],
+    [6.133, 300.0],
+    [6.167, 687.8],
+  ];
+  const bids = [
+    [6.004, 8.5],
+    [5.995, 12.9],
+    [5.987, 6.2],
+    [5.978, 15.3],
+    [5.965, 82.8],
+    [5.961, 25.4],
+  ];
+
+  MARKET_A_USDC = await setupMarket({
+    baseMint: MINT_A,
+    quoteMint: USDC,
+    marketMaker: {
+      account: marketMaker.account,
+      baseToken: marketMaker.tokens[MINT_A.toString()],
+      quoteToken: marketMaker.tokens[USDC.toString()],
+    },
+    bids,
+    asks,
+    provider,
+  });
+  MARKET_B_USDC = await setupMarket({
+    baseMint: MINT_B,
+    quoteMint: USDC,
+    marketMaker: {
+      account: marketMaker.account,
+      baseToken: marketMaker.tokens[MINT_B.toString()],
+      quoteToken: marketMaker.tokens[USDC.toString()],
+    },
+    bids,
+    asks,
+    provider,
+  });
+
+  return {
+    marketA: MARKET_A_USDC,
+    marketB: MARKET_B_USDC,
+    marketMaker,
+    mintA: MINT_A,
+    mintB: MINT_B,
+    usdc: USDC,
+    godA: GOD_A,
+    godB: GOD_B,
+    godUsdc: GOD_USDC,
+  };
+}
+
+// Creates everything needed for an orderbook to be running
+//
+// * Mints for both the base and quote currencies.
+// * Lists the market.
+// * Provides resting orders on the market.
+//
+// Returns a client that can be used to interact with the market
+// (and some other data, e.g., the mints and market maker account).
+async function initOrderbook({ provider, bids, asks }) {
+  if (!bids || !asks) {
+    asks = [
+      [6.041, 7.8],
+      [6.051, 72.3],
+      [6.055, 5.4],
+      [6.067, 15.7],
+      [6.077, 390.0],
+      [6.09, 24.0],
+      [6.11, 36.3],
+      [6.133, 300.0],
+      [6.167, 687.8],
+    ];
+    bids = [
+      [6.004, 8.5],
+      [5.995, 12.9],
+      [5.987, 6.2],
+      [5.978, 15.3],
+      [5.965, 82.8],
+      [5.961, 25.4],
+    ];
+  }
+  // Create base and quote currency mints.
+  const decimals = 6;
+  const [MINT_A, GOD_A] = await serumCmn.createMintAndVault(
+    provider,
+    new BN(1000000000000000),
+    undefined,
+    decimals
+  );
+  const [USDC, GOD_USDC] = await serumCmn.createMintAndVault(
+    provider,
+    new BN(1000000000000000),
+    undefined,
+    decimals
+  );
+
+  // Create a funded account to act as market maker.
+  const amount = 100000 * 10 ** decimals;
+  const marketMaker = await fundAccount({
+    provider,
+    mints: [
+      { god: GOD_A, mint: MINT_A, amount, decimals },
+      { god: GOD_USDC, mint: USDC, amount, decimals },
+    ],
+  });
+
+  marketClient = await setupMarket({
+    baseMint: MINT_A,
+    quoteMint: USDC,
+    marketMaker: {
+      account: marketMaker.account,
+      baseToken: marketMaker.tokens[MINT_A.toString()],
+      quoteToken: marketMaker.tokens[USDC.toString()],
+    },
+    bids,
+    asks,
+    provider,
+  });
+
+  return {
+    marketClient,
+    baseMint: MINT_A,
+    quoteMint: USDC,
+    marketMaker,
+  };
+}
+
+async function fundAccount({ provider, mints }) {
+  const MARKET_MAKER = new Account();
+
+  const marketMaker = {
+    tokens: {},
+    account: MARKET_MAKER,
+  };
+
+  // Transfer lamports to market maker.
+  await provider.send(
+    (() => {
+      const tx = new Transaction();
+      tx.add(
+        SystemProgram.transfer({
+          fromPubkey: provider.wallet.publicKey,
+          toPubkey: MARKET_MAKER.publicKey,
+          lamports: 100000000000,
+        })
+      );
+      return tx;
+    })()
+  );
+
+  // Transfer SPL tokens to the market maker.
+  for (let k = 0; k < mints.length; k += 1) {
+    const { mint, god, amount, decimals } = mints[k];
+    let MINT_A = mint;
+    let GOD_A = god;
+    // Setup token accounts owned by the market maker.
+    const mintAClient = new Token(
+      provider.connection,
+      MINT_A,
+      TOKEN_PROGRAM_ID,
+      provider.wallet.payer // node only
+    );
+    const marketMakerTokenA = await mintAClient.createAccount(
+      MARKET_MAKER.publicKey
+    );
+
+    await provider.send(
+      (() => {
+        const tx = new Transaction();
+        tx.add(
+          Token.createTransferCheckedInstruction(
+            TOKEN_PROGRAM_ID,
+            GOD_A,
+            MINT_A,
+            marketMakerTokenA,
+            provider.wallet.publicKey,
+            [],
+            amount,
+            decimals
+          )
+        );
+        return tx;
+      })()
+    );
+
+    marketMaker.tokens[mint.toString()] = marketMakerTokenA;
+  }
+
+  return marketMaker;
+}
+
+async function setupMarket({
+  provider,
+  marketMaker,
+  baseMint,
+  quoteMint,
+  bids,
+  asks,
+}) {
+  const marketAPublicKey = await listMarket({
+    connection: provider.connection,
+    wallet: provider.wallet,
+    baseMint: baseMint,
+    quoteMint: quoteMint,
+    baseLotSize: 100000,
+    quoteLotSize: 100,
+    dexProgramId: DEX_PID,
+    feeRateBps: 0,
+  });
+  const MARKET_A_USDC = await Market.load(
+    provider.connection,
+    marketAPublicKey,
+    { commitment: "recent" },
+    DEX_PID
+  );
+  for (let k = 0; k < asks.length; k += 1) {
+    let ask = asks[k];
+    const {
+      transaction,
+      signers,
+    } = await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, {
+      owner: marketMaker.account,
+      payer: marketMaker.baseToken,
+      side: "sell",
+      price: ask[0],
+      size: ask[1],
+      orderType: "postOnly",
+      clientId: undefined,
+      openOrdersAddressKey: undefined,
+      openOrdersAccount: undefined,
+      feeDiscountPubkey: null,
+      selfTradeBehavior: "abortTransaction",
+    });
+    await provider.send(transaction, signers.concat(marketMaker.account));
+  }
+
+  for (let k = 0; k < bids.length; k += 1) {
+    let bid = bids[k];
+    const {
+      transaction,
+      signers,
+    } = await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, {
+      owner: marketMaker.account,
+      payer: marketMaker.quoteToken,
+      side: "buy",
+      price: bid[0],
+      size: bid[1],
+      orderType: "postOnly",
+      clientId: undefined,
+      openOrdersAddressKey: undefined,
+      openOrdersAccount: undefined,
+      feeDiscountPubkey: null,
+      selfTradeBehavior: "abortTransaction",
+    });
+    await provider.send(transaction, signers.concat(marketMaker.account));
+  }
+
+  return MARKET_A_USDC;
+}
+
+async function listMarket({
+  connection,
+  wallet,
+  baseMint,
+  quoteMint,
+  baseLotSize,
+  quoteLotSize,
+  dexProgramId,
+  feeRateBps,
+}) {
+  const market = new Account();
+  const requestQueue = new Account();
+  const eventQueue = new Account();
+  const bids = new Account();
+  const asks = new Account();
+  const baseVault = new Account();
+  const quoteVault = new Account();
+  const quoteDustThreshold = new BN(100);
+
+  const [vaultOwner, vaultSignerNonce] = await getVaultOwnerAndNonce(
+    market.publicKey,
+    dexProgramId
+  );
+
+  const tx1 = new Transaction();
+  tx1.add(
+    SystemProgram.createAccount({
+      fromPubkey: wallet.publicKey,
+      newAccountPubkey: baseVault.publicKey,
+      lamports: await connection.getMinimumBalanceForRentExemption(165),
+      space: 165,
+      programId: TOKEN_PROGRAM_ID,
+    }),
+    SystemProgram.createAccount({
+      fromPubkey: wallet.publicKey,
+      newAccountPubkey: quoteVault.publicKey,
+      lamports: await connection.getMinimumBalanceForRentExemption(165),
+      space: 165,
+      programId: TOKEN_PROGRAM_ID,
+    }),
+    TokenInstructions.initializeAccount({
+      account: baseVault.publicKey,
+      mint: baseMint,
+      owner: vaultOwner,
+    }),
+    TokenInstructions.initializeAccount({
+      account: quoteVault.publicKey,
+      mint: quoteMint,
+      owner: vaultOwner,
+    })
+  );
+
+  const tx2 = new Transaction();
+  tx2.add(
+    SystemProgram.createAccount({
+      fromPubkey: wallet.publicKey,
+      newAccountPubkey: market.publicKey,
+      lamports: await connection.getMinimumBalanceForRentExemption(
+        Market.getLayout(dexProgramId).span
+      ),
+      space: Market.getLayout(dexProgramId).span,
+      programId: dexProgramId,
+    }),
+    SystemProgram.createAccount({
+      fromPubkey: wallet.publicKey,
+      newAccountPubkey: requestQueue.publicKey,
+      lamports: await connection.getMinimumBalanceForRentExemption(5120 + 12),
+      space: 5120 + 12,
+      programId: dexProgramId,
+    }),
+    SystemProgram.createAccount({
+      fromPubkey: wallet.publicKey,
+      newAccountPubkey: eventQueue.publicKey,
+      lamports: await connection.getMinimumBalanceForRentExemption(262144 + 12),
+      space: 262144 + 12,
+      programId: dexProgramId,
+    }),
+    SystemProgram.createAccount({
+      fromPubkey: wallet.publicKey,
+      newAccountPubkey: bids.publicKey,
+      lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
+      space: 65536 + 12,
+      programId: dexProgramId,
+    }),
+    SystemProgram.createAccount({
+      fromPubkey: wallet.publicKey,
+      newAccountPubkey: asks.publicKey,
+      lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
+      space: 65536 + 12,
+      programId: dexProgramId,
+    }),
+    DexInstructions.initializeMarket({
+      market: market.publicKey,
+      requestQueue: requestQueue.publicKey,
+      eventQueue: eventQueue.publicKey,
+      bids: bids.publicKey,
+      asks: asks.publicKey,
+      baseVault: baseVault.publicKey,
+      quoteVault: quoteVault.publicKey,
+      baseMint,
+      quoteMint,
+      baseLotSize: new BN(baseLotSize),
+      quoteLotSize: new BN(quoteLotSize),
+      feeRateBps,
+      vaultSignerNonce,
+      quoteDustThreshold,
+      programId: dexProgramId,
+    })
+  );
+
+  const signedTransactions = await signTransactions({
+    transactionsAndSigners: [
+      { transaction: tx1, signers: [baseVault, quoteVault] },
+      {
+        transaction: tx2,
+        signers: [market, requestQueue, eventQueue, bids, asks],
+      },
+    ],
+    wallet,
+    connection,
+  });
+  for (let signedTransaction of signedTransactions) {
+    await sendAndConfirmRawTransaction(
+      connection,
+      signedTransaction.serialize()
+    );
+  }
+  const acc = await connection.getAccountInfo(market.publicKey);
+
+  return market.publicKey;
+}
+
+async function signTransactions({
+  transactionsAndSigners,
+  wallet,
+  connection,
+}) {
+  const blockhash = (await connection.getRecentBlockhash("max")).blockhash;
+  transactionsAndSigners.forEach(({ transaction, signers = [] }) => {
+    transaction.recentBlockhash = blockhash;
+    transaction.setSigners(
+      wallet.publicKey,
+      ...signers.map((s) => s.publicKey)
+    );
+    if (signers?.length > 0) {
+      transaction.partialSign(...signers);
+    }
+  });
+  return await wallet.signAllTransactions(
+    transactionsAndSigners.map(({ transaction }) => transaction)
+  );
+}
+
+async function sendAndConfirmRawTransaction(
+  connection,
+  raw,
+  commitment = "recent"
+) {
+  let tx = await connection.sendRawTransaction(raw, {
+    skipPreflight: true,
+  });
+  return await connection.confirmTransaction(tx, commitment);
+}
+
+async function getVaultOwnerAndNonce(marketPublicKey, dexProgramId = DEX_PID) {
+  const nonce = new BN(0);
+  while (nonce.toNumber() < 255) {
+    try {
+      const vaultOwner = await PublicKey.createProgramAddress(
+        [marketPublicKey.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)],
+        dexProgramId
+      );
+      return [vaultOwner, nonce];
+    } catch (e) {
+      nonce.iaddn(1);
+    }
+  }
+  throw new Error("Unable to find nonce");
+}
+
+module.exports = {
+  fundAccount,
+  setupMarket,
+  initOrderbook,
+  setupTwoMarkets,
+  DEX_PID,
+  getVaultOwnerAndNonce,
+};

+ 19 - 0
lang/src/context.rs

@@ -34,6 +34,7 @@ where
     T: ToAccountMetas + ToAccountInfos<'info>,
 {
     pub accounts: T,
+    pub remaining_accounts: Vec<AccountInfo<'info>>,
     pub program: AccountInfo<'info>,
     pub signer_seeds: &'a [&'b [&'c [u8]]],
 }
@@ -46,6 +47,7 @@ where
         Self {
             accounts,
             program,
+            remaining_accounts: Vec::new(),
             signer_seeds: &[],
         }
     }
@@ -59,6 +61,7 @@ where
             accounts,
             program,
             signer_seeds,
+            remaining_accounts: Vec::new(),
         }
     }
 
@@ -66,6 +69,20 @@ where
         self.signer_seeds = signer_seeds;
         self
     }
+
+    pub fn with_remaining_accounts(mut self, ra: Vec<AccountInfo<'info>>) -> Self {
+        self.remaining_accounts = ra;
+        self
+    }
+}
+
+impl<'info, T: Accounts<'info>> ToAccountInfos<'info> for CpiContext<'_, '_, '_, 'info, T> {
+    fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
+        let mut infos = self.accounts.to_account_infos();
+        infos.extend_from_slice(&self.remaining_accounts);
+        infos.push(self.program.clone());
+        infos
+    }
 }
 
 /// Context specifying non-argument inputs for cross-program-invocations
@@ -83,6 +100,7 @@ impl<'a, 'b, 'c, 'info, T: Accounts<'info>> CpiStateContext<'a, 'b, 'c, 'info, T
                 accounts,
                 program,
                 signer_seeds: &[],
+                remaining_accounts: Vec::new(),
             },
         }
     }
@@ -99,6 +117,7 @@ impl<'a, 'b, 'c, 'info, T: Accounts<'info>> CpiStateContext<'a, 'b, 'c, 'info, T
                 accounts,
                 program,
                 signer_seeds,
+                remaining_accounts: Vec::new(),
             },
         }
     }

+ 15 - 29
lang/syn/src/codegen/program.rs

@@ -82,6 +82,10 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
             };
 
             dispatch(program_id, accounts, sighash, ix_data)
+                .map_err(|e| {
+                    anchor_lang::solana_program::msg!(&e.to_string());
+                    e
+                })
         }
 
         #dispatch
@@ -355,9 +359,12 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
                 accounts: &mut anchor_lang::idl::IdlCreateAccounts,
                 data_len: u64,
             ) -> ProgramResult {
+                if program_id != accounts.program.key {
+                    return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(98)); // todo proper error
+                }
                 // Create the IDL's account.
                 let from = accounts.from.key;
-                let (base, nonce) = Pubkey::find_program_address(&[], accounts.program.key);
+                let (base, nonce) = Pubkey::find_program_address(&[], program_id);
                 let seed = anchor_lang::idl::IdlAccount::seed();
                 let owner = accounts.program.key;
                 let to = Pubkey::create_with_seed(&base, seed, owner).unwrap();
@@ -513,10 +520,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
                                         remaining_accounts,
                                     ),
                                     #(#ctor_untyped_args),*
-                                ).map_err(|e| {
-                                    anchor_lang::solana_program::msg!(&e.to_string());
-                                    e
-                                })?;
+                                )?;
                             }
 
                             // Exit routines.
@@ -546,10 +550,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
                                     remaining_accounts,
                                 ),
                                 #(#ctor_untyped_args),*
-                            ).map_err(|e| {
-                                anchor_lang::solana_program::msg!(&e.to_string());
-                                e
-                            })?;
+                            )?;
 
                             // Create the solana account for the ctor data.
                             let from = ctor_accounts.from.key;
@@ -647,10 +648,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
                                         state.#ix_name(
                                             ctx,
                                             #(#ix_arg_names),*
-                                        ).map_err(|e| {
-                                            anchor_lang::solana_program::msg!(&e.to_string());
-                                            e
-                                        })?;
+                                        )?;
                                     }
                                     // Serialize the state and save it to storage.
                                     accounts.exit(program_id)?;
@@ -693,10 +691,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
                                     state.#ix_name(
                                         ctx,
                                         #(#ix_arg_names),*
-                                    ).map_err(|e| {
-                                        anchor_lang::solana_program::msg!(&e.to_string());
-                                        e
-                                    })?;
+                                    )?;
 
                                     // Serialize the state and save it to storage.
                                     accounts.exit(program_id)?;
@@ -781,10 +776,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
                                             state.#ix_name(
                                                 ctx,
                                                 #(#ix_arg_names),*
-                                            ).map_err(|e| {
-                                                anchor_lang::solana_program::msg!(&e.to_string());
-                                                e
-                                            })?;
+                                            )?;
 
                                             // Serialize the state and save it to storage.
                                             accounts.exit(program_id)?;
@@ -813,10 +805,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
                                             #state_name::#ix_name(
                                                 Context::new(program_id, &mut accounts, remaining_accounts),
                                                 #(#ix_arg_names),*
-                                            ).map_err(|e| {
-                                                anchor_lang::solana_program::msg!(&e.to_string());
-                                                e
-                                            })?;
+                                            )?;
                                             accounts.exit(program_id)
                                         }
                                     }
@@ -849,10 +838,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
                     #program_name::#ix_name(
                         Context::new(program_id, &mut accounts, remaining_accounts),
                         #(#ix_arg_names),*
-                    ).map_err(|e| {
-                        anchor_lang::solana_program::msg!(&e.to_string());
-                        e
-                    })?;
+                    )?;
                     accounts.exit(program_id)
                 }
             }

+ 3 - 1
spl/Cargo.toml

@@ -8,5 +8,7 @@ description = "CPI clients for SPL programs"
 
 [dependencies]
 anchor-lang = { path = "../lang", version = "0.4.4", features = ["derive"] }
-spl-token = { version = "3.0.1", features = ["no-entrypoint"] }
+lazy_static = "1.4.0"
+serum_dex = { git = "https://github.com/project-serum/serum-dex", features = ["no-entrypoint"] }
 solana-program = "1.6.6"
+spl-token = { version = "3.0.1", features = ["no-entrypoint"] }

+ 114 - 0
spl/src/dex.rs

@@ -0,0 +1,114 @@
+use anchor_lang::solana_program::account_info::AccountInfo;
+use anchor_lang::solana_program::entrypoint::ProgramResult;
+use anchor_lang::{Accounts, CpiContext, ToAccountInfos};
+use serum_dex::instruction::SelfTradeBehavior;
+use serum_dex::matching::{OrderType, Side};
+use std::num::NonZeroU64;
+
+pub use serum_dex;
+
+anchor_lang::solana_program::declare_id!("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
+
+pub fn new_order_v3<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, NewOrderV3<'info>>,
+    side: Side,
+    limit_price: NonZeroU64,
+    max_coin_qty: NonZeroU64,
+    max_native_pc_qty_including_fees: NonZeroU64,
+    self_trade_behavior: SelfTradeBehavior,
+    order_type: OrderType,
+    client_order_id: u64,
+    limit: u16,
+) -> ProgramResult {
+    let referral = ctx.remaining_accounts.iter().next();
+    let ix = serum_dex::instruction::new_order(
+        ctx.accounts.market.key,
+        ctx.accounts.open_orders.key,
+        ctx.accounts.request_queue.key,
+        ctx.accounts.event_queue.key,
+        ctx.accounts.market_bids.key,
+        ctx.accounts.market_asks.key,
+        ctx.accounts.order_payer_token_account.key,
+        ctx.accounts.open_orders_authority.key,
+        ctx.accounts.coin_vault.key,
+        ctx.accounts.pc_vault.key,
+        ctx.accounts.token_program.key,
+        ctx.accounts.rent.key,
+        referral.map(|r| r.key),
+        &ID,
+        side,
+        limit_price,
+        max_coin_qty,
+        order_type,
+        client_order_id,
+        self_trade_behavior,
+        limit,
+        max_native_pc_qty_including_fees,
+    )?;
+    solana_program::program::invoke_signed(
+        &ix,
+        &ToAccountInfos::to_account_infos(&ctx),
+        ctx.signer_seeds,
+    )?;
+    Ok(())
+}
+
+pub fn settle_funds<'info>(
+    ctx: CpiContext<'_, '_, '_, 'info, SettleFunds<'info>>,
+) -> ProgramResult {
+    let referral = ctx.remaining_accounts.iter().next();
+    let ix = serum_dex::instruction::settle_funds(
+        &ID,
+        ctx.accounts.market.key,
+        ctx.accounts.token_program.key,
+        ctx.accounts.open_orders.key,
+        ctx.accounts.open_orders_authority.key,
+        ctx.accounts.coin_vault.key,
+        ctx.accounts.coin_wallet.key,
+        ctx.accounts.pc_vault.key,
+        ctx.accounts.pc_wallet.key,
+        referral.map(|r| r.key),
+        ctx.accounts.vault_signer.key,
+    )?;
+    solana_program::program::invoke_signed(
+        &ix,
+        &ToAccountInfos::to_account_infos(&ctx),
+        ctx.signer_seeds,
+    )?;
+    Ok(())
+}
+
+#[derive(Accounts)]
+pub struct NewOrderV3<'info> {
+    pub market: AccountInfo<'info>,
+    pub open_orders: AccountInfo<'info>,
+    pub request_queue: AccountInfo<'info>,
+    pub event_queue: AccountInfo<'info>,
+    pub market_bids: AccountInfo<'info>,
+    pub market_asks: AccountInfo<'info>,
+    // Token account where funds are transferred from for the order. If
+    // posting a bid market A/B, then this is the SPL token account for B.
+    pub order_payer_token_account: AccountInfo<'info>,
+    pub open_orders_authority: AccountInfo<'info>,
+    // Also known as the "base" currency. For a given A/B market,
+    // this is the vault for the A mint.
+    pub coin_vault: AccountInfo<'info>,
+    // Also known as the "quote" currency. For a given A/B market,
+    // this is the vault for the B mint.
+    pub pc_vault: AccountInfo<'info>,
+    pub token_program: AccountInfo<'info>,
+    pub rent: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct SettleFunds<'info> {
+    pub market: AccountInfo<'info>,
+    pub open_orders: AccountInfo<'info>,
+    pub open_orders_authority: AccountInfo<'info>,
+    pub coin_vault: AccountInfo<'info>,
+    pub pc_vault: AccountInfo<'info>,
+    pub coin_wallet: AccountInfo<'info>,
+    pub pc_wallet: AccountInfo<'info>,
+    pub vault_signer: AccountInfo<'info>,
+    pub token_program: AccountInfo<'info>,
+}

+ 1 - 0
spl/src/lib.rs

@@ -1,2 +1,3 @@
+pub mod dex;
 pub mod shmem;
 pub mod token;

+ 21 - 0
spl/src/token.rs

@@ -3,6 +3,7 @@ use anchor_lang::solana_program::account_info::AccountInfo;
 use anchor_lang::solana_program::entrypoint::ProgramResult;
 use anchor_lang::solana_program::program_error::ProgramError;
 use anchor_lang::solana_program::program_pack::Pack;
+use anchor_lang::solana_program::pubkey::Pubkey;
 use anchor_lang::{Accounts, CpiContext};
 use std::ops::Deref;
 
@@ -201,3 +202,23 @@ impl Deref for Mint {
         &self.0
     }
 }
+
+// Field parsers to save compute. All account validation is assumed to be done
+// outside of these methods.
+pub mod accessor {
+    use super::*;
+
+    pub fn amount<'info>(account: &AccountInfo<'info>) -> Result<u64, ProgramError> {
+        let bytes = account.try_borrow_data()?;
+        let mut amount_bytes = [0u8; 8];
+        amount_bytes.copy_from_slice(&bytes[64..72]);
+        Ok(u64::from_le_bytes(amount_bytes))
+    }
+
+    pub fn mint<'info>(account: &AccountInfo<'info>) -> Result<Pubkey, ProgramError> {
+        let bytes = account.try_borrow_data()?;
+        let mut mint_bytes = [0u8; 32];
+        mint_bytes.copy_from_slice(&bytes[..32]);
+        Ok(Pubkey::new_from_array(mint_bytes))
+    }
+}