Skip to content

Commit

Permalink
offers: validate offer amount user input
Browse files Browse the repository at this point in the history
  • Loading branch information
orbitalturtle committed Jan 18, 2024
1 parent 5e17eda commit 7717ebe
Showing 1 changed file with 102 additions and 1 deletion.
103 changes: 102 additions & 1 deletion src/lndk_offers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use bitcoin::secp256k1::{Error as Secp256k1Error, PublicKey};
use futures::executor::block_on;
use lightning::offers::invoice_request::{InvoiceRequest, UnsignedInvoiceRequest};
use lightning::offers::merkle::SignError;
use lightning::offers::offer::Offer;
use lightning::offers::offer::{Amount, Offer};
use lightning::offers::parse::{Bolt12ParseError, Bolt12SemanticError};
use std::error::Error;
use std::fmt::Display;
Expand All @@ -20,22 +20,36 @@ use tonic_lnd::Client;
#[derive(Debug)]
/// OfferError is an error that occurs during the process of paying an offer.
pub enum OfferError<Secp256k1Error> {
/// AlreadyProcessing indicates that we're already in the process of paying an offer.
AlreadyProcessing,
/// BuildUIRFailure indicates a failure to build the unsigned invoice request.
BuildUIRFailure(Bolt12SemanticError),
/// SignError indicates a failure to sign the invoice request.
SignError(SignError<Secp256k1Error>),
/// DeriveKeyFailure indicates a failure to derive key for signing the invoice request.
DeriveKeyFailure(Status),
/// User provided an invalid amount.
InvalidAmount(String),
/// Invalid currency contained in the offer.
InvalidCurrency,
/// Unable to connect to peer.
PeerConnectError(Status),
}

impl Display for OfferError<Secp256k1Error> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OfferError::AlreadyProcessing => {
write!(f, "LNDK is already trying to pay for provided offer")
}
OfferError::BuildUIRFailure(e) => write!(f, "Error building invoice request: {e:?}"),
OfferError::SignError(e) => write!(f, "Error signing invoice request: {e:?}"),
OfferError::DeriveKeyFailure(e) => write!(f, "Error signing invoice request: {e:?}"),
OfferError::InvalidAmount(e) => write!(f, "User provided an invalid amount: {e:?}"),
OfferError::InvalidCurrency => write!(
f,
"LNDK doesn't yet support offer currencies other than bitcoin"
),
OfferError::PeerConnectError(e) => write!(f, "Error connecting to peer: {e:?}"),
}
}
Expand All @@ -48,6 +62,53 @@ pub fn decode(offer_str: String) -> Result<Offer, Bolt12ParseError> {
offer_str.parse::<Offer>()
}

// Checks that the user-provided amount matches the offer.
pub async fn validate_amount(
offer: &Offer,
amount_msats: Option<u64>,
) -> Result<(), OfferError<bitcoin::secp256k1::Error>> {
match offer.amount() {
Some(offer_amount) => {
match *offer_amount {
Amount::Bitcoin {
amount_msats: bitcoin_amt,
} => {
if let Some(msats) = amount_msats {
if msats < bitcoin_amt {
return Err(OfferError::InvalidAmount(format!(
"{msats} is less than offer amount {}",
bitcoin_amt
)));
}
msats
} else {
// If user didn't set amount, set it to the offer amount.
if bitcoin_amt == 0 {
return Err(OfferError::InvalidAmount(
"Offer doesn't set an amount, so user must specify one".to_string(),
));
}
bitcoin_amt
}
}
_ => {
return Err(OfferError::InvalidCurrency);
}
}
}
None => {
if let Some(msats) = amount_msats {
msats
} else {
return Err(OfferError::InvalidAmount(
"Offer doesn't set an amount, so user must specify one".to_string(),
));
}
}
};
Ok(())
}

// connect_to_peer connects to the provided node if we're not already connected.
pub async fn connect_to_peer(
mut connector: impl PeerConnector,
Expand Down Expand Up @@ -194,13 +255,31 @@ impl MessageSigner for Client {
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey};
use lightning::offers::offer::{OfferBuilder, Quantity};
use mockall::mock;
use std::str::FromStr;
use std::time::{Duration, SystemTime};

fn get_offer() -> String {
"lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcgqgn3qzsyvfkx26qkyypvr5hfx60h9w9k934lt8s2n6zc0wwtgqlulw7dythr83dqx8tzumg".to_string()
}

fn build_custom_offer(amount_msats: u64) -> Offer {
let secp_ctx = Secp256k1::new();
let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
let pubkey = PublicKey::from(keys);

let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60);
OfferBuilder::new("coffee".to_string(), pubkey)
.amount_msats(amount_msats)
.supported_quantity(Quantity::Unbounded)
.absolute_expiry(expiration.duration_since(SystemTime::UNIX_EPOCH).unwrap())
.issuer("Foo Bar".to_string())
.build()
.unwrap()
}

fn get_pubkey() -> String {
"0313ba7ccbd754c117962b9afab6c2870eb3ef43f364a9f6c43d0fabb4553776ba".to_string()
}
Expand Down Expand Up @@ -304,6 +383,28 @@ mod tests {
)
}

#[tokio::test]
async fn test_validate_amount() {
// If the amount the user provided is greater than the offer-provided amount, then
// we should be good.
let offer = build_custom_offer(20000);
assert!(validate_amount(&offer, Some(20000)).await.is_ok());

let offer = build_custom_offer(0);
assert!(validate_amount(&offer, Some(20000)).await.is_ok());
}

#[tokio::test]
async fn test_validate_invalid_amount() {
// If the amount the user provided is lower than the offer amount, we error.
let offer = build_custom_offer(20000);
assert!(validate_amount(&offer, Some(1000)).await.is_err());

// Both user amount and offer amount can't be 0.
let offer = build_custom_offer(0);
assert!(validate_amount(&offer, None).await.is_err());
}

#[tokio::test]
async fn test_connect_peer() {
let mut connector_mock = MockTestPeerConnector::new();
Expand Down

0 comments on commit 7717ebe

Please sign in to comment.