1mod utils;
8pub use crate::{
9 bitcoind::{interface::WalletTransaction, BitcoindError},
10 communication::ServerStatus,
11 revaultd::{BlockchainTip, UserRole, VaultStatus},
12};
13use crate::{
14 communication::{
15 announce_spend_transaction, check_spend_transaction_size, coord_share_rev_signatures,
16 coordinator_status, cosigners_status, fetch_cosigs_signatures, share_unvault_signatures,
17 watchtowers_status, wts_share_rev_signatures, CommunicationError,
18 },
19 database::{
20 actions::{
21 db_delete_spend, db_insert_spend, db_mark_activating_vault,
22 db_mark_broadcastable_spend, db_mark_securing_vault, db_update_presigned_txs,
23 db_update_spend, db_update_vault_status,
24 },
25 interface::{
26 db_cancel_transaction_by_txid, db_emer_transaction, db_list_spends,
27 db_spend_transaction, db_tip, db_unvault_emer_transaction, db_unvault_transaction,
28 db_vault_by_deposit, db_vault_by_unvault_txid, db_vaults, db_vaults_from_spend,
29 db_vaults_min_status,
30 },
31 schema::DbTransaction,
32 },
33 threadmessages::BitcoindThread,
34 DaemonControl, VERSION,
35};
36use utils::{
37 deser_amount_from_sats, deser_from_str, finalized_emer_txs, gethistory, listvaults_from_db,
38 presigned_txs, ser_amount, ser_to_string, spend_entry, unvault_tx, vaults_from_deposits,
39};
40
41use revault_tx::{
42 bitcoin::{
43 consensus::encode, secp256k1, util::bip32, Address, Amount, Network, OutPoint,
44 PublicKey as BitcoinPubKey, Transaction as BitcoinTransaction, TxOut, Txid,
45 },
46 scripts::{CpfpDescriptor, DepositDescriptor, UnvaultDescriptor},
47 transactions::{
48 spend_tx_from_deposits, transaction_chain, transaction_chain_manager, CancelTransaction,
49 CpfpableTransaction, EmergencyTransaction, RevaultPresignedTransaction, RevaultTransaction,
50 SpendTransaction, UnvaultEmergencyTransaction, UnvaultTransaction,
51 },
52 txouts::{DepositTxOut, SpendTxOut},
53};
54
55use std::{collections::BTreeMap, fmt};
56
57use serde::{Deserialize, Serialize};
58
59#[derive(Debug)]
61pub enum CommandError {
62 UnknownOutpoint(OutPoint),
63 InvalidStatus(VaultStatus, VaultStatus),
65 InvalidStatusFor(VaultStatus, OutPoint),
66 InvalidParams(String),
68 UnknownCancel(Txid),
69 Communication(CommunicationError),
70 Bitcoind(BitcoindError),
71 Tx(revault_tx::Error),
72 SpendFeerateTooLow(u64, u64),
74 SpendTooLarge,
75 SpendUnknownUnVault(Txid),
76 UnknownSpend(Txid),
77 SpendSpent(Txid),
78 SpendNotEnoughSig(usize, usize),
80 SpendInvalidSig(Vec<u8>),
81 MissingCpfpKey,
82 ManagerOnly,
83 StakeholderOnly,
84 Race,
85}
86
87impl fmt::Display for CommandError {
88 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
89 match self {
90 Self::UnknownOutpoint(op) => write!(f, "No vault at '{}'", op),
91 Self::InvalidStatus(got, expected) => {
92 write!(f, "Invalid vault status: '{}'. Need '{}'.", got, expected)
93 }
94 Self::InvalidStatusFor(status, outpoint) => write!(
95 f,
96 "Invalid vault status '{}' for deposit outpoint '{}'",
97 status, outpoint
98 ),
99 Self::InvalidParams(e) => write!(f, "{}", e),
100 Self::Communication(e) => write!(f, "Communication error: '{}'", e),
101 Self::Bitcoind(e) => write!(f, "Bitcoind error: '{}'", e),
102 Self::Tx(e) => write!(f, "Transaction related error: '{}'", e),
103 Self::SpendFeerateTooLow(req, actual) => write!(
104 f,
105 "Required feerate ('{}') is significantly higher than actual feerate ('{}')",
106 req, actual
107 ),
108 Self::SpendTooLarge => write!(
109 f,
110 "Spend transaction is too large, try spending less outpoints"
111 ),
112 Self::SpendUnknownUnVault(txid) => {
113 write!(f, "Spend transaction refers an unknown Unvault: '{}'", txid)
114 }
115 Self::UnknownSpend(txid) => {
116 write!(f, "Unknown Spend transaction '{}'", txid)
117 }
118 Self::SpendSpent(txid) => {
119 write!(f, "Spend '{}' refers to a spent vault", txid)
120 }
121 Self::MissingCpfpKey => {
122 write!(
123 f,
124 "Can't read the cpfp key. Make sure you have a file called \
125 cpfp_secret containing the private key in your datadir and \
126 restart the daemon."
127 )
128 }
129 Self::SpendNotEnoughSig(got, req) => {
130 write!(
131 f,
132 "Not enough signatures, needed: {}, current: {}",
133 req, got
134 )
135 }
136 Self::SpendInvalidSig(sig) => {
137 write!(
138 f,
139 "Spend PSBT contains an invalid signature: '{}'",
140 encode::serialize_hex(&sig)
141 )
142 }
143 Self::StakeholderOnly => {
144 write!(f, "This is a stakeholder command")
145 }
146 Self::ManagerOnly => {
147 write!(f, "This is a manager command")
148 }
149 Self::Race => write!(f, "Internal error due to a race. Please try again."),
150 Self::UnknownCancel(txid) => write!(f, "Unknown Cancel transaction: '{}'", txid),
151 }
152 }
153}
154
155impl std::error::Error for CommandError {}
156
157impl From<BitcoindError> for CommandError {
158 fn from(e: BitcoindError) -> Self {
159 Self::Bitcoind(e)
160 }
161}
162
163impl From<CommunicationError> for CommandError {
164 fn from(e: CommunicationError) -> Self {
165 Self::Communication(e)
166 }
167}
168
169impl From<revault_tx::Error> for CommandError {
170 fn from(e: revault_tx::Error) -> Self {
171 Self::Tx(e)
172 }
173}
174
175impl CommandError {
176 pub fn code(&self) -> ErrorCode {
177 match self {
178 CommandError::UnknownOutpoint(_) => ErrorCode::RESOURCE_NOT_FOUND_ERROR,
179 CommandError::InvalidStatus(..) => ErrorCode::INVALID_STATUS_ERROR,
180 CommandError::InvalidStatusFor(..) => ErrorCode::INVALID_STATUS_ERROR,
181 CommandError::InvalidParams(_) => ErrorCode::INVALID_PARAMS,
182 CommandError::Communication(e) => match e {
183 CommunicationError::Net(_) => ErrorCode::TRANSPORT_ERROR,
184 CommunicationError::WatchtowerNack(_, _) => ErrorCode::WT_SIG_NACK,
185 CommunicationError::SignatureStorage => ErrorCode::COORDINATOR_SIG_STORE_ERROR,
186 CommunicationError::SpendTxStorage => ErrorCode::COORDINATOR_SPEND_STORE_ERROR,
187 CommunicationError::CosigAlreadySigned => ErrorCode::COSIGNER_ALREADY_SIGN_ERROR,
188 CommunicationError::CosigInsanePsbt => ErrorCode::COSIGNER_INSANE_ERROR,
189 },
190 CommandError::Bitcoind(_) => ErrorCode::BITCOIND_ERROR,
191 CommandError::Tx(_) => ErrorCode::INTERNAL_ERROR,
192 CommandError::SpendFeerateTooLow(_, _) => ErrorCode::INVALID_PARAMS,
193 CommandError::SpendTooLarge
195 | CommandError::SpendUnknownUnVault(_)
196 | CommandError::UnknownSpend(_)
197 | CommandError::SpendSpent(_)
198 | CommandError::SpendNotEnoughSig(_, _)
199 | CommandError::SpendInvalidSig(_)
200 | CommandError::MissingCpfpKey
201 | CommandError::UnknownCancel(_) => ErrorCode::INVALID_PARAMS,
202 CommandError::StakeholderOnly | CommandError::ManagerOnly => ErrorCode::INVALID_REQUEST,
203 CommandError::Race => ErrorCode::INTERNAL_ERROR,
204 }
205 }
206}
207
208#[allow(non_camel_case_types)]
209pub enum ErrorCode {
210 INVALID_PARAMS = -32602,
212 INVALID_REQUEST = -32600,
214 INTERNAL_ERROR = -32603,
216 TRANSPORT_ERROR = 12000,
218 WT_SIG_NACK = 13_000,
220 COORDINATOR_SIG_STORE_ERROR = 13100,
222 COORDINATOR_SPEND_STORE_ERROR = 13101,
224 COSIGNER_ALREADY_SIGN_ERROR = 13201,
226 COSIGNER_INSANE_ERROR = 13202,
228 BITCOIND_ERROR = 14000,
230 RESOURCE_NOT_FOUND_ERROR = 15000,
232 INVALID_STATUS_ERROR = 15001,
234}
235
236macro_rules! stakeholder_only {
237 ($revaultd:ident) => {
238 if !$revaultd.is_stakeholder() {
239 return Err(CommandError::StakeholderOnly);
240 }
241 };
242}
243
244macro_rules! manager_only {
245 ($revaultd:ident) => {
246 if !$revaultd.is_manager() {
247 return Err(CommandError::ManagerOnly);
248 }
249 };
250}
251
252impl DaemonControl {
253 pub fn get_info(&self) -> GetInfoResult {
255 let revaultd = self.revaultd.read().unwrap();
256
257 let BlockchainTip {
259 height: blockheight,
260 ..
261 } = db_tip(&revaultd.db_file()).expect("Database must not be dead");
262 let number_of_vaults = self
263 .list_vaults(None, None)
264 .iter()
265 .filter(|l| {
266 l.status != VaultStatus::Spent
267 && l.status != VaultStatus::Canceled
268 && l.status != VaultStatus::Unvaulted
269 && l.status != VaultStatus::EmergencyVaulted
270 })
271 .count();
272
273 assert!(revaultd.is_stakeholder() || revaultd.is_manager());
274 let participant_type = if revaultd.is_manager() && revaultd.is_stakeholder() {
275 UserRole::StakeholderManager
276 } else if revaultd.is_manager() {
277 UserRole::Manager
278 } else {
279 UserRole::Stakeholder
280 };
281
282 GetInfoResult {
283 version: VERSION.to_string(),
284 network: revaultd.bitcoind_config.network,
285 blockheight: blockheight as i32,
286 sync: self.bitcoind_conn.sync_progress(),
287 vaults: number_of_vaults,
288 managers_threshold: revaultd.managers_threshold(),
289 descriptors: GetInfoDescriptors {
290 deposit: revaultd.deposit_descriptor.clone(),
291 unvault: revaultd.unvault_descriptor.clone(),
292 cpfp: revaultd.cpfp_descriptor.clone(),
293 },
294 participant_type,
295 }
296 }
297
298 pub fn list_vaults(
300 &self,
301 statuses: Option<&[VaultStatus]>,
302 deposit_outpoints: Option<&[OutPoint]>,
303 ) -> Vec<ListVaultsEntry> {
304 let revaultd = self.revaultd.read().unwrap();
305 listvaults_from_db(&revaultd, statuses, deposit_outpoints)
306 .expect("Database must be available")
307 }
308
309 pub fn get_deposit_address(&self) -> Address {
311 self.revaultd.read().unwrap().deposit_address()
312 }
313
314 pub(crate) fn get_deposit_address_at(&self, index: bip32::ChildNumber) -> Address {
316 self.revaultd.read().unwrap().vault_address(index)
317 }
318
319 pub fn get_revocation_txs(
326 &self,
327 deposit_outpoint: OutPoint,
328 ) -> Result<RevocationTransactions, CommandError> {
329 let revaultd = self.revaultd.read().unwrap();
330 stakeholder_only!(revaultd);
331 let db_path = &revaultd.db_file();
332
333 let vault = db_vault_by_deposit(db_path, &deposit_outpoint)
335 .expect("Database must be available")
336 .ok_or_else(|| CommandError::UnknownOutpoint(deposit_outpoint))?;
337 if matches!(vault.status, VaultStatus::Unconfirmed) {
338 return Err(CommandError::InvalidStatus(
339 vault.status,
340 VaultStatus::Funded,
341 ));
342 };
343
344 let emer_address = revaultd
345 .emergency_address
346 .clone()
347 .expect("Must be stakeholder");
348 let (_, cancel_batch, emergency_tx, emergency_unvault_tx) = transaction_chain(
349 deposit_outpoint,
350 vault.amount,
351 &revaultd.deposit_descriptor,
352 &revaultd.unvault_descriptor,
353 &revaultd.cpfp_descriptor,
354 vault.derivation_index,
355 emer_address,
356 &revaultd.secp_ctx,
357 )
358 .expect("We wouldn't have put a vault with an invalid chain in DB");
359
360 Ok(RevocationTransactions {
361 cancel_txs: cancel_batch.all_feerates(),
362 emergency_tx,
363 emergency_unvault_tx,
364 })
365 }
366
367 pub fn set_revocation_txs(
374 &self,
375 deposit_outpoint: OutPoint,
376 revocation_txs: RevocationTransactions,
377 ) -> Result<(), CommandError> {
378 let revaultd = self.revaultd.read().unwrap();
379 stakeholder_only!(revaultd);
380 let db_path = revaultd.db_file();
381 let secp_ctx = &revaultd.secp_ctx;
382
383 let db_vault = db_vault_by_deposit(&db_path, &deposit_outpoint)
386 .expect("Database must be available")
387 .ok_or_else(|| CommandError::UnknownOutpoint(deposit_outpoint))?;
388 if !matches!(db_vault.status, VaultStatus::Funded) {
389 return Err(CommandError::InvalidStatus(
390 db_vault.status,
391 VaultStatus::Funded,
392 ));
393 };
394
395 let mut cancel_txs_sigs = Vec::with_capacity(revocation_txs.cancel_txs.len());
397 for cancel_tx in revocation_txs.cancel_txs.iter() {
398 let cancel_db_tx = db_cancel_transaction_by_txid(&db_path, &cancel_tx.txid())
399 .expect("The database must be available")
400 .ok_or_else(|| CommandError::UnknownCancel(cancel_tx.txid()))?;
401 let rpc_txid = cancel_tx.tx().wtxid();
402 let db_txid = cancel_db_tx.psbt.wtxid();
403 if rpc_txid != db_txid {
404 return Err(CommandError::InvalidParams(format!(
405 "Invalid Cancel tx: db wtxid is '{}' but this PSBT's is '{}' ",
406 db_txid, rpc_txid
407 )));
408 }
409 cancel_txs_sigs.push((cancel_db_tx, cancel_tx.signatures()));
410 }
411 let mut emer_db_tx = db_emer_transaction(&revaultd.db_file(), db_vault.id)
412 .expect("The database must be available")
413 .ok_or(CommandError::Race)?;
414 let rpc_txid = revocation_txs.emergency_tx.tx().wtxid();
415 let db_txid = emer_db_tx.psbt.wtxid();
416 if rpc_txid != db_txid {
417 return Err(CommandError::InvalidParams(format!(
418 "Invalid Emergency tx: db wtxid is '{}' but this PSBT's is '{}' ",
419 db_txid, rpc_txid
420 )));
421 }
422 let mut unvault_emer_db_tx = db_unvault_emer_transaction(&revaultd.db_file(), db_vault.id)
423 .expect("The database must be available")
424 .ok_or(CommandError::Race)?;
425 let rpc_txid = revocation_txs.emergency_unvault_tx.tx().wtxid();
426 let db_txid = unvault_emer_db_tx.psbt.wtxid();
427 if rpc_txid != db_txid {
428 return Err(CommandError::InvalidParams(format!(
429 "Invalid Unvault Emergency tx: db wtxid is '{}' but this PSBT's is '{}' ",
430 db_txid, rpc_txid
431 )));
432 }
433
434 let deriv_index = db_vault.derivation_index;
436 let emer_sigs = revocation_txs.emergency_tx.signatures();
437 let unvault_emer_sigs = revocation_txs.emergency_unvault_tx.signatures();
438
439 let our_pubkey = revaultd
441 .our_stk_xpub_at(deriv_index)
442 .expect("We are a stakeholder, checked at the beginning of the call.");
443 for (_, sigs) in cancel_txs_sigs.iter() {
444 if !sigs.contains_key(&our_pubkey) {
445 return Err(CommandError::InvalidParams(format!(
446 "No signature for ourselves ({}) in Cancel transaction",
447 our_pubkey
448 )));
449 }
450 }
451 if !emer_sigs.contains_key(&our_pubkey) {
454 return Err(CommandError::InvalidParams(
455 "No signature for ourselves in Emergency transaction".to_string(),
456 ));
457 }
458 if !unvault_emer_sigs.contains_key(&our_pubkey) {
459 return Err(CommandError::InvalidParams(
460 "No signature for ourselves in UnvaultEmergency transaction".to_string(),
461 ));
462 }
463
464 let stk_keys = revaultd.stakeholders_xpubs_at(deriv_index);
466 for (_, sigs) in cancel_txs_sigs.iter() {
467 for (ref key, _) in sigs.iter() {
468 if !stk_keys.contains(key) {
469 return Err(CommandError::InvalidParams(format!(
470 "Unknown key in Cancel transaction signatures: {}",
471 key
472 )));
473 }
474 }
475 }
476 for (ref key, _) in emer_sigs.iter() {
477 if !stk_keys.contains(key) {
478 return Err(CommandError::InvalidParams(format!(
479 "Unknown key in Emergency transaction signatures: {}",
480 key
481 )));
482 }
483 }
484 for (ref key, _) in unvault_emer_sigs.iter() {
485 if !stk_keys.contains(key) {
486 return Err(CommandError::InvalidParams(format!(
487 "Unknown key in UnvaultEmergency transaction signatures: {}",
488 key
489 )));
490 }
491 }
492
493 for (ref mut cancel_db_tx, sigs) in cancel_txs_sigs.iter_mut() {
495 for (key, sig) in sigs.iter() {
496 if sig.is_empty() {
497 return Err(CommandError::InvalidParams(format!(
498 "Empty signature for key '{}' in Cancel PSBT",
499 key
500 )));
501 }
502 let sig = secp256k1::Signature::from_der(&sig[..sig.len() - 1]).map_err(|_| {
503 CommandError::InvalidParams(format!("Non DER signature in Cancel PSBT"))
504 })?;
505 cancel_db_tx
506 .psbt
507 .add_signature(key.key, sig, secp_ctx)
508 .map_err(|e| {
509 CommandError::InvalidParams(format!(
510 "Invalid signature '{}' in Cancel PSBT: '{}'",
511 sig, e
512 ))
513 })?;
514 }
515 }
516 for (key, sig) in emer_sigs {
517 if sig.is_empty() {
518 return Err(CommandError::InvalidParams(format!(
519 "Empty signature for key '{}' in Emergency PSBT",
520 key
521 )));
522 }
523 let sig = secp256k1::Signature::from_der(&sig[..sig.len() - 1]).map_err(|_| {
524 CommandError::InvalidParams(format!("Non DER signature in Emergency PSBT"))
525 })?;
526 emer_db_tx
527 .psbt
528 .add_signature(key.key, sig, secp_ctx)
529 .map_err(|e| {
530 CommandError::InvalidParams(format!(
531 "Invalid signature '{}' in Emergency PSBT: '{}'",
532 sig, e
533 ))
534 })?;
535 }
536 for (key, sig) in unvault_emer_sigs {
537 if sig.is_empty() {
538 return Err(CommandError::InvalidParams(format!(
539 "Empty signature for key '{}' in UnvaultEmergency PSBT",
540 key
541 )));
542 }
543 let sig = secp256k1::Signature::from_der(&sig[..sig.len() - 1]).map_err(|_| {
544 CommandError::InvalidParams(format!("Non DER signature in UnvaultEmergency PSBT",))
545 })?;
546 unvault_emer_db_tx
547 .psbt
548 .add_signature(key.key, sig, secp_ctx)
549 .map_err(|e| {
550 CommandError::InvalidParams(format!(
551 "Invalid signature '{}' in UnvaultEmergency PSBT: '{}'",
552 sig, e
553 ))
554 })?;
555 }
556
557 let rev_txs: Vec<DbTransaction> = cancel_txs_sigs
560 .into_iter()
561 .map(|(tx, _)| tx)
562 .chain([emer_db_tx, unvault_emer_db_tx].iter().cloned())
563 .collect();
564 db_update_presigned_txs(&db_path, &db_vault, rev_txs.clone(), secp_ctx)
565 .expect("The database must be available");
566 db_mark_securing_vault(&db_path, db_vault.id).expect("The database must be available");
567
568 let emer_tx = db_emer_transaction(&db_path, db_vault.id)
570 .expect("Database must be available")
571 .ok_or(CommandError::Race)?;
572 let (_, cancel_batch) = transaction_chain_manager(
573 db_vault.deposit_outpoint,
574 db_vault.amount,
575 &revaultd.deposit_descriptor,
576 &revaultd.unvault_descriptor,
577 &revaultd.cpfp_descriptor,
578 db_vault.derivation_index,
579 &revaultd.secp_ctx,
580 )
581 .expect("We wouldn't have put a vault with an invalid chain in DB");
582 let cancel_txs = cancel_batch
583 .feerates_map()
584 .into_iter()
585 .map(|(amount, cancel_tx)| {
586 db_cancel_transaction_by_txid(&db_path, &cancel_tx.txid())
587 .expect("Database must be available")
588 .ok_or(CommandError::Race)
589 .map(|tx| (amount, tx))
590 })
591 .collect::<Result<BTreeMap<_, _>, CommandError>>()?;
592 let unemer_tx = db_unvault_emer_transaction(&db_path, db_vault.id)
593 .expect("Database must be available")
594 .ok_or(CommandError::Race)?;
595 let all_rev_fully_signed = emer_tx
596 .psbt
597 .unwrap_emer()
598 .is_finalizable(&revaultd.secp_ctx)
599 && cancel_txs.iter().all(|(_, cancel_tx)| {
600 cancel_tx
601 .psbt
602 .unwrap_cancel()
603 .is_finalizable(&revaultd.secp_ctx)
604 })
605 && unemer_tx
606 .psbt
607 .unwrap_unvault_emer()
608 .is_finalizable(&revaultd.secp_ctx);
609
610 if all_rev_fully_signed {
612 if let Some(ref watchtowers) = revaultd.watchtowers {
613 wts_share_rev_signatures(
614 &revaultd.noise_secret,
615 &watchtowers,
616 db_vault.deposit_outpoint,
617 db_vault.derivation_index,
618 &emer_tx,
619 &cancel_txs,
620 &unemer_tx,
621 )?;
622 }
623 }
624 db_update_vault_status(&db_path, &db_vault).expect("The database must be available");
625
626 coord_share_rev_signatures(
628 revaultd.coordinator_host,
629 &revaultd.noise_secret,
630 &revaultd.coordinator_noisekey,
631 &rev_txs,
632 )?;
633
634 Ok(())
635 }
636
637 pub fn get_unvault_tx(
644 &self,
645 deposit_outpoint: OutPoint,
646 ) -> Result<UnvaultTransaction, CommandError> {
647 let revaultd = self.revaultd.read().unwrap();
648 stakeholder_only!(revaultd);
649 let db_path = &revaultd.db_file();
650 assert!(revaultd.is_stakeholder());
651
652 let vault = db_vault_by_deposit(db_path, &deposit_outpoint)
655 .expect("The database must be available")
656 .ok_or_else(|| CommandError::UnknownOutpoint(deposit_outpoint))?;
657 if matches!(vault.status, VaultStatus::Unconfirmed) {
658 return Err(CommandError::InvalidStatus(
659 vault.status,
660 VaultStatus::Funded,
661 ));
662 }
663
664 Ok(unvault_tx(&revaultd, &vault)
665 .expect("We wouldn't have a vault with an invalid Unvault in DB"))
666 }
667
668 pub fn set_unvault_tx(
675 &self,
676 deposit_outpoint: OutPoint,
677 unvault_tx: UnvaultTransaction,
678 ) -> Result<(), CommandError> {
679 let revaultd = self.revaultd.read().unwrap();
680 stakeholder_only!(revaultd);
681 let db_path = revaultd.db_file();
682 let secp_ctx = &revaultd.secp_ctx;
683
684 let db_vault = db_vault_by_deposit(&db_path, &deposit_outpoint)
689 .expect("The database must be available")
690 .ok_or_else(|| CommandError::UnknownOutpoint(deposit_outpoint))?;
691 if !matches!(db_vault.status, VaultStatus::Secured) {
692 return Err(CommandError::InvalidStatus(
693 db_vault.status,
694 VaultStatus::Secured,
695 ));
696 }
697
698 let mut unvault_db_tx = db_unvault_transaction(&db_path, db_vault.id)
700 .expect("The database must be available")
701 .ok_or(CommandError::Race)?;
702 let rpc_txid = unvault_tx.tx().wtxid();
703 let db_txid = unvault_db_tx.psbt.wtxid();
704 if rpc_txid != db_txid {
705 return Err(CommandError::InvalidParams(format!(
706 "Invalid Unvault tx: db wtxid is '{}' but this PSBT's is '{}' ",
707 db_txid, rpc_txid
708 )));
709 }
710
711 let sigs = unvault_tx.signatures();
712 let stk_keys = revaultd.stakeholders_xpubs_at(db_vault.derivation_index);
713 let our_key = revaultd
714 .our_stk_xpub_at(db_vault.derivation_index)
715 .expect("We are a stakeholder, checked at the beginning.");
716 if !sigs.contains_key(&our_key) {
719 return Err(CommandError::InvalidParams(format!(
720 "No signature for ourselves ({}) in Unvault transaction",
721 our_key
722 )));
723 }
724
725 for (key, sig) in sigs {
726 if !stk_keys.contains(&key) {
728 return Err(CommandError::InvalidParams(format!(
729 "Unknown key in Unvault transaction signatures: {}",
730 key
731 )));
732 }
733
734 if sig.is_empty() {
735 return Err(CommandError::InvalidParams(format!(
736 "Empty signature for key '{}' in Unvault PSBT",
737 key
738 )));
739 }
740 let sig = secp256k1::Signature::from_der(&sig[..sig.len() - 1]).map_err(|_| {
741 CommandError::InvalidParams(format!("Non DER signature in Unvault PSBT"))
742 })?;
743
744 unvault_db_tx
745 .psbt
746 .add_signature(key.key, sig, secp_ctx)
747 .map_err(|e| {
748 CommandError::InvalidParams(format!(
749 "Invalid signature '{}' in Unvault PSBT: '{}'",
750 sig, e
751 ))
752 })?;
753 }
754
755 db_update_presigned_txs(&db_path, &db_vault, vec![unvault_db_tx.clone()], secp_ctx)
757 .expect("The database must be available");
758 db_mark_activating_vault(&db_path, db_vault.id).expect("The database must be available");
759 db_update_vault_status(&db_path, &db_vault).expect("The database must be available");
760 share_unvault_signatures(
761 revaultd.coordinator_host,
762 &revaultd.noise_secret,
763 &revaultd.coordinator_noisekey,
764 &unvault_db_tx,
765 )?;
766
767 Ok(())
768 }
769
770 pub fn list_presigned_txs(
777 &self,
778 outpoints: &[OutPoint],
779 ) -> Result<Vec<ListPresignedTxEntry>, CommandError> {
780 let revaultd = self.revaultd.read().unwrap();
781 let db_path = revaultd.db_file();
782 let db_vaults = if outpoints.is_empty() {
783 db_vaults_min_status(&db_path, VaultStatus::Funded).expect("Database must be available")
784 } else {
785 vaults_from_deposits(&db_path, &outpoints, &[VaultStatus::Unconfirmed])?
786 };
787
788 presigned_txs(&revaultd, db_vaults).ok_or(CommandError::Race)
789 }
790
791 pub fn list_onchain_txs(
798 &self,
799 outpoints: &[OutPoint],
800 ) -> Result<Vec<ListOnchainTxEntry>, CommandError> {
801 let revaultd = self.revaultd.read().unwrap();
802 let db_path = &revaultd.db_file();
803
804 let db_vaults = if outpoints.is_empty() {
805 db_vaults(&db_path).expect("Database must be available")
806 } else {
807 vaults_from_deposits(&db_path, &outpoints, &[])?
809 };
810
811 let mut tx_list = Vec::with_capacity(db_vaults.len());
812 for db_vault in db_vaults {
813 let vault_outpoint = db_vault.deposit_outpoint;
814
815 let deposit = self
817 .bitcoind_conn
818 .wallet_tx(db_vault.deposit_outpoint.txid)?
819 .expect("Vault exists but not deposit tx?");
820
821 let (unvault, cancel, emergency, unvault_emergency, spend) = match db_vault.status {
825 VaultStatus::Unvaulting | VaultStatus::Unvaulted => {
826 let unvault_db_tx = db_unvault_transaction(db_path, db_vault.id)
827 .expect("Database must be available")
828 .ok_or(CommandError::Race)?;
829 let unvault = self.bitcoind_conn.wallet_tx(unvault_db_tx.psbt.txid())?;
830 (unvault, None, None, None, None)
831 }
832 VaultStatus::Spending | VaultStatus::Spent => {
833 let unvault_db_tx = db_unvault_transaction(db_path, db_vault.id)
834 .expect("Database must be available")
835 .ok_or(CommandError::Race)?;
836 let unvault = self.bitcoind_conn.wallet_tx(unvault_db_tx.psbt.txid())?;
837 let spend = if let Some(spend_txid) = db_vault.final_txid {
838 self.bitcoind_conn.wallet_tx(spend_txid)?
839 } else {
840 None
841 };
842 (unvault, None, None, None, spend)
843 }
844 VaultStatus::Canceling | VaultStatus::Canceled => {
845 let unvault_db_tx = db_unvault_transaction(db_path, db_vault.id)
846 .expect("Database must be available")
847 .ok_or(CommandError::Race)?;
848 let unvault = self.bitcoind_conn.wallet_tx(unvault_db_tx.psbt.txid())?;
849 let cancel = if let Some(cancel_txid) = db_vault.final_txid {
850 self.bitcoind_conn.wallet_tx(cancel_txid)?
851 } else {
852 None
853 };
854 (unvault, cancel, None, None, None)
855 }
856 VaultStatus::EmergencyVaulting | VaultStatus::EmergencyVaulted => {
857 if revaultd.is_stakeholder() {
859 let emer_db_tx = db_emer_transaction(db_path, db_vault.id)
860 .expect("Database must be available")
861 .ok_or(CommandError::Race)?;
862 let emergency = self.bitcoind_conn.wallet_tx(emer_db_tx.psbt.txid())?;
863 (None, None, emergency, None, None)
864 } else {
865 (None, None, None, None, None)
866 }
867 }
868 VaultStatus::UnvaultEmergencyVaulting | VaultStatus::UnvaultEmergencyVaulted => {
869 let unvault_db_tx = db_unvault_transaction(db_path, db_vault.id)
870 .expect("Database must be available")
871 .ok_or(CommandError::Race)?;
872 let unvault = self.bitcoind_conn.wallet_tx(unvault_db_tx.psbt.txid())?;
873
874 if revaultd.is_stakeholder() {
876 let unemer_db_tx = db_emer_transaction(db_path, db_vault.id)
877 .expect("Database must be available")
878 .ok_or(CommandError::Race)?;
879 let unvault_emergency =
880 self.bitcoind_conn.wallet_tx(unemer_db_tx.psbt.txid())?;
881 (unvault, None, None, unvault_emergency, None)
882 } else {
883 (unvault, None, None, None, None)
884 }
885 }
886 VaultStatus::Unconfirmed
888 | VaultStatus::Funded
889 | VaultStatus::Securing
890 | VaultStatus::Secured
891 | VaultStatus::Activating
892 | VaultStatus::Active => (None, None, None, None, None),
893 };
894
895 tx_list.push(ListOnchainTxEntry {
896 vault_outpoint,
897 deposit,
898 unvault,
899 cancel,
900 emergency,
901 unvault_emergency,
902 spend,
903 });
904 }
905
906 Ok(tx_list)
907 }
908
909 pub fn get_spend_tx(
921 &self,
922 outpoints: &[OutPoint],
923 destinations: &BTreeMap<Address, u64>,
924 feerate_vb: u64,
925 ) -> Result<ListSpendEntry, CommandError> {
926 let revaultd = self.revaultd.read().unwrap();
927 manager_only!(revaultd);
928 let db_file = &revaultd.db_file();
929
930 assert!(feerate_vb > 0, "Spend feerate can't be null.");
932
933 let mut spent_vaults = Vec::with_capacity(outpoints.len());
935 let mut txins = Vec::with_capacity(outpoints.len());
937 let mut change_index = bip32::ChildNumber::from(0);
941 for outpoint in outpoints {
942 let vault = db_vault_by_deposit(db_file, outpoint)
943 .expect("Database must be available")
944 .ok_or_else(|| CommandError::UnknownOutpoint(*outpoint))?;
945 if matches!(vault.status, VaultStatus::Active) {
946 if vault.derivation_index > change_index {
947 change_index = vault.derivation_index;
948 }
949 txins.push((*outpoint, vault.amount, vault.derivation_index));
950 spent_vaults.push(vault);
951 } else {
952 return Err(CommandError::InvalidStatus(
953 vault.status,
954 VaultStatus::Active,
955 ));
956 }
957 }
958
959 let txos: Vec<SpendTxOut> = destinations
960 .iter()
961 .map(|(addr, value)| {
962 let script_pubkey = addr.script_pubkey();
963 SpendTxOut::new(TxOut {
964 value: *value,
965 script_pubkey,
966 })
967 })
968 .collect();
969
970 log::debug!(
971 "Creating a Spend transaction with deposit txins: '{:?}' and txos: '{:?}'",
972 &txins,
973 &txos
974 );
975
976 let nochange_tx = spend_tx_from_deposits(
979 txins.clone(),
980 txos.clone(),
981 None, &revaultd.deposit_descriptor,
983 &revaultd.unvault_descriptor,
984 &revaultd.cpfp_descriptor,
985 revaultd.lock_time,
986 false,
988 &revaultd.secp_ctx,
989 )
990 .map_err(|e| revault_tx::Error::from(e))?;
991
992 log::debug!(
993 "Spend tx without change: '{}'",
994 nochange_tx.as_psbt_string()
995 );
996
997 let nochange_feerate_vb = nochange_tx
1000 .max_feerate()
1001 .checked_mul(4)
1002 .expect("bug in feerate computation");
1003 if nochange_feerate_vb * 10 < feerate_vb * 9 {
1004 return Err(CommandError::SpendFeerateTooLow(
1005 feerate_vb,
1006 nochange_feerate_vb,
1007 ));
1008 }
1009
1010 const P2WSH_TXO_WEIGHT: u64 = 43 * 4;
1014 let with_change_weight = nochange_tx
1015 .max_weight()
1016 .checked_add(P2WSH_TXO_WEIGHT)
1017 .expect("weight computation bug");
1018 let cur_fees = nochange_tx.fees();
1019 let want_fees = with_change_weight
1020 .checked_mul(feerate_vb + 3)
1022 .map(|vbyte| Amount::from_sat(vbyte.checked_div(4).unwrap()));
1023 let change_value = want_fees.map(|f| cur_fees.checked_sub(f)).flatten();
1024 log::debug!(
1025 "Weight with change: '{}' -- Fees without change: '{}' -- Wanted feerate: '{}' \
1026 -- Wanted fees: '{:?}' -- Change value: '{:?}'",
1027 with_change_weight,
1028 cur_fees,
1029 feerate_vb,
1030 want_fees,
1031 change_value
1032 );
1033
1034 let change_txo = change_value.and_then(|change_value| {
1035 let cpfp_overhead = Amount::from_sat(16 * P2WSH_TXO_WEIGHT);
1038 let min_deposit =
1039 Amount::from_sat(revault_tx::transactions::DEPOSIT_MIN_SATS) + cpfp_overhead;
1040 if change_value > min_deposit {
1042 let change_txo = DepositTxOut::new(
1043 change_value - cpfp_overhead,
1045 &revaultd
1046 .deposit_descriptor
1047 .derive(change_index, &revaultd.secp_ctx),
1048 );
1049 log::debug!("Adding a change txo: '{:?}'", change_txo);
1050 Some(change_txo)
1051 } else {
1052 None
1053 }
1054 });
1055
1056 let tx_res = spend_tx_from_deposits(
1058 txins,
1059 txos,
1060 change_txo,
1061 &revaultd.deposit_descriptor,
1062 &revaultd.unvault_descriptor,
1063 &revaultd.cpfp_descriptor,
1064 revaultd.lock_time,
1065 true,
1066 &revaultd.secp_ctx,
1067 )
1068 .map_err(|e| revault_tx::Error::from(e))?;
1069
1070 if !check_spend_transaction_size(&revaultd, tx_res.clone()) {
1071 return Err(CommandError::SpendTooLarge);
1072 };
1073 log::debug!("Final Spend transaction: '{:?}'", tx_res);
1074
1075 Ok(spend_entry(
1076 &revaultd,
1077 tx_res,
1078 spent_vaults.iter(),
1079 ListSpendStatus::NonFinal,
1080 ))
1081 }
1082
1083 pub fn update_spend_tx(&self, spend_tx: SpendTransaction) -> Result<(), CommandError> {
1090 let revaultd = self.revaultd.read().unwrap();
1091 manager_only!(revaultd);
1092 let db_path = revaultd.db_file();
1093 let spend_txid = spend_tx.tx().txid();
1094
1095 let spend_inputs = &spend_tx.tx().input;
1097 let mut db_unvaults = Vec::with_capacity(spend_inputs.len());
1098 for txin in spend_inputs.iter() {
1099 let (db_vault, db_unvault) =
1100 db_vault_by_unvault_txid(&db_path, &txin.previous_output.txid)
1101 .expect("Database must be available")
1102 .ok_or_else(|| CommandError::SpendUnknownUnVault(txin.previous_output.txid))?;
1103
1104 if !matches!(db_vault.status, VaultStatus::Active) {
1105 return Err(CommandError::InvalidStatus(
1106 db_vault.status,
1107 VaultStatus::Active,
1108 ));
1109 }
1110
1111 db_unvaults.push(db_unvault);
1112 }
1113
1114 if db_spend_transaction(&db_path, &spend_txid)
1117 .expect("Database must be available")
1118 .is_some()
1119 {
1120 log::debug!("Updating Spend transaction '{}'", spend_txid);
1121 db_update_spend(&db_path, &spend_tx, false).expect("Database must be available");
1122 } else {
1123 log::debug!("Storing new Spend transaction '{}'", spend_txid);
1124 db_insert_spend(&db_path, &db_unvaults, &spend_tx).expect("Database must be available");
1125 }
1126
1127 Ok(())
1128 }
1129
1130 pub fn del_spend_tx(&self, spend_txid: &Txid) -> Result<(), CommandError> {
1136 let revaultd = self.revaultd.read().unwrap();
1137 manager_only!(revaultd);
1138 let db_path = revaultd.db_file();
1139 db_delete_spend(&db_path, spend_txid).expect("Database must be available");
1140 Ok(())
1141 }
1142
1143 pub fn list_spend_txs(
1150 &self,
1151 statuses: Option<&[ListSpendStatus]>,
1152 ) -> Result<Vec<ListSpendEntry>, CommandError> {
1153 let revaultd = self.revaultd.read().unwrap();
1154 manager_only!(revaultd);
1155 let db_path = revaultd.db_file();
1156
1157 let spend_tx_map = db_list_spends(&db_path).expect("Database must be available");
1158 let mut listspend_entries = Vec::with_capacity(spend_tx_map.len());
1159 for (_, (db_spend, _)) in spend_tx_map {
1160 let mut status = match db_spend.broadcasted {
1161 Some(true) => ListSpendStatus::Broadcasted,
1162 Some(false) => ListSpendStatus::Pending,
1163 None => ListSpendStatus::NonFinal,
1164 };
1165
1166 let spent_vaults = db_vaults_from_spend(&db_path, &db_spend.psbt.txid())
1167 .expect("Database must be available");
1168
1169 if let Some((_, vault)) = spent_vaults
1170 .iter()
1171 .find(|(_, v)| v.status == VaultStatus::Spent || v.status == VaultStatus::Canceled)
1172 {
1173 if vault.status == VaultStatus::Canceled
1176 || vault.final_txid != Some(db_spend.psbt.txid())
1177 {
1178 status = ListSpendStatus::Deprecated;
1179 } else {
1180 status = ListSpendStatus::Confirmed;
1181 }
1182 };
1183
1184 if let Some(s) = &statuses {
1186 if !s.contains(&status) {
1187 continue;
1188 }
1189 }
1190
1191 listspend_entries.push(spend_entry(
1192 &revaultd,
1193 db_spend.psbt,
1194 spent_vaults.values(),
1195 status,
1196 ));
1197 }
1198
1199 Ok(listspend_entries)
1200 }
1201
1202 pub fn set_spend_tx(&self, spend_txid: &Txid, priority: bool) -> Result<(), CommandError> {
1214 let revaultd = self.revaultd.read().unwrap();
1215 manager_only!(revaultd);
1216 let db_path = revaultd.db_file();
1217
1218 if priority && revaultd.cpfp_key.is_none() {
1219 return Err(CommandError::MissingCpfpKey);
1220 }
1221
1222 let mut spend_tx = db_spend_transaction(&db_path, &spend_txid)
1224 .expect("Database must be available")
1225 .ok_or_else(|| CommandError::UnknownSpend(*spend_txid))?;
1226 let spent_vaults =
1227 db_vaults_from_spend(&db_path, &spend_txid).expect("Database must be available");
1228 let tx = &spend_tx.psbt.tx();
1229 if spent_vaults.len() < tx.input.len() {
1230 return Err(CommandError::SpendSpent(*spend_txid));
1231 }
1232
1233 let signatures: Vec<BTreeMap<BitcoinPubKey, Vec<u8>>> = spend_tx
1237 .psbt
1238 .psbt()
1239 .inputs
1240 .iter()
1241 .map(|i| i.partial_sigs.clone())
1242 .collect();
1243 let mans_thresh = revaultd.managers_threshold();
1244 for (i, sigmap) in signatures.iter().enumerate() {
1245 if sigmap.len() < mans_thresh {
1246 return Err(CommandError::SpendNotEnoughSig(sigmap.len(), mans_thresh));
1247 }
1248 for (pubkey, raw_sig) in sigmap {
1249 let sig = secp256k1::Signature::from_der(&raw_sig[..raw_sig.len() - 1])
1250 .map_err(|_| CommandError::SpendInvalidSig(raw_sig.clone()))?;
1251 spend_tx
1252 .psbt
1253 .add_signature(i, pubkey.key, sig, &revaultd.secp_ctx)
1254 .map_err(|_| CommandError::SpendInvalidSig(raw_sig.clone()))?
1255 .expect("The signature was already there");
1256 }
1257 }
1258
1259 if !check_spend_transaction_size(&revaultd, spend_tx.psbt.clone()) {
1262 return Err(CommandError::SpendTooLarge);
1263 };
1264
1265 let cosigs = revaultd.cosigs.as_ref().expect("We are manager");
1268 if !cosigs.is_empty() {
1269 log::debug!("Fetching signatures from Cosigning servers");
1270 fetch_cosigs_signatures(
1271 &revaultd.secp_ctx,
1272 &revaultd.noise_secret,
1273 &mut spend_tx.psbt,
1274 cosigs,
1275 )?;
1276 }
1277 let mut finalized_spend = spend_tx.psbt.clone();
1278 finalized_spend.finalize(&revaultd.secp_ctx)?;
1279
1280 let deposit_outpoints: Vec<_> = spent_vaults
1282 .values()
1283 .map(|db_vault| db_vault.deposit_outpoint)
1284 .collect();
1285 announce_spend_transaction(
1286 revaultd.coordinator_host,
1287 &revaultd.noise_secret,
1288 &revaultd.coordinator_noisekey,
1289 finalized_spend,
1290 deposit_outpoints,
1291 )?;
1292 db_update_spend(&db_path, &spend_tx.psbt, priority).expect("Database must be available");
1293
1294 log::debug!(
1297 "Broadcasting Unvault transactions with ids '{:?}'",
1298 spent_vaults.keys()
1299 );
1300 let bitcoin_txs = spent_vaults
1301 .values()
1302 .into_iter()
1303 .map(|db_vault| {
1304 let mut unvault_tx = db_unvault_transaction(&db_path, db_vault.id)
1305 .expect("Database must be available")
1306 .ok_or(CommandError::Race)?
1307 .psbt
1308 .assert_unvault();
1309 unvault_tx.finalize(&revaultd.secp_ctx)?;
1310 Ok(unvault_tx.into_psbt().extract_tx())
1311 })
1312 .collect::<Result<Vec<BitcoinTransaction>, CommandError>>()?;
1313 self.bitcoind_conn.broadcast(bitcoin_txs)?;
1314 db_mark_broadcastable_spend(&db_path, spend_txid).expect("Database must be available");
1315
1316 Ok(())
1317 }
1318
1319 pub fn revault(&self, deposit_outpoint: OutPoint) -> Result<(), CommandError> {
1326 let revaultd = self.revaultd.read().unwrap();
1327 let db_path = revaultd.db_file();
1328
1329 let vault = db_vault_by_deposit(&db_path, &deposit_outpoint)
1332 .expect("Database must be accessible")
1333 .ok_or_else(|| CommandError::UnknownOutpoint(deposit_outpoint))?;
1334
1335 if !matches!(
1336 vault.status,
1337 VaultStatus::Unvaulting | VaultStatus::Unvaulted | VaultStatus::Spending
1338 ) {
1339 return Err(CommandError::InvalidStatus(
1340 vault.status,
1341 VaultStatus::Unvaulting,
1342 ));
1343 }
1344
1345 let (_, cancel_batch) = transaction_chain_manager(
1349 deposit_outpoint,
1350 vault.amount,
1351 &revaultd.deposit_descriptor,
1352 &revaultd.unvault_descriptor,
1353 &revaultd.cpfp_descriptor,
1354 vault.derivation_index,
1355 &revaultd.secp_ctx,
1356 )
1357 .expect("We wouldn't have put a vault with an invalid chain in DB");
1358 let mut cancel_tx =
1360 db_cancel_transaction_by_txid(&db_path, &cancel_batch.feerate_20().txid())
1361 .expect("Database must be available")
1362 .ok_or(CommandError::Race)?
1363 .psbt
1364 .assert_cancel();
1365
1366 cancel_tx.finalize(&revaultd.secp_ctx)?;
1367 let transaction = cancel_tx.into_psbt().extract_tx();
1368 log::debug!(
1369 "Broadcasting Cancel transactions with id '{:?}'",
1370 transaction.txid()
1371 );
1372 self.bitcoind_conn.broadcast(vec![transaction])?;
1373
1374 Ok(())
1375 }
1376
1377 pub fn emergency(&self) -> Result<(), CommandError> {
1382 let revaultd = self.revaultd.read().unwrap();
1383 stakeholder_only!(revaultd);
1384
1385 let emers = finalized_emer_txs(&revaultd)?;
1390 self.bitcoind_conn.broadcast(emers)?;
1391
1392 Ok(())
1393 }
1394
1395 pub fn get_servers_statuses(&self) -> ServersStatuses {
1397 let revaultd = self.revaultd.read().unwrap();
1398 let coordinator = coordinator_status(&revaultd);
1399 let cosigners = cosigners_status(&revaultd);
1400 let watchtowers = watchtowers_status(&revaultd);
1401
1402 ServersStatuses {
1403 coordinator,
1404 cosigners,
1405 watchtowers,
1406 }
1407 }
1408
1409 pub fn get_history(
1415 &self,
1416 start: u32,
1417 end: u32,
1418 limit: u64,
1419 kind: &[HistoryEventKind],
1420 ) -> Result<Vec<HistoryEvent>, CommandError> {
1421 let revaultd = self.revaultd.read().unwrap();
1422 gethistory(&revaultd, &self.bitcoind_conn, start, end, limit, kind)
1423 }
1424}
1425
1426#[derive(Debug, Clone, Serialize, Deserialize)]
1428pub struct GetInfoDescriptors {
1429 #[serde(serialize_with = "ser_to_string", deserialize_with = "deser_from_str")]
1430 pub deposit: DepositDescriptor,
1431 #[serde(serialize_with = "ser_to_string", deserialize_with = "deser_from_str")]
1432 pub unvault: UnvaultDescriptor,
1433 #[serde(serialize_with = "ser_to_string", deserialize_with = "deser_from_str")]
1434 pub cpfp: CpfpDescriptor,
1435}
1436
1437#[derive(Debug, Clone, Serialize, Deserialize)]
1439pub struct GetInfoResult {
1440 pub version: String,
1441 pub network: Network,
1442 pub blockheight: i32,
1443 pub sync: f64,
1444 pub vaults: usize,
1445 pub managers_threshold: usize,
1446 pub descriptors: GetInfoDescriptors,
1447 #[serde(serialize_with = "ser_to_string", deserialize_with = "deser_from_str")]
1448 pub participant_type: UserRole,
1449}
1450
1451#[derive(Debug, Clone, Serialize, Deserialize)]
1453pub struct ListVaultsEntry {
1454 #[serde(
1455 serialize_with = "ser_amount",
1456 deserialize_with = "deser_amount_from_sats"
1457 )]
1458 pub amount: Amount,
1459 pub blockheight: Option<u32>,
1460 #[serde(serialize_with = "ser_to_string", deserialize_with = "deser_from_str")]
1461 pub status: VaultStatus,
1462 pub txid: Txid,
1463 pub vout: u32,
1464 pub derivation_index: bip32::ChildNumber,
1465 pub address: Address,
1466 pub funded_at: Option<u32>,
1467 pub secured_at: Option<u32>,
1468 pub delegated_at: Option<u32>,
1469 pub moved_at: Option<u32>,
1470}
1471
1472#[derive(Debug, Clone, Serialize, Deserialize)]
1474pub struct RevocationTransactions {
1475 pub cancel_txs: [CancelTransaction; 5],
1476 pub emergency_tx: EmergencyTransaction,
1477 pub emergency_unvault_tx: UnvaultEmergencyTransaction,
1479}
1480
1481#[derive(Debug, Clone, Serialize, Deserialize)]
1483pub struct ListPresignedTxEntry {
1484 pub vault_outpoint: OutPoint,
1485 pub unvault: UnvaultTransaction,
1486 pub cancel: [CancelTransaction; 5],
1487 pub emergency: Option<EmergencyTransaction>,
1489 pub unvault_emergency: Option<UnvaultEmergencyTransaction>,
1491}
1492
1493#[derive(Debug, Clone, Serialize, Deserialize)]
1495pub struct ListOnchainTxEntry {
1496 pub vault_outpoint: OutPoint,
1497 pub deposit: WalletTransaction,
1498 pub unvault: Option<WalletTransaction>,
1499 pub cancel: Option<WalletTransaction>,
1500 pub emergency: Option<WalletTransaction>,
1502 pub unvault_emergency: Option<WalletTransaction>,
1504 pub spend: Option<WalletTransaction>,
1505}
1506
1507#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1509#[serde(rename_all = "snake_case")]
1510pub enum ListSpendStatus {
1511 NonFinal,
1512 Pending,
1513 Broadcasted,
1514 Confirmed,
1516 Deprecated,
1518}
1519
1520#[derive(Debug, Clone, Serialize, Deserialize)]
1522pub struct ListSpendEntry {
1523 pub deposit_outpoints: Vec<OutPoint>,
1524 #[serde(
1525 serialize_with = "ser_amount",
1526 deserialize_with = "deser_amount_from_sats"
1527 )]
1528 pub deposit_amount: Amount,
1529 #[serde(
1531 serialize_with = "ser_amount",
1532 deserialize_with = "deser_amount_from_sats"
1533 )]
1534 pub cpfp_amount: Amount,
1535 pub psbt: SpendTransaction,
1536 pub cpfp_index: usize,
1537 pub change_index: Option<usize>,
1538 pub status: ListSpendStatus,
1539}
1540
1541#[derive(Debug, Clone, Serialize, Deserialize)]
1543pub struct ServersStatuses {
1544 pub coordinator: ServerStatus,
1545 pub cosigners: Vec<ServerStatus>,
1546 pub watchtowers: Vec<ServerStatus>,
1547}
1548
1549#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1551pub enum HistoryEventKind {
1552 #[serde(rename = "cancel")]
1553 Cancel,
1554 #[serde(rename = "deposit")]
1555 Deposit,
1556 #[serde(rename = "spend")]
1557 Spend,
1558}
1559
1560impl fmt::Display for HistoryEventKind {
1561 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1562 match self {
1563 Self::Cancel => write!(f, "Cancel"),
1564 Self::Deposit => write!(f, "Deposit"),
1565 Self::Spend => write!(f, "Spend"),
1566 }
1567 }
1568}
1569
1570#[derive(Debug, Clone, Serialize, Deserialize)]
1572pub struct HistoryEvent {
1573 pub kind: HistoryEventKind,
1574 pub date: u32,
1575 pub blockheight: u32,
1576 pub amount: Option<u64>,
1577 pub cpfp_amount: Option<u64>,
1578 pub miner_fee: Option<u64>,
1579 pub txid: Txid,
1580 pub vaults: Vec<OutPoint>,
1581}