1use crate::https_service::HttpsService;
2use http::uri::Authority;
3#[cfg(test)]
4use lnd_test_context::LndTestContext;
5use lnrpc::{
6 lightning_client::LightningClient, AddInvoiceResponse, Invoice, ListInvoiceRequest, PaymentHash,
7};
8use openssl::x509::X509;
9use std::convert::TryInto;
10#[cfg(test)]
11use std::sync::Arc;
12use tonic::{
13 metadata::AsciiMetadataValue,
14 service::interceptor::{InterceptedService, Interceptor},
15 Code, Request, Status,
16};
17
18pub use millisatoshi::Millisatoshi;
19
20mod https_service;
21mod millisatoshi;
22
23pub mod lnrpc {
24 use crate::millisatoshi::Millisatoshi;
25 use std::convert::TryInto;
26
27 tonic::include_proto!("lnrpc");
28
29 impl Invoice {
30 pub fn value_msat(&self) -> Millisatoshi {
31 Millisatoshi::new(
32 self
33 .value_msat
34 .try_into()
35 .expect("value_msat is always positive"),
36 )
37 }
38 }
39}
40
41#[derive(Clone)]
42struct MacaroonInterceptor {
43 macaroon: Option<AsciiMetadataValue>,
44}
45
46impl Interceptor for MacaroonInterceptor {
47 fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
48 if let Some(macaroon) = &self.macaroon {
49 request.metadata_mut().insert("macaroon", macaroon.clone());
50 }
51 Ok(request)
52 }
53}
54
55#[derive(Debug, Clone)]
56pub struct Client {
57 inner: LightningClient<InterceptedService<HttpsService, MacaroonInterceptor>>,
58 #[cfg(test)]
59 lnd_test_context: Arc<LndTestContext>,
60}
61
62impl Client {
63 pub async fn new(
64 authority: Authority,
65 certificate: Option<X509>,
66 macaroon: Option<Vec<u8>>,
67 #[cfg(test)] lnd_test_context: LndTestContext,
68 ) -> Result<Client, openssl::error::ErrorStack> {
69 let grpc_service = HttpsService::new(authority, certificate)?;
70
71 let macaroon = macaroon.map(|macaroon| {
72 hex::encode_upper(macaroon)
73 .parse::<AsciiMetadataValue>()
74 .expect("Client::new: hex characters are valid metadata values")
75 });
76
77 let inner = LightningClient::with_interceptor(grpc_service, MacaroonInterceptor { macaroon });
78
79 Ok(Client {
80 inner,
81 #[cfg(test)]
82 lnd_test_context: Arc::new(lnd_test_context),
83 })
84 }
85
86 pub async fn ping(&mut self) -> Result<(), Status> {
87 let request = tonic::Request::new(ListInvoiceRequest {
88 index_offset: 0,
89 num_max_invoices: 0,
90 pending_only: false,
91 reversed: false,
92 });
93
94 self.inner.list_invoices(request).await?;
95
96 Ok(())
97 }
98
99 pub async fn add_invoice(
100 &mut self,
101 memo: &str,
102 value_msat: Millisatoshi,
103 ) -> Result<AddInvoiceResponse, Status> {
104 let request = tonic::Request::new(Invoice {
105 memo: memo.to_owned(),
106 value_msat: value_msat.value().try_into().map_err(|source| {
107 Status::new(
108 Code::InvalidArgument,
109 format!("invalid value for `value_msat`: {}", source),
110 )
111 })?,
112 ..Invoice::default()
113 });
114 Ok(self.inner.add_invoice(request).await?.into_inner())
115 }
116
117 pub async fn lookup_invoice(&mut self, r_hash: [u8; 32]) -> Result<Option<Invoice>, Status> {
118 let request = tonic::Request::new(PaymentHash {
119 r_hash: r_hash.to_vec(),
120 ..PaymentHash::default()
121 });
122 match self.inner.lookup_invoice(request).await {
123 Ok(response) => Ok(Some(response.into_inner())),
124 Err(status) => {
125 if status.code() == Code::Unknown
126 && (status.message() == "there are no existing invoices"
127 || status.message() == "unable to locate invoice")
128 {
129 Ok(None)
130 } else {
131 Err(status)
132 }
133 }
134 }
135 }
136
137 #[cfg(test)]
138 async fn with_cert(lnd_test_context: LndTestContext, cert: &str) -> Self {
139 Self::new(
140 format!("localhost:{}", lnd_test_context.lnd_rpc_port)
141 .parse()
142 .unwrap(),
143 Some(X509::from_pem(cert.as_bytes()).unwrap()),
144 Some(
145 tokio::fs::read(lnd_test_context.invoice_macaroon_path())
146 .await
147 .unwrap(),
148 ),
149 lnd_test_context,
150 )
151 .await
152 .unwrap()
153 }
154
155 #[cfg(test)]
156 async fn with_test_context(lnd_test_context: LndTestContext) -> Self {
157 let cert = std::fs::read_to_string(lnd_test_context.cert_path()).unwrap();
158 Self::with_cert(lnd_test_context, &cert).await
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[tokio::test]
167 async fn ping() {
168 Client::with_test_context(LndTestContext::new().await)
169 .await
170 .ping()
171 .await
172 .unwrap();
173 }
174
175 #[tokio::test]
176 async fn fails_on_wrong_lnd_certificate() {
177 const INVALID_TEST_CERT: &str = "-----BEGIN CERTIFICATE-----
178MIICTDCCAfGgAwIBAgIQdJJBvsv1/V23RMoX9fOOuTAKBggqhkjOPQQDAjAwMR8w
179HQYDVQQKExZsbmQgYXV0b2dlbmVyYXRlZCBjZXJ0MQ0wCwYDVQQDEwRwcmFnMB4X
180DTIxMDYyNzIxMTg1NloXDTIyMDgyMjIxMTg1NlowMDEfMB0GA1UEChMWbG5kIGF1
181dG9nZW5lcmF0ZWQgY2VydDENMAsGA1UEAxMEcHJhZzBZMBMGByqGSM49AgEGCCqG
182SM49AwEHA0IABL4lYBbOPVAtglBKPV3LwB7eC1j/Y6Nt0O23M1dSrcLdrNHUP87n
1835clDvrur4EaJTmnZHI2141usNs/pljzMHmqjgewwgekwDgYDVR0PAQH/BAQDAgKk
184MBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE
185FIQ2zY1Z6g9NRGbMtXbSZEesaIqhMIGRBgNVHREEgYkwgYaCBHByYWeCCWxvY2Fs
186aG9zdIIEdW5peIIKdW5peHBhY2tldIIHYnVmY29ubocEfwAAAYcQAAAAAAAAAAAA
187AAAAAAAAAYcEwKgBDocErBEAAYcErBIAAYcErBMAAYcEwKgBC4cQ/oAAAAAAAAA2
1886QIJT4EyIocQ/oAAAAAAAABD0/8gsXGsVzAKBggqhkjOPQQDAgNJADBGAiEA3lrs
189qmJp1luuw/ElVG3DdHtz4Lx8iK8EanRdHA3T+78CIQDfuWGMe0IGtwLuDpDixvGy
190jlZBq5hr8Nv2qStFfw9qzw==
191-----END CERTIFICATE-----
192";
193 let error = Client::with_cert(LndTestContext::new().await, INVALID_TEST_CERT)
194 .await
195 .ping()
196 .await
197 .unwrap_err();
198 #[track_caller]
199 fn assert_contains(input: &str, expected: &str) {
200 assert!(
201 input.contains(expected),
202 "assert_contains:\n{}\ndidn't contain\n{}",
203 input,
204 expected
205 );
206 }
207 assert_contains(&error.to_string(), "error trying to connect: ");
208 assert_contains(&error.to_string(), "certificate verify failed");
209 assert_contains(&error.to_string(), "self signed certificate");
210 }
211
212 #[tokio::test]
213 async fn add_invoice() {
214 let mut client = Client::with_test_context(LndTestContext::new().await).await;
215 let response = client
216 .add_invoice("", Millisatoshi::new(1_000))
217 .await
218 .unwrap();
219 assert!(
220 !response.payment_request.is_empty(),
221 "Bad response: {:?}",
222 response
223 );
224 }
225
226 #[tokio::test]
227 async fn add_invoice_memo_and_value() {
228 let mut client = Client::with_test_context(LndTestContext::new().await).await;
229 let r_hash = client
230 .add_invoice("test-memo", Millisatoshi::new(42_000))
231 .await
232 .unwrap()
233 .r_hash;
234 let invoice = client
235 .lookup_invoice(r_hash.try_into().unwrap())
236 .await
237 .unwrap()
238 .unwrap();
239 assert_eq!(invoice.memo, "test-memo");
240 assert_eq!(invoice.value, 42);
241 }
242
243 #[tokio::test]
244 async fn lookup_invoice() {
245 let mut client = Client::with_test_context(LndTestContext::new().await).await;
246 let _ignored1 = client
247 .add_invoice("foo", Millisatoshi::new(1_000))
248 .await
249 .unwrap();
250 let created = client
251 .add_invoice("bar", Millisatoshi::new(2_000))
252 .await
253 .unwrap();
254 let _ignored2 = client
255 .add_invoice("baz", Millisatoshi::new(3_000))
256 .await
257 .unwrap();
258 let retrieved = client
259 .lookup_invoice(created.r_hash.as_slice().try_into().unwrap())
260 .await
261 .unwrap()
262 .unwrap();
263 assert_eq!(
264 (
265 created.add_index,
266 created.r_hash,
267 created.payment_request,
268 created.payment_addr
269 ),
270 (
271 retrieved.add_index,
272 retrieved.r_hash,
273 retrieved.payment_request,
274 retrieved.payment_addr
275 )
276 );
277 assert_eq!(retrieved.memo, "bar");
278 assert_eq!(retrieved.value, 2);
279 }
280
281 #[tokio::test]
282 async fn lookup_invoice_not_found_no_invoices() {
283 let mut client = Client::with_test_context(LndTestContext::new().await).await;
284 assert_eq!(client.lookup_invoice([0; 32]).await.unwrap(), None);
285 }
286
287 #[tokio::test]
288 async fn lookup_invoice_not_found_some_invoices() {
289 let mut client = Client::with_test_context(LndTestContext::new().await).await;
290 let _ignored1 = client
291 .add_invoice("foo", Millisatoshi::new(1_000))
292 .await
293 .unwrap();
294 assert_eq!(client.lookup_invoice([0; 32]).await.unwrap(), None);
295 }
296}