Просмотр исходного кода

app: migrate all kvdbs (except chatview) to a unified turso sql db. is easier to work with

darkfi 7 часов назад
Родитель
Сommit
116b3bba4c

+ 21 - 19
bin/app/Cargo.lock

@@ -221,7 +221,7 @@ version = "1.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
 dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
@@ -232,7 +232,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
 dependencies = [
  "anstyle",
  "once_cell_polyfill",
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
@@ -1121,7 +1121,7 @@ version = "3.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f"
 dependencies = [
- "darling 0.23.0",
+ "darling 0.21.3",
  "ident_case",
  "prettyplease",
  "proc-macro2",
@@ -1485,7 +1485,7 @@ version = "3.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
 dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -1967,6 +1967,7 @@ dependencies = [
  "tracing-android",
  "tracing-appender",
  "tracing-subscriber",
+ "turso",
  "unic-langid",
  "url",
  "x11-clipboard",
@@ -2173,6 +2174,7 @@ dependencies = [
  "ident_case",
  "proc-macro2",
  "quote",
+ "strsim 0.11.1",
  "syn 2.0.118",
 ]
 
@@ -2309,7 +2311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0bc153c91ebf221a2e7feb166aee259acf8a00ecf35c83df8b79fe8f5c3861d7"
 dependencies = [
  "derive-deftly-macros",
- "heck 0.5.0",
+ "heck 0.4.1",
 ]
 
 [[package]]
@@ -2318,7 +2320,7 @@ version = "1.11.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1747ed5fb4ab3a9f8253da3401efe7ca8f8e5ec373b2e700ff55c3304d84e31f"
 dependencies = [
- "heck 0.5.0",
+ "heck 0.4.1",
  "indexmap 2.14.0",
  "itertools 0.15.0",
  "proc-macro-crate",
@@ -2466,7 +2468,7 @@ dependencies = [
  "libc",
  "option-ext",
  "redox_users 0.5.2",
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
@@ -2828,7 +2830,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
 dependencies = [
  "libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -4263,7 +4265,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
 dependencies = [
  "hermit-abi 0.5.2",
  "libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -5050,7 +5052,7 @@ version = "0.50.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
 dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
@@ -6061,7 +6063,7 @@ dependencies = [
  "derive-deftly",
  "libc",
  "paste",
- "thiserror 2.0.18",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -6147,7 +6149,7 @@ dependencies = [
  "libc",
  "socket2",
  "tracing",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -6689,7 +6691,7 @@ dependencies = [
  "errno",
  "libc",
  "linux-raw-sys 0.4.15",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -6702,7 +6704,7 @@ dependencies = [
  "errno",
  "libc",
  "linux-raw-sys 0.12.1",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -7334,7 +7336,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
 dependencies = [
  "libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
@@ -7880,7 +7882,7 @@ dependencies = [
  "getrandom 0.4.3",
  "once_cell",
  "rustix 1.1.4",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -9380,9 +9382,9 @@ dependencies = [
 
 [[package]]
 name = "turso"
-version = "0.7.0"
+version = "0.7.0-pre.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cac8d131650c1bf6a2721202208b3dfba218be25c975f48bebda766173fbdc3b"
+checksum = "299b9f6a6e33687ed23d0ec532a67f11e2ac349695d94927f6ea76fddea9ea25"
 dependencies = [
  "mimalloc",
  "thiserror 2.0.18",
@@ -10054,7 +10056,7 @@ version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
 dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]

+ 1 - 0
bin/app/Cargo.toml

@@ -41,6 +41,7 @@ async-recursion = "1.1.1"
 colored = "3.1.1"
 #rustpython-vm = "0.3.1"
 kvdb-overlay = {git = "https://git.dark.fi/darkrenaissance/kvdb-overlay", version = "0.1.0"}
+turso = "0.7.0-pre.14"
 url = "2.5.8"
 semver = "1.0.28"
 chrono = "0.4.44"

+ 1 - 0
bin/app/Makefile

@@ -17,6 +17,7 @@ ADB_DEVICE_INST =
 ADB_DEVICE_LOG  =
 
 SRC = \
+	app.sql \
 	build.rs \
 	Cargo.lock \
 	Cargo.toml \

+ 24 - 0
bin/app/app.sql

@@ -0,0 +1,24 @@
+CREATE TABLE IF NOT EXISTS channels (
+    name   TEXT PRIMARY KEY,
+    secret BLOB
+);
+CREATE TABLE IF NOT EXISTS contacts (
+    name   TEXT PRIMARY KEY,
+    public BLOB NOT NULL
+);
+CREATE TABLE IF NOT EXISTS profiles (
+    id        INTEGER PRIMARY KEY CHECK (id = 1),
+    nick      TEXT NOT NULL,
+    dm_secret BLOB NOT NULL
+);
+CREATE TABLE IF NOT EXISTS settings (
+    name  TEXT NOT NULL,
+    idx   INTEGER NOT NULL,
+    type  TEXT NOT NULL,
+    value BLOB NOT NULL,
+    PRIMARY KEY (name, idx)
+);
+CREATE TABLE IF NOT EXISTS flags (
+    name  TEXT PRIMARY KEY,
+    value BLOB NOT NULL
+);

+ 9 - 13
bin/app/src/app/mod.rs

@@ -16,7 +16,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use kvdb_overlay::Database;
+use kvdb_overlay::Database as KvDb;
 use smol::Task;
 use std::sync::{
     atomic::{AtomicBool, Ordering},
@@ -26,7 +26,7 @@ use std::sync::{
 #[cfg(target_os = "android")]
 use crate::android;
 use crate::{
-    error::Error,
+    db::AppDbPtr,
     gfx::{EpochIndex, GraphicsEventPublisherPtr, Renderer},
     prop::{PropertyAtomicGuard, Role},
     scene::{Pimpl, SceneNodePtr},
@@ -49,7 +49,7 @@ macro_rules! i { ($($arg:tt)*) => { info!(target: "app", $($arg)*); } }
 //macro_rules! w { ($($arg:tt)*) => { warn!(target: "app", $($arg)*); } }
 macro_rules! e { ($($arg:tt)*) => { error!(target: "app", $($arg)*); } }
 
-const IS_FIRST_TIME_KEY: &[u8] = b"is_first_time";
+const IS_FIRST_TIME_KEY: &str = "is_first_time";
 
 //fn print_type_of<T>(_: &T) {
 //    println!("{}", std::any::type_name::<T>())
@@ -89,21 +89,19 @@ impl App {
 
     /// Does not require miniquad to be init. Created the scene graph tree / schema and all
     /// the objects.
-    pub async fn setup(&self, db: Database) -> Result<Option<i32>, Error> {
+    pub async fn setup(&self, kv_db: KvDb, app_db: AppDbPtr) {
         t!("App::setup()");
 
-        let settings_tree = db.open_tree_default("settings").unwrap();
-        let flags_tree = db.open_tree_default("app_flags").unwrap();
-        let is_first_time = !flags_tree.contains_key(IS_FIRST_TIME_KEY).unwrap();
+        let is_first_time = !app_db.flag_contains(IS_FIRST_TIME_KEY).await.unwrap();
         if is_first_time {
-            flags_tree.insert(IS_FIRST_TIME_KEY, b"").unwrap();
+            app_db.flag_set(IS_FIRST_TIME_KEY).await.unwrap();
         }
         self.is_first_time.store(is_first_time, Ordering::Relaxed);
 
         let ex = self.ex.clone();
+        let app_db2 = app_db.clone();
         let setting = create_setting("setting");
-        let setting =
-            setting.setup(|me| async move { Setting::new(me, settings_tree, ex).await }).await;
+        let setting = setting.setup(|me| async move { Setting::new(me, app_db2, ex).await }).await;
         self.sg_root.link(setting);
 
         let i18n_fish = self.setup_locale();
@@ -147,7 +145,7 @@ impl App {
         self.sg_root.link(window.clone());
 
         #[cfg(feature = "schema-app")]
-        schema::make(&self, window.clone(), &i18n_fish, db).await;
+        schema::make(&self, window.clone(), &i18n_fish, kv_db, app_db).await;
 
         #[cfg(feature = "schema-test")]
         schema::test::make(&self, window.clone(), &i18n_fish).await;
@@ -162,8 +160,6 @@ impl App {
         compile_error!("Only one schema can be selected");
 
         d!("Schema loaded");
-
-        Ok(None)
     }
 
     fn setup_locale(&self) -> I18nBabelFish {

+ 3 - 3
bin/app/src/app/schema/chat.rs

@@ -19,7 +19,7 @@
 use darkfi_serial::{Decodable, Encodable};
 #[cfg(feature = "enable-plugin-darkirc")]
 use irc2::Privmsg;
-use kvdb_overlay::{Database, Tree};
+use kvdb_overlay::{Database as KvDb, Tree};
 use std::{sync::Arc, time::UNIX_EPOCH};
 
 #[cfg(feature = "enable-plugin-darkirc")]
@@ -198,7 +198,7 @@ pub async fn make(
     ex: &ExecutorPtr,
     content: SceneNodePtr,
     channel: &str,
-    db: &Database,
+    kv_db: &KvDb,
     i18n_fish: &I18nBabelFish,
     emoji_meshes: emoji_picker::EmojiMeshesPtr,
     redraw: RedrawTrigger,
@@ -624,7 +624,7 @@ pub async fn make(
     }
 
     let tree_name = channel.to_string() + "__chat_tree";
-    let chat_tree = db.open_tree_default(&tree_name).unwrap();
+    let chat_tree = kv_db.open_tree_default(&tree_name).unwrap();
     //if chat_tree.is_empty() {
     //    populate_tree(&chat_tree);
     //}

+ 9 - 16
bin/app/src/app/schema/menu/channel.rs

@@ -18,7 +18,7 @@
 
 use bs58;
 use darkfi_serial::{async_trait, deserialize, Encodable, SerialDecodable, SerialEncodable};
-use kvdb_overlay::{Database, Tree};
+use kvdb_overlay::Database as KvDb;
 use rand::{rngs::OsRng, Rng};
 use ui_consts::*;
 
@@ -47,6 +47,7 @@ use crate::{
         },
         App,
     },
+    db::AppDbPtr,
     expr,
     gfx::gfxtag,
     mesh::{COLOR_CYAN, COLOR_INACTIVE, COLOR_MINT, COLOR_MINT_OP, MINT_BTN_GRADIENT},
@@ -168,8 +169,8 @@ pub async fn make(
     window_scale: PropertyFloat32,
     contact_is_visible: PropertyBool,
     channel_is_visible: PropertyBool,
-    channels_tree: Tree,
-    db: &Database,
+    app_db: AppDbPtr,
+    kv_db: &KvDb,
     emoji_meshes: EmojiMeshesPtr,
 ) -> SceneNodePtr {
     let mut cc = expr::Compiler::new();
@@ -1519,9 +1520,7 @@ pub async fn make(
 
     let prop = node.get_property("items").unwrap();
     let mut channel_names: Vec<String> = vec![];
-    for item in channels_tree.iter() {
-        let (_key, val) = item.unwrap();
-        let channel = deserialize::<Channel>(&val).unwrap();
+    for channel in app_db.channels().await.unwrap() {
         channel_names.push(format!("#{}", channel.name));
     }
     channel_names.sort();
@@ -1543,7 +1542,7 @@ pub async fn make(
     content_area.link(menu_node.clone());
 
     // Setup add_channel button handler now that menu_node exists
-    let channels_tree2 = channels_tree.clone();
+    let app_db2 = app_db.clone();
     let nickedit2 = nickedit_node.clone();
     let secedit2 = secedit_node.clone();
     let menu_prop2 = menu_node.get_property("items").unwrap();
@@ -1586,12 +1585,7 @@ pub async fn make(
                 channel.secret = Some(arr);
             }
 
-            let mut val = vec![];
-            channel.encode(&mut val).unwrap();
-
-            let key = name;
-
-            channels_tree2.insert(key.as_bytes(), &val).unwrap();
+            app_db2.channel_insert(&channel).await.unwrap();
 
             let atom = &mut redraw2.make_guard(gfxtag!("add_channel"));
             menu_prop2.push_str(atom, Role::App, &channel_name).unwrap();
@@ -1623,11 +1617,10 @@ pub async fn make(
     let sg_root = app.sg_root.clone();
     let renderer = app.renderer.clone();
     let ex = app.ex.clone();
-    let channels_tree2 = channels_tree.clone();
     let content2 = content.clone();
     let channel_vis = channel_is_visible.clone();
     let window_scale2 = window_scale.clone();
-    let db2 = db.clone();
+    let kv_db2 = kv_db.clone();
     let i18n_fish2 = i18n_fish.clone();
     let emoji_meshes2 = emoji_meshes.clone();
     let redraw2 = app.redraw_trigger.clone();
@@ -1655,7 +1648,7 @@ pub async fn make(
                 &ex,
                 content,
                 &channel,
-                &db2,
+                &kv_db2,
                 &i18n_fish2,
                 emoji_meshes2.clone(),
                 redraw2.clone(),

+ 11 - 14
bin/app/src/app/schema/menu/contact.rs

@@ -18,7 +18,7 @@
 
 use bs58;
 use darkfi_serial::{async_trait, deserialize, Encodable, SerialDecodable, SerialEncodable};
-use kvdb_overlay::{Database, Tree};
+use kvdb_overlay::Database as KvDb;
 use ui_consts::*;
 
 macro_rules! d { ($($arg:tt)*) => { debug!(target: "app::contact", $($arg)*); } }
@@ -47,6 +47,7 @@ use crate::{
         },
         App,
     },
+    db::AppDbPtr,
     expr,
     gfx::gfxtag,
     mesh::{COLOR_CYAN, COLOR_INACTIVE, COLOR_MINT, COLOR_MINT_OP, MINT_BTN_GRADIENT},
@@ -57,7 +58,7 @@ use crate::{
         emoji_picker::EmojiMeshesPtr, BaseEdit, BaseEditType, Button, Layer, Menu, ShapeVertex,
         Shortcut, Text, UIObject, VectorArt, VectorShape,
     },
-    util::i18n::I18nBabelFish,
+    util::{clipboard, i18n::I18nBabelFish},
 };
 
 #[cfg(any(target_os = "android", feature = "emulate-android"))]
@@ -168,8 +169,8 @@ pub async fn make(
     window_scale: PropertyFloat32,
     contact_is_visible: PropertyBool,
     channel_is_visible: PropertyBool,
-    contacts_tree: Tree,
-    db: &Database,
+    app_db: AppDbPtr,
+    kv_db: &KvDb,
     emoji_meshes: EmojiMeshesPtr,
 ) -> SceneNodePtr {
     let mut cc = expr::Compiler::new();
@@ -1267,7 +1268,7 @@ pub async fn make(
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             debug!(target: "app::menu", "secret paste button clicked");
-            match crate::clipboard::get() {
+            match clipboard::get() {
                 Some(clipboard_text) => {
                     let text_prop = secedit_node2.get_property("text").unwrap();
                     let atom = &mut redraw_clone.make_guard(gfxtag!("secret paste"));
@@ -1437,9 +1438,7 @@ pub async fn make(
 
     let prop = node.get_property("items").unwrap();
     let mut contact_names: Vec<String> = vec![];
-    for item in contacts_tree.iter() {
-        let (_key, val) = item.unwrap();
-        let contact = deserialize::<Contact>(&val).unwrap();
+    for contact in app_db.contacts().await.unwrap() {
         contact_names.push(format!("@{}", contact.name));
     }
     contact_names.sort();
@@ -1464,7 +1463,7 @@ pub async fn make(
     btns.connect_edit_handlers(app, &menu_node, None);
 
     // "add contact" button handler: persist the contact and notify the plugin
-    let contacts_tree2 = contacts_tree.clone();
+    let app_db2 = app_db.clone();
     let nickedit2 = nickedit_node.clone();
     let secedit2 = secedit_node.clone();
     let menu_prop2 = menu_node.get_property("items").unwrap();
@@ -1500,9 +1499,7 @@ pub async fn make(
             public.copy_from_slice(&public_bytes);
 
             let contact = Contact { name: name.clone(), public };
-            let mut val = vec![];
-            contact.encode(&mut val).unwrap();
-            contacts_tree2.insert(name.as_bytes(), &val).unwrap();
+            app_db2.contact_insert(&contact).await.unwrap();
 
             let contact_name = format!("@{}", name);
             let atom = &mut redraw2.make_guard(gfxtag!("add_contact"));
@@ -1523,7 +1520,7 @@ pub async fn make(
     let sg_root = app.sg_root.clone();
     let renderer = app.renderer.clone();
     let ex = app.ex.clone();
-    let db2 = db.clone();
+    let kv_db2 = kv_db.clone();
     let i18n_fish2 = i18n_fish.clone();
     let emoji_meshes2 = emoji_meshes.clone();
     let redraw2 = app.redraw_trigger.clone();
@@ -1549,7 +1546,7 @@ pub async fn make(
                 &ex,
                 content,
                 &contact,
-                &db2,
+                &kv_db2,
                 &i18n_fish2,
                 emoji_meshes2.clone(),
                 redraw2.clone(),

+ 8 - 8
bin/app/src/app/schema/menu/mod.rs

@@ -17,7 +17,7 @@
  */
 
 use darkfi_serial::deserialize;
-use kvdb_overlay::{Database, Tree};
+use kvdb_overlay::Database as KvDb;
 use std::io::Write;
 use ui_consts::*;
 
@@ -27,6 +27,7 @@ use crate::{
         node::{create_button, create_layer, create_menu, create_text, create_vector_art},
         App,
     },
+    db::AppDbPtr,
     expr,
     gfx::gfxtag,
     mesh::COLOR_CYAN,
@@ -113,9 +114,8 @@ pub async fn make(
     app: &App,
     chat_layer: SceneNodePtr,
     i18n_fish: &I18nBabelFish,
-    channels_tree: Tree,
-    contacts_tree: Tree,
-    db: &Database,
+    app_db: AppDbPtr,
+    kv_db: &KvDb,
     emoji_meshes: EmojiMeshesPtr,
 ) {
     let window_scale = PropertyFloat32::wrap(
@@ -197,8 +197,8 @@ pub async fn make(
         window_scale.clone(),
         contact_is_visible.clone(),
         channel_is_visible.clone(),
-        contacts_tree.clone(),
-        db,
+        app_db.clone(),
+        kv_db,
         emoji_meshes.clone(),
     )
     .await;
@@ -211,8 +211,8 @@ pub async fn make(
         window_scale.clone(),
         contact_is_visible.clone(),
         channel_is_visible.clone(),
-        channels_tree.clone(),
-        db,
+        app_db.clone(),
+        kv_db,
         emoji_meshes.clone(),
     )
     .await;

+ 23 - 29
bin/app/src/app/schema/mod.rs

@@ -19,9 +19,9 @@
 use std::{fs::File, io::Write};
 
 use darkfi::system::msleep;
-use darkfi_serial::{deserialize, Encodable};
+
 use indoc::indoc;
-use kvdb_overlay::Database;
+use kvdb_overlay::Database as KvDb;
 
 use crate::{
     app::{
@@ -31,13 +31,14 @@ use crate::{
         },
         App,
     },
+    db::AppDbPtr,
     expr::{self, Compiler},
     gfx::gfxtag,
     prop::{PropertyAtomicGuard, PropertyFloat32, Role},
     scene::{SceneNodePtr, Slot},
     sfx, shape,
     ui::{emoji_picker, Button, Layer, Text, TextScramble, VectorArt, VectorShape, Video},
-    util::i18n::I18nBabelFish,
+    util::{clipboard, i18n::I18nBabelFish},
 };
 
 mod chat;
@@ -209,7 +210,13 @@ pub fn write_joined_channels(items: &[String]) {
     let _ = std::fs::write(get_joined_channels_filename(), items.join("\n"));
 }
 
-pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db: Database) {
+pub async fn make(
+    app: &App,
+    window: SceneNodePtr,
+    i18n_fish: &I18nBabelFish,
+    kv_db: KvDb,
+    app_db: AppDbPtr,
+) {
     let mut cc = Compiler::new();
     cc.add_const_f32("NETSTATUS_ICON_SIZE", NETSTATUS_ICON_SIZE);
     cc.add_const_f32("SETTINGS_ICON_SIZE", SETTINGS_ICON_SIZE);
@@ -402,16 +409,11 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
         }
     });
 
-    let channels_tree = db.open_tree_default("channels").expect("cannot open channels tree");
-    let contacts_tree = db.open_tree_default("contacts").expect("cannot open contacts tree");
-
-    // Initialize default channels if tree is empty
-    if channels_tree.is_empty().unwrap() {
+    // Initialize default channels if the table is empty
+    if app_db.channels().await.expect("cannot read channels").is_empty() {
         for channel_name in DEFAULT_CHANNELS {
             let channel = Channel { name: channel_name.to_string(), secret: None };
-            let mut val = vec![];
-            channel.encode(&mut val).unwrap();
-            channels_tree.insert(channel_name.to_string().as_bytes(), &val).unwrap();
+            app_db.channel_insert(&channel).await.expect("cannot seed channel");
         }
     }
 
@@ -714,7 +716,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
             #[cfg(target_os = "android")]
             {
                 let info = crate::android::get_display_debug_info();
-                crate::clipboard::set(&info);
+                clipboard::set(&info);
                 i!("Copied report!");
             }
             */
@@ -726,27 +728,19 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
         node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     overlay_node.link(node);
 
-    menu::make(
-        app,
-        chat_layer.clone(),
-        i18n_fish,
-        channels_tree.clone(),
-        contacts_tree.clone(),
-        &db,
-        emoji_meshes.clone(),
-    )
-    .await;
+    menu::make(app, chat_layer.clone(), i18n_fish, app_db.clone(), &kv_db, emoji_meshes.clone())
+        .await;
 
     // Create chat layers only for joined channels/contacts, in joined order.
     for name in read_joined_channels() {
         let bare = if name.starts_with('#') || name.starts_with('@') { &name[1..] } else { &name };
-        let in_tree = match name.chars().next() {
-            Some('#') => channels_tree.contains_key(bare.to_string().as_bytes()).unwrap_or(false),
-            Some('@') => contacts_tree.contains_key(bare.to_string().as_bytes()).unwrap_or(false),
+        let in_db = match name.chars().next() {
+            Some('#') => app_db.channel_get(bare).await.ok().flatten().is_some(),
+            Some('@') => app_db.contact_get(bare).await.ok().flatten().is_some(),
             _ => false,
         };
-        if !in_tree {
-            warn!(target: "app::schema", "Joined entry '{name}' not found in tree; skipping");
+        if !in_db {
+            warn!(target: "app::schema", "Joined entry '{name}' not found in kv_db; skipping");
             continue
         }
 
@@ -756,7 +750,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
             &app.ex,
             chat_layer.clone(),
             &name,
-            &db,
+            &kv_db,
             i18n_fish,
             emoji_meshes.clone(),
             app.redraw_trigger.clone(),

+ 3 - 2
bin/app/src/app/schema/test.rs

@@ -31,6 +31,7 @@ use crate::{
     ui::{ChatView, Layer, Text, VectorArt, VectorShape, Video},
     util::i18n::I18nBabelFish,
 };
+use kvdb_overlay::Database as KvDb;
 
 const LIGHTMODE: bool = false;
 
@@ -370,8 +371,8 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         prop.set_f32(atom, Role::App, 3, 1.).unwrap();
     }
 
-    let db = kvdb_overlay::Database::open_default(&get_chatdb_path()).expect("cannot open kvdb");
-    let chat_tree = db.open_tree(b"chat").unwrap();
+    let kv_db = KvDb::open_default(&get_chatdb_path()).expect("cannot open kvdb");
+    let chat_tree = kv_db.open_tree(b"chat").unwrap();
     if chat_tree.is_empty() {
         populate_tree(&chat_tree);
     }

+ 2 - 2
bin/app/src/app/schema/wallet/receive.rs

@@ -22,14 +22,14 @@ use crate::{
         schema::COLOR_SCHEME,
         App,
     },
-    clipboard, expr,
+    expr,
     gfx::gfxtag,
     mesh::COLOR_CYAN,
     prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, PropertyStr, Role},
     scene::{SceneNodePtr, Slot},
     shape,
     ui::{Button, Layer, Text, VectorArt},
-    util::i18n::I18nBabelFish,
+    util::{clipboard, i18n::I18nBabelFish},
 };
 
 use super::{super::ColorScheme, data::*, util::*};

+ 2 - 2
bin/app/src/app/schema/wallet/send_step2.rs

@@ -27,14 +27,14 @@ use crate::{
         schema::COLOR_SCHEME,
         App,
     },
-    clipboard, expr,
+    expr,
     gfx::gfxtag,
     mesh::COLOR_CYAN,
     prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, Role},
     scene::{Pimpl, SceneNodePtr, Slot},
     shape,
     ui::{BaseEdit, BaseEditType, Button, Layer, Text, VectorArt, VectorShape},
-    util::i18n::I18nBabelFish,
+    util::{clipboard, i18n::I18nBabelFish},
 };
 
 use super::{super::ColorScheme, data::*, util::*};

+ 355 - 0
bin/app/src/db.rs

@@ -0,0 +1,355 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2026 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::{path::PathBuf, sync::Arc};
+
+use smol::lock::Mutex as AsyncMutex;
+use turso::{Connection, Value};
+
+use crate::{
+    app::schema::menu::{channel::Channel, contact::Contact},
+    error::{Error, Result},
+};
+
+pub type AppDbPtr = Arc<AppDb>;
+
+/// Single turso SQL database owning all app persistent state: channels,
+/// contacts, darkirc identity, settings, and app flags. The kvdb-overlay
+/// database remains exclusively for the event graph and chat history trees.
+pub struct AppDb {
+    conn: AsyncMutex<Connection>,
+}
+
+impl AppDb {
+    pub async fn new(path: &str) -> Result<AppDbPtr> {
+        let db = turso::Builder::new_local(path).build().await.map_err(Error::from)?;
+        let conn = db.connect().map_err(Error::from)?;
+        let self_ = Arc::new(Self { conn: AsyncMutex::new(conn) });
+        self_.init_schema().await?;
+        Ok(self_)
+    }
+
+    async fn init_schema(&self) -> Result<()> {
+        let conn = self.conn.lock().await;
+        conn.execute_batch(include_str!("../app.sql")).await?;
+        // First run: generate the DM identity secret right away. On
+        // subsequent runs the row exists and the insert is ignored.
+        let secret: [u8; 32] = rand::random();
+        conn.execute(
+            "INSERT OR IGNORE INTO profiles (id, nick, dm_secret) VALUES (1, 'anon', ?1)",
+            vec![Value::Blob(secret.to_vec())],
+        )
+        .await?;
+        Ok(())
+    }
+
+    pub async fn channels(&self) -> Result<Vec<Channel>> {
+        let conn = self.conn.lock().await;
+        let mut stmt = conn.prepare("SELECT name, secret FROM channels ORDER BY name").await?;
+        let mut rows = stmt.query(()).await?;
+        let mut out = vec![];
+        while let Some(row) = rows.next().await? {
+            let name = row.get_value(0)?.as_text().ok_or(Error::TursoErr)?.to_string();
+            let secret = match row.get_value(1)? {
+                Value::Blob(b) if b.len() == 32 => Some(b.as_slice().try_into().unwrap()),
+                _ => None,
+            };
+            out.push(Channel { name, secret });
+        }
+        Ok(out)
+    }
+
+    pub async fn channel_get(&self, name: &str) -> Result<Option<Channel>> {
+        let conn = self.conn.lock().await;
+        let mut stmt = conn.prepare("SELECT name, secret FROM channels WHERE name = ?1").await?;
+        let mut rows = stmt.query(vec![Value::Text(name.to_string())]).await?;
+        let Some(row) = rows.next().await? else { return Ok(None) };
+        let name = row.get_value(0)?.as_text().ok_or(Error::TursoErr)?.to_string();
+        let secret = match row.get_value(1)? {
+            Value::Blob(b) if b.len() == 32 => Some(b.as_slice().try_into().unwrap()),
+            _ => None,
+        };
+        Ok(Some(Channel { name, secret }))
+    }
+
+    pub async fn channel_insert(&self, channel: &Channel) -> Result<()> {
+        let conn = self.conn.lock().await;
+        let secret = channel.secret.map(|s| Value::Blob(s.to_vec())).unwrap_or(Value::Null);
+        conn.execute(
+            "INSERT OR REPLACE INTO channels (name, secret) VALUES (?1, ?2)",
+            vec![Value::Text(channel.name.clone()), secret],
+        )
+        .await?;
+        Ok(())
+    }
+
+    pub async fn contacts(&self) -> Result<Vec<Contact>> {
+        let conn = self.conn.lock().await;
+        let mut stmt = conn.prepare("SELECT name, public FROM contacts ORDER BY name").await?;
+        let mut rows = stmt.query(()).await?;
+        let mut out = vec![];
+        while let Some(row) = rows.next().await? {
+            let name = row.get_value(0)?.as_text().ok_or(Error::TursoErr)?.to_string();
+            let public =
+                row.get_value(1)?.as_blob().ok_or(Error::TursoErr)?.as_slice().try_into().unwrap();
+            out.push(Contact { name, public });
+        }
+        Ok(out)
+    }
+
+    pub async fn contact_get(&self, name: &str) -> Result<Option<Contact>> {
+        let conn = self.conn.lock().await;
+        let mut stmt = conn.prepare("SELECT name, public FROM contacts WHERE name = ?1").await?;
+        let mut rows = stmt.query(vec![Value::Text(name.to_string())]).await?;
+        let Some(row) = rows.next().await? else { return Ok(None) };
+        let name = row.get_value(0)?.as_text().ok_or(Error::TursoErr)?.to_string();
+        let public =
+            row.get_value(1)?.as_blob().ok_or(Error::TursoErr)?.as_slice().try_into().unwrap();
+        Ok(Some(Contact { name, public }))
+    }
+
+    pub async fn contact_insert(&self, contact: &Contact) -> Result<()> {
+        let conn = self.conn.lock().await;
+        conn.execute(
+            "INSERT OR REPLACE INTO contacts (name, public) VALUES (?1, ?2)",
+            vec![Value::Text(contact.name.clone()), Value::Blob(contact.public.to_vec())],
+        )
+        .await?;
+        Ok(())
+    }
+
+    pub async fn nick_get(&self) -> Result<Option<String>> {
+        let conn = self.conn.lock().await;
+        let mut stmt = conn.prepare("SELECT nick FROM profiles WHERE id = 1").await?;
+        let mut rows = stmt.query(()).await?;
+        match rows.next().await? {
+            Some(row) => Ok(Some(row.get_value(0)?.as_text().ok_or(Error::TursoErr)?.to_string())),
+            None => Ok(None),
+        }
+    }
+
+    pub async fn nick_set(&self, nick: &str) -> Result<()> {
+        let conn = self.conn.lock().await;
+        conn.execute(
+            "UPDATE profiles SET nick = ?1 WHERE id = 1",
+            vec![Value::Text(nick.to_string())],
+        )
+        .await?;
+        Ok(())
+    }
+
+    /// The identity row is created with a fresh random secret during
+    /// schema init, so this only reads.
+    pub async fn dm_secret(&self) -> Result<[u8; 32]> {
+        let conn = self.conn.lock().await;
+        let mut stmt = conn.prepare("SELECT dm_secret FROM profiles WHERE id = 1").await?;
+        let mut rows = stmt.query(()).await?;
+        let Some(row) = rows.next().await? else { return Err(Error::TursoErr) };
+        let Value::Blob(b) = row.get_value(0)? else { return Err(Error::TursoErr) };
+        if b.len() != 32 {
+            return Err(Error::TursoErr)
+        }
+        Ok(b.as_slice().try_into().unwrap())
+    }
+
+    /// Load all settings rows as (name, idx, value bytes).
+    pub async fn settings_all(&self) -> Result<Vec<(String, u32, Vec<u8>)>> {
+        let conn = self.conn.lock().await;
+        let mut stmt =
+            conn.prepare("SELECT name, idx, value FROM settings ORDER BY name, idx").await?;
+        let mut rows = stmt.query(()).await?;
+        let mut out = vec![];
+        while let Some(row) = rows.next().await? {
+            let name = row.get_value(0)?.as_text().ok_or(Error::TursoErr)?.to_string();
+            let idx = *row.get_value(1)?.as_integer().ok_or(Error::TursoErr)?;
+            let value = row.get_value(2)?.as_blob().ok_or(Error::TursoErr)?.to_vec();
+            out.push((name, idx as u32, value));
+        }
+        Ok(out)
+    }
+
+    pub async fn setting_get(&self, name: &str, idx: u32) -> Result<Option<Vec<u8>>> {
+        let conn = self.conn.lock().await;
+        let mut stmt =
+            conn.prepare("SELECT value FROM settings WHERE name = ?1 AND idx = ?2").await?;
+        let mut rows =
+            stmt.query(vec![Value::Text(name.to_string()), Value::Integer(idx as i64)]).await?;
+        match rows.next().await? {
+            Some(row) => Ok(Some(row.get_value(0)?.as_blob().ok_or(Error::TursoErr)?.to_vec())),
+            None => Ok(None),
+        }
+    }
+
+    pub async fn setting_put(&self, name: &str, idx: u32, typ: &str, value: &[u8]) -> Result<()> {
+        let conn = self.conn.lock().await;
+        conn.execute(
+            "INSERT OR REPLACE INTO settings (name, idx, type, value) VALUES (?1, ?2, ?3, ?4)",
+            vec![
+                Value::Text(name.to_string()),
+                Value::Integer(idx as i64),
+                Value::Text(typ.to_string()),
+                Value::Blob(value.to_vec()),
+            ],
+        )
+        .await?;
+        Ok(())
+    }
+
+    pub async fn setting_remove_idx(&self, name: &str, idx: u32) -> Result<()> {
+        let conn = self.conn.lock().await;
+        conn.execute(
+            "DELETE FROM settings WHERE name = ?1 AND idx = ?2",
+            vec![Value::Text(name.to_string()), Value::Integer(idx as i64)],
+        )
+        .await?;
+        Ok(())
+    }
+
+    pub async fn flag_contains(&self, name: &str) -> Result<bool> {
+        let conn = self.conn.lock().await;
+        let mut stmt = conn.prepare("SELECT 1 FROM flags WHERE name = ?1").await?;
+        let mut rows = stmt.query(vec![Value::Text(name.to_string())]).await?;
+        Ok(rows.next().await?.is_some())
+    }
+
+    pub async fn flag_set(&self, name: &str) -> Result<()> {
+        let conn = self.conn.lock().await;
+        conn.execute(
+            "INSERT OR REPLACE INTO flags (name, value) VALUES (?1, ?2)",
+            vec![Value::Text(name.to_string()), Value::Blob(vec![])],
+        )
+        .await?;
+        Ok(())
+    }
+}
+
+#[cfg(target_os = "android")]
+pub fn get_app_db_path() -> PathBuf {
+    crate::android::get_appdata_path().join("app.db")
+}
+
+#[cfg(not(target_os = "android"))]
+pub fn get_app_db_path() -> PathBuf {
+    dirs::data_local_dir().unwrap().join("darkfi/app/app.db")
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crypto_box::SecretKey;
+
+    fn temp_db_path(tag: &str) -> String {
+        let path = std::env::temp_dir()
+            .join(format!("darkfi-app-db-test-{tag}-{}.db", std::process::id()));
+        let _ = std::fs::remove_file(&path);
+        path.to_str().unwrap().to_string()
+    }
+
+    /// Covers the app-storage spec: fresh start creates schema + seeds,
+    /// channels/contacts/settings/flags roundtrip, identity (nick + DM
+    /// secret) survives a reopen.
+    #[test]
+    fn app_db_persistence() {
+        let path = temp_db_path("persist");
+
+        let seed_secret: [u8; 32] = core::array::from_fn(|i| i as u8);
+        let dm_public = {
+            let db = smol::block_on(AppDb::new(&path)).unwrap();
+
+            assert!(smol::block_on(db.channels()).unwrap().is_empty());
+            smol::block_on(db.channel_insert(&Channel { name: "dev".into(), secret: None }))
+                .unwrap();
+            smol::block_on(db.channel_insert(&Channel {
+                name: "secret_chan".into(),
+                secret: Some(seed_secret),
+            }))
+            .unwrap();
+
+            smol::block_on(
+                db.contact_insert(&Contact { name: "alice".into(), public: seed_secret }),
+            )
+            .unwrap();
+
+            smol::block_on(db.nick_set("testnick")).unwrap();
+
+            smol::block_on(db.setting_put("net.localnet", 0, "bool", &[1])).unwrap();
+            smol::block_on(db.setting_put("net.localnet", 2, "bool", &[0])).unwrap();
+
+            assert!(!smol::block_on(db.flag_contains("is_first_time")).unwrap());
+            smol::block_on(db.flag_set("is_first_time")).unwrap();
+
+            let secret = smol::block_on(db.dm_secret()).unwrap();
+            SecretKey::from_bytes(secret).public_key().to_bytes()
+        };
+
+        {
+            let db = smol::block_on(AppDb::new(&path)).unwrap();
+
+            let channels = smol::block_on(db.channels()).unwrap();
+            assert_eq!(channels.len(), 2);
+            assert_eq!(channels[0].name, "dev");
+            assert_eq!(channels[0].secret, None);
+            assert_eq!(channels[1].name, "secret_chan");
+            assert_eq!(channels[1].secret, Some(seed_secret));
+
+            let chan = smol::block_on(db.channel_get("dev")).unwrap().unwrap();
+            assert_eq!(chan.name, "dev");
+
+            let contacts = smol::block_on(db.contacts()).unwrap();
+            assert_eq!(contacts.len(), 1);
+            assert_eq!(contacts[0].name, "alice");
+            assert_eq!(contacts[0].public, seed_secret);
+
+            assert_eq!(smol::block_on(db.nick_get()).unwrap(), Some("testnick".into()));
+
+            assert_eq!(smol::block_on(db.setting_get("net.localnet", 0)).unwrap(), Some(vec![1u8]));
+            assert_eq!(smol::block_on(db.setting_get("net.localnet", 2)).unwrap(), Some(vec![0u8]));
+            assert_eq!(smol::block_on(db.setting_get("net.localnet", 1)).unwrap(), None);
+
+            smol::block_on(db.setting_remove_idx("net.localnet", 2)).unwrap();
+            assert_eq!(smol::block_on(db.setting_get("net.localnet", 2)).unwrap(), None);
+            assert_eq!(smol::block_on(db.settings_all()).unwrap().len(), 1);
+
+            assert!(smol::block_on(db.flag_contains("is_first_time")).unwrap());
+
+            // DM identity must be stable across reopen
+            let secret = smol::block_on(db.dm_secret()).unwrap();
+            assert_eq!(SecretKey::from_bytes(secret).public_key().to_bytes(), dm_public);
+        }
+
+        let _ = std::fs::remove_file(&path);
+    }
+
+    /// Covers: no-nick start keeps the default row, first-run generates and
+    /// persists a fresh DM identity (nonzero).
+    #[test]
+    fn app_db_fresh_identity() {
+        let path = temp_db_path("fresh");
+        let db = smol::block_on(AppDb::new(&path)).unwrap();
+
+        assert_eq!(smol::block_on(db.nick_get()).unwrap(), Some("anon".into()));
+
+        let secret = smol::block_on(db.dm_secret()).unwrap();
+        assert!(secret.iter().any(|&x| x != 0));
+
+        let again = smol::block_on(db.dm_secret()).unwrap();
+        assert_eq!(secret, again);
+
+        let _ = std::fs::remove_file(&path);
+    }
+}

+ 9 - 0
bin/app/src/error.rs

@@ -143,6 +143,9 @@ pub enum Error {
 
     #[error("Serialization error")]
     SerialErr = 48,
+
+    #[error("SQL database error")]
+    TursoErr = 49,
 }
 
 impl From<kvdb_overlay::Error> for Error {
@@ -156,3 +159,9 @@ impl From<std::io::Error> for Error {
         Error::SerialErr
     }
 }
+
+impl From<turso::Error> for Error {
+    fn from(_: turso::Error) -> Error {
+        Error::TursoErr
+    }
+}

+ 18 - 10
bin/app/src/main.rs

@@ -34,7 +34,7 @@ extern crate tracing;
 mod android;
 mod app;
 mod build_info;
-mod clipboard;
+mod db;
 mod error;
 mod expr;
 mod gfx;
@@ -57,19 +57,22 @@ mod util;
 
 use crate::{
     app::{App, AppPtr},
+    db::{AppDb, AppDbPtr},
     gfx::EpochIndex,
     prop::{Property, PropertySubType, PropertyType},
     scene::{CallArgType, SceneNode, SceneNodePtr, SceneNodeType},
+    ui::RedrawTrigger,
     util::AsyncRuntime,
 };
 #[cfg(feature = "enable-netdebug")]
 use net::ZeroMQAdapter;
 use {
-    // Local imports
     app::schema::get_main_db_path,
+    // Local imports
+    db::get_app_db_path,
     gfx::Renderer,
     // Global imports
-    kvdb_overlay::Database,
+    kvdb_overlay::Database as KvDb,
     prop::{PropertyBool, PropertyStr, Role},
     scene::Slot,
     std::io::Cursor,
@@ -157,7 +160,10 @@ impl God {
         std::env::set_current_dir(basename).unwrap();
 
         let db_path = get_main_db_path();
-        let db = Database::open_default(&db_path).expect("KVDB failed to open");
+        let kv_db = KvDb::open_default(&db_path).expect("KVDB failed to open");
+        let app_db_path = get_app_db_path();
+        let app_db = smol::block_on(AppDb::new(app_db_path.to_str().unwrap()))
+            .expect("turso app db failed to open");
 
         let bg_ex = Arc::new(smol::Executor::new());
         let fg_ex = Arc::new(smol::Executor::new());
@@ -179,9 +185,10 @@ impl God {
         let app2 = app.clone();
         let cv_app_is_setup = Arc::new(CondVar::new());
         let cv = cv_app_is_setup.clone();
-        let db2 = db.clone();
+        let kv_db2 = kv_db.clone();
+        let app_db2 = app_db.clone();
         let app_task = fg_ex.spawn(async move {
-            app2.setup(db2).await.unwrap();
+            app2.setup(kv_db2, app_db2).await;
             cv.notify();
         });
         fg_runtime.push_task(app_task);
@@ -204,7 +211,7 @@ impl God {
             let cv = cv_app_is_setup.clone();
             let redraw = app.redraw_trigger.clone();
             let plug_task = bg_ex.spawn(async move {
-                load_plugins(ex, sg_root, redraw, cv, db).await;
+                load_plugins(ex, sg_root, redraw, cv, kv_db, app_db).await;
             });
             bg_runtime.push_task(plug_task);
         }
@@ -276,9 +283,10 @@ static GOD: OnceLock<God> = OnceLock::new();
 async fn load_plugins(
     ex: ExecutorPtr,
     sg_root: SceneNodePtr,
-    redraw: crate::ui::RedrawTrigger,
+    redraw: RedrawTrigger,
     cv: Arc<CondVar>,
-    db: Database,
+    kv_db: KvDb,
+    app_db: AppDbPtr,
 ) {
     let plugin = SceneNode::new("plugin", SceneNodeType::PluginRoot);
     let plugin = plugin.setup_null();
@@ -294,7 +302,7 @@ async fn load_plugins(
         let darkirc = create_darkirc("darkirc");
         let darkirc = darkirc
             .setup(|me| async {
-                plugin::DarkIrc::new(me, sg_root.clone(), ex.clone(), db)
+                plugin::DarkIrc::new(me, sg_root.clone(), ex.clone(), kv_db, app_db)
                     .await
                     .expect("DarkIrc pimpl setup")
             })

+ 22 - 58
bin/app/src/plugin/darkirc.rs

@@ -48,7 +48,7 @@ use irc2::{
     irc::{server::MAX_NICK_LEN, IrcChannel, IrcContact},
     pad, unpad, Privmsg,
 };
-use kvdb_overlay::{Database, Tree};
+use kvdb_overlay::Database as KvDb;
 use parking_lot::Mutex as SyncMutex;
 
 use crate::{
@@ -57,6 +57,7 @@ use crate::{
         menu::{channel::Channel, contact::Contact},
         read_joined_channels,
     },
+    db::AppDbPtr,
     error::{Error, Result},
     prop::{BatchGuardPtr, PropertyAtomicGuard, PropertyPtr, PropertyStr, Role},
     scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak, Slot},
@@ -172,9 +173,7 @@ pub struct DarkIrc {
     nick: PropertyStr,
     pub channels: RwLock<HashMap<String, IrcChannel>>,
     pub contacts: RwLock<HashMap<String, IrcContact>>,
-    channels_tree: Tree,
-    contacts_tree: Tree,
-    nick_tree: Tree,
+    app_db: AppDbPtr,
     dm_secret: SecretKey,
     ex: ExecutorPtr,
 }
@@ -184,24 +183,16 @@ impl DarkIrc {
         node: SceneNodeWeak,
         sg_root: SceneNodePtr,
         ex: ExecutorPtr,
-        db: Database,
+        kv_db: KvDb,
+        app_db: AppDbPtr,
     ) -> Result<Pimpl> {
         let node_ref = &node.upgrade().unwrap();
         let nick = PropertyStr::wrap(node_ref, Role::Internal, "nick", 0).unwrap();
 
         i!("Starting DarkIRC backend");
 
-        // Use the unified db for reading channels (UI stores channels there)
-        let channels_tree = db.open_tree_default("channels")?;
-        i!("Opened channels tree from unified db");
-
-        let contacts_tree = db.open_tree_default("contacts")?;
-        i!("Opened contacts tree from unified db");
-
-        let nick_tree = db.open_tree_default("nick")?;
-        i!("Opened nick tree from unified db");
-
-        let dm_secret = Self::load_or_create_dm_identity(&db);
+        let dm_secret_bytes = app_db.dm_secret().await?;
+        let dm_secret = SecretKey::from_bytes(dm_secret_bytes);
         let dm_public_b58 = bs58::encode(dm_secret.public_key().to_bytes()).into_string();
         // Expose our DM public key on the plugin node so it can be displayed/shared.
         node_ref
@@ -270,7 +261,7 @@ impl DarkIrc {
 
         let event_graph = match EventGraph::new(
             p2p.clone(),
-            db.clone(),
+            kv_db.clone(),
             std::path::PathBuf::new(),
             false,
             EventGraphConfig {
@@ -292,10 +283,8 @@ impl DarkIrc {
             }
         };
 
-        if let Ok(Some(nick_bytes)) = nick_tree.get(b"value") {
-            if let Ok(prev_nick) = String::from_utf8(nick_bytes.to_vec()) {
-                nick.set(&mut PropertyAtomicGuard::none(), prev_nick);
-            }
+        if let Some(prev_nick) = app_db.nick_get().await? {
+            nick.set(&mut PropertyAtomicGuard::none(), prev_nick);
         }
 
         let self_ = Arc::new(Self {
@@ -310,9 +299,7 @@ impl DarkIrc {
 
             channels: RwLock::new(HashMap::new()),
             contacts: RwLock::new(HashMap::new()),
-            channels_tree,
-            contacts_tree,
-            nick_tree,
+            app_db,
             dm_secret,
 
             ex: ex.clone(),
@@ -727,24 +714,20 @@ impl DarkIrc {
         }
     }
 
-    /// Load channels from UI database and populate encryption keys
+    /// Load channels from the app database and populate encryption keys
     pub async fn load_channels_from_db(&self) {
         let mut channels = self.channels.write().await;
         channels.clear();
 
         let joined: HashSet<String> = read_joined_channels().into_iter().collect();
 
-        for item in self.channels_tree.iter() {
-            let (key, val) = item.unwrap();
-            let channel_name = String::from_utf8_lossy(&key).to_string();
-            let full_name = format!("#{}", channel_name);
+        for ui_channel in self.app_db.channels().await.unwrap() {
+            let full_name = format!("#{}", ui_channel.name);
 
             if !joined.contains(&full_name) {
                 continue
             }
 
-            let ui_channel = deserialize_async::<Channel>(&val).await.unwrap();
-
             // Convert to IrcChannel with encryption
             let mut irc_channel =
                 IrcChannel { topic: String::new(), nicks: HashSet::new(), saltbox: None };
@@ -762,48 +745,29 @@ impl DarkIrc {
 
             let is_encrypted = irc_channel.saltbox.is_some();
             channels.insert(full_name, irc_channel);
-            i!("Loaded channel: #{} (encrypted: {})", channel_name, is_encrypted);
+            i!("Loaded channel: #{} (encrypted: {})", ui_channel.name, is_encrypted);
         }
     }
 
-    /// Load (or generate on first run) the single global DM identity key.
-    fn load_or_create_dm_identity(db: &Database) -> SecretKey {
-        let tree = db.open_tree_default("dm_identity").expect("cannot open dm_identity tree");
-        if let Ok(Some(stored)) = tree.get(b"secret") {
-            if stored.len() == 32 {
-                let arr: [u8; 32] = stored.try_into().unwrap();
-                return SecretKey::from_bytes(arr);
-            }
-        }
-        let bytes: [u8; 32] = rand::random();
-        let _ = tree.insert(b"secret", &bytes);
-        SecretKey::from_bytes(bytes)
-    }
-
-    /// Load contacts from the UI database and build their encryption boxes.
+    /// Load contacts from the app database and build their encryption boxes.
     pub async fn load_contacts_from_db(&self) {
         let mut contacts = self.contacts.write().await;
         contacts.clear();
 
         let joined: HashSet<String> = read_joined_channels().into_iter().collect();
 
-        for item in self.contacts_tree.iter() {
-            let (key, val) = item.unwrap();
-            let name = String::from_utf8_lossy(&key).to_string();
-
-            if !joined.contains(&format!("@{}", name)) {
+        for contact in self.app_db.contacts().await.unwrap() {
+            if !joined.contains(&format!("@{}", contact.name)) {
                 continue
             }
 
-            let contact = deserialize_async::<Contact>(&val).await.unwrap();
-
             let their_public = PublicKey::from(contact.public);
             let saltbox = Arc::new(ChaChaBox::new(&their_public, &self.dm_secret));
             let self_saltbox =
                 Arc::new(ChaChaBox::new(&self.dm_secret.public_key(), &self.dm_secret));
 
-            contacts.insert(name.clone(), IrcContact { saltbox, self_saltbox });
-            i!("Loaded contact: {name}");
+            contacts.insert(contact.name.clone(), IrcContact { saltbox, self_saltbox });
+            i!("Loaded contact: {}", contact.name);
         }
     }
 
@@ -960,8 +924,8 @@ impl DarkIrc {
 
         let mut on_modify = OnModify::new(ex.clone(), self.node.clone(), me.clone());
         async fn save_nick(self_: Arc<DarkIrc>, _batch: BatchGuardPtr) {
-            if let Err(err) = self_.nick_tree.insert(b"value", self_.nick.get().as_bytes()) {
-                e!("Failed persisting nick to kvdb: {err}");
+            if let Err(err) = self_.app_db.nick_set(&self_.nick.get()).await {
+                e!("Failed persisting nick to app db: {err}");
             }
         }
         on_modify.when_change(self.nick.prop(), save_nick);

+ 18 - 11
bin/app/src/plugin/fud.rs

@@ -33,7 +33,7 @@ use fud::{
     util::{hash_to_string, FileSelection},
     Fud,
 };
-use kvdb_overlay::Database;
+use kvdb_overlay::Database as KvDb;
 use smol::lock::Mutex;
 use std::{
     collections::HashSet,
@@ -136,8 +136,8 @@ impl FudPlugin {
 
         i!("Starting Fud backend");
         let db_path = get_db_path();
-        let db = match Database::open_default(&db_path) {
-            Ok(db) => db,
+        let kv_db = match KvDb::open_default(&db_path) {
+            Ok(kv_db) => kv_db,
             Err(err) => {
                 e!("Kvdb database '{}' failed to open: {err}!", db_path.display());
                 return Err(Error::KvdbErr)
@@ -241,14 +241,21 @@ impl FudPlugin {
         p2p.session_direct().start_peer_discovery();
 
         let event_pub = Publisher::new();
-        let fud: Arc<Fud> =
-            match Fud::new(fud_settings, p2p.clone(), &db, event_pub.clone(), ex.clone()).await {
-                Ok(fud) => fud,
-                Err(err) => {
-                    e!("Cannot create fud instance: {err}");
-                    return Err(Error::ServiceFailed)
-                }
-            };
+        let fud: Arc<Fud> = match Fud::new(
+            fud_settings,
+            p2p.clone(),
+            &kv_db,
+            event_pub.clone(),
+            ex.clone(),
+        )
+        .await
+        {
+            Ok(fud) => fud,
+            Err(err) => {
+                e!("Cannot create fud instance: {err}");
+                return Err(Error::ServiceFailed)
+            }
+        };
 
         let self_ = Arc::new(Self {
             node: node.clone(),

+ 19 - 34
bin/app/src/setting.rs

@@ -21,10 +21,10 @@ use std::{
     sync::{Arc, Mutex as SyncMutex},
 };
 
-use darkfi_serial::{Decodable, Encodable, VarInt};
-use kvdb_overlay::Tree;
+use darkfi_serial::{Decodable, Encodable};
 
 use crate::{
+    db::AppDbPtr,
     error::{Error, Result},
     prop::{
         Property, PropertyAtomicGuard, PropertyPtr, PropertySubType, PropertyType, PropertyValue,
@@ -79,26 +79,24 @@ pub struct Setting {
 }
 
 impl Setting {
-    pub async fn new(node: SceneNodeWeak, db_tree: Tree, ex: ExecutorPtr) -> Pimpl {
+    pub async fn new(node: SceneNodeWeak, app_db: AppDbPtr, ex: ExecutorPtr) -> Pimpl {
         let node_ref = node.upgrade().unwrap();
 
         // Load any persisted properties from the db.
-        for entry in db_tree.iter() {
-            let (key, data) = entry.unwrap();
-            let key = String::from_utf8(key).unwrap();
-            let prop = node_ref.get_property(&key).unwrap();
-            Self::load_prop(&prop, &data).unwrap();
+        for (name, idx, data) in app_db.settings_all().await.unwrap() {
+            let prop = node_ref.get_property(&name).unwrap();
+            Self::load_prop(&prop, idx, &data).unwrap();
         }
 
         // Spawn tasks persisting our properties to the db when they change.
         let mut tasks = vec![];
         for prop in &node_ref.props {
-            let db_tree2 = db_tree.clone();
+            let app_db2 = app_db.clone();
             let prop2 = prop.clone();
             let on_modify_sub = prop.subscribe_modify();
             let task = ex.spawn(async move {
                 while let Ok((_role, _action, _guard)) = on_modify_sub.receive().await {
-                    Self::save_prop(&prop2, &db_tree2).unwrap();
+                    Self::save_prop(&prop2, &app_db2).await.unwrap();
                 }
             });
             tasks.push(task);
@@ -107,31 +105,21 @@ impl Setting {
         Pimpl::Setting(Arc::new(Self { tasks: SyncMutex::new(tasks) }))
     }
 
-    /// Persist the state of a property under its name as the db key.
-    /// The db value is a list of (prop_idx, prop_value) pairs exactly
-    /// specifying which idxs inside the property are set. If none are
-    /// set the key is dropped.
-    fn save_prop(prop: &PropertyPtr, db_tree: &Tree) -> Result<()> {
+    /// Persist the state of a property: one row per set index keyed by
+    /// (prop name, idx). Unset indexes have their row deleted.
+    async fn save_prop(prop: &PropertyPtr, app_db: &AppDbPtr) -> Result<()> {
         assert!(prop.is_bounded());
 
-        let mut data = vec![];
-        let mut is_set = false;
-
         for i in 0..prop.get_len() {
             if prop.is_unset(i)? {
+                app_db.setting_remove_idx(&prop.name, i as u32).await?;
                 continue
             }
 
-            is_set = true;
             let val = prop.get_value(i)?;
-            VarInt(i as u64).encode(&mut data)?;
+            let mut data = vec![];
             Self::encode_value(prop, &val, &mut data)?;
-        }
-
-        if is_set {
-            db_tree.insert(prop.name.as_bytes(), &data)?;
-        } else {
-            db_tree.remove(prop.name.as_bytes())?;
+            app_db.setting_put(&prop.name, i as u32, "prop", &data).await?;
         }
 
         Ok(())
@@ -159,18 +147,15 @@ impl Setting {
         Ok(())
     }
 
-    /// Restore a property state previously written by `Self::save_prop()`.
-    fn load_prop(prop: &PropertyPtr, data: &[u8]) -> Result<()> {
+    /// Restore the value of a property at `idx` previously written by
+    /// `Self::save_prop()`.
+    fn load_prop(prop: &PropertyPtr, idx: u32, data: &[u8]) -> Result<()> {
         assert!(prop.is_bounded());
 
         let mut cur = Cursor::new(data);
         let atom = &mut PropertyAtomicGuard::none();
-
-        while (cur.position() as usize) < data.len() {
-            let i = VarInt::decode(&mut cur)?.0 as usize;
-            let val = Self::decode_value(prop, &mut cur)?;
-            Self::apply_value(prop, atom, i, val)?;
-        }
+        let val = Self::decode_value(prop, &mut cur)?;
+        Self::apply_value(prop, atom, idx as usize, val)?;
 
         Ok(())
     }

+ 3 - 2
bin/app/src/ui/chatview/mod.rs

@@ -44,7 +44,6 @@ pub use page::FileMessageStatus;
 use page::MessageBuffer;
 
 use crate::{
-    clipboard,
     gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, RenderApi, Renderer},
     mesh::{Color, MeshBuilder},
     prop::{
@@ -52,7 +51,9 @@ use crate::{
         PropertyUint32, Role,
     },
     scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak},
-    text, ExecutorPtr,
+    text,
+    util::clipboard,
+    ExecutorPtr,
 };
 
 use super::{DrawUpdate, OnModify, RedrawTrigger, UIObject};

+ 1 - 1
bin/app/src/ui/edit/mod.rs

@@ -38,7 +38,6 @@ use tracing::instrument;
 #[cfg(target_os = "android")]
 use crate::android::{is_ime_visible, textinput::AndroidTextInputState};
 use crate::{
-    clipboard,
     gfx::{
         anim::Frame as AnimFrame, gfxtag, DrawCall, DrawInstruction, DrawMesh, ManagedSeqAnimPtr,
         Point, Rectangle, RenderApi, Renderer, Vertex,
@@ -50,6 +49,7 @@ use crate::{
     },
     scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak},
     text::{self, Editor},
+    util::clipboard,
     ExecutorPtr,
 };
 

+ 0 - 0
bin/app/src/clipboard.rs → bin/app/src/util/clipboard.rs


+ 1 - 0
bin/app/src/util/mod.rs

@@ -20,6 +20,7 @@
 use colored::Colorize;
 use std::time::{SystemTime, UNIX_EPOCH};
 
+pub mod clipboard;
 pub mod i18n;
 mod rt;
 pub use rt::{AsyncRuntime, ExecutorPtr};