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 ArcConfig {
pub url: String,
pub api_key: Option<String>,
pub timeout_ms: u64,
}
impl Default for ArcConfig {
fn default() -> Self {
Self {
url: "https://arc.taal.com".to_string(),
api_key: None,
timeout_ms: 30_000,
}
}
}
pub struct ArcBroadcaster {
config: ArcConfig,
#[cfg(feature = "http")]
client: reqwest::Client,
}
impl Default for ArcBroadcaster {
fn default() -> Self {
Self::new("https://arc.taal.com", None)
}
}
impl ArcBroadcaster {
pub fn new(url: &str, api_key: Option<String>) -> Self {
Self {
config: ArcConfig {
url: url.to_string(),
api_key,
..Default::default()
},
#[cfg(feature = "http")]
client: reqwest::Client::new(),
}
}
pub fn with_config(config: ArcConfig) -> 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 ArcBroadcaster {
#[cfg(feature = "http")]
async fn broadcast(&self, tx: &Transaction) -> BroadcastResult {
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct ArcRequest {
#[serde(rename = "rawTx")]
raw_tx: String,
}
#[derive(Deserialize)]
struct ArcResponse {
txid: Option<String>,
#[serde(rename = "txStatus")]
tx_status: Option<String>,
#[serde(rename = "extraInfo")]
extra_info: Option<String>,
status: Option<u16>,
title: Option<String>,
detail: Option<String>,
}
let url = format!("{}/v1/tx", self.config.url);
let raw_tx = tx.to_hex();
let txid = tx.id();
let mut request = self
.client
.post(&url)
.header("Content-Type", "application/json")
.json(&ArcRequest { raw_tx });
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: ArcResponse = response.json().await.map_err(|e| BroadcastFailure {
status: BroadcastStatus::Error,
code: "PARSE_ERROR".to_string(),
txid: Some(txid.clone()),
description: format!("Failed to parse response: {}", e),
more: None,
})?;
if status_code.is_success() {
Ok(BroadcastResponse {
status: BroadcastStatus::Success,
txid: body.txid.unwrap_or(txid),
message: body.tx_status.unwrap_or_else(|| "Success".to_string()),
competing_txs: None,
})
} else {
Err(BroadcastFailure {
status: BroadcastStatus::Error,
code: body
.status
.map(|s| s.to_string())
.unwrap_or_else(|| "UNKNOWN".to_string()),
txid: Some(txid),
description: body
.detail
.or(body.title)
.unwrap_or_else(|| "Unknown error".to_string()),
more: body.extra_info.map(serde_json::Value::String),
})
}
}
#[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_arc_config_default() {
let config = ArcConfig::default();
assert_eq!(config.url, "https://arc.taal.com");
assert!(config.api_key.is_none());
assert_eq!(config.timeout_ms, 30_000);
}
#[test]
fn test_arc_broadcaster_new() {
let broadcaster =
ArcBroadcaster::new("https://custom.arc.com", Some("api_key".to_string()));
assert_eq!(broadcaster.url(), "https://custom.arc.com");
assert_eq!(broadcaster.api_key(), Some("api_key"));
}
#[test]
fn test_arc_broadcaster_default() {
let broadcaster = ArcBroadcaster::default();
assert_eq!(broadcaster.url(), "https://arc.taal.com");
assert!(broadcaster.api_key().is_none());
}
#[test]
fn test_arc_broadcaster_with_config() {
let config = ArcConfig {
url: "https://test.arc.com".to_string(),
api_key: Some("test-key".to_string()),
timeout_ms: 60_000,
};
let broadcaster = ArcBroadcaster::with_config(config);
assert_eq!(broadcaster.url(), "https://test.arc.com");
assert_eq!(broadcaster.api_key(), Some("test-key"));
}
}