Skip to content

Commit

Permalink
Merge pull request #784 from breez/ok300-fix-swap-issue-invoice-known
Browse files Browse the repository at this point in the history
Handle `redeem_swap` error in case the invoice is known
  • Loading branch information
ok300 authored Feb 12, 2024
2 parents f7d89c4 + 01a90d9 commit 5ce2d1b
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 62 deletions.
1 change: 1 addition & 0 deletions libs/sdk-core/src/breez_services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1804,6 +1804,7 @@ impl BreezServicesBuilder {

let btc_receive_swapper = Arc::new(BTCReceiveSwap::new(
self.config.network.into(),
unwrapped_node_api.clone(),
self.swapper_api
.clone()
.unwrap_or_else(|| breez_server.clone()),
Expand Down
80 changes: 80 additions & 0 deletions libs/sdk-core/src/greenlight/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,84 @@ pub(crate) fn parse_cln_error(status: tonic::Status) -> Result<JsonRpcErrCode> {
.map_or(None, JsonRpcErrCode::from_repr)
})
.ok_or(anyhow!("No code found"))
.or(parse_cln_error_wrapped(status))
}

/// Try to parse and extract the status code from nested tonic statuses.
///
/// ```ignore
/// Example: Generic: Generic: status: Internal, message: \"converting invoice response to grpc:
/// error calling RPC: RPC error response: RpcError { code: 901, message: \\\"preimage already used\\\",
/// data: None }\", details: [], metadata: MetadataMap { headers: {\"content-type\": \"application/grpc\",
/// \"date\": \"Thu, 08 Feb 2024 20:57:17 GMT\", \"content-length\": \"0\"} }
/// ```
///
/// The [tonic::Status] is nested into an [tonic::Code::Internal] one here:
/// <https://github.com/Blockstream/greenlight/blob/e87f60e473edf9395631086c48ba6234c0c052ff/libs/gl-plugin/src/node/wrapper.rs#L90-L93>
pub(crate) fn parse_cln_error_wrapped(status: tonic::Status) -> Result<JsonRpcErrCode> {
let re: Regex = Regex::new(r"code: (?<code>-?\d+)")?;
re.captures(status.message())
.and_then(|caps| {
caps["code"]
.parse::<i16>()
.map_or(None, JsonRpcErrCode::from_repr)
})
.ok_or(anyhow!("No code found"))
}

#[cfg(test)]
mod tests {
use anyhow::Result;
use tonic::Code;

use crate::greenlight::error::{parse_cln_error, parse_cln_error_wrapped, JsonRpcErrCode};

#[test]
fn test_parse_cln_error() -> Result<()> {
assert!(parse_cln_error(tonic::Status::new(Code::Internal, "...")).is_err());

assert!(matches!(
parse_cln_error(tonic::Status::new(Code::Internal, "... Some(208) ...")),
Ok(JsonRpcErrCode::PayNoSuchPayment)
));

assert!(matches!(
parse_cln_error(tonic::Status::new(Code::Internal, "... Some(901) ...")),
Ok(JsonRpcErrCode::InvoicePreimageAlreadyExists)
));

// Test if it falls back to parsing the nested status
assert!(matches!(
parse_cln_error(tonic::Status::new(
Code::Internal,
"... RpcError { code: 901, message ... } ..."
)),
Ok(JsonRpcErrCode::InvoicePreimageAlreadyExists)
));

Ok(())
}

#[test]
fn test_parse_cln_error_wrapped() -> Result<()> {
assert!(parse_cln_error_wrapped(tonic::Status::new(Code::Internal, "...")).is_err());

assert!(matches!(
parse_cln_error_wrapped(tonic::Status::new(
Code::Internal,
"... RpcError { code: 208, message ... } ..."
)),
Ok(JsonRpcErrCode::PayNoSuchPayment)
));

assert!(matches!(
parse_cln_error_wrapped(tonic::Status::new(
Code::Internal,
"... RpcError { code: 901, message ... } ..."
)),
Ok(JsonRpcErrCode::InvoicePreimageAlreadyExists)
));

Ok(())
}
}
19 changes: 19 additions & 0 deletions libs/sdk-core/src/greenlight/node_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,25 @@ impl NodeAPI for Greenlight {
Ok(res.bolt11)
}

async fn fetch_bolt11(&self, payment_hash: Vec<u8>) -> NodeResult<Option<String>> {
let request = cln::ListinvoicesRequest {
payment_hash: Some(payment_hash),
..Default::default()
};

let found_bolt11 = self
.get_node_client()
.await?
.list_invoices(request)
.await?
.into_inner()
.invoices
.first()
.cloned()
.and_then(|res| res.bolt11);
Ok(found_bolt11)
}

// implement pull changes from greenlight
async fn pull_changed(
&self,
Expand Down
6 changes: 4 additions & 2 deletions libs/sdk-core/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,12 @@ pub struct Swap {
pub bitcoin_address: String,
pub swapper_pubkey: Vec<u8>,
pub lock_height: i64,
pub max_allowed_deposit: i64,
pub error_message: String,
pub required_reserve: i64,
/// Minimum amount, in sats, that should be sent to `bitcoin_address` for a successful swap
pub min_allowed_deposit: i64,
/// Maximum amount, in sats, that should be sent to `bitcoin_address` for a successful swap
pub max_allowed_deposit: i64,
}

/// Trait covering functionality involving swaps
Expand Down Expand Up @@ -1243,7 +1245,7 @@ pub struct SwapInfo {
pub public_key: Vec<u8>,
/// The public key in binary format from the swapping service. Received from [SwapperAPI::create_swap].
pub swapper_public_key: Vec<u8>,
/// The lockingsscript for the generated bitcoin address. Received from [SwapperAPI::create_swap].
/// The locking script for the generated bitcoin address. Received from [SwapperAPI::create_swap].
pub script: Vec<u8>,

/// bolt11 invoice to claim the sent funds.
Expand Down
2 changes: 2 additions & 0 deletions libs/sdk-core/src/node_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ pub trait NodeAPI: Send + Sync {
expiry: Option<u32>,
cltv: Option<u32>,
) -> NodeResult<String>;
/// Fetches an existing BOLT11 invoice from the node
async fn fetch_bolt11(&self, payment_hash: Vec<u8>) -> NodeResult<Option<String>>;
async fn pull_changed(
&self,
since_timestamp: u64,
Expand Down
126 changes: 67 additions & 59 deletions libs/sdk-core/src/swap_in/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use anyhow::{anyhow, Result};
use rand::Rng;
use ripemd::{Digest, Ripemd160};

use crate::binding::parse_invoice;
use crate::bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR;
use crate::bitcoin::blockdata::opcodes;
use crate::bitcoin::blockdata::script::Builder;
Expand All @@ -19,8 +18,10 @@ use crate::bitcoin::{
};
use crate::breez_services::{BreezEvent, BreezServer, PaymentReceiver, Receiver};
use crate::chain::{get_utxos, AddressUtxos, ChainService, MempoolSpace, OnchainTx};
use crate::error::ReceivePaymentError;
use crate::grpc::{AddFundInitRequest, GetSwapPaymentRequest};
use crate::models::{Swap, SwapInfo, SwapStatus, SwapperAPI};
use crate::node_api::NodeAPI;
use crate::swap_in::error::SwapError;
use crate::{
OpeningFeeParams, PrepareRefundRequest, PrepareRefundResponse, ReceivePaymentRequest,
Expand Down Expand Up @@ -61,19 +62,25 @@ impl SwapperAPI for BreezServer {
let req = GetSwapPaymentRequest {
payment_request: bolt11,
};
self.get_fund_manager_client()
let resp = self
.get_fund_manager_client()
.await?
.get_swap_payment(req)
.await?
.into_inner();
Ok(())

match resp.swap_error() {
crate::grpc::get_swap_payment_reply::SwapError::NoError => Ok(()),
err => Err(anyhow!("Failed to complete swap: {}", err.as_str_name())),
}
}
}

/// This struct is responsible for handling on-chain funds with lightning payments.
/// It uses internally an implementation of SwapperAPI that represents the actually swapper service.
pub(crate) struct BTCReceiveSwap {
network: crate::bitcoin::Network,
node_api: Arc<dyn NodeAPI>,
swapper_api: Arc<dyn SwapperAPI>,
persister: Arc<crate::persist::db::SqliteStorage>,
chain_service: Arc<dyn ChainService>,
Expand All @@ -83,13 +90,15 @@ pub(crate) struct BTCReceiveSwap {
impl BTCReceiveSwap {
pub(crate) fn new(
network: crate::bitcoin::Network,
node_api: Arc<dyn NodeAPI>,
swapper_api: Arc<dyn SwapperAPI>,
persister: Arc<crate::persist::db::SqliteStorage>,
chain_service: Arc<MempoolSpace>,
payment_receiver: Arc<PaymentReceiver>,
) -> Self {
Self {
network,
node_api,
swapper_api,
persister,
chain_service,
Expand Down Expand Up @@ -276,24 +285,16 @@ impl BTCReceiveSwap {
// redeem swaps
let redeemable_swaps = self.list_redeemables()?;
for s in redeemable_swaps {
let redeem_res = self.redeem_swap(s.bitcoin_address.clone()).await;

if redeem_res.is_err() {
let err = redeem_res.as_ref().err().unwrap();
error!(
"failed to redeem swap {:?}: {} {}",
err,
s.bitcoin_address,
s.bolt11.unwrap_or_default(),
);
self.persister
.update_swap_redeem_error(s.bitcoin_address, err.to_string())?;
} else {
info!(
"succeed to redeem swap {:?}: {}",
s.bitcoin_address,
s.bolt11.unwrap_or_default()
)
let swap_address = s.bitcoin_address;
let bolt11 = s.bolt11.unwrap_or_default();

match self.redeem_swap(swap_address.clone()).await {
Ok(_) => info!("succeed to redeem swap {swap_address}: {bolt11}"),
Err(err) => {
error!("failed to redeem swap {err:?}: {swap_address} {bolt11}");
self.persister
.update_swap_redeem_error(swap_address, err.to_string())?;
}
}
}

Expand Down Expand Up @@ -393,49 +394,55 @@ impl BTCReceiveSwap {
/// redeem_swap executes the final step of receiving lightning payment
/// in exchange for the on chain funds.
async fn redeem_swap(&self, bitcoin_address: String) -> Result<()> {
let mut swap_info = self
let swap_info = self
.persister
.get_swap_info_by_address(bitcoin_address.clone())?
.ok_or_else(|| anyhow!(format!("swap address {bitcoin_address} was not found")))?;

// we are creating and invoice for this swap if we didn't
// do it already
if swap_info.bolt11.is_none() {
let invoice = self
.payment_receiver
.receive_payment(ReceivePaymentRequest {
amount_msat: swap_info.confirmed_sats * 1000,
description: String::from("Bitcoin Transfer"),
preimage: Some(swap_info.preimage),
opening_fee_params: swap_info.channel_opening_fees,
use_description_hash: Some(false),
expiry: Some(SWAP_PAYMENT_FEE_EXPIRY_SECONDS),
cltv: None,
})
.await?
.ln_invoice;
self.persister
.update_swap_bolt11(bitcoin_address.clone(), invoice.bolt11)?;
swap_info = self
.persister
.get_swap_info_by_address(bitcoin_address)?
.unwrap();
}

// Making sure the invoice amount matches the on-chain amount
let payreq = swap_info.bolt11.unwrap();
let ln_invoice = parse_invoice(payreq.clone())?;
debug!("swap info confirmed = {}", swap_info.confirmed_sats);
if ln_invoice.amount_msat.unwrap() != (swap_info.confirmed_sats * 1000) {
warn!(
"invoice amount doesn't match confirmed sats {:?}",
ln_invoice.amount_msat.unwrap()
);
return Err(anyhow!("Does not match confirmed sats"));
}
let bolt11 = match swap_info.bolt11 {
Some(known_bolt11) => known_bolt11,
None => {
// No invoice known for this swap, we try to create one
let create_invoice_res = self
.payment_receiver
.receive_payment(ReceivePaymentRequest {
amount_msat: swap_info.confirmed_sats * 1_000,
description: String::from("Bitcoin Transfer"),
preimage: Some(swap_info.preimage),
opening_fee_params: swap_info.channel_opening_fees,
use_description_hash: Some(false),
expiry: Some(SWAP_PAYMENT_FEE_EXPIRY_SECONDS),
cltv: None,
})
.await;

let new_bolt11 = match create_invoice_res {
// Extract created invoice
Ok(create_invoice_response) => Ok(create_invoice_response.ln_invoice.bolt11),

// If settling the invoice failed on a different device (for example because the
// swap was initiated there), then the unsettled invoice exists on the GL node.
// Trying to create the invoice here will fail because we're using the same preimage.
// In this case, fetch the invoice from GL instead of creating it.
Err(ReceivePaymentError::InvoicePreimageAlreadyExists { .. }) => self
.node_api
.fetch_bolt11(swap_info.payment_hash)
.await?
.ok_or(anyhow!("Preimage already known, but invoice not found")),

// In all other cases: throw error
Err(err) => Err(anyhow!("Failed to create invoice: {err}")),
}?;

// If we have a new invoice, created or fetched from GL, associate it with the swap
self.persister
.update_swap_bolt11(bitcoin_address, new_bolt11.clone())?;
new_bolt11
}
};

// Asking the service to initiate the lightning payment.
self.swapper_api.complete_swap(payreq.clone()).await
self.swapper_api.complete_swap(bolt11).await
}

pub(crate) async fn prepare_refund_swap(
Expand Down Expand Up @@ -684,7 +691,7 @@ mod tests {

use crate::chain::{AddressUtxos, Utxo};
use crate::swap_in::swap::{compute_refund_tx_weight, compute_tx_fee, prepare_refund_tx};
use crate::test_utils::get_test_ofp;
use crate::test_utils::{get_test_ofp, MockNodeAPI};
use crate::{
bitcoin::consensus::deserialize,
bitcoin::hashes::{hex::FromHex, sha256},
Expand Down Expand Up @@ -1128,6 +1135,7 @@ mod tests {

let swapper = BTCReceiveSwap {
network: crate::bitcoin::Network::Bitcoin,
node_api: Arc::new(MockNodeAPI::new(get_dummy_node_state())),
swapper_api: Arc::new(MockSwapperAPI {}),
persister: persister.clone(),
chain_service: chain_service.clone(),
Expand Down
4 changes: 4 additions & 0 deletions libs/sdk-core/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,10 @@ impl NodeAPI for MockNodeAPI {
async fn get_routing_hints(&self) -> NodeResult<(Vec<RouteHint>, bool)> {
Ok((vec![], false))
}

async fn fetch_bolt11(&self, _payment_hash: Vec<u8>) -> NodeResult<Option<String>> {
Ok(None)
}
}

impl MockNodeAPI {
Expand Down
2 changes: 1 addition & 1 deletion libs/sdk-flutter/lib/bridge_generated.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1758,7 +1758,7 @@ class SwapInfo {
/// The public key in binary format from the swapping service. Received from [SwapperAPI::create_swap].
final Uint8List swapperPublicKey;

/// The lockingsscript for the generated bitcoin address. Received from [SwapperAPI::create_swap].
/// The locking script for the generated bitcoin address. Received from [SwapperAPI::create_swap].
final Uint8List script;

/// bolt11 invoice to claim the sent funds.
Expand Down

0 comments on commit 5ce2d1b

Please sign in to comment.