fedimint_gateway_server_db/
lib.rs

1use std::collections::BTreeMap;
2use std::str::FromStr;
3
4use bitcoin::hashes::{Hash, sha256};
5use fedimint_api_client::api::net::ConnectorType;
6use fedimint_core::config::FederationId;
7use fedimint_core::db::{
8    Database, DatabaseTransaction, DatabaseVersion, GeneralDbMigrationFn,
9    GeneralDbMigrationFnContext, IDatabaseTransactionOpsCore, IDatabaseTransactionOpsCoreTyped,
10};
11use fedimint_core::encoding::btc::NetworkLegacyEncodingWrapper;
12use fedimint_core::encoding::{Decodable, Encodable};
13use fedimint_core::invite_code::InviteCode;
14use fedimint_core::module::registry::ModuleDecoderRegistry;
15use fedimint_core::{Amount, impl_db_lookup, impl_db_record, push_db_pair_items, secp256k1};
16use fedimint_gateway_common::FederationConfig;
17use fedimint_gateway_common::envs::FM_GATEWAY_IROH_SECRET_KEY_OVERRIDE_ENV;
18use fedimint_ln_common::serde_routing_fees;
19use fedimint_lnv2_common::contracts::{IncomingContract, PaymentImage};
20use fedimint_lnv2_common::gateway_api::PaymentFee;
21use futures::{FutureExt, StreamExt};
22use lightning_invoice::RoutingFees;
23use rand::Rng;
24use rand::rngs::OsRng;
25use secp256k1::{Keypair, Secp256k1};
26use serde::{Deserialize, Serialize};
27use strum::IntoEnumIterator;
28use strum_macros::EnumIter;
29
30pub trait GatewayDbExt {
31    fn get_client_database(&self, federation_id: &FederationId) -> Database;
32}
33
34impl GatewayDbExt for Database {
35    fn get_client_database(&self, federation_id: &FederationId) -> Database {
36        let mut prefix = vec![DbKeyPrefix::ClientDatabase as u8];
37        prefix.append(&mut federation_id.consensus_encode_to_vec());
38        self.with_prefix(prefix)
39    }
40}
41
42#[allow(async_fn_in_trait)]
43pub trait GatewayDbtxNcExt {
44    async fn save_federation_config(&mut self, config: &FederationConfig);
45    async fn load_federation_configs_v0(&mut self) -> BTreeMap<FederationId, FederationConfigV0>;
46    async fn load_federation_configs(&mut self) -> BTreeMap<FederationId, FederationConfig>;
47    async fn load_federation_config(
48        &mut self,
49        federation_id: FederationId,
50    ) -> Option<FederationConfig>;
51    async fn remove_federation_config(&mut self, federation_id: FederationId);
52
53    /// Returns the keypair that uniquely identifies the gateway.
54    async fn load_gateway_keypair(&mut self) -> Option<Keypair>;
55
56    /// Returns the keypair that uniquely identifies the gateway.
57    ///
58    /// # Panics
59    /// Gateway keypair does not exist.
60    async fn load_gateway_keypair_assert_exists(&mut self) -> Keypair;
61
62    /// Returns the keypair that uniquely identifies the gateway, creating it if
63    /// it does not exist. Remember to commit the transaction after calling this
64    /// method.
65    async fn load_or_create_gateway_keypair(&mut self) -> Keypair;
66
67    async fn save_new_preimage_authentication(
68        &mut self,
69        payment_hash: sha256::Hash,
70        preimage_auth: sha256::Hash,
71    );
72
73    async fn load_preimage_authentication(
74        &mut self,
75        payment_hash: sha256::Hash,
76    ) -> Option<sha256::Hash>;
77
78    /// Saves a registered incoming contract, returning the previous contract
79    /// with the same payment hash if it existed.
80    async fn save_registered_incoming_contract(
81        &mut self,
82        federation_id: FederationId,
83        incoming_amount: Amount,
84        contract: IncomingContract,
85    ) -> Option<RegisteredIncomingContract>;
86
87    async fn load_registered_incoming_contract(
88        &mut self,
89        payment_image: PaymentImage,
90    ) -> Option<RegisteredIncomingContract>;
91
92    /// Reads and serializes structures from the gateway's database for the
93    /// purpose for serializing to JSON for inspection.
94    async fn dump_database(
95        &mut self,
96        prefix_names: Vec<String>,
97    ) -> BTreeMap<String, Box<dyn erased_serde::Serialize + Send>>;
98
99    /// Returns `iroh::SecretKey` and saves it to the database if it does not
100    /// exist
101    async fn load_or_create_iroh_key(&mut self) -> iroh::SecretKey;
102}
103
104impl<Cap: Send> GatewayDbtxNcExt for DatabaseTransaction<'_, Cap> {
105    async fn save_federation_config(&mut self, config: &FederationConfig) {
106        let id = config.invite_code.federation_id();
107        self.insert_entry(&FederationConfigKey { id }, config).await;
108    }
109
110    async fn load_federation_configs_v0(&mut self) -> BTreeMap<FederationId, FederationConfigV0> {
111        self.find_by_prefix(&FederationConfigKeyPrefixV0)
112            .await
113            .map(|(key, config): (FederationConfigKeyV0, FederationConfigV0)| (key.id, config))
114            .collect::<BTreeMap<FederationId, FederationConfigV0>>()
115            .await
116    }
117
118    async fn load_federation_configs(&mut self) -> BTreeMap<FederationId, FederationConfig> {
119        self.find_by_prefix(&FederationConfigKeyPrefix)
120            .await
121            .map(|(key, config): (FederationConfigKey, FederationConfig)| (key.id, config))
122            .collect::<BTreeMap<FederationId, FederationConfig>>()
123            .await
124    }
125
126    async fn load_federation_config(
127        &mut self,
128        federation_id: FederationId,
129    ) -> Option<FederationConfig> {
130        self.get_value(&FederationConfigKey { id: federation_id })
131            .await
132    }
133
134    async fn remove_federation_config(&mut self, federation_id: FederationId) {
135        self.remove_entry(&FederationConfigKey { id: federation_id })
136            .await;
137    }
138
139    async fn load_gateway_keypair(&mut self) -> Option<Keypair> {
140        self.get_value(&GatewayPublicKey).await
141    }
142
143    async fn load_gateway_keypair_assert_exists(&mut self) -> Keypair {
144        self.get_value(&GatewayPublicKey)
145            .await
146            .expect("Gateway keypair does not exist")
147    }
148
149    async fn load_or_create_gateway_keypair(&mut self) -> Keypair {
150        if let Some(key_pair) = self.get_value(&GatewayPublicKey).await {
151            key_pair
152        } else {
153            let context = Secp256k1::new();
154            let (secret_key, _public_key) = context.generate_keypair(&mut OsRng);
155            let key_pair = Keypair::from_secret_key(&context, &secret_key);
156
157            self.insert_new_entry(&GatewayPublicKey, &key_pair).await;
158            key_pair
159        }
160    }
161
162    async fn save_new_preimage_authentication(
163        &mut self,
164        payment_hash: sha256::Hash,
165        preimage_auth: sha256::Hash,
166    ) {
167        self.insert_new_entry(&PreimageAuthentication { payment_hash }, &preimage_auth)
168            .await;
169    }
170
171    async fn load_preimage_authentication(
172        &mut self,
173        payment_hash: sha256::Hash,
174    ) -> Option<sha256::Hash> {
175        self.get_value(&PreimageAuthentication { payment_hash })
176            .await
177    }
178
179    async fn save_registered_incoming_contract(
180        &mut self,
181        federation_id: FederationId,
182        incoming_amount: Amount,
183        contract: IncomingContract,
184    ) -> Option<RegisteredIncomingContract> {
185        self.insert_entry(
186            &RegisteredIncomingContractKey(contract.commitment.payment_image.clone()),
187            &RegisteredIncomingContract {
188                federation_id,
189                incoming_amount_msats: incoming_amount.msats,
190                contract,
191            },
192        )
193        .await
194    }
195
196    async fn load_registered_incoming_contract(
197        &mut self,
198        payment_image: PaymentImage,
199    ) -> Option<RegisteredIncomingContract> {
200        self.get_value(&RegisteredIncomingContractKey(payment_image))
201            .await
202    }
203
204    async fn dump_database(
205        &mut self,
206        prefix_names: Vec<String>,
207    ) -> BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> {
208        let mut gateway_items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> =
209            BTreeMap::new();
210        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
211            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
212        });
213
214        for table in filtered_prefixes {
215            match table {
216                DbKeyPrefix::FederationConfig => {
217                    push_db_pair_items!(
218                        self,
219                        FederationConfigKeyPrefix,
220                        FederationConfigKey,
221                        FederationConfig,
222                        gateway_items,
223                        "Federation Config"
224                    );
225                }
226                DbKeyPrefix::GatewayPublicKey => {
227                    if let Some(public_key) = self.load_gateway_keypair().await {
228                        gateway_items
229                            .insert("Gateway Public Key".to_string(), Box::new(public_key));
230                    }
231                }
232                _ => {}
233            }
234        }
235
236        gateway_items
237    }
238
239    async fn load_or_create_iroh_key(&mut self) -> iroh::SecretKey {
240        if let Some(iroh_sk) = self.get_value(&IrohKey).await {
241            iroh_sk
242        } else {
243            let iroh_sk = if let Ok(var) = std::env::var(FM_GATEWAY_IROH_SECRET_KEY_OVERRIDE_ENV) {
244                iroh::SecretKey::from_str(&var).expect("Invalid overridden iroh secret key")
245            } else {
246                iroh::SecretKey::generate(&mut OsRng)
247            };
248
249            self.insert_new_entry(&IrohKey, &iroh_sk).await;
250            iroh_sk
251        }
252    }
253}
254
255#[repr(u8)]
256#[derive(Clone, EnumIter, Debug)]
257enum DbKeyPrefix {
258    FederationConfig = 0x04,
259    GatewayPublicKey = 0x06,
260    GatewayConfiguration = 0x07,
261    PreimageAuthentication = 0x08,
262    RegisteredIncomingContract = 0x09,
263    ClientDatabase = 0x10,
264    Iroh = 0x11,
265}
266
267impl std::fmt::Display for DbKeyPrefix {
268    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
269        write!(f, "{self:?}")
270    }
271}
272
273#[derive(Debug, Encodable, Decodable)]
274struct FederationConfigKeyPrefixV0;
275
276#[derive(Debug, Encodable, Decodable)]
277struct FederationConfigKeyPrefixV1;
278
279#[derive(Debug, Encodable, Decodable)]
280struct FederationConfigKeyPrefix;
281
282#[derive(Debug, Clone, Encodable, Decodable, Eq, PartialEq, Hash, Ord, PartialOrd)]
283struct FederationConfigKeyV0 {
284    id: FederationId,
285}
286
287#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
288pub struct FederationConfigV0 {
289    pub invite_code: InviteCode,
290    pub federation_index: u64,
291    pub timelock_delta: u64,
292    #[serde(with = "serde_routing_fees")]
293    pub fees: RoutingFees,
294}
295
296#[derive(Debug, Clone, Encodable, Decodable, Eq, PartialEq, Hash, Ord, PartialOrd)]
297struct FederationConfigKeyV1 {
298    id: FederationId,
299}
300
301#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
302pub struct FederationConfigV1 {
303    pub invite_code: InviteCode,
304    // Unique integer identifier per-federation that is assigned when the gateways joins a
305    // federation.
306    #[serde(alias = "mint_channel_id")]
307    pub federation_index: u64,
308    pub timelock_delta: u64,
309    #[serde(with = "serde_routing_fees")]
310    pub fees: RoutingFees,
311    pub connector: ConnectorType,
312}
313
314#[derive(Debug, Clone, Encodable, Decodable, Eq, PartialEq, Hash, Ord, PartialOrd)]
315struct FederationConfigKey {
316    id: FederationId,
317}
318
319impl_db_record!(
320    key = FederationConfigKeyV0,
321    value = FederationConfigV0,
322    db_prefix = DbKeyPrefix::FederationConfig,
323);
324
325impl_db_record!(
326    key = FederationConfigKeyV1,
327    value = FederationConfigV1,
328    db_prefix = DbKeyPrefix::FederationConfig,
329);
330
331impl_db_record!(
332    key = FederationConfigKey,
333    value = FederationConfig,
334    db_prefix = DbKeyPrefix::FederationConfig,
335);
336
337impl_db_lookup!(
338    key = FederationConfigKeyV0,
339    query_prefix = FederationConfigKeyPrefixV0
340);
341impl_db_lookup!(
342    key = FederationConfigKeyV1,
343    query_prefix = FederationConfigKeyPrefixV1
344);
345impl_db_lookup!(
346    key = FederationConfigKey,
347    query_prefix = FederationConfigKeyPrefix
348);
349
350#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable)]
351struct GatewayPublicKey;
352
353impl_db_record!(
354    key = GatewayPublicKey,
355    value = Keypair,
356    db_prefix = DbKeyPrefix::GatewayPublicKey,
357);
358
359#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable)]
360struct GatewayConfigurationKeyV0;
361
362#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
363struct GatewayConfigurationV0 {
364    password: String,
365    num_route_hints: u32,
366    #[serde(with = "serde_routing_fees")]
367    routing_fees: RoutingFees,
368    network: NetworkLegacyEncodingWrapper,
369}
370
371#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable)]
372pub struct GatewayConfigurationKeyV1;
373
374#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
375pub struct GatewayConfigurationV1 {
376    pub hashed_password: sha256::Hash,
377    pub num_route_hints: u32,
378    #[serde(with = "serde_routing_fees")]
379    pub routing_fees: RoutingFees,
380    pub network: NetworkLegacyEncodingWrapper,
381    pub password_salt: [u8; 16],
382}
383
384#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable)]
385pub struct GatewayConfigurationKeyV2;
386
387#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
388pub struct GatewayConfigurationV2 {
389    pub num_route_hints: u32,
390    #[serde(with = "serde_routing_fees")]
391    pub routing_fees: RoutingFees,
392    pub network: NetworkLegacyEncodingWrapper,
393}
394
395impl_db_record!(
396    key = GatewayConfigurationKeyV0,
397    value = GatewayConfigurationV0,
398    db_prefix = DbKeyPrefix::GatewayConfiguration,
399);
400
401impl_db_record!(
402    key = GatewayConfigurationKeyV1,
403    value = GatewayConfigurationV1,
404    db_prefix = DbKeyPrefix::GatewayConfiguration,
405);
406
407impl_db_record!(
408    key = GatewayConfigurationKeyV2,
409    value = GatewayConfigurationV2,
410    db_prefix = DbKeyPrefix::GatewayConfiguration,
411);
412
413#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable)]
414struct PreimageAuthentication {
415    payment_hash: sha256::Hash,
416}
417
418impl_db_record!(
419    key = PreimageAuthentication,
420    value = sha256::Hash,
421    db_prefix = DbKeyPrefix::PreimageAuthentication
422);
423
424#[allow(dead_code)] // used in tests
425#[derive(Debug, Encodable, Decodable)]
426struct PreimageAuthenticationPrefix;
427
428impl_db_lookup!(
429    key = PreimageAuthentication,
430    query_prefix = PreimageAuthenticationPrefix
431);
432
433#[derive(Debug, Encodable, Decodable)]
434struct IrohKey;
435
436impl_db_record!(
437    key = IrohKey,
438    value = iroh::SecretKey,
439    db_prefix = DbKeyPrefix::Iroh
440);
441
442pub fn get_gatewayd_database_migrations() -> BTreeMap<DatabaseVersion, GeneralDbMigrationFn> {
443    let mut migrations: BTreeMap<DatabaseVersion, GeneralDbMigrationFn> = BTreeMap::new();
444    migrations.insert(
445        DatabaseVersion(0),
446        Box::new(|ctx| migrate_to_v1(ctx).boxed()),
447    );
448    migrations.insert(
449        DatabaseVersion(1),
450        Box::new(|ctx| migrate_to_v2(ctx).boxed()),
451    );
452    migrations.insert(
453        DatabaseVersion(2),
454        Box::new(|ctx| migrate_to_v3(ctx).boxed()),
455    );
456    migrations.insert(
457        DatabaseVersion(3),
458        Box::new(|ctx| migrate_to_v4(ctx).boxed()),
459    );
460    migrations.insert(
461        DatabaseVersion(4),
462        Box::new(|ctx| migrate_to_v5(ctx).boxed()),
463    );
464    migrations
465}
466
467async fn migrate_to_v1(mut ctx: GeneralDbMigrationFnContext<'_>) -> Result<(), anyhow::Error> {
468    /// Creates a password hash by appending a 4 byte salt to the plaintext
469    /// password.
470    fn hash_password(plaintext_password: &str, salt: [u8; 16]) -> sha256::Hash {
471        let mut bytes = Vec::new();
472        bytes.append(&mut plaintext_password.consensus_encode_to_vec());
473        bytes.append(&mut salt.consensus_encode_to_vec());
474        sha256::Hash::hash(&bytes)
475    }
476
477    let mut dbtx = ctx.dbtx();
478
479    // If there is no old gateway configuration, there is nothing to do.
480    if let Some(old_gateway_config) = dbtx.remove_entry(&GatewayConfigurationKeyV0).await {
481        let password_salt: [u8; 16] = rand::thread_rng().r#gen();
482        let hashed_password = hash_password(&old_gateway_config.password, password_salt);
483        let new_gateway_config = GatewayConfigurationV1 {
484            hashed_password,
485            num_route_hints: old_gateway_config.num_route_hints,
486            routing_fees: old_gateway_config.routing_fees,
487            network: old_gateway_config.network,
488            password_salt,
489        };
490        dbtx.insert_entry(&GatewayConfigurationKeyV1, &new_gateway_config)
491            .await;
492    }
493
494    Ok(())
495}
496
497async fn migrate_to_v2(mut ctx: GeneralDbMigrationFnContext<'_>) -> Result<(), anyhow::Error> {
498    let mut dbtx = ctx.dbtx();
499
500    // If there is no old federation configuration, there is nothing to do.
501    for (old_federation_id, _old_federation_config) in dbtx.load_federation_configs_v0().await {
502        if let Some(old_federation_config) = dbtx
503            .remove_entry(&FederationConfigKeyV0 {
504                id: old_federation_id,
505            })
506            .await
507        {
508            let new_federation_config = FederationConfigV1 {
509                invite_code: old_federation_config.invite_code,
510                federation_index: old_federation_config.federation_index,
511                timelock_delta: old_federation_config.timelock_delta,
512                fees: old_federation_config.fees,
513                connector: ConnectorType::default(),
514            };
515            let new_federation_key = FederationConfigKeyV1 {
516                id: old_federation_id,
517            };
518            dbtx.insert_entry(&new_federation_key, &new_federation_config)
519                .await;
520        }
521    }
522    Ok(())
523}
524
525async fn migrate_to_v3(mut ctx: GeneralDbMigrationFnContext<'_>) -> Result<(), anyhow::Error> {
526    let mut dbtx = ctx.dbtx();
527
528    // If there is no old gateway configuration, there is nothing to do.
529    if let Some(old_gateway_config) = dbtx.remove_entry(&GatewayConfigurationKeyV1).await {
530        let new_gateway_config = GatewayConfigurationV2 {
531            num_route_hints: old_gateway_config.num_route_hints,
532            routing_fees: old_gateway_config.routing_fees,
533            network: old_gateway_config.network,
534        };
535        dbtx.insert_entry(&GatewayConfigurationKeyV2, &new_gateway_config)
536            .await;
537    }
538
539    Ok(())
540}
541
542async fn migrate_to_v4(mut ctx: GeneralDbMigrationFnContext<'_>) -> Result<(), anyhow::Error> {
543    let mut dbtx = ctx.dbtx();
544
545    dbtx.remove_entry(&GatewayConfigurationKeyV2).await;
546
547    let configs = dbtx
548        .find_by_prefix(&FederationConfigKeyPrefixV1)
549        .await
550        .collect::<Vec<_>>()
551        .await;
552    for (fed_id, _old_config) in configs {
553        if let Some(old_federation_config) = dbtx.remove_entry(&fed_id).await {
554            let new_fed_config = FederationConfig {
555                invite_code: old_federation_config.invite_code,
556                federation_index: old_federation_config.federation_index,
557                lightning_fee: old_federation_config.fees.into(),
558                transaction_fee: PaymentFee::TRANSACTION_FEE_DEFAULT,
559                connector: ConnectorType::default(),
560            };
561            let new_key = FederationConfigKey { id: fed_id.id };
562            dbtx.insert_new_entry(&new_key, &new_fed_config).await;
563        }
564    }
565    Ok(())
566}
567
568/// Introduced in v0.5, there is a db key clash between the `FederationConfig`
569/// record and the isolated databases used for each client. We must migrate the
570/// isolated databases to be behind the `ClientDatabase` prefix to allow the
571/// gateway to properly read the federation configs.
572async fn migrate_to_v5(mut ctx: GeneralDbMigrationFnContext<'_>) -> Result<(), anyhow::Error> {
573    let mut dbtx = ctx.dbtx();
574    migrate_federation_configs(&mut dbtx).await
575}
576
577async fn migrate_federation_configs(
578    dbtx: &mut DatabaseTransaction<'_>,
579) -> Result<(), anyhow::Error> {
580    // We need to migrate all isolated database entries to be behind the 0x10
581    // prefix. The problem is, if there is a `FederationId` that starts with
582    // 0x04, we cannot read the `FederationId` because the database will be confused
583    // between the isolated DB and the `FederationConfigKey` record. To solve this,
584    // we try and decode each key as a Federation ID and each value as a
585    // FederationConfig. If that is successful and the federation ID in the
586    // config matches the key, then we skip that record and migrate the rest of
587    // the entries.
588    let problem_entries = dbtx
589        .raw_find_by_prefix(&[0x04])
590        .await?
591        .collect::<BTreeMap<_, _>>()
592        .await;
593    for (mut problem_key, value) in problem_entries {
594        // Try and decode the key as a FederationId and the value as a FederationConfig
595        // The key should be 33 bytes because a FederationID is 32 bytes and there is a
596        // 1 byte prefix.
597        if problem_key.len() == 33
598            && let Ok(federation_id) = FederationId::consensus_decode_whole(
599                &problem_key[1..33],
600                &ModuleDecoderRegistry::default(),
601            )
602            && let Ok(federation_config) =
603                FederationConfig::consensus_decode_whole(&value, &ModuleDecoderRegistry::default())
604            && federation_id == federation_config.invite_code.federation_id()
605        {
606            continue;
607        }
608
609        dbtx.raw_remove_entry(&problem_key).await?;
610        let mut new_key = vec![DbKeyPrefix::ClientDatabase as u8];
611        new_key.append(&mut problem_key);
612        dbtx.raw_insert_bytes(&new_key, &value).await?;
613    }
614
615    // Migrate all entries of the isolated databases that don't overlap with
616    // `FederationConfig` entries.
617    let fed_ids = dbtx
618        .find_by_prefix(&FederationConfigKeyPrefix)
619        .await
620        .collect::<BTreeMap<_, _>>()
621        .await;
622    for fed_id in fed_ids.keys() {
623        let federation_id_bytes = fed_id.id.consensus_encode_to_vec();
624        let isolated_entries = dbtx
625            .raw_find_by_prefix(&federation_id_bytes)
626            .await?
627            .collect::<BTreeMap<_, _>>()
628            .await;
629        for (mut key, value) in isolated_entries {
630            dbtx.raw_remove_entry(&key).await?;
631            let mut new_key = vec![DbKeyPrefix::ClientDatabase as u8];
632            new_key.append(&mut key);
633            dbtx.raw_insert_bytes(&new_key, &value).await?;
634        }
635    }
636
637    Ok(())
638}
639
640#[derive(Debug, Encodable, Decodable)]
641struct RegisteredIncomingContractKey(pub PaymentImage);
642
643#[derive(Debug, Encodable, Decodable)]
644pub struct RegisteredIncomingContract {
645    pub federation_id: FederationId,
646    /// The amount of the incoming contract, in msats.
647    pub incoming_amount_msats: u64,
648    pub contract: IncomingContract,
649}
650
651impl_db_record!(
652    key = RegisteredIncomingContractKey,
653    value = RegisteredIncomingContract,
654    db_prefix = DbKeyPrefix::RegisteredIncomingContract,
655);
656
657#[cfg(test)]
658mod migration_tests;