Skip to content
Merged
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
9 changes: 9 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2131,6 +2131,15 @@ components:
invoice:
type: string
example: lnbcrt30u1pjv6yzndqud3jxktt5w46x7unfv9kz6mn0v3jsnp4qdpc280eur52luxppv6f3nnj8l6vnd9g2hnv3qv6mjhmhvlzf6327pp5tjjasx6g9dqptea3fhm6yllq5wxzycnnvp8l6wcq3d6j2uvpryuqsp5l8az8x3g8fe05dg7cmgddld3da09nfjvky8xftwsk4cj8p2l7kfq9qyysgqcqpcxqzdylzlwfnkyw3jv344x4rzwgkk53ng0fhxy5rdduk4g5tpvea8xa6rfckkza35va28xjn2tqkhgarcxep5umm4x5k56wfcdvu95eq7qzp20vrl4xz76syapsa3c09j7lg5gerkaj63llj0ark7ph8hfketn6fkqzm8laf66dhsncm23wkwm5l5377we9e8lnlknnkwje5eefkccusqm6rqt8
amt_msat:
type: integer
example: 3000000
asset_id:
type: string
example: rgb:CJkb4YZw-jRiz2sk-~PARPio-wtVYI1c-XAEYCqO-wTfvRZ8
asset_amount:
type: integer
example: 100
SendPaymentResponse:
type: object
properties:
Expand Down
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ pub enum APIError {
#[error("Failed to send onion message: {0}")]
FailedSendingOnionMessage(String),

#[error("For an RGB operation both asset_id and asset_amount must be set")]
#[error("For an RGB operation both the asset ID and amount are necessary")]
IncompleteRGBInfo,

#[error("Not enough assets")]
Expand Down
27 changes: 21 additions & 6 deletions src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,8 @@ pub(crate) struct SendOnionMessageRequest {
pub(crate) struct SendPaymentRequest {
pub(crate) invoice: String,
pub(crate) amt_msat: Option<u64>,
pub(crate) asset_id: Option<String>,
Comment thread
nicbus marked this conversation as resolved.
pub(crate) asset_amount: Option<u64>,
}

#[derive(Deserialize, Serialize)]
Expand Down Expand Up @@ -3553,17 +3555,30 @@ pub(crate) async fn send_payment(
(Some(rgb_contract_id), Some(rgb_amount)) => {
if amt_msat < INVOICE_MIN_MSAT {
return Err(APIError::InvalidAmount(format!(
"msat amount in invoice sending an RGB asset cannot be less than {INVOICE_MIN_MSAT}"
"amt_msat in invoice sending an RGB asset cannot be less than {INVOICE_MIN_MSAT}"
)));
}
Some((rgb_contract_id, rgb_amount))
},
(None, None) => None,
(Some(_), None) => {
return Err(APIError::InvalidInvoice(s!(
"invoice has an RGB contract ID but not an RGB amount"
)))
(Some(rgb_contract_id), None) => {
if amt_msat < INVOICE_MIN_MSAT {
return Err(APIError::InvalidAmount(format!(
"amt_msat in invoice sending an RGB asset cannot be less than {INVOICE_MIN_MSAT}"
)));
}
if let Some(asset_id) = payload.asset_id.as_ref() {
let payload_contract_id = ContractId::from_str(asset_id)
.map_err(|_| APIError::InvalidAssetID(asset_id.clone()))?;
if payload_contract_id != rgb_contract_id {
return Err(APIError::InvalidInvoice(s!(
"invoice RGB contract ID doesn't match the requested one"
)));
}
}
let rgb_amount = payload.asset_amount.ok_or(APIError::IncompleteRGBInfo)?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

please update the IncompleteRGBInfo message from For an RGB operation both asset_id and asset_amount must be set to a more generic For an RGB operation both the asset ID and amount are necessary

Some((rgb_contract_id, rgb_amount))
}
(None, None) => None,
(None, Some(_)) => {
return Err(APIError::InvalidInvoice(s!(
"invoice has an RGB amount but not an RGB contract ID"
Expand Down
4 changes: 4 additions & 0 deletions src/test/concurrent_btc_payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ async fn concurrent_btc_payments() {
let payload_1 = SendPaymentRequest {
invoice: invoice_1.clone(),
amt_msat: None,
asset_id: None,
asset_amount: None,
};
let res_1 = reqwest::Client::new()
.post(format!("http://{node3_addr}/sendpayment"))
Expand All @@ -86,6 +88,8 @@ async fn concurrent_btc_payments() {
let payload_2 = SendPaymentRequest {
invoice: invoice_2.clone(),
amt_msat: None,
asset_id: None,
asset_amount: None,
};
let res_2 = reqwest::Client::new()
.post(format!("http://{node4_addr}/sendpayment"))
Expand Down
175 changes: 175 additions & 0 deletions src/test/invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ async fn zero_amount_invoice() {
let payload = SendPaymentRequest {
invoice: invoice.clone(),
amt_msat: Some(payment_amount),
asset_id: None,
asset_amount: None,
};
let res = reqwest::Client::new()
.post(format!("http://{node1_addr}/sendpayment"))
Expand Down Expand Up @@ -158,4 +160,177 @@ async fn zero_amount_invoice() {
"Receiver payment should have the amount that was received, not zero"
);
assert_eq!(payment_receiver.status, HTLCStatus::Succeeded);

// Also cover RGB invoice payment where RGB amount is provided at send time.
Comment thread
bitwalt marked this conversation as resolved.
let asset_id = issue_asset_nia(node1_addr).await.asset_id;
open_channel(
node1_addr,
&node2_pubkey,
Some(NODE2_PEER_PORT),
None,
Some(3_500_000),
Some(600),
Some(&asset_id),
)
.await;

let payload = LNInvoiceRequest {
amt_msat: Some(3_000_000),
expiry_sec: 900,
asset_id: Some(asset_id.clone()),
asset_amount: None,
};
let invoice_without_amount = reqwest::Client::new()
.post(format!("http://{node2_addr}/lninvoice"))
.json(&payload)
.send()
.await
.unwrap()
.json::<LNInvoiceResponse>()
.await
.unwrap()
.invoice;

let decoded_without_amount = decode_ln_invoice(node1_addr, &invoice_without_amount).await;
assert_eq!(decoded_without_amount.asset_id, Some(asset_id.clone()));
assert_eq!(decoded_without_amount.asset_amount, None);

// If the RGB invoice already includes asset_id and asset_amount, sendpayment can omit both.
let payload = LNInvoiceRequest {
amt_msat: Some(3_000_000),
expiry_sec: 900,
asset_id: Some(asset_id.clone()),
asset_amount: Some(50),
};
let invoice_with_amount = reqwest::Client::new()
.post(format!("http://{node2_addr}/lninvoice"))
.json(&payload)
.send()
.await
.unwrap()
.json::<LNInvoiceResponse>()
.await
.unwrap()
.invoice;

let decoded_with_amount = decode_ln_invoice(node1_addr, &invoice_with_amount).await;
assert_eq!(decoded_with_amount.asset_id, Some(asset_id.clone()));
assert_eq!(decoded_with_amount.asset_amount, Some(50));

let payload = SendPaymentRequest {
invoice: invoice_with_amount,
amt_msat: Some(3_000_000),
asset_id: None,
asset_amount: None,
};
let res = reqwest::Client::new()
.post(format!("http://{node1_addr}/sendpayment"))
.json(&payload)
.send()
.await
.unwrap()
.json::<SendPaymentResponse>()
.await
.unwrap();
wait_for_ln_payment(
node2_addr,
&res.payment_hash.unwrap(),
HTLCStatus::Succeeded,
)
.await;
let payment = get_payment(node2_addr, &decoded_with_amount.payment_hash).await;
assert_eq!(payment.asset_id, Some(asset_id.clone()));
assert_eq!(payment.asset_amount, Some(50));

// Attempting to pay without both RGB fields should fail.
let payload = SendPaymentRequest {
invoice: invoice_without_amount.clone(),
amt_msat: Some(3_000_000),
asset_id: None,
asset_amount: None,
};
let res = reqwest::Client::new()
.post(format!("http://{node1_addr}/sendpayment"))
.json(&payload)
.send()
.await
.unwrap();
check_response_is_nok(
res,
reqwest::StatusCode::BAD_REQUEST,
"both the asset ID and amount are necessary",
"IncompleteRGBInfo",
)
.await;

// Providing an invalid RGB asset_id format should fail.
let payload = SendPaymentRequest {
invoice: invoice_without_amount.clone(),
amt_msat: Some(3_000_000),
asset_id: Some(s!("not-a-valid-contract-id")),
asset_amount: Some(100),
};
let res = reqwest::Client::new()
.post(format!("http://{node1_addr}/sendpayment"))
.json(&payload)
.send()
.await
.unwrap();
check_response_is_nok(
res,
reqwest::StatusCode::BAD_REQUEST,
"Invalid asset ID",
"InvalidAssetID",
)
.await;

// Providing a different but valid RGB asset_id should fail with contract mismatch.
let other_asset_id = issue_asset_nia(node1_addr).await.asset_id;
let payload = SendPaymentRequest {
invoice: invoice_without_amount.clone(),
amt_msat: Some(3_000_000),
asset_id: Some(other_asset_id),
asset_amount: Some(100),
};
let res = reqwest::Client::new()
.post(format!("http://{node1_addr}/sendpayment"))
.json(&payload)
.send()
.await
.unwrap();
check_response_is_nok(
res,
reqwest::StatusCode::BAD_REQUEST,
"contract ID doesn't match the requested one",
"InvalidInvoice",
)
.await;

// Providing the RGB fields in sendpayment should succeed.
let payload = SendPaymentRequest {
invoice: invoice_without_amount,
amt_msat: Some(3_000_000),
asset_id: Some(asset_id),
asset_amount: Some(100),
};
let res = reqwest::Client::new()
.post(format!("http://{node1_addr}/sendpayment"))
.json(&payload)
.send()
.await
.unwrap();
assert_eq!(
res.status(),
reqwest::StatusCode::OK,
"paying RGB invoice by providing asset_amount in sendpayment should succeed"
);
let res = res.json::<SendPaymentResponse>().await.unwrap();
wait_for_ln_payment(
node2_addr,
&res.payment_hash.unwrap(),
HTLCStatus::Succeeded,
)
.await;
let payment = get_payment(node2_addr, &decoded_without_amount.payment_hash).await;
assert_eq!(payment.asset_amount, Some(100));
}
2 changes: 2 additions & 0 deletions src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1416,6 +1416,8 @@ async fn send_payment_raw(node_address: SocketAddr, invoice: String) -> SendPaym
let payload = SendPaymentRequest {
invoice,
amt_msat: None,
asset_id: None,
asset_amount: None,
};
let res = reqwest::Client::new()
.post(format!("http://{node_address}/sendpayment"))
Expand Down
2 changes: 2 additions & 0 deletions src/test/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ async fn same_invoice_twice_and_expired_inbound_payments() {
let payload = SendPaymentRequest {
invoice: invoice.clone(),
amt_msat: None,
asset_id: None,
asset_amount: None,
};
let res = reqwest::Client::new()
.post(format!("http://{node1_addr}/sendpayment"))
Expand Down
Loading