Merge branch 'main' into dan/gas-escalator-middleware

dan/gas-escalator-middleware
Daniel Savu 2 weeks ago
commit d7745e8942
No known key found for this signature in database
GPG Key ID: 795E587829AF7E08
  1. 7
      .changeset/dry-ties-approve.md
  2. 5
      .changeset/thirty-actors-wonder.md
  3. 5
      .changeset/unlucky-pillows-clap.md
  4. 1
      rust/main/Cargo.lock
  5. 1
      rust/main/Cargo.toml
  6. 1
      rust/main/chains/hyperlane-sealevel/Cargo.toml
  7. 6
      rust/main/chains/hyperlane-sealevel/src/error.rs
  8. 174
      rust/main/chains/hyperlane-sealevel/src/mailbox.rs
  9. 72
      rust/main/chains/hyperlane-sealevel/src/provider.rs
  10. 65
      rust/main/chains/hyperlane-sealevel/src/transaction.rs
  11. 230
      rust/main/chains/hyperlane-sealevel/src/transaction/tests.rs
  12. 27
      typescript/cli/src/config/core.ts
  13. 16
      typescript/cli/src/config/hooks.ts
  14. 1
      typescript/cli/src/deploy/core.ts
  15. 43
      typescript/cli/src/tests/commands/core.ts
  16. 199
      typescript/cli/src/tests/core.e2e-test.ts
  17. 11
      typescript/cli/src/tests/warp-deploy.e2e-test.ts
  18. 10
      typescript/cli/src/utils/input.ts
  19. 8
      typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts
  20. 6
      typescript/infra/test/govern.hardhat-test.ts
  21. 1
      typescript/sdk/package.json
  22. 42
      typescript/sdk/src/core/EvmCoreModule.ts
  23. 32
      typescript/sdk/src/core/EvmCoreReader.ts
  24. 5
      typescript/sdk/src/core/schemas.ts
  25. 55
      typescript/sdk/src/deploy/proxy.ts
  26. 94
      typescript/sdk/src/gas/token-prices.test.ts
  27. 85
      typescript/sdk/src/gas/token-prices.ts
  28. 35
      typescript/sdk/src/gas/utils.ts
  29. 27
      typescript/sdk/src/hook/EvmHookReader.test.ts
  30. 2
      typescript/sdk/src/index.ts
  31. 14
      typescript/sdk/src/ism/EvmIsmReader.test.ts
  32. 47
      typescript/sdk/src/test/MockCoinGecko.ts
  33. 42
      typescript/sdk/src/token/EvmERC20WarpModule.ts
  34. 4
      typescript/sdk/src/token/IToken.ts
  35. 14
      typescript/widgets/.eslintrc
  36. 10
      typescript/widgets/.storybook/main.ts
  37. 2
      typescript/widgets/package.json
  38. 726
      yarn.lock

@ -0,0 +1,7 @@
---
'@hyperlane-xyz/infra': minor
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---
Add support for updating the mailbox proxy admin owner

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---
Added coinGeckoId as an optional property of the TokenConfigSchema

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': major
---
Remove getCoingeckoTokenPrices (use CoinGeckoTokenPriceGetter instead)

@ -4629,6 +4629,7 @@ dependencies = [
"hyperlane-sealevel-multisig-ism-message-id",
"hyperlane-sealevel-validator-announce",
"jsonrpc-core",
"lazy_static",
"multisig-ism",
"num-traits",
"reqwest",

@ -83,6 +83,7 @@ itertools = "*"
jobserver = "=0.1.26"
jsonrpc-core = "18.0"
k256 = { version = "0.13.4", features = ["arithmetic", "std", "ecdsa"] }
lazy_static = "1.5.0"
log = "0.4"
macro_rules_attribute = "0.2"
maplit = "1.0"

@ -11,6 +11,7 @@ bincode.workspace = true
borsh.workspace = true
derive-new.workspace = true
jsonrpc-core.workspace = true
lazy_static.workspace = true
num-traits.workspace = true
reqwest.workspace = true
serde.workspace = true

@ -42,6 +42,12 @@ pub enum HyperlaneSealevelError {
/// Empty compute units consumed
#[error("received empty compute units consumed in transaction")]
EmptyComputeUnitsConsumed,
/// Too many non-native programs
#[error("transaction contains too many non-native programs, hash: {0:?}")]
TooManyNonNativePrograms(H512),
/// No non-native programs
#[error("transaction contains no non-native programs, hash: {0:?}")]
NoNonNativePrograms(H512),
}
impl From<HyperlaneSealevelError> for ChainCommunicationError {

@ -33,6 +33,7 @@ use solana_client::{
use solana_sdk::{
account::Account,
bs58,
clock::Slot,
commitment_config::CommitmentConfig,
compute_budget::ComputeBudgetInstruction,
hash::Hash,
@ -61,7 +62,9 @@ use hyperlane_core::{
use crate::account::{search_accounts_by_discriminator, search_and_validate_account};
use crate::error::HyperlaneSealevelError;
use crate::transaction::search_dispatched_message_transactions;
use crate::transaction::{
is_message_delivery_instruction, is_message_dispatch_instruction, search_message_transactions,
};
use crate::utils::{decode_h256, decode_h512, from_base58};
use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient};
@ -694,44 +697,15 @@ impl SealevelMailboxIndexer {
let hyperlane_message =
HyperlaneMessage::read_from(&mut &dispatched_message_account.encoded_message[..])?;
let block = self
.mailbox
.provider
.rpc()
.get_block(dispatched_message_account.slot)
let log_meta = self
.dispatch_message_log_meta(
U256::from(nonce),
&valid_message_storage_pda_pubkey,
&dispatched_message_account.slot,
)
.await?;
let block_hash = decode_h256(&block.blockhash)?;
let transactions =
block.transactions.ok_or(HyperlaneSealevelError::NoTransactions("block which should contain message dispatch transaction does not contain any transaction".to_owned()))?;
let transaction_hashes = search_dispatched_message_transactions(
&self.mailbox.program_id,
&valid_message_storage_pda_pubkey,
transactions,
);
// We expect to see that there is only one message dispatch transaction
if transaction_hashes.len() > 1 {
Err(HyperlaneSealevelError::TooManyTransactions("Block contains more than one dispatch message transaction operating on the same dispatch message store PDA".to_owned()))?
}
let (transaction_index, transaction_hash) = transaction_hashes
.into_iter()
.next()
.ok_or(HyperlaneSealevelError::NoTransactions("block which should contain message dispatch transaction does not contain any after filtering".to_owned()))?;
Ok((
hyperlane_message.into(),
LogMeta {
address: self.mailbox.program_id.to_bytes().into(),
block_number: dispatched_message_account.slot,
block_hash,
transaction_id: transaction_hash,
transaction_index: transaction_index as u64,
log_index: U256::from(nonce),
},
))
Ok((hyperlane_message.into(), log_meta))
}
fn dispatched_message_account(&self, account: &Account) -> ChainResult<Pubkey> {
@ -748,6 +722,28 @@ impl SealevelMailboxIndexer {
Ok(expected_pubkey)
}
async fn dispatch_message_log_meta(
&self,
log_index: U256,
message_storage_pda_pubkey: &Pubkey,
message_account_slot: &Slot,
) -> ChainResult<LogMeta> {
let error_msg_no_txn = "block which should contain message dispatch transaction does not contain any transaction".to_owned();
let error_msg_too_many_txns = "block contains more than one dispatch message transaction operating on the same dispatch message store PDA".to_owned();
let error_msg_no_txn_after_filtering = "block which should contain message dispatch transaction does not contain any after filtering".to_owned();
self.log_meta(
log_index,
message_storage_pda_pubkey,
message_account_slot,
&is_message_dispatch_instruction,
error_msg_no_txn,
error_msg_too_many_txns,
error_msg_no_txn_after_filtering,
)
.await
}
async fn get_delivered_message_with_nonce(
&self,
nonce: u32,
@ -782,19 +778,15 @@ impl SealevelMailboxIndexer {
.into_inner();
let message_id = delivered_message_account.message_id;
Ok((
message_id.into(),
LogMeta {
address: self.mailbox.program_id.to_bytes().into(),
block_number: delivered_message_account.slot,
// TODO: get these when building out scraper support.
// It's inconvenient to get these :|
block_hash: H256::zero(),
transaction_id: H512::zero(),
transaction_index: 0,
log_index: U256::zero(),
},
))
let log_meta = self
.delivered_message_log_meta(
U256::from(nonce),
&valid_message_storage_pda_pubkey,
&delivered_message_account.slot,
)
.await?;
Ok((message_id.into(), log_meta))
}
fn delivered_message_account(&self, account: &Account) -> ChainResult<Pubkey> {
@ -808,6 +800,88 @@ impl SealevelMailboxIndexer {
})?;
Ok(expected_pubkey)
}
async fn delivered_message_log_meta(
&self,
log_index: U256,
message_storage_pda_pubkey: &Pubkey,
message_account_slot: &Slot,
) -> ChainResult<LogMeta> {
let error_msg_no_txn = "block which should contain message delivery transaction does not contain any transaction".to_owned();
let error_msg_too_many_txns = "block contains more than one deliver message transaction operating on the same delivery message store PDA".to_owned();
let error_msg_no_txn_after_filtering = "block which should contain message delivery transaction does not contain any after filtering".to_owned();
self.log_meta(
log_index,
message_storage_pda_pubkey,
message_account_slot,
&is_message_delivery_instruction,
error_msg_no_txn,
error_msg_too_many_txns,
error_msg_no_txn_after_filtering,
)
.await
}
async fn log_meta<F>(
&self,
log_index: U256,
message_storage_pda_pubkey: &Pubkey,
message_account_slot: &Slot,
is_message_instruction: &F,
error_msg_no_txn: String,
error_msg_too_many_txns: String,
error_msg_no_txn_after_filtering: String,
) -> ChainResult<LogMeta>
where
F: Fn(instruction::Instruction) -> bool,
{
let block = self
.mailbox
.provider
.rpc()
.get_block(*message_account_slot)
.await?;
let block_hash = decode_h256(&block.blockhash)?;
let transactions = block
.transactions
.ok_or(HyperlaneSealevelError::NoTransactions(error_msg_no_txn))?;
let transaction_hashes = search_message_transactions(
&self.mailbox.program_id,
&message_storage_pda_pubkey,
transactions,
&is_message_instruction,
);
// We expect to see that there is only one message dispatch transaction
if transaction_hashes.len() > 1 {
Err(HyperlaneSealevelError::TooManyTransactions(
error_msg_too_many_txns,
))?
}
let (transaction_index, transaction_hash) =
transaction_hashes
.into_iter()
.next()
.ok_or(HyperlaneSealevelError::NoTransactions(
error_msg_no_txn_after_filtering,
))?;
let log_meta = LogMeta {
address: self.mailbox.program_id.to_bytes().into(),
block_number: *message_account_slot,
block_hash,
transaction_id: transaction_hash,
transaction_index: transaction_index as u64,
log_index,
};
Ok(log_meta)
}
}
#[async_trait]

@ -1,10 +1,13 @@
use std::collections::HashSet;
use std::sync::Arc;
use async_trait::async_trait;
use lazy_static::lazy_static;
use solana_sdk::signature::Signature;
use solana_transaction_status::{
option_serializer::OptionSerializer, EncodedTransaction, EncodedTransactionWithStatusMeta,
UiMessage, UiTransaction, UiTransactionStatusMeta,
UiInstruction, UiMessage, UiParsedInstruction, UiParsedMessage, UiTransaction,
UiTransactionStatusMeta,
};
use tracing::warn;
@ -18,6 +21,19 @@ use crate::error::HyperlaneSealevelError;
use crate::utils::{decode_h256, decode_h512, decode_pubkey};
use crate::{ConnectionConf, SealevelRpcClient};
lazy_static! {
static ref NATIVE_PROGRAMS: HashSet<String> = HashSet::from([
solana_sdk::bpf_loader_upgradeable::ID.to_string(),
solana_sdk::compute_budget::ID.to_string(),
solana_sdk::config::program::ID.to_string(),
solana_sdk::ed25519_program::ID.to_string(),
solana_sdk::secp256k1_program::ID.to_string(),
solana_sdk::stake::program::ID.to_string(),
solana_sdk::system_program::ID.to_string(),
solana_sdk::vote::program::ID.to_string(),
]);
}
/// A wrapper around a Sealevel provider to get generic blockchain information.
#[derive(Debug)]
pub struct SealevelProvider {
@ -33,7 +49,7 @@ impl SealevelProvider {
let rpc_client = Arc::new(SealevelRpcClient::new(conf.url.to_string()));
let native_token = conf.native_token.clone();
SealevelProvider {
Self {
domain,
rpc_client,
native_token,
@ -64,12 +80,7 @@ impl SealevelProvider {
}
fn sender(hash: &H512, txn: &UiTransaction) -> ChainResult<H256> {
let message = match &txn.message {
UiMessage::Parsed(m) => m,
m => Err(Into::<ChainCommunicationError>::into(
HyperlaneSealevelError::UnsupportedMessageEncoding(m.clone()),
))?,
};
let message = Self::parsed_message(txn)?;
let signer = message
.account_keys
@ -80,6 +91,39 @@ impl SealevelProvider {
Ok(sender)
}
fn recipient(hash: &H512, txn: &UiTransaction) -> ChainResult<H256> {
let message = Self::parsed_message(txn)?;
let programs = message
.instructions
.iter()
.filter_map(|ii| {
if let UiInstruction::Parsed(iii) = ii {
Some(iii)
} else {
None
}
})
.map(|ii| match ii {
UiParsedInstruction::Parsed(iii) => &iii.program_id,
UiParsedInstruction::PartiallyDecoded(iii) => &iii.program_id,
})
.filter(|program_id| !NATIVE_PROGRAMS.contains(*program_id))
.collect::<Vec<&String>>();
if programs.len() > 1 {
Err(HyperlaneSealevelError::TooManyNonNativePrograms(*hash))?;
}
let program_id = programs
.first()
.ok_or(HyperlaneSealevelError::NoNonNativePrograms(*hash))?;
let pubkey = decode_pubkey(program_id)?;
let recipient = H256::from_slice(&pubkey.to_bytes());
Ok(recipient)
}
fn gas(meta: &UiTransactionStatusMeta) -> ChainResult<U256> {
let OptionSerializer::Some(gas) = meta.compute_units_consumed else {
Err(HyperlaneSealevelError::EmptyComputeUnitsConsumed)?
@ -108,6 +152,15 @@ impl SealevelProvider {
.ok_or(HyperlaneSealevelError::EmptyMetadata)?;
Ok(meta)
}
fn parsed_message(txn: &UiTransaction) -> ChainResult<&UiParsedMessage> {
Ok(match &txn.message {
UiMessage::Parsed(m) => m,
m => Err(Into::<ChainCommunicationError>::into(
HyperlaneSealevelError::UnsupportedMessageEncoding(m.clone()),
))?,
})
}
}
impl HyperlaneChain for SealevelProvider {
@ -165,6 +218,7 @@ impl HyperlaneProvider for SealevelProvider {
Self::validate_transaction(hash, txn)?;
let sender = Self::sender(hash, txn)?;
let recipient = Self::recipient(hash, txn)?;
let meta = Self::meta(txn_with_meta)?;
let gas_used = Self::gas(meta)?;
let fee = self.fee(meta)?;
@ -189,7 +243,7 @@ impl HyperlaneProvider for SealevelProvider {
gas_price,
nonce: 0,
sender,
recipient: None,
recipient: Some(recipient),
receipt: Some(receipt),
raw_input_data: None,
})

@ -13,27 +13,35 @@ use hyperlane_core::H512;
use crate::utils::{decode_h512, from_base58};
/// This function searches for a transaction which dispatches Hyperlane message and returns
/// list of hashes of such transactions.
/// This function searches for a transaction which specified instruction on Hyperlane message and
/// returns list of hashes of such transactions.
///
/// This function takes the mailbox program identifier and the identifier for PDA for storing
/// a dispatched message and searches a message dispatch transaction in a list of transaction.
/// a dispatched or delivered message and searches a message dispatch or delivery transaction
/// in a list of transactions.
///
/// The list of transaction is usually comes from a block. The function returns list of hashes
/// of such transactions.
/// of such transactions and their relative index in the block.
///
/// The transaction will be searched with the following criteria:
/// 1. Transaction contains Mailbox program id in the list of accounts.
/// 2. Transaction contains dispatched message PDA in the list of accounts.
/// 3. Transaction is performing message dispatch (OutboxDispatch).
/// 2. Transaction contains dispatched/delivered message PDA in the list of accounts.
/// 3. Transaction is performing the specified message instruction.
///
/// * `mailbox_program_id` - Identifier of Mailbox program
/// * `message_storage_pda_pubkey` - Identifier for dispatch message store PDA
/// * `message_storage_pda_pubkey` - Identifier for message store PDA
/// * `transactions` - List of transactions
pub fn search_dispatched_message_transactions(
/// * `is_specified_message_instruction` - Function which returns `true` for specified message
/// instruction
pub fn search_message_transactions<F>(
mailbox_program_id: &Pubkey,
message_storage_pda_pubkey: &Pubkey,
transactions: Vec<EncodedTransactionWithStatusMeta>,
) -> Vec<(usize, H512)> {
is_specified_message_instruction: &F,
) -> Vec<(usize, H512)>
where
F: Fn(Instruction) -> bool,
{
transactions
.into_iter()
.enumerate()
@ -43,38 +51,43 @@ pub fn search_dispatched_message_transactions(
.map(|(hash, account_keys, instructions)| (index, hash, account_keys, instructions))
})
.filter_map(|(index, hash, account_keys, instructions)| {
filter_not_relevant(
filter_by_relevancy(
mailbox_program_id,
message_storage_pda_pubkey,
hash,
account_keys,
instructions,
is_specified_message_instruction,
)
.map(|hash| (index, hash))
})
.collect::<Vec<(usize, H512)>>()
}
fn filter_not_relevant(
fn filter_by_relevancy<F>(
mailbox_program_id: &Pubkey,
message_storage_pda_pubkey: &Pubkey,
hash: H512,
account_keys: Vec<String>,
instructions: Vec<UiCompiledInstruction>,
) -> Option<H512> {
is_specified_message_instruction: &F,
) -> Option<H512>
where
F: Fn(Instruction) -> bool,
{
let account_index_map = account_index_map(account_keys);
let mailbox_program_id_str = mailbox_program_id.to_string();
let mailbox_program_index = match account_index_map.get(&mailbox_program_id_str) {
Some(i) => *i as u8,
None => return None, // If account keys do not contain Mailbox program, transaction is not message dispatch.
None => return None, // If account keys do not contain Mailbox program, transaction is not message dispatch/delivery.
};
let message_storage_pda_pubkey_str = message_storage_pda_pubkey.to_string();
let dispatch_message_pda_account_index =
let message_storage_pda_account_index =
match account_index_map.get(&message_storage_pda_pubkey_str) {
Some(i) => *i as u8,
None => return None, // If account keys do not contain dispatch message store PDA account, transaction is not message dispatch.
None => return None, // If account keys do not contain dispatch/delivery message store PDA account, transaction is not message dispatch/delivery.
};
let mailbox_program_maybe = instructions
@ -83,35 +96,43 @@ fn filter_not_relevant(
let mailbox_program = match mailbox_program_maybe {
Some(p) => p,
None => return None, // If transaction does not contain call into Mailbox, transaction is not message dispatch.
None => return None, // If transaction does not contain call into Mailbox, transaction is not message dispatch/delivery.
};
// If Mailbox program does not operate on dispatch message store PDA account, transaction is not message dispatch.
// If Mailbox program does not operate on dispatch/delivery message store PDA account, transaction is not message dispatch/delivery.
if !mailbox_program
.accounts
.contains(&dispatch_message_pda_account_index)
.contains(&message_storage_pda_account_index)
{
return None;
}
let instruction_data = match from_base58(&mailbox_program.data) {
Ok(d) => d,
Err(_) => return None, // If we cannot decode instruction data, transaction is not message dispatch.
Err(_) => return None, // If we cannot decode instruction data, transaction is not message dispatch/delivery.
};
let instruction = match Instruction::from_instruction_data(&instruction_data) {
Ok(ii) => ii,
Err(_) => return None, // If we cannot parse instruction data, transaction is not message dispatch.
Err(_) => return None, // If we cannot parse instruction data, transaction is not message dispatch/delivery.
};
// If the call into Mailbox program is not OutboxDispatch, transaction is not message dispatch.
if !matches!(instruction, Instruction::OutboxDispatch(_)) {
// If the call into Mailbox program is not OutboxDispatch/InboxProcess, transaction is not message dispatch/delivery.
if is_specified_message_instruction(instruction) {
return None;
}
Some(hash)
}
pub fn is_message_dispatch_instruction(instruction: Instruction) -> bool {
!matches!(instruction, Instruction::OutboxDispatch(_))
}
pub fn is_message_delivery_instruction(instruction: Instruction) -> bool {
!matches!(instruction, Instruction::InboxProcess(_))
}
fn filter_by_validity(
tx: UiTransaction,
meta: UiTransactionStatusMeta,

@ -1,29 +1,57 @@
use solana_sdk::pubkey::Pubkey;
use solana_transaction_status::EncodedTransactionWithStatusMeta;
use crate::transaction::search_dispatched_message_transactions;
use crate::transaction::{
is_message_delivery_instruction, is_message_dispatch_instruction, search_message_transactions,
};
use crate::utils::decode_pubkey;
#[test]
pub fn test_search_dispatched_message_transaction() {
// given
let mailbox_program_id = decode_pubkey("E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi").unwrap();
let dispatched_message_pda_account =
decode_pubkey("6eG8PheL41qLFFUtPjSYMtsp4aoAQsMgcsYwkGCB8kwT").unwrap();
let transaction = serde_json::from_str::<EncodedTransactionWithStatusMeta>(JSON).unwrap();
let transactions = vec![transaction];
let (mailbox_program_id, transactions) = transactions(DISPATCH_TXN_JSON);
// when
let transaction_hashes = search_dispatched_message_transactions(
let transaction_hashes = search_message_transactions(
&mailbox_program_id,
&dispatched_message_pda_account,
transactions,
&is_message_dispatch_instruction,
);
// then
assert!(!transaction_hashes.is_empty());
}
const JSON: &str = r#"
#[test]
pub fn test_search_delivered_message_transaction() {
// given
let delivered_message_pda_account =
decode_pubkey("Dj7jk47KKXvw4nseNGdyHtNHtjPes2XSfByhF8xymrtS").unwrap();
let (mailbox_program_id, transactions) = transactions(DELIVERY_TXN_JSON);
// when
let transaction_hashes = search_message_transactions(
&mailbox_program_id,
&delivered_message_pda_account,
transactions,
&is_message_delivery_instruction,
);
// then
assert!(!transaction_hashes.is_empty());
}
fn transactions(json: &str) -> (Pubkey, Vec<EncodedTransactionWithStatusMeta>) {
let mailbox_program_id = decode_pubkey("E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi").unwrap();
let transaction = serde_json::from_str::<EncodedTransactionWithStatusMeta>(json).unwrap();
let transactions = vec![transaction];
(mailbox_program_id, transactions)
}
const DISPATCH_TXN_JSON: &str = r#"
{
"blockTime": 1729865514,
"meta": {
@ -327,3 +355,193 @@ const JSON: &str = r#"
}
}
"#;
const DELIVERY_TXN_JSON: &str = r#"
{
"blockTime": 1726514134,
"meta": {
"computeUnitsConsumed": 200654,
"err": null,
"fee": 5000,
"innerInstructions": [
{
"index": 1,
"instructions": [
{
"accounts": [
10
],
"data": "8YGwT5LUTP4",
"programIdIndex": 9,
"stackHeight": 2
},
{
"accounts": [
12
],
"data": "2848tnNKZjKzgKikguTY4s5nESn7KLYUbLsrp6Z1FYq4BmM31xRwBXnJU5RW9rEvRUjJfJa58kXdgQYEQpg4sDrRfx5HnGsgXfitkxJw5NKVcFAYLSqKvpkYxer2tAn3a8ZzPvuDD9iqyLkvJnRZ3TbcoAHNisFfvBeWK95YL8zxsyzDS9ZBMaoYrLKQx9b915xj9oijw2UNk7FF5qxThZDKwF8rwckb6t2o6ypzFEqYeQCsRW5quayYsLBjHi8RdY18NDkcnPVkQbdR7FmfrncV4H5ZYZaayMtgAs6kHxRgeuuBEtrYG1UbGjWTQAss9zmeXcKipqS3S2bee96U5w9Cd981e8dkakCtKR7KusjE9nhsFTfXoxcwkRhi3TzqDicrqt7Erf78K",
"programIdIndex": 8,
"stackHeight": 2
},
{
"accounts": [
0,
3
],
"data": "11117UpxCJ2YqmddN2ykgdMGRXkyPgnqEtj5XYrnk1iC4P1xrvXq2zvZQkj3uNaitHEw2k",
"programIdIndex": 5,
"stackHeight": 2
},
{
"accounts": [
11,
5,
10,
1,
5,
2
],
"data": "7MHiQP8ahsZcB5cM9ZXGa2foMYQENm7GnrFaV4AmfgKNzSndaXhrcqbVNRgN2kGmrrsfTi8bNEGkAJn6MWjY95PnakaF2HAchXrUUBzQrWKQdRp8VbKjDsnH1tEUiAWm439Y12TpWTW3uSphh1oycpTJP",
"programIdIndex": 9,
"stackHeight": 2
},
{
"accounts": [
2,
1
],
"data": "3Bxs4ThwQbE4vyj5",
"programIdIndex": 5,
"stackHeight": 3
}
]
}
],
"loadedAddresses": {
"readonly": [],
"writable": []
},
"logMessages": [
"Program ComputeBudget111111111111111111111111111111 invoke [1]",
"Program ComputeBudget111111111111111111111111111111 success",
"Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi invoke [1]",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg invoke [2]",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg consumed 4402 of 1363482 compute units",
"Program return: 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg AA==",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg success",
"Program 372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC invoke [2]",
"Program 372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC consumed 106563 of 1353660 compute units",
"Program 372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC success",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg invoke [2]",
"Program 11111111111111111111111111111111 invoke [3]",
"Program 11111111111111111111111111111111 success",
"Program log: Warp route transfer completed from origin: 1408864445, recipient: 528MctBmY7rXqufM3r8k7t9DTfVNuB4K1rr8xVU4naJM, remote_amount: 100000",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg consumed 28117 of 1240216 compute units",
"Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg success",
"Program log: Hyperlane inbox processed message 0x34ed0705362554568a1a2d24aef6bfde71894dd1bb2f0457fb4bd66016074fcc",
"Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi consumed 200504 of 1399850 compute units",
"Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi success"
],
"postBalances": [
338367600,
199691000,
891880,
1287600,
1211040,
1,
1,
1141440,
1141440,
1141440,
2686560,
0,
8017920,
1141440
],
"postTokenBalances": [],
"preBalances": [
339660200,
199591000,
991880,
0,
1211040,
1,
1,
1141440,
1141440,
1141440,
2686560,
0,
8017920,
1141440
],
"preTokenBalances": [],
"rewards": [],
"status": {
"Ok": null
}
},
"slot": 290198208,
"transaction": {
"message": {
"accountKeys": [
"G5FM3UKwcBJ47PwLWLLY1RQpqNtTMgnqnd6nZGcJqaBp",
"528MctBmY7rXqufM3r8k7t9DTfVNuB4K1rr8xVU4naJM",
"5H4cmX5ybSqK6Ro6nvr9eiR8G8ATTYRwVsZ42VRRW3wa",
"Dj7jk47KKXvw4nseNGdyHtNHtjPes2XSfByhF8xymrtS",
"H3EgdESu59M4hn5wrbeyi9VjmFiLYM7iUAbGtrA5uHNE",
"11111111111111111111111111111111",
"ComputeBudget111111111111111111111111111111",
"noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV",
"372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC",
"4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg",
"A2nmLy86tmraneRMEZ5yWbDGq6YsPKNcESGaTZKkRWZU",
"DmU32nL975xAshVYgLLdyMoaUzHa2aCzHJyfLyKRdz3M",
"E2jimXLCtTiuZ6jbXP8B7SyZ5vVc1PKYYnMeho9yJ1en",
"E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi"
],
"header": {
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 9,
"numRequiredSignatures": 1
},
"instructions": [
{
"accounts": [],
"data": "K1FDJ7",
"programIdIndex": 6,
"stackHeight": null
},
{
"accounts": [
0,
5,
4,
11,
3,
10,
7,
8,
12,
9,
5,
10,
1,
5,
2
],
"data": "3RwSrioTudpACxczi2EejzKoZCPVuzq6qWLCQYAWoZoTcRPBobUn7tB5SFvMPNHGJ551rmjXDyKdaQLuzX3d5bjHSrSsquwHqWgM6L2kMEEJZjtygNyx3RhJD9GyZqekDuK19cfYfn1dyLuo7SSqswV3t6yptLhnCv8DhxBLRuXhV2GdNy9PLU3VNc9PvPWxg1Grtr9UZ5GnmdKDeqRvonM9AqmuN6mnv3UaqjjAEX8yDKPhWHm6w1HRzfgbjkXQVL5aSqdgJeF3EVBKJCzvMKbUVjTRgD6iHQyUVrSYvrHpKZxc6EctBHN6tyeZrW5RD1M6giasnm4WqrjDwUyz9xwvk31srJrZp7W7D6i2tTajmBbiKjpNo75iaHj4dycf1H",
"programIdIndex": 13,
"stackHeight": null
}
],
"recentBlockhash": "AzQN8x5uKk7ExXW4eUu2FiqRG1BX73uvfHcQeBDHcu8a"
},
"signatures": [
"5pBEVfDD3siir1CBf9taeWuee44GspA7EixYkKnzN1hkeYXLxtKYrbe3aE6hxswbY3hhDRVPDor1ZsSXUorC7bcR"
]
}
}
"#;

@ -1,6 +1,11 @@
import { stringify as yamlStringify } from 'yaml';
import { CoreConfigSchema, HookConfig, IsmConfig } from '@hyperlane-xyz/sdk';
import {
CoreConfigSchema,
HookConfig,
IsmConfig,
OwnableConfig,
} from '@hyperlane-xyz/sdk';
import { CommandContext } from '../context/types.js';
import { errorRed, log, logBlue, logGreen } from '../logger.js';
@ -18,6 +23,9 @@ import {
} from './hooks.js';
import { createAdvancedIsmConfig, createTrustedRelayerConfig } from './ism.js';
const ENTER_DESIRED_VALUE_MSG = 'Enter the desired';
const SIGNER_PROMPT_LABEL = 'signer';
export async function createCoreDeployConfig({
context,
configFilePath,
@ -31,9 +39,9 @@ export async function createCoreDeployConfig({
const owner = await detectAndConfirmOrPrompt(
async () => context.signer?.getAddress(),
'Enter the desired',
ENTER_DESIRED_VALUE_MSG,
'owner address',
'signer',
SIGNER_PROMPT_LABEL,
);
const defaultIsm: IsmConfig = advanced
@ -41,6 +49,7 @@ export async function createCoreDeployConfig({
: await createTrustedRelayerConfig(context, advanced);
let defaultHook: HookConfig, requiredHook: HookConfig;
let proxyAdmin: OwnableConfig;
if (advanced) {
defaultHook = await createHookConfig({
context,
@ -52,9 +61,20 @@ export async function createCoreDeployConfig({
selectMessage: 'Select required hook type',
advanced,
});
proxyAdmin = {
owner: await detectAndConfirmOrPrompt(
async () => context.signer?.getAddress(),
ENTER_DESIRED_VALUE_MSG,
'ProxyAdmin owner address',
SIGNER_PROMPT_LABEL,
),
};
} else {
defaultHook = await createMerkleTreeConfig();
requiredHook = await createProtocolFeeConfig(context, advanced);
proxyAdmin = {
owner,
};
}
try {
@ -63,6 +83,7 @@ export async function createCoreDeployConfig({
defaultIsm,
defaultHook,
requiredHook,
proxyAdmin,
});
logBlue(`Core config is valid, writing to file ${configFilePath}:\n`);
log(indentYamlOrJson(yamlStringify(coreConfig, null, 2), 4));

@ -8,12 +8,12 @@ import {
ChainMap,
ChainMetadata,
ChainName,
CoinGeckoTokenPriceGetter,
HookConfig,
HookConfigSchema,
HookType,
IgpHookConfig,
MultiProtocolProvider,
getCoingeckoTokenPrices,
getGasPrice,
getLocalStorageGasOracleConfig,
} from '@hyperlane-xyz/sdk';
@ -305,9 +305,17 @@ async function getIgpTokenPrices(
) {
const isTestnet =
context.chainMetadata[Object.keys(filteredMetadata)[0]].isTestnet;
const fetchedPrices = isTestnet
? objMap(filteredMetadata, () => '10')
: await getCoingeckoTokenPrices(filteredMetadata);
let fetchedPrices: ChainMap<string>;
if (isTestnet) {
fetchedPrices = objMap(filteredMetadata, () => '10');
} else {
const tokenPriceGetter = new CoinGeckoTokenPriceGetter({
chainMetadata: filteredMetadata,
});
const results = await tokenPriceGetter.getAllTokenPrices();
fetchedPrices = objMap(results, (v) => v.toString());
}
logBlue(
isTestnet

@ -34,6 +34,7 @@ interface DeployParams {
interface ApplyParams extends DeployParams {
deployedCoreAddresses: DeployedCoreAddresses;
}
/**
* Executes the core deploy command.
*/

@ -1,5 +1,9 @@
import { $ } from 'zx';
import { CoreConfig } from '@hyperlane-xyz/sdk';
import { readYamlOrJson } from '../../utils/files.js';
import { ANVIL_KEY, REGISTRY_PATH } from './helpers.js';
/**
@ -17,3 +21,42 @@ export async function hyperlaneCoreDeploy(
--verbosity debug \
--yes`;
}
/**
* Reads a Hyperlane core deployment on the specified chain using the provided config.
*/
export async function hyperlaneCoreRead(chain: string, coreOutputPath: string) {
return $`yarn workspace @hyperlane-xyz/cli run hyperlane core read \
--registry ${REGISTRY_PATH} \
--config ${coreOutputPath} \
--chain ${chain} \
--verbosity debug \
--yes`;
}
/**
* Updates a Hyperlane core deployment on the specified chain using the provided config.
*/
export async function hyperlaneCoreApply(
chain: string,
coreOutputPath: string,
) {
return $`yarn workspace @hyperlane-xyz/cli run hyperlane core apply \
--registry ${REGISTRY_PATH} \
--config ${coreOutputPath} \
--chain ${chain} \
--key ${ANVIL_KEY} \
--verbosity debug \
--yes`;
}
/**
* Reads the Core deployment config and outputs it to specified output path.
*/
export async function readCoreConfig(
chain: string,
coreConfigPath: string,
): Promise<CoreConfig> {
await hyperlaneCoreRead(chain, coreConfigPath);
return readYamlOrJson(coreConfigPath);
}

@ -0,0 +1,199 @@
import { expect } from 'chai';
import { Signer, Wallet, ethers } from 'ethers';
import { ProxyAdmin__factory } from '@hyperlane-xyz/core';
import {
CoreConfig,
ProtocolFeeHookConfig,
randomAddress,
} from '@hyperlane-xyz/sdk';
import { Address } from '@hyperlane-xyz/utils';
import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js';
import {
hyperlaneCoreApply,
hyperlaneCoreDeploy,
readCoreConfig,
} from './commands/core.js';
import { ANVIL_KEY, REGISTRY_PATH } from './commands/helpers.js';
const CHAIN_NAME = 'anvil2';
const EXAMPLES_PATH = './examples';
const CORE_CONFIG_PATH = `${EXAMPLES_PATH}/core-config.yaml`;
const TEMP_PATH = '/tmp'; // /temp gets removed at the end of all-test.sh
const CORE_READ_CONFIG_PATH = `${TEMP_PATH}/anvil2/core-config-read.yaml`;
const TEST_TIMEOUT = 100_000; // Long timeout since these tests can take a while
describe('hyperlane core e2e tests', async function () {
this.timeout(TEST_TIMEOUT);
let signer: Signer;
let initialOwnerAddress: Address;
before(async () => {
const chainMetadata: any = readYamlOrJson(
`${REGISTRY_PATH}/chains/${CHAIN_NAME}/metadata.yaml`,
);
const provider = new ethers.providers.JsonRpcProvider(
chainMetadata.rpcUrls[0].http,
);
const wallet = new Wallet(ANVIL_KEY);
signer = wallet.connect(provider);
initialOwnerAddress = await signer.getAddress();
});
describe('core.deploy', () => {
it('should create a core deployment with the signer as the mailbox owner', async () => {
await hyperlaneCoreDeploy(CHAIN_NAME, CORE_CONFIG_PATH);
const coreConfig: CoreConfig = await readCoreConfig(
CHAIN_NAME,
CORE_READ_CONFIG_PATH,
);
expect(coreConfig.owner).to.equal(initialOwnerAddress);
expect(coreConfig.proxyAdmin?.owner).to.equal(initialOwnerAddress);
// Assuming that the ProtocolFeeHook is used for deployment
expect((coreConfig.requiredHook as ProtocolFeeHookConfig).owner).to.equal(
initialOwnerAddress,
);
});
it('should create a core deployment with the mailbox owner set to the address in the config', async () => {
const coreConfig: CoreConfig = await readYamlOrJson(CORE_CONFIG_PATH);
const newOwner = randomAddress().toLowerCase();
coreConfig.owner = newOwner;
writeYamlOrJson(CORE_READ_CONFIG_PATH, coreConfig);
// Deploy the core contracts with the updated mailbox owner
await hyperlaneCoreDeploy(CHAIN_NAME, CORE_READ_CONFIG_PATH);
// Verify that the owner has been set correctly without modifying any other owner values
const updatedConfig: CoreConfig = await readCoreConfig(
CHAIN_NAME,
CORE_READ_CONFIG_PATH,
);
expect(updatedConfig.owner.toLowerCase()).to.equal(newOwner);
expect(updatedConfig.proxyAdmin?.owner).to.equal(initialOwnerAddress);
// Assuming that the ProtocolFeeHook is used for deployment
expect(
(updatedConfig.requiredHook as ProtocolFeeHookConfig).owner,
).to.equal(initialOwnerAddress);
});
it('should create a core deployment with ProxyAdmin owner of the mailbox set to the address in the config', async () => {
const coreConfig: CoreConfig = await readYamlOrJson(CORE_CONFIG_PATH);
const newOwner = randomAddress().toLowerCase();
coreConfig.proxyAdmin = { owner: newOwner };
writeYamlOrJson(CORE_READ_CONFIG_PATH, coreConfig);
// Deploy the core contracts with the updated mailbox owner
await hyperlaneCoreDeploy(CHAIN_NAME, CORE_READ_CONFIG_PATH);
// Verify that the owner has been set correctly without modifying any other owner values
const updatedConfig: CoreConfig = await readCoreConfig(
CHAIN_NAME,
CORE_READ_CONFIG_PATH,
);
expect(updatedConfig.owner).to.equal(initialOwnerAddress);
expect(updatedConfig.proxyAdmin?.owner.toLowerCase()).to.equal(newOwner);
// Assuming that the ProtocolFeeHook is used for deployment
expect(
(updatedConfig.requiredHook as ProtocolFeeHookConfig).owner,
).to.equal(initialOwnerAddress);
});
});
describe('core.apply', () => {
it('should update the mailbox owner', async () => {
await hyperlaneCoreDeploy(CHAIN_NAME, CORE_CONFIG_PATH);
const coreConfig: CoreConfig = await readCoreConfig(
CHAIN_NAME,
CORE_READ_CONFIG_PATH,
);
expect(coreConfig.owner).to.equal(initialOwnerAddress);
const newOwner = randomAddress().toLowerCase();
coreConfig.owner = newOwner;
writeYamlOrJson(CORE_READ_CONFIG_PATH, coreConfig);
await hyperlaneCoreApply(CHAIN_NAME, CORE_READ_CONFIG_PATH);
// Verify that the owner has been set correctly without modifying any other owner values
const updatedConfig: CoreConfig = await readCoreConfig(
CHAIN_NAME,
CORE_READ_CONFIG_PATH,
);
expect(updatedConfig.owner.toLowerCase()).to.equal(newOwner);
expect(updatedConfig.proxyAdmin?.owner).to.equal(initialOwnerAddress);
// Assuming that the ProtocolFeeHook is used for deployment
expect(
(updatedConfig.requiredHook as ProtocolFeeHookConfig).owner,
).to.equal(initialOwnerAddress);
});
it('should update the ProxyAdmin to a new one for the mailbox', async () => {
await hyperlaneCoreDeploy(CHAIN_NAME, CORE_CONFIG_PATH);
const coreConfig: CoreConfig = await readCoreConfig(
CHAIN_NAME,
CORE_READ_CONFIG_PATH,
);
expect(coreConfig.owner).to.equal(initialOwnerAddress);
const proxyFactory = new ProxyAdmin__factory().connect(signer);
const deployTx = await proxyFactory.deploy();
const newProxyAdmin = await deployTx.deployed();
coreConfig.proxyAdmin!.address = newProxyAdmin.address;
writeYamlOrJson(CORE_READ_CONFIG_PATH, coreConfig);
await hyperlaneCoreApply(CHAIN_NAME, CORE_READ_CONFIG_PATH);
// Verify that the owner has been set correctly without modifying any other owner values
const updatedConfig: CoreConfig = await readCoreConfig(
CHAIN_NAME,
CORE_READ_CONFIG_PATH,
);
expect(updatedConfig.owner).to.equal(initialOwnerAddress);
expect(updatedConfig.proxyAdmin?.address).to.equal(newProxyAdmin.address);
// Assuming that the ProtocolFeeHook is used for deployment
expect(
(updatedConfig.requiredHook as ProtocolFeeHookConfig).owner,
).to.equal(initialOwnerAddress);
});
it('should update the ProxyAdmin owner for the mailbox', async () => {
await hyperlaneCoreDeploy(CHAIN_NAME, CORE_CONFIG_PATH);
const coreConfig: CoreConfig = await readCoreConfig(
CHAIN_NAME,
CORE_READ_CONFIG_PATH,
);
expect(coreConfig.owner).to.equal(initialOwnerAddress);
const newOwner = randomAddress().toLowerCase();
coreConfig.proxyAdmin!.owner = newOwner;
writeYamlOrJson(CORE_READ_CONFIG_PATH, coreConfig);
await hyperlaneCoreApply(CHAIN_NAME, CORE_READ_CONFIG_PATH);
// Verify that the owner has been set correctly without modifying any other owner values
const updatedConfig: CoreConfig = await readCoreConfig(
CHAIN_NAME,
CORE_READ_CONFIG_PATH,
);
expect(updatedConfig.owner).to.equal(initialOwnerAddress);
expect(updatedConfig.proxyAdmin?.owner.toLowerCase()).to.equal(newOwner);
// Assuming that the ProtocolFeeHook is used for deployment
expect(
(updatedConfig.requiredHook as ProtocolFeeHookConfig).owner,
).to.equal(initialOwnerAddress);
});
});
});

@ -34,6 +34,7 @@ const WARP_CORE_CONFIG_PATH_2_3 = `${REGISTRY_PATH}/deployments/warp_routes/VAUL
const TEST_TIMEOUT = 60_000; // Long timeout since these tests can take a while
describe('WarpDeploy e2e tests', async function () {
let chain2Addresses: ChainAddresses = {};
let chain3Addresses: ChainAddresses = {};
let token: any;
let vault: any;
@ -46,7 +47,11 @@ describe('WarpDeploy e2e tests', async function () {
ANVIL_KEY,
);
await deployOrUseExistingCore(CHAIN_NAME_3, CORE_CONFIG_PATH, ANVIL_KEY);
chain3Addresses = await deployOrUseExistingCore(
CHAIN_NAME_3,
CORE_CONFIG_PATH,
ANVIL_KEY,
);
token = await deployToken(ANVIL_KEY, CHAIN_NAME_2);
vault = await deploy4626Vault(ANVIL_KEY, CHAIN_NAME_2, token.address);
@ -81,8 +86,8 @@ describe('WarpDeploy e2e tests', async function () {
},
[CHAIN_NAME_3]: {
type: TokenType.syntheticRebase,
mailbox: chain2Addresses.mailbox,
owner: chain2Addresses.mailbox,
mailbox: chain3Addresses.mailbox,
owner: chain3Addresses.mailbox,
collateralChainName: CHAIN_NAME_2,
},
};

@ -19,14 +19,16 @@ import ansiEscapes from 'ansi-escapes';
import chalk from 'chalk';
import { ProxyAdmin__factory } from '@hyperlane-xyz/core';
import { ChainName, DeployedOwnableConfig } from '@hyperlane-xyz/sdk';
import { WarpCoreConfig } from '@hyperlane-xyz/sdk';
import {
ChainName,
DeployedOwnableConfig,
WarpCoreConfig,
} from '@hyperlane-xyz/sdk';
import { Address, isAddress, rootLogger } from '@hyperlane-xyz/utils';
import { readWarpCoreConfig } from '../config/warp.js';
import { CommandContext } from '../context/types.js';
import { logGray } from '../logger.js';
import { logRed } from '../logger.js';
import { logGray, logRed } from '../logger.js';
import { indentYamlOrJson } from './files.js';
import { selectRegistryWarpRoute } from './tokens.js';

@ -3,12 +3,12 @@ import { ethers } from 'ethers';
import { Gauge, Registry } from 'prom-client';
import {
ERC20__factory,
HypXERC20Lockbox__factory,
HypXERC20__factory,
IXERC20,
IXERC20__factory,
} from '@hyperlane-xyz/core';
import { ERC20__factory } from '@hyperlane-xyz/core';
import { createWarpRouteConfigId } from '@hyperlane-xyz/registry';
import {
ChainMap,
@ -638,10 +638,10 @@ async function checkWarpRouteMetrics(
tokenConfig: WarpRouteConfig,
chainMetadata: ChainMap<ChainMetadata>,
) {
const tokenPriceGetter = CoinGeckoTokenPriceGetter.withDefaultCoinGecko(
const tokenPriceGetter = new CoinGeckoTokenPriceGetter({
chainMetadata,
await getCoinGeckoApiKey(),
);
apiKey: await getCoinGeckoApiKey(),
});
setInterval(async () => {
try {

@ -1,6 +1,6 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js';
import { expect } from 'chai';
import { BigNumber, utils } from 'ethers';
import { BigNumber } from 'ethers';
import hre from 'hardhat';
import {
@ -27,6 +27,7 @@ import {
TestChainName,
TestCoreApp,
TestCoreDeployer,
randomAddress,
} from '@hyperlane-xyz/sdk';
import { Address, CallData, eqAddress } from '@hyperlane-xyz/utils';
@ -35,9 +36,6 @@ import {
HyperlaneAppGovernor,
} from '../src/govern/HyperlaneAppGovernor.js';
// TODO de-dupe with test-utils after migrating this file to the SDK
const randomAddress = () => utils.hexlify(utils.randomBytes(20));
export class TestApp extends HyperlaneApp<{}> {}
export class TestChecker extends HyperlaneAppChecker<TestApp, OwnableConfig> {

@ -15,7 +15,6 @@
"@solana/spl-token": "^0.4.9",
"@solana/web3.js": "^1.95.4",
"bignumber.js": "^9.1.1",
"coingecko-api-v3": "^0.0.29",
"cosmjs-types": "^0.9.0",
"cross-fetch": "^3.1.5",
"ethers": "^5.7.2",

@ -1,4 +1,8 @@
import { Mailbox, Mailbox__factory } from '@hyperlane-xyz/core';
import {
Mailbox,
Mailbox__factory,
Ownable__factory,
} from '@hyperlane-xyz/core';
import {
Address,
Domain,
@ -24,6 +28,7 @@ import {
ProxyFactoryFactories,
proxyFactoryFactories,
} from '../deploy/contracts.js';
import { proxyAdminUpdateTxs } from '../deploy/proxy.js';
import { ContractVerifier } from '../deploy/verify/ContractVerifier.js';
import { HookFactories } from '../hook/contracts.js';
import { EvmIsmModule } from '../ism/EvmIsmModule.js';
@ -65,6 +70,7 @@ export class EvmCoreModule extends HyperlaneModule<
this.chainName = multiProvider.getChainName(args.chain);
this.chainId = multiProvider.getEvmChainId(args.chain);
this.domainId = multiProvider.getDomainId(args.chain);
this.chainId = multiProvider.getEvmChainId(args.chain);
}
/**
@ -92,6 +98,12 @@ export class EvmCoreModule extends HyperlaneModule<
transactions.push(
...(await this.createDefaultIsmUpdateTxs(actualConfig, expectedConfig)),
...this.createMailboxOwnerUpdateTxs(actualConfig, expectedConfig),
...proxyAdminUpdateTxs(
this.chainId,
this.args.addresses.mailbox,
actualConfig,
expectedConfig,
),
);
return transactions;
@ -276,15 +288,17 @@ export class EvmCoreModule extends HyperlaneModule<
);
// Deploy proxyAdmin
const proxyAdmin = (
await coreDeployer.deployContract(chainName, 'proxyAdmin', [])
).address;
const proxyAdmin = await coreDeployer.deployContract(
chainName,
'proxyAdmin',
[],
);
// Deploy Mailbox
const mailbox = await this.deployMailbox({
config,
coreDeployer,
proxyAdmin,
proxyAdmin: proxyAdmin.address,
multiProvider,
chain,
});
@ -333,11 +347,27 @@ export class EvmCoreModule extends HyperlaneModule<
const { merkleTreeHook, interchainGasPaymaster } =
serializedContracts[chainName];
// Update the ProxyAdmin owner of the Mailbox if the config defines a different owner from the current signer
const currentProxyOwner = await proxyAdmin.owner();
if (
config?.proxyAdmin?.owner &&
!eqAddress(config.proxyAdmin.owner, currentProxyOwner)
) {
await multiProvider.sendTransaction(chainName, {
annotation: `Transferring ownership of ProxyAdmin to the configured address ${config.proxyAdmin.owner}`,
to: proxyAdmin.address,
data: Ownable__factory.createInterface().encodeFunctionData(
'transferOwnership(address)',
[config.proxyAdmin.owner],
),
});
}
// Set Core & extra addresses
return {
...ismFactoryFactories,
proxyAdmin,
proxyAdmin: proxyAdmin.address,
mailbox: mailbox.address,
interchainAccountRouter,
interchainAccountIsm,

@ -1,6 +1,6 @@
import { providers } from 'ethers';
import { Mailbox__factory } from '@hyperlane-xyz/core';
import { Mailbox__factory, ProxyAdmin__factory } from '@hyperlane-xyz/core';
import {
Address,
objMap,
@ -9,6 +9,8 @@ import {
} from '@hyperlane-xyz/utils';
import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js';
import { proxyAdmin } from '../deploy/proxy.js';
import { DeployedOwnableConfig } from '../deploy/types.js';
import { EvmHookReader } from '../hook/EvmHookReader.js';
import { EvmIsmReader } from '../ism/EvmIsmReader.js';
import { MultiProvider } from '../providers/MultiProvider.js';
@ -46,11 +48,13 @@ export class EvmCoreReader implements CoreReader {
*/
async deriveCoreConfig(address: Address): Promise<CoreConfig> {
const mailbox = Mailbox__factory.connect(address, this.provider);
const [defaultIsm, defaultHook, requiredHook] = await Promise.all([
mailbox.defaultIsm(),
mailbox.defaultHook(),
mailbox.requiredHook(),
]);
const [defaultIsm, defaultHook, requiredHook, mailboxProxyAdmin] =
await Promise.all([
mailbox.defaultIsm(),
mailbox.defaultHook(),
mailbox.requiredHook(),
proxyAdmin(this.provider, mailbox.address),
]);
// Parallelize each configuration request
const results = await promiseObjAll(
@ -60,6 +64,7 @@ export class EvmCoreReader implements CoreReader {
defaultIsm: this.evmIsmReader.deriveIsmConfig(defaultIsm),
defaultHook: this.evmHookReader.deriveHookConfig(defaultHook),
requiredHook: this.evmHookReader.deriveHookConfig(requiredHook),
proxyAdmin: this.getProxyAdminConfig(mailboxProxyAdmin),
},
async (_, readerCall) => {
try {
@ -77,4 +82,19 @@ export class EvmCoreReader implements CoreReader {
return results as CoreConfig;
}
private async getProxyAdminConfig(
proxyAdminAddress: Address,
): Promise<DeployedOwnableConfig> {
const instance = ProxyAdmin__factory.connect(
proxyAdminAddress,
this.provider,
);
const owner = await instance.owner();
return {
owner,
address: proxyAdminAddress,
};
}
}

@ -3,12 +3,15 @@ import { z } from 'zod';
import { ProxyFactoryFactoriesSchema } from '../deploy/schemas.js';
import { HookConfigSchema } from '../hook/schemas.js';
import { IsmConfigSchema } from '../ism/schemas.js';
import { OwnableSchema } from '../schemas.js';
import { DeployedOwnableSchema, OwnableSchema } from '../schemas.js';
export const CoreConfigSchema = OwnableSchema.extend({
defaultIsm: IsmConfigSchema,
defaultHook: HookConfigSchema,
requiredHook: HookConfigSchema,
// This field is set as optional because the old core config
// did not have it and we want to maintain backward compatibility
proxyAdmin: DeployedOwnableSchema.optional(),
});
export const DeployedCoreAddressesSchema = ProxyFactoryFactoriesSchema.extend({

@ -1,6 +1,12 @@
import { ethers } from 'ethers';
import { Address, eqAddress } from '@hyperlane-xyz/utils';
import { ProxyAdmin__factory } from '@hyperlane-xyz/core';
import { Address, ChainId, eqAddress } from '@hyperlane-xyz/utils';
import { transferOwnershipTransactions } from '../contracts/contracts.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { DeployedOwnableConfig } from './types.js';
export type UpgradeConfig = {
timelock: {
@ -67,3 +73,50 @@ export async function isProxy(
const admin = await proxyAdmin(provider, proxy);
return !eqAddress(admin, ethers.constants.AddressZero);
}
export function proxyAdminUpdateTxs(
chainId: ChainId,
proxyAddress: Address,
actualConfig: Readonly<{ proxyAdmin?: DeployedOwnableConfig }>,
expectedConfig: Readonly<{ proxyAdmin?: DeployedOwnableConfig }>,
): AnnotatedEV5Transaction[] {
const transactions: AnnotatedEV5Transaction[] = [];
// Return early because old config files did not have the
// proxyAdmin property
if (!expectedConfig.proxyAdmin?.address) {
return transactions;
}
const actualProxyAdmin = actualConfig.proxyAdmin!;
const parsedChainId =
typeof chainId === 'string' ? parseInt(chainId) : chainId;
if (
actualProxyAdmin.address &&
actualProxyAdmin.address !== expectedConfig.proxyAdmin.address
) {
transactions.push({
chainId: parsedChainId,
annotation: `Updating ProxyAdmin for proxy at "${proxyAddress}" from "${actualProxyAdmin.address}" to "${expectedConfig.proxyAdmin.address}"`,
to: actualProxyAdmin.address,
data: ProxyAdmin__factory.createInterface().encodeFunctionData(
'changeProxyAdmin(address,address)',
[proxyAddress, expectedConfig.proxyAdmin.address],
),
});
} else {
transactions.push(
// Internally the createTransferOwnershipTx method already checks if the
// two owner values are the same and produces an empty tx batch if they are
...transferOwnershipTransactions(
parsedChainId,
actualProxyAdmin.address!,
actualProxyAdmin,
expectedConfig.proxyAdmin,
),
);
}
return transactions;
}

@ -1,51 +1,83 @@
import { expect } from 'chai';
import sinon from 'sinon';
import { ethereum, solanamainnet } from '@hyperlane-xyz/registry';
import { TestChainName, testChainMetadata } from '../consts/testChains.js';
import { MockCoinGecko } from '../test/MockCoinGecko.js';
import { CoinGeckoTokenPriceGetter } from './token-prices.js';
const MOCK_FETCH_CALLS = true;
describe('TokenPriceGetter', () => {
let tokenPriceGetter: CoinGeckoTokenPriceGetter;
let mockCoinGecko: MockCoinGecko;
const chainA = TestChainName.test1,
chainB = TestChainName.test2,
priceA = 1,
priceB = 5.5;
before(async () => {
mockCoinGecko = new MockCoinGecko();
// Origin token
mockCoinGecko.setTokenPrice(chainA, priceA);
// Destination token
mockCoinGecko.setTokenPrice(chainB, priceB);
tokenPriceGetter = new CoinGeckoTokenPriceGetter(
mockCoinGecko,
testChainMetadata,
undefined,
0,
);
const chainA = TestChainName.test1;
const chainB = TestChainName.test2;
const priceA = 2;
const priceB = 5;
let stub: sinon.SinonStub;
beforeEach(() => {
tokenPriceGetter = new CoinGeckoTokenPriceGetter({
chainMetadata: { ethereum, solanamainnet, ...testChainMetadata },
apiKey: 'test',
expirySeconds: 10,
sleepMsBetweenRequests: 10,
});
if (MOCK_FETCH_CALLS) {
stub = sinon
.stub(tokenPriceGetter, 'fetchPriceData')
.returns(Promise.resolve([priceA, priceB]));
}
});
describe('getTokenPrice', () => {
it('returns a token price', async () => {
expect(await tokenPriceGetter.getTokenPrice(chainA)).to.equal(priceA);
afterEach(() => {
if (MOCK_FETCH_CALLS && stub) {
stub.restore();
}
});
describe('getTokenPriceByIds', () => {
it('returns token prices', async () => {
// stubbed results
expect(
await tokenPriceGetter.getTokenPriceByIds([
ethereum.name,
solanamainnet.name,
]),
).to.eql([priceA, priceB]);
});
});
it('caches a token price', async () => {
mockCoinGecko.setFail(chainA, true);
expect(await tokenPriceGetter.getTokenPrice(chainA)).to.equal(priceA);
mockCoinGecko.setFail(chainA, false);
describe('getTokenPrice', () => {
it('returns a token price', async () => {
// hardcoded result of 1 for testnets
expect(
await tokenPriceGetter.getTokenPrice(TestChainName.test1),
).to.equal(1);
// stubbed result for non-testnet
expect(await tokenPriceGetter.getTokenPrice(ethereum.name)).to.equal(
priceA,
);
});
});
describe('getTokenExchangeRate', () => {
it('returns a value consistent with getTokenPrice()', async () => {
const exchangeRate = await tokenPriceGetter.getTokenExchangeRate(
chainA,
chainB,
);
// Should equal 1 because testnet prices are always forced to 1
expect(exchangeRate).to.equal(1);
// hardcoded result of 1 for testnets
expect(
await tokenPriceGetter.getTokenExchangeRate(chainA, chainB),
).to.equal(1);
// stubbed result for non-testnet
expect(
await tokenPriceGetter.getTokenExchangeRate(
ethereum.name,
solanamainnet.name,
),
).to.equal(priceA / priceB);
});
});
});

@ -1,21 +1,15 @@
import { CoinGeckoClient, SimplePriceResponse } from 'coingecko-api-v3';
import { rootLogger, sleep } from '@hyperlane-xyz/utils';
import { objKeys, rootLogger, sleep } from '@hyperlane-xyz/utils';
import { ChainMetadata } from '../metadata/chainMetadataTypes.js';
import { ChainMap, ChainName } from '../types.js';
const COINGECKO_PRICE_API = 'https://api.coingecko.com/api/v3/simple/price';
export interface TokenPriceGetter {
getTokenPrice(chain: ChainName): Promise<number>;
getTokenExchangeRate(base: ChainName, quote: ChainName): Promise<number>;
}
export type CoinGeckoInterface = Pick<CoinGeckoClient, 'simplePrice'>;
export type CoinGeckoSimplePriceInterface = CoinGeckoClient['simplePrice'];
export type CoinGeckoSimplePriceParams =
Parameters<CoinGeckoSimplePriceInterface>[0];
export type CoinGeckoResponse = ReturnType<CoinGeckoSimplePriceInterface>;
type TokenPriceCacheEntry = {
price: number;
timestamp: Date;
@ -65,38 +59,28 @@ class TokenPriceCache {
}
export class CoinGeckoTokenPriceGetter implements TokenPriceGetter {
protected coinGecko: CoinGeckoInterface;
protected cache: TokenPriceCache;
protected apiKey?: string;
protected sleepMsBetweenRequests: number;
protected metadata: ChainMap<ChainMetadata>;
constructor(
coinGecko: CoinGeckoInterface,
chainMetadata: ChainMap<ChainMetadata>,
expirySeconds?: number,
constructor({
chainMetadata,
apiKey,
expirySeconds,
sleepMsBetweenRequests = 5000,
) {
this.coinGecko = coinGecko;
}: {
chainMetadata: ChainMap<ChainMetadata>;
apiKey?: string;
expirySeconds?: number;
sleepMsBetweenRequests?: number;
}) {
this.apiKey = apiKey;
this.cache = new TokenPriceCache(expirySeconds);
this.metadata = chainMetadata;
this.sleepMsBetweenRequests = sleepMsBetweenRequests;
}
static withDefaultCoinGecko(
chainMetadata: ChainMap<ChainMetadata>,
apiKey?: string,
expirySeconds?: number,
sleepMsBetweenRequests = 5000,
): CoinGeckoTokenPriceGetter {
const coinGecko = new CoinGeckoClient(undefined, apiKey);
return new CoinGeckoTokenPriceGetter(
coinGecko,
chainMetadata,
expirySeconds,
sleepMsBetweenRequests,
);
}
async getTokenPrice(
chain: ChainName,
currency: string = 'usd',
@ -105,6 +89,15 @@ export class CoinGeckoTokenPriceGetter implements TokenPriceGetter {
return price;
}
async getAllTokenPrices(currency: string = 'usd'): Promise<ChainMap<number>> {
const chains = objKeys(this.metadata);
const prices = await this.getTokenPrices(chains, currency);
return chains.reduce(
(agg, chain, i) => ({ ...agg, [chain]: prices[i] }),
{},
);
}
async getTokenExchangeRate(
base: ChainName,
quote: ChainName,
@ -153,14 +146,9 @@ export class CoinGeckoTokenPriceGetter implements TokenPriceGetter {
await sleep(this.sleepMsBetweenRequests);
if (toQuery.length > 0) {
let response: SimplePriceResponse;
try {
response = await this.coinGecko.simplePrice({
ids: toQuery.join(','),
vs_currencies: currency,
});
const prices = toQuery.map((id) => response[id][currency]);
toQuery.map((id, i) => this.cache.put(id, prices[i]));
const prices = await this.fetchPriceData(toQuery, currency);
prices.forEach((price, i) => this.cache.put(toQuery[i], price));
} catch (e) {
rootLogger.warn('Error when querying token prices', e);
return undefined;
@ -168,4 +156,25 @@ export class CoinGeckoTokenPriceGetter implements TokenPriceGetter {
}
return ids.map((id) => this.cache.fetch(id));
}
public async fetchPriceData(
ids: string[],
currency: string,
): Promise<number[]> {
let url = `${COINGECKO_PRICE_API}?ids=${Object.entries(ids).join(
',',
)}&vs_currencies=${currency}`;
if (this.apiKey) {
url += `&x-cg-pro-api-key=${this.apiKey}`;
}
const resp = await fetch(url);
const idPrices = await resp.json();
return ids.map((id) => {
const price = idPrices[id]?.[currency];
if (!price) throw new Error(`No price found for ${id}`);
return Number(price);
});
}
}

@ -9,7 +9,6 @@ import {
} from '../consts/igp.js';
import { ChainMetadataManager } from '../metadata/ChainMetadataManager.js';
import { AgentCosmosGasPrice } from '../metadata/agentConfig.js';
import { ChainMetadata } from '../metadata/chainMetadataTypes.js';
import { MultiProtocolProvider } from '../providers/MultiProtocolProvider.js';
import { ChainMap, ChainName } from '../types.js';
import { getCosmosRegistryChain } from '../utils/cosmos.js';
@ -215,37 +214,3 @@ export function getLocalStorageGasOracleConfig({
};
}, {} as ChainMap<StorageGasOracleConfig>);
}
const COINGECKO_PRICE_API = 'https://api.coingecko.com/api/v3/simple/price';
export async function getCoingeckoTokenPrices(
chainMetadata: ChainMap<ChainMetadata>,
currency = 'usd',
): Promise<ChainMap<string | undefined>> {
const ids = objMap(
chainMetadata,
(_, metadata) => metadata.gasCurrencyCoinGeckoId ?? metadata.name,
);
const resp = await fetch(
`${COINGECKO_PRICE_API}?ids=${Object.entries(ids).join(
',',
)}&vs_currencies=${currency}`,
);
const idPrices = await resp.json();
const prices = objMap(ids, (chain, id) => {
const idData = idPrices[id];
if (!idData) {
return undefined;
}
const price = idData[currency];
if (!price) {
return undefined;
}
return price.toString();
});
return prices;
}

@ -19,6 +19,7 @@ import { WithAddress } from '@hyperlane-xyz/utils';
import { TestChainName, test1 } from '../consts/testChains.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { randomAddress } from '../test/testUtils.js';
import { EvmHookReader } from './EvmHookReader.js';
import {
@ -35,8 +36,6 @@ describe('EvmHookReader', () => {
let multiProvider: MultiProvider;
let sandbox: sinon.SinonSandbox;
const generateRandomAddress = () => ethers.Wallet.createRandom().address;
beforeEach(() => {
sandbox = sinon.createSandbox();
multiProvider = MultiProvider.createTestMultiProvider();
@ -48,8 +47,8 @@ describe('EvmHookReader', () => {
});
it('should derive merkle tree config correctly', async () => {
const mockAddress = generateRandomAddress();
const mockOwner = generateRandomAddress();
const mockAddress = randomAddress();
const mockOwner = randomAddress();
// Mocking the connect method + returned what we need from contract object
const mockContract = {
@ -78,9 +77,9 @@ describe('EvmHookReader', () => {
});
it('should derive protocol fee hook correctly', async () => {
const mockAddress = generateRandomAddress();
const mockOwner = generateRandomAddress();
const mockBeneficiary = generateRandomAddress();
const mockAddress = randomAddress();
const mockOwner = randomAddress();
const mockBeneficiary = randomAddress();
// Mocking the connect method + returned what we need from contract object
const mockContract = {
@ -116,8 +115,8 @@ describe('EvmHookReader', () => {
});
it('should derive pausable config correctly', async () => {
const mockAddress = generateRandomAddress();
const mockOwner = generateRandomAddress();
const mockAddress = randomAddress();
const mockOwner = randomAddress();
const mockPaused = randomBytes(1)[0] % 2 === 0;
// Mocking the connect method + returned what we need from contract object
@ -151,9 +150,9 @@ describe('EvmHookReader', () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
it('should derive op stack config correctly', async () => {
const mockAddress = generateRandomAddress();
const mockOwner = generateRandomAddress();
const l1Messenger = generateRandomAddress();
const mockAddress = randomAddress();
const mockOwner = randomAddress();
const l1Messenger = randomAddress();
// Mocking the connect method + returned what we need from contract object
const mockContract = {
@ -187,8 +186,8 @@ describe('EvmHookReader', () => {
});
it('should throw if derivation fails', async () => {
const mockAddress = generateRandomAddress();
const mockOwner = generateRandomAddress();
const mockAddress = randomAddress();
const mockOwner = randomAddress();
// Mocking the connect method + returned what we need from contract object
const mockContract = {

@ -26,6 +26,7 @@ export {
testCosmosChain,
testSealevelChain,
} from './consts/testChains.js';
export { randomAddress } from './test/testUtils.js';
export {
attachAndConnectContracts,
attachContracts,
@ -554,7 +555,6 @@ export {
ChainGasOracleParams,
GasPriceConfig,
NativeTokenPriceConfig,
getCoingeckoTokenPrices,
getCosmosChainGasPrice,
getGasPrice,
getLocalStorageGasOracleConfig,

@ -1,5 +1,4 @@
import { expect } from 'chai';
import { ethers } from 'ethers';
import sinon from 'sinon';
import {
@ -20,6 +19,7 @@ import { WithAddress } from '@hyperlane-xyz/utils';
import { TestChainName } from '../consts/testChains.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { randomAddress } from '../test/testUtils.js';
import { EvmIsmReader } from './EvmIsmReader.js';
import {
@ -35,8 +35,6 @@ describe('EvmIsmReader', () => {
let multiProvider: MultiProvider;
let sandbox: sinon.SinonSandbox;
const generateRandomAddress = () => ethers.Wallet.createRandom().address;
beforeEach(() => {
sandbox = sinon.createSandbox();
multiProvider = MultiProvider.createTestMultiProvider();
@ -48,8 +46,8 @@ describe('EvmIsmReader', () => {
});
it('should derive multisig config correctly', async () => {
const mockAddress = generateRandomAddress();
const mockValidators = [generateRandomAddress(), generateRandomAddress()];
const mockAddress = randomAddress();
const mockValidators = [randomAddress(), randomAddress()];
const mockThreshold = 2;
// Mocking the connect method + returned what we need from contract object
@ -83,8 +81,8 @@ describe('EvmIsmReader', () => {
});
it('should derive pausable config correctly', async () => {
const mockAddress = generateRandomAddress();
const mockOwner = generateRandomAddress();
const mockAddress = randomAddress();
const mockOwner = randomAddress();
const mockPaused = true;
// Mocking the connect method + returned what we need from contract object
@ -120,7 +118,7 @@ describe('EvmIsmReader', () => {
});
it('should derive test ISM config correctly', async () => {
const mockAddress = generateRandomAddress();
const mockAddress = randomAddress();
// Mocking the connect method + returned what we need from contract object
const mockContract = {

@ -1,47 +0,0 @@
import { SimplePriceResponse } from 'coingecko-api-v3';
import type {
CoinGeckoInterface,
CoinGeckoResponse,
CoinGeckoSimplePriceInterface,
CoinGeckoSimplePriceParams,
} from '../gas/token-prices.js';
import type { ChainName } from '../types.js';
// A mock CoinGecko intended to be used by tests
export class MockCoinGecko implements CoinGeckoInterface {
// Prices keyed by coingecko id
private tokenPrices: Record<string, number>;
// Whether or not to fail to return a response, keyed by coingecko id
private fail: Record<string, boolean>;
constructor() {
this.tokenPrices = {};
this.fail = {};
}
price(input: CoinGeckoSimplePriceParams): CoinGeckoResponse {
const data: SimplePriceResponse = {};
for (const id of input.ids) {
if (this.fail[id]) {
return Promise.reject(`Failed to fetch price for ${id}`);
}
data[id] = {
usd: this.tokenPrices[id],
};
}
return Promise.resolve(data);
}
get simplePrice(): CoinGeckoSimplePriceInterface {
return this.price;
}
setTokenPrice(chain: ChainName, price: number): void {
this.tokenPrices[chain] = price;
}
setFail(chain: ChainName, fail: boolean): void {
this.fail[chain] = fail;
}
}

@ -15,7 +15,6 @@ import {
addressToBytes32,
assert,
deepEquals,
eqAddress,
isObjEmpty,
objMap,
rootLogger,
@ -26,6 +25,7 @@ import {
HyperlaneModule,
HyperlaneModuleParams,
} from '../core/AbstractHyperlaneModule.js';
import { proxyAdminUpdateTxs } from '../deploy/proxy.js';
import { EvmIsmModule } from '../ism/EvmIsmModule.js';
import { DerivedIsmConfig } from '../ism/EvmIsmReader.js';
import { MultiProvider } from '../providers/MultiProvider.js';
@ -67,6 +67,7 @@ export class EvmERC20WarpModule extends HyperlaneModule<
this.chainName = this.multiProvider.getChainName(args.chain);
this.chainId = multiProvider.getEvmChainId(args.chain);
this.domainId = multiProvider.getDomainId(args.chain);
this.chainId = multiProvider.getEvmChainId(args.chain);
this.contractVerifier ??= new ContractVerifier(
multiProvider,
{},
@ -112,7 +113,12 @@ export class EvmERC20WarpModule extends HyperlaneModule<
...this.createRemoteRoutersUpdateTxs(actualConfig, expectedConfig),
...this.createSetDestinationGasUpdateTxs(actualConfig, expectedConfig),
...this.createOwnershipUpdateTxs(actualConfig, expectedConfig),
...this.updateProxyAdminOwnershipTxs(actualConfig, expectedConfig),
...proxyAdminUpdateTxs(
this.chainId,
this.args.addresses.deployedTokenRoute,
actualConfig,
expectedConfig,
),
);
return transactions;
@ -291,38 +297,6 @@ export class EvmERC20WarpModule extends HyperlaneModule<
);
}
updateProxyAdminOwnershipTxs(
actualConfig: Readonly<TokenRouterConfig>,
expectedConfig: Readonly<TokenRouterConfig>,
): AnnotatedEV5Transaction[] {
const transactions: AnnotatedEV5Transaction[] = [];
// Return early because old warp config files did not have the
// proxyAdmin property
if (!expectedConfig.proxyAdmin) {
return transactions;
}
const actualProxyAdmin = actualConfig.proxyAdmin!;
assert(
eqAddress(actualProxyAdmin.address!, expectedConfig.proxyAdmin.address!),
`ProxyAdmin contract addresses do not match. Expected ${expectedConfig.proxyAdmin.address}, got ${actualProxyAdmin.address}`,
);
transactions.push(
// Internally the createTransferOwnershipTx method already checks if the
// two owner values are the same and produces an empty tx batch if they are
...transferOwnershipTransactions(
this.chainId,
actualProxyAdmin.address!,
actualProxyAdmin,
expectedConfig.proxyAdmin,
),
);
return transactions;
}
/**
* Updates or deploys the ISM using the provided configuration.
*

@ -47,6 +47,10 @@ export const TokenConfigSchema = z.object({
.array(TokenConnectionConfigSchema)
.optional()
.describe('The list of token connections (e.g. warp or IBC)'),
coinGeckoId: z
.string()
.optional()
.describe('The CoinGecko id of the token, used for price lookups'),
});
export type TokenArgs = Omit<

@ -1,6 +1,18 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier"
],
"plugins": ["react", "react-hooks", "@typescript-eslint"],
"rules": {
// TODO use utils rootLogger in widgets lib
"no-console": ["off"]
"no-console": ["off"],
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}

@ -1,3 +1,4 @@
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
import type { StorybookConfig } from '@storybook/react-vite';
import { mergeConfig } from 'vite';
@ -19,6 +20,15 @@ const config: StorybookConfig = {
async viteFinal(config, { configType }) {
return mergeConfig(config, {
define: { 'process.env': {} },
optimizeDeps: {
esbuildOptions: {
plugins: [
NodeGlobalsPolyfillPlugin({
buffer: true,
}),
],
},
},
});
},
};

@ -32,6 +32,8 @@
"babel-loader": "^8.3.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-storybook": "^0.6.15",
"postcss": "^8.4.21",
"prettier": "^2.8.8",

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save