ln_gateway/rpc/
rpc_client.rs

1use bitcoin::address::NetworkUnchecked;
2use bitcoin::{Address, Txid};
3use fedimint_core::util::SafeUrl;
4use fedimint_lightning::{
5    ChannelInfo, CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, OpenChannelRequest,
6    SendOnchainRequest,
7};
8use lightning_invoice::Bolt11Invoice;
9use reqwest::{Method, StatusCode};
10use serde::de::DeserializeOwned;
11use serde::Serialize;
12use thiserror::Error;
13
14use super::{
15    BackupPayload, ConfigPayload, ConnectFedPayload, CreateInvoiceForOperatorPayload,
16    DepositAddressPayload, DepositAddressRecheckPayload, FederationInfo, GatewayBalances,
17    GatewayFedConfig, GatewayInfo, LeaveFedPayload, MnemonicResponse, PayInvoiceForOperatorPayload,
18    PaymentLogPayload, PaymentLogResponse, ReceiveEcashPayload, ReceiveEcashResponse,
19    SetFeesPayload, SpendEcashPayload, SpendEcashResponse, WithdrawPayload, WithdrawResponse,
20    ADDRESS_ENDPOINT, ADDRESS_RECHECK_ENDPOINT, BACKUP_ENDPOINT, CLOSE_CHANNELS_WITH_PEER_ENDPOINT,
21    CONFIGURATION_ENDPOINT, CONNECT_FED_ENDPOINT, CREATE_BOLT11_INVOICE_FOR_OPERATOR_ENDPOINT,
22    GATEWAY_INFO_ENDPOINT, GATEWAY_INFO_POST_ENDPOINT, GET_BALANCES_ENDPOINT,
23    GET_LN_ONCHAIN_ADDRESS_ENDPOINT, LEAVE_FED_ENDPOINT, LIST_ACTIVE_CHANNELS_ENDPOINT,
24    MNEMONIC_ENDPOINT, OPEN_CHANNEL_ENDPOINT, PAYMENT_LOG_ENDPOINT,
25    PAY_INVOICE_FOR_OPERATOR_ENDPOINT, RECEIVE_ECASH_ENDPOINT, SEND_ONCHAIN_ENDPOINT,
26    SET_FEES_ENDPOINT, SPEND_ECASH_ENDPOINT, STOP_ENDPOINT, WITHDRAW_ENDPOINT,
27};
28
29pub struct GatewayRpcClient {
30    /// Base URL to gateway web server
31    /// This should include an applicable API version, e.g. http://localhost:8080/v1
32    base_url: SafeUrl,
33    /// A request client
34    client: reqwest::Client,
35    /// Optional gateway password
36    password: Option<String>,
37}
38
39impl GatewayRpcClient {
40    pub fn new(versioned_api: SafeUrl, password: Option<String>) -> Self {
41        Self {
42            base_url: versioned_api,
43            client: reqwest::Client::new(),
44            password,
45        }
46    }
47
48    pub fn with_password(&self, password: Option<String>) -> Self {
49        GatewayRpcClient::new(self.base_url.clone(), password)
50    }
51
52    pub async fn get_info(&self) -> GatewayRpcResult<GatewayInfo> {
53        let url = self
54            .base_url
55            .join(GATEWAY_INFO_ENDPOINT)
56            .expect("invalid base url");
57        self.call_get(url).await
58    }
59
60    // FIXME: deprecated >= 0.3.0
61    pub async fn get_info_legacy(&self) -> GatewayRpcResult<GatewayInfo> {
62        let url = self
63            .base_url
64            .join(GATEWAY_INFO_POST_ENDPOINT)
65            .expect("invalid base url");
66        self.call_post(url, ()).await
67    }
68
69    pub async fn get_config(&self, payload: ConfigPayload) -> GatewayRpcResult<GatewayFedConfig> {
70        let url = self
71            .base_url
72            .join(CONFIGURATION_ENDPOINT)
73            .expect("invalid base url");
74        self.call_post(url, payload).await
75    }
76
77    pub async fn get_deposit_address(
78        &self,
79        payload: DepositAddressPayload,
80    ) -> GatewayRpcResult<Address<NetworkUnchecked>> {
81        let url = self
82            .base_url
83            .join(ADDRESS_ENDPOINT)
84            .expect("invalid base url");
85        self.call_post(url, payload).await
86    }
87
88    pub async fn withdraw(&self, payload: WithdrawPayload) -> GatewayRpcResult<WithdrawResponse> {
89        let url = self
90            .base_url
91            .join(WITHDRAW_ENDPOINT)
92            .expect("invalid base url");
93        self.call_post(url, payload).await
94    }
95
96    pub async fn connect_federation(
97        &self,
98        payload: ConnectFedPayload,
99    ) -> GatewayRpcResult<FederationInfo> {
100        let url = self
101            .base_url
102            .join(CONNECT_FED_ENDPOINT)
103            .expect("invalid base url");
104        self.call_post(url, payload).await
105    }
106
107    pub async fn leave_federation(
108        &self,
109        payload: LeaveFedPayload,
110    ) -> GatewayRpcResult<FederationInfo> {
111        let url = self
112            .base_url
113            .join(LEAVE_FED_ENDPOINT)
114            .expect("invalid base url");
115        self.call_post(url, payload).await
116    }
117
118    pub async fn backup(&self, payload: BackupPayload) -> GatewayRpcResult<()> {
119        let url = self
120            .base_url
121            .join(BACKUP_ENDPOINT)
122            .expect("invalid base url");
123        self.call_post(url, payload).await
124    }
125
126    pub async fn set_fees(&self, payload: SetFeesPayload) -> GatewayRpcResult<()> {
127        let url = self
128            .base_url
129            .join(SET_FEES_ENDPOINT)
130            .expect("invalid base url");
131        self.call_post(url, payload).await
132    }
133
134    pub async fn create_invoice_for_self(
135        &self,
136        payload: CreateInvoiceForOperatorPayload,
137    ) -> GatewayRpcResult<Bolt11Invoice> {
138        let url = self
139            .base_url
140            .join(CREATE_BOLT11_INVOICE_FOR_OPERATOR_ENDPOINT)
141            .expect("invalid base url");
142        self.call_post(url, payload).await
143    }
144
145    pub async fn pay_invoice(
146        &self,
147        payload: PayInvoiceForOperatorPayload,
148    ) -> GatewayRpcResult<String> {
149        let url = self
150            .base_url
151            .join(PAY_INVOICE_FOR_OPERATOR_ENDPOINT)
152            .expect("invalid base url");
153        self.call_post(url, payload).await
154    }
155
156    pub async fn get_ln_onchain_address(&self) -> GatewayRpcResult<Address<NetworkUnchecked>> {
157        let url = self
158            .base_url
159            .join(GET_LN_ONCHAIN_ADDRESS_ENDPOINT)
160            .expect("invalid base url");
161        self.call_get(url).await
162    }
163
164    pub async fn open_channel(&self, payload: OpenChannelRequest) -> GatewayRpcResult<Txid> {
165        let url = self
166            .base_url
167            .join(OPEN_CHANNEL_ENDPOINT)
168            .expect("invalid base url");
169        self.call_post(url, payload).await
170    }
171
172    pub async fn close_channels_with_peer(
173        &self,
174        payload: CloseChannelsWithPeerRequest,
175    ) -> GatewayRpcResult<CloseChannelsWithPeerResponse> {
176        let url = self
177            .base_url
178            .join(CLOSE_CHANNELS_WITH_PEER_ENDPOINT)
179            .expect("invalid base url");
180        self.call_post(url, payload).await
181    }
182
183    pub async fn list_active_channels(&self) -> GatewayRpcResult<Vec<ChannelInfo>> {
184        let url = self
185            .base_url
186            .join(LIST_ACTIVE_CHANNELS_ENDPOINT)
187            .expect("invalid base url");
188        self.call_get(url).await
189    }
190
191    pub async fn send_onchain(&self, payload: SendOnchainRequest) -> GatewayRpcResult<Txid> {
192        let url = self
193            .base_url
194            .join(SEND_ONCHAIN_ENDPOINT)
195            .expect("invalid base url");
196        self.call_post(url, payload).await
197    }
198
199    pub async fn recheck_address(
200        &self,
201        payload: DepositAddressRecheckPayload,
202    ) -> GatewayRpcResult<serde_json::Value> {
203        let url = self
204            .base_url
205            .join(ADDRESS_RECHECK_ENDPOINT)
206            .expect("invalid base url");
207        self.call_post(url, payload).await
208    }
209
210    pub async fn spend_ecash(
211        &self,
212        payload: SpendEcashPayload,
213    ) -> GatewayRpcResult<SpendEcashResponse> {
214        let url = self
215            .base_url
216            .join(SPEND_ECASH_ENDPOINT)
217            .expect("invalid base url");
218        self.call_post(url, payload).await
219    }
220
221    pub async fn receive_ecash(
222        &self,
223        payload: ReceiveEcashPayload,
224    ) -> GatewayRpcResult<ReceiveEcashResponse> {
225        let url = self
226            .base_url
227            .join(RECEIVE_ECASH_ENDPOINT)
228            .expect("invalid base url");
229        self.call_post(url, payload).await
230    }
231
232    pub async fn get_balances(&self) -> GatewayRpcResult<GatewayBalances> {
233        let url = self
234            .base_url
235            .join(GET_BALANCES_ENDPOINT)
236            .expect("invalid base url");
237        self.call_get(url).await
238    }
239
240    pub async fn get_mnemonic(&self) -> GatewayRpcResult<MnemonicResponse> {
241        let url = self
242            .base_url
243            .join(MNEMONIC_ENDPOINT)
244            .expect("invalid base url");
245        self.call_get(url).await
246    }
247
248    pub async fn stop(&self) -> GatewayRpcResult<()> {
249        let url = self.base_url.join(STOP_ENDPOINT).expect("invalid base url");
250        self.call_get(url).await
251    }
252
253    pub async fn payment_log(
254        &self,
255        payload: PaymentLogPayload,
256    ) -> GatewayRpcResult<PaymentLogResponse> {
257        let url = self
258            .base_url
259            .join(PAYMENT_LOG_ENDPOINT)
260            .expect("Invalid base url");
261        self.call_post(url, payload).await
262    }
263
264    async fn call<P: Serialize, T: DeserializeOwned>(
265        &self,
266        method: Method,
267        url: SafeUrl,
268        payload: Option<P>,
269    ) -> Result<T, GatewayRpcError> {
270        let mut builder = self.client.request(method, url.clone().to_unsafe());
271        if let Some(password) = self.password.clone() {
272            builder = builder.bearer_auth(password);
273        }
274        if let Some(payload) = payload {
275            builder = builder
276                .json(&payload)
277                .header(reqwest::header::CONTENT_TYPE, "application/json");
278        }
279
280        let response = builder.send().await?;
281
282        match response.status() {
283            StatusCode::OK => Ok(response.json::<T>().await?),
284            status => Err(GatewayRpcError::BadStatus(status)),
285        }
286    }
287
288    async fn call_get<T: DeserializeOwned>(&self, url: SafeUrl) -> Result<T, GatewayRpcError> {
289        self.call(Method::GET, url, None::<()>).await
290    }
291
292    async fn call_post<P: Serialize, T: DeserializeOwned>(
293        &self,
294        url: SafeUrl,
295        payload: P,
296    ) -> Result<T, GatewayRpcError> {
297        self.call(Method::POST, url, Some(payload)).await
298    }
299}
300
301pub type GatewayRpcResult<T> = Result<T, GatewayRpcError>;
302
303#[derive(Error, Debug)]
304pub enum GatewayRpcError {
305    #[error("Bad status returned {0}")]
306    BadStatus(StatusCode),
307    #[error(transparent)]
308    RequestError(#[from] reqwest::Error),
309}