357 lines
18 KiB
Rust
357 lines
18 KiB
Rust
use std::error::Error;
|
|
use bs58;
|
|
use chrono;
|
|
use solana_sdk::message::Message;
|
|
|
|
use crate::dto::{AccountKeys, ParsedAccount, TokenData, TokenInfo, Transaction, TokenSocials };
|
|
use crate::utils::{base58_to_pubkey, get_token_socials};
|
|
use anyhow::Result;
|
|
use anyhow::anyhow;
|
|
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::account::Account;
|
|
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 utils::sanitize_string;
|
|
use solana_rpc_calls::*;
|
|
use crate::telegram_publish::send_to_telegram;
|
|
|
|
mod dto;
|
|
mod utils;
|
|
mod solana_rpc_calls;
|
|
mod telegram_publish;
|
|
|
|
lazy_static! {
|
|
static ref RPC_CLIENT: RpcClient =
|
|
RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
|
|
}
|
|
|
|
#[tokio::main]
|
|
async 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::<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],
|
|
},
|
|
};
|
|
|
|
dbg!(&transaction);
|
|
|
|
let mint_pubkey =
|
|
transaction.account_keys.mint_account;
|
|
|
|
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 / 10u64.pow(token_info.decimals as u32),
|
|
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");
|
|
|
|
|
|
let token = "8176178685:AAHMlXsjx4ffH9sQ1v_eg1nkc-W9XUncIhE";
|
|
let chat_id = "945119667"; // can be a group chat too
|
|
|
|
let socials= get_token_socials(&token_data.uri).await.ok();
|
|
|
|
let socials = socials.unwrap_or_default();
|
|
|
|
let message = format!("🚨 #New token detected!\
|
|
\n**Name**: {} ({}) \
|
|
\n**Uri**: {} \
|
|
\nSignature: https://solscan.io/tx/{} \
|
|
\nMint address: https://solscan.io/token/{} \
|
|
\nDev address: https://solscan.io/account/{} \
|
|
\nSource: https://pump.fun/coin/{} \
|
|
\nDescription: {} \
|
|
\nTwitter: {} \
|
|
\nWebsite: {} \
|
|
\nImage: {} \
|
|
|
|
", token_data.name,
|
|
token_data.symbol,
|
|
token_data.uri,
|
|
transaction.signature,
|
|
transaction.account_keys.mint_account,
|
|
transaction.account_keys.payer,
|
|
transaction.account_keys.mint_account,
|
|
socials.description,
|
|
socials.twitter,
|
|
socials.website,
|
|
socials.image);
|
|
|
|
if let Err(e) = send_to_telegram(token, chat_id, message.as_str()).await {
|
|
eprintln!("Error sending to Telegram: {}", e);
|
|
}
|
|
|
|
|
|
|
|
} 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 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
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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));
|
|
}
|
|
}
|