bitcoin_nostr_relay/
lib.rs1pub mod bitcoin_rpc;
2pub mod validation;
3pub mod nostr;
4pub mod relay;
5pub mod networks;
6pub mod error;
7
8pub use bitcoin_rpc::BitcoinRpcClient;
10pub use validation::{TransactionValidator, ValidationConfig};
11pub use nostr::NostrClient;
12pub use relay::{RelayServer, RelayConfig};
13pub use networks::{Network, network_config};
14pub use error::{RelayError, ConfigError, BitcoinRpcError, NostrError, ValidationError, NetworkError};
15
16pub type Result<T, E = RelayError> = std::result::Result<T, E>;
18
19pub struct BitcoinNostrRelay {
21 bitcoin_client: BitcoinRpcClient,
22 nostr_client: Option<NostrClient>,
23 validator: TransactionValidator,
24 config: RelayConfig,
25}
26
27impl BitcoinNostrRelay {
28 pub fn new(config: RelayConfig) -> Result<Self> {
30 let bitcoin_client = BitcoinRpcClient::new(
31 config.bitcoin_rpc_url.clone(),
32 config.bitcoin_rpc_auth.username.clone(),
33 config.bitcoin_rpc_auth.password.clone(),
34 );
35
36 let bitcoin_port = if let Ok(url) = url::Url::parse(&config.bitcoin_rpc_url) {
38 url.port().unwrap_or(18332)
39 } else {
40 18332
41 };
42
43 let validator = TransactionValidator::new(
44 config.validation_config.clone(),
45 bitcoin_port,
46 );
47
48 Ok(Self {
49 bitcoin_client,
50 nostr_client: None,
51 validator,
52 config,
53 })
54 }
55
56 pub async fn connect_nostr(&mut self, ws_stream: tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>) -> Result<()> {
58 self.nostr_client = Some(NostrClient::new(ws_stream));
59 Ok(())
60 }
61
62 pub async fn start(&mut self) -> Result<()> {
64 let relay_server = RelayServer::new(
65 self.bitcoin_client.clone(),
66 self.nostr_client.take(),
67 self.validator.clone(),
68 self.config.clone(),
69 )?;
70
71 relay_server.run().await
72 }
73
74 pub async fn broadcast_transaction(&self, tx_hex: &str, block_hash: &str) -> Result<()> {
76 if let Some(nostr_client) = &self.nostr_client {
77 nostr_client.send_tx_event(tx_hex, block_hash).await.map_err(RelayError::from)
78 } else {
79 Err(NostrError::Disconnected.into())
80 }
81 }
82
83 pub async fn validate_transaction(&self, tx_hex: &str) -> Result<(), ValidationError> {
85 self.validator.validate(tx_hex).await
86 }
87
88 pub fn config(&self) -> &RelayConfig {
90 &self.config
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::relay::RelayConfig;
98
99 #[test]
100 fn test_bitcoin_nostr_relay_creation() {
101 let config = RelayConfig::for_network(Network::Regtest, 1);
102 let relay = BitcoinNostrRelay::new(config);
103
104 assert!(relay.is_ok());
105 let relay = relay.unwrap();
106
107 assert!(relay.nostr_client.is_none());
109 }
110
111 #[test]
112 fn test_bitcoin_nostr_relay_with_different_configs() {
113 let regtest_config = RelayConfig::for_network(Network::Regtest, 1);
115 let regtest_relay = BitcoinNostrRelay::new(regtest_config);
116 assert!(regtest_relay.is_ok());
117
118 let testnet_config = RelayConfig::for_network(Network::Testnet4, 2);
120 let testnet_relay = BitcoinNostrRelay::new(testnet_config);
121 assert!(testnet_relay.is_ok());
122
123 let custom_config = RelayConfig::new(
125 "http://127.0.0.1:19000".to_string(),
126 "ws://127.0.0.1:8000".to_string(),
127 "3".to_string(),
128 "127.0.0.1:7781".parse().unwrap(),
129 ).unwrap();
130 let custom_relay = BitcoinNostrRelay::new(custom_config);
131 assert!(custom_relay.is_ok());
132 }
133
134 #[test]
135 fn test_bitcoin_nostr_relay_with_validation_config() {
136 let mut validation_config = ValidationConfig::default();
137 validation_config.enable_validation = false;
138 validation_config.cache_size = 500;
139
140 let config = RelayConfig::for_network(Network::Regtest, 1)
141 .with_validation(validation_config.clone());
142
143 let relay = BitcoinNostrRelay::new(config);
144 assert!(relay.is_ok());
145
146 let relay = relay.unwrap();
147 assert_eq!(relay.validator.config().enable_validation, false);
148 assert_eq!(relay.validator.config().cache_size, 500);
149 }
150
151 #[tokio::test]
152 async fn test_validate_transaction_with_disabled_validation() {
153 let mut validation_config = ValidationConfig::default();
154 validation_config.enable_validation = false;
155
156 let config = RelayConfig::for_network(Network::Regtest, 1)
157 .with_validation(validation_config);
158
159 let relay = BitcoinNostrRelay::new(config).unwrap();
160
161 let result = relay.validate_transaction("invalid_hex").await;
163 assert!(result.is_ok());
164 }
165
166 #[tokio::test]
167 async fn test_validate_transaction_with_empty_input() {
168 let config = RelayConfig::for_network(Network::Regtest, 1);
169 let relay = BitcoinNostrRelay::new(config).unwrap();
170
171 let result = relay.validate_transaction("").await;
173 assert!(result.is_err());
174
175 match result {
177 Err(ValidationError::EmptyTransaction) => {
178 }
180 Err(ValidationError::InvalidStructure) => {
181 }
183 _ => panic!("Expected EmptyTransaction or InvalidStructure error, got: {:?}", result)
184 }
185 }
186
187 #[tokio::test]
188 async fn test_validate_transaction_with_invalid_hex() {
189 let config = RelayConfig::for_network(Network::Regtest, 1);
190 let relay = BitcoinNostrRelay::new(config).unwrap();
191
192 let result = relay.validate_transaction("not_hex_characters").await;
194 assert!(result.is_err());
195
196 if let Err(ValidationError::InvalidHex) = result {
197 } else {
199 panic!("Expected InvalidHex error, got: {:?}", result);
200 }
201 }
202
203 #[tokio::test]
204 async fn test_validate_transaction_with_invalid_size() {
205 let config = RelayConfig::for_network(Network::Regtest, 1);
206 let relay = BitcoinNostrRelay::new(config).unwrap();
207
208 let small_tx = "a".repeat(118); let result = relay.validate_transaction(&small_tx).await;
211 assert!(result.is_err());
212
213 match result {
215 Err(ValidationError::InvalidSize { size: 59 }) => {
216 }
218 Err(ValidationError::InvalidStructure) => {
219 }
221 _ => panic!("Expected InvalidSize{{size: 59}} or InvalidStructure error, got: {:?}", result)
222 }
223 }
224
225 #[tokio::test]
226 async fn test_broadcast_transaction_without_nostr_client() {
227 let config = RelayConfig::for_network(Network::Regtest, 1);
228 let relay = BitcoinNostrRelay::new(config).unwrap();
229
230 let result = relay.broadcast_transaction("deadbeef", "block_hash").await;
232 assert!(result.is_err());
233 assert!(result.unwrap_err().to_string().contains("Nostr relay disconnected"));
234 }
235
236 #[test]
237 fn test_bitcoin_nostr_relay_config_integration() {
238 let config = RelayConfig::for_network(Network::Regtest, 1)
239 .with_auth("custom_user".to_string(), "custom_pass".to_string())
240 .with_mempool_poll_interval_secs(5);
241
242 let relay = BitcoinNostrRelay::new(config).unwrap();
243
244 assert_eq!(relay.config.bitcoin_rpc_auth.username, "custom_user");
246 assert_eq!(relay.config.bitcoin_rpc_auth.password, "custom_pass");
247 assert_eq!(relay.config.mempool_poll_interval.as_secs(), 5);
248 }
249
250 #[tokio::test]
252 #[ignore] async fn test_connect_nostr_integration() {
254 }
272
273 #[tokio::test]
275 #[ignore] async fn test_start_relay_integration() {
277 }
294}