examples: Swap on the serum orderbook (#224)
This commit is contained in:
parent
af735a49cf
commit
a1464d14d5
|
@ -0,0 +1,3 @@
|
|||
[submodule "examples/swap/deps/serum-dex"]
|
||||
path = examples/swap/deps/serum-dex
|
||||
url = https://github.com/project-serum/serum-dex
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -8,3 +8,6 @@ members = [
|
|||
"lang/syn",
|
||||
"spl",
|
||||
]
|
||||
exclude = [
|
||||
"examples/swap/deps/serum-dex"
|
||||
]
|
|
@ -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"
|
|
@ -0,0 +1,7 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
||||
exclude = [
|
||||
"deps/serum-dex"
|
||||
]
|
|
@ -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
|
||||
```
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 19c8e37bf41d044a084b21e58182a50d119d46a2
|
|
@ -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.
|
||||
}
|
|
@ -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" }
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -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,
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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,2 +1,3 @@
|
|||
pub mod dex;
|
||||
pub mod shmem;
|
||||
pub mod token;
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue