spl-token command-line utility
This commit is contained in:
parent
735eafb7e0
commit
2dbe6d1129
|
@ -27,6 +27,15 @@ updates:
|
|||
labels:
|
||||
- "automerge"
|
||||
open-pull-requests-limit: 3
|
||||
- package-ecosystem: cargo
|
||||
directory: "/token-cli"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "01:00"
|
||||
timezone: America/Los_Angeles
|
||||
labels:
|
||||
- "automerge"
|
||||
open-pull-requests-limit: 3
|
||||
- package-ecosystem: cargo
|
||||
directory: "/memo"
|
||||
schedule:
|
||||
|
@ -53,5 +62,5 @@ updates:
|
|||
timezone: America/Los_Angeles
|
||||
labels:
|
||||
- "automerge"
|
||||
open-pull-requests-limit: 3
|
||||
|
||||
open-pull-requests-limit: 3
|
||||
|
||||
|
|
|
@ -16,6 +16,10 @@ git diff --exit-code token/inc/token.h
|
|||
./do.sh test token
|
||||
cc token/inc/token.h -o token/target/token.gch
|
||||
|
||||
# Test cli
|
||||
./do.sh fmt token-cli --all -- --check
|
||||
./do.sh clippy token-cli -- --deny=warnings
|
||||
|
||||
# Test js bindings
|
||||
cd "$(dirname "$0")/../token/js"
|
||||
npm install
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,36 @@
|
|||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
description = "SPL-Token Command-line Utility"
|
||||
edition = "2018"
|
||||
homepage = "https://spl.solana.com/token"
|
||||
license = "Apache-2.0"
|
||||
name = "spl-token-cli"
|
||||
repository = "https://github.com/solana-labs/solana-program-library"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.0"
|
||||
solana-clap-utils = { version = "1.3.0"}
|
||||
solana-cli-config = { version = "1.3.0" }
|
||||
solana-client = { version = "1.3.0" }
|
||||
solana-logger = { version = "1.3.0" }
|
||||
solana-sdk = { version = "1.3.0" }
|
||||
spl-token = { package = "spl-token", version = "1.0", path="../token" }
|
||||
|
||||
# Workflow for developing against local Solana crates
|
||||
#
|
||||
# 0. Uncomment the below patches
|
||||
# 1. Replace PATH_TO_SOLANA with your local filestem path to the Solana mono-repo
|
||||
# 2. Run `cargo update`
|
||||
# 3. Use `cargo` normally
|
||||
#
|
||||
[patch.crates-io]
|
||||
#solana-clap-utils = {path = "PATH_TO_SOLANA/clap-utils" }
|
||||
#solana-cli-config = {path = "PATH_TO_SOLANA/cli-config" }
|
||||
#solana-client = { path = "PATH_TO_SOLANA/client"}
|
||||
#solana-logger = {path = "PATH_TO_SOLANA/logger" }
|
||||
#solana-sdk = { path = "PATH_TO_SOLANA/sdk" }
|
||||
|
||||
[[bin]]
|
||||
name = "spl-token"
|
||||
path = "src/main.rs"
|
|
@ -0,0 +1,801 @@
|
|||
use clap::{
|
||||
crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, AppSettings, Arg,
|
||||
SubCommand,
|
||||
};
|
||||
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, rpc_request::TokenAccountsFilter};
|
||||
use solana_sdk::{
|
||||
commitment_config::CommitmentConfig,
|
||||
native_token::*,
|
||||
pubkey::Pubkey,
|
||||
signature::{read_keypair_file, Keypair, Signer},
|
||||
system_instruction,
|
||||
transaction::Transaction,
|
||||
};
|
||||
use spl_token::{
|
||||
self,
|
||||
instruction::*,
|
||||
native_mint,
|
||||
state::{Account, Mint},
|
||||
};
|
||||
use std::{mem::size_of, process::exit};
|
||||
|
||||
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 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(())
|
||||
}
|
||||
}
|
||||
|
||||
fn check_owner_balance(config: &Config, required_balance: u64) -> Result<(), Error> {
|
||||
let balance = config.rpc_client.get_balance(&config.owner.pubkey())?;
|
||||
if balance < required_balance {
|
||||
Err(format!(
|
||||
"Owner, {}, has insufficient balance: {} required, {} available",
|
||||
config.owner.pubkey(),
|
||||
lamports_to_sol(required_balance),
|
||||
lamports_to_sol(balance)
|
||||
)
|
||||
.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_decimals_for_token(config: &Config, token: &Pubkey) -> Result<u8, Error> {
|
||||
if *token == native_mint::id() {
|
||||
Ok(native_mint::DECIMALS)
|
||||
} else {
|
||||
let mint = config
|
||||
.rpc_client
|
||||
.get_token_mint_with_commitment(token, config.commitment_config)?
|
||||
.value
|
||||
.ok_or_else(|| format!("Invalid token: {}", token))?;
|
||||
Ok(mint.decimals)
|
||||
}
|
||||
}
|
||||
|
||||
fn command_create_token(config: &Config, decimals: u8) -> CommmandResult {
|
||||
let token = Keypair::new();
|
||||
println!("Creating token {}", token.pubkey());
|
||||
|
||||
let minimum_balance_for_rent_exemption = config
|
||||
.rpc_client
|
||||
.get_minimum_balance_for_rent_exemption(size_of::<Mint>())?;
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
system_instruction::create_account(
|
||||
&config.fee_payer.pubkey(),
|
||||
&token.pubkey(),
|
||||
minimum_balance_for_rent_exemption,
|
||||
size_of::<Mint>() as u64,
|
||||
&spl_token::id(),
|
||||
),
|
||||
initialize_mint(
|
||||
&spl_token::id(),
|
||||
&token.pubkey(),
|
||||
None,
|
||||
Some(&config.owner.pubkey()),
|
||||
0,
|
||||
decimals,
|
||||
)?,
|
||||
],
|
||||
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, &token],
|
||||
recent_blockhash,
|
||||
);
|
||||
Ok(Some(transaction))
|
||||
}
|
||||
|
||||
fn command_create_account(config: &Config, token: Pubkey) -> CommmandResult {
|
||||
let account = Keypair::new();
|
||||
println!("Creating account {}", account.pubkey());
|
||||
|
||||
let minimum_balance_for_rent_exemption = config
|
||||
.rpc_client
|
||||
.get_minimum_balance_for_rent_exemption(size_of::<Account>())?;
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
system_instruction::create_account(
|
||||
&config.fee_payer.pubkey(),
|
||||
&account.pubkey(),
|
||||
minimum_balance_for_rent_exemption,
|
||||
size_of::<Account>() as u64,
|
||||
&spl_token::id(),
|
||||
),
|
||||
initialize_account(
|
||||
&spl_token::id(),
|
||||
&account.pubkey(),
|
||||
&token,
|
||||
&config.owner.pubkey(),
|
||||
)?,
|
||||
],
|
||||
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, &account],
|
||||
recent_blockhash,
|
||||
);
|
||||
Ok(Some(transaction))
|
||||
}
|
||||
|
||||
fn command_assign(config: &Config, account: Pubkey, new_owner: Pubkey) -> CommmandResult {
|
||||
println!(
|
||||
"Assigning {}\n Current owner: {}\n New owner: {}",
|
||||
account,
|
||||
config.owner.pubkey(),
|
||||
new_owner
|
||||
);
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[set_owner(
|
||||
&spl_token::id(),
|
||||
&account,
|
||||
&new_owner,
|
||||
&config.owner.pubkey(),
|
||||
&[],
|
||||
)?],
|
||||
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_transfer(
|
||||
config: &Config,
|
||||
sender: Pubkey,
|
||||
ui_amount: f64,
|
||||
recipient: Pubkey,
|
||||
) -> CommmandResult {
|
||||
println!(
|
||||
"Transfer {} tokens\n Sender: {}\n Recipient: {}",
|
||||
ui_amount, sender, recipient
|
||||
);
|
||||
|
||||
let sender_token_account = config
|
||||
.rpc_client
|
||||
.get_token_account_with_commitment(&sender, config.commitment_config)?
|
||||
.value;
|
||||
let recipient_token_account = config
|
||||
.rpc_client
|
||||
.get_token_account_with_commitment(&recipient, config.commitment_config)?
|
||||
.value;
|
||||
|
||||
let decimals = match (sender_token_account, recipient_token_account) {
|
||||
(Some(sender_token_account), Some(recipient_token_account)) => {
|
||||
if sender_token_account.mint != recipient_token_account.mint {
|
||||
eprintln!("Error: token mismatch between sender and recipient");
|
||||
exit(1)
|
||||
}
|
||||
get_decimals_for_token(config, &sender_token_account.mint.parse::<Pubkey>()?)?
|
||||
}
|
||||
(None, _) => {
|
||||
eprintln!(
|
||||
"Error: sender account is invalid or does not exist: {}",
|
||||
sender
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
(Some(_), None) => {
|
||||
eprintln!(
|
||||
"Error: recipient account is invalid or does not exist: {}",
|
||||
recipient
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
let amount = spl_token::ui_amount_to_amount(ui_amount, decimals);
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[transfer(
|
||||
&spl_token::id(),
|
||||
&sender,
|
||||
&recipient,
|
||||
&config.owner.pubkey(),
|
||||
&[],
|
||||
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_burn(config: &Config, source: Pubkey, ui_amount: f64) -> CommmandResult {
|
||||
println!("Burn {} tokens\n Source: {}", ui_amount, source);
|
||||
|
||||
let source_token_account = config
|
||||
.rpc_client
|
||||
.get_token_account_with_commitment(&source, config.commitment_config)?
|
||||
.value;
|
||||
|
||||
let decimals = match source_token_account {
|
||||
Some(source_token_account) => {
|
||||
get_decimals_for_token(config, &source_token_account.mint.parse::<Pubkey>()?)?
|
||||
}
|
||||
None => {
|
||||
eprintln!(
|
||||
"Error: burn account is invalid or does not exist: {}",
|
||||
source
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
let amount = spl_token::ui_amount_to_amount(ui_amount, decimals);
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[burn(
|
||||
&spl_token::id(),
|
||||
&source,
|
||||
&config.owner.pubkey(),
|
||||
&[],
|
||||
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_mint(
|
||||
config: &Config,
|
||||
token: Pubkey,
|
||||
ui_amount: f64,
|
||||
recipient: Pubkey,
|
||||
) -> CommmandResult {
|
||||
println!(
|
||||
"Minting {} tokens\n Token: {}\n Recipient: {}",
|
||||
ui_amount, token, recipient
|
||||
);
|
||||
|
||||
let recipient_token_account = config
|
||||
.rpc_client
|
||||
.get_token_account_with_commitment(&recipient, config.commitment_config)?
|
||||
.value;
|
||||
|
||||
let decimals = match recipient_token_account {
|
||||
Some(recipient_token_account) => {
|
||||
get_decimals_for_token(config, &recipient_token_account.mint.parse::<Pubkey>()?)?
|
||||
}
|
||||
None => {
|
||||
eprintln!(
|
||||
"Error: recipient account is invalid or does not exist: {}",
|
||||
recipient
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
let amount = spl_token::ui_amount_to_amount(ui_amount, decimals);
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[mint_to(
|
||||
&spl_token::id(),
|
||||
&token,
|
||||
&recipient,
|
||||
&config.owner.pubkey(),
|
||||
&[],
|
||||
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_wrap(config: &Config, sol: f64) -> CommmandResult {
|
||||
let account = Keypair::new();
|
||||
let lamports = sol_to_lamports(sol);
|
||||
println!("Wrapping {} SOL into {}", sol, account.pubkey());
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
system_instruction::create_account(
|
||||
&config.owner.pubkey(),
|
||||
&account.pubkey(),
|
||||
lamports,
|
||||
size_of::<Account>() as u64,
|
||||
&spl_token::id(),
|
||||
),
|
||||
initialize_account(
|
||||
&spl_token::id(),
|
||||
&account.pubkey(),
|
||||
&native_mint::id(),
|
||||
&config.owner.pubkey(),
|
||||
)?,
|
||||
],
|
||||
Some(&config.fee_payer.pubkey()),
|
||||
);
|
||||
|
||||
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
|
||||
check_owner_balance(config, lamports)?;
|
||||
check_fee_payer_balance(config, fee_calculator.calculate_fee(&transaction.message()))?;
|
||||
transaction.sign(
|
||||
&[&config.fee_payer, &config.owner, &account],
|
||||
recent_blockhash,
|
||||
);
|
||||
Ok(Some(transaction))
|
||||
}
|
||||
|
||||
fn command_unwrap(config: &Config, address: Pubkey) -> CommmandResult {
|
||||
println!("Unwrapping {}", address);
|
||||
println!(
|
||||
" Amount: {} SOL\n Recipient: {}",
|
||||
lamports_to_sol(
|
||||
config
|
||||
.rpc_client
|
||||
.get_balance_with_commitment(&address, config.commitment_config)?
|
||||
.value
|
||||
),
|
||||
config.owner.pubkey()
|
||||
);
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[close_account(
|
||||
&spl_token::id(),
|
||||
&address,
|
||||
&config.owner.pubkey(),
|
||||
&config.owner.pubkey(),
|
||||
&[],
|
||||
)?],
|
||||
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_balance(config: &Config, address: Pubkey) -> CommmandResult {
|
||||
let balance = config
|
||||
.rpc_client
|
||||
.get_token_account_balance_with_commitment(&address, config.commitment_config)?
|
||||
.value;
|
||||
println!("{}", balance.ui_amount);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn command_supply(config: &Config, address: Pubkey) -> CommmandResult {
|
||||
let supply = config
|
||||
.rpc_client
|
||||
.get_token_supply_with_commitment(&address, config.commitment_config)?
|
||||
.value;
|
||||
|
||||
println!("{}", supply.ui_amount);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn command_accounts(config: &Config, token: Option<Pubkey>) -> CommmandResult {
|
||||
let accounts = config
|
||||
.rpc_client
|
||||
.get_token_accounts_by_owner_with_commitment(
|
||||
&config.owner.pubkey(),
|
||||
match token {
|
||||
Some(token) => TokenAccountsFilter::Mint(token),
|
||||
None => TokenAccountsFilter::ProgramId(spl_token::id()),
|
||||
},
|
||||
config.commitment_config,
|
||||
)?
|
||||
.value;
|
||||
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_with_commitment(&address, config.commitment_config)
|
||||
{
|
||||
Ok(response) => response.value.ui_amount.to_string(),
|
||||
Err(err) => format!("{}", err),
|
||||
};
|
||||
|
||||
println!("{:<44} {:<44} {}", address, account.mint, balance);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let default_decimals = &format!("{}", native_mint::DECIMALS);
|
||||
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 token owner 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-token").about("Create a new token")
|
||||
.arg(
|
||||
Arg::with_name("decimals")
|
||||
.long("decimals")
|
||||
.validator(|s| {
|
||||
s.parse::<u8>().map_err(|e| format!("{}", e))?;
|
||||
Ok(())
|
||||
})
|
||||
.value_name("DECIMALS")
|
||||
.takes_value(true)
|
||||
.default_value(&default_decimals)
|
||||
.help("Number of base 10 digits to the right of the decimal place"),
|
||||
)
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("create-account")
|
||||
.about("Create a new token account")
|
||||
.arg(
|
||||
Arg::with_name("token")
|
||||
.validator(is_pubkey_or_keypair)
|
||||
.value_name("TOKEN_ADDRESS")
|
||||
.takes_value(true)
|
||||
.index(1)
|
||||
.required(true)
|
||||
.help("The token that the account will hold"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("assign")
|
||||
.about("Assign a token or token account to a new owner")
|
||||
.arg(
|
||||
Arg::with_name("address")
|
||||
.validator(is_pubkey_or_keypair)
|
||||
.value_name("TOKEN_ADDRESS")
|
||||
.takes_value(true)
|
||||
.index(1)
|
||||
.required(true)
|
||||
.help("The address of the token account"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("new_owner")
|
||||
.validator(is_pubkey_or_keypair)
|
||||
.value_name("OWNER_ADDRESS")
|
||||
.takes_value(true)
|
||||
.index(2)
|
||||
.required(true)
|
||||
.help("The address of the new owner"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("transfer")
|
||||
.about("Transfer tokens between accounts")
|
||||
.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("burn")
|
||||
.about("Burn tokens from an account")
|
||||
.arg(
|
||||
Arg::with_name("source")
|
||||
.validator(is_pubkey_or_keypair)
|
||||
.value_name("SOURCE_TOKEN_ACCOUNT_ADDRESS")
|
||||
.takes_value(true)
|
||||
.index(1)
|
||||
.required(true)
|
||||
.help("The token account address to burn from"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("amount")
|
||||
.validator(is_amount)
|
||||
.value_name("TOKEN_AMOUNT")
|
||||
.takes_value(true)
|
||||
.index(2)
|
||||
.required(true)
|
||||
.help("Amount to burn, in tokens"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("mint")
|
||||
.about("Mint new tokens")
|
||||
.arg(
|
||||
Arg::with_name("token")
|
||||
.validator(is_pubkey_or_keypair)
|
||||
.value_name("TOKEN_ADDRESS")
|
||||
.takes_value(true)
|
||||
.index(1)
|
||||
.required(true)
|
||||
.help("The token to mint"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("amount")
|
||||
.validator(is_amount)
|
||||
.value_name("TOKEN_AMOUNT")
|
||||
.takes_value(true)
|
||||
.index(2)
|
||||
.required(true)
|
||||
.help("Amount to mint, 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("balance")
|
||||
.about("Get token account balance")
|
||||
.arg(
|
||||
Arg::with_name("address")
|
||||
.validator(is_pubkey_or_keypair)
|
||||
.value_name("TOKEN_ACCOUNT_ADDRESS")
|
||||
.takes_value(true)
|
||||
.index(1)
|
||||
.required(true)
|
||||
.help("The token account address"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("supply")
|
||||
.about("Get token supply")
|
||||
.arg(
|
||||
Arg::with_name("address")
|
||||
.validator(is_pubkey_or_keypair)
|
||||
.value_name("TOKEN_ADDRESS")
|
||||
.takes_value(true)
|
||||
.index(1)
|
||||
.required(true)
|
||||
.help("The token address"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("accounts")
|
||||
.about("List all token accounts by owner")
|
||||
.arg(
|
||||
Arg::with_name("token")
|
||||
.validator(is_pubkey_or_keypair)
|
||||
.value_name("TOKEN_ADDRESS")
|
||||
.takes_value(true)
|
||||
.index(1)
|
||||
.help("Limit results to the given token. [Default: list accounts for all tokens]"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("wrap")
|
||||
.about("Wrap native SOL in a SOL token account")
|
||||
.arg(
|
||||
Arg::with_name("amount")
|
||||
.validator(is_amount)
|
||||
.value_name("AMOUNT")
|
||||
.takes_value(true)
|
||||
.index(1)
|
||||
.required(true)
|
||||
.help("Amount of SOL to wrap"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("unwrap")
|
||||
.about("Unwrap a SOL token account")
|
||||
.arg(
|
||||
Arg::with_name("address")
|
||||
.validator(is_pubkey_or_keypair)
|
||||
.value_name("TOKEN_ACCOUNT_ADDRESS")
|
||||
.takes_value(true)
|
||||
.index(1)
|
||||
.required(true)
|
||||
.help("The address of the token account to unwrap"),
|
||||
),
|
||||
)
|
||||
.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::single(),
|
||||
}
|
||||
};
|
||||
|
||||
solana_logger::setup_with_default("solana=info");
|
||||
|
||||
let _ = match matches.subcommand() {
|
||||
("create-token", Some(arg_matches)) => {
|
||||
let decimals = value_t_or_exit!(arg_matches, "decimals", u8);
|
||||
command_create_token(&config, decimals)
|
||||
}
|
||||
("create-account", Some(arg_matches)) => {
|
||||
let token = pubkey_of(arg_matches, "token").unwrap();
|
||||
command_create_account(&config, token)
|
||||
}
|
||||
("assign", Some(arg_matches)) => {
|
||||
let address = pubkey_of(arg_matches, "address").unwrap();
|
||||
let new_owner = pubkey_of(arg_matches, "new_owner").unwrap();
|
||||
command_assign(&config, address, new_owner)
|
||||
}
|
||||
("transfer", 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_transfer(&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);
|
||||
command_burn(&config, source, amount)
|
||||
}
|
||||
("mint", Some(arg_matches)) => {
|
||||
let token = pubkey_of(arg_matches, "token").unwrap();
|
||||
let amount = value_t_or_exit!(arg_matches, "amount", f64);
|
||||
let recipient = pubkey_of(arg_matches, "recipient").unwrap();
|
||||
command_mint(&config, token, amount, recipient)
|
||||
}
|
||||
("wrap", Some(arg_matches)) => {
|
||||
let amount = value_t_or_exit!(arg_matches, "amount", f64);
|
||||
command_wrap(&config, amount)
|
||||
}
|
||||
("unwrap", Some(arg_matches)) => {
|
||||
let address = pubkey_of(arg_matches, "address").unwrap();
|
||||
command_unwrap(&config, address)
|
||||
}
|
||||
("balance", Some(arg_matches)) => {
|
||||
let address = pubkey_of(arg_matches, "address").unwrap();
|
||||
command_balance(&config, address)
|
||||
}
|
||||
("supply", Some(arg_matches)) => {
|
||||
let address = pubkey_of(arg_matches, "address").unwrap();
|
||||
command_supply(&config, address)
|
||||
}
|
||||
("accounts", Some(arg_matches)) => {
|
||||
let token = pubkey_of(arg_matches, "token");
|
||||
command_accounts(&config, token)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
.and_then(|transaction| {
|
||||
if let Some(transaction) = transaction {
|
||||
// TODO: Upgrade to solana-client 1.3 and
|
||||
// `send_and_confirm_transaction_with_spinner_and_commitment()` with single
|
||||
// confirmation by default for better UX
|
||||
let signature = config
|
||||
.rpc_client
|
||||
.send_and_confirm_transaction_with_spinner_and_commitment(
|
||||
&transaction,
|
||||
config.commitment_config,
|
||||
)?;
|
||||
println!("Signature: {}", signature);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|err| {
|
||||
eprintln!("{}", err);
|
||||
exit(1);
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue