Retrieve token data and make some checks

This commit is contained in:
Alexandre RAY-BERNAT 2025-04-23 15:23:56 +02:00
parent 73fe72f829
commit b4a6da9a81
5 changed files with 2609 additions and 1288 deletions

3196
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,16 +4,27 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
solana-client = "1.17" solana-client = "2.2.6"
solana-sdk = "1.17" solana-sdk = "2.2.2"
bs58 = "0.5.0" bs58 = "0.5.0"
solana-commitment-config = "2.2.1" solana-commitment-config = "2.2.1"
solana-transaction-status-client-types = "2.2.4" solana-transaction-status-client-types = "2.2.4"
solana-transaction-status = "1.18.26" solana-transaction-status = "2.2.6"
spl-token = "8.0.0" spl-token = "8.0.0"
base64 = "0.22.1" base64 = "0.22.1"
bincode = "1.3.3" bincode = "1.3.3"
hex = "0.4" hex = "0.4"
borsh = "1.5.7"
borsh-derive = "1.5.7"
serde = { version = "1.0.219", features = ["derive"] }
solana-account-decoder = "2.2.6"
chrono = "0.4.40"
anyhow = "1.0"
lazy_static = "1.5.0"
mpl-token-metadata = "5.1.0"
solana-program = "2.2.1"
serde_json = "1.0.140"
log = "0.4.27"
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]

72
src/dto.rs Normal file
View File

@ -0,0 +1,72 @@
use borsh_derive::BorshDeserialize;
use serde::Deserialize;
use solana_program::clock::Slot;
use solana_program::program_option::COption;
use solana_program::pubkey::Pubkey;
use solana_sdk::transaction::TransactionError;
#[derive(BorshDeserialize, Debug, Deserialize)]
pub struct TokenInfo {
pub(crate) mint_address: String,
pub(crate) decimals: u8,
pub(crate) mint_authority: bool,
pub(crate) freeze_authority: bool,
pub(crate) supply: u64,
}
#[derive(BorshDeserialize, Debug, Deserialize)]
pub struct TokenAmount {
amount: String,
decimals: u8,
pub(crate) uiAmount: Option<f64>,
uiAmountString: String,
}
#[derive(BorshDeserialize, Debug, Deserialize)]
pub struct ParsedAccount {
pub(crate) info: TokenInfoWithAmount,
}
#[derive(BorshDeserialize, Debug, Deserialize)]
pub struct TokenInfoWithAmount {
isNative: bool,
mint: String,
owner: String,
state: String,
pub(crate) tokenAmount: TokenAmount,
}
#[derive(BorshDeserialize, Debug, Deserialize)]
pub struct UiParsedTokenAccount {
program: String,
parsed: ParsedAccount,
space: Option<u64>,
}
pub struct Transaction {
pub(crate) signature: String,
pub(crate) slot: Slot,
pub(crate) block_time: Option<i64>,
pub(crate) status: Option<TransactionError>,
pub(crate) account_keys: AccountKeys
}
pub struct AccountKeys {
pub(crate) payer: Pubkey,
pub(crate) mint_account: Pubkey,
pub(crate) rent_account: Pubkey,
pub(crate) mint_authority: Pubkey,
pub(crate) token_program: Pubkey
}
#[derive(Debug, Clone)]
pub struct TokenData {
pub(crate) name: String,
pub(crate) symbol: String,
pub(crate) uri: String,
pub(crate) supply: u64,
pub(crate) dev_balance: f64,
pub(crate) mint_authority: bool,
pub(crate) freeze_authority: bool,
}

View File

@ -1,250 +1,461 @@
use solana_client::rpc_client::RpcClient;
use solana_sdk::pubkey::Pubkey;
use bs58; use bs58;
use solana_sdk::instruction::Instruction; use chrono;
use solana_sdk::message::Message; use solana_sdk::message::Message;
use solana_client::rpc_response::RpcConfirmedTransactionStatusWithSignature;
use solana_sdk::transaction::Transaction; use anyhow::anyhow;
use solana_transaction_status::UiTransactionEncoding; use anyhow::Result;
use solana_sdk::signature::Signature;
use std::str::FromStr;
use spl_token::state::Mint;
use solana_transaction_status::EncodedTransaction::{Json, Binary};
use solana_transaction_status::UiMessage::{Parsed, Raw};
use base64::prelude::*; use base64::prelude::*;
use solana_sdk::message::VersionedMessage; use borsh::{BorshDeserialize, BorshSerialize};
use solana_sdk::transaction::VersionedTransaction; use chrono::Utc;
use hex; 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 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() { fn main() {
let rpc_url = "https://api.mainnet-beta.solana.com"; // Adresse du programme Token (SPL Token Program) --> pumpfun
let client = RpcClient::new(rpc_url.to_string());
// Adresse du programme Token (SPL Token Program)
let token_program_address = "TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM"; let token_program_address = "TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM";
let buy_router_address = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"; let buy_router_address = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
match base58_to_pubkey(token_program_address) { match base58_to_pubkey(token_program_address) {
Ok(token_program) => { Ok(token_program) => {
println!("Récupération des transactions pour le programme Token..."); info!(
"{}",
format!(
"Récupération des transactions sur le programme {:?}",
token_program_address
)
);
let signatures = RPC_CLIENT.get_signatures_for_address(&token_program);
// Récupérer les transactions associées au programme Token match signatures {
let transactions = client.get_signatures_for_address(&token_program);
match transactions {
Ok(sigs) => { Ok(sigs) => {
println!("Nombre total de transactions trouvées : {}", sigs.len()); debug!(
for sig in sigs.iter().take(1000) { // Limiter à 10 transactions pour commencer "{}",
// Convertir la signature en Signature format!("Nombre total de transactions trouvées : {}", sigs.len())
);
for sig in sigs.iter().take(1000) {
let signature = Signature::from_str(&sig.signature).unwrap(); let signature = Signature::from_str(&sig.signature).unwrap();
info!("================ Signature ========================");
let config = solana_client::rpc_config::RpcTransactionConfig { match get_transactions(&RPC_CLIENT, &signature) {
encoding: Some(UiTransactionEncoding::Base64),
commitment: Some(solana_sdk::commitment_config::CommitmentConfig::confirmed()),
max_supported_transaction_version: Some(0),
};
// Récupérer les détails de la transaction
match client.get_transaction_with_config(&signature, config) {
Ok(tx) => { Ok(tx) => {
if let Some(meta) = tx.transaction.meta { if let Some(meta) = tx.transaction.meta {
if let solana_transaction_status::option_serializer::OptionSerializer::Some(inner_instructions) = meta.inner_instructions { if let option_serializer::OptionSerializer::Some(
//Vérifier si c'est une création de token inner_instructions,
) = meta.inner_instructions
{
let is_token_creation = inner_instructions.iter().any(|inner| { let is_token_creation = inner_instructions.iter().any(|inner| {
inner.instructions.iter().any(|ix| { inner.instructions.iter().any(|ix| {
//println!("ix: {:?}", ix);
match ix { match ix {
solana_transaction_status::UiInstruction::Compiled(instruction) => { solana_transaction_status::UiInstruction::Compiled(instruction) => {
is_token_creation(instruction) is_token_creation(instruction)
}, }
_ => false _ => false
} }
}) })
}); });
// if is_token_creation { if !is_token_creation {
// println!("Création de token trouvée :"); continue
// println!(" Signature: {}", sig.signature); }
// println!(" Slot: {}", sig.slot);
// println!(" Block time: {:?}", sig.block_time);
// println!(" Status: {:?}", meta.err);
// println!("---");
// }
//println!("tx.transaction.transaction: {:?}", tx.transaction.transaction);
match tx.transaction.transaction { match tx.transaction.transaction {
Binary(data, encoding) => { Binary(data, encoding) => {
let decoded = BASE64_STANDARD.decode(data).unwrap(); let decoded = BASE64_STANDARD.decode(data).unwrap();
// Try to deserialize as a VersionedTransaction if let Ok(versioned_tx) =
if let Ok(versioned_tx) = bincode::deserialize::<VersionedTransaction>(&decoded) { bincode::deserialize::<VersionedTransaction>(
&decoded,
// dbg!(&versioned_tx); )
{
// Try to deserialize the message
if let Ok(message) = bincode::deserialize::<VersionedMessage>(&versioned_tx.message.serialize()) {
let account_keys = versioned_tx.message.static_account_keys(); let account_keys = versioned_tx.message.static_account_keys();
println!("\nAccount Keys in Transaction:"); let transaction = Transaction {
println!(" Payer (account_keys[0]): {}", account_keys[0]); signature: sig.clone().signature,
println!(" Mint Account (account_keys[1]): {}", account_keys[1]); slot: sig.clone().slot,
println!(" Rent Account (account_keys[2]): {}", account_keys[2]); block_time: sig.clone().block_time,
println!(" Mint Authority (account_keys[3]): {}", account_keys[3]); status: meta.err,
println!(" Token Program (account_keys[4]): {}", account_keys[4]); account_keys: AccountKeys {
// println!("Instructions: {:?}", message.compiled_instructions); 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(
match get_token_info(&client, account_keys[1].to_string().as_str()) { &RPC_CLIENT,
&mint_pubkey,
) {
Ok(token_info) => { Ok(token_info) => {
display_token_info(&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) => { Err(e) => {
println!("Erreur lors de la récupération des informations du token : {:?}", e); error!(
"Erreur lors de la récupération des informations du token : {:?}",
e
);
} }
} }
} }
Err(_) => {}
};
} }
// If not a versioned transaction, try as a legacy transaction println!(
if let Ok(legacy_tx) = bincode::deserialize::<Transaction>(&decoded) { "============================================="
// dbg!(&legacy_tx); );
} }
println!("--------------------------------");
// match json_transaction.message {
// Parsed(msg) => {
// println!("binary msg: {:?}", msg);
// // println!("Token trouvé : {}", &msg);
// // println!("Supply total : {}", &msg.supply);
// // println!("Decimals : {}", &msg.decimals);
// },
// Raw(msg) => {
// println!("Raw msg: {:?}", msg);
// // println!("Token trouvé : {}", &msg);
// // println!("Supply total : {}", &msg.supply);
// // println!("Decimals : {}", &msg.decimals);
// }
// }
},
_ => {} _ => {}
} }
} }
} }
} }
Err(err) => eprintln!("Erreur lors de la récupération de la transaction : {:?}", err), 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 récupération des transactions : {:?}",
err
),
} }
} }
Err(err) => eprintln!("Erreur lors de la conversion de l'adresse : {:?}", err), Err(err) => eprintln!("Erreur lors de la conversion de l'adresse : {:?}", err),
} }
} }
fn base58_to_pubkey(address: &str) -> Result<Pubkey, Box<dyn std::error::Error>> { fn is_safe_token(data: TokenData) -> bool {
let decoded = bs58::decode(address).into_vec()?; let holding_percentage = data.dev_balance * 100.0 / data.supply as f64;
if decoded.len() != 32 {
return Err("Address must be 32 bytes long".into()); let has_control = data.mint_authority || data.freeze_authority;
let holds_too_much = holding_percentage > 20.0;
!(has_control || holds_too_much)
} }
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&decoded);
Ok(Pubkey::new_from_array(bytes)) 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<Metadata> {
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<Pubkey> {
// 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::<Vec<_>>();
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::<Signature>()
.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<VersionedMessage> = match tx {
Binary(data, encoding) => {
let decoded = BASE64_STANDARD.decode(data)?;
if let Ok(versioned_tx) = bincode::deserialize::<VersionedTransaction>(&decoded)
{
if let Ok(message) = bincode::deserialize::<VersionedMessage>(
&versioned_tx.message.serialize(),
) {
Ok(message)
} else {
Err(anyhow!(""))
}
} else {
Err(anyhow!(""))
}
}
_ => Err(anyhow!("Encoding of transaction is not supported")),
};
let payer: Result<Message> = 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 { fn is_token_creation(instruction: &solana_transaction_status::UiCompiledInstruction) -> bool {
// L'instruction de création de token a un discriminant de 0 match bs58::decode(&instruction.data).into_vec() {
if instruction.data.len() < 1 { Ok(data) => {
return false; info!("It is a token creation");
matches!(data.first(), Some(0))
}
Err(_) => {
info!("not a token creation");
false
}
}
} }
// instruction.data == "0".to_string() fn get_dev_balance_for_token(rpc: &RpcClient, dev_pubkey: &Pubkey, mint: &Pubkey) -> Result<f64> {
println!("instruction: {:?}", instruction); let token_accounts =
true rpc.get_token_accounts_by_owner(dev_pubkey, TokenAccountsFilter::Mint(*mint))?;
//instruction.data[0] == 0
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);
} }
#[derive(Debug)] Ok(total_balance)
struct TokenInfo {
mint_address: String,
transaction_signature: String,
slot: u64,
block_time: Option<i64>,
decimals: u8,
mint_authority: Option<String>,
freeze_authority: Option<String>,
supply: u64,
} }
fn get_token_info(client: &RpcClient, mint_address: &str) -> Result<TokenInfo, Box<dyn std::error::Error>> { fn fetch_account(rpc_client: &RpcClient, mint_pubkey: &Pubkey) -> Result<Account> {
let mint_pubkey = Pubkey::from_str(mint_address)?; let config = RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::Base64),
..RpcAccountInfoConfig::default()
};
// Get the account data match rpc_client.get_account_with_config(&mint_pubkey, config) {
let account = client.get_account(&mint_pubkey)?; Ok(response) => {
match response.value {
Some(account) => {
Ok(account)
}
None => {
Err(anyhow::Error::msg(format!("Account not found")))
// Afficher les données brutes en hexadécimal pour le débogage }
println!("Données brutes en hex: {}", hex::encode(&account.data)); }
}
Err(e) => {
Err(anyhow::Error::msg(format!("RPC error {}", e)))
}
}
}
// Décoder les données en utilisant Mint::unpack fn get_token_info(client: &RpcClient, mint_pubkey: &Pubkey) -> Result<TokenInfo> {
let mint = Mint::unpack(&account.data)?;
// Afficher les informations décodées let account = fetch_account(client, mint_pubkey);
println!("\nInformations du token décodées:");
println!("Decimals: {}", mint.decimals);
println!("Supply: {}", mint.supply);
println!("Mint Authority: {:?}", mint.mint_authority);
println!("Freeze Authority: {:?}", mint.freeze_authority);
let mint = Mint::unpack(&*account?.data);
match mint {
Ok(mint) => {
Ok(TokenInfo { Ok(TokenInfo {
mint_address: mint_address.to_string(), mint_address: mint_pubkey.to_string(),
transaction_signature: "".to_string(),
slot: 0,
block_time: None,
decimals: mint.decimals, decimals: mint.decimals,
mint_authority: mint.mint_authority.map(|pk| pk.to_string()), mint_authority: mint.mint_authority.is_some(),
freeze_authority: mint.freeze_authority.map(|pk| pk.to_string()), freeze_authority: mint.freeze_authority.is_some(),
supply: mint.supply, supply: mint.supply,
}) })
} }
Err(e) => Err(anyhow::Error::msg(format!("ProgramError: {:?}", e))),
fn display_token_info(token: &TokenInfo) {
println!("\nToken Information:");
println!(" Mint Address: {}", token.mint_address);
println!(" Decimals: {}", token.decimals);
println!(" Supply: {}", token.supply);
match &token.mint_authority {
Some(auth) => println!(" Mint Authority: {}", auth),
None => println!(" Mint Authority: None (immutable)"),
} }
match &token.freeze_authority {
Some(auth) => println!(" Freeze Authority: {}", auth),
None => println!(" Freeze Authority: None (unfrozen)"),
}
println!(" Transaction: {}", token.transaction_signature);
println!(" Slot: {}", token.slot);
if let Some(time) = token.block_time {
println!(" Block Time: {}", time);
}
println!("---");
} }
fn get_transactions(
client: &RpcClient,
signature: &Signature,
) -> solana_client::client_error::Result<EncodedConfirmedTransactionWithStatusMeta> {
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));
}
}

15
src/utils.rs Normal file
View File

@ -0,0 +1,15 @@
use solana_program::pubkey::Pubkey;
pub fn sanitize_string(s: &str) -> String {
s.trim_end_matches('\0').to_string()
}
pub fn base58_to_pubkey(address: &str) -> anyhow::Result<Pubkey, Box<dyn std::error::Error>> {
let decoded = bs58::decode(address).into_vec()?;
if decoded.len() != 32 {
return Err("Address must be 32 bytes long".into());
}
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&decoded);
Ok(Pubkey::new_from_array(bytes))
}