Skip to content

Commit

Permalink
Allow changing nostr keys on the fly
Browse files Browse the repository at this point in the history
  • Loading branch information
benthecarman committed Mar 29, 2024
1 parent 35f8f69 commit 63e38f8
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 12 deletions.
75 changes: 64 additions & 11 deletions mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ use ::nostr::nips::nip57;
#[cfg(target_arch = "wasm32")]
use ::nostr::prelude::rand::rngs::OsRng;
use ::nostr::prelude::ZapRequestData;
use ::nostr::{EventBuilder, EventId, JsonUtil, Kind};
#[cfg(target_arch = "wasm32")]
use ::nostr::{Keys, Tag};
use ::nostr::Tag;
use ::nostr::{EventBuilder, EventId, JsonUtil, Keys, Kind};
use async_lock::RwLock;
use bdk_chain::ConfirmationTime;
use bip39::Mnemonic;
Expand Down Expand Up @@ -2058,6 +2058,26 @@ impl<S: MutinyStorage> MutinyWallet<S> {
Err(MutinyError::NostrError)
}

/// Change our active nostr keys to the given keys
pub async fn change_nostr_keys(
&self,
keys: Option<Keys>,
#[cfg(target_arch = "wasm32")] extension_pk: Option<::nostr::PublicKey>,
) -> Result<::nostr::PublicKey, MutinyError> {
#[cfg(target_arch = "wasm32")]
let source = utils::build_nostr_key_source(keys, extension_pk)?;

#[cfg(not(target_arch = "wasm32"))]
let source = utils::build_nostr_key_source(keys)?;

let new_pk = self.nostr.change_nostr_keys(source, self.xprivkey).await?;

// re-sync nostr profile data
self.sync_nostr().await?;

Ok(new_pk)
}

/// Syncs all of our nostr data from the configured primal instance
pub async fn sync_nostr(&self) -> Result<(), MutinyError> {
let npub = self.nostr.get_npub().await;
Expand Down Expand Up @@ -3153,7 +3173,7 @@ mod tests {
use crate::labels::{Contact, LabelStorage};
use crate::nostr::NostrKeySource;
use crate::utils::{now, parse_npub, sleep};
use nostr::Keys;
use nostr::{Keys, Metadata};
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};

wasm_bindgen_test_configure!(run_in_browser);
Expand Down Expand Up @@ -3435,10 +3455,6 @@ mod tests {

assert_eq!(messages.len(), 5);

for x in &messages {
log!("{}", x.message);
}

// get next messages
let limit = 2;
let util = messages.iter().min_by_key(|m| m.date).unwrap().date - 1;
Expand All @@ -3447,10 +3463,6 @@ mod tests {
.await
.unwrap();

for x in next.iter() {
log!("{}", x.message);
}

// check that we got different messages
assert_eq!(next.len(), 2);
assert!(next.iter().all(|m| !messages.contains(m)));
Expand All @@ -3465,6 +3477,47 @@ mod tests {
assert!(future_msgs.is_empty());
}

#[test]
async fn test_change_nostr_keys() {
// create fresh wallet
let mnemonic = generate_seed(12).unwrap();
let network = Network::Regtest;
let xpriv = ExtendedPrivKey::new_master(network, &mnemonic.to_seed("")).unwrap();
let storage = MemoryStorage::new(None, None, None);
let config = MutinyWalletConfigBuilder::new(xpriv)
.with_network(network)
.build();
let mw = MutinyWalletBuilder::new(xpriv, storage.clone())
.with_config(config)
.build()
.await
.expect("mutiny wallet should initialize");

let first_npub = mw.nostr.get_npub().await;
let first_profile = mw.nostr.get_profile().unwrap();
let first_follows = mw.nostr.get_follow_list().unwrap();
assert_eq!(first_profile, Metadata::default());
assert!(first_profile.name.is_none());
assert!(first_follows.is_empty());

// change signer, can just use npub for test
let ben =
parse_npub("npub1u8lnhlw5usp3t9vmpz60ejpyt649z33hu82wc2hpv6m5xdqmuxhs46turz").unwrap();
mw.change_nostr_keys(Some(Keys::from_public_key(ben)), None)
.await
.unwrap();

// check that we have all new data
let npub = mw.nostr.get_npub().await;
let profile = mw.nostr.get_profile().unwrap();
let follows = mw.nostr.get_follow_list().unwrap();
assert_ne!(npub, first_npub);
assert_ne!(profile, first_profile);
assert_ne!(follows, first_follows);
assert!(!follows.is_empty());
assert!(profile.name.is_some());
}

#[test]
fn test_max_routing_fee_amount() {
max_routing_fee_amount();
Expand Down
33 changes: 33 additions & 0 deletions mutiny-core/src/nostr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,39 @@ impl<S: MutinyStorage> NostrManager<S> {
}
}

/// Change our active nostr keys to the given keys
pub(crate) async fn change_nostr_keys(
&self,
key_source: NostrKeySource,
xprivkey: ExtendedPrivKey,
) -> Result<nostr::PublicKey, MutinyError> {
// see if we can build new nostr keys first
let new_nostr_keys = NostrKeys::from_key_source(key_source, xprivkey)?;
let new_pk = new_nostr_keys.public_key;

// get lock on signer
let mut nostr_keys = self.nostr_keys.write().await;

// change our client's signer
self.client
.set_signer(Some(new_nostr_keys.signer.clone()))
.await;

// change our signer
*nostr_keys = new_nostr_keys;
drop(nostr_keys);

// delete our old nostr caches in storage
self.storage.delete_nostr_caches()?;

// update filters
let dm_filter = self.get_dm_filter().await?;
let profile = self.get_contacts_list_filter().await?;
self.client.subscribe(vec![dm_filter, profile], None).await;

Ok(new_pk)
}

pub fn get_relays(&self) -> Vec<String> {
let mut relays: Vec<String> = self
.nwc
Expand Down
8 changes: 8 additions & 0 deletions mutiny-core/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,14 @@ pub trait MutinyStorage: Clone + Sized + Send + Sync + 'static {
self.set_data(NOSTR_PROFILE_METADATA.to_string(), metadata, None)
}

fn delete_nostr_caches(&self) -> Result<(), MutinyError> {
self.delete(&[
NOSTR_PROFILE_METADATA,
LAST_DM_SYNC_TIME_KEY,
NOSTR_CONTACT_LIST,
])
}

fn get_device_id(&self) -> Result<String, MutinyError> {
match self.get_data(DEVICE_ID_KEY)? {
Some(id) => Ok(id),
Expand Down
23 changes: 22 additions & 1 deletion mutiny-core/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::error::MutinyError;
use crate::nostr::NostrKeySource;
use bitcoin::key::XOnlyPublicKey;
use bitcoin::Network;
use core::cell::{RefCell, RefMut};
Expand All @@ -14,7 +15,7 @@ use lightning::util::ser::Writeable;
use lightning::util::ser::Writer;
use lightning_invoice::Bolt11Invoice;
use nostr::nips::nip05;
use nostr::{Event, Filter, FromBech32, JsonUtil, Kind, Metadata};
use nostr::{Event, Filter, FromBech32, JsonUtil, Keys, Kind, Metadata};
use reqwest::Client;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -97,6 +98,26 @@ pub fn get_random_bip32_child_index() -> u32 {
random_value % (max_value + 1)
}

pub(crate) fn build_nostr_key_source(
keys: Option<Keys>,
#[cfg(target_arch = "wasm32")] extension_pk: Option<::nostr::PublicKey>,
) -> Result<NostrKeySource, MutinyError> {
#[cfg(target_arch = "wasm32")]
if keys.is_some() && extension_pk.is_some() {
return Err(MutinyError::InvalidArgumentsError);
}

#[cfg(target_arch = "wasm32")]
if let Some(pk) = extension_pk {
return Ok(NostrKeySource::Extension(pk));
}

match keys {
None => Ok(NostrKeySource::Derived),
Some(keys) => Ok(NostrKeySource::Imported(keys)),
}
}

pub type LockResult<Guard> = Result<Guard, ()>;

pub struct Mutex<T: ?Sized> {
Expand Down
24 changes: 24 additions & 0 deletions mutiny-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,30 @@ impl MutinyWallet {
.map(|s| s.to_bech32().expect("bech32"))
}

/// Change our active nostr keys to the given nsec
#[wasm_bindgen]
pub async fn change_nostr_keys(
&self,
nsec: Option<String>,
extension_pk: Option<String>,
) -> Result<String, MutinyJsError> {
let nsec = nsec
.map(|n| Keys::parse(n).map_err(|_| MutinyJsError::InvalidArgumentsError))
.transpose()?;

let extension_pk = extension_pk.map(|p| parse_npub(&p)).transpose()?;

if nsec.is_some() && extension_pk.is_some() {
return Err(MutinyJsError::InvalidArgumentsError);
}

Ok(self
.inner
.change_nostr_keys(nsec, extension_pk)
.await
.map(|pk| pk.to_bech32().expect("bech32"))?)
}

/// Returns the network of the wallet.
#[wasm_bindgen]
pub fn get_network(&self) -> String {
Expand Down

0 comments on commit 63e38f8

Please sign in to comment.