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. 2
      CODE_OF_CONDUCT.md
  6. 20
      rust/Cargo.lock
  7. 4
      rust/agents/relayer/src/msg/op_queue.rs
  8. 11
      rust/agents/relayer/src/msg/op_submitter.rs
  9. 17
      rust/agents/relayer/src/msg/pending_message.rs
  10. 2
      rust/hyperlane-base/Cargo.toml
  11. 10
      rust/hyperlane-base/src/db/rocks/hyperlane_db.rs
  12. 6
      rust/hyperlane-base/src/lib.rs
  13. 3
      rust/hyperlane-base/src/settings/mod.rs
  14. 8
      rust/hyperlane-base/src/settings/trace/mod.rs
  15. 62
      rust/hyperlane-core/src/traits/pending_operation.rs
  16. 31
      rust/utils/run-locally/src/invariants.rs
  17. 24
      rust/utils/run-locally/src/utils.rs
  18. 10
      solidity/CHANGELOG.md
  19. 5
      solidity/contracts/interfaces/avs/vendored/IDelegationManager.sol
  20. 4
      solidity/package.json
  21. 2
      typescript/ccip-server/CHANGELOG.md
  22. 2
      typescript/ccip-server/package.json
  23. 38
      typescript/cli/CHANGELOG.md
  24. 43
      typescript/cli/ci-advanced-test.sh
  25. 8
      typescript/cli/cli.ts
  26. 19
      typescript/cli/examples/core-config.yaml
  27. 12
      typescript/cli/examples/hooks.yaml
  28. 9
      typescript/cli/package.json
  29. 449
      typescript/cli/src/avs/check.ts
  30. 3
      typescript/cli/src/avs/config.ts
  31. 4
      typescript/cli/src/avs/stakeRegistry.ts
  32. 69
      typescript/cli/src/commands/avs.ts
  33. 99
      typescript/cli/src/commands/config.ts
  34. 155
      typescript/cli/src/commands/core.ts
  35. 97
      typescript/cli/src/commands/deploy.ts
  36. 4
      typescript/cli/src/commands/hook.ts
  37. 4
      typescript/cli/src/commands/ism.ts
  38. 46
      typescript/cli/src/commands/options.ts
  39. 79
      typescript/cli/src/commands/registry.ts
  40. 63
      typescript/cli/src/commands/send.ts
  41. 2
      typescript/cli/src/commands/types.ts
  42. 74
      typescript/cli/src/commands/validator.ts
  43. 290
      typescript/cli/src/commands/warp.ts
  44. 75
      typescript/cli/src/config/agent.ts
  45. 193
      typescript/cli/src/config/chain.ts
  46. 71
      typescript/cli/src/config/core.ts
  47. 466
      typescript/cli/src/config/hooks.ts
  48. 432
      typescript/cli/src/config/ism.ts
  49. 18
      typescript/cli/src/config/utils.ts
  50. 76
      typescript/cli/src/config/warp.ts
  51. 7
      typescript/cli/src/context/context.ts
  52. 463
      typescript/cli/src/deploy/core.ts
  53. 3
      typescript/cli/src/deploy/dry-run.ts
  54. 56
      typescript/cli/src/deploy/utils.ts
  55. 162
      typescript/cli/src/deploy/warp.ts
  56. 8
      typescript/cli/src/logger.ts
  57. 58
      typescript/cli/src/send/message.ts
  58. 23
      typescript/cli/src/send/transfer.ts
  59. 2
      typescript/cli/src/status/message.ts
  60. 2
      typescript/cli/src/submit/submit.ts
  61. 31
      typescript/cli/src/tests/hooks.test.ts
  62. 4
      typescript/cli/src/tests/hooks/safe-parse-fail.yaml
  63. 4
      typescript/cli/src/tests/ism.test.ts
  64. 29
      typescript/cli/src/utils/chains.ts
  65. 17
      typescript/cli/src/utils/cli-options.ts
  66. 8
      typescript/cli/src/utils/files.ts
  67. 55
      typescript/cli/src/utils/input.ts
  68. 4
      typescript/cli/src/utils/keys.ts
  69. 34
      typescript/cli/src/utils/tokens.ts
  70. 20
      typescript/cli/src/validator/address.ts
  71. 117
      typescript/cli/src/validator/preFlightCheck.ts
  72. 69
      typescript/cli/src/validator/utils.ts
  73. 2
      typescript/cli/src/version.ts
  74. 20
      typescript/helloworld/CHANGELOG.md
  75. 6
      typescript/helloworld/package.json
  76. 3
      typescript/helloworld/src/deploy/deploy.ts
  77. 22
      typescript/infra/CHANGELOG.md
  78. 8
      typescript/infra/config/environments/mainnet3/core.ts
  79. 39
      typescript/infra/config/environments/mainnet3/igp.ts
  80. 6
      typescript/infra/config/environments/test/core.ts
  81. 52
      typescript/infra/config/environments/test/igp.ts
  82. 16
      typescript/infra/config/environments/test/multisigIsm.ts
  83. 8
      typescript/infra/config/environments/testnet4/core.ts
  84. 41
      typescript/infra/config/environments/testnet4/igp.ts
  85. 9
      typescript/infra/package.json
  86. 15
      typescript/infra/scripts/check-deploy.ts
  87. 8
      typescript/infra/scripts/deploy.ts
  88. 150
      typescript/infra/scripts/generate-renzo-warp-route-config.ts
  89. 12
      typescript/infra/scripts/helloworld/kathy.ts
  90. 20
      typescript/infra/scripts/send-test-messages.ts
  91. 18
      typescript/infra/src/govern/HyperlaneAppGovernor.ts
  92. 7
      typescript/infra/test/govern.hardhat-test.ts
  93. 19
      typescript/sdk/CHANGELOG.md
  94. 6
      typescript/sdk/package.json
  95. 2
      typescript/sdk/src/aws/s3.ts
  96. 4
      typescript/sdk/src/aws/validator.ts
  97. 10
      typescript/sdk/src/consts/testChains.ts
  98. 9
      typescript/sdk/src/contracts/contracts.ts
  99. 6
      typescript/sdk/src/core/AbstractHyperlaneModule.ts
  100. 24
      typescript/sdk/src/core/CoreDeployer.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

@ -11,7 +11,7 @@ This CoC applies to all members of the Hyperlane Network's community including,
1. Never harass or bully anyone. Not verbally, not physically, not sexually. Harassment will not be tolerated.
2. Never discriminate on the basis of personal characteristics or group membership.
3. Treat your fellow contributors with respect, fairness, and professionalism, especially in situations of high pressure.
4. Seek, offer, and accept objective criticism of yours and others work, strive to acknowledge the contributions of others.
4. Seek, offer, and accept objective criticism of yours and others work, strive to acknowledge the contributions of others.
5. Be transparent and honest about your qualifications and any potential conflicts of interest. Transparency is a key tenet of the Hyperlane project and we expect it from all contributors.
6. Bring an open and curious mind, the Hyperlane project is designed to enable developers to express their curiosity, experiment, and build things we couldn't have imagined ourselves.
7. Stay on track - Do your best to avoid off-topic discussion and make sure you are posting to the correct channel and repositories. Distractions are costly and it is far too easy for work to go off track.

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,79 +75,29 @@ 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({
default: false,
message:
'Do you want to set block or gas properties for this chain config?',
});
if (wantAdvancedConfig) {
const wantBlockConfig = await confirm({
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)',
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)',
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)',
validate: (value) => parseInt(value) >= 0 && parseInt(value) <= 20,
});
metadata.blocks = {
confirmations: parseInt(blockConfirmation, 10),
reorgPeriod: parseInt(blockReorgPeriod, 10),
estimateBlockTime: parseInt(blockTimeEstimate, 10),
};
}
const wantGasConfig = await confirm({
message: 'Do you want to add gas config for this chain?',
});
if (wantGasConfig) {
const isEIP1559 = await confirm({
message: 'Is your chain an EIP1559 enabled?',
});
if (isEIP1559) {
const maxFeePerGas = await input({
message: 'Enter the max fee per gas in gwei',
});
const maxPriorityFeePerGas = await input({
message: 'Enter the max priority fee per gas in gwei',
});
metadata.transactionOverrides = {
maxFeePerGas: BigInt(maxFeePerGas) * BigInt(10 ** 9),
maxPriorityFeePerGas: BigInt(maxPriorityFeePerGas) * BigInt(10 ** 9),
};
} else {
const gasPrice = await input({
message: 'Enter the gas price in gwei',
});
metadata.transactionOverrides = {
gasPrice: BigInt(gasPrice) * BigInt(10 ** 9),
};
}
}
}
await addBlockOrGasConfig(metadata);
await addNativeTokenConfig(metadata);
const parseResult = ChainMetadataSchema.safeParse(metadata);
if (parseResult.success) {
logGreen(`Chain config is valid, writing to registry`);
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(
@ -152,3 +107,97 @@ export async function createChainConfig({
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',
});
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',
});
if (wantBlockConfig) {
const blockConfirmation = await input({
message:
'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):',
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):',
validate: (value) => parseInt(value) >= 0 && parseInt(value) <= 20,
});
metadata.blocks = {
confirmations: parseInt(blockConfirmation, 10),
reorgPeriod: parseInt(blockReorgPeriod, 10),
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',
});
if (wantGasConfig) {
const isEIP1559 = await confirm({
message: 'Is your chain an EIP1559 enabled',
});
if (isEIP1559) {
const maxFeePerGas = await input({
message: 'Enter the max fee per gas (gwei):',
});
const maxPriorityFeePerGas = await input({
message: 'Enter the max priority fee per gas (gwei):',
});
metadata.transactionOverrides = {
maxFeePerGas: BigInt(maxFeePerGas) * BigInt(10 ** 9),
maxPriorityFeePerGas: BigInt(maxPriorityFeePerGas) * BigInt(10 ** 9),
};
} else {
const gasPrice = await input({
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:",
});
}
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,193 +96,190 @@ 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 {
throw new Error(`Invalid hook type: ${hookType}`);
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(
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',
});
}
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 protocolFee = toWei(
await input({
message: `Enter protocol fee in ${nativeTokenAndDecimals(
context,
chain,
)} e.g. 0.01)`,
}),
);
if (BigNumberJs(protocolFee).gt(maxProtocolFee)) {
errorRed('Protocol fee cannot be greater than max protocol fee');
throw new Error('Invalid protocol fee');
}
export const createMerkleTreeConfig = callWithConfigCreationLogs(
async (): Promise<HookConfig> => {
return { type: HookType.MERKLE_TREE };
},
HookType.MERKLE_TREE,
);
return {
type: HookType.PROTOCOL_FEE,
maxProtocolFee: maxProtocolFee.toString(),
protocolFee: protocolFee.toString(),
beneficiary: beneficiaryAddress,
owner: ownerAddress,
};
}
export const createProtocolFeeConfig = callWithConfigCreationLogs(
async (
context: CommandContext,
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;
export async function createIGPConfig(
remotes: ChainName[],
): Promise<HookConfig> {
const owner = await input({
message: 'Enter owner address',
});
const ownerAddress = normalizeAddressEvm(owner);
let beneficiary, oracleKey;
let sameAsOwner = false;
sameAsOwner = 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',
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);
}
// TODO: input in gwei, wei, etc
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.`,
}),
)
: 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 (${protocolFee}) cannot be greater than max protocol fee (${maxProtocolFee}).`,
);
throw new Error(`Invalid protocol fee (${protocolFee}).`);
}
return {
type: HookType.PROTOCOL_FEE,
maxProtocolFee,
protocolFee,
beneficiary,
owner,
};
},
HookType.PROTOCOL_FEE,
);
// TODO: make this usable
export const createIGPConfig = callWithConfigCreationLogs(
async (remotes: ChainName[]): Promise<HookConfig> => {
const unnormalizedOwner = await input({
message: 'Enter owner address for IGP hook',
});
oracleKey = await input({
message: 'Enter gasOracleKey address',
const owner = normalizeAddressEvm(unnormalizedOwner);
let beneficiary = owner;
let oracleKey = owner;
const beneficiarySameAsOwner = await confirm({
message: 'Use this same address for the beneficiary and gasOracleKey?',
});
}
const beneficiaryAddress = normalizeAddressEvm(beneficiary);
const oracleKeyAddress = normalizeAddressEvm(oracleKey);
const overheads: ChainMap<number> = {};
for (const chain of remotes) {
const overhead = parseInt(
if (!beneficiarySameAsOwner) {
const unnormalizedBeneficiary = await input({
message: 'Enter beneficiary address for IGP hook',
});
beneficiary = normalizeAddressEvm(unnormalizedBeneficiary);
const unnormalizedOracleKey = await input({
message: 'Enter gasOracleKey address for IGP hook',
});
oracleKey = normalizeAddressEvm(unnormalizedOracleKey);
}
const overheads: ChainMap<number> = {};
for (const chain of remotes) {
const overhead = parseInt(
await input({
message: `Enter overhead for ${chain} (eg 75000) for IGP hook`,
}),
);
overheads[chain] = overhead;
}
return {
type: HookType.INTERCHAIN_GAS_PAYMASTER,
beneficiary,
owner,
oracleKey,
overhead: overheads,
oracleConfig: {},
};
},
HookType.INTERCHAIN_GAS_PAYMASTER,
);
export const createAggregationConfig = callWithConfigCreationLogs(
async (
context: CommandContext,
advanced: boolean = false,
): Promise<HookConfig> => {
const hooksNum = parseInt(
await input({
message: `Enter overhead for ${chain} (eg 75000)`,
message: 'Enter the number of hooks to aggregate (number)',
}),
10,
);
overheads[chain] = overhead;
}
return {
type: HookType.INTERCHAIN_GAS_PAYMASTER,
beneficiary: beneficiaryAddress,
owner: ownerAddress,
oracleKey: oracleKeyAddress,
overhead: overheads,
gasOracleType: objMap(
overheads,
() => GasOracleContractType.StorageGasOracle,
),
};
}
export async function createAggregationConfig(
context: CommandContext,
chain: ChainName,
remotes: ChainName[],
): Promise<HookConfig> {
const hooksNum = parseInt(
await input({
message: 'Enter the number of hooks to aggregate (number)',
}),
10,
);
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));
}
return {
type: HookType.AGGREGATION,
hooks,
};
}
export async function createRoutingConfig(
context: CommandContext,
origin: ChainName,
remotes: ChainName[],
): Promise<HookConfig> {
const owner = await input({
message: 'Enter owner address',
});
const ownerAddress = owner;
const hooks: Array<HookConfig> = [];
for (let i = 0; i < hooksNum; i++) {
logBlue(`Creating hook ${i + 1} of ${hooksNum} ...`);
hooks.push(
await createHookConfig({
context,
advanced,
}),
);
}
return {
type: HookType.AGGREGATION,
hooks,
};
},
HookType.AGGREGATION,
);
const domainsMap: ChainMap<HookConfig> = {};
for (const chain of remotes) {
await confirm({
message: `You are about to configure hook for remote chain ${chain}. Continue?`,
export const createRoutingConfig = callWithConfigCreationLogs(
async (
context: CommandContext,
advanced: boolean = false,
): Promise<HookConfig> => {
const owner = await input({
message: 'Enter owner address for routing ISM',
});
const config = await createHookConfig(context, origin, remotes);
domainsMap[chain] = config;
}
return {
type: HookType.ROUTING,
owner: ownerAddress,
domains: domainsMap,
};
}
const ownerAddress = owner;
const chains = await runMultiChainSelectionStep(context.chainMetadata);
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'
}`;
}
const domainsMap: ChainMap<HookConfig> = {};
for (const chain of chains) {
await confirm({
message: `You are about to configure hook for remote chain ${chain}. Continue?`,
});
const config = await createHookConfig({ context, advanced });
domainsMap[chain] = config;
}
return {
type: HookType.ROUTING,
owner: ownerAddress,
domains: domainsMap,
};
},
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),
}),
);
import { readYamlOrJson } from '../utils/files.js';
import { detectAndConfirmOrPrompt } from '../utils/input.js';
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',
},
);
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,196 +65,210 @@ 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);
// TODO consider re-enabling. Disabling based on feedback from @nambrot for now.
// repeat = await confirm({
// message: 'Use this same config for remaining chains?',
// });
}
if (isValildIsmConfig(result)) {
logGreen(`ISM config is valid, writing to file ${outPath}`);
mergeYamlOrJson(outPath, result);
} else {
errorRed(
`ISM config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/ism.yaml for an example`,
);
throw new Error('Invalid ISM config');
}
}
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)',
},
],
choices: Object.entries(ISM_TYPE_DESCRIPTIONS).map(
([value, description]) => ({
value,
description,
}),
),
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}}`);
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}.`);
}
return lastConfig;
}
export async function createMultisigConfig(
type: IsmType.MERKLE_ROOT_MULTISIG | IsmType.MESSAGE_ID_MULTISIG,
): Promise<ZodIsmConfig> {
const thresholdInput = await input({
message: 'Enter threshold of validators (number)',
});
const threshold = parseInt(thresholdInput, 10);
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(
`Merkle root multisig signer threshold (${threshold}) cannot be greater than total number of validators (${validators.length}).`,
);
throw new Error('Invalid protocol fee.');
}
return {
type: IsmType.MERKLE_ROOT_MULTISIG,
threshold,
validators,
};
},
IsmType.MERKLE_ROOT_MULTISIG,
);
const validatorsInput = await input({
message: 'Enter validator addresses (comma separated list)',
});
const validators = validatorsInput.split(',').map((v) => v.trim());
return {
type,
threshold,
validators,
};
}
export const createMessageIdMultisigConfig = callWithConfigCreationLogs(
async (): Promise<MultisigIsmConfig> => {
const thresholdInput = await input({
message:
'Enter threshold of validators (number) for message ID multisig ISM',
});
const threshold = parseInt(thresholdInput, 10);
async function createTrustedRelayerConfig(): Promise<ZodIsmConfig> {
const relayer = await input({
message: 'Enter relayer address',
});
return {
type: IsmType.TRUSTED_RELAYER,
relayer,
};
}
const validatorsInput = await input({
message:
'Enter validator addresses (comma separated list) for message ID multisig ISM',
});
const validators = validatorsInput.split(',').map((v) => v.trim());
return {
type: IsmType.MESSAGE_ID_MULTISIG,
threshold,
validators,
};
},
IsmType.MESSAGE_ID_MULTISIG,
);
export async function createAggregationConfig(
remote: ChainName,
chains: ChainName[],
): Promise<ZodIsmConfig> {
const isms = parseInt(
await input({
message: 'Enter the number of ISMs to aggregate (number)',
}),
10,
);
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,
);
const threshold = parseInt(
await input({
message: 'Enter the threshold of ISMs to for verification (number)',
}),
10,
);
export const createAggregationConfig = callWithConfigCreationLogs(
async (context: CommandContext): Promise<AggregationIsmConfig> => {
const isms = parseInt(
await input({
message: 'Enter the number of ISMs to aggregate (number)',
}),
10,
);
const modules: Array<ZodIsmConfig> = [];
for (let i = 0; i < isms; i++) {
modules.push(await createIsmConfig(remote, chains));
}
return {
type: IsmType.AGGREGATION,
modules,
threshold,
};
}
const threshold = parseInt(
await input({
message: 'Enter the threshold of ISMs for verification (number)',
}),
10,
);
export async function createRoutingConfig(
type: IsmType.ROUTING | IsmType.FALLBACK_ROUTING,
remote: ChainName,
chains: ChainName[],
): Promise<ZodIsmConfig> {
const owner = await input({
message: 'Enter owner address',
});
const ownerAddress = owner;
const origins = chains.filter((chain) => chain !== remote);
const modules: Array<IsmConfig> = [];
for (let i = 0; i < isms; i++) {
modules.push(await createAdvancedIsmConfig(context));
}
return {
type: IsmType.AGGREGATION,
modules,
threshold,
};
},
IsmType.AGGREGATION,
);
const domainsMap: ChainMap<ZodIsmConfig> = {};
for (const chain of origins) {
await confirm({
message: `You are about to configure ISM from source chain ${chain}. Continue?`,
export const createRoutingConfig = callWithConfigCreationLogs(
async (context: CommandContext): Promise<IsmConfig> => {
const owner = await input({
message: 'Enter owner address for routing ISM',
});
const config = await createIsmConfig(chain, chains);
domainsMap[chain] = config;
}
return {
type,
owner: ownerAddress,
domains: domainsMap,
};
}
const ownerAddress = owner;
const requireMultiple = true;
const chains = await runMultiChainSelectionStep(
context.chainMetadata,
'Select chains to configure routing ISM for',
requireMultiple,
);
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: 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);
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`,
);
const initialBalances = await prepareDeploy(context, userAddress, [chain]);
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,42 +95,32 @@ 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!');
return;
} 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(
@ -143,11 +128,4 @@ async function executeDelivery({
);
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,20 +58,24 @@ const storageGasOracleConfig: AllStorageGasOracleConfigs =
(local) => remoteOverhead(local),
);
export const igp: ChainMap<IgpConfig> = objMap(owners, (local, owner) => ({
...owner,
ownerOverrides: {
...owner.ownerOverrides,
interchainGasPaymaster: DEPLOYER,
storageGasOracle: DEPLOYER,
},
oracleKey: DEPLOYER,
beneficiary: DEPLOYER,
overhead: Object.fromEntries(
exclude(local, supportedChainNames).map((remote) => [
remote,
remoteOverhead(remote),
]),
),
oracleConfig: storageGasOracleConfig[local],
}));
export const igp: ChainMap<IgpConfig> = objMap(
owners,
(local, owner): IgpConfig => ({
type: HookType.INTERCHAIN_GAS_PAYMASTER,
...owner,
ownerOverrides: {
...owner.ownerOverrides,
interchainGasPaymaster: DEPLOYER,
storageGasOracle: DEPLOYER,
},
oracleKey: DEPLOYER,
beneficiary: DEPLOYER,
overhead: Object.fromEntries(
exclude(local, supportedChainNames).map((remote) => [
remote,
remoteOverhead(remote),
]),
),
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,30 +10,25 @@ 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) => {
const overhead = Object.fromEntries(
exclude(chain, testChainNames).map((remote) => [
remote,
multisigIsmVerificationCost(
multisigIsm[remote].threshold,
multisigIsm[remote].validators.length,
),
]),
);
return {
oracleKey: ownerConfig.owner as Address, // owner can be AccountConfig
beneficiary: ownerConfig.owner as Address, // same as above
gasOracleType: getGasOracles(chain),
overhead,
...ownerConfig,
};
});
export const igp: ChainMap<IgpConfig> = objMap(
owners,
(chain, ownerConfig): IgpConfig => {
const overhead = Object.fromEntries(
exclude(chain, testChainNames).map((remote) => [
remote,
multisigIsmVerificationCost(
multisigIsm[remote].threshold,
multisigIsm[remote].validators.length,
),
]),
);
return {
type: HookType.INTERCHAIN_GAS_PAYMASTER,
oracleKey: ownerConfig.owner as Address, // owner can be AccountConfig
beneficiary: ownerConfig.owner as Address, // same as above
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,21 +11,25 @@ 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) => {
return {
...ownerConfig,
oracleKey: ownerConfig.owner as Address,
beneficiary: ownerConfig.owner as Address,
oracleConfig: storageGasOracleConfig[chain],
overhead: Object.fromEntries(
exclude(chain, supportedChainNames).map((remote) => [
remote,
multisigIsmVerificationCost(
// TODO: parameterize this
defaultMultisigConfigs[remote].threshold,
defaultMultisigConfigs[remote].validators.length,
),
]),
),
};
});
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,
oracleConfig: storageGasOracleConfig[chain],
overhead: Object.fromEntries(
exclude(chain, supportedChainNames).map((remote) => [
remote,
multisigIsmVerificationCost(
// TODO: parameterize this
defaultMultisigConfigs[remote].threshold,
defaultMultisigConfigs[remote].validators.length,
),
]),
),
};
},
);

@ -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);
}),
);
});

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

Loading…
Cancel
Save