use std::collections::HashMap;
use std::fs;
use anchor_lang::AccountSerialize;
use anchor_spl::token::spl_token;
use solana_client::rpc_client::RpcClient;
use solana_program::program_pack::Pack;
use solana_sdk::account::Account;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::rent::Rent;
use solana_sdk::system_program;
use pump_rust_client::accounts::pump_amm::decode_pool;
use pump_rust_client::accounts::{decode_bonding_curve, decode_global};
use pump_rust_client::constants;
use pump_rust_client::pda;
use pump_rust_client::state::Global;
#[path = "../../../tests/common/fixtures.rs"]
mod fixtures;
use fixtures::{FixtureMint, FIXTURE_MINTS};
const OUT_PATH: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/artifacts/accounts_to_load.zst"
);
#[derive(Clone, Copy, Debug)]
enum Network {
Devnet,
Mainnet,
}
impl Network {
fn parse(s: &str) -> Result<Self, String> {
match s.trim().to_ascii_lowercase().as_str() {
"mainnet" | "mainnet-beta" => Ok(Network::Mainnet),
"devnet" => Ok(Network::Devnet),
other => Err(format!(
"network must be devnet or mainnet (or mainnet-beta), got `{other}`"
)),
}
}
fn from_env() -> Self {
let s = std::env::var("PUMP_NETWORK").unwrap_or_else(|_| "devnet".into());
Self::parse(&s).unwrap_or_else(|e| panic!("{e}"))
}
fn default_rpc(self) -> &'static str {
match self {
Network::Devnet => "https://api.devnet.solana.com",
Network::Mainnet => "https://api.mainnet-beta.solana.com",
}
}
fn alt(self) -> Pubkey {
match self {
Network::Devnet => constants::DEVNET_ALT,
Network::Mainnet => constants::MAINNET_ALT,
}
}
fn alt_label(self) -> &'static str {
match self {
Network::Devnet => "alt:devnet",
Network::Mainnet => "alt:mainnet",
}
}
}
struct Cli {
network: Option<Network>,
rpc_url: Option<String>,
}
fn print_usage() {
println!(
"\
clone_devnet_accounts — snapshot on-chain accounts for the local validator
Usage:
clone_devnet_accounts [OPTIONS] [RPC_URL]
Options:
-n, --network <NETWORK> devnet | mainnet (sets which ALT / fixtures apply).
Overrides PUMP_NETWORK.
-r, --rpc-url <URL> RPC HTTP endpoint. Overrides PUMP_CLONE_RPC.
-h, --help Print this help.
If RPC_URL is given as the first non-option argument, it is treated like --rpc-url.
Do not pass both --rpc-url and a positional RPC_URL.
RPC resolution: CLI (-r or positional) → PUMP_CLONE_RPC → public cluster RPC.
A .env file in the current directory is loaded when present.
Examples:
clone_devnet_accounts --network devnet
clone_devnet_accounts -r https://api.devnet.solana.com
clone_devnet_accounts https://api.mainnet-beta.solana.com --network mainnet
"
);
}
fn parse_cli() -> Result<Cli, String> {
let mut network = None;
let mut rpc_flag = None;
let mut positional = None;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"-h" | "--help" => {
print_usage();
std::process::exit(0);
}
"-n" | "--network" => {
let v = args
.next()
.ok_or_else(|| "--network requires a value (devnet or mainnet)".to_string())?;
network = Some(Network::parse(&v)?);
}
"-r" | "--rpc-url" => {
let v = args
.next()
.ok_or_else(|| "--rpc-url requires a URL".to_string())?;
rpc_flag = Some(v);
}
s if s.starts_with('-') => {
return Err(format!("unknown option `{s}` (try --help)"));
}
s => {
if positional.is_some() {
return Err(format!("unexpected extra argument `{s}`"));
}
positional = Some(s.to_string());
}
}
}
if rpc_flag.is_some() && positional.is_some() {
return Err("use either --rpc-url or one positional RPC URL, not both".into());
}
Ok(Cli {
network,
rpc_url: rpc_flag.or(positional),
})
}
fn fixed_pdas(network: Network) -> Vec<(&'static str, Pubkey, bool)> {
vec![
("pump:global", pda::pump::global().0, true),
(
"pump:event_authority",
pda::pump::event_authority().0,
false,
),
("pump:mint_authority", pda::pump::mint_authority().0, false),
(
"pump:global_volume_accumulator",
pda::pump::global_volume_accumulator().0,
true,
),
("pump:fee_config", pda::pump::fee_config().0, true),
(
"pump_amm:global_config",
pda::pump_amm::global_config().0,
false,
),
(
"pump_amm:event_authority",
pda::pump_amm::event_authority().0,
false,
),
(
"pump_amm:global_volume_accumulator",
pda::pump_amm::global_volume_accumulator().0,
false,
),
("pump_amm:fee_config", pda::pump_amm::fee_config().0, true),
(
"pump_agent_payments:global_config",
pda::pump_agent_payments::global_config().0,
false,
),
("mayhem:global_params", pda::mayhem::global_params().0, true),
("mayhem:sol_vault", pda::mayhem::sol_vault().0, true),
(network.alt_label(), network.alt(), true),
]
}
fn print_account(label: &str, key: &Pubkey, acct: &Account) {
println!(
" {:<40} {} lamports={} owner={} data={}B",
label,
key,
acct.lamports,
acct.owner,
acct.data.len()
);
}
fn clone_one(
rpc: &RpcClient,
out: &mut HashMap<Pubkey, Account>,
label: &str,
key: Pubkey,
required: bool,
) -> Result<Option<Account>, Box<dyn std::error::Error>> {
if let Some(existing) = out.get(&key) {
print_account(label, &key, existing);
return Ok(Some(existing.clone()));
}
let acct = rpc
.get_account_with_commitment(&key, rpc.commitment())?
.value;
match acct {
Some(acct) => {
print_account(label, &key, &acct);
out.insert(key, acct.clone());
Ok(Some(acct))
}
None if required => {
panic!("required fixture account `{label}` missing on cluster at {key}")
}
None => {
println!(" {:<40} {} (not on cluster — skipped)", label, key);
Ok(None)
}
}
}
fn clone_fixture_mint(
rpc: &RpcClient,
out: &mut HashMap<Pubkey, Account>,
fixture: &FixtureMint,
) -> Result<(), Box<dyn std::error::Error>> {
let mint = fixture.mint;
println!("📥 Fixture {} ({}):", fixture.label, mint);
clone_one(rpc, out, " mint", mint, true)?;
let bonding_curve_key = pda::pump::bonding_curve(&mint).0;
let bc_account = clone_one(rpc, out, " bonding_curve", bonding_curve_key, true)?
.expect("bonding_curve required entry returned None");
let bc = decode_bonding_curve(&bc_account.data)?;
clone_one(
rpc,
out,
" creator_vault",
pda::pump::creator_vault(&bc.creator).0,
false,
)?;
clone_one(
rpc,
out,
" sharing_config",
pda::pump::sharing_config(&mint).0,
false,
)?;
clone_one(
rpc,
out,
" bonding_curve_v2",
pda::pump::bonding_curve_v2(&mint).0,
false,
)?;
clone_one(
rpc,
out,
" bonding_curve_base_ata",
pda::associated_token(
&bonding_curve_key,
&constants::SPL_TOKEN_2022_PROGRAM_ID,
&mint,
)
.0,
false,
)?;
clone_one(
rpc,
out,
" bonding_curve_wsol_ata",
pda::associated_token(
&bonding_curve_key,
&constants::SPL_TOKEN_PROGRAM_ID,
&constants::NATIVE_MINT,
)
.0,
false,
)?;
if bc.complete {
let pool_creator = pda::pump::pool_authority(&mint).0;
let pool_key = pda::pump_amm::pool(0, &pool_creator, &mint, &constants::NATIVE_MINT).0;
let pool_account = clone_one(rpc, out, " pool", pool_key, false)?;
if let Some(pool_account) = pool_account {
let pool = decode_pool(&pool_account.data)?;
clone_one(rpc, out, " lp_mint", pool.lp_mint, true)?;
clone_one(
rpc,
out,
" pool_base_token_account",
pool.pool_base_token_account,
true,
)?;
clone_one(
rpc,
out,
" pool_quote_token_account",
pool.pool_quote_token_account,
true,
)?;
let cc_vault_authority =
pda::pump_amm::coin_creator_vault_authority(&pool.coin_creator).0;
clone_one(
rpc,
out,
" coin_creator_vault_authority",
cc_vault_authority,
false,
)?;
clone_one(
rpc,
out,
" coin_creator_vault_quote_ata",
pda::associated_token(
&cc_vault_authority,
&constants::SPL_TOKEN_PROGRAM_ID,
&constants::NATIVE_MINT,
)
.0,
false,
)?;
}
}
Ok(())
}
fn whitelist_test_quote_mint_in_global(
out: &mut HashMap<Pubkey, Account>,
global: &Global,
) -> Result<(), Box<dyn std::error::Error>> {
if global
.whitelisted_quote_mints
.contains(&fixtures::TEST_QUOTE_MINT)
{
println!(
"🛠 Quote mint {} already whitelisted in Global — skipping",
fixtures::TEST_QUOTE_MINT
);
return Ok(());
}
let mut patched = global.clone();
patched.whitelisted_quote_mints[0] = fixtures::TEST_QUOTE_MINT;
let mut new_data = Vec::new();
patched.try_serialize(&mut new_data)?;
let key = pda::pump::global().0;
let entry = out
.get_mut(&key)
.expect("pump:global must be present in `out` before patching");
assert_eq!(
entry.data.len(),
new_data.len(),
"Global re-serialization changed data length ({} -> {}); IDL drift?",
entry.data.len(),
new_data.len()
);
entry.data = new_data;
println!(
"🛠 Whitelisted quote mint {} in Global",
fixtures::TEST_QUOTE_MINT
);
Ok(())
}
fn synthesize_test_quote_mint(out: &mut HashMap<Pubkey, Account>) {
let mint = spl_token::state::Mint {
mint_authority: solana_program::program_option::COption::Some(
fixtures::TEST_QUOTE_MINT_AUTHORITY,
),
supply: 0,
decimals: 6,
is_initialized: true,
freeze_authority: solana_program::program_option::COption::None,
};
let mut data = vec![0u8; spl_token::state::Mint::LEN];
spl_token::state::Mint::pack(mint, &mut data).expect("pack test quote Mint");
let acct = Account {
lamports: Rent::default().minimum_balance(spl_token::state::Mint::LEN),
data,
owner: spl_token::ID,
executable: false,
rent_epoch: 0,
};
out.insert(fixtures::TEST_QUOTE_MINT, acct);
println!(
"🛠 Synthesized test quote mint at {} (authority {})",
fixtures::TEST_QUOTE_MINT,
fixtures::TEST_QUOTE_MINT_AUTHORITY
);
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = dotenvy::dotenv();
let cli = parse_cli().map_err(|msg| std::io::Error::new(std::io::ErrorKind::InvalidInput, msg))?;
let network = cli.network.unwrap_or_else(Network::from_env);
let rpc_url = cli
.rpc_url
.or_else(|| std::env::var("PUMP_CLONE_RPC").ok())
.unwrap_or_else(|| network.default_rpc().to_string());
let rpc = RpcClient::new(rpc_url.clone());
println!("🌐 Cloning {network:?} from: {rpc_url}");
let labeled = fixed_pdas(network);
let keys: Vec<Pubkey> = labeled.iter().map(|(_, k, _)| *k).collect();
let fetched = rpc.get_multiple_accounts(&keys)?;
let mut out: HashMap<Pubkey, Account> = HashMap::new();
let mut global_account: Option<Account> = None;
println!("📥 Fixed PDAs:");
for ((label, key, required), maybe_acct) in labeled.iter().zip(fetched.into_iter()) {
match maybe_acct {
Some(acct) => {
print_account(label, key, &acct);
if *label == "pump:global" {
global_account = Some(acct.clone());
}
out.insert(*key, acct);
}
None if *required => {
panic!("required account `{label}` missing on {network:?} at {key}");
}
None => {
println!(" {:<40} {} (not on cluster — skipped)", label, key);
}
}
}
let global = decode_global(&global_account.expect("pump:global must be set above").data)?;
whitelist_test_quote_mint_in_global(&mut out, &global)?;
let mut recipients: Vec<Pubkey> = Vec::new();
recipients.push(global.fee_recipient);
recipients.extend(global.fee_recipients.iter().copied());
recipients.push(global.reserved_fee_recipient);
recipients.extend(global.reserved_fee_recipients.iter().copied());
recipients.extend(global.buyback_fee_recipients.iter().copied());
recipients.retain(|p| *p != Pubkey::default());
recipients.sort();
recipients.dedup();
println!(
"🔎 Global yielded {} distinct fee/buyback recipient(s)",
recipients.len()
);
if !recipients.is_empty() {
let recipient_accounts = rpc.get_multiple_accounts(&recipients)?;
println!("📥 Recipients:");
for (key, maybe_acct) in recipients.iter().zip(recipient_accounts.into_iter()) {
let acct = maybe_acct.unwrap_or_else(|| Account {
lamports: 0,
data: vec![],
owner: system_program::ID,
executable: false,
rent_epoch: 0,
});
print_account("recipient", key, &acct);
out.entry(*key).or_insert(acct);
}
}
for fixture in FIXTURE_MINTS {
clone_fixture_mint(&rpc, &mut out, fixture)?;
}
synthesize_test_quote_mint(&mut out);
let bytes = bincode::serialize(&out)?;
let compressed = zstd::stream::encode_all(&bytes[..], 3)?;
if let Some(parent) = std::path::Path::new(OUT_PATH).parent() {
fs::create_dir_all(parent)?;
}
fs::write(OUT_PATH, &compressed)?;
println!(
"✅ Wrote {} accounts to {} ({} bytes raw, {} bytes zstd)",
out.len(),
OUT_PATH,
bytes.len(),
compressed.len()
);
Ok(())
}