1use self::{
57 local_state::{apply_zkapp_command_first_pass, apply_zkapp_command_second_pass, LocalStateEnv},
58 protocol_state::{GlobalState, ProtocolStateView},
59 signed_command::{SignedCommand, SignedCommandPayload},
60 transaction_applied::{
61 signed_command_applied::{self, SignedCommandApplied},
62 TransactionApplied,
63 },
64 zkapp_command::{AccessedOrNot, ZkAppCommand},
65};
66use super::{
67 currency::{Amount, Balance, Fee, Magnitude, Nonce, Signed, Slot},
68 fee_excess::FeeExcess,
69 fee_rate::FeeRate,
70 scan_state::transaction_snark::OneOrTwo,
71};
72use crate::{
73 scan_state::transaction_logic::{
74 transaction_applied::{CommandApplied, Varying},
75 zkapp_command::MaybeWithStatus,
76 },
77 sparse_ledger::LedgerIntf,
78 zkapps::non_snark::LedgerNonSnark,
79 Account, AccountId, BaseLedger, ControlTag, Timing, TokenId, VerificationKeyWire,
80};
81use mina_core::constants::ConstraintConstants;
82use mina_curves::pasta::Fp;
83use mina_macros::SerdeYojsonEnum;
84use mina_p2p_messages::{
85 bigint::InvalidBigInt,
86 binprot,
87 v2::{MinaBaseUserCommandStableV2, MinaTransactionTransactionStableV2},
88};
89use mina_signer::CompressedPubKey;
90use poseidon::hash::params::MINA_ZKAPP_MEMO;
91use std::{
92 collections::{BTreeMap, HashMap, HashSet},
93 fmt::Display,
94};
95
96pub mod local_state;
97pub mod protocol_state;
98pub mod signed_command;
99pub mod transaction_applied;
100pub mod transaction_partially_applied;
101pub mod transaction_union_payload;
102pub mod transaction_witness;
103pub mod valid;
104pub mod verifiable;
105pub mod zkapp_command;
106pub mod zkapp_statement;
107pub use transaction_partially_applied::{
108 apply_transaction_first_pass, apply_transaction_second_pass, apply_transactions,
109 apply_user_command, set_with_location, AccountState,
110};
111pub use transaction_union_payload::{
112 account_check_timing, add_amount, checked_cons_signed_command_payload,
113 cons_signed_command_payload, cons_zkapp_command_commitment, get_with_location, sub_amount,
114 timing_error_to_user_command_status, validate_nonces, validate_timing, Body, Common,
115 ExistingOrNew, Tag, TimingValidation, TransactionUnion, TransactionUnionPayload,
116};
117
118#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
120pub enum TransactionFailure {
121 Predicate,
122 SourceNotPresent,
123 ReceiverNotPresent,
124 AmountInsufficientToCreateAccount,
125 CannotPayCreationFeeInToken,
126 SourceInsufficientBalance,
127 SourceMinimumBalanceViolation,
128 ReceiverAlreadyExists,
129 TokenOwnerNotCaller,
130 Overflow,
131 GlobalExcessOverflow,
132 LocalExcessOverflow,
133 LocalSupplyIncreaseOverflow,
134 GlobalSupplyIncreaseOverflow,
135 SignedCommandOnZkappAccount,
136 ZkappAccountNotPresent,
137 UpdateNotPermittedBalance,
138 UpdateNotPermittedAccess,
139 UpdateNotPermittedTiming,
140 UpdateNotPermittedDelegate,
141 UpdateNotPermittedAppState,
142 UpdateNotPermittedVerificationKey,
143 UpdateNotPermittedActionState,
144 UpdateNotPermittedZkappUri,
145 UpdateNotPermittedTokenSymbol,
146 UpdateNotPermittedPermissions,
147 UpdateNotPermittedNonce,
148 UpdateNotPermittedVotingFor,
149 ZkappCommandReplayCheckFailed,
150 FeePayerNonceMustIncrease,
151 FeePayerMustBeSigned,
152 AccountBalancePreconditionUnsatisfied,
153 AccountNoncePreconditionUnsatisfied,
154 AccountReceiptChainHashPreconditionUnsatisfied,
155 AccountDelegatePreconditionUnsatisfied,
156 AccountActionStatePreconditionUnsatisfied,
157 AccountAppStatePreconditionUnsatisfied(u64),
158 AccountProvedStatePreconditionUnsatisfied,
159 AccountIsNewPreconditionUnsatisfied,
160 ProtocolStatePreconditionUnsatisfied,
161 UnexpectedVerificationKeyHash,
162 ValidWhilePreconditionUnsatisfied,
163 IncorrectNonce,
164 InvalidFeeExcess,
165 Cancelled,
166}
167
168impl Display for TransactionFailure {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 let message = match self {
171 Self::Predicate => "Predicate",
172 Self::SourceNotPresent => "Source_not_present",
173 Self::ReceiverNotPresent => "Receiver_not_present",
174 Self::AmountInsufficientToCreateAccount => "Amount_insufficient_to_create_account",
175 Self::CannotPayCreationFeeInToken => "Cannot_pay_creation_fee_in_token",
176 Self::SourceInsufficientBalance => "Source_insufficient_balance",
177 Self::SourceMinimumBalanceViolation => "Source_minimum_balance_violation",
178 Self::ReceiverAlreadyExists => "Receiver_already_exists",
179 Self::TokenOwnerNotCaller => "Token_owner_not_caller",
180 Self::Overflow => "Overflow",
181 Self::GlobalExcessOverflow => "Global_excess_overflow",
182 Self::LocalExcessOverflow => "Local_excess_overflow",
183 Self::LocalSupplyIncreaseOverflow => "Local_supply_increase_overflow",
184 Self::GlobalSupplyIncreaseOverflow => "Global_supply_increase_overflow",
185 Self::SignedCommandOnZkappAccount => "Signed_command_on_zkapp_account",
186 Self::ZkappAccountNotPresent => "Zkapp_account_not_present",
187 Self::UpdateNotPermittedBalance => "Update_not_permitted_balance",
188 Self::UpdateNotPermittedAccess => "Update_not_permitted_access",
189 Self::UpdateNotPermittedTiming => "Update_not_permitted_timing",
190 Self::UpdateNotPermittedDelegate => "update_not_permitted_delegate",
191 Self::UpdateNotPermittedAppState => "Update_not_permitted_app_state",
192 Self::UpdateNotPermittedVerificationKey => "Update_not_permitted_verification_key",
193 Self::UpdateNotPermittedActionState => "Update_not_permitted_action_state",
194 Self::UpdateNotPermittedZkappUri => "Update_not_permitted_zkapp_uri",
195 Self::UpdateNotPermittedTokenSymbol => "Update_not_permitted_token_symbol",
196 Self::UpdateNotPermittedPermissions => "Update_not_permitted_permissions",
197 Self::UpdateNotPermittedNonce => "Update_not_permitted_nonce",
198 Self::UpdateNotPermittedVotingFor => "Update_not_permitted_voting_for",
199 Self::ZkappCommandReplayCheckFailed => "Zkapp_command_replay_check_failed",
200 Self::FeePayerNonceMustIncrease => "Fee_payer_nonce_must_increase",
201 Self::FeePayerMustBeSigned => "Fee_payer_must_be_signed",
202 Self::AccountBalancePreconditionUnsatisfied => {
203 "Account_balance_precondition_unsatisfied"
204 }
205 Self::AccountNoncePreconditionUnsatisfied => "Account_nonce_precondition_unsatisfied",
206 Self::AccountReceiptChainHashPreconditionUnsatisfied => {
207 "Account_receipt_chain_hash_precondition_unsatisfied"
208 }
209 Self::AccountDelegatePreconditionUnsatisfied => {
210 "Account_delegate_precondition_unsatisfied"
211 }
212 Self::AccountActionStatePreconditionUnsatisfied => {
213 "Account_action_state_precondition_unsatisfied"
214 }
215 Self::AccountAppStatePreconditionUnsatisfied(i) => {
216 return write!(f, "Account_app_state_{}_precondition_unsatisfied", i);
217 }
218 Self::AccountProvedStatePreconditionUnsatisfied => {
219 "Account_proved_state_precondition_unsatisfied"
220 }
221 Self::AccountIsNewPreconditionUnsatisfied => "Account_is_new_precondition_unsatisfied",
222 Self::ProtocolStatePreconditionUnsatisfied => "Protocol_state_precondition_unsatisfied",
223 Self::IncorrectNonce => "Incorrect_nonce",
224 Self::InvalidFeeExcess => "Invalid_fee_excess",
225 Self::Cancelled => "Cancelled",
226 Self::UnexpectedVerificationKeyHash => "Unexpected_verification_key_hash",
227 Self::ValidWhilePreconditionUnsatisfied => "Valid_while_precondition_unsatisfied",
228 };
229
230 write!(f, "{}", message)
231 }
232}
233
234#[derive(SerdeYojsonEnum, Debug, Clone, PartialEq, Eq)]
236pub enum TransactionStatus {
237 Applied,
238 Failed(Vec<Vec<TransactionFailure>>),
239}
240
241impl TransactionStatus {
242 pub fn is_applied(&self) -> bool {
243 matches!(self, Self::Applied)
244 }
245 pub fn is_failed(&self) -> bool {
246 matches!(self, Self::Failed(_))
247 }
248}
249
250#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
252pub struct WithStatus<T> {
253 pub data: T,
254 pub status: TransactionStatus,
255}
256
257impl<T> WithStatus<T> {
258 pub fn applied(data: T) -> Self {
259 Self {
260 data,
261 status: TransactionStatus::Applied,
262 }
263 }
264
265 pub fn failed(data: T, failures: Vec<Vec<TransactionFailure>>) -> Self {
266 Self {
267 data,
268 status: TransactionStatus::Failed(failures),
269 }
270 }
271
272 pub fn map<F, R>(&self, fun: F) -> WithStatus<R>
273 where
274 F: Fn(&T) -> R,
275 {
276 WithStatus {
277 data: fun(&self.data),
278 status: self.status.clone(),
279 }
280 }
281
282 pub fn into_map<F, R>(self, fun: F) -> WithStatus<R>
283 where
284 F: Fn(T) -> R,
285 {
286 WithStatus {
287 data: fun(self.data),
288 status: self.status,
289 }
290 }
291}
292
293pub trait GenericCommand {
294 fn fee(&self) -> Fee;
295
296 fn forget(&self) -> UserCommand;
297}
298
299pub trait GenericTransaction: Sized {
300 fn is_fee_transfer(&self) -> bool;
301
302 fn is_coinbase(&self) -> bool;
303
304 fn is_command(&self) -> bool;
305}
306
307impl<T> GenericCommand for WithStatus<T>
308where
309 T: GenericCommand,
310{
311 fn fee(&self) -> Fee {
312 self.data.fee()
313 }
314
315 fn forget(&self) -> UserCommand {
316 self.data.forget()
317 }
318}
319
320#[derive(Debug, Clone, PartialEq)]
322pub struct SingleFeeTransfer {
323 pub receiver_pk: CompressedPubKey,
324 pub fee: Fee,
325 pub fee_token: TokenId,
326}
327
328impl SingleFeeTransfer {
329 pub fn receiver(&self) -> AccountId {
330 AccountId {
331 public_key: self.receiver_pk.clone(),
332 token_id: self.fee_token.clone(),
333 }
334 }
335
336 pub fn create(receiver_pk: CompressedPubKey, fee: Fee, fee_token: TokenId) -> Self {
337 Self {
338 receiver_pk,
339 fee,
340 fee_token,
341 }
342 }
343}
344
345#[derive(Debug, Clone, PartialEq)]
347pub struct FeeTransfer(pub(super) OneOrTwo<SingleFeeTransfer>);
348
349impl std::ops::Deref for FeeTransfer {
350 type Target = OneOrTwo<SingleFeeTransfer>;
351
352 fn deref(&self) -> &Self::Target {
353 &self.0
354 }
355}
356
357impl FeeTransfer {
358 pub fn fee_tokens(&self) -> impl Iterator<Item = &TokenId> {
359 self.0.iter().map(|fee_transfer| &fee_transfer.fee_token)
360 }
361
362 pub fn receiver_pks(&self) -> impl Iterator<Item = &CompressedPubKey> {
363 self.0.iter().map(|fee_transfer| &fee_transfer.receiver_pk)
364 }
365
366 pub fn receivers(&self) -> impl Iterator<Item = AccountId> + '_ {
367 self.0.iter().map(|fee_transfer| AccountId {
368 public_key: fee_transfer.receiver_pk.clone(),
369 token_id: fee_transfer.fee_token.clone(),
370 })
371 }
372
373 pub fn fee_excess(&self) -> Result<FeeExcess, String> {
375 let one_or_two = self.0.map(|SingleFeeTransfer { fee, fee_token, .. }| {
376 (fee_token.clone(), Signed::<Fee>::of_unsigned(*fee).negate())
377 });
378 FeeExcess::of_one_or_two(one_or_two)
379 }
380
381 pub fn of_singles(singles: OneOrTwo<SingleFeeTransfer>) -> Result<Self, String> {
383 match singles {
384 OneOrTwo::One(a) => Ok(Self(OneOrTwo::One(a))),
385 OneOrTwo::Two((one, two)) => {
386 if one.fee_token == two.fee_token {
387 Ok(Self(OneOrTwo::Two((one, two))))
388 } else {
389 Err(format!(
392 "Cannot combine single fee transfers with incompatible tokens: {:?} <> {:?}",
393 one, two
394 ))
395 }
396 }
397 }
398 }
399}
400
401#[derive(Debug, Clone, PartialEq)]
402pub struct CoinbaseFeeTransfer {
403 pub receiver_pk: CompressedPubKey,
404 pub fee: Fee,
405}
406
407impl CoinbaseFeeTransfer {
408 pub fn create(receiver_pk: CompressedPubKey, fee: Fee) -> Self {
409 Self { receiver_pk, fee }
410 }
411
412 pub fn receiver(&self) -> AccountId {
413 AccountId {
414 public_key: self.receiver_pk.clone(),
415 token_id: TokenId::default(),
416 }
417 }
418}
419
420#[derive(Debug, Clone, PartialEq)]
422pub struct Coinbase {
423 pub receiver: CompressedPubKey,
424 pub amount: Amount,
425 pub fee_transfer: Option<CoinbaseFeeTransfer>,
426}
427
428impl Coinbase {
429 fn is_valid(&self) -> bool {
430 match &self.fee_transfer {
431 None => true,
432 Some(CoinbaseFeeTransfer { fee, .. }) => Amount::of_fee(fee) <= self.amount,
433 }
434 }
435
436 pub fn create(
437 amount: Amount,
438 receiver: CompressedPubKey,
439 fee_transfer: Option<CoinbaseFeeTransfer>,
440 ) -> Result<Coinbase, String> {
441 let mut this = Self {
442 receiver: receiver.clone(),
443 amount,
444 fee_transfer,
445 };
446
447 if this.is_valid() {
448 let adjusted_fee_transfer = this.fee_transfer.as_ref().and_then(|ft| {
449 if receiver != ft.receiver_pk {
450 Some(ft.clone())
451 } else {
452 None
453 }
454 });
455 this.fee_transfer = adjusted_fee_transfer;
456 Ok(this)
457 } else {
458 Err("Coinbase.create: invalid coinbase".to_string())
459 }
460 }
461
462 fn expected_supply_increase(&self) -> Result<Amount, String> {
464 let Self {
465 amount,
466 fee_transfer,
467 ..
468 } = self;
469
470 match fee_transfer {
471 None => Ok(*amount),
472 Some(CoinbaseFeeTransfer { fee, .. }) => amount
473 .checked_sub(&Amount::of_fee(fee))
474 .map(|_| *amount)
476 .ok_or_else(|| "Coinbase underflow".to_string()),
477 }
478 }
479
480 pub fn fee_excess(&self) -> Result<FeeExcess, String> {
481 self.expected_supply_increase().map(|_| FeeExcess::empty())
482 }
483
484 pub fn receiver(&self) -> AccountId {
486 AccountId::new(self.receiver.clone(), TokenId::default())
487 }
488
489 pub fn account_access_statuses(
491 &self,
492 status: &TransactionStatus,
493 ) -> Vec<(AccountId, zkapp_command::AccessedOrNot)> {
494 let access_status = match status {
495 TransactionStatus::Applied => zkapp_command::AccessedOrNot::Accessed,
496 TransactionStatus::Failed(_) => zkapp_command::AccessedOrNot::NotAccessed,
497 };
498
499 let mut ids = Vec::with_capacity(2);
500
501 if let Some(fee_transfer) = self.fee_transfer.as_ref() {
502 ids.push((fee_transfer.receiver(), access_status.clone()));
503 };
504
505 ids.push((self.receiver(), access_status));
506
507 ids
508 }
509
510 pub fn accounts_referenced(&self) -> Vec<AccountId> {
512 self.account_access_statuses(&TransactionStatus::Applied)
513 .into_iter()
514 .map(|(id, _status)| id)
515 .collect()
516 }
517}
518
519#[derive(Clone, PartialEq)]
524pub struct Memo(pub [u8; 34]);
525
526impl std::fmt::Debug for Memo {
527 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528 use crate::staged_ledger::hash::OCamlString;
529
530 f.write_fmt(format_args!("\"{}\"", self.0.to_ocaml_str()))
534 }
535}
536
537impl std::str::FromStr for Memo {
538 type Err = ();
539
540 fn from_str(s: &str) -> Result<Self, Self::Err> {
541 let length = std::cmp::min(s.len(), Self::DIGEST_LENGTH) as u8;
542 let mut memo: [u8; Self::MEMO_LENGTH] = std::array::from_fn(|i| (i == 0) as u8);
543 memo[Self::TAG_INDEX] = Self::BYTES_TAG;
544 memo[Self::LENGTH_INDEX] = length;
545 let padded = format!("{s:\0<32}");
546 memo[2..].copy_from_slice(
547 &padded.as_bytes()[..std::cmp::min(padded.len(), Self::DIGEST_LENGTH)],
548 );
549 Ok(Memo(memo))
550 }
551}
552
553impl std::fmt::Display for Memo {
554 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
555 if self.0[0] != Self::BYTES_TAG {
556 return Err(std::fmt::Error);
557 }
558
559 let length = self.0[1] as usize;
560 let memo_slice = &self.0[2..2 + length];
561 let memo_str = String::from_utf8_lossy(memo_slice).to_string();
562 let trimmed = memo_str.trim_end_matches('\0').to_string();
563
564 write!(f, "{trimmed}")
565 }
566}
567
568impl Memo {
569 const TAG_INDEX: usize = 0;
570 const LENGTH_INDEX: usize = 1;
571
572 const DIGEST_TAG: u8 = 0x00;
573 const BYTES_TAG: u8 = 0x01;
574
575 const DIGEST_LENGTH: usize = 32; const DIGEST_LENGTH_BYTE: u8 = Self::DIGEST_LENGTH as u8;
577
578 const MEMO_LENGTH: usize = Self::DIGEST_LENGTH + 2;
580
581 const MAX_INPUT_LENGTH: usize = Self::DIGEST_LENGTH;
582
583 const MAX_DIGESTIBLE_STRING_LENGTH: usize = 1000;
584
585 pub fn to_bits(&self) -> [bool; std::mem::size_of::<Self>() * 8] {
586 use crate::proofs::transaction::legacy_input::BitsIterator;
587
588 const NBYTES: usize = 34;
589 const NBITS: usize = NBYTES * 8;
590 assert_eq!(std::mem::size_of::<Self>(), NBYTES);
591
592 let mut iter = BitsIterator {
593 index: 0,
594 number: self.0,
595 }
596 .take(NBITS);
597 std::array::from_fn(|_| iter.next().unwrap())
598 }
599
600 pub fn hash(&self) -> Fp {
601 use poseidon::hash::{hash_with_kimchi, legacy};
602
603 let mut inputs = legacy::Inputs::new();
605 inputs.append_bytes(&self.0);
606 hash_with_kimchi(&MINA_ZKAPP_MEMO, &inputs.to_fields())
607 }
608
609 pub fn as_slice(&self) -> &[u8] {
610 self.0.as_slice()
611 }
612
613 pub fn dummy() -> Self {
615 Self([0; 34])
617 }
618
619 pub fn empty() -> Self {
620 let mut array = [0; 34];
621 array[0] = 1;
622 Self(array)
623 }
624
625 #[cfg(test)]
628 pub fn from_ocaml_str(s: &str) -> Self {
629 use crate::staged_ledger::hash::OCamlString;
630
631 Self(<[u8; 34]>::from_ocaml_str(s))
632 }
633
634 pub fn with_number(number: usize) -> Self {
635 let s = format!("{:034}", number);
636 assert_eq!(s.len(), 34);
637 Self(s.into_bytes().try_into().unwrap())
638 }
639
640 fn create_by_digesting_string_exn(s: &str) -> Self {
642 if s.len() > Self::MAX_DIGESTIBLE_STRING_LENGTH {
643 panic!("Too_long_digestible_string");
644 }
645
646 let mut memo = [0; 34];
647 memo[Self::TAG_INDEX] = Self::DIGEST_TAG;
648 memo[Self::LENGTH_INDEX] = Self::DIGEST_LENGTH_BYTE;
649
650 use blake2::{
651 digest::{Update, VariableOutput},
652 Blake2bVar,
653 };
654 let mut hasher = Blake2bVar::new(32).expect("Invalid Blake2bVar output size");
655 hasher.update(s.as_bytes());
656 hasher.finalize_variable(&mut memo[2..]).unwrap();
657
658 Self(memo)
659 }
660
661 pub fn gen() -> Self {
663 use rand::distributions::{Alphanumeric, DistString};
664 let random_string = Alphanumeric.sample_string(&mut rand::thread_rng(), 50);
665
666 Self::create_by_digesting_string_exn(&random_string)
667 }
668}
669
670#[derive(Clone, Debug, PartialEq)]
671pub enum UserCommand {
672 SignedCommand(Box<signed_command::SignedCommand>),
673 ZkAppCommand(Box<zkapp_command::ZkAppCommand>),
674}
675
676impl From<&UserCommand> for MinaBaseUserCommandStableV2 {
677 fn from(user_command: &UserCommand) -> Self {
678 match user_command {
679 UserCommand::SignedCommand(signed_command) => {
680 MinaBaseUserCommandStableV2::SignedCommand((&(*(signed_command.clone()))).into())
681 }
682 UserCommand::ZkAppCommand(zkapp_command) => {
683 MinaBaseUserCommandStableV2::ZkappCommand((&(*(zkapp_command.clone()))).into())
684 }
685 }
686 }
687}
688
689impl TryFrom<&MinaBaseUserCommandStableV2> for UserCommand {
690 type Error = InvalidBigInt;
691
692 fn try_from(user_command: &MinaBaseUserCommandStableV2) -> Result<Self, Self::Error> {
693 match user_command {
694 MinaBaseUserCommandStableV2::SignedCommand(signed_command) => Ok(
695 UserCommand::SignedCommand(Box::new(signed_command.try_into()?)),
696 ),
697 MinaBaseUserCommandStableV2::ZkappCommand(zkapp_command) => Ok(
698 UserCommand::ZkAppCommand(Box::new(zkapp_command.try_into()?)),
699 ),
700 }
701 }
702}
703
704impl binprot::BinProtWrite for UserCommand {
705 fn binprot_write<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
706 let p2p: MinaBaseUserCommandStableV2 = self.into();
707 p2p.binprot_write(w)
708 }
709}
710
711impl binprot::BinProtRead for UserCommand {
712 fn binprot_read<R: std::io::Read + ?Sized>(r: &mut R) -> Result<Self, binprot::Error> {
713 let p2p = MinaBaseUserCommandStableV2::binprot_read(r)?;
714 match UserCommand::try_from(&p2p) {
715 Ok(cmd) => Ok(cmd),
716 Err(e) => Err(binprot::Error::CustomError(Box::new(e))),
717 }
718 }
719}
720
721impl UserCommand {
722 pub fn account_access_statuses(
724 &self,
725 status: &TransactionStatus,
726 ) -> Vec<(AccountId, AccessedOrNot)> {
727 match self {
728 UserCommand::SignedCommand(cmd) => cmd.account_access_statuses(status).to_vec(),
729 UserCommand::ZkAppCommand(cmd) => cmd.account_access_statuses(status),
730 }
731 }
732
733 pub fn accounts_referenced(&self) -> Vec<AccountId> {
735 self.account_access_statuses(&TransactionStatus::Applied)
736 .into_iter()
737 .map(|(id, _status)| id)
738 .collect()
739 }
740
741 pub fn fee_payer(&self) -> AccountId {
742 match self {
743 UserCommand::SignedCommand(cmd) => cmd.fee_payer(),
744 UserCommand::ZkAppCommand(cmd) => cmd.fee_payer(),
745 }
746 }
747
748 pub fn valid_until(&self) -> Slot {
749 match self {
750 UserCommand::SignedCommand(cmd) => cmd.valid_until(),
751 UserCommand::ZkAppCommand(cmd) => {
752 let ZkAppCommand { fee_payer, .. } = &**cmd;
753 fee_payer.body.valid_until.unwrap_or_else(Slot::max)
754 }
755 }
756 }
757
758 pub fn applicable_at_nonce(&self) -> Nonce {
759 match self {
760 UserCommand::SignedCommand(cmd) => cmd.nonce(),
761 UserCommand::ZkAppCommand(cmd) => cmd.applicable_at_nonce(),
762 }
763 }
764
765 pub fn expected_target_nonce(&self) -> Nonce {
766 self.applicable_at_nonce().succ()
767 }
768
769 pub fn fee(&self) -> Fee {
771 match self {
772 UserCommand::SignedCommand(cmd) => cmd.fee(),
773 UserCommand::ZkAppCommand(cmd) => cmd.fee(),
774 }
775 }
776
777 pub fn weight(&self) -> u64 {
778 match self {
779 UserCommand::SignedCommand(cmd) => cmd.weight(),
780 UserCommand::ZkAppCommand(cmd) => cmd.weight(),
781 }
782 }
783
784 pub fn fee_per_wu(&self) -> FeeRate {
786 FeeRate::make_exn(self.fee(), self.weight())
787 }
788
789 pub fn fee_token(&self) -> TokenId {
790 match self {
791 UserCommand::SignedCommand(cmd) => cmd.fee_token(),
792 UserCommand::ZkAppCommand(cmd) => cmd.fee_token(),
793 }
794 }
795
796 pub fn extract_vks(&self) -> Vec<(AccountId, VerificationKeyWire)> {
797 match self {
798 UserCommand::SignedCommand(_) => vec![],
799 UserCommand::ZkAppCommand(zkapp) => zkapp.extract_vks(),
800 }
801 }
802
803 pub fn to_valid_unsafe(self) -> valid::UserCommand {
805 match self {
806 UserCommand::SignedCommand(cmd) => valid::UserCommand::SignedCommand(cmd),
807 UserCommand::ZkAppCommand(cmd) => {
808 valid::UserCommand::ZkAppCommand(Box::new(zkapp_command::valid::ZkAppCommand {
809 zkapp_command: *cmd,
810 }))
811 }
812 }
813 }
814
815 pub fn to_verifiable<F>(
817 &self,
818 status: &TransactionStatus,
819 find_vk: F,
820 ) -> Result<verifiable::UserCommand, String>
821 where
822 F: Fn(Fp, &AccountId) -> Result<VerificationKeyWire, String>,
823 {
824 use verifiable::UserCommand::{SignedCommand, ZkAppCommand};
825 match self {
826 UserCommand::SignedCommand(cmd) => Ok(SignedCommand(cmd.clone())),
827 UserCommand::ZkAppCommand(zkapp) => Ok(ZkAppCommand(Box::new(
828 zkapp_command::verifiable::create(zkapp, status.is_failed(), find_vk)?,
829 ))),
830 }
831 }
832
833 pub fn load_vks_from_ledger(
834 account_ids: HashSet<AccountId>,
835 ledger: &crate::Mask,
836 ) -> HashMap<AccountId, VerificationKeyWire> {
837 let ids: Vec<_> = account_ids.iter().cloned().collect();
838 let locations: Vec<_> = ledger
839 .location_of_account_batch(&ids)
840 .into_iter()
841 .filter_map(|(_, addr)| addr)
842 .collect();
843 ledger
844 .get_batch(&locations)
845 .into_iter()
846 .filter_map(|(_, account)| {
847 let account = account.unwrap();
848 let zkapp = account.zkapp.as_ref()?;
849 let vk = zkapp.verification_key.clone()?;
850 Some((account.id(), vk))
851 })
852 .collect()
853 }
854
855 pub fn load_vks_from_ledger_accounts(
856 accounts: &BTreeMap<AccountId, Account>,
857 ) -> HashMap<AccountId, VerificationKeyWire> {
858 accounts
859 .iter()
860 .filter_map(|(_, account)| {
861 let zkapp = account.zkapp.as_ref()?;
862 let vk = zkapp.verification_key.clone()?;
863 Some((account.id(), vk))
864 })
865 .collect()
866 }
867
868 pub fn to_all_verifiable<S, F>(
869 ts: Vec<MaybeWithStatus<UserCommand>>,
870 load_vk_cache: F,
871 ) -> Result<Vec<MaybeWithStatus<verifiable::UserCommand>>, String>
872 where
873 S: zkapp_command::ToVerifiableStrategy,
874 F: Fn(HashSet<AccountId>) -> S::Cache,
875 {
876 let accounts_referenced: HashSet<AccountId> = ts
877 .iter()
878 .flat_map(|cmd| match cmd.cmd() {
879 UserCommand::SignedCommand(_) => Vec::new(),
880 UserCommand::ZkAppCommand(cmd) => cmd.accounts_referenced(),
881 })
882 .collect();
883 let mut vk_cache = load_vk_cache(accounts_referenced);
884
885 ts.into_iter()
886 .map(|cmd| {
887 let is_failed = cmd.is_failed();
888 let MaybeWithStatus { cmd, status } = cmd;
889 match cmd {
890 UserCommand::SignedCommand(c) => Ok(MaybeWithStatus {
891 cmd: verifiable::UserCommand::SignedCommand(c),
892 status,
893 }),
894 UserCommand::ZkAppCommand(c) => {
895 let zkapp_verifiable = S::create_all(&c, is_failed, &mut vk_cache)?;
896 Ok(MaybeWithStatus {
897 cmd: verifiable::UserCommand::ZkAppCommand(Box::new(zkapp_verifiable)),
898 status,
899 })
900 }
901 }
902 })
903 .collect()
904 }
905
906 fn has_insufficient_fee(&self) -> bool {
907 const MINIMUM_USER_COMMAND_FEE: Fee = Fee::from_u64(1000000);
909 self.fee() < MINIMUM_USER_COMMAND_FEE
910 }
911
912 fn has_zero_vesting_period(&self) -> bool {
913 match self {
914 UserCommand::SignedCommand(_cmd) => false,
915 UserCommand::ZkAppCommand(cmd) => cmd.has_zero_vesting_period(),
916 }
917 }
918
919 fn is_incompatible_version(&self) -> bool {
920 match self {
921 UserCommand::SignedCommand(_cmd) => false,
922 UserCommand::ZkAppCommand(cmd) => cmd.is_incompatible_version(),
923 }
924 }
925
926 fn is_disabled(&self) -> bool {
927 match self {
928 UserCommand::SignedCommand(_cmd) => false,
929 UserCommand::ZkAppCommand(_cmd) => false, }
931 }
932
933 fn valid_size(&self) -> Result<(), String> {
934 match self {
935 UserCommand::SignedCommand(_cmd) => Ok(()),
936 UserCommand::ZkAppCommand(cmd) => cmd.valid_size(),
937 }
938 }
939
940 pub fn check_well_formedness(&self) -> Result<(), Vec<WellFormednessError>> {
941 let mut errors: Vec<_> = [
942 (
943 Self::has_insufficient_fee as fn(_) -> _,
944 WellFormednessError::InsufficientFee,
945 ),
946 (
947 Self::has_zero_vesting_period,
948 WellFormednessError::ZeroVestingPeriod,
949 ),
950 (
951 Self::is_incompatible_version,
952 WellFormednessError::IncompatibleVersion,
953 ),
954 (
955 Self::is_disabled,
956 WellFormednessError::TransactionTypeDisabled,
957 ),
958 ]
959 .iter()
960 .filter_map(|(fun, e)| if fun(self) { Some(e.clone()) } else { None })
961 .collect();
962
963 if let Err(e) = self.valid_size() {
964 errors.push(WellFormednessError::ZkappTooBig(e));
965 }
966
967 if errors.is_empty() {
968 Ok(())
969 } else {
970 Err(errors)
971 }
972 }
973}
974
975#[derive(Debug, Clone, Hash, PartialEq, Eq, thiserror::Error)]
976pub enum WellFormednessError {
977 #[error("Insufficient Fee")]
978 InsufficientFee,
979 #[error("Zero vesting period")]
980 ZeroVestingPeriod,
981 #[error("Zkapp too big: {0}")]
982 ZkappTooBig(String),
983 #[error("Transaction type disabled")]
984 TransactionTypeDisabled,
985 #[error("Incompatible version")]
986 IncompatibleVersion,
987}
988
989impl GenericCommand for UserCommand {
990 fn fee(&self) -> Fee {
991 match self {
992 UserCommand::SignedCommand(cmd) => cmd.fee(),
993 UserCommand::ZkAppCommand(cmd) => cmd.fee(),
994 }
995 }
996
997 fn forget(&self) -> UserCommand {
998 self.clone()
999 }
1000}
1001
1002impl GenericTransaction for Transaction {
1003 fn is_fee_transfer(&self) -> bool {
1004 matches!(self, Transaction::FeeTransfer(_))
1005 }
1006 fn is_coinbase(&self) -> bool {
1007 matches!(self, Transaction::Coinbase(_))
1008 }
1009 fn is_command(&self) -> bool {
1010 matches!(self, Transaction::Command(_))
1011 }
1012}
1013
1014#[derive(Clone, Debug, derive_more::From)]
1051pub enum Transaction {
1052 Command(UserCommand),
1054 FeeTransfer(FeeTransfer),
1056 Coinbase(Coinbase),
1058}
1059
1060impl Transaction {
1061 pub fn is_zkapp(&self) -> bool {
1062 matches!(self, Self::Command(UserCommand::ZkAppCommand(_)))
1063 }
1064
1065 pub fn fee_excess(&self) -> Result<FeeExcess, String> {
1066 use Transaction::*;
1067 use UserCommand::*;
1068
1069 match self {
1070 Command(SignedCommand(cmd)) => Ok(cmd.fee_excess()),
1071 Command(ZkAppCommand(cmd)) => Ok(cmd.fee_excess()),
1072 FeeTransfer(ft) => ft.fee_excess(),
1073 Coinbase(cb) => cb.fee_excess(),
1074 }
1075 }
1076
1077 pub fn public_keys(&self) -> Vec<CompressedPubKey> {
1079 use Transaction::*;
1080 use UserCommand::*;
1081
1082 let to_pks = |ids: Vec<AccountId>| ids.into_iter().map(|id| id.public_key).collect();
1083
1084 match self {
1085 Command(SignedCommand(cmd)) => to_pks(cmd.accounts_referenced()),
1086 Command(ZkAppCommand(cmd)) => to_pks(cmd.accounts_referenced()),
1087 FeeTransfer(ft) => ft.receiver_pks().cloned().collect(),
1088 Coinbase(cb) => to_pks(cb.accounts_referenced()),
1089 }
1090 }
1091
1092 pub fn account_access_statuses(
1094 &self,
1095 status: &TransactionStatus,
1096 ) -> Vec<(AccountId, zkapp_command::AccessedOrNot)> {
1097 use Transaction::*;
1098 use UserCommand::*;
1099
1100 match self {
1101 Command(SignedCommand(cmd)) => cmd.account_access_statuses(status).to_vec(),
1102 Command(ZkAppCommand(cmd)) => cmd.account_access_statuses(status),
1103 FeeTransfer(ft) => ft
1104 .receivers()
1105 .map(|account_id| (account_id, AccessedOrNot::Accessed))
1106 .collect(),
1107 Coinbase(cb) => cb.account_access_statuses(status),
1108 }
1109 }
1110
1111 pub fn accounts_referenced(&self) -> Vec<AccountId> {
1113 self.account_access_statuses(&TransactionStatus::Applied)
1114 .into_iter()
1115 .map(|(id, _status)| id)
1116 .collect()
1117 }
1118}
1119
1120impl From<&Transaction> for MinaTransactionTransactionStableV2 {
1121 fn from(value: &Transaction) -> Self {
1122 match value {
1123 Transaction::Command(v) => Self::Command(Box::new(v.into())),
1124 Transaction::FeeTransfer(v) => Self::FeeTransfer(v.into()),
1125 Transaction::Coinbase(v) => Self::Coinbase(v.into()),
1126 }
1127 }
1128}
1129
1130#[cfg(any(test, feature = "fuzzing"))]
1131pub mod for_tests;