@@ -75,7 +75,7 @@ use crate::{nostr::NostrManager, utils::sleep};
7575use :: nostr:: key:: XOnlyPublicKey ;
7676use :: nostr:: nips:: nip57;
7777use :: nostr:: prelude:: ZapRequestData ;
78- use :: nostr:: { Event , EventId , JsonUtil , Kind } ;
78+ use :: nostr:: { Event , EventId , JsonUtil , Kind , Metadata } ;
7979use async_lock:: RwLock ;
8080use bdk_chain:: ConfirmationTime ;
8181use bip39:: Mnemonic ;
@@ -85,6 +85,7 @@ use bitcoin::secp256k1::PublicKey;
8585use bitcoin:: { hashes:: sha256, Network } ;
8686use fedimint_core:: { api:: InviteCode , config:: FederationId } ;
8787use futures:: { pin_mut, select, FutureExt } ;
88+ use futures_util:: join;
8889use hex_conservative:: { DisplayHex , FromHex } ;
8990#[ cfg( target_arch = "wasm32" ) ]
9091use instant:: Instant ;
@@ -94,6 +95,7 @@ use lightning::{log_debug, log_error, log_info, log_trace, log_warn};
9495use lightning_invoice:: { Bolt11Invoice , Bolt11InvoiceDescription } ;
9596use lnurl:: { lnurl:: LnUrl , AsyncClient as LnUrlClient , LnUrlResponse , Response } ;
9697use nostr_sdk:: { Client , RelayPoolNotification } ;
98+ use reqwest:: multipart:: { Form , Part } ;
9799use serde:: { Deserialize , Serialize } ;
98100use serde_json:: { json, Value } ;
99101use 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
23462430fn 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