Skip to content

Commit 8ce4f00

Browse files
authored
Use blossom for file uploads (#537)
1 parent 5127b42 commit 8ce4f00

File tree

23 files changed

+839
-600
lines changed

23 files changed

+839
-600
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# 0.3.15
22

33
* Upload and download files to and from Nostr using Blossom
4+
* Add `nostr_hash` to `File` (breaking DB change)
45

56
# 0.3.14
67

clippy.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
too-many-arguments-threshold=12
1+
too-many-arguments-threshold=13

crates/bcr-ebill-api/src/external/file_storage.rs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use nostr::hashes::{
66
Hash,
77
sha256::{self, Hash as Sha256Hash},
88
};
9+
use reqwest::Url;
910
use serde::Deserialize;
1011
use thiserror::Error;
1112

@@ -60,6 +61,23 @@ impl FileStorageClient {
6061
}
6162
}
6263

64+
fn to_url(relay_url: &str, to_join: &str) -> Result<Url> {
65+
let mut url = reqwest::Url::parse(relay_url)
66+
.and_then(|url| url.join(to_join))
67+
.map_err(|_| Error::InvalidRelayUrl)?;
68+
match url.scheme() {
69+
"ws" => {
70+
url.set_scheme("http").map_err(|_| Error::InvalidRelayUrl)?;
71+
}
72+
"wss" => {
73+
url.set_scheme("https")
74+
.map_err(|_| Error::InvalidRelayUrl)?;
75+
}
76+
_ => (),
77+
};
78+
Ok(url)
79+
}
80+
6381
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
6482
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
6583
impl FileStorageClientApi for FileStorageClient {
@@ -71,12 +89,15 @@ impl FileStorageClientApi for FileStorageClient {
7189
}
7290
let hash = sha256::Hash::from_engine(hash_engine);
7391

74-
// PUT {relay_url}/upload
75-
let url = reqwest::Url::parse(relay_url)
76-
.and_then(|url| url.join("upload"))
77-
.map_err(|_| Error::InvalidRelayUrl)?;
7892
// Make upload request
79-
let resp: BlobDescriptorReply = self.cl.put(url).body(bytes).send().await?.json().await?;
93+
let resp: BlobDescriptorReply = self
94+
.cl
95+
.put(to_url(relay_url, "upload")?)
96+
.body(bytes)
97+
.send()
98+
.await?
99+
.json()
100+
.await?;
80101
let nostr_hash = resp.sha256;
81102

82103
// Check hash
@@ -88,12 +109,15 @@ impl FileStorageClientApi for FileStorageClient {
88109
}
89110

90111
async fn download(&self, relay_url: &str, nostr_hash: &str) -> Result<Vec<u8>> {
91-
// GET {relay_url}/{hash}
92-
let url = reqwest::Url::parse(relay_url)
93-
.and_then(|url| url.join(nostr_hash))
94-
.map_err(|_| Error::InvalidRelayUrl)?;
95112
// Make download request
96-
let resp: Vec<u8> = self.cl.get(url).send().await?.bytes().await?.into();
113+
let resp: Vec<u8> = self
114+
.cl
115+
.get(to_url(relay_url, nostr_hash)?)
116+
.send()
117+
.await?
118+
.bytes()
119+
.await?
120+
.into();
97121

98122
// Calculate hash to compare with the hash we sent
99123
let mut hash_engine = sha256::HashEngine::default();

crates/bcr-ebill-api/src/service/bill_service/issue.rs

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,28 @@ use bcr_ebill_core::{
1414
util::BcrKeys,
1515
};
1616
use bcr_ebill_transport::BillChainEvent;
17-
use log::{debug, error};
17+
use log::{debug, error, info};
1818

1919
impl BillService {
20+
async fn encrypt_and_save_uploaded_file(
21+
&self,
22+
file_name: &str,
23+
file_bytes: &[u8],
24+
bill_id: &str,
25+
bill_public_key: &str,
26+
relay_url: &str,
27+
) -> Result<File> {
28+
let file_hash = util::sha256_hash(file_bytes);
29+
let encrypted = util::crypto::encrypt_ecies(file_bytes, bill_public_key)?;
30+
let nostr_hash = self.file_upload_client.upload(relay_url, encrypted).await?;
31+
info!("Saved file {file_name} with hash {file_hash} for bill {bill_id}");
32+
Ok(File {
33+
name: file_name.to_owned(),
34+
hash: file_hash,
35+
nostr_hash: nostr_hash.to_string(),
36+
})
37+
}
38+
2039
pub(super) async fn issue_bill(&self, data: BillIssueData) -> Result<BitcreditBill> {
2140
debug!(
2241
"issuing bill with type {}, blank: {}",
@@ -108,6 +127,7 @@ impl BillService {
108127
debug!("issuing bill with drawee {public_data_drawee:?} and payee {public_data_payee:?}");
109128

110129
let identity = self.identity_store.get_full().await?;
130+
let nostr_relays = identity.identity.nostr_relays.clone();
111131
let keys = BcrKeys::new();
112132
let public_key = keys.get_public_key();
113133

@@ -118,16 +138,24 @@ impl BillService {
118138
};
119139

120140
let mut bill_files: Vec<File> = vec![];
121-
for file_upload_id in data.file_upload_ids.iter() {
122-
let (file_name, file_bytes) = &self
123-
.file_upload_store
124-
.read_temp_upload_file(file_upload_id)
125-
.await
126-
.map_err(|_| Error::NoFileForFileUploadId)?;
127-
bill_files.push(
128-
self.encrypt_and_save_uploaded_file(file_name, file_bytes, &bill_id, &public_key)
141+
if let Some(nostr_relay) = nostr_relays.first() {
142+
for file_upload_id in data.file_upload_ids.iter() {
143+
let (file_name, file_bytes) = &self
144+
.file_upload_store
145+
.read_temp_upload_file(file_upload_id)
146+
.await
147+
.map_err(|_| Error::NoFileForFileUploadId)?;
148+
bill_files.push(
149+
self.encrypt_and_save_uploaded_file(
150+
file_name,
151+
file_bytes,
152+
&bill_id,
153+
&public_key,
154+
nostr_relay,
155+
)
129156
.await?,
130-
);
157+
);
158+
}
131159
}
132160

133161
let bill = BitcreditBill {

crates/bcr-ebill-api/src/service/bill_service/mod.rs

Lines changed: 39 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use crate::blockchain::bill::BillBlockchain;
22
use crate::data::{
3-
File,
43
bill::{
54
BillCombinedBitcoinKey, BillKeys, BillsBalanceOverview, BillsFilterRole, BitcreditBill,
65
BitcreditBillResult, Endorsement, LightBitcreditBillResult, PastEndorsee,
@@ -83,20 +82,10 @@ pub trait BillServiceApi: ServiceTraitBounds {
8382
async fn open_and_decrypt_attached_file(
8483
&self,
8584
bill_id: &str,
86-
file_name: &str,
85+
file: &bcr_ebill_core::File,
8786
bill_private_key: &str,
8887
) -> Result<Vec<u8>>;
8988

90-
/// encrypts and saves the given uploaded file, returning the file name, as well as the hash of
91-
/// the unencrypted file
92-
async fn encrypt_and_save_uploaded_file(
93-
&self,
94-
file_name: &str,
95-
file_bytes: &[u8],
96-
bill_id: &str,
97-
bill_public_key: &str,
98-
) -> Result<File>;
99-
10089
/// issues a new bill
10190
async fn issue_new_bill(&self, data: BillIssueData) -> Result<BitcreditBill>;
10291

@@ -235,7 +224,7 @@ pub mod tests {
235224
util,
236225
};
237226
use bcr_ebill_core::{
238-
ValidationError,
227+
File, ValidationError,
239228
bill::{
240229
BillAcceptanceStatus, BillCurrentWaitingState, BillPaymentStatus, BillRecourseStatus,
241230
BillSellStatus, BillWaitingForPaymentState, PastPaymentStatus, RecourseReason,
@@ -259,9 +248,11 @@ pub mod tests {
259248
util::date::DateTimeUtc,
260249
};
261250
use cashu::nut02 as cdk02;
262-
use core::str;
263251
use mockall::predicate::{always, eq, function};
264-
use std::collections::{HashMap, HashSet};
252+
use std::{
253+
collections::{HashMap, HashSet},
254+
str::FromStr,
255+
};
265256
use test_utils::{
266257
accept_block, get_baseline_bill, get_baseline_cached_bill, get_baseline_identity, get_ctx,
267258
get_genesis_chain, get_service, offer_to_sell_block, recourse_block, reject_accept_block,
@@ -480,9 +471,12 @@ pub mod tests {
480471
ctx.file_upload_store
481472
.expect_remove_temp_upload_folder()
482473
.returning(|_| Ok(()));
483-
ctx.file_upload_store
484-
.expect_save_attached_file()
485-
.returning(move |_, _, _| Ok(()));
474+
ctx.file_upload_client.expect_upload().returning(|_, _| {
475+
Ok(nostr::hashes::sha256::Hash::from_str(
476+
"d277fe40da2609ca08215cdfbeac44835d4371a72f1416a63c87efd67ee24bfa",
477+
)
478+
.unwrap())
479+
});
486480
ctx.bill_store.expect_save_keys().returning(|_, _| Ok(()));
487481
ctx.bill_store
488482
.expect_save_bill_to_cache()
@@ -543,9 +537,12 @@ pub mod tests {
543537
ctx.file_upload_store
544538
.expect_remove_temp_upload_folder()
545539
.returning(|_| Ok(()));
546-
ctx.file_upload_store
547-
.expect_save_attached_file()
548-
.returning(move |_, _, _| Ok(()));
540+
ctx.file_upload_client.expect_upload().returning(|_, _| {
541+
Ok(nostr::hashes::sha256::Hash::from_str(
542+
"d277fe40da2609ca08215cdfbeac44835d4371a72f1416a63c87efd67ee24bfa",
543+
)
544+
.unwrap())
545+
});
549546
ctx.bill_store.expect_save_keys().returning(|_, _| Ok(()));
550547
ctx.bill_store
551548
.expect_save_bill_to_cache()
@@ -686,9 +683,12 @@ pub mod tests {
686683
ctx.file_upload_store
687684
.expect_remove_temp_upload_folder()
688685
.returning(|_| Ok(()));
689-
ctx.file_upload_store
690-
.expect_save_attached_file()
691-
.returning(move |_, _, _| Ok(()));
686+
ctx.file_upload_client.expect_upload().returning(|_, _| {
687+
Ok(nostr::hashes::sha256::Hash::from_str(
688+
"d277fe40da2609ca08215cdfbeac44835d4371a72f1416a63c87efd67ee24bfa",
689+
)
690+
.unwrap())
691+
});
692692
ctx.bill_store.expect_save_keys().returning(|_, _| Ok(()));
693693
ctx.bill_store
694694
.expect_save_bill_to_cache()
@@ -734,71 +734,26 @@ pub mod tests {
734734
}
735735

736736
#[tokio::test]
737-
async fn save_encrypt_open_decrypt_compare_hashes() {
738-
let mut ctx = get_ctx();
739-
let bill_id = "test_bill_id";
740-
let file_name = "invoice_00000000-0000-0000-0000-000000000000.pdf";
741-
let file_bytes = String::from("hello world").as_bytes().to_vec();
742-
let expected_encrypted =
743-
util::crypto::encrypt_ecies(&file_bytes, TEST_PUB_KEY_SECP).unwrap();
744-
745-
ctx.file_upload_store
746-
.expect_save_attached_file()
747-
.with(always(), eq(bill_id), eq(file_name))
748-
.times(1)
749-
.returning(|_, _, _| Ok(()));
750-
751-
ctx.file_upload_store
752-
.expect_open_attached_file()
753-
.with(eq(bill_id), eq(file_name))
754-
.times(1)
755-
.returning(move |_, _| Ok(expected_encrypted.clone()));
756-
let service = get_service(ctx);
757-
758-
let bill_file = service
759-
.encrypt_and_save_uploaded_file(file_name, &file_bytes, bill_id, TEST_PUB_KEY_SECP)
760-
.await
761-
.unwrap();
762-
assert_eq!(
763-
bill_file.hash,
764-
String::from("DULfJyE3WQqNxy3ymuhAChyNR3yufT88pmqvAazKFMG4")
765-
);
766-
assert_eq!(bill_file.name, String::from(file_name));
767-
768-
let decrypted = service
769-
.open_and_decrypt_attached_file(bill_id, file_name, TEST_PRIVATE_KEY_SECP)
770-
.await
771-
.unwrap();
772-
assert_eq!(str::from_utf8(&decrypted).unwrap(), "hello world");
773-
}
774-
775-
#[tokio::test]
776-
async fn save_encrypt_propagates_write_file_error() {
777-
let mut ctx = get_ctx();
778-
ctx.file_upload_store
779-
.expect_save_attached_file()
780-
.returning(|_, _, _| Err(persistence::Error::Io(std::io::Error::other("test error"))));
781-
let service = get_service(ctx);
782-
783-
assert!(
784-
service
785-
.encrypt_and_save_uploaded_file("file_name", &[], "test", TEST_PUB_KEY_SECP)
786-
.await
787-
.is_err()
788-
);
789-
}
790-
791-
#[tokio::test]
792-
async fn open_decrypt_propagates_read_file_error() {
737+
async fn open_decrypt_propagates_download_error() {
793738
let mut ctx = get_ctx();
794-
ctx.file_upload_store
795-
.expect_open_attached_file()
796-
.returning(|_, _| Err(persistence::Error::Io(std::io::Error::other("test error"))));
739+
ctx.file_upload_client.expect_download().returning(|_, _| {
740+
Err(crate::external::Error::ExternalFileStorageApi(
741+
crate::external::file_storage::Error::InvalidRelayUrl,
742+
))
743+
});
797744
let service = get_service(ctx);
798745

799746
assert!(
800747
service
801-
.open_and_decrypt_attached_file("test", "test", TEST_PRIVATE_KEY_SECP)
748+
.open_and_decrypt_attached_file(
749+
"test",
750+
&File {
751+
name: "some_file".into(),
752+
hash: "".into(),
753+
nostr_hash: "".into()
754+
},
755+
TEST_PRIVATE_KEY_SECP
756+
)
802757
.await
803758
.is_err()
804759
);
@@ -6033,7 +5988,6 @@ pub mod tests {
60335988
1731593930,
60345989
)
60355990
.await;
6036-
println!("{res:?}");
60375991
assert!(res.is_ok());
60385992
}
60395993

0 commit comments

Comments
 (0)