agent-pay 0.1.0

L402 + DID-signed invoices: agent-to-agent Lightning payments (Rust port of @p-vbordei/agent-pay)
Documentation
//! In-memory mock Lightning node (mirrors the TS memory-node).

use std::sync::Arc;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};

use async_trait::async_trait;
use bitcoin::hashes::{sha256, Hash};
use bitcoin::secp256k1::{Secp256k1, SecretKey};
use lightning_invoice::{Currency, InvoiceBuilder, PaymentSecret};
use rand::RngCore;
use sha2::{Digest, Sha256};

use crate::error::Error;
use crate::lightning::{
    Invoice, InvoiceCreateRequest, InvoiceLookup, LightningNode, PaymentResult,
};

const SIGNING_KEY_HEX: &str = "e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734";

#[derive(Debug, Clone)]
struct Entry {
    amount_msat: u64,
    preimage: [u8; 32],
    bolt11: String,
    settled: bool,
    #[allow(dead_code)]
    payee: String,
}

#[derive(Default)]
pub struct MemoryLedger {
    invoices: Mutex<std::collections::HashMap<String, Entry>>,
}

impl MemoryLedger {
    pub fn new() -> Arc<Self> {
        Arc::new(Self {
            invoices: Mutex::new(std::collections::HashMap::new()),
        })
    }
}

pub struct MemoryNode {
    ledger: Arc<MemoryLedger>,
    name: String,
}

impl MemoryNode {
    pub fn new(ledger: Arc<MemoryLedger>, name: impl Into<String>) -> Self {
        Self {
            ledger,
            name: name.into(),
        }
    }
}

#[async_trait]
impl LightningNode for MemoryNode {
    async fn create_invoice(&self, req: InvoiceCreateRequest) -> Result<Invoice, Error> {
        let secp = Secp256k1::new();
        let sk_bytes = hex::decode(SIGNING_KEY_HEX)
            .map_err(|e| Error::Lightning(format!("signing key hex: {e}")))?;
        let sk = SecretKey::from_slice(&sk_bytes)
            .map_err(|e| Error::Lightning(format!("secret key: {e}")))?;

        let mut preimage = [0u8; 32];
        rand::thread_rng().fill_bytes(&mut preimage);
        let payment_hash = sha256::Hash::hash(&preimage);
        let payment_hash_hex = hex::encode(payment_hash.to_byte_array());

        let mut secret_bytes = [0u8; 32];
        rand::thread_rng().fill_bytes(&mut secret_bytes);
        let payment_secret = PaymentSecret(secret_bytes);

        let ts = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map_err(|e| Error::Lightning(format!("time: {e}")))?;
        let memo = req.memo.unwrap_or_default();
        let expiry = req.expiry_seconds.unwrap_or(300);

        let inv = InvoiceBuilder::new(Currency::Regtest)
            .description(memo)
            .payment_hash(payment_hash)
            .payment_secret(payment_secret)
            .amount_milli_satoshis(req.amount_msat)
            .duration_since_epoch(ts)
            .min_final_cltv_expiry_delta(10)
            .expiry_time(std::time::Duration::from_secs(expiry))
            .build_signed(|h| secp.sign_ecdsa_recoverable(h, &sk))
            .map_err(|e| Error::Lightning(format!("bolt11 build: {e:?}")))?;
        let bolt11_str = inv.to_string();

        let mut map = self.ledger.invoices.lock().unwrap();
        map.insert(
            payment_hash_hex.clone(),
            Entry {
                amount_msat: req.amount_msat,
                preimage,
                bolt11: bolt11_str.clone(),
                settled: false,
                payee: self.name.clone(),
            },
        );
        Ok(Invoice {
            bolt11: bolt11_str,
            payment_hash: payment_hash_hex,
        })
    }

    async fn lookup_invoice(&self, payment_hash: &str) -> Result<InvoiceLookup, Error> {
        let map = self.ledger.invoices.lock().unwrap();
        let entry = map
            .get(payment_hash)
            .ok_or_else(|| Error::Lightning(format!("unknown payment_hash: {payment_hash}")))?;
        Ok(InvoiceLookup {
            settled: entry.settled,
            amount_msat: entry.amount_msat,
            preimage: if entry.settled {
                Some(entry.preimage.to_vec())
            } else {
                None
            },
        })
    }

    async fn pay_invoice(&self, bolt11: &str) -> Result<PaymentResult, Error> {
        let mut map = self.ledger.invoices.lock().unwrap();
        for entry in map.values_mut() {
            if entry.bolt11 == bolt11 {
                if entry.settled {
                    return Err(Error::Lightning("invoice already settled".into()));
                }
                entry.settled = true;
                return Ok(PaymentResult {
                    preimage: entry.preimage.to_vec(),
                    fee_msat: 0,
                });
            }
        }
        Err(Error::Lightning(format!(
            "unknown bolt11: {}...",
            &bolt11[..32.min(bolt11.len())]
        )))
    }
}

// Used to silence dead_code on Sha256 import in some configs.
#[allow(dead_code)]
fn _dummy_sha256() {
    let mut h = Sha256::new();
    h.update([0u8; 0]);
    let _ = h.finalize();
}