refactor nwc_pay

This commit is contained in:
daywalker90 2025-11-09 15:15:10 +01:00
parent a462e625a3
commit ac2f4b1acd
No known key found for this signature in database
6 changed files with 313 additions and 218 deletions

View file

@ -316,7 +316,7 @@ async fn nwc_request_handler(
error: None,
result: Some(nip47::ResponseResult::PayKeysend(o)),
},
id,
Some(id),
),
Err(e) => (
nip47::Response {
@ -324,7 +324,7 @@ async fn nwc_request_handler(
error: Some(e),
result: None,
},
id,
Some(id),
),
},
]
@ -341,7 +341,7 @@ async fn nwc_request_handler(
error: None,
result: Some(nip47::ResponseResult::MakeInvoice(o)),
},
String::new(),
None,
),
Err(e) => (
nip47::Response {
@ -349,7 +349,7 @@ async fn nwc_request_handler(
error: Some(e),
result: None,
},
String::new(),
None,
),
},
]
@ -363,7 +363,7 @@ async fn nwc_request_handler(
error: None,
result: Some(nip47::ResponseResult::LookupInvoice(o)),
},
String::new(),
None,
),
Err(e) => (
nip47::Response {
@ -371,7 +371,7 @@ async fn nwc_request_handler(
error: Some(e),
result: None,
},
String::new(),
None,
),
},
]
@ -385,7 +385,7 @@ async fn nwc_request_handler(
error: None,
result: Some(nip47::ResponseResult::ListTransactions(o)),
},
String::new(),
None,
),
Err(e) => (
nip47::Response {
@ -393,7 +393,7 @@ async fn nwc_request_handler(
error: Some(e),
result: None,
},
String::new(),
None,
),
},
]
@ -406,7 +406,7 @@ async fn nwc_request_handler(
error: None,
result: Some(nip47::ResponseResult::GetBalance(o)),
},
String::new(),
None,
),
Err(e) => (
nip47::Response {
@ -414,7 +414,7 @@ async fn nwc_request_handler(
error: Some(e),
result: None,
},
String::new(),
None,
),
}]
}
@ -426,7 +426,7 @@ async fn nwc_request_handler(
error: None,
result: Some(nip47::ResponseResult::GetInfo(o)),
},
String::new(),
None,
),
Err(e) => (
nip47::Response {
@ -434,7 +434,7 @@ async fn nwc_request_handler(
error: Some(e),
result: None,
},
String::new(),
None,
),
}]
}
@ -448,7 +448,7 @@ async fn nwc_request_handler(
}),
result: None,
},
String::new(),
None,
)]
}
nip47::RequestParams::CancelHoldInvoice(_cancel_hold_invoice_request) => {
@ -461,7 +461,7 @@ async fn nwc_request_handler(
}),
result: None,
},
String::new(),
None,
)]
}
nip47::RequestParams::SettleHoldInvoice(_settle_hold_invoice_request) => {
@ -474,7 +474,7 @@ async fn nwc_request_handler(
}),
result: None,
},
String::new(),
None,
)]
}
};
@ -513,13 +513,13 @@ async fn nwc_request_handler(
let mut response_builder = EventBuilder::new(Kind::WalletConnectResponse, content)
.tag(Tag::event(event.id))
.tag(Tag::public_key(client_pubkey));
if !id.is_empty() {
if let Some(i) = id {
response_builder = response_builder.tag(Tag::custom(
TagKind::SingleLetter(SingleLetterTag {
character: Alphabet::D,
uppercase: false,
}),
vec![id],
vec![i],
));
}
let response_event = match response_builder.sign_with_keys(&wallet_keys) {

View file

@ -6,7 +6,7 @@ use crate::{structs::PluginState, util::load_nwc_store};
pub async fn get_balance(
plugin: Plugin<PluginState>,
label: &String,
label: &str,
) -> Result<nip47::GetBalanceResponse, nip47::NIP47Error> {
let mut rpc = plugin.state().rpc_lock.lock().await;

View file

@ -8,7 +8,7 @@ use nostr_sdk::nips::nip47;
pub async fn get_info(
plugin: Plugin<PluginState>,
label: &String,
label: &str,
) -> Result<nip47::GetInfoResponse, nip47::NIP47Error> {
let mut rpc = plugin.state().rpc_lock.lock().await;

View file

@ -16,7 +16,7 @@ use crate::{
pub async fn pay_keysend(
plugin: Plugin<PluginState>,
params: nip47::PayKeysendRequest,
label: &String,
label: &str,
) -> Result<nip47::PayKeysendResponse, nip47::NIP47Error> {
let mut rpc = plugin.state().rpc_lock.lock().await;
@ -122,8 +122,8 @@ pub async fn pay_keysend(
pub async fn multi_pay_keysend(
plugin: Plugin<PluginState>,
params: nip47::MultiPayKeysendRequest,
label: &String,
) -> Vec<(nip47::Response, String)> {
label: &str,
) -> Vec<(nip47::Response, Option<String>)> {
let mut responses = Vec::new();
for pay in params.keysends {
let result = pay_keysend(plugin.clone(), pay.clone(), label).await;
@ -135,7 +135,7 @@ pub async fn multi_pay_keysend(
error: None,
result: Some(nip47::ResponseResult::MultiPayKeysend(resp)),
},
id,
Some(id),
),
Err(e) => (
nip47::Response {
@ -143,7 +143,7 @@ pub async fn multi_pay_keysend(
error: Some(e),
result: None,
},
id,
Some(id),
),
};
responses.push(response_res);

View file

@ -2,26 +2,51 @@ use std::time::Duration;
use cln_plugin::Plugin;
use cln_rpc::{
model::requests::{DecodeRequest, PayRequest, XpayRequest},
primitives::Amount,
model::{
requests::{DecodeRequest, PayRequest, XpayRequest},
responses::DecodeResponse,
},
primitives::{Amount, Secret},
ClnRpc, RpcError,
};
use nostr_sdk::nips::nip47;
use tokio::time;
use crate::{
structs::PluginState,
structs::{NwcStore, PluginState, NOT_INV_ERR},
util::{at_or_above_version, budget_amount_check, load_nwc_store, update_nwc_store},
};
pub async fn pay_invoice(
plugin: Plugin<PluginState>,
params: nip47::PayInvoiceRequest,
label: &String,
) -> Result<(nip47::PayInvoiceResponse, String), (nip47::NIP47Error, String)> {
label: &str,
) -> Result<(nip47::PayInvoiceResponse, Option<String>), (nip47::NIP47Error, Option<String>)> {
let mut rpc = plugin.state().rpc_lock.lock().await;
let id = params.id.clone().unwrap_or_default();
let decoded_invoice = decode_and_validate_invoice(&mut rpc, &params).await?;
let id = get_payment_id(&params, &decoded_invoice)?;
let invoice_amt_msat = get_invoice_amount_msat(&decoded_invoice, &id)?;
let nwc_store =
load_nwc_and_check_budget(&mut rpc, label, &params, invoice_amt_msat, &id).await?;
let my_version = plugin.state().config.lock().clone().my_cln_version;
let use_xpay = check_cln_version(&my_version, &id)?;
if use_xpay {
pay_with_xpay_full(&mut rpc, params, label, nwc_store, &id).await
} else {
pay_with_legacy_full(&mut rpc, params, label, nwc_store, &id).await
}
}
async fn decode_and_validate_invoice(
rpc: &mut ClnRpc,
params: &nip47::PayInvoiceRequest,
) -> Result<DecodeResponse, (nip47::NIP47Error, Option<String>)> {
let invoice_decoded = rpc
.call_typed(&DecodeRequest {
string: params.invoice.clone(),
@ -33,47 +58,94 @@ pub async fn pay_invoice(
code: nip47::ErrorCode::Internal,
message: e.to_string(),
},
id.clone(),
params.id.clone(),
)
})?;
let not_invoice_error = Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: "Not an invoice or invalid invoice".to_owned(),
},
id.clone(),
));
if !invoice_decoded.valid {
return not_invoice_error;
return Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: NOT_INV_ERR.to_owned(),
},
params.id.clone(),
));
}
let id = if let Some(i) = params.id {
i
if !matches!(
invoice_decoded.item_type,
cln_rpc::model::responses::DecodeType::BOLT11_INVOICE
) {
return Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: NOT_INV_ERR.to_owned(),
},
params.id.clone(),
));
}
Ok(invoice_decoded)
}
fn get_payment_id(
params: &nip47::PayInvoiceRequest,
decoded_invoice: &DecodeResponse,
) -> Result<String, (nip47::NIP47Error, Option<String>)> {
let id = if let Some(i) = &params.id {
i.clone()
} else {
match invoice_decoded.item_type {
cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => {
invoice_decoded.payment_hash.unwrap().to_string()
}
_ => return not_invoice_error,
}
decoded_invoice
.payment_hash
.as_ref()
.ok_or_else(|| {
(
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: "payment_hash missing in decoded invoice".to_owned(),
},
None,
)
})?
.to_string()
};
let invoice_amt_msat = match invoice_decoded.item_type {
cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => {
invoice_decoded.amount_msat.unwrap().msat()
}
_ => return not_invoice_error,
};
Ok(id)
}
let mut nwc_store = load_nwc_store(&mut rpc, label).await.map_err(|e| {
fn get_invoice_amount_msat(
decoded_invoice: &DecodeResponse,
id: &str,
) -> Result<u64, (nip47::NIP47Error, Option<String>)> {
decoded_invoice
.amount_msat
.as_ref()
.ok_or_else(|| {
(
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: "Missing amount_msat in decoded invoice".to_owned(),
},
Some(id.to_owned()),
)
})
.map(Amount::msat)
}
async fn load_nwc_and_check_budget(
rpc: &mut ClnRpc,
label: &str,
params: &nip47::PayInvoiceRequest,
invoice_amt_msat: u64,
id: &str,
) -> Result<NwcStore, (nip47::NIP47Error, Option<String>)> {
let nwc_store = load_nwc_store(rpc, label).await.map_err(|e| {
(
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
},
id.clone(),
Some(id.to_owned()),
)
})?;
@ -84,186 +156,209 @@ pub async fn pay_invoice(
code: nip47::ErrorCode::QuotaExceeded,
message: e.to_string(),
},
id.clone(),
Some(id.to_owned()),
)
},
)?;
let my_version = plugin.state().config.lock().clone().my_cln_version;
Ok(nwc_store)
}
if at_or_above_version(&my_version, "24.11").map_err(|e| {
fn check_cln_version(
my_version: &str,
id: &str,
) -> Result<bool, (nip47::NIP47Error, Option<String>)> {
at_or_above_version(my_version, "24.11").map_err(|e| {
(
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
},
id.clone(),
Some(id.to_owned()),
)
})? {
match rpc
.call_typed(&XpayRequest {
amount_msat: params.amount.map(Amount::from_msat),
maxdelay: None,
maxfee: None,
partial_msat: None,
retry_for: None,
layers: None,
invstring: params.invoice,
})
.await
{
Ok(o) => {
if let Some(ref mut bdg) = nwc_store.budget_msat {
*bdg = bdg.saturating_sub(o.amount_sent_msat.msat());
update_nwc_store(&mut rpc, label, nwc_store)
.await
.map_err(|e| {
(
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
},
id.clone(),
)
})?;
}
})
}
let preimage = hex::encode(o.payment_preimage.to_vec());
let fees_paid = o.amount_sent_msat.msat() - o.amount_msat.msat();
Ok((
nip47::PayInvoiceResponse {
preimage,
fees_paid: Some(fees_paid),
},
id,
))
}
Err(e) => match e.code {
Some(c) => match c {
207 | 219 => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: e.to_string(),
},
id,
)),
203 | 205 | 209 => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::PaymentFailed,
message: e.to_string(),
},
id,
)),
_ => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
},
id,
)),
},
None => Err((
async fn update_budget_and_create_response(
rpc: &mut ClnRpc,
label: &str,
nwc_store: &mut NwcStore,
amount_sent_msat: u64,
amount_msat: u64,
preimage: Secret,
id: &str,
) -> Result<(nip47::PayInvoiceResponse, Option<String>), (nip47::NIP47Error, Option<String>)> {
if let Some(ref mut bdg) = nwc_store.budget_msat {
*bdg = bdg.saturating_sub(amount_sent_msat);
update_nwc_store(rpc, label, nwc_store.clone())
.await
.map_err(|e| {
(
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
},
id,
)),
},
}
} else {
match rpc
.call_typed(&PayRequest {
amount_msat: params.amount.map(Amount::from_msat),
description: None,
exemptfee: None,
label: None,
localinvreqid: None,
maxdelay: None,
maxfee: None,
maxfeepercent: None,
partial_msat: None,
retry_for: None,
riskfactor: None,
exclude: None,
bolt11: params.invoice,
})
.await
{
Ok(o) => {
if let Some(ref mut bdg) = nwc_store.budget_msat {
*bdg = bdg.saturating_sub(o.amount_sent_msat.msat());
update_nwc_store(&mut rpc, label, nwc_store)
.await
.map_err(|e| {
(
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
},
id.clone(),
)
})?;
}
let preimage = hex::encode(o.payment_preimage.to_vec());
let fees_paid = o.amount_sent_msat.msat() - o.amount_msat.msat();
Ok((
nip47::PayInvoiceResponse {
preimage,
fees_paid: Some(fees_paid),
},
id,
))
}
Err(e) => match e.code {
Some(c) => match c {
201 | 207 | 219 => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: e.to_string(),
},
id,
)),
203 | 205 | 209 | 210 => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::PaymentFailed,
message: e.to_string(),
},
id,
)),
206 => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::InsufficientBalance,
message: e.to_string(),
},
id,
)),
_ => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
},
id,
)),
},
None => Err((
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
},
id,
)),
},
}
Some(id.to_owned()),
)
})?;
}
let preimage_str = hex::encode(preimage.to_vec());
let fees_paid = amount_sent_msat - amount_msat;
Ok((
nip47::PayInvoiceResponse {
preimage: preimage_str,
fees_paid: Some(fees_paid),
},
Some(id.to_owned()),
))
}
fn map_cln_error_to_nip47(
e: &RpcError,
id: &str,
is_xpay: bool,
) -> (nip47::NIP47Error, Option<String>) {
match e.code {
Some(c) => {
let other_codes = if is_xpay {
vec![207, 219]
} else {
vec![201, 207, 219]
};
let failed_codes = if is_xpay {
vec![203, 205, 209]
} else {
vec![203, 205, 209, 210]
};
if other_codes.contains(&c) {
(
nip47::NIP47Error {
code: nip47::ErrorCode::Other,
message: e.to_string(),
},
Some(id.to_owned()),
)
} else if failed_codes.contains(&c) {
(
nip47::NIP47Error {
code: nip47::ErrorCode::PaymentFailed,
message: e.to_string(),
},
Some(id.to_owned()),
)
} else if !is_xpay && c == 206 {
(
nip47::NIP47Error {
code: nip47::ErrorCode::InsufficientBalance,
message: e.to_string(),
},
Some(id.to_owned()),
)
} else {
(
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
},
Some(id.to_owned()),
)
}
}
None => (
nip47::NIP47Error {
code: nip47::ErrorCode::Internal,
message: e.to_string(),
},
Some(id.to_owned()),
),
}
}
async fn pay_with_xpay_full(
rpc: &mut ClnRpc,
params: nip47::PayInvoiceRequest,
label: &str,
mut nwc_store: NwcStore,
id: &str,
) -> Result<(nip47::PayInvoiceResponse, Option<String>), (nip47::NIP47Error, Option<String>)> {
let payment_result = rpc
.call_typed(&XpayRequest {
amount_msat: params.amount.map(Amount::from_msat),
maxdelay: None,
maxfee: None,
partial_msat: None,
retry_for: None,
layers: None,
invstring: params.invoice,
})
.await
.map_err(|e| map_cln_error_to_nip47(&e, id, true))?;
let amount_sent_msat = payment_result.amount_sent_msat.msat();
let amount_msat = payment_result.amount_msat.msat();
let preimage = payment_result.payment_preimage;
update_budget_and_create_response(
rpc,
label,
&mut nwc_store,
amount_sent_msat,
amount_msat,
preimage,
id,
)
.await
}
async fn pay_with_legacy_full(
rpc: &mut ClnRpc,
params: nip47::PayInvoiceRequest,
label: &str,
mut nwc_store: NwcStore,
id: &str,
) -> Result<(nip47::PayInvoiceResponse, Option<String>), (nip47::NIP47Error, Option<String>)> {
let payment_result = rpc
.call_typed(&PayRequest {
amount_msat: params.amount.map(Amount::from_msat),
description: None,
exemptfee: None,
label: None,
localinvreqid: None,
maxdelay: None,
maxfee: None,
maxfeepercent: None,
partial_msat: None,
retry_for: None,
riskfactor: None,
exclude: None,
bolt11: params.invoice,
})
.await
.map_err(|e| map_cln_error_to_nip47(&e, id, false))?;
let amount_sent_msat = payment_result.amount_sent_msat.msat();
let amount_msat = payment_result.amount_msat.msat();
let preimage = payment_result.payment_preimage;
update_budget_and_create_response(
rpc,
label,
&mut nwc_store,
amount_sent_msat,
amount_msat,
preimage,
id,
)
.await
}
pub async fn multi_pay_invoice(
plugin: Plugin<PluginState>,
params: nip47::MultiPayInvoiceRequest,
label: &String,
) -> Vec<(nip47::Response, String)> {
label: &str,
) -> Vec<(nip47::Response, Option<String>)> {
let mut responses = Vec::new();
for pay in params.invoices {
let result = pay_invoice(plugin.clone(), pay, label).await;

View file

@ -42,10 +42,10 @@ pub fn budget_amount_check(
Ok(())
}
pub async fn load_nwc_store(rpc: &mut ClnRpc, label: &String) -> Result<NwcStore, anyhow::Error> {
pub async fn load_nwc_store(rpc: &mut ClnRpc, label: &str) -> Result<NwcStore, anyhow::Error> {
let nwc_store_store = rpc
.call_typed(&ListdatastoreRequest {
key: Some(vec![PLUGIN_NAME.to_owned(), label.clone()]),
key: Some(vec![PLUGIN_NAME.to_owned(), label.to_owned()]),
})
.await?
.datastore;
@ -62,11 +62,11 @@ pub async fn load_nwc_store(rpc: &mut ClnRpc, label: &String) -> Result<NwcStore
pub async fn update_nwc_store(
rpc: &mut ClnRpc,
label: &String,
label: &str,
nwc_store: NwcStore,
) -> Result<(), anyhow::Error> {
rpc.call_typed(&DatastoreRequest {
key: vec![PLUGIN_NAME.to_owned(), label.clone()],
key: vec![PLUGIN_NAME.to_owned(), label.to_owned()],
generation: None,
hex: None,
mode: Some(DatastoreMode::CREATE_OR_REPLACE),