nifty_cli/commands/
mint_batch.rs

1use std::{sync::Arc, time::Duration};
2
3use crate::transaction::pack_instructions;
4
5use super::*;
6
7use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
8use tokio::sync::Mutex;
9
10pub struct MintBatchArgs {
11    pub keypair_path: Option<PathBuf>,
12    pub rpc_url: Option<String>,
13    pub asset_files_dir: PathBuf,
14    pub delay: u64,
15}
16
17pub struct AssetStruct {
18    pub asset: AssetFile,
19    pub asset_sk: Keypair,
20    pub owner: Pubkey,
21}
22
23pub struct AssetResult {
24    pub asset_pubkey: Pubkey,
25    pub asset_name: String,
26    pub tx_result: TxResult,
27}
28
29pub enum TxResult {
30    Success,
31    Failure(String),
32}
33
34impl TxResult {
35    pub fn is_success(&self) -> bool {
36        matches!(self, TxResult::Success)
37    }
38
39    pub fn is_failure(&self) -> bool {
40        matches!(self, TxResult::Failure(_))
41    }
42
43    pub fn get_error(&self) -> Option<&str> {
44        match self {
45            TxResult::Success => None,
46            TxResult::Failure(err) => Some(err),
47        }
48    }
49}
50
51pub async fn handle_mint_batch(args: MintBatchArgs) -> Result<()> {
52    let config = CliConfig::new(args.keypair_path, args.rpc_url)?;
53
54    // Get all JSON asset files in the directory
55    let asset_files = std::fs::read_dir(args.asset_files_dir)?
56        .filter_map(|entry| {
57            let entry = entry.ok()?;
58            let path = entry.path();
59            if path.extension()?.to_str()? == "json" {
60                Some(path)
61            } else {
62                None
63            }
64        })
65        .collect::<Vec<PathBuf>>();
66
67    let authority_sk = config.keypair;
68
69    let mut instructions = vec![];
70
71    let asset_keys = Arc::new(Mutex::new(vec![]));
72    let asset_results = Arc::new(Mutex::new(vec![]));
73
74    for asset_file_path in asset_files {
75        let asset_data: AssetFile = serde_json::from_reader(File::open(asset_file_path)?)?;
76
77        let asset_sk = if let Some(path) = &asset_data.asset_keypair_path {
78            read_keypair_file(path).expect("failed to read keypair file")
79        } else {
80            Keypair::new()
81        };
82
83        let asset = asset_sk.pubkey();
84
85        asset_keys.lock().await.push(asset_sk);
86
87        asset_results.lock().await.push(AssetResult {
88            asset_pubkey: asset,
89            asset_name: asset_data.name.clone(),
90            tx_result: TxResult::Success,
91        });
92
93        let owner = asset_data.owner;
94
95        let accounts = MintAccounts {
96            asset,
97            owner,
98            payer: Some(authority_sk.pubkey()),
99        };
100        let asset_args = AssetArgs {
101            name: asset_data.name,
102            standard: Standard::NonFungible,
103            mutable: asset_data.mutable,
104        };
105
106        let extension_args = asset_data
107            .extensions
108            .iter()
109            .map(|extension| ExtensionArgs {
110                extension_type: extension.extension_type.clone(),
111                data: extension.value.clone().into_data(),
112            })
113            .collect::<Vec<ExtensionArgs>>();
114
115        instructions.push(mint(MintIxArgs {
116            accounts,
117            asset_args,
118            extension_args,
119        })?);
120    }
121
122    // Instructions for each asset must be submitted serially, but we can parallelize the minting of
123    // the assets themselves.
124
125    let mut futures = vec![];
126
127    let client = Arc::new(config.client);
128    let authority_sk = Arc::new(authority_sk);
129
130    let mp: MultiProgress = MultiProgress::new();
131    let sty =
132        ProgressStyle::with_template("[{percent}%] {bar:40.blue/white} {pos:>7}/{len:7} {msg}")
133            .unwrap()
134            .progress_chars("=>-");
135
136    for (i, asset_instructions) in instructions.into_iter().enumerate() {
137        let client = client.clone();
138        let authority_sk = authority_sk.clone();
139        let asset_keys = asset_keys.clone();
140        let asset_results = asset_results.clone();
141
142        let sty_clone = sty.clone();
143        let mp_clone = mp.clone();
144
145        // Each thread concurrently mints a single asset by serially executing all the transactions needed
146        // to create the asset and set its extension data.
147        futures.push(tokio::spawn(async move {
148            // Pack all the instructions for minting this asset into as few transactions as possible.
149            let packed_transactions =
150                pack_instructions(2, &authority_sk.pubkey(), &asset_instructions);
151
152            // Create a progress bar for each asset w/ the number of transactions to send.
153            let pb = mp_clone.add(ProgressBar::new(packed_transactions.len() as u64));
154            pb.set_style(sty_clone.clone());
155
156            let asset_sk = &asset_keys.lock().await[i];
157            let asset_address = &asset_sk.pubkey();
158            pb.set_message(format!("sending transactions for asset {asset_address}"));
159
160            for transaction in packed_transactions {
161                let res = send_and_confirm_tx(&client, &[&authority_sk, &asset_sk], &transaction);
162                pb.inc(1);
163
164                match res {
165                    Ok(_) => continue,
166                    Err(err) => {
167                        let mut results = asset_results.lock().await;
168                        results[i].tx_result = TxResult::Failure(err.to_string());
169                        break;
170                    }
171                }
172            }
173            pb.finish_with_message(format!("asset {asset_address} minted"));
174        }));
175
176        // Sleep for a short time to avoid sending transactions too quickly
177        tokio::time::sleep(Duration::from_millis(args.delay)).await;
178    }
179
180    for future in futures {
181        future.await?;
182    }
183
184    let results = asset_results.lock().await;
185    for result in results.iter() {
186        if result.tx_result.is_failure() {
187            println!(
188                "Failed to mint asset {}: {}",
189                result.asset_name,
190                result.tx_result.get_error().unwrap()
191            );
192        }
193    }
194
195    mp.clear().unwrap();
196
197    Ok(())
198}