Skip to main content

breez_sdk_spark/persist/backend/
mod.rs

1//! Storage backend abstraction.
2//!
3//! A [`StorageBackend`] produces the four per-tenant stores the SDK needs: the
4//! main [`Storage`], plus a [`TreeStore`], [`TokenOutputStore`] and
5//! [`SessionStore`]. Each built-in backend is an independent module — adding
6//! one touches nothing else, and there is no central enum of storage options.
7//!
8//! [`SdkBuilder::with_storage_backend`](crate::SdkBuilder::with_storage_backend)
9//! takes an `Arc<dyn StorageBackend>`; build one with [`default_storage`],
10//! [`postgres_storage`], [`mysql_storage`] or
11//! [`custom_storage`].
12
13use std::sync::Arc;
14
15use macros::async_trait;
16use spark_wallet::{SessionStore, TokenOutputStore, TreeStore};
17
18use crate::{Network, SdkError, persist::Storage};
19
20mod prebuilt;
21
22pub use prebuilt::PrebuiltBackend;
23
24#[cfg(feature = "sqlite")]
25mod sqlite;
26
27#[cfg(feature = "postgres")]
28mod postgres;
29
30#[cfg(feature = "mysql")]
31mod mysql;
32
33/// The four per-tenant stores produced by a [`StorageBackend`].
34///
35/// An opaque handle: the SDK reads its stores internally; the fields never
36/// cross the FFI boundary.
37#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
38pub struct ResolvedStores {
39    pub(crate) storage: Arc<dyn Storage>,
40    pub(crate) tree_store: Option<Arc<dyn TreeStore>>,
41    pub(crate) token_output_store: Option<Arc<dyn TokenOutputStore>>,
42    pub(crate) session_store: Option<Arc<dyn SessionStore>>,
43}
44
45/// A factory for a tenant's storage.
46///
47/// A single backend may back many SDK instances; each
48/// [`create_stores`](Self::create_stores) call yields the store set scoped to
49/// one tenant `identity` (a serialized public key). `network` lets file-based
50/// backends segregate tenants by network; database backends ignore it.
51#[cfg_attr(feature = "uniffi", uniffi::export)]
52#[async_trait]
53pub trait StorageBackend: Send + Sync {
54    async fn create_stores(
55        &self,
56        network: Network,
57        identity: Vec<u8>,
58    ) -> Result<Arc<ResolvedStores>, SdkError>;
59}
60
61/// Wraps a caller-supplied [`Storage`] implementation as a [`StorageBackend`].
62/// The tree, token-output and session stores use the in-memory defaults.
63#[cfg_attr(feature = "uniffi", uniffi::export)]
64#[must_use]
65pub fn custom_storage(storage: Arc<dyn Storage>) -> Arc<dyn StorageBackend> {
66    Arc::new(prebuilt::PrebuiltBackend::new(storage, None, None, None))
67}
68
69/// The session store `backend` provides for `identity` on `network` (its own
70/// persistence: `PostgreSQL`/`MySQL` when the backend is DB-backed, else an
71/// in-memory store).
72///
73/// Returned so it can be wrapped in a decorating [`SessionStore`] and passed to
74/// [`SdkBuilder::with_session_store`](crate::SdkBuilder::with_session_store),
75/// keeping the backend's persistence. A typical use is at-rest encryption (the
76/// SDK does not encrypt tokens itself): wrap it in a store that encrypts on
77/// write and decrypts on read. `identity` is the wallet identity public key
78/// bytes (the same value the SDK derives from the signer).
79#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
80pub async fn default_session_store(
81    backend: Arc<dyn StorageBackend>,
82    network: Network,
83    identity: Vec<u8>,
84) -> Result<Arc<dyn crate::session_store::SessionStore>, SdkError> {
85    let stores = backend.create_stores(network, identity).await?;
86    let inner = stores
87        .session_store
88        .clone()
89        .unwrap_or_else(|| Arc::new(spark_wallet::InMemorySessionStore::default()));
90    Ok(Arc::new(crate::session_store::SparkSessionStoreAdapter(
91        inner,
92    )))
93}
94
95/// File-based `SQLite` storage rooted at `storage_dir` — the default for
96/// mobile and desktop apps. Each tenant gets its own database file under the
97/// directory.
98#[cfg(feature = "sqlite")]
99#[cfg_attr(feature = "uniffi", uniffi::export)]
100#[must_use]
101pub fn default_storage(storage_dir: String) -> Arc<dyn StorageBackend> {
102    Arc::new(sqlite::SqliteBackend::new(storage_dir))
103}
104
105/// `PostgreSQL`-backed storage built from `config`. Opens the connection pool;
106/// fails if `config` is invalid, or if this build of the SDK does not include
107/// `PostgreSQL` support.
108#[cfg(any(feature = "postgres", feature = "db-storage-api"))]
109#[cfg_attr(feature = "uniffi", uniffi::export)]
110#[allow(clippy::needless_pass_by_value)]
111pub fn postgres_storage(
112    config: crate::persist::postgres::PostgresStorageConfig,
113) -> Result<Arc<dyn StorageBackend>, SdkError> {
114    #[cfg(feature = "postgres")]
115    {
116        let run_migration = config.run_migration;
117        let pool = crate::persist::postgres::create_pool(&config)?;
118        Ok(Arc::new(postgres::PostgresBackend::new(
119            pool,
120            run_migration,
121        )))
122    }
123    #[cfg(not(feature = "postgres"))]
124    {
125        let _ = config;
126        Err(backend_unavailable("PostgreSQL"))
127    }
128}
129
130/// `MySQL`-backed storage built from `config`. Opens the connection pool;
131/// fails if `config` is invalid, or if this build of the SDK does not include
132/// `MySQL` support.
133#[cfg(any(feature = "mysql", feature = "db-storage-api"))]
134#[cfg_attr(feature = "uniffi", uniffi::export)]
135#[allow(clippy::needless_pass_by_value)]
136pub fn mysql_storage(
137    config: crate::persist::mysql::MysqlStorageConfig,
138) -> Result<Arc<dyn StorageBackend>, SdkError> {
139    #[cfg(feature = "mysql")]
140    {
141        let run_migration = config.run_migration;
142        let foreign_key_mode = config.foreign_key_mode;
143        let pool = crate::persist::mysql::create_pool(&config)?;
144        Ok(Arc::new(mysql::MysqlBackend::new(
145            pool,
146            run_migration,
147            foreign_key_mode,
148        )))
149    }
150    #[cfg(not(feature = "mysql"))]
151    {
152        let _ = config;
153        Err(backend_unavailable("MySQL"))
154    }
155}
156
157/// With `db-storage-api`, every database backend is exported so the API is the
158/// same in every build. A build without a backend's feature rejects it at
159/// runtime.
160#[cfg(all(
161    feature = "db-storage-api",
162    any(not(feature = "postgres"), not(feature = "mysql"))
163))]
164fn backend_unavailable(backend: &str) -> SdkError {
165    SdkError::Generic(format!(
166        "{backend} storage is not available in this build of the SDK"
167    ))
168}