Fix a few possible concurrency bugs

This commit is contained in:
Jon Staab
2026-06-02 10:16:14 -07:00
parent 1d5c825e15
commit 430f33383b
3 changed files with 33 additions and 11 deletions
+13
View File
@@ -469,6 +469,14 @@ impl Billing {
pub async fn ensure_bolt11_for_invoice(&self, invoice: &Invoice) -> Result<Bolt11> {
let now = chrono::Utc::now().timestamp();
// A resolved (paid or voided) invoice must never mint a new payable
// bolt11; return the existing one if present, otherwise refuse.
if invoice.paid_at.is_some() || invoice.voided_at.is_some() {
return query::get_bolt11_for_invoice(&invoice.id)
.await?
.ok_or_else(|| anyhow!("invoice is not open"));
}
if let Some(existing) = query::get_bolt11_for_invoice(&invoice.id).await?
&& (existing.settled_at.is_none() || now < existing.expires_at)
{
@@ -487,6 +495,11 @@ impl Billing {
pub async fn reconcile_bolt11_for_invoice(&self, invoice: &Invoice) -> Result<Option<Bolt11>> {
if let Some(bolt11) = query::get_bolt11_for_invoice(&invoice.id).await? {
// Don't settle an invoice that is already resolved
if invoice.paid_at.is_some() || invoice.voided_at.is_some() {
return Ok(Some(bolt11));
}
return self.reconcile_bolt11(&bolt11).await;
};
+20 -10
View File
@@ -393,7 +393,8 @@ pub async fn settle_invoice_via_nwc(
with_tx(async |tx| {
clear_tenant_nwc_error_tx(tx, tenant_pubkey).await?;
mark_bolt11_settled_tx(tx, bolt11_id).await?;
mark_invoice_paid_tx(tx, invoice_id, "nwc").await
mark_invoice_paid_tx(tx, invoice_id, "nwc").await?;
Ok(())
})
.await
}
@@ -402,7 +403,8 @@ pub async fn settle_invoice_via_nwc(
pub async fn settle_invoice_out_of_band(bolt11_id: &str, invoice_id: &str) -> Result<()> {
with_tx(async |tx| {
mark_bolt11_settled_tx(tx, bolt11_id).await?;
mark_invoice_paid_tx(tx, invoice_id, "oob").await
mark_invoice_paid_tx(tx, invoice_id, "oob").await?;
Ok(())
})
.await
}
@@ -417,7 +419,8 @@ pub async fn settle_invoice_via_stripe(
with_tx(async |tx| {
insert_intent_tx(tx, intent_id, invoice_id).await?;
clear_tenant_stripe_error_tx(tx, tenant_pubkey).await?;
mark_invoice_paid_tx(tx, invoice_id, "stripe").await
mark_invoice_paid_tx(tx, invoice_id, "stripe").await?;
Ok(())
})
.await
}
@@ -592,10 +595,11 @@ async fn set_relay_status_tx(
insert_activity_tx(tx, activity_type, &relay.id, snapshot).await
}
/// Stamp a bolt11 as settled but don't overwrite an existing settled_at.
async fn mark_bolt11_settled_tx(tx: &mut Transaction<'_, Sqlite>, bolt11_id: &str) -> Result<()> {
let settled_at = chrono::Utc::now().timestamp();
sqlx::query("UPDATE bolt11 SET settled_at = ? WHERE id = ?")
sqlx::query("UPDATE bolt11 SET settled_at = ? WHERE id = ? AND settled_at IS NULL")
.bind(settled_at)
.bind(bolt11_id)
.execute(&mut **tx)
@@ -603,6 +607,9 @@ async fn mark_bolt11_settled_tx(tx: &mut Transaction<'_, Sqlite>, bolt11_id: &st
Ok(())
}
/// Mark an invoice paid, but only while it is still open — a late Lightning
/// payment never flips a voided/forgiven invoice to paid, and a Stripe-paid
/// invoice never has its provenance overwritten by a later bolt11.
async fn mark_invoice_paid_tx(
tx: &mut Transaction<'_, Sqlite>,
invoice_id: &str,
@@ -610,12 +617,15 @@ async fn mark_invoice_paid_tx(
) -> Result<()> {
let paid_at = chrono::Utc::now().timestamp();
sqlx::query("UPDATE invoice SET method = ?, paid_at = ? WHERE id = ?")
.bind(method)
.bind(paid_at)
.bind(invoice_id)
.execute(&mut **tx)
.await?;
sqlx::query(
"UPDATE invoice SET method = ?, paid_at = ?
WHERE id = ? AND paid_at IS NULL AND voided_at IS NULL",
)
.bind(method)
.bind(paid_at)
.bind(invoice_id)
.execute(&mut **tx)
.await?;
Ok(())
}
-1
View File
@@ -9,7 +9,6 @@ import Login from "@/views/Login"
import { createRelayForActiveTenant } from "@/lib/hooks"
import { account } from "@/lib/state"
import FlotillaLogo from "@/assets/flotilla-logo.svg"
import ChachiLogo from "@/assets/chachi-logo.svg"
import NostordLogo from "@/assets/nostord-logo.svg"
export default function Home() {