bitcoin_harness/
wallet.rs

1use crate::bitcoind_rpc::{Client, Result};
2use crate::bitcoind_rpc_api::{BitcoindRpcApi, PsbtBase64, WalletProcessPsbtResponse};
3use bitcoin::hashes::hex::FromHex;
4use bitcoin::{Address, Amount, Transaction, Txid};
5use bitcoincore_rpc_json::{
6    FinalizePsbtResult, GetAddressInfoResult, GetTransactionResult, GetWalletInfoResult,
7    ListUnspentResultEntry,
8};
9use url::Url;
10
11/// A wrapper to bitcoind wallet
12#[derive(Debug)]
13pub struct Wallet {
14    name: String,
15    pub client: Client,
16}
17
18impl Wallet {
19    /// Create a wallet on the bitcoind instance or use the wallet with the same name
20    /// if it exists.
21    pub async fn new(name: &str, url: Url) -> Result<Self> {
22        let client = Client::new(url);
23
24        let wallet = Self {
25            name: name.to_string(),
26            client,
27        };
28
29        wallet.init().await?;
30
31        Ok(wallet)
32    }
33
34    async fn init(&self) -> Result<()> {
35        match self.info().await {
36            Err(_) => {
37                self.client
38                    .createwallet(&self.name, None, None, None, None)
39                    .await?;
40                Ok(())
41            }
42            Ok(_) => Ok(()),
43        }
44    }
45
46    pub async fn info(&self) -> Result<GetWalletInfoResult> {
47        Ok(self.client.with_wallet(&self.name)?.getwalletinfo().await?)
48    }
49
50    pub async fn median_time(&self) -> Result<u64> {
51        self.client.median_time().await
52    }
53
54    #[deprecated(
55        since = "0.3.0",
56        note = "please directly use `client.getblockcount` instead"
57    )]
58    #[allow(clippy::cast_possible_truncation)]
59    // It is going to be fine for a while and this method is deprecated
60    pub async fn block_height(&self) -> Result<u32> {
61        Ok(self.client.getblockcount().await?)
62    }
63
64    pub async fn new_address(&self) -> Result<Address> {
65        Ok(self
66            .client
67            .with_wallet(&self.name)?
68            .getnewaddress(None, Some("bech32".into()))
69            .await?)
70    }
71
72    pub async fn balance(&self) -> Result<Amount> {
73        let response = self
74            .client
75            .with_wallet(&self.name)?
76            .getbalance(None, None, None)
77            .await?;
78        let amount = Amount::from_btc(response)?;
79        Ok(amount)
80    }
81
82    pub async fn send_to_address(&self, address: Address, amount: Amount) -> Result<Txid> {
83        let txid = self
84            .client
85            .with_wallet(&self.name)?
86            .sendtoaddress(address, amount.to_btc())
87            .await?;
88        let txid = Txid::from_hex(&txid)?;
89
90        Ok(txid)
91    }
92
93    pub async fn send_raw_transaction(&self, transaction: Transaction) -> Result<Txid> {
94        let txid = self
95            .client
96            .with_wallet(&self.name)?
97            .sendrawtransaction(transaction.into())
98            .await?;
99        let txid = Txid::from_hex(&txid)?;
100        Ok(txid)
101    }
102
103    pub async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
104        self.client.get_raw_transaction(txid).await
105    }
106
107    pub async fn get_wallet_transaction(&self, txid: Txid) -> Result<GetTransactionResult> {
108        let res = self
109            .client
110            .with_wallet(&self.name)?
111            .gettransaction(txid)
112            .await?;
113
114        Ok(res)
115    }
116
117    pub async fn address_info(&self, address: &Address) -> Result<GetAddressInfoResult> {
118        self.client.address_info(&self.name, address).await
119    }
120
121    pub async fn list_unspent(&self) -> Result<Vec<ListUnspentResultEntry>> {
122        let unspents = self
123            .client
124            .with_wallet(&self.name)?
125            .listunspent(None, None, None, None)
126            .await?;
127        Ok(unspents)
128    }
129
130    pub async fn fund_psbt(&self, address: Address, amount: Amount) -> Result<String> {
131        self.client
132            .fund_psbt(&self.name, &[], address, amount)
133            .await
134    }
135
136    pub async fn join_psbts(&self, psbts: &[String]) -> Result<PsbtBase64> {
137        self.client.join_psbts(&self.name, psbts).await
138    }
139
140    pub async fn wallet_process_psbt(&self, psbt: PsbtBase64) -> Result<WalletProcessPsbtResponse> {
141        self.client.wallet_process_psbt(&self.name, psbt).await
142    }
143
144    pub async fn finalize_psbt(&self, psbt: PsbtBase64) -> Result<FinalizePsbtResult> {
145        self.client.finalize_psbt(&self.name, psbt).await
146    }
147
148    pub async fn transaction_block_height(&self, txid: Txid) -> Result<Option<u32>> {
149        let res = self.client.get_raw_transaction_verbose(txid).await?;
150
151        let block_hash = match res.blockhash {
152            Some(block_hash) => block_hash,
153            None => return Ok(None),
154        };
155
156        let res = self.client.getblock(&block_hash).await?;
157
158        // Won't be an issue for the next 800 centuries
159        #[allow(clippy::cast_possible_truncation)]
160        Ok(Some(res.height as u32))
161    }
162}
163
164#[cfg(test)]
165mod test {
166    use std::time::Duration;
167
168    use crate::{Bitcoind, Wallet};
169    use bitcoin::util::psbt::PartiallySignedTransaction;
170    use bitcoin::{Amount, Transaction, TxOut};
171    use tokio::time::sleep;
172
173    #[tokio::test]
174    async fn get_wallet_transaction() {
175        let tc_client = testcontainers::clients::Cli::default();
176        let bitcoind = Bitcoind::new(&tc_client).unwrap();
177        bitcoind.init(5).await.unwrap();
178
179        let wallet = Wallet::new("wallet", bitcoind.node_url.clone())
180            .await
181            .unwrap();
182        let mint_address = wallet.new_address().await.unwrap();
183        let mint_amount = bitcoin::Amount::from_btc(3.0).unwrap();
184        bitcoind.mint(mint_address, mint_amount).await.unwrap();
185
186        let pay_address = wallet.new_address().await.unwrap();
187        let pay_amount = bitcoin::Amount::from_btc(1.0).unwrap();
188        let txid = wallet
189            .send_to_address(pay_address, pay_amount)
190            .await
191            .unwrap();
192
193        let _res = wallet.get_wallet_transaction(txid).await.unwrap();
194    }
195
196    #[tokio::test]
197    async fn two_party_psbt_test() {
198        let tc_client = testcontainers::clients::Cli::default();
199        let bitcoind = Bitcoind::new(&tc_client).unwrap();
200        bitcoind.init(5).await.unwrap();
201
202        let alice = Wallet::new("alice", bitcoind.node_url.clone())
203            .await
204            .unwrap();
205        let address = alice.new_address().await.unwrap();
206        let amount = bitcoin::Amount::from_btc(3.0).unwrap();
207        bitcoind.mint(address, amount).await.unwrap();
208        let joined_address = alice.new_address().await.unwrap();
209        let alice_result = alice
210            .fund_psbt(joined_address.clone(), Amount::from_btc(1.0).unwrap())
211            .await
212            .unwrap();
213
214        let bob = Wallet::new("bob", bitcoind.node_url.clone()).await.unwrap();
215        let address = bob.new_address().await.unwrap();
216        let amount = bitcoin::Amount::from_btc(3.0).unwrap();
217        bitcoind.mint(address, amount).await.unwrap();
218        let bob_psbt = bob
219            .fund_psbt(joined_address.clone(), Amount::from_btc(1.0).unwrap())
220            .await
221            .unwrap();
222
223        let joined_psbts = alice
224            .join_psbts(&[alice_result.clone(), bob_psbt.clone()])
225            .await
226            .unwrap();
227
228        let partial_signed_bitcoin_transaction: PartiallySignedTransaction = {
229            let as_hex = base64::decode(joined_psbts.0).unwrap();
230            bitcoin::consensus::deserialize(&as_hex).unwrap()
231        };
232
233        let transaction = partial_signed_bitcoin_transaction.extract_tx();
234        let mut outputs = vec![];
235
236        transaction.output.iter().for_each(|output| {
237            // filter out shared output
238            if output.script_pubkey != joined_address.clone().script_pubkey() {
239                outputs.push(output.clone());
240            }
241        });
242        // add shared output with twice the btc to fit change addresses
243        outputs.push(TxOut {
244            value: Amount::from_btc(2.0).unwrap().to_sat(),
245            script_pubkey: joined_address.clone().script_pubkey(),
246        });
247
248        let transaction = Transaction {
249            output: outputs,
250            ..transaction
251        };
252
253        assert_eq!(
254            transaction.input.len(),
255            2,
256            "We expect 2 inputs, one from alice, one from bob"
257        );
258        assert_eq!(
259            transaction.output.len(),
260            3,
261            "We expect 3 outputs, change for alice, change for bob and shared address"
262        );
263
264        let psbt = {
265            let partial_signed_bitcoin_transaction =
266                PartiallySignedTransaction::from_unsigned_tx(transaction).unwrap();
267            let hex_vec = bitcoin::consensus::serialize(&partial_signed_bitcoin_transaction);
268            base64::encode(hex_vec).into()
269        };
270
271        let alice_signed_psbt = alice.wallet_process_psbt(psbt).await.unwrap();
272        let bob_signed_psbt = bob
273            .wallet_process_psbt(alice_signed_psbt.into())
274            .await
275            .unwrap();
276
277        let alice_finalized_psbt = alice.finalize_psbt(bob_signed_psbt.into()).await.unwrap();
278
279        let transaction = alice_finalized_psbt.transaction().unwrap().unwrap();
280        let txid = alice.send_raw_transaction(transaction).await.unwrap();
281        println!("Final tx_id: {:?}", txid);
282    }
283
284    #[tokio::test]
285    async fn transaction_block_height() {
286        let tc_client = testcontainers::clients::Cli::default();
287        let bitcoind = Bitcoind::new(&tc_client).unwrap();
288        bitcoind.init(5).await.unwrap();
289
290        let wallet = Wallet::new("wallet", bitcoind.node_url.clone())
291            .await
292            .unwrap();
293        let mint_address = wallet.new_address().await.unwrap();
294        let mint_amount = bitcoin::Amount::from_btc(3.0).unwrap();
295        bitcoind.mint(mint_address, mint_amount).await.unwrap();
296
297        let pay_address = wallet.new_address().await.unwrap();
298        let pay_amount = bitcoin::Amount::from_btc(1.0).unwrap();
299        let txid = wallet
300            .send_to_address(pay_address, pay_amount)
301            .await
302            .unwrap();
303
304        // wait for the transaction to be included in a block, so that
305        // it has a block height field assigned to it when calling
306        // `getrawtransaction`
307        sleep(Duration::from_secs(2)).await;
308
309        let _res = wallet.transaction_block_height(txid).await.unwrap();
310    }
311}