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

Commit 8635a1f

Browse files
Merge pull request #992 from MutinyWallet/edit-profile
Create nostr profile
2 parents 2fee185 + d30d184 commit 8635a1f

11 files changed

Lines changed: 306 additions & 31 deletions

File tree

Cargo.lock

Lines changed: 23 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mutiny-core/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ cargo-features = ["per-package-target"]
22

33
[package]
44
name = "mutiny-core"
5-
version = "0.5.9"
5+
version = "0.6.0-rc1"
66
edition = "2021"
77
authors = ["Tony Giorgio <tony@mutinywallet.com>", "benthecarman <ben@mutinywallet.com>"]
88
description = "The core SDK for the mutiny node"
@@ -35,7 +35,7 @@ lightning-transaction-sync = { version = "0.0.121", default-features = false, fe
3535
lightning-liquidity = { git = "https://github.com/lightningdevkit/lightning-liquidity.git", rev = "478ccf9324e2650d200ea289a0ba8905afe420b6" }
3636
chrono = "0.4.22"
3737
futures-util = { version = "0.3", default-features = false }
38-
reqwest = { version = "0.11", default-features = false, features = ["json"] }
38+
reqwest = { version = "0.11", default-features = false, features = ["multipart", "json"] }
3939
async-trait = "0.1.68"
4040
url = { version = "2.3.1", features = ["serde"] }
4141
nostr = { version = "0.27.0", default-features = false, features = ["nip04", "nip05", "nip47", "nip57"] }

mutiny-core/src/labels.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,20 @@ pub trait LabelStorage {
155155
}
156156
Ok(None)
157157
}
158+
/// Finds a contact that has the given npub
159+
fn get_contact_for_npub(
160+
&self,
161+
npub: XOnlyPublicKey,
162+
) -> Result<Option<(String, Contact)>, MutinyError> {
163+
// todo this is not efficient, we should have a map of npub to contact
164+
let contacts = self.get_contacts()?;
165+
for (id, contact) in contacts {
166+
if contact.npub == Some(npub) {
167+
return Ok(Some((id, contact)));
168+
}
169+
}
170+
Ok(None)
171+
}
158172
}
159173

160174
impl<S: MutinyStorage> LabelStorage for S {

mutiny-core/src/lib.rs

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ use crate::{nostr::NostrManager, utils::sleep};
7575
use ::nostr::key::XOnlyPublicKey;
7676
use ::nostr::nips::nip57;
7777
use ::nostr::prelude::ZapRequestData;
78-
use ::nostr::{Event, EventId, JsonUtil, Kind};
78+
use ::nostr::{Event, EventId, JsonUtil, Kind, Metadata};
7979
use async_lock::RwLock;
8080
use bdk_chain::ConfirmationTime;
8181
use bip39::Mnemonic;
@@ -85,6 +85,7 @@ use bitcoin::secp256k1::PublicKey;
8585
use bitcoin::{hashes::sha256, Network};
8686
use fedimint_core::{api::InviteCode, config::FederationId};
8787
use futures::{pin_mut, select, FutureExt};
88+
use futures_util::join;
8889
use hex_conservative::{DisplayHex, FromHex};
8990
#[cfg(target_arch = "wasm32")]
9091
use instant::Instant;
@@ -94,6 +95,7 @@ use lightning::{log_debug, log_error, log_info, log_trace, log_warn};
9495
use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription};
9596
use lnurl::{lnurl::LnUrl, AsyncClient as LnUrlClient, LnUrlResponse, Response};
9697
use nostr_sdk::{Client, RelayPoolNotification};
98+
use reqwest::multipart::{Form, Part};
9799
use serde::{Deserialize, Serialize};
98100
use serde_json::{json, Value};
99101
use std::sync::Arc;
@@ -1612,7 +1614,7 @@ impl<S: MutinyStorage> MutinyWallet<S> {
16121614
if profile.tag != NwcProfileTag::Subscription {
16131615
let mut nwc = profile.clone();
16141616
nwc.tag = NwcProfileTag::Subscription;
1615-
self.nostr.edit_profile(nwc)?;
1617+
self.nostr.edit_nwc_profile(nwc)?;
16161618
}
16171619
}
16181620
}
@@ -1637,6 +1639,42 @@ impl<S: MutinyStorage> MutinyWallet<S> {
16371639
Ok(())
16381640
}
16391641

1642+
/// Uploads a profile pic to nostr.build and returns the uploaded file's URL
1643+
pub async fn upload_profile_pic(&self, image_bytes: Vec<u8>) -> Result<String, MutinyError> {
1644+
let client = reqwest::Client::new();
1645+
1646+
let form = Form::new().part("fileToUpload", Part::bytes(image_bytes));
1647+
let res: NostrBuildResult = client
1648+
.post("https://nostr.build/api/v2/upload/profile")
1649+
.multipart(form)
1650+
.send()
1651+
.await
1652+
.map_err(|_| MutinyError::NostrError)?
1653+
.json()
1654+
.await
1655+
.map_err(|_| MutinyError::NostrError)?;
1656+
1657+
if res.status != "success" {
1658+
log_error!(
1659+
self.logger,
1660+
"Error uploading profile picture: {}",
1661+
res.message
1662+
);
1663+
return Err(MutinyError::NostrError);
1664+
}
1665+
1666+
// get url from response body
1667+
if let Some(value) = res.data.first() {
1668+
return value
1669+
.get("url")
1670+
.and_then(|v| v.as_str())
1671+
.map(|s| s.to_string())
1672+
.ok_or(MutinyError::NostrError);
1673+
}
1674+
1675+
Err(MutinyError::NostrError)
1676+
}
1677+
16401678
/// Makes a request to the primal api
16411679
async fn primal_request(
16421680
client: &reqwest::Client,
@@ -1655,6 +1693,45 @@ impl<S: MutinyStorage> MutinyWallet<S> {
16551693
.map_err(|_| MutinyError::NostrError)
16561694
}
16571695

1696+
/// Syncs all of our nostr data from the configured primal instance
1697+
pub async fn sync_nostr(&self) -> Result<(), MutinyError> {
1698+
let contacts_fut = self.sync_nostr_contacts(self.nostr.public_key);
1699+
let profile_fut = self.sync_nostr_profile();
1700+
1701+
// join futures and handle result
1702+
let (contacts_res, profile_res) = join!(contacts_fut, profile_fut);
1703+
contacts_res?;
1704+
profile_res?;
1705+
1706+
Ok(())
1707+
}
1708+
1709+
/// Fetches our latest nostr profile from primal and saves to storage
1710+
async fn sync_nostr_profile(&self) -> Result<(), MutinyError> {
1711+
let url = self
1712+
.config
1713+
.primal_url
1714+
.as_deref()
1715+
.unwrap_or("https://primal-cache.mutinywallet.com/api");
1716+
let client = reqwest::Client::new();
1717+
1718+
let body = json!(["user_profile", { "pubkey": self.nostr.public_key } ]);
1719+
let data: Vec<Value> = Self::primal_request(&client, url, body).await?;
1720+
1721+
if let Some(json) = data.first().cloned() {
1722+
let event: Event = serde_json::from_value(json).map_err(|_| MutinyError::NostrError)?;
1723+
if event.kind != Kind::Metadata {
1724+
return Ok(());
1725+
}
1726+
1727+
let metadata: Metadata =
1728+
serde_json::from_str(&event.content).map_err(|_| MutinyError::NostrError)?;
1729+
self.storage.set_nostr_profile(metadata)?;
1730+
}
1731+
1732+
Ok(())
1733+
}
1734+
16581735
/// Get contacts from the given npub and sync them to the wallet
16591736
pub async fn sync_nostr_contacts(&self, npub: XOnlyPublicKey) -> Result<(), MutinyError> {
16601737
let url = self
@@ -2342,6 +2419,13 @@ pub(crate) async fn create_new_federation<S: MutinyStorage>(
23422419
})
23432420
}
23442421

2422+
#[derive(Deserialize)]
2423+
struct NostrBuildResult {
2424+
status: String,
2425+
message: String,
2426+
data: Vec<Value>,
2427+
}
2428+
23452429
// max amount that can be spent through a gateway
23462430
fn max_spendable_amount(current_balance_sat: u64, routing_fees: &GatewayFees) -> Option<u64> {
23472431
let current_balance_msat = current_balance_sat as f64 * 1_000.0;
@@ -2823,7 +2907,16 @@ mod tests {
28232907

28242908
// check that we got different messages
28252909
assert_eq!(next.len(), 2);
2826-
assert!(next.iter().all(|m| !messages.contains(m)))
2910+
assert!(next.iter().all(|m| !messages.contains(m)));
2911+
2912+
// test check for future messages, should be empty
2913+
let since = messages.iter().max_by_key(|m| m.date).unwrap().date + 1;
2914+
let future_msgs = mw
2915+
.get_dm_conversation(npub, limit, None, Some(since))
2916+
.await
2917+
.unwrap();
2918+
2919+
assert!(future_msgs.is_empty());
28272920
}
28282921

28292922
#[test]

0 commit comments

Comments
 (0)