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

Hermes address change #1130

Merged
merged 3 commits into from
Apr 12, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 190 additions & 22 deletions mutiny-core/src/hermes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ use futures::{pin_mut, select, FutureExt};
use lightning::util::logger::Logger;
use lightning::{log_error, log_info, log_warn};
use lightning_invoice::Bolt11Invoice;
use nostr::prelude::decrypt_received_private_zap_message;
use nostr::secp256k1::SecretKey;
use nostr::{nips::nip04::decrypt, Event, JsonUtil, Keys, Tag, ToBech32};
use nostr::{prelude::decrypt_received_private_zap_message, EventBuilder};
use nostr::{Filter, Kind, Timestamp};
use nostr_sdk::{Client, RelayPoolNotification};
use reqwest::Method;
Expand All @@ -44,6 +44,10 @@ const HERMES_SERVICE_ID: u32 = 1;
const HERMES_FREE_PLAN_ID: u32 = 1;
const HERMES_PAID_PLAN_ID: u32 = 2;

const REGISTRATION_CHECK_EVENT_KIND: Kind = Kind::Custom(93_186);
const NEW_FEDERATION_EVENT_KIND: Kind = Kind::Custom(93_187);
const DISABLE_ZAPS_EVENT_KIND: Kind = Kind::Custom(93_188);

#[derive(Deserialize, Serialize)]
pub struct RegisterRequest {
pub name: Option<String>,
Expand Down Expand Up @@ -132,10 +136,10 @@ impl<S: MutinyStorage> HermesClient<S> {
/// Starts the hermes background checker
/// This should only error if there's an initial unrecoverable error
/// Otherwise it will loop in the background until a stop signal
pub fn start(&self, profile_key: Option<Keys>) -> Result<(), MutinyError> {
pub async fn start(&self, profile_key: Option<Keys>) -> Result<(), MutinyError> {
// if we haven't synced before, use now and save to storage
let last_sync_time = self.storage.get_dm_sync_time(true)?;
let mut time_stamp = match last_sync_time {
let time_stamp = match last_sync_time {
None => {
let now = Timestamp::from(0);
self.storage.set_dm_sync_time(now.as_u64(), true)?;
Expand All @@ -144,35 +148,92 @@ impl<S: MutinyStorage> HermesClient<S> {
Some(time) => Timestamp::from(time + 1), // add one so we get only new events
};

// if we have a time stamp before the bug, reset to 0
if time_stamp.as_u64() < 1712862315 {
time_stamp = Timestamp::from(0);
}

// check to see if we currently have an address
let logger_check_clone = self.logger.clone();
let stop_check_clone = self.stop.clone();
let http_client_check_clone = self.http_client.clone();
let public_key_check_clone = self.public_key;
let nostr_client_check_clone = self.client.clone();
let base_url_check_clone = self.base_url.clone();
let current_address_check_clone = self.current_address.clone();
let first_federation = self.get_first_federation().await.clone();
utils::spawn(async move {
loop {
if stop_check_clone.load(Ordering::Relaxed) {
break;
};

match check_hermes_name_for_pubkey(
match check_hermes_registration_info(
&http_client_check_clone,
&base_url_check_clone,
public_key_check_clone,
nostr_client_check_clone.clone(),
)
.await
{
Ok(o) => {
let mut c = current_address_check_clone.write().await;
log_info!(logger_check_clone, "checked lightning address: {o:?}");
*c = (o, true);
{
let mut c = current_address_check_clone.write().await;
log_info!(
logger_check_clone,
"checked lightning address: {:?}",
o.name
);
*c = (o.name.clone(), true);
}

// check that federation is still the same
if let Some(f) = first_federation {
TonyGiorgio marked this conversation as resolved.
Show resolved Hide resolved
// if a registered federation exists and is what we have
// then there is no reason to update
if o.federation_id.is_some()
&& f.federation_id == o.federation_id.unwrap()
{
break;
}

// user has a federation registered but is different than current
// so we should update it
match change_federation_info(
&http_client_check_clone,
&base_url_check_clone,
nostr_client_check_clone,
current_address_check_clone,
f,
&logger_check_clone,
)
.await
{
Ok(_) => (),
Err(e) => {
log_error!(
logger_check_clone,
"could not change the federation: {e}"
);
}
}
} else {
// handle the case where the user no longer has a federation
// if user is already disabled, no need to call again
if !o.disabled_zaps {
match disable_zaps(
&http_client_check_clone,
&base_url_check_clone,
nostr_client_check_clone,
current_address_check_clone,
&logger_check_clone,
)
.await
{
Ok(_) => (),
Err(e) => {
log_error!(
logger_check_clone,
"could not disable zaps: {e}"
);
}
}
}
}

break;
}
Err(e) => {
Expand Down Expand Up @@ -262,6 +323,32 @@ impl<S: MutinyStorage> HermesClient<S> {
Ok(())
}

pub async fn change_federation_info(
&self,
federation: FederationIdentity,
) -> Result<(), MutinyError> {
change_federation_info(
&self.http_client,
&self.base_url,
self.client.clone(),
self.current_address.clone(),
federation,
&self.logger,
)
.await
}

pub async fn disable_zaps(&self) -> Result<(), MutinyError> {
disable_zaps(
&self.http_client,
&self.base_url,
self.client.clone(),
self.current_address.clone(),
&self.logger,
)
.await
}

pub async fn check_available_name(&self, name: String) -> Result<bool, MutinyError> {
check_name_request(&self.http_client, &self.base_url, name).await
}
Expand Down Expand Up @@ -338,8 +425,77 @@ impl<S: MutinyStorage> HermesClient<S> {
None => None,
}
}
}

#[derive(Serialize, Deserialize, Clone)]
pub struct ChangeFederationRequest {
pub name: Option<String>,
pub federation_id: Option<FederationId>,
}

async fn change_federation_info(
http_client: &reqwest::Client,
base_url: &str,
nostr_client: Client,
current_address: Arc<RwLock<(Option<String>, bool)>>,
federation: FederationIdentity,
logger: &MutinyLogger,
) -> Result<(), MutinyError> {
// make sure name is registered already
if current_address.read().await.0.is_none() {
return Ok(());
}
Comment on lines +444 to +447
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and the one for disable_zaps has a minor race condition if the user manages to remove/switch federation before the check call has finished. Could do rechecking until .1 is true


// create nostr event
let signer = nostr_client.signer().await?;
let event_builder = EventBuilder::new(
NEW_FEDERATION_EVENT_KIND,
federation.invite_code.to_string(),
[],
);
let event = signer.sign_event_builder(event_builder).await?;

// send request
let url = Url::parse(&format!("{}/v1/change-federation", base_url))
.map_err(|_| MutinyError::ConnectionFailed)?;
let request = http_client.request(Method::POST, url).json(&event);
let _ = utils::fetch_with_timeout(http_client, request.build().expect("should build req"))
.await
.map_err(|_| MutinyError::ConnectionFailed)?;

log_info!(logger, "changed federation info to current federation");

Ok(())
}

async fn disable_zaps(
http_client: &reqwest::Client,
base_url: &str,
nostr_client: Client,
current_address: Arc<RwLock<(Option<String>, bool)>>,
logger: &MutinyLogger,
) -> Result<(), MutinyError> {
// make sure name is registered already
if current_address.read().await.0.is_none() {
return Ok(());
}

// create nostr event
let signer = nostr_client.signer().await?;
let event_builder = EventBuilder::new(DISABLE_ZAPS_EVENT_KIND, "", []);
let event = signer.sign_event_builder(event_builder).await?;

// send request
let url = Url::parse(&format!("{}/v1/disable-zaps", base_url))
.map_err(|_| MutinyError::ConnectionFailed)?;
let request = http_client.request(Method::POST, url).json(&event);
let _ = utils::fetch_with_timeout(http_client, request.build().expect("should build req"))
.await
.map_err(|_| MutinyError::ConnectionFailed)?;

log_info!(logger, "disabled zaps for the user");

// TODO need a way to change the federation if the user's federation changes
Ok(())
}

fn find_hermes_token(
Expand All @@ -352,18 +508,30 @@ fn find_hermes_token(
.find(|token| token.service_id == service_id && token.plan_id == plan_id)
}

async fn check_hermes_name_for_pubkey(
#[derive(Serialize, Deserialize, Clone)]
pub struct RegistrationInfo {
pub name: Option<String>,
pub federation_id: Option<FederationId>,
pub disabled_zaps: bool,
}

async fn check_hermes_registration_info(
http_client: &reqwest::Client,
base_url: &str,
pubkey: nostr::PublicKey,
) -> Result<Option<String>, MutinyError> {
let url = Url::parse(&format!("{}/v1/check-pubkey/{pubkey}", base_url,))
nostr_client: Client,
) -> Result<RegistrationInfo, MutinyError> {
// create nostr event
let signer = nostr_client.signer().await?;
let event_builder = EventBuilder::new(REGISTRATION_CHECK_EVENT_KIND, "", []);
let event = signer.sign_event_builder(event_builder).await?;

// send request
let url = Url::parse(&format!("{}/v1/check-registration", base_url))
.map_err(|_| MutinyError::ConnectionFailed)?;
let request = http_client.request(Method::GET, url);

let request = http_client.request(Method::POST, url).json(&event);
let res = utils::fetch_with_timeout(http_client, request.build().expect("should build req"))
.await?
.json::<Option<String>>()
.json::<RegistrationInfo>()
.await
.map_err(|_| MutinyError::ConnectionFailed)?;

Expand Down
37 changes: 32 additions & 5 deletions mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,7 @@ impl<S: MutinyStorage> MutinyWalletBuilder<S> {
#[cfg(target_arch = "wasm32")]
NostrSigner::NIP07(_) => None,
};
mw.start_hermes(profile_key)?;
mw.start_hermes(profile_key).await?;

log_info!(
mw.logger,
Expand Down Expand Up @@ -2399,6 +2399,7 @@ impl<S: MutinyStorage> MutinyWallet<S> {
self.logger.clone(),
self.federation_storage.clone(),
self.federations.clone(),
self.hermes_client.clone(),
federation_code,
)
.await
Expand Down Expand Up @@ -2451,6 +2452,16 @@ impl<S: MutinyStorage> MutinyWallet<S> {
return Err(MutinyError::NotFound);
}

// remove the federation from hermes
if let Some(h) = self.hermes_client.as_ref() {
match h.disable_zaps().await {
Ok(_) => (),
Err(e) => {
log_error!(self.logger, "could not disable hermes zaps: {e}")
}
}
}

Ok(())
}

Expand Down Expand Up @@ -2856,9 +2867,9 @@ impl<S: MutinyStorage> MutinyWallet<S> {
}

/// Starts up the hermes client if available
pub fn start_hermes(&self, profile_key: Option<Keys>) -> Result<(), MutinyError> {
pub async fn start_hermes(&self, profile_key: Option<Keys>) -> Result<(), MutinyError> {
if let Some(hermes_client) = self.hermes_client.as_ref() {
hermes_client.start(profile_key)?
hermes_client.start(profile_key).await?
}
Ok(())
}
Expand Down Expand Up @@ -2969,6 +2980,7 @@ pub(crate) async fn create_new_federation<S: MutinyStorage>(
logger: Arc<MutinyLogger>,
federation_storage: Arc<RwLock<FederationStorage>>,
federations: Arc<RwLock<HashMap<FederationId, Arc<FederationClient<S>>>>>,
hermes_client: Option<Arc<HermesClient<S>>>,
federation_code: InviteCode,
) -> Result<FederationIdentity, MutinyError> {
// Begin with a mutex lock so that nothing else can
Expand Down Expand Up @@ -3021,15 +3033,30 @@ pub(crate) async fn create_new_federation<S: MutinyStorage>(
.await
.insert(federation_id, Arc::new(new_federation));

Ok(FederationIdentity {
let new_federation_identity = FederationIdentity {
uuid: next_federation_uuid.clone(),
federation_id,
invite_code: federation_code,
federation_name,
federation_expiry_timestamp,
welcome_message,
gateway_fees,
})
};

// change the federation with hermes, if available
if let Some(h) = hermes_client {
match h
.change_federation_info(new_federation_identity.clone())
.await
{
Ok(_) => (),
Err(e) => {
log_error!(logger, "could not change hermes federation: {e}")
}
}
}

Ok(new_federation_identity)
}

#[derive(Deserialize)]
Expand Down
Loading