1use std::{collections::HashMap, sync::Arc};
2
3use dataloader::non_cached::Loader;
4use juniper::{graphql_object, FieldResult, GraphQLInputObject, GraphQLObject};
5use ledger::{
6 scan_state::currency::{Balance, Magnitude, Slot},
7 Account, AccountId, FpExt, Timing,
8};
9use mina_p2p_messages::{
10 string::{TokenSymbol, ZkAppUri},
11 v2::{
12 MinaBaseAccountUpdateUpdateTimingInfoStableV1, MinaBaseVerificationKeyWireStableV1,
13 ReceiptChainHash, TokenIdKeyHash,
14 },
15};
16use mina_signer::CompressedPubKey;
17use node::rpc::{AccountQuery, RpcRequest};
18use openmina_node_common::rpc::RpcSender;
19
20use super::{Context, ConversionError};
21
22pub(crate) type AccountLoader =
23 Loader<AccountId, Result<GraphQLAccount, Arc<ConversionError>>, AccountBatcher>;
24
25pub(crate) struct AccountBatcher {
26 rpc_sender: RpcSender,
27}
28
29impl dataloader::BatchFn<AccountId, Result<GraphQLAccount, Arc<ConversionError>>>
30 for AccountBatcher
31{
32 async fn load(
33 &mut self,
34 keys: &[AccountId],
35 ) -> HashMap<AccountId, Result<GraphQLAccount, Arc<ConversionError>>> {
36 self.rpc_sender
37 .oneshot_request::<Vec<Account>>(RpcRequest::LedgerAccountsGet(
38 AccountQuery::MultipleIds(keys.to_vec()),
39 ))
40 .await
41 .unwrap_or_default()
42 .into_iter()
43 .map(|account| (account.id(), account.try_into().map_err(Arc::new)))
44 .collect()
45 }
46}
47
48pub(crate) fn create_account_loader(rpc_sender: RpcSender) -> AccountLoader {
49 Loader::new(AccountBatcher { rpc_sender }).with_yield_count(25)
51}
52
53#[derive(Debug, Clone)]
54pub(crate) struct GraphQLAccount {
55 inner: Account,
56 public_key: String,
57 token_id: String,
58 token: String,
59 token_symbol: String,
60 nonce: String,
62 receipt_chain_hash: String,
63 delegate_key: Option<CompressedPubKey>,
65 voting_for: String,
66 timing: GraphQLTiming,
67 permissions: GraphQLPermissions,
68 zkapp_state: Option<Vec<String>>,
71 verification_key: Option<GraphQLVerificationKey>,
72 action_state: Option<Vec<String>>,
73 proved_state: Option<bool>,
74 zkapp_uri: Option<String>,
75}
76
77impl GraphQLAccount {
78 fn min_balance(&self, global_slot: Option<u32>) -> Option<Balance> {
79 global_slot.map(|slot| match self.inner.timing {
80 Timing::Untimed => Balance::zero(),
81 Timing::Timed { .. } => self.inner.min_balance_at_slot(Slot::from_u32(slot)),
82 })
83 }
84
85 fn liquid_balance(&self, global_slot: Option<u32>) -> Option<Balance> {
86 let min_balance = self.min_balance(global_slot);
87 let total = self.inner.balance;
88 min_balance.map(|mb| {
89 if total > mb {
90 total.checked_sub(&mb).expect("overflow")
91 } else {
92 Balance::zero()
93 }
94 })
95 }
96}
97
98#[graphql_object(context = Context)]
99#[graphql(description = "A Mina account")]
100impl GraphQLAccount {
101 fn public_key(&self) -> &str {
102 &self.public_key
103 }
104
105 fn token_id(&self) -> &str {
106 &self.token_id
107 }
108
109 fn token(&self) -> &str {
110 &self.token
111 }
112
113 fn token_symbol(&self) -> &str {
114 &self.token_symbol
115 }
116
117 async fn balance(&self, context: &Context) -> GraphQLBalance {
118 let best_tip = context.get_or_fetch_best_tip().await;
119 let global_slot = best_tip.as_ref().map(|bt| bt.global_slot());
120
121 GraphQLBalance {
122 total: self.inner.balance.as_u64().to_string(),
123 block_height: best_tip
124 .as_ref()
125 .map(|bt| bt.height())
126 .unwrap_or_default()
127 .to_string(),
128 state_hash: best_tip.as_ref().map(|bt| bt.hash().to_string()),
129 liquid: self
130 .liquid_balance(global_slot)
131 .map(|b| b.as_u64().to_string()),
132 locked: self
133 .min_balance(global_slot)
134 .map(|b| b.as_u64().to_string()),
135 unknown: self.inner.balance.as_u64().to_string(),
136 }
137 }
138
139 fn nonce(&self) -> &str {
140 &self.nonce
141 }
142
143 fn receipt_chain_hash(&self) -> &str {
144 &self.receipt_chain_hash
145 }
146
147 async fn delegate_account(
148 &self,
149 context: &Context,
150 ) -> FieldResult<Option<Box<GraphQLAccount>>> {
151 if let Some(delegate_key) = self.delegate_key.as_ref() {
153 let delegate_id = AccountId::new_with_default_token(delegate_key.clone());
155 Ok(context.load_account(delegate_id).await.map(Box::new))
157 } else {
158 Ok(None)
160 }
161 }
162
163 pub async fn delegators(&self, context: &Context) -> FieldResult<Vec<GraphQLAccount>> {
164 if let Some(best_tip) = context.get_or_fetch_best_tip().await {
165 let staking_ledger_hash = best_tip.staking_epoch_ledger_hash();
166
167 let id = self.inner.id();
168 let delegators = context
169 .fetch_delegators(staking_ledger_hash.clone(), id.clone())
170 .await
171 .unwrap_or_default();
172
173 Ok(delegators
174 .into_iter()
175 .map(GraphQLAccount::try_from)
176 .collect::<Result<Vec<_>, _>>()?)
177 } else {
178 Ok(vec![])
179 }
180 }
181
182 fn voting_for(&self) -> &str {
183 &self.voting_for
184 }
185
186 fn timing(&self) -> &GraphQLTiming {
187 &self.timing
188 }
189
190 fn permissions(&self) -> &GraphQLPermissions {
191 &self.permissions
192 }
193
194 fn zkapp_state(&self) -> &Option<Vec<String>> {
195 &self.zkapp_state
196 }
197
198 fn verification_key(&self) -> &Option<GraphQLVerificationKey> {
199 &self.verification_key
200 }
201
202 fn action_state(&self) -> &Option<Vec<String>> {
203 &self.action_state
204 }
205
206 fn proved_state(&self) -> &Option<bool> {
207 &self.proved_state
208 }
209
210 fn zkapp_uri(&self) -> &Option<String> {
211 &self.zkapp_uri
212 }
213}
214
215#[derive(GraphQLObject, Debug, Clone)]
216pub struct GraphQLDelegateAccount {
217 pub public_key: String,
218}
219
220#[derive(GraphQLObject, Debug, Clone)]
221pub struct GraphQLTiming {
222 pub initial_minimum_balance: Option<String>,
224 pub cliff_time: Option<i32>,
225 pub cliff_amount: Option<String>,
226 pub vesting_period: Option<i32>,
227 pub vesting_increment: Option<String>,
228}
229
230#[derive(GraphQLInputObject, Debug, Clone)]
231pub struct InputGraphQLTiming {
232 pub initial_minimum_balance: String,
234 pub cliff_time: i32,
235 pub cliff_amount: String,
236 pub vesting_period: i32,
237 pub vesting_increment: String,
238}
239
240impl From<MinaBaseAccountUpdateUpdateTimingInfoStableV1> for GraphQLTiming {
241 fn from(value: MinaBaseAccountUpdateUpdateTimingInfoStableV1) -> Self {
242 Self {
243 initial_minimum_balance: Some(value.initial_minimum_balance.0.as_u64().to_string()),
244 cliff_time: Some(value.cliff_time.as_u32() as i32),
245 cliff_amount: Some(value.cliff_amount.as_u64().to_string()),
246 vesting_period: Some(value.vesting_period.as_u32() as i32),
247 vesting_increment: Some(value.vesting_increment.0.as_u64().to_string()),
248 }
249 }
250}
251
252#[derive(GraphQLObject, Debug, Clone)]
253pub struct GraphQLPermissions {
254 pub edit_state: String,
255 pub access: String,
256 pub send: String,
257 pub receive: String,
258 pub set_delegate: String,
259 pub set_permissions: String,
260 pub set_verification_key: GraphQLSetVerificationKey,
261 pub set_zkapp_uri: String,
262 pub edit_action_state: String,
263 pub set_token_symbol: String,
264 pub increment_nonce: String,
265 pub set_voting_for: String,
266 pub set_timing: String,
267}
268
269#[derive(GraphQLObject, Debug, Clone)]
270pub struct GraphQLSetVerificationKey {
271 pub auth: String,
272 pub txn_version: String,
273}
274
275#[derive(GraphQLObject, Debug, Clone)]
276pub struct GraphQLBalance {
277 pub total: String,
278 pub block_height: String,
279 pub state_hash: Option<String>,
280 pub liquid: Option<String>,
281 pub locked: Option<String>,
282 pub unknown: String,
283}
284
285#[derive(GraphQLObject, Debug, Clone)]
297pub struct GraphQLVerificationKey {
298 pub verification_key: String,
302 pub hash: String,
303}
304
305impl From<ledger::SetVerificationKey<ledger::AuthRequired>> for GraphQLSetVerificationKey {
306 fn from(value: ledger::SetVerificationKey<ledger::AuthRequired>) -> Self {
307 Self {
308 auth: value.auth.to_string(),
309 txn_version: value.txn_version.as_u32().to_string(),
310 }
311 }
312}
313
314impl From<ledger::Permissions<ledger::AuthRequired>> for GraphQLPermissions {
315 fn from(value: ledger::Permissions<ledger::AuthRequired>) -> Self {
316 Self {
317 edit_state: value.edit_state.to_string(),
318 access: value.access.to_string(),
319 send: value.send.to_string(),
320 receive: value.receive.to_string(),
321 set_delegate: value.set_delegate.to_string(),
322 set_permissions: value.set_permissions.to_string(),
323 set_verification_key: GraphQLSetVerificationKey::from(value.set_verification_key),
324 set_zkapp_uri: value.set_zkapp_uri.to_string(),
325 edit_action_state: value.edit_action_state.to_string(),
326 set_token_symbol: value.set_token_symbol.to_string(),
327 increment_nonce: value.increment_nonce.to_string(),
328 set_voting_for: value.set_voting_for.to_string(),
329 set_timing: value.set_timing.to_string(),
330 }
331 }
332}
333
334impl From<ledger::Timing> for GraphQLTiming {
335 fn from(value: ledger::Timing) -> Self {
336 match value {
337 ledger::Timing::Untimed => Self {
338 initial_minimum_balance: None,
339 vesting_period: None,
340 cliff_time: None,
341 cliff_amount: None,
342 vesting_increment: None,
343 },
344 ledger::Timing::Timed {
345 initial_minimum_balance,
346 cliff_time,
347 cliff_amount,
348 vesting_period,
349 vesting_increment,
350 } => Self {
351 initial_minimum_balance: Some(initial_minimum_balance.as_u64().to_string()),
352 cliff_time: Some(cliff_time.as_u32() as i32),
353 cliff_amount: Some(cliff_amount.as_u64().to_string()),
354 vesting_period: Some(vesting_period.as_u32() as i32),
355 vesting_increment: Some(vesting_increment.as_u64().to_string()),
356 },
357 }
358 }
359}
360
361impl TryFrom<ledger::Account> for GraphQLAccount {
362 type Error = ConversionError;
363
364 fn try_from(value: ledger::Account) -> Result<Self, Self::Error> {
365 let verification_key = value
367 .zkapp
368 .clone()
369 .and_then(|zkapp| {
370 zkapp.verification_key.map(|vk| {
371 let ser = MinaBaseVerificationKeyWireStableV1::from(vk.vk()).to_base64()?;
372
373 Ok(GraphQLVerificationKey {
374 verification_key: ser,
375 hash: vk.hash().to_decimal(),
376 }) as Result<GraphQLVerificationKey, Self::Error>
377 })
378 })
379 .transpose()?; Ok(Self {
382 inner: value.clone(),
383 public_key: value.public_key.into_address(),
384 token_id: TokenIdKeyHash::from(value.token_id.clone()).to_string(),
385 token: TokenIdKeyHash::from(value.token_id).to_string(),
386 token_symbol: TokenSymbol::from(&value.token_symbol).to_string(),
387 nonce: value.nonce.as_u32().to_string(),
389 receipt_chain_hash: ReceiptChainHash::from(value.receipt_chain_hash).to_string(),
390 delegate_key: value.delegate,
391 voting_for: value.voting_for.to_base58check_graphql(),
392 timing: GraphQLTiming::from(value.timing),
393 permissions: GraphQLPermissions::from(value.permissions),
394 zkapp_state: value.zkapp.clone().map(|zkapp| {
397 zkapp
398 .app_state
399 .into_iter()
400 .map(|v| v.to_decimal())
401 .collect::<Vec<_>>()
402 }),
403 verification_key,
404 action_state: value.zkapp.clone().map(|zkapp| {
405 zkapp
406 .action_state
407 .into_iter()
408 .map(|v| v.to_decimal())
409 .collect::<Vec<_>>()
410 }),
411 proved_state: value.zkapp.clone().map(|zkapp| zkapp.proved_state),
412 zkapp_uri: value
413 .zkapp
414 .map(|zkapp| ZkAppUri::from(&zkapp.zkapp_uri).to_string()),
415 })
416 }
417}