bitcoin_harness/
wallet.rs1use 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#[derive(Debug)]
13pub struct Wallet {
14 name: String,
15 pub client: Client,
16}
17
18impl Wallet {
19 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 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 #[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 if output.script_pubkey != joined_address.clone().script_pubkey() {
239 outputs.push(output.clone());
240 }
241 });
242 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 sleep(Duration::from_secs(2)).await;
308
309 let _res = wallet.transaction_block_height(txid).await.unwrap();
310 }
311}