Skip to content

Commit

Permalink
Merge pull request #992 from MutinyWallet/edit-profile
Browse files Browse the repository at this point in the history
Create nostr profile
  • Loading branch information
TonyGiorgio committed Feb 15, 2024
2 parents 2fee185 + d30d184 commit 8635a1f
Show file tree
Hide file tree
Showing 11 changed files with 306 additions and 31 deletions.
25 changes: 23 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions mutiny-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>", "benthecarman <[email protected]>"]
description = "The core SDK for the mutiny node"
Expand Down Expand Up @@ -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"] }
Expand Down
14 changes: 14 additions & 0 deletions mutiny-core/src/labels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<(String, Contact)>, 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<S: MutinyStorage> LabelStorage for S {
Expand Down
99 changes: 96 additions & 3 deletions mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -1612,7 +1614,7 @@ impl<S: MutinyStorage> MutinyWallet<S> {
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)?;
}
}
}
Expand All @@ -1637,6 +1639,42 @@ impl<S: MutinyStorage> MutinyWallet<S> {
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<u8>) -> Result<String, MutinyError> {
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,
Expand All @@ -1655,6 +1693,45 @@ impl<S: MutinyStorage> MutinyWallet<S> {
.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<Value> = 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
Expand Down Expand Up @@ -2342,6 +2419,13 @@ pub(crate) async fn create_new_federation<S: MutinyStorage>(
})
}

#[derive(Deserialize)]
struct NostrBuildResult {
status: String,
message: String,
data: Vec<Value>,
}

// max amount that can be spent through a gateway
fn max_spendable_amount(current_balance_sat: u64, routing_fees: &GatewayFees) -> Option<u64> {
let current_balance_msat = current_balance_sat as f64 * 1_000.0;
Expand Down Expand Up @@ -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]
Expand Down
Loading

0 comments on commit 8635a1f

Please sign in to comment.