-
Notifications
You must be signed in to change notification settings - Fork 39
/
mod.rs
154 lines (136 loc) · 5.81 KB
/
mod.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
use std::collections::HashMap;
use std::str::FromStr;
use anyhow::{anyhow, Context, Result};
use bitcoincore_rpc::bitcoin::Amount;
use bitcoincore_rpc::RpcApi;
use payjoin::bitcoin::psbt::Psbt;
use payjoin::send::RequestContext;
use payjoin::{bitcoin, PjUri};
pub mod config;
use crate::app::config::AppConfig;
#[cfg(all(not(feature = "v2"), feature = "v1"))]
pub(crate) mod v1;
#[cfg(feature = "v2")]
pub(crate) mod v2;
#[cfg(feature = "danger-local-https")]
pub const LOCAL_CERT_FILE: &str = "localhost.der";
#[async_trait::async_trait]
pub trait App {
fn new(config: AppConfig) -> Result<Self>
where
Self: Sized;
fn bitcoind(&self) -> Result<bitcoincore_rpc::Client>;
async fn send_payjoin(&self, bip21: &str, fee_rate: &f32) -> Result<()>;
async fn receive_payjoin(self, amount_arg: &str) -> Result<()>;
fn create_pj_request(&self, uri: &PjUri, fee_rate: &f32) -> Result<RequestContext> {
let amount = uri.amount.ok_or_else(|| anyhow!("please specify the amount in the Uri"))?;
// wallet_create_funded_psbt requires a HashMap<address: String, Amount>
let mut outputs = HashMap::with_capacity(1);
outputs.insert(uri.address.to_string(), amount);
let fee_rate_sat_per_kwu = fee_rate * 250.0_f32;
let fee_rate: bitcoin::FeeRate =
bitcoin::FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu.ceil() as u64);
let fee_sat_per_kvb =
fee_rate.to_sat_per_kwu().checked_mul(4).ok_or(anyhow!("Invalid fee rate"))?;
let fee_per_kvb = Amount::from_sat(fee_sat_per_kvb);
log::debug!("Fee rate sat/kvb: {}", fee_per_kvb.display_in(bitcoin::Denomination::Satoshi));
let options = bitcoincore_rpc::json::WalletCreateFundedPsbtOptions {
lock_unspent: Some(true),
fee_rate: Some(fee_per_kvb),
..Default::default()
};
let psbt = self
.bitcoind()?
.wallet_create_funded_psbt(
&[], // inputs
&outputs,
None, // locktime
Some(options),
None,
)
.context("Failed to create PSBT")?
.psbt;
let psbt = self
.bitcoind()?
.wallet_process_psbt(&psbt, None, None, None)
.with_context(|| "Failed to process PSBT")?
.psbt;
let psbt = Psbt::from_str(&psbt).with_context(|| "Failed to load PSBT from base64")?;
log::debug!("Original psbt: {:#?}", psbt);
let req_ctx = payjoin::send::RequestBuilder::from_psbt_and_uri(psbt, uri.clone())
.with_context(|| "Failed to build payjoin request")?
.build_recommended(fee_rate)
.with_context(|| "Failed to build payjoin request")?;
Ok(req_ctx)
}
fn process_pj_response(&self, psbt: Psbt) -> Result<bitcoin::Txid> {
log::debug!("Proposed psbt: {:#?}", psbt);
let psbt = self
.bitcoind()?
.wallet_process_psbt(&psbt.to_string(), None, None, None)
.with_context(|| "Failed to process PSBT")?
.psbt;
let tx = self
.bitcoind()?
.finalize_psbt(&psbt, Some(true))
.with_context(|| "Failed to finalize PSBT")?
.hex
.ok_or_else(|| anyhow!("Incomplete PSBT"))?;
let txid = self
.bitcoind()?
.send_raw_transaction(&tx)
.with_context(|| "Failed to send raw transaction")?;
println!("Payjoin sent. TXID: {}", txid);
Ok(txid)
}
}
fn try_contributing_inputs(
payjoin: &mut payjoin::receive::ProvisionalProposal,
bitcoind: &bitcoincore_rpc::Client,
) -> Result<()> {
use bitcoin::OutPoint;
let available_inputs = bitcoind
.list_unspent(None, None, None, None, None)
.context("Failed to list unspent from bitcoind")?;
let candidate_inputs: HashMap<Amount, OutPoint> = available_inputs
.iter()
.map(|i| (i.amount, OutPoint { txid: i.txid, vout: i.vout }))
.collect();
let selected_outpoint = payjoin.try_preserving_privacy(candidate_inputs).expect("gg");
let selected_utxo = available_inputs
.iter()
.find(|i| i.txid == selected_outpoint.txid && i.vout == selected_outpoint.vout)
.context("This shouldn't happen. Failed to retrieve the privacy preserving utxo from those we provided to the seclector.")?;
log::debug!("selected utxo: {:#?}", selected_utxo);
// calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt,
let txo_to_contribute = bitcoin::TxOut {
value: selected_utxo.amount,
script_pubkey: selected_utxo.script_pub_key.clone(),
};
let outpoint_to_contribute =
bitcoin::OutPoint { txid: selected_utxo.txid, vout: selected_utxo.vout };
payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute);
Ok(())
}
#[cfg(feature = "danger-local-https")]
fn http_agent() -> Result<reqwest::Client> { Ok(http_agent_builder()?.build()?) }
#[cfg(not(feature = "danger-local-https"))]
fn http_agent() -> Result<reqwest::Client> { Ok(reqwest::Client::new()) }
#[cfg(feature = "danger-local-https")]
fn http_agent_builder() -> Result<reqwest::ClientBuilder> {
use rustls::pki_types::CertificateDer;
use rustls::RootCertStore;
let cert_der = read_local_cert()?;
let mut root_cert_store = RootCertStore::empty();
root_cert_store.add(CertificateDer::from(cert_der.as_slice()))?;
Ok(reqwest::ClientBuilder::new()
.danger_accept_invalid_certs(true)
.use_rustls_tls()
.add_root_certificate(reqwest::tls::Certificate::from_der(cert_der.as_slice())?))
}
#[cfg(feature = "danger-local-https")]
fn read_local_cert() -> Result<Vec<u8>> {
let mut local_cert_path = std::env::temp_dir();
local_cert_path.push(LOCAL_CERT_FILE);
Ok(std::fs::read(local_cert_path)?)
}