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
//! The crate's error type.
//!
//! One rule shapes this enum: a **reverting replay is not an error** — it's a
//! successful observation, reported inside [`crate::replay::ReplayResult`]. An
//! `Err` here always means svmscope itself could not do what was asked: the RPC
//! failed, the transaction doesn't exist, a mutation targeted a missing account,
//! a named field couldn't be resolved. That distinction is what lets a scenario
//! that *expects* a revert never be satisfied by an infrastructure failure.
/// Convenience alias used across the crate.
pub type Result<T> = std::result::Result<T, Error>;
/// Everything that can go wrong short of the transaction itself reverting.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// An RPC request failed (network, rate limit, node error).
#[error("rpc request failed: {0}")]
Rpc(#[source] Box<solana_client::client_error::ClientError>),
/// The RPC answered, but not with the shape we expect.
#[error("unexpected rpc response: {0}")]
MalformedRpcResponse(String),
/// No transaction with this signature exists on the queried cluster.
#[error("transaction not found: {0}")]
TransactionNotFound(String),
/// The address has no transaction history on the queried cluster.
#[error("no transactions found for address {0}")]
NoSignatures(String),
/// The string is not a valid base58 Solana address.
#[error("not a valid address: {0}")]
InvalidAddress(String),
/// The account does not exist on the queried cluster.
#[error("account not found: {0}")]
AccountNotFound(String),
/// The program publishes no on-chain Anchor IDL (and none was supplied).
#[error("{0} publishes no on-chain Anchor IDL — supply the IDL JSON to decode against it")]
NoIdl(String),
/// A mutation targeted an account the replay never loaded — usually a typo'd
/// address. Reported as a hard error so an `expect: revert` scenario can
/// never silently pass because of it.
#[error("mutation targets an account not loaded in the replay: {0}")]
MutationTargetMissing(String),
/// A data patch would write past the end of the account's data.
#[error(
"patch out of range for {address}: offset {offset} + {len} bytes > account size {size}"
)]
PatchOutOfRange {
/// The account the patch targeted.
address: String,
/// Byte offset where the patch begins.
offset: usize,
/// Length of the patch in bytes.
len: usize,
/// The account's actual data size in bytes.
size: usize,
},
/// A named-field assert referenced a field the account's layout doesn't have.
#[error("no field \"{field}\" on this {type_name} (fields: {})", available.join(", "))]
UnknownField {
/// The field name that was requested.
field: String,
/// The account type the field was looked up on.
type_name: String,
/// Every field name the layout actually has.
available: Vec<String>,
},
/// A short field name matched several fields — the candidates disambiguate.
#[error("\"{field}\" is ambiguous here — use one of: {}", candidates.join(", "))]
AmbiguousField {
/// The ambiguous short name.
field: String,
/// The fully qualified names it could mean.
candidates: Vec<String>,
},
/// Only integer/bool fields can be compared numerically.
#[error("{field} is a {ty} — only integer/bool fields can be asserted")]
NonNumericField {
/// The field that was asserted.
field: String,
/// Its actual (non-numeric) type.
ty: String,
},
/// A field or offset read fell outside the account's data.
#[error("{what} out of range (account data is {len} bytes)")]
OutOfRange {
/// What was being read (a field or byte range description).
what: String,
/// The account's data length in bytes.
len: usize,
},
/// The account's bytes couldn't be decoded into named fields.
#[error(
"can't decode {address} into named fields (no known layout, and no IDL for its program {owner})"
)]
UndecodableAccount {
/// The account that couldn't be decoded.
address: String,
/// The program that owns it.
owner: String,
},
/// A transaction's wire bytes couldn't be decoded (bad base64 or bincode).
#[error("could not decode transaction: {0}")]
TxDecode(String),
/// A fixture couldn't be serialized, parsed, or reloaded.
#[error("fixture error: {0}")]
Fixture(String),
/// A suite/scenario/mutation specification was invalid (bad op, kind, hex…).
#[error("{0}")]
InvalidSpec(String),
/// A submitted transaction did not reach confirmed commitment in time.
#[error("timed out waiting for transaction confirmation: {signature}")]
ConfirmationTimeout {
/// The submitted transaction's signature.
signature: String,
},
/// The transaction confirmed, but the RPC node had not indexed its
/// `getTransaction` metadata before the wait timed out.
#[error(
"transaction {signature} was confirmed, but its metadata was unavailable before timeout"
)]
TransactionMetadataUnavailable {
/// The confirmed transaction's signature.
signature: String,
},
/// The requested method name does not exist in the program's IDL.
#[error("method {method} was not found in the IDL for program {program}")]
MethodNotFound {
/// The program whose IDL was searched.
program: String,
/// The method name that wasn't found.
method: String,
},
/// [`MethodBuilder::payer`](crate::MethodBuilder::payer) was never called.
#[error("no payer was supplied for method {method}")]
MissingPayer {
/// The method being built.
method: String,
},
/// The IDL requires an account that was never supplied to the builder.
#[error("account {account} is required by method {method}")]
MissingInstructionAccount {
/// The method being built.
method: String,
/// The IDL name of the missing account.
account: String,
},
/// The IDL marks an account as a signer, but no matching signer was added.
#[error("missing signer for account {account} ({address})")]
MissingSigner {
/// The IDL name of the account that must sign.
account: String,
/// The address that has no registered signer.
address: String,
},
/// The IDL requires an argument that was never supplied to the builder.
#[error("argument {argument} is required by method {method}")]
MissingArgument {
/// The method being built.
method: String,
/// The IDL name of the missing argument.
argument: String,
},
/// An argument was supplied that the method's IDL does not declare —
/// usually a typo'd name.
#[error("unknown argument {argument} for method {method}")]
UnknownArgument {
/// The method being built.
method: String,
/// The unrecognized argument name.
argument: String,
},
/// An argument's JSON value couldn't be Borsh-encoded as its IDL type.
#[error("could not encode argument {argument}: {reason}")]
ArgumentEncoding {
/// Dotted path to the failing value (e.g. `config.limits[1]`).
argument: String,
/// Why encoding failed.
reason: String,
},
/// The signed transaction couldn't be assembled (e.g. a signature failed).
#[error("could not construct transaction: {0}")]
TransactionBuild(String),
}
impl Error {
/// Wrap a solana client error (boxed — `ClientError` is large).
pub(crate) fn rpc(e: solana_client::client_error::ClientError) -> Error {
Error::Rpc(Box::new(e))
}
}