mina_tree/scan_state/transaction_logic/
signed_command.rs

1use mina_p2p_messages::v2::MinaBaseSignedCommandStableV2;
2use mina_signer::{CompressedPubKey, Signature};
3
4use crate::{
5    decompress_pk,
6    scan_state::{
7        currency::{Amount, Fee, Nonce, Signed, Slot},
8        fee_excess::FeeExcess,
9    },
10    AccountId, TokenId,
11};
12
13use super::{zkapp_command::AccessedOrNot, Memo, TransactionStatus};
14
15/// Common fields shared by all signed command payloads.
16///
17/// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:34-48
18/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
19/// Last verified: 2025-10-10
20#[derive(Debug, Clone, PartialEq)]
21pub struct Common {
22    /// Fee paid to the block producer
23    pub fee: Fee,
24    /// Public key paying the fee
25    pub fee_payer_pk: CompressedPubKey,
26    /// Account nonce for replay protection
27    pub nonce: Nonce,
28    /// Slot after which the transaction expires
29    pub valid_until: Slot,
30    /// Optional memo field (34 bytes)
31    pub memo: Memo,
32}
33
34/// Payment payload for transferring MINA tokens.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct PaymentPayload {
37    /// Recipient's public key
38    pub receiver_pk: CompressedPubKey,
39    /// Amount to transfer
40    pub amount: Amount,
41}
42
43/// Stake delegation payload for delegating stake to another account.
44///
45/// OCaml reference: src/lib/mina_base/stake_delegation.ml L:11-13
46/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
47/// Last verified: 2025-10-10
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum StakeDelegationPayload {
50    /// Delegate stake to a new delegate
51    SetDelegate {
52        /// Public key of the new delegate
53        new_delegate: CompressedPubKey,
54    },
55}
56
57impl StakeDelegationPayload {
58    /// OCaml reference: src/lib/mina_base/stake_delegation.ml L:35-37
59    /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
60    /// Last verified: 2025-10-10
61    pub fn receiver(&self) -> AccountId {
62        let Self::SetDelegate { new_delegate } = self;
63        AccountId::new(new_delegate.clone(), TokenId::default())
64    }
65
66    /// OCaml reference: src/lib/mina_base/stake_delegation.ml L:33-33
67    /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
68    /// Last verified: 2025-10-10
69    pub fn receiver_pk(&self) -> &CompressedPubKey {
70        let Self::SetDelegate { new_delegate } = self;
71        new_delegate
72    }
73}
74
75/// The body of a signed command, which can be either a payment or stake
76/// delegation.
77///
78/// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:179-181
79/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
80/// Last verified: 2025-10-10
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum Body {
83    /// Transfer MINA tokens from fee payer to receiver
84    Payment(PaymentPayload),
85    /// Delegate fee payer's stake to another account
86    StakeDelegation(StakeDelegationPayload),
87}
88
89/// Signed command payload containing common fields and the transaction body.
90///
91/// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:239-243
92/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
93/// Last verified: 2025-10-10
94#[derive(Debug, Clone, PartialEq)]
95pub struct SignedCommandPayload {
96    /// Common fields (fee, fee payer, nonce, valid_until, memo)
97    pub common: Common,
98    /// Transaction body (payment or stake delegation)
99    pub body: Body,
100}
101
102impl SignedCommandPayload {
103    pub fn create(
104        fee: Fee,
105        fee_payer_pk: CompressedPubKey,
106        nonce: Nonce,
107        valid_until: Option<Slot>,
108        memo: Memo,
109        body: Body,
110    ) -> Self {
111        Self {
112            common: Common {
113                fee,
114                fee_payer_pk,
115                nonce,
116                valid_until: valid_until.unwrap_or_else(Slot::max),
117                memo,
118            },
119            body,
120        }
121    }
122}
123
124/// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:352-362
125/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
126/// Last verified: 2025-10-10
127mod weight {
128    use super::*;
129
130    fn payment(_: &PaymentPayload) -> u64 {
131        1
132    }
133    fn stake_delegation(_: &StakeDelegationPayload) -> u64 {
134        1
135    }
136    pub fn of_body(body: &Body) -> u64 {
137        match body {
138            Body::Payment(p) => payment(p),
139            Body::StakeDelegation(s) => stake_delegation(s),
140        }
141    }
142}
143
144/// A signed command is a transaction that transfers value or delegates stake.
145///
146/// Signed commands are authorized by a cryptographic signature and consist of a
147/// payload (containing the transaction details) and the signature proving
148/// authorization.
149#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
150#[serde(into = "MinaBaseSignedCommandStableV2")]
151#[serde(try_from = "MinaBaseSignedCommandStableV2")]
152pub struct SignedCommand {
153    /// The transaction payload (common fields and body)
154    pub payload: SignedCommandPayload,
155    /// The public key that signed the transaction
156    pub signer: CompressedPubKey, // TODO: This should be a `mina_signer::PubKey`
157    /// The cryptographic signature
158    pub signature: Signature,
159}
160
161impl SignedCommand {
162    pub fn valid_until(&self) -> Slot {
163        self.payload.common.valid_until
164    }
165
166    /// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:292-292
167    /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
168    /// Last verified: 2025-10-10
169    pub fn fee_payer(&self) -> AccountId {
170        let public_key = self.payload.common.fee_payer_pk.clone();
171        AccountId::new(public_key, TokenId::default())
172    }
173
174    /// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:290-290
175    /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
176    /// Last verified: 2025-10-10
177    pub fn fee_payer_pk(&self) -> &CompressedPubKey {
178        &self.payload.common.fee_payer_pk
179    }
180
181    pub fn weight(&self) -> u64 {
182        let Self {
183            payload: SignedCommandPayload { common: _, body },
184            signer: _,
185            signature: _,
186        } = self;
187        weight::of_body(body)
188    }
189
190    /// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:288-288
191    /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
192    /// Last verified: 2025-10-10
193    pub fn fee_token(&self) -> TokenId {
194        TokenId::default()
195    }
196
197    pub fn fee(&self) -> Fee {
198        self.payload.common.fee
199    }
200
201    /// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:304-304
202    /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
203    /// Last verified: 2025-10-10
204    pub fn receiver(&self) -> AccountId {
205        match &self.payload.body {
206            Body::Payment(payload) => {
207                AccountId::new(payload.receiver_pk.clone(), TokenId::default())
208            }
209            Body::StakeDelegation(payload) => payload.receiver(),
210        }
211    }
212
213    /// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:302-302
214    /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
215    /// Last verified: 2025-10-10
216    pub fn receiver_pk(&self) -> &CompressedPubKey {
217        match &self.payload.body {
218            Body::Payment(payload) => &payload.receiver_pk,
219            Body::StakeDelegation(payload) => payload.receiver_pk(),
220        }
221    }
222
223    pub fn amount(&self) -> Option<Amount> {
224        match &self.payload.body {
225            Body::Payment(payload) => Some(payload.amount),
226            Body::StakeDelegation(_) => None,
227        }
228    }
229
230    pub fn nonce(&self) -> Nonce {
231        self.payload.common.nonce
232    }
233
234    pub fn fee_excess(&self) -> FeeExcess {
235        FeeExcess::of_single((self.fee_token(), Signed::<Fee>::of_unsigned(self.fee())))
236    }
237
238    /// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:320-338
239    /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
240    /// Last verified: 2025-10-10
241    pub fn account_access_statuses(
242        &self,
243        status: &TransactionStatus,
244    ) -> Vec<(AccountId, AccessedOrNot)> {
245        use AccessedOrNot::*;
246        use TransactionStatus::*;
247
248        match status {
249            Applied => vec![(self.fee_payer(), Accessed), (self.receiver(), Accessed)],
250            // Note: The fee payer is always accessed, even if the transaction fails
251            // OCaml reference: src/lib/mina_base/signed_command_payload.mli L:205-209
252            Failed(_) => vec![(self.fee_payer(), Accessed), (self.receiver(), NotAccessed)],
253        }
254    }
255
256    pub fn accounts_referenced(&self) -> Vec<AccountId> {
257        self.account_access_statuses(&TransactionStatus::Applied)
258            .into_iter()
259            .map(|(id, _status)| id)
260            .collect()
261    }
262
263    /// OCaml reference: src/lib/mina_base/signed_command.ml L:417-420
264    /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
265    /// Last verified: 2025-10-10
266    pub fn public_keys(&self) -> [&CompressedPubKey; 2] {
267        [self.fee_payer_pk(), self.receiver_pk()]
268    }
269
270    /// OCaml reference: src/lib/mina_base/signed_command.ml L:422-424
271    /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
272    /// Last verified: 2025-10-10
273    pub fn check_valid_keys(&self) -> bool {
274        self.public_keys()
275            .into_iter()
276            .all(|pk| decompress_pk(pk).is_some())
277    }
278}