281 lines
8.9 KiB
Rust
281 lines
8.9 KiB
Rust
#![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<String, Box<dyn std::error::Error>> {
|
|
// 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<Pubkey, Box<dyn std::error::Error>> {
|
|
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<u8> {
|
|
self.keypair.to_bytes().to_vec()
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Keys> for SolClient {
|
|
type Error = String;
|
|
|
|
fn try_from(keys: Keys) -> Result<Self, Self::Error> {
|
|
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<KeysFile> for SolClient {
|
|
type Error = String;
|
|
|
|
fn try_from(keys_file: KeysFile) -> Result<Self, Self::Error> {
|
|
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<Pubkey> {
|
|
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()
|
|
}
|