1use 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}