use async_trait::async_trait;
use reqwest::Client;
use crate::transaction::broadcaster::{BroadcastFailure, BroadcastResponse, Broadcaster};
use crate::transaction::Transaction;
pub struct WhatsOnChainBroadcaster {
network: String,
client: Client,
}
impl WhatsOnChainBroadcaster {
pub fn new(network: &str) -> Self {
Self {
network: network.to_string(),
client: Client::new(),
}
}
fn base_url(&self) -> String {
format!("https://api.whatsonchain.com/v1/bsv/{}", self.network)
}
}
pub struct WhatsOnChainBroadcasterWithUrl {
network: String,
base_url: String,
client: Client,
}
impl WhatsOnChainBroadcasterWithUrl {
pub fn new(network: &str, base_url: &str) -> Self {
Self {
network: network.to_string(),
base_url: base_url.trim_end_matches('/').to_string(),
client: Client::new(),
}
}
}
#[async_trait]
impl Broadcaster for WhatsOnChainBroadcaster {
async fn broadcast(&self, tx: &Transaction) -> Result<BroadcastResponse, BroadcastFailure> {
let raw_hex = tx.to_hex().map_err(|e| BroadcastFailure {
status: 0,
code: "SERIALIZE_ERROR".to_string(),
description: format!("failed to serialize transaction: {}", e),
})?;
let url = format!("{}/tx/raw", self.base_url());
let response = self
.client
.post(&url)
.json(&serde_json::json!({ "txhex": raw_hex }))
.send()
.await
.map_err(|e| BroadcastFailure {
status: 0,
code: "NETWORK_ERROR".to_string(),
description: format!("network error: {}", e),
})?;
let status = response.status().as_u16() as u32;
let body_text = response.text().await.unwrap_or_default();
if status == 200 || status == 201 {
let txid = body_text.trim().trim_matches('"').to_string();
Ok(BroadcastResponse {
status: "success".to_string(),
txid,
message: String::new(),
})
} else {
Err(BroadcastFailure {
status,
code: "BROADCAST_FAILED".to_string(),
description: body_text,
})
}
}
}
#[async_trait]
impl Broadcaster for WhatsOnChainBroadcasterWithUrl {
async fn broadcast(&self, tx: &Transaction) -> Result<BroadcastResponse, BroadcastFailure> {
let raw_hex = tx.to_hex().map_err(|e| BroadcastFailure {
status: 0,
code: "SERIALIZE_ERROR".to_string(),
description: format!("failed to serialize transaction: {}", e),
})?;
let url = format!("{}/v1/bsv/{}/tx/raw", self.base_url, self.network);
let response = self
.client
.post(&url)
.json(&serde_json::json!({ "txhex": raw_hex }))
.send()
.await
.map_err(|e| BroadcastFailure {
status: 0,
code: "NETWORK_ERROR".to_string(),
description: format!("network error: {}", e),
})?;
let status = response.status().as_u16() as u32;
let body_text = response.text().await.unwrap_or_default();
if status == 200 || status == 201 {
let txid = body_text.trim().trim_matches('"').to_string();
Ok(BroadcastResponse {
status: "success".to_string(),
txid,
message: String::new(),
})
} else {
Err(BroadcastFailure {
status,
code: "BROADCAST_FAILED".to_string(),
description: body_text,
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use wiremock::matchers;
use wiremock::{Mock, MockServer, ResponseTemplate};
fn make_test_tx() -> Transaction {
Transaction::new()
}
#[tokio::test]
async fn test_woc_broadcast_success() {
let mock_server = MockServer::start().await;
Mock::given(matchers::method("POST"))
.and(matchers::path("/v1/bsv/main/tx/raw"))
.respond_with(ResponseTemplate::new(200).set_body_string("\"abc123def456\""))
.mount(&mock_server)
.await;
let broadcaster = WhatsOnChainBroadcasterWithUrl::new("main", &mock_server.uri());
let tx = make_test_tx();
let result = broadcaster.broadcast(&tx).await;
assert!(result.is_ok());
let resp = result.unwrap();
assert_eq!(resp.txid, "abc123def456");
assert_eq!(resp.status, "success");
}
#[tokio::test]
async fn test_woc_broadcast_failure() {
let mock_server = MockServer::start().await;
Mock::given(matchers::method("POST"))
.and(matchers::path("/v1/bsv/main/tx/raw"))
.respond_with(ResponseTemplate::new(400).set_body_string("Invalid transaction format"))
.mount(&mock_server)
.await;
let broadcaster = WhatsOnChainBroadcasterWithUrl::new("main", &mock_server.uri());
let tx = make_test_tx();
let result = broadcaster.broadcast(&tx).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.status, 400);
assert_eq!(err.description, "Invalid transaction format");
}
}