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
//! Synchronous (blocking) wrapper around
//! [`LexeWallet`](crate::wallet::LexeWallet).
//!
//! Enabled by the `blocking` feature flag.
//
// All async methods are executed using `block_on`, which wraps the future
// with `async_compat::Compat` so it runs on the shared tokio runtime used
// by UniFFI, then blocks the current thread until the future completes.
use std::{path::PathBuf, time::Duration};
use lexe_api::types::payments::PaymentCreatedIndex;
#[cfg(feature = "unstable")]
use crate::unstable;
use crate::{
config::WalletEnvConfig,
types::{
auth::{CredentialsRef, RootSeed, UserPk},
command::{
AnalyzeRequest, AnalyzeResponse, CreateInvoiceRequest,
CreateInvoiceResponse, CreateOfferRequest, CreateOfferResponse,
GetPaymentRequest, GetPaymentResponse, ListPaymentsResponse,
NodeInfo, PayInvoiceRequest, PayInvoiceResponse, PayOfferRequest,
PayOfferResponse, PayRequest, PayResponse, PaymentSyncSummary,
UpdatePaymentNoteRequest,
},
payment::{Order, Payment, PaymentFilter},
},
wallet::LexeWallet,
};
/// Synchronous wallet handle. Provides the same API as [`LexeWallet`] but
/// with blocking methods instead of async.
pub struct BlockingLexeWallet {
inner: LexeWallet,
}
impl BlockingLexeWallet {
// --- Constructors --- //
/// Create a fresh [`BlockingLexeWallet`], deleting any existing database
/// state for this user. Data for other users and environments is not
/// affected.
///
/// It is recommended to always pass the same `lexe_data_dir`,
/// regardless of which environment we're in (dev/staging/prod) and which
/// user this wallet is for. Users and environments will not interfere
/// with each other as all data is namespaced internally.
/// Defaults to `~/.lexe` if not specified.
pub fn fresh(
env_config: WalletEnvConfig,
credentials: CredentialsRef<'_>,
lexe_data_dir: Option<PathBuf>,
) -> anyhow::Result<Self> {
let inner = LexeWallet::fresh(env_config, credentials, lexe_data_dir)?;
Ok(Self { inner })
}
/// Load an existing [`BlockingLexeWallet`] with persistence from
/// `lexe_data_dir`. Returns [`None`] if no local data exists, in which
/// case you should use [`fresh`](Self::fresh) to create the wallet.
///
/// If you are authenticating with [`RootSeed`]s and this returns [`None`],
/// you should call [`signup`](Self::signup) after creating the wallet if
/// you're not sure whether the user has been signed up with Lexe.
///
/// It is recommended to always pass the same `lexe_data_dir`,
/// regardless of which environment we're in (dev/staging/prod) and which
/// user this wallet is for. Users and environments will not interfere
/// with each other as all data is namespaced internally.
/// Defaults to `~/.lexe` if not specified.
pub fn load(
env_config: WalletEnvConfig,
credentials: CredentialsRef<'_>,
lexe_data_dir: Option<PathBuf>,
) -> anyhow::Result<Option<Self>> {
LexeWallet::load(env_config, credentials, lexe_data_dir)
.map(|opt| opt.map(|inner| Self { inner }))
}
/// Load an existing [`BlockingLexeWallet`] with persistence from
/// `lexe_data_dir`, or create a fresh one if no local data exists. If you
/// are authenticating with client credentials, this is generally what you
/// want to use.
///
/// It is recommended to always pass the same `lexe_data_dir`,
/// regardless of which environment we're in (dev/staging/prod) and which
/// user this wallet is for. Users and environments will not interfere
/// with each other as all data is namespaced internally.
/// Defaults to `~/.lexe` if not specified.
pub fn load_or_fresh(
env_config: WalletEnvConfig,
credentials: CredentialsRef<'_>,
lexe_data_dir: Option<PathBuf>,
) -> anyhow::Result<Self> {
let inner =
LexeWallet::load_or_fresh(env_config, credentials, lexe_data_dir)?;
Ok(Self { inner })
}
/// Create a [`BlockingLexeWallet`] without any persistence.
/// It is recommended to use [`fresh`] or [`load`] instead, to initialize
/// with persistence.
///
/// Node operations (invoices, payments, node info) work normally.
/// Local payment cache operations ([`sync_payments`], [`list_payments`],
/// [`clear_payments`]) are not available and will return an error.
///
/// [`fresh`]: BlockingLexeWallet::fresh
/// [`load`]: BlockingLexeWallet::load
/// [`sync_payments`]: BlockingLexeWallet::sync_payments
/// [`list_payments`]: BlockingLexeWallet::list_payments
/// [`clear_payments`]: BlockingLexeWallet::clear_payments
pub fn without_db(
env_config: WalletEnvConfig,
credentials: CredentialsRef<'_>,
) -> anyhow::Result<Self> {
let inner = LexeWallet::without_db(env_config, credentials)?;
Ok(Self { inner })
}
// --- DB-required methods --- //
/// Sync payments from the user node to the local payments cache.
///
/// Returns an error if local persistence is disabled for this wallet.
pub fn sync_payments(&self) -> anyhow::Result<PaymentSyncSummary> {
block_on(self.inner.sync_payments())
}
/// List payments from local storage with cursor-based pagination.
///
/// Defaults to descending order (newest first) with a limit of 100.
///
/// To continue paginating, set `after` to the `next_index` from the
/// previous response. `after` is an *exclusive* index.
///
/// If needed, use [`sync_payments`] to fetch the latest data from the
/// node before calling this method.
///
/// Returns an error if local persistence is disabled for this wallet.
///
/// [`sync_payments`]: Self::sync_payments
pub fn list_payments(
&self,
filter: &PaymentFilter,
order: Option<Order>,
limit: Option<usize>,
after: Option<&PaymentCreatedIndex>,
) -> anyhow::Result<ListPaymentsResponse> {
self.inner.list_payments(filter, order, limit, after)
}
/// Clear all locally cached payment data for this wallet.
///
/// Clears the local payment cache only. Remote data on the node is not
/// affected. Call [`sync_payments`](Self::sync_payments) to re-populate.
///
/// Returns an error if local persistence is disabled for this wallet.
pub fn clear_payments(&self) -> anyhow::Result<()> {
self.inner.clear_payments()
}
/// Wait for a payment to reach a terminal state (completed or failed).
///
/// Polls the node with exponential backoff until the payment finalizes or
/// the timeout is reached. Defaults to 600 seconds (10 minutes).
/// Maximum timeout is 86,400 seconds (24 hours).
pub fn wait_for_payment(
&self,
index: PaymentCreatedIndex,
timeout: Option<Duration>,
) -> anyhow::Result<Payment> {
block_on(self.inner.wait_for_payment(index, timeout))
}
/// Get a reference to the
/// [`WalletDb`](crate::unstable::wallet_db::WalletDb).
///
/// Returns [`None`] if local persistence is disabled for this wallet.
#[cfg(feature = "unstable")]
pub fn db(
&self,
) -> Option<&unstable::wallet_db::WalletDb<unstable::ffs::DiskFs>> {
self.inner.db()
}
/// Get a reference to the payments database.
/// This is the primary data source for constructing a payments
/// list UI.
///
/// Returns [`None`] if local persistence is disabled for this wallet.
#[cfg(feature = "unstable")]
pub fn payments_db(
&self,
) -> Option<&unstable::payments_db::PaymentsDb<unstable::ffs::DiskFs>> {
self.inner.payments_db()
}
// --- Shared methods --- //
/// Get a reference to the user's wallet configuration.
pub fn user_config(&self) -> &crate::config::WalletUserConfig {
self.inner.user_config()
}
/// Registers this user with Lexe, then provisions the node.
/// This method must be called after the user's [`BlockingLexeWallet`]
/// has been created for the first time, otherwise subsequent requests
/// will fail.
///
/// It is only necessary to call this method once, ever, per user, but
/// it is also okay to call this method even if the user has already
/// been signed up; in other words, this method is idempotent.
///
/// After a successful signup, make sure the user's root seed has been
/// persisted somewhere! Without access to their root seed, your user
/// will lose their funds forever. If adding Lexe to a broader wallet,
/// a good strategy is to derive Lexe's [`RootSeed`] from your own
/// root seed.
///
/// - `partner_pk`: Set to your company's [`UserPk`] to earn a share of this
/// wallet's fees.
pub fn signup(
&self,
root_seed: &RootSeed,
partner_pk: Option<UserPk>,
) -> anyhow::Result<()> {
block_on(self.inner.signup(root_seed, partner_pk))
}
/// [`signup`](Self::signup) but with extra parameters generally only used
/// by the Lexe App.
#[cfg(feature = "unstable")]
pub fn signup_custom(
&self,
root_seed: &RootSeed,
partner_pk: Option<UserPk>,
allow_gvfs_access: bool,
backup_password: Option<&str>,
google_auth_code: Option<String>,
) -> anyhow::Result<()> {
block_on(self.inner.signup_custom(
root_seed,
partner_pk,
allow_gvfs_access,
backup_password,
google_auth_code,
))
}
/// Ensures the wallet is provisioned to all recent trusted releases.
/// This should be called every time the wallet is loaded, to ensure the
/// node is running the most up-to-date enclave software.
///
/// This fetches the current enclaves from the gateway, computes which
/// releases need to be provisioned, and provisions them.
pub fn provision(
&self,
credentials: CredentialsRef<'_>,
) -> anyhow::Result<()> {
block_on(self.inner.provision(credentials))
}
/// [`provision`](Self::provision) but with extra parameters generally only
/// used by the Lexe App.
#[cfg(feature = "unstable")]
pub fn provision_custom(
&self,
credentials: CredentialsRef<'_>,
allow_gvfs_access: bool,
encrypted_seed: Option<Vec<u8>>,
google_auth_code: Option<String>,
) -> anyhow::Result<()> {
block_on(self.inner.provision_custom(
credentials,
allow_gvfs_access,
encrypted_seed,
google_auth_code,
))
}
/// Get a reference to the
/// [`GatewayClient`](lexe_node_client::client::GatewayClient).
#[cfg(feature = "unstable")]
pub fn gateway_client(&self) -> &lexe_node_client::client::GatewayClient {
self.inner.gateway_client()
}
/// Get a reference to the
/// [`NodeClient`](lexe_node_client::client::NodeClient).
#[cfg(feature = "unstable")]
pub fn node_client(&self) -> &lexe_node_client::client::NodeClient {
self.inner.node_client()
}
/// Get a reference to the
/// [`Bip353Client`](lexe_payment_uri::bip353::Bip353Client).
#[cfg(feature = "unstable")]
pub fn bip353_client(&self) -> &lexe_payment_uri::bip353::Bip353Client {
self.inner.bip353_client()
}
/// Get a reference to the
/// [`LnurlClient`](lexe_payment_uri::lnurl::LnurlClient).
#[cfg(feature = "unstable")]
pub fn lnurl_client(&self) -> &lexe_payment_uri::lnurl::LnurlClient {
self.inner.lnurl_client()
}
// --- Command API --- //
/// Get information about this Lexe node, including balance and channels.
pub fn node_info(&self) -> anyhow::Result<NodeInfo> {
block_on(self.inner.node_info())
}
/// Get information about a Bitcoin or Lightning payment string, including:
/// - `payable`: The payable string encoding the payment method.
/// - `method`: The [`PaymentMethod`] struct encapsulating information
/// specific to the payment method (e.g. payment hash, metadata, etc...)
/// - `amount`/`min_amount`/`max_amount`: The amount constraints requested
/// by the receiver.
///
/// See [`PayableDetails`] for all fields.
///
/// The following encodings are supported:
/// - BIP 321 URI: `bitcoin:bc1...`
/// - Lightning URI: `lightning:ln...`
/// - BOLT 11 invoice: `lnbc1...`
/// - BOLT 12 offer: `lno1...`
/// - Onchain bitcoin address: `bc1...`
/// - Human Bitcoin Address: `â‚¿satoshi@lexe.app`
/// - Lightning Address: `satoshi@lexe.app`
/// - LNURL: `lnurl1...` or `lnurlp://domain.com/path`
///
/// Within the encodings, the following payment methods are supported:
/// - BOLT 11 invoice
/// - BOLT 12 offer
/// - Bitcoin address
/// - Lightning Address
/// - LNURL
///
/// [`PaymentMethod`]: lexe_payment_uri::PaymentMethod
/// [`PayableDetails`]: crate::types::command::PayableDetails
// Sync the encodings list with `pay`
pub fn analyze(
&self,
req: AnalyzeRequest,
) -> anyhow::Result<AnalyzeResponse> {
block_on(self.inner.analyze(req))
}
/// Pay any string which encodes a Bitcoin or Lightning payment method.
///
/// If there exist multiple encoded payment methods, one best recommended
/// payment method will be chosen.
///
/// For finer control over how to pay, consider first using
/// [`analyze`](Self::analyze) to resolve the contents of the
/// payable string, then invoking the specific `pay` function for the
/// payment method of choice: [`pay_invoice`](Self::pay_invoice),
/// [`pay_offer`](Self::pay_offer), etc.
///
/// The following encodings are supported:
/// - BIP 321 URI: `bitcoin:bc1...`
/// - Lightning URI: `lightning:ln...`
/// - BOLT 11 invoice: `lnbc1...`
/// - BOLT 12 offer: `lno1...`
/// - Onchain bitcoin address: `bc1...`
/// - Human Bitcoin Address: `â‚¿satoshi@lexe.app`
/// - Lightning Address: `satoshi@lexe.app`
/// - LNURL: `lnurl1...` or `lnurlp://domain.com/path`
///
/// See [`PaymentMethod`] for more details on supported payment methods.
///
/// [`PaymentMethod`]: lexe_payment_uri::PaymentMethod
// Sync the encodings list with `analyze`
pub fn pay(&self, req: PayRequest) -> anyhow::Result<PayResponse> {
block_on(self.inner.pay(req))
}
/// Create a BOLT 11 invoice to receive a Lightning payment.
pub fn create_invoice(
&self,
req: CreateInvoiceRequest,
) -> anyhow::Result<CreateInvoiceResponse> {
block_on(self.inner.create_invoice(req))
}
/// Pay a BOLT 11 invoice over Lightning.
pub fn pay_invoice(
&self,
req: PayInvoiceRequest,
) -> anyhow::Result<PayInvoiceResponse> {
block_on(self.inner.pay_invoice(req))
}
/// Create a BOLT 12 offer to receive Lightning payments.
///
/// Unlike invoices, offers are reusable: multiple payments can be made to
/// it, including from multiple payers.
pub fn create_offer(
&self,
req: CreateOfferRequest,
) -> anyhow::Result<CreateOfferResponse> {
block_on(self.inner.create_offer(req))
}
/// Pay a BOLT 12 offer over Lightning.
pub fn pay_offer(
&self,
req: PayOfferRequest,
) -> anyhow::Result<PayOfferResponse> {
block_on(self.inner.pay_offer(req))
}
/// Get information about a payment by its created index.
pub fn get_payment(
&self,
req: GetPaymentRequest,
) -> anyhow::Result<GetPaymentResponse> {
block_on(self.inner.get_payment(req))
}
/// Update the personal note on an existing payment.
/// The note is stored on the user node and is not visible to the
/// counterparty.
pub fn update_payment_note(
&self,
req: UpdatePaymentNoteRequest,
) -> anyhow::Result<()> {
block_on(self.inner.update_payment_note(req))
}
}
/// Wraps the future with `async_compat::Compat` so it runs on the shared
/// tokio runtime, then blocks with `futures::executor::block_on`.
fn block_on<F: std::future::Future>(f: F) -> F::Output {
let f = async_compat::Compat::new(f);
futures::executor::block_on(f)
}