Skip to main content

spl_forge/
client.rs

1use std::fs;
2
3use crate::common::paths::path;
4use anyhow::{Result, anyhow};
5use serde::Deserialize;
6use solana_client::rpc_client::RpcClient;
7
8use solana_sdk::{
9    program_pack::Pack,
10    pubkey::Pubkey,
11    signer::{
12        Signer,
13        keypair::{Keypair, read_keypair_file},
14    },
15    system_instruction,
16    transaction::Transaction,
17};
18
19use spl_associated_token_account::{
20    get_associated_token_address, instruction::create_associated_token_account,
21};
22use spl_token::state::Mint;
23
24#[derive(Deserialize)]
25struct AppConfig {
26    json_rpc_url: String,
27    keypair_path: String,
28}
29
30pub struct SplForgeClient {
31    pub client: RpcClient,
32    pub signer: Keypair,
33}
34
35impl SplForgeClient {
36    pub fn new() -> Result<Self> {
37        let config_path = path().map_err(|e| anyhow!("Failed to get config path: {}", e))?;
38        let config_contents = fs::read_to_string(&config_path)
39            .map_err(|e| anyhow!("Failed to read config file at {:?}: {}", config_path, e))?;
40        let app_config: AppConfig = serde_json::from_str(&config_contents)
41            .map_err(|e| anyhow!("Failed to parse config file: {}", e))?;
42        let client = RpcClient::new(app_config.json_rpc_url);
43        let signer = read_keypair_file(&app_config.keypair_path).map_err(|e| {
44            anyhow!(
45                "Failed to read keypair file at {}: {}",
46                app_config.keypair_path,
47                e
48            )
49        })?;
50        Ok(Self { client, signer })
51    }
52
53    pub fn create_mint_account(
54        &self,
55        decimals: u8,
56        mint_authority: Pubkey,
57        freeze_authority: Option<Pubkey>,
58    ) -> Result<Pubkey> {
59        let mint_account = Keypair::new();
60        println!("Creating mint account: {}", mint_account.pubkey());
61        // SPL token mints must use Pack length, not Rust struct size.
62        let mint_size = Mint::LEN;
63        println!("Mint account size: {}", mint_size);
64        let rent = self
65            .client
66            .get_minimum_balance_for_rent_exemption(mint_size)?;
67        println!("Rent exemption amount: {}", rent);
68
69        let create_account_ix = system_instruction::create_account(
70            &self.signer.pubkey(),
71            &mint_account.pubkey(),
72            rent,
73            mint_size as u64,
74            &spl_token::id(),
75        );
76
77        let initialize_mint_ix = spl_token::instruction::initialize_mint(
78            &spl_token::id(),
79            &mint_account.pubkey(),
80            &mint_authority,
81            freeze_authority.as_ref(),
82            decimals,
83        )?;
84
85        let recent_blockhash = self.client.get_latest_blockhash()?;
86        let transaction = Transaction::new_signed_with_payer(
87            &[create_account_ix, initialize_mint_ix],
88            Some(&self.signer.pubkey()),
89            &[&self.signer, &mint_account],
90            recent_blockhash,
91        );
92        self.client.send_and_confirm_transaction(&transaction)?;
93        Ok(mint_account.pubkey())
94    }
95
96    pub fn create_metadata_account(
97        &self,
98        mint: Pubkey,
99        name: String,
100        symbol: String,
101        uri: String,
102        is_mutable: bool,
103    ) -> Result<()> {
104        let _ = (mint, name, symbol, uri, is_mutable);
105        Err(anyhow!(
106            "metadata creation is temporarily disabled while dependency versions are being stabilized"
107        ))
108    }
109
110    pub fn mint_to(&self, mint: Pubkey, amount: u64) -> Result<()> {
111        let signer_pubkey = self.signer.pubkey();
112        let destination_ata = get_associated_token_address(&signer_pubkey, &mint);
113        let mut instructions = vec![];
114
115        // Check if ATA exists
116        if self.client.get_account(&destination_ata).is_err() {
117            instructions.push(create_associated_token_account(
118                &signer_pubkey,
119                &signer_pubkey,
120                &mint,
121                &spl_token::id(),
122            ));
123        }
124
125        // Get mint info to calculate scaled amount
126        let mint_account = self.client.get_account(&mint)?;
127        let mint_data = Mint::unpack(&mint_account.data)?;
128        let scaled_amount = amount * 10_u64.pow(mint_data.decimals as u32);
129
130        instructions.push(spl_token::instruction::mint_to(
131            &spl_token::id(),
132            &mint,
133            &destination_ata,
134            &signer_pubkey,
135            &[],
136            scaled_amount,
137        )?);
138
139        let recent_blockhash = self.client.get_latest_blockhash()?;
140        let transaction = Transaction::new_signed_with_payer(
141            &instructions,
142            Some(&signer_pubkey),
143            &[&self.signer],
144            recent_blockhash,
145        );
146        self.client.send_and_confirm_transaction(&transaction)?;
147        Ok(())
148    }
149
150    pub fn revoke_mint_authority(&self, mint: Pubkey) -> Result<()> {
151        let signer_pubkey = self.signer.pubkey();
152        let revoke_ix = spl_token::instruction::set_authority(
153            &spl_token::id(),
154            &mint,
155            None,
156            spl_token::instruction::AuthorityType::MintTokens,
157            &signer_pubkey,
158            &[&signer_pubkey],
159        )?;
160
161        let recent_blockhash = self.client.get_latest_blockhash()?;
162        let transaction = Transaction::new_signed_with_payer(
163            &[revoke_ix],
164            Some(&signer_pubkey),
165            &[&self.signer],
166            recent_blockhash,
167        );
168        self.client.send_and_confirm_transaction(&transaction)?;
169        Ok(())
170    }
171}