Fix bridge fee and implement CLI

Change-Id: Ib17b335e05359fd4baf614d0b4eaae459814b04d
This commit is contained in:
Hendrik Hofstadt 2021-06-23 16:49:25 +02:00
parent d677311d70
commit ece0de4bef
8 changed files with 450 additions and 198 deletions

View File

@ -1,4 +1,5 @@
target
bin
cli/target
agent/target
bridge/target
solitaire/target
modules/token_bridge/target

View File

@ -34,7 +34,7 @@ dependencies = [
"bridge",
"bs58",
"byteorder",
"clap 2.33.3",
"clap",
"futures 0.3.15",
"hex",
"libc",
@ -463,44 +463,12 @@ dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim 0.8.0",
"textwrap 0.11.0",
"strsim",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "clap"
version = "3.0.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"indexmap",
"lazy_static",
"os_str_bytes",
"strsim 0.10.0",
"termcolor",
"textwrap 0.12.1",
"unicode-width",
"vec_map",
]
[[package]]
name = "clap_derive"
version = "3.0.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2 1.0.27",
"quote 1.0.9",
"syn 1.0.73",
]
[[package]]
name = "client"
version = "0.1.0"
@ -508,9 +476,12 @@ dependencies = [
"anyhow",
"borsh",
"bridge",
"clap 3.0.0-beta.2",
"clap",
"hex",
"rand 0.7.3",
"shellexpand",
"solana-clap-utils",
"solana-cli-config",
"solana-client",
"solana-program",
"solana-sdk",
@ -2024,12 +1995,6 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "os_str_bytes"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
[[package]]
name = "ouroboros"
version = "0.5.1"
@ -2282,30 +2247,6 @@ dependencies = [
"toml",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2 1.0.27",
"quote 1.0.9",
"syn 1.0.73",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2 1.0.27",
"quote 1.0.9",
"version_check",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.19"
@ -3026,7 +2967,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "484288242b2b175bf2b7554497318e39b23ee921989f976387dbe48e60b2f256"
dependencies = [
"chrono",
"clap 2.33.3",
"clap",
"rpassword",
"solana-remote-wallet",
"solana-sdk",
@ -3059,7 +3000,7 @@ dependencies = [
"base64 0.13.0",
"bincode",
"bs58",
"clap 2.33.3",
"clap",
"indicatif",
"jsonrpc-core",
"log",
@ -3132,7 +3073,7 @@ checksum = "ed5e6adf551ca4e761c3395bb684ba5d907a051007a6dbf2d57cb99d2691e031"
dependencies = [
"bincode",
"byteorder",
"clap 2.33.3",
"clap",
"log",
"serde",
"serde_derive",
@ -3222,7 +3163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa68e25fb6452b85733cf5c301988b56fd2d5d5a8e93c75cf38cbec06efc2eae"
dependencies = [
"bincode",
"clap 2.33.3",
"clap",
"log",
"nix",
"rand 0.7.3",
@ -3591,12 +3532,6 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "subtle"
version = "1.0.0"
@ -3711,15 +3646,6 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "textwrap"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789"
dependencies = [
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.25"

View File

@ -7,12 +7,15 @@ edition = "2018"
[dependencies]
anyhow = "1.0.40"
borsh = "0.8.1"
bridge = {path = "../program", features = ["no-idl", "no-entrypoint", "client"]}
clap = "3.0.0-beta.2"
bridge = { path = "../program", features = ["no-idl", "no-entrypoint", "client"] }
clap = "2.33.0"
rand = "0.7.3"
shellexpand = "2.1.0"
solana-client = "=1.7.0"
solana-program = "=1.7.0"
solana-sdk = "=1.7.0"
solana-cli-config = "=1.7.0"
solitaire = { path = "../../solitaire/program" }
solitaire-client = {path = "../../solitaire/client" }
solitaire-client = { path = "../../solitaire/client" }
solana-clap-utils = "=1.7.0"
hex = "0.4.3"

View File

@ -1,104 +1,414 @@
use borsh::BorshSerialize;
use bridge::{
api,
types,
#![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 clap::Clap;
use solana_client::{
rpc_client::RpcClient,
rpc_config::RpcSendTransactionConfig,
};
use solana_program::pubkey::Pubkey;
use solana_sdk::{
commitment_config::CommitmentConfig,
native_token::*,
program_error::ProgramError::AccountAlreadyInitialized,
pubkey::Pubkey,
signature::{
read_keypair_file,
Signer as SolSigner,
Keypair,
Signer,
},
system_instruction::transfer,
transaction::Transaction,
};
use solitaire_client::{
AccEntry,
ToInstruction,
};
use bridge::accounts::{
GuardianSet,
GuardianSetDerivationData,
};
use solitaire::{
processors::seeded::Seeded,
AccountState,
};
use std::error;
#[derive(Clap)]
pub struct Opts {
#[clap(long)]
bridge_address: Pubkey,
struct Config {
rpc_client: RpcClient,
owner: Keypair,
fee_payer: Keypair,
commitment_config: CommitmentConfig,
}
pub type ErrBox = Box<dyn error::Error>;
type Error = Box<dyn std::error::Error>;
type CommmandResult = Result<Option<Transaction>, Error>;
pub const DEFAULT_MESSAGE_FEE: u64 = 42;
pub const DEFAULT_GUARDIAN_SET_EXPIRATION_TIME: u32 = 42;
fn command_deploy_bridge(
config: &Config,
bridge: &Pubkey,
_initial_guardian: Vec<[u8; 20]>,
guardian_expiration: u32,
message_fee: u64,
) -> CommmandResult {
println!("Initializing Wormhole bridge {}", bridge);
fn main() -> Result<(), ErrBox> {
let opts = Opts::parse();
let minimum_balance_for_rent_exemption = config
.rpc_client
.get_minimum_balance_for_rent_exemption(size_of::<BridgeData>())?;
let payer = read_keypair_file(&*shellexpand::tilde("~/.config/solana/id.json"))
.expect("Example requires a keypair file");
let ix = bridge::client_instructions::initialize(
*bridge,
config.owner.pubkey(),
message_fee,
guardian_expiration,
)
.unwrap();
println!("config account: {}, ", ix.accounts[0].pubkey.to_string());
let mut transaction = Transaction::new_with_payer(&[ix], Some(&config.fee_payer.pubkey()));
// Keypair is not Clone
let payer_for_tx = read_keypair_file(&*shellexpand::tilde("~/.config/solana/id.json"))
.expect("Example requires a keypair file");
let url = "http://localhost:8899".to_owned();
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))
}
let client = RpcClient::new(url);
fn command_post_message(
config: &Config,
bridge: &Pubkey,
nonce: u32,
payload: Vec<u8>,
) -> CommmandResult {
println!("Posting a message to the wormhole");
let program_id = opts.bridge_address;
// 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);
use AccEntry::*;
let init = api::InitializeAccounts {
bridge: Derived(program_id.clone()),
guardian_set: Unprivileged(<GuardianSet<'_, { AccountState::Uninitialized }>>::key(
&GuardianSetDerivationData { index: 0 },
&program_id,
)),
payer: Signer(payer),
let transfer_ix = transfer(
&config.owner.pubkey(),
&FeeCollector::key(None, bridge),
bridge_config.config.fee,
);
let ix = bridge::client_instructions::post_message(
*bridge,
config.owner.pubkey(),
config.fee_payer.pubkey(),
nonce,
payload,
)
.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"),
),
)
.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 init_args = bridge::instruction::Instruction::Initialize(types::BridgeConfig {
guardian_set_expiration_time: DEFAULT_GUARDIAN_SET_EXPIRATION_TIME,
fee: DEFAULT_MESSAGE_FEE,
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();
command_post_message(&config, &bridge, nonce, data)
}
_ => 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);
});
let ix_data = init_args.try_to_vec()?;
let (ix, signers) = init.to_ix(program_id, ix_data.as_slice())?;
let (recent_blockhash, _) = client.get_recent_blockhash()?;
println!("Instruction ready.");
println!(
"Signing for {} signer(s): {:?}",
signers.len(),
signers.iter().map(|s| s.pubkey()).collect::<Vec<_>>()
);
let mut tx = Transaction::new_with_payer(&[ix], Some(&payer_for_tx.pubkey()));
tx.try_sign(&signers.iter().collect::<Vec<_>>(), recent_blockhash)?;
println!("Transaction signed.");
let signature = client.send_and_confirm_transaction_with_spinner_and_config(
&tx,
CommitmentConfig::processed(),
RpcSendTransactionConfig {
skip_preflight: true,
preflight_commitment: None,
encoding: None,
},
)?;
println!("Signature: {}", signature);
Ok(())
}
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(&config.fee_payer.pubkey())?;
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(())
}
}

View File

@ -1,6 +1,7 @@
use crate::{
accounts::{
Bridge,
FeeCollector,
GuardianSet,
GuardianSetDerivationData,
},
@ -17,6 +18,7 @@ type Payer<'a> = Signer<Info<'a>>;
pub struct Initialize<'b> {
pub bridge: Bridge<'b, { AccountState::Uninitialized }>,
pub guardian_set: GuardianSet<'b, { AccountState::Uninitialized }>,
pub fee_collector: FeeCollector<'b>,
pub payer: Payer<'b>,
}
@ -49,5 +51,15 @@ pub fn initialize(
accs.bridge.guardian_set_index = 0;
accs.bridge.config = config;
// Initialize the fee collector account so it's rent exempt and will keep funds
accs.fee_collector.create(
ctx,
accs.payer.key,
Exempt,
0,
&solana_program::system_program::id(),
)?;
accs.bridge.last_lamports = accs.fee_collector.lamports();
Ok(())
}

View File

@ -89,13 +89,12 @@ pub fn post_message(
.verify_derivation(ctx.program_id, &msg_derivation)?;
// Fee handling
let fee = transfer_fee();
if accs
.fee_collector
.lamports()
.checked_sub(accs.bridge.last_lamports)
.ok_or(MathOverflow)?
< fee
< accs.bridge.config.fee
{
return Err(InsufficientFees.into());
}
@ -124,7 +123,3 @@ pub fn post_message(
Ok(())
}
pub fn transfer_fee() -> u64 {
500
}

View File

@ -1,6 +1,4 @@
use borsh::BorshSerialize;
use solitaire::processors::seeded::Seeded;
use solitaire::AccountState;
use solana_program::{
borsh::try_from_slice_unchecked,
hash,
@ -17,6 +15,10 @@ use solana_program::{
system_program,
sysvar,
};
use solitaire::{
processors::seeded::Seeded,
AccountState,
};
use crate::{
accounts::{
@ -41,25 +43,22 @@ use crate::{
pub fn initialize(
program_id: Pubkey,
payer: Pubkey,
bridge: Pubkey,
guardian_set_index: u32,
guardian_set: Pubkey,
fee: u64,
guardian_set_expiration_time: u32,
) -> solitaire::Result<Instruction> {
let bridge = Bridge::<'_, { AccountState::Uninitialized }>::key(None, &program_id);
let guardian_set = GuardianSet::<'_, { AccountState::Uninitialized }>::key(
&GuardianSetDerivationData {
index: guardian_set_index,
},
&GuardianSetDerivationData { index: 0 },
&program_id,
);
let fee_collector = FeeCollector::key(None, &program_id);
Ok(Instruction {
program_id,
accounts: vec![
AccountMeta::new(bridge, false),
AccountMeta::new(guardian_set, false),
AccountMeta::new(fee_collector, false),
AccountMeta::new(payer, true),
AccountMeta::new_readonly(sysvar::rent::id(), false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
@ -75,22 +74,27 @@ pub fn initialize(
pub fn post_message(
program_id: Pubkey,
payer: Pubkey,
bridge: Pubkey,
emitter: Pubkey,
message: PostedMessage,
sequence: u64,
nonce: u32,
payload: Vec<u8>,
) -> solitaire::Result<Instruction> {
let bridge = Bridge::<'_, { AccountState::Uninitialized }>::key(None, &program_id);
let fee_collector = FeeCollector::<'_>::key(None, &program_id);
let sequence = Sequence::<'_>::key(&SequenceDerivationData {
emitter_key: &emitter,
}, &program_id);
let message = Message::<'_, { AccountState::Uninitialized }>::key(&MessageDerivationData {
emitter_key: emitter.to_bytes(),
emitter_chain: message.emitter_chain,
nonce: message.nonce,
payload: message.payload.clone(),
}, &program_id);
let sequence = Sequence::<'_>::key(
&SequenceDerivationData {
emitter_key: &emitter,
},
&program_id,
);
let message = Message::<'_, { AccountState::Uninitialized }>::key(
&MessageDerivationData {
emitter_key: emitter.to_bytes(),
emitter_chain: 1,
nonce,
payload: payload.clone(),
},
&program_id,
);
Ok(Instruction {
program_id,
@ -108,10 +112,10 @@ pub fn post_message(
],
data: crate::instruction::Instruction::PostMessage(PostMessageData {
nonce: 0,
payload: vec![],
nonce,
payload: payload.clone(),
})
.try_to_vec()?
.try_to_vec()?,
})
}
@ -155,7 +159,7 @@ pub fn verify_signatures(
signers,
initial_creation: true,
})
.try_to_vec()?
.try_to_vec()?,
})
}
@ -206,7 +210,7 @@ pub fn post_vaa(
],
data: crate::instruction::Instruction::PostVAA(vaa)
.try_to_vec()
.unwrap(),
.try_to_vec()
.unwrap(),
}
}

View File

@ -8,6 +8,7 @@ use solana_program::msg;
// package as soon as possible.
pub mod accounts;
pub mod api;
pub mod client_instructions;
pub mod types;
pub mod vaa;
@ -25,14 +26,14 @@ pub use api::{
PostMessageData,
PostVAA,
PostVAAData,
Signature,
UninitializedMessage,
UpgradeContract,
UpgradeContractData,
UpgradeGuardianSet,
UpgradeGuardianSetData,
VerifySignatures,
VerifySignaturesData,
UninitializedMessage,
Signature,
};
use types::BridgeConfig;