Skip to main content

agent_pay/
memory_node.rs

1//! In-memory mock Lightning node (mirrors the TS memory-node).
2
3use std::sync::Arc;
4use std::sync::Mutex;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use async_trait::async_trait;
8use bitcoin::hashes::{sha256, Hash};
9use bitcoin::secp256k1::{Secp256k1, SecretKey};
10use lightning_invoice::{Currency, InvoiceBuilder, PaymentSecret};
11use rand::RngCore;
12use sha2::{Digest, Sha256};
13
14use crate::error::Error;
15use crate::lightning::{
16    Invoice, InvoiceCreateRequest, InvoiceLookup, LightningNode, PaymentResult,
17};
18
19const SIGNING_KEY_HEX: &str = "e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734";
20
21#[derive(Debug, Clone)]
22struct Entry {
23    amount_msat: u64,
24    preimage: [u8; 32],
25    bolt11: String,
26    settled: bool,
27    #[allow(dead_code)]
28    payee: String,
29}
30
31#[derive(Default)]
32pub struct MemoryLedger {
33    invoices: Mutex<std::collections::HashMap<String, Entry>>,
34}
35
36impl MemoryLedger {
37    pub fn new() -> Arc<Self> {
38        Arc::new(Self {
39            invoices: Mutex::new(std::collections::HashMap::new()),
40        })
41    }
42}
43
44pub struct MemoryNode {
45    ledger: Arc<MemoryLedger>,
46    name: String,
47}
48
49impl MemoryNode {
50    pub fn new(ledger: Arc<MemoryLedger>, name: impl Into<String>) -> Self {
51        Self {
52            ledger,
53            name: name.into(),
54        }
55    }
56}
57
58#[async_trait]
59impl LightningNode for MemoryNode {
60    async fn create_invoice(&self, req: InvoiceCreateRequest) -> Result<Invoice, Error> {
61        let secp = Secp256k1::new();
62        let sk_bytes = hex::decode(SIGNING_KEY_HEX)
63            .map_err(|e| Error::Lightning(format!("signing key hex: {e}")))?;
64        let sk = SecretKey::from_slice(&sk_bytes)
65            .map_err(|e| Error::Lightning(format!("secret key: {e}")))?;
66
67        let mut preimage = [0u8; 32];
68        rand::thread_rng().fill_bytes(&mut preimage);
69        let payment_hash = sha256::Hash::hash(&preimage);
70        let payment_hash_hex = hex::encode(payment_hash.to_byte_array());
71
72        let mut secret_bytes = [0u8; 32];
73        rand::thread_rng().fill_bytes(&mut secret_bytes);
74        let payment_secret = PaymentSecret(secret_bytes);
75
76        let ts = SystemTime::now()
77            .duration_since(UNIX_EPOCH)
78            .map_err(|e| Error::Lightning(format!("time: {e}")))?;
79        let memo = req.memo.unwrap_or_default();
80        let expiry = req.expiry_seconds.unwrap_or(300);
81
82        let inv = InvoiceBuilder::new(Currency::Regtest)
83            .description(memo)
84            .payment_hash(payment_hash)
85            .payment_secret(payment_secret)
86            .amount_milli_satoshis(req.amount_msat)
87            .duration_since_epoch(ts)
88            .min_final_cltv_expiry_delta(10)
89            .expiry_time(std::time::Duration::from_secs(expiry))
90            .build_signed(|h| secp.sign_ecdsa_recoverable(h, &sk))
91            .map_err(|e| Error::Lightning(format!("bolt11 build: {e:?}")))?;
92        let bolt11_str = inv.to_string();
93
94        let mut map = self.ledger.invoices.lock().unwrap();
95        map.insert(
96            payment_hash_hex.clone(),
97            Entry {
98                amount_msat: req.amount_msat,
99                preimage,
100                bolt11: bolt11_str.clone(),
101                settled: false,
102                payee: self.name.clone(),
103            },
104        );
105        Ok(Invoice {
106            bolt11: bolt11_str,
107            payment_hash: payment_hash_hex,
108        })
109    }
110
111    async fn lookup_invoice(&self, payment_hash: &str) -> Result<InvoiceLookup, Error> {
112        let map = self.ledger.invoices.lock().unwrap();
113        let entry = map
114            .get(payment_hash)
115            .ok_or_else(|| Error::Lightning(format!("unknown payment_hash: {payment_hash}")))?;
116        Ok(InvoiceLookup {
117            settled: entry.settled,
118            amount_msat: entry.amount_msat,
119            preimage: if entry.settled {
120                Some(entry.preimage.to_vec())
121            } else {
122                None
123            },
124        })
125    }
126
127    async fn pay_invoice(&self, bolt11: &str) -> Result<PaymentResult, Error> {
128        let mut map = self.ledger.invoices.lock().unwrap();
129        for entry in map.values_mut() {
130            if entry.bolt11 == bolt11 {
131                if entry.settled {
132                    return Err(Error::Lightning("invoice already settled".into()));
133                }
134                entry.settled = true;
135                return Ok(PaymentResult {
136                    preimage: entry.preimage.to_vec(),
137                    fee_msat: 0,
138                });
139            }
140        }
141        Err(Error::Lightning(format!(
142            "unknown bolt11: {}...",
143            &bolt11[..32.min(bolt11.len())]
144        )))
145    }
146}
147
148// Used to silence dead_code on Sha256 import in some configs.
149#[allow(dead_code)]
150fn _dummy_sha256() {
151    let mut h = Sha256::new();
152    h.update([0u8; 0]);
153    let _ = h.finalize();
154}