Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: watch-only wallet #1045

Merged
merged 6 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 5 additions & 9 deletions sn_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,13 @@ async fn main() -> Result<()> {
Some(bootstrap_peers)
};

let joins_gossip = false;
// use gossipsub only for the wallet cmd that requires it.
let joins_gossipsub = matches!(opt.cmd, SubCmd::Wallet(WalletCmds::ReceiveOnline { .. }));

let client = Client::new(
secret_key,
bootstrap_peers,
joins_gossip,
joins_gossipsub,
opt.connection_timeout,
)
.await?;
Expand All @@ -116,13 +118,7 @@ async fn main() -> Result<()> {
wallet_cmds(cmds, &client, &client_data_dir_path, should_verify_store).await?
}
SubCmd::Files(cmds) => {
files_cmds(
cmds,
client.clone(),
&client_data_dir_path,
should_verify_store,
)
.await?
files_cmds(cmds, &client, &client_data_dir_path, should_verify_store).await?
}
SubCmd::Register(cmds) => {
register_cmds(cmds, &client, &client_data_dir_path, should_verify_store).await?
Expand Down
6 changes: 3 additions & 3 deletions sn_cli/src/subcommands/files/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ pub enum FilesCmds {

pub(crate) async fn files_cmds(
cmds: FilesCmds,
client: Client,
client: &Client,
root_dir: &Path,
verify_store: bool,
) -> Result<()> {
Expand Down Expand Up @@ -112,7 +112,7 @@ pub(crate) async fn files_cmds(
);
}

let file_api: Files = Files::new(client, root_dir.to_path_buf());
let file_api: Files = Files::new(client.clone(), root_dir.to_path_buf());

match (file_name, file_addr) {
(Some(name), Some(address)) => {
Expand Down Expand Up @@ -146,7 +146,7 @@ pub(crate) async fn files_cmds(
/// verify if the data was stored successfully.
async fn upload_files(
files_path: PathBuf,
client: Client,
client: &Client,
root_dir: &Path,
verify_store: bool,
batch_size: usize,
Expand Down
30 changes: 14 additions & 16 deletions sn_cli/src/subcommands/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use color_eyre::{eyre::eyre, Result};
use sn_client::{Client, ClientEvent, Error as ClientError};
use sn_transfers::{
CashNoteRedemption, Error as TransferError, LocalWallet, MainPubkey, MainSecretKey, NanoTokens,
SpendAddress, Transfer, UniquePubkey, WalletError,
SpendAddress, Transfer, UniquePubkey, WalletError, WatchOnlyWallet,
};
use std::{
io::Read,
Expand Down Expand Up @@ -96,14 +96,14 @@ pub enum WalletCmds {
},
/// Listen for transfer notifications from the network over gossipsub protocol.
///
/// Transfers will be deposited to a local wallet.
/// Transfers will be deposited to a local (watch-only) wallet.
///
/// Only cash notes owned by the public key corresponding to the provided SK will be accepted,
/// verified to be valid against the network, and deposited onto a locally stored wallet.
/// Only cash notes owned by the provided public key will be accepted, verified to be valid
/// against the network, and deposited onto a locally stored watch-only wallet.
ReceiveOnline {
/// Hex-encoded main secret key
#[clap(name = "sk")]
sk: String,
/// Hex-encoded main public key
#[clap(name = "pk")]
pk: String,
/// Optional path where to store the wallet
#[clap(name = "path")]
path: Option<PathBuf>,
Expand Down Expand Up @@ -170,9 +170,9 @@ pub(crate) async fn wallet_cmds(
WalletCmds::Send { amount, to } => send(amount, to, client, root_dir, verify_store).await,
WalletCmds::Receive { file, transfer } => receive(transfer, file, client, root_dir).await,
WalletCmds::GetFaucet { url } => get_faucet(root_dir, client, url.clone()).await,
WalletCmds::ReceiveOnline { sk, path } => {
WalletCmds::ReceiveOnline { pk, path } => {
let wallet_dir = path.unwrap_or(root_dir.join(DEFAULT_RECEIVE_ONLINE_WALLET_DIR));
listen_notifs_and_deposit(&wallet_dir, client, sk).await
listen_notifs_and_deposit(&wallet_dir, client, pk).await
}
WalletCmds::Verify {
spend_address,
Expand Down Expand Up @@ -388,17 +388,15 @@ async fn receive(transfer: String, is_file: bool, client: &Client, root_dir: &Pa
Ok(())
}

async fn listen_notifs_and_deposit(root_dir: &Path, client: &Client, sk: String) -> Result<()> {
let mut wallet = match SecretKey::from_hex(&sk) {
Ok(sk) => {
let pk_hex = sk.public_key().to_hex();
let main_sk = MainSecretKey::new(sk);
async fn listen_notifs_and_deposit(root_dir: &Path, client: &Client, pk_hex: String) -> Result<()> {
let mut wallet = match MainPubkey::from_hex(&pk_hex) {
Ok(main_pk) => {
let folder_name = format!("pk_{}_{}", &pk_hex[..6], &pk_hex[pk_hex.len() - 6..]);
let wallet_dir = root_dir.join(folder_name);
println!("Loading local wallet from: {}", wallet_dir.display());
LocalWallet::load_from_path(&wallet_dir, Some(main_sk))?
WatchOnlyWallet::load_from(&wallet_dir, main_pk)?
}
Err(err) => return Err(eyre!("Failed to parse hex-encoded SK: {err:?}")),
Err(err) => return Err(eyre!("Failed to parse hex-encoded public key: {err:?}")),
};

let main_pk = wallet.address();
Expand Down
19 changes: 9 additions & 10 deletions sn_node_rpc_client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use sn_protocol::safenode_proto::{
TransferNotifsFilterRequest,
};
use sn_protocol::storage::SpendAddress;
use sn_transfers::{LocalWallet, MainPubkey, MainSecretKey};
use sn_transfers::{MainPubkey, WatchOnlyWallet};
use std::{fs, net::SocketAddr, path::PathBuf, time::Duration};
use tokio_stream::StreamExt;
use tonic::Request;
Expand Down Expand Up @@ -219,19 +219,19 @@ pub async fn transfers_events(
log_cash_notes: Option<PathBuf>,
bootstrap_peers: Option<Vec<Multiaddr>>,
) -> Result<()> {
let (client, mut wallet, pk) = match SecretKey::from_hex(&sk) {
Ok(sk) => {
let pk = sk.public_key();
let client = Client::new(sk.clone(), bootstrap_peers, true, None).await?;
let main_sk = MainSecretKey::new(sk);
let (client, mut wallet) = match MainPubkey::from_hex(&sk) {
Ok(main_pubkey) => {
let client = Client::new(SecretKey::random(), bootstrap_peers, true, None).await?;
let wallet_dir = TempDir::new()?;
let wallet = LocalWallet::load_from_main_key(&wallet_dir, main_sk)?;
(client, wallet, pk)
let wallet = WatchOnlyWallet::load_from(&wallet_dir, main_pubkey)?;
(client, wallet)
}
Err(err) => return Err(eyre!("Failed to parse hex-encoded SK: {err:?}")),
Err(err) => return Err(eyre!("Failed to parse hex-encoded PK: {err:?}")),
};
let endpoint = format!("https://{addr}");
let mut node_client = SafeNodeClient::connect(endpoint).await?;
let main_pk = wallet.address();
let pk = main_pk.public_key();
let _ = node_client
.transfer_notifs_filter(Request::new(TransferNotifsFilterRequest {
pk: pk.to_bytes().to_vec(),
Expand All @@ -257,7 +257,6 @@ pub async fn transfers_events(
println!();

let mut stream = response.into_inner();
let main_pk = MainPubkey(pk);
while let Some(Ok(e)) = stream.next().await {
let cash_notes = match NodeEvent::from_bytes(&e.event) {
Ok(NodeEvent::TransferNotif {
Expand Down
10 changes: 10 additions & 0 deletions sn_transfers/src/cashnotes/cashnote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@ impl CashNote {
Ok(main_key.derive_key(&self.derivation_index()))
}

/// Return UniquePubkey using MainPubkey supplied by caller.
/// Will return an error if the supplied MainPubkey does not match the
/// CashNote MainPubkey.
pub fn derived_pubkey(&self, main_pubkey: &MainPubkey) -> Result<UniquePubkey> {
if main_pubkey != self.main_pubkey() {
return Err(Error::MainPubkeyMismatch);
}
Ok(main_pubkey.new_unique_pubkey(&self.derivation_index()))
}

/// Return the derivation index that was used to derive UniquePubkey and corresponding DerivedSecretKey of a CashNote.
pub fn derivation_index(&self) -> DerivationIndex {
self.derivation_index
Expand Down
2 changes: 2 additions & 0 deletions sn_transfers/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ pub enum Error {
UniquePubkeyNotFound,
#[error("Main key does not match public address.")]
MainSecretKeyDoesNotMatchMainPubkey,
#[error("Main pub key does not match.")]
MainPubkeyMismatch,
#[error("Could not deserialize specified hex string to a CashNote: {0}")]
HexDeserializationFailed(String),
#[error("Could not serialize CashNote to hex: {0}")]
Expand Down
1 change: 1 addition & 0 deletions sn_transfers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub use transfers::create_offline_transfer;
pub use wallet::bls_secret_from_hex;
pub use wallet::{
Error as WalletError, LocalWallet, Payment, PaymentQuote, Result as WalletResult,
WatchOnlyWallet,
};

// re-export crates used in our public API
Expand Down
3 changes: 3 additions & 0 deletions sn_transfers/src/wallet/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ pub enum Error {
/// Failed to parse bytes into a bls key
#[error("Unconfirmed transactions still persist even after retries")]
UnconfirmedTxAfterRetries,
/// Main pub key doesn't match the key found when loading wallet from path
#[error("Main pub key doesn't match the key found when loading wallet from path: {0:#?}")]
PubKeyMismatch(std::path::PathBuf),
/// Failed to parse bytes into a bls key
#[error("Failed to parse bls key")]
FailedToParseBlsKey,
Expand Down
23 changes: 22 additions & 1 deletion sn_transfers/src/wallet/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

use super::error::{Error, Result};

use crate::MainSecretKey;
use crate::{MainPubkey, MainSecretKey};

use hex::{decode, encode};
use std::path::Path;
Expand Down Expand Up @@ -41,6 +41,27 @@ pub(super) fn get_main_key(wallet_dir: &Path) -> Result<Option<MainSecretKey>> {
Ok(Some(MainSecretKey::new(secret)))
}

/// Writes the public address (hex-encoded) to disk.
pub(crate) fn store_new_pubkey(wallet_dir: &Path, main_pubkey: &MainPubkey) -> Result<()> {
let public_key_path = wallet_dir.join(MAIN_PUBKEY_FILENAME);
std::fs::write(public_key_path, encode(main_pubkey.to_bytes()))
.map_err(|e| Error::FailedToHexEncodeKey(e.to_string()))?;
Ok(())
}

/// Returns Some(sn_transfers::MainPubkey) or None if file doesn't exist. It assumes it's hex-encoded.
pub(super) fn get_main_pubkey(wallet_dir: &Path) -> Result<Option<MainPubkey>> {
let path = wallet_dir.join(MAIN_PUBKEY_FILENAME);
if !path.is_file() {
return Ok(None);
}

let pk_hex_bytes = std::fs::read(&path)?;
let main_pk = MainPubkey::from_hex(pk_hex_bytes)?;

Ok(Some(main_pk))
}

/// Construct a BLS secret key from a hex-encoded string.
pub fn bls_secret_from_hex<T: AsRef<[u8]>>(hex: T) -> Result<bls::SecretKey> {
let bytes = decode(hex).map_err(|_| Error::FailedToDecodeHexToKey)?;
Expand Down