build meteora swap and send it. It fails
This commit is contained in:
17
.env
17
.env
@@ -1,10 +1,25 @@
|
|||||||
YELLOWSTONE_ENDPOINT=https://solana-yellowstone-grpc.publicnode.com:443
|
YELLOWSTONE_ENDPOINT=https://solana-yellowstone-grpc.publicnode.com:443
|
||||||
YELLOWSTONE_COMMITMENT=processed
|
YELLOWSTONE_COMMITMENT=processed
|
||||||
|
|
||||||
|
RPC_URL=https://solana-rpc.publicnode.com
|
||||||
|
RPC_COMMITMENT=processed
|
||||||
|
SIMULATE_FIRST=true
|
||||||
|
|
||||||
|
|
||||||
BUNDLER=5jYaYv7HoiFVrY9bAcruj6dH8fCBseky4sBmnTFGSaeW
|
BUNDLER=5jYaYv7HoiFVrY9bAcruj6dH8fCBseky4sBmnTFGSaeW
|
||||||
|
|
||||||
RUST_LOG=info
|
RUST_LOG=info
|
||||||
|
|
||||||
BIN_PATH=logs/frames/bin/1762440223791_slot378317940_4t6rKnrWTjmM_tx.bin
|
BIN_PATH=logs/frames/bin/1762440223791_slot378317940_4t6rKnrWTjmM_tx.bin
|
||||||
# Wallet
|
# 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
3
Cargo.lock
generated
@@ -2744,6 +2744,9 @@ name = "sniper-bot"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bincode",
|
||||||
"bs58",
|
"bs58",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"futures",
|
"futures",
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|||||||
|
|
||||||
# TLS / HTTP libs si las necesitas luego (reqwest usa rustls)
|
# TLS / HTTP libs si las necesitas luego (reqwest usa rustls)
|
||||||
reqwest = { version = "0.12", features = ["rustls-tls", "json"] }
|
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 si lo necesitas para otras cosas (dejamos vendored)
|
||||||
openssl = { version = "0.10", features = ["vendored"] }
|
openssl = { version = "0.10", features = ["vendored"] }
|
||||||
|
|||||||
92
src/engine.rs
Normal file
92
src/engine.rs
Normal 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))
|
||||||
|
}
|
||||||
@@ -1,2 +1,5 @@
|
|||||||
|
pub mod utils;
|
||||||
pub mod listener;
|
pub mod listener;
|
||||||
pub mod utils;
|
pub mod engine;
|
||||||
|
pub mod protocols;
|
||||||
|
pub mod rpc;
|
||||||
91
src/main.rs
91
src/main.rs
@@ -1,9 +1,10 @@
|
|||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use yellowstone_grpc_proto::geyser::SubscribeUpdateTransaction;
|
use yellowstone_grpc_proto::geyser::SubscribeUpdateTransaction;
|
||||||
|
use yellowstone_grpc_proto::prost::Message;
|
||||||
use bs58;
|
use bs58;
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use tracing_subscriber::EnvFilter;
|
use std::fs;
|
||||||
|
|
||||||
use sniper_bot::listener;
|
use sniper_bot::listener;
|
||||||
use sniper_bot::listener::YellowstoneSource;
|
use sniper_bot::listener::YellowstoneSource;
|
||||||
@@ -11,8 +12,14 @@ use sniper_bot::listener::YellowstoneSource;
|
|||||||
use sniper_bot::utils::{
|
use sniper_bot::utils::{
|
||||||
save_tx_update,
|
save_tx_update,
|
||||||
load_keypair_and_pubkey_from_json,
|
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]
|
#[tokio::main]
|
||||||
@@ -22,17 +29,69 @@ async fn main() -> Result<()> {
|
|||||||
// Load environment variables from .env file
|
// Load environment variables from .env file
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
let bundler = std::env::var("BUNDLER")?;
|
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");
|
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
|
// Create a channel for transaction updates
|
||||||
let (tx, mut rx) = mpsc::channel::<SubscribeUpdateTransaction>(1024);
|
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
148
src/protocols.rs
Normal 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
239
src/rpc.rs
Normal 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)
|
||||||
|
}
|
||||||
19
src/utils.rs
19
src/utils.rs
@@ -3,6 +3,8 @@ use std::fs as std_fs;
|
|||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use bs58;
|
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).
|
/// 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`.
|
/// - 64 bytes: clave secreta completa (secret + public) -> `Keypair::from_bytes`.
|
||||||
/// - 32 bytes: semilla ed25519 -> `Keypair::from_seed`.
|
/// - 32 bytes: semilla ed25519 -> `Keypair::from_seed`.
|
||||||
pub fn load_keypair_and_pubkey_from_json<P: AsRef<Path>>(path: P) -> Result<(Keypair, Pubkey)> {
|
pub fn load_keypair_and_pubkey_from_json() -> Result<(Keypair, Pubkey)> {
|
||||||
let path_ref = path.as_ref();
|
let wallet_path = std::env::var("WALLET_PATH")?;
|
||||||
|
let path_ref = Path::new(&wallet_path);
|
||||||
let data = std_fs::read_to_string(path_ref)
|
let data = std_fs::read_to_string(path_ref)
|
||||||
.with_context(|| format!("Leyendo wallet JSON: {}", path_ref.display()))?;
|
.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))
|
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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user