mina_tree/scan_state/transaction_logic/
transaction_union_payload.rs

1//! Unified transaction representation for SNARK circuits
2//!
3//! This module provides a single, unified structure ([`TransactionUnion`]) that
4//! can represent all transaction types (payments, stake delegations, fee
5//! transfers, coinbase) for SNARK circuit processing. This enables efficient
6//! proof generation by using a single circuit design regardless of the specific
7//! transaction type.
8//!
9//! # Transaction Union
10//!
11//! The [`TransactionUnion`] type encodes all transaction variants using a
12//! tagged union approach:
13//!
14//! - [`Common`]: Fields present in all transactions (fee, nonce, memo, etc.)
15//! - [`Body`]: Transaction-specific fields with interpretation based on [`Tag`]
16//! - [`Tag`]: Discriminates between Payment, StakeDelegation, FeeTransfer, and
17//!   Coinbase
18//!
19//! # Field Interpretation
20//!
21//! Fields in [`Body`] are interpreted differently based on the [`Tag`] value:
22//!
23//! - **Payment**: `source_pk` and `receiver_pk` are sender and recipient
24//! - **Stake Delegation**: `receiver_pk` is the new delegate
25//! - **Fee Transfer**: `receiver_pk` is the fee recipient, `amount` is the fee
26//! - **Coinbase**: `receiver_pk` is the block producer, `amount` is the reward
27//!
28//! # Receipt Chain Hash
29//!
30//! This module also provides functions for computing receipt chain hashes,
31//! which commit to the sequence of transactions applied to an account:
32//!
33//! - [`cons_signed_command_payload`]: Updates receipt chain hash for signed
34//!   commands
35//! - [`cons_zkapp_command_commitment`]: Updates receipt chain hash for zkApp
36//!   commands
37//! - [`checked_cons_signed_command_payload`]: Checked version for use in
38//!   circuits
39//!
40//! # Timing and Vesting
41//!
42//! The module implements timing validation for timed (vested) accounts:
43//!
44//! - [`validate_timing`]: Ensures timing constraints are met for an account
45//!   deduction
46//! - [`validate_nonces`]: Validates transaction nonce matches account nonce
47//! - [`account_check_timing`]: Checks timing status for an account
48//! - [`timing_error_to_user_command_status`]: Converts timing errors to
49//!   transaction failures
50//!
51//! Timed accounts have a minimum balance that decreases over time according to
52//! a vesting schedule. When the minimum balance reaches zero, the account
53//! automatically becomes untimed.
54//!
55//! # Account Helpers
56//!
57//! Utility functions for account operations:
58//!
59//! - [`get_with_location`]: Retrieves an account or creates a placeholder for
60//!   new accounts
61//! - [`ExistingOrNew`]: Indicates whether an account exists or is newly created
62//! - [`add_amount`]/[`sub_amount`]: Safe balance arithmetic with
63//!   overflow/underflow checking
64
65use super::{
66    signed_command::{
67        self, PaymentPayload, SignedCommand, SignedCommandPayload, StakeDelegationPayload,
68    },
69    transaction_partially_applied::set_with_location,
70    Coinbase, CoinbaseFeeTransfer, Memo, SingleFeeTransfer, Transaction, TransactionFailure,
71    UserCommand,
72};
73use crate::{
74    decompress_pk,
75    proofs::{field::Boolean, witness::Witness},
76    scan_state::{
77        currency::{Amount, Balance, Fee, Index, Magnitude, Nonce, Slot},
78        scan_state::transaction_snark::OneOrTwo,
79    },
80    sparse_ledger::LedgerIntf,
81    zkapps::zkapp_logic::ZkAppCommandElt,
82    Account, AccountId, AppendToInputs, ReceiptChainHash, Timing, TokenId,
83};
84use ark_ff::PrimeField;
85use mina_curves::pasta::Fp;
86use mina_hasher::{Hashable, ROInput as LegacyInput};
87use mina_signer::{CompressedPubKey, NetworkId, PubKey, Signature};
88use poseidon::hash::{hash_with_kimchi, params::CODA_RECEIPT_UC, Inputs};
89
90#[derive(Clone)]
91pub struct Common {
92    pub fee: Fee,
93    pub fee_token: TokenId,
94    pub fee_payer_pk: CompressedPubKey,
95    pub nonce: Nonce,
96    pub valid_until: Slot,
97    pub memo: Memo,
98}
99
100#[derive(Clone, Debug)]
101pub enum Tag {
102    Payment = 0,
103    StakeDelegation = 1,
104    FeeTransfer = 2,
105    Coinbase = 3,
106}
107
108impl Tag {
109    pub fn is_user_command(&self) -> Boolean {
110        match self {
111            Tag::Payment | Tag::StakeDelegation => Boolean::True,
112            Tag::FeeTransfer | Tag::Coinbase => Boolean::False,
113        }
114    }
115
116    pub fn is_payment(&self) -> Boolean {
117        match self {
118            Tag::Payment => Boolean::True,
119            Tag::FeeTransfer | Tag::Coinbase | Tag::StakeDelegation => Boolean::False,
120        }
121    }
122
123    pub fn is_stake_delegation(&self) -> Boolean {
124        match self {
125            Tag::StakeDelegation => Boolean::True,
126            Tag::FeeTransfer | Tag::Coinbase | Tag::Payment => Boolean::False,
127        }
128    }
129
130    pub fn is_fee_transfer(&self) -> Boolean {
131        match self {
132            Tag::FeeTransfer => Boolean::True,
133            Tag::StakeDelegation | Tag::Coinbase | Tag::Payment => Boolean::False,
134        }
135    }
136
137    pub fn is_coinbase(&self) -> Boolean {
138        match self {
139            Tag::Coinbase => Boolean::True,
140            Tag::StakeDelegation | Tag::FeeTransfer | Tag::Payment => Boolean::False,
141        }
142    }
143
144    pub fn to_bits(&self) -> [bool; 3] {
145        let tag = self.clone() as u8;
146        let mut bits = [false; 3];
147        for (index, bit) in [4, 2, 1].iter().enumerate() {
148            bits[index] = tag & bit != 0;
149        }
150        bits
151    }
152
153    pub fn to_untagged_bits(&self) -> [bool; 5] {
154        let mut is_payment = false;
155        let mut is_stake_delegation = false;
156        let mut is_fee_transfer = false;
157        let mut is_coinbase = false;
158        let mut is_user_command = false;
159
160        match self {
161            Tag::Payment => {
162                is_payment = true;
163                is_user_command = true;
164            }
165            Tag::StakeDelegation => {
166                is_stake_delegation = true;
167                is_user_command = true;
168            }
169            Tag::FeeTransfer => is_fee_transfer = true,
170            Tag::Coinbase => is_coinbase = true,
171        }
172
173        [
174            is_payment,
175            is_stake_delegation,
176            is_fee_transfer,
177            is_coinbase,
178            is_user_command,
179        ]
180    }
181}
182
183#[derive(Clone)]
184pub struct Body {
185    pub tag: Tag,
186    pub source_pk: CompressedPubKey,
187    pub receiver_pk: CompressedPubKey,
188    pub token_id: TokenId,
189    pub amount: Amount,
190}
191
192#[derive(Clone)]
193pub struct TransactionUnionPayload {
194    pub common: Common,
195    pub body: Body,
196}
197
198impl Hashable for TransactionUnionPayload {
199    type D = NetworkId;
200
201    fn to_roinput(&self) -> LegacyInput {
202        /*
203            Payment transactions only use the default token-id value 1.
204            The old transaction format encoded the token-id as an u64,
205            however zkApps encode the token-id as a Fp.
206
207            For testing/fuzzing purposes we want the ability to encode
208            arbitrary values different from the default token-id, for this
209            we will extract the LS u64 of the token-id.
210        */
211        let fee_token_id = self.common.fee_token.0.into_bigint().0[0];
212        let token_id = self.body.token_id.0.into_bigint().0[0];
213
214        let mut roi = LegacyInput::new()
215            .append_field(self.common.fee_payer_pk.x)
216            .append_field(self.body.source_pk.x)
217            .append_field(self.body.receiver_pk.x)
218            .append_u64(self.common.fee.as_u64())
219            .append_u64(fee_token_id)
220            .append_bool(self.common.fee_payer_pk.is_odd)
221            .append_u32(self.common.nonce.as_u32())
222            .append_u32(self.common.valid_until.as_u32())
223            .append_bytes(&self.common.memo.0);
224
225        let tag = self.body.tag.clone() as u8;
226        for bit in [4, 2, 1] {
227            roi = roi.append_bool(tag & bit != 0);
228        }
229
230        roi.append_bool(self.body.source_pk.is_odd)
231            .append_bool(self.body.receiver_pk.is_odd)
232            .append_u64(token_id)
233            .append_u64(self.body.amount.as_u64())
234            .append_bool(false) // Used to be `self.body.token_locked`
235    }
236
237    // TODO: this is unused, is it needed?
238    fn domain_string(network_id: NetworkId) -> Option<String> {
239        // Domain strings must have length <= 20
240        match network_id {
241            NetworkId::MAINNET => mina_core::network::mainnet::SIGNATURE_PREFIX,
242            NetworkId::TESTNET => mina_core::network::devnet::SIGNATURE_PREFIX,
243        }
244        .to_string()
245        .into()
246    }
247}
248
249impl TransactionUnionPayload {
250    pub fn of_user_command_payload(payload: &SignedCommandPayload) -> Self {
251        use signed_command::Body::{Payment, StakeDelegation};
252
253        Self {
254            common: Common {
255                fee: payload.common.fee,
256                fee_token: TokenId::default(),
257                fee_payer_pk: payload.common.fee_payer_pk.clone(),
258                nonce: payload.common.nonce,
259                valid_until: payload.common.valid_until,
260                memo: payload.common.memo.clone(),
261            },
262            body: match &payload.body {
263                Payment(PaymentPayload {
264                    receiver_pk,
265                    amount,
266                }) => Body {
267                    tag: Tag::Payment,
268                    source_pk: payload.common.fee_payer_pk.clone(),
269                    receiver_pk: receiver_pk.clone(),
270                    token_id: TokenId::default(),
271                    amount: *amount,
272                },
273                StakeDelegation(StakeDelegationPayload::SetDelegate { new_delegate }) => Body {
274                    tag: Tag::StakeDelegation,
275                    source_pk: payload.common.fee_payer_pk.clone(),
276                    receiver_pk: new_delegate.clone(),
277                    token_id: TokenId::default(),
278                    amount: Amount::zero(),
279                },
280            },
281        }
282    }
283
284    /// <https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/mina_base/transaction_union_payload.ml#L309>
285    pub fn to_input_legacy(&self) -> ::poseidon::hash::legacy::Inputs<Fp> {
286        let mut roi = ::poseidon::hash::legacy::Inputs::new();
287
288        // Self.common
289        {
290            roi.append_u64(self.common.fee.0);
291
292            // TokenId.default
293            // <https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/mina_base/signed_command_payload.ml#L19>
294            roi.append_bool(true);
295            for _ in 0..63 {
296                roi.append_bool(false);
297            }
298
299            // fee_payer_pk
300            roi.append_field(self.common.fee_payer_pk.x);
301            roi.append_bool(self.common.fee_payer_pk.is_odd);
302
303            // nonce
304            roi.append_u32(self.common.nonce.0);
305
306            // valid_until
307            roi.append_u32(self.common.valid_until.0);
308
309            // memo
310            roi.append_bytes(&self.common.memo.0);
311        }
312
313        // Self.body
314        {
315            // tag
316            let tag = self.body.tag.clone() as u8;
317            for bit in [4, 2, 1] {
318                roi.append_bool(tag & bit != 0);
319            }
320
321            // source_pk
322            roi.append_field(self.body.source_pk.x);
323            roi.append_bool(self.body.source_pk.is_odd);
324
325            // receiver_pk
326            roi.append_field(self.body.receiver_pk.x);
327            roi.append_bool(self.body.receiver_pk.is_odd);
328
329            // default token_id
330            roi.append_u64(1);
331
332            // amount
333            roi.append_u64(self.body.amount.0);
334
335            // token_locked
336            roi.append_bool(false);
337        }
338
339        roi
340    }
341}
342
343pub struct TransactionUnion {
344    pub payload: TransactionUnionPayload,
345    pub signer: PubKey,
346    pub signature: Signature,
347}
348
349impl TransactionUnion {
350    /// For SNARK purposes, we inject [Transaction.t]s into a single-variant
351    /// 'tagged-union' record capable of representing all the variants. We
352    /// interpret the fields of this union in different ways depending on the
353    /// value of the [payload.body.tag] field, which represents which variant of
354    /// [Transaction.t] the value corresponds to.
355    ///
356    /// Sometimes we interpret fields in surprising ways in different cases to
357    /// save as much space in the SNARK as possible (e.g.,
358    /// [payload.body.public_key] is interpreted as the recipient of a payment,
359    /// the new delegate of a stake delegation command, and a fee transfer
360    /// recipient for both coinbases and fee-transfers.
361    pub fn of_transaction(tx: &Transaction) -> Self {
362        match tx {
363            Transaction::Command(cmd) => {
364                let UserCommand::SignedCommand(cmd) = cmd else {
365                    unreachable!();
366                };
367
368                let SignedCommand {
369                    payload,
370                    signer,
371                    signature,
372                } = cmd.as_ref();
373
374                TransactionUnion {
375                    payload: TransactionUnionPayload::of_user_command_payload(payload),
376                    signer: decompress_pk(signer).unwrap(),
377                    signature: signature.clone(),
378                }
379            }
380            Transaction::Coinbase(Coinbase {
381                receiver,
382                amount,
383                fee_transfer,
384            }) => {
385                let CoinbaseFeeTransfer {
386                    receiver_pk: other_pk,
387                    fee: other_amount,
388                } = fee_transfer
389                    .clone()
390                    .unwrap_or_else(|| CoinbaseFeeTransfer::create(receiver.clone(), Fee::zero()));
391
392                let signer = decompress_pk(&other_pk).unwrap();
393                let payload = TransactionUnionPayload {
394                    common: Common {
395                        fee: other_amount,
396                        fee_token: TokenId::default(),
397                        fee_payer_pk: other_pk.clone(),
398                        nonce: Nonce::zero(),
399                        valid_until: Slot::max(),
400                        memo: Memo::empty(),
401                    },
402                    body: Body {
403                        source_pk: other_pk,
404                        receiver_pk: receiver.clone(),
405                        token_id: TokenId::default(),
406                        amount: *amount,
407                        tag: Tag::Coinbase,
408                    },
409                };
410
411                TransactionUnion {
412                    payload,
413                    signer,
414                    signature: Signature::dummy(),
415                }
416            }
417            Transaction::FeeTransfer(tr) => {
418                let two = |SingleFeeTransfer {
419                               receiver_pk: pk1,
420                               fee: fee1,
421                               fee_token,
422                           },
423                           SingleFeeTransfer {
424                               receiver_pk: pk2,
425                               fee: fee2,
426                               fee_token: token_id,
427                           }| {
428                    let signer = decompress_pk(&pk2).unwrap();
429                    let payload = TransactionUnionPayload {
430                        common: Common {
431                            fee: fee2,
432                            fee_token,
433                            fee_payer_pk: pk2.clone(),
434                            nonce: Nonce::zero(),
435                            valid_until: Slot::max(),
436                            memo: Memo::empty(),
437                        },
438                        body: Body {
439                            source_pk: pk2,
440                            receiver_pk: pk1,
441                            token_id,
442                            amount: Amount::of_fee(&fee1),
443                            tag: Tag::FeeTransfer,
444                        },
445                    };
446
447                    TransactionUnion {
448                        payload,
449                        signer,
450                        signature: Signature::dummy(),
451                    }
452                };
453
454                match tr.0.clone() {
455                    OneOrTwo::One(t) => {
456                        let other = SingleFeeTransfer::create(
457                            t.receiver_pk.clone(),
458                            Fee::zero(),
459                            t.fee_token.clone(),
460                        );
461                        two(t, other)
462                    }
463                    OneOrTwo::Two((t1, t2)) => two(t1, t2),
464                }
465            }
466        }
467    }
468}
469
470/// Returns the new `receipt_chain_hash`
471pub fn cons_signed_command_payload(
472    command_payload: &SignedCommandPayload,
473    last_receipt_chain_hash: ReceiptChainHash,
474) -> ReceiptChainHash {
475    // Note: Not sure why they use the legacy way of hashing here
476
477    use poseidon::hash::legacy;
478
479    let ReceiptChainHash(last_receipt_chain_hash) = last_receipt_chain_hash;
480    let union = TransactionUnionPayload::of_user_command_payload(command_payload);
481
482    let mut inputs = union.to_input_legacy();
483    inputs.append_field(last_receipt_chain_hash);
484    let hash = legacy::hash_with_kimchi(&legacy::params::CODA_RECEIPT_UC, &inputs.to_fields());
485
486    ReceiptChainHash(hash)
487}
488
489/// Returns the new `receipt_chain_hash`
490pub fn checked_cons_signed_command_payload(
491    payload: &TransactionUnionPayload,
492    last_receipt_chain_hash: ReceiptChainHash,
493    w: &mut Witness<Fp>,
494) -> ReceiptChainHash {
495    use crate::proofs::transaction::{
496        legacy_input::CheckedLegacyInput, transaction_snark::checked_legacy_hash,
497    };
498    use poseidon::hash::legacy;
499
500    let mut inputs = payload.to_checked_legacy_input_owned(w);
501    inputs.append_field(last_receipt_chain_hash.0);
502
503    let receipt_chain_hash = checked_legacy_hash(&legacy::params::CODA_RECEIPT_UC, inputs, w);
504
505    ReceiptChainHash(receipt_chain_hash)
506}
507
508/// prepend account_update index computed by Zkapp_command_logic.apply
509///
510/// <https://github.com/MinaProtocol/mina/blob/3753a8593cc1577bcf4da16620daf9946d88e8e5/src/lib/mina_base/receipt.ml#L66>
511pub fn cons_zkapp_command_commitment(
512    index: Index,
513    e: ZkAppCommandElt,
514    receipt_hash: &ReceiptChainHash,
515) -> ReceiptChainHash {
516    let ZkAppCommandElt::ZkAppCommandCommitment(x) = e;
517
518    let mut inputs = Inputs::new();
519
520    inputs.append(&index);
521    inputs.append_field(x.0);
522    inputs.append(receipt_hash);
523
524    ReceiptChainHash(hash_with_kimchi(&CODA_RECEIPT_UC, &inputs.to_fields()))
525}
526
527pub fn validate_nonces(txn_nonce: Nonce, account_nonce: Nonce) -> Result<(), String> {
528    if account_nonce == txn_nonce {
529        return Ok(());
530    }
531
532    Err(format!(
533        "Nonce in account {:?} different from nonce in transaction {:?}",
534        account_nonce, txn_nonce,
535    ))
536}
537
538pub fn validate_timing(
539    account: &Account,
540    txn_amount: Amount,
541    txn_global_slot: &Slot,
542) -> Result<Timing, String> {
543    let (timing, _) = validate_timing_with_min_balance(account, txn_amount, txn_global_slot)?;
544
545    Ok(timing)
546}
547
548pub fn account_check_timing(
549    txn_global_slot: &Slot,
550    account: &Account,
551) -> (TimingValidation<bool>, Timing) {
552    let (invalid_timing, timing, _) =
553        validate_timing_with_min_balance_impl(account, Amount::from_u64(0), txn_global_slot);
554    // TODO: In OCaml the returned Timing is actually converted to None/Some(fields of Timing structure)
555    (invalid_timing, timing)
556}
557
558fn validate_timing_with_min_balance(
559    account: &Account,
560    txn_amount: Amount,
561    txn_global_slot: &Slot,
562) -> Result<(Timing, MinBalance), String> {
563    use TimingValidation::*;
564
565    let (possibly_error, timing, min_balance) =
566        validate_timing_with_min_balance_impl(account, txn_amount, txn_global_slot);
567
568    match possibly_error {
569        InsufficientBalance(true) => Err(format!(
570            "For timed account, the requested transaction for amount {:?} \
571         at global slot {:?}, the balance {:?} \
572         is insufficient",
573            txn_amount, txn_global_slot, account.balance
574        )),
575        InvalidTiming(true) => Err(format!(
576            "For timed account {}, the requested transaction for amount {:?} \
577         at global slot {:?}, applying the transaction would put the \
578         balance below the calculated minimum balance of {:?}",
579            account.public_key.into_address(),
580            txn_amount,
581            txn_global_slot,
582            min_balance.0
583        )),
584        InsufficientBalance(false) => {
585            panic!("Broken invariant in validate_timing_with_min_balance'")
586        }
587        InvalidTiming(false) => Ok((timing, min_balance)),
588    }
589}
590
591pub fn timing_error_to_user_command_status(
592    timing_result: Result<Timing, String>,
593) -> Result<Timing, TransactionFailure> {
594    match timing_result {
595        Ok(timing) => Ok(timing),
596        Err(err_str) => {
597            /*
598                HACK: we are matching over the full error string instead
599                of including an extra tag string to the Err variant
600            */
601            if err_str.contains("minimum balance") {
602                return Err(TransactionFailure::SourceMinimumBalanceViolation);
603            }
604
605            if err_str.contains("is insufficient") {
606                return Err(TransactionFailure::SourceInsufficientBalance);
607            }
608
609            panic!("Unexpected timed account validation error")
610        }
611    }
612}
613
614pub enum TimingValidation<B> {
615    InsufficientBalance(B),
616    InvalidTiming(B),
617}
618
619#[derive(Debug)]
620struct MinBalance(Balance);
621
622fn validate_timing_with_min_balance_impl(
623    account: &Account,
624    txn_amount: Amount,
625    txn_global_slot: &Slot,
626) -> (TimingValidation<bool>, Timing, MinBalance) {
627    use crate::Timing::*;
628    use TimingValidation::*;
629
630    match &account.timing {
631        Untimed => {
632            // no time restrictions
633            match account.balance.sub_amount(txn_amount) {
634                None => (
635                    InsufficientBalance(true),
636                    Untimed,
637                    MinBalance(Balance::zero()),
638                ),
639                Some(_) => (InvalidTiming(false), Untimed, MinBalance(Balance::zero())),
640            }
641        }
642        Timed {
643            initial_minimum_balance,
644            ..
645        } => {
646            let account_balance = account.balance;
647
648            let (invalid_balance, invalid_timing, curr_min_balance) =
649                match account_balance.sub_amount(txn_amount) {
650                    None => {
651                        // NB: The [initial_minimum_balance] here is the incorrect value,
652                        // but:
653                        // * we don't use it anywhere in this error case; and
654                        // * we don't want to waste time computing it if it will be unused.
655                        (true, false, *initial_minimum_balance)
656                    }
657                    Some(proposed_new_balance) => {
658                        let curr_min_balance = account.min_balance_at_slot(*txn_global_slot);
659
660                        if proposed_new_balance < curr_min_balance {
661                            (false, true, curr_min_balance)
662                        } else {
663                            (false, false, curr_min_balance)
664                        }
665                    }
666                };
667
668            // once the calculated minimum balance becomes zero, the account becomes untimed
669            let possibly_error = if invalid_balance {
670                InsufficientBalance(invalid_balance)
671            } else {
672                InvalidTiming(invalid_timing)
673            };
674
675            if curr_min_balance > Balance::zero() {
676                (
677                    possibly_error,
678                    account.timing.clone(),
679                    MinBalance(curr_min_balance),
680                )
681            } else {
682                (possibly_error, Untimed, MinBalance(Balance::zero()))
683            }
684        }
685    }
686}
687
688pub fn sub_amount(balance: Balance, amount: Amount) -> Result<Balance, String> {
689    balance
690        .sub_amount(amount)
691        .ok_or_else(|| "insufficient funds".to_string())
692}
693
694pub fn add_amount(balance: Balance, amount: Amount) -> Result<Balance, String> {
695    balance
696        .add_amount(amount)
697        .ok_or_else(|| "overflow".to_string())
698}
699
700#[derive(Clone, Debug)]
701pub enum ExistingOrNew<Loc> {
702    Existing(Loc),
703    New,
704}
705
706pub fn get_with_location<L>(
707    ledger: &mut L,
708    account_id: &AccountId,
709) -> Result<(ExistingOrNew<L::Location>, Box<Account>), String>
710where
711    L: LedgerIntf,
712{
713    match ledger.location_of_account(account_id) {
714        Some(location) => match ledger.get(&location) {
715            Some(account) => Ok((ExistingOrNew::Existing(location), account)),
716            None => panic!("Ledger location with no account"),
717        },
718        None => Ok((
719            ExistingOrNew::New,
720            Box::new(Account::create_with(account_id.clone(), Balance::zero())),
721        )),
722    }
723}
724
725pub fn get_account<L>(
726    ledger: &mut L,
727    account_id: AccountId,
728) -> (Box<Account>, ExistingOrNew<L::Location>)
729where
730    L: LedgerIntf,
731{
732    let (loc, account) = get_with_location(ledger, &account_id).unwrap();
733    (account, loc)
734}
735
736pub fn set_account<'a, L>(
737    l: &'a mut L,
738    (a, loc): (Box<Account>, &ExistingOrNew<L::Location>),
739) -> &'a mut L
740where
741    L: LedgerIntf,
742{
743    set_with_location(l, loc, a).unwrap();
744    l
745}