dex: Move in permissioned markets (#189)
This commit is contained in:
parent
0c730d678f
commit
298bcd409c
|
@ -4,3 +4,6 @@ target/
|
|||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
node_modules
|
||||
yarn.lock
|
||||
.anchor
|
43
.travis.yml
43
.travis.yml
|
@ -1,51 +1,38 @@
|
|||
dist: bionic
|
||||
language: shell
|
||||
os: linux
|
||||
language: rust
|
||||
rust:
|
||||
- stable
|
||||
cache: cargo
|
||||
env:
|
||||
global:
|
||||
- NODE_VERSION="v14.7.0"
|
||||
- SOLANA_VERSION="v1.6.18"
|
||||
- SOLANA_VERSION="v1.8.0"
|
||||
|
||||
_defaults: &defaults
|
||||
cache: false
|
||||
services:
|
||||
- docker
|
||||
before_install:
|
||||
- scripts/travis/run-docker.sh
|
||||
before_cache:
|
||||
- scripts/travis/stop-docker.sh
|
||||
|
||||
_localnet: &localnet
|
||||
language: rust
|
||||
rust:
|
||||
- stable
|
||||
before_script:
|
||||
- rustup component add rustfmt clippy
|
||||
- nvm install $NODE_VERSION
|
||||
- sudo apt-get install -y pkg-config build-essential libudev-dev
|
||||
- sudo apt-get install -y pkg-config build-essential libudev-dev jq
|
||||
- sh -c "$(curl -sSfL https://release.solana.com/${SOLANA_VERSION}/install)"
|
||||
- export PATH="/home/travis/.local/share/solana/install/active_release/bin:$PATH"
|
||||
- export PATH="/home/travis/.local/share/solana/install/active_release/bin:$PATH"
|
||||
- yes | solana-keygen new
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- <<: *defaults
|
||||
name: Dex tests
|
||||
<<: *localnet
|
||||
script:
|
||||
- docker exec dev ./scripts/travis/dex-tests.sh
|
||||
- ./scripts/travis/dex-tests.sh
|
||||
- <<: *defaults
|
||||
name: Permissioned Dex tests
|
||||
script:
|
||||
- cd dex/tests/permissioned/ && yarn && yarn build && yarn test && cd ../../../
|
||||
- <<: *defaults
|
||||
name: Fmt and Common Tests
|
||||
script:
|
||||
- docker exec dev cargo build
|
||||
- docker exec dev cargo fmt -- --check
|
||||
- docker exec -w=/workdir/common dev cargo test --features client,strict
|
||||
- <<: *localnet
|
||||
name: Permissioned Markets Tests
|
||||
script:
|
||||
- mkdir -p /tmp/tests && cd /tmp/tests
|
||||
- git clone --recursive https://github.com/project-serum/permissioned-markets-quickstart.git
|
||||
- cd permissioned-markets-quickstart
|
||||
- yarn
|
||||
- yarn build
|
||||
- yarn test
|
||||
- cargo build
|
||||
- cargo fmt -- --check
|
||||
- cargo test --features client,strict
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,6 +3,7 @@ members = [
|
|||
"assert-owner",
|
||||
"common",
|
||||
"dex/crank",
|
||||
"dex/permissioned",
|
||||
"pool",
|
||||
"pool/schema",
|
||||
"pool/examples/admin-controlled",
|
||||
|
|
|
@ -76,6 +76,5 @@ The easiest way to run a local cluster is to use [solana-test-validator](https:/
|
|||
* `assert-owner`: Solana utility program for checking account ownership.
|
||||
* `common`: Common rust utilities.
|
||||
* `dex`: Serum DEX program and client utility.
|
||||
* `docker`: Docker image definitions.
|
||||
* `pool`: Serum pool protocol.
|
||||
* `scripts`: Bash scripts for development.
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "serum-dex-permissioned"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = "0.18.0"
|
||||
anchor-spl = "0.18.0"
|
||||
serum_dex = { path = "../" }
|
||||
spl-token = { version = "3.1.1", features = ["no-entrypoint"] }
|
|
@ -0,0 +1,5 @@
|
|||
mod middleware;
|
||||
mod proxy;
|
||||
|
||||
pub use middleware::*;
|
||||
pub use proxy::*;
|
|
@ -0,0 +1,548 @@
|
|||
use crate::{open_orders_authority, open_orders_init_authority};
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::solana_program::instruction::Instruction;
|
||||
use anchor_lang::solana_program::system_program;
|
||||
use anchor_lang::Accounts;
|
||||
use anchor_spl::{dex, token};
|
||||
use serum_dex::instruction::*;
|
||||
use serum_dex::matching::Side;
|
||||
use serum_dex::state::OpenOrders;
|
||||
use std::mem::size_of;
|
||||
|
||||
/// Per request context. Can be used to share data between middleware handlers.
|
||||
pub struct Context<'a, 'info> {
|
||||
pub program_id: &'a Pubkey,
|
||||
pub dex_program_id: &'a Pubkey,
|
||||
pub accounts: Vec<AccountInfo<'info>>,
|
||||
pub seeds: Seeds,
|
||||
// Instructions to execute *prior* to the DEX relay CPI.
|
||||
pub pre_instructions: Vec<(Instruction, Vec<AccountInfo<'info>>, Seeds)>,
|
||||
// Instructions to execution *after* the DEX relay CPI.
|
||||
pub post_instructions: Vec<(Instruction, Vec<AccountInfo<'info>>, Seeds)>,
|
||||
}
|
||||
|
||||
type Seeds = Vec<Vec<Vec<u8>>>;
|
||||
|
||||
impl<'a, 'info> Context<'a, 'info> {
|
||||
pub fn new(
|
||||
program_id: &'a Pubkey,
|
||||
dex_program_id: &'a Pubkey,
|
||||
accounts: Vec<AccountInfo<'info>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
program_id,
|
||||
dex_program_id,
|
||||
accounts,
|
||||
seeds: Vec::new(),
|
||||
pre_instructions: Vec::new(),
|
||||
post_instructions: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementing this trait allows one to hook into requests to the Serum DEX
|
||||
/// via a frontend proxy.
|
||||
pub trait MarketMiddleware {
|
||||
/// Called before any instruction, giving middleware access to the raw
|
||||
/// instruction data. This can be used to access extra data that is
|
||||
/// prepended to the DEX data, allowing one to expand the capabilities of
|
||||
/// any instruction by reading the instruction data here and then
|
||||
/// using it in any of the method handlers.
|
||||
fn instruction(&mut self, _data: &mut &[u8]) -> ProgramResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_open_orders(&self, _ctx: &mut Context) -> ProgramResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_order_v3(&self, _ctx: &mut Context, _ix: &NewOrderInstructionV3) -> ProgramResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cancel_order_v2(&self, _ctx: &mut Context, _ix: &CancelOrderInstructionV2) -> ProgramResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cancel_order_by_client_id_v2(&self, _ctx: &mut Context, _client_id: u64) -> ProgramResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn settle_funds(&self, _ctx: &mut Context) -> ProgramResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close_open_orders(&self, _ctx: &mut Context) -> ProgramResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prune(&self, _ctx: &mut Context, _limit: u16) -> ProgramResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Called when the instruction data doesn't match any DEX instruction.
|
||||
fn fallback(&self, _ctx: &mut Context) -> ProgramResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that the given open orders account signs the transaction and then
|
||||
/// replaces it with the open orders account, which must be a PDA.
|
||||
#[derive(Default)]
|
||||
pub struct OpenOrdersPda {
|
||||
bump: u8,
|
||||
bump_init: u8,
|
||||
}
|
||||
|
||||
impl OpenOrdersPda {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
bump: 0,
|
||||
bump_init: 0,
|
||||
}
|
||||
}
|
||||
fn prepare_pda<'info>(acc_info: &AccountInfo<'info>) -> AccountInfo<'info> {
|
||||
let mut acc_info = acc_info.clone();
|
||||
acc_info.is_signer = true;
|
||||
acc_info
|
||||
}
|
||||
}
|
||||
|
||||
impl MarketMiddleware for OpenOrdersPda {
|
||||
fn instruction(&mut self, data: &mut &[u8]) -> ProgramResult {
|
||||
// Strip the discriminator.
|
||||
let disc = data[0];
|
||||
*data = &data[1..];
|
||||
|
||||
// Discriminator == 0 implies it's the init instruction.
|
||||
if disc == 0 {
|
||||
self.bump = data[0];
|
||||
self.bump_init = data[1];
|
||||
*data = &data[2..];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Accounts:
|
||||
///
|
||||
/// 0. Dex program.
|
||||
/// 1. System program.
|
||||
/// .. serum_dex::MarketInstruction::InitOpenOrders.
|
||||
///
|
||||
/// Data:
|
||||
///
|
||||
/// 0. Discriminant.
|
||||
/// 1..2 Borsh(struct { bump: u8, bump_init: u8 }).
|
||||
/// ..
|
||||
fn init_open_orders<'a, 'info>(&self, ctx: &mut Context<'a, 'info>) -> ProgramResult {
|
||||
let market = &ctx.accounts[4];
|
||||
let user = &ctx.accounts[3];
|
||||
|
||||
// Initialize PDA.
|
||||
let mut accounts = &ctx.accounts[..];
|
||||
InitAccount::try_accounts(ctx.program_id, &mut accounts, &[self.bump, self.bump_init])?;
|
||||
|
||||
// Add signer to context.
|
||||
ctx.seeds.push(open_orders_authority! {
|
||||
program = ctx.program_id,
|
||||
dex_program = ctx.dex_program_id,
|
||||
market = market.key,
|
||||
authority = user.key,
|
||||
bump = self.bump
|
||||
});
|
||||
ctx.seeds.push(open_orders_init_authority! {
|
||||
program = ctx.program_id,
|
||||
dex_program = ctx.dex_program_id,
|
||||
market = market.key,
|
||||
bump = self.bump_init
|
||||
});
|
||||
|
||||
// Chop off the first two accounts needed for initializing the PDA.
|
||||
ctx.accounts = (&ctx.accounts[2..]).to_vec();
|
||||
|
||||
// Set PDAs.
|
||||
ctx.accounts[1] = Self::prepare_pda(&ctx.accounts[0]);
|
||||
ctx.accounts[4].is_signer = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Accounts:
|
||||
///
|
||||
/// ..
|
||||
///
|
||||
/// Data:
|
||||
///
|
||||
/// 0. Discriminant.
|
||||
/// ..
|
||||
fn new_order_v3(&self, ctx: &mut Context, ix: &NewOrderInstructionV3) -> ProgramResult {
|
||||
// The user must authorize the tx.
|
||||
let user = &ctx.accounts[7];
|
||||
if !user.is_signer {
|
||||
return Err(ErrorCode::UnauthorizedUser.into());
|
||||
}
|
||||
|
||||
let market = &ctx.accounts[0];
|
||||
let open_orders = &ctx.accounts[1];
|
||||
let token_account_payer = &ctx.accounts[6];
|
||||
|
||||
// Pre: Give the PDA delegate access.
|
||||
let pre_instruction = {
|
||||
let amount = match ix.side {
|
||||
Side::Bid => ix.max_native_pc_qty_including_fees.get(),
|
||||
Side::Ask => {
|
||||
// +5 for padding.
|
||||
let coin_lot_idx = 5 + 43 * 8;
|
||||
let data = market.try_borrow_data()?;
|
||||
let mut coin_lot_array = [0u8; 8];
|
||||
coin_lot_array.copy_from_slice(&data[coin_lot_idx..coin_lot_idx + 8]);
|
||||
let coin_lot_size = u64::from_le_bytes(coin_lot_array);
|
||||
ix.max_coin_qty.get().checked_mul(coin_lot_size).unwrap()
|
||||
}
|
||||
};
|
||||
let ix = spl_token::instruction::approve(
|
||||
&spl_token::ID,
|
||||
token_account_payer.key,
|
||||
open_orders.key,
|
||||
user.key,
|
||||
&[],
|
||||
amount,
|
||||
)?;
|
||||
let accounts = vec![
|
||||
token_account_payer.clone(),
|
||||
open_orders.clone(),
|
||||
user.clone(),
|
||||
];
|
||||
(ix, accounts, Vec::new())
|
||||
};
|
||||
ctx.pre_instructions.push(pre_instruction);
|
||||
|
||||
// Post: Revoke the PDA's delegate access.
|
||||
let post_instruction = {
|
||||
let ix = spl_token::instruction::revoke(
|
||||
&spl_token::ID,
|
||||
token_account_payer.key,
|
||||
user.key,
|
||||
&[],
|
||||
)?;
|
||||
let accounts = vec![token_account_payer.clone(), user.clone()];
|
||||
(ix, accounts, Vec::new())
|
||||
};
|
||||
ctx.post_instructions.push(post_instruction);
|
||||
|
||||
// Proxy: PDA must sign the new order.
|
||||
ctx.seeds.push(open_orders_authority! {
|
||||
program = ctx.program_id,
|
||||
dex_program = ctx.dex_program_id,
|
||||
market = market.key,
|
||||
authority = user.key
|
||||
});
|
||||
ctx.accounts[7] = Self::prepare_pda(open_orders);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Accounts:
|
||||
///
|
||||
/// ..
|
||||
///
|
||||
/// Data:
|
||||
///
|
||||
/// 0. Discriminant.
|
||||
/// ..
|
||||
fn cancel_order_v2(&self, ctx: &mut Context, _ix: &CancelOrderInstructionV2) -> ProgramResult {
|
||||
let market = &ctx.accounts[0];
|
||||
let user = &ctx.accounts[4];
|
||||
if !user.is_signer {
|
||||
return Err(ErrorCode::UnauthorizedUser.into());
|
||||
}
|
||||
|
||||
ctx.seeds.push(open_orders_authority! {
|
||||
program = ctx.program_id,
|
||||
dex_program = ctx.dex_program_id,
|
||||
market = market.key,
|
||||
authority = user.key
|
||||
});
|
||||
|
||||
ctx.accounts[4] = Self::prepare_pda(&ctx.accounts[3]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Accounts:
|
||||
///
|
||||
/// ..
|
||||
///
|
||||
/// Data:
|
||||
///
|
||||
/// 0. Discriminant.
|
||||
/// ..
|
||||
fn cancel_order_by_client_id_v2(&self, ctx: &mut Context, _client_id: u64) -> ProgramResult {
|
||||
let market = &ctx.accounts[0];
|
||||
let user = &ctx.accounts[4];
|
||||
if !user.is_signer {
|
||||
return Err(ErrorCode::UnauthorizedUser.into());
|
||||
}
|
||||
|
||||
ctx.seeds.push(open_orders_authority! {
|
||||
program = ctx.program_id,
|
||||
dex_program = ctx.dex_program_id,
|
||||
market = market.key,
|
||||
authority = user.key
|
||||
});
|
||||
|
||||
ctx.accounts[4] = Self::prepare_pda(&ctx.accounts[3]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Accounts:
|
||||
///
|
||||
/// ..
|
||||
///
|
||||
/// Data:
|
||||
///
|
||||
/// 0. Discriminant.
|
||||
/// ..
|
||||
fn settle_funds(&self, ctx: &mut Context) -> ProgramResult {
|
||||
let market = &ctx.accounts[0];
|
||||
let user = &ctx.accounts[2];
|
||||
if !user.is_signer {
|
||||
return Err(ErrorCode::UnauthorizedUser.into());
|
||||
}
|
||||
|
||||
ctx.seeds.push(open_orders_authority! {
|
||||
program = ctx.program_id,
|
||||
dex_program = ctx.dex_program_id,
|
||||
market = market.key,
|
||||
authority = user.key
|
||||
});
|
||||
|
||||
ctx.accounts[2] = Self::prepare_pda(&ctx.accounts[1]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Accounts:
|
||||
///
|
||||
/// ..
|
||||
///
|
||||
/// Data:
|
||||
///
|
||||
/// 0. Discriminant.
|
||||
/// ..
|
||||
fn close_open_orders(&self, ctx: &mut Context) -> ProgramResult {
|
||||
let market = &ctx.accounts[3];
|
||||
let user = &ctx.accounts[1];
|
||||
if !user.is_signer {
|
||||
return Err(ErrorCode::UnauthorizedUser.into());
|
||||
}
|
||||
|
||||
ctx.seeds.push(open_orders_authority! {
|
||||
program = ctx.program_id,
|
||||
dex_program = ctx.dex_program_id,
|
||||
market = market.key,
|
||||
authority = user.key
|
||||
});
|
||||
|
||||
ctx.accounts[1] = Self::prepare_pda(&ctx.accounts[0]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Accounts:
|
||||
///
|
||||
/// ..
|
||||
///
|
||||
/// Data:
|
||||
///
|
||||
/// 0. Discriminant.
|
||||
/// ..
|
||||
fn prune(&self, ctx: &mut Context, _limit: u16) -> ProgramResult {
|
||||
// Set owner of open orders to be itself.
|
||||
ctx.accounts[5] = ctx.accounts[4].clone();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Logs each request.
|
||||
pub struct Logger;
|
||||
impl MarketMiddleware for Logger {
|
||||
fn init_open_orders(&self, _ctx: &mut Context) -> ProgramResult {
|
||||
msg!("proxying open orders");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_order_v3(&self, _ctx: &mut Context, ix: &NewOrderInstructionV3) -> ProgramResult {
|
||||
msg!("proxying new order v3 {:?}", ix);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cancel_order_v2(&self, _ctx: &mut Context, ix: &CancelOrderInstructionV2) -> ProgramResult {
|
||||
msg!("proxying cancel order v2 {:?}", ix);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cancel_order_by_client_id_v2(&self, _ctx: &mut Context, client_id: u64) -> ProgramResult {
|
||||
msg!("proxying cancel order by client id v2 {:?}", client_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn settle_funds(&self, _ctx: &mut Context) -> ProgramResult {
|
||||
msg!("proxying settle funds");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close_open_orders(&self, _ctx: &mut Context) -> ProgramResult {
|
||||
msg!("proxying close open orders");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prune(&self, _ctx: &mut Context, limit: u16) -> ProgramResult {
|
||||
msg!("proxying prune {:?}", limit);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Enforces referal fees being sent to the configured address.
|
||||
pub struct ReferralFees {
|
||||
referral: Pubkey,
|
||||
}
|
||||
|
||||
impl ReferralFees {
|
||||
pub fn new(referral: Pubkey) -> Self {
|
||||
Self { referral }
|
||||
}
|
||||
}
|
||||
|
||||
impl MarketMiddleware for ReferralFees {
|
||||
/// Accounts:
|
||||
///
|
||||
/// .. serum_dex::MarketInstruction::SettleFunds.
|
||||
fn settle_funds(&self, ctx: &mut Context) -> ProgramResult {
|
||||
let referral = token::accessor::authority(&ctx.accounts[9])?;
|
||||
require!(referral == self.referral, ErrorCode::InvalidReferral);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Macros.
|
||||
|
||||
/// Returns the seeds used for a user's open orders account PDA.
|
||||
#[macro_export]
|
||||
macro_rules! open_orders_authority {
|
||||
(
|
||||
program = $program:expr,
|
||||
dex_program = $dex_program:expr,
|
||||
market = $market:expr,
|
||||
authority = $authority:expr,
|
||||
bump = $bump:expr
|
||||
) => {
|
||||
vec![
|
||||
b"open-orders".to_vec(),
|
||||
$dex_program.as_ref().to_vec(),
|
||||
$market.as_ref().to_vec(),
|
||||
$authority.as_ref().to_vec(),
|
||||
vec![$bump],
|
||||
]
|
||||
};
|
||||
(
|
||||
program = $program:expr,
|
||||
dex_program = $dex_program:expr,
|
||||
market = $market:expr,
|
||||
authority = $authority:expr
|
||||
) => {
|
||||
vec![
|
||||
b"open-orders".to_vec(),
|
||||
$dex_program.as_ref().to_vec(),
|
||||
$market.as_ref().to_vec(),
|
||||
$authority.as_ref().to_vec(),
|
||||
vec![
|
||||
Pubkey::find_program_address(
|
||||
&[
|
||||
b"open-orders".as_ref(),
|
||||
$dex_program.as_ref(),
|
||||
$market.as_ref(),
|
||||
$authority.as_ref(),
|
||||
],
|
||||
$program,
|
||||
)
|
||||
.1,
|
||||
],
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the seeds used for the open orders init authority.
|
||||
/// This is the account that must sign to create a new open orders account on
|
||||
/// the DEX market.
|
||||
#[macro_export]
|
||||
macro_rules! open_orders_init_authority {
|
||||
(
|
||||
program = $program:expr,
|
||||
dex_program = $dex_program:expr,
|
||||
market = $market:expr,
|
||||
bump = $bump:expr
|
||||
) => {
|
||||
vec![
|
||||
b"open-orders-init".to_vec(),
|
||||
$dex_program.as_ref().to_vec(),
|
||||
$market.as_ref().to_vec(),
|
||||
vec![$bump],
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Errors.
|
||||
|
||||
#[error(offset = 500)]
|
||||
pub enum ErrorCode {
|
||||
#[msg("Program ID does not match the Serum DEX")]
|
||||
InvalidDexPid,
|
||||
#[msg("Invalid instruction given")]
|
||||
InvalidInstruction,
|
||||
#[msg("Could not unpack the instruction")]
|
||||
CannotUnpack,
|
||||
#[msg("Invalid referral address given")]
|
||||
InvalidReferral,
|
||||
#[msg("The user didn't sign")]
|
||||
UnauthorizedUser,
|
||||
#[msg("Not enough accounts were provided")]
|
||||
NotEnoughAccounts,
|
||||
#[msg("Invalid target program ID")]
|
||||
InvalidTargetProgram,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(bump: u8, bump_init: u8)]
|
||||
pub struct InitAccount<'info> {
|
||||
#[account(address = dex::ID)]
|
||||
pub dex_program: AccountInfo<'info>,
|
||||
#[account(address = system_program::ID)]
|
||||
pub system_program: AccountInfo<'info>,
|
||||
#[account(
|
||||
init,
|
||||
seeds = [b"open-orders", dex_program.key.as_ref(), market.key.as_ref(), authority.key.as_ref()],
|
||||
bump = bump,
|
||||
payer = authority,
|
||||
owner = dex::ID,
|
||||
space = size_of::<OpenOrders>() + SERUM_PADDING,
|
||||
)]
|
||||
pub open_orders: AccountInfo<'info>,
|
||||
#[account(signer)]
|
||||
pub authority: AccountInfo<'info>,
|
||||
pub market: AccountInfo<'info>,
|
||||
pub rent: Sysvar<'info, Rent>,
|
||||
#[account(
|
||||
seeds = [b"open-orders-init", dex_program.key.as_ref(), market.key.as_ref()],
|
||||
bump = bump_init,
|
||||
)]
|
||||
pub open_orders_init_authority: AccountInfo<'info>,
|
||||
}
|
||||
|
||||
// Constants.
|
||||
|
||||
// Padding added to every serum account.
|
||||
//
|
||||
// b"serum".len() + b"padding".len().
|
||||
const SERUM_PADDING: usize = 12;
|
|
@ -0,0 +1,177 @@
|
|||
use crate::{Context, ErrorCode, MarketMiddleware};
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::solana_program::program;
|
||||
use anchor_lang::solana_program::pubkey::Pubkey;
|
||||
use anchor_spl::dex;
|
||||
use serum_dex::instruction::*;
|
||||
|
||||
/// MarketProxy provides an abstraction for implementing proxy programs to the
|
||||
/// Serum orderbook, allowing one to implement a middleware for the purposes
|
||||
/// of intercepting and modifying requests before being relayed to the
|
||||
/// orderbook.
|
||||
///
|
||||
/// The only requirement for a middleware is that, when all are done processing,
|
||||
/// a valid DEX instruction--accounts and instruction data--must be left to
|
||||
/// forward to the orderbook program.
|
||||
#[derive(Default)]
|
||||
pub struct MarketProxy<'a> {
|
||||
middlewares: Vec<&'a mut dyn MarketMiddleware>,
|
||||
}
|
||||
|
||||
impl<'a> MarketProxy<'a> {
|
||||
/// Constructs a new `MarketProxy`.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
middlewares: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder method for adding a middleware to the proxy.
|
||||
pub fn middleware(mut self, mw: &'a mut dyn MarketMiddleware) -> Self {
|
||||
self.middlewares.push(mw);
|
||||
self
|
||||
}
|
||||
|
||||
/// Entrypoint to the program.
|
||||
pub fn run(
|
||||
mut self,
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
data: &[u8],
|
||||
) -> ProgramResult {
|
||||
let mut ix_data = data;
|
||||
|
||||
// First account is the Serum DEX executable--used for CPI.
|
||||
let dex = &accounts[0];
|
||||
require!(dex.key == &dex::ID, ErrorCode::InvalidTargetProgram);
|
||||
let acc_infos = (&accounts[1..]).to_vec();
|
||||
|
||||
// Process the instruction data.
|
||||
for mw in &mut self.middlewares {
|
||||
mw.instruction(&mut ix_data)?;
|
||||
}
|
||||
|
||||
// Request context.
|
||||
let mut ctx = Context::new(program_id, dex.key, acc_infos);
|
||||
|
||||
// Decode instruction.
|
||||
let ix = MarketInstruction::unpack(ix_data);
|
||||
|
||||
// Method dispatch.
|
||||
match ix {
|
||||
Some(MarketInstruction::InitOpenOrders) => {
|
||||
require!(ctx.accounts.len() >= 4, ErrorCode::NotEnoughAccounts);
|
||||
for mw in &self.middlewares {
|
||||
mw.init_open_orders(&mut ctx)?;
|
||||
}
|
||||
}
|
||||
Some(MarketInstruction::NewOrderV3(ix)) => {
|
||||
require!(ctx.accounts.len() >= 12, ErrorCode::NotEnoughAccounts);
|
||||
for mw in &self.middlewares {
|
||||
mw.new_order_v3(&mut ctx, &ix)?;
|
||||
}
|
||||
}
|
||||
Some(MarketInstruction::CancelOrderV2(ix)) => {
|
||||
require!(ctx.accounts.len() >= 6, ErrorCode::NotEnoughAccounts);
|
||||
for mw in &self.middlewares {
|
||||
mw.cancel_order_v2(&mut ctx, &ix)?;
|
||||
}
|
||||
}
|
||||
Some(MarketInstruction::CancelOrderByClientIdV2(ix)) => {
|
||||
require!(ctx.accounts.len() >= 6, ErrorCode::NotEnoughAccounts);
|
||||
for mw in &self.middlewares {
|
||||
mw.cancel_order_by_client_id_v2(&mut ctx, ix)?;
|
||||
}
|
||||
}
|
||||
Some(MarketInstruction::SettleFunds) => {
|
||||
require!(ctx.accounts.len() >= 10, ErrorCode::NotEnoughAccounts);
|
||||
for mw in &self.middlewares {
|
||||
mw.settle_funds(&mut ctx)?;
|
||||
}
|
||||
}
|
||||
Some(MarketInstruction::CloseOpenOrders) => {
|
||||
require!(ctx.accounts.len() >= 4, ErrorCode::NotEnoughAccounts);
|
||||
for mw in &self.middlewares {
|
||||
mw.close_open_orders(&mut ctx)?;
|
||||
}
|
||||
}
|
||||
Some(MarketInstruction::Prune(limit)) => {
|
||||
require!(ctx.accounts.len() >= 7, ErrorCode::NotEnoughAccounts);
|
||||
for mw in &self.middlewares {
|
||||
mw.prune(&mut ctx, limit)?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for mw in &self.middlewares {
|
||||
mw.fallback(&mut ctx)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Extract the middleware adjusted context.
|
||||
let Context {
|
||||
seeds,
|
||||
accounts,
|
||||
pre_instructions,
|
||||
post_instructions,
|
||||
..
|
||||
} = ctx;
|
||||
|
||||
// Execute pre instructions.
|
||||
for (ix, acc_infos, seeds) in pre_instructions {
|
||||
let tmp_signers: Vec<Vec<&[u8]>> = seeds
|
||||
.iter()
|
||||
.map(|seeds| {
|
||||
let seeds: Vec<&[u8]> = seeds.iter().map(|seed| &seed[..]).collect();
|
||||
seeds
|
||||
})
|
||||
.collect();
|
||||
let signers: Vec<&[&[u8]]> = tmp_signers.iter().map(|seeds| &seeds[..]).collect();
|
||||
program::invoke_signed(&ix, &acc_infos, &signers)?;
|
||||
}
|
||||
|
||||
// Execute the main dex relay.
|
||||
{
|
||||
let tmp_signers: Vec<Vec<&[u8]>> = seeds
|
||||
.iter()
|
||||
.map(|seeds| {
|
||||
let seeds: Vec<&[u8]> = seeds.iter().map(|seed| &seed[..]).collect();
|
||||
seeds
|
||||
})
|
||||
.collect();
|
||||
let signers: Vec<&[&[u8]]> = tmp_signers.iter().map(|seeds| &seeds[..]).collect();
|
||||
|
||||
// CPI to the DEX.
|
||||
let dex_accounts = accounts
|
||||
.iter()
|
||||
.map(|acc| AccountMeta {
|
||||
pubkey: *acc.key,
|
||||
is_signer: acc.is_signer,
|
||||
is_writable: acc.is_writable,
|
||||
})
|
||||
.collect();
|
||||
let ix = anchor_lang::solana_program::instruction::Instruction {
|
||||
data: ix_data.to_vec(),
|
||||
accounts: dex_accounts,
|
||||
program_id: dex::ID,
|
||||
};
|
||||
program::invoke_signed(&ix, &accounts, &signers)?;
|
||||
}
|
||||
|
||||
// Execute post instructions.
|
||||
for (ix, acc_infos, seeds) in post_instructions {
|
||||
let tmp_signers: Vec<Vec<&[u8]>> = seeds
|
||||
.iter()
|
||||
.map(|seeds| {
|
||||
let seeds: Vec<&[u8]> = seeds.iter().map(|seed| &seed[..]).collect();
|
||||
seeds
|
||||
})
|
||||
.collect();
|
||||
let signers: Vec<&[&[u8]]> = tmp_signers.iter().map(|seeds| &seeds[..]).collect();
|
||||
program::invoke_signed(&ix, &acc_infos, &signers)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
[provider]
|
||||
cluster = "localnet"
|
||||
wallet = "~/.config/solana/id.json"
|
||||
|
||||
[[test.genesis]]
|
||||
address = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
|
||||
program = "../../target/deploy/serum_dex.so"
|
||||
|
||||
[scripts]
|
||||
test = "npx mocha -t 1000000 tests/"
|
|
@ -0,0 +1,4 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "permissioned-markets-quickstart",
|
||||
"version": "1.0.0",
|
||||
"description": "This repo demonstrates how to create \"permissioned markets\" on Serum via a proxy smart contract. A permissioned market is a regular Serum market with an additional open orders authority, which must sign every transaction to create an open orders account.",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"test": "tests"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "anchor test",
|
||||
"localnet": "./scripts/localnet.sh",
|
||||
"build": "yarn build:dex && anchor build",
|
||||
"build:dex": "cd ../../ && cargo build-bpf && cd tests/permissioned/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/project-serum/permissioned-markets-quickstart.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/project-serum/permissioned-markets-quickstart/issues"
|
||||
},
|
||||
"homepage": "https://github.com/project-serum/permissioned-markets-quickstart#readme",
|
||||
"devDependencies": {
|
||||
"@project-serum/anchor": "^0.18.0",
|
||||
"@project-serum/anchor-cli": "^0.18.2",
|
||||
"@project-serum/common": "^0.0.1-beta.3",
|
||||
"@project-serum/serum": "^0.13.55",
|
||||
"@solana/spl-token": "^0.1.6",
|
||||
"mocha": "^9.0.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "permissioned-markets"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "permissioned_markets"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
no-idl = []
|
||||
cpi = ["no-entrypoint"]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = "0.13.2"
|
||||
anchor-spl = "0.13.2"
|
||||
solana-program = "=1.7.8"
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,233 @@
|
|||
// Note. This example depends on unreleased Serum DEX changes.
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::dex::serum_dex::instruction::{CancelOrderInstructionV2, NewOrderInstructionV3};
|
||||
use anchor_spl::dex::{
|
||||
Context, Logger, MarketMiddleware, MarketProxy, OpenOrdersPda, ReferralFees,
|
||||
};
|
||||
use solana_program::account_info::AccountInfo;
|
||||
use solana_program::entrypoint::ProgramResult;
|
||||
use solana_program::pubkey::Pubkey;
|
||||
use solana_program::sysvar::rent;
|
||||
|
||||
/// # Permissioned Markets
|
||||
///
|
||||
/// This demonstrates how to create "permissioned markets" on Serum via a proxy.
|
||||
/// A permissioned market is a regular Serum market with an additional
|
||||
/// open orders authority, which must sign every transaction to create an open
|
||||
/// orders account.
|
||||
///
|
||||
/// In practice, what this means is that one can create a program that acts
|
||||
/// as this authority *and* that marks its own PDAs as the *owner* of all
|
||||
/// created open orders accounts, making the program the sole arbiter over
|
||||
/// who can trade on a given market.
|
||||
///
|
||||
/// For example, this example forces all trades that execute on this market
|
||||
/// to set the referral to a hardcoded address--`referral::ID`--and requires
|
||||
/// the client to pass in an identity token, authorizing the user.
|
||||
///
|
||||
/// # Extending the proxy via middleware
|
||||
///
|
||||
/// To implement a custom proxy, one can implement the `MarketMiddleware` trait
|
||||
/// to intercept, modify, and perform any access control on DEX requests before
|
||||
/// they get forwarded to the orderbook. These middleware can be mixed and
|
||||
/// matched. Note, however, that the order of middleware matters since they can
|
||||
/// mutate the request.
|
||||
///
|
||||
/// One useful pattern is to treat the request like layers of an onion, where
|
||||
/// each middleware unwraps the request by stripping accounts and instruction
|
||||
/// data before relaying it to the next middleware and ultimately to the
|
||||
/// orderbook. This allows one to easily extend the behavior of a proxy by
|
||||
/// adding a custom middleware that may process information that is unknown to
|
||||
/// any other middleware or to the DEX.
|
||||
///
|
||||
/// After adding a middleware, the only additional requirement, of course, is
|
||||
/// to make sure the client sending transactions does the same, but in reverse.
|
||||
/// It should wrap the transaction in the opposite order. For convenience, an
|
||||
/// identical abstraction is provided in the JavaScript client.
|
||||
///
|
||||
/// # Alternatives to middleware
|
||||
///
|
||||
/// Note that this middleware abstraction is not required to host a
|
||||
/// permissioned market. One could write a regular program that manages the PDAs
|
||||
/// and CPI invocations onesself, if desired.
|
||||
#[program]
|
||||
pub mod permissioned_markets {
|
||||
use super::*;
|
||||
pub fn entry(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
|
||||
MarketProxy::new()
|
||||
.middleware(&mut Logger)
|
||||
.middleware(&mut Identity)
|
||||
.middleware(&mut ReferralFees::new(referral::ID))
|
||||
.middleware(&mut OpenOrdersPda::new())
|
||||
.run(program_id, accounts, data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs token based authorization, confirming the identity of the user.
|
||||
/// The identity token must be given as the fist account.
|
||||
struct Identity;
|
||||
|
||||
impl Identity {
|
||||
fn prepare_pda<'info>(acc_info: &AccountInfo<'info>) -> AccountInfo<'info> {
|
||||
let mut acc_info = acc_info.clone();
|
||||
acc_info.is_signer = true;
|
||||
acc_info
|
||||
}
|
||||
}
|
||||
|
||||
impl MarketMiddleware for Identity {
|
||||
/// Accounts:
|
||||
///
|
||||
/// 0. Authorization token.
|
||||
/// ..
|
||||
fn init_open_orders(&self, ctx: &mut Context) -> ProgramResult {
|
||||
verify_and_strip_auth(ctx)
|
||||
}
|
||||
|
||||
/// Accounts:
|
||||
///
|
||||
/// 0. Authorization token.
|
||||
/// ..
|
||||
fn new_order_v3(&self, ctx: &mut Context, _ix: &NewOrderInstructionV3) -> ProgramResult {
|
||||
verify_and_strip_auth(ctx)
|
||||
}
|
||||
|
||||
/// Accounts:
|
||||
///
|
||||
/// 0. Authorization token.
|
||||
/// ..
|
||||
fn cancel_order_v2(&self, ctx: &mut Context, _ix: &CancelOrderInstructionV2) -> ProgramResult {
|
||||
verify_and_strip_auth(ctx)
|
||||
}
|
||||
|
||||
/// Accounts:
|
||||
///
|
||||
/// 0. Authorization token.
|
||||
/// ..
|
||||
fn cancel_order_by_client_id_v2(&self, ctx: &mut Context, _client_id: u64) -> ProgramResult {
|
||||
verify_and_strip_auth(ctx)
|
||||
}
|
||||
|
||||
/// Accounts:
|
||||
///
|
||||
/// 0. Authorization token.
|
||||
/// ..
|
||||
fn settle_funds(&self, ctx: &mut Context) -> ProgramResult {
|
||||
verify_and_strip_auth(ctx)
|
||||
}
|
||||
|
||||
/// Accounts:
|
||||
///
|
||||
/// 0. Authorization token.
|
||||
/// ..
|
||||
fn close_open_orders(&self, ctx: &mut Context) -> ProgramResult {
|
||||
verify_and_strip_auth(ctx)
|
||||
}
|
||||
|
||||
/// Accounts:
|
||||
///
|
||||
/// 0. Authorization token (revoked).
|
||||
/// ..
|
||||
fn prune(&self, ctx: &mut Context, _limit: u16) -> ProgramResult {
|
||||
verify_revoked_and_strip_auth(ctx)?;
|
||||
|
||||
// Sign with the prune authority.
|
||||
let market = &ctx.accounts[0];
|
||||
ctx.seeds.push(prune_authority! {
|
||||
program = ctx.program_id,
|
||||
dex_program = ctx.dex_program_id,
|
||||
market = market.key
|
||||
});
|
||||
|
||||
ctx.accounts[3] = Self::prepare_pda(&ctx.accounts[3]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Accounts:
|
||||
///
|
||||
/// 0. Authorization token.
|
||||
/// ..
|
||||
fn fallback(&self, ctx: &mut Context) -> ProgramResult {
|
||||
verify_and_strip_auth(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Utils.
|
||||
|
||||
fn verify_and_strip_auth(ctx: &mut Context) -> ProgramResult {
|
||||
// The rent sysvar is used as a dummy example of an identity token.
|
||||
let auth = &ctx.accounts[0];
|
||||
require!(auth.key == &rent::ID, InvalidAuth);
|
||||
|
||||
// Strip off the account before possing on the message.
|
||||
ctx.accounts = (&ctx.accounts[1..]).to_vec();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_revoked_and_strip_auth(ctx: &mut Context) -> ProgramResult {
|
||||
// The rent sysvar is used as a dummy example of an identity token.
|
||||
let auth = &ctx.accounts[0];
|
||||
require!(auth.key != &rent::ID, TokenNotRevoked);
|
||||
|
||||
// Strip off the account before possing on the message.
|
||||
ctx.accounts = (&ctx.accounts[1..]).to_vec();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Macros.
|
||||
|
||||
/// Returns the seeds used for the prune authority.
|
||||
#[macro_export]
|
||||
macro_rules! prune_authority {
|
||||
(
|
||||
program = $program:expr,
|
||||
dex_program = $dex_program:expr,
|
||||
market = $market:expr,
|
||||
bump = $bump:expr
|
||||
) => {
|
||||
vec![
|
||||
b"prune".to_vec(),
|
||||
$dex_program.as_ref().to_vec(),
|
||||
$market.as_ref().to_vec(),
|
||||
vec![$bump],
|
||||
]
|
||||
};
|
||||
(
|
||||
program = $program:expr,
|
||||
dex_program = $dex_program:expr,
|
||||
market = $market:expr
|
||||
) => {
|
||||
vec![
|
||||
b"prune".to_vec(),
|
||||
$dex_program.as_ref().to_vec(),
|
||||
$market.as_ref().to_vec(),
|
||||
vec![
|
||||
Pubkey::find_program_address(
|
||||
&[b"prune".as_ref(), $dex_program.as_ref(), $market.as_ref()],
|
||||
$program,
|
||||
)
|
||||
.1,
|
||||
],
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Error.
|
||||
|
||||
#[error]
|
||||
pub enum ErrorCode {
|
||||
#[msg("Invalid auth token provided")]
|
||||
InvalidAuth,
|
||||
#[msg("Auth token not revoked")]
|
||||
TokenNotRevoked,
|
||||
}
|
||||
|
||||
// Constants.
|
||||
|
||||
pub mod referral {
|
||||
// This is a dummy address for testing. Do not use in production.
|
||||
solana_program::declare_id!("3oSfkjQZKCneYvsCTZc9HViGAPqR8pYr4h9YeGB5ZxHf");
|
||||
}
|
|
@ -0,0 +1,351 @@
|
|||
const assert = require("assert");
|
||||
const { Token, TOKEN_PROGRAM_ID } = require("@solana/spl-token");
|
||||
const anchor = require("@project-serum/anchor");
|
||||
const serum = require("@project-serum/serum");
|
||||
const { BN } = anchor;
|
||||
const {
|
||||
Keypair,
|
||||
Transaction,
|
||||
TransactionInstruction,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
SYSVAR_RENT_PUBKEY,
|
||||
} = anchor.web3;
|
||||
const {
|
||||
DexInstructions,
|
||||
OpenOrders,
|
||||
OpenOrdersPda,
|
||||
Logger,
|
||||
ReferralFees,
|
||||
MarketProxyBuilder,
|
||||
} = serum;
|
||||
const { genesis, sleep } = require("./utils");
|
||||
|
||||
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
|
||||
const REFERRAL_AUTHORITY = new PublicKey(
|
||||
"3oSfkjQZKCneYvsCTZc9HViGAPqR8pYr4h9YeGB5ZxHf"
|
||||
);
|
||||
|
||||
describe("permissioned-markets", () => {
|
||||
// Anchor client setup.
|
||||
const provider = anchor.Provider.env();
|
||||
anchor.setProvider(provider);
|
||||
const program = anchor.workspace.PermissionedMarkets;
|
||||
|
||||
// Token client.
|
||||
let usdcClient;
|
||||
|
||||
// Global DEX accounts and clients shared accross all tests.
|
||||
let marketProxy, tokenAccount, usdcAccount;
|
||||
let openOrders, openOrdersBump, openOrdersInitAuthority, openOrdersBumpinit;
|
||||
let usdcPosted;
|
||||
let referralTokenAddress;
|
||||
|
||||
it("BOILERPLATE: Initializes an orderbook", async () => {
|
||||
const { marketProxyClient, godA, godUsdc, usdc } = await genesis({
|
||||
provider,
|
||||
proxyProgramId: program.programId,
|
||||
});
|
||||
marketProxy = marketProxyClient;
|
||||
usdcAccount = godUsdc;
|
||||
tokenAccount = godA;
|
||||
|
||||
usdcClient = new Token(
|
||||
provider.connection,
|
||||
usdc,
|
||||
TOKEN_PROGRAM_ID,
|
||||
provider.wallet.payer
|
||||
);
|
||||
|
||||
referral = await usdcClient.createAccount(REFERRAL_AUTHORITY);
|
||||
});
|
||||
|
||||
it("BOILERPLATE: Calculates open orders addresses", async () => {
|
||||
const [_openOrders, bump] = await PublicKey.findProgramAddress(
|
||||
[
|
||||
anchor.utils.bytes.utf8.encode("open-orders"),
|
||||
DEX_PID.toBuffer(),
|
||||
marketProxy.market.address.toBuffer(),
|
||||
program.provider.wallet.publicKey.toBuffer(),
|
||||
],
|
||||
program.programId
|
||||
);
|
||||
const [
|
||||
_openOrdersInitAuthority,
|
||||
bumpInit,
|
||||
] = await PublicKey.findProgramAddress(
|
||||
[
|
||||
anchor.utils.bytes.utf8.encode("open-orders-init"),
|
||||
DEX_PID.toBuffer(),
|
||||
marketProxy.market.address.toBuffer(),
|
||||
],
|
||||
program.programId
|
||||
);
|
||||
|
||||
// Save global variables re-used across tests.
|
||||
openOrders = _openOrders;
|
||||
openOrdersBump = bump;
|
||||
openOrdersInitAuthority = _openOrdersInitAuthority;
|
||||
openOrdersBumpInit = bumpInit;
|
||||
});
|
||||
|
||||
it("Creates an open orders account", async () => {
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
await marketProxy.instruction.initOpenOrders(
|
||||
program.provider.wallet.publicKey,
|
||||
marketProxy.market.address,
|
||||
marketProxy.market.address, // Dummy. Replaced by middleware.
|
||||
marketProxy.market.address // Dummy. Replaced by middleware.
|
||||
)
|
||||
);
|
||||
await provider.send(tx);
|
||||
|
||||
const account = await provider.connection.getAccountInfo(openOrders);
|
||||
assert.ok(account.owner.toString() === DEX_PID.toString());
|
||||
});
|
||||
|
||||
it("Posts a bid on the orderbook", async () => {
|
||||
const size = 1;
|
||||
const price = 1;
|
||||
usdcPosted = new BN(
|
||||
marketProxy.market._decoded.quoteLotSize.toNumber()
|
||||
).mul(
|
||||
marketProxy.market
|
||||
.baseSizeNumberToLots(size)
|
||||
.mul(marketProxy.market.priceNumberToLots(price))
|
||||
);
|
||||
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
marketProxy.instruction.newOrderV3({
|
||||
owner: program.provider.wallet.publicKey,
|
||||
payer: usdcAccount,
|
||||
side: "buy",
|
||||
price,
|
||||
size,
|
||||
orderType: "postOnly",
|
||||
clientId: new BN(999),
|
||||
openOrdersAddressKey: openOrders,
|
||||
selfTradeBehavior: "abortTransaction",
|
||||
})
|
||||
);
|
||||
await provider.send(tx);
|
||||
});
|
||||
|
||||
it("Cancels a bid on the orderbook", async () => {
|
||||
// Given.
|
||||
const beforeOoAccount = await OpenOrders.load(
|
||||
provider.connection,
|
||||
openOrders,
|
||||
DEX_PID
|
||||
);
|
||||
|
||||
// When.
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
await marketProxy.instruction.cancelOrderByClientId(
|
||||
program.provider.wallet.publicKey,
|
||||
openOrders,
|
||||
new BN(999)
|
||||
)
|
||||
);
|
||||
await provider.send(tx);
|
||||
|
||||
// Then.
|
||||
const afterOoAccount = await OpenOrders.load(
|
||||
provider.connection,
|
||||
openOrders,
|
||||
DEX_PID
|
||||
);
|
||||
assert.ok(beforeOoAccount.quoteTokenFree.eq(new BN(0)));
|
||||
assert.ok(beforeOoAccount.quoteTokenTotal.eq(usdcPosted));
|
||||
assert.ok(afterOoAccount.quoteTokenFree.eq(usdcPosted));
|
||||
assert.ok(afterOoAccount.quoteTokenTotal.eq(usdcPosted));
|
||||
});
|
||||
|
||||
it("Settles funds on the orderbook", async () => {
|
||||
// Given.
|
||||
const beforeTokenAccount = await usdcClient.getAccountInfo(usdcAccount);
|
||||
|
||||
// When.
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
await marketProxy.instruction.settleFunds(
|
||||
openOrders,
|
||||
provider.wallet.publicKey,
|
||||
tokenAccount,
|
||||
usdcAccount,
|
||||
referral
|
||||
)
|
||||
);
|
||||
await provider.send(tx);
|
||||
|
||||
// Then.
|
||||
const afterTokenAccount = await usdcClient.getAccountInfo(usdcAccount);
|
||||
assert.ok(
|
||||
afterTokenAccount.amount.sub(beforeTokenAccount.amount).toNumber() ===
|
||||
usdcPosted.toNumber()
|
||||
);
|
||||
});
|
||||
|
||||
// Need to crank the cancel so that we can close later.
|
||||
it("Cranks the cancel transaction", async () => {
|
||||
await crankEventQueue(provider, marketProxy);
|
||||
});
|
||||
|
||||
it("Closes an open orders account", async () => {
|
||||
// Given.
|
||||
const beforeAccount = await program.provider.connection.getAccountInfo(
|
||||
program.provider.wallet.publicKey
|
||||
);
|
||||
|
||||
// When.
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
marketProxy.instruction.closeOpenOrders(
|
||||
openOrders,
|
||||
provider.wallet.publicKey,
|
||||
provider.wallet.publicKey
|
||||
)
|
||||
);
|
||||
await provider.send(tx);
|
||||
|
||||
// Then.
|
||||
const afterAccount = await program.provider.connection.getAccountInfo(
|
||||
program.provider.wallet.publicKey
|
||||
);
|
||||
const closedAccount = await program.provider.connection.getAccountInfo(
|
||||
openOrders
|
||||
);
|
||||
assert.ok(23352768 === afterAccount.lamports - beforeAccount.lamports);
|
||||
assert.ok(closedAccount === null);
|
||||
});
|
||||
|
||||
it("Re-opens an open orders account", async () => {
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
await marketProxy.instruction.initOpenOrders(
|
||||
program.provider.wallet.publicKey,
|
||||
marketProxy.market.address,
|
||||
marketProxy.market.address, // Dummy. Replaced by middleware.
|
||||
marketProxy.market.address // Dummy. Replaced by middleware.
|
||||
)
|
||||
);
|
||||
await provider.send(tx);
|
||||
|
||||
const account = await provider.connection.getAccountInfo(openOrders);
|
||||
assert.ok(account.owner.toString() === DEX_PID.toString());
|
||||
});
|
||||
|
||||
it("Posts several bids and asks on the orderbook", async () => {
|
||||
const size = 10;
|
||||
const price = 2;
|
||||
for (let k = 0; k < 10; k += 1) {
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
marketProxy.instruction.newOrderV3({
|
||||
owner: program.provider.wallet.publicKey,
|
||||
payer: usdcAccount,
|
||||
side: "buy",
|
||||
price,
|
||||
size,
|
||||
orderType: "postOnly",
|
||||
clientId: new BN(999),
|
||||
openOrdersAddressKey: openOrders,
|
||||
selfTradeBehavior: "abortTransaction",
|
||||
})
|
||||
);
|
||||
await provider.send(tx);
|
||||
}
|
||||
|
||||
const sizeAsk = 10;
|
||||
const priceAsk = 10;
|
||||
|
||||
for (let k = 0; k < 10; k += 1) {
|
||||
const txAsk = new Transaction();
|
||||
txAsk.add(
|
||||
marketProxy.instruction.newOrderV3({
|
||||
owner: program.provider.wallet.publicKey,
|
||||
payer: tokenAccount,
|
||||
side: "sell",
|
||||
price: priceAsk,
|
||||
size: sizeAsk,
|
||||
orderType: "postOnly",
|
||||
clientId: new BN(1000),
|
||||
openOrdersAddressKey: openOrders,
|
||||
selfTradeBehavior: "abortTransaction",
|
||||
})
|
||||
);
|
||||
await provider.send(txAsk);
|
||||
}
|
||||
});
|
||||
|
||||
it("Prunes the orderbook", async () => {
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
marketProxy.instruction.prune(openOrders, provider.wallet.publicKey)
|
||||
);
|
||||
await provider.send(tx);
|
||||
});
|
||||
|
||||
it("Settles the account", async () => {
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
await marketProxy.instruction.settleFunds(
|
||||
openOrders,
|
||||
provider.wallet.publicKey,
|
||||
tokenAccount,
|
||||
usdcAccount,
|
||||
referral
|
||||
)
|
||||
);
|
||||
await provider.send(tx);
|
||||
});
|
||||
|
||||
it("Cranks the prune transaction", async () => {
|
||||
await crankEventQueue(provider, marketProxy);
|
||||
});
|
||||
|
||||
it("Closes an open orders account", async () => {
|
||||
// Given.
|
||||
const beforeAccount = await program.provider.connection.getAccountInfo(
|
||||
program.provider.wallet.publicKey
|
||||
);
|
||||
|
||||
// When.
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
marketProxy.instruction.closeOpenOrders(
|
||||
openOrders,
|
||||
provider.wallet.publicKey,
|
||||
provider.wallet.publicKey
|
||||
)
|
||||
);
|
||||
await provider.send(tx);
|
||||
|
||||
// Then.
|
||||
const afterAccount = await program.provider.connection.getAccountInfo(
|
||||
program.provider.wallet.publicKey
|
||||
);
|
||||
const closedAccount = await program.provider.connection.getAccountInfo(
|
||||
openOrders
|
||||
);
|
||||
assert.ok(23352768 === afterAccount.lamports - beforeAccount.lamports);
|
||||
assert.ok(closedAccount === null);
|
||||
});
|
||||
});
|
||||
|
||||
async function crankEventQueue(provider, marketProxy) {
|
||||
// TODO: can do this in a single transaction if we covert the pubkey bytes
|
||||
// into a [u64; 4] array and sort. I'm lazy though.
|
||||
let eq = await marketProxy.market.loadEventQueue(provider.connection);
|
||||
while (eq.length > 0) {
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
marketProxy.market.makeConsumeEventsInstruction([eq[0].openOrders], 1)
|
||||
);
|
||||
await provider.send(tx);
|
||||
eq = await marketProxy.market.loadEventQueue(provider.connection);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
const { PublicKey, Account } = require("@project-serum/anchor").web3;
|
||||
|
||||
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
|
||||
|
||||
// This msut be kept in sync with `scripts/localnet.sh`.
|
||||
const PROGRAM_KP = new Account([
|
||||
168,
|
||||
86,
|
||||
206,
|
||||
125,
|
||||
127,
|
||||
105,
|
||||
201,
|
||||
250,
|
||||
37,
|
||||
102,
|
||||
161,
|
||||
124,
|
||||
80,
|
||||
181,
|
||||
60,
|
||||
2,
|
||||
166,
|
||||
123,
|
||||
176,
|
||||
161,
|
||||
228,
|
||||
188,
|
||||
134,
|
||||
186,
|
||||
158,
|
||||
68,
|
||||
197,
|
||||
240,
|
||||
202,
|
||||
193,
|
||||
174,
|
||||
234,
|
||||
167,
|
||||
123,
|
||||
252,
|
||||
186,
|
||||
72,
|
||||
51,
|
||||
203,
|
||||
70,
|
||||
153,
|
||||
234,
|
||||
190,
|
||||
2,
|
||||
134,
|
||||
184,
|
||||
197,
|
||||
156,
|
||||
113,
|
||||
8,
|
||||
65,
|
||||
1,
|
||||
83,
|
||||
220,
|
||||
152,
|
||||
62,
|
||||
200,
|
||||
174,
|
||||
40,
|
||||
180,
|
||||
218,
|
||||
61,
|
||||
224,
|
||||
6,
|
||||
]);
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sleep,
|
||||
DEX_PID,
|
||||
PROGRAM_KP,
|
||||
};
|
|
@ -0,0 +1,97 @@
|
|||
const anchor = require("@project-serum/anchor");
|
||||
const BN = anchor.BN;
|
||||
const { Account, Transaction, SystemProgram } = anchor.web3;
|
||||
const serumCmn = require("@project-serum/common");
|
||||
const { TOKEN_PROGRAM_ID, Token } = require("@solana/spl-token");
|
||||
|
||||
const DECIMALS = 6;
|
||||
|
||||
// Creates mints and a token account funded with each mint.
|
||||
async function createMintGods(provider, mintCount) {
|
||||
// Setup mints with initial tokens owned by the provider.
|
||||
|
||||
let mintGods = [];
|
||||
for (let k = 0; k < mintCount; k += 1) {
|
||||
const [mint, god] = await serumCmn.createMintAndVault(
|
||||
provider,
|
||||
new BN("1000000000000000000"),
|
||||
undefined,
|
||||
DECIMALS
|
||||
);
|
||||
mintGods.push({ mint, god });
|
||||
}
|
||||
|
||||
return mintGods;
|
||||
}
|
||||
|
||||
async function createFundedAccount(provider, mints, newAccount) {
|
||||
if (!newAccount) {
|
||||
newAccount = new Account();
|
||||
}
|
||||
|
||||
const marketMaker = {
|
||||
tokens: {},
|
||||
account: newAccount,
|
||||
};
|
||||
|
||||
// Transfer lamports to market maker.
|
||||
await provider.send(
|
||||
(() => {
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: provider.wallet.publicKey,
|
||||
toPubkey: newAccount.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 } = 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(
|
||||
newAccount.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;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createMintGods,
|
||||
createFundedAccount,
|
||||
DECIMALS,
|
||||
};
|
|
@ -0,0 +1,98 @@
|
|||
const { BN } = require("@project-serum/anchor");
|
||||
const { PublicKey } = require("@project-serum/anchor").web3;
|
||||
const marketProxy = require("./market-proxy");
|
||||
const marketLister = require("./market-lister");
|
||||
const faucet = require("./faucet");
|
||||
const { DEX_PID } = require("./common");
|
||||
const marketMaker = require("./market-maker");
|
||||
|
||||
// Initializes the genesis state for the tests and localnetwork.
|
||||
async function genesis({ provider, proxyProgramId }) {
|
||||
//
|
||||
// Create all mints and funded god accounts.
|
||||
//
|
||||
const mintGods = await faucet.createMintGods(provider, 2);
|
||||
const [mintGodA, mintGodB] = mintGods;
|
||||
|
||||
//
|
||||
// Fund an additional account.
|
||||
//
|
||||
const fundedAccount = await faucet.createFundedAccount(
|
||||
provider,
|
||||
mintGods.map((mintGod) => {
|
||||
return {
|
||||
...mintGod,
|
||||
amount: new BN("10000000000000").muln(10 ** faucet.DECIMALS),
|
||||
};
|
||||
}),
|
||||
marketMaker.KEYPAIR
|
||||
);
|
||||
|
||||
//
|
||||
// Structure the market maker object.
|
||||
//
|
||||
const marketMakerAccounts = {
|
||||
...fundedAccount,
|
||||
baseToken: fundedAccount.tokens[mintGodA.mint.toString()],
|
||||
quoteToken: fundedAccount.tokens[mintGodB.mint.toString()],
|
||||
};
|
||||
|
||||
//
|
||||
// List the market.
|
||||
//
|
||||
const [marketAPublicKey] = await marketLister.list({
|
||||
connection: provider.connection,
|
||||
wallet: provider.wallet,
|
||||
baseMint: mintGodA.mint,
|
||||
quoteMint: mintGodB.mint,
|
||||
baseLotSize: 100000,
|
||||
quoteLotSize: 100,
|
||||
dexProgramId: DEX_PID,
|
||||
proxyProgramId,
|
||||
feeRateBps: 0,
|
||||
});
|
||||
|
||||
//
|
||||
// Load a proxy client for the market.
|
||||
//
|
||||
const marketProxyClient = await marketProxy.load(
|
||||
provider.connection,
|
||||
proxyProgramId,
|
||||
DEX_PID,
|
||||
marketAPublicKey
|
||||
);
|
||||
|
||||
//
|
||||
// Market maker initializes an open orders account.
|
||||
//
|
||||
await marketMaker.initOpenOrders(
|
||||
provider,
|
||||
marketProxyClient,
|
||||
marketMakerAccounts
|
||||
);
|
||||
|
||||
//
|
||||
// Market maker posts trades on the orderbook.
|
||||
//
|
||||
await marketMaker.postOrders(
|
||||
provider,
|
||||
marketProxyClient,
|
||||
marketMakerAccounts
|
||||
);
|
||||
|
||||
//
|
||||
// Done.
|
||||
//
|
||||
return {
|
||||
marketProxyClient,
|
||||
mintA: mintGodA.mint,
|
||||
usdc: mintGodB.mint,
|
||||
godA: mintGodA.god,
|
||||
godUsdc: mintGodB.god,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
genesis,
|
||||
DEX_PID,
|
||||
};
|
|
@ -0,0 +1,244 @@
|
|||
const anchor = require("@project-serum/anchor");
|
||||
const { BN } = anchor;
|
||||
const {
|
||||
Account,
|
||||
PublicKey,
|
||||
Transaction,
|
||||
SystemProgram,
|
||||
} = require("@project-serum/anchor").web3;
|
||||
const { TOKEN_PROGRAM_ID } = require("@solana/spl-token");
|
||||
const serum = require("@project-serum/serum");
|
||||
const {
|
||||
DexInstructions,
|
||||
TokenInstructions,
|
||||
OpenOrdersPda,
|
||||
MARKET_STATE_LAYOUT_V3,
|
||||
} = serum;
|
||||
const { Identity } = require("./market-proxy");
|
||||
const { DEX_PID } = require("./common");
|
||||
|
||||
// Creates a market on the dex.
|
||||
async function list({
|
||||
connection,
|
||||
wallet,
|
||||
baseMint,
|
||||
quoteMint,
|
||||
baseLotSize,
|
||||
quoteLotSize,
|
||||
dexProgramId,
|
||||
proxyProgramId,
|
||||
feeRateBps,
|
||||
}) {
|
||||
const market = MARKET_KP;
|
||||
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_STATE_LAYOUT_V3.span
|
||||
),
|
||||
space: MARKET_STATE_LAYOUT_V3.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,
|
||||
authority: await OpenOrdersPda.marketAuthority(
|
||||
market.publicKey,
|
||||
DEX_PID,
|
||||
proxyProgramId
|
||||
),
|
||||
pruneAuthority: await Identity.pruneAuthority(
|
||||
market.publicKey,
|
||||
DEX_PID,
|
||||
proxyProgramId
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
const transactions = [
|
||||
{ transaction: tx1, signers: [baseVault, quoteVault] },
|
||||
{
|
||||
transaction: tx2,
|
||||
signers: [market, requestQueue, eventQueue, bids, asks],
|
||||
},
|
||||
];
|
||||
for (let tx of transactions) {
|
||||
await anchor.getProvider().send(tx.transaction, tx.signers);
|
||||
}
|
||||
const acc = await connection.getAccountInfo(market.publicKey);
|
||||
|
||||
return [market.publicKey, vaultOwner];
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
// Dummy keypair for a consistent market address. Helpful when doing UI work.
|
||||
// Don't use in production.
|
||||
const MARKET_KP = new Account([
|
||||
13,
|
||||
174,
|
||||
53,
|
||||
150,
|
||||
78,
|
||||
228,
|
||||
12,
|
||||
98,
|
||||
170,
|
||||
254,
|
||||
212,
|
||||
211,
|
||||
125,
|
||||
193,
|
||||
2,
|
||||
241,
|
||||
97,
|
||||
137,
|
||||
49,
|
||||
209,
|
||||
189,
|
||||
199,
|
||||
27,
|
||||
215,
|
||||
220,
|
||||
65,
|
||||
57,
|
||||
203,
|
||||
215,
|
||||
93,
|
||||
105,
|
||||
203,
|
||||
217,
|
||||
32,
|
||||
5,
|
||||
194,
|
||||
157,
|
||||
118,
|
||||
162,
|
||||
47,
|
||||
102,
|
||||
126,
|
||||
235,
|
||||
65,
|
||||
99,
|
||||
80,
|
||||
56,
|
||||
231,
|
||||
217,
|
||||
114,
|
||||
25,
|
||||
225,
|
||||
239,
|
||||
140,
|
||||
169,
|
||||
92,
|
||||
150,
|
||||
146,
|
||||
211,
|
||||
218,
|
||||
183,
|
||||
139,
|
||||
9,
|
||||
104,
|
||||
]);
|
||||
|
||||
module.exports = {
|
||||
list,
|
||||
};
|
|
@ -0,0 +1,160 @@
|
|||
const { Account, Transaction } = require("@project-serum/anchor").web3;
|
||||
const { OpenOrdersPda } = require("@project-serum/serum");
|
||||
|
||||
// Dummy keypair.
|
||||
const KEYPAIR = new Account([
|
||||
54,
|
||||
213,
|
||||
91,
|
||||
255,
|
||||
163,
|
||||
120,
|
||||
88,
|
||||
183,
|
||||
223,
|
||||
23,
|
||||
220,
|
||||
204,
|
||||
82,
|
||||
117,
|
||||
212,
|
||||
214,
|
||||
118,
|
||||
184,
|
||||
2,
|
||||
29,
|
||||
89,
|
||||
149,
|
||||
22,
|
||||
233,
|
||||
108,
|
||||
177,
|
||||
60,
|
||||
249,
|
||||
218,
|
||||
166,
|
||||
30,
|
||||
221,
|
||||
59,
|
||||
168,
|
||||
233,
|
||||
123,
|
||||
204,
|
||||
37,
|
||||
123,
|
||||
124,
|
||||
86,
|
||||
176,
|
||||
214,
|
||||
12,
|
||||
63,
|
||||
195,
|
||||
231,
|
||||
15,
|
||||
1,
|
||||
143,
|
||||
7,
|
||||
7,
|
||||
232,
|
||||
38,
|
||||
69,
|
||||
214,
|
||||
45,
|
||||
58,
|
||||
115,
|
||||
55,
|
||||
129,
|
||||
25,
|
||||
228,
|
||||
30,
|
||||
]);
|
||||
|
||||
async function initOpenOrders(provider, marketProxy, marketMakerAccounts) {
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
marketProxy.instruction.initOpenOrders(
|
||||
marketMakerAccounts.account.publicKey,
|
||||
marketProxy.market.address,
|
||||
marketProxy.market.address, // Dummy. Replaced by middleware.
|
||||
marketProxy.market.address // Dummy. Replaced by middleware.
|
||||
)
|
||||
);
|
||||
let signers = [marketMakerAccounts.account];
|
||||
await provider.send(tx, signers);
|
||||
}
|
||||
|
||||
async function postOrders(provider, marketProxy, marketMakerAccounts) {
|
||||
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],
|
||||
];
|
||||
const openOrdersAddressKey = await OpenOrdersPda.openOrdersAddress(
|
||||
marketProxy.market.address,
|
||||
marketMakerAccounts.account.publicKey,
|
||||
marketProxy.dexProgramId,
|
||||
marketProxy.proxyProgramId
|
||||
);
|
||||
// Use an explicit signer because the provider wallet, which pays for
|
||||
// the tx, is different from the market maker wallet.
|
||||
let signers = [marketMakerAccounts.account];
|
||||
for (let k = 0; k < asks.length; k += 1) {
|
||||
let ask = asks[k];
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
await marketProxy.instruction.newOrderV3({
|
||||
owner: marketMakerAccounts.account.publicKey,
|
||||
payer: marketMakerAccounts.baseToken,
|
||||
side: "sell",
|
||||
price: ask[0],
|
||||
size: ask[1],
|
||||
orderType: "postOnly",
|
||||
clientId: undefined,
|
||||
openOrdersAddressKey,
|
||||
feeDiscountPubkey: null,
|
||||
selfTradeBehavior: "abortTransaction",
|
||||
})
|
||||
);
|
||||
await provider.send(tx, signers);
|
||||
}
|
||||
|
||||
for (let k = 0; k < bids.length; k += 1) {
|
||||
let bid = bids[k];
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
await marketProxy.instruction.newOrderV3({
|
||||
owner: marketMakerAccounts.account.publicKey,
|
||||
payer: marketMakerAccounts.quoteToken,
|
||||
side: "buy",
|
||||
price: bid[0],
|
||||
size: bid[1],
|
||||
orderType: "postOnly",
|
||||
clientId: undefined,
|
||||
openOrdersAddressKey,
|
||||
feeDiscountPubkey: null,
|
||||
selfTradeBehavior: "abortTransaction",
|
||||
})
|
||||
);
|
||||
await provider.send(tx, signers);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
postOrders,
|
||||
initOpenOrders,
|
||||
KEYPAIR,
|
||||
};
|
|
@ -0,0 +1,90 @@
|
|||
const anchor = require("@project-serum/anchor");
|
||||
const {
|
||||
PublicKey,
|
||||
SYSVAR_RENT_PUBKEY,
|
||||
SYSVAR_CLOCK_PUBKEY,
|
||||
} = require("@solana/web3.js");
|
||||
const {
|
||||
OpenOrders,
|
||||
OpenOrdersPda,
|
||||
Logger,
|
||||
ReferralFees,
|
||||
MarketProxyBuilder,
|
||||
} = require("@project-serum/serum");
|
||||
|
||||
// Returns a client for the market proxy.
|
||||
//
|
||||
// If changing the program, one will likely need to change the builder/middleware
|
||||
// here as well.
|
||||
async function load(connection, proxyProgramId, dexProgramId, market) {
|
||||
return new MarketProxyBuilder()
|
||||
.middleware(
|
||||
new OpenOrdersPda({
|
||||
proxyProgramId,
|
||||
dexProgramId,
|
||||
})
|
||||
)
|
||||
.middleware(new ReferralFees())
|
||||
.middleware(new Identity())
|
||||
.middleware(new Logger())
|
||||
.load({
|
||||
connection,
|
||||
market,
|
||||
dexProgramId,
|
||||
proxyProgramId,
|
||||
options: { commitment: "recent" },
|
||||
});
|
||||
}
|
||||
|
||||
// Dummy identity middleware used for testing.
|
||||
class Identity {
|
||||
initOpenOrders(ix) {
|
||||
this.proxy(ix);
|
||||
}
|
||||
newOrderV3(ix) {
|
||||
this.proxy(ix);
|
||||
}
|
||||
cancelOrderV2(ix) {
|
||||
this.proxy(ix);
|
||||
}
|
||||
cancelOrderByClientIdV2(ix) {
|
||||
this.proxy(ix);
|
||||
}
|
||||
settleFunds(ix) {
|
||||
this.proxy(ix);
|
||||
}
|
||||
closeOpenOrders(ix) {
|
||||
this.proxy(ix);
|
||||
}
|
||||
prune(ix) {
|
||||
this.proxyRevoked(ix);
|
||||
}
|
||||
proxy(ix) {
|
||||
ix.keys = [
|
||||
{ pubkey: SYSVAR_RENT_PUBKEY, isWritable: false, isSigner: false },
|
||||
...ix.keys,
|
||||
];
|
||||
}
|
||||
proxyRevoked(ix) {
|
||||
ix.keys = [
|
||||
{ pubkey: SYSVAR_CLOCK_PUBKEY, isWritable: false, isSigner: false },
|
||||
...ix.keys,
|
||||
];
|
||||
}
|
||||
static async pruneAuthority(market, dexProgramId, proxyProgramId) {
|
||||
const [addr] = await PublicKey.findProgramAddress(
|
||||
[
|
||||
anchor.utils.bytes.utf8.encode("prune"),
|
||||
dexProgramId.toBuffer(),
|
||||
market.toBuffer(),
|
||||
],
|
||||
proxyProgramId
|
||||
);
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
load,
|
||||
Identity,
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
IMG_ORG ?= projectserum
|
||||
IMG_VER ?= latest
|
||||
|
||||
WORKDIR=$(PWD)
|
||||
|
||||
.PHONY: development development-push development-shell
|
||||
|
||||
default:
|
||||
|
||||
development: development/Dockerfile
|
||||
@docker build $@ -t $(IMG_ORG)/$@:$(IMG_VER)
|
||||
|
||||
development-push:
|
||||
@docker push $(IMG_ORG)/development:$(IMG_VER)
|
||||
|
||||
development-shell:
|
||||
@docker run -ti --rm --net=host \
|
||||
-v $(WORKDIR)/..:/workdir \
|
||||
$(IMG_ORG)/development:$(IMG_VER) bash
|
|
@ -1,24 +0,0 @@
|
|||
# Docker
|
||||
|
||||
A development docker image is used in CI to build and test the project.
|
||||
|
||||
## Development
|
||||
|
||||
To build the development image run
|
||||
|
||||
```
|
||||
make development
|
||||
```
|
||||
|
||||
To push to dockerhub, assuming you have push access, run
|
||||
|
||||
```
|
||||
make development-push
|
||||
```
|
||||
|
||||
To run the development image locally (if you don't want to install all the
|
||||
dependencies yourself), run
|
||||
|
||||
```
|
||||
make development-shell
|
||||
```
|
|
@ -1,32 +0,0 @@
|
|||
FROM ubuntu:18.04
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ARG SOLANA_CHANNEL=v1.2.17
|
||||
ARG SOLANA_CLI=v1.6.18
|
||||
|
||||
ENV HOME="/root"
|
||||
ENV PATH="${HOME}/.cargo/bin:${PATH}"
|
||||
ENV PATH="${HOME}/.local/share/solana/install/active_release/bin:${PATH}"
|
||||
|
||||
# Install base utilities.
|
||||
RUN mkdir -p /workdir && mkdir -p /tmp && \
|
||||
apt-get update -qq && apt-get upgrade -qq && apt-get install -qq \
|
||||
build-essential git curl wget jq pkg-config python3-pip \
|
||||
libssl-dev libudev-dev
|
||||
|
||||
# Install rust.
|
||||
RUN curl "https://sh.rustup.rs" -sfo rustup.sh && \
|
||||
sh rustup.sh -y && \
|
||||
rustup component add rustfmt clippy
|
||||
|
||||
# Install Solana tools.
|
||||
RUN curl -sSf https://raw.githubusercontent.com/solana-labs/solana/${SOLANA_CLI}/install/solana-install-init.sh | sh -s - ${SOLANA_CLI} && \
|
||||
# BPF sdk.
|
||||
curl -L --retry 5 --retry-delay 2 -o bpf-sdk.tar.bz2 http://solana-sdk.s3.amazonaws.com/${SOLANA_CHANNEL}/bpf-sdk.tar.bz2 && \
|
||||
rm -rf bpf-sdk && \
|
||||
mkdir -p bpf-sdk && \
|
||||
tar jxf bpf-sdk.tar.bz2 && \
|
||||
rm -f bpf-sdk.tar.bz2
|
||||
|
||||
WORKDIR /workdir
|
|
@ -48,7 +48,7 @@ dex_whole_shebang() {
|
|||
#
|
||||
# Deploy the program.
|
||||
#
|
||||
local dex_program_id="$(solana deploy --output json-compact --url ${CLUSTER_URL} dex/target/bpfel-unknown-unknown/release/serum_dex.so | jq .programId -r)"
|
||||
local dex_program_id="$(solana deploy --output json-compact --url ${CLUSTER_URL} dex/target/deploy/serum_dex.so | jq .programId -r)"
|
||||
#
|
||||
# Run the whole-shebang.
|
||||
#
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
main() {
|
||||
docker pull projectserum/development:latest
|
||||
#
|
||||
# Bind the relevant host directories to the docker image so that the
|
||||
# files are synced.
|
||||
#
|
||||
docker volume create --driver local \
|
||||
--opt type=none \
|
||||
--opt device=$TRAVIS_BUILD_DIR \
|
||||
--opt o=bind \
|
||||
workdir
|
||||
#
|
||||
# Start the container.
|
||||
#
|
||||
docker run -it -d --net host --name dev \
|
||||
-v workdir:/workdir \
|
||||
projectserum/development:latest bash
|
||||
}
|
||||
|
||||
main
|
|
@ -1,9 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
main() {
|
||||
docker stop dev
|
||||
}
|
||||
|
||||
main
|
Loading…
Reference in New Issue