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"]
|
[submodule "examples/swap/deps/serum-dex"]
|
||||||
path = examples/swap/deps/serum-dex
|
path = examples/swap/deps/serum-dex
|
||||||
url = https://github.com/project-serum/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/chat && yarn && anchor test && popd
|
||||||
- pushd examples/ido-pool && 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/swap/deps/serum-dex/dex && cargo build-bpf && cd ../../../ && anchor test && popd
|
||||||
|
- pushd examples/cfo && anchor run test && popd
|
||||||
- <<: *examples
|
- <<: *examples
|
||||||
name: Runs the examples 3
|
name: Runs the examples 3
|
||||||
script:
|
script:
|
||||||
|
|
|
@ -11,6 +11,14 @@ incremented for features.
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.9.0] - 2021-06-15
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
|
@ -214,6 +214,7 @@ dependencies = [
|
||||||
"bs58",
|
"bs58",
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2 1.0.24",
|
||||||
|
"proc-macro2-diagnostics",
|
||||||
"quote 1.0.9",
|
"quote 1.0.9",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -2270,6 +2271,19 @@ dependencies = [
|
||||||
"unicode-xid 0.2.1",
|
"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]]
|
[[package]]
|
||||||
name = "qstring"
|
name = "qstring"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
|
@ -4312,6 +4326,12 @@ dependencies = [
|
||||||
"linked-hash-map",
|
"linked-hash-map",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yansi"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zeroize"
|
name = "zeroize"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|
|
@ -15,5 +15,6 @@ members = [
|
||||||
"spl",
|
"spl",
|
||||||
]
|
]
|
||||||
exclude = [
|
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::pubkey::Pubkey;
|
||||||
use solana_sdk::signature::Keypair;
|
use solana_sdk::signature::Keypair;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
@ -16,6 +17,7 @@ use std::str::FromStr;
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub provider: ProviderConfig,
|
pub provider: ProviderConfig,
|
||||||
pub clusters: ClustersConfig,
|
pub clusters: ClustersConfig,
|
||||||
|
pub scripts: ScriptsConfig,
|
||||||
pub test: Option<Test>,
|
pub test: Option<Test>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +27,8 @@ pub struct ProviderConfig {
|
||||||
pub wallet: WalletPath,
|
pub wallet: WalletPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type ScriptsConfig = BTreeMap<String, String>;
|
||||||
|
|
||||||
pub type ClustersConfig = BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>;
|
pub type ClustersConfig = BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>;
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
@ -100,7 +104,8 @@ impl Config {
|
||||||
struct _Config {
|
struct _Config {
|
||||||
provider: Provider,
|
provider: Provider,
|
||||||
test: Option<Test>,
|
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)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
@ -125,6 +130,10 @@ impl ToString for Config {
|
||||||
wallet: self.provider.wallet.to_string(),
|
wallet: self.provider.wallet.to_string(),
|
||||||
},
|
},
|
||||||
test: self.test.clone(),
|
test: self.test.clone(),
|
||||||
|
scripts: match self.scripts.is_empty() {
|
||||||
|
true => None,
|
||||||
|
false => Some(self.scripts.clone()),
|
||||||
|
},
|
||||||
clusters,
|
clusters,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -143,6 +152,7 @@ impl FromStr for Config {
|
||||||
cluster: cfg.provider.cluster.parse()?,
|
cluster: cfg.provider.cluster.parse()?,
|
||||||
wallet: shellexpand::tilde(&cfg.provider.wallet).parse()?,
|
wallet: shellexpand::tilde(&cfg.provider.wallet).parse()?,
|
||||||
},
|
},
|
||||||
|
scripts: cfg.scripts.unwrap_or_else(|| BTreeMap::new()),
|
||||||
test: cfg.test,
|
test: cfg.test,
|
||||||
clusters: cfg
|
clusters: cfg
|
||||||
.clusters
|
.clusters
|
||||||
|
@ -153,22 +163,27 @@ impl FromStr for Config {
|
||||||
|
|
||||||
fn ser_clusters(
|
fn ser_clusters(
|
||||||
clusters: &BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>,
|
clusters: &BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>,
|
||||||
) -> BTreeMap<String, BTreeMap<String, String>> {
|
) -> BTreeMap<String, BTreeMap<String, serde_json::Value>> {
|
||||||
clusters
|
clusters
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(cluster, programs)| {
|
.map(|(cluster, programs)| {
|
||||||
let cluster = cluster.to_string();
|
let cluster = cluster.to_string();
|
||||||
let programs = programs
|
let programs = programs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(name, deployment)| (name.clone(), deployment.program_id.to_string()))
|
.map(|(name, deployment)| {
|
||||||
.collect::<BTreeMap<String, String>>();
|
(
|
||||||
|
name.clone(),
|
||||||
|
serde_json::to_value(&_ProgramDeployment::from(deployment)).unwrap(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<BTreeMap<String, serde_json::Value>>();
|
||||||
(cluster, programs)
|
(cluster, programs)
|
||||||
})
|
})
|
||||||
.collect::<BTreeMap<String, BTreeMap<String, String>>>()
|
.collect::<BTreeMap<String, BTreeMap<String, serde_json::Value>>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deser_clusters(
|
fn deser_clusters(
|
||||||
clusters: BTreeMap<String, BTreeMap<String, String>>,
|
clusters: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
|
||||||
) -> Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>> {
|
) -> Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>> {
|
||||||
clusters
|
clusters
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -179,10 +194,17 @@ fn deser_clusters(
|
||||||
.map(|(name, program_id)| {
|
.map(|(name, program_id)| {
|
||||||
Ok((
|
Ok((
|
||||||
name.clone(),
|
name.clone(),
|
||||||
ProgramDeployment {
|
ProgramDeployment::try_from(match &program_id {
|
||||||
name: name.clone(),
|
serde_json::Value::String(address) => _ProgramDeployment {
|
||||||
program_id: program_id.parse()?,
|
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>>>()?;
|
.collect::<Result<BTreeMap<String, ProgramDeployment>>>()?;
|
||||||
|
@ -269,8 +291,33 @@ impl Program {
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct ProgramDeployment {
|
pub struct ProgramDeployment {
|
||||||
pub name: String,
|
pub address: Pubkey,
|
||||||
pub program_id: 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 {
|
pub struct ProgramWorkspace {
|
||||||
|
|
|
@ -147,6 +147,11 @@ pub enum Command {
|
||||||
/// Starts a node shell with an Anchor client setup according to the local
|
/// Starts a node shell with an Anchor client setup according to the local
|
||||||
/// config.
|
/// config.
|
||||||
Shell,
|
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)]
|
#[derive(Debug, Clap)]
|
||||||
|
@ -267,6 +272,7 @@ fn main() -> Result<()> {
|
||||||
Command::Airdrop => airdrop(cfg_override),
|
Command::Airdrop => airdrop(cfg_override),
|
||||||
Command::Cluster { subcmd } => cluster(subcmd),
|
Command::Cluster { subcmd } => cluster(subcmd),
|
||||||
Command::Shell => shell(&opts.cfg_override),
|
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<()> {
|
fn shell(cfg_override: &ConfigOverride) -> Result<()> {
|
||||||
with_workspace(cfg_override, |cfg, _path, _cargo| {
|
with_workspace(cfg_override, |cfg, _path, _cargo| {
|
||||||
let programs = {
|
let programs = {
|
||||||
let idls: HashMap<String, Idl> = read_all_programs()?
|
let mut idls: HashMap<String, Idl> = read_all_programs()?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|program| (program.idl.name.clone(), program.idl.clone()))
|
.map(|program| (program.idl.name.clone(), program.idl.clone()))
|
||||||
.collect();
|
.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) {
|
match cfg.clusters.get(&cfg.provider.cluster) {
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
Some(programs) => programs
|
Some(programs) => programs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(name, program_deployment)| ProgramWorkspace {
|
.map(|(name, program_deployment)| ProgramWorkspace {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
program_id: program_deployment.program_id,
|
program_id: program_deployment.address,
|
||||||
idl: match idls.get(name) {
|
idl: match idls.get(name) {
|
||||||
None => {
|
None => {
|
||||||
println!("Unable to find IDL for {}", name);
|
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
|
// with_workspace ensures the current working directory is always the top level
|
||||||
// workspace directory, i.e., where the `Anchor.toml` file is located, before
|
// workspace directory, i.e., where the `Anchor.toml` file is located, before
|
||||||
// and after the closure invocation.
|
// 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]
|
[dependencies]
|
||||||
anchor-lang = { path = "../../../../lang" }
|
anchor-lang = { path = "../../../../lang" }
|
||||||
|
anchor-spl = { path = "../../../../spl" }
|
||||||
misc2 = { path = "../misc2", features = ["cpi"] }
|
misc2 = { path = "../misc2", features = ["cpi"] }
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
//! It's not too instructive/coherent by itself, so please see other examples.
|
//! It's not too instructive/coherent by itself, so please see other examples.
|
||||||
|
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
use anchor_spl::token::{Mint, TokenAccount};
|
||||||
use misc2::misc2::MyState as Misc2State;
|
use misc2::misc2::MyState as Misc2State;
|
||||||
use misc2::Auth;
|
use misc2::Auth;
|
||||||
|
|
||||||
|
@ -123,6 +124,29 @@ pub mod misc {
|
||||||
acc.data = 1234;
|
acc.data = 1234;
|
||||||
Ok(())
|
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)]
|
#[derive(Accounts)]
|
||||||
|
@ -219,7 +243,7 @@ pub struct TestClose<'info> {
|
||||||
// the program.
|
// the program.
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
pub struct TestInitAssociatedAccount<'info> {
|
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>,
|
my_account: ProgramAccount<'info, TestData>,
|
||||||
#[account(mut, signer)]
|
#[account(mut, signer)]
|
||||||
authority: AccountInfo<'info>,
|
authority: AccountInfo<'info>,
|
||||||
|
@ -231,7 +255,7 @@ pub struct TestInitAssociatedAccount<'info> {
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
pub struct TestAssociatedAccount<'info> {
|
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>,
|
my_account: ProgramAccount<'info, TestData>,
|
||||||
#[account(mut, signer)]
|
#[account(mut, signer)]
|
||||||
authority: AccountInfo<'info>,
|
authority: AccountInfo<'info>,
|
||||||
|
|
|
@ -2,6 +2,7 @@ const anchor = require("@project-serum/anchor");
|
||||||
const PublicKey = anchor.web3.PublicKey;
|
const PublicKey = anchor.web3.PublicKey;
|
||||||
const serumCmn = require("@project-serum/common");
|
const serumCmn = require("@project-serum/common");
|
||||||
const assert = require("assert");
|
const assert = require("assert");
|
||||||
|
const { TOKEN_PROGRAM_ID, Token } = require("@solana/spl-token");
|
||||||
|
|
||||||
describe("misc", () => {
|
describe("misc", () => {
|
||||||
// Configure the client to use the local cluster.
|
// Configure the client to use the local cluster.
|
||||||
|
@ -140,18 +141,17 @@ describe("misc", () => {
|
||||||
|
|
||||||
// Manual associated address calculation for test only. Clients should use
|
// Manual associated address calculation for test only. Clients should use
|
||||||
// the generated methods.
|
// the generated methods.
|
||||||
const [
|
const [associatedAccount, nonce] =
|
||||||
associatedAccount,
|
await anchor.web3.PublicKey.findProgramAddress(
|
||||||
nonce,
|
[
|
||||||
] = await anchor.web3.PublicKey.findProgramAddress(
|
anchor.utils.bytes.utf8.encode("anchor"),
|
||||||
[
|
program.provider.wallet.publicKey.toBuffer(),
|
||||||
Buffer.from([97, 110, 99, 104, 111, 114]), // b"anchor".
|
state.toBuffer(),
|
||||||
program.provider.wallet.publicKey.toBuffer(),
|
data.publicKey.toBuffer(),
|
||||||
state.toBuffer(),
|
anchor.utils.bytes.utf8.encode("my-seed"),
|
||||||
data.publicKey.toBuffer(),
|
],
|
||||||
],
|
program.programId
|
||||||
program.programId
|
);
|
||||||
);
|
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
async () => {
|
async () => {
|
||||||
await program.account.testData.fetch(associatedAccount);
|
await program.account.testData.fetch(associatedAccount);
|
||||||
|
@ -178,25 +178,25 @@ describe("misc", () => {
|
||||||
const account = await program.account.testData.associated(
|
const account = await program.account.testData.associated(
|
||||||
program.provider.wallet.publicKey,
|
program.provider.wallet.publicKey,
|
||||||
state,
|
state,
|
||||||
data.publicKey
|
data.publicKey,
|
||||||
|
anchor.utils.bytes.utf8.encode("my-seed")
|
||||||
);
|
);
|
||||||
assert.ok(account.data.toNumber() === 1234);
|
assert.ok(account.data.toNumber() === 1234);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Can use an associated program account", async () => {
|
it("Can use an associated program account", async () => {
|
||||||
const state = await program.state.address();
|
const state = await program.state.address();
|
||||||
const [
|
const [associatedAccount, nonce] =
|
||||||
associatedAccount,
|
await anchor.web3.PublicKey.findProgramAddress(
|
||||||
nonce,
|
[
|
||||||
] = await anchor.web3.PublicKey.findProgramAddress(
|
anchor.utils.bytes.utf8.encode("anchor"),
|
||||||
[
|
program.provider.wallet.publicKey.toBuffer(),
|
||||||
Buffer.from([97, 110, 99, 104, 111, 114]), // b"anchor".
|
state.toBuffer(),
|
||||||
program.provider.wallet.publicKey.toBuffer(),
|
data.publicKey.toBuffer(),
|
||||||
state.toBuffer(),
|
anchor.utils.bytes.utf8.encode("my-seed"),
|
||||||
data.publicKey.toBuffer(),
|
],
|
||||||
],
|
program.programId
|
||||||
program.programId
|
);
|
||||||
);
|
|
||||||
await program.rpc.testAssociatedAccount(new anchor.BN(5), {
|
await program.rpc.testAssociatedAccount(new anchor.BN(5), {
|
||||||
accounts: {
|
accounts: {
|
||||||
myAccount: associatedAccount,
|
myAccount: associatedAccount,
|
||||||
|
@ -209,7 +209,8 @@ describe("misc", () => {
|
||||||
const account = await program.account.testData.associated(
|
const account = await program.account.testData.associated(
|
||||||
program.provider.wallet.publicKey,
|
program.provider.wallet.publicKey,
|
||||||
state,
|
state,
|
||||||
data.publicKey
|
data.publicKey,
|
||||||
|
anchor.utils.bytes.utf8.encode("my-seed")
|
||||||
);
|
);
|
||||||
assert.ok(account.data.toNumber() === 5);
|
assert.ok(account.data.toNumber() === 5);
|
||||||
});
|
});
|
||||||
|
@ -402,4 +403,36 @@ describe("misc", () => {
|
||||||
assert.ok(myPdaAccount.data === 1234);
|
assert.ok(myPdaAccount.data === 1234);
|
||||||
assert.ok((myPdaAccount.bump = bump));
|
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
|
/// Reloads the account from storage. This is useful, for example, when
|
||||||
/// observing side effects after CPI.
|
/// observing side effects after CPI.
|
||||||
pub fn reload(&self) -> Result<CpiAccount<'a, T>, ProgramError> {
|
pub fn reload(&self) -> Result<CpiAccount<'a, T>, ProgramError> {
|
||||||
|
|
|
@ -44,6 +44,8 @@ pub enum ErrorCode {
|
||||||
ConstraintAssociatedInit,
|
ConstraintAssociatedInit,
|
||||||
#[msg("A close constraint was violated")]
|
#[msg("A close constraint was violated")]
|
||||||
ConstraintClose,
|
ConstraintClose,
|
||||||
|
#[msg("An address constraint was violated")]
|
||||||
|
ConstraintAddress,
|
||||||
|
|
||||||
// Accounts.
|
// Accounts.
|
||||||
#[msg("The account discriminator was already set on this account")]
|
#[msg("The account discriminator was already set on this account")]
|
||||||
|
|
|
@ -210,6 +210,25 @@ pub trait Bump {
|
||||||
fn seed(&self) -> u8;
|
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.
|
/// The prelude contains all commonly used components of the crate.
|
||||||
/// All programs should include it via `anchor_lang::prelude::*;`.
|
/// All programs should include it via `anchor_lang::prelude::*;`.
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
|
@ -287,3 +306,24 @@ pub mod __private {
|
||||||
pub use crate::state::PROGRAM_STATE_SEED;
|
pub use crate::state::PROGRAM_STATE_SEED;
|
||||||
pub const CLOSED_ACCOUNT_DISCRIMINATOR: [u8; 8] = [255, 255, 255, 255, 255, 255, 255, 255];
|
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)?,
|
T::try_deserialize_unchecked(&mut data)?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_inner(self) -> T {
|
||||||
|
self.inner.account
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'info, T> Accounts<'info> for ProgramAccount<'info, T>
|
impl<'info, T> Accounts<'info> for ProgramAccount<'info, T>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
CompositeField, Constraint, ConstraintAssociatedGroup, ConstraintBelongsTo, ConstraintClose,
|
CompositeField, Constraint, ConstraintAddress, ConstraintAssociatedGroup, ConstraintBelongsTo,
|
||||||
ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut,
|
ConstraintClose, ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral,
|
||||||
ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeedsGroup, ConstraintSigner,
|
ConstraintMut, ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeedsGroup,
|
||||||
ConstraintState, Field, Ty,
|
ConstraintSigner, ConstraintState, Field, PdaKind, Ty,
|
||||||
};
|
};
|
||||||
use proc_macro2_diagnostics::SpanDiagnosticExt;
|
use proc_macro2_diagnostics::SpanDiagnosticExt;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::LitInt;
|
use syn::Expr;
|
||||||
|
|
||||||
pub fn generate(f: &Field) -> proc_macro2::TokenStream {
|
pub fn generate(f: &Field) -> proc_macro2::TokenStream {
|
||||||
let checks: Vec<proc_macro2::TokenStream> = linearize(&f.constraints)
|
let checks: Vec<proc_macro2::TokenStream> = linearize(&f.constraints)
|
||||||
|
@ -53,6 +53,7 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec<Constraint> {
|
||||||
state,
|
state,
|
||||||
associated,
|
associated,
|
||||||
close,
|
close,
|
||||||
|
address,
|
||||||
} = c_group.clone();
|
} = c_group.clone();
|
||||||
|
|
||||||
let mut constraints = Vec::new();
|
let mut constraints = Vec::new();
|
||||||
|
@ -100,6 +101,9 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec<Constraint> {
|
||||||
if let Some(c) = close {
|
if let Some(c) = close {
|
||||||
constraints.push(Constraint::Close(c));
|
constraints.push(Constraint::Close(c));
|
||||||
}
|
}
|
||||||
|
if let Some(c) = address {
|
||||||
|
constraints.push(Constraint::Address(c));
|
||||||
|
}
|
||||||
constraints
|
constraints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,6 +122,7 @@ fn generate_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStream {
|
||||||
Constraint::State(c) => generate_constraint_state(f, c),
|
Constraint::State(c) => generate_constraint_state(f, c),
|
||||||
Constraint::AssociatedGroup(c) => generate_constraint_associated(f, c),
|
Constraint::AssociatedGroup(c) => generate_constraint_associated(f, c),
|
||||||
Constraint::Close(c) => generate_constraint_close(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 {
|
pub fn generate_constraint_init(_f: &Field, _c: &ConstraintInit) -> proc_macro2::TokenStream {
|
||||||
quote! {}
|
quote! {}
|
||||||
}
|
}
|
||||||
|
@ -232,11 +247,8 @@ pub fn generate_constraint_rent_exempt(
|
||||||
c: &ConstraintRentExempt,
|
c: &ConstraintRentExempt,
|
||||||
) -> proc_macro2::TokenStream {
|
) -> proc_macro2::TokenStream {
|
||||||
let ident = &f.ident;
|
let ident = &f.ident;
|
||||||
let info = match f.ty {
|
let info = quote! {
|
||||||
Ty::AccountInfo => quote! { #ident },
|
#ident.to_account_info()
|
||||||
Ty::ProgramAccount(_) => quote! { #ident.to_account_info() },
|
|
||||||
Ty::Loader(_) => quote! { #ident.to_account_info() },
|
|
||||||
_ => panic!("Invalid syntax: rent exemption cannot be specified."),
|
|
||||||
};
|
};
|
||||||
match c {
|
match c {
|
||||||
ConstraintRentExempt::Skip => quote! {},
|
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 payer = #p.to_account_info();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let seeds_constraint = generate_constraint_seeds_address(f, c);
|
||||||
let seeds_with_nonce = {
|
let seeds_with_nonce = {
|
||||||
let s = &c.seeds;
|
let s = &c.seeds;
|
||||||
let seeds_constraint = generate_constraint_seeds_address(f, c);
|
|
||||||
quote! {
|
quote! {
|
||||||
#seeds_constraint
|
[#s]
|
||||||
let seeds = [#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(
|
fn generate_constraint_seeds_address(
|
||||||
|
@ -315,44 +334,78 @@ pub fn generate_constraint_associated_init(
|
||||||
let payer = #p.to_account_info();
|
let payer = #p.to_account_info();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let associated_seeds_constraint = generate_constraint_associated_seeds(f, c);
|
let seeds_constraint = generate_constraint_associated_seeds(f, c);
|
||||||
let seeds_with_nonce = if c.associated_seeds.is_empty() {
|
let seeds_with_nonce = {
|
||||||
quote! {
|
if c.associated_seeds.is_empty() {
|
||||||
#associated_seeds_constraint
|
quote! {
|
||||||
let seeds = [
|
[
|
||||||
&b"anchor"[..],
|
&b"anchor"[..],
|
||||||
#associated_target.to_account_info().key.as_ref(),
|
#associated_target.to_account_info().key.as_ref(),
|
||||||
&[nonce],
|
&[nonce],
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let seeds = to_seeds_tts(&c.associated_seeds);
|
let seeds = to_seeds_tts(&c.associated_seeds);
|
||||||
quote! {
|
quote! {
|
||||||
#associated_seeds_constraint
|
[
|
||||||
let seeds = [
|
&b"anchor"[..],
|
||||||
&b"anchor"[..],
|
#associated_target.to_account_info().key.as_ref(),
|
||||||
#associated_target.to_account_info().key.as_ref(),
|
#seeds
|
||||||
#seeds
|
&[nonce],
|
||||||
&[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(
|
pub fn generate_pda(
|
||||||
f: &Field,
|
f: &Field,
|
||||||
|
seeds_constraint: proc_macro2::TokenStream,
|
||||||
seeds_with_nonce: proc_macro2::TokenStream,
|
seeds_with_nonce: proc_macro2::TokenStream,
|
||||||
payer: proc_macro2::TokenStream,
|
payer: proc_macro2::TokenStream,
|
||||||
space: &Option<LitInt>,
|
space: &Option<Expr>,
|
||||||
assign_nonce: bool,
|
assign_nonce: bool,
|
||||||
|
kind: &PdaKind,
|
||||||
) -> proc_macro2::TokenStream {
|
) -> proc_macro2::TokenStream {
|
||||||
let field = &f.ident;
|
let field = &f.ident;
|
||||||
let (account_ty, is_zero_copy) = match &f.ty {
|
let (account_ty, account_wrapper_ty, is_zero_copy) = parse_ty(&f);
|
||||||
Ty::ProgramAccount(ty) => (&ty.account_ident, false),
|
|
||||||
Ty::Loader(ty) => (&ty.account_ident, true),
|
|
||||||
_ => panic!("Invalid type for initializing a program derived address"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let space = match space {
|
let space = match space {
|
||||||
// If no explicit space param was given, serialize the type to bytes
|
// 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 {
|
let nonce_assignment = match assign_nonce {
|
||||||
false => quote! {},
|
false => quote! {},
|
||||||
true => match is_zero_copy {
|
true => match &f.ty {
|
||||||
false => quote! {
|
Ty::CpiAccount(_) => quote! {},
|
||||||
pa.__nonce = nonce;
|
_ => match is_zero_copy {
|
||||||
},
|
false => quote! {
|
||||||
// Zero copy is not deserialized, so the data must be lazy loaded.
|
pa.__nonce = nonce;
|
||||||
true => quote! {
|
},
|
||||||
pa.load_init()?.__nonce = nonce;
|
// Zero copy is not deserialized, so the data must be lazy loaded.
|
||||||
|
true => quote! {
|
||||||
|
pa.load_init()?.__nonce = nonce;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
quote! {
|
match kind {
|
||||||
let #field: #account_wrapper_ty<#account_ty> = {
|
PdaKind::Token { owner, mint } => quote! {
|
||||||
#space
|
let #field: #account_wrapper_ty<#account_ty> = {
|
||||||
#payer
|
#space
|
||||||
|
#payer
|
||||||
|
#seeds_constraint
|
||||||
|
|
||||||
let lamports = rent.minimum_balance(space);
|
// Fund the account for rent exemption.
|
||||||
let ix = anchor_lang::solana_program::system_instruction::create_account(
|
let required_lamports = rent
|
||||||
payer.to_account_info().key,
|
.minimum_balance(anchor_spl::token::TokenAccount::LEN)
|
||||||
#field.to_account_info().key,
|
.max(1)
|
||||||
lamports,
|
.saturating_sub(#field.to_account_info().lamports());
|
||||||
space as u64,
|
if required_lamports > 0 {
|
||||||
program_id,
|
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
|
// Allocate space.
|
||||||
let signer = &[&seeds[..]];
|
anchor_lang::solana_program::program::invoke_signed(
|
||||||
anchor_lang::solana_program::program::invoke_signed(
|
&anchor_lang::solana_program::system_instruction::allocate(
|
||||||
&ix,
|
#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(),
|
// Assign to the spl token program.
|
||||||
payer.to_account_info(),
|
let __ix = anchor_lang::solana_program::system_instruction::assign(
|
||||||
system_program.to_account_info(),
|
#field.to_account_info().key,
|
||||||
],
|
token_program.to_account_info().key,
|
||||||
signer,
|
);
|
||||||
).map_err(|e| {
|
anchor_lang::solana_program::program::invoke_signed(
|
||||||
anchor_lang::solana_program::msg!("Unable to create associated account");
|
&__ix,
|
||||||
e
|
&[
|
||||||
})?;
|
#field.to_account_info(),
|
||||||
// For now, we assume all accounts created with the `associated`
|
system_program.to_account_info(),
|
||||||
// attribute have a `nonce` field in their account.
|
],
|
||||||
let mut pa: #account_wrapper_ty<#account_ty> = #account_wrapper_ty::try_from_init(
|
&[&#seeds_with_nonce[..]],
|
||||||
&#field.to_account_info(),
|
)?;
|
||||||
)?;
|
|
||||||
#nonce_assignment
|
// Initialize the token account.
|
||||||
pa
|
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
|
#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! {
|
quote! {
|
||||||
let (__associated_field, nonce) = Pubkey::find_program_address(
|
let (__associated_field, nonce) = Pubkey::find_program_address(
|
||||||
&[#seeds_no_nonce],
|
&[#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.
|
// 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);
|
assert!(seeds.len() > 0);
|
||||||
let seed_0 = &seeds[0];
|
let seed_0 = &seeds[0];
|
||||||
let mut tts = quote! {
|
let mut tts = match seed_0 {
|
||||||
#seed_0.to_account_info().key.as_ref(),
|
syn::Expr::Path(_) => quote! {
|
||||||
|
anchor_lang::Key::key(&#seed_0).as_ref(),
|
||||||
|
},
|
||||||
|
_ => quote! {
|
||||||
|
#seed_0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
for seed in &seeds[1..] {
|
for seed in &seeds[1..] {
|
||||||
tts = quote! {
|
tts = match seed {
|
||||||
#tts
|
syn::Expr::Path(_) => quote! {
|
||||||
#seed.to_account_info().key.as_ref(),
|
#tts
|
||||||
|
anchor_lang::Key::key(&#seed).as_ref(),
|
||||||
|
},
|
||||||
|
_ => quote! {
|
||||||
|
#tts
|
||||||
|
#seed,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
tts
|
tts
|
||||||
|
|
|
@ -268,6 +268,7 @@ pub struct ConstraintGroup {
|
||||||
literal: Vec<ConstraintLiteral>,
|
literal: Vec<ConstraintLiteral>,
|
||||||
raw: Vec<ConstraintRaw>,
|
raw: Vec<ConstraintRaw>,
|
||||||
close: Option<ConstraintClose>,
|
close: Option<ConstraintClose>,
|
||||||
|
address: Option<ConstraintAddress>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConstraintGroup {
|
impl ConstraintGroup {
|
||||||
|
@ -306,6 +307,7 @@ pub enum Constraint {
|
||||||
State(ConstraintState),
|
State(ConstraintState),
|
||||||
AssociatedGroup(ConstraintAssociatedGroup),
|
AssociatedGroup(ConstraintAssociatedGroup),
|
||||||
Close(ConstraintClose),
|
Close(ConstraintClose),
|
||||||
|
Address(ConstraintAddress),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constraint token is a single keyword in a `#[account(<TOKEN>)]` attribute.
|
// Constraint token is a single keyword in a `#[account(<TOKEN>)]` attribute.
|
||||||
|
@ -327,6 +329,9 @@ pub enum ConstraintToken {
|
||||||
AssociatedPayer(Context<ConstraintAssociatedPayer>),
|
AssociatedPayer(Context<ConstraintAssociatedPayer>),
|
||||||
AssociatedSpace(Context<ConstraintAssociatedSpace>),
|
AssociatedSpace(Context<ConstraintAssociatedSpace>),
|
||||||
AssociatedWith(Context<ConstraintAssociatedWith>),
|
AssociatedWith(Context<ConstraintAssociatedWith>),
|
||||||
|
Address(Context<ConstraintAddress>),
|
||||||
|
TokenMint(Context<ConstraintTokenMint>),
|
||||||
|
TokenAuthority(Context<ConstraintTokenAuthority>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse for ConstraintToken {
|
impl Parse for ConstraintToken {
|
||||||
|
@ -346,7 +351,7 @@ pub struct ConstraintSigner {}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ConstraintBelongsTo {
|
pub struct ConstraintBelongsTo {
|
||||||
pub join_target: Ident,
|
pub join_target: Expr,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -361,7 +366,12 @@ pub struct ConstraintRaw {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ConstraintOwner {
|
pub struct ConstraintOwner {
|
||||||
pub owner_target: Ident,
|
pub owner_target: Expr,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ConstraintAddress {
|
||||||
|
pub address: Expr,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -375,7 +385,8 @@ pub struct ConstraintSeedsGroup {
|
||||||
pub is_init: bool,
|
pub is_init: bool,
|
||||||
pub seeds: Punctuated<Expr, Token![,]>,
|
pub seeds: Punctuated<Expr, Token![,]>,
|
||||||
pub payer: Option<Ident>,
|
pub payer: Option<Ident>,
|
||||||
pub space: Option<LitInt>,
|
pub space: Option<Expr>,
|
||||||
|
pub kind: PdaKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -394,15 +405,16 @@ pub struct ConstraintState {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ConstraintAssociatedGroup {
|
pub struct ConstraintAssociatedGroup {
|
||||||
pub is_init: bool,
|
pub is_init: bool,
|
||||||
pub associated_target: Ident,
|
pub associated_target: Expr,
|
||||||
pub associated_seeds: Vec<Ident>,
|
pub associated_seeds: Vec<Expr>,
|
||||||
pub payer: Option<Ident>,
|
pub payer: Option<Ident>,
|
||||||
pub space: Option<LitInt>,
|
pub space: Option<Expr>,
|
||||||
|
pub kind: PdaKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ConstraintAssociated {
|
pub struct ConstraintAssociated {
|
||||||
pub target: Ident,
|
pub target: Expr,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -412,12 +424,18 @@ pub struct ConstraintAssociatedPayer {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ConstraintAssociatedWith {
|
pub struct ConstraintAssociatedWith {
|
||||||
pub target: Ident,
|
pub target: Expr,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ConstraintAssociatedSpace {
|
pub struct ConstraintAssociatedSpace {
|
||||||
pub space: LitInt,
|
pub space: Expr,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum PdaKind {
|
||||||
|
Program,
|
||||||
|
Token { owner: Expr, mint: Expr },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -425,6 +443,16 @@ pub struct ConstraintClose {
|
||||||
pub sol_dest: Ident,
|
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.
|
// Syntaxt context object for preserving metadata about the inner item.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Context<T> {
|
pub struct Context<T> {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
ConstraintAssociated, ConstraintAssociatedGroup, ConstraintAssociatedPayer,
|
ConstraintAddress, ConstraintAssociated, ConstraintAssociatedGroup, ConstraintAssociatedPayer,
|
||||||
ConstraintAssociatedSpace, ConstraintAssociatedWith, ConstraintBelongsTo, ConstraintClose,
|
ConstraintAssociatedSpace, ConstraintAssociatedWith, ConstraintBelongsTo, ConstraintClose,
|
||||||
ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut,
|
ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut,
|
||||||
ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeeds, ConstraintSeedsGroup,
|
ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeeds, ConstraintSeedsGroup,
|
||||||
ConstraintSigner, ConstraintState, ConstraintToken, Context, Ty,
|
ConstraintSigner, ConstraintState, ConstraintToken, ConstraintTokenAuthority,
|
||||||
|
ConstraintTokenMint, Context, PdaKind, Ty,
|
||||||
};
|
};
|
||||||
use syn::ext::IdentExt;
|
use syn::ext::IdentExt;
|
||||||
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
|
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
|
||||||
|
@ -154,6 +155,24 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
|
||||||
sol_dest: stream.parse()?,
|
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"))?,
|
_ => Err(ParseError::new(ident.span(), "Invalid attribute"))?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,6 +200,9 @@ pub struct ConstraintGroupBuilder<'ty> {
|
||||||
pub associated_space: Option<Context<ConstraintAssociatedSpace>>,
|
pub associated_space: Option<Context<ConstraintAssociatedSpace>>,
|
||||||
pub associated_with: Vec<Context<ConstraintAssociatedWith>>,
|
pub associated_with: Vec<Context<ConstraintAssociatedWith>>,
|
||||||
pub close: Option<Context<ConstraintClose>>,
|
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> {
|
impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||||
|
@ -203,6 +225,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||||
associated_space: None,
|
associated_space: None,
|
||||||
associated_with: Vec::new(),
|
associated_with: Vec::new(),
|
||||||
close: None,
|
close: None,
|
||||||
|
address: None,
|
||||||
|
token_mint: None,
|
||||||
|
token_authority: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn build(mut self) -> ParseResult<ConstraintGroup> {
|
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 {
|
let ConstraintGroupBuilder {
|
||||||
f_ty: _,
|
f_ty: _,
|
||||||
init,
|
init,
|
||||||
|
@ -251,6 +285,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||||
associated_space,
|
associated_space,
|
||||||
associated_with,
|
associated_with,
|
||||||
close,
|
close,
|
||||||
|
address,
|
||||||
|
token_mint,
|
||||||
|
token_authority,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
// Converts Option<Context<T>> -> Option<T>.
|
// Converts Option<Context<T>> -> Option<T>.
|
||||||
|
@ -279,12 +316,29 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||||
raw: into_inner_vec!(raw),
|
raw: into_inner_vec!(raw),
|
||||||
owner: into_inner!(owner),
|
owner: into_inner!(owner),
|
||||||
rent_exempt: into_inner!(rent_exempt),
|
rent_exempt: into_inner!(rent_exempt),
|
||||||
seeds: seeds.map(|c| ConstraintSeedsGroup {
|
seeds: seeds
|
||||||
is_init,
|
.map(|c| {
|
||||||
seeds: c.into_inner().seeds,
|
Ok(ConstraintSeedsGroup {
|
||||||
payer: into_inner!(associated_payer.clone()).map(|a| a.target),
|
is_init,
|
||||||
space: associated_space.clone().map(|s| s.space.clone()),
|
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),
|
executable: into_inner!(executable),
|
||||||
state: into_inner!(state),
|
state: into_inner!(state),
|
||||||
associated: associated.map(|associated| ConstraintAssociatedGroup {
|
associated: associated.map(|associated| ConstraintAssociatedGroup {
|
||||||
|
@ -293,8 +347,19 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||||
associated_seeds: associated_with.iter().map(|s| s.target.clone()).collect(),
|
associated_seeds: associated_with.iter().map(|s| s.target.clone()).collect(),
|
||||||
payer: associated_payer.map(|p| p.target.clone()),
|
payer: associated_payer.map(|p| p.target.clone()),
|
||||||
space: associated_space.map(|s| s.space.clone()),
|
space: associated_space.map(|s| s.space.clone()),
|
||||||
|
kind: match token_mint {
|
||||||
|
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),
|
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::AssociatedSpace(c) => self.add_associated_space(c),
|
||||||
ConstraintToken::AssociatedWith(c) => self.add_associated_with(c),
|
ConstraintToken::AssociatedWith(c) => self.add_associated_with(c),
|
||||||
ConstraintToken::Close(c) => self.add_close(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(())
|
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<()> {
|
fn add_mut(&mut self, c: Context<ConstraintMut>) -> ParseResult<()> {
|
||||||
if self.mutable.is_some() {
|
if self.mutable.is_some() {
|
||||||
return Err(ParseError::new(c.span(), "mut already provided"));
|
return Err(ParseError::new(c.span(), "mut already provided"));
|
||||||
|
|
|
@ -117,6 +117,24 @@ pub fn close_open_orders<'info>(
|
||||||
Ok(())
|
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)]
|
#[derive(Accounts)]
|
||||||
pub struct NewOrderV3<'info> {
|
pub struct NewOrderV3<'info> {
|
||||||
pub market: AccountInfo<'info>,
|
pub market: AccountInfo<'info>,
|
||||||
|
@ -167,3 +185,13 @@ pub struct CloseOpenOrders<'info> {
|
||||||
pub destination: AccountInfo<'info>,
|
pub destination: AccountInfo<'info>,
|
||||||
pub market: 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 dex;
|
||||||
|
pub mod mint;
|
||||||
pub mod shmem;
|
pub mod shmem;
|
||||||
pub mod token;
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct TokenAccount(spl_token::state::Account);
|
pub struct TokenAccount(spl_token::state::Account);
|
||||||
|
|
||||||
|
impl TokenAccount {
|
||||||
|
pub const LEN: usize = spl_token::state::Account::LEN;
|
||||||
|
}
|
||||||
|
|
||||||
impl anchor_lang::AccountDeserialize for TokenAccount {
|
impl anchor_lang::AccountDeserialize for TokenAccount {
|
||||||
fn try_deserialize(buf: &mut &[u8]) -> Result<Self, ProgramError> {
|
fn try_deserialize(buf: &mut &[u8]) -> Result<Self, ProgramError> {
|
||||||
TokenAccount::try_deserialize_unchecked(buf)
|
TokenAccount::try_deserialize_unchecked(buf)
|
||||||
|
|
|
@ -69,6 +69,7 @@ const LangErrorCode = {
|
||||||
ConstraintAssociated: 149,
|
ConstraintAssociated: 149,
|
||||||
ConstraintAssociatedInit: 150,
|
ConstraintAssociatedInit: 150,
|
||||||
ConstraintClose: 151,
|
ConstraintClose: 151,
|
||||||
|
ConstraintAddress: 152,
|
||||||
|
|
||||||
// Accounts.
|
// Accounts.
|
||||||
AccountDiscriminatorAlreadySet: 160,
|
AccountDiscriminatorAlreadySet: 160,
|
||||||
|
@ -132,6 +133,7 @@ const LangErrorMessage = new Map([
|
||||||
"An associated init constraint was violated",
|
"An associated init constraint was violated",
|
||||||
],
|
],
|
||||||
[LangErrorCode.ConstraintClose, "A close constraint was violated"],
|
[LangErrorCode.ConstraintClose, "A close constraint was violated"],
|
||||||
|
[LangErrorCode.ConstraintAddress, "An address constraint was violated"],
|
||||||
|
|
||||||
// Accounts.
|
// Accounts.
|
||||||
[
|
[
|
||||||
|
|
|
@ -250,14 +250,8 @@ export class Program {
|
||||||
this._coder = new Coder(idl);
|
this._coder = new Coder(idl);
|
||||||
|
|
||||||
// Dynamic namespaces.
|
// Dynamic namespaces.
|
||||||
const [
|
const [rpc, instruction, transaction, account, simulate, state] =
|
||||||
rpc,
|
NamespaceFactory.build(idl, this._coder, programId, this._provider);
|
||||||
instruction,
|
|
||||||
transaction,
|
|
||||||
account,
|
|
||||||
simulate,
|
|
||||||
state,
|
|
||||||
] = NamespaceFactory.build(idl, this._coder, programId, this._provider);
|
|
||||||
this.rpc = rpc;
|
this.rpc = rpc;
|
||||||
this.instruction = instruction;
|
this.instruction = instruction;
|
||||||
this.transaction = transaction;
|
this.transaction = transaction;
|
||||||
|
|
|
@ -17,6 +17,7 @@ import Coder, {
|
||||||
} from "../../coder";
|
} from "../../coder";
|
||||||
import { Subscription, Address, translateAddress } from "../common";
|
import { Subscription, Address, translateAddress } from "../common";
|
||||||
import { getProvider } from "../../";
|
import { getProvider } from "../../";
|
||||||
|
import * as pubkeyUtil from "../../utils/pubkey";
|
||||||
|
|
||||||
export default class AccountFactory {
|
export default class AccountFactory {
|
||||||
public static build(
|
public static build(
|
||||||
|
@ -234,9 +235,10 @@ export class AccountClient {
|
||||||
fromPubkey: this._provider.wallet.publicKey,
|
fromPubkey: this._provider.wallet.publicKey,
|
||||||
newAccountPubkey: signer.publicKey,
|
newAccountPubkey: signer.publicKey,
|
||||||
space: sizeOverride ?? size,
|
space: sizeOverride ?? size,
|
||||||
lamports: await this._provider.connection.getMinimumBalanceForRentExemption(
|
lamports:
|
||||||
sizeOverride ?? size
|
await this._provider.connection.getMinimumBalanceForRentExemption(
|
||||||
),
|
sizeOverride ?? size
|
||||||
|
),
|
||||||
programId: this._programId,
|
programId: this._programId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -245,7 +247,7 @@ export class AccountClient {
|
||||||
* Function returning the associated account. Args are keys to associate.
|
* Function returning the associated account. Args are keys to associate.
|
||||||
* Order matters.
|
* Order matters.
|
||||||
*/
|
*/
|
||||||
async associated(...args: PublicKey[]): Promise<any> {
|
async associated(...args: Array<PublicKey | Buffer>): Promise<any> {
|
||||||
const addr = await this.associatedAddress(...args);
|
const addr = await this.associatedAddress(...args);
|
||||||
return await this.fetch(addr);
|
return await this.fetch(addr);
|
||||||
}
|
}
|
||||||
|
@ -254,13 +256,10 @@ export class AccountClient {
|
||||||
* Function returning the associated address. Args are keys to associate.
|
* Function returning the associated address. Args are keys to associate.
|
||||||
* Order matters.
|
* Order matters.
|
||||||
*/
|
*/
|
||||||
async associatedAddress(...args: PublicKey[]): Promise<PublicKey> {
|
async associatedAddress(
|
||||||
let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor".
|
...args: Array<PublicKey | Buffer>
|
||||||
args.forEach((arg) => {
|
): Promise<PublicKey> {
|
||||||
seeds.push(translateAddress(arg).toBuffer());
|
return await pubkeyUtil.associated(this._programId, ...args);
|
||||||
});
|
|
||||||
const [assoc] = await PublicKey.findProgramAddress(seeds, this._programId);
|
|
||||||
return assoc;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,3 +2,4 @@ export * as sha256 from "./sha256";
|
||||||
export * as rpc from "./rpc";
|
export * as rpc from "./rpc";
|
||||||
export * as publicKey from "./pubkey";
|
export * as publicKey from "./pubkey";
|
||||||
export * as bytes from "./bytes";
|
export * as bytes from "./bytes";
|
||||||
|
export * as token from "./token";
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import BN from "bn.js";
|
import BN from "bn.js";
|
||||||
import { sha256 as sha256Sync } from "js-sha256";
|
import { sha256 as sha256Sync } from "js-sha256";
|
||||||
import { PublicKey } from "@solana/web3.js";
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { Address, translateAddress } from "../program/common";
|
||||||
|
|
||||||
// Sync version of web3.PublicKey.createWithSeed.
|
// Sync version of web3.PublicKey.createWithSeed.
|
||||||
export function createWithSeedSync(
|
export function createWithSeedSync(
|
||||||
|
@ -76,3 +77,21 @@ const toBuffer = (arr: Buffer | Uint8Array | Array<number>): Buffer => {
|
||||||
return Buffer.from(arr);
|
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(
|
function attachWorkspaceOverride(
|
||||||
workspaceCache: { [key: string]: Program },
|
workspaceCache: { [key: string]: Program },
|
||||||
overrideConfig: { [key: string]: string },
|
overrideConfig: { [key: string]: string | { address: string; idl?: string } },
|
||||||
idlMap: Map<string, Idl>
|
idlMap: Map<string, Idl>
|
||||||
) {
|
) {
|
||||||
Object.keys(overrideConfig).forEach((programName) => {
|
Object.keys(overrideConfig).forEach((programName) => {
|
||||||
const wsProgramName = camelCase(programName, { pascalCase: true });
|
const wsProgramName = camelCase(programName, { pascalCase: true });
|
||||||
const overrideAddress = new PublicKey(overrideConfig[programName]);
|
const entry = overrideConfig[programName];
|
||||||
workspaceCache[wsProgramName] = new Program(
|
const overrideAddress = new PublicKey(
|
||||||
idlMap.get(programName),
|
typeof entry === "string" ? entry : entry.address
|
||||||
overrideAddress
|
|
||||||
);
|
);
|
||||||
|
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