keetanetwork-vote 0.3.1

Vote and VoteStaple model, codec, and signing for Keetanetwork blockchain
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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
//! Error types for vote construction, decoding, and validation.
//!
//! Every fallible operation in the crate surfaces a [`VoteError`]. Each
//! variant additionally exposes a stable, programmatic identifier through
//! [`VoteError::code`] suitable for cross-implementation error matching
//! and structured logging.

use alloc::string::ToString;

use keetanetwork_account::AccountError;
use keetanetwork_asn1::Asn1Error;
use keetanetwork_block::BlockError;
use keetanetwork_crypto::error::CryptoError;
use keetanetwork_error::KeetaNetError;
use keetanetwork_utils::impl_source_error_from;
use snafu::Snafu;

/// A required field of the [`VoteBuilder`](crate::VoteBuilder) that must be
/// set before an unsigned vote can be sealed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VoteField {
	/// The vote serial number.
	Serial,
	/// The issuing representative.
	Issuer,
	/// The validity window (`validityFrom` / `validityTo`).
	Validity,
}

/// Errors produced by the vote crate.
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum VoteError {
	/// ASN.1 transport (de)serialization failed
	#[snafu(display("ASN.1 error: {source}"))]
	Asn1 {
		/// Underlying ASN.1 error
		source: Asn1Error,
	},
	/// Account operation failed
	#[snafu(display("account error: {source}"))]
	Account {
		/// Underlying account error
		source: AccountError,
	},
	/// Cryptographic operation failed
	#[snafu(display("crypto error: {source}"))]
	Crypto {
		/// Underlying crypto error
		source: CryptoError,
	},
	/// Block decoding or validation failed
	#[snafu(display("block error: {source}"))]
	Block {
		/// Underlying block error
		source: BlockError,
	},
	// ----- Vote-level -----
	/// Subject DN serial does not match the certificate serial
	#[snafu(display("vote subject serial does not match certificate serial"))]
	SerialMismatch,
	/// Vote certificate version is not supported
	#[snafu(display("vote certificate version must be 3"))]
	InvalidVersion,
	/// Vote could not be constructed from the supplied bytes
	#[snafu(display("vote bytes are not a recognized construction"))]
	InvalidConstruction,
	/// Vote JSON object is missing required fields or is otherwise unusable
	#[snafu(display("vote JSON is not a valid construction"))]
	InvalidConstructionJson,
	/// Vote signature did not verify against the issuer public key
	#[snafu(display("vote signature did not verify"))]
	SignatureInvalid,
	/// Vote validity period has elapsed at the configured moment
	#[snafu(display("vote has expired"))]
	Expired,
	/// `validityFrom > validityTo`
	#[snafu(display("vote validity range is invalid"))]
	InvalidValidity,
	/// The check moment lands before `validityFrom` minus the slop
	#[snafu(display("vote was issued in the future"))]
	MomentBeforeValidityFrom,
	/// `no_std` callers must supply a validation moment; the
	/// `BlockTime::now`-based default is only available with `std`.
	#[snafu(display("validation moment must be provided in no_std builds"))]
	MissingMoment,

	// ----- Staple-level -----
	/// Staple bytes do not contain a valid two-element SEQUENCE
	#[snafu(display("malformed vote staple wrapper"))]
	MalformedStaple,
	/// Staple does not contain at least one block
	#[snafu(display("vote staple must contain at least one block"))]
	StapleBlocksAtLeastOne,
	/// Staple does not contain at least one vote
	#[snafu(display("vote staple must contain at least one vote"))]
	StapleVotesAtLeastOne,
	/// Inner staple element shape was wrong
	#[snafu(display("malformed vote staple element: {what}"))]
	MalformedStapleElement {
		/// Which element was malformed (`"blocks"` or `"votes"`)
		what: &'static str,
	},
	/// All votes in a staple must cover the same number of blocks
	#[snafu(display("votes within a staple disagree on block count"))]
	StapleBlockCountMismatch,
	/// A vote references a block hash that is not in the staple
	#[snafu(display("vote references a block hash not in the staple"))]
	StapleMissingBlock,
	/// Votes within a staple disagree on block ordering
	#[snafu(display("votes within a staple disagree on block ordering"))]
	StapleBlockOrderMismatch,
	/// Two votes in a staple share the same issuer
	#[snafu(display("vote staple has duplicate issuer"))]
	StapleDuplicateIssuer,
	/// Mixed permanent and temporary votes in a single staple
	#[snafu(display("vote staple mixes permanent and temporary votes"))]
	StaplePermanenceMismatch,
	/// Vote staple was attempted to be constructed with no inputs
	#[snafu(display("invalid vote staple construction"))]
	StapleInvalidConstruction,

	// ----- Builder-level -----
	/// A required builder field was not set before sealing
	#[snafu(display("vote builder is missing a required field: {field:?}"))]
	BuilderMissingField {
		/// Which required field was absent
		field: VoteField,
	},
	/// Builder received a value that is not a block hash
	#[snafu(display("invalid block reference for vote builder"))]
	BuilderInvalidBlockType,
	/// Fee value supplied to builder is invalid
	#[snafu(display("invalid fee supplied to vote builder"))]
	BuilderInvalidFee,

	// ----- Transport / DER details -----
	/// Outer wrapper does not parse as a 3-element SEQUENCE
	#[snafu(display("malformed vote wrapper"))]
	MalformedWrapper,
	/// TBS slot inside the wrapper was not a SEQUENCE
	#[snafu(display("malformed vote (tbs slot must be a sequence)"))]
	MalformedVoteWrapper,
	/// TBS certificate did not parse as expected
	#[snafu(display("malformed vote certificate body"))]
	MalformedVoteContent,
	/// TBS certificate has unexpected trailing data
	#[snafu(display("vote certificate has extra trailing data"))]
	MalformedVoteContentExtraData,
	/// Version field had the wrong tag or contents
	#[snafu(display("malformed vote version field"))]
	MalformedVoteVersion,
	/// Serial field was missing or wrong-typed
	#[snafu(display("malformed vote serial field"))]
	MalformedVoteSerial,
	/// Signature algorithm SEQUENCE was malformed
	#[snafu(display("malformed vote signature algorithm field"))]
	MalformedVoteSignatureInformation,
	/// Issuer DN was malformed
	#[snafu(display("malformed vote issuer information"))]
	MalformedVoteIssuerInformation,
	/// Subject DN was malformed
	#[snafu(display("malformed vote subject information"))]
	MalformedVoteSubjectInformation,
	/// Validity SEQUENCE was malformed
	#[snafu(display("malformed vote validity information"))]
	MalformedVoteValidityInformation,
	/// Extensions context tag or layout was malformed
	#[snafu(display("malformed vote extensions field"))]
	MalformedVoteExtensions,
	/// Per-extension data field was malformed
	#[snafu(display("malformed vote extension contents"))]
	MalformedVoteExtensionsData,
	/// Per-extension element shape was malformed
	#[snafu(display("malformed vote extension element"))]
	MalformedVoteExtensionsValue,
	/// Per-extension OID was missing or wrong-typed
	#[snafu(display("malformed vote extension OID"))]
	MalformedVoteExtensionsValueOid,
	/// Per-extension `critical` flag was wrong-typed
	#[snafu(display("malformed vote extension critical flag"))]
	MalformedVoteExtensionsValueCritical,
	/// An unknown critical extension was encountered
	#[snafu(display("unknown critical vote extension"))]
	MalformedVoteExtensionsValueCriticalType,
	/// Wrapper signature algorithm and TBS algorithm disagree
	#[snafu(display("vote signature algorithm does not match wrapper"))]
	MalformedVoteSignatureSchemeDoesNotMatchWrapper,
	/// Wrapper signature algorithm does not match the issuer key type
	#[snafu(display("vote signature algorithm does not match issuer"))]
	MalformedVoteSignatureSchemeDoesNotMatchIssuer,
	/// Signature algorithm OID is not one we know how to verify
	#[snafu(display("unsupported vote signature scheme"))]
	MalformedVoteSignatureUnsupportedScheme,
	/// ECDSA signature algorithm carried a curve OID that is not
	/// one of the supported NIST curves
	#[snafu(display("vote ECDSA curve is not supported"))]
	MalformedVoteSignatureSchemeEcdsaInvalidCurve,
	/// SubjectPublicKeyInfo had the wrong shape
	#[snafu(display("malformed subject public key info"))]
	MalformedVoteSubjectPublicKeyInformation,
	/// Signature BIT STRING was missing or malformed
	#[snafu(display("malformed vote signature value"))]
	MalformedVoteSignatureValue,
	/// No `hashData` extension was present in the vote certificate
	#[snafu(display("vote contains no block-hash extension"))]
	MalformedVoteNoBlocksFound,

	// ----- DN parsing helpers -----
	/// DN was not a SEQUENCE while looking for an RDN
	#[snafu(display("malformed DN: not a sequence"))]
	MalformedFindRdnInvalidType,
	/// DN was empty
	#[snafu(display("malformed DN: empty"))]
	MalformedFindRdnMustHaveOne,
	/// DN element was not well-formed (missing `name` or `value`)
	#[snafu(display("malformed DN: element not well-formed"))]
	MalformedFindRdnPartWellFormed,
	/// DN element was not a SET
	#[snafu(display("malformed DN: element not a set"))]
	MalformedFindRdnMustBeSet,
	/// DN element name was not an OID
	#[snafu(display("malformed DN: element name not an OID"))]
	MalformedFindRdnTypeMustBeOid,

	// ----- hashData extension contents -----
	/// `hashData` extension input was not parseable as ASN.1
	#[snafu(display("malformed hashData extension input"))]
	MalformedHashesFromVoteInvalidInput,
	/// `hashData` extension was not a context-specific tag
	#[snafu(display("hashData extension is not a context-specific tag"))]
	MalformedHashesFromVoteInvalidType,
	/// `hashData` extension context tag value was wrong
	#[snafu(display("hashData extension has wrong context tag"))]
	MalformedHashesFromVoteInvalidContextSpecific,
	/// `hashData` extension did not contain a 2-element SEQUENCE
	#[snafu(display("hashData extension data is not a sequence"))]
	MalformedHashesFromVoteDataHashDataMustBeSequence,
	/// `hashData` extension SEQUENCE had wrong number of items
	#[snafu(display("hashData extension expected exactly two items"))]
	MalformedHashesFromVoteDataNotTwoItems,
	/// `hashData` extension hash-algorithm OID was missing
	#[snafu(display("hashData extension is missing the hash algorithm OID"))]
	MalformedHashesFromVoteDataNeedsOid,
	/// `hashData` extension declared an unsupported hash algorithm
	#[snafu(display("hashData extension uses an unsupported hash algorithm"))]
	MalformedHashesFromVoteDataUnsupportedHashFunc,
	/// `hashData` extension blocks element was not a SEQUENCE
	#[snafu(display("hashData extension blocks element must be a sequence"))]
	MalformedHashesFromVoteDataSecondMustBeSequence,
	/// `hashData` extension contained a block hash that was not an OCTET STRING
	#[snafu(display("hashData extension contains a non-octet-string block hash"))]
	MalformedHashesFromVoteDataUnsupportedHashType,

	// ----- Fees extension -----
	/// Fee amount was negative
	#[snafu(display("fee amount cannot be negative"))]
	MalformedFeesAmount,
	/// Fees extension was not parseable as ASN.1
	#[snafu(display("fees extension is not valid ASN.1"))]
	MalformedFeesFromVoteInvalidInput,
	/// Permanent vote may not carry fees
	#[snafu(display("permanent votes cannot carry fees"))]
	MalformedFeesInPermanentVote,
	/// Fees extension `quote` flag did not match the vote variant
	#[snafu(display("fees extension quote flag does not match vote variant"))]
	MalformedFeesQuoteInvalid,
	/// Fee entries within a single fees extension disagree on the
	/// `quote` flag, or the flag was missing entirely
	#[snafu(display("fees extension has inconsistent or missing quote flag"))]
	MalformedFeesInvalidQuoteValue,
	/// Fees extension was missing both the single and multiple branches
	#[snafu(display("fees extension is missing its kind discriminator"))]
	MalformedFeesKindMissing,
	/// Multi-fee extension was an empty array
	#[snafu(display("multiple-fee extension array is empty"))]
	MalformedFeesMultipleFeeEmpty,
	/// `payTo` field was not a valid account or storage address
	#[snafu(display("fee payTo is not an account or storage address"))]
	MalformedFeesPayToInvalid,
	/// `token` field was not a token account
	#[snafu(display("fee token field is not a token account"))]
	MalformedFeesTokenNotToken,

	// ----- Quote / non-quote distinction -----
	/// Tried to construct a `Vote` from quote bytes
	#[snafu(display("attempted to construct a Vote from a VoteQuote"))]
	FeeIsQuote,
	/// Tried to construct a `VoteQuote` from non-quote bytes
	#[snafu(display("attempted to construct a VoteQuote from a Vote"))]
	FeeNotQuote,
	/// Quote vote was missing fees
	#[snafu(display("quote vote must carry fees"))]
	FeeQuoteMissingFees,

	// ----- Round-trip strictness -----
	/// Vote bytes did not round-trip through canonical DER encoding
	#[snafu(display("vote bytes are not canonical DER"))]
	MalformedNonCanonicalEncoding,
}

impl_source_error_from!(VoteError, {
	Asn1Error => Asn1,
	AccountError => Account,
	CryptoError => Crypto,
	BlockError => Block,
});

impl VoteError {
	/// A stable, programmatic identifier for this error.
	///
	/// Returns [`None`] for the wrapped variants ([`VoteError::Asn1`],
	/// [`VoteError::Account`], [`VoteError::Crypto`], [`VoteError::Block`])
	/// whose underlying source error already carries its own code.
	pub fn code(&self) -> Option<&'static str> {
		let code = match self {
			VoteError::SerialMismatch => "VOTE_SERIAL_MISMATCH",
			VoteError::InvalidVersion => "VOTE_INVALID_VERSION",
			VoteError::InvalidConstruction => "VOTE_INVALID_CONSTRUCTION",
			VoteError::InvalidConstructionJson => "VOTE_INVALID_CONSTRUCTION_JSON",
			VoteError::SignatureInvalid => "VOTE_SIGNATURE_INVALID",
			VoteError::Expired => "VOTE_EXPIRED",
			VoteError::InvalidValidity => "VOTE_INVALID_VALIDITY",
			VoteError::MomentBeforeValidityFrom => "VOTE_MOMENT_BEFORE_VALIDITY_FROM",
			VoteError::MissingMoment => "VOTE_MISSING_MOMENT",

			VoteError::StapleInvalidConstruction => "VOTE_STAPLE_INVALID_CONSTRUCTION",
			VoteError::StapleBlockCountMismatch => "VOTE_STAPLE_ALL_VOTES_MUST_HAVE_SAME_BLOCKS_COUNT",
			VoteError::StapleMissingBlock => "VOTE_STAPLE_ALL_VOTES_MUST_HAVE_SAME_BLOCKS_MISSING",
			VoteError::StapleBlockOrderMismatch => "VOTE_STAPLE_ALL_VOTES_MUST_HAVE_SAME_BLOCKS_ORDER",
			VoteError::StapleDuplicateIssuer => "VOTE_STAPLE_DUPLICATE_VOTE_ISSUER",
			VoteError::StaplePermanenceMismatch => "VOTE_STAPLE_PERMANENCE_MISMATCH",

			VoteError::BuilderMissingField { field: VoteField::Serial } => "VOTE_BUILDER_INVALID_SERIAL",
			VoteError::BuilderMissingField { field: VoteField::Issuer } => "VOTE_BUILDER_INVALID_CONSTRUCTION",
			VoteError::BuilderMissingField { field: VoteField::Validity } => "VOTE_BUILDER_INVALID_VALID_TO_FROM",
			VoteError::BuilderInvalidBlockType => "VOTE_BUILDER_INVALID_BLOCK_TYPE",
			VoteError::BuilderInvalidFee => "VOTE_BUILDER_INVALID_FEE",

			VoteError::MalformedWrapper => "VOTE_MALFORMED_WRAPPER",
			VoteError::MalformedVoteWrapper => "VOTE_MALFORMED_VOTE_WRAPPER",
			VoteError::MalformedVoteContent => "VOTE_MALFORMED_VOTE_CONTENT",
			VoteError::MalformedVoteContentExtraData => "VOTE_MALFORMED_VOTE_CONTENT_EXTRA_DATA",
			VoteError::MalformedVoteVersion => "VOTE_MALFORMED_VOTE_VERSION",
			VoteError::MalformedVoteSerial => "VOTE_MALFORMED_VOTE_SERIAL",
			VoteError::MalformedVoteSignatureInformation => "VOTE_MALFORMED_VOTE_SIGNATURE_INFORMATION",
			VoteError::MalformedVoteIssuerInformation => "VOTE_MALFORMED_VOTE_ISSUER_INFORMATION",
			VoteError::MalformedVoteSubjectInformation => "VOTE_MALFORMED_VOTE_SUBJECT_INFORMATION",
			VoteError::MalformedVoteValidityInformation => "VOTE_MALFORMED_VOTE_VALIDITY_INFORMATION",
			VoteError::MalformedVoteExtensions => "VOTE_MALFORMED_VOTE_EXTENSIONS",
			VoteError::MalformedVoteExtensionsData => "VOTE_MALFORMED_VOTE_EXTENSIONS_DATA",
			VoteError::MalformedVoteExtensionsValue => "VOTE_MALFORMED_VOTE_EXTENSIONS_VALUE",
			VoteError::MalformedVoteExtensionsValueOid => "VOTE_MALFORMED_VOTE_EXTENSIONS_VALUE_OID",
			VoteError::MalformedVoteExtensionsValueCritical => "VOTE_MALFORMED_VOTE_EXTENSIONS_VALUE_CRITICAL",
			VoteError::MalformedVoteExtensionsValueCriticalType => "VOTE_MALFORMED_VOTE_EXTENSIONS_VALUE_CRITICAL_TYPE",
			VoteError::MalformedVoteSignatureSchemeDoesNotMatchWrapper => {
				"VOTE_MALFORMED_VOTE_SIGNATURE_SCHEME_DOES_NOT_MATCH_WRAPPER"
			}
			VoteError::MalformedVoteSignatureSchemeDoesNotMatchIssuer => {
				"VOTE_MALFORMED_VOTE_SIGNATURE_SCHEME_DOES_NOT_MATCH_ISSUER"
			}
			VoteError::MalformedVoteSignatureUnsupportedScheme => "VOTE_MALFORMED_VOTE_SIGNATURE_UNSUPPORTED_SCHEME",
			VoteError::MalformedVoteSignatureSchemeEcdsaInvalidCurve => {
				"VOTE_MALFORMED_VOTE_SIGNATURE_SCHEME_ECDSA_INVALID_CURVE"
			}
			VoteError::MalformedVoteSubjectPublicKeyInformation => "VOTE_MALFORMED_VOTE_SUBJECT_PUBLIC_KEY_INFORMATION",
			VoteError::MalformedVoteSignatureValue => "VOTE_MALFORMED_VOTE_SIGNATURE_VALUE",
			VoteError::MalformedVoteNoBlocksFound => "VOTE_MALFORMED_VOTE_NO_BLOCKS_FOUND",

			VoteError::MalformedStaple => "VOTE_MALFORMED_STAPLE",
			VoteError::MalformedStapleElement { what: "blocks" } => "VOTE_MALFORMED_STAPLE_BLOCKS",
			VoteError::MalformedStapleElement { what: "votes" } => "VOTE_MALFORMED_STAPLE_VOTES",
			VoteError::StapleBlocksAtLeastOne => "VOTE_MALFORMED_STAPLE_BLOCKS_AT_LEAST_ONE",
			VoteError::StapleVotesAtLeastOne => "VOTE_MALFORMED_STAPLE_VOTES_AT_LEAST_ONE",

			VoteError::MalformedFindRdnInvalidType => "VOTE_MALFORMED_FIND_RDN_INVALID_TYPE",
			VoteError::MalformedFindRdnMustHaveOne => "VOTE_MALFORMED_FIND_RDN_MUST_HAVE_ONE",
			VoteError::MalformedFindRdnPartWellFormed => "VOTE_MALFORMED_FIND_RDN_PART_WELL_FORMED",
			VoteError::MalformedFindRdnMustBeSet => "VOTE_MALFORMED_FIND_RDN_MUST_BE_SET",
			VoteError::MalformedFindRdnTypeMustBeOid => "VOTE_MALFORMED_FIND_RDN_TYPE_MUST_BE_OID",

			VoteError::MalformedHashesFromVoteInvalidInput => "VOTE_MALFORMED_HASHES_FROM_VOTE_INVALID_INPUT",
			VoteError::MalformedHashesFromVoteInvalidType => "VOTE_MALFORMED_HASHES_FROM_VOTE_INVALID_TYPE",
			VoteError::MalformedHashesFromVoteInvalidContextSpecific => {
				"VOTE_MALFORMED_HASHES_FROM_VOTE_INVALID_CONTEXT_SPECIFIC"
			}
			VoteError::MalformedHashesFromVoteDataHashDataMustBeSequence => {
				"VOTE_MALFORMED_HASHES_FROM_VOTE_DATA_HASH_DATA_MUST_BE_SEQUENCE"
			}
			VoteError::MalformedHashesFromVoteDataNotTwoItems => "VOTE_MALFORMED_HASHES_FROM_VOTE_DATA_NOT_TWO_ITEMS",
			VoteError::MalformedHashesFromVoteDataNeedsOid => "VOTE_MALFORMED_HASHES_FROM_VOTE_DATA_NEEDS_OID",
			VoteError::MalformedHashesFromVoteDataUnsupportedHashFunc => {
				"VOTE_MALFORMED_HASHES_FROM_VOTE_DATA_UNSUPPORTED_HASH_FUNC"
			}
			VoteError::MalformedHashesFromVoteDataSecondMustBeSequence => {
				"VOTE_MALFORMED_HASHES_FROM_VOTE_DATA_SECOND_MUST_BE_SEQUENCE"
			}
			VoteError::MalformedHashesFromVoteDataUnsupportedHashType => {
				"VOTE_MALFORMED_HASHES_FROM_VOTE_DATA_UNSUPPORTED_HASH_TYPE"
			}

			VoteError::MalformedFeesAmount => "VOTE_MALFORMED_FEES_AMOUNT",
			VoteError::MalformedFeesFromVoteInvalidInput => "VOTE_MALFORMED_FEES_FROM_VOTE_INVALID_INPUT",
			VoteError::MalformedFeesInPermanentVote => "VOTE_MALFORMED_FEES_IN_PERMANENT_VOTE",
			VoteError::MalformedFeesQuoteInvalid => "VOTE_MALFORMED_FEES_QUOTE_INVALID",
			VoteError::MalformedFeesInvalidQuoteValue => "VOTE_MALFORMED_FEES_INVALID_QUOTE_VALUE",
			VoteError::MalformedFeesKindMissing => "VOTE_MALFORMED_FEES_KIND_MISSING",
			VoteError::MalformedFeesMultipleFeeEmpty => "VOTE_MALFORMED_FEES_MULTIPLE_FEE_EMPTY",
			VoteError::MalformedFeesPayToInvalid => "VOTE_MALFORMED_FEES_PAY_TO_INVALID",
			VoteError::MalformedFeesTokenNotToken => "VOTE_MALFORMED_FEES_TOKEN_NOT_TOKEN",

			VoteError::FeeIsQuote => "VOTE_FEE_IS_QUOTE",
			VoteError::FeeNotQuote => "VOTE_FEE_NOT_QUOTE",
			VoteError::FeeQuoteMissingFees => "VOTE_FEE_QUOTE_MISSING_FEES",

			_ => return None,
		};

		Some(code)
	}
}

impl From<VoteError> for KeetaNetError {
	fn from(error: VoteError) -> Self {
		if let Some(code) = error.code() {
			KeetaNetError::Code { code: code.to_string(), message: error.to_string() }
		} else {
			KeetaNetError::Unknown { msg: error.to_string() }
		}
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn test_code_mapping() {
		assert_eq!(VoteError::SerialMismatch.code(), Some("VOTE_SERIAL_MISMATCH"));
		assert_eq!(VoteError::Expired.code(), Some("VOTE_EXPIRED"));
		assert_eq!(VoteError::FeeIsQuote.code(), Some("VOTE_FEE_IS_QUOTE"));
		assert_eq!(VoteError::Crypto { source: CryptoError::InvalidInput }.code(), None);
	}

	#[test]
	fn test_staple_element_codes_split() {
		assert_eq!(VoteError::MalformedStapleElement { what: "blocks" }.code(), Some("VOTE_MALFORMED_STAPLE_BLOCKS"));
		assert_eq!(VoteError::MalformedStapleElement { what: "votes" }.code(), Some("VOTE_MALFORMED_STAPLE_VOTES"));
	}

	#[test]
	fn test_keetanet_error_bridge() {
		let bridged = KeetaNetError::from(VoteError::SerialMismatch);
		assert!(matches!(bridged, KeetaNetError::Code { code, .. } if code == "VOTE_SERIAL_MISMATCH"));

		let unknown = KeetaNetError::from(VoteError::Crypto { source: CryptoError::InvalidInput });
		assert!(matches!(unknown, KeetaNetError::Unknown { .. }));
	}

	#[test]
	fn test_source_conversions() {
		assert!(matches!(VoteError::from(Asn1Error::InvalidVoteVersion), VoteError::Asn1 { .. }));
		assert!(matches!(VoteError::from(AccountError::InvalidKeyType), VoteError::Account { .. }));
		assert!(matches!(VoteError::from(CryptoError::InvalidInput), VoteError::Crypto { .. }));
	}
}