Skip to content

Commit 4ada2b7

Browse files
author
Adrian Nagy
committed
feat(graphql): add sendPayment mutation
1 parent 30c9e68 commit 4ada2b7

File tree

6 files changed

+376
-19
lines changed

6 files changed

+376
-19
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

node/native/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ redux = { workspace = true, features=["serializable_callbacks"] }
2222
ledger = { workspace = true }
2323
mina-p2p-messages = { workspace = true }
2424
mina-signer = { workspace = true }
25+
o1-utils = { workspace = true }
2526
bytes = "1.4.0"
2627
tracing-subscriber = { version = "0.3.17", features = ["json", "env-filter"] }
2728
tracing = "0.1.37"
@@ -33,6 +34,7 @@ jsonpath-rust = "0.5.0"
3334
sha3 = "0.10.8"
3435
strum = "0.26.2"
3536
strum_macros = "0.26.4"
37+
hex = { version = "0.4.3" }
3638

3739
openmina-core = { path = "../../core" }
3840
openmina-node-common = { path = "../common" }

node/native/src/graphql/mod.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use node::{
1414
rpc::{AccountQuery, RpcRequest, RpcSyncStatsGetResponse, SyncStatsQuery},
1515
stats::sync::SyncKind,
1616
};
17+
use o1_utils::field_helpers::FieldHelpersError;
1718
use openmina_core::block::AppliedBlock;
1819
use openmina_core::consensus::ConsensusConstants;
1920
use openmina_core::constants::constraint_constants;
@@ -26,6 +27,7 @@ pub mod account;
2627
pub mod block;
2728
pub mod constants;
2829
pub mod transaction;
30+
pub mod user_command;
2931
pub mod zkapp;
3032

3133
#[derive(Debug, thiserror::Error)]
@@ -54,6 +56,8 @@ pub enum ConversionError {
5456
InvalidDecimalNumber(#[from] mina_p2p_messages::bigint::InvalidDecimalNumber),
5557
#[error("Invalid bigint")]
5658
InvalidBigInt,
59+
#[error("Invalid hex")]
60+
InvalidHex,
5761
#[error(transparent)]
5862
ParseInt(#[from] std::num::ParseIntError),
5963
#[error(transparent)]
@@ -66,6 +70,8 @@ pub enum ConversionError {
6670
InvalidLength,
6771
#[error("Custom: {0}")]
6872
Custom(String),
73+
#[error(transparent)]
74+
FieldHelpers(#[from] FieldHelpersError),
6975
}
7076

7177
struct Context(RpcSender);
@@ -357,6 +363,69 @@ impl Mutation {
357363
}
358364
}
359365
}
366+
367+
async fn send_payment(
368+
input: user_command::InputGraphQLPayment,
369+
signature: user_command::UserCommandSignature,
370+
context: &Context,
371+
) -> juniper::FieldResult<user_command::GraphQLSendPaymentResponse> {
372+
// Payment commands are always for the default (MINA) token
373+
let token_id = TokenIdKeyHash::default();
374+
let public_key = AccountPublicKey::from_str(&input.from)?;
375+
376+
// Grab the sender's account to get the infered nonce
377+
let accounts: Vec<Account> = context
378+
.0
379+
.oneshot_request(RpcRequest::LedgerAccountsGet(
380+
AccountQuery::PubKeyWithTokenId(public_key, token_id),
381+
))
382+
.await
383+
.ok_or(Error::StateMachineEmptyResponse)?;
384+
385+
let infered_nonce = accounts
386+
.first()
387+
.ok_or(Error::StateMachineEmptyResponse)?
388+
.nonce;
389+
let command = input.create_user_command(infered_nonce, signature)?;
390+
391+
let res: RpcTransactionInjectResponse = context
392+
.0
393+
.oneshot_request(RpcRequest::TransactionInject(vec![command]))
394+
.await
395+
.ok_or(Error::StateMachineEmptyResponse)?;
396+
397+
match res {
398+
RpcTransactionInjectResponse::Success(res) => {
399+
let payment_cmd: MinaBaseUserCommandStableV2 = match res.first().cloned() {
400+
Some(RpcTransactionInjectedCommand::Payment(payment)) => payment.into(),
401+
_ => unreachable!(),
402+
};
403+
Ok(payment_cmd.try_into()?)
404+
}
405+
RpcTransactionInjectResponse::Rejected(rejected) => {
406+
let error_list = rejected
407+
.into_iter()
408+
.map(|(_, err)| graphql_value!({ "message": err.to_string() }))
409+
.collect::<Vec<_>>();
410+
411+
Err(FieldError::new(
412+
"Transaction rejected",
413+
graphql_value!(juniper::Value::List(error_list)),
414+
))
415+
}
416+
RpcTransactionInjectResponse::Failure(failure) => {
417+
let error_list = failure
418+
.into_iter()
419+
.map(|err| graphql_value!({ "message": err.to_string() }))
420+
.collect::<Vec<_>>();
421+
422+
Err(FieldError::new(
423+
"Transaction failed",
424+
graphql_value!(juniper::Value::List(error_list)),
425+
))
426+
}
427+
}
428+
}
360429
}
361430

362431
pub fn routes(
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
use std::str::FromStr;
2+
3+
use juniper::{GraphQLInputObject, GraphQLObject};
4+
use ledger::scan_state::{
5+
currency::{Amount, Fee, Magnitude, Nonce, Slot},
6+
transaction_logic::{signed_command, Memo},
7+
};
8+
use mina_p2p_messages::{
9+
bigint::BigInt,
10+
v2::{self, TokenIdKeyHash},
11+
};
12+
use mina_signer::CompressedPubKey;
13+
use node::account::AccountPublicKey;
14+
use o1_utils::field_helpers::FieldHelpers;
15+
16+
use super::zkapp::GraphQLFailureReason;
17+
18+
// #[derive(GraphQLInputObject, Debug)]
19+
// pub struct InputGraphQLSendPayment {
20+
// pub input: InputGraphQLPayment,
21+
// pub signature: UserCommandSignature,
22+
// }
23+
24+
#[derive(GraphQLInputObject, Debug)]
25+
pub struct InputGraphQLPayment {
26+
pub from: String,
27+
pub to: String,
28+
pub amount: String,
29+
pub valid_until: Option<String>,
30+
pub fee: String,
31+
pub memo: Option<String>,
32+
pub nonce: Option<String>,
33+
}
34+
35+
#[derive(GraphQLInputObject, Debug, Clone)]
36+
pub struct UserCommandSignature {
37+
pub field: Option<String>,
38+
pub scalar: Option<String>,
39+
// Note: either raw_signature or scalar + field must be provided
40+
pub raw_signature: Option<String>,
41+
}
42+
43+
impl TryFrom<UserCommandSignature> for mina_signer::Signature {
44+
type Error = super::ConversionError;
45+
46+
fn try_from(value: UserCommandSignature) -> Result<Self, Self::Error> {
47+
let UserCommandSignature {
48+
field,
49+
scalar,
50+
raw_signature,
51+
} = value;
52+
53+
if let Some(raw_signature) = raw_signature {
54+
let sig_parts_len = raw_signature
55+
.len()
56+
.checked_div(2)
57+
.ok_or(super::ConversionError::InvalidLength)?;
58+
let (rx_hex, s_hex) = raw_signature.split_at(sig_parts_len);
59+
60+
let rx_bytes = hex::decode(rx_hex).map_err(|_| super::ConversionError::InvalidHex)?;
61+
let s_bytes = hex::decode(s_hex).map_err(|_| super::ConversionError::InvalidHex)?;
62+
63+
let rx = mina_signer::BaseField::from_bytes(&rx_bytes)?;
64+
let s = mina_signer::ScalarField::from_bytes(&s_bytes)?;
65+
66+
Ok(Self { rx, s })
67+
} else if let (Some(field), Some(scalar)) = (field, scalar) {
68+
let sig = Self {
69+
rx: BigInt::from_decimal(&field)?
70+
.try_into()
71+
.map_err(|_| super::ConversionError::InvalidBigInt)?,
72+
s: BigInt::from_decimal(&scalar)?
73+
.try_into()
74+
.map_err(|_| super::ConversionError::InvalidBigInt)?,
75+
};
76+
77+
Ok(sig)
78+
} else {
79+
Err(super::ConversionError::MissingField(
80+
"Either raw_signature or scalar + field must be provided".to_string(),
81+
))
82+
}
83+
}
84+
}
85+
86+
impl TryFrom<&UserCommandSignature> for mina_signer::Signature {
87+
type Error = super::ConversionError;
88+
89+
fn try_from(value: &UserCommandSignature) -> Result<Self, Self::Error> {
90+
value.clone().try_into()
91+
}
92+
}
93+
94+
impl UserCommandSignature {
95+
pub fn validate(&self) -> Result<(), super::Error> {
96+
if self.raw_signature.is_some() || (self.scalar.is_some() && self.field.is_some()) {
97+
Ok(())
98+
} else {
99+
Err(super::Error::Custom(
100+
"Either raw_signature or scalar + field must be provided".to_string(),
101+
))
102+
}
103+
}
104+
}
105+
106+
#[derive(GraphQLObject, Debug)]
107+
pub struct GraphQLSendPaymentResponse {
108+
pub payment: GraphQLUserCommand,
109+
}
110+
111+
#[derive(GraphQLObject, Debug)]
112+
pub struct GraphQLUserCommand {
113+
pub amount: String,
114+
pub fee: String,
115+
pub failure_reason: Option<GraphQLFailureReason>,
116+
// TODO: add the account type
117+
pub fee_payer: String,
118+
pub fee_token: String,
119+
pub hash: String,
120+
pub id: String,
121+
pub is_delegation: bool,
122+
pub kind: String,
123+
pub memo: String,
124+
pub nonce: String,
125+
// TODO: add the account type
126+
pub receiver: String,
127+
// TODO: add the account type
128+
pub source: String,
129+
pub token: String,
130+
pub valid_until: String,
131+
}
132+
133+
impl TryFrom<v2::MinaBaseUserCommandStableV2> for GraphQLSendPaymentResponse {
134+
type Error = super::ConversionError;
135+
fn try_from(value: v2::MinaBaseUserCommandStableV2) -> Result<Self, Self::Error> {
136+
if let v2::MinaBaseUserCommandStableV2::SignedCommand(ref signed_cmd) = value {
137+
if let v2::MinaBaseSignedCommandPayloadBodyStableV2::Payment(ref payment) =
138+
signed_cmd.payload.body
139+
{
140+
let res = GraphQLSendPaymentResponse {
141+
payment: GraphQLUserCommand {
142+
amount: payment.amount.to_string(),
143+
fee: signed_cmd.payload.common.fee.to_string(),
144+
failure_reason: None,
145+
fee_payer: signed_cmd.payload.common.fee_payer_pk.to_string(),
146+
fee_token: TokenIdKeyHash::default().to_string(),
147+
hash: signed_cmd.hash()?.to_string(),
148+
id: signed_cmd.to_base64()?,
149+
is_delegation: false,
150+
kind: "Payment".to_string(),
151+
memo: signed_cmd.payload.common.memo.to_base58check(),
152+
nonce: signed_cmd.payload.common.nonce.to_string(),
153+
receiver: payment.receiver_pk.to_string(),
154+
source: signed_cmd.payload.common.fee_payer_pk.to_string(),
155+
token: TokenIdKeyHash::default().to_string(),
156+
valid_until: signed_cmd.payload.common.valid_until.as_u32().to_string(),
157+
},
158+
};
159+
Ok(res)
160+
} else {
161+
Err(super::ConversionError::WrongVariant)
162+
}
163+
} else {
164+
Err(super::ConversionError::WrongVariant)
165+
}
166+
}
167+
}
168+
169+
impl InputGraphQLPayment {
170+
pub fn create_user_command(
171+
&self,
172+
infered_nonce: Nonce,
173+
signature: UserCommandSignature,
174+
) -> Result<v2::MinaBaseUserCommandStableV2, super::ConversionError> {
175+
let infered_nonce = infered_nonce.incr();
176+
177+
let nonce = if let Some(nonce) = &self.nonce {
178+
let input_nonce = Nonce::from_u32(
179+
nonce
180+
.parse::<u32>()
181+
.map_err(|_| super::ConversionError::InvalidBigInt)?,
182+
);
183+
184+
if input_nonce.is_zero() || input_nonce > infered_nonce {
185+
return Err(super::ConversionError::Custom(
186+
"Provided nonce is zero or greater than infered nonce".to_string(),
187+
));
188+
} else {
189+
input_nonce
190+
}
191+
} else {
192+
infered_nonce
193+
};
194+
195+
let valid_until = if let Some(valid_until) = &self.valid_until {
196+
Some(Slot::from_u32(
197+
valid_until
198+
.parse::<u32>()
199+
.map_err(|_| super::ConversionError::InvalidBigInt)?,
200+
))
201+
} else {
202+
None
203+
};
204+
205+
let memo = if let Some(memo) = &self.memo {
206+
Memo::from_str(memo)
207+
.map_err(|_| super::ConversionError::Custom("Invalid memo".to_string()))?
208+
} else {
209+
Memo::empty()
210+
};
211+
212+
let from: CompressedPubKey = AccountPublicKey::from_str(&self.from)?
213+
.try_into()
214+
.map_err(|_| super::ConversionError::InvalidBigInt)?;
215+
216+
let signature = signature.try_into()?;
217+
218+
let sc: signed_command::SignedCommand = signed_command::SignedCommand {
219+
payload: signed_command::SignedCommandPayload::create(
220+
Fee::from_u64(
221+
self.fee
222+
.parse::<u64>()
223+
.map_err(|_| super::ConversionError::InvalidBigInt)?,
224+
),
225+
from.clone(),
226+
nonce,
227+
valid_until,
228+
memo,
229+
signed_command::Body::Payment(signed_command::PaymentPayload {
230+
receiver_pk: AccountPublicKey::from_str(&self.to)?
231+
.try_into()
232+
.map_err(|_| super::ConversionError::InvalidBigInt)?,
233+
amount: Amount::from_u64(
234+
self.amount
235+
.parse::<u64>()
236+
.map_err(|_| super::ConversionError::InvalidBigInt)?,
237+
),
238+
}),
239+
),
240+
signer: from.clone(),
241+
signature,
242+
};
243+
244+
Ok(v2::MinaBaseUserCommandStableV2::SignedCommand(sc.into()))
245+
}
246+
}
247+
// impl TryFrom<InputGraphQLSendPayment> for v2::MinaBaseUserCommandStableV2 {
248+
// type Error = super::ConversionError;
249+
250+
// fn try_from(value: InputGraphQLSendPayment) -> Result<Self, Self::Error> {
251+
// let InputGraphQLSendPayment { input, signature } = value;
252+
253+
// }
254+
// }

0 commit comments

Comments
 (0)