Skip to main content

agent_pay/
envelope.rs

1//! DID-signed envelopes for invoices and receipts.
2
3use base64::engine::general_purpose::STANDARD as B64_STD;
4use base64::Engine;
5use serde::{Deserialize, Serialize};
6use serde_json::json;
7use sha2::{Digest, Sha256};
8
9use crate::error::Error;
10use crate::jws::{sign_compact, verify_compact, ResolveKey};
11use crate::keys::verification_method_id;
12
13pub const VERSION: &str = "agent-pay/0.1";
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct InvoiceEnvelope {
17    pub v: String,
18    pub invoice_hash: String,
19    pub did: String,
20    pub price_msat: String,
21    pub resource: String,
22    pub expires_at: String,
23    pub nonce: String,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ReceiptEnvelope {
28    pub v: String,
29    pub invoice_hash: String,
30    pub preimage_hash: String,
31    pub resource: String,
32    pub paid_at: String,
33    pub did: String,
34}
35
36fn invoice_hash_hex(bolt11: &str) -> String {
37    let mut h = Sha256::new();
38    h.update(bolt11.as_bytes());
39    hex::encode(h.finalize())
40}
41
42pub struct SignInvoiceOpts<'a> {
43    pub bolt11: &'a str,
44    pub did: &'a str,
45    pub private_key: &'a [u8; 32],
46    pub price_msat: u64,
47    pub resource: &'a str,
48    pub expires_at: &'a str,
49    pub nonce: &'a [u8],
50}
51
52pub async fn sign_invoice_envelope(opts: SignInvoiceOpts<'_>) -> Result<String, Error> {
53    let payload = json!({
54        "v": VERSION,
55        "invoice_hash": invoice_hash_hex(opts.bolt11),
56        "did": opts.did,
57        "price_msat": opts.price_msat.to_string(),
58        "resource": opts.resource,
59        "expires_at": opts.expires_at,
60        "nonce": B64_STD.encode(opts.nonce),
61    });
62    let kid = verification_method_id(opts.did)?;
63    sign_compact(&payload, opts.private_key, &kid).await
64}
65
66pub async fn verify_invoice_envelope(
67    token: &str,
68    bolt11: &str,
69    resolver: &ResolveKey,
70) -> Result<InvoiceEnvelope, Error> {
71    let (payload, _kid) = verify_compact(token, resolver).await?;
72    let env: InvoiceEnvelope = serde_json::from_value(payload)
73        .map_err(|e| Error::Envelope(format!("invalid envelope shape: {e}")))?;
74    if env.v != VERSION {
75        return Err(Error::Envelope(format!(
76            "unsupported envelope version: {}",
77            env.v
78        )));
79    }
80    let expected = invoice_hash_hex(bolt11);
81    if env.invoice_hash != expected {
82        return Err(Error::Envelope(format!(
83            "invoice_hash mismatch: envelope={} bolt11={}",
84            env.invoice_hash, expected
85        )));
86    }
87    Ok(env)
88}
89
90pub struct SignReceiptOpts<'a> {
91    pub bolt11: &'a str,
92    pub did: &'a str,
93    pub private_key: &'a [u8; 32],
94    pub preimage: &'a [u8],
95    pub resource: &'a str,
96    pub paid_at: &'a str,
97}
98
99pub async fn sign_receipt(opts: SignReceiptOpts<'_>) -> Result<String, Error> {
100    let mut h = Sha256::new();
101    h.update(opts.preimage);
102    let preimage_hash = hex::encode(h.finalize());
103    let payload = json!({
104        "v": VERSION,
105        "invoice_hash": invoice_hash_hex(opts.bolt11),
106        "preimage_hash": preimage_hash,
107        "resource": opts.resource,
108        "paid_at": opts.paid_at,
109        "did": opts.did,
110    });
111    let kid = verification_method_id(opts.did)?;
112    sign_compact(&payload, opts.private_key, &kid).await
113}
114
115pub async fn verify_receipt(
116    token: &str,
117    bolt11: &str,
118    resolver: &ResolveKey,
119) -> Result<ReceiptEnvelope, Error> {
120    let (payload, _kid) = verify_compact(token, resolver).await?;
121    let env: ReceiptEnvelope = serde_json::from_value(payload)
122        .map_err(|e| Error::Envelope(format!("invalid receipt shape: {e}")))?;
123    if env.v != VERSION {
124        return Err(Error::Envelope(format!(
125            "unsupported receipt version: {}",
126            env.v
127        )));
128    }
129    if env.invoice_hash != invoice_hash_hex(bolt11) {
130        return Err(Error::Envelope("receipt invoice_hash mismatch".into()));
131    }
132    Ok(env)
133}