build meteora swap and send it. It fails

This commit is contained in:
2025-11-06 20:33:54 +01:00
parent 5e05955308
commit 22480e2a6d
9 changed files with 596 additions and 21 deletions

17
.env
View File

@@ -1,10 +1,25 @@
YELLOWSTONE_ENDPOINT=https://solana-yellowstone-grpc.publicnode.com:443
YELLOWSTONE_COMMITMENT=processed
RPC_URL=https://solana-rpc.publicnode.com
RPC_COMMITMENT=processed
SIMULATE_FIRST=true
BUNDLER=5jYaYv7HoiFVrY9bAcruj6dH8fCBseky4sBmnTFGSaeW
RUST_LOG=info
BIN_PATH=logs/frames/bin/1762440223791_slot378317940_4t6rKnrWTjmM_tx.bin
# Wallet
WALLET_PATH=keys/wallet_01.json
WALLET_PATH=keys/wallet_01.json
# Compra
BUY_MODE=amount # amount | percent
BUY_AMOUNT_LAMPORTS=20000000 # 0.02 SOL en lamports (si BUY_MODE=amount)
BUY_WSOL=true # wrap SOL -> wSOL ATA (si hace falta)
# Slippage y prioridad
MAX_SLIPPAGE_BPS=150 # 1.50% (bps = basis points)
PRIORITY_FEE_MICROLAMPORTS=0 # 0 = sin prioridad extra (ajústalo si hace falta)
COMPUTE_UNIT_LIMIT=1200000 # si quieres imponer tu CU (o deja las del bundler)

3
Cargo.lock generated
View File

@@ -2744,6 +2744,9 @@ name = "sniper-bot"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"base64 0.22.1",
"bincode",
"bs58",
"dotenvy",
"futures",

View File

@@ -13,6 +13,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# TLS / HTTP libs si las necesitas luego (reqwest usa rustls)
reqwest = { version = "0.12", features = ["rustls-tls", "json"] }
async-trait = "0.1"
base64 = "0.22"
bincode = "1"
# openssl si lo necesitas para otras cosas (dejamos vendored)
openssl = { version = "0.10", features = ["vendored"] }

92
src/engine.rs Normal file
View File

@@ -0,0 +1,92 @@
// src/engine.rs
use anyhow::{anyhow, Result};
use yellowstone_grpc_proto::geyser::SubscribeUpdateTransaction;
use solana_sdk::{
instruction::CompiledInstruction,
message::{v0::LoadedAddresses, VersionedMessage},
pubkey::Pubkey,
};
#[derive(Clone, Debug)]
pub struct TxuSnapshot {
pub slot: u64,
pub sig_b58_short: String,
pub txu: SubscribeUpdateTransaction,
}
/// Plan neutral que debe devolver extract_plan
#[derive(Clone)]
pub struct SwapPlan {
/// El VersionedMessage original **(opcional)** si lo tienes; útil para rebuild.
pub original_message: Option<VersionedMessage>,
/// account_keys completos (en el mismo orden que el message)
pub account_keys: Vec<Pubkey>,
/// instrucciones tal cual (program_id_index, accounts indices, data)
pub instructions: Vec<CompiledInstruction>,
/// si aplica: loaded address lookups (ALT)
pub loaded_addresses: Option<Vec<LoadedAddresses>>,
/// cuentas que identificamos como 'pool PDAs' (no sustituir)
pub pool_accounts: Vec<Pubkey>,
/// cuentas identificadas como 'trader accounts' (ATAs/payer) que deberemos sustituir
pub trader_accounts: Vec<Pubkey>,
/// (Opcional) amounts / min_out extraídos por decodificar data
pub amounts: Option<(u64, u64)>, // ejemplo: (amount_in, min_out)
}
/// Resultado: mensaje preparado (VersionedMessage) listo para firmar.
/// Si tu pipeline necesita devolver VersionedTransaction, puedes adaptarlo.
pub struct PreparedMessage {
pub message: VersionedMessage,
}
pub trait SwapAdapter {
fn name(&self) -> &'static str;
fn probe(&self, snap: &TxuSnapshot) -> bool;
/// Extrae el SwapPlan desde el txu (analiza message/ixs/data)
fn extract_plan(&self, snap: &TxuSnapshot) -> Result<SwapPlan>;
/// Con un plan y tu owner (pubkey), construye el VersionedMessage listo para firmar.
fn build_message(&self, plan: SwapPlan, my_owner: &Pubkey) -> Result<PreparedMessage>;
}
/// Helper: crea snapshot simple desde txu
pub fn snapshot_from_txu(txu: SubscribeUpdateTransaction) -> TxuSnapshot {
let slot = txu.slot;
let sig_b58_short = txu
.transaction
.as_ref()
.and_then(|t| (!t.signature.is_empty()).then(|| bs58::encode(&t.signature).into_string()))
.map(|s| s.chars().take(12).collect::<String>())
.unwrap_or_else(|| "nosig".to_string());
TxuSnapshot { slot, sig_b58_short, txu }
}
/// Crea snapshot, selecciona adapter y devuelve el resultado.
pub fn prepare_and_build(
txu: SubscribeUpdateTransaction,
adapters: &[Box<dyn SwapAdapter>],
my_owner: &Pubkey,
) -> Result<(SwapPlan, PreparedMessage)> {
let snap = snapshot_from_txu(txu);
let adapter = adapters
.iter()
.find(|a| a.probe(&snap))
.ok_or_else(|| anyhow!("Ningún adapter reconoce este txu"))?;
let plan = adapter.extract_plan(&snap)?;
let prepared = adapter.build_message(plan.clone(), my_owner)?;
Ok((plan, prepared))
}

View File

@@ -1,2 +1,5 @@
pub mod utils;
pub mod listener;
pub mod utils;
pub mod engine;
pub mod protocols;
pub mod rpc;

View File

@@ -1,9 +1,10 @@
use anyhow::Result;
use anyhow::{Context, Result};
use tokio::sync::mpsc;
use yellowstone_grpc_proto::geyser::SubscribeUpdateTransaction;
use yellowstone_grpc_proto::prost::Message;
use bs58;
use dotenvy::dotenv;
use tracing_subscriber::EnvFilter;
use std::fs;
use sniper_bot::listener;
use sniper_bot::listener::YellowstoneSource;
@@ -11,8 +12,14 @@ use sniper_bot::listener::YellowstoneSource;
use sniper_bot::utils::{
save_tx_update,
load_keypair_and_pubkey_from_json,
init_tracing,
};
use sniper_bot::engine::prepare_and_build;
use sniper_bot::protocols::default_adapters;
use sniper_bot::rpc::{HttpRpc, Rpc, sign_versioned_tx};
use solana_sdk::{hash::Hash, message::VersionedMessage};
#[tokio::main]
@@ -22,17 +29,69 @@ async fn main() -> Result<()> {
// Load environment variables from .env file
dotenv().ok();
let bundler = std::env::var("BUNDLER")?;
let wallet_path = std::env::var("WALLET_PATH")?;
// Wallet
let (kp, my_owner) = load_keypair_and_pubkey_from_json(&wallet_path)?;
println!("🔑 Owner: {my_owner}");
// Testing conditions
//
let simulate_first = std::env::var("SIMULATE_FIRST").unwrap_or_else(|_| "true".to_string()) == "true";
let bin_path = std::env::var("BIN_PATH").context("Falta BIN_PATH en .env (o variable de entorno)")?;
//
// Testing conditions
// Load wallet
let (kp, kp_pub) = load_keypair_and_pubkey_from_json()?;
eprintln!("Keypair pubkey: {}", kp_pub);
tracing::info!("[INFO] Bot initialited");
// RPC client
let rpc = HttpRpc::new()?;
// Testing conditions
//
// Cargar y decodificar el BIN a txu (protobuf)
let bytes = fs::read(&bin_path).with_context(|| format!("Leyendo binario: {bin_path}"))?;
let txu = SubscribeUpdateTransaction::decode(bytes.as_slice()).context("Decodificando prost: SubscribeUpdateTransaction")?;
//
// Testing conditions
let adapters = default_adapters();
// === A partir de aquí: orquestar compra ===
// 1) Plan + mensaje preparado (por ahora: replay 1:1)
let (_plan, prepared) = prepare_and_build(txu, &adapters, &kp_pub)?;
// 2) Blockhash fresco + slot
let (new_bh, ctx_slot) = rpc.get_latest_blockhash().await?;
println!("Latest slot solana: {ctx_slot}");
// 3) Actualiza blockhash del mensaje
let vm = set_recent_blockhash(prepared.message, new_bh);
// 4) Firma con tu keypair
let vtx = sign_versioned_tx(vm, &[&kp])?;
// 5) Simula (opcional)
if simulate_first {
rpc.simulate(&vtx, Some(ctx_slot)).await?;
println!("🧪 simulateTransaction OK");
}
// 6) Enviar
let sig = rpc.send_transaction(&vtx, Some(ctx_slot)).await?;
println!("🚀 Enviada: {}", sig);
// Testing conditions
//
return Ok(());
//
// Testing conditions
// Create a channel for transaction updates
let (tx, mut rx) = mpsc::channel::<SubscribeUpdateTransaction>(1024);
@@ -77,15 +136,13 @@ async fn main() -> Result<()> {
}
fn init_tracing() {
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));
// No peta si ya estaba inicializado (devuelve Err y lo ignoramos)
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.try_init();
}
fn set_recent_blockhash(mut vm: VersionedMessage, new_bh: Hash) -> VersionedMessage {
match &mut vm {
VersionedMessage::V0(m) => { m.recent_blockhash = new_bh; }
VersionedMessage::Legacy(m) => { m.recent_blockhash = new_bh; }
}
vm
}

148
src/protocols.rs Normal file
View File

@@ -0,0 +1,148 @@
//! protocols.rs
//! Stubs de adapters. Hoy: Meteora “siempre sí” para probar el flujo.
//! Luego añades PumpFun/Bonk con su probe real y (más tarde) extract/build.
use anyhow::{anyhow, Result};
use solana_sdk::{
instruction::CompiledInstruction,
message::{
v0::{Message as V0Message, MessageAddressTableLookup as V0AddressTableLookup},
MessageHeader, VersionedMessage,
},
pubkey::Pubkey,
hash::Hash,
};
use crate::engine::{PreparedMessage, SwapAdapter, SwapPlan, TxuSnapshot};
pub struct MeteoraPoolsAdapter;
impl MeteoraPoolsAdapter { pub fn new() -> Self { Self } }
impl SwapAdapter for MeteoraPoolsAdapter {
fn name(&self) -> &'static str { "MeteoraPools" }
fn probe(&self, snap: &TxuSnapshot) -> bool {
// Aquí podrías filtrar por logs o por program_id de Meteora.
// Para no bloquear el flujo, seguimos aceptando.
let _ = snap;
true
}
fn extract_plan(&self, snap: &TxuSnapshot) -> Result<SwapPlan> {
// 1) Acceder al Message del protobuf (tal como viene en tu txu)
let txi = snap.txu.transaction.as_ref()
.ok_or_else(|| anyhow!("txu.transaction = None"))?;
let msg_pb = txi.transaction.as_ref()
.and_then(|t| t.message.as_ref())
.ok_or_else(|| anyhow!("txu.transaction.message = None"))?;
// 2) Header
let header_pb = msg_pb.header.as_ref()
.ok_or_else(|| anyhow!("txu.message.header = None"))?;
let header = MessageHeader {
num_required_signatures: header_pb.num_required_signatures as u8,
num_readonly_signed_accounts: header_pb.num_readonly_signed_accounts as u8,
num_readonly_unsigned_accounts: header_pb.num_readonly_unsigned_accounts as u8,
};
// 3) account_keys: [[u8;32]] -> Vec<Pubkey>
let account_keys: Vec<Pubkey> = msg_pb.account_keys.iter().map(|k32| {
let mut arr = [0u8; 32];
arr.copy_from_slice(&k32[..]);
Pubkey::new_from_array(arr)
}).collect();
// 4) recent_blockhash: [u8;32] -> Hash
let mut bh = [0u8; 32];
bh.copy_from_slice(&msg_pb.recent_blockhash[..]);
let recent_blockhash: Hash = bh.into();
// 5) instructions
let instructions: Vec<CompiledInstruction> = msg_pb.instructions.iter().map(|ix| {
CompiledInstruction {
program_id_index: ix.program_id_index as u8,
accounts: ix.accounts.iter().map(|&i| i as u8).collect(),
data: ix.data.clone(),
}
}).collect();
// 6) Address Lookup Tables (V0)
let address_table_lookups: Vec<V0AddressTableLookup> = msg_pb.address_table_lookups.iter().map(|l| {
let mut key = [0u8; 32];
key.copy_from_slice(&l.account_key[..]);
V0AddressTableLookup {
account_key: Pubkey::new_from_array(key),
writable_indexes: l.writable_indexes.clone(),
readonly_indexes: l.readonly_indexes.clone(),
}
}).collect();
// 7) Mensaje V0 reconstruido
let message_v0 = V0Message {
header,
account_keys: account_keys.clone(),
recent_blockhash,
instructions: instructions.clone(),
address_table_lookups,
};
let original_vm = VersionedMessage::V0(message_v0);
// 8) trader/pool accounts (queda pendiente hasta que metas tu heurística/ABI)
let trader_accounts: Vec<Pubkey> = Vec::new();
let pool_accounts: Vec<Pubkey> = Vec::new();
// 9) Por ahora, pool_accounts vacío (identificar por ABI es un siguiente paso)
let pool_accounts: Vec<Pubkey> = Vec::new();
Ok(SwapPlan {
original_message: Some(original_vm),
account_keys,
instructions,
loaded_addresses: None, // no hace falta aquí porque ya están en V0.address_table_lookups
pool_accounts,
trader_accounts,
amounts: None,
})
}
fn build_message(&self, mut plan: SwapPlan, my_owner: &Pubkey) -> Result<PreparedMessage> {
// Debe existir el mensaje original (v0 o legacy)
let mut vm = plan
.original_message
.ok_or_else(|| anyhow!("No original_message in plan"))?;
match &mut vm {
VersionedMessage::V0(m) => {
// Los primeros N account_keys son firmantes
let n = m.header.num_required_signatures as usize;
if n == 0 {
return Err(anyhow!("Mensaje sin firmantes (num_required_signatures=0)"));
}
// Reemplaza TODOS los firmantes por tu pubkey (lo normal es N=1)
for i in 0..n {
m.account_keys[i] = *my_owner;
}
// (opcional) si sospechas que había >1 firmante y tú solo vas a firmar con 1:
// m.header.num_required_signatures = 1; // y ajusta account_keys[0]=*my_owner
}
VersionedMessage::Legacy(m) => {
let n = m.header.num_required_signatures as usize;
if n == 0 {
return Err(anyhow!("Mensaje legacy sin firmantes (num_required_signatures=0)"));
}
for i in 0..n {
m.account_keys[i] = *my_owner;
}
// (opcional) m.header.num_required_signatures = 1;
}
}
Ok(PreparedMessage { message: vm })
}
}
pub fn default_adapters() -> Vec<Box<dyn SwapAdapter>> {
vec![
Box::new(MeteoraPoolsAdapter::new())
]
}

239
src/rpc.rs Normal file
View File

@@ -0,0 +1,239 @@
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use bs58;
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
use bincode;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use solana_sdk::{
hash::Hash,
signature::Keypair, // <- quitamos Signer import directo (no hace falta)
transaction::VersionedTransaction,
message::VersionedMessage,
};
#[async_trait]
pub trait Rpc {
async fn get_latest_blockhash(&self) -> Result<(Hash, u64)>;
async fn simulate(&self, tx: &VersionedTransaction, min_context_slot: Option<u64>) -> Result<()>;
async fn send_transaction(&self, tx: &VersionedTransaction, min_context_slot: Option<u64>) -> Result<String>;
}
#[derive(Deserialize)]
struct RpcContext<T> {
context: RpcContextInfo,
value: T,
}
#[derive(Deserialize)]
struct RpcContextInfo {
slot: u64,
}
#[derive(Clone)]
pub struct HttpRpc {
client: Client,
url: String,
commitment: String,
}
impl HttpRpc {
pub fn new() -> Result<Self> {
let rpc_url = std::env::var("RPC_URL")
.context("Falta RPC_URL en .env")?;
let commitment = std::env::var("RPC_COMMITMENT")
.unwrap_or_else(|_| "processed".to_string());
Ok(Self {
client: Client::new(),
url: rpc_url.into(),
commitment: commitment.into(),
})
}
}
/* ========= JSON-RPC payloads ========= */
#[derive(Serialize)]
struct RpcRequest<'a, T> {
jsonrpc: &'static str,
id: u64,
method: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
params: Option<T>,
}
#[derive(Deserialize, Debug)]
struct RpcResponse<T> {
jsonrpc: String,
id: u64,
// OJO: no pongas #[serde(default)] en genéricos; Option ya deserializa a None sin T: Default
result: Option<T>,
error: Option<RpcError>,
}
#[derive(Deserialize, Debug)]
struct RpcError {
code: i64,
message: String,
#[serde(default)]
data: serde_json::Value,
}
#[derive(Deserialize)]
struct GetLatestBlockhashResult {
value: GetLatestBlockhashValue,
}
#[derive(Deserialize)]
struct GetLatestBlockhashValue {
blockhash: String,
}
#[derive(Serialize)]
struct SimSendConfig<'a> {
encoding: &'a str, // "base64"
#[serde(rename = "sigVerify")]
sig_verify: bool, // false
#[serde(rename = "replaceRecentBlockhash")]
replace_recent_blockhash: bool, // true/false
#[serde(rename = "commitment")]
commitment: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
preflight_commitment: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
skip_preflight: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", rename = "minContextSlot")]
min_context_slot: Option<u64>,
}
#[async_trait]
impl Rpc for HttpRpc {
async fn get_latest_blockhash(&self) -> Result<(Hash, u64)> {
let req = RpcRequest {
jsonrpc: "2.0",
id: 1,
method: "getLatestBlockhash",
params: Some(vec![serde_json::json!({
"commitment": self.commitment
})]),
};
let resp: RpcResponse<RpcContext<GetLatestBlockhashValue>> = self
.client
.post(&self.url)
.json(&req)
.send()
.await
.context("HTTP getLatestBlockhash")?
.json()
.await
.context("JSON getLatestBlockhash")?;
if let Some(err) = resp.error {
return Err(anyhow!("RPC error getLatestBlockhash: {} {:?}", err.message, err.data));
}
let r = resp.result.ok_or_else(|| anyhow!("RPC: sin result en getLatestBlockhash"))?;
let bh_bytes = bs58::decode(&r.value.blockhash)
.into_vec()
.context("decode base58 blockhash")?;
let mut arr = [0u8; 32];
arr.copy_from_slice(&bh_bytes[..]);
let hash = Hash::new_from_array(arr);
let slot = r.context.slot;
Ok((hash, slot))
}
async fn simulate(&self, tx: &VersionedTransaction, min_context_slot: Option<u64>) -> Result<()> {
let tx_b64 = serialize_tx_base64(tx)?;
let cfg = SimSendConfig {
encoding: "base64",
sig_verify: false,
replace_recent_blockhash: false, // usa el mismo blockhash que envías
commitment: &self.commitment,
preflight_commitment: Some(&self.commitment),
skip_preflight: None,
min_context_slot,
};
let req = RpcRequest {
jsonrpc: "2.0",
id: 1,
method: "simulateTransaction",
params: Some(vec![serde_json::json!(tx_b64), serde_json::to_value(cfg)?]),
};
let resp: RpcResponse<serde_json::Value> = self
.client
.post(&self.url)
.json(&req)
.send()
.await
.context("HTTP simulateTransaction")?
.json()
.await
.context("JSON simulateTransaction")?;
if let Some(err) = resp.error {
return Err(anyhow!("RPC error simulateTransaction: {} {:?}", err.message, err.data));
}
Ok(())
}
async fn send_transaction(&self, tx: &VersionedTransaction, min_context_slot: Option<u64>) -> Result<String> {
let tx_b64 = serialize_tx_base64(tx)?;
let cfg = SimSendConfig {
encoding: "base64",
sig_verify: false,
replace_recent_blockhash: false,
commitment: &self.commitment,
preflight_commitment: Some(&self.commitment),
skip_preflight: Some(false),
min_context_slot, // 👈 importante
};
let req = RpcRequest {
jsonrpc: "2.0",
id: 1,
method: "sendTransaction",
params: Some(vec![serde_json::json!(tx_b64), serde_json::to_value(cfg)?]),
};
let resp: RpcResponse<String> = self
.client
.post(&self.url)
.json(&req)
.send()
.await
.context("HTTP sendTransaction")?
.json()
.await
.context("JSON sendTransaction")?;
if let Some(err) = resp.error {
return Err(anyhow!("RPC error sendTransaction: {} {:?}", err.message, err.data));
}
let sig_b58 = resp.result.ok_or_else(|| anyhow!("RPC: sin result en sendTransaction"))?;
Ok(sig_b58)
}
}
/* ========= Helpers ========= */
fn serialize_tx_base64(tx: &VersionedTransaction) -> Result<String> {
let bytes = bincode::serialize(tx).context("bincode serialize VersionedTransaction")?;
Ok(B64.encode(bytes))
}
/// Firma una VersionedTransaction con el/los keypair(s) dados.
pub fn sign_versioned_tx(message: VersionedMessage, signers: &[&Keypair]) -> Result<VersionedTransaction> {
let tx = VersionedTransaction::try_new(message, signers)
.map_err(|e| anyhow!("VersionedTransaction::try_new: {e}"))?;
Ok(tx)
}

View File

@@ -3,6 +3,8 @@ use std::fs as std_fs;
use std::time::{SystemTime, UNIX_EPOCH};
use std::convert::TryFrom;
use tracing_subscriber::EnvFilter;
use anyhow::{anyhow, Context, Result};
use bs58;
@@ -17,8 +19,9 @@ use solana_sdk::signature::{Keypair, Signer};
/// Carga un keypair desde un JSON (array de 64 u 32 enteros) y devuelve (Keypair, Pubkey).
/// - 64 bytes: clave secreta completa (secret + public) -> `Keypair::from_bytes`.
/// - 32 bytes: semilla ed25519 -> `Keypair::from_seed`.
pub fn load_keypair_and_pubkey_from_json<P: AsRef<Path>>(path: P) -> Result<(Keypair, Pubkey)> {
let path_ref = path.as_ref();
pub fn load_keypair_and_pubkey_from_json() -> Result<(Keypair, Pubkey)> {
let wallet_path = std::env::var("WALLET_PATH")?;
let path_ref = Path::new(&wallet_path);
let data = std_fs::read_to_string(path_ref)
.with_context(|| format!("Leyendo wallet JSON: {}", path_ref.display()))?;
@@ -94,3 +97,15 @@ pub async fn save_tx_update(txu: &SubscribeUpdateTransaction) -> Result<(PathBuf
Ok((bin_path, txt_path))
}
pub fn init_tracing() {
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));
// No peta si ya estaba inicializado (devuelve Err y lo ignoramos)
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.try_init();
}