1use std::{marker::PhantomData, path::PathBuf};
2
3use anyhow::{Context, anyhow, ensure};
4use common::{
5 api::{
6 auth::{
7 UserSignupRequestWire, UserSignupRequestWireV1,
8 UserSignupRequestWireV2,
9 },
10 user::{NodePkProof, UserPk},
11 },
12 rng::Crng,
13 root_seed::RootSeed,
14};
15use lexe_api::{
16 def::{AppBackendApi, AppNodeRunApi},
17 models::command::{
18 EnclavesToProvisionRequest, LxPaymentIdStruct, PayInvoiceRequest,
19 UpdatePaymentNote,
20 },
21 types::payments::PaymentCreatedIndex,
22};
23use node_client::{
24 client::{GatewayClient, NodeClient},
25 credentials::CredentialsRef,
26};
27use payment_uri::{
28 bip353::{self, Bip353Client},
29 lnurl::LnurlClient,
30};
31use sdk_core::models::{
32 SdkCreateInvoiceRequest, SdkCreateInvoiceResponse, SdkGetPaymentRequest,
33 SdkGetPaymentResponse, SdkNodeInfo, SdkPayInvoiceRequest,
34 SdkPayInvoiceResponse,
35};
36use tracing::info;
37
38use crate::{
39 config::{
40 WalletEnvConfig, WalletEnvDbConfig, WalletUserConfig,
41 WalletUserDbConfig,
42 },
43 payments_db::PaymentsDb,
44 unstable::{ffs::DiskFs, provision, wallet_db::WalletDb},
45};
46
47pub struct WithDb;
49pub struct WithoutDb;
51
52pub struct LexeWallet<Db> {
56 user_config: WalletUserConfig,
57
58 db: Option<WalletDb<DiskFs>>,
61
62 gateway_client: GatewayClient,
63 node_client: NodeClient,
64 #[allow(dead_code)] bip353_client: Bip353Client,
66 #[allow(dead_code)] lnurl_client: LnurlClient,
68
69 _marker: PhantomData<Db>,
70}
71
72impl LexeWallet<WithDb> {
76 pub fn fresh(
85 rng: &mut impl Crng,
86 env_config: WalletEnvConfig,
87 credentials: CredentialsRef<'_>,
88 lexe_data_dir: Option<PathBuf>,
89 ) -> anyhow::Result<Self> {
90 let lexe_data_dir =
91 lexe_data_dir.map_or_else(crate::default_lexe_data_dir, Ok)?;
92 let env_db_config =
93 WalletEnvDbConfig::new(env_config.wallet_env, lexe_data_dir);
94 let user_db_config =
95 WalletUserDbConfig::from_credentials(credentials, env_db_config)?;
96
97 let db = WalletDb::fresh(user_db_config)
98 .context("Failed to create fresh wallet db")?;
99
100 Self::with_db(rng, env_config, credentials, db)
101 }
102
103 pub fn load(
120 rng: &mut impl Crng,
121 env_config: WalletEnvConfig,
122 credentials: CredentialsRef<'_>,
123 lexe_data_dir: Option<PathBuf>,
124 ) -> anyhow::Result<Option<Self>> {
125 let lexe_data_dir =
126 lexe_data_dir.map_or_else(crate::default_lexe_data_dir, Ok)?;
127 let env_db_config =
128 WalletEnvDbConfig::new(env_config.wallet_env, lexe_data_dir);
129 let user_db_config =
130 WalletUserDbConfig::from_credentials(credentials, env_db_config)?;
131
132 let maybe_db = WalletDb::load(user_db_config)
133 .context("Failed to load wallet db")?;
134 let db = match maybe_db {
135 Some(d) => d,
136 None => return Ok(None),
137 };
138
139 Self::with_db(rng, env_config, credentials, db).map(Some)
140 }
141
142 pub fn load_or_fresh(
152 rng: &mut impl Crng,
153 env_config: WalletEnvConfig,
154 credentials: CredentialsRef<'_>,
155 lexe_data_dir: Option<PathBuf>,
156 ) -> anyhow::Result<Self> {
157 let lexe_data_dir =
158 lexe_data_dir.map_or_else(crate::default_lexe_data_dir, Ok)?;
159 let env_db_config =
160 WalletEnvDbConfig::new(env_config.wallet_env, lexe_data_dir);
161 let user_db_config =
162 WalletUserDbConfig::from_credentials(credentials, env_db_config)?;
163
164 let db = WalletDb::load_or_fresh(user_db_config)
165 .context("Failed to load or create wallet db")?;
166
167 Self::with_db(rng, env_config, credentials, db)
168 }
169
170 fn with_db(
172 rng: &mut impl Crng,
173 env_config: WalletEnvConfig,
174 credentials: CredentialsRef<'_>,
175 db: WalletDb<DiskFs>,
176 ) -> anyhow::Result<Self> {
177 let user_pk = credentials.user_pk().context(
178 "Client credentials are out of date. \
179 Please create a new one from within the Lexe wallet app.",
180 )?;
181
182 let user_config = WalletUserConfig {
183 user_pk,
184 env_config: env_config.clone(),
185 };
186
187 let gateway_client = GatewayClient::new(
188 env_config.wallet_env.deploy_env,
189 env_config.gateway_url.clone(),
190 env_config.user_agent.clone(),
191 )
192 .context("Failed to build GatewayClient")?;
193
194 let node_client = NodeClient::new(
195 rng,
196 env_config.wallet_env.use_sgx,
197 env_config.wallet_env.deploy_env,
198 gateway_client.clone(),
199 credentials,
200 )
201 .context("Failed to build NodeClient")?;
202
203 let bip353_client = Bip353Client::new(bip353::GOOGLE_DOH_ENDPOINT)
204 .context("Failed to build BIP353 client")?;
205
206 let lnurl_client = LnurlClient::new(env_config.wallet_env.deploy_env)
207 .context("Failed to build LNURL client")?;
208
209 Ok(Self {
210 user_config,
211 db: Some(db),
212 gateway_client,
213 node_client,
214 bip353_client,
215 lnurl_client,
216 _marker: PhantomData,
217 })
218 }
219
220 #[cfg(feature = "unstable")]
222 pub fn db(&self) -> &WalletDb<DiskFs> {
223 self.db.as_ref().expect("WithDb always has db")
224 }
225
226 pub fn payments_db(&self) -> &PaymentsDb<DiskFs> {
229 self.db
230 .as_ref()
231 .expect("WithDb always has db")
232 .payments_db()
233 }
234
235 pub async fn sync_payments(
241 &self,
242 ) -> anyhow::Result<crate::payments_db::PaymentSyncSummary> {
243 self.db
244 .as_ref()
245 .expect("WithDb always has db")
246 .sync_payments(
247 &self.node_client,
248 common::constants::DEFAULT_PAYMENTS_BATCH_SIZE,
249 )
250 .await
251 }
252}
253
254impl LexeWallet<WithoutDb> {
255 pub fn without_db(
261 rng: &mut impl Crng,
262 env_config: WalletEnvConfig,
263 credentials: CredentialsRef<'_>,
264 ) -> anyhow::Result<Self> {
265 let user_pk = credentials.user_pk().context(
266 "Client credentials are out of date. \
267 Please create a new one from within the Lexe wallet app.",
268 )?;
269
270 let user_config = WalletUserConfig {
271 user_pk,
272 env_config: env_config.clone(),
273 };
274
275 let gateway_client = GatewayClient::new(
276 env_config.wallet_env.deploy_env,
277 env_config.gateway_url.clone(),
278 env_config.user_agent.clone(),
279 )
280 .context("Failed to build GatewayClient")?;
281
282 let node_client = NodeClient::new(
283 rng,
284 env_config.wallet_env.use_sgx,
285 env_config.wallet_env.deploy_env,
286 gateway_client.clone(),
287 credentials,
288 )
289 .context("Failed to build NodeClient")?;
290
291 let bip353_client = Bip353Client::new(bip353::GOOGLE_DOH_ENDPOINT)
292 .context("Failed to build BIP353 client")?;
293
294 let lnurl_client = LnurlClient::new(env_config.wallet_env.deploy_env)
295 .context("Failed to build LNURL client")?;
296
297 Ok(Self {
298 user_config,
299 db: None,
300 gateway_client,
301 node_client,
302 bip353_client,
303 lnurl_client,
304 _marker: PhantomData,
305 })
306 }
307}
308
309impl<D> LexeWallet<D> {
310 pub async fn signup(
329 &self,
330 rng: &mut impl Crng,
331 root_seed: &RootSeed,
332 partner_pk: Option<UserPk>,
333 ) -> anyhow::Result<()> {
334 self.signup_inner(
335 rng, root_seed, partner_pk, None, false, None, None, )
340 .await
341 }
342
343 #[cfg(feature = "unstable")]
346 pub async fn signup_custom(
347 &self,
348 rng: &mut impl Crng,
349 root_seed: &RootSeed,
350 partner_pk: Option<UserPk>,
351 signup_code: Option<String>,
352 allow_gvfs_access: bool,
353 backup_password: Option<&str>,
354 google_auth_code: Option<String>,
355 ) -> anyhow::Result<()> {
356 self.signup_inner(
357 rng,
358 root_seed,
359 partner_pk,
360 signup_code,
361 allow_gvfs_access,
362 backup_password,
363 google_auth_code,
364 )
365 .await
366 }
367
368 #[cfg_attr(not(feature = "unstable"), allow(dead_code))]
370 async fn signup_inner(
371 &self,
372 rng: &mut impl Crng,
373 root_seed: &RootSeed,
374 partner_pk: Option<UserPk>,
375 signup_code: Option<String>,
376 allow_gvfs_access: bool,
377 backup_password: Option<&str>,
378 google_auth_code: Option<String>,
379 ) -> anyhow::Result<()> {
380 let user_key_pair = root_seed.derive_user_key_pair();
382 let node_key_pair = root_seed.derive_node_key_pair(rng);
383 let node_pk_proof = NodePkProof::sign(rng, &node_key_pair);
384
385 let signup_req = UserSignupRequestWire::V2(UserSignupRequestWireV2 {
386 v1: UserSignupRequestWireV1 {
387 node_pk_proof,
388 signup_code,
389 },
390 partner: partner_pk,
391 });
392 let signed_signup_req = user_key_pair
393 .sign_struct(&signup_req)
394 .map(|(_buf, signed)| signed)
395 .expect("Should never fail to serialize UserSignupRequestWire");
396
397 self.gateway_client
399 .signup_v2(&signed_signup_req)
400 .await
401 .context("Failed to signup user")?;
402
403 let encrypted_seed = backup_password
406 .map(|password| root_seed.password_encrypt(rng, password))
407 .transpose()
408 .context("Could not encrypt root seed under password")?;
409
410 let credentials = CredentialsRef::from(root_seed);
412 self.provision_inner(
413 credentials,
414 allow_gvfs_access,
415 encrypted_seed,
416 google_auth_code,
417 )
418 .await
419 .context("Initial provision failed")?;
420
421 Ok(())
422 }
423
424 pub async fn provision(
431 &self,
432 credentials: CredentialsRef<'_>,
433 ) -> anyhow::Result<()> {
434 self.provision_inner(
435 credentials,
436 false, None, None, )
440 .await
441 }
442
443 #[cfg(feature = "unstable")]
446 pub async fn provision_custom(
447 &self,
448 credentials: CredentialsRef<'_>,
449 allow_gvfs_access: bool,
450 encrypted_seed: Option<Vec<u8>>,
451 google_auth_code: Option<String>,
452 ) -> anyhow::Result<()> {
453 self.provision_inner(
454 credentials,
455 allow_gvfs_access,
456 encrypted_seed,
457 google_auth_code,
458 )
459 .await
460 }
461
462 #[cfg_attr(not(feature = "unstable"), allow(dead_code))]
464 async fn provision_inner(
465 &self,
466 credentials: CredentialsRef<'_>,
467 allow_gvfs_access: bool,
468 encrypted_seed: Option<Vec<u8>>,
469 google_auth_code: Option<String>,
470 ) -> anyhow::Result<()> {
471 let CredentialsRef::RootSeed(_root_seed_ref) = credentials else {
473 return Err(anyhow!(
474 "Delegated provisioning is not implemented yet"
475 ));
476 };
477
478 let wallet_env = self.user_config.env_config.wallet_env;
479
480 let token = self
482 .node_client
483 .request_provision_token()
484 .await
485 .context("Could not get bearer token")?;
486
487 let req = EnclavesToProvisionRequest {
489 trusted_measurements: provision::LATEST_TRUSTED_MEASUREMENTS
490 .clone(),
491 };
492
493 let enclaves_to_provision = self
494 .gateway_client
495 .enclaves_to_provision(&req, token)
496 .await
497 .context("Could not fetch enclaves to provision")?;
498
499 if wallet_env.deploy_env.is_staging_or_prod() {
502 let all_trusted =
503 enclaves_to_provision.enclaves.iter().all(|enclave| {
504 provision::LATEST_TRUSTED_MEASUREMENTS
505 .contains(&enclave.measurement)
506 });
507 ensure!(all_trusted, "Backend returned untrusted enclaves:");
508 }
509
510 if enclaves_to_provision.enclaves.is_empty() {
511 info!("Already provisioned to all recent releases");
512 return Ok(());
513 }
514
515 info!(
516 "Provisioning enclaves: {:?}",
517 enclaves_to_provision.enclaves
518 );
519
520 match credentials {
521 CredentialsRef::RootSeed(root_seed_ref) => {
522 let root_seed = provision::clone_root_seed(root_seed_ref);
523
524 provision::provision_all(
525 self.node_client.clone(),
526 enclaves_to_provision.enclaves,
527 root_seed,
528 wallet_env,
529 google_auth_code,
530 allow_gvfs_access,
531 encrypted_seed,
532 )
533 .await
534 .context("Root seed provision_all failed")?;
535 }
536 CredentialsRef::ClientCredentials(_) =>
538 return Err(anyhow!(
539 "Delegated provisioning is not implemented yet"
540 )),
541 }
542
543 Ok(())
544 }
545
546 pub fn user_config(&self) -> &WalletUserConfig {
548 &self.user_config
549 }
550
551 #[cfg(feature = "unstable")]
553 pub fn gateway_client(&self) -> &GatewayClient {
554 &self.gateway_client
555 }
556
557 #[cfg(feature = "unstable")]
559 pub fn node_client(&self) -> &NodeClient {
560 &self.node_client
561 }
562
563 #[cfg(feature = "unstable")]
565 pub fn bip353_client(&self) -> &Bip353Client {
566 &self.bip353_client
567 }
568
569 #[cfg(feature = "unstable")]
571 pub fn lnurl_client(&self) -> &LnurlClient {
572 &self.lnurl_client
573 }
574
575 pub async fn node_info(&self) -> anyhow::Result<SdkNodeInfo> {
579 self.node_client
580 .node_info()
581 .await
582 .map(SdkNodeInfo::from)
583 .context("Failed to get node info")
584 }
585
586 pub async fn create_invoice(
588 &self,
589 req: SdkCreateInvoiceRequest,
590 ) -> anyhow::Result<SdkCreateInvoiceResponse> {
591 let resp = self
592 .node_client
593 .create_invoice(req.into())
594 .await
595 .context("Failed to create invoice")?;
596
597 let index = resp.created_index.context("Node is out of date")?;
598
599 Ok(SdkCreateInvoiceResponse::new(index, resp.invoice))
600 }
601
602 pub async fn pay_invoice(
604 &self,
605 req: SdkPayInvoiceRequest,
606 ) -> anyhow::Result<SdkPayInvoiceResponse> {
607 let id = req.invoice.payment_id();
608 let resp = self
609 .node_client
610 .pay_invoice(PayInvoiceRequest::from(req))
611 .await
612 .context("Failed to pay invoice")?;
613
614 let index = PaymentCreatedIndex {
615 created_at: resp.created_at,
616 id,
617 };
618
619 Ok(SdkPayInvoiceResponse {
620 index,
621 created_at: resp.created_at,
622 })
623 }
624
625 pub async fn get_payment(
627 &self,
628 req: SdkGetPaymentRequest,
629 ) -> anyhow::Result<SdkGetPaymentResponse> {
630 let id = req.index.id;
631 let payment = self
632 .node_client
633 .get_payment_by_id(LxPaymentIdStruct { id })
634 .await
635 .context("Failed to get payment")?
636 .maybe_payment
637 .map(Into::into);
638
639 Ok(SdkGetPaymentResponse { payment })
640 }
641
642 pub async fn update_payment_note(
644 &self,
645 req: UpdatePaymentNote,
646 ) -> anyhow::Result<()> {
647 self.node_client
649 .update_payment_note(req.clone())
650 .await
651 .context("Failed to update payment note on user node")?;
652
653 if let Some(db) = &self.db {
655 db.payments_db().update_payment_note(req)?;
656 }
657
658 Ok(())
659 }
660}