use blvm_node::config::PaymentConfig;
use blvm_node::module::registry::manifest::ModuleManifest;
use blvm_node::module::registry::manifest::{MaintainerSignature, SignatureSection};
use blvm_node::payment::processor::{PaymentError, PaymentProcessor};
use blvm_protocol::payment::{Payment, PaymentOutput};
use secp256k1::{Secp256k1, SecretKey};
use sha2::{Digest, Sha256};
fn default_payment_config() -> PaymentConfig {
PaymentConfig::default()
}
fn apply_valid_payment_signature(
manifest: &mut ModuleManifest,
author_address: &str,
commons_address: &str,
price_sats: u64,
) {
let secp = Secp256k1::new();
let test_key = SecretKey::from_slice(&[1; 32]).expect("test key");
let message_data = format!("{author_address}||{commons_address}||{price_sats}");
let message_hash = Sha256::digest(message_data.as_bytes());
let message = secp256k1::Message::from_digest_slice(&message_hash).expect("message");
let signature = secp.sign_ecdsa(&message, &test_key);
let signature_hex = hex::encode(signature.serialize_compact());
let pubkey = secp256k1::PublicKey::from_secret_key(&secp, &test_key);
let pubkey_hex = hex::encode(pubkey.serialize());
manifest.signatures = Some(SignatureSection {
maintainers: vec![MaintainerSignature {
name: "test-maintainer".to_string(),
public_key: pubkey_hex,
signature: "dummy".to_string(),
}],
threshold: Some("1-of-1".to_string()),
});
manifest
.payment
.as_mut()
.expect("payment section")
.payment_signature = Some(signature_hex);
}
#[tokio::test]
async fn test_create_payment_request() {
let config = default_payment_config();
let processor = PaymentProcessor::new(config).expect("Failed to create payment processor");
let outputs = vec![PaymentOutput {
script: vec![0x51, 0x00], amount: Some(100000),
}];
let merchant_data = Some(b"test_merchant_data".to_vec());
let payment_request = processor
.create_payment_request(outputs.clone(), merchant_data, None)
.await
.expect("Failed to create payment request");
assert_eq!(payment_request.payment_details.outputs.len(), 1);
assert_eq!(
payment_request.payment_details.outputs[0].amount,
Some(100000)
);
assert_eq!(
payment_request.payment_details.merchant_data,
Some(b"test_merchant_data".to_vec())
);
assert!(payment_request.payment_details.time > 0);
}
#[tokio::test]
async fn test_process_payment() {
let config = default_payment_config();
let processor = PaymentProcessor::new(config).expect("Failed to create payment processor");
let outputs = vec![PaymentOutput {
script: vec![0x51, 0x00],
amount: Some(100000),
}];
let payment_request = processor
.create_payment_request(outputs, None, None)
.await
.expect("Failed to create payment request");
use sha2::{Digest, Sha256};
let serialized = bincode::serialize(&payment_request).unwrap_or_default();
let hash = Sha256::digest(&serialized);
let payment_id = hex::encode(&hash[..16]);
let mut payment = Payment::new(vec![]); payment.merchant_data = payment_request.payment_details.merchant_data.clone();
let result = processor.process_payment(payment, payment_id, None).await;
assert!(result.is_err());
match result.unwrap_err() {
PaymentError::ValidationFailed(_) => {
}
_ => panic!("Expected ValidationFailed error for empty transactions"),
}
}
#[tokio::test]
async fn test_bip47_derivation_error_paths() {
let config = default_payment_config();
let processor = PaymentProcessor::new(config).expect("Failed to create payment processor");
use blvm_node::module::registry::manifest::PaymentSection;
let mut manifest = ModuleManifest {
name: "test_module".to_string(),
version: "1.0.0".to_string(),
entry_point: "test".to_string(),
description: None,
author: None,
capabilities: vec![],
dependencies: std::collections::HashMap::new(),
optional_dependencies: std::collections::HashMap::new(),
config_schema: std::collections::HashMap::new(),
signatures: None,
binary: None,
payment: Some(PaymentSection {
required: true,
price_sats: Some(10000),
author_payment_code: Some("PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVgc2NZK5LXiAtWVt2SN3AoRcTHMihqVh2V9Gns5T7HHmNq".to_string()),
author_address: None, commons_payment_code: Some("PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVgc2NZK5LXiAtWVt2SN3AoRcTHMihqVh2V9Gns5T7HHmNq".to_string()),
commons_address: None, payment_signature: None,
}),
};
let module_hash = [0u8; 32];
let node_script = vec![0x51, 0x00];
let result = processor
.create_module_payment_request(&manifest, &module_hash, node_script.clone(), None)
.await;
assert!(
result.is_err(),
"Should fail when BIP47 derivation fails and no legacy address provided"
);
match result.unwrap_err() {
PaymentError::ProcessingError(msg) => {
assert!(
msg.contains("BIP47 payment code provided but derivation failed")
|| msg.contains("legacy address not provided"),
"Error message should mention BIP47 derivation failure or missing legacy address"
);
}
_ => panic!("Expected ProcessingError for BIP47 without fallback"),
}
let author_leg = "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh";
let commons_leg = "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh";
manifest.payment.as_mut().unwrap().author_address = Some(author_leg.to_string());
manifest.payment.as_mut().unwrap().commons_address = Some(commons_leg.to_string());
let price_sats = manifest.payment.as_ref().unwrap().price_sats.unwrap_or(0);
apply_valid_payment_signature(&mut manifest, author_leg, commons_leg, price_sats);
let result = processor
.create_module_payment_request(&manifest, &module_hash, node_script.clone(), None)
.await;
assert!(
result.is_ok(),
"Should succeed with legacy address fallback"
);
let payment_request = result.unwrap();
assert!(
!payment_request.payment_details.outputs.is_empty(),
"Payment request should have outputs"
);
manifest.payment.as_mut().unwrap().author_payment_code = None;
manifest.payment.as_mut().unwrap().commons_payment_code = None;
apply_valid_payment_signature(&mut manifest, author_leg, commons_leg, price_sats);
let result = processor
.create_module_payment_request(&manifest, &module_hash, node_script.clone(), None)
.await;
assert!(result.is_ok(), "Should succeed with legacy address only");
let payment_request = result.unwrap();
assert!(
!payment_request.payment_details.outputs.is_empty(),
"Payment request should have outputs"
);
manifest.payment.as_mut().unwrap().author_address = None;
manifest.payment.as_mut().unwrap().commons_address = None;
let result = processor
.create_module_payment_request(&manifest, &module_hash, node_script, None)
.await;
assert!(
result.is_err(),
"Should fail when no address or payment code provided"
);
match result.unwrap_err() {
PaymentError::ProcessingError(msg) => {
assert!(
msg.contains("payment address or payment code not specified"),
"Error message should mention missing address or payment code"
);
}
_ => panic!("Expected ProcessingError for missing address"),
}
}
#[tokio::test]
async fn test_payment_request_not_found() {
let config = default_payment_config();
let processor = PaymentProcessor::new(config).expect("Failed to create payment processor");
let result = processor.get_payment_request("nonexistent_id").await;
assert!(result.is_err());
match result.unwrap_err() {
PaymentError::RequestNotFound(_) => {}
_ => panic!("Expected RequestNotFound error"),
}
}
#[tokio::test]
async fn test_payment_processor_config_validation() {
let mut config = PaymentConfig::default();
config.p2p_enabled = false;
config.http_enabled = true;
#[cfg(not(feature = "bip70-http"))]
{
let result = PaymentProcessor::new(config);
assert!(result.is_err());
if let Err(PaymentError::FeatureNotEnabled(_)) = result {
} else {
assert!(result.is_err());
}
}
#[cfg(feature = "bip70-http")]
{
let result = PaymentProcessor::new(config);
assert!(result.is_ok());
}
}
#[tokio::test]
async fn test_payment_processor_no_transport_enabled() {
let mut config = PaymentConfig::default();
config.p2p_enabled = false;
config.http_enabled = false;
let result = PaymentProcessor::new(config);
assert!(result.is_err());
let err = match result {
Err(e) => e,
Ok(_) => panic!("Expected error but got Ok"),
};
match err {
PaymentError::NoTransportEnabled => {}
_ => panic!("Expected NoTransportEnabled error"),
}
}
#[tokio::test]
async fn test_payment_processor_p2p_only() {
let config = default_payment_config();
let processor = PaymentProcessor::new(config).expect("Failed to create payment processor");
let outputs = vec![PaymentOutput {
script: vec![0x51, 0x00],
amount: Some(100000),
}];
let _request = processor
.create_payment_request(outputs, None, None)
.await
.expect("Failed to create payment request");
}
#[tokio::test]
async fn test_payment_id_generation() {
let config = default_payment_config();
let processor = PaymentProcessor::new(config).expect("Failed to create payment processor");
let outputs = vec![PaymentOutput {
script: vec![0x51, 0x00],
amount: Some(100000),
}];
let req1 = processor
.create_payment_request(outputs.clone(), None, None)
.await
.expect("Failed to create payment request 1");
let req2 = processor
.create_payment_request(outputs.clone(), Some(b"different".to_vec()), None)
.await
.expect("Failed to create payment request 2");
use sha2::{Digest, Sha256};
let serialized1 = bincode::serialize(&req1).unwrap_or_default();
let hash1 = Sha256::digest(&serialized1);
let id1 = hex::encode(&hash1[..16]);
let serialized2 = bincode::serialize(&req2).unwrap_or_default();
let hash2 = Sha256::digest(&serialized2);
let id2 = hex::encode(&hash2[..16]);
assert_ne!(id1, id2);
assert_eq!(id1.len(), 32); }
#[tokio::test]
async fn test_payment_request_storage() {
let config = default_payment_config();
let processor = PaymentProcessor::new(config).expect("Failed to create payment processor");
let outputs = vec![PaymentOutput {
script: vec![0x51, 0x00],
amount: Some(100000),
}];
let payment_request = processor
.create_payment_request(outputs, None, None)
.await
.expect("Failed to create payment request");
use sha2::{Digest, Sha256};
let serialized = bincode::serialize(&payment_request).unwrap_or_default();
let hash = Sha256::digest(&serialized);
let payment_id = hex::encode(&hash[..16]);
let retrieved = processor
.get_payment_request(&payment_id)
.await
.expect("Failed to retrieve payment request");
assert_eq!(
retrieved.payment_details.outputs[0].amount,
payment_request.payment_details.outputs[0].amount
);
}