build meteora swap and send it. It fails
This commit is contained in:
15
.env
15
.env
@@ -1,6 +1,11 @@
|
||||
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
|
||||
@@ -8,3 +13,13 @@ RUST_LOG=info
|
||||
BIN_PATH=logs/frames/bin/1762440223791_slot378317940_4t6rKnrWTjmM_tx.bin
|
||||
# Wallet
|
||||
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"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"bincode",
|
||||
"bs58",
|
||||
"dotenvy",
|
||||
"futures",
|
||||
|
||||
@@ -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
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 listener;
|
||||
pub mod utils;
|
||||
pub mod listener;
|
||||
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 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
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::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();
|
||||
}
|
||||
Reference in New Issue
Block a user