Token PDAs and CLI scripts (#400)
This commit is contained in:
parent
609c836d63
commit
f067624add
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -15,5 +15,6 @@ members = [
|
|||
"spl",
|
||||
]
|
||||
exclude = [
|
||||
"examples/swap/deps/serum-dex"
|
||||
"examples/swap/deps/serum-dex",
|
||||
"examples/cfo/deps/serum-dex",
|
||||
]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
|
@ -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
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
// Migrations are an early feature. Currently, they're nothing more than this
|
||||
// single deploy script that's invoked from the CLI, injecting a provider
|
||||
// configured from the workspace's Anchor.toml.
|
||||
|
||||
const anchor = require("@project-serum/anchor");
|
||||
|
||||
module.exports = async function (provider) {
|
||||
// Configure client to use the provider.
|
||||
anchor.setProvider(provider);
|
||||
|
||||
// Add your deploy script here.
|
||||
}
|
|
@ -0,0 +1,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"] }
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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));
|
||||
}
|
|
@ -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();
|
|
@ -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
|
|
@ -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]
|
|
@ -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();
|
|
@ -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
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -16,4 +16,5 @@ default = []
|
|||
|
||||
[dependencies]
|
||||
anchor-lang = { path = "../../../../lang" }
|
||||
anchor-spl = { path = "../../../../spl" }
|
||||
misc2 = { path = "../misc2", features = ["cpi"] }
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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)][..],
|
||||
]
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod dex;
|
||||
pub mod mint;
|
||||
pub mod shmem;
|
||||
pub mod token;
|
||||
|
|
|
@ -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");
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
[
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue