v1.4: Distribute spl tokens (#13688)

* Add helpers to covert between sdk types

* Add distribute-spl-tokens to args and arg-parsing

* Build spl-token transfer-checked instructions

* Check spl-token balances properly

* Add display handling to support spl-token

* Small refactor to allow failures in allocation iter

* Use Associated Token Account for spl-token distributions

* Add spl token support to balances command

* Update readme

* Add spl-token tests

* Rename spl-tokens file

* Move a couple more things out of commands

* Stop requiring lockup_date heading for non-stake distributions

* Adjust solana_rbpf log level up in coverage

* Use epsilon for allocation retention
This commit is contained in:
Tyera Eulberg
2020-11-19 10:32:27 -07:00
committed by GitHub
parent 7e5b75fa7e
commit e7e7cbe632
15 changed files with 1321 additions and 84 deletions

19
Cargo.lock generated
View File

@ -4945,6 +4945,7 @@ dependencies = [
"indicatif",
"pickledb",
"serde",
"solana-account-decoder",
"solana-banks-client",
"solana-banks-server",
"solana-clap-utils",
@ -4952,11 +4953,15 @@ dependencies = [
"solana-client",
"solana-core",
"solana-logger 1.4.10",
"solana-program-test",
"solana-remote-wallet",
"solana-runtime",
"solana-sdk",
"solana-stake-program",
"solana-transaction-status",
"solana-version",
"spl-associated-token-account",
"spl-token",
"tempfile",
"thiserror",
"tokio 0.3.2",
@ -5109,9 +5114,9 @@ dependencies = [
[[package]]
name = "solana_rbpf"
version = "0.1.32"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a95dbe2b00920ac4e1524b7442cf5319f01e8fa5742930ac60148882fd7738b"
checksum = "14a45ec96d6902676708f52d180229ea3933df93eadb3e96e356377d467831b6"
dependencies = [
"byteorder",
"combine",
@ -5131,6 +5136,16 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "spl-associated-token-account"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41a25d15fe67b755f95c575ce074e6e39c809fea86b2edb1bf2ae8b0473d5a1d"
dependencies = [
"solana-program 1.4.4",
"spl-token",
]
[[package]]
name = "spl-memo"
version = "2.0.0"

View File

@ -23,6 +23,16 @@ pub fn spl_token_v2_0_native_mint() -> Pubkey {
Pubkey::from_str(&spl_token_v2_0::native_mint::id().to_string()).unwrap()
}
// A helper function to convert a solana_sdk::pubkey::Pubkey to spl_sdk::pubkey::Pubkey
pub fn spl_token_v2_0_pubkey(pubkey: &Pubkey) -> SplTokenPubkey {
SplTokenPubkey::from_str(&pubkey.to_string()).unwrap()
}
// A helper function to convert a spl_sdk::pubkey::Pubkey to solana_sdk::pubkey::Pubkey
pub fn pubkey_from_spl_token_v2_0(pubkey: &SplTokenPubkey) -> Pubkey {
Pubkey::from_str(&pubkey.to_string()).unwrap()
}
pub fn parse_token(
data: &[u8],
mint_decimals: Option<u8>,

View File

@ -53,7 +53,7 @@ if [[ -n $CI || -z $1 ]]; then
fi
RUST_LOG=solana=trace _ "$cargo" nightly test --target-dir target/cov --no-run "${packages[@]}"
if RUST_LOG=solana=trace _ "$cargo" nightly test --target-dir target/cov "${packages[@]}" 2> target/cov/coverage-stderr.log; then
if RUST_LOG=solana=trace,solana_rbpf::vm=debug _ "$cargo" nightly test --target-dir target/cov "${packages[@]}" 2> target/cov/coverage-stderr.log; then
test_status=0
else
test_status=$?

View File

@ -18,6 +18,7 @@ indexmap = "1.5.1"
indicatif = "0.15.0"
pickledb = "0.4.1"
serde = { version = "1.0", features = ["derive"] }
solana-account-decoder = { path = "../account-decoder", version = "1.4.10" }
solana-banks-client = { path = "../banks-client", version = "1.4.10" }
solana-clap-utils = { path = "../clap-utils", version = "1.4.10" }
solana-cli-config = { path = "../cli-config", version = "1.4.10" }
@ -26,7 +27,10 @@ solana-remote-wallet = { path = "../remote-wallet", version = "1.4.10" }
solana-runtime = { path = "../runtime", version = "1.4.10" }
solana-sdk = { path = "../sdk", version = "1.4.10" }
solana-stake-program = { path = "../programs/stake", version = "1.4.10" }
solana-transaction-status = { path = "../transaction-status", version = "1.4.10" }
solana-version = { path = "../version", version = "1.4.10" }
spl-associated-token-account-v1-0 = { package = "spl-associated-token-account", version = "=1.0.1" }
spl-token-v2-0 = { package = "spl-token", version = "=3.0.0", features = ["no-entrypoint"] }
tempfile = "3.1.0"
thiserror = "1.0"
tokio = { version = "0.3", features = ["full"] }
@ -37,4 +41,5 @@ bincode = "1.3.1"
solana-banks-server = { path = "../banks-server", version = "1.4.10" }
solana-core = { path = "../core", version = "1.4.10" }
solana-logger = { path = "../logger", version = "1.4.10" }
solana-program-test = { path = "../program-test", version = "1.4.10" }
solana-runtime = { path = "../runtime", version = "1.4.10" }

View File

@ -38,7 +38,7 @@ solana-tokens distribute-tokens --from <KEYPAIR> --input-csv <RECIPIENTS_CSV> --
Example output:
```text
Recipient Expected Balance (◎)
Recipient Expected Balance
3ihfUy1n9gaqihM5bJCiTAGLgWc5zo3DqVUS6T736NLM 42
UKUcTXgbeTYh65RaVV5gSf6xBHevqHvAXMo3e8Q6np8k 43
```
@ -77,7 +77,7 @@ recipient,amount,lockup_date
Example output:
```text
Recipient Expected Balance (◎)
Recipient Expected Balance
6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv 10
7aHDubg5FBYj1SgmyBgU3ZJdtfuqYCQsJQK2pTR5JUqr 42
```
@ -102,7 +102,7 @@ solana-tokens distribute-tokens --transfer-amount 10 --from <KEYPAIR> --input-cs
Example output:
```text
Recipient Expected Balance (◎)
Recipient Expected Balance
6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv 10
7aHDubg5FBYj1SgmyBgU3ZJdtfuqYCQsJQK2pTR5JUqr 10
CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT 10
@ -125,3 +125,115 @@ recipient address. That SOL can be used to pay transaction fees on staking
operations such as delegating stake. The rest of the allocation is put in
a stake account. The new stake account address is output in the transaction
log.
## Distribute SPL tokens
Distributing SPL Tokens works very similarly to distributing SOL, but requires
the `--owner` parameter to sign transactions. Each recipient account must be an
system account that will own an Associated Token Account for the SPL Token mint.
The Associated Token Account will be created, and funded by the fee_payer, if it
does not already exist.
Send SPL tokens to the recipients in `<RECIPIENTS_CSV>`.
Example recipients.csv:
```text
recipient,amount
CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT,75.4
C56nwrDVFpPrqwGYsTgQxv1ZraTh81H14PV4RHvZe36s,10
7aHDubg5FBYj1SgmyBgU3ZJdtfuqYCQsJQK2pTR5JUqr,42.1
7qQPmVAQxEQ5djPDCtiEUrxaPf8wKtLG1m6SB1brejJ1,20
```
You can check the status of the recipients before beginning a distribution. You
must include the SPL Token mint address:
```bash
solana-tokens spl-token-balances --mint <ADDRESS> --input-csv <RECIPIENTS_CSV>
```
Example output:
```text
Token: JDte736XZ1jGUtfAS32DLpBUWBR7WGSHy1hSZ36VRQ5V
Recipient Expected Balance Actual Balance Difference
CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT 75.40 0.00 -75.40
C56nwrDVFpPrqwGYsTgQxv1ZraTh81H14PV4RHvZe36s 10.000 Associated token account not yet created
7aHDubg5FBYj1SgmyBgU3ZJdtfuqYCQsJQK2pTR5JUqr 42.10 0.00 -42.10
7qQPmVAQxEQ5djPDCtiEUrxaPf8wKtLG1m6SB1brejJ1 20.000 Associated token account not yet created
```
To run the distribution:
```bash
solana-tokens distribute-spl-tokens --from <ADDRESS> --owner <KEYPAIR> \
--input-csv <RECIPIENTS_CSV> --fee-payer <KEYPAIR>
```
Example output:
```text
Total in input_csv: 147.5 tokens
Distributed: 0 tokens
Undistributed: 147.5 tokens
Total: 147.5 tokens
Recipient Expected Balance
CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT 75.400
C56nwrDVFpPrqwGYsTgQxv1ZraTh81H14PV4RHvZe36s 10.000
7aHDubg5FBYj1SgmyBgU3ZJdtfuqYCQsJQK2pTR5JUqr 42.100
7qQPmVAQxEQ5djPDCtiEUrxaPf8wKtLG1m6SB1brejJ1 20.000
```
### Calculate what tokens should be sent
As with SOL, you can List the differences between a list of expected
distributions and the record of what transactions have already been sent using
the `--dry-run` parameter, or `solana-tokens balances`.
Example updated recipients.csv:
```text
recipient,amount
CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT,100
C56nwrDVFpPrqwGYsTgQxv1ZraTh81H14PV4RHvZe36s,100
7aHDubg5FBYj1SgmyBgU3ZJdtfuqYCQsJQK2pTR5JUqr,100
7qQPmVAQxEQ5djPDCtiEUrxaPf8wKtLG1m6SB1brejJ1,100
```
Using dry-run:
```bash
solana-tokens distribute-tokens --dry-run --input-csv <RECIPIENTS_CSV>
```
Example output:
```text
Total in input_csv: 400 tokens
Distributed: 147.5 tokens
Undistributed: 252.5 tokens
Total: 400 tokens
Recipient Expected Balance
CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT 24.600
C56nwrDVFpPrqwGYsTgQxv1ZraTh81H14PV4RHvZe36s 90.000
7aHDubg5FBYj1SgmyBgU3ZJdtfuqYCQsJQK2pTR5JUqr 57.900
7qQPmVAQxEQ5djPDCtiEUrxaPf8wKtLG1m6SB1brejJ1 80.000
```
Or:
```bash
solana-tokens balances --mint <ADDRESS> --input-csv <RECIPIENTS_CSV>
```
Example output:
```text
Token: JDte736XZ1jGUtfAS32DLpBUWBR7WGSHy1hSZ36VRQ5V
Recipient Expected Balance Actual Balance Difference
CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT 100.000 75.400 -24.600
C56nwrDVFpPrqwGYsTgQxv1ZraTh81H14PV4RHvZe36s 100.000 10.000 -90.000
7aHDubg5FBYj1SgmyBgU3ZJdtfuqYCQsJQK2pTR5JUqr 100.000 42.100 -57.900
7qQPmVAQxEQ5djPDCtiEUrxaPf8wKtLG1m6SB1brejJ1 100.000 20.000 -80.000
```

View File

@ -1,11 +1,11 @@
use crate::args::{
Args, BalancesArgs, Command, DistributeTokensArgs, StakeArgs, TransactionLogArgs,
Args, BalancesArgs, Command, DistributeTokensArgs, SplTokenArgs, StakeArgs, TransactionLogArgs,
};
use clap::{
crate_description, crate_name, value_t, value_t_or_exit, App, Arg, ArgMatches, SubCommand,
};
use solana_clap_utils::{
input_parsers::value_of,
input_parsers::{pubkey_of_signer, value_of},
input_validators::{is_amount, is_valid_pubkey, is_valid_signer},
keypair::{pubkey_from_path, signer_from_path},
};
@ -42,7 +42,7 @@ where
)
.subcommand(
SubCommand::with_name("distribute-tokens")
.about("Distribute tokens")
.about("Distribute SOL")
.arg(
Arg::with_name("db_path")
.long("db-path")
@ -201,6 +201,78 @@ where
.help("Fee payer"),
),
)
.subcommand(
SubCommand::with_name("distribute-spl-tokens")
.about("Distribute SPL tokens")
.arg(
Arg::with_name("db_path")
.long("db-path")
.required(true)
.takes_value(true)
.value_name("FILE")
.help(
"Location for storing distribution database. \
The database is used for tracking transactions as they are finalized \
and preventing double spends.",
),
)
.arg(
Arg::with_name("input_csv")
.long("input-csv")
.required(true)
.takes_value(true)
.value_name("FILE")
.help("Allocations CSV file"),
)
.arg(
Arg::with_name("dry_run")
.long("dry-run")
.help("Do not execute any transfers"),
)
.arg(
Arg::with_name("transfer_amount")
.long("transfer-amount")
.takes_value(true)
.value_name("AMOUNT")
.validator(is_amount)
.help("The amount of SPL tokens to send to each recipient"),
)
.arg(
Arg::with_name("output_path")
.long("output-path")
.short("o")
.value_name("FILE")
.takes_value(true)
.help("Write the transaction log to this file"),
)
.arg(
Arg::with_name("token_account_address")
.long("from")
.required(true)
.takes_value(true)
.value_name("TOKEN_ACCOUNT_ADDRESS")
.validator(is_valid_pubkey)
.help("SPL token account to send from"),
)
.arg(
Arg::with_name("token_owner")
.long("owner")
.required(true)
.takes_value(true)
.value_name("TOKEN_ACCOUNT_OWNER_KEYPAIR")
.validator(is_valid_signer)
.help("SPL token account owner"),
)
.arg(
Arg::with_name("fee_payer")
.long("fee-payer")
.required(true)
.takes_value(true)
.value_name("KEYPAIR")
.validator(is_valid_signer)
.help("Fee payer"),
),
)
.subcommand(
SubCommand::with_name("balances")
.about("Balance of each account")
@ -213,6 +285,27 @@ where
.help("Allocations CSV file"),
),
)
.subcommand(
SubCommand::with_name("spl-token-balances")
.about("Balance of SPL token associated accounts")
.arg(
Arg::with_name("input_csv")
.long("input-csv")
.required(true)
.takes_value(true)
.value_name("FILE")
.help("Allocations CSV file"),
)
.arg(
Arg::with_name("mint_address")
.long("mint")
.required(true)
.takes_value(true)
.value_name("MINT_ADDRESS")
.validator(is_valid_pubkey)
.help("SPL token mint of distribution"),
),
)
.subcommand(
SubCommand::with_name("transaction-log")
.about("Print the database to a CSV file")
@ -266,6 +359,7 @@ fn parse_distribute_tokens_args(
sender_keypair,
fee_payer,
stake_args: None,
spl_token_args: None,
transfer_amount: value_of(matches, "transfer_amount"),
})
}
@ -342,14 +436,68 @@ fn parse_distribute_stake_args(
sender_keypair,
fee_payer,
stake_args: Some(stake_args),
spl_token_args: None,
transfer_amount: None,
})
}
fn parse_balances_args(matches: &ArgMatches<'_>) -> BalancesArgs {
BalancesArgs {
fn parse_distribute_spl_tokens_args(
matches: &ArgMatches<'_>,
) -> Result<DistributeTokensArgs, Box<dyn Error>> {
let mut wallet_manager = maybe_wallet_manager()?;
let signer_matches = ArgMatches::default(); // No default signer
let token_owner_str = value_t_or_exit!(matches, "token_owner", String);
let token_owner = signer_from_path(
&signer_matches,
&token_owner_str,
"owner",
&mut wallet_manager,
)?;
let fee_payer_str = value_t_or_exit!(matches, "fee_payer", String);
let fee_payer = signer_from_path(
&signer_matches,
&fee_payer_str,
"fee-payer",
&mut wallet_manager,
)?;
let token_account_address_str = value_t_or_exit!(matches, "token_account_address", String);
let token_account_address = pubkey_from_path(
&signer_matches,
&token_account_address_str,
"token account address",
&mut wallet_manager,
)?;
Ok(DistributeTokensArgs {
input_csv: value_t_or_exit!(matches, "input_csv", String),
}
transaction_db: value_t_or_exit!(matches, "db_path", String),
output_path: matches.value_of("output_path").map(|path| path.to_string()),
dry_run: matches.is_present("dry_run"),
sender_keypair: token_owner,
fee_payer,
stake_args: None,
spl_token_args: Some(SplTokenArgs {
token_account_address,
..SplTokenArgs::default()
}),
transfer_amount: value_of(matches, "transfer_amount"),
})
}
fn parse_balances_args(matches: &ArgMatches<'_>) -> Result<BalancesArgs, Box<dyn Error>> {
let mut wallet_manager = maybe_wallet_manager()?;
let spl_token_args =
pubkey_of_signer(matches, "mint_address", &mut wallet_manager)?.map(|mint| SplTokenArgs {
mint,
..SplTokenArgs::default()
});
Ok(BalancesArgs {
input_csv: value_t_or_exit!(matches, "input_csv", String),
spl_token_args,
})
}
fn parse_transaction_log_args(matches: &ArgMatches<'_>) -> TransactionLogArgs {
@ -375,7 +523,11 @@ where
("distribute-stake", Some(matches)) => {
Command::DistributeTokens(parse_distribute_stake_args(matches)?)
}
("balances", Some(matches)) => Command::Balances(parse_balances_args(matches)),
("distribute-spl-tokens", Some(matches)) => {
Command::DistributeTokens(parse_distribute_spl_tokens_args(matches)?)
}
("balances", Some(matches)) => Command::Balances(parse_balances_args(matches)?),
("spl-token-balances", Some(matches)) => Command::Balances(parse_balances_args(matches)?),
("transaction-log", Some(matches)) => {
Command::TransactionLog(parse_transaction_log_args(matches))
}

View File

@ -8,6 +8,7 @@ pub struct DistributeTokensArgs {
pub sender_keypair: Box<dyn Signer>,
pub fee_payer: Box<dyn Signer>,
pub stake_args: Option<StakeArgs>,
pub spl_token_args: Option<SplTokenArgs>,
pub transfer_amount: Option<f64>,
}
@ -19,8 +20,16 @@ pub struct StakeArgs {
pub lockup_authority: Option<Box<dyn Signer>>,
}
#[derive(Default)]
pub struct SplTokenArgs {
pub token_account_address: Pubkey,
pub mint: Pubkey,
pub decimals: u8,
}
pub struct BalancesArgs {
pub input_csv: String,
pub spl_token_args: Option<SplTokenArgs>,
}
pub struct TransactionLogArgs {

View File

@ -1,5 +1,9 @@
use crate::args::{BalancesArgs, DistributeTokensArgs, StakeArgs, TransactionLogArgs};
use crate::db::{self, TransactionInfo};
use crate::{
args::{BalancesArgs, DistributeTokensArgs, StakeArgs, TransactionLogArgs},
db::{self, TransactionInfo},
spl_token::*,
token_display::Token,
};
use chrono::prelude::*;
use console::style;
use csv::{ReaderBuilder, Trim};
@ -7,6 +11,7 @@ use indexmap::IndexMap;
use indicatif::{ProgressBar, ProgressStyle};
use pickledb::PickleDb;
use serde::{Deserialize, Serialize};
use solana_account_decoder::parse_token::{pubkey_from_spl_token_v2_0, spl_token_v2_0_pubkey};
use solana_banks_client::{BanksClient, BanksClientExt};
use solana_sdk::{
commitment_config::CommitmentLevel,
@ -22,6 +27,8 @@ use solana_stake_program::{
stake_instruction::{self, LockupArgs},
stake_state::{Authorized, Lockup, StakeAuthorize},
};
use spl_associated_token_account_v1_0::get_associated_token_address;
use spl_token_v2_0::solana_program::program_error::ProgramError;
use std::{
cmp::{self},
io,
@ -30,15 +37,16 @@ use std::{
use tokio::time::sleep;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct Allocation {
recipient: String,
amount: f64,
lockup_date: String,
pub struct Allocation {
pub recipient: String,
pub amount: f64,
pub lockup_date: String,
}
#[derive(Debug, PartialEq)]
pub enum FundingSource {
FeePayer,
SplTokenAccount,
StakeAccount,
SystemAccount,
}
@ -83,6 +91,8 @@ pub enum Error {
MissingLockupAuthority,
#[error("insufficient funds in {0:?}, requires {1} SOL")]
InsufficientFunds(FundingSources, f64),
#[error("Program error")]
ProgramError(#[from] ProgramError),
}
fn merge_allocations(allocations: &[Allocation]) -> Vec<Allocation> {
@ -125,7 +135,7 @@ fn apply_previous_transactions(
}
}
}
allocations.retain(|x| x.amount > 0.5);
allocations.retain(|x| x.amount > f64::EPSILON);
}
async fn transfer<S: Signer>(
@ -150,8 +160,9 @@ fn distribution_instructions(
new_stake_account_address: &Pubkey,
args: &DistributeTokensArgs,
lockup_date: Option<DateTime<Utc>>,
do_create_associated_token_account: bool,
) -> Vec<Instruction> {
if args.stake_args.is_none() {
if args.stake_args.is_none() && args.spl_token_args.is_none() {
let from = args.sender_keypair.pubkey();
let to = allocation.recipient.parse().unwrap();
let lamports = sol_to_lamports(allocation.amount);
@ -159,6 +170,10 @@ fn distribution_instructions(
return vec![instruction];
}
if args.spl_token_args.is_some() {
return build_spl_token_instructions(allocation, args, do_create_associated_token_account);
}
let stake_args = args.stake_args.as_ref().unwrap();
let unlocked_sol = stake_args.unlocked_sol;
let sender_pubkey = args.sender_keypair.pubkey();
@ -225,34 +240,65 @@ async fn distribute_allocations(
args: &DistributeTokensArgs,
) -> Result<(), Error> {
type StakeExtras = Vec<(Keypair, Option<DateTime<Utc>>)>;
let (messages, stake_extras): (Vec<Message>, StakeExtras) = allocations
.iter()
.map(|allocation| {
let new_stake_account_keypair = Keypair::new();
let lockup_date = if allocation.lockup_date == "" {
None
} else {
Some(allocation.lockup_date.parse::<DateTime<Utc>>().unwrap())
};
let mut messages: Vec<Message> = vec![];
let mut stake_extras: StakeExtras = vec![];
let mut created_accounts = 0;
for allocation in allocations.iter() {
let new_stake_account_keypair = Keypair::new();
let lockup_date = if allocation.lockup_date == "" {
None
} else {
Some(allocation.lockup_date.parse::<DateTime<Utc>>().unwrap())
};
println!("{:<44} {:>24.9}", allocation.recipient, allocation.amount);
let instructions = distribution_instructions(
allocation,
&new_stake_account_keypair.pubkey(),
args,
lockup_date,
);
let fee_payer_pubkey = args.fee_payer.pubkey();
let message = Message::new(&instructions, Some(&fee_payer_pubkey));
(message, (new_stake_account_keypair, lockup_date))
})
.unzip();
let (decimals, do_create_associated_token_account) =
if let Some(spl_token_args) = &args.spl_token_args {
let wallet_address = allocation.recipient.parse().unwrap();
let associated_token_address = get_associated_token_address(
&wallet_address,
&spl_token_v2_0_pubkey(&spl_token_args.mint),
);
let do_create_associated_token_account = client
.get_account(pubkey_from_spl_token_v2_0(&associated_token_address))
.await?
.is_none();
if do_create_associated_token_account {
created_accounts += 1;
}
(
spl_token_args.decimals as usize,
do_create_associated_token_account,
)
} else {
(9, false)
};
println!(
"{:<44} {:>24.2$}",
allocation.recipient, allocation.amount, decimals
);
let instructions = distribution_instructions(
allocation,
&new_stake_account_keypair.pubkey(),
args,
lockup_date,
do_create_associated_token_account,
);
let fee_payer_pubkey = args.fee_payer.pubkey();
let message = Message::new(&instructions, Some(&fee_payer_pubkey));
messages.push(message);
stake_extras.push((new_stake_account_keypair, lockup_date));
}
let num_signatures = messages
.iter()
.map(|message| message.header.num_required_signatures as usize)
.sum();
check_payer_balances(num_signatures, allocations, client, args).await?;
if args.spl_token_args.is_some() {
check_spl_token_balances(num_signatures, allocations, client, args, created_accounts)
.await?;
} else {
check_payer_balances(num_signatures, allocations, client, args).await?;
}
for ((allocation, message), (new_stake_account_keypair, lockup_date)) in
allocations.iter().zip(messages).zip(stake_extras)
@ -304,7 +350,11 @@ async fn distribute_allocations(
Ok(())
}
fn read_allocations(input_csv: &str, transfer_amount: Option<f64>) -> io::Result<Vec<Allocation>> {
fn read_allocations(
input_csv: &str,
transfer_amount: Option<f64>,
require_lockup_heading: bool,
) -> io::Result<Vec<Allocation>> {
let mut rdr = ReaderBuilder::new().trim(Trim::All).from_path(input_csv)?;
let allocations = if let Some(amount) = transfer_amount {
let recipients: Vec<String> = rdr
@ -319,8 +369,21 @@ fn read_allocations(input_csv: &str, transfer_amount: Option<f64>) -> io::Result
lockup_date: "".to_string(),
})
.collect()
} else {
} else if require_lockup_heading {
rdr.deserialize().map(|entry| entry.unwrap()).collect()
} else {
let recipients: Vec<(String, f64)> = rdr
.deserialize()
.map(|recipient| recipient.unwrap())
.collect();
recipients
.into_iter()
.map(|(recipient, amount)| Allocation {
recipient,
amount,
lockup_date: "".to_string(),
})
.collect()
};
Ok(allocations)
}
@ -337,11 +400,17 @@ pub async fn process_allocations(
client: &mut BanksClient,
args: &DistributeTokensArgs,
) -> Result<Option<usize>, Error> {
let mut allocations: Vec<Allocation> = read_allocations(&args.input_csv, args.transfer_amount)?;
let require_lockup_heading = args.stake_args.is_some();
let mut allocations: Vec<Allocation> = read_allocations(
&args.input_csv,
args.transfer_amount,
require_lockup_heading,
)?;
let is_sol = args.spl_token_args.is_none();
let starting_total_tokens: f64 = allocations.iter().map(|x| x.amount).sum();
let starting_total_tokens = Token::from(allocations.iter().map(|x| x.amount).sum(), is_sol);
println!(
"{} {}",
"{} {}",
style("Total in input_csv:").bold(),
starting_total_tokens,
);
@ -359,27 +428,23 @@ pub async fn process_allocations(
return Ok(confirmations);
}
let distributed_tokens: f64 = transaction_infos.iter().map(|x| x.amount).sum();
let undistributed_tokens: f64 = allocations.iter().map(|x| x.amount).sum();
println!("{} {}", style("Distributed:").bold(), distributed_tokens,);
let distributed_tokens = Token::from(transaction_infos.iter().map(|x| x.amount).sum(), is_sol);
let undistributed_tokens = Token::from(allocations.iter().map(|x| x.amount).sum(), is_sol);
println!("{} {}", style("Distributed:").bold(), distributed_tokens,);
println!(
"{} {}",
"{} {}",
style("Undistributed:").bold(),
undistributed_tokens,
);
println!(
"{} {}",
"{} {}",
style("Total:").bold(),
distributed_tokens + undistributed_tokens,
);
println!(
"{}",
style(format!(
"{:<44} {:>24}",
"Recipient", "Expected Balance (◎)"
))
.bold()
style(format!("{:<44} {:>24}", "Recipient", "Expected Balance",)).bold()
);
distribute_allocations(client, &mut db, &allocations, args).await?;
@ -563,33 +628,41 @@ async fn check_payer_balances(
Ok(())
}
pub async fn process_balances(
client: &mut BanksClient,
args: &BalancesArgs,
) -> Result<(), csv::Error> {
let allocations: Vec<Allocation> = read_allocations(&args.input_csv, None)?;
pub async fn process_balances(client: &mut BanksClient, args: &BalancesArgs) -> Result<(), Error> {
let allocations: Vec<Allocation> = read_allocations(&args.input_csv, None, false)?;
let allocations = merge_allocations(&allocations);
let token = if let Some(spl_token_args) = &args.spl_token_args {
spl_token_args.mint.to_string()
} else {
"".to_string()
};
println!("{} {}", style("Token:").bold(), token);
println!(
"{}",
style(format!(
"{:<44} {:>24} {:>24} {:>24}",
"Recipient", "Expected Balance (◎)", "Actual Balance (◎)", "Difference (◎)"
"Recipient", "Expected Balance", "Actual Balance", "Difference"
))
.bold()
);
for allocation in &allocations {
let address = allocation.recipient.parse().unwrap();
let expected = lamports_to_sol(sol_to_lamports(allocation.amount));
let actual = lamports_to_sol(client.get_balance(address).await.unwrap());
println!(
"{:<44} {:>24.9} {:>24.9} {:>24.9}",
allocation.recipient,
expected,
actual,
actual - expected
);
if let Some(spl_token_args) = &args.spl_token_args {
print_token_balances(client, allocation, spl_token_args).await?;
} else {
let address: Pubkey = allocation.recipient.parse().unwrap();
let expected = lamports_to_sol(sol_to_lamports(allocation.amount));
let actual = lamports_to_sol(client.get_balance(address).await.unwrap());
println!(
"{:<44} {:>24.9} {:>24.9} {:>24.9}",
allocation.recipient,
expected,
actual,
actual - expected,
);
}
}
Ok(())
@ -665,6 +738,7 @@ pub async fn test_process_distribute_tokens_with_client(
transaction_db: transaction_db.clone(),
output_path: Some(output_path.clone()),
stake_args: None,
spl_token_args: None,
transfer_amount,
};
let confirmations = process_allocations(client, &args).await.unwrap();
@ -788,6 +862,7 @@ pub async fn test_process_distribute_stake_with_client(
transaction_db: transaction_db.clone(),
output_path: Some(output_path.clone()),
stake_args: Some(stake_args),
spl_token_args: None,
sender_keypair: Box::new(sender_keypair),
transfer_amount: None,
};
@ -841,7 +916,7 @@ pub async fn test_process_distribute_stake_with_client(
}
#[cfg(test)]
mod tests {
pub(crate) mod tests {
use super::*;
use solana_banks_client::start_client;
use solana_banks_server::banks_server::start_local_server;
@ -909,11 +984,78 @@ mod tests {
wtr.flush().unwrap();
assert_eq!(
read_allocations(&input_csv, None).unwrap(),
read_allocations(&input_csv, None, false).unwrap(),
vec![allocation.clone()]
);
assert_eq!(
read_allocations(&input_csv, None, true).unwrap(),
vec![allocation]
);
}
#[test]
fn test_read_allocations_no_lockup() {
let pubkey0 = solana_sdk::pubkey::new_rand();
let pubkey1 = solana_sdk::pubkey::new_rand();
let file = NamedTempFile::new().unwrap();
let input_csv = file.path().to_str().unwrap().to_string();
let mut wtr = csv::WriterBuilder::new().from_writer(file);
wtr.serialize(("recipient".to_string(), "amount".to_string()))
.unwrap();
wtr.serialize((&pubkey0.to_string(), 42.0)).unwrap();
wtr.serialize((&pubkey1.to_string(), 43.0)).unwrap();
wtr.flush().unwrap();
let expected_allocations = vec![
Allocation {
recipient: pubkey0.to_string(),
amount: 42.0,
lockup_date: "".to_string(),
},
Allocation {
recipient: pubkey1.to_string(),
amount: 43.0,
lockup_date: "".to_string(),
},
];
assert_eq!(
read_allocations(&input_csv, None, false).unwrap(),
expected_allocations
);
}
#[test]
#[should_panic]
fn test_read_allocations_malformed() {
let pubkey0 = solana_sdk::pubkey::new_rand();
let pubkey1 = solana_sdk::pubkey::new_rand();
let file = NamedTempFile::new().unwrap();
let input_csv = file.path().to_str().unwrap().to_string();
let mut wtr = csv::WriterBuilder::new().from_writer(file);
wtr.serialize(("recipient".to_string(), "amount".to_string()))
.unwrap();
wtr.serialize((&pubkey0.to_string(), 42.0)).unwrap();
wtr.serialize((&pubkey1.to_string(), 43.0)).unwrap();
wtr.flush().unwrap();
let expected_allocations = vec![
Allocation {
recipient: pubkey0.to_string(),
amount: 42.0,
lockup_date: "".to_string(),
},
Allocation {
recipient: pubkey1.to_string(),
amount: 43.0,
lockup_date: "".to_string(),
},
];
assert_eq!(
read_allocations(&input_csv, None, true).unwrap(),
expected_allocations
);
}
#[test]
fn test_read_allocations_transfer_amount() {
let pubkey0 = solana_sdk::pubkey::new_rand();
@ -948,7 +1090,7 @@ mod tests {
},
];
assert_eq!(
read_allocations(&input_csv, Some(amount)).unwrap(),
read_allocations(&input_csv, Some(amount), false).unwrap(),
expected_allocations
);
}
@ -1058,6 +1200,7 @@ mod tests {
transaction_db: "".to_string(),
output_path: None,
stake_args: Some(stake_args),
spl_token_args: None,
sender_keypair: Box::new(Keypair::new()),
transfer_amount: None,
};
@ -1067,6 +1210,7 @@ mod tests {
&new_stake_account_address,
&args,
Some(lockup_date),
false,
);
let lockup_instruction =
bincode::deserialize(&instructions[SET_LOCKUP_INDEX].data).unwrap();
@ -1079,7 +1223,7 @@ mod tests {
}
}
fn tmp_file_path(name: &str, pubkey: &Pubkey) -> String {
pub fn tmp_file_path(name: &str, pubkey: &Pubkey) -> String {
use std::env;
let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
@ -1106,6 +1250,7 @@ mod tests {
transaction_db: "".to_string(),
output_path: None,
stake_args,
spl_token_args: None,
transfer_amount: None,
};
(allocations, args)

View File

@ -2,3 +2,5 @@ pub mod arg_parser;
pub mod args;
pub mod commands;
mod db;
pub mod spl_token;
pub mod token_display;

View File

@ -1,6 +1,6 @@
use solana_banks_client::start_tcp_client;
use solana_cli_config::{Config, CONFIG_FILE};
use solana_tokens::{arg_parser::parse_args, args::Command, commands};
use solana_tokens::{arg_parser::parse_args, args::Command, commands, spl_token};
use std::{env, error::Error, path::Path, process};
use tokio::runtime::Runtime;
use url::Url;
@ -26,10 +26,18 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut banks_client = runtime.block_on(start_tcp_client(&host_port))?;
match command_args.command {
Command::DistributeTokens(args) => {
Command::DistributeTokens(mut args) => {
runtime.block_on(spl_token::update_token_args(
&mut banks_client,
&mut args.spl_token_args,
))?;
runtime.block_on(commands::process_allocations(&mut banks_client, &args))?;
}
Command::Balances(args) => {
Command::Balances(mut args) => {
runtime.block_on(spl_token::update_decimals(
&mut banks_client,
&mut args.spl_token_args,
))?;
runtime.block_on(commands::process_balances(&mut banks_client, &args))?;
}
Command::TransactionLog(args) => {

698
tokens/src/spl_token.rs Normal file
View File

@ -0,0 +1,698 @@
use crate::{
args::{DistributeTokensArgs, SplTokenArgs},
commands::{Allocation, Error, FundingSource},
};
use console::style;
use solana_account_decoder::parse_token::{
pubkey_from_spl_token_v2_0, spl_token_v2_0_pubkey, token_amount_to_ui_amount,
};
use solana_banks_client::{BanksClient, BanksClientExt};
use solana_sdk::{instruction::Instruction, native_token::lamports_to_sol};
use solana_transaction_status::parse_token::spl_token_v2_0_instruction;
use spl_associated_token_account_v1_0::{
create_associated_token_account, get_associated_token_address,
};
use spl_token_v2_0::{
solana_program::program_pack::Pack,
state::{Account as SplTokenAccount, Mint},
};
pub async fn update_token_args(
client: &mut BanksClient,
args: &mut Option<SplTokenArgs>,
) -> Result<(), Error> {
if let Some(spl_token_args) = args {
let sender_account = client
.get_account(spl_token_args.token_account_address)
.await?
.unwrap_or_default();
let mint_address =
pubkey_from_spl_token_v2_0(&SplTokenAccount::unpack(&sender_account.data)?.mint);
spl_token_args.mint = mint_address;
update_decimals(client, args).await?;
}
Ok(())
}
pub async fn update_decimals(
client: &mut BanksClient,
args: &mut Option<SplTokenArgs>,
) -> Result<(), Error> {
if let Some(spl_token_args) = args {
let mint_account = client
.get_account(spl_token_args.mint)
.await?
.unwrap_or_default();
let mint = Mint::unpack(&mint_account.data)?;
spl_token_args.decimals = mint.decimals;
}
Ok(())
}
pub fn spl_token_amount(amount: f64, decimals: u8) -> u64 {
(amount * 10_usize.pow(decimals as u32) as f64) as u64
}
pub fn build_spl_token_instructions(
allocation: &Allocation,
args: &DistributeTokensArgs,
do_create_associated_token_account: bool,
) -> Vec<Instruction> {
let spl_token_args = args
.spl_token_args
.as_ref()
.expect("spl_token_args must be some");
let wallet_address = allocation.recipient.parse().unwrap();
let associated_token_address = get_associated_token_address(
&wallet_address,
&spl_token_v2_0_pubkey(&spl_token_args.mint),
);
let mut instructions = vec![];
if do_create_associated_token_account {
let create_associated_token_account_instruction = create_associated_token_account(
&spl_token_v2_0_pubkey(&args.fee_payer.pubkey()),
&wallet_address,
&spl_token_v2_0_pubkey(&spl_token_args.mint),
);
instructions.push(spl_token_v2_0_instruction(
create_associated_token_account_instruction,
));
}
let spl_instruction = spl_token_v2_0::instruction::transfer_checked(
&spl_token_v2_0::id(),
&spl_token_v2_0_pubkey(&spl_token_args.token_account_address),
&spl_token_v2_0_pubkey(&spl_token_args.mint),
&associated_token_address,
&spl_token_v2_0_pubkey(&args.sender_keypair.pubkey()),
&[],
spl_token_amount(allocation.amount, spl_token_args.decimals),
spl_token_args.decimals,
)
.unwrap();
instructions.push(spl_token_v2_0_instruction(spl_instruction));
instructions
}
pub async fn check_spl_token_balances(
num_signatures: usize,
allocations: &[Allocation],
client: &mut BanksClient,
args: &DistributeTokensArgs,
created_accounts: u64,
) -> Result<(), Error> {
let spl_token_args = args
.spl_token_args
.as_ref()
.expect("spl_token_args must be some");
let undistributed_tokens: f64 = allocations.iter().map(|x| x.amount).sum();
let allocation_amount = spl_token_amount(undistributed_tokens, spl_token_args.decimals);
let (fee_calculator, _blockhash, _last_valid_slot) = client.get_fees().await?;
let fees = fee_calculator
.lamports_per_signature
.checked_mul(num_signatures as u64)
.unwrap();
let rent = client.get_rent().await?;
let token_account_rent_exempt_balance = rent.minimum_balance(SplTokenAccount::LEN);
let account_creation_amount = created_accounts * token_account_rent_exempt_balance;
let fee_payer_balance = client.get_balance(args.fee_payer.pubkey()).await?;
if fee_payer_balance < fees + account_creation_amount {
return Err(Error::InsufficientFunds(
vec![FundingSource::FeePayer].into(),
lamports_to_sol(fees + account_creation_amount),
));
}
let source_token_account = client
.get_account(spl_token_args.token_account_address)
.await?
.unwrap_or_default();
let source_token = SplTokenAccount::unpack(&source_token_account.data)?;
if source_token.amount < allocation_amount {
return Err(Error::InsufficientFunds(
vec![FundingSource::SplTokenAccount].into(),
token_amount_to_ui_amount(allocation_amount, spl_token_args.decimals).ui_amount,
));
}
Ok(())
}
pub async fn print_token_balances(
client: &mut BanksClient,
allocation: &Allocation,
spl_token_args: &SplTokenArgs,
) -> Result<(), Error> {
let address = allocation.recipient.parse().unwrap();
let expected = allocation.amount;
let associated_token_address = get_associated_token_address(
&spl_token_v2_0_pubkey(&address),
&spl_token_v2_0_pubkey(&spl_token_args.mint),
);
let recipient_account = client
.get_account(pubkey_from_spl_token_v2_0(&associated_token_address))
.await?
.unwrap_or_default();
let (actual, difference) =
if let Ok(recipient_token) = SplTokenAccount::unpack(&recipient_account.data) {
let actual = token_amount_to_ui_amount(recipient_token.amount, spl_token_args.decimals)
.ui_amount;
(
style(format!(
"{:>24.1$}",
actual, spl_token_args.decimals as usize
)),
format!(
"{:>24.1$}",
actual - expected,
spl_token_args.decimals as usize
),
)
} else {
(
style("Associated token account not yet created".to_string()).yellow(),
"".to_string(),
)
};
println!(
"{:<44} {:>24.4$} {:>24} {:>24}",
allocation.recipient, expected, actual, difference, spl_token_args.decimals as usize
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
commands::{process_allocations, tests::tmp_file_path, Allocation},
db::{self, check_output_file},
spl_token::spl_token_amount,
};
use solana_account_decoder::parse_token::{spl_token_id_v2_0, spl_token_v2_0_pubkey};
use solana_program_test::*;
use solana_sdk::{
hash::Hash,
signature::{read_keypair_file, write_keypair_file, Keypair, Signer},
system_instruction,
transaction::Transaction,
};
use solana_transaction_status::parse_token::spl_token_v2_0_instruction;
use spl_associated_token_account_v1_0::{
create_associated_token_account, get_associated_token_address,
};
use spl_token_v2_0::{
instruction::{initialize_account, initialize_mint, mint_to},
solana_program::pubkey::Pubkey,
};
use tempfile::{tempdir, NamedTempFile};
fn program_test() -> ProgramTest {
// Add SPL Associated Token program
let mut pc = ProgramTest::new(
"spl_associated_token_account",
pubkey_from_spl_token_v2_0(&spl_associated_token_account_v1_0::id()),
None,
);
// Add SPL Token program
pc.add_program("spl_token", spl_token_id_v2_0(), None);
pc
}
async fn initialize_test_mint(
banks_client: &mut BanksClient,
fee_payer: &Keypair,
mint: &Keypair,
decimals: u8,
recent_blockhash: Hash,
) {
let rent = banks_client.get_rent().await.unwrap();
let expected_mint_balance = rent.minimum_balance(Mint::LEN);
let instructions = vec![
system_instruction::create_account(
&fee_payer.pubkey(),
&mint.pubkey(),
expected_mint_balance,
Mint::LEN as u64,
&spl_token_id_v2_0(),
),
spl_token_v2_0_instruction(
initialize_mint(
&spl_token_v2_0::id(),
&spl_token_v2_0_pubkey(&mint.pubkey()),
&spl_token_v2_0_pubkey(&mint.pubkey()),
None,
decimals,
)
.unwrap(),
),
];
let mut transaction = Transaction::new_with_payer(&instructions, Some(&fee_payer.pubkey()));
transaction.sign(&[fee_payer, mint], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
}
async fn initialize_token_account(
banks_client: &mut BanksClient,
fee_payer: &Keypair,
sender_account: &Keypair,
mint: &Keypair,
owner: &Keypair,
recent_blockhash: Hash,
) {
let rent = banks_client.get_rent().await.unwrap();
let expected_token_account_balance = rent.minimum_balance(SplTokenAccount::LEN);
let instructions = vec![
system_instruction::create_account(
&fee_payer.pubkey(),
&sender_account.pubkey(),
expected_token_account_balance,
SplTokenAccount::LEN as u64,
&spl_token_id_v2_0(),
),
spl_token_v2_0_instruction(
initialize_account(
&spl_token_v2_0::id(),
&spl_token_v2_0_pubkey(&sender_account.pubkey()),
&spl_token_v2_0_pubkey(&mint.pubkey()),
&spl_token_v2_0_pubkey(&owner.pubkey()),
)
.unwrap(),
),
];
let mut transaction = Transaction::new_with_payer(&instructions, Some(&fee_payer.pubkey()));
transaction.sign(&[fee_payer, sender_account], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
}
async fn mint_to_account(
banks_client: &mut BanksClient,
fee_payer: &Keypair,
sender_account: &Keypair,
mint: &Keypair,
recent_blockhash: Hash,
) {
let instructions = vec![spl_token_v2_0_instruction(
mint_to(
&spl_token_v2_0::id(),
&spl_token_v2_0_pubkey(&mint.pubkey()),
&spl_token_v2_0_pubkey(&sender_account.pubkey()),
&spl_token_v2_0_pubkey(&mint.pubkey()),
&[],
200_000,
)
.unwrap(),
)];
let mut transaction = Transaction::new_with_payer(&instructions, Some(&fee_payer.pubkey()));
transaction.sign(&[fee_payer, mint], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
}
async fn test_process_distribute_spl_tokens_with_client(
banks_client: &mut BanksClient,
fee_payer: Keypair,
transfer_amount: Option<f64>,
recent_blockhash: Hash,
) {
// Initialize Token Mint
let decimals = 2;
let mint = Keypair::new();
initialize_test_mint(banks_client, &fee_payer, &mint, decimals, recent_blockhash).await;
// Initialize Sender Token Account and Mint
let sender_account = Keypair::new();
let owner = Keypair::new();
initialize_token_account(
banks_client,
&fee_payer,
&sender_account,
&mint,
&owner,
recent_blockhash,
)
.await;
mint_to_account(
banks_client,
&fee_payer,
&sender_account,
&mint,
recent_blockhash,
)
.await;
// Initialize one recipient Associated Token Account
let wallet_address_0 = Pubkey::new_unique();
let instructions = vec![spl_token_v2_0_instruction(create_associated_token_account(
&spl_token_v2_0_pubkey(&fee_payer.pubkey()),
&wallet_address_0,
&spl_token_v2_0_pubkey(&mint.pubkey()),
))];
let mut transaction = Transaction::new_with_payer(&instructions, Some(&fee_payer.pubkey()));
transaction.sign(&[&fee_payer], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
let wallet_address_1 = Pubkey::new_unique();
// Create allocations csv
let allocation_amount = if let Some(amount) = transfer_amount {
amount
} else {
1000.0
};
let allocations_file = NamedTempFile::new().unwrap();
let input_csv = allocations_file.path().to_str().unwrap().to_string();
let mut wtr = csv::WriterBuilder::new().from_writer(allocations_file);
let allocation = Allocation {
recipient: wallet_address_0.to_string(),
amount: allocation_amount,
lockup_date: "".to_string(),
};
wtr.serialize(&allocation).unwrap();
let allocation = Allocation {
recipient: wallet_address_1.to_string(),
amount: allocation_amount,
lockup_date: "".to_string(),
};
wtr.serialize(&allocation).unwrap();
wtr.flush().unwrap();
let dir = tempdir().unwrap();
let transaction_db = dir
.path()
.join("transactions.db")
.to_str()
.unwrap()
.to_string();
let output_file = NamedTempFile::new().unwrap();
let output_path = output_file.path().to_str().unwrap().to_string();
let args = DistributeTokensArgs {
sender_keypair: Box::new(owner),
fee_payer: Box::new(fee_payer),
dry_run: false,
input_csv,
transaction_db: transaction_db.clone(),
output_path: Some(output_path.clone()),
stake_args: None,
spl_token_args: Some(SplTokenArgs {
token_account_address: sender_account.pubkey(),
mint: mint.pubkey(),
decimals,
}),
transfer_amount,
};
// Distribute Allocations
let confirmations = process_allocations(banks_client, &args).await.unwrap();
assert_eq!(confirmations, None);
let associated_token_address_0 =
get_associated_token_address(&wallet_address_0, &spl_token_v2_0_pubkey(&mint.pubkey()));
let associated_token_address_1 =
get_associated_token_address(&wallet_address_1, &spl_token_v2_0_pubkey(&mint.pubkey()));
let transaction_infos =
db::read_transaction_infos(&db::open_db(&transaction_db, true).unwrap());
assert_eq!(transaction_infos.len(), 2);
assert!(transaction_infos
.iter()
.any(|info| info.recipient == pubkey_from_spl_token_v2_0(&wallet_address_0)));
assert!(transaction_infos
.iter()
.any(|info| info.recipient == pubkey_from_spl_token_v2_0(&wallet_address_1)));
let expected_amount = spl_token_amount(allocation.amount, decimals);
assert_eq!(
spl_token_amount(transaction_infos[0].amount, decimals),
expected_amount
);
assert_eq!(
spl_token_amount(transaction_infos[1].amount, decimals),
expected_amount
);
let recipient_account_0 = banks_client
.get_account(pubkey_from_spl_token_v2_0(&associated_token_address_0))
.await
.unwrap()
.unwrap_or_default();
assert_eq!(
SplTokenAccount::unpack(&recipient_account_0.data)
.unwrap()
.amount,
expected_amount,
);
let recipient_account_1 = banks_client
.get_account(pubkey_from_spl_token_v2_0(&associated_token_address_1))
.await
.unwrap()
.unwrap_or_default();
assert_eq!(
SplTokenAccount::unpack(&recipient_account_1.data)
.unwrap()
.amount,
expected_amount,
);
check_output_file(&output_path, &db::open_db(&transaction_db, true).unwrap());
// Now, run it again, and check there's no double-spend.
process_allocations(banks_client, &args).await.unwrap();
let transaction_infos =
db::read_transaction_infos(&db::open_db(&transaction_db, true).unwrap());
assert_eq!(transaction_infos.len(), 2);
assert!(transaction_infos
.iter()
.any(|info| info.recipient == pubkey_from_spl_token_v2_0(&wallet_address_0)));
assert!(transaction_infos
.iter()
.any(|info| info.recipient == pubkey_from_spl_token_v2_0(&wallet_address_1)));
let expected_amount = spl_token_amount(allocation.amount, decimals);
assert_eq!(
spl_token_amount(transaction_infos[0].amount, decimals),
expected_amount
);
assert_eq!(
spl_token_amount(transaction_infos[1].amount, decimals),
expected_amount
);
let recipient_account_0 = banks_client
.get_account(pubkey_from_spl_token_v2_0(&associated_token_address_0))
.await
.unwrap()
.unwrap_or_default();
assert_eq!(
SplTokenAccount::unpack(&recipient_account_0.data)
.unwrap()
.amount,
expected_amount,
);
let recipient_account_1 = banks_client
.get_account(pubkey_from_spl_token_v2_0(&associated_token_address_1))
.await
.unwrap()
.unwrap_or_default();
assert_eq!(
SplTokenAccount::unpack(&recipient_account_1.data)
.unwrap()
.amount,
expected_amount,
);
check_output_file(&output_path, &db::open_db(&transaction_db, true).unwrap());
}
#[tokio::test]
async fn test_process_spl_token_allocations() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
test_process_distribute_spl_tokens_with_client(
&mut banks_client,
payer,
None,
recent_blockhash,
)
.await;
}
#[tokio::test]
async fn test_process_spl_token_transfer_amount_allocations() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
test_process_distribute_spl_tokens_with_client(
&mut banks_client,
payer,
Some(105.5),
recent_blockhash,
)
.await;
}
#[tokio::test]
async fn test_check_check_spl_token_balances() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let (fee_calculator, _, _) = banks_client.get_fees().await.unwrap();
let signatures = 2;
let fees = fee_calculator.lamports_per_signature * signatures;
let fees_in_sol = lamports_to_sol(fees);
let rent = banks_client.get_rent().await.unwrap();
let expected_token_account_balance = rent.minimum_balance(SplTokenAccount::LEN);
let expected_token_account_balance_sol = lamports_to_sol(expected_token_account_balance);
// Initialize Token Mint
let decimals = 2;
let mint = Keypair::new();
initialize_test_mint(&mut banks_client, &payer, &mint, decimals, recent_blockhash).await;
// Initialize Sender Token Account and Mint
let sender_account = Keypair::new();
let owner = Keypair::new();
let owner_keypair_file = tmp_file_path("keypair_file", &owner.pubkey());
write_keypair_file(&owner, &owner_keypair_file).unwrap();
initialize_token_account(
&mut banks_client,
&payer,
&sender_account,
&mint,
&owner,
recent_blockhash,
)
.await;
let unfunded_fee_payer = Keypair::new();
let allocation_amount = 42.0;
let allocations = vec![Allocation {
recipient: Pubkey::new_unique().to_string(),
amount: allocation_amount,
lockup_date: "".to_string(),
}];
let mut args = DistributeTokensArgs {
sender_keypair: read_keypair_file(&owner_keypair_file).unwrap().into(),
fee_payer: Box::new(unfunded_fee_payer),
dry_run: false,
input_csv: "".to_string(),
transaction_db: "".to_string(),
output_path: None,
stake_args: None,
spl_token_args: Some(SplTokenArgs {
token_account_address: sender_account.pubkey(),
mint: mint.pubkey(),
decimals,
}),
transfer_amount: None,
};
// Unfunded fee_payer
let err_result = check_spl_token_balances(
signatures as usize,
&allocations,
&mut banks_client,
&args,
1,
)
.await
.unwrap_err();
if let Error::InsufficientFunds(sources, amount) = err_result {
assert_eq!(sources, vec![FundingSource::FeePayer].into());
assert!(
(amount - (fees_in_sol + expected_token_account_balance_sol)).abs() < f64::EPSILON
);
} else {
panic!("check_spl_token_balances should have errored");
}
// Unfunded sender SPL Token account
let fee_payer = Keypair::new();
let instruction = system_instruction::transfer(
&payer.pubkey(),
&fee_payer.pubkey(),
fees + expected_token_account_balance,
);
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
args.fee_payer = Box::new(fee_payer);
let err_result = check_spl_token_balances(
signatures as usize,
&allocations,
&mut banks_client,
&args,
1,
)
.await
.unwrap_err();
if let Error::InsufficientFunds(sources, amount) = err_result {
assert_eq!(sources, vec![FundingSource::SplTokenAccount].into());
assert!((amount - allocation_amount).abs() < f64::EPSILON);
} else {
panic!("check_spl_token_balances should have errored");
}
// Fully funded payers
mint_to_account(
&mut banks_client,
&payer,
&sender_account,
&mint,
recent_blockhash,
)
.await;
check_spl_token_balances(
signatures as usize,
&allocations,
&mut banks_client,
&args,
1,
)
.await
.unwrap();
// Partially-funded fee payer can afford fees, but not to create Associated Token Account
let partially_funded_fee_payer = Keypair::new();
let instruction = system_instruction::transfer(
&payer.pubkey(),
&partially_funded_fee_payer.pubkey(),
fees,
);
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
args.fee_payer = Box::new(partially_funded_fee_payer);
let err_result = check_spl_token_balances(
signatures as usize,
&allocations,
&mut banks_client,
&args,
1,
)
.await
.unwrap_err();
if let Error::InsufficientFunds(sources, amount) = err_result {
assert_eq!(sources, vec![FundingSource::FeePayer].into());
assert!(
(amount - (fees_in_sol + expected_token_account_balance_sol)).abs() < f64::EPSILON
);
} else {
panic!("check_spl_token_balances should have errored");
}
// Succeeds if no account creation required
check_spl_token_balances(
signatures as usize,
&allocations,
&mut banks_client,
&args,
0,
)
.await
.unwrap();
}
}

View File

@ -0,0 +1,62 @@
use std::{
fmt::{Debug, Display, Formatter, Result},
ops::Add,
};
const SOL_SYMBOL: &str = "";
#[derive(PartialEq)]
pub enum TokenType {
Sol,
SplToken,
}
pub struct Token {
amount: f64,
token_type: TokenType,
}
impl Token {
fn write_with_symbol(&self, f: &mut Formatter) -> Result {
match &self.token_type {
TokenType::Sol => write!(f, "{}{}", SOL_SYMBOL, self.amount,),
TokenType::SplToken => write!(f, "{} tokens", self.amount,),
}
}
pub fn from(amount: f64, is_sol: bool) -> Self {
let token_type = if is_sol {
TokenType::Sol
} else {
TokenType::SplToken
};
Self { amount, token_type }
}
}
impl Display for Token {
fn fmt(&self, f: &mut Formatter) -> Result {
self.write_with_symbol(f)
}
}
impl Debug for Token {
fn fmt(&self, f: &mut Formatter) -> Result {
self.write_with_symbol(f)
}
}
impl Add for Token {
type Output = Token;
fn add(self, other: Self) -> Self {
if self.token_type == other.token_type {
Self {
amount: self.amount + other.amount,
token_type: self.token_type,
}
} else {
self
}
}
}

Binary file not shown.

BIN
tokens/tests/fixtures/spl_token.so vendored Normal file

Binary file not shown.

View File

@ -2,11 +2,14 @@ use crate::parse_instruction::{
check_num_accounts, ParsableProgram, ParseInstructionError, ParsedInstructionEnum,
};
use serde_json::{json, Map, Value};
use solana_account_decoder::parse_token::token_amount_to_ui_amount;
use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey};
use solana_account_decoder::parse_token::{pubkey_from_spl_token_v2_0, token_amount_to_ui_amount};
use solana_sdk::{
instruction::{AccountMeta, CompiledInstruction, Instruction},
pubkey::Pubkey,
};
use spl_token_v2_0::{
instruction::{AuthorityType, TokenInstruction},
solana_program::program_option::COption,
solana_program::{instruction::Instruction as SplTokenInstruction, program_option::COption},
};
pub fn parse_token(
@ -410,6 +413,22 @@ fn check_num_token_accounts(accounts: &[u8], num: usize) -> Result<(), ParseInst
check_num_accounts(accounts, num, ParsableProgram::SplToken)
}
pub fn spl_token_v2_0_instruction(instruction: SplTokenInstruction) -> Instruction {
Instruction {
program_id: pubkey_from_spl_token_v2_0(&instruction.program_id),
accounts: instruction
.accounts
.iter()
.map(|meta| AccountMeta {
pubkey: pubkey_from_spl_token_v2_0(&meta.pubkey),
is_signer: meta.is_signer,
is_writable: meta.is_writable,
})
.collect(),
data: instruction.data,
}
}
#[cfg(test)]
mod test {
use super::*;