Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 205 additions & 3 deletions payjoin-ffi/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,9 +344,6 @@ impl InitialReceiveTransition {
}
}

#[derive(Clone, Debug, uniffi::Object)]
pub struct ReceiverBuilder(payjoin::receive::v2::ReceiverBuilder);

/// Primitive representation of a transaction output for the FFI boundary.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, uniffi::Record)]
pub struct TxOut {
Expand Down Expand Up @@ -458,6 +455,9 @@ impl From<payjoin::bitcoin::Weight> for Weight {
fn from(value: payjoin::bitcoin::Weight) -> Self { Weight { weight_units: value.to_wu() } }
}

#[derive(Clone, Debug, uniffi::Object)]
pub struct ReceiverBuilder(payjoin::receive::v2::ReceiverBuilder);

#[uniffi::export]
impl ReceiverBuilder {
/// Creates a new [`Initialized`] with the provided parameters.
Expand Down Expand Up @@ -728,6 +728,23 @@ impl UncheckedOriginalPayload {
)))))
}

pub fn extract_tx_to_check_broadcast_suitability(&self) -> Vec<u8> {
payjoin::bitcoin::consensus::encode::serialize(
&self.0.clone().extract_tx_to_check_broadcast_suitability(),
)
}

pub fn apply_broadcast_suitability(
&self,
min_fee_rate_sat_per_kwu: Option<u64>,
can_broadcast: bool,
) -> Result<UncheckedOriginalPayloadTransition, FfiValidationError> {
let min_fee_rate = validate_fee_rate_sat_per_kwu_opt(min_fee_rate_sat_per_kwu)?;
Ok(UncheckedOriginalPayloadTransition(Arc::new(RwLock::new(Some(
self.0.clone().apply_broadcast_suitability(min_fee_rate, can_broadcast),
)))))
}

/// Call this method if the only way to initiate a Payjoin with this receiver
/// requires manual intervention, as in most consumer wallets.
///
Expand All @@ -740,6 +757,34 @@ impl UncheckedOriginalPayload {
}
}

#[derive(Debug, uniffi::Object)]
pub struct InputOwnedReference(
payjoin::receive::Reference<payjoin::bitcoin::ScriptBuf, payjoin::receive::InputOwnedTag>,
);

#[uniffi::export]
impl InputOwnedReference {
pub fn get_value(&self) -> Vec<u8> { self.0.get_value().to_bytes() }

pub fn mark(&self, result: bool) -> Arc<InputOwnedTaggedReference> {
Arc::new(InputOwnedTaggedReference(self.0.mark(result)))
}
}

#[derive(Debug, uniffi::Object)]
pub struct InputOwnedTaggedReference(
payjoin::receive::TaggedReference<payjoin::bitcoin::ScriptBuf, payjoin::receive::InputOwnedTag>,
);

#[uniffi::export]
impl InputOwnedTaggedReference {
pub fn get_value(&self) -> Vec<u8> { self.0.get_value().to_bytes() }

pub fn get_result(&self) -> bool { self.0.get_result() }

pub fn get_index(&self) -> u64 { self.0.get_index() as u64 }
}

#[derive(Clone, uniffi::Object)]
pub struct MaybeInputsOwned(payjoin::receive::v2::Receiver<payjoin::receive::v2::MaybeInputsOwned>);

Expand Down Expand Up @@ -783,6 +828,7 @@ impl MaybeInputsOwned {
&self.0.clone().extract_tx_to_schedule_broadcast(),
)
}

pub fn check_inputs_not_owned(
&self,
is_owned: Arc<dyn IsScriptOwned>,
Expand All @@ -793,6 +839,58 @@ impl MaybeInputsOwned {
}),
))))
}

pub fn get_input_script_refs(&self) -> Result<Vec<Arc<InputOwnedReference>>, ReceiverError> {
self.0
.clone()
.get_input_script_refs()
.map(|iter| {
iter.map(|input_script_ref| Arc::new(InputOwnedReference(input_script_ref)))
.collect::<Vec<_>>()
})
.map_err(ReceiverError::from)
}

pub fn apply_input_owned_checks(
&self,
checked_input_scripts: Vec<Arc<InputOwnedTaggedReference>>,
) -> MaybeInputsOwnedTransition {
MaybeInputsOwnedTransition(Arc::new(RwLock::new(Some(
self.0.clone().apply_input_owned_checks(checked_input_scripts.into_iter().map(|r| {
Arc::try_unwrap(r)
.expect("InputOwnedTaggedReference Arc should have a single owner")
.0
})),
))))
}
}

#[derive(Debug, uniffi::Object)]
pub struct InputSeenReference(
payjoin::receive::Reference<payjoin::bitcoin::OutPoint, payjoin::receive::InputSeenTag>,
);

#[uniffi::export]
impl InputSeenReference {
pub fn get_value(&self) -> OutPoint { self.0.get_value().into() }

pub fn mark(&self, result: bool) -> Arc<InputSeenTaggedReference> {
Arc::new(InputSeenTaggedReference(self.0.mark(result)))
}
}

#[derive(Debug, uniffi::Object)]
pub struct InputSeenTaggedReference(
payjoin::receive::TaggedReference<payjoin::bitcoin::OutPoint, payjoin::receive::InputSeenTag>,
);

#[uniffi::export]
impl InputSeenTaggedReference {
pub fn get_value(&self) -> OutPoint { self.0.get_value().into() }

pub fn get_result(&self) -> bool { self.0.get_result() }

pub fn get_index(&self) -> u64 { self.0.get_index() as u64 }
}

#[derive(Clone, uniffi::Object)]
Expand Down Expand Up @@ -844,6 +942,58 @@ impl MaybeInputsSeen {
}),
))))
}

pub fn get_input_outpoint_refs(&self) -> Vec<Arc<InputSeenReference>> {
self.0
.clone()
.get_input_outpoint_refs()
.map(|input_outpoint_ref| Arc::new(InputSeenReference(input_outpoint_ref)))
.collect::<Vec<_>>()
}

pub fn apply_input_seen_checks(
&self,
checked_input_outpoints: Vec<Arc<InputSeenTaggedReference>>,
) -> MaybeInputsSeenTransition {
MaybeInputsSeenTransition(Arc::new(RwLock::new(Some(
self.0.clone().apply_input_seen_checks(checked_input_outpoints.into_iter().map(|r| {
Arc::try_unwrap(r)
.expect("InputSeenTaggedReference Arc should have a single owner")
.0
})),
))))
}
}

#[derive(Debug, uniffi::Object)]
pub struct OutputOwnedReference(
payjoin::receive::Reference<payjoin::bitcoin::ScriptBuf, payjoin::receive::OutputOwnedTag>,
);

#[uniffi::export]
impl OutputOwnedReference {
pub fn get_value(&self) -> Vec<u8> { self.0.get_value().to_bytes() }

pub fn mark(&self, result: bool) -> Arc<OutputOwnedTaggedReference> {
Arc::new(OutputOwnedTaggedReference(self.0.mark(result)))
}
}

#[derive(Debug, uniffi::Object)]
pub struct OutputOwnedTaggedReference(
payjoin::receive::TaggedReference<
payjoin::bitcoin::ScriptBuf,
payjoin::receive::OutputOwnedTag,
>,
);

#[uniffi::export]
impl OutputOwnedTaggedReference {
pub fn get_value(&self) -> Vec<u8> { self.0.get_value().to_bytes() }

pub fn get_result(&self) -> bool { self.0.get_result() }

pub fn get_index(&self) -> u64 { self.0.get_index() as u64 }
}

/// The receiver has not yet identified which outputs belong to the receiver.
Expand Down Expand Up @@ -893,6 +1043,27 @@ impl OutputsUnknown {
}),
))))
}

pub fn get_output_script_refs(&self) -> Vec<Arc<OutputOwnedReference>> {
self.0
.clone()
.get_output_script_refs()
.map(|output_script_ref| Arc::new(OutputOwnedReference(output_script_ref)))
.collect::<Vec<_>>()
}

pub fn apply_output_owned_checks(
&self,
checked_output_scripts: Vec<Arc<OutputOwnedTaggedReference>>,
) -> OutputsUnknownTransition {
OutputsUnknownTransition(Arc::new(RwLock::new(Some(
self.0.clone().apply_output_owned_checks(checked_output_scripts.into_iter().map(|r| {
Arc::try_unwrap(r)
.expect("OutputOwnedTaggedReference Arc should have a single owner")
.0
})),
))))
}
}

#[derive(uniffi::Object)]
Expand Down Expand Up @@ -1177,6 +1348,14 @@ impl ProvisionalProposal {
}

pub fn psbt_to_sign(&self) -> String { self.0.clone().psbt_to_sign().to_string() }

pub fn finalize_signed_proposal(&self, signed_psbt: String) -> ProvisionalProposalTransition {
ProvisionalProposalTransition(Arc::new(RwLock::new(Some(
self.0.clone().finalize_proposal(|_| {
Ok(Psbt::from_str(&signed_psbt).map_err(ImplementationError::new)?)
}),
))))
}
}

#[derive(Clone, uniffi::Object)]
Expand Down Expand Up @@ -1433,6 +1612,29 @@ impl Monitor {
.map_err(|e| ImplementationError::new(e).into())
})))))
}
pub fn extract_fallback_txid(&self) -> String {
self.0.clone().extract_fallback_txid().to_string()
}

pub fn extract_payjoin_proposal_txid(&self) -> String {
self.0.clone().extract_payjoin_proposal_txid().to_string()
}

pub fn check_fallback_monitorable(&self) -> MonitorTransition {
MonitorTransition(Arc::new(RwLock::new(Some(self.0.clone().check_fallback_monitorable()))))
}

pub fn fallback_tx_exists(&self) -> MonitorTransition {
MonitorTransition(Arc::new(RwLock::new(Some(self.0.clone().fallback_tx_exists()))))
}

pub fn payjoin_tx_exists(
&self,
payjoin_tx: Vec<u8>,
) -> Result<MonitorTransition, ForeignError> {
let tx = try_deserialize_tx(payjoin_tx)?;
Ok(MonitorTransition(Arc::new(RwLock::new(Some(self.0.clone().payjoin_tx_exists(tx))))))
}
}

/// Session persister that should save and load events as JSON strings.
Expand Down
2 changes: 1 addition & 1 deletion payjoin/src/core/receive/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,7 @@ mod tests {
.calculate_psbt_context_with_fee_range(None, None)
.expect("Contributed inputs should allow for valid fee contributions");
let payjoin_proposal =
psbt_context.finalize_proposal(|_| Ok(processed_psbt.clone())).expect("Valid psbt");
psbt_context.finalize_signed_proposal(processed_psbt.clone()).expect("Valid psbt");

assert!(payjoin_proposal.xpub.is_empty());

Expand Down
5 changes: 5 additions & 0 deletions payjoin/src/core/receive/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::{error, fmt};
use crate::error_codes::ErrorCode::{
self, NotEnoughMoney, OriginalPsbtRejected, Unavailable, VersionUnsupported,
};
use crate::ImplementationError;

/// The top-level error type for the payjoin receiver
#[derive(Debug)]
Expand All @@ -29,6 +30,10 @@ impl From<ProtocolError> for Error {
fn from(e: ProtocolError) -> Self { Error::Protocol(e) }
}

impl From<ImplementationError> for Error {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this extends the public API, and since ImplementationError has some From impls (which arguably also shouldn't be there) effectively allows any &str or Box<dyn Error + Send + sync>) to be converted to an Error

@chavic IIRC you have been auditing and removing some of these to remove unintended pub functionality, thoughts?

fn from(e: ImplementationError) -> Self { Error::Implementation(e) }
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Expand Down
Loading