Skip to content

Commit 1fa86a7

Browse files
committed
plugins: lsps: split up handlers
This commit separates the business logic from core-lighting behaviour and moves the core-lightning related implementations into the cln_adapters module. Splits up tests as well Signed-off-by: Peter Neuroth <pet.v.ne@gmail.com>
1 parent 69b4003 commit 1fa86a7

File tree

8 files changed

+1731
-7
lines changed

8 files changed

+1731
-7
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod hooks;
2+
pub mod rpc;
23
pub mod sender;
34
pub mod state;
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
use crate::{
2+
core::lsps2::provider::{
3+
Blockheight, BlockheightProvider, DatastoreProvider, LightningProvider, Lsps2OfferProvider,
4+
},
5+
proto::{
6+
lsps0::Msat,
7+
lsps2::{
8+
DatastoreEntry, Lsps2PolicyGetChannelCapacityRequest,
9+
Lsps2PolicyGetChannelCapacityResponse, Lsps2PolicyGetInfoRequest,
10+
Lsps2PolicyGetInfoResponse, OpeningFeeParams,
11+
},
12+
},
13+
};
14+
use anyhow::{Context, Result};
15+
use async_trait::async_trait;
16+
use bitcoin::secp256k1::PublicKey;
17+
use cln_rpc::{
18+
model::{
19+
requests::{
20+
DatastoreMode, DatastoreRequest, DeldatastoreRequest, FundchannelRequest,
21+
GetinfoRequest, ListdatastoreRequest, ListpeerchannelsRequest,
22+
},
23+
responses::ListdatastoreResponse,
24+
},
25+
primitives::{Amount, AmountOrAll, ChannelState, Sha256, ShortChannelId},
26+
ClnRpc,
27+
};
28+
use core::fmt;
29+
use serde::Serialize;
30+
use std::path::PathBuf;
31+
32+
pub const DS_MAIN_KEY: &'static str = "lsps";
33+
pub const DS_SUB_KEY: &'static str = "lsps2";
34+
35+
#[derive(Clone)]
36+
pub struct ClnApiRpc {
37+
rpc_path: PathBuf,
38+
}
39+
40+
impl ClnApiRpc {
41+
pub fn new(rpc_path: PathBuf) -> Self {
42+
Self { rpc_path }
43+
}
44+
45+
async fn create_rpc(&self) -> Result<ClnRpc> {
46+
ClnRpc::new(&self.rpc_path).await
47+
}
48+
}
49+
50+
#[async_trait]
51+
impl LightningProvider for ClnApiRpc {
52+
async fn fund_jit_channel(
53+
&self,
54+
peer_id: &PublicKey,
55+
amount: &Msat,
56+
) -> Result<(Sha256, String)> {
57+
let mut rpc = self.create_rpc().await?;
58+
let res = rpc
59+
.call_typed(&FundchannelRequest {
60+
announce: Some(false),
61+
close_to: None,
62+
compact_lease: None,
63+
feerate: None,
64+
minconf: None,
65+
mindepth: Some(0),
66+
push_msat: None,
67+
request_amt: None,
68+
reserve: None,
69+
channel_type: Some(vec![12, 46, 50]),
70+
utxos: None,
71+
amount: AmountOrAll::Amount(Amount::from_msat(amount.msat())),
72+
id: peer_id.to_owned(),
73+
})
74+
.await
75+
.with_context(|| "calling fundchannel")?;
76+
Ok((res.channel_id, res.txid))
77+
}
78+
79+
async fn is_channel_ready(&self, peer_id: &PublicKey, channel_id: &Sha256) -> Result<bool> {
80+
let mut rpc = self.create_rpc().await?;
81+
let r = rpc
82+
.call_typed(&ListpeerchannelsRequest {
83+
id: Some(peer_id.to_owned()),
84+
short_channel_id: None,
85+
})
86+
.await
87+
.with_context(|| "calling listpeerchannels")?;
88+
89+
let chs = r
90+
.channels
91+
.iter()
92+
.find(|&ch| ch.channel_id.is_some_and(|id| id == *channel_id));
93+
if let Some(ch) = chs {
94+
if ch.state == ChannelState::CHANNELD_NORMAL {
95+
return Ok(true);
96+
}
97+
}
98+
99+
return Ok(false);
100+
}
101+
}
102+
103+
#[async_trait]
104+
impl DatastoreProvider for ClnApiRpc {
105+
async fn store_buy_request(
106+
&self,
107+
scid: &ShortChannelId,
108+
peer_id: &PublicKey,
109+
opening_fee_params: &OpeningFeeParams,
110+
expected_payment_size: &Option<Msat>,
111+
) -> Result<bool> {
112+
let mut rpc = self.create_rpc().await?;
113+
#[derive(Serialize)]
114+
struct BorrowedDatastoreEntry<'a> {
115+
peer_id: &'a PublicKey,
116+
opening_fee_params: &'a OpeningFeeParams,
117+
#[serde(borrow)]
118+
expected_payment_size: &'a Option<Msat>,
119+
}
120+
121+
let ds = BorrowedDatastoreEntry {
122+
peer_id,
123+
opening_fee_params,
124+
expected_payment_size,
125+
};
126+
let json_str = serde_json::to_string(&ds)?;
127+
128+
let ds = DatastoreRequest {
129+
generation: None,
130+
hex: None,
131+
mode: Some(DatastoreMode::MUST_CREATE),
132+
string: Some(json_str),
133+
key: vec![
134+
DS_MAIN_KEY.to_string(),
135+
DS_SUB_KEY.to_string(),
136+
scid.to_string(),
137+
],
138+
};
139+
140+
let _ = rpc
141+
.call_typed(&ds)
142+
.await
143+
.map_err(anyhow::Error::new)
144+
.with_context(|| "calling datastore")?;
145+
146+
Ok(true)
147+
}
148+
149+
async fn get_buy_request(&self, scid: &ShortChannelId) -> Result<DatastoreEntry> {
150+
let mut rpc = self.create_rpc().await?;
151+
let key = vec![
152+
DS_MAIN_KEY.to_string(),
153+
DS_SUB_KEY.to_string(),
154+
scid.to_string(),
155+
];
156+
let res = rpc
157+
.call_typed(&ListdatastoreRequest {
158+
key: Some(key.clone()),
159+
})
160+
.await
161+
.with_context(|| "calling listdatastore")?;
162+
163+
let (rec, _) = deserialize_by_key(&res, key)?;
164+
Ok(rec)
165+
}
166+
167+
async fn del_buy_request(&self, scid: &ShortChannelId) -> Result<()> {
168+
let mut rpc = self.create_rpc().await?;
169+
let key = vec![
170+
DS_MAIN_KEY.to_string(),
171+
DS_SUB_KEY.to_string(),
172+
scid.to_string(),
173+
];
174+
175+
let _ = rpc
176+
.call_typed(&DeldatastoreRequest {
177+
generation: None,
178+
key,
179+
})
180+
.await;
181+
182+
Ok(())
183+
}
184+
}
185+
186+
#[async_trait]
187+
impl Lsps2OfferProvider for ClnApiRpc {
188+
async fn get_offer(
189+
&self,
190+
request: &Lsps2PolicyGetInfoRequest,
191+
) -> Result<Lsps2PolicyGetInfoResponse> {
192+
let mut rpc = self.create_rpc().await?;
193+
rpc.call_raw("lsps2-policy-getpolicy", request)
194+
.await
195+
.context("failed to call lsps2-policy-getpolicy")
196+
}
197+
198+
async fn get_channel_capacity(
199+
&self,
200+
params: &Lsps2PolicyGetChannelCapacityRequest,
201+
) -> Result<Lsps2PolicyGetChannelCapacityResponse> {
202+
let mut rpc = self.create_rpc().await?;
203+
rpc.call_raw("lsps2-policy-getchannelcapacity", params)
204+
.await
205+
.map_err(anyhow::Error::new)
206+
.with_context(|| "calling lsps2-policy-getchannelcapacity")
207+
}
208+
}
209+
210+
#[async_trait]
211+
impl BlockheightProvider for ClnApiRpc {
212+
async fn get_blockheight(&self) -> Result<Blockheight> {
213+
let mut rpc = self.create_rpc().await?;
214+
let info = rpc
215+
.call_typed(&GetinfoRequest {})
216+
.await
217+
.map_err(anyhow::Error::new)
218+
.with_context(|| "calling getinfo")?;
219+
Ok(info.blockheight)
220+
}
221+
}
222+
223+
#[derive(Debug)]
224+
pub enum DsError {
225+
/// No datastore entry with this exact key.
226+
NotFound { key: Vec<String> },
227+
/// Entry existed but had neither `string` nor `hex`.
228+
MissingValue { key: Vec<String> },
229+
/// JSON parse failed (from `string` or decoded `hex`).
230+
JsonParse {
231+
key: Vec<String>,
232+
source: serde_json::Error,
233+
},
234+
/// Hex decode failed.
235+
HexDecode {
236+
key: Vec<String>,
237+
source: hex::FromHexError,
238+
},
239+
}
240+
241+
impl fmt::Display for DsError {
242+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243+
match self {
244+
DsError::NotFound { key } => write!(f, "no datastore entry for key {:?}", key),
245+
DsError::MissingValue { key } => write!(
246+
f,
247+
"datastore entry had neither `string` nor `hex` for key {:?}",
248+
key
249+
),
250+
DsError::JsonParse { key, source } => {
251+
write!(f, "failed to parse JSON at key {:?}: {}", key, source)
252+
}
253+
DsError::HexDecode { key, source } => {
254+
write!(f, "failed to decode hex at key {:?}: {}", key, source)
255+
}
256+
}
257+
}
258+
}
259+
260+
impl std::error::Error for DsError {}
261+
262+
pub fn deserialize_by_key<K>(
263+
resp: &ListdatastoreResponse,
264+
key: K,
265+
) -> std::result::Result<(DatastoreEntry, Option<u64>), DsError>
266+
where
267+
K: AsRef<[String]>,
268+
{
269+
let wanted: &[String] = key.as_ref();
270+
271+
let ds = resp
272+
.datastore
273+
.iter()
274+
.find(|d| d.key.as_slice() == wanted)
275+
.ok_or_else(|| DsError::NotFound {
276+
key: wanted.to_vec(),
277+
})?;
278+
279+
// Prefer `string`, fall back to `hex`
280+
if let Some(s) = &ds.string {
281+
let value = serde_json::from_str::<DatastoreEntry>(s).map_err(|e| DsError::JsonParse {
282+
key: ds.key.clone(),
283+
source: e,
284+
})?;
285+
return Ok((value, ds.generation));
286+
}
287+
288+
if let Some(hx) = &ds.hex {
289+
let bytes = hex::decode(hx).map_err(|e| DsError::HexDecode {
290+
key: ds.key.clone(),
291+
source: e,
292+
})?;
293+
let value =
294+
serde_json::from_slice::<DatastoreEntry>(&bytes).map_err(|e| DsError::JsonParse {
295+
key: ds.key.clone(),
296+
source: e,
297+
})?;
298+
return Ok((value, ds.generation));
299+
}
300+
301+
Err(DsError::MissingValue {
302+
key: ds.key.clone(),
303+
})
304+
}

0 commit comments

Comments
 (0)