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 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 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 let (tx, redeem_script) =
76 self.tx_builder
77 .build_htlc_tx(¶ms, funding_utxos.clone(), change_address)?;
78
79 let p2sh_address = self.script_builder.script_to_p2sh_address(&redeem_script)?;
81 info!("📍 P2SH address: {}", p2sh_address);
82
83 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 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 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 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 let txid = self.rpc_client.send_raw_transaction(&tx_hex).await?;
146
147 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 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 let htlc = self.database.get_htlc_by_id(htlc_id)?;
174
175 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 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 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 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 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 let redeem_txid = self.rpc_client.send_raw_transaction(&tx_hex).await?;
227
228 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 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 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 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 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 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 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 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 let refund_txid = self.rpc_client.send_raw_transaction(&tx_hex).await?;
308
309 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 pub fn get_htlc(&self, htlc_id: &str) -> Result<ZcashHTLC, HTLCClientError> {
328 Ok(self.database.get_htlc_by_id(htlc_id)?)
329 }
330
331 pub async fn get_current_block_height(&self) -> Result<u64, HTLCClientError> {
342 Ok(self.rpc_client.get_block_count().await?)
343 }
344
345 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 pub fn generate_privkey(&self) -> String {
361 self.signer.generate_privkey()
362 }
363
364 pub fn derive_pubkey(&self, privkey: &str) -> Result<String, HTLCClientError> {
366 Ok(self.signer.derive_pubkey(privkey)?)
367 }
368
369 pub fn generate_hash_lock(&self, secret: &str) -> String {
371 self.signer.generate_hash_lock(secret)
372 }
373
374 pub fn network(&self) -> ZcashNetwork {
378 self.config.network
379 }
380
381 pub fn database(&self) -> &Database {
383 &self.database
384 }
385}
386
387#[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}