fedimint_gateway_client/
lib.rs

1use bitcoin::address::NetworkUnchecked;
2use bitcoin::{Address, Txid};
3use fedimint_core::util::SafeUrl;
4use fedimint_gateway_common::{
5    ADDRESS_ENDPOINT, ADDRESS_RECHECK_ENDPOINT, BACKUP_ENDPOINT, BackupPayload,
6    CLOSE_CHANNELS_WITH_PEER_ENDPOINT, CONFIGURATION_ENDPOINT, CONNECT_FED_ENDPOINT,
7    CREATE_BOLT11_INVOICE_FOR_OPERATOR_ENDPOINT, CREATE_BOLT12_OFFER_FOR_OPERATOR_ENDPOINT,
8    ChannelInfo, CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, ConfigPayload,
9    ConnectFedPayload, CreateInvoiceForOperatorPayload, CreateOfferPayload, CreateOfferResponse,
10    DepositAddressPayload, DepositAddressRecheckPayload, FederationInfo, GATEWAY_INFO_ENDPOINT,
11    GATEWAY_INFO_POST_ENDPOINT, GET_BALANCES_ENDPOINT, GET_INVOICE_ENDPOINT,
12    GET_LN_ONCHAIN_ADDRESS_ENDPOINT, GatewayBalances, GatewayFedConfig, GatewayInfo,
13    GetInvoiceRequest, GetInvoiceResponse, LEAVE_FED_ENDPOINT, LIST_ACTIVE_CHANNELS_ENDPOINT,
14    LIST_TRANSACTIONS_ENDPOINT, LeaveFedPayload, ListTransactionsPayload, ListTransactionsResponse,
15    MNEMONIC_ENDPOINT, MnemonicResponse, OPEN_CHANNEL_ENDPOINT, OpenChannelRequest,
16    PAY_INVOICE_FOR_OPERATOR_ENDPOINT, PAY_OFFER_FOR_OPERATOR_ENDPOINT, PAYMENT_LOG_ENDPOINT,
17    PAYMENT_SUMMARY_ENDPOINT, PayInvoiceForOperatorPayload, PayOfferPayload, PayOfferResponse,
18    PaymentLogPayload, PaymentLogResponse, PaymentSummaryPayload, PaymentSummaryResponse,
19    RECEIVE_ECASH_ENDPOINT, ReceiveEcashPayload, ReceiveEcashResponse, SEND_ONCHAIN_ENDPOINT,
20    SET_FEES_ENDPOINT, SPEND_ECASH_ENDPOINT, STOP_ENDPOINT, SendOnchainRequest, SetFeesPayload,
21    SpendEcashPayload, SpendEcashResponse, WITHDRAW_ENDPOINT, WithdrawPayload, WithdrawResponse,
22};
23use lightning_invoice::Bolt11Invoice;
24use reqwest::{Method, StatusCode};
25use serde::Serialize;
26use serde::de::DeserializeOwned;
27use thiserror::Error;
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    pub async fn payment_summary(
265        &self,
266        payload: PaymentSummaryPayload,
267    ) -> GatewayRpcResult<PaymentSummaryResponse> {
268        let url = self
269            .base_url
270            .join(PAYMENT_SUMMARY_ENDPOINT)
271            .expect("invalid base url");
272        self.call_post(url, payload).await
273    }
274
275    pub async fn get_invoice(
276        &self,
277        payload: GetInvoiceRequest,
278    ) -> GatewayRpcResult<Option<GetInvoiceResponse>> {
279        let url = self
280            .base_url
281            .join(GET_INVOICE_ENDPOINT)
282            .expect("invalid base url");
283        self.call_post(url, payload).await
284    }
285
286    pub async fn list_transactions(
287        &self,
288        payload: ListTransactionsPayload,
289    ) -> GatewayRpcResult<ListTransactionsResponse> {
290        let url = self
291            .base_url
292            .join(LIST_TRANSACTIONS_ENDPOINT)
293            .expect("invalid base url");
294        self.call_post(url, payload).await
295    }
296
297    pub async fn create_offer(
298        &self,
299        payload: CreateOfferPayload,
300    ) -> GatewayRpcResult<CreateOfferResponse> {
301        let url = self
302            .base_url
303            .join(CREATE_BOLT12_OFFER_FOR_OPERATOR_ENDPOINT)
304            .expect("invalid base url");
305        self.call_post(url, payload).await
306    }
307
308    pub async fn pay_offer(&self, payload: PayOfferPayload) -> GatewayRpcResult<PayOfferResponse> {
309        let url = self
310            .base_url
311            .join(PAY_OFFER_FOR_OPERATOR_ENDPOINT)
312            .expect("invalid base url");
313        self.call_post(url, payload).await
314    }
315
316    async fn call<P: Serialize, T: DeserializeOwned>(
317        &self,
318        method: Method,
319        url: SafeUrl,
320        payload: Option<P>,
321    ) -> Result<T, GatewayRpcError> {
322        let mut builder = self.client.request(method, url.clone().to_unsafe());
323        if let Some(password) = self.password.clone() {
324            builder = builder.bearer_auth(password);
325        }
326        if let Some(payload) = payload {
327            builder = builder
328                .json(&payload)
329                .header(reqwest::header::CONTENT_TYPE, "application/json");
330        }
331
332        let response = builder.send().await?;
333
334        match response.status() {
335            StatusCode::OK => Ok(response.json::<T>().await?),
336            status => Err(GatewayRpcError::BadStatus(status)),
337        }
338    }
339
340    async fn call_get<T: DeserializeOwned>(&self, url: SafeUrl) -> Result<T, GatewayRpcError> {
341        self.call(Method::GET, url, None::<()>).await
342    }
343
344    async fn call_post<P: Serialize, T: DeserializeOwned>(
345        &self,
346        url: SafeUrl,
347        payload: P,
348    ) -> Result<T, GatewayRpcError> {
349        self.call(Method::POST, url, Some(payload)).await
350    }
351}
352
353pub type GatewayRpcResult<T> = Result<T, GatewayRpcError>;
354
355#[derive(Error, Debug)]
356pub enum GatewayRpcError {
357    #[error("Bad status returned {0}")]
358    BadStatus(StatusCode),
359    #[error(transparent)]
360    RequestError(#[from] reqwest::Error),
361}