@@ -2,6 +2,10 @@ use std::fs;
22use std:: path:: PathBuf ;
33
44use anyhow:: Result ;
5+ use base64:: engine:: general_purpose:: STANDARD as BASE64 ;
6+ use base64:: Engine ;
7+ use bitcoin_hashes:: sha256:: Hash as Sha256Hash ;
8+ use bitcoin_hashes:: Hash ;
59use chacha20poly1305:: aead:: { Aead , KeyInit } ;
610use chacha20poly1305:: { ChaCha20Poly1305 , Key , Nonce } ;
711use nostr_sdk:: prelude:: * ;
@@ -38,7 +42,7 @@ fn derive_shared_key(trade_keys: &Keys, admin_pubkey: &PublicKey) -> [u8; 32] {
3842 . xonly ( )
3943 . expect ( "failed to get x-only public key for admin" ) ;
4044 let secp_pk = SecpPublicKey :: from_x_only_public_key ( xonly, Parity :: Even ) ;
41- let mut point_bytes = shared_secret_point ( & secp_pk, & sk) . as_slice ( ) . to_vec ( ) ;
45+ let mut point_bytes = shared_secret_point ( & secp_pk, sk) . as_slice ( ) . to_vec ( ) ;
4246 point_bytes. resize ( 32 , 0 ) ;
4347 point_bytes
4448 . try_into ( )
@@ -78,39 +82,113 @@ fn encrypt_blob(shared_key: [u8; 32], plaintext: &[u8]) -> Result<(Vec<u8>, Stri
7882 Ok ( ( blob, nonce_hex) )
7983}
8084
81- async fn upload_to_blossom ( encrypted_blob : Vec < u8 > ) -> Result < String > {
85+ /// Upload encrypted blob to a Blossom server.
86+ /// Blossom BUD-01 requires kind 24242 with: content (human-readable), tags ["t","upload"],
87+ /// ["expiration", "<future unix ts>"], ["x", "<sha256 hex>"].
88+ async fn upload_to_blossom ( trade_keys : & Keys , encrypted_blob : Vec < u8 > ) -> Result < String > {
8289 use reqwest:: StatusCode ;
8390
8491 let client = reqwest:: Client :: new ( ) ;
92+ let payload_hash = Sha256Hash :: hash ( & encrypted_blob) ;
93+ let payload_hex = payload_hash. to_string ( ) ;
94+
95+ // Expiration: 1 hour from now (BUD-01 requires expiration in the future)
96+ let expiration = Timestamp :: from ( Timestamp :: now ( ) . as_u64 ( ) + 3600 ) ;
8597
8698 for server in BLOSSOM_SERVERS {
87- let url = format ! ( "{}/upload" , server. trim_end_matches( '/' ) ) ;
88- let res = client
89- . put ( & url)
90- . header ( "Content-Type" , "application/octet-stream" )
91- . body ( encrypted_blob. clone ( ) )
92- . send ( )
93- . await ;
94-
95- match res {
96- Ok ( resp) if resp. status ( ) == StatusCode :: OK => {
97- let body = resp
98- . text ( )
99- . await
100- . map_err ( |e| anyhow:: anyhow!( "failed to read blossom response: {e}" ) ) ?;
101- if body. trim ( ) . starts_with ( "blossom://" ) {
102- return Ok ( body. trim ( ) . to_string ( ) ) ;
103- }
104- }
105- Ok ( resp) => {
106- eprintln ! (
107- "Blossom upload failed on {server} with status {}" ,
108- resp. status( )
109- ) ;
99+ let url_str = format ! ( "{}/upload" , server. trim_end_matches( '/' ) ) ;
100+ let upload_url = match Url :: parse ( & url_str) {
101+ Ok ( u) => u,
102+ Err ( e) => {
103+ eprintln ! ( "Blossom invalid URL {url_str}: {e}" ) ;
104+ continue ;
110105 }
106+ } ;
107+
108+ let normalized_url_str = upload_url. as_str ( ) ;
109+
110+ // BUD-01: kind 24242, content human-readable, tags: t=upload, expiration, x=sha256
111+ let tags = [
112+ Tag :: hashtag ( "upload" ) , // ["t", "upload"]
113+ Tag :: expiration ( expiration) ,
114+ Tag :: custom ( TagKind :: x ( ) , [ payload_hex. clone ( ) ] ) ,
115+ ] ;
116+ let event = match EventBuilder :: new ( Kind :: BlossomAuth , "Upload Blob" )
117+ . tags ( tags)
118+ . sign_with_keys ( trade_keys)
119+ {
120+ Ok ( e) => e,
111121 Err ( e) => {
112- eprintln ! ( "Blossom upload error on {server}: {e}" ) ;
122+ eprintln ! ( "Blossom auth event build failed for {server}: {e}" ) ;
123+ continue ;
124+ }
125+ } ;
126+ let auth_header = format ! ( "Nostr {}" , BASE64 . encode( event. as_json( ) ) ) ;
127+
128+ // Many Blossom servers reject application/octet-stream. Try application/zip first, then image/png.
129+ let content_types = [ "application/zip" , "image/png" ] ;
130+ let mut last_status = None ;
131+ let mut last_error_body = String :: new ( ) ;
132+
133+ for content_type in content_types {
134+ let res = client
135+ . put ( normalized_url_str)
136+ . header ( "Content-Type" , content_type)
137+ . header ( "Authorization" , & auth_header)
138+ . body ( encrypted_blob. clone ( ) )
139+ . send ( )
140+ . await ;
141+
142+ match res {
143+ Ok ( resp) if resp. status ( ) == StatusCode :: OK => {
144+ let body = resp
145+ . text ( )
146+ . await
147+ . map_err ( |e| anyhow:: anyhow!( "failed to read blossom response: {e}" ) ) ?;
148+ let body = body. trim ( ) ;
149+ // BUD-02: server may return JSON blob descriptor with "url" field
150+ if let Ok ( v) = serde_json:: from_str :: < serde_json:: Value > ( body) {
151+ if let Some ( url) = v. get ( "url" ) . and_then ( |u| u. as_str ( ) ) {
152+ return Ok ( url. to_string ( ) ) ;
153+ }
154+ }
155+ if body. starts_with ( "blossom://" ) {
156+ return Ok ( body. to_string ( ) ) ;
157+ }
158+ last_status = Some ( StatusCode :: OK ) ;
159+ last_error_body = body. to_string ( ) ;
160+ break ;
161+ }
162+ Ok ( resp) => {
163+ let status = resp. status ( ) ;
164+ last_status = Some ( status) ;
165+ last_error_body = resp. text ( ) . await . unwrap_or_default ( ) ;
166+ // If rejected for content type, try next; otherwise report and break
167+ if status != StatusCode :: UNSUPPORTED_MEDIA_TYPE
168+ && status != StatusCode :: BAD_REQUEST
169+ {
170+ break ;
171+ }
172+ }
173+ Err ( e) => {
174+ last_status = None ;
175+ last_error_body = e. to_string ( ) ;
176+ break ;
177+ }
178+ }
179+ }
180+
181+ if let Some ( status) = last_status {
182+ let status_text = status. canonical_reason ( ) . unwrap_or ( "Unknown" ) ;
183+ eprintln ! (
184+ "Blossom upload failed on {server} with status {} {}" ,
185+ status, status_text
186+ ) ;
187+ if !last_error_body. is_empty ( ) && last_error_body. len ( ) < 500 {
188+ eprintln ! ( " Response body: {}" , last_error_body) ;
113189 }
190+ } else {
191+ eprintln ! ( "Blossom upload error on {server}: {}" , last_error_body) ;
114192 }
115193 }
116194
@@ -185,7 +263,7 @@ pub async fn execute_send_admin_dm_attach(
185263 let shared_key = derive_shared_key ( & trade_keys, & receiver) ;
186264 let ( encrypted_blob, nonce_hex) = encrypt_blob ( shared_key, & file_bytes) ?;
187265 let encrypted_size = encrypted_blob. len ( ) ;
188- let blossom_url = upload_to_blossom ( encrypted_blob) . await ?;
266+ let blossom_url = upload_to_blossom ( & trade_keys , encrypted_blob) . await ?;
189267
190268 let filename = file_path
191269 . file_name ( )
0 commit comments