Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
387 changes: 370 additions & 17 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ rand = "0.8.5"
rand_chacha = "0.3.1"
aes-gcm = "0.10.3"
argon2 = "0.5.3"
hkdf = "0.12"
sha2 = "0.10"
nostr-sdk = { version = "0.44.1", features = ["nip44"] }

# Async
tokio = "1.39.2"
Expand Down
7 changes: 1 addition & 6 deletions crates/sage-api/endpoints.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
{
"login": true,
"logout": true,
"resync": true,
"generate_mnemonic": false,
"import_key": true,
"delete_key": false,
"delete_database": false,
"rename_key": false,
"set_wallet_emoji": false,
"get_key": false,
"get_secret_key": false,
"get_keys": false,
"get_sync_status": true,
"get_wallet_receive_address": true,
"get_version": false,
"get_database_stats": true,
"perform_database_maintenance": true,
Expand Down Expand Up @@ -84,12 +81,10 @@
"set_discover_peers": true,
"set_target_peers": true,
"set_network": true,
"set_network_override": true,
"get_networks": false,
"get_network": false,
"set_delta_sync": false,
"set_delta_sync_override": false,
"set_change_address": true,
"resync_cat": true,
"update_cat": true,
"update_did": true,
Expand Down
26 changes: 26 additions & 0 deletions crates/sage-api/src/requests/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,32 @@ pub struct GetVersionResponse {
pub version: String,
}

/// Get the current receive address for any wallet by fingerprint
#[cfg_attr(
feature = "openapi",
crate::openapi_attr(
tag = "System & Sync",
description = "Get the receive address for a wallet without switching the active session."
)
)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "tauri", derive(specta::Type))]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GetWalletReceiveAddress {
/// Wallet fingerprint
pub fingerprint: u32,
}

/// Response with the wallet receive address
#[cfg_attr(feature = "openapi", crate::openapi_attr(tag = "System & Sync"))]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "tauri", derive(specta::Type))]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GetWalletReceiveAddressResponse {
/// Encoded receive address
pub address: String,
}

/// Check if specific coins are spendable
#[cfg_attr(
feature = "openapi",
Expand Down
23 changes: 23 additions & 0 deletions crates/sage-config/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::SyncConfig;
use serde::{Deserialize, Serialize};
use specta::Type;

Expand All @@ -8,6 +9,7 @@ pub struct Config {
pub global: GlobalConfig,
pub network: NetworkConfig,
pub rpc: RpcConfig,
pub sync: SyncConfig,
}

impl Default for Config {
Expand All @@ -17,6 +19,7 @@ impl Default for Config {
global: GlobalConfig::default(),
network: NetworkConfig::default(),
rpc: RpcConfig::default(),
sync: SyncConfig::default(),
}
}
}
Expand Down Expand Up @@ -70,3 +73,23 @@ impl Default for RpcConfig {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn sync_config_in_config_defaults_correctly() {
let cfg = Config::default();
assert_eq!(cfg.sync.relays.len(), 3);
}

#[test]
fn config_toml_roundtrip_preserves_sync() {
let mut cfg = Config::default();
cfg.sync.relays = vec!["wss://custom.relay".to_string()];
let toml = toml::to_string_pretty(&cfg).unwrap();
let parsed: Config = toml::from_str(&toml).unwrap();
assert_eq!(parsed.sync.relays, vec!["wss://custom.relay"]);
}
}
2 changes: 2 additions & 0 deletions crates/sage-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
mod config;
mod network;
mod old;
mod sync;
mod wallet;

pub use config::*;
pub use network::*;
pub use old::*;
pub use sync::*;
pub use wallet::*;
2 changes: 2 additions & 0 deletions crates/sage-config/src/old.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ pub fn migrate_config(old: OldConfig) -> Result<(Config, WalletConfig), ParseInt
enabled: old.rpc.run_on_startup,
port: old.rpc.server_port,
},
sync: crate::SyncConfig::default(),
};

let mut wallet_config = WalletConfig {
Expand All @@ -155,6 +156,7 @@ pub fn migrate_config(old: OldConfig) -> Result<(Config, WalletConfig), ParseInt
delta_sync: None,
emoji: None,
change_address: None,
sync_enabled: false,
});
}

Expand Down
55 changes: 55 additions & 0 deletions crates/sage-config/src/sync.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use serde::{Deserialize, Serialize};
use specta::Type;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Type)]
#[serde(default)]
pub struct SyncConfig {
pub relays: Vec<String>,
}

impl Default for SyncConfig {
fn default() -> Self {
Self {
relays: vec![
"wss://relay.damus.io".to_string(),
"wss://relay.nostr.band".to_string(),
"wss://nos.lol".to_string(),
],
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn default_relays_are_populated() {
let cfg = SyncConfig::default();
assert_eq!(
cfg.relays,
vec![
"wss://relay.damus.io",
"wss://relay.nostr.band",
"wss://nos.lol",
]
);
}

#[test]
fn toml_roundtrip_preserves_relays() {
let original = SyncConfig {
relays: vec!["wss://relay.example.com".to_string()],
};
let toml = toml::to_string_pretty(&original).unwrap();
let parsed: SyncConfig = toml::from_str(&toml).unwrap();
assert_eq!(parsed.relays, original.relays);
}

#[test]
fn empty_relay_list_deserializes() {
let toml = r#"relays = []"#;
let cfg: SyncConfig = toml::from_str(toml).unwrap();
assert!(cfg.relays.is_empty());
}
}
12 changes: 10 additions & 2 deletions crates/sage-config/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ pub struct Wallet {
pub emoji: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub change_address: Option<String>,
#[serde(default)]
pub sync_enabled: bool,
}

impl Wallet {
Expand All @@ -48,6 +50,7 @@ impl Default for Wallet {
delta_sync: None,
emoji: None,
change_address: None,
sync_enabled: false,
}
}
}
Expand All @@ -68,6 +71,7 @@ mod tests {
change_address: Some(
"xch1dtfukqqka3ftqtdlhmc5spc5vd44h7ejrtnjcewxlueam5yrnnqqyczg8t".to_string(),
),
sync_enabled: false,
}
}

Expand Down Expand Up @@ -95,6 +99,7 @@ mod tests {
name = "Main"
fingerprint = 1000000
change_address = "xch1dtfukqqka3ftqtdlhmc5spc5vd44h7ejrtnjcewxlueam5yrnnqqyczg8t"
sync_enabled = false
"#]],
&expect![[r#"
{
Expand All @@ -106,7 +111,8 @@ mod tests {
"name": "Main",
"fingerprint": 1000000,
"delta_sync": null,
"change_address": "xch1dtfukqqka3ftqtdlhmc5spc5vd44h7ejrtnjcewxlueam5yrnnqqyczg8t"
"change_address": "xch1dtfukqqka3ftqtdlhmc5spc5vd44h7ejrtnjcewxlueam5yrnnqqyczg8t",
"sync_enabled": false
}
]
}"#]],
Expand All @@ -126,6 +132,7 @@ mod tests {
name = "Main"
fingerprint = 1000000
change_address = "xch1dtfukqqka3ftqtdlhmc5spc5vd44h7ejrtnjcewxlueam5yrnnqqyczg8t"
sync_enabled = false
"#]],
&expect![[r#"
{
Expand All @@ -137,7 +144,8 @@ mod tests {
"name": "Main",
"fingerprint": 1000000,
"delta_sync": null,
"change_address": "xch1dtfukqqka3ftqtdlhmc5spc5vd44h7ejrtnjcewxlueam5yrnnqqyczg8t"
"change_address": "xch1dtfukqqka3ftqtdlhmc5spc5vd44h7ejrtnjcewxlueam5yrnnqqyczg8t",
"sync_enabled": false
}
]
}"#]],
Expand Down
19 changes: 18 additions & 1 deletion crates/sage-rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,23 @@ pub async fn start_rpc(sage: Arc<Mutex<Sage>>) -> Result<()> {
Ok(())
}

async fn set_network_override(
State(state): State<AppState>,
Json(req): Json<sage_api::SetNetworkOverride>,
) -> Response {
handle(state.sage.lock().await.set_network_override(req).await)
}

async fn set_change_address(
State(state): State<AppState>,
Json(req): Json<sage_api::SetChangeAddress>,
) -> Response {
handle(state.sage.lock().await.set_change_address(req).await)
}

pub fn make_router(sage: Arc<Mutex<Sage>>) -> Router {
api_router().with_state(AppState { sage })
api_router()
.route("/set_network_override", post(set_network_override))
.route("/set_change_address", post(set_change_address))
.with_state(AppState { sage })
}
9 changes: 9 additions & 0 deletions crates/sage-rpc/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ impl TestApp {
self.consume_until(|event| matches!(event, SyncEvent::PuzzleBatchSynced))
.await;
}

async fn login(&self, body: sage_api::Login) -> Result<sage_api::LoginResponse> {
self.call_rpc("/login", body).await
}

#[allow(unused)]
async fn logout(&self, body: sage_api::Logout) -> Result<sage_api::LogoutResponse> {
self.call_rpc("/logout", body).await
}
}

impl_endpoints! {
Expand Down
30 changes: 25 additions & 5 deletions crates/sage/src/endpoints/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ use sage_api::{
GetPendingTransactions, GetPendingTransactionsResponse, GetSpendableCoinCount,
GetSpendableCoinCountResponse, GetSyncStatus, GetSyncStatusResponse, GetToken,
GetTokenResponse, GetTransaction, GetTransactionResponse, GetTransactions,
GetTransactionsResponse, GetVersion, GetVersionResponse, IsAssetOwned, IsAssetOwnedResponse,
NftCollectionRecord, NftData, NftRecord, NftSortMode as ApiNftSortMode, NftSpecialUseType,
OptionRecord, OptionSortMode as ApiOptionSortMode, PendingTransactionRecord,
PerformDatabaseMaintenance, PerformDatabaseMaintenanceResponse, TokenRecord,
TransactionCoinRecord, TransactionRecord,
GetTransactionsResponse, GetVersion, GetVersionResponse, GetWalletReceiveAddress,
GetWalletReceiveAddressResponse, IsAssetOwned, IsAssetOwnedResponse, NftCollectionRecord,
NftData, NftRecord, NftSortMode as ApiNftSortMode, NftSpecialUseType, OptionRecord,
OptionSortMode as ApiOptionSortMode, PendingTransactionRecord, PerformDatabaseMaintenance,
PerformDatabaseMaintenanceResponse, TokenRecord, TransactionCoinRecord, TransactionRecord,
};
use sage_database::{
AssetFilter, CoinFilterMode, CoinSortMode, NftGroupSearch, NftRow, NftSortMode, OptionSortMode,
Expand Down Expand Up @@ -120,6 +120,26 @@ impl Sage {
})
}

pub async fn get_wallet_receive_address(
&self,
req: GetWalletReceiveAddress,
) -> Result<GetWalletReceiveAddressResponse> {
let pool = self.connect_to_database(req.fingerprint).await?;
let db = sage_database::Database::new(pool);

let Some(max_idx) = db.max_derivation_index(false).await? else {
return Err(Error::NotLoggedIn);
};

let (derivations, _) = db.derivations(false, 1, max_idx).await?;
let Some(row) = derivations.first() else {
return Err(Error::NotLoggedIn);
};

let address = Address::new(row.p2_puzzle_hash, self.network().prefix()).encode()?;
Ok(GetWalletReceiveAddressResponse { address })
}

pub async fn check_address(&self, req: CheckAddress) -> Result<CheckAddressResponse> {
let wallet = self.wallet()?;

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"spoiled": "^0.4.0",
"tailwind-merge": "^2.6.1",
"tailwindcss-animate": "^1.0.7",
"tauri-plugin-nostr-sync-api": "file:../tauri-plugin-nostr",
"tauri-plugin-safe-area-insets": "^0.1.0",
"tauri-plugin-sage": "file:tauri-plugin-sage",
"theme-o-rama": "0.2.0",
Expand Down
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading