diff --git a/.github/workflows/agent-release-artifacts.yml b/.github/workflows/agent-release-artifacts.yml index 79c421f22a..2827120989 100644 --- a/.github/workflows/agent-release-artifacts.yml +++ b/.github/workflows/agent-release-artifacts.yml @@ -49,7 +49,7 @@ jobs: run: | sudo apt-get update -qq sudo apt-get install -qq crossbuild-essential-arm64 crossbuild-essential-armhf - + # some additional configuration for cross-compilation on linux cat >>~/.cargo/config <), } impl From for ChainCommunicationError { diff --git a/rust/chains/hyperlane-cosmos/src/interchain_security_module.rs b/rust/chains/hyperlane-cosmos/src/interchain_security_module.rs index bfde29f298..dd495be89a 100644 --- a/rust/chains/hyperlane-cosmos/src/interchain_security_module.rs +++ b/rust/chains/hyperlane-cosmos/src/interchain_security_module.rs @@ -82,7 +82,7 @@ impl InterchainSecurityModule for CosmosInterchainSecurityModule { .await?; let module_type_response = - serde_json::from_slice::(&data)?; + serde_json::from_slice::(&data)?; Ok(IsmType(module_type_response.typ).into()) } diff --git a/rust/chains/hyperlane-cosmos/src/lib.rs b/rust/chains/hyperlane-cosmos/src/lib.rs index 82a4a0ece1..c0ce3ad549 100644 --- a/rust/chains/hyperlane-cosmos/src/lib.rs +++ b/rust/chains/hyperlane-cosmos/src/lib.rs @@ -16,6 +16,7 @@ mod multisig_ism; mod payloads; mod providers; mod routing_ism; +mod rpc_clients; mod signers; mod trait_builder; mod types; diff --git a/rust/chains/hyperlane-cosmos/src/payloads/aggregate_ism.rs b/rust/chains/hyperlane-cosmos/src/payloads/aggregate_ism.rs index 8276675ff7..23bb35a8f8 100644 --- a/rust/chains/hyperlane-cosmos/src/payloads/aggregate_ism.rs +++ b/rust/chains/hyperlane-cosmos/src/payloads/aggregate_ism.rs @@ -1,11 +1,11 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct VerifyRequest { pub verify: VerifyRequestInner, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct VerifyRequestInner { pub metadata: String, pub message: String, diff --git a/rust/chains/hyperlane-cosmos/src/payloads/general.rs b/rust/chains/hyperlane-cosmos/src/payloads/general.rs index 488cae2d37..af2a4b0b6e 100644 --- a/rust/chains/hyperlane-cosmos/src/payloads/general.rs +++ b/rust/chains/hyperlane-cosmos/src/payloads/general.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct EmptyStruct {} #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/rust/chains/hyperlane-cosmos/src/payloads/ism_routes.rs b/rust/chains/hyperlane-cosmos/src/payloads/ism_routes.rs index 052a1cc48b..4a0563945f 100644 --- a/rust/chains/hyperlane-cosmos/src/payloads/ism_routes.rs +++ b/rust/chains/hyperlane-cosmos/src/payloads/ism_routes.rs @@ -1,12 +1,12 @@ use super::general::EmptyStruct; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct IsmRouteRequest { pub route: IsmRouteRequestInner, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct IsmRouteRequestInner { pub message: String, // hexbinary } @@ -16,22 +16,22 @@ pub struct IsmRouteRespnose { pub ism: String, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct QueryRoutingIsmGeneralRequest { pub routing_ism: T, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct QueryRoutingIsmRouteResponse { pub ism: String, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct QueryIsmGeneralRequest { pub ism: T, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct QueryIsmModuleTypeRequest { pub module_type: EmptyStruct, } @@ -39,5 +39,5 @@ pub struct QueryIsmModuleTypeRequest { #[derive(Serialize, Deserialize, Debug)] pub struct QueryIsmModuleTypeResponse { #[serde(rename = "type")] - pub typ: hpl_interface::ism::IsmType, + pub typ: hyperlane_cosmwasm_interface::ism::IsmType, } diff --git a/rust/chains/hyperlane-cosmos/src/payloads/mailbox.rs b/rust/chains/hyperlane-cosmos/src/payloads/mailbox.rs index 145ba5b16c..75eef04595 100644 --- a/rust/chains/hyperlane-cosmos/src/payloads/mailbox.rs +++ b/rust/chains/hyperlane-cosmos/src/payloads/mailbox.rs @@ -3,52 +3,52 @@ use serde::{Deserialize, Serialize}; use super::general::EmptyStruct; // Requests -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct GeneralMailboxQuery { pub mailbox: T, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct CountRequest { pub count: EmptyStruct, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct NonceRequest { pub nonce: EmptyStruct, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct RecipientIsmRequest { pub recipient_ism: RecipientIsmRequestInner, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct RecipientIsmRequestInner { pub recipient_addr: String, // hexbinary } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct DefaultIsmRequest { pub default_ism: EmptyStruct, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct DeliveredRequest { pub message_delivered: DeliveredRequestInner, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct DeliveredRequestInner { pub id: String, // hexbinary } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct ProcessMessageRequest { pub process: ProcessMessageRequestInner, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct ProcessMessageRequestInner { pub metadata: String, pub message: String, diff --git a/rust/chains/hyperlane-cosmos/src/payloads/merkle_tree_hook.rs b/rust/chains/hyperlane-cosmos/src/payloads/merkle_tree_hook.rs index 7635f0ef72..e960628771 100644 --- a/rust/chains/hyperlane-cosmos/src/payloads/merkle_tree_hook.rs +++ b/rust/chains/hyperlane-cosmos/src/payloads/merkle_tree_hook.rs @@ -4,24 +4,24 @@ use super::general::EmptyStruct; const TREE_DEPTH: usize = 32; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct MerkleTreeGenericRequest { pub merkle_hook: T, } // --------- Requests --------- -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct MerkleTreeRequest { pub tree: EmptyStruct, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct MerkleTreeCountRequest { pub count: EmptyStruct, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct CheckPointRequest { pub check_point: EmptyStruct, } diff --git a/rust/chains/hyperlane-cosmos/src/payloads/multisig_ism.rs b/rust/chains/hyperlane-cosmos/src/payloads/multisig_ism.rs index 204e726dc7..c56588d1d6 100644 --- a/rust/chains/hyperlane-cosmos/src/payloads/multisig_ism.rs +++ b/rust/chains/hyperlane-cosmos/src/payloads/multisig_ism.rs @@ -1,11 +1,11 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct VerifyInfoRequest { pub verify_info: VerifyInfoRequestInner, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct VerifyInfoRequestInner { pub message: String, // hexbinary } diff --git a/rust/chains/hyperlane-cosmos/src/payloads/validator_announce.rs b/rust/chains/hyperlane-cosmos/src/payloads/validator_announce.rs index fdf449c7c4..cf4e5eb1f8 100644 --- a/rust/chains/hyperlane-cosmos/src/payloads/validator_announce.rs +++ b/rust/chains/hyperlane-cosmos/src/payloads/validator_announce.rs @@ -2,17 +2,17 @@ use serde::{Deserialize, Serialize}; use super::general::EmptyStruct; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct GetAnnouncedValidatorsRequest { pub get_announced_validators: EmptyStruct, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct GetAnnounceStorageLocationsRequest { pub get_announce_storage_locations: GetAnnounceStorageLocationsRequestInner, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct GetAnnounceStorageLocationsRequestInner { pub validators: Vec, } diff --git a/rust/chains/hyperlane-cosmos/src/providers/grpc.rs b/rust/chains/hyperlane-cosmos/src/providers/grpc.rs index 3594c7398e..a6bc070aba 100644 --- a/rust/chains/hyperlane-cosmos/src/providers/grpc.rs +++ b/rust/chains/hyperlane-cosmos/src/providers/grpc.rs @@ -24,7 +24,9 @@ use cosmrs::{ tx::{self, Fee, MessageExt, SignDoc, SignerInfo}, Any, Coin, }; +use derive_new::new; use hyperlane_core::{ + rpc_clients::{BlockNumberGetter, FallbackProvider}, ChainCommunicationError, ChainResult, ContractLocator, FixedPointNumber, HyperlaneDomain, U256, }; use protobuf::Message as _; @@ -33,9 +35,10 @@ use tonic::{ transport::{Channel, Endpoint}, GrpcMethod, IntoRequest, }; +use url::Url; -use crate::HyperlaneCosmosError; use crate::{address::CosmosAddress, CosmosAmount}; +use crate::{rpc_clients::CosmosFallbackProvider, HyperlaneCosmosError}; use crate::{signers::Signer, ConnectionConf}; /// A multiplier applied to a simulated transaction's gas usage to @@ -45,6 +48,36 @@ const GAS_ESTIMATE_MULTIPLIER: f64 = 1.25; /// be valid for. const TIMEOUT_BLOCKS: u64 = 1000; +#[derive(Debug, Clone, new)] +struct CosmosChannel { + channel: Channel, + /// The url that this channel is connected to. + /// Not explicitly used, but useful for debugging. + _url: Url, +} + +#[async_trait] +impl BlockNumberGetter for CosmosChannel { + async fn get_block_number(&self) -> Result { + let mut client = ServiceClient::new(self.channel.clone()); + let request = tonic::Request::new(GetLatestBlockRequest {}); + + let response = client + .get_latest_block(request) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + let height = response + .block + .ok_or_else(|| ChainCommunicationError::from_other_str("block not present"))? + .header + .ok_or_else(|| ChainCommunicationError::from_other_str("header not present"))? + .height; + + Ok(height as u64) + } +} + #[async_trait] /// Cosmwasm GRPC Provider pub trait WasmProvider: Send + Sync { @@ -56,14 +89,14 @@ pub trait WasmProvider: Send + Sync { async fn latest_block_height(&self) -> ChainResult; /// Perform a wasm query against the stored contract address. - async fn wasm_query( + async fn wasm_query( &self, payload: T, block_height: Option, ) -> ChainResult>; /// Perform a wasm query against a specified contract address. - async fn wasm_query_to( + async fn wasm_query_to( &self, to: String, payload: T, @@ -71,14 +104,17 @@ pub trait WasmProvider: Send + Sync { ) -> ChainResult>; /// Send a wasm tx. - async fn wasm_send( + async fn wasm_send( &self, payload: T, gas_limit: Option, ) -> ChainResult; /// Estimate gas for a wasm tx. - async fn wasm_estimate_gas(&self, payload: T) -> ChainResult; + async fn wasm_estimate_gas( + &self, + payload: T, + ) -> ChainResult; } #[derive(Debug, Clone)] @@ -95,7 +131,7 @@ pub struct WasmGrpcProvider { signer: Option, /// GRPC Channel that can be cheaply cloned. /// See `` - channel: Channel, + provider: CosmosFallbackProvider, gas_price: CosmosAmount, } @@ -108,9 +144,21 @@ impl WasmGrpcProvider { locator: Option, signer: Option, ) -> ChainResult { - let endpoint = - Endpoint::new(conf.get_grpc_url()).map_err(Into::::into)?; - let channel = endpoint.connect_lazy(); + // get all the configured grpc urls and convert them to a Vec + let channels: Result, _> = conf + .get_grpc_urls() + .into_iter() + .map(|url| { + Endpoint::new(url.to_string()) + .map(|e| CosmosChannel::new(e.connect_lazy(), url)) + .map_err(Into::::into) + }) + .collect(); + let mut builder = FallbackProvider::builder(); + builder = builder.add_providers(channels?); + let fallback_provider = builder.build(); + let provider = CosmosFallbackProvider::new(fallback_provider); + let contract_address = locator .map(|l| { CosmosAddress::from_h256( @@ -126,7 +174,7 @@ impl WasmGrpcProvider { conf, contract_address, signer, - channel, + provider, gas_price, }) } @@ -225,21 +273,36 @@ impl WasmGrpcProvider { // https://github.com/cosmos/cosmjs/blob/44893af824f0712d1f406a8daa9fcae335422235/packages/stargate/src/modules/tx/queries.ts#L67 signatures: vec![vec![]], }; - - let mut client = TxServiceClient::new(self.channel.clone()); let tx_bytes = raw_tx .to_bytes() .map_err(ChainCommunicationError::from_other)?; - #[allow(deprecated)] - let sim_req = tonic::Request::new(SimulateRequest { tx: None, tx_bytes }); - let gas_used = client - .simulate(sim_req) - .await - .map_err(ChainCommunicationError::from_other)? - .into_inner() - .gas_info - .ok_or_else(|| ChainCommunicationError::from_other_str("gas info not present"))? - .gas_used; + let gas_used = self + .provider + .call(move |provider| { + let tx_bytes_clone = tx_bytes.clone(); + let future = async move { + let mut client = TxServiceClient::new(provider.channel.clone()); + #[allow(deprecated)] + let sim_req = tonic::Request::new(SimulateRequest { + tx: None, + tx_bytes: tx_bytes_clone, + }); + let gas_used = client + .simulate(sim_req) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner() + .gas_info + .ok_or_else(|| { + ChainCommunicationError::from_other_str("gas info not present") + })? + .gas_used; + + Ok(gas_used) + }; + Box::pin(future) + }) + .await?; let gas_estimate = (gas_used as f64 * GAS_ESTIMATE_MULTIPLIER) as u64; @@ -248,14 +311,25 @@ impl WasmGrpcProvider { /// Fetches balance for a given `address` and `denom` pub async fn get_balance(&self, address: String, denom: String) -> ChainResult { - let mut client = QueryBalanceClient::new(self.channel.clone()); - - let balance_request = tonic::Request::new(QueryBalanceRequest { address, denom }); - let response = client - .balance(balance_request) - .await - .map_err(ChainCommunicationError::from_other)? - .into_inner(); + let response = self + .provider + .call(move |provider| { + let address = address.clone(); + let denom = denom.clone(); + let future = async move { + let mut client = QueryBalanceClient::new(provider.channel.clone()); + let balance_request = + tonic::Request::new(QueryBalanceRequest { address, denom }); + let response = client + .balance(balance_request) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + Ok(response) + }; + Box::pin(future) + }) + .await?; let balance = response .balance @@ -272,14 +346,23 @@ impl WasmGrpcProvider { return self.account_query_injective(account).await; } - let mut client = QueryAccountClient::new(self.channel.clone()); - - let request = tonic::Request::new(QueryAccountRequest { address: account }); - let response = client - .account(request) - .await - .map_err(ChainCommunicationError::from_other)? - .into_inner(); + let response = self + .provider + .call(move |provider| { + let address = account.clone(); + let future = async move { + let mut client = QueryAccountClient::new(provider.channel.clone()); + let request = tonic::Request::new(QueryAccountRequest { address }); + let response = client + .account(request) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + Ok(response) + }; + Box::pin(future) + }) + .await?; let account = BaseAccount::decode( response @@ -294,32 +377,46 @@ impl WasmGrpcProvider { /// Injective-specific logic for querying an account. async fn account_query_injective(&self, account: String) -> ChainResult { - let request = tonic::Request::new( - injective_std::types::cosmos::auth::v1beta1::QueryAccountRequest { address: account }, - ); - - // Borrowed from the logic of `QueryAccountClient` in `cosmrs`, but using injective types. - - let mut grpc_client = tonic::client::Grpc::new(self.channel.clone()); - grpc_client - .ready() - .await - .map_err(Into::::into)?; - - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static("/cosmos.auth.v1beta1.Query/Account"); - let mut req: tonic::Request< - injective_std::types::cosmos::auth::v1beta1::QueryAccountRequest, - > = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("cosmos.auth.v1beta1.Query", "Account")); - - let response: tonic::Response< - injective_std::types::cosmos::auth::v1beta1::QueryAccountResponse, - > = grpc_client - .unary(req, path, codec) - .await - .map_err(Into::::into)?; + let response = self + .provider + .call(move |provider| { + let address = account.clone(); + let future = async move { + let request = tonic::Request::new( + injective_std::types::cosmos::auth::v1beta1::QueryAccountRequest { + address, + }, + ); + + // Borrowed from the logic of `QueryAccountClient` in `cosmrs`, but using injective types. + + let mut grpc_client = tonic::client::Grpc::new(provider.channel.clone()); + grpc_client + .ready() + .await + .map_err(Into::::into)?; + + let codec = tonic::codec::ProstCodec::default(); + let path = + http::uri::PathAndQuery::from_static("/cosmos.auth.v1beta1.Query/Account"); + let mut req: tonic::Request< + injective_std::types::cosmos::auth::v1beta1::QueryAccountRequest, + > = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("cosmos.auth.v1beta1.Query", "Account")); + + let response: tonic::Response< + injective_std::types::cosmos::auth::v1beta1::QueryAccountResponse, + > = grpc_client + .unary(req, path, codec) + .await + .map_err(Into::::into)?; + + Ok(response) + }; + Box::pin(future) + }) + .await?; let mut eth_account = injective_protobuf::proto::account::EthAccount::parse_from_bytes( response @@ -349,14 +446,23 @@ impl WasmGrpcProvider { #[async_trait] impl WasmProvider for WasmGrpcProvider { async fn latest_block_height(&self) -> ChainResult { - let mut client = ServiceClient::new(self.channel.clone()); - let request = tonic::Request::new(GetLatestBlockRequest {}); + let response = self + .provider + .call(move |provider| { + let future = async move { + let mut client = ServiceClient::new(provider.channel.clone()); + let request = tonic::Request::new(GetLatestBlockRequest {}); + let response = client + .get_latest_block(request) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + Ok(response) + }; + Box::pin(future) + }) + .await?; - let response = client - .get_latest_block(request) - .await - .map_err(ChainCommunicationError::from_other)? - .into_inner(); let height = response .block .ok_or_else(|| ChainCommunicationError::from_other_str("block not present"))? @@ -369,7 +475,7 @@ impl WasmProvider for WasmGrpcProvider { async fn wasm_query(&self, payload: T, block_height: Option) -> ChainResult> where - T: Serialize + Send + Sync, + T: Serialize + Send + Sync + Clone, { let contract_address = self.contract_address.as_ref().ok_or_else(|| { ChainCommunicationError::from_other_str("No contract address available") @@ -385,39 +491,48 @@ impl WasmProvider for WasmGrpcProvider { block_height: Option, ) -> ChainResult> where - T: Serialize + Send + Sync, + T: Serialize + Send + Sync + Clone, { - let mut client = WasmQueryClient::new(self.channel.clone()); - let mut request = tonic::Request::new(QuerySmartContractStateRequest { - address: to, - query_data: serde_json::to_string(&payload)?.as_bytes().to_vec(), - }); - - if let Some(block_height) = block_height { - request - .metadata_mut() - .insert("x-cosmos-block-height", block_height.into()); - } - - let response = client - .smart_contract_state(request) - .await - .map_err(ChainCommunicationError::from_other)? - .into_inner(); + let query_data = serde_json::to_string(&payload)?.as_bytes().to_vec(); + let response = self + .provider + .call(move |provider| { + let to = to.clone(); + let query_data = query_data.clone(); + let future = async move { + let mut client = WasmQueryClient::new(provider.channel.clone()); + + let mut request = tonic::Request::new(QuerySmartContractStateRequest { + address: to, + query_data, + }); + if let Some(block_height) = block_height { + request + .metadata_mut() + .insert("x-cosmos-block-height", block_height.into()); + } + let response = client + .smart_contract_state(request) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + Ok(response) + }; + Box::pin(future) + }) + .await?; Ok(response.data) } async fn wasm_send(&self, payload: T, gas_limit: Option) -> ChainResult where - T: Serialize + Send + Sync, + T: Serialize + Send + Sync + Clone, { let signer = self.get_signer()?; - let mut client = TxServiceClient::new(self.channel.clone()); let contract_address = self.contract_address.as_ref().ok_or_else(|| { ChainCommunicationError::from_other_str("No contract address available") })?; - let msgs = vec![MsgExecuteContract { sender: signer.address.clone(), contract: contract_address.address(), @@ -426,9 +541,6 @@ impl WasmProvider for WasmGrpcProvider { } .to_any() .map_err(ChainCommunicationError::from_other)?]; - - // We often use U256s to represent gas limits, but Cosmos expects u64s. Try to convert, - // and if it fails, just fallback to None which will result in gas estimation. let gas_limit: Option = gas_limit.and_then(|limit| match limit.try_into() { Ok(limit) => Some(limit), Err(err) => { @@ -439,20 +551,30 @@ impl WasmProvider for WasmGrpcProvider { None } }); - - let tx_req = BroadcastTxRequest { - tx_bytes: self.generate_raw_signed_tx(msgs, gas_limit).await?, - mode: BroadcastMode::Sync as i32, - }; - - let tx_res = client - .broadcast_tx(tx_req) - .await - .map_err(Into::::into)? - .into_inner() - .tx_response - .ok_or_else(|| ChainCommunicationError::from_other_str("Empty tx_response"))?; - + let tx_bytes = self.generate_raw_signed_tx(msgs, gas_limit).await?; + let tx_res = self + .provider + .call(move |provider| { + let tx_bytes = tx_bytes.clone(); + let future = async move { + let mut client = TxServiceClient::new(provider.channel.clone()); + // We often use U256s to represent gas limits, but Cosmos expects u64s. Try to convert, + // and if it fails, just fallback to None which will result in gas estimation. + let tx_req = BroadcastTxRequest { + tx_bytes, + mode: BroadcastMode::Sync as i32, + }; + client + .broadcast_tx(tx_req) + .await + .map_err(Into::::into)? + .into_inner() + .tx_response + .ok_or_else(|| ChainCommunicationError::from_other_str("Empty tx_response")) + }; + Box::pin(future) + }) + .await?; Ok(tx_res) } @@ -482,3 +604,10 @@ impl WasmProvider for WasmGrpcProvider { Ok(response) } } + +#[async_trait] +impl BlockNumberGetter for WasmGrpcProvider { + async fn get_block_number(&self) -> Result { + self.latest_block_height().await + } +} diff --git a/rust/chains/hyperlane-cosmos/src/rpc_clients/fallback.rs b/rust/chains/hyperlane-cosmos/src/rpc_clients/fallback.rs new file mode 100644 index 0000000000..cf933ee73d --- /dev/null +++ b/rust/chains/hyperlane-cosmos/src/rpc_clients/fallback.rs @@ -0,0 +1,140 @@ +use std::{ + fmt::{Debug, Formatter}, + ops::Deref, +}; + +use derive_new::new; +use hyperlane_core::rpc_clients::FallbackProvider; + +/// Wrapper of `FallbackProvider` for use in `hyperlane-cosmos` +#[derive(new, Clone)] +pub struct CosmosFallbackProvider { + fallback_provider: FallbackProvider, +} + +impl Deref for CosmosFallbackProvider { + type Target = FallbackProvider; + + fn deref(&self) -> &Self::Target { + &self.fallback_provider + } +} + +impl Debug for CosmosFallbackProvider +where + C: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.fallback_provider.fmt(f) + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use async_trait::async_trait; + use hyperlane_core::rpc_clients::test::ProviderMock; + use hyperlane_core::rpc_clients::{BlockNumberGetter, FallbackProviderBuilder}; + use hyperlane_core::ChainCommunicationError; + use tokio::time::sleep; + + use super::*; + + #[derive(Debug, Clone)] + struct CosmosProviderMock(ProviderMock); + + impl Deref for CosmosProviderMock { + type Target = ProviderMock; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl Default for CosmosProviderMock { + fn default() -> Self { + Self(ProviderMock::default()) + } + } + + impl CosmosProviderMock { + fn new(request_sleep: Option) -> Self { + Self(ProviderMock::new(request_sleep)) + } + } + + #[async_trait] + impl BlockNumberGetter for CosmosProviderMock { + async fn get_block_number(&self) -> Result { + Ok(0) + } + } + + impl Into> for CosmosProviderMock { + fn into(self) -> Box { + Box::new(self) + } + } + + impl CosmosFallbackProvider { + async fn low_level_test_call(&mut self) -> Result<(), ChainCommunicationError> { + self.call(|provider| { + provider.push("GET", "http://localhost:1234"); + let future = async move { + let body = tonic::body::BoxBody::default(); + let response = http::Response::builder().status(200).body(body).unwrap(); + if let Some(sleep_duration) = provider.request_sleep() { + sleep(sleep_duration).await; + } + Ok(response) + }; + Box::pin(future) + }) + .await?; + Ok(()) + } + } + + #[tokio::test] + async fn test_first_provider_is_attempted() { + let fallback_provider_builder = FallbackProviderBuilder::default(); + let providers = vec![ + CosmosProviderMock::default(), + CosmosProviderMock::default(), + CosmosProviderMock::default(), + ]; + let fallback_provider = fallback_provider_builder.add_providers(providers).build(); + let mut cosmos_fallback_provider = CosmosFallbackProvider::new(fallback_provider); + cosmos_fallback_provider + .low_level_test_call() + .await + .unwrap(); + let provider_call_count: Vec<_> = + ProviderMock::get_call_counts(&cosmos_fallback_provider).await; + assert_eq!(provider_call_count, vec![1, 0, 0]); + } + + #[tokio::test] + async fn test_one_stalled_provider() { + let fallback_provider_builder = FallbackProviderBuilder::default(); + let providers = vec![ + CosmosProviderMock::new(Some(Duration::from_millis(10))), + CosmosProviderMock::default(), + CosmosProviderMock::default(), + ]; + let fallback_provider = fallback_provider_builder + .add_providers(providers) + .with_max_block_time(Duration::from_secs(0)) + .build(); + let mut cosmos_fallback_provider = CosmosFallbackProvider::new(fallback_provider); + cosmos_fallback_provider + .low_level_test_call() + .await + .unwrap(); + + let provider_call_count: Vec<_> = + ProviderMock::get_call_counts(&cosmos_fallback_provider).await; + assert_eq!(provider_call_count, vec![0, 0, 1]); + } +} diff --git a/rust/chains/hyperlane-cosmos/src/rpc_clients/mod.rs b/rust/chains/hyperlane-cosmos/src/rpc_clients/mod.rs new file mode 100644 index 0000000000..536845688d --- /dev/null +++ b/rust/chains/hyperlane-cosmos/src/rpc_clients/mod.rs @@ -0,0 +1,3 @@ +pub use self::fallback::*; + +mod fallback; diff --git a/rust/chains/hyperlane-cosmos/src/trait_builder.rs b/rust/chains/hyperlane-cosmos/src/trait_builder.rs index 2bacb4d2f5..1bb3627b9d 100644 --- a/rust/chains/hyperlane-cosmos/src/trait_builder.rs +++ b/rust/chains/hyperlane-cosmos/src/trait_builder.rs @@ -2,12 +2,13 @@ use std::str::FromStr; use derive_new::new; use hyperlane_core::{ChainCommunicationError, FixedPointNumber}; +use url::Url; /// Cosmos connection configuration #[derive(Debug, Clone)] pub struct ConnectionConf { /// The GRPC url to connect to - grpc_url: String, + grpc_urls: Vec, /// The RPC url to connect to rpc_url: String, /// The chain ID @@ -76,8 +77,8 @@ pub enum ConnectionConfError { impl ConnectionConf { /// Get the GRPC url - pub fn get_grpc_url(&self) -> String { - self.grpc_url.clone() + pub fn get_grpc_urls(&self) -> Vec { + self.grpc_urls.clone() } /// Get the RPC url @@ -112,7 +113,7 @@ impl ConnectionConf { /// Create a new connection configuration pub fn new( - grpc_url: String, + grpc_urls: Vec, rpc_url: String, chain_id: String, bech32_prefix: String, @@ -121,7 +122,7 @@ impl ConnectionConf { contract_address_bytes: usize, ) -> Self { Self { - grpc_url, + grpc_urls, rpc_url, chain_id, bech32_prefix, diff --git a/rust/chains/hyperlane-cosmos/src/types.rs b/rust/chains/hyperlane-cosmos/src/types.rs index d7647e7ddf..aa5a954650 100644 --- a/rust/chains/hyperlane-cosmos/src/types.rs +++ b/rust/chains/hyperlane-cosmos/src/types.rs @@ -1,10 +1,10 @@ use cosmrs::proto::cosmos::base::abci::v1beta1::TxResponse; use hyperlane_core::{ChainResult, ModuleType, TxOutcome, H256, U256}; -pub struct IsmType(pub hpl_interface::ism::IsmType); +pub struct IsmType(pub hyperlane_cosmwasm_interface::ism::IsmType); -impl From for IsmType { - fn from(value: hpl_interface::ism::IsmType) -> Self { +impl From for IsmType { + fn from(value: hyperlane_cosmwasm_interface::ism::IsmType) -> Self { IsmType(value) } } @@ -12,14 +12,20 @@ impl From for IsmType { impl From for ModuleType { fn from(value: IsmType) -> Self { match value.0 { - hpl_interface::ism::IsmType::Unused => ModuleType::Unused, - hpl_interface::ism::IsmType::Routing => ModuleType::Routing, - hpl_interface::ism::IsmType::Aggregation => ModuleType::Aggregation, - hpl_interface::ism::IsmType::LegacyMultisig => ModuleType::MessageIdMultisig, - hpl_interface::ism::IsmType::MerkleRootMultisig => ModuleType::MerkleRootMultisig, - hpl_interface::ism::IsmType::MessageIdMultisig => ModuleType::MessageIdMultisig, - hpl_interface::ism::IsmType::Null => ModuleType::Null, - hpl_interface::ism::IsmType::CcipRead => ModuleType::CcipRead, + hyperlane_cosmwasm_interface::ism::IsmType::Unused => ModuleType::Unused, + hyperlane_cosmwasm_interface::ism::IsmType::Routing => ModuleType::Routing, + hyperlane_cosmwasm_interface::ism::IsmType::Aggregation => ModuleType::Aggregation, + hyperlane_cosmwasm_interface::ism::IsmType::LegacyMultisig => { + ModuleType::MessageIdMultisig + } + hyperlane_cosmwasm_interface::ism::IsmType::MerkleRootMultisig => { + ModuleType::MerkleRootMultisig + } + hyperlane_cosmwasm_interface::ism::IsmType::MessageIdMultisig => { + ModuleType::MessageIdMultisig + } + hyperlane_cosmwasm_interface::ism::IsmType::Null => ModuleType::Null, + hyperlane_cosmwasm_interface::ism::IsmType::CcipRead => ModuleType::CcipRead, } } } diff --git a/rust/chains/hyperlane-ethereum/Cargo.toml b/rust/chains/hyperlane-ethereum/Cargo.toml index 8d6db17f45..a72855a00b 100644 --- a/rust/chains/hyperlane-ethereum/Cargo.toml +++ b/rust/chains/hyperlane-ethereum/Cargo.toml @@ -30,7 +30,7 @@ tracing-futures.workspace = true tracing.workspace = true url.workspace = true -hyperlane-core = { path = "../../hyperlane-core" } +hyperlane-core = { path = "../../hyperlane-core", features = ["fallback-provider"]} ethers-prometheus = { path = "../../ethers-prometheus", features = ["serde"] } [build-dependencies] diff --git a/rust/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs b/rust/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs index 2ac6ae009f..1243ecc106 100644 --- a/rust/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs +++ b/rust/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs @@ -12,23 +12,23 @@ use serde_json::Value; use tokio::time::sleep; use tracing::{instrument, warn_span}; -use ethers_prometheus::json_rpc_client::PrometheusJsonRpcClientConfigExt; +use ethers_prometheus::json_rpc_client::{JsonRpcBlockGetter, PrometheusJsonRpcClientConfigExt}; use crate::rpc_clients::{categorize_client_response, CategorizedResponse}; /// Wrapper of `FallbackProvider` for use in `hyperlane-ethereum` #[derive(new)] -pub struct EthereumFallbackProvider(FallbackProvider); +pub struct EthereumFallbackProvider(FallbackProvider); -impl Deref for EthereumFallbackProvider { - type Target = FallbackProvider; +impl Deref for EthereumFallbackProvider { + type Target = FallbackProvider; fn deref(&self) -> &Self::Target { &self.0 } } -impl Debug for EthereumFallbackProvider +impl Debug for EthereumFallbackProvider where C: JsonRpcClient + PrometheusJsonRpcClientConfigExt, { @@ -73,16 +73,17 @@ impl From for ProviderError { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl JsonRpcClient for EthereumFallbackProvider +impl JsonRpcClient for EthereumFallbackProvider> where C: JsonRpcClient + + Into> + PrometheusJsonRpcClientConfigExt - + Into> + Clone, + JsonRpcBlockGetter: BlockNumberGetter, { type Error = ProviderError; - // TODO: Refactor the reusable parts of this function when implementing the cosmos-specific logic + // TODO: Refactor to use `FallbackProvider::call` #[instrument] async fn request(&self, method: &str, params: T) -> Result where @@ -108,7 +109,7 @@ where let resp = fut.await; self.handle_stalled_provider(priority, provider).await; let _span = - warn_span!("request_with_fallback", fallback_count=%idx, provider_index=%priority.index, ?provider).entered(); + warn_span!("request", fallback_count=%idx, provider_index=%priority.index, ?provider).entered(); match categorize_client_response(method, resp) { IsOk(v) => return Ok(serde_json::from_value(v)?), @@ -125,41 +126,37 @@ where #[cfg(test)] mod tests { use ethers_prometheus::json_rpc_client::{JsonRpcBlockGetter, BLOCK_NUMBER_RPC}; + use hyperlane_core::rpc_clients::test::ProviderMock; use hyperlane_core::rpc_clients::FallbackProviderBuilder; use super::*; - use std::sync::{Arc, Mutex}; #[derive(Debug, Clone)] - struct ProviderMock { - // Store requests as tuples of (method, params) - // Even if the tests were single-threaded, need the arc-mutex - // for interior mutability in `JsonRpcClient::request` - requests: Arc>>, - } + struct EthereumProviderMock(ProviderMock); - impl ProviderMock { - fn new() -> Self { - Self { - requests: Arc::new(Mutex::new(vec![])), - } + impl Deref for EthereumProviderMock { + type Target = ProviderMock; + + fn deref(&self) -> &Self::Target { + &self.0 } + } - fn push(&self, method: &str, params: T) { - self.requests - .lock() - .unwrap() - .push((method.to_owned(), format!("{:?}", params))); + impl Default for EthereumProviderMock { + fn default() -> Self { + Self(ProviderMock::default()) } + } - fn requests(&self) -> Vec<(String, String)> { - self.requests.lock().unwrap().clone() + impl EthereumProviderMock { + fn new(request_sleep: Option) -> Self { + Self(ProviderMock::new(request_sleep)) } } - impl Into> for ProviderMock { - fn into(self) -> Box { - Box::new(JsonRpcBlockGetter::new(self.clone())) + impl Into> for EthereumProviderMock { + fn into(self) -> JsonRpcBlockGetter { + JsonRpcBlockGetter::new(self) } } @@ -171,7 +168,7 @@ mod tests { } #[async_trait] - impl JsonRpcClient for ProviderMock { + impl JsonRpcClient for EthereumProviderMock { type Error = HttpClientError; /// Pushes the `(method, params)` to the back of the `requests` queue, @@ -182,12 +179,14 @@ mod tests { params: T, ) -> Result { self.push(method, params); - sleep(Duration::from_millis(10)).await; + if let Some(sleep_duration) = self.request_sleep() { + sleep(sleep_duration).await; + } dummy_return_value() } } - impl PrometheusJsonRpcClientConfigExt for ProviderMock { + impl PrometheusJsonRpcClientConfigExt for EthereumProviderMock { fn node_host(&self) -> &str { todo!() } @@ -197,35 +196,32 @@ mod tests { } } - async fn get_call_counts(fallback_provider: &FallbackProvider) -> Vec { - fallback_provider - .inner - .priorities - .read() - .await - .iter() - .map(|p| { - let provider = &fallback_provider.inner.providers[p.index]; - provider.requests().len() - }) - .collect() + impl EthereumFallbackProvider> + where + C: JsonRpcClient + + PrometheusJsonRpcClientConfigExt + + Into> + + Clone, + JsonRpcBlockGetter: BlockNumberGetter, + { + async fn low_level_test_call(&self) { + self.request::<_, u64>(BLOCK_NUMBER_RPC, ()).await.unwrap(); + } } #[tokio::test] async fn test_first_provider_is_attempted() { let fallback_provider_builder = FallbackProviderBuilder::default(); let providers = vec![ - ProviderMock::new(), - ProviderMock::new(), - ProviderMock::new(), + EthereumProviderMock::default(), + EthereumProviderMock::default(), + EthereumProviderMock::default(), ]; let fallback_provider = fallback_provider_builder.add_providers(providers).build(); let ethereum_fallback_provider = EthereumFallbackProvider::new(fallback_provider); - ethereum_fallback_provider - .request::<_, u64>(BLOCK_NUMBER_RPC, ()) - .await - .unwrap(); - let provider_call_count: Vec<_> = get_call_counts(ðereum_fallback_provider).await; + ethereum_fallback_provider.low_level_test_call().await; + let provider_call_count: Vec<_> = + ProviderMock::get_call_counts(ðereum_fallback_provider).await; assert_eq!(provider_call_count, vec![1, 0, 0]); } @@ -233,21 +229,18 @@ mod tests { async fn test_one_stalled_provider() { let fallback_provider_builder = FallbackProviderBuilder::default(); let providers = vec![ - ProviderMock::new(), - ProviderMock::new(), - ProviderMock::new(), + EthereumProviderMock::new(Some(Duration::from_millis(10))), + EthereumProviderMock::default(), + EthereumProviderMock::default(), ]; let fallback_provider = fallback_provider_builder .add_providers(providers) .with_max_block_time(Duration::from_secs(0)) .build(); let ethereum_fallback_provider = EthereumFallbackProvider::new(fallback_provider); - ethereum_fallback_provider - .request::<_, u64>(BLOCK_NUMBER_RPC, ()) - .await - .unwrap(); - - let provider_call_count: Vec<_> = get_call_counts(ðereum_fallback_provider).await; + ethereum_fallback_provider.low_level_test_call().await; + let provider_call_count: Vec<_> = + ProviderMock::get_call_counts(ðereum_fallback_provider).await; assert_eq!(provider_call_count, vec![0, 0, 2]); } diff --git a/rust/chains/hyperlane-ethereum/src/trait_builder.rs b/rust/chains/hyperlane-ethereum/src/trait_builder.rs index 31fa128d86..a0ab93af48 100644 --- a/rust/chains/hyperlane-ethereum/src/trait_builder.rs +++ b/rust/chains/hyperlane-ethereum/src/trait_builder.rs @@ -16,8 +16,8 @@ use reqwest::{Client, Url}; use thiserror::Error; use ethers_prometheus::json_rpc_client::{ - JsonRpcClientMetrics, JsonRpcClientMetricsBuilder, NodeInfo, PrometheusJsonRpcClient, - PrometheusJsonRpcClientConfig, + JsonRpcBlockGetter, JsonRpcClientMetrics, JsonRpcClientMetricsBuilder, NodeInfo, + PrometheusJsonRpcClient, PrometheusJsonRpcClientConfig, }; use ethers_prometheus::middleware::{ MiddlewareMetrics, PrometheusMiddleware, PrometheusMiddlewareConf, @@ -116,7 +116,10 @@ pub trait BuildableWithProvider { builder = builder.add_provider(metrics_provider); } let fallback_provider = builder.build(); - let ethereum_fallback_provider = EthereumFallbackProvider::new(fallback_provider); + let ethereum_fallback_provider = EthereumFallbackProvider::< + _, + JsonRpcBlockGetter>, + >::new(fallback_provider); self.build( ethereum_fallback_provider, locator, diff --git a/rust/chains/hyperlane-fuel/Cargo.toml b/rust/chains/hyperlane-fuel/Cargo.toml index 7dabcdd514..82bdbc782e 100644 --- a/rust/chains/hyperlane-fuel/Cargo.toml +++ b/rust/chains/hyperlane-fuel/Cargo.toml @@ -19,7 +19,7 @@ tracing-futures.workspace = true tracing.workspace = true url.workspace = true -hyperlane-core = { path = "../../hyperlane-core" } +hyperlane-core = { path = "../../hyperlane-core", features = ["fallback-provider"]} [build-dependencies] abigen = { path = "../../utils/abigen", features = ["fuels"] } diff --git a/rust/chains/hyperlane-sealevel/Cargo.toml b/rust/chains/hyperlane-sealevel/Cargo.toml index 248e3dfac1..ab4e9b17f6 100644 --- a/rust/chains/hyperlane-sealevel/Cargo.toml +++ b/rust/chains/hyperlane-sealevel/Cargo.toml @@ -24,7 +24,7 @@ tracing.workspace = true url.workspace = true account-utils = { path = "../../sealevel/libraries/account-utils" } -hyperlane-core = { path = "../../hyperlane-core", features = ["solana"] } +hyperlane-core = { path = "../../hyperlane-core", features = ["solana", "fallback-provider"] } hyperlane-sealevel-interchain-security-module-interface = { path = "../../sealevel/libraries/interchain-security-module-interface" } hyperlane-sealevel-mailbox = { path = "../../sealevel/programs/mailbox", features = ["no-entrypoint"] } hyperlane-sealevel-igp = { path = "../../sealevel/programs/hyperlane-sealevel-igp", features = ["no-entrypoint"] } diff --git a/rust/config/mainnet3_config.json b/rust/config/mainnet3_config.json index 6c91592fb7..522196bf79 100644 --- a/rust/config/mainnet3_config.json +++ b/rust/config/mainnet3_config.json @@ -465,6 +465,42 @@ "from": 437300 } }, + "neutron": { + "name": "neutron", + "domainId": "1853125230", + "chainId": "neutron-1", + "mailbox": "0x848426d50eb2104d5c6381ec63757930b1c14659c40db8b8081e516e7c5238fc", + "interchainGasPaymaster": "0x504ee9ac43ec5814e00c7d21869a90ec52becb489636bdf893b7df9d606b5d67", + "validatorAnnounce": "0xf3aa0d652226e21ae35cd9035c492ae41725edc9036edf0d6a48701b153b90a0", + "merkleTreeHook": "0xcd30a0001cc1f436c41ef764a712ebabc5a144140e3fd03eafe64a9a24e4e27c", + "protocol": "cosmos", + "finalityBlocks": 1, + "rpcUrls": [ + { + "http": "https://rpc-kralum.neutron-1.neutron.org" + } + ], + "grpcUrl": "https://grpc-kralum.neutron-1.neutron.org:80", + "canonicalAsset": "untrn", + "bech32Prefix": "neutron", + "gasPrice": { + "amount": "0.57", + "denom": "untrn" + }, + "contractAddressBytes": 32, + "index": { + "from": 4000000, + "chunk": 100000 + }, + "blocks": { + "reorgPeriod": 1 + }, + "signer": { + "type": "cosmosKey", + "key": "0x5486418967eabc770b0fcb995f7ef6d9a72f7fc195531ef76c5109f44f51af26", + "prefix": "neutron" + } + }, "injective": { "name": "injective", "domainId": "6909546", @@ -480,7 +516,11 @@ "http": "https://rpc-injective.goldenratiostaking.net:443" } ], - "grpcUrl": "https://injective-grpc.publicnode.com/", + "grpcUrls": [ + { + "http": "https://injective-grpc.goldenratiostaking.net:443" + } + ], "canonicalAsset": "inj", "bech32Prefix": "inj", "gasPrice": { @@ -860,4 +900,4 @@ } }, "defaultRpcConsensusType": "fallback" -} +} \ No newline at end of file diff --git a/rust/ethers-prometheus/src/json_rpc_client.rs b/rust/ethers-prometheus/src/json_rpc_client.rs index 7e0c4d1fee..2cc8defe9b 100644 --- a/rust/ethers-prometheus/src/json_rpc_client.rs +++ b/rust/ethers-prometheus/src/json_rpc_client.rs @@ -186,9 +186,11 @@ where } } -impl From> for Box { +impl From> + for JsonRpcBlockGetter> +{ fn from(val: PrometheusJsonRpcClient) -> Self { - Box::new(JsonRpcBlockGetter::new(val)) + JsonRpcBlockGetter::new(val) } } diff --git a/rust/helm/hyperlane-agent/templates/external-secret.yaml b/rust/helm/hyperlane-agent/templates/external-secret.yaml index 5d0eae5ced..36f287eca3 100644 --- a/rust/helm/hyperlane-agent/templates/external-secret.yaml +++ b/rust/helm/hyperlane-agent/templates/external-secret.yaml @@ -29,7 +29,7 @@ spec: {{- if not .disabled }} HYP_CHAINS_{{ .name | upper }}_CUSTOMRPCURLS: {{ printf "'{{ .%s_rpcs | mustFromJson | join \",\" }}'" .name }} {{- if eq .protocol "cosmos" }} - HYP_CHAINS_{{ .name | upper }}_GRPCURL: {{ printf "'{{ .%s_grpc }}'" .name }} + HYP_CHAINS_{{ .name | upper }}_GRPCURLS: {{ printf "'{{ .%s_grpcs | mustFromJson | join \",\" }}'" .name }} {{- end }} {{- end }} {{- end }} @@ -44,9 +44,9 @@ spec: remoteRef: key: {{ printf "%s-rpc-endpoints-%s" $.Values.hyperlane.runEnv .name }} {{- if eq .protocol "cosmos" }} - - secretKey: {{ printf "%s_grpc" .name }} + - secretKey: {{ printf "%s_grpcs" .name }} remoteRef: - key: {{ printf "%s-grpc-endpoint-%s" $.Values.hyperlane.runEnv .name }} + key: {{ printf "%s-grpc-endpoints-%s" $.Values.hyperlane.runEnv .name }} {{- end }} {{- end }} {{- end }} diff --git a/rust/hyperlane-base/src/settings/parser/connection_parser.rs b/rust/hyperlane-base/src/settings/parser/connection_parser.rs index 5d42ce4411..0d47d6eca9 100644 --- a/rust/hyperlane-base/src/settings/parser/connection_parser.rs +++ b/rust/hyperlane-base/src/settings/parser/connection_parser.rs @@ -6,7 +6,7 @@ use url::Url; use crate::settings::envs::*; use crate::settings::ChainConnectionConf; -use super::{parse_cosmos_gas_price, ValueParser}; +use super::{parse_base_and_override_urls, parse_cosmos_gas_price, ValueParser}; pub fn build_ethereum_connection_conf( rpcs: &[Url], @@ -43,19 +43,8 @@ pub fn build_cosmos_connection_conf( err: &mut ConfigParsingError, ) -> Option { let mut local_err = ConfigParsingError::default(); - - let grpc_url = chain - .chain(&mut local_err) - .get_key("grpcUrl") - .parse_string() - .end() - .or_else(|| { - local_err.push( - &chain.cwp + "grpc_url", - eyre!("Missing grpc definitions for chain"), - ); - None - }); + let grpcs = + parse_base_and_override_urls(chain, "grpcUrls", "customGrpcUrls", "http", &mut local_err); let chain_id = chain .chain(&mut local_err) @@ -114,7 +103,7 @@ pub fn build_cosmos_connection_conf( None } else { Some(ChainConnectionConf::Cosmos(h_cosmos::ConnectionConf::new( - grpc_url.unwrap().to_string(), + grpcs, rpcs.first().unwrap().to_string(), chain_id.unwrap().to_string(), prefix.unwrap().to_string(), diff --git a/rust/hyperlane-base/src/settings/parser/mod.rs b/rust/hyperlane-base/src/settings/parser/mod.rs index 76c88fe1b3..3bfb52f91f 100644 --- a/rust/hyperlane-base/src/settings/parser/mod.rs +++ b/rust/hyperlane-base/src/settings/parser/mod.rs @@ -18,6 +18,7 @@ use hyperlane_core::{ use itertools::Itertools; use serde::Deserialize; use serde_json::Value; +use url::Url; pub use self::json_value_parser::ValueParser; pub use super::envs::*; @@ -134,47 +135,7 @@ fn parse_chain( .parse_u32() .unwrap_or(1); - let rpcs_base = chain - .chain(&mut err) - .get_key("rpcUrls") - .into_array_iter() - .map(|urls| { - urls.filter_map(|v| { - v.chain(&mut err) - .get_key("http") - .parse_from_str("Invalid http url") - .end() - }) - .collect_vec() - }) - .unwrap_or_default(); - - let rpc_overrides = chain - .chain(&mut err) - .get_opt_key("customRpcUrls") - .parse_string() - .end() - .map(|urls| { - urls.split(',') - .filter_map(|url| { - url.parse() - .take_err(&mut err, || &chain.cwp + "customRpcUrls") - }) - .collect_vec() - }); - - let rpcs = rpc_overrides.unwrap_or(rpcs_base); - - if rpcs.is_empty() { - err.push( - &chain.cwp + "rpc_urls", - eyre!("Missing base rpc definitions for chain"), - ); - err.push( - &chain.cwp + "custom_rpc_urls", - eyre!("Also missing rpc overrides for chain"), - ); - } + let rpcs = parse_base_and_override_urls(&chain, "rpcUrls", "customRpcUrls", "http", &mut err); let from = chain .chain(&mut err) @@ -418,3 +379,66 @@ fn parse_cosmos_gas_price(gas_price: ValueParser) -> ConfigResult Vec { + chain + .chain(err) + .get_key(key) + .into_array_iter() + .map(|urls| { + urls.filter_map(|v| { + v.chain(err) + .get_key(protocol) + .parse_from_str("Invalid url") + .end() + }) + .collect_vec() + }) + .unwrap_or_default() +} + +fn parse_custom_urls( + chain: &ValueParser, + key: &str, + err: &mut ConfigParsingError, +) -> Option> { + chain + .chain(err) + .get_opt_key(key) + .parse_string() + .end() + .map(|urls| { + urls.split(',') + .filter_map(|url| url.parse().take_err(err, || &chain.cwp + "customGrpcUrls")) + .collect_vec() + }) +} + +fn parse_base_and_override_urls( + chain: &ValueParser, + base_key: &str, + override_key: &str, + protocol: &str, + err: &mut ConfigParsingError, +) -> Vec { + let base = parse_urls(chain, base_key, protocol, err); + let overrides = parse_custom_urls(chain, override_key, err); + let combined = overrides.unwrap_or(base); + + if combined.is_empty() { + err.push( + &chain.cwp + "rpc_urls", + eyre!("Missing base rpc definitions for chain"), + ); + err.push( + &chain.cwp + "custom_rpc_urls", + eyre!("Also missing rpc overrides for chain"), + ); + } + combined +} diff --git a/rust/hyperlane-core/Cargo.toml b/rust/hyperlane-core/Cargo.toml index 8329bce5f2..40468bef6c 100644 --- a/rust/hyperlane-core/Cargo.toml +++ b/rust/hyperlane-core/Cargo.toml @@ -37,6 +37,7 @@ serde_json = { workspace = true } sha3 = { workspace = true } strum = { workspace = true, optional = true, features = ["derive"] } thiserror = { workspace = true } +tokio = { workspace = true, optional = true, features = ["rt", "time"] } tracing.workspace = true primitive-types = { workspace = true, optional = true } solana-sdk = { workspace = true, optional = true } @@ -54,3 +55,4 @@ agent = ["ethers", "strum"] strum = ["dep:strum"] ethers = ["dep:ethers-core", "dep:ethers-contract", "dep:ethers-providers", "dep:primitive-types"] solana = ["dep:solana-sdk"] +fallback-provider = ["tokio"] diff --git a/rust/hyperlane-core/src/rpc_clients/fallback.rs b/rust/hyperlane-core/src/rpc_clients/fallback.rs index 6adcb76f55..6f75cbc4c8 100644 --- a/rust/hyperlane-core/src/rpc_clients/fallback.rs +++ b/rust/hyperlane-core/src/rpc_clients/fallback.rs @@ -1,15 +1,22 @@ use async_rwlock::RwLock; use async_trait::async_trait; use derive_new::new; +use itertools::Itertools; use std::{ - fmt::Debug, + fmt::{Debug, Formatter}, + future::Future, + marker::PhantomData, + pin::Pin, sync::Arc, time::{Duration, Instant}, }; -use tracing::info; +use tokio; +use tracing::{info, trace, warn_span}; use crate::ChainCommunicationError; +use super::RpcClientError; + /// Read the current block number from a chain. #[async_trait] pub trait BlockNumberGetter: Send + Sync + Debug { @@ -38,7 +45,6 @@ impl PrioritizedProviderInner { } } } - /// Sub-providers and priority information pub struct PrioritizedProviders { /// Unsorted list of providers this provider calls @@ -49,28 +55,56 @@ pub struct PrioritizedProviders { /// A provider that bundles multiple providers and attempts to call the first, /// then the second, and so on until a response is received. -pub struct FallbackProvider { +/// +/// Although no trait bounds are used in the struct definition, the intended purpose of `B` +/// is to be bound by `BlockNumberGetter` and have `T` be convertible to `B`. That is, +/// inner providers should be able to get the current block number, or be convertible into +/// something that is. +pub struct FallbackProvider { /// The sub-providers called by this provider pub inner: Arc>, max_block_time: Duration, + _phantom: PhantomData, } -impl Clone for FallbackProvider { +impl Clone for FallbackProvider { fn clone(&self) -> Self { Self { inner: self.inner.clone(), max_block_time: self.max_block_time, + _phantom: PhantomData, } } } -impl FallbackProvider +impl Debug for FallbackProvider +where + T: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + // iterate the inner providers and write them to the formatter + f.debug_struct("FallbackProvider") + .field( + "providers", + &self + .inner + .providers + .iter() + .map(|v| format!("{:?}", v)) + .join(", "), + ) + .finish() + } +} + +impl FallbackProvider where - T: Into> + Debug + Clone, + T: Into + Debug + Clone, + B: BlockNumberGetter, { /// Convenience method for creating a `FallbackProviderBuilder` with same /// `JsonRpcClient` types - pub fn builder() -> FallbackProviderBuilder { + pub fn builder() -> FallbackProviderBuilder { FallbackProviderBuilder::default() } @@ -112,7 +146,7 @@ where return; } - let block_getter: Box = provider.clone().into(); + let block_getter: B = provider.clone().into(); let current_block_height = block_getter .get_block_number() .await @@ -130,25 +164,62 @@ where .await; } } + + /// Call the first provider, then the second, and so on (in order of priority) until a response is received. + /// If all providers fail, return an error. + pub async fn call( + &self, + mut f: impl FnMut(T) -> Pin> + Send>>, + ) -> Result { + let mut errors = vec![]; + // make sure we do at least 4 total retries. + while errors.len() <= 3 { + if !errors.is_empty() { + tokio::time::sleep(Duration::from_millis(100)).await; + } + let priorities_snapshot = self.take_priorities_snapshot().await; + for (idx, priority) in priorities_snapshot.iter().enumerate() { + let provider = &self.inner.providers[priority.index]; + let resp = f(provider.clone()).await; + self.handle_stalled_provider(priority, provider).await; + let _span = + warn_span!("FallbackProvider::call", fallback_count=%idx, provider_index=%priority.index, ?provider).entered(); + match resp { + Ok(v) => return Ok(v), + Err(e) => { + trace!( + error=?e, + "Got error from inner fallback provider", + ); + errors.push(e) + } + } + } + } + + Err(RpcClientError::FallbackProvidersFailed(errors).into()) + } } /// Builder to create a new fallback provider. #[derive(Debug, Clone)] -pub struct FallbackProviderBuilder { +pub struct FallbackProviderBuilder { providers: Vec, max_block_time: Duration, + _phantom: PhantomData, } -impl Default for FallbackProviderBuilder { +impl Default for FallbackProviderBuilder { fn default() -> Self { Self { providers: Vec::new(), max_block_time: MAX_BLOCK_TIME, + _phantom: PhantomData, } } } -impl FallbackProviderBuilder { +impl FallbackProviderBuilder { /// Add a new provider to the set. Each new provider will be a lower /// priority than the previous. pub fn add_provider(mut self, provider: T) -> Self { @@ -170,7 +241,7 @@ impl FallbackProviderBuilder { } /// Create a fallback provider. - pub fn build(self) -> FallbackProvider { + pub fn build(self) -> FallbackProvider { let provider_count = self.providers.len(); let prioritized_providers = PrioritizedProviders { providers: self.providers, @@ -184,6 +255,80 @@ impl FallbackProviderBuilder { FallbackProvider { inner: Arc::new(prioritized_providers), max_block_time: self.max_block_time, + _phantom: PhantomData, + } + } +} + +/// Utilities to import when testing chain-specific fallback providers +pub mod test { + use super::*; + use std::{ + ops::Deref, + sync::{Arc, Mutex}, + }; + + /// Provider that stores requests and optionally sleeps before returning a dummy value + #[derive(Debug, Clone)] + pub struct ProviderMock { + // Store requests as tuples of (method, params) + // Even if the tests were single-threaded, need the arc-mutex + // for interior mutability in `JsonRpcClient::request` + requests: Arc>>, + request_sleep: Option, + } + + impl Default for ProviderMock { + fn default() -> Self { + Self { + requests: Arc::new(Mutex::new(vec![])), + request_sleep: None, + } + } + } + + impl ProviderMock { + /// Create a new provider + pub fn new(request_sleep: Option) -> Self { + Self { + request_sleep, + ..Default::default() + } + } + + /// Push a request to the internal store for later inspection + pub fn push(&self, method: &str, params: T) { + self.requests + .lock() + .unwrap() + .push((method.to_owned(), format!("{:?}", params))); + } + + /// Get the stored requests + pub fn requests(&self) -> Vec<(String, String)> { + self.requests.lock().unwrap().clone() + } + + /// Set the sleep duration + pub fn request_sleep(&self) -> Option { + self.request_sleep + } + + /// Get how many times each provider was called + pub async fn get_call_counts, B>( + fallback_provider: &FallbackProvider, + ) -> Vec { + fallback_provider + .inner + .priorities + .read() + .await + .iter() + .map(|p| { + let provider = &fallback_provider.inner.providers[p.index]; + provider.requests().len() + }) + .collect() } } } diff --git a/rust/hyperlane-core/src/rpc_clients/mod.rs b/rust/hyperlane-core/src/rpc_clients/mod.rs index 78851f9f26..02aaae99f5 100644 --- a/rust/hyperlane-core/src/rpc_clients/mod.rs +++ b/rust/hyperlane-core/src/rpc_clients/mod.rs @@ -1,4 +1,8 @@ -pub use self::{error::*, fallback::*}; +pub use self::error::*; + +#[cfg(feature = "fallback-provider")] +pub use self::fallback::*; mod error; +#[cfg(feature = "fallback-provider")] mod fallback; diff --git a/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/program-ids.json b/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/program-ids.json index ba62748efe..c5e945eae3 100644 --- a/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/program-ids.json +++ b/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/program-ids.json @@ -1,10 +1,10 @@ { - "sealeveltest1": { - "hex": "0xa77b4e2ed231894cc8cb8eee21adcc705d8489bccc6b2fcf40a358de23e60b7b", - "base58": "CGn8yNtSD3aTTqJfYhUb6s1aVTN75NzwtsFKo1e83aga" - }, "sealeveltest2": { "hex": "0x2317f9615d4ebc2419ad4b88580e2a80a03b2c7a60bc960de7d6934dbc37a87e", "base58": "3MzUPjP5LEkiHH82nEAe28Xtz9ztuMqWc8UmuKxrpVQH" + }, + "sealeveltest1": { + "hex": "0xa77b4e2ed231894cc8cb8eee21adcc705d8489bccc6b2fcf40a358de23e60b7b", + "base58": "CGn8yNtSD3aTTqJfYhUb6s1aVTN75NzwtsFKo1e83aga" } } \ No newline at end of file diff --git a/rust/utils/abigen/src/lib.rs b/rust/utils/abigen/src/lib.rs index 2e8d5ca081..b4b7970fdc 100644 --- a/rust/utils/abigen/src/lib.rs +++ b/rust/utils/abigen/src/lib.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "fuels")] use fuels_code_gen::ProgramType; use std::collections::BTreeSet; use std::ffi::OsStr; diff --git a/rust/utils/run-locally/Cargo.toml b/rust/utils/run-locally/Cargo.toml index 03d771734e..bf57138f4e 100644 --- a/rust/utils/run-locally/Cargo.toml +++ b/rust/utils/run-locally/Cargo.toml @@ -29,7 +29,7 @@ ureq = { workspace = true, default-features = false } which.workspace = true macro_rules_attribute.workspace = true regex.workspace = true -hpl-interface.workspace = true +hyperlane-cosmwasm-interface.workspace = true cosmwasm-schema.workspace = true [features] diff --git a/rust/utils/run-locally/src/cosmos/crypto.rs b/rust/utils/run-locally/src/cosmos/crypto.rs index 9b336f4df7..75924df69a 100644 --- a/rust/utils/run-locally/src/cosmos/crypto.rs +++ b/rust/utils/run-locally/src/cosmos/crypto.rs @@ -24,7 +24,7 @@ pub fn pub_to_addr(pub_key: &[u8], prefix: &str) -> String { let sha_hash = sha256_digest(pub_key); let rip_hash = ripemd160_digest(sha_hash); - let addr = hpl_interface::types::bech32_encode(prefix, &rip_hash).unwrap(); + let addr = hyperlane_cosmwasm_interface::types::bech32_encode(prefix, &rip_hash).unwrap(); addr.to_string() } diff --git a/rust/utils/run-locally/src/cosmos/deploy.rs b/rust/utils/run-locally/src/cosmos/deploy.rs index ab02a2bee4..4da016d865 100644 --- a/rust/utils/run-locally/src/cosmos/deploy.rs +++ b/rust/utils/run-locally/src/cosmos/deploy.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use hpl_interface::{core, hook, igp, ism}; +use hyperlane_cosmwasm_interface::{core, hook, igp, ism}; use macro_rules_attribute::apply; use crate::utils::as_task; @@ -9,19 +9,18 @@ use super::{ types::{Codes, Deployments}, }; -#[cw_serde] -pub struct IsmMultisigInstantiateMsg { - pub owner: String, -} - #[cw_serde] pub struct TestMockMsgReceiverInstantiateMsg { pub hrp: String, } #[cw_serde] -pub struct IGPOracleInstantiateMsg { +struct IgpInstantiateMsg { + pub hrp: String, pub owner: String, + pub gas_token: String, + pub beneficiary: String, + pub default_gas_usage: String, // u128 doesnt work with cw_serde } #[cw_serde] @@ -52,22 +51,12 @@ pub fn deploy_cw_hyperlane( "hpl_mailbox", ); - // deploy igp set - #[cw_serde] - pub struct GasOracleInitMsg { - pub hrp: String, - pub owner: String, - pub gas_token: String, - pub beneficiary: String, - pub default_gas_usage: String, - } - let igp = cli.wasm_init( &endpoint, &deployer, Some(deployer_addr), codes.hpl_igp, - GasOracleInitMsg { + IgpInstantiateMsg { hrp: BECH32_PREFIX.to_string(), owner: deployer_addr.clone(), gas_token: "uosmo".to_string(), @@ -107,12 +96,25 @@ pub fn deploy_cw_hyperlane( &deployer, Some(deployer_addr), codes.hpl_ism_multisig, - IsmMultisigInstantiateMsg { + ism::multisig::InstantiateMsg { owner: deployer_addr.clone(), }, "hpl_ism_multisig", ); + // deploy pausable ism + let ism_pausable = cli.wasm_init( + &endpoint, + &deployer, + Some(deployer_addr), + codes.hpl_ism_pausable, + ism::pausable::InstantiateMsg { + owner: deployer_addr.clone(), + paused: false, + }, + "hpl_ism_pausable", + ); + // deploy ism - aggregation let ism_aggregate = cli.wasm_init( &endpoint, @@ -121,8 +123,8 @@ pub fn deploy_cw_hyperlane( codes.hpl_ism_aggregate, ism::aggregate::InstantiateMsg { owner: deployer_addr.clone(), - threshold: 1, - isms: vec![ism_multisig.clone()], + threshold: 2, + isms: vec![ism_multisig.clone(), ism_pausable.clone()], }, "hpl_ism_aggregate", ); @@ -134,7 +136,6 @@ pub fn deploy_cw_hyperlane( Some(deployer_addr), codes.hpl_hook_merkle, hook::merkle::InstantiateMsg { - owner: deployer_addr.clone(), mailbox: mailbox.to_string(), }, "hpl_hook_merkle", diff --git a/rust/utils/run-locally/src/cosmos/link.rs b/rust/utils/run-locally/src/cosmos/link.rs index ff3de059fa..ebf53bd36c 100644 --- a/rust/utils/run-locally/src/cosmos/link.rs +++ b/rust/utils/run-locally/src/cosmos/link.rs @@ -1,7 +1,7 @@ use std::path::Path; use cosmwasm_schema::cw_serde; -use hpl_interface::{ +use hyperlane_cosmwasm_interface::{ core, ism::{self}, }; @@ -69,18 +69,14 @@ pub struct MockQuoteDispatch { #[cw_serde] pub struct GeneralIsmValidatorMessage { - pub enroll_validator: EnrollValidatorMsg, + pub set_validators: SetValidatorsMsg, } #[cw_serde] -pub struct EnrollValidatorMsg { - pub set: EnrollValidatorSet, -} - -#[cw_serde] -pub struct EnrollValidatorSet { +pub struct SetValidatorsMsg { pub domain: u32, - pub validator: String, + pub threshold: u8, + pub validators: Vec, } fn link_network( @@ -105,7 +101,7 @@ fn link_network( let public_key = validator.priv_key.verifying_key().to_encoded_point(false); let public_key = public_key.as_bytes(); - let hash = hpl_interface::types::keccak256_hash(&public_key[1..]); + let hash = hyperlane_cosmwasm_interface::types::keccak256_hash(&public_key[1..]); let mut bytes = [0u8; 20]; bytes.copy_from_slice(&hash.as_slice()[12..]); @@ -115,24 +111,10 @@ fn link_network( linker, &network.deployments.ism_multisig, GeneralIsmValidatorMessage { - enroll_validator: EnrollValidatorMsg { - set: EnrollValidatorSet { - domain: target_domain, - validator: hex::encode(bytes).to_string(), - }, - }, - }, - vec![], - ); - - cli.wasm_execute( - &network.launch_resp.endpoint, - linker, - &network.deployments.ism_multisig, - ism::multisig::ExecuteMsg::SetThreshold { - set: ism::multisig::ThresholdSet { - domain: target_domain, + set_validators: SetValidatorsMsg { threshold: 1, + domain: target_domain, + validators: vec![hex::encode(bytes).to_string()], }, }, vec![], diff --git a/rust/utils/run-locally/src/cosmos/mod.rs b/rust/utils/run-locally/src/cosmos/mod.rs index a61df94166..b1b2b4dd47 100644 --- a/rust/utils/run-locally/src/cosmos/mod.rs +++ b/rust/utils/run-locally/src/cosmos/mod.rs @@ -5,8 +5,8 @@ use std::time::{Duration, Instant}; use std::{env, fs}; use cosmwasm_schema::cw_serde; -use hpl_interface::types::bech32_decode; use hyperlane_cosmos::RawCosmosAmount; +use hyperlane_cosmwasm_interface::types::bech32_decode; use macro_rules_attribute::apply; use maplit::hashmap; use tempfile::tempdir; @@ -57,8 +57,8 @@ fn default_keys<'a>() -> [(&'a str, &'a str); 6] { ] } -const CW_HYPERLANE_GIT: &str = "https://github.com/many-things/cw-hyperlane"; -const CW_HYPERLANE_VERSION: &str = "0.0.6-rc6"; +const CW_HYPERLANE_GIT: &str = "https://github.com/hyperlane-xyz/cosmwasm"; +const CW_HYPERLANE_VERSION: &str = "v0.0.6"; fn make_target() -> String { let os = if cfg!(target_os = "linux") { @@ -101,19 +101,22 @@ pub fn install_codes(dir: Option, local: bool) -> BTreeMap path map fs::read_dir(dir_path) diff --git a/rust/utils/run-locally/src/cosmos/types.rs b/rust/utils/run-locally/src/cosmos/types.rs index 795d12ff39..120ed05afd 100644 --- a/rust/utils/run-locally/src/cosmos/types.rs +++ b/rust/utils/run-locally/src/cosmos/types.rs @@ -1,7 +1,7 @@ use std::{collections::BTreeMap, path::PathBuf}; -use hpl_interface::types::bech32_decode; use hyperlane_cosmos::RawCosmosAmount; +use hyperlane_cosmwasm_interface::types::bech32_decode; use super::{cli::OsmosisCLI, CosmosNetwork}; @@ -44,6 +44,7 @@ pub struct Codes { pub hpl_igp_oracle: u64, pub hpl_ism_aggregate: u64, pub hpl_ism_multisig: u64, + pub hpl_ism_pausable: u64, pub hpl_ism_routing: u64, pub hpl_test_mock_ism: u64, pub hpl_test_mock_hook: u64, @@ -118,7 +119,7 @@ pub struct AgentConfig { pub protocol: String, pub chain_id: String, pub rpc_urls: Vec, - pub grpc_url: String, + pub grpc_urls: Vec, pub bech32_prefix: String, pub signer: AgentConfigSigner, pub index: AgentConfigIndex, @@ -156,7 +157,15 @@ impl AgentConfig { network.launch_resp.endpoint.rpc_addr.replace("tcp://", "") ), }], - grpc_url: format!("http://{}", network.launch_resp.endpoint.grpc_addr), + grpc_urls: vec![ + // The first url points to a nonexistent node, but is used for checking fallback provider logic + AgentUrl { + http: "localhost:1337".to_string(), + }, + AgentUrl { + http: format!("http://{}", network.launch_resp.endpoint.grpc_addr), + }, + ], bech32_prefix: "osmo".to_string(), signer: AgentConfigSigner { typ: "cosmosKey".to_string(), diff --git a/solidity/CHANGELOG.md b/solidity/CHANGELOG.md index cf7b6a84e6..fa903faa7c 100644 --- a/solidity/CHANGELOG.md +++ b/solidity/CHANGELOG.md @@ -1,5 +1,11 @@ # @hyperlane-xyz/core +## 3.6.2 + +### Patch Changes + +- @hyperlane-xyz/utils@3.6.2 + ## 3.6.1 ### Patch Changes diff --git a/solidity/package.json b/solidity/package.json index 102273e354..f3a6bb2b2d 100644 --- a/solidity/package.json +++ b/solidity/package.json @@ -1,10 +1,10 @@ { "name": "@hyperlane-xyz/core", "description": "Core solidity contracts for Hyperlane", - "version": "3.6.1", + "version": "3.6.2", "dependencies": { "@eth-optimism/contracts": "^0.6.0", - "@hyperlane-xyz/utils": "3.6.1", + "@hyperlane-xyz/utils": "3.6.2", "@openzeppelin/contracts": "^4.9.3", "@openzeppelin/contracts-upgradeable": "^v4.9.3" }, diff --git a/typescript/cli/CHANGELOG.md b/typescript/cli/CHANGELOG.md index d85ed3175d..ec9579f252 100644 --- a/typescript/cli/CHANGELOG.md +++ b/typescript/cli/CHANGELOG.md @@ -1,5 +1,13 @@ # @hyperlane-xyz/cli +## 3.6.2 + +### Patch Changes + +- 99fe93a5b: Removed IGP from preset hook config + - @hyperlane-xyz/sdk@3.6.2 + - @hyperlane-xyz/utils@3.6.2 + ## 3.6.1 ### Patch Changes diff --git a/typescript/cli/package.json b/typescript/cli/package.json index e2b5c9bda0..3e366d04c8 100644 --- a/typescript/cli/package.json +++ b/typescript/cli/package.json @@ -1,10 +1,10 @@ { "name": "@hyperlane-xyz/cli", - "version": "3.6.1", + "version": "3.6.2", "description": "A command-line utility for common Hyperlane operations", "dependencies": { - "@hyperlane-xyz/sdk": "3.6.1", - "@hyperlane-xyz/utils": "3.6.1", + "@hyperlane-xyz/sdk": "3.6.2", + "@hyperlane-xyz/utils": "3.6.2", "@inquirer/prompts": "^3.0.0", "bignumber.js": "^9.1.1", "chalk": "^5.3.0", diff --git a/typescript/cli/src/config/hooks.ts b/typescript/cli/src/config/hooks.ts index 6392d60f95..0c0f49111c 100644 --- a/typescript/cli/src/config/hooks.ts +++ b/typescript/cli/src/config/hooks.ts @@ -9,10 +9,7 @@ import { GasOracleContractType, HookType, HooksConfig, - MultisigConfig, chainMetadata, - defaultMultisigConfigs, - multisigIsmVerificationCost, } from '@hyperlane-xyz/sdk'; import { Address, @@ -83,41 +80,7 @@ export function isValidHookConfigMap(config: any) { return HooksConfigMapSchema.safeParse(config).success; } -export function presetHookConfigs( - owner: Address, - local: ChainName, - destinationChains: ChainName[], - multisigConfig?: MultisigConfig, -): HooksConfig { - const gasOracleType = destinationChains.reduce< - ChainMap - >((acc, chain) => { - acc[chain] = GasOracleContractType.StorageGasOracle; - return acc; - }, {}); - const overhead = destinationChains.reduce>((acc, chain) => { - let validatorThreshold: number; - let validatorCount: number; - if (multisigConfig) { - validatorThreshold = multisigConfig.threshold; - validatorCount = multisigConfig.validators.length; - } else if (local in defaultMultisigConfigs) { - validatorThreshold = defaultMultisigConfigs[local].threshold; - validatorCount = defaultMultisigConfigs[local].validators.length; - } else { - // default values - // fix here: https://github.com/hyperlane-xyz/issues/issues/773 - validatorThreshold = 2; - validatorCount = 3; - } - acc[chain] = multisigIsmVerificationCost( - validatorThreshold, - validatorCount, - ); - return acc; - }, {}); - - // TODO improve types here to avoid need for `as` casts +export function presetHookConfigs(owner: Address): HooksConfig { return { required: { type: HookType.PROTOCOL_FEE, @@ -127,20 +90,7 @@ export function presetHookConfigs( owner: owner, }, default: { - type: HookType.AGGREGATION, - hooks: [ - { - type: HookType.MERKLE_TREE, - }, - { - type: HookType.INTERCHAIN_GAS_PAYMASTER, - owner: owner, - beneficiary: owner, - gasOracleType, - overhead, - oracleKey: owner, - }, - ], + type: HookType.MERKLE_TREE, }, }; } diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index 5f9537e132..14617f9b83 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -328,7 +328,6 @@ async function executeDeploy({ chains, defaultIsms, hooksConfig, - multisigConfigs, ); const coreContracts = await coreDeployer.deploy(coreConfigs); @@ -391,17 +390,9 @@ function buildCoreConfigMap( chains: ChainName[], defaultIsms: ChainMap, hooksConfig: ChainMap, - multisigConfigs: ChainMap, ): ChainMap { return chains.reduce>((config, chain) => { - const hooks = - hooksConfig[chain] ?? - presetHookConfigs( - owner, - chain, - chains.filter((c) => c !== chain), - multisigConfigs[chain], // if no multisig config, uses default 2/3 - ); + const hooks = hooksConfig[chain] ?? presetHookConfigs(owner); config[chain] = { owner, defaultIsm: defaultIsms[chain], @@ -481,18 +472,20 @@ async function writeAgentConfig( multiProvider: MultiProvider, ) { const startBlocks: ChainMap = {}; + const core = HyperlaneCore.fromAddressesMap(artifacts, multiProvider); + for (const chain of chains) { - const core = HyperlaneCore.fromAddressesMap(artifacts, multiProvider); const mailbox = core.getContracts(chain).mailbox; startBlocks[chain] = (await mailbox.deployedBlock()).toNumber(); } + const mergedAddressesMap = objMerge( sdkContractAddressesMap, artifacts, ) as ChainMap; const agentConfig = buildAgentConfig( - Object.keys(mergedAddressesMap), + chains, // Use only the chains that were deployed to multiProvider, mergedAddressesMap, startBlocks, diff --git a/typescript/cli/src/version.ts b/typescript/cli/src/version.ts index 86c2700664..658ea336e5 100644 --- a/typescript/cli/src/version.ts +++ b/typescript/cli/src/version.ts @@ -1 +1 @@ -export const VERSION = '3.6.1'; +export const VERSION = '3.6.2'; diff --git a/typescript/helloworld/CHANGELOG.md b/typescript/helloworld/CHANGELOG.md index 992eedca50..40cff8da4b 100644 --- a/typescript/helloworld/CHANGELOG.md +++ b/typescript/helloworld/CHANGELOG.md @@ -1,5 +1,12 @@ # @hyperlane-xyz/helloworld +## 3.6.2 + +### Patch Changes + +- @hyperlane-xyz/core@3.6.2 +- @hyperlane-xyz/sdk@3.6.2 + ## 3.6.1 ### Patch Changes diff --git a/typescript/helloworld/package.json b/typescript/helloworld/package.json index 773687366c..ba7ca9c7fc 100644 --- a/typescript/helloworld/package.json +++ b/typescript/helloworld/package.json @@ -1,10 +1,10 @@ { "name": "@hyperlane-xyz/helloworld", "description": "A basic skeleton of an Hyperlane app", - "version": "3.6.1", + "version": "3.6.2", "dependencies": { - "@hyperlane-xyz/core": "3.6.1", - "@hyperlane-xyz/sdk": "3.6.1", + "@hyperlane-xyz/core": "3.6.2", + "@hyperlane-xyz/sdk": "3.6.2", "@openzeppelin/contracts-upgradeable": "^4.9.3", "ethers": "^5.7.2" }, diff --git a/typescript/infra/CHANGELOG.md b/typescript/infra/CHANGELOG.md index 7d1fb96962..7e7210dca9 100644 --- a/typescript/infra/CHANGELOG.md +++ b/typescript/infra/CHANGELOG.md @@ -1,5 +1,13 @@ # @hyperlane-xyz/infra +## 3.6.2 + +### Patch Changes + +- @hyperlane-xyz/helloworld@3.6.2 +- @hyperlane-xyz/sdk@3.6.2 +- @hyperlane-xyz/utils@3.6.2 + ## 3.6.1 ### Patch Changes diff --git a/typescript/infra/config/environments/mainnet3/chains.ts b/typescript/infra/config/environments/mainnet3/chains.ts index 2cd00593f8..b919d1a7fe 100644 --- a/typescript/infra/config/environments/mainnet3/chains.ts +++ b/typescript/infra/config/environments/mainnet3/chains.ts @@ -50,13 +50,14 @@ export const ethereumMainnetConfigs: ChainMap = { // Blessed non-Ethereum chains. export const nonEthereumMainnetConfigs: ChainMap = { - solana: chainMetadata.solana, - neutron: chainMetadata.neutron, + // solana: chainMetadata.solana, + // neutron: chainMetadata.neutron, + injective: chainMetadata.injective, }; export const mainnetConfigs: ChainMap = { ...ethereumMainnetConfigs, - // ...nonEthereumMainnetConfigs, + ...nonEthereumMainnetConfigs, }; export type MainnetChains = keyof typeof mainnetConfigs; diff --git a/typescript/infra/config/environments/mainnet3/core/verification.json b/typescript/infra/config/environments/mainnet3/core/verification.json index 7b27ed5bc8..c559233f17 100644 --- a/typescript/infra/config/environments/mainnet3/core/verification.json +++ b/typescript/infra/config/environments/mainnet3/core/verification.json @@ -2048,5 +2048,97 @@ "constructorArguments": "0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a7", "isProxy": false } + ], + "inevm": [ + { + "name": "ProxyAdmin", + "address": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "Mailbox", + "address": "0x4Ed7d626f1E96cD1C0401607Bf70D95243E3dEd1", + "constructorArguments": "00000000000000000000000000000000000000000000000000000000000009dd", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "constructorArguments": "0000000000000000000000004ed7d626f1e96cd1c0401607bf70d95243e3ded10000000000000000000000000761b0827849abbf7b0cc09ce14e1c93d87f500400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "isProxy": true + }, + { + "name": "MerkleTreeHook", + "address": "0x0972954923a1e2b2aAb04Fa0c4a0797e5989Cd65", + "constructorArguments": "0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a7", + "isProxy": false + }, + { + "name": "StorageGasOracle", + "address": "0x6119E37Bd66406A1Db74920aC79C15fB8411Ba76", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "InterchainGasPaymaster", + "address": "0x481171eb1aad17eDE6a56005B7F1aB00C581ef13", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x19dc38aeae620380430C200a6E990D5Af5480117", + "constructorArguments": "000000000000000000000000481171eb1aad17ede6a56005b7f1ab00c581ef130000000000000000000000000761b0827849abbf7b0cc09ce14e1c93d87f500400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044485cc955000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true + }, + { + "name": "MerkleTreeHook", + "address": "0x0972954923a1e2b2aAb04Fa0c4a0797e5989Cd65", + "constructorArguments": "0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a7", + "isProxy": false + }, + { + "name": "StorageGasOracle", + "address": "0x6119E37Bd66406A1Db74920aC79C15fB8411Ba76", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "InterchainGasPaymaster", + "address": "0x481171eb1aad17eDE6a56005B7F1aB00C581ef13", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x19dc38aeae620380430C200a6E990D5Af5480117", + "constructorArguments": "000000000000000000000000481171eb1aad17ede6a56005b7f1ab00c581ef130000000000000000000000000761b0827849abbf7b0cc09ce14e1c93d87f500400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044485cc955000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true + }, + { + "name": "ProtocolFee", + "address": "0x0D63128D887159d63De29497dfa45AFc7C699AE4", + "constructorArguments": "000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba", + "isProxy": false + }, + { + "name": "ValidatorAnnounce", + "address": "0x15ab173bDB6832f9b64276bA128659b0eD77730B", + "constructorArguments": "0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a7", + "isProxy": false + }, + { + "name": "PausableHook", + "address": "0xBDa330Ea8F3005C421C8088e638fBB64fA71b9e0", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "PausableHook", + "address": "0xBDa330Ea8F3005C421C8088e638fBB64fA71b9e0", + "constructorArguments": "", + "isProxy": false + } ] } diff --git a/typescript/infra/config/environments/mainnet3/gas-oracle.ts b/typescript/infra/config/environments/mainnet3/gas-oracle.ts index fe5c40ebb6..0775a7abc1 100644 --- a/typescript/infra/config/environments/mainnet3/gas-oracle.ts +++ b/typescript/infra/config/environments/mainnet3/gas-oracle.ts @@ -50,6 +50,8 @@ const gasPrices: ChainMap = { polygonzkevm: ethers.utils.parseUnits('2', 'gwei'), neutron: ethers.utils.parseUnits('1', 'gwei'), mantapacific: ethers.utils.parseUnits('1', 'gwei'), + inevm: ethers.utils.parseUnits('1', 'gwei'), + injective: ethers.utils.parseUnits('1', 'gwei'), viction: ethers.utils.parseUnits('0.25', 'gwei'), }; @@ -93,6 +95,9 @@ const tokenUsdPrices: ChainMap = { '1619.00', TOKEN_EXCHANGE_RATE_DECIMALS, ), + // https://www.coingecko.com/en/coins/injective + injective: ethers.utils.parseUnits('32.78', TOKEN_EXCHANGE_RATE_DECIMALS), + inevm: ethers.utils.parseUnits('32.78', TOKEN_EXCHANGE_RATE_DECIMALS), // 1:1 injective // https://www.coingecko.com/en/coins/viction viction: ethers.utils.parseUnits('0.881', TOKEN_EXCHANGE_RATE_DECIMALS), }; diff --git a/typescript/infra/config/environments/mainnet3/igp.ts b/typescript/infra/config/environments/mainnet3/igp.ts index 19a894d136..d5e999abfa 100644 --- a/typescript/infra/config/environments/mainnet3/igp.ts +++ b/typescript/infra/config/environments/mainnet3/igp.ts @@ -18,6 +18,8 @@ import { owners } from './owners'; const KEY_FUNDER_ADDRESS = '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba'; const DEPLOYER_ADDRESS = '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba'; +const FOREIGN_DEFAULT_OVERHEAD = 600_000; // cosmwasm warp route somewhat arbitrarily chosen + function getGasOracles(local: MainnetChains) { return Object.fromEntries( exclude(local, supportedChainNames).map((name) => [ @@ -27,19 +29,23 @@ function getGasOracles(local: MainnetChains) { ); } -export const igp: ChainMap = objMap(owners, (chain, owner) => ({ +const remoteOverhead = (remote: MainnetChains) => + ethereumChainNames.includes(remote) + ? multisigIsmVerificationCost( + defaultMultisigConfigs[remote].threshold, + defaultMultisigConfigs[remote].validators.length, + ) + : FOREIGN_DEFAULT_OVERHEAD; // non-ethereum overhead + +export const igp: ChainMap = objMap(owners, (local, owner) => ({ ...owner, oracleKey: DEPLOYER_ADDRESS, beneficiary: KEY_FUNDER_ADDRESS, - gasOracleType: getGasOracles(chain), + gasOracleType: getGasOracles(local), overhead: Object.fromEntries( - // Not setting overhead for non-Ethereum destination chains - exclude(chain, ethereumChainNames).map((remote) => [ + exclude(local, supportedChainNames).map((remote) => [ remote, - multisigIsmVerificationCost( - defaultMultisigConfigs[remote].threshold, - defaultMultisigConfigs[remote].validators.length, - ), + remoteOverhead(remote), ]), ), })); diff --git a/typescript/infra/config/environments/mainnet3/ism/verification.json b/typescript/infra/config/environments/mainnet3/ism/verification.json index e3941bf803..1b8f42b360 100644 --- a/typescript/infra/config/environments/mainnet3/ism/verification.json +++ b/typescript/infra/config/environments/mainnet3/ism/verification.json @@ -2244,5 +2244,67 @@ "constructorArguments": "", "isProxy": true } + ], + "inevm": [ + { + "name": "MerkleRootMultisigIsmFactory", + "address": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMerkleRootMultisigIsm", + "address": "0x4725F7b8037513915aAf6D6CBDE2920E28540dDc", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "MessageIdMultisigIsmFactory", + "address": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMessageIdMultisigIsm", + "address": "0xAF03386044373E2fe26C5b1dCedF5a7e854a7a3F", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "AggregationIsmFactory", + "address": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationIsm", + "address": "0x882CD0C5D50b6dD74b36Da4BDb059507fddEDdf2", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "AggregationHookFactory", + "address": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationHook", + "address": "0x19930232E9aFC4f4F09d09fe2375680fAc2100D0", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "RoutingIsmFactory", + "address": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "DomaingRoutingIsm", + "address": "0x12Ed1BbA182CbC63692F813651BD493B7445C874", + "constructorArguments": "", + "isProxy": true + } ] } diff --git a/typescript/infra/config/environments/mainnet3/owners.ts b/typescript/infra/config/environments/mainnet3/owners.ts index b01cb0ed7e..9794189456 100644 --- a/typescript/infra/config/environments/mainnet3/owners.ts +++ b/typescript/infra/config/environments/mainnet3/owners.ts @@ -1,5 +1,7 @@ import { ChainMap, OwnableConfig } from '@hyperlane-xyz/sdk'; -import { Address, objMap } from '@hyperlane-xyz/utils'; +import { Address } from '@hyperlane-xyz/utils'; + +import { ethereumChainNames } from './chains'; export const timelocks: ChainMap
= { arbitrum: '0xAC98b0cD1B64EA4fe133C6D2EDaf842cE5cF4b01', @@ -16,19 +18,18 @@ export const safes: ChainMap
= { moonbeam: '0xF0cb1f968Df01fc789762fddBfA704AE0F952197', gnosis: '0x36b0AA0e7d04e7b825D7E409FEa3c9A3d57E4C22', // solana: 'EzppBFV2taxWw8kEjxNYvby6q7W1biJEqwP3iC7YgRe3', - // TODO: create gnosis safes here - base: undefined, - scroll: undefined, - polygonzkevm: undefined, - mantapacific: undefined, - viction: undefined, }; const deployer = '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba'; -export const owners: ChainMap = objMap(safes, (local, __) => ({ - owner: deployer, // TODO: change this to the safe - ownerOverrides: { - proxyAdmin: timelocks[local] ?? safes[local] ?? deployer, - }, -})); +export const owners: ChainMap = Object.fromEntries( + ethereumChainNames.map((local) => [ + local, + { + owner: deployer, // TODO: change this to the safe + ownerOverrides: { + proxyAdmin: timelocks[local] ?? safes[local] ?? deployer, + }, + }, + ]), +); diff --git a/typescript/infra/config/environments/mainnet3/validators.ts b/typescript/infra/config/environments/mainnet3/validators.ts index e7fe44e984..273e3ae412 100644 --- a/typescript/infra/config/environments/mainnet3/validators.ts +++ b/typescript/infra/config/environments/mainnet3/validators.ts @@ -175,6 +175,38 @@ export const validatorChainConfig = ( 'base', ), }, + injective: { + interval: 5, + reorgPeriod: getReorgPeriod(chainMetadata.injective), + validators: validatorsConfig( + { + [Contexts.Hyperlane]: [ + '0xbfb8911b72cfb138c7ce517c57d9c691535dc517', + '0x6faa139c33a7e6f53cb101f6b2ae392298283ed2', + '0x0115e3a66820fb99da30d30e2ce52a453ba99d92', + ], + [Contexts.ReleaseCandidate]: [], + [Contexts.Neutron]: [], + }, + 'injective', + ), + }, + inevm: { + interval: 5, + reorgPeriod: getReorgPeriod(chainMetadata.inevm), + validators: validatorsConfig( + { + [Contexts.Hyperlane]: [ + '0xf9e35ee88e4448a3673b4676a4e153e3584a08eb', + '0xae3e6bb6b3ece1c425aa6f47adc8cb0453c1f9a2', + '0xd98c9522cd9d3e3e00bee05ff76c34b91b266ec3', + ], + [Contexts.ReleaseCandidate]: [], + [Contexts.Neutron]: [], + }, + 'inevm', + ), + }, scroll: { interval: 5, reorgPeriod: getReorgPeriod(chainMetadata.scroll), diff --git a/typescript/infra/config/environments/mainnet3/warp/addresses.json b/typescript/infra/config/environments/mainnet3/warp/addresses.json index 79b01185bb..a84f3a7094 100644 --- a/typescript/infra/config/environments/mainnet3/warp/addresses.json +++ b/typescript/infra/config/environments/mainnet3/warp/addresses.json @@ -1,5 +1,5 @@ { - "arbitrum": { - "router": "0x93ca0d85837FF83158Cd14D65B169CdB223b1921" + "inevm": { + "router": "0x26f32245fCF5Ad53159E875d5Cae62aEcf19c2d4" } } diff --git a/typescript/infra/config/environments/testnet4/validators.ts b/typescript/infra/config/environments/testnet4/validators.ts index 90677f9aa2..3617f04796 100644 --- a/typescript/infra/config/environments/testnet4/validators.ts +++ b/typescript/infra/config/environments/testnet4/validators.ts @@ -289,6 +289,18 @@ export const validatorChainConfig = ( 'polygonzkevmtestnet', ), }, + injective: { + interval: 5, + reorgPeriod: getReorgPeriod(chainMetadata.injective), + validators: validatorsConfig( + { + [Contexts.Hyperlane]: ['0x10686BEe585491A0DA5bfCd5ABfbB95Ab4d6c86d'], + [Contexts.ReleaseCandidate]: [], + [Contexts.Neutron]: [], + }, + 'injective', + ), + }, // proteustestnet: { // interval: 5, // reorgPeriod: getReorgPeriod(chainMetadata.proteustestnet), diff --git a/typescript/infra/package.json b/typescript/infra/package.json index fe04c9a5ee..2059a0221a 100644 --- a/typescript/infra/package.json +++ b/typescript/infra/package.json @@ -1,7 +1,7 @@ { "name": "@hyperlane-xyz/infra", "description": "Infrastructure utilities for the Hyperlane Network", - "version": "3.6.1", + "version": "3.6.2", "dependencies": { "@arbitrum/sdk": "^3.0.0", "@aws-sdk/client-iam": "^3.74.0", @@ -12,9 +12,9 @@ "@ethersproject/experimental": "^5.7.0", "@ethersproject/hardware-wallets": "^5.7.0", "@ethersproject/providers": "^5.7.2", - "@hyperlane-xyz/helloworld": "3.6.1", - "@hyperlane-xyz/sdk": "3.6.1", - "@hyperlane-xyz/utils": "3.6.1", + "@hyperlane-xyz/helloworld": "3.6.2", + "@hyperlane-xyz/sdk": "3.6.2", + "@hyperlane-xyz/utils": "3.6.2", "@nomiclabs/hardhat-etherscan": "^3.0.3", "@safe-global/api-kit": "^1.3.0", "@safe-global/protocol-kit": "^1.2.0", diff --git a/typescript/infra/scripts/announce-validators.ts b/typescript/infra/scripts/announce-validators.ts index 509c0d5237..f070329f3e 100644 --- a/typescript/infra/scripts/announce-validators.ts +++ b/typescript/infra/scripts/announce-validators.ts @@ -126,7 +126,9 @@ async function main() { const announced = announcedLocations[0].includes(location); if (!announced) { const signature = ethers.utils.joinSignature(announcement.signature); - console.log(`Announcing ${address} checkpoints at ${location}`); + console.log( + `[${chain}] Announcing ${address} checkpoints at ${location}`, + ); await validatorAnnounce.announce( address, location, @@ -134,7 +136,9 @@ async function main() { multiProvider.getTransactionOverrides(chain), ); } else { - console.log(`Already announced ${address} checkpoints at ${location}`); + console.log( + `[${chain}] Already announced ${address} checkpoints at ${location}`, + ); } } } diff --git a/typescript/infra/scripts/utils.ts b/typescript/infra/scripts/utils.ts index 645aa2ca05..f8a36a58df 100644 --- a/typescript/infra/scripts/utils.ts +++ b/typescript/infra/scripts/utils.ts @@ -1,3 +1,4 @@ +import debug from 'debug'; import path from 'path'; import yargs from 'yargs'; @@ -16,6 +17,7 @@ import { ProtocolType, objMap, promiseObjAll } from '@hyperlane-xyz/utils'; import { Contexts } from '../config/contexts'; import { environments } from '../config/environments'; +import { ethereumChainNames } from '../config/environments/mainnet3/chains'; import { getCurrentKubernetesContext } from '../src/agents'; import { getCloudAgentKey } from '../src/agents/key-utils'; import { CloudAgentKey } from '../src/agents/keys'; @@ -29,6 +31,8 @@ import { EnvironmentNames, deployEnvToSdkEnv } from '../src/config/environment'; import { Role } from '../src/roles'; import { assertContext, assertRole, readJSON } from '../src/utils/utils'; +const debugLog = debug('infra:scripts:utils'); + export enum Modules { // TODO: change PROXY_FACTORY = 'ism', @@ -130,6 +134,7 @@ export function assertEnvironment(env: string): DeployEnvironment { } export function getEnvironmentConfig(environment: DeployEnvironment) { + debugLog(`Getting environment config for ${environment}`); return environments[environment]; } @@ -154,12 +159,17 @@ export function getAgentConfig( typeof environment == 'string' ? getEnvironmentConfig(environment) : environment; + + debugLog( + `Getting agent config for ${context} context in ${coreConfig.environment} environment`, + ); + const agentConfig = coreConfig.agents[context]; if (!agentConfig) throw Error( - `Invalid context ${context} for environment, must be one of ${Object.keys( - coreConfig.agents, - )}.`, + `Invalid context ${context} for ${ + coreConfig.environment + } environment, must be one of ${Object.keys(coreConfig.agents)}.`, ); return agentConfig; } @@ -171,6 +181,7 @@ export function getKeyForRole( role: Role, index?: number, ): CloudAgentKey { + debugLog(`Getting key for ${role} role`); const environmentConfig = environments[environment]; const agentConfig = getAgentConfig(context, environmentConfig); return getCloudAgentKey(agentConfig, role, chain, index); @@ -185,17 +196,25 @@ export async function getMultiProviderForRole( // TODO: rename to consensusType? connectionType?: RpcConsensusType, ): Promise { + debugLog(`Getting multiprovider for ${role} role`); if (process.env.CI === 'true') { + debugLog('Returning multiprovider with default RPCs in CI'); return new MultiProvider(); // use default RPCs } const multiProvider = new MultiProvider(txConfigs); await promiseObjAll( objMap(txConfigs, async (chain, _) => { - const provider = await fetchProvider(environment, chain, connectionType); - const key = getKeyForRole(environment, context, chain, role, index); - const signer = await key.getSigner(provider); - multiProvider.setProvider(chain, provider); - multiProvider.setSigner(chain, signer); + if (ethereumChainNames.includes(chain)) { + const provider = await fetchProvider( + environment, + chain, + connectionType, + ); + const key = getKeyForRole(environment, context, chain, role, index); + const signer = await key.getSigner(provider); + multiProvider.setProvider(chain, provider); + multiProvider.setSigner(chain, signer); + } }), ); @@ -212,6 +231,7 @@ export async function getKeysForRole( index?: number, ): Promise> { if (process.env.CI === 'true') { + debugLog('No keys to return in CI'); return {}; } diff --git a/typescript/infra/src/agents/aws/key.ts b/typescript/infra/src/agents/aws/key.ts index fb42d10aab..a6c47369e5 100644 --- a/typescript/infra/src/agents/aws/key.ts +++ b/typescript/infra/src/agents/aws/key.ts @@ -16,6 +16,7 @@ import { UpdateAliasCommand, } from '@aws-sdk/client-kms'; import { KmsEthersSigner } from 'aws-kms-ethers-signer'; +import { Debugger, debug } from 'debug'; import { ethers } from 'ethers'; import { AgentSignerKeyType, ChainName } from '@hyperlane-xyz/sdk'; @@ -41,6 +42,7 @@ export class AgentAwsKey extends CloudAgentKey { private client: KMSClient | undefined; private region: string; public remoteKey: RemoteKey = { fetched: false }; + protected logger: Debugger; constructor( agentConfig: AgentContextConfig, @@ -53,16 +55,22 @@ export class AgentAwsKey extends CloudAgentKey { throw new Error('Not configured as AWS'); } this.region = agentConfig.aws.region; + this.logger = debug(`infra:agents:key:aws:${this.identifier}`); } get privateKey(): string { + this.logger( + 'Attempting to access private key, which is unavailable for AWS keys', + ); throw new Error('Private key unavailable for AWS keys'); } async getClient(): Promise { if (this.client) { + this.logger('Returning existing KMSClient instance'); return this.client; } + this.logger('Creating new KMSClient instance'); this.client = new KMSClient({ region: this.region, }); @@ -94,6 +102,7 @@ export class AgentAwsKey extends CloudAgentKey { } async fetch() { + this.logger('Fetching key'); const address = await this.fetchAddressFromAws(); this.remoteKey = { fetched: true, @@ -102,24 +111,28 @@ export class AgentAwsKey extends CloudAgentKey { } async createIfNotExists() { + this.logger('Checking if key exists and creating if not'); const keyId = await this.getId(); // If it doesn't exist, create it if (!keyId) { - // TODO should this be awaited? create is async - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.create(); + this.logger('Key does not exist, creating new key'); + await this.create(); // It can take a moment for the change to propagate await sleep(1000); + } else { + this.logger('Key already exists'); } await this.fetch(); } async delete() { + this.logger('Delete operation called, but not implemented'); throw Error('Not implemented yet'); } // Allows the `userArn` to use the key async putKeyPolicy(userArn: string) { + this.logger(`Putting key policy for user ARN: ${userArn}`); const client = await this.getClient(); const policy = { Version: '2012-10-17', @@ -151,22 +164,29 @@ export class AgentAwsKey extends CloudAgentKey { PolicyName: 'default', // This is the only accepted name }); await client.send(cmd); + this.logger('Key policy put successfully'); } // Gets the Key's ID if it exists, undefined otherwise async getId() { try { + this.logger('Attempting to describe key to get ID'); const keyDescription = await this.describeKey(); - return keyDescription.KeyMetadata?.KeyId; + const keyId = keyDescription.KeyMetadata?.KeyId; + this.logger(`Key ID retrieved: ${keyId}`); + return keyId; } catch (err: any) { if (err.name === 'NotFoundException') { + this.logger('Key not found'); return undefined; } + this.logger(`Error retrieving key ID: ${err}`); throw err; } } create() { + this.logger('Creating new key'); return this._create(false); } @@ -175,6 +195,7 @@ export class AgentAwsKey extends CloudAgentKey { * @returns The address of the new key */ update() { + this.logger('Updating key (creating new key for rotation)'); return this._create(true); } @@ -182,6 +203,7 @@ export class AgentAwsKey extends CloudAgentKey { * Requires update to have been called on this key prior */ async rotate() { + this.logger('Rotating keys'); const canonicalAlias = this.identifier; const newAlias = canonicalAlias + '-new'; const oldAlias = canonicalAlias + '-old'; @@ -226,15 +248,19 @@ export class AgentAwsKey extends CloudAgentKey { // Address should have changed now await this.fetch(); + this.logger('Keys rotated successfully'); } async getSigner( provider?: ethers.providers.Provider, ): Promise { + this.logger('Getting signer'); const keyId = await this.getId(); if (!keyId) { + this.logger('Key ID not defined, cannot get signer'); throw Error('Key ID not defined'); } + this.logger(`Creating KmsEthersSigner with key ID: ${keyId}`); // @ts-ignore We're using a newer version of Provider than // KmsEthersSigner. The return type for getFeeData for this newer // type is a superset of the return type for getFeeData for the older type, @@ -252,12 +278,15 @@ export class AgentAwsKey extends CloudAgentKey { private requireFetched() { if (!this.remoteKey.fetched) { + this.logger('Key has not been fetched yet'); throw new Error('Key not fetched'); } + this.logger('Key has been fetched'); } // Creates a new key and returns its address private async _create(rotate: boolean) { + this.logger(`Creating key with rotation: ${rotate}`); const client = await this.getClient(); const alias = this.identifier; if (!rotate) { @@ -269,6 +298,7 @@ export class AgentAwsKey extends CloudAgentKey { (_) => _.AliasName === alias, ); if (match) { + this.logger(`Alias ${alias} already exists`); throw new Error( `Attempted to create new key but alias ${alias} already exists`, ); @@ -288,6 +318,7 @@ export class AgentAwsKey extends CloudAgentKey { const createResponse = await client.send(command); if (!createResponse.KeyMetadata) { + this.logger('KeyMetadata was not returned when creating the key'); throw new Error('KeyMetadata was not returned when creating the key'); } const keyId = createResponse.KeyMetadata?.KeyId; @@ -298,10 +329,12 @@ export class AgentAwsKey extends CloudAgentKey { ); const address = this.fetchAddressFromAws(keyId); + this.logger(`New key created with ID: ${keyId}`); return address; } private async fetchAddressFromAws(keyId?: string) { + this.logger(`Fetching address from AWS for key ID: ${keyId}`); const client = await this.getClient(); if (!keyId) { @@ -312,10 +345,15 @@ export class AgentAwsKey extends CloudAgentKey { new GetPublicKeyCommand({ KeyId: keyId }), ); - return getEthereumAddress(Buffer.from(publicKeyResponse.PublicKey!)); + const address = getEthereumAddress( + Buffer.from(publicKeyResponse.PublicKey!), + ); + this.logger(`Address fetched: ${address}`); + return address; } private async describeKey(): Promise { + this.logger('Describing key'); const client = await this.getClient(); return client.send( new DescribeKeyCommand({ @@ -325,6 +363,7 @@ export class AgentAwsKey extends CloudAgentKey { } private async getAliases(): Promise { + this.logger('Getting aliases'); const client = await this.getClient(); let aliases: AliasListEntry[] = []; let marker: string | undefined = undefined; @@ -350,6 +389,7 @@ export class AgentAwsKey extends CloudAgentKey { break; } } + this.logger(`Aliases retrieved: ${aliases.length}`); return aliases; } } diff --git a/typescript/infra/src/agents/gcp.ts b/typescript/infra/src/agents/gcp.ts index 4ba314256e..e0fc75874a 100644 --- a/typescript/infra/src/agents/gcp.ts +++ b/typescript/infra/src/agents/gcp.ts @@ -1,9 +1,6 @@ -import { - encodeSecp256k1Pubkey, - pubkeyToAddress, - rawSecp256k1PubkeyToRawAddress, -} from '@cosmjs/amino'; +import { encodeSecp256k1Pubkey, pubkeyToAddress } from '@cosmjs/amino'; import { Keypair } from '@solana/web3.js'; +import { Debugger, debug } from 'debug'; import { Wallet, ethers } from 'ethers'; import { ChainName } from '@hyperlane-xyz/sdk'; @@ -42,6 +39,8 @@ interface FetchedKey { type RemoteKey = UnfetchedKey | FetchedKey; export class AgentGCPKey extends CloudAgentKey { + protected logger: Debugger; + constructor( environment: DeployEnvironment, context: Contexts, @@ -51,18 +50,23 @@ export class AgentGCPKey extends CloudAgentKey { private remoteKey: RemoteKey = { fetched: false }, ) { super(environment, context, role, chainName, index); + this.logger = debug(`infra:agents:key:gcp:${this.identifier}`); } async createIfNotExists() { + this.logger('Checking if key exists and creating if not'); try { await this.fetch(); + this.logger('Key already exists'); } catch (err) { + this.logger('Key does not exist, creating new key'); await this.create(); } } serializeAsAddress() { this.requireFetched(); + this.logger('Serializing key as address'); return { identifier: this.identifier, // @ts-ignore @@ -98,6 +102,7 @@ export class AgentGCPKey extends CloudAgentKey { addressForProtocol(protocol: ProtocolType): string | undefined { this.requireFetched(); + this.logger(`Getting address for protocol: ${protocol}`); switch (protocol) { case ProtocolType.Ethereum: @@ -106,7 +111,7 @@ export class AgentGCPKey extends CloudAgentKey { return Keypair.fromSeed( Buffer.from(strip0x(this.privateKey), 'hex'), ).publicKey.toBase58(); - case ProtocolType.Cosmos: + case ProtocolType.Cosmos: { const compressedPubkey = ethers.utils.computePublicKey( this.privateKey, true, @@ -117,12 +122,15 @@ export class AgentGCPKey extends CloudAgentKey { // TODO support other prefixes? // https://cosmosdrops.io/en/tools/bech32-converter is useful for converting to other prefixes. return pubkeyToAddress(encodedPubkey, 'neutron'); + } default: + this.logger(`Unsupported protocol: ${protocol}`); return undefined; } } async fetch() { + this.logger('Fetching key'); const secret: SecretManagerPersistedKeys = (await fetchGCPSecret( this.identifier, )) as any; @@ -131,25 +139,34 @@ export class AgentGCPKey extends CloudAgentKey { privateKey: secret.privateKey, address: secret.address, }; + this.logger(`Key fetched successfully: ${secret.address}`); } async create() { + this.logger('Creating new key'); this.remoteKey = await this._create(false); + this.logger('Key created successfully'); } async update() { + this.logger('Updating key'); this.remoteKey = await this._create(true); + this.logger('Key updated successfully'); return this.address; } async delete() { + this.logger('Deleting key'); await execCmd(`gcloud secrets delete ${this.identifier} --quiet`); + this.logger('Key deleted successfully'); } async getSigner( provider?: ethers.providers.Provider, ): Promise { + this.logger('Getting signer'); if (!this.remoteKey.fetched) { + this.logger('Key not fetched, fetching now'); await this.fetch(); } return new Wallet(this.privateKey, provider); @@ -157,12 +174,14 @@ export class AgentGCPKey extends CloudAgentKey { private requireFetched() { if (!this.remoteKey.fetched) { + this.logger('Key not fetched, throwing error'); throw new Error("Can't persist without address"); } } // eslint-disable-next-line @typescript-eslint/no-unused-vars private async _create(rotate: boolean) { + this.logger(`Creating key with rotation: ${rotate}`); const wallet = Wallet.createRandom(); const address = await wallet.getAddress(); const identifier = this.identifier; @@ -187,6 +206,7 @@ export class AgentGCPKey extends CloudAgentKey { }), }, ); + this.logger('Key creation data persisted to GCP'); return { fetched: true, diff --git a/typescript/infra/src/agents/key-utils.ts b/typescript/infra/src/agents/key-utils.ts index 11e669d238..b597cb395e 100644 --- a/typescript/infra/src/agents/key-utils.ts +++ b/typescript/infra/src/agents/key-utils.ts @@ -1,3 +1,5 @@ +import debug from 'debug'; + import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; import { objMap } from '@hyperlane-xyz/utils'; @@ -17,6 +19,8 @@ import { AgentAwsKey } from './aws/key'; import { AgentGCPKey } from './gcp'; import { CloudAgentKey } from './keys'; +const debugLog = debug('infra:agents:key:utils'); + export interface KeyAsAddress { identifier: string; address: string; @@ -156,6 +160,7 @@ function getRoleKeyMapPerChain( export function getAllCloudAgentKeys( agentConfig: RootAgentConfig, ): Array { + debugLog('Retrieving all cloud agent keys'); const keysPerChain = getRoleKeyMapPerChain(agentConfig); const keysByIdentifier = Object.keys(keysPerChain).reduce( @@ -190,21 +195,22 @@ export function getCloudAgentKey( chainName?: ChainName, index?: number, ): CloudAgentKey { + debugLog(`Retrieving cloud agent key for ${role} on ${chainName}`); switch (role) { case Role.Validator: if (chainName === undefined || index === undefined) { - throw Error(`Must provide chainName and index for validator key`); + throw Error('Must provide chainName and index for validator key'); } // For now just get the validator key, and not the chain signer. return getValidatorKeysForChain(agentConfig, chainName, index).validator; case Role.Relayer: if (chainName === undefined) { - throw Error(`Must provide chainName for relayer key`); + throw Error('Must provide chainName for relayer key'); } return getRelayerKeyForChain(agentConfig, chainName); case Role.Kathy: if (chainName === undefined) { - throw Error(`Must provide chainName for kathy key`); + throw Error('Must provide chainName for kathy key'); } return getKathyKeyForChain(agentConfig, chainName); case Role.Deployer: @@ -223,6 +229,7 @@ export function getRelayerKeyForChain( agentConfig: AgentContextConfig, chainName: ChainName, ): CloudAgentKey { + debugLog(`Retrieving relayer key for ${chainName}`); // If AWS is enabled and the chain is an Ethereum-based chain, we want to use // an AWS key. if (agentConfig.aws && isEthereumProtocolChain(chainName)) { @@ -240,6 +247,7 @@ export function getKathyKeyForChain( agentConfig: AgentContextConfig, chainName: ChainName, ): CloudAgentKey { + debugLog(`Retrieving kathy key for ${chainName}`); // If AWS is enabled and the chain is an Ethereum-based chain, we want to use // an AWS key. if (agentConfig.aws && isEthereumProtocolChain(chainName)) { @@ -252,6 +260,7 @@ export function getKathyKeyForChain( // Returns the deployer key. This is always a GCP key, not chain specific, // and in the Hyperlane context. export function getDeployerKey(agentConfig: AgentContextConfig): CloudAgentKey { + debugLog('Retrieving deployer key'); return new AgentGCPKey(agentConfig.runEnv, Contexts.Hyperlane, Role.Deployer); } @@ -267,6 +276,7 @@ export function getValidatorKeysForChain( validator: CloudAgentKey; chainSigner: CloudAgentKey; } { + debugLog(`Retrieving validator keys for ${chainName}`); const validator = agentConfig.aws ? new AgentAwsKey(agentConfig, Role.Validator, chainName, index) : new AgentGCPKey( @@ -279,15 +289,19 @@ export function getValidatorKeysForChain( // If the chain is Ethereum-based, we can just use the validator key (even if it's AWS-based) // as the chain signer. Otherwise, we need to use a GCP key. - const chainSigner = isEthereumProtocolChain(chainName) - ? validator - : new AgentGCPKey( - agentConfig.runEnv, - agentConfig.context, - Role.Validator, - chainName, - index, - ); + let chainSigner; + if (isEthereumProtocolChain(chainName)) { + chainSigner = validator; + } else { + debugLog(`Retrieving GCP key for ${chainName}, as it is not EVM`); + chainSigner = new AgentGCPKey( + agentConfig.runEnv, + agentConfig.context, + Role.Validator, + chainName, + index, + ); + } return { validator, @@ -302,6 +316,7 @@ export function getValidatorKeysForChain( export async function createAgentKeysIfNotExists( agentConfig: AgentContextConfig, ) { + debugLog('Creating agent keys if none exist'); const keys = getAllCloudAgentKeys(agentConfig); await Promise.all( @@ -318,6 +333,7 @@ export async function createAgentKeysIfNotExists( } export async function deleteAgentKeys(agentConfig: AgentContextConfig) { + debugLog('Deleting agent keys'); const keys = getAllCloudAgentKeys(agentConfig); await Promise.all(keys.map((key) => key.delete())); await execCmd( @@ -333,6 +349,7 @@ export async function rotateKey( role: Role, chainName: ChainName, ) { + debugLog(`Rotating key for ${role} on ${chainName}`); const key = getCloudAgentKey(agentConfig, role, chainName); await key.update(); const keyIdentifier = key.identifier; @@ -357,6 +374,9 @@ async function persistAddresses( context: Contexts, keys: KeyAsAddress[], ) { + debugLog( + `Persisting addresses to GCP for ${context} context in ${environment} environment`, + ); await setGCPSecret( addressesIdentifier(environment, context), JSON.stringify(keys), @@ -371,6 +391,9 @@ async function fetchGCPKeyAddresses( environment: DeployEnvironment, context: Contexts, ) { + debugLog( + `Fetching addresses from GCP for ${context} context in ${environment} environment`, + ); const addresses = await fetchGCPSecret( addressesIdentifier(environment, context), ); diff --git a/typescript/infra/src/utils/gcloud.ts b/typescript/infra/src/utils/gcloud.ts index ef2650d357..dde9411ed0 100644 --- a/typescript/infra/src/utils/gcloud.ts +++ b/typescript/infra/src/utils/gcloud.ts @@ -1,3 +1,4 @@ +import debug from 'debug'; import fs from 'fs'; import { rm, writeFile } from 'fs/promises'; @@ -9,6 +10,8 @@ interface IamCondition { expression: string; } +const debugLog = debug('infra:utils:gcloud'); + // Allows secrets to be overridden via environment variables to avoid // gcloud calls. This is particularly useful for running commands in k8s, // where we can use external-secrets to fetch secrets from GCP secret manager, @@ -23,7 +26,7 @@ export async function fetchGCPSecret( const envVarOverride = tryGCPSecretFromEnvVariable(secretName); if (envVarOverride !== undefined) { - console.log( + debugLog( `Using environment variable instead of GCP secret with name ${secretName}`, ); output = envVarOverride; @@ -41,7 +44,7 @@ export async function fetchGCPSecret( // If the environment variable GCP_SECRET_OVERRIDES_ENABLED is `true`, // this will attempt to find an environment variable of the form: -// `GCP_SECRET_OVERRIDE_${gcpSecretName..replaceAll('-', '_').toUpperCase()}` +// `GCP_SECRET_OVERRIDE_${gcpSecretName.replaceAll('-', '_').toUpperCase()}` // If found, it's returned, otherwise, undefined is returned. function tryGCPSecretFromEnvVariable(gcpSecretName: string) { const overridingEnabled = @@ -58,9 +61,12 @@ function tryGCPSecretFromEnvVariable(gcpSecretName: string) { export async function gcpSecretExists(secretName: string) { const fullName = `projects/${await getCurrentProjectNumber()}/secrets/${secretName}`; + debugLog(`Checking if GCP secret exists for ${fullName}`); + const matches = await execCmdAndParseJson( `gcloud secrets list --filter name=${fullName} --format json`, ); + debugLog(`Matches: ${matches.length}`); return matches.length > 0; } @@ -80,10 +86,12 @@ export async function setGCPSecret( await execCmd( `gcloud secrets create ${secretName} --data-file=${fileName} --replication-policy=automatic --labels=${labelString}`, ); + debugLog(`Created new GCP secret for ${secretName}`); } else { await execCmd( `gcloud secrets versions add ${secretName} --data-file=${fileName}`, ); + debugLog(`Added new version to existing GCP secret for ${secretName}`); } await rm(fileName); } @@ -95,6 +103,9 @@ export async function createServiceAccountIfNotExists( let serviceAccountInfo = await getServiceAccountInfo(serviceAccountName); if (!serviceAccountInfo) { serviceAccountInfo = await createServiceAccount(serviceAccountName); + debugLog(`Created new service account with name ${serviceAccountName}`); + } else { + debugLog(`Service account with name ${serviceAccountName} already exists`); } return serviceAccountInfo.email; } @@ -110,6 +121,7 @@ export async function grantServiceAccountRoleIfNotExists( matchedBinding && iamConditionsEqual(condition, matchedBinding.condition) ) { + debugLog(`Service account ${serviceAccountEmail} already has role ${role}`); return; } await execCmd( @@ -119,6 +131,7 @@ export async function grantServiceAccountRoleIfNotExists( : '' }`, ); + debugLog(`Granted role ${role} to service account ${serviceAccountEmail}`); } export async function createServiceAccountKey(serviceAccountEmail: string) { @@ -128,12 +141,14 @@ export async function createServiceAccountKey(serviceAccountEmail: string) { ); const key = JSON.parse(fs.readFileSync(localKeyFile, 'utf8')); fs.rmSync(localKeyFile); + debugLog(`Created new service account key for ${serviceAccountEmail}`); return key; } // The alphanumeric project name / ID export async function getCurrentProject() { const [result] = await execCmd('gcloud config get-value project'); + debugLog(`Current GCP project ID: ${result.trim()}`); return result.trim(); } @@ -150,10 +165,12 @@ async function getIamMemberPolicyBindings(memberEmail: string) { const unprocessedRoles = await execCmdAndParseJson( `gcloud projects get-iam-policy $(gcloud config get-value project) --format "json(bindings)" --flatten="bindings[].members" --filter="bindings.members:${memberEmail}"`, ); - return unprocessedRoles.map((unprocessedRoleObject: any) => ({ + const bindings = unprocessedRoles.map((unprocessedRoleObject: any) => ({ role: unprocessedRoleObject.bindings.role, condition: unprocessedRoleObject.bindings.condition, })); + debugLog(`Retrieved IAM policy bindings for ${memberEmail}`); + return bindings; } async function createServiceAccount(serviceAccountName: string) { @@ -169,8 +186,10 @@ async function getServiceAccountInfo(serviceAccountName: string) { `gcloud iam service-accounts list --format json --filter displayName="${serviceAccountName}"`, ); if (matches.length === 0) { + debugLog(`No service account found with name ${serviceAccountName}`); return undefined; } + debugLog(`Found service account with name ${serviceAccountName}`); return matches[0]; } diff --git a/typescript/sdk/CHANGELOG.md b/typescript/sdk/CHANGELOG.md index e90a804e6d..d4a314049b 100644 --- a/typescript/sdk/CHANGELOG.md +++ b/typescript/sdk/CHANGELOG.md @@ -1,5 +1,12 @@ # @hyperlane-xyz/sdk +## 3.6.2 + +### Patch Changes + +- @hyperlane-xyz/core@3.6.2 +- @hyperlane-xyz/utils@3.6.2 + ## 3.6.1 ### Patch Changes diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index bf03327d05..7d526909d8 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -1,12 +1,12 @@ { "name": "@hyperlane-xyz/sdk", "description": "The official SDK for the Hyperlane Network", - "version": "3.6.1", + "version": "3.6.2", "dependencies": { "@cosmjs/cosmwasm-stargate": "^0.31.3", "@cosmjs/stargate": "^0.31.3", - "@hyperlane-xyz/core": "3.6.1", - "@hyperlane-xyz/utils": "3.6.1", + "@hyperlane-xyz/core": "3.6.2", + "@hyperlane-xyz/utils": "3.6.2", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.78.0", "@types/coingecko-api": "^1.0.10", diff --git a/typescript/sdk/src/consts/chainMetadata.ts b/typescript/sdk/src/consts/chainMetadata.ts index 7a8a469b66..458c2e6e48 100644 --- a/typescript/sdk/src/consts/chainMetadata.ts +++ b/typescript/sdk/src/consts/chainMetadata.ts @@ -419,6 +419,58 @@ export const gnosis: ChainMetadata = { ], }; +export const inevm: ChainMetadata = { + blockExplorers: [ + { + apiUrl: 'https://inevm.calderaexplorer.xyz/api', + family: ExplorerFamily.Blockscout, + name: 'Caldera inEVM Explorer', + url: 'https://inevm.calderaexplorer.xyz', + }, + ], + blocks: { + confirmations: 1, + estimateBlockTime: 3, + reorgPeriod: 0, + }, + chainId: 2525, + displayName: 'Injective EVM', + displayNameShort: 'inEVM', + domainId: 2525, + name: Chains.inevm, + nativeToken: { + decimals: 18, + name: 'Injective', + symbol: 'INJ', + }, + protocol: ProtocolType.Ethereum, + rpcUrls: [{ http: 'https://inevm.calderachain.xyz/http' }], +}; + +export const injective: ChainMetadata = { + bech32Prefix: 'inj', + blockExplorers: [], + blocks: { + confirmations: 1, + estimateBlockTime: 3, + reorgPeriod: 1, + }, + chainId: 'injective-1', + displayName: 'Injective', + domainId: 6909546, + grpcUrls: [{ http: 'sentry.chain.grpc.injective.network:443' }], + name: Chains.injective, + nativeToken: { + decimals: 18, + name: 'Injective', + symbol: 'INJ', + }, + protocol: ProtocolType.Cosmos, + restUrls: [{ http: 'https://sentry.lcd.injective.network:443' }], + rpcUrls: [{ http: 'https://sentry.tm.injective.network:443' }], + slip44: 118, +}; + export const lineagoerli: ChainMetadata = { blockExplorers: [ { @@ -853,6 +905,7 @@ export const sepolia: ChainMetadata = { nativeToken: etherToken, protocol: ProtocolType.Ethereum, rpcUrls: [ + { http: 'https://ethereum-sepolia.publicnode.com' }, { http: 'https://ethereum-sepolia.blockpi.network/v1/rpc/public' }, { http: 'https://rpc.sepolia.org' }, ], @@ -1036,6 +1089,9 @@ export const viction: ChainMetadata = { }, protocol: ProtocolType.Ethereum, rpcUrls: [ + { + http: 'https://rpc.tomochain.com/', + }, { http: 'https://viction.blockpi.network/v1/rpc/public', }, @@ -1064,6 +1120,8 @@ export const chainMetadata: ChainMap = { fuji, gnosis, goerli, + inevm, + injective, lineagoerli, mantapacific, moonbasealpha, diff --git a/typescript/sdk/src/consts/chains.ts b/typescript/sdk/src/consts/chains.ts index 1711c1d107..09b3749811 100644 --- a/typescript/sdk/src/consts/chains.ts +++ b/typescript/sdk/src/consts/chains.ts @@ -17,6 +17,8 @@ export enum Chains { fuji = 'fuji', gnosis = 'gnosis', goerli = 'goerli', + inevm = 'inevm', + injective = 'injective', lineagoerli = 'lineagoerli', mantapacific = 'mantapacific', moonbasealpha = 'moonbasealpha', @@ -71,6 +73,8 @@ export const Mainnets: Array = [ Chains.base, Chains.scroll, Chains.polygonzkevm, + Chains.injective, + Chains.inevm, Chains.viction, // Chains.solana, ]; diff --git a/typescript/sdk/src/consts/environments/mainnet.json b/typescript/sdk/src/consts/environments/mainnet.json index 041af75f41..82f22b523a 100644 --- a/typescript/sdk/src/consts/environments/mainnet.json +++ b/typescript/sdk/src/consts/environments/mainnet.json @@ -257,6 +257,26 @@ "fallbackRoutingHook": "0xD1E267d2d7876e97E217BfE61c34AB50FEF52807", "interchainSecurityModule": "0xDEed16fe4b1c9b2a93483EDFf34C77A9b57D31Ff" }, + "inevm": { + "merkleRootMultisigIsmFactory": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "messageIdMultisigIsmFactory": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "aggregationIsmFactory": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "aggregationHookFactory": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "routingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "domainRoutingIsm": "0xBD70Ea9D599a0FC8158B026797177773C3445730", + "proxyAdmin": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "storageGasOracle": "0x6119E37Bd66406A1Db74920aC79C15fB8411Ba76", + "interchainGasPaymaster": "0x19dc38aeae620380430C200a6E990D5Af5480117", + "merkleTreeHook": "0x0972954923a1e2b2aAb04Fa0c4a0797e5989Cd65", + "aggregationHook": "0xe0dDb5dE7D52918237cC1Ae131F29dcAbcb0F62B", + "protocolFee": "0x0D63128D887159d63De29497dfa45AFc7C699AE4", + "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "validatorAnnounce": "0x15ab173bDB6832f9b64276bA128659b0eD77730B", + "interchainSecurityModule": "0x3052aD50De54aAAc5D364d80bBE681d29e924597", + "pausableIsm": "0x6Fae4D9935E2fcb11fC79a64e917fb2BF14DaFaa", + "staticAggregationIsm": "0x3052aD50De54aAAc5D364d80bBE681d29e924597", + "pausableHook": "0xBDa330Ea8F3005C421C8088e638fBB64fA71b9e0" + }, "viction": { "merkleRootMultisigIsmFactory": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", "messageIdMultisigIsmFactory": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", diff --git a/typescript/sdk/src/consts/multisigIsm.ts b/typescript/sdk/src/consts/multisigIsm.ts index fb507c693e..2d2bde114f 100644 --- a/typescript/sdk/src/consts/multisigIsm.ts +++ b/typescript/sdk/src/consts/multisigIsm.ts @@ -143,6 +143,24 @@ export const defaultMultisigConfigs: ChainMap = { ], }, + inevm: { + threshold: 2, + validators: [ + '0xf9e35ee88e4448a3673b4676a4e153e3584a08eb', + '0xae3e6bb6b3ece1c425aa6f47adc8cb0453c1f9a2', + '0xd98c9522cd9d3e3e00bee05ff76c34b91b266ec3', + ], + }, + + injective: { + threshold: 2, + validators: [ + '0xbfb8911b72cfb138c7ce517c57d9c691535dc517', + '0x6faa139c33a7e6f53cb101f6b2ae392298283ed2', + '0x0115e3a66820fb99da30d30e2ce52a453ba99d92', + ], + }, + lineagoerli: { threshold: 2, validators: [ diff --git a/typescript/sdk/src/metadata/agentConfig.ts b/typescript/sdk/src/metadata/agentConfig.ts index e7133b4c4a..5aa89687ff 100644 --- a/typescript/sdk/src/metadata/agentConfig.ts +++ b/typescript/sdk/src/metadata/agentConfig.ts @@ -124,7 +124,7 @@ export const AgentChainMetadataSchema = ChainMetadataSchemaObject.merge( .string() .optional() .describe( - 'Specify a comma seperated list of custom RPC URLs to use for this chain. If not specified, the default RPC urls will be used.', + 'Specify a comma separated list of custom RPC URLs to use for this chain. If not specified, the default RPC urls will be used.', ), rpcConsensusType: z .nativeEnum(RpcConsensusType) diff --git a/typescript/sdk/src/metadata/chainMetadataTypes.ts b/typescript/sdk/src/metadata/chainMetadataTypes.ts index b13cea0349..565a151af0 100644 --- a/typescript/sdk/src/metadata/chainMetadataTypes.ts +++ b/typescript/sdk/src/metadata/chainMetadataTypes.ts @@ -115,6 +115,12 @@ export const ChainMetadataSchemaObject = z.object({ .array(RpcUrlSchema) .describe('For cosmos chains only, a list of gRPC API URLs') .optional(), + customGrpcUrls: z + .string() + .optional() + .describe( + 'Specify a comma separated list of custom GRPC URLs to use for this chain. If not specified, the default GRPC urls will be used.', + ), blockExplorers: z .array( z.object({ diff --git a/typescript/utils/CHANGELOG.md b/typescript/utils/CHANGELOG.md index bacd69aaf4..57f4c424d6 100644 --- a/typescript/utils/CHANGELOG.md +++ b/typescript/utils/CHANGELOG.md @@ -1,5 +1,7 @@ # @hyperlane-xyz/utils +## 3.6.2 + ## 3.6.1 ### Patch Changes diff --git a/typescript/utils/package.json b/typescript/utils/package.json index 9f88525122..a578e30cb8 100644 --- a/typescript/utils/package.json +++ b/typescript/utils/package.json @@ -1,7 +1,7 @@ { "name": "@hyperlane-xyz/utils", "description": "General utilities and types for the Hyperlane network", - "version": "3.6.1", + "version": "3.6.2", "dependencies": { "@cosmjs/encoding": "^0.31.3", "@solana/web3.js": "^1.78.0", diff --git a/yarn.lock b/yarn.lock index 34d6186d52..e2c15d053e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4235,8 +4235,8 @@ __metadata: version: 0.0.0-use.local resolution: "@hyperlane-xyz/cli@workspace:typescript/cli" dependencies: - "@hyperlane-xyz/sdk": "npm:3.6.1" - "@hyperlane-xyz/utils": "npm:3.6.1" + "@hyperlane-xyz/sdk": "npm:3.6.2" + "@hyperlane-xyz/utils": "npm:3.6.2" "@inquirer/prompts": "npm:^3.0.0" "@types/mocha": "npm:^10.0.1" "@types/node": "npm:^18.14.5" @@ -4261,12 +4261,12 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/core@npm:3.6.1, @hyperlane-xyz/core@workspace:solidity": +"@hyperlane-xyz/core@npm:3.6.2, @hyperlane-xyz/core@workspace:solidity": version: 0.0.0-use.local resolution: "@hyperlane-xyz/core@workspace:solidity" dependencies: "@eth-optimism/contracts": "npm:^0.6.0" - "@hyperlane-xyz/utils": "npm:3.6.1" + "@hyperlane-xyz/utils": "npm:3.6.2" "@nomiclabs/hardhat-ethers": "npm:^2.2.1" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@openzeppelin/contracts": "npm:^4.9.3" @@ -4293,12 +4293,12 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/helloworld@npm:3.6.1, @hyperlane-xyz/helloworld@workspace:typescript/helloworld": +"@hyperlane-xyz/helloworld@npm:3.6.2, @hyperlane-xyz/helloworld@workspace:typescript/helloworld": version: 0.0.0-use.local resolution: "@hyperlane-xyz/helloworld@workspace:typescript/helloworld" dependencies: - "@hyperlane-xyz/core": "npm:3.6.1" - "@hyperlane-xyz/sdk": "npm:3.6.1" + "@hyperlane-xyz/core": "npm:3.6.2" + "@hyperlane-xyz/sdk": "npm:3.6.2" "@nomiclabs/hardhat-ethers": "npm:^2.2.1" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@openzeppelin/contracts-upgradeable": "npm:^4.9.3" @@ -4343,9 +4343,9 @@ __metadata: "@ethersproject/experimental": "npm:^5.7.0" "@ethersproject/hardware-wallets": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.2" - "@hyperlane-xyz/helloworld": "npm:3.6.1" - "@hyperlane-xyz/sdk": "npm:3.6.1" - "@hyperlane-xyz/utils": "npm:3.6.1" + "@hyperlane-xyz/helloworld": "npm:3.6.2" + "@hyperlane-xyz/sdk": "npm:3.6.2" + "@hyperlane-xyz/utils": "npm:3.6.2" "@nomiclabs/hardhat-ethers": "npm:^2.2.1" "@nomiclabs/hardhat-etherscan": "npm:^3.0.3" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" @@ -4393,14 +4393,14 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/sdk@npm:3.6.1, @hyperlane-xyz/sdk@workspace:typescript/sdk": +"@hyperlane-xyz/sdk@npm:3.6.2, @hyperlane-xyz/sdk@workspace:typescript/sdk": version: 0.0.0-use.local resolution: "@hyperlane-xyz/sdk@workspace:typescript/sdk" dependencies: "@cosmjs/cosmwasm-stargate": "npm:^0.31.3" "@cosmjs/stargate": "npm:^0.31.3" - "@hyperlane-xyz/core": "npm:3.6.1" - "@hyperlane-xyz/utils": "npm:3.6.1" + "@hyperlane-xyz/core": "npm:3.6.2" + "@hyperlane-xyz/utils": "npm:3.6.2" "@nomiclabs/hardhat-ethers": "npm:^2.2.1" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@solana/spl-token": "npm:^0.3.8" @@ -4437,7 +4437,7 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/utils@npm:3.6.1, @hyperlane-xyz/utils@workspace:typescript/utils": +"@hyperlane-xyz/utils@npm:3.6.2, @hyperlane-xyz/utils@workspace:typescript/utils": version: 0.0.0-use.local resolution: "@hyperlane-xyz/utils@workspace:typescript/utils" dependencies: