Distribute spl tokens (#13559)
* 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 * Use epsilon for allocation retention
This commit is contained in:
parent
1ffab5de77
commit
2ef4369237
|
@ -4971,17 +4971,21 @@ dependencies = [
|
|||
"indicatif",
|
||||
"pickledb",
|
||||
"serde",
|
||||
"solana-account-decoder",
|
||||
"solana-clap-utils",
|
||||
"solana-cli-config",
|
||||
"solana-client",
|
||||
"solana-core",
|
||||
"solana-logger 1.5.0",
|
||||
"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",
|
||||
]
|
||||
|
@ -5154,6 +5158,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"
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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.5.0" }
|
||||
solana-clap-utils = { path = "../clap-utils", version = "1.5.0" }
|
||||
solana-cli-config = { path = "../cli-config", version = "1.5.0" }
|
||||
solana-client = { path = "../client", version = "1.5.0" }
|
||||
|
@ -27,6 +28,8 @@ solana-sdk = { path = "../sdk", version = "1.5.0" }
|
|||
solana-stake-program = { path = "../programs/stake", version = "1.5.0" }
|
||||
solana-transaction-status = { path = "../transaction-status", version = "1.5.0" }
|
||||
solana-version = { path = "../version", version = "1.5.0" }
|
||||
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"
|
||||
|
||||
|
@ -34,3 +37,4 @@ thiserror = "1.0"
|
|||
bincode = "1.3.1"
|
||||
solana-core = { path = "../core", version = "1.5.0" }
|
||||
solana-logger = { path = "../logger", version = "1.5.0" }
|
||||
solana-program-test = { path = "../program-test", version = "1.5.0" }
|
||||
|
|
118
tokens/README.md
118
tokens/README.md
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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_client::{
|
||||
client_error::{ClientError, Result as ClientResult},
|
||||
rpc_client::RpcClient,
|
||||
|
@ -25,6 +30,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,
|
||||
|
@ -33,15 +40,16 @@ use std::{
|
|||
};
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
@ -86,6 +94,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> {
|
||||
|
@ -128,7 +138,7 @@ fn apply_previous_transactions(
|
|||
}
|
||||
}
|
||||
}
|
||||
allocations.retain(|x| x.amount > 0.5);
|
||||
allocations.retain(|x| x.amount > f64::EPSILON);
|
||||
}
|
||||
|
||||
fn transfer<S: Signer>(
|
||||
|
@ -153,8 +163,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);
|
||||
|
@ -162,6 +173,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();
|
||||
|
@ -228,34 +243,65 @@ 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 (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 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 do_create_associated_token_account = client
|
||||
.get_multiple_accounts(&[pubkey_from_spl_token_v2_0(&associated_token_address)])?
|
||||
[0]
|
||||
.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)?;
|
||||
if args.spl_token_args.is_some() {
|
||||
check_spl_token_balances(num_signatures, allocations, client, args, created_accounts)?;
|
||||
} else {
|
||||
check_payer_balances(num_signatures, allocations, client, args)?;
|
||||
}
|
||||
|
||||
for ((allocation, message), (new_stake_account_keypair, lockup_date)) in
|
||||
allocations.iter().zip(messages).zip(stake_extras)
|
||||
|
@ -313,7 +359,11 @@ 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
|
||||
|
@ -328,8 +378,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)
|
||||
}
|
||||
|
@ -346,11 +409,17 @@ pub fn process_allocations(
|
|||
client: &RpcClient,
|
||||
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,
|
||||
);
|
||||
|
@ -368,27 +437,23 @@ pub 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)?;
|
||||
|
@ -572,30 +637,41 @@ fn check_payer_balances(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_balances(client: &RpcClient, args: &BalancesArgs) -> Result<(), csv::Error> {
|
||||
let allocations: Vec<Allocation> = read_allocations(&args.input_csv, None)?;
|
||||
pub fn process_balances(client: &RpcClient, 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).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)?;
|
||||
} 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).unwrap());
|
||||
println!(
|
||||
"{:<44} {:>24.9} {:>24.9} {:>24.9}",
|
||||
allocation.recipient,
|
||||
expected,
|
||||
actual,
|
||||
actual - expected,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -666,6 +742,7 @@ pub 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).unwrap();
|
||||
|
@ -777,6 +854,7 @@ pub fn test_process_distribute_stake_with_client(client: &RpcClient, sender_keyp
|
|||
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,
|
||||
};
|
||||
|
@ -910,11 +988,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();
|
||||
|
@ -949,7 +1094,7 @@ mod tests {
|
|||
},
|
||||
];
|
||||
assert_eq!(
|
||||
read_allocations(&input_csv, Some(amount)).unwrap(),
|
||||
read_allocations(&input_csv, Some(amount), false).unwrap(),
|
||||
expected_allocations
|
||||
);
|
||||
}
|
||||
|
@ -1059,6 +1204,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,
|
||||
};
|
||||
|
@ -1068,6 +1214,7 @@ mod tests {
|
|||
&new_stake_account_address,
|
||||
&args,
|
||||
Some(lockup_date),
|
||||
false,
|
||||
);
|
||||
let lockup_instruction =
|
||||
bincode::deserialize(&instructions[SET_LOCKUP_INDEX].data).unwrap();
|
||||
|
@ -1107,6 +1254,7 @@ mod tests {
|
|||
transaction_db: "".to_string(),
|
||||
output_path: None,
|
||||
stake_args,
|
||||
spl_token_args: None,
|
||||
transfer_amount: None,
|
||||
};
|
||||
(allocations, args)
|
||||
|
|
|
@ -2,3 +2,5 @@ pub mod arg_parser;
|
|||
pub mod args;
|
||||
pub mod commands;
|
||||
mod db;
|
||||
pub mod spl_token;
|
||||
pub mod token_display;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use solana_cli_config::{Config, CONFIG_FILE};
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
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};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
@ -19,10 +19,12 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
let client = RpcClient::new(json_rpc_url);
|
||||
|
||||
match command_args.command {
|
||||
Command::DistributeTokens(args) => {
|
||||
Command::DistributeTokens(mut args) => {
|
||||
spl_token::update_token_args(&client, &mut args.spl_token_args)?;
|
||||
commands::process_allocations(&client, &args)?;
|
||||
}
|
||||
Command::Balances(args) => {
|
||||
Command::Balances(mut args) => {
|
||||
spl_token::update_decimals(&client, &mut args.spl_token_args)?;
|
||||
commands::process_balances(&client, &args)?;
|
||||
}
|
||||
Command::TransactionLog(args) => {
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
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_client::rpc_client::RpcClient;
|
||||
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 fn update_token_args(client: &RpcClient, 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)
|
||||
.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)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_decimals(client: &RpcClient, args: &mut Option<SplTokenArgs>) -> Result<(), Error> {
|
||||
if let Some(spl_token_args) = args {
|
||||
let mint_account = client.get_account(&spl_token_args.mint).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 fn check_spl_token_balances(
|
||||
num_signatures: usize,
|
||||
allocations: &[Allocation],
|
||||
client: &RpcClient,
|
||||
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 = client.get_recent_blockhash()?.1;
|
||||
let fees = fee_calculator
|
||||
.lamports_per_signature
|
||||
.checked_mul(num_signatures as u64)
|
||||
.unwrap();
|
||||
|
||||
let token_account_rent_exempt_balance =
|
||||
client.get_minimum_balance_for_rent_exemption(SplTokenAccount::LEN)?;
|
||||
let account_creation_amount = created_accounts * token_account_rent_exempt_balance;
|
||||
let fee_payer_balance = client.get_balance(&args.fee_payer.pubkey())?;
|
||||
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)
|
||||
.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 fn print_token_balances(
|
||||
client: &RpcClient,
|
||||
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))
|
||||
.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 {
|
||||
// The following unit tests were written for v1.4 using the ProgramTest framework, passing its
|
||||
// BanksClient into the `solana-tokens` methods. With the revert to RpcClient in this module
|
||||
// (https://github.com/solana-labs/solana/pull/13623), that approach was no longer viable.
|
||||
// These tests were removed rather than rewritten to avoid accruing technical debt. Once a new
|
||||
// rpc/client framework is implemented, they should be restored.
|
||||
//
|
||||
// async fn test_process_spl_token_allocations()
|
||||
// async fn test_process_spl_token_transfer_amount_allocations()
|
||||
// async fn test_check_spl_token_balances()
|
||||
//
|
||||
// TODO: link to v1.4 tests
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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::*;
|
||||
|
|
Loading…
Reference in New Issue