mina_node_native/graphql/
user_command.rs

1use std::str::FromStr;
2
3use juniper::{GraphQLInputObject, GraphQLObject};
4use ledger::scan_state::{
5    currency::{Amount, Fee, Magnitude, Nonce, Slot},
6    transaction_logic::{signed_command, Memo},
7};
8use mina_p2p_messages::{
9    bigint::BigInt,
10    v2::{self, TokenIdKeyHash},
11};
12use mina_signer::CompressedPubKey;
13use node::account::AccountPublicKey;
14use o1_utils::field_helpers::FieldHelpers;
15
16use super::zkapp::GraphQLFailureReason;
17
18#[derive(GraphQLInputObject, Debug)]
19pub struct InputGraphQLPayment {
20    pub from: String,
21    pub to: String,
22    pub amount: String,
23    pub valid_until: Option<String>,
24    pub fee: String,
25    pub memo: Option<String>,
26    pub nonce: Option<String>,
27}
28
29#[derive(GraphQLInputObject, Debug)]
30pub struct InputGraphQLDelegation {
31    pub from: String,
32    pub to: String,
33    pub valid_until: Option<String>,
34    pub fee: String,
35    pub memo: Option<String>,
36    pub nonce: Option<String>,
37}
38
39#[derive(GraphQLInputObject, Debug, Clone)]
40pub struct UserCommandSignature {
41    pub field: Option<String>,
42    pub scalar: Option<String>,
43    // Note: either raw_signature or scalar + field must be provided
44    pub raw_signature: Option<String>,
45}
46
47impl TryFrom<UserCommandSignature> for mina_signer::Signature {
48    type Error = super::ConversionError;
49
50    fn try_from(value: UserCommandSignature) -> Result<Self, Self::Error> {
51        let UserCommandSignature {
52            field,
53            scalar,
54            raw_signature,
55        } = value;
56
57        if let Some(raw_signature) = raw_signature {
58            let sig_parts_len = raw_signature
59                .len()
60                .checked_div(2)
61                .ok_or(super::ConversionError::InvalidLength)?;
62            let (rx_hex, s_hex) = raw_signature.split_at(sig_parts_len);
63
64            let rx_bytes = hex::decode(rx_hex).map_err(|_| super::ConversionError::InvalidHex)?;
65            let s_bytes = hex::decode(s_hex).map_err(|_| super::ConversionError::InvalidHex)?;
66
67            let rx = mina_signer::BaseField::from_bytes(&rx_bytes)?;
68            let s = mina_signer::ScalarField::from_bytes(&s_bytes)?;
69
70            Ok(Self { rx, s })
71        } else if let (Some(field), Some(scalar)) = (field, scalar) {
72            let sig = Self {
73                rx: BigInt::from_decimal(&field)?
74                    .try_into()
75                    .map_err(|_| super::ConversionError::InvalidBigInt)?,
76                s: BigInt::from_decimal(&scalar)?
77                    .try_into()
78                    .map_err(|_| super::ConversionError::InvalidBigInt)?,
79            };
80
81            Ok(sig)
82        } else {
83            Err(super::ConversionError::MissingField(
84                "Either raw_signature or scalar + field must be provided".to_string(),
85            ))
86        }
87    }
88}
89
90impl TryFrom<&UserCommandSignature> for mina_signer::Signature {
91    type Error = super::ConversionError;
92
93    fn try_from(value: &UserCommandSignature) -> Result<Self, Self::Error> {
94        value.clone().try_into()
95    }
96}
97
98impl UserCommandSignature {
99    pub fn validate(&self) -> Result<(), super::Error> {
100        if self.raw_signature.is_some() || (self.scalar.is_some() && self.field.is_some()) {
101            Ok(())
102        } else {
103            Err(super::Error::Custom(
104                "Either raw_signature or scalar + field must be provided".to_string(),
105            ))
106        }
107    }
108}
109
110#[derive(GraphQLObject, Debug)]
111pub struct GraphQLSendPaymentResponse {
112    pub payment: GraphQLUserCommand,
113}
114
115#[derive(GraphQLObject, Debug)]
116pub struct GraphQLSendDelegationResponse {
117    pub delegation: GraphQLUserCommand,
118}
119
120#[derive(GraphQLObject, Debug)]
121pub struct GraphQLUserCommand {
122    pub amount: String,
123    pub fee: String,
124    pub failure_reason: Option<GraphQLFailureReason>,
125    // TODO: add the account type
126    pub fee_payer: String,
127    pub fee_token: String,
128    pub hash: String,
129    pub id: String,
130    pub is_delegation: bool,
131    pub kind: String,
132    pub memo: String,
133    pub nonce: String,
134    // TODO: add the account type
135    pub receiver: String,
136    // TODO: add the account type
137    pub source: String,
138    pub token: String,
139    pub valid_until: String,
140}
141
142impl TryFrom<v2::MinaBaseUserCommandStableV2> for GraphQLSendPaymentResponse {
143    type Error = super::ConversionError;
144    fn try_from(value: v2::MinaBaseUserCommandStableV2) -> Result<Self, Self::Error> {
145        if let v2::MinaBaseUserCommandStableV2::SignedCommand(ref signed_cmd) = value {
146            if let v2::MinaBaseSignedCommandPayloadBodyStableV2::Payment(ref payment) =
147                signed_cmd.payload.body
148            {
149                let res = GraphQLSendPaymentResponse {
150                    payment: GraphQLUserCommand {
151                        amount: payment.amount.to_string(),
152                        fee: signed_cmd.payload.common.fee.to_string(),
153                        failure_reason: None,
154                        fee_payer: signed_cmd.payload.common.fee_payer_pk.to_string(),
155                        fee_token: TokenIdKeyHash::default().to_string(),
156                        hash: signed_cmd.hash()?.to_string(),
157                        id: signed_cmd.to_base64()?,
158                        is_delegation: false,
159                        kind: "PAYMENT".to_string(),
160                        memo: signed_cmd.payload.common.memo.to_base58check(),
161                        nonce: signed_cmd.payload.common.nonce.to_string(),
162                        receiver: payment.receiver_pk.to_string(),
163                        source: signed_cmd.payload.common.fee_payer_pk.to_string(),
164                        token: TokenIdKeyHash::default().to_string(),
165                        valid_until: signed_cmd.payload.common.valid_until.as_u32().to_string(),
166                    },
167                };
168                Ok(res)
169            } else {
170                Err(super::ConversionError::WrongVariant)
171            }
172        } else {
173            Err(super::ConversionError::WrongVariant)
174        }
175    }
176}
177
178impl TryFrom<v2::MinaBaseUserCommandStableV2> for GraphQLSendDelegationResponse {
179    type Error = super::ConversionError;
180    fn try_from(value: v2::MinaBaseUserCommandStableV2) -> Result<Self, Self::Error> {
181        if let v2::MinaBaseUserCommandStableV2::SignedCommand(ref signed_cmd) = value {
182            if let v2::MinaBaseSignedCommandPayloadBodyStableV2::StakeDelegation(ref delegation) =
183                signed_cmd.payload.body
184            {
185                let v2::MinaBaseStakeDelegationStableV2::SetDelegate { new_delegate } = delegation;
186                let res = GraphQLSendDelegationResponse {
187                    delegation: GraphQLUserCommand {
188                        amount: "0".to_string(),
189                        fee: signed_cmd.payload.common.fee.to_string(),
190                        failure_reason: None,
191                        fee_payer: signed_cmd.payload.common.fee_payer_pk.to_string(),
192                        fee_token: TokenIdKeyHash::default().to_string(),
193                        hash: signed_cmd.hash()?.to_string(),
194                        id: signed_cmd.to_base64()?,
195                        is_delegation: true,
196                        kind: "STAKE_DELEGATION".to_string(),
197                        memo: signed_cmd.payload.common.memo.to_base58check(),
198                        nonce: signed_cmd.payload.common.nonce.to_string(),
199                        receiver: new_delegate.to_string(),
200                        source: signed_cmd.payload.common.fee_payer_pk.to_string(),
201                        token: TokenIdKeyHash::default().to_string(),
202                        valid_until: signed_cmd.payload.common.valid_until.as_u32().to_string(),
203                    },
204                };
205                Ok(res)
206            } else {
207                Err(super::ConversionError::WrongVariant)
208            }
209        } else {
210            Err(super::ConversionError::WrongVariant)
211        }
212    }
213}
214
215impl InputGraphQLPayment {
216    pub fn create_user_command(
217        &self,
218        infered_nonce: Nonce,
219        signature: UserCommandSignature,
220    ) -> Result<v2::MinaBaseUserCommandStableV2, super::ConversionError> {
221        let infered_nonce = infered_nonce.incr();
222
223        let nonce = if let Some(nonce) = &self.nonce {
224            let input_nonce = Nonce::from_u32(
225                nonce
226                    .parse::<u32>()
227                    .map_err(|_| super::ConversionError::InvalidBigInt)?,
228            );
229
230            if input_nonce.is_zero() || input_nonce > infered_nonce {
231                return Err(super::ConversionError::Custom(
232                    "Provided nonce is zero or greater than infered nonce".to_string(),
233                ));
234            } else {
235                input_nonce
236            }
237        } else {
238            infered_nonce
239        };
240
241        let valid_until = if let Some(valid_until) = &self.valid_until {
242            Some(Slot::from_u32(
243                valid_until
244                    .parse::<u32>()
245                    .map_err(|_| super::ConversionError::InvalidBigInt)?,
246            ))
247        } else {
248            None
249        };
250
251        let memo = if let Some(memo) = &self.memo {
252            Memo::from_str(memo)
253                .map_err(|_| super::ConversionError::Custom("Invalid memo".to_string()))?
254        } else {
255            Memo::empty()
256        };
257
258        let from: CompressedPubKey = AccountPublicKey::from_str(&self.from)?
259            .try_into()
260            .map_err(|_| super::ConversionError::InvalidBigInt)?;
261
262        let signature = signature.try_into()?;
263
264        let sc: signed_command::SignedCommand = signed_command::SignedCommand {
265            payload: signed_command::SignedCommandPayload::create(
266                Fee::from_u64(
267                    self.fee
268                        .parse::<u64>()
269                        .map_err(|_| super::ConversionError::InvalidBigInt)?,
270                ),
271                from.clone(),
272                nonce,
273                valid_until,
274                memo,
275                signed_command::Body::Payment(signed_command::PaymentPayload {
276                    receiver_pk: AccountPublicKey::from_str(&self.to)?
277                        .try_into()
278                        .map_err(|_| super::ConversionError::InvalidBigInt)?,
279                    amount: Amount::from_u64(
280                        self.amount
281                            .parse::<u64>()
282                            .map_err(|_| super::ConversionError::InvalidBigInt)?,
283                    ),
284                }),
285            ),
286            signer: from.clone(),
287            signature,
288        };
289
290        Ok(v2::MinaBaseUserCommandStableV2::SignedCommand(sc.into()))
291    }
292}
293
294impl InputGraphQLDelegation {
295    pub fn create_user_command(
296        &self,
297        infered_nonce: Nonce,
298        signature: UserCommandSignature,
299    ) -> Result<v2::MinaBaseUserCommandStableV2, super::ConversionError> {
300        let infered_nonce = infered_nonce.incr();
301
302        let nonce = if let Some(nonce) = &self.nonce {
303            let input_nonce = Nonce::from_u32(
304                nonce
305                    .parse::<u32>()
306                    .map_err(|_| super::ConversionError::InvalidBigInt)?,
307            );
308
309            if input_nonce.is_zero() || input_nonce > infered_nonce {
310                return Err(super::ConversionError::Custom(
311                    "Provided nonce is zero or greater than infered nonce".to_string(),
312                ));
313            } else {
314                input_nonce
315            }
316        } else {
317            infered_nonce
318        };
319
320        let valid_until = if let Some(valid_until) = &self.valid_until {
321            Some(Slot::from_u32(
322                valid_until
323                    .parse::<u32>()
324                    .map_err(|_| super::ConversionError::InvalidBigInt)?,
325            ))
326        } else {
327            None
328        };
329
330        let memo = if let Some(memo) = &self.memo {
331            Memo::from_str(memo)
332                .map_err(|_| super::ConversionError::Custom("Invalid memo".to_string()))?
333        } else {
334            Memo::empty()
335        };
336
337        let from: CompressedPubKey = AccountPublicKey::from_str(&self.from)?
338            .try_into()
339            .map_err(|_| super::ConversionError::InvalidBigInt)?;
340
341        let signature = signature.try_into()?;
342
343        let sc: signed_command::SignedCommand = signed_command::SignedCommand {
344            payload: signed_command::SignedCommandPayload::create(
345                Fee::from_u64(
346                    self.fee
347                        .parse::<u64>()
348                        .map_err(|_| super::ConversionError::InvalidBigInt)?,
349                ),
350                from.clone(),
351                nonce,
352                valid_until,
353                memo,
354                signed_command::Body::StakeDelegation(
355                    signed_command::StakeDelegationPayload::SetDelegate {
356                        new_delegate: AccountPublicKey::from_str(&self.to)?
357                            .try_into()
358                            .map_err(|_| super::ConversionError::InvalidBigInt)?,
359                    },
360                ),
361            ),
362            signer: from.clone(),
363            signature,
364        };
365
366        Ok(v2::MinaBaseUserCommandStableV2::SignedCommand(sc.into()))
367    }
368}