Skip to main content

amaru_kernel/cardano/memoized/
transaction_output.rs

1// Copyright 2025 PRAGMA
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use pallas_primitives::conway::Multiasset;
16
17use crate::{
18    Address, Bytes, Hash, Legacy, MemoizedDatum, MemoizedScript, NonEmptyKeyValuePairs, PositiveCoin,
19    ShelleyDelegationPart, StakeCredential, Value, cbor, decode_script, encode_script, serialize_memoized_script,
20    size::CREDENTIAL,
21};
22
23#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
24pub struct MemoizedTransactionOutput {
25    #[serde(skip)]
26    pub is_legacy: bool,
27
28    #[serde(serialize_with = "serialize_address")]
29    #[serde(deserialize_with = "deserialize_address")]
30    pub address: Address,
31
32    #[serde(serialize_with = "serialize_value")]
33    #[serde(deserialize_with = "deserialize_value")]
34    pub value: Value,
35
36    pub datum: MemoizedDatum,
37
38    #[serde(serialize_with = "serialize_script")]
39    #[serde(deserialize_with = "deserialize_script")]
40    pub script: Option<MemoizedScript>,
41}
42
43impl MemoizedTransactionOutput {
44    pub fn delegate(&self) -> Option<StakeCredential> {
45        match &self.address {
46            Address::Shelley(shelley) => match shelley.delegation() {
47                ShelleyDelegationPart::Key(key) => Some(StakeCredential::AddrKeyhash(*key)),
48                ShelleyDelegationPart::Script(script) => Some(StakeCredential::ScriptHash(*script)),
49                ShelleyDelegationPart::Pointer(..) | ShelleyDelegationPart::Null => None,
50            },
51            Address::Byron(..) => None,
52            Address::Stake(..) => unreachable!("stake address inside output?"),
53        }
54    }
55}
56
57impl<'b, C> cbor::Decode<'b, C> for MemoizedTransactionOutput {
58    fn decode(d: &mut cbor::Decoder<'b>, ctx: &mut C) -> Result<Self, cbor::decode::Error> {
59        let data_type = d.datatype()?;
60
61        if matches!(data_type, cbor::Type::MapIndef | cbor::Type::Map) {
62            decode_modern_output(d, ctx)
63        } else if matches!(data_type, cbor::Type::ArrayIndef | cbor::Type::Array) {
64            decode_legacy_output(d, ctx)
65        } else {
66            Err(cbor::decode::Error::type_mismatch(data_type))
67        }
68    }
69}
70
71fn decode_legacy_output<C>(
72    d: &mut cbor::Decoder<'_>,
73    ctx: &mut C,
74) -> Result<MemoizedTransactionOutput, cbor::decode::Error> {
75    let len = d.array()?;
76
77    Ok(MemoizedTransactionOutput {
78        is_legacy: true,
79        address: decode_address(d.bytes()?)?,
80        value: d.decode_with(ctx)?,
81        datum: match len {
82            Some(2) => MemoizedDatum::None,
83            Some(3) => d.decode_with::<_, Legacy<_>>(ctx)?.0,
84            Some(_) => {
85                return Err(cbor::decode::Error::message(format!(
86                    "expected legacy transaction output array length of 2 or 3, got {len:?}",
87                )));
88            }
89            None => {
90                if cbor::decode_break(d, len)? {
91                    MemoizedDatum::None
92                } else {
93                    let datum: Legacy<MemoizedDatum> = d.decode_with(ctx)?;
94                    if !cbor::decode_break(d, len)? {
95                        return Err(cbor::decode::Error::message(
96                            "expected break after legacy transaction output datum",
97                        ));
98                    }
99                    datum.0
100                }
101            }
102        },
103        script: None,
104    })
105}
106
107fn decode_modern_output<C>(
108    d: &mut cbor::Decoder<'_>,
109    ctx: &mut C,
110) -> Result<MemoizedTransactionOutput, cbor::decode::Error> {
111    let (address, value, datum, script) = cbor::heterogeneous_map(
112        d,
113        (None, None, MemoizedDatum::None, None),
114        |d| d.u8(),
115        |d, state, field| {
116            match field {
117                0 => state.0 = Some(decode_address(d.bytes()?)?),
118                1 => state.1 = Some(d.decode_with(ctx)?),
119                2 => state.2 = d.decode_with(ctx)?,
120                3 => state.3 = Some(decode_script(d, ctx)?),
121                _ => return cbor::unexpected_field::<MemoizedTransactionOutput, _>(field),
122            }
123            Ok(())
124        },
125    )?;
126
127    Ok(MemoizedTransactionOutput {
128        is_legacy: false,
129        address: address.ok_or_else(|| cbor::missing_field::<MemoizedTransactionOutput, Address>(0))?,
130        value: value.ok_or_else(|| cbor::missing_field::<MemoizedTransactionOutput, Value>(1))?,
131        datum,
132        script,
133    })
134}
135
136fn decode_address(address_bytes: &[u8]) -> Result<Address, cbor::decode::Error> {
137    Address::from_bytes(address_bytes).map_err(|e| cbor::decode::Error::message(format!("invalid address: {e:?}")))
138}
139
140impl<C> cbor::Encode<C> for MemoizedTransactionOutput {
141    fn encode<W: cbor::encode::Write>(
142        &self,
143        e: &mut cbor::Encoder<W>,
144        ctx: &mut C,
145    ) -> Result<(), cbor::encode::Error<W::Error>> {
146        if self.is_legacy {
147            e.begin_array()?;
148            e.bytes(&self.address.to_vec())?;
149            e.encode_with(&self.value, ctx)?;
150            match self.datum {
151                MemoizedDatum::None => (),
152                MemoizedDatum::Hash(hash) => {
153                    e.bytes(&hash[..])?;
154                }
155                MemoizedDatum::Inline(..) => unreachable!("legacy output with inline datum ?!"),
156            }
157            e.end()?;
158        } else {
159            e.begin_map()?;
160
161            e.u8(0)?;
162            e.bytes(&self.address.to_vec())?;
163
164            e.u8(1)?;
165            e.encode_with(&self.value, ctx)?;
166
167            if !matches!(&self.datum, &MemoizedDatum::None) {
168                e.u8(2)?;
169            }
170            e.encode_with(&self.datum, ctx)?;
171
172            match &self.script {
173                None => (),
174                Some(script) => {
175                    e.u8(3)?;
176                    encode_script(script, e)?;
177                }
178            }
179
180            e.end()?;
181        }
182
183        Ok(())
184    }
185}
186
187// --------------------------------------------------------------------- Helpers
188
189fn serialize_address<S: serde::ser::Serializer>(addr: &Address, serializer: S) -> Result<S::Ok, S::Error> {
190    serializer.serialize_str(&addr.to_hex())
191}
192
193fn deserialize_address<'de, D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Address, D::Error> {
194    let bytes: &str = serde::Deserialize::deserialize(deserializer)?;
195    Address::from_hex(bytes).map_err(serde::de::Error::custom)
196}
197
198// FIXME: Eventually allow serializing complete values, not just coins.
199fn serialize_value<S: serde::ser::Serializer>(value: &Value, serializer: S) -> Result<S::Ok, S::Error> {
200    match value {
201        Value::Coin(coin) => serializer.serialize_u64(*coin),
202        Value::Multiasset(coin, _) => serializer.serialize_u64(*coin),
203    }
204}
205
206fn deserialize_value<'de, D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Value, D::Error> {
207    #[derive(serde::Deserialize)]
208    enum ValueHelper {
209        Coin(u64),
210        Multiasset(u64, Vec<(String, Vec<(String, u64)>)>),
211    }
212
213    let helper: ValueHelper = serde::Deserialize::deserialize(deserializer)?;
214
215    match helper {
216        ValueHelper::Coin(coin) => Ok(Value::Coin(coin)),
217        ValueHelper::Multiasset(coin, multiasset_data) => {
218            let mut converted_multiasset = Vec::new();
219
220            for (policy_id, assets) in multiasset_data {
221                let policy_id = hex::decode(&policy_id)
222                    .map_err(|_| serde::de::Error::custom(format!("invalid hex string: {policy_id}")))?;
223
224                let mut converted_assets = Vec::new();
225                for (asset_name, quantity) in assets {
226                    let asset_name = hex::decode(&asset_name)
227                        .map_err(|_| serde::de::Error::custom(format!("invalid hex string: {asset_name}")))?;
228
229                    converted_assets.push((
230                        Bytes::from(asset_name),
231                        quantity
232                            .try_into()
233                            .map_err(|_| serde::de::Error::custom(format!("invalid quantity value: {quantity}")))?,
234                    ));
235                }
236
237                let policy_id: Hash<CREDENTIAL> = Hash::from(policy_id.as_slice());
238
239                let pairs = NonEmptyKeyValuePairs::try_from(converted_assets)
240                    .map_err(|e| serde::de::Error::custom(format!("invalid asset bundle: {e}")))?
241                    .as_pallas();
242
243                converted_multiasset.push((policy_id, pairs));
244            }
245
246            let multiasset: Multiasset<PositiveCoin> =
247                Multiasset::from_vec(converted_multiasset).ok_or(serde::de::Error::custom("empty multiasset"))?;
248            Ok(Value::Multiasset(coin, multiasset))
249        }
250    }
251}
252
253pub fn serialize_script<S: serde::ser::Serializer>(
254    opt: &Option<MemoizedScript>,
255    serializer: S,
256) -> Result<S::Ok, S::Error> {
257    match opt {
258        None => serializer.serialize_none(),
259        Some(script) => serialize_memoized_script(script, serializer),
260    }
261}
262
263pub fn deserialize_script<'de, D: serde::de::Deserializer<'de>>(
264    deserializer: D,
265) -> Result<Option<MemoizedScript>, D::Error> {
266    match serde::Deserialize::deserialize(deserializer)? {
267        None::<super::PlaceholderScript> => Ok(None),
268        Some(placeholder) => Ok(Some(MemoizedScript::try_from(placeholder).map_err(serde::de::Error::custom)?)),
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use crate::{
276        Hash,
277        cbor::{self, Encode},
278    };
279
280    #[test]
281    fn test_encode_decode_output_with_datum_hash() {
282        let hash_bytes = [1u8; 32];
283        let datum_hash = Hash::<32>::from(hash_bytes);
284
285        let datum = MemoizedDatum::Hash(datum_hash);
286
287        let original = MemoizedTransactionOutput {
288            is_legacy: false,
289            address: Address::from_hex("61bbe56449ba4ee08c471d69978e01db384d31e29133af4546e6057335").unwrap(),
290            value: Value::Coin(1500000),
291            datum,
292            script: None,
293        };
294
295        let mut encoder = cbor::Encoder::new(Vec::new());
296        let mut ctx = ();
297        original.encode(&mut encoder, &mut ctx).unwrap();
298        let encoded_bytes = encoder.writer().clone();
299
300        let mut decoder = cbor::Decoder::new(&encoded_bytes);
301        let decoded: MemoizedTransactionOutput = decoder.decode_with(&mut ctx).unwrap();
302
303        assert_eq!(original, decoded);
304    }
305
306    #[test]
307    fn test_encode_decode_output_with_datum_hash_legacy() {
308        let hash_bytes = [1u8; 32];
309        let datum_hash = Hash::<32>::from(hash_bytes);
310
311        let datum = MemoizedDatum::Hash(datum_hash);
312
313        let original = MemoizedTransactionOutput {
314            is_legacy: true,
315            address: Address::from_hex("61bbe56449ba4ee08c471d69978e01db384d31e29133af4546e6057335").unwrap(),
316            value: Value::Coin(1500000),
317            datum,
318            script: None,
319        };
320
321        let mut encoder = cbor::Encoder::new(Vec::new());
322        let mut ctx = ();
323        original.encode(&mut encoder, &mut ctx).unwrap();
324        let encoded_bytes = encoder.writer().clone();
325
326        let mut decoder = cbor::Decoder::new(&encoded_bytes);
327        let decoded: MemoizedTransactionOutput = decoder.decode_with(&mut ctx).unwrap();
328
329        assert_eq!(original, decoded);
330    }
331
332    #[test]
333    fn test_encode_decode_output_no_datum_no_script() {
334        let original = MemoizedTransactionOutput {
335            is_legacy: false,
336            address: Address::from_hex("61bbe56449ba4ee08c471d69978e01db384d31e29133af4546e6057335").unwrap(),
337            value: Value::Coin(1500000),
338            datum: MemoizedDatum::None,
339            script: None,
340        };
341
342        let mut encoder = cbor::Encoder::new(Vec::new());
343        let mut ctx = ();
344        original.encode(&mut encoder, &mut ctx).unwrap();
345        let encoded_bytes = encoder.writer().clone();
346
347        let mut decoder = cbor::Decoder::new(&encoded_bytes);
348        let decoded: MemoizedTransactionOutput = decoder.decode_with(&mut ctx).unwrap();
349
350        assert_eq!(original, decoded);
351    }
352}