Skip to content

Commit

Permalink
add spl airdrop (#307)
Browse files Browse the repository at this point in the history
* add spl airdrop

* bump to Jib 0.5.0; include priority fee option

* fix Cargo toml file
  • Loading branch information
samuelvanderwaal committed Dec 28, 2023
1 parent 44c6c43 commit 84ee7cb
Show file tree
Hide file tree
Showing 7 changed files with 412 additions and 119 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ futures = "0.3.29"
glob = "0.3.1"
indexmap = { version = "1.9.3", features = ["serde"] }
indicatif = { version = "0.16.2", features = ["rayon"] }
jib = "0.4.1"
jib = "0.5.0"
lazy_static = "1.4.0"
log = "0.4.20"
metaboss_lib = "0.17.0"
mpl-token-metadata = { version = "3.2.3", features = ["serde"] }
num_cpus = "1.16.0"
once_cell = "1.19.0"
phf = { version = "0.10", features = ["macros"] }
ratelimit = "0.4.4"
rayon = "1.8.0"
Expand All @@ -45,7 +46,6 @@ structopt = "0.3.26"
thiserror = "1.0.51"
tokio = "1.35.1"
regex = "1.10.2"
once_cell = "1.19.0"

[features]

Expand Down
127 changes: 13 additions & 114 deletions src/airdrop/mod.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
use std::{collections::HashMap, fs::File, path::PathBuf, str::FromStr};
pub mod sol;
pub mod spl;
pub use sol::*;
pub use spl::*;

use anyhow::Result;
use jib::{Jib, Network};
use log::debug;
use serde::{Deserialize, Serialize};
use solana_client::rpc_client::RpcClient;
use solana_sdk::{pubkey::Pubkey, signer::Signer};
pub use std::{collections::HashMap, fs::File, path::PathBuf, str::FromStr};

use crate::update::{parse_keypair, parse_solana_config};
pub use anyhow::Result;
pub use jib::{Jib, Network};
pub use log::debug;
pub use serde::{Deserialize, Serialize};
pub use solana_client::rpc_client::RpcClient;
pub use solana_sdk::{pubkey::Pubkey, signer::Signer};

pub struct AirdropSolArgs {
pub client: RpcClient,
pub keypair: Option<String>,
pub network: Network,
pub recipient_list: Option<String>,
pub cache_file: Option<String>,
}
pub use crate::update::{parse_keypair, parse_solana_config};

#[derive(Debug, Clone, Deserialize, Serialize)]
struct FailedTransaction {
Expand All @@ -30,102 +27,4 @@ struct Recipient {
amount: u64,
}

pub async fn airdrop_sol(args: AirdropSolArgs) -> Result<()> {
let solana_opts = parse_solana_config();
let keypair = parse_keypair(args.keypair, solana_opts);

let mut jib = Jib::new(vec![keypair], args.network)?;

let mut instructions = vec![];

if args.recipient_list.is_some() && args.cache_file.is_some() {
eprintln!("Cannot provide both a recipient list and a cache file.");
std::process::exit(1);
}

// Get the current time as yyyy-mm-dd-hh-mm-ss
let now = chrono::Local::now();
let timestamp = now.format("%Y-%m-%d-%H-%M-%S").to_string();

let mut cache_file_name = format!("mb-cache-airdrop-{timestamp}.json");
let successful_tx_file_name = format!("mb-successful-airdrops-{timestamp}.json");

let mut airdrop_list: HashMap<String, u64> = if let Some(list_file) = args.recipient_list {
serde_json::from_reader(File::open(list_file)?)?
} else if let Some(cache_file) = args.cache_file {
cache_file_name = PathBuf::from(cache_file.clone())
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string();

let failed_txes: Vec<FailedTransaction> = serde_json::from_reader(File::open(cache_file)?)?;
failed_txes
.iter()
.flat_map(|f| f.recipients.clone())
.collect()
} else {
eprintln!("No recipient list or cache file provided.");
std::process::exit(1);
};

for (address, amount) in &airdrop_list {
let pubkey = match Pubkey::from_str(address) {
Ok(pubkey) => pubkey,
Err(_) => {
eprintln!("Invalid address: {}, skipping...", address);
continue;
}
};

instructions.push(solana_sdk::system_instruction::transfer(
&jib.payer().pubkey(),
&pubkey,
*amount,
));
}

jib.set_instructions(instructions);
let results = jib.hoist()?;

if results.iter().any(|r| r.is_failure()) {
println!("Some transactions failed. Check the {cache_file_name} cache file for details.");
}

let mut successes = vec![];
let mut failures = vec![];

results.iter().for_each(|r| {
if r.is_failure() {
let tx = r.transaction().unwrap(); // Transactions exist on failures.
let account_keys = tx.message().account_keys.clone();
let transaction_accounts = account_keys.iter().map(|k| k.to_string()).collect();

// All accounts except the first and last are recipients.
let recipients: HashMap<String, u64> = account_keys[1..account_keys.len() - 1]
.iter()
.map(|pubkey| pubkey.to_string())
.map(|a| airdrop_list.remove_entry(&a).expect("Recipient not found"))
.collect();

failures.push(FailedTransaction {
transaction_accounts,
recipients,
error: r.error().unwrap(), // Errors exist on failures.
})
} else {
debug!("Transaction successful: {}", r.signature().unwrap()); // Signatures exist on successes.
successes.push(r.signature().unwrap()); // Signatures exist on successes.
}
});

// Write cache file and successful transactions.
let successful_tx_file = std::fs::File::create(successful_tx_file_name)?;
serde_json::to_writer_pretty(successful_tx_file, &successes)?;

let cache_file = std::fs::File::create(cache_file_name)?;
serde_json::to_writer_pretty(cache_file, &failures)?;

Ok(())
}
pub const PRIORITY_FEE: u64 = 25_000;
109 changes: 109 additions & 0 deletions src/airdrop/sol.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use super::*;

pub struct AirdropSolArgs {
pub client: RpcClient,
pub keypair: Option<String>,
pub network: Network,
pub recipient_list: Option<String>,
pub cache_file: Option<String>,
}

pub async fn airdrop_sol(args: AirdropSolArgs) -> Result<()> {
let solana_opts = parse_solana_config();
let keypair = parse_keypair(args.keypair, solana_opts);

let mut jib = Jib::new(vec![keypair], args.network)?;

let mut instructions = vec![];

if args.recipient_list.is_some() && args.cache_file.is_some() {
eprintln!("Cannot provide both a recipient list and a cache file.");
std::process::exit(1);
}

// Get the current time as yyyy-mm-dd-hh-mm-ss
let now = chrono::Local::now();
let timestamp = now.format("%Y-%m-%d-%H-%M-%S").to_string();

let mut cache_file_name = format!("mb-cache-airdrop-{timestamp}.json");
let successful_tx_file_name = format!("mb-successful-airdrops-{timestamp}.json");

let mut airdrop_list: HashMap<String, u64> = if let Some(list_file) = args.recipient_list {
serde_json::from_reader(File::open(list_file)?)?
} else if let Some(cache_file) = args.cache_file {
cache_file_name = PathBuf::from(cache_file.clone())
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string();

let failed_txes: Vec<FailedTransaction> = serde_json::from_reader(File::open(cache_file)?)?;
failed_txes
.iter()
.flat_map(|f| f.recipients.clone())
.collect()
} else {
eprintln!("No recipient list or cache file provided.");
std::process::exit(1);
};

for (address, amount) in &airdrop_list {
let pubkey = match Pubkey::from_str(address) {
Ok(pubkey) => pubkey,
Err(_) => {
eprintln!("Invalid address: {}, skipping...", address);
continue;
}
};

instructions.push(solana_sdk::system_instruction::transfer(
&jib.payer().pubkey(),
&pubkey,
*amount,
));
}

jib.set_instructions(instructions);
let results = jib.hoist()?;

if results.iter().any(|r| r.is_failure()) {
println!("Some transactions failed. Check the {cache_file_name} cache file for details.");
}

let mut successes = vec![];
let mut failures = vec![];

results.iter().for_each(|r| {
if r.is_failure() {
let tx = r.transaction().unwrap(); // Transactions exist on failures.
let account_keys = tx.message().account_keys.clone();
let transaction_accounts = account_keys.iter().map(|k| k.to_string()).collect();

// All accounts except the first and last are recipients.
let recipients: HashMap<String, u64> = account_keys[1..account_keys.len() - 1]
.iter()
.map(|pubkey| pubkey.to_string())
.map(|a| airdrop_list.remove_entry(&a).expect("Recipient not found"))
.collect();

failures.push(FailedTransaction {
transaction_accounts,
recipients,
error: r.error().unwrap(), // Errors exist on failures.
})
} else {
debug!("Transaction successful: {}", r.signature().unwrap()); // Signatures exist on successes.
successes.push(r.signature().unwrap()); // Signatures exist on successes.
}
});

// Write cache file and successful transactions.
let successful_tx_file = std::fs::File::create(successful_tx_file_name)?;
serde_json::to_writer_pretty(successful_tx_file, &successes)?;

let cache_file = std::fs::File::create(cache_file_name)?;
serde_json::to_writer_pretty(cache_file, &failures)?;

Ok(())
}
Loading

0 comments on commit 84ee7cb

Please sign in to comment.