fedimint_walletv2_client/
api.rs1use std::collections::BTreeMap;
2
3use fedimint_api_client::api::{
4 FederationApiExt, FederationError, FederationGeneralError, FederationResult,
5 IModuleFederationApi,
6};
7use fedimint_api_client::query::ThresholdAgreement;
8use fedimint_core::module::ApiRequestErased;
9use fedimint_core::task::{MaybeSend, MaybeSync};
10use fedimint_core::{NumPeersExt, OutPoint, PeerId, apply, async_trait_maybe_send};
11use fedimint_walletv2_common::endpoint_constants::{
12 CONSENSUS_BLOCK_COUNT_ENDPOINT, CONSENSUS_FEERATE_ENDPOINT, FEDERATION_WALLET_ENDPOINT,
13 OUTPUT_INFO_SLICE_ENDPOINT, PENDING_TRANSACTION_CHAIN_ENDPOINT, RECEIVE_FEE_ENDPOINT,
14 SEND_FEE_ENDPOINT, TRANSACTION_CHAIN_ENDPOINT, TRANSACTION_ID_ENDPOINT,
15};
16use fedimint_walletv2_common::{FederationWallet, OutputInfo, TxInfo};
17
18fn describe_fee_quotes(quotes: &BTreeMap<PeerId, Option<bitcoin::Amount>>) -> String {
20 quotes
21 .iter()
22 .map(|(peer, fee)| match fee {
23 Some(fee) => format!("peer {peer}: {} sats", fee.to_sat()),
24 None => format!("peer {peer}: no quote"),
25 })
26 .collect::<Vec<_>>()
27 .join("; ")
28}
29
30#[apply(async_trait_maybe_send!)]
31pub trait WalletFederationApi {
32 async fn consensus_block_count(&self) -> FederationResult<u64>;
33
34 async fn consensus_feerate(&self) -> FederationResult<Option<u64>>;
35
36 async fn federation_wallet(&self) -> FederationResult<Option<FederationWallet>>;
37
38 async fn send_fee(&self) -> FederationResult<Option<bitcoin::Amount>>;
39
40 async fn receive_fee(&self) -> FederationResult<Option<bitcoin::Amount>>;
41
42 async fn pending_tx_chain(&self) -> FederationResult<Vec<TxInfo>>;
43
44 async fn tx_chain(&self) -> FederationResult<Vec<TxInfo>>;
45
46 async fn output_info_slice(
47 &self,
48 start_index: u64,
49 end_index: u64,
50 ) -> FederationResult<Vec<OutputInfo>>;
51
52 async fn tx_id(&self, outpoint: OutPoint) -> Option<bitcoin::Txid>;
53}
54
55#[apply(async_trait_maybe_send!)]
56impl<T: ?Sized> WalletFederationApi for T
57where
58 T: IModuleFederationApi + MaybeSend + MaybeSync + 'static,
59{
60 async fn consensus_block_count(&self) -> FederationResult<u64> {
61 self.request_current_consensus(
62 CONSENSUS_BLOCK_COUNT_ENDPOINT.to_string(),
63 ApiRequestErased::new(()),
64 )
65 .await
66 }
67
68 async fn consensus_feerate(&self) -> FederationResult<Option<u64>> {
69 self.request_current_consensus(
70 CONSENSUS_FEERATE_ENDPOINT.to_string(),
71 ApiRequestErased::new(()),
72 )
73 .await
74 }
75
76 async fn federation_wallet(&self) -> FederationResult<Option<FederationWallet>> {
77 self.request_current_consensus(
78 FEDERATION_WALLET_ENDPOINT.to_string(),
79 ApiRequestErased::new(()),
80 )
81 .await
82 }
83
84 async fn send_fee(&self) -> FederationResult<Option<bitcoin::Amount>> {
85 let quotes = match self
92 .request_with_strategy(
93 ThresholdAgreement::new(self.all_peers().to_num_peers()),
94 SEND_FEE_ENDPOINT.to_string(),
95 ApiRequestErased::new(()),
96 )
97 .await?
98 {
99 Ok(fee) => return Ok(fee),
100 Err(quotes) => quotes,
101 };
102
103 Err(FederationError::general(
104 SEND_FEE_ENDPOINT.to_string(),
105 ApiRequestErased::new(()),
106 FederationGeneralError::ThresholdFailed {
107 message: format!(
108 "Guardians disagree on the onchain send fee ({}). The fee is \
109 consensus state, so a guardian returning a different value has \
110 diverged - because its Bitcoin backend is lagging, or because its \
111 view of the pending transaction stack differs. The same fee \
112 validates the send, so it cannot be accepted until they agree.",
113 describe_fee_quotes("es)
114 ),
115 },
116 ))
117 }
118
119 async fn receive_fee(&self) -> FederationResult<Option<bitcoin::Amount>> {
120 let quotes = match self
126 .request_with_strategy(
127 ThresholdAgreement::new(self.all_peers().to_num_peers()),
128 RECEIVE_FEE_ENDPOINT.to_string(),
129 ApiRequestErased::new(()),
130 )
131 .await?
132 {
133 Ok(fee) => return Ok(fee),
134 Err(quotes) => quotes,
135 };
136
137 Err(FederationError::general(
138 RECEIVE_FEE_ENDPOINT.to_string(),
139 ApiRequestErased::new(()),
140 FederationGeneralError::ThresholdFailed {
141 message: format!(
142 "Guardians disagree on the onchain receive fee ({}). The fee is \
143 consensus state, so a guardian returning a different value has \
144 diverged - because its Bitcoin backend is lagging, or because its \
145 view of the pending transaction stack differs. The same fee \
146 validates the claim, so it cannot be accepted until they agree.",
147 describe_fee_quotes("es)
148 ),
149 },
150 ))
151 }
152
153 async fn pending_tx_chain(&self) -> FederationResult<Vec<TxInfo>> {
154 self.request_current_consensus(
155 PENDING_TRANSACTION_CHAIN_ENDPOINT.to_string(),
156 ApiRequestErased::new(()),
157 )
158 .await
159 }
160
161 async fn tx_chain(&self) -> FederationResult<Vec<TxInfo>> {
162 self.request_current_consensus(
163 TRANSACTION_CHAIN_ENDPOINT.to_string(),
164 ApiRequestErased::new(()),
165 )
166 .await
167 }
168
169 async fn output_info_slice(
170 &self,
171 start_index: u64,
172 end_index: u64,
173 ) -> FederationResult<Vec<OutputInfo>> {
174 self.request_current_consensus(
175 OUTPUT_INFO_SLICE_ENDPOINT.to_string(),
176 ApiRequestErased::new((start_index, end_index)),
177 )
178 .await
179 }
180
181 async fn tx_id(&self, outpoint: OutPoint) -> Option<bitcoin::Txid> {
182 self.request_current_consensus_retry(
183 TRANSACTION_ID_ENDPOINT.to_string(),
184 ApiRequestErased::new(outpoint),
185 )
186 .await
187 }
188}