Repository: hyperlane-xyz/hyperlane-monorepo
Pull request #5644: feat: sort messages based on how many retries
Author: login: kamiyaa
Created: 2025-03-11T03:04:59Z | Merged: 2025-03-18T16:49:51Z

### Description

- sort pending operations based on how many retries its had, from least retried first

### Backward compatibility

No, order of operations returned from list_operations endpoint are different

### Testing

 - added another unittest to test ordering is correct


Diff:
diff --git a/rust/main/agents/relayer/src/msg/op_queue.rs b/rust/main/agents/relayer/src/msg/op_queue.rs
index 8e2322ed9a6..7f382f6dc15 100644
--- a/rust/main/agents/relayer/src/msg/op_queue.rs
+++ b/rust/main/agents/relayer/src/msg/op_queue.rs
@@ -185,6 +185,7 @@ pub mod test {
         recipient_address: H256,
         seconds_to_next_attempt: u64,
         destination_domain: HyperlaneDomain,
+        retry_count: u32,
     }
 
     impl MockPendingOperation {
@@ -197,6 +198,7 @@ pub mod test {
                 sender_address: H256::random(),
                 recipient_address: H256::random(),
                 origin_domain_id: 0,
+                retry_count: 0,
             }
         }
 
@@ -208,6 +210,7 @@ pub mod test {
                 origin_domain_id: message.origin,
                 destination_domain_id: message.destination,
                 seconds_to_next_attempt: 0,
+                retry_count: 0,
                 destination_domain: HyperlaneDomain::Unknown {
                     domain_id: message.destination,
                     domain_name: "test".to_string(),
@@ -249,6 +252,11 @@ pub mod test {
                 ..self
             }
         }
+
+        pub fn with_retry_count(mut self, retry_count: u32) -> Self {
+            self.set_retries(retry_count);
+            self
+        }
     }
 
     impl TryBatchAs<HyperlaneMessage> for MockPendingOperation {}
@@ -353,8 +361,11 @@ pub mod test {
             todo!()
         }
 
-        fn set_retries(&mut self, _retries: u32) {
-            todo!()
+        fn set_retries(&mut self, retries: u32) {
+            self.retry_count = retries;
+        }
+        fn get_retries(&self) -> u32 {
+            self.retry_count
         }
     }
 
diff --git a/rust/main/agents/relayer/src/msg/pending_message.rs b/rust/main/agents/relayer/src/msg/pending_message.rs
index 48b84d73fd6..758da83ae89 100644
--- a/rust/main/agents/relayer/src/msg/pending_message.rs
+++ b/rust/main/agents/relayer/src/msg/pending_message.rs
@@ -516,6 +516,10 @@ impl PendingOperation for PendingMessage {
         self.set_retries(retries);
     }
 
+    fn get_retries(&self) -> u32 {
+        self.num_retries
+    }
+
     fn try_get_mailbox(&self) -> Option<Arc<dyn Mailbox>> {
         Some(self.ctx.destination_mailbox.clone())
     }
diff --git a/rust/main/agents/relayer/src/server/list_messages.rs b/rust/main/agents/relayer/src/server/list_messages.rs
index e21f39a5dfb..662bd1769cf 100644
--- a/rust/main/agents/relayer/src/server/list_messages.rs
+++ b/rust/main/agents/relayer/src/server/list_messages.rs
@@ -5,7 +5,6 @@ use axum::{
 use derive_new::new;
 use hyperlane_core::{QueueOperation, H256};
 use serde::{Deserialize, Serialize};
-use serde_json::Value;
 use std::collections::HashMap;
 
 use crate::msg::op_queue::OperationPriorityQueue;
@@ -55,13 +54,30 @@ impl<'a> OperationWithId<'a> {
 }
 
 pub async fn format_queue(queue: OperationPriorityQueue) -> String {
-    let res: Result<Vec<Value>, _> = queue
+    let mut sorted_operations: Vec<_> = queue
         .lock()
         .await
         .iter()
-        .map(|reverse| serde_json::to_value(OperationWithId::new(&reverse.0)))
+        .map(|reverse| {
+            (
+                reverse.0.get_retries(),
+                serde_json::to_value(OperationWithId::new(&reverse.0)),
+            )
+        })
         .collect();
-    match res.and_then(|v| serde_json::to_string_pretty(&v)) {
+    sorted_operations.sort_by(|a, b| a.0.cmp(&b.0));
+
+    let mut res = Vec::with_capacity(sorted_operations.len());
+    for (_, op_json_res) in sorted_operations {
+        match op_json_res {
+            Ok(op_json) => res.push(op_json),
+            Err(err) => {
+                return format!("Error formatting queue: {}", err);
+            }
+        }
+    }
+
+    match serde_json::to_string_pretty(&res) {
         Ok(s) => s,
         Err(e) => format!("Error formatting queue: {}", e),
     }
@@ -81,18 +97,26 @@ impl ListOperationsApi {
 
 #[cfg(test)]
 mod tests {
+    use axum::http::StatusCode;
+    use std::{cmp::Reverse, net::SocketAddr, sync::Arc};
+    use tokio::sync::{self, Mutex};
+
+    use hyperlane_core::KnownHyperlaneDomain;
+
     use crate::msg::op_queue::{
         test::{dummy_metrics_and_label, MockPendingOperation},
         OpQueue,
     };
 
     use super::*;
-    use axum::http::StatusCode;
-    use hyperlane_core::KnownHyperlaneDomain;
-    use std::{cmp::Reverse, net::SocketAddr, sync::Arc};
-    use tokio::sync::{self, Mutex};
 
     const DUMMY_DOMAIN: KnownHyperlaneDomain = KnownHyperlaneDomain::Arbitrum;
+    const MESSAGE_ID_1: &str = "0x1acbee9798118b11ebef0d94b0a2936eafd58e3bfab91b05da875825c4a1c39b";
+    const MESSAGE_ID_2: &str = "0x51e7be221ce90a49dee46ca0d0270c48d338a7b9d85c2a89d83fac0816571914";
+    const SENDER_ADDRESS_1: &str =
+        "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08";
+    const RECIPIENT_ADDRESS_1: &str =
+        "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08";
 
     fn setup_test_server() -> (SocketAddr, OperationPriorityQueue) {
         let (metrics, queue_metrics_label) = dummy_metrics_and_label();
@@ -119,61 +143,136 @@ mod tests {
         (addr, op_queue.queue.clone())
     }
 
+    fn generate_dummy_operation_1(retry_count: u32) -> QueueOperation {
+        Box::new(
+            MockPendingOperation::new(1, DUMMY_DOMAIN.into())
+                .with_id(MESSAGE_ID_1)
+                .with_sender_address(SENDER_ADDRESS_1)
+                .with_recipient_address(RECIPIENT_ADDRESS_1)
+                .with_retry_count(retry_count),
+        ) as QueueOperation
+    }
+
+    fn generate_dummy_operation_2(retry_count: u32) -> QueueOperation {
+        Box::new(
+            MockPendingOperation::new(2, DUMMY_DOMAIN.into())
+                .with_id(MESSAGE_ID_2)
+                .with_sender_address(SENDER_ADDRESS_1)
+                .with_recipient_address(RECIPIENT_ADDRESS_1)
+                .with_retry_count(retry_count),
+        ) as QueueOperation
+    }
+
     #[tokio::test]
     async fn test_message_id_retry() {
         let (addr, op_queue) = setup_test_server();
-        let id_1 = "0x1acbee9798118b11ebef0d94b0a2936eafd58e3bfab91b05da875825c4a1c39b";
-        let id_2 = "0x51e7be221ce90a49dee46ca0d0270c48d338a7b9d85c2a89d83fac0816571914";
-        let sender_address = "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08";
-        let recipient_address =
-            "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08";
-        let dummy_operation_1 = Box::new(
-            MockPendingOperation::new(1, DUMMY_DOMAIN.into())
-                .with_id(id_1)
-                .with_sender_address(sender_address)
-                .with_recipient_address(recipient_address),
-        ) as QueueOperation;
-        let dummy_operation_2 = Box::new(
-            MockPendingOperation::new(2, DUMMY_DOMAIN.into())
-                .with_id(id_2)
-                .with_sender_address(sender_address)
-                .with_recipient_address(recipient_address),
-        ) as QueueOperation;
+        let retry_count_1 = 1;
+        let retry_count_2 = 2;
+        let dummy_operation_1 = generate_dummy_operation_1(retry_count_1);
+        let dummy_operation_2 = generate_dummy_operation_2(retry_count_2);
 
         // The reason there already is an id inside `operation` here is because it's a field on `MockPendingOperation` - that field is
         // missing on `PendingMessage` because it's derived, hence the need to hence the need to have it explicitly serialized alongside the operation.
-        let expected_response = r#"[
-  {
+        let expected_response = format!(
+            r#"[
+  {{
     "id": "0x1acbee9798118b11ebef0d94b0a2936eafd58e3bfab91b05da875825c4a1c39b",
-    "operation": {
-      "destination_domain": {
+    "operation": {{
+      "destination_domain": {{
         "Known": "Arbitrum"
-      },
+      }},
       "destination_domain_id": 42161,
       "id": "0x1acbee9798118b11ebef0d94b0a2936eafd58e3bfab91b05da875825c4a1c39b",
       "origin_domain_id": 0,
       "recipient_address": "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08",
+      "retry_count": {retry_count_1},
       "seconds_to_next_attempt": 1,
       "sender_address": "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08",
       "type": "MockPendingOperation"
-    }
-  },
-  {
+    }}
+  }},
+  {{
     "id": "0x51e7be221ce90a49dee46ca0d0270c48d338a7b9d85c2a89d83fac0816571914",
-    "operation": {
-      "destination_domain": {
+    "operation": {{
+      "destination_domain": {{
         "Known": "Arbitrum"
-      },
+      }},
       "destination_domain_id": 42161,
       "id": "0x51e7be221ce90a49dee46ca0d0270c48d338a7b9d85c2a89d83fac0816571914",
       "origin_domain_id": 0,
       "recipient_address": "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08",
+      "retry_count": {retry_count_2},
       "seconds_to_next_attempt": 2,
       "sender_address": "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08",
       "type": "MockPendingOperation"
+    }}
+  }}
+]"#
+        );
+        op_queue.lock().await.push(Reverse(dummy_operation_1));
+        op_queue.lock().await.push(Reverse(dummy_operation_2));
+
+        // Send a GET request to the server
+        let response = reqwest::get(format!(
+            "http://{}{}?destination_domain={}",
+            addr, LIST_OPERATIONS_API_BASE, DUMMY_DOMAIN as u32
+        ))
+        .await
+        .unwrap();
+
+        // Check that the response status code is OK
+        assert_eq!(response.status(), StatusCode::OK);
+
+        let response_text = response.text().await.unwrap();
+        assert_eq!(response_text, expected_response);
     }
-  }
-]"#;
+
+    #[tokio::test]
+    async fn test_sorted_by_retry_count() {
+        let (addr, op_queue) = setup_test_server();
+        let retry_count_1 = 4;
+        let retry_count_2 = 1;
+        let dummy_operation_1 = generate_dummy_operation_1(retry_count_1);
+        let dummy_operation_2 = generate_dummy_operation_2(retry_count_2);
+
+        // The reason there already is an id inside `operation` here is because it's a field on `MockPendingOperation` - that field is
+        // missing on `PendingMessage` because it's derived, hence the need to hence the need to have it explicitly serialized alongside the operation.
+        let expected_response = format!(
+            r#"[
+  {{
+    "id": "0x51e7be221ce90a49dee46ca0d0270c48d338a7b9d85c2a89d83fac0816571914",
+    "operation": {{
+      "destination_domain": {{
+        "Known": "Arbitrum"
+      }},
+      "destination_domain_id": 42161,
+      "id": "0x51e7be221ce90a49dee46ca0d0270c48d338a7b9d85c2a89d83fac0816571914",
+      "origin_domain_id": 0,
+      "recipient_address": "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08",
+      "retry_count": {retry_count_2},
+      "seconds_to_next_attempt": 2,
+      "sender_address": "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08",
+      "type": "MockPendingOperation"
+    }}
+  }},
+  {{
+    "id": "0x1acbee9798118b11ebef0d94b0a2936eafd58e3bfab91b05da875825c4a1c39b",
+    "operation": {{
+      "destination_domain": {{
+        "Known": "Arbitrum"
+      }},
+      "destination_domain_id": 42161,
+      "id": "0x1acbee9798118b11ebef0d94b0a2936eafd58e3bfab91b05da875825c4a1c39b",
+      "origin_domain_id": 0,
+      "recipient_address": "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08",
+      "retry_count": {retry_count_1},
+      "seconds_to_next_attempt": 1,
+      "sender_address": "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08",
+      "type": "MockPendingOperation"
+    }}
+  }}
+]"#
+        );
         op_queue.lock().await.push(Reverse(dummy_operation_1));
         op_queue.lock().await.push(Reverse(dummy_operation_2));
 
diff --git a/rust/main/hyperlane-core/src/traits/pending_operation.rs b/rust/main/hyperlane-core/src/traits/pending_operation.rs
index 9cc2f85a7b4..d17f5575280 100644
--- a/rust/main/hyperlane-core/src/traits/pending_operation.rs
+++ b/rust/main/hyperlane-core/src/traits/pending_operation.rs
@@ -159,6 +159,9 @@ pub trait PendingOperation: Send + Sync + Debug + TryBatchAs<HyperlaneMessage> {
     #[cfg(any(test, feature = "test-utils"))]
     fn set_retries(&mut self, retries: u32);
 
+    /// Get the number of times this operation has been retried.
+    fn get_retries(&self) -> u32;
+
     /// If this operation points to a mailbox contract, return it
     fn try_get_mailbox(&self) -> Option<Arc<dyn Mailbox>> {
         None


Reviews:
user: login: daniel-savu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T16:03:44Z
user: login: kamiyaa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T16:46:37Z
user: login: kamiyaa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T16:46:51Z
user: login: daniel-savu
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T11:28:14Z

Review comments:
diff_hunk: @@ -55,13 +55,31 @@ impl<'a> OperationWithId<'a> {
 }
 
 pub async fn format_queue(queue: OperationPriorityQueue) -> String {
-    let res: Result<Vec<Value>, _> = queue
+    let mut sorted_operations: Vec<_> = queue
         .lock()
         .await
         .iter()
-        .map(|reverse| serde_json::to_value(OperationWithId::new(&reverse.0)))
+        .map(|reverse| {
+            (
+                reverse.0.get_retries(),
+                serde_json::to_value(OperationWithId::new(&reverse.0)),
+            )
+        })
         .collect();
-    match res.and_then(|v| serde_json::to_string_pretty(&v)) {
+    // reverse sort because we want higher retry count in the front
+    sorted_operations.sort_by(|a, b| a.0.cmp(&b.0).reverse());
path: rust/main/agents/relayer/src/server/list_messages.rs
user: login: daniel-savu
body: we want the lowest number of retries first
created_at: 2025-03-11T10:49:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,10 +189,88 @@ mod tests {
       "id": "0x51e7be221ce90a49dee46ca0d0270c48d338a7b9d85c2a89d83fac0816571914",
       "origin_domain_id": 0,
       "recipient_address": "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08",
+      "retry_count": 1,
+      "seconds_to_next_attempt": 2,
+      "sender_address": "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08",
+      "type": "MockPendingOperation"
+    }
+  }
+]"#;
+        op_queue.lock().await.push(Reverse(dummy_operation_1));
+        op_queue.lock().await.push(Reverse(dummy_operation_2));
+
+        // Send a GET request to the server
+        let response = reqwest::get(format!(
+            "http://{}{}?destination_domain={}",
+            addr, LIST_OPERATIONS_API_BASE, DUMMY_DOMAIN as u32
+        ))
+        .await
+        .unwrap();
+
+        // Check that the response status code is OK
+        assert_eq!(response.status(), StatusCode::OK);
+
+        let response_text = response.text().await.unwrap();
+        assert_eq!(response_text, expected_response);
+    }
+
+    #[tokio::test]
+    async fn test_sorted_by_retry_count() {
+        let (addr, op_queue) = setup_test_server();
+        let id_1 = "0x1acbee9798118b11ebef0d94b0a2936eafd58e3bfab91b05da875825c4a1c39b";
+        let id_2 = "0x51e7be221ce90a49dee46ca0d0270c48d338a7b9d85c2a89d83fac0816571914";
+        let sender_address = "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08";
+        let recipient_address =
+            "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08";
+        let dummy_operation_1 = Box::new(
+            MockPendingOperation::new(1, DUMMY_DOMAIN.into())
+                .with_id(id_1)
+                .with_sender_address(sender_address)
+                .with_recipient_address(recipient_address)
+                .with_retry_count(1),
+        ) as QueueOperation;
+        let dummy_operation_2 = Box::new(
+            MockPendingOperation::new(2, DUMMY_DOMAIN.into())
+                .with_id(id_2)
+                .with_sender_address(sender_address)
+                .with_recipient_address(recipient_address)
+                .with_retry_count(4),
+        ) as QueueOperation;
+
+        // The reason there already is an id inside `operation` here is because it's a field on `MockPendingOperation` - that field is
+        // missing on `PendingMessage` because it's derived, hence the need to hence the need to have it explicitly serialized alongside the operation.
+        let expected_response = r#"[
+  {
+    "id": "0x51e7be221ce90a49dee46ca0d0270c48d338a7b9d85c2a89d83fac0816571914",
+    "operation": {
path: rust/main/agents/relayer/src/server/list_messages.rs
user: login: daniel-savu
body: if this is the same data as in the other unit test, it can be made global to the `mod test` or returned from a function to avoid duplication
created_at: 2025-03-11T12:05:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,10 +189,88 @@ mod tests {
       "id": "0x51e7be221ce90a49dee46ca0d0270c48d338a7b9d85c2a89d83fac0816571914",
       "origin_domain_id": 0,
       "recipient_address": "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08",
+      "retry_count": 1,
+      "seconds_to_next_attempt": 2,
+      "sender_address": "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08",
+      "type": "MockPendingOperation"
+    }
+  }
+]"#;
+        op_queue.lock().await.push(Reverse(dummy_operation_1));
+        op_queue.lock().await.push(Reverse(dummy_operation_2));
+
+        // Send a GET request to the server
+        let response = reqwest::get(format!(
+            "http://{}{}?destination_domain={}",
+            addr, LIST_OPERATIONS_API_BASE, DUMMY_DOMAIN as u32
+        ))
+        .await
+        .unwrap();
+
+        // Check that the response status code is OK
+        assert_eq!(response.status(), StatusCode::OK);
+
+        let response_text = response.text().await.unwrap();
+        assert_eq!(response_text, expected_response);
+    }
+
+    #[tokio::test]
+    async fn test_sorted_by_retry_count() {
path: rust/main/agents/relayer/src/server/list_messages.rs
user: login: daniel-savu
body: it took me a while to understand what this function checks that's different from `test_message_id_retry`. The expected response should definitely be moved to a parameterized function to make things more readable
created_at: 2025-03-11T16:03:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,13 +55,31 @@ impl<'a> OperationWithId<'a> {
 }
 
 pub async fn format_queue(queue: OperationPriorityQueue) -> String {
-    let res: Result<Vec<Value>, _> = queue
+    let mut sorted_operations: Vec<_> = queue
         .lock()
         .await
         .iter()
-        .map(|reverse| serde_json::to_value(OperationWithId::new(&reverse.0)))
+        .map(|reverse| {
+            (
+                reverse.0.get_retries(),
+                serde_json::to_value(OperationWithId::new(&reverse.0)),
+            )
+        })
         .collect();
-    match res.and_then(|v| serde_json::to_string_pretty(&v)) {
+    // reverse sort because we want higher retry count in the front
+    sorted_operations.sort_by(|a, b| a.0.cmp(&b.0).reverse());
path: rust/main/agents/relayer/src/server/list_messages.rs
user: login: kamiyaa
body: Fixed
created_at: 2025-03-13T16:46:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,10 +189,88 @@ mod tests {
       "id": "0x51e7be221ce90a49dee46ca0d0270c48d338a7b9d85c2a89d83fac0816571914",
       "origin_domain_id": 0,
       "recipient_address": "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08",
+      "retry_count": 1,
+      "seconds_to_next_attempt": 2,
+      "sender_address": "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08",
+      "type": "MockPendingOperation"
+    }
+  }
+]"#;
+        op_queue.lock().await.push(Reverse(dummy_operation_1));
+        op_queue.lock().await.push(Reverse(dummy_operation_2));
+
+        // Send a GET request to the server
+        let response = reqwest::get(format!(
+            "http://{}{}?destination_domain={}",
+            addr, LIST_OPERATIONS_API_BASE, DUMMY_DOMAIN as u32
+        ))
+        .await
+        .unwrap();
+
+        // Check that the response status code is OK
+        assert_eq!(response.status(), StatusCode::OK);
+
+        let response_text = response.text().await.unwrap();
+        assert_eq!(response_text, expected_response);
+    }
+
+    #[tokio::test]
+    async fn test_sorted_by_retry_count() {
path: rust/main/agents/relayer/src/server/list_messages.rs
user: login: kamiyaa
body: Added helper function to generate test data
created_at: 2025-03-13T16:46:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -189,7 +198,7 @@ mod tests {
       "id": "0x51e7be221ce90a49dee46ca0d0270c48d338a7b9d85c2a89d83fac0816571914",
       "origin_domain_id": 0,
       "recipient_address": "0x586d41b02fb35df0f84ecb2b73e076b40c929ee3e1ceeada9a078aa7b46d3b08",
-      "retry_count": 1,
+      "retry_count": 2,
path: rust/main/agents/relayer/src/server/list_messages.rs
user: login: daniel-savu
body: nit: for both expected operations, inline the `retry_count` value to be a variable that's also used as the parameter when calling `generate_dummy_operation_*`. In the other test as well
created_at: 2025-03-18T11:27:50Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-11T03:05:03Z
body: ###  ⚠️  No Changeset found

Latest commit: d73a8489b1b6ea272878433983d007c4891ed89b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/hyperlane-xyz/hyperlane-monorepo/new/jeff/sort_list?filename=.changeset/wicked-cameras-join.md&value=---%0A%0A---%0A%0Afeat%3A%20sort%20messages%20based%20on%20how%20many%20retries%0A)


author_association: NONE
user: login: codecov[bot]
created_at: 2025-03-11T03:14:20Z
body: ## [Codecov](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/5644?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 78.33%. Comparing base [(`9324334`)](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/commit/93243342bc911785e82c0ec4f4d5aee9b65c5071?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) to head [(`d73a848`)](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/commit/d73a8489b1b6ea272878433983d007c4891ed89b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz).
> Report is 18 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #5644   +/-   ##
=======================================
  Coverage   78.33%   78.33%           
=======================================
  Files         109      109           
  Lines        2165     2165           
  Branches      195      195           
=======================================
  Hits         1696     1696           
  Misses        448      448           
  Partials       21       21           
```

| [Components](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/5644/components?src=pr&el=components&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | Coverage Δ | |
|---|---|---|
| [core](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/5644/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `87.80% <ø> (ø)` | |
| [hooks](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/5644/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `82.78% <ø> (ø)` | |
| [isms](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/5644/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `84.71% <ø> (ø)` | |
| [token](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/5644/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `90.78% <ø> (ø)` | |
| [middlewares](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/5644/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `79.80% <ø> (ø)` | |

</details>
<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE