Skip to content

Commit 403c882

Browse files
committed
add pay_offer phoenixd
1 parent ca5e6b5 commit 403c882

File tree

7 files changed

+129
-17
lines changed

7 files changed

+129
-17
lines changed

bindings/lni_nodejs/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export declare class PhoenixdNode {
175175
getInfo(): Promise<NodeInfo>
176176
makeInvoice(params: PhoenixdMakeInvoiceParams): Promise<Transaction>
177177
lookupInvoice(paymentHash: string): Promise<Transaction>
178+
payOffer(offer: string, amount: number, payerNote?: string | undefined | null): Promise<PayInvoiceResponse>
178179
listTransactions(params: ListTransactionsParams): Promise<Array<Transaction>>
179180
}
180181
export declare class Db {

bindings/lni_nodejs/main.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ const invoice = await node.makeInvoice({
2121
});
2222
console.log("Invoice:", invoice);
2323

24-
const lookupInvoice = await node.lookupInvoice(config.test_hash);
24+
const lookupInvoice = await node.lookupInvoice(process.env.PHOENIXD_TEST_PAYMENT_HASH);
2525
console.log("lookupInvoice:", lookupInvoice);
2626

27+
const payOffer = await node.payOffer(process.env.TEST_OFFER, 22, 'payment from lni nodejs');
28+
console.log("payOffer:", payOffer);
29+
2730
const txns = await node.listTransactions({
2831
from: 0,
2932
until: 0,

bindings/lni_nodejs/src/phoenixd.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,28 @@ impl PhoenixdNode {
7070
}
7171

7272
#[napi]
73-
pub async fn list_transactions(&self, params: lni::phoenixd::lib::ListTransactionsParams) -> napi::Result<Vec<lni::Transaction>> {
73+
pub async fn pay_offer(
74+
&self,
75+
offer: String,
76+
amount: i64,
77+
payer_note: Option<String>,
78+
) -> napi::Result<lni::PayInvoiceResponse> {
79+
let offer = lni::phoenixd::api::pay_offer(
80+
self.inner.url.clone(),
81+
self.inner.password.clone(),
82+
offer,
83+
amount,
84+
payer_note,
85+
).await
86+
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
87+
Ok(offer)
88+
}
89+
90+
#[napi]
91+
pub async fn list_transactions(
92+
&self,
93+
params: lni::phoenixd::lib::ListTransactionsParams,
94+
) -> napi::Result<Vec<lni::Transaction>> {
7495
let txns = lni::phoenixd::api::list_transactions(
7596
self.inner.url.clone(),
7697
self.inner.password.clone(),

bindings/lni_uniffi/src/lni.udl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ interface PhoenixdNode {
1414

1515
[Throws=ApiError, Async]
1616
sequence<Transaction> list_transactions(ListTransactionsParams params);
17+
18+
[Throws=ApiError, Async]
19+
PayInvoiceResponse pay_offer(string offer, i64 amount, string? payer_note );
20+
1721
};
1822

1923
interface Db {

crates/lni/phoenixd/api.rs

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use crate::{ApiError, InvoiceType, NodeInfo, Transaction};
1+
use super::lib::Bolt11Resp;
2+
use crate::{ApiError, InvoiceType, NodeInfo, PayInvoiceResponse, Transaction};
23
use serde::{Deserialize, Serialize};
34
use serde_urlencoded;
4-
use super::lib::Bolt11Resp;
55

66
/// https://phoenix.acinq.co/server/api
77
@@ -75,6 +75,17 @@ pub struct OutgoingPaymentResponse {
7575
pub is_paid: bool,
7676
}
7777

78+
#[derive(Debug, Serialize, Deserialize)]
79+
pub struct PayResponse {
80+
#[serde(rename = "paymentId")]
81+
pub payment_id: String,
82+
#[serde(rename = "paymentPreimage")]
83+
pub preimage: String,
84+
#[serde(rename = "paymentHash")]
85+
pub payment_hash: String,
86+
#[serde(rename = "routingFeeSat")]
87+
pub routing_fee_sat: i64,
88+
}
7889

7990
pub fn get_info(url: String, password: String) -> Result<NodeInfo, ApiError> {
8091
let url = format!("{}/getinfo", url);
@@ -114,8 +125,8 @@ pub async fn make_invoice(
114125
description: description.clone(),
115126
amount_sat: amount,
116127
expiry_seconds: expiry.unwrap_or(3600),
117-
external_id: None, // TODO
118-
webhook_url: None, // TODO
128+
external_id: None, // TODO
129+
webhook_url: None, // TODO
119130
};
120131

121132
let response: reqwest::blocking::Response = client
@@ -128,7 +139,7 @@ pub async fn make_invoice(
128139
println!("Status: {}", response.status());
129140

130141
let invoice_str = response.text().unwrap();
131-
let invoice_str = invoice_str.as_str();
142+
let invoice_str = invoice_str.as_str();
132143
println!("Bolt11 {}", &invoice_str.to_string());
133144

134145
let bolt11_resp: Bolt11Resp =
@@ -175,6 +186,37 @@ pub async fn make_invoice(
175186
}
176187
}
177188

189+
pub async fn pay_offer(
190+
url: String,
191+
password: String,
192+
offer: String,
193+
amount: i64,
194+
payer_note: Option<String>,
195+
) -> Result<PayInvoiceResponse, ApiError> {
196+
let client = reqwest::blocking::Client::new();
197+
let req_url = format!("{}/payoffer", url);
198+
let response: reqwest::blocking::Response = client
199+
.post(&req_url)
200+
.basic_auth("", Some(password))
201+
.form(&[
202+
("amountSat", amount.to_string()),
203+
("offer", offer),
204+
("message", payer_note.unwrap_or_default()),
205+
])
206+
.send()
207+
.unwrap();
208+
let response_text = response.text().unwrap();
209+
let response_text = response_text.as_str();
210+
let pay_resp: PayResponse = match serde_json::from_str(&response_text) {
211+
Ok(resp) => resp,
212+
Err(e) => return Err(ApiError::Json { reason: e.to_string() }),
213+
};
214+
Ok(PayInvoiceResponse {
215+
preimage: pay_resp.preimage,
216+
fee: pay_resp.routing_fee_sat,
217+
})
218+
}
219+
178220
pub fn lookup_invoice(
179221
url: String,
180222
password: String,
@@ -203,7 +245,6 @@ pub fn lookup_invoice(
203245
Ok(txn)
204246
}
205247

206-
207248
pub fn list_transactions(
208249
url: String,
209250
password: String,
@@ -263,10 +304,10 @@ pub fn list_transactions(
263304
amount: inv.received_sat,
264305
fees_paid: inv.fees * 1000,
265306
created_at: (inv.created_at / 1000) as i64,
266-
expires_at: 0, // TODO
307+
expires_at: 0, // TODO
267308
settled_at: settled_at.unwrap_or(0),
268309
description: inv.payer_note.unwrap_or_default(), // TODO description or payer_note?
269-
description_hash: "".to_string(), // or parse if needed
310+
description_hash: "".to_string(), // or parse if needed
270311
}
271312
})
272313
.collect();
@@ -298,7 +339,8 @@ pub fn list_transactions(
298339
.send();
299340
let outgoing_text = outgoing_resp.unwrap().text().unwrap();
300341
let outgoing_text = outgoing_text.as_str();
301-
let outgoing_payments: Vec<OutgoingPaymentResponse> = serde_json::from_str(&outgoing_text).unwrap();
342+
let outgoing_payments: Vec<OutgoingPaymentResponse> =
343+
serde_json::from_str(&outgoing_text).unwrap();
302344

303345
// Convert outgoing payments into "outgoing" Transaction
304346
for payment in outgoing_payments {
@@ -309,15 +351,15 @@ pub fn list_transactions(
309351
};
310352
transactions.push(Transaction {
311353
type_: "outgoing".to_string(),
312-
invoice: "".to_string(), // TODO
354+
invoice: "".to_string(), // TODO
313355
preimage: payment.preimage,
314356
payment_hash: payment.payment_hash,
315357
amount: payment.sent * 1000,
316358
fees_paid: payment.fees * 1000,
317359
created_at: (payment.created_at / 1000) as i64,
318360
expires_at: 0, // TODO
319361
settled_at: settled_at.unwrap_or(0),
320-
description: "".to_string(), // not in OutgoingPaymentResponse data
362+
description: "".to_string(), // not in OutgoingPaymentResponse data
321363
description_hash: "".to_string(),
322364
});
323365
}
@@ -327,4 +369,3 @@ pub fn list_transactions(
327369

328370
Ok(transactions)
329371
}
330-

crates/lni/phoenixd/lib.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#[cfg(feature = "napi_rs")]
22
use napi_derive::napi;
33

4-
use crate::{phoenixd::api::*, ApiError, InvoiceType, Transaction};
4+
use crate::{phoenixd::api::*, ApiError, InvoiceType, PayInvoiceResponse, Transaction};
55
use serde::{Deserialize, Serialize};
66

77
#[cfg_attr(feature = "napi_rs", napi(object))]
@@ -76,7 +76,26 @@ impl PhoenixdNode {
7676
.await
7777
}
7878

79-
pub async fn lookup_invoice(&self, payment_hash: String) -> Result<crate::Transaction, ApiError> {
79+
pub async fn pay_offer(
80+
&self,
81+
offer: String,
82+
amount: i64,
83+
payer_note: Option<String>,
84+
) -> Result<PayInvoiceResponse, ApiError> {
85+
crate::phoenixd::api::pay_offer(
86+
self.url.clone(),
87+
self.password.clone(),
88+
offer,
89+
amount,
90+
payer_note,
91+
)
92+
.await
93+
}
94+
95+
pub async fn lookup_invoice(
96+
&self,
97+
payment_hash: String,
98+
) -> Result<crate::Transaction, ApiError> {
8099
crate::phoenixd::api::lookup_invoice(self.url.clone(), self.password.clone(), payment_hash)
81100
}
82101

@@ -124,6 +143,10 @@ mod tests {
124143
dotenv().ok();
125144
env::var("PHOENIXD_TEST_PAYMENT_HASH").expect("PHOENIXD_TEST_PAYMENT_HASH must be set")
126145
};
146+
static ref TEST_OFFER: String = {
147+
dotenv().ok();
148+
env::var("TEST_OFFER").expect("TEST_OFFER must be set")
149+
};
127150
}
128151

129152
#[test]
@@ -164,6 +187,22 @@ mod tests {
164187
}
165188
}
166189

190+
#[test]
191+
async fn test_pay_offer() {
192+
match NODE
193+
.pay_offer(TEST_OFFER.to_string(), 11, Some("payment from lni".to_string()))
194+
.await
195+
{
196+
Ok(resp) => {
197+
println!("Pay invoice resp: {:?}", resp);
198+
assert!(!resp.preimage.is_empty(), "Preimage should not be empty");
199+
}
200+
Err(e) => {
201+
panic!("Failed to pay offer: {:?}", e);
202+
}
203+
}
204+
}
205+
167206
#[test]
168207
async fn test_list_transactions() {
169208
let params = ListTransactionsParams {

readme.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ let cln_rune = cln_node.key();
4040
```rust
4141
lni.create_invoice(amount, expiration, memo, BOLT11 | BOLT12)
4242
lni.pay_invoice()
43+
lni.pay_offer(offer)
4344
lni.fetch_invoice_from_offer('lno***')
4445
lni.decode_invoice(invoice)
4546
lni.check_invoice_status(invoice)
@@ -119,6 +120,7 @@ node main.mjs
119120
PHOENIXD_URL=http://localhost:9740
120121
PHOENIXD_PASSWORD=YOUR_HTTP_PASSWORD
121122
PHOENIXD_TEST_PAYMENT_HASH=YOUR_TEST_PAYMENT_HASH
123+
TEST_OFFER=lno***
122124
```
123125
124126
Bindings
@@ -127,7 +129,8 @@ Bindings
127129
- nodejs
128130
- napi_rs
129131
- https://napi.rs/docs/introduction/simple-package
130-
- `cd bindings/lni_nodejs && cargo build`
132+
- `cd bindings/lni_nodejs && cargo build && yarn build`
133+
- test `node main.mjs`
131134
132135
- react-native
133136
- uniffi-bindgen-react-native

0 commit comments

Comments
 (0)