Token PDAs and CLI scripts (#400)

This commit is contained in:
Armani Ferrante 2021-06-27 13:17:05 -07:00 committed by GitHub
parent 609c836d63
commit f067624add
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 2868 additions and 181 deletions

9
.gitmodules vendored
View File

@ -1,3 +1,12 @@
[submodule "examples/swap/deps/serum-dex"]
path = examples/swap/deps/serum-dex
url = https://github.com/project-serum/serum-dex
[submodule "examples/cfo/deps/serum-dex"]
path = examples/cfo/deps/serum-dex
url = https://github.com/project-serum/serum-dex
[submodule "examples/cfo/deps/swap"]
path = examples/cfo/deps/swap
url = https://github.com/project-serum/swap.git
[submodule "examples/cfo/deps/stake"]
path = examples/cfo/deps/stake
url = https://github.com/project-serum/stake.git

View File

@ -64,6 +64,7 @@ jobs:
- pushd examples/chat && yarn && anchor test && popd
- pushd examples/ido-pool && yarn && anchor test && popd
- pushd examples/swap/deps/serum-dex/dex && cargo build-bpf && cd ../../../ && anchor test && popd
- pushd examples/cfo && anchor run test && popd
- <<: *examples
name: Runs the examples 3
script:

View File

@ -11,6 +11,14 @@ incremented for features.
## [Unreleased]
### Features
* lang: Add `#[account(address = <expr>)]` constraint for asserting the address of an account ([#400](https://github.com/project-serum/anchor/pull/400)).
* lang: Add `#[account(init, token = <mint-target>, authority = <token-owner-target>...)]` constraint for initializing SPL token accounts as program derived addresses for the program. Can be used when initialized via `seeds` or `associated` ([#400](https://github.com/project-serum/anchor/pull/400)).
* lang: Add `associated_seeds!` macro for generating signer seeds for CPIs signed by an `#[account(associated = <target>)]` account ([#400](https://github.com/project-serum/anchor/pull/400)).
* cli: Add `[scripts]` section to the Anchor.toml for specifying workspace scripts that can be run via `anchor run <script>` ([#400](https://github.com/project-serum/anchor/pull/400)).
* cli: `[clusters.<network>]` table entries can now also use `{ address = <base58-str>, idl = <filepath-str> }` to specify workspace programs ([#400](https://github.com/project-serum/anchor/pull/400)).
## [0.9.0] - 2021-06-15
### Features

20
Cargo.lock generated
View File

@ -214,6 +214,7 @@ dependencies = [
"bs58",
"heck",
"proc-macro2 1.0.24",
"proc-macro2-diagnostics",
"quote 1.0.9",
"serde",
"serde_json",
@ -2270,6 +2271,19 @@ dependencies = [
"unicode-xid 0.2.1",
]
[[package]]
name = "proc-macro2-diagnostics"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.9",
"syn 1.0.67",
"version_check",
"yansi",
]
[[package]]
name = "qstring"
version = "0.7.2"
@ -4312,6 +4326,12 @@ dependencies = [
"linked-hash-map",
]
[[package]]
name = "yansi"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71"
[[package]]
name = "zeroize"
version = "1.2.0"

View File

@ -15,5 +15,6 @@ members = [
"spl",
]
exclude = [
"examples/swap/deps/serum-dex"
"examples/swap/deps/serum-dex",
"examples/cfo/deps/serum-dex",
]

View File

@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Keypair;
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::fs::{self, File};
use std::io::prelude::*;
use std::path::Path;
@ -16,6 +17,7 @@ use std::str::FromStr;
pub struct Config {
pub provider: ProviderConfig,
pub clusters: ClustersConfig,
pub scripts: ScriptsConfig,
pub test: Option<Test>,
}
@ -25,6 +27,8 @@ pub struct ProviderConfig {
pub wallet: WalletPath,
}
pub type ScriptsConfig = BTreeMap<String, String>;
pub type ClustersConfig = BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>;
impl Config {
@ -100,7 +104,8 @@ impl Config {
struct _Config {
provider: Provider,
test: Option<Test>,
clusters: Option<BTreeMap<String, BTreeMap<String, String>>>,
scripts: Option<ScriptsConfig>,
clusters: Option<BTreeMap<String, BTreeMap<String, serde_json::Value>>>,
}
#[derive(Debug, Serialize, Deserialize)]
@ -125,6 +130,10 @@ impl ToString for Config {
wallet: self.provider.wallet.to_string(),
},
test: self.test.clone(),
scripts: match self.scripts.is_empty() {
true => None,
false => Some(self.scripts.clone()),
},
clusters,
};
@ -143,6 +152,7 @@ impl FromStr for Config {
cluster: cfg.provider.cluster.parse()?,
wallet: shellexpand::tilde(&cfg.provider.wallet).parse()?,
},
scripts: cfg.scripts.unwrap_or_else(|| BTreeMap::new()),
test: cfg.test,
clusters: cfg
.clusters
@ -153,22 +163,27 @@ impl FromStr for Config {
fn ser_clusters(
clusters: &BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>,
) -> BTreeMap<String, BTreeMap<String, String>> {
) -> BTreeMap<String, BTreeMap<String, serde_json::Value>> {
clusters
.iter()
.map(|(cluster, programs)| {
let cluster = cluster.to_string();
let programs = programs
.iter()
.map(|(name, deployment)| (name.clone(), deployment.program_id.to_string()))
.collect::<BTreeMap<String, String>>();
.map(|(name, deployment)| {
(
name.clone(),
serde_json::to_value(&_ProgramDeployment::from(deployment)).unwrap(),
)
})
.collect::<BTreeMap<String, serde_json::Value>>();
(cluster, programs)
})
.collect::<BTreeMap<String, BTreeMap<String, String>>>()
.collect::<BTreeMap<String, BTreeMap<String, serde_json::Value>>>()
}
fn deser_clusters(
clusters: BTreeMap<String, BTreeMap<String, String>>,
clusters: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
) -> Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>> {
clusters
.iter()
@ -179,10 +194,17 @@ fn deser_clusters(
.map(|(name, program_id)| {
Ok((
name.clone(),
ProgramDeployment {
name: name.clone(),
program_id: program_id.parse()?,
},
ProgramDeployment::try_from(match &program_id {
serde_json::Value::String(address) => _ProgramDeployment {
address: address.parse()?,
idl: None,
},
serde_json::Value::Object(_) => {
serde_json::from_value(program_id.clone())
.map_err(|_| anyhow!("Unable to read toml"))?
}
_ => return Err(anyhow!("Invalid toml type")),
})?,
))
})
.collect::<Result<BTreeMap<String, ProgramDeployment>>>()?;
@ -269,8 +291,33 @@ impl Program {
#[derive(Debug, Default)]
pub struct ProgramDeployment {
pub name: String,
pub program_id: Pubkey,
pub address: Pubkey,
pub idl: Option<String>,
}
impl TryFrom<_ProgramDeployment> for ProgramDeployment {
type Error = anyhow::Error;
fn try_from(pd: _ProgramDeployment) -> Result<Self, Self::Error> {
Ok(ProgramDeployment {
address: pd.address.parse()?,
idl: pd.idl,
})
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct _ProgramDeployment {
pub address: String,
pub idl: Option<String>,
}
impl From<&ProgramDeployment> for _ProgramDeployment {
fn from(pd: &ProgramDeployment) -> Self {
Self {
address: pd.address.to_string(),
idl: pd.idl.clone(),
}
}
}
pub struct ProgramWorkspace {

View File

@ -147,6 +147,11 @@ pub enum Command {
/// Starts a node shell with an Anchor client setup according to the local
/// config.
Shell,
/// Runs the script defined by the current workspace's Anchor.toml.
Run {
/// The name of the script to run.
script: String,
},
}
#[derive(Debug, Clap)]
@ -267,6 +272,7 @@ fn main() -> Result<()> {
Command::Airdrop => airdrop(cfg_override),
Command::Cluster { subcmd } => cluster(subcmd),
Command::Shell => shell(&opts.cfg_override),
Command::Run { script } => run(&opts.cfg_override, script),
}
}
@ -1613,17 +1619,31 @@ fn cluster(_cmd: ClusterCommand) -> Result<()> {
fn shell(cfg_override: &ConfigOverride) -> Result<()> {
with_workspace(cfg_override, |cfg, _path, _cargo| {
let programs = {
let idls: HashMap<String, Idl> = read_all_programs()?
let mut idls: HashMap<String, Idl> = read_all_programs()?
.iter()
.map(|program| (program.idl.name.clone(), program.idl.clone()))
.collect();
// Insert all manually specified idls into the idl map.
cfg.clusters.get(&cfg.provider.cluster).map(|programs| {
let _ = programs
.iter()
.map(|(name, pd)| {
if let Some(idl_fp) = &pd.idl {
let file_str =
std::fs::read_to_string(idl_fp).expect("Unable to read IDL file");
let idl = serde_json::from_str(&file_str).expect("Idl not readable");
idls.insert(name.clone(), idl);
}
})
.collect::<Vec<_>>();
});
match cfg.clusters.get(&cfg.provider.cluster) {
None => Vec::new(),
Some(programs) => programs
.iter()
.map(|(name, program_deployment)| ProgramWorkspace {
name: name.to_string(),
program_id: program_deployment.program_id,
program_id: program_deployment.address,
idl: match idls.get(name) {
None => {
println!("Unable to find IDL for {}", name);
@ -1655,6 +1675,26 @@ fn shell(cfg_override: &ConfigOverride) -> Result<()> {
})
}
fn run(cfg_override: &ConfigOverride, script: String) -> Result<()> {
with_workspace(cfg_override, |cfg, _path, _cargo| {
let script = cfg
.scripts
.get(&script)
.ok_or(anyhow!("Unable to find script"))?;
let exit = std::process::Command::new("bash")
.arg("-c")
.arg(&script)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.unwrap();
if !exit.status.success() {
std::process::exit(exit.status.code().unwrap_or(1));
}
Ok(())
})
}
// with_workspace ensures the current working directory is always the top level
// workspace directory, i.e., where the `Anchor.toml` file is located, before
// and after the closure invocation.

41
examples/cfo/Anchor.toml Normal file
View File

@ -0,0 +1,41 @@
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[clusters.localnet]
registry = { address = "GrAkKfEpTKQuVHG2Y97Y2FF4i7y7Q5AHLK94JBy7Y5yv", idl = "./deps/stake/target/idl/registry.json" }
lockup = { address = "6ebQNeTPZ1j7k3TtkCCtEPRvG7GQsucQrZ7sSEDQi9Ks", idl = "./deps/stake/target/idl/lockup.json" }
[scripts]
#
# Testing.
#
test = "anchor run build && anchor test --skip-build"
#
# Build the program and all CPI dependencies.
#
build = "anchor run build-deps && anchor build"
build-deps = "anchor run build-dex && anchor run build-swap && anchor run build-stake"
build-dex = "pushd deps/serum-dex/dex/ && cargo build-bpf && popd"
build-swap = "cd deps/swap && pwd && anchor build && cd ../../"
build-stake = "pushd deps/stake && anchor build && popd"
#
# Runs a localnet with all the programs deployed.
#
localnet = "./scripts/localnet.sh"
[[test.genesis]]
address = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
program = "./deps/serum-dex/dex/target/deploy/serum_dex.so"
[[test.genesis]]
address = "22Y43yTVxuUkoRKdm9thyRhQ3SdgQS7c7kB6UNCiaczD"
program = "./deps/swap/target/deploy/swap.so"
[[test.genesis]]
address = "GrAkKfEpTKQuVHG2Y97Y2FF4i7y7Q5AHLK94JBy7Y5yv"
program = "./deps/stake/target/deploy/registry.so"
[[test.genesis]]
address = "6ebQNeTPZ1j7k3TtkCCtEPRvG7GQsucQrZ7sSEDQi9Ks"
program = "./deps/stake/target/deploy/lockup.so"

9
examples/cfo/Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[workspace]
members = [
"programs/*"
]
exclude = [
"deps/serum-dex",
"deps/stake",
"deps/swap"
]

@ -0,0 +1 @@
Subproject commit ed9d54a717bec01de2924f6e6ca465f942b072aa

@ -0,0 +1 @@
Subproject commit a6c389d6ece753d83bff1cff38d315775fefb467

@ -0,0 +1 @@
Subproject commit 0382f2e27db5f95d09aec5e6df7bb01bfc8f0e7f

View File

@ -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.
}

View File

@ -0,0 +1,24 @@
[package]
name = "cfo"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"
[lib]
crate-type = ["cdylib", "lib"]
name = "cfo"
[features]
no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = ["test"]
test = []
[dependencies]
anchor-lang = { path = "../../../../lang" }
anchor-spl = { path = "../../../../spl" }
spl-token = { version ="3.1.1", features = ["no-entrypoint"] }
swap = { path = "../../deps/swap/programs/swap", features = ["cpi"] }
registry = { path = "../../deps/stake/programs/registry", features = ["cpi"] }
lockup = { path = "../../deps/stake/programs/lockup", features = ["cpi"] }

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,818 @@
// WIP. This program has been checkpointed and is not production ready.
use anchor_lang::associated_seeds;
use anchor_lang::prelude::*;
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
use anchor_lang::solana_program::{system_instruction, system_program};
use anchor_spl::token::{self, Mint, TokenAccount};
use anchor_spl::{dex, mint};
use registry::{Registrar, RewardVendorKind};
use std::convert::TryInto;
/// CFO is the program representing the Serum chief financial officer. It is
/// the program responsible for collecting and distributing fees from the Serum
/// DEX.
#[program]
pub mod cfo {
use super::*;
/// Creates a financial officer account associated with a DEX program ID.
#[access_control(is_distribution_valid(&d))]
pub fn create_officer(
ctx: Context<CreateOfficer>,
d: Distribution,
registrar: Pubkey,
msrm_registrar: Pubkey,
) -> Result<()> {
let officer = &mut ctx.accounts.officer;
officer.authority = *ctx.accounts.authority.key;
officer.swap_program = *ctx.accounts.swap_program.key;
officer.dex_program = *ctx.accounts.dex_program.key;
officer.distribution = d;
officer.registrar = registrar;
officer.msrm_registrar = msrm_registrar;
officer.stake = *ctx.accounts.stake.to_account_info().key;
officer.treasury = *ctx.accounts.treasury.to_account_info().key;
officer.srm_vault = *ctx.accounts.srm_vault.to_account_info().key;
emit!(OfficerDidCreate {
pubkey: *officer.to_account_info().key,
});
Ok(())
}
/// Creates a deterministic token account owned by the CFO.
/// This should be used when a new mint is used for collecting fees.
/// Can only be called once per token CFO and token mint.
pub fn create_officer_token(_ctx: Context<CreateOfficerToken>) -> Result<()> {
Ok(())
}
/// Updates the cfo's fee distribution.
#[access_control(is_distribution_valid(&d))]
pub fn set_distribution(ctx: Context<SetDistribution>, d: Distribution) -> Result<()> {
ctx.accounts.officer.distribution = d.clone();
emit!(DistributionDidChange { distribution: d });
Ok(())
}
/// Transfers fees from the dex to the CFO.
pub fn sweep_fees<'info>(ctx: Context<'_, '_, '_, 'info, SweepFees<'info>>) -> Result<()> {
let seeds = associated_seeds! {
account = ctx.accounts.officer,
associated = ctx.accounts.dex.dex_program
};
let cpi_ctx: CpiContext<'_, '_, '_, 'info, dex::SweepFees<'info>> = (&*ctx.accounts).into();
dex::sweep_fees(cpi_ctx.with_signer(&[seeds]))?;
Ok(())
}
/// Convert the CFO's entire non-SRM token balance into USDC.
/// Assumes USDC is the quote currency.
#[access_control(is_not_trading(&ctx.accounts.instructions))]
pub fn swap_to_usdc<'info>(
ctx: Context<'_, '_, '_, 'info, SwapToUsdc<'info>>,
min_exchange_rate: ExchangeRate,
) -> Result<()> {
let seeds = associated_seeds! {
account = ctx.accounts.officer,
associated = ctx.accounts.dex_program
};
let cpi_ctx: CpiContext<'_, '_, '_, 'info, swap::Swap<'info>> = (&*ctx.accounts).into();
swap::cpi::swap(
cpi_ctx.with_signer(&[seeds]),
swap::Side::Bid,
token::accessor::amount(&ctx.accounts.from_vault)?,
min_exchange_rate.into(),
)?;
Ok(())
}
/// Convert the CFO's entire token balance into SRM.
/// Assumes SRM is the base currency.
#[access_control(is_not_trading(&ctx.accounts.instructions))]
pub fn swap_to_srm<'info>(
ctx: Context<'_, '_, '_, 'info, SwapToSrm<'info>>,
min_exchange_rate: ExchangeRate,
) -> Result<()> {
let seeds = associated_seeds! {
account = ctx.accounts.officer,
associated = ctx.accounts.dex_program
};
let cpi_ctx: CpiContext<'_, '_, '_, 'info, swap::Swap<'info>> = (&*ctx.accounts).into();
swap::cpi::swap(
cpi_ctx.with_signer(&[seeds]),
swap::Side::Bid,
token::accessor::amount(&ctx.accounts.from_vault)?,
min_exchange_rate.into(),
)?;
Ok(())
}
/// Distributes srm tokens to the various categories. Before calling this,
/// one must convert the fees into SRM via the swap APIs.
#[access_control(is_distribution_ready(&ctx.accounts))]
pub fn distribute<'info>(ctx: Context<'_, '_, '_, 'info, Distribute<'info>>) -> Result<()> {
let total_fees = ctx.accounts.srm_vault.amount;
let seeds = associated_seeds! {
account = ctx.accounts.officer,
associated = ctx.accounts.dex_program
};
// Burn.
let burn_amount: u64 = u128::from(total_fees)
.checked_mul(ctx.accounts.officer.distribution.burn.into())
.unwrap()
.checked_div(100)
.unwrap()
.try_into()
.map_err(|_| ErrorCode::U128CannotConvert)?;
token::burn(ctx.accounts.into_burn().with_signer(&[seeds]), burn_amount)?;
// Stake.
let stake_amount: u64 = u128::from(total_fees)
.checked_mul(ctx.accounts.officer.distribution.stake.into())
.unwrap()
.checked_div(100)
.unwrap()
.try_into()
.map_err(|_| ErrorCode::U128CannotConvert)?;
token::transfer(
ctx.accounts.into_stake_transfer().with_signer(&[seeds]),
stake_amount,
)?;
// Treasury.
let treasury_amount: u64 = u128::from(total_fees)
.checked_mul(ctx.accounts.officer.distribution.treasury.into())
.unwrap()
.checked_div(100)
.unwrap()
.try_into()
.map_err(|_| ErrorCode::U128CannotConvert)?;
token::transfer(
ctx.accounts.into_treasury_transfer().with_signer(&[seeds]),
treasury_amount,
)?;
Ok(())
}
#[access_control(is_stake_reward_ready(&ctx.accounts))]
pub fn drop_stake_reward<'info>(
ctx: Context<'_, '_, '_, 'info, DropStakeReward<'info>>,
) -> Result<()> {
// Common reward parameters.
let expiry_ts = 1853942400; // 9/30/2028.
let expiry_receiver = *ctx.accounts.officer.to_account_info().key;
let locked_kind = {
let start_ts = 1633017600; // 9/30/2021.
let end_ts = 1822320000; // 9/30/2027.
let period_count = 2191;
RewardVendorKind::Locked {
start_ts,
end_ts,
period_count,
}
};
let seeds = associated_seeds! {
account = ctx.accounts.officer,
associated = ctx.accounts.dex_program
};
// Total amount staked denominated in SRM (i.e. MSRM is converted to
// SRM)
let total_pool_value = u128::from(ctx.accounts.srm.pool_mint.supply)
.checked_mul(500)
.unwrap()
.checked_add(
u128::from(ctx.accounts.msrm.pool_mint.supply)
.checked_mul(1_000_000)
.unwrap(),
)
.unwrap();
// Total reward split between both the SRM and MSRM stake pools.
let total_reward_amount = u128::from(ctx.accounts.stake.amount);
// Proportion of the reward going to the srm pool.
//
// total_reward_amount * (srm_pool_value / total_pool_value)
//
let srm_amount: u64 = u128::from(ctx.accounts.srm.pool_mint.supply)
.checked_mul(500)
.unwrap()
.checked_mul(total_reward_amount)
.unwrap()
.checked_div(total_pool_value)
.unwrap()
.try_into()
.map_err(|_| ErrorCode::U128CannotConvert)?;
// Proportion of the reward going to the msrm pool.
//
// total_reward_amount * (msrm_pool_value / total_pool_value)
//
let msrm_amount = u128::from(ctx.accounts.msrm.pool_mint.supply)
.checked_mul(total_reward_amount)
.unwrap()
.checked_div(total_pool_value)
.unwrap()
.try_into()
.map_err(|_| ErrorCode::U128CannotConvert)?;
// SRM drop.
{
// Drop locked reward.
let (_, nonce) = Pubkey::find_program_address(
&[
ctx.accounts.srm.registrar.to_account_info().key.as_ref(),
ctx.accounts.srm.vendor.to_account_info().key.as_ref(),
],
ctx.accounts.token_program.key,
);
registry::cpi::drop_reward(
ctx.accounts.into_srm_reward().with_signer(&[seeds]),
locked_kind.clone(),
srm_amount.try_into().unwrap(),
expiry_ts,
expiry_receiver,
nonce,
)?;
// Drop unlocked reward.
registry::cpi::drop_reward(
ctx.accounts.into_srm_reward().with_signer(&[seeds]),
RewardVendorKind::Unlocked,
srm_amount,
expiry_ts,
expiry_receiver,
nonce,
)?;
}
// MSRM drop.
{
// Drop locked reward.
let (_, nonce) = Pubkey::find_program_address(
&[
ctx.accounts.msrm.registrar.to_account_info().key.as_ref(),
ctx.accounts.msrm.vendor.to_account_info().key.as_ref(),
],
ctx.accounts.token_program.key,
);
registry::cpi::drop_reward(
ctx.accounts.into_msrm_reward().with_signer(&[seeds]),
locked_kind,
msrm_amount,
expiry_ts,
expiry_receiver,
nonce,
)?;
// Drop unlocked reward.
registry::cpi::drop_reward(
ctx.accounts.into_msrm_reward().with_signer(&[seeds]),
RewardVendorKind::Unlocked,
msrm_amount,
expiry_ts,
expiry_receiver,
nonce,
)?;
}
Ok(())
}
}
// Context accounts.
#[derive(Accounts)]
pub struct CreateOfficer<'info> {
#[account(init, associated = dex_program, payer = authority)]
officer: ProgramAccount<'info, Officer>,
#[account(
init,
token = mint,
associated = officer, with = b"vault",
space = TokenAccount::LEN,
payer = authority,
)]
srm_vault: CpiAccount<'info, TokenAccount>,
#[account(
init,
token = mint,
associated = officer, with = b"stake",
space = TokenAccount::LEN,
payer = authority,
)]
stake: CpiAccount<'info, TokenAccount>,
#[account(
init,
token = mint,
associated = officer, with = b"treasury",
space = TokenAccount::LEN,
payer = authority,
)]
treasury: CpiAccount<'info, TokenAccount>,
#[account(signer)]
authority: AccountInfo<'info>,
#[cfg_attr(
not(feature = "test"),
account(address = mint::SRM),
)]
mint: AccountInfo<'info>,
#[account(executable)]
dex_program: AccountInfo<'info>,
#[account(executable)]
swap_program: AccountInfo<'info>,
#[account(address = system_program::ID)]
system_program: AccountInfo<'info>,
#[account(address = spl_token::ID)]
token_program: AccountInfo<'info>,
rent: Sysvar<'info, Rent>,
}
#[derive(Accounts)]
pub struct CreateOfficerToken<'info> {
officer: ProgramAccount<'info, Officer>,
#[account(
init,
token = mint,
associated = officer, with = mint,
space = TokenAccount::LEN,
payer = payer,
)]
token: CpiAccount<'info, TokenAccount>,
#[account(owner = token_program)]
mint: AccountInfo<'info>,
#[account(mut, signer)]
payer: AccountInfo<'info>,
#[account(address = system_program::ID)]
system_program: AccountInfo<'info>,
#[account(address = spl_token::ID)]
token_program: AccountInfo<'info>,
rent: Sysvar<'info, Rent>,
}
#[derive(Accounts)]
pub struct SetDistribution<'info> {
#[account(has_one = authority)]
officer: ProgramAccount<'info, Officer>,
#[account(signer)]
authority: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct SweepFees<'info> {
#[account(associated = dex.dex_program)]
officer: ProgramAccount<'info, Officer>,
#[account(
mut,
owner = dex.token_program,
associated = officer, with = mint,
)]
sweep_vault: AccountInfo<'info>,
mint: AccountInfo<'info>,
dex: Dex<'info>,
}
#[derive(Accounts)]
pub struct Dex<'info> {
#[account(mut)]
market: AccountInfo<'info>,
#[account(mut)]
pc_vault: AccountInfo<'info>,
sweep_authority: AccountInfo<'info>,
vault_signer: AccountInfo<'info>,
dex_program: AccountInfo<'info>,
#[account(address = spl_token::ID)]
token_program: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct SwapToUsdc<'info> {
#[account(associated = dex_program)]
officer: ProgramAccount<'info, Officer>,
market: DexMarketAccounts<'info>,
#[account(
owner = token_program,
constraint = &officer.treasury != from_vault.key,
constraint = &officer.stake != from_vault.key,
)]
from_vault: AccountInfo<'info>,
#[account(owner = token_program)]
quote_vault: AccountInfo<'info>,
#[account(associated = officer, with = mint::USDC)]
usdc_vault: AccountInfo<'info>,
#[account(address = swap::ID)]
swap_program: AccountInfo<'info>,
#[account(address = dex::ID)]
dex_program: AccountInfo<'info>,
#[account(address = token::ID)]
token_program: AccountInfo<'info>,
rent: Sysvar<'info, Rent>,
#[account(address = tx_instructions::ID)]
instructions: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct SwapToSrm<'info> {
#[account(associated = dex_program)]
officer: ProgramAccount<'info, Officer>,
market: DexMarketAccounts<'info>,
#[account(
owner = token_program,
constraint = &officer.treasury != from_vault.key,
constraint = &officer.stake != from_vault.key,
)]
from_vault: AccountInfo<'info>,
#[account(owner = token_program)]
quote_vault: AccountInfo<'info>,
#[account(
associated = officer,
with = mint::SRM,
constraint = &officer.treasury != from_vault.key,
constraint = &officer.stake != from_vault.key,
)]
srm_vault: AccountInfo<'info>,
#[account(address = swap::ID)]
swap_program: AccountInfo<'info>,
#[account(address = dex::ID)]
dex_program: AccountInfo<'info>,
#[account(address = token::ID)]
token_program: AccountInfo<'info>,
rent: Sysvar<'info, Rent>,
#[account(address = tx_instructions::ID)]
instructions: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct DexMarketAccounts<'info> {
#[account(mut)]
market: AccountInfo<'info>,
#[account(mut)]
open_orders: AccountInfo<'info>,
#[account(mut)]
request_queue: AccountInfo<'info>,
#[account(mut)]
event_queue: AccountInfo<'info>,
#[account(mut)]
bids: AccountInfo<'info>,
#[account(mut)]
asks: AccountInfo<'info>,
// The `spl_token::Account` that funds will be taken from, i.e., transferred
// from the user into the market's vault.
//
// For bids, this is the base currency. For asks, the quote.
#[account(mut)]
order_payer_token_account: AccountInfo<'info>,
// Also known as the "base" currency. For a given A/B market,
// this is the vault for the A mint.
#[account(mut)]
coin_vault: AccountInfo<'info>,
// Also known as the "quote" currency. For a given A/B market,
// this is the vault for the B mint.
#[account(mut)]
pc_vault: AccountInfo<'info>,
// PDA owner of the DEX's token accounts for base + quote currencies.
vault_signer: AccountInfo<'info>,
// User wallets.
#[account(mut)]
coin_wallet: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct Distribute<'info> {
#[account(has_one = treasury, has_one = stake)]
officer: ProgramAccount<'info, Officer>,
treasury: AccountInfo<'info>,
stake: AccountInfo<'info>,
#[account(
owner = token_program,
constraint = srm_vault.mint == mint::SRM,
)]
srm_vault: CpiAccount<'info, TokenAccount>,
#[account(address = mint::SRM)]
mint: AccountInfo<'info>,
#[account(address = spl_token::ID)]
token_program: AccountInfo<'info>,
#[account(address = dex::ID)]
dex_program: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct DropStakeReward<'info> {
#[account(
has_one = stake,
constraint = srm.registrar.key == &officer.registrar,
constraint = msrm.registrar.key == &officer.msrm_registrar,
)]
officer: ProgramAccount<'info, Officer>,
#[account(associated = officer, with = b"stake", with = mint)]
stake: CpiAccount<'info, TokenAccount>,
#[cfg_attr(
not(feature = "test"),
account(address = mint::SRM),
)]
mint: AccountInfo<'info>,
srm: DropStakeRewardPool<'info>,
msrm: DropStakeRewardPool<'info>,
#[account(owner = registry_program)]
msrm_registrar: CpiAccount<'info, Registrar>,
#[account(address = token::ID)]
token_program: AccountInfo<'info>,
#[account(address = registry::ID)]
registry_program: AccountInfo<'info>,
#[account(address = lockup::ID)]
lockup_program: AccountInfo<'info>,
#[account(address = dex::ID)]
dex_program: AccountInfo<'info>,
clock: Sysvar<'info, Clock>,
rent: Sysvar<'info, Rent>,
}
// Don't bother doing validation on the individual accounts. Allow the stake
// program to handle it.
#[derive(Accounts)]
pub struct DropStakeRewardPool<'info> {
registrar: AccountInfo<'info>,
reward_event_q: AccountInfo<'info>,
pool_mint: CpiAccount<'info, Mint>,
vendor: AccountInfo<'info>,
vendor_vault: AccountInfo<'info>,
}
// Accounts.
#[associated]
#[derive(Default)]
pub struct Officer {
// Priviledged account.
pub authority: Pubkey,
// Vault holding the officer's SRM tokens prior to distribution.
pub srm_vault: Pubkey,
// Escrow SRM vault holding tokens which are dropped onto stakers.
pub stake: Pubkey,
// SRM token account to send treasury earned tokens to.
pub treasury: Pubkey,
// Defines the fee distribution, i.e., what percent each fee category gets.
pub distribution: Distribution,
// Swap frontend for the dex.
pub swap_program: Pubkey,
// Dex program the officer is associated with.
pub dex_program: Pubkey,
// SRM stake pool address
pub registrar: Pubkey,
// MSRM stake pool address.
pub msrm_registrar: Pubkey,
}
#[derive(AnchorSerialize, AnchorDeserialize, Default, Clone)]
pub struct Distribution {
burn: u8,
stake: u8,
treasury: u8,
}
// CpiContext transformations.
impl<'info> From<&SweepFees<'info>> for CpiContext<'_, '_, '_, 'info, dex::SweepFees<'info>> {
fn from(sweep: &SweepFees<'info>) -> Self {
let program = sweep.dex.dex_program.to_account_info();
let accounts = dex::SweepFees {
market: sweep.dex.market.to_account_info(),
pc_vault: sweep.dex.pc_vault.to_account_info(),
sweep_authority: sweep.dex.sweep_authority.to_account_info(),
sweep_receiver: sweep.sweep_vault.to_account_info(),
vault_signer: sweep.dex.vault_signer.to_account_info(),
token_program: sweep.dex.token_program.to_account_info(),
};
CpiContext::new(program, accounts)
}
}
impl<'info> From<&SwapToSrm<'info>> for CpiContext<'_, '_, '_, 'info, swap::Swap<'info>> {
fn from(accs: &SwapToSrm<'info>) -> Self {
let program = accs.swap_program.to_account_info();
let accounts = swap::Swap {
market: swap::MarketAccounts {
market: accs.market.market.clone(),
open_orders: accs.market.open_orders.clone(),
request_queue: accs.market.request_queue.clone(),
event_queue: accs.market.event_queue.clone(),
bids: accs.market.bids.clone(),
asks: accs.market.asks.clone(),
order_payer_token_account: accs.market.order_payer_token_account.clone(),
coin_vault: accs.market.coin_vault.clone(),
pc_vault: accs.market.pc_vault.clone(),
vault_signer: accs.market.vault_signer.clone(),
coin_wallet: accs.srm_vault.clone(),
},
authority: accs.officer.to_account_info(),
pc_wallet: accs.from_vault.to_account_info(),
dex_program: accs.dex_program.to_account_info(),
token_program: accs.token_program.to_account_info(),
rent: accs.rent.to_account_info(),
};
CpiContext::new(program, accounts)
}
}
impl<'info> From<&SwapToUsdc<'info>> for CpiContext<'_, '_, '_, 'info, swap::Swap<'info>> {
fn from(accs: &SwapToUsdc<'info>) -> Self {
let program = accs.swap_program.to_account_info();
let accounts = swap::Swap {
market: swap::MarketAccounts {
market: accs.market.market.clone(),
open_orders: accs.market.open_orders.clone(),
request_queue: accs.market.request_queue.clone(),
event_queue: accs.market.event_queue.clone(),
bids: accs.market.bids.clone(),
asks: accs.market.asks.clone(),
order_payer_token_account: accs.market.order_payer_token_account.clone(),
coin_vault: accs.market.coin_vault.clone(),
pc_vault: accs.market.pc_vault.clone(),
vault_signer: accs.market.vault_signer.clone(),
coin_wallet: accs.from_vault.to_account_info(),
},
authority: accs.officer.to_account_info(),
pc_wallet: accs.usdc_vault.clone(),
dex_program: accs.dex_program.to_account_info(),
token_program: accs.token_program.to_account_info(),
rent: accs.rent.to_account_info(),
};
CpiContext::new(program, accounts)
}
}
impl<'info> From<&Distribute<'info>> for CpiContext<'_, '_, '_, 'info, token::Burn<'info>> {
fn from(accs: &Distribute<'info>) -> Self {
let program = accs.token_program.to_account_info();
let accounts = token::Burn {
mint: accs.mint.to_account_info(),
to: accs.srm_vault.to_account_info(),
authority: accs.officer.to_account_info(),
};
CpiContext::new(program, accounts)
}
}
impl<'info> DropStakeReward<'info> {
fn into_srm_reward(&self) -> CpiContext<'_, '_, '_, 'info, registry::DropReward<'info>> {
let program = self.registry_program.clone();
let accounts = registry::DropReward {
registrar: ProgramAccount::try_from(&self.srm.registrar).unwrap(),
reward_event_q: ProgramAccount::try_from(&self.srm.reward_event_q).unwrap(),
pool_mint: self.srm.pool_mint.clone(),
vendor: ProgramAccount::try_from(&self.srm.vendor).unwrap(),
vendor_vault: CpiAccount::try_from(&self.srm.vendor_vault).unwrap(),
depositor: self.stake.to_account_info(),
depositor_authority: self.officer.to_account_info(),
token_program: self.token_program.clone(),
clock: self.clock.clone(),
rent: self.rent.clone(),
};
CpiContext::new(program, accounts)
}
fn into_msrm_reward(&self) -> CpiContext<'_, '_, '_, 'info, registry::DropReward<'info>> {
let program = self.registry_program.clone();
let accounts = registry::DropReward {
registrar: ProgramAccount::try_from(&self.msrm.registrar).unwrap(),
reward_event_q: ProgramAccount::try_from(&self.msrm.reward_event_q).unwrap(),
pool_mint: self.msrm.pool_mint.clone(),
vendor: ProgramAccount::try_from(&self.msrm.vendor).unwrap(),
vendor_vault: CpiAccount::try_from(&self.msrm.vendor_vault).unwrap(),
depositor: self.stake.to_account_info(),
depositor_authority: self.officer.to_account_info(),
token_program: self.token_program.clone(),
clock: self.clock.clone(),
rent: self.rent.clone(),
};
CpiContext::new(program, accounts)
}
}
impl<'info> Distribute<'info> {
fn into_burn(&self) -> CpiContext<'_, '_, '_, 'info, token::Burn<'info>> {
let program = self.token_program.clone();
let accounts = token::Burn {
mint: self.mint.clone(),
to: self.srm_vault.to_account_info(),
authority: self.officer.to_account_info(),
};
CpiContext::new(program, accounts)
}
fn into_stake_transfer(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.clone();
let accounts = token::Transfer {
from: self.srm_vault.to_account_info(),
to: self.stake.to_account_info(),
authority: self.officer.to_account_info(),
};
CpiContext::new(program, accounts)
}
fn into_treasury_transfer(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.clone();
let accounts = token::Transfer {
from: self.srm_vault.to_account_info(),
to: self.treasury.to_account_info(),
authority: self.officer.to_account_info(),
};
CpiContext::new(program, accounts)
}
}
// Events.
#[event]
pub struct DistributionDidChange {
distribution: Distribution,
}
#[event]
pub struct OfficerDidCreate {
pubkey: Pubkey,
}
// Error.
#[error]
pub enum ErrorCode {
#[msg("Distribution does not add to 100")]
InvalidDistribution,
#[msg("u128 cannot be converted into u64")]
U128CannotConvert,
#[msg("Only one instruction is allowed for this transaction")]
TooManyInstructions,
#[msg("Not enough SRM has been accumulated to distribute")]
InsufficientDistributionAmount,
#[msg("Must drop more SRM onto the stake pool")]
InsufficientStakeReward,
}
// Access control.
fn is_distribution_valid(d: &Distribution) -> Result<()> {
if d.burn + d.stake + d.treasury != 100 {
return Err(ErrorCode::InvalidDistribution.into());
}
Ok(())
}
fn is_distribution_ready(accounts: &Distribute) -> Result<()> {
if accounts.srm_vault.amount < 1_000_000 {
return Err(ErrorCode::InsufficientDistributionAmount.into());
}
Ok(())
}
// `ixs` must be the Instructions sysvar.
fn is_not_trading(ixs: &AccountInfo) -> Result<()> {
let data = ixs.try_borrow_data()?;
match tx_instructions::load_instruction_at(1, &data) {
Ok(_) => Err(ErrorCode::TooManyInstructions.into()),
Err(_) => Ok(()),
}
}
fn is_stake_reward_ready(accounts: &DropStakeReward) -> Result<()> {
// Min drop is 15,0000 SRM.
let min_reward: u64 = 15_000_000_000;
if accounts.stake.amount < min_reward {
return Err(ErrorCode::InsufficientStakeReward.into());
}
Ok(())
}
// Redefintions.
//
// The following types are redefined so that they can be parsed into the IDL,
// since Anchor doesn't yet support idl parsing across multiple crates.
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct ExchangeRate {
rate: u64,
from_decimals: u8,
quote_decimals: u8,
strict: bool,
}
impl From<ExchangeRate> for swap::ExchangeRate {
fn from(e: ExchangeRate) -> Self {
let ExchangeRate {
rate,
from_decimals,
quote_decimals,
strict,
} = e;
Self {
rate,
from_decimals,
quote_decimals,
strict,
}
}
}

View File

@ -0,0 +1,19 @@
cleanup() {
pkill -P $$ || true
wait || true
}
trap_add() {
trap_add_cmd=$1; shift || fatal "${FUNCNAME} usage error"
for trap_add_name in "$@"; do
trap -- "$(
extract_trap_cmd() { printf '%s\n' "${3:-}"; }
eval "extract_trap_cmd $(trap -p "${trap_add_name}")"
printf '%s\n' "${trap_add_cmd}"
)" "${trap_add_name}" \
|| fatal "unable to add to trap ${trap_add_name}"
done
}
declare -f -t trap_add
trap_add 'cleanup' EXIT

34
examples/cfo/scripts/fees.js Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env node
const process = require("process");
const fs = require("fs");
const anchor = require("@project-serum/anchor");
const { Market, OpenOrders } = require("@project-serum/serum");
const Account = anchor.web3.Account;
const Program = anchor.Program;
const provider = anchor.Provider.local();
const secret = JSON.parse(fs.readFileSync("./scripts/market-maker.json"));
const MARKET_MAKER = new Account(secret);
const PublicKey = anchor.web3.PublicKey;
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
async function main() {
const market = new PublicKey(process.argv[2]);
while (true) {
let marketClient = await Market.load(
provider.connection,
market,
{ commitment: "recent" },
DEX_PID
);
console.log("Fees: ", marketClient._decoded.quoteFeesAccrued.toString());
await sleep(3000);
}
}
main();
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
// Script to list a market, logging the address to stdout.
const utils = require("../tests/utils");
const fs = require("fs");
const anchor = require("@project-serum/anchor");
const provider = anchor.Provider.local();
async function main() {
ORDERBOOK_ENV = await utils.initMarket({
provider,
});
const out = {
market: ORDERBOOK_ENV.marketA._decoded.ownAddress.toString(),
};
console.log(JSON.stringify(out));
}
main();

View File

@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
source scripts/common.sh
DEX_PID="9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
PAYER_FILEPATH="$HOME/.config/solana/id.json"
CRANK="/home/armaniferrante/Documents/code/src/github.com/project-serum/serum-dex/target/debug/crank"
VALIDATOR_OUT="./validator-stdout.txt"
CRANK_LOGS="crank-logs.txt"
CRANK_STDOUT="crank-stdout.txt"
TRADE_BOT_STDOUT="trade-bot-stdout.txt"
FEES_STDOUT="fees.txt"
main () {
echo "Cleaning old output files..."
rm -rf test-ledger
rm -f $TRADE_BOT_STDOUT
rm -f $FEES_STDOUT
rm -f $VALIDATOR_OUT
rm -f $CRANK_LOGS && touch $CRANK_LOGS
echo "Starting local network..."
solana-test-validator \
--bpf-program 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin ./deps/serum-dex/dex/target/deploy/serum_dex.so \
--bpf-program 22Y43yTVxuUkoRKdm9thyRhQ3SdgQS7c7kB6UNCiaczD ./deps/swap/target/deploy/swap.so \
--bpf-program GrAkKfEpTKQuVHG2Y97Y2FF4i7y7Q5AHLK94JBy7Y5yv ./deps/stake/target/deploy/registry.so \
--bpf-program 6ebQNeTPZ1j7k3TtkCCtEPRvG7GQsucQrZ7sSEDQi9Ks ./deps/stake/target/deploy/lockup.so \
--bpf-program 5CHQcwNhkFiFXXM8HakHi8cB7AKP3M3GPdEBDeRJBWQq ./target/deploy/cfo.so > $VALIDATOR_OUT &
sleep 2
echo "Listing market..."
market=$(./scripts/list-market.js | jq -r .market)
sleep 2
echo "Market listed $market"
echo "Running crank..."
$CRANK localnet consume-events \
-c $market \
-d $DEX_PID -e 5 \
--log-directory $CRANK_LOGS \
--market $market \
--num-workers 1 \
--payer $PAYER_FILEPATH \
--pc-wallet $market > $CRANK_STDOUT &
echo "Running trade bot..."
./scripts/trade-bot.js $market > $TRADE_BOT_STDOUT &
echo "Running fees listener..."
./scripts/fees.js $market > $FEES_STDOUT &
echo "Localnet running..."
echo "Ctl-c to exit."
wait
}
main

View File

@ -0,0 +1 @@
[13,174,53,150,78,228,12,98,170,254,212,211,125,193,2,241,97,137,49,209,189,199,27,215,220,65,57,203,215,93,105,203,217,32,5,194,157,118,162,47,102,126,235,65,99,80,56,231,217,114,25,225,239,140,169,92,150,146,211,218,183,139,9,104]

View File

@ -0,0 +1,16 @@
#!/usr/bin/env node
// Script to infinitely post orders that are immediately filled.
const process = require("process");
const anchor = require("@project-serum/anchor");
const PublicKey = anchor.web3.PublicKey;
const { runTradeBot } = require("../tests/utils");
async function main() {
const market = new PublicKey(process.argv[2]);
const provider = anchor.Provider.local();
runTradeBot(market, provider);
}
main();

202
examples/cfo/tests/cfo.js Normal file
View File

@ -0,0 +1,202 @@
const assert = require("assert");
const { Token } = require("@solana/spl-token");
const anchor = require("@project-serum/anchor");
const serumCmn = require("@project-serum/common");
const { Market } = require("@project-serum/serum");
const { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } = anchor.web3;
const utils = require("./utils");
const { setupStakePool } = require("./utils/stake");
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
const SWAP_PID = new PublicKey("22Y43yTVxuUkoRKdm9thyRhQ3SdgQS7c7kB6UNCiaczD");
const TOKEN_PID = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
const REGISTRY_PID = new PublicKey(
"GrAkKfEpTKQuVHG2Y97Y2FF4i7y7Q5AHLK94JBy7Y5yv"
);
const LOCKUP_PID = new PublicKey(
"6ebQNeTPZ1j7k3TtkCCtEPRvG7GQsucQrZ7sSEDQi9Ks"
);
const FEES = "6160355581";
describe("cfo", () => {
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Cfo;
let officer;
let TOKEN_CLIENT;
let officerAccount;
const sweepAuthority = program.provider.wallet.publicKey;
// Accounts used to setup the orderbook.
let ORDERBOOK_ENV,
// Accounts used for A -> USDC swap transactions.
SWAP_A_USDC_ACCOUNTS,
// Accounts used for USDC -> A swap transactions.
SWAP_USDC_A_ACCOUNTS,
// Serum DEX vault PDA for market A/USDC.
marketAVaultSigner,
// Serum DEX vault PDA for market B/USDC.
marketBVaultSigner;
let registrar, msrmRegistrar;
it("BOILERPLATE: Sets up a market with funded fees", async () => {
ORDERBOOK_ENV = await utils.initMarket({
provider: program.provider,
});
console.log("Token A: ", ORDERBOOK_ENV.marketA.baseMintAddress.toString());
console.log(
"Token USDC: ",
ORDERBOOK_ENV.marketA.quoteMintAddress.toString()
);
TOKEN_CLIENT = new Token(
program.provider.connection,
ORDERBOOK_ENV.usdc,
TOKEN_PID,
program.provider.wallet.payer
);
await TOKEN_CLIENT.transfer(
ORDERBOOK_ENV.godUsdc,
ORDERBOOK_ENV.marketA._decoded.quoteVault,
program.provider.wallet.payer,
[],
10000000000000
);
const tokenAccount = await TOKEN_CLIENT.getAccountInfo(
ORDERBOOK_ENV.marketA._decoded.quoteVault
);
assert.ok(tokenAccount.amount.toString() === "10000902263700");
});
it("BOILERPLATE: Executes trades to generate fees", async () => {
await utils.runTradeBot(
ORDERBOOK_ENV.marketA._decoded.ownAddress,
program.provider,
1
);
let marketClient = await Market.load(
program.provider.connection,
ORDERBOOK_ENV.marketA._decoded.ownAddress,
{ commitment: "recent" },
DEX_PID
);
assert.ok(marketClient._decoded.quoteFeesAccrued.toString() === FEES);
});
it("BOILERPLATE: Sets up the staking pools", async () => {
await setupStakePool(ORDERBOOK_ENV.mintA, ORDERBOOK_ENV.godA);
registrar = ORDERBOOK_ENV.usdc;
msrmRegistrar = registrar;
});
it("Creates a CFO!", async () => {
let distribution = {
burn: 80,
stake: 20,
treasury: 0,
};
officer = await program.account.officer.associatedAddress(DEX_PID);
const srmVault = await anchor.utils.publicKey.associated(
program.programId,
officer,
anchor.utils.bytes.utf8.encode("vault"),
);
const stake = await anchor.utils.publicKey.associated(
program.programId,
officer,
anchor.utils.bytes.utf8.encode("stake"),
);
const treasury = await anchor.utils.publicKey.associated(
program.programId,
officer,
Buffer.from(anchor.utils.bytes.utf8.encode("treasury")),
);
await program.rpc.createOfficer(distribution, registrar, msrmRegistrar, {
accounts: {
officer,
srmVault,
stake,
treasury,
mint: ORDERBOOK_ENV.mintA,
authority: program.provider.wallet.publicKey,
dexProgram: DEX_PID,
swapProgram: SWAP_PID,
tokenProgram: TOKEN_PID,
systemProgram: SystemProgram.programId,
rent: SYSVAR_RENT_PUBKEY,
},
});
officerAccount = await program.account.officer.associated(DEX_PID);
assert.ok(
officerAccount.authority.equals(program.provider.wallet.publicKey)
);
assert.ok(
JSON.stringify(officerAccount.distribution) ===
JSON.stringify(distribution)
);
});
it("Creates a token account for the officer associated with the market", async () => {
const token = await anchor.utils.publicKey.associated(
program.programId,
officer,
ORDERBOOK_ENV.usdc
);
await program.rpc.createOfficerToken({
accounts: {
officer,
token,
mint: ORDERBOOK_ENV.usdc,
payer: program.provider.wallet.publicKey,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PID,
rent: SYSVAR_RENT_PUBKEY,
},
});
const tokenAccount = await TOKEN_CLIENT.getAccountInfo(token);
assert.ok(tokenAccount.state === 1);
assert.ok(tokenAccount.isInitialized);
});
it("Sweeps fees", async () => {
const sweepVault = await anchor.utils.publicKey.associated(
program.programId,
officer,
ORDERBOOK_ENV.usdc
);
const beforeTokenAccount = await serumCmn.getTokenAccount(
program.provider,
sweepVault
);
await program.rpc.sweepFees({
accounts: {
officer,
sweepVault,
mint: ORDERBOOK_ENV.usdc,
dex: {
market: ORDERBOOK_ENV.marketA._decoded.ownAddress,
pcVault: ORDERBOOK_ENV.marketA._decoded.quoteVault,
sweepAuthority,
vaultSigner: ORDERBOOK_ENV.vaultSigner,
dexProgram: DEX_PID,
tokenProgram: TOKEN_PID,
},
},
});
const afterTokenAccount = await serumCmn.getTokenAccount(
program.provider,
sweepVault
);
assert.ok(
afterTokenAccount.amount.sub(beforeTokenAccount.amount).toString() ===
FEES
);
});
it("TODO", async () => {
// todo
});
});

View File

@ -0,0 +1,647 @@
// 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 secret = JSON.parse(
require("fs").readFileSync("./scripts/market-maker.json")
);
const MARKET_MAKER = new Account(secret);
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,
});
return {
marketA: MARKET_A_USDC,
vaultSigner,
marketMaker,
mintA: MINT_A,
usdc: USDC,
godA: GOD_A,
godUsdc: GOD_USDC,
};
}
// Creates everything needed for an orderbook to be running
//
// * Mints for both the base and quote currencies.
// * Lists the market.
// * Provides resting orders on the market.
//
// Returns a client that can be used to interact with the market
// (and some other data, e.g., the mints and market maker account).
async function initOrderbook({ provider, bids, asks }) {
if (!bids || !asks) {
asks = [
[6.041, 7.8],
[6.051, 72.3],
[6.055, 5.4],
[6.067, 15.7],
[6.077, 390.0],
[6.09, 24.0],
[6.11, 36.3],
[6.133, 300.0],
[6.167, 687.8],
];
bids = [
[6.004, 8.5],
[5.995, 12.9],
[5.987, 6.2],
[5.978, 15.3],
[5.965, 82.8],
[5.961, 25.4],
];
}
// Create base and quote currency mints.
const decimals = 6;
const [MINT_A, GOD_A] = await serumCmn.createMintAndVault(
provider,
new BN(1000000000000000),
undefined,
decimals
);
const [USDC, GOD_USDC] = await serumCmn.createMintAndVault(
provider,
new BN(1000000000000000),
undefined,
decimals
);
// Create a funded account to act as market maker.
const amount = 100000 * 10 ** decimals;
const marketMaker = await fundAccount({
provider,
mints: [
{ god: GOD_A, mint: MINT_A, amount, decimals },
{ god: GOD_USDC, mint: USDC, amount, decimals },
],
});
[marketClient, 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,
});
return {
marketClient,
baseMint: MINT_A,
quoteMint: USDC,
marketMaker,
vaultSigner,
};
}
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");
}
async function runTradeBot(market, provider, iterations = undefined) {
let marketClient = await Market.load(
provider.connection,
market,
{ commitment: "recent" },
DEX_PID
);
const baseTokenUser1 = (
await marketClient.getTokenAccountsByOwnerForMint(
provider.connection,
MARKET_MAKER.publicKey,
marketClient.baseMintAddress
)
)[0].pubkey;
const quoteTokenUser1 = (
await marketClient.getTokenAccountsByOwnerForMint(
provider.connection,
MARKET_MAKER.publicKey,
marketClient.quoteMintAddress
)
)[0].pubkey;
const baseTokenUser2 = (
await marketClient.getTokenAccountsByOwnerForMint(
provider.connection,
provider.wallet.publicKey,
marketClient.baseMintAddress
)
)[0].pubkey;
const quoteTokenUser2 = (
await marketClient.getTokenAccountsByOwnerForMint(
provider.connection,
provider.wallet.publicKey,
marketClient.quoteMintAddress
)
)[0].pubkey;
const makerOpenOrdersUser1 = (
await OpenOrders.findForMarketAndOwner(
provider.connection,
market,
MARKET_MAKER.publicKey,
DEX_PID
)
)[0];
makerOpenOrdersUser2 = (
await OpenOrders.findForMarketAndOwner(
provider.connection,
market,
provider.wallet.publicKey,
DEX_PID
)
)[0];
const price = 6.041;
const size = 700000.8;
let maker = MARKET_MAKER;
let taker = provider.wallet.payer;
let baseToken = baseTokenUser1;
let quoteToken = quoteTokenUser2;
let makerOpenOrders = makerOpenOrdersUser1;
let k = 1;
while (true) {
if (iterations && k > iterations) {
break;
}
const clientId = new anchor.BN(k);
if (k % 5 === 0) {
if (maker.publicKey.equals(MARKET_MAKER.publicKey)) {
maker = provider.wallet.payer;
makerOpenOrders = makerOpenOrdersUser2;
taker = MARKET_MAKER;
baseToken = baseTokenUser2;
quoteToken = quoteTokenUser1;
} else {
maker = MARKET_MAKER;
makerOpenOrders = makerOpenOrdersUser1;
taker = provider.wallet.payer;
baseToken = baseTokenUser1;
quoteToken = quoteTokenUser2;
}
}
// Post ask.
const { transaction: tx_ask, signers: sigs_ask } =
await marketClient.makePlaceOrderTransaction(provider.connection, {
owner: maker,
payer: baseToken,
side: "sell",
price,
size,
orderType: "postOnly",
clientId,
openOrdersAddressKey: undefined,
openOrdersAccount: undefined,
feeDiscountPubkey: null,
selfTradeBehavior: "abortTransaction",
});
let txSig = await provider.send(tx_ask, sigs_ask.concat(maker));
console.log("Ask", txSig);
// Take.
const { transaction: tx_bid, signers: sigs_bid } =
await marketClient.makePlaceOrderTransaction(provider.connection, {
owner: taker,
payer: quoteToken,
side: "buy",
price,
size,
orderType: "ioc",
clientId: undefined,
openOrdersAddressKey: undefined,
openOrdersAccount: undefined,
feeDiscountPubkey: null,
selfTradeBehavior: "abortTransaction",
});
txSig = await provider.send(tx_bid, sigs_bid.concat(taker));
console.log("Bid", txSig);
await sleep(1000);
// Cancel anything remaining.
try {
txSig = await marketClient.cancelOrderByClientId(
provider.connection,
maker,
makerOpenOrders.address,
clientId
);
console.log("Cancelled the rest", txSig);
await sleep(1000);
} catch (e) {
console.log("Unable to cancel order", e);
}
k += 1;
// If the open orders account wasn't previously initialized, it is now.
if (makerOpenOrdersUser2 === undefined) {
makerOpenOrdersUser2 = (
await OpenOrders.findForMarketAndOwner(
provider.connection,
market,
provider.wallet.publicKey,
DEX_PID
)
)[0];
}
}
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
module.exports = {
fundAccount,
initMarket,
initOrderbook,
setupMarket,
DEX_PID,
getVaultOwnerAndNonce,
runTradeBot,
};

View File

@ -0,0 +1,184 @@
const anchor = require("@project-serum/anchor");
const serumCmn = require("@project-serum/common");
const TokenInstructions = require("@project-serum/serum").TokenInstructions;
const utils = require("../../deps/stake/tests/utils");
const lockup = anchor.workspace.Lockup;
const registry = anchor.workspace.Registry;
const provider = anchor.Provider.env();
let lockupAddress = null;
let mint = null;
let god = null;
let registrarAccount = null;
let registrarSigner = null;
let nonce = null;
let poolMint = null;
const registrar = new anchor.web3.Account();
const rewardQ = new anchor.web3.Account();
const withdrawalTimelock = new anchor.BN(4);
const stakeRate = new anchor.BN(2);
const rewardQLen = 170;
let member = null;
let memberAccount = null;
let memberSigner = null;
let balances = null;
let balancesLocked = null;
const WHITELIST_SIZE = 10;
async function setupStakePool(mint, god) {
// Registry genesis.
const [_registrarSigner, _nonce] =
await anchor.web3.PublicKey.findProgramAddress(
[registrar.publicKey.toBuffer()],
registry.programId
);
registrarSigner = _registrarSigner;
nonce = _nonce;
poolMint = await serumCmn.createMint(provider, registrarSigner);
try {
// Init registry.
await registry.state.rpc.new({
accounts: { lockupProgram: lockup.programId },
});
// Init lockup.
await lockup.state.rpc.new({
accounts: {
authority: provider.wallet.publicKey,
},
});
} catch (err) {
// Skip errors for convenience when developing locally,
// since the state constructors can only be called once.
}
// Initialize stake pool.
await registry.rpc.initialize(
mint,
provider.wallet.publicKey,
nonce,
withdrawalTimelock,
stakeRate,
rewardQLen,
{
accounts: {
registrar: registrar.publicKey,
poolMint,
rewardEventQ: rewardQ.publicKey,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
},
signers: [registrar, rewardQ],
instructions: [
await registry.account.registrar.createInstruction(registrar),
await registry.account.rewardQueue.createInstruction(rewardQ, 8250),
],
}
);
registrarAccount = await registry.account.registrar.fetch(
registrar.publicKey
);
console.log("Registrar", registrar.publicKey.toString());
console.log("Wallet", registry.provider.wallet.publicKey.toString());
// Create account for staker.
const seed = anchor.utils.sha256
.hash(`${registrar.publicKey.toString()}:Member`)
.slice(0, 32);
member = await anchor.web3.PublicKey.createWithSeed(
registry.provider.wallet.publicKey,
seed,
registry.programId
);
const [_memberSigner, nonce2] =
await anchor.web3.PublicKey.findProgramAddress(
[registrar.publicKey.toBuffer(), member.toBuffer()],
registry.programId
);
memberSigner = _memberSigner;
const [mainTx, _balances] = await utils.createBalanceSandbox(
provider,
registrarAccount,
memberSigner
);
const [lockedTx, _balancesLocked] = await utils.createBalanceSandbox(
provider,
registrarAccount,
memberSigner
);
balances = _balances;
balancesLocked = _balancesLocked;
const tx = registry.transaction.createMember(nonce2, {
accounts: {
registrar: registrar.publicKey,
member: member,
beneficiary: provider.wallet.publicKey,
memberSigner,
balances,
balancesLocked,
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
},
instructions: [
anchor.web3.SystemProgram.createAccountWithSeed({
fromPubkey: registry.provider.wallet.publicKey,
newAccountPubkey: member,
basePubkey: registry.provider.wallet.publicKey,
seed,
lamports:
await registry.provider.connection.getMinimumBalanceForRentExemption(
registry.account.member.size
),
space: registry.account.member.size,
programId: registry.programId,
}),
],
});
const signers = [provider.wallet.payer];
const allTxs = [mainTx, lockedTx, { tx, signers }];
await provider.sendAll(allTxs);
memberAccount = await registry.account.member.fetch(member);
// Deposit into stake program.
const depositAmount = new anchor.BN(120);
await registry.rpc.deposit(depositAmount, {
accounts: {
depositor: god,
depositorAuthority: provider.wallet.publicKey,
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
vault: memberAccount.balances.vault,
beneficiary: provider.wallet.publicKey,
member: member,
},
});
// Stake.
const stakeAmount = new anchor.BN(10);
await registry.rpc.stake(stakeAmount, false, {
accounts: {
// Stake instance.
registrar: registrar.publicKey,
rewardEventQ: rewardQ.publicKey,
poolMint,
// Member.
member: member,
beneficiary: provider.wallet.publicKey,
balances,
balancesLocked,
// Program signers.
memberSigner,
registrarSigner,
// Misc.
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
},
});
}
module.exports = {
setupStakePool,
};

View File

@ -16,4 +16,5 @@ default = []
[dependencies]
anchor-lang = { path = "../../../../lang" }
anchor-spl = { path = "../../../../spl" }
misc2 = { path = "../misc2", features = ["cpi"] }

View File

@ -2,6 +2,7 @@
//! It's not too instructive/coherent by itself, so please see other examples.
use anchor_lang::prelude::*;
use anchor_spl::token::{Mint, TokenAccount};
use misc2::misc2::MyState as Misc2State;
use misc2::Auth;
@ -123,6 +124,29 @@ pub mod misc {
acc.data = 1234;
Ok(())
}
pub fn test_token_seeds_init(_ctx: Context<TestTokenSeedsInit>, _nonce: u8) -> ProgramResult {
Ok(())
}
}
#[derive(Accounts)]
#[instruction(nonce: u8)]
pub struct TestTokenSeedsInit<'info> {
#[account(
init,
token = mint,
authority = authority,
seeds = [b"my-token-seed".as_ref(), &[nonce]],
payer = authority,
space = TokenAccount::LEN,
)]
pub my_pda: CpiAccount<'info, TokenAccount>,
pub mint: CpiAccount<'info, Mint>,
pub authority: AccountInfo<'info>,
pub system_program: AccountInfo<'info>,
pub rent: Sysvar<'info, Rent>,
pub token_program: AccountInfo<'info>,
}
#[derive(Accounts)]
@ -219,7 +243,7 @@ pub struct TestClose<'info> {
// the program.
#[derive(Accounts)]
pub struct TestInitAssociatedAccount<'info> {
#[account(init, associated = authority, with = state, with = data)]
#[account(init, associated = authority, with = state, with = data, with = b"my-seed")]
my_account: ProgramAccount<'info, TestData>,
#[account(mut, signer)]
authority: AccountInfo<'info>,
@ -231,7 +255,7 @@ pub struct TestInitAssociatedAccount<'info> {
#[derive(Accounts)]
pub struct TestAssociatedAccount<'info> {
#[account(mut, associated = authority, with = state, with = data)]
#[account(mut, associated = authority, with = state, with = data, with = b"my-seed")]
my_account: ProgramAccount<'info, TestData>,
#[account(mut, signer)]
authority: AccountInfo<'info>,

View File

@ -2,6 +2,7 @@ const anchor = require("@project-serum/anchor");
const PublicKey = anchor.web3.PublicKey;
const serumCmn = require("@project-serum/common");
const assert = require("assert");
const { TOKEN_PROGRAM_ID, Token } = require("@solana/spl-token");
describe("misc", () => {
// Configure the client to use the local cluster.
@ -140,18 +141,17 @@ describe("misc", () => {
// Manual associated address calculation for test only. Clients should use
// the generated methods.
const [
associatedAccount,
nonce,
] = await anchor.web3.PublicKey.findProgramAddress(
[
Buffer.from([97, 110, 99, 104, 111, 114]), // b"anchor".
program.provider.wallet.publicKey.toBuffer(),
state.toBuffer(),
data.publicKey.toBuffer(),
],
program.programId
);
const [associatedAccount, nonce] =
await anchor.web3.PublicKey.findProgramAddress(
[
anchor.utils.bytes.utf8.encode("anchor"),
program.provider.wallet.publicKey.toBuffer(),
state.toBuffer(),
data.publicKey.toBuffer(),
anchor.utils.bytes.utf8.encode("my-seed"),
],
program.programId
);
await assert.rejects(
async () => {
await program.account.testData.fetch(associatedAccount);
@ -178,25 +178,25 @@ describe("misc", () => {
const account = await program.account.testData.associated(
program.provider.wallet.publicKey,
state,
data.publicKey
data.publicKey,
anchor.utils.bytes.utf8.encode("my-seed")
);
assert.ok(account.data.toNumber() === 1234);
});
it("Can use an associated program account", async () => {
const state = await program.state.address();
const [
associatedAccount,
nonce,
] = await anchor.web3.PublicKey.findProgramAddress(
[
Buffer.from([97, 110, 99, 104, 111, 114]), // b"anchor".
program.provider.wallet.publicKey.toBuffer(),
state.toBuffer(),
data.publicKey.toBuffer(),
],
program.programId
);
const [associatedAccount, nonce] =
await anchor.web3.PublicKey.findProgramAddress(
[
anchor.utils.bytes.utf8.encode("anchor"),
program.provider.wallet.publicKey.toBuffer(),
state.toBuffer(),
data.publicKey.toBuffer(),
anchor.utils.bytes.utf8.encode("my-seed"),
],
program.programId
);
await program.rpc.testAssociatedAccount(new anchor.BN(5), {
accounts: {
myAccount: associatedAccount,
@ -209,7 +209,8 @@ describe("misc", () => {
const account = await program.account.testData.associated(
program.provider.wallet.publicKey,
state,
data.publicKey
data.publicKey,
anchor.utils.bytes.utf8.encode("my-seed")
);
assert.ok(account.data.toNumber() === 5);
});
@ -402,4 +403,36 @@ describe("misc", () => {
assert.ok(myPdaAccount.data === 1234);
assert.ok((myPdaAccount.bump = bump));
});
it("Can create a token account from seeds pda", async () => {
const mint = await Token.createMint(
program.provider.connection,
program.provider.wallet.payer,
program.provider.wallet.publicKey,
null,
0,
TOKEN_PROGRAM_ID
);
const [myPda, bump] = await PublicKey.findProgramAddress(
[Buffer.from(anchor.utils.bytes.utf8.encode("my-token-seed"))],
program.programId
);
await program.rpc.testTokenSeedsInit(bump, {
accounts: {
myPda,
mint: mint.publicKey,
authority: program.provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
tokenProgram: TOKEN_PROGRAM_ID,
},
});
const account = await mint.getAccountInfo(myPda);
assert.ok(account.state === 1);
assert.ok(account.amount.toNumber() === 0);
assert.ok(account.isInitialized);
assert.ok(account.owner.equals(program.provider.wallet.publicKey));
assert.ok(account.mint.equals(mint.publicKey));
});
});

View File

@ -30,6 +30,10 @@ impl<'a, T: AccountDeserialize + Clone> CpiAccount<'a, T> {
))
}
pub fn try_from_init(info: &AccountInfo<'a>) -> Result<CpiAccount<'a, T>, ProgramError> {
Self::try_from(info)
}
/// Reloads the account from storage. This is useful, for example, when
/// observing side effects after CPI.
pub fn reload(&self) -> Result<CpiAccount<'a, T>, ProgramError> {

View File

@ -44,6 +44,8 @@ pub enum ErrorCode {
ConstraintAssociatedInit,
#[msg("A close constraint was violated")]
ConstraintClose,
#[msg("An address constraint was violated")]
ConstraintAddress,
// Accounts.
#[msg("The account discriminator was already set on this account")]

View File

@ -210,6 +210,25 @@ pub trait Bump {
fn seed(&self) -> u8;
}
pub trait Key {
fn key(&self) -> Pubkey;
}
impl<'info, T> Key for T
where
T: ToAccountInfo<'info>,
{
fn key(&self) -> Pubkey {
*self.to_account_info().key
}
}
impl Key for Pubkey {
fn key(&self) -> Pubkey {
*self
}
}
/// The prelude contains all commonly used components of the crate.
/// All programs should include it via `anchor_lang::prelude::*;`.
pub mod prelude {
@ -287,3 +306,24 @@ pub mod __private {
pub use crate::state::PROGRAM_STATE_SEED;
pub const CLOSED_ACCOUNT_DISCRIMINATOR: [u8; 8] = [255, 255, 255, 255, 255, 255, 255, 255];
}
/// Returns the program-derived-address seeds used for creating the associated
/// account.
#[macro_export]
macro_rules! associated_seeds {
(account = $pda:expr, associated = $associated:expr) => {
&[
b"anchor".as_ref(),
$associated.to_account_info().key.as_ref(),
&[anchor_lang::Bump::seed(&*$pda)],
]
};
(account = $pda:expr, associated = $associated:expr, $(with = $with:expr),+) => {
&[
b"anchor".as_ref(),
$associated.to_account_info().key.as_ref(),
$($with.to_account_info().key.as_ref()),+,
&[anchor_lang::Bump::seed(&*$pda)][..],
]
};
}

View File

@ -61,6 +61,10 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T>
T::try_deserialize_unchecked(&mut data)?,
))
}
pub fn into_inner(self) -> T {
self.inner.account
}
}
impl<'info, T> Accounts<'info> for ProgramAccount<'info, T>

View File

@ -1,12 +1,12 @@
use crate::{
CompositeField, Constraint, ConstraintAssociatedGroup, ConstraintBelongsTo, ConstraintClose,
ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut,
ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeedsGroup, ConstraintSigner,
ConstraintState, Field, Ty,
CompositeField, Constraint, ConstraintAddress, ConstraintAssociatedGroup, ConstraintBelongsTo,
ConstraintClose, ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral,
ConstraintMut, ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeedsGroup,
ConstraintSigner, ConstraintState, Field, PdaKind, Ty,
};
use proc_macro2_diagnostics::SpanDiagnosticExt;
use quote::quote;
use syn::LitInt;
use syn::Expr;
pub fn generate(f: &Field) -> proc_macro2::TokenStream {
let checks: Vec<proc_macro2::TokenStream> = linearize(&f.constraints)
@ -53,6 +53,7 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec<Constraint> {
state,
associated,
close,
address,
} = c_group.clone();
let mut constraints = Vec::new();
@ -100,6 +101,9 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec<Constraint> {
if let Some(c) = close {
constraints.push(Constraint::Close(c));
}
if let Some(c) = address {
constraints.push(Constraint::Address(c));
}
constraints
}
@ -118,6 +122,7 @@ fn generate_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStream {
Constraint::State(c) => generate_constraint_state(f, c),
Constraint::AssociatedGroup(c) => generate_constraint_associated(f, c),
Constraint::Close(c) => generate_constraint_close(f, c),
Constraint::Address(c) => generate_constraint_address(f, c),
}
}
@ -129,6 +134,16 @@ fn generate_constraint_composite(_f: &CompositeField, c: &Constraint) -> proc_ma
}
}
fn generate_constraint_address(f: &Field, c: &ConstraintAddress) -> proc_macro2::TokenStream {
let field = &f.ident;
let addr = &c.address;
quote! {
if #field.to_account_info().key != &#addr {
return Err(anchor_lang::__private::ErrorCode::ConstraintAddress.into());
}
}
}
pub fn generate_constraint_init(_f: &Field, _c: &ConstraintInit) -> proc_macro2::TokenStream {
quote! {}
}
@ -232,11 +247,8 @@ pub fn generate_constraint_rent_exempt(
c: &ConstraintRentExempt,
) -> proc_macro2::TokenStream {
let ident = &f.ident;
let info = match f.ty {
Ty::AccountInfo => quote! { #ident },
Ty::ProgramAccount(_) => quote! { #ident.to_account_info() },
Ty::Loader(_) => quote! { #ident.to_account_info() },
_ => panic!("Invalid syntax: rent exemption cannot be specified."),
let info = quote! {
#ident.to_account_info()
};
match c {
ConstraintRentExempt::Skip => quote! {},
@ -263,15 +275,22 @@ fn generate_constraint_seeds_init(f: &Field, c: &ConstraintSeedsGroup) -> proc_m
let payer = #p.to_account_info();
}
};
let seeds_constraint = generate_constraint_seeds_address(f, c);
let seeds_with_nonce = {
let s = &c.seeds;
let seeds_constraint = generate_constraint_seeds_address(f, c);
quote! {
#seeds_constraint
let seeds = [#s];
[#s]
}
};
generate_pda(f, seeds_with_nonce, payer, &c.space, false)
generate_pda(
f,
seeds_constraint,
seeds_with_nonce,
payer,
&c.space,
false,
&c.kind,
)
}
fn generate_constraint_seeds_address(
@ -315,44 +334,78 @@ pub fn generate_constraint_associated_init(
let payer = #p.to_account_info();
},
};
let associated_seeds_constraint = generate_constraint_associated_seeds(f, c);
let seeds_with_nonce = if c.associated_seeds.is_empty() {
quote! {
#associated_seeds_constraint
let seeds = [
&b"anchor"[..],
#associated_target.to_account_info().key.as_ref(),
&[nonce],
];
}
} else {
let seeds = to_seeds_tts(&c.associated_seeds);
quote! {
#associated_seeds_constraint
let seeds = [
&b"anchor"[..],
#associated_target.to_account_info().key.as_ref(),
#seeds
&[nonce],
];
let seeds_constraint = generate_constraint_associated_seeds(f, c);
let seeds_with_nonce = {
if c.associated_seeds.is_empty() {
quote! {
[
&b"anchor"[..],
#associated_target.to_account_info().key.as_ref(),
&[nonce],
]
}
} else {
let seeds = to_seeds_tts(&c.associated_seeds);
quote! {
[
&b"anchor"[..],
#associated_target.to_account_info().key.as_ref(),
#seeds
&[nonce],
]
}
}
};
generate_pda(f, seeds_with_nonce, payer, &c.space, true)
generate_pda(
f,
seeds_constraint,
seeds_with_nonce,
payer,
&c.space,
true,
&c.kind,
)
}
fn parse_ty(f: &Field) -> (&syn::Ident, proc_macro2::TokenStream, bool) {
match &f.ty {
Ty::ProgramAccount(ty) => (
&ty.account_ident,
quote! {
anchor_lang::ProgramAccount
},
false,
),
Ty::Loader(ty) => (
&ty.account_ident,
quote! {
anchor_lang::Loader
},
true,
),
Ty::CpiAccount(ty) => (
&ty.account_ident,
quote! {
anchor_lang::CpiAccount
},
false,
),
_ => panic!("Invalid type for initializing a program derived address"),
}
}
pub fn generate_pda(
f: &Field,
seeds_constraint: proc_macro2::TokenStream,
seeds_with_nonce: proc_macro2::TokenStream,
payer: proc_macro2::TokenStream,
space: &Option<LitInt>,
space: &Option<Expr>,
assign_nonce: bool,
kind: &PdaKind,
) -> proc_macro2::TokenStream {
let field = &f.ident;
let (account_ty, is_zero_copy) = match &f.ty {
Ty::ProgramAccount(ty) => (&ty.account_ident, false),
Ty::Loader(ty) => (&ty.account_ident, true),
_ => panic!("Invalid type for initializing a program derived address"),
};
let (account_ty, account_wrapper_ty, is_zero_copy) = parse_ty(&f);
let space = match space {
// If no explicit space param was given, serialize the type to bytes
@ -375,64 +428,133 @@ pub fn generate_pda(
},
};
let account_wrapper_ty = match is_zero_copy {
false => quote! {
anchor_lang::ProgramAccount
},
true => quote! {
anchor_lang::Loader
},
};
let nonce_assignment = match assign_nonce {
false => quote! {},
true => match is_zero_copy {
false => quote! {
pa.__nonce = nonce;
},
// Zero copy is not deserialized, so the data must be lazy loaded.
true => quote! {
pa.load_init()?.__nonce = nonce;
true => match &f.ty {
Ty::CpiAccount(_) => quote! {},
_ => match is_zero_copy {
false => quote! {
pa.__nonce = nonce;
},
// Zero copy is not deserialized, so the data must be lazy loaded.
true => quote! {
pa.load_init()?.__nonce = nonce;
},
},
},
};
quote! {
let #field: #account_wrapper_ty<#account_ty> = {
#space
#payer
match kind {
PdaKind::Token { owner, mint } => quote! {
let #field: #account_wrapper_ty<#account_ty> = {
#space
#payer
#seeds_constraint
let lamports = rent.minimum_balance(space);
let ix = anchor_lang::solana_program::system_instruction::create_account(
payer.to_account_info().key,
#field.to_account_info().key,
lamports,
space as u64,
program_id,
);
// Fund the account for rent exemption.
let required_lamports = rent
.minimum_balance(anchor_spl::token::TokenAccount::LEN)
.max(1)
.saturating_sub(#field.to_account_info().lamports());
if required_lamports > 0 {
anchor_lang::solana_program::program::invoke(
&anchor_lang::solana_program::system_instruction::transfer(
payer.to_account_info().key,
#field.to_account_info().key,
required_lamports,
),
&[
payer.to_account_info(),
#field.to_account_info(),
system_program.to_account_info().clone(),
],
)?;
}
#seeds_with_nonce
let signer = &[&seeds[..]];
anchor_lang::solana_program::program::invoke_signed(
&ix,
&[
// Allocate space.
anchor_lang::solana_program::program::invoke_signed(
&anchor_lang::solana_program::system_instruction::allocate(
#field.to_account_info().key,
anchor_spl::token::TokenAccount::LEN as u64,
),
&[
#field.to_account_info(),
system_program.clone(),
],
&[&#seeds_with_nonce[..]],
)?;
#field.to_account_info(),
payer.to_account_info(),
system_program.to_account_info(),
],
signer,
).map_err(|e| {
anchor_lang::solana_program::msg!("Unable to create associated account");
e
})?;
// 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(),
)?;
#nonce_assignment
pa
};
// Assign to the spl token program.
let __ix = anchor_lang::solana_program::system_instruction::assign(
#field.to_account_info().key,
token_program.to_account_info().key,
);
anchor_lang::solana_program::program::invoke_signed(
&__ix,
&[
#field.to_account_info(),
system_program.to_account_info(),
],
&[&#seeds_with_nonce[..]],
)?;
// Initialize the token account.
let cpi_program = token_program.to_account_info();
let accounts = anchor_spl::token::InitializeAccount {
account: #field.to_account_info(),
mint: #mint.to_account_info(),
authority: #owner.to_account_info(),
rent: rent.to_account_info(),
};
let cpi_ctx = CpiContext::new(cpi_program, accounts);
anchor_spl::token::initialize_account(cpi_ctx)?;
anchor_lang::CpiAccount::try_from_init(
&#field.to_account_info(),
)?
};
},
PdaKind::Program => {
quote! {
let #field: #account_wrapper_ty<#account_ty> = {
#space
#payer
#seeds_constraint
let lamports = rent.minimum_balance(space);
let ix = anchor_lang::solana_program::system_instruction::create_account(
payer.to_account_info().key,
#field.to_account_info().key,
lamports,
space as u64,
program_id,
);
anchor_lang::solana_program::program::invoke_signed(
&ix,
&[
#field.to_account_info(),
payer.to_account_info(),
system_program.to_account_info(),
],
&[&#seeds_with_nonce[..]]
).map_err(|e| {
anchor_lang::solana_program::msg!("Unable to create associated account");
e
})?;
// 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(),
)?;
#nonce_assignment
pa
};
}
}
}
}
@ -455,7 +577,13 @@ pub fn generate_constraint_associated_seeds(
#seeds
}
};
let associated_field = if c.is_init {
let is_find_nonce = match &f.ty {
Ty::CpiAccount(_) => true,
Ty::AccountInfo => true,
_ => c.is_init,
};
let associated_field = if is_find_nonce {
quote! {
let (__associated_field, nonce) = Pubkey::find_program_address(
&[#seeds_no_nonce],
@ -518,16 +646,27 @@ pub fn generate_constraint_state(f: &Field, c: &ConstraintState) -> proc_macro2:
}
// Returns the inner part of the seeds slice as a token stream.
fn to_seeds_tts(seeds: &[syn::Ident]) -> proc_macro2::TokenStream {
fn to_seeds_tts(seeds: &[syn::Expr]) -> proc_macro2::TokenStream {
assert!(seeds.len() > 0);
let seed_0 = &seeds[0];
let mut tts = quote! {
#seed_0.to_account_info().key.as_ref(),
let mut tts = match seed_0 {
syn::Expr::Path(_) => quote! {
anchor_lang::Key::key(&#seed_0).as_ref(),
},
_ => quote! {
#seed_0,
},
};
for seed in &seeds[1..] {
tts = quote! {
#tts
#seed.to_account_info().key.as_ref(),
tts = match seed {
syn::Expr::Path(_) => quote! {
#tts
anchor_lang::Key::key(&#seed).as_ref(),
},
_ => quote! {
#tts
#seed,
},
};
}
tts

View File

@ -268,6 +268,7 @@ pub struct ConstraintGroup {
literal: Vec<ConstraintLiteral>,
raw: Vec<ConstraintRaw>,
close: Option<ConstraintClose>,
address: Option<ConstraintAddress>,
}
impl ConstraintGroup {
@ -306,6 +307,7 @@ pub enum Constraint {
State(ConstraintState),
AssociatedGroup(ConstraintAssociatedGroup),
Close(ConstraintClose),
Address(ConstraintAddress),
}
// Constraint token is a single keyword in a `#[account(<TOKEN>)]` attribute.
@ -327,6 +329,9 @@ pub enum ConstraintToken {
AssociatedPayer(Context<ConstraintAssociatedPayer>),
AssociatedSpace(Context<ConstraintAssociatedSpace>),
AssociatedWith(Context<ConstraintAssociatedWith>),
Address(Context<ConstraintAddress>),
TokenMint(Context<ConstraintTokenMint>),
TokenAuthority(Context<ConstraintTokenAuthority>),
}
impl Parse for ConstraintToken {
@ -346,7 +351,7 @@ pub struct ConstraintSigner {}
#[derive(Debug, Clone)]
pub struct ConstraintBelongsTo {
pub join_target: Ident,
pub join_target: Expr,
}
#[derive(Debug, Clone)]
@ -361,7 +366,12 @@ pub struct ConstraintRaw {
#[derive(Debug, Clone)]
pub struct ConstraintOwner {
pub owner_target: Ident,
pub owner_target: Expr,
}
#[derive(Debug, Clone)]
pub struct ConstraintAddress {
pub address: Expr,
}
#[derive(Debug, Clone)]
@ -375,7 +385,8 @@ pub struct ConstraintSeedsGroup {
pub is_init: bool,
pub seeds: Punctuated<Expr, Token![,]>,
pub payer: Option<Ident>,
pub space: Option<LitInt>,
pub space: Option<Expr>,
pub kind: PdaKind,
}
#[derive(Debug, Clone)]
@ -394,15 +405,16 @@ pub struct ConstraintState {
#[derive(Debug, Clone)]
pub struct ConstraintAssociatedGroup {
pub is_init: bool,
pub associated_target: Ident,
pub associated_seeds: Vec<Ident>,
pub associated_target: Expr,
pub associated_seeds: Vec<Expr>,
pub payer: Option<Ident>,
pub space: Option<LitInt>,
pub space: Option<Expr>,
pub kind: PdaKind,
}
#[derive(Debug, Clone)]
pub struct ConstraintAssociated {
pub target: Ident,
pub target: Expr,
}
#[derive(Debug, Clone)]
@ -412,12 +424,18 @@ pub struct ConstraintAssociatedPayer {
#[derive(Debug, Clone)]
pub struct ConstraintAssociatedWith {
pub target: Ident,
pub target: Expr,
}
#[derive(Debug, Clone)]
pub struct ConstraintAssociatedSpace {
pub space: LitInt,
pub space: Expr,
}
#[derive(Debug, Clone)]
pub enum PdaKind {
Program,
Token { owner: Expr, mint: Expr },
}
#[derive(Debug, Clone)]
@ -425,6 +443,16 @@ pub struct ConstraintClose {
pub sol_dest: Ident,
}
#[derive(Debug, Clone)]
pub struct ConstraintTokenMint {
mint: Expr,
}
#[derive(Debug, Clone)]
pub struct ConstraintTokenAuthority {
auth: Expr,
}
// Syntaxt context object for preserving metadata about the inner item.
#[derive(Debug, Clone)]
pub struct Context<T> {

View File

@ -1,9 +1,10 @@
use crate::{
ConstraintAssociated, ConstraintAssociatedGroup, ConstraintAssociatedPayer,
ConstraintAddress, ConstraintAssociated, ConstraintAssociatedGroup, ConstraintAssociatedPayer,
ConstraintAssociatedSpace, ConstraintAssociatedWith, ConstraintBelongsTo, ConstraintClose,
ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut,
ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeeds, ConstraintSeedsGroup,
ConstraintSigner, ConstraintState, ConstraintToken, Context, Ty,
ConstraintSigner, ConstraintState, ConstraintToken, ConstraintTokenAuthority,
ConstraintTokenMint, Context, PdaKind, Ty,
};
use syn::ext::IdentExt;
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
@ -154,6 +155,24 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
sol_dest: stream.parse()?,
},
)),
"address" => ConstraintToken::Address(Context::new(
span,
ConstraintAddress {
address: stream.parse()?,
},
)),
"token" => ConstraintToken::TokenMint(Context::new(
ident.span(),
ConstraintTokenMint {
mint: stream.parse()?,
},
)),
"authority" => ConstraintToken::TokenAuthority(Context::new(
ident.span(),
ConstraintTokenAuthority {
auth: stream.parse()?,
},
)),
_ => Err(ParseError::new(ident.span(), "Invalid attribute"))?,
}
}
@ -181,6 +200,9 @@ pub struct ConstraintGroupBuilder<'ty> {
pub associated_space: Option<Context<ConstraintAssociatedSpace>>,
pub associated_with: Vec<Context<ConstraintAssociatedWith>>,
pub close: Option<Context<ConstraintClose>>,
pub address: Option<Context<ConstraintAddress>>,
pub token_mint: Option<Context<ConstraintTokenMint>>,
pub token_authority: Option<Context<ConstraintTokenAuthority>>,
}
impl<'ty> ConstraintGroupBuilder<'ty> {
@ -203,6 +225,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
associated_space: None,
associated_with: Vec::new(),
close: None,
address: None,
token_mint: None,
token_authority: None,
}
}
pub fn build(mut self) -> ParseResult<ConstraintGroup> {
@ -233,6 +258,15 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
}
}
if let Some(token_mint) = &self.token_mint {
if self.init.is_none() || (self.associated.is_none() && self.seeds.is_none()) {
return Err(ParseError::new(
token_mint.span(),
"init is required for a pda token",
));
}
}
let ConstraintGroupBuilder {
f_ty: _,
init,
@ -251,6 +285,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
associated_space,
associated_with,
close,
address,
token_mint,
token_authority,
} = self;
// Converts Option<Context<T>> -> Option<T>.
@ -279,12 +316,29 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
raw: into_inner_vec!(raw),
owner: into_inner!(owner),
rent_exempt: into_inner!(rent_exempt),
seeds: seeds.map(|c| ConstraintSeedsGroup {
is_init,
seeds: c.into_inner().seeds,
payer: into_inner!(associated_payer.clone()).map(|a| a.target),
space: associated_space.clone().map(|s| s.space.clone()),
}),
seeds: seeds
.map(|c| {
Ok(ConstraintSeedsGroup {
is_init,
seeds: c.into_inner().seeds,
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,
Some(tm) => PdaKind::Token {
mint: tm.clone().into_inner().mint,
owner: match &token_authority {
Some(a) => a.clone().into_inner().auth,
None => return Err(ParseError::new(
tm.span(),
"authority must be provided to initialize a token program derived address"
)),
},
},
},
})
})
.transpose()?,
executable: into_inner!(executable),
state: into_inner!(state),
associated: associated.map(|associated| ConstraintAssociatedGroup {
@ -293,8 +347,19 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
associated_seeds: associated_with.iter().map(|s| s.target.clone()).collect(),
payer: associated_payer.map(|p| p.target.clone()),
space: associated_space.map(|s| s.space.clone()),
kind: match token_mint {
None => PdaKind::Program,
Some(tm) => PdaKind::Token {
mint: tm.into_inner().mint,
owner: match token_authority {
Some(a) => a.into_inner().auth,
None => associated.target.clone(),
},
},
},
}),
close: into_inner!(close),
address: into_inner!(address),
})
}
@ -316,6 +381,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
ConstraintToken::AssociatedSpace(c) => self.add_associated_space(c),
ConstraintToken::AssociatedWith(c) => self.add_associated_with(c),
ConstraintToken::Close(c) => self.add_close(c),
ConstraintToken::Address(c) => self.add_address(c),
ConstraintToken::TokenAuthority(c) => self.add_token_authority(c),
ConstraintToken::TokenMint(c) => self.add_token_mint(c),
}
}
@ -349,6 +417,45 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
Ok(())
}
fn add_address(&mut self, c: Context<ConstraintAddress>) -> ParseResult<()> {
if self.address.is_some() {
return Err(ParseError::new(c.span(), "address already provided"));
}
self.address.replace(c);
Ok(())
}
fn add_token_mint(&mut self, c: Context<ConstraintTokenMint>) -> ParseResult<()> {
if self.token_mint.is_some() {
return Err(ParseError::new(c.span(), "token mint already provided"));
}
if self.init.is_none() {
return Err(ParseError::new(
c.span(),
"init must be provided before token",
));
}
self.token_mint.replace(c);
Ok(())
}
fn add_token_authority(&mut self, c: Context<ConstraintTokenAuthority>) -> ParseResult<()> {
if self.token_authority.is_some() {
return Err(ParseError::new(
c.span(),
"token authority already provided",
));
}
if self.token_mint.is_none() {
return Err(ParseError::new(
c.span(),
"token must bne provided before authority",
));
}
self.token_authority.replace(c);
Ok(())
}
fn add_mut(&mut self, c: Context<ConstraintMut>) -> ParseResult<()> {
if self.mutable.is_some() {
return Err(ParseError::new(c.span(), "mut already provided"));

View File

@ -117,6 +117,24 @@ pub fn close_open_orders<'info>(
Ok(())
}
pub fn sweep_fees<'info>(ctx: CpiContext<'_, '_, '_, 'info, SweepFees<'info>>) -> ProgramResult {
let ix = serum_dex::instruction::sweep_fees(
&ID,
ctx.accounts.market.key,
ctx.accounts.pc_vault.key,
ctx.accounts.sweep_authority.key,
ctx.accounts.sweep_receiver.key,
ctx.accounts.vault_signer.key,
ctx.accounts.token_program.key,
)?;
solana_program::program::invoke_signed(
&ix,
&ToAccountInfos::to_account_infos(&ctx),
ctx.signer_seeds,
)?;
Ok(())
}
#[derive(Accounts)]
pub struct NewOrderV3<'info> {
pub market: AccountInfo<'info>,
@ -167,3 +185,13 @@ pub struct CloseOpenOrders<'info> {
pub destination: AccountInfo<'info>,
pub market: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct SweepFees<'info> {
pub market: AccountInfo<'info>,
pub pc_vault: AccountInfo<'info>,
pub sweep_authority: AccountInfo<'info>,
pub sweep_receiver: AccountInfo<'info>,
pub vault_signer: AccountInfo<'info>,
pub token_program: AccountInfo<'info>,
}

View File

@ -1,3 +1,4 @@
pub mod dex;
pub mod mint;
pub mod shmem;
pub mod token;

13
spl/src/mint.rs Normal file
View File

@ -0,0 +1,13 @@
use anchor_lang::solana_program::declare_id;
pub use srm::ID as SRM;
mod srm {
use super::*;
declare_id!("SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt");
}
pub use usdc::ID as USDC;
mod usdc {
use super::*;
declare_id!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
}

View File

@ -201,6 +201,10 @@ pub struct SetAuthority<'info> {
#[derive(Clone)]
pub struct TokenAccount(spl_token::state::Account);
impl TokenAccount {
pub const LEN: usize = spl_token::state::Account::LEN;
}
impl anchor_lang::AccountDeserialize for TokenAccount {
fn try_deserialize(buf: &mut &[u8]) -> Result<Self, ProgramError> {
TokenAccount::try_deserialize_unchecked(buf)

View File

@ -69,6 +69,7 @@ const LangErrorCode = {
ConstraintAssociated: 149,
ConstraintAssociatedInit: 150,
ConstraintClose: 151,
ConstraintAddress: 152,
// Accounts.
AccountDiscriminatorAlreadySet: 160,
@ -132,6 +133,7 @@ const LangErrorMessage = new Map([
"An associated init constraint was violated",
],
[LangErrorCode.ConstraintClose, "A close constraint was violated"],
[LangErrorCode.ConstraintAddress, "An address constraint was violated"],
// Accounts.
[

View File

@ -250,14 +250,8 @@ export class Program {
this._coder = new Coder(idl);
// Dynamic namespaces.
const [
rpc,
instruction,
transaction,
account,
simulate,
state,
] = NamespaceFactory.build(idl, this._coder, programId, this._provider);
const [rpc, instruction, transaction, account, simulate, state] =
NamespaceFactory.build(idl, this._coder, programId, this._provider);
this.rpc = rpc;
this.instruction = instruction;
this.transaction = transaction;

View File

@ -17,6 +17,7 @@ import Coder, {
} from "../../coder";
import { Subscription, Address, translateAddress } from "../common";
import { getProvider } from "../../";
import * as pubkeyUtil from "../../utils/pubkey";
export default class AccountFactory {
public static build(
@ -234,9 +235,10 @@ export class AccountClient {
fromPubkey: this._provider.wallet.publicKey,
newAccountPubkey: signer.publicKey,
space: sizeOverride ?? size,
lamports: await this._provider.connection.getMinimumBalanceForRentExemption(
sizeOverride ?? size
),
lamports:
await this._provider.connection.getMinimumBalanceForRentExemption(
sizeOverride ?? size
),
programId: this._programId,
});
}
@ -245,7 +247,7 @@ export class AccountClient {
* Function returning the associated account. Args are keys to associate.
* Order matters.
*/
async associated(...args: PublicKey[]): Promise<any> {
async associated(...args: Array<PublicKey | Buffer>): Promise<any> {
const addr = await this.associatedAddress(...args);
return await this.fetch(addr);
}
@ -254,13 +256,10 @@ export class AccountClient {
* Function returning the associated address. Args are keys to associate.
* Order matters.
*/
async associatedAddress(...args: PublicKey[]): Promise<PublicKey> {
let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor".
args.forEach((arg) => {
seeds.push(translateAddress(arg).toBuffer());
});
const [assoc] = await PublicKey.findProgramAddress(seeds, this._programId);
return assoc;
async associatedAddress(
...args: Array<PublicKey | Buffer>
): Promise<PublicKey> {
return await pubkeyUtil.associated(this._programId, ...args);
}
}

View File

@ -2,3 +2,4 @@ export * as sha256 from "./sha256";
export * as rpc from "./rpc";
export * as publicKey from "./pubkey";
export * as bytes from "./bytes";
export * as token from "./token";

View File

@ -1,6 +1,7 @@
import BN from "bn.js";
import { sha256 as sha256Sync } from "js-sha256";
import { PublicKey } from "@solana/web3.js";
import { Address, translateAddress } from "../program/common";
// Sync version of web3.PublicKey.createWithSeed.
export function createWithSeedSync(
@ -76,3 +77,21 @@ const toBuffer = (arr: Buffer | Uint8Array | Array<number>): Buffer => {
return Buffer.from(arr);
}
};
export async function associated(
programId: Address,
...args: Array<PublicKey | Buffer>
): Promise<PublicKey> {
let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor".
args.forEach((arg) => {
seeds.push(
// @ts-ignore
arg.buffer !== undefined ? arg : translateAddress(arg).toBuffer()
);
});
const [assoc] = await PublicKey.findProgramAddress(
seeds,
translateAddress(programId)
);
return assoc;
}

23
ts/src/utils/token.ts Normal file
View File

@ -0,0 +1,23 @@
import { PublicKey } from "@solana/web3.js";
const TOKEN_PROGRAM_ID = new PublicKey(
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
);
const ASSOCIATED_PROGRAM_ID = new PublicKey(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
);
export async function associatedAddress({
mint,
owner,
}: {
mint: PublicKey;
owner: PublicKey;
}): Promise<PublicKey> {
return (
await PublicKey.findProgramAddress(
[owner.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()],
ASSOCIATED_PROGRAM_ID
)
)[0];
}

View File

@ -84,16 +84,20 @@ const workspace = new Proxy({} as any, {
function attachWorkspaceOverride(
workspaceCache: { [key: string]: Program },
overrideConfig: { [key: string]: string },
overrideConfig: { [key: string]: string | { address: string; idl?: string } },
idlMap: Map<string, Idl>
) {
Object.keys(overrideConfig).forEach((programName) => {
const wsProgramName = camelCase(programName, { pascalCase: true });
const overrideAddress = new PublicKey(overrideConfig[programName]);
workspaceCache[wsProgramName] = new Program(
idlMap.get(programName),
overrideAddress
const entry = overrideConfig[programName];
const overrideAddress = new PublicKey(
typeof entry === "string" ? entry : entry.address
);
let idl = idlMap.get(programName);
if (typeof entry !== "string" && entry.idl) {
idl = JSON.parse(require("fs").readFileSync(entry.idl, "utf-8"));
}
workspaceCache[wsProgramName] = new Program(idl, overrideAddress);
});
}