1use std::path::PathBuf;
2use std::sync::Arc;
3
4use anyhow::Context;
5use axum::Router;
6use axum::extract::State;
7use axum::http::StatusCode;
8use axum::routing::{get, post};
9use cln_rpc::ClnRpc;
10use cln_rpc::primitives::{Amount as ClnAmount, AmountOrAny};
11use fedimint_core::fedimint_build_code_version_env;
12use fedimint_core::util::handle_version_hash_command;
13use fedimint_gateway_common::V1_API_ENDPOINT;
14use fedimint_logging::TracingSetup;
15use tokio::net::TcpListener;
16use tokio::sync::Mutex;
17use tower_http::cors::CorsLayer;
18
19use crate::cli::FaucetOpts;
20use crate::envs::FM_CLIENT_DIR_ENV;
21
22#[derive(Clone)]
23pub struct Faucet {
24 #[allow(unused)]
25 bitcoin: Arc<bitcoincore_rpc::Client>,
26 ln_rpc: Arc<Mutex<ClnRpc>>,
27}
28
29impl Faucet {
30 pub async fn new(opts: &FaucetOpts) -> anyhow::Result<Self> {
31 let url = opts.bitcoind_rpc.parse()?;
32 let (host, auth) = fedimint_bitcoind::bitcoincore::from_url_to_url_auth(&url)?;
33 let bitcoin = Arc::new(bitcoincore_rpc::Client::new(&host, auth)?);
34 let ln_rpc = Arc::new(Mutex::new(
35 ClnRpc::new(&opts.cln_socket)
36 .await
37 .with_context(|| format!("couldn't open CLN socket {}", &opts.cln_socket))?,
38 ));
39 Ok(Faucet { bitcoin, ln_rpc })
40 }
41
42 async fn pay_invoice(&self, invoice: String) -> anyhow::Result<()> {
43 let invoice_status = self
44 .ln_rpc
45 .lock()
46 .await
47 .call_typed(&cln_rpc::model::requests::PayRequest {
48 bolt11: invoice,
49 amount_msat: None,
50 label: None,
51 riskfactor: None,
52 maxfeepercent: None,
53 retry_for: None,
54 maxdelay: None,
55 exemptfee: None,
56 localinvreqid: None,
57 exclude: None,
58 maxfee: None,
59 description: None,
60 partial_msat: None,
61 })
62 .await?
63 .status;
64
65 anyhow::ensure!(
66 matches!(
67 invoice_status,
68 cln_rpc::model::responses::PayStatus::COMPLETE
69 ),
70 "payment not complete"
71 );
72 Ok(())
73 }
74
75 async fn generate_invoice(&self, amount: u64) -> anyhow::Result<String> {
76 Ok(self
77 .ln_rpc
78 .lock()
79 .await
80 .call_typed(&cln_rpc::model::requests::InvoiceRequest {
81 amount_msat: AmountOrAny::Amount(ClnAmount::from_sat(amount)),
82 description: "lnd-gw-to-cln".to_string(),
83 label: format!("faucet-{}", rand::random::<u64>()),
84 expiry: None,
85 fallbacks: None,
86 preimage: None,
87 cltv: None,
88 deschashonly: None,
89 exposeprivatechannels: None,
90 })
91 .await?
92 .bolt11)
93 }
94}
95
96fn get_invite_code(invite_code: Option<String>) -> anyhow::Result<String> {
97 if let Some(s) = invite_code {
98 Ok(s)
99 } else {
100 let data_dir = std::env::var(FM_CLIENT_DIR_ENV)?;
101 Ok(std::fs::read_to_string(
102 PathBuf::from(data_dir).join("invite-code"),
103 )?)
104 }
105}
106
107pub async fn run(opts: FaucetOpts) -> anyhow::Result<()> {
108 TracingSetup::default().init()?;
109
110 handle_version_hash_command(fedimint_build_code_version_env!());
111
112 let faucet = Faucet::new(&opts).await?;
113 let router = Router::new()
114 .route(
115 "/connect-string",
116 get(|| async {
117 get_invite_code(opts.invite_code)
118 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))
119 }),
120 )
121 .route(
122 "/pay",
123 post(|State(faucet): State<Faucet>, invoice: String| async move {
124 faucet
125 .pay_invoice(invoice)
126 .await
127 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))
128 }),
129 )
130 .route(
131 "/invoice",
132 post(|State(faucet): State<Faucet>, amt: String| async move {
133 let amt = amt
134 .parse::<u64>()
135 .map_err(|e| (StatusCode::BAD_REQUEST, format!("{e:?}")))?;
136 faucet
137 .generate_invoice(amt)
138 .await
139 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))
140 }),
141 )
142 .route(
143 "/gateway-api",
144 get(move || async move {
145 format!("http://127.0.0.1:{}/{V1_API_ENDPOINT}", opts.gw_lnd_port)
146 }),
147 )
148 .layer(CorsLayer::permissive())
149 .with_state(faucet);
150
151 let listener = TcpListener::bind(&opts.bind_addr).await?;
152 axum::serve(listener, router.into_make_service()).await?;
153 Ok(())
154}