pump-dump 0.1.0

A CLI tool for inspecting Pump.fun pools, events, and transactions on Solana
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use color_eyre::{Result, eyre::eyre};
use lil_tabby::tabby;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;

use crate::utils::SolanaLinks;

// --- Discriminator Constants ---
/// Account discriminator for BondingCurve
/// Hex: 17b7f83760d8ac60
pub const ACCOUNT_DISCRIMINATOR_BONDING_CURVE: [u8; 8] = [23, 183, 248, 55, 96, 216, 172, 96];
/// Account discriminator for Global
/// Hex: a7e8e8b1c86c727f
pub const ACCOUNT_DISCRIMINATOR_GLOBAL: [u8; 8] = [167, 232, 232, 177, 200, 108, 114, 127];
/// Account discriminator for LastWithdraw
/// Hex: cb12dc677891bb02
pub const ACCOUNT_DISCRIMINATOR_LAST_WITHDRAW: [u8; 8] = [203, 18, 220, 103, 120, 145, 187, 2];

// --- Custom Type Definitions (includes types, account structs, event structs) ---

#[serde_with::serde_as]
#[derive(BorshSchema, BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug, Clone)]
pub struct BondingCurve {
    pub virtual_token_reserves: u64,
    pub virtual_sol_reserves: u64,
    pub real_token_reserves: u64,
    pub real_sol_reserves: u64,
    pub token_total_supply: u64,
    pub complete: bool,
    #[serde_as(as = "serde_with::DisplayFromStr")]
    pub creator: Pubkey,
}

#[serde_with::serde_as]
#[derive(BorshSchema, BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug, Clone)]
pub struct Global {
    pub initialized: bool,
    #[serde_as(as = "serde_with::DisplayFromStr")]
    pub authority: Pubkey,
    #[serde_as(as = "serde_with::DisplayFromStr")]
    pub fee_recipient: Pubkey,
    pub initial_virtual_token_reserves: u64,
    pub initial_virtual_sol_reserves: u64,
    pub initial_real_token_reserves: u64,
    pub token_total_supply: u64,
    pub fee_basis_points: u64,
    #[serde_as(as = "serde_with::DisplayFromStr")]
    pub withdraw_authority: Pubkey,
    pub enable_migrate: bool,
    pub pool_migration_fee: u64,
    pub creator_fee: u64,
    #[serde_as(as = "[serde_with::DisplayFromStr; 7]")]
    pub fee_recipients: [Pubkey; 7],
    #[serde_as(as = "serde_with::DisplayFromStr")]
    pub set_creator_authority: Pubkey,
    #[serde_as(as = "serde_with::DisplayFromStr")]
    pub admin_set_creator_authority: Pubkey,
}

#[serde_with::serde_as]
#[derive(BorshSchema, BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug, Clone)]
pub struct LastWithdraw {
    pub last_withdraw_timestamp: i64,
}

#[allow(clippy::large_enum_variant)]
#[serde_with::serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Account {
    Global(Global),
    BondingCurve(BondingCurve),
    LastWithdraw(LastWithdraw),
}

pub fn decode_account(account_bytes: &[u8]) -> Result<Account> {
    if account_bytes.len() < 8 {
        return Err(eyre!(
            "Event data too short to contain a discriminator.".to_string()
        ));
    }
    let discriminator: [u8; 8] = account_bytes[0..8]
        .try_into()
        .map_err(|_| eyre!("Failed to extract event discriminator slice"))?;
    let rest = &account_bytes[8..];

    match discriminator {
        ACCOUNT_DISCRIMINATOR_BONDING_CURVE => {
            let len = borsh::max_serialized_size::<BondingCurve>()
                .map_err(|e| eyre!("Can't get max serialized size: {:#?}", e))?;
            borsh::from_slice::<BondingCurve>(&rest[0..len])
                .map(Account::BondingCurve)
                .map_err(|e| eyre!("Can't deserialize BondingCurve: {:#?}", e))
        }
        ACCOUNT_DISCRIMINATOR_GLOBAL => {
            let len = borsh::max_serialized_size::<Global>()
                .map_err(|e| eyre!("Can't get max serialized size: {:#?}", e))?;
            borsh::from_slice::<Global>(&rest[0..len])
                .map(Account::Global)
                .map_err(|e| eyre!("Can't deserialize Global: {:#?}", e))
        }
        ACCOUNT_DISCRIMINATOR_LAST_WITHDRAW => {
            let len = borsh::max_serialized_size::<LastWithdraw>()
                .map_err(|e| eyre!("Can't get max serialized size: {:#?}", e))?;
            borsh::from_slice::<LastWithdraw>(&rest[0..len])
                .map(Account::LastWithdraw)
                .map_err(|e| eyre!("Can't deserialize LastWithdraw: {:#?}", e))
        }
        _ => Err(eyre!("Unknown account discriminator: {:?}", discriminator)),
    }
}

impl std::fmt::Display for Account {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Account::BondingCurve(account) => {
                writeln!(
                    f,
                    "{}",
                    tabby![
                        { color: yellow, header: bright_green, labels: magenta }
                        [format!("BondingCurve")],
                        ["virtual_token_reserves", &account.virtual_token_reserves.to_string()],
                        ["virtual_sol_reserves", &account.virtual_sol_reserves.to_string()],
                        ["real_token_reserves", &account.real_token_reserves.to_string()],
                        ["real_sol_reserves", &account.real_sol_reserves.to_string()],
                        ["token_total_supply", &account.token_total_supply.to_string()],
                        ["complete", &account.complete.to_string()],
                        ["creator", &account.creator.acc()]
                    ]
                )
            }
            Account::Global(account) => {
                writeln!(
                    f,
                    "{}",
                    tabby![
                        { color: yellow, header: bright_green, labels: magenta }
                        [format!("Global")],
                        ["initialized", &account.initialized.to_string()],
                        ["authority", &account.authority.acc()],
                        ["fee_recipient", &account.fee_recipient.acc()],
                        ["initial_virtual_token_reserves", &account.initial_virtual_token_reserves.to_string()],
                        ["initial_virtual_sol_reserves", &account.initial_virtual_sol_reserves.to_string()],
                        ["initial_real_token_reserves", &account.initial_real_token_reserves.to_string()],
                        ["token_total_supply", &account.token_total_supply.to_string()],
                        ["fee_basis_points", &account.fee_basis_points.to_string()],
                        ["withdraw_authority", &account.withdraw_authority.acc()],
                        ["enable_migrate", &account.enable_migrate.to_string()],
                        ["pool_migration_fee", &account.pool_migration_fee.to_string()],
                        ["creator_fee", &account.creator_fee.to_string()],
                        ["fee_recipients", &account.fee_recipients.iter().map(|p| p.acc().to_string()).collect::<Vec<String>>().join("\n")]
                    ]
                )
            }
            Account::LastWithdraw(account) => {
                writeln!(
                    f,
                    "{}",
                    tabby![
                        { color: yellow, header: bright_green, labels: magenta }
                        [format!("LastWithdraw")],
                        ["last_withdraw_timestamp", &account.last_withdraw_timestamp.to_string()]
                    ]
                )
            }
        }
    }
}