cli/commands/wallet/
balance.rs

1use anyhow::Context;
2use mina_node_account::AccountSecretKey;
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, clap::Args)]
7pub struct Balance {
8    /// Public key to query the balance for
9    #[arg(long, conflicts_with = "from")]
10    pub address: Option<String>,
11
12    /// Path to encrypted key file
13    #[arg(long, conflicts_with = "address")]
14    pub from: Option<PathBuf>,
15
16    /// Password to decrypt the key
17    #[arg(
18        env = "MINA_PRIVKEY_PASS",
19        default_value = "",
20        help = "Password to decrypt the key (env: MINA_PRIVKEY_PASS)"
21    )]
22    pub password: String,
23
24    /// GraphQL endpoint URL
25    #[arg(
26        long,
27        default_value = "http://localhost:3000/graphql",
28        help = "GraphQL endpoint URL"
29    )]
30    pub endpoint: String,
31
32    /// Output format (text or json)
33    #[arg(long, default_value = "text")]
34    pub format: OutputFormat,
35}
36
37#[derive(Debug, Clone, clap::ValueEnum)]
38pub enum OutputFormat {
39    Text,
40    Json,
41}
42
43#[derive(Serialize)]
44struct GraphQLRequest {
45    query: String,
46    variables: serde_json::Value,
47}
48
49#[derive(Deserialize, Debug)]
50struct GraphQLResponse {
51    data: Option<DataResponse>,
52    errors: Option<Vec<GraphQLError>>,
53}
54
55#[derive(Deserialize, Debug)]
56struct DataResponse {
57    account: Option<AccountResponse>,
58}
59
60#[derive(Deserialize, Debug)]
61struct AccountResponse {
62    balance: BalanceResponse,
63    nonce: String,
64    #[serde(rename = "delegateAccount")]
65    delegate_account: Option<DelegateAccount>,
66}
67
68#[derive(Deserialize, Debug)]
69struct BalanceResponse {
70    total: String,
71    liquid: Option<String>,
72    locked: Option<String>,
73}
74
75#[derive(Deserialize, Debug)]
76struct DelegateAccount {
77    #[serde(rename = "publicKey")]
78    public_key: String,
79}
80
81#[derive(Deserialize, Debug)]
82struct GraphQLError {
83    message: String,
84}
85
86#[derive(Serialize, Debug)]
87struct BalanceOutput {
88    account: String,
89    balance: BalanceOutputData,
90    nonce: String,
91    delegate: Option<String>,
92}
93
94#[derive(Serialize, Debug)]
95struct BalanceOutputData {
96    total: String,
97    total_mina: String,
98    liquid: Option<String>,
99    liquid_mina: Option<String>,
100    locked: Option<String>,
101    locked_mina: Option<String>,
102}
103
104impl Balance {
105    pub fn run(self) -> anyhow::Result<()> {
106        // Get the public key either from address or from key file
107        let public_key = if let Some(address) = self.address {
108            address
109        } else if let Some(from) = self.from {
110            if self.password.is_empty() {
111                anyhow::bail!(
112                    "Password is required when using --from. Provide it via --password argument or MINA_PRIVKEY_PASS environment variable"
113                );
114            }
115            let secret_key = AccountSecretKey::from_encrypted_file(&from, &self.password)
116                .with_context(|| format!("Failed to decrypt key file: {}", from.display()))?;
117            secret_key.public_key().to_string()
118        } else {
119            anyhow::bail!("Either --address or --from must be provided to specify the account");
120        };
121
122        // GraphQL query
123        let query = r#"
124            query GetBalance($publicKey: String!) {
125                account(publicKey: $publicKey) {
126                    balance {
127                        total
128                        liquid
129                        locked
130                    }
131                    nonce
132                    delegateAccount {
133                        publicKey
134                    }
135                }
136            }
137        "#;
138
139        let variables = serde_json::json!({
140            "publicKey": public_key
141        });
142
143        let request = GraphQLRequest {
144            query: query.to_string(),
145            variables,
146        };
147
148        // Make the GraphQL request
149        let client = reqwest::blocking::Client::new();
150        let response = client
151            .post(&self.endpoint)
152            .json(&request)
153            .send()
154            .with_context(|| format!("Failed to connect to GraphQL endpoint: {}", self.endpoint))?;
155
156        if !response.status().is_success() {
157            anyhow::bail!("GraphQL request failed with status: {}", response.status());
158        }
159
160        let graphql_response: GraphQLResponse = response
161            .json()
162            .context("Failed to parse GraphQL response")?;
163
164        // Check for GraphQL errors
165        if let Some(errors) = graphql_response.errors {
166            let error_messages: Vec<String> = errors.iter().map(|e| e.message.clone()).collect();
167            anyhow::bail!("GraphQL errors: {}", error_messages.join(", "));
168        }
169
170        // Extract account data
171        let account = graphql_response
172            .data
173            .and_then(|d| d.account)
174            .with_context(|| format!("Account not found: {}", public_key))?;
175
176        // Create output structure
177        let output = BalanceOutput {
178            account: public_key.clone(),
179            balance: BalanceOutputData {
180                total: account.balance.total.clone(),
181                total_mina: format_balance(&account.balance.total),
182                liquid: account.balance.liquid.clone(),
183                liquid_mina: account.balance.liquid.as_ref().map(|l| format_balance(l)),
184                locked: account.balance.locked.clone(),
185                locked_mina: account.balance.locked.as_ref().map(|l| format_balance(l)),
186            },
187            nonce: account.nonce.clone(),
188            delegate: account
189                .delegate_account
190                .as_ref()
191                .map(|d| d.public_key.clone()),
192        };
193
194        // Display the balance information based on format
195        match self.format {
196            OutputFormat::Json => {
197                let json = serde_json::to_string_pretty(&output)
198                    .context("Failed to serialize output to JSON")?;
199                println!("{}", json);
200            }
201            OutputFormat::Text => {
202                println!("Account: {}", output.account);
203                println!();
204                println!("Balance:");
205                println!("  Total:  {} MINA", output.balance.total_mina);
206
207                if let Some(liquid_mina) = &output.balance.liquid_mina {
208                    println!("  Liquid: {} MINA", liquid_mina);
209                }
210
211                if let Some(locked_mina) = &output.balance.locked_mina {
212                    println!("  Locked: {} MINA", locked_mina);
213                }
214
215                println!();
216                println!("Nonce: {}", output.nonce);
217
218                if let Some(delegate) = &output.delegate {
219                    println!();
220                    println!("Delegate: {}", delegate);
221                }
222            }
223        }
224
225        Ok(())
226    }
227}
228
229fn format_balance(nanomina: &str) -> String {
230    // Convert nanomina to MINA (1 MINA = 1,000,000,000 nanomina)
231    let nano = nanomina.parse::<u64>().unwrap_or(0);
232    let mina = nano as f64 / 1_000_000_000.0;
233    format!("{:.9}", mina)
234}