The home for Hyperlane core contracts, sdk packages, and other infrastructure
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
hyperlane-monorepo/rust/sealevel/client/src/main.rs

1514 lines
53 KiB

//! 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<String>,
#[arg(long, short)]
keypair: Option<String>,
#[arg(long, short = 'b', default_value_t = MAX_COMPUTE_UNIT_LIMIT)]
compute_budget: u32,
#[arg(long, short = 'a')]
heap_size: Option<u32>,
#[arg(long, short = 'C')]
config: Option<String>,
#[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<u64>,
}
#[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<PathBuf>,
#[arg(long)]
overhead_config_file: Option<PathBuf>,
#[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<u32>,
#[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<Pubkey>,
}
#[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<GetIgpArgs, SetIgpArgs>,
}
#[derive(Subcommand)]
enum GetSetCmd<G: Args, S: Args> {
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<Pubkey>,
}
#[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<GetGasOracleArgs, SetGasOracleArgs>,
}
#[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<Vec<u32>>,
}
#[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<H160>,
#[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::<CollateralPlugin>::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();
}
}
}