bitcoin_nostr_relay/
lib.rs

1pub mod bitcoin_rpc;
2pub mod validation;
3pub mod nostr;
4pub mod relay;
5pub mod networks;
6pub mod error;
7
8// Re-export core types for easy access
9pub 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
16/// Library result type using our custom error
17pub type Result<T, E = RelayError> = std::result::Result<T, E>;
18
19/// High-level API for Bitcoin-over-Nostr relay functionality
20pub struct BitcoinNostrRelay {
21    bitcoin_client: BitcoinRpcClient,
22    nostr_client: Option<NostrClient>,
23    validator: TransactionValidator,
24    config: RelayConfig,
25}
26
27impl BitcoinNostrRelay {
28    /// Create a new BitcoinNostrRelay instance with the given configuration
29    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        // Extract port from Bitcoin RPC URL for validator
37        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    /// Connect to the Nostr relay
57    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    /// Start the relay server (monitors mempool and relays transactions)
63    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    /// Broadcast a transaction to the Nostr network
75    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    /// Validate a transaction using the configured validator
84    pub async fn validate_transaction(&self, tx_hex: &str) -> Result<(), ValidationError> {
85        self.validator.validate(tx_hex).await
86    }
87    
88    /// Get the relay configuration
89    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        // Should be created without Nostr client initially
108        assert!(relay.nostr_client.is_none());
109    }
110    
111    #[test]
112    fn test_bitcoin_nostr_relay_with_different_configs() {
113        // Test regtest config
114        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        // Test testnet4 config
119        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        // Test custom config
124        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        // Should pass validation when disabled, even with invalid hex
162        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        // Should fail validation with empty transaction
172        let result = relay.validate_transaction("").await;
173        assert!(result.is_err());
174        
175        // Could be EmptyTransaction or InvalidStructure depending on validation order
176        match result {
177            Err(ValidationError::EmptyTransaction) => {
178                // Expected error type
179            }
180            Err(ValidationError::InvalidStructure) => {
181                // Also acceptable as TXID extraction fails first
182            }
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        // Should fail validation with invalid hex
193        let result = relay.validate_transaction("not_hex_characters").await;
194        assert!(result.is_err());
195        
196        if let Err(ValidationError::InvalidHex) = result {
197            // Expected error type
198        } 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        // Should fail validation with transaction too small (less than 60 bytes)
209        let small_tx = "a".repeat(118); // 59 bytes
210        let result = relay.validate_transaction(&small_tx).await;
211        assert!(result.is_err());
212        
213        // Could be InvalidSize (from precheck) or InvalidStructure (from TXID extraction)
214        match result {
215            Err(ValidationError::InvalidSize { size: 59 }) => {
216                // Expected error type from precheck
217            }
218            Err(ValidationError::InvalidStructure) => {
219                // Also acceptable as TXID extraction fails first
220            }
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        // Should fail to broadcast without Nostr client
231        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        // Config should be properly integrated
245        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    // Integration test that would require a real WebSocket connection
251    #[tokio::test]
252    #[ignore] // Use `cargo test -- --ignored` to run this test
253    async fn test_connect_nostr_integration() {
254        // This test would require setting up a real Nostr relay connection
255        // For now, we'll skip it in regular test runs
256        
257        // In a real integration test, you would:
258        // 1. Set up a test Nostr relay (like strfry in test mode)
259        // 2. Connect to it via WebSocket
260        // 3. Create BitcoinNostrRelay and connect
261        // 4. Test broadcasting transactions
262        
263        // Example structure:
264        // let config = RelayConfig::regtest(1);
265        // let mut relay = BitcoinNostrRelay::new(config).unwrap();
266        // let url = "ws://localhost:7777";
267        // let (ws_stream, _) = tokio_tungstenite::connect_async(url).await.unwrap();
268        // relay.connect_nostr(ws_stream).await.unwrap();
269        // let result = relay.broadcast_transaction("deadbeef", "block_hash").await;
270        // assert!(result.is_ok());
271    }
272    
273    // Integration test that would require a running Bitcoin node
274    #[tokio::test]
275    #[ignore] // Use `cargo test -- --ignored` to run this test
276    async fn test_start_relay_integration() {
277        // This test would require a full integration setup
278        // For now, we'll skip it in regular test runs
279        
280        // In a real integration test, you would:
281        // 1. Set up test Bitcoin node
282        // 2. Set up test Nostr relay
283        // 3. Create BitcoinNostrRelay with test config
284        // 4. Start the relay server
285        // 5. Test transaction flow
286        
287        // Example structure:
288        // let config = RelayConfig::regtest(1);
289        // let mut relay = BitcoinNostrRelay::new(config).unwrap();
290        // // Connect WebSocket, then start
291        // let result = relay.start().await;
292        // assert!(result.is_ok());
293    }
294}