Skip to content
This repository was archived by the owner on Feb 3, 2025. It is now read-only.

Create nostr profile #992

Merged
merged 9 commits into from
Feb 15, 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
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
Loading