#[cfg(not(feature = "paymail"))]
compile_error!("Paymail support requires the 'paymail' feature.");
use crate::beef::Beef;
use crate::client::BlockHeadersClient;
use crate::errors::{Result, ShiaError};
use base64::{engine::general_purpose, Engine as _};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymailEnvelope {
pub beef: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub proofs: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
impl PaymailEnvelope {
pub fn from_beef(
beef: &Beef,
proofs: Option<Vec<String>>,
metadata: Option<HashMap<String, serde_json::Value>>,
) -> Result<Self> {
let beef_bytes = beef.serialize()?;
let beef_b64 = general_purpose::STANDARD.encode(&beef_bytes);
Ok(Self {
beef: beef_b64,
proofs,
metadata,
})
}
pub fn from_json(json: &str) -> Result<Self> {
serde_json::from_str(json).map_err(|e| ShiaError::Verification(e.to_string()).into())
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string(self)
.map_err(|e| ShiaError::Verification(e.to_string()).into())
}
pub fn to_beef(&self) -> Result<Beef> {
let beef_bytes = general_purpose::STANDARD
.decode(&self.beef)
.map_err(|e| ShiaError::Verification(format!("Base64 decode: {}", e)))?;
Beef::deserialize(&beef_bytes)
}
pub fn verify(&self, headers_client: &impl BlockHeadersClient) -> Result<()> {
let beef = self.to_beef()?;
beef.verify(headers_client)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::beef::Beef;
use crate::client::MockHeadersClient;
use crate::tx::Transaction;
use hex;
use std::collections::HashMap;
#[test]
fn test_from_beef_roundtrip() {
let subject_raw = hex::decode("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0504ffff001dffffffff0100ca9a3b000000001976a914000000000000000000000000000000000000000088ac00000000").unwrap();
let subject_tx = Transaction::from_raw(&subject_raw).unwrap();
let ancestors = HashMap::new();
let bump_map = HashMap::new();
let beef = Beef::build(subject_tx, ancestors, bump_map, false).unwrap();
let proofs = Some(vec!["deadbeef".to_string()]);
let mut metadata = HashMap::new();
metadata.insert("alias".to_string(), serde_json::Value::String("roy".to_string()));
let envelope = PaymailEnvelope::from_beef(&beef, proofs.clone(), Some(metadata)).unwrap();
let json = envelope.to_json().unwrap();
let roundtrip = PaymailEnvelope::from_json(&json).unwrap();
assert_eq!(roundtrip.beef, envelope.beef);
assert_eq!(roundtrip.proofs, proofs);
assert_eq!(roundtrip.metadata.as_ref().unwrap().get("alias").unwrap().as_str().unwrap(), "roy");
let extracted_beef = roundtrip.to_beef().unwrap();
assert_eq!(extracted_beef.txs.len(), beef.txs.len());
}
#[test]
fn test_verify_envelope() {
let minimal_beef_hex = "f1c6c3ef00010100000000000000000000000000000000000000000000000000000000000000000000000000000000000504ffff001dffffffff0100ca9a3b000000001976a914000000000000000000000000000000000000000088ac0000000000";
let beef = Beef::from_hex(minimal_beef_hex).unwrap();
let envelope = PaymailEnvelope::from_beef(&beef, None, None).unwrap();
let mock_client = MockHeadersClient;
assert!(envelope.verify(&mock_client).is_ok());
}
#[test]
fn test_invalid_json() {
let bad_json = r#"{"beef": "invalid_base64"}"#;
assert!(PaymailEnvelope::from_json(bad_json).is_err());
}
}