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
//! Errors assoicated with [`crate::lightclient::LightClient`].
use std::convert::Infallible;
use zcash_protocol::TxId;
use pepper_sync::error::{SyncError, SyncModeError};
use zingo_netutils::GetClientError;
#[cfg(feature = "nym")]
use crate::wallet::error::PriceError;
use crate::wallet::{
error::{CalculateTransactionError, ProposeSendError, ProposeShieldError, WalletError},
output::OutputRef,
};
#[derive(Debug, thiserror::Error)]
pub enum LightClientError {
/// Sync failed to launch..
#[error("Sync failed to launch.")]
SyncLaunchError,
/// Sync not running.
#[error("No sync handle. Sync is not running.")]
SyncNotRunning,
/// Sync error.
#[error("Sync error.")]
SyncError(#[from] SyncError<WalletError>),
/// Sync mode error.
#[error("Sync mode error.")]
SyncModeError(#[from] SyncModeError),
/// Send error.
#[error("Send error.")]
SendError(#[from] SendError),
/// gPRC client error.
#[error("gRPC client error.")]
ClientError(#[from] GetClientError),
/// Indexer request error.
#[error("Indexer request error.")]
IndexerError(#[from] zingo_netutils::Status),
/// File error.
#[error("File error. {0}")]
FileError(std::io::Error),
/// Wallet error.
#[error("Wallet error.")]
WalletError(#[from] WalletError),
/// Ironwood migration error.
#[error("Ironwood migration error.")]
MigrationError(#[from] MigrationError),
/// No indexer configured. Call set_indexer_uri() to connect before calling network operations.
#[error("Offline: no indexer configured. Call set_indexer_uri() to connect.")]
Offline,
/// Price fetch error. Exists only in nym builds: the fetch compiles
/// only with the mixnet stack, so other builds have no fetch to fail.
#[cfg(feature = "nym")]
#[error("Price fetch error.")]
PriceError(#[from] PriceError),
/// A mixnet-covered surface was attempted while the mixnet was unavailable.
#[cfg(feature = "nym")]
#[error(transparent)]
MixnetNotReady(#[from] crate::mixnet::MixnetNotReady),
/// A probe target outside the one endpoint shape the mixnet exit
/// policy carries.
#[cfg(feature = "nym")]
#[error("probe targets must be https on port 443; got '{0}'")]
IneligibleProbeTarget(http::Uri),
/// The configured migration transmission target shares the
/// synchronization endpoint's host, which would let that server correlate
/// the wallet's sync stream with its migration cohort (ADR 0011,
/// 2026-07-23).
#[error(
"the migration transmission target '{host}' is the synchronization endpoint; migration \
parts never go to the sync server. Configure a different migration_transmission_uri or \
remove it to use the Destination Rotation."
)]
MigrationTransmissionTargetIsSyncEndpoint {
/// The host both endpoints share.
host: String,
},
/// No Destination remains to carry migration parts.
#[error(transparent)]
NoEligibleDestination(#[from] crate::destination::servers::NoEligibleDestinations),
}
/// Errors from the Orchard→Ironwood migration entry points
/// ([`crate::lightclient::LightClient`] methods backed by
/// [`crate::wallet::migration`]). Plain-data payloads so an FFI layer can
/// map each variant mechanically.
#[derive(Debug, thiserror::Error)]
pub enum MigrationError {
/// An operation needed a migration and none is in progress.
#[error("No migration in progress.")]
NoMigration,
/// A second migration was started while one is in progress.
#[error("A migration is already in progress.")]
AlreadyInProgress,
/// ZIP 244 folds the anchor into the signature hash, so a transaction
/// signed ahead of its boundary commits to the wrong anchor.
#[error(
"The pre-signed strategy is unavailable until the Ironwood transaction digest excludes the anchor from the signature hash."
)]
PreSignedUnavailable,
/// The wallet's notes changed between planning and consent, so the
/// consented plan no longer describes what would be sent.
#[error("The wallet's notes changed since the plan was displayed. Re-plan and re-confirm.")]
ConsentStale,
/// The transmission cadence can change only while every part is unsent.
#[error("Phase 2 has begun; the transmission cadence can no longer change.")]
CadenceFixed,
/// Note splitting kept producing new rounds past the round bound.
#[error("Migration did not converge within {0} rounds.")]
SplitDidNotConverge(usize),
/// A note-splitting transaction failed or disappeared from the wallet.
#[error("Note-splitting transaction {0} failed or disappeared.")]
SplitTransactionFailed(TxId),
/// Note-splitting transactions were not confirmed within the polling
/// window.
#[error("Timed out waiting for note-splitting transactions to confirm.")]
SplitConfirmationTimeout,
/// The scheduled flow was asked to start over a plan that still needs
/// note splitting, which no scheduled-flow driver executes yet.
#[error(
"The wallet's notes need splitting before a scheduled migration, and the scheduled flow \
does not drive note splitting yet. Run the immediate migration instead."
)]
NoteSplittingRequired,
/// The one-call immediate path found a consented scheduled migration
/// and must not collapse its schedule.
#[error(
"A consented scheduled migration is in progress. Let it run (auto transmitting), advance \
it with catch-up, or cancel it before an immediate migration."
)]
ScheduledMigrationExists,
/// The migration in progress belongs to a different account.
#[error("The migration in progress belongs to a different account.")]
DifferentAccount,
/// The Ironwood era is too young to hold a part. A part transmits in one
/// bucket and anchors in a lower one, and both must sit above the NU6.3
/// activation, so the earliest window that can hold a part opens two
/// buckets above the activation's. Until then there is no legal anchor,
/// whatever the wallet's notes look like.
#[error(
"The Ironwood era is too young for a migration part: the first window that can \
hold one opens at height {retry_after}. Retry after it."
)]
IronwoodEraTooYoung {
/// The first transmission window boundary that can hold a part.
retry_after: zcash_protocol::consensus::BlockHeight,
},
}
#[derive(Debug, thiserror::Error)]
pub enum SendError {
/// Propose send error.
#[error("Propose send error.")]
ProposeSendError(#[from] ProposeSendError),
/// Propose shield error.
#[error("Propose shield error.")]
ProposeShieldError(#[from] ProposeShieldError),
/// Failed to construct sending transaction.
#[error("Failed to construct sending transaction. {0}")]
CalculateSendError(CalculateTransactionError<OutputRef>),
/// Failed to construct shielding transaction.
#[error("Failed to construct shielding transaction. {0}")]
CalculateShieldError(CalculateTransactionError<Infallible>),
/// Failed to retarget the stored proposal for offline signing.
#[error("Failed to retarget the stored proposal for offline signing. {0}")]
RetargetError(zcash_client_backend::proposal::ProposalError),
/// No proposal found in the wallet.
#[error("No proposal found in the wallet.")]
NoStoredProposal,
/// Transmission error.
#[error("Transmission error.")]
TransmissionError(#[from] TransmissionError),
/// OP_RETURN send error.
#[error("OP_RETURN send error. {0}")]
OpReturn(crate::wallet::error::WalletError),
/// An OP_RETURN proposal cannot be calculated without transmitting.
/// Its second transaction spends an output of the first.
#[error("An OP_RETURN proposal cannot be calculated without transmitting.")]
OpReturnNotCalculable,
/// The OP_RETURN proposal's source address was reserved by another send
/// after the proposal was made. Propose again.
#[error(
"The OP_RETURN proposal is stale: its source address is no longer next. Propose again."
)]
OpReturnSourceAddressStale,
/// The deshield was transmitted and a later step failed. If
/// `op_return_txid` is `None`, the proposal is stored again with the
/// deshield txid and `send_stored_proposal` resumes from the OP_RETURN
/// step. If it is `Some`, the OP_RETURN transaction is in the wallet
/// with `Calculated` status and `transmit_calculated` resends it.
#[error("OP_RETURN send failed after the deshield {deshield_txid} was transmitted. {source}")]
OpReturnAfterDeshield {
/// The transmitted deshield.
deshield_txid: TxId,
/// The calculated OP_RETURN transaction, if it was built.
op_return_txid: Option<TxId>,
/// The failure.
#[source]
source: Box<LightClientError>,
},
}
#[derive(Debug, thiserror::Error)]
pub enum TransmissionError {
/// Transmission failed.
#[error("Transmission failed. {0}")]
TransmissionFailed(String),
/// Transaction to transmit does not have `Calculated` status: {0}
#[error("Transaction to transmit does not have `Calculated` status: {0}")]
IncorrectTransactionStatus(TxId),
/// Txid reported by server does not match calculated txid.
#[error(
"Server error: txid reported by the server does not match calculated txid.\ncalculated txid:\n{0}\ntxid from server: {1}"
)]
IncorrectTxidFromServer(TxId, TxId),
}