Skip to content

Commit

Permalink
Add payjoin-bitcoincore-axum example receive part
Browse files Browse the repository at this point in the history
  • Loading branch information
jbesraa committed Jan 2, 2024
1 parent 231da54 commit 11ea8e9
Show file tree
Hide file tree
Showing 8 changed files with 1,294 additions and 268 deletions.
1,076 changes: 1,076 additions & 0 deletions examples/tutorial/Cargo.lock

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions examples/tutorial/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "tutorial"
version = "0.1.0"
edition = "2021"

[workspace]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
axum = "0.7.2"
# bitcoin = "0.30.0"
bitcoincore-rpc = "0.17.0"
http-body-util = "0.1.0"
payjoin = { path = "../../payjoin", features = ["send", "receive", "base64"] }
tokio = {version = "1.35.1", features = ["rt", "macros", "rt-multi-thread"] }
ureq = "2.9.1"
18 changes: 18 additions & 0 deletions examples/tutorial/src/blockchain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use bitcoincore_rpc::RpcApi;
pub struct CoreRpcConnection;

impl CoreRpcConnection {
pub fn new() -> (bitcoincore_rpc::Client, bitcoincore_rpc::Client) {
let sender_wallet = bitcoincore_rpc::Client::new(
"http://127.0.0.1:18332/wallet/sender",
bitcoincore_rpc::Auth::UserPass("user".to_string(), "password".to_string()),
)
.unwrap();
let receiver_wallet = bitcoincore_rpc::Client::new(
"http://127.0.0.1:18332/wallet/receiver",
bitcoincore_rpc::Auth::UserPass("user".to_string(), "password".to_string()),
)
.unwrap();
(receiver_wallet, sender_wallet)
}
}
11 changes: 11 additions & 0 deletions examples/tutorial/src/http_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use axum::Router;

pub struct HttpServer;

impl HttpServer {
pub async fn new(port: u16, router: Router) {
let url = format!("0.0.0.0:{}", port);
let listener = tokio::net::TcpListener::bind(url).await.unwrap();
axum::serve(listener, router).await.unwrap();
}
}
48 changes: 48 additions & 0 deletions examples/tutorial/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use std::collections::HashMap;

use axum::routing::post;
use axum::Router;
use bitcoincore_rpc::RpcApi;
use http_server::HttpServer;
use receive::Receiver;

mod blockchain;
mod http_server;
mod receive;

#[tokio::main]
async fn main() {
let (receiver_wallet, sender_wallet) = blockchain::CoreRpcConnection::new();
tokio::task::spawn(async move {
let receive_router = Router::new().route("/payjoin", post(Receiver::new));
let receive_port = 3227;
HttpServer::new(receive_port, receive_router).await;
});
let amount = payjoin::bitcoin::Amount::from_sat(1000);
let endpoint = payjoin::Url::parse("https://localhost:3227/payjoin").unwrap();
let pj_uri = payjoin::PjUriBuilder::new(
receiver_wallet.get_new_address(None, None).unwrap().assume_checked(),
endpoint,
)
.amount(Some(amount))
.build()
.unwrap();

let mut outputs = HashMap::with_capacity(1);
outputs.insert(pj_uri.address.to_string(), amount);
let options = bitcoincore_rpc::json::WalletCreateFundedPsbtOptions {
lock_unspent: Some(true),
fee_rate: Some(bitcoincore_rpc::bitcoin::Amount::ONE_SAT),
..Default::default()
};
let sender_psbt = sender_wallet
.wallet_create_funded_psbt(
&[], // inputs
&outputs,
None, // locktime
Some(options),
None,
)
.unwrap();
dbg!(&sender_psbt);
}
124 changes: 124 additions & 0 deletions examples/tutorial/src/receive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use std::{collections::HashMap, str::FromStr};

use axum::extract::Request;
use axum::http::HeaderMap;
use axum::response::IntoResponse;
use bitcoincore_rpc::{bitcoin::psbt::Psbt, RpcApi};
use http_body_util::BodyExt;
use payjoin::{
base64,
receive::{PayjoinProposal, ProvisionalProposal},
};

use crate::blockchain::CoreRpcConnection;

struct Headers(HeaderMap);

impl payjoin::receive::Headers for Headers {
fn get_header(&self, key: &str) -> Option<&str> {
self.0.get(key).and_then(|v| v.to_str().ok())
}
}

fn try_contributing_inputs(provisional_proposal: &mut ProvisionalProposal) -> Result<(), ()> {
use payjoin::bitcoin::OutPoint;
let (bitcoind, _) = CoreRpcConnection::new();

let available_inputs = bitcoind.list_unspent(None, None, None, None, None).unwrap();
let candidate_inputs: HashMap<payjoin::bitcoin::Amount, OutPoint> = available_inputs
.iter()
.map(|i| (i.amount, OutPoint { txid: i.txid, vout: i.vout }))
.collect();

let selected_outpoint = provisional_proposal.try_preserving_privacy(candidate_inputs).unwrap();
let selected_utxo = available_inputs
.iter()
.find(|i| i.txid == selected_outpoint.txid && i.vout == selected_outpoint.vout)
.unwrap();

// calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt
let txo_to_contribute = payjoin::bitcoin::TxOut {
value: selected_utxo.amount.to_sat(),
script_pubkey: selected_utxo.script_pub_key.clone(),
};
let outpoint_to_contribute =
payjoin::bitcoin::OutPoint { txid: selected_utxo.txid, vout: selected_utxo.vout };
provisional_proposal.contribute_witness_input(txo_to_contribute, outpoint_to_contribute);
Ok(())
}

async fn handle_request(request: Request, bitcoind: bitcoincore_rpc::Client) -> impl IntoResponse {
let (parts, body) = request.into_parts();
let bytes = body.collect().await.unwrap().to_bytes();
let headers = Headers(parts.headers.clone());
let proposal =
payjoin::receive::UncheckedProposal::from_request(&bytes[..], "", headers).unwrap();
let network = bitcoincore_rpc::bitcoin::Network::Testnet;

let min_fee_rate = None;
let checked_1 = proposal
.check_broadcast_suitability(min_fee_rate, |tx| {
let raw_tx = bitcoincore_rpc::bitcoin::consensus::encode::serialize_hex(&tx);
let mempool_results = bitcoind.test_mempool_accept(&[raw_tx]).unwrap();
match mempool_results.first() {
Some(result) => Ok(result.allowed),
None => panic!(""),
}
})
.unwrap();
let checked_2 = checked_1
.check_inputs_not_owned(|input| {
if let Ok(address) = payjoin::bitcoin::Address::from_script(input, network) {
Ok(bitcoind
.get_address_info(&address)
.map(|info| info.is_mine.unwrap_or(false))
.unwrap())
} else {
Ok(false)
}
})
.unwrap();
let checked_3 = checked_2.check_no_mixed_input_scripts().unwrap();
let checked_4 = checked_3.check_no_inputs_seen_before(|_outpoint| Ok(true)).unwrap();
let mut prov_proposal = checked_4
.identify_receiver_outputs(|output_script| {
if let Ok(address) = payjoin::bitcoin::Address::from_script(output_script, network) {
Ok(bitcoind
.get_address_info(&address)
.map(|info| info.is_mine.unwrap_or(false))
.unwrap())
} else {
Ok(false)
}
})
.unwrap();
let _ = try_contributing_inputs(&mut prov_proposal);
// Select receiver payjoin inputs.
let receiver_substitute_address = bitcoind.get_new_address(None, None).unwrap();
prov_proposal.substitute_output_address(receiver_substitute_address.assume_checked());
let payjoin_proposal: PayjoinProposal = prov_proposal
.finalize_proposal(
|psbt: &Psbt| {
Ok(bitcoind
.wallet_process_psbt(&base64::encode(psbt.serialize()), None, None, Some(false))
.map(|res| Psbt::from_str(&res.psbt).unwrap())
.unwrap())
},
Some(payjoin::bitcoin::FeeRate::MIN),
)
.unwrap();
let payjoin_proposal_psbt = payjoin_proposal.psbt();
dbg!(&payjoin_proposal_psbt);

""
}

pub struct Receiver;

impl Receiver {
pub async fn new(request: Request) -> impl IntoResponse {
let (receiver_wallet, _) = CoreRpcConnection::new();
let _request = handle_request(request, receiver_wallet).await;
"Hello, World!"
}
}
Empty file added examples/tutorial/src/send.rs
Empty file.
Loading

0 comments on commit 11ea8e9

Please sign in to comment.