Skip to content

Commit

Permalink
Send payjoin
Browse files Browse the repository at this point in the history
  • Loading branch information
DanGould authored and benalleng committed Nov 22, 2023
1 parent c0ce5b5 commit 4908b6a
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 3 deletions.
31 changes: 31 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions mutiny-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ cbc = { version = "0.1", features = ["alloc"] }
aes = { version = "0.8" }
jwt-compact = { version = "0.8.0-beta.1", features = ["es256k"] }
argon2 = { version = "0.5.0", features = ["password-hash", "alloc"] }
payjoin = { version = "0.10.0", features = ["send", "base64"] }

base64 = "0.13.0"
pbkdf2 = "0.11"
Expand Down
21 changes: 21 additions & 0 deletions mutiny-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,15 @@ pub enum MutinyError {
/// Cannot change password to the same password
#[error("Cannot change password to the same password.")]
SamePassword,
/// Payjoin request creation failed.
#[error("Failed to create payjoin request.")]
PayjoinCreateRequest,
/// Payjoin response validation failed.
#[error("Failed to validate payjoin response.")]
PayjoinValidateResponse(payjoin::send::ValidationError),
/// Payjoin configuration error
#[error("Payjoin configuration failed.")]
PayjoinConfigError,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
Expand Down Expand Up @@ -446,3 +455,15 @@ impl From<nostr::event::builder::Error> for MutinyError {
Self::NostrError
}
}

impl From<payjoin::send::CreateRequestError> for MutinyError {
fn from(_e: payjoin::send::CreateRequestError) -> Self {
Self::PayjoinCreateRequest
}
}

impl From<payjoin::send::ValidationError> for MutinyError {
fn from(e: payjoin::send::ValidationError) -> Self {
Self::PayjoinValidateResponse(e)
}
}
76 changes: 75 additions & 1 deletion mutiny-core/src/nodemanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ use crate::{gossip::*, scorer::HubPreferentialScorer};
use crate::{labels::LabelStorage, subscription::MutinySubscriptionClient};
use anyhow::anyhow;
use bdk::chain::{BlockId, ConfirmationTime};
use bdk::{wallet::AddressIndex, LocalUtxo};
use bdk::{wallet::AddressIndex, FeeRate, LocalUtxo};
use bitcoin::blockdata::script;
use bitcoin::hashes::hex::ToHex;
use bitcoin::hashes::{sha256, Hash};
use bitcoin::psbt::PartiallySignedTransaction;
use bitcoin::secp256k1::{rand, PublicKey};
use bitcoin::util::bip32::ExtendedPrivKey;
use bitcoin::{Address, Network, OutPoint, Transaction, Txid};
Expand All @@ -51,9 +52,11 @@ use lnurl::lnurl::LnUrl;
use lnurl::{AsyncClient as LnUrlClient, LnUrlResponse, Response};
use nostr::key::XOnlyPublicKey;
use nostr::{EventBuilder, Keys, Kind, Tag, TagKind};
use payjoin::{PjUri, PjUriExt};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::io::Cursor;
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{collections::HashMap, ops::Deref, sync::Arc};
Expand Down Expand Up @@ -1018,6 +1021,77 @@ impl<S: MutinyStorage> NodeManager<S> {
})
}

pub async fn send_payjoin(
&self,
uri: PjUri<'_>,
amount: u64,
labels: Vec<String>,
fee_rate: Option<f32>,
) -> Result<Txid, MutinyError> {
let address = Address::from_str(&uri.address.to_string())
.map_err(|_| MutinyError::PayjoinConfigError)?;
let original_psbt = self.wallet.create_signed_psbt(address, amount, fee_rate)?;

let payout_scripts = std::iter::once(uri.address.script_pubkey());
let fee_rate = if let Some(rate) = fee_rate {
FeeRate::from_sat_per_vb(rate)
} else {
let sat_per_kwu = self.fee_estimator.get_normal_fee_rate();
FeeRate::from_sat_per_kwu(sat_per_kwu as f32)
};
let fee_rate = payjoin::bitcoin::FeeRate::from_sat_per_kwu(fee_rate.sat_per_kwu() as u64);
let original_psbt = payjoin::bitcoin::psbt::PartiallySignedTransaction::from_str(
&original_psbt.to_string(),
)
.map_err(|_| MutinyError::PayjoinConfigError)?;
let pj_params =
payjoin::send::Configuration::recommended(&original_psbt, payout_scripts, fee_rate)
.map_err(|_| MutinyError::PayjoinConfigError)?;

log_debug!(self.logger, "Creating payjoin request");
let (req, ctx) = uri.create_pj_request(original_psbt.clone(), pj_params)?;

let client = Client::builder()
.build()
.map_err(|_| MutinyError::PayjoinConfigError)?;

log_debug!(self.logger, "Sending payjoin request");
let res = client
.post(req.url)
.body(req.body)
.header("Content-Type", "text/plain")
.send()
.await
.map_err(|_| MutinyError::PayjoinCreateRequest)?
.bytes()
.await
.map_err(|_| MutinyError::PayjoinCreateRequest)?;

let mut cursor = Cursor::new(res.to_vec());

log_debug!(self.logger, "Processing payjoin response");
let proposal_psbt = ctx.process_response(&mut cursor).map_err(|e| {
log_error!(self.logger, "Error processing payjoin response: {e}");
e
})?;

// convert to pdk types
let original_psbt = PartiallySignedTransaction::from_str(&original_psbt.to_string())
.map_err(|_| MutinyError::PayjoinConfigError)?;
let proposal_psbt = PartiallySignedTransaction::from_str(&proposal_psbt.to_string())
.map_err(|_| MutinyError::PayjoinConfigError)?;

log_debug!(self.logger, "Sending payjoin..");
let tx = self
.wallet
.send_payjoin(original_psbt, proposal_psbt, labels)
.await?;
let txid = tx.txid();
self.broadcast_transaction(tx).await?;
log_debug!(self.logger, "Payjoin broadcast! TXID: {txid}");
Ok(txid)
}

/// Sends an on-chain transaction to the given address.
/// The amount is in satoshis and the fee rate is in sat/vbyte.
///
Expand Down
57 changes: 55 additions & 2 deletions mutiny-core/src/onchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ use bdk::{FeeRate, LocalUtxo, SignOptions, TransactionDetails, Wallet};
use bdk_esplora::EsploraAsyncExt;
use bitcoin::consensus::serialize;
use bitcoin::hashes::hex::ToHex;
use bitcoin::psbt::PartiallySignedTransaction;
use bitcoin::psbt::{Input, PartiallySignedTransaction};
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey};
use bitcoin::{Address, Network, OutPoint, Script, Transaction, Txid};
use lightning::events::bump_transaction::{Utxo, WalletSource};
use lightning::util::logger::Logger;
use lightning::{log_debug, log_error, log_info, log_warn};
use lightning::{log_debug, log_error, log_info, log_trace, log_warn};

use crate::error::MutinyError;
use crate::fees::MutinyFeeEstimator;
Expand Down Expand Up @@ -478,6 +478,59 @@ impl<S: MutinyStorage> OnChainWallet<S> {
Ok(txid)
}

pub async fn send_payjoin(
&self,
mut original_psbt: PartiallySignedTransaction,
mut proposal_psbt: PartiallySignedTransaction,
labels: Vec<String>,
) -> Result<Transaction, MutinyError> {
let wallet = self.wallet.try_read()?;

// add original psbt input map data in place so BDK knows which scripts to sign,
// proposal_psbt only contains the sender input outpoints, not scripts, which BDK
// does not look up
fn input_pairs(
psbt: &mut PartiallySignedTransaction,
) -> Box<dyn Iterator<Item = (&bitcoin::TxIn, &mut Input)> + '_> {
Box::new(psbt.unsigned_tx.input.iter().zip(&mut psbt.inputs))
}

let mut original_inputs = input_pairs(&mut original_psbt).peekable();

for (proposed_txin, proposed_psbtin) in input_pairs(&mut proposal_psbt) {
log_trace!(
self.logger,
"Proposed txin: {:?}",
proposed_txin.previous_output
);
if let Some((original_txin, original_psbtin)) = original_inputs.peek() {
log_trace!(
self.logger,
"Original txin: {:?}",
original_txin.previous_output
);
log_trace!(self.logger, "Original psbtin: {original_psbtin:?}");
if proposed_txin.previous_output == original_txin.previous_output {
proposed_psbtin.witness_utxo = original_psbtin.witness_utxo.clone();
proposed_psbtin.non_witness_utxo = original_psbtin.non_witness_utxo.clone();
original_inputs.next();
}
}
}

log_trace!(self.logger, "Augmented PSBT: {proposal_psbt:?}");
// sign and finalize payjoin
let result = wallet.sign(&mut proposal_psbt, SignOptions::default());
log_trace!(self.logger, "Sign result: {result:?}");
result?;
drop(wallet);

self.label_psbt(&proposal_psbt, labels)?;
let payjoin = proposal_psbt.extract_tx();

Ok(payjoin)
}

pub fn create_sweep_psbt(
&self,
spk: Script,
Expand Down
1 change: 1 addition & 0 deletions mutiny-wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ getrandom = { version = "0.2", features = ["js"] }
futures = "0.3.25"
urlencoding = "2.1.2"
once_cell = "1.18.0"
payjoin = { version = "0.10.0", features = ["send", "base64"] }

# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
Expand Down
12 changes: 12 additions & 0 deletions mutiny-wasm/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,15 @@ pub enum MutinyJsError {
/// Cannot change password to the same password
#[error("Cannot change password to the same password.")]
SamePassword,
/// Payjoin request creation failed.
#[error("Failed to create payjoin request.")]
PayjoinCreateRequest,
/// Payjoin response validation failed.
#[error("Failed to validate payjoin response.")]
PayjoinValidateResponse,
/// Payjoin configuration error
#[error("Payjoin configuration failed.")]
PayjoinConfigError,
/// Unknown error.
#[error("Unknown Error")]
UnknownError,
Expand Down Expand Up @@ -194,6 +203,9 @@ impl From<MutinyError> for MutinyJsError {
MutinyError::InvalidArgumentsError => MutinyJsError::InvalidArgumentsError,
MutinyError::LspAmountTooHighError => MutinyJsError::LspAmountTooHighError,
MutinyError::NetworkMismatch => MutinyJsError::NetworkMismatch,
MutinyError::PayjoinConfigError => MutinyJsError::PayjoinConfigError,
MutinyError::PayjoinCreateRequest => MutinyJsError::PayjoinCreateRequest,
MutinyError::PayjoinValidateResponse(_) => MutinyJsError::PayjoinValidateResponse,
}
}
}
Expand Down
26 changes: 26 additions & 0 deletions mutiny-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ use mutiny_core::{labels::LabelStorage, nodemanager::NodeManager};
use mutiny_core::{logging::MutinyLogger, nostr::ProfileType};
use nostr::key::XOnlyPublicKey;
use nostr::prelude::FromBech32;
use payjoin::UriExt;
use std::str::FromStr;
use std::sync::Arc;
use std::{
Expand Down Expand Up @@ -451,6 +452,31 @@ impl MutinyWallet {
.to_string())
}

#[wasm_bindgen]
pub async fn send_payjoin(
&self,
payjoin_uri: String,
amount: u64, /* override the uri amount if desired */
labels: &JsValue, /* Vec<String> */
fee_rate: Option<f32>,
) -> Result<String, MutinyJsError> {
// I know walia parses `pj=` and `pjos=` but payjoin::Uri parses the whole bip21 uri
let pj_uri = payjoin::Uri::try_from(payjoin_uri.as_str())
.map_err(|_| MutinyJsError::InvalidArgumentsError)?
.assume_checked()
.check_pj_supported()
.map_err(|_| MutinyJsError::InvalidArgumentsError)?;
let labels: Vec<String> = labels
.into_serde()
.map_err(|_| MutinyJsError::InvalidArgumentsError)?;
Ok(self
.inner
.node_manager
.send_payjoin(pj_uri, amount, labels, fee_rate)
.await?
.to_string())
}

/// Sweeps all the funds from the wallet to the given address.
/// The fee rate is in sat/vbyte.
///
Expand Down

0 comments on commit 4908b6a

Please sign in to comment.