1use bitcoin::{Address, Amount};
2use fedimint_api_client::api::{
3 FederationApiExt, FederationError, FederationGeneralError, FederationResult,
4 IModuleFederationApi, ServerResult,
5};
6use fedimint_api_client::query::{FilterMapThreshold, ThresholdAgreement};
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, RECOVERY_COUNT_ENDPOINT, RECOVERY_SLICE_ENDPOINT,
15 UTXO_CONFIRMED_ENDPOINT, WALLET_SUMMARY_ENDPOINT,
16};
17use fedimint_wallet_common::{PegOutFees, RecoveryItem, WalletSummary};
18
19#[apply(async_trait_maybe_send!)]
20pub trait WalletFederationApi {
21 async fn module_consensus_version(&self) -> FederationResult<ModuleConsensusVersion>;
22
23 async fn fetch_consensus_block_count(&self) -> FederationResult<u64>;
24
25 async fn fetch_peg_out_fees(
26 &self,
27 address: &Address,
28 amount: Amount,
29 ) -> FederationResult<Option<PegOutFees>>;
30
31 async fn fetch_bitcoin_rpc_kind(&self, peer_id: PeerId) -> FederationResult<String>;
32
33 async fn fetch_bitcoin_rpc_config(&self, auth: ApiAuth) -> FederationResult<BitcoinRpcConfig>;
34
35 async fn fetch_wallet_summary(&self) -> FederationResult<WalletSummary>;
36
37 async fn fetch_block_count_local(&self) -> FederationResult<u32>;
38
39 async fn is_utxo_confirmed(&self, outpoint: bitcoin::OutPoint) -> FederationResult<bool>;
40
41 async fn activate_consensus_version_voting(&self, auth: ApiAuth) -> FederationResult<()>;
42
43 async fn fetch_recovery_count(&self) -> FederationResult<u64>;
45
46 async fn fetch_recovery_slice(&self, start: u64, end: u64) -> Vec<RecoveryItem>;
52}
53
54#[apply(async_trait_maybe_send!)]
55impl<T: ?Sized> WalletFederationApi for T
56where
57 T: IModuleFederationApi + MaybeSend + MaybeSync + 'static,
58{
59 async fn module_consensus_version(&self) -> FederationResult<ModuleConsensusVersion> {
60 let response = self
61 .request_current_consensus(
62 MODULE_CONSENSUS_VERSION_ENDPOINT.to_string(),
63 ApiRequestErased::default(),
64 )
65 .await;
66
67 if let Err(e) = &response
68 && e.any_peer_error_method_not_found()
69 {
70 return Ok(ModuleConsensusVersion::new(2, 0));
71 }
72
73 response
74 }
75
76 async fn is_utxo_confirmed(&self, outpoint: bitcoin::OutPoint) -> FederationResult<bool> {
77 let res = self
78 .request_current_consensus(
79 UTXO_CONFIRMED_ENDPOINT.to_string(),
80 ApiRequestErased::new(outpoint),
81 )
82 .await;
83
84 if let Err(e) = &res
85 && e.any_peer_error_method_not_found()
86 {
87 return Ok(false);
88 }
89
90 res
91 }
92
93 async fn fetch_consensus_block_count(&self) -> FederationResult<u64> {
94 self.request_current_consensus(
95 BLOCK_COUNT_ENDPOINT.to_string(),
96 ApiRequestErased::default(),
97 )
98 .await
99 }
100
101 async fn fetch_block_count_local(&self) -> FederationResult<u32> {
102 let filter_map = |_peer: PeerId, block_count: Option<u32>| -> ServerResult<Option<u32>> {
103 Ok(block_count)
104 };
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}