revaultd 0.4.0

Revault wallet daemon
Documentation
#[cfg(test)]
pub mod test_utils {
    use crate::config::Config;
    use crate::{
        bitcoind::{interface::WalletTransaction, BitcoindError},
        database::interface::db_exec,
        revaultd::{RevaultD, UserRole, VaultStatus},
        threadmessages::{
            BitcoindMessageOut, BitcoindSender, BitcoindThread, SigFetcherMessageOut,
        },
        DaemonControl,
    };
    use revault_tx::bitcoin::{
        util::bip32::ChildNumber, Amount, OutPoint, Transaction as BitcoinTransaction, Txid,
    };

    use std::{
        collections::HashMap,
        fs,
        path::{Path, PathBuf},
        sync::{mpsc, Arc, RwLock},
        thread,
    };

    use rusqlite::params;

    pub fn test_datadir() -> PathBuf {
        static mut COUNTER: u64 = 0;
        unsafe {
            COUNTER += 1;
            format!("scratch_test_{:?}-{}", std::thread::current().id(), COUNTER).into()
        }
    }

    // Create a RevaultD state instance using a scratch data directory, trying to be portable
    // across UNIX, MacOS, and Windows
    pub fn dummy_revaultd(datadir: PathBuf, role: UserRole) -> RevaultD {
        // TODO directly create RevaultD instead of using conf strings
        let stake_config = r#"
[stakeholder_config]
xpub = "xpub6EKrK11LwLcNyJ4arJnCtxPGAuxSYPX35fMfJcmadvTSue6YZn2W9kEUHy7PFyQsy7zkrbmhxtevsgwsfyCiRBayJdWSTohRQua43jMw9FQ"
watchtowers = [ { host = "127.0.0.1:1", noise_key = "46084f8a7da40ef7ffc38efa5af8a33a742b90f920885d17c533bb2a0b680cb3" } ]
emergency_address = "bcrt1qewc2348370pgw8kjz8gy09z8xyh0d9fxde6nzamd3txc9gkmjqmq8m4cdq"
"#;

        let man_config = r#"
[manager_config]
xpub = "xpub6De9kLSb2vvujzLqBbQvcnfaNfCGv8vBKNikWsvu3yDL6CpdNEE5CKH9J6TpT6ARFsiAdTpH7iJdA8tgAwNeo46FH9CSTv6CURBJSCPAeUu"
cosigners = [ { host = "127.0.0.1:1", noise_key = "087629614d227ff2b9ed5f2ce2eb7cd527d2d18f866b24009647251fce58de38" } ]
"#;

        let mut config = r#"
log_level = "debug"

coordinator_host = "127.0.0.1:1"
coordinator_noise_key = "d91563973102454a7830137e92d0548bc83b4ea2799f1df04622ca1307381402"

[scripts_config]
cpfp_descriptor = "wsh(thresh(1,pk(xpub6BhQvtXJmw6hi2ALFeWMi9m7G8rGterJnMTNRqUm29uvB6dVTELvnEs7hfxyN3JM48FR2oh4t8chsvw7bRRRukkyhqp9WZD4oB9UvxAMpqC/*)))#c6th4le3"
deposit_descriptor = "wsh(multi(2,xpub6EKrK11LwLcNyJ4arJnCtxPGAuxSYPX35fMfJcmadvTSue6YZn2W9kEUHy7PFyQsy7zkrbmhxtevsgwsfyCiRBayJdWSTohRQua43jMw9FQ/*,xpub6BhQvtXJmw6hksh9rRRfdLjaWjQiNMZWtkM5ebn8QkAgh5na2Un6mCDABwkUmHhPCMYtsM9zHY5jxbQ86ayvjfY8XtavbovB6NcNy8KyQLa/*))#wu049esu"
unvault_descriptor = "wsh(andor(thresh(1,pk(xpub6De9kLSb2vvujzLqBbQvcnfaNfCGv8vBKNikWsvu3yDL6CpdNEE5CKH9J6TpT6ARFsiAdTpH7iJdA8tgAwNeo46FH9CSTv6CURBJSCPAeUu/*)),and_v(v:multi(2,0332e5c86d0938a83ed80d13c5644ec92fd16b9d7184bb35d6ead8227b4ad47803,02807e5c4f7b228aa9f1aef77effde768356480f5268376644464e714d0205eb1c),older(6)),thresh(2,pkh(xpub6EKrK11LwLcNyJ4arJnCtxPGAuxSYPX35fMfJcmadvTSue6YZn2W9kEUHy7PFyQsy7zkrbmhxtevsgwsfyCiRBayJdWSTohRQua43jMw9FQ/*),a:pkh(xpub6BhQvtXJmw6hksh9rRRfdLjaWjQiNMZWtkM5ebn8QkAgh5na2Un6mCDABwkUmHhPCMYtsM9zHY5jxbQ86ayvjfY8XtavbovB6NcNy8KyQLa/*))))#ef5vfw3p"

[bitcoind_config]
network = "regtest"
cookie_path = "/home/user/.bitcoin/.cookie"
addr = "127.0.0.1:8332"
"#.to_string();

        match role {
            UserRole::Stakeholder => config += stake_config,
            UserRole::Manager => config += man_config,
            UserRole::StakeholderManager => {
                config += stake_config;
                config += man_config;
            }
        };

        // Just in case there is a leftover from a previous run
        fs::remove_dir_all(&datadir).unwrap_or_else(|_| ());

        let mut config: Config = toml::from_str(&config).expect("Parsing valid config file");
        config.data_dir = Some(datadir);
        RevaultD::from_config(config).expect("Creating state from config")
    }

    // Get a dummy handle for the RPC calls.
    // FIXME: we could do something cleaner at some point
    pub fn dummy_rpcutil(datadir: PathBuf, role: UserRole) -> DaemonControl {
        let revaultd = Arc::from(RwLock::from(dummy_revaultd(datadir, role)));

        let (bitcoind_tx, bitcoind_rx) = mpsc::channel();
        let (sigfetcher_tx, sigfetcher_rx) = mpsc::channel();

        let _ = Arc::from(RwLock::from(thread::spawn(move || {
            for msg in bitcoind_rx {
                match msg {
                    BitcoindMessageOut::Shutdown => return,
                    _ => unreachable!(),
                }
            }
        })));
        let _ = Arc::from(RwLock::from(thread::spawn(move || {
            for msg in sigfetcher_rx {
                match msg {
                    SigFetcherMessageOut::Shutdown => return,
                }
            }
        })));

        DaemonControl {
            revaultd,
            bitcoind_conn: BitcoindSender::from(bitcoind_tx),
            sigfetcher_conn: sigfetcher_tx.into(),
        }
    }

    /// Insert a new vault in the database
    #[allow(clippy::too_many_arguments)]
    pub fn insert_vault_in_db(
        db_path: &Path,
        wallet_id: u32,
        deposit_outpoint: &OutPoint,
        amount: &Amount,
        blockheight: u32,
        derivation_index: ChildNumber,
        funded_at: Option<u32>,
        moved_at: Option<u32>,
        status: VaultStatus,
        final_txid: Option<&Txid>,
    ) {
        db_exec(db_path, |tx| {
            let derivation_index: u32 = derivation_index.into();
            tx.execute(
            "INSERT INTO vaults ( \
                wallet_id, status, deposit_blockheight, deposit_txid, deposit_vout, amount, derivation_index, \
                funded_at, moved_at, final_txid \
            ) \
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
            params![
                wallet_id,
                status as u32,
                blockheight,
                deposit_outpoint.txid.to_vec(),
                deposit_outpoint.vout,
                amount.as_sat() as i64,
                derivation_index,
                funded_at,
                moved_at,
                final_txid.map(|txid| txid.to_vec())
            ],
        )
        .expect("Must not fail to insert vault in a test database");

            Ok(())
        }).unwrap()
    }

    /// MockBitcoindThread implements the BitcoindThread trait as a mock backend.
    pub struct MockBitcoindThread {
        txs: HashMap<Txid, WalletTransaction>,
    }

    impl MockBitcoindThread {
        pub fn new(txs: HashMap<Txid, WalletTransaction>) -> Self {
            Self { txs }
        }
    }

    impl BitcoindThread for MockBitcoindThread {
        fn wallet_tx(&self, txid: Txid) -> Result<Option<WalletTransaction>, BitcoindError> {
            let tx = self.txs.get(&txid).map(|tx| (*tx).clone());
            Ok(tx)
        }
        fn broadcast(&self, _transactions: Vec<BitcoinTransaction>) -> Result<(), BitcoindError> {
            Ok(())
        }
        fn shutdown(&self) {}
        fn sync_progress(&self) -> f64 {
            1.0
        }
    }
}