|
5 | 5 | // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in |
6 | 6 | // accordance with one or both of these licenses. |
7 | 7 |
|
| 8 | +use std::collections::{HashMap, HashSet}; |
| 9 | +use std::sync::{Arc, Mutex}; |
| 10 | + |
8 | 11 | use lightning::impl_writeable_tlv_based; |
9 | 12 | use lightning::impl_writeable_tlv_based_enum; |
10 | 13 | use lightning::ln::channelmanager::PaymentId; |
11 | 14 | use lightning::offers::offer::Offer as LdkOffer; |
12 | 15 | use lightning::offers::refund::Refund as LdkRefund; |
13 | 16 | use lightning_invoice::Bolt11Invoice as LdkBolt11Invoice; |
14 | 17 |
|
15 | | -use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate}; |
| 18 | +use crate::data_store::{DataStore, StorableObject, StorableObjectId, StorableObjectUpdate}; |
16 | 19 | 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}; |
17 | 25 | use crate::payment::store::LSPFeeLimits; |
| 26 | +use crate::types::DynStore; |
| 27 | +use crate::Error; |
18 | 28 |
|
19 | 29 | /// An opaque identifier for a payment metadata entry. |
20 | 30 | #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] |
@@ -113,3 +123,146 @@ impl StorableObjectUpdate<PaymentMetadataEntry> for PaymentMetadataEntryUpdate { |
113 | 123 | self.id |
114 | 124 | } |
115 | 125 | } |
| 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