Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,21 @@ name = "silentpayments"
crate-type = ["lib"]

[features]
default = ["sending", "receiving"]
sending = []
receiving = []
default = ["encode", "sending", "receiving"]
encode = ["dep:bech32"]
serde = ["dep:serde"]
sending = ["dep:bitcoin_hashes", "dep:hex", "encode"]
receiving = ["dep:bitcoin_hashes", "dep:hex", "dep:bimap", "serde", "encode"]

[dependencies]
secp256k1 = {version = "0.28.1", features = ["rand"] }
hex = "0.4"
bech32 = "0.9"
bimap = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
bitcoin_hashes = "0.13.0"
hex = { version = "0.4", optional = true }
bech32 = { version = "0.9", optional = true }
bimap = { version = "0.6", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
bitcoin_hashes = { version = "0.13.0", optional = true }

[dev-dependencies]
rust-bip39 = { version = "1.0.0", features = ["rand"] }
bitcoin ={ version = "0.31.1", features = ["serde"] }
serde_json = "1.0"
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,47 @@ In the future, the library will probably be expanded to rely on structs from rus

The library is split up in two parts: sending and receiving.

## Feature Flags

This library offers granular feature flags to minimize dependencies for different use cases:

- **default**: Enables all features (`encode`, `sending`, `receiving`)
- **encode**: Enables string encoding/decoding for `SilentPaymentAddress` (adds `bech32` dependency)
- **serde**: Enables serde serialization/deserialization for types (adds `serde` dependency)
- **sending**: Enables sending functionality (adds `bitcoin_hashes`, `hex` dependencies)
- **receiving**: Enables receiving functionality (adds `bitcoin_hashes`, `hex`, `bimap`, `serde` dependencies)

### Minimal Usage

If you only need the type definitions (`Network` and `SilentPaymentAddress`) without any protocol functionality:

```toml
[dependencies]
silentpayments = { version = "0.4", default-features = false }
```

This configuration only pulls in `secp256k1` as a dependency, significantly reducing the dependency tree for applications that only need to work with silent payment addresses without implementing the full protocol.

**Bring Your Own Parser**: Even without the `encode` feature, you can construct a `SilentPaymentAddress` using `SilentPaymentAddress::new()` if you parse the bech32 yourself. This is useful if your application already has a bech32 parser and you want to avoid duplicate dependencies. The constructor documentation includes the complete bech32 format specification.

### Custom Feature Combinations

You can enable only the features you need:

```toml
# Just types and string encoding (no protocol implementation)
silentpayments = { version = "0.4", default-features = false, features = ["encode"] }

# Types with serde support (no protocol or encoding)
silentpayments = { version = "0.4", default-features = false, features = ["serde"] }

# Only sending capability
silentpayments = { version = "0.4", default-features = false, features = ["sending"] }

# Only receiving capability
silentpayments = { version = "0.4", default-features = false, features = ["receiving"] }
```

## Sending

For sending to a silent payment address, you can call the `sender::generate_recipient_pubkeys` function.
Expand Down
49 changes: 49 additions & 0 deletions examples/custom_parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/// Example showing how to construct a SilentPaymentAddress without the `encode` feature.
///
/// This is useful if your application already has a bech32 parser and you want to
/// avoid duplicate dependencies. You can parse the address yourself and then
/// construct the type using the public constructor.
///
/// To run this example:
/// ```bash
/// cargo run --example custom_parser --no-default-features
/// ```

fn main() {
use secp256k1::{PublicKey, Secp256k1, SecretKey};
use silentpayments::{Network, SilentPaymentAddress};

// Example: Simulating what you'd get after parsing bech32 yourself
// In a real application, you would:
// 1. Parse the bech32m string (e.g., "sp1...")
// 2. Extract the HRP to determine the network
// 3. Decode the data part to get version + 33-byte scan key + 33-byte spend key
// 4. Deserialize the pubkeys from those bytes
// 5. Construct the SilentPaymentAddress using the constructor

// For this example, we'll generate valid pubkeys
let secp = Secp256k1::new();
let scan_secret = SecretKey::from_slice(&[0x01; 32]).expect("valid key");
let spend_secret = SecretKey::from_slice(&[0x02; 32]).expect("valid key");

let scan_pubkey = PublicKey::from_secret_key(&secp, &scan_secret);
let spend_pubkey = PublicKey::from_secret_key(&secp, &spend_secret);

// Construct the SilentPaymentAddress without needing the `encode` feature
let address = SilentPaymentAddress::new(
scan_pubkey,
spend_pubkey,
Network::Mainnet,
0, // version
)
.expect("Failed to create address");

println!("Successfully created SilentPaymentAddress without encode feature!");
println!("Network: {:?}", address.get_network());
println!("Version: {}", address.get_version());
println!("Scan key: {:?}", address.get_scan_key());
println!("Spend key: {:?}", address.get_spend_key());

println!("\n✅ This example compiled and ran with ZERO features enabled!");
println!(" Only dependencies: secp256k1, rand, rand_core, secp256k1-sys");
}
2 changes: 2 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ impl fmt::Display for Error {

impl std::error::Error for Error {}

#[cfg(any(feature = "sending", feature = "receiving"))]
impl From<hex::FromHexError> for Error {
fn from(e: hex::FromHexError) -> Self {
Error::InvalidLabel(e.to_string())
}
}

#[cfg(feature = "encode")]
impl From<bech32::Error> for Error {
fn from(e: bech32::Error) -> Self {
Error::InvalidAddress(e.to_string())
Expand Down
31 changes: 28 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
//! A rust implementation of BIP352: Silent Payments. This library
//! can be used to add silent payment support to wallets.
//!
//! This library is split up in two parts: sending and receiving.
//! Either of these can be implemented independently using
//! the `sending` or `receiving` features.
//! ## Feature Flags
//!
//! This library offers granular feature flags to minimize dependencies:
//!
//! - **default**: Enables `encode`, `sending`, and `receiving` features
//! - **encode**: Enables string encoding/decoding for `SilentPaymentAddress` (requires `bech32`)
//! - **serde**: Enables serde serialization/deserialization for types
//! - **sending**: Enables sending functionality (requires `bitcoin_hashes`, `hex`, and `encode`)
//! - **receiving**: Enables receiving functionality (requires `bitcoin_hashes`, `hex`, `bimap`, `serde`, and `encode`)
//!
//! ### Minimal Usage
//!
//! If you only need the type definitions (`Network` and `SilentPaymentAddress`) without
//! any additional functionality, you can disable all default features:
//!
//! ```toml
//! [dependencies]
//! silentpayments = { version = "0.4", default-features = false }
//! ```
//!
//! This will only pull in `secp256k1` as a dependency, giving you access to the core types
//! without any encoding, serialization, or protocol functionality.
//!
//! **Note**: Even without the `encode` feature, you can still construct a `SilentPaymentAddress`
//! from its components using `SilentPaymentAddress::new()`. This allows you to use your own
//! bech32 parser (if your application already has one) and avoid duplicate dependencies.
//! See the `SilentPaymentAddress::new()` documentation for the bech32 format specification.
//!
//! ## Examples
//!
Expand All @@ -22,6 +46,7 @@ pub mod receiving;
pub mod sending;
pub mod utils;

#[cfg(any(feature = "sending", feature = "receiving"))]
pub use bitcoin_hashes;
pub use secp256k1;

Expand Down
2 changes: 1 addition & 1 deletion src/receiving.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ impl Serialize for SerializablePubkey {
S: serde::Serializer,
{
let mut seq = serializer.serialize_tuple(self.0.len())?;
for element in self.0.as_ref() {
for element in &self.0[..] {
seq.serialize_element(element)?;
}
seq.end()
Expand Down
1 change: 1 addition & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//!
//! This module contains functions that are more 'high-level'
//! than the basic sending and receiving logic.
#[cfg(any(feature = "sending", feature = "receiving"))]
pub(crate) mod hash;
#[cfg(feature = "receiving")]
pub mod receiving;
Expand Down
62 changes: 60 additions & 2 deletions src/utils/common.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
#[cfg(feature = "encode")]
use core::fmt;

#[cfg(any(feature = "sending", feature = "receiving"))]
use crate::utils::hash::SharedSecretHash;
use crate::Error;
use crate::Result;
#[cfg(feature = "encode")]
use bech32::{FromBase32, ToBase32};
#[cfg(any(feature = "sending", feature = "receiving"))]
use bitcoin_hashes::Hash;
use secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey};
use secp256k1::PublicKey;
#[cfg(any(feature = "sending", feature = "receiving"))]
use secp256k1::{Scalar, Secp256k1, SecretKey};
#[cfg(all(feature = "serde", feature = "encode"))]
use serde::ser::Serializer;
#[cfg(all(feature = "serde", feature = "encode"))]
use serde::Deserializer;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

#[cfg(any(feature = "sending", feature = "receiving"))]
pub(crate) fn calculate_t_n(ecdh_shared_secret: &PublicKey, k: u32) -> Result<SecretKey> {
let hash = SharedSecretHash::from_ecdh_and_k(ecdh_shared_secret, k).to_byte_array();
let sk = SecretKey::from_slice(&hash)?;

Ok(sk)
}

#[cfg(any(feature = "sending", feature = "receiving"))]
pub(crate) fn calculate_P_n(B_spend: &PublicKey, t_n: Scalar) -> Result<PublicKey> {
let secp = Secp256k1::new();

Expand All @@ -29,7 +40,8 @@ pub(crate) fn calculate_P_n(B_spend: &PublicKey, t_n: Scalar) -> Result<PublicKe
///
/// There are three network types: Mainnet (`sp1..`), Testnet (`tsp1..`), and Regtest (`sprt1..`).
/// Signet uses the same network type as Testnet.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Deserialize, Serialize)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub enum Network {
Mainnet,
Testnet,
Expand Down Expand Up @@ -69,6 +81,7 @@ pub struct SilentPaymentAddress {
network: Network,
}

#[cfg(all(feature = "serde", feature = "encode"))]
impl Serialize for SilentPaymentAddress {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
Expand All @@ -79,6 +92,7 @@ impl Serialize for SilentPaymentAddress {
}
}

#[cfg(all(feature = "serde", feature = "encode"))]
impl<'de> Deserialize<'de> for SilentPaymentAddress {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
Expand All @@ -91,6 +105,38 @@ impl<'de> Deserialize<'de> for SilentPaymentAddress {
}

impl SilentPaymentAddress {
/// Construct a `SilentPaymentAddress` from its component parts.
///
/// This constructor is always available, even without the `encode` feature.
/// If you have your own bech32 parser, you can use it to extract the components
/// and then construct the address using this method.
///
/// # Bech32 Format (for external parsers)
///
/// Silent payment addresses use bech32m encoding with the following structure:
/// - **HRP (Human Readable Part)**:
/// - Mainnet: `"sp"`
/// - Testnet/Signet: `"tsp"`
/// - Regtest: `"sprt"`
/// - **Data**: version (1 byte) + scan_pubkey (33 bytes) + spend_pubkey (33 bytes)
///
/// # Example
///
/// ```ignore
/// use secp256k1::PublicKey;
/// use silentpayments::{SilentPaymentAddress, Network};
///
/// // After parsing bech32 yourself and extracting the pubkeys:
/// let scan_pubkey = PublicKey::from_slice(&scan_bytes)?;
/// let spend_pubkey = PublicKey::from_slice(&spend_bytes)?;
///
/// let address = SilentPaymentAddress::new(
/// scan_pubkey,
/// spend_pubkey,
/// Network::Mainnet,
/// 0 // version
/// )?;
/// ```
pub fn new(
scan_pubkey: PublicKey,
m_pubkey: PublicKey,
Expand All @@ -111,25 +157,35 @@ impl SilentPaymentAddress {
})
}

/// Get the scan public key.
pub fn get_scan_key(&self) -> PublicKey {
self.scan_pubkey
}

/// Get the spend public key.
pub fn get_spend_key(&self) -> PublicKey {
self.m_pubkey
}

/// Get the network.
pub fn get_network(&self) -> Network {
self.network
}

/// Get the version byte.
pub fn get_version(&self) -> u8 {
self.version
}
}

#[cfg(feature = "encode")]
impl fmt::Display for SilentPaymentAddress {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", <SilentPaymentAddress as Into<String>>::into(*self))
}
}

#[cfg(feature = "encode")]
impl TryFrom<&str> for SilentPaymentAddress {
type Error = Error;

Expand Down Expand Up @@ -163,6 +219,7 @@ impl TryFrom<&str> for SilentPaymentAddress {
}
}

#[cfg(feature = "encode")]
impl TryFrom<String> for SilentPaymentAddress {
type Error = Error;

Expand All @@ -171,6 +228,7 @@ impl TryFrom<String> for SilentPaymentAddress {
}
}

#[cfg(feature = "encode")]
impl From<SilentPaymentAddress> for String {
fn from(val: SilentPaymentAddress) -> Self {
let hrp = match val.network {
Expand Down
Loading