From 6f0e3c9f909e1061f25740c59c926b607a50adfc Mon Sep 17 00:00:00 2001 From: benthecarman Date: Thu, 25 Jan 2024 22:07:20 +0000 Subject: [PATCH] Create nostr profile --- mutiny-core/src/lib.rs | 41 +++++++++++++++++++++++++++- mutiny-core/src/nostr/mod.rs | 53 ++++++++++++++++++++++++++++++++++-- mutiny-core/src/storage.rs | 10 +++++++ mutiny-wasm/src/lib.rs | 46 ++++++++++++++++++++++++++++++- 4 files changed, 146 insertions(+), 4 deletions(-) diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 7434b26c9..fce31805b 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -69,7 +69,7 @@ use crate::{nostr::NostrManager, utils::sleep}; use ::nostr::key::XOnlyPublicKey; use ::nostr::nips::nip57; use ::nostr::prelude::ZapRequestData; -use ::nostr::{Event, EventId, JsonUtil, Kind}; +use ::nostr::{Event, EventId, JsonUtil, Kind, Metadata}; use async_lock::RwLock; use bdk_chain::ConfirmationTime; use bip39::Mnemonic; @@ -80,6 +80,7 @@ use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::Network; use fedimint_core::{api::InviteCode, config::FederationId}; use futures::{pin_mut, select, FutureExt}; +use futures_util::join; use lightning::ln::PaymentHash; use lightning::{log_debug, util::logger::Logger}; use lightning::{log_error, log_info, log_warn}; @@ -1401,6 +1402,44 @@ impl MutinyWallet { .map_err(|_| MutinyError::NostrError) } + /// Syncs all of our nostr data from the configured primal instance + pub async fn sync_nostr(&self) -> Result<(), MutinyError> { + let contacts_fut = self.sync_nostr_contacts(self.nostr.public_key); + let profile_fut = self.sync_nostr_profile(); + + // join futures and handle result + let (contacts_res, profile_res) = join!(contacts_fut, profile_fut); + contacts_res?; + profile_res?; + + Ok(()) + } + + /// Fetches our latest nostr profile from primal and saves to storage + async fn sync_nostr_profile(&self) -> Result<(), MutinyError> { + let url = self + .config + .primal_url + .as_deref() + .unwrap_or("https://primal-cache.mutinywallet.com/api"); + let client = reqwest::Client::new(); + + let body = json!(["user_profile", { "pubkey": self.nostr.public_key } ]); + let data: Vec = Self::primal_request(&client, url, body).await?; + + if let Some(json) = data.first().cloned() { + let event: Event = serde_json::from_value(json)?; + if event.kind != Kind::Metadata { + return Ok(()); + } + + let metadata: Metadata = serde_json::from_str(&event.content)?; + self.storage.set_nostr_profile(metadata)?; + } + + Ok(()) + } + /// Get contacts from the given npub and sync them to the wallet pub async fn sync_nostr_contacts(&self, npub: XOnlyPublicKey) -> Result<(), MutinyError> { let url = self diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index 18da25869..31d88a81b 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -19,12 +19,15 @@ use bitcoin::{ use futures::{pin_mut, select, FutureExt}; use futures_util::lock::Mutex; use lightning::util::logger::Logger; -use lightning::{log_debug, log_error, log_warn}; +use lightning::{log_debug, log_error, log_info, log_warn}; use lightning_invoice::Bolt11Invoice; +use lnurl::lnurl::LnUrl; use nostr::key::{SecretKey, XOnlyPublicKey}; use nostr::nips::nip47::*; use nostr::prelude::{decrypt, encrypt}; -use nostr::{Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, Tag, Timestamp}; +use nostr::{ + url::Url, Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, Metadata, Tag, Timestamp, +}; use nostr_sdk::{Client, ClientSigner, RelayPoolNotification}; use std::collections::HashSet; use std::sync::{atomic::Ordering, Arc, RwLock}; @@ -188,6 +191,52 @@ impl NostrManager { Ok(nwc) } + /// Sets the user's nostr profile metadata + pub async fn edit_profile( + &self, + name: Option, + img_url: Option, + lnurl: Option, + nip05: Option, + ) -> Result { + let current = self.get_profile()?; + + let with_name = if let Some(name) = name { + current.name(name) + } else { + current + }; + let with_img = if let Some(img_url) = img_url { + with_name.picture(img_url) + } else { + with_name + }; + let with_lnurl = if let Some(lnurl) = lnurl { + if let Some(ln_addr) = lnurl.lightning_address() { + with_img.lud16(ln_addr.to_string()) + } else { + with_img.lud06(lnurl.to_string()) + } + } else { + with_img + }; + let with_nip05 = if let Some(nip05) = nip05 { + with_lnurl.nip05(nip05) + } else { + with_lnurl + }; + + let event_id = self.client.set_metadata(&with_nip05).await?; + log_info!(self.logger, "New kind 0: {event_id}"); + self.storage.set_nostr_profile(with_nip05.clone())?; + + Ok(with_nip05) + } + + pub fn get_profile(&self) -> Result { + Ok(self.storage.get_nostr_profile()?.unwrap_or_default()) + } + pub fn get_nwc_uri(&self, index: u32) -> Result, MutinyError> { let opt = self .nwc diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 1ad82bf82..787ff401b 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -19,6 +19,7 @@ use bitcoin::hashes::Hash; use hex::FromHex; use lightning::{ln::PaymentHash, util::logger::Logger}; use lightning::{log_error, log_trace}; +use nostr::Metadata; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; @@ -40,6 +41,7 @@ pub(crate) const EXPECTED_NETWORK_KEY: &str = "network"; const PAYMENT_INBOUND_PREFIX_KEY: &str = "payment_inbound/"; const PAYMENT_OUTBOUND_PREFIX_KEY: &str = "payment_outbound/"; pub const LAST_DM_SYNC_TIME_KEY: &str = "last_dm_sync_time"; +pub const NOSTR_PROFILE_METADATA: &str = "nostr_profile_metadata"; fn needs_encryption(key: &str) -> bool { match key { @@ -439,6 +441,14 @@ pub trait MutinyStorage: Clone + Sized + Send + Sync + 'static { } } + fn get_nostr_profile(&self) -> Result, MutinyError> { + self.get_data(NOSTR_PROFILE_METADATA) + } + + fn set_nostr_profile(&self, metadata: Metadata) -> Result<(), MutinyError> { + self.set_data(NOSTR_PROFILE_METADATA.to_string(), metadata, None) + } + fn get_device_id(&self) -> Result { match self.get_data(DEVICE_ID_KEY)? { Some(id) => Ok(id), diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 06658b511..4945b9750 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -419,7 +419,7 @@ impl MutinyWallet { self.mnemonic.to_string() } - /// Returns the npub for receiving dms + /// Returns the user's npub #[wasm_bindgen] pub fn get_npub(&self) -> String { self.inner.nostr.public_key.to_bech32().expect("bech32") @@ -1597,6 +1597,50 @@ impl MutinyWallet { Ok(()) } + /// Returns the user's nostr profile data + #[wasm_bindgen] + pub fn get_nostr_profile(&self) -> Result { + let profile = self.inner.nostr.get_profile()?; + Ok(JsValue::from_serde(&profile)?) + } + + /// Returns the user's nostr profile data + #[wasm_bindgen] + pub async fn edit_nostr_profile( + &self, + name: Option, + img_url: Option, + lnurl: Option, + nip05: Option, + ) -> Result { + let img_url = img_url + .map(|i| nostr::url::Url::from_str(&i)) + .transpose() + .map_err(|_| MutinyJsError::InvalidArgumentsError)?; + + let lnurl = lnurl + .map(|l| { + LightningAddress::from_str(&l) + .map(|a| a.lnurl()) + .or(LnUrl::from_str(&l)) + }) + .transpose() + .map_err(|_| MutinyJsError::InvalidArgumentsError)?; + + let profile = self + .inner + .nostr + .edit_profile(name, img_url, lnurl, nip05) + .await?; + Ok(JsValue::from_serde(&profile)?) + } + + /// Syncs all of our nostr data from the configured primal instance + pub async fn sync_nostr(&self) -> Result<(), MutinyJsError> { + self.inner.sync_nostr().await?; + Ok(()) + } + /// Get contacts from the given npub and sync them to the wallet pub async fn sync_nostr_contacts(&self, npub_str: String) -> Result<(), MutinyJsError> { let npub = parse_npub_or_nip05(&npub_str).await?;