Skip to main content

blvm_protocol/
payment.rs

1//! BIP70: Payment Protocol (P2P Variant)
2//!
3//! Specification: https://github.com/bitcoin/bips/blob/master/bip-0070.mediawiki
4//!
5//! This is a P2P-based variant of BIP70 that addresses security concerns:
6//! - Uses Bitcoin P2P network instead of HTTP/HTTPS (privacy-preserving)
7//! - Uses Bitcoin public key signatures instead of X.509 certificates (decentralized)
8//! - Supports signed refund addresses (prevents refund attacks)
9//! - Works with TCP, Iroh, and QUIC transports
10//!
11//! Core messages:
12//! - PaymentRequest: Merchant payment details signed with Bitcoin key
13//! - Payment: Customer payment transaction(s)
14//! - PaymentACK: Merchant confirmation of payment
15//!
16//! Security enhancements:
17//! - Merchant authentication via Bitcoin public keys (on-chain verifiable)
18//! - Signed refund addresses prevent refund attacks
19//! - P2P routing preserves customer privacy (no direct merchant connection)
20
21use crate::Hash;
22use blvm_secp256k1::ecdsa::{
23    ecdsa_sig_parse_compact, ecdsa_sig_verify, ecdsa_sign_compact_rfc6979, ge_from_pubkey_bytes,
24    ge_to_compressed, pubkey_from_secret, verify_ecdsa_direct,
25};
26use blvm_secp256k1::scalar::Scalar;
27use hex;
28use serde::{Deserialize, Serialize};
29use sha2::{Digest, Sha256};
30
31// ---------------------------------------------------------------------------
32// Internal helpers: sign / verify using blvm-secp256k1
33// ---------------------------------------------------------------------------
34
35/// SHA-256 hash → compact ECDSA signature (64 bytes, low-S, RFC 6979 nonce).
36fn sign_compact(hash: &[u8; 32], seckey: &[u8; 32]) -> Result<Vec<u8>, Bip70Error> {
37    ecdsa_sign_compact_rfc6979(hash, seckey)
38        .map(|s| s.to_vec())
39        .ok_or_else(|| Bip70Error::SignatureError("ECDSA signing failed".to_string()))
40}
41
42/// Verify compact (64-byte) or DER signature against `hash` and `pubkey`.
43fn verify_sig(sig_bytes: &[u8], pubkey: &[u8], hash: &[u8; 32]) -> Result<(), Bip70Error> {
44    let valid = if sig_bytes.len() == 64 {
45        let compact: &[u8; 64] = sig_bytes
46            .try_into()
47            .map_err(|_| Bip70Error::SignatureError("Invalid signature length".to_string()))?;
48        let (sigr, sigs) = ecdsa_sig_parse_compact(compact)
49            .ok_or_else(|| Bip70Error::SignatureError("Invalid compact signature".to_string()))?;
50        let pk = ge_from_pubkey_bytes(pubkey)
51            .ok_or_else(|| Bip70Error::SignatureError("Invalid public key".to_string()))?;
52        let mut msg = Scalar::zero();
53        let _ = msg.set_b32(hash);
54        ecdsa_sig_verify(&sigr, &sigs, &pk, &msg)
55    } else {
56        verify_ecdsa_direct(sig_bytes, pubkey, hash, false, false)
57            .ok_or_else(|| Bip70Error::SignatureError("Signature parse error".to_string()))?
58    };
59    if valid {
60        Ok(())
61    } else {
62        Err(Bip70Error::SignatureError(
63            "Signature verification failed".to_string(),
64        ))
65    }
66}
67
68/// Derive compressed public key bytes (33 bytes) from a raw secret key scalar.
69fn pubkey_bytes_from_secret(seckey: &[u8; 32]) -> Result<[u8; 33], Bip70Error> {
70    let mut sec = Scalar::zero();
71    if sec.set_b32(seckey) || sec.is_zero() {
72        return Err(Bip70Error::SignatureError("Invalid secret key".to_string()));
73    }
74    let ge = pubkey_from_secret(&sec);
75    Ok(ge_to_compressed(&ge))
76}
77
78/// BIP70 Payment Protocol version
79pub const PAYMENT_PROTOCOL_VERSION: u32 = 1;
80
81/// Payment Details - Core payment information
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct PaymentDetails {
84    /// Network identifier (mainnet, testnet, regtest)
85    pub network: String,
86    /// Payment outputs (address, amount)
87    pub outputs: Vec<PaymentOutput>,
88    /// Payment expiration time (Unix timestamp)
89    pub time: u64,
90    /// Payment expiration time
91    pub expires: Option<u64>,
92    /// Memo for merchant
93    pub memo: Option<String>,
94    /// Memo for customer
95    pub payment_url: Option<String>,
96    /// Merchant data (opaque to customer)
97    pub merchant_data: Option<Vec<u8>>,
98}
99
100/// Payment Output - Address and amount
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct PaymentOutput {
103    /// Bitcoin address or script
104    pub script: Vec<u8>,
105    /// Amount in satoshis (None = all available)
106    pub amount: Option<u64>,
107}
108
109/// Signed refund address - Pre-authorized refund address with merchant signature
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct SignedRefundAddress {
112    /// Refund address/script
113    pub address: PaymentOutput,
114    /// Merchant signature over address (prevents refund attacks)
115    pub signature: Vec<u8>,
116}
117
118/// Payment Request - Main payment protocol message
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct PaymentRequest {
121    /// Payment details
122    pub payment_details: PaymentDetails,
123    /// Merchant's Bitcoin public key (compressed, 33 bytes)
124    /// Replaces X.509 certificates with on-chain verifiable keys
125    pub merchant_pubkey: Option<Vec<u8>>,
126    /// Signature over payment_details by merchant's Bitcoin key
127    pub signature: Option<Vec<u8>>,
128    /// Pre-authorized refund addresses (signed by merchant)
129    /// Prevents refund address attacks by requiring merchant signature
130    pub authorized_refund_addresses: Option<Vec<SignedRefundAddress>>,
131}
132
133/// Payment - Customer payment transaction(s)
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct Payment {
136    /// Serialized transaction(s)
137    pub transactions: Vec<Vec<u8>>,
138    /// Refund addresses (if change needed)
139    pub refund_to: Option<Vec<PaymentOutput>>,
140    /// Merchant data (echo back from PaymentRequest)
141    pub merchant_data: Option<Vec<u8>>,
142    /// Memo from customer
143    pub memo: Option<String>,
144}
145
146/// Payment ACK - Merchant confirmation
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct PaymentACK {
149    /// Original payment message
150    pub payment: Payment,
151    /// Confirmation memo from merchant
152    pub memo: Option<String>,
153    /// Merchant signature over PaymentACK (optional)
154    pub signature: Option<Vec<u8>>,
155}
156
157impl PaymentACK {
158    /// Sign PaymentACK with merchant's private key (raw 32-byte scalar).
159    pub fn sign(&mut self, private_key: &[u8; 32]) -> Result<(), Bip70Error> {
160        let mut ack_for_signing = self.clone();
161        ack_for_signing.signature = None;
162        let serialized = bincode::serialize(&ack_for_signing)
163            .map_err(|e| Bip70Error::SerializationError(e.to_string()))?;
164        let hash: [u8; 32] = Sha256::digest(&serialized).into();
165        self.signature = Some(sign_compact(&hash, private_key)?);
166        Ok(())
167    }
168
169    /// Verify PaymentACK signature.
170    pub fn verify_signature(&self, merchant_pubkey: &[u8]) -> Result<(), Bip70Error> {
171        let sig = self
172            .signature
173            .as_ref()
174            .ok_or_else(|| Bip70Error::SignatureError("No signature".to_string()))?;
175        let mut ack = self.clone();
176        ack.signature = None;
177        let serialized =
178            bincode::serialize(&ack).map_err(|e| Bip70Error::SerializationError(e.to_string()))?;
179        let hash: [u8; 32] = Sha256::digest(&serialized).into();
180        verify_sig(sig, merchant_pubkey, &hash).map_err(|_| {
181            Bip70Error::SignatureError("PaymentACK signature verification failed".to_string())
182        })
183    }
184}
185
186impl PaymentRequest {
187    /// Create a new payment request
188    pub fn new(network: String, outputs: Vec<PaymentOutput>, time: u64) -> Self {
189        Self {
190            payment_details: PaymentDetails {
191                network,
192                outputs,
193                time,
194                expires: None,
195                memo: None,
196                payment_url: None,
197                merchant_data: None,
198            },
199            merchant_pubkey: None,
200            signature: None,
201            authorized_refund_addresses: None,
202        }
203    }
204
205    /// Set merchant public key
206    pub fn with_merchant_key(mut self, pubkey: [u8; 33]) -> Self {
207        self.merchant_pubkey = Some(pubkey.to_vec());
208        self
209    }
210
211    /// Add authorized refund address (signed by merchant)
212    pub fn with_authorized_refund(mut self, signed_refund: SignedRefundAddress) -> Self {
213        if self.authorized_refund_addresses.is_none() {
214            self.authorized_refund_addresses = Some(Vec::new());
215        }
216        self.authorized_refund_addresses
217            .as_mut()
218            .unwrap()
219            .push(signed_refund);
220        self
221    }
222
223    /// Set expiration time
224    pub fn with_expires(mut self, expires: u64) -> Self {
225        self.payment_details.expires = Some(expires);
226        self
227    }
228
229    /// Set memo for merchant
230    pub fn with_memo(mut self, memo: String) -> Self {
231        self.payment_details.memo = Some(memo);
232        self
233    }
234
235    /// Set payment URL (where to send Payment message)
236    pub fn with_payment_url(mut self, url: String) -> Self {
237        self.payment_details.payment_url = Some(url);
238        self
239    }
240
241    /// Set merchant data (opaque customer data)
242    pub fn with_merchant_data(mut self, data: Vec<u8>) -> Self {
243        self.payment_details.merchant_data = Some(data);
244        self
245    }
246
247    /// Sign payment request with merchant's private key (raw 32-byte scalar).
248    pub fn sign(&mut self, private_key: &[u8; 32]) -> Result<(), Bip70Error> {
249        let serialized = bincode::serialize(&self.payment_details)
250            .map_err(|e| Bip70Error::SerializationError(e.to_string()))?;
251        let hash: [u8; 32] = Sha256::digest(&serialized).into();
252        let pubkey = pubkey_bytes_from_secret(private_key)?;
253        self.signature = Some(sign_compact(&hash, private_key)?);
254        self.merchant_pubkey = Some(pubkey.to_vec());
255        Ok(())
256    }
257
258    /// Verify payment request signature.
259    pub fn verify_signature(&self) -> Result<(), Bip70Error> {
260        let pubkey = self
261            .merchant_pubkey
262            .as_ref()
263            .ok_or_else(|| Bip70Error::SignatureError("No merchant public key".to_string()))?;
264        let sig = self
265            .signature
266            .as_ref()
267            .ok_or_else(|| Bip70Error::SignatureError("No signature".to_string()))?;
268        let serialized = bincode::serialize(&self.payment_details)
269            .map_err(|e| Bip70Error::SerializationError(e.to_string()))?;
270        let hash: [u8; 32] = Sha256::digest(&serialized).into();
271        verify_sig(sig, pubkey, &hash)
272            .map_err(|_| Bip70Error::SignatureError("Signature verification failed".to_string()))
273    }
274
275    /// Validate payment request
276    pub fn validate(&self) -> Result<(), Bip70Error> {
277        // Check expiration
278        if let Some(expires) = self.payment_details.expires {
279            let now = crate::time::current_timestamp();
280            if now > expires {
281                return Err(Bip70Error::Expired);
282            }
283        }
284
285        // Validate outputs
286        if self.payment_details.outputs.is_empty() {
287            return Err(Bip70Error::InvalidRequest("No payment outputs".to_string()));
288        }
289
290        // Validate network
291        let valid_networks = ["main", "test", "regtest"];
292        if !valid_networks.contains(&self.payment_details.network.as_str()) {
293            return Err(Bip70Error::InvalidRequest(format!(
294                "Invalid network: {}",
295                self.payment_details.network
296            )));
297        }
298
299        Ok(())
300    }
301}
302
303impl Payment {
304    /// Create a new payment
305    pub fn new(transactions: Vec<Vec<u8>>) -> Self {
306        Self {
307            transactions,
308            refund_to: None,
309            merchant_data: None,
310            memo: None,
311        }
312    }
313
314    /// Add refund address (must be pre-authorized in PaymentRequest)
315    pub fn with_refund_to(mut self, outputs: Vec<PaymentOutput>) -> Self {
316        self.refund_to = Some(outputs);
317        self
318    }
319
320    /// Validate refund addresses against PaymentRequest authorized list
321    pub fn validate_refund_addresses(
322        &self,
323        authorized_refunds: &[SignedRefundAddress],
324    ) -> Result<(), Bip70Error> {
325        if let Some(ref refund_to) = self.refund_to {
326            for refund_addr in refund_to {
327                // Check if refund address is in authorized list
328                let is_authorized = authorized_refunds.iter().any(|auth| {
329                    auth.address.script == refund_addr.script
330                        && auth.address.amount == refund_addr.amount
331                });
332
333                if !is_authorized {
334                    return Err(Bip70Error::InvalidPayment(format!(
335                        "Refund address not authorized: {:?}",
336                        refund_addr.script
337                    )));
338                }
339            }
340        }
341        Ok(())
342    }
343
344    /// Set merchant data (echo from PaymentRequest)
345    pub fn with_merchant_data(mut self, data: Vec<u8>) -> Self {
346        self.merchant_data = Some(data);
347        self
348    }
349
350    /// Set customer memo
351    pub fn with_memo(mut self, memo: String) -> Self {
352        self.memo = Some(memo);
353        self
354    }
355
356    /// Validate payment
357    pub fn validate(&self) -> Result<(), Bip70Error> {
358        if self.transactions.is_empty() {
359            return Err(Bip70Error::InvalidPayment("No transactions".to_string()));
360        }
361
362        Ok(())
363    }
364}
365
366/// BIP70 Error types
367#[derive(Debug, thiserror::Error)]
368pub enum Bip70Error {
369    #[error("Payment request expired")]
370    Expired,
371
372    #[error("Invalid payment request: {0}")]
373    InvalidRequest(String),
374
375    #[error("Invalid payment: {0}")]
376    InvalidPayment(String),
377
378    #[error("Certificate validation failed: {0}")]
379    CertificateError(String),
380
381    #[error("Signature verification failed: {0}")]
382    SignatureError(String),
383
384    #[error("HTTP error: {0}")]
385    HttpError(String),
386
387    #[error("Serialization error: {0}")]
388    SerializationError(String),
389
390    #[error("Validation error: {0}")]
391    ValidationError(String),
392}
393
394/// BIP70 Payment Protocol client (for making payments via P2P)
395///
396/// Note: Node-specific message creation functions are in blvm-node.
397/// This struct provides protocol-level validation only.
398pub struct PaymentProtocolClient;
399
400impl PaymentProtocolClient {
401    /// Validate a PaymentRequest (protocol-level validation)
402    pub fn validate_payment_request(
403        payment_request: &PaymentRequest,
404        expected_merchant_pubkey: Option<&[u8]>,
405    ) -> Result<(), Bip70Error> {
406        // Verify signature
407        payment_request.verify_signature()?;
408
409        // Validate payment request
410        payment_request.validate()?;
411
412        // Verify merchant pubkey matches if provided
413        if let Some(expected_pubkey) = expected_merchant_pubkey {
414            if let Some(ref req_pubkey) = payment_request.merchant_pubkey {
415                if req_pubkey.as_slice() != expected_pubkey {
416                    return Err(Bip70Error::SignatureError(
417                        "PaymentRequest pubkey mismatch".to_string(),
418                    ));
419                }
420            }
421        }
422
423        Ok(())
424    }
425
426    /// Validate PaymentACK from merchant (protocol-level validation)
427    pub fn validate_payment_ack(
428        payment_ack: &PaymentACK,
429        merchant_signature: &[u8],
430        merchant_pubkey: &[u8],
431    ) -> Result<(), Bip70Error> {
432        if let Some(ref sig) = payment_ack.signature {
433            if !sig.is_empty() {
434                return payment_ack.verify_signature(merchant_pubkey);
435            }
436        }
437        // Legacy: external signature
438        if !merchant_signature.is_empty() {
439            let mut ack = payment_ack.clone();
440            ack.signature = None;
441            let serialized = bincode::serialize(&ack)
442                .map_err(|e| Bip70Error::SerializationError(e.to_string()))?;
443            let hash: [u8; 32] = Sha256::digest(&serialized).into();
444            verify_sig(merchant_signature, merchant_pubkey, &hash)?;
445        }
446        Ok(())
447    }
448}
449
450/// BIP70 Payment Protocol server (for receiving payments via P2P)
451///
452/// Note: Node-specific message creation functions are in blvm-node.
453/// This struct provides protocol-level processing only.
454pub struct PaymentProtocolServer;
455
456impl PaymentProtocolServer {
457    /// Create signed payment request (protocol-level).
458    pub fn create_signed_payment_request(
459        details: PaymentDetails,
460        merchant_private_key: &[u8; 32],
461        authorized_refunds: Option<Vec<SignedRefundAddress>>,
462    ) -> Result<PaymentRequest, Bip70Error> {
463        let mut payment_request = PaymentRequest {
464            payment_details: details,
465            merchant_pubkey: None,
466            signature: None,
467            authorized_refund_addresses: authorized_refunds,
468        };
469        payment_request.sign(merchant_private_key)?;
470        Ok(payment_request)
471    }
472
473    /// Process incoming payment (protocol-level validation and ACK creation).
474    pub fn process_payment(
475        payment: &Payment,
476        original_request: &PaymentRequest,
477        merchant_private_key: Option<&[u8; 32]>,
478    ) -> Result<PaymentACK, Bip70Error> {
479        payment.validate()?;
480        if let Some(ref authorized_refunds) = original_request.authorized_refund_addresses {
481            payment.validate_refund_addresses(authorized_refunds)?;
482        }
483        if let Some(ref pm_data) = payment.merchant_data {
484            if let Some(ref req_data) = original_request.payment_details.merchant_data {
485                if pm_data != req_data {
486                    return Err(Bip70Error::ValidationError(
487                        "Merchant data mismatch".to_string(),
488                    ));
489                }
490            }
491        } else if original_request.payment_details.merchant_data.is_some() {
492            return Err(Bip70Error::ValidationError(
493                "Payment missing merchant data".to_string(),
494            ));
495        }
496        Self::verify_payment_transactions(payment, original_request)?;
497        let mut payment_ack = PaymentACK {
498            payment: payment.clone(),
499            memo: Some("Payment received".to_string()),
500            signature: None,
501        };
502        if let Some(private_key) = merchant_private_key {
503            payment_ack.sign(private_key)?;
504        }
505        Ok(payment_ack)
506    }
507
508    /// Verify that payment transactions match PaymentRequest outputs.
509    fn verify_payment_transactions(
510        payment: &Payment,
511        original_request: &PaymentRequest,
512    ) -> Result<(), Bip70Error> {
513        use crate::Transaction;
514
515        // Deserialize all transactions
516        let mut all_outputs = Vec::new();
517        for tx_bytes in &payment.transactions {
518            let tx: Transaction = bincode::deserialize(tx_bytes)
519                .map_err(|e| Bip70Error::SerializationError(format!("Invalid transaction: {e}")))?;
520
521            // Collect all outputs from this transaction
522            for output in &tx.outputs {
523                all_outputs.push(PaymentOutput {
524                    script: output.script_pubkey.clone(),
525                    amount: Some(output.value as u64), // Convert i64 to u64
526                });
527            }
528        }
529
530        // Check that all requested outputs are present
531        // Note: Payment may include additional outputs (change, refunds), but must include all requested
532        for requested_output in &original_request.payment_details.outputs {
533            let found = all_outputs.iter().any(|output| {
534                output.script == requested_output.script
535                    && match (output.amount, requested_output.amount) {
536                        (Some(amt), Some(req_amt)) => amt >= req_amt, // Allow overpayment
537                        (Some(_), None) => true,                      // Requested "all available"
538                        (None, Some(_)) => false, // Output has no amount but request requires one
539                        (None, None) => true,     // Both "all available"
540                    }
541            });
542
543            if !found {
544                return Err(Bip70Error::ValidationError(format!(
545                    "Payment missing required output: script={}, amount={:?}",
546                    hex::encode(&requested_output.script),
547                    requested_output.amount
548                )));
549            }
550        }
551
552        Ok(())
553    }
554
555    /// Sign a refund address for inclusion in PaymentRequest.
556    pub fn sign_refund_address(
557        address: PaymentOutput,
558        merchant_private_key: &[u8; 32],
559    ) -> Result<SignedRefundAddress, Bip70Error> {
560        let serialized = bincode::serialize(&address)
561            .map_err(|e| Bip70Error::SerializationError(e.to_string()))?;
562        let hash: [u8; 32] = Sha256::digest(&serialized).into();
563        Ok(SignedRefundAddress {
564            address,
565            signature: sign_compact(&hash, merchant_private_key)?,
566        })
567    }
568
569    /// Verify signed refund address.
570    pub fn verify_refund_address(
571        signed_refund: &SignedRefundAddress,
572        merchant_pubkey: &[u8],
573    ) -> Result<(), Bip70Error> {
574        let serialized = bincode::serialize(&signed_refund.address)
575            .map_err(|e| Bip70Error::SerializationError(e.to_string()))?;
576        let hash: [u8; 32] = Sha256::digest(&serialized).into();
577        verify_sig(&signed_refund.signature, merchant_pubkey, &hash).map_err(|_| {
578            Bip70Error::SignatureError("Refund address signature verification failed".to_string())
579        })
580    }
581}
582
583// --- CTV covenant types (shared by P2P payment messages and node covenant engine; serde-stable) ---
584
585/// CTV covenant proof for payment commitment
586#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
587pub struct CovenantProof {
588    /// CTV template hash (32 bytes)
589    pub template_hash: Hash,
590    /// Transaction template structure (without signatures)
591    pub transaction_template: TransactionTemplate,
592    /// Payment request ID this proof commits to
593    pub payment_request_id: String,
594    /// Timestamp when proof was created
595    pub created_at: u64,
596    /// Optional cryptographic signature of the proof
597    pub signature: Option<Vec<u8>>,
598}
599
600/// Transaction template for CTV (without scriptSig)
601#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
602pub struct TransactionTemplate {
603    pub version: u32,
604    pub inputs: Vec<TemplateInput>,
605    pub outputs: Vec<TemplateOutput>,
606    pub lock_time: u32,
607}
608
609/// Template input (CTV format: no scriptSig)
610#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
611pub struct TemplateInput {
612    pub prevout_hash: Hash,
613    pub prevout_index: u32,
614    pub sequence: u32,
615}
616
617/// Template output
618#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
619pub struct TemplateOutput {
620    pub value: u64,
621    pub script_pubkey: Vec<u8>,
622}
623
624/// Settlement status for payment
625#[derive(Debug, Clone, Serialize, Deserialize)]
626pub enum SettlementStatus {
627    /// Payment proof created, not yet broadcast
628    ProofCreated,
629    /// Payment proof broadcast, transaction not yet in mempool
630    ProofBroadcast,
631    /// Transaction in mempool, waiting for confirmation
632    InMempool { tx_hash: Hash },
633    /// Settlement confirmed on-chain
634    Settled {
635        tx_hash: Hash,
636        block_hash: Hash,
637        confirmation_count: u32,
638    },
639    /// Payment failed or rejected
640    Failed { reason: String },
641}
642
643#[cfg(test)]
644mod tests {
645    use super::*;
646
647    #[test]
648    fn test_payment_request_creation() {
649        let output = PaymentOutput {
650            script: vec![blvm_consensus::opcodes::OP_1],
651            amount: Some(100000), // 0.001 BTC
652        };
653
654        let request = PaymentRequest::new("main".to_string(), vec![output], 1234567890);
655
656        assert_eq!(request.payment_details.network, "main");
657        assert_eq!(request.payment_details.outputs.len(), 1);
658        assert_eq!(request.payment_details.time, 1234567890);
659    }
660
661    #[test]
662    fn test_payment_request_validation() {
663        let request = PaymentRequest::new(
664            "main".to_string(),
665            vec![PaymentOutput {
666                script: vec![blvm_consensus::opcodes::OP_1],
667                amount: Some(100000),
668            }],
669            1234567890,
670        );
671
672        assert!(request.validate().is_ok());
673    }
674
675    #[test]
676    fn test_payment_request_expired() {
677        let expired_time = 1000;
678        let request = PaymentRequest::new(
679            "main".to_string(),
680            vec![PaymentOutput {
681                script: vec![blvm_consensus::opcodes::OP_1],
682                amount: Some(100000),
683            }],
684            expired_time,
685        )
686        .with_expires(1001);
687
688        // Should fail validation (expired)
689        let result = request.validate();
690        assert!(result.is_err());
691    }
692
693    #[test]
694    fn test_payment_creation() {
695        let tx = vec![0x01, 0x00, 0x00, 0x00]; // Arbitrary bytes for Payment::new structure test (not a valid tx)
696        let payment = Payment::new(vec![tx.clone()]);
697
698        assert_eq!(payment.transactions.len(), 1);
699        assert_eq!(payment.transactions[0], tx);
700    }
701
702    #[test]
703    fn test_payment_validation() {
704        let payment = Payment::new(vec![vec![0x01, 0x02, 0x03]]);
705        assert!(payment.validate().is_ok());
706
707        let empty_payment = Payment::new(vec![]);
708        assert!(empty_payment.validate().is_err());
709    }
710}