Skip to main content

iota_sdk_types/transaction/
serialization.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use serde_with::{DeserializeAs, SerializeAs};
7
8use crate::{Identifier, ObjectId, ObjectReference};
9
10mod input_argument {
11    use super::*;
12    use crate::{
13        Version,
14        transaction::{Input, SharedObjectReference},
15    };
16
17    // Mirrors the default derived serialization of `Input`; the manual impl
18    // only exists to keep the BCS form as the `CallArg`/`ObjectArg` protocol
19    // encoding below.
20    #[derive(serde::Deserialize, serde::Serialize)]
21    #[serde(rename = "Input")]
22    enum ReadableInput {
23        /// A move value serialized as BCS.
24        ///
25        /// For normal operations this is required to be a move primitive type
26        /// and not contain structs or objects.
27        Pure(#[serde(with = "crate::_serde::ReadableBase64Encoded")] Vec<u8>),
28        /// A move object that is either immutable or address owned
29        ImmutableOrOwned(ObjectReference),
30        /// A move object whose owner is "Shared"
31        Shared(SharedObjectReference),
32        /// A move object that is attempted to be received in this transaction.
33        Receiving(ObjectReference),
34    }
35
36    #[derive(serde::Deserialize, serde::Serialize)]
37    #[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
38    enum CallArg {
39        Pure(#[serde(with = "::serde_with::As::<::serde_with::Bytes>")] Vec<u8>),
40        Object(ObjectArg),
41    }
42
43    #[derive(serde::Deserialize, serde::Serialize)]
44    #[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
45    enum ObjectArg {
46        ImmutableOrOwned(ObjectReference),
47        Shared {
48            object_id: ObjectId,
49            initial_shared_version: Version,
50            mutable: bool,
51        },
52        Receiving(ObjectReference),
53    }
54
55    impl Serialize for Input {
56        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
57        where
58            S: Serializer,
59        {
60            if serializer.is_human_readable() {
61                let readable = match self.clone() {
62                    Input::Pure(value) => ReadableInput::Pure(value),
63                    Input::ImmutableOrOwned(object_ref) => {
64                        ReadableInput::ImmutableOrOwned(object_ref)
65                    }
66                    Input::Shared(shared) => ReadableInput::Shared(shared),
67                    Input::Receiving(object_ref) => ReadableInput::Receiving(object_ref),
68                };
69                readable.serialize(serializer)
70            } else {
71                let binary = match self.clone() {
72                    Input::Pure(value) => CallArg::Pure(value),
73                    Input::ImmutableOrOwned(object_ref) => {
74                        CallArg::Object(ObjectArg::ImmutableOrOwned(object_ref))
75                    }
76                    Input::Shared(SharedObjectReference {
77                        object_id,
78                        initial_shared_version,
79                        mutable,
80                    }) => CallArg::Object(ObjectArg::Shared {
81                        object_id,
82                        initial_shared_version,
83                        mutable,
84                    }),
85                    Input::Receiving(object_ref) => {
86                        CallArg::Object(ObjectArg::Receiving(object_ref))
87                    }
88                };
89                binary.serialize(serializer)
90            }
91        }
92    }
93
94    impl<'de> Deserialize<'de> for Input {
95        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
96        where
97            D: Deserializer<'de>,
98        {
99            if deserializer.is_human_readable() {
100                ReadableInput::deserialize(deserializer).map(|readable| match readable {
101                    ReadableInput::Pure(value) => Input::Pure(value),
102                    ReadableInput::ImmutableOrOwned(object_ref) => {
103                        Input::ImmutableOrOwned(object_ref)
104                    }
105                    ReadableInput::Shared(shared) => Input::Shared(shared),
106                    ReadableInput::Receiving(object_ref) => Input::Receiving(object_ref),
107                })
108            } else {
109                CallArg::deserialize(deserializer).map(|binary| match binary {
110                    CallArg::Pure(value) => Input::Pure(value),
111                    CallArg::Object(ObjectArg::ImmutableOrOwned(object_ref)) => {
112                        Input::ImmutableOrOwned(object_ref)
113                    }
114                    CallArg::Object(ObjectArg::Shared {
115                        object_id,
116                        initial_shared_version,
117                        mutable,
118                    }) => Input::Shared(SharedObjectReference {
119                        object_id,
120                        initial_shared_version,
121                        mutable,
122                    }),
123                    CallArg::Object(ObjectArg::Receiving(object_ref)) => {
124                        Input::Receiving(object_ref)
125                    }
126                })
127            }
128        }
129    }
130}
131
132pub(crate) use signed_transaction::SignedTransactionWithIntentMessage;
133
134mod signed_transaction {
135    use serde::ser::SerializeSeq;
136
137    use super::*;
138    use crate::{
139        Intent, UserSignature,
140        transaction::{SignedTransaction, Transaction},
141    };
142
143    pub(crate) struct SignedTransactionWithIntentMessage;
144
145    #[derive(serde::Serialize)]
146    #[serde(rename = "SignedTransaction")]
147    struct BinarySignedTransactionWithIntentMessageRef<'a> {
148        intent: &'a Intent,
149        transaction: &'a Transaction,
150        signatures: &'a Vec<UserSignature>,
151    }
152
153    #[derive(serde::Deserialize)]
154    #[cfg_attr(
155        feature = "bcs-schema",
156        derive(iota_bcs_schema::BcsSchema),
157        bcs_schema(name = "intent-signed-transaction")
158    )]
159    #[serde(rename = "SignedTransaction")]
160    struct BinarySignedTransactionWithIntentMessage {
161        intent: Intent,
162        transaction: Transaction,
163        signatures: Vec<UserSignature>,
164    }
165
166    impl SerializeAs<SignedTransaction> for SignedTransactionWithIntentMessage {
167        fn serialize_as<S>(
168            transaction: &SignedTransaction,
169            serializer: S,
170        ) -> Result<S::Ok, S::Error>
171        where
172            S: Serializer,
173        {
174            if serializer.is_human_readable() {
175                transaction.serialize(serializer)
176            } else {
177                let SignedTransaction {
178                    transaction,
179                    signatures,
180                } = transaction;
181                let intent = Intent {
182                    scope: crate::IntentScope::TransactionData,
183                    version: crate::IntentVersion::V0,
184                    app_id: crate::IntentAppId::Iota,
185                };
186                let binary = BinarySignedTransactionWithIntentMessageRef {
187                    intent: &intent,
188                    transaction,
189                    signatures,
190                };
191
192                let mut s = serializer.serialize_seq(Some(1))?;
193                s.serialize_element(&binary)?;
194                s.end()
195            }
196        }
197    }
198
199    impl<'de> DeserializeAs<'de, SignedTransaction> for SignedTransactionWithIntentMessage {
200        fn deserialize_as<D>(deserializer: D) -> Result<SignedTransaction, D::Error>
201        where
202            D: Deserializer<'de>,
203        {
204            if deserializer.is_human_readable() {
205                SignedTransaction::deserialize(deserializer)
206            } else {
207                struct V;
208                impl<'de> serde::de::Visitor<'de> for V {
209                    type Value = SignedTransaction;
210
211                    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
212                        formatter.write_str("expected a sequence with length 1")
213                    }
214
215                    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
216                    where
217                        A: serde::de::SeqAccess<'de>,
218                    {
219                        if seq.size_hint().is_some_and(|size| size != 1) {
220                            return Err(serde::de::Error::custom(
221                                "expected a sequence with length 1",
222                            ));
223                        }
224
225                        let BinarySignedTransactionWithIntentMessage {
226                            intent:
227                                Intent {
228                                    scope: crate::IntentScope::TransactionData,
229                                    version: crate::IntentVersion::V0,
230                                    app_id: crate::IntentAppId::Iota,
231                                },
232                            transaction,
233                            signatures,
234                        } = seq.next_element()?.ok_or_else(|| {
235                            serde::de::Error::custom("expected a sequence with length 1")
236                        })?
237                        else {
238                            return Err(serde::de::Error::custom("invalid intent"));
239                        };
240                        Ok(SignedTransaction {
241                            transaction,
242                            signatures,
243                        })
244                    }
245                }
246
247                deserializer.deserialize_seq(V)
248            }
249        }
250    }
251}
252
253/// Deserialize an `Identifier` without validating that it is a valid Move
254/// identifier. This is used for deserializing the module in `MoveCall`
255/// commands, where BCS bytes could contain invalid identifiers but we still
256/// want to be able to deserialize them and let the move VM handle the
257/// validation.
258pub(super) fn deserialize_ident_unchecked<'de, D>(d: D) -> Result<Identifier, D::Error>
259where
260    D: serde::Deserializer<'de>,
261{
262    if d.is_human_readable() {
263        serde::Deserialize::deserialize(d)
264    } else {
265        let s: String = serde::Deserialize::deserialize(d)?;
266        Ok(Identifier::new_unchecked(s))
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use base64ct::{Base64, Encoding};
273    #[cfg(target_arch = "wasm32")]
274    use wasm_bindgen_test::wasm_bindgen_test as test;
275
276    use crate::{
277        Address, ObjectDigest, ObjectId, ObjectReference, Version,
278        transaction::{
279            Argument, EndOfEpochTransactionKind, Input, SharedObjectReference, Transaction,
280            TransactionDenyRulesUpdate, TransactionKind,
281        },
282    };
283
284    #[test]
285    fn argument() {
286        let test_cases = [
287            (Argument::Gas, serde_json::json!("Gas")),
288            (Argument::Input(1), serde_json::json!({"Input": 1})),
289            (Argument::Result(2), serde_json::json!({"Result": 2})),
290            (
291                Argument::NestedResult(3, 4),
292                serde_json::json!({"NestedResult": [3, 4]}),
293            ),
294        ];
295
296        for (case, expected) in test_cases {
297            let actual = serde_json::to_value(case).unwrap();
298            assert_eq!(actual, expected);
299            println!("{actual}");
300
301            let deser = serde_json::from_value(expected).unwrap();
302            assert_eq!(case, deser);
303        }
304    }
305
306    #[test]
307    fn input_argument() {
308        let object_ref = serde_json::json!({
309            "object_id": "0x0000000000000000000000000000000000000000000000000000000000000000",
310            "version": "1",
311            "digest": "11111111111111111111111111111111"
312        });
313        let test_cases = [
314            (
315                Input::Pure(vec![1, 2, 3, 4]),
316                serde_json::json!({ "Pure": "AQIDBA==" }),
317            ),
318            (
319                Input::ImmutableOrOwned(ObjectReference::new(
320                    ObjectId::ZERO,
321                    Version::from_u64(1),
322                    ObjectDigest::ZERO,
323                )),
324                serde_json::json!({ "ImmutableOrOwned": object_ref }),
325            ),
326            (
327                Input::Shared(SharedObjectReference {
328                    object_id: ObjectId::ZERO,
329                    initial_shared_version: Version::from_u64(1),
330                    mutable: true,
331                }),
332                serde_json::json!({
333                  "Shared": {
334                    "object_id": "0x0000000000000000000000000000000000000000000000000000000000000000",
335                    "initial_shared_version": "1",
336                    "mutable": true
337                  }
338                }),
339            ),
340            (
341                Input::Receiving(ObjectReference::new(
342                    ObjectId::ZERO,
343                    Version::from_u64(1),
344                    ObjectDigest::ZERO,
345                )),
346                serde_json::json!({ "Receiving": object_ref }),
347            ),
348        ];
349
350        for (case, expected) in test_cases {
351            let actual = serde_json::to_value(&case).unwrap();
352            assert_eq!(actual, expected);
353            println!("{actual}");
354
355            let deser = serde_json::from_value(expected).unwrap();
356            assert_eq!(case, deser);
357        }
358    }
359
360    #[test]
361    fn transaction_fixtures() {
362        // Look in the fixtures folder to see how to update them
363        const GENESIS_TRANSACTION: &str = include_str!("fixtures/genesis");
364        const CONSENSUS_PROLOGUE: &str = include_str!("fixtures/consensus-commit-prologue-v1");
365        const EPOCH_CHANGE: &str = include_str!("fixtures/change-epoch-v2");
366        const PTB: &str = include_str!("fixtures/ptb");
367
368        for fixture in [GENESIS_TRANSACTION, CONSENSUS_PROLOGUE, EPOCH_CHANGE, PTB] {
369            let fixture = Base64::decode_vec(fixture.trim()).unwrap();
370            let tx: Transaction = bcs::from_bytes(&fixture).unwrap();
371            assert_eq!(bcs::to_bytes(&tx).unwrap(), fixture);
372
373            let json = serde_json::to_string_pretty(&tx).unwrap();
374            println!("{json}");
375            assert_eq!(tx, serde_json::from_str(&json).unwrap());
376        }
377    }
378
379    /// Pins the BCS wire format of
380    /// [`TransactionKind::TransactionDenyRulesUpdate`].
381    ///
382    /// Runs one sample per switch (one-hot) so swapping any two bool fields
383    /// changes some expected byte string; the six delta lists carry distinct
384    /// contents and lengths for the same reason.
385    #[test]
386    fn transaction_deny_rules_update_bcs_pin() {
387        // Field order: package_publish, package_upgrade, shared_object,
388        // user_transaction, receiving_objects, move_authenticator.
389        let mut samples: Vec<[bool; 6]> = (0..6)
390            .map(|hot| std::array::from_fn(|i| i == hot))
391            .collect();
392        samples.push([false; 6]);
393        samples.push([true; 6]);
394
395        for switches in samples {
396            let [
397                package_publish_disabled,
398                package_upgrade_disabled,
399                shared_object_disabled,
400                user_transaction_disabled,
401                receiving_objects_disabled,
402                move_authenticator_disabled,
403            ] = switches;
404            let kind = TransactionKind::TransactionDenyRulesUpdate(TransactionDenyRulesUpdate {
405                epoch: 7,
406                round: 11,
407                added_addresses: [Address::new([1; 32]), Address::new([2; 32])].into(),
408                removed_addresses: [Address::new([3; 32])].into(),
409                added_objects: [ObjectId::new([4; 32])].into(),
410                removed_objects: [ObjectId::new([5; 32]), ObjectId::new([6; 32])].into(),
411                added_packages: [
412                    ObjectId::new([7; 32]),
413                    ObjectId::new([8; 32]),
414                    ObjectId::new([9; 32]),
415                ]
416                .into(),
417                removed_packages: [].into(),
418                package_publish_disabled,
419                package_upgrade_disabled,
420                shared_object_disabled,
421                user_transaction_disabled,
422                receiving_objects_disabled,
423                move_authenticator_disabled,
424                deny_rules_obj_initial_shared_version: Version::from_u64(42),
425            });
426
427            let mut expected = vec![6]; // TransactionKind variant tag
428            expected.extend(7u64.to_le_bytes()); // epoch
429            expected.extend(11u64.to_le_bytes()); // round
430            expected.push(2); // added_addresses length
431            expected.extend([1; 32]);
432            expected.extend([2; 32]);
433            expected.push(1); // removed_addresses length
434            expected.extend([3; 32]);
435            expected.push(1); // added_objects length
436            expected.extend([4; 32]);
437            expected.push(2); // removed_objects length
438            expected.extend([5; 32]);
439            expected.extend([6; 32]);
440            expected.push(3); // added_packages length
441            expected.extend([7; 32]);
442            expected.extend([8; 32]);
443            expected.extend([9; 32]);
444            expected.push(0); // removed_packages length
445            expected.extend(switches.map(u8::from));
446            expected.extend(42u64.to_le_bytes()); // deny_rules_obj_initial_shared_version
447
448            assert_eq!(bcs::to_bytes(&kind).unwrap(), expected);
449            assert_eq!(bcs::from_bytes::<TransactionKind>(&expected).unwrap(), kind);
450        }
451    }
452
453    /// Pins the BCS wire format of
454    /// [`EndOfEpochTransactionKind::TransactionDenyRulesCreate`].
455    #[test]
456    fn transaction_deny_rules_create_bcs_pin() {
457        let kind = EndOfEpochTransactionKind::TransactionDenyRulesCreate;
458        assert_eq!(bcs::to_bytes(&kind).unwrap(), [4]);
459        assert_eq!(
460            bcs::from_bytes::<EndOfEpochTransactionKind>(&[4]).unwrap(),
461            kind
462        );
463
464        let tx_kind = TransactionKind::EndOfEpoch(vec![kind]);
465        let expected = [4, 1, 4]; // EndOfEpoch tag, list length, create tag
466        assert_eq!(bcs::to_bytes(&tx_kind).unwrap(), expected);
467        assert_eq!(
468            bcs::from_bytes::<TransactionKind>(&expected).unwrap(),
469            tx_kind
470        );
471    }
472}