use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
pub const TX_KIND: u32 = 23500;
pub const FAUCET_KIND: u32 = 23501;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TxPost {
pub url: String,
pub body: String,
}
fn base(url: &str) -> &str {
url.trim().trim_end_matches('/')
}
pub fn tx_post(base_url: &str, hex: &str) -> TxPost {
TxPost {
url: format!("{}/tx", base(base_url)),
body: hex.trim().to_string(),
}
}
pub fn coins_url(base_url: &str, script_hex: &str) -> String {
format!(
"{}/coins/{}",
base(base_url),
script_hex.trim().to_ascii_lowercase()
)
}
pub fn tip_url(base_url: &str) -> String {
format!("{}/tip", base(base_url))
}
pub fn chain_url(base_url: &str) -> String {
format!("{}/chain.json", base(base_url))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EventTemplate {
pub kind: u32,
pub created_at: u64,
pub tags: Vec<Vec<String>>,
pub content: String,
}
pub fn tx_event(chain_id: &str, hex: &str, created_at: u64) -> EventTemplate {
EventTemplate {
kind: TX_KIND,
created_at,
tags: vec![vec!["chain".to_string(), chain_id.to_string()]],
content: hex.trim().to_string(),
}
}
pub fn faucet_request(chain_id: &str, address: &str, created_at: u64) -> EventTemplate {
EventTemplate {
kind: FAUCET_KIND,
created_at,
tags: vec![vec!["chain".to_string(), chain_id.to_string()]],
content: address.trim().to_string(),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Accepted {
pub txid: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fee: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vsize: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dup: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tip {
pub height: u32,
pub hash: String,
pub time: u32,
}
pub fn parse_tx_response(text: &str) -> Result<Accepted> {
#[derive(Deserialize)]
struct Reply {
error: Option<String>,
#[serde(flatten)]
ok: Option<Accepted>,
}
let r: Reply = serde_json::from_str(text)?;
if let Some(e) = r.error {
return Err(Error::Refused(e));
}
r.ok.ok_or_else(|| Error::Encoding(format!("not a producer reply: {text}")))
}
#[cfg(feature = "client")]
pub mod client {
use super::{chain_url, coins_url, parse_tx_response, tip_url, tx_post, Accepted, Tip};
use crate::coins::{from_json, Coin};
use crate::error::{Error, Result};
use sidestr_core::document::ChainDocument;
fn agent() -> ureq::Agent {
ureq::Agent::new_with_config(
ureq::Agent::config_builder()
.http_status_as_error(false)
.build(),
)
}
fn get(url: &str) -> Result<String> {
agent()
.get(url)
.call()
.map_err(|e| Error::Http(e.to_string()))?
.body_mut()
.read_to_string()
.map_err(|e| Error::Http(e.to_string()))
}
pub fn coins(base_url: &str, script_hex: &str) -> Result<Vec<Coin>> {
from_json(&get(&coins_url(base_url, script_hex))?)
}
pub fn tip(base_url: &str) -> Result<Tip> {
Ok(serde_json::from_str(&get(&tip_url(base_url))?)?)
}
pub fn chain(base_url: &str) -> Result<ChainDocument> {
Ok(ChainDocument::from_json(&get(&chain_url(base_url))?)?)
}
pub fn post_tx(base_url: &str, hex: &str) -> Result<Accepted> {
let p = tx_post(base_url, hex);
let text = agent()
.post(&p.url)
.content_type("text/plain")
.send(p.body.as_bytes())
.map_err(|e| Error::Http(e.to_string()))?
.body_mut()
.read_to_string()
.map_err(|e| Error::Http(e.to_string()))?;
parse_tx_response(&text)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn urls_and_replies() {
assert_eq!(tip_url("http://h:1/"), "http://h:1/tip");
assert_eq!(chain_url("http://h:1"), "http://h:1/chain.json");
let dup = parse_tx_response(r#"{"txid":"ab","dup":true}"#).unwrap();
assert_eq!(dup.dup, Some(true));
assert!(
matches!(parse_tx_response(r#"{"error":"no"}"#), Err(Error::Refused(e)) if e == "no")
);
assert!(parse_tx_response("{}").is_err());
let f = faucet_request("sidestr:trial", "trl1p…", 1);
assert_eq!(f.kind, FAUCET_KIND);
assert!(serde_json::to_string(&f)
.unwrap()
.contains("\"kind\":23501"));
}
}