diff --git a/rust/Cargo.lock b/rust/Cargo.lock index e714475b55..2aed8d25d9 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -3792,6 +3792,7 @@ dependencies = [ "hex 0.4.3", "hyperlane-core", "hyperlane-sealevel-connection-client", + "hyperlane-sealevel-hello-world", "hyperlane-sealevel-igp", "hyperlane-sealevel-mailbox", "hyperlane-sealevel-multisig-ism-message-id", @@ -3823,6 +3824,26 @@ dependencies = [ "solana-program", ] +[[package]] +name = "hyperlane-sealevel-hello-world" +version = "0.1.0" +dependencies = [ + "access-control", + "account-utils", + "borsh 0.9.3", + "hyperlane-core", + "hyperlane-sealevel-connection-client", + "hyperlane-sealevel-igp", + "hyperlane-sealevel-mailbox", + "hyperlane-sealevel-message-recipient-interface", + "hyperlane-test-utils", + "serializable-account-meta", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-noop", +] + [[package]] name = "hyperlane-sealevel-igp" version = "0.1.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 6dec8bf51d..b1400bbf87 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -22,6 +22,7 @@ members = [ "sealevel/libraries/serializable-account-meta", "sealevel/libraries/test-transaction-utils", "sealevel/libraries/test-utils", + "sealevel/programs/helloworld", "sealevel/programs/hyperlane-sealevel-token", "sealevel/programs/hyperlane-sealevel-token-collateral", "sealevel/programs/hyperlane-sealevel-token-native", diff --git a/rust/agents/relayer/src/msg/metadata/base.rs b/rust/agents/relayer/src/msg/metadata/base.rs index 020b6d8eac..45a6879358 100644 --- a/rust/agents/relayer/src/msg/metadata/base.rs +++ b/rust/agents/relayer/src/msg/metadata/base.rs @@ -74,9 +74,14 @@ impl MetadataBuilder for BaseMetadataBuilder { ism_address: H256, message: &HyperlaneMessage, ) -> Result>> { - const CTX: &str = "When fetching module type"; - let ism = self.build_ism(ism_address).await.context(CTX)?; - let module_type = ism.module_type().await.context(CTX)?; + let ism = self + .build_ism(ism_address) + .await + .context("When building ISM")?; + let module_type = ism + .module_type() + .await + .context("When fetching module type")?; let base = self.clone_with_incremented_depth()?; let metadata_builder: Box = match module_type { @@ -94,7 +99,7 @@ impl MetadataBuilder for BaseMetadataBuilder { metadata_builder .build(ism_address, message) .await - .context(CTX) + .context("When building metadata") } } diff --git a/rust/hyperlane-core/src/traits/interchain_security_module.rs b/rust/hyperlane-core/src/traits/interchain_security_module.rs index 1d6907fad9..2fa7263a30 100644 --- a/rust/hyperlane-core/src/traits/interchain_security_module.rs +++ b/rust/hyperlane-core/src/traits/interchain_security_module.rs @@ -4,12 +4,23 @@ use async_trait::async_trait; use auto_impl::auto_impl; use borsh::{BorshDeserialize, BorshSerialize}; use num_derive::FromPrimitive; +use serde::{Deserialize, Serialize}; use crate::{ChainResult, HyperlaneContract, HyperlaneMessage, U256}; /// Enumeration of all known module types #[derive( - FromPrimitive, Clone, Debug, Default, Copy, PartialEq, Eq, BorshDeserialize, BorshSerialize, + FromPrimitive, + Clone, + Debug, + Default, + Copy, + PartialEq, + Eq, + BorshDeserialize, + BorshSerialize, + Serialize, + Deserialize, )] #[cfg_attr(feature = "strum", derive(strum::Display))] pub enum ModuleType { diff --git a/rust/sealevel/client/Cargo.toml b/rust/sealevel/client/Cargo.toml index 6f4b67220c..976bf11632 100644 --- a/rust/sealevel/client/Cargo.toml +++ b/rust/sealevel/client/Cargo.toml @@ -31,4 +31,5 @@ hyperlane-sealevel-igp = { path = "../programs/hyperlane-sealevel-igp", features hyperlane-sealevel-token-collateral = { path = "../programs/hyperlane-sealevel-token-collateral", features = ["no-entrypoint"] } hyperlane-sealevel-token-lib = { path = "../libraries/hyperlane-sealevel-token" } hyperlane-sealevel-token-native = { path = "../programs/hyperlane-sealevel-token-native", features = ["no-entrypoint"] } -hyperlane-sealevel-validator-announce = { path = "../programs/validator-announce", features = ["no-entrypoint"] } \ No newline at end of file +hyperlane-sealevel-validator-announce = { path = "../programs/validator-announce", features = ["no-entrypoint"] } +hyperlane-sealevel-hello-world = { path = "../programs/helloworld" } \ No newline at end of file diff --git a/rust/sealevel/client/src/artifacts.rs b/rust/sealevel/client/src/artifacts.rs new file mode 100644 index 0000000000..7caf4806f0 --- /dev/null +++ b/rust/sealevel/client/src/artifacts.rs @@ -0,0 +1,59 @@ +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +use hyperlane_core::H256; +use solana_program::pubkey::Pubkey; + +use std::{fs::File, io::Write, path::Path, str::FromStr}; + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct SingularProgramIdArtifact { + #[serde(with = "crate::serde::serde_pubkey")] + pub program_id: Pubkey, +} + +impl From for SingularProgramIdArtifact { + fn from(val: Pubkey) -> Self { + SingularProgramIdArtifact { program_id: val } + } +} + +#[derive(Serialize, Deserialize)] +pub(crate) struct HexAndBase58ProgramIdArtifact { + hex: String, + base58: String, +} + +impl From for HexAndBase58ProgramIdArtifact { + fn from(val: H256) -> Self { + HexAndBase58ProgramIdArtifact { + hex: format!("0x{}", hex::encode(val)), + base58: Pubkey::new_from_array(val.to_fixed_bytes()).to_string(), + } + } +} + +impl From<&HexAndBase58ProgramIdArtifact> for Pubkey { + fn from(val: &HexAndBase58ProgramIdArtifact) -> Self { + Pubkey::from_str(&val.base58).unwrap() + } +} + +pub(crate) fn write_json(path: &Path, program_id: T) +where + T: Serialize, +{ + let json = serde_json::to_string_pretty(&program_id).unwrap(); + println!("Writing to file {} contents:\n{}", path.display(), json); + + let mut file = File::create(path).expect("Failed to create file"); + file.write_all(json.as_bytes()) + .expect("Failed write JSON to file"); +} + +pub(crate) fn read_json(path: &Path) -> T +where + T: DeserializeOwned, +{ + let file = File::open(path).expect("Failed to open JSON file"); + serde_json::from_reader(file).expect("Failed to read JSON file") +} diff --git a/rust/sealevel/client/src/context.rs b/rust/sealevel/client/src/context.rs index bc1bd5190e..80b0ea9e2b 100644 --- a/rust/sealevel/client/src/context.rs +++ b/rust/sealevel/client/src/context.rs @@ -26,6 +26,7 @@ pub(crate) struct Context { payer_keypair: Option, pub commitment: CommitmentConfig, pub initial_instructions: RefCell>, + pub require_tx_approval: bool, } pub(crate) struct InstructionWithDescription { @@ -58,6 +59,7 @@ impl Context { payer_keypair: Option, commitment: CommitmentConfig, initial_instructions: RefCell>, + require_tx_approval: bool, ) -> Self { Self { client, @@ -65,6 +67,7 @@ impl Context { payer_keypair, commitment, initial_instructions, + require_tx_approval, } } @@ -183,6 +186,13 @@ impl<'ctx, 'rpc> TxnBuilder<'ctx, 'rpc> { return None; } + // Print the tx as an indication for what's about to happen + self.pretty_print_transaction(); + + if self.ctx.require_tx_approval { + wait_for_user_confirmation(); + } + let client = self.client.unwrap_or(&self.ctx.client); let recent_blockhash = client.get_latest_blockhash().unwrap(); diff --git a/rust/sealevel/client/src/core.rs b/rust/sealevel/client/src/core.rs index 2f0f29f77a..8aaf51adb1 100644 --- a/rust/sealevel/client/src/core.rs +++ b/rust/sealevel/client/src/core.rs @@ -4,10 +4,12 @@ use solana_program::pubkey::Pubkey; use solana_sdk::signature::Signer; use std::collections::HashMap; -use std::{fs::File, io::Write, path::Path}; +use std::{fs::File, path::Path}; use crate::{ + artifacts::{read_json, write_json}, cmd_utils::{create_and_write_keypair, create_new_directory, deploy_program}, + multisig_ism::deploy_multisig_ism_message_id, Context, CoreCmd, CoreDeploy, CoreSubCmd, }; use hyperlane_core::H256; @@ -21,7 +23,12 @@ pub(crate) fn process_core_cmd(mut ctx: Context, cmd: CoreCmd) { let core_dir = create_new_directory(&chain_dir, "core"); let key_dir = create_new_directory(&core_dir, "keys"); - let ism_program_id = deploy_multisig_ism_message_id(&mut ctx, &core, &key_dir); + let ism_program_id = deploy_multisig_ism_message_id( + &mut ctx, + &core.built_so_dir, + core.use_existing_keys, + &key_dir, + ); let mailbox_program_id = deploy_mailbox(&mut ctx, &core, &key_dir, ism_program_id); @@ -44,43 +51,6 @@ pub(crate) fn process_core_cmd(mut ctx: Context, cmd: CoreCmd) { } } -fn deploy_multisig_ism_message_id(ctx: &mut Context, cmd: &CoreDeploy, key_dir: &Path) -> Pubkey { - let (keypair, keypair_path) = create_and_write_keypair( - key_dir, - "hyperlane_sealevel_multisig_ism_message_id-keypair.json", - cmd.use_existing_keys, - ); - let program_id = keypair.pubkey(); - - deploy_program( - ctx.payer_keypair_path(), - keypair_path.to_str().unwrap(), - cmd.built_so_dir - .join("hyperlane_sealevel_multisig_ism_message_id.so") - .to_str() - .unwrap(), - &ctx.client.url(), - ); - - println!( - "Deployed Multisig ISM Message ID at program ID {}", - program_id - ); - - // Initialize - let instruction = hyperlane_sealevel_multisig_ism_message_id::instruction::init_instruction( - program_id, - ctx.payer_pubkey, - ) - .unwrap(); - - ctx.new_txn().add(instruction).send_with_payer(); - - println!("Initialized Multisig ISM Message ID "); - - program_id -} - fn deploy_mailbox( ctx: &mut Context, core: &CoreDeploy, @@ -325,55 +295,23 @@ fn deploy_igp(ctx: &mut Context, core: &CoreDeploy, key_dir: &Path) -> (Pubkey, } #[derive(Debug, Serialize, Deserialize)] -pub(crate) struct CoreProgramIds { - #[serde(with = "serde_pubkey")] +pub struct CoreProgramIds { + #[serde(with = "crate::serde::serde_pubkey")] pub mailbox: Pubkey, - #[serde(with = "serde_pubkey")] + #[serde(with = "crate::serde::serde_pubkey")] pub validator_announce: Pubkey, - #[serde(with = "serde_pubkey")] + #[serde(with = "crate::serde::serde_pubkey")] pub multisig_ism_message_id: Pubkey, - #[serde(with = "serde_pubkey")] + #[serde(with = "crate::serde::serde_pubkey")] pub igp_program_id: Pubkey, - #[serde(with = "serde_pubkey")] + #[serde(with = "crate::serde::serde_pubkey")] pub overhead_igp_account: Pubkey, - #[serde(with = "serde_pubkey")] + #[serde(with = "crate::serde::serde_pubkey")] pub igp_account: Pubkey, } -mod serde_pubkey { - use borsh::BorshDeserialize; - use serde::{Deserialize, Deserializer, Serializer}; - use solana_sdk::pubkey::Pubkey; - use std::str::FromStr; - - #[derive(Deserialize)] - #[serde(untagged)] - enum RawPubkey { - String(String), - Bytes(Vec), - } - - pub fn serialize(k: &Pubkey, ser: S) -> Result { - ser.serialize_str(&k.to_string()) - } - - pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result { - match RawPubkey::deserialize(de)? { - RawPubkey::String(s) => Pubkey::from_str(&s).map_err(serde::de::Error::custom), - RawPubkey::Bytes(b) => Pubkey::try_from_slice(&b).map_err(serde::de::Error::custom), - } - } -} - fn write_program_ids(core_dir: &Path, program_ids: CoreProgramIds) { - let json = serde_json::to_string_pretty(&program_ids).unwrap(); - let path = core_dir.join("program-ids.json"); - - println!("Writing program IDs to {}:\n{}", path.display(), json); - - let mut file = File::create(path).expect("Failed to create keypair file"); - file.write_all(json.as_bytes()) - .expect("Failed to write program IDs to file"); + write_json(&core_dir.join("program-ids.json"), program_ids); } pub(crate) fn read_core_program_ids( @@ -386,6 +324,5 @@ pub(crate) fn read_core_program_ids( .join(chain) .join("core") .join("program-ids.json"); - let file = File::open(path).expect("Failed to open program IDs file"); - serde_json::from_reader(file).expect("Failed to read program IDs file") + read_json(&path) } diff --git a/rust/sealevel/client/src/helloworld.rs b/rust/sealevel/client/src/helloworld.rs new file mode 100644 index 0000000000..6226b48282 --- /dev/null +++ b/rust/sealevel/client/src/helloworld.rs @@ -0,0 +1,197 @@ +use std::collections::HashMap; + +use hyperlane_core::H256; +use hyperlane_sealevel_connection_client::router::RemoteRouterConfig; +use hyperlane_sealevel_hello_world::{ + accounts::{HelloWorldStorage, HelloWorldStorageAccount}, + instruction::{ + enroll_remote_routers_instruction, init_instruction, + set_interchain_security_module_instruction, + }, + program_storage_pda_seeds, +}; +use serde::{Deserialize, Serialize}; +use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; + +use crate::{ + cmd_utils::account_exists, + router::{ + deploy_routers, ChainMetadata, ConnectionClient, Ownable, RouterConfig, RouterConfigGetter, + RouterDeployer, + }, + Context, CoreProgramIds, HelloWorldCmd, HelloWorldDeploy, HelloWorldSubCmd, RpcClient, +}; + +pub(crate) fn process_helloworld_cmd(mut ctx: Context, cmd: HelloWorldCmd) { + match cmd.cmd { + HelloWorldSubCmd::Deploy(deploy) => { + deploy_helloworld(&mut ctx, deploy); + } + HelloWorldSubCmd::Query(query) => { + let program_storage_key = + Pubkey::find_program_address(program_storage_pda_seeds!(), &query.program_id); + let account = ctx.client.get_account(&program_storage_key.0).unwrap(); + let storage = HelloWorldStorageAccount::fetch(&mut &account.data[..]) + .unwrap() + .into_inner(); + println!("HelloWorld storage: {:?}", storage); + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct HelloWorldConfig { + #[serde(flatten)] + router_config: RouterConfig, +} + +struct HelloWorldDeployer {} + +impl HelloWorldDeployer { + fn new() -> Self { + Self {} + } + + fn get_storage(&self, client: &RpcClient, program_id: &Pubkey) -> HelloWorldStorage { + let (program_storage_account, _program_storage_bump) = + Pubkey::find_program_address(program_storage_pda_seeds!(), program_id); + + let account = client.get_account(&program_storage_account).unwrap(); + *HelloWorldStorageAccount::fetch(&mut &account.data[..]) + .unwrap() + .into_inner() + } +} + +impl RouterDeployer for HelloWorldDeployer { + fn program_name(&self, _config: &HelloWorldConfig) -> &str { + "hyperlane_sealevel_hello_world" + } + + fn enroll_remote_routers_instruction( + &self, + program_id: Pubkey, + payer: Pubkey, + router_configs: Vec, + ) -> Instruction { + enroll_remote_routers_instruction(program_id, payer, router_configs).unwrap() + } + + fn get_routers(&self, client: &RpcClient, program_id: &Pubkey) -> HashMap { + let storage = self.get_storage(client, program_id); + + storage.routers + } + + fn init_program_idempotent( + &self, + ctx: &mut Context, + client: &RpcClient, + core_program_ids: &CoreProgramIds, + chain_config: &ChainMetadata, + app_config: &HelloWorldConfig, + program_id: Pubkey, + ) { + let (program_storage_account, _program_storage_bump) = + Pubkey::find_program_address(program_storage_pda_seeds!(), &program_id); + if account_exists(client, &program_storage_account).unwrap() { + println!("HelloWorld storage already exists, skipping init"); + return; + } + + let domain_id = chain_config.domain_id(); + let mailbox = app_config + .router_config() + .connection_client + .mailbox(core_program_ids.mailbox); + let ism = app_config + .router_config() + .connection_client + .interchain_security_module(); + let owner = Some(app_config.router_config().ownable.owner(ctx.payer_pubkey)); + + ctx.new_txn() + .add_with_description( + init_instruction( + program_id, + ctx.payer_pubkey, + domain_id, + mailbox, + ism, + // TODO revisit this when we want to deploy with IGPs + None, + owner, + ) + .unwrap(), + format!( + "Initializing HelloWorld program: domain_id: {}, mailbox: {}, ism: {:?}, owner: {:?}", + domain_id, mailbox, ism, owner + ) + ) + .with_client(client) + .send_with_payer(); + } +} + +impl RouterConfigGetter for HelloWorldConfig { + fn router_config(&self) -> &RouterConfig { + &self.router_config + } +} + +impl Ownable for HelloWorldDeployer { + /// Gets the owner configured on-chain. + fn get_owner(&self, client: &RpcClient, program_id: &Pubkey) -> Option { + let storage = self.get_storage(client, program_id); + + storage.owner + } + + /// Gets an instruction to set the owner. + fn set_owner_instruction( + &self, + _client: &RpcClient, + _program_id: &Pubkey, + _new_owner: Option, + ) -> Instruction { + unimplemented!("HelloWorld does not support changing the owner") + } +} + +impl ConnectionClient for HelloWorldDeployer { + fn get_interchain_security_module( + &self, + client: &RpcClient, + program_id: &Pubkey, + ) -> Option { + let storage = self.get_storage(client, program_id); + + storage.ism + } + + fn set_interchain_security_module_instruction( + &self, + client: &RpcClient, + program_id: &Pubkey, + ism: Option, + ) -> Instruction { + let storage = self.get_storage(client, program_id); + + set_interchain_security_module_instruction(*program_id, storage.owner.unwrap(), ism) + .unwrap() + } +} + +fn deploy_helloworld(ctx: &mut Context, deploy: HelloWorldDeploy) { + deploy_routers( + ctx, + HelloWorldDeployer::new(), + "helloworld", + &deploy.context, + deploy.config_file, + deploy.chain_config_file, + deploy.environments_dir, + &deploy.environment, + deploy.built_so_dir, + ) +} diff --git a/rust/sealevel/client/src/main.rs b/rust/sealevel/client/src/main.rs index 29b8050f10..bef6cc2095 100644 --- a/rust/sealevel/client/src/main.rs +++ b/rust/sealevel/client/src/main.rs @@ -20,7 +20,7 @@ use solana_sdk::{ }; use account_utils::DiscriminatorEncode; -use hyperlane_core::{Encode, HyperlaneMessage, H160, H256}; +use hyperlane_core::{H160, H256}; use hyperlane_sealevel_connection_client::router::RemoteRouterConfig; use hyperlane_sealevel_igp::{ accounts::{ @@ -32,19 +32,12 @@ use hyperlane_sealevel_igp::{ }; use hyperlane_sealevel_mailbox::{ accounts::{InboxAccount, OutboxAccount}, - instruction::{InboxProcess, Instruction as MailboxInstruction, OutboxDispatch, VERSION}, + instruction::{Instruction as MailboxInstruction, OutboxDispatch}, mailbox_dispatched_message_pda_seeds, mailbox_inbox_pda_seeds, mailbox_message_dispatch_authority_pda_seeds, mailbox_outbox_pda_seeds, mailbox_processed_message_pda_seeds, spl_noop, }; -use hyperlane_sealevel_multisig_ism_message_id::{ - access_control_pda_seeds as multisig_ism_message_id_access_control_pda_seeds, - accounts::AccessControlAccount, - domain_data_pda_seeds as multisig_ism_message_id_domain_data_pda_seeds, - instruction::{ - Domained, Instruction as MultisigIsmMessageIdInstruction, ValidatorsAndThreshold, - }, -}; + use hyperlane_sealevel_token::{ hyperlane_token_ata_payer_pda_seeds, hyperlane_token_mint_pda_seeds, spl_associated_token_account::get_associated_token_address_with_program_id, spl_token_2022, @@ -69,14 +62,21 @@ use hyperlane_sealevel_validator_announce::{ }; use warp_route::parse_token_account_data; -use crate::warp_route::process_warp_route_cmd; -pub(crate) use crate::{context::*, core::*}; - +mod artifacts; mod cmd_utils; mod context; mod r#core; +mod helloworld; +mod multisig_ism; +mod router; +mod serde; mod warp_route; +use crate::helloworld::process_helloworld_cmd; +use crate::multisig_ism::process_multisig_ism_message_id_cmd; +use crate::warp_route::process_warp_route_cmd; +pub(crate) use crate::{context::*, core::*}; + // Note: from solana_program_runtime::compute_budget const DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT: u32 = 200_000; const MAX_COMPUTE_UNIT_LIMIT: u32 = 1_400_000; @@ -99,6 +99,8 @@ struct Cli { heap_size: Option, #[arg(long, short = 'C')] config: Option, + #[arg(long, default_value_t = false)] + require_tx_approval: bool, } #[derive(Subcommand)] @@ -110,6 +112,7 @@ enum HyperlaneSealevelCmd { ValidatorAnnounce(ValidatorAnnounceCmd), MultisigIsmMessageId(MultisigIsmMessageIdCmd), WarpRoute(WarpRouteCmd), + HelloWorld(HelloWorldCmd), } #[derive(Args)] @@ -194,9 +197,9 @@ enum MailboxSubCmd { Init(Init), Query(Query), Send(Outbox), - Receive(Inbox), Delivered(Delivered), TransferOwnership(TransferOwnership), + SetDefaultIsm(SetDefaultIsm), } const MAILBOX_PROG_ID: Pubkey = pubkey!("692KZJaoe2KRcD6uhCQDLLXnLNA5ZLnfvdqjE4aX9iu1"); @@ -221,6 +224,14 @@ struct Query { program_id: Pubkey, } +#[derive(Args)] +struct SetDefaultIsm { + #[arg(long, short)] + program_id: Pubkey, + #[arg(long, short)] + default_ism: Pubkey, +} + #[derive(Args)] struct Outbox { #[arg(long, short, default_value_t = ECLIPSE_DOMAIN)] @@ -259,32 +270,6 @@ struct Delivered { message_id: H256, } -// Actual content depends on which ISM is used. -struct ExampleMetadata { - pub root: H256, - pub index: u32, - pub leaf_index: u32, - // pub proof: [H256; 32], - pub signatures: Vec, -} -impl Encode for ExampleMetadata { - fn write_to(&self, writer: &mut W) -> std::io::Result - where - W: std::io::Write, - { - writer.write_all(self.root.as_ref())?; - writer.write_all(&self.index.to_be_bytes())?; - writer.write_all(&self.leaf_index.to_be_bytes())?; - // for hash in self.proof { - // writer.write_all(hash.as_ref())?; - // } - for signature in &self.signatures { - writer.write_all(signature.as_ref())?; - } - Ok(32 + 4 + 4 + (32 * 32) + (self.signatures.len() * 32)) - } -} - #[derive(Args)] struct TokenCmd { #[command(subcommand)] @@ -297,6 +282,7 @@ enum TokenSubCmd { TransferRemote(TokenTransferRemote), EnrollRemoteRouter(TokenEnrollRemoteRouter), TransferOwnership(TransferOwnership), + SetInterchainSecurityModule(SetInterchainSecurityModule), Igp(Igp), } @@ -337,6 +323,14 @@ struct TokenEnrollRemoteRouter { router: H256, } +#[derive(Args)] +struct SetInterchainSecurityModule { + #[arg(long, short)] + program_id: Pubkey, + #[arg(long, short)] + ism: Option, +} + #[derive(Args)] struct TransferOwnership { #[arg(long, short)] @@ -531,10 +525,36 @@ struct MultisigIsmMessageIdCmd { #[derive(Subcommand)] enum MultisigIsmMessageIdSubCmd { + Deploy(MultisigIsmMessageIdDeploy), Init(MultisigIsmMessageIdInit), SetValidatorsAndThreshold(MultisigIsmMessageIdSetValidatorsAndThreshold), - Query(MultisigIsmMessageIdInit), + Query(MultisigIsmMessageIdQuery), TransferOwnership(TransferOwnership), + Configure(MultisigIsmMessageIdConfigure), +} + +#[derive(Args)] +struct MultisigIsmMessageIdDeploy { + #[arg(long)] + environment: String, + #[arg(long)] + environments_dir: PathBuf, + #[arg(long)] + built_so_dir: PathBuf, + #[arg(long)] + chain: String, + #[arg(long)] + context: String, +} + +#[derive(Args)] +struct MultisigIsmMessageIdConfigure { + #[arg(long)] + program_id: Pubkey, + #[arg(long)] + multisig_config_file: PathBuf, + #[arg(long)] + chain_config_file: PathBuf, } #[derive(Args)] @@ -543,6 +563,14 @@ struct MultisigIsmMessageIdInit { program_id: Pubkey, } +#[derive(Args)] +struct MultisigIsmMessageIdQuery { + #[arg(long, short)] + program_id: Pubkey, + #[arg(long, value_delimiter = ',')] + domains: Option>, +} + #[derive(Args)] struct MultisigIsmMessageIdSetValidatorsAndThreshold { #[arg(long, short, default_value_t = MULTISIG_ISM_MESSAGE_ID_PROG_ID)] @@ -555,6 +583,40 @@ struct MultisigIsmMessageIdSetValidatorsAndThreshold { threshold: u8, } +#[derive(Args)] +pub(crate) struct HelloWorldCmd { + #[command(subcommand)] + cmd: HelloWorldSubCmd, +} + +#[derive(Subcommand)] +pub(crate) enum HelloWorldSubCmd { + Deploy(HelloWorldDeploy), + Query(HelloWorldQuery), +} + +#[derive(Args)] +pub(crate) struct HelloWorldDeploy { + #[arg(long)] + environment: String, + #[arg(long)] + environments_dir: PathBuf, + #[arg(long)] + built_so_dir: PathBuf, + #[arg(long)] + config_file: PathBuf, + #[arg(long)] + chain_config_file: PathBuf, + #[arg(long)] + context: String, +} + +#[derive(Args)] +pub(crate) struct HelloWorldQuery { + #[arg(long)] + program_id: Pubkey, +} + fn main() { pretty_env_logger::init(); @@ -617,6 +679,7 @@ fn main() { payer_keypair, commitment, instructions.into(), + cli.require_tx_approval, ); match cli.cmd { HyperlaneSealevelCmd::Mailbox(cmd) => process_mailbox_cmd(ctx, cmd), @@ -627,6 +690,7 @@ fn main() { } HyperlaneSealevelCmd::Core(cmd) => process_core_cmd(ctx, cmd), HyperlaneSealevelCmd::WarpRoute(cmd) => process_warp_route_cmd(ctx, cmd), + HyperlaneSealevelCmd::HelloWorld(cmd) => process_helloworld_cmd(ctx, cmd), HyperlaneSealevelCmd::Igp(cmd) => process_igp_cmd(ctx, cmd), } } @@ -690,7 +754,6 @@ fn process_mailbox_cmd(ctx: Context, cmd: MailboxCmd) { destination_domain: outbox.destination, recipient: H256(outbox.recipient.to_bytes()), message_body: outbox.message.into(), - // message_body: std::iter::repeat(0x41).take(outbox.message_len).collect(), }); let outbox_instruction = Instruction { program_id: outbox.program_id, @@ -703,50 +766,6 @@ fn process_mailbox_cmd(ctx: Context, cmd: MailboxCmd) { }; ctx.new_txn().add(outbox_instruction).send_with_payer(); } - MailboxSubCmd::Receive(inbox) => { - // TODO this probably needs some love - - let (inbox_account, _inbox_bump) = - Pubkey::find_program_address(mailbox_inbox_pda_seeds!(), &inbox.program_id); - let hyperlane_message = HyperlaneMessage { - version: VERSION, - nonce: inbox.nonce, - origin: inbox.origin, - sender: H256::repeat_byte(123), - destination: inbox.local_domain, - recipient: H256::from(inbox.recipient.to_bytes()), - body: inbox.message.bytes().collect(), - }; - let mut encoded_message = vec![]; - hyperlane_message.write_to(&mut encoded_message).unwrap(); - let metadata = ExampleMetadata { - root: Default::default(), - index: 1, - leaf_index: 0, - // proof: Default::default(), - signatures: vec![], - }; - let mut encoded_metadata = vec![]; - metadata.write_to(&mut encoded_metadata).unwrap(); - - let ixn = MailboxInstruction::InboxProcess(InboxProcess { - metadata: encoded_metadata, - message: encoded_message, - }); - let inbox_instruction = Instruction { - program_id: inbox.program_id, - data: ixn.into_instruction_data().unwrap(), - accounts: vec![ - AccountMeta::new(inbox_account, false), - AccountMeta::new_readonly(spl_noop::id(), false), - AccountMeta::new_readonly(inbox.ism, false), - AccountMeta::new_readonly(inbox.recipient, false), - // Note: we would have to provide ism accounts and recipient accounts here if - // they were to use other accounts. - ], - }; - ctx.new_txn().add(inbox_instruction).send_with_payer(); - } MailboxSubCmd::Delivered(delivered) => { let (processed_message_account_key, _processed_message_account_bump) = Pubkey::find_program_address( @@ -779,6 +798,20 @@ fn process_mailbox_cmd(ctx: Context, cmd: MailboxCmd) { ) .send_with_payer(); } + MailboxSubCmd::SetDefaultIsm(set_default_ism) => { + let instruction = hyperlane_sealevel_mailbox::instruction::set_default_ism_instruction( + set_default_ism.program_id, + ctx.payer_pubkey, + set_default_ism.default_ism, + ) + .unwrap(); + ctx.new_txn() + .add_with_description( + instruction, + format!("Setting default ISM to {}", set_default_ism.default_ism), + ) + .send_with_payer(); + } }; } @@ -1131,6 +1164,19 @@ fn process_token_cmd(ctx: Context, cmd: TokenCmd) { ) .send_with_payer(); } + TokenSubCmd::SetInterchainSecurityModule(set_ism) => { + let instruction = + hyperlane_sealevel_token_lib::instruction::set_interchain_security_module_instruction( + set_ism.program_id, + ctx.payer_pubkey, + set_ism.ism + ) + .unwrap(); + + ctx.new_txn() + .add_with_description(instruction, format!("Set ISM to {:?}", set_ism.ism)) + .send_with_payer(); + } TokenSubCmd::Igp(args) => match args.cmd { GetSetCmd::Set(set_args) => { let igp_type: InterchainGasPaymasterType = match set_args.igp_type { @@ -1268,91 +1314,6 @@ fn process_validator_announce_cmd(ctx: Context, cmd: ValidatorAnnounceCmd) { } } -fn process_multisig_ism_message_id_cmd(ctx: Context, cmd: MultisigIsmMessageIdCmd) { - match cmd.cmd { - MultisigIsmMessageIdSubCmd::Init(init) => { - let init_instruction = - hyperlane_sealevel_multisig_ism_message_id::instruction::init_instruction( - init.program_id, - ctx.payer_pubkey, - ) - .unwrap(); - ctx.new_txn().add(init_instruction).send_with_payer(); - } - MultisigIsmMessageIdSubCmd::SetValidatorsAndThreshold(set_config) => { - let (access_control_pda_key, _access_control_pda_bump) = Pubkey::find_program_address( - multisig_ism_message_id_access_control_pda_seeds!(), - &set_config.program_id, - ); - - let (domain_data_pda_key, _domain_data_pda_bump) = Pubkey::find_program_address( - multisig_ism_message_id_domain_data_pda_seeds!(set_config.domain), - &set_config.program_id, - ); - - let ixn = MultisigIsmMessageIdInstruction::SetValidatorsAndThreshold(Domained { - domain: set_config.domain, - data: ValidatorsAndThreshold { - validators: set_config.validators, - threshold: set_config.threshold, - }, - }); - - // Accounts: - // 0. `[signer]` The access control owner and payer of the domain PDA. - // 1. `[]` The access control PDA account. - // 2. `[writable]` The PDA relating to the provided domain. - // 3. `[executable]` OPTIONAL - The system program account. Required if creating the domain PDA. - let accounts = vec![ - AccountMeta::new(ctx.payer_pubkey, true), - AccountMeta::new_readonly(access_control_pda_key, false), - AccountMeta::new(domain_data_pda_key, false), - AccountMeta::new_readonly(system_program::id(), false), - ]; - - let set_instruction = Instruction { - program_id: set_config.program_id, - data: ixn.encode().unwrap(), - accounts, - }; - ctx.new_txn().add(set_instruction).send_with_payer(); - } - MultisigIsmMessageIdSubCmd::Query(query) => { - let (access_control_pda_key, _access_control_pda_bump) = Pubkey::find_program_address( - multisig_ism_message_id_access_control_pda_seeds!(), - &query.program_id, - ); - - let accounts = ctx - .client - .get_multiple_accounts_with_commitment(&[access_control_pda_key], ctx.commitment) - .unwrap() - .value; - let access_control = - AccessControlAccount::fetch(&mut &accounts[0].as_ref().unwrap().data[..]) - .unwrap() - .into_inner(); - println!("Access control: {:#?}", access_control); - } - MultisigIsmMessageIdSubCmd::TransferOwnership(transfer_ownership) => { - let instruction = - hyperlane_sealevel_multisig_ism_message_id::instruction::transfer_ownership_instruction( - transfer_ownership.program_id, - ctx.payer_pubkey, - Some(transfer_ownership.new_owner), - ) - .unwrap(); - - ctx.new_txn() - .add_with_description( - instruction, - format!("Transfer ownership to {}", transfer_ownership.new_owner), - ) - .send_with_payer(); - } - } -} - fn process_igp_cmd(ctx: Context, cmd: IgpCmd) { match cmd.cmd { IgpSubCmd::Query(query) => { diff --git a/rust/sealevel/client/src/multisig_ism.rs b/rust/sealevel/client/src/multisig_ism.rs new file mode 100644 index 0000000000..d5676b2815 --- /dev/null +++ b/rust/sealevel/client/src/multisig_ism.rs @@ -0,0 +1,312 @@ +use std::collections::{HashMap, HashSet}; +use std::{fs::File, path::Path}; + +use serde::{Deserialize, Serialize}; +use solana_program::pubkey::Pubkey; +use solana_sdk::signature::Signer; + +use crate::{ + artifacts::{write_json, SingularProgramIdArtifact}, + cmd_utils::{create_and_write_keypair, create_new_directory, deploy_program}, + router::ChainMetadata, + Context, MultisigIsmMessageIdCmd, MultisigIsmMessageIdSubCmd, +}; + +use hyperlane_core::H160; + +use hyperlane_sealevel_multisig_ism_message_id::{ + access_control_pda_seeds, + accounts::{AccessControlAccount, DomainDataAccount}, + domain_data_pda_seeds, + instruction::{set_validators_and_threshold_instruction, ValidatorsAndThreshold}, +}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct MultisigIsmConfig { + /// Note this type is ignored in this tooling. It'll always assume this + /// relates to a multisig-ism-message-id variant, which is the only type + /// implemented in Sealevel. + #[serde(rename = "type")] + pub module_type: u8, + pub validators: Vec, + pub threshold: u8, +} + +impl From for ValidatorsAndThreshold { + fn from(val: MultisigIsmConfig) -> Self { + ValidatorsAndThreshold { + validators: val.validators, + threshold: val.threshold, + } + } +} + +pub(crate) fn process_multisig_ism_message_id_cmd(mut ctx: Context, cmd: MultisigIsmMessageIdCmd) { + match cmd.cmd { + MultisigIsmMessageIdSubCmd::Deploy(deploy) => { + let environments_dir = + create_new_directory(&deploy.environments_dir, &deploy.environment); + let ism_dir = create_new_directory(&environments_dir, "multisig-ism-message-id"); + let chain_dir = create_new_directory(&ism_dir, &deploy.chain); + let context_dir = create_new_directory(&chain_dir, &deploy.context); + let key_dir = create_new_directory(&context_dir, "keys"); + + let ism_program_id = + deploy_multisig_ism_message_id(&mut ctx, &deploy.built_so_dir, true, &key_dir); + + write_json::( + &context_dir.join("program-ids.json"), + ism_program_id.into(), + ); + } + MultisigIsmMessageIdSubCmd::Init(init) => { + let init_instruction = + hyperlane_sealevel_multisig_ism_message_id::instruction::init_instruction( + init.program_id, + ctx.payer_pubkey, + ) + .unwrap(); + ctx.new_txn().add(init_instruction).send_with_payer(); + } + MultisigIsmMessageIdSubCmd::SetValidatorsAndThreshold(set_config) => { + set_validators_and_threshold( + &mut ctx, + set_config.program_id, + set_config.domain, + ValidatorsAndThreshold { + validators: set_config.validators, + threshold: set_config.threshold, + }, + ); + } + MultisigIsmMessageIdSubCmd::Query(query) => { + let (access_control_pda_key, _access_control_pda_bump) = + Pubkey::find_program_address(access_control_pda_seeds!(), &query.program_id); + + let accounts = ctx + .client + .get_multiple_accounts_with_commitment(&[access_control_pda_key], ctx.commitment) + .unwrap() + .value; + let access_control = + AccessControlAccount::fetch(&mut &accounts[0].as_ref().unwrap().data[..]) + .unwrap() + .into_inner(); + println!("Access control: {:#?}", access_control); + + if let Some(domains) = query.domains { + for domain in domains { + println!("Querying domain data for origin domain: {}", domain); + + let (domain_data_pda_key, _domain_data_pda_bump) = Pubkey::find_program_address( + domain_data_pda_seeds!(domain), + &query.program_id, + ); + + let accounts = ctx + .client + .get_multiple_accounts_with_commitment( + &[domain_data_pda_key], + ctx.commitment, + ) + .unwrap() + .value; + + if let Some(account) = &accounts[0] { + let domain_data = DomainDataAccount::fetch(&mut &account.data[..]) + .unwrap() + .into_inner(); + println!("Domain data for {}:\n{:#?}", domain, domain_data); + } else { + println!("No domain data for domain {}", domain); + } + } + } + } + MultisigIsmMessageIdSubCmd::TransferOwnership(transfer_ownership) => { + let instruction = + hyperlane_sealevel_multisig_ism_message_id::instruction::transfer_ownership_instruction( + transfer_ownership.program_id, + ctx.payer_pubkey, + Some(transfer_ownership.new_owner), + ) + .unwrap(); + + ctx.new_txn() + .add_with_description( + instruction, + format!("Transfer ownership to {}", transfer_ownership.new_owner), + ) + .send_with_payer(); + } + MultisigIsmMessageIdSubCmd::Configure(configure) => { + configure_multisig_ism_message_id( + &mut ctx, + configure.program_id, + &configure.multisig_config_file, + &configure.chain_config_file, + ); + } + } +} + +pub(crate) fn deploy_multisig_ism_message_id( + ctx: &mut Context, + built_so_dir: &Path, + use_existing_keys: bool, + key_dir: &Path, +) -> Pubkey { + let (keypair, keypair_path) = create_and_write_keypair( + key_dir, + "hyperlane_sealevel_multisig_ism_message_id-keypair.json", + use_existing_keys, + ); + let program_id = keypair.pubkey(); + + deploy_program( + ctx.payer_keypair_path(), + keypair_path.to_str().unwrap(), + built_so_dir + .join("hyperlane_sealevel_multisig_ism_message_id.so") + .to_str() + .unwrap(), + &ctx.client.url(), + ); + + println!( + "Deployed Multisig ISM Message ID at program ID {}", + program_id + ); + + // Initialize + let instruction = hyperlane_sealevel_multisig_ism_message_id::instruction::init_instruction( + program_id, + ctx.payer_pubkey, + ) + .unwrap(); + + ctx.new_txn() + .add_with_description( + instruction, + format!( + "Initializing Multisig ISM Message ID with payer & owner {}", + ctx.payer_pubkey + ), + ) + .send_with_payer(); + + program_id +} + +/// Configures the multisig-ism-message-id program +/// with the validators and thresholds for each of the domains +/// specified in the multisig config file. +fn configure_multisig_ism_message_id( + ctx: &mut Context, + program_id: Pubkey, + multisig_config_file_path: &Path, + chain_config_path: &Path, +) { + let multisig_config_file = + File::open(multisig_config_file_path).expect("Failed to open config file"); + let multisig_configs: HashMap = + serde_json::from_reader(multisig_config_file).expect("Failed to read config file"); + + let chain_config_file = File::open(chain_config_path).unwrap(); + let chain_configs: HashMap = + serde_json::from_reader(chain_config_file).unwrap(); + + for (chain_name, multisig_ism_config) in multisig_configs { + println!( + "Configuring Multisig ISM Message ID for chain {} and config {:?}", + chain_name, multisig_ism_config + ); + let chain_config = chain_configs.get(&chain_name).unwrap(); + + let matches = multisig_ism_config_matches_chain( + ctx, + program_id, + chain_config.domain_id(), + &multisig_ism_config, + ); + + if matches { + println!( + "Multisig ISM Message ID already correctly configured for chain {}", + chain_name + ); + } else { + println!( + "Multisig ISM Message ID incorrectly configured for chain {}, configuring now", + chain_name + ); + set_validators_and_threshold( + ctx, + program_id, + chain_config.domain_id(), + multisig_ism_config.into(), + ); + } + } +} + +fn multisig_ism_config_matches_chain( + ctx: &mut Context, + program_id: Pubkey, + remote_domain: u32, + expected: &MultisigIsmConfig, +) -> bool { + let (domain_data_key, _domain_data_bump) = + Pubkey::find_program_address(domain_data_pda_seeds!(remote_domain), &program_id); + + let domain_data_account = ctx + .client + .get_account_with_commitment(&domain_data_key, ctx.commitment) + .expect("Failed to get domain data account") + .value; + + if let Some(domain_data_account) = domain_data_account { + let domain_data = DomainDataAccount::fetch(&mut &domain_data_account.data[..]) + .unwrap() + .into_inner(); + let expected_validator_set = + HashSet::::from_iter(expected.validators.iter().cloned()); + let actual_validator_set = HashSet::::from_iter( + domain_data + .validators_and_threshold + .validators + .iter() + .cloned(), + ); + + expected_validator_set == actual_validator_set + && expected.threshold == domain_data.validators_and_threshold.threshold + } else { + false + } +} + +pub(crate) fn set_validators_and_threshold( + ctx: &mut Context, + program_id: Pubkey, + domain: u32, + validators_and_threshold: ValidatorsAndThreshold, +) { + let description = format!( + "Set for remote domain {} validators and threshold: {:?}", + domain, validators_and_threshold + ); + ctx.new_txn() + .add_with_description( + set_validators_and_threshold_instruction( + program_id, + ctx.payer_pubkey, + domain, + validators_and_threshold, + ) + .unwrap(), + description, + ) + .send_with_payer(); +} diff --git a/rust/sealevel/client/src/router.rs b/rust/sealevel/client/src/router.rs new file mode 100644 index 0000000000..4eaf4cae78 --- /dev/null +++ b/rust/sealevel/client/src/router.rs @@ -0,0 +1,593 @@ +use hyperlane_core::{utils::hex_or_base58_to_h256, H256}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + fs::File, + path::{Path, PathBuf}, +}; + +use solana_client::rpc_client::RpcClient; +use solana_program::instruction::Instruction; +use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Signer}; + +use account_utils::DiscriminatorData; +use hyperlane_sealevel_connection_client::router::RemoteRouterConfig; +use hyperlane_sealevel_igp::accounts::{Igp, InterchainGasPaymasterType, OverheadIgp}; + +use crate::{ + artifacts::{write_json, HexAndBase58ProgramIdArtifact}, + cmd_utils::{create_and_write_keypair, create_new_directory, deploy_program_idempotent}, + read_core_program_ids, Context, CoreProgramIds, +}; + +/// Optional connection client configuration. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct OptionalConnectionClientConfig { + #[serde(default)] + #[serde(with = "crate::serde::serde_option_pubkey")] + mailbox: Option, + #[serde(default)] + #[serde(with = "crate::serde::serde_option_pubkey")] + interchain_gas_paymaster: Option, + #[serde(default)] + #[serde(with = "crate::serde::serde_option_pubkey")] + interchain_security_module: Option, +} + +impl OptionalConnectionClientConfig { + pub fn mailbox(&self, default: Pubkey) -> Pubkey { + self.mailbox.unwrap_or(default) + } + + pub fn interchain_security_module(&self) -> Option { + self.interchain_security_module + } + + /// Uses the configured IGP account, if Some, to get the IGP program ID + /// and generate a config of the form Some((program_id, Igp account)). + pub fn interchain_gas_paymaster_config( + &self, + client: &RpcClient, + ) -> Option<(Pubkey, InterchainGasPaymasterType)> { + if let Some(igp_pubkey) = self.interchain_gas_paymaster { + let account = client + .get_account(&self.interchain_gas_paymaster.unwrap()) + .unwrap(); + + match &account.data[1..9] { + Igp::DISCRIMINATOR_SLICE => { + Some((account.owner, InterchainGasPaymasterType::Igp(igp_pubkey))) + } + OverheadIgp::DISCRIMINATOR_SLICE => Some(( + account.owner, + InterchainGasPaymasterType::OverheadIgp(igp_pubkey), + )), + _ => { + panic!("Invalid IGP account configured {}", igp_pubkey); + } + } + } else { + None + } + } +} + +/// Optional ownable configuration. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct OptionalOwnableConfig { + #[serde(default)] + #[serde(with = "crate::serde::serde_option_pubkey")] + pub owner: Option, +} + +impl OptionalOwnableConfig { + pub fn owner(&self, default: Pubkey) -> Pubkey { + self.owner.unwrap_or(default) + } +} + +/// Router configuration. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RouterConfig { + // Kept as a string to allow for hex or base58 + pub foreign_deployment: Option, + #[serde(flatten)] + pub ownable: OptionalOwnableConfig, + #[serde(flatten)] + pub connection_client: OptionalConnectionClientConfig, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RpcUrlConfig { + pub http: String, +} + +/// An abridged version of the Typescript ChainMetadata +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ChainMetadata { + chain_id: u32, + /// Hyperlane domain, only required if differs from id above + domain_id: Option, + name: String, + /// Collection of RPC endpoints + rpc_urls: Vec, +} + +impl ChainMetadata { + pub fn client(&self) -> RpcClient { + RpcClient::new_with_commitment(self.rpc_urls[0].http.clone(), CommitmentConfig::confirmed()) + } + + pub fn domain_id(&self) -> u32 { + self.domain_id.unwrap_or(self.chain_id) + } +} + +pub trait RouterConfigGetter { + fn router_config(&self) -> &RouterConfig; +} + +pub(crate) trait RouterDeployer: + ConnectionClient +{ + #[allow(clippy::too_many_arguments)] + fn deploy( + &self, + ctx: &mut Context, + key_dir: &Path, + environments_dir: &Path, + environment: &str, + built_so_dir: &Path, + chain_config: &ChainMetadata, + app_config: &Config, + existing_program_ids: Option<&HashMap>, + ) -> Pubkey { + let program_name = self.program_name(app_config); + + println!( + "Attempting deploy {} on chain: {}\nApp config: {:?}", + program_name, chain_config.name, app_config + ); + + let program_id = existing_program_ids + .and_then(|existing_program_ids| { + existing_program_ids.get(&chain_config.name).and_then(|id| { + chain_config + .client() + .get_account_with_commitment(id, ctx.commitment) + .unwrap() + .value + .map(|_| { + println!("Recovered existing program id {}", id); + *id + }) + }) + }) + .unwrap_or_else(|| { + let (keypair, keypair_path) = create_and_write_keypair( + key_dir, + format!("{}-{}.json", program_name, chain_config.name).as_str(), + true, + ); + let program_id = keypair.pubkey(); + + deploy_program_idempotent( + ctx.payer_keypair_path(), + &keypair, + keypair_path.to_str().unwrap(), + built_so_dir + .join(format!("{}.so", program_name)) + .to_str() + .unwrap(), + &chain_config.rpc_urls[0].http, + ) + .unwrap(); + + program_id + }); + + let core_program_ids = + read_core_program_ids(environments_dir, environment, &chain_config.name); + self.init_program_idempotent( + ctx, + &chain_config.client(), + &core_program_ids, + chain_config, + app_config, + program_id, + ); + + program_id + } + + fn init_program_idempotent( + &self, + ctx: &mut Context, + client: &RpcClient, + core_program_ids: &CoreProgramIds, + chain_config: &ChainMetadata, + app_config: &Config, + program_id: Pubkey, + ); + + fn post_deploy( + &self, + _ctx: &mut Context, + _app_configs: &HashMap, + _app_configs_to_deploy: &HashMap<&String, &Config>, + _chain_configs: &HashMap, + _routers: &HashMap, + ) { + // By default, do nothing. + } + + /// The program's name, i.e. the name of the program's .so file (without the .so suffix) + /// and the name that will be used to create the keypair file + fn program_name(&self, config: &Config) -> &str; + + fn enroll_remote_routers_instruction( + &self, + program_id: Pubkey, + payer: Pubkey, + router_configs: Vec, + ) -> Instruction; + + fn get_routers(&self, rpc_client: &RpcClient, program_id: &Pubkey) -> HashMap; +} + +pub(crate) trait Ownable { + /// Gets the owner configured on-chain. + fn get_owner(&self, client: &RpcClient, program_id: &Pubkey) -> Option; + + /// Gets an instruction to set the owner. + fn set_owner_instruction( + &self, + client: &RpcClient, + program_id: &Pubkey, + new_owner: Option, + ) -> Instruction; +} + +pub(crate) trait ConnectionClient: Ownable { + /// Gets the interchain security module configured on-chain. + fn get_interchain_security_module( + &self, + client: &RpcClient, + program_id: &Pubkey, + ) -> Option; + + /// Gets an instruction to set the interchain security module. + fn set_interchain_security_module_instruction( + &self, + client: &RpcClient, + program_id: &Pubkey, + ism: Option, + ) -> Instruction; +} + +/// Idempotently deploys routers on multiple Sealevel chains and enrolls all routers (including +/// foreign deployments) on each Sealevel chain. +#[allow(clippy::too_many_arguments)] +pub(crate) fn deploy_routers< + Config: for<'a> Deserialize<'a> + RouterConfigGetter + std::fmt::Debug + Clone, + Deployer: RouterDeployer, +>( + ctx: &mut Context, + deployer: Deployer, + app_name: &str, + deploy_name: &str, + app_config_file_path: PathBuf, + chain_config_file_path: PathBuf, + environments_dir_path: PathBuf, + environment: &str, + built_so_dir_path: PathBuf, +) { + // Load the app configs from the app config file. + let app_config_file = File::open(app_config_file_path).unwrap(); + let app_configs: HashMap = serde_json::from_reader(app_config_file).unwrap(); + + // Load the chain configs from the chain config file. + let chain_config_file = File::open(chain_config_file_path).unwrap(); + let chain_configs: HashMap = + serde_json::from_reader(chain_config_file).unwrap(); + + let environments_dir = create_new_directory(&environments_dir_path, environment); + + let artifacts_dir = create_new_directory(&environments_dir, app_name); + let deploy_dir = create_new_directory(&artifacts_dir, deploy_name); + let keys_dir = create_new_directory(&deploy_dir, "keys"); + + let existing_program_ids = read_router_program_ids(&deploy_dir); + + // Builds a HashMap of all the foreign deployments from the app config. + // These domains with foreign deployments will not have any txs / deployments + // made directly to them, but the routers will be enrolled on the other chains. + let foreign_deployments = app_configs + .iter() + .filter_map(|(chain_name, app_config)| { + app_config + .router_config() + .foreign_deployment + .as_ref() + .map(|foreign_deployment| { + let chain_config = chain_configs.get(chain_name).unwrap(); + ( + chain_config.domain_id(), + hex_or_base58_to_h256(foreign_deployment).unwrap(), + ) + }) + }) + .collect::>(); + + // A map of all the routers, including the foreign deployments. + let mut routers: HashMap = foreign_deployments; + + // Non-foreign app configs to deploy to. + let app_configs_to_deploy = app_configs + .iter() + .filter(|(_, app_config)| app_config.router_config().foreign_deployment.is_none()) + .collect::>(); + + // Now we deploy to chains that don't have a foreign deployment + for (chain_name, app_config) in app_configs_to_deploy.iter() { + let chain_config = chain_configs + .get(*chain_name) + .unwrap_or_else(|| panic!("Chain config not found for chain: {}", chain_name)); + + if let Some(configured_owner) = app_config.router_config().ownable.owner { + if configured_owner != ctx.payer_pubkey { + println!("WARNING: Ownership transfer is not yet supported in this deploy tooling, ownership is granted to the payer account"); + } + } + + // Deploy - this is idempotent. + let program_id = deployer.deploy( + ctx, + &keys_dir, + &environments_dir_path, + environment, + &built_so_dir_path, + chain_config, + app_config, + existing_program_ids.as_ref(), + ); + + // Add the router to the list of routers. + routers.insert( + chain_config.domain_id(), + H256::from_slice(&program_id.to_bytes()[..]), + ); + + configure_connection_client( + ctx, + &deployer, + &program_id, + app_config.router_config(), + chain_config, + ); + + configure_owner( + ctx, + &deployer, + &program_id, + app_config.router_config(), + chain_config, + ); + } + + // Now enroll all the routers. + enroll_all_remote_routers( + &deployer, + ctx, + &app_configs_to_deploy, + &chain_configs, + &routers, + ); + + // Call the post-deploy hook. + deployer.post_deploy( + ctx, + &app_configs, + &app_configs_to_deploy, + &chain_configs, + &routers, + ); + + // Now write the program ids to a file! + let routers_by_name: HashMap = routers + .iter() + .map(|(domain_id, router)| { + ( + chain_configs + .iter() + .find(|(_, chain_config)| chain_config.domain_id() == *domain_id) + .unwrap() + .0 + .clone(), + *router, + ) + }) + .collect::>(); + write_router_program_ids(&deploy_dir, &routers_by_name); +} + +// Idempotent. +// TODO: This should really be brought out into some nicer abstraction, and we should +// also look for IGP inconsistency etc. +fn configure_connection_client( + ctx: &mut Context, + deployer: &impl ConnectionClient, + program_id: &Pubkey, + router_config: &RouterConfig, + chain_config: &ChainMetadata, +) { + // Just ISM for now + + let client = chain_config.client(); + + let actual_ism = deployer.get_interchain_security_module(&client, program_id); + let expected_ism = router_config.connection_client.interchain_security_module(); + + if actual_ism != expected_ism { + ctx.new_txn() + .add_with_description( + deployer.set_interchain_security_module_instruction( + &client, + program_id, + expected_ism, + ), + format!( + "Setting ISM for chain: {} ({}) to {:?}", + chain_config.name, + chain_config.domain_id(), + expected_ism + ), + ) + .with_client(&client) + .send_with_payer(); + } +} + +// Idempotent. +// TODO: This should really be brought out into some nicer abstraction +fn configure_owner( + ctx: &mut Context, + deployer: &impl ConnectionClient, + program_id: &Pubkey, + router_config: &RouterConfig, + chain_config: &ChainMetadata, +) { + let client = chain_config.client(); + + let actual_owner = deployer.get_owner(&client, program_id); + let expected_owner = Some(router_config.ownable.owner(ctx.payer_pubkey)); + + if actual_owner != expected_owner { + ctx.new_txn() + .add_with_description( + deployer.set_owner_instruction(&client, program_id, expected_owner), + format!( + "Setting owner for chain: {} ({}) to {:?}", + chain_config.name, + chain_config.domain_id(), + expected_owner, + ), + ) + .with_client(&client) + .send_with_payer(); + } +} + +/// For each chain in app_configs_to_deploy, enrolls all the remote routers. +/// Idempotent. +fn enroll_all_remote_routers< + Config: for<'a> Deserialize<'a> + RouterConfigGetter + std::fmt::Debug + Clone, +>( + deployer: &impl RouterDeployer, + ctx: &mut Context, + app_configs_to_deploy: &HashMap<&String, &Config>, + chain_configs: &HashMap, + routers: &HashMap, +) { + for (chain_name, _) in app_configs_to_deploy.iter() { + let chain_config = chain_configs + .get(*chain_name) + .unwrap_or_else(|| panic!("Chain config not found for chain: {}", chain_name)); + + let domain_id = chain_config.domain_id(); + let program_id: Pubkey = + Pubkey::new_from_array(*routers.get(&domain_id).unwrap().as_fixed_bytes()); + + let enrolled_routers = deployer.get_routers(&chain_config.client(), &program_id); + let expected_routers = routers + .iter() + .filter(|(router_domain_id, _)| *router_domain_id != &domain_id) + .map(|(domain, router)| { + ( + *domain, + RemoteRouterConfig { + domain: *domain, + router: Some(*router), + }, + ) + }) + .collect::>(); + + // Routers to enroll (or update to a Some value) + let routers_to_enroll = expected_routers + .iter() + .filter(|(domain, router_config)| { + enrolled_routers.get(domain) != router_config.router.as_ref() + }) + .map(|(_, router_config)| router_config.clone()); + + // Routers to remove + let routers_to_unenroll = enrolled_routers + .iter() + .filter(|(domain, _)| !expected_routers.contains_key(domain)) + .map(|(domain, _)| RemoteRouterConfig { + domain: *domain, + router: None, + }); + + // All router config changes + let router_configs = routers_to_enroll + .chain(routers_to_unenroll) + .collect::>(); + + if !router_configs.is_empty() { + println!( + "Enrolling routers for chain: {}, program_id {}, routers: {:?}", + chain_name, program_id, router_configs, + ); + + ctx.new_txn() + .add(deployer.enroll_remote_routers_instruction( + program_id, + ctx.payer_pubkey, + router_configs, + )) + .with_client(&chain_config.client()) + .send_with_payer(); + } else { + println!( + "No router changes for chain: {}, program_id {}", + chain_name, program_id + ); + } + } +} + +// Writes router program IDs as hex and base58. +fn write_router_program_ids(deploy_dir: &Path, routers: &HashMap) { + let serialized_program_ids = routers + .iter() + .map(|(chain_name, router)| (chain_name.clone(), (*router).into())) + .collect::>(); + + let program_ids_file = deploy_dir.join("program-ids.json"); + write_json(&program_ids_file, serialized_program_ids); +} + +fn read_router_program_ids(deploy_dir: &Path) -> Option> { + let program_ids_file = deploy_dir.join("program-ids.json"); + + if !program_ids_file.exists() { + return None; + } + + let serialized_program_ids: HashMap = + serde_json::from_reader(File::open(program_ids_file).unwrap()).unwrap(); + + let existing_program_ids = serialized_program_ids + .iter() + .map(|(chain_name, program_id)| (chain_name.clone(), program_id.into())) + .collect::>(); + + Some(existing_program_ids) +} diff --git a/rust/sealevel/client/src/serde.rs b/rust/sealevel/client/src/serde.rs new file mode 100644 index 0000000000..b73a25bd59 --- /dev/null +++ b/rust/sealevel/client/src/serde.rs @@ -0,0 +1,68 @@ +/// For serializing and deserializing Pubkey +pub(crate) mod serde_pubkey { + use borsh::BorshDeserialize; + use serde::{Deserialize, Deserializer, Serializer}; + use solana_sdk::pubkey::Pubkey; + use std::str::FromStr; + + #[derive(Deserialize)] + #[serde(untagged)] + enum RawPubkey { + String(String), + Bytes(Vec), + } + + pub fn serialize(k: &Pubkey, ser: S) -> Result { + ser.serialize_str(&k.to_string()) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result { + match RawPubkey::deserialize(de)? { + RawPubkey::String(s) => Pubkey::from_str(&s).map_err(serde::de::Error::custom), + RawPubkey::Bytes(b) => Pubkey::try_from_slice(&b).map_err(serde::de::Error::custom), + } + } +} + +/// For serializing and deserializing Option +pub(crate) mod serde_option_pubkey { + use borsh::BorshDeserialize; + use serde::{Deserialize, Deserializer, Serializer}; + use solana_sdk::pubkey::Pubkey; + use std::str::FromStr; + + #[derive(Deserialize)] + #[serde(untagged)] + enum RawPubkey { + String(String), + Bytes(Vec), + } + + pub fn serialize(k: &Option, ser: S) -> Result { + ser.serialize_str(&k.map(|k| k.to_string()).unwrap_or_default()) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result, D::Error> { + match Option::::deserialize(de)? { + Some(RawPubkey::String(s)) => { + if s.is_empty() { + Ok(None) + } else { + Pubkey::from_str(&s) + .map_err(serde::de::Error::custom) + .map(Some) + } + } + Some(RawPubkey::Bytes(b)) => { + if b.is_empty() { + Ok(None) + } else { + Pubkey::try_from_slice(&b) + .map_err(serde::de::Error::custom) + .map(Some) + } + } + None => Ok(None), + } + } +} diff --git a/rust/sealevel/client/src/warp_route.rs b/rust/sealevel/client/src/warp_route.rs index 6bd9cb69ea..5e67fcf379 100644 --- a/rust/sealevel/client/src/warp_route.rs +++ b/rust/sealevel/client/src/warp_route.rs @@ -1,12 +1,13 @@ -use hyperlane_core::{utils::hex_or_base58_to_h256, H256}; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_core::H256; use hyperlane_sealevel_token_collateral::plugin::CollateralPlugin; use hyperlane_sealevel_token_native::plugin::NativePlugin; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt::Debug, fs::File, path::Path, str::FromStr}; +use std::{collections::HashMap, fmt::Debug}; use solana_client::{client_error::ClientError, rpc_client::RpcClient}; -use solana_program::program_error::ProgramError; -use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Signer}; + +use solana_sdk::{instruction::Instruction, program_error::ProgramError, pubkey::Pubkey}; use hyperlane_sealevel_connection_client::{ gas_router::GasRouterConfig, router::RemoteRouterConfig, @@ -16,19 +17,25 @@ use hyperlane_sealevel_token::{ hyperlane_token_mint_pda_seeds, plugin::SyntheticPlugin, spl_token, spl_token_2022, }; use hyperlane_sealevel_token_lib::{ - accounts::HyperlaneTokenAccount, + accounts::{HyperlaneToken, HyperlaneTokenAccount}, hyperlane_token_pda_seeds, - instruction::{enroll_remote_routers_instruction, set_destination_gas_configs, Init}, + instruction::{ + enroll_remote_routers_instruction, set_destination_gas_configs, + set_interchain_security_module_instruction, transfer_ownership_instruction, Init, + }, }; use crate::{ - cmd_utils::{ - account_exists, create_and_write_keypair, create_new_directory, deploy_program_idempotent, + cmd_utils::account_exists, + core::CoreProgramIds, + router::{ + deploy_routers, ChainMetadata, ConnectionClient, Ownable, RouterConfig, RouterConfigGetter, + RouterDeployer, }, - core::{read_core_program_ids, CoreProgramIds}, Context, TokenType as FlatTokenType, WarpRouteCmd, WarpRouteSubCmd, }; +/// Configuration relating to decimals. #[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "camelCase")] struct DecimalMetadata { @@ -42,6 +49,7 @@ impl DecimalMetadata { } } +/// Configuration relating to a Warp Route token. #[derive(Clone, Serialize, Deserialize, Debug)] #[serde(tag = "type", rename_all = "camelCase")] enum TokenType { @@ -51,14 +59,6 @@ enum TokenType { } impl TokenType { - fn program_name(&self) -> &str { - match self { - TokenType::Native => "hyperlane_sealevel_token_native", - TokenType::Synthetic(_) => "hyperlane_sealevel_token", - TokenType::Collateral(_) => "hyperlane_sealevel_token_collateral", - } - } - // Borrowed from HypERC20Deployer's `gasOverheadDefault`. fn gas_overhead_default(&self) -> u64 { // TODO: note these are the amounts specific to the EVM. @@ -104,406 +104,361 @@ struct CollateralInfo { spl_token_program: Option, } -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -struct InterchainGasPaymasterConfig { - program_id: Pubkey, - igp_account: InterchainGasPaymasterType, -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -struct OptionalConnectionClientConfig { - mailbox: Option, - interchain_gas_paymaster: Option, - interchain_security_module: Option, -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -struct OptionalOwnableConfig { - owner: Option, -} - #[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "camelCase")] struct TokenConfig { #[serde(flatten)] token_type: TokenType, - foreign_deployment: Option, #[serde(flatten)] decimal_metadata: DecimalMetadata, #[serde(flatten)] - ownable: OptionalOwnableConfig, - #[serde(flatten)] - connection_client: OptionalConnectionClientConfig, + router_config: RouterConfig, } -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct RpcUrlConfig { - pub http: String, +pub(crate) fn process_warp_route_cmd(mut ctx: Context, cmd: WarpRouteCmd) { + match cmd.cmd { + WarpRouteSubCmd::Deploy(deploy) => { + deploy_routers( + &mut ctx, + WarpRouteDeployer::new(deploy.ata_payer_funding_amount), + "warp-routes", + &deploy.warp_route_name, + deploy.token_config_file, + deploy.chain_config_file, + deploy.environments_dir, + &deploy.environment, + deploy.built_so_dir, + ); + } + WarpRouteSubCmd::DestinationGas(args) => { + let destination_gas = get_destination_gas(&ctx.client, &args.program_id).unwrap(); + println!( + "Destination gas: {:?}", + destination_gas[&args.destination_domain] + ); + } + } } -/// An abridged version of the Typescript ChainMetadata -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ChainMetadata { - chain_id: u32, - /// Hyperlane domain, only required if differs from id above - domain_id: Option, - name: String, - /// Collection of RPC endpoints - public_rpc_urls: Vec, +struct WarpRouteDeployer { + ata_payer_funding_amount: Option, } -impl ChainMetadata { - fn client(&self) -> RpcClient { - RpcClient::new_with_commitment( - self.public_rpc_urls[0].http.clone(), - CommitmentConfig::confirmed(), - ) +impl WarpRouteDeployer { + fn new(ata_payer_funding_amount: Option) -> Self { + Self { + ata_payer_funding_amount, + } } +} + +impl WarpRouteDeployer {} - fn domain_id(&self) -> u32 { - self.domain_id.unwrap_or(self.chain_id) +impl RouterDeployer for WarpRouteDeployer { + fn program_name(&self, config: &TokenConfig) -> &str { + match config.token_type { + TokenType::Native => "hyperlane_sealevel_token_native", + TokenType::Synthetic(_) => "hyperlane_sealevel_token", + TokenType::Collateral(_) => "hyperlane_sealevel_token_collateral", + } } -} -pub(crate) fn process_warp_route_cmd(mut ctx: Context, cmd: WarpRouteCmd) { - match cmd.cmd { - WarpRouteSubCmd::Deploy(deploy) => { - let token_config_file = File::open(deploy.token_config_file).unwrap(); - let token_configs: HashMap = - serde_json::from_reader(token_config_file).unwrap(); + fn enroll_remote_routers_instruction( + &self, + program_id: Pubkey, + payer: Pubkey, + router_configs: Vec, + ) -> Instruction { + enroll_remote_routers_instruction(program_id, payer, router_configs).unwrap() + } + + fn get_routers(&self, client: &RpcClient, program_id: &Pubkey) -> HashMap { + let token_data = get_token_data::<()>(client, program_id); + + token_data.remote_routers + } - let chain_config_file = File::open(deploy.chain_config_file).unwrap(); - let chain_configs: HashMap = - serde_json::from_reader(chain_config_file).unwrap(); + fn init_program_idempotent( + &self, + ctx: &mut Context, + client: &RpcClient, + core_program_ids: &CoreProgramIds, + chain_config: &ChainMetadata, + app_config: &TokenConfig, + program_id: Pubkey, + ) { + if let Some(ata_payer_funding_amount) = self.ata_payer_funding_amount { + if matches!( + app_config.token_type, + TokenType::Collateral(_) | TokenType::Synthetic(_) + ) { + fund_ata_payer_up_to(ctx, client, program_id, ata_payer_funding_amount); + } + } - let environments_dir = - create_new_directory(&deploy.environments_dir, &deploy.environment); + let (token_pda, _token_bump) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), &program_id); + if account_exists(client, &token_pda).unwrap() { + println!("Warp route token already exists, skipping init"); + return; + } - let artifacts_dir = create_new_directory(&environments_dir, "warp-routes"); - let warp_route_dir = create_new_directory(&artifacts_dir, &deploy.warp_route_name); - let keys_dir = create_new_directory(&warp_route_dir, "keys"); + let domain_id = chain_config.domain_id(); - let foreign_deployments = token_configs - .iter() - .filter(|(_, token_config)| token_config.foreign_deployment.is_some()) - .map(|(chain_name, token_config)| { - let chain_config = chain_configs.get(chain_name).unwrap(); - ( - chain_config.domain_id(), - hex_or_base58_to_h256(token_config.foreign_deployment.as_ref().unwrap()) - .unwrap(), - ) - }) - .collect::>(); - - let mut routers: HashMap = foreign_deployments; - - let token_configs_to_deploy = token_configs - .clone() - .into_iter() - .filter(|(_, token_config)| token_config.foreign_deployment.is_none()) - .collect::>(); - - // Deploy to chains that don't have a foreign deployment - for (chain_name, token_config) in token_configs_to_deploy.iter() { - let chain_config = chain_configs - .get(chain_name) - .unwrap_or_else(|| panic!("Chain config not found for chain: {}", chain_name)); - - if token_config.ownable.owner.is_some() { - println!("WARNING: Ownership transfer is not yet supported in this deploy tooling, ownership is granted to the payer account"); - } - - let program_id = deploy_warp_route( - &mut ctx, - &keys_dir, - &deploy.environments_dir, - &deploy.environment, - &deploy.built_so_dir, - chain_config, - token_config, - deploy.ata_payer_funding_amount, - ); + // TODO: consider pulling the setting of defaults into router.rs, + // and possibly have a more distinct connection client abstration. + + let mailbox = app_config + .router_config() + .connection_client + .mailbox(core_program_ids.mailbox); + let interchain_security_module = app_config + .router_config() + .connection_client + .interchain_security_module(); + let owner = Some(app_config.router_config().ownable.owner(ctx.payer_pubkey)); + + // Default to the Overhead IGP + let interchain_gas_paymaster = Some( + app_config + .router_config() + .connection_client + .interchain_gas_paymaster_config(client) + .unwrap_or(( + core_program_ids.igp_program_id, + InterchainGasPaymasterType::OverheadIgp(core_program_ids.overhead_igp_account), + )), + ); + + println!( + "Initializing Warp Route program: domain_id: {}, mailbox: {}, ism: {:?}, owner: {:?}, igp: {:?}", + domain_id, mailbox, interchain_security_module, owner, interchain_gas_paymaster + ); + + let init = Init { + mailbox, + interchain_security_module, + interchain_gas_paymaster, + decimals: app_config.decimal_metadata.decimals, + remote_decimals: app_config.decimal_metadata.remote_decimals(), + }; + + match &app_config.token_type { + TokenType::Native => ctx.new_txn().add( + hyperlane_sealevel_token_native::instruction::init_instruction( + program_id, + ctx.payer_pubkey, + init, + ) + .unwrap(), + ), + TokenType::Synthetic(_token_metadata) => { + let decimals = init.decimals; - routers.insert( - chain_config.domain_id(), - H256::from_slice(&program_id.to_bytes()[..]), + let init_txn = ctx.new_txn().add( + hyperlane_sealevel_token::instruction::init_instruction( + program_id, + ctx.payer_pubkey, + init, + ) + .unwrap(), ); - } - // Now enroll routers - for (chain_name, _) in token_configs_to_deploy { - let chain_config = chain_configs - .get(&chain_name) - .unwrap_or_else(|| panic!("Chain config not found for chain: {}", chain_name)); - - let domain_id = chain_config.domain_id(); - let program_id: Pubkey = - Pubkey::new_from_array(*routers.get(&domain_id).unwrap().as_fixed_bytes()); - - let enrolled_routers = get_routers(&chain_config.client(), &program_id).unwrap(); - - let expected_routers = routers - .iter() - .filter(|(router_domain_id, _)| *router_domain_id != &domain_id) - .map(|(domain, router)| { - ( - *domain, - RemoteRouterConfig { - domain: *domain, - router: Some(*router), - }, - ) - }) - .collect::>(); - - // Routers to enroll (or update to a Some value) - let routers_to_enroll = expected_routers - .iter() - .filter(|(domain, router_config)| { - enrolled_routers.get(domain) != router_config.router.as_ref() - }) - .map(|(_, router_config)| router_config.clone()); - - // Routers to remove - let routers_to_unenroll = enrolled_routers - .iter() - .filter(|(domain, _)| !expected_routers.contains_key(domain)) - .map(|(domain, _)| RemoteRouterConfig { - domain: *domain, - router: None, - }); - - // All router config changes - let router_configs = routers_to_enroll - .chain(routers_to_unenroll) - .collect::>(); - - if !router_configs.is_empty() { - println!( - "Enrolling routers for chain: {}, program_id {}, routers: {:?}", - chain_name, program_id, router_configs, - ); - - ctx.new_txn() - .add( - enroll_remote_routers_instruction( - program_id, - ctx.payer_pubkey, - router_configs, - ) - .unwrap(), - ) - .with_client(&chain_config.client()) - .send_with_payer(); - } else { - println!( - "No router changes for chain: {}, program_id {}", - chain_name, program_id - ); - } - - // And set destination gas - let configured_destination_gas = - get_destination_gas(&chain_config.client(), &program_id).unwrap(); - - let expected_destination_gas = token_configs - .iter() - // filter out local chain - .filter(|(dest_chain_name, _)| *dest_chain_name != &chain_name) - .map(|(dest_chain_name, token_config)| { - let domain = chain_configs.get(dest_chain_name).unwrap().domain_id(); - ( - domain, - GasRouterConfig { - domain, - gas: Some(token_config.token_type.gas_overhead_default()), - }, - ) - }) - .collect::>(); - - // Destination gas to set or update to a Some value - let destination_gas_to_set = expected_destination_gas - .iter() - .filter(|(domain, expected_config)| { - configured_destination_gas.get(domain) != expected_config.gas.as_ref() - }) - .map(|(_, expected_config)| expected_config.clone()); - - // Destination gas to remove - let destination_gas_to_unset = configured_destination_gas - .iter() - .filter(|(domain, _)| !expected_destination_gas.contains_key(domain)) - .map(|(domain, _)| GasRouterConfig { - domain: *domain, - gas: None, - }); - - // All destination gas config changes - let destination_gas_configs = destination_gas_to_set - .chain(destination_gas_to_unset) - .collect::>(); - - if !destination_gas_configs.is_empty() { - println!( - "Setting destination gas amounts for chain: {}, program_id {}, destination gas: {:?}", - chain_name, program_id, destination_gas_configs, - ); - - ctx.new_txn() - .add( - set_destination_gas_configs( - program_id, - ctx.payer_pubkey, - destination_gas_configs, - ) - .unwrap(), - ) - .with_client(&chain_config.client()) - .send_with_payer(); - } else { - println!( - "No destination gas amount changes for chain: {}, program_id {}", - chain_name, program_id - ); - } + let (mint_account, _mint_bump) = + Pubkey::find_program_address(hyperlane_token_mint_pda_seeds!(), &program_id); + // TODO: Also set Metaplex metadata? + init_txn.add( + spl_token_2022::instruction::initialize_mint2( + &spl_token_2022::id(), + &mint_account, + &mint_account, + None, + decimals, + ) + .unwrap(), + ) } + TokenType::Collateral(collateral_info) => ctx.new_txn().add( + hyperlane_sealevel_token_collateral::instruction::init_instruction( + program_id, + ctx.payer_pubkey, + init, + collateral_info + .spl_token_program + .as_ref() + .expect("Cannot initalize collateral warp route without SPL token program") + .program_id(), + collateral_info.mint.parse().expect("Invalid mint address"), + ) + .unwrap(), + ), + } + .with_client(client) + .send_with_payer(); + } - let routers_by_name: HashMap = routers + /// Sets gas router configs on all deployable chains. + fn post_deploy( + &self, + ctx: &mut Context, + app_configs: &HashMap, + app_configs_to_deploy: &HashMap<&String, &TokenConfig>, + chain_configs: &HashMap, + routers: &HashMap, + ) { + // Set gas amounts for each destination chain + for chain_name in app_configs_to_deploy.keys() { + let chain_config = chain_configs + .get(*chain_name) + .unwrap_or_else(|| panic!("Chain config not found for chain: {}", chain_name)); + + let domain_id = chain_config.domain_id(); + let program_id: Pubkey = + Pubkey::new_from_array(*routers.get(&domain_id).unwrap().as_fixed_bytes()); + + // And set destination gas + let configured_destination_gas = + get_destination_gas(&chain_config.client(), &program_id).unwrap(); + + let expected_destination_gas = app_configs .iter() - .map(|(domain_id, router)| { + // filter out local chain + .filter(|(dest_chain_name, _)| dest_chain_name != chain_name) + .map(|(dest_chain_name, app_config)| { + let domain = chain_configs.get(dest_chain_name).unwrap().domain_id(); ( - chain_configs - .iter() - .find(|(_, chain_config)| chain_config.domain_id() == *domain_id) - .unwrap() - .0 - .clone(), - *router, + domain, + GasRouterConfig { + domain, + gas: Some(app_config.token_type.gas_overhead_default()), + }, ) }) - .collect::>(); - write_program_ids(&warp_route_dir, &routers_by_name); - } - WarpRouteSubCmd::DestinationGas(args) => { - let destination_gas = get_destination_gas(&ctx.client, &args.program_id).unwrap(); - println!( - "Destination gas: {:?}", - destination_gas[&args.destination_domain] - ); + .collect::>(); + + // Destination gas to set or update to a Some value + let destination_gas_to_set = expected_destination_gas + .iter() + .filter(|(domain, expected_config)| { + configured_destination_gas.get(domain) != expected_config.gas.as_ref() + }) + .map(|(_, expected_config)| expected_config.clone()); + + // Destination gas to remove + let destination_gas_to_unset = configured_destination_gas + .iter() + .filter(|(domain, _)| !expected_destination_gas.contains_key(domain)) + .map(|(domain, _)| GasRouterConfig { + domain: *domain, + gas: None, + }); + + // All destination gas config changes + let destination_gas_configs = destination_gas_to_set + .chain(destination_gas_to_unset) + .collect::>(); + + if !destination_gas_configs.is_empty() { + let description = format!( + "Setting destination gas amounts for chain: {}, program_id {}, destination gas: {:?}", + chain_name, program_id, destination_gas_configs, + ); + ctx.new_txn() + .add_with_description( + set_destination_gas_configs( + program_id, + ctx.payer_pubkey, + destination_gas_configs, + ) + .unwrap(), + description, + ) + .with_client(&chain_config.client()) + .send_with_payer(); + } else { + println!( + "No destination gas amount changes for chain: {}, program_id {}", + chain_name, program_id + ); + } } } } -#[allow(clippy::too_many_arguments)] -fn deploy_warp_route( - ctx: &mut Context, - key_dir: &Path, - environments_dir: &Path, - environment: &str, - built_so_dir: &Path, - chain_config: &ChainMetadata, - token_config: &TokenConfig, - ata_payer_funding_amount: Option, -) -> Pubkey { - println!( - "Attempting deploy on chain: {}\nToken config: {:?}", - chain_config.name, token_config - ); +impl RouterConfigGetter for TokenConfig { + fn router_config(&self) -> &RouterConfig { + &self.router_config + } +} - let (keypair, keypair_path) = create_and_write_keypair( - key_dir, - format!( - "{}-{}.json", - token_config.token_type.program_name(), - chain_config.name - ) - .as_str(), - true, - ); - let program_id = keypair.pubkey(); - - deploy_program_idempotent( - ctx.payer_keypair_path(), - &keypair, - keypair_path.to_str().unwrap(), - built_so_dir - .join(format!("{}.so", token_config.token_type.program_name())) - .to_str() - .unwrap(), - &chain_config.public_rpc_urls[0].http, - ) - .unwrap(); - - let core_program_ids = read_core_program_ids(environments_dir, environment, &chain_config.name); - init_warp_route_idempotent( - ctx, - &chain_config.client(), - &core_program_ids, - chain_config, - token_config, - program_id, - ata_payer_funding_amount, - ) - .unwrap(); - - match &token_config.token_type { - TokenType::Native => { - println!("Deploying native token"); - } - TokenType::Synthetic(_token_metadata) => { - println!("Deploying synthetic token"); - } - TokenType::Collateral(_collateral_info) => { - println!("Deploying collateral token"); - } +impl Ownable for WarpRouteDeployer { + /// Gets the owner configured on-chain. + fn get_owner(&self, client: &RpcClient, program_id: &Pubkey) -> Option { + let token = get_token_data::<()>(client, program_id); + + token.owner } - program_id + /// Gets an instruction to set the owner. + fn set_owner_instruction( + &self, + client: &RpcClient, + program_id: &Pubkey, + new_owner: Option, + ) -> Instruction { + let token = get_token_data::<()>(client, program_id); + + transfer_ownership_instruction(*program_id, token.owner.unwrap(), new_owner).unwrap() + } } -fn init_warp_route_idempotent( - ctx: &mut Context, - client: &RpcClient, - core_program_ids: &CoreProgramIds, - _chain_config: &ChainMetadata, - token_config: &TokenConfig, - program_id: Pubkey, - ata_payer_funding_amount: Option, -) -> Result<(), ProgramError> { - let (token_pda, _token_bump) = - Pubkey::find_program_address(hyperlane_token_pda_seeds!(), &program_id); - - if let Some(ata_payer_funding_amount) = ata_payer_funding_amount { - if matches!( - token_config.token_type, - TokenType::Collateral(_) | TokenType::Synthetic(_) - ) { - fund_ata_payer_up_to(ctx, client, program_id, ata_payer_funding_amount); - } +impl ConnectionClient for WarpRouteDeployer { + fn get_interchain_security_module( + &self, + client: &RpcClient, + program_id: &Pubkey, + ) -> Option { + let token_data = get_token_data::<()>(client, program_id); + + token_data.interchain_security_module } - if account_exists(client, &token_pda).unwrap() { - println!("Token PDA already exists, skipping init"); - return Ok(()); + fn set_interchain_security_module_instruction( + &self, + client: &RpcClient, + program_id: &Pubkey, + ism: Option, + ) -> Instruction { + let token_data = get_token_data::<()>(client, program_id); + + set_interchain_security_module_instruction(*program_id, token_data.owner.unwrap(), ism) + .unwrap() } +} + +fn get_token_data(client: &RpcClient, program_id: &Pubkey) -> HyperlaneToken +where + T: BorshDeserialize + BorshSerialize + Default + account_utils::Data, +{ + let (token_pda, _token_bump) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), program_id); + + let account = client.get_account(&token_pda).unwrap(); + *HyperlaneTokenAccount::::fetch(&mut &account.data[..]) + .unwrap() + .into_inner() +} + +fn get_destination_gas( + client: &RpcClient, + program_id: &Pubkey, +) -> Result, ClientError> { + let token_data = get_token_data::<()>(client, program_id); - init_warp_route( - ctx, - client, - core_program_ids, - _chain_config, - token_config, - program_id, - ) + Ok(token_data.destination_gas) } +// Funds the ATA payer up to the specified amount. fn fund_ata_payer_up_to( ctx: &mut Context, client: &RpcClient, @@ -524,172 +479,22 @@ fn fund_ata_payer_up_to( return; } - println!( - "Funding ATA payer {} with funding_amount {} to reach total balance of {}", - ata_payer_account, funding_amount, ata_payer_funding_amount - ); ctx.new_txn() - .add(solana_program::system_instruction::transfer( - &ctx.payer_pubkey, - &ata_payer_account, - funding_amount, - )) + .add_with_description( + solana_program::system_instruction::transfer( + &ctx.payer_pubkey, + &ata_payer_account, + funding_amount, + ), + format!( + "Funding ATA payer {} with funding_amount {} to reach total balance of {}", + ata_payer_account, funding_amount, ata_payer_funding_amount + ), + ) .with_client(client) .send_with_payer(); } -fn init_warp_route( - ctx: &mut Context, - client: &RpcClient, - core_program_ids: &CoreProgramIds, - _chain_config: &ChainMetadata, - token_config: &TokenConfig, - program_id: Pubkey, -) -> Result<(), ProgramError> { - // If the Mailbox was provided as configuration, use that. Otherwise, default to - // the Mailbox found in the core program ids. - let mailbox = token_config - .connection_client - .mailbox - .as_ref() - .map(|s| Pubkey::from_str(s).unwrap()) - .unwrap_or(core_program_ids.mailbox); - - // TODO for now not specifying an IGP for compatibility with the warp route UI. - - // let interchain_gas_paymaster = Some(token_config - // .connection_client - // .interchain_gas_paymaster - // .clone() - // .map(|config| (config.program_id, config.igp_account)) - // .unwrap_or(( - // core_program_ids.igp_program_id, - // InterchainGasPaymasterType::OverheadIgp(core_program_ids.overhead_igp_account), - // )) - // ); - - let interchain_gas_paymaster = None; - - let init = Init { - mailbox, - interchain_security_module: token_config - .connection_client - .interchain_security_module - .as_ref() - .map(|s| Pubkey::from_str(s).unwrap()), - interchain_gas_paymaster, - decimals: token_config.decimal_metadata.decimals, - remote_decimals: token_config.decimal_metadata.remote_decimals(), - }; - - match &token_config.token_type { - TokenType::Native => ctx.new_txn().add( - hyperlane_sealevel_token_native::instruction::init_instruction( - program_id, - ctx.payer_pubkey, - init, - )?, - ), - TokenType::Synthetic(_token_metadata) => { - let decimals = init.decimals; - - let init_txn = - ctx.new_txn() - .add(hyperlane_sealevel_token::instruction::init_instruction( - program_id, - ctx.payer_pubkey, - init, - )?); - - let (mint_account, _mint_bump) = - Pubkey::find_program_address(hyperlane_token_mint_pda_seeds!(), &program_id); - // TODO: Also set Metaplex metadata? - init_txn.add( - spl_token_2022::instruction::initialize_mint2( - &spl_token_2022::id(), - &mint_account, - &mint_account, - None, - decimals, - ) - .unwrap(), - ) - } - TokenType::Collateral(collateral_info) => ctx.new_txn().add( - hyperlane_sealevel_token_collateral::instruction::init_instruction( - program_id, - ctx.payer_pubkey, - init, - collateral_info - .spl_token_program - .as_ref() - .expect("Cannot initalize collateral warp route without SPL token program") - .program_id(), - collateral_info.mint.parse().expect("Invalid mint address"), - )?, - ), - } - .with_client(client) - .send_with_payer(); - - Ok(()) -} - -fn get_routers( - client: &RpcClient, - token_program_id: &Pubkey, -) -> Result, ClientError> { - let (token_pda, _token_bump) = - Pubkey::find_program_address(hyperlane_token_pda_seeds!(), token_program_id); - - let account = client.get_account(&token_pda)?; - let token_data = HyperlaneTokenAccount::<()>::fetch(&mut &account.data[..]) - .unwrap() - .into_inner(); - - Ok(token_data.remote_routers) -} - -fn get_destination_gas( - client: &RpcClient, - token_program_id: &Pubkey, -) -> Result, ClientError> { - let (token_pda, _token_bump) = - Pubkey::find_program_address(hyperlane_token_pda_seeds!(), token_program_id); - - let account = client.get_account(&token_pda)?; - let token_data = HyperlaneTokenAccount::<()>::fetch(&mut &account.data[..]) - .unwrap() - .into_inner(); - - Ok(token_data.destination_gas) -} - -#[derive(Serialize, Deserialize)] -struct SerializedProgramId { - hex: String, - base58: String, -} - -fn write_program_ids(warp_route_dir: &Path, routers: &HashMap) { - let serialized_program_ids = routers - .iter() - .map(|(chain_name, router)| { - ( - chain_name.clone(), - SerializedProgramId { - hex: format!("0x{}", hex::encode(router)), - base58: Pubkey::new_from_array(router.to_fixed_bytes()).to_string(), - }, - ) - }) - .collect::>(); - - let program_ids_file = warp_route_dir.join("program-ids.json"); - let program_ids_file = File::create(program_ids_file).unwrap(); - serde_json::to_writer_pretty(program_ids_file, &serialized_program_ids).unwrap(); -} - pub fn parse_token_account_data(token_type: FlatTokenType, data: &mut &[u8]) { fn print_data_or_err(data: Result) { match data { diff --git a/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_mailbox-keypair.json b/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_mailbox-keypair.json deleted file mode 100644 index 484e2fb182..0000000000 --- a/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_mailbox-keypair.json +++ /dev/null @@ -1 +0,0 @@ -[113,244,152,170,85,122,42,51,10,74,244,18,91,8,135,77,156,19,172,122,139,50,248,3,186,184,186,140,110,165,78,161,76,88,146,213,185,127,121,92,132,2,249,73,19,192,73,170,105,85,247,241,48,175,67,28,165,29,224,252,173,165,38,140] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json b/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json deleted file mode 100644 index 243fcbd9ad..0000000000 --- a/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json +++ /dev/null @@ -1 +0,0 @@ -[135,153,145,193,50,88,169,205,206,171,48,1,17,242,3,43,225,72,101,163,93,126,105,165,159,44,243,196,182,240,4,87,22,253,47,198,217,75,23,60,181,129,251,103,140,170,111,35,152,97,16,23,64,17,198,239,79,225,120,141,55,38,60,86] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_validator_announce-keypair.json b/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_validator_announce-keypair.json deleted file mode 100644 index 3428c9c497..0000000000 --- a/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_validator_announce-keypair.json +++ /dev/null @@ -1 +0,0 @@ -[252,76,67,201,250,68,86,32,216,136,163,46,192,20,249,175,209,94,101,235,24,240,204,4,246,159,180,138,253,20,48,146,182,104,250,124,231,168,239,248,95,199,219,250,126,156,57,113,83,209,232,171,10,90,153,238,72,138,186,34,77,87,172,211] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/solanadevnet/core/program-ids.json b/rust/sealevel/environments/devnet/solanadevnet/core/program-ids.json deleted file mode 100644 index a9acf6dfd7..0000000000 --- a/rust/sealevel/environments/devnet/solanadevnet/core/program-ids.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "mailbox": "692KZJaoe2KRcD6uhCQDLLXnLNA5ZLnfvdqjE4aX9iu1", - "validator_announce": "DH43ae1LwemXAboWwSh8zc9pG8j72gKUEXNi57w8fEnn", - "multisig_ism_message_id": "2YjtZDiUoptoSsA5eVrDCcX6wxNK6YoEVW7y82x5Z2fw" -} \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_mailbox-keypair.json b/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_mailbox-keypair.json deleted file mode 100644 index 31a9bdeb37..0000000000 --- a/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_mailbox-keypair.json +++ /dev/null @@ -1 +0,0 @@ -[6,132,236,247,134,194,48,96,56,63,48,146,121,215,228,1,80,199,79,128,232,145,31,24,170,246,162,253,52,12,244,198,141,84,17,12,114,138,14,65,37,185,65,155,156,209,188,73,159,63,157,69,158,103,155,16,217,78,19,53,6,226,115,117] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json b/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json deleted file mode 100644 index 4e8c405ce3..0000000000 --- a/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json +++ /dev/null @@ -1 +0,0 @@ -[75,122,156,10,143,146,130,96,41,203,245,228,178,140,170,105,167,226,18,171,187,4,70,210,1,234,232,194,206,26,65,248,243,199,245,54,127,196,31,152,114,133,16,172,1,103,105,249,111,240,129,216,26,184,14,131,242,197,189,46,163,142,2,120] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_validator_announce-keypair.json b/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_validator_announce-keypair.json deleted file mode 100644 index 153a856cec..0000000000 --- a/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_validator_announce-keypair.json +++ /dev/null @@ -1 +0,0 @@ -[60,166,246,212,217,15,197,101,188,59,172,187,217,44,158,58,65,180,5,179,193,73,206,199,134,54,56,70,26,169,141,82,49,9,182,63,146,255,211,243,158,55,120,3,60,23,151,134,195,85,195,50,62,205,7,162,107,106,40,106,220,117,82,91] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/solanadevnet1/core/program-ids.json b/rust/sealevel/environments/devnet/solanadevnet1/core/program-ids.json deleted file mode 100644 index 105a160e39..0000000000 --- a/rust/sealevel/environments/devnet/solanadevnet1/core/program-ids.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "mailbox": "AWgqPcY1vjHRoFLHNgs15fdvy4bqEakHmYXW78B8GgYk", - "validator_announce": "4JRZrYJnXJn6KPSCG4tA6GBomP2zwQv8bD65anWnHmNz", - "multisig_ism_message_id": "HQcv2ibNRuJdHU8Lt9t655YUjXj4Rp9nW8mbcA26cYqM" -} \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/chain-config.json b/rust/sealevel/environments/devnet/warp-routes/chain-config.json deleted file mode 100644 index 96691fdff5..0000000000 --- a/rust/sealevel/environments/devnet/warp-routes/chain-config.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "solanadevnet": { - "chainId": 13375, - "name": "solanadevnet", - "publicRpcUrls": [ - { - "http": "https://api.devnet.solana.com" - } - ] - }, - "solanadevnet1": { - "chainId": 13376, - "name": "solanadevnet1", - "publicRpcUrls": [ - { - "http": "https://api.devnet.solana.com" - } - ] - }, - "fuji": { - "chainId": 43113, - "name": "fuji", - "publicRpcUrls": [ - { - "http": "https://api.avax-test.network/ext/bc/C/rpc" - } - ] - } -} \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/collateraltest/keys/hyperlane_sealevel_token-solanadevnet1.json b/rust/sealevel/environments/devnet/warp-routes/collateraltest/keys/hyperlane_sealevel_token-solanadevnet1.json deleted file mode 100644 index 7ef8d00db3..0000000000 --- a/rust/sealevel/environments/devnet/warp-routes/collateraltest/keys/hyperlane_sealevel_token-solanadevnet1.json +++ /dev/null @@ -1 +0,0 @@ -[234,24,62,69,201,85,23,105,162,73,39,96,54,24,252,131,65,61,204,71,240,230,98,153,79,12,102,57,135,254,59,159,71,62,216,28,221,183,176,75,40,167,248,151,145,3,242,74,196,153,147,167,98,202,124,87,70,27,115,81,78,50,199,68] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/collateraltest/keys/hyperlane_sealevel_token_collateral-solanadevnet.json b/rust/sealevel/environments/devnet/warp-routes/collateraltest/keys/hyperlane_sealevel_token_collateral-solanadevnet.json deleted file mode 100644 index 5bd77b5616..0000000000 --- a/rust/sealevel/environments/devnet/warp-routes/collateraltest/keys/hyperlane_sealevel_token_collateral-solanadevnet.json +++ /dev/null @@ -1 +0,0 @@ -[39,54,74,37,67,213,195,70,204,38,146,230,111,25,95,162,197,128,223,145,57,112,78,217,51,236,68,252,254,70,26,37,135,224,112,242,167,101,9,162,147,37,98,70,138,147,6,126,136,247,145,107,228,139,68,251,82,120,107,18,4,102,190,221] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/collateraltest/program-ids.json b/rust/sealevel/environments/devnet/warp-routes/collateraltest/program-ids.json deleted file mode 100644 index 0a7a1eab0a..0000000000 --- a/rust/sealevel/environments/devnet/warp-routes/collateraltest/program-ids.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "solanadevnet1": { - "hex": "0x473ed81cddb7b04b28a7f8979103f24ac49993a762ca7c57461b73514e32c744", - "base58": "5o7XXLy8N67cgCjSt4zKNnzAbDRpkVruu8BbpPNehBrK" - }, - "fuji": { - "hex": "0x000000000000000000000000b3af04fc8b461138eca4f5fc1d5955bbe6d20fca", - "base58": "1111111111113WC5zqqJzmcsiZZcai6ZxbvW84do" - }, - "solanadevnet": { - "hex": "0x87e070f2a76509a2932562468a93067e88f7916be48b44fb52786b120466bedd", - "base58": "A9QY6ZQ3t1T3Pk58gTx1vsSeH2B2AywwK2V7SpH2w2cC" - } -} \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/collateraltest/token-config.json b/rust/sealevel/environments/devnet/warp-routes/collateraltest/token-config.json deleted file mode 100644 index d63f7a11e2..0000000000 --- a/rust/sealevel/environments/devnet/warp-routes/collateraltest/token-config.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "solanadevnet": { - "type": "collateral", - "token": "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr", - "splTokenProgram": "token", - "decimals": 6, - "name": "USD Coin Dev", - "symbol": "USDC" - }, - "solanadevnet1": { - "type": "synthetic", - "decimals": 6, - "name": "USD Coin Dev", - "symbol": "USDC" - }, - "fuji": { - "type": "synthetic", - "decimals": 6, - "name": "USD Coin Dev", - "symbol": "USDC", - "foreignDeployment": "0xb3AF04Fc8b461138eCA4F5fC1D5955Bbe6D20Fca" - } -} \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/nativetest/keys/hyperlane_sealevel_token-solanadevnet1.json b/rust/sealevel/environments/devnet/warp-routes/nativetest/keys/hyperlane_sealevel_token-solanadevnet1.json deleted file mode 100644 index 85b4ecd615..0000000000 --- a/rust/sealevel/environments/devnet/warp-routes/nativetest/keys/hyperlane_sealevel_token-solanadevnet1.json +++ /dev/null @@ -1 +0,0 @@ -[108,3,96,252,94,52,97,146,193,61,252,4,209,156,21,178,42,234,170,70,97,252,167,156,146,209,57,48,52,224,211,72,40,108,141,225,165,106,19,22,48,134,115,13,111,173,228,116,229,5,197,167,246,245,139,19,60,75,183,152,49,124,190,33] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/nativetest/keys/hyperlane_sealevel_token_native-solanadevnet.json b/rust/sealevel/environments/devnet/warp-routes/nativetest/keys/hyperlane_sealevel_token_native-solanadevnet.json deleted file mode 100644 index b9d79c759d..0000000000 --- a/rust/sealevel/environments/devnet/warp-routes/nativetest/keys/hyperlane_sealevel_token_native-solanadevnet.json +++ /dev/null @@ -1 +0,0 @@ -[169,25,77,166,171,9,74,84,180,104,209,80,36,170,223,85,56,255,50,104,185,250,53,188,65,168,235,7,176,81,99,182,113,69,170,191,248,224,191,67,181,13,107,166,133,126,157,101,165,157,24,202,25,96,195,132,107,100,86,78,48,232,7,142] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/nativetest/program-ids.json b/rust/sealevel/environments/devnet/warp-routes/nativetest/program-ids.json deleted file mode 100644 index 811f6d7ecd..0000000000 --- a/rust/sealevel/environments/devnet/warp-routes/nativetest/program-ids.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "solanadevnet": { - "hex": "0x7145aabff8e0bf43b50d6ba6857e9d65a59d18ca1960c3846b64564e30e8078e", - "base58": "8dAgevgBAnhvxoB5mWNUfmXi6H8WLC3ZaP8poaRHkzaR" - }, - "solanadevnet1": { - "hex": "0x286c8de1a56a13163086730d6fade474e505c5a7f6f58b133c4bb798317cbe21", - "base58": "3ioKCgR4pyrtkJvsB3zketopnR3mqjjBszSgtiQXQz7i" - }, - "fuji": { - "hex": "0x00000000000000000000000011cf63c916263d6bbd710f43816ee6703e1c5da3", - "base58": "111111111111FPh8wLMbV6vLtqUcvxKR496PAXL" - } -} \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/nativetest/token-config.json b/rust/sealevel/environments/devnet/warp-routes/nativetest/token-config.json deleted file mode 100644 index 712b1f31a8..0000000000 --- a/rust/sealevel/environments/devnet/warp-routes/nativetest/token-config.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "solanadevnet": { - "type": "native", - "decimals": 9 - }, - "solanadevnet1": { - "type": "synthetic", - "decimals": 9, - "name": "Solana (solanadevnet)", - "symbol": "SOL" - }, - "fuji": { - "type": "synthetic", - "decimals": 9, - "name": "Solana (solanadevnet)", - "symbol": "SOL", - "foreignDeployment": "0x11CF63c916263d6BBD710F43816ee6703e1C5da3" - } -} \ No newline at end of file diff --git a/rust/sealevel/environments/local-e2e/warp-routes/chain-config.json b/rust/sealevel/environments/local-e2e/chain-config.json similarity index 85% rename from rust/sealevel/environments/local-e2e/warp-routes/chain-config.json rename to rust/sealevel/environments/local-e2e/chain-config.json index 5914c12d6a..eadbdf9404 100644 --- a/rust/sealevel/environments/local-e2e/warp-routes/chain-config.json +++ b/rust/sealevel/environments/local-e2e/chain-config.json @@ -2,7 +2,7 @@ "sealeveltest1": { "chainId": 13375, "name": "sealeveltest1", - "publicRpcUrls": [ + "rpcUrls": [ { "http": "http://localhost:8899" } @@ -11,7 +11,7 @@ "sealeveltest2": { "chainId": 13376, "name": "sealeveltest2", - "publicRpcUrls": [ + "rpcUrls": [ { "http": "http://localhost:8899" } diff --git a/rust/sealevel/environments/mainnet2/chain-config.json b/rust/sealevel/environments/mainnet2/chain-config.json new file mode 100644 index 0000000000..c8c314c777 --- /dev/null +++ b/rust/sealevel/environments/mainnet2/chain-config.json @@ -0,0 +1,350 @@ +{ + "bsc": { + "chainId": 56, + "domainId": 56, + "name": "bsc", + "protocol": "ethereum", + "displayName": "Binance Smart Chain", + "displayNameShort": "Binance", + "nativeToken": { + "decimals": 18, + "name": "BNB", + "symbol": "BNB" + }, + "rpcUrls": [ + { + "http": "https://bsc-dataseed.binance.org" + }, + { + "http": "https://rpc.ankr.com/bsc" + } + ], + "blockExplorers": [ + { + "name": "BscScan", + "url": "https://bscscan.com", + "apiUrl": "https://api.bscscan.com/api", + "family": "etherscan" + } + ], + "blocks": { + "confirmations": 1, + "reorgPeriod": 15, + "estimateBlockTime": 3 + }, + "gasCurrencyCoinGeckoId": "binancecoin", + "gnosisSafeTransactionServiceUrl": "https://safe-transaction-bsc.safe.global/", + "transactionOverrides": { + "gasPrice": 7000000000 + } + }, + "avalanche": { + "chainId": 43114, + "domainId": 43114, + "name": "avalanche", + "protocol": "ethereum", + "displayName": "Avalanche", + "nativeToken": { + "decimals": 18, + "name": "Avalanche", + "symbol": "AVAX" + }, + "rpcUrls": [ + { + "http": "https://api.avax.network/ext/bc/C/rpc", + "pagination": { + "maxBlockRange": 100000, + "minBlockNumber": 6765067 + } + } + ], + "blockExplorers": [ + { + "name": "SnowTrace", + "url": "https://snowtrace.io", + "apiUrl": "https://api.snowtrace.io/api", + "family": "other" + } + ], + "blocks": { + "confirmations": 3, + "reorgPeriod": 3, + "estimateBlockTime": 2 + }, + "gasCurrencyCoinGeckoId": "avalanche-2", + "gnosisSafeTransactionServiceUrl": "https://safe-transaction-avalanche.safe.global/" + }, + "polygon": { + "chainId": 137, + "domainId": 137, + "name": "polygon", + "protocol": "ethereum", + "displayName": "Polygon", + "nativeToken": { + "name": "Ether", + "symbol": "ETH", + "decimals": 18 + }, + "rpcUrls": [ + { + "http": "https://rpc-mainnet.matic.quiknode.pro", + "pagination": { + "maxBlockRange": 10000, + "minBlockNumber": 19657100 + } + }, + { + "http": "https://polygon-rpc.com" + } + ], + "blockExplorers": [ + { + "name": "PolygonScan", + "url": "https://polygonscan.com", + "apiUrl": "https://api.polygonscan.com/api", + "family": "etherscan" + } + ], + "blocks": { + "confirmations": 3, + "reorgPeriod": 256, + "estimateBlockTime": 2 + }, + "gasCurrencyCoinGeckoId": "matic-network", + "gnosisSafeTransactionServiceUrl": "https://safe-transaction-polygon.safe.global/", + "transactionOverrides": { + "maxFeePerGas": 500000000000, + "maxPriorityFeePerGas": 100000000000 + } + }, + "celo": { + "chainId": 42220, + "domainId": 42220, + "name": "celo", + "protocol": "ethereum", + "displayName": "Celo", + "nativeToken": { + "decimals": 18, + "name": "CELO", + "symbol": "CELO" + }, + "rpcUrls": [ + { + "http": "https://forno.celo.org" + } + ], + "blockExplorers": [ + { + "name": "CeloScan", + "url": "https://celoscan.io", + "apiUrl": "https://api.celoscan.io/api", + "family": "etherscan" + }, + { + "name": "Blockscout", + "url": "https://explorer.celo.org", + "apiUrl": "https://explorer.celo.org/mainnet/api", + "family": "blockscout" + } + ], + "blocks": { + "confirmations": 1, + "reorgPeriod": 0, + "estimateBlockTime": 5 + }, + "gnosisSafeTransactionServiceUrl": "https://mainnet-tx-svc.celo-safe-prod.celo-networks-dev.org/" + }, + "arbitrum": { + "chainId": 42161, + "domainId": 42161, + "name": "arbitrum", + "protocol": "ethereum", + "displayName": "Arbitrum", + "nativeToken": { + "name": "Ether", + "symbol": "ETH", + "decimals": 18 + }, + "rpcUrls": [ + { + "http": "https://arb1.arbitrum.io/rpc" + } + ], + "blockExplorers": [ + { + "name": "Arbiscan", + "url": "https://arbiscan.io", + "apiUrl": "https://api.arbiscan.io/api", + "family": "etherscan" + } + ], + "blocks": { + "confirmations": 1, + "reorgPeriod": 0, + "estimateBlockTime": 3 + }, + "gasCurrencyCoinGeckoId": "ethereum", + "gnosisSafeTransactionServiceUrl": "https://safe-transaction-arbitrum.safe.global/" + }, + "optimism": { + "chainId": 10, + "domainId": 10, + "name": "optimism", + "protocol": "ethereum", + "displayName": "Optimism", + "nativeToken": { + "name": "Ether", + "symbol": "ETH", + "decimals": 18 + }, + "rpcUrls": [ + { + "http": "https://mainnet.optimism.io" + } + ], + "blockExplorers": [ + { + "name": "Etherscan", + "url": "https://optimistic.etherscan.io", + "apiUrl": "https://api-optimistic.etherscan.io/api", + "family": "etherscan" + } + ], + "blocks": { + "confirmations": 1, + "reorgPeriod": 0, + "estimateBlockTime": 3 + }, + "gasCurrencyCoinGeckoId": "ethereum", + "gnosisSafeTransactionServiceUrl": "https://safe-transaction-optimism.safe.global/" + }, + "ethereum": { + "chainId": 1, + "domainId": 1, + "name": "ethereum", + "protocol": "ethereum", + "displayName": "Ethereum", + "nativeToken": { + "name": "Ether", + "symbol": "ETH", + "decimals": 18 + }, + "rpcUrls": [ + { + "http": "https://mainnet.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161" + }, + { + "http": "https://cloudflare-eth.com" + } + ], + "blockExplorers": [ + { + "name": "Etherscan", + "url": "https://etherscan.io", + "apiUrl": "https://api.etherscan.io/api", + "family": "etherscan" + }, + { + "name": "Blockscout", + "url": "https://blockscout.com/eth/mainnet", + "apiUrl": "https://blockscout.com/eth/mainnet/api", + "family": "blockscout" + } + ], + "blocks": { + "confirmations": 3, + "reorgPeriod": 14, + "estimateBlockTime": 13 + }, + "gnosisSafeTransactionServiceUrl": "https://safe-transaction-mainnet.safe.global/", + "transactionOverrides": { + "maxFeePerGas": 150000000000, + "maxPriorityFeePerGas": 5000000000 + } + }, + "moonbeam": { + "chainId": 1284, + "domainId": 1284, + "name": "moonbeam", + "protocol": "ethereum", + "displayName": "Moonbeam", + "nativeToken": { + "decimals": 18, + "name": "GLMR", + "symbol": "GLMR" + }, + "rpcUrls": [ + { + "http": "https://rpc.api.moonbeam.network" + } + ], + "blockExplorers": [ + { + "name": "MoonScan", + "url": "https://moonscan.io", + "apiUrl": "https://api-moonbeam.moonscan.io/api", + "family": "etherscan" + } + ], + "blocks": { + "confirmations": 2, + "reorgPeriod": 2, + "estimateBlockTime": 12 + }, + "gnosisSafeTransactionServiceUrl": "https://transaction.multisig.moonbeam.network" + }, + "gnosis": { + "chainId": 100, + "domainId": 100, + "name": "gnosis", + "protocol": "ethereum", + "displayName": "Gnosis", + "nativeToken": { + "name": "xDai", + "symbol": "xDai", + "decimals": 18 + }, + "rpcUrls": [ + { + "http": "https://rpc.gnosischain.com", + "pagination": { + "maxBlockRange": 10000, + "minBlockNumber": 25997478 + } + } + ], + "blockExplorers": [ + { + "name": "GnosisScan", + "url": "https://gnosisscan.io", + "apiUrl": "https://api.gnosisscan.io/api", + "family": "etherscan" + } + ], + "blocks": { + "confirmations": 1, + "reorgPeriod": 14, + "estimateBlockTime": 5 + }, + "gasCurrencyCoinGeckoId": "xdai", + "gnosisSafeTransactionServiceUrl": "https://safe-transaction-gnosis-chain.safe.global/" + }, + "solana": { + "chainId": 1399811149, + "name": "solana", + "rpcUrls": [ + { + "http": "https://api.mainnet-beta.solana.com" + } + ] + }, + "nautilus": { + "chainId": 22222, + "name": "nautilus", + "rpcUrls": [ + { + "http": "https://api.nautilus.nautchain.xyz" + } + ] + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet2/helloworld/hyperlane/helloworld-config.json b/rust/sealevel/environments/mainnet2/helloworld/hyperlane/helloworld-config.json new file mode 100644 index 0000000000..42d35e1bc2 --- /dev/null +++ b/rust/sealevel/environments/mainnet2/helloworld/hyperlane/helloworld-config.json @@ -0,0 +1,30 @@ +{ + "solana": {}, + "bsc": { + "foreignDeployment": "0xB97d3bF2fC296c2cAC4056bBC8A783ff39408e20" + }, + "avalanche": { + "foreignDeployment": "0x2A925CD8a5d919c5c6599633090c37fe38A561b6" + }, + "polygon": { + "foreignDeployment": "0x6c0aC8cEA75232aa7BeD8cbe9C4f820E7a77a9C3" + }, + "celo": { + "foreignDeployment": "0x4151773Db70C0b2D4c43Ea44A5FB5803ff1d3e0B" + }, + "arbitrum": { + "foreignDeployment": "0x96271cA0ab9eeFB3Ca481749c0Ca4c705fD4F523" + }, + "optimism": { + "foreignDeployment": "0xA6f0A37DFDe9C2c8F46F010989C47d9edB3a9FA8" + }, + "ethereum": { + "foreignDeployment": "0x9311cEE522A7C122B843b66cC31C6a63e2F92641" + }, + "moonbeam": { + "foreignDeployment": "0xAe067C08703508230357025B38c35Cd12793628c" + }, + "gnosis": { + "foreignDeployment": "0x26f32245fCF5Ad53159E875d5Cae62aEcf19c2d4" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet2/helloworld/hyperlane/program-ids.json b/rust/sealevel/environments/mainnet2/helloworld/hyperlane/program-ids.json new file mode 100644 index 0000000000..24dbe8648d --- /dev/null +++ b/rust/sealevel/environments/mainnet2/helloworld/hyperlane/program-ids.json @@ -0,0 +1,42 @@ +{ + "moonbeam": { + "hex": "0x000000000000000000000000ae067c08703508230357025b38c35cd12793628c", + "base58": "1111111111113RcuHPfDctyAnFWHvj8tS1q8UHPh" + }, + "bsc": { + "hex": "0x000000000000000000000000b97d3bf2fc296c2cac4056bbc8a783ff39408e20", + "base58": "1111111111113atAoP8gQ2GYeue77ETUPAf8w9zw" + }, + "optimism": { + "hex": "0x000000000000000000000000a6f0a37dfde9c2c8f46f010989c47d9edb3a9fa8", + "base58": "1111111111113KtqevvpYv7NCiadmp6tRRfivB8K" + }, + "avalanche": { + "hex": "0x0000000000000000000000002a925cd8a5d919c5c6599633090c37fe38a561b6", + "base58": "111111111111bQB6b7XVDHSyvi7XmLrQMT8C3xH" + }, + "ethereum": { + "hex": "0x0000000000000000000000009311cee522a7c122b843b66cc31c6a63e2f92641", + "base58": "11111111111133qb6DzNiJ7whNaYGud2WqqtjxFS" + }, + "solana": { + "hex": "0x3797d0096b18b5b645c346a66d7f18c6c5738782c6bce24da57a3462bdef82b1", + "base58": "4k1gruSdH1r57V9QQK4aunzfMYzLFfF83jdYkkEwyem6" + }, + "celo": { + "hex": "0x0000000000000000000000004151773db70c0b2d4c43ea44a5fb5803ff1d3e0b", + "base58": "111111111111unDVQcjdeHntE83qvf1vsKCZ4av" + }, + "polygon": { + "hex": "0x0000000000000000000000006c0ac8cea75232aa7bed8cbe9c4f820e7a77a9c3", + "base58": "1111111111112WJXE3PCAsCXYZxU9Kh51sSZEa5G" + }, + "arbitrum": { + "hex": "0x00000000000000000000000096271ca0ab9eefb3ca481749c0ca4c705fd4f523", + "base58": "11111111111136L61X7cdT9tPZ4GKBtzJtrjFAd8" + }, + "gnosis": { + "hex": "0x00000000000000000000000026f32245fcf5ad53159e875d5cae62aecf19c2d4", + "base58": "111111111111YURfyMRiiTWy8X6pYHAqmYPmBpf" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet2/helloworld/rc/helloworld-config.json b/rust/sealevel/environments/mainnet2/helloworld/rc/helloworld-config.json new file mode 100644 index 0000000000..e6e92d919e --- /dev/null +++ b/rust/sealevel/environments/mainnet2/helloworld/rc/helloworld-config.json @@ -0,0 +1,32 @@ +{ + "solana": { + "interchainSecurityModule": "BYTsxBuKVbwgsZFswzB91nrxveQySghwXzaKqn8exNnC" + }, + "gnosis": { + "foreignDeployment": "0x99ca8c74cE7Cfa9d72A51fbb05F9821f5f826b3a" + }, + "bsc": { + "foreignDeployment": "0xe5554478F167936dB253f79f57c41770bfa00Bae" + }, + "avalanche": { + "foreignDeployment": "0xe1De9910fe71cC216490AC7FCF019e13a34481D7" + }, + "polygon": { + "foreignDeployment": "0xAb65C41a1BC580a52f0b166879122EFdce0cB868" + }, + "celo": { + "foreignDeployment": "0xfE29f6a4468536029Fc9c97d3a9669b9fe38E114" + }, + "arbitrum": { + "foreignDeployment": "0x414B67F62b143d6db6E9b633168Dd6fd4DA20642" + }, + "optimism": { + "foreignDeployment": "0xB4caf2CA864B413DAA502fA18A8D48cD0740fC52" + }, + "ethereum": { + "foreignDeployment": "0xed31c20c5517EaC05decD5F6dCd01Fe6d16fD09D" + }, + "moonbeam": { + "foreignDeployment": "0x3eB9eE2CFC8DCB6F58B5869D33336CFcBf1dC354" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet2/helloworld/rc/program-ids.json b/rust/sealevel/environments/mainnet2/helloworld/rc/program-ids.json new file mode 100644 index 0000000000..201236ac6a --- /dev/null +++ b/rust/sealevel/environments/mainnet2/helloworld/rc/program-ids.json @@ -0,0 +1,42 @@ +{ + "solana": { + "hex": "0x29dacc0e7124ea39b1fd43ab0fd30e038cf405c0229890229d0086d0b6516f9c", + "base58": "3pPDp16iVTJFge2sm85Q61hW61UN5xNqeG24gqFhzLFV" + }, + "avalanche": { + "hex": "0x000000000000000000000000e1de9910fe71cc216490ac7fcf019e13a34481d7", + "base58": "11111111111149We9K5tM8ijcyNy9zDMG9RyDBCJ" + }, + "arbitrum": { + "hex": "0x000000000000000000000000414b67f62b143d6db6e9b633168dd6fd4da20642", + "base58": "111111111111um79Yc6Evs5e1m2fdD2x7T1cpXb" + }, + "moonbeam": { + "hex": "0x0000000000000000000000003eb9ee2cfc8dcb6f58b5869d33336cfcbf1dc354", + "base58": "111111111111sgjzaeuHfqhExkdPQ1gJdhcSr4j" + }, + "optimism": { + "hex": "0x000000000000000000000000b4caf2ca864b413daa502fa18a8d48cd0740fc52", + "base58": "1111111111113X64nhkfMi9X5MbxKsiDTeeTmjsw" + }, + "ethereum": { + "hex": "0x000000000000000000000000ed31c20c5517eac05decd5f6dcd01fe6d16fd09d", + "base58": "1111111111114JfPmRiKEsR445qonVzCpsAvXCR2" + }, + "gnosis": { + "hex": "0x00000000000000000000000099ca8c74ce7cfa9d72a51fbb05f9821f5f826b3a", + "base58": "11111111111139Gc7eyQjpZrmWkkYQRyA2Grcvmf" + }, + "bsc": { + "hex": "0x000000000000000000000000e5554478f167936db253f79f57c41770bfa00bae", + "base58": "1111111111114CJxuV4VoAh5NsJy9qCGHqryoTCy" + }, + "polygon": { + "hex": "0x000000000000000000000000ab65c41a1bc580a52f0b166879122efdce0cb868", + "base58": "1111111111113PVkHAU9H7moDSoQvhC3Y2wgmovX" + }, + "celo": { + "hex": "0x000000000000000000000000fe29f6a4468536029fc9c97d3a9669b9fe38e114", + "base58": "1111111111114YNh3uhCWh2NjyPttobeNRyuDHYo" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet2/multisig-ism-message-id/solana/hyperlane/multisig-config.json b/rust/sealevel/environments/mainnet2/multisig-ism-message-id/solana/hyperlane/multisig-config.json new file mode 100644 index 0000000000..49cb69474f --- /dev/null +++ b/rust/sealevel/environments/mainnet2/multisig-ism-message-id/solana/hyperlane/multisig-config.json @@ -0,0 +1,106 @@ +{ + "celo": { + "type": 3, + "threshold": 4, + "validators": [ + "0x1f20274b1210046769d48174c2f0e7c25ca7d5c5", + "0x3bc014bafa43f93d534aed34f750997cdffcf007", + "0xd79d506d741fa735938f7b7847a926e34a6fe6b0", + "0xe4a258bc61e65914c2a477b2a8a433ab4ebdf44b", + "0x6aea63b0be4679c1385c26a92a3ff8aa6a8379f2", + "0xc0085e1a49bcc69e534272adb82c74c0e007e1ca" + ] + }, + "ethereum": { + "type": 3, + "threshold": 4, + "validators": [ + "0x4c327ccb881a7542be77500b2833dc84c839e7b7", + "0x84cb373148ef9112b277e68acf676fefa9a9a9a0", + "0x0d860c2b28bec3af4fd3a5997283e460ff6f2789", + "0xd4c1211f0eefb97a846c4e6d6589832e52fc03db", + "0x600c90404d5c9df885404d2cc5350c9b314ea3a2", + "0x892DC66F5B2f8C438E03f6323394e34A9C24F2D6" + ] + }, + "avalanche": { + "type": 3, + "threshold": 4, + "validators": [ + "0xa7aa52623fe3d78c343008c95894be669e218b8d", + "0xb6004433fb04f643e2d48ae765c0e7f890f0bc0c", + "0xa07e213e0985b21a6128e6c22ab5fb73948b0cc2", + "0x73853ed9a5f6f2e4c521970a94d43469e3cdaea6", + "0xbd2e136cda02ba627ca882e49b184cbe976081c8", + "0x1418126f944a44dad9edbab32294a8c890e7a9e3" + ] + }, + "polygon": { + "type": 3, + "threshold": 4, + "validators": [ + "0x59a001c3451e7f9f3b4759ea215382c1e9aa5fc1", + "0x009fb042d28944017177920c1d40da02bfebf474", + "0xba4b13e23705a5919c1901150d9697e8ffb3ea71", + "0x2faa4071b718972f9b4beec1d8cbaa4eb6cca6c6", + "0x5ae9b0f833dfe09ef455562a1f603f1634504dd6", + "0x6a163d312f7352a95c9b81dca15078d5bf77a442" + ] + }, + "bsc": { + "type": 3, + "threshold": 4, + "validators": [ + "0xcc84b1eb711e5076b2755cf4ad1d2b42c458a45e", + "0xefe34eae2bca1846b895d2d0762ec21796aa196a", + "0x662674e80e189b0861d6835c287693f50ee0c2ff", + "0x8a0f59075af466841808c529624807656309c9da", + "0xdd2ff046ccd748a456b4757a73d47f165469669f", + "0x034c4924c30ec4aa1b7f3ad58548988f0971e1bf" + ] + }, + "arbitrum": { + "type": 3, + "threshold": 4, + "validators": [ + "0xbcb815f38d481a5eba4d7ac4c9e74d9d0fc2a7e7", + "0xd839424e2e5ace0a81152298dc2b1e3bb3c7fb20", + "0xb8085c954b75b7088bcce69e61d12fcef797cd8d", + "0x9856dcb10fd6e5407fa74b5ab1d3b96cc193e9b7", + "0x505dff4e0827aa5065f5e001db888e0569d46490", + "0x25c6779d4610f940bf2488732e10bcffb9d36f81" + ] + }, + "optimism": { + "type": 3, + "threshold": 4, + "validators": [ + "0x9f2296d5cfc6b5176adc7716c7596898ded13d35", + "0x9c10bbe8efa03a8f49dfdb5c549258e3a8dca097", + "0x62144d4a52a0a0335ea5bb84392ef9912461d9dd", + "0xaff4718d5d637466ad07441ee3b7c4af8e328dbd", + "0xc64d1efeab8ae222bc889fe669f75d21b23005d9", + "0xfa174eb2b4921bb652bc1ada3e8b00e7e280bf3c" + ] + }, + "moonbeam": { + "type": 3, + "threshold": 3, + "validators": [ + "0x237243d32d10e3bdbbf8dbcccc98ad44c1c172ea", + "0x9509c8cf0a06955f27342262af501b74874e98fb", + "0xb7113c999e4d587b162dd1a28c73f3f51c6bdcdc", + "0x26725501597d47352a23cd26f122709f69ad53bc" + ] + }, + "gnosis": { + "type": 3, + "threshold": 3, + "validators": [ + "0xd0529ec8df08d0d63c0f023786bfa81e4bb51fd6", + "0x8a72ff8571c53c62c7ca02e8c97a443cd5674383", + "0x4075c2f6bd6d9562067cfe551d49c2bcafa7d692", + "0xa18580444eaeb1c5957e7b66a6bf84b6519f904d" + ] + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet2/multisig-ism-message-id/solana/nautilus/multisig-config.json b/rust/sealevel/environments/mainnet2/multisig-ism-message-id/solana/nautilus/multisig-config.json new file mode 100644 index 0000000000..5e08f27c0c --- /dev/null +++ b/rust/sealevel/environments/mainnet2/multisig-ism-message-id/solana/nautilus/multisig-config.json @@ -0,0 +1,18 @@ +{ + "bsc": { + "type": 3, + "threshold": 1, + "validators": [ + "0x0000000000000000000000000000000000000001" + ] + }, + "nautilus": { + "type": 3, + "threshold": 2, + "validators": [ + "0x9c920af9467595a23cb3433adefc3854d498a437", + "0x87611503e37ce041527c11c24263e8760fccf81f", + "0x573443248cf9929af0001b88f62131f2de29fe9f" + ] + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet2/multisig-ism-message-id/solana/nautilus/program-ids.json b/rust/sealevel/environments/mainnet2/multisig-ism-message-id/solana/nautilus/program-ids.json new file mode 100644 index 0000000000..23f0cdd550 --- /dev/null +++ b/rust/sealevel/environments/mainnet2/multisig-ism-message-id/solana/nautilus/program-ids.json @@ -0,0 +1,3 @@ +{ + "program_id": "9k74DkJvS2x9QhG4XfnKsLkqaCDyVfaj8s6FyJyhAeEP" +} \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet2/multisig-ism-message-id/solana/rc/multisig-config.json b/rust/sealevel/environments/mainnet2/multisig-ism-message-id/solana/rc/multisig-config.json new file mode 100644 index 0000000000..fc090e25db --- /dev/null +++ b/rust/sealevel/environments/mainnet2/multisig-ism-message-id/solana/rc/multisig-config.json @@ -0,0 +1,65 @@ +{ + "celo": { + "type": 3, + "threshold": 1, + "validators": [ + "0xe7a82e210f512f8e9900d6bc2acbf7981c63e66e" + ] + }, + "ethereum": { + "type": 3, + "threshold": 1, + "validators": [ + "0xaea1adb1c687b061e5b60b9da84cb69e7b5fab44" + ] + }, + "avalanche": { + "type": 3, + "threshold": 1, + "validators": [ + "0x706976391e23dea28152e0207936bd942aba01ce" + ] + }, + "polygon": { + "type": 3, + "threshold": 1, + "validators": [ + "0xef372f6ff7775989b3ac884506ee31c79638c989" + ] + }, + "bsc": { + "type": 3, + "threshold": 1, + "validators": [ + "0x0823081031a4a6f97c6083775c191d17ca96d0ab" + ] + }, + "arbitrum": { + "type": 3, + "threshold": 1, + "validators": [ + "0x1a95b35fb809d57faf1117c1cc29a6c5df289df1" + ] + }, + "optimism": { + "type": 3, + "threshold": 1, + "validators": [ + "0x60e938bf280bbc21bacfd8bf435459d9003a8f98" + ] + }, + "moonbeam": { + "type": 3, + "threshold": 1, + "validators": [ + "0x0df7140811e309dc69638352545151ebb9d5e0fd" + ] + }, + "gnosis": { + "type": 3, + "threshold": 1, + "validators": [ + "0x15f48e78092a4f79febface509cfd76467c6cdbb" + ] + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet2/multisig-ism-message-id/solana/rc/program-ids.json b/rust/sealevel/environments/mainnet2/multisig-ism-message-id/solana/rc/program-ids.json new file mode 100644 index 0000000000..0bf925699e --- /dev/null +++ b/rust/sealevel/environments/mainnet2/multisig-ism-message-id/solana/rc/program-ids.json @@ -0,0 +1,3 @@ +{ + "program_id": "BYTsxBuKVbwgsZFswzB91nrxveQySghwXzaKqn8exNnC" +} \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet2/warp-routes/chain-config.json b/rust/sealevel/environments/mainnet2/warp-routes/chain-config.json deleted file mode 100644 index d6029dfc51..0000000000 --- a/rust/sealevel/environments/mainnet2/warp-routes/chain-config.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "solana": { - "chainId": 1399811149, - "name": "solana", - "publicRpcUrls": [ - { - "http": "https://api.mainnet-beta.solana.com" - } - ] - }, - "bsc": { - "chainId": 56, - "name": "bsc", - "publicRpcUrls": [ - { - "http": "https://bsc-dataseed.binance.org" - } - ] - }, - "nautilus": { - "chainId": 22222, - "name": "nautilus", - "publicRpcUrls": [ - { - "http": "https://api.nautilus.nautchain.xyz" - } - ] - } - } \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet2/warp-routes/zbc/program-ids.json b/rust/sealevel/environments/mainnet2/warp-routes/zbc/program-ids.json index 76bf385a99..3df636e1f4 100644 --- a/rust/sealevel/environments/mainnet2/warp-routes/zbc/program-ids.json +++ b/rust/sealevel/environments/mainnet2/warp-routes/zbc/program-ids.json @@ -3,10 +3,6 @@ "hex": "0xc5ba229fa2822fe65ac2bd0a93d8371d75292c3415dd381923c1088a3308528b", "base58": "EJqwFjvVJSAxH8Ur2PYuMfdvoJeutjmH6GkoEFQ4MdSa" }, - "bsc": { - "hex": "0x000000000000000000000000c27980812e2e66491fd457d488509b7e04144b98", - "base58": "1111111111113i9HKBuaFrAQeGvhv3fJnCCDkg7h" - }, "nautilus": { "hex": "0x0000000000000000000000004501bbe6e731a4bc5c60c03a77435b2f6d5e9fe7", "base58": "111111111111xm5qkrK7gZ8Cmjr4ggPLRxy2T8a" diff --git a/rust/sealevel/environments/mainnet2/warp-routes/zbc/token-config.json b/rust/sealevel/environments/mainnet2/warp-routes/zbc/token-config.json index af0d1439be..51e235a3e3 100644 --- a/rust/sealevel/environments/mainnet2/warp-routes/zbc/token-config.json +++ b/rust/sealevel/environments/mainnet2/warp-routes/zbc/token-config.json @@ -4,15 +4,9 @@ "decimals": 9, "remoteDecimals": 9, "token": "wzbcJyhGhQDLTV1S99apZiiBdE4jmYfbw99saMMdP59", - "splTokenProgram": "token" - }, - "bsc": { - "type": "collateral", - "decimals": 9, - "token": "0x37a56cdcD83Dce2868f721De58cB3830C44C6303", - "name": "Zebec", - "symbol": "ZBC", - "foreignDeployment": "0xC27980812E2E66491FD457D488509b7E04144b98" + "splTokenProgram": "token", + "interchainSecurityModule": "9k74DkJvS2x9QhG4XfnKsLkqaCDyVfaj8s6FyJyhAeEP", + "owner": "EzppBFV2taxWw8kEjxNYvby6q7W1biJEqwP3iC7YgRe3" }, "nautilus": { "type": "native", diff --git a/rust/sealevel/environments/testnet3/chain-config.json b/rust/sealevel/environments/testnet3/chain-config.json new file mode 100644 index 0000000000..ae5158a8b6 --- /dev/null +++ b/rust/sealevel/environments/testnet3/chain-config.json @@ -0,0 +1,335 @@ +{ + "alfajores": { + "chainId": 44787, + "domainId": 44787, + "name": "alfajores", + "protocol": "ethereum", + "displayName": "Alfajores", + "nativeToken": { + "decimals": 18, + "name": "CELO", + "symbol": "CELO" + }, + "rpcUrls": [ + { + "http": "https://alfajores-forno.celo-testnet.org" + } + ], + "blockExplorers": [ + { + "name": "CeloScan", + "url": "https://alfajores.celoscan.io", + "apiUrl": "https://api-alfajores.celoscan.io/api", + "family": "etherscan" + }, + { + "name": "Blockscout", + "url": "https://explorer.celo.org/alfajores", + "apiUrl": "https://explorer.celo.org/alfajores/api", + "family": "blockscout" + } + ], + "blocks": { + "confirmations": 1, + "reorgPeriod": 0, + "estimateBlockTime": 5 + }, + "isTestnet": true + }, + "fuji": { + "chainId": 43113, + "domainId": 43113, + "name": "fuji", + "protocol": "ethereum", + "displayName": "Fuji", + "nativeToken": { + "decimals": 18, + "name": "Avalanche", + "symbol": "AVAX" + }, + "rpcUrls": [ + { + "http": "https://api.avax-test.network/ext/bc/C/rpc", + "pagination": { + "maxBlockRange": 2048 + } + } + ], + "blockExplorers": [ + { + "name": "SnowTrace", + "url": "https://testnet.snowtrace.io", + "apiUrl": "https://api-testnet.snowtrace.io/api", + "family": "etherscan" + } + ], + "blocks": { + "confirmations": 3, + "reorgPeriod": 3, + "estimateBlockTime": 2 + }, + "isTestnet": true + }, + "mumbai": { + "chainId": 80001, + "domainId": 80001, + "name": "mumbai", + "protocol": "ethereum", + "displayName": "Mumbai", + "nativeToken": { + "name": "MATIC", + "symbol": "MATIC", + "decimals": 18 + }, + "rpcUrls": [ + { + "http": "https://rpc.ankr.com/polygon_mumbai", + "pagination": { + "maxBlockRange": 10000, + "minBlockNumber": 22900000 + } + }, + { + "http": "https://matic-mumbai.chainstacklabs.com" + } + ], + "blockExplorers": [ + { + "name": "PolygonScan", + "url": "https://mumbai.polygonscan.com", + "apiUrl": "https://api-testnet.polygonscan.com/api", + "family": "etherscan" + } + ], + "blocks": { + "confirmations": 3, + "reorgPeriod": 32, + "estimateBlockTime": 5 + }, + "isTestnet": true + }, + "bsctestnet": { + "chainId": 97, + "domainId": 97, + "name": "bsctestnet", + "protocol": "ethereum", + "displayName": "BSC Testnet", + "nativeToken": { + "decimals": 18, + "name": "BNB", + "symbol": "BNB" + }, + "rpcUrls": [ + { + "http": "https://data-seed-prebsc-1-s3.binance.org:8545" + } + ], + "blockExplorers": [ + { + "name": "BscScan", + "url": "https://testnet.bscscan.com", + "apiUrl": "https://api-testnet.bscscan.com/api", + "family": "etherscan" + } + ], + "blocks": { + "confirmations": 1, + "reorgPeriod": 9, + "estimateBlockTime": 3 + }, + "isTestnet": true + }, + "goerli": { + "chainId": 5, + "domainId": 5, + "name": "goerli", + "protocol": "ethereum", + "displayName": "Goerli", + "nativeToken": { + "name": "Ether", + "symbol": "ETH", + "decimals": 18 + }, + "rpcUrls": [ + { + "http": "https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161" + }, + { + "http": "https://rpc.ankr.com/eth_goerli" + }, + { + "http": "https://eth-goerli.public.blastapi.io" + } + ], + "blockExplorers": [ + { + "name": "Etherscan", + "url": "https://goerli.etherscan.io", + "apiUrl": "https://api-goerli.etherscan.io/api", + "family": "etherscan" + } + ], + "blocks": { + "confirmations": 1, + "reorgPeriod": 2, + "estimateBlockTime": 13 + }, + "isTestnet": true + }, + "moonbasealpha": { + "chainId": 1287, + "domainId": 1287, + "name": "moonbasealpha", + "protocol": "ethereum", + "displayName": "Moonbase Alpha", + "displayNameShort": "Moonbase", + "nativeToken": { + "decimals": 18, + "name": "DEV", + "symbol": "DEV" + }, + "rpcUrls": [ + { + "http": "https://rpc.api.moonbase.moonbeam.network" + } + ], + "blockExplorers": [ + { + "name": "MoonScan", + "url": "https://moonbase.moonscan.io", + "apiUrl": "https://api-moonbase.moonscan.io/api", + "family": "etherscan" + } + ], + "blocks": { + "confirmations": 1, + "reorgPeriod": 1, + "estimateBlockTime": 12 + }, + "isTestnet": true + }, + "optimismgoerli": { + "chainId": 420, + "domainId": 420, + "name": "optimismgoerli", + "protocol": "ethereum", + "displayName": "Optimism Goerli", + "displayNameShort": "Opt. Goerli", + "nativeToken": { + "name": "Ether", + "symbol": "ETH", + "decimals": 18 + }, + "rpcUrls": [ + { + "http": "https://goerli.optimism.io" + } + ], + "blockExplorers": [ + { + "name": "Etherscan", + "url": "https://goerli-optimism.etherscan.io", + "apiUrl": "https://api-goerli-optimism.etherscan.io/api", + "family": "etherscan" + } + ], + "blocks": { + "confirmations": 1, + "reorgPeriod": 1, + "estimateBlockTime": 3 + }, + "isTestnet": true + }, + "arbitrumgoerli": { + "chainId": 421613, + "domainId": 421613, + "name": "arbitrumgoerli", + "protocol": "ethereum", + "displayName": "Arbitrum Goerli", + "displayNameShort": "Arb. Goerli", + "nativeToken": { + "name": "Ether", + "symbol": "ETH", + "decimals": 18 + }, + "rpcUrls": [ + { + "http": "https://goerli-rollup.arbitrum.io/rpc" + } + ], + "blockExplorers": [ + { + "name": "Arbiscan", + "url": "https://goerli.arbiscan.io", + "apiUrl": "https://api-goerli.arbiscan.io/api", + "family": "etherscan" + } + ], + "blocks": { + "confirmations": 1, + "reorgPeriod": 1, + "estimateBlockTime": 3 + }, + "isTestnet": true + }, + "sepolia": { + "chainId": 11155111, + "domainId": 11155111, + "name": "sepolia", + "protocol": "ethereum", + "displayName": "Sepolia", + "nativeToken": { + "name": "Ether", + "symbol": "ETH", + "decimals": 18 + }, + "rpcUrls": [ + { + "http": "https://endpoints.omniatech.io/v1/eth/sepolia/public" + }, + { + "http": "https://rpc.sepolia.org" + } + ], + "blockExplorers": [ + { + "name": "Etherscan", + "url": "https://sepolia.etherscan.io", + "apiUrl": "https://api-sepolia.etherscan.io/api", + "family": "etherscan" + } + ], + "blocks": { + "confirmations": 1, + "reorgPeriod": 2, + "estimateBlockTime": 13 + }, + "isTestnet": true + }, + "solanadevnet": { + "chainId": 1399811151, + "name": "solanadevnet", + "rpcUrls": [ + { + "http": "https://api.devnet.solana.com" + } + ] + }, + "proteustestnet": { + "chainId": 88002, + "domainId": 88002, + "name": "proteustestnet", + "protocol": "ethereum", + "displayName": "Proteus Testnet", + "nativeToken": { + "name": "Zebec", + "symbol": "ZBC", + "decimals": 18 + }, + "rpcUrls": [ + { + "http": "https://api.proteus.nautchain.xyz/solana" + } + ] + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/helloworld/hyperlane/helloworld-config.json b/rust/sealevel/environments/testnet3/helloworld/hyperlane/helloworld-config.json new file mode 100644 index 0000000000..a03eae987f --- /dev/null +++ b/rust/sealevel/environments/testnet3/helloworld/hyperlane/helloworld-config.json @@ -0,0 +1,32 @@ +{ + "solanadevnet": { + "interchainSecurityModule": "64xkGhsZbxgP5rBJfpPcpmzkzTGkpSVHiDLcMKS5gmQw" + }, + "alfajores": { + "foreignDeployment": "0x477D860f8F41bC69dDD32821F2Bf2C2Af0243F16" + }, + "fuji": { + "foreignDeployment": "0x5da3b8d6F73dF6003A490072106730218c475AAd" + }, + "mumbai": { + "foreignDeployment": "0x1A4d8a5eD6C93Af828655e15C44eeE2c2851F0D6" + }, + "bsctestnet": { + "foreignDeployment": "0xE09BF59dCA6e622efC33f6fbd8EF85dE45233388" + }, + "goerli": { + "foreignDeployment": "0x405BFdEcB33230b4Ad93C29ba4499b776CfBa189" + }, + "moonbasealpha": { + "foreignDeployment": "0x89e02C3C7b97bCBa63279E10E2a44e6cEF69E6B2" + }, + "optimismgoerli": { + "foreignDeployment": "0x3582d1238cBC812165981E4fFaB0E8D9a4518910" + }, + "arbitrumgoerli": { + "foreignDeployment": "0x339B46496D60b1b6B42e9715DeD8B3D2154dA0Bb" + }, + "sepolia": { + "foreignDeployment": "0x5d56B8a669F50193b54319442c6EEE5edD662381" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/helloworld/hyperlane/keys/hyperlane_sealevel_hello_world-solanadevnet.json b/rust/sealevel/environments/testnet3/helloworld/hyperlane/keys/hyperlane_sealevel_hello_world-solanadevnet.json new file mode 100644 index 0000000000..db75227a7c --- /dev/null +++ b/rust/sealevel/environments/testnet3/helloworld/hyperlane/keys/hyperlane_sealevel_hello_world-solanadevnet.json @@ -0,0 +1 @@ +[42,226,42,33,87,42,251,0,57,248,173,166,139,84,91,50,218,150,183,254,74,195,88,116,92,195,145,231,63,39,9,98,171,58,146,166,209,139,158,82,151,114,58,235,5,25,129,244,219,192,239,35,53,229,191,115,243,59,174,210,94,26,161,101] \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/helloworld/hyperlane/program-ids.json b/rust/sealevel/environments/testnet3/helloworld/hyperlane/program-ids.json new file mode 100644 index 0000000000..1eca03712d --- /dev/null +++ b/rust/sealevel/environments/testnet3/helloworld/hyperlane/program-ids.json @@ -0,0 +1,42 @@ +{ + "bsctestnet": { + "hex": "0x000000000000000000000000e09bf59dca6e622efc33f6fbd8ef85de45233388", + "base58": "11111111111148VaL9DFuVc9DbDjRR7c3qyCEjyy" + }, + "optimismgoerli": { + "hex": "0x0000000000000000000000003582d1238cbc812165981e4ffab0e8d9a4518910", + "base58": "111111111111kEreeMSXc3Nh2JoYtisyZj8pb6X" + }, + "fuji": { + "hex": "0x0000000000000000000000005da3b8d6f73df6003a490072106730218c475aad", + "base58": "1111111111112JfXZf7EYaEMM1st6wFZbcLN2uwA" + }, + "solanadevnet": { + "hex": "0xab3a92a6d18b9e5297723aeb051981f4dbc0ef2335e5bf73f33baed25e1aa165", + "base58": "CXQX54kdkU5GqdRJjCmHpwHfEMgFb5SeBmMWntP2Ds7J" + }, + "mumbai": { + "hex": "0x0000000000000000000000001a4d8a5ed6c93af828655e15c44eee2c2851f0d6", + "base58": "111111111111NFiXSaSzEqfGUNtwSd5dSDgCymP" + }, + "alfajores": { + "hex": "0x000000000000000000000000477d860f8f41bc69ddd32821f2bf2c2af0243f16", + "base58": "111111111111zmUjMVNXAe5bcqPR8cvaPz5SrQu" + }, + "goerli": { + "hex": "0x000000000000000000000000405bfdecb33230b4ad93c29ba4499b776cfba189", + "base58": "111111111111u1H27LrKRuu1G7bDpPWUXKphQSt" + }, + "sepolia": { + "hex": "0x0000000000000000000000005d56b8a669f50193b54319442c6eee5edd662381", + "base58": "1111111111112JRRxgtLh6eyMDsTHUehn6bJcPJ8" + }, + "arbitrumgoerli": { + "hex": "0x000000000000000000000000339b46496d60b1b6b42e9715ded8b3d2154da0bb", + "base58": "111111111111ihbsGG5PRTKTSYSGewGtDFs2vfc" + }, + "moonbasealpha": { + "hex": "0x00000000000000000000000089e02c3c7b97bcba63279e10e2a44e6cef69e6b2", + "base58": "1111111111112vQhuwgKwhQ7SM1HZEm6yXQkzCau" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/helloworld/rc/helloworld-config.json b/rust/sealevel/environments/testnet3/helloworld/rc/helloworld-config.json new file mode 100644 index 0000000000..e2a072b56c --- /dev/null +++ b/rust/sealevel/environments/testnet3/helloworld/rc/helloworld-config.json @@ -0,0 +1,32 @@ +{ + "solanadevnet": { + "interchainSecurityModule": "2NE6Y1rXp1Kpp6vBNqDHYL7HNk7iqh8BKmvCoZtUcZLn" + }, + "alfajores": { + "foreignDeployment": "0x40Adcb03F3C58170b4751c4140636FC6085Ff475" + }, + "fuji": { + "foreignDeployment": "0xAc003FcDD0EE223664F2A000B5A59D082745700b" + }, + "mumbai": { + "foreignDeployment": "0xaB0892029C3E7dD4c0235590dc296E618A7b4d03" + }, + "bsctestnet": { + "foreignDeployment": "0xd259b0e793535325786675542aB296c451535c27" + }, + "goerli": { + "foreignDeployment": "0x03e9531ae74e8F0f96DE26788a22d35bdaD24185" + }, + "moonbasealpha": { + "foreignDeployment": "0xE9D6317a10860340f035f3d09052D9d376855bE8" + }, + "optimismgoerli": { + "foreignDeployment": "0x057d38d184d74192B96840D8FbB37e584dDb569A" + }, + "arbitrumgoerli": { + "foreignDeployment": "0xaAF1BF6f2BfaE290ea8615066fd167e396a2f578" + }, + "sepolia": { + "foreignDeployment": "0x6AD4DEBA8A147d000C09de6465267a9047d1c217" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/helloworld/rc/keys/hyperlane_sealevel_hello_world-solanadevnet.json b/rust/sealevel/environments/testnet3/helloworld/rc/keys/hyperlane_sealevel_hello_world-solanadevnet.json new file mode 100644 index 0000000000..341933a08e --- /dev/null +++ b/rust/sealevel/environments/testnet3/helloworld/rc/keys/hyperlane_sealevel_hello_world-solanadevnet.json @@ -0,0 +1 @@ +[158,232,241,234,223,84,236,122,65,31,146,220,11,236,43,97,184,113,181,80,237,157,204,188,166,199,112,171,77,38,68,13,187,162,244,131,230,66,68,157,10,57,239,229,249,96,63,124,85,148,35,172,235,211,200,84,208,117,96,204,208,67,146,40] \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/helloworld/rc/program-ids.json b/rust/sealevel/environments/testnet3/helloworld/rc/program-ids.json new file mode 100644 index 0000000000..99f4db5461 --- /dev/null +++ b/rust/sealevel/environments/testnet3/helloworld/rc/program-ids.json @@ -0,0 +1,42 @@ +{ + "sepolia": { + "hex": "0x0000000000000000000000006ad4deba8a147d000c09de6465267a9047d1c217", + "base58": "1111111111112VKnX2KMsqSTDw9YoXRsZJTwTcUW" + }, + "goerli": { + "hex": "0x00000000000000000000000003e9531ae74e8f0f96de26788a22d35bdad24185", + "base58": "1111111111114AKBbRbDjAP93LQgmXJPvfVU7SC" + }, + "solanadevnet": { + "hex": "0xbba2f483e642449d0a39efe5f9603f7c559423acebd3c854d07560ccd0439228", + "base58": "DdTMkk9nuqH5LnD56HLkPiKMV3yB3BNEYSQfgmJHa5i7" + }, + "optimismgoerli": { + "hex": "0x000000000000000000000000057d38d184d74192b96840d8fbb37e584ddb569a", + "base58": "1111111111115SFp65pWdvPTRK5fmHa3sc4Eq6Z" + }, + "fuji": { + "hex": "0x000000000000000000000000ac003fcdd0ee223664f2a000b5a59d082745700b", + "base58": "1111111111113Pz2bmxxVNgkKkZPpxgouHiZAjTx" + }, + "moonbasealpha": { + "hex": "0x000000000000000000000000e9d6317a10860340f035f3d09052d9d376855be8", + "base58": "1111111111114Fx2onL6wvVgGmyjgzGhy48HzCZM" + }, + "arbitrumgoerli": { + "hex": "0x000000000000000000000000aaf1bf6f2bfae290ea8615066fd167e396a2f578", + "base58": "1111111111113P8WPEsejkHP1Zysy1xXafVFFnaT" + }, + "bsctestnet": { + "hex": "0x000000000000000000000000d259b0e793535325786675542ab296c451535c27", + "base58": "1111111111113vyKMMTb6aSQDhDLqEvqcPBcTtRC" + }, + "alfajores": { + "hex": "0x00000000000000000000000040adcb03f3c58170b4751c4140636fc6085ff475", + "base58": "111111111111uGFbQYrmpk8K5cfeu9x438LAGiQ" + }, + "mumbai": { + "hex": "0x000000000000000000000000ab0892029c3e7dd4c0235590dc296e618a7b4d03", + "base58": "1111111111113PCgiXuWFu2FmvhykJp51x5y5jyC" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/multisig-ism-message-id/solanadevnet/hyperlane/multisig-config.json b/rust/sealevel/environments/testnet3/multisig-ism-message-id/solanadevnet/hyperlane/multisig-config.json new file mode 100644 index 0000000000..c758f40bc3 --- /dev/null +++ b/rust/sealevel/environments/testnet3/multisig-ism-message-id/solanadevnet/hyperlane/multisig-config.json @@ -0,0 +1,92 @@ +{ + "alfajores": { + "type": 3, + "threshold": 2, + "validators": [ + "0xe6072396568e73ce6803b12b7e04164e839f1e54", + "0x9f177f51289b22515f41f95872e1511391b8e105", + "0x15f77400845eb1c971ad08de050861d5508cad6c" + ] + }, + "fuji": { + "type": 3, + "threshold": 2, + "validators": [ + "0x9fa19ead5ec76e437948b35e227511b106293c40", + "0x227e7d6507762ece0c94678f8c103eff9d682476", + "0x2379e43740e4aa4fde48cf4f00a3106df1d8420d" + ] + }, + "mumbai": { + "type": 3, + "threshold": 2, + "validators": [ + "0x0a664ea799447da6b15645cf8b9e82072a68343f", + "0x6ae6f12929a960aba24ba74ea310e3d37d0ac045", + "0x51f70c047cd73bc7873273707501568857a619c4" + ] + }, + "bsctestnet": { + "type": 3, + "threshold": 2, + "validators": [ + "0x23338c8714976dd4a57eaeff17cbd26d7e275c08", + "0x85a618d7450ebc37e0d682371f08dac94eec7a76", + "0x95b76562e4ba1791a27ba4236801271c9115b141" + ] + }, + "goerli": { + "type": 3, + "threshold": 2, + "validators": [ + "0xf43fbd072fd38e1121d4b3b0b8a35116bbb01ea9", + "0xa33020552a21f35e75bd385c6ab95c3dfa82d930", + "0x0bba4043ff242f8bf3f39bafa8930a84d644d947" + ] + }, + "sepolia": { + "type": 3, + "threshold": 2, + "validators": [ + "0xbc748ee311f5f2d1975d61cdf531755ce8ce3066", + "0xc4233b2bfe5aec08964a94b403052abb3eafcf07", + "0x6b36286c19f5c10bdc139ea9ee7f82287303f61d" + ] + }, + "moonbasealpha": { + "type": 3, + "threshold": 2, + "validators": [ + "0x890c2aeac157c3f067f3e42b8afc797939c59a32", + "0x1b06d6fe69b972ed7420c83599d5a5c0fc185904", + "0xe70b85206a968a99a597581f0fa09c99e7681093" + ] + }, + "optimismgoerli": { + "type": 3, + "threshold": 2, + "validators": [ + "0xbb8d77eefbecc55db6e5a19b0fc3dc290776f189", + "0x69792508b4ddaa3ca52241ccfcd1e0b119a1ee65", + "0x11ddb46c6b653e0cdd7ad5bee32ae316e18f8453" + ] + }, + "arbitrumgoerli": { + "type": 3, + "threshold": 2, + "validators": [ + "0xce798fa21e323f6b24d9838a10ffecdefdfc4f30", + "0xa792d39dca4426927e0f00c1618d61c9cb41779d", + "0xdf181fcc11dfac5d01467e4547101a856dd5aa04" + ] + }, + "proteustestnet": { + "type": 3, + "threshold": 2, + "validators": [ + "0x79fc73656abb9eeaa5ee853c4569124f5bdaf9d8", + "0x72840388d5ab57323bc4f6e6d3ddedfd5cc911f0", + "0xd4b2a50c53fc6614bb3cd3198e0fdc03f5da973f" + ] + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/multisig-ism-message-id/solanadevnet/rc/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json b/rust/sealevel/environments/testnet3/multisig-ism-message-id/solanadevnet/rc/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json new file mode 100644 index 0000000000..8542855f64 --- /dev/null +++ b/rust/sealevel/environments/testnet3/multisig-ism-message-id/solanadevnet/rc/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json @@ -0,0 +1 @@ +[187,239,78,162,24,178,190,184,243,9,66,169,19,139,40,129,55,222,218,2,184,14,122,68,163,6,144,157,76,14,169,237,20,75,176,226,241,81,96,106,31,68,222,130,94,67,105,175,112,84,241,60,117,11,107,135,95,48,20,213,115,123,100,3] \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/multisig-ism-message-id/solanadevnet/rc/multisig-config.json b/rust/sealevel/environments/testnet3/multisig-ism-message-id/solanadevnet/rc/multisig-config.json new file mode 100644 index 0000000000..870829e4ab --- /dev/null +++ b/rust/sealevel/environments/testnet3/multisig-ism-message-id/solanadevnet/rc/multisig-config.json @@ -0,0 +1,65 @@ +{ + "alfajores": { + "type": 3, + "threshold": 1, + "validators": [ + "0x45e5c228b38e1cf09e9a3423ed0cf4862c4bf3de" + ] + }, + "fuji": { + "type": 3, + "threshold": 1, + "validators": [ + "0xd81ba169170a9b582812cf0e152d2c168572e21f" + ] + }, + "mumbai": { + "type": 3, + "threshold": 1, + "validators": [ + "0xb537c4ce34e1cad718be52aa30b095e416eae46a" + ] + }, + "bsctestnet": { + "type": 3, + "threshold": 1, + "validators": [ + "0x77f80ef5b18977e15d81aea8dd3a88e7df4bc0eb" + ] + }, + "goerli": { + "type": 3, + "threshold": 1, + "validators": [ + "0x9597ddb4ad2af237665559574b820596bb77ae7a" + ] + }, + "sepolia": { + "type": 3, + "threshold": 1, + "validators": [ + "0x183f15924f3a464c54c9393e8d268eb44d2b208c" + ] + }, + "moonbasealpha": { + "type": 3, + "threshold": 1, + "validators": [ + "0xbeaf158f85d7b64ced36b8aea0bbc4cd0f2d1a5d" + ] + }, + "optimismgoerli": { + "type": 3, + "threshold": 1, + "validators": [ + "0x1d6798671ac532f2bf30c3a5230697a4695705e4" + ] + }, + "arbitrumgoerli": { + "type": 3, + "threshold": 1, + "validators": [ + "0x6d13367c7cd713a4ea79a2552adf824bf1ecdd5e" + ] + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/multisig-ism-message-id/solanadevnet/rc/program-ids.json b/rust/sealevel/environments/testnet3/multisig-ism-message-id/solanadevnet/rc/program-ids.json new file mode 100644 index 0000000000..e615b252e8 --- /dev/null +++ b/rust/sealevel/environments/testnet3/multisig-ism-message-id/solanadevnet/rc/program-ids.json @@ -0,0 +1,3 @@ +{ + "program_id": "2NE6Y1rXp1Kpp6vBNqDHYL7HNk7iqh8BKmvCoZtUcZLn" +} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/solanadevnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json b/rust/sealevel/environments/testnet3/solanadevnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json index 69fd3baad8..d86f1638c3 100644 --- a/rust/sealevel/environments/testnet3/solanadevnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json +++ b/rust/sealevel/environments/testnet3/solanadevnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json @@ -1 +1 @@ -[63,21,4,198,48,27,204,153,114,92,118,116,234,163,49,14,128,10,0,19,56,226,121,151,6,205,21,108,169,125,212,113,29,16,150,112,133,212,123,146,110,230,188,148,124,117,183,159,93,85,69,97,122,78,86,187,44,166,129,154,160,73,41,186] \ No newline at end of file +[247,149,169,2,196,128,74,124,111,206,244,112,63,16,180,19,219,212,45,229,21,114,33,11,202,148,12,47,22,26,192,78,75,78,53,149,190,51,57,253,29,141,136,215,159,45,181,164,239,148,140,163,30,108,158,76,94,113,11,4,142,0,192,20] \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/solanadevnet/core/program-ids.json b/rust/sealevel/environments/testnet3/solanadevnet/core/program-ids.json index 324e0f3d48..ab3f1f5933 100644 --- a/rust/sealevel/environments/testnet3/solanadevnet/core/program-ids.json +++ b/rust/sealevel/environments/testnet3/solanadevnet/core/program-ids.json @@ -1,7 +1,7 @@ { "mailbox": "4v25Dz9RccqUrTzmfHzJMsjd1iVoNrWzeJ4o6GYuJrVn", "validator_announce": "CMHKvdq4CopDf7qXnDCaTybS15QekQeRt4oUB219yxsp", - "multisig_ism_message_id": "2xTVcwDWZgBu69aawCdYHXqH7xQP36iBQ7rN2px1g7ms", + "multisig_ism_message_id": "64xkGhsZbxgP5rBJfpPcpmzkzTGkpSVHiDLcMKS5gmQw", "igp_program_id": "HyPQPLfGXDTAQTxzGp7r1uy18KxS89GKgreSHpjeuYDn", "overhead_igp_account": "AR4hjWPqXEobLvzmv8MTh5k4Se49iTDzbvNX4DpdQGJZ", "igp_account": "7hMPEGdgBQFsjEz3aaNwZp8WMFHs615zAM3erXBDJuJR" diff --git a/rust/sealevel/environments/testnet3/warp-routes/chain-config.json b/rust/sealevel/environments/testnet3/warp-routes/chain-config.json deleted file mode 100644 index 06517c08d9..0000000000 --- a/rust/sealevel/environments/testnet3/warp-routes/chain-config.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "solanadevnet": { - "chainId": 1399811151, - "name": "solanadevnet", - "publicRpcUrls": [ - { - "http": "https://api.devnet.solana.com" - } - ] - }, - "bsctestnet": { - "chainId": 97, - "name": "bsctestnet", - "publicRpcUrls": [ - { - "http": "https://rpc.ankr.com/bsc_testnet_chapel" - } - ] - }, - "proteustestnet": { - "chainId": 88002, - "name": "proteustestnet", - "publicRpcUrls": [ - { - "http": "https://api.proteus.nautchain.xyz/solana" - } - ] - } -} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/warp-routes/proteustest/program-ids.json b/rust/sealevel/environments/testnet3/warp-routes/proteustest/program-ids.json index 316560afb1..f2372bc90c 100644 --- a/rust/sealevel/environments/testnet3/warp-routes/proteustest/program-ids.json +++ b/rust/sealevel/environments/testnet3/warp-routes/proteustest/program-ids.json @@ -1,14 +1,14 @@ { - "bsctestnet": { - "hex": "0x00000000000000000000000031b5234a896fbc4b3e2f7237592d054716762131", - "base58": "111111111111hAc1aTgvQGRBFHrYpXpfUqGyqgk" + "solanadevnet": { + "hex": "0x05b6502b1d91c60ca0c0d0ab20a16ec40c66f2559becc7888a4fc3c0cefff9a5", + "base58": "PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx" }, "proteustestnet": { "hex": "0x00000000000000000000000034a9af13c5555bad0783c220911b9ef59cfdbcef", "base58": "111111111111jZ775N1rpEpJ2M8RAzLNNr9Lh7U" }, - "solanadevnet": { - "hex": "0x05b6502b1d91c60ca0c0d0ab20a16ec40c66f2559becc7888a4fc3c0cefff9a5", - "base58": "PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx" + "bsctestnet": { + "hex": "0x00000000000000000000000031b5234a896fbc4b3e2f7237592d054716762131", + "base58": "111111111111hAc1aTgvQGRBFHrYpXpfUqGyqgk" } } \ No newline at end of file diff --git a/rust/sealevel/libraries/account-utils/src/lib.rs b/rust/sealevel/libraries/account-utils/src/lib.rs index 6301c66f43..aa7fa69744 100644 --- a/rust/sealevel/libraries/account-utils/src/lib.rs +++ b/rust/sealevel/libraries/account-utils/src/lib.rs @@ -154,11 +154,15 @@ where { /// Stores the account data in the given account, reallocing the account /// if necessary, and ensuring it is rent exempt. + /// Requires `_system_program_info` to be passed in despite not being directly + /// used to ensure that a CPI to the system program in the case of a realloc + /// that requires additional lamports for rent exemption will be successful. pub fn store_with_rent_exempt_realloc<'a, 'b>( &self, account_info: &'a AccountInfo<'b>, rent: &Rent, payer_info: &'a AccountInfo<'b>, + _system_program_info: &'a AccountInfo<'b>, ) -> Result<(), ProgramError> { let required_size = self.size(); diff --git a/rust/sealevel/libraries/hyperlane-sealevel-token/src/instruction.rs b/rust/sealevel/libraries/hyperlane-sealevel-token/src/instruction.rs index 40cf0f6007..ae1276a226 100644 --- a/rust/sealevel/libraries/hyperlane-sealevel-token/src/instruction.rs +++ b/rust/sealevel/libraries/hyperlane-sealevel-token/src/instruction.rs @@ -201,6 +201,35 @@ pub fn transfer_ownership_instruction( Ok(instruction) } +/// Gets an instruction to set the ISM. +pub fn set_interchain_security_module_instruction( + program_id: Pubkey, + owner_payer: Pubkey, + new_interchain_security_module: Option, +) -> Result { + let (token_key, _token_bump) = + Pubkey::try_find_program_address(hyperlane_token_pda_seeds!(), &program_id) + .ok_or(ProgramError::InvalidSeeds)?; + + let ixn = Instruction::SetInterchainSecurityModule(new_interchain_security_module); + + // Accounts: + // 0. [writeable] The token PDA account. + // 1. [signer] The current owner. + let accounts = vec![ + AccountMeta::new(token_key, false), + AccountMeta::new_readonly(owner_payer, true), + ]; + + let instruction = SolanaInstruction { + program_id, + data: ixn.encode()?, + accounts, + }; + + Ok(instruction) +} + /// Sets the igp for a warp route pub fn set_igp_instruction( program_id: Pubkey, diff --git a/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs b/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs index 04587274b8..c104828572 100644 --- a/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs +++ b/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs @@ -661,6 +661,7 @@ where token_account, &Rent::get()?, owner_account, + system_program, )?; Ok(()) @@ -709,6 +710,7 @@ where token_account, &Rent::get()?, owner_account, + system_program, )?; Ok(()) @@ -849,6 +851,7 @@ where token_account, &Rent::get()?, owner_account, + system_program, )?; Ok(()) diff --git a/rust/sealevel/programs/helloworld/Cargo.toml b/rust/sealevel/programs/helloworld/Cargo.toml new file mode 100644 index 0000000000..b0cbf2eaf0 --- /dev/null +++ b/rust/sealevel/programs/helloworld/Cargo.toml @@ -0,0 +1,30 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-hello-world" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] +test-client = ["dep:solana-program-test", "dep:solana-sdk", "dep:hyperlane-test-utils", "dep:spl-noop"] + +[dependencies] +borsh.workspace = true +solana-program-test = { workspace = true, optional = true } +solana-program.workspace = true +solana-sdk = { workspace = true, optional = true } +spl-noop = { workspace = true, optional = true } + +access-control = { path = "../../libraries/access-control" } +account-utils = { path = "../../libraries/account-utils" } +hyperlane-core = { path = "../../../hyperlane-core" } +hyperlane-sealevel-mailbox = { path = "../mailbox", features = ["no-entrypoint"] } +hyperlane-sealevel-igp = { path = "../hyperlane-sealevel-igp", features = ["no-entrypoint"] } +hyperlane-sealevel-connection-client = { path = "../../libraries/hyperlane-sealevel-connection-client" } +hyperlane-sealevel-message-recipient-interface = { path = "../../libraries/message-recipient-interface" } +hyperlane-test-utils = { path = "../../libraries/test-utils", optional = true } +serializable-account-meta = { path = "../../libraries/serializable-account-meta" } + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/programs/helloworld/src/accounts.rs b/rust/sealevel/programs/helloworld/src/accounts.rs new file mode 100644 index 0000000000..14e68c3b4c --- /dev/null +++ b/rust/sealevel/programs/helloworld/src/accounts.rs @@ -0,0 +1,104 @@ +//! HelloWorld accounts. +use std::collections::HashMap; + +use access_control::AccessControl; +use account_utils::{AccountData, SizedData}; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_core::H256; +use hyperlane_sealevel_connection_client::{ + router::{HyperlaneRouter, RemoteRouterConfig}, + HyperlaneConnectionClient, +}; +use hyperlane_sealevel_igp::accounts::InterchainGasPaymasterType; + +use solana_program::{program_error::ProgramError, pubkey::Pubkey}; + +/// The storage account. +pub type HelloWorldStorageAccount = AccountData; + +/// The storage account's data. +#[derive(BorshSerialize, BorshDeserialize, Debug, Default)] +pub struct HelloWorldStorage { + /// The local domain. + pub local_domain: u32, + /// The mailbox. + pub mailbox: Pubkey, + /// The ISM. + pub ism: Option, + /// The IGP. + pub igp: Option<(Pubkey, InterchainGasPaymasterType)>, + /// The owner. + pub owner: Option, + /// A counter of how many messages have been sent from this contract. + pub sent: u64, + /// A counter of how many messages have been received by this contract. + pub received: u64, + /// Keyed by domain, a counter of how many messages that have been sent + /// from this contract to the domain. + pub sent_to: HashMap, + /// Keyed by domain, a counter of how many messages that have been received + /// by this contract from the domain. + pub received_from: HashMap, + /// Keyed by domain, the router for the remote domain. + pub routers: HashMap, +} + +impl SizedData for HelloWorldStorage { + fn size(&self) -> usize { + // local domain + std::mem::size_of::() + + // mailbox + 32 + + // ism + 1 + 32 + + // igp + 1 + 32 + 1 + 32 + + // owner + 1 + 32 + + // sent + std::mem::size_of::() + + // received + std::mem::size_of::() + + // sent_to + (self.sent_to.len() * (std::mem::size_of::() + std::mem::size_of::())) + + // received_from + (self.received_from.len() * (std::mem::size_of::() + std::mem::size_of::())) + + // routers + (self.routers.len() * (std::mem::size_of::() + 32)) + } +} + +impl AccessControl for HelloWorldStorage { + fn owner(&self) -> Option<&Pubkey> { + self.owner.as_ref() + } + + fn set_owner(&mut self, new_owner: Option) -> Result<(), ProgramError> { + self.owner = new_owner; + Ok(()) + } +} + +impl HyperlaneRouter for HelloWorldStorage { + fn router(&self, origin: u32) -> Option<&H256> { + self.routers.get(&origin) + } + + fn enroll_remote_router(&mut self, config: RemoteRouterConfig) { + self.routers.insert(config.domain, config.router.unwrap()); + } +} + +impl HyperlaneConnectionClient for HelloWorldStorage { + fn mailbox(&self) -> &Pubkey { + &self.mailbox + } + + fn interchain_gas_paymaster(&self) -> Option<&(Pubkey, InterchainGasPaymasterType)> { + self.igp.as_ref() + } + + fn interchain_security_module(&self) -> Option<&Pubkey> { + self.ism.as_ref() + } +} diff --git a/rust/sealevel/programs/helloworld/src/instruction.rs b/rust/sealevel/programs/helloworld/src/instruction.rs new file mode 100644 index 0000000000..5c0a42edac --- /dev/null +++ b/rust/sealevel/programs/helloworld/src/instruction.rs @@ -0,0 +1,146 @@ +//! HelloWorld instructions. + +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_sealevel_connection_client::router::RemoteRouterConfig; +use hyperlane_sealevel_igp::accounts::InterchainGasPaymasterType; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::program_storage_pda_seeds; + +/// Init instruction data. +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct Init { + /// The local domain. + pub local_domain: u32, + /// The mailbox. + pub mailbox: Pubkey, + /// The ISM. + pub ism: Option, + /// The IGP. + pub igp: Option<(Pubkey, InterchainGasPaymasterType)>, + /// The owner. + pub owner: Option, +} + +/// A HelloWorld message. +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct HelloWorldMessage { + /// The destination domain. + pub destination: u32, + /// The message. + pub message: String, +} + +/// Instructions for the program. +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub enum HelloWorldInstruction { + /// Initializes the program. + Init(Init), + /// Dispatches a message using the dispatch authority. + SendHelloWorld(HelloWorldMessage), + /// Sets the ISM. + SetInterchainSecurityModule(Option), + /// Enrolls remote routers + EnrollRemoteRouters(Vec), +} + +/// Gets an instruction to initialize the program. +pub fn init_instruction( + program_id: Pubkey, + payer: Pubkey, + local_domain: u32, + mailbox: Pubkey, + ism: Option, + igp: Option<(Pubkey, InterchainGasPaymasterType)>, + owner: Option, +) -> Result { + let (program_storage_account, _program_storage_bump) = + Pubkey::try_find_program_address(program_storage_pda_seeds!(), &program_id) + .ok_or(ProgramError::InvalidSeeds)?; + + let init = Init { + local_domain, + mailbox, + ism, + igp, + owner, + }; + + // Accounts: + // 0. [executable] System program. + // 1. [signer] Payer. + // 2. [writeable] Storage PDA. + let accounts = vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(payer, true), + AccountMeta::new(program_storage_account, false), + ]; + + let instruction = Instruction { + program_id, + data: HelloWorldInstruction::Init(init).try_to_vec()?, + accounts, + }; + + Ok(instruction) +} + +/// Gets an instruction to enroll remote routers. +pub fn enroll_remote_routers_instruction( + program_id: Pubkey, + owner: Pubkey, + configs: Vec, +) -> Result { + let (program_storage_account, _program_storage_bump) = + Pubkey::try_find_program_address(program_storage_pda_seeds!(), &program_id) + .ok_or(ProgramError::InvalidSeeds)?; + + // Accounts: + // 0. [executable] System program. + // 1. [signer] Payer. + // 2. [writeable] Storage PDA. + let accounts = vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(program_storage_account, false), + AccountMeta::new(owner, true), + ]; + + let instruction = Instruction { + program_id, + data: HelloWorldInstruction::EnrollRemoteRouters(configs).try_to_vec()?, + accounts, + }; + + Ok(instruction) +} + +/// Gets an instruction to set the interchain security module. +pub fn set_interchain_security_module_instruction( + program_id: Pubkey, + owner: Pubkey, + ism: Option, +) -> Result { + let (program_storage_account, _program_storage_bump) = + Pubkey::try_find_program_address(program_storage_pda_seeds!(), &program_id) + .ok_or(ProgramError::InvalidSeeds)?; + + // Accounts: + // 0. [writeable] Storage PDA account. + // 1. [signer] Owner. + let accounts = vec![ + AccountMeta::new(program_storage_account, false), + AccountMeta::new(owner, true), + ]; + + let instruction = Instruction { + program_id, + data: HelloWorldInstruction::SetInterchainSecurityModule(ism).try_to_vec()?, + accounts, + }; + + Ok(instruction) +} diff --git a/rust/sealevel/programs/helloworld/src/lib.rs b/rust/sealevel/programs/helloworld/src/lib.rs new file mode 100644 index 0000000000..7900575e42 --- /dev/null +++ b/rust/sealevel/programs/helloworld/src/lib.rs @@ -0,0 +1,9 @@ +//! A HelloWorld program that sends and receives messages to & from other routers. + +#![deny(warnings)] +#![deny(missing_docs)] +#![deny(unsafe_code)] + +pub mod accounts; +pub mod instruction; +pub mod processor; diff --git a/rust/sealevel/programs/helloworld/src/processor.rs b/rust/sealevel/programs/helloworld/src/processor.rs new file mode 100644 index 0000000000..4e1fdcd924 --- /dev/null +++ b/rust/sealevel/programs/helloworld/src/processor.rs @@ -0,0 +1,542 @@ +//! HelloWorld program. + +use access_control::AccessControl; +use account_utils::{create_pda_account, SizedData}; +use borsh::{BorshDeserialize, BorshSerialize}; + +use hyperlane_sealevel_connection_client::{ + router::{HyperlaneRouterAccessControl, HyperlaneRouterDispatch, RemoteRouterConfig}, + HyperlaneConnectionClient, +}; +use hyperlane_sealevel_igp::accounts::InterchainGasPaymasterType; +use hyperlane_sealevel_mailbox::{ + mailbox_message_dispatch_authority_pda_seeds, mailbox_process_authority_pda_seeds, +}; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use serializable_account_meta::{SerializableAccountMeta, SimulationReturnData}; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + instruction::AccountMeta, + msg, + program::set_return_data, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + system_program, + sysvar::Sysvar, +}; + +use crate::{ + accounts::{HelloWorldStorage, HelloWorldStorageAccount}, + instruction::{HelloWorldInstruction, HelloWorldMessage, Init}, +}; + +/// The amount of gas to pay for. +/// TODO: when we actually enforce gas amounts for messages to Solana, +/// we'll need to revisit this and change HelloWorld to use GasRouter. +pub const HANDLE_GAS_AMOUNT: u64 = 50000; + +/// Seeds relating to the PDA account with program data. +#[macro_export] +macro_rules! program_storage_pda_seeds { + () => {{ + &[b"hello_world", b"-", b"handle", b"-", b"storage"] + }}; + + ($bump_seed:expr) => {{ + &[ + b"hello_world", + b"-", + b"handle", + b"-", + b"storage", + &[$bump_seed], + ] + }}; +} + +#[cfg(not(feature = "no-entrypoint"))] +solana_program::entrypoint!(process_instruction); + +/// The program's entrypoint. +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + if let Ok(recipient_instruction) = MessageRecipientInstruction::decode(instruction_data) { + return match recipient_instruction { + MessageRecipientInstruction::InterchainSecurityModule => { + get_interchain_security_module(program_id, accounts) + } + MessageRecipientInstruction::InterchainSecurityModuleAccountMetas => { + set_account_meta_return_data(program_id) + } + MessageRecipientInstruction::Handle(instruction) => { + handle(program_id, accounts, instruction) + } + MessageRecipientInstruction::HandleAccountMetas(_) => { + set_account_meta_return_data(program_id) + } + }; + } + + let instruction = HelloWorldInstruction::try_from_slice(instruction_data) + .map_err(|_| ProgramError::InvalidInstructionData)?; + match instruction { + HelloWorldInstruction::Init(mailbox) => init(program_id, accounts, mailbox), + HelloWorldInstruction::SendHelloWorld(hello_world) => { + send_hello_world(program_id, accounts, hello_world) + } + HelloWorldInstruction::SetInterchainSecurityModule(ism) => { + set_interchain_security_module(program_id, accounts, ism) + } + HelloWorldInstruction::EnrollRemoteRouters(configs) => { + enroll_remote_routers(program_id, accounts, configs) + } + } +} + +/// Creates the storage PDA. +/// +/// Accounts: +/// 0. [executable] System program. +/// 1. [signer] Payer. +/// 2. [writeable] Storage PDA. +fn init(program_id: &Pubkey, accounts: &[AccountInfo], init: Init) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: System program. + let system_program_info = next_account_info(accounts_iter)?; + if system_program_info.key != &system_program::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 1: Payer. + let payer_info = next_account_info(accounts_iter)?; + if !payer_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + // Account 2: Storage PDA. + let storage_info = next_account_info(accounts_iter)?; + let (storage_pda_key, storage_pda_bump_seed) = + Pubkey::find_program_address(program_storage_pda_seeds!(), program_id); + if storage_info.key != &storage_pda_key { + return Err(ProgramError::InvalidArgument); + } + + let storage_account = HelloWorldStorageAccount::from(HelloWorldStorage { + local_domain: init.local_domain, + mailbox: init.mailbox, + ism: init.ism, + igp: init.igp, + owner: init.owner, + ..Default::default() + }); + create_pda_account( + payer_info, + &Rent::get()?, + storage_account.size(), + program_id, + system_program_info, + storage_info, + program_storage_pda_seeds!(storage_pda_bump_seed), + )?; + // Store it + storage_account.store(storage_info, false)?; + + Ok(()) +} + +/// Dispatches a message using the dispatch authority. +/// +/// Accounts: +/// 0. [writeable] Program storage. +/// 1. [executable] The Mailbox program. +/// 2. [writeable] Outbox PDA. +/// 3. [] This program's dispatch authority. +/// 4. [executable] System program. +/// 5. [executable] SPL Noop program. +/// 6. [signer] Payer. +/// 7. [signer] Unique message account. +/// 8. [writeable] Dispatched message PDA. An empty message PDA relating to the seeds +/// `mailbox_dispatched_message_pda_seeds` where the message contents will be stored. +/// ---- if an IGP is configured ---- +/// 9. [executable] The IGP program. +/// 10. [writeable] The IGP program data. +/// 11. [writeable] The gas payment PDA. +/// 12. [] OPTIONAL - The Overhead IGP program, if the configured IGP is an Overhead IGP. +/// 13. [writeable] The IGP account. +/// ---- end if an IGP is configured ---- +fn send_hello_world( + program_id: &Pubkey, + accounts: &[AccountInfo], + hello_world: HelloWorldMessage, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Program storage. + let storage_info = next_account_info(accounts_iter)?; + let (storage_pda_key, _storage_pda_bump) = + Pubkey::find_program_address(program_storage_pda_seeds!(), program_id); + if storage_info.key != &storage_pda_key { + return Err(ProgramError::InvalidArgument); + } + let mut storage = + HelloWorldStorageAccount::fetch(&mut &storage_info.data.borrow()[..])?.into_inner(); + + // Account 1: Mailbox program. + let _mailbox_info = next_account_info(accounts_iter)?; + + // Account 2: Outbox PDA. + let mailbox_outbox_info = next_account_info(accounts_iter)?; + + // Account 3: Dispatch authority. + let dispatch_authority_info = next_account_info(accounts_iter)?; + let (expected_dispatch_authority_key, expected_dispatch_authority_bump) = + Pubkey::find_program_address(mailbox_message_dispatch_authority_pda_seeds!(), program_id); + if dispatch_authority_info.key != &expected_dispatch_authority_key { + return Err(ProgramError::InvalidArgument); + } + + // Account 4: System program. + let system_program_info = next_account_info(accounts_iter)?; + + // Account 5: SPL Noop program. + let spl_noop_info = next_account_info(accounts_iter)?; + + // Account 6: Payer. + let payer_info = next_account_info(accounts_iter)?; + + // Account 7: Unique message account. + let unique_message_account_info = next_account_info(accounts_iter)?; + + // Account 8: Dispatched message PDA. + let dispatched_message_info = next_account_info(accounts_iter)?; + + let dispatch_account_metas = vec![ + AccountMeta::new(*mailbox_outbox_info.key, false), + AccountMeta::new_readonly(*dispatch_authority_info.key, true), + AccountMeta::new_readonly(*system_program_info.key, false), + AccountMeta::new_readonly(*spl_noop_info.key, false), + AccountMeta::new(*payer_info.key, true), + AccountMeta::new_readonly(*unique_message_account_info.key, true), + AccountMeta::new(*dispatched_message_info.key, false), + ]; + let dispatch_account_infos = &[ + mailbox_outbox_info.clone(), + dispatch_authority_info.clone(), + system_program_info.clone(), + spl_noop_info.clone(), + payer_info.clone(), + unique_message_account_info.clone(), + dispatched_message_info.clone(), + ]; + + let igp_payment_accounts = + if let Some((igp_program_id, igp_account_type)) = storage.interchain_gas_paymaster() { + // Account 9: The IGP program + let igp_program_account_info = next_account_info(accounts_iter)?; + if igp_program_account_info.key != igp_program_id { + return Err(ProgramError::InvalidArgument); + } + + // Account 10: The IGP program data. + // No verification is performed here, the IGP will do that. + let igp_program_data_account_info = next_account_info(accounts_iter)?; + + // Account 11: The gas payment PDA. + // No verification is performed here, the IGP will do that. + let igp_payment_pda_account_info = next_account_info(accounts_iter)?; + + // Account 12: The configured IGP account. + let configured_igp_account_info = next_account_info(accounts_iter)?; + if configured_igp_account_info.key != igp_account_type.key() { + return Err(ProgramError::InvalidArgument); + } + + // Accounts expected by the IGP's `PayForGas` instruction: + // + // 0. [executable] The system program. + // 1. [signer] The payer. + // 2. [writeable] The IGP program data. + // 3. [signer] Unique gas payment account. + // 4. [writeable] Gas payment PDA. + // 5. [writeable] The IGP account. + // 6. [] Overhead IGP account (optional). + + let mut igp_payment_account_metas = vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(*payer_info.key, true), + AccountMeta::new(*igp_program_data_account_info.key, false), + AccountMeta::new_readonly(*unique_message_account_info.key, true), + AccountMeta::new(*igp_payment_pda_account_info.key, false), + ]; + let mut igp_payment_account_infos = vec![ + system_program_info.clone(), + payer_info.clone(), + igp_program_data_account_info.clone(), + unique_message_account_info.clone(), + igp_payment_pda_account_info.clone(), + ]; + + match igp_account_type { + InterchainGasPaymasterType::Igp(_) => { + igp_payment_account_metas + .push(AccountMeta::new(*configured_igp_account_info.key, false)); + igp_payment_account_infos.push(configured_igp_account_info.clone()); + } + InterchainGasPaymasterType::OverheadIgp(_) => { + // Account 13: The inner IGP account. + let inner_igp_account_info = next_account_info(accounts_iter)?; + + // The inner IGP is expected first, then the overhead IGP. + igp_payment_account_metas.extend([ + AccountMeta::new(*inner_igp_account_info.key, false), + AccountMeta::new_readonly(*configured_igp_account_info.key, false), + ]); + igp_payment_account_infos.extend([ + inner_igp_account_info.clone(), + configured_igp_account_info.clone(), + ]); + } + }; + + Some((igp_payment_account_metas, igp_payment_account_infos)) + } else { + None + }; + + let dispatch_authority_seeds: &[&[u8]] = + mailbox_message_dispatch_authority_pda_seeds!(expected_dispatch_authority_bump); + + if let Some((igp_payment_account_metas, igp_payment_account_infos)) = igp_payment_accounts { + // Dispatch the message and pay for gas. + storage.dispatch_with_gas( + program_id, + dispatch_authority_seeds, + hello_world.destination, + hello_world.message.into(), + HANDLE_GAS_AMOUNT, + dispatch_account_metas, + dispatch_account_infos, + igp_payment_account_metas, + &igp_payment_account_infos, + )?; + } else { + // Dispatch the message. + storage.dispatch( + program_id, + dispatch_authority_seeds, + hello_world.destination, + hello_world.message.into(), + dispatch_account_metas, + dispatch_account_infos, + )?; + } + + storage.sent += 1; + storage + .sent_to + .entry(hello_world.destination) + .and_modify(|c| *c += 1) + .or_insert(1); + + // Store it + HelloWorldStorageAccount::from(storage).store(storage_info, false)?; + + Ok(()) +} + +/// Handles a message. +/// +/// Accounts: +/// 0. [writeable] Process authority specific to this program. +/// 1. [] Storage PDA account. +pub fn handle( + program_id: &Pubkey, + accounts: &[AccountInfo], + handle: HandleInstruction, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Process authority specific to this program. + let process_authority = next_account_info(accounts_iter)?; + + // Account 1: Storage PDA account. + let storage_info = next_account_info(accounts_iter)?; + let mut storage = + HelloWorldStorageAccount::fetch(&mut &storage_info.data.borrow()[..])?.into_inner(); + + // Verify the process authority + let (expected_process_authority_key, _expected_process_authority_bump) = + Pubkey::find_program_address( + mailbox_process_authority_pda_seeds!(program_id), + &storage.mailbox, + ); + if process_authority.key != &expected_process_authority_key { + return Err(ProgramError::InvalidArgument); + } + if !process_authority.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + // Increment counters + storage.received += 1; + storage + .received_from + .entry(handle.origin) + .and_modify(|c| *c += 1) + .or_insert(1); + + let local_domain = storage.local_domain; + + // Store it. + // We don't expect the size of the storage account to change because this is accounted for + // when a remote router is enrolled. + HelloWorldStorageAccount::from(storage).store(storage_info, false)?; + + msg!( + "Received hello world: origin {}, local domain {}, sender {}, message {}", + handle.origin, + local_domain, + handle.sender, + std::str::from_utf8(&handle.message).unwrap() + ); + + Ok(()) +} + +/// Accounts: +/// 0. [writeable] Storage PDA account. +/// 1. [signer] Owner. +fn set_interchain_security_module( + program_id: &Pubkey, + accounts: &[AccountInfo], + ism: Option, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Storage PDA account. + // Not bothering with validity checks because this is a test program + let storage_info = next_account_info(accounts_iter)?; + let (expected_storage_pda_key, _expected_storage_pda_bump) = + Pubkey::find_program_address(program_storage_pda_seeds!(), program_id); + if storage_info.key != &expected_storage_pda_key { + return Err(ProgramError::InvalidArgument); + } + let mut storage = + HelloWorldStorageAccount::fetch(&mut &storage_info.data.borrow()[..])?.into_inner(); + + // Account 1: Owner. + let owner_info = next_account_info(accounts_iter)?; + storage.ensure_owner_signer(owner_info)?; + + storage.ism = ism; + + // Store it + HelloWorldStorageAccount::from(storage).store(storage_info, false)?; + + Ok(()) +} + +/// Accounts: +/// 0. [] Storage PDA account. +fn get_interchain_security_module(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Storage PDA account. + let storage_info = next_account_info(accounts_iter)?; + let (expected_storage_pda_key, _expected_storage_pda_bump) = + Pubkey::find_program_address(program_storage_pda_seeds!(), program_id); + if storage_info.key != &expected_storage_pda_key { + return Err(ProgramError::InvalidArgument); + } + let storage = + HelloWorldStorageAccount::fetch(&mut &storage_info.data.borrow()[..])?.into_inner(); + + set_return_data( + &storage + .ism + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?[..], + ); + + Ok(()) +} + +fn set_account_meta_return_data(program_id: &Pubkey) -> ProgramResult { + let (storage_pda_key, _storage_pda_bump) = + Pubkey::find_program_address(program_storage_pda_seeds!(), program_id); + + let account_metas: Vec = + vec![AccountMeta::new(storage_pda_key, false).into()]; + + // Wrap it in the SimulationReturnData because serialized account_metas + // may end with zero byte(s), which are incorrectly truncated as + // simulated transaction return data. + // See `SimulationReturnData` for details. + let bytes = SimulationReturnData::new(account_metas) + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?; + set_return_data(&bytes[..]); + + Ok(()) +} + +/// Enrolls remote routers. +/// +/// Accounts: +/// 0. [executable] System program. +/// 1. [writeable] Storage PDA account. +/// 2. [signer] Owner. +fn enroll_remote_routers( + program_id: &Pubkey, + accounts: &[AccountInfo], + configs: Vec, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: System program. + let system_program_info = next_account_info(accounts_iter)?; + if system_program_info.key != &system_program::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 1: Storage PDA account. + let storage_info = next_account_info(accounts_iter)?; + let (expected_storage_pda_key, _expected_storage_pda_bump) = + Pubkey::find_program_address(program_storage_pda_seeds!(), program_id); + if storage_info.key != &expected_storage_pda_key { + return Err(ProgramError::InvalidArgument); + } + let mut storage = + HelloWorldStorageAccount::fetch(&mut &storage_info.data.borrow()[..])?.into_inner(); + + // Account 2: Owner. + let owner_info = next_account_info(accounts_iter)?; + storage.ensure_owner_signer(owner_info)?; + + for config in &configs { + // If the sent_to or received_from map doesn't have an entry for this domain yet, + // init it to 0. This is important so that we realloc here if necessary. + storage.sent_to.entry(config.domain).or_insert(0); + storage.received_from.entry(config.domain).or_insert(0); + } + + storage.enroll_remote_routers_only_owner(owner_info, configs)?; + + // Store it, & realloc if needed + HelloWorldStorageAccount::from(storage).store_with_rent_exempt_realloc( + storage_info, + &Rent::get()?, + owner_info, + system_program_info, + )?; + + Ok(()) +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-igp/src/processor.rs b/rust/sealevel/programs/hyperlane-sealevel-igp/src/processor.rs index 5ee90985cf..044ce42fc1 100644 --- a/rust/sealevel/programs/hyperlane-sealevel-igp/src/processor.rs +++ b/rust/sealevel/programs/hyperlane-sealevel-igp/src/processor.rs @@ -594,7 +594,6 @@ fn set_destination_gas_overheads( let accounts_iter = &mut accounts.iter(); // Account 0: System program. - // Required to invoke `system_instruction::transfer` in `store_with_rent_exempt_realloc`. let system_program_info = next_account_info(accounts_iter)?; if system_program_info.key != &solana_program::system_program::id() { return Err(ProgramError::IncorrectProgramId); @@ -621,6 +620,7 @@ fn set_destination_gas_overheads( overhead_igp_info, &Rent::get()?, owner_info, + system_program_info, )?; Ok(()) @@ -659,7 +659,12 @@ fn set_gas_oracle_configs( let igp_account = IgpAccount::new(igp.into()); - igp_account.store_with_rent_exempt_realloc(igp_info, &Rent::get()?, owner_info)?; + igp_account.store_with_rent_exempt_realloc( + igp_info, + &Rent::get()?, + owner_info, + system_program_info, + )?; Ok(()) } diff --git a/rust/sealevel/programs/ism/multisig-ism-message-id/src/instruction.rs b/rust/sealevel/programs/ism/multisig-ism-message-id/src/instruction.rs index 30c605a97e..0732c705f5 100644 --- a/rust/sealevel/programs/ism/multisig-ism-message-id/src/instruction.rs +++ b/rust/sealevel/programs/ism/multisig-ism-message-id/src/instruction.rs @@ -5,11 +5,12 @@ use solana_program::{ instruction::{AccountMeta, Instruction as SolanaInstruction}, program_error::ProgramError, pubkey::Pubkey, + system_program, }; use std::collections::HashSet; -use crate::{access_control_pda_seeds, error::Error}; +use crate::{access_control_pda_seeds, domain_data_pda_seeds, error::Error}; #[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] pub enum Instruction { @@ -143,6 +144,44 @@ pub fn transfer_ownership_instruction( Ok(instruction) } +/// Greats a SetValidatorsAndThreshold instruction. +pub fn set_validators_and_threshold_instruction( + program_id: Pubkey, + owner_payer: Pubkey, + domain: u32, + validators_and_threshold: ValidatorsAndThreshold, +) -> Result { + let (access_control_pda_key, _access_control_pda_bump) = + Pubkey::find_program_address(access_control_pda_seeds!(), &program_id); + + let (domain_data_pda_key, _domain_data_pda_bump) = + Pubkey::find_program_address(domain_data_pda_seeds!(domain), &program_id); + + let ixn = Instruction::SetValidatorsAndThreshold(Domained { + domain, + data: validators_and_threshold.clone(), + }); + + // Accounts: + // 0. `[signer]` The access control owner and payer of the domain PDA. + // 1. `[]` The access control PDA account. + // 2. `[writable]` The PDA relating to the provided domain. + // 3. `[executable]` OPTIONAL - The system program account. Required if creating the domain PDA. + let accounts = vec![ + AccountMeta::new(owner_payer, true), + AccountMeta::new_readonly(access_control_pda_key, false), + AccountMeta::new(domain_data_pda_key, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + let instruction = SolanaInstruction { + program_id, + data: ixn.encode().unwrap(), + accounts, + }; + Ok(instruction) +} + #[cfg(test)] mod test { use super::*; diff --git a/rust/sealevel/programs/mailbox/src/instruction.rs b/rust/sealevel/programs/mailbox/src/instruction.rs index 9f130b5021..78a553f92d 100644 --- a/rust/sealevel/programs/mailbox/src/instruction.rs +++ b/rust/sealevel/programs/mailbox/src/instruction.rs @@ -142,3 +142,31 @@ pub fn transfer_ownership_instruction( }; Ok(instruction) } + +/// Creates an InboxSetDefaultIsm instruction. +pub fn set_default_ism_instruction( + program_id: Pubkey, + owner_payer: Pubkey, + default_ism: Pubkey, +) -> Result { + let (inbox_account, _inbox_bump) = + Pubkey::try_find_program_address(mailbox_inbox_pda_seeds!(), &program_id) + .ok_or(ProgramError::InvalidSeeds)?; + let (outbox_account, _outbox_bump) = + Pubkey::try_find_program_address(mailbox_outbox_pda_seeds!(), &program_id) + .ok_or(ProgramError::InvalidSeeds)?; + + // 0. [writeable] - The Inbox PDA account. + // 1. [] - The Outbox PDA account. + // 2. [signer] - The owner of the Mailbox. + let instruction = SolanaInstruction { + program_id, + data: Instruction::InboxSetDefaultIsm(default_ism).into_instruction_data()?, + accounts: vec![ + AccountMeta::new(inbox_account, false), + AccountMeta::new_readonly(outbox_account, false), + AccountMeta::new(owner_payer, true), + ], + }; + Ok(instruction) +} diff --git a/rust/sealevel/programs/test-send-receiver/src/program.rs b/rust/sealevel/programs/test-send-receiver/src/program.rs index 66e7c86c61..0f2bbc37af 100644 --- a/rust/sealevel/programs/test-send-receiver/src/program.rs +++ b/rust/sealevel/programs/test-send-receiver/src/program.rs @@ -5,9 +5,7 @@ use account_utils::{create_pda_account, AccountData, SizedData}; use borsh::{BorshDeserialize, BorshSerialize}; use hyperlane_sealevel_mailbox::{ instruction::{InboxProcess, Instruction as MailboxInstruction, OutboxDispatch}, - mailbox_message_dispatch_authority_pda_seeds, - mailbox_process_authority_pda_seeds, - // mailbox_inbox_pda_seeds + mailbox_message_dispatch_authority_pda_seeds, mailbox_process_authority_pda_seeds, }; use hyperlane_sealevel_message_recipient_interface::{ HandleInstruction, MessageRecipientInstruction, diff --git a/rust/utils/run-locally/src/solana.rs b/rust/utils/run-locally/src/solana.rs index 584aec164f..0dcf6a3a75 100644 --- a/rust/utils/run-locally/src/solana.rs +++ b/rust/utils/run-locally/src/solana.rs @@ -51,8 +51,7 @@ const SOLANA_KEYPAIR: &str = "config/test-sealevel-keys/test_deployer-keypair.js const SOLANA_DEPLOYER_ACCOUNT: &str = "config/test-sealevel-keys/test_deployer-account.json"; const SOLANA_WARPROUTE_TOKEN_CONFIG_FILE: &str = "sealevel/environments/local-e2e/warp-routes/testwarproute/token-config.json"; -const SOLANA_CHAIN_CONFIG_FILE: &str = - "sealevel/environments/local-e2e/warp-routes/chain-config.json"; +const SOLANA_CHAIN_CONFIG_FILE: &str = "sealevel/environments/local-e2e/chain-config.json"; const SOLANA_ENVS_DIR: &str = "sealevel/environments"; const SOLANA_ENV_NAME: &str = "local-e2e"; diff --git a/typescript/helloworld/src/app/app.ts b/typescript/helloworld/src/app/app.ts index 0b8043f6a5..ea3f4165d6 100644 --- a/typescript/helloworld/src/app/app.ts +++ b/typescript/helloworld/src/app/app.ts @@ -9,7 +9,7 @@ import { MultiProvider, RouterApp, } from '@hyperlane-xyz/sdk'; -import { debug } from '@hyperlane-xyz/utils'; +import { Address, debug } from '@hyperlane-xyz/utils'; import { HelloWorld } from '../types'; @@ -21,8 +21,9 @@ export class HelloWorldApp extends RouterApp { public readonly core: HyperlaneCore, contractsMap: HyperlaneContractsMap, multiProvider: MultiProvider, + foreignDeployments: ChainMap
= {}, ) { - super(contractsMap, multiProvider); + super(contractsMap, multiProvider, undefined, foreignDeployments); } router(contracts: HyperlaneContracts): HelloWorld { diff --git a/typescript/infra/config/environments/mainnet2/agent.ts b/typescript/infra/config/environments/mainnet2/agent.ts index 15fb9c2767..f104a10fe1 100644 --- a/typescript/infra/config/environments/mainnet2/agent.ts +++ b/typescript/infra/config/environments/mainnet2/agent.ts @@ -4,7 +4,7 @@ import { getDomainId, hyperlaneEnvironments, } from '@hyperlane-xyz/sdk'; -import { objMap } from '@hyperlane-xyz/utils'; +import { ProtocolType, objFilter, objMap } from '@hyperlane-xyz/utils'; import { GasPaymentEnforcementPolicyType, @@ -24,13 +24,14 @@ const releaseCandidateHelloworldMatchingList = routerMatchingList( helloWorld[Contexts.ReleaseCandidate].addresses, ); -const interchainQueryRouters = objMap( - hyperlaneEnvironments.mainnet, - (_, addresses) => { +const interchainQueryRouters = objFilter( + objMap(hyperlaneEnvironments.mainnet, (_, addresses) => { return { router: addresses.interchainQueryRouter, }; - }, + }), + (chain, _addresses): _addresses is { router: string } => + chainMetadata[chain].protocol === ProtocolType.Ethereum, ); const interchainQueriesMatchingList = routerMatchingList( @@ -99,6 +100,18 @@ const gasPaymentEnforcement: GasPaymentEnforcementConfig[] = [ destinationDomain: [getDomainId(chainMetadata.nautilus)], recipientAddress: '*', }, + // Similarly, temporarily not charging gas for Helloworld from Solana + { + originDomain: [getDomainId(chainMetadata.solana)], + senderAddress: [ + // Hyperlane context + '4k1gruSdH1r57V9QQK4aunzfMYzLFfF83jdYkkEwyem6', + // Rc context + '3pPDp16iVTJFge2sm85Q61hW61UN5xNqeG24gqFhzLFV', + ], + destinationDomain: '*', + recipientAddress: '*', + }, ], }, { @@ -158,7 +171,7 @@ const releaseCandidate: RootAgentConfig = { connectionType: AgentConnectionType.HttpFallback, docker: { repo, - tag: '3b0685f-20230815-110725', + tag: '35fdc74-20230913-104940', }, whitelist: releaseCandidateHelloworldMatchingList, gasPaymentEnforcement, diff --git a/typescript/infra/config/environments/mainnet2/chains.ts b/typescript/infra/config/environments/mainnet2/chains.ts index bbd7194897..4a38456d86 100644 --- a/typescript/infra/config/environments/mainnet2/chains.ts +++ b/typescript/infra/config/environments/mainnet2/chains.ts @@ -2,7 +2,7 @@ import { ChainMap, ChainMetadata, chainMetadata } from '@hyperlane-xyz/sdk'; import { AgentChainNames, Role } from '../../../src/roles'; -export const mainnetConfigs: ChainMap = { +export const ethereumMainnetConfigs: ChainMap = { bsc: { ...chainMetadata.bsc, transactionOverrides: { @@ -40,12 +40,26 @@ export const mainnetConfigs: ChainMap = { gnosis: chainMetadata.gnosis, }; +// Blessed non-Ethereum chains. +export const nonEthereumMainnetConfigs: ChainMap = { + solana: chainMetadata.solana, +}; + +export const mainnetConfigs: ChainMap = { + ...ethereumMainnetConfigs, + ...nonEthereumMainnetConfigs, +}; + export type MainnetChains = keyof typeof mainnetConfigs; export const supportedChainNames = Object.keys( mainnetConfigs, ) as MainnetChains[]; export const environment = 'mainnet2'; +export const ethereumChainNames = Object.keys( + ethereumMainnetConfigs, +) as MainnetChains[]; + const validatorChainNames = [ ...supportedChainNames, chainMetadata.solana.name, @@ -57,5 +71,5 @@ const relayerChainNames = validatorChainNames; export const agentChainNames: AgentChainNames = { [Role.Validator]: validatorChainNames, [Role.Relayer]: relayerChainNames, - [Role.Scraper]: supportedChainNames, + [Role.Scraper]: ethereumChainNames, }; diff --git a/typescript/infra/config/environments/mainnet2/core.ts b/typescript/infra/config/environments/mainnet2/core.ts index 82f79024de..8b77be2595 100644 --- a/typescript/infra/config/environments/mainnet2/core.ts +++ b/typescript/infra/config/environments/mainnet2/core.ts @@ -1,13 +1,13 @@ import { ChainMap, CoreConfig } from '@hyperlane-xyz/sdk'; import { objMap } from '@hyperlane-xyz/utils'; -import { aggregationIsm } from '../../aggregationIsm'; import { Contexts } from '../../contexts'; +import { routingIsm } from '../../routingIsm'; import { owners } from './owners'; export const core: ChainMap = objMap(owners, (local, owner) => { - const defaultIsm = aggregationIsm('mainnet2', local, Contexts.Hyperlane); + const defaultIsm = routingIsm('mainnet2', local, Contexts.Hyperlane); if (local === 'arbitrum') { return { diff --git a/typescript/infra/config/environments/mainnet2/gas-oracle.ts b/typescript/infra/config/environments/mainnet2/gas-oracle.ts index a5b9344bb3..7e98342a37 100644 --- a/typescript/infra/config/environments/mainnet2/gas-oracle.ts +++ b/typescript/infra/config/environments/mainnet2/gas-oracle.ts @@ -6,26 +6,23 @@ import { AllStorageGasOracleConfigs, getAllStorageGasOracleConfigs, } from '../../../src/config'; -import { TOKEN_EXCHANGE_RATE_DECIMALS } from '../../../src/config/gas-oracle'; +import { + TOKEN_EXCHANGE_RATE_DECIMALS, + getTokenExchangeRateFromValues, +} from '../../../src/config/gas-oracle'; import { supportedChainNames } from './chains'; -// Overcharge by 30% to account for market making risk -const TOKEN_EXCHANGE_RATE_MULTIPLIER = ethers.utils.parseUnits( - '1.30', - TOKEN_EXCHANGE_RATE_DECIMALS, -); - // Taken by looking at each network's gas history and overestimating // Last updated Mar 9, 2023. const gasPrices: ChainMap = { - // https://dune.com/hicrypto/BNBChain-Gas + // https://bscscan.com/chart/gasprice bsc: ethers.utils.parseUnits('7', 'gwei'), // https://snowtrace.io/chart/gasprice - avalanche: ethers.utils.parseUnits('45', 'gwei'), - // https://dune.com/sealaunch/Polygon-Gas-Prices + avalanche: ethers.utils.parseUnits('35', 'gwei'), + // https://polygonscan.com/chart/gasprice polygon: ethers.utils.parseUnits('200', 'gwei'), - // https://explorer.bitquery.io/celo_rc1/gas + // https://celoscan.io/chart/gasprice // This one is interesting - the average is high (~20 gwei) // but the median is low (< 10). This is likely because a popular wallet is // overpaying, but all our txs tend to be < 10 gwei. @@ -45,6 +42,9 @@ const gasPrices: ChainMap = { // https://gnosisscan.io/chart/gasprice // People also seem to be overpaying here gnosis: ethers.utils.parseUnits('2', 'gwei'), + // Arbitrarily chosen as gas prices aren't really a thing + // in Solana. + solana: ethers.BigNumber.from('28'), }; // Accurate from coingecko as of Mar 9, 2023. @@ -52,23 +52,25 @@ const gasPrices: ChainMap = { // tokens are what matters. These generally have high beta const tokenUsdPrices: ChainMap = { // https://www.coingecko.com/en/coins/bnb - bsc: ethers.utils.parseUnits('330.60', TOKEN_EXCHANGE_RATE_DECIMALS), + bsc: ethers.utils.parseUnits('211.55', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/avalanche - avalanche: ethers.utils.parseUnits('18.39', TOKEN_EXCHANGE_RATE_DECIMALS), + avalanche: ethers.utils.parseUnits('9.25', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/polygon - polygon: ethers.utils.parseUnits('1.13', TOKEN_EXCHANGE_RATE_DECIMALS), + polygon: ethers.utils.parseUnits('0.518', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/celo - celo: ethers.utils.parseUnits('0.69', TOKEN_EXCHANGE_RATE_DECIMALS), + celo: ethers.utils.parseUnits('0.42', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/ethereum - arbitrum: ethers.utils.parseUnits('1922.00', TOKEN_EXCHANGE_RATE_DECIMALS), + arbitrum: ethers.utils.parseUnits('1619.00', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/ethereum - optimism: ethers.utils.parseUnits('1922.00', TOKEN_EXCHANGE_RATE_DECIMALS), + optimism: ethers.utils.parseUnits('1619.00', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/ethereum - ethereum: ethers.utils.parseUnits('1922.00', TOKEN_EXCHANGE_RATE_DECIMALS), + ethereum: ethers.utils.parseUnits('1619.00', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/moonbeam - moonbeam: ethers.utils.parseUnits('0.38', TOKEN_EXCHANGE_RATE_DECIMALS), + moonbeam: ethers.utils.parseUnits('0.166', TOKEN_EXCHANGE_RATE_DECIMALS), // xDAI gnosis: ethers.utils.parseUnits('1.00', TOKEN_EXCHANGE_RATE_DECIMALS), + // https://www.coingecko.com/en/coins/solana + solana: ethers.utils.parseUnits('18.85', TOKEN_EXCHANGE_RATE_DECIMALS), }; // Gets the exchange rate of the remote quoted in local tokens @@ -76,8 +78,7 @@ function getTokenExchangeRate(local: ChainName, remote: ChainName): BigNumber { const localValue = tokenUsdPrices[local]; const remoteValue = tokenUsdPrices[remote]; - // Apply multiplier to overcharge - return remoteValue.mul(TOKEN_EXCHANGE_RATE_MULTIPLIER).div(localValue); + return getTokenExchangeRateFromValues(local, localValue, remote, remoteValue); } export const storageGasOracleConfig: AllStorageGasOracleConfigs = diff --git a/typescript/infra/config/environments/mainnet2/helloworld/hyperlane/addresses.json b/typescript/infra/config/environments/mainnet2/helloworld/hyperlane/addresses.json index b567822e8e..eee1f4b7c2 100644 --- a/typescript/infra/config/environments/mainnet2/helloworld/hyperlane/addresses.json +++ b/typescript/infra/config/environments/mainnet2/helloworld/hyperlane/addresses.json @@ -25,5 +25,8 @@ }, "gnosis": { "router": "0x26f32245fCF5Ad53159E875d5Cae62aEcf19c2d4" + }, + "solana": { + "router": "4k1gruSdH1r57V9QQK4aunzfMYzLFfF83jdYkkEwyem6" } } diff --git a/typescript/infra/config/environments/mainnet2/helloworld/rc/addresses.json b/typescript/infra/config/environments/mainnet2/helloworld/rc/addresses.json index ace3e7c1e4..fc05200b7e 100644 --- a/typescript/infra/config/environments/mainnet2/helloworld/rc/addresses.json +++ b/typescript/infra/config/environments/mainnet2/helloworld/rc/addresses.json @@ -25,5 +25,8 @@ }, "moonbeam": { "router": "0x3eB9eE2CFC8DCB6F58B5869D33336CFcBf1dC354" + }, + "solana": { + "router": "3pPDp16iVTJFge2sm85Q61hW61UN5xNqeG24gqFhzLFV" } } diff --git a/typescript/infra/config/environments/mainnet2/igp.ts b/typescript/infra/config/environments/mainnet2/igp.ts index 6ef8545c85..35e4131cbb 100644 --- a/typescript/infra/config/environments/mainnet2/igp.ts +++ b/typescript/infra/config/environments/mainnet2/igp.ts @@ -7,7 +7,11 @@ import { } from '@hyperlane-xyz/sdk'; import { exclude, objMap } from '@hyperlane-xyz/utils'; -import { MainnetChains, supportedChainNames } from './chains'; +import { + MainnetChains, + ethereumChainNames, + supportedChainNames, +} from './chains'; import { core } from './core'; import { owners } from './owners'; @@ -33,7 +37,8 @@ export const igp: ChainMap = objMap( beneficiary: KEY_FUNDER_ADDRESS, gasOracleType: getGasOracles(chain), overhead: Object.fromEntries( - exclude(chain, supportedChainNames).map((remote) => [ + // Not setting overhead for non-Ethereum destination chains + exclude(chain, ethereumChainNames).map((remote) => [ remote, multisigIsmVerificationCost( defaultMultisigIsmConfigs[remote].threshold, diff --git a/typescript/infra/config/environments/mainnet2/ism/verification.json b/typescript/infra/config/environments/mainnet2/ism/verification.json index cad366185e..199b97842e 100644 --- a/typescript/infra/config/environments/mainnet2/ism/verification.json +++ b/typescript/infra/config/environments/mainnet2/ism/verification.json @@ -29,6 +29,12 @@ "address": "0x1079056da3EC7D55521F27e1E094015C0d39Cc65", "constructorArguments": "", "isProxy": false + }, + { + "name": "DomainRoutingIsm", + "address": "0x870aeae147917c0b87ba55613d977026d3019018", + "constructorArguments": "", + "isProxy": false } ], "ethereum": [ @@ -61,6 +67,12 @@ "address": "0x60305F347C369E31782B3bc42D08dC2c7F218807", "constructorArguments": "", "isProxy": false + }, + { + "name": "DomainRoutingIsm", + "address": "0x2f14526ec4a7ec292e792ebf836fea303ce038fa", + "constructorArguments": "", + "isProxy": false } ], "avalanche": [ @@ -93,6 +105,12 @@ "address": "0x6f82eC078317aAbE2ae5200f09d4550987A1bF28", "constructorArguments": "", "isProxy": false + }, + { + "name": "DomainRoutingIsm", + "address": "0x5d6945e61a2d67f5a8e93873eaf60c9ee1313314", + "constructorArguments": "", + "isProxy": false } ], "polygon": [ @@ -125,6 +143,12 @@ "address": "0x720663C310D3Db506F28a8F65c39Af37BFBe0248", "constructorArguments": "", "isProxy": false + }, + { + "name": "DomainRoutingIsm", + "address": "0x3c8d4e08ea1ad7644dcfc23e0fe708eb6a1f7ad7", + "constructorArguments": "", + "isProxy": false } ], "bsc": [ @@ -157,6 +181,12 @@ "address": "0xcf14426dF1D973A656e20c3AcD2B9E18C3C05793", "constructorArguments": "", "isProxy": false + }, + { + "name": "DomainRoutingIsm", + "address": "0xae16de9c4e403c2ed47c8be6e32e58dbcc2551e2", + "constructorArguments": "", + "isProxy": false } ], "arbitrum": [ @@ -189,6 +219,12 @@ "address": "0x9D7B7245C6dAEBD5543080C91523e0702fC27024", "constructorArguments": "", "isProxy": false + }, + { + "name": "DomainRoutingIsm", + "address": "0x0bf2518aADbAb4E16b16Ba21231CE8B17E3c8843", + "constructorArguments": "", + "isProxy": false } ], "optimism": [ @@ -221,6 +257,12 @@ "address": "0x3A7Ff7b700681A2655377cFDc88eE2031cf270E1", "constructorArguments": "", "isProxy": false + }, + { + "name": "DomainRoutingIsm", + "address": "0x0ca4d54b91f2d8b728117ef3fb1ce7b52017d335", + "constructorArguments": "", + "isProxy": false } ], "moonbeam": [ @@ -253,6 +295,12 @@ "address": "0x147F8C69f0FCF571c03093dE4180718FaecbA31E", "constructorArguments": "", "isProxy": false + }, + { + "name": "DomainRoutingIsm", + "address": "0x96b595b97bb5106518ac830c59b2d7cd32f2a150", + "constructorArguments": "", + "isProxy": false } ], "gnosis": [ @@ -285,6 +333,12 @@ "address": "0xC4275763D7b621eb732847957012F1fb35C90BB8", "constructorArguments": "", "isProxy": false + }, + { + "name": "DomainRoutingIsm", + "address": "0x2f134de027e88142aaad486572cc91c1f81214bf", + "constructorArguments": "", + "isProxy": false } ] } diff --git a/typescript/infra/config/environments/mainnet2/owners.ts b/typescript/infra/config/environments/mainnet2/owners.ts index 2899e3ada1..b900b75075 100644 --- a/typescript/infra/config/environments/mainnet2/owners.ts +++ b/typescript/infra/config/environments/mainnet2/owners.ts @@ -11,4 +11,5 @@ export const owners: ChainMap
= { optimism: '0xb523CFAf45AACF472859f8B793CB0BFDB16bD257', moonbeam: '0xF0cb1f968Df01fc789762fddBfA704AE0F952197', gnosis: '0x36b0AA0e7d04e7b825D7E409FEa3c9A3d57E4C22', + solana: 'EzppBFV2taxWw8kEjxNYvby6q7W1biJEqwP3iC7YgRe3', }; diff --git a/typescript/infra/config/environments/testnet3/chains.ts b/typescript/infra/config/environments/testnet3/chains.ts index 7523789e93..0490b0cbfc 100644 --- a/typescript/infra/config/environments/testnet3/chains.ts +++ b/typescript/infra/config/environments/testnet3/chains.ts @@ -2,13 +2,14 @@ import { ChainMap, ChainMetadata, chainMetadata } from '@hyperlane-xyz/sdk'; import { AgentChainNames, Role } from '../../../src/roles'; -export const testnetConfigs: ChainMap = { +// Blessed +export const ethereumTestnetConfigs: ChainMap = { alfajores: chainMetadata.alfajores, fuji: chainMetadata.fuji, mumbai: { ...chainMetadata.mumbai, transactionOverrides: { - maxFeePerGas: 70 * 10 ** 9, // 70 gwei + maxFeePerGas: 150 * 10 ** 9, // 70 gwei maxPriorityFeePerGas: 40 * 10 ** 9, // 40 gwei }, }, @@ -20,16 +21,29 @@ export const testnetConfigs: ChainMap = { arbitrumgoerli: chainMetadata.arbitrumgoerli, }; -// "Blessed" chains that we want core contracts for. +// Blessed non-Ethereum chains. +export const nonEthereumTestnetConfigs: ChainMap = { + solanadevnet: chainMetadata.solanadevnet, +}; + +export const testnetConfigs: ChainMap = { + ...ethereumTestnetConfigs, + ...nonEthereumTestnetConfigs, +}; + export type TestnetChains = keyof typeof testnetConfigs; export const supportedChainNames = Object.keys( testnetConfigs, ) as TestnetChains[]; + +export const ethereumChainNames = Object.keys( + ethereumTestnetConfigs, +) as TestnetChains[]; + export const environment = 'testnet3'; const validatorChainNames = [ ...supportedChainNames, - chainMetadata.solanadevnet.name, chainMetadata.proteustestnet.name, ]; @@ -38,5 +52,5 @@ const relayerChainNames = validatorChainNames; export const agentChainNames: AgentChainNames = { [Role.Validator]: validatorChainNames, [Role.Relayer]: relayerChainNames, - [Role.Scraper]: supportedChainNames, + [Role.Scraper]: ethereumChainNames, }; diff --git a/typescript/infra/config/environments/testnet3/core.ts b/typescript/infra/config/environments/testnet3/core.ts index 3736dffed0..69e8bc3b37 100644 --- a/typescript/infra/config/environments/testnet3/core.ts +++ b/typescript/infra/config/environments/testnet3/core.ts @@ -1,13 +1,13 @@ import { ChainMap, CoreConfig } from '@hyperlane-xyz/sdk'; import { objMap } from '@hyperlane-xyz/utils'; -import { aggregationIsm } from '../../aggregationIsm'; import { Contexts } from '../../contexts'; +import { routingIsm } from '../../routingIsm'; import { owners } from './owners'; export const core: ChainMap = objMap(owners, (local, owner) => { - const defaultIsm = aggregationIsm('testnet3', local, Contexts.Hyperlane); + const defaultIsm = routingIsm('testnet3', local, Contexts.Hyperlane); return { owner, defaultIsm, diff --git a/typescript/infra/config/environments/testnet3/gas-oracle.ts b/typescript/infra/config/environments/testnet3/gas-oracle.ts index 4325105bc4..4f50915532 100644 --- a/typescript/infra/config/environments/testnet3/gas-oracle.ts +++ b/typescript/infra/config/environments/testnet3/gas-oracle.ts @@ -6,16 +6,13 @@ import { AllStorageGasOracleConfigs, getAllStorageGasOracleConfigs, } from '../../../src/config'; -import { TOKEN_EXCHANGE_RATE_DECIMALS } from '../../../src/config/gas-oracle'; +import { + TOKEN_EXCHANGE_RATE_DECIMALS, + getTokenExchangeRateFromValues, +} from '../../../src/config/gas-oracle'; import { TestnetChains, supportedChainNames } from './chains'; -// Overcharge by 30% to account for market making risk -const TOKEN_EXCHANGE_RATE_MULTIPLIER = ethers.utils.parseUnits( - '1.30', - TOKEN_EXCHANGE_RATE_DECIMALS, -); - // Taken by looking at each testnet and overestimating gas prices const gasPrices: ChainMap = { alfajores: ethers.utils.parseUnits('10', 'gwei'), @@ -27,6 +24,7 @@ const gasPrices: ChainMap = { moonbasealpha: ethers.utils.parseUnits('5', 'gwei'), optimismgoerli: ethers.utils.parseUnits('0.5', 'gwei'), arbitrumgoerli: ethers.utils.parseUnits('0.5', 'gwei'), + solanadevnet: ethers.BigNumber.from('28'), }; // Used to categorize rarity of testnet tokens & approximate exchange rates. @@ -55,6 +53,7 @@ const chainTokenRarity: ChainMap = { moonbasealpha: Rarity.Common, optimismgoerli: Rarity.Mythic, arbitrumgoerli: Rarity.Mythic, + solanadevnet: Rarity.Common, }; // Gets the "value" of a testnet chain @@ -68,8 +67,7 @@ function getTokenExchangeRate(local: ChainName, remote: ChainName): BigNumber { const localValue = getApproximateValue(local); const remoteValue = getApproximateValue(remote); - // Apply multiplier to overcharge - return remoteValue.mul(TOKEN_EXCHANGE_RATE_MULTIPLIER).div(localValue); + return getTokenExchangeRateFromValues(local, localValue, remote, remoteValue); } export const storageGasOracleConfig: AllStorageGasOracleConfigs = diff --git a/typescript/infra/config/environments/testnet3/helloworld/hyperlane/addresses.json b/typescript/infra/config/environments/testnet3/helloworld/hyperlane/addresses.json index 61628d21c0..d958fc0217 100644 --- a/typescript/infra/config/environments/testnet3/helloworld/hyperlane/addresses.json +++ b/typescript/infra/config/environments/testnet3/helloworld/hyperlane/addresses.json @@ -25,5 +25,8 @@ }, "sepolia": { "router": "0x5d56B8a669F50193b54319442c6EEE5edD662381" + }, + "solanadevnet": { + "router": "CXQX54kdkU5GqdRJjCmHpwHfEMgFb5SeBmMWntP2Ds7J" } } diff --git a/typescript/infra/config/environments/testnet3/igp.ts b/typescript/infra/config/environments/testnet3/igp.ts index 9ce151551b..c548a3b241 100644 --- a/typescript/infra/config/environments/testnet3/igp.ts +++ b/typescript/infra/config/environments/testnet3/igp.ts @@ -7,7 +7,11 @@ import { } from '@hyperlane-xyz/sdk'; import { exclude, objMap } from '@hyperlane-xyz/utils'; -import { TestnetChains, supportedChainNames } from './chains'; +import { + TestnetChains, + ethereumChainNames, + supportedChainNames, +} from './chains'; import { owners } from './owners'; function getGasOracles(local: TestnetChains) { @@ -28,7 +32,8 @@ export const igp: ChainMap = objMap( beneficiary: owner, gasOracleType: getGasOracles(chain), overhead: Object.fromEntries( - exclude(chain, supportedChainNames).map((remote) => [ + // Not setting overhead for non-Ethereum destination chains + exclude(chain, ethereumChainNames).map((remote) => [ remote, multisigIsmVerificationCost( defaultMultisigIsmConfigs[remote].threshold, diff --git a/typescript/infra/config/environments/testnet3/owners.ts b/typescript/infra/config/environments/testnet3/owners.ts index a618c83b5d..2fc52f1cfe 100644 --- a/typescript/infra/config/environments/testnet3/owners.ts +++ b/typescript/infra/config/environments/testnet3/owners.ts @@ -1,10 +1,15 @@ -import { ChainMap } from '@hyperlane-xyz/sdk'; +import { ChainMap, chainMetadata } from '@hyperlane-xyz/sdk'; import { Address } from '@hyperlane-xyz/utils'; -import { supportedChainNames } from './chains'; +import { ethereumChainNames, supportedChainNames } from './chains'; -const DEPLOYER_ADDRESS = '0xfaD1C94469700833717Fa8a3017278BC1cA8031C'; +const ETHEREUM_DEPLOYER_ADDRESS = '0xfaD1C94469700833717Fa8a3017278BC1cA8031C'; +const SEALEVEL_DEPLOYER_ADDRESS = + '6DjHX6Ezjpq3zZMZ8KsqyoFYo1zPSDoiZmLLkxD4xKXS'; -export const owners: ChainMap
= Object.fromEntries( - supportedChainNames.map((chain) => [chain, DEPLOYER_ADDRESS]), -); +export const owners: ChainMap
= { + ...Object.fromEntries( + ethereumChainNames.map((chain) => [chain, ETHEREUM_DEPLOYER_ADDRESS]), + ), + [chainMetadata.solanadevnet.name]: SEALEVEL_DEPLOYER_ADDRESS, +}; diff --git a/typescript/infra/config/multisigIsm.ts b/typescript/infra/config/multisigIsm.ts index 364fcfbb22..4c27ffb60d 100644 --- a/typescript/infra/config/multisigIsm.ts +++ b/typescript/infra/config/multisigIsm.ts @@ -65,6 +65,13 @@ export const rcMultisigIsmConfigs: ChainMap = { '0x15f48e78092a4f79febface509cfd76467c6cdbb', // abacus ], }, + solana: { + type: ModuleType.LEGACY_MULTISIG, + threshold: 1, + validators: [ + '0x8cc7dbfb5de339e4133f3af059c927ec383ace38', // abacus + ], + }, // ----------------- Testnets ----------------- alfajores: { type: ModuleType.LEGACY_MULTISIG, @@ -111,4 +118,9 @@ export const rcMultisigIsmConfigs: ChainMap = { threshold: 1, validators: ['0x6d13367c7cd713a4ea79a2552adf824bf1ecdd5e'], }, + solanadevnet: { + type: ModuleType.LEGACY_MULTISIG, + threshold: 1, + validators: ['0x21b9eff4d1a6d3122596c7fb80315bf094b6e5c2'], + }, }; diff --git a/typescript/infra/config/aggregationIsm.ts b/typescript/infra/config/routingIsm.ts similarity index 56% rename from typescript/infra/config/aggregationIsm.ts rename to typescript/infra/config/routingIsm.ts index 6807bf37f9..e745989051 100644 --- a/typescript/infra/config/aggregationIsm.ts +++ b/typescript/infra/config/routingIsm.ts @@ -9,7 +9,12 @@ import { RoutingIsmConfig, defaultMultisigIsmConfigs, } from '@hyperlane-xyz/sdk'; -import { Address, objFilter, objMap } from '@hyperlane-xyz/utils'; +import { + Address, + arrayToObject, + objFilter, + objMap, +} from '@hyperlane-xyz/utils'; import { DeployEnvironment } from '../src/config'; @@ -34,81 +39,107 @@ const owners = { test: testOwners, }; -export const multisigIsms = ( - env: DeployEnvironment, - local: ChainName, - type: MultisigIsmConfig['type'], - context: Contexts, -): ChainMap => - objMap( - objFilter( - context === Contexts.ReleaseCandidate - ? rcMultisigIsmConfigs - : defaultMultisigIsmConfigs, - (chain, config): config is MultisigIsmConfig => - chain !== local && chains[env].includes(chain), - ), - (_, config) => ({ - ...config, - type, - }), - ); +export const mainnetHyperlaneDefaultIsmCache: ChainMap
= { + ethereum: '0x3Ef03aEf1392E5e0C16fd4D22C3c3b4f81C8AF0C', + optimism: '0xA7a0f9CB7d3bc3520A82ECF009F8f3949a926237', + arbitrum: '0xD629aB5353D6B11f52eD80EFb26a28e5E347B52F', + avalanche: '0x143A34E3Eaf1E77a8c994EcADb5268d717183150', + polygon: '0xE1403b9d64185f715414A4a7BEcec124Bd9198A7', + bsc: '0x307c66E1E2E9f20b6ED3c4561398215CF9b633c4', + celo: '0xAC0246a09f1FEaF4CEe32e43792eE12d6B277332', + moonbeam: '0xB32d147334AF9C15A65716Ab254a2460307648D1', + gnosis: '0xF6c174AcC399eD8407663387857f30f92B0db958', + solana: '6pHP4EeX2Xek24Be7PPTWCqcpmNEPENW1m9RnZSFSmA1', +}; -/// Routing => Multisig ISM type +// Intended to be the "entrypoint" ISM. +// Routing ISM => Aggregation (1/2) +// | | +// | | +// v v +// Merkle Root Message ID export const routingIsm = ( environment: DeployEnvironment, local: ChainName, - type: MultisigIsmConfig['type'], context: Contexts, -): RoutingIsmConfig => { - const defaultMultisigIsmConfigs = multisigIsms( - environment, - local, - type, - context, +): RoutingIsmConfig | string => { + if (environment === 'mainnet2' && context === Contexts.Hyperlane) { + return mainnetHyperlaneDefaultIsmCache[local]; + } + + const aggregationIsms: ChainMap = chains[ + environment + ].reduce( + (acc, chain) => ({ + ...acc, + [chain]: aggregationIsm(chain, context), + }), + {}, ); + return { type: ModuleType.ROUTING, - domains: defaultMultisigIsmConfigs, + domains: aggregationIsms, owner: owners[environment][local], }; }; -// mainnet cache -const aggregationIsmAddresses: Record = { - [Chains.arbitrum]: '0x7995D00bdDb146334d6568b627bcd2a7DdA3B005', - [Chains.avalanche]: '0xF6bF41939ebA2363A6e311E886Ed4a5ab3dc1F5D', - [Chains.bsc]: '0x294F19d5fe29646f8E2cA4A71b6B18b78db10F9f', - [Chains.celo]: '0x656bF500F0E2EE55F26dF3bc69b44c6eA84dd065', - [Chains.ethereum]: '0xe39eA548F36d1c3DA9b871Badd11345f836a290A', - [Chains.gnosis]: '0xD0Ec4de35069520CD17522281D36DD299525d85f', - [Chains.moonbeam]: '0x04100049AC8e279C85E895d48aab1E188152e939', - [Chains.optimism]: '0x99663d142576204284b91e96d39771db94eD5188', - [Chains.polygon]: '0x0673cc1cc5eb80816E0d0E2dA5FE10053Da97943', -}; - -/// 1/2 Aggregation => Routing => Multisig ISM +// Aggregation (1/2) +// | | +// | | +// v v +// Merkle Root Message ID export const aggregationIsm = ( - environment: DeployEnvironment, - local: ChainName, + remote: ChainName, context: Contexts, -): AggregationIsmConfig | Address => { - if (environment === 'mainnet2') { - return aggregationIsmAddresses[local]; - } - - const config: AggregationIsmConfig = { +): AggregationIsmConfig => { + return { type: ModuleType.AGGREGATION, modules: [ - // ORDERING MATTERS - routingIsm(environment, local, ModuleType.MERKLE_ROOT_MULTISIG, context), - routingIsm(environment, local, ModuleType.MESSAGE_ID_MULTISIG, context), + // Ordering matters to preserve determinism + multisigIsm(remote, ModuleType.MERKLE_ROOT_MULTISIG, context), + multisigIsm(remote, ModuleType.MESSAGE_ID_MULTISIG, context), ], threshold: 1, }; - return config; }; +export const multisigIsm = ( + remote: ChainName, + type: MultisigIsmConfig['type'], + context: Contexts, +): MultisigIsmConfig => { + const configs = + context === Contexts.ReleaseCandidate + ? rcMultisigIsmConfigs + : defaultMultisigIsmConfigs; + + return { + ...configs[remote], + type, + }; +}; + +export const multisigIsms = ( + env: DeployEnvironment, + local: ChainName, + type: MultisigIsmConfig['type'], + context: Contexts, +): ChainMap => + objMap( + objFilter( + context === Contexts.ReleaseCandidate + ? rcMultisigIsmConfigs + : defaultMultisigIsmConfigs, + (chain, config): config is MultisigIsmConfig => + chain !== local && chains[env].includes(chain), + ), + (_, config) => ({ + ...config, + type, + }), + ); + const replacerEnum = (key: string, value: any) => { if (key === 'type') { switch (value) { diff --git a/typescript/infra/scripts/check-deploy.ts b/typescript/infra/scripts/check-deploy.ts index 4393cca27f..c9a0e3ed18 100644 --- a/typescript/infra/scripts/check-deploy.ts +++ b/typescript/infra/scripts/check-deploy.ts @@ -1,3 +1,4 @@ +import { HelloWorldChecker } from '@hyperlane-xyz/helloworld'; import { HyperlaneCore, HyperlaneCoreChecker, @@ -8,32 +9,39 @@ import { InterchainAccountChecker, InterchainQuery, InterchainQueryChecker, + filterChainMapToProtocol, } from '@hyperlane-xyz/sdk'; +import { ProtocolType } from '@hyperlane-xyz/utils'; +import { Contexts } from '../config/contexts'; import { deployEnvToSdkEnv } from '../src/config/environment'; +import { helloWorldRouterConfig } from '../src/config/helloworld'; import { HyperlaneAppGovernor } from '../src/govern/HyperlaneAppGovernor'; import { HyperlaneCoreGovernor } from '../src/govern/HyperlaneCoreGovernor'; import { HyperlaneIgpGovernor } from '../src/govern/HyperlaneIgpGovernor'; import { ProxiedRouterGovernor } from '../src/govern/ProxiedRouterGovernor'; +import { Role } from '../src/roles'; import { impersonateAccount, useLocalProvider } from '../src/utils/fork'; +import { getHelloWorldApp } from './helloworld/utils'; import { Modules, getEnvironmentConfig, getProxiedRouterConfig, getArgs as getRootArgs, + withContext, withModuleAndFork, } from './utils'; function getArgs() { - return withModuleAndFork(getRootArgs()) + return withModuleAndFork(withContext(getRootArgs())) .boolean('govern') .default('govern', false) .alias('g', 'govern').argv; } async function check() { - const { fork, govern, module, environment } = await getArgs(); + const { fork, govern, module, environment, context } = await getArgs(); const config = getEnvironmentConfig(environment); const multiProvider = await config.getMultiProvider(); @@ -64,8 +72,9 @@ async function check() { const checker = new HyperlaneIgpChecker(multiProvider, igp, config.igp); governor = new HyperlaneIgpGovernor(checker, config.owners); } else if (module === Modules.INTERCHAIN_ACCOUNTS) { - const routerConfig = await getProxiedRouterConfig( - environment, + const routerConfig = filterChainMapToProtocol( + await getProxiedRouterConfig(environment, multiProvider), + ProtocolType.Ethereum, multiProvider, ); const ica = InterchainAccount.fromEnvironment(env, multiProvider); @@ -87,6 +96,29 @@ async function check() { routerConfig, ); governor = new ProxiedRouterGovernor(checker, config.owners); + } else if (module === Modules.HELLO_WORLD) { + const app = await getHelloWorldApp( + config, + context, + Role.Deployer, + Contexts.Hyperlane, // Owner should always be from the hyperlane context + ); + const hwConfig = await helloWorldRouterConfig( + environment, + context, + multiProvider, + ); + const ismFactory = HyperlaneIsmFactory.fromEnvironment( + deployEnvToSdkEnv[environment], + multiProvider, + ); + const checker = new HelloWorldChecker( + multiProvider, + app, + hwConfig, + ismFactory, + ); + governor = new ProxiedRouterGovernor(checker, config.owners); } else { console.log(`Skipping ${module}, checker or governor unimplemented`); return; diff --git a/typescript/infra/scripts/gas-oracle/update-storage-gas-oracle.ts b/typescript/infra/scripts/gas-oracle/update-storage-gas-oracle.ts index af3e4c4f49..a3b36a5815 100644 --- a/typescript/infra/scripts/gas-oracle/update-storage-gas-oracle.ts +++ b/typescript/infra/scripts/gas-oracle/update-storage-gas-oracle.ts @@ -1,4 +1,5 @@ -import { ChainName, HyperlaneIgp } from '@hyperlane-xyz/sdk'; +import { ChainName, HyperlaneIgp, MultiProvider } from '@hyperlane-xyz/sdk'; +import { ProtocolType } from '@hyperlane-xyz/utils'; import { RemoteGasData, StorageGasOracleConfig } from '../../src/config'; import { deployEnvToSdkEnv } from '../../src/config/environment'; @@ -38,8 +39,16 @@ async function main() { ); for (const chain of igp.chains()) { + if ( + multiProvider.getChainMetadata(chain).protocol !== ProtocolType.Ethereum + ) { + console.log(`Skipping ${chain} because it is not an Ethereum chain`); + continue; + } + await setStorageGasOracleValues( igp, + multiProvider, storageGasOracleConfig[chain], chain, args.dryRun, @@ -50,6 +59,9 @@ async function main() { async function setStorageGasOracleValues( igp: HyperlaneIgp, + // This multiProvider is used instead of the one on the IGP because the IGP's + // multiprovider will have filtered out non-Ethereum chains. + multiProvider: MultiProvider, localStorageGasOracleConfig: StorageGasOracleConfig, local: ChainName, dryRun: boolean, @@ -61,7 +73,7 @@ async function setStorageGasOracleValues( for (const remote in localStorageGasOracleConfig) { const desiredGasData = localStorageGasOracleConfig[remote]!; - const remoteId = igp.multiProvider.getDomainId(remote); + const remoteId = multiProvider.getDomainId(remote); const existingGasData: RemoteGasData = await storageGasOracle.remoteGasData( remoteId, @@ -92,7 +104,7 @@ async function setStorageGasOracleValues( console.log(`Updating ${configsToSet.length} configs on local ${local}:`); console.log( configsToSet - .map((config) => prettyRemoteGasDataConfig(igp.multiProvider, config)) + .map((config) => prettyRemoteGasDataConfig(multiProvider, config)) .join('\n\t--\n'), ); diff --git a/typescript/infra/scripts/helloworld/check.ts b/typescript/infra/scripts/helloworld/check.ts deleted file mode 100644 index 4be8f09110..0000000000 --- a/typescript/infra/scripts/helloworld/check.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { HelloWorldChecker } from '@hyperlane-xyz/helloworld'; - -import { Contexts } from '../../config/contexts'; -import { Role } from '../../src/roles'; -import { - getArgs, - getEnvironmentConfig, - getRouterConfig, - withContext, -} from '../utils'; - -import { getHelloWorldApp } from './utils'; - -async function main() { - const { environment, context } = await withContext(getArgs()).argv; - const coreConfig = getEnvironmentConfig(environment); - const multiProvider = await coreConfig.getMultiProvider(); - const app = await getHelloWorldApp( - coreConfig, - context, - Role.Deployer, - Contexts.Hyperlane, // Owner should always be from the hyperlane context - ); - const configMap = await getRouterConfig(environment, multiProvider, true); - const checker = new HelloWorldChecker(multiProvider, app, configMap); - await checker.check(); - checker.expectEmpty(); -} - -main() - .then(() => console.info('HelloWorld check complete')) - .catch(console.error); diff --git a/typescript/infra/scripts/helloworld/deploy.ts b/typescript/infra/scripts/helloworld/deploy.ts index e39a612075..e88258971e 100644 --- a/typescript/infra/scripts/helloworld/deploy.ts +++ b/typescript/infra/scripts/helloworld/deploy.ts @@ -1,29 +1,55 @@ import path from 'path'; import { HelloWorldDeployer } from '@hyperlane-xyz/helloworld'; -import { serializeContractsMap } from '@hyperlane-xyz/sdk'; +import { + HyperlaneIsmFactory, + filterChainMapExcludeProtocol, + serializeContractsMap, +} from '@hyperlane-xyz/sdk'; +import { + ProtocolType, + hexOrBase58ToHex, + objMap, + objMerge, +} from '@hyperlane-xyz/utils'; import { Contexts } from '../../config/contexts'; +import { deployEnvToSdkEnv } from '../../src/config/environment'; +import { helloWorldRouterConfig } from '../../src/config/helloworld'; import { Role } from '../../src/roles'; import { readJSON, writeJSON } from '../../src/utils/utils'; import { - getArgs, getEnvironmentConfig, getEnvironmentDirectory, - getRouterConfig, + getArgs as getRootArgs, withContext, } from '../utils'; +function getArgs() { + return withContext(getRootArgs()) + .boolean('govern') + .default('govern', false) + .alias('g', 'govern').argv; +} + async function main() { - const { environment, context } = await withContext(getArgs()).argv; + const { environment, context } = await getArgs(); const coreConfig = getEnvironmentConfig(environment); // Always deploy from the hyperlane deployer const multiProvider = await coreConfig.getMultiProvider( Contexts.Hyperlane, Role.Deployer, ); - const configMap = await getRouterConfig(environment, multiProvider, true); - const deployer = new HelloWorldDeployer(multiProvider); + const configMap = await helloWorldRouterConfig( + environment, + context, + multiProvider, + ); + const ismFactory = HyperlaneIsmFactory.fromEnvironment( + deployEnvToSdkEnv[environment], + multiProvider, + ); + const deployer = new HelloWorldDeployer(multiProvider, ismFactory); const dir = path.join( getEnvironmentDirectory(environment), 'helloworld', @@ -39,18 +65,32 @@ async function main() { console.info(`Could not load previous deployment, file may not exist`); } + const configMapWithForeignDeployments = objMerge( + configMap, + objMap( + filterChainMapExcludeProtocol( + deployer.cachedAddresses, + ProtocolType.Ethereum, + multiProvider, + ), + (_chain, addresses) => ({ + foreignDeployment: hexOrBase58ToHex(addresses.router), + }), + ), + ); + try { - await deployer.deploy(configMap); + await deployer.deploy(configMapWithForeignDeployments); } catch (e) { console.error(`Encountered error during deploy`); console.error(e); } - writeJSON( - dir, - 'addresses.json', - serializeContractsMap(deployer.deployedContracts), - ); + writeJSON(dir, 'addresses.json', { + // To include foreign deployments that may be present in the addresses.json already + ...deployer.cachedAddresses, + ...serializeContractsMap(deployer.deployedContracts), + }); writeJSON( dir, 'verification.json', diff --git a/typescript/infra/scripts/helloworld/utils.ts b/typescript/infra/scripts/helloworld/utils.ts index 63276048a9..58e6325a55 100644 --- a/typescript/infra/scripts/helloworld/utils.ts +++ b/typescript/infra/scripts/helloworld/utils.ts @@ -11,12 +11,12 @@ import { MultiProtocolProvider, MultiProvider, attachContractsMap, + attachContractsMapAndGetForeignDeployments, chainMetadata, - filterAddressesToProtocol, + filterChainMapToProtocol, hyperlaneEnvironments, igpFactories, } from '@hyperlane-xyz/sdk'; -import { hyperlaneEnvironmentsWithSealevel } from '@hyperlane-xyz/sdk/src'; import { ProtocolType, objMerge } from '@hyperlane-xyz/utils'; import { Contexts } from '../../config/contexts'; @@ -38,15 +38,24 @@ export async function getHelloWorldApp( connectionType, ); const helloworldConfig = getHelloWorldConfig(coreConfig, context); - const contracts = attachContractsMap( - helloworldConfig.addresses, - helloWorldFactories, - ); + + const { contractsMap, foreignDeployments } = + attachContractsMapAndGetForeignDeployments( + helloworldConfig.addresses, + helloWorldFactories, + multiProvider, + ); + const core = HyperlaneCore.fromEnvironment( deployEnvToSdkEnv[coreConfig.environment], multiProvider, ); - return new HelloWorldApp(core, contracts, multiProvider); + return new HelloWorldApp( + core, + contractsMap, + multiProvider, + foreignDeployments, + ); } export async function getHelloWorldMultiProtocolApp( @@ -76,7 +85,7 @@ export async function getHelloWorldMultiProtocolApp( } const core = MultiProtocolCore.fromAddressesMap( - hyperlaneEnvironmentsWithSealevel[sdkEnvName], + hyperlaneEnvironments[sdkEnvName], multiProtocolProvider, ); @@ -93,7 +102,7 @@ export async function getHelloWorldMultiProtocolApp( // Using an standard IGP for just evm chains for now // Unfortunately this requires hacking surgically around certain addresses const envAddresses = hyperlaneEnvironments[sdkEnvName]; - const filteredAddresses = filterAddressesToProtocol( + const filteredAddresses = filterChainMapToProtocol( envAddresses, ProtocolType.Ethereum, multiProtocolProvider, diff --git a/typescript/infra/scripts/module-can-verify.ts b/typescript/infra/scripts/module-can-verify.ts new file mode 100644 index 0000000000..be03de1d89 --- /dev/null +++ b/typescript/infra/scripts/module-can-verify.ts @@ -0,0 +1,63 @@ +import { HyperlaneCore, moduleCanCertainlyVerify } from '@hyperlane-xyz/sdk'; +import { Address, ProtocolType } from '@hyperlane-xyz/utils'; + +import { mainnetHyperlaneDefaultIsmCache } from '../config/routingIsm'; +import { deployEnvToSdkEnv } from '../src/config/environment'; + +import { getArgs, getEnvironmentConfig } from './utils'; + +// Hacky temporary script just to make sure that default ISMs are correct. +// Testnet3 has already been updated, mainnet2 hasn't, so the above cache +// is used for mainnet2. + +async function main() { + const args = await getArgs().argv; + + const { environment } = args; + + const config = getEnvironmentConfig(environment); + const multiProvider = await config.getMultiProvider(); + + const core = HyperlaneCore.fromEnvironment( + deployEnvToSdkEnv[environment], + multiProvider, + ); + + for (const local of core.chains()) { + if ( + multiProvider.getChainMetadata(local).protocol !== ProtocolType.Ethereum + ) { + continue; + } + + let ismToCheck = ''; + if (environment === 'testnet3') { + ismToCheck = await core.getContracts(local).mailbox.defaultIsm(); + } else if (environment === 'mainnet2') { + ismToCheck = mainnetHyperlaneDefaultIsmCache[local]!; + } else { + throw new Error(`Unsupported environment ${environment}`); + } + + const remotes = multiProvider.getRemoteChains(local); + for (const remote of remotes) { + console.log(`Checking chain ${local} can receive from ${remote}...`); + const canVerify = await moduleCanCertainlyVerify( + ismToCheck, + multiProvider, + remote, + local, + ); + if (canVerify) { + console.log('All good!'); + } else { + console.error(`Chain ${local} cannot receive from ${remote}!!!!`); + } + } + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/typescript/infra/scripts/print-chain-metadatas.ts b/typescript/infra/scripts/print-chain-metadatas.ts new file mode 100644 index 0000000000..7b96c35807 --- /dev/null +++ b/typescript/infra/scripts/print-chain-metadatas.ts @@ -0,0 +1,17 @@ +import { getArgs, getEnvironmentConfig } from './utils'; + +// This script exists to print the chain metadata configs for a given environment +// so they can easily be copied into the Sealevel tooling. :'( + +async function main() { + const args = await getArgs().argv; + + const environmentConfig = getEnvironmentConfig(args.environment); + + console.log(JSON.stringify(environmentConfig.chainMetadataConfigs, null, 2)); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/typescript/infra/scripts/print-multisig-ism-config.ts b/typescript/infra/scripts/print-multisig-ism-config.ts new file mode 100644 index 0000000000..1e7abead88 --- /dev/null +++ b/typescript/infra/scripts/print-multisig-ism-config.ts @@ -0,0 +1,29 @@ +import { AllChains, ModuleType } from '@hyperlane-xyz/sdk'; + +import { multisigIsms } from '../config/routingIsm'; + +import { getArgs, withContext } from './utils'; + +// This script exists to print the default multisig ISM validator sets for a given environment +// so they can easily be copied into the Sealevel tooling. :'( + +async function main() { + const args = await withContext(getArgs()) + .describe('local', 'local chain') + .choices('local', AllChains) + .demandOption('local').argv; + + const config = multisigIsms( + args.environment, + args.local, + ModuleType.LEGACY_MULTISIG, + args.context, + ); + + console.log(JSON.stringify(config, null, 2)); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/typescript/infra/scripts/utils.ts b/typescript/infra/scripts/utils.ts index 895ae36cf6..55688eea77 100644 --- a/typescript/infra/scripts/utils.ts +++ b/typescript/infra/scripts/utils.ts @@ -190,7 +190,6 @@ export async function getMultiProviderForRole( if (process.env.CI === 'true') { return new MultiProvider(); // use default RPCs } - const multiProvider = new MultiProvider(txConfigs); await promiseObjAll( objMap(txConfigs, async (chain, _) => { @@ -201,6 +200,7 @@ export async function getMultiProviderForRole( multiProvider.setSigner(chain, signer); }), ); + return multiProvider; } @@ -317,10 +317,29 @@ export async function getRouterConfig( ).intersection; for (const chain of knownChains) { + // CI will not have signers for all known chains. To avoid failing, we + // default to the owner configured in the environment if we cannot get a + // signer address. + const getSignerAddress = (chain: ChainName) => { + const signer = multiProvider.tryGetSigner(chain); + if (!signer) { + const owner = owners[chain]; + console.warn( + `Unable to get signer for chain, ${chain}, defaulting to configured owner ${owner}`, + ); + return owner; + } + return signer.getAddress(); + }; + + // MultiProvider signers are only used for Ethereum chains. + const owner = + useMultiProviderOwners && + multiProvider.getChainMetadata(chain).protocol === ProtocolType.Ethereum + ? await getSignerAddress(chain) + : owners[chain]; config[chain] = { - owner: useMultiProviderOwners - ? await multiProvider.getSignerAddress(chain) - : owners[chain], + owner: owner, mailbox: core.getContracts(chain).mailbox.address, interchainGasPaymaster: igp.getContracts(chain).defaultIsmInterchainGasPaymaster.address, diff --git a/typescript/infra/src/config/chain.ts b/typescript/infra/src/config/chain.ts index 617be5512f..3149605603 100644 --- a/typescript/infra/src/config/chain.ts +++ b/typescript/infra/src/config/chain.ts @@ -53,7 +53,6 @@ export async function fetchProvider( // and are ordered randomly for each RPC. priority: index, }; - console.log('fallbackProviderConfig', fallbackProviderConfig); return fallbackProviderConfig; }), 1, // a single provider is "quorum", but failure will cause failover to the next provider diff --git a/typescript/infra/src/config/gas-oracle.ts b/typescript/infra/src/config/gas-oracle.ts index b28661e86f..41bf8ccac7 100644 --- a/typescript/infra/src/config/gas-oracle.ts +++ b/typescript/infra/src/config/gas-oracle.ts @@ -1,6 +1,9 @@ import { BigNumber, ethers } from 'ethers'; import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; +import { convertDecimalsEthersBigNumber } from '@hyperlane-xyz/utils'; + +import { mustGetChainNativeTokenDecimals } from '../utils/utils'; export type RemoteGasData = { tokenExchangeRate: BigNumber; @@ -24,6 +27,12 @@ export const TOKEN_EXCHANGE_RATE_SCALE = ethers.utils.parseUnits( TOKEN_EXCHANGE_RATE_DECIMALS, ); +// Overcharge by 30% to account for market making risk +const TOKEN_EXCHANGE_RATE_MULTIPLIER = ethers.utils.parseUnits( + '1.30', + TOKEN_EXCHANGE_RATE_DECIMALS, +); + // Gets the StorageGasOracleConfig for a particular local chain function getLocalStorageGasOracleConfig( local: ChainName, @@ -62,3 +71,21 @@ export function getAllStorageGasOracleConfigs( }; }, {}) as AllStorageGasOracleConfigs; } + +export function getTokenExchangeRateFromValues( + local: ChainName, + localValue: BigNumber, + remote: ChainName, + remoteValue: BigNumber, +) { + // This does not yet account for decimals! + const exchangeRate = remoteValue + .mul(TOKEN_EXCHANGE_RATE_MULTIPLIER) + .div(localValue); + + return convertDecimalsEthersBigNumber( + mustGetChainNativeTokenDecimals(remote), + mustGetChainNativeTokenDecimals(local), + exchangeRate, + ); +} diff --git a/typescript/infra/src/config/helloworld.ts b/typescript/infra/src/config/helloworld.ts index 071e7464f3..d004ef7d00 100644 --- a/typescript/infra/src/config/helloworld.ts +++ b/typescript/infra/src/config/helloworld.ts @@ -1,6 +1,16 @@ import { AgentConnectionType, ChainMap, ChainName } from '@hyperlane-xyz/sdk'; +import { MultiProvider, RouterConfig } from '@hyperlane-xyz/sdk'; +import { objMap } from '@hyperlane-xyz/utils'; + +import { Contexts } from '../../config/contexts'; +import { + mainnetHyperlaneDefaultIsmCache, + routingIsm, +} from '../../config/routingIsm'; +import { getRouterConfig } from '../../scripts/utils'; import { DockerConfig } from './agent'; +import { DeployEnvironment } from './environment'; export enum HelloWorldKathyRunMode { // Sends messages between all pairwise chains @@ -38,3 +48,19 @@ export interface HelloWorldConfig { addresses: ChainMap<{ router: string }>; kathy: HelloWorldKathyConfig; } + +export async function helloWorldRouterConfig( + environment: DeployEnvironment, + context: Contexts, + multiProvider: MultiProvider, +): Promise> { + const routerConfig = await getRouterConfig(environment, multiProvider, true); + return objMap(routerConfig, (chain, config) => ({ + ...config, + interchainSecurityModule: + context === Contexts.Hyperlane + ? // TODO move back to `undefined` after these are verified and made the default ISMs + mainnetHyperlaneDefaultIsmCache[chain] + : routingIsm(environment, chain, context), + })); +} diff --git a/typescript/infra/src/govern/HyperlaneCoreGovernor.ts b/typescript/infra/src/govern/HyperlaneCoreGovernor.ts index 3b98153d87..019c7321a2 100644 --- a/typescript/infra/src/govern/HyperlaneCoreGovernor.ts +++ b/typescript/infra/src/govern/HyperlaneCoreGovernor.ts @@ -25,7 +25,7 @@ export class HyperlaneCoreGovernor extends HyperlaneAppGovernor< } protected async handleMailboxViolation(violation: MailboxViolation) { - switch (violation.mailboxType) { + switch (violation.subType) { case MailboxViolationType.DefaultIsm: { let ismAddress: string; if (typeof violation.expected === 'object') { diff --git a/typescript/infra/src/govern/ProxiedRouterGovernor.ts b/typescript/infra/src/govern/ProxiedRouterGovernor.ts index c30018ca6e..b2e5eccb99 100644 --- a/typescript/infra/src/govern/ProxiedRouterGovernor.ts +++ b/typescript/infra/src/govern/ProxiedRouterGovernor.ts @@ -3,8 +3,12 @@ import { ConnectionClientViolation, ConnectionClientViolationType, HyperlaneAppChecker, + OwnerViolation, RouterApp, RouterConfig, + RouterViolation, + RouterViolationType, + ViolationType, } from '@hyperlane-xyz/sdk'; import { Address } from '@hyperlane-xyz/utils'; @@ -23,13 +27,18 @@ export class ProxiedRouterGovernor< protected async mapViolationsToCalls() { for (const violation of this.checker.violations) { - if ( - violation.type === - ConnectionClientViolationType.InterchainSecurityModule - ) { - this.handleIsmViolation(violation as ConnectionClientViolation); - } else { - throw new Error(`Unsupported violation type ${violation.type}`); + switch (violation.type) { + case ConnectionClientViolationType.InterchainSecurityModule: + this.handleIsmViolation(violation as ConnectionClientViolation); + break; + case RouterViolationType.EnrolledRouter: + this.handleEnrolledRouterViolation(violation as RouterViolation); + break; + case ViolationType.Owner: + this.handleOwnerViolation(violation as OwnerViolation); + break; + default: + throw new Error(`Unsupported violation type ${violation.type}`); } } } @@ -44,4 +53,18 @@ export class ProxiedRouterGovernor< description: `Set ISM of ${violation.contract.address} to ${violation.expected}`, }); } + + protected handleEnrolledRouterViolation(violation: RouterViolation) { + const remoteDomain = this.checker.multiProvider.getDomainId( + violation.remoteChain, + ); + this.pushCall(violation.chain, { + to: violation.contract.address, + data: violation.contract.interface.encodeFunctionData( + 'enrollRemoteRouter', + [remoteDomain, violation.expected], + ), + description: `Enroll router for remote chain ${violation.remoteChain} (${remoteDomain}) ${violation.expected} in ${violation.contract.address}`, + }); + } } diff --git a/typescript/infra/src/utils/utils.ts b/typescript/infra/src/utils/utils.ts index 7837487088..1a8a79f8e5 100644 --- a/typescript/infra/src/utils/utils.ts +++ b/typescript/infra/src/utils/utils.ts @@ -5,7 +5,12 @@ import { ethers } from 'ethers'; import fs from 'fs'; import path from 'path'; -import { AllChains, ChainName, CoreChainName } from '@hyperlane-xyz/sdk'; +import { + AllChains, + ChainName, + CoreChainName, + chainMetadata, +} from '@hyperlane-xyz/sdk'; import { objMerge } from '@hyperlane-xyz/utils'; import { Contexts } from '../../config/contexts'; @@ -260,3 +265,11 @@ export function diagonalize(array: Array>): Array { } return diagonalized; } + +export function mustGetChainNativeTokenDecimals(chain: ChainName): number { + const metadata = chainMetadata[chain]; + if (!metadata.nativeToken) { + throw new Error(`No native token for chain ${chain}`); + } + return metadata.nativeToken.decimals; +} diff --git a/typescript/sdk/src/consts/chainMetadata.ts b/typescript/sdk/src/consts/chainMetadata.ts index 78aa6c1c1a..0113c96f1a 100644 --- a/typescript/sdk/src/consts/chainMetadata.ts +++ b/typescript/sdk/src/consts/chainMetadata.ts @@ -215,7 +215,10 @@ export const celo: ChainMetadata = { reorgPeriod: 0, estimateBlockTime: 5, }, - gnosisSafeTransactionServiceUrl: 'https://safe-transaction-celo.safe.global/', + // The official Gnosis safe URL `https://safe-transaction-celo.safe.global` doesn't work well + // with delegates on a multisig created with the old unofficial Celo tooling. + gnosisSafeTransactionServiceUrl: + 'https://mainnet-tx-svc.celo-safe-prod.celo-networks-dev.org/', }; export const ethereum: ChainMetadata = { diff --git a/typescript/sdk/src/consts/chains.ts b/typescript/sdk/src/consts/chains.ts index 20dea0674c..a13a128cb3 100644 --- a/typescript/sdk/src/consts/chains.ts +++ b/typescript/sdk/src/consts/chains.ts @@ -53,6 +53,7 @@ export const Mainnets: Array = [ Chains.optimism, Chains.polygon, Chains.gnosis, + Chains.solana, ]; export const Testnets: Array = [ @@ -65,6 +66,7 @@ export const Testnets: Array = [ Chains.mumbai, Chains.optimismgoerli, Chains.sepolia, + Chains.solanadevnet, ]; export const TestChains: Array = [ diff --git a/typescript/sdk/src/consts/environments/index.ts b/typescript/sdk/src/consts/environments/index.ts index 39c2879c76..55e49c1bb7 100644 --- a/typescript/sdk/src/consts/environments/index.ts +++ b/typescript/sdk/src/consts/environments/index.ts @@ -5,14 +5,9 @@ import { CoreChainName } from '../chains'; import mainnet from './mainnet.json'; import test from './test.json'; -import testnetSealevel from './testnet-sealevel.json'; import testnet from './testnet.json'; export const hyperlaneEnvironments = { test, testnet, mainnet }; -export const hyperlaneEnvironmentsWithSealevel = { - ...hyperlaneEnvironments, - testnet: { ...testnet, ...testnetSealevel }, -}; export type HyperlaneEnvironment = keyof typeof hyperlaneEnvironments; export type HyperlaneEnvironmentChain = Extract< diff --git a/typescript/sdk/src/consts/environments/mainnet.json b/typescript/sdk/src/consts/environments/mainnet.json index 7523050dac..b5c4f927ac 100644 --- a/typescript/sdk/src/consts/environments/mainnet.json +++ b/typescript/sdk/src/consts/environments/mainnet.json @@ -153,5 +153,22 @@ "interchainAccountRouter": "0xe9E3444DDD80c50276c0Fcf316026f6d7fEc2c47", "merkleRootMultisigIsmFactory": "0x784b9D0f4eF9fb8444DfB5d24AB221C9D1A85395", "messageIdMultisigIsmFactory": "0xC4275763D7b621eb732847957012F1fb35C90BB8" + }, + "solana": { + "storageGasOracle": "11111111111111111111111111111111", + "validatorAnnounce": "C88Lk5GR6cPxYoJxPbNDDEwsx5Kxn1wZEomvQ2So333g", + "proxyAdmin": "11111111111111111111111111111111", + "mailbox": "Ge9atjAc3Ltu91VTbNpJDCjZ9CFxFyck4h3YBcTF9XPq", + "interchainGasPaymaster": "FCNfmLSZLo5x7oNYmkYU8WdPUu7pj636P9CaMxkmaCp7", + "defaultIsmInterchainGasPaymaster": "GTj6WzNxLNFydq5zJrV9p13fyqotRoo1MQykNCWuVpbS", + "multisigIsm": "11111111111111111111111111111111", + "testRecipient": "11111111111111111111111111111111", + "interchainAccountIsm": "11111111111111111111111111111111", + "aggregationIsmFactory": "11111111111111111111111111111111", + "routingIsmFactory": "11111111111111111111111111111111", + "interchainQueryRouter": "11111111111111111111111111111111", + "interchainAccountRouter": "11111111111111111111111111111111", + "merkleRootMultisigIsmFactory": "11111111111111111111111111111111", + "messageIdMultisigIsmFactory": "11111111111111111111111111111111" } } diff --git a/typescript/sdk/src/consts/environments/testnet-sealevel.json b/typescript/sdk/src/consts/environments/testnet-sealevel.json deleted file mode 100644 index 214c32ecef..0000000000 --- a/typescript/sdk/src/consts/environments/testnet-sealevel.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "solanadevnet": { - "storageGasOracle": "", - "validatorAnnounce": "", - "proxyAdmin": "", - "mailbox": "4v25Dz9RccqUrTzmfHzJMsjd1iVoNrWzeJ4o6GYuJrVn", - "interchainGasPaymaster": "", - "defaultIsmInterchainGasPaymaster": "", - "multisigIsm": "", - "testRecipient": "", - "interchainAccountIsm": "", - "aggregationIsmFactory": "", - "routingIsmFactory": "", - "interchainQueryRouter": "", - "interchainAccountRouter": "", - "merkleRootMultisigIsmFactory": "", - "messageIdMultisigIsmFactory": "" - } -} diff --git a/typescript/sdk/src/consts/environments/testnet.json b/typescript/sdk/src/consts/environments/testnet.json index 634ff8a0f4..bf09e67509 100644 --- a/typescript/sdk/src/consts/environments/testnet.json +++ b/typescript/sdk/src/consts/environments/testnet.json @@ -14,7 +14,8 @@ "interchainQueryRouter": "0xc341cBC69745C541d698cb2cB4eDb91c2F0413aE", "interchainAccountRouter": "0x2b0db6161f2f7aE86b7eA07711354575a6D99667", "merkleRootMultisigIsmFactory": "0x6525Ac4008E38e0E70DaEf59d5f0e1721bd8aA83", - "messageIdMultisigIsmFactory": "0x4C739E01f295B70762C0bA9D86123E1775C2f703" + "messageIdMultisigIsmFactory": "0x4C739E01f295B70762C0bA9D86123E1775C2f703", + "timelockController": "0x0000000000000000000000000000000000000000" }, "fuji": { "storageGasOracle": "0xd44E79A697136888f0d720Fb6703400a9204FD39", @@ -31,7 +32,8 @@ "interchainQueryRouter": "0x7192d5Ad540E9fEfc3FD1845d41c18EE86980AAb", "interchainAccountRouter": "0xb0811feF53FF499bd8E09018F8E568b95c42A721", "merkleRootMultisigIsmFactory": "0x76a1aaE73e9D837ceF10Ac5AF5AfDD30d7612f98", - "messageIdMultisigIsmFactory": "0xA1e6d12a3F5F7e05E4D6cb39E71534F27fE29561" + "messageIdMultisigIsmFactory": "0xA1e6d12a3F5F7e05E4D6cb39E71534F27fE29561", + "timelockController": "0x0000000000000000000000000000000000000000" }, "mumbai": { "storageGasOracle": "0x8970bdA0B0A01d1bA2656b510B2859560e75a7c5", @@ -48,7 +50,8 @@ "interchainQueryRouter": "0xD786eC480Da58792175c9DDEdD99802Badf1037E", "interchainAccountRouter": "0x03Ae748aee6E31560D4E46AdbCF15c6D60bD70e7", "merkleRootMultisigIsmFactory": "0xbA38823853Fa2994823F3E62d3EAC5cC701C8CCA", - "messageIdMultisigIsmFactory": "0x5F8A06fb970775C0C317AF6AfA5aD45B11FBD342" + "messageIdMultisigIsmFactory": "0x5F8A06fb970775C0C317AF6AfA5aD45B11FBD342", + "timelockController": "0x0000000000000000000000000000000000000000" }, "bsctestnet": { "storageGasOracle": "0x1a75f55e8f574CdB5abb1b2702b9caF2E5F7d4D6", @@ -65,7 +68,8 @@ "interchainQueryRouter": "0x6117c92e1D05fD23Adc6077bA0d2956EE3175984", "interchainAccountRouter": "0x404693BeD61D6B17F44738c0b4ddF9c2D65Ba0BF", "merkleRootMultisigIsmFactory": "0x8DA546024850D998Be3b65204c0F0f63C1f3B0A1", - "messageIdMultisigIsmFactory": "0x7Bc0bb71aE0E9bDC0Ac53e932870728D95FA28bF" + "messageIdMultisigIsmFactory": "0x7Bc0bb71aE0E9bDC0Ac53e932870728D95FA28bF", + "timelockController": "0x0000000000000000000000000000000000000000" }, "goerli": { "storageGasOracle": "0xce8E9D701A1DFfe672c1d8dB20De2B3fa6F4437D", @@ -82,7 +86,8 @@ "interchainQueryRouter": "0x46A2B1C3E8a93C3613Ebf326235FbD3e2f65660F", "interchainAccountRouter": "0x55486284a85d7b51a7bBfd343702414D65276fa6", "merkleRootMultisigIsmFactory": "0x4dD7716b876441355657a18c7E7b02129F88E3c0", - "messageIdMultisigIsmFactory": "0x14b0F0c0a59704E92f95252cE24Ef6aB1d679733" + "messageIdMultisigIsmFactory": "0x14b0F0c0a59704E92f95252cE24Ef6aB1d679733", + "timelockController": "0x0000000000000000000000000000000000000000" }, "moonbasealpha": { "storageGasOracle": "0xAd754Dbc3F725259E49A90CAB347AeC343D432ed", @@ -98,7 +103,8 @@ "interchainAccountIsm": "0x4B7657A56f51B42A8702957EecFF8CBE86fF48C8", "interchainAccountRouter": "0xFB03bC45D20848F94DAF6884A92795dd44dDE241", "merkleRootMultisigIsmFactory": "0x0616A79374e81eB1d2275eCe5837aD050f9c53f1", - "messageIdMultisigIsmFactory": "0x3D696c38Dd958e635f9077e65b64aA9cf7c92627" + "messageIdMultisigIsmFactory": "0x3D696c38Dd958e635f9077e65b64aA9cf7c92627", + "timelockController": "0x0000000000000000000000000000000000000000" }, "optimismgoerli": { "storageGasOracle": "0xdE72697715aAeC4CaBbD638C0Aba64488005C64b", @@ -115,7 +121,8 @@ "interchainQueryRouter": "0x6385E09099d889f912F90c47F10E903fe4feBF69", "interchainAccountRouter": "0x6f393F8Dfb327d99c946e0Dd2f39F51B1aB446bf", "merkleRootMultisigIsmFactory": "0xC5Bb8CDD44B6c56695df45c7AA8012a97dD6ED13", - "messageIdMultisigIsmFactory": "0x39a8711BF44165A2292Cb5cB43229659c2Bb11c9" + "messageIdMultisigIsmFactory": "0x39a8711BF44165A2292Cb5cB43229659c2Bb11c9", + "timelockController": "0x0000000000000000000000000000000000000000" }, "arbitrumgoerli": { "storageGasOracle": "0xfFAEF09B3cd11D9b20d1a19bECca54EEC2884766", @@ -132,7 +139,8 @@ "interchainQueryRouter": "0x5b1E05e1fdDBc0f3d31c4E634ff4D5d84A56deEe", "interchainAccountRouter": "0x3C636ccC50222a9eb54849C7a622D60a40928a5E", "merkleRootMultisigIsmFactory": "0x0502Be39aE255D022013DC0aeAa52fDBCD5f0331", - "messageIdMultisigIsmFactory": "0x71eAD731EBdd1334d80a89a572fDFA67830C504c" + "messageIdMultisigIsmFactory": "0x71eAD731EBdd1334d80a89a572fDFA67830C504c", + "timelockController": "0x0000000000000000000000000000000000000000" }, "sepolia": { "storageGasOracle": "0x1D5EbC3e15e9ECDe0e3530C85899556797eeaea5", @@ -149,6 +157,24 @@ "interchainQueryRouter": "0x507C18fa4e3b0ce6beBD494488D62d1ed0fB0555", "interchainAccountRouter": "0x9cA4A31af0f3a8fe2806599467912809D3e17ECB", "merkleRootMultisigIsmFactory": "0xA9999B4abC373FF2BB95B8725FABC96CA883d811", - "messageIdMultisigIsmFactory": "0xCCC126d96efcc342BF2781A7d224D3AB1F25B19C" + "messageIdMultisigIsmFactory": "0xCCC126d96efcc342BF2781A7d224D3AB1F25B19C", + "timelockController": "0x0000000000000000000000000000000000000000" + }, + "solanadevnet": { + "storageGasOracle": "0x0000000000000000000000000000000000000000", + "validatorAnnounce": "0x0000000000000000000000000000000000000000", + "proxyAdmin": "0x0000000000000000000000000000000000000000", + "mailbox": "4v25Dz9RccqUrTzmfHzJMsjd1iVoNrWzeJ4o6GYuJrVn", + "interchainGasPaymaster": "7hMPEGdgBQFsjEz3aaNwZp8WMFHs615zAM3erXBDJuJR", + "defaultIsmInterchainGasPaymaster": "0x0000000000000000000000000000000000000000", + "multisigIsm": "0x0000000000000000000000000000000000000000", + "testRecipient": "0x0000000000000000000000000000000000000000", + "interchainAccountIsm": "0x0000000000000000000000000000000000000000", + "aggregationIsmFactory": "0x0000000000000000000000000000000000000000", + "routingIsmFactory": "0x0000000000000000000000000000000000000000", + "interchainQueryRouter": "0x0000000000000000000000000000000000000000", + "interchainAccountRouter": "0x0000000000000000000000000000000000000000", + "merkleRootMultisigIsmFactory": "0x0000000000000000000000000000000000000000", + "messageIdMultisigIsmFactory": "0x0000000000000000000000000000000000000000" } } diff --git a/typescript/sdk/src/consts/multisigIsm.ts b/typescript/sdk/src/consts/multisigIsm.ts index 84e5389d55..91ebefb3a2 100644 --- a/typescript/sdk/src/consts/multisigIsm.ts +++ b/typescript/sdk/src/consts/multisigIsm.ts @@ -107,6 +107,15 @@ export const defaultMultisigIsmConfigs: ChainMap = { '0xa18580444eaeb1c5957e7b66a6bf84b6519f904d', // staked ], }, + solana: { + type: ModuleType.LEGACY_MULTISIG, + threshold: 2, + validators: [ + '0x3cd1a081f38874bbb075bf10b62adcb858db864c', // abacus + '0x2b0c45f6111ae1c1684d4287792e3bd6ebd1abcc', // ZKV + '0x7b9ec253a8ba38994457eb9dbe386938d545351a', // everstake + ], + }, // ----------------- Testnets ----------------- alfajores: { type: ModuleType.LEGACY_MULTISIG, @@ -189,4 +198,13 @@ export const defaultMultisigIsmConfigs: ChainMap = { '0xdf181fcc11dfac5d01467e4547101a856dd5aa04', ], }, + solanadevnet: { + type: ModuleType.LEGACY_MULTISIG, + threshold: 2, + validators: [ + '0xec0f73dbc5b1962a20f7dcbe07c98414025b0c43', + '0x9c20a149dfa09ea9f77f5a7ca09ed44f9c025133', + '0x967c5ecdf2625ae86580bd203b630abaaf85cd62', + ], + }, }; diff --git a/typescript/sdk/src/contracts/contracts.ts b/typescript/sdk/src/contracts/contracts.ts index be0934209c..e52fe1cadb 100644 --- a/typescript/sdk/src/contracts/contracts.ts +++ b/typescript/sdk/src/contracts/contracts.ts @@ -1,10 +1,11 @@ -import { Contract } from 'ethers'; +import { Contract, ethers } from 'ethers'; import { Ownable } from '@hyperlane-xyz/core'; import { Address, ProtocolType, ValueOf, + hexOrBase58ToHex, objFilter, objMap, pick, @@ -65,19 +66,30 @@ export function filterAddressesMap( ); } -export function filterAddressesToProtocol( - addresses: HyperlaneAddressesMap, +export function filterChainMapToProtocol( + contractsMap: ChainMap, protocolType: ProtocolType, - // Note, MultiProviders are passable here metadataManager: ChainMetadataManager, -): HyperlaneAddressesMap { +): ChainMap { return objFilter( - addresses, + contractsMap, (c, _addrs): _addrs is any => metadataManager.tryGetChainMetadata(c)?.protocol === protocolType, ); } +export function filterChainMapExcludeProtocol( + contractsMap: ChainMap, + protocolType: ProtocolType, + metadataManager: ChainMetadataManager, +): ChainMap { + return objFilter( + contractsMap, + (c, _addrs): _addrs is any => + metadataManager.tryGetChainMetadata(c)?.protocol !== protocolType, + ); +} + export function attachContracts( addresses: HyperlaneAddresses, factories: F, @@ -98,6 +110,40 @@ export function attachContractsMap( ) as HyperlaneContractsMap; } +export function attachContractsMapAndGetForeignDeployments< + F extends HyperlaneFactories, +>( + addressesMap: HyperlaneAddressesMap, + factories: F, + metadataManager: ChainMetadataManager, +): { + contractsMap: HyperlaneContractsMap; + foreignDeployments: ChainMap
; +} { + const contractsMap = attachContractsMap( + filterChainMapToProtocol( + addressesMap, + ProtocolType.Ethereum, + metadataManager, + ), + factories, + ); + + const foreignDeployments = objMap( + filterChainMapExcludeProtocol( + addressesMap, + ProtocolType.Ethereum, + metadataManager, + ), + (_, addresses) => hexOrBase58ToHex(addresses.router), + ); + + return { + contractsMap, + foreignDeployments, + }; +} + export function connectContracts( contracts: HyperlaneContracts, connection: Connection, @@ -143,9 +189,31 @@ export function appFromAddressesMapHelper( contractsMap: HyperlaneContractsMap; multiProvider: MultiProvider; } { + // Hack to accommodate non-Ethereum artifacts, while still retaining their + // presence in the addressesMap so that they are included in the list of chains + // on the MultiProvider (needed for getting metadata). A non-Ethereum-style address + // from another execution environment will cause Ethers to throw if we try to attach + // it, so we just replace it with the zero address. + const addressesMapWithEthereumizedAddresses = objMap( + addressesMap, + (chain, addresses) => { + const metadata = multiProvider.getChainMetadata(chain); + if (metadata.protocol === ProtocolType.Ethereum) { + return addresses; + } + return objMap( + addresses, + (_key, _address) => ethers.constants.AddressZero, + ); + }, + ); + // Attaches contracts for each chain for which we have a complete set of // addresses - const contractsMap = attachContractsMap(addressesMap, factories); + const contractsMap = attachContractsMap( + addressesMapWithEthereumizedAddresses, + factories, + ); // Filters out providers for chains for which we don't have a complete set // of addresses diff --git a/typescript/sdk/src/core/HyperlaneCoreChecker.ts b/typescript/sdk/src/core/HyperlaneCoreChecker.ts index ad9cd2c958..5d42e431fc 100644 --- a/typescript/sdk/src/core/HyperlaneCoreChecker.ts +++ b/typescript/sdk/src/core/HyperlaneCoreChecker.ts @@ -85,7 +85,7 @@ export class HyperlaneCoreChecker extends HyperlaneAppChecker< if (!matches) { const violation: MailboxViolation = { type: CoreViolationType.Mailbox, - mailboxType: MailboxViolationType.DefaultIsm, + subType: MailboxViolationType.DefaultIsm, contract: mailbox, chain, actual: actualIsm, diff --git a/typescript/sdk/src/core/types.ts b/typescript/sdk/src/core/types.ts index efc70f00a5..d08e86603b 100644 --- a/typescript/sdk/src/core/types.ts +++ b/typescript/sdk/src/core/types.ts @@ -25,8 +25,8 @@ export enum MailboxViolationType { export interface MailboxViolation extends CheckerViolation { type: CoreViolationType.Mailbox; + subType: MailboxViolationType; contract: Mailbox; - mailboxType: MailboxViolationType; } export interface MailboxMultisigIsmViolation extends MailboxViolation { diff --git a/typescript/sdk/src/deploy/HyperlaneAppChecker.ts b/typescript/sdk/src/deploy/HyperlaneAppChecker.ts index 014660fafe..3caef2dcb5 100644 --- a/typescript/sdk/src/deploy/HyperlaneAppChecker.ts +++ b/typescript/sdk/src/deploy/HyperlaneAppChecker.ts @@ -3,6 +3,7 @@ import { keccak256 } from 'ethers/lib/utils'; import { Ownable, TimelockController } from '@hyperlane-xyz/core'; import { Address, + ProtocolType, assert, eqAddress, objMap, @@ -49,7 +50,11 @@ export abstract class HyperlaneAppChecker< async check(): Promise { Object.keys(this.configMap) - .filter((_) => !this.app.chains().includes(_)) + .filter( + (chain) => + this.multiProvider.getChainMetadata(chain).protocol === + ProtocolType.Ethereum && !this.app.chains().includes(chain), + ) .forEach((chain: string) => this.addViolation({ type: ViolationType.NotDeployed, @@ -60,6 +65,7 @@ export abstract class HyperlaneAppChecker< ); return Promise.all( + // this.app.chains() will only return Ethereum chains that can be interacted with. this.app.chains().map((chain) => this.checkChain(chain)), ); } @@ -230,4 +236,24 @@ export abstract class HyperlaneAppChecker< const count = this.violations.length; assert(count === 0, `Found ${count} violations`); } + + logViolationsTable(): void { + const violations = this.violations; + if (violations.length > 0) { + // eslint-disable-next-line no-console + console.table(violations, [ + 'chain', + 'remote', + 'name', + 'type', + 'subType', + 'actual', + 'expected', + 'description', + ]); + } else { + // eslint-disable-next-line no-console + console.info(`${module} Checker found no violations`); + } + } } diff --git a/typescript/sdk/src/deploy/HyperlaneDeployer.ts b/typescript/sdk/src/deploy/HyperlaneDeployer.ts index 74cfb7a20b..9738849dee 100644 --- a/typescript/sdk/src/deploy/HyperlaneDeployer.ts +++ b/typescript/sdk/src/deploy/HyperlaneDeployer.ts @@ -13,7 +13,12 @@ import { TransparentUpgradeableProxy, TransparentUpgradeableProxy__factory, } from '@hyperlane-xyz/core'; -import { Address, eqAddress, runWithTimeout } from '@hyperlane-xyz/utils'; +import { + Address, + ProtocolType, + eqAddress, + runWithTimeout, +} from '@hyperlane-xyz/utils'; import { HyperlaneAddressesMap, @@ -77,8 +82,14 @@ export abstract class HyperlaneDeployer< configMap: ChainMap, ): Promise> { const configChains = Object.keys(configMap); + const ethereumConfigChains = configChains.filter( + (chain) => + this.multiProvider.getChainMetadata(chain).protocol === + ProtocolType.Ethereum, + ); + const targetChains = this.multiProvider.intersect( - configChains, + ethereumConfigChains, true, ).intersection; diff --git a/typescript/sdk/src/gas/HyperlaneIgpChecker.ts b/typescript/sdk/src/gas/HyperlaneIgpChecker.ts index 05d119384e..396140c972 100644 --- a/typescript/sdk/src/gas/HyperlaneIgpChecker.ts +++ b/typescript/sdk/src/gas/HyperlaneIgpChecker.ts @@ -1,4 +1,4 @@ -import { BigNumber, utils as ethersUtils } from 'ethers'; +import { BigNumber, ethers, utils as ethersUtils } from 'ethers'; import { Address, eqAddress } from '@hyperlane-xyz/utils'; @@ -105,7 +105,13 @@ export class HyperlaneIgpChecker extends HyperlaneAppChecker< const remotes = this.app.remoteChains(local); for (const remote of remotes) { - const expectedOverhead = this.configMap[local].overhead[remote]; + let expectedOverhead = this.configMap[local].overhead[remote]; + if (!expectedOverhead) { + this.app.logger( + `No overhead configured for ${local} -> ${remote}, defaulting to 0`, + ); + expectedOverhead = 0; + } const remoteId = this.multiProvider.getDomainId(remote); const existingOverhead = await defaultIsmIgp.destinationGasOverhead( @@ -141,13 +147,18 @@ export class HyperlaneIgpChecker extends HyperlaneAppChecker< expected: {}, }; - const remotes = this.app.remoteChains(local); + // In addition to all remote chains on the app, which are just Ethereum chains, + // also consider what the config says about non-Ethereum chains. + const remotes = new Set([ + ...this.app.remoteChains(local), + ...Object.keys(this.configMap[local].gasOracleType), + ]); for (const remote of remotes) { const remoteId = this.multiProvider.getDomainId(remote); const actualGasOracle = await igp.gasOracles(remoteId); const expectedGasOracle = this.getGasOracleAddress(local, remote); - if (eqAddress(actualGasOracle, expectedGasOracle)) { + if (!eqAddress(actualGasOracle, expectedGasOracle)) { const remoteChain = remote as ChainName; gasOraclesViolation.actual[remoteChain] = actualGasOracle; gasOraclesViolation.expected[remoteChain] = expectedGasOracle; @@ -160,7 +171,7 @@ export class HyperlaneIgpChecker extends HyperlaneAppChecker< const actualBeneficiary = await igp.beneficiary(); const expectedBeneficiary = this.configMap[local].beneficiary; - if (eqAddress(actualBeneficiary, expectedBeneficiary)) { + if (!eqAddress(actualBeneficiary, expectedBeneficiary)) { const violation: IgpBeneficiaryViolation = { type: 'InterchainGasPaymaster', subType: IgpViolationType.Beneficiary, @@ -177,9 +188,10 @@ export class HyperlaneIgpChecker extends HyperlaneAppChecker< const config = this.configMap[local]; const gasOracleType = config.gasOracleType[remote]; if (!gasOracleType) { - throw Error( - `Expected gas oracle type for local ${local} and remote ${remote}`, + this.app.logger( + `No gas oracle for local ${local} and remote ${remote}, defaulting to zero address`, ); + return ethers.constants.AddressZero; } const coreContracts = this.app.getContracts(local); switch (gasOracleType) { diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 5926197fad..451bea7862 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -28,17 +28,18 @@ export { HyperlaneEnvironmentChain, hyperlaneContractAddresses, hyperlaneEnvironments, - hyperlaneEnvironmentsWithSealevel, } from './consts/environments'; export { defaultMultisigIsmConfigs } from './consts/multisigIsm'; export { SEALEVEL_SPL_NOOP_ADDRESS } from './consts/sealevel'; export { attachContracts, attachContractsMap, + attachContractsMapAndGetForeignDeployments, connectContracts, connectContractsMap, filterAddressesMap, - filterAddressesToProtocol, + filterChainMapExcludeProtocol, + filterChainMapToProtocol, filterOwnableContracts, serializeContracts, serializeContractsMap, @@ -118,6 +119,7 @@ export { export { HyperlaneIsmFactory, collectValidators, + moduleCanCertainlyVerify, } from './ism/HyperlaneIsmFactory'; export { HyperlaneIsmFactoryDeployer } from './ism/HyperlaneIsmFactoryDeployer'; export { @@ -252,6 +254,8 @@ export { ConnectionClientConfig, ConnectionClientViolation, ConnectionClientViolationType, + RouterViolation, + RouterViolationType, ForeignDeploymentConfig, GasConfig, GasRouterConfig, diff --git a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts index d509231fcb..c712354e7d 100644 --- a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts +++ b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts @@ -370,6 +370,12 @@ export async function moduleMatchesConfig( return eqAddress(moduleAddress, config); } + // If the module address is zero, it can't match any object-based config. + // The subsequent check of what moduleType it is will throw, so we fail here. + if (eqAddress(moduleAddress, ethers.constants.AddressZero)) { + return false; + } + const provider = multiProvider.getProvider(chain); const module = IInterchainSecurityModule__factory.connect( moduleAddress, diff --git a/typescript/sdk/src/middleware/account/InterchainAccount.ts b/typescript/sdk/src/middleware/account/InterchainAccount.ts index 0c4a1d0684..bfa2a061fa 100644 --- a/typescript/sdk/src/middleware/account/InterchainAccount.ts +++ b/typescript/sdk/src/middleware/account/InterchainAccount.ts @@ -1,10 +1,14 @@ import { InterchainAccountRouter } from '@hyperlane-xyz/core'; +import { ProtocolType } from '@hyperlane-xyz/utils'; import { HyperlaneEnvironment, hyperlaneEnvironments, } from '../../consts/environments'; -import { appFromAddressesMapHelper } from '../../contracts/contracts'; +import { + appFromAddressesMapHelper, + filterChainMapToProtocol, +} from '../../contracts/contracts'; import { HyperlaneAddressesMap, HyperlaneContracts, @@ -32,7 +36,13 @@ export class InterchainAccount extends RouterApp { if (!envAddresses) { throw new Error(`No addresses found for ${env}`); } - return InterchainAccount.fromAddressesMap(envAddresses, multiProvider); + // Filter out non-EVM chains, as interchain accounts are EVM only at the moment. + const ethAddresses = filterChainMapToProtocol( + envAddresses, + ProtocolType.Ethereum, + multiProvider, + ); + return InterchainAccount.fromAddressesMap(ethAddresses, multiProvider); } static fromAddressesMap( diff --git a/typescript/sdk/src/router/HyperlaneRouterChecker.ts b/typescript/sdk/src/router/HyperlaneRouterChecker.ts index 442ac11837..be0196ea8d 100644 --- a/typescript/sdk/src/router/HyperlaneRouterChecker.ts +++ b/typescript/sdk/src/router/HyperlaneRouterChecker.ts @@ -1,10 +1,15 @@ import { ethers } from 'ethers'; -import { addressToBytes32, assert, eqAddress } from '@hyperlane-xyz/utils'; +import { addressToBytes32, eqAddress } from '@hyperlane-xyz/utils'; import { HyperlaneFactories } from '../contracts/types'; import { HyperlaneAppChecker } from '../deploy/HyperlaneAppChecker'; -import { ChainName } from '../types'; +import { + HyperlaneIsmFactory, + moduleMatchesConfig, +} from '../ism/HyperlaneIsmFactory'; +import { MultiProvider } from '../providers/MultiProvider'; +import { ChainMap, ChainName } from '../types'; import { RouterApp } from './RouterApps'; import { @@ -13,6 +18,8 @@ import { ConnectionClientViolationType, OwnableConfig, RouterConfig, + RouterViolation, + RouterViolationType, } from './types'; export class HyperlaneRouterChecker< @@ -20,6 +27,15 @@ export class HyperlaneRouterChecker< App extends RouterApp, Config extends RouterConfig, > extends HyperlaneAppChecker { + constructor( + multiProvider: MultiProvider, + app: App, + configMap: ChainMap, + readonly ismFactory?: HyperlaneIsmFactory, + ) { + super(multiProvider, app, configMap); + } + async checkChain(chain: ChainName): Promise { await this.checkHyperlaneConnectionClient(chain); await this.checkEnrolledRouters(chain); @@ -34,10 +50,44 @@ export class HyperlaneRouterChecker< violationType: ConnectionClientViolationType, ) => { const actual = await router[property](); - // TODO: check for IsmConfig const value = this.configMap[chain][property]; - if (value && typeof value === 'object') - throw new Error('ISM as object unimplemented'); + + // If the value is an object, it's an ISM config + // and we should make sure it matches the actual ISM config + if (value && typeof value === 'object') { + if (!this.ismFactory) { + throw Error( + 'ISM factory not provided to HyperlaneRouterChecker, cannot check object-based ISM config', + ); + } + + const matches = await moduleMatchesConfig( + chain, + actual, + value, + this.multiProvider, + this.ismFactory!.chainMap[chain], + ); + + if (!matches) { + this.app.logger( + `Deploying ISM; ISM config of actual ${actual} does not match expected config ${JSON.stringify( + value, + )}`, + ); + const deployedIsm = await this.ismFactory.deploy(chain, value); + const violation: ConnectionClientViolation = { + chain, + type: violationType, + contract: router, + actual, + expected: deployedIsm.address, + description: `ISM config does not match deployed ISM at ${deployedIsm.address}`, + }; + this.addViolation(violation); + } + return; + } const expected = value && typeof value === 'string' ? value @@ -73,12 +123,21 @@ export class HyperlaneRouterChecker< await Promise.all( this.app.remoteChains(chain).map(async (remoteChain) => { - const remoteRouter = this.app.router( - this.app.getContracts(remoteChain), - ); + const remoteRouterAddress = this.app.routerAddress(remoteChain); const remoteDomainId = this.multiProvider.getDomainId(remoteChain); - const address = await router.routers(remoteDomainId); - assert(address === addressToBytes32(remoteRouter.address)); + const actualRouter = await router.routers(remoteDomainId); + const expectedRouter = addressToBytes32(remoteRouterAddress); + if (actualRouter !== expectedRouter) { + const violation: RouterViolation = { + chain, + remoteChain, + type: RouterViolationType.EnrolledRouter, + contract: router, + actual: actualRouter, + expected: expectedRouter, + }; + this.addViolation(violation); + } }), ); } diff --git a/typescript/sdk/src/router/RouterApps.ts b/typescript/sdk/src/router/RouterApps.ts index d805338d5e..fc12218019 100644 --- a/typescript/sdk/src/router/RouterApps.ts +++ b/typescript/sdk/src/router/RouterApps.ts @@ -1,10 +1,21 @@ +import debug from 'debug'; import type { BigNumber } from 'ethers'; import { GasRouter, Router } from '@hyperlane-xyz/core'; -import { Address, objMap, promiseObjAll } from '@hyperlane-xyz/utils'; +import { + Address, + ProtocolType, + objMap, + promiseObjAll, +} from '@hyperlane-xyz/utils'; import { HyperlaneApp } from '../app/HyperlaneApp'; -import { HyperlaneContracts, HyperlaneFactories } from '../contracts/types'; +import { + HyperlaneContracts, + HyperlaneContractsMap, + HyperlaneFactories, +} from '../contracts/types'; +import { MultiProvider } from '../providers/MultiProvider'; import { ChainMap, ChainName } from '../types'; export { Router } from '@hyperlane-xyz/core'; @@ -12,8 +23,34 @@ export { Router } from '@hyperlane-xyz/core'; export abstract class RouterApp< Factories extends HyperlaneFactories, > extends HyperlaneApp { + constructor( + contractsMap: HyperlaneContractsMap, + multiProvider: MultiProvider, + logger?: debug.Debugger, + readonly foreignDeployments: ChainMap
= {}, + ) { + super(contractsMap, multiProvider, logger); + } + abstract router(contracts: HyperlaneContracts): Router; + routerAddress(chainName: string): Address { + if ( + this.multiProvider.getChainMetadata(chainName).protocol === + ProtocolType.Ethereum + ) { + return this.router(this.contractsMap[chainName]).address; + } + return this.foreignDeployments[chainName]; + } + + override remoteChains(chainName: string): string[] { + return [ + ...super.remoteChains(chainName), + ...Object.keys(this.foreignDeployments), + ].filter((chain) => chain !== chainName); + } + getSecurityModules(): Promise> { return promiseObjAll( objMap(this.chainMap, (_, contracts) => diff --git a/typescript/sdk/src/router/types.ts b/typescript/sdk/src/router/types.ts index 9a56f06d42..792be1c067 100644 --- a/typescript/sdk/src/router/types.ts +++ b/typescript/sdk/src/router/types.ts @@ -1,6 +1,7 @@ import { HyperlaneConnectionClient, ProxyAdmin__factory, + Router, TimelockController__factory, } from '@hyperlane-xyz/core'; import type { Address } from '@hyperlane-xyz/utils'; @@ -61,4 +62,17 @@ export interface ConnectionClientViolation extends CheckerViolation { contract: HyperlaneConnectionClient; actual: string; expected: string; + description?: string; +} + +export enum RouterViolationType { + EnrolledRouter = 'EnrolledRouter', +} + +export interface RouterViolation extends CheckerViolation { + type: RouterViolationType.EnrolledRouter; + remoteChain: string; + contract: Router; + actual: string; + expected: string; } diff --git a/typescript/utils/index.ts b/typescript/utils/index.ts index e0b66e099e..a60d68b8d0 100644 --- a/typescript/utils/index.ts +++ b/typescript/utils/index.ts @@ -30,6 +30,7 @@ export { } from './src/addresses'; export { convertDecimals, + convertDecimalsEthersBigNumber, eqAmountApproximate, fromWei, fromWeiRounded, @@ -44,7 +45,7 @@ export { sleep, timeout, } from './src/async'; -export { base58ToBuffer, bufferToBase58 } from './src/base58'; +export { base58ToBuffer, bufferToBase58, hexOrBase58ToHex } from './src/base58'; export { fromBase64, toBase64 } from './src/base64'; export { BigNumberMax, diff --git a/typescript/utils/src/amount.ts b/typescript/utils/src/amount.ts index b4c115b86e..9aae2cad65 100644 --- a/typescript/utils/src/amount.ts +++ b/typescript/utils/src/amount.ts @@ -1,5 +1,6 @@ import { formatUnits, parseUnits } from '@ethersproject/units'; import BigNumber from 'bignumber.js'; +import { ethers } from 'ethers'; const DEFAULT_MIN_ROUNDED_VALUE = 0.00001; const DEFAULT_DISPLAY_DECIMALS = 4; @@ -117,3 +118,28 @@ export function convertDecimals( return amount.times(new BigNumber(10).pow(difference)); } } + +/** + * Converts a value with `fromDecimals` decimals to a value with `toDecimals` decimals. + * Incurs a loss of precision when `fromDecimals` > `toDecimals`. + * @param fromDecimals The number of decimals `value` has. + * @param toDecimals The number of decimals to convert `value` to. + * @param value The value to convert. + * @returns `value` represented with `toDecimals` decimals. + */ +export function convertDecimalsEthersBigNumber( + fromDecimals: number, + toDecimals: number, + value: ethers.BigNumber, +) { + if (fromDecimals === toDecimals) return value; + else if (fromDecimals > toDecimals) { + const difference = fromDecimals - toDecimals; + return value.div(ethers.BigNumber.from('10').pow(difference)); + } + // fromDecimals < toDecimals + else { + const difference = toDecimals - fromDecimals; + return value.mul(ethers.BigNumber.from('10').pow(difference)); + } +} diff --git a/typescript/utils/src/base58.ts b/typescript/utils/src/base58.ts index 9a591f0ae6..b30bbcb4bc 100644 --- a/typescript/utils/src/base58.ts +++ b/typescript/utils/src/base58.ts @@ -7,3 +7,11 @@ export function base58ToBuffer(value: string) { export function bufferToBase58(value: Buffer) { return utils.base58.encode(value); } + +// If the value is already hex (checked by 0x prefix), return it as is. +// Otherwise, treat it as base58 and convert it to hex. +export function hexOrBase58ToHex(value: string) { + if (value.startsWith('0x')) return value; + + return utils.hexlify(base58ToBuffer(value)); +} diff --git a/typescript/utils/src/objects.ts b/typescript/utils/src/objects.ts index 4ceb780b26..a99e7ff1d2 100644 --- a/typescript/utils/src/objects.ts +++ b/typescript/utils/src/objects.ts @@ -95,8 +95,8 @@ export function invertKeysAndValues(data: any) { } // Returns an object with the keys as values from an array and value set to true -export function arrayToObject(keys: Array, val = true) { - return keys.reduce>((result, k) => { +export function arrayToObject(keys: Array, val = true) { + return keys.reduce>((result, k) => { result[k] = val; return result; }, {});