mirror of
https://github.com/scsibug/nostr-rs-relay.git
synced 2024-12-23 08:55:51 -05:00
feat: add cln payment processor
This commit is contained in:
parent
0d04b5eefa
commit
5a21890625
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
|||
nostr.db
|
||||
nostr.db-*
|
||||
justfile
|
||||
result
|
||||
|
|
77
Cargo.lock
generated
77
Cargo.lock
generated
|
@ -438,6 +438,26 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin"
|
||||
version = "0.30.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1945a5048598e4189e239d3f809b19bdad4845c4b2ba400d304d2dcf26d2c462"
|
||||
dependencies = [
|
||||
"bech32",
|
||||
"bitcoin-private",
|
||||
"bitcoin_hashes 0.12.0",
|
||||
"hex_lit",
|
||||
"secp256k1 0.27.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-private"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.10.0"
|
||||
|
@ -456,6 +476,16 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501"
|
||||
dependencies = [
|
||||
"bitcoin-private",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
@ -611,6 +641,24 @@ version = "0.7.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
||||
|
||||
[[package]]
|
||||
name = "cln-rpc"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "974dac6f40275b7b828087f4f9973c39658f9b4a46cc589c083a2c6c27cf67cb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitcoin 0.30.2",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cloudabi"
|
||||
version = "0.0.3"
|
||||
|
@ -1323,6 +1371,12 @@ version = "0.4.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hex_lit"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
|
@ -1773,7 +1827,7 @@ checksum = "35c0446103768cddfb2bc1b87a52e98c35227b82711c2b3ce7098f8d85d9b0ee"
|
|||
dependencies = [
|
||||
"aes",
|
||||
"base64 0.21.7",
|
||||
"bitcoin",
|
||||
"bitcoin 0.29.2",
|
||||
"cbc",
|
||||
"getrandom",
|
||||
"instant",
|
||||
|
@ -1797,6 +1851,7 @@ dependencies = [
|
|||
"bitcoin_hashes 0.10.0",
|
||||
"chrono",
|
||||
"clap",
|
||||
"cln-rpc",
|
||||
"config",
|
||||
"console-subscriber",
|
||||
"const_format",
|
||||
|
@ -2820,6 +2875,17 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f"
|
||||
dependencies = [
|
||||
"bitcoin_hashes 0.12.0",
|
||||
"secp256k1-sys 0.8.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1-sys"
|
||||
version = "0.4.2"
|
||||
|
@ -2838,6 +2904,15 @@ dependencies = [
|
|||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1-sys"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.9.2"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
@ -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>"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
137
src/payment/cln_rest.rs
Normal file
137
src/payment/cln_rest.rs
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
use crate::error::{Error, Result};
|
||||
use crate::event::Event;
|
||||
use crate::payment::cln_rest::ClnRestPaymentProcessor;
|
||||
use crate::payment::lnbits::LNBitsPaymentProcessor;
|
||||
use crate::repo::NostrRepo;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -10,6 +11,7 @@ use async_trait::async_trait;
|
|||
use nostr::key::{FromPkStr, FromSkStr};
|
||||
use nostr::{key::Keys, Event as NostrEvent, EventBuilder};
|
||||
|
||||
pub mod cln_rest;
|
||||
pub mod lnbits;
|
||||
|
||||
/// Payment handler
|
||||
|
@ -41,6 +43,7 @@ pub trait PaymentProcessor: Send + Sync {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub enum Processor {
|
||||
LNBits,
|
||||
ClnRest,
|
||||
}
|
||||
|
||||
/// Possible states of an invoice
|
||||
|
@ -109,8 +112,9 @@ impl Payment {
|
|||
};
|
||||
|
||||
// Create processor kind defined in settings
|
||||
let processor = match &settings.pay_to_relay.processor {
|
||||
let processor: Arc<dyn PaymentProcessor> = match &settings.pay_to_relay.processor {
|
||||
Processor::LNBits => Arc::new(LNBitsPaymentProcessor::new(&settings)),
|
||||
Processor::ClnRest => Arc::new(ClnRestPaymentProcessor::new(&settings)?),
|
||||
};
|
||||
|
||||
Ok(Payment {
|
||||
|
|
|
@ -568,6 +568,11 @@ async fn handle_web_request(
|
|||
.unwrap());
|
||||
}
|
||||
|
||||
// Account is checked async so user will have to refresh the page a couple times after
|
||||
// they have paid.
|
||||
if let Err(e) = payment_tx.send(PaymentMessage::CheckAccount(pubkey.clone())) {
|
||||
warn!("Could not check account: {}", e);
|
||||
}
|
||||
// Checks if user is already admitted
|
||||
let text =
|
||||
if let Ok((admission_status, _)) = repo.get_account_balance(&key.unwrap()).await {
|
||||
|
@ -894,11 +899,17 @@ pub fn start_server(settings: &Settings, shutdown_rx: MpscReceiver<()>) -> Resul
|
|||
bcast_tx.clone(),
|
||||
settings.clone(),
|
||||
);
|
||||
if let Ok(mut p) = payment_opt {
|
||||
tokio::task::spawn(async move {
|
||||
info!("starting payment process ...");
|
||||
p.run().await;
|
||||
});
|
||||
match payment_opt {
|
||||
Ok(mut p) => {
|
||||
tokio::task::spawn(async move {
|
||||
info!("starting payment process ...");
|
||||
p.run().await;
|
||||
});
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to start payment process {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user