fedimint_wallet_client/
api.rs

1use anyhow::anyhow;
2use bitcoin::{Address, Amount};
3use fedimint_api_client::api::{
4    FederationApiExt, FederationError, FederationResult, IModuleFederationApi, PeerResult,
5};
6use fedimint_api_client::query::FilterMapThreshold;
7use fedimint_core::envs::BitcoinRpcConfig;
8use fedimint_core::module::{ApiAuth, ApiRequestErased, ModuleConsensusVersion};
9use fedimint_core::task::{MaybeSend, MaybeSync};
10use fedimint_core::{NumPeersExt, PeerId, apply, async_trait_maybe_send};
11use fedimint_wallet_common::endpoint_constants::{
12    ACTIVATE_CONSENSUS_VERSION_VOTING_ENDPOINT, BITCOIN_KIND_ENDPOINT, BITCOIN_RPC_CONFIG_ENDPOINT,
13    BLOCK_COUNT_ENDPOINT, BLOCK_COUNT_LOCAL_ENDPOINT, MODULE_CONSENSUS_VERSION_ENDPOINT,
14    PEG_OUT_FEES_ENDPOINT, UTXO_CONFIRMED_ENDPOINT, WALLET_SUMMARY_ENDPOINT,
15};
16use fedimint_wallet_common::{PegOutFees, WalletSummary};
17
18#[apply(async_trait_maybe_send!)]
19pub trait WalletFederationApi {
20    async fn module_consensus_version(&self) -> FederationResult<ModuleConsensusVersion>;
21
22    async fn fetch_consensus_block_count(&self) -> FederationResult<u64>;
23
24    async fn fetch_peg_out_fees(
25        &self,
26        address: &Address,
27        amount: Amount,
28    ) -> FederationResult<Option<PegOutFees>>;
29
30    async fn fetch_bitcoin_rpc_kind(&self, peer_id: PeerId) -> FederationResult<String>;
31
32    async fn fetch_bitcoin_rpc_config(&self, auth: ApiAuth) -> FederationResult<BitcoinRpcConfig>;
33
34    async fn fetch_wallet_summary(&self) -> FederationResult<WalletSummary>;
35
36    async fn fetch_block_count_local(&self) -> FederationResult<u32>;
37
38    async fn is_utxo_confirmed(&self, outpoint: bitcoin::OutPoint) -> FederationResult<bool>;
39
40    async fn activate_consensus_version_voting(&self, auth: ApiAuth) -> FederationResult<()>;
41}
42
43#[apply(async_trait_maybe_send!)]
44impl<T: ?Sized> WalletFederationApi for T
45where
46    T: IModuleFederationApi + MaybeSend + MaybeSync + 'static,
47{
48    async fn module_consensus_version(&self) -> FederationResult<ModuleConsensusVersion> {
49        let response = self
50            .request_current_consensus(
51                MODULE_CONSENSUS_VERSION_ENDPOINT.to_string(),
52                ApiRequestErased::default(),
53            )
54            .await;
55
56        if let Err(e) = &response
57            && e.any_peer_error_method_not_found()
58        {
59            return Ok(ModuleConsensusVersion::new(2, 0));
60        }
61
62        response
63    }
64
65    async fn is_utxo_confirmed(&self, outpoint: bitcoin::OutPoint) -> FederationResult<bool> {
66        let res = self
67            .request_current_consensus(
68                UTXO_CONFIRMED_ENDPOINT.to_string(),
69                ApiRequestErased::new(outpoint),
70            )
71            .await;
72
73        if let Err(e) = &res
74            && e.any_peer_error_method_not_found()
75        {
76            return Ok(false);
77        }
78
79        res
80    }
81
82    async fn fetch_consensus_block_count(&self) -> FederationResult<u64> {
83        self.request_current_consensus(
84            BLOCK_COUNT_ENDPOINT.to_string(),
85            ApiRequestErased::default(),
86        )
87        .await
88    }
89
90    async fn fetch_block_count_local(&self) -> FederationResult<u32> {
91        let filter_map = |_peer: PeerId, block_count: Option<u32>| -> PeerResult<Option<u32>> {
92            Ok(block_count)
93        };
94
95        let block_count_responses = self
96            .request_with_strategy(
97                FilterMapThreshold::<Option<u32>, Option<u32>>::new(
98                    filter_map,
99                    self.all_peers().to_num_peers().threshold().into(),
100                ),
101                BLOCK_COUNT_LOCAL_ENDPOINT.to_string(),
102                ApiRequestErased::default(),
103            )
104            .await?;
105
106        let mut response: Vec<u32> = block_count_responses.into_values().flatten().collect();
107
108        if response.is_empty() {
109            return Err(FederationError::general(
110                BLOCK_COUNT_LOCAL_ENDPOINT.to_string(),
111                ApiRequestErased::default(),
112                anyhow!("No valid block counts received"),
113            ));
114        }
115
116        response.sort_unstable();
117        let final_block_count = response[response.len() / 2];
118
119        Ok(final_block_count)
120    }
121
122    async fn fetch_peg_out_fees(
123        &self,
124        address: &Address,
125        amount: Amount,
126    ) -> FederationResult<Option<PegOutFees>> {
127        self.request_current_consensus(
128            PEG_OUT_FEES_ENDPOINT.to_string(),
129            ApiRequestErased::new((address, amount.to_sat())),
130        )
131        .await
132    }
133
134    async fn fetch_bitcoin_rpc_kind(&self, peer_id: PeerId) -> FederationResult<String> {
135        self.request_single_peer_federation(
136            BITCOIN_KIND_ENDPOINT.to_string(),
137            ApiRequestErased::default(),
138            peer_id,
139        )
140        .await
141    }
142
143    async fn fetch_bitcoin_rpc_config(&self, auth: ApiAuth) -> FederationResult<BitcoinRpcConfig> {
144        self.request_admin(
145            BITCOIN_RPC_CONFIG_ENDPOINT,
146            ApiRequestErased::default(),
147            auth,
148        )
149        .await
150    }
151
152    async fn fetch_wallet_summary(&self) -> FederationResult<WalletSummary> {
153        self.request_current_consensus(
154            WALLET_SUMMARY_ENDPOINT.to_string(),
155            ApiRequestErased::default(),
156        )
157        .await
158    }
159
160    async fn activate_consensus_version_voting(&self, auth: ApiAuth) -> FederationResult<()> {
161        self.request_admin(
162            ACTIVATE_CONSENSUS_VERSION_VOTING_ENDPOINT,
163            ApiRequestErased::default(),
164            auth,
165        )
166        .await
167    }
168}