examples: Permissioned markets (#483)
This commit is contained in:
parent
46cccd8946
commit
5018e98a9c
|
@ -10,3 +10,6 @@
|
||||||
[submodule "examples/cfo/deps/stake"]
|
[submodule "examples/cfo/deps/stake"]
|
||||||
path = examples/cfo/deps/stake
|
path = examples/cfo/deps/stake
|
||||||
url = https://github.com/project-serum/stake.git
|
url = https://github.com/project-serum/stake.git
|
||||||
|
[submodule "examples/permissioned-markets/deps/serum-dex"]
|
||||||
|
path = examples/permissioned-markets/deps/serum-dex
|
||||||
|
url = https://github.com/project-serum/serum-dex
|
||||||
|
|
|
@ -11,8 +11,15 @@ incremented for features.
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* lang: Adds `require` macro for specifying assertions that return error codes on failure ([#483](https://github.com/project-serum/anchor/pull/483)).
|
||||||
|
* lang: Allow one to specify arbitrary programs as the owner when creating PDA ([#483](https://github.com/project-serum/anchor/pull/483)).
|
||||||
|
* lang: A new `bump` keyword is added to the accounts constraints, which is used to add an optional bump seed to the end of a `seeds` array. When used in conjunction with *both* `init` and `seeds`, then the program executes `find_program_address` to assert that the given bump is the canonical bump ([#483](https://github.com/project-serum/anchor/pull/483)).
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
* lang: Preserve all instruction data for fallback functions ([#483](https://github.com/project-serum/anchor/pull/483)).
|
||||||
* ts: Event listener not firing when creating associated accounts ([#356](https://github.com/project-serum/anchor/issues/356)).
|
* ts: Event listener not firing when creating associated accounts ([#356](https://github.com/project-serum/anchor/issues/356)).
|
||||||
|
|
||||||
## [0.11.0] - 2021-07-03
|
## [0.11.0] - 2021-07-03
|
||||||
|
|
|
@ -17,4 +17,5 @@ members = [
|
||||||
exclude = [
|
exclude = [
|
||||||
"examples/swap/deps/serum-dex",
|
"examples/swap/deps/serum-dex",
|
||||||
"examples/cfo/deps/serum-dex",
|
"examples/cfo/deps/serum-dex",
|
||||||
|
"examples/permissioned-markets/deps/serum-dex",
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
[provider]
|
||||||
|
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,8 @@
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"programs/*"
|
||||||
|
]
|
||||||
|
|
||||||
|
exclude = [
|
||||||
|
"deps/serum-dex",
|
||||||
|
]
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 1f6d5867019e242a470deed79cddca0d1f15e0a3
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
// Migrations are an early feature. Currently, they're nothing more than this
|
||||||
|
// single deploy script that's invoked from the CLI, injecting a provider
|
||||||
|
// configured from the workspace's Anchor.toml.
|
||||||
|
|
||||||
|
const anchor = require("@project-serum/anchor");
|
||||||
|
|
||||||
|
module.exports = async function (provider) {
|
||||||
|
// Configure client to use the provider.
|
||||||
|
anchor.setProvider(provider);
|
||||||
|
|
||||||
|
// Add your deploy script here.
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
[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 = { path = "../../../../lang" }
|
||||||
|
anchor-spl = { path = "../../../../spl" }
|
||||||
|
serum_dex = { path = "../../deps/serum-dex/dex", features = ["no-entrypoint"] }
|
||||||
|
solana-program = "1.7.4"
|
|
@ -0,0 +1,2 @@
|
||||||
|
[target.bpfel-unknown-unknown.dependencies.std]
|
||||||
|
features = []
|
|
@ -0,0 +1,345 @@
|
||||||
|
// Note. This example depends on unreleased Serum DEX changes.
|
||||||
|
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
use anchor_spl::dex;
|
||||||
|
use serum_dex::instruction::MarketInstruction;
|
||||||
|
use serum_dex::state::OpenOrders;
|
||||||
|
use solana_program::instruction::Instruction;
|
||||||
|
use solana_program::program;
|
||||||
|
use solana_program::system_program;
|
||||||
|
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.
|
||||||
|
///
|
||||||
|
/// 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, i.e., `fee_owner::ID`.
|
||||||
|
#[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],
|
||||||
|
) -> ProgramResult {
|
||||||
|
require!(accounts.len() >= 1, NotEnoughAccounts);
|
||||||
|
|
||||||
|
let dex_acc_info = &accounts[0];
|
||||||
|
let dex_accounts = &accounts[1..];
|
||||||
|
let mut acc_infos = dex_accounts.to_vec();
|
||||||
|
|
||||||
|
// Decode instruction.
|
||||||
|
let ix = MarketInstruction::unpack(data).ok_or_else(|| 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);
|
||||||
|
|
||||||
|
let (market, user) = {
|
||||||
|
let market = &acc_infos[0];
|
||||||
|
let user = &acc_infos[7];
|
||||||
|
|
||||||
|
if !user.is_signer {
|
||||||
|
return Err(ErrorCode::UnauthorizedUser.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
(*market.key, *user.key)
|
||||||
|
};
|
||||||
|
|
||||||
|
acc_infos[7] = prepare_pda(&acc_infos[1]);
|
||||||
|
|
||||||
|
(market, user)
|
||||||
|
}
|
||||||
|
MarketInstruction::CancelOrderV2(_) => {
|
||||||
|
require!(dex_accounts.len() >= 6, NotEnoughAccounts);
|
||||||
|
|
||||||
|
let (market, user) = {
|
||||||
|
let market = &acc_infos[0];
|
||||||
|
let user = &acc_infos[4];
|
||||||
|
|
||||||
|
if !user.is_signer {
|
||||||
|
return Err(ErrorCode::UnauthorizedUser.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
(*market.key, *user.key)
|
||||||
|
};
|
||||||
|
|
||||||
|
acc_infos[4] = prepare_pda(&acc_infos[3]);
|
||||||
|
|
||||||
|
(market, user)
|
||||||
|
}
|
||||||
|
MarketInstruction::CancelOrderByClientIdV2(_) => {
|
||||||
|
require!(dex_accounts.len() >= 6, NotEnoughAccounts);
|
||||||
|
|
||||||
|
let (market, user) = {
|
||||||
|
let market = &acc_infos[0];
|
||||||
|
let user = &acc_infos[4];
|
||||||
|
|
||||||
|
if !user.is_signer {
|
||||||
|
return Err(ErrorCode::UnauthorizedUser.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
(*market.key, *user.key)
|
||||||
|
};
|
||||||
|
|
||||||
|
acc_infos[4] = prepare_pda(&acc_infos[3]);
|
||||||
|
|
||||||
|
(market, user)
|
||||||
|
}
|
||||||
|
MarketInstruction::SettleFunds => {
|
||||||
|
require!(dex_accounts.len() >= 10, NotEnoughAccounts);
|
||||||
|
|
||||||
|
let (market, user) = {
|
||||||
|
let market = &acc_infos[0];
|
||||||
|
let user = &acc_infos[2];
|
||||||
|
let referral = &dex_accounts[9];
|
||||||
|
|
||||||
|
if !DISABLE_REFERRAL && referral.key != &referral::ID {
|
||||||
|
return Err(ErrorCode::InvalidReferral.into());
|
||||||
|
}
|
||||||
|
if !user.is_signer {
|
||||||
|
return Err(ErrorCode::UnauthorizedUser.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
(*market.key, *user.key)
|
||||||
|
};
|
||||||
|
|
||||||
|
acc_infos[2] = prepare_pda(&acc_infos[1]);
|
||||||
|
|
||||||
|
(market, user)
|
||||||
|
}
|
||||||
|
MarketInstruction::CloseOpenOrders => {
|
||||||
|
require!(dex_accounts.len() >= 4, NotEnoughAccounts);
|
||||||
|
|
||||||
|
let (market, user) = {
|
||||||
|
let market = &acc_infos[3];
|
||||||
|
let user = &acc_infos[1];
|
||||||
|
|
||||||
|
if !user.is_signer {
|
||||||
|
return Err(ErrorCode::UnauthorizedUser.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
(*market.key, *user.key)
|
||||||
|
};
|
||||||
|
|
||||||
|
acc_infos[1] = prepare_pda(&acc_infos[0]);
|
||||||
|
|
||||||
|
(market, user)
|
||||||
|
}
|
||||||
|
_ => return Err(ErrorCode::InvalidInstruction.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// CPI to the dex.
|
||||||
|
let dex_accounts = acc_infos
|
||||||
|
.iter()
|
||||||
|
.map(|acc| AccountMeta {
|
||||||
|
pubkey: *acc.key,
|
||||||
|
is_signer: acc.is_signer,
|
||||||
|
is_writable: acc.is_writable,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
acc_infos.push(dex_acc_info.clone());
|
||||||
|
let ix = Instruction {
|
||||||
|
data: data.to_vec(),
|
||||||
|
accounts: dex_accounts,
|
||||||
|
program_id: dex::ID,
|
||||||
|
};
|
||||||
|
let seeds = open_orders_authority! {
|
||||||
|
program = program_id,
|
||||||
|
market = market,
|
||||||
|
authority = user
|
||||||
|
};
|
||||||
|
program::invoke_signed(&ix, &acc_infos, &[seeds])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accounts context.
|
||||||
|
|
||||||
|
#[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(
|
||||||
|
init,
|
||||||
|
seeds = [b"open-orders", 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(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access control modifiers.
|
||||||
|
|
||||||
|
fn is_serum<'info>(accounts: &[AccountInfo<'info>]) -> Result<()> {
|
||||||
|
let dex_acc_info = &accounts[0];
|
||||||
|
if dex_acc_info.key != &dex::ID {
|
||||||
|
return Err(ErrorCode::InvalidDexPid.into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error.
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Macros.
|
||||||
|
|
||||||
|
/// Returns the seeds used for creating the open orders account PDA.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! open_orders_authority {
|
||||||
|
(program = $program:expr, market = $market:expr, authority = $authority:expr, bump = $bump:expr) => {
|
||||||
|
&[
|
||||||
|
b"open-orders".as_ref(),
|
||||||
|
$market.as_ref(),
|
||||||
|
$authority.as_ref(),
|
||||||
|
&[$bump],
|
||||||
|
]
|
||||||
|
};
|
||||||
|
(program = $program:expr, market = $market:expr, authority = $authority:expr) => {
|
||||||
|
&[
|
||||||
|
b"open-orders".as_ref(),
|
||||||
|
$market.as_ref(),
|
||||||
|
$authority.as_ref(),
|
||||||
|
&[Pubkey::find_program_address(
|
||||||
|
&[
|
||||||
|
b"open-orders".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, market = $market:expr) => {
|
||||||
|
&[
|
||||||
|
b"open-orders-init".as_ref(),
|
||||||
|
$market.as_ref(),
|
||||||
|
&[Pubkey::find_program_address(
|
||||||
|
&[b"open-orders-init".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.
|
||||||
|
|
||||||
|
// Padding added to every serum account.
|
||||||
|
//
|
||||||
|
// b"serum".len() + b"padding".len().
|
||||||
|
const SERUM_PADDING: usize = 12;
|
||||||
|
|
||||||
|
// True if we don't care about referral access control (for testing).
|
||||||
|
const DISABLE_REFERRAL: bool = true;
|
||||||
|
|
||||||
|
/// The address that will receive all fees for all markets controlled by this
|
||||||
|
/// program. Note: this is a dummy address. Do not use in production.
|
||||||
|
pub mod referral {
|
||||||
|
solana_program::declare_id!("2k1bb16Hu7ocviT2KC3wcCgETtnC8tEUuvFBH4C5xStG");
|
||||||
|
}
|
|
@ -0,0 +1,284 @@
|
||||||
|
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 { Transaction, TransactionInstruction } = anchor.web3;
|
||||||
|
const { DexInstructions, OpenOrders, Market } = serum;
|
||||||
|
const { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } = anchor.web3;
|
||||||
|
const { initMarket, sleep } = require("./utils");
|
||||||
|
|
||||||
|
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
|
||||||
|
const REFERRAL = new PublicKey("2k1bb16Hu7ocviT2KC3wcCgETtnC8tEUuvFBH4C5xStG");
|
||||||
|
|
||||||
|
describe("permissioned-markets", () => {
|
||||||
|
// Anchor client setup.
|
||||||
|
const provider = anchor.Provider.env();
|
||||||
|
anchor.setProvider(provider);
|
||||||
|
const program = anchor.workspace.PermissionedMarkets;
|
||||||
|
|
||||||
|
// Token clients.
|
||||||
|
let usdcClient;
|
||||||
|
|
||||||
|
// Global DEX accounts and clients shared accross all tests.
|
||||||
|
let marketClient, tokenAccount, usdcAccount;
|
||||||
|
let openOrders, openOrdersBump, openOrdersInitAuthority, openOrdersBumpinit;
|
||||||
|
let usdcPosted;
|
||||||
|
let marketMakerOpenOrders;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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, {
|
||||||
|
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(
|
||||||
|
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);
|
||||||
|
|
||||||
|
// When.
|
||||||
|
const tx = new Transaction();
|
||||||
|
tx.add(
|
||||||
|
serumProxy(
|
||||||
|
DexInstructions.settleFunds({
|
||||||
|
market: marketClient._decoded.ownAddress,
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
serumProxy(
|
||||||
|
DexInstructions.closeOpenOrders({
|
||||||
|
market: marketClient._decoded.ownAddress,
|
||||||
|
openOrders,
|
||||||
|
owner: program.provider.wallet.publicKey,
|
||||||
|
solWallet: program.provider.wallet.publicKey,
|
||||||
|
programId: program.programId,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,427 @@
|
||||||
|
// Boilerplate utils to bootstrap an orderbook for testing on a localnet.
|
||||||
|
// not super relevant to the point of the example, though may be useful to
|
||||||
|
// include into your own workspace for testing.
|
||||||
|
//
|
||||||
|
// TODO: Modernize all these apis. This is all quite clunky.
|
||||||
|
|
||||||
|
const Token = require("@solana/spl-token").Token;
|
||||||
|
const TOKEN_PROGRAM_ID = require("@solana/spl-token").TOKEN_PROGRAM_ID;
|
||||||
|
const TokenInstructions = require("@project-serum/serum").TokenInstructions;
|
||||||
|
const { Market, OpenOrders } = require("@project-serum/serum");
|
||||||
|
const DexInstructions = require("@project-serum/serum").DexInstructions;
|
||||||
|
const web3 = require("@project-serum/anchor").web3;
|
||||||
|
const Connection = web3.Connection;
|
||||||
|
const anchor = require("@project-serum/anchor");
|
||||||
|
const BN = anchor.BN;
|
||||||
|
const serumCmn = require("@project-serum/common");
|
||||||
|
const Account = web3.Account;
|
||||||
|
const Transaction = web3.Transaction;
|
||||||
|
const PublicKey = web3.PublicKey;
|
||||||
|
const SystemProgram = web3.SystemProgram;
|
||||||
|
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
|
||||||
|
const MARKET_MAKER = new Account();
|
||||||
|
|
||||||
|
async function initMarket({ provider }) {
|
||||||
|
// Setup mints with initial tokens owned by the provider.
|
||||||
|
const decimals = 6;
|
||||||
|
const [MINT_A, GOD_A] = await serumCmn.createMintAndVault(
|
||||||
|
provider,
|
||||||
|
new BN("1000000000000000000"),
|
||||||
|
undefined,
|
||||||
|
decimals
|
||||||
|
);
|
||||||
|
const [USDC, GOD_USDC] = await serumCmn.createMintAndVault(
|
||||||
|
provider,
|
||||||
|
new BN("1000000000000000000"),
|
||||||
|
undefined,
|
||||||
|
decimals
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a funded account to act as market maker.
|
||||||
|
const amount = new BN("10000000000000").muln(10 ** decimals);
|
||||||
|
const marketMaker = await fundAccount({
|
||||||
|
provider,
|
||||||
|
mints: [
|
||||||
|
{ god: GOD_A, mint: MINT_A, amount, decimals },
|
||||||
|
{ god: GOD_USDC, mint: USDC, amount, decimals },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup A/USDC with resting orders.
|
||||||
|
const asks = [
|
||||||
|
[6.041, 7.8],
|
||||||
|
[6.051, 72.3],
|
||||||
|
[6.055, 5.4],
|
||||||
|
[6.067, 15.7],
|
||||||
|
[6.077, 390.0],
|
||||||
|
[6.09, 24.0],
|
||||||
|
[6.11, 36.3],
|
||||||
|
[6.133, 300.0],
|
||||||
|
[6.167, 687.8],
|
||||||
|
];
|
||||||
|
const bids = [
|
||||||
|
[6.004, 8.5],
|
||||||
|
[5.995, 12.9],
|
||||||
|
[5.987, 6.2],
|
||||||
|
[5.978, 15.3],
|
||||||
|
[5.965, 82.8],
|
||||||
|
[5.961, 25.4],
|
||||||
|
];
|
||||||
|
|
||||||
|
[MARKET_A_USDC, vaultSigner] = await setupMarket({
|
||||||
|
baseMint: MINT_A,
|
||||||
|
quoteMint: USDC,
|
||||||
|
marketMaker: {
|
||||||
|
account: marketMaker.account,
|
||||||
|
baseToken: marketMaker.tokens[MINT_A.toString()],
|
||||||
|
quoteToken: marketMaker.tokens[USDC.toString()],
|
||||||
|
},
|
||||||
|
bids,
|
||||||
|
asks,
|
||||||
|
provider,
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
godUsdc: GOD_USDC,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fundAccount({ provider, mints }) {
|
||||||
|
const marketMaker = {
|
||||||
|
tokens: {},
|
||||||
|
account: MARKET_MAKER,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transfer lamports to market maker.
|
||||||
|
await provider.send(
|
||||||
|
(() => {
|
||||||
|
const tx = new Transaction();
|
||||||
|
tx.add(
|
||||||
|
SystemProgram.transfer({
|
||||||
|
fromPubkey: provider.wallet.publicKey,
|
||||||
|
toPubkey: MARKET_MAKER.publicKey,
|
||||||
|
lamports: 100000000000,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return tx;
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Transfer SPL tokens to the market maker.
|
||||||
|
for (let k = 0; k < mints.length; k += 1) {
|
||||||
|
const { mint, god, amount, decimals } = mints[k];
|
||||||
|
let MINT_A = mint;
|
||||||
|
let GOD_A = god;
|
||||||
|
// Setup token accounts owned by the market maker.
|
||||||
|
const mintAClient = new Token(
|
||||||
|
provider.connection,
|
||||||
|
MINT_A,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
provider.wallet.payer // node only
|
||||||
|
);
|
||||||
|
const marketMakerTokenA = await mintAClient.createAccount(
|
||||||
|
MARKET_MAKER.publicKey
|
||||||
|
);
|
||||||
|
|
||||||
|
await provider.send(
|
||||||
|
(() => {
|
||||||
|
const tx = new Transaction();
|
||||||
|
tx.add(
|
||||||
|
Token.createTransferCheckedInstruction(
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
GOD_A,
|
||||||
|
MINT_A,
|
||||||
|
marketMakerTokenA,
|
||||||
|
provider.wallet.publicKey,
|
||||||
|
[],
|
||||||
|
amount,
|
||||||
|
decimals
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return tx;
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
|
||||||
|
marketMaker.tokens[mint.toString()] = marketMakerTokenA;
|
||||||
|
}
|
||||||
|
|
||||||
|
return marketMaker;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupMarket({
|
||||||
|
provider,
|
||||||
|
marketMaker,
|
||||||
|
baseMint,
|
||||||
|
quoteMint,
|
||||||
|
bids,
|
||||||
|
asks,
|
||||||
|
}) {
|
||||||
|
const [marketAPublicKey, vaultOwner] = await listMarket({
|
||||||
|
connection: provider.connection,
|
||||||
|
wallet: provider.wallet,
|
||||||
|
baseMint: baseMint,
|
||||||
|
quoteMint: quoteMint,
|
||||||
|
baseLotSize: 100000,
|
||||||
|
quoteLotSize: 100,
|
||||||
|
dexProgramId: DEX_PID,
|
||||||
|
feeRateBps: 0,
|
||||||
|
});
|
||||||
|
const MARKET_A_USDC = await Market.load(
|
||||||
|
provider.connection,
|
||||||
|
marketAPublicKey,
|
||||||
|
{ commitment: "recent" },
|
||||||
|
DEX_PID
|
||||||
|
);
|
||||||
|
for (let k = 0; k < asks.length; k += 1) {
|
||||||
|
let ask = asks[k];
|
||||||
|
const {
|
||||||
|
transaction,
|
||||||
|
signers,
|
||||||
|
} = await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, {
|
||||||
|
owner: marketMaker.account,
|
||||||
|
payer: marketMaker.baseToken,
|
||||||
|
side: "sell",
|
||||||
|
price: ask[0],
|
||||||
|
size: ask[1],
|
||||||
|
orderType: "postOnly",
|
||||||
|
clientId: undefined,
|
||||||
|
openOrdersAddressKey: undefined,
|
||||||
|
openOrdersAccount: undefined,
|
||||||
|
feeDiscountPubkey: null,
|
||||||
|
selfTradeBehavior: "abortTransaction",
|
||||||
|
});
|
||||||
|
await provider.send(transaction, signers.concat(marketMaker.account));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let k = 0; k < bids.length; k += 1) {
|
||||||
|
let bid = bids[k];
|
||||||
|
const {
|
||||||
|
transaction,
|
||||||
|
signers,
|
||||||
|
} = await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, {
|
||||||
|
owner: marketMaker.account,
|
||||||
|
payer: marketMaker.quoteToken,
|
||||||
|
side: "buy",
|
||||||
|
price: bid[0],
|
||||||
|
size: bid[1],
|
||||||
|
orderType: "postOnly",
|
||||||
|
clientId: undefined,
|
||||||
|
openOrdersAddressKey: undefined,
|
||||||
|
openOrdersAccount: undefined,
|
||||||
|
feeDiscountPubkey: null,
|
||||||
|
selfTradeBehavior: "abortTransaction",
|
||||||
|
});
|
||||||
|
await provider.send(transaction, signers.concat(marketMaker.account));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [MARKET_A_USDC, vaultOwner];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listMarket({
|
||||||
|
connection,
|
||||||
|
wallet,
|
||||||
|
baseMint,
|
||||||
|
quoteMint,
|
||||||
|
baseLotSize,
|
||||||
|
quoteLotSize,
|
||||||
|
dexProgramId,
|
||||||
|
feeRateBps,
|
||||||
|
}) {
|
||||||
|
const market = new Account();
|
||||||
|
const requestQueue = new Account();
|
||||||
|
const eventQueue = new Account();
|
||||||
|
const bids = new Account();
|
||||||
|
const asks = new Account();
|
||||||
|
const baseVault = new Account();
|
||||||
|
const quoteVault = new Account();
|
||||||
|
const quoteDustThreshold = new BN(100);
|
||||||
|
|
||||||
|
const [vaultOwner, vaultSignerNonce] = await getVaultOwnerAndNonce(
|
||||||
|
market.publicKey,
|
||||||
|
dexProgramId
|
||||||
|
);
|
||||||
|
|
||||||
|
const tx1 = new Transaction();
|
||||||
|
tx1.add(
|
||||||
|
SystemProgram.createAccount({
|
||||||
|
fromPubkey: wallet.publicKey,
|
||||||
|
newAccountPubkey: baseVault.publicKey,
|
||||||
|
lamports: await connection.getMinimumBalanceForRentExemption(165),
|
||||||
|
space: 165,
|
||||||
|
programId: TOKEN_PROGRAM_ID,
|
||||||
|
}),
|
||||||
|
SystemProgram.createAccount({
|
||||||
|
fromPubkey: wallet.publicKey,
|
||||||
|
newAccountPubkey: quoteVault.publicKey,
|
||||||
|
lamports: await connection.getMinimumBalanceForRentExemption(165),
|
||||||
|
space: 165,
|
||||||
|
programId: TOKEN_PROGRAM_ID,
|
||||||
|
}),
|
||||||
|
TokenInstructions.initializeAccount({
|
||||||
|
account: baseVault.publicKey,
|
||||||
|
mint: baseMint,
|
||||||
|
owner: vaultOwner,
|
||||||
|
}),
|
||||||
|
TokenInstructions.initializeAccount({
|
||||||
|
account: quoteVault.publicKey,
|
||||||
|
mint: quoteMint,
|
||||||
|
owner: vaultOwner,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const tx2 = new Transaction();
|
||||||
|
tx2.add(
|
||||||
|
SystemProgram.createAccount({
|
||||||
|
fromPubkey: wallet.publicKey,
|
||||||
|
newAccountPubkey: market.publicKey,
|
||||||
|
lamports: await connection.getMinimumBalanceForRentExemption(
|
||||||
|
Market.getLayout(dexProgramId).span
|
||||||
|
),
|
||||||
|
space: Market.getLayout(dexProgramId).span,
|
||||||
|
programId: dexProgramId,
|
||||||
|
}),
|
||||||
|
SystemProgram.createAccount({
|
||||||
|
fromPubkey: wallet.publicKey,
|
||||||
|
newAccountPubkey: requestQueue.publicKey,
|
||||||
|
lamports: await connection.getMinimumBalanceForRentExemption(5120 + 12),
|
||||||
|
space: 5120 + 12,
|
||||||
|
programId: dexProgramId,
|
||||||
|
}),
|
||||||
|
SystemProgram.createAccount({
|
||||||
|
fromPubkey: wallet.publicKey,
|
||||||
|
newAccountPubkey: eventQueue.publicKey,
|
||||||
|
lamports: await connection.getMinimumBalanceForRentExemption(262144 + 12),
|
||||||
|
space: 262144 + 12,
|
||||||
|
programId: dexProgramId,
|
||||||
|
}),
|
||||||
|
SystemProgram.createAccount({
|
||||||
|
fromPubkey: wallet.publicKey,
|
||||||
|
newAccountPubkey: bids.publicKey,
|
||||||
|
lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
|
||||||
|
space: 65536 + 12,
|
||||||
|
programId: dexProgramId,
|
||||||
|
}),
|
||||||
|
SystemProgram.createAccount({
|
||||||
|
fromPubkey: wallet.publicKey,
|
||||||
|
newAccountPubkey: asks.publicKey,
|
||||||
|
lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
|
||||||
|
space: 65536 + 12,
|
||||||
|
programId: dexProgramId,
|
||||||
|
}),
|
||||||
|
DexInstructions.initializeMarket({
|
||||||
|
market: market.publicKey,
|
||||||
|
requestQueue: requestQueue.publicKey,
|
||||||
|
eventQueue: eventQueue.publicKey,
|
||||||
|
bids: bids.publicKey,
|
||||||
|
asks: asks.publicKey,
|
||||||
|
baseVault: baseVault.publicKey,
|
||||||
|
quoteVault: quoteVault.publicKey,
|
||||||
|
baseMint,
|
||||||
|
quoteMint,
|
||||||
|
baseLotSize: new BN(baseLotSize),
|
||||||
|
quoteLotSize: new BN(quoteLotSize),
|
||||||
|
feeRateBps,
|
||||||
|
vaultSignerNonce,
|
||||||
|
quoteDustThreshold,
|
||||||
|
programId: dexProgramId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const signedTransactions = await signTransactions({
|
||||||
|
transactionsAndSigners: [
|
||||||
|
{ transaction: tx1, signers: [baseVault, quoteVault] },
|
||||||
|
{
|
||||||
|
transaction: tx2,
|
||||||
|
signers: [market, requestQueue, eventQueue, bids, asks],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
wallet,
|
||||||
|
connection,
|
||||||
|
});
|
||||||
|
for (let signedTransaction of signedTransactions) {
|
||||||
|
await sendAndConfirmRawTransaction(
|
||||||
|
connection,
|
||||||
|
signedTransaction.serialize()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const acc = await connection.getAccountInfo(market.publicKey);
|
||||||
|
|
||||||
|
return [market.publicKey, vaultOwner];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signTransactions({
|
||||||
|
transactionsAndSigners,
|
||||||
|
wallet,
|
||||||
|
connection,
|
||||||
|
}) {
|
||||||
|
const blockhash = (await connection.getRecentBlockhash("max")).blockhash;
|
||||||
|
transactionsAndSigners.forEach(({ transaction, signers = [] }) => {
|
||||||
|
transaction.recentBlockhash = blockhash;
|
||||||
|
transaction.setSigners(
|
||||||
|
wallet.publicKey,
|
||||||
|
...signers.map((s) => s.publicKey)
|
||||||
|
);
|
||||||
|
if (signers?.length > 0) {
|
||||||
|
transaction.partialSign(...signers);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return await wallet.signAllTransactions(
|
||||||
|
transactionsAndSigners.map(({ transaction }) => transaction)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendAndConfirmRawTransaction(
|
||||||
|
connection,
|
||||||
|
raw,
|
||||||
|
commitment = "recent"
|
||||||
|
) {
|
||||||
|
let tx = await connection.sendRawTransaction(raw, {
|
||||||
|
skipPreflight: true,
|
||||||
|
});
|
||||||
|
return await connection.confirmTransaction(tx, commitment);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getVaultOwnerAndNonce(marketPublicKey, dexProgramId = DEX_PID) {
|
||||||
|
const nonce = new BN(0);
|
||||||
|
while (nonce.toNumber() < 255) {
|
||||||
|
try {
|
||||||
|
const vaultOwner = await PublicKey.createProgramAddress(
|
||||||
|
[marketPublicKey.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)],
|
||||||
|
dexProgramId
|
||||||
|
);
|
||||||
|
return [vaultOwner, nonce];
|
||||||
|
} catch (e) {
|
||||||
|
nonce.iaddn(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Unable to find nonce");
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
fundAccount,
|
||||||
|
initMarket,
|
||||||
|
setupMarket,
|
||||||
|
DEX_PID,
|
||||||
|
getVaultOwnerAndNonce,
|
||||||
|
sleep,
|
||||||
|
};
|
|
@ -42,7 +42,7 @@ use syn::parse_macro_input;
|
||||||
/// | `#[account(init)]` | On `ProgramAccount` structs. | Marks the account as being initialized, skipping the account discriminator check. When using `init`, a `rent` `Sysvar` must be present in the `Accounts` struct. |
|
/// | `#[account(init)]` | On `ProgramAccount` structs. | Marks the account as being initialized, skipping the account discriminator check. When using `init`, a `rent` `Sysvar` must be present in the `Accounts` struct. |
|
||||||
/// | `#[account(close = <target>)]` | On `ProgramAccount` and `Loader` structs. | Marks the account as being closed at the end of the instruction's execution, sending the rent exemption lamports to the specified <target>. |
|
/// | `#[account(close = <target>)]` | On `ProgramAccount` and `Loader` structs. | Marks the account as being closed at the end of the instruction's execution, sending the rent exemption lamports to the specified <target>. |
|
||||||
/// | `#[account(has_one = <target>)]` | On `ProgramAccount` or `CpiAccount` structs | Checks the `target` field on the account matches the `target` field in the struct deriving `Accounts`. |
|
/// | `#[account(has_one = <target>)]` | On `ProgramAccount` or `CpiAccount` structs | Checks the `target` field on the account matches the `target` field in the struct deriving `Accounts`. |
|
||||||
/// | `#[account(seeds = [<seeds>])]` | On `AccountInfo` structs | Seeds for the program derived address an `AccountInfo` struct represents. |
|
/// | `#[account(seeds = [<seeds>], bump? = <target>, payer? = <target>, space? = <target>, owner? = <target>)]` | On `AccountInfo` structs | Seeds for the program derived address an `AccountInfo` struct represents. If bump is provided, then appends it to the seeds. On initialization, validates the given bump is the bump provided by `Pubkey::find_program_address`.|
|
||||||
/// | `#[account(constraint = <expression>)]` | On any type deriving `Accounts` | Executes the given code as a constraint. The expression should evaluate to a boolean. |
|
/// | `#[account(constraint = <expression>)]` | On any type deriving `Accounts` | Executes the given code as a constraint. The expression should evaluate to a boolean. |
|
||||||
/// | `#[account("<literal>")]` | Deprecated | Executes the given code literal as a constraint. The literal should evaluate to a boolean. |
|
/// | `#[account("<literal>")]` | Deprecated | Executes the given code literal as a constraint. The literal should evaluate to a boolean. |
|
||||||
/// | `#[account(rent_exempt = <skip>)]` | On `AccountInfo` or `ProgramAccount` structs | Optional attribute to skip the rent exemption check. By default, all accounts marked with `#[account(init)]` will be rent exempt, and so this should rarely (if ever) be used. Similarly, omitting `= skip` will mark the account rent exempt. |
|
/// | `#[account(rent_exempt = <skip>)]` | On `AccountInfo` or `ProgramAccount` structs | Optional attribute to skip the rent exemption check. By default, all accounts marked with `#[account(init)]` will be rent exempt, and so this should rarely (if ever) be used. Similarly, omitting `= skip` will mark the account rent exempt. |
|
||||||
|
|
|
@ -233,10 +233,10 @@ impl Key for Pubkey {
|
||||||
/// All programs should include it via `anchor_lang::prelude::*;`.
|
/// All programs should include it via `anchor_lang::prelude::*;`.
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use super::{
|
pub use super::{
|
||||||
access_control, account, associated, emit, error, event, interface, program, state,
|
access_control, account, associated, emit, error, event, interface, program, require,
|
||||||
zero_copy, AccountDeserialize, AccountSerialize, Accounts, AccountsExit, AccountsInit,
|
state, zero_copy, AccountDeserialize, AccountSerialize, Accounts, AccountsExit,
|
||||||
AnchorDeserialize, AnchorSerialize, Context, CpiAccount, CpiContext, CpiState,
|
AccountsInit, AnchorDeserialize, AnchorSerialize, Context, CpiAccount, CpiContext,
|
||||||
CpiStateContext, Loader, ProgramAccount, ProgramState, Sysvar, ToAccountInfo,
|
CpiState, CpiStateContext, Loader, ProgramAccount, ProgramState, Sysvar, ToAccountInfo,
|
||||||
ToAccountInfos, ToAccountMetas,
|
ToAccountInfos, ToAccountMetas,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -327,3 +327,34 @@ macro_rules! associated_seeds {
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensures a condition is true, otherwise returns the given error.
|
||||||
|
/// Use this with a custom error type.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// After defining an `ErrorCode`
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// #[error]
|
||||||
|
/// pub struct ErrorCode {
|
||||||
|
/// InvalidArgument,
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// One can write a `require` assertion as
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// require!(condition, InvalidArgument);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// which would exit the program with the `InvalidArgument` error code if
|
||||||
|
/// `condition` is false.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! require {
|
||||||
|
($invariant:expr, $error:tt $(,)?) => {
|
||||||
|
if !($invariant) {
|
||||||
|
return Err(crate::ErrorCode::$error.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -265,8 +265,13 @@ fn generate_constraint_seeds_init(f: &Field, c: &ConstraintSeedsGroup) -> proc_m
|
||||||
let seeds_constraint = generate_constraint_seeds_address(f, c);
|
let seeds_constraint = generate_constraint_seeds_address(f, c);
|
||||||
let seeds_with_nonce = {
|
let seeds_with_nonce = {
|
||||||
let s = &c.seeds;
|
let s = &c.seeds;
|
||||||
quote! {
|
match c.bump.as_ref() {
|
||||||
|
None => quote! {
|
||||||
[#s]
|
[#s]
|
||||||
|
},
|
||||||
|
Some(b) => quote! {
|
||||||
|
[#s, &[#b]]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
generate_pda(
|
generate_pda(
|
||||||
|
@ -285,16 +290,49 @@ fn generate_constraint_seeds_address(
|
||||||
c: &ConstraintSeedsGroup,
|
c: &ConstraintSeedsGroup,
|
||||||
) -> proc_macro2::TokenStream {
|
) -> proc_macro2::TokenStream {
|
||||||
let name = &f.ident;
|
let name = &f.ident;
|
||||||
let seeds = &c.seeds;
|
|
||||||
|
// If the bump is provided on *initialization*, then force it to be the
|
||||||
|
// canonical nonce.
|
||||||
|
if c.is_init && c.bump.is_some() {
|
||||||
|
let s = &c.seeds;
|
||||||
|
let b = c.bump.as_ref().unwrap();
|
||||||
|
quote! {
|
||||||
|
let (__program_signer, __bump) = anchor_lang::solana_program::pubkey::Pubkey::find_program_address(
|
||||||
|
&[#s],
|
||||||
|
program_id,
|
||||||
|
);
|
||||||
|
if #name.to_account_info().key != &__program_signer {
|
||||||
|
return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into());
|
||||||
|
}
|
||||||
|
if __bump != #b {
|
||||||
|
return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let seeds = match c.bump.as_ref() {
|
||||||
|
None => {
|
||||||
|
let s = &c.seeds;
|
||||||
|
quote! {
|
||||||
|
[#s]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(b) => {
|
||||||
|
let s = &c.seeds;
|
||||||
|
quote! {
|
||||||
|
[#s, &[#b]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
quote! {
|
quote! {
|
||||||
let __program_signer = Pubkey::create_program_address(
|
let __program_signer = Pubkey::create_program_address(
|
||||||
&[#seeds],
|
&#seeds,
|
||||||
program_id,
|
program_id,
|
||||||
).map_err(|_| anchor_lang::__private::ErrorCode::ConstraintSeeds)?;
|
).map_err(|_| anchor_lang::__private::ErrorCode::ConstraintSeeds)?;
|
||||||
if #name.to_account_info().key != &__program_signer {
|
if #name.to_account_info().key != &__program_signer {
|
||||||
return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into());
|
return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_constraint_associated(
|
pub fn generate_constraint_associated(
|
||||||
|
@ -355,28 +393,50 @@ pub fn generate_constraint_associated_init(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_ty(f: &Field) -> (&syn::TypePath, proc_macro2::TokenStream, bool) {
|
fn parse_ty(f: &Field) -> (proc_macro2::TokenStream, proc_macro2::TokenStream, bool) {
|
||||||
match &f.ty {
|
match &f.ty {
|
||||||
Ty::ProgramAccount(ty) => (
|
Ty::ProgramAccount(ty) => {
|
||||||
&ty.account_type_path,
|
let ident = &ty.account_type_path;
|
||||||
|
(
|
||||||
|
quote! {
|
||||||
|
#ident
|
||||||
|
},
|
||||||
quote! {
|
quote! {
|
||||||
anchor_lang::ProgramAccount
|
anchor_lang::ProgramAccount
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
),
|
)
|
||||||
Ty::Loader(ty) => (
|
}
|
||||||
&ty.account_type_path,
|
Ty::Loader(ty) => {
|
||||||
|
let ident = &ty.account_type_path;
|
||||||
|
(
|
||||||
|
quote! {
|
||||||
|
#ident
|
||||||
|
},
|
||||||
quote! {
|
quote! {
|
||||||
anchor_lang::Loader
|
anchor_lang::Loader
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
),
|
)
|
||||||
Ty::CpiAccount(ty) => (
|
}
|
||||||
&ty.account_type_path,
|
Ty::CpiAccount(ty) => {
|
||||||
|
let ident = &ty.account_type_path;
|
||||||
|
(
|
||||||
|
quote! {
|
||||||
|
#ident
|
||||||
|
},
|
||||||
quote! {
|
quote! {
|
||||||
anchor_lang::CpiAccount
|
anchor_lang::CpiAccount
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Ty::AccountInfo => (
|
||||||
|
quote! {
|
||||||
|
AccountInfo
|
||||||
|
},
|
||||||
|
quote! {},
|
||||||
|
false,
|
||||||
),
|
),
|
||||||
_ => panic!("Invalid type for initializing a program derived address"),
|
_ => panic!("Invalid type for initializing a program derived address"),
|
||||||
}
|
}
|
||||||
|
@ -431,9 +491,30 @@ pub fn generate_pda(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let (combined_account_ty, try_from) = match f.ty {
|
||||||
|
Ty::AccountInfo => (
|
||||||
|
quote! {
|
||||||
|
AccountInfo
|
||||||
|
},
|
||||||
|
quote! {
|
||||||
|
#field.to_account_info()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_ => (
|
||||||
|
quote! {
|
||||||
|
#account_wrapper_ty<#account_ty>
|
||||||
|
},
|
||||||
|
quote! {
|
||||||
|
#account_wrapper_ty::try_from_init(
|
||||||
|
&#field.to_account_info(),
|
||||||
|
)?
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
match kind {
|
match kind {
|
||||||
PdaKind::Token { owner, mint } => quote! {
|
PdaKind::Token { owner, mint } => quote! {
|
||||||
let #field: #account_wrapper_ty<#account_ty> = {
|
let #field: #combined_account_ty = {
|
||||||
#space
|
#space
|
||||||
#payer
|
#payer
|
||||||
#seeds_constraint
|
#seeds_constraint
|
||||||
|
@ -500,9 +581,19 @@ pub fn generate_pda(
|
||||||
)?
|
)?
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PdaKind::Program => {
|
PdaKind::Program { owner } => {
|
||||||
|
// Owner of the account being created. If not specified,
|
||||||
|
// default to the currently executing program.
|
||||||
|
let owner = match owner {
|
||||||
|
None => quote! {
|
||||||
|
program_id
|
||||||
|
},
|
||||||
|
Some(o) => quote! {
|
||||||
|
&#o
|
||||||
|
},
|
||||||
|
};
|
||||||
quote! {
|
quote! {
|
||||||
let #field: #account_wrapper_ty<#account_ty> = {
|
let #field = {
|
||||||
#space
|
#space
|
||||||
#payer
|
#payer
|
||||||
#seeds_constraint
|
#seeds_constraint
|
||||||
|
@ -513,10 +604,9 @@ pub fn generate_pda(
|
||||||
#field.to_account_info().key,
|
#field.to_account_info().key,
|
||||||
lamports,
|
lamports,
|
||||||
space as u64,
|
space as u64,
|
||||||
program_id,
|
#owner,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
anchor_lang::solana_program::program::invoke_signed(
|
anchor_lang::solana_program::program::invoke_signed(
|
||||||
&ix,
|
&ix,
|
||||||
&[
|
&[
|
||||||
|
@ -533,9 +623,7 @@ pub fn generate_pda(
|
||||||
|
|
||||||
// For now, we assume all accounts created with the `associated`
|
// For now, we assume all accounts created with the `associated`
|
||||||
// attribute have a `nonce` field in their account.
|
// attribute have a `nonce` field in their account.
|
||||||
let mut pa: #account_wrapper_ty<#account_ty> = #account_wrapper_ty::try_from_init(
|
let mut pa: #combined_account_ty = #try_from;
|
||||||
&#field.to_account_info(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
#nonce_assignment
|
#nonce_assignment
|
||||||
pa
|
pa
|
||||||
|
|
|
@ -135,7 +135,21 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
|
||||||
/// With this 8 byte identifier, Anchor performs method dispatch,
|
/// With this 8 byte identifier, Anchor performs method dispatch,
|
||||||
/// matching the given 8 byte identifier to the associated method
|
/// matching the given 8 byte identifier to the associated method
|
||||||
/// handler, which leads to user defined code being eventually invoked.
|
/// handler, which leads to user defined code being eventually invoked.
|
||||||
fn dispatch(program_id: &Pubkey, accounts: &[AccountInfo], sighash: [u8; 8], ix_data: &[u8]) -> ProgramResult {
|
fn dispatch(
|
||||||
|
program_id: &Pubkey,
|
||||||
|
accounts: &[AccountInfo],
|
||||||
|
data: &[u8],
|
||||||
|
) -> ProgramResult {
|
||||||
|
// Split the instruction data into the first 8 byte method
|
||||||
|
// identifier (sighash) and the serialized instruction data.
|
||||||
|
let mut ix_data: &[u8] = data;
|
||||||
|
let sighash: [u8; 8] = {
|
||||||
|
let mut sighash: [u8; 8] = [0; 8];
|
||||||
|
sighash.copy_from_slice(&ix_data[..8]);
|
||||||
|
ix_data = &ix_data[8..];
|
||||||
|
sighash
|
||||||
|
};
|
||||||
|
|
||||||
// If the method identifier is the IDL tag, then execute an IDL
|
// If the method identifier is the IDL tag, then execute an IDL
|
||||||
// instruction, injected into all Anchor programs.
|
// instruction, injected into all Anchor programs.
|
||||||
if cfg!(not(feature = "no-idl")) {
|
if cfg!(not(feature = "no-idl")) {
|
||||||
|
@ -167,7 +181,7 @@ pub fn gen_fallback(program: &Program) -> Option<proc_macro2::TokenStream> {
|
||||||
let method = &fallback_fn.raw_method;
|
let method = &fallback_fn.raw_method;
|
||||||
let fn_name = &method.sig.ident;
|
let fn_name = &method.sig.ident;
|
||||||
quote! {
|
quote! {
|
||||||
#program_name::#fn_name(program_id, accounts, ix_data)
|
#program_name::#fn_name(program_id, accounts, data)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,26 +50,16 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
|
||||||
/// The `entry` function here, defines the standard entry to a Solana
|
/// The `entry` function here, defines the standard entry to a Solana
|
||||||
/// program, where execution begins.
|
/// program, where execution begins.
|
||||||
#[cfg(not(feature = "no-entrypoint"))]
|
#[cfg(not(feature = "no-entrypoint"))]
|
||||||
fn entry(program_id: &Pubkey, accounts: &[AccountInfo], ix_data: &[u8]) -> ProgramResult {
|
fn entry(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
|
||||||
#[cfg(feature = "anchor-debug")]
|
#[cfg(feature = "anchor-debug")]
|
||||||
{
|
{
|
||||||
msg!("anchor-debug is active");
|
msg!("anchor-debug is active");
|
||||||
}
|
}
|
||||||
if ix_data.len() < 8 {
|
if data.len() < 8 {
|
||||||
return #fallback_maybe
|
return #fallback_maybe
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split the instruction data into the first 8 byte method
|
dispatch(program_id, accounts, data)
|
||||||
// identifier (sighash) and the serialized instruction data.
|
|
||||||
let mut ix_data: &[u8] = ix_data;
|
|
||||||
let sighash: [u8; 8] = {
|
|
||||||
let mut sighash: [u8; 8] = [0; 8];
|
|
||||||
sighash.copy_from_slice(&ix_data[..8]);
|
|
||||||
ix_data = &ix_data[8..];
|
|
||||||
sighash
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch(program_id, accounts, sighash, ix_data)
|
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
anchor_lang::solana_program::msg!(&e.to_string());
|
anchor_lang::solana_program::msg!(&e.to_string());
|
||||||
e
|
e
|
||||||
|
|
|
@ -341,6 +341,7 @@ pub enum ConstraintToken {
|
||||||
Address(Context<ConstraintAddress>),
|
Address(Context<ConstraintAddress>),
|
||||||
TokenMint(Context<ConstraintTokenMint>),
|
TokenMint(Context<ConstraintTokenMint>),
|
||||||
TokenAuthority(Context<ConstraintTokenAuthority>),
|
TokenAuthority(Context<ConstraintTokenAuthority>),
|
||||||
|
Bump(Context<ConstraintTokenBump>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse for ConstraintToken {
|
impl Parse for ConstraintToken {
|
||||||
|
@ -396,6 +397,7 @@ pub struct ConstraintSeedsGroup {
|
||||||
pub payer: Option<Ident>,
|
pub payer: Option<Ident>,
|
||||||
pub space: Option<Expr>,
|
pub space: Option<Expr>,
|
||||||
pub kind: PdaKind,
|
pub kind: PdaKind,
|
||||||
|
pub bump: Option<Expr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -444,7 +446,7 @@ pub struct ConstraintAssociatedSpace {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum PdaKind {
|
pub enum PdaKind {
|
||||||
Program,
|
Program { owner: Option<Expr> },
|
||||||
Token { owner: Expr, mint: Expr },
|
Token { owner: Expr, mint: Expr },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -463,6 +465,11 @@ pub struct ConstraintTokenAuthority {
|
||||||
auth: Expr,
|
auth: Expr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ConstraintTokenBump {
|
||||||
|
bump: Expr,
|
||||||
|
}
|
||||||
|
|
||||||
// Syntaxt context object for preserving metadata about the inner item.
|
// Syntaxt context object for preserving metadata about the inner item.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Context<T> {
|
pub struct Context<T> {
|
||||||
|
|
|
@ -1,11 +1,4 @@
|
||||||
use crate::{
|
use crate::*;
|
||||||
ConstraintAddress, ConstraintAssociated, ConstraintAssociatedGroup, ConstraintAssociatedPayer,
|
|
||||||
ConstraintAssociatedSpace, ConstraintAssociatedWith, ConstraintClose, ConstraintExecutable,
|
|
||||||
ConstraintGroup, ConstraintHasOne, ConstraintInit, ConstraintLiteral, ConstraintMut,
|
|
||||||
ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeeds, ConstraintSeedsGroup,
|
|
||||||
ConstraintSigner, ConstraintState, ConstraintToken, ConstraintTokenAuthority,
|
|
||||||
ConstraintTokenMint, Context, PdaKind, Ty,
|
|
||||||
};
|
|
||||||
use syn::ext::IdentExt;
|
use syn::ext::IdentExt;
|
||||||
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
|
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
|
||||||
use syn::punctuated::Punctuated;
|
use syn::punctuated::Punctuated;
|
||||||
|
@ -183,6 +176,12 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
|
||||||
auth: stream.parse()?,
|
auth: stream.parse()?,
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
|
"bump" => ConstraintToken::Bump(Context::new(
|
||||||
|
ident.span(),
|
||||||
|
ConstraintTokenBump {
|
||||||
|
bump: stream.parse()?,
|
||||||
|
},
|
||||||
|
)),
|
||||||
_ => return Err(ParseError::new(ident.span(), "Invalid attribute")),
|
_ => return Err(ParseError::new(ident.span(), "Invalid attribute")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,6 +212,7 @@ pub struct ConstraintGroupBuilder<'ty> {
|
||||||
pub address: Option<Context<ConstraintAddress>>,
|
pub address: Option<Context<ConstraintAddress>>,
|
||||||
pub token_mint: Option<Context<ConstraintTokenMint>>,
|
pub token_mint: Option<Context<ConstraintTokenMint>>,
|
||||||
pub token_authority: Option<Context<ConstraintTokenAuthority>>,
|
pub token_authority: Option<Context<ConstraintTokenAuthority>>,
|
||||||
|
pub bump: Option<Context<ConstraintTokenBump>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'ty> ConstraintGroupBuilder<'ty> {
|
impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||||
|
@ -238,6 +238,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||||
address: None,
|
address: None,
|
||||||
token_mint: None,
|
token_mint: None,
|
||||||
token_authority: None,
|
token_authority: None,
|
||||||
|
bump: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn build(mut self) -> ParseResult<ConstraintGroup> {
|
pub fn build(mut self) -> ParseResult<ConstraintGroup> {
|
||||||
|
@ -298,6 +299,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||||
address,
|
address,
|
||||||
token_mint,
|
token_mint,
|
||||||
token_authority,
|
token_authority,
|
||||||
|
bump,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
// Converts Option<Context<T>> -> Option<T>.
|
// Converts Option<Context<T>> -> Option<T>.
|
||||||
|
@ -316,6 +318,14 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (owner, pda_owner) = {
|
||||||
|
if seeds.is_some() || associated.is_some() {
|
||||||
|
(None, owner.map(|o| o.owner_target.clone()))
|
||||||
|
} else {
|
||||||
|
(owner, None)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let is_init = init.is_some();
|
let is_init = init.is_some();
|
||||||
Ok(ConstraintGroup {
|
Ok(ConstraintGroup {
|
||||||
init: into_inner!(init),
|
init: into_inner!(init),
|
||||||
|
@ -334,7 +344,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||||
payer: into_inner!(associated_payer.clone()).map(|a| a.target),
|
payer: into_inner!(associated_payer.clone()).map(|a| a.target),
|
||||||
space: associated_space.clone().map(|s| s.space.clone()),
|
space: associated_space.clone().map(|s| s.space.clone()),
|
||||||
kind: match &token_mint {
|
kind: match &token_mint {
|
||||||
None => PdaKind::Program,
|
None => PdaKind::Program {
|
||||||
|
owner: pda_owner.clone(),
|
||||||
|
},
|
||||||
Some(tm) => PdaKind::Token {
|
Some(tm) => PdaKind::Token {
|
||||||
mint: tm.clone().into_inner().mint,
|
mint: tm.clone().into_inner().mint,
|
||||||
owner: match &token_authority {
|
owner: match &token_authority {
|
||||||
|
@ -346,6 +358,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
bump: into_inner!(bump).map(|b| b.bump),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
|
@ -358,7 +371,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||||
payer: associated_payer.map(|p| p.target.clone()),
|
payer: associated_payer.map(|p| p.target.clone()),
|
||||||
space: associated_space.map(|s| s.space.clone()),
|
space: associated_space.map(|s| s.space.clone()),
|
||||||
kind: match token_mint {
|
kind: match token_mint {
|
||||||
None => PdaKind::Program,
|
None => PdaKind::Program { owner: pda_owner },
|
||||||
Some(tm) => PdaKind::Token {
|
Some(tm) => PdaKind::Token {
|
||||||
mint: tm.into_inner().mint,
|
mint: tm.into_inner().mint,
|
||||||
owner: match token_authority {
|
owner: match token_authority {
|
||||||
|
@ -394,6 +407,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||||
ConstraintToken::Address(c) => self.add_address(c),
|
ConstraintToken::Address(c) => self.add_address(c),
|
||||||
ConstraintToken::TokenAuthority(c) => self.add_token_authority(c),
|
ConstraintToken::TokenAuthority(c) => self.add_token_authority(c),
|
||||||
ConstraintToken::TokenMint(c) => self.add_token_mint(c),
|
ConstraintToken::TokenMint(c) => self.add_token_mint(c),
|
||||||
|
ConstraintToken::Bump(c) => self.add_bump(c),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -449,6 +463,20 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_bump(&mut self, c: Context<ConstraintTokenBump>) -> ParseResult<()> {
|
||||||
|
if self.bump.is_some() {
|
||||||
|
return Err(ParseError::new(c.span(), "bump already provided"));
|
||||||
|
}
|
||||||
|
if self.seeds.is_none() {
|
||||||
|
return Err(ParseError::new(
|
||||||
|
c.span(),
|
||||||
|
"seeds must be provided before bump",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.bump.replace(c);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn add_token_authority(&mut self, c: Context<ConstraintTokenAuthority>) -> ParseResult<()> {
|
fn add_token_authority(&mut self, c: Context<ConstraintTokenAuthority>) -> ParseResult<()> {
|
||||||
if self.token_authority.is_some() {
|
if self.token_authority.is_some() {
|
||||||
return Err(ParseError::new(
|
return Err(ParseError::new(
|
||||||
|
|
Loading…
Reference in New Issue