use bs58; use chrono; use solana_sdk::message::Message; use anyhow::anyhow; use anyhow::Result; use base64::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; use chrono::Utc; use hex; use lazy_static::lazy_static; use log::{debug, error, info}; use mpl_token_metadata::accounts::Metadata; use serde::{Deserialize, Serialize}; use solana_account_decoder::{UiAccountData, UiAccountEncoding}; use solana_client::rpc_client::RpcClient; use solana_client::rpc_config::RpcAccountInfoConfig; use solana_client::rpc_request::TokenAccountsFilter; use solana_program::{program_pack::Pack, pubkey::Pubkey}; use solana_sdk::message::VersionedMessage; use solana_sdk::signature::Signature; use solana_sdk::transaction::{TransactionError, VersionedTransaction}; use solana_transaction_status::EncodedTransaction::Binary; use solana_transaction_status::{EncodedConfirmedTransactionWithStatusMeta, UiTransactionEncoding}; use solana_transaction_status_client_types::option_serializer; use spl_token::state::Mint; use std::str::FromStr; use solana_program::clock::Slot; use solana_sdk::account::Account; use utils::sanitize_string; use crate::dto::{AccountKeys, ParsedAccount, TokenData, TokenInfo, Transaction}; use crate::utils::base58_to_pubkey; mod utils; mod dto; lazy_static! { static ref RPC_CLIENT: RpcClient = RpcClient::new("https://api.mainnet-beta.solana.com".to_string()); } fn main() { // Adresse du programme Token (SPL Token Program) --> pumpfun let token_program_address = "TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM"; let buy_router_address = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"; match base58_to_pubkey(token_program_address) { Ok(token_program) => { info!( "{}", format!( "Récupération des transactions sur le programme {:?}", token_program_address ) ); let signatures = RPC_CLIENT.get_signatures_for_address(&token_program); match signatures { Ok(sigs) => { debug!( "{}", format!("Nombre total de transactions trouvées : {}", sigs.len()) ); for sig in sigs.iter().take(1000) { let signature = Signature::from_str(&sig.signature).unwrap(); info!("================ Signature ========================"); match get_transactions(&RPC_CLIENT, &signature) { Ok(tx) => { if let Some(meta) = tx.transaction.meta { if let option_serializer::OptionSerializer::Some( inner_instructions, ) = meta.inner_instructions { let is_token_creation = inner_instructions.iter().any(|inner| { inner.instructions.iter().any(|ix| { match ix { solana_transaction_status::UiInstruction::Compiled(instruction) => { is_token_creation(instruction) } _ => false } }) }); if !is_token_creation { continue } match tx.transaction.transaction { Binary(data, encoding) => { let decoded = BASE64_STANDARD.decode(data).unwrap(); if let Ok(versioned_tx) = bincode::deserialize::( &decoded, ) { let account_keys = versioned_tx.message.static_account_keys(); let transaction = Transaction { signature: sig.clone().signature, slot: sig.clone().slot, block_time: sig.clone().block_time, status: meta.err, account_keys: AccountKeys { payer: account_keys[0], mint_account: account_keys[1], rent_account: account_keys[2], mint_authority: account_keys[3], token_program: account_keys[4], }, }; let mint_pubkey =transaction.account_keys.mint_account; let dev_pubkey = transaction.account_keys.payer; match get_token_metadata( &RPC_CLIENT, &mint_pubkey, ) { Ok(token_metadata) => { let dev_balance = get_dev_balance_for_token( &RPC_CLIENT, &transaction.account_keys.payer, &mint_pubkey, ); if let Err(e) = dev_balance { error!("{}",format!("{}", e) ); continue } match get_token_info( &RPC_CLIENT, &mint_pubkey, ) { Ok(token_info) => { let token_data = TokenData { name: sanitize_string(&token_metadata.name), symbol: sanitize_string(&token_metadata.symbol), uri: sanitize_string(&token_metadata.uri), supply: token_info.supply, dev_balance: dev_balance.unwrap().clone(), mint_authority: token_info.mint_authority, freeze_authority: token_info.freeze_authority, }; dbg!(token_data.clone()); let is_safe_token = is_safe_token(token_data.clone()); if(is_safe_token) { println!("Token OK") } else { println!("Token NOK") } } Err(e) => { error!( "Erreur lors de la récupération des informations du token : {:?}", e ); } } } Err(_) => {} }; } println!( "=============================================" ); } _ => {} } } } } Err(err) => eprintln!( "Erreur lors de la récupération de la transaction : {:?}", err ), } } } Err(err) => eprintln!( "Erreur lors de la récupération des transactions : {:?}", err ), } } Err(err) => eprintln!("Erreur lors de la conversion de l'adresse : {:?}", err), } } fn is_safe_token(data: TokenData) -> bool { let holding_percentage = data.dev_balance * 100.0 / data.supply as f64; let has_control = data.mint_authority || data.freeze_authority; let holds_too_much = holding_percentage > 20.0; !(has_control || holds_too_much) } fn find_metadata_account(mint: &Pubkey) -> (Pubkey, u8) { let metadata_program_id = Pubkey::from_str("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s").unwrap(); Pubkey::find_program_address( &[b"metadata", metadata_program_id.as_ref(), mint.as_ref()], &metadata_program_id, ) } fn get_token_metadata(client: &RpcClient, mint: &Pubkey) -> Result { let (metadata_pubkey, _) = find_metadata_account(mint); let account = client.get_account(&metadata_pubkey)?; let metadata = Metadata::safe_deserialize(&mut account.data.as_ref())?; Ok(metadata) } pub const TOKEN_METADATA_PROGRAM_ID: Pubkey = Pubkey::new_from_array([ 0x6D, 0x65, 0x74, 0x61, 0x70, 0x6C, 0x65, 0x78, 0x2D, 0x6D, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]); fn get_creator_of_mint(rpc_client: &RpcClient, mint_address: &Pubkey) -> anyhow::Result { // 1. Récupère les signatures associées au compte mint let signatures = rpc_client .get_signatures_for_address(mint_address) .into_iter() .rev() // on inverse pour avoir les plus anciennes d'abord .collect::>(); let first_signature = signatures.first(); match first_signature { None => Err(anyhow!("Pas de transactions trouvées pour ce mint")), Some(sig) => { let sig2 = sig.first().expect("Pzs de signature trouvée"); let sig = sig2 .signature .parse::() .expect("Signature invalide"); // 2. Récupère les détails de la première transaction let tx = get_transactions(rpc_client, &sig)?.transaction.transaction; let tx: anyhow::Result = match tx { Binary(data, encoding) => { let decoded = BASE64_STANDARD.decode(data)?; if let Ok(versioned_tx) = bincode::deserialize::(&decoded) { if let Ok(message) = bincode::deserialize::( &versioned_tx.message.serialize(), ) { Ok(message) } else { Err(anyhow!("")) } } else { Err(anyhow!("")) } } _ => Err(anyhow!("Encoding of transaction is not supported")), }; let payer: Result = match tx { Ok(msg) => { match msg { VersionedMessage::Legacy(legacy_msg) => { println!("{:?}", legacy_msg); Ok(legacy_msg) } //VersionedMessage::V0(v0_msg) => { println!("{:?}", v0_msg); v0_msg} _ => Err(anyhow!("Encoding of transaction is not supported")), } } Err(_) => Err(anyhow!("Encoding of transaction is not supported")), }; let payer = payer; Ok(*payer?.account_keys.first().unwrap()) } } } fn is_token_creation(instruction: &solana_transaction_status::UiCompiledInstruction) -> bool { match bs58::decode(&instruction.data).into_vec() { Ok(data) => { info!("It is a token creation"); matches!(data.first(), Some(0)) } Err(_) => { info!("not a token creation"); false } } } fn get_dev_balance_for_token(rpc: &RpcClient, dev_pubkey: &Pubkey, mint: &Pubkey) -> Result { let token_accounts = rpc.get_token_accounts_by_owner(dev_pubkey, TokenAccountsFilter::Mint(*mint))?; let mut total_balance: f64 = 0.0; for account_info in token_accounts { let amount_option = match account_info.account.data.clone() { UiAccountData::Json(test) => { let json = serde_json::to_value(&test.parsed)?; let token: ParsedAccount = serde_json::from_value(json)?; token.info.tokenAmount.uiAmount } _ => { error!("Cannot parse account_data"); None } }; total_balance += amount_option.unwrap_or_else(|| 0.0); } Ok(total_balance) } fn fetch_account(rpc_client: &RpcClient, mint_pubkey: &Pubkey) -> Result { let config = RpcAccountInfoConfig { encoding: Some(UiAccountEncoding::Base64), ..RpcAccountInfoConfig::default() }; match rpc_client.get_account_with_config(&mint_pubkey, config) { Ok(response) => { match response.value { Some(account) => { Ok(account) } None => { Err(anyhow::Error::msg(format!("Account not found"))) } } } Err(e) => { Err(anyhow::Error::msg(format!("RPC error {}", e))) } } } fn get_token_info(client: &RpcClient, mint_pubkey: &Pubkey) -> Result { let account = fetch_account(client, mint_pubkey); let mint = Mint::unpack(&*account?.data); match mint { Ok(mint) => { Ok(TokenInfo { mint_address: mint_pubkey.to_string(), decimals: mint.decimals, mint_authority: mint.mint_authority.is_some(), freeze_authority: mint.freeze_authority.is_some(), supply: mint.supply, }) } Err(e) => Err(anyhow::Error::msg(format!("ProgramError: {:?}", e))), } } fn get_transactions( client: &RpcClient, signature: &Signature, ) -> solana_client::client_error::Result { let config = solana_client::rpc_config::RpcTransactionConfig { encoding: Some(UiTransactionEncoding::Base64), commitment: Some(solana_sdk::commitment_config::CommitmentConfig::confirmed()), max_supported_transaction_version: Some(0), }; client.get_transaction_with_config(&signature, config) } #[cfg(test)] mod tests { use super::*; #[test] fn safe_token_no_control_and_low_holding() { let data = TokenData { name: "".to_string(), symbol: "".to_string(), dev_balance: 10.0, supply: 1000, mint_authority: false, freeze_authority: false, uri: "".to_string(), }; assert!(is_safe_token(data)); } #[test] fn unsafe_token_due_to_mint_authority() { let data = TokenData { name: "".to_string(), symbol: "".to_string(), dev_balance: 10.0, supply: 1000, mint_authority: true, freeze_authority: false, uri: "".to_string(), }; assert!(!is_safe_token(data)); } #[test] fn unsafe_token_due_to_freeze_authority() { let data = TokenData { name: "".to_string(), symbol: "".to_string(), dev_balance: 10.0, supply: 1000, mint_authority: false, freeze_authority: true, uri: "".to_string(), }; assert!(!is_safe_token(data)); } #[test] fn unsafe_token_due_to_high_dev_balance() { let data = TokenData { name: "".to_string(), symbol: "".to_string(), dev_balance: 250.0, supply: 1000, mint_authority: false, freeze_authority: false, uri: "".to_string(), }; assert!(!is_safe_token(data)); } }