use gloo_net::http::Request;
use serde::{Deserialize, Serialize};
use serde_json::json;
use solana_commitment_config::CommitmentConfig;
use solana_hash::Hash;
use wasm_bindgen::JsValue;
use crate::error::{Error, Result};
#[derive(Clone, Debug)]
pub struct RpcClient {
endpoint: String,
}
impl RpcClient {
pub fn new(endpoint: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
}
}
pub fn mainnet() -> Self {
Self::new("https://api.mainnet-beta.solana.com")
}
pub fn devnet() -> Self {
Self::new("https://api.devnet.solana.com")
}
pub fn testnet() -> Self {
Self::new("https://api.testnet.solana.com")
}
async fn call<T: for<'de> Deserialize<'de>>(
&self,
method: &str,
params: serde_json::Value,
) -> Result<T> {
let body = json!({
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": params,
});
let body_str = body.to_string();
web_sys::console::log_3(
&"[leptos-solana rpc] →".into(),
&JsValue::from_str(&self.endpoint),
&JsValue::from_str(&body_str),
);
let resp = Request::post(&self.endpoint)
.header("content-type", "application/json")
.body(body_str)
.map_err(|e| Error::Rpc(e.to_string()))?
.send()
.await
.map_err(|e| Error::Rpc(e.to_string()))?;
let text = resp.text().await.map_err(|e| Error::Rpc(e.to_string()))?;
web_sys::console::log_2(
&format!("[leptos-solana rpc] ← {method}").into(),
&JsValue::from_str(&text),
);
let wrapped: RpcResponse<T> =
serde_json::from_str(&text).map_err(|e| Error::Rpc(e.to_string()))?;
match wrapped {
RpcResponse::Ok { result, .. } => Ok(result),
RpcResponse::Err { error, .. } => Err(Error::Rpc(format!(
"{} ({})",
error.message, error.code
))),
}
}
pub async fn get_latest_blockhash(&self, commitment: CommitmentConfig) -> Result<Hash> {
#[derive(Deserialize)]
struct Value {
blockhash: String,
}
#[derive(Deserialize)]
struct Resp {
value: Value,
}
let resp: Resp = self
.call(
"getLatestBlockhash",
json!([{ "commitment": commitment.commitment.to_string() }]),
)
.await?;
resp.value
.blockhash
.parse::<Hash>()
.map_err(|e| Error::Decode(format!("blockhash: {e}")))
}
pub async fn send_transaction_b64(&self, signed_b64: &str) -> Result<String> {
self.call(
"sendTransaction",
json!([
signed_b64,
{ "encoding": "base64" }
]),
)
.await
}
pub async fn get_balance(&self, address: &str) -> Result<u64> {
self.get_balance_with_commitment(address, CommitmentConfig::confirmed())
.await
}
pub async fn get_balance_with_commitment(
&self,
address: &str,
commitment: CommitmentConfig,
) -> Result<u64> {
#[derive(Deserialize)]
struct Resp {
value: u64,
}
let resp: Resp = self
.call(
"getBalance",
json!([address, { "commitment": commitment.commitment.to_string() }]),
)
.await?;
Ok(resp.value)
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum RpcResponse<T> {
Ok {
#[allow(dead_code)]
jsonrpc: String,
result: T,
},
Err {
#[allow(dead_code)]
jsonrpc: String,
error: RpcError,
},
}
#[derive(Deserialize, Serialize, Debug)]
struct RpcError {
code: i64,
message: String,
}