1use std::future::ready;
2
3use bitcoin::{Address, Amount};
4use fedimint_api_client::api::{
5 FederationApiExt, FederationError, FederationGeneralError, FederationResult,
6 IModuleFederationApi,
7};
8use fedimint_api_client::query::{FilterMapThreshold, ThresholdAgreement};
9use fedimint_core::envs::BitcoinRpcConfig;
10use fedimint_core::module::{ApiAuth, ApiRequestErased, ModuleConsensusVersion};
11use fedimint_core::task::{MaybeSend, MaybeSync};
12use fedimint_core::{NumPeersExt, PeerId, apply, async_trait_maybe_send};
13use fedimint_wallet_common::endpoint_constants::{
14 ACTIVATE_CONSENSUS_VERSION_VOTING_ENDPOINT, BITCOIN_KIND_ENDPOINT, BITCOIN_RPC_CONFIG_ENDPOINT,
15 BLOCK_COUNT_ENDPOINT, BLOCK_COUNT_LOCAL_ENDPOINT, MODULE_CONSENSUS_VERSION_ENDPOINT,
16 PEG_OUT_FEES_ENDPOINT, RECOVERY_COUNT_ENDPOINT, RECOVERY_SLICE_ENDPOINT,
17 UTXO_CONFIRMED_ENDPOINT, WALLET_SUMMARY_ENDPOINT,
18};
19use fedimint_wallet_common::{PegOutFees, RecoveryItem, WalletSummary};
20
21#[apply(async_trait_maybe_send!)]
22pub trait WalletFederationApi {
23 async fn module_consensus_version(&self) -> FederationResult<ModuleConsensusVersion>;
24
25 async fn fetch_consensus_block_count(&self) -> FederationResult<u64>;
26
27 async fn fetch_peg_out_fees(
28 &self,
29 address: &Address,
30 amount: Amount,
31 ) -> FederationResult<Option<PegOutFees>>;
32
33 async fn fetch_bitcoin_rpc_kind(&self, peer_id: PeerId) -> FederationResult<String>;
34
35 async fn fetch_bitcoin_rpc_config(&self, auth: ApiAuth) -> FederationResult<BitcoinRpcConfig>;
36
37 async fn fetch_wallet_summary(&self) -> FederationResult<WalletSummary>;
38
39 async fn fetch_block_count_local(&self) -> FederationResult<u32>;
40
41 async fn is_utxo_confirmed(&self, outpoint: bitcoin::OutPoint) -> FederationResult<bool>;
42
43 async fn activate_consensus_version_voting(&self, auth: ApiAuth) -> FederationResult<()>;
44
45 async fn fetch_recovery_count(&self) -> FederationResult<u64>;
47
48 async fn fetch_recovery_slice(&self, start: u64, end: u64) -> Vec<RecoveryItem>;
54}
55
56#[apply(async_trait_maybe_send!)]
57impl<T: ?Sized> WalletFederationApi for T
58where
59 T: IModuleFederationApi + MaybeSend + MaybeSync + 'static,
60{
61 async fn module_consensus_version(&self) -> FederationResult<ModuleConsensusVersion> {
62 let response = self
63 .request_current_consensus(
64 MODULE_CONSENSUS_VERSION_ENDPOINT.to_string(),
65 ApiRequestErased::default(),
66 )
67 .await;
68
69 if let Err(e) = &response
70 && e.any_peer_error_method_not_found()
71 {
72 return Ok(ModuleConsensusVersion::new(2, 0));
73 }
74
75 response
76 }
77
78 async fn is_utxo_confirmed(&self, outpoint: bitcoin::OutPoint) -> FederationResult<bool> {
79 let res = self
80 .request_current_consensus(
81 UTXO_CONFIRMED_ENDPOINT.to_string(),
82 ApiRequestErased::new(outpoint),
83 )
84 .await;
85
86 if let Err(e) = &res
87 && e.any_peer_error_method_not_found()
88 {
89 return Ok(false);
90 }
91
92 res
93 }
94
95 async fn fetch_consensus_block_count(&self) -> FederationResult<u64> {
96 self.request_current_consensus(
97 BLOCK_COUNT_ENDPOINT.to_string(),
98 ApiRequestErased::default(),
99 )
100 .await
101 }
102
103 async fn fetch_block_count_local(&self) -> FederationResult<u32> {
104 let filter_map = |_peer: PeerId, block_count: Option<u32>| ready(Ok(block_count));
105
106 let block_count_responses = self
107 .request_with_strategy(
108 FilterMapThreshold::<Option<u32>, Option<u32>>::new(
109 filter_map,
110 self.all_peers().to_num_peers().threshold().into(),
111 ),
112 BLOCK_COUNT_LOCAL_ENDPOINT.to_string(),
113 ApiRequestErased::default(),
114 )
115 .await?;
116
117 let mut response: Vec<u32> = block_count_responses.into_values().flatten().collect();
118
119 if response.is_empty() {
120 return Err(FederationError::general(
121 BLOCK_COUNT_LOCAL_ENDPOINT.to_string(),
122 ApiRequestErased::default(),
123 FederationGeneralError::ThresholdFailed {
124 message: "No valid block counts received".to_string(),
125 },
126 ));
127 }
128
129 response.sort_unstable();
130 let final_block_count = response[response.len() / 2];
131
132 Ok(final_block_count)
133 }
134
135 async fn fetch_peg_out_fees(
136 &self,
137 address: &Address,
138 amount: Amount,
139 ) -> FederationResult<Option<PegOutFees>> {
140 let params = ApiRequestErased::new((address, amount.to_sat()));
141
142 let quotes = match self
147 .request_with_strategy(
148 ThresholdAgreement::new(self.all_peers().to_num_peers()),
149 PEG_OUT_FEES_ENDPOINT.to_string(),
150 params.clone(),
151 )
152 .await?
153 {
154 Ok(fees) => return Ok(fees),
155 Err(quotes) => quotes,
156 };
157
158 let detail = quotes
159 .iter()
160 .map(|(peer, quote)| match quote {
161 Some(fees) => format!(
162 "peer {peer}: {} sats/kvb, weight {}",
163 fees.fee_rate.sats_per_kvb, fees.total_weight
164 ),
165 None => format!("peer {peer}: no quote"),
166 })
167 .collect::<Vec<_>>()
168 .join("; ");
169
170 Err(FederationError::general(
171 PEG_OUT_FEES_ENDPOINT.to_string(),
172 params,
173 FederationGeneralError::ThresholdFailed {
174 message: format!(
175 "Guardians disagree on peg-out fees ({detail}). The quote is consensus \
176 state, so a guardian returning a different value has diverged - most \
177 often because its Bitcoin backend is lagging. The same rate validates \
178 the peg-out, so it cannot be accepted until the federation agrees."
179 ),
180 },
181 ))
182 }
183
184 async fn fetch_bitcoin_rpc_kind(&self, peer_id: PeerId) -> FederationResult<String> {
185 self.request_single_peer_federation(
186 BITCOIN_KIND_ENDPOINT.to_string(),
187 ApiRequestErased::default(),
188 peer_id,
189 )
190 .await
191 }
192
193 async fn fetch_bitcoin_rpc_config(&self, auth: ApiAuth) -> FederationResult<BitcoinRpcConfig> {
194 self.request_admin(
195 BITCOIN_RPC_CONFIG_ENDPOINT,
196 ApiRequestErased::default(),
197 auth,
198 )
199 .await
200 }
201
202 async fn fetch_wallet_summary(&self) -> FederationResult<WalletSummary> {
203 self.request_current_consensus(
204 WALLET_SUMMARY_ENDPOINT.to_string(),
205 ApiRequestErased::default(),
206 )
207 .await
208 }
209
210 async fn activate_consensus_version_voting(&self, auth: ApiAuth) -> FederationResult<()> {
211 self.request_admin(
212 ACTIVATE_CONSENSUS_VERSION_VOTING_ENDPOINT,
213 ApiRequestErased::default(),
214 auth,
215 )
216 .await
217 }
218
219 async fn fetch_recovery_count(&self) -> FederationResult<u64> {
220 self.request_current_consensus::<u64>(
221 RECOVERY_COUNT_ENDPOINT.to_string(),
222 ApiRequestErased::default(),
223 )
224 .await
225 }
226
227 async fn fetch_recovery_slice(&self, start: u64, end: u64) -> Vec<RecoveryItem> {
228 self.request_current_consensus_retry::<Vec<RecoveryItem>>(
229 RECOVERY_SLICE_ENDPOINT.to_string(),
230 ApiRequestErased::new((start, end)),
231 )
232 .await
233 }
234}