1use anyhow::Result;
2use base64::{engine::general_purpose::STANDARD, Engine as _};
3use bincode;
4use once_cell::sync::Lazy;
5use serde::{Deserialize, Serialize};
6use solana_sdk::transaction::VersionedTransaction;
7use std::str::FromStr;
8use std::sync::Arc;
9use std::time::Duration;
10use tokio::sync::RwLock;
11
12#[derive(Deserialize, Debug, Serialize)]
14struct RpcResponse {
15 result: BlockhashResponse,
16}
17
18#[derive(Deserialize, Debug, Serialize)]
19struct BlockhashResponse {
20 value: BlockhashValue,
21}
22
23#[derive(Deserialize, Debug, Serialize)]
24struct BlockhashValue {
25 blockhash: String,
26}
27
28#[derive(Debug, thiserror::Error)]
29pub enum BlockhashCacheError {
30 #[error("[BlockhashCache] Failed to fetch blockhash")]
31 FetchError(reqwest::Error),
32
33 #[error("[BlockhashCache] Failed to parse blockhash")]
34 ParseError(reqwest::Error),
35
36 #[error("[BlockhashCache] Failed to convert blockhash to Hash")]
37 HashConversionError,
38}
39
40pub static BLOCKHASH_CACHE: Lazy<BlockhashCache> = Lazy::new(|| {
41 BlockhashCache::new(&std::env::var("SOLANA_RPC_URL").expect("SOLANA_RPC_URL must be set"))
42});
43
44pub struct BlockhashCache {
45 blockhash: Arc<RwLock<solana_sdk::hash::Hash>>,
46 client: reqwest::Client,
47 rpc_url: String,
48}
49
50impl BlockhashCache {
51 pub fn new(rpc_url: &str) -> Self {
52 let client = reqwest::Client::new();
53 let blockhash = Arc::new(RwLock::new(solana_sdk::hash::Hash::default()));
54 let rpc_url = rpc_url.to_string();
55
56 let cache = Self {
57 blockhash,
58 client,
59 rpc_url,
60 };
61 cache.start_update_task();
62 cache
63 }
64
65 fn start_update_task(&self) {
66 let blockhash = self.blockhash.clone();
67 let client = self.client.clone();
68 let rpc_url = self.rpc_url.clone();
69
70 tokio::spawn(async move {
71 loop {
72 match Self::fetch_blockhash(&client, &rpc_url).await {
73 Ok(new_blockhash) => {
74 let mut hash_writer = blockhash.write().await;
75 *hash_writer = new_blockhash;
76 drop(hash_writer);
77 }
78 Err(err) => {
79 tracing::error!("Failed to fetch blockhash: {}", err);
80 }
81 }
82
83 tokio::time::sleep(Duration::from_secs(2)).await;
84 }
85 });
86 }
87
88 async fn fetch_blockhash(
89 client: &reqwest::Client,
90 rpc_url: &str,
91 ) -> Result<solana_sdk::hash::Hash, BlockhashCacheError> {
92 let request_body = serde_json::json!({
93 "jsonrpc": "2.0",
94 "id": 1,
95 "method": "getLatestBlockhash",
96 "params": [{"commitment": "finalized"}]
97 });
98
99 let response = client
100 .post(rpc_url)
101 .json(&request_body)
102 .send()
103 .await
104 .map_err(BlockhashCacheError::FetchError)?;
105 let response_json: RpcResponse = response
106 .json()
107 .await
108 .map_err(BlockhashCacheError::ParseError)?;
109
110 let blockhash = response_json.result.value.blockhash;
112 let hash = solana_sdk::hash::Hash::from_str(&blockhash)
113 .map_err(|_| BlockhashCacheError::HashConversionError)?;
114 Ok(hash)
115 }
116
117 pub async fn get_blockhash(&self) -> Result<solana_sdk::hash::Hash, BlockhashCacheError> {
118 let current_hash = self.blockhash.read().await.clone();
119
120 if current_hash != solana_sdk::hash::Hash::default() {
122 return Ok(current_hash);
123 }
124
125 Self::fetch_blockhash(&self.client, &self.rpc_url).await
127 }
128}
129
130pub fn inject_blockhash_into_encoded_tx(base64_tx: &str, blockhash: &str) -> Result<String> {
131 let tx_bytes = STANDARD.decode(base64_tx)?;
133
134 let updated_tx_bytes = match bincode::deserialize::<VersionedTransaction>(&tx_bytes) {
136 Ok(mut transaction) => {
137 let blockhash = solana_sdk::hash::Hash::from_str(&blockhash)
139 .map_err(|_| anyhow::anyhow!("Invalid blockhash format"))?;
140
141 match &mut transaction.message {
142 solana_sdk::message::VersionedMessage::Legacy(message) => {
143 message.recent_blockhash = blockhash;
144 }
145 solana_sdk::message::VersionedMessage::V0(message) => {
146 message.recent_blockhash = blockhash;
147 }
148 }
149 bincode::serialize(&transaction)?
150 }
151 Err(_) => {
152 let mut transaction =
154 bincode::deserialize::<solana_sdk::transaction::Transaction>(&tx_bytes)?;
155
156 let blockhash = solana_sdk::hash::Hash::from_str(&blockhash)
157 .map_err(|_| anyhow::anyhow!("Invalid blockhash format"))?;
158
159 transaction.message.recent_blockhash = blockhash;
160 bincode::serialize(&transaction)?
161 }
162 };
163
164 Ok(STANDARD.encode(updated_tx_bytes))
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use solana_sdk::{
172 message::Message,
173 pubkey::Pubkey,
174 signature::{Keypair, Signer},
175 system_instruction,
176 transaction::Transaction,
177 };
178
179 #[tokio::test]
180 async fn test_blockhash_cache() {
181 dotenv::dotenv().ok();
182 let blockhash = super::BLOCKHASH_CACHE.get_blockhash().await.unwrap();
183 assert_ne!(blockhash, solana_sdk::hash::Hash::default());
184 }
185
186 #[test]
187 fn test_inject_blockhash_standard_transaction() {
188 let payer = Keypair::new();
190 let to = Pubkey::new_unique();
191 let instruction = system_instruction::transfer(&payer.pubkey(), &to, 1000);
192 let message = Message::new(&[instruction], Some(&payer.pubkey()));
193 let tx = Transaction::new_unsigned(message);
194
195 let tx_bytes = bincode::serialize(&tx).unwrap();
197 let base64_tx = STANDARD.encode(&tx_bytes);
198
199 let new_blockhash = "CkqVVMoo6LUqzqKSQVFNL4Yxv3TXyTh1NQxTSG2Z9gTq";
201 let result = inject_blockhash_into_encoded_tx(&base64_tx, new_blockhash).unwrap();
202
203 let updated_bytes = STANDARD.decode(result).unwrap();
205 let updated_tx: Transaction = bincode::deserialize(&updated_bytes).unwrap();
206
207 assert_eq!(
208 updated_tx.message.recent_blockhash.to_string(),
209 new_blockhash
210 );
211 }
212}