Skip to content

Commit

Permalink
Draft send payjoin
Browse files Browse the repository at this point in the history
  • Loading branch information
DanGould committed Sep 30, 2023
1 parent b297674 commit c859a25
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 0 deletions.
1 change: 1 addition & 0 deletions mutiny-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ aes = { version = "0.8" }
jwt-compact = { version = "0.8.0-beta.1", features = ["es256k"] }
argon2 = { version = "0.5.0", features = ["password-hash", "alloc"] }
hashbrown = { version = "0.8" }
payjoin = { version = "0.10.0", features = ["send", "base64"] }

base64 = "0.13.0"
pbkdf2 = "0.11"
Expand Down
27 changes: 27 additions & 0 deletions mutiny-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,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 configuration error
#[error("Payjoin configuration failed.")]
PayjoinConfigError,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
Expand Down Expand Up @@ -377,3 +386,21 @@ 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
}
}

impl From<payjoin::send::ConfigurationError> for MutinyError {
fn from(_e: payjoin::send::ConfigurationError) -> Self {
Self::PayjoinConfigError
}
}
52 changes: 52 additions & 0 deletions mutiny-core/src/nodemanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ 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;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
Expand Down Expand Up @@ -1060,6 +1061,57 @@ impl<S: MutinyStorage> NodeManager<S> {
})
}

pub async fn send_payjoin(
&self,
pj_uri: PjUri,
amount: u64,
labels: Vec<String>,
fee_rate: Option<f32>,
) -> Result<Txid, MutinyError> {
let uri = payjoin::Uri::try_from(uri)?
.require_network(self.network)
.map_err(|_| MutinyError::IncorrectNetwork(self.network))?
.check_pj_supported()
.map_err(|_| MutinyError::PayjoinConfigError)?;

let original_psbt = self
.wallet
.create_signed_psbt(uri.address, amount, Some(fee_rate))?;

let payout_scripts = std::iter::once(uri.address.script_pubkey()).collect();
let fee_rate = if let Some(rate) = fee_rate {
FeeRate::from_sat_per_vb(rate)
} else {
let sat_per_kwu = self
.fees
.get_est_sat_per_1000_weight(ConfirmationTarget::Normal);
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 pj_params =
payjoin::send::Configuration::recommended(original_psbt, payout_scripts, fee_rate)?;
let (req, ctx) = uri.create_pj_request(psbt, pj_params)?;

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

let mut res = client
.post(req.url)
.body(req.body)
.header("Content-Type", "text/plain")
.send()
.await
.map_err(|_| MutinyError::PayjoinValidateResponseError)?
.text()
.await
.map_err(|_| MutinyError::PayjoinValidateResponseError)?;

let mut proposal_psbt = ctx.process_response(res)?;

self.wallet.send_payjoin(original_psbt, proposal_psbt).await
}

/// Sends an on-chain transaction to the given address.
/// The amount is in satoshis and the fee rate is in sat/vbyte.
///
Expand Down
40 changes: 40 additions & 0 deletions mutiny-core/src/onchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use lightning::chain::chaininterface::{ConfirmationTarget, FeeEstimator};
use lightning::events::bump_transaction::{Utxo, WalletSource};
use lightning::util::logger::Logger;
use lightning::{log_debug, log_error, log_info, log_warn};
use payjoin::{PjUri, PjUriExt, UriExt};

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

pub fn send_payjoin(
&self,
original_psbt: PartiallySignedTransaction,
proposal_psbt: PartiallySignedTransaction,
) -> Result<Txid, MutinyError> {
let mut wallet = self.wallet.try_write()?;

// 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 = (&bdk::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) {
if let Some((original_txin, original_psbtin)) = original_inputs.peek() {
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();
}
}

// sign and finalize payjoin
let payjoin = wallet
.sign(&mut proposal_psbt, SignOptions::default())?
.extract_tx();
let txid = payjoin.txid();

self.broadcast_transaction(payjoin.clone()).await?;
log_debug!(self.logger, "Payjoin broadcast! TXID: {txid}");
Ok(txid)
}

pub fn create_sweep_psbt(
&self,
spk: Script,
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 @@ -137,6 +137,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 @@ -189,6 +198,9 @@ impl From<MutinyError> for MutinyJsError {
}
MutinyError::InvalidArgumentsError => MutinyJsError::InvalidArgumentsError,
MutinyError::LspAmountTooHighError => MutinyJsError::LspAmountTooHighError,
MutinyError::PayjoinConfigError => MutinyJsError::PayjoinConfigError,
MutinyError::PayjoinCreateRequest => MutinyJsError::PayjoinCreateRequest,
MutinyError::PayjoinValidateResponse => MutinyJsError::PayjoinValidateResponse,
}
}
}
Expand Down
21 changes: 21 additions & 0 deletions mutiny-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,27 @@ 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())?;
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 c859a25

Please sign in to comment.