Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow LDK node to send payjoin transactions #295

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ libc = "0.2"
uniffi = { version = "0.27.3", features = ["build"], optional = true }
serde = { version = "1.0.210", default-features = false, features = ["std", "derive"] }
serde_json = { version = "1.0.128", default-features = false, features = ["std"] }
payjoin = { version = "0.21.0", default-features = false, features = ["send", "v2"] }

vss-client = "0.3"
prost = { version = "0.11.6", default-features = false}
Expand All @@ -89,6 +90,8 @@ electrum-client = { version = "0.21.0", default-features = true }
bitcoincore-rpc = { version = "0.19.0", default-features = false }
proptest = "1.0.0"
regex = "1.5.6"
payjoin = { version = "0.21.0", default-features = false, features = ["send", "v2", "receive"] }
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "blocking"] }

[target.'cfg(not(no_download))'.dev-dependencies]
electrsd = { version = "0.29.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_25_0"] }
Expand Down
27 changes: 27 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ interface Node {
SpontaneousPayment spontaneous_payment();
OnchainPayment onchain_payment();
UnifiedQrPayment unified_qr_payment();
PayjoinPayment payjoin_payment();
[Throws=NodeError]
void connect(PublicKey node_id, SocketAddress address, boolean persist);
[Throws=NodeError]
Expand Down Expand Up @@ -171,6 +172,13 @@ interface UnifiedQrPayment {
QrPaymentResult send([ByRef]string uri_str);
};

interface PayjoinPayment {
[Throws=NodeError]
void send(string payjoin_uri);
[Throws=NodeError]
void send_with_amount(string payjoin_uri, u64 amount_sats);
};

[Error]
enum NodeError {
"AlreadyRunning",
Expand Down Expand Up @@ -222,6 +230,12 @@ enum NodeError {
"InsufficientFunds",
"LiquiditySourceUnavailable",
"LiquidityFeeTooHigh",
"PayjoinUnavailable",
"PayjoinUriInvalid",
"PayjoinRequestMissingAmount",
"PayjoinRequestCreationFailed",
"PayjoinRequestSendingFailed",
"PayjoinResponseProcessingFailed",
};

dictionary NodeStatus {
Expand Down Expand Up @@ -249,6 +263,7 @@ enum BuildError {
"InvalidChannelMonitor",
"InvalidListeningAddresses",
"InvalidNodeAlias",
"InvalidPayjoinConfig",
"ReadFailed",
"WriteFailed",
"StoragePathAccessFailed",
Expand Down Expand Up @@ -281,6 +296,9 @@ interface Event {
ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo);
ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id);
ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason);
PayjoinPaymentAwaitingConfirmation(Txid txid, u64 amount_sats);
PayjoinPaymentSuccessful(Txid txid, u64 amount_sats, boolean is_original_psbt_modified);
PayjoinPaymentFailed(Txid txid, u64 amount_sats, PayjoinPaymentFailureReason reason);
};

enum PaymentFailureReason {
Expand All @@ -295,6 +313,12 @@ enum PaymentFailureReason {
"InvoiceRequestRejected",
};

enum PayjoinPaymentFailureReason {
"Timeout",
"RequestSendingFailed",
"ResponseProcessingFailed",
};

[Enum]
interface ClosureReason {
CounterpartyForceClosed(UntrustedString peer_msg);
Expand All @@ -321,6 +345,7 @@ interface PaymentKind {
Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity);
Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, UntrustedString? payer_note, u64? quantity);
Spontaneous(PaymentHash hash, PaymentPreimage? preimage);
Payjoin();
};

[Enum]
Expand Down Expand Up @@ -353,6 +378,8 @@ dictionary PaymentDetails {
PaymentDirection direction;
PaymentStatus status;
u64 latest_update_timestamp;
Txid? txid;
BestBlock? best_block;
};

dictionary SendingParameters {
Expand Down
46 changes: 44 additions & 2 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use crate::types::{
use crate::wallet::persist::KVStoreWalletPersister;
use crate::wallet::Wallet;
use crate::{io, NodeMetrics};
use crate::PayjoinHandler;
use crate::{LogLevel, Node};

use lightning::chain::{chainmonitor, BestBlock, Watch};
Expand Down Expand Up @@ -100,6 +101,11 @@ struct LiquiditySourceConfig {
lsps2_service: Option<(SocketAddress, PublicKey, Option<String>)>,
}

#[derive(Debug, Clone)]
struct PayjoinConfig {
payjoin_relay: payjoin::Url,
}

impl Default for LiquiditySourceConfig {
fn default() -> Self {
Self { lsps2_service: None }
Expand Down Expand Up @@ -141,6 +147,8 @@ pub enum BuildError {
WalletSetupFailed,
/// We failed to setup the logger.
LoggerSetupFailed,
/// Invalid Payjoin configuration.
InvalidPayjoinConfig,
}

impl fmt::Display for BuildError {
Expand All @@ -162,6 +170,10 @@ impl fmt::Display for BuildError {
Self::WalletSetupFailed => write!(f, "Failed to setup onchain wallet."),
Self::LoggerSetupFailed => write!(f, "Failed to setup the logger."),
Self::InvalidNodeAlias => write!(f, "Given node alias is invalid."),
Self::InvalidPayjoinConfig => write!(
f,
"Invalid Payjoin configuration. Make sure the provided arguments are valid URLs."
),
}
}
}
Expand All @@ -182,6 +194,7 @@ pub struct NodeBuilder {
chain_data_source_config: Option<ChainDataSourceConfig>,
gossip_source_config: Option<GossipSourceConfig>,
liquidity_source_config: Option<LiquiditySourceConfig>,
payjoin_config: Option<PayjoinConfig>,
}

impl NodeBuilder {
Expand All @@ -197,12 +210,14 @@ impl NodeBuilder {
let chain_data_source_config = None;
let gossip_source_config = None;
let liquidity_source_config = None;
let payjoin_config = None;
Self {
config,
entropy_source_config,
chain_data_source_config,
gossip_source_config,
liquidity_source_config,
payjoin_config,
}
}

Expand Down Expand Up @@ -273,6 +288,14 @@ impl NodeBuilder {
self
}

/// Configures the [`Node`] instance to enable payjoin transactions.
pub fn set_payjoin_config(&mut self, payjoin_relay: String) -> Result<&mut Self, BuildError> {
let payjoin_relay =
payjoin::Url::parse(&payjoin_relay).map_err(|_| BuildError::InvalidPayjoinConfig)?;
self.payjoin_config = Some(PayjoinConfig { payjoin_relay });
Ok(self)
}

/// Configures the [`Node`] instance to source its inbound liquidity from the given
/// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md)
/// service.
Expand Down Expand Up @@ -480,6 +503,7 @@ impl NodeBuilder {
self.chain_data_source_config.as_ref(),
self.gossip_source_config.as_ref(),
self.liquidity_source_config.as_ref(),
self.payjoin_config.as_ref(),
seed_bytes,
logger,
Arc::new(vss_store),
Expand All @@ -501,6 +525,7 @@ impl NodeBuilder {
self.chain_data_source_config.as_ref(),
self.gossip_source_config.as_ref(),
self.liquidity_source_config.as_ref(),
self.payjoin_config.as_ref(),
seed_bytes,
logger,
kv_store,
Expand Down Expand Up @@ -586,6 +611,11 @@ impl ArcedNodeBuilder {
self.inner.write().unwrap().set_gossip_source_p2p();
}

/// Configures the [`Node`] instance to enable payjoin transactions.
pub fn set_payjoin_config(&self, payjoin_relay: String) -> Result<(), BuildError> {
self.inner.write().unwrap().set_payjoin_config(payjoin_relay).map(|_| ())
}

/// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync
/// server.
pub fn set_gossip_source_rgs(&self, rgs_server_url: String) {
Expand Down Expand Up @@ -733,8 +763,9 @@ impl ArcedNodeBuilder {
fn build_with_store_internal(
config: Arc<Config>, chain_data_source_config: Option<&ChainDataSourceConfig>,
gossip_source_config: Option<&GossipSourceConfig>,
liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64],
logger: Arc<FilesystemLogger>, kv_store: Arc<DynStore>,
liquidity_source_config: Option<&LiquiditySourceConfig>,
payjoin_config: Option<&PayjoinConfig>, seed_bytes: [u8; 64], logger: Arc<FilesystemLogger>,
kv_store: Arc<DynStore>,
) -> Result<Node, BuildError> {
// Initialize the status fields.
let is_listening = Arc::new(AtomicBool::new(false));
Expand Down Expand Up @@ -1201,6 +1232,16 @@ fn build_with_store_internal(
let (stop_sender, _) = tokio::sync::watch::channel(());
let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(());

let payjoin_handler = payjoin_config.map(|pj_config| {
Arc::new(PayjoinHandler::new(
Arc::clone(&event_queue),
Arc::clone(&logger),
pj_config.payjoin_relay.clone(),
Arc::clone(&payment_store),
Arc::clone(&wallet),
))
});

Ok(Node {
runtime,
stop_sender,
Expand All @@ -1213,6 +1254,7 @@ fn build_with_store_internal(
channel_manager,
chain_monitor,
output_sweeper,
payjoin_handler,
peer_manager,
onion_messenger,
connection_manager,
Expand Down
9 changes: 9 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ pub(crate) const RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL: u32 = 6;
// The time in-between peer reconnection attempts.
pub(crate) const PEER_RECONNECTION_INTERVAL: Duration = Duration::from_secs(10);

// The time before a payjoin http request is considered timed out.
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: s/http/HTTP/ here and below.

pub(crate) const PAYJOIN_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);

// The duration between retries of a payjoin http request.
pub(crate) const PAYJOIN_RETRY_INTERVAL: Duration = Duration::from_secs(3);

// The total duration of retrying to send a payjoin http request.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment isn't very helpful. What does 'total duration of retrying' mean? Do we abort the flow afterwards, or do we just give up?

pub(crate) const PAYJOIN_REQUEST_TOTAL_DURATION: Duration = Duration::from_secs(24 * 60 * 60);

// The time in-between RGS sync attempts.
pub(crate) const RGS_SYNC_INTERVAL: Duration = Duration::from_secs(60 * 60);

Expand Down
36 changes: 36 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@ pub enum Error {
LiquiditySourceUnavailable,
/// The given operation failed due to the LSP's required opening fee being too high.
LiquidityFeeTooHigh,
/// Failed to access Payjoin object.
PayjoinUnavailable,
/// Payjoin URI is invalid.
PayjoinUriInvalid,
/// Amount is neither user-provided nor defined in the URI.
PayjoinRequestMissingAmount,
/// Failed to build a Payjoin request.
PayjoinRequestCreationFailed,
/// Failed to send Payjoin request.
PayjoinRequestSendingFailed,
/// Payjoin response processing failed.
PayjoinResponseProcessingFailed,
}

impl fmt::Display for Error {
Expand Down Expand Up @@ -184,6 +196,30 @@ impl fmt::Display for Error {
Self::LiquidityFeeTooHigh => {
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
},
Self::PayjoinUnavailable => {
write!(
f,
"Failed to access Payjoin object. Make sure you have enabled Payjoin support."
)
},
Self::PayjoinRequestMissingAmount => {
write!(
f,
"Amount is neither user-provided nor defined in the provided Payjoin URI."
)
},
Self::PayjoinRequestCreationFailed => {
write!(f, "Failed construct a Payjoin request")
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Please end all messages with a . for uniformity with other variants.

},
Self::PayjoinUriInvalid => {
write!(f, "The provided Payjoin URI is invalid")
},
Self::PayjoinRequestSendingFailed => {
write!(f, "Failed to send Payjoin request")
},
Self::PayjoinResponseProcessingFailed => {
write!(f, "Payjoin receiver responded to our request with an invalid response")
},
}
}
}
Expand Down
Loading
Loading