Merge remote-tracking branch 'origin' into dan/gas-escalator-middleware

pull/3852/head
Daniel Savu 5 months ago
commit db31f2b63d
No known key found for this signature in database
GPG Key ID: 795E587829AF7E08
  1. 7
      .changeset/clean-numbers-know.md
  2. 1
      .eslintrc
  3. 4
      .github/workflows/test-skipped.yml
  4. 13
      .github/workflows/test.yml
  5. 20
      rust/Cargo.lock
  6. 4
      rust/agents/relayer/src/msg/op_queue.rs
  7. 11
      rust/agents/relayer/src/msg/op_submitter.rs
  8. 17
      rust/agents/relayer/src/msg/pending_message.rs
  9. 2
      rust/hyperlane-base/Cargo.toml
  10. 10
      rust/hyperlane-base/src/db/rocks/hyperlane_db.rs
  11. 6
      rust/hyperlane-base/src/lib.rs
  12. 3
      rust/hyperlane-base/src/settings/mod.rs
  13. 8
      rust/hyperlane-base/src/settings/trace/mod.rs
  14. 62
      rust/hyperlane-core/src/traits/pending_operation.rs
  15. 31
      rust/utils/run-locally/src/invariants.rs
  16. 24
      rust/utils/run-locally/src/utils.rs
  17. 10
      solidity/CHANGELOG.md
  18. 5
      solidity/contracts/interfaces/avs/vendored/IDelegationManager.sol
  19. 4
      solidity/package.json
  20. 2
      typescript/ccip-server/CHANGELOG.md
  21. 2
      typescript/ccip-server/package.json
  22. 38
      typescript/cli/CHANGELOG.md
  23. 43
      typescript/cli/ci-advanced-test.sh
  24. 8
      typescript/cli/cli.ts
  25. 19
      typescript/cli/examples/core-config.yaml
  26. 12
      typescript/cli/examples/hooks.yaml
  27. 9
      typescript/cli/package.json
  28. 449
      typescript/cli/src/avs/check.ts
  29. 3
      typescript/cli/src/avs/config.ts
  30. 4
      typescript/cli/src/avs/stakeRegistry.ts
  31. 69
      typescript/cli/src/commands/avs.ts
  32. 99
      typescript/cli/src/commands/config.ts
  33. 155
      typescript/cli/src/commands/core.ts
  34. 97
      typescript/cli/src/commands/deploy.ts
  35. 4
      typescript/cli/src/commands/hook.ts
  36. 4
      typescript/cli/src/commands/ism.ts
  37. 46
      typescript/cli/src/commands/options.ts
  38. 79
      typescript/cli/src/commands/registry.ts
  39. 63
      typescript/cli/src/commands/send.ts
  40. 2
      typescript/cli/src/commands/types.ts
  41. 74
      typescript/cli/src/commands/validator.ts
  42. 290
      typescript/cli/src/commands/warp.ts
  43. 75
      typescript/cli/src/config/agent.ts
  44. 123
      typescript/cli/src/config/chain.ts
  45. 71
      typescript/cli/src/config/core.ts
  46. 358
      typescript/cli/src/config/hooks.ts
  47. 350
      typescript/cli/src/config/ism.ts
  48. 18
      typescript/cli/src/config/utils.ts
  49. 76
      typescript/cli/src/config/warp.ts
  50. 7
      typescript/cli/src/context/context.ts
  51. 463
      typescript/cli/src/deploy/core.ts
  52. 3
      typescript/cli/src/deploy/dry-run.ts
  53. 56
      typescript/cli/src/deploy/utils.ts
  54. 162
      typescript/cli/src/deploy/warp.ts
  55. 8
      typescript/cli/src/logger.ts
  56. 56
      typescript/cli/src/send/message.ts
  57. 23
      typescript/cli/src/send/transfer.ts
  58. 2
      typescript/cli/src/status/message.ts
  59. 2
      typescript/cli/src/submit/submit.ts
  60. 31
      typescript/cli/src/tests/hooks.test.ts
  61. 4
      typescript/cli/src/tests/hooks/safe-parse-fail.yaml
  62. 4
      typescript/cli/src/tests/ism.test.ts
  63. 29
      typescript/cli/src/utils/chains.ts
  64. 17
      typescript/cli/src/utils/cli-options.ts
  65. 8
      typescript/cli/src/utils/files.ts
  66. 55
      typescript/cli/src/utils/input.ts
  67. 4
      typescript/cli/src/utils/keys.ts
  68. 34
      typescript/cli/src/utils/tokens.ts
  69. 20
      typescript/cli/src/validator/address.ts
  70. 117
      typescript/cli/src/validator/preFlightCheck.ts
  71. 69
      typescript/cli/src/validator/utils.ts
  72. 2
      typescript/cli/src/version.ts
  73. 20
      typescript/helloworld/CHANGELOG.md
  74. 6
      typescript/helloworld/package.json
  75. 3
      typescript/helloworld/src/deploy/deploy.ts
  76. 22
      typescript/infra/CHANGELOG.md
  77. 8
      typescript/infra/config/environments/mainnet3/core.ts
  78. 9
      typescript/infra/config/environments/mainnet3/igp.ts
  79. 6
      typescript/infra/config/environments/test/core.ts
  80. 22
      typescript/infra/config/environments/test/igp.ts
  81. 16
      typescript/infra/config/environments/test/multisigIsm.ts
  82. 8
      typescript/infra/config/environments/testnet4/core.ts
  83. 9
      typescript/infra/config/environments/testnet4/igp.ts
  84. 9
      typescript/infra/package.json
  85. 15
      typescript/infra/scripts/check-deploy.ts
  86. 8
      typescript/infra/scripts/deploy.ts
  87. 150
      typescript/infra/scripts/generate-renzo-warp-route-config.ts
  88. 12
      typescript/infra/scripts/helloworld/kathy.ts
  89. 20
      typescript/infra/scripts/send-test-messages.ts
  90. 18
      typescript/infra/src/govern/HyperlaneAppGovernor.ts
  91. 7
      typescript/infra/test/govern.hardhat-test.ts
  92. 19
      typescript/sdk/CHANGELOG.md
  93. 6
      typescript/sdk/package.json
  94. 2
      typescript/sdk/src/aws/s3.ts
  95. 4
      typescript/sdk/src/aws/validator.ts
  96. 10
      typescript/sdk/src/consts/testChains.ts
  97. 9
      typescript/sdk/src/contracts/contracts.ts
  98. 6
      typescript/sdk/src/core/AbstractHyperlaneModule.ts
  99. 24
      typescript/sdk/src/core/CoreDeployer.hardhat-test.ts
  100. 164
      typescript/sdk/src/core/EvmCoreModule.hardhat-test.ts
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,7 +0,0 @@
---
'@hyperlane-xyz/helloworld': minor
'@hyperlane-xyz/infra': minor
'@hyperlane-xyz/cli': minor
---
Upgrade registry to 2.1.1

@ -23,6 +23,7 @@
"no-extra-boolean-cast": ["error"],
"no-ex-assign": ["error"],
"no-constant-condition": ["off"],
"no-return-await": ["error"],
"guard-for-in": ["error"],
"@typescript-eslint/ban-ts-comment": ["off"],
"@typescript-eslint/explicit-module-boundary-types": ["off"],

@ -70,7 +70,7 @@ jobs:
- name: Instant pass
run: echo "e2e job passed"
cli-e2e:
cli-advanced-e2e:
runs-on: ubuntu-latest
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') || github.event_name == 'merge_group'
strategy:
@ -81,7 +81,7 @@ jobs:
- test-type: pi_with_core_chain
steps:
- name: Instant pass
run: echo "cli-e2e job passed"
run: echo "cli-advanced-e2e job passed"
env-test:
runs-on: ubuntu-latest

@ -1,11 +1,12 @@
name: test
on:
# Triggers the workflow on push or pull request against main
# Triggers the workflow on pushes to main branch
push:
branches: [main]
paths-ignore:
- '*.md'
# Triggers on pull requests ignoring md files
pull_request:
branches:
- '*' # run against all branches
@ -187,7 +188,7 @@ jobs:
e2e-matrix:
runs-on: larger-runner
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') || github.event_name == 'merge_group'
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main' || github.base_ref == 'cli-2.0') || github.event_name == 'merge_group'
needs: [yarn-build]
strategy:
matrix:
@ -281,7 +282,7 @@ jobs:
prebuild-cli-e2e:
runs-on: larger-runner
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') || github.event_name == 'merge_group'
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main' || github.base_ref == 'cli-2.0') || github.event_name == 'merge_group'
steps:
- uses: actions/checkout@v4
with:
@ -328,9 +329,9 @@ jobs:
env:
RUST_BACKTRACE: 'full'
cli-e2e:
cli-advanced-e2e:
runs-on: larger-runner
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') || github.event_name == 'merge_group'
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main' || github.base_ref == 'cli-2.0') || github.event_name == 'merge_group'
needs: [yarn-build, prebuild-cli-e2e]
strategy:
matrix:
@ -399,7 +400,7 @@ jobs:
uses: ./.github/actions/checkout-registry
- name: cli e2e tests
run: ./typescript/cli/ci-test.sh ${{ matrix.test-type }}
run: ./typescript/cli/ci-advanced-test.sh ${{ matrix.test-type }}
env-test:
runs-on: ubuntu-latest

20
rust/Cargo.lock generated

@ -1094,13 +1094,13 @@ dependencies = [
[[package]]
name = "clang-sys"
version = "1.7.0"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading 0.8.1",
"libloading 0.8.4",
]
[[package]]
@ -5182,12 +5182,12 @@ dependencies = [
[[package]]
name = "libloading"
version = "0.8.1"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161"
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
"windows-targets 0.52.0",
]
[[package]]
@ -5273,9 +5273,9 @@ dependencies = [
[[package]]
name = "libz-sys"
version = "1.1.14"
version = "1.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "295c17e837573c8c821dbaeb3cceb3d745ad082f7572191409e69cbc1b3fd050"
checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9"
dependencies = [
"cc",
"pkg-config",
@ -6469,9 +6469,9 @@ dependencies = [
[[package]]
name = "prettyplease"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5"
checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7"
dependencies = [
"proc-macro2 1.0.76",
"syn 2.0.48",

@ -166,6 +166,10 @@ mod test {
todo!()
}
fn retrieve_status_from_db(&self) -> Option<PendingOperationStatus> {
todo!()
}
fn get_operation_labels(&self) -> (String, String) {
Default::default()
}

@ -186,9 +186,14 @@ async fn receive_task(
// make sure things are getting wired up correctly; if this works in testing it
// should also be valid in production.
debug_assert_eq!(*op.destination_domain(), domain);
prepare_queue
.push(op, Some(PendingOperationStatus::FirstPrepareAttempt))
.await;
let status = op.retrieve_status_from_db().unwrap_or_else(|| {
trace!(
?op,
"No status found for message, defaulting to FirstPrepareAttempt"
);
PendingOperationStatus::FirstPrepareAttempt
});
prepare_queue.push(op, Some(status)).await;
}
}

@ -127,6 +127,13 @@ impl PendingOperation for PendingMessage {
}
fn set_status(&mut self, status: PendingOperationStatus) {
if let Err(e) = self
.ctx
.origin_db
.store_status_by_message_id(&self.message.id(), &self.status)
{
warn!(message_id = ?self.message.id(), err = %e, status = %self.status, "Persisting `status` failed for message");
}
self.status = status;
}
@ -142,6 +149,16 @@ impl PendingOperation for PendingMessage {
self.ctx.destination_mailbox.domain()
}
fn retrieve_status_from_db(&self) -> Option<PendingOperationStatus> {
match self.ctx.origin_db.retrieve_status_by_message_id(&self.id()) {
Ok(status) => status,
Err(e) => {
warn!(error=?e, "Failed to retrieve status for message");
None
}
}
}
fn app_context(&self) -> Option<String> {
self.app_context.clone()
}

@ -46,7 +46,6 @@ url.workspace = true
warp.workspace = true
ya-gcp.workspace = true
backtrace = { workspace = true, optional = true }
backtrace-oneline = { path = "../utils/backtrace-oneline", optional = true }
@ -58,7 +57,6 @@ hyperlane-sealevel = { path = "../chains/hyperlane-sealevel" }
hyperlane-cosmos = { path = "../chains/hyperlane-cosmos"}
hyperlane-test = { path = "../hyperlane-test" }
# dependency version is determined by etheres
rusoto_core = "*"
rusoto_kms = "*"

@ -7,7 +7,7 @@ use hyperlane_core::{
GasPaymentKey, HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage,
HyperlaneSequenceAwareIndexerStoreReader, HyperlaneWatermarkedLogStore, Indexed,
InterchainGasExpenditure, InterchainGasPayment, InterchainGasPaymentMeta, LogMeta,
MerkleTreeInsertion, H256,
MerkleTreeInsertion, PendingOperationStatus, H256,
};
use super::{
@ -27,6 +27,7 @@ const HIGHEST_SEEN_MESSAGE_NONCE: &str = "highest_seen_message_nonce_";
const GAS_PAYMENT_FOR_MESSAGE_ID: &str = "gas_payment_sequence_for_message_id_v2_";
const GAS_PAYMENT_META_PROCESSED: &str = "gas_payment_meta_processed_v3_";
const GAS_EXPENDITURE_FOR_MESSAGE_ID: &str = "gas_expenditure_for_message_id_v2_";
const STATUS_BY_MESSAGE_ID: &str = "status_by_message_id_";
const PENDING_MESSAGE_RETRY_COUNT_FOR_MESSAGE_ID: &str =
"pending_message_retry_count_for_message_id_";
const MERKLE_TREE_INSERTION: &str = "merkle_tree_insertion_";
@ -501,6 +502,13 @@ make_store_and_retrieve!(pub(self), dispatched_block_number_by_nonce, MESSAGE_DI
make_store_and_retrieve!(pub, processed_by_nonce, NONCE_PROCESSED, u32, bool);
make_store_and_retrieve!(pub(self), processed_by_gas_payment_meta, GAS_PAYMENT_META_PROCESSED, InterchainGasPaymentMeta, bool);
make_store_and_retrieve!(pub(self), interchain_gas_expenditure_data_by_message_id, GAS_EXPENDITURE_FOR_MESSAGE_ID, H256, InterchainGasExpenditureData);
make_store_and_retrieve!(
pub,
status_by_message_id,
STATUS_BY_MESSAGE_ID,
H256,
PendingOperationStatus
);
make_store_and_retrieve!(pub(self), interchain_gas_payment_data_by_gas_payment_key, GAS_PAYMENT_FOR_MESSAGE_ID, GasPaymentKey, InterchainGasPaymentData);
make_store_and_retrieve!(pub(self), gas_payment_by_sequence, GAS_PAYMENT_BY_SEQUENCE, u32, InterchainGasPayment);
make_store_and_retrieve!(pub(self), gas_payment_block_by_sequence, GAS_PAYMENT_BY_SEQUENCE, u32, u64);

@ -12,6 +12,9 @@ pub mod settings;
mod agent;
pub use agent::*;
/// The local database used by agents
pub mod db;
pub mod metrics;
pub use metrics::*;
@ -28,8 +31,5 @@ pub use traits::*;
mod types;
pub use types::*;
/// Hyperlane database utils
pub mod db;
#[cfg(feature = "oneline-eyre")]
pub mod oneline_eyre;

@ -65,9 +65,6 @@
pub use base::*;
pub use chains::*;
pub use checkpoint_syncer::*;
/// Export this so they don't need to import paste.
#[doc(hidden)]
pub use paste;
pub use signers::*;
pub use trace::*;

@ -66,12 +66,12 @@ impl TracingConfig {
if self.level < Level::DependencyTrace {
// Reduce log noise from trusted libraries that we can reasonably assume are working correctly
target_layer = target_layer
.with_target("hyper", Level::Info)
.with_target("hyper::", Level::Info)
.with_target("rusoto_core", Level::Info)
.with_target("rustls", Level::Info)
.with_target("reqwest", Level::Info)
.with_target("runtime", Level::Debug)
.with_target("h2", Level::Info)
.with_target("h2::", Level::Info)
.with_target("tower", Level::Info)
.with_target("tendermint", Level::Info)
.with_target("tokio", Level::Debug)
@ -81,7 +81,9 @@ impl TracingConfig {
if self.level < Level::Trace {
// only show sqlx query logs at trace level
target_layer = target_layer.with_target("sqlx::query", Level::Warn);
target_layer = target_layer
.with_target("sqlx::query", Level::Warn)
.with_target("hyper::", Level::Warn);
}
let fmt_layer: LogOutputLayer<_> = self.fmt.into();
let err_layer = tracing_error::ErrorLayer::default();

@ -1,12 +1,14 @@
use serde::{Deserialize, Serialize};
use std::{
cmp::Ordering,
fmt::{Debug, Display},
io::Write,
time::{Duration, Instant},
};
use crate::{
ChainResult, FixedPointNumber, HyperlaneDomain, HyperlaneMessage, TryBatchAs, TxOutcome, H256,
U256,
ChainResult, Decode, Encode, FixedPointNumber, HyperlaneDomain, HyperlaneMessage,
HyperlaneProtocolError, TryBatchAs, TxOutcome, H256, U256,
};
use async_trait::async_trait;
use num::CheckedDiv;
@ -50,6 +52,9 @@ pub trait PendingOperation: Send + Sync + Debug + TryBatchAs<HyperlaneMessage> {
/// The domain this originates from.
fn origin_domain_id(&self) -> u32;
/// Get the operation status from the local db, if there is one
fn retrieve_status_from_db(&self) -> Option<PendingOperationStatus>;
/// The domain this operation will take place on.
fn destination_domain(&self) -> &HyperlaneDomain;
@ -114,8 +119,10 @@ pub trait PendingOperation: Send + Sync + Debug + TryBatchAs<HyperlaneMessage> {
fn set_retries(&mut self, retries: u32);
}
#[derive(Debug, Display, Clone)]
#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq)]
/// Status of a pending operation
/// WARNING: This enum is serialized to JSON and stored in the database, so to keep backwards compatibility, we shouldn't remove or rename any variants.
/// Adding new variants is fine.
pub enum PendingOperationStatus {
/// The operation is ready to be prepared for the first time, or has just been loaded from storage
FirstPrepareAttempt,
@ -129,8 +136,38 @@ pub enum PendingOperationStatus {
Confirm(ConfirmReason),
}
#[derive(Display, Debug, Clone)]
impl Encode for PendingOperationStatus {
fn write_to<W>(&self, writer: &mut W) -> std::io::Result<usize>
where
W: Write,
{
// Serialize to JSON and write to the writer, to avoid having to implement the encoding manually
let serialized = serde_json::to_vec(self)
.map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "Failed to serialize"))?;
writer.write(&serialized)
}
}
impl Decode for PendingOperationStatus {
fn read_from<R>(reader: &mut R) -> Result<Self, HyperlaneProtocolError>
where
R: std::io::Read,
Self: Sized,
{
// Deserialize from JSON and read from the reader, to avoid having to implement the encoding / decoding manually
serde_json::from_reader(reader).map_err(|err| {
HyperlaneProtocolError::IoError(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to deserialize. Error: {}", err),
))
})
}
}
#[derive(Display, Debug, Clone, Serialize, Deserialize, PartialEq)]
/// Reasons for repreparing an operation
/// WARNING: This enum is serialized to JSON and stored in the database, so to keep backwards compatibility, we shouldn't remove or rename any variants.
/// Adding new variants is fine.
pub enum ReprepareReason {
#[strum(to_string = "Error checking message delivery status")]
/// Error checking message delivery status
@ -167,8 +204,10 @@ pub enum ReprepareReason {
RevertedOrReorged,
}
#[derive(Display, Debug, Clone)]
#[derive(Display, Debug, Clone, Serialize, Deserialize, PartialEq)]
/// Reasons for repreparing an operation
/// WARNING: This enum is serialized to JSON and stored in the database, so to keep backwards compatibility, we shouldn't remove or rename any variants.
/// Adding new variants is fine.
pub enum ConfirmReason {
#[strum(to_string = "Submitted by this relayer")]
/// Operation was submitted by this relayer
@ -274,3 +313,16 @@ pub enum PendingOperationResult {
/// Send this message straight to the confirm queue
Confirm(ConfirmReason),
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_encoding_pending_operation_status() {
let status = PendingOperationStatus::Retry(ReprepareReason::CouldNotFetchMetadata);
let encoded = status.to_vec();
let decoded = PendingOperationStatus::read_from(&mut &encoded[..]).unwrap();
assert_eq!(status, decoded);
}
}

@ -62,12 +62,17 @@ pub fn termination_invariants_met(
.sum::<u32>();
let log_file_path = AGENT_LOGGING_DIR.join("RLY-output.log");
const STORING_NEW_MESSAGE_LOG_MESSAGE: &str = "Storing new message in db";
const LOOKING_FOR_EVENTS_LOG_MESSAGE: &str = "Looking for events in index range";
const HYPER_INCOMING_BODY_LOG_MESSAGE: &str = "incoming body completed";
let relayer_logfile = File::open(log_file_path)?;
let gas_expenditure_log_count =
get_matching_lines(&relayer_logfile, GAS_EXPENDITURE_LOG_MESSAGE)
.unwrap()
.len();
let invariant_logs = &[
STORING_NEW_MESSAGE_LOG_MESSAGE,
LOOKING_FOR_EVENTS_LOG_MESSAGE,
GAS_EXPENDITURE_LOG_MESSAGE,
HYPER_INCOMING_BODY_LOG_MESSAGE,
];
let log_counts = get_matching_lines(&relayer_logfile, invariant_logs);
// Zero insertion messages don't reach `submit` stage where gas is spent, so we only expect these logs for the other messages.
// TODO: Sometimes we find more logs than expected. This may either mean that gas is deducted twice for the same message due to a bug,
// or that submitting the message transaction fails for some messages. Figure out which is the case and convert this check to
@ -75,12 +80,26 @@ pub fn termination_invariants_met(
// EDIT: Having had a quick look, it seems like there are some legitimate reverts happening in the confirm step
// (`Transaction attempting to process message either reverted or was reorged`)
// in which case more gas expenditure logs than messages are expected.
let gas_expenditure_log_count = log_counts.get(GAS_EXPENDITURE_LOG_MESSAGE).unwrap();
assert!(
gas_expenditure_log_count as u32 >= total_messages_expected,
gas_expenditure_log_count >= &total_messages_expected,
"Didn't record gas payment for all delivered messages. Got {} gas payment logs, expected at least {}",
gas_expenditure_log_count,
total_messages_expected
);
// These tests check that we fixed https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3915, where some logs would not show up
assert!(
log_counts.get(STORING_NEW_MESSAGE_LOG_MESSAGE).unwrap() > &0,
"Didn't find any logs about storing messages in db"
);
assert!(
log_counts.get(LOOKING_FOR_EVENTS_LOG_MESSAGE).unwrap() > &0,
"Didn't find any logs about looking for events in index range"
);
assert!(
log_counts.get(HYPER_INCOMING_BODY_LOG_MESSAGE).is_none(),
"Verbose logs not expected at the log level set in e2e"
);
let gas_payment_sealevel_events_count = fetch_metric(
"9092",

@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::fs::File;
use std::io::{self, BufRead};
use std::path::{Path, PathBuf};
@ -118,15 +119,18 @@ pub fn stop_child(child: &mut Child) {
};
}
pub fn get_matching_lines(file: &File, search_string: &str) -> io::Result<Vec<String>> {
pub fn get_matching_lines(file: &File, search_strings: &[&str]) -> HashMap<String, u32> {
let reader = io::BufReader::new(file);
// Read lines and collect those that contain the search string
let matching_lines: Vec<String> = reader
.lines()
.map_while(Result::ok)
.filter(|line| line.contains(search_string))
.collect();
Ok(matching_lines)
let mut matches = HashMap::new();
let mut lines = reader.lines();
while let Some(Ok(line)) = lines.next() {
search_strings.iter().for_each(|search_string| {
if line.contains(search_string) {
let count = matches.entry(search_string.to_string()).or_insert(0);
*count += 1;
}
});
}
matches
}

@ -1,5 +1,15 @@
# @hyperlane-xyz/core
## 4.0.0
### Minor Changes
- 44cc9bf6b: Add CLI command to support AVS validator status check
### Patch Changes
- @hyperlane-xyz/utils@4.0.0
## 3.16.0
### Patch Changes

@ -20,6 +20,11 @@ interface IDelegationManager {
uint32 stakerOptOutWindowBlocks;
}
event OperatorMetadataURIUpdated(
address indexed operator,
string metadataURI
);
function registerAsOperator(
OperatorDetails calldata registeringOperatorDetails,
string calldata metadataURI

@ -1,10 +1,10 @@
{
"name": "@hyperlane-xyz/core",
"description": "Core solidity contracts for Hyperlane",
"version": "3.16.0",
"version": "4.0.0",
"dependencies": {
"@eth-optimism/contracts": "^0.6.0",
"@hyperlane-xyz/utils": "3.16.0",
"@hyperlane-xyz/utils": "4.0.0",
"@layerzerolabs/lz-evm-oapp-v2": "2.0.2",
"@openzeppelin/contracts": "^4.9.3",
"@openzeppelin/contracts-upgradeable": "^v4.9.3",

@ -1,5 +1,7 @@
# @hyperlane-xyz/ccip-server
## 4.0.0
## 3.16.0
## 3.15.1

@ -1,6 +1,6 @@
{
"name": "@hyperlane-xyz/ccip-server",
"version": "3.16.0",
"version": "4.0.0",
"description": "CCIP server",
"typings": "dist/index.d.ts",
"typedocMain": "src/index.ts",

@ -1,5 +1,43 @@
# @hyperlane-xyz/cli
## 4.0.0
### Major Changes
- df6a18053: Release CLI v4.0.0.
### Minor Changes
- 44cc9bf6b: Add CLI command to support AVS validator status check
- b05ae38ac: Gracefully handle RPC failures during warp send & fix deriving hook error that prevents warp and core test messages on the cli.
- 9304fe241: Use metadata builders in message relaying
- 6398aab72: Upgrade registry to 2.1.1
- 5c8ba0b85: Rename hyperlane config create chain -> hyperlane registry init. Rename all `configure` to `init`
- cd419c98a: Add a validator preFlightCheck command verifying that the validator has been announced for a given chain
- 35f869950: Add command to support creating agent configs
- bf7ad09da: feat(cli): add `warp --symbol` flag
- b0828b3d0: Reintroduce `ism read` and `hook read` commands
- 129bd871d: Add chain displayName prompt with default
- 4040db723: Fix createDefaultWarpIsmConfig to default to trusted relayer and fallback routing without prompts
- 6db9fa9ad: Implement hyperlane warp deploy
- bd3ca9195: Updates ci-test.sh to ci-advanced-test.sh.
- b7003cf35: Add stdout.rows to pagesize calculation with DEFAULT_PAGE_SIZE
### Patch Changes
- 3283eefd6: Removes default pattern for chain name when creating a new chain.
- 4dd2651ee: Add xerc20 limit lookups to warp read
- 6b63c5d82: Adds deployment support for IsmConfig within a WarpRouteConfig
- Updated dependencies [b05ae38ac]
- Updated dependencies [9304fe241]
- Updated dependencies [bdcbe1d16]
- Updated dependencies [6b63c5d82]
- Updated dependencies [e38d31685]
- Updated dependencies [e0f226806]
- Updated dependencies [6db9fa9ad]
- @hyperlane-xyz/sdk@4.0.0
- @hyperlane-xyz/utils@4.0.0
## 3.16.0
### Patch Changes

@ -15,7 +15,7 @@ _main() {
# with the routing over igp hook (which is closer to production deployment)
TEST_TYPE=$1
if [ -z "$TEST_TYPE" ]; then
echo "Usage: ci-test.sh <$TEST_TYPE_PRESET_HOOK | $TEST_TYPE_CONFIGURED_HOOK | $TEST_TYPE_PI_CORE>"
echo "Usage: ci-advanced-test.sh <$TEST_TYPE_PRESET_HOOK | $TEST_TYPE_CONFIGURED_HOOK | $TEST_TYPE_PI_CORE>"
exit 1
fi
@ -34,11 +34,11 @@ _main() {
run_hyperlane_deploy_warp;
run_hyperlane_send_message;
cd ./rust;
# cd ./rust;
run_validator;
run_relayer;
run_hyperlane_status;
# run_validator;
# run_relayer;
# run_hyperlane_status;
kill_anvil;
@ -75,6 +75,7 @@ prepare_environment_vars() {
}
prepare_anvil() {
CHAIN1_PORT=8545
CHAIN2_PORT=8555
@ -142,12 +143,11 @@ run_hyperlane_deploy_core_dry_run() {
update_deployer_balance;
echo -e "\nDry-running contract deployments to Alfajores"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy core \
yarn workspace @hyperlane-xyz/cli run hyperlane core deploy \
--dry-run alfajores \
--registry ${TEST_CONFIGS_PATH}/dry-run \
--overrides " " \
$(if [ "$HOOK_FLAG" == "true" ]; then echo "--hook ${EXAMPLES_PATH}/hooks.yaml"; fi) \
--ism ${TEST_CONFIGS_PATH}/dry-run/ism.yaml \
--config ${EXAMPLES_PATH}/core-config.yaml \
--from-address 0xfaD1C94469700833717Fa8a3017278BC1cA8031C \
--yes
@ -162,7 +162,7 @@ run_hyperlane_deploy_warp_dry_run() {
update_deployer_balance;
echo -e "\nDry-running warp route deployments to Alfajores"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy warp \
yarn workspace @hyperlane-xyz/cli run hyperlane warp deploy \
--dry-run alfajores \
--overrides ${TEST_CONFIGS_PATH}/dry-run \
--config ${TEST_CONFIGS_PATH}/dry-run/warp-route-deployment.yaml \
@ -175,14 +175,21 @@ run_hyperlane_deploy_warp_dry_run() {
run_hyperlane_deploy_core() {
update_deployer_balance;
echo -e "\nDeploying contracts to ${CHAIN1} and ${CHAIN2}"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy core \
echo -e "\nDeploying contracts to ${CHAIN1}"
yarn workspace @hyperlane-xyz/cli run hyperlane core deploy \
--registry $REGISTRY_PATH \
--overrides " " \
--config ${EXAMPLES_PATH}/core-config.yaml \
--chain $CHAIN1 \
--key $ANVIL_KEY \
--yes
echo -e "\nDeploying contracts to ${CHAIN2}"
yarn workspace @hyperlane-xyz/cli run hyperlane core deploy \
--registry $REGISTRY_PATH \
--overrides " " \
--targets ${CHAIN1},${CHAIN2} \
$(if [ "$HOOK_FLAG" == "true" ]; then echo "--hook ${EXAMPLES_PATH}/hooks.yaml"; fi) \
--ism $CORE_ISM_PATH \
--agent /tmp/agent-config.json \
--config ${EXAMPLES_PATH}/core-config.yaml \
--chain $CHAIN2 \
--key $ANVIL_KEY \
--yes
@ -193,7 +200,7 @@ run_hyperlane_deploy_warp() {
update_deployer_balance;
echo -e "\nDeploying hypNative warp route"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy warp \
yarn workspace @hyperlane-xyz/cli run hyperlane warp deploy \
--registry $REGISTRY_PATH \
--overrides " " \
--config $WARP_DEPLOY_CONFIG_PATH \
@ -206,7 +213,7 @@ run_hyperlane_deploy_warp() {
/tmp/warp-collateral-deployment.json \
echo "Deploying hypCollateral warp route"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy warp \
yarn workspace @hyperlane-xyz/cli run hyperlane warp deploy \
--registry $REGISTRY_PATH \
--overrides " " \
--config /tmp/warp-collateral-deployment.json \
@ -238,7 +245,7 @@ run_hyperlane_send_message() {
WARP_CONFIG_FILE="$REGISTRY_PATH/deployments/warp_routes/FAKE/${CHAIN1}-${CHAIN2}-config.yaml"
echo -e "\nSending test warp transfer"
yarn workspace @hyperlane-xyz/cli run hyperlane send transfer \
yarn workspace @hyperlane-xyz/cli run hyperlane warp send \
--registry $REGISTRY_PATH \
--overrides " " \
--origin ${CHAIN1} \

@ -6,8 +6,8 @@ import type { LogFormat, LogLevel } from '@hyperlane-xyz/utils';
import './env.js';
import { avsCommand } from './src/commands/avs.js';
import { chainsCommand } from './src/commands/chains.js';
import { configCommand } from './src/commands/config.js';
import { coreCommand } from './src/commands/core.js';
import { deployCommand } from './src/commands/deploy.js';
import { hookCommand } from './src/commands/hook.js';
import { ismCommand } from './src/commands/ism.js';
@ -19,9 +19,11 @@ import {
registryUriCommandOption,
skipConfirmationOption,
} from './src/commands/options.js';
import { registryCommand } from './src/commands/registry.js';
import { sendCommand } from './src/commands/send.js';
import { statusCommand } from './src/commands/status.js';
import { validatorCommand } from './src/commands/validator.js';
import { warpCommand } from './src/commands/warp.js';
import { contextMiddleware } from './src/context/context.js';
import { configureLogger, errorRed } from './src/logger.js';
import { checkVersion } from './src/utils/version-check.js';
@ -51,14 +53,16 @@ try {
contextMiddleware,
])
.command(avsCommand)
.command(chainsCommand)
.command(configCommand)
.command(coreCommand)
.command(deployCommand)
.command(hookCommand)
.command(ismCommand)
.command(registryCommand)
.command(sendCommand)
.command(statusCommand)
.command(validatorCommand)
.command(warpCommand)
.version(VERSION)
.demandCommand()
.strict()

@ -0,0 +1,19 @@
# A config to define the core contract deployments
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
defaultIsm:
type: 'testIsm'
threshold: 1 # Number: Signatures required to approve a message
validators: # Array: List of validator addresses
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
defaultHook:
type: protocolFee
maxProtocolFee: '1000000000000000000' # in wei (string)
protocolFee: '200000000000000' # in wei (string)
beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
requiredHook:
type: protocolFee
maxProtocolFee: '1000000000000000000' # in wei (string)
protocolFee: '200000000000000' # in wei (string)
beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'

@ -39,8 +39,10 @@ anvil1:
oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
overhead:
anvil2: 50000 # gas amount (number)
gasOracleType:
anvil2: StorageGasOracle
oracleConfig:
anvil2:
gasPrice: '100'
tokenExchangeRate: '100'
anvil2:
required:
type: protocolFee
@ -62,5 +64,7 @@ anvil2:
oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
overhead:
anvil1: 50000
gasOracleType:
anvil1: StorageGasOracle
oracleConfig:
anvil1:
gasPrice: '100'
tokenExchangeRate: '100'

@ -1,13 +1,13 @@
{
"name": "@hyperlane-xyz/cli",
"version": "3.16.0",
"version": "4.0.0",
"description": "A command-line utility for common Hyperlane operations",
"dependencies": {
"@aws-sdk/client-kms": "^3.577.0",
"@aws-sdk/client-s3": "^3.577.0",
"@hyperlane-xyz/registry": "2.1.1",
"@hyperlane-xyz/sdk": "3.16.0",
"@hyperlane-xyz/utils": "3.16.0",
"@hyperlane-xyz/sdk": "4.0.0",
"@hyperlane-xyz/utils": "4.0.0",
"@inquirer/prompts": "^3.0.0",
"asn1.js": "^5.4.1",
"bignumber.js": "^9.1.1",
@ -18,7 +18,8 @@
"tsx": "^4.7.1",
"yaml": "^2.4.1",
"yargs": "^17.7.2",
"zod": "^3.21.2"
"zod": "^3.21.2",
"zod-validation-error": "^3.3.0"
},
"devDependencies": {
"@ethersproject/abi": "*",

@ -0,0 +1,449 @@
import { Wallet } from 'ethers';
import {
ECDSAStakeRegistry__factory,
IDelegationManager__factory,
MerkleTreeHook__factory,
ValidatorAnnounce__factory,
} from '@hyperlane-xyz/core';
import { ChainMap, ChainName, MultiProvider } from '@hyperlane-xyz/sdk';
import { Address, ProtocolType, isObjEmpty } from '@hyperlane-xyz/utils';
import { CommandContext } from '../context/types.js';
import {
errorRed,
log,
logBlue,
logBlueKeyValue,
logBoldBlue,
logDebug,
logGreen,
warnYellow,
} from '../logger.js';
import { indentYamlOrJson } from '../utils/files.js';
import {
getLatestMerkleTreeCheckpointIndex,
getLatestValidatorCheckpointIndexAndUrl,
getValidatorStorageLocations,
isValidatorSigningLatestCheckpoint,
} from '../validator/utils.js';
import { avsAddresses } from './config.js';
import { readOperatorFromEncryptedJson } from './stakeRegistry.js';
interface ChainInfo {
storageLocation?: string;
latestMerkleTreeCheckpointIndex?: number;
latestValidatorCheckpointIndex?: number;
validatorSynced?: boolean;
warnings?: string[];
}
interface ValidatorInfo {
operatorAddress: Address;
operatorName?: string;
chains: ChainMap<ChainInfo>;
}
export const checkValidatorAvsSetup = async (
chain: string,
context: CommandContext,
operatorKeyPath?: string,
operatorAddress?: string,
) => {
logBlue(
`Checking AVS validator status for ${chain}, ${
!operatorKeyPath ? 'this may take up to a minute to run' : ''
}...`,
);
const { multiProvider } = context;
const topLevelErrors: string[] = [];
let operatorWallet: Wallet | undefined;
if (operatorKeyPath) {
operatorWallet = await readOperatorFromEncryptedJson(operatorKeyPath);
}
const avsOperatorRecord = await getAvsOperators(
chain,
multiProvider,
topLevelErrors,
operatorAddress ?? operatorWallet?.address,
);
await setOperatorName(
chain,
avsOperatorRecord,
multiProvider,
topLevelErrors,
);
if (!isObjEmpty(avsOperatorRecord)) {
await setValidatorInfo(context, avsOperatorRecord, topLevelErrors);
}
logOutput(avsOperatorRecord, topLevelErrors);
};
const getAvsOperators = async (
chain: string,
multiProvider: MultiProvider,
topLevelErrors: string[],
operatorKey?: string,
): Promise<ChainMap<ValidatorInfo>> => {
const avsOperators: Record<Address, ValidatorInfo> = {};
const ecdsaStakeRegistryAddress = getEcdsaStakeRegistryAddress(
chain,
topLevelErrors,
);
if (!ecdsaStakeRegistryAddress) {
return avsOperators;
}
const ecdsaStakeRegistry = ECDSAStakeRegistry__factory.connect(
ecdsaStakeRegistryAddress,
multiProvider.getProvider(chain),
);
if (operatorKey) {
// If operator key is provided, only fetch the operator's validator info
const signingKey = await ecdsaStakeRegistry.getLastestOperatorSigningKey(
operatorKey,
);
avsOperators[signingKey] = {
operatorAddress: operatorKey,
chains: {},
};
return avsOperators;
}
const filter = ecdsaStakeRegistry.filters.SigningKeyUpdate(null, null);
const provider = multiProvider.getProvider(chain);
const latestBlock = await provider.getBlockNumber();
const blockLimit = 50000; // 50k blocks per query
let fromBlock = 1625972; // when ecdsaStakeRegistry was deployed
while (fromBlock < latestBlock) {
const toBlock = Math.min(fromBlock + blockLimit, latestBlock);
const logs = await ecdsaStakeRegistry.queryFilter(
filter,
fromBlock,
toBlock,
);
logs.forEach((log) => {
const event = ecdsaStakeRegistry.interface.parseLog(log);
const operatorKey = event.args.operator;
const signingKey = event.args.newSigningKey;
if (avsOperators[signingKey]) {
avsOperators[signingKey].operatorAddress = operatorKey;
} else {
avsOperators[signingKey] = {
operatorAddress: operatorKey,
chains: {},
};
}
});
fromBlock = toBlock + 1;
}
return avsOperators;
};
const getAVSMetadataURI = async (
chain: string,
operatorAddress: string,
multiProvider: MultiProvider,
): Promise<string | undefined> => {
const delegationManagerAddress = avsAddresses[chain]['delegationManager'];
const delegationManager = IDelegationManager__factory.connect(
delegationManagerAddress,
multiProvider.getProvider(chain),
);
const filter = delegationManager.filters.OperatorMetadataURIUpdated(
operatorAddress,
null,
);
const provider = multiProvider.getProvider(chain);
const latestBlock = await provider.getBlockNumber();
const blockLimit = 50000; // 50k blocks per query
let fromBlock = 17445563;
while (fromBlock < latestBlock) {
const toBlock = Math.min(fromBlock + blockLimit, latestBlock);
const logs = await delegationManager.queryFilter(
filter,
fromBlock,
toBlock,
);
if (logs.length > 0) {
const event = delegationManager.interface.parseLog(logs[0]);
return event.args.metadataURI;
}
fromBlock = toBlock + 1;
}
return undefined;
};
const setOperatorName = async (
chain: string,
avsOperatorRecord: Record<Address, ValidatorInfo>,
multiProvider: MultiProvider,
topLevelErrors: string[] = [],
) => {
for (const [_, validatorInfo] of Object.entries(avsOperatorRecord)) {
const metadataURI = await getAVSMetadataURI(
chain,
validatorInfo.operatorAddress,
multiProvider,
);
if (metadataURI) {
const operatorName = await fetchOperatorName(metadataURI);
if (operatorName) {
validatorInfo.operatorName = operatorName;
} else {
topLevelErrors.push(
` Failed to fetch operator name from metadataURI: ${metadataURI}`,
);
}
}
}
};
const setValidatorInfo = async (
context: CommandContext,
avsOperatorRecord: Record<Address, ValidatorInfo>,
topLevelErrors: string[],
) => {
const { multiProvider, registry, chainMetadata } = context;
const failedToReadChains: string[] = [];
const validatorAddresses = Object.keys(avsOperatorRecord);
const chains = await registry.getChains();
const addresses = await registry.getAddresses();
for (const chain of chains) {
// skip if chain is not an Ethereum chain
if (chainMetadata[chain].protocol !== ProtocolType.Ethereum) continue;
const chainAddresses = addresses[chain];
// skip if no contract addresses are found for this chain
if (chainAddresses === undefined) continue;
if (!chainAddresses.validatorAnnounce) {
topLevelErrors.push(` ValidatorAnnounce is not deployed on ${chain}`);
}
if (!chainAddresses.merkleTreeHook) {
topLevelErrors.push(` MerkleTreeHook is not deployed on ${chain}`);
}
if (!chainAddresses.validatorAnnounce || !chainAddresses.merkleTreeHook) {
continue;
}
const validatorAnnounce = ValidatorAnnounce__factory.connect(
chainAddresses.validatorAnnounce,
multiProvider.getProvider(chain),
);
const merkleTreeHook = MerkleTreeHook__factory.connect(
chainAddresses.merkleTreeHook,
multiProvider.getProvider(chain),
);
const latestMerkleTreeCheckpointIndex =
await getLatestMerkleTreeCheckpointIndex(merkleTreeHook, chain);
const validatorStorageLocations = await getValidatorStorageLocations(
validatorAnnounce,
validatorAddresses,
chain,
);
if (!validatorStorageLocations) {
failedToReadChains.push(chain);
continue;
}
for (let i = 0; i < validatorAddresses.length; i++) {
const validatorAddress = validatorAddresses[i];
const storageLocation = validatorStorageLocations[i];
const warnings: string[] = [];
// Skip if no storage location is found, address is not validating on this chain or if storage location string doesn't not start with s3://
if (
storageLocation.length === 0 ||
!storageLocation[0].startsWith('s3://')
) {
continue;
}
const [latestValidatorCheckpointIndex, latestCheckpointUrl] =
(await getLatestValidatorCheckpointIndexAndUrl(storageLocation[0])) ?? [
undefined,
undefined,
];
if (!latestMerkleTreeCheckpointIndex) {
warnings.push(
` Failed to fetch latest checkpoint index of merkleTreeHook on ${chain}.`,
);
}
if (!latestValidatorCheckpointIndex) {
warnings.push(
` Failed to fetch latest signed checkpoint index of validator on ${chain}, this is likely due to failing to read an S3 bucket`,
);
}
let validatorSynced = undefined;
if (latestMerkleTreeCheckpointIndex && latestValidatorCheckpointIndex) {
validatorSynced = isValidatorSigningLatestCheckpoint(
latestValidatorCheckpointIndex,
latestMerkleTreeCheckpointIndex,
);
}
const chainInfo: ChainInfo = {
storageLocation: latestCheckpointUrl,
latestMerkleTreeCheckpointIndex,
latestValidatorCheckpointIndex,
validatorSynced,
warnings,
};
const validatorInfo = avsOperatorRecord[validatorAddress];
if (validatorInfo) {
validatorInfo.chains[chain as ChainName] = chainInfo;
}
}
}
if (failedToReadChains.length > 0) {
topLevelErrors.push(
` Failed to read storage locations onchain for ${failedToReadChains.join(
', ',
)}`,
);
}
};
const logOutput = (
avsKeysRecord: Record<Address, ValidatorInfo>,
topLevelErrors: string[],
) => {
if (topLevelErrors.length > 0) {
for (const error of topLevelErrors) {
errorRed(error);
}
}
for (const [validatorAddress, data] of Object.entries(avsKeysRecord)) {
log('\n\n');
if (data.operatorName) logBlueKeyValue('Operator name', data.operatorName);
logBlueKeyValue('Operator address', data.operatorAddress);
logBlueKeyValue('Validator address', validatorAddress);
if (!isObjEmpty(data.chains)) {
logBoldBlue(indentYamlOrJson('Validating on...', 2));
for (const [chain, chainInfo] of Object.entries(data.chains)) {
logBoldBlue(indentYamlOrJson(chain, 2));
if (chainInfo.storageLocation) {
logBlueKeyValue(
indentYamlOrJson('Storage location', 2),
chainInfo.storageLocation,
);
}
if (chainInfo.latestMerkleTreeCheckpointIndex) {
logBlueKeyValue(
indentYamlOrJson('Latest merkle tree checkpoint index', 2),
String(chainInfo.latestMerkleTreeCheckpointIndex),
);
}
if (chainInfo.latestValidatorCheckpointIndex) {
logBlueKeyValue(
indentYamlOrJson('Latest validator checkpoint index', 2),
String(chainInfo.latestValidatorCheckpointIndex),
);
if (chainInfo.validatorSynced) {
logGreen(
indentYamlOrJson('✅ Validator is signing latest checkpoint', 2),
);
} else {
errorRed(
indentYamlOrJson(
'❌ Validator is not signing latest checkpoint',
2,
),
);
}
} else {
errorRed(
indentYamlOrJson(
'❌ Failed to fetch latest signed checkpoint index',
2,
),
);
}
if (chainInfo.warnings && chainInfo.warnings.length > 0) {
warnYellow(
indentYamlOrJson('The following warnings were encountered:', 2),
);
for (const warning of chainInfo.warnings) {
warnYellow(indentYamlOrJson(warning, 3));
}
}
}
} else {
logBlue('Validator is not validating on any chain');
}
}
};
const getEcdsaStakeRegistryAddress = (
chain: string,
topLevelErrors: string[],
): Address | undefined => {
try {
return avsAddresses[chain]['ecdsaStakeRegistry'];
} catch (err) {
topLevelErrors.push(
` EcdsaStakeRegistry address not found for ${chain}`,
);
return undefined;
}
};
const fetchOperatorName = async (metadataURI: string) => {
try {
const response = await fetch(metadataURI);
const data = await response.json();
return data['name'];
} catch (err) {
logDebug(`Failed to fetch operator name from ${metadataURI}: ${err}`);
return undefined;
}
};

@ -3,6 +3,7 @@ import { Address } from '@hyperlane-xyz/utils';
interface AVSContracts {
avsDirectory: Address;
delegationManager: Address;
proxyAdmin: Address;
ecdsaStakeRegistry: Address;
hyperlaneServiceManager: Address;
@ -12,12 +13,14 @@ interface AVSContracts {
export const avsAddresses: ChainMap<AVSContracts> = {
holesky: {
avsDirectory: '0x055733000064333CaDDbC92763c58BF0192fFeBf',
delegationManager: '0xA44151489861Fe9e3055d95adC98FbD462B948e7',
proxyAdmin: '0x33dB966328Ea213b0f76eF96CA368AB37779F065',
ecdsaStakeRegistry: '0xFfa913705484C9BAea32Ffe9945BeA099A1DFF72',
hyperlaneServiceManager: '0xc76E477437065093D353b7d56c81ff54D167B0Ab',
},
ethereum: {
avsDirectory: '0x135dda560e946695d6f155dacafc6f1f25c1f5af',
delegationManager: '0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A',
proxyAdmin: '0x75EE15Ee1B4A75Fa3e2fDF5DF3253c25599cc659',
ecdsaStakeRegistry: '0x272CF0BB70D3B4f79414E0823B426d2EaFd48910',
hyperlaneServiceManager: '0xe8E59c6C8B56F2c178f63BCFC4ce5e5e2359c8fc',

@ -109,7 +109,7 @@ export async function deregisterOperator({
);
}
async function readOperatorFromEncryptedJson(
export async function readOperatorFromEncryptedJson(
operatorKeyPath: string,
): Promise<Wallet> {
const encryptedJson = readFileAtPath(resolvePath(operatorKeyPath));
@ -119,7 +119,7 @@ async function readOperatorFromEncryptedJson(
message: 'Enter the password for the operator key file: ',
});
return await Wallet.fromEncryptedJson(encryptedJson, keyFilePassword);
return Wallet.fromEncryptedJson(encryptedJson, keyFilePassword);
}
async function getOperatorSignature(

@ -1,14 +1,21 @@
import { CommandModule, Options } from 'yargs';
import { ChainName } from '@hyperlane-xyz/sdk';
import { Address } from '@hyperlane-xyz/utils';
import { Address, ProtocolType } from '@hyperlane-xyz/utils';
import { checkValidatorAvsSetup } from '../avs/check.js';
import {
deregisterOperator,
registerOperatorWithSignature,
} from '../avs/stakeRegistry.js';
import { CommandModuleWithWriteContext } from '../context/types.js';
import { log } from '../logger.js';
import { errorRed, log } from '../logger.js';
import {
avsChainCommandOption,
demandOption,
operatorKeyPathCommandOption,
} from './options.js';
/**
* Parent command
@ -20,6 +27,7 @@ export const avsCommand: CommandModule = {
yargs
.command(registerCommand)
.command(deregisterCommand)
.command(checkCommand)
.version(false)
.demandCommand(),
handler: () => log('Command required'),
@ -29,17 +37,8 @@ export const avsCommand: CommandModule = {
* Registration command
*/
export const registrationOptions: { [k: string]: Options } = {
chain: {
type: 'string',
description: 'Chain to interact with the AVS on',
demandOption: true,
choices: ['holesky', 'ethereum'],
},
operatorKeyPath: {
type: 'string',
description: 'Path to the operator key file',
demandOption: true,
},
chain: avsChainCommandOption,
operatorKeyPath: demandOption(operatorKeyPathCommandOption),
avsSigningKeyAddress: {
type: 'string',
description: 'Address of the AVS signing key',
@ -87,3 +86,47 @@ const deregisterCommand: CommandModuleWithWriteContext<{
process.exit(0);
},
};
const checkCommand: CommandModuleWithWriteContext<{
chain: ChainName;
operatorKeyPath?: string;
operatorAddress?: string;
}> = {
command: 'check',
describe: 'Check AVS',
builder: {
chain: avsChainCommandOption,
operatorKeyPath: operatorKeyPathCommandOption,
operatorAddress: {
type: 'string',
description: 'Address of the operator to check',
},
},
handler: async ({ context, chain, operatorKeyPath, operatorAddress }) => {
const { multiProvider } = context;
// validate chain
if (!multiProvider.hasChain(chain)) {
errorRed(
`❌ No metadata found for ${chain}. Ensure it is included in your configured registry.`,
);
process.exit(1);
}
const chainMetadata = multiProvider.getChainMetadata(chain);
if (chainMetadata.protocol !== ProtocolType.Ethereum) {
errorRed(`\n❌ Validator AVS check only supports EVM chains. Exiting.`);
process.exit(1);
}
await checkValidatorAvsSetup(
chain,
context,
operatorKeyPath,
operatorAddress,
);
process.exit(0);
},
};

@ -1,20 +1,13 @@
import { CommandModule } from 'yargs';
import { createChainConfig, readChainConfigs } from '../config/chain.js';
import { createHooksConfigMap } from '../config/hooks.js';
import { createIsmConfigMap, readIsmConfig } from '../config/ism.js';
import {
createMultisigConfig,
readMultisigConfig,
} from '../config/multisig.js';
import {
createWarpRouteDeployConfig,
readWarpRouteDeployConfig,
} from '../config/warp.js';
import { readChainConfigs } from '../config/chain.js';
import { readIsmConfig } from '../config/ism.js';
import { readMultisigConfig } from '../config/multisig.js';
import { readWarpRouteDeployConfig } from '../config/warp.js';
import { CommandModuleWithContext } from '../context/types.js';
import { log, logGreen } from '../logger.js';
import { inputFileCommandOption, outputFileCommandOption } from './options.js';
import { inputFileCommandOption } from './options.js';
/**
* Parent command
@ -23,90 +16,10 @@ export const configCommand: CommandModule = {
command: 'config',
describe: 'Create or validate Hyperlane configs',
builder: (yargs) =>
yargs
.command(createCommand)
.command(validateCommand)
.version(false)
.demandCommand(),
yargs.command(validateCommand).version(false).demandCommand(),
handler: () => log('Command required'),
};
/**
* Create commands
*/
const createCommand: CommandModule = {
command: 'create',
describe: 'Create a new Hyperlane config',
builder: (yargs) =>
yargs
.command(createChainConfigCommand)
.command(createIsmConfigCommand)
.command(createHookConfigCommand)
.command(createWarpRouteDeployConfigCommand)
.version(false)
.demandCommand(),
handler: () => log('Command required'),
};
const createChainConfigCommand: CommandModuleWithContext<{}> = {
command: 'chain',
describe: 'Create a new, minimal Hyperlane chain config (aka chain metadata)',
handler: async ({ context }) => {
await createChainConfig({ context });
process.exit(0);
},
};
const createIsmConfigCommand: CommandModuleWithContext<{
out: string;
advanced: boolean;
}> = {
command: 'ism',
describe: 'Create a basic or advanced ISM config for a validator set',
builder: {
out: outputFileCommandOption('./configs/ism.yaml'),
advanced: {
type: 'boolean',
describe: 'Create an advanced ISM configuration',
default: false,
},
},
handler: async ({ context, out, advanced }) => {
if (advanced) {
await createIsmConfigMap({ context, outPath: out });
} else {
await createMultisigConfig({ context, outPath: out });
}
process.exit(0);
},
};
const createHookConfigCommand: CommandModuleWithContext<{ out: string }> = {
command: 'hooks',
describe: 'Create a new hooks config (required & default)',
builder: {
out: outputFileCommandOption('./configs/hooks.yaml'),
},
handler: async ({ context, out }) => {
await createHooksConfigMap({ context, outPath: out });
process.exit(0);
},
};
const createWarpRouteDeployConfigCommand: CommandModuleWithContext<{
out: string;
}> = {
command: 'warp',
describe: 'Create a new Warp Route deployment config',
builder: {
out: outputFileCommandOption('./configs/warp-route-deployment.yaml'),
},
handler: async ({ context, out }) => {
await createWarpRouteDeployConfig({ context, outPath: out });
process.exit(0);
},
};
/**
* Validate commands
*/

@ -0,0 +1,155 @@
import { stringify as yamlStringify } from 'yaml';
import { CommandModule } from 'yargs';
import { EvmCoreReader } from '@hyperlane-xyz/sdk';
import { createCoreDeployConfig } from '../config/core.js';
import {
CommandModuleWithContext,
CommandModuleWithWriteContext,
} from '../context/types.js';
import { runCoreDeploy } from '../deploy/core.js';
import { evaluateIfDryRunFailure } from '../deploy/dry-run.js';
import { log, logGray, logGreen } from '../logger.js';
import {
indentYamlOrJson,
readYamlOrJson,
writeYamlOrJson,
} from '../utils/files.js';
import {
chainCommandOption,
dryRunCommandOption,
fromAddressCommandOption,
outputFileCommandOption,
} from './options.js';
/**
* Parent command
*/
export const coreCommand: CommandModule = {
command: 'core',
describe: 'Manage core Hyperlane contracts & configs',
builder: (yargs) =>
yargs
.command(deploy)
.command(init)
.command(read)
.version(false)
.demandCommand(),
handler: () => log('Command required'),
};
/**
* Generates a command module for deploying Hyperlane contracts, given a command
*
* @param commandName - the deploy command key used to look up the deployFunction
* @returns A command module used to deploy Hyperlane contracts.
*/
export const deploy: CommandModuleWithWriteContext<{
chain: string;
config: string;
dryRun: string;
fromAddress: string;
}> = {
command: 'deploy',
describe: 'Deploy Hyperlane contracts',
builder: {
chain: chainCommandOption,
config: outputFileCommandOption(
'./configs/core-config.yaml',
false,
'The path to a JSON or YAML file with a core deployment config.',
),
'dry-run': dryRunCommandOption,
'from-address': fromAddressCommandOption,
},
handler: async ({ context, chain, config: configFilePath, dryRun }) => {
logGray(`Hyperlane permissionless deployment${dryRun ? ' dry-run' : ''}`);
logGray(`------------------------------------------------`);
try {
await runCoreDeploy({
context,
chain,
config: readYamlOrJson(configFilePath),
});
} catch (error: any) {
evaluateIfDryRunFailure(error, dryRun);
throw error;
}
process.exit(0);
},
};
export const init: CommandModuleWithContext<{
advanced: boolean;
config: string;
}> = {
command: 'init',
describe: 'Create a core configuration, including ISMs and hooks.',
builder: {
advanced: {
type: 'boolean',
describe: 'Create an advanced ISM & hook configuration',
default: false,
},
config: outputFileCommandOption(
'./configs/core-config.yaml',
false,
'The path to output a Core Config JSON or YAML file.',
),
},
handler: async ({ context, advanced, config: configFilePath }) => {
logGray('Hyperlane Core Configure');
logGray('------------------------');
await createCoreDeployConfig({
context,
configFilePath,
advanced,
});
process.exit(0);
},
};
export const read: CommandModuleWithContext<{
chain: string;
mailbox: string;
config: string;
}> = {
command: 'read',
describe: 'Reads onchain Core configuration for a given mailbox address',
builder: {
chain: {
...chainCommandOption,
demandOption: true,
},
mailbox: {
type: 'string',
description: 'Mailbox address used to derive the core config',
demandOption: true,
},
config: outputFileCommandOption(
'./configs/core-config.yaml',
false,
'The path to output a Core Config JSON or YAML file.',
),
},
handler: async ({ context, chain, mailbox, config: configFilePath }) => {
logGray('Hyperlane Core Read');
logGray('-------------------');
const evmCoreReader = new EvmCoreReader(context.multiProvider, chain);
const coreConfig = await evmCoreReader.deriveCoreConfig(mailbox);
writeYamlOrJson(configFilePath, coreConfig, 'yaml');
logGreen(
`✅ Warp route config written successfully to ${configFilePath}:\n`,
);
log(indentYamlOrJson(yamlStringify(coreConfig, null, 2), 4));
process.exit(0);
},
};

@ -1,25 +1,13 @@
import { CommandModule } from 'yargs';
import {
CommandModuleWithContext,
CommandModuleWithWriteContext,
} from '../context/types.js';
import { CommandModuleWithContext } from '../context/types.js';
import { runKurtosisAgentDeploy } from '../deploy/agent.js';
import { runCoreDeploy } from '../deploy/core.js';
import { evaluateIfDryRunFailure } from '../deploy/dry-run.js';
import { runWarpRouteDeploy } from '../deploy/warp.js';
import { log, logGray } from '../logger.js';
import {
agentConfigCommandOption,
agentTargetsCommandOption,
coreTargetsCommandOption,
dryRunCommandOption,
fromAddressCommandOption,
hookCommandOption,
ismCommandOption,
originCommandOption,
warpDeploymentConfigCommandOption,
} from './options.js';
/**
@ -29,12 +17,7 @@ export const deployCommand: CommandModule = {
command: 'deploy',
describe: 'Permissionlessly deploy a Hyperlane contracts or extensions',
builder: (yargs) =>
yargs
.command(coreCommand)
.command(warpCommand)
.command(agentCommand)
.version(false)
.demandCommand(),
yargs.command(agentCommand).version(false).demandCommand(),
handler: () => log('Command required'),
};
@ -65,79 +48,3 @@ const agentCommand: CommandModuleWithContext<{
process.exit(0);
},
};
/**
* Core command
*/
const coreCommand: CommandModuleWithWriteContext<{
targets: string;
ism?: string;
hook?: string;
'dry-run': string;
'from-address': string;
agent: string;
}> = {
command: 'core',
describe: 'Deploy core Hyperlane contracts',
builder: {
targets: coreTargetsCommandOption,
ism: ismCommandOption,
hook: hookCommandOption,
agent: agentConfigCommandOption(false, './configs/agent.json'),
'dry-run': dryRunCommandOption,
'from-address': fromAddressCommandOption,
},
handler: async ({ context, targets, ism, hook, agent, dryRun }) => {
logGray(
`Hyperlane permissionless core deployment${dryRun ? ' dry-run' : ''}`,
);
logGray(`------------------------------------------------`);
try {
const chains = targets?.split(',').map((r: string) => r.trim());
await runCoreDeploy({
context,
chains,
ismConfigPath: ism,
hookConfigPath: hook,
agentOutPath: agent,
});
} catch (error: any) {
evaluateIfDryRunFailure(error, dryRun);
throw error;
}
process.exit(0);
},
};
/**
* Warp command
*/
const warpCommand: CommandModuleWithWriteContext<{
config: string;
'dry-run': string;
'from-address': string;
}> = {
command: 'warp',
describe: 'Deploy Warp Route contracts',
builder: {
config: warpDeploymentConfigCommandOption,
'dry-run': dryRunCommandOption,
'from-address': fromAddressCommandOption,
},
handler: async ({ context, config, dryRun }) => {
logGray(`Hyperlane warp route deployment${dryRun ? ' dry-run' : ''}`);
logGray('------------------------------------------------');
try {
await runWarpRouteDeploy({
context,
warpRouteDeploymentConfigPath: config,
});
} catch (error: any) {
evaluateIfDryRunFailure(error, dryRun);
throw error;
}
process.exit(0);
},
};

@ -2,7 +2,7 @@ import { CommandModule } from 'yargs';
import { CommandModuleWithContext } from '../context/types.js';
import { readHookConfig } from '../hook/read.js';
import { log } from '../logger.js';
import { log, logGray } from '../logger.js';
import {
addressCommandOption,
@ -41,6 +41,8 @@ export const read: CommandModuleWithContext<{
out: outputFileCommandOption(),
},
handler: async (args) => {
logGray('Hyperlane Hook Read');
logGray('------------------');
await readHookConfig(args);
process.exit(0);
},

@ -2,7 +2,7 @@ import { CommandModule } from 'yargs';
import { CommandModuleWithContext } from '../context/types.js';
import { readIsmConfig } from '../ism/read.js';
import { log } from '../logger.js';
import { log, logGray } from '../logger.js';
import {
addressCommandOption,
@ -46,6 +46,8 @@ export const read: CommandModuleWithContext<{
out: outputFileCommandOption(),
},
handler: async (argv) => {
logGray('Hyperlane ISM Read');
logGray('------------------');
await readIsmConfig(argv);
process.exit(0);
},

@ -8,6 +8,11 @@ import { ENV } from '../utils/env.js';
/* Global options */
export const demandOption = (option: Options): Options => ({
...option,
demandOption: true,
});
export const logFormatCommandOption: Options = {
type: 'string',
description: 'Log output format',
@ -91,8 +96,6 @@ export const warpCoreConfigCommandOption: Options = {
type: 'string',
description: 'File path to Warp Route config',
alias: 'w',
// TODO make this optional and have the commands get it from the registry
demandOption: true,
};
export const agentConfigCommandOption = (
@ -106,11 +109,23 @@ export const agentConfigCommandOption = (
default: defaultPath,
});
export const outputFileCommandOption = (defaultPath?: string): Options => ({
export const chainTargetsCommandOption: Options = {
type: 'string',
description: 'Comma-separated list of chain names',
alias: 'c',
demandOption: true,
};
export const outputFileCommandOption = (
defaultPath?: string,
demandOption = false,
description = 'Output file path',
): Options => ({
type: 'string',
description: 'Output file path',
description,
default: defaultPath,
alias: 'o',
demandOption,
});
export const inputFileCommandOption: Options = {
@ -138,6 +153,17 @@ export const chainCommandOption: Options = {
description: 'The specific chain to perform operations with.',
};
export const symbolCommandOption: Options = {
type: 'string',
description: 'Token symbol (e.g. ETH, USDC)',
};
export const validatorCommandOption: Options = {
type: 'string',
description: 'Comma separated list of validator addresses',
demandOption: true,
};
export const addressCommandOption = (
description: string,
demandOption = false,
@ -178,3 +204,15 @@ export const awsKeyIdCommandOption: Options = {
type: 'string',
describe: 'Key ID from AWS KMS',
};
export const operatorKeyPathCommandOption: Options = {
type: 'string',
description: 'Path to the operator key file',
};
export const avsChainCommandOption: Options = {
type: 'string',
description: 'Chain to interact with the AVS on',
demandOption: true,
choices: ['holesky', 'ethereum'],
};

@ -1,21 +1,28 @@
import { CommandModule } from 'yargs';
import { CommandModuleWithContext } from '../context/types.js';
import { log, logBlue, logGray, logTable } from '../logger.js';
import { createAgentConfig } from '../config/agent.js';
import { createChainConfig } from '../config/chain.js';
import { CommandContext, CommandModuleWithContext } from '../context/types.js';
import { log, logBlue, logGray, logRed, logTable } from '../logger.js';
const ChainTypes = ['mainnet', 'testnet'];
type ChainType = (typeof ChainTypes)[number];
import {
chainTargetsCommandOption,
outputFileCommandOption,
} from './options.js';
import { ChainType, ChainTypes } from './types.js';
/**
* Parent command
*/
export const chainsCommand: CommandModule = {
command: 'chains',
describe: 'View information about Hyperlane chains in a registry',
export const registryCommand: CommandModule = {
command: 'registry',
describe: 'Manage Hyperlane chains in a registry',
builder: (yargs) =>
yargs
.command(listCommand)
.command(addressesCommand)
.command(createAgentConfigCommand)
.command(initCommand)
.command(listCommand)
.version(false)
.demandCommand(),
handler: () => log('Command required'),
@ -88,3 +95,59 @@ const addressesCommand: CommandModuleWithContext<{ name: string }> = {
}
},
};
/**
* agent-config command
*/
const createAgentConfigCommand: CommandModuleWithContext<{
chains: string;
out: string;
}> = {
command: 'agent-config',
describe: 'Create a new agent config',
builder: {
chains: chainTargetsCommandOption,
out: outputFileCommandOption(
'./configs/agent-config.json',
false,
'The path to output an agent config JSON file.',
),
},
handler: async ({
context,
chains,
out,
}: {
context: CommandContext;
chains: string;
out: string;
}) => {
const { multiProvider } = context;
const chainNames = chains.split(',');
const invalidChainNames = chainNames.filter(
(chainName) => !multiProvider.hasChain(chainName),
);
if (invalidChainNames.length > 0) {
logRed(
`Invalid chain names: ${invalidChainNames
.join(', ')
.replace(/, $/, '')}`,
);
process.exit(1);
}
await createAgentConfig({ context, chains: chainNames, out });
process.exit(0);
},
};
const initCommand: CommandModuleWithContext<{}> = {
command: 'init',
describe: 'Create a new, minimal Hyperlane chain config (aka chain metadata)',
handler: async ({ context }) => {
await createChainConfig({ context });
process.exit(0);
},
};

@ -4,22 +4,15 @@ import { CommandModule, Options } from 'yargs';
import { CommandModuleWithWriteContext } from '../context/types.js';
import { log } from '../logger.js';
import { sendTestMessage } from '../send/message.js';
import { sendTestTransfer } from '../send/transfer.js';
import { warpCoreConfigCommandOption } from './options.js';
/**
* Parent command
*/
export const sendCommand: CommandModule = {
command: 'send',
describe: 'Send a test message or transfer',
describe: 'Send a test message',
builder: (yargs) =>
yargs
.command(messageCommand)
.command(transferCommand)
.version(false)
.demandCommand(),
yargs.command(messageCommand).version(false).demandCommand(),
handler: () => log('Command required'),
};
@ -94,55 +87,3 @@ const messageCommand: CommandModuleWithWriteContext<
process.exit(0);
},
};
/**
* Transfer command
*/
const transferCommand: CommandModuleWithWriteContext<
MessageOptionsArgTypes & {
warp: string;
router?: string;
wei: string;
recipient?: string;
}
> = {
command: 'transfer',
describe: 'Send a test token transfer on a warp route',
builder: {
...messageOptions,
warp: warpCoreConfigCommandOption,
wei: {
type: 'string',
description: 'Amount in wei to send',
default: 1,
},
recipient: {
type: 'string',
description: 'Token recipient address (defaults to sender)',
},
},
handler: async ({
context,
origin,
destination,
timeout,
quick,
relay,
warp,
wei,
recipient,
}) => {
await sendTestTransfer({
context,
warpConfigPath: warp,
origin,
destination,
wei,
recipient,
timeoutSec: timeout,
skipWaitForDelivery: quick,
selfRelay: relay,
});
process.exit(0);
},
};

@ -0,0 +1,2 @@
export const ChainTypes = ['mainnet', 'testnet'];
export type ChainType = (typeof ChainTypes)[number];

@ -1,8 +1,16 @@
import { CommandModule } from 'yargs';
import {
Address,
ProtocolType,
isValidAddressEvm,
normalizeAddressEvm,
} from '@hyperlane-xyz/utils';
import { CommandModuleWithContext } from '../context/types.js';
import { log } from '../logger.js';
import { errorRed, log } from '../logger.js';
import { getValidatorAddress } from '../validator/address.js';
import { checkValidatorSetup } from '../validator/preFlightCheck.js';
import {
awsAccessKeyCommandOption,
@ -10,13 +18,20 @@ import {
awsKeyIdCommandOption,
awsRegionCommandOption,
awsSecretKeyCommandOption,
chainCommandOption,
demandOption,
validatorCommandOption,
} from './options.js';
// Parent command to help configure and set up Hyperlane validators
export const validatorCommand: CommandModule = {
command: 'validator',
describe: 'Configure and manage Hyperlane validators',
builder: (yargs) => yargs.command(addressCommand).demandCommand(),
builder: (yargs) =>
yargs
.command(addressCommand)
.command(preFlightCheckCommand)
.demandCommand(),
handler: () => log('Command required'),
};
@ -49,3 +64,58 @@ const addressCommand: CommandModuleWithContext<{
process.exit(0);
},
};
const preFlightCheckCommand: CommandModuleWithContext<{
chain: string;
validators: string;
}> = {
command: 'check',
describe: 'Check the validator has announced correctly for a given chain',
builder: {
chain: demandOption(chainCommandOption),
validators: validatorCommandOption,
},
handler: async ({ context, chain, validators }) => {
const { multiProvider } = context;
// validate chain
if (!multiProvider.hasChain(chain)) {
errorRed(
`❌ No metadata found for ${chain}. Ensure it is included in your configured registry.`,
);
process.exit(1);
}
const chainMetadata = multiProvider.getChainMetadata(chain);
if (chainMetadata.protocol !== ProtocolType.Ethereum) {
errorRed(
`\n❌ Validator pre flight check only supports EVM chains. Exiting.`,
);
process.exit(1);
}
// validate validators addresses
const validatorList = validators.split(',');
const invalidAddresses: Set<Address> = new Set();
const validAddresses: Set<Address> = new Set();
for (const address of validatorList) {
if (isValidAddressEvm(address)) {
validAddresses.add(normalizeAddressEvm(address));
} else {
invalidAddresses.add(address);
}
}
if (invalidAddresses.size > 0) {
errorRed(
`❌ Invalid addresses: ${Array.from(invalidAddresses).join(', ')}`,
);
process.exit(1);
}
await checkValidatorSetup(context, chain, validAddresses);
process.exit(0);
},
};

@ -0,0 +1,290 @@
import { ethers } from 'ethers';
import { stringify as yamlStringify } from 'yaml';
import { CommandModule } from 'yargs';
import {
HypXERC20Lockbox__factory,
HypXERC20__factory,
IXERC20__factory,
} from '@hyperlane-xyz/core';
import {
ChainMap,
EvmERC20WarpRouteReader,
TokenStandard,
WarpCoreConfig,
} from '@hyperlane-xyz/sdk';
import { objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import {
createWarpRouteDeployConfig,
readWarpCoreConfig,
} from '../config/warp.js';
import {
CommandModuleWithContext,
CommandModuleWithWriteContext,
} from '../context/types.js';
import { evaluateIfDryRunFailure } from '../deploy/dry-run.js';
import { runWarpRouteDeploy } from '../deploy/warp.js';
import { log, logGray, logGreen, logRed, logTable } from '../logger.js';
import { sendTestTransfer } from '../send/transfer.js';
import { indentYamlOrJson, writeYamlOrJson } from '../utils/files.js';
import { selectRegistryWarpRoute } from '../utils/tokens.js';
import {
addressCommandOption,
chainCommandOption,
dryRunCommandOption,
fromAddressCommandOption,
outputFileCommandOption,
symbolCommandOption,
warpCoreConfigCommandOption,
warpDeploymentConfigCommandOption,
} from './options.js';
import { MessageOptionsArgTypes, messageOptions } from './send.js';
/**
* Parent command
*/
export const warpCommand: CommandModule = {
command: 'warp',
describe: 'Manage Hyperlane warp routes',
builder: (yargs) =>
yargs
.command(deploy)
.command(init)
.command(read)
.command(send)
.version(false)
.demandCommand(),
handler: () => log('Command required'),
};
export const deploy: CommandModuleWithWriteContext<{
config: string;
'dry-run': string;
'from-address': string;
}> = {
command: 'deploy',
describe: 'Deploy Warp Route contracts',
builder: {
config: warpDeploymentConfigCommandOption,
'dry-run': dryRunCommandOption,
'from-address': fromAddressCommandOption,
},
handler: async ({ context, config, dryRun }) => {
logGray(`Hyperlane warp route deployment${dryRun ? ' dry-run' : ''}`);
logGray('------------------------------------------------');
try {
await runWarpRouteDeploy({
context,
warpRouteDeploymentConfigPath: config,
});
} catch (error: any) {
evaluateIfDryRunFailure(error, dryRun);
throw error;
}
process.exit(0);
},
};
export const init: CommandModuleWithContext<{
advanced: boolean;
out: string;
}> = {
command: 'init',
describe: 'Create a warp route configuration.',
builder: {
advanced: {
type: 'boolean',
describe: 'Create an advanced ISM',
default: false,
},
out: outputFileCommandOption('./configs/warp-route-deployment.yaml'),
},
handler: async ({ context, advanced, out }) => {
logGray('Hyperlane Warp Configure');
logGray('------------------------');
await createWarpRouteDeployConfig({
context,
outPath: out,
advanced,
});
process.exit(0);
},
};
export const read: CommandModuleWithContext<{
chain?: string;
address?: string;
out?: string;
symbol?: string;
}> = {
command: 'read',
describe: 'Derive the warp route config from onchain artifacts',
builder: {
symbol: {
...symbolCommandOption,
demandOption: false,
},
chain: {
...chainCommandOption,
demandOption: false,
},
address: addressCommandOption(
'Address of the router contract to read.',
false,
),
out: outputFileCommandOption(),
},
handler: async ({ context, chain, address, out, symbol }) => {
logGray('Hyperlane Warp Reader');
logGray('---------------------');
const { multiProvider } = context;
let addresses: ChainMap<string>;
if (symbol) {
const warpCoreConfig = await selectRegistryWarpRoute(
context.registry,
symbol,
);
// TODO: merge with XERC20TokenAdapter and WarpRouteReader
const xerc20Limits = await Promise.all(
warpCoreConfig.tokens
.filter(
(t) =>
t.standard === TokenStandard.EvmHypXERC20 ||
t.standard === TokenStandard.EvmHypXERC20Lockbox,
)
.map(async (t) => {
const provider = multiProvider.getProvider(t.chainName);
const router = t.addressOrDenom!;
const xerc20Address =
t.standard === TokenStandard.EvmHypXERC20Lockbox
? await HypXERC20Lockbox__factory.connect(
router,
provider,
).xERC20()
: await HypXERC20__factory.connect(
router,
provider,
).wrappedToken();
const xerc20 = IXERC20__factory.connect(xerc20Address, provider);
const mint = await xerc20.mintingCurrentLimitOf(router);
const burn = await xerc20.burningCurrentLimitOf(router);
const formattedLimits = objMap({ mint, burn }, (_, v) =>
ethers.utils.formatUnits(v, t.decimals),
);
return [t.chainName, formattedLimits];
}),
);
if (xerc20Limits.length > 0) {
logGray('xERC20 Limits:');
logTable(Object.fromEntries(xerc20Limits));
}
addresses = Object.fromEntries(
warpCoreConfig.tokens.map((t) => [t.chainName, t.addressOrDenom!]),
);
} else if (chain && address) {
addresses = {
[chain]: address,
};
} else {
logGreen(`Please specify either a symbol or chain and address`);
process.exit(0);
}
const config = await promiseObjAll(
objMap(addresses, async (chain, address) =>
new EvmERC20WarpRouteReader(multiProvider, chain).deriveWarpRouteConfig(
address,
),
),
);
if (out) {
writeYamlOrJson(out, config, 'yaml');
logGreen(`✅ Warp route config written successfully to ${out}:\n`);
} else {
logGreen(`✅ Warp route config read successfully:\n`);
}
log(indentYamlOrJson(yamlStringify(config, null, 2), 4));
process.exit(0);
},
};
const send: CommandModuleWithWriteContext<
MessageOptionsArgTypes & {
warp?: string;
symbol?: string;
router?: string;
amount: string;
recipient?: string;
}
> = {
command: 'send',
describe: 'Send a test token transfer on a warp route',
builder: {
...messageOptions,
symbol: {
...symbolCommandOption,
demandOption: false,
},
warp: {
...warpCoreConfigCommandOption,
demandOption: false,
},
amount: {
type: 'string',
description: 'Amount to send (in smallest unit)',
default: 1,
},
recipient: {
type: 'string',
description: 'Token recipient address (defaults to sender)',
},
},
handler: async ({
context,
origin,
destination,
timeout,
quick,
relay,
symbol,
warp,
amount,
recipient,
}) => {
let warpCoreConfig: WarpCoreConfig;
if (symbol) {
warpCoreConfig = await selectRegistryWarpRoute(context.registry, symbol);
} else if (warp) {
warpCoreConfig = readWarpCoreConfig(warp);
} else {
logRed(`Please specify either a symbol or warp config`);
process.exit(0);
}
await sendTestTransfer({
context,
warpCoreConfig,
origin,
destination,
amount,
recipient,
timeoutSec: timeout,
skipWaitForDelivery: quick,
selfRelay: relay,
});
process.exit(0);
},
};

@ -0,0 +1,75 @@
import { fromError } from 'zod-validation-error';
import {
AgentConfigSchema,
ChainMap,
HyperlaneCore,
HyperlaneDeploymentArtifacts,
buildAgentConfig,
} from '@hyperlane-xyz/sdk';
import { objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import { CommandContext } from '../context/types.js';
import { errorRed, logBlue, logGreen, logRed } from '../logger.js';
import { writeYamlOrJson } from '../utils/files.js';
export async function createAgentConfig({
context,
chains,
out,
}: {
context: CommandContext;
chains: string[];
out: string;
}) {
logBlue('\nCreating agent config...');
const { registry, multiProvider, chainMetadata } = context;
const addresses = await registry.getAddresses();
const core = HyperlaneCore.fromAddressesMap(addresses, multiProvider);
const startBlocks = await promiseObjAll(
objMap(addresses, async (chain, _) => {
// If the index.from is specified in the chain metadata, use that.
const indexFrom = chainMetadata[chain].index?.from;
if (indexFrom !== undefined) {
return indexFrom;
}
const mailbox = core.getContracts(chain).mailbox;
try {
const deployedBlock = await mailbox.deployedBlock();
return deployedBlock.toNumber();
} catch (err) {
logRed(
`Failed to get deployed block to set an index for ${chain}, this is potentially an issue with rpc provider or a misconfiguration`,
);
process.exit(1);
}
}),
);
// @TODO: consider adding additional config used to pass in gas prices for Cosmos chains
const agentConfig = buildAgentConfig(
chains,
multiProvider,
addresses as ChainMap<HyperlaneDeploymentArtifacts>,
startBlocks,
);
try {
AgentConfigSchema.parse(agentConfig);
} catch (e) {
errorRed(
`Agent config is invalid, this is possibly due to required contracts not being deployed. See details below:\n${fromError(
e,
).toString()}`,
);
process.exit(1);
}
logBlue(`Agent config is valid, writing to file ${out}`);
writeYamlOrJson(out, agentConfig, 'json');
logGreen(`✅ Agent config successfully written to ${out}`);
}

@ -1,13 +1,18 @@
import { confirm, input } from '@inquirer/prompts';
import { ethers } from 'ethers';
import { stringify as yamlStringify } from 'yaml';
import { ChainMetadata, ChainMetadataSchema } from '@hyperlane-xyz/sdk';
import {
ChainMetadata,
ChainMetadataSchema,
ZChainName,
} from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { CommandContext } from '../context/types.js';
import { errorRed, log, logBlue, logGreen } from '../logger.js';
import { detectAndConfirmOrPrompt } from '../utils/chains.js';
import { readYamlOrJson } from '../utils/files.js';
import { indentYamlOrJson, readYamlOrJson } from '../utils/files.js';
import { detectAndConfirmOrPrompt } from '../utils/input.js';
export function readChainConfigs(filePath: string) {
log(`Reading file configs in ${filePath}`);
@ -48,19 +53,19 @@ export async function createChainConfig({
},
'Enter http or https',
'rpc url',
'JSON RPC provider',
);
const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
const name = await detectAndConfirmOrPrompt(
async () => {
const clientName = await provider.send('web3_clientVersion', []);
const port = rpcUrl.split(':').slice(-1);
const client = clientName.split('/')[0];
return `${client}${port}`;
},
'Enter (one word, lower case)',
'chain name',
);
const name = await input({
message: 'Enter chain name (one word, lower case)',
validate: (chainName) => ZChainName.safeParse(chainName).success,
});
const displayName = await input({
message: 'Enter chain display name',
default: name[0].toUpperCase() + name.slice(1),
});
const chainId = parseInt(
await detectAndConfirmOrPrompt(
@ -70,40 +75,67 @@ export async function createChainConfig({
},
'Enter a (number)',
'chain id',
'JSON RPC provider',
),
10,
);
const metadata: ChainMetadata = {
name,
displayName,
chainId,
domainId: chainId,
protocol: ProtocolType.Ethereum,
rpcUrls: [{ http: rpcUrl }],
};
const wantAdvancedConfig = await confirm({
await addBlockOrGasConfig(metadata);
await addNativeTokenConfig(metadata);
const parseResult = ChainMetadataSchema.safeParse(metadata);
if (parseResult.success) {
logGreen(`Chain config is valid, writing to registry:`);
const metadataYaml = yamlStringify(metadata, null, 2);
log(indentYamlOrJson(metadataYaml, 4));
await context.registry.updateChain({ chainName: metadata.name, metadata });
} else {
errorRed(
`Chain config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/chain-config.yaml for an example`,
);
errorRed(JSON.stringify(parseResult.error.errors));
throw new Error('Invalid chain config');
}
}
async function addBlockOrGasConfig(metadata: ChainMetadata): Promise<void> {
const wantBlockOrGasConfig = await confirm({
default: false,
message:
'Do you want to set block or gas properties for this chain config?',
message: 'Do you want to set block or gas properties for this chain config',
});
if (wantAdvancedConfig) {
if (wantBlockOrGasConfig) {
await addBlockConfig(metadata);
await addGasConfig(metadata);
}
}
async function addBlockConfig(metadata: ChainMetadata): Promise<void> {
const wantBlockConfig = await confirm({
message: 'Do you want to add block config for this chain?',
message: 'Do you want to add block config for this chain',
});
if (wantBlockConfig) {
const blockConfirmation = await input({
message:
'Enter no. of blocks to wait before considering a transaction confirmed(0-500)',
'Enter no. of blocks to wait before considering a transaction confirmed (0-500):',
validate: (value) => parseInt(value) >= 0 && parseInt(value) <= 500,
});
const blockReorgPeriod = await input({
message:
'Enter no. of blocks before a transaction has a near-zero chance of reverting(0-500)',
'Enter no. of blocks before a transaction has a near-zero chance of reverting (0-500):',
validate: (value) => parseInt(value) >= 0 && parseInt(value) <= 500,
});
const blockTimeEstimate = await input({
message: 'Enter the rough estimate of time per block in seconds(0-20)',
message: 'Enter the rough estimate of time per block in seconds (0-20):',
validate: (value) => parseInt(value) >= 0 && parseInt(value) <= 20,
});
metadata.blocks = {
@ -112,19 +144,22 @@ export async function createChainConfig({
estimateBlockTime: parseInt(blockTimeEstimate, 10),
};
}
}
async function addGasConfig(metadata: ChainMetadata): Promise<void> {
const wantGasConfig = await confirm({
message: 'Do you want to add gas config for this chain?',
message: 'Do you want to add gas config for this chain',
});
if (wantGasConfig) {
const isEIP1559 = await confirm({
message: 'Is your chain an EIP1559 enabled?',
message: 'Is your chain an EIP1559 enabled',
});
if (isEIP1559) {
const maxFeePerGas = await input({
message: 'Enter the max fee per gas in gwei',
message: 'Enter the max fee per gas (gwei):',
});
const maxPriorityFeePerGas = await input({
message: 'Enter the max priority fee per gas in gwei',
message: 'Enter the max priority fee per gas (gwei):',
});
metadata.transactionOverrides = {
maxFeePerGas: BigInt(maxFeePerGas) * BigInt(10 ** 9),
@ -132,23 +167,37 @@ export async function createChainConfig({
};
} else {
const gasPrice = await input({
message: 'Enter the gas price in gwei',
message: 'Enter the gas price (gwei):',
});
metadata.transactionOverrides = {
gasPrice: BigInt(gasPrice) * BigInt(10 ** 9),
};
}
}
}
async function addNativeTokenConfig(metadata: ChainMetadata): Promise<void> {
const wantNativeConfig = await confirm({
default: false,
message:
'Do you want to set native token properties for this chain config (defaults to ETH)',
});
let symbol, name, decimals;
if (wantNativeConfig) {
symbol = await input({
message: "Enter the native token's symbol:",
});
name = await input({
message: `Enter the native token's name:`,
});
decimals = await input({
message: "Enter the native token's decimals:",
});
}
const parseResult = ChainMetadataSchema.safeParse(metadata);
if (parseResult.success) {
logGreen(`Chain config is valid, writing to registry`);
await context.registry.updateChain({ chainName: metadata.name, metadata });
} else {
errorRed(
`Chain config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/chain-config.yaml for an example`,
);
errorRed(JSON.stringify(parseResult.error.errors));
throw new Error('Invalid chain config');
}
metadata.nativeToken = {
symbol: symbol ?? 'ETH',
name: name ?? 'Ether',
decimals: decimals ? parseInt(decimals, 10) : 18,
};
}

@ -0,0 +1,71 @@
import { stringify as yamlStringify } from 'yaml';
import { CoreConfigSchema, HookConfig, IsmConfig } from '@hyperlane-xyz/sdk';
import { CommandContext } from '../context/types.js';
import { errorRed, log, logBlue, logGreen } from '../logger.js';
import { indentYamlOrJson, writeYamlOrJson } from '../utils/files.js';
import { detectAndConfirmOrPrompt } from '../utils/input.js';
import {
createHookConfig,
createMerkleTreeConfig,
createProtocolFeeConfig,
} from './hooks.js';
import { createAdvancedIsmConfig, createTrustedRelayerConfig } from './ism.js';
export async function createCoreDeployConfig({
context,
configFilePath,
advanced = false,
}: {
context: CommandContext;
configFilePath: string;
advanced: boolean;
}) {
logBlue('Creating a new core deployment config...');
const owner = await detectAndConfirmOrPrompt(
async () => context.signer?.getAddress(),
'Enter the desired',
'owner address',
'signer',
);
const defaultIsm: IsmConfig = advanced
? await createAdvancedIsmConfig(context)
: await createTrustedRelayerConfig(context, advanced);
let defaultHook: HookConfig, requiredHook: HookConfig;
if (advanced) {
defaultHook = await createHookConfig({
context,
selectMessage: 'Select default hook type',
advanced,
});
requiredHook = await createHookConfig({
context,
selectMessage: 'Select required hook type',
advanced,
});
} else {
defaultHook = await createMerkleTreeConfig();
requiredHook = await createProtocolFeeConfig(context, advanced);
}
try {
const coreConfig = CoreConfigSchema.parse({
owner,
defaultIsm,
defaultHook,
requiredHook,
});
logBlue(`Core config is valid, writing to file ${configFilePath}:\n`);
log(indentYamlOrJson(yamlStringify(coreConfig, null, 2), 4));
writeYamlOrJson(configFilePath, coreConfig, 'yaml');
logGreen('✅ Successfully created new core deployment config.');
} catch (e) {
errorRed(`Core deployment config is invalid.`);
throw e;
}
}

@ -6,9 +6,9 @@ import { z } from 'zod';
import {
ChainMap,
ChainName,
GasOracleContractType,
HookConfig,
HookConfigSchema,
HookType,
HooksConfig,
} from '@hyperlane-xyz/sdk';
import {
Address,
@ -18,65 +18,24 @@ import {
} from '@hyperlane-xyz/utils';
import { CommandContext } from '../context/types.js';
import { errorRed, log, logBlue, logGreen, logRed } from '../logger.js';
import { errorRed, logBlue, logGreen, logRed } from '../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js';
import { mergeYamlOrJson, readYamlOrJson } from '../utils/files.js';
import { readYamlOrJson } from '../utils/files.js';
import { detectAndConfirmOrPrompt, inputWithInfo } from '../utils/input.js';
const ProtocolFeeSchema = z.object({
type: z.literal(HookType.PROTOCOL_FEE),
owner: z.string(),
beneficiary: z.string(),
maxProtocolFee: z.string(),
protocolFee: z.string(),
});
const MerkleTreeSchema = z.object({
type: z.literal(HookType.MERKLE_TREE),
});
const IGPSchema = z.object({
type: z.literal(HookType.INTERCHAIN_GAS_PAYMASTER),
owner: z.string(),
beneficiary: z.string(),
overhead: z.record(z.number()),
gasOracleType: z.record(z.literal(GasOracleContractType.StorageGasOracle)),
oracleKey: z.string(),
});
const RoutingConfigSchema: z.ZodSchema<any> = z.lazy(() =>
z.object({
type: z.literal(HookType.ROUTING),
owner: z.string(),
domains: z.record(HookConfigSchema),
}),
);
const AggregationConfigSchema: z.ZodSchema<any> = z.lazy(() =>
z.object({
type: z.literal(HookType.AGGREGATION),
hooks: z.array(HookConfigSchema),
}),
);
const HookConfigSchema = z.union([
ProtocolFeeSchema,
MerkleTreeSchema,
IGPSchema,
RoutingConfigSchema,
AggregationConfigSchema,
]);
export type HookConfig = z.infer<typeof HookConfigSchema>;
import { callWithConfigCreationLogs } from './utils.js';
// TODO: deprecate in favor of CoreConfigSchema
const HooksConfigSchema = z.object({
required: HookConfigSchema,
default: HookConfigSchema,
required: HookConfigSchema,
});
export type HooksConfig = z.infer<typeof HooksConfigSchema>;
const HooksConfigMapSchema = z.record(HooksConfigSchema);
export type HooksConfigMap = z.infer<typeof HooksConfigMapSchema>;
export function isValidHookConfigMap(config: any) {
return HooksConfigMapSchema.safeParse(config).success;
}
const MAX_PROTOCOL_FEE_DEFAULT: string = toWei('0.1');
const PROTOCOL_FEE_DEFAULT: string = toWei('0');
export function presetHookConfigs(owner: Address): HooksConfig {
return {
@ -99,14 +58,7 @@ export function readHooksConfigMap(filePath: string) {
logRed(`No hook config found at ${filePath}`);
return;
}
const result = HooksConfigMapSchema.safeParse(config);
if (!result.success) {
const firstIssue = result.error.issues[0];
throw new Error(
`Invalid hook config: ${firstIssue.path} => ${firstIssue.message}`,
);
}
const parsedConfig = result.data;
const parsedConfig = HooksConfigMapSchema.parse(config);
const hooks: ChainMap<HooksConfig> = objMap(
parsedConfig,
(_, config) => config as HooksConfig,
@ -115,47 +67,24 @@ export function readHooksConfigMap(filePath: string) {
return hooks;
}
export async function createHooksConfigMap({
export async function createHookConfig({
context,
outPath,
selectMessage = 'Select hook type',
advanced = false,
}: {
context: CommandContext;
outPath: string;
}) {
logBlue('Creating a new hook config');
const chains = await runMultiChainSelectionStep(context.chainMetadata);
const result: HooksConfigMap = {};
for (const chain of chains) {
for (const hookRequirements of ['required', 'default']) {
log(`Setting ${hookRequirements} hook for chain ${chain}`);
const remotes = chains.filter((c) => c !== chain);
result[chain] = {
...result[chain],
[hookRequirements]: await createHookConfig(context, chain, remotes),
};
}
if (isValidHookConfigMap(result)) {
logGreen(`Hook config is valid, writing to file ${outPath}`);
mergeYamlOrJson(outPath, result);
} else {
errorRed(
`Hook config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/hooks.yaml for an example`,
);
throw new Error('Invalid hook config');
}
}
}
export async function createHookConfig(
context: CommandContext,
chain: ChainName,
remotes: ChainName[],
): Promise<HookConfig> {
let lastConfig: HookConfig;
selectMessage?: string;
advanced?: boolean;
}): Promise<HookConfig> {
const hookType = await select({
message: 'Select hook type',
message: selectMessage,
choices: [
{
value: HookType.AGGREGATION,
name: HookType.AGGREGATION,
description:
'Aggregate multiple hooks into a single hook (e.g. merkle tree + IGP) which will be called in sequence',
},
{
value: HookType.MERKLE_TREE,
name: HookType.MERKLE_TREE,
@ -167,147 +96,142 @@ export async function createHookConfig(
name: HookType.PROTOCOL_FEE,
description: 'Charge fees for each message dispatch from this chain',
},
{
value: HookType.INTERCHAIN_GAS_PAYMASTER,
name: HookType.INTERCHAIN_GAS_PAYMASTER,
description:
'Allow for payments for expected gas to be paid by the relayer while delivering on remote chain',
},
{
value: HookType.AGGREGATION,
name: HookType.AGGREGATION,
description:
'Aggregate multiple hooks into a single hook (e.g. merkle tree + IGP) which will be called in sequence',
},
{
value: HookType.ROUTING,
name: HookType.ROUTING,
description:
'Each destination domain can have its own hook configured via DomainRoutingHook',
},
],
pageSize: 10,
});
if (hookType === HookType.MERKLE_TREE) {
lastConfig = { type: HookType.MERKLE_TREE };
} else if (hookType === HookType.PROTOCOL_FEE) {
lastConfig = await createProtocolFeeConfig(context, chain);
} else if (hookType === HookType.INTERCHAIN_GAS_PAYMASTER) {
lastConfig = await createIGPConfig(remotes);
} else if (hookType === HookType.AGGREGATION) {
lastConfig = await createAggregationConfig(context, chain, remotes);
} else if (hookType === HookType.ROUTING) {
lastConfig = await createRoutingConfig(context, chain, remotes);
} else {
switch (hookType) {
case HookType.AGGREGATION:
return createAggregationConfig(context, advanced);
case HookType.MERKLE_TREE:
return createMerkleTreeConfig();
case HookType.PROTOCOL_FEE:
return createProtocolFeeConfig(context, advanced);
default:
throw new Error(`Invalid hook type: ${hookType}`);
}
return lastConfig;
}
export async function createProtocolFeeConfig(
export const createMerkleTreeConfig = callWithConfigCreationLogs(
async (): Promise<HookConfig> => {
return { type: HookType.MERKLE_TREE };
},
HookType.MERKLE_TREE,
);
export const createProtocolFeeConfig = callWithConfigCreationLogs(
async (
context: CommandContext,
chain: ChainName,
): Promise<HookConfig> {
const owner = await input({
message: 'Enter owner address',
});
const ownerAddress = normalizeAddressEvm(owner);
let beneficiary;
let sameAsOwner = false;
sameAsOwner = await confirm({
message: 'Use this same address for the beneficiary?',
});
if (sameAsOwner) {
beneficiary = ownerAddress;
} else {
beneficiary = await input({
message: 'Enter beneficiary address',
advanced: boolean = false,
): Promise<HookConfig> => {
const unnormalizedOwner =
!advanced && context.signer
? await context.signer.getAddress()
: await detectAndConfirmOrPrompt(
async () => context.signer?.getAddress(),
'For protocol fee hook, enter',
'owner address',
'signer',
);
const owner = normalizeAddressEvm(unnormalizedOwner);
let beneficiary = owner;
const isBeneficiarySameAsOwner = advanced
? await confirm({
message: `Use this same address (${owner}) for the beneficiary?`,
})
: true;
if (!isBeneficiarySameAsOwner) {
const unnormalizedBeneficiary = await input({
message: 'Enter beneficiary address for protocol fee hook:',
});
beneficiary = normalizeAddressEvm(unnormalizedBeneficiary);
}
const beneficiaryAddress = normalizeAddressEvm(beneficiary);
// TODO: input in gwei, wei, etc
const maxProtocolFee = toWei(
await input({
message: `Enter max protocol fee ${nativeTokenAndDecimals(
context,
chain,
)} e.g. 1.0)`,
const maxProtocolFee = advanced
? toWei(
await inputWithInfo({
message: `Enter max protocol fee for protocol fee hook (wei):`,
info: `The max protocol fee (ProtocolFee.MAX_PROTOCOL_FEE) is the maximum value the protocol fee on the ProtocolFee hook contract can ever be set to.\nDefault is set to ${MAX_PROTOCOL_FEE_DEFAULT} wei; between 0.001 and 0.1 wei is recommended.`,
}),
);
const protocolFee = toWei(
await input({
message: `Enter protocol fee in ${nativeTokenAndDecimals(
context,
chain,
)} e.g. 0.01)`,
)
: MAX_PROTOCOL_FEE_DEFAULT;
const protocolFee = advanced
? toWei(
await inputWithInfo({
message: `Enter protocol fee for protocol fee hook (wei):`,
info: `The protocol fee is the fee collected by the beneficiary of the ProtocolFee hook for every transaction executed with this hook.\nDefault is set to 0 wei; must be less than max protocol fee of ${maxProtocolFee}.`,
}),
);
)
: PROTOCOL_FEE_DEFAULT;
if (BigNumberJs(protocolFee).gt(maxProtocolFee)) {
errorRed('Protocol fee cannot be greater than max protocol fee');
throw new Error('Invalid protocol fee');
errorRed(
`Protocol fee (${protocolFee}) cannot be greater than max protocol fee (${maxProtocolFee}).`,
);
throw new Error(`Invalid protocol fee (${protocolFee}).`);
}
return {
type: HookType.PROTOCOL_FEE,
maxProtocolFee: maxProtocolFee.toString(),
protocolFee: protocolFee.toString(),
beneficiary: beneficiaryAddress,
owner: ownerAddress,
maxProtocolFee,
protocolFee,
beneficiary,
owner,
};
}
},
HookType.PROTOCOL_FEE,
);
export async function createIGPConfig(
remotes: ChainName[],
): Promise<HookConfig> {
const owner = await input({
message: 'Enter owner address',
// TODO: make this usable
export const createIGPConfig = callWithConfigCreationLogs(
async (remotes: ChainName[]): Promise<HookConfig> => {
const unnormalizedOwner = await input({
message: 'Enter owner address for IGP hook',
});
const ownerAddress = normalizeAddressEvm(owner);
let beneficiary, oracleKey;
let sameAsOwner = false;
sameAsOwner = await confirm({
const owner = normalizeAddressEvm(unnormalizedOwner);
let beneficiary = owner;
let oracleKey = owner;
const beneficiarySameAsOwner = await confirm({
message: 'Use this same address for the beneficiary and gasOracleKey?',
});
if (sameAsOwner) {
beneficiary = ownerAddress;
oracleKey = ownerAddress;
} else {
beneficiary = await input({
message: 'Enter beneficiary address',
if (!beneficiarySameAsOwner) {
const unnormalizedBeneficiary = await input({
message: 'Enter beneficiary address for IGP hook',
});
oracleKey = await input({
message: 'Enter gasOracleKey address',
beneficiary = normalizeAddressEvm(unnormalizedBeneficiary);
const unnormalizedOracleKey = await input({
message: 'Enter gasOracleKey address for IGP hook',
});
oracleKey = normalizeAddressEvm(unnormalizedOracleKey);
}
const beneficiaryAddress = normalizeAddressEvm(beneficiary);
const oracleKeyAddress = normalizeAddressEvm(oracleKey);
const overheads: ChainMap<number> = {};
for (const chain of remotes) {
const overhead = parseInt(
await input({
message: `Enter overhead for ${chain} (eg 75000)`,
message: `Enter overhead for ${chain} (eg 75000) for IGP hook`,
}),
);
overheads[chain] = overhead;
}
return {
type: HookType.INTERCHAIN_GAS_PAYMASTER,
beneficiary: beneficiaryAddress,
owner: ownerAddress,
oracleKey: oracleKeyAddress,
beneficiary,
owner,
oracleKey,
overhead: overheads,
gasOracleType: objMap(
overheads,
() => GasOracleContractType.StorageGasOracle,
),
oracleConfig: {},
};
}
},
HookType.INTERCHAIN_GAS_PAYMASTER,
);
export async function createAggregationConfig(
export const createAggregationConfig = callWithConfigCreationLogs(
async (
context: CommandContext,
chain: ChainName,
remotes: ChainName[],
): Promise<HookConfig> {
advanced: boolean = false,
): Promise<HookConfig> => {
const hooksNum = parseInt(
await input({
message: 'Enter the number of hooks to aggregate (number)',
@ -317,30 +241,38 @@ export async function createAggregationConfig(
const hooks: Array<HookConfig> = [];
for (let i = 0; i < hooksNum; i++) {
logBlue(`Creating hook ${i + 1} of ${hooksNum} ...`);
hooks.push(await createHookConfig(context, chain, remotes));
hooks.push(
await createHookConfig({
context,
advanced,
}),
);
}
return {
type: HookType.AGGREGATION,
hooks,
};
}
},
HookType.AGGREGATION,
);
export async function createRoutingConfig(
export const createRoutingConfig = callWithConfigCreationLogs(
async (
context: CommandContext,
origin: ChainName,
remotes: ChainName[],
): Promise<HookConfig> {
advanced: boolean = false,
): Promise<HookConfig> => {
const owner = await input({
message: 'Enter owner address',
message: 'Enter owner address for routing ISM',
});
const ownerAddress = owner;
const chains = await runMultiChainSelectionStep(context.chainMetadata);
const domainsMap: ChainMap<HookConfig> = {};
for (const chain of remotes) {
for (const chain of chains) {
await confirm({
message: `You are about to configure hook for remote chain ${chain}. Continue?`,
});
const config = await createHookConfig(context, origin, remotes);
const config = await createHookConfig({ context, advanced });
domainsMap[chain] = config;
}
return {
@ -348,12 +280,6 @@ export async function createRoutingConfig(
owner: ownerAddress,
domains: domainsMap,
};
}
function nativeTokenAndDecimals(context: CommandContext, chain: ChainName) {
return `10^${
context.chainMetadata[chain].nativeToken?.decimals ?? '18'
} which you cannot exceed (in ${
context.chainMetadata[chain].nativeToken?.symbol ?? 'eth'
}`;
}
},
HookType.ROUTING,
);

@ -1,7 +1,15 @@
import { confirm, input, select } from '@inquirer/prompts';
import { input, select } from '@inquirer/prompts';
import { z } from 'zod';
import { ChainMap, ChainName, IsmType, ZHash } from '@hyperlane-xyz/sdk';
import {
AggregationIsmConfig,
ChainMap,
IsmConfig,
IsmConfigSchema,
IsmType,
MultisigIsmConfig,
TrustedRelayerIsmConfig,
} from '@hyperlane-xyz/sdk';
import { CommandContext } from '../context/types.js';
import {
@ -9,70 +17,22 @@ import {
log,
logBlue,
logBoldUnderlinedRed,
logGreen,
logRed,
} from '../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js';
import { mergeYamlOrJson, readYamlOrJson } from '../utils/files.js';
const MultisigIsmConfigSchema = z.object({
type: z.union([
z.literal(IsmType.MERKLE_ROOT_MULTISIG),
z.literal(IsmType.MESSAGE_ID_MULTISIG),
]),
threshold: z.number(),
validators: z.array(ZHash),
});
const RoutingIsmConfigSchema: z.ZodSchema<any> = z.lazy(() =>
z.object({
type: z.union([
z.literal(IsmType.ROUTING),
z.literal(IsmType.FALLBACK_ROUTING),
]),
owner: ZHash,
domains: z.record(IsmConfigSchema),
}),
);
const AggregationIsmConfigSchema: z.ZodSchema<any> = z
.lazy(() =>
z.object({
type: z.literal(IsmType.AGGREGATION),
modules: z.array(IsmConfigSchema),
threshold: z.number(),
}),
)
.refine(
// check ig modules.length >= threshold
(ismConfig) => {
return ismConfig.modules.length >= ismConfig.threshold;
},
{
message: 'Threshold cannot be greater than number of modules',
},
);
import { readYamlOrJson } from '../utils/files.js';
import { detectAndConfirmOrPrompt } from '../utils/input.js';
const TestIsmConfigSchema = z.object({
type: z.literal(IsmType.TEST_ISM),
});
import { callWithConfigCreationLogs } from './utils.js';
const TrustedRelayerIsmConfigSchema = z.object({
type: z.literal(IsmType.TRUSTED_RELAYER),
relayer: ZHash,
});
const IsmConfigSchema = z.union([
MultisigIsmConfigSchema,
RoutingIsmConfigSchema,
AggregationIsmConfigSchema,
TestIsmConfigSchema,
TrustedRelayerIsmConfigSchema,
]);
const IsmConfigMapSchema = z.record(IsmConfigSchema).refine(
(ismConfigMap) => {
// check if any key in IsmConfigMap is found in its own RoutingIsmConfigSchema.domains
for (const [key, config] of Object.entries(ismConfigMap)) {
if (typeof config === 'string') {
continue;
}
if (config.type === IsmType.ROUTING) {
if (config.domains && key in config.domains) {
return false;
@ -86,8 +46,6 @@ const IsmConfigMapSchema = z.record(IsmConfigSchema).refine(
'Cannot set RoutingIsm.domain to the same chain you are configuring',
},
);
export type ZodIsmConfig = z.infer<typeof IsmConfigSchema>;
export type ZodIsmConfigMap = z.infer<typeof IsmConfigMapSchema>;
export function parseIsmConfig(filePath: string) {
const config = readYamlOrJson(filePath);
@ -107,149 +65,134 @@ export function readIsmConfig(filePath: string) {
return parsedConfig;
}
export function isValildIsmConfig(config: any) {
return IsmConfigMapSchema.safeParse(config).success;
}
const ISM_TYPE_DESCRIPTIONS: Record<string, string> = {
[IsmType.AGGREGATION]:
'You can aggregate multiple ISMs into one ISM via AggregationISM',
[IsmType.FALLBACK_ROUTING]:
"You can specify ISM type for specific chains you like and fallback to mailbox's default ISM for other chains via DefaultFallbackRoutingISM",
[IsmType.MERKLE_ROOT_MULTISIG]:
'Validators need to sign the root of the merkle tree of all messages from origin chain',
[IsmType.MESSAGE_ID_MULTISIG]: 'Validators need to sign just this messageId',
[IsmType.ROUTING]:
'Each origin chain can be verified by the specified ISM type via RoutingISM',
[IsmType.TEST_ISM]:
'ISM where you can deliver messages without any validation (WARNING: only for testing, do not use in production)',
[IsmType.TRUSTED_RELAYER]: 'Deliver messages from an authorized address',
};
export async function createIsmConfigMap({
context,
outPath,
}: {
context: CommandContext;
outPath: string;
}) {
export async function createAdvancedIsmConfig(
context: CommandContext,
): Promise<IsmConfig> {
logBlue('Creating a new advanced ISM config');
logBoldUnderlinedRed('WARNING: USE AT YOUR RISK.');
logRed(
'Advanced ISM configs require knowledge of different ISM types and how they work together topologically. If possible, use the basic ISM configs are recommended.',
);
const chains = await runMultiChainSelectionStep(
context.chainMetadata,
'Select chains to configure ISM for',
true,
);
const result: ZodIsmConfigMap = {};
for (const chain of chains) {
log(`Setting values for chain ${chain}`);
result[chain] = await createIsmConfig(chain, chains);
const moduleType = await select({
message: 'Select ISM type',
choices: Object.entries(ISM_TYPE_DESCRIPTIONS).map(
([value, description]) => ({
value,
description,
}),
),
pageSize: 10,
});
// TODO consider re-enabling. Disabling based on feedback from @nambrot for now.
// repeat = await confirm({
// message: 'Use this same config for remaining chains?',
// });
switch (moduleType) {
case IsmType.AGGREGATION:
return createAggregationConfig(context);
case IsmType.FALLBACK_ROUTING:
return createFallbackRoutingConfig(context);
case IsmType.MERKLE_ROOT_MULTISIG:
return createMerkleRootMultisigConfig(context);
case IsmType.MESSAGE_ID_MULTISIG:
return createMessageIdMultisigConfig(context);
case IsmType.ROUTING:
return createRoutingConfig(context);
case IsmType.TEST_ISM:
return { type: IsmType.TEST_ISM };
case IsmType.TRUSTED_RELAYER:
return createTrustedRelayerConfig(context, true);
default:
throw new Error(`Invalid ISM type: ${moduleType}.`);
}
}
if (isValildIsmConfig(result)) {
logGreen(`ISM config is valid, writing to file ${outPath}`);
mergeYamlOrJson(outPath, result);
} else {
export const createMerkleRootMultisigConfig = callWithConfigCreationLogs(
async (): Promise<MultisigIsmConfig> => {
const validatorsInput = await input({
message:
'Enter validator addresses (comma separated list) for merkle root multisig ISM:',
});
const validators = validatorsInput.split(',').map((v) => v.trim());
const thresholdInput = await input({
message:
'Enter threshold of validators (number) for merkle root multisig ISM:',
});
const threshold = parseInt(thresholdInput, 10);
if (threshold > validators.length) {
errorRed(
`ISM config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/ism.yaml for an example`,
`Merkle root multisig signer threshold (${threshold}) cannot be greater than total number of validators (${validators.length}).`,
);
throw new Error('Invalid ISM config');
throw new Error('Invalid protocol fee.');
}
}
export async function createIsmConfig(
remote: ChainName,
origins: ChainName[],
): Promise<ZodIsmConfig> {
let lastConfig: ZodIsmConfig;
const moduleType = await select({
message: 'Select ISM type',
choices: [
{
value: IsmType.MESSAGE_ID_MULTISIG,
description: 'Validators need to sign just this messageId',
},
{
value: IsmType.MERKLE_ROOT_MULTISIG,
description:
'Validators need to sign the root of the merkle tree of all messages from origin chain',
},
{
value: IsmType.ROUTING,
description:
'Each origin chain can be verified by the specified ISM type via RoutingISM',
},
{
value: IsmType.FALLBACK_ROUTING,
description:
"You can specify ISM type for specific chains you like and fallback to mailbox's default ISM for other chains via DefaultFallbackRoutingISM",
},
{
value: IsmType.AGGREGATION,
description:
'You can aggregate multiple ISMs into one ISM via AggregationISM',
},
{
value: IsmType.TRUSTED_RELAYER,
description: 'Deliver messages from an authorized address',
},
{
value: IsmType.TEST_ISM,
description:
'ISM where you can deliver messages without any validation (WARNING: only for testing, do not use in production)',
return {
type: IsmType.MERKLE_ROOT_MULTISIG,
threshold,
validators,
};
},
],
pageSize: 10,
});
if (
moduleType === IsmType.MESSAGE_ID_MULTISIG ||
moduleType === IsmType.MERKLE_ROOT_MULTISIG
) {
lastConfig = await createMultisigConfig(moduleType);
} else if (
moduleType === IsmType.ROUTING ||
moduleType === IsmType.FALLBACK_ROUTING
) {
lastConfig = await createRoutingConfig(moduleType, remote, origins);
} else if (moduleType === IsmType.AGGREGATION) {
lastConfig = await createAggregationConfig(remote, origins);
} else if (moduleType === IsmType.TEST_ISM) {
lastConfig = { type: IsmType.TEST_ISM };
} else if (moduleType === IsmType.TRUSTED_RELAYER) {
lastConfig = await createTrustedRelayerConfig();
} else {
throw new Error(`Invalid ISM type: ${moduleType}}`);
}
return lastConfig;
}
IsmType.MERKLE_ROOT_MULTISIG,
);
export async function createMultisigConfig(
type: IsmType.MERKLE_ROOT_MULTISIG | IsmType.MESSAGE_ID_MULTISIG,
): Promise<ZodIsmConfig> {
export const createMessageIdMultisigConfig = callWithConfigCreationLogs(
async (): Promise<MultisigIsmConfig> => {
const thresholdInput = await input({
message: 'Enter threshold of validators (number)',
message:
'Enter threshold of validators (number) for message ID multisig ISM',
});
const threshold = parseInt(thresholdInput, 10);
const validatorsInput = await input({
message: 'Enter validator addresses (comma separated list)',
message:
'Enter validator addresses (comma separated list) for message ID multisig ISM',
});
const validators = validatorsInput.split(',').map((v) => v.trim());
return {
type,
type: IsmType.MESSAGE_ID_MULTISIG,
threshold,
validators,
};
}
},
IsmType.MESSAGE_ID_MULTISIG,
);
async function createTrustedRelayerConfig(): Promise<ZodIsmConfig> {
const relayer = await input({
message: 'Enter relayer address',
});
export const createTrustedRelayerConfig = callWithConfigCreationLogs(
async (
context: CommandContext,
advanced: boolean = false,
): Promise<TrustedRelayerIsmConfig> => {
const relayer =
!advanced && context.signer
? await context.signer.getAddress()
: await detectAndConfirmOrPrompt(
async () => context.signer?.getAddress(),
'For trusted relayer ISM, enter',
'relayer address',
'signer',
);
return {
type: IsmType.TRUSTED_RELAYER,
relayer,
};
}
},
IsmType.TRUSTED_RELAYER,
);
export async function createAggregationConfig(
remote: ChainName,
chains: ChainName[],
): Promise<ZodIsmConfig> {
export const createAggregationConfig = callWithConfigCreationLogs(
async (context: CommandContext): Promise<AggregationIsmConfig> => {
const isms = parseInt(
await input({
message: 'Enter the number of ISMs to aggregate (number)',
@ -259,44 +202,73 @@ export async function createAggregationConfig(
const threshold = parseInt(
await input({
message: 'Enter the threshold of ISMs to for verification (number)',
message: 'Enter the threshold of ISMs for verification (number)',
}),
10,
);
const modules: Array<ZodIsmConfig> = [];
const modules: Array<IsmConfig> = [];
for (let i = 0; i < isms; i++) {
modules.push(await createIsmConfig(remote, chains));
modules.push(await createAdvancedIsmConfig(context));
}
return {
type: IsmType.AGGREGATION,
modules,
threshold,
};
}
},
IsmType.AGGREGATION,
);
export async function createRoutingConfig(
type: IsmType.ROUTING | IsmType.FALLBACK_ROUTING,
remote: ChainName,
chains: ChainName[],
): Promise<ZodIsmConfig> {
export const createRoutingConfig = callWithConfigCreationLogs(
async (context: CommandContext): Promise<IsmConfig> => {
const owner = await input({
message: 'Enter owner address',
message: 'Enter owner address for routing ISM',
});
const ownerAddress = owner;
const origins = chains.filter((chain) => chain !== remote);
const requireMultiple = true;
const chains = await runMultiChainSelectionStep(
context.chainMetadata,
'Select chains to configure routing ISM for',
requireMultiple,
);
const domainsMap: ChainMap<ZodIsmConfig> = {};
for (const chain of origins) {
await confirm({
message: `You are about to configure ISM from source chain ${chain}. Continue?`,
});
const config = await createIsmConfig(chain, chains);
const domainsMap: ChainMap<IsmConfig> = {};
for (const chain of chains) {
log(`You are about to configure routing ISM from source chain ${chain}.`);
const config = await createAdvancedIsmConfig(context);
domainsMap[chain] = config;
}
return {
type,
type: IsmType.ROUTING,
owner: ownerAddress,
domains: domainsMap,
};
}
},
IsmType.ROUTING,
);
export const createFallbackRoutingConfig = callWithConfigCreationLogs(
async (context: CommandContext): Promise<IsmConfig> => {
const chains = await runMultiChainSelectionStep(
context.chainMetadata,
'Select chains to configure fallback routing ISM for',
true,
);
const domainsMap: ChainMap<IsmConfig> = {};
for (const chain of chains) {
log(
`You are about to configure fallback routing ISM from source chain ${chain}.`,
);
const config = await createAdvancedIsmConfig(context);
domainsMap[chain] = config;
}
return {
type: IsmType.FALLBACK_ROUTING,
owner: '',
domains: domainsMap,
};
},
IsmType.FALLBACK_ROUTING,
);

@ -0,0 +1,18 @@
import { HookConfig, HookType, IsmConfig, IsmType } from '@hyperlane-xyz/sdk';
import { logGray } from '../logger.js';
export function callWithConfigCreationLogs<T extends IsmConfig | HookConfig>(
fn: (...args: any[]) => Promise<T>,
type: IsmType | HookType,
) {
return async (...args: any[]): Promise<T> => {
logGray(`Creating ${type}...`);
try {
const result = await fn(...args);
return result;
} finally {
logGray(`Created ${type}!`);
}
};
}

@ -1,7 +1,10 @@
import { input, select } from '@inquirer/prompts';
import { stringify as yamlStringify } from 'yaml';
import {
ChainMap,
IsmConfig,
IsmType,
MailboxClientConfig,
TokenType,
WarpCoreConfig,
@ -9,15 +12,19 @@ import {
WarpRouteDeployConfig,
WarpRouteDeployConfigSchema,
} from '@hyperlane-xyz/sdk';
import { assert, objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import { Address, assert, objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import { CommandContext } from '../context/types.js';
import { errorRed, logBlue, logGreen } from '../logger.js';
import { errorRed, log, logBlue, logGreen } from '../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js';
import {
detectAndConfirmOrPrompt,
runMultiChainSelectionStep,
} from '../utils/chains.js';
import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js';
indentYamlOrJson,
readYamlOrJson,
writeYamlOrJson,
} from '../utils/files.js';
import { detectAndConfirmOrPrompt } from '../utils/input.js';
import { createAdvancedIsmConfig } from './ism.js';
const TYPE_DESCRIPTIONS: Record<TokenType, string> = {
[TokenType.synthetic]: 'A new ERC20 with remote transfer functionality',
@ -94,16 +101,19 @@ export function isValidWarpRouteDeployConfig(config: any) {
export async function createWarpRouteDeployConfig({
context,
outPath,
advanced = false,
}: {
context: CommandContext;
outPath: string;
advanced: boolean;
}) {
logBlue('Creating a new warp route deployment config');
logBlue('Creating a new warp route deployment config...');
const owner = await detectAndConfirmOrPrompt(
async () => context.signer?.getAddress(),
'Enter the desired',
'owner address',
'signer',
);
const warpChains = await runMultiChainSelectionStep(
@ -130,8 +140,13 @@ export async function createWarpRouteDeployConfig({
},
`For ${chain}, enter the`,
'mailbox address',
'hyperlane-registry',
);
const interchainSecurityModule = advanced
? await createAdvancedIsmConfig(context)
: createDefaultWarpIsmConfig(owner);
switch (type) {
case TokenType.collateral:
case TokenType.XERC20:
@ -145,23 +160,32 @@ export async function createWarpRouteDeployConfig({
type,
owner,
isNft,
interchainSecurityModule,
token: await input({
message: `Enter the existing token address on chain ${chain}`,
}),
};
break;
default:
result[chain] = { mailbox, type, owner, isNft };
result[chain] = {
mailbox,
type,
owner,
isNft,
interchainSecurityModule,
};
}
}
try {
const parsed = WarpRouteDeployConfigSchema.parse(result);
logGreen(`Warp Route config is valid, writing to file ${outPath}`);
writeYamlOrJson(outPath, parsed);
const warpRouteDeployConfig = WarpRouteDeployConfigSchema.parse(result);
logBlue(`Warp Route config is valid, writing to file ${outPath}:\n`);
log(indentYamlOrJson(yamlStringify(warpRouteDeployConfig, null, 2), 4));
writeYamlOrJson(outPath, warpRouteDeployConfig, 'yaml');
logGreen('✅ Successfully created new warp route deployment config.');
} catch (e) {
errorRed(
`Warp route deployment config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/warp-route-deployment.yaml for an example`,
`Warp route deployment config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/warp-route-deployment.yaml for an example.`,
);
throw e;
}
@ -169,8 +193,34 @@ export async function createWarpRouteDeployConfig({
// Note, this is different than the function above which reads a config
// for a DEPLOYMENT. This gets a config for using a warp route (aka WarpCoreConfig)
export function readWarpRouteConfig(filePath: string): WarpCoreConfig {
export function readWarpCoreConfig(filePath: string): WarpCoreConfig {
const config = readYamlOrJson(filePath);
if (!config) throw new Error(`No warp route config found at ${filePath}`);
return WarpCoreConfigSchema.parse(config);
}
/**
* Creates a default configuration for an ISM with a TRUSTED_RELAYER and FALLBACK_ROUTING.
*
* Properties relayer and owner are both set as input owner.
*
* @param owner - The address of the owner of the ISM.
* @returns The default Aggregation ISM configuration.
*/
function createDefaultWarpIsmConfig(owner: Address): IsmConfig {
return {
type: IsmType.AGGREGATION,
modules: [
{
type: IsmType.TRUSTED_RELAYER,
relayer: owner,
},
{
type: IsmType.FALLBACK_ROUTING,
domains: {},
owner,
},
],
threshold: 1,
};
}

@ -56,7 +56,7 @@ export async function getContext({
const registry = getRegistry(registryUri, registryOverrideUri);
let signer: ethers.Wallet | undefined = undefined;
if (requiresKey) {
if (key || requiresKey) {
({ key, signer } = await getSigner({ key, skipConfirmation }));
}
const multiProvider = await getMultiProvider(registry, signer);
@ -99,9 +99,8 @@ export async function getDryRunContext(
logBlue(`Dry-running against chain: ${chain}`);
await verifyAnvil();
const multiProvider = await getMultiProvider(registry);
await forkNetworkToMultiProvider(multiProvider, chain);
let multiProvider = await getMultiProvider(registry);
multiProvider = await forkNetworkToMultiProvider(multiProvider, chain);
const { impersonatedKey, impersonatedSigner } = await getImpersonatedSigner({
fromAddress,
key,

@ -1,461 +1,90 @@
import { confirm } from '@inquirer/prompts';
import { ethers } from 'ethers';
import { stringify as yamlStringify } from 'yaml';
import { ChainAddresses, IRegistry } from '@hyperlane-xyz/registry';
import {
ChainMap,
ChainName,
CoreConfig,
GasOracleContractType,
HooksConfig,
HyperlaneAddressesMap,
HyperlaneContractsMap,
HyperlaneCore,
HyperlaneCoreDeployer,
HyperlaneIsmFactory,
HyperlaneProxyFactoryDeployer,
IgpConfig,
IsmConfig,
IsmType,
MultisigConfig,
RoutingIsmConfig,
buildAgentConfig,
buildAggregationIsmConfigs,
defaultMultisigConfigs,
multisigIsmVerificationCost,
serializeContractsMap,
} from '@hyperlane-xyz/sdk';
import { Address, objFilter, objMap, objMerge } from '@hyperlane-xyz/utils';
import { ChainName, CoreConfig, EvmCoreModule } from '@hyperlane-xyz/sdk';
import { presetHookConfigs, readHooksConfigMap } from '../config/hooks.js';
import { readIsmConfig } from '../config/ism.js';
import { readMultisigConfig } from '../config/multisig.js';
import { MINIMUM_CORE_DEPLOY_GAS } from '../consts.js';
import { WriteCommandContext } from '../context/types.js';
import {
log,
logBlue,
logBoldUnderlinedRed,
logGray,
logGreen,
logRed,
} from '../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js';
import { runFileSelectionStep, writeJson } from '../utils/files.js';
import { log, logBlue, logGreen } from '../logger.js';
import { runSingleChainSelectionStep } from '../utils/chains.js';
import { indentYamlOrJson } from '../utils/files.js';
import {
completeDeploy,
isISMConfig,
isZODISMConfig,
prepareDeploy,
runDeployPlanStep,
runPreflightChecksForChains,
} from './utils.js';
const CONTRACT_CACHE_EXCLUSIONS = ['interchainGasPaymaster'];
interface DeployParams {
context: WriteCommandContext;
chain: ChainName;
config: CoreConfig;
}
/**
* Executes the core deploy command.
*/
export async function runCoreDeploy({
context,
chains,
ismConfigPath,
hookConfigPath,
agentOutPath,
chain,
config,
}: {
context: WriteCommandContext;
chains?: ChainName[];
ismConfigPath?: string;
hookConfigPath?: string;
agentOutPath: string;
chain: ChainName;
config: CoreConfig;
}) {
const { chainMetadata, signer, dryRunChain, skipConfirmation } = context;
if (dryRunChain) chains = [dryRunChain];
else if (!chains?.length) {
if (skipConfirmation) throw new Error('No chains provided');
chains = await runMultiChainSelectionStep(
const {
signer,
isDryRun,
chainMetadata,
dryRunChain,
registry,
skipConfirmation,
} = context;
// Select a dry-run chain if it's not supplied
if (dryRunChain) {
chain = dryRunChain;
} else if (!chain) {
if (skipConfirmation) throw new Error('No chain provided');
chain = await runSingleChainSelectionStep(
chainMetadata,
'Select chains to connect:',
true,
'Select chain to connect:',
);
}
const result = await runIsmStep(chains, skipConfirmation, ismConfigPath);
// we can either specify the full ISM config or just the multisig config
const isIsmConfig = isISMConfig(result);
const ismConfigs = isIsmConfig ? (result as ChainMap<IsmConfig>) : undefined;
const multisigConfigs = isIsmConfig
? defaultMultisigConfigs
: (result as ChainMap<MultisigConfig>);
const hooksConfig = await runHookStep(chains, hookConfigPath);
const deploymentParams: DeployParams = {
context,
chains,
ismConfigs,
multisigConfigs,
hooksConfig,
agentOutPath,
chain,
config,
};
await runDeployPlanStep(deploymentParams);
await runPreflightChecksForChains({
...deploymentParams,
chains: [chain],
minGas: MINIMUM_CORE_DEPLOY_GAS,
});
const userAddress = await signer.getAddress();
const initialBalances = await prepareDeploy(context, userAddress, chains);
await executeDeploy(deploymentParams);
const initialBalances = await prepareDeploy(context, userAddress, [chain]);
await completeDeploy(context, 'core', initialBalances, userAddress, chains);
}
async function runIsmStep(
selectedChains: ChainName[],
skipConfirmation: boolean,
ismConfigPath?: string,
) {
if (!ismConfigPath) {
logBlue(
'\n',
'Hyperlane instances requires an Interchain Security Module (ISM).',
);
logGray(
'Example config: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/cli/typescript/cli/examples/ism.yaml',
);
if (skipConfirmation) throw new Error('ISM config required');
ismConfigPath = await runFileSelectionStep(
'./configs',
'ISM config',
'ism',
);
}
const isAdvancedIsm = isZODISMConfig(ismConfigPath);
// separate flow for 'ism' and 'ism-advanced' options
if (isAdvancedIsm) {
logBoldUnderlinedRed(
'WARNING: YOU ARE DEPLOYING WITH AN ADVANCED ISM CONFIG',
);
logRed(
'Advanced ISM configs require knowledge of different ISM types and how they work together topologically. If possible, use the basic ISM configs are recommended.',
);
const ismConfig = readIsmConfig(ismConfigPath);
const requiredIsms = objFilter(
ismConfig,
(chain, config): config is IsmConfig => selectedChains.includes(chain),
);
// selected chains - (user configs + default configs) = missing config
const missingConfigs = selectedChains.filter(
(c) => !Object.keys(ismConfig).includes(c),
);
if (missingConfigs.length > 0) {
throw new Error(
`Missing advanced ISM config for one or more chains: ${missingConfigs.join(
', ',
)}`,
);
}
log(`Found configs for chains: ${selectedChains.join(', ')}`);
return requiredIsms as ChainMap<IsmConfig>;
} else {
const multisigConfigs = {
...defaultMultisigConfigs,
...readMultisigConfig(ismConfigPath),
} as ChainMap<MultisigConfig>;
const requiredMultisigs = objFilter(
multisigConfigs,
(chain, config): config is MultisigConfig =>
selectedChains.includes(chain),
);
// selected chains - (user configs + default configs) = missing config
const missingConfigs = selectedChains.filter(
(c) => !Object.keys(requiredMultisigs).includes(c),
);
if (missingConfigs.length > 0) {
throw new Error(
`Missing ISM config for one or more chains: ${missingConfigs.join(
', ',
)}`,
);
}
log(`Found configs for chains: ${selectedChains.join(', ')}`);
return requiredMultisigs as ChainMap<MultisigConfig>;
}
}
async function runHookStep(
_selectedChains: ChainName[],
hookConfigPath?: string,
) {
if (!hookConfigPath) return {};
return readHooksConfigMap(hookConfigPath);
}
interface DeployParams {
context: WriteCommandContext;
chains: ChainName[];
ismConfigs?: ChainMap<IsmConfig>;
multisigConfigs?: ChainMap<MultisigConfig>;
hooksConfig?: ChainMap<HooksConfig>;
agentOutPath: string;
}
async function runDeployPlanStep({ context, chains }: DeployParams) {
const { signer, skipConfirmation } = context;
const address = await signer.getAddress();
logBlue('\nDeployment plan');
logGray('===============');
log(`Transaction signer and owner of new contracts will be ${address}`);
log(`Deploying to ${chains.join(', ')}`);
log(
`There are several contracts required for each chain but contracts in your provided registries will be skipped`,
);
if (skipConfirmation) return;
const isConfirmed = await confirm({
message: 'Is this deployment plan correct?',
});
if (!isConfirmed) throw new Error('Deployment cancelled');
}
async function executeDeploy({
context,
chains,
ismConfigs = {},
multisigConfigs = {},
hooksConfig = {},
agentOutPath,
}: DeployParams) {
logBlue('All systems ready, captain! Beginning deployment...');
const { signer, multiProvider, registry } = context;
let chainAddresses = await registry.getAddresses();
chainAddresses = filterAddressesToCache(chainAddresses);
const owner = await signer.getAddress();
let artifacts: HyperlaneAddressesMap<any> = {};
// 1. Deploy ISM factories to all deployable chains that don't have them.
logBlue('Deploying ISM factory contracts');
const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider);
ismFactoryDeployer.cacheAddressesMap(chainAddresses);
const ismFactoryConfig = chains.reduce((chainMap, curr) => {
chainMap[curr] = {};
return chainMap;
}, {} as ChainMap<{}>);
const ismFactoryContracts = await ismFactoryDeployer.deploy(ismFactoryConfig);
artifacts = await updateChainAddresses(
registry,
ismFactoryContracts,
artifacts,
context.isDryRun,
);
logGreen('ISM factory contracts deployed');
// Build an IsmFactory that covers all chains so that we can
// use it to deploy ISMs to remote chains.
const ismFactory = HyperlaneIsmFactory.fromAddressesMap(
chainAddresses,
multiProvider,
);
// 3. Construct ISM configs for all deployable chains
const defaultIsms: ChainMap<IsmConfig> = {};
for (const ismOrigin of chains) {
defaultIsms[ismOrigin] =
ismConfigs[ismOrigin] ??
buildIsmConfig(owner, ismOrigin, chains, multisigConfigs);
}
// 4. Deploy core contracts to chains
logBlue(`Deploying core contracts to ${chains.join(', ')}`);
const coreDeployer = new HyperlaneCoreDeployer(multiProvider, ismFactory);
coreDeployer.cacheAddressesMap(chainAddresses as any);
const coreConfigs = buildCoreConfigMap(
owner,
chains,
defaultIsms,
hooksConfig,
);
const coreContracts = await coreDeployer.deploy(coreConfigs);
// 4.5 recover the toplevel ISM address
const isms: HyperlaneAddressesMap<any> = {};
for (const chain of chains) {
isms[chain] = {
interchainSecurityModule:
coreDeployer.cachedAddresses[chain].interchainSecurityModule,
};
}
artifacts = objMerge(artifacts, isms);
artifacts = await updateChainAddresses(
registry,
coreContracts,
artifacts,
context.isDryRun,
);
logGreen('✅ Core contracts deployed');
log(JSON.stringify(artifacts, null, 2));
await writeAgentConfig(context, artifacts, chains, agentOutPath);
logBlue('Deployment is complete!');
}
function filterAddressesToCache(addressesMap: ChainMap<ChainAddresses>) {
// Filter out the certain addresses that must always be
// deployed when deploying to a PI chain.
// See https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/2983
// And https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/3183
return objMap(addressesMap, (_chain, addresses) =>
objFilter(
addresses,
(contract, _address): _address is string =>
!CONTRACT_CACHE_EXCLUSIONS.includes(contract),
),
);
}
function buildIsmConfig(
owner: Address,
local: ChainName,
chains: ChainName[],
multisigIsmConfigs: ChainMap<MultisigConfig>,
): RoutingIsmConfig {
const aggregationIsmConfigs = buildAggregationIsmConfigs(
local,
chains,
multisigIsmConfigs,
);
return {
owner,
type: IsmType.ROUTING,
domains: aggregationIsmConfigs,
};
}
function buildCoreConfigMap(
owner: Address,
chains: ChainName[],
defaultIsms: ChainMap<IsmConfig>,
hooksConfig: ChainMap<HooksConfig>,
): ChainMap<CoreConfig> {
return chains.reduce<ChainMap<CoreConfig>>((config, chain) => {
const hooks = hooksConfig[chain] ?? presetHookConfigs(owner);
config[chain] = {
owner,
defaultIsm: defaultIsms[chain],
defaultHook: hooks.default,
requiredHook: hooks.required,
};
return config;
}, {});
}
export function buildIgpConfigMap(
owner: Address,
chains: ChainName[],
multisigConfigs: ChainMap<MultisigConfig>,
): ChainMap<IgpConfig> {
const configMap: ChainMap<IgpConfig> = {};
for (const chain of chains) {
const overhead: ChainMap<number> = {};
const gasOracleType: ChainMap<GasOracleContractType> = {};
for (const remote of chains) {
if (chain === remote) continue;
// TODO: accurate estimate of gas from ChainMap<ISMConfig>
const threshold = multisigConfigs[remote]
? multisigConfigs[remote].threshold
: 2;
const validatorsLength = multisigConfigs[remote]
? multisigConfigs[remote].validators.length
: 3;
overhead[remote] = multisigIsmVerificationCost(
threshold,
validatorsLength,
);
gasOracleType[remote] = GasOracleContractType.StorageGasOracle;
}
configMap[chain] = {
owner,
beneficiary: owner,
gasOracleType,
overhead,
oracleKey: owner,
};
}
return configMap;
}
async function updateChainAddresses(
registry: IRegistry,
newContracts: HyperlaneContractsMap<any>,
otherAddresses: HyperlaneAddressesMap<any>,
isDryRun?: boolean,
) {
let newAddresses = serializeContractsMap(newContracts);
// The HyperlaneCoreDeployer is returning a nested object with ISM addresses
// from other chains, which don't need to be in the artifacts atm.
newAddresses = objMap(newAddresses, (_, newChainAddresses) => {
// For each chain in the addresses chainmap, filter the values to those that are just strings
return objFilter(
newChainAddresses,
(_, value): value is string => typeof value === 'string',
);
const evmCoreModule = await EvmCoreModule.create({
chain,
config,
multiProvider: context.multiProvider,
});
const mergedAddresses = objMerge(otherAddresses, newAddresses);
if (isDryRun) return mergedAddresses;
await completeDeploy(context, 'core', initialBalances, userAddress, [chain]);
const deployedAddresses = evmCoreModule.serialize();
for (const chainName of Object.keys(newContracts)) {
if (!isDryRun) {
await registry.updateChain({
chainName,
addresses: mergedAddresses[chainName],
chainName: chain,
addresses: deployedAddresses,
});
}
return mergedAddresses;
}
async function writeAgentConfig(
context: WriteCommandContext,
artifacts: HyperlaneAddressesMap<any>,
chains: ChainName[],
outPath: string,
) {
if (context.isDryRun) return;
log('Writing agent configs');
const { multiProvider, registry } = context;
const startBlocks: ChainMap<number> = {};
const core = HyperlaneCore.fromAddressesMap(artifacts, multiProvider);
for (const chain of chains) {
const mailbox = core.getContracts(chain).mailbox;
startBlocks[chain] = (await mailbox.deployedBlock()).toNumber();
}
const chainAddresses = await registry.getAddresses();
for (const chain of chains) {
if (!chainAddresses[chain].interchainGasPaymaster) {
chainAddresses[chain].interchainGasPaymaster =
ethers.constants.AddressZero;
}
}
const agentConfig = buildAgentConfig(
chains, // Use only the chains that were deployed to
multiProvider,
chainAddresses as any,
startBlocks,
);
writeJson(outPath, agentConfig);
logGreen('Agent configs written');
logGreen('✅ Core contract deployments complete:\n');
log(indentYamlOrJson(yamlStringify(deployedAddresses, null, 2), 4));
}

@ -21,10 +21,11 @@ export async function forkNetworkToMultiProvider(
chain: string,
) {
multiProvider = multiProvider.extendChainMetadata({
[chain]: { blocks: { confirmations: 0 } },
[chain]: { blocks: { confirmations: 1 } },
});
await setFork(multiProvider, chain);
return multiProvider;
}
/**

@ -1,7 +1,9 @@
import { confirm } from '@inquirer/prompts';
import { BigNumber, ethers } from 'ethers';
import {
ChainMap,
ChainMetadata,
ChainName,
IsmConfig,
MultisigConfig,
@ -11,7 +13,14 @@ import { Address, ProtocolType } from '@hyperlane-xyz/utils';
import { parseIsmConfig } from '../config/ism.js';
import { WriteCommandContext } from '../context/types.js';
import { log, logGreen, logPink } from '../logger.js';
import {
log,
logBlue,
logGray,
logGreen,
logPink,
logTable,
} from '../logger.js';
import { gasBalancesAreSufficient } from '../utils/balances.js';
import { ENV } from '../utils/env.js';
import { assertSigner } from '../utils/keys.js';
@ -55,6 +64,35 @@ export async function runPreflightChecksForChains({
if (sufficient) logGreen('✅ Balances are sufficient');
}
export async function runDeployPlanStep({
context,
chain,
}: {
context: WriteCommandContext;
chain: ChainName;
}) {
const { signer, chainMetadata: chainMetadataMap, skipConfirmation } = context;
const address = await signer.getAddress();
logBlue('\nDeployment plan');
logGray('===============');
log(`Transaction signer and owner of new contracts: ${address}`);
log(`Deploying core contracts to network: ${chain}`);
const transformedChainMetadata = transformChainMetadataForDisplay(
chainMetadataMap[chain],
);
logTable(transformedChainMetadata);
log(
`Note: There are several contracts required for each chain, but contracts in your provided registries will be skipped.`,
);
if (skipConfirmation) return;
const isConfirmed = await confirm({
message: 'Is this deployment plan correct?',
});
if (!isConfirmed) throw new Error('Deployment cancelled');
}
// from parsed types
export function isISMConfig(
config: ChainMap<MultisigConfig> | ChainMap<IsmConfig>,
@ -106,7 +144,7 @@ export async function completeDeploy(
`\t- Gas required for ${command} ${
isDryRun ? 'dry-run' : 'deploy'
} on ${chain}: ${ethers.utils.formatEther(balanceDelta)} ${
multiProvider.getChainMetadata(chain).nativeToken?.symbol
multiProvider.getChainMetadata(chain).nativeToken?.symbol ?? 'ETH'
}`,
);
}
@ -117,3 +155,17 @@ export async function completeDeploy(
export function toUpperCamelCase(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function transformChainMetadataForDisplay(chainMetadata: ChainMetadata) {
return {
Name: chainMetadata.name,
'Display Name': chainMetadata.displayName,
'Chain ID': chainMetadata.chainId,
'Domain ID': chainMetadata.domainId,
Protocol: chainMetadata.protocol,
'JSON RPC URL': chainMetadata.rpcUrls[0].http,
'Native Token: Symbol': chainMetadata.nativeToken?.symbol,
'Native Token: Name': chainMetadata.nativeToken?.name,
'Native Token: Decimals': chainMetadata.nativeToken?.decimals,
};
}

@ -1,13 +1,15 @@
import { confirm } from '@inquirer/prompts';
import { stringify as yamlStringify } from 'yaml';
import { IRegistry } from '@hyperlane-xyz/registry';
import {
HypXERC20Lockbox__factory,
HypXERC20__factory,
} from '@hyperlane-xyz/core';
import {
EvmIsmModule,
HypERC20Deployer,
HypERC721Deployer,
HyperlaneAddresses,
HyperlaneContractsMap,
HyperlaneProxyFactoryDeployer,
MultiProvider,
TOKEN_TYPE_TO_STANDARD,
TokenFactories,
TokenType,
@ -15,14 +17,19 @@ import {
WarpRouteDeployConfig,
getTokenConnectionId,
isTokenMetadata,
serializeContracts,
} from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { ProtocolType, objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import { readWarpRouteDeployConfig } from '../config/warp.js';
import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js';
import { WriteCommandContext } from '../context/types.js';
import { log, logBlue, logGray, logGreen, logTable } from '../logger.js';
import { isFile, runFileSelectionStep } from '../utils/files.js';
import {
indentYamlOrJson,
isFile,
runFileSelectionStep,
} from '../utils/files.js';
import {
completeDeploy,
@ -123,17 +130,124 @@ async function executeDeploy(params: DeployParams) {
? { [dryRunChain]: configMap[dryRunChain] }
: configMap;
const deployedContracts = await deployer.deploy(config);
const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider);
logGreen('✅ Warp contract deployments complete');
// For each chain in WarpRouteConfig, deploy each Ism Factory, if it's not in the registry
// Then return a modified config with the ism address as a string
const modifiedConfig = await deployAndResolveWarpIsm(
config,
multiProvider,
registry,
ismFactoryDeployer,
);
const deployedContracts = await deployer.deploy(modifiedConfig);
const warpCoreConfig = await getWarpCoreConfig(params, deployedContracts);
logGreen('✅ Warp contract deployments complete');
if (!isDryRun) {
log('Writing deployment artifacts');
log('Writing deployment artifacts...');
await registry.addWarpRoute(warpCoreConfig);
}
log(JSON.stringify(warpCoreConfig, null, 2));
logBlue('Deployment is complete!');
log(indentYamlOrJson(yamlStringify(warpCoreConfig, null, 2), 4));
}
async function deployAndResolveWarpIsm(
warpConfig: WarpRouteDeployConfig,
multiProvider: MultiProvider,
registry: IRegistry,
ismFactoryDeployer: HyperlaneProxyFactoryDeployer,
): Promise<WarpRouteDeployConfig> {
return promiseObjAll(
objMap(warpConfig, async (chain, config) => {
// Skip deployment if Ism is empty, or a string
if (
!config.interchainSecurityModule ||
typeof config.interchainSecurityModule === 'string'
) {
logGray(
`Config Ism is ${
!config.interchainSecurityModule
? 'empty'
: config.interchainSecurityModule
}, skipping deployment`,
);
return config;
}
logBlue('Loading Registry factory addresses');
let chainAddresses = await registry.getChainAddresses(chain); // Can includes other addresses
if (!chainAddresses) {
logGray('Registry factory addresses not found, deploying');
chainAddresses = serializeContracts(
await ismFactoryDeployer.deployContracts(chain),
) as Record<string, string>;
}
logGray(
`Creating ${config.interchainSecurityModule.type} Ism for ${config.type} token on ${chain} chain`,
);
const deployedIsm = await createWarpIsm(
chain,
warpConfig,
multiProvider,
{
domainRoutingIsmFactory: chainAddresses.domainRoutingIsmFactory,
staticAggregationHookFactory:
chainAddresses.staticAggregationHookFactory,
staticAggregationIsmFactory:
chainAddresses.staticAggregationIsmFactory,
staticMerkleRootMultisigIsmFactory:
chainAddresses.staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory:
chainAddresses.staticMessageIdMultisigIsmFactory,
},
);
logGreen(
`Finished creating ${config.interchainSecurityModule.type} Ism for ${config.type} token on ${chain} chain`,
);
return { ...warpConfig[chain], interchainSecurityModule: deployedIsm };
}),
);
}
/**
* Deploys the Warp ISM for a given config
*
* @returns The deployed ism address
*/
async function createWarpIsm(
chain: string,
warpConfig: WarpRouteDeployConfig,
multiProvider: MultiProvider,
factoryAddresses: HyperlaneAddresses<any>,
): Promise<string> {
const {
domainRoutingIsmFactory,
staticAggregationHookFactory,
staticAggregationIsmFactory,
staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory,
} = factoryAddresses;
const evmIsmModule = await EvmIsmModule.create({
chain,
multiProvider,
mailbox: warpConfig[chain].mailbox,
proxyFactoryFactories: {
domainRoutingIsmFactory,
staticAggregationHookFactory,
staticAggregationIsmFactory,
staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory,
},
config: warpConfig[chain].interchainSecurityModule!,
});
const { deployedIsm } = evmIsmModule.serialize();
return deployedIsm;
}
async function getWarpCoreConfig(
@ -165,30 +279,8 @@ async function getWarpCoreConfig(
throw new Error('Missing decimals on token metadata');
}
const collateralAddressOrDenom = await (async () => {
if (config.type === TokenType.XERC20Lockbox) {
const provider = context.multiProvider.tryGetProvider(chainName);
if (!provider) {
throw new Error(`Unable to pull provider for ${chainName}`);
}
const xERC20 = await HypXERC20Lockbox__factory.connect(
config.token,
provider,
).xERC20();
const wrappedToken = await HypXERC20__factory.connect(
xERC20,
provider,
).wrappedToken();
return wrappedToken;
}
if (config.type === TokenType.collateral) {
return config.token;
}
return undefined;
})();
const collateralAddressOrDenom =
config.type === TokenType.collateral ? config.token : undefined;
warpCoreConfig.tokens.push({
chainName,
standard: TOKEN_TYPE_TO_STANDARD[config.type],

@ -35,6 +35,9 @@ export function logColor(
}
}
export const logBlue = (...args: any) => logColor('info', chalk.blue, ...args);
export const logBlueKeyValue = (key: string, value: string) => {
logBlue(`${chalk.bold(`${key}:`)} ${value}`);
};
export const logPink = (...args: any) =>
logColor('info', chalk.magentaBright, ...args);
export const logGray = (...args: any) => logColor('info', chalk.gray, ...args);
@ -43,11 +46,16 @@ export const logGreen = (...args: any) =>
export const logRed = (...args: any) => logColor('info', chalk.red, ...args);
export const logBoldUnderlinedRed = (...args: any) =>
logColor('info', chalk.red.bold.underline, ...args);
export const logBoldBlue = (...args: any) =>
logColor('info', chalk.blue.bold, ...args);
export const logTip = (...args: any) =>
logColor('info', chalk.bgYellow, ...args);
export const warnYellow = (...args: any) =>
logColor('warn', chalk.yellow, ...args);
export const errorRed = (...args: any) => logColor('error', chalk.red, ...args);
export const logDebug = (msg: string, ...args: any) =>
logger.debug(msg, ...args);
// No support for table in pino so print directly to console
export const logTable = (...args: any) => console.table(...args);

@ -1,4 +1,4 @@
import { ethers } from 'ethers';
import { stringify as yamlStringify } from 'yaml';
import { ChainName, HyperlaneCore } from '@hyperlane-xyz/sdk';
import { addressToBytes32, timeout } from '@hyperlane-xyz/utils';
@ -8,6 +8,7 @@ import { CommandContext, WriteCommandContext } from '../context/types.js';
import { runPreflightChecksForChains } from '../deploy/utils.js';
import { errorRed, log, logBlue, logGreen } from '../logger.js';
import { runSingleChainSelectionStep } from '../utils/chains.js';
import { indentYamlOrJson } from '../utils/files.js';
export async function sendTestMessage({
context,
@ -81,18 +82,12 @@ async function executeDelivery({
const { registry, multiProvider } = context;
const chainAddresses = await registry.getAddresses();
const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider);
const mailbox = core.getContracts(origin).mailbox;
let hook = chainAddresses[origin]?.customHook;
const hook = chainAddresses[origin]?.customHook;
if (hook) {
logBlue(`Using custom hook ${hook} for ${origin} -> ${destination}`);
} else {
hook = await mailbox.defaultHook();
logBlue(`Using default hook ${hook} for ${origin} -> ${destination}`);
}
const destinationDomain = multiProvider.getDomainId(destination);
let txReceipt: ethers.ContractReceipt;
try {
const recipient = chainAddresses[destination].testRecipient;
if (!recipient) {
@ -100,54 +95,37 @@ async function executeDelivery({
}
const formattedRecipient = addressToBytes32(recipient);
log('Getting gas quote');
const value = await mailbox[
'quoteDispatch(uint32,bytes32,bytes,bytes,address)'
](
destinationDomain,
formattedRecipient,
messageBody,
ethers.utils.hexlify([]),
hook,
);
log(`Paying for gas with ${value} wei`);
log('Dispatching message');
const messageTx = await mailbox[
'dispatch(uint32,bytes32,bytes,bytes,address)'
](
destinationDomain,
const { dispatchTx, message } = await core.sendMessage(
origin,
destination,
formattedRecipient,
messageBody,
ethers.utils.hexlify([]),
hook,
{
value,
},
undefined,
);
txReceipt = await multiProvider.handleTx(origin, messageTx);
const message = core.getDispatchedMessages(txReceipt)[0];
logBlue(`Sent message from ${origin} to ${recipient} on ${destination}.`);
logBlue(`Message ID: ${message.id}`);
log(`Message: ${JSON.stringify(message)}`);
log(`Message:\n${indentYamlOrJson(yamlStringify(message, null, 2), 4)}`);
if (selfRelay) {
log('Attempting self-relay of message');
await core.relayMessage(message);
await core.relayMessage(message, dispatchTx);
logGreen('Message was self-relayed!');
} else {
if (skipWaitForDelivery) {
return;
}
log('Waiting for message delivery on destination chain...');
// Max wait 10 minutes
await core.waitForMessageProcessed(dispatchTx, 10000, 60);
logGreen('Message was delivered!');
}
} catch (e) {
errorRed(
`Encountered error sending message from ${origin} to ${destination}`,
);
throw e;
}
if (skipWaitForDelivery) return;
log('Waiting for message delivery on destination chain...');
// Max wait 10 minutes
await core.waitForMessageProcessed(txReceipt, 10000, 60);
logGreen('Message was delivered!');
}

@ -10,7 +10,6 @@ import {
} from '@hyperlane-xyz/sdk';
import { timeout } from '@hyperlane-xyz/utils';
import { readWarpRouteConfig } from '../config/warp.js';
import { MINIMUM_TEST_SEND_GAS } from '../consts.js';
import { WriteCommandContext } from '../context/types.js';
import { runPreflightChecksForChains } from '../deploy/utils.js';
@ -20,20 +19,20 @@ import { runTokenSelectionStep } from '../utils/tokens.js';
export async function sendTestTransfer({
context,
warpConfigPath,
warpCoreConfig,
origin,
destination,
wei,
amount,
recipient,
timeoutSec,
skipWaitForDelivery,
selfRelay,
}: {
context: WriteCommandContext;
warpConfigPath: string;
warpCoreConfig: WarpCoreConfig;
origin?: ChainName;
destination?: ChainName;
wei: string;
amount: string;
recipient?: string;
timeoutSec: number;
skipWaitForDelivery: boolean;
@ -41,8 +40,6 @@ export async function sendTestTransfer({
}) {
const { chainMetadata } = context;
const warpCoreConfig = readWarpRouteConfig(warpConfigPath);
if (!origin) {
origin = await runSingleChainSelectionStep(
chainMetadata,
@ -70,7 +67,7 @@ export async function sendTestTransfer({
origin,
destination,
warpCoreConfig,
wei,
amount,
recipient,
skipWaitForDelivery,
selfRelay,
@ -85,7 +82,7 @@ async function executeDelivery({
origin,
destination,
warpCoreConfig,
wei,
amount,
recipient,
skipWaitForDelivery,
selfRelay,
@ -94,7 +91,7 @@ async function executeDelivery({
origin: ChainName;
destination: ChainName;
warpCoreConfig: WarpCoreConfig;
wei: string;
amount: string;
recipient?: string;
skipWaitForDelivery: boolean;
selfRelay?: boolean;
@ -131,7 +128,7 @@ async function executeDelivery({
const senderAddress = await signer.getAddress();
const errors = await warpCore.validateTransfer({
originTokenAmount: token.amount(wei),
originTokenAmount: token.amount(amount),
destination,
recipient: recipient ?? senderAddress,
sender: senderAddress,
@ -142,7 +139,7 @@ async function executeDelivery({
}
const transferTxs = await warpCore.getTransferRemoteTxs({
originTokenAmount: new TokenAmount(wei, token),
originTokenAmount: new TokenAmount(amount, token),
destination,
sender: senderAddress,
recipient: recipient ?? senderAddress,
@ -164,7 +161,7 @@ async function executeDelivery({
logBlue(`Message ID: ${message.id}`);
if (selfRelay) {
await core.relayMessage(message);
await core.relayMessage(message, transferTxReceipt);
logGreen('Message was self-relayed!');
return;
}

@ -57,7 +57,7 @@ export async function checkMessageStatus({
const receipt = await core.getDispatchTx(origin, messageId);
const messages = core.getDispatchedMessages(receipt);
await core.relayMessage(messages[0]);
await core.relayMessage(messages[0], receipt);
logGreen(`Message ${messageId} was self-relayed!`);
}
}

@ -75,7 +75,7 @@ async function getTransformer<TProtocol extends ProtocolType>(
transformerMetadata: TransformerMetadata,
): Promise<TxTransformerInterface<TProtocol>> {
switch (transformerMetadata.type) {
case TxTransformerType.ICA:
case TxTransformerType.INTERCHAIN_ACCOUNT:
return new EV5InterchainAccountTxTransformer(
multiProvider,
transformerMetadata.props,

@ -1,19 +1,14 @@
import { expect } from 'chai';
import {
ChainMap,
GasOracleContractType,
HookType,
HooksConfig,
} from '@hyperlane-xyz/sdk';
import { HookType } from '@hyperlane-xyz/sdk';
import { readHooksConfigMap } from '../config/hooks.js';
import { HooksConfigMap, readHooksConfigMap } from '../config/hooks.js';
describe('readHooksConfigMap', () => {
it('parses and validates example correctly', () => {
const hooks = readHooksConfigMap('examples/hooks.yaml');
const exampleHooksConfig: ChainMap<HooksConfig> = {
const exampleHooksConfig: HooksConfigMap = {
anvil1: {
required: {
type: HookType.PROTOCOL_FEE,
@ -37,10 +32,13 @@ describe('readHooksConfigMap', () => {
beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
gasOracleType: {
anvil2: GasOracleContractType.StorageGasOracle,
},
overhead: { anvil2: 50000 },
oracleConfig: {
anvil2: {
gasPrice: '100',
tokenExchangeRate: '100',
},
},
},
],
},
@ -70,10 +68,13 @@ describe('readHooksConfigMap', () => {
beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
gasOracleType: {
anvil1: GasOracleContractType.StorageGasOracle,
},
overhead: { anvil1: 50000 },
oracleConfig: {
anvil1: {
gasPrice: '100',
tokenExchangeRate: '100',
},
},
},
],
},
@ -87,6 +88,6 @@ describe('readHooksConfigMap', () => {
it('parsing failure, missing internal key "overhead"', () => {
expect(() => {
readHooksConfigMap('src/tests/hooks/safe-parse-fail.yaml');
}).to.throw('Invalid hook config: anvil2,default => Invalid input');
}).to.throw();
});
});

@ -19,8 +19,6 @@ anvil1:
oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
overhead:
anvil2: 50000
gasOracleType:
anvil2: StorageGasOracle
anvil2:
required:
type: protocolFee
@ -40,5 +38,3 @@ anvil2:
beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
gasOracleType:
anvil1: StorageGasOracle

@ -80,6 +80,8 @@ describe('readIsmConfig', () => {
it('parsing failure, threshold > modules.length', () => {
expect(function () {
readIsmConfig('src/tests/ism/threshold-gt-modules-length-fail.yaml');
}).to.throw('Threshold cannot be greater than number of modules');
}).to.throw(
'Threshold must be less than or equal to the number of modules',
);
});
});

@ -1,4 +1,4 @@
import { Separator, checkbox, confirm, input } from '@inquirer/prompts';
import { Separator, checkbox } from '@inquirer/prompts';
import select from '@inquirer/select';
import chalk from 'chalk';
@ -6,6 +6,8 @@ import { ChainMap, ChainMetadata } from '@hyperlane-xyz/sdk';
import { log, logRed, logTip } from '../logger.js';
import { calculatePageSize } from './cli-options.js';
// A special value marker to indicate user selected
// a new chain in the list
const NEW_CHAIN_MARKER = '__new__';
@ -18,7 +20,7 @@ export async function runSingleChainSelectionStep(
const chain = (await select({
message,
choices,
pageSize: 30,
pageSize: calculatePageSize(2),
})) as string;
handleNewChain([chain]);
return chain;
@ -35,7 +37,7 @@ export async function runMultiChainSelectionStep(
const chains = (await checkbox({
message,
choices,
pageSize: 30,
pageSize: calculatePageSize(2),
})) as string[];
handleNewChain(chains);
if (requireMultiple && chains?.length < 2) {
@ -73,24 +75,3 @@ function handleNewChain(chainNames: string[]) {
process.exit(0);
}
}
export async function detectAndConfirmOrPrompt(
detect: () => Promise<string | undefined>,
prompt: string,
label: string,
): Promise<string> {
let detectedValue: string | undefined;
try {
detectedValue = await detect();
if (detectedValue) {
const confirmed = await confirm({
message: `Detected ${label} as ${detectedValue}, is this correct?`,
});
if (confirmed) {
return detectedValue;
}
}
// eslint-disable-next-line no-empty
} catch (e) {}
return input({ message: `${prompt} ${label}`, default: detectedValue });
}

@ -0,0 +1,17 @@
// Functions used to manipulate CLI specific options
/**
* Calculates the page size for a CLI Terminal output, taking into account the number of lines to skip and a default page size.
*
* @param skipSize - The number of lines to skip, which can be used to skip previous prompts.
* @param defaultPageSize - The default page size to use if the terminal height is too small.
* @returns The calculated pageSize, which is the terminal height minus the skip size, or the default page size if the terminal height is too small.
*/
export function calculatePageSize(
skipSize: number = 0,
defaultPageSize: number = 15,
) {
return process.stdout.rows > skipSize
? process.stdout.rows - skipSize
: defaultPageSize;
}

@ -210,3 +210,11 @@ export async function runFileSelectionStep(
if (filename) return filename;
else throw new Error(`No filepath entered ${description}`);
}
export function indentYamlOrJson(str: string, indentLevel: number): string {
const indent = ' '.repeat(indentLevel);
return str
.split('\n')
.map((line) => indent + line)
.join('\n');
}

@ -0,0 +1,55 @@
import { confirm, input } from '@inquirer/prompts';
import { logGray } from '../logger.js';
import { indentYamlOrJson } from './files.js';
export async function detectAndConfirmOrPrompt(
detect: () => Promise<string | undefined>,
prompt: string,
label: string,
source?: string,
): Promise<string> {
let detectedValue: string | undefined;
try {
detectedValue = await detect();
if (detectedValue) {
const confirmed = await confirm({
message: `Detected ${label} as ${detectedValue}${
source ? ` from ${source}` : ''
}, is this correct?`,
});
if (confirmed) {
return detectedValue;
}
}
// eslint-disable-next-line no-empty
} catch (e) {}
return input({ message: `${prompt} ${label}:`, default: detectedValue });
}
const INFO_COMMAND: string = 'i';
const DOCS_NOTICE: string =
'For more information, please visit https://docs.hyperlane.xyz.';
export async function inputWithInfo({
message,
info = 'No additional information available.',
defaultAnswer,
}: {
message: string;
info?: string;
defaultAnswer?: string;
}): Promise<string> {
let answer: string = '';
do {
answer = await input({
message: message.concat(` [enter '${INFO_COMMAND}' for more info]`),
default: defaultAnswer,
});
answer = answer.trim().toLowerCase();
const indentedInfo = indentYamlOrJson(`${info}\n${DOCS_NOTICE}\n`, 4);
if (answer === INFO_COMMAND) logGray(indentedInfo);
} while (answer === INFO_COMMAND);
return answer;
}

@ -68,7 +68,7 @@ async function addressToImpersonatedSigner(
if (address.length != ETHEREUM_ADDRESS_LENGTH)
throw new Error('Invalid address length.');
else if (ethers.utils.isHexString(ensure0x(formattedKey)))
return await impersonateAccount(address);
return impersonateAccount(address);
else throw new Error('Invalid address format');
}
@ -93,7 +93,7 @@ async function retrieveKey(
): Promise<string> {
if (skipConfirmation) throw new Error(`No private key provided`);
else
return await input({
return input({
message: `Please enter private key or use the HYP_KEY environment variable.`,
});
}

@ -1,6 +1,9 @@
import select from '@inquirer/select';
import { Token } from '@hyperlane-xyz/sdk';
import { IRegistry } from '@hyperlane-xyz/registry';
import { Token, WarpCoreConfig } from '@hyperlane-xyz/sdk';
import { logGreen, logRed } from '../logger.js';
export async function runTokenSelectionStep(
tokens: Token[],
@ -17,3 +20,32 @@ export async function runTokenSelectionStep(
})) as string;
return routerAddress;
}
export async function selectRegistryWarpRoute(
registry: IRegistry,
symbol: string,
): Promise<WarpCoreConfig> {
const matching = await registry.getWarpRoutes({
symbol,
});
const routes = Object.entries(matching);
let warpCoreConfig: WarpCoreConfig;
if (routes.length === 0) {
logRed(`No warp routes found for symbol ${symbol}`);
process.exit(0);
} else if (routes.length === 1) {
warpCoreConfig = routes[0][1];
} else {
logGreen(`Multiple warp routes found for symbol ${symbol}`);
const chosenRouteId = await select({
message: 'Select from matching warp routes',
choices: routes.map(([routeId, _]) => ({
value: routeId,
})),
});
warpCoreConfig = matching[chosenRouteId];
}
return warpCoreConfig;
}

@ -24,7 +24,7 @@ export async function getValidatorAddress({
region?: string;
bucket?: string;
keyId?: string;
}) {
}): Promise<void> {
if (!bucket && !keyId) {
throw new Error('Must provide either an S3 bucket or a KMS Key ID.');
}
@ -38,7 +38,7 @@ export async function getValidatorAddress({
assert(secretKey, 'No secret access key set.');
assert(region, 'No AWS region set.');
let validatorAddress;
let validatorAddress: string;
if (bucket) {
validatorAddress = await getAddressFromBucket(
bucket,
@ -68,7 +68,7 @@ async function getAddressFromBucket(
accessKeyId: string,
secretAccessKey: string,
region: string,
) {
): Promise<string> {
const s3Client = new S3Client({
region: region,
credentials: {
@ -101,7 +101,7 @@ async function getAddressFromKey(
accessKeyId: string,
secretAccessKey: string,
region: string,
) {
): Promise<string> {
const client = new KMSClient({
region: region,
credentials: {
@ -138,28 +138,28 @@ function getEthereumAddress(publicKey: Buffer): string {
return `0x${address.slice(-40)}`; // take last 20 bytes as ethereum address
}
async function getAccessKeyId(skipConfirmation: boolean) {
async function getAccessKeyId(skipConfirmation: boolean): Promise<string> {
if (skipConfirmation) throw new Error('No AWS access key ID set.');
else
return await input({
return input({
message:
'Please enter AWS access key ID or use the AWS_ACCESS_KEY_ID environment variable.',
});
}
async function getSecretAccessKey(skipConfirmation: boolean) {
async function getSecretAccessKey(skipConfirmation: boolean): Promise<string> {
if (skipConfirmation) throw new Error('No AWS secret access key set.');
else
return await input({
return input({
message:
'Please enter AWS secret access key or use the AWS_SECRET_ACCESS_KEY environment variable.',
});
}
async function getRegion(skipConfirmation: boolean) {
async function getRegion(skipConfirmation: boolean): Promise<string> {
if (skipConfirmation) throw new Error('No AWS region set.');
else
return await input({
return input({
message:
'Please enter AWS region or use the AWS_REGION environment variable.',
});

@ -0,0 +1,117 @@
import { MerkleTreeHook__factory } from '@hyperlane-xyz/core';
import { HyperlaneCore, S3Validator } from '@hyperlane-xyz/sdk';
import { Address } from '@hyperlane-xyz/utils';
import { CommandContext } from '../context/types.js';
import { errorRed, logBlue, logGreen, warnYellow } from '../logger.js';
export const checkValidatorSetup = async (
context: CommandContext,
chain: string,
validators: Set<Address>,
) => {
const { multiProvider, registry } = context;
const addresses = await registry.getAddresses();
const core = HyperlaneCore.fromAddressesMap(addresses, multiProvider);
const validatorAnnounce = core.getContracts(chain).validatorAnnounce;
const merkleTreeHook = MerkleTreeHook__factory.connect(
addresses[chain].merkleTreeHook,
multiProvider.getProvider(chain),
);
let merkleTreeLatestCheckpointIndex: number | undefined;
try {
const [_, latestCheckpointIndex] = await merkleTreeHook.latestCheckpoint();
merkleTreeLatestCheckpointIndex = latestCheckpointIndex;
logBlue(
`\nLatest checkpoint index of incremental merkle tree: ${merkleTreeLatestCheckpointIndex}\n`,
);
} catch (err) {
warnYellow(
` Failed to fetch latest checkpoint index of merkleTreeHook on ${chain}: ${err} \n`,
);
}
const errorSet = new Set<string>();
const validatorsArray = Array.from(validators);
let validatorStorageLocations: string[][] | undefined;
try {
validatorStorageLocations =
await validatorAnnounce.getAnnouncedStorageLocations(validatorsArray);
} catch (e) {
errorSet.add('Failed to read announced storage locations on chain.');
}
if (validatorStorageLocations) {
for (let i = 0; i < validatorsArray.length; i++) {
const validator = validatorsArray[i];
const storageLocations = validatorStorageLocations[i];
if (storageLocations.length === 0) {
errorRed(`❌ Validator ${validator} has not been announced\n`);
errorSet.add('Some validators have not been announced.');
continue;
}
const s3StorageLocation = storageLocations[0];
let s3Validator: S3Validator;
try {
s3Validator = await S3Validator.fromStorageLocation(s3StorageLocation);
} catch (e) {
errorRed(
`❌ Failed to fetch storage locations for validator ${validator}, this may be due to the storage location not being an S3 bucket\n\n`,
);
errorSet.add('Failed to fetch storage locations for some validators.');
continue;
}
const latestCheckpointIndex =
await s3Validator.getLatestCheckpointIndex();
logBlue(
`✅ Validator ${validator} announced\nstorage location: ${s3StorageLocation}\nlatest checkpoint index: ${latestCheckpointIndex}`,
);
// check is latestCheckpointIndex is within 1% of the merkleTreeLatestCheckpointIndex
if (merkleTreeLatestCheckpointIndex) {
const diff = Math.abs(
latestCheckpointIndex - merkleTreeLatestCheckpointIndex,
);
if (diff > merkleTreeLatestCheckpointIndex / 100) {
errorRed(
`❌ Validator is not signing the latest available checkpoint\n\n`,
);
errorSet.add(
`Some validators are not signing the latest available checkpoint`,
);
} else {
logBlue(
`✅ Validator is signing the latest available checkpoint\n\n`,
);
}
} else {
warnYellow(
` Cannot compare validator checkpoint signatures to latest checkpoint in the incremental merkletree, merkletree checkpoint could not be read\n`,
);
}
}
}
if (errorSet.size > 0) {
errorRed(
`\n❌ Validator pre flight check failed:\n${Array.from(errorSet).join(
'\n',
)}`,
);
process.exit(1);
} else {
logGreen(`\n✅ Validator pre flight check passed`);
}
};

@ -0,0 +1,69 @@
import { MerkleTreeHook, ValidatorAnnounce } from '@hyperlane-xyz/core';
import { S3Validator } from '@hyperlane-xyz/sdk';
import { logDebug } from '../logger.js';
export const getLatestMerkleTreeCheckpointIndex = async (
merkleTreeHook: MerkleTreeHook,
chainName?: string,
): Promise<number | undefined> => {
try {
const [_, latestCheckpointIndex] = await merkleTreeHook.latestCheckpoint();
return latestCheckpointIndex;
} catch (err) {
const debugMessage = `Failed to get latest checkpoint index from merkleTreeHook contract ${
chainName ? `on ${chainName}` : ''
} : ${err}`;
logDebug(debugMessage);
return undefined;
}
};
export const getValidatorStorageLocations = async (
validatorAnnounce: ValidatorAnnounce,
validators: string[],
chainName?: string,
): Promise<string[][] | undefined> => {
try {
return await validatorAnnounce.getAnnouncedStorageLocations(validators);
} catch (err) {
const debugMessage = `Failed to get announced storage locations from validatorAnnounce contract ${
chainName ? `on ${chainName}` : ''
} : ${err}`;
logDebug(debugMessage);
return undefined;
}
};
export const getLatestValidatorCheckpointIndexAndUrl = async (
s3StorageLocation: string,
): Promise<[number, string] | undefined> => {
let s3Validator: S3Validator;
try {
s3Validator = await S3Validator.fromStorageLocation(s3StorageLocation);
} catch (err) {
logDebug(
`Failed to instantiate S3Validator at location ${s3StorageLocation}: ${err}`,
);
return undefined;
}
try {
const latestCheckpointIndex = await s3Validator.getLatestCheckpointIndex();
return [latestCheckpointIndex, s3Validator.getLatestCheckpointUrl()];
} catch (err) {
logDebug(
`Failed to get latest checkpoint index from S3Validator at location ${s3StorageLocation}: ${err}`,
);
return undefined;
}
};
export const isValidatorSigningLatestCheckpoint = (
latestValidatorCheckpointIndex: number,
latestMerkleTreeCheckpointIndex: number,
): boolean => {
const diff = Math.abs(
latestValidatorCheckpointIndex - latestMerkleTreeCheckpointIndex,
);
return diff < latestMerkleTreeCheckpointIndex / 100;
};

@ -1 +1 @@
export const VERSION = '3.16.0';
export const VERSION = '4.0.0';

@ -1,5 +1,25 @@
# @hyperlane-xyz/helloworld
## 4.0.0
### Minor Changes
- 6398aab72: Upgrade registry to 2.1.1
- bf7ad09da: feat(cli): add `warp --symbol` flag
### Patch Changes
- Updated dependencies [44cc9bf6b]
- Updated dependencies [b05ae38ac]
- Updated dependencies [9304fe241]
- Updated dependencies [bdcbe1d16]
- Updated dependencies [6b63c5d82]
- Updated dependencies [e38d31685]
- Updated dependencies [e0f226806]
- Updated dependencies [6db9fa9ad]
- @hyperlane-xyz/core@4.0.0
- @hyperlane-xyz/sdk@4.0.0
## 3.16.0
### Patch Changes

@ -1,11 +1,11 @@
{
"name": "@hyperlane-xyz/helloworld",
"description": "A basic skeleton of an Hyperlane app",
"version": "3.16.0",
"version": "4.0.0",
"dependencies": {
"@hyperlane-xyz/core": "3.16.0",
"@hyperlane-xyz/core": "4.0.0",
"@hyperlane-xyz/registry": "2.1.1",
"@hyperlane-xyz/sdk": "3.16.0",
"@hyperlane-xyz/sdk": "4.0.0",
"@openzeppelin/contracts-upgradeable": "^4.9.3",
"ethers": "^5.7.2"
},

@ -38,8 +38,9 @@ export class HelloWorldDeployer extends HyperlaneRouterDeployer<
async deployContracts(chain: ChainName, config: HelloWorldConfig) {
const router = await this.deployContract(chain, 'router', [
config.mailbox,
config.hook ?? ethers.constants.AddressZero,
ethers.constants.AddressZero,
]);
await super.configureClient(chain, router, config);
return {
router,
};

@ -1,5 +1,27 @@
# @hyperlane-xyz/infra
## 4.0.0
### Minor Changes
- 6398aab72: Upgrade registry to 2.1.1
- bf7ad09da: feat(cli): add `warp --symbol` flag
### Patch Changes
- Updated dependencies [b05ae38ac]
- Updated dependencies [9304fe241]
- Updated dependencies [6398aab72]
- Updated dependencies [bdcbe1d16]
- Updated dependencies [6b63c5d82]
- Updated dependencies [bf7ad09da]
- Updated dependencies [e38d31685]
- Updated dependencies [e0f226806]
- Updated dependencies [6db9fa9ad]
- @hyperlane-xyz/sdk@4.0.0
- @hyperlane-xyz/helloworld@4.0.0
- @hyperlane-xyz/utils@4.0.0
## 3.16.0
### Minor Changes

@ -7,7 +7,6 @@ import {
CoreConfig,
FallbackRoutingHookConfig,
HookType,
IgpHookConfig,
IsmType,
MerkleTreeHookConfig,
MultisigConfig,
@ -59,6 +58,7 @@ export const core: ChainMap<CoreConfig> = objMap(owners, (local, owner) => {
const pausableIsm: PausableIsmConfig = {
type: IsmType.PAUSABLE,
paused: false,
owner: DEPLOYER, // keep pausable hot
};
@ -72,13 +72,11 @@ export const core: ChainMap<CoreConfig> = objMap(owners, (local, owner) => {
type: HookType.MERKLE_TREE,
};
const igpHook: IgpHookConfig = {
type: HookType.INTERCHAIN_GAS_PAYMASTER,
...igp[local],
};
const igpHook = igp[local];
const pausableHook: PausableHookConfig = {
type: HookType.PAUSABLE,
paused: false,
owner: DEPLOYER, // keep pausable hot
};
const aggregationHooks = objMap(

@ -3,6 +3,7 @@ import { BigNumber, ethers } from 'ethers';
import {
ChainMap,
ChainName,
HookType,
IgpConfig,
TOKEN_EXCHANGE_RATE_DECIMALS,
defaultMultisigConfigs,
@ -57,7 +58,10 @@ const storageGasOracleConfig: AllStorageGasOracleConfigs =
(local) => remoteOverhead(local),
);
export const igp: ChainMap<IgpConfig> = objMap(owners, (local, owner) => ({
export const igp: ChainMap<IgpConfig> = objMap(
owners,
(local, owner): IgpConfig => ({
type: HookType.INTERCHAIN_GAS_PAYMASTER,
...owner,
ownerOverrides: {
...owner.ownerOverrides,
@ -73,4 +77,5 @@ export const igp: ChainMap<IgpConfig> = objMap(owners, (local, owner) => ({
]),
),
oracleConfig: storageGasOracleConfig[local],
}));
}),
);

@ -6,7 +6,6 @@ import {
CoreConfig,
FallbackRoutingHookConfig,
HookType,
IgpHookConfig,
IsmType,
MerkleTreeHookConfig,
ProtocolFeeHookConfig,
@ -34,10 +33,7 @@ export const core: ChainMap<CoreConfig> = objMap(owners, (local, owner) => {
type: HookType.MERKLE_TREE,
};
const igpHook: IgpHookConfig = {
type: HookType.INTERCHAIN_GAS_PAYMASTER,
...igp[local],
};
const igpHook = igp[local];
const aggregationHook: AggregationHookConfig = {
type: HookType.AGGREGATION,

@ -1,7 +1,6 @@
import {
ChainMap,
ChainName,
GasOracleContractType,
HookType,
IgpConfig,
multisigIsmVerificationCost,
} from '@hyperlane-xyz/sdk';
@ -11,16 +10,9 @@ import { testChainNames } from './chains.js';
import { multisigIsm } from './multisigIsm.js';
import { owners } from './owners.js';
function getGasOracles(local: ChainName) {
return Object.fromEntries(
exclude(local, testChainNames).map((name) => [
name,
GasOracleContractType.StorageGasOracle,
]),
);
}
export const igp: ChainMap<IgpConfig> = objMap(owners, (chain, ownerConfig) => {
export const igp: ChainMap<IgpConfig> = objMap(
owners,
(chain, ownerConfig): IgpConfig => {
const overhead = Object.fromEntries(
exclude(chain, testChainNames).map((remote) => [
remote,
@ -31,10 +23,12 @@ export const igp: ChainMap<IgpConfig> = objMap(owners, (chain, ownerConfig) => {
]),
);
return {
type: HookType.INTERCHAIN_GAS_PAYMASTER,
oracleKey: ownerConfig.owner as Address, // owner can be AccountConfig
beneficiary: ownerConfig.owner as Address, // same as above
gasOracleType: getGasOracles(chain),
overhead,
oracleConfig: {},
...ownerConfig,
};
});
},
);

@ -1,11 +1,18 @@
import { ChainMap, IsmType, MultisigIsmConfig } from '@hyperlane-xyz/sdk';
import {
ChainMap,
IsmType,
MultisigIsmConfig,
TestChainName,
} from '@hyperlane-xyz/sdk';
import { Address } from '@hyperlane-xyz/utils';
// the addresses here must line up with the e2e test's validator addresses
// Validators are anvil accounts 4-6
export const chainToValidator: Record<string, string> = {
// Validators are anvil accounts 4-7
export const chainToValidator: Record<TestChainName, Address> = {
test1: '0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65',
test2: '0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc',
test3: '0x976EA74026E726554dB657fA54763abd0C3a0aa9',
test4: '0x14dC79964da2C08b23698B3D3cc7Ca32193d9955',
};
export const merkleRootMultisig = (validatorKey: string): MultisigIsmConfig => {
@ -26,8 +33,9 @@ export const messageIdMultisig = (validatorKey: string): MultisigIsmConfig => {
// the addresses here must line up with the e2e test's validator addresses
export const multisigIsm: ChainMap<MultisigIsmConfig> = {
// Validators are anvil accounts 4-6
// Validators are anvil accounts 4-7
test1: messageIdMultisig(chainToValidator['test1']),
test2: merkleRootMultisig(chainToValidator['test2']),
test3: messageIdMultisig(chainToValidator['test3']),
test4: messageIdMultisig(chainToValidator['test4']),
};

@ -7,7 +7,6 @@ import {
CoreConfig,
FallbackRoutingHookConfig,
HookType,
IgpHookConfig,
IsmType,
MerkleTreeHookConfig,
MultisigConfig,
@ -61,6 +60,7 @@ export const core: ChainMap<CoreConfig> = objMap(
const pausableIsm: PausableIsmConfig = {
type: IsmType.PAUSABLE,
paused: false,
...ownerConfig,
};
@ -74,13 +74,11 @@ export const core: ChainMap<CoreConfig> = objMap(
type: HookType.MERKLE_TREE,
};
const igpHook: IgpHookConfig = {
type: HookType.INTERCHAIN_GAS_PAYMASTER,
...igp[local],
};
const igpHook = igp[local];
const pausableHook: PausableHookConfig = {
type: HookType.PAUSABLE,
paused: false,
...ownerConfig,
};

@ -1,5 +1,6 @@
import {
ChainMap,
HookType,
IgpConfig,
defaultMultisigConfigs,
multisigIsmVerificationCost,
@ -10,8 +11,11 @@ import { storageGasOracleConfig } from './gas-oracle.js';
import { owners } from './owners.js';
import { supportedChainNames } from './supportedChainNames.js';
export const igp: ChainMap<IgpConfig> = objMap(owners, (chain, ownerConfig) => {
export const igp: ChainMap<IgpConfig> = objMap(
owners,
(chain, ownerConfig): IgpConfig => {
return {
type: HookType.INTERCHAIN_GAS_PAYMASTER,
...ownerConfig,
oracleKey: ownerConfig.owner as Address,
beneficiary: ownerConfig.owner as Address,
@ -27,4 +31,5 @@ export const igp: ChainMap<IgpConfig> = objMap(owners, (chain, ownerConfig) => {
]),
),
};
});
},
);

@ -1,7 +1,7 @@
{
"name": "@hyperlane-xyz/infra",
"description": "Infrastructure utilities for the Hyperlane Network",
"version": "3.16.0",
"version": "4.0.0",
"dependencies": {
"@arbitrum/sdk": "^3.0.0",
"@aws-sdk/client-iam": "^3.74.0",
@ -13,10 +13,10 @@
"@ethersproject/hardware-wallets": "^5.7.0",
"@ethersproject/providers": "^5.7.2",
"@google-cloud/secret-manager": "^5.5.0",
"@hyperlane-xyz/helloworld": "3.16.0",
"@hyperlane-xyz/helloworld": "4.0.0",
"@hyperlane-xyz/registry": "2.1.1",
"@hyperlane-xyz/sdk": "3.16.0",
"@hyperlane-xyz/utils": "3.16.0",
"@hyperlane-xyz/sdk": "4.0.0",
"@hyperlane-xyz/utils": "4.0.0",
"@nomiclabs/hardhat-etherscan": "^3.0.3",
"@solana/web3.js": "^1.78.0",
"asn1.js": "5.4.1",
@ -25,6 +25,7 @@
"json-stable-stringify": "^1.1.1",
"prom-client": "^14.0.1",
"prompts": "^2.4.2",
"yaml": "^2.4.5",
"yargs": "^17.7.2"
},
"devDependencies": {

@ -10,7 +10,6 @@ import {
InterchainAccountChecker,
InterchainQuery,
InterchainQueryChecker,
resolveOrDeployAccountOwner,
} from '@hyperlane-xyz/sdk';
import { Contexts } from '../config/contexts.js';
@ -36,15 +35,15 @@ import { getHelloWorldApp } from './helloworld/utils.js';
function getArgs() {
return withChain(withModuleAndFork(withContext(getRootArgs())))
.boolean('asdeployer')
.default('asdeployer', false)
.boolean('asDeployer')
.default('asDeployer', false)
.boolean('govern')
.default('govern', false)
.alias('g', 'govern').argv;
}
async function check() {
const { fork, govern, module, environment, context, chain, asdeployer } =
const { fork, govern, module, environment, context, chain, asDeployer } =
await getArgs();
const envConfig = getEnvironmentConfig(environment);
let multiProvider = await envConfig.getMultiProvider();
@ -58,13 +57,7 @@ async function check() {
[fork]: { blocks: { confirmations: 0 } },
});
const owner = asdeployer
? DEPLOYER
: await resolveOrDeployAccountOwner(
multiProvider,
fork,
envConfig.core[fork].owner,
);
const owner = asDeployer ? DEPLOYER : envConfig.core[fork].owner;
const signer = await impersonateAccount(owner, 1e18);
multiProvider.setSigner(fork, signer);

@ -8,7 +8,6 @@ import {
ChainMap,
ContractVerifier,
ExplorerLicenseType,
FallbackRoutingHookConfig,
HypERC20Deployer,
HyperlaneCoreDeployer,
HyperlaneDeployer,
@ -26,7 +25,6 @@ import { objFilter, objMap } from '@hyperlane-xyz/utils';
import { Contexts } from '../config/contexts.js';
import { core as coreConfig } from '../config/environments/mainnet3/core.js';
import { DEPLOYER } from '../config/environments/mainnet3/owners.js';
import { getEnvAddresses } from '../config/registry.js';
import { getWarpConfig } from '../config/warp.js';
import { deployWithArtifacts } from '../src/deployment/deploy.js';
@ -200,11 +198,7 @@ async function main() {
);
// Config is intended to be changed for ad-hoc use cases:
config = {
ethereum: {
...(coreConfig.ethereum.defaultHook as FallbackRoutingHookConfig)
.domains.ancient8,
owner: DEPLOYER,
},
ethereum: coreConfig.ethereum.defaultHook,
};
} else {
console.log(`Skipping ${module}, deployer unimplemented`);

@ -0,0 +1,150 @@
import { writeFileSync } from 'fs';
import { stringify as yamlStringify } from 'yaml';
import { GithubRegistry } from '@hyperlane-xyz/registry';
import {
IsmType,
TokenRouterConfig,
TokenType,
WarpRouteDeployConfig,
WarpRouteDeployConfigSchema,
buildAggregationIsmConfigs,
} from '@hyperlane-xyz/sdk';
const lockbox = '0xC8140dA31E6bCa19b287cC35531c2212763C2059';
const xERC20 = '0x2416092f143378750bb29b79eD961ab195CcEea5';
const lockboxChain = 'ethereum';
const chainsToDeploy = [
'arbitrum',
'optimism',
'base',
'blast',
'bsc',
'mode',
'linea',
'ethereum',
];
const ezEthValidators = {
arbitrum: {
threshold: 1,
validators: [
'0xc27032c6bbd48c20005f552af3aaa0dbf14260f3', // Renzo
'0x9bCcFAd3BD12Ef0Ee8aE839dD9ED7835BcCaDc9D', // Everclear
],
},
optimism: {
threshold: 1,
validators: [
'0xe2593D205F5E7F74A50fA900824501084E092eBd', // Renzo
'0x6f4cb8e96db5d44422a4495faa73fffb9d30e9e2', // Everclear
],
},
base: {
threshold: 1,
validators: [
'0x25BA4eE5268CbfB8D69BAc531Aa10368778702BD', // Renzo
'0x9ec803b503e9c7d2611e231521ef3fde73f7a21c', // Everclear
],
},
blast: {
threshold: 1,
validators: [
'0x54Bb0036F777202371429e062FE6AEE0d59442F9', // Renzo
'0x1652d8ba766821cf01aeea34306dfc1cab964a32', // Everclear
],
},
bsc: {
threshold: 1,
validators: [
'0x3156Db97a3B3e2dcc3D69FdDfD3e12dc7c937b6D', // Renzo
'0x9a0326c43e4713ae2477f09e0f28ffedc24d8266', // Everclear
],
},
mode: {
threshold: 1,
validators: [
'0x7e29608C6E5792bBf9128599ca309Be0728af7B4', // Renzo
'0x456fbbe05484fc9f2f38ea09648424f54d6872be', // Everclear
],
},
linea: {
threshold: 1,
validators: [
'0xcb3e44EdD2229860bDBaA58Ba2c3817D111bEE9A', // Renzo
'0x06a5a2a429560034d38bf62ca6d470942535947e', // Everclear
],
},
ethereum: {
threshold: 1,
validators: [
'0xc7f7b94a6BaF2FFFa54DfE1dDE6E5Fcbb749e04f', // Renzo
'0x1fd889337F60986aa57166bc5AC121eFD13e4fdd', // Everclear
],
},
};
const zeroAddress = '0x0000000000000000000000000000000000000001';
async function main() {
const registry = new GithubRegistry();
const tokenConfig: WarpRouteDeployConfig =
Object.fromEntries<TokenRouterConfig>(
await Promise.all(
chainsToDeploy.map(
async (chain): Promise<[string, TokenRouterConfig]> => {
const ret: [string, TokenRouterConfig] = [
chain,
{
isNft: false,
type:
chain === lockboxChain
? TokenType.XERC20Lockbox
: TokenType.XERC20,
token: chain === lockboxChain ? lockbox : xERC20,
owner: zeroAddress,
mailbox: (await registry.getChainAddresses(chain))!.mailbox,
interchainSecurityModule: {
type: IsmType.AGGREGATION,
threshold: 2,
modules: [
{
type: IsmType.ROUTING,
owner: zeroAddress,
domains: buildAggregationIsmConfigs(
chain,
chainsToDeploy,
ezEthValidators,
),
},
{
type: IsmType.FALLBACK_ROUTING,
domains: {},
owner: zeroAddress,
},
],
},
},
];
return ret;
},
),
),
);
const parsed = WarpRouteDeployConfigSchema.safeParse(tokenConfig);
if (!parsed.success) {
console.dir(parsed.error.format(), { depth: null });
return;
}
writeFileSync(
'renzo-warp-route-config.yaml',
yamlStringify(parsed.data, null, 2),
);
}
main().catch(console.error).then(console.log);

@ -12,7 +12,6 @@ import {
MultiProvider,
ProviderType,
TypedTransactionReceipt,
resolveOrDeployAccountOwner,
} from '@hyperlane-xyz/sdk';
import {
Address,
@ -234,12 +233,11 @@ async function main(): Promise<boolean> {
}
chains.map(async (chain) => {
const owner = await resolveOrDeployAccountOwner(
multiProvider,
return updateWalletBalanceMetricFor(
app,
chain,
coreConfig.owners[chain].owner,
);
return updateWalletBalanceMetricFor(app, chain, owner);
});
// Incremented each time an entire cycle has occurred
@ -358,11 +356,7 @@ async function main(): Promise<boolean> {
messagesSendCount.labels({ ...labels, status: 'failure' }).inc();
errorOccurred = true;
}
const owner = await resolveOrDeployAccountOwner(
multiProvider,
origin,
coreConfig.owners[origin].owner,
);
const owner = coreConfig.owners[origin].owner;
updateWalletBalanceMetricFor(app, origin, owner).catch((e) => {
logger.warn('Failed to update wallet balance for chain', {
chain: origin,

@ -1,3 +1,4 @@
import { Provider } from '@ethersproject/providers';
import { Wallet } from 'ethers';
import fs from 'fs';
import yargs from 'yargs';
@ -100,15 +101,28 @@ async function main() {
const { timeout, defaultHook, requiredHook, mineforever } = args;
let messages = args.messages;
// Limit the test chains to a subset of the known chains
// E2E in Rust only knows about test1, test2 and test3
const kathyTestChains = [
TestChainName.test1,
TestChainName.test2,
TestChainName.test3,
];
// Create a multi-provider with a signer
const signer = new Wallet(ANVIL_KEY);
const multiProvider = MultiProvider.createTestMultiProvider({ signer });
// Get the provider for the first chain
const provider = multiProvider.getProvider(TestChainName.test1);
// Create core from addresses
const addresses = JSON.parse(
fs.readFileSync('./config/environments/test/core/addresses.json', 'utf8'),
);
const core = HyperlaneCore.fromAddressesMap(addresses, multiProvider);
// helper function to get a random element from a list
const randomElement = <T>(list: T[]) =>
list[Math.floor(Math.random() * list.length)];
@ -121,9 +135,11 @@ async function main() {
const run_forever = messages === 0;
while (run_forever || messages-- > 0) {
// Round robin origin chain
const local = core.chains()[messages % core.chains().length];
const local = kathyTestChains[messages % kathyTestChains.length];
// Random remote chain
const remote: ChainName = randomElement(await core.remoteChains(local));
const remote: ChainName = randomElement(
kathyTestChains.filter((c) => c !== local),
);
const remoteId = multiProvider.getDomainId(remote);
const contracts = core.getContracts(local);
const mailbox = contracts.mailbox;

@ -200,12 +200,18 @@ export abstract class HyperlaneAppGovernor<
accountConfig.owner,
)} on ${origin}`,
);
const callRemote = await this.interchainAccount.getCallRemote(
origin,
chain,
[call],
accountConfig,
);
const callRemote = await this.interchainAccount.getCallRemote({
chain: origin,
destination: chain,
innerCalls: [
{
to: call.to,
data: call.data,
value: call.value?.toString() || '0',
},
],
config: accountConfig,
});
if (!callRemote.to || !callRemote.data) {
return SubmissionType.MANUAL;
}

@ -26,7 +26,6 @@ import {
TestChainName,
TestCoreApp,
TestCoreDeployer,
resolveOrDeployAccountOwner,
} from '@hyperlane-xyz/sdk';
import { Address, CallData, eqAddress } from '@hyperlane-xyz/utils';
@ -134,11 +133,7 @@ describe('ICA governance', async () => {
localRouter: remote.address,
};
accountOwner = await resolveOrDeployAccountOwner(
multiProvider,
remoteChain,
accountConfig,
);
accountOwner = await icaApp.deployAccount(remoteChain, accountConfig);
const recipientF = new TestRecipient__factory(signer);
recipient = await recipientF.deploy();

@ -1,5 +1,24 @@
# @hyperlane-xyz/sdk
## 4.0.0
### Minor Changes
- b05ae38ac: Gracefully handle RPC failures during warp send & fix deriving hook error that prevents warp and core test messages on the cli.
- 9304fe241: Use metadata builders in message relaying
- bdcbe1d16: Add EvmWarpModule with create()
- e38d31685: Add logic to set smart provider log level to disable provider logs during Warp TokenType derive
- e0f226806: - Enables creation of new Hooks through the `EvmHookModule`.
- Introduces an `EvmModuleDeployer` to perform the barebones tasks of deploying contracts/proxies.
- 6db9fa9ad: Implement hyperlane warp deploy
### Patch Changes
- 6b63c5d82: Adds deployment support for IsmConfig within a WarpRouteConfig
- Updated dependencies [44cc9bf6b]
- @hyperlane-xyz/core@4.0.0
- @hyperlane-xyz/utils@4.0.0
## 3.16.0
### Minor Changes

@ -1,13 +1,13 @@
{
"name": "@hyperlane-xyz/sdk",
"description": "The official SDK for the Hyperlane Network",
"version": "3.16.0",
"version": "4.0.0",
"dependencies": {
"@aws-sdk/client-s3": "^3.74.0",
"@cosmjs/cosmwasm-stargate": "^0.31.3",
"@cosmjs/stargate": "^0.31.3",
"@hyperlane-xyz/core": "3.16.0",
"@hyperlane-xyz/utils": "3.16.0",
"@hyperlane-xyz/core": "4.0.0",
"@hyperlane-xyz/utils": "4.0.0",
"@safe-global/api-kit": "1.3.0",
"@safe-global/protocol-kit": "1.3.0",
"@solana/spl-token": "^0.3.8",

@ -75,6 +75,6 @@ export class S3Wrapper {
url(key: string): string {
const Key = this.formatKey(key);
return `https://${this.config.bucket}.${this.config.region}.s3.amazonaws.com/${Key}`;
return `https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/${Key}`;
}
}

@ -103,4 +103,8 @@ export class S3Validator extends BaseValidator {
storageLocation(): string {
return `${LOCATION_PREFIX}/${this.s3Bucket.config.bucket}/${this.s3Bucket.config.region}`;
}
getLatestCheckpointUrl(): string {
return this.s3Bucket.url(LATEST_KEY);
}
}

@ -10,6 +10,7 @@ export enum TestChainName {
test1 = 'test1',
test2 = 'test2',
test3 = 'test3',
test4 = 'test4',
}
export const testChains: Array<ChainName> = Object.values(TestChainName);
@ -65,10 +66,19 @@ export const test3: ChainMetadata = {
name: 'test3',
};
export const test4: ChainMetadata = {
...test1,
chainId: 31337,
displayName: 'Test 4',
domainId: 31337,
name: 'test4',
};
export const testChainMetadata: ChainMap<ChainMetadata> = {
test1,
test2,
test3,
test4,
};
export const testCosmosChain: ChainMetadata = {

@ -146,6 +146,15 @@ export function attachContractsMapAndGetForeignDeployments<
};
}
export function attachAndConnectContracts<F extends HyperlaneFactories>(
addresses: HyperlaneAddresses<F>,
factories: F,
connection: Connection,
): HyperlaneContracts<F> {
const contracts = attachContracts(addresses, factories);
return connectContracts(contracts, connection);
}
export function connectContracts<F extends HyperlaneFactories>(
contracts: HyperlaneContracts<F>,
connection: Connection,

@ -5,7 +5,7 @@ import { Annotated, ProtocolType } from '@hyperlane-xyz/utils';
import { ProtocolTypedTransaction } from '../providers/ProviderType.js';
import { ChainNameOrId } from '../types.js';
export type HyperlaneModuleArgs<
export type HyperlaneModuleParams<
TConfig,
TAddressMap extends Record<string, any>,
> = {
@ -22,7 +22,7 @@ export abstract class HyperlaneModule<
protected abstract readonly logger: Logger;
protected constructor(
protected readonly args: HyperlaneModuleArgs<TConfig, TAddressMap>,
protected readonly args: HyperlaneModuleParams<TConfig, TAddressMap>,
) {}
public serialize(): TAddressMap {
@ -32,7 +32,7 @@ export abstract class HyperlaneModule<
public abstract read(): Promise<TConfig>;
public abstract update(
config: TConfig,
): Promise<Annotated<ProtocolTypedTransaction<TProtocol>[]>>;
): Promise<Annotated<ProtocolTypedTransaction<TProtocol>['transaction'][]>>;
// /*
// Types and static methods can be challenging. Ensure each implementation includes a static create function.

@ -8,7 +8,7 @@ import { objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import { TestChainName, testChains } from '../consts/testChains.js';
import { HyperlaneContractsMap } from '../contracts/types.js';
import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js';
import { HookConfig } from '../hook/types.js';
import { DerivedHookConfig } from '../hook/EvmHookReader.js';
import { DerivedIsmConfig } from '../ism/EvmIsmReader.js';
import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
import { AggregationIsmConfig, IsmType } from '../ism/types.js';
@ -127,7 +127,7 @@ describe('core', async () => {
});
async function deriveCoreConfig(chainName: string, mailboxAddress: string) {
return await new EvmCoreReader(multiProvider, chainName).deriveCoreConfig(
return new EvmCoreReader(multiProvider, chainName).deriveCoreConfig(
mailboxAddress,
);
}
@ -140,12 +140,12 @@ describe('core', async () => {
);
// Cast because we don't expect the 'string' type
const defaultIsmOnchain =
const { address: _, ...defaultIsmOnchain } =
coreConfigOnChain.defaultIsm as DerivedIsmConfig;
const defaultIsmTest = coreConfig[chainName]
.defaultIsm as DerivedIsmConfig;
expect(defaultIsmOnchain.type).to.be.equal(defaultIsmTest.type);
expect(defaultIsmOnchain).to.deep.equal(defaultIsmTest);
}),
);
});
@ -158,12 +158,12 @@ describe('core', async () => {
);
// Cast because we don't expect the 'string' type
const defaultHookOnchain =
coreConfigOnChain.defaultHook as HookConfig;
const { address: _, ...defaultHookOnchain } =
coreConfigOnChain.defaultHook as DerivedHookConfig;
const defaultHookTest = coreConfig[chainName]
.defaultHook as HookConfig;
.defaultHook as DerivedHookConfig;
expect(defaultHookOnchain.type).to.be.equal(defaultHookTest.type);
expect(defaultHookOnchain).to.deep.equal(defaultHookTest);
}),
);
});
@ -174,13 +174,11 @@ describe('core', async () => {
chainName,
contract.mailbox.address,
);
const requiredHookOnchain = coreConfigOnChain.requiredHook;
const { address: _, ...requiredHookOnchain } =
coreConfigOnChain.requiredHook as DerivedHookConfig;
const requiredHookTest = coreConfig[chainName].requiredHook;
// Test all the fields
objMap(requiredHookTest, (key, value) => {
expect(requiredHookOnchain[key]).to.be.equal(value);
});
expect(requiredHookOnchain).to.deep.equal(requiredHookTest);
}),
);
});

@ -0,0 +1,164 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js';
import { expect } from 'chai';
import { constants } from 'ethers';
import hre from 'hardhat';
import {
Mailbox__factory,
ProxyAdmin__factory,
TestRecipient__factory,
TimelockController__factory,
ValidatorAnnounce__factory,
} from '@hyperlane-xyz/core';
import { objMap } from '@hyperlane-xyz/utils';
import { TestChainName } from '../consts/testChains.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { testCoreConfig } from '../test/testUtils.js';
import { EvmCoreModule } from './EvmCoreModule.js';
import { CoreConfig } from './types.js';
describe('EvmCoreModule', async () => {
const CHAIN = TestChainName.test1;
const DELAY = 1892391283182;
let config: CoreConfig;
let signer: SignerWithAddress;
let multiProvider: MultiProvider;
let evmCoreModule: EvmCoreModule;
let proxyAdminContract: any;
let mailboxContract: any;
let validatorAnnounceContract: any;
let testRecipientContract: any;
let timelockControllerContract: any;
before(async () => {
[signer] = await hre.ethers.getSigners();
multiProvider = MultiProvider.createTestMultiProvider({ signer });
config = {
...testCoreConfig([CHAIN])[CHAIN],
upgrade: {
timelock: {
delay: DELAY,
roles: {
executor: signer.address,
proposer: signer.address,
},
},
},
};
evmCoreModule = await EvmCoreModule.create({
chain: CHAIN,
config,
multiProvider,
});
const {
proxyAdmin,
mailbox,
validatorAnnounce,
testRecipient,
timelockController,
} = evmCoreModule.serialize();
proxyAdminContract = ProxyAdmin__factory.connect(
proxyAdmin!,
multiProvider.getProvider(CHAIN),
);
mailboxContract = Mailbox__factory.connect(
mailbox!,
multiProvider.getProvider(CHAIN),
);
validatorAnnounceContract = ValidatorAnnounce__factory.connect(
validatorAnnounce!,
multiProvider.getProvider(CHAIN),
);
testRecipientContract = TestRecipient__factory.connect(
testRecipient!,
multiProvider.getProvider(CHAIN),
);
timelockControllerContract = TimelockController__factory.connect(
timelockController!,
multiProvider.getProvider(CHAIN),
);
});
describe('Create', async () => {
it('should create deploy an ICA', () => {
const { interchainAccountRouter, interchainAccountIsm } =
evmCoreModule.serialize();
expect(interchainAccountIsm).to.exist;
expect(interchainAccountRouter).to.exist;
});
it('should deploy ISM factories', () => {
// Each ISM factory
const deployedContracts = evmCoreModule.serialize();
objMap(deployedContracts as any, (_, address) => {
expect(address).to.exist;
expect(address).to.not.equal(constants.AddressZero);
});
});
it('should deploy proxyAdmin', () => {
expect(evmCoreModule.serialize().proxyAdmin).to.exist;
});
it('should set proxyAdmin owner to deployer', async () => {
expect(await proxyAdminContract.owner()).to.equal(signer.address);
});
it('should deploy mailbox', async () => {
const mailboxAddress = evmCoreModule.serialize().mailbox;
expect(mailboxAddress).to.exist;
// Check that it's actually a mailbox by calling one of it's methods
expect(await mailboxContract.localDomain()).to.equal(
multiProvider.getChainId(CHAIN),
);
});
it('should set mailbox owner to config owner', async () => {
expect(await mailboxContract.owner()).to.equal(config.owner);
});
it('should deploy mailbox default Ism', async () => {
expect(await mailboxContract.defaultIsm()).to.not.equal(
constants.AddressZero,
);
});
it('should deploy mailbox default hook', async () => {
expect(await mailboxContract.defaultHook()).to.not.equal(
constants.AddressZero,
);
});
it('should deploy mailbox required hook', async () => {
expect(await mailboxContract.requiredHook()).to.not.equal(
constants.AddressZero,
);
});
it('should deploy validatorAnnounce', async () => {
expect(evmCoreModule.serialize().validatorAnnounce).to.exist;
expect(await validatorAnnounceContract.owner()).to.equal(signer.address);
});
it('should deploy testRecipient', async () => {
expect(evmCoreModule.serialize().testRecipient).to.exist;
expect(await testRecipientContract.owner()).to.equal(signer.address);
});
it('should deploy timelock if upgrade is set', async () => {
expect(evmCoreModule.serialize().timelockController).to.exist;
expect(await timelockControllerContract.getMinDelay()).to.equal(DELAY);
});
});
});

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save