Skip to content

Commit

Permalink
feat: add cln payment processor
Browse files Browse the repository at this point in the history
  • Loading branch information
JosephGoulden authored and scsibug committed Aug 14, 2024
1 parent 0d04b5e commit 5a21890
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 16 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
nostr.db
nostr.db-*
justfile
result
77 changes: 76 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ nostr = { version = "0.18.0", default-features = false, features = ["base", "nip
log = "0.4"
[target.'cfg(all(not(target_env = "msvc"), not(target_os = "openbsd")))'.dependencies]
tikv-jemallocator = "0.5"
cln-rpc = "0.1.9"

[dev-dependencies]
anyhow = "1"
Expand Down
8 changes: 7 additions & 1 deletion config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -203,18 +203,24 @@ limit_scrapers = false
# Enable pay to relay
#enabled = false

# Node interface to use
#processor = "ClnRest/LNBits"

# The cost to be admitted to relay
#admission_cost = 4200

# The cost in sats per post
#cost_per_event = 0

# Url of lnbits api
# Url of node api
#node_url = "<node url>"

# LNBits api secret
#api_secret = "<ln bits api>"

# Path to CLN rune
#rune_path = "<rune path>"

# Nostr direct message on signup
#direct_message=false

Expand Down
24 changes: 17 additions & 7 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ pub struct PayToRelay {
pub direct_message: bool, // Send direct message to user with invoice and terms
pub secret_key: Option<String>,
pub processor: Processor,
pub rune_path: Option<String>, // To access clightning API
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -247,17 +248,25 @@ impl Settings {

// Validate pay to relay settings
if settings.pay_to_relay.enabled {
assert_ne!(settings.pay_to_relay.api_secret, "");
if settings.pay_to_relay.processor == Processor::ClnRest {
assert!(settings
.pay_to_relay
.rune_path
.as_ref()
.is_some_and(|path| path != "<rune path>"));
} else if settings.pay_to_relay.processor == Processor::LNBits {
assert_ne!(settings.pay_to_relay.api_secret, "");
}
// Should check that url is valid
assert_ne!(settings.pay_to_relay.node_url, "");
assert_ne!(settings.pay_to_relay.terms_message, "");

if settings.pay_to_relay.direct_message {
assert_ne!(
settings.pay_to_relay.secret_key,
Some("<nostr nsec>".to_string())
);
assert!(settings.pay_to_relay.secret_key.is_some());
assert!(settings
.pay_to_relay
.secret_key
.as_ref()
.is_some_and(|key| key != "<nostr nsec>"));
}
}

Expand Down Expand Up @@ -309,7 +318,7 @@ impl Default for Settings {
event_persist_buffer: 4096,
event_kind_blacklist: None,
event_kind_allowlist: None,
limit_scrapers: false
limit_scrapers: false,
},
authorization: Authorization {
pubkey_whitelist: None, // Allow any address to publish
Expand All @@ -323,6 +332,7 @@ impl Default for Settings {
terms_message: "".to_string(),
node_url: "".to_string(),
api_secret: "".to_string(),
rune_path: None,
sign_ups: false,
direct_message: false,
secret_key: None,
Expand Down
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub enum Error {
CommandUnknownError,
#[error("SQL error")]
SqlError(rusqlite::Error),
#[error("Config error")]
#[error("Config error : {0}")]
ConfigError(config::ConfigError),
#[error("Data directory does not exist")]
DatabaseDirError,
Expand Down
137 changes: 137 additions & 0 deletions src/payment/cln_rest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use std::{fs, str::FromStr};

use async_trait::async_trait;
use cln_rpc::{
model::{
requests::InvoiceRequest,
responses::{InvoiceResponse, ListinvoicesInvoicesStatus, ListinvoicesResponse},
},
primitives::{Amount, AmountOrAny},
};
use config::ConfigError;
use http::{header::CONTENT_TYPE, HeaderValue, Uri};
use hyper::{client::HttpConnector, Client};
use hyper_rustls::HttpsConnector;
use nostr::Keys;
use rand::random;

use crate::{
config::Settings,
error::{Error, Result},
};

use super::{InvoiceInfo, InvoiceStatus, PaymentProcessor};

#[derive(Clone)]
pub struct ClnRestPaymentProcessor {
client: hyper::Client<HttpsConnector<HttpConnector>, hyper::Body>,
settings: Settings,
rune_header: HeaderValue,
}

impl ClnRestPaymentProcessor {
pub fn new(settings: &Settings) -> Result<Self> {
let rune_path = settings
.pay_to_relay
.rune_path
.clone()
.ok_or(ConfigError::NotFound("rune_path".to_string()))?;
let rune = String::from_utf8(fs::read(rune_path)?)
.map_err(|_| ConfigError::Message("Rune should be UTF8".to_string()))?;
let mut rune_header = HeaderValue::from_str(&rune.trim())
.map_err(|_| ConfigError::Message("Invalid Rune header".to_string()))?;
rune_header.set_sensitive(true);

let https = hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()
.https_only()
.enable_http1()
.build();
let client = Client::builder().build::<_, hyper::Body>(https);

Ok(Self {
client,
settings: settings.clone(),
rune_header,
})
}
}

#[async_trait]
impl PaymentProcessor for ClnRestPaymentProcessor {
async fn get_invoice(&self, key: &Keys, amount: u64) -> Result<InvoiceInfo, Error> {
let random_number: u16 = random();
let memo = format!("{}: {}", random_number, key.public_key());

let body = InvoiceRequest {
cltv: None,
deschashonly: None,
expiry: None,
preimage: None,
exposeprivatechannels: None,
fallbacks: None,
amount_msat: AmountOrAny::Amount(Amount::from_sat(amount)),
description: memo.clone(),
label: "Nostr".to_string(),
};
let uri = Uri::from_str(&format!(
"{}/v1/invoice",
&self.settings.pay_to_relay.node_url
))
.map_err(|_| ConfigError::Message("Bad node URL".to_string()))?;

let req = hyper::Request::builder()
.method(hyper::Method::POST)
.uri(uri)
.header(CONTENT_TYPE, HeaderValue::from_static("application/json"))
.header("Rune", self.rune_header.clone())
.body(hyper::Body::from(serde_json::to_string(&body)?))
.expect("request builder");

let res = self.client.request(req).await?;

let body = hyper::body::to_bytes(res.into_body()).await?;
let invoice_response: InvoiceResponse = serde_json::from_slice(&body)?;

Ok(InvoiceInfo {
pubkey: key.public_key().to_string(),
payment_hash: invoice_response.payment_hash.to_string(),
bolt11: invoice_response.bolt11,
amount,
memo,
status: InvoiceStatus::Unpaid,
confirmed_at: None,
})
}

async fn check_invoice(&self, payment_hash: &str) -> Result<InvoiceStatus, Error> {
let uri = Uri::from_str(&format!(
"{}/v1/listinvoices?payment_hash={}",
&self.settings.pay_to_relay.node_url, payment_hash
))
.map_err(|_| ConfigError::Message("Bad node URL".to_string()))?;

let req = hyper::Request::builder()
.method(hyper::Method::POST)
.uri(uri)
.header(CONTENT_TYPE, HeaderValue::from_static("application/json"))
.header("Rune", self.rune_header.clone())
.body(hyper::Body::empty())
.expect("request builder");

let res = self.client.request(req).await?;

let body = hyper::body::to_bytes(res.into_body()).await?;
let invoice_response: ListinvoicesResponse = serde_json::from_slice(&body)?;
let invoice = invoice_response
.invoices
.first()
.ok_or(Error::CustomError("Invoice not found".to_string()))?;
let status = match invoice.status {
ListinvoicesInvoicesStatus::PAID => InvoiceStatus::Paid,
ListinvoicesInvoicesStatus::UNPAID => InvoiceStatus::Unpaid,
ListinvoicesInvoicesStatus::EXPIRED => InvoiceStatus::Expired,
};
Ok(status)
}
}
Loading

0 comments on commit 5a21890

Please sign in to comment.