Skip to content

Commit df3150b

Browse files
committed
sdk: Added implementations for sending/receiving off-/on-chain funds
1 parent 75af5ec commit df3150b

File tree

5 files changed

+175
-29
lines changed

5 files changed

+175
-29
lines changed

libs/gl-plugin/src/node/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ static LIMITER: OnceCell<RateLimiter<NotKeyed, InMemoryState, MonotonicClock>> =
3434
static RPC_CLIENT: OnceCell<Arc<Mutex<cln_rpc::ClnRpc>>> = OnceCell::const_new();
3535
static RPC_POLL_INTERVAL: Duration = Duration::from_millis(500);
3636

37+
#[allow(unused)]
3738
const OPT_SUPPORTS_LSPS: usize = 729;
3839

3940
pub async fn get_rpc<P: AsRef<Path>>(path: P) -> Arc<Mutex<cln_rpc::ClnRpc>> {
@@ -616,6 +617,7 @@ use cln_grpc::pb::node_server::NodeServer;
616617
#[derive(Clone, Debug)]
617618
struct Lsps2Offer {
618619
node_id: String,
620+
#[allow(unused)]
619621
params: Vec<crate::responses::OpeningFeeParams>,
620622
}
621623

libs/gl-sdk/.tasks.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,23 +76,22 @@ tasks:
7676
cmds:
7777
- task: build-release
7878
- task: bindings-python
79-
- uv run python build.py
8079
- uv build
8180

8281
install-python:
8382
desc: "Install Python package in development mode"
8483
dir: "{{.ROOT_DIR}}/libs/gl-sdk"
8584
cmds:
8685
- task: package-python
86+
- uv sync
8787
- uv pip install -e .
8888

8989
prepare-test:
9090
desc: "Prepare test environment by copying bindings to package"
9191
dir: "{{.ROOT_DIR}}/libs/gl-sdk"
9292
cmds:
9393
- task: bindings-python
94-
- uv run python build.py
95-
94+
- task: install-python
9695
test:
9796
desc: "Run gl-sdk tests (loads library from source)"
9897
dir: "{{.ROOT_DIR}}/libs/gl-sdk"

libs/gl-sdk/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ pub enum Error {
1616

1717
#[error("Error calling the rpc: {0}")]
1818
Rpc(String),
19+
20+
#[error("Invalid argument: {0}={1}")]
21+
Argument(String, String),
1922

2023
#[error("Generic error: {0}")]
2124
Other(String),

libs/gl-sdk/src/node.rs

Lines changed: 161 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ impl Node {
3535
})
3636
}
3737

38+
/// Stop the node if it is currently running.
3839
pub fn stop(&self) -> Result<(), Error> {
3940
let mut cln_client = exec(self.get_cln_client())?.clone();
4041

@@ -43,7 +44,7 @@ impl Node {
4344
// It's ok, the error here is expected and should just be
4445
// telling us that we've lost the connection. This is to
4546
// be expected on shutdown, so we clamp this to success.
46-
exec(cln_client.stop(req));
47+
let _ = exec(cln_client.stop(req));
4748
Ok(())
4849
}
4950

@@ -78,18 +79,95 @@ impl Node {
7879
Ok(ReceiveResponse { bolt11: res.bolt11 })
7980
}
8081

81-
fn onchain_send(&self, req: &OnchainSendRequest) -> Result<OnchainSendResponse, Error> {
82-
unimplemented!()
82+
fn send(&self, invoice: String, amount_msat: Option<u64>) -> Result<SendResponse, Error> {
83+
let mut cln_client = exec(self.get_cln_client())?.clone();
84+
let req = clnpb::PayRequest {
85+
amount_msat: match amount_msat {
86+
Some(a) => Some(clnpb::Amount { msat: a }),
87+
None => None,
88+
},
89+
90+
bolt11: invoice,
91+
description: None,
92+
exclude: vec![],
93+
exemptfee: None,
94+
label: None,
95+
localinvreqid: None,
96+
maxdelay: None,
97+
maxfee: None,
98+
maxfeepercent: None,
99+
partial_msat: None,
100+
retry_for: None,
101+
riskfactor: None,
102+
};
103+
exec(cln_client.pay(req))
104+
.map_err(|e| Error::Rpc(e.to_string()))
105+
.map(|r| r.into_inner().into())
83106
}
84-
fn onchain_receive(
107+
108+
fn onchain_send(
85109
&self,
86-
addresstype: Option<AddressType>,
87-
) -> Result<OnchainReceiveResponse, Error> {
88-
unimplemented!()
110+
destination: String,
111+
amount_or_all: String,
112+
) -> Result<OnchainSendResponse, Error> {
113+
let mut cln_client = exec(self.get_cln_client())?.clone();
114+
115+
// Decode what the user intends to do. Either we have `all`,
116+
// or we have an amount that we can parse.
117+
let (num, suffix): (String, String) = amount_or_all.chars().partition(|c| c.is_digit(10));
118+
119+
let num = if num.len() > 0 {
120+
num.parse::<u64>().unwrap()
121+
} else {
122+
0
123+
};
124+
let satoshi = match (num, suffix.as_ref()) {
125+
(n, "") | (n, "sat") => clnpb::AmountOrAll {
126+
// No value suffix, interpret as satoshis. This is an
127+
// onchain RPC method, hence the sat denomination by
128+
// default.
129+
value: Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount {
130+
msat: n * 1000,
131+
})),
132+
},
133+
(n, "msat") => clnpb::AmountOrAll {
134+
// No value suffix, interpret as satoshis. This is an
135+
// onchain RPC method, hence the sat denomination by
136+
// default.
137+
value: Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount {
138+
msat: n * 1000,
139+
})),
140+
},
141+
(0, "all") => clnpb::AmountOrAll {
142+
value: Some(clnpb::amount_or_all::Value::All(true)),
143+
},
144+
(_, _) => return Err(Error::Argument("amount_or_all".to_owned(), amount_or_all)),
145+
};
146+
147+
let req = clnpb::WithdrawRequest {
148+
destination: destination,
149+
minconf: None,
150+
feerate: None,
151+
satoshi: Some(satoshi),
152+
utxos: vec![],
153+
};
154+
155+
exec(cln_client.withdraw(req))
156+
.map_err(|e| Error::Rpc(e.to_string()))
157+
.map(|r| r.into_inner().into())
89158
}
90159

91-
fn send(&self, invoice: String) -> SendResponse {
92-
unimplemented!()
160+
fn onchain_receive(&self) -> Result<OnchainReceiveResponse, Error> {
161+
let mut cln_client = exec(self.get_cln_client())?.clone();
162+
163+
let req = clnpb::NewaddrRequest {
164+
addresstype: Some(clnpb::newaddr_request::NewaddrAddresstype::All.into()),
165+
};
166+
167+
let res = exec(cln_client.new_addr(req))
168+
.map_err(|e| Error::Rpc(e.to_string()))?
169+
.into_inner();
170+
Ok(res.into())
93171
}
94172
}
95173

@@ -113,31 +191,92 @@ impl Node {
113191
}
114192
}
115193

194+
#[allow(unused)]
116195
#[derive(uniffi::Object)]
117-
struct OnchainSendResponse {}
118-
119-
#[derive(uniffi::Object)]
120-
struct OnchainSendRequest {
121-
label: String,
122-
description: String,
123-
amount_msat: Option<u64>,
196+
struct OnchainSendResponse {
197+
tx: Vec<u8>,
198+
txid: Vec<u8>,
199+
psbt: String,
124200
}
125201

126-
#[derive(uniffi::Enum)]
127-
enum AddressType {
128-
BECH32,
129-
P2TR,
202+
impl From<clnpb::WithdrawResponse> for OnchainSendResponse {
203+
fn from(other: clnpb::WithdrawResponse) -> Self {
204+
Self {
205+
tx: other.tx,
206+
txid: other.txid,
207+
psbt: other.psbt,
208+
}
209+
}
130210
}
131211

212+
#[allow(unused)]
132213
#[derive(uniffi::Object)]
133214
struct OnchainReceiveResponse {
134-
address: String,
215+
bech32: String,
216+
p2tr: String,
217+
}
218+
219+
impl From<clnpb::NewaddrResponse> for OnchainReceiveResponse {
220+
fn from(other: clnpb::NewaddrResponse) -> Self {
221+
OnchainReceiveResponse {
222+
bech32: other.bech32.unwrap_or_default(),
223+
p2tr: other.p2tr.unwrap_or_default(),
224+
}
225+
}
135226
}
136227

228+
#[allow(unused)]
137229
#[derive(uniffi::Object)]
138-
struct SendResponse {}
230+
struct SendResponse {
231+
status: PayStatus,
232+
preimage: Vec<u8>,
233+
amount_msat: u64,
234+
amount_sent_msat: u64,
235+
parts: u32,
236+
}
237+
238+
impl From<clnpb::PayResponse> for SendResponse {
239+
fn from(other: clnpb::PayResponse) -> Self {
240+
Self {
241+
status: other.status.into(),
242+
preimage: other.payment_preimage,
243+
amount_msat: other.amount_msat.unwrap().msat,
244+
amount_sent_msat: other.amount_sent_msat.unwrap().msat,
245+
parts: other.parts,
246+
}
247+
}
248+
}
139249

250+
#[allow(unused)]
140251
#[derive(uniffi::Object)]
141252
struct ReceiveResponse {
142253
bolt11: String,
143254
}
255+
256+
#[derive(uniffi::Enum)]
257+
enum PayStatus {
258+
COMPLETE = 0,
259+
PENDING = 1,
260+
FAILED = 2,
261+
}
262+
263+
impl From<clnpb::pay_response::PayStatus> for PayStatus {
264+
fn from(other: clnpb::pay_response::PayStatus) -> Self {
265+
match other {
266+
clnpb::pay_response::PayStatus::Complete => PayStatus::COMPLETE,
267+
clnpb::pay_response::PayStatus::Failed => PayStatus::FAILED,
268+
clnpb::pay_response::PayStatus::Pending => PayStatus::PENDING,
269+
}
270+
}
271+
}
272+
273+
impl From<i32> for PayStatus {
274+
fn from(i: i32) -> Self {
275+
match i {
276+
0 => PayStatus::COMPLETE,
277+
1 => PayStatus::PENDING,
278+
2 => PayStatus::FAILED,
279+
o => panic!("Unknown pay_status {}", o),
280+
}
281+
}
282+
}

libs/gl-sdk/src/signer.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{Credentials, Error};
2-
use bip39::{Language, Mnemonic};
2+
use bip39::Mnemonic;
33
use std::str::FromStr;
44

55
#[derive(uniffi::Object, Clone)]
@@ -13,7 +13,7 @@ pub struct Signer {
1313
impl Signer {
1414
#[uniffi::constructor()]
1515
fn new(phrase: String) -> Result<Signer, Error> {
16-
let phrase = Mnemonic::from_str(phrase.as_str()).map_err(|e| Error::PhraseCorrupted())?;
16+
let phrase = Mnemonic::from_str(phrase.as_str()).map_err(|_e| Error::PhraseCorrupted())?;
1717
let seed = phrase.to_seed_normalized(&"").to_vec();
1818

1919
// FIXME: We may need to give the signer real credentials to
@@ -52,7 +52,7 @@ impl Signer {
5252
}
5353

5454
fn start(&self) -> Result<Handle, Error> {
55-
let (mut tx, mut rx) = tokio::sync::mpsc::channel(1);
55+
let (tx, rx) = tokio::sync::mpsc::channel(1);
5656

5757
let clone = self.clone();
5858
tokio::spawn(async move {
@@ -70,7 +70,10 @@ impl Signer {
7070
// Not exported through uniffi, internal logic only.
7171
impl Signer {
7272
async fn run(&self, signal: tokio::sync::mpsc::Receiver<()>) {
73-
self.inner.run_forever(signal).await.expect("Error running signer loop");
73+
self.inner
74+
.run_forever(signal)
75+
.await
76+
.expect("Error running signer loop");
7477
}
7578
}
7679

0 commit comments

Comments
 (0)