diff --git a/Cargo.lock b/Cargo.lock index f26d86558..3396193ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2188,6 +2188,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniscript" version = "9.0.2" @@ -2266,7 +2276,7 @@ dependencies = [ [[package]] name = "mutiny-core" -version = "0.5.9" +version = "0.6.0-rc1" dependencies = [ "aes", "aes-gcm", @@ -2331,10 +2341,11 @@ dependencies = [ [[package]] name = "mutiny-wasm" -version = "0.5.9" +version = "0.6.0-rc1" dependencies = [ "anyhow", "async-trait", + "base64 0.13.1", "bip39", "bitcoin 0.30.2", "console_error_panic_hook", @@ -2938,6 +2949,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -3824,6 +3836,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" diff --git a/mutiny-core/Cargo.toml b/mutiny-core/Cargo.toml index 7cdbb30c5..c3fc3317c 100644 --- a/mutiny-core/Cargo.toml +++ b/mutiny-core/Cargo.toml @@ -2,7 +2,7 @@ cargo-features = ["per-package-target"] [package] name = "mutiny-core" -version = "0.5.9" +version = "0.6.0-rc1" edition = "2021" authors = ["Tony Giorgio ", "benthecarman "] description = "The core SDK for the mutiny node" @@ -35,7 +35,7 @@ lightning-transaction-sync = { version = "0.0.121", default-features = false, fe lightning-liquidity = { git = "https://github.com/lightningdevkit/lightning-liquidity.git", rev = "478ccf9324e2650d200ea289a0ba8905afe420b6" } chrono = "0.4.22" futures-util = { version = "0.3", default-features = false } -reqwest = { version = "0.11", default-features = false, features = ["json"] } +reqwest = { version = "0.11", default-features = false, features = ["multipart", "json"] } async-trait = "0.1.68" url = { version = "2.3.1", features = ["serde"] } nostr = { version = "0.27.0", default-features = false, features = ["nip04", "nip05", "nip47", "nip57"] } diff --git a/mutiny-core/src/labels.rs b/mutiny-core/src/labels.rs index da5db7faa..eff6ceb11 100644 --- a/mutiny-core/src/labels.rs +++ b/mutiny-core/src/labels.rs @@ -155,6 +155,20 @@ pub trait LabelStorage { } Ok(None) } + /// Finds a contact that has the given npub + fn get_contact_for_npub( + &self, + npub: XOnlyPublicKey, + ) -> Result, MutinyError> { + // todo this is not efficient, we should have a map of npub to contact + let contacts = self.get_contacts()?; + for (id, contact) in contacts { + if contact.npub == Some(npub) { + return Ok(Some((id, contact))); + } + } + Ok(None) + } } impl LabelStorage for S { diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 82a750954..d74a6c22d 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -75,7 +75,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; @@ -85,6 +85,7 @@ use bitcoin::secp256k1::PublicKey; use bitcoin::{hashes::sha256, Network}; use fedimint_core::{api::InviteCode, config::FederationId}; use futures::{pin_mut, select, FutureExt}; +use futures_util::join; use hex_conservative::{DisplayHex, FromHex}; #[cfg(target_arch = "wasm32")] use instant::Instant; @@ -94,6 +95,7 @@ use lightning::{log_debug, log_error, log_info, log_trace, log_warn}; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; use lnurl::{lnurl::LnUrl, AsyncClient as LnUrlClient, LnUrlResponse, Response}; use nostr_sdk::{Client, RelayPoolNotification}; +use reqwest::multipart::{Form, Part}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::sync::Arc; @@ -1612,7 +1614,7 @@ impl MutinyWallet { if profile.tag != NwcProfileTag::Subscription { let mut nwc = profile.clone(); nwc.tag = NwcProfileTag::Subscription; - self.nostr.edit_profile(nwc)?; + self.nostr.edit_nwc_profile(nwc)?; } } } @@ -1637,6 +1639,42 @@ impl MutinyWallet { Ok(()) } + /// Uploads a profile pic to nostr.build and returns the uploaded file's URL + pub async fn upload_profile_pic(&self, image_bytes: Vec) -> Result { + let client = reqwest::Client::new(); + + let form = Form::new().part("fileToUpload", Part::bytes(image_bytes)); + let res: NostrBuildResult = client + .post("https://nostr.build/api/v2/upload/profile") + .multipart(form) + .send() + .await + .map_err(|_| MutinyError::NostrError)? + .json() + .await + .map_err(|_| MutinyError::NostrError)?; + + if res.status != "success" { + log_error!( + self.logger, + "Error uploading profile picture: {}", + res.message + ); + return Err(MutinyError::NostrError); + } + + // get url from response body + if let Some(value) = res.data.first() { + return value + .get("url") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .ok_or(MutinyError::NostrError); + } + + Err(MutinyError::NostrError) + } + /// Makes a request to the primal api async fn primal_request( client: &reqwest::Client, @@ -1655,6 +1693,45 @@ 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).map_err(|_| MutinyError::NostrError)?; + if event.kind != Kind::Metadata { + return Ok(()); + } + + let metadata: Metadata = + serde_json::from_str(&event.content).map_err(|_| MutinyError::NostrError)?; + 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 @@ -2342,6 +2419,13 @@ pub(crate) async fn create_new_federation( }) } +#[derive(Deserialize)] +struct NostrBuildResult { + status: String, + message: String, + data: Vec, +} + // max amount that can be spent through a gateway fn max_spendable_amount(current_balance_sat: u64, routing_fees: &GatewayFees) -> Option { let current_balance_msat = current_balance_sat as f64 * 1_000.0; @@ -2823,7 +2907,16 @@ mod tests { // check that we got different messages assert_eq!(next.len(), 2); - assert!(next.iter().all(|m| !messages.contains(m))) + assert!(next.iter().all(|m| !messages.contains(m))); + + // test check for future messages, should be empty + let since = messages.iter().max_by_key(|m| m.date).unwrap().date + 1; + let future_msgs = mw + .get_dm_conversation(npub, limit, None, Some(since)) + .await + .unwrap(); + + assert!(future_msgs.is_empty()); } #[test] diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index 04b0034d5..a78259e3d 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -16,12 +16,15 @@ use bitcoin::{hashes::hex::FromHex, secp256k1::ThirtyTwoByteHash}; 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}; @@ -186,6 +189,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 @@ -294,7 +343,7 @@ impl NostrManager { Ok(()) } - pub fn edit_profile(&self, profile: NwcProfile) -> Result { + pub fn edit_nwc_profile(&self, profile: NwcProfile) -> Result { let mut profiles = self.nwc.write().unwrap(); let index = profile.index; @@ -363,7 +412,7 @@ impl NostrManager { Ok(nwc_profile) } - pub fn get_profile(&self, index: u32) -> Result { + pub fn get_nwc_profile(&self, index: u32) -> Result { let profiles = self.nwc.read().unwrap(); let nwc = profiles @@ -456,7 +505,7 @@ impl NostrManager { } /// Creates a new NWC profile and saves to storage - pub(crate) fn create_new_profile( + pub(crate) fn create_new_nwc_profile_internal( &self, profile_type: ProfileType, spending_conditions: SpendingConditions, @@ -504,7 +553,8 @@ impl NostrManager { spending_conditions: SpendingConditions, tag: NwcProfileTag, ) -> Result { - let profile = self.create_new_profile(profile_type, spending_conditions, tag)?; + let profile = + self.create_new_nwc_profile_internal(profile_type, spending_conditions, tag)?; // add relay if needed let needs_connect = self.client.add_relay(profile.relay.as_str()).await?; if needs_connect { @@ -1409,7 +1459,7 @@ mod test { let name = "test".to_string(); let profile = nostr_manager - .create_new_profile( + .create_new_nwc_profile_internal( ProfileType::Normal { name: name.clone() }, SpendingConditions::default(), Default::default(), @@ -1452,7 +1502,7 @@ mod test { let name = MUTINY_PLUS_SUBSCRIPTION_LABEL.to_string(); let profile = nostr_manager - .create_new_profile( + .create_new_nwc_profile_internal( ProfileType::Reserved(ReservedProfile::MutinySubscription), SpendingConditions::default(), Default::default(), @@ -1485,7 +1535,7 @@ mod test { let name = "test".to_string(); let profile = nostr_manager - .create_new_profile( + .create_new_nwc_profile_internal( ProfileType::Normal { name: name.clone() }, SpendingConditions::default(), Default::default(), @@ -1617,7 +1667,7 @@ mod test { let name = "test".to_string(); let mut profile = nostr_manager - .create_new_profile( + .create_new_nwc_profile_internal( ProfileType::Normal { name: name.clone() }, SpendingConditions::default(), Default::default(), @@ -1630,7 +1680,7 @@ mod test { profile.relay = "wss://relay.damus.io".to_string(); - nostr_manager.edit_profile(profile).unwrap(); + nostr_manager.edit_nwc_profile(profile).unwrap(); let profiles = nostr_manager.profiles(); assert_eq!(profiles.len(), 1); @@ -1658,7 +1708,7 @@ mod test { let name = "test".to_string(); let profile = nostr_manager - .create_new_profile( + .create_new_nwc_profile_internal( ProfileType::Normal { name: name.clone() }, SpendingConditions::default(), Default::default(), @@ -1690,7 +1740,7 @@ mod test { let name = "test".to_string(); let profile = nostr_manager - .create_new_profile( + .create_new_nwc_profile_internal( ProfileType::Normal { name }, SpendingConditions::default(), Default::default(), diff --git a/mutiny-core/src/nostr/nwc.rs b/mutiny-core/src/nostr/nwc.rs index 9dc60c5fd..186aeab62 100644 --- a/mutiny-core/src/nostr/nwc.rs +++ b/mutiny-core/src/nostr/nwc.rs @@ -1236,7 +1236,7 @@ mod wasm_test { .unwrap(); let profile = nostr_manager - .create_new_profile( + .create_new_nwc_profile_internal( ProfileType::Normal { name: "test".to_string(), }, @@ -1293,7 +1293,7 @@ mod wasm_test { .unwrap(); let profile = nostr_manager - .create_new_profile( + .create_new_nwc_profile_internal( ProfileType::Normal { name: "test".to_string(), }, @@ -1541,7 +1541,7 @@ mod wasm_test { let budget = 10_000; let profile = nostr_manager - .create_new_profile( + .create_new_nwc_profile_internal( ProfileType::Normal { name: "test".to_string(), }, @@ -1628,7 +1628,7 @@ mod wasm_test { let budget = 10_000; let profile = nostr_manager - .create_new_profile( + .create_new_nwc_profile_internal( ProfileType::Normal { name: "test".to_string(), }, diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 21b2e0970..1a126e86d 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -19,6 +19,7 @@ use futures_util::lock::Mutex; use hex_conservative::*; 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; @@ -41,6 +42,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"; const DELAYED_WRITE_MS: i32 = 50; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -518,6 +520,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/Cargo.toml b/mutiny-wasm/Cargo.toml index a2e9c754c..92f4492bd 100644 --- a/mutiny-wasm/Cargo.toml +++ b/mutiny-wasm/Cargo.toml @@ -2,7 +2,7 @@ cargo-features = ["per-package-target"] [package] name = "mutiny-wasm" -version = "0.5.9" +version = "0.6.0-rc1" edition = "2021" authors = ["Tony Giorgio ", "benthecarman "] forced-target = "wasm32-unknown-unknown" @@ -24,6 +24,7 @@ wasm-bindgen = "0.2.88" wasm-bindgen-futures = "0.4.38" serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0" } +base64 = "0.13.1" bitcoin = { version = "0.30.2", default-features = false, features = ["std", "serde", "secp-recovery", "rand"] } lightning = { version = "0.0.121", default-features = false, features = ["std"] } lightning-invoice = { version = "0.29.0" } diff --git a/mutiny-wasm/src/error.rs b/mutiny-wasm/src/error.rs index 77ac7e99f..bc02daf28 100644 --- a/mutiny-wasm/src/error.rs +++ b/mutiny-wasm/src/error.rs @@ -236,6 +236,12 @@ impl From for MutinyJsError { } } +impl From for MutinyJsError { + fn from(_e: base64::DecodeError) -> Self { + Self::InvalidArgumentsError + } +} + impl From for MutinyJsError { fn from(_e: bip39::Error) -> Self { Self::InvalidMnemonic diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index ab409022b..4b827cc53 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -438,7 +438,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") @@ -1222,7 +1222,7 @@ impl MutinyWallet { .set_invoice_labels(invoice, labels)?) } - pub fn get_contacts(&self) -> Result*/, MutinyJsError> { + pub async fn get_contacts(&self) -> Result*/, MutinyJsError> { Ok(JsValue::from_serde( &self .inner @@ -1234,7 +1234,7 @@ impl MutinyWallet { )?) } - pub fn get_contacts_sorted(&self) -> Result*/, MutinyJsError> { + pub async fn get_contacts_sorted(&self) -> Result*/, MutinyJsError> { let mut contacts: Vec = self .inner .node_manager @@ -1331,6 +1331,19 @@ impl MutinyWallet { Ok(self.inner.node_manager.edit_contact(id, contact)?) } + pub async fn get_contact_for_npub( + &self, + npub: String, + ) -> Result, MutinyJsError> { + let npub = parse_npub(&npub)?; + let contact = self.inner.node_manager.get_contact_for_npub(npub)?; + + match contact { + Some((id, c)) => Ok(Some((id, c).into())), + None => Ok(None), + } + } + pub fn get_tag_items(&self) -> Result, MutinyJsError> { let mut tags: Vec = self .inner @@ -1498,15 +1511,15 @@ impl MutinyWallet { &self, profile_index: u32, ) -> Result { - let mut profile = self.inner.nostr.get_profile(profile_index)?; + let mut profile = self.inner.nostr.get_nwc_profile(profile_index)?; profile.spending_conditions = SpendingConditions::RequireApproval; - Ok(self.inner.nostr.edit_profile(profile)?.into()) + Ok(self.inner.nostr.edit_nwc_profile(profile)?.into()) } /// Finds a nostr wallet connect profile by index #[wasm_bindgen] pub async fn get_nwc_profile(&self, index: u32) -> Result { - Ok(self.inner.nostr.get_profile(index)?.into()) + Ok(self.inner.nostr.get_nwc_profile(index)?.into()) } /// Create a single use nostr wallet connect profile @@ -1640,6 +1653,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)?) + } + + /// Sets 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?; @@ -1673,6 +1730,12 @@ impl MutinyWallet { Ok(event_id.to_hex()) } + /// Uploads a profile pic to nostr.build and returns the uploaded file's URL + pub async fn upload_profile_pic(&self, img_base64: String) -> Result { + let bytes = base64::decode(&img_base64)?; + Ok(self.inner.upload_profile_pic(bytes).await?) + } + /// Resets the scorer and network graph. This can be useful if you get stuck in a bad state. #[wasm_bindgen] pub async fn reset_router(&self) -> Result<(), MutinyJsError> { diff --git a/mutiny-wasm/src/models.rs b/mutiny-wasm/src/models.rs index e9ebb93fc..21e1f0bb8 100644 --- a/mutiny-wasm/src/models.rs +++ b/mutiny-wasm/src/models.rs @@ -120,6 +120,7 @@ pub struct MutinyInvoice { payee_pubkey: Option, pub amount_sats: Option, pub expire: u64, + pub expired: bool, status: String, pub fees_paid: Option, pub inbound: bool, @@ -182,6 +183,7 @@ impl From for MutinyInvoice { Some(ref b) => utils::is_hodl_invoice(b), None => false, }; + let now = utils::now().as_secs(); MutinyInvoice { bolt11: m.bolt11, description: m.description, @@ -190,6 +192,7 @@ impl From for MutinyInvoice { payee_pubkey: m.payee_pubkey.map(|p| p.serialize().to_lower_hex_string()), amount_sats: m.amount_sats, expire: m.expire, + expired: m.expire < now, status: m.status.to_string(), fees_paid: m.fees_paid, inbound: m.inbound, @@ -696,6 +699,8 @@ pub struct TagItem { lnurl: Option, #[serde(skip_serializing_if = "Option::is_none")] image_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + primal_image_url: Option, /// Epoch time in seconds when this tag was last used pub last_used_time: u64, } @@ -754,6 +759,11 @@ impl TagItem { pub fn image_url(&self) -> Option { self.image_url.clone() } + + #[wasm_bindgen(getter)] + pub fn primal_image_url(&self) -> Option { + self.primal_image_url.clone() + } } impl From<(String, MutinyContact)> for TagItem { @@ -765,6 +775,12 @@ impl From<(String, MutinyContact)> for TagItem { npub: contact.npub.map(|n| n.to_bech32().expect("bech32")), ln_address: contact.ln_address, lnurl: contact.lnurl, + primal_image_url: contact.image_url.as_ref().map(|i| { + format!( + "https://primal.b-cdn.net/media-cache?s=s&a=1&u={}", + urlencoding::encode(i) + ) + }), image_url: contact.image_url, last_used_time: contact.last_used, } @@ -782,6 +798,7 @@ impl From for TagItem { ln_address: None, lnurl: None, image_url: None, + primal_image_url: None, last_used_time: item.last_used_time, }, labels::TagItem::Contact(contact) => contact.into(),