ibc-relayer 0.21.0

Implementation of an IBC Relayer in Rust, as a library
Documentation
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
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
//! This module defines the various errors that be raised in the relayer.

use core::time::Duration;

use flex_error::{define_error, DisplayOnly, TraceError};
use http::uri::InvalidUri;
use humantime::format_duration;
use ibc_proto::protobuf::Error as TendermintProtoError;
use prost::{DecodeError, EncodeError};
use regex::Regex;
use tendermint::abci;
use tendermint::Error as TendermintError;
use tendermint_light_client::components::io::IoError as LightClientIoError;
use tendermint_light_client::errors::{
    Error as LightClientError, ErrorDetail as LightClientErrorDetail,
};
use tendermint_rpc::endpoint::abci_query::AbciQuery;
use tendermint_rpc::endpoint::broadcast::tx_sync::Response as TxSyncResponse;
use tendermint_rpc::Error as TendermintRpcError;
use tonic::{
    metadata::errors::InvalidMetadataValue, transport::Error as TransportError,
    Status as GrpcStatus,
};

use ibc_relayer_types::{
    applications::ics29_fee::error::Error as FeeError,
    clients::ics07_tendermint::error as tendermint_error,
    core::{
        ics02_client::{client_type::ClientType, error as client_error},
        ics03_connection::error as connection_error,
        ics23_commitment::error as commitment_error,
        ics24_host::identifier::{ChainId, ChannelId, ConnectionId},
    },
    proofs::ProofError,
    relayer::ics18_relayer::error as relayer_error,
};

use crate::chain::cosmos::version;
use crate::chain::cosmos::BLOCK_MAX_BYTES_MAX_FRACTION;
use crate::config::Error as ConfigError;
use crate::event::monitor;
use crate::keyring::{errors::Error as KeyringError, KeyType};
use crate::sdk_error::SdkError;

define_error! {
    Error {
        Io
            [ TraceError<std::io::Error> ]
            |_| { "I/O error" },

        Rpc
            { url: tendermint_rpc::Url }
            [ TendermintRpcError ]
            |e| { format!("RPC error to endpoint {}", e.url) },

        AbciQuery
            { query: AbciQuery }
            |e| { format!("ABCI query returned an error: {:?}", e.query) },

        Config
            [ ConfigError ]
            |_| { "Configuration error" },

        CheckTx
            {
                response: TxSyncResponse,
            }
            | e | { format!("CheckTx returned an error: {:?}", e.response) },

        DeliverTx
            {
                detail: SdkError,
                tx: abci::response::DeliverTx,
            }
            |e| { format!("DeliverTx Commit returns error: {0}. RawResult: {1:?}", e.detail, e.tx) },

        SendTx
            {
                detail: String
            }
            |e| { format_args!("send_tx resulted in chain error event: {}", e.detail) },

        WebSocket
            { url: tendermint_rpc::Url }
            |e| { format!("Websocket error to endpoint {}", e.url) },

        EventMonitor
            [ monitor::Error ]
            |_| { "event monitor error" },

        Grpc
            |_| { "gRPC error" },

        GrpcStatus
            { status: GrpcStatus }
            |e| { format!("gRPC call failed with status: {0}", e.status) },

        GrpcTransport
            [ TraceError<TransportError> ]
            |_| { "error in underlying transport when making gRPC call" },

        GrpcResponseParam
            { param: String }
            |e| { format!("missing parameter in GRPC response: {}", e.param) },

        Decode
            [ TendermintProtoError ]
            |_| { "error decoding protobuf" },

        LightClientVerification
            { chain_id: String }
            [ LightClientError ]
            |e| { format!("light client verification error for chain id {0}", e.chain_id) },

        LightClientState
            [ client_error::Error ]
            |_| { "light client encountered error due to client state".to_string() },

        LightClientIo
            { address: String }
            [ LightClientIoError ]
            |e| { format!("light client error for RPC address {0}", e.address) },

        ChainNotCaughtUp
            {
                address: String,
                chain_id: ChainId,
            }
            |e| { format!("node at {} running chain {} not caught up", e.address, e.chain_id) },

        PrivateStore
            |_| { "requested proof for a path in the private store" },

        Event
            |_| { "bad notification" },

        ConversionFromAny
            [ TendermintProtoError ]
            |_| { "conversion from a protobuf `Any` into a domain type failed" },

        EmptyUpgradedClientState
            |_| { "found no upgraded client state" },

        ConsensusStateTypeMismatch
            {
                expected: ClientType,
                got: ClientType,
            }
            |e| { format!("consensus state type mismatch; hint: expected client type '{0}', got '{1}'", e.expected, e.got) },

        EmptyResponseValue
            |_| { "empty response value" },

        EmptyResponseProof
            |_| { "empty response proof" },

        RpcResponse
            { detail: String }
            | e | { format!("RPC client returns error response: {}", e.detail) },

        MalformedProof
            [ ProofError ]
            |_| { "malformed proof" },

        InvalidHeight
            [ TendermintError ]
            |_| { "invalid height" },

        InvalidHeightNoSource
            |_| { "invalid height" },

        InvalidMetadata
            [ TraceError<InvalidMetadataValue> ]
            |_| { "invalid metadata" },

        BuildClientStateFailure
            |_| { "failed to create client state" },

        CreateClient
            { client_id: String }
            |e| { format!("failed to create client {0}", e.client_id) },

        ClientStateType
            { client_state_type: String }
            |e| { format!("unexpected client state type {0}", e.client_state_type) },

        ConnectionNotFound
            { connection_id: ConnectionId }
            |e| { format!("connection not found: {0}", e.connection_id) },

        BadConnectionState
            |_| { "bad connection state" },

        ConnOpen
            { connection_id: ConnectionId, reason: String }
            |e| {
                format!("failed to build conn open message {0}: {1}", e.connection_id, e.reason)
            },

        ConnOpenInit
            { reason: String }
            |e| { format!("failed to build conn open init: {0}", e.reason) },

        ConnOpenTry
            { reason: String }
            |e| { format!("failed to build conn open try: {0}", e.reason) },

        ChanOpenAck
            { channel_id: ChannelId, reason: String }
            |e| {
                format!("failed to build channel open ack {0}: {1}", e.channel_id, e.reason)
            },

        ChanOpenConfirm
            { channel_id: ChannelId, reason: String }
            |e| {
                format!("failed to build channel open confirm {0}: {1}", e.channel_id, e.reason)
            },

        ConsensusProof
            [ ProofError ]
            |_| { "failed to build consensus proof" },

        Packet
            { channel_id: ChannelId, reason: String }
            |e| {
                format!("failed to build packet {0}: {1}", e.channel_id, e.reason)
            },

        RecvPacket
            { channel_id: ChannelId, reason: String }
            |e| {
                format!("failed to build recv packet {0}: {1}", e.channel_id, e.reason)
            },

        AckPacket
            { channel_id: ChannelId, reason: String }
            |e| {
                format!("failed to build acknowledge packet {0}: {1}", e.channel_id, e.reason)
            },

        TimeoutPacket
            { channel_id: ChannelId, reason: String }
            |e| {
                format!("failed to build timeout packet {0}: {1}", e.channel_id, e.reason)
            },

        MessageTransaction
            { reason: String }
            |e| { format!("message transaction failure: {0}", e.reason) },

        Query
            { query: String }
            |e| { format!("query error occurred (failed to query for {0})", e.query) },

        KeyBase
            [ KeyringError ]
            |_| { "keyring error" },

        KeyNotFound
            { key_name: String }
            [ KeyringError ]
            |e| { format!("signature key not found: {}", e.key_name) },

        Ics02
            [ client_error::Error ]
            |e| { format!("ICS 02 error: {}", e.source) },

        Ics03
            [ connection_error::Error ]
            |_| { "ICS 03 error" },

        Ics07
            [ tendermint_error::Error ]
            |_| { "ICS 07 error" },

        Ics18
            [ relayer_error::Error ]
            |_| { "ICS 18 error" },

        Ics23
            [ commitment_error::Error ]
            |_| { "ICS 23 error" },

        Ics29
            [ FeeError ]
            | _ | { "ICS 29 error" },

        InvalidUri
            { uri: String }
            [ TraceError<InvalidUri> ]
            |e| { format!("error parsing URI {}", e.uri) },

        ChainIdentifier
            { chain_id: String }
            |e| { format!("invalid chain identifier format: {0}", e.chain_id) },

        NonProvableData
            |_| { "requested proof for data in the privateStore" },

        ChannelSend
            |_| { "internal message-passing failure while sending inter-thread request/response" },

        ChannelReceive
            [ TraceError<crossbeam_channel::RecvError> ]
            |_| { "internal message-passing failure while receiving inter-thread request/response" },

        ChannelReceiveTimeout
            [ TraceError<crossbeam_channel::RecvTimeoutError> ]
            |_| { "timeout when waiting for reponse over inter-thread channel" },

        InvalidInputHeader
            |_| { "the input header is not recognized as a header for this chain" },

        TxNoConfirmation
            |_| { "failed tx: no confirmation" },

        Misbehaviour
            { reason: String }
            |e| { format!("error raised while submitting the misbehaviour evidence: {0}", e.reason) },

        InvalidKeyAddress
            { address: String }
            [ TendermintError ]
            |e| { format!("invalid key address: {0}", e.address) },

        Bech32Encoding
            [ TraceError<bech32::Error> ]
            |_| { "bech32 encoding failed" },

        ClientTypeMismatch
            {
                expected: ClientType,
                got: ClientType,
            }
            |e| {
                format!("client type mismatch: expected '{}', got '{}'",
                e.expected, e.got)
            },

        ProtobufDecode
            { payload_type: String }
            [ TraceError<DecodeError> ]
            |e| { format!("error decoding protocol buffer for {}", e.payload_type) },

        ProtobufEncode
            { payload_type: String }
            [ TraceError<EncodeError> ]
            |e| { format!("error encoding protocol buffer for {}", e.payload_type) },

        TxSimulateGasEstimateExceeded
            {
                chain_id: ChainId,
                estimated_gas: u64,
                max_gas: u64,
            }
            |e| {
                format!("{} gas estimate {} from simulated Tx exceeds the maximum configured {}",
                    e.chain_id, e.estimated_gas, e.max_gas)
            },

        HealthCheckJsonRpc
            {
                chain_id: ChainId,
                address: String,
                endpoint: String,
            }
            [ DisplayOnly<tendermint_rpc::error::Error> ]
            |e| {
                format!("health check failed for endpoint {0} on the JSON-RPC interface of chain {1}:{2}",
                    e.endpoint, e.chain_id, e.address)
            },

        FetchVersionParsing
            {
                chain_id: ChainId,
                address: String,
            }
            [ version::Error ]
            |e| {
                format!("failed while parsing version info for chain {0}:{1}; caused by: {2}",
                    e.chain_id, e.address, e.source)
            },

        FetchVersionGrpcTransport
            {
                chain_id: ChainId,
                address: String,
                endpoint: String,
            }
            [ DisplayOnly<tonic::transport::Error> ]
            |e| {
                format!("failed while fetching version info from endpoint {0} on the gRPC interface of chain {1}:{2}",
                    e.endpoint, e.chain_id, e.address)
            },

        FetchVersionGrpcStatus
            {
                chain_id: ChainId,
                address: String,
                endpoint: String,
                status: tonic::Status
            }
            |e| {
                format!("failed while fetching version info from endpoint {0} on the gRPC interface of chain {1}:{2}; caused by: {3}",
                    e.endpoint, e.chain_id, e.address, e.status)
            },

        FetchVersionInvalidVersionResponse
            {
                chain_id: ChainId,
                address: String,
                endpoint: String,
            }
            |e| {
                format!("failed while fetching version info from endpoint {0} on the gRPC interface of chain {1}:{2}; the gRPC response contains no application version information",
                    e.endpoint, e.chain_id, e.address)
            },

        ConfigValidationJsonRpc
            {
                chain_id: ChainId,
                address: String,
                endpoint: String,
            }
            [ DisplayOnly<tendermint_rpc::error::Error> ]
            |e| {
                format!("semantic config validation: failed to reach endpoint {0} on the JSON-RPC interface of chain {1}:{2}",
                    e.endpoint, e.chain_id, e.address)
            },

        ConfigValidationTxSizeOutOfBounds
            {
                chain_id: ChainId,
                configured_bound: usize,
                genesis_bound: u64,
            }
            |e| {
                format!("semantic config validation failed for option `max_tx_size` for chain '{}', reason: `max_tx_size` = {} is greater than {}% of the consensus parameter `max_size` = {}",
                    e.chain_id, e.configured_bound, BLOCK_MAX_BYTES_MAX_FRACTION * 100.0, e.genesis_bound)
            },

        ConfigValidationMaxGasTooHigh
            {
                chain_id: ChainId,
                configured_max_gas: u64,
                consensus_max_gas: i64,
            }
            |e| {
                format!("semantic config validation failed for option `max_gas` for chain '{}', reason: `max_gas` = {} is greater than the consensus parameter `max_gas` = {}",
                    e.chain_id, e.configured_max_gas, e.consensus_max_gas)
            },

        ConfigValidationTrustingPeriodSmallerThanZero
            {
                chain_id: ChainId,
                trusting_period: Duration,
            }
            |e| {
                format!("semantic config validation failed for option `trusting_period` of chain '{}', reason: trusting period ({}) must be greater than zero",
                    e.chain_id, format_duration(e.trusting_period))
            },

        ConfigValidationTrustingPeriodGreaterThanUnbondingPeriod
            {
                chain_id: ChainId,
                trusting_period: Duration,
                unbonding_period: Duration,
            }
            |e| {
                format!("semantic config validation failed for option `trusting_period` of chain '{}', reason: trusting period ({}) must be smaller than the unbonding period ({})",
                    e.chain_id, format_duration(e.trusting_period), format_duration(e.unbonding_period))
            },

        ConfigValidationDefaultGasTooHigh
            {
                chain_id: ChainId,
                default_gas: u64,
                max_gas: u64,
            }
            |e| {
                format!("semantic config validation failed for option `default_gas` of chain '{}', reason: default gas ({}) must be smaller than the max gas ({})",
                    e.chain_id, e.default_gas, e.max_gas)
            },

        ConfigValidationGasMultiplierLow
            {
                chain_id: ChainId,
                gas_multiplier: f64,
            }
            |e| {
                format!("semantic config validation failed for option `gas_multiplier` of chain '{}', reason: gas multiplier ({}) is smaller than `1.1`, which could trigger gas fee errors in production", e.chain_id, e.gas_multiplier)
            },

        SdkModuleVersion
            {
                chain_id: ChainId,
                address: String,
                cause: String
            }
            |e| {
                format!("Hermes health check failed while verifying the application compatibility for chain {0}:{1}; caused by: {2}",
                    e.chain_id, e.address, e.cause)
            },

        UnknownAccountType
            {
                type_url: String
            }
            |e| {
                format!("failed to deserialize account of an unknown protobuf type: {0}", e.type_url)
            },

        EmptyBaseAccount
            |_| { "empty BaseAccount within EthAccount" },

        EmptyQueryAccount
            { address: String }
            |e| { format!("Query/Account RPC returned an empty account for address: {}", e.address) },

        NoHistoricalEntries
            { chain_id: ChainId }
            |e| {
                format_args!(
                    "staking module for chain '{}' does not maintain any historical entries \
                    (`historical_entries` staking params is set to 0)",
                    e.chain_id
                )
            },

        GasPriceTooLow
            { chain_id: ChainId }
            |e| { format!("Hermes gas price is lower than the minimum gas price set by node operator'{}'", e.chain_id) },

        TxIndexingDisabled
            { chain_id: ChainId }
            |e| {
                format_args!(
                    "transaction indexing for chain '{}' is disabled (`node_info.other.tx_index` is off)",
                    e.chain_id
                )
            },

        EmptyDenomTrace
            { hash: String }
            |e| {
                format_args!(
                    "Query/DenomTrace RPC returned an empty denom trace for trace hash: {}", e.hash)
            },

        MessageTooBigForTx
            { len: usize }
            |e| {
                format_args!("message with length {} is too large for a transaction", e.len)
            },

        InvalidKeyType
            { key_type: KeyType }
            |e| {
                format!("Invalid key type {} for the current chain", e.key_type)
            }
    }
}

impl Error {
    pub fn send<T>(_: crossbeam_channel::SendError<T>) -> Error {
        Error::channel_send()
    }

    pub fn is_trusted_state_outside_trusting_period_error(&self) -> bool {
        match self.detail() {
            ErrorDetail::LightClientVerification(e) => matches!(
                e.source,
                LightClientErrorDetail::TrustedStateOutsideTrustingPeriod(_)
            ),
            _ => false,
        }
    }
}

impl GrpcStatusSubdetail {
    /// Check whether this gRPC error matches
    /// - message: verification failed: ... failed packet acknowledgement verification for client: client state height < proof height ...
    pub fn is_client_state_height_too_low(&self) -> bool {
        // Gaia v6.0.1 (SDK 0.44.5) returns code`InvalidArgument`, whereas gaia v6.0.4
        // (SDK 0.44.6, and potentially others) returns code `Unknown`.
        // Workaround by matching strictly on the status message.
        // if self.status.code() != tonic::Code::InvalidArgument
        //     return false;
        // }

        let msg = self.status.message();
        msg.contains("verification failed") && msg.contains("client state height < proof height")
    }

    /// Check whether this gRPC error message contains the string "account sequence mismatch".
    ///
    /// ## Note
    /// This predicate is tested and validated against errors
    /// that appear at the `estimate_gas` step. The error
    /// predicate to be used at the `broadcast_tx_sync` step
    /// is different & relies on parsing the Response error code.
    ///
    /// It is currently expected that, in the case of a match, the error message is of form:
    /// "account sequence mismatch, expected E, got G: incorrect account sequence",
    /// where E > G.
    /// The case where E < G is considered recoverable and should have been previously handled
    /// (see `is_account_sequence_mismatch_that_can_be_ignored` for which the error is ignored and
    /// simulation uses default gas).
    /// However, if in future cosmos-sdk releases the gRPC error message changes such that
    /// it still starts with "account sequence mismatch" but the rest doesn't match the remainder of
    /// the pattern (", expected E, got G: incorrect account sequence"), or
    /// there are hermes code changes such that the E < G case is not previously caught anymore,
    /// then this predicate will catch all "account sequence mismatch" errors
    pub fn is_account_sequence_mismatch_that_requires_refresh(&self) -> bool {
        self.status.message().contains("account sequence mismatch")
    }

    /// Check whether this gRPC error message contains the string "packet sequence out of order".
    ///
    /// ## Note
    /// This error may happen even when packets are submitted in order when the `simulate_tx`
    /// gRPC endpoint is allowed to be called after a block is created and before
    /// Tendermint/mempool finishes `recheck_tx`, similary to the issue described in
    /// <https://github.com/informalsystems/hermes/issues/2249>.
    ///
    /// See <https://github.com/informalsystems/hermes/issues/2670> for more info.
    pub fn is_out_of_order_packet_sequence_error(&self) -> bool {
        self.status
            .message()
            .contains("packet sequence is out of order")
    }

    /// Check whether this gRPC error matches:
    /// "account sequence mismatch, expected E, got G",
    /// where E < G.
    /// It is currently expected that, in the case of a match, the error message is of form:
    /// "account sequence mismatch, expected E, got G: incorrect account sequence"
    ///
    /// # Note:
    /// This predicate is tested and validated against errors
    /// that appear during the `estimate_gas` step.
    /// If it evaluates to true then the error is ignored and the transaction that caused this
    /// simulation error is still sent to mempool with `broadcast_tx_sync` allowing for potential
    /// recovery after mempool's `recheckTxs` step.
    /// More details in <https://github.com/informalsystems/hermes/issues/2249>
    pub fn is_account_sequence_mismatch_that_can_be_ignored(&self) -> bool {
        match parse_sequences_in_mismatch_error_message(self.status.message()) {
            None => false,
            Some((expected, got)) => expected < got,
        }
    }
}

/// Assumes that the cosmos-sdk account sequence mismatch error message, that may be seen
/// during simulating or broadcasting a transaction, includes the following pattern:
/// "account sequence mismatch, expected E, got G".
/// If a match is found it extracts and returns (E, G).
fn parse_sequences_in_mismatch_error_message(message: &str) -> Option<(u64, u64)> {
    let re =
        Regex::new(r#"account sequence mismatch, expected (?P<expected>\d+), got (?P<got>\d+)"#)
            .unwrap();
    match re.captures(message) {
        None => None,
        Some(captures) => match (captures["expected"].parse(), captures["got"].parse()) {
            (Ok(e), Ok(g)) => Some((e, g)),
            _ => None,
        },
    }
}

pub const QUERY_PROOF_EXPECT_MSG: &str =
    "Internal error. Requested proof with query but no proof was returned.";

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

    #[test]
    fn test_parse_sequences_in_mismatch_error_message() {
        struct Test<'a> {
            name: &'a str,
            message: &'a str,
            result: Option<(u64, u64)>,
        }
        let tests: Vec<Test<'_>> = vec![
            Test {
                name: "good mismatch error, expected < got",
                message:
                    "account sequence mismatch, expected 100, got 200: incorrect account sequence",
                result: Some((100, 200)),
            },
            Test {
                name: "good mismatch error, expected > got",
                message:
                    "account sequence mismatch, expected 200, got 100: incorrect account sequence",
                result: Some((200, 100)),
            },
            Test {
                name: "good changed mismatch error, expected < got",
                message: "account sequence mismatch, expected 100, got 200: this part has changed",
                result: Some((100, 200)),
            },
            Test {
                name: "good changed mismatch error, expected > got",
                message:
                    "account sequence mismatch, expected 200, got 100 --> this part has changed",
                result: Some((200, 100)),
            },
            Test {
                name: "good changed mismatch error, expected > got",
                message:
                    "codespace sdk code 32: incorrect account sequence: account sequence mismatch, expected 200, got 100",
                result: Some((200, 100)),
            },
            Test {
                name: "bad mismatch error, bad expected",
                message:
                    "account sequence mismatch, expected 2a5, got 100: incorrect account sequence",
                result: None,
            },
            Test {
                name: "bad mismatch error, bad got",
                message:
                    "account sequence mismatch, expected 25, got -29: incorrect account sequence",
                result: None,
            },
            Test {
                name: "not a mismatch error",
                message: "some other error message",
                result: None,
            },
        ];

        for test in tests {
            assert_eq!(
                test.result,
                parse_sequences_in_mismatch_error_message(test.message),
                "{}",
                test.name
            )
        }
    }
}