agent-pay 0.1.0

L402 + DID-signed invoices: agent-to-agent Lightning payments (Rust port of @p-vbordei/agent-pay)
Documentation
use std::sync::Arc;

use agent_pay::{
    did_key_from_public_key, generate_key_pair, public_key_from_did_key, sign_invoice_envelope,
    sign_receipt, verify_invoice_envelope, verify_receipt, Error, ResolveKey, SignInvoiceOpts,
    SignReceiptOpts,
};

const FAKE_BOLT11: &str = "lnbc10n1pdummy";

fn make_resolver(did: String) -> ResolveKey {
    Arc::new(move |kid: String| {
        let did = did.clone();
        Box::pin(async move {
            if !kid.starts_with(&did) {
                return Err(Error::Other(format!("unknown kid {kid}")));
            }
            public_key_from_did_key(&did)
        })
    })
}

#[tokio::test]
async fn sign_verify_invoice_envelope_roundtrip() {
    let kp = generate_key_pair();
    let did = did_key_from_public_key(&kp.public_key).unwrap();
    let resolver = make_resolver(did.clone());
    let token = sign_invoice_envelope(SignInvoiceOpts {
        bolt11: FAKE_BOLT11,
        did: &did,
        private_key: &kp.private_key,
        price_msat: 1000,
        resource: "/report",
        expires_at: "2030-01-01T00:00:00Z",
        nonce: &[0u8; 16],
    })
    .await
    .unwrap();
    let env = verify_invoice_envelope(&token, FAKE_BOLT11, &resolver)
        .await
        .unwrap();
    assert_eq!(env.did, did);
    assert_eq!(env.price_msat, "1000");
    assert_eq!(env.resource, "/report");
}

#[tokio::test]
async fn verify_invoice_envelope_rejects_mismatched_bolt11() {
    let kp = generate_key_pair();
    let did = did_key_from_public_key(&kp.public_key).unwrap();
    let resolver = make_resolver(did.clone());
    let token = sign_invoice_envelope(SignInvoiceOpts {
        bolt11: FAKE_BOLT11,
        did: &did,
        private_key: &kp.private_key,
        price_msat: 1000,
        resource: "/report",
        expires_at: "2030-01-01T00:00:00Z",
        nonce: &[0u8; 16],
    })
    .await
    .unwrap();
    let err = verify_invoice_envelope(&token, "lnbc1pdifferent", &resolver)
        .await
        .unwrap_err();
    let msg = format!("{err}").to_lowercase();
    assert!(msg.contains("invoice_hash"), "msg={msg}");
}

#[tokio::test]
async fn sign_verify_receipt_roundtrip() {
    let kp = generate_key_pair();
    let did = did_key_from_public_key(&kp.public_key).unwrap();
    let resolver = make_resolver(did.clone());
    let token = sign_receipt(SignReceiptOpts {
        bolt11: FAKE_BOLT11,
        did: &did,
        private_key: &kp.private_key,
        preimage: &[0u8; 32],
        resource: "/report",
        paid_at: "2030-01-01T00:00:00Z",
    })
    .await
    .unwrap();
    let env = verify_receipt(&token, FAKE_BOLT11, &resolver)
        .await
        .unwrap();
    assert_eq!(env.did, did);
    assert_eq!(env.resource, "/report");
}