Fix NIP-04 to standard wire format: raw-X ECDH key, ct?iv=iv layout
nip04_encrypt/decrypt previously double-hashed the ECDH shared secret (SHA256 over the libsecp default hash) and packed base64(IV || ciphertext), making signer-produced DMs readable only by itself. - core/keys.rs: add ecdh_shared_secret_raw_x() using secp256k1::ecdh::shared_secret_point — raw X coordinate, unhashed. Existing hashed helpers unchanged (NIP-44 still uses them). - nips/nip004.rs: use the raw-X key; emit base64(ct)?iv=base64(iv); decrypt parses only the ?iv= format (standard-only, no legacy fallback; missing separator -> Nip04InvalidFormat). Key derivation is parity-independent, so the both-parity dance is removed. - Tests: known-answer vector generated with nak (fixed key 1111..11, peer 4f355bdcb7cc..., plaintext 'nak vector' -> H3hKxnfd4MX/a9Rl0cvmFg==?iv=y8Ngg9dcK8XinwERJpzNsA==); format and legacy-rejection tests; raw-X symmetry/regression test vs hashed ECDH. - End-to-end verified both directions against a live signer daemon and nak decrypt --nip04 / nak encrypt --nip04. - Also includes pre-existing local changes: nip060/nip061, relay pool, cashu service, Cargo.lock.
This commit is contained in:
Generated
+9
-9
@@ -385,7 +385,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "event-signer"
|
||||
version = "0.0.3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nostr-core",
|
||||
"nostr-nips",
|
||||
@@ -868,7 +868,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "integration-tests"
|
||||
version = "0.0.3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nostr-core",
|
||||
"nostr-nips",
|
||||
@@ -910,7 +910,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "keypair-generator"
|
||||
version = "0.0.3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nostr-core",
|
||||
]
|
||||
@@ -1041,7 +1041,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-core"
|
||||
version = "0.0.3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64",
|
||||
@@ -1063,7 +1063,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-core-umbrella"
|
||||
version = "0.0.3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nostr-core",
|
||||
"nostr-nips",
|
||||
@@ -1074,7 +1074,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-nips"
|
||||
version = "0.0.3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"block-modes",
|
||||
@@ -1096,7 +1096,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-relay"
|
||||
version = "0.0.3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"nostr-core",
|
||||
@@ -1114,7 +1114,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-services"
|
||||
version = "0.0.3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nostr-core",
|
||||
"nostr-relay",
|
||||
@@ -1129,7 +1129,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-signer"
|
||||
version = "0.0.3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"cbc",
|
||||
|
||||
@@ -105,6 +105,37 @@ pub fn ecdh_shared_secret(
|
||||
Ok(result_even)
|
||||
}
|
||||
|
||||
/// Compute the raw X coordinate of the ECDH shared point, unhashed.
|
||||
///
|
||||
/// This is the key derivation required by NIP-04 (legacy encrypted direct
|
||||
/// messages): the AES-256 key is the X coordinate of `secret_key *
|
||||
/// public_key` as a curve point, with no hashing applied. Note that
|
||||
/// [`ecdh_shared_secret`] and [`ecdh_shared_secret_both`] apply libsecp256k1's
|
||||
/// default SHA256 hash over the compressed point and are used by NIP-44; do
|
||||
/// not mix the two derivations.
|
||||
pub fn ecdh_shared_secret_raw_x(
|
||||
secret_key: &SecretKey,
|
||||
public_key: &PublicKey,
|
||||
) -> NostrResult<[u8; 32]> {
|
||||
let sk = SecpSecretKey::from_slice(secret_key.as_bytes())
|
||||
.map_err(|_| NostrError::InvalidInput)?;
|
||||
|
||||
let xonly = XOnlyPublicKey::from_slice(public_key.as_bytes())
|
||||
.map_err(|_| NostrError::InvalidInput)?;
|
||||
|
||||
// NIP-04 does not specify a parity convention; the shared point is
|
||||
// computed against the full public key reconstructed with even parity
|
||||
// (the standard x-only reconstruction used throughout this crate).
|
||||
let pk = secp256k1::PublicKey::from_x_only_public_key(xonly, secp256k1::Parity::Even);
|
||||
|
||||
// Returns the raw (x, y) coordinates of the shared point, 32 bytes each,
|
||||
// with no hashing. The X coordinate is the first 32 bytes.
|
||||
let xy = secp256k1::ecdh::shared_secret_point(&pk, &sk);
|
||||
let mut result = [0u8; 32];
|
||||
result.copy_from_slice(&xy[..32]);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Compute both possible ECDH shared secrets (even and odd parity).
|
||||
/// Returns (even_parity_result, odd_parity_result).
|
||||
pub fn ecdh_shared_secret_both(
|
||||
|
||||
+182
-43
@@ -1,14 +1,24 @@
|
||||
//! NIP-04: Legacy Encrypted Direct Messages (AES-256-CBC).
|
||||
//!
|
||||
//! Provides encryption and decryption of direct messages using the legacy
|
||||
//! NIP-04 scheme (AES-256-CBC with base64 encoding).
|
||||
//! Implements the standard NIP-04 scheme as deployed by normal Nostr
|
||||
//! clients:
|
||||
//!
|
||||
//! * The AES-256 key is the **raw X coordinate** of the ECDH shared point
|
||||
//! (`secret_key * recipient_public_key`), unhashed.
|
||||
//! * The wire format is `base64(ciphertext)?iv=base64(iv)` — the ciphertext
|
||||
//! first, then a `?iv=` separator, then the base64-encoded IV.
|
||||
//!
|
||||
//! Any deviation from this (e.g. hashing the shared secret, or packing the
|
||||
//! IV in front of the ciphertext) produces messages that only the deviating
|
||||
//! implementation can read. See the known-answer tests below, generated with
|
||||
//! `nak`, for pinned interop vectors.
|
||||
|
||||
use cbc::{Decryptor, Encryptor};
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
|
||||
use nostr_core::crypto::keys::ecdh_shared_secret_both;
|
||||
use nostr_core::crypto::keys::ecdh_shared_secret_raw_x;
|
||||
use nostr_core::error::NostrError;
|
||||
use nostr_core::types::{PublicKey, SecretKey};
|
||||
use nostr_core::NostrResult;
|
||||
@@ -17,25 +27,21 @@ type Aes256CbcEnc = Encryptor<aes::Aes256>;
|
||||
type Aes256CbcDec = Decryptor<aes::Aes256>;
|
||||
|
||||
const IV_SIZE: usize = 16;
|
||||
const KEY_SIZE: usize = 32;
|
||||
|
||||
/// Derive a NIP-04 encryption key from an ECDH shared secret.
|
||||
fn derive_nip04_key(shared_secret: &[u8; 32]) -> [u8; KEY_SIZE] {
|
||||
use sha2::Digest;
|
||||
let hash = sha2::Sha256::digest(shared_secret);
|
||||
let mut key = [0u8; KEY_SIZE];
|
||||
key.copy_from_slice(&hash);
|
||||
key
|
||||
}
|
||||
/// Separator between the base64 ciphertext and the base64 IV, per NIP-04.
|
||||
const IV_SEPARATOR: &str = "?iv=";
|
||||
|
||||
/// Encrypt a plaintext using NIP-04 scheme.
|
||||
/// Encrypt a plaintext using the standard NIP-04 scheme.
|
||||
///
|
||||
/// Returns `base64(ciphertext)?iv=base64(iv)`.
|
||||
pub fn nip04_encrypt(
|
||||
sender_sk: &SecretKey,
|
||||
recipient_pk: &PublicKey,
|
||||
plaintext: &str,
|
||||
) -> NostrResult<String> {
|
||||
let (shared_even, _) = ecdh_shared_secret_both(sender_sk, recipient_pk)?;
|
||||
let key = derive_nip04_key(&shared_even);
|
||||
// Raw X coordinate of the shared point, unhashed — the NIP-04 AES key.
|
||||
// The X coordinate is parity-independent, so no parity handling needed.
|
||||
let key = ecdh_shared_secret_raw_x(sender_sk, recipient_pk)?;
|
||||
|
||||
let mut iv = [0u8; IV_SIZE];
|
||||
OsRng.fill_bytes(&mut iv);
|
||||
@@ -50,46 +56,48 @@ pub fn nip04_encrypt(
|
||||
.encrypt_padded_mut::<Pkcs7>(&mut buf, plaintext.len())
|
||||
.map_err(|_| NostrError::Nip04InvalidFormat)?;
|
||||
|
||||
let mut result = Vec::with_capacity(IV_SIZE + encrypted.len());
|
||||
result.extend_from_slice(&iv);
|
||||
result.extend_from_slice(encrypted);
|
||||
|
||||
Ok(nostr_core::util::base64_encode(&result))
|
||||
Ok(format!(
|
||||
"{}{}{}",
|
||||
nostr_core::util::base64_encode(encrypted),
|
||||
IV_SEPARATOR,
|
||||
nostr_core::util::base64_encode(&iv)
|
||||
))
|
||||
}
|
||||
|
||||
/// Decrypt a NIP-04 encrypted message.
|
||||
/// Decrypt a standard NIP-04 encrypted message.
|
||||
///
|
||||
/// Expects `base64(ciphertext)?iv=base64(iv)`. Returns
|
||||
/// [`NostrError::Nip04InvalidFormat`] if the `?iv=` separator is missing or
|
||||
/// either part fails to base64-decode.
|
||||
pub fn nip04_decrypt(
|
||||
recipient_sk: &SecretKey,
|
||||
sender_pk: &PublicKey,
|
||||
ciphertext_b64: &str,
|
||||
) -> NostrResult<String> {
|
||||
let (shared_even, shared_odd) = ecdh_shared_secret_both(recipient_sk, sender_pk)?;
|
||||
let (ct_b64, iv_b64) = ciphertext_b64
|
||||
.split_once(IV_SEPARATOR)
|
||||
.ok_or(NostrError::Nip04InvalidFormat)?;
|
||||
|
||||
let decoded = nostr_core::util::base64_decode(ciphertext_b64)?;
|
||||
let encrypted = nostr_core::util::base64_decode(ct_b64)?;
|
||||
let iv = nostr_core::util::base64_decode(iv_b64)?;
|
||||
|
||||
if decoded.len() < IV_SIZE + 16 {
|
||||
if iv.len() != IV_SIZE || encrypted.len() < 16 {
|
||||
return Err(NostrError::Nip04InvalidFormat);
|
||||
}
|
||||
|
||||
let iv = &decoded[..IV_SIZE];
|
||||
let encrypted = &decoded[IV_SIZE..];
|
||||
// Raw X coordinate of the shared point, unhashed — the NIP-04 AES key.
|
||||
// Parity-independent, so a single key derivation suffices.
|
||||
let key = ecdh_shared_secret_raw_x(recipient_sk, sender_pk)?;
|
||||
|
||||
// Try both parities
|
||||
for shared_secret in [shared_even, shared_odd] {
|
||||
let key = derive_nip04_key(&shared_secret);
|
||||
let cipher = match Aes256CbcDec::new_from_slices(&key, iv) {
|
||||
Ok(c) => c,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let mut buf = encrypted.to_vec();
|
||||
if let Ok(decrypted) = cipher.decrypt_padded_mut::<Pkcs7>(&mut buf) {
|
||||
if let Ok(text) = String::from_utf8(decrypted.to_vec()) {
|
||||
return Ok(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
let cipher = Aes256CbcDec::new_from_slices(&key, &iv)
|
||||
.map_err(|_| NostrError::Nip04InvalidFormat)?;
|
||||
|
||||
Err(NostrError::Nip04DecryptFailed)
|
||||
let mut buf = encrypted;
|
||||
let decrypted = cipher
|
||||
.decrypt_padded_mut::<Pkcs7>(&mut buf)
|
||||
.map_err(|_| NostrError::Nip04DecryptFailed)?;
|
||||
|
||||
String::from_utf8(decrypted.to_vec()).map_err(|_| NostrError::Nip04DecryptFailed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -110,12 +118,143 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_nip04_wrong_key() {
|
||||
let (sk_a, pk_a) = generate_keypair();
|
||||
let (sk_b, pk_b) = generate_keypair();
|
||||
let (sk_a, _pk_a) = generate_keypair();
|
||||
let (_sk_b, pk_b) = generate_keypair();
|
||||
let (sk_c, _) = generate_keypair();
|
||||
|
||||
let plaintext = "secret";
|
||||
let encrypted = nip04_encrypt(&sk_a, &pk_b, plaintext).unwrap();
|
||||
assert!(nip04_decrypt(&sk_c, &pk_b, &encrypted).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nip04_standard_format() {
|
||||
let (sk_a, pk_a) = generate_keypair();
|
||||
let (sk_b, pk_b) = generate_keypair();
|
||||
|
||||
let encrypted = nip04_encrypt(&sk_a, &pk_b, "format check").unwrap();
|
||||
|
||||
// Standard NIP-04 wire format: base64(ct)?iv=base64(iv)
|
||||
let (ct_b64, iv_b64) = encrypted
|
||||
.split_once(IV_SEPARATOR)
|
||||
.expect("output must contain the ?iv= separator");
|
||||
let iv = nostr_core::util::base64_decode(iv_b64).unwrap();
|
||||
assert_eq!(iv.len(), IV_SIZE);
|
||||
// Ciphertext must be PKCS7-padded AES blocks (multiple of 16).
|
||||
let ct = nostr_core::util::base64_decode(ct_b64).unwrap();
|
||||
assert_eq!(ct.len() % 16, 0);
|
||||
assert!(!ct.is_empty());
|
||||
|
||||
// The reverse direction must decrypt it (ECDH is symmetric).
|
||||
let decrypted = nip04_decrypt(&sk_b, &pk_a, &encrypted).unwrap();
|
||||
assert_eq!(decrypted, "format check");
|
||||
let _ = pk_a;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nip04_rejects_missing_iv_separator() {
|
||||
let (_sk_a, pk_a) = generate_keypair();
|
||||
let (sk_b, _pk_b) = generate_keypair();
|
||||
|
||||
// Legacy non-standard layout base64(IV || ct) — must be rejected.
|
||||
let legacy = nostr_core::util::base64_encode(&[0u8; 48]);
|
||||
assert_eq!(
|
||||
nip04_decrypt(&sk_b, &pk_a, &legacy),
|
||||
Err(NostrError::Nip04InvalidFormat)
|
||||
);
|
||||
}
|
||||
|
||||
// ── Known-answer vectors generated with `nak` (authoritative) ───────────
|
||||
//
|
||||
// Generated with:
|
||||
// nak key public 1111...11
|
||||
// -> 4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa
|
||||
// nak encrypt --nip04 --sec 1111...11 \
|
||||
// -p 4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa \
|
||||
// "nak vector"
|
||||
// -> H3hKxnfd4MX/a9Rl0cvmFg==?iv=y8Ngg9dcK8XinwERJpzNsA==
|
||||
//
|
||||
// The decrypt vector pins the raw-X (unhashed) key derivation and the
|
||||
// `base64(ct)?iv=base64(iv)` wire format against an independent
|
||||
// implementation. The encrypt test relies on the same shared key path,
|
||||
// so a regression to a hashed key or a packed IV layout fails here.
|
||||
|
||||
#[test]
|
||||
fn test_nip04_nak_known_answer_decrypt() {
|
||||
let sk = SecretKey::from_bytes(TEST_SK);
|
||||
let peer = PublicKey::from_bytes(TEST_PEER_PK);
|
||||
let decrypted = nip04_decrypt(&sk, &peer, NAK_CIPHERTEXT).unwrap();
|
||||
assert_eq!(decrypted, NAK_PLAINTEXT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nip04_nak_known_answer_encrypt() {
|
||||
use aes::cipher::KeyIvInit;
|
||||
|
||||
let sk = SecretKey::from_bytes(TEST_SK);
|
||||
let peer = PublicKey::from_bytes(TEST_PEER_PK);
|
||||
|
||||
// The IV is random per encryption, so a byte-for-byte comparison of
|
||||
// nip04_encrypt output against nak's output is impossible. Instead,
|
||||
// pin the key derivation: derive the raw-X key, encrypt the plaintext
|
||||
// with nak's IV, and the result must equal nak's ciphertext exactly.
|
||||
let key = ecdh_shared_secret_raw_x(&sk, &peer).unwrap();
|
||||
let iv = nostr_core::util::base64_decode(NAK_IV_B64).unwrap();
|
||||
|
||||
let mut buf = vec![0u8; NAK_PLAINTEXT.len() + 16 + 16];
|
||||
buf[..NAK_PLAINTEXT.len()].copy_from_slice(NAK_PLAINTEXT.as_bytes());
|
||||
let ct = Aes256CbcEnc::new_from_slices(&key, &iv)
|
||||
.unwrap()
|
||||
.encrypt_padded_mut::<Pkcs7>(&mut buf, NAK_PLAINTEXT.len())
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
nostr_core::util::base64_encode(ct),
|
||||
NAK_CT_B64,
|
||||
"raw-X key + nak IV must reproduce nak's ciphertext byte-for-byte"
|
||||
);
|
||||
|
||||
// And the full nip04_encrypt round-trip must hold.
|
||||
let encrypted = nip04_encrypt(&sk, &peer, NAK_PLAINTEXT).unwrap();
|
||||
assert!(encrypted.contains(IV_SEPARATOR));
|
||||
let roundtrip = nip04_decrypt(&sk, &peer, &encrypted).unwrap();
|
||||
assert_eq!(roundtrip, NAK_PLAINTEXT);
|
||||
}
|
||||
|
||||
// Fixed test keys (NOT used for anything real).
|
||||
const TEST_SK: [u8; 32] = [
|
||||
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
|
||||
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
|
||||
0x11, 0x11,
|
||||
];
|
||||
// `nak key public 1111...11`
|
||||
const TEST_PEER_PK: [u8; 32] = [
|
||||
0x4f, 0x35, 0x5b, 0xdc, 0xb7, 0xcc, 0x0a, 0xf7, 0x28, 0xef, 0x3c, 0xce, 0xb9, 0x61, 0x5d,
|
||||
0x90, 0x68, 0x4b, 0xb5, 0xb2, 0xca, 0x5f, 0x85, 0x9a, 0xb0, 0xf0, 0xb7, 0x04, 0x07, 0x58,
|
||||
0x71, 0xaa,
|
||||
];
|
||||
const NAK_PLAINTEXT: &str = "nak vector";
|
||||
// Full nak output: base64(ct)?iv=base64(iv)
|
||||
const NAK_CIPHERTEXT: &str = "H3hKxnfd4MX/a9Rl0cvmFg==?iv=y8Ngg9dcK8XinwERJpzNsA==";
|
||||
// Just the ciphertext part, for the same-IV comparison in the encrypt test.
|
||||
const NAK_CT_B64: &str = "H3hKxnfd4MX/a9Rl0cvmFg==";
|
||||
// Just the IV part, for the same-IV comparison in the encrypt test.
|
||||
const NAK_IV_B64: &str = "y8Ngg9dcK8XinwERJpzNsA==";
|
||||
|
||||
#[test]
|
||||
fn test_nip04_raw_x_key_symmetric_and_unhashed() {
|
||||
use nostr_core::crypto::keys::{ecdh_shared_secret, ecdh_shared_secret_raw_x};
|
||||
|
||||
let (sk_a, pk_a) = generate_keypair();
|
||||
let (sk_b, pk_b) = generate_keypair();
|
||||
|
||||
// ECDH is symmetric: both parties must derive the same raw-X key.
|
||||
let key_ab = ecdh_shared_secret_raw_x(&sk_a, &pk_b).unwrap();
|
||||
let key_ba = ecdh_shared_secret_raw_x(&sk_b, &pk_a).unwrap();
|
||||
assert_eq!(key_ab, key_ba);
|
||||
|
||||
// Regression guard: the raw-X key must NOT equal the hashed ECDH
|
||||
// secret used by NIP-44 (the old bug double-hashed this value).
|
||||
let hashed = ecdh_shared_secret(&sk_a, &pk_b).unwrap();
|
||||
assert_ne!(key_ab, hashed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ use nostr_core::types::{Event, Kind, PublicKey};
|
||||
use nostr_core::NostrResult;
|
||||
|
||||
/// A Cashu proof.
|
||||
///
|
||||
/// Field names `C`/`B` are uppercase to match the Cashu protocol JSON.
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct CashuProof {
|
||||
pub id: String,
|
||||
|
||||
@@ -23,6 +23,9 @@ pub struct Nutzap {
|
||||
}
|
||||
|
||||
/// A Nutzap proof.
|
||||
///
|
||||
/// Field name `C` is uppercase to match the Cashu protocol JSON.
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct NutzapProof {
|
||||
pub id: String,
|
||||
|
||||
@@ -20,10 +20,7 @@ use crate::ws::{NostrWsClient, WsState};
|
||||
// ── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
const MAX_RELAYS: usize = 32;
|
||||
const MAX_SUBSCRIPTIONS: usize = 64;
|
||||
const MAX_SEEN_EVENTS: usize = 1000;
|
||||
const DEFAULT_TIMEOUT_MS: u64 = 5000;
|
||||
const PING_INTERVAL_SECS: u64 = 59;
|
||||
|
||||
// ── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -142,15 +139,12 @@ impl RelayEntry {
|
||||
/// A pool subscription.
|
||||
pub struct PoolSubscription {
|
||||
id: SubscriptionId,
|
||||
filters: Vec<Filter>,
|
||||
relay_urls: Vec<String>,
|
||||
on_event: Option<EventCallback>,
|
||||
on_eose: Option<EoseCallback>,
|
||||
close_on_eose: bool,
|
||||
enable_deduplication: bool,
|
||||
result_mode: EoseResultMode,
|
||||
relay_timeout_seconds: u64,
|
||||
eose_timeout_seconds: u64,
|
||||
/// Track which relays have sent EOSE.
|
||||
eose_received: Arc<Mutex<Vec<String>>>,
|
||||
/// Track received event IDs for deduplication.
|
||||
@@ -164,27 +158,21 @@ pub struct PoolSubscription {
|
||||
impl PoolSubscription {
|
||||
fn new(
|
||||
id: SubscriptionId,
|
||||
filters: Vec<Filter>,
|
||||
relay_urls: Vec<String>,
|
||||
on_event: Option<EventCallback>,
|
||||
on_eose: Option<EoseCallback>,
|
||||
close_on_eose: bool,
|
||||
enable_deduplication: bool,
|
||||
result_mode: EoseResultMode,
|
||||
relay_timeout_seconds: u64,
|
||||
eose_timeout_seconds: u64,
|
||||
) -> Self {
|
||||
PoolSubscription {
|
||||
id,
|
||||
filters,
|
||||
relay_urls,
|
||||
on_event,
|
||||
on_eose,
|
||||
close_on_eose,
|
||||
enable_deduplication,
|
||||
result_mode,
|
||||
relay_timeout_seconds,
|
||||
eose_timeout_seconds,
|
||||
eose_received: Arc::new(Mutex::new(Vec::new())),
|
||||
seen_events: Arc::new(Mutex::new(Vec::new())),
|
||||
events: Arc::new(Mutex::new(Vec::new())),
|
||||
@@ -407,22 +395,17 @@ impl RelayPool {
|
||||
close_on_eose: bool,
|
||||
enable_deduplication: bool,
|
||||
result_mode: EoseResultMode,
|
||||
relay_timeout_seconds: u64,
|
||||
eose_timeout_seconds: u64,
|
||||
) -> NostrResult<Arc<PoolSubscription>> {
|
||||
let id = SubscriptionId::new(uuid::Uuid::new_v4().to_string());
|
||||
|
||||
let subscription = Arc::new(PoolSubscription::new(
|
||||
id.clone(),
|
||||
filters.clone(),
|
||||
relay_urls.to_vec(),
|
||||
on_event,
|
||||
on_eose,
|
||||
close_on_eose,
|
||||
enable_deduplication,
|
||||
result_mode,
|
||||
relay_timeout_seconds,
|
||||
eose_timeout_seconds,
|
||||
));
|
||||
|
||||
// Register the subscription
|
||||
@@ -903,8 +886,6 @@ impl RelayPool {
|
||||
true, // close_on_eose
|
||||
true, // enable_deduplication
|
||||
EoseResultMode::FullSet,
|
||||
5, // relay_timeout_seconds
|
||||
10, // eose_timeout_seconds
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1053,8 +1034,6 @@ mod tests {
|
||||
true,
|
||||
true,
|
||||
EoseResultMode::FullSet,
|
||||
5,
|
||||
10,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -63,6 +63,9 @@ pub struct CashuMeltQuote {
|
||||
}
|
||||
|
||||
/// A blinded message submitted to the mint for minting or swapping.
|
||||
///
|
||||
/// Field name `B` is uppercase to match the Cashu protocol JSON.
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct BlindedMessage {
|
||||
pub amount: u64,
|
||||
@@ -71,6 +74,9 @@ pub struct BlindedMessage {
|
||||
}
|
||||
|
||||
/// A blind signature returned by the mint.
|
||||
///
|
||||
/// Field name `C` is uppercase to match the Cashu protocol JSON.
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct BlindSignature {
|
||||
pub amount: u64,
|
||||
@@ -85,6 +91,9 @@ pub struct MintResponse {
|
||||
}
|
||||
|
||||
/// A Cashu proof (token) that can be spent or swapped.
|
||||
///
|
||||
/// Field name `C` is uppercase to match the Cashu protocol JSON.
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Proof {
|
||||
pub amount: u64,
|
||||
|
||||
Reference in New Issue
Block a user