use crate::base58::Pubkey;
use crate::versioned_tx::Blockhash;
use base64::prelude::{BASE64_STANDARD, Engine as _};
use serde_json::{json, Value};
use std::cell::RefCell;
use std::collections::VecDeque;
use std::str::FromStr;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct BlockhashInfo {
pub blockhash: Blockhash,
pub last_valid_block_height: u64,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct AccountInfo {
pub data: Vec<u8>,
pub owner: Pubkey,
pub lamports: u64,
pub executable: bool,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct TxSummary {
pub signature: String,
pub err: Option<String>,
pub slot: u64,
pub block_time: Option<i64>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum RpcError {
Transport(String),
Json(String),
Rpc { code: i64, message: String },
Unexpected(String),
Base58(String),
Base64(String),
}
pub trait Rpc {
fn get_latest_blockhash(&self) -> Result<BlockhashInfo, RpcError>;
fn get_account_info(&self, pubkey: &Pubkey) -> Result<Option<AccountInfo>, RpcError>;
fn get_signatures_for_address(&self, pubkey: &Pubkey, limit: usize) -> Result<Vec<TxSummary>, RpcError>;
fn send_transaction(&self, tx: &[u8]) -> Result<String, RpcError>;
}
pub fn rpc_request(id: u64, method: &str, params: Value) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params })
}
pub fn rpc_result(v: &Value) -> Result<&Value, RpcError> {
if let Some(err) = v.get("error") {
return Err(RpcError::Rpc {
code: err.get("code").and_then(|c| c.as_i64()).unwrap_or(0),
message: err.get("message").and_then(|m| m.as_str()).unwrap_or("").to_string(),
});
}
v.get("result")
.ok_or_else(|| RpcError::Unexpected("missing `result` or `error` in JSON-RPC response".to_string()))
}
pub fn parse_blockhash(v: &Value) -> Result<BlockhashInfo, RpcError> {
let value = v
.get("value")
.ok_or_else(|| RpcError::Unexpected("missing result.value".to_string()))?;
let bh_str = value
.get("blockhash")
.and_then(|b| b.as_str())
.ok_or_else(|| RpcError::Unexpected("missing blockhash".to_string()))?;
let blockhash = Pubkey::from_str(bh_str)
.map_err(|e| RpcError::Base58(e.to_string()))?
.to_bytes();
let last_valid_block_height = value
.get("lastValidBlockHeight")
.and_then(|h| h.as_u64())
.ok_or_else(|| RpcError::Unexpected("missing lastValidBlockHeight".to_string()))?;
Ok(BlockhashInfo { blockhash, last_valid_block_height })
}
pub fn parse_account_info(v: &Value) -> Result<Option<AccountInfo>, RpcError> {
let value = match v.get("value") {
None => return Err(RpcError::Unexpected("missing result.value".to_string())),
Some(Value::Null) => return Ok(None),
Some(v) => v,
};
let data_arr = value
.get("data")
.ok_or_else(|| RpcError::Unexpected("missing data".to_string()))?;
let b64 = data_arr
.get(0)
.and_then(|d| d.as_str())
.ok_or_else(|| RpcError::Unexpected("data[0] not a string".to_string()))?;
let data = BASE64_STANDARD
.decode(b64)
.map_err(|e| RpcError::Base64(e.to_string()))?;
let owner_str = value
.get("owner")
.and_then(|o| o.as_str())
.ok_or_else(|| RpcError::Unexpected("missing owner".to_string()))?;
let owner = Pubkey::from_str(owner_str).map_err(|e| RpcError::Base58(e.to_string()))?;
let lamports = value
.get("lamports")
.and_then(|l| l.as_u64())
.ok_or_else(|| RpcError::Unexpected("missing lamports".to_string()))?;
let executable = value.get("executable").and_then(|e| e.as_bool()).unwrap_or(false);
Ok(Some(AccountInfo { data, owner, lamports, executable }))
}
pub fn parse_signatures(v: &Value) -> Result<Vec<TxSummary>, RpcError> {
let arr = v
.as_array()
.ok_or_else(|| RpcError::Unexpected("signatures result not an array".to_string()))?;
let mut out = Vec::with_capacity(arr.len());
for item in arr {
let signature = item
.get("signature")
.and_then(|s| s.as_str())
.ok_or_else(|| RpcError::Unexpected("missing signature".to_string()))?
.to_string();
let err = match item.get("err") {
None | Some(Value::Null) => None,
Some(e) => Some(if let Some(s) = e.as_str() { s.to_string() } else { e.to_string() }),
};
let slot = item
.get("slot")
.and_then(|s| s.as_u64())
.ok_or_else(|| RpcError::Unexpected("missing slot".to_string()))?;
let block_time = item.get("blockTime").and_then(|b| b.as_i64());
out.push(TxSummary { signature, err, slot, block_time });
}
Ok(out)
}
pub fn parse_send_tx(v: &Value) -> Result<String, RpcError> {
v.as_str()
.map(|s| s.to_string())
.ok_or_else(|| RpcError::Unexpected("sendTransaction result not a string".to_string()))
}
pub struct WakiRpc {
endpoint: String,
api_key: Option<String>,
id: AtomicU64,
}
impl WakiRpc {
pub fn new(endpoint: String, api_key: Option<String>) -> Self {
Self { endpoint, api_key, id: AtomicU64::new(0) }
}
pub fn endpoint(&self) -> &str { &self.endpoint }
pub fn api_key(&self) -> Option<&str> { self.api_key.as_deref() }
fn next_id(&self) -> u64 { self.id.fetch_add(1, Ordering::Relaxed) + 1 }
fn post_json(&self, req: &Value) -> Result<Value, RpcError> {
let mut builder = waki::Client::new().post(&self.endpoint);
if let Some(key) = &self.api_key {
let auth = format!("Bearer {key}");
builder = builder.header("Authorization", auth.as_str());
}
let resp = builder
.json(req)
.send()
.map_err(|e| RpcError::Transport(e.to_string()))?;
resp.json::<Value>().map_err(|e| RpcError::Json(e.to_string()))
}
}
impl Rpc for WakiRpc {
fn get_latest_blockhash(&self) -> Result<BlockhashInfo, RpcError> {
let req = rpc_request(self.next_id(), "getLatestBlockhash", json!([]));
let resp = self.post_json(&req)?;
parse_blockhash(rpc_result(&resp)?)
}
fn get_account_info(&self, pubkey: &Pubkey) -> Result<Option<AccountInfo>, RpcError> {
let req = rpc_request(
self.next_id(),
"getAccountInfo",
json!([pubkey.to_string(), { "encoding": "base64" }]),
);
let resp = self.post_json(&req)?;
parse_account_info(rpc_result(&resp)?)
}
fn get_signatures_for_address(&self, pubkey: &Pubkey, limit: usize) -> Result<Vec<TxSummary>, RpcError> {
let req = rpc_request(
self.next_id(),
"getSignaturesForAddress",
json!([pubkey.to_string(), { "limit": limit }]),
);
let resp = self.post_json(&req)?;
parse_signatures(rpc_result(&resp)?)
}
fn send_transaction(&self, tx: &[u8]) -> Result<String, RpcError> {
let b64 = BASE64_STANDARD.encode(tx);
let req = rpc_request(self.next_id(), "sendTransaction", json!([b64, { "encoding": "base64" }]));
let resp = self.post_json(&req)?;
parse_send_tx(rpc_result(&resp)?)
}
}
pub struct MockRpc {
responses: RefCell<VecDeque<Value>>,
}
impl MockRpc {
pub fn new(responses: Vec<Value>) -> Self {
Self { responses: RefCell::new(responses.into()) }
}
fn next_response(&self) -> Result<Value, RpcError> {
self.responses
.borrow_mut()
.pop_front()
.ok_or_else(|| RpcError::Unexpected("MockRpc: no more scripted responses".to_string()))
}
}
impl Rpc for MockRpc {
fn get_latest_blockhash(&self) -> Result<BlockhashInfo, RpcError> {
let resp = self.next_response()?;
parse_blockhash(rpc_result(&resp)?)
}
fn get_account_info(&self, _pubkey: &Pubkey) -> Result<Option<AccountInfo>, RpcError> {
let resp = self.next_response()?;
parse_account_info(rpc_result(&resp)?)
}
fn get_signatures_for_address(&self, _pubkey: &Pubkey, _limit: usize) -> Result<Vec<TxSummary>, RpcError> {
let resp = self.next_response()?;
parse_signatures(rpc_result(&resp)?)
}
fn send_transaction(&self, _tx: &[u8]) -> Result<String, RpcError> {
let resp = self.next_response()?;
parse_send_tx(rpc_result(&resp)?)
}
}