//! Test client for Hyperlane Sealevel Mailbox contract. // #![deny(missing_docs)] // FIXME #![deny(unsafe_code)] use std::{path::PathBuf, str::FromStr}; use clap::{Args, Parser, Subcommand, ValueEnum}; use solana_clap_utils::input_validators::{is_keypair, is_url, normalize_to_url_if_moniker}; use solana_cli_config::{Config, CONFIG_FILE}; use solana_client::rpc_client::RpcClient; use solana_program::pubkey; use solana_sdk::{ commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, instruction::{AccountMeta, Instruction}, pubkey::Pubkey, signature::{read_keypair_file, Keypair, Signer as _}, system_program, }; use account_utils::DiscriminatorEncode; use hyperlane_core::{H160, H256}; use hyperlane_sealevel_connection_client::router::RemoteRouterConfig; use hyperlane_sealevel_igp::{ accounts::{ GasOracle, GasPaymentAccount, IgpAccount, InterchainGasPaymasterType, OverheadIgpAccount, ProgramDataAccount as IgpProgramDataAccount, RemoteGasData, }, igp_gas_payment_pda_seeds, igp_program_data_pda_seeds, instruction::{GasOracleConfig, GasOverheadConfig}, }; use hyperlane_sealevel_mailbox::{ accounts::{InboxAccount, OutboxAccount}, 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_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, }; use hyperlane_sealevel_token_collateral::{ hyperlane_token_escrow_pda_seeds, plugin::CollateralPlugin, }; use hyperlane_sealevel_token_lib::{ accounts::HyperlaneTokenAccount, hyperlane_token_pda_seeds, instruction::{Instruction as HtInstruction, TransferRemote as HtTransferRemote}, }; use hyperlane_sealevel_token_native::hyperlane_token_native_collateral_pda_seeds; use hyperlane_sealevel_validator_announce::{ accounts::ValidatorStorageLocationsAccount, instruction::{ AnnounceInstruction as ValidatorAnnounceAnnounceInstruction, Instruction as ValidatorAnnounceInstruction, }, replay_protection_pda_seeds, validator_announce_pda_seeds, validator_storage_locations_pda_seeds, }; use warp_route::parse_token_account_data; 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; const MAX_HEAP_FRAME_BYTES: u32 = 256 * 1024; const ECLIPSE_DOMAIN: u32 = 13375; // TODO import from hyperlane #[derive(Parser)] #[command(version, about)] struct Cli { #[command(subcommand)] cmd: HyperlaneSealevelCmd, #[arg(long, short)] url: Option, #[arg(long, short)] keypair: Option, #[arg(long, short = 'b', default_value_t = MAX_COMPUTE_UNIT_LIMIT)] compute_budget: u32, #[arg(long, short = 'a')] heap_size: Option, #[arg(long, short = 'C')] config: Option, #[arg(long, default_value_t = false)] require_tx_approval: bool, } #[derive(Subcommand)] enum HyperlaneSealevelCmd { Core(CoreCmd), Mailbox(MailboxCmd), Token(TokenCmd), Igp(IgpCmd), ValidatorAnnounce(ValidatorAnnounceCmd), MultisigIsmMessageId(MultisigIsmMessageIdCmd), WarpRoute(WarpRouteCmd), HelloWorld(HelloWorldCmd), } #[derive(Args)] pub(crate) struct WarpRouteCmd { #[command(subcommand)] cmd: WarpRouteSubCmd, } #[derive(Subcommand)] pub(crate) enum WarpRouteSubCmd { Deploy(WarpRouteDeploy), DestinationGas(DestinationGasArgs), } #[derive(Args)] pub(crate) struct WarpRouteDeploy { #[arg(long)] environment: String, #[arg(long)] environments_dir: PathBuf, #[arg(long)] built_so_dir: PathBuf, #[arg(long)] warp_route_name: String, #[arg(long)] token_config_file: PathBuf, #[arg(long)] chain_config_file: PathBuf, #[arg(long)] ata_payer_funding_amount: Option, } #[derive(Args)] struct DestinationGasArgs { #[arg(long)] program_id: Pubkey, #[arg(long)] destination_domain: u32, } #[derive(Args)] struct CoreCmd { #[command(subcommand)] cmd: CoreSubCmd, } #[derive(Subcommand)] enum CoreSubCmd { Deploy(CoreDeploy), } #[derive(Args)] struct CoreDeploy { #[arg(long)] local_domain: u32, #[arg(long)] environment: String, #[arg(long)] gas_oracle_config_file: Option, #[arg(long)] overhead_config_file: Option, #[arg(long)] chain: String, #[arg(long)] use_existing_keys: bool, #[arg(long)] environments_dir: PathBuf, #[arg(long, num_args = 1.., value_delimiter = ',')] remote_domains: Vec, #[arg(long)] built_so_dir: PathBuf, } #[derive(Args)] struct MailboxCmd { #[command(subcommand)] cmd: MailboxSubCmd, } #[derive(Subcommand)] enum MailboxSubCmd { Init(Init), Query(Query), Send(Outbox), Delivered(Delivered), TransferOwnership(TransferOwnership), SetDefaultIsm(SetDefaultIsm), } const MAILBOX_PROG_ID: Pubkey = pubkey!("692KZJaoe2KRcD6uhCQDLLXnLNA5ZLnfvdqjE4aX9iu1"); const HYPERLANE_TOKEN_PROG_ID: Pubkey = pubkey!("3MzUPjP5LEkiHH82nEAe28Xtz9ztuMqWc8UmuKxrpVQH"); const MULTISIG_ISM_MESSAGE_ID_PROG_ID: Pubkey = pubkey!("2YjtZDiUoptoSsA5eVrDCcX6wxNK6YoEVW7y82x5Z2fw"); const VALIDATOR_ANNOUNCE_PROG_ID: Pubkey = pubkey!("DH43ae1LwemXAboWwSh8zc9pG8j72gKUEXNi57w8fEnn"); #[derive(Args)] struct Init { #[arg(long, short, default_value_t = MAILBOX_PROG_ID)] program_id: Pubkey, #[arg(long, short, default_value_t = ECLIPSE_DOMAIN)] local_domain: u32, #[arg(long, short, default_value_t = MULTISIG_ISM_MESSAGE_ID_PROG_ID)] default_ism: Pubkey, } #[derive(Args)] struct Query { #[arg(long, short, default_value_t = MAILBOX_PROG_ID)] 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)] destination: u32, #[arg(long, short)] recipient: Pubkey, #[arg(long, short, default_value = "Hello, World!")] message: String, #[arg(long, short, default_value_t = MAILBOX_PROG_ID)] program_id: Pubkey, } #[derive(Args)] struct Inbox { #[arg(long, short, default_value_t = ECLIPSE_DOMAIN)] local_domain: u32, #[arg(long, short, default_value_t = ECLIPSE_DOMAIN)] origin: u32, #[arg(long, short)] recipient: Pubkey, #[arg(long, short, default_value = "Hello, World!")] message: String, #[arg(long, short, default_value_t = 1)] nonce: u32, #[arg(long, short, default_value_t = MAILBOX_PROG_ID)] program_id: Pubkey, #[arg(long, default_value_t = MULTISIG_ISM_MESSAGE_ID_PROG_ID)] ism: Pubkey, } #[derive(Args)] struct Delivered { #[arg(long, short, default_value_t = MAILBOX_PROG_ID)] program_id: Pubkey, #[arg(long, short)] message_id: H256, } #[derive(Args)] struct TokenCmd { #[command(subcommand)] cmd: TokenSubCmd, } #[derive(Subcommand)] enum TokenSubCmd { Query(TokenQuery), TransferRemote(TokenTransferRemote), EnrollRemoteRouter(TokenEnrollRemoteRouter), TransferOwnership(TransferOwnership), SetInterchainSecurityModule(SetInterchainSecurityModule), Igp(Igp), } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] pub enum TokenType { Native, Synthetic, Collateral, } #[derive(Args)] struct TokenQuery { #[arg(long, short, default_value_t = HYPERLANE_TOKEN_PROG_ID)] program_id: Pubkey, #[arg(value_enum)] token_type: TokenType, } #[derive(Args)] struct TokenTransferRemote { #[arg(long, short, default_value_t = HYPERLANE_TOKEN_PROG_ID)] program_id: Pubkey, // Note this is the keypair for normal account not the derived associated token account or delegate. sender: String, amount: u64, // #[arg(long, short, default_value_t = ECLIPSE_DOMAIN)] destination_domain: u32, recipient: String, #[arg(value_enum)] token_type: TokenType, } #[derive(Args)] struct TokenEnrollRemoteRouter { #[arg(long, short, default_value_t = HYPERLANE_TOKEN_PROG_ID)] program_id: Pubkey, domain: u32, router: H256, } #[derive(Args)] struct SetInterchainSecurityModule { #[arg(long, short)] program_id: Pubkey, #[arg(long, short)] ism: Option, } #[derive(Args)] struct TransferOwnership { #[arg(long, short)] program_id: Pubkey, // To avoid accidentally transferring ownership to None, // only support transferring to other Pubkeys for now. new_owner: Pubkey, } #[derive(Args)] struct Igp { #[arg(long, short, default_value_t = HYPERLANE_TOKEN_PROG_ID)] program_id: Pubkey, #[command(subcommand)] cmd: GetSetCmd, } #[derive(Subcommand)] enum GetSetCmd { Get(G), Set(S), } #[derive(Args)] struct SetIgpArgs { igp_program: Pubkey, #[arg(value_enum)] igp_type: IgpType, igp_account: Pubkey, } #[derive(Args)] struct GetIgpArgs { token_type: TokenType, } #[derive(ValueEnum, Clone)] enum IgpType { Igp, OverheadIgp, } #[derive(Args)] struct IgpCmd { #[command(subcommand)] cmd: IgpSubCmd, } #[derive(Subcommand)] enum IgpSubCmd { Query(IgpQueryArgs), PayForGas(PayForGasArgs), GasOracleConfig(GasOracleConfigArgs), DestinationGasOverhead(DestinationGasOverheadArgs), TransferIgpOwnership(TransferIgpOwnership), TransferOverheadIgpOwnership(TransferIgpOwnership), } #[derive(Args)] struct IgpQueryArgs { #[arg(long)] program_id: Pubkey, #[arg(long)] igp_account: Pubkey, #[arg(long)] gas_payment_account: Option, } #[derive(Args)] struct TransferIgpOwnership { #[arg(long, short)] program_id: Pubkey, // To avoid accidentally transferring ownership to None, // only support transferring to other Pubkeys for now. new_owner: Pubkey, #[arg(long)] igp_account: Pubkey, } #[derive(Args)] struct PayForGasArgs { #[arg(long)] program_id: Pubkey, #[arg(long)] message_id: String, #[arg(long)] destination_domain: u32, #[arg(long)] gas: u64, } #[derive(Args)] struct GasOracleConfigArgs { #[arg(long)] environment: String, #[arg(long)] environments_dir: PathBuf, #[arg(long)] chain_name: String, #[arg(long)] remote_domain: u32, #[command(subcommand)] cmd: GetSetCmd, } #[derive(Args)] struct SetGasOracleArgs { #[arg(long)] token_exchange_rate: u128, #[arg(long)] gas_price: u128, #[arg(long)] token_decimals: u8, } #[derive(Args)] struct GetGasOracleArgs; #[derive(Args)] struct DestinationGasOverheadArgs { #[arg(long)] environment: String, #[arg(long)] environments_dir: PathBuf, #[arg(long)] chain_name: String, #[arg(long)] remote_domain: u32, #[command(subcommand)] cmd: GasOverheadSubCmd, } #[derive(Subcommand)] enum GasOverheadSubCmd { Set(SetGasOverheadArgs), Get, } #[derive(Args)] struct SetGasOverheadArgs { #[arg(long)] gas_overhead: u64, } #[derive(Args)] struct ValidatorAnnounceCmd { #[command(subcommand)] cmd: ValidatorAnnounceSubCmd, } #[derive(Subcommand)] enum ValidatorAnnounceSubCmd { Init(ValidatorAnnounceInit), Announce(ValidatorAnnounceAnnounce), Query(ValidatorAnnounceQuery), } #[derive(Args)] struct ValidatorAnnounceInit { #[arg(long, short, default_value_t = VALIDATOR_ANNOUNCE_PROG_ID)] program_id: Pubkey, #[arg(long, short, default_value_t = MAILBOX_PROG_ID)] mailbox_id: Pubkey, #[arg(long, short, default_value_t = ECLIPSE_DOMAIN)] local_domain: u32, } #[derive(Args)] struct ValidatorAnnounceAnnounce { #[arg(long, short, default_value_t = VALIDATOR_ANNOUNCE_PROG_ID)] program_id: Pubkey, #[arg(long)] validator: H160, #[arg(long)] storage_location: String, #[arg(long)] signature: String, } #[derive(Args)] struct ValidatorAnnounceQuery { #[arg(long, short, default_value_t = VALIDATOR_ANNOUNCE_PROG_ID)] program_id: Pubkey, validator: H160, } #[derive(Args)] struct MultisigIsmMessageIdCmd { #[command(subcommand)] cmd: MultisigIsmMessageIdSubCmd, } #[derive(Subcommand)] enum MultisigIsmMessageIdSubCmd { Deploy(MultisigIsmMessageIdDeploy), Init(MultisigIsmMessageIdInit), SetValidatorsAndThreshold(MultisigIsmMessageIdSetValidatorsAndThreshold), 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)] struct MultisigIsmMessageIdInit { #[arg(long, short, default_value_t = MULTISIG_ISM_MESSAGE_ID_PROG_ID)] 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)] program_id: Pubkey, #[arg(long)] domain: u32, #[arg(long, value_delimiter = ',')] validators: Vec, #[arg(long)] 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(); let cli = Cli::parse(); let config = match cli.config.as_ref().or(CONFIG_FILE.as_ref()) { Some(config_file) => Config::load(config_file) .map_err(|e| format!("Failed to load solana config file {}: {}", config_file, e)) .unwrap(), None => Config::default(), }; let url = normalize_to_url_if_moniker(cli.url.unwrap_or(config.json_rpc_url)); is_url(&url).unwrap(); let client = RpcClient::new(url); let keypair_path = cli.keypair.unwrap_or(config.keypair_path); let (payer_pubkey, payer_keypair) = if let Ok(payer_keypair) = read_keypair_file(&keypair_path) { ( payer_keypair.pubkey(), Some(PayerKeypair { keypair: payer_keypair, keypair_path, }), ) } else { println!( "Provided key is not a keypair file, treating as a public key {}", keypair_path ); (Pubkey::from_str(&keypair_path).unwrap(), None) }; let commitment = CommitmentConfig::processed(); let mut instructions = vec![]; if cli.compute_budget != DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT { assert!(cli.compute_budget <= MAX_COMPUTE_UNIT_LIMIT); instructions.push( ( ComputeBudgetInstruction::set_compute_unit_limit(cli.compute_budget), Some(format!("Set compute unit limit to {}", cli.compute_budget)), ) .into(), ); } if let Some(heap_size) = cli.heap_size { assert!(heap_size <= MAX_HEAP_FRAME_BYTES); instructions.push( ( ComputeBudgetInstruction::request_heap_frame(heap_size), Some(format!("Request heap frame of {} bytes", heap_size)), ) .into(), ); } let ctx = Context::new( client, payer_pubkey, payer_keypair, commitment, instructions.into(), cli.require_tx_approval, ); match cli.cmd { HyperlaneSealevelCmd::Mailbox(cmd) => process_mailbox_cmd(ctx, cmd), HyperlaneSealevelCmd::Token(cmd) => process_token_cmd(ctx, cmd), HyperlaneSealevelCmd::ValidatorAnnounce(cmd) => process_validator_announce_cmd(ctx, cmd), HyperlaneSealevelCmd::MultisigIsmMessageId(cmd) => { process_multisig_ism_message_id_cmd(ctx, cmd) } 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), } } fn process_mailbox_cmd(ctx: Context, cmd: MailboxCmd) { match cmd.cmd { MailboxSubCmd::Init(init) => { let instruction = hyperlane_sealevel_mailbox::instruction::init_instruction( init.program_id, init.local_domain, init.default_ism, ctx.payer_pubkey, ) .unwrap(); ctx.new_txn().add(instruction).send_with_payer(); } MailboxSubCmd::Query(query) => { let (inbox_account, inbox_bump) = Pubkey::find_program_address(mailbox_inbox_pda_seeds!(), &query.program_id); let (outbox_account, outbox_bump) = Pubkey::find_program_address(mailbox_outbox_pda_seeds!(), &query.program_id); let accounts = ctx .client .get_multiple_accounts_with_commitment( &[inbox_account, outbox_account], ctx.commitment, ) .unwrap() .value; println!("mailbox={}", query.program_id); println!("--------------------------------"); println!("Inbox: {}, bump={}", inbox_account, inbox_bump); if let Some(info) = &accounts[0] { println!("{:#?}", info); match InboxAccount::fetch(&mut info.data.as_ref()) { Ok(inbox) => println!("{:#?}", inbox.into_inner()), Err(err) => println!("Failed to deserialize account data: {}", err), } } else { println!("Not yet created?"); } println!("--------------------------------"); println!("Outbox: {}, bump={}", outbox_account, outbox_bump); if let Some(info) = &accounts[1] { println!("{:#?}", info); match OutboxAccount::fetch(&mut info.data.as_ref()) { Ok(outbox) => println!("{:#?}", outbox.into_inner()), Err(err) => println!("Failed to deserialize account data: {}", err), } } else { println!("Not yet created?"); } } MailboxSubCmd::Send(outbox) => { let (outbox_account, _outbox_bump) = Pubkey::find_program_address(mailbox_outbox_pda_seeds!(), &outbox.program_id); let ixn = MailboxInstruction::OutboxDispatch(OutboxDispatch { sender: ctx.payer_pubkey, destination_domain: outbox.destination, recipient: H256(outbox.recipient.to_bytes()), message_body: outbox.message.into(), }); let outbox_instruction = Instruction { program_id: outbox.program_id, data: ixn.into_instruction_data().unwrap(), accounts: vec![ AccountMeta::new(outbox_account, false), AccountMeta::new_readonly(ctx.payer_pubkey, true), AccountMeta::new_readonly(spl_noop::id(), false), ], }; ctx.new_txn().add(outbox_instruction).send_with_payer(); } MailboxSubCmd::Delivered(delivered) => { let (processed_message_account_key, _processed_message_account_bump) = Pubkey::find_program_address( mailbox_processed_message_pda_seeds!(delivered.message_id), &delivered.program_id, ); let account = ctx .client .get_account_with_commitment(&processed_message_account_key, ctx.commitment) .unwrap() .value; if account.is_none() { println!("Message not delivered"); } else { println!("Message delivered"); } } MailboxSubCmd::TransferOwnership(transfer_ownership) => { let instruction = hyperlane_sealevel_mailbox::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(); } 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(); } }; } fn process_token_cmd(ctx: Context, cmd: TokenCmd) { match cmd.cmd { TokenSubCmd::Query(query) => { let (token_account, token_bump) = Pubkey::find_program_address(hyperlane_token_pda_seeds!(), &query.program_id); let mut accounts_to_query = vec![token_account]; match query.token_type { TokenType::Native => { let (native_collateral_account, _native_collateral_bump) = Pubkey::find_program_address( hyperlane_token_native_collateral_pda_seeds!(), &query.program_id, ); accounts_to_query.push(native_collateral_account); } TokenType::Synthetic => { let (mint_account, _mint_bump) = Pubkey::find_program_address( hyperlane_token_mint_pda_seeds!(), &query.program_id, ); let (ata_payer_account, _ata_payer_bump) = Pubkey::find_program_address( hyperlane_token_ata_payer_pda_seeds!(), &query.program_id, ); accounts_to_query.push(mint_account); accounts_to_query.push(ata_payer_account); } TokenType::Collateral => { let (escrow_account, _escrow_bump) = Pubkey::find_program_address( hyperlane_token_escrow_pda_seeds!(), &query.program_id, ); accounts_to_query.push(escrow_account); } } let accounts = ctx .client .get_multiple_accounts_with_commitment(&accounts_to_query, ctx.commitment) .unwrap() .value; println!("hyperlane-sealevel-token={}", query.program_id); println!("--------------------------------"); println!( "Hyperlane Token Storage: {}, bump={}", token_account, token_bump ); if let Some(info) = &accounts[0] { println!("{:#?}", info); parse_token_account_data(query.token_type, &mut info.data.as_ref()); } else { println!("Not yet created?"); } println!("--------------------------------"); match query.token_type { TokenType::Native => { let (native_collateral_account, native_collateral_bump) = Pubkey::find_program_address( hyperlane_token_native_collateral_pda_seeds!(), &query.program_id, ); println!( "Native Token Collateral: {}, bump={}", native_collateral_account, native_collateral_bump ); if let Some(info) = &accounts[1] { println!("{:#?}", info); } else { println!("Not yet created?"); } println!("--------------------------------"); } TokenType::Synthetic => { let (mint_account, mint_bump) = Pubkey::find_program_address( hyperlane_token_mint_pda_seeds!(), &query.program_id, ); println!( "Mint / Mint Authority: {}, bump={}", mint_account, mint_bump ); if let Some(info) = &accounts[1] { println!("{:#?}", info); use solana_program::program_pack::Pack as _; match spl_token_2022::state::Mint::unpack_from_slice(info.data.as_ref()) { Ok(mint) => println!("{:#?}", mint), Err(err) => println!("Failed to deserialize account data: {}", err), } } else { println!("Not yet created?"); } let (ata_payer_account, ata_payer_bump) = Pubkey::find_program_address( hyperlane_token_ata_payer_pda_seeds!(), &query.program_id, ); println!( "ATA payer account: {}, bump={}", ata_payer_account, ata_payer_bump, ); } TokenType::Collateral => { let (escrow_account, escrow_bump) = Pubkey::find_program_address( hyperlane_token_escrow_pda_seeds!(), &query.program_id, ); println!( "escrow_account (key, bump)=({}, {})", escrow_account, escrow_bump, ); let (ata_payer_account, ata_payer_bump) = Pubkey::find_program_address( hyperlane_token_ata_payer_pda_seeds!(), &query.program_id, ); println!( "ATA payer account: {}, bump={}", ata_payer_account, ata_payer_bump, ); } } } TokenSubCmd::TransferRemote(xfer) => { is_keypair(&xfer.sender).unwrap(); let sender = read_keypair_file(xfer.sender).unwrap(); let recipient = if xfer.recipient.starts_with("0x") { H256::from_str(&xfer.recipient).unwrap() } else { let pubkey = Pubkey::from_str(&xfer.recipient).unwrap(); H256::from_slice(&pubkey.to_bytes()[..]) }; let (token_account, _token_bump) = Pubkey::find_program_address(hyperlane_token_pda_seeds!(), &xfer.program_id); let (dispatch_authority_account, _dispatch_authority_bump) = Pubkey::find_program_address( mailbox_message_dispatch_authority_pda_seeds!(), &xfer.program_id, ); let fetched_token_account = ctx .client .get_account_with_commitment(&token_account, ctx.commitment) .unwrap() .value .unwrap(); let token = HyperlaneTokenAccount::<()>::fetch(&mut &fetched_token_account.data[..]) .unwrap() .into_inner(); let unique_message_account_keypair = Keypair::new(); let (dispatched_message_account, _dispatched_message_bump) = Pubkey::find_program_address( mailbox_dispatched_message_pda_seeds!(&unique_message_account_keypair.pubkey()), &token.mailbox, ); let (mailbox_outbox_account, _mailbox_outbox_bump) = Pubkey::find_program_address(mailbox_outbox_pda_seeds!(), &token.mailbox); let ixn = HtInstruction::TransferRemote(HtTransferRemote { destination_domain: xfer.destination_domain, recipient, amount_or_id: xfer.amount.into(), }); // Transfers tokens to a remote. // Burns the tokens from the sender's associated token account and // then dispatches a message to the remote recipient. // // 0. `[executable]` The system program. // 1. `[executable]` The spl_noop program. // 2. `[]` The token PDA account. // 3. `[executable]` The mailbox program. // 4. `[writeable]` The mailbox outbox account. // 5. `[]` Message dispatch authority. // 6. `[signer]` The token sender and mailbox payer. // 7. `[signer]` Unique message / gas payment account. // 8. `[writeable]` Message storage PDA. // ---- If using an IGP ---- // 9. `[executable]` The IGP program. // 10. `[writeable]` The IGP program data. // 11. `[writeable]` Gas payment PDA. // 12. `[]` OPTIONAL - The Overhead IGP program, if the configured IGP is an Overhead IGP. // 13. `[writeable]` The IGP account. // ---- End if ---- // 14..N `[??..??]` Plugin-specific accounts. let mut accounts = vec![ AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(spl_noop::id(), false), AccountMeta::new_readonly(token_account, false), AccountMeta::new_readonly(token.mailbox, false), AccountMeta::new(mailbox_outbox_account, false), AccountMeta::new_readonly(dispatch_authority_account, false), AccountMeta::new(sender.pubkey(), true), AccountMeta::new_readonly(unique_message_account_keypair.pubkey(), true), AccountMeta::new(dispatched_message_account, false), ]; if let Some((igp_program_id, igp_account_type)) = token.interchain_gas_paymaster { let (igp_program_data, _bump) = Pubkey::find_program_address(igp_program_data_pda_seeds!(), &igp_program_id); let (gas_payment_pda, _bump) = Pubkey::find_program_address( igp_gas_payment_pda_seeds!(&unique_message_account_keypair.pubkey()), &igp_program_id, ); accounts.extend([ AccountMeta::new_readonly(igp_program_id, false), AccountMeta::new(igp_program_data, false), AccountMeta::new(gas_payment_pda, false), ]); match igp_account_type { InterchainGasPaymasterType::OverheadIgp(overhead_igp_account_id) => { let overhead_igp_account = ctx .client .get_account_with_commitment(&overhead_igp_account_id, ctx.commitment) .unwrap() .value .unwrap(); let overhead_igp_account = OverheadIgpAccount::fetch(&mut &overhead_igp_account.data[..]) .unwrap() .into_inner(); accounts.extend([ AccountMeta::new_readonly(overhead_igp_account_id, false), AccountMeta::new(overhead_igp_account.inner, false), ]); } InterchainGasPaymasterType::Igp(igp_account_id) => { accounts.push(AccountMeta::new(igp_account_id, false)); } } } match xfer.token_type { TokenType::Native => { // 5. `[executable]` The system program. // 6. `[writeable]` The native token collateral PDA account. let (native_collateral_account, _native_collateral_bump) = Pubkey::find_program_address( hyperlane_token_native_collateral_pda_seeds!(), &xfer.program_id, ); accounts.extend([ AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new(native_collateral_account, false), ]); } TokenType::Synthetic => { // 5. `[executable]` The spl_token_2022 program. // 6. `[writeable]` The mint / mint authority PDA account. // 7. `[writeable]` The token sender's associated token account, from which tokens will be burned. let (mint_account, _mint_bump) = Pubkey::find_program_address( hyperlane_token_mint_pda_seeds!(), &xfer.program_id, ); let sender_associated_token_account = get_associated_token_address_with_program_id( &sender.pubkey(), &mint_account, &spl_token_2022::id(), ); accounts.extend([ AccountMeta::new_readonly(spl_token_2022::id(), false), AccountMeta::new(mint_account, false), AccountMeta::new(sender_associated_token_account, false), ]); } TokenType::Collateral => { // 5. `[executable]` The SPL token program for the mint. // 6. `[writeable]` The mint. // 7. `[writeable]` The token sender's associated token account, from which tokens will be sent. // 8. `[writeable]` The escrow PDA account. let token = HyperlaneTokenAccount::::fetch( &mut &fetched_token_account.data[..], ) .unwrap() .into_inner(); let sender_associated_token_account = get_associated_token_address_with_program_id( &sender.pubkey(), &token.plugin_data.mint, &token.plugin_data.spl_token_program, ); accounts.extend([ AccountMeta::new_readonly(token.plugin_data.spl_token_program, false), AccountMeta::new(token.plugin_data.mint, false), AccountMeta::new(sender_associated_token_account, false), AccountMeta::new(token.plugin_data.escrow, false), ]); } } eprintln!("accounts={:#?}", accounts); // FIXME remove let xfer_instruction = Instruction { program_id: xfer.program_id, data: ixn.encode().unwrap(), accounts, }; let tx_result = ctx.new_txn().add(xfer_instruction).send(&[ &*ctx.payer_signer(), &sender, &unique_message_account_keypair, ]); // Print the output so it can be used in e2e tests println!("{:?}", tx_result); } TokenSubCmd::EnrollRemoteRouter(enroll) => { let enroll_instruction = HtInstruction::EnrollRemoteRouter(RemoteRouterConfig { domain: enroll.domain, router: enroll.router.into(), }); let (token_account, _token_bump) = Pubkey::find_program_address(hyperlane_token_pda_seeds!(), &enroll.program_id); let instruction = Instruction { program_id: enroll.program_id, data: enroll_instruction.encode().unwrap(), accounts: vec![ AccountMeta::new(token_account, false), AccountMeta::new_readonly(ctx.payer_pubkey, true), ], }; ctx.new_txn().add(instruction).send_with_payer(); } TokenSubCmd::TransferOwnership(transfer) => { let instruction = hyperlane_sealevel_token_lib::instruction::transfer_ownership_instruction( transfer.program_id, ctx.payer_pubkey, Some(transfer.new_owner), ) .unwrap(); ctx.new_txn() .add_with_description( instruction, format!("Transfer ownership to {}", transfer.new_owner), ) .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 { IgpType::Igp => InterchainGasPaymasterType::Igp(set_args.igp_account), IgpType::OverheadIgp => { InterchainGasPaymasterType::OverheadIgp(set_args.igp_account) } }; let instruction = hyperlane_sealevel_token_lib::instruction::set_igp_instruction( args.program_id, ctx.payer_pubkey, Some((set_args.igp_program, igp_type.clone())), ) .unwrap(); ctx.new_txn() .add_with_description( instruction, format!( "Set IGP of {} to program {}, type {:?}", args.program_id, set_args.igp_program, igp_type ), ) .send_with_payer(); } GetSetCmd::Get(get_args) => { let (token_account, _token_bump) = Pubkey::find_program_address(hyperlane_token_pda_seeds!(), &args.program_id); let token_account = ctx .client .get_account_with_commitment(&token_account, ctx.commitment) .unwrap() .value .expect( "Token account not found. Make sure you are connected to the right RPC.", ); parse_token_account_data(get_args.token_type, &mut &token_account.data[..]); } }, } } fn process_validator_announce_cmd(ctx: Context, cmd: ValidatorAnnounceCmd) { match cmd.cmd { ValidatorAnnounceSubCmd::Init(init) => { let init_instruction = hyperlane_sealevel_validator_announce::instruction::init_instruction( init.program_id, ctx.payer_pubkey, init.mailbox_id, init.local_domain, ) .unwrap(); ctx.new_txn().add(init_instruction).send_with_payer(); } ValidatorAnnounceSubCmd::Announce(announce) => { let signature = hex::decode(if announce.signature.starts_with("0x") { &announce.signature[2..] } else { &announce.signature }) .unwrap(); let announce_instruction = ValidatorAnnounceAnnounceInstruction { validator: announce.validator, storage_location: announce.storage_location, signature, }; let (validator_announce_account, _validator_announce_bump) = Pubkey::find_program_address(validator_announce_pda_seeds!(), &announce.program_id); let (validator_storage_locations_key, _validator_storage_locations_bump_seed) = Pubkey::find_program_address( validator_storage_locations_pda_seeds!(announce.validator), &announce.program_id, ); let replay_id = announce_instruction.replay_id(); let (replay_protection_pda_key, _replay_protection_bump_seed) = Pubkey::find_program_address( replay_protection_pda_seeds!(replay_id), &announce.program_id, ); let ixn = ValidatorAnnounceInstruction::Announce(announce_instruction); // Accounts: // 0. `[signer]` The payer. // 1. `[executable]` The system program. // 2. `[]` The ValidatorAnnounce PDA account. // 3. `[writeable]` The validator-specific ValidatorStorageLocationsAccount PDA account. // 4. `[writeable]` The ReplayProtection PDA account specific to the announcement being made. let accounts = vec![ AccountMeta::new_readonly(ctx.payer_pubkey, true), AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(validator_announce_account, false), AccountMeta::new(validator_storage_locations_key, false), AccountMeta::new(replay_protection_pda_key, false), ]; let announce_instruction = Instruction { program_id: announce.program_id, data: ixn.into_instruction_data().unwrap(), accounts, }; ctx.new_txn().add(announce_instruction).send_with_payer(); } ValidatorAnnounceSubCmd::Query(query) => { let (validator_storage_locations_key, _validator_storage_locations_bump_seed) = Pubkey::find_program_address( validator_storage_locations_pda_seeds!(query.validator), &query.program_id, ); let account = ctx .client .get_account_with_commitment(&validator_storage_locations_key, ctx.commitment) .unwrap() .value; if let Some(account) = account { let validator_storage_locations = ValidatorStorageLocationsAccount::fetch(&mut &account.data[..]) .unwrap() .into_inner(); println!( "Validator {} storage locations:\n{:#?}", query.validator, validator_storage_locations ); } else { println!("Validator not yet announced"); } } } } fn process_igp_cmd(ctx: Context, cmd: IgpCmd) { match cmd.cmd { IgpSubCmd::Query(query) => { let (program_data_account_pda, _program_data_account_bump) = Pubkey::find_program_address(igp_program_data_pda_seeds!(), &query.program_id); let accounts = ctx .client .get_multiple_accounts_with_commitment( &[program_data_account_pda, query.igp_account], ctx.commitment, ) .unwrap() .value; let igp_program_data = IgpProgramDataAccount::fetch(&mut &accounts[0].as_ref().unwrap().data[..]) .unwrap() .into_inner(); println!("IGP program data: {:?}", igp_program_data); let igp = IgpAccount::fetch(&mut &accounts[1].as_ref().unwrap().data[..]) .unwrap() .into_inner(); println!("IGP account: {:?}", igp); if let Some(gas_payment_account_pubkey) = query.gas_payment_account { let account = ctx .client .get_account_with_commitment(&gas_payment_account_pubkey, ctx.commitment) .unwrap() .value .unwrap(); let gas_payment_account = GasPaymentAccount::fetch(&mut &account.data[..]) .unwrap() .into_inner(); println!("Gas payment account: {:?}", gas_payment_account); } } IgpSubCmd::PayForGas(payment_details) => { let unique_gas_payment_keypair = Keypair::new(); let salt = H256::zero(); let (igp_account, _igp_account_bump) = Pubkey::find_program_address( hyperlane_sealevel_igp::igp_pda_seeds!(salt), &payment_details.program_id, ); let (overhead_igp_account, _) = Pubkey::find_program_address( hyperlane_sealevel_igp::overhead_igp_pda_seeds!(salt), &payment_details.program_id, ); let (ixn, gas_payment_data_account) = hyperlane_sealevel_igp::instruction::pay_for_gas_instruction( payment_details.program_id, ctx.payer_pubkey, igp_account, Some(overhead_igp_account), unique_gas_payment_keypair.pubkey(), H256::from_str(&payment_details.message_id).unwrap(), payment_details.destination_domain, payment_details.gas, ) .unwrap(); ctx.new_txn() .add(ixn) .send(&[&*ctx.payer_signer(), &unique_gas_payment_keypair]); println!( "Made a payment for message {} with gas payment data account {}", payment_details.message_id, gas_payment_data_account ); } IgpSubCmd::GasOracleConfig(args) => { let core_program_ids = read_core_program_ids(&args.environments_dir, &args.environment, &args.chain_name); match args.cmd { GetSetCmd::Set(set_args) => { let remote_gas_data = RemoteGasData { token_exchange_rate: set_args.token_exchange_rate, gas_price: set_args.gas_price, token_decimals: set_args.token_decimals, }; let gas_oracle_config = GasOracleConfig { domain: args.remote_domain, gas_oracle: Some(GasOracle::RemoteGasData(remote_gas_data)), }; let instruction = hyperlane_sealevel_igp::instruction::set_gas_oracle_configs_instruction( core_program_ids.igp_program_id, core_program_ids.igp_account, ctx.payer_pubkey, vec![gas_oracle_config], ) .unwrap(); ctx.new_txn().add(instruction).send_with_payer(); println!("Set gas oracle for remote domain {:?}", args.remote_domain); } GetSetCmd::Get(_) => { let igp_account = ctx .client .get_account_with_commitment(&core_program_ids.igp_account, ctx.commitment) .unwrap() .value .expect( "IGP account not found. Make sure you are connected to the right RPC.", ); let igp_account = IgpAccount::fetch(&mut &igp_account.data[..]) .unwrap() .into_inner(); println!( "IGP account gas oracle: {:#?}", igp_account.gas_oracles.get(&args.remote_domain) ); } } } IgpSubCmd::DestinationGasOverhead(args) => { let core_program_ids = read_core_program_ids(&args.environments_dir, &args.environment, &args.chain_name); match args.cmd { GasOverheadSubCmd::Get => { // Read the gas overhead config let overhead_igp_account = ctx .client .get_account_with_commitment( &core_program_ids.overhead_igp_account, ctx.commitment, ) .unwrap() .value .expect("Overhead IGP account not found. Make sure you are connected to the right RPC."); let overhead_igp_account = OverheadIgpAccount::fetch(&mut &overhead_igp_account.data[..]) .unwrap() .into_inner(); println!( "Overhead IGP account gas oracle: {:#?}", overhead_igp_account.gas_overheads.get(&args.remote_domain) ); } GasOverheadSubCmd::Set(set_args) => { let overhead_config = GasOverheadConfig { destination_domain: args.remote_domain, gas_overhead: Some(set_args.gas_overhead), }; // Set the gas overhead config let instruction = hyperlane_sealevel_igp::instruction::set_destination_gas_overheads( core_program_ids.igp_program_id, core_program_ids.overhead_igp_account, ctx.payer_pubkey, vec![overhead_config], ) .unwrap(); ctx.new_txn().add(instruction).send_with_payer(); println!( "Set gas overheads for remote domain {:?}", args.remote_domain ) } } } IgpSubCmd::TransferIgpOwnership(ref transfer_ownership) | IgpSubCmd::TransferOverheadIgpOwnership(ref transfer_ownership) => { let igp_account_type = match cmd.cmd { IgpSubCmd::TransferIgpOwnership(_) => { InterchainGasPaymasterType::Igp(transfer_ownership.igp_account) } IgpSubCmd::TransferOverheadIgpOwnership(_) => { InterchainGasPaymasterType::OverheadIgp(transfer_ownership.igp_account) } _ => unreachable!(), }; let instruction = hyperlane_sealevel_igp::instruction::transfer_igp_account_ownership_instruction( transfer_ownership.program_id, igp_account_type.clone(), ctx.payer_pubkey, Some(transfer_ownership.new_owner), ) .unwrap(); ctx.new_txn() .add_with_description( instruction, format!( "Transfer ownership of {:?} to {}", igp_account_type, transfer_ownership.new_owner ), ) .send_with_payer(); } } }