fedimint_walletv2_client/
api.rs1use std::collections::BTreeMap;
2
3use anyhow::anyhow;
4use fedimint_api_client::api::{
5 FederationApiExt, FederationError, FederationResult, 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 anyhow!(
107 "Guardians disagree on the onchain send fee ({}). The fee is \
108 consensus state, so a guardian returning a different value has \
109 diverged - because its Bitcoin backend is lagging, or because its \
110 view of the pending transaction stack differs. The same fee \
111 validates the send, so it cannot be accepted until they agree.",
112 describe_fee_quotes("es)
113 ),
114 ))
115 }
116
117 async fn receive_fee(&self) -> FederationResult<Option<bitcoin::Amount>> {
118 let quotes = match self
124 .request_with_strategy(
125 ThresholdAgreement::new(self.all_peers().to_num_peers()),
126 RECEIVE_FEE_ENDPOINT.to_string(),
127 ApiRequestErased::new(()),
128 )
129 .await?
130 {
131 Ok(fee) => return Ok(fee),
132 Err(quotes) => quotes,
133 };
134
135 Err(FederationError::general(
136 RECEIVE_FEE_ENDPOINT.to_string(),
137 ApiRequestErased::new(()),
138 anyhow!(
139 "Guardians disagree on the onchain receive fee ({}). The fee is \
140 consensus state, so a guardian returning a different value has \
141 diverged - because its Bitcoin backend is lagging, or because its \
142 view of the pending transaction stack differs. The same fee \
143 validates the claim, so it cannot be accepted until they agree.",
144 describe_fee_quotes("es)
145 ),
146 ))
147 }
148
149 async fn pending_tx_chain(&self) -> FederationResult<Vec<TxInfo>> {
150 self.request_current_consensus(
151 PENDING_TRANSACTION_CHAIN_ENDPOINT.to_string(),
152 ApiRequestErased::new(()),
153 )
154 .await
155 }
156
157 async fn tx_chain(&self) -> FederationResult<Vec<TxInfo>> {
158 self.request_current_consensus(
159 TRANSACTION_CHAIN_ENDPOINT.to_string(),
160 ApiRequestErased::new(()),
161 )
162 .await
163 }
164
165 async fn output_info_slice(
166 &self,
167 start_index: u64,
168 end_index: u64,
169 ) -> FederationResult<Vec<OutputInfo>> {
170 self.request_current_consensus(
171 OUTPUT_INFO_SLICE_ENDPOINT.to_string(),
172 ApiRequestErased::new((start_index, end_index)),
173 )
174 .await
175 }
176
177 async fn tx_id(&self, outpoint: OutPoint) -> Option<bitcoin::Txid> {
178 self.request_current_consensus_retry(
179 TRANSACTION_ID_ENDPOINT.to_string(),
180 ApiRequestErased::new(outpoint),
181 )
182 .await
183 }
184}