zcash_htlc_builder/
lib.rs

1pub mod builder;
2pub mod config;
3pub mod database;
4pub mod models;
5pub mod rpc;
6pub mod script;
7pub mod signer;
8
9use chrono::Utc;
10use std::sync::Arc;
11use tracing::info;
12use uuid::Uuid;
13
14pub use builder::{TransactionBuilder, TxBuilderError};
15pub use config::{ConfigError, ZcashConfig};
16pub use models::*;
17pub use rpc::{RpcClientError, ZcashRpcClient};
18pub use script::{HTLCScriptBuilder, HTLCScriptError};
19pub use signer::{SignerError, TransactionSigner};
20
21use crate::database::{Database, DatabaseError};
22
23pub struct ZcashHTLCClient {
24    config: ZcashConfig,
25    database: Arc<Database>,
26    rpc_client: ZcashRpcClient,
27    tx_builder: TransactionBuilder,
28    signer: TransactionSigner,
29    script_builder: HTLCScriptBuilder,
30}
31
32impl ZcashHTLCClient {
33    /// Create new client from configuration
34    pub fn new(config: ZcashConfig, database: Arc<Database>) -> Self {
35        let rpc_client = ZcashRpcClient::new(
36            config.rpc_url.clone(),
37            config.rpc_user.clone(),
38            config.rpc_password.clone(),
39            config.network,
40        );
41
42        let rpc_client = if let Some(explorer) = &config.explorer_api {
43            rpc_client.with_custom_explorer(explorer.clone())
44        } else {
45            rpc_client
46        };
47
48        let tx_builder = TransactionBuilder::new(config.network);
49        let script_builder = HTLCScriptBuilder::new(config.network);
50        let signer = TransactionSigner::new(script_builder.clone());
51
52        Self {
53            config,
54            database,
55            rpc_client,
56            tx_builder,
57            signer,
58            script_builder: script_builder.clone(),
59        }
60    }
61
62    // ==================== HTLC Operations ====================
63
64    /// Create a new HTLC
65    pub async fn create_htlc(
66        &self,
67        params: HTLCParams,
68        funding_utxos: Vec<UTXO>,
69        change_address: &str,
70        funding_privkeys: Vec<&str>,
71    ) -> Result<HTLCCreationResult, HTLCClientError> {
72        info!("🔨 Creating HTLC for {} ZEC", params.amount);
73
74        // Build HTLC transaction
75        let (tx, redeem_script) =
76            self.tx_builder
77                .build_htlc_tx(&params, funding_utxos.clone(), change_address)?;
78
79        // Generate P2SH address
80        let p2sh_address = self.script_builder.script_to_p2sh_address(&redeem_script)?;
81        info!("📍 P2SH address: {}", p2sh_address);
82
83        // Build script pubkeys for signing
84        let input_scripts: Vec<_> = funding_utxos
85            .iter()
86            .map(|utxo| {
87                hex::decode(&utxo.script_pubkey)
88                    .map(bitcoin::blockdata::script::Script::from)
89                    .map_err(|_| HTLCClientError::InvalidScript)
90            })
91            .collect::<Result<Vec<_>, _>>()?;
92
93        // Sign transaction
94        let signed_tx = self
95            .signer
96            .sign_htlc_creation(tx, input_scripts, funding_privkeys)?;
97
98        let tx_hex = self.tx_builder.serialize_tx(&signed_tx);
99        let htlc_id = Uuid::new_v4().to_string();
100
101        // Create database record
102        let htlc = ZcashHTLC {
103            id: htlc_id.clone(),
104            txid: None,
105            p2sh_address: p2sh_address.clone(),
106            hash_lock: params.hash_lock.clone(),
107            secret: None,
108            timelock: params.timelock,
109            recipient_pubkey: params.recipient_pubkey.clone(),
110            refund_pubkey: params.refund_pubkey.clone(),
111            amount: params.amount.clone(),
112            network: self.config.network,
113            state: HTLCState::Pending,
114            vout: None,
115            script_hex: hex::encode(redeem_script.as_bytes()),
116            redeem_script_hex: hex::encode(redeem_script.as_bytes()),
117            signed_redeem_tx: None,
118            created_at: Utc::now(),
119            updated_at: Utc::now(),
120        };
121
122        self.database.create_htlc(&htlc)?;
123
124        // Create operation record
125        let operation_id = Uuid::new_v4().to_string();
126        let operation = HTLCOperation {
127            id: operation_id,
128            htlc_id: htlc_id.clone(),
129            operation_type: HTLCOperationType::Create,
130            txid: None,
131            raw_tx_hex: Some(tx_hex.clone()),
132            signed_tx_hex: Some(tx_hex.clone()),
133            broadcast_at: None,
134            confirmed_at: None,
135            block_height: None,
136            status: OperationStatus::Signed,
137            error_message: None,
138            created_at: Utc::now(),
139            updated_at: Utc::now(),
140        };
141
142        self.database.create_operation(&operation)?;
143
144        // Broadcast transaction
145        let txid = self.rpc_client.send_raw_transaction(&tx_hex).await?;
146
147        // Update database
148        self.database.update_htlc_txid(&htlc_id, &txid, 0)?;
149        self.database
150            .update_operation_broadcast(&operation.id, &txid)?;
151
152        info!("✅ HTLC created with txid: {}", txid);
153
154        Ok(HTLCCreationResult {
155            htlc_id,
156            txid,
157            p2sh_address,
158            redeem_script: hex::encode(redeem_script.as_bytes()),
159        })
160    }
161
162    /// Redeem an HTLC with the secret
163    pub async fn redeem_htlc(
164        &self,
165        htlc_id: &str,
166        secret: &str,
167        recipient_address: &str,
168        recipient_privkey: &str,
169    ) -> Result<String, HTLCClientError> {
170        info!("🔓 Redeeming HTLC: {}", htlc_id);
171
172        // Load HTLC from database
173        let htlc = self.database.get_htlc_by_id(htlc_id)?;
174
175        // Verify secret
176        if !self.script_builder.verify_secret(secret, &htlc.hash_lock) {
177            return Err(HTLCClientError::InvalidSecret);
178        }
179
180        let txid = htlc.txid.ok_or(HTLCClientError::HTLCNotLocked)?;
181        let vout = htlc.vout.ok_or(HTLCClientError::HTLCNotLocked)?;
182
183        // Decode redeem script
184        let redeem_script_bytes =
185            hex::decode(&htlc.redeem_script_hex).map_err(|_| HTLCClientError::InvalidScript)?;
186        let redeem_script = bitcoin::blockdata::script::Script::from(redeem_script_bytes);
187
188        // Build redeem transaction
189        let tx = self.tx_builder.build_redeem_tx(
190            &txid,
191            vout,
192            &htlc.amount,
193            secret,
194            &redeem_script,
195            recipient_address,
196        )?;
197
198        // Sign transaction
199        let signed_tx =
200            self.signer
201                .sign_htlc_redeem(tx, 0, &redeem_script, secret, recipient_privkey)?;
202
203        let tx_hex = self.tx_builder.serialize_tx(&signed_tx);
204
205        // Create operation record
206        let operation_id = Uuid::new_v4().to_string();
207        let operation = HTLCOperation {
208            id: operation_id.clone(),
209            htlc_id: htlc_id.to_string(),
210            operation_type: HTLCOperationType::Redeem,
211            txid: None,
212            raw_tx_hex: Some(tx_hex.clone()),
213            signed_tx_hex: Some(tx_hex.clone()),
214            broadcast_at: None,
215            confirmed_at: None,
216            block_height: None,
217            status: OperationStatus::Signed,
218            error_message: None,
219            created_at: Utc::now(),
220            updated_at: Utc::now(),
221        };
222
223        self.database.create_operation(&operation)?;
224
225        // Broadcast transaction
226        let redeem_txid = self.rpc_client.send_raw_transaction(&tx_hex).await?;
227
228        // Update database
229        self.database
230            .update_htlc_state(htlc_id, HTLCState::Redeemed)?;
231        self.database.update_htlc_secret(htlc_id, secret)?;
232        self.database
233            .update_operation_broadcast(&operation_id, &redeem_txid)?;
234
235        info!("✅ HTLC redeemed with txid: {}", redeem_txid);
236
237        Ok(redeem_txid)
238    }
239
240    /// Refund an HTLC after timelock expiry
241    pub async fn refund_htlc(
242        &self,
243        htlc_id: &str,
244        refund_address: &str,
245        refund_privkey: &str,
246    ) -> Result<String, HTLCClientError> {
247        info!("♻️ Refunding HTLC: {}", htlc_id);
248
249        // Load HTLC from database
250        let htlc = self.database.get_htlc_by_id(htlc_id)?;
251
252        let txid = htlc.txid.ok_or(HTLCClientError::HTLCNotLocked)?;
253        let vout = htlc.vout.ok_or(HTLCClientError::HTLCNotLocked)?;
254
255        // Check timelock
256        let current_block = self.rpc_client.get_block_count().await?;
257        if current_block < htlc.timelock {
258            return Err(HTLCClientError::TimelockNotExpired {
259                current: current_block,
260                required: htlc.timelock,
261            });
262        }
263
264        // Decode redeem script
265        let redeem_script_bytes =
266            hex::decode(&htlc.redeem_script_hex).map_err(|_| HTLCClientError::InvalidScript)?;
267        let redeem_script = bitcoin::blockdata::script::Script::from(redeem_script_bytes);
268
269        // Build refund transaction
270        let tx = self.tx_builder.build_refund_tx(
271            &txid,
272            vout,
273            &htlc.amount,
274            htlc.timelock,
275            &redeem_script,
276            refund_address,
277        )?;
278
279        // Sign transaction
280        let signed_tx = self
281            .signer
282            .sign_htlc_refund(tx, 0, &redeem_script, refund_privkey)?;
283
284        let tx_hex = self.tx_builder.serialize_tx(&signed_tx);
285
286        // Create operation record
287        let operation_id = Uuid::new_v4().to_string();
288        let operation = HTLCOperation {
289            id: operation_id.clone(),
290            htlc_id: htlc_id.to_string(),
291            operation_type: HTLCOperationType::Refund,
292            txid: None,
293            raw_tx_hex: Some(tx_hex.clone()),
294            signed_tx_hex: Some(tx_hex.clone()),
295            broadcast_at: None,
296            confirmed_at: None,
297            block_height: None,
298            status: OperationStatus::Signed,
299            error_message: None,
300            created_at: Utc::now(),
301            updated_at: Utc::now(),
302        };
303
304        self.database.create_operation(&operation)?;
305
306        // Broadcast transaction
307        let refund_txid = self.rpc_client.send_raw_transaction(&tx_hex).await?;
308
309        // Update database
310        self.database
311            .update_htlc_state(htlc_id, HTLCState::Refunded)?;
312        self.database
313            .update_operation_broadcast(&operation_id, &refund_txid)?;
314
315        info!("✅ HTLC refunded with txid: {}", refund_txid);
316
317        Ok(refund_txid)
318    }
319
320    pub async fn broadcast_raw_tx(&self, tx_hex: &str) -> Result<String, HTLCClientError> {
321        Ok(self.rpc_client.send_raw_transaction(tx_hex).await?)
322    }
323
324    // ==================== Query Methods ====================
325
326    /// Get HTLC by ID
327    pub fn get_htlc(&self, htlc_id: &str) -> Result<ZcashHTLC, HTLCClientError> {
328        Ok(self.database.get_htlc_by_id(htlc_id)?)
329    }
330
331    // Get UTXOs for address
332    // pub async fn get_utxos(&self, address: &str) -> Result<Vec<UTXO>, HTLCClientError> {
333    //     Ok(self.rpc_client.get_utxos(address).await?)
334    // }
335
336    // /// Get address balance
337    // pub async fn get_balance(&self, address: &str) -> Result<String, HTLCClientError> {
338    //     Ok(self.rpc_client.get_balance(address).await?)
339    // }
340
341    pub async fn get_current_block_height(&self) -> Result<u64, HTLCClientError> {
342        Ok(self.rpc_client.get_block_count().await?)
343    }
344
345    /// Wait for transaction confirmation
346    pub async fn wait_for_confirmation(
347        &self,
348        txid: &str,
349        confirmations: u32,
350    ) -> Result<u32, HTLCClientError> {
351        Ok(self
352            .rpc_client
353            .wait_for_confirmations(txid, confirmations, 60)
354            .await?)
355    }
356
357    // ==================== Key Management ====================
358
359    /// Generate new private key
360    pub fn generate_privkey(&self) -> String {
361        self.signer.generate_privkey()
362    }
363
364    /// Derive public key from private key
365    pub fn derive_pubkey(&self, privkey: &str) -> Result<String, HTLCClientError> {
366        Ok(self.signer.derive_pubkey(privkey)?)
367    }
368
369    /// Generate hash lock from secret
370    pub fn generate_hash_lock(&self, secret: &str) -> String {
371        self.signer.generate_hash_lock(secret)
372    }
373
374    // ==================== Utilities ====================
375
376    /// Get current network
377    pub fn network(&self) -> ZcashNetwork {
378        self.config.network
379    }
380
381    /// Get database reference
382    pub fn database(&self) -> &Database {
383        &self.database
384    }
385}
386
387// ==================== Error Types ====================
388
389#[derive(Debug, thiserror::Error)]
390pub enum HTLCClientError {
391    #[error("Config error: {0}")]
392    ConfigError(#[from] ConfigError),
393
394    #[error("Database error: {0}")]
395    DatabaseError(#[from] DatabaseError),
396
397    #[error("RPC error: {0}")]
398    RpcError(#[from] RpcClientError),
399
400    #[error("Transaction builder error: {0}")]
401    TxBuilderError(#[from] TxBuilderError),
402
403    #[error("Script error: {0}")]
404    ScriptError(#[from] HTLCScriptError),
405
406    #[error("Signer error: {0}")]
407    SignerError(#[from] SignerError),
408
409    #[error("Invalid secret for hash lock")]
410    InvalidSecret,
411
412    #[error("HTLC not locked (missing txid or vout)")]
413    HTLCNotLocked,
414
415    #[error("Invalid script format")]
416    InvalidScript,
417
418    #[error("Timelock not expired (current: {current}, required: {required})")]
419    TimelockNotExpired { current: u64, required: u64 },
420}