Skip to content

Commit

Permalink
add invoice get rpc and check expire
Browse files Browse the repository at this point in the history
  • Loading branch information
chenyukang committed Oct 16, 2024
1 parent 856a2ad commit 5ccb247
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 5 deletions.
13 changes: 13 additions & 0 deletions src/fiber/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,19 @@ where
let tlcs = state.get_tlcs_for_settle_down();
for tlc_info in tlcs {
let tlc = tlc_info.tlc.clone();
if let Some(invoice) = self.store.get_invoice(&tlc.payment_hash) {
if invoice.is_expired() {
let command = RemoveTlcCommand {
id: tlc.get_id(),
reason: RemoveTlcReason::RemoveTlcFail(TlcErrPacket::new(TlcErr::new(
TlcErrorCode::InvoiceExpired,
))),
};
let result = self.handle_remove_tlc_command(state, command);
info!("try to settle down tlc: {:?} result: {:?}", &tlc, &result);
}
}

let preimage = if let Some(preimage) = tlc.payment_preimage {
preimage
} else if let Some(preimage) = self.store.get_invoice_preimage(&tlc.payment_hash) {
Expand Down
6 changes: 6 additions & 0 deletions src/fiber/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,12 @@ impl SendPaymentData {
.transpose()
.map_err(|_| "invoice is invalid".to_string())?;

if let Some(invoice) = invoice.clone() {
if invoice.is_expired() {
return Err("invoice is expired".to_string());
}
}

fn validate_field<T: PartialEq + Clone>(
field: Option<T>,
invoice_field: Option<T>,
Expand Down
2 changes: 2 additions & 0 deletions src/fiber/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1420,6 +1420,7 @@ pub enum TlcErrorCode {
IncorrectCltvExpiry = UPDATE | 13,
ExpiryTooSoon = UPDATE | 14,
IncorrectOrUnknownPaymentDetails = PERM | 15,
InvoiceExpired = PERM | 16,
FinalIncorrectCltvExpiry = 18,
FinalIncorrectHtlcAmount = 19,
ChannelDisabled = UPDATE | 20,
Expand Down Expand Up @@ -1451,6 +1452,7 @@ impl TlcErrorCode {
TlcErrorCode::IncorrectOrUnknownPaymentDetails
| TlcErrorCode::FinalIncorrectCltvExpiry
| TlcErrorCode::FinalIncorrectHtlcAmount
| TlcErrorCode::InvoiceExpired
| TlcErrorCode::MppTimeout => true,
_ => false,
}
Expand Down
7 changes: 7 additions & 0 deletions src/invoice/invoice_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,13 @@ impl CkbInvoice {
&self.data.payment_hash
}

pub fn is_expired(&self) -> bool {
self.expiry_time().map_or(false, |expiry| {
self.data.timestamp + expiry.as_millis()
< std::time::UNIX_EPOCH.elapsed().unwrap().as_millis()
})
}

/// Check that the invoice is signed correctly and that key recovery works
pub fn check_signature(&self) -> Result<(), InvoiceError> {
if self.signature.is_none() {
Expand Down
15 changes: 15 additions & 0 deletions src/invoice/tests/invoice_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,3 +437,18 @@ fn test_invoice_udt_script() {
let decoded = serde_json::from_str::<CkbInvoice>(&res.unwrap()).unwrap();
assert_eq!(decoded, invoice);
}

#[test]
fn test_invoice_check_expired() {
let private_key = gen_rand_private_key();
let invoice = InvoiceBuilder::new(Currency::Fibb)
.amount(Some(1280))
.payment_hash(rand_sha256_hash())
.expiry_time(Duration::from_secs(1))
.build_with_sign(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key))
.unwrap();

assert_eq!(invoice.is_expired(), false);
std::thread::sleep(Duration::from_secs(2));
assert_eq!(invoice.is_expired(), true);
}
72 changes: 67 additions & 5 deletions src/rpc/invoice.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::time::Duration;

use crate::fiber::graph::{NetworkGraphStateStore, PaymentSessionStatus};
use crate::fiber::hash_algorithm::HashAlgorithm;
use crate::fiber::serde_utils::{U128Hex, U64Hex};
use crate::fiber::types::Hash256;
Expand Down Expand Up @@ -32,7 +33,7 @@ pub(crate) struct NewInvoiceParams {
}

#[derive(Clone, Serialize, Deserialize)]
pub(crate) struct NewInvoiceResult {
pub(crate) struct InvoiceResult {
invoice_address: String,
invoice: CkbInvoice,
}
Expand All @@ -47,19 +48,45 @@ pub(crate) struct ParseInvoiceResult {
invoice: CkbInvoice,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct GetInvoiceParams {
payment_hash: Hash256,
}

#[derive(Clone, Serialize, Deserialize)]
enum InvoiceStatus {
Unpaid,
Inflight,
Paid,
Expired,
}

#[derive(Clone, Serialize, Deserialize)]
pub(crate) struct GetInvoiceResult {
invoice_address: String,
invoice: CkbInvoice,
status: InvoiceStatus,
}

#[rpc(server)]
trait InvoiceRpc {
#[method(name = "new_invoice")]
async fn new_invoice(
&self,
params: NewInvoiceParams,
) -> Result<NewInvoiceResult, ErrorObjectOwned>;
) -> Result<InvoiceResult, ErrorObjectOwned>;

#[method(name = "parse_invoice")]
async fn parse_invoice(
&self,
params: ParseInvoiceParams,
) -> Result<ParseInvoiceResult, ErrorObjectOwned>;

#[method(name = "get_invoice")]
async fn get_invoice(
&self,
payment_hash: GetInvoiceParams,
) -> Result<GetInvoiceResult, ErrorObjectOwned>;
}

pub(crate) struct InvoiceRpcServerImpl<S> {
Expand All @@ -76,12 +103,12 @@ impl<S> InvoiceRpcServerImpl<S> {
#[async_trait]
impl<S> InvoiceRpcServer for InvoiceRpcServerImpl<S>
where
S: InvoiceStore + Send + Sync + 'static,
S: InvoiceStore + NetworkGraphStateStore + Send + Sync + 'static,
{
async fn new_invoice(
&self,
params: NewInvoiceParams,
) -> Result<NewInvoiceResult, ErrorObjectOwned> {
) -> Result<InvoiceResult, ErrorObjectOwned> {
let mut invoice_builder = InvoiceBuilder::new(params.currency)
.amount(Some(params.amount))
.payment_preimage(params.payment_preimage);
Expand Down Expand Up @@ -116,7 +143,7 @@ where
.store
.insert_invoice(invoice.clone(), Some(params.payment_preimage))
{
Ok(_) => Ok(NewInvoiceResult {
Ok(_) => Ok(InvoiceResult {
invoice_address: invoice.to_string(),
invoice,
}),
Expand Down Expand Up @@ -150,4 +177,39 @@ where
)),
}
}

async fn get_invoice(
&self,
params: GetInvoiceParams,
) -> Result<GetInvoiceResult, ErrorObjectOwned> {
let payment_hash = params.payment_hash;
match self.store.get_invoice(&payment_hash) {
Some(invoice) => {
let invoice_status = if invoice.is_expired() {
InvoiceStatus::Expired
} else {
InvoiceStatus::Unpaid
};
let payment_session = self.store.get_payment_session(payment_hash);
let status = match payment_session {
Some(session) => match session.status {
PaymentSessionStatus::Inflight => InvoiceStatus::Inflight,
PaymentSessionStatus::Success => InvoiceStatus::Paid,
_ => invoice_status,
},
None => invoice_status,
};
Ok(GetInvoiceResult {
invoice_address: invoice.to_string(),
invoice,
status,
})
}
None => Err(ErrorObjectOwned::owned(
CALL_EXECUTION_FAILED_CODE,
"invoice not found".to_string(),
Some(payment_hash),
)),
}
}
}
1 change: 1 addition & 0 deletions tests/bruno/e2e/router-pay/12-node1-send-payment.bru
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ assert {
script:post-response {
// Sleep for sometime to make sure current operation finishes before next request starts.
await new Promise(r => setTimeout(r, 100));
console.log("12 step result: ", res.body);
}
65 changes: 65 additions & 0 deletions tests/bruno/e2e/router-pay/22-node3-gen-expiring-invoice.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
meta {
name: generate a invoice which will expiring in short time
type: http
seq: 22
}

post {
url: {{NODE3_RPC_URL}}
body: json
auth: none
}

headers {
Content-Type: application/json
Accept: application/json
}

body:json {
{
"id": "42",
"jsonrpc": "2.0",
"method": "new_invoice",
"params": [
{
"amount": "0x613",
"currency": "Fibb",
"description": "test invoice generated by node3",
"expiry": "0x2",
"final_cltv": "0x28",
"payment_preimage": "{{payment_preimage}}"
}
]
}
}

assert {
res.body.error: isUndefined
res.body.result: isDefined
}

script:pre-request {
// generate random preimage
function generateRandomPreimage() {
let hash = '0x';
for (let i = 0; i < 64; i++) {
hash += Math.floor(Math.random() * 16).toString(16);
}
return hash;
}
const payment_preimage = generateRandomPreimage();
bru.setVar("payment_preimage", payment_preimage);
let hash_algorithm = bru.getEnvVar("HASH_ALGORITHM");
if (hash_algorithm !== null) {
let body = req.getBody();
body.params[0].hash_algorithm = hash_algorithm;
req.setBody(body);
}
}

script:post-response {
// Sleep for sometime to make sure current operation finishes before next request starts.
await new Promise(r => setTimeout(r, 3000));
console.log("generated result: ", res.body.result);
bru.setVar("encoded_invoice", res.body.result.invoice_address);
}
48 changes: 48 additions & 0 deletions tests/bruno/e2e/router-pay/23-node1-send-payment-will-fail.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
meta {
name: Node1 send payment with router
type: http
seq: 23
}

post {
url: {{NODE1_RPC_URL}}
body: json
auth: none
}

headers {
Content-Type: application/json
Accept: application/json
}

body:json {
{
"id": "42",
"jsonrpc": "2.0",
"method": "send_payment",
"params": [
{
"invoice": "{{encoded_invoice}}"
}
]
}
}

assert {
res.body.error: isDefined
}


script:pre-request {
// sleep for a while
await new Promise(r => setTimeout(r, 1000));
}


script:post-response {
// Sleep for sometime to make sure current operation finishes before next request starts.
await new Promise(r => setTimeout(r, 100));
if (!(res.body.error.message.includes("invoice is expired"))) {
throw new Error("Assertion failed: error message is not right");
}
}

0 comments on commit 5ccb247

Please sign in to comment.