use async_trait::async_trait;
#[cfg(feature = "http")]
use crate::transaction::BroadcastResponse;
use crate::transaction::{
BroadcastFailure, BroadcastResult, BroadcastStatus, Broadcaster, Transaction,
};
#[derive(Debug, Clone)]
pub struct TeranodeConfig {
pub url: String,
pub api_key: Option<String>,
pub timeout_ms: u64,
}
pub struct TeranodeBroadcaster {
config: TeranodeConfig,
#[cfg(feature = "http")]
client: reqwest::Client,
}
impl TeranodeBroadcaster {
pub fn new(url: &str, api_key: Option<String>) -> Self {
Self {
config: TeranodeConfig {
url: url.to_string(),
api_key,
timeout_ms: 30_000,
},
#[cfg(feature = "http")]
client: reqwest::Client::new(),
}
}
pub fn with_config(config: TeranodeConfig) -> Self {
Self {
config,
#[cfg(feature = "http")]
client: reqwest::Client::new(),
}
}
pub fn url(&self) -> &str {
&self.config.url
}
pub fn api_key(&self) -> Option<&str> {
self.config.api_key.as_deref()
}
}
#[async_trait(?Send)]
impl Broadcaster for TeranodeBroadcaster {
#[cfg(feature = "http")]
async fn broadcast(&self, tx: &Transaction) -> BroadcastResult {
let txid = tx.id();
let ef_bytes = tx.to_ef().map_err(|e| BroadcastFailure {
status: BroadcastStatus::Error,
code: "EF_SERIALIZATION_ERROR".to_string(),
txid: Some(txid.clone()),
description: format!("Failed to serialize transaction to EF format: {}", e),
more: None,
})?;
let url = format!("{}/v1/tx", self.config.url);
let mut request = self
.client
.post(&url)
.header("Content-Type", "application/octet-stream")
.body(ef_bytes);
if let Some(ref api_key) = self.config.api_key {
request = request.header("Authorization", format!("Bearer {}", api_key));
}
let response = request
.timeout(std::time::Duration::from_millis(self.config.timeout_ms))
.send()
.await
.map_err(|e| BroadcastFailure {
status: BroadcastStatus::Error,
code: "NETWORK_ERROR".to_string(),
txid: Some(txid.clone()),
description: format!("Network error: {}", e),
more: None,
})?;
let status_code = response.status();
let body = response.text().await.map_err(|e| BroadcastFailure {
status: BroadcastStatus::Error,
code: "PARSE_ERROR".to_string(),
txid: Some(txid.clone()),
description: format!("Failed to read response: {}", e),
more: None,
})?;
if status_code.is_success() {
Ok(BroadcastResponse {
status: BroadcastStatus::Success,
txid,
message: if body.is_empty() {
"Success".to_string()
} else {
body
},
competing_txs: None,
})
} else {
Err(BroadcastFailure {
status: BroadcastStatus::Error,
code: status_code.as_u16().to_string(),
txid: Some(txid),
description: if body.is_empty() {
format!("HTTP {}", status_code)
} else {
body
},
more: None,
})
}
}
#[cfg(not(feature = "http"))]
async fn broadcast(&self, tx: &Transaction) -> BroadcastResult {
Err(BroadcastFailure {
status: BroadcastStatus::Error,
code: "NO_HTTP".to_string(),
txid: Some(tx.id()),
description: "HTTP feature not enabled. Add 'http' feature to Cargo.toml".to_string(),
more: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_teranode_broadcaster_new() {
let broadcaster =
TeranodeBroadcaster::new("https://teranode.example.com", Some("api_key".to_string()));
assert_eq!(broadcaster.url(), "https://teranode.example.com");
assert_eq!(broadcaster.api_key(), Some("api_key"));
}
#[test]
fn test_teranode_broadcaster_with_config() {
let config = TeranodeConfig {
url: "https://test.teranode.com".to_string(),
api_key: Some("test-key".to_string()),
timeout_ms: 60_000,
};
let broadcaster = TeranodeBroadcaster::with_config(config);
assert_eq!(broadcaster.url(), "https://test.teranode.com");
assert_eq!(broadcaster.api_key(), Some("test-key"));
}
#[test]
fn test_teranode_no_default() {
let broadcaster = TeranodeBroadcaster::new("https://custom.teranode.io", None);
assert_eq!(broadcaster.url(), "https://custom.teranode.io");
assert!(broadcaster.api_key().is_none());
}
}