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
//! CDK BDK onchain backend errors
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),
/// 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),
/// Database error
#[error("Database error: {0}")]
Database(#[from] bdk_wallet::rusqlite::Error),
/// 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),
/// Esplora error
#[error("Esplora error: {0}")]
Esplora(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),
/// 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,
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,
}
}
}