mod error;
mod submit;
use reqwest::{blocking::Client, Url};
use transact::protocol::batch::Batch;
use crate::hex::parse_hex;
use crate::protocol::SCABBARD_PROTOCOL_VERSION;
use super::SERVICE_TYPE;
pub use error::Error;
use submit::{submit_batches, wait_for_batches};
pub struct ScabbardClient {
url: String,
}
impl ScabbardClient {
pub fn new(url: &str) -> Self {
Self { url: url.into() }
}
pub fn submit(
&self,
service_id: &ServiceId,
batches: Vec<Batch>,
wait: Option<u64>,
) -> Result<(), Error> {
let batch_link = submit_batches(
&self.url,
service_id.circuit(),
service_id.service_id(),
batches,
)?;
if let Some(wait_secs) = wait {
wait_for_batches(&self.url, &batch_link, wait_secs)
} else {
Ok(())
}
}
pub fn get_state_at_address(
&self,
service_id: &ServiceId,
address: &str,
) -> Result<Option<Vec<u8>>, Error> {
parse_hex(address).map_err(|err| Error::new_with_source("invalid address", err.into()))?;
let url = Url::parse(&format!(
"{}/{}/{}/{}/state/{}",
&self.url,
SERVICE_TYPE,
service_id.circuit(),
service_id.service_id(),
address
))
.map_err(|err| Error::new_with_source("invalid URL", err.into()))?;
let request = Client::new().get(url);
let response = request
.header("SplinterProtocolVersion", SCABBARD_PROTOCOL_VERSION)
.send()
.map_err(|err| Error::new_with_source("request failed", err.into()))?;
if response.status().is_success() {
Ok(Some(response.json().map_err(|err| {
Error::new_with_source("failed to deserialize response body", err.into())
})?))
} else if response.status().as_u16() == 404 {
Ok(None)
} else {
let status = response.status();
let msg: ErrorResponse = response.json().map_err(|err| {
Error::new_with_source("failed to deserialize error response body", err.into())
})?;
Err(Error::new(&format!(
"failed to get state at address: {}: {}",
status, msg
)))
}
}
pub fn get_state_with_prefix(
&self,
service_id: &ServiceId,
prefix: Option<&str>,
) -> Result<Vec<StateEntry>, Error> {
let mut url = Url::parse(&format!(
"{}/{}/{}/{}/state",
&self.url,
SERVICE_TYPE,
service_id.circuit(),
service_id.service_id()
))
.map_err(|err| Error::new_with_source("invalid URL", err.into()))?;
if let Some(prefix) = prefix {
parse_hex(prefix)
.map_err(|err| Error::new_with_source("invalid prefix", err.into()))?;
if prefix.len() > 70 {
return Err(Error::new("prefix must be less than 70 characters"));
}
url.set_query(Some(&format!("prefix={}", prefix)))
}
let request = Client::new().get(url);
let response = request
.header("SplinterProtocolVersion", SCABBARD_PROTOCOL_VERSION)
.send()
.map_err(|err| Error::new_with_source("request failed", err.into()))?;
if response.status().is_success() {
response.json().map_err(|err| {
Error::new_with_source("failed to deserialize response body", err.into())
})
} else {
let status = response.status();
let msg: ErrorResponse = response.json().map_err(|err| {
Error::new_with_source("failed to deserialize error response body", err.into())
})?;
Err(Error::new(&format!(
"failed to get state with prefix: {}: {}",
status, msg
)))
}
}
}
pub struct ServiceId {
circuit: String,
service_id: String,
}
impl ServiceId {
pub fn new(circuit: &str, service_id: &str) -> Self {
Self {
circuit: circuit.into(),
service_id: service_id.into(),
}
}
pub fn from_string(full_id: &str) -> Result<Self, Error> {
let ids = full_id.splitn(2, "::").collect::<Vec<_>>();
let circuit = (*ids
.get(0)
.ok_or_else(|| Error::new("service ID invalid: cannot be empty"))?)
.to_string();
if circuit.is_empty() {
return Err(Error::new("service ID invalid: circuit ID cannot be empty"));
}
let service_id = (*ids.get(1).ok_or_else(|| {
Error::new("service ID invalid: must be of the form 'circuit_id::service_id'")
})?)
.to_string();
if service_id.is_empty() {
return Err(Error::new("service ID invalid: service ID cannot be empty"));
}
Ok(Self {
circuit,
service_id,
})
}
pub fn circuit(&self) -> &str {
&self.circuit
}
pub fn service_id(&self) -> &str {
&self.service_id
}
}
#[derive(Deserialize, Debug)]
pub struct StateEntry {
address: String,
value: Vec<u8>,
}
impl StateEntry {
pub fn address(&self) -> &str {
&self.address
}
pub fn value(&self) -> &[u8] {
&self.value
}
}
#[derive(Deserialize, Debug)]
struct ErrorResponse {
message: String,
}
impl std::fmt::Display for ErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn service_id_from_string() {
assert!(ServiceId::from_string("").is_err());
assert!(ServiceId::from_string("circuit").is_err());
assert!(ServiceId::from_string("::").is_err());
assert!(ServiceId::from_string("circuit::").is_err());
assert!(ServiceId::from_string("::service_id").is_err());
let service_id = ServiceId::from_string("circuit::service_id").expect("failed to parse");
assert_eq!(service_id.circuit(), "circuit");
assert_eq!(service_id.service_id(), "service_id");
}
}