examples, spl: Permissioned markets via proxy middleware (#519)
This commit is contained in:
parent
bb89cc3f72
commit
615764b9c8
|
@ -2811,7 +2811,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "serum_dex"
|
||||
version = "0.3.1"
|
||||
source = "git+https://github.com/project-serum/serum-dex?tag=v0.3.1#7d1d41538417aa8721aabea9503bf9d99eab7cc4"
|
||||
source = "git+https://github.com/project-serum/serum-dex?branch=armani/auth#2037a646f82e689f8e7a00c8a34b30e20253ba11"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"bincode",
|
||||
|
|
|
@ -2,12 +2,6 @@
|
|||
cluster = "localnet"
|
||||
wallet = "~/.config/solana/id.json"
|
||||
|
||||
[scripts]
|
||||
test = "anchor run build && anchor test"
|
||||
build = "anchor run build-deps && anchor build"
|
||||
build-deps = "anchor run build-dex"
|
||||
build-dex = "pushd deps/serum-dex/dex/ && cargo build-bpf && popd"
|
||||
|
||||
[[test.genesis]]
|
||||
address = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
|
||||
program = "./deps/serum-dex/dex/target/deploy/serum_dex.so"
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
.PHONY: test build build-deps build-dex
|
||||
|
||||
test: build
|
||||
anchor test
|
||||
|
||||
build: build-dex
|
||||
anchor build
|
||||
|
||||
build-dex:
|
||||
cd deps/serum-dex/dex/ && cargo build-bpf && cd ../../../
|
||||
|
||||
localnet:
|
||||
./scripts/localnet.sh
|
|
@ -1 +1 @@
|
|||
Subproject commit 1f6d5867019e242a470deed79cddca0d1f15e0a3
|
||||
Subproject commit 2037a646f82e689f8e7a00c8a34b30e20253ba11
|
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "permissioned-markets-middleware"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "permissioned_markets_middleware"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
no-idl = []
|
||||
cpi = ["no-entrypoint"]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = { path = "../../../../lang" }
|
||||
anchor-spl = { path = "../../../../spl" }
|
||||
solana-program = "1.7.4"
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,155 @@
|
|||
// 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 or
|
||||
/// close 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_middleware {
|
||||
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 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.
|
||||
/// ..
|
||||
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(())
|
||||
}
|
||||
|
||||
// Error.
|
||||
|
||||
#[error]
|
||||
pub enum ErrorCode {
|
||||
#[msg("Invalid auth token provided")]
|
||||
InvalidAuth,
|
||||
}
|
||||
|
||||
// Constants.
|
||||
|
||||
pub mod referral {
|
||||
// This is a dummy address for testing. Do not use in production.
|
||||
solana_program::declare_id!("3oSfkjQZKCneYvsCTZc9HViGAPqR8pYr4h9YeGB5ZxHf");
|
||||
}
|
|
@ -18,4 +18,5 @@ default = []
|
|||
anchor-lang = { path = "../../../../lang" }
|
||||
anchor-spl = { path = "../../../../spl" }
|
||||
serum_dex = { path = "../../deps/serum-dex/dex", features = ["no-entrypoint"] }
|
||||
solana-program = "1.7.4"
|
||||
solana-program = "1.7.4"
|
||||
spl-token = { version = "3.1.1", features = ["no-entrypoint"] }
|
||||
|
|
|
@ -3,79 +3,112 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::dex;
|
||||
use serum_dex::instruction::MarketInstruction;
|
||||
use serum_dex::matching::Side;
|
||||
use serum_dex::state::OpenOrders;
|
||||
use solana_program::instruction::Instruction;
|
||||
use solana_program::program;
|
||||
use solana_program::system_program;
|
||||
use solana_program::sysvar::rent;
|
||||
use std::mem::size_of;
|
||||
|
||||
/// This demonstrates how to create "permissioned markets" on Serum. A
|
||||
/// permissioned market is a regular Serum market with an additional
|
||||
/// open orders authority, which must sign every transaction to create or
|
||||
/// close an open orders account.
|
||||
/// A low level example of permissioned markets.
|
||||
///
|
||||
/// 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.
|
||||
/// It's recommended to instead study `programs/permissioned-markets-middleware`
|
||||
/// in this workspace, which achieves the same functionality in a simpler, more
|
||||
/// extendable fashion via a middleware abstraction. This program achieves
|
||||
/// mostly the same proxy + middleware functionality, but in a much uglier way.
|
||||
///
|
||||
/// For example, this example forces all trades that execute on this market
|
||||
/// to set the referral to a hardcoded address, i.e., `fee_owner::ID`.
|
||||
/// This example is provided as a (very) rough guide for how to might implement
|
||||
/// a permissioned market in a raw program, which may be useful in the
|
||||
/// unexpected case that the middleware abstraction does not fit ones use case.
|
||||
///
|
||||
/// Note that a fallback function is used here as the entrypoint instead of
|
||||
/// higher level Anchor instruction handers. This is done to keep the example
|
||||
/// consistent with `programs/permissioned-markets-middleware`. A program
|
||||
/// with explicit instruction handlers would work, though then one would lose
|
||||
/// the middleware abstraction, which may or may not be acceptibl depending on
|
||||
/// your use case.
|
||||
#[program]
|
||||
pub mod permissioned_markets {
|
||||
use super::*;
|
||||
|
||||
/// Creates an open orders account controlled by this program on behalf of
|
||||
/// the user.
|
||||
///
|
||||
/// Note that although the owner of the open orders account is the dex
|
||||
/// program, This instruction must be executed within this program, rather
|
||||
/// than a relay, because it initializes a PDA.
|
||||
pub fn init_account(ctx: Context<InitAccount>, bump: u8, bump_init: u8) -> Result<()> {
|
||||
let cpi_ctx = CpiContext::from(&*ctx.accounts);
|
||||
let seeds = open_orders_authority! {
|
||||
program = ctx.program_id,
|
||||
market = ctx.accounts.market.key,
|
||||
authority = ctx.accounts.authority.key,
|
||||
bump = bump
|
||||
};
|
||||
let seeds_init = open_orders_init_authority! {
|
||||
program = ctx.program_id,
|
||||
market = ctx.accounts.market.key,
|
||||
bump = bump_init
|
||||
};
|
||||
dex::init_open_orders(cpi_ctx.with_signer(&[seeds, seeds_init]))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fallback function to relay calls to the serum DEX.
|
||||
///
|
||||
/// For instructions requiring an open orders authority, checks for
|
||||
/// a user signature and then swaps the account info for one controlled
|
||||
/// by the program.
|
||||
///
|
||||
/// Note: the "authority" of each open orders account is the account
|
||||
/// itself, since it's a PDA.
|
||||
#[access_control(is_serum(accounts))]
|
||||
pub fn dex_instruction(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
data: &[u8],
|
||||
mut data: &[u8],
|
||||
) -> ProgramResult {
|
||||
require!(accounts.len() >= 1, NotEnoughAccounts);
|
||||
require!(!accounts.is_empty(), NotEnoughAccounts);
|
||||
|
||||
let dex_acc_info = &accounts[0];
|
||||
let dex_accounts = &accounts[1..];
|
||||
let mut acc_infos = dex_accounts.to_vec();
|
||||
// Strip instruction data.
|
||||
let bumps = {
|
||||
// Strip the discriminator off the data, which is provided by the client
|
||||
// for prepending extra instruction data.
|
||||
let disc = data[0];
|
||||
data = &data[1..];
|
||||
|
||||
// For the init open orders instruction, bump seeds are provided.
|
||||
if disc == 0 {
|
||||
let bump = data[0];
|
||||
let bump_init = data[1];
|
||||
data = &data[2..]; // Strip bumps off.
|
||||
Some((bump, bump_init))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Strip accounts.
|
||||
let (dex, mut acc_infos) = {
|
||||
// First account is the dex executable--used for CPI.
|
||||
let dex = &accounts[0];
|
||||
|
||||
// Second account is the auth token.
|
||||
let auth_token = &accounts[1];
|
||||
if auth_token.key != &rent::ID {
|
||||
// Rent sysvar as dummy example.
|
||||
return Err(ErrorCode::InvalidAuthToken.into());
|
||||
}
|
||||
|
||||
// Strip.
|
||||
let acc_infos = (&accounts[2..]).to_vec();
|
||||
|
||||
(dex, acc_infos)
|
||||
};
|
||||
|
||||
let mut pre_instruction: Option<CpiInstruction> = None;
|
||||
let mut post_instruction: Option<CpiInstruction> = None;
|
||||
|
||||
// Decode instruction.
|
||||
let ix = MarketInstruction::unpack(data).ok_or_else(|| ErrorCode::CannotUnpack)?;
|
||||
let ix = MarketInstruction::unpack(data).ok_or(ErrorCode::CannotUnpack)?;
|
||||
|
||||
// Swap the user's account, which is in the open orders authority
|
||||
// position, for the program's PDA (the real authority).
|
||||
let (market, user) = match ix {
|
||||
MarketInstruction::NewOrderV3(_) => {
|
||||
require!(dex_accounts.len() >= 12, NotEnoughAccounts);
|
||||
MarketInstruction::InitOpenOrders => {
|
||||
let (market, user) = {
|
||||
let market = &acc_infos[4];
|
||||
let user = &acc_infos[3];
|
||||
|
||||
let (bump, bump_init) = bumps.as_ref().unwrap();
|
||||
|
||||
// Initialize PDA.
|
||||
let mut accounts = &acc_infos[..];
|
||||
InitAccount::try_accounts(program_id, &mut accounts, &[*bump, *bump_init])?;
|
||||
|
||||
(*market.key, *user.key)
|
||||
};
|
||||
// Chop off the first two accounts used initializing the PDA.
|
||||
acc_infos = (&acc_infos[2..]).to_vec();
|
||||
|
||||
// Set signers.
|
||||
acc_infos[1] = prepare_pda(&acc_infos[0]);
|
||||
acc_infos[4].is_signer = true;
|
||||
|
||||
(market, user)
|
||||
}
|
||||
MarketInstruction::NewOrderV3(ix) => {
|
||||
require!(acc_infos.len() >= 12, NotEnoughAccounts);
|
||||
|
||||
let (market, user) = {
|
||||
let market = &acc_infos[0];
|
||||
|
@ -88,12 +121,60 @@ pub mod permissioned_markets {
|
|||
(*market.key, *user.key)
|
||||
};
|
||||
|
||||
// Pre-instruction to approve delegate.
|
||||
{
|
||||
let market = &acc_infos[0];
|
||||
let user = &acc_infos[7];
|
||||
let open_orders = &acc_infos[1];
|
||||
let token_account_payer = &acc_infos[6];
|
||||
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(),
|
||||
];
|
||||
pre_instruction = Some((ix, accounts, Vec::new()));
|
||||
};
|
||||
|
||||
// Post-instruction to revoke delegate.
|
||||
{
|
||||
let user = &acc_infos[7];
|
||||
let token_account_payer = &acc_infos[6];
|
||||
let ix = spl_token::instruction::revoke(
|
||||
&spl_token::ID,
|
||||
token_account_payer.key,
|
||||
user.key,
|
||||
&[],
|
||||
)?;
|
||||
let accounts = vec![token_account_payer.clone(), user.clone()];
|
||||
post_instruction = Some((ix, accounts, Vec::new()));
|
||||
}
|
||||
|
||||
acc_infos[7] = prepare_pda(&acc_infos[1]);
|
||||
|
||||
(market, user)
|
||||
}
|
||||
MarketInstruction::CancelOrderV2(_) => {
|
||||
require!(dex_accounts.len() >= 6, NotEnoughAccounts);
|
||||
require!(acc_infos.len() >= 6, NotEnoughAccounts);
|
||||
|
||||
let (market, user) = {
|
||||
let market = &acc_infos[0];
|
||||
|
@ -111,7 +192,7 @@ pub mod permissioned_markets {
|
|||
(market, user)
|
||||
}
|
||||
MarketInstruction::CancelOrderByClientIdV2(_) => {
|
||||
require!(dex_accounts.len() >= 6, NotEnoughAccounts);
|
||||
require!(acc_infos.len() >= 6, NotEnoughAccounts);
|
||||
|
||||
let (market, user) = {
|
||||
let market = &acc_infos[0];
|
||||
|
@ -129,12 +210,12 @@ pub mod permissioned_markets {
|
|||
(market, user)
|
||||
}
|
||||
MarketInstruction::SettleFunds => {
|
||||
require!(dex_accounts.len() >= 10, NotEnoughAccounts);
|
||||
require!(acc_infos.len() >= 10, NotEnoughAccounts);
|
||||
|
||||
let (market, user) = {
|
||||
let market = &acc_infos[0];
|
||||
let user = &acc_infos[2];
|
||||
let referral = &dex_accounts[9];
|
||||
let referral = &acc_infos[9];
|
||||
|
||||
if !DISABLE_REFERRAL && referral.key != &referral::ID {
|
||||
return Err(ErrorCode::InvalidReferral.into());
|
||||
|
@ -151,7 +232,7 @@ pub mod permissioned_markets {
|
|||
(market, user)
|
||||
}
|
||||
MarketInstruction::CloseOpenOrders => {
|
||||
require!(dex_accounts.len() >= 4, NotEnoughAccounts);
|
||||
require!(acc_infos.len() >= 4, NotEnoughAccounts);
|
||||
|
||||
let (market, user) = {
|
||||
let market = &acc_infos[3];
|
||||
|
@ -171,6 +252,19 @@ pub mod permissioned_markets {
|
|||
_ => return Err(ErrorCode::InvalidInstruction.into()),
|
||||
};
|
||||
|
||||
// Execute pre instruction.
|
||||
if let Some((ix, accounts, seeds)) = pre_instruction {
|
||||
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, &accounts, &signers)?;
|
||||
}
|
||||
|
||||
// CPI to the dex.
|
||||
let dex_accounts = acc_infos
|
||||
.iter()
|
||||
|
@ -180,7 +274,6 @@ pub mod permissioned_markets {
|
|||
is_writable: acc.is_writable,
|
||||
})
|
||||
.collect();
|
||||
acc_infos.push(dex_acc_info.clone());
|
||||
let ix = Instruction {
|
||||
data: data.to_vec(),
|
||||
accounts: dex_accounts,
|
||||
|
@ -188,10 +281,31 @@ pub mod permissioned_markets {
|
|||
};
|
||||
let seeds = open_orders_authority! {
|
||||
program = program_id,
|
||||
dex_program = dex.key,
|
||||
market = market,
|
||||
authority = user
|
||||
};
|
||||
program::invoke_signed(&ix, &acc_infos, &[seeds])
|
||||
let seeds_init = open_orders_init_authority! {
|
||||
program = program_id,
|
||||
dex_program = dex.key,
|
||||
market = market
|
||||
};
|
||||
program::invoke_signed(&ix, &acc_infos, &[seeds, seeds_init])?;
|
||||
|
||||
// Execute post instruction.
|
||||
if let Some((ix, accounts, seeds)) = post_instruction {
|
||||
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, &accounts, &signers)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -200,11 +314,13 @@ pub mod permissioned_markets {
|
|||
#[derive(Accounts)]
|
||||
#[instruction(bump: u8, bump_init: u8)]
|
||||
pub struct InitAccount<'info> {
|
||||
#[account(seeds = [b"open-orders-init", market.key.as_ref(), &[bump_init]])]
|
||||
pub open_orders_init_authority: AccountInfo<'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", market.key.as_ref(), authority.key.as_ref()],
|
||||
seeds = [b"open-orders", dex_program.key.as_ref(), market.key.as_ref(), authority.key.as_ref()],
|
||||
bump = bump,
|
||||
payer = authority,
|
||||
owner = dex::ID,
|
||||
|
@ -215,34 +331,16 @@ pub struct InitAccount<'info> {
|
|||
pub authority: AccountInfo<'info>,
|
||||
pub market: AccountInfo<'info>,
|
||||
pub rent: Sysvar<'info, Rent>,
|
||||
#[account(address = system_program::ID)]
|
||||
pub system_program: AccountInfo<'info>,
|
||||
#[account(address = dex::ID)]
|
||||
pub dex_program: AccountInfo<'info>,
|
||||
}
|
||||
|
||||
// CpiContext transformations.
|
||||
|
||||
impl<'info> From<&InitAccount<'info>>
|
||||
for CpiContext<'_, '_, '_, 'info, dex::InitOpenOrders<'info>>
|
||||
{
|
||||
fn from(accs: &InitAccount<'info>) -> Self {
|
||||
// TODO: add the open orders init authority account here once the
|
||||
// dex is upgraded.
|
||||
let accounts = dex::InitOpenOrders {
|
||||
open_orders: accs.open_orders.clone(),
|
||||
authority: accs.open_orders.clone(),
|
||||
market: accs.market.clone(),
|
||||
rent: accs.rent.to_account_info(),
|
||||
};
|
||||
let program = accs.dex_program.clone();
|
||||
CpiContext::new(program, accounts)
|
||||
}
|
||||
#[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>,
|
||||
}
|
||||
|
||||
// Access control modifiers.
|
||||
|
||||
fn is_serum<'info>(accounts: &[AccountInfo<'info>]) -> Result<()> {
|
||||
fn is_serum(accounts: &[AccountInfo]) -> Result<()> {
|
||||
let dex_acc_info = &accounts[0];
|
||||
if dex_acc_info.key != &dex::ID {
|
||||
return Err(ErrorCode::InvalidDexPid.into());
|
||||
|
@ -266,29 +364,53 @@ pub enum ErrorCode {
|
|||
UnauthorizedUser,
|
||||
#[msg("Not enough accounts were provided")]
|
||||
NotEnoughAccounts,
|
||||
#[msg("Invalid auth token provided")]
|
||||
InvalidAuthToken,
|
||||
}
|
||||
|
||||
// Utils.
|
||||
|
||||
fn prepare_pda<'info>(acc_info: &AccountInfo<'info>) -> AccountInfo<'info> {
|
||||
let mut acc_info = acc_info.clone();
|
||||
acc_info.is_signer = true;
|
||||
acc_info
|
||||
}
|
||||
|
||||
// Macros.
|
||||
|
||||
/// Returns the seeds used for creating the open orders account PDA.
|
||||
/// Returns the seeds used for a user's open orders account PDA.
|
||||
#[macro_export]
|
||||
macro_rules! open_orders_authority {
|
||||
(program = $program:expr, market = $market:expr, authority = $authority:expr, bump = $bump:expr) => {
|
||||
(
|
||||
program = $program:expr,
|
||||
dex_program = $dex_program:expr,
|
||||
market = $market:expr,
|
||||
authority = $authority:expr,
|
||||
bump = $bump:expr
|
||||
) => {
|
||||
&[
|
||||
b"open-orders".as_ref(),
|
||||
$dex_program.as_ref(),
|
||||
$market.as_ref(),
|
||||
$authority.as_ref(),
|
||||
&[$bump],
|
||||
]
|
||||
};
|
||||
(program = $program:expr, market = $market:expr, authority = $authority:expr) => {
|
||||
(
|
||||
program = $program:expr,
|
||||
dex_program = $dex_program:expr,
|
||||
market = $market:expr,
|
||||
authority = $authority:expr
|
||||
) => {
|
||||
&[
|
||||
b"open-orders".as_ref(),
|
||||
$dex_program.as_ref(),
|
||||
$market.as_ref(),
|
||||
$authority.as_ref(),
|
||||
&[Pubkey::find_program_address(
|
||||
&[
|
||||
b"open-orders".as_ref(),
|
||||
$dex_program.as_ref(),
|
||||
$market.as_ref(),
|
||||
$authority.as_ref(),
|
||||
],
|
||||
|
@ -304,28 +426,36 @@ macro_rules! open_orders_authority {
|
|||
/// the DEX market.
|
||||
#[macro_export]
|
||||
macro_rules! open_orders_init_authority {
|
||||
(program = $program:expr, market = $market:expr) => {
|
||||
(
|
||||
program = $program:expr,
|
||||
dex_program = $dex_program:expr,
|
||||
market = $market:expr,
|
||||
bump = $bump:expr
|
||||
) => {
|
||||
&[
|
||||
b"open-orders-init".as_ref(),
|
||||
$dex_program.as_ref().as_ref(),
|
||||
$market.as_ref().as_ref(),
|
||||
&[$bump],
|
||||
]
|
||||
};
|
||||
|
||||
(program = $program:expr, dex_program = $dex_program:expr, market = $market:expr) => {
|
||||
&[
|
||||
b"open-orders-init".as_ref(),
|
||||
$dex_program.as_ref(),
|
||||
$market.as_ref(),
|
||||
&[Pubkey::find_program_address(
|
||||
&[b"open-orders-init".as_ref(), $market.as_ref()],
|
||||
&[
|
||||
b"open-orders-init".as_ref(),
|
||||
$dex_program.as_ref(),
|
||||
$market.as_ref(),
|
||||
],
|
||||
$program,
|
||||
)
|
||||
.1],
|
||||
]
|
||||
};
|
||||
(program = $program:expr, market = $market:expr, bump = $bump:expr) => {
|
||||
&[b"open-orders-init".as_ref(), $market.as_ref(), &[$bump]]
|
||||
};
|
||||
}
|
||||
|
||||
// Utils.
|
||||
|
||||
fn prepare_pda<'info>(acc_info: &AccountInfo<'info>) -> AccountInfo<'info> {
|
||||
let mut acc_info = acc_info.clone();
|
||||
acc_info.is_signer = true;
|
||||
acc_info
|
||||
}
|
||||
|
||||
// Constants.
|
||||
|
@ -343,3 +473,5 @@ const DISABLE_REFERRAL: bool = true;
|
|||
pub mod referral {
|
||||
solana_program::declare_id!("2k1bb16Hu7ocviT2KC3wcCgETtnC8tEUuvFBH4C5xStG");
|
||||
}
|
||||
|
||||
type CpiInstruction<'info> = (Instruction, Vec<AccountInfo<'info>>, Vec<Vec<Vec<u8>>>);
|
||||
|
|
|
@ -3,123 +3,159 @@ 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 { Transaction, TransactionInstruction } = anchor.web3;
|
||||
const { DexInstructions, OpenOrders, Market } = serum;
|
||||
const { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } = anchor.web3;
|
||||
const {
|
||||
Keypair,
|
||||
Transaction,
|
||||
TransactionInstruction,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
SYSVAR_RENT_PUBKEY,
|
||||
} = anchor.web3;
|
||||
const {
|
||||
DexInstructions,
|
||||
OpenOrders,
|
||||
OpenOrdersPda,
|
||||
Logger,
|
||||
ReferralFees,
|
||||
MarketProxyBuilder,
|
||||
} = serum;
|
||||
const { initMarket, sleep } = require("./utils");
|
||||
|
||||
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
|
||||
const REFERRAL = new PublicKey("2k1bb16Hu7ocviT2KC3wcCgETtnC8tEUuvFBH4C5xStG");
|
||||
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;
|
||||
const programs = [
|
||||
anchor.workspace.PermissionedMarkets,
|
||||
anchor.workspace.PermissionedMarketsMiddleware,
|
||||
];
|
||||
|
||||
// Token clients.
|
||||
let usdcClient;
|
||||
programs.forEach((program, index) => {
|
||||
// Token client.
|
||||
let usdcClient;
|
||||
|
||||
// Global DEX accounts and clients shared accross all tests.
|
||||
let marketClient, tokenAccount, usdcAccount;
|
||||
let openOrders, openOrdersBump, openOrdersInitAuthority, openOrdersBumpinit;
|
||||
let usdcPosted;
|
||||
let marketMakerOpenOrders;
|
||||
// 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 {
|
||||
marketMakerOpenOrders: mmOo,
|
||||
marketA,
|
||||
godA,
|
||||
godUsdc,
|
||||
usdc,
|
||||
} = await initMarket({ provider });
|
||||
marketClient = marketA;
|
||||
marketClient._programId = program.programId;
|
||||
usdcAccount = godUsdc;
|
||||
tokenAccount = godA;
|
||||
marketMakerOpenOrders = mmOo;
|
||||
it("BOILERPLATE: Initializes an orderbook", async () => {
|
||||
const getAuthority = async (market) => {
|
||||
return (
|
||||
await PublicKey.findProgramAddress(
|
||||
[
|
||||
anchor.utils.bytes.utf8.encode("open-orders-init"),
|
||||
DEX_PID.toBuffer(),
|
||||
market.toBuffer(),
|
||||
],
|
||||
program.programId
|
||||
)
|
||||
)[0];
|
||||
};
|
||||
const marketLoader = async (market) => {
|
||||
return new MarketProxyBuilder()
|
||||
.middleware(
|
||||
new OpenOrdersPda({
|
||||
proxyProgramId: program.programId,
|
||||
dexProgramId: DEX_PID,
|
||||
})
|
||||
)
|
||||
.middleware(new ReferralFees())
|
||||
.middleware(new Identity())
|
||||
.middleware(new Logger())
|
||||
.load({
|
||||
connection: provider.connection,
|
||||
market,
|
||||
dexProgramId: DEX_PID,
|
||||
proxyProgramId: program.programId,
|
||||
options: { commitment: "recent" },
|
||||
});
|
||||
};
|
||||
const { marketA, godA, godUsdc, usdc } = await initMarket({
|
||||
provider,
|
||||
getAuthority,
|
||||
proxyProgramId: program.programId,
|
||||
marketLoader,
|
||||
});
|
||||
marketProxy = marketA;
|
||||
usdcAccount = godUsdc;
|
||||
tokenAccount = godA;
|
||||
|
||||
usdcClient = new Token(
|
||||
provider.connection,
|
||||
usdc,
|
||||
TOKEN_PROGRAM_ID,
|
||||
provider.wallet.payer
|
||||
);
|
||||
});
|
||||
usdcClient = new Token(
|
||||
provider.connection,
|
||||
usdc,
|
||||
TOKEN_PROGRAM_ID,
|
||||
provider.wallet.payer
|
||||
);
|
||||
|
||||
it("BOILERPLATE: Calculates open orders addresses", async () => {
|
||||
const [_openOrders, bump] = await PublicKey.findProgramAddress(
|
||||
[
|
||||
anchor.utils.bytes.utf8.encode("open-orders"),
|
||||
marketClient.address.toBuffer(),
|
||||
program.provider.wallet.publicKey.toBuffer(),
|
||||
],
|
||||
program.programId
|
||||
);
|
||||
const [
|
||||
_openOrdersInitAuthority,
|
||||
bumpInit,
|
||||
] = await PublicKey.findProgramAddress(
|
||||
[
|
||||
anchor.utils.bytes.utf8.encode("open-orders-init"),
|
||||
marketClient.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 () => {
|
||||
await program.rpc.initAccount(openOrdersBump, openOrdersBumpInit, {
|
||||
accounts: {
|
||||
openOrdersInitAuthority,
|
||||
openOrders,
|
||||
authority: program.provider.wallet.publicKey,
|
||||
market: marketClient.address,
|
||||
rent: SYSVAR_RENT_PUBKEY,
|
||||
systemProgram: SystemProgram.programId,
|
||||
dexProgram: DEX_PID,
|
||||
},
|
||||
referral = await usdcClient.createAccount(REFERRAL_AUTHORITY);
|
||||
});
|
||||
|
||||
const account = await provider.connection.getAccountInfo(openOrders);
|
||||
assert.ok(account.owner.toString() === DEX_PID.toString());
|
||||
});
|
||||
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
|
||||
);
|
||||
|
||||
it("Posts a bid on the orderbook", async () => {
|
||||
const size = 1;
|
||||
const price = 1;
|
||||
// Save global variables re-used across tests.
|
||||
openOrders = _openOrders;
|
||||
openOrdersBump = bump;
|
||||
openOrdersInitAuthority = _openOrdersInitAuthority;
|
||||
openOrdersBumpInit = bumpInit;
|
||||
});
|
||||
|
||||
// The amount of USDC transferred into the dex for the trade.
|
||||
usdcPosted = new BN(marketClient._decoded.quoteLotSize.toNumber()).mul(
|
||||
marketClient
|
||||
.baseSizeNumberToLots(size)
|
||||
.mul(marketClient.priceNumberToLots(price))
|
||||
);
|
||||
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);
|
||||
|
||||
// Note: Prepend delegate approve to the tx since the owner of the token
|
||||
// account must match the owner of the open orders account. We
|
||||
// can probably hide this in the serum client.
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
Token.createApproveInstruction(
|
||||
TOKEN_PROGRAM_ID,
|
||||
usdcAccount,
|
||||
openOrders,
|
||||
program.provider.wallet.publicKey,
|
||||
[],
|
||||
usdcPosted.toNumber()
|
||||
)
|
||||
);
|
||||
tx.add(
|
||||
serumProxy(
|
||||
marketClient.makePlaceOrderInstruction(program.provider.connection, {
|
||||
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",
|
||||
|
@ -130,155 +166,135 @@ describe("permissioned-markets", () => {
|
|||
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(
|
||||
serumProxy(
|
||||
(
|
||||
await marketClient.makeCancelOrderByClientIdTransaction(
|
||||
program.provider.connection,
|
||||
program.provider.wallet.publicKey,
|
||||
openOrders,
|
||||
new BN(999)
|
||||
)
|
||||
).instructions[0]
|
||||
)
|
||||
);
|
||||
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));
|
||||
});
|
||||
|
||||
// Need to crank the cancel so that we can close later.
|
||||
it("Cranks the cancel transaction", async () => {
|
||||
// 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 marketClient.loadEventQueue(provider.connection);
|
||||
while (eq.length > 0) {
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
DexInstructions.consumeEvents({
|
||||
market: marketClient._decoded.ownAddress,
|
||||
eventQueue: marketClient._decoded.eventQueue,
|
||||
coinFee: marketClient._decoded.eventQueue,
|
||||
pcFee: marketClient._decoded.eventQueue,
|
||||
openOrdersAccounts: [eq[0].openOrders],
|
||||
limit: 1,
|
||||
programId: DEX_PID,
|
||||
})
|
||||
);
|
||||
await provider.send(tx);
|
||||
eq = await marketClient.loadEventQueue(provider.connection);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("Settles funds on the orderbook", async () => {
|
||||
// Given.
|
||||
const beforeTokenAccount = await usdcClient.getAccountInfo(usdcAccount);
|
||||
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(
|
||||
serumProxy(
|
||||
DexInstructions.settleFunds({
|
||||
market: marketClient._decoded.ownAddress,
|
||||
// When.
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
await marketProxy.instruction.cancelOrderByClientId(
|
||||
program.provider.wallet.publicKey,
|
||||
openOrders,
|
||||
owner: provider.wallet.publicKey,
|
||||
baseVault: marketClient._decoded.baseVault,
|
||||
quoteVault: marketClient._decoded.quoteVault,
|
||||
baseWallet: tokenAccount,
|
||||
quoteWallet: usdcAccount,
|
||||
vaultSigner: await PublicKey.createProgramAddress(
|
||||
[
|
||||
marketClient.address.toBuffer(),
|
||||
marketClient._decoded.vaultSignerNonce.toArrayLike(
|
||||
Buffer,
|
||||
"le",
|
||||
8
|
||||
),
|
||||
],
|
||||
DEX_PID
|
||||
),
|
||||
programId: program.programId,
|
||||
referrerQuoteWallet: usdcAccount,
|
||||
})
|
||||
)
|
||||
);
|
||||
await provider.send(tx);
|
||||
new BN(999)
|
||||
)
|
||||
);
|
||||
await provider.send(tx);
|
||||
|
||||
// Then.
|
||||
const afterTokenAccount = await usdcClient.getAccountInfo(usdcAccount);
|
||||
assert.ok(
|
||||
afterTokenAccount.amount.sub(beforeTokenAccount.amount).toNumber() ===
|
||||
usdcPosted.toNumber()
|
||||
);
|
||||
});
|
||||
// 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("Closes an open orders account", async () => {
|
||||
// Given.
|
||||
const beforeAccount = await program.provider.connection.getAccountInfo(
|
||||
program.provider.wallet.publicKey
|
||||
);
|
||||
// Need to crank the cancel so that we can close later.
|
||||
it("Cranks the cancel transaction", async () => {
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
// When.
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
serumProxy(
|
||||
DexInstructions.closeOpenOrders({
|
||||
market: marketClient._decoded.ownAddress,
|
||||
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,
|
||||
owner: program.provider.wallet.publicKey,
|
||||
solWallet: program.provider.wallet.publicKey,
|
||||
programId: program.programId,
|
||||
})
|
||||
)
|
||||
);
|
||||
await provider.send(tx);
|
||||
provider.wallet.publicKey,
|
||||
tokenAccount,
|
||||
usdcAccount,
|
||||
referral
|
||||
)
|
||||
);
|
||||
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);
|
||||
// Then.
|
||||
const afterTokenAccount = await usdcClient.getAccountInfo(usdcAccount);
|
||||
assert.ok(
|
||||
afterTokenAccount.amount.sub(beforeTokenAccount.amount).toNumber() ===
|
||||
usdcPosted.toNumber()
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Adds the serum dex account to the instruction so that proxies can
|
||||
// relay (CPI requires the executable account).
|
||||
//
|
||||
// TODO: we should add flag in the dex client that says if a proxy is being
|
||||
// used, and if so, do this automatically.
|
||||
function serumProxy(ix) {
|
||||
ix.keys = [
|
||||
{ pubkey: DEX_PID, isWritable: false, isSigner: false },
|
||||
...ix.keys,
|
||||
];
|
||||
return ix;
|
||||
// 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);
|
||||
}
|
||||
proxy(ix) {
|
||||
ix.keys = [
|
||||
{ pubkey: SYSVAR_RENT_PUBKEY, isWritable: false, isSigner: false },
|
||||
...ix.keys,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,22 +6,36 @@
|
|||
|
||||
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, OpenOrders } = require("@project-serum/serum");
|
||||
const DexInstructions = require("@project-serum/serum").DexInstructions;
|
||||
const web3 = require("@project-serum/anchor").web3;
|
||||
const Connection = web3.Connection;
|
||||
const serum = require('@project-serum/serum');
|
||||
const {
|
||||
DexInstructions,
|
||||
TokenInstructions,
|
||||
MarketProxy,
|
||||
OpenOrders,
|
||||
OpenOrdersPda,
|
||||
MARKET_STATE_LAYOUT_V3,
|
||||
} = serum;
|
||||
const anchor = require("@project-serum/anchor");
|
||||
const BN = anchor.BN;
|
||||
const web3 = anchor.web3;
|
||||
const {
|
||||
SYSVAR_RENT_PUBKEY,
|
||||
COnnection,
|
||||
Account,
|
||||
Transaction,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
} = web3;
|
||||
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");
|
||||
const MARKET_MAKER = new Account();
|
||||
|
||||
async function initMarket({ provider }) {
|
||||
async function initMarket({
|
||||
provider,
|
||||
getAuthority,
|
||||
proxyProgramId,
|
||||
marketLoader,
|
||||
}) {
|
||||
// Setup mints with initial tokens owned by the provider.
|
||||
const decimals = 6;
|
||||
const [MINT_A, GOD_A] = await serumCmn.createMintAndVault(
|
||||
|
@ -79,22 +93,14 @@ async function initMarket({ provider }) {
|
|||
bids,
|
||||
asks,
|
||||
provider,
|
||||
getAuthority,
|
||||
proxyProgramId,
|
||||
marketLoader,
|
||||
});
|
||||
|
||||
const marketMakerOpenOrders = (
|
||||
await OpenOrders.findForMarketAndOwner(
|
||||
provider.connection,
|
||||
MARKET_A_USDC.address,
|
||||
marketMaker.account.publicKey,
|
||||
DEX_PID
|
||||
)
|
||||
)[0].address;
|
||||
|
||||
return {
|
||||
marketA: MARKET_A_USDC,
|
||||
vaultSigner,
|
||||
marketMaker,
|
||||
marketMakerOpenOrders,
|
||||
mintA: MINT_A,
|
||||
usdc: USDC,
|
||||
godA: GOD_A,
|
||||
|
@ -171,6 +177,9 @@ async function setupMarket({
|
|||
quoteMint,
|
||||
bids,
|
||||
asks,
|
||||
getAuthority,
|
||||
proxyProgramId,
|
||||
marketLoader,
|
||||
}) {
|
||||
const [marketAPublicKey, vaultOwner] = await listMarket({
|
||||
connection: provider.connection,
|
||||
|
@ -181,55 +190,9 @@ async function setupMarket({
|
|||
quoteLotSize: 100,
|
||||
dexProgramId: DEX_PID,
|
||||
feeRateBps: 0,
|
||||
getAuthority,
|
||||
});
|
||||
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));
|
||||
}
|
||||
|
||||
const MARKET_A_USDC = await marketLoader(marketAPublicKey);
|
||||
return [MARKET_A_USDC, vaultOwner];
|
||||
}
|
||||
|
||||
|
@ -242,6 +205,7 @@ async function listMarket({
|
|||
quoteLotSize,
|
||||
dexProgramId,
|
||||
feeRateBps,
|
||||
getAuthority,
|
||||
}) {
|
||||
const market = new Account();
|
||||
const requestQueue = new Account();
|
||||
|
@ -291,9 +255,9 @@ async function listMarket({
|
|||
fromPubkey: wallet.publicKey,
|
||||
newAccountPubkey: market.publicKey,
|
||||
lamports: await connection.getMinimumBalanceForRentExemption(
|
||||
Market.getLayout(dexProgramId).span
|
||||
MARKET_STATE_LAYOUT_V3.span
|
||||
),
|
||||
space: Market.getLayout(dexProgramId).span,
|
||||
space: MARKET_STATE_LAYOUT_V3.span,
|
||||
programId: dexProgramId,
|
||||
}),
|
||||
SystemProgram.createAccount({
|
||||
|
@ -340,25 +304,19 @@ async function listMarket({
|
|||
vaultSignerNonce,
|
||||
quoteDustThreshold,
|
||||
programId: dexProgramId,
|
||||
authority: await getAuthority(market.publicKey),
|
||||
})
|
||||
);
|
||||
|
||||
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 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);
|
||||
|
||||
|
@ -386,17 +344,6 @@ async function signTransactions({
|
|||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -357,4 +357,9 @@ macro_rules! require {
|
|||
return Err(crate::ErrorCode::$error.into());
|
||||
}
|
||||
};
|
||||
($invariant:expr, $error:expr $(,)?) => {
|
||||
if !($invariant) {
|
||||
return Err($error.into());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,6 +12,6 @@ devnet = []
|
|||
[dependencies]
|
||||
anchor-lang = { path = "../lang", version = "0.11.1", features = ["derive"] }
|
||||
lazy_static = "1.4.0"
|
||||
serum_dex = { git = "https://github.com/project-serum/serum-dex", tag = "v0.3.1", version = "0.3.1", features = ["no-entrypoint"] }
|
||||
serum_dex = { git = "https://github.com/project-serum/serum-dex", branch = "armani/auth", version = "0.3.1", features = ["no-entrypoint"] }
|
||||
solana-program = "1.7.4"
|
||||
spl-token = { version = "3.1.1", features = ["no-entrypoint"] }
|
||||
|
|
|
@ -5,8 +5,6 @@ use serum_dex::instruction::SelfTradeBehavior;
|
|||
use serum_dex::matching::{OrderType, Side};
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
pub use serum_dex;
|
||||
|
||||
#[cfg(not(feature = "devnet"))]
|
||||
anchor_lang::solana_program::declare_id!("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
|
||||
|
||||
|
@ -115,6 +113,7 @@ pub fn init_open_orders<'info>(
|
|||
ctx.accounts.open_orders.key,
|
||||
ctx.accounts.authority.key,
|
||||
ctx.accounts.market.key,
|
||||
ctx.remaining_accounts.first().map(|acc| acc.key),
|
||||
)?;
|
||||
solana_program::program::invoke_signed(
|
||||
&ix,
|
||||
|
@ -205,6 +204,8 @@ pub struct SettleFunds<'info> {
|
|||
pub token_program: AccountInfo<'info>,
|
||||
}
|
||||
|
||||
/// To use an (optional) market authority, add it as the first account of the
|
||||
/// CpiContext's `remaining_accounts` Vec.
|
||||
#[derive(Accounts)]
|
||||
pub struct InitOpenOrders<'info> {
|
||||
pub open_orders: AccountInfo<'info>,
|
|
@ -0,0 +1,524 @@
|
|||
use crate::{dex, open_orders_authority, open_orders_init_authority, token};
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::solana_program::instruction::Instruction;
|
||||
use anchor_lang::solana_program::system_program;
|
||||
use anchor_lang::Accounts;
|
||||
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(())
|
||||
}
|
||||
|
||||
/// 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(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 cancel order by client id v2");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close_open_orders(&self, _ctx: &mut Context) -> ProgramResult {
|
||||
msg!("proxying close open orders");
|
||||
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]
|
||||
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,8 @@
|
|||
mod cpi;
|
||||
mod middleware;
|
||||
mod proxy;
|
||||
|
||||
pub use cpi::*;
|
||||
pub use middleware::*;
|
||||
pub use proxy::*;
|
||||
pub use serum_dex;
|
|
@ -0,0 +1,171 @@
|
|||
use crate::dex;
|
||||
use crate::dex::middleware::{Context, ErrorCode, MarketMiddleware};
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::solana_program::program;
|
||||
use anchor_lang::solana_program::pubkey::Pubkey;
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -292,4 +292,11 @@ pub mod accessor {
|
|||
mint_bytes.copy_from_slice(&bytes[..32]);
|
||||
Ok(Pubkey::new_from_array(mint_bytes))
|
||||
}
|
||||
|
||||
pub fn authority(account: &AccountInfo) -> Result<Pubkey, ProgramError> {
|
||||
let bytes = account.try_borrow_data()?;
|
||||
let mut owner_bytes = [0u8; 32];
|
||||
owner_bytes.copy_from_slice(&bytes[32..64]);
|
||||
Ok(Pubkey::new_from_array(owner_bytes))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue