1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
//! CDK BDK onchain backend errors
use std::path::PathBuf;
use cdk_common::CurrencyUnit;
use thiserror::Error;
use uuid::Uuid;
/// CDK BDK onchain backend error
#[derive(Debug, Error)]
pub enum Error {
/// Fee estimation failed
#[error("Fee estimation failed: {0}")]
FeeEstimationFailed(String),
/// Arithmetic overflow during fee estimation
#[error("Fee estimation overflow")]
FeeEstimationOverflow,
/// Fee estimation unavailable
#[error("Fee estimation unavailable")]
FeeEstimationUnavailable,
/// Wallet has no spendable UTXOs available for an onchain quote
#[error("No spendable UTXOs available for onchain payment quote")]
NoSpendableUtxos,
/// Start called but tasks are already running
#[error("Start called but background tasks are already running")]
AlreadyStarted,
/// Invalid backend configuration
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
/// Unsupported payment type for onchain backend
#[error("Unsupported payment type for onchain backend")]
UnsupportedOnchain,
/// Wallet selected a `fee_index` outside the configured BDK fee options.
#[error("unknown fee_index {0}; expected one of the configured BDK fee options")]
UnknownFeeIndex(u32),
/// JSON error
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
/// Amount conversion error
#[error("Amount conversion error: {0}")]
AmountConversion(#[from] cdk_common::amount::Error),
/// A typed amount does not match the payment unit supplied to the backend.
#[error("Amount unit {actual} does not match payment unit {expected}")]
AmountUnitMismatch {
/// Unit supplied to the payment backend.
expected: CurrencyUnit,
/// Unit carried by the typed amount.
actual: CurrencyUnit,
},
/// An onchain payment amount cannot be represented as whole satoshis.
#[error("Onchain payment amount {amount_msat} msat is not a whole number of satoshis")]
FractionalSatoshiAmount {
/// Requested payment amount in millisatoshis.
amount_msat: u64,
},
/// Database error
#[error("Database error: {0}")]
Database(#[from] bdk_wallet::rusqlite::Error),
/// An existing-mint migration did not find the persisted BDK wallet.
#[error("Persisted BDK wallet database is missing at {path}")]
ExistingWalletMissing {
/// Expected BDK wallet database path.
path: PathBuf,
},
/// A BDK SQLite file exists but does not contain an initialized wallet.
#[error("Persisted BDK wallet database at {path} does not contain an initialized wallet")]
ExistingWalletNotInitialized {
/// BDK wallet database path.
path: PathBuf,
},
/// Wallet error
#[error("Wallet error: {0}")]
Wallet(String),
/// Bitcoin RPC error
#[cfg(feature = "bitcoin-rpc")]
#[error("Bitcoin RPC error: {0}")]
BitcoinRpc(#[from] bdk_bitcoind_rpc::bitcoincore_rpc::Error),
/// The Bitcoin Core chain tip could not be determined for a fresh wallet.
#[cfg(feature = "bitcoin-rpc")]
#[error("Failed to determine the Bitcoin Core chain tip for a fresh wallet: {source}")]
ChainTipFetchFailed {
/// Underlying Bitcoin Core RPC error.
#[source]
source: bdk_bitcoind_rpc::bitcoincore_rpc::Error,
},
/// The configured fresh-wallet rescan height is above the current chain tip.
#[error("Wallet rescan height {requested} is above the current chain tip {tip}")]
WalletRescanHeightTooHigh {
/// Configured rescan height.
requested: u32,
/// Current Bitcoin Core chain tip.
tip: u32,
},
/// Esplora error
#[error("Esplora error: {0}")]
Esplora(String),
/// Electrum error
#[cfg(feature = "electrum")]
#[error("Electrum error: {0}")]
Electrum(String),
/// Bip32 key derivation error
#[error("Bip32 key derivation error: {0}")]
Bip32(#[from] bdk_wallet::bitcoin::bip32::Error),
/// Key derivation error
#[error("Key derivation error: {0}")]
KeyDerivation(#[from] bdk_wallet::keys::KeyError),
/// Could not sign transaction
#[error("Could not sign transaction")]
CouldNotSign,
/// Path error
#[error("Path error")]
Path,
/// IO error
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
/// KV Store error
#[error("KV Store error: {0}")]
KvStore(#[from] cdk_common::database::Error),
/// Could not find matching output vout in transaction
#[error("Could not find matching output vout in transaction")]
VoutNotFound,
/// Send intent not found in storage
#[error("Send intent not found: {0}")]
SendIntentNotFound(Uuid),
/// Send batch not found in storage
#[error("Send batch not found: {0}")]
SendBatchNotFound(Uuid),
/// Send intent with quote id already exists in storage
#[error("Send intent already exists for quote id: {0}")]
DuplicateQuoteId(String),
/// Batch fee exceeds the combined max fee of all included intents
#[error("Batch fee {actual_fee} exceeds combined max fee {max_fee}")]
BatchFeeTooHigh {
/// Actual transaction fee in sats
actual_fee: u64,
/// Maximum combined fee from included intents
max_fee: u64,
},
/// Current fee estimate exceeds the max fee accepted by a melt quote.
#[error("Estimated fee {estimated_fee} exceeds max fee {max_fee}")]
EstimatedFeeTooHigh {
/// Current estimated fee reserve in sats
estimated_fee: u64,
/// Maximum fee accepted by the quote in sats
max_fee: u64,
},
/// No valid fee allocation exists for the batch
#[error("No valid fee allocation for batch")]
NoValidFeeAllocation,
/// Requested recipient output is below the dust limit for its script type
#[error("Requested output amount {amount} sats is below dust limit {dust_limit} sats")]
DustOutput {
/// Requested recipient amount in sats
amount: u64,
/// Minimum non-dust amount for the destination script in sats
dust_limit: u64,
},
/// Requested send amount is below the backend's configured minimum.
#[error("Requested send amount {amount} sats is below minimum {min} sats")]
AmountBelowMinimumSend {
/// Requested recipient amount in sats
amount: u64,
/// Configured minimum send amount in sats
min: u64,
},
/// Batch record is missing an output assignment for one of its member intents.
///
/// This indicates a persistence invariant violation: every intent ID listed
/// in a Signed/Broadcast batch must have a corresponding assignment entry.
#[error("Batch {batch_id} is missing an output assignment for intent {intent_id}")]
BatchAssignmentMissing {
/// Batch that is missing the assignment
batch_id: Uuid,
/// Intent with no assignment entry
intent_id: Uuid,
},
/// Receive intent not found in storage
#[error("Receive intent not found: {0}")]
ReceiveIntentNotFound(Uuid),
/// Receive address not found in storage
#[error("Receive address not found: {0}")]
ReceiveAddressNotFound(String),
/// No unreserved receive address was found within the retry limit.
#[error("Could not reserve a fresh receive address after {attempts} attempts")]
ReceiveAddressReservationExhausted {
/// Number of addresses derived before giving up.
attempts: usize,
},
/// Database
#[error("Database error")]
BdkPersist,
}
impl From<Error> for cdk_common::payment::Error {
fn from(e: Error) -> Self {
Self::Onchain(Box::new(e))
}
}
impl Error {
/// Returns `true` when the error is a transient network / upstream
/// condition that is expected to resolve on retry.
///
/// This is used by the sync supervisor to decide whether to continue
/// retrying on the next tick (transient) or to treat the failure as
/// part of the backoff/restart policy (non-transient).
pub fn is_transient(&self) -> bool {
match self {
// Chain-source I/O is always transient: network blips, reorg
// races, upstream 5xx, DNS/TLS timeouts, etc. The sync loop
// retries them on the next tick regardless of the specific
// sub-variant, so classifying the whole variant as transient
// is accurate for operational purposes.
#[cfg(feature = "bitcoin-rpc")]
Self::BitcoinRpc(_) => true,
#[cfg(feature = "electrum")]
Self::Electrum(_) => true,
Self::Esplora(_) => true,
Self::Io(e) => matches!(
e.kind(),
std::io::ErrorKind::TimedOut
| std::io::ErrorKind::ConnectionRefused
| std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::NotConnected
| std::io::ErrorKind::BrokenPipe
| std::io::ErrorKind::Interrupted
| std::io::ErrorKind::UnexpectedEof
| std::io::ErrorKind::WouldBlock
),
_ => false,
}
}
}