Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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

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

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

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

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

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

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

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

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

3 changes: 3 additions & 0 deletions crates/sage-api/endpoints.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"resync": true,
"generate_mnemonic": false,
"import_wallet": true,
"import_watch_wallet": true,
"add_watch_addresses": true,
"remove_watch_addresses": true,
"delete_wallet": false,
"delete_database": false,
"rename_wallet": false,
Expand Down
98 changes: 98 additions & 0 deletions crates/sage-api/src/requests/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -638,3 +638,101 @@ pub struct DeleteUserTheme {
#[cfg_attr(feature = "tauri", derive(specta::Type))]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct DeleteUserThemeResponse {}

/// Import a watch-only wallet from addresses
#[cfg_attr(
feature = "openapi",
crate::openapi_attr(
tag = "Authentication & Keys",
description = "Import a read-only wallet that watches one or more addresses. Their prefix isn't checked, since the wallet's network decides how they're shown, so addresses for another network can be imported before switching the wallet to it. Its balances, assets and history are synced, but it can't create or sign transactions, since the puzzle reveals of its coins are unknown. Optionally logs in automatically."
)
)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "tauri", derive(specta::Type))]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ImportWatchWallet {
/// Display name for the wallet
pub name: String,
/// Addresses to watch, on any network. The first one is the receive address
pub addresses: Vec<String>,
/// Whether to automatically login after import
#[serde(default = "yes")]
#[cfg_attr(feature = "openapi", schema(default = true))]
pub login: bool,
/// Optional emoji identifier
#[serde(default)]
#[cfg_attr(feature = "openapi", schema(nullable = true))]
pub emoji: Option<String>,
}

/// Response with the imported watch wallet's fingerprint
#[cfg_attr(
feature = "openapi",
crate::openapi_attr(tag = "Authentication & Keys")
)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "tauri", derive(specta::Type))]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ImportWatchWalletResponse {
/// Fingerprint assigned to the wallet. It's derived from its addresses, since it has no key
#[cfg_attr(feature = "openapi", schema(example = 1_234_567_890))]
pub fingerprint: u32,
}

/// Watch more addresses in a watch wallet
#[cfg_attr(
feature = "openapi",
crate::openapi_attr(
tag = "Authentication & Keys",
description = "Add addresses to a watch wallet. If it's the active wallet, they're synced right away, without a full resync. Addresses it already watches are ignored."
)
)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "tauri", derive(specta::Type))]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct AddWatchAddresses {
/// Fingerprint of the watch wallet
#[cfg_attr(feature = "openapi", schema(example = 1_234_567_890))]
pub fingerprint: u32,
/// Addresses to watch, on any network
pub addresses: Vec<String>,
}

/// Response for adding watched addresses
#[cfg_attr(
feature = "openapi",
crate::openapi_attr(tag = "Authentication & Keys")
)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "tauri", derive(specta::Type))]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct AddWatchAddressesResponse {}

/// Stop watching addresses in a watch wallet
#[cfg_attr(
feature = "openapi",
crate::openapi_attr(
tag = "Authentication & Keys",
description = "Remove addresses from a watch wallet, along with the coins synced for them. A watch wallet must keep at least one address."
)
)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "tauri", derive(specta::Type))]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct RemoveWatchAddresses {
/// Fingerprint of the watch wallet
#[cfg_attr(feature = "openapi", schema(example = 1_234_567_890))]
pub fingerprint: u32,
/// Addresses to stop watching
pub addresses: Vec<String>,
}

/// Response for removing watched addresses
#[cfg_attr(
feature = "openapi",
crate::openapi_attr(tag = "Authentication & Keys")
)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "tauri", derive(specta::Type))]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct RemoveWatchAddressesResponse {}
7 changes: 6 additions & 1 deletion crates/sage-api/src/types/wallet_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ pub struct WalletInfo {
}

/// The kind of wallet, which determines how its addresses are found and whether it can sign
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "tauri", derive(specta::Type))]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum WalletKind {
/// A BLS key, whose addresses are derived from its master public key. The key is in the
/// keychain under the wallet's fingerprint
Bls,
/// A read-only list of addresses, whose puzzle reveals are unknown
Watch {
/// The addresses being watched. The first one is the receive address
addresses: Vec<String>,
},
}
8 changes: 8 additions & 0 deletions crates/sage-apps/src/bridge/bridge_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,14 @@ async fn process_shared(
}
}

if method.requires_signing() && app_state.lock().await.is_watch_only() {
return Ok(RustBridgeInvokeResult::error(
&request.id,
"watch_only",
"Watch-only wallets can't create or sign transactions",
));
}

match method
.prepare_approval(
BridgeContext { app },
Expand Down
6 changes: 6 additions & 0 deletions crates/sage-apps/src/bridge/methods/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ pub(crate) trait BridgeMethod: Send + Sync {
false
}

/// Whether the method signs with the wallet's keys. Watch wallets have none, so these are
/// refused before the user is asked to approve them.
fn requires_signing(&self) -> bool {
false
}

/// Override only when approved execution differs from the normal request path.
async fn handle_approved(
&self,
Expand Down
4 changes: 4 additions & 0 deletions crates/sage-apps/src/bridge/methods/user/wallet/send_xch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ impl BridgeMethod for WalletSendXch {
true
}

fn requires_signing(&self) -> bool {
true
}

async fn handle_approved(
&self,
approval: &RustBridgeApprovalRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ impl BridgeMethod for WalletSignCoinSpends {
true
}

fn requires_signing(&self) -> bool {
true
}

async fn prepare_approval(
&self,
_ctx: BridgeContext<'_>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ impl BridgeMethod for WalletSignMessage {
true
}

fn requires_signing(&self) -> bool {
true
}

fn approval_request(
&self,
_ctx: BridgeContext<'_>,
Expand Down
5 changes: 4 additions & 1 deletion crates/sage-cli/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use anyhow::{Context, Result, anyhow, bail};
use clap::{Args, Subcommand};
use sage_api::{
Amount, GetCats, GetSyncStatus, GetToken, GetWallet, HandleConfirmation, RecipientError,
RecipientErrorKind, SendCat, SendXch, SignCoinSpends, TransactionResponse, Unit,
RecipientErrorKind, SendCat, SendXch, SignCoinSpends, TransactionResponse, Unit, WalletKind,
};
use sage_client::{Client, SageRpcError};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -116,6 +116,9 @@ async fn status(client: &Client, json: bool) -> Result<()> {
};

println!("Wallet: {} ({})", wallet.name, wallet.fingerprint);
if matches!(wallet.kind, WalletKind::Watch { .. }) {
println!("Kind: watch-only");
}
println!("Network: {}", wallet.network_id);
println!(
"Balance: {} {} spendable",
Expand Down
Loading
Loading