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