openmina_node_native/graphql/
account.rs

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    // TODO(adonagy): is 25 enough?
50    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    // balance: GraphQLBalance,
61    nonce: String,
62    receipt_chain_hash: String,
63    // Storing the key for later
64    delegate_key: Option<CompressedPubKey>,
65    voting_for: String,
66    timing: GraphQLTiming,
67    permissions: GraphQLPermissions,
68    // can we flatten?
69    // pub zkapp: Option<GraphQLZkAppAccount>,
70    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 we have a delegate key
152        if let Some(delegate_key) = self.delegate_key.as_ref() {
153            // A delegate always has the default token id
154            let delegate_id = AccountId::new_with_default_token(delegate_key.clone());
155            // Use the loader to fetch the delegate account
156            Ok(context.load_account(delegate_id).await.map(Box::new))
157        } else {
158            // No delegate
159            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 is_timed: bool,
223    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 is_timed: bool,
233    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)]
286// pub struct GraphQLZkAppAccount {
287//     pub app_state: Vec<String>,
288//     pub verification_key: Option<GraphQLVerificationKey>,
289//     pub zkapp_version: i32,
290//     pub action_state: Vec<String>,
291//     pub last_action_slot: i32,
292//     pub proved_state: bool,
293//     pub zkapp_uri: String,
294// }
295
296#[derive(GraphQLObject, Debug, Clone)]
297pub struct GraphQLVerificationKey {
298    // pub max_proofs_verified: String,
299    // pub actual_wrap_domain_size: String,
300    // pub wrap_index: String,
301    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        // Process the verification_key with proper error handling
366        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()?; // Transpose Option<Result<...>> to Result<Option<...>>
380
381        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            // balance: GraphQLBalance::from(value.balance),
388            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: value.zkapp.map(GraphQLZkAppAccount::from),
395            // TODO: keep as array?
396            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}