use crate::client::http::{decode, json_headers, send, ClientError, NamespaceConfig};
use crate::client::transport::RequestBody;
use crate::client::types::{PoeVerifyInput, RecordResource, RecordsListInput, RecordsListResponse};
use crate::verifier::fetch::HttpMethod;
pub struct RecordsNamespace<'t> {
config: NamespaceConfig<'t>,
}
impl<'t> RecordsNamespace<'t> {
#[must_use]
pub fn new(config: NamespaceConfig<'t>) -> Self {
Self { config }
}
pub fn list(
&self,
input: Option<&RecordsListInput>,
) -> Result<RecordsListResponse, ClientError> {
let mut query: Vec<(String, String)> = Vec::new();
if let Some(input) = input {
if input.sealed == Some(true) {
query.push(("sealed".to_string(), "true".to_string()));
}
if let Some(limit) = input.limit {
query.push(("limit".to_string(), limit.to_string()));
}
if let Some(cursor) = &input.cursor {
query.push(("cursor".to_string(), cursor.clone()));
}
}
let url = if query.is_empty() {
format!("{}/api/v1/records", self.config.base_url)
} else {
format!(
"{}/api/v1/records?{}",
self.config.base_url,
encode_query(&query)
)
};
let headers = json_headers(self.config.api_key.as_deref(), None);
let response = send(
self.config.transport,
&url,
HttpMethod::Get,
&headers,
&RequestBody::None,
)?;
let mut page: RecordsListResponse = decode(&response.body)?;
if page.tip_block_height.is_none() {
page.tip_block_height = derive_tip_block_height(&page.data);
}
Ok(page)
}
pub fn get(&self, tx_hash: &str) -> Result<RecordResource, ClientError> {
let url = format!(
"{}/api/v1/records/{}",
self.config.base_url,
encode_path_segment(tx_hash)
);
let headers = json_headers(self.config.api_key.as_deref(), None);
let response = send(
self.config.transport,
&url,
HttpMethod::Get,
&headers,
&RequestBody::None,
)?;
decode(&response.body)
}
pub fn verify(
&self,
tx_hash: &str,
input: Option<&PoeVerifyInput>,
) -> Result<serde_json::Value, ClientError> {
let body = verify_input_to_json(input);
let url = format!(
"{}/api/v1/records/{}/verify",
self.config.base_url,
encode_path_segment(tx_hash)
);
let headers = json_headers(self.config.api_key.as_deref(), None);
let response = send(
self.config.transport,
&url,
HttpMethod::Post,
&headers,
&RequestBody::Json(serde_json::to_string(&body).expect("verify body serialises")),
)?;
if response.body.is_empty() {
return Ok(serde_json::Value::Null);
}
serde_json::from_slice(&response.body).map_err(|e| ClientError::Decode(e.to_string()))
}
}
fn derive_tip_block_height(records: &[RecordResource]) -> Option<u64> {
records
.iter()
.filter_map(|r| {
r.block_height
.map(|bh| bh.saturating_add(r.num_confirmations).saturating_sub(1))
})
.max()
}
fn verify_input_to_json(input: Option<&PoeVerifyInput>) -> serde_json::Value {
let Some(input) = input else {
return serde_json::Value::Object(serde_json::Map::new());
};
let mut map = serde_json::Map::new();
if let Some(verify_uris) = input.verify_uris {
map.insert(
"verify_uris".to_string(),
serde_json::Value::Bool(verify_uris),
);
}
if let Some(decryptions) = &input.decryption {
let arr = decryptions
.iter()
.map(|d| {
let mut entry = serde_json::Map::new();
entry.insert(
"item_idx".to_string(),
serde_json::Value::Number(d.item_idx.into()),
);
if let Some(sk) = &d.recipient_secret_key {
entry.insert(
"recipient_secret_key".to_string(),
serde_json::Value::String(sk.clone()),
);
}
if let Some(pass) = &d.passphrase {
entry.insert(
"passphrase".to_string(),
serde_json::Value::String(pass.clone()),
);
}
serde_json::Value::Object(entry)
})
.collect();
map.insert("decryption".to_string(), serde_json::Value::Array(arr));
}
serde_json::Value::Object(map)
}
fn encode_query(pairs: &[(String, String)]) -> String {
pairs
.iter()
.map(|(k, v)| {
format!(
"{}={}",
encode_query_component(k),
encode_query_component(v)
)
})
.collect::<Vec<_>>()
.join("&")
}
fn encode_query_component(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for byte in value.bytes() {
match byte {
b' ' => out.push('+'),
b if b.is_ascii_alphanumeric()
|| matches!(
b,
b'-' | b'_' | b'.' | b'!' | b'~' | b'*' | b'\'' | b'(' | b')'
) =>
{
out.push(b as char);
}
b => {
out.push('%');
out.push_str(&format!("{b:02X}"));
}
}
}
out
}
fn encode_path_segment(segment: &str) -> String {
let mut out = String::with_capacity(segment.len());
for byte in segment.bytes() {
let unreserved = byte.is_ascii_alphanumeric()
|| matches!(
byte,
b'-' | b'_' | b'.' | b'!' | b'~' | b'*' | b'\'' | b'(' | b')'
);
if unreserved {
out.push(byte as char);
} else {
out.push('%');
out.push_str(&format!("{byte:02X}"));
}
}
out
}