examples: Permissioned markets (#483)
This commit is contained in:
parent
46cccd8946
commit
5018e98a9c
|
@ -10,3 +10,6 @@
|
|||
[submodule "examples/cfo/deps/stake"]
|
||||
path = examples/cfo/deps/stake
|
||||
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]
|
||||
|
||||
### 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
|
||||
|
||||
* 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)).
|
||||
|
||||
## [0.11.0] - 2021-07-03
|
||||
|
|
|
@ -17,4 +17,5 @@ members = [
|
|||
exclude = [
|
||||
"examples/swap/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(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(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("<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. |
|
||||
|
|
|
@ -233,10 +233,10 @@ impl Key for Pubkey {
|
|||
/// All programs should include it via `anchor_lang::prelude::*;`.
|
||||
pub mod prelude {
|
||||
pub use super::{
|
||||
access_control, account, associated, emit, error, event, interface, program, state,
|
||||
zero_copy, AccountDeserialize, AccountSerialize, Accounts, AccountsExit, AccountsInit,
|
||||
AnchorDeserialize, AnchorSerialize, Context, CpiAccount, CpiContext, CpiState,
|
||||
CpiStateContext, Loader, ProgramAccount, ProgramState, Sysvar, ToAccountInfo,
|
||||
access_control, account, associated, emit, error, event, interface, program, require,
|
||||
state, zero_copy, AccountDeserialize, AccountSerialize, Accounts, AccountsExit,
|
||||
AccountsInit, AnchorDeserialize, AnchorSerialize, Context, CpiAccount, CpiContext,
|
||||
CpiState, CpiStateContext, Loader, ProgramAccount, ProgramState, Sysvar, ToAccountInfo,
|
||||
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_with_nonce = {
|
||||
let s = &c.seeds;
|
||||
quote! {
|
||||
match c.bump.as_ref() {
|
||||
None => quote! {
|
||||
[#s]
|
||||
},
|
||||
Some(b) => quote! {
|
||||
[#s, &[#b]]
|
||||
},
|
||||
}
|
||||
};
|
||||
generate_pda(
|
||||
|
@ -285,10 +290,42 @@ fn generate_constraint_seeds_address(
|
|||
c: &ConstraintSeedsGroup,
|
||||
) -> proc_macro2::TokenStream {
|
||||
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! {
|
||||
let __program_signer = Pubkey::create_program_address(
|
||||
&[#seeds],
|
||||
&#seeds,
|
||||
program_id,
|
||||
).map_err(|_| anchor_lang::__private::ErrorCode::ConstraintSeeds)?;
|
||||
if #name.to_account_info().key != &__program_signer {
|
||||
|
@ -296,6 +333,7 @@ fn generate_constraint_seeds_address(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_constraint_associated(
|
||||
f: &Field,
|
||||
|
@ -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 {
|
||||
Ty::ProgramAccount(ty) => (
|
||||
&ty.account_type_path,
|
||||
Ty::ProgramAccount(ty) => {
|
||||
let ident = &ty.account_type_path;
|
||||
(
|
||||
quote! {
|
||||
#ident
|
||||
},
|
||||
quote! {
|
||||
anchor_lang::ProgramAccount
|
||||
},
|
||||
false,
|
||||
),
|
||||
Ty::Loader(ty) => (
|
||||
&ty.account_type_path,
|
||||
)
|
||||
}
|
||||
Ty::Loader(ty) => {
|
||||
let ident = &ty.account_type_path;
|
||||
(
|
||||
quote! {
|
||||
#ident
|
||||
},
|
||||
quote! {
|
||||
anchor_lang::Loader
|
||||
},
|
||||
true,
|
||||
),
|
||||
Ty::CpiAccount(ty) => (
|
||||
&ty.account_type_path,
|
||||
)
|
||||
}
|
||||
Ty::CpiAccount(ty) => {
|
||||
let ident = &ty.account_type_path;
|
||||
(
|
||||
quote! {
|
||||
#ident
|
||||
},
|
||||
quote! {
|
||||
anchor_lang::CpiAccount
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
Ty::AccountInfo => (
|
||||
quote! {
|
||||
AccountInfo
|
||||
},
|
||||
quote! {},
|
||||
false,
|
||||
),
|
||||
_ => 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 {
|
||||
PdaKind::Token { owner, mint } => quote! {
|
||||
let #field: #account_wrapper_ty<#account_ty> = {
|
||||
let #field: #combined_account_ty = {
|
||||
#space
|
||||
#payer
|
||||
#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! {
|
||||
let #field: #account_wrapper_ty<#account_ty> = {
|
||||
let #field = {
|
||||
#space
|
||||
#payer
|
||||
#seeds_constraint
|
||||
|
@ -513,10 +604,9 @@ pub fn generate_pda(
|
|||
#field.to_account_info().key,
|
||||
lamports,
|
||||
space as u64,
|
||||
program_id,
|
||||
#owner,
|
||||
);
|
||||
|
||||
|
||||
anchor_lang::solana_program::program::invoke_signed(
|
||||
&ix,
|
||||
&[
|
||||
|
@ -533,9 +623,7 @@ pub fn generate_pda(
|
|||
|
||||
// For now, we assume all accounts created with the `associated`
|
||||
// attribute have a `nonce` field in their account.
|
||||
let mut pa: #account_wrapper_ty<#account_ty> = #account_wrapper_ty::try_from_init(
|
||||
&#field.to_account_info(),
|
||||
)?;
|
||||
let mut pa: #combined_account_ty = #try_from;
|
||||
|
||||
#nonce_assignment
|
||||
pa
|
||||
|
|
|
@ -135,7 +135,21 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
|
|||
/// With this 8 byte identifier, Anchor performs method dispatch,
|
||||
/// matching the given 8 byte identifier to the associated method
|
||||
/// 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
|
||||
// instruction, injected into all Anchor programs.
|
||||
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 fn_name = &method.sig.ident;
|
||||
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
|
||||
/// program, where execution begins.
|
||||
#[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")]
|
||||
{
|
||||
msg!("anchor-debug is active");
|
||||
}
|
||||
if ix_data.len() < 8 {
|
||||
if data.len() < 8 {
|
||||
return #fallback_maybe
|
||||
}
|
||||
|
||||
// Split the instruction data into the first 8 byte method
|
||||
// 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)
|
||||
dispatch(program_id, accounts, data)
|
||||
.map_err(|e| {
|
||||
anchor_lang::solana_program::msg!(&e.to_string());
|
||||
e
|
||||
|
|
|
@ -341,6 +341,7 @@ pub enum ConstraintToken {
|
|||
Address(Context<ConstraintAddress>),
|
||||
TokenMint(Context<ConstraintTokenMint>),
|
||||
TokenAuthority(Context<ConstraintTokenAuthority>),
|
||||
Bump(Context<ConstraintTokenBump>),
|
||||
}
|
||||
|
||||
impl Parse for ConstraintToken {
|
||||
|
@ -396,6 +397,7 @@ pub struct ConstraintSeedsGroup {
|
|||
pub payer: Option<Ident>,
|
||||
pub space: Option<Expr>,
|
||||
pub kind: PdaKind,
|
||||
pub bump: Option<Expr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -444,7 +446,7 @@ pub struct ConstraintAssociatedSpace {
|
|||
#[derive(Debug, Clone)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum PdaKind {
|
||||
Program,
|
||||
Program { owner: Option<Expr> },
|
||||
Token { owner: Expr, mint: Expr },
|
||||
}
|
||||
|
||||
|
@ -463,6 +465,11 @@ pub struct ConstraintTokenAuthority {
|
|||
auth: Expr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConstraintTokenBump {
|
||||
bump: Expr,
|
||||
}
|
||||
|
||||
// Syntaxt context object for preserving metadata about the inner item.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context<T> {
|
||||
|
|
|
@ -1,11 +1,4 @@
|
|||
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 crate::*;
|
||||
use syn::ext::IdentExt;
|
||||
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
|
||||
use syn::punctuated::Punctuated;
|
||||
|
@ -183,6 +176,12 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
|
|||
auth: stream.parse()?,
|
||||
},
|
||||
)),
|
||||
"bump" => ConstraintToken::Bump(Context::new(
|
||||
ident.span(),
|
||||
ConstraintTokenBump {
|
||||
bump: stream.parse()?,
|
||||
},
|
||||
)),
|
||||
_ => return Err(ParseError::new(ident.span(), "Invalid attribute")),
|
||||
}
|
||||
}
|
||||
|
@ -213,6 +212,7 @@ pub struct ConstraintGroupBuilder<'ty> {
|
|||
pub address: Option<Context<ConstraintAddress>>,
|
||||
pub token_mint: Option<Context<ConstraintTokenMint>>,
|
||||
pub token_authority: Option<Context<ConstraintTokenAuthority>>,
|
||||
pub bump: Option<Context<ConstraintTokenBump>>,
|
||||
}
|
||||
|
||||
impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||
|
@ -238,6 +238,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
|||
address: None,
|
||||
token_mint: None,
|
||||
token_authority: None,
|
||||
bump: None,
|
||||
}
|
||||
}
|
||||
pub fn build(mut self) -> ParseResult<ConstraintGroup> {
|
||||
|
@ -298,6 +299,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
|||
address,
|
||||
token_mint,
|
||||
token_authority,
|
||||
bump,
|
||||
} = self;
|
||||
|
||||
// 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();
|
||||
Ok(ConstraintGroup {
|
||||
init: into_inner!(init),
|
||||
|
@ -334,7 +344,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
|||
payer: into_inner!(associated_payer.clone()).map(|a| a.target),
|
||||
space: associated_space.clone().map(|s| s.space.clone()),
|
||||
kind: match &token_mint {
|
||||
None => PdaKind::Program,
|
||||
None => PdaKind::Program {
|
||||
owner: pda_owner.clone(),
|
||||
},
|
||||
Some(tm) => PdaKind::Token {
|
||||
mint: tm.clone().into_inner().mint,
|
||||
owner: match &token_authority {
|
||||
|
@ -346,6 +358,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
|||
},
|
||||
},
|
||||
},
|
||||
bump: into_inner!(bump).map(|b| b.bump),
|
||||
})
|
||||
})
|
||||
.transpose()?,
|
||||
|
@ -358,7 +371,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
|||
payer: associated_payer.map(|p| p.target.clone()),
|
||||
space: associated_space.map(|s| s.space.clone()),
|
||||
kind: match token_mint {
|
||||
None => PdaKind::Program,
|
||||
None => PdaKind::Program { owner: pda_owner },
|
||||
Some(tm) => PdaKind::Token {
|
||||
mint: tm.into_inner().mint,
|
||||
owner: match token_authority {
|
||||
|
@ -394,6 +407,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
|||
ConstraintToken::Address(c) => self.add_address(c),
|
||||
ConstraintToken::TokenAuthority(c) => self.add_token_authority(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(())
|
||||
}
|
||||
|
||||
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<()> {
|
||||
if self.token_authority.is_some() {
|
||||
return Err(ParseError::new(
|
||||
|
|
Loading…
Reference in New Issue