437 lines
13 KiB
Rust
437 lines
13 KiB
Rust
#![feature(const_generics)]
|
|
#![allow(warnings)]
|
|
|
|
use std::{
|
|
fmt::Display,
|
|
mem::size_of,
|
|
process::exit,
|
|
};
|
|
|
|
use borsh::BorshDeserialize;
|
|
use bridge::{
|
|
accounts::{
|
|
Bridge,
|
|
FeeCollector,
|
|
},
|
|
types::BridgeData,
|
|
};
|
|
use clap::{
|
|
crate_description,
|
|
crate_name,
|
|
crate_version,
|
|
value_t,
|
|
App,
|
|
AppSettings,
|
|
Arg,
|
|
SubCommand,
|
|
};
|
|
use hex;
|
|
use solana_clap_utils::{
|
|
input_parsers::{
|
|
keypair_of,
|
|
pubkey_of,
|
|
value_of,
|
|
},
|
|
input_validators::{
|
|
is_keypair,
|
|
is_pubkey_or_keypair,
|
|
is_url,
|
|
},
|
|
};
|
|
use solana_client::{
|
|
rpc_client::RpcClient,
|
|
rpc_config::RpcSendTransactionConfig,
|
|
};
|
|
use solana_sdk::{
|
|
commitment_config::{
|
|
CommitmentConfig,
|
|
CommitmentLevel,
|
|
},
|
|
native_token::*,
|
|
program_error::ProgramError::AccountAlreadyInitialized,
|
|
pubkey::Pubkey,
|
|
signature::{
|
|
read_keypair_file,
|
|
Keypair,
|
|
Signer,
|
|
},
|
|
system_instruction::transfer,
|
|
transaction::Transaction,
|
|
};
|
|
use solitaire::{
|
|
processors::seeded::Seeded,
|
|
AccountState,
|
|
};
|
|
|
|
struct Config {
|
|
rpc_client: RpcClient,
|
|
owner: Keypair,
|
|
fee_payer: Keypair,
|
|
commitment_config: CommitmentConfig,
|
|
}
|
|
|
|
type Error = Box<dyn std::error::Error>;
|
|
type CommmandResult = Result<Option<Transaction>, Error>;
|
|
|
|
fn command_deploy_bridge(
|
|
config: &Config,
|
|
bridge: &Pubkey,
|
|
initial_guardians: Vec<[u8; 20]>,
|
|
guardian_expiration: u32,
|
|
message_fee: u64,
|
|
) -> CommmandResult {
|
|
println!("Initializing Wormhole bridge {}", bridge);
|
|
|
|
let minimum_balance_for_rent_exemption = config
|
|
.rpc_client
|
|
.get_minimum_balance_for_rent_exemption(size_of::<BridgeData>())?;
|
|
|
|
let ix = bridge::instructions::initialize(
|
|
*bridge,
|
|
config.owner.pubkey(),
|
|
message_fee,
|
|
guardian_expiration,
|
|
initial_guardians.as_slice(),
|
|
)
|
|
.unwrap();
|
|
println!("config account: {}, ", ix.accounts[0].pubkey.to_string());
|
|
let mut transaction = Transaction::new_with_payer(&[ix], Some(&config.fee_payer.pubkey()));
|
|
|
|
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
|
|
check_fee_payer_balance(
|
|
config,
|
|
minimum_balance_for_rent_exemption + fee_calculator.calculate_fee(&transaction.message()),
|
|
)?;
|
|
transaction.sign(&[&config.fee_payer, &config.owner], recent_blockhash);
|
|
Ok(Some(transaction))
|
|
}
|
|
|
|
fn command_post_message(
|
|
config: &Config,
|
|
bridge: &Pubkey,
|
|
nonce: u32,
|
|
payload: Vec<u8>,
|
|
persist: bool,
|
|
) -> CommmandResult {
|
|
println!("Posting a message to the wormhole");
|
|
|
|
// Fetch the message fee
|
|
let bridge_config_account = config
|
|
.rpc_client
|
|
.get_account(&Bridge::<'_, { AccountState::Initialized }>::key(
|
|
None, bridge,
|
|
))?;
|
|
let bridge_config = BridgeData::try_from_slice(bridge_config_account.data.as_slice())?;
|
|
println!("Message fee: {} lamports", bridge_config.config.fee);
|
|
|
|
let transfer_ix = transfer(
|
|
&config.owner.pubkey(),
|
|
&FeeCollector::key(None, bridge),
|
|
bridge_config.config.fee,
|
|
);
|
|
let (_, ix) = bridge::instructions::post_message(
|
|
*bridge,
|
|
config.owner.pubkey(),
|
|
config.fee_payer.pubkey(),
|
|
nonce,
|
|
payload,
|
|
persist,
|
|
)
|
|
.unwrap();
|
|
let mut transaction =
|
|
Transaction::new_with_payer(&[transfer_ix, ix], Some(&config.fee_payer.pubkey()));
|
|
|
|
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
|
|
check_fee_payer_balance(config, fee_calculator.calculate_fee(&transaction.message()))?;
|
|
transaction.sign(&[&config.fee_payer, &config.owner], recent_blockhash);
|
|
Ok(Some(transaction))
|
|
}
|
|
|
|
fn main() {
|
|
let matches = App::new(crate_name!())
|
|
.about(crate_description!())
|
|
.version(crate_version!())
|
|
.setting(AppSettings::SubcommandRequiredElseHelp)
|
|
.arg({
|
|
let arg = Arg::with_name("config_file")
|
|
.short("C")
|
|
.long("config")
|
|
.value_name("PATH")
|
|
.takes_value(true)
|
|
.global(true)
|
|
.help("Configuration file to use");
|
|
if let Some(ref config_file) = *solana_cli_config::CONFIG_FILE {
|
|
arg.default_value(&config_file)
|
|
} else {
|
|
arg
|
|
}
|
|
})
|
|
.arg(
|
|
Arg::with_name("json_rpc_url")
|
|
.long("url")
|
|
.value_name("URL")
|
|
.takes_value(true)
|
|
.validator(is_url)
|
|
.help("JSON RPC URL for the cluster. Default from the configuration file."),
|
|
)
|
|
.arg(
|
|
Arg::with_name("owner")
|
|
.long("owner")
|
|
.value_name("KEYPAIR")
|
|
.validator(is_keypair)
|
|
.takes_value(true)
|
|
.help(
|
|
"Specify the contract payer account. \
|
|
This may be a keypair file, the ASK keyword. \
|
|
Defaults to the client keypair.",
|
|
),
|
|
)
|
|
.arg(
|
|
Arg::with_name("fee_payer")
|
|
.long("fee-payer")
|
|
.value_name("KEYPAIR")
|
|
.validator(is_keypair)
|
|
.takes_value(true)
|
|
.help(
|
|
"Specify the fee-payer account. \
|
|
This may be a keypair file, the ASK keyword. \
|
|
Defaults to the client keypair.",
|
|
),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("create-bridge")
|
|
.about("Create a new bridge")
|
|
.arg(
|
|
Arg::with_name("bridge")
|
|
.long("bridge")
|
|
.value_name("BRIDGE_KEY")
|
|
.validator(is_pubkey_or_keypair)
|
|
.takes_value(true)
|
|
.index(1)
|
|
.required(true)
|
|
.help("Specify the bridge program address"),
|
|
)
|
|
.arg(
|
|
Arg::with_name("guardian")
|
|
.validator(is_hex)
|
|
.value_name("GUARDIAN_ADDRESS")
|
|
.takes_value(true)
|
|
.index(2)
|
|
.required(true)
|
|
.help("Address of the initial guardian"),
|
|
)
|
|
.arg(
|
|
Arg::with_name("guardian_set_expiration")
|
|
.validator(is_u32)
|
|
.value_name("GUARDIAN_SET_EXPIRATION")
|
|
.takes_value(true)
|
|
.index(3)
|
|
.required(true)
|
|
.help("Time in seconds after which a guardian set expires after an update"),
|
|
)
|
|
.arg(
|
|
Arg::with_name("message_fee")
|
|
.validator(is_u64)
|
|
.value_name("MESSAGE_FEE")
|
|
.takes_value(true)
|
|
.index(4)
|
|
.required(true)
|
|
.help("Initial message posting fee"),
|
|
),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("post-message")
|
|
.about("Post a message via Wormhole")
|
|
.arg(
|
|
Arg::with_name("bridge")
|
|
.long("bridge")
|
|
.value_name("BRIDGE_KEY")
|
|
.validator(is_pubkey_or_keypair)
|
|
.takes_value(true)
|
|
.index(1)
|
|
.required(true)
|
|
.help("Specify the bridge program address"),
|
|
)
|
|
.arg(
|
|
Arg::with_name("nonce")
|
|
.validator(is_u32)
|
|
.value_name("NONCE")
|
|
.takes_value(true)
|
|
.index(2)
|
|
.required(true)
|
|
.help("Nonce of the message"),
|
|
)
|
|
.arg(
|
|
Arg::with_name("data")
|
|
.validator(is_hex)
|
|
.value_name("DATA")
|
|
.takes_value(true)
|
|
.index(3)
|
|
.required(true)
|
|
.help("Payload of the message"),
|
|
)
|
|
.arg(
|
|
Arg::with_name("persist")
|
|
.short("p")
|
|
.long("persist")
|
|
.takes_value(false)
|
|
.help("Indicates that the VAA should be persisted on-chain"),
|
|
),
|
|
)
|
|
.get_matches();
|
|
|
|
let config = {
|
|
let cli_config = if let Some(config_file) = matches.value_of("config_file") {
|
|
solana_cli_config::Config::load(config_file).unwrap_or_default()
|
|
} else {
|
|
solana_cli_config::Config::default()
|
|
};
|
|
let json_rpc_url = value_t!(matches, "json_rpc_url", String)
|
|
.unwrap_or_else(|_| cli_config.json_rpc_url.clone());
|
|
|
|
let client_keypair = || {
|
|
read_keypair_file(&cli_config.keypair_path).unwrap_or_else(|err| {
|
|
eprintln!("Unable to read {}: {}", cli_config.keypair_path, err);
|
|
exit(1)
|
|
})
|
|
};
|
|
|
|
let owner = keypair_of(&matches, "owner").unwrap_or_else(client_keypair);
|
|
let fee_payer = keypair_of(&matches, "fee_payer").unwrap_or_else(client_keypair);
|
|
|
|
Config {
|
|
rpc_client: RpcClient::new(json_rpc_url),
|
|
owner,
|
|
fee_payer,
|
|
commitment_config: CommitmentConfig::processed(),
|
|
}
|
|
};
|
|
|
|
let _ = match matches.subcommand() {
|
|
("create-bridge", Some(arg_matches)) => {
|
|
let bridge = pubkey_of(arg_matches, "bridge").unwrap();
|
|
let initial_guardian: String = value_of(arg_matches, "guardian").unwrap();
|
|
let initial_data = hex::decode(initial_guardian).unwrap();
|
|
let guardian_expiration: u32 =
|
|
value_of(arg_matches, "guardian_set_expiration").unwrap();
|
|
let msg_fee: u64 = value_of(arg_matches, "message_fee").unwrap();
|
|
|
|
let mut guardian = [0u8; 20];
|
|
guardian.copy_from_slice(&initial_data);
|
|
command_deploy_bridge(
|
|
&config,
|
|
&bridge,
|
|
vec![guardian],
|
|
guardian_expiration,
|
|
msg_fee,
|
|
)
|
|
}
|
|
("post-message", Some(arg_matches)) => {
|
|
let bridge = pubkey_of(arg_matches, "bridge").unwrap();
|
|
let data_str: String = value_of(arg_matches, "data").unwrap();
|
|
let data = hex::decode(data_str).unwrap();
|
|
let nonce: u32 = value_of(arg_matches, "nonce").unwrap();
|
|
let persist = arg_matches.is_present("persist");
|
|
|
|
command_post_message(&config, &bridge, nonce, data, persist)
|
|
}
|
|
|
|
_ => unreachable!(),
|
|
}
|
|
.and_then(|transaction| {
|
|
if let Some(transaction) = transaction {
|
|
let signature = config
|
|
.rpc_client
|
|
.send_and_confirm_transaction_with_spinner_and_config(
|
|
&transaction,
|
|
config.commitment_config,
|
|
RpcSendTransactionConfig {
|
|
skip_preflight: true,
|
|
preflight_commitment: None,
|
|
encoding: None,
|
|
},
|
|
)?;
|
|
println!("Signature: {}", signature);
|
|
}
|
|
Ok(())
|
|
})
|
|
.map_err(|err| {
|
|
eprintln!("{}", err);
|
|
exit(1);
|
|
});
|
|
}
|
|
|
|
pub fn is_u8<T>(amount: T) -> Result<(), String>
|
|
where
|
|
T: AsRef<str> + Display,
|
|
{
|
|
if amount.as_ref().parse::<u8>().is_ok() {
|
|
Ok(())
|
|
} else {
|
|
Err(format!(
|
|
"Unable to parse input amount as integer, provided: {}",
|
|
amount
|
|
))
|
|
}
|
|
}
|
|
|
|
pub fn is_u32<T>(amount: T) -> Result<(), String>
|
|
where
|
|
T: AsRef<str> + Display,
|
|
{
|
|
if amount.as_ref().parse::<u32>().is_ok() {
|
|
Ok(())
|
|
} else {
|
|
Err(format!(
|
|
"Unable to parse input amount as integer, provided: {}",
|
|
amount
|
|
))
|
|
}
|
|
}
|
|
|
|
pub fn is_u64<T>(amount: T) -> Result<(), String>
|
|
where
|
|
T: AsRef<str> + Display,
|
|
{
|
|
if amount.as_ref().parse::<u64>().is_ok() {
|
|
Ok(())
|
|
} else {
|
|
Err(format!(
|
|
"Unable to parse input amount as integer, provided: {}",
|
|
amount
|
|
))
|
|
}
|
|
}
|
|
|
|
pub fn is_hex<T>(value: T) -> Result<(), String>
|
|
where
|
|
T: AsRef<str> + Display,
|
|
{
|
|
hex::decode(value.to_string())
|
|
.map(|_| ())
|
|
.map_err(|e| format!("{}", e))
|
|
}
|
|
|
|
fn check_fee_payer_balance(config: &Config, required_balance: u64) -> Result<(), Error> {
|
|
let balance = config
|
|
.rpc_client
|
|
.get_balance_with_commitment(
|
|
&config.fee_payer.pubkey(),
|
|
CommitmentConfig {
|
|
commitment: CommitmentLevel::Processed,
|
|
},
|
|
)?
|
|
.value;
|
|
if balance < required_balance {
|
|
Err(format!(
|
|
"Fee payer, {}, has insufficient balance: {} required, {} available",
|
|
config.fee_payer.pubkey(),
|
|
lamports_to_sol(required_balance),
|
|
lamports_to_sol(balance)
|
|
)
|
|
.into())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|