Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d6466a0
implemented sealevel delivered query
mtsitrin Dec 22, 2025
b717c50
Refactor delivery tracking in relayer: replace SealevelDb with Delive…
mtsitrin Dec 22, 2025
cc00152
logs
mtsitrin Dec 22, 2025
6921394
Enhance documentation for message ID handling in delivery API. Clarif…
mtsitrin Dec 22, 2025
0df6976
compilation fix
mtsitrin Dec 22, 2025
a4e37af
base58 for solana tx
mtsitrin Dec 22, 2025
78aa453
added hash->msg_id query
mtsitrin Dec 22, 2025
ddd5760
refactor the reverse lookup
mtsitrin Dec 22, 2025
c1f4c3b
compilation fixes
mtsitrin Dec 22, 2025
6542fac
compilation fix
mtsitrin Dec 22, 2025
be8a0bc
cleanbup
mtsitrin Dec 23, 2025
cbe5b93
compilation fix
mtsitrin Dec 23, 2025
5009807
fixed by_tx query
mtsitrin Dec 23, 2025
04cceb7
compilation fix
mtsitrin Dec 23, 2025
458a221
logs cleanup
mtsitrin Dec 23, 2025
f9f15d0
enabled advanced_log_meta
mtsitrin Dec 23, 2025
73b26b9
cleanup
mtsitrin Dec 23, 2025
243f212
simplified by_tx query
mtsitrin Dec 23, 2025
9b1e7f5
linter
mtsitrin Dec 23, 2025
8ce3210
renamed dispatched API for retrieving Hyperlane message ID by transac…
mtsitrin Dec 24, 2025
9e4943c
Merge remote-tracking branch 'origin/main-dym' into delivered_endpoint
mtsitrin Dec 24, 2025
7ad7446
Added CORS support to the reprocess_message route in the server opera…
mtsitrin Jan 14, 2026
ff6a980
claude: merge cors_fix into main-dym
danwt Jan 28, 2026
d992989
claude: address code review feedback
danwt Jan 28, 2026
8af3e37
claude: address second round of review feedback
danwt Jan 28, 2026
efc0bbb
claude: address third round of review feedback
danwt Jan 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rust/main/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust/main/agents/relayer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ version.workspace = true
[dependencies]
async-trait.workspace = true
axum.workspace = true
bs58.workspace = true
chrono.workspace = true
config.workspace = true
console-subscriber.workspace = true
Expand Down
4 changes: 4 additions & 0 deletions rust/main/agents/relayer/src/msg/db_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,10 @@ pub mod test {
fn store_payload_uuids_by_message_id(&self, message_id: &H256, payload_uuids: Vec<UniqueIdentifier>) -> DbResult<()>;

fn retrieve_payload_uuids_by_message_id(&self, message_id: &H256) -> DbResult<Option<Vec<UniqueIdentifier>>>;

fn store_message_id_by_dispatch_tx(&self, dispatch_tx_id: &hyperlane_core::H512, message_id: &H256) -> DbResult<()>;

fn retrieve_message_id_by_dispatch_tx(&self, dispatch_tx_id: &hyperlane_core::H512) -> DbResult<Option<H256>>;
}
}

Expand Down
27 changes: 26 additions & 1 deletion rust/main/agents/relayer/src/msg/pending_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use hyperlane_base::{
};
use hyperlane_core::{
gas_used_by_operation, BatchItem, ChainCommunicationError, ChainResult, ConfirmReason,
FixedPointNumber, HyperlaneChain, HyperlaneDomain, HyperlaneMessage, Mailbox,
DeliveryDb, FixedPointNumber, HyperlaneChain, HyperlaneDomain, HyperlaneMessage, Mailbox,
MessageSubmissionData, PendingOperation, PendingOperationResult, PendingOperationStatus,
ReprepareReason, TryBatchAs, TxCostEstimate, TxOutcome, H256, U256,
};
Expand Down Expand Up @@ -68,6 +68,8 @@ pub struct MessageContext {
pub destination_mailbox: Arc<dyn Mailbox>,
/// Origin chain database to verify gas payments.
pub origin_db: Arc<dyn HyperlaneDb>,
/// Destination chain database for storing delivery tx hashes.
pub destination_db: Arc<dyn DeliveryDb>,
/// Cache to store commonly used data calls.
pub cache: OptionalCache<MeteredCache<LocalCache>>,
/// Used to construct the ISM metadata needed to verify a message from the
Expand Down Expand Up @@ -918,6 +920,27 @@ impl PendingMessage {
.store_processed_by_nonce(&self.message.nonce, &true)?;
self.ctx.metrics.update_nonce(&self.message);
self.ctx.metrics.messages_processed.inc();

// Store delivery tx hash for all destinations
if let Some(outcome) = &self.submission_outcome {
let message_id = self.message.id();

// Best-effort: store delivery tx hash for /delivered endpoint
if let Err(e) = self
.ctx
.destination_db
.store_delivery_tx(&message_id, &outcome.transaction_id)
{
debug!(
message_id = ?message_id,
tx_id = ?outcome.transaction_id,
destination = ?self.message.destination,
error = %e,
"failed to store delivery tx hash"
);
}
}

Ok(())
}

Expand Down Expand Up @@ -1248,6 +1271,8 @@ mod test {
fn retrieve_highest_seen_message_nonce_number(&self) -> DbResult<Option<u32>>;
fn store_payload_uuids_by_message_id(&self, message_id: &H256, payload_uuids: Vec<UniqueIdentifier>) -> DbResult<()>;
fn retrieve_payload_uuids_by_message_id(&self, message_id: &H256) -> DbResult<Option<Vec<UniqueIdentifier>>>;
fn store_message_id_by_dispatch_tx(&self, dispatch_tx_id: &hyperlane_core::H512, message_id: &H256) -> DbResult<()>;
fn retrieve_message_id_by_dispatch_tx(&self, dispatch_tx_id: &hyperlane_core::H512) -> DbResult<Option<H256>>;
}
}

Expand Down
9 changes: 7 additions & 2 deletions rust/main/agents/relayer/src/relayer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ use hyperlane_base::{
};
use hyperlane_core::{
rpc_clients::call_and_retry_n_times, ChainCommunicationError, ChainResult, ContractSyncCursor,
HyperlaneDomain, HyperlaneMessage, InterchainGasPayment, MerkleTreeInsertion, QueueOperation,
Signature, H512, U256,
DeliveryDb, HyperlaneDomain, HyperlaneMessage, InterchainGasPayment, MerkleTreeInsertion,
QueueOperation, Signature, H512, U256,
};
use hyperlane_cosmos::native::CosmosNativeMailbox;
use lander::DispatcherMetrics;
Expand Down Expand Up @@ -256,6 +256,10 @@ impl BaseAgent for Relayer {
origin_chain_setup.ignore_reorg_reports,
);

// Create destination_db for storing delivery tx hashes
let destination_db: Arc<dyn DeliveryDb> =
Arc::new(destination.database.clone());

msg_ctxs.insert(
ContextKey {
origin: origin_domain.clone(),
Expand All @@ -264,6 +268,7 @@ impl BaseAgent for Relayer {
Arc::new(MessageContext {
destination_mailbox: destination_mailbox.clone(),
origin_db: Arc::new(db.clone()),
destination_db,
cache: cache.clone(),
metadata_builder: Arc::new(metadata_builder),
origin_gas_payment_enforcer,
Expand Down
200 changes: 200 additions & 0 deletions rust/main/agents/relayer/src/server/delivered/dispatched.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
use axum::{
extract::{Query, State},
http::StatusCode,
};
use serde::{Deserialize, Serialize};
use tracing::{debug, error};

use hyperlane_base::{
db::HyperlaneDb,
server::utils::{
ServerErrorBody, ServerErrorResponse, ServerResult, ServerSuccessResponse,
},
};
use hyperlane_core::{HyperlaneDomainProtocol, H512};

use bs58;

use crate::server::delivered::ServerState;

/// Solana transaction signatures are 64 bytes
const SOLANA_SIGNATURE_BYTES: usize = 64;

#[derive(Clone, Debug, Deserialize)]
pub struct QueryParams {
/// The transaction hash (base58 for Sealevel, hex for others)
pub tx_hash: String,
/// The domain ID (where the transaction hash is from)
pub domain_id: u32,
}

#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct MessageIdResponse {
/// The Hyperlane message ID
pub message_id: String,
/// The destination domain ID
pub destination_domain_id: u32,
}

/// Retrieve the Hyperlane message ID for a given transaction hash (reverse lookup)
pub async fn handler(
State(state): State<ServerState>,
Query(query_params): Query<QueryParams>,
) -> ServerResult<ServerSuccessResponse<MessageIdResponse>> {
let tx_hash_str = &query_params.tx_hash;
let domain_id = query_params.domain_id;

let db = match state.dbs.get(&domain_id) {
Some(db) => db,
None => {
debug!(
%tx_hash_str,
%domain_id,
available_domains = ?state.dbs.keys().collect::<Vec<_>>(),
"no database found for origin domain"
);
return Err(ServerErrorResponse::new(
StatusCode::NOT_FOUND,
ServerErrorBody {
message: format!(
"No database found for origin domain: {}. Available domains: {:?}",
domain_id,
state.dbs.keys().collect::<Vec<_>>()
),
},
));
}
};

let domain = db.domain();
let is_sealevel = domain.domain_protocol() == HyperlaneDomainProtocol::Sealevel;

let tx_hash_h512: H512 = if is_sealevel {
debug!(%tx_hash_str, %domain_id, "parsing tx_hash as base58 (Sealevel)");
match bs58::decode(tx_hash_str).into_vec() {
Ok(bytes) => {
if bytes.len() != SOLANA_SIGNATURE_BYTES {
debug!(
%tx_hash_str,
%domain_id,
bytes_len = %bytes.len(),
"invalid base58 tx_hash length"
);
return Err(ServerErrorResponse::new(
StatusCode::BAD_REQUEST,
ServerErrorBody {
message: format!(
"Invalid base58 tx_hash length: expected {} bytes, got {}",
SOLANA_SIGNATURE_BYTES,
bytes.len()
),
},
));
}
H512::from_slice(&bytes)
}
Err(e) => {
debug!(%tx_hash_str, %domain_id, error = %e, "failed to parse base58 tx_hash");
return Err(ServerErrorResponse::new(
StatusCode::BAD_REQUEST,
ServerErrorBody {
message: format!("Invalid base58 tx_hash format: {}", e),
},
));
}
}
} else {
match tx_hash_str.parse() {
Ok(hash) => hash,
Err(e) => {
debug!(%tx_hash_str, %domain_id, error = %e, "failed to parse hex tx_hash");
return Err(ServerErrorResponse::new(
StatusCode::BAD_REQUEST,
ServerErrorBody {
message: format!(
"Invalid hex tx_hash format: {}. Expected 128 hex characters (64 bytes), with or without 0x prefix",
e
),
},
));
}
}
};

let message_id = match db.retrieve_message_id_by_dispatch_tx(&tx_hash_h512) {
Ok(Some(message_id)) => {
debug!(%tx_hash_str, %domain_id, message_id = ?message_id, "found message_id");
message_id
}
Ok(None) => {
debug!(%tx_hash_str, %domain_id, "no message_id found");
return Err(ServerErrorResponse::new(
StatusCode::NOT_FOUND,
ServerErrorBody {
message: format!(
"No message found for tx_hash: {} on origin domain: {}",
tx_hash_str, domain_id
),
},
));
}
Err(e) => {
error!(%tx_hash_str, %domain_id, error = %e, "database error retrieving message_id");
return Err(ServerErrorResponse::new(
StatusCode::INTERNAL_SERVER_ERROR,
ServerErrorBody {
message: format!("Database error: {}", e),
},
));
}
};

let message = match db.retrieve_message_by_id(&message_id) {
Ok(Some(message)) => {
debug!(
%tx_hash_str,
%domain_id,
message_id = ?message_id,
destination_domain_id = %message.destination,
"retrieved message"
);
message
}
Ok(None) => {
error!(
%tx_hash_str,
%domain_id,
message_id = ?message_id,
"message_id found but full message missing"
);
return Err(ServerErrorResponse::new(
StatusCode::INTERNAL_SERVER_ERROR,
ServerErrorBody {
message: "Message ID found but message data missing from database".to_string(),
},
));
}
Err(e) => {
error!(
%tx_hash_str,
%domain_id,
message_id = ?message_id,
error = %e,
"database error retrieving message"
);
return Err(ServerErrorResponse::new(
StatusCode::INTERNAL_SERVER_ERROR,
ServerErrorBody {
message: format!("Database error: {}", e),
},
));
}
};

let response = MessageIdResponse {
message_id: format!("{:x}", message_id),
destination_domain_id: message.destination,
};

Ok(ServerSuccessResponse::new(response))
}
Loading
Loading