blitzi 0.3.0

Easy to use Bitcoin Lighning client that uses Fedimint as its backend.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
//! Easy to use Bitcoin Lighning client that uses [Fedimint](https://fedimint.org/) as its backend.
//!
//! You want to build a lightning powered app but don't look forward to dealing
//! with the complexity of running your own Lightning node? Blitzi is for you!
//! With Blitzi you can outsource the infrastructure to any Fedimint federation
//! of your choosing (or just go with the default for small amounts) and receive
//! and send Lightning payments without any hassle.
//!
//! # Examples
//! The fastest way to get started is to create a new Blitzi client with default
//! settings. This is only advisable for small amounts since it will use a
//! default Fedimint federation which the author of this library trusts, but
//! ultimately can't guarantee the security of. For larger amounts we recommend
//! making your own choice which federation to use based on your own due
//! diligence.
//!
//! ```no_run
//! # use anyhow::Result;
//! use blitzi::{Amount, Blitzi};
//!
//! # #[tokio::main]
//! # async fn main() -> Result<()> {
//! // Create a new Blitzi client with default settings
//! let blitzi = Blitzi::new().await?;
//!
//! // Generate a new Lightning invoice for 1000 millisatoshi and await its payment
//! let invoice = blitzi
//!     .lightning_invoice(Amount::from_msats(1000), "Test payment")
//!     .await?;
//! println!("Invoice: {}", invoice);
//!
//! match blitzi.await_incoming_payment(&invoice).await {
//!     Ok(()) => println!("Payment received"),
//!     Err(_) => println!("Invoice expired"),
//! }
//!
//! # Ok(())
//! # }
//! ```
//!
//! # Fedimint
//! Blitzi uses Fedimint, an open source federated ecash mint implementation on
//! Bitcoin, to connect you to the Lighning network. Federated in this context
//! means that each federation is run by a group of people, also called
//! guardians, who are jointly responsible for the security of the funds held in
//! the federation. This means, while no signle guardian can steal your funds,
//! if a majority of the guardians are compromised, the funds are at risk, so
//! chose your federation wisely.
//!
//! The default federation used by Blitzi is [E-Cash Club], which for various
//! reasons seems the most reasonable choice at the time of writing (long run
//! time, multiple ASNs, etc.). For anything but toy amounts users should make
//! their own choice though. You can find a list of publicly known federations
//! on [Fedimint Observer], which also provices statistics and uptime statistics
//! about them.
//!
//! [E-Cash Club]: (https://observer.fedimint.org/federations/aeca6cc80ffc530bd2d54b09681f6edb9a415c362e4af2fe3d5e04137006fa21)
//! [Fedimint Observer]: (https://observer.fedimint.org/)
//!
//! # About the name
//!
//! Lightning bolts are called "Blitz" in German and adding an "i" at the end
//! makes it sound cute and wholesome for me :D
use std::path::PathBuf;
use std::str::FromStr;

use anyhow::{Context, anyhow, ensure};
use fedimint_bip39::{Bip39RootSecretStrategy, Mnemonic};
use fedimint_client::meta::MetaService;
use fedimint_client::module::meta::LegacyMetaSource;
use fedimint_client::secret::RootSecretStrategy;
use fedimint_client::{Client, ClientHandle, ClientModuleInstance, RootSecret};
use fedimint_core::bitcoin::hashes::sha256;
use fedimint_core::core::OperationId;
use fedimint_core::db::{Database, IRawDatabaseExt};
use fedimint_core::invite_code::InviteCode;
use fedimint_core::util::BoxStream;
use fedimint_core::{BitcoinHash, anyhow, hex};
use fedimint_ln_client::{
    LightningClientInit, LightningClientModule, LightningOperationMeta, LightningOperationMetaPay,
    LightningOperationMetaVariant, LnReceiveState, PayType,
};
use fedimint_meta_client::MetaModuleMetaSourceWithFallback;
use fedimint_mint_client::MintClientInit;
use futures_lite::stream::StreamExt;
use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};

const ECASH_CLUB_INVITE: &str = "fed11qgqzggnhwden5te0v9cxjtn9vd3jue3wvfkxjmnyva6kzunyd9skutnwv46z7qqpyzhv5mxgpl79xz7j649sj6qldmde5s2uxchy4uh7840qgymsqmazzp6sn43";

/// Utility type for amounts in millisatoshi reexported from fedimint-core.
pub use fedimint_core::Amount;
/// Utility module for parsing lightning invoices reexported from
/// lightning-invoice.
pub use lightning_invoice;

/// Builder for the Blitzi client that allows configuring the fedimint client's
/// settings.
///
/// ```no_run
/// # use anyhow::Result;
/// use blitzi::Blitzi;
///
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let blitzi = Blitzi::builder()
///     .datadir("/path/to/data")
///     .federation("fed11qgqzggnhwden5te0v9cxjtn9vd3jue3wvfkxjmnyva6kzunyd9skutnwv46z7qqpyzhv5mxgpl79xz7j649sj6qldmde5s2uxchy4uh743")?
///     .build()
///     .await?;
/// # Ok(())
/// # }
/// ```
pub struct BlitziBuilder {
    datadir: PathBuf,
    federation: InviteCode,
}

impl Default for BlitziBuilder {
    fn default() -> Self {
        let xdg = xdg::BaseDirectories::new();

        Self {
            datadir: xdg
                .data_home
                .expect("Could not determine XDG data home")
                .join("fedimint/default"),
            federation: InviteCode::from_str(ECASH_CLUB_INVITE).expect("can be parsed"),
        }
    }
}

impl BlitziBuilder {
    /// Sets the directory where Fedimint data will be stored. Defaults to
    /// `$XDG_DATA_HOME/fedimint/default`
    pub fn datadir(mut self, path: impl Into<PathBuf>) -> Self {
        self.datadir = path.into();
        self
    }

    /// Sets the federation to connect to via an already parsed invite code. If
    /// you have a string invite code, use [`Self::federation`] instead.
    pub fn federation_invite(mut self, invite: InviteCode) -> Self {
        self.federation = invite;
        self
    }

    /// Sets the federation to connect to via an invite code string. If you
    /// already have a parsed invite code, use [`Self::federation_invite`]
    /// instead.
    pub fn federation(mut self, invite: &str) -> anyhow::Result<Self> {
        let invite = InviteCode::from_str(invite)?;
        self.federation = invite;
        Ok(self)
    }

    /// Builds the Blitzi client.
    ///
    /// This function will open the existing Fedimint client or join the
    /// federation depending on whether the client has already been initialized.
    ///
    /// # Errors
    /// Returns an error if the database cannot be opened or if joining the
    /// federation fails.
    pub async fn build(self) -> anyhow::Result<Blitzi> {
        let mut client_builder = fedimint_client::Client::builder().await?;
        client_builder.with_module(MintClientInit);
        client_builder.with_module(LightningClientInit::default());
        let mut client_builder = client_builder.with_iroh_enable_next(false);
        client_builder.with_meta_service(MetaService::new(MetaModuleMetaSourceWithFallback::<
            LegacyMetaSource,
        >::default()));

        let db = fedimint_rocksdb::RocksDb::open(self.datadir)
            .await?
            .into_database();

        // TODO: use config being present to decide if to open or join
        let client = if let Some(root_secret) = try_load_root_secret(&db).await? {
            client_builder.open(db, root_secret).await?
        } else {
            let root_secret = generate_root_secret(&db).await?;
            client_builder
                .preview(&self.federation)
                .await?
                .join(db, root_secret)
                .await?
        };

        Ok(Blitzi { client })
    }
}

async fn try_load_root_secret(db: &Database) -> anyhow::Result<Option<RootSecret>> {
    let Some(entropy) = Client::load_decodable_client_secret_opt::<Vec<u8>>(db).await? else {
        return Ok(None);
    };

    let mnemonic = Mnemonic::from_entropy(&entropy)?;

    Ok(Some(RootSecret::StandardDoubleDerive(
        Bip39RootSecretStrategy::<12>::to_root_secret(&mnemonic),
    )))
}

async fn generate_root_secret(db: &Database) -> anyhow::Result<RootSecret> {
    let mnemonic = Mnemonic::generate(12)?;
    let entropy = mnemonic.to_entropy();

    Client::store_encodable_client_secret(db, &entropy).await?;

    Ok(RootSecret::StandardDoubleDerive(Bip39RootSecretStrategy::<
        12,
    >::to_root_secret(
        &mnemonic
    )))
}

/// The Blitzi client that allows paying and receiving payments on Lightning.
///
/// ```no_run
/// # use anyhow::Result;
/// # use fedimint_core::hex;
/// use blitzi::{Amount, Blitzi};
///
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let blitzi = Blitzi::new().await?;
///
/// let invoice = blitzi
///     .lightning_invoice(Amount::from_msats(1000), "Test payment")
///     .await?;
/// println!("Invoice: {}", invoice);
///
/// let preimage = blitzi.pay(&invoice).await?;
/// println!("Preimage: {}", hex::encode(preimage));
///
/// # Ok(())
/// # }
/// ```
pub struct Blitzi {
    client: ClientHandle,
}

impl Blitzi {
    /// Creates a new Blitzi client with default settings.
    pub async fn new() -> anyhow::Result<Self> {
        Self::builder().build().await
    }

    /// Creates a new Blitzi builder for more control. If you just want to go
    /// with the defaults use [`Blitzi::new`] instead.
    pub fn builder() -> BlitziBuilder {
        BlitziBuilder::default()
    }

    fn ln_module(&self) -> ClientModuleInstance<'_, LightningClientModule> {
        self.client
            .get_first_module::<LightningClientModule>()
            .expect("LN module not found")
    }

    /// Returns the current balance held by Blitzi.
    ///
    /// If you want to be notified when the balance changes, use
    /// [`Self::subscribe_balance_changes`] instead.
    pub async fn balance(&self) -> Amount {
        self.client
            .get_balance()
            .await
            .expect("Primary module not available, should not happen")
    }

    /// Returns a stream that yields the current balance every time it changes.
    /// Returns the balance in millisatoshi right away.
    pub async fn subscribe_balance_changes(&self) -> BoxStream<'static, Amount> {
        self.client.subscribe_balance_changes().await
    }

    /// Generates a new Lightning invoice for the given `amount` (up to milli
    /// satoshi precision) containing the given `description`.
    ///
    /// # Errors
    /// Returns an error if no LN gateway is available or if the invoice cannot
    /// be generated for any other reason.
    pub async fn lightning_invoice(
        &self,
        amount: Amount,
        description: &str,
    ) -> anyhow::Result<Bolt11Invoice> {
        let ln_client = self.ln_module();

        let ln_gateway = ln_client
            .get_gateway(None, false)
            .await?
            .ok_or_else(|| anyhow!("No LN gateway available"))?;
        let (_, invoice, _) = ln_client
            .create_bolt11_invoice(
                amount,
                Bolt11InvoiceDescription::Direct(Description::new(description.into())?),
                None,
                (),
                Some(ln_gateway),
            )
            .await?;

        Ok(invoice)
    }

    /// Waits for an invoice generated using [`Self::lightning_invoice`] to be
    /// paid.
    ///
    /// Returns an error in case it times out. There is no need to call this
    /// function unless you need to know if an invoice was paid. The funds will
    /// be received either way.
    pub async fn await_incoming_payment(&self, invoice: &Bolt11Invoice) -> anyhow::Result<()> {
        self.await_incoming_payment_by_hash(invoice.payment_hash())
            .await
    }

    /// Waits for an invoice generated using [`Self::lightning_invoice`] to be
    /// paid. See [`Self::await_incoming_payment`] for more details.
    pub async fn await_incoming_payment_by_hash(
        &self,
        payment_hash: &sha256::Hash,
    ) -> anyhow::Result<()> {
        let operation_id = OperationId(*payment_hash.as_ref());

        let operation = self
            .client
            .operation_log()
            .get_operation(operation_id)
            .await
            .context(
                "No operation found for payment hash, was the invoice issued by us?".to_string(),
            )?;
        ensure!(
            operation.operation_module_kind() == "ln",
            "Operation associated with payment hash is not an LN operation"
        );

        let operation_meta = operation.meta::<LightningOperationMeta>();
        ensure!(
            matches!(
                operation_meta.variant,
                LightningOperationMetaVariant::Receive { .. }
            ),
            "Operation associated with the payment hash is not an incoming payment"
        );

        let ln_module = self.ln_module();
        let mut update_stream = ln_module
            .subscribe_ln_receive(operation_id)
            .await
            .context("Unexpected error subscribing to operation")?
            .into_stream();
        while let Some(update) = update_stream.next().await {
            match update {
                LnReceiveState::Canceled { reason } => {
                    return Err(anyhow!("Payment was canceled: {}", reason));
                }
                LnReceiveState::Claimed => {
                    return Ok(());
                }
                _ => {}
            }
        }

        unreachable!("Stream ended unexpectedly");
    }

    /// Pays an invoice and returns the preimage of the payment.
    ///
    /// If an payment was already made to the same invoice, the result of the
    /// previous payment will be returned again. This allows building safe retry
    /// logic that just tries to pay an invoice again if it's unclear if a
    /// previous call to this function succeeded or not (e.g. in the case of a
    /// crash).
    ///
    /// Retries are not supported for now since they will likely fail too if the
    /// original attempt failed and would add additional complexity.
    pub async fn pay(&self, invoice: &Bolt11Invoice) -> anyhow::Result<[u8; 32]> {
        let ln_client = self.ln_module();
        let operation_id = Self::get_payment_operation_id(invoice.payment_hash());
        let pay_type = if let Some(operation) = self
            .client
            .operation_log()
            .get_operation(operation_id)
            .await
        {
            match operation.meta::<LightningOperationMeta>().variant {
                LightningOperationMetaVariant::Pay(LightningOperationMetaPay {
                    is_internal_payment,
                    ..
                }) => {
                    if is_internal_payment {
                        PayType::Internal(operation_id)
                    } else {
                        PayType::Lightning(operation_id)
                    }
                }
                _ => {
                    return Err(anyhow!(
                        "Operation associated with the payment hash is not an incoming payment"
                    ));
                }
            }
        } else {
            let ln_gateway = ln_client
                .get_gateway(None, false)
                .await?
                .ok_or_else(|| anyhow!("No LN gateway available"))?;

            let payment = ln_client
                .pay_bolt11_invoice(Some(ln_gateway), invoice.clone(), ())
                .await?;
            payment.payment_type
        };

        let preimage = match pay_type {
            PayType::Internal(operation_id) => {
                match ln_client
                    .subscribe_internal_pay(operation_id)
                    .await?
                    .await_outcome()
                    .await
                    .context("No outcome found for payment, should never happen")?
                {
                    fedimint_ln_client::InternalPayState::Preimage(preimage) => preimage.0,
                    state => return Err(anyhow!("Payment failed: {:?}", state)),
                }
            }
            PayType::Lightning(operation_id) => {
                match ln_client
                    .subscribe_ln_pay(operation_id)
                    .await?
                    .await_outcome()
                    .await
                    .context("No outcome found for payment, should never happen")?
                {
                    fedimint_ln_client::LnPayState::Success { preimage } => hex::decode(preimage)
                        .context("Invalid preimage")?
                        .try_into()
                        .ok()
                        .context("Invalid preimage length")?,
                    state => return Err(anyhow!("Payment failed: {:?}", state)),
                }
            }
        };

        Ok(preimage)
    }

    fn get_payment_operation_id(payment_hash: &sha256::Hash) -> OperationId {
        // Copied from fedimint-ln-client
        fn get_payment_operation_id(payment_hash: &sha256::Hash, index: u16) -> OperationId {
            // Copy the 32 byte payment hash and a 2 byte index to make every payment
            // attempt have a unique `OperationId`
            let mut bytes = [0; 34];
            bytes[0..32].copy_from_slice(&payment_hash.to_byte_array());
            bytes[32..34].copy_from_slice(&index.to_le_bytes());
            let hash: sha256::Hash = BitcoinHash::hash(&bytes);
            OperationId(hash.to_byte_array())
        }

        // The first actual payment attempt starts at index 1 since index 0 is used to
        // indicate that there was no previous payment attempt.
        get_payment_operation_id(payment_hash, 1)
    }
}