track wrapped asset meta, test CLI with acc creation

This commit is contained in:
Hendrik Hofstadt 2020-08-07 21:48:22 +02:00
parent ff0b4766ae
commit 30666fd7ff
7 changed files with 379 additions and 102 deletions

View File

@ -106,6 +106,7 @@ followed by:
| 6 | token_program | SplToken | | | | |
| 7 | token | WrappedAsset | | | opt | ✅ |
| 8 | destination | TokenAccount | | ✅ | opt | |
| 9 | wrapped_meta_account | WrappedAssetMeta | | ✅ | opt | ✅ |
##### Transfer: Ethereum (wrapped) -> Solana (native)
@ -120,8 +121,7 @@ followed by:
| Index | Name | Type | signer | writeable | empty | derived |
| ----- | ------------ | ------------------- | ------ | --------- | ----- | ------- |
| 7 | out_proposal | TransferOutProposal | | ✅ | | ✅ |
| 8 | sender | Account | ✅ | | | |
| 6 | out_proposal | TransferOutProposal | | ✅ | | ✅ |
## Accounts

View File

@ -11,7 +11,7 @@ use solana_sdk::{
};
use crate::error::Error::VAATooLong;
use crate::instruction::BridgeInstruction::Initialize;
use crate::instruction::BridgeInstruction::{Initialize, PostVAA, TransferOut};
use crate::state::{AssetMeta, Bridge, BridgeConfig};
use crate::syscalls::RawKey;
use crate::vaa::{VAABody, VAA};
@ -118,6 +118,16 @@ impl BridgeInstruction {
Initialize(*payload)
}
1 => {
let payload: &TransferOutPayload = unpack(input)?;
TransferOut(*payload)
}
2 => {
let payload: &VAAData = unpack(input)?;
PostVAA(*payload)
}
_ => return Err(ProgramError::InvalidInstructionData),
})
}
@ -310,9 +320,12 @@ pub fn post_vaa(
t.asset.chain,
t.asset.address,
)?;
let wrapped_meta_key =
Bridge::derive_wrapped_meta_id(program_id, &bridge_key, &wrapped_key)?;
accounts.push(AccountMeta::new_readonly(spl_token::id(), false));
accounts.push(AccountMeta::new(wrapped_key, false));
accounts.push(AccountMeta::new(Pubkey::new(&t.target_address), false));
accounts.push(AccountMeta::new(wrapped_meta_key, false));
}
}
}

View File

@ -133,6 +133,7 @@ impl Bridge {
accounts: &[AccountInfo],
t: &TransferOutPayload,
) -> ProgramResult {
info!("wrapped transfer out");
let account_info_iter = &mut accounts.iter();
next_account_info(account_info_iter)?; // System program
next_account_info(account_info_iter)?; // Token program
@ -145,18 +146,12 @@ impl Bridge {
let sender = Bridge::token_account_deserialize(sender_account_info)?;
let bridge = Bridge::bridge_deserialize(bridge_info)?;
let mint = Bridge::mint_deserialize(mint_info)?;
// Does the token belong to the mint
if sender.mint != *mint_info.key {
return Err(Error::TokenMintMismatch.into());
}
// Is the mint owned by the program
if mint.owner.unwrap() != *program_id {
return Err(Error::WrongMintOwner.into());
}
// Check that the mint is actually a wrapped asset belonging to *this* bridge instance
let expected_mint_address = Bridge::derive_wrapped_asset_id(
program_id,
@ -218,6 +213,7 @@ impl Bridge {
accounts: &[AccountInfo],
t: &TransferOutPayload,
) -> ProgramResult {
info!("native transfer out");
let account_info_iter = &mut accounts.iter();
next_account_info(account_info_iter)?; // System program
next_account_info(account_info_iter)?; // Token program
@ -230,18 +226,12 @@ impl Bridge {
let sender = Bridge::token_account_deserialize(sender_account_info)?;
let bridge = Bridge::bridge_deserialize(bridge_info)?;
let mint = Bridge::mint_deserialize(mint_info)?;
// Does the token belong to the mint
if sender.mint != *mint_info.key {
return Err(Error::TokenMintMismatch.into());
}
// If the mint is owned by the program, it's a wrapped asset
if mint.owner.unwrap() == *program_id {
return Err(Error::WrongMintOwner.into());
}
// Create transfer account
let transfer_seed = Bridge::derive_transfer_id_seeds(
bridge_info.key,
@ -293,7 +283,7 @@ impl Bridge {
return Err(Error::WrongTokenAccountOwner.into());
}
// Transfer tokens to custody
// Transfer tokens to custody - This also checks that custody mint = mint
Bridge::token_transfer_caller(
accounts,
&bridge.config.token_program,
@ -500,6 +490,7 @@ impl Bridge {
next_account_info(account_info_iter)?; // Token program
let mint_info = next_account_info(account_info_iter)?;
let destination_info = next_account_info(account_info_iter)?;
let wrapped_meta_info = next_account_info(account_info_iter)?;
let destination = Self::token_account_deserialize(destination_info)?;
if destination.mint != *mint_info.key {
@ -546,6 +537,25 @@ impl Bridge {
payer_info.key,
&b.asset,
)?;
// Check and create wrapped asset meta if it is unset
let wrapped_meta_seeds =
Bridge::derive_wrapped_meta_seeds(bridge_info.key, mint_info.key);
Bridge::check_and_create_account::<WrappedAssetMeta>(
program_id,
accounts,
wrapped_meta_info.key,
payer_info.key,
&wrapped_meta_seeds,
)?;
let mut wrapped_meta_data = wrapped_meta_info.data.borrow_mut();
let wrapped_meta: &mut WrappedAssetMeta =
Bridge::unpack_unchecked(&mut wrapped_meta_data)?;
wrapped_meta.is_initialized = true;
wrapped_meta.address = b.asset.address;
wrapped_meta.chain = b.asset.chain;
}
Bridge::wrapped_mint_to(
@ -735,7 +745,7 @@ impl Bridge {
payer: &Pubkey,
) -> Result<(), ProgramError> {
Self::create_account::<Mint>(
program_id,
token_program,
accounts,
mint,
payer,
@ -756,7 +766,7 @@ impl Bridge {
asset: &AssetMeta,
) -> Result<(), ProgramError> {
Self::create_account::<Mint>(
program_id,
token_program,
accounts,
mint,
payer,
@ -806,7 +816,8 @@ impl Bridge {
program_id,
);
let s: Vec<_> = seeds.iter().map(|item| item.as_slice()).collect();
invoke_signed(&ix, accounts, &[s.as_slice()])
//invoke_signed(&ix, accounts, &[s.as_slice()])
Ok(())
}
}

View File

@ -99,6 +99,25 @@ impl IsInitialized for ClaimedVAA {
}
}
/// metadata tracking for wrapped assets
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct WrappedAssetMeta {
/// chain id of the native chain of this asset
pub chain: u8,
/// address of the asset on the native chain
pub address: ForeignAddress,
/// Is `true` if this structure has been initialized.
pub is_initialized: bool,
}
impl IsInitialized for WrappedAssetMeta {
fn is_initialized(&self) -> bool {
self.is_initialized
}
}
/// Metadata about an asset
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
@ -265,6 +284,15 @@ impl Bridge {
]
}
/// Calculates derived seeds for a wrapped asset meta entry
pub fn derive_wrapped_meta_seeds<'a>(bridge: &Pubkey, mint: &Pubkey) -> Vec<Vec<u8>> {
vec![
"claim".as_bytes().to_vec(),
bridge.to_bytes().to_vec(),
mint.to_bytes().to_vec(),
]
}
/// Calculates a derived address for this program
pub fn derive_bridge_id(program_id: &Pubkey) -> Result<Pubkey, Error> {
Self::derive_key(program_id, &Self::derive_bridge_seeds())
@ -288,6 +316,15 @@ impl Bridge {
Self::derive_key(program_id, &Self::derive_claim_seeds(bridge, hash))
}
/// Calculates a derived address for a wrapped asset meta entry
pub fn derive_wrapped_meta_id(
program_id: &Pubkey,
bridge: &Pubkey,
mint: &Pubkey,
) -> Result<Pubkey, Error> {
Self::derive_key(program_id, &Self::derive_wrapped_meta_seeds(bridge, mint))
}
/// Calculates a derived address for this program
pub fn derive_guardian_set_id(
program_id: &Pubkey,

3
solana/cli/Cargo.lock generated
View File

@ -291,13 +291,14 @@ name = "cli"
version = "0.1.0"
dependencies = [
"clap",
"primitive-types",
"solana-clap-utils",
"solana-cli-config",
"solana-client",
"solana-faucet",
"solana-logger",
"solana-sdk",
"spl-token 1.0.6",
"spl-token 1.0.3",
"wormhole-bridge",
]

View File

@ -14,5 +14,6 @@ solana-logger = { version = "1.2.17" }
solana-sdk = { version = "1.2.17" }
solana-client = { version = "1.2.17" }
solana-faucet = "1.2.17"
spl-token = { version = "1.0.6" }
spl-token = { package = "spl-token", git="https://github.com/hendrikhofstadt/solana-program-library", branch="256b-primitive-types" }
wormhole-bridge = { path = "../bridge" }
primitive-types = {version ="0.7.2", default-features = false}

View File

@ -1,3 +1,4 @@
use std::fmt::Display;
use std::net::{IpAddr, SocketAddr};
use std::str::FromStr;
use std::thread::sleep;
@ -8,13 +9,16 @@ use clap::{
crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, AppSettings, Arg,
SubCommand,
};
use primitive_types::U256;
use solana_clap_utils::{
input_parsers::{keypair_of, pubkey_of},
input_validators::{is_amount, is_keypair, is_pubkey_or_keypair, is_url},
};
use solana_client::rpc_client::RpcClient;
use solana_client::rpc_request::TokenAccountsFilter;
use solana_faucet::faucet::request_airdrop_transaction;
use solana_sdk::hash::Hash;
use solana_sdk::system_instruction::create_account;
use solana_sdk::{
native_token::*,
pubkey::Pubkey,
@ -28,8 +32,13 @@ use spl_token::{
state::{Account, Mint},
};
use spl_bridge::instruction::initialize;
use spl_bridge::state::BridgeConfig;
use spl_bridge::instruction::{
initialize, post_vaa, transfer_out, BridgeInstruction, ForeignAddress, TransferOutPayload,
VAAData, CHAIN_ID_SOLANA,
};
use spl_bridge::state::{
AssetMeta, Bridge, BridgeConfig, ClaimedVAA, GuardianSet, TransferOutProposal,
};
use spl_bridge::syscalls::RawKey;
struct Config {
@ -41,50 +50,6 @@ struct Config {
type Error = Box<dyn std::error::Error>;
type CommmandResult = Result<Option<Transaction>, Error>;
fn requestAirdrop(config: &Config, request_sol: u64) -> CommmandResult {
let (blockhash, _fee_calculator) = config.rpc_client.get_recent_blockhash()?;
let faucet_addr = SocketAddr::new(IpAddr::from_str("127.0.0.1").unwrap(), 9900);
match {
let mut retries = 5;
loop {
let result = FaucetKeypair::new_keypair(
&faucet_addr,
&config.owner.pubkey(),
LAMPORTS_PER_SOL * request_sol,
blockhash,
);
if result.is_ok() || retries == 0 {
break result;
}
retries -= 1;
sleep(Duration::from_secs(1));
}
} {
Ok(kp) => Ok(Some(kp.airdrop_transaction())),
Err(e) => Err(e),
}
}
struct FaucetKeypair {
transaction: Transaction,
}
impl FaucetKeypair {
fn new_keypair(
faucet_addr: &SocketAddr,
to_pubkey: &Pubkey,
lamports: u64,
blockhash: Hash,
) -> Result<Self, Error> {
let transaction = request_airdrop_transaction(faucet_addr, to_pubkey, lamports, blockhash)?;
Ok(Self { transaction })
}
fn airdrop_transaction(&self) -> Transaction {
self.transaction.clone()
}
}
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 {
@ -115,28 +80,6 @@ fn check_owner_balance(config: &Config, required_balance: u64) -> Result<(), Err
}
}
fn command_request_airdrop(config: &Config) -> CommmandResult {
let token = Keypair::new();
println!("Requesting airdrop");
let minimum_balance_for_rent_exemption = config
.rpc_client
.get_minimum_balance_for_rent_exemption(size_of::<Mint>())?;
let mut transaction: Transaction = requestAirdrop(config, 20)?.unwrap();
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, &token],
recent_blockhash,
);
Ok(Some(transaction))
}
fn command_deploy_bridge(config: &Config) -> CommmandResult {
println!("Deploying bridge program");
@ -144,20 +87,134 @@ fn command_deploy_bridge(config: &Config) -> CommmandResult {
.rpc_client
.get_minimum_balance_for_rent_exemption(size_of::<Mint>())?;
let p = Pubkey::from_str("7AeSppn3AjaeYScZsnRf1ZRQvtyo4Ke5gx7PAJ3r7BFp").unwrap();
let ix = initialize(
&Pubkey::from_str("5x4kJ1G4UgJc3yNsznZpxEAB2vrnJirSBxZDu1uwaqnZ").unwrap(),
&p,
&config.owner.pubkey(),
RawKey {
x: [8; 32],
y: [2; 32],
y_parity: true,
},
&BridgeConfig {
vaa_expiration_time: 200000000,
token_program: spl_token::id(),
},
)?;
println!("bridge: {}, ", ix.accounts[0].pubkey.to_string());
println!("bridge: {}, ", ix.accounts[2].pubkey.to_string());
println!("payer: {}, ", ix.accounts[3].pubkey.to_string());
let mut ix_c = create_account(
&config.owner.pubkey(),
&ix.accounts[2].pubkey,
100000000,
size_of::<Bridge>() as u64,
&p,
);
ix_c.accounts[1].is_signer = false;
let mut ix_c2 = create_account(
&config.owner.pubkey(),
&ix.accounts[3].pubkey,
100000000,
size_of::<GuardianSet>() as u64,
&p,
);
ix_c2.accounts[1].is_signer = false;
let mut transaction =
Transaction::new_with_payer(&[ix_c, ix_c2, 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_lock_tokens(
config: &Config,
account: Pubkey,
token: Pubkey,
amount: u64,
to_chain: u8,
target: ForeignAddress,
nonce: u32,
) -> CommmandResult {
println!("Initiating transfer to foreign chain");
let minimum_balance_for_rent_exemption = config
.rpc_client
.get_minimum_balance_for_rent_exemption(size_of::<Mint>())?;
let p = Pubkey::from_str("7AeSppn3AjaeYScZsnRf1ZRQvtyo4Ke5gx7PAJ3r7BFp").unwrap();
let ix = transfer_out(
&p,
&config.owner.pubkey(),
&account,
&token,
&TransferOutPayload {
amount: U256::from(amount),
chain_id: to_chain,
asset: AssetMeta {
address: token.to_bytes(), // TODO fetch from WASSET (if WASSET)
chain: CHAIN_ID_SOLANA, //TODO fetch from WASSET (if WASSET)
},
target,
nonce,
},
)?;
println!("custody: {}, ", ix.accounts[7].pubkey.to_string());
// Approve tokens
let mut ix_a = approve(
&spl_token::id(),
&account,
&ix.accounts[3].pubkey,
&config.owner.pubkey(),
&[],
U256::from(amount),
);
// TODO remove create calls
let mut ix_c = create_account(
&config.owner.pubkey(),
&ix.accounts[4].pubkey,
100000000,
size_of::<TransferOutProposal>() as u64,
&p,
);
ix_c.accounts[1].is_signer = false;
let mut ix_c2 = create_account(
&config.owner.pubkey(),
&ix.accounts[7].pubkey,
100000000,
size_of::<Account>() as u64,
&spl_token::id(),
);
ix_c2.accounts[1].is_signer = false;
let mut transaction =
Transaction::new_with_payer(&[ix_a, ix_c, ix_c2, 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_submit_vaa(config: &Config, vaa: &[u8]) -> CommmandResult {
println!("Submitting VAA");
let minimum_balance_for_rent_exemption = config
.rpc_client
.get_minimum_balance_for_rent_exemption(size_of::<Mint>())?;
let p = Pubkey::from_str("7AeSppn3AjaeYScZsnRf1ZRQvtyo4Ke5gx7PAJ3r7BFp").unwrap();
let ix = post_vaa(&p, &config.owner.pubkey(), vaa)?;
let mut transaction = Transaction::new_with_payer(&[ix], Some(&config.fee_payer.pubkey()));
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
@ -191,7 +248,7 @@ fn command_create_token(config: &Config) -> CommmandResult {
&token.pubkey(),
None,
Some(&config.owner.pubkey()),
0,
U256::from(0),
9, // hard code 9 decimal places to match the sol/lamports relationship
)?,
],
@ -293,7 +350,37 @@ fn command_transfer(
&recipient,
&config.owner.pubkey(),
&[],
amount,
U256::from(amount),
)?],
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 command_approve(
config: &Config,
sender: Pubkey,
ui_amount: f64,
recipient: Pubkey,
) -> CommmandResult {
println!(
"Approve {} tokens\n Sender: {}\n Recipient: {}",
ui_amount, sender, recipient
);
let amount = sol_to_lamports(ui_amount);
let mut transaction = Transaction::new_with_payer(
&[approve(
&spl_token::id(),
&sender,
&recipient,
&config.owner.pubkey(),
&[],
U256::from(amount),
)?],
Some(&config.fee_payer.pubkey()),
);
@ -314,7 +401,7 @@ fn command_burn(config: &Config, source: Pubkey, ui_amount: f64) -> CommmandResu
&source,
&config.owner.pubkey(),
&[],
amount,
U256::from(amount),
)?],
Some(&config.fee_payer.pubkey()),
);
@ -344,7 +431,7 @@ fn command_mint(
&recipient,
&config.owner.pubkey(),
&[],
amount,
U256::from(amount),
)?],
Some(&config.fee_payer.pubkey()),
);
@ -414,8 +501,9 @@ fn command_unwrap(config: &Config, address: Pubkey) -> CommmandResult {
Ok(Some(transaction))
}
fn command_balance(_config: &Config, address: Pubkey) -> CommmandResult {
println!("balance {}", address);
fn command_balance(config: &Config, address: Pubkey) -> CommmandResult {
let balance = config.rpc_client.get_token_account_balance(&address)?;
println!("{}", balance);
Ok(None)
}
@ -424,8 +512,28 @@ fn command_supply(_config: &Config, address: Pubkey) -> CommmandResult {
Ok(None)
}
fn command_accounts(_config: &Config, token: Option<Pubkey>) -> CommmandResult {
println!("accounts {:?}", token);
fn command_accounts(config: &Config, token: Option<Pubkey>) -> CommmandResult {
let accounts = config.rpc_client.get_token_accounts_by_owner(
&config.owner.pubkey(),
match token {
Some(token) => TokenAccountsFilter::Mint(token),
None => TokenAccountsFilter::ProgramId(spl_token::id()),
},
)?;
if accounts.is_empty() {
println!("None");
}
println!("Account Token Balance");
println!("-------------------------------------------------------------------------------------------------");
for (address, account) in accounts {
let balance = match config.rpc_client.get_token_account_balance(&address) {
Ok(response) => response,
Err(err) => 0,
};
println!("{:<44} {:<44} {}", address, account.lamports, balance);
}
Ok(None)
}
@ -482,7 +590,6 @@ fn main() {
)
.subcommand(SubCommand::with_name("create-token").about("Create a new token"))
.subcommand(SubCommand::with_name("create-bridge").about("Create a new bridge"))
.subcommand(SubCommand::with_name("airdrop").about("Request an airdrop"))
.subcommand(
SubCommand::with_name("create-account")
.about("Create a new token account")
@ -549,6 +656,86 @@ fn main() {
.help("The token account address of recipient"),
),
)
.subcommand(
SubCommand::with_name("approve")
.about("Approve token sprending")
.arg(
Arg::with_name("sender")
.validator(is_pubkey_or_keypair)
.value_name("SENDER_TOKEN_ACCOUNT_ADDRESS")
.takes_value(true)
.index(1)
.required(true)
.help("The token account address of the sender"),
)
.arg(
Arg::with_name("amount")
.validator(is_amount)
.value_name("TOKEN_AMOUNT")
.takes_value(true)
.index(2)
.required(true)
.help("Amount to send, in tokens"),
)
.arg(
Arg::with_name("recipient")
.validator(is_pubkey_or_keypair)
.value_name("RECIPIENT_TOKEN_ACCOUNT_ADDRESS")
.takes_value(true)
.index(3)
.required(true)
.help("The token account address of recipient"),
),
)
.subcommand(
SubCommand::with_name("lock")
.about("Transfer tokens to another chain")
.arg(
Arg::with_name("sender")
.validator(is_pubkey_or_keypair)
.value_name("SENDER_TOKEN_ACCOUNT_ADDRESS")
.takes_value(true)
.index(1)
.required(true)
.help("The token account address of the sender"),
)
.arg(
Arg::with_name("token")
.validator(is_pubkey_or_keypair)
.value_name("TOKEN_ADDRESS")
.takes_value(true)
.index(2)
.required(true)
.help("The mint address"),
)
.arg(
Arg::with_name("amount")
.validator(is_amount)
.value_name("AMOUNT")
.takes_value(true)
.index(3)
.required(true)
.help("Amount to transfer out"),
)
.arg(
Arg::with_name("chain")
.validator(is_u8)
.value_name("CHAIN")
.takes_value(true)
.index(4)
.required(true)
.help("Chain to transfer to"),
)
.arg(
Arg::with_name("nonce")
.validator(is_u8)
.value_name("NONCE")
.takes_value(true)
.index(5)
.required(true)
.help("Nonce of the transfer"),
),
)
.subcommand(
SubCommand::with_name("burn")
.about("Burn tokens from an account")
@ -697,9 +884,16 @@ fn main() {
solana_logger::setup_with_default("solana=info");
let _ = match matches.subcommand() {
("airdrop", Some(_arg_matches)) => command_request_airdrop(&config),
("create-token", Some(_arg_matches)) => command_create_token(&config),
("create-bridge", Some(_arg_matches)) => command_deploy_bridge(&config),
("lock", Some(arg_matches)) => {
let account = pubkey_of(arg_matches, "sender").unwrap();
let amount = value_t_or_exit!(arg_matches, "amount", u64);
let nonce = value_t_or_exit!(arg_matches, "nonce", u32);
let chain = value_t_or_exit!(arg_matches, "chain", u8);
let token = pubkey_of(arg_matches, "token").unwrap();
command_lock_tokens(&config, account, token, amount, chain, [0; 32], nonce)
}
("create-account", Some(arg_matches)) => {
let token = pubkey_of(arg_matches, "token").unwrap();
command_create_account(&config, token)
@ -715,6 +909,12 @@ fn main() {
let recipient = pubkey_of(arg_matches, "recipient").unwrap();
command_transfer(&config, sender, amount, recipient)
}
("approve", Some(arg_matches)) => {
let sender = pubkey_of(arg_matches, "sender").unwrap();
let amount = value_t_or_exit!(arg_matches, "amount", f64);
let recipient = pubkey_of(arg_matches, "recipient").unwrap();
command_approve(&config, sender, amount, recipient)
}
("burn", Some(arg_matches)) => {
let source = pubkey_of(arg_matches, "source").unwrap();
let amount = value_t_or_exit!(arg_matches, "amount", f64);
@ -765,3 +965,17 @@ fn main() {
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
))
}
}