use base64::engine::general_purpose::STANDARD as B64_STD;
use base64::Engine;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::{Digest, Sha256};
use crate::error::Error;
use crate::jws::{sign_compact, verify_compact, ResolveKey};
use crate::keys::verification_method_id;
pub const VERSION: &str = "agent-pay/0.1";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvoiceEnvelope {
pub v: String,
pub invoice_hash: String,
pub did: String,
pub price_msat: String,
pub resource: String,
pub expires_at: String,
pub nonce: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceiptEnvelope {
pub v: String,
pub invoice_hash: String,
pub preimage_hash: String,
pub resource: String,
pub paid_at: String,
pub did: String,
}
fn invoice_hash_hex(bolt11: &str) -> String {
let mut h = Sha256::new();
h.update(bolt11.as_bytes());
hex::encode(h.finalize())
}
pub struct SignInvoiceOpts<'a> {
pub bolt11: &'a str,
pub did: &'a str,
pub private_key: &'a [u8; 32],
pub price_msat: u64,
pub resource: &'a str,
pub expires_at: &'a str,
pub nonce: &'a [u8],
}
pub async fn sign_invoice_envelope(opts: SignInvoiceOpts<'_>) -> Result<String, Error> {
let payload = json!({
"v": VERSION,
"invoice_hash": invoice_hash_hex(opts.bolt11),
"did": opts.did,
"price_msat": opts.price_msat.to_string(),
"resource": opts.resource,
"expires_at": opts.expires_at,
"nonce": B64_STD.encode(opts.nonce),
});
let kid = verification_method_id(opts.did)?;
sign_compact(&payload, opts.private_key, &kid).await
}
pub async fn verify_invoice_envelope(
token: &str,
bolt11: &str,
resolver: &ResolveKey,
) -> Result<InvoiceEnvelope, Error> {
let (payload, _kid) = verify_compact(token, resolver).await?;
let env: InvoiceEnvelope = serde_json::from_value(payload)
.map_err(|e| Error::Envelope(format!("invalid envelope shape: {e}")))?;
if env.v != VERSION {
return Err(Error::Envelope(format!(
"unsupported envelope version: {}",
env.v
)));
}
let expected = invoice_hash_hex(bolt11);
if env.invoice_hash != expected {
return Err(Error::Envelope(format!(
"invoice_hash mismatch: envelope={} bolt11={}",
env.invoice_hash, expected
)));
}
Ok(env)
}
pub struct SignReceiptOpts<'a> {
pub bolt11: &'a str,
pub did: &'a str,
pub private_key: &'a [u8; 32],
pub preimage: &'a [u8],
pub resource: &'a str,
pub paid_at: &'a str,
}
pub async fn sign_receipt(opts: SignReceiptOpts<'_>) -> Result<String, Error> {
let mut h = Sha256::new();
h.update(opts.preimage);
let preimage_hash = hex::encode(h.finalize());
let payload = json!({
"v": VERSION,
"invoice_hash": invoice_hash_hex(opts.bolt11),
"preimage_hash": preimage_hash,
"resource": opts.resource,
"paid_at": opts.paid_at,
"did": opts.did,
});
let kid = verification_method_id(opts.did)?;
sign_compact(&payload, opts.private_key, &kid).await
}
pub async fn verify_receipt(
token: &str,
bolt11: &str,
resolver: &ResolveKey,
) -> Result<ReceiptEnvelope, Error> {
let (payload, _kid) = verify_compact(token, resolver).await?;
let env: ReceiptEnvelope = serde_json::from_value(payload)
.map_err(|e| Error::Envelope(format!("invalid receipt shape: {e}")))?;
if env.v != VERSION {
return Err(Error::Envelope(format!(
"unsupported receipt version: {}",
env.v
)));
}
if env.invoice_hash != invoice_hash_hex(bolt11) {
return Err(Error::Envelope("receipt invoice_hash mismatch".into()));
}
Ok(env)
}