Improve solana-tokens UX (#12253)
* Fix computed banks port * Readme incorrect * Return error if csv cannot be read * Move column headers over columns * Add dry-run check for sender/fee-payer balances * Use clap requires method for paired args * Write transaction-log anytime outfile is specified * Replace campaign-name with required db-path * Remove bids * Exclude new_stake_account_address from logs for non-stake distributions * Fix readme
This commit is contained in:
parent
3930cb865a
commit
90a591da0e
|
@ -83,7 +83,7 @@ impl Config {
|
||||||
}
|
}
|
||||||
let mut url = json_rpc_url.unwrap();
|
let mut url = json_rpc_url.unwrap();
|
||||||
let port = url.port_or_known_default().unwrap_or(80);
|
let port = url.port_or_known_default().unwrap_or(80);
|
||||||
url.set_port(Some(port + 2)).expect("unable to set port");
|
url.set_port(Some(port + 3)).expect("unable to set port");
|
||||||
url.to_string()
|
url.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,21 +138,21 @@ mod test {
|
||||||
fn compute_rpc_banks_url() {
|
fn compute_rpc_banks_url() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Config::compute_rpc_banks_url(&"http://devnet.solana.com"),
|
Config::compute_rpc_banks_url(&"http://devnet.solana.com"),
|
||||||
"http://devnet.solana.com:82/".to_string()
|
"http://devnet.solana.com:83/".to_string()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Config::compute_rpc_banks_url(&"https://devnet.solana.com"),
|
Config::compute_rpc_banks_url(&"https://devnet.solana.com"),
|
||||||
"https://devnet.solana.com:445/".to_string()
|
"https://devnet.solana.com:446/".to_string()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Config::compute_rpc_banks_url(&"http://example.com:8899"),
|
Config::compute_rpc_banks_url(&"http://example.com:8899"),
|
||||||
"http://example.com:8901/".to_string()
|
"http://example.com:8902/".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Config::compute_rpc_banks_url(&"https://example.com:1234"),
|
Config::compute_rpc_banks_url(&"https://example.com:1234"),
|
||||||
"https://example.com:1236/".to_string()
|
"https://example.com:1237/".to_string()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(Config::compute_rpc_banks_url(&"garbage"), String::new());
|
assert_eq!(Config::compute_rpc_banks_url(&"garbage"), String::new());
|
||||||
|
|
|
@ -7,38 +7,38 @@ expected amount are sent. The command-line tool here automates that process.
|
||||||
|
|
||||||
## Distribute tokens
|
## Distribute tokens
|
||||||
|
|
||||||
Send tokens to the recipients in `<BIDS_CSV>`.
|
Send tokens to the recipients in `<RECIPIENTS_CSV>`.
|
||||||
|
|
||||||
Example bids.csv:
|
Example recipients.csv:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
primary_address,bid_amount_dollars
|
recipient,amount,lockup_date
|
||||||
6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv,6.6
|
3ihfUy1n9gaqihM5bJCiTAGLgWc5zo3DqVUS6T736NLM,42.0,
|
||||||
|
CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT,43.0,
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
solana-tokens distribute-tokens --from <KEYPAIR> --dollars-per-sol <NUMBER> --from-bids --input-csv <BIDS_CSV> --fee-payer <KEYPAIR>
|
solana-tokens distribute-tokens --from <KEYPAIR> --input-csv <RECIPIENTS_CSV> --fee-payer <KEYPAIR>
|
||||||
```
|
```
|
||||||
|
|
||||||
Example transaction log before:
|
Example transaction log before:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
recipient,amount,signature
|
recipient,amount,finalized_date,signature
|
||||||
6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv,30,1111111111111111111111111111111111111111111111111111111111111111
|
6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv,70.0,2020-09-15T23:29:26.879747Z,UB168XhBhecxzeD1w2ZRUhwTHpPSqv2WNh8NrZHqz1F2EqxxbSW6iFfVtsg3HkU9NX2cD7R92D8VRLSyArZ9xKQ
|
||||||
```
|
```
|
||||||
|
|
||||||
Send tokens to the recipients in `<BIDS_CSV>` if the distribution is
|
Send tokens to the recipients in `<RECIPIENTS_CSV>` if the distribution is
|
||||||
not already recordered in the transaction log.
|
not already recorded in the transaction log.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
solana-tokens distribute-tokens --from <KEYPAIR> --dollars-per-sol <NUMBER> --from-bids --input-csv <BIDS_CSV> --fee-payer <KEYPAIR>
|
solana-tokens distribute-tokens --from <KEYPAIR> --input-csv <RECIPIENTS_CSV> --fee-payer <KEYPAIR>
|
||||||
```
|
```
|
||||||
|
|
||||||
Example output:
|
Example output:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Recipient Amount
|
Recipient Expected Balance (◎)
|
||||||
6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv 70
|
|
||||||
3ihfUy1n9gaqihM5bJCiTAGLgWc5zo3DqVUS6T736NLM 42
|
3ihfUy1n9gaqihM5bJCiTAGLgWc5zo3DqVUS6T736NLM 42
|
||||||
UKUcTXgbeTYh65RaVV5gSf6xBHevqHvAXMo3e8Q6np8k 43
|
UKUcTXgbeTYh65RaVV5gSf6xBHevqHvAXMo3e8Q6np8k 43
|
||||||
```
|
```
|
||||||
|
@ -52,10 +52,9 @@ solana-tokens transaction-log --output-path transactions.csv
|
||||||
|
|
||||||
```text
|
```text
|
||||||
recipient,amount,signature
|
recipient,amount,signature
|
||||||
6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv,30,1111111111111111111111111111111111111111111111111111111111111111
|
6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv,70.0,2020-09-15T23:29:26.879747Z,UB168XhBhecxzeD1w2ZRUhwTHpPSqv2WNh8NrZHqz1F2EqxxbSW6iFfVtsg3HkU9NX2cD7R92D8VRLSyArZ9xKQ
|
||||||
6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv,70,1111111111111111111111111111111111111111111111111111111111111111
|
3ihfUy1n9gaqihM5bJCiTAGLgWc5zo3DqVUS6T736NLM,42.0,2020-09-15T23:31:50.264241Z,53AVNEVpQBteJBRAKp6naxXsgESDjqe1ge9Dg2HeCSpYWTuGTLqHrBpkHTnpvPJURNgKWxkJfihuRa5STVRjL2hy
|
||||||
3ihfUy1n9gaqihM5bJCiTAGLgWc5zo3DqVUS6T736NLM,42,1111111111111111111111111111111111111111111111111111111111111111
|
CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT,43.0,2020-09-15T23:33:53.680821Z,4XsMfLx9D2ZxVpdJ5xdkV2w4X4SKEQ5zbQhcH4NcRwgZDkdRNiZjvnMFaWaWHUh5eF1LwFPpQdjn6mzSsiCVj3L7
|
||||||
UKUcTXgbeTYh65RaVV5gSf6xBHevqHvAXMo3e8Q6np8k,43,1111111111111111111111111111111111111111111111111111111111111111
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Calculate what tokens should be sent
|
### Calculate what tokens should be sent
|
||||||
|
@ -64,26 +63,23 @@ List the differences between a list of expected distributions and the record of
|
||||||
transactions have already been sent.
|
transactions have already been sent.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
solana-tokens distribute-tokens --dollars-per-sol <NUMBER> --dry-run --from-bids --input-csv <BIDS_CSV>
|
solana-tokens distribute-tokens --dry-run --input-csv <RECIPIENTS_CSV>
|
||||||
```
|
```
|
||||||
|
|
||||||
Example bids.csv:
|
Example recipients.csv:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
primary_address,bid_amount_dollars
|
recipient,amount,lockup_date
|
||||||
6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv,6.6
|
6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv,80,
|
||||||
6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv,15.4
|
7aHDubg5FBYj1SgmyBgU3ZJdtfuqYCQsJQK2pTR5JUqr,42,
|
||||||
3ihfUy1n9gaqihM5bJCiTAGLgWc5zo3DqVUS6T736NLM,9.24
|
|
||||||
UKUcTXgbeTYh65RaVV5gSf6xBHevqHvAXMo3e8Q6np8k,9.46
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Example output:
|
Example output:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Recipient Amount
|
Recipient Expected Balance (◎)
|
||||||
6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv 70
|
6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv 10
|
||||||
3ihfUy1n9gaqihM5bJCiTAGLgWc5zo3DqVUS6T736NLM 42
|
7aHDubg5FBYj1SgmyBgU3ZJdtfuqYCQsJQK2pTR5JUqr 42
|
||||||
UKUcTXgbeTYh65RaVV5gSf6xBHevqHvAXMo3e8Q6np8k 43
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Distribute stake accounts
|
## Distribute stake accounts
|
||||||
|
|
|
@ -41,16 +41,16 @@ where
|
||||||
SubCommand::with_name("distribute-tokens")
|
SubCommand::with_name("distribute-tokens")
|
||||||
.about("Distribute tokens")
|
.about("Distribute tokens")
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("campaign_name")
|
Arg::with_name("db_path")
|
||||||
.long("campaign-name")
|
.long("db-path")
|
||||||
|
.required(true)
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.value_name("NAME")
|
.value_name("FILE")
|
||||||
.help("Campaign name for storing transaction data"),
|
.help(
|
||||||
)
|
"Location for storing distribution database. \
|
||||||
.arg(
|
The database is used for tracking transactions as they are finalized \
|
||||||
Arg::with_name("from_bids")
|
and preventing double spends.",
|
||||||
.long("from-bids")
|
),
|
||||||
.help("Input CSV contains bids in dollars, not allocations in SOL"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("input_csv")
|
Arg::with_name("input_csv")
|
||||||
|
@ -60,18 +60,19 @@ where
|
||||||
.value_name("FILE")
|
.value_name("FILE")
|
||||||
.help("Input CSV file"),
|
.help("Input CSV file"),
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::with_name("dollars_per_sol")
|
|
||||||
.long("dollars-per-sol")
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("NUMBER")
|
|
||||||
.help("Dollars per SOL, if input CSV contains bids"),
|
|
||||||
)
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("dry_run")
|
Arg::with_name("dry_run")
|
||||||
.long("dry-run")
|
.long("dry-run")
|
||||||
.help("Do not execute any transfers"),
|
.help("Do not execute any transfers"),
|
||||||
)
|
)
|
||||||
|
.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(
|
||||||
Arg::with_name("sender_keypair")
|
Arg::with_name("sender_keypair")
|
||||||
.long("from")
|
.long("from")
|
||||||
|
@ -95,11 +96,16 @@ where
|
||||||
SubCommand::with_name("distribute-stake")
|
SubCommand::with_name("distribute-stake")
|
||||||
.about("Distribute stake accounts")
|
.about("Distribute stake accounts")
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("campaign_name")
|
Arg::with_name("db_path")
|
||||||
.long("campaign-name")
|
.long("db-path")
|
||||||
|
.required(true)
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.value_name("NAME")
|
.value_name("FILE")
|
||||||
.help("Campaign name for storing transaction data"),
|
.help(
|
||||||
|
"Location for storing distribution database. \
|
||||||
|
The database is used for tracking transactions as they are finalized \
|
||||||
|
and preventing double spends.",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("input_csv")
|
Arg::with_name("input_csv")
|
||||||
|
@ -114,6 +120,14 @@ where
|
||||||
.long("dry-run")
|
.long("dry-run")
|
||||||
.help("Do not execute any transfers"),
|
.help("Do not execute any transfers"),
|
||||||
)
|
)
|
||||||
|
.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(
|
||||||
Arg::with_name("sender_keypair")
|
Arg::with_name("sender_keypair")
|
||||||
.long("from")
|
.long("from")
|
||||||
|
@ -186,29 +200,18 @@ where
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.value_name("FILE")
|
.value_name("FILE")
|
||||||
.help("Bids CSV file"),
|
.help("Bids CSV file"),
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("from_bids")
|
|
||||||
.long("from-bids")
|
|
||||||
.help("Input CSV contains bids in dollars, not allocations in SOL"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("dollars_per_sol")
|
|
||||||
.long("dollars-per-sol")
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("NUMBER")
|
|
||||||
.help("Dollars per SOL"),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("transaction-log")
|
SubCommand::with_name("transaction-log")
|
||||||
.about("Print the database to a CSV file")
|
.about("Print the database to a CSV file")
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("campaign_name")
|
Arg::with_name("db_path")
|
||||||
.long("campaign-name")
|
.long("db-path")
|
||||||
|
.required(true)
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.value_name("NAME")
|
.value_name("FILE")
|
||||||
.help("Campaign name for storing transaction data"),
|
.help("Location of database to query"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("output_path")
|
Arg::with_name("output_path")
|
||||||
|
@ -222,22 +225,6 @@ where
|
||||||
.get_matches_from(args)
|
.get_matches_from(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_db_path(campaign_name: Option<String>) -> String {
|
|
||||||
let (prefix, hyphen) = if let Some(name) = campaign_name {
|
|
||||||
(name, "-")
|
|
||||||
} else {
|
|
||||||
("".to_string(), "")
|
|
||||||
};
|
|
||||||
let path = dirs::home_dir().unwrap();
|
|
||||||
let filename = format!("{}{}transactions.db", prefix, hyphen);
|
|
||||||
path.join(".config")
|
|
||||||
.join("solana-tokens")
|
|
||||||
.join(filename)
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_distribute_tokens_args(
|
fn parse_distribute_tokens_args(
|
||||||
matches: &ArgMatches<'_>,
|
matches: &ArgMatches<'_>,
|
||||||
) -> Result<DistributeTokensArgs, Box<dyn Error>> {
|
) -> Result<DistributeTokensArgs, Box<dyn Error>> {
|
||||||
|
@ -262,9 +249,8 @@ fn parse_distribute_tokens_args(
|
||||||
|
|
||||||
Ok(DistributeTokensArgs {
|
Ok(DistributeTokensArgs {
|
||||||
input_csv: value_t_or_exit!(matches, "input_csv", String),
|
input_csv: value_t_or_exit!(matches, "input_csv", String),
|
||||||
from_bids: matches.is_present("from_bids"),
|
transaction_db: value_t_or_exit!(matches, "db_path", String),
|
||||||
transaction_db: create_db_path(value_t!(matches, "campaign_name", String).ok()),
|
output_path: matches.value_of("output_path").map(|path| path.to_string()),
|
||||||
dollars_per_sol: value_t!(matches, "dollars_per_sol", f64).ok(),
|
|
||||||
dry_run: matches.is_present("dry_run"),
|
dry_run: matches.is_present("dry_run"),
|
||||||
sender_keypair,
|
sender_keypair,
|
||||||
fee_payer,
|
fee_payer,
|
||||||
|
@ -338,9 +324,8 @@ fn parse_distribute_stake_args(
|
||||||
};
|
};
|
||||||
Ok(DistributeTokensArgs {
|
Ok(DistributeTokensArgs {
|
||||||
input_csv: value_t_or_exit!(matches, "input_csv", String),
|
input_csv: value_t_or_exit!(matches, "input_csv", String),
|
||||||
from_bids: false,
|
transaction_db: value_t_or_exit!(matches, "db_path", String),
|
||||||
transaction_db: create_db_path(value_t!(matches, "campaign_name", String).ok()),
|
output_path: matches.value_of("output_path").map(|path| path.to_string()),
|
||||||
dollars_per_sol: None,
|
|
||||||
dry_run: matches.is_present("dry_run"),
|
dry_run: matches.is_present("dry_run"),
|
||||||
sender_keypair,
|
sender_keypair,
|
||||||
fee_payer,
|
fee_payer,
|
||||||
|
@ -351,14 +336,12 @@ fn parse_distribute_stake_args(
|
||||||
fn parse_balances_args(matches: &ArgMatches<'_>) -> BalancesArgs {
|
fn parse_balances_args(matches: &ArgMatches<'_>) -> BalancesArgs {
|
||||||
BalancesArgs {
|
BalancesArgs {
|
||||||
input_csv: value_t_or_exit!(matches, "input_csv", String),
|
input_csv: value_t_or_exit!(matches, "input_csv", String),
|
||||||
from_bids: matches.is_present("from_bids"),
|
|
||||||
dollars_per_sol: value_t!(matches, "dollars_per_sol", f64).ok(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_transaction_log_args(matches: &ArgMatches<'_>) -> TransactionLogArgs {
|
fn parse_transaction_log_args(matches: &ArgMatches<'_>) -> TransactionLogArgs {
|
||||||
TransactionLogArgs {
|
TransactionLogArgs {
|
||||||
transaction_db: create_db_path(value_t!(matches, "campaign_name", String).ok()),
|
transaction_db: value_t_or_exit!(matches, "db_path", String),
|
||||||
output_path: value_t_or_exit!(matches, "output_path", String),
|
output_path: value_t_or_exit!(matches, "output_path", String),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,8 @@ use solana_sdk::{pubkey::Pubkey, signature::Signer};
|
||||||
|
|
||||||
pub struct DistributeTokensArgs {
|
pub struct DistributeTokensArgs {
|
||||||
pub input_csv: String,
|
pub input_csv: String,
|
||||||
pub from_bids: bool,
|
|
||||||
pub transaction_db: String,
|
pub transaction_db: String,
|
||||||
pub dollars_per_sol: Option<f64>,
|
pub output_path: Option<String>,
|
||||||
pub dry_run: bool,
|
pub dry_run: bool,
|
||||||
pub sender_keypair: Box<dyn Signer>,
|
pub sender_keypair: Box<dyn Signer>,
|
||||||
pub fee_payer: Box<dyn Signer>,
|
pub fee_payer: Box<dyn Signer>,
|
||||||
|
@ -21,8 +20,6 @@ pub struct StakeArgs {
|
||||||
|
|
||||||
pub struct BalancesArgs {
|
pub struct BalancesArgs {
|
||||||
pub input_csv: String,
|
pub input_csv: String,
|
||||||
pub from_bids: bool,
|
|
||||||
pub dollars_per_sol: Option<f64>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TransactionLogArgs {
|
pub struct TransactionLogArgs {
|
||||||
|
|
|
@ -54,6 +54,12 @@ pub enum Error {
|
||||||
TransportError(#[from] TransportError),
|
TransportError(#[from] TransportError),
|
||||||
#[error("Missing lockup authority")]
|
#[error("Missing lockup authority")]
|
||||||
MissingLockupAuthority,
|
MissingLockupAuthority,
|
||||||
|
#[error("insufficient funds for fee ({0} SOL)")]
|
||||||
|
InsufficientFundsForFees(f64),
|
||||||
|
#[error("insufficient funds for distribution ({0} SOL)")]
|
||||||
|
InsufficientFundsForDistribution(f64),
|
||||||
|
#[error("insufficient funds for distribution ({0} SOL) and fee ({1} SOL)")]
|
||||||
|
InsufficientFundsForDistributionAndFees(f64, f64),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_allocations(allocations: &[Allocation]) -> Vec<Allocation> {
|
fn merge_allocations(allocations: &[Allocation]) -> Vec<Allocation> {
|
||||||
|
@ -99,14 +105,6 @@ fn apply_previous_transactions(
|
||||||
allocations.retain(|x| x.amount > 0.5);
|
allocations.retain(|x| x.amount > 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_allocation(bid: &Bid, dollars_per_sol: f64) -> Allocation {
|
|
||||||
Allocation {
|
|
||||||
recipient: bid.primary_address.clone(),
|
|
||||||
amount: bid.accepted_amount_dollars / dollars_per_sol,
|
|
||||||
lockup_date: "".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn transfer<S: Signer>(
|
async fn transfer<S: Signer>(
|
||||||
client: &mut BanksClient,
|
client: &mut BanksClient,
|
||||||
lamports: u64,
|
lamports: u64,
|
||||||
|
@ -203,6 +201,7 @@ async fn distribute_allocations(
|
||||||
allocations: &[Allocation],
|
allocations: &[Allocation],
|
||||||
args: &DistributeTokensArgs,
|
args: &DistributeTokensArgs,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
let mut num_signatures = 0;
|
||||||
for allocation in allocations {
|
for allocation in allocations {
|
||||||
let new_stake_account_keypair = Keypair::new();
|
let new_stake_account_keypair = Keypair::new();
|
||||||
let new_stake_account_address = new_stake_account_keypair.pubkey();
|
let new_stake_account_address = new_stake_account_keypair.pubkey();
|
||||||
|
@ -221,6 +220,7 @@ async fn distribute_allocations(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let signers = unique_signers(signers);
|
let signers = unique_signers(signers);
|
||||||
|
num_signatures += signers.len();
|
||||||
|
|
||||||
let lockup_date = if allocation.lockup_date == "" {
|
let lockup_date = if allocation.lockup_date == "" {
|
||||||
None
|
None
|
||||||
|
@ -250,7 +250,7 @@ async fn distribute_allocations(
|
||||||
&allocation.recipient.parse().unwrap(),
|
&allocation.recipient.parse().unwrap(),
|
||||||
allocation.amount,
|
allocation.amount,
|
||||||
&transaction,
|
&transaction,
|
||||||
Some(&new_stake_account_address),
|
args.stake_args.as_ref().map(|_| &new_stake_account_address),
|
||||||
false,
|
false,
|
||||||
last_valid_slot,
|
last_valid_slot,
|
||||||
lockup_date,
|
lockup_date,
|
||||||
|
@ -261,26 +261,22 @@ async fn distribute_allocations(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if args.dry_run {
|
||||||
|
let undistributed_tokens: f64 = allocations.iter().map(|x| x.amount).sum();
|
||||||
|
check_payer_balances(
|
||||||
|
num_signatures,
|
||||||
|
sol_to_lamports(undistributed_tokens),
|
||||||
|
client,
|
||||||
|
args,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_allocations(
|
fn read_allocations(input_csv: &str) -> io::Result<Vec<Allocation>> {
|
||||||
input_csv: &str,
|
let mut rdr = ReaderBuilder::new().trim(Trim::All).from_path(input_csv)?;
|
||||||
from_bids: bool,
|
Ok(rdr.deserialize().map(|entry| entry.unwrap()).collect())
|
||||||
dollars_per_sol: Option<f64>,
|
|
||||||
) -> Vec<Allocation> {
|
|
||||||
let rdr = ReaderBuilder::new().trim(Trim::All).from_path(input_csv);
|
|
||||||
if from_bids {
|
|
||||||
let bids: Vec<Bid> = rdr.unwrap().deserialize().map(|bid| bid.unwrap()).collect();
|
|
||||||
bids.into_iter()
|
|
||||||
.map(|bid| create_allocation(&bid, dollars_per_sol.unwrap()))
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
rdr.unwrap()
|
|
||||||
.deserialize()
|
|
||||||
.map(|entry| entry.unwrap())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_spinner_progress_bar() -> ProgressBar {
|
fn new_spinner_progress_bar() -> ProgressBar {
|
||||||
|
@ -295,8 +291,7 @@ pub async fn process_allocations(
|
||||||
client: &mut BanksClient,
|
client: &mut BanksClient,
|
||||||
args: &DistributeTokensArgs,
|
args: &DistributeTokensArgs,
|
||||||
) -> Result<Option<usize>, Error> {
|
) -> Result<Option<usize>, Error> {
|
||||||
let mut allocations: Vec<Allocation> =
|
let mut allocations: Vec<Allocation> = read_allocations(&args.input_csv)?;
|
||||||
read_allocations(&args.input_csv, args.from_bids, args.dollars_per_sol);
|
|
||||||
|
|
||||||
let starting_total_tokens: f64 = allocations.iter().map(|x| x.amount).sum();
|
let starting_total_tokens: f64 = allocations.iter().map(|x| x.amount).sum();
|
||||||
println!(
|
println!(
|
||||||
|
@ -304,13 +299,6 @@ pub async fn process_allocations(
|
||||||
style("Total in input_csv:").bold(),
|
style("Total in input_csv:").bold(),
|
||||||
starting_total_tokens,
|
starting_total_tokens,
|
||||||
);
|
);
|
||||||
if let Some(dollars_per_sol) = args.dollars_per_sol {
|
|
||||||
println!(
|
|
||||||
"{} ${}",
|
|
||||||
style("Total in input_csv:").bold(),
|
|
||||||
starting_total_tokens * dollars_per_sol,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut db = db::open_db(&args.transaction_db, args.dry_run)?;
|
let mut db = db::open_db(&args.transaction_db, args.dry_run)?;
|
||||||
|
|
||||||
|
@ -325,6 +313,20 @@ pub async fn process_allocations(
|
||||||
return Ok(confirmations);
|
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,);
|
||||||
|
println!(
|
||||||
|
"{} ◎{}",
|
||||||
|
style("Undistributed:").bold(),
|
||||||
|
undistributed_tokens,
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"{} ◎{}",
|
||||||
|
style("Total:").bold(),
|
||||||
|
distributed_tokens + undistributed_tokens,
|
||||||
|
);
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
style(format!(
|
style(format!(
|
||||||
|
@ -334,44 +336,16 @@ pub async fn process_allocations(
|
||||||
.bold()
|
.bold()
|
||||||
);
|
);
|
||||||
|
|
||||||
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,);
|
|
||||||
if let Some(dollars_per_sol) = args.dollars_per_sol {
|
|
||||||
println!(
|
|
||||||
"{} ${}",
|
|
||||||
style("Distributed:").bold(),
|
|
||||||
distributed_tokens * dollars_per_sol,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
println!(
|
|
||||||
"{} ◎{}",
|
|
||||||
style("Undistributed:").bold(),
|
|
||||||
undistributed_tokens,
|
|
||||||
);
|
|
||||||
if let Some(dollars_per_sol) = args.dollars_per_sol {
|
|
||||||
println!(
|
|
||||||
"{} ${}",
|
|
||||||
style("Undistributed:").bold(),
|
|
||||||
undistributed_tokens * dollars_per_sol,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
println!(
|
|
||||||
"{} ◎{}",
|
|
||||||
style("Total:").bold(),
|
|
||||||
distributed_tokens + undistributed_tokens,
|
|
||||||
);
|
|
||||||
if let Some(dollars_per_sol) = args.dollars_per_sol {
|
|
||||||
println!(
|
|
||||||
"{} ${}",
|
|
||||||
style("Total:").bold(),
|
|
||||||
(distributed_tokens + undistributed_tokens) * dollars_per_sol,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
distribute_allocations(client, &mut db, &allocations, args).await?;
|
distribute_allocations(client, &mut db, &allocations, args).await?;
|
||||||
|
|
||||||
let opt_confirmations = finalize_transactions(client, &mut db, args.dry_run).await?;
|
let opt_confirmations = finalize_transactions(client, &mut db, args.dry_run).await?;
|
||||||
|
|
||||||
|
if !args.dry_run {
|
||||||
|
if let Some(output_path) = &args.output_path {
|
||||||
|
db::write_transaction_log(&db, &output_path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(opt_confirmations)
|
Ok(opt_confirmations)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -455,12 +429,45 @@ async fn update_finalized_transactions(
|
||||||
Ok(confirmations)
|
Ok(confirmations)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_payer_balances(
|
||||||
|
num_signatures: usize,
|
||||||
|
allocation_lamports: u64,
|
||||||
|
client: &mut BanksClient,
|
||||||
|
args: &DistributeTokensArgs,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
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();
|
||||||
|
if args.fee_payer.pubkey() == args.sender_keypair.pubkey() {
|
||||||
|
let balance = client.get_balance(args.fee_payer.pubkey()).await?;
|
||||||
|
if balance < fees + allocation_lamports {
|
||||||
|
return Err(Error::InsufficientFundsForDistributionAndFees(
|
||||||
|
lamports_to_sol(allocation_lamports),
|
||||||
|
lamports_to_sol(fees),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let fee_payer_balance = client.get_balance(args.fee_payer.pubkey()).await?;
|
||||||
|
if fee_payer_balance < fees {
|
||||||
|
return Err(Error::InsufficientFundsForFees(lamports_to_sol(fees)));
|
||||||
|
}
|
||||||
|
let sender_balance = client.get_balance(args.sender_keypair.pubkey()).await?;
|
||||||
|
if sender_balance < allocation_lamports {
|
||||||
|
return Err(Error::InsufficientFundsForDistribution(lamports_to_sol(
|
||||||
|
allocation_lamports,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn process_balances(
|
pub async fn process_balances(
|
||||||
client: &mut BanksClient,
|
client: &mut BanksClient,
|
||||||
args: &BalancesArgs,
|
args: &BalancesArgs,
|
||||||
) -> Result<(), csv::Error> {
|
) -> Result<(), csv::Error> {
|
||||||
let allocations: Vec<Allocation> =
|
let allocations: Vec<Allocation> = read_allocations(&args.input_csv)?;
|
||||||
read_allocations(&args.input_csv, args.from_bids, args.dollars_per_sol);
|
|
||||||
let allocations = merge_allocations(&allocations);
|
let allocations = merge_allocations(&allocations);
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
|
@ -494,6 +501,7 @@ pub fn process_transaction_log(args: &TransactionLogArgs) -> Result<(), Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use crate::db::check_output_file;
|
||||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||||
use tempfile::{tempdir, NamedTempFile};
|
use tempfile::{tempdir, NamedTempFile};
|
||||||
pub async fn test_process_distribute_tokens_with_client(
|
pub async fn test_process_distribute_tokens_with_client(
|
||||||
|
@ -541,14 +549,16 @@ pub async fn test_process_distribute_tokens_with_client(
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
|
let output_file = NamedTempFile::new().unwrap();
|
||||||
|
let output_path = output_file.path().to_str().unwrap().to_string();
|
||||||
|
|
||||||
let args = DistributeTokensArgs {
|
let args = DistributeTokensArgs {
|
||||||
sender_keypair: Box::new(sender_keypair),
|
sender_keypair: Box::new(sender_keypair),
|
||||||
fee_payer: Box::new(fee_payer),
|
fee_payer: Box::new(fee_payer),
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
input_csv,
|
input_csv,
|
||||||
from_bids: false,
|
|
||||||
transaction_db: transaction_db.clone(),
|
transaction_db: transaction_db.clone(),
|
||||||
dollars_per_sol: None,
|
output_path: Some(output_path.clone()),
|
||||||
stake_args: None,
|
stake_args: None,
|
||||||
};
|
};
|
||||||
let confirmations = process_allocations(client, &args).await.unwrap();
|
let confirmations = process_allocations(client, &args).await.unwrap();
|
||||||
|
@ -569,6 +579,8 @@ pub async fn test_process_distribute_tokens_with_client(
|
||||||
expected_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.
|
// Now, run it again, and check there's no double-spend.
|
||||||
process_allocations(client, &args).await.unwrap();
|
process_allocations(client, &args).await.unwrap();
|
||||||
let transaction_infos =
|
let transaction_infos =
|
||||||
|
@ -585,6 +597,8 @@ pub async fn test_process_distribute_tokens_with_client(
|
||||||
client.get_balance(alice_pubkey).await.unwrap(),
|
client.get_balance(alice_pubkey).await.unwrap(),
|
||||||
expected_amount,
|
expected_amount,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
check_output_file(&output_path, &db::open_db(&transaction_db, true).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn test_process_distribute_stake_with_client(
|
pub async fn test_process_distribute_stake_with_client(
|
||||||
|
@ -651,6 +665,9 @@ pub async fn test_process_distribute_stake_with_client(
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
|
let output_file = NamedTempFile::new().unwrap();
|
||||||
|
let output_path = output_file.path().to_str().unwrap().to_string();
|
||||||
|
|
||||||
let stake_args = StakeArgs {
|
let stake_args = StakeArgs {
|
||||||
stake_account_address,
|
stake_account_address,
|
||||||
stake_authority: Box::new(stake_authority),
|
stake_authority: Box::new(stake_authority),
|
||||||
|
@ -663,10 +680,9 @@ pub async fn test_process_distribute_stake_with_client(
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
input_csv,
|
input_csv,
|
||||||
transaction_db: transaction_db.clone(),
|
transaction_db: transaction_db.clone(),
|
||||||
|
output_path: Some(output_path.clone()),
|
||||||
stake_args: Some(stake_args),
|
stake_args: Some(stake_args),
|
||||||
from_bids: false,
|
|
||||||
sender_keypair: Box::new(sender_keypair),
|
sender_keypair: Box::new(sender_keypair),
|
||||||
dollars_per_sol: None,
|
|
||||||
};
|
};
|
||||||
let confirmations = process_allocations(client, &args).await.unwrap();
|
let confirmations = process_allocations(client, &args).await.unwrap();
|
||||||
assert_eq!(confirmations, None);
|
assert_eq!(confirmations, None);
|
||||||
|
@ -691,6 +707,8 @@ pub async fn test_process_distribute_stake_with_client(
|
||||||
expected_amount - sol_to_lamports(1.0),
|
expected_amount - sol_to_lamports(1.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
check_output_file(&output_path, &db::open_db(&transaction_db, true).unwrap());
|
||||||
|
|
||||||
// Now, run it again, and check there's no double-spend.
|
// Now, run it again, and check there's no double-spend.
|
||||||
process_allocations(client, &args).await.unwrap();
|
process_allocations(client, &args).await.unwrap();
|
||||||
let transaction_infos =
|
let transaction_infos =
|
||||||
|
@ -711,6 +729,8 @@ pub async fn test_process_distribute_stake_with_client(
|
||||||
client.get_balance(new_stake_account_address).await.unwrap(),
|
client.get_balance(new_stake_account_address).await.unwrap(),
|
||||||
expected_amount - sol_to_lamports(1.0),
|
expected_amount - sol_to_lamports(1.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
check_output_file(&output_path, &db::open_db(&transaction_db, true).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -760,31 +780,7 @@ mod tests {
|
||||||
wtr.serialize(&allocation).unwrap();
|
wtr.serialize(&allocation).unwrap();
|
||||||
wtr.flush().unwrap();
|
wtr.flush().unwrap();
|
||||||
|
|
||||||
assert_eq!(read_allocations(&input_csv, false, None), vec![allocation]);
|
assert_eq!(read_allocations(&input_csv).unwrap(), vec![allocation]);
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_read_allocations_from_bids() {
|
|
||||||
let alice_pubkey = Pubkey::new_rand();
|
|
||||||
let bid = Bid {
|
|
||||||
primary_address: alice_pubkey.to_string(),
|
|
||||||
accepted_amount_dollars: 42.0,
|
|
||||||
};
|
|
||||||
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(&bid).unwrap();
|
|
||||||
wtr.flush().unwrap();
|
|
||||||
|
|
||||||
let allocation = Allocation {
|
|
||||||
recipient: bid.primary_address,
|
|
||||||
amount: 84.0,
|
|
||||||
lockup_date: "".to_string(),
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
read_allocations(&input_csv, true, Some(0.5)),
|
|
||||||
vec![allocation]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -890,10 +886,9 @@ mod tests {
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
input_csv: "".to_string(),
|
input_csv: "".to_string(),
|
||||||
transaction_db: "".to_string(),
|
transaction_db: "".to_string(),
|
||||||
|
output_path: None,
|
||||||
stake_args: Some(stake_args),
|
stake_args: Some(stake_args),
|
||||||
from_bids: false,
|
|
||||||
sender_keypair: Box::new(Keypair::new()),
|
sender_keypair: Box::new(Keypair::new()),
|
||||||
dollars_per_sol: None,
|
|
||||||
};
|
};
|
||||||
let lockup_date = lockup_date_str.parse().unwrap();
|
let lockup_date = lockup_date_str.parse().unwrap();
|
||||||
let instructions = distribution_instructions(
|
let instructions = distribution_instructions(
|
||||||
|
|
|
@ -20,6 +20,7 @@ pub struct TransactionInfo {
|
||||||
struct SignedTransactionInfo {
|
struct SignedTransactionInfo {
|
||||||
recipient: String,
|
recipient: String,
|
||||||
amount: f64,
|
amount: f64,
|
||||||
|
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||||
new_stake_account_address: String,
|
new_stake_account_address: String,
|
||||||
finalized_date: Option<DateTime<Utc>>,
|
finalized_date: Option<DateTime<Utc>>,
|
||||||
signature: String,
|
signature: String,
|
||||||
|
@ -178,6 +179,33 @@ pub fn update_finalized_transaction(
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use csv::{ReaderBuilder, Trim};
|
||||||
|
pub(crate) fn check_output_file(path: &str, db: &PickleDb) {
|
||||||
|
let mut rdr = ReaderBuilder::new()
|
||||||
|
.trim(Trim::All)
|
||||||
|
.from_path(path)
|
||||||
|
.unwrap();
|
||||||
|
let logged_infos: Vec<SignedTransactionInfo> =
|
||||||
|
rdr.deserialize().map(|entry| entry.unwrap()).collect();
|
||||||
|
|
||||||
|
let mut transaction_infos = read_transaction_infos(db);
|
||||||
|
transaction_infos.sort_by(compare_transaction_infos);
|
||||||
|
let transaction_infos: Vec<SignedTransactionInfo> = transaction_infos
|
||||||
|
.iter()
|
||||||
|
.map(|info| SignedTransactionInfo {
|
||||||
|
recipient: info.recipient.to_string(),
|
||||||
|
amount: info.amount,
|
||||||
|
new_stake_account_address: info
|
||||||
|
.new_stake_account_address
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.unwrap_or_else(|| "".to_string()),
|
||||||
|
finalized_date: info.finalized_date,
|
||||||
|
signature: info.transaction.signatures[0].to_string(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert_eq!(logged_infos, transaction_infos);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
Loading…
Reference in New Issue