app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 6d70d9b6f36dfab170960fb9cab7e59f15c18fa4
parent 977fc15b6e3c4b184b08e805ded8f10f412fe024
Author: triesap <triesap@radroots.dev>
Date:   Mon, 19 Jan 2026 04:43:03 +0000

app-core: add tangle client

- add tangle client types plus export/sync models
- implement web tangle adapter backed by sql engine and schema
- convert signed nostr drafts for ingest and sync summaries
- re-export tangle schema types and enable events conversion

Diffstat:
MCargo.lock | 236+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.toml | 7+++++++
Mcrates/core/Cargo.toml | 9++++++++-
Mcrates/core/src/tangle/mod.rs | 19+++++++++++++++++++
Acrates/core/src/tangle/types.rs | 419+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/core/src/tangle/web.rs | 1165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 1854 insertions(+), 1 deletion(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -45,6 +45,15 @@ dependencies = [ ] [[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] name = "any_spawner" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -126,6 +135,12 @@ dependencies = [ ] [[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] name = "base16" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -269,6 +284,19 @@ dependencies = [ ] [[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] name = "cipher" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -378,6 +406,12 @@ dependencies = [ ] [[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -787,6 +821,30 @@ dependencies = [ ] [[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] name = "icu_collections" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1185,6 +1243,15 @@ dependencies = [ ] [[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] name = "num_cpus" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1473,14 +1540,21 @@ version = "0.1.0" dependencies = [ "async-trait", "base64", + "chrono", "futures", "getrandom 0.2.17", + "hex", "js-sys", "radroots-nostr", + "radroots-sql-core", + "radroots-tangle-db", + "radroots-tangle-db-schema", + "radroots-tangle-events", "rusqlite", "serde", "serde-wasm-bindgen", "serde_json", + "sha2", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -1488,16 +1562,104 @@ dependencies = [ ] [[package]] +name = "radroots-core" +version = "0.1.0" +dependencies = [ + "rust_decimal", + "rust_decimal_macros", + "serde", +] + +[[package]] +name = "radroots-events" +version = "0.1.0" +dependencies = [ + "radroots-core", + "serde", +] + +[[package]] +name = "radroots-events-codec" +version = "0.1.0" +dependencies = [ + "radroots-core", + "radroots-events", + "serde", + "serde_json", +] + +[[package]] name = "radroots-nostr" version = "0.1.0" dependencies = [ "nostr", + "radroots-events", "serde", "serde_json", "thiserror 1.0.69", ] [[package]] +name = "radroots-sql-core" +version = "0.1.0" +dependencies = [ + "chrono", + "rusqlite", + "serde", + "serde_json", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "radroots-tangle-db" +version = "0.1.0" +dependencies = [ + "hex", + "radroots-sql-core", + "radroots-tangle-db-schema", + "radroots-types", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "radroots-tangle-db-schema" +version = "0.1.0" +dependencies = [ + "radroots-types", + "serde", + "serde_json", +] + +[[package]] +name = "radroots-tangle-events" +version = "0.1.0" +dependencies = [ + "base64", + "hex", + "radroots-events", + "radroots-events-codec", + "radroots-sql-core", + "radroots-tangle-db", + "radroots-tangle-db-schema", + "radroots-types", + "serde", + "serde_json", + "sha2", + "uuid", +] + +[[package]] +name = "radroots-types" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1649,6 +1811,27 @@ dependencies = [ ] [[package]] +name = "rust_decimal" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" +dependencies = [ + "arrayvec", + "num-traits", + "serde", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519" +dependencies = [ + "quote", + "syn", +] + +[[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2377,12 +2560,65 @@ dependencies = [ ] [[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -64,7 +64,14 @@ base64 = "0.22" serde-wasm-bindgen = "0.6" rusqlite = { version = "0.31", default-features = false } url = "2" +chrono = "0.4" +hex = "0.4" +sha2 = "0.10" radroots-nostr = { path = "refs/crates/nostr" } +radroots-sql-core = { path = "refs/crates/sql-core" } +radroots-tangle-db = { path = "refs/crates/tangle-db" } +radroots-tangle-db-schema = { path = "refs/crates/tangle-db-schema" } +radroots-tangle-events = { path = "refs/crates/tangle-events" } [profile.release] codegen-units = 1 diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml @@ -15,9 +15,16 @@ serde = { workspace = true } serde_json = { workspace = true } getrandom = { workspace = true } base64 = { workspace = true } -radroots-nostr = { workspace = true } +radroots-nostr = { workspace = true, features = ["events"] } rusqlite = { workspace = true, features = ["bundled", "serialize"] } url = { workspace = true } +chrono = { workspace = true } +hex = { workspace = true } +sha2 = { workspace = true } +radroots-sql-core = { workspace = true, features = ["native"] } +radroots-tangle-db = { workspace = true } +radroots-tangle-db-schema = { workspace = true } +radroots-tangle-events = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = { workspace = true } diff --git a/crates/core/src/tangle/mod.rs b/crates/core/src/tangle/mod.rs @@ -1,3 +1,22 @@ pub mod error; +pub mod types; +pub mod web; pub use error::{RadrootsClientTangleError, RadrootsClientTangleErrorMessage}; +pub use types::{ + RadrootsClientTangle, + RadrootsClientTangleConfig, + RadrootsClientTangleDatabaseExportManifest, + RadrootsClientTangleDatabaseExportManifestClient, + RadrootsClientTangleDatabaseExportManifestRs, + RadrootsClientTangleDatabaseExportOptions, + RadrootsClientTangleDatabaseExportSnapshot, + RadrootsClientTangleDatabaseJsonExport, + RadrootsClientTangleNostrEventDraft, + RadrootsClientTangleNostrSyncBundle, + RadrootsClientTangleNostrSyncOptions, + RadrootsClientTangleNostrSyncSigner, + RadrootsClientTangleNostrSyncSummary, + RadrootsClientTangleResult, +}; +pub use web::RadrootsClientWebTangle; diff --git a/crates/core/src/tangle/types.rs b/crates/core/src/tangle/types.rs @@ -0,0 +1,419 @@ +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use radroots_tangle_db::backup::DatabaseBackup; +use radroots_tangle_db::TangleDbExportManifestRs; +pub use radroots_tangle_db_schema::{ + farm::*, + farm_gcs_location::*, + farm_member::*, + farm_member_claim::*, + farm_tag::*, + gcs_location::*, + log_error::*, + media_image::*, + nostr_event_state::*, + nostr_profile::*, + nostr_profile_relay::*, + nostr_relay::*, + plot::*, + plot_gcs_location::*, + plot_tag::*, + trade_product::*, + trade_product_location::*, + trade_product_media::*, +}; +use radroots_tangle_events::{RadrootsTangleEventDraft, RadrootsTangleSyncBundle}; + +use crate::idb::RadrootsClientIdbConfig; +use crate::sql::{RadrootsClientSqlCipherConfig, RadrootsClientSqlMigrationState}; + +use super::RadrootsClientTangleError; + +pub type RadrootsClientTangleResult<T> = Result<T, RadrootsClientTangleError>; +pub type RadrootsClientTangleDatabaseJsonExport = DatabaseBackup; +pub type RadrootsClientTangleDatabaseExportManifestRs = TangleDbExportManifestRs; +pub type RadrootsClientTangleNostrEventDraft = RadrootsTangleEventDraft; +pub type RadrootsClientTangleNostrSyncBundle = RadrootsTangleSyncBundle; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RadrootsClientTangleDatabaseExportManifestClient { + pub app_name: String, + pub app_version: String, + pub exported_at: String, + pub db_sha256: String, + pub db_size_bytes: u64, + pub store_key: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub nostr_event: Option<Value>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RadrootsClientTangleDatabaseExportManifest { + pub rust: RadrootsClientTangleDatabaseExportManifestRs, + pub client: RadrootsClientTangleDatabaseExportManifestClient, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RadrootsClientTangleDatabaseExportSnapshot { + pub manifest: RadrootsClientTangleDatabaseExportManifest, + pub db_bytes: Vec<u8>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsClientTangleDatabaseExportOptions { + pub app_name: String, + pub app_version: String, + pub store_key: Option<String>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsClientTangleNostrSyncSigner { + pub secret_key: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsClientTangleNostrSyncOptions { + pub relays: Vec<String>, + pub signers: Vec<RadrootsClientTangleNostrSyncSigner>, + pub publish_timeout_ms: Option<u64>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsClientTangleNostrSyncSummary { + pub events_total: usize, + pub events_published: usize, + pub events_failed: usize, + pub events_skipped: usize, + pub missing_signers: Vec<String>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsClientTangleConfig { + pub store_key: Option<String>, + pub idb_config: Option<RadrootsClientIdbConfig>, + pub cipher_config: Option<RadrootsClientSqlCipherConfig>, + pub sql_wasm_path: Option<String>, +} + +#[async_trait(?Send)] +pub trait RadrootsClientTangle { + async fn init(&self) -> RadrootsClientTangleResult<()>; + async fn close(&self) -> RadrootsClientTangleResult<()>; + async fn migration_state( + &self, + ) -> RadrootsClientTangleResult<RadrootsClientSqlMigrationState>; + async fn reset(&self) -> RadrootsClientTangleResult<RadrootsClientSqlMigrationState>; + async fn reinit(&self) -> RadrootsClientTangleResult<RadrootsClientSqlMigrationState>; + fn get_store_key(&self) -> &str; + async fn export_json( + &self, + ) -> RadrootsClientTangleResult<RadrootsClientTangleDatabaseJsonExport>; + async fn import_json( + &self, + backup: RadrootsClientTangleDatabaseJsonExport, + ) -> RadrootsClientTangleResult<()>; + async fn export_database( + &self, + opts: RadrootsClientTangleDatabaseExportOptions, + ) -> RadrootsClientTangleResult<RadrootsClientTangleDatabaseExportSnapshot>; + async fn nostr_sync_all( + &self, + opts: RadrootsClientTangleNostrSyncOptions, + ) -> RadrootsClientTangleResult<RadrootsClientTangleNostrSyncSummary>; + async fn farm_create(&self, opts: IFarmCreate) -> RadrootsClientTangleResult<IFarmCreateResolve>; + async fn farm_find_one(&self, opts: IFarmFindOne) -> RadrootsClientTangleResult<IFarmFindOneResolve>; + async fn farm_find_many(&self, opts: IFarmFindMany) -> RadrootsClientTangleResult<IFarmFindManyResolve>; + async fn farm_delete(&self, opts: IFarmDelete) -> RadrootsClientTangleResult<IFarmDeleteResolve>; + async fn farm_update(&self, opts: IFarmUpdate) -> RadrootsClientTangleResult<IFarmUpdateResolve>; + async fn plot_create(&self, opts: IPlotCreate) -> RadrootsClientTangleResult<IPlotCreateResolve>; + async fn plot_find_one(&self, opts: IPlotFindOne) -> RadrootsClientTangleResult<IPlotFindOneResolve>; + async fn plot_find_many(&self, opts: IPlotFindMany) -> RadrootsClientTangleResult<IPlotFindManyResolve>; + async fn plot_delete(&self, opts: IPlotDelete) -> RadrootsClientTangleResult<IPlotDeleteResolve>; + async fn plot_update(&self, opts: IPlotUpdate) -> RadrootsClientTangleResult<IPlotUpdateResolve>; + async fn gcs_location_create( + &self, + opts: IGcsLocationCreate, + ) -> RadrootsClientTangleResult<IGcsLocationCreateResolve>; + async fn gcs_location_find_one( + &self, + opts: IGcsLocationFindOne, + ) -> RadrootsClientTangleResult<IGcsLocationFindOneResolve>; + async fn gcs_location_find_many( + &self, + opts: IGcsLocationFindMany, + ) -> RadrootsClientTangleResult<IGcsLocationFindManyResolve>; + async fn gcs_location_delete( + &self, + opts: IGcsLocationDelete, + ) -> RadrootsClientTangleResult<IGcsLocationDeleteResolve>; + async fn gcs_location_update( + &self, + opts: IGcsLocationUpdate, + ) -> RadrootsClientTangleResult<IGcsLocationUpdateResolve>; + async fn farm_gcs_location_create( + &self, + opts: IFarmGcsLocationCreate, + ) -> RadrootsClientTangleResult<IFarmGcsLocationCreateResolve>; + async fn farm_gcs_location_find_one( + &self, + opts: IFarmGcsLocationFindOne, + ) -> RadrootsClientTangleResult<IFarmGcsLocationFindOneResolve>; + async fn farm_gcs_location_find_many( + &self, + opts: IFarmGcsLocationFindMany, + ) -> RadrootsClientTangleResult<IFarmGcsLocationFindManyResolve>; + async fn farm_gcs_location_delete( + &self, + opts: IFarmGcsLocationDelete, + ) -> RadrootsClientTangleResult<IFarmGcsLocationDeleteResolve>; + async fn farm_gcs_location_update( + &self, + opts: IFarmGcsLocationUpdate, + ) -> RadrootsClientTangleResult<IFarmGcsLocationUpdateResolve>; + async fn plot_gcs_location_create( + &self, + opts: IPlotGcsLocationCreate, + ) -> RadrootsClientTangleResult<IPlotGcsLocationCreateResolve>; + async fn plot_gcs_location_find_one( + &self, + opts: IPlotGcsLocationFindOne, + ) -> RadrootsClientTangleResult<IPlotGcsLocationFindOneResolve>; + async fn plot_gcs_location_find_many( + &self, + opts: IPlotGcsLocationFindMany, + ) -> RadrootsClientTangleResult<IPlotGcsLocationFindManyResolve>; + async fn plot_gcs_location_delete( + &self, + opts: IPlotGcsLocationDelete, + ) -> RadrootsClientTangleResult<IPlotGcsLocationDeleteResolve>; + async fn plot_gcs_location_update( + &self, + opts: IPlotGcsLocationUpdate, + ) -> RadrootsClientTangleResult<IPlotGcsLocationUpdateResolve>; + async fn farm_tag_create( + &self, + opts: IFarmTagCreate, + ) -> RadrootsClientTangleResult<IFarmTagCreateResolve>; + async fn farm_tag_find_one( + &self, + opts: IFarmTagFindOne, + ) -> RadrootsClientTangleResult<IFarmTagFindOneResolve>; + async fn farm_tag_find_many( + &self, + opts: IFarmTagFindMany, + ) -> RadrootsClientTangleResult<IFarmTagFindManyResolve>; + async fn farm_tag_delete( + &self, + opts: IFarmTagDelete, + ) -> RadrootsClientTangleResult<IFarmTagDeleteResolve>; + async fn farm_tag_update( + &self, + opts: IFarmTagUpdate, + ) -> RadrootsClientTangleResult<IFarmTagUpdateResolve>; + async fn plot_tag_create( + &self, + opts: IPlotTagCreate, + ) -> RadrootsClientTangleResult<IPlotTagCreateResolve>; + async fn plot_tag_find_one( + &self, + opts: IPlotTagFindOne, + ) -> RadrootsClientTangleResult<IPlotTagFindOneResolve>; + async fn plot_tag_find_many( + &self, + opts: IPlotTagFindMany, + ) -> RadrootsClientTangleResult<IPlotTagFindManyResolve>; + async fn plot_tag_delete( + &self, + opts: IPlotTagDelete, + ) -> RadrootsClientTangleResult<IPlotTagDeleteResolve>; + async fn plot_tag_update( + &self, + opts: IPlotTagUpdate, + ) -> RadrootsClientTangleResult<IPlotTagUpdateResolve>; + async fn farm_member_create( + &self, + opts: IFarmMemberCreate, + ) -> RadrootsClientTangleResult<IFarmMemberCreateResolve>; + async fn farm_member_find_one( + &self, + opts: IFarmMemberFindOne, + ) -> RadrootsClientTangleResult<IFarmMemberFindOneResolve>; + async fn farm_member_find_many( + &self, + opts: IFarmMemberFindMany, + ) -> RadrootsClientTangleResult<IFarmMemberFindManyResolve>; + async fn farm_member_delete( + &self, + opts: IFarmMemberDelete, + ) -> RadrootsClientTangleResult<IFarmMemberDeleteResolve>; + async fn farm_member_update( + &self, + opts: IFarmMemberUpdate, + ) -> RadrootsClientTangleResult<IFarmMemberUpdateResolve>; + async fn farm_member_claim_create( + &self, + opts: IFarmMemberClaimCreate, + ) -> RadrootsClientTangleResult<IFarmMemberClaimCreateResolve>; + async fn farm_member_claim_find_one( + &self, + opts: IFarmMemberClaimFindOne, + ) -> RadrootsClientTangleResult<IFarmMemberClaimFindOneResolve>; + async fn farm_member_claim_find_many( + &self, + opts: IFarmMemberClaimFindMany, + ) -> RadrootsClientTangleResult<IFarmMemberClaimFindManyResolve>; + async fn farm_member_claim_delete( + &self, + opts: IFarmMemberClaimDelete, + ) -> RadrootsClientTangleResult<IFarmMemberClaimDeleteResolve>; + async fn farm_member_claim_update( + &self, + opts: IFarmMemberClaimUpdate, + ) -> RadrootsClientTangleResult<IFarmMemberClaimUpdateResolve>; + async fn nostr_event_state_create( + &self, + opts: INostrEventStateCreate, + ) -> RadrootsClientTangleResult<INostrEventStateCreateResolve>; + async fn nostr_event_state_find_one( + &self, + opts: INostrEventStateFindOne, + ) -> RadrootsClientTangleResult<INostrEventStateFindOneResolve>; + async fn nostr_event_state_find_many( + &self, + opts: INostrEventStateFindMany, + ) -> RadrootsClientTangleResult<INostrEventStateFindManyResolve>; + async fn nostr_event_state_delete( + &self, + opts: INostrEventStateDelete, + ) -> RadrootsClientTangleResult<INostrEventStateDeleteResolve>; + async fn nostr_event_state_update( + &self, + opts: INostrEventStateUpdate, + ) -> RadrootsClientTangleResult<INostrEventStateUpdateResolve>; + async fn log_error_create( + &self, + opts: ILogErrorCreate, + ) -> RadrootsClientTangleResult<ILogErrorCreateResolve>; + async fn log_error_find_one( + &self, + opts: ILogErrorFindOne, + ) -> RadrootsClientTangleResult<ILogErrorFindOneResolve>; + async fn log_error_find_many( + &self, + opts: ILogErrorFindMany, + ) -> RadrootsClientTangleResult<ILogErrorFindManyResolve>; + async fn log_error_delete( + &self, + opts: ILogErrorDelete, + ) -> RadrootsClientTangleResult<ILogErrorDeleteResolve>; + async fn log_error_update( + &self, + opts: ILogErrorUpdate, + ) -> RadrootsClientTangleResult<ILogErrorUpdateResolve>; + async fn media_image_create( + &self, + opts: IMediaImageCreate, + ) -> RadrootsClientTangleResult<IMediaImageCreateResolve>; + async fn media_image_find_one( + &self, + opts: IMediaImageFindOne, + ) -> RadrootsClientTangleResult<IMediaImageFindOneResolve>; + async fn media_image_find_many( + &self, + opts: IMediaImageFindMany, + ) -> RadrootsClientTangleResult<IMediaImageFindManyResolve>; + async fn media_image_delete( + &self, + opts: IMediaImageDelete, + ) -> RadrootsClientTangleResult<IMediaImageDeleteResolve>; + async fn media_image_update( + &self, + opts: IMediaImageUpdate, + ) -> RadrootsClientTangleResult<IMediaImageUpdateResolve>; + async fn nostr_profile_create( + &self, + opts: INostrProfileCreate, + ) -> RadrootsClientTangleResult<INostrProfileCreateResolve>; + async fn nostr_profile_find_one( + &self, + opts: INostrProfileFindOne, + ) -> RadrootsClientTangleResult<INostrProfileFindOneResolve>; + async fn nostr_profile_find_many( + &self, + opts: INostrProfileFindMany, + ) -> RadrootsClientTangleResult<INostrProfileFindManyResolve>; + async fn nostr_profile_delete( + &self, + opts: INostrProfileDelete, + ) -> RadrootsClientTangleResult<INostrProfileDeleteResolve>; + async fn nostr_profile_update( + &self, + opts: INostrProfileUpdate, + ) -> RadrootsClientTangleResult<INostrProfileUpdateResolve>; + async fn nostr_relay_create( + &self, + opts: INostrRelayCreate, + ) -> RadrootsClientTangleResult<INostrRelayCreateResolve>; + async fn nostr_relay_find_one( + &self, + opts: INostrRelayFindOne, + ) -> RadrootsClientTangleResult<INostrRelayFindOneResolve>; + async fn nostr_relay_find_many( + &self, + opts: INostrRelayFindMany, + ) -> RadrootsClientTangleResult<INostrRelayFindManyResolve>; + async fn nostr_relay_delete( + &self, + opts: INostrRelayDelete, + ) -> RadrootsClientTangleResult<INostrRelayDeleteResolve>; + async fn nostr_relay_update( + &self, + opts: INostrRelayUpdate, + ) -> RadrootsClientTangleResult<INostrRelayUpdateResolve>; + async fn trade_product_create( + &self, + opts: ITradeProductCreate, + ) -> RadrootsClientTangleResult<ITradeProductCreateResolve>; + async fn trade_product_find_one( + &self, + opts: ITradeProductFindOne, + ) -> RadrootsClientTangleResult<ITradeProductFindOneResolve>; + async fn trade_product_find_many( + &self, + opts: ITradeProductFindMany, + ) -> RadrootsClientTangleResult<ITradeProductFindManyResolve>; + async fn trade_product_delete( + &self, + opts: ITradeProductDelete, + ) -> RadrootsClientTangleResult<ITradeProductDeleteResolve>; + async fn trade_product_update( + &self, + opts: ITradeProductUpdate, + ) -> RadrootsClientTangleResult<ITradeProductUpdateResolve>; + async fn nostr_profile_relay_set( + &self, + opts: INostrProfileRelayRelation, + ) -> RadrootsClientTangleResult<INostrProfileRelayResolve>; + async fn nostr_profile_relay_unset( + &self, + opts: INostrProfileRelayRelation, + ) -> RadrootsClientTangleResult<INostrProfileRelayResolve>; + async fn trade_product_location_set( + &self, + opts: ITradeProductLocationRelation, + ) -> RadrootsClientTangleResult<ITradeProductLocationResolve>; + async fn trade_product_location_unset( + &self, + opts: ITradeProductLocationRelation, + ) -> RadrootsClientTangleResult<ITradeProductLocationResolve>; + async fn trade_product_media_set( + &self, + opts: ITradeProductMediaRelation, + ) -> RadrootsClientTangleResult<ITradeProductMediaResolve>; + async fn trade_product_media_unset( + &self, + opts: ITradeProductMediaRelation, + ) -> RadrootsClientTangleResult<ITradeProductMediaResolve>; +} diff --git a/crates/core/src/tangle/web.rs b/crates/core/src/tangle/web.rs @@ -0,0 +1,1165 @@ +use std::cell::RefCell; +use std::collections::{BTreeMap, BTreeSet}; +use std::rc::Rc; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; + +use async_trait::async_trait; +use radroots_nostr::prelude::{ + radroots_event_from_nostr, + radroots_nostr_build_event, + RadrootsNostrEvent, + RadrootsNostrKeys, + RadrootsNostrSecretKey, +}; +use radroots_sql_core::error::SqlError; +use radroots_sql_core::{ExecOutcome, SqlExecutor}; +use radroots_sql_core::sqlite_util; +use radroots_tangle_db::{export_manifest, TangleSql}; +use radroots_tangle_events::{ + radroots_tangle_ingest_event, + radroots_tangle_sync_all, + RadrootsTangleEventDraft, + RadrootsTangleFarmSelector, + RadrootsTangleSyncRequest, +}; +use rusqlite::{params_from_iter, Connection}; +use sha2::{Digest, Sha256}; + +use crate::idb::{IDB_CONFIG_TANGLE, RadrootsClientIdbConfig}; +use crate::sql::{ + RadrootsClientSqlCipherConfig, + RadrootsClientSqlEngine, + RadrootsClientSqlEngineConfig, + RadrootsClientSqlError, + RadrootsClientSqlMigrationState, + RadrootsClientSqlParams, + RadrootsClientSqlResultRow, + RadrootsClientWebSqlEngine, +}; + +use super::error::RadrootsClientTangleError; +use super::types::{ + IFarmCreate, + IFarmCreateResolve, + IFarmDelete, + IFarmDeleteResolve, + IFarmFindMany, + IFarmFindManyResolve, + IFarmFindOne, + IFarmFindOneResolve, + IFarmUpdate, + IFarmUpdateResolve, + IFarmGcsLocationCreate, + IFarmGcsLocationCreateResolve, + IFarmGcsLocationDelete, + IFarmGcsLocationDeleteResolve, + IFarmGcsLocationFindMany, + IFarmGcsLocationFindManyResolve, + IFarmGcsLocationFindOne, + IFarmGcsLocationFindOneResolve, + IFarmGcsLocationUpdate, + IFarmGcsLocationUpdateResolve, + IFarmMemberClaimCreate, + IFarmMemberClaimCreateResolve, + IFarmMemberClaimDelete, + IFarmMemberClaimDeleteResolve, + IFarmMemberClaimFindMany, + IFarmMemberClaimFindManyResolve, + IFarmMemberClaimFindOne, + IFarmMemberClaimFindOneResolve, + IFarmMemberClaimUpdate, + IFarmMemberClaimUpdateResolve, + IFarmMemberCreate, + IFarmMemberCreateResolve, + IFarmMemberDelete, + IFarmMemberDeleteResolve, + IFarmMemberFindMany, + IFarmMemberFindManyResolve, + IFarmMemberFindOne, + IFarmMemberFindOneResolve, + IFarmMemberUpdate, + IFarmMemberUpdateResolve, + IFarmTagCreate, + IFarmTagCreateResolve, + IFarmTagDelete, + IFarmTagDeleteResolve, + IFarmTagFindMany, + IFarmTagFindManyResolve, + IFarmTagFindOne, + IFarmTagFindOneResolve, + IFarmTagUpdate, + IFarmTagUpdateResolve, + IGcsLocationCreate, + IGcsLocationCreateResolve, + IGcsLocationDelete, + IGcsLocationDeleteResolve, + IGcsLocationFindMany, + IGcsLocationFindManyResolve, + IGcsLocationFindOne, + IGcsLocationFindOneResolve, + IGcsLocationUpdate, + IGcsLocationUpdateResolve, + ILogErrorCreate, + ILogErrorCreateResolve, + ILogErrorDelete, + ILogErrorDeleteResolve, + ILogErrorFindMany, + ILogErrorFindManyResolve, + ILogErrorFindOne, + ILogErrorFindOneResolve, + ILogErrorUpdate, + ILogErrorUpdateResolve, + IMediaImageCreate, + IMediaImageCreateResolve, + IMediaImageDelete, + IMediaImageDeleteResolve, + IMediaImageFindMany, + IMediaImageFindManyResolve, + IMediaImageFindOne, + IMediaImageFindOneResolve, + IMediaImageUpdate, + IMediaImageUpdateResolve, + INostrEventStateCreate, + INostrEventStateCreateResolve, + INostrEventStateDelete, + INostrEventStateDeleteResolve, + INostrEventStateFindMany, + INostrEventStateFindManyResolve, + INostrEventStateFindOne, + INostrEventStateFindOneResolve, + INostrEventStateUpdate, + INostrEventStateUpdateResolve, + INostrProfileCreate, + INostrProfileCreateResolve, + INostrProfileDelete, + INostrProfileDeleteResolve, + INostrProfileFindMany, + INostrProfileFindManyResolve, + INostrProfileFindOne, + INostrProfileFindOneResolve, + INostrProfileUpdate, + INostrProfileUpdateResolve, + INostrRelayCreate, + INostrRelayCreateResolve, + INostrRelayDelete, + INostrRelayDeleteResolve, + INostrRelayFindMany, + INostrRelayFindManyResolve, + INostrRelayFindOne, + INostrRelayFindOneResolve, + INostrRelayUpdate, + INostrRelayUpdateResolve, + IPlotCreate, + IPlotCreateResolve, + IPlotDelete, + IPlotDeleteResolve, + IPlotFindMany, + IPlotFindManyResolve, + IPlotFindOne, + IPlotFindOneResolve, + IPlotUpdate, + IPlotUpdateResolve, + IPlotGcsLocationCreate, + IPlotGcsLocationCreateResolve, + IPlotGcsLocationDelete, + IPlotGcsLocationDeleteResolve, + IPlotGcsLocationFindMany, + IPlotGcsLocationFindManyResolve, + IPlotGcsLocationFindOne, + IPlotGcsLocationFindOneResolve, + IPlotGcsLocationUpdate, + IPlotGcsLocationUpdateResolve, + IPlotTagCreate, + IPlotTagCreateResolve, + IPlotTagDelete, + IPlotTagDeleteResolve, + IPlotTagFindMany, + IPlotTagFindManyResolve, + IPlotTagFindOne, + IPlotTagFindOneResolve, + IPlotTagUpdate, + IPlotTagUpdateResolve, + ITradeProductCreate, + ITradeProductCreateResolve, + ITradeProductDelete, + ITradeProductDeleteResolve, + ITradeProductFindMany, + ITradeProductFindManyResolve, + ITradeProductFindOne, + ITradeProductFindOneResolve, + ITradeProductUpdate, + ITradeProductUpdateResolve, + INostrProfileRelayRelation, + INostrProfileRelayResolve, + ITradeProductLocationRelation, + ITradeProductLocationResolve, + ITradeProductMediaRelation, + ITradeProductMediaResolve, + RadrootsClientTangle, + RadrootsClientTangleConfig, + RadrootsClientTangleDatabaseExportManifest, + RadrootsClientTangleDatabaseExportManifestClient, + RadrootsClientTangleDatabaseExportOptions, + RadrootsClientTangleDatabaseExportSnapshot, + RadrootsClientTangleDatabaseJsonExport, + RadrootsClientTangleNostrSyncOptions, + RadrootsClientTangleNostrSyncSigner, + RadrootsClientTangleNostrSyncSummary, + RadrootsClientTangleResult, +}; + +const DEFAULT_TANGLE_STORE_KEY: &str = "radroots-pwa-v1-tangle-db"; + +pub struct RadrootsClientWebTangle { + store_key: String, + idb_config: RadrootsClientIdbConfig, + cipher_config: RadrootsClientSqlCipherConfig, + sql_wasm_path: Option<String>, + engine: RefCell<Option<Rc<RadrootsClientWebSqlEngine>>>, + init_in_progress: RefCell<bool>, +} + +impl RadrootsClientWebTangle { + pub fn new(config: Option<RadrootsClientTangleConfig>) -> Self { + let config = config.unwrap_or(RadrootsClientTangleConfig { + store_key: None, + idb_config: None, + cipher_config: None, + sql_wasm_path: None, + }); + let store_key = config + .store_key + .unwrap_or_else(|| DEFAULT_TANGLE_STORE_KEY.to_string()); + let idb_config = config.idb_config.unwrap_or(IDB_CONFIG_TANGLE); + let cipher_config = + config + .cipher_config + .unwrap_or(RadrootsClientSqlCipherConfig::Disabled); + let sql_wasm_path = config.sql_wasm_path; + Self { + store_key, + idb_config, + cipher_config, + sql_wasm_path, + engine: RefCell::new(None), + init_in_progress: RefCell::new(false), + } + } + + fn engine_config(&self) -> RadrootsClientSqlEngineConfig { + RadrootsClientSqlEngineConfig { + store_key: self.store_key.clone(), + idb_config: self.idb_config, + cipher_config: self.cipher_config.clone(), + sql_wasm_path: self.sql_wasm_path.clone(), + } + } + + async fn init_engine(&self) -> RadrootsClientTangleResult<Rc<RadrootsClientWebSqlEngine>> { + let engine = RadrootsClientWebSqlEngine::create(self.engine_config()) + .await + .map_err(map_engine_error)?; + let tangle = self.tangle(&engine); + tangle.migrate_up().map_err(|_| RadrootsClientTangleError::InitFailure)?; + let engine = Rc::new(engine); + self.engine.borrow_mut().replace(engine.clone()); + Ok(engine) + } + + async fn ensure_ready(&self) -> RadrootsClientTangleResult<Rc<RadrootsClientWebSqlEngine>> { + if let Some(engine) = self.engine.borrow().as_ref() { + return Ok(Rc::clone(engine)); + } + if *self.init_in_progress.borrow() { + return Err(RadrootsClientTangleError::InitFailure); + } + *self.init_in_progress.borrow_mut() = true; + let result = self.init_engine().await; + *self.init_in_progress.borrow_mut() = false; + result + } + + fn tangle(&self, engine: &RadrootsClientWebSqlEngine) -> TangleSql<TangleSqlExecutor> { + TangleSql::new(TangleSqlExecutor::new(engine.shared_connection())) + } +} + +#[async_trait(?Send)] +impl RadrootsClientTangle for RadrootsClientWebTangle { + async fn init(&self) -> RadrootsClientTangleResult<()> { + let _ = self.ensure_ready().await?; + Ok(()) + } + + async fn close(&self) -> RadrootsClientTangleResult<()> { + if let Some(engine) = self.engine.borrow_mut().take() { + engine + .close() + .await + .map_err(map_engine_error)?; + } + *self.init_in_progress.borrow_mut() = false; + Ok(()) + } + + async fn migration_state(&self) -> RadrootsClientTangleResult<RadrootsClientSqlMigrationState> { + let engine = self.ensure_ready().await?; + let rows = engine + .query( + "select id, name, applied_at from __migrations order by id asc", + RadrootsClientSqlParams::Positional(Vec::new()), + ) + .map_err(map_engine_error)?; + migration_state_from_rows(rows) + } + + async fn reset(&self) -> RadrootsClientTangleResult<RadrootsClientSqlMigrationState> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + tangle.migrate_down().map_err(|_| RadrootsClientTangleError::InvalidResponse)?; + tangle.migrate_up().map_err(|_| RadrootsClientTangleError::InvalidResponse)?; + self.migration_state().await + } + + async fn reinit(&self) -> RadrootsClientTangleResult<RadrootsClientSqlMigrationState> { + if let Some(engine) = self.engine.borrow_mut().take() { + engine + .purge_storage() + .await + .map_err(map_engine_error)?; + engine + .close() + .await + .map_err(map_engine_error)?; + } + self.migration_state().await + } + + fn get_store_key(&self) -> &str { + &self.store_key + } + + async fn export_json( + &self, + ) -> RadrootsClientTangleResult<RadrootsClientTangleDatabaseJsonExport> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + tangle + .backup_database() + .map_err(|_| RadrootsClientTangleError::InvalidResponse) + } + + async fn import_json( + &self, + backup: RadrootsClientTangleDatabaseJsonExport, + ) -> RadrootsClientTangleResult<()> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + tangle + .restore_database(&backup) + .map_err(|_| RadrootsClientTangleError::InvalidResponse) + } + + async fn export_database( + &self, + opts: RadrootsClientTangleDatabaseExportOptions, + ) -> RadrootsClientTangleResult<RadrootsClientTangleDatabaseExportSnapshot> { + if let Some(store_key) = opts.store_key.clone() { + if store_key != self.store_key { + let alt = RadrootsClientWebTangle::new(Some(RadrootsClientTangleConfig { + store_key: Some(store_key), + idb_config: Some(self.idb_config), + cipher_config: Some(self.cipher_config.clone()), + sql_wasm_path: self.sql_wasm_path.clone(), + })); + let mut opts = opts.clone(); + opts.store_key = None; + let snapshot = alt.export_database(opts).await?; + let _ = alt.close().await; + return Ok(snapshot); + } + } + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + let manifest_rs = export_manifest(tangle.executor()) + .map_err(|_| RadrootsClientTangleError::InvalidResponse)?; + let db_bytes = engine.export_bytes().map_err(map_engine_error)?; + let db_sha256 = sha256_hex(&db_bytes); + let exported_at = export_timestamp(); + let manifest_client = RadrootsClientTangleDatabaseExportManifestClient { + app_name: opts.app_name, + app_version: opts.app_version, + exported_at, + db_sha256, + db_size_bytes: db_bytes.len() as u64, + store_key: self.store_key.clone(), + nostr_event: None, + }; + let manifest = RadrootsClientTangleDatabaseExportManifest { + rust: manifest_rs, + client: manifest_client, + }; + Ok(RadrootsClientTangleDatabaseExportSnapshot { manifest, db_bytes }) + } + + async fn nostr_sync_all( + &self, + opts: RadrootsClientTangleNostrSyncOptions, + ) -> RadrootsClientTangleResult<RadrootsClientTangleNostrSyncSummary> { + let engine = self.ensure_ready().await?; + let relays = normalize_relays(&opts.relays); + if relays.is_empty() || opts.signers.is_empty() { + return Err(RadrootsClientTangleError::InvalidResponse); + } + let signer_map = build_signer_map(&opts.signers); + if signer_map.is_empty() { + return Err(RadrootsClientTangleError::InvalidResponse); + } + let tangle = self.tangle(&engine); + let farms = map_db_result(tangle.farm_find_many(&IFarmFindMany { filter: None }))?; + let mut event_map: BTreeMap<String, RadrootsTangleEventDraft> = BTreeMap::new(); + for farm in farms.results { + let request = RadrootsTangleSyncRequest { + farm: RadrootsTangleFarmSelector { + id: Some(farm.id), + d_tag: None, + pubkey: None, + }, + options: None, + }; + let bundle = radroots_tangle_sync_all(tangle.executor(), &request) + .map_err(|_| RadrootsClientTangleError::InvalidResponse)?; + for draft in bundle.events { + let key = tangle_sync_event_key(&draft); + event_map.entry(key).or_insert(draft); + } + } + if event_map.is_empty() { + return Ok(RadrootsClientTangleNostrSyncSummary { + events_total: 0, + events_published: 0, + events_failed: 0, + events_skipped: 0, + missing_signers: Vec::new(), + }); + } + let mut events_published = 0; + let mut events_failed = 0; + let mut events_skipped = 0; + let mut missing_signers = BTreeSet::new(); + + for draft in event_map.values() { + let Some(secret_key) = signer_map.get(&draft.author) else { + missing_signers.insert(draft.author.clone()); + events_skipped += 1; + continue; + }; + let event = sign_draft_event(draft, secret_key)?; + let event = radroots_event_from_nostr(&event); + match radroots_tangle_ingest_event(tangle.executor(), &event) { + Ok(_) => events_published += 1, + Err(_) => events_failed += 1, + } + } + let summary = RadrootsClientTangleNostrSyncSummary { + events_total: event_map.len(), + events_published, + events_failed, + events_skipped, + missing_signers: missing_signers.into_iter().collect(), + }; + if !summary.missing_signers.is_empty() || summary.events_failed > 0 { + return Err(RadrootsClientTangleError::InvalidResponse); + } + let _ = relays; + Ok(summary) + } + + async fn farm_create(&self, opts: IFarmCreate) -> RadrootsClientTangleResult<IFarmCreateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_create(&opts)) + } + + async fn farm_find_one(&self, opts: IFarmFindOne) -> RadrootsClientTangleResult<IFarmFindOneResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_find_one(&opts)) + } + + async fn farm_find_many(&self, opts: IFarmFindMany) -> RadrootsClientTangleResult<IFarmFindManyResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_find_many(&opts)) + } + + async fn farm_delete(&self, opts: IFarmDelete) -> RadrootsClientTangleResult<IFarmDeleteResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_delete(&opts)) + } + + async fn farm_update(&self, opts: IFarmUpdate) -> RadrootsClientTangleResult<IFarmUpdateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_update(&opts)) + } + + async fn plot_create(&self, opts: IPlotCreate) -> RadrootsClientTangleResult<IPlotCreateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.plot_create(&opts)) + } + + async fn plot_find_one(&self, opts: IPlotFindOne) -> RadrootsClientTangleResult<IPlotFindOneResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.plot_find_one(&opts)) + } + + async fn plot_find_many(&self, opts: IPlotFindMany) -> RadrootsClientTangleResult<IPlotFindManyResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.plot_find_many(&opts)) + } + + async fn plot_delete(&self, opts: IPlotDelete) -> RadrootsClientTangleResult<IPlotDeleteResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.plot_delete(&opts)) + } + + async fn plot_update(&self, opts: IPlotUpdate) -> RadrootsClientTangleResult<IPlotUpdateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.plot_update(&opts)) + } + + async fn gcs_location_create(&self, opts: IGcsLocationCreate) -> RadrootsClientTangleResult<IGcsLocationCreateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.gcs_location_create(&opts)) + } + + async fn gcs_location_find_one(&self, opts: IGcsLocationFindOne) -> RadrootsClientTangleResult<IGcsLocationFindOneResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.gcs_location_find_one(&opts)) + } + + async fn gcs_location_find_many(&self, opts: IGcsLocationFindMany) -> RadrootsClientTangleResult<IGcsLocationFindManyResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.gcs_location_find_many(&opts)) + } + + async fn gcs_location_delete(&self, opts: IGcsLocationDelete) -> RadrootsClientTangleResult<IGcsLocationDeleteResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.gcs_location_delete(&opts)) + } + + async fn gcs_location_update(&self, opts: IGcsLocationUpdate) -> RadrootsClientTangleResult<IGcsLocationUpdateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.gcs_location_update(&opts)) + } + + async fn farm_gcs_location_create(&self, opts: IFarmGcsLocationCreate) -> RadrootsClientTangleResult<IFarmGcsLocationCreateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_gcs_location_create(&opts)) + } + + async fn farm_gcs_location_find_one(&self, opts: IFarmGcsLocationFindOne) -> RadrootsClientTangleResult<IFarmGcsLocationFindOneResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_gcs_location_find_one(&opts)) + } + + async fn farm_gcs_location_find_many(&self, opts: IFarmGcsLocationFindMany) -> RadrootsClientTangleResult<IFarmGcsLocationFindManyResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_gcs_location_find_many(&opts)) + } + + async fn farm_gcs_location_delete(&self, opts: IFarmGcsLocationDelete) -> RadrootsClientTangleResult<IFarmGcsLocationDeleteResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_gcs_location_delete(&opts)) + } + + async fn farm_gcs_location_update(&self, opts: IFarmGcsLocationUpdate) -> RadrootsClientTangleResult<IFarmGcsLocationUpdateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_gcs_location_update(&opts)) + } + + async fn plot_gcs_location_create(&self, opts: IPlotGcsLocationCreate) -> RadrootsClientTangleResult<IPlotGcsLocationCreateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.plot_gcs_location_create(&opts)) + } + + async fn plot_gcs_location_find_one(&self, opts: IPlotGcsLocationFindOne) -> RadrootsClientTangleResult<IPlotGcsLocationFindOneResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.plot_gcs_location_find_one(&opts)) + } + + async fn plot_gcs_location_find_many(&self, opts: IPlotGcsLocationFindMany) -> RadrootsClientTangleResult<IPlotGcsLocationFindManyResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.plot_gcs_location_find_many(&opts)) + } + + async fn plot_gcs_location_delete(&self, opts: IPlotGcsLocationDelete) -> RadrootsClientTangleResult<IPlotGcsLocationDeleteResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.plot_gcs_location_delete(&opts)) + } + + async fn plot_gcs_location_update(&self, opts: IPlotGcsLocationUpdate) -> RadrootsClientTangleResult<IPlotGcsLocationUpdateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.plot_gcs_location_update(&opts)) + } + + async fn farm_tag_create(&self, opts: IFarmTagCreate) -> RadrootsClientTangleResult<IFarmTagCreateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_tag_create(&opts)) + } + + async fn farm_tag_find_one(&self, opts: IFarmTagFindOne) -> RadrootsClientTangleResult<IFarmTagFindOneResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_tag_find_one(&opts)) + } + + async fn farm_tag_find_many(&self, opts: IFarmTagFindMany) -> RadrootsClientTangleResult<IFarmTagFindManyResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_tag_find_many(&opts)) + } + + async fn farm_tag_delete(&self, opts: IFarmTagDelete) -> RadrootsClientTangleResult<IFarmTagDeleteResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_tag_delete(&opts)) + } + + async fn farm_tag_update(&self, opts: IFarmTagUpdate) -> RadrootsClientTangleResult<IFarmTagUpdateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_tag_update(&opts)) + } + + async fn plot_tag_create(&self, opts: IPlotTagCreate) -> RadrootsClientTangleResult<IPlotTagCreateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.plot_tag_create(&opts)) + } + + async fn plot_tag_find_one(&self, opts: IPlotTagFindOne) -> RadrootsClientTangleResult<IPlotTagFindOneResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.plot_tag_find_one(&opts)) + } + + async fn plot_tag_find_many(&self, opts: IPlotTagFindMany) -> RadrootsClientTangleResult<IPlotTagFindManyResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.plot_tag_find_many(&opts)) + } + + async fn plot_tag_delete(&self, opts: IPlotTagDelete) -> RadrootsClientTangleResult<IPlotTagDeleteResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.plot_tag_delete(&opts)) + } + + async fn plot_tag_update(&self, opts: IPlotTagUpdate) -> RadrootsClientTangleResult<IPlotTagUpdateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.plot_tag_update(&opts)) + } + + async fn farm_member_create(&self, opts: IFarmMemberCreate) -> RadrootsClientTangleResult<IFarmMemberCreateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_member_create(&opts)) + } + + async fn farm_member_find_one(&self, opts: IFarmMemberFindOne) -> RadrootsClientTangleResult<IFarmMemberFindOneResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_member_find_one(&opts)) + } + + async fn farm_member_find_many(&self, opts: IFarmMemberFindMany) -> RadrootsClientTangleResult<IFarmMemberFindManyResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_member_find_many(&opts)) + } + + async fn farm_member_delete(&self, opts: IFarmMemberDelete) -> RadrootsClientTangleResult<IFarmMemberDeleteResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_member_delete(&opts)) + } + + async fn farm_member_update(&self, opts: IFarmMemberUpdate) -> RadrootsClientTangleResult<IFarmMemberUpdateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_member_update(&opts)) + } + + async fn farm_member_claim_create(&self, opts: IFarmMemberClaimCreate) -> RadrootsClientTangleResult<IFarmMemberClaimCreateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_member_claim_create(&opts)) + } + + async fn farm_member_claim_find_one(&self, opts: IFarmMemberClaimFindOne) -> RadrootsClientTangleResult<IFarmMemberClaimFindOneResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_member_claim_find_one(&opts)) + } + + async fn farm_member_claim_find_many(&self, opts: IFarmMemberClaimFindMany) -> RadrootsClientTangleResult<IFarmMemberClaimFindManyResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_member_claim_find_many(&opts)) + } + + async fn farm_member_claim_delete(&self, opts: IFarmMemberClaimDelete) -> RadrootsClientTangleResult<IFarmMemberClaimDeleteResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_member_claim_delete(&opts)) + } + + async fn farm_member_claim_update(&self, opts: IFarmMemberClaimUpdate) -> RadrootsClientTangleResult<IFarmMemberClaimUpdateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.farm_member_claim_update(&opts)) + } + + async fn nostr_event_state_create(&self, opts: INostrEventStateCreate) -> RadrootsClientTangleResult<INostrEventStateCreateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_event_state_create(&opts)) + } + + async fn nostr_event_state_find_one(&self, opts: INostrEventStateFindOne) -> RadrootsClientTangleResult<INostrEventStateFindOneResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_event_state_find_one(&opts)) + } + + async fn nostr_event_state_find_many(&self, opts: INostrEventStateFindMany) -> RadrootsClientTangleResult<INostrEventStateFindManyResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_event_state_find_many(&opts)) + } + + async fn nostr_event_state_delete(&self, opts: INostrEventStateDelete) -> RadrootsClientTangleResult<INostrEventStateDeleteResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_event_state_delete(&opts)) + } + + async fn nostr_event_state_update(&self, opts: INostrEventStateUpdate) -> RadrootsClientTangleResult<INostrEventStateUpdateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_event_state_update(&opts)) + } + + async fn log_error_create(&self, opts: ILogErrorCreate) -> RadrootsClientTangleResult<ILogErrorCreateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.log_error_create(&opts)) + } + + async fn log_error_find_one(&self, opts: ILogErrorFindOne) -> RadrootsClientTangleResult<ILogErrorFindOneResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.log_error_find_one(&opts)) + } + + async fn log_error_find_many(&self, opts: ILogErrorFindMany) -> RadrootsClientTangleResult<ILogErrorFindManyResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.log_error_find_many(&opts)) + } + + async fn log_error_delete(&self, opts: ILogErrorDelete) -> RadrootsClientTangleResult<ILogErrorDeleteResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.log_error_delete(&opts)) + } + + async fn log_error_update(&self, opts: ILogErrorUpdate) -> RadrootsClientTangleResult<ILogErrorUpdateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.log_error_update(&opts)) + } + + async fn media_image_create(&self, opts: IMediaImageCreate) -> RadrootsClientTangleResult<IMediaImageCreateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.media_image_create(&opts)) + } + + async fn media_image_find_one(&self, opts: IMediaImageFindOne) -> RadrootsClientTangleResult<IMediaImageFindOneResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.media_image_find_one(&opts)) + } + + async fn media_image_find_many(&self, opts: IMediaImageFindMany) -> RadrootsClientTangleResult<IMediaImageFindManyResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.media_image_find_many(&opts)) + } + + async fn media_image_delete(&self, opts: IMediaImageDelete) -> RadrootsClientTangleResult<IMediaImageDeleteResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.media_image_delete(&opts)) + } + + async fn media_image_update(&self, opts: IMediaImageUpdate) -> RadrootsClientTangleResult<IMediaImageUpdateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.media_image_update(&opts)) + } + + async fn nostr_profile_create(&self, opts: INostrProfileCreate) -> RadrootsClientTangleResult<INostrProfileCreateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_profile_create(&opts)) + } + + async fn nostr_profile_find_one(&self, opts: INostrProfileFindOne) -> RadrootsClientTangleResult<INostrProfileFindOneResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_profile_find_one(&opts)) + } + + async fn nostr_profile_find_many(&self, opts: INostrProfileFindMany) -> RadrootsClientTangleResult<INostrProfileFindManyResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_profile_find_many(&opts)) + } + + async fn nostr_profile_delete(&self, opts: INostrProfileDelete) -> RadrootsClientTangleResult<INostrProfileDeleteResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_profile_delete(&opts)) + } + + async fn nostr_profile_update(&self, opts: INostrProfileUpdate) -> RadrootsClientTangleResult<INostrProfileUpdateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_profile_update(&opts)) + } + + async fn nostr_relay_create(&self, opts: INostrRelayCreate) -> RadrootsClientTangleResult<INostrRelayCreateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_relay_create(&opts)) + } + + async fn nostr_relay_find_one(&self, opts: INostrRelayFindOne) -> RadrootsClientTangleResult<INostrRelayFindOneResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_relay_find_one(&opts)) + } + + async fn nostr_relay_find_many(&self, opts: INostrRelayFindMany) -> RadrootsClientTangleResult<INostrRelayFindManyResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_relay_find_many(&opts)) + } + + async fn nostr_relay_delete(&self, opts: INostrRelayDelete) -> RadrootsClientTangleResult<INostrRelayDeleteResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_relay_delete(&opts)) + } + + async fn nostr_relay_update(&self, opts: INostrRelayUpdate) -> RadrootsClientTangleResult<INostrRelayUpdateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_relay_update(&opts)) + } + + async fn trade_product_create(&self, opts: ITradeProductCreate) -> RadrootsClientTangleResult<ITradeProductCreateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.trade_product_create(&opts)) + } + + async fn trade_product_find_one(&self, opts: ITradeProductFindOne) -> RadrootsClientTangleResult<ITradeProductFindOneResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.trade_product_find_one(&opts)) + } + + async fn trade_product_find_many(&self, opts: ITradeProductFindMany) -> RadrootsClientTangleResult<ITradeProductFindManyResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.trade_product_find_many(&opts)) + } + + async fn trade_product_delete(&self, opts: ITradeProductDelete) -> RadrootsClientTangleResult<ITradeProductDeleteResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.trade_product_delete(&opts)) + } + + async fn trade_product_update(&self, opts: ITradeProductUpdate) -> RadrootsClientTangleResult<ITradeProductUpdateResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.trade_product_update(&opts)) + } + + async fn nostr_profile_relay_set(&self, opts: INostrProfileRelayRelation) -> RadrootsClientTangleResult<INostrProfileRelayResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_profile_relay_set(&opts)) + } + + async fn nostr_profile_relay_unset(&self, opts: INostrProfileRelayRelation) -> RadrootsClientTangleResult<INostrProfileRelayResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.nostr_profile_relay_unset(&opts)) + } + + async fn trade_product_location_set(&self, opts: ITradeProductLocationRelation) -> RadrootsClientTangleResult<ITradeProductLocationResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.trade_product_location_set(&opts)) + } + + async fn trade_product_location_unset(&self, opts: ITradeProductLocationRelation) -> RadrootsClientTangleResult<ITradeProductLocationResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.trade_product_location_unset(&opts)) + } + + async fn trade_product_media_set(&self, opts: ITradeProductMediaRelation) -> RadrootsClientTangleResult<ITradeProductMediaResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.trade_product_media_set(&opts)) + } + + async fn trade_product_media_unset(&self, opts: ITradeProductMediaRelation) -> RadrootsClientTangleResult<ITradeProductMediaResolve> { + let engine = self.ensure_ready().await?; + let tangle = self.tangle(&engine); + map_db_result(tangle.trade_product_media_unset(&opts)) + } + +} + +struct TangleSqlExecutor { + conn: Arc<Mutex<Connection>>, +} + +impl TangleSqlExecutor { + fn new(conn: Arc<Mutex<Connection>>) -> Self { + Self { conn } + } +} + +impl SqlExecutor for TangleSqlExecutor { + fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { + let binds = sqlite_util::parse_params(params_json)?; + let conn = self.conn.lock().map_err(|_| SqlError::Internal)?; + let changes = conn + .execute(sql, params_from_iter(binds.into_iter())) + .map_err(SqlError::from)?; + let last_insert_id = conn.last_insert_rowid(); + Ok(ExecOutcome { + changes: changes as i64, + last_insert_id, + }) + } + + fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { + let binds = sqlite_util::parse_params(params_json)?; + let rows = { + let conn = self.conn.lock().map_err(|_| SqlError::Internal)?; + let mut stmt = conn.prepare(sql).map_err(SqlError::from)?; + let mapped = stmt.query_map( + params_from_iter(binds.into_iter()), + sqlite_util::row_to_json, + )?; + mapped + .collect::<Result<Vec<_>, _>>() + .map_err(SqlError::from)? + }; + serde_json::to_string(&rows).map_err(SqlError::from) + } + + fn begin(&self) -> Result<(), SqlError> { + let conn = self.conn.lock().map_err(|_| SqlError::Internal)?; + conn.execute("BEGIN", []).map_err(SqlError::from)?; + Ok(()) + } + + fn commit(&self) -> Result<(), SqlError> { + let conn = self.conn.lock().map_err(|_| SqlError::Internal)?; + conn.execute("COMMIT", []).map_err(SqlError::from)?; + Ok(()) + } + + fn rollback(&self) -> Result<(), SqlError> { + let conn = self.conn.lock().map_err(|_| SqlError::Internal)?; + conn.execute("ROLLBACK", []).map_err(SqlError::from)?; + Ok(()) + } +} + +fn map_engine_error(err: RadrootsClientSqlError) -> RadrootsClientTangleError { + match err { + RadrootsClientSqlError::EngineUnavailable => RadrootsClientTangleError::RuntimeUnavailable, + RadrootsClientSqlError::IdbUndefined => RadrootsClientTangleError::RuntimeUnavailable, + RadrootsClientSqlError::ImportFailure => RadrootsClientTangleError::InvalidResponse, + RadrootsClientSqlError::ExportFailure => RadrootsClientTangleError::InvalidResponse, + RadrootsClientSqlError::BackupFailure => RadrootsClientTangleError::InvalidResponse, + RadrootsClientSqlError::InvalidParams => RadrootsClientTangleError::ParseFailure, + RadrootsClientSqlError::QueryFailure => RadrootsClientTangleError::InvalidResponse, + } +} + +fn migration_state_from_rows( + rows: Vec<RadrootsClientSqlResultRow>, +) -> RadrootsClientTangleResult<RadrootsClientSqlMigrationState> { + let mut names = Vec::with_capacity(rows.len()); + for row in rows { + let name = row + .get("name") + .and_then(|value| value.as_str()) + .ok_or(RadrootsClientTangleError::ParseFailure)?; + names.push(name.to_string()); + } + Ok(RadrootsClientSqlMigrationState { + applied_names: names.clone(), + applied_count: names.len(), + }) +} + +fn map_db_result<T, E>(result: Result<T, E>) -> RadrootsClientTangleResult<T> { + result.map_err(|_| RadrootsClientTangleError::InvalidResponse) +} + +fn normalize_relays(relays: &[String]) -> Vec<String> { + let mut unique = BTreeSet::new(); + for relay in relays { + let relay = relay.trim(); + if relay.is_empty() { + continue; + } + unique.insert(relay.to_string()); + } + unique.into_iter().collect() +} + +fn tangle_sync_event_key(draft: &RadrootsTangleEventDraft) -> String { + let d_tag = draft_d_tag(&draft.tags); + format!("{}:{}:{}", draft.kind, draft.author, d_tag.unwrap_or_default()) +} + +fn draft_d_tag(tags: &[Vec<String>]) -> Option<String> { + for tag in tags { + if tag.first().map(|value| value.as_str()) == Some("d") { + if let Some(value) = tag.get(1) { + return Some(value.clone()); + } + } + } + None +} + +fn build_signer_map( + signers: &[RadrootsClientTangleNostrSyncSigner], +) -> BTreeMap<String, String> { + let mut map = BTreeMap::new(); + for signer in signers { + let secret_key = match RadrootsNostrSecretKey::from_str(&signer.secret_key) { + Ok(secret_key) => secret_key, + Err(_) => continue, + }; + let keys = RadrootsNostrKeys::new(secret_key); + map.insert(keys.public_key().to_hex(), signer.secret_key.clone()); + } + map +} + +fn sign_draft_event( + draft: &RadrootsTangleEventDraft, + secret_key: &str, +) -> RadrootsClientTangleResult<RadrootsNostrEvent> { + let secret_key = RadrootsNostrSecretKey::from_str(secret_key) + .map_err(|_| RadrootsClientTangleError::CryptoUnavailable)?; + let keys = RadrootsNostrKeys::new(secret_key); + let builder = radroots_nostr_build_event(draft.kind, draft.content.clone(), draft.tags.clone()) + .map_err(|_| RadrootsClientTangleError::CryptoUnavailable)?; + builder + .sign_with_keys(&keys) + .map_err(|_| RadrootsClientTangleError::CryptoUnavailable) +} + +fn sha256_hex(bytes: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(bytes); + hex::encode(hasher.finalize()) +} + +#[cfg(target_arch = "wasm32")] +fn export_timestamp() -> String { + js_sys::Date::new_0().to_iso_string().into() +} + +#[cfg(not(target_arch = "wasm32"))] +fn export_timestamp() -> String { + chrono::Utc::now().to_rfc3339() +} + +#[cfg(test)] +mod tests { + use super::{ + RadrootsClientWebTangle, + DEFAULT_TANGLE_STORE_KEY, + }; + use crate::tangle::{ + RadrootsClientTangle, + RadrootsClientTangleError, + RadrootsClientTangleNostrSyncOptions, + RadrootsClientTangleNostrSyncSigner, + }; + + #[test] + fn default_store_key_is_set() { + let tangle = RadrootsClientWebTangle::new(None); + assert_eq!(tangle.get_store_key(), DEFAULT_TANGLE_STORE_KEY); + } + + #[test] + fn nostr_sync_requires_relays() { + let tangle = RadrootsClientWebTangle::new(None); + let opts = RadrootsClientTangleNostrSyncOptions { + relays: Vec::new(), + signers: vec![RadrootsClientTangleNostrSyncSigner { + secret_key: "deadbeef".to_string(), + }], + publish_timeout_ms: None, + }; + let err = futures::executor::block_on(tangle.nostr_sync_all(opts)) + .expect_err("invalid response"); + assert_eq!(err, RadrootsClientTangleError::InvalidResponse); + } +}