Skip to content

Commit bd0c110

Browse files
authored
Merge pull request #101 from bitwalt/rgb-zero-amount-sendpayment-fix
add support for RGB zero asset amount invoices
2 parents 73d676b + 86cd708 commit bd0c110

File tree

7 files changed

+214
-7
lines changed

7 files changed

+214
-7
lines changed

openapi.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2131,6 +2131,15 @@ components:
21312131
invoice:
21322132
type: string
21332133
example: lnbcrt30u1pjv6yzndqud3jxktt5w46x7unfv9kz6mn0v3jsnp4qdpc280eur52luxppv6f3nnj8l6vnd9g2hnv3qv6mjhmhvlzf6327pp5tjjasx6g9dqptea3fhm6yllq5wxzycnnvp8l6wcq3d6j2uvpryuqsp5l8az8x3g8fe05dg7cmgddld3da09nfjvky8xftwsk4cj8p2l7kfq9qyysgqcqpcxqzdylzlwfnkyw3jv344x4rzwgkk53ng0fhxy5rdduk4g5tpvea8xa6rfckkza35va28xjn2tqkhgarcxep5umm4x5k56wfcdvu95eq7qzp20vrl4xz76syapsa3c09j7lg5gerkaj63llj0ark7ph8hfketn6fkqzm8laf66dhsncm23wkwm5l5377we9e8lnlknnkwje5eefkccusqm6rqt8
2134+
amt_msat:
2135+
type: integer
2136+
example: 3000000
2137+
asset_id:
2138+
type: string
2139+
example: rgb:CJkb4YZw-jRiz2sk-~PARPio-wtVYI1c-XAEYCqO-wTfvRZ8
2140+
asset_amount:
2141+
type: integer
2142+
example: 100
21342143
SendPaymentResponse:
21352144
type: object
21362145
properties:

src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ pub enum APIError {
8989
#[error("Failed to send onion message: {0}")]
9090
FailedSendingOnionMessage(String),
9191

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

9595
#[error("Not enough assets")]

src/routes.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,8 @@ pub(crate) struct SendOnionMessageRequest {
985985
pub(crate) struct SendPaymentRequest {
986986
pub(crate) invoice: String,
987987
pub(crate) amt_msat: Option<u64>,
988+
pub(crate) asset_id: Option<String>,
989+
pub(crate) asset_amount: Option<u64>,
988990
}
989991

990992
#[derive(Deserialize, Serialize)]
@@ -3553,17 +3555,30 @@ pub(crate) async fn send_payment(
35533555
(Some(rgb_contract_id), Some(rgb_amount)) => {
35543556
if amt_msat < INVOICE_MIN_MSAT {
35553557
return Err(APIError::InvalidAmount(format!(
3556-
"msat amount in invoice sending an RGB asset cannot be less than {INVOICE_MIN_MSAT}"
3558+
"amt_msat in invoice sending an RGB asset cannot be less than {INVOICE_MIN_MSAT}"
35573559
)));
35583560
}
35593561
Some((rgb_contract_id, rgb_amount))
35603562
},
3561-
(None, None) => None,
3562-
(Some(_), None) => {
3563-
return Err(APIError::InvalidInvoice(s!(
3564-
"invoice has an RGB contract ID but not an RGB amount"
3565-
)))
3563+
(Some(rgb_contract_id), None) => {
3564+
if amt_msat < INVOICE_MIN_MSAT {
3565+
return Err(APIError::InvalidAmount(format!(
3566+
"amt_msat in invoice sending an RGB asset cannot be less than {INVOICE_MIN_MSAT}"
3567+
)));
3568+
}
3569+
if let Some(asset_id) = payload.asset_id.as_ref() {
3570+
let payload_contract_id = ContractId::from_str(asset_id)
3571+
.map_err(|_| APIError::InvalidAssetID(asset_id.clone()))?;
3572+
if payload_contract_id != rgb_contract_id {
3573+
return Err(APIError::InvalidInvoice(s!(
3574+
"invoice RGB contract ID doesn't match the requested one"
3575+
)));
3576+
}
3577+
}
3578+
let rgb_amount = payload.asset_amount.ok_or(APIError::IncompleteRGBInfo)?;
3579+
Some((rgb_contract_id, rgb_amount))
35663580
}
3581+
(None, None) => None,
35673582
(None, Some(_)) => {
35683583
return Err(APIError::InvalidInvoice(s!(
35693584
"invoice has an RGB amount but not an RGB contract ID"

src/test/concurrent_btc_payments.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ async fn concurrent_btc_payments() {
7373
let payload_1 = SendPaymentRequest {
7474
invoice: invoice_1.clone(),
7575
amt_msat: None,
76+
asset_id: None,
77+
asset_amount: None,
7678
};
7779
let res_1 = reqwest::Client::new()
7880
.post(format!("http://{node3_addr}/sendpayment"))
@@ -86,6 +88,8 @@ async fn concurrent_btc_payments() {
8688
let payload_2 = SendPaymentRequest {
8789
invoice: invoice_2.clone(),
8890
amt_msat: None,
91+
asset_id: None,
92+
asset_amount: None,
8993
};
9094
let res_2 = reqwest::Client::new()
9195
.post(format!("http://{node4_addr}/sendpayment"))

src/test/invoice.rs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ async fn zero_amount_invoice() {
121121
let payload = SendPaymentRequest {
122122
invoice: invoice.clone(),
123123
amt_msat: Some(payment_amount),
124+
asset_id: None,
125+
asset_amount: None,
124126
};
125127
let res = reqwest::Client::new()
126128
.post(format!("http://{node1_addr}/sendpayment"))
@@ -158,4 +160,177 @@ async fn zero_amount_invoice() {
158160
"Receiver payment should have the amount that was received, not zero"
159161
);
160162
assert_eq!(payment_receiver.status, HTLCStatus::Succeeded);
163+
164+
// Also cover RGB invoice payment where RGB amount is provided at send time.
165+
let asset_id = issue_asset_nia(node1_addr).await.asset_id;
166+
open_channel(
167+
node1_addr,
168+
&node2_pubkey,
169+
Some(NODE2_PEER_PORT),
170+
None,
171+
Some(3_500_000),
172+
Some(600),
173+
Some(&asset_id),
174+
)
175+
.await;
176+
177+
let payload = LNInvoiceRequest {
178+
amt_msat: Some(3_000_000),
179+
expiry_sec: 900,
180+
asset_id: Some(asset_id.clone()),
181+
asset_amount: None,
182+
};
183+
let invoice_without_amount = reqwest::Client::new()
184+
.post(format!("http://{node2_addr}/lninvoice"))
185+
.json(&payload)
186+
.send()
187+
.await
188+
.unwrap()
189+
.json::<LNInvoiceResponse>()
190+
.await
191+
.unwrap()
192+
.invoice;
193+
194+
let decoded_without_amount = decode_ln_invoice(node1_addr, &invoice_without_amount).await;
195+
assert_eq!(decoded_without_amount.asset_id, Some(asset_id.clone()));
196+
assert_eq!(decoded_without_amount.asset_amount, None);
197+
198+
// If the RGB invoice already includes asset_id and asset_amount, sendpayment can omit both.
199+
let payload = LNInvoiceRequest {
200+
amt_msat: Some(3_000_000),
201+
expiry_sec: 900,
202+
asset_id: Some(asset_id.clone()),
203+
asset_amount: Some(50),
204+
};
205+
let invoice_with_amount = reqwest::Client::new()
206+
.post(format!("http://{node2_addr}/lninvoice"))
207+
.json(&payload)
208+
.send()
209+
.await
210+
.unwrap()
211+
.json::<LNInvoiceResponse>()
212+
.await
213+
.unwrap()
214+
.invoice;
215+
216+
let decoded_with_amount = decode_ln_invoice(node1_addr, &invoice_with_amount).await;
217+
assert_eq!(decoded_with_amount.asset_id, Some(asset_id.clone()));
218+
assert_eq!(decoded_with_amount.asset_amount, Some(50));
219+
220+
let payload = SendPaymentRequest {
221+
invoice: invoice_with_amount,
222+
amt_msat: Some(3_000_000),
223+
asset_id: None,
224+
asset_amount: None,
225+
};
226+
let res = reqwest::Client::new()
227+
.post(format!("http://{node1_addr}/sendpayment"))
228+
.json(&payload)
229+
.send()
230+
.await
231+
.unwrap()
232+
.json::<SendPaymentResponse>()
233+
.await
234+
.unwrap();
235+
wait_for_ln_payment(
236+
node2_addr,
237+
&res.payment_hash.unwrap(),
238+
HTLCStatus::Succeeded,
239+
)
240+
.await;
241+
let payment = get_payment(node2_addr, &decoded_with_amount.payment_hash).await;
242+
assert_eq!(payment.asset_id, Some(asset_id.clone()));
243+
assert_eq!(payment.asset_amount, Some(50));
244+
245+
// Attempting to pay without both RGB fields should fail.
246+
let payload = SendPaymentRequest {
247+
invoice: invoice_without_amount.clone(),
248+
amt_msat: Some(3_000_000),
249+
asset_id: None,
250+
asset_amount: None,
251+
};
252+
let res = reqwest::Client::new()
253+
.post(format!("http://{node1_addr}/sendpayment"))
254+
.json(&payload)
255+
.send()
256+
.await
257+
.unwrap();
258+
check_response_is_nok(
259+
res,
260+
reqwest::StatusCode::BAD_REQUEST,
261+
"both the asset ID and amount are necessary",
262+
"IncompleteRGBInfo",
263+
)
264+
.await;
265+
266+
// Providing an invalid RGB asset_id format should fail.
267+
let payload = SendPaymentRequest {
268+
invoice: invoice_without_amount.clone(),
269+
amt_msat: Some(3_000_000),
270+
asset_id: Some(s!("not-a-valid-contract-id")),
271+
asset_amount: Some(100),
272+
};
273+
let res = reqwest::Client::new()
274+
.post(format!("http://{node1_addr}/sendpayment"))
275+
.json(&payload)
276+
.send()
277+
.await
278+
.unwrap();
279+
check_response_is_nok(
280+
res,
281+
reqwest::StatusCode::BAD_REQUEST,
282+
"Invalid asset ID",
283+
"InvalidAssetID",
284+
)
285+
.await;
286+
287+
// Providing a different but valid RGB asset_id should fail with contract mismatch.
288+
let other_asset_id = issue_asset_nia(node1_addr).await.asset_id;
289+
let payload = SendPaymentRequest {
290+
invoice: invoice_without_amount.clone(),
291+
amt_msat: Some(3_000_000),
292+
asset_id: Some(other_asset_id),
293+
asset_amount: Some(100),
294+
};
295+
let res = reqwest::Client::new()
296+
.post(format!("http://{node1_addr}/sendpayment"))
297+
.json(&payload)
298+
.send()
299+
.await
300+
.unwrap();
301+
check_response_is_nok(
302+
res,
303+
reqwest::StatusCode::BAD_REQUEST,
304+
"contract ID doesn't match the requested one",
305+
"InvalidInvoice",
306+
)
307+
.await;
308+
309+
// Providing the RGB fields in sendpayment should succeed.
310+
let payload = SendPaymentRequest {
311+
invoice: invoice_without_amount,
312+
amt_msat: Some(3_000_000),
313+
asset_id: Some(asset_id),
314+
asset_amount: Some(100),
315+
};
316+
let res = reqwest::Client::new()
317+
.post(format!("http://{node1_addr}/sendpayment"))
318+
.json(&payload)
319+
.send()
320+
.await
321+
.unwrap();
322+
assert_eq!(
323+
res.status(),
324+
reqwest::StatusCode::OK,
325+
"paying RGB invoice by providing asset_amount in sendpayment should succeed"
326+
);
327+
let res = res.json::<SendPaymentResponse>().await.unwrap();
328+
wait_for_ln_payment(
329+
node2_addr,
330+
&res.payment_hash.unwrap(),
331+
HTLCStatus::Succeeded,
332+
)
333+
.await;
334+
let payment = get_payment(node2_addr, &decoded_without_amount.payment_hash).await;
335+
assert_eq!(payment.asset_amount, Some(100));
161336
}

src/test/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,6 +1416,8 @@ async fn send_payment_raw(node_address: SocketAddr, invoice: String) -> SendPaym
14161416
let payload = SendPaymentRequest {
14171417
invoice,
14181418
amt_msat: None,
1419+
asset_id: None,
1420+
asset_amount: None,
14191421
};
14201422
let res = reqwest::Client::new()
14211423
.post(format!("http://{node_address}/sendpayment"))

src/test/payment.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@ async fn same_invoice_twice_and_expired_inbound_payments() {
257257
let payload = SendPaymentRequest {
258258
invoice: invoice.clone(),
259259
amt_msat: None,
260+
asset_id: None,
261+
asset_amount: None,
260262
};
261263
let res = reqwest::Client::new()
262264
.post(format!("http://{node1_addr}/sendpayment"))

0 commit comments

Comments
 (0)