use alloy::primitives::Address;
use alloy::providers::Provider;
use alloy::rpc::json_rpc::RpcError;
use alloy::rpc::json_rpc::{RpcRecv, RpcSend};
use anyhow::anyhow;
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use bytes::Bytes;
use displaydoc::Display;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::fmt::Debug;
use thiserror::Error;
use crate::{ArkivRoClient, Hash, NumericAnnotation, StringAnnotation};
#[derive(Debug, Display, Error)]
pub enum Error {
RpcRequestError(String),
Base64DecodeError(String),
ResponseDeserializationError(String),
UnexpectedError(String),
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EntityMetaData {
pub expires_at_block: Option<u64>,
pub payload: Option<String>,
pub string_annotations: Vec<StringAnnotation>,
pub numeric_annotations: Vec<NumericAnnotation>,
pub owner: Address,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SearchResult {
#[serde(rename = "key")]
pub key: Hash,
#[serde(rename = "value", deserialize_with = "deserialize_base64")]
pub value: Bytes,
}
fn deserialize_base64<'de, D>(deserializer: D) -> Result<Bytes, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
BASE64
.decode(s)
.map(Bytes::from)
.map_err(serde::de::Error::custom)
}
impl SearchResult {
pub fn value_as_string(&self) -> anyhow::Result<String> {
String::from_utf8(self.value.to_vec())
.map_err(|e| anyhow::anyhow!("Failed to decode search result to string: {e}"))
}
}
impl ArkivRoClient {
pub(crate) async fn rpc_call<S: RpcSend, R: RpcRecv>(
&self,
method: impl Into<Cow<'static, str>>,
params: S,
) -> Result<R, Error> {
let method = method.into();
tracing::debug!("RPC Call - Method: {method}, Params: {params:?}");
self.provider
.client()
.request(method.clone(), params)
.await
.inspect(|res| tracing::debug!("RPC Response: {res:?}"))
.map_err(|e| match e {
RpcError::ErrorResp(err) => {
anyhow!("Error response from RPC service: {err}")
}
RpcError::SerError(err) => {
anyhow!("Serialization error: {err}")
}
RpcError::DeserError { err, text } => {
tracing::debug!("Deserialization error: {err}, response text: {text}");
anyhow!("Deserialization error: {err}")
}
_ => anyhow!("{e}"),
})
.map_err(|e| Error::RpcRequestError(e.to_string()))
}
pub async fn get_entity_count(&self) -> Result<u64, Error> {
self.rpc_call::<(), u64>("golembase_getEntityCount", ())
.await
}
pub async fn get_all_entity_keys(&self) -> Result<Vec<Hash>, Error> {
let result = self
.rpc_call::<(), Option<Vec<Hash>>>("golembase_getAllEntityKeys", ())
.await?;
Ok(result.unwrap_or_default())
}
pub async fn get_entities_of_owner(&self, address: Address) -> Result<Vec<Hash>, Error> {
let result = self
.rpc_call::<&[Address], Option<Vec<Hash>>>("golembase_getEntitiesOfOwner", &[address])
.await?;
Ok(result.unwrap_or_default())
}
pub async fn get_storage_value<T: TryFrom<Vec<u8>>>(&self, key: Hash) -> Result<T, Error>
where
<T as TryFrom<Vec<u8>>>::Error: std::fmt::Display,
{
let encoded_value = self
.rpc_call::<&[Hash], String>("golembase_getStorageValue", &[key])
.await?;
let decoded = BASE64
.decode(&encoded_value)
.map_err(|e| Error::Base64DecodeError(e.to_string()))?;
T::try_from(decoded).map_err(|e| Error::UnexpectedError(e.to_string()))
}
pub async fn query_entities(&self, query: &str) -> Result<Vec<SearchResult>, Error> {
let results = self
.rpc_call::<&[&str], Option<Vec<SearchResult>>>("golembase_queryEntities", &[query])
.await?;
Ok(results.unwrap_or_default())
}
pub async fn query_entity_keys(&self, query: &str) -> Result<Vec<Hash>, Error> {
let results = self.query_entities(query).await?;
Ok(results.into_iter().map(|result| result.key).collect())
}
pub async fn get_entities_to_expire_at_block(
&self,
block_number: u64,
) -> Result<Vec<Hash>, Error> {
let result = self
.rpc_call::<u64, Option<Vec<Hash>>>(
"golembase_getEntitiesToExpireAtBlock",
block_number,
)
.await?;
Ok(result.unwrap_or_default())
}
pub async fn get_entity_metadata(&self, key: Hash) -> Result<EntityMetaData, Error> {
self.rpc_call::<&[Hash], EntityMetaData>("golembase_getEntityMetaData", &[key])
.await
}
}