Skip to content

Commit eff00fc

Browse files
danieldaquinoyukibtc
authored andcommitted
blossom: initial Blossom support
This commit adds initial Blossom support via: 1. the implementation of Blossom-related event kinds and tags. 2. the implementation of a new `blossom` crate containing models, as well as a Blossom client implementation. Closes #800 Closes #838 Pull-Request: #838 Signed-off-by: Yuki Kishimoto <[email protected]>
1 parent 53bc506 commit eff00fc

File tree

20 files changed

+951
-3
lines changed

20 files changed

+951
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
### Added
3131

32+
* blossom: add new crate with Blossom support ([Daniel D’Aquino])
3233
* mls-storage: add new crate with traits and types for mls storage implementations ([JeffG])
3334

3435
## v0.41.0 - 2025/04/15
@@ -1236,3 +1237,4 @@ added `nostrdb` storage backend, added NIP32 and completed NIP51 support and mor
12361237
[awiteb]: https://git.4rs.nl (nostr:nprofile1qqsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgpzfmhxue69uhkummnw3ezudrjwvhxumq3dg0ly)
12371238
[magine]: https://github.com/ma233 (?)
12381239
[daywalker90]: https://github.com/daywalker90 (nostr:npub1kuemsj7xryp0uje36dr53scn9mxxh8ema90hw9snu46633n9n2hqp3drjt)
1240+
[Daniel D’Aquino]: https://github.com/danieldaquino (nostr:npub13v47pg9dxjq96an8jfev9znhm0k7ntwtlh9y335paj9kyjsjpznqzzl3l8)

Cargo.lock

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

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ rust-version = "1.70.0"
1414
async-utility = "0.3"
1515
async-wsocket = "0.13"
1616
atomic-destructor = { version = "0.3", default-features = false }
17+
base64 = { version = "0.22", default-features = false }
18+
clap = "=4.4.18"
1719
js-sys = "0.3"
1820
lru = { version = "0.13", default-features = false }
1921
negentropy = { version = "0.5", default-features = false }
@@ -28,6 +30,7 @@ nostr-ndb = { version = "0.41", path = "./crates/nostr-ndb", default-features =
2830
nostr-relay-builder = { version = "0.41", path = "./crates/nostr-relay-builder", default-features = false }
2931
nostr-relay-pool = { version = "0.41", path = "./crates/nostr-relay-pool", default-features = false }
3032
nostr-sdk = { version = "0.41", path = "./crates/nostr-sdk", default-features = false }
33+
reqwest = { version = "0.12", default-features = false }
3134
serde = { version = "1.0", default-features = false }
3235
serde_json = { version = "1.0", default-features = false }
3336
tokio = { version = ">=1.37", default-features = false }

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The project is split up into several crates in the `crates/` directory:
66

77
* Libraries:
88
* [**nostr**](./crates/nostr): Rust implementation of Nostr protocol
9+
* [**nostr-blossom**](./crates/nostr-blossom): A library for interacting with the Blossom protocol
910
* [**nostr-connect**](./crates/nostr-connect): Nostr Connect (NIP46)
1011
* [**nostr-database**](./crates/nostr-database): Database for Nostr apps
1112
* [**nostr-lmdb**](./crates/nostr-lmdb): LMDB storage backend

crates/nostr-blossom/Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "nostr-blossom"
3+
version = "0.41.0"
4+
edition = "2021"
5+
description = "A library for interacting with the Blossom protocol"
6+
authors.workspace = true
7+
homepage.workspace = true
8+
repository.workspace = true
9+
license.workspace = true
10+
readme = "README.md"
11+
rust-version.workspace = true
12+
keywords = ["nostr", "blossom"]
13+
14+
[dependencies]
15+
base64.workspace = true
16+
nostr = { workspace = true, features = ["std"] }
17+
reqwest = { workspace = true, default-features = false, features = ["json", "rustls-tls"] }
18+
serde = { workspace = true, features = ["derive"] }
19+
serde_json.workspace = true
20+
21+
[dev-dependencies]
22+
clap = { workspace = true, features = ["derive"] }
23+
tokio = { workspace = true, features = ["full"] }

crates/nostr-blossom/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Blossom
2+
3+
A library for interacting with the [Blossom protocol](https://github.com/hzrd149/blossom).
4+
5+
## State
6+
7+
**This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways.
8+
9+
## Implemented BUDs
10+
11+
- **Basic data structures:** [BUD-01](https://github.com/hzrd149/blossom/blob/master/buds/01.md), [BUD-02](https://github.com/hzrd149/blossom/blob/master/buds/02.md)
12+
- **Client:** [BUD-01](https://github.com/hzrd149/blossom/blob/master/buds/01.md), [BUD-02](https://github.com/hzrd149/blossom/blob/master/buds/02.md)
13+
- **Server:** Not implemented
14+
15+
## Donations
16+
17+
`rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate).
18+
19+
## License
20+
21+
This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use std::error::Error;
2+
use std::str::FromStr;
3+
4+
use clap::Parser;
5+
use nostr::hashes::sha256;
6+
use nostr::key::SecretKey;
7+
use nostr::Keys;
8+
use nostr_blossom::client::BlossomClient;
9+
10+
#[derive(Parser, Debug)]
11+
#[command(author, version, about = "Delete a blob from a Blossom server", long_about = None)]
12+
struct Args {
13+
/// The server URL to connect to
14+
#[arg(long)]
15+
server: String,
16+
17+
/// The SHA256 hash of the blob to delete (in hex)
18+
#[arg(long)]
19+
sha256: String,
20+
21+
/// Optional private key for signing the deletion (in hex)
22+
#[arg(long, value_name = "PRIVATE_KEY")]
23+
private_key: String,
24+
}
25+
26+
#[tokio::main]
27+
async fn main() -> Result<(), Box<dyn Error>> {
28+
let args = Args::parse();
29+
30+
let client = BlossomClient::new(&args.server);
31+
32+
// Create signer keys using the given private key
33+
let keys = Keys::new(SecretKey::from_hex(&args.private_key)?);
34+
35+
println!("Attempting to delete blob with SHA256: {}", args.sha256);
36+
37+
let blob_hash = sha256::Hash::from_str(&args.sha256)?;
38+
39+
match client.delete_blob(blob_hash, None, &keys).await {
40+
Ok(()) => println!("Blob deleted successfully."),
41+
Err(e) => eprintln!("Failed to delete blob: {}", e),
42+
}
43+
44+
Ok(())
45+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use std::error::Error;
2+
use std::fs;
3+
use std::str::FromStr;
4+
5+
use clap::Parser;
6+
use nostr::hashes::sha256;
7+
use nostr_blossom::client::BlossomClient;
8+
9+
#[derive(Parser, Debug)]
10+
#[command(author, version, about = "Download a blob from a Blossom server", long_about = None)]
11+
struct Args {
12+
/// The server URL to connect to
13+
#[arg(long)]
14+
server: String,
15+
16+
/// SHA256 hash of the blob to download
17+
#[arg(long)]
18+
sha256: String,
19+
20+
/// Private key to use for authorization (in hex)
21+
#[arg(long)]
22+
private_key: String,
23+
}
24+
25+
#[tokio::main]
26+
async fn main() -> Result<(), Box<dyn Error>> {
27+
let args = Args::parse();
28+
29+
// Initialize the client.
30+
let client = BlossomClient::new(&args.server);
31+
32+
// Convert the provided SHA256 string into a hash.
33+
let blob_sha = sha256::Hash::from_str(&args.sha256)?;
34+
35+
// Parse the private key.
36+
let keypair = nostr::Keys::parse(&args.private_key)?;
37+
38+
// Download the blob with optional authorization.
39+
match client.get_blob(blob_sha, None, None, Some(&keypair)).await {
40+
Ok(blob) => {
41+
println!("Successfully downloaded blob with {} bytes", blob.len());
42+
let file_name = format!("{}", blob_sha);
43+
fs::write(&file_name, &blob)?;
44+
println!("Blob saved as {}", file_name);
45+
}
46+
Err(err) => {
47+
eprintln!("Failed to download blob: {}", err);
48+
}
49+
}
50+
51+
Ok(())
52+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use std::error::Error;
2+
use std::fs;
3+
use std::path::PathBuf;
4+
5+
use clap::Parser;
6+
use nostr::hashes::{sha256, Hash};
7+
use nostr::key::SecretKey;
8+
use nostr::Keys;
9+
use nostr_blossom::client::BlossomClient;
10+
11+
/// Integration test for various Blossom operations: upload, check (HEAD), list, download, and delete.
12+
#[derive(Parser, Debug)]
13+
#[command(author, version, about = "Run several operations against a Blossom server for demonstration and testing", long_about = None)]
14+
struct Args {
15+
/// The Blossom server URL
16+
#[arg(long)]
17+
server: String,
18+
19+
/// Optional file path to a blob to upload. If omitted, a small test blob is used.
20+
#[arg(long)]
21+
file: Option<PathBuf>,
22+
23+
/// Private key to use for signing (in hex)
24+
#[arg(long, value_name = "PRIVATE_KEY")]
25+
private_key: String,
26+
}
27+
28+
#[tokio::main]
29+
async fn main() -> Result<(), Box<dyn Error>> {
30+
// Parse command line arguments.
31+
let args = Args::parse();
32+
33+
// Create the Blossom client.
34+
let client = BlossomClient::new(&args.server);
35+
// Create signer keys from the provided private key.
36+
let keys = Keys::new(SecretKey::from_hex(args.private_key.as_str())?);
37+
38+
// Read the blob data from a file if provided, otherwise use default test data.
39+
let data = if let Some(file_path) = args.file {
40+
println!("Reading blob data from file: {:?}", file_path);
41+
fs::read(file_path)?
42+
} else {
43+
let default_blob = b"Test blob data from integration_test";
44+
println!(
45+
"No file provided. Using default test data: {:?}",
46+
String::from_utf8_lossy(default_blob)
47+
);
48+
default_blob.to_vec()
49+
};
50+
51+
// Compute SHA256 hash of the blob data.
52+
let blob_hash = sha256::Hash::hash(&data);
53+
let blob_hash_hex = blob_hash.to_string();
54+
println!("\nBlob SHA256: {}", blob_hash_hex);
55+
56+
// 1. Upload Blob
57+
println!("\n[1] Uploading blob...");
58+
// Use a basic content type
59+
let content_type = Some("application/octet-stream".to_string());
60+
let descriptor = client
61+
.upload_blob(data.clone(), content_type, None, Some(&keys))
62+
.await?;
63+
println!("Uploaded BlobDescriptor: {:#?}", descriptor);
64+
65+
// 2. Check blob existence using HEAD (has_blob method)
66+
println!("\n[2] Checking blob existence via HEAD request...");
67+
let exists = client.has_blob(blob_hash, None, Some(&keys)).await?;
68+
println!("has_blob result: {}", exists);
69+
70+
// 3. List blobs for the pubkey
71+
println!("\n[3] Listing blobs for public key...");
72+
let pubkey = keys.public_key();
73+
let blobs = client
74+
.list_blobs::<Keys>(&pubkey, None, None, None, Some(&keys))
75+
.await?;
76+
println!("List Blobs results:");
77+
for blob in blobs.iter() {
78+
println!(" - {:?}", blob);
79+
}
80+
81+
// 4. Download blob and compare hash
82+
println!("\n[4] Downloading blob...");
83+
84+
let downloaded_data: Vec<u8> = client.get_blob(blob_hash, None, None, Some(&keys)).await?;
85+
let downloaded_hash = sha256::Hash::hash(&downloaded_data);
86+
println!("Downloaded blob hash: {}", downloaded_hash);
87+
if downloaded_hash == blob_hash {
88+
println!("Downloaded blob hash matches the original.");
89+
} else {
90+
println!(
91+
"Hash mismatch! Original: {} Downloaded: {}",
92+
blob_hash, downloaded_hash
93+
);
94+
}
95+
96+
// 5. Delete blob
97+
println!("\n[5] Deleting blob...");
98+
client.delete_blob(blob_hash, None, &keys).await?;
99+
println!("Blob deleted successfully.");
100+
101+
// Final check: verify deletion using HEAD
102+
let exists_after = client.has_blob(blob_hash, None, Some(&keys)).await?;
103+
if !exists_after {
104+
println!("Verified: Blob no longer exists on the server.");
105+
} else {
106+
println!("Warning: Blob still exists on the server.");
107+
}
108+
109+
println!("\nIntegration test complete.");
110+
Ok(())
111+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use std::error::Error;
2+
use std::str::FromStr;
3+
4+
use clap::Parser;
5+
use nostr::key::SecretKey;
6+
use nostr::{Keys, PublicKey};
7+
use nostr_blossom::client::BlossomClient;
8+
9+
#[derive(Parser, Debug)]
10+
#[command(author, version, about = "List blob on a Blossom server", long_about = None)]
11+
struct Args {
12+
/// The server URL to connect to
13+
#[arg(long)]
14+
server: String,
15+
16+
/// The public key to list blobs for (in hex format)
17+
#[arg(long)]
18+
pubkey: String,
19+
20+
/// Optional private key for authorization (in hex)
21+
#[arg(long)]
22+
private_key: Option<String>,
23+
}
24+
25+
#[tokio::main]
26+
async fn main() -> Result<(), Box<dyn Error>> {
27+
let args = Args::parse();
28+
let client = BlossomClient::new(&args.server);
29+
30+
let pubkey = PublicKey::from_str(&args.pubkey)?;
31+
32+
// Check if a private key was provided and branch accordingly
33+
if let Some(private_key_str) = args.private_key {
34+
// Attempt to create the secret key, propagating error if parsing fails
35+
let secret_key = SecretKey::from_hex(&private_key_str)?;
36+
let keys = Keys::new(secret_key);
37+
38+
let descriptors = client
39+
.list_blobs(&pubkey, None, None, None, Some(&keys))
40+
.await?;
41+
42+
println!("Successfully listed blobs (with auth):");
43+
for descriptor in descriptors {
44+
println!("{:?}", descriptor);
45+
}
46+
} else {
47+
let descriptors = client
48+
.list_blobs(&pubkey, None, None, None, None::<&Keys>)
49+
.await?;
50+
51+
println!("Successfully listed blobs (without auth):");
52+
for descriptor in descriptors {
53+
println!("{:?}", descriptor);
54+
}
55+
}
56+
57+
Ok(())
58+
}

0 commit comments

Comments
 (0)