devimint/
faucet.rs

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}