Skip to content

Commit d5e604a

Browse files
committed
Add PaymentMetadataStore wrapper and persistence
Add the `PaymentMetadataStore` struct that wraps a `DataStore` with a reverse index from `PaymentId` to `MetadataId`. This enables efficient lookup of metadata entries by payment ID. API methods: - `insert` / `get` / `remove` — basic CRUD with reverse index upkeep - `add_payment_id` — associate additional payment IDs with an entry - `get_for_payment_id` — reverse index lookup - `get_lsp_fee_limits_for_payment_id` — convenience for LSP fee checks - `remove_payment_id` — clean up reverse index when payments are removed Also add the `payment_metadata` namespace constants in `io/mod.rs`. Generated with the help of AI (Claude Code). Co-Authored-By: HAL 9000
1 parent 26b9cce commit d5e604a

File tree

2 files changed

+158
-1
lines changed

2 files changed

+158
-1
lines changed

src/io/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,7 @@ pub(crate) const STATIC_INVOICE_STORE_PRIMARY_NAMESPACE: &str = "static_invoices
8282
/// The pending payment information will be persisted under this prefix.
8383
pub(crate) const PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = "pending_payments";
8484
pub(crate) const PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";
85+
86+
/// The payment metadata will be persisted under this prefix.
87+
pub(crate) const PAYMENT_METADATA_PERSISTENCE_PRIMARY_NAMESPACE: &str = "payment_metadata";
88+
pub(crate) const PAYMENT_METADATA_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";

src/payment/metadata_store.rs

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,26 @@
55
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
66
// accordance with one or both of these licenses.
77

8+
use std::collections::{HashMap, HashSet};
9+
use std::sync::{Arc, Mutex};
10+
811
use lightning::impl_writeable_tlv_based;
912
use lightning::impl_writeable_tlv_based_enum;
1013
use lightning::ln::channelmanager::PaymentId;
1114
use lightning::offers::offer::Offer as LdkOffer;
1215
use lightning::offers::refund::Refund as LdkRefund;
1316
use lightning_invoice::Bolt11Invoice as LdkBolt11Invoice;
1417

15-
use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate};
18+
use crate::data_store::{DataStore, StorableObject, StorableObjectId, StorableObjectUpdate};
1619
use crate::hex_utils;
20+
use crate::io::{
21+
PAYMENT_METADATA_PERSISTENCE_PRIMARY_NAMESPACE,
22+
PAYMENT_METADATA_PERSISTENCE_SECONDARY_NAMESPACE,
23+
};
24+
use crate::logger::{log_error, LdkLogger, Logger};
1725
use crate::payment::store::LSPFeeLimits;
26+
use crate::types::DynStore;
27+
use crate::Error;
1828

1929
/// An opaque identifier for a payment metadata entry.
2030
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
@@ -113,3 +123,146 @@ impl StorableObjectUpdate<PaymentMetadataEntry> for PaymentMetadataEntryUpdate {
113123
self.id
114124
}
115125
}
126+
127+
/// A store for payment metadata, backed by a [`DataStore`].
128+
///
129+
/// Maintains a reverse index from [`PaymentId`] to [`MetadataId`] for efficient lookups.
130+
pub(crate) struct PaymentMetadataStore {
131+
inner: DataStore<PaymentMetadataEntry, Arc<Logger>>,
132+
reverse_index: Mutex<HashMap<PaymentId, HashSet<MetadataId>>>,
133+
}
134+
135+
impl PaymentMetadataStore {
136+
pub(crate) fn new(
137+
entries: Vec<PaymentMetadataEntry>, kv_store: Arc<DynStore>, logger: Arc<Logger>,
138+
) -> Self {
139+
let mut reverse_index: HashMap<PaymentId, HashSet<MetadataId>> = HashMap::new();
140+
for entry in &entries {
141+
for payment_id in &entry.payment_ids {
142+
reverse_index.entry(*payment_id).or_default().insert(entry.id);
143+
}
144+
}
145+
146+
let inner = DataStore::new(
147+
entries,
148+
PAYMENT_METADATA_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
149+
PAYMENT_METADATA_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
150+
kv_store,
151+
logger,
152+
);
153+
154+
Self { inner, reverse_index: Mutex::new(reverse_index) }
155+
}
156+
157+
/// Insert a new metadata entry and update the reverse index.
158+
pub(crate) fn insert(&self, entry: PaymentMetadataEntry) -> Result<MetadataId, Error> {
159+
let id = entry.id;
160+
let payment_ids = entry.payment_ids.clone();
161+
162+
self.inner.insert(entry)?;
163+
164+
let mut locked_index = self.reverse_index.lock().unwrap();
165+
for payment_id in payment_ids {
166+
locked_index.entry(payment_id).or_default().insert(id);
167+
}
168+
169+
Ok(id)
170+
}
171+
172+
/// Associate an additional [`PaymentId`] with an existing metadata entry.
173+
pub(crate) fn add_payment_id(
174+
&self, metadata_id: MetadataId, payment_id: PaymentId,
175+
) -> Result<(), Error> {
176+
if let Some(mut entry) = self.inner.get(&metadata_id) {
177+
if !entry.payment_ids.contains(&payment_id) {
178+
entry.payment_ids.push(payment_id);
179+
let update = PaymentMetadataEntryUpdate {
180+
id: metadata_id,
181+
payment_ids: Some(entry.payment_ids),
182+
};
183+
self.inner.update(update)?;
184+
self.reverse_index
185+
.lock()
186+
.unwrap()
187+
.entry(payment_id)
188+
.or_default()
189+
.insert(metadata_id);
190+
}
191+
Ok(())
192+
} else {
193+
Err(Error::PersistenceFailed)
194+
}
195+
}
196+
197+
/// Get a metadata entry by its ID.
198+
pub(crate) fn get(&self, metadata_id: &MetadataId) -> Option<PaymentMetadataEntry> {
199+
self.inner.get(metadata_id)
200+
}
201+
202+
/// Get all metadata entries associated with a given payment ID.
203+
pub(crate) fn get_for_payment_id(&self, payment_id: &PaymentId) -> Vec<PaymentMetadataEntry> {
204+
let locked_index = self.reverse_index.lock().unwrap();
205+
if let Some(metadata_ids) = locked_index.get(payment_id) {
206+
metadata_ids.iter().filter_map(|mid| self.inner.get(mid)).collect()
207+
} else {
208+
Vec::new()
209+
}
210+
}
211+
212+
/// Convenience method to get the [`LSPFeeLimits`] for a given payment ID, if any.
213+
pub(crate) fn get_lsp_fee_limits_for_payment_id(
214+
&self, payment_id: &PaymentId,
215+
) -> Option<LSPFeeLimits> {
216+
let entries = self.get_for_payment_id(payment_id);
217+
for entry in entries {
218+
if let PaymentMetadataKind::LSPFeeLimits { limits } = entry.kind {
219+
return Some(limits);
220+
}
221+
}
222+
None
223+
}
224+
225+
/// Remove a metadata entry and clean up the reverse index.
226+
pub(crate) fn remove(&self, metadata_id: &MetadataId) -> Result<(), Error> {
227+
if let Some(entry) = self.inner.get(metadata_id) {
228+
let mut locked_index = self.reverse_index.lock().unwrap();
229+
for payment_id in &entry.payment_ids {
230+
if let Some(set) = locked_index.get_mut(payment_id) {
231+
set.remove(metadata_id);
232+
if set.is_empty() {
233+
locked_index.remove(payment_id);
234+
}
235+
}
236+
}
237+
}
238+
self.inner.remove(metadata_id)
239+
}
240+
241+
/// Remove a [`PaymentId`] from all associated metadata entries.
242+
///
243+
/// This should be called when a payment store entry is removed to keep the reverse index
244+
/// consistent. If a metadata entry's `payment_ids` becomes empty after removal, it is
245+
/// **not** automatically deleted (the metadata may still be useful).
246+
pub(crate) fn remove_payment_id(&self, payment_id: &PaymentId) -> Result<(), Error> {
247+
let metadata_ids = {
248+
let mut locked_index = self.reverse_index.lock().unwrap();
249+
match locked_index.remove(payment_id) {
250+
Some(ids) => ids,
251+
None => return Ok(()),
252+
}
253+
};
254+
255+
for metadata_id in metadata_ids {
256+
if let Some(mut entry) = self.inner.get(&metadata_id) {
257+
entry.payment_ids.retain(|id| id != payment_id);
258+
let update = PaymentMetadataEntryUpdate {
259+
id: metadata_id,
260+
payment_ids: Some(entry.payment_ids),
261+
};
262+
self.inner.update(update)?;
263+
}
264+
}
265+
266+
Ok(())
267+
}
268+
}

0 commit comments

Comments
 (0)