cli/commands/wallet/
balance.rs1use 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 #[arg(long, conflicts_with = "from")]
10 pub address: Option<String>,
11
12 #[arg(long, conflicts_with = "address")]
14 pub from: Option<PathBuf>,
15
16 #[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 #[arg(
26 long,
27 default_value = "http://localhost:3000/graphql",
28 help = "GraphQL endpoint URL"
29 )]
30 pub endpoint: String,
31
32 #[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 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 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 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 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 let account = graphql_response
172 .data
173 .and_then(|d| d.account)
174 .with_context(|| format!("Account not found: {}", public_key))?;
175
176 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 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 let nano = nanomina.parse::<u64>().unwrap_or(0);
232 let mina = nano as f64 / 1_000_000_000.0;
233 format!("{:.9}", mina)
234}