|
| 1 | +use crate::{ |
| 2 | + jsonrpc::{server::RequestHandler, JsonRpcResponse as _, RequestObject, RpcError}, |
| 3 | + lsps2::model::{ |
| 4 | + Lsps2GetInfoRequest, Lsps2GetInfoResponse, Lsps2PolicyGetInfoRequest, |
| 5 | + Lsps2PolicyGetInfoResponse, OpeningFeeParams, Promise, |
| 6 | + }, |
| 7 | + util::unwrap_payload_with_peer_id, |
| 8 | +}; |
| 9 | +use anyhow::{Context, Result as AnyResult}; |
| 10 | +use async_trait::async_trait; |
| 11 | +use cln_rpc::ClnRpc; |
| 12 | +use std::path::PathBuf; |
| 13 | + |
| 14 | +#[async_trait] |
| 15 | +pub trait ClnApi: Send + Sync { |
| 16 | + async fn lsps2_getpolicy( |
| 17 | + &self, |
| 18 | + params: &Lsps2PolicyGetInfoRequest, |
| 19 | + ) -> AnyResult<Lsps2PolicyGetInfoResponse>; |
| 20 | +} |
| 21 | + |
| 22 | +#[derive(Clone)] |
| 23 | +pub struct ClnApiRpc { |
| 24 | + rpc_path: PathBuf, |
| 25 | +} |
| 26 | + |
| 27 | +impl ClnApiRpc { |
| 28 | + pub fn new(rpc_path: PathBuf) -> Self { |
| 29 | + Self { rpc_path } |
| 30 | + } |
| 31 | + |
| 32 | + async fn create_rpc(&self) -> AnyResult<ClnRpc> { |
| 33 | + ClnRpc::new(&self.rpc_path).await |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +#[async_trait] |
| 38 | +impl ClnApi for ClnApiRpc { |
| 39 | + async fn lsps2_getpolicy( |
| 40 | + &self, |
| 41 | + params: &Lsps2PolicyGetInfoRequest, |
| 42 | + ) -> AnyResult<Lsps2PolicyGetInfoResponse> { |
| 43 | + let mut rpc = self.create_rpc().await?; |
| 44 | + rpc.call_raw("dev-lsps2-getpolicy", params) |
| 45 | + .await |
| 46 | + .map_err(anyhow::Error::new) |
| 47 | + .with_context(|| "calling dev-lsps2-getpolicy") |
| 48 | + } |
| 49 | +} |
| 50 | + |
| 51 | +/// Handler for the `lsps2.get_info` method. |
| 52 | +pub struct Lsps2GetInfoHandler<A: ClnApi> { |
| 53 | + pub api: A, |
| 54 | + pub promise_secret: [u8; 32], |
| 55 | +} |
| 56 | + |
| 57 | +impl<A: ClnApi> Lsps2GetInfoHandler<A> { |
| 58 | + pub fn new(api: A, promise_secret: [u8; 32]) -> Self { |
| 59 | + Self { |
| 60 | + api, |
| 61 | + promise_secret, |
| 62 | + } |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +/// The RequestHandler calls the internal rpc command `dev-lsps2-getinfo`. It |
| 67 | +/// expects a plugin has registered this command and manages policies for the |
| 68 | +/// LSPS2 service. |
| 69 | +#[async_trait] |
| 70 | +impl<T: ClnApi + 'static> RequestHandler for Lsps2GetInfoHandler<T> { |
| 71 | + async fn handle(&self, payload: &[u8]) -> core::result::Result<Vec<u8>, RpcError> { |
| 72 | + let (payload, _) = unwrap_payload_with_peer_id(payload); |
| 73 | + |
| 74 | + let req: RequestObject<Lsps2GetInfoRequest> = serde_json::from_slice(&payload) |
| 75 | + .map_err(|e| RpcError::parse_error(format!("failed to parse request: {e}")))?; |
| 76 | + |
| 77 | + if req.id.is_none() { |
| 78 | + // Is a notification we can not reply so we just return |
| 79 | + return Ok(vec![]); |
| 80 | + } |
| 81 | + let params = req |
| 82 | + .params |
| 83 | + .ok_or(RpcError::invalid_params("expected params but was missing"))?; |
| 84 | + |
| 85 | + let policy_params: Lsps2PolicyGetInfoRequest = params.into(); |
| 86 | + let res_data: Lsps2PolicyGetInfoResponse = self |
| 87 | + .api |
| 88 | + .lsps2_getpolicy(&policy_params) |
| 89 | + .await |
| 90 | + .map_err(|e| RpcError { |
| 91 | + code: 200, |
| 92 | + message: format!("failed to fetch policy {e:#}"), |
| 93 | + data: None, |
| 94 | + })?; |
| 95 | + |
| 96 | + let opening_fee_params_menu = res_data |
| 97 | + .policy_opening_fee_params_menu |
| 98 | + .iter() |
| 99 | + .map(|v| { |
| 100 | + let promise: Promise = v |
| 101 | + .get_hmac_hex(&self.promise_secret) |
| 102 | + .try_into() |
| 103 | + .map_err(|e| RpcError::internal_error(format!("invalid promise: {e}")))?; |
| 104 | + Ok(OpeningFeeParams { |
| 105 | + min_fee_msat: v.min_fee_msat, |
| 106 | + proportional: v.proportional, |
| 107 | + valid_until: v.valid_until, |
| 108 | + min_lifetime: v.min_lifetime, |
| 109 | + max_client_to_self_delay: v.max_client_to_self_delay, |
| 110 | + min_payment_size_msat: v.min_payment_size_msat, |
| 111 | + max_payment_size_msat: v.max_payment_size_msat, |
| 112 | + promise, |
| 113 | + }) |
| 114 | + }) |
| 115 | + .collect::<Result<Vec<_>, RpcError>>()?; |
| 116 | + |
| 117 | + let res = Lsps2GetInfoResponse { |
| 118 | + opening_fee_params_menu, |
| 119 | + } |
| 120 | + .into_response(req.id.unwrap()); // We checked that we got an id before. |
| 121 | + |
| 122 | + serde_json::to_vec(&res) |
| 123 | + .map_err(|e| RpcError::internal_error(format!("Failed to serialize response: {}", e))) |
| 124 | + } |
| 125 | +} |
| 126 | + |
| 127 | +#[cfg(test)] |
| 128 | +mod tests { |
| 129 | + use std::sync::{Arc, Mutex}; |
| 130 | + |
| 131 | + use super::*; |
| 132 | + use crate::{ |
| 133 | + jsonrpc::{JsonRpcRequest, ResponseObject}, |
| 134 | + lsps0::primitives::{Msat, Ppm}, |
| 135 | + lsps2::model::PolicyOpeningFeeParams, |
| 136 | + util::wrap_payload_with_peer_id, |
| 137 | + }; |
| 138 | + use chrono::{TimeZone, Utc}; |
| 139 | + use cln_rpc::primitives::PublicKey; |
| 140 | + use cln_rpc::RpcError as ClnRpcError; |
| 141 | + |
| 142 | + const PUBKEY: [u8; 33] = [ |
| 143 | + 0x02, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, 0xce, 0x87, |
| 144 | + 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9, 0x59, 0xf2, 0x81, 0x5b, 0x16, |
| 145 | + 0xf8, 0x17, 0x98, |
| 146 | + ]; |
| 147 | + |
| 148 | + fn create_peer_id() -> PublicKey { |
| 149 | + PublicKey::from_slice(&PUBKEY).expect("Valid pubkey") |
| 150 | + } |
| 151 | + |
| 152 | + fn create_wrapped_request(request: &RequestObject<Lsps2GetInfoRequest>) -> Vec<u8> { |
| 153 | + let payload = serde_json::to_vec(request).expect("Failed to serialize request"); |
| 154 | + wrap_payload_with_peer_id(&payload, create_peer_id()) |
| 155 | + } |
| 156 | + |
| 157 | + #[derive(Clone, Default)] |
| 158 | + struct FakeCln { |
| 159 | + lsps2_getpolicy_response: Arc<Mutex<Option<Lsps2PolicyGetInfoResponse>>>, |
| 160 | + lsps2_getpolicy_error: Arc<Mutex<Option<ClnRpcError>>>, |
| 161 | + } |
| 162 | + |
| 163 | + #[async_trait] |
| 164 | + impl ClnApi for FakeCln { |
| 165 | + async fn lsps2_getpolicy( |
| 166 | + &self, |
| 167 | + _params: &Lsps2PolicyGetInfoRequest, |
| 168 | + ) -> Result<Lsps2PolicyGetInfoResponse, anyhow::Error> { |
| 169 | + if let Some(err) = self.lsps2_getpolicy_error.lock().unwrap().take() { |
| 170 | + return Err(anyhow::Error::new(err).context("from fake api")); |
| 171 | + }; |
| 172 | + if let Some(res) = self.lsps2_getpolicy_response.lock().unwrap().take() { |
| 173 | + return Ok(res); |
| 174 | + }; |
| 175 | + panic!("No lsps2 response defined"); |
| 176 | + } |
| 177 | + } |
| 178 | + |
| 179 | + #[tokio::test] |
| 180 | + async fn test_successful_get_info() { |
| 181 | + let promise_secret = [0u8; 32]; |
| 182 | + let params = Lsps2PolicyGetInfoResponse { |
| 183 | + policy_opening_fee_params_menu: vec![PolicyOpeningFeeParams { |
| 184 | + min_fee_msat: Msat(2000), |
| 185 | + proportional: Ppm(10000), |
| 186 | + valid_until: Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap(), |
| 187 | + min_lifetime: 1000, |
| 188 | + max_client_to_self_delay: 42, |
| 189 | + min_payment_size_msat: Msat(1000000), |
| 190 | + max_payment_size_msat: Msat(100000000), |
| 191 | + }], |
| 192 | + }; |
| 193 | + let promise = params.policy_opening_fee_params_menu[0].get_hmac_hex(&promise_secret); |
| 194 | + let fake = FakeCln::default(); |
| 195 | + *fake.lsps2_getpolicy_response.lock().unwrap() = Some(params); |
| 196 | + let handler = Lsps2GetInfoHandler::new(fake, promise_secret); |
| 197 | + |
| 198 | + let request = Lsps2GetInfoRequest { token: None }.into_request(Some("test-id".to_string())); |
| 199 | + let payload = create_wrapped_request(&request); |
| 200 | + |
| 201 | + let result = handler.handle(&payload).await.unwrap(); |
| 202 | + let response: ResponseObject<Lsps2GetInfoResponse> = |
| 203 | + serde_json::from_slice(&result).unwrap(); |
| 204 | + let response = response.into_inner().unwrap(); |
| 205 | + |
| 206 | + assert_eq!( |
| 207 | + response.opening_fee_params_menu[0].min_payment_size_msat, |
| 208 | + Msat(1000000) |
| 209 | + ); |
| 210 | + assert_eq!( |
| 211 | + response.opening_fee_params_menu[0].max_payment_size_msat, |
| 212 | + Msat(100000000) |
| 213 | + ); |
| 214 | + assert_eq!( |
| 215 | + response.opening_fee_params_menu[0].promise, |
| 216 | + promise.try_into().unwrap() |
| 217 | + ); |
| 218 | + } |
| 219 | + |
| 220 | + #[tokio::test] |
| 221 | + async fn test_get_info_rpc_error_handling() { |
| 222 | + let fake = FakeCln::default(); |
| 223 | + *fake.lsps2_getpolicy_error.lock().unwrap() = Some(ClnRpcError { |
| 224 | + code: Some(-1), |
| 225 | + message: "not found".to_string(), |
| 226 | + data: None, |
| 227 | + }); |
| 228 | + let handler = Lsps2GetInfoHandler::new(fake, [0; 32]); |
| 229 | + let request = Lsps2GetInfoRequest { token: None }.into_request(Some("test-id".to_string())); |
| 230 | + let payload = create_wrapped_request(&request); |
| 231 | + |
| 232 | + let result = handler.handle(&payload).await; |
| 233 | + |
| 234 | + assert!(result.is_err()); |
| 235 | + let error = result.unwrap_err(); |
| 236 | + assert_eq!(error.code, 200); |
| 237 | + assert!(error.message.contains("failed to fetch policy")); |
| 238 | + } |
| 239 | +} |
0 commit comments