#![allow(dead_code)] use crate::{grpc::challenge::Keys, persistence::KeysFile}; use mpl_token_metadata::{ instructions::{CreateMetadataAccountV3, CreateMetadataAccountV3InstructionArgs}, types::DataV2, ID as mpl_token_metadata_id, }; use solana_client::nonblocking::rpc_client::RpcClient; use solana_program::program_pack::Pack; use solana_sdk::{ pubkey::Pubkey, signature::keypair::Keypair, signer::Signer, system_instruction, transaction::Transaction, }; use spl_associated_token_account::{ get_associated_token_address, instruction::create_associated_token_account, }; use spl_token::{ instruction::{initialize_mint, mint_to}, state::Mint, }; use tokio::time::{sleep, Duration}; #[cfg(feature = "test")] const RPC_URL: &str = "https://api.devnet.solana.com"; #[cfg(not(feature = "test"))] const RPC_URL: &str = "https://api.mainnet-beta.solana.com"; pub struct SolClient { client: RpcClient, keypair: Keypair, token: Pubkey, } impl SolClient { pub async fn create_new_token() -> Self { let client = RpcClient::new(RPC_URL.to_string()); let keypair = Keypair::new(); let token = create_token(&client, &keypair).await; Self { client, keypair, token } } pub fn get_keys(&self) -> Keys { Keys { keypair: self.get_keypair_bytes(), token_address: self.get_token_address() } } pub fn get_keys_file(&self) -> KeysFile { self.get_keys().into() } pub async fn mint(&self, recipient: &str) -> Result> { // TODO: add the priority fee for the case when the solana network is congested use std::str::FromStr; let recipient = solana_sdk::pubkey::Pubkey::from_str(recipient)?; let associated_token_address = self.create_token_account(&recipient).await?; let mint_to_instruction = mint_to( &spl_token::id(), &self.token, &associated_token_address, &self.keypair.pubkey(), &[], 1_000_000_000, )?; let transaction = Transaction::new_signed_with_payer( &[mint_to_instruction], Some(&self.keypair.pubkey()), &[&self.keypair], self.client.get_latest_blockhash().await?, ); let signature = self.client.send_and_confirm_transaction(&transaction).await?; Ok(signature.to_string()) } async fn create_token_account( &self, recipient: &Pubkey, ) -> Result> { let address = get_associated_token_address(recipient, &self.token); if self.client.get_account(&address).await.is_err() { let create_token_account_instruction = create_associated_token_account( &self.keypair.pubkey(), recipient, &self.token, &spl_token::id(), ); let recent_blockhash = self.client.get_latest_blockhash().await?; let tx = Transaction::new_signed_with_payer( &[create_token_account_instruction], Some(&self.keypair.pubkey()), &[&self.keypair], recent_blockhash, ); self.client.send_and_confirm_transaction(&tx).await?; } Ok(address) } pub fn get_wallet_pubkey(&self) -> String { // Return the base58 string representation of the public key self.keypair.pubkey().to_string() } pub fn get_token_address(&self) -> String { self.token.to_string() } pub fn get_keypair_bytes(&self) -> Vec { self.keypair.to_bytes().to_vec() } } impl TryFrom for SolClient { type Error = String; fn try_from(keys: Keys) -> Result { use std::str::FromStr; let keypair = match Keypair::from_bytes(&keys.keypair) { Ok(k) => k, Err(_) => return Err("Could not parse keypair.".into()), }; let token = Pubkey::from_str(&keys.token_address) .map_err(|_| "Could not parse wallet address.".to_string())?; Ok(Self { client: RpcClient::new(RPC_URL.to_string()), keypair, token }) } } impl TryFrom for SolClient { type Error = String; fn try_from(keys_file: KeysFile) -> Result { let keys: Keys = keys_file.into(); Self::try_from(keys) } } async fn create_token(client: &RpcClient, payer_keypair: &Keypair) -> Pubkey { // #1 top up the payer println!("Waiting for at least 0.01 SOL in address {}", payer_keypair.pubkey()); while !has_enough_lamports(client, &payer_keypair.pubkey(), 10_000_000).await { sleep(Duration::from_secs(30)).await; } // #2 create token mint println!("Creating Token Mint"); let mut mint_pubkey = None; while mint_pubkey.is_none() { sleep(Duration::from_secs(30)).await; mint_pubkey = create_mint(client, payer_keypair).await; } let mint_pubkey = mint_pubkey.unwrap(); println!("Token Mint created: {mint_pubkey}"); // #3 create token meta println!("Creating Token Metadata"); while !create_meta(client, payer_keypair, &mint_pubkey).await { sleep(Duration::from_secs(30)).await; } println!("Token Metadata created"); mint_pubkey } async fn has_enough_lamports(client: &RpcClient, pubkey: &Pubkey, threshold: u64) -> bool { match client.get_balance(pubkey).await { Ok(balance) => { println!("{pubkey} needs {threshold} and has {balance}"); balance >= threshold } Err(e) => { println!("Could not get balance: {e:?}"); false } } } async fn create_mint(client: &RpcClient, payer_keypair: &Keypair) -> Option { let mint_keypair = Keypair::new(); let mint_rent = client .get_minimum_balance_for_rent_exemption(Mint::LEN) .await .map_err(|e| println!("Can't get minimum balance for rent exemption: {e}")) .ok()?; let create_mint_account_ix = system_instruction::create_account( &payer_keypair.pubkey(), &mint_keypair.pubkey(), mint_rent, Mint::LEN as u64, &spl_token::id(), ); let init_mint_ix = initialize_mint(&spl_token::id(), &mint_keypair.pubkey(), &payer_keypair.pubkey(), None, 9) .map_err(|e| println!("Can't initialize mint: {e}")) .ok()?; let recent_blockhash = client .get_latest_blockhash() .await .map_err(|e| println!("Can't get latest blockhash: {e}")) .ok()?; let tx = Transaction::new_signed_with_payer( &[create_mint_account_ix, init_mint_ix], Some(&payer_keypair.pubkey()), &[&payer_keypair, &mint_keypair], recent_blockhash, ); client .send_and_confirm_transaction(&tx) .await .map_err(|e| println!("Can't execute transaction: {e}")) .map(|s| { println!("Transaction signature: {}", s); mint_keypair.pubkey() }) .ok() } async fn create_meta(client: &RpcClient, payer_keypair: &Keypair, mint_pubkey: &Pubkey) -> bool { create_meta_int(client, payer_keypair, mint_pubkey).await.is_some() } async fn create_meta_int( client: &RpcClient, payer_keypair: &Keypair, mint_pubkey: &Pubkey, ) -> Option<()> { let metadata_seeds = &[b"metadata", mpl_token_metadata_id.as_ref(), mint_pubkey.as_ref()]; let (metadata_pda, _bump) = Pubkey::find_program_address(metadata_seeds, &mpl_token_metadata_id); let data_v2 = DataV2 { name: "DeTEE Hacker Challenge Token".to_string(), symbol: "DTHC".to_string(), uri: "https://detee.ltd/dthc/meta.json".to_string(), seller_fee_basis_points: 0, // usually for NFTs, can be 0 for fungible creators: None, // or Some(vec![Creator { ... }]) if you want to specify creators collection: None, uses: None, }; let create_metadata_account_ix = CreateMetadataAccountV3 { metadata: metadata_pda, mint: *mint_pubkey, mint_authority: payer_keypair.pubkey(), payer: payer_keypair.pubkey(), update_authority: (payer_keypair.pubkey(), true), system_program: solana_program::system_program::id(), rent: None, } .instruction(CreateMetadataAccountV3InstructionArgs { data: data_v2, is_mutable: true, collection_details: None, }); let recent_blockhash = client .get_latest_blockhash() .await .map_err(|e| println!("Can't get latest blockhash: {e}")) .ok()?; let tx = Transaction::new_signed_with_payer( &[create_metadata_account_ix], Some(&payer_keypair.pubkey()), &[&payer_keypair], recent_blockhash, ); client .send_and_confirm_transaction(&tx) .await .map_err(|e| println!("Can't execute transaction: {e}")) .map(|s| { println!("Transaction signature: {}", s); }) .ok() }