Skip to content

Commit

Permalink
Merge pull request #550 from dleutenegger/lnurlp-expose-payment-hash
Browse files Browse the repository at this point in the history
Add payment hash to `lnurl_pay()` return type
  • Loading branch information
dangeross authored Nov 7, 2023
2 parents e393903 + 097f53a commit 6b1b0d5
Show file tree
Hide file tree
Showing 11 changed files with 618 additions and 81 deletions.
13 changes: 12 additions & 1 deletion libs/sdk-bindings/src/breez_sdk.udl
Original file line number Diff line number Diff line change
Expand Up @@ -511,10 +511,20 @@ dictionary BitcoinAddressData {
string? message;
};

dictionary LnUrlPaySuccessData {
SuccessActionProcessed? success_action;
string payment_hash;
};

dictionary LnUrlErrorData {
string reason;
};

dictionary LnUrlPayErrorData {
string payment_hash;
string reason;
};

dictionary LnUrlPayRequest {
LnUrlPayRequestData data;
u64 amount_msat;
Expand All @@ -541,8 +551,9 @@ dictionary LnUrlWithdrawRequestData {

[Enum]
interface LnUrlPayResult {
EndpointSuccess(SuccessActionProcessed? data);
EndpointSuccess(LnUrlPaySuccessData data);
EndpointError(LnUrlErrorData data);
PayError(LnUrlPayErrorData data);
};

[Enum]
Expand Down
15 changes: 8 additions & 7 deletions libs/sdk-bindings/src/uniffi_binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ use breez_sdk_core::{
ClosedChannelPaymentDetails, Config, CurrencyInfo, EnvironmentType, EventListener,
FeeratePreset, FiatCurrency, GreenlightCredentials, GreenlightNodeConfig, InputType,
InvoicePaidDetails, LNInvoice, ListPaymentsRequest, LnPaymentDetails, LnUrlAuthRequestData,
LnUrlCallbackStatus, LnUrlErrorData, LnUrlPayRequest, LnUrlPayRequestData, LnUrlPayResult,
LnUrlWithdrawRequest, LnUrlWithdrawRequestData, LnUrlWithdrawResult, LnUrlWithdrawSuccessData,
LocaleOverrides, LocalizedName, LogEntry, LogStream, LspInformation, MessageSuccessActionData,
MetadataItem, Network, NodeConfig, NodeState, OpenChannelFeeRequest, OpenChannelFeeResponse,
OpeningFeeParams, OpeningFeeParamsMenu, Payment, PaymentDetails, PaymentFailedData,
PaymentStatus, PaymentType, PaymentTypeFilter, PrepareRefundRequest, PrepareRefundResponse,
PrepareSweepRequest, PrepareSweepResponse, Rate, ReceiveOnchainRequest, ReceivePaymentRequest,
LnUrlCallbackStatus, LnUrlErrorData, LnUrlPayErrorData, LnUrlPayRequest, LnUrlPayRequestData,
LnUrlPayResult, LnUrlPaySuccessData, LnUrlWithdrawRequest, LnUrlWithdrawRequestData,
LnUrlWithdrawResult, LnUrlWithdrawSuccessData, LocaleOverrides, LocalizedName, LogEntry,
LogStream, LspInformation, MessageSuccessActionData, MetadataItem, Network, NodeConfig,
NodeState, OpenChannelFeeRequest, OpenChannelFeeResponse, OpeningFeeParams,
OpeningFeeParamsMenu, Payment, PaymentDetails, PaymentFailedData, PaymentStatus, PaymentType,
PaymentTypeFilter, PrepareRefundRequest, PrepareRefundResponse, PrepareSweepRequest,
PrepareSweepResponse, Rate, ReceiveOnchainRequest, ReceivePaymentRequest,
ReceivePaymentResponse, RecommendedFees, RefundRequest, RefundResponse, ReverseSwapFeesRequest,
ReverseSwapInfo, ReverseSwapPairInfo, ReverseSwapStatus, RouteHint, RouteHintHop,
SendOnchainRequest, SendOnchainResponse, SendPaymentRequest, SendPaymentResponse,
Expand Down
27 changes: 24 additions & 3 deletions libs/sdk-core/src/breez_services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,10 +310,28 @@ impl BreezServices {
}
ValidatedCallbackResponse::EndpointSuccess { data: cb } => {
let pay_req = SendPaymentRequest {
bolt11: cb.pr,
bolt11: cb.pr.clone(),
amount_msat: None,
};
let payment = self.send_payment(pay_req).await?.payment;

let payment = match self.send_payment(pay_req).await {
Ok(p) => Ok(p),
Err(e) => match e {
SendPaymentError::InvalidInvoice { .. } => Err(e),
SendPaymentError::ServiceConnectivity { .. } => Err(e),
_ => {
let invoice = parse_invoice(cb.pr.as_str())?;

return Ok(LnUrlPayResult::PayError {
data: LnUrlPayErrorData {
payment_hash: invoice.payment_hash,
reason: e.to_string(),
},
});
}
},
}?
.payment;
let details = match &payment.details {
PaymentDetails::ClosedChannel { .. } => {
return Err(LnUrlPayError::Generic {
Expand Down Expand Up @@ -358,7 +376,10 @@ impl BreezServices {
)?;

Ok(LnUrlPayResult::EndpointSuccess {
data: maybe_sa_processed,
data: LnUrlPaySuccessData {
payment_hash: details.payment_hash.clone(),
success_action: maybe_sa_processed,
},
})
}
}
Expand Down
39 changes: 38 additions & 1 deletion libs/sdk-core/src/bridge_generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ use crate::invoice::LNInvoice;
use crate::invoice::RouteHint;
use crate::invoice::RouteHintHop;
use crate::lnurl::pay::model::AesSuccessActionDataDecrypted;
use crate::lnurl::pay::model::LnUrlPayErrorData;
use crate::lnurl::pay::model::LnUrlPayResult;
use crate::lnurl::pay::model::LnUrlPaySuccessData;
use crate::lnurl::pay::model::MessageSuccessActionData;
use crate::lnurl::pay::model::SuccessActionProcessed;
use crate::lnurl::pay::model::UrlSuccessActionData;
Expand Down Expand Up @@ -1196,6 +1198,22 @@ impl rust2dart::IntoIntoDart<LnUrlErrorData> for LnUrlErrorData {
}
}

impl support::IntoDart for LnUrlPayErrorData {
fn into_dart(self) -> support::DartAbi {
vec![
self.payment_hash.into_into_dart().into_dart(),
self.reason.into_into_dart().into_dart(),
]
.into_dart()
}
}
impl support::IntoDartExceptPrimitive for LnUrlPayErrorData {}
impl rust2dart::IntoIntoDart<LnUrlPayErrorData> for LnUrlPayErrorData {
fn into_into_dart(self) -> Self {
self
}
}

impl support::IntoDart for LnUrlPayRequestData {
fn into_dart(self) -> support::DartAbi {
vec![
Expand All @@ -1220,8 +1238,11 @@ impl rust2dart::IntoIntoDart<LnUrlPayRequestData> for LnUrlPayRequestData {
impl support::IntoDart for LnUrlPayResult {
fn into_dart(self) -> support::DartAbi {
match self {
Self::EndpointSuccess { data } => vec![0.into_dart(), data.into_dart()],
Self::EndpointSuccess { data } => {
vec![0.into_dart(), data.into_into_dart().into_dart()]
}
Self::EndpointError { data } => vec![1.into_dart(), data.into_into_dart().into_dart()],
Self::PayError { data } => vec![2.into_dart(), data.into_into_dart().into_dart()],
}
.into_dart()
}
Expand All @@ -1233,6 +1254,22 @@ impl rust2dart::IntoIntoDart<LnUrlPayResult> for LnUrlPayResult {
}
}

impl support::IntoDart for LnUrlPaySuccessData {
fn into_dart(self) -> support::DartAbi {
vec![
self.payment_hash.into_into_dart().into_dart(),
self.success_action.into_dart(),
]
.into_dart()
}
}
impl support::IntoDartExceptPrimitive for LnUrlPaySuccessData {}
impl rust2dart::IntoIntoDart<LnUrlPaySuccessData> for LnUrlPaySuccessData {
fn into_into_dart(self) -> Self {
self
}
}

impl support::IntoDart for LnUrlWithdrawRequestData {
fn into_dart(self) -> support::DartAbi {
vec![
Expand Down
8 changes: 8 additions & 0 deletions libs/sdk-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,14 @@ impl From<InvoiceError> for SendPaymentError {
}
}

impl From<InvoiceError> for LnUrlPayError {
fn from(err: InvoiceError) -> Self {
Self::InvalidInvoice {
err: err.to_string(),
}
}
}

impl From<NodeError> for SendPaymentError {
fn from(value: NodeError) -> Self {
match value {
Expand Down
120 changes: 102 additions & 18 deletions libs/sdk-core/src/lnurl/pay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,26 @@ pub(crate) mod model {
///
/// * `EndpointError` indicates a generic issue the LNURL endpoint encountered, including a freetext
/// field with the reason.
///
/// * `PayError` indicates that an error occurred while trying to pay the invoice from the LNURL endpoint.
/// This includes the payment hash of the failed invoice and the failure reason.
#[derive(Debug, Serialize, Deserialize)]
pub enum LnUrlPayResult {
EndpointSuccess {
data: Option<SuccessActionProcessed>,
},
EndpointError {
data: LnUrlErrorData,
},
EndpointSuccess { data: LnUrlPaySuccessData },
EndpointError { data: LnUrlErrorData },
PayError { data: LnUrlPayErrorData },
}

#[derive(Serialize, Deserialize, Debug)]
pub struct LnUrlPayErrorData {
pub payment_hash: String,
pub reason: String,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct LnUrlPaySuccessData {
pub payment_hash: String,
pub success_action: Option<SuccessActionProcessed>,
}

#[derive(Deserialize, Debug)]
Expand Down Expand Up @@ -363,13 +375,13 @@ mod tests {

use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use anyhow::{anyhow, Result};
use bitcoin::hashes::hex::ToHex;
use gl_client::signer::model::greenlight::PayStatus;

use crate::{test_utils::*, LnUrlPayRequest};
use mockito::Mock;
use rand::random;

use crate::{test_utils::*, LnUrlPayRequest};

struct LnurlPayCallbackParams<'a> {
pay_req: &'a LnUrlPayRequestData,
user_amount_msat: u64,
Expand Down Expand Up @@ -848,10 +860,20 @@ mod tests {
})
.await?
{
LnUrlPayResult::EndpointSuccess { data: None } => Ok(()),
LnUrlPayResult::EndpointSuccess { data: Some(_) } => {
Err(anyhow!("Unexpected success action"))
}
LnUrlPayResult::EndpointSuccess {
data:
LnUrlPaySuccessData {
success_action: None,
..
},
} => Ok(()),
LnUrlPayResult::EndpointSuccess {
data:
LnUrlPaySuccessData {
success_action: Some(_),
..
},
} => Err(anyhow!("Unexpected success action")),
_ => Err(anyhow!("Unexpected success action type")),
}
}
Expand Down Expand Up @@ -886,6 +908,38 @@ mod tests {
Ok(())
}

#[tokio::test]
async fn test_lnurl_pay_success_payment_hash() -> Result<()> {
let comment = rand_string(COMMENT_LENGHT as usize);
let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGHT);
let temp_desc = pay_req.metadata_str.clone();
let inv = rand_invoice_with_description_hash(temp_desc)?;
let user_amount_msat = inv.amount_milli_satoshis().unwrap();
let _m = mock_lnurl_pay_callback_endpoint_msg_success_action(LnurlPayCallbackParams {
pay_req: &pay_req,
user_amount_msat,
error: None,
pr: Some(inv.to_string()),
comment: comment.clone(),
})?;

let mock_breez_services = crate::breez_services::tests::breez_services().await?;
match mock_breez_services
.lnurl_pay(LnUrlPayRequest {
data: pay_req,
amount_msat: user_amount_msat,
comment: Some(comment),
})
.await?
{
LnUrlPayResult::EndpointSuccess { data } => match data.payment_hash {
s if s == inv.payment_hash().to_hex() => Ok(()),
_ => Err(anyhow!("Unexpected payment hash")),
},
_ => Err(anyhow!("Unexpected result")),
}
}

#[tokio::test]
async fn test_lnurl_pay_msg_success_action() -> Result<()> {
let comment = rand_string(COMMENT_LENGHT as usize);
Expand All @@ -910,11 +964,21 @@ mod tests {
})
.await?
{
LnUrlPayResult::EndpointSuccess { data: None } => Err(anyhow!(
LnUrlPayResult::EndpointSuccess {
data:
LnUrlPaySuccessData {
success_action: None,
..
},
} => Err(anyhow!(
"Expected success action in callback, but none provided"
)),
LnUrlPayResult::EndpointSuccess {
data: Some(SuccessActionProcessed::Message { data: msg }),
data:
LnUrlPaySuccessData {
success_action: Some(SuccessActionProcessed::Message { data: msg }),
..
},
} => match msg.message {
s if s == "test msg" => Ok(()),
_ => Err(anyhow!("Unexpected success action message content")),
Expand Down Expand Up @@ -1013,7 +1077,11 @@ mod tests {
.await?
{
LnUrlPayResult::EndpointSuccess {
data: Some(SuccessActionProcessed::Url { data: url }),
data:
LnUrlPaySuccessData {
success_action: Some(SuccessActionProcessed::Url { data: url }),
..
},
} => {
if url.url == "https://localhost/test-url" && url.description == "test description"
{
Expand All @@ -1022,7 +1090,13 @@ mod tests {
Err(anyhow!("Unexpected success action content"))
}
}
LnUrlPayResult::EndpointSuccess { data: None } => Err(anyhow!(
LnUrlPayResult::EndpointSuccess {
data:
LnUrlPaySuccessData {
success_action: None,
..
},
} => Err(anyhow!(
"Expected success action in callback, but none provided"
)),
_ => Err(anyhow!("Unexpected success action type")),
Expand Down Expand Up @@ -1085,14 +1159,24 @@ mod tests {
.await?
{
LnUrlPayResult::EndpointSuccess {
data: Some(received_sa),
data:
LnUrlPaySuccessData {
success_action: Some(received_sa),
..
},
} => match received_sa == sa {
true => Ok(()),
false => Err(anyhow!(
"Decrypted payload and description doesn't match expected success action"
)),
},
LnUrlPayResult::EndpointSuccess { data: None } => Err(anyhow!(
LnUrlPayResult::EndpointSuccess {
data:
LnUrlPaySuccessData {
success_action: None,
..
},
} => Err(anyhow!(
"Expected success action in callback, but none provided"
)),
_ => Err(anyhow!("Unexpected success action type")),
Expand Down
Loading

0 comments on commit 6b1b0d5

Please sign in to comment.