462 lines
19 KiB
Rust
Raw Normal View History

2025-03-27 08:17:57 +01:00
use bs58;
use chrono;
2025-03-27 08:17:57 +01:00
use solana_sdk::message::Message;
use anyhow::anyhow;
use anyhow::Result;
2025-04-03 08:16:44 +02:00
use base64::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};
use chrono::Utc;
2025-04-03 08:16:44 +02:00
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};
2025-04-03 08:16:44 +02:00
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;
2025-04-03 08:16:44 +02:00
mod utils;
mod dto;
2025-03-27 08:17:57 +01:00
lazy_static! {
static ref RPC_CLIENT: RpcClient =
RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
}
2025-03-27 08:17:57 +01:00
fn main() {
// Adresse du programme Token (SPL Token Program) --> pumpfun
2025-04-03 08:16:44 +02:00
let token_program_address = "TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM";
let buy_router_address = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
2025-03-27 08:17:57 +01:00
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);
2025-03-27 08:17:57 +01:00
match signatures {
2025-03-27 08:17:57 +01:00
Ok(sigs) => {
debug!(
"{}",
format!("Nombre total de transactions trouvées : {}", sigs.len())
);
for sig in sigs.iter().take(1000) {
2025-04-03 08:16:44 +02:00
let signature = Signature::from_str(&sig.signature).unwrap();
info!("================ Signature ========================");
2025-04-03 08:16:44 +02:00
match get_transactions(&RPC_CLIENT, &signature) {
2025-03-27 08:17:57 +01:00
Ok(tx) => {
2025-04-03 08:16:44 +02:00
if let Some(meta) = tx.transaction.meta {
if let option_serializer::OptionSerializer::Some(
inner_instructions,
) = meta.inner_instructions
{
2025-04-03 08:16:44 +02:00
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)
}
2025-04-03 08:16:44 +02:00
_ => 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::<VersionedTransaction>(
&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;
2025-04-03 08:16:44 +02:00
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
);
}
}
2025-04-03 08:16:44 +02:00
}
Err(_) => {}
};
}
2025-04-03 08:16:44 +02:00
println!(
"============================================="
);
}
_ => {}
}
2025-04-03 08:16:44 +02:00
}
}
2025-03-27 08:17:57 +01:00
}
Err(err) => eprintln!(
"Erreur lors de la récupération de la transaction : {:?}",
err
),
2025-03-27 08:17:57 +01:00
}
}
}
Err(err) => eprintln!(
"Erreur lors de la récupération des transactions : {:?}",
err
),
2025-03-27 08:17:57 +01:00
}
}
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<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())
}
2025-04-03 08:16:44 +02:00
}
2025-04-03 08:16:44 +02:00
}
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<f64> {
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);
2025-04-03 08:16:44 +02:00
}
Ok(total_balance)
2025-04-03 08:16:44 +02:00
}
fn fetch_account(rpc_client: &RpcClient, mint_pubkey: &Pubkey) -> Result<Account> {
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)))
}
}
2025-04-03 08:16:44 +02:00
}
fn get_token_info(client: &RpcClient, mint_pubkey: &Pubkey) -> Result<TokenInfo> {
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<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)
2025-04-03 08:16:44 +02:00
}
#[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));
2025-04-03 08:16:44 +02:00
}
#[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));
2025-04-03 08:16:44 +02:00
}
#[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));
2025-04-03 08:16:44 +02:00
}
}