1use std::mem;
16
17#[cfg(any(test, feature = "test-utils"))]
18use crate::to_cbor;
19use crate::{
20 AssetName, Bytes, Certificate, Hash, Hasher, Lovelace, MemoizedTransactionOutput, NULL_HASH32, NetworkId,
21 NonEmptyKeyValuePairs, NonEmptySet, NonZeroInt, PositiveCoin, Proposal, ProposalId, RewardAccount, Set,
22 TransactionInput, Voter, VotingProcedure, cbor,
23 size::{CREDENTIAL, KEY},
24};
25
26#[derive(Debug, Clone, PartialEq, Eq, cbor::Encode, serde::Serialize, serde::Deserialize)]
36#[cbor(map)]
37pub struct TransactionBody {
38 #[cbor(skip)]
39 hash: Hash<{ TransactionBody::HASH_SIZE }>,
40
41 #[cbor(skip)]
42 original_size: u64,
43
44 #[n(0)]
45 pub inputs: Set<TransactionInput>,
46
47 #[n(1)]
48 pub outputs: Vec<MemoizedTransactionOutput>,
49
50 #[n(2)]
51 pub fee: Lovelace,
52
53 #[n(3)]
54 pub validity_interval_end: Option<u64>,
55
56 #[n(4)]
57 pub certificates: Option<NonEmptySet<Certificate>>,
58
59 #[n(5)]
60 pub withdrawals: Option<NonEmptyKeyValuePairs<RewardAccount, Lovelace>>,
61
62 #[n(7)]
63 pub auxiliary_data_hash: Option<Bytes>,
64
65 #[n(8)]
66 pub validity_interval_start: Option<u64>,
67
68 #[n(9)]
69 pub mint: Option<NonEmptyKeyValuePairs<Hash<CREDENTIAL>, NonEmptyKeyValuePairs<AssetName, NonZeroInt>>>,
70
71 #[n(11)]
72 pub script_data_hash: Option<Hash<32>>,
73
74 #[n(13)]
75 pub collateral: Option<NonEmptySet<TransactionInput>>,
76
77 #[n(14)]
78 pub required_signers: Option<NonEmptySet<Hash<KEY>>>,
79
80 #[n(15)]
81 pub network_id: Option<NetworkId>,
82
83 #[n(16)]
84 pub collateral_return: Option<MemoizedTransactionOutput>,
85
86 #[n(17)]
87 pub total_collateral: Option<Lovelace>,
88
89 #[n(18)]
90 pub reference_inputs: Option<NonEmptySet<TransactionInput>>,
91
92 #[n(19)]
93 pub votes: Option<NonEmptyKeyValuePairs<Voter, NonEmptyKeyValuePairs<ProposalId, VotingProcedure>>>,
94
95 #[n(20)]
96 pub proposals: Option<NonEmptySet<Proposal>>,
97
98 #[n(21)]
99 pub treasury_value: Option<Lovelace>,
100
101 #[n(22)]
102 pub donation: Option<PositiveCoin>,
103}
104
105impl TransactionBody {
106 pub const HASH_SIZE: usize = 32;
108
109 pub fn id(&self) -> Hash<{ Self::HASH_SIZE }> {
113 self.hash
114 }
115
116 #[allow(clippy::len_without_is_empty)]
117 pub fn len(&self) -> u64 {
118 self.original_size
119 }
120
121 #[cfg(any(test, feature = "test-utils"))]
124 pub fn new(
125 inputs: impl IntoIterator<Item = TransactionInput>,
126 outputs: impl IntoIterator<Item = MemoizedTransactionOutput>,
127 fee: Lovelace,
128 ) -> Self {
129 let mut body = Self {
130 inputs: Set::from(inputs.into_iter().collect::<Vec<_>>()),
131 outputs: outputs.into_iter().collect(),
132 fee,
133 ..Self::default()
134 };
135
136 let bytes = to_cbor(&body);
137
138 body.original_size = bytes.len() as u64;
139 body.hash = Hasher::<{ 8 * Self::HASH_SIZE }>::hash(&bytes[..]);
140
141 body
142 }
143}
144
145impl Default for TransactionBody {
146 fn default() -> Self {
147 Self {
148 hash: NULL_HASH32,
149 original_size: 0,
150 inputs: Set::from(vec![]),
151 outputs: vec![],
152 fee: 0,
153 validity_interval_end: None,
154 certificates: None,
155 withdrawals: None,
156 auxiliary_data_hash: None,
157 validity_interval_start: None,
158 mint: None,
159 script_data_hash: None,
160 collateral: None,
161 required_signers: None,
162 network_id: None,
163 collateral_return: None,
164 total_collateral: None,
165 reference_inputs: None,
166 votes: None,
167 proposals: None,
168 treasury_value: None,
169 donation: None,
170 }
171 }
172}
173
174impl<'b, C> cbor::Decode<'b, C> for TransactionBody {
188 fn decode(d: &mut cbor::Decoder<'b>, ctx: &mut C) -> Result<Self, cbor::decode::Error> {
189 #[derive(Default)]
190 struct RequiredFields {
191 inputs: Option<Set<TransactionInput>>,
192 outputs: Option<Vec<MemoizedTransactionOutput>>,
193 fee: Option<Lovelace>,
194 }
195
196 #[derive(Default)]
197 struct State {
198 optional: TransactionBody,
199 required: RequiredFields,
200 }
201
202 let original_bytes = d.input();
203 let start_position = d.position();
204
205 let mut state = State::default();
206
207 cbor::heterogeneous_map(
208 d,
209 &mut state,
210 |d| d.u64(),
211 |d, st, k| {
212 match k {
213 0 => blanket(&mut st.required.inputs, k, d, ctx)?,
214 1 => blanket(&mut st.required.outputs, k, d, ctx)?,
215 2 => blanket(&mut st.required.fee, k, d, ctx)?,
216 3 => blanket(&mut st.optional.validity_interval_end, k, d, ctx)?,
217 4 => blanket(&mut st.optional.certificates, k, d, ctx)?,
218 5 => blanket(&mut st.optional.withdrawals, k, d, ctx)?,
219 7 => blanket(&mut st.optional.auxiliary_data_hash, k, d, ctx)?,
221 8 => blanket(&mut st.optional.validity_interval_start, k, d, ctx)?,
222 9 => blanket(&mut st.optional.mint, k, d, ctx)?,
223 11 => blanket(&mut st.optional.script_data_hash, k, d, ctx)?,
225 13 => blanket(&mut st.optional.collateral, k, d, ctx)?,
227 14 => blanket(&mut st.optional.required_signers, k, d, ctx)?,
228 15 => blanket(&mut st.optional.network_id, k, d, ctx)?,
229 16 => blanket(&mut st.optional.collateral_return, k, d, ctx)?,
230 17 => blanket(&mut st.optional.total_collateral, k, d, ctx)?,
231 18 => blanket(&mut st.optional.reference_inputs, k, d, ctx)?,
232 19 => blanket(&mut st.optional.votes, k, d, ctx)?,
233 20 => blanket(&mut st.optional.proposals, k, d, ctx)?,
234 21 => blanket(&mut st.optional.treasury_value, k, d, ctx)?,
235 22 => blanket(&mut st.optional.donation, k, d, ctx)?,
236 _ => {
237 let position = d.position();
238 return Err(cbor::decode::Error::message(format!("unrecognised field key: {k}")).at(position));
239 }
240 };
241
242 Ok(())
243 },
244 )?;
245
246 let end_position = d.position();
247
248 Ok(TransactionBody {
249 hash: Hasher::<{ TransactionBody::HASH_SIZE * 8 }>::hash(&original_bytes[start_position..end_position]),
250 original_size: (end_position - start_position) as u64, inputs: expect_field(mem::take(&mut state.required.inputs), 0, "inputs")?,
252 outputs: expect_field(mem::take(&mut state.required.outputs), 1, "outputs")?,
253 fee: expect_field(mem::take(&mut state.required.fee), 2, "fee")?,
254 ..state.optional
255 })
256 }
257}
258
259fn blanket<'d, C, T>(
260 field: &mut Option<T>,
261 k: u64,
262 d: &mut cbor::Decoder<'d>,
263 ctx: &mut C,
264) -> Result<(), cbor::decode::Error>
265where
266 T: cbor::Decode<'d, C>,
267{
268 if field.is_some() {
269 return Err(cbor::decode::Error::message(format!("duplicate field entry with key {k}")));
270 }
271
272 *field = Some(d.decode_with(ctx)?);
273
274 Ok(())
275}
276
277fn expect_field<T>(decoded: Option<T>, key: u64, name: &str) -> Result<T, cbor::decode::Error> {
278 decoded.ok_or(cbor::decode::Error::message(format!("missing expected field '{name}' at key {key}")))
279}
280
281#[cfg(test)]
282mod tests {
283 use test_case::test_case;
284
285 use super::TransactionBody;
286 use crate::cbor;
287
288 macro_rules! fixture {
289 ("conway", $id:expr) => { fixture!(@inner "conway", $id) };
291
292 ($era:literal, $id:expr) => {
294 compile_error!(
295 "invalid era: expected one of: \"conway\""
296 );
297 };
298
299 (@inner $era:literal, $id:expr) => {{
300 $crate::try_include_cbor!(concat!(
301 "cbor.decode/transaction_body/",
302 $era,
303 "/",
304 $id,
305 "/sample.cbor",
306 ))
307 }};
308 }
309
310 #[test_case(
311 fixture!("conway", "70beb79b18459ff5b826ebeea82ecf566ab79e166ff5749f761ed402ad459466"), 86;
312 "simple input -> output payout"
313 )]
314 #[test_case(
315 fixture!("conway", "950bde838976daf2f0019ac6cc5a995e86d99f5ff1b2ffddaaa9ef44b558e4db"), 48;
316 "present but empty inputs set"
317 )]
318 #[test_case(
319 fixture!("conway", "ab17a2693346d625ea9afd9bdb9f9b357e60533b3e4cb7576e2c117b94d1c180"), 49;
320 "present but empty outputs set"
321 )]
322 #[test_case(
323 fixture!("conway", "c20c7e395ef81d8a6172510408446afc240d533bff18f9dca905e78187c2bcd8"), 80;
324 "null fee"
325 )]
326 fn decode_wellformed(result: Result<TransactionBody, cbor::decode::Error>, expected_size: u64) {
327 match result {
328 Err(err) => panic!("{err}"),
329 Ok(body) => assert_eq!(body.len(), expected_size),
330 }
331 }
332
333 #[test_case(
334 fixture!("conway", "d36a2619a672494604e11bb447cbcf5231e9f2ba25c2169177edc941bd50ad6c"),
335 "decode error: missing expected field 'inputs' at key 0";
336 "empty body"
337 )]
338 #[test_case(
339 fixture!("conway", "b563891d222561e435b475632a3bdcca58cc3c8ec80ab6b51e0a5c96b6a35e1b"),
340 "decode error: missing expected field 'outputs' at key 1";
341 "missing outputs"
342 )]
343 #[test_case(
344 fixture!("conway", "9d34025191e23c5996e20c2c0d1718f5cb1d9c4a37a5cb153cbd03c66b59128f"),
345 "decode error: missing expected field 'fee' at key 2";
346 "missing fee"
347 )]
348 #[test_case(
349 fixture!("conway", "c5f2d5b7e9b8f615c52296e04b3050cf35ad4e8a457a25adaeb2a933de1bf624"),
350 "decode error at position 81: empty set when expecting at least one element";
351 "empty certificates"
352 )]
353 #[test_case(
354 fixture!("conway", "3b5478c6446496b6ff71c738c83fbf251841dd45cda074b0ac935b1428a52f66"),
355 "unexpected type map at position 81: expected array";
356 "malformed certificates"
357 )]
358 #[test_case(
359 fixture!("conway", "5123113da4c8e2829748dbcd913ac69f572516836731810c2fc1f8b86351bfee"),
360 "decode error at position 87: empty map when expecting at least one key/value pair";
361 "empty votes"
362 )]
363 #[test_case(
364 fixture!("conway", "6c6596eda4e61f6f294b522c17f3c9fb6fbddcfac0e55af88ddc96747b3e0478"),
365 "unexpected type array at position 87: expected map";
366 "malformed votes"
367 )]
368 #[test_case(
369 fixture!("conway", "402a8a9024d4160928e574c73aa66c66d92f9856c3fa2392242f7a92b8e9c347"),
370 "decode error at position 81: empty map when expecting at least one key/value pair";
371 "empty mint"
372 )]
373 #[test_case(
374 fixture!("conway", "48d5440656ceefda1ac25506dcd175e77a486113733a89e48a5a2f401d2cbfda"),
375 "decode error at position 87: empty set when expecting at least one element";
376 "empty collateral inputs"
377 )]
378 #[test_case(
379 fixture!("conway", "5cbed05f218d893dac6d9af847aa7429576019a1314b633e3fde55cb74e43be1"),
380 "decode error at position 87: empty set when expecting at least one element";
381 "empty required signers"
382 )]
383 #[test_case(
384 fixture!("conway", "71d780bdcc0cf8d1a8dafc6641797d46f1be835be6dd63b2b4bb5651df808d79"),
385 "decode error at position 81: empty map when expecting at least one key/value pair";
386 "empty withdrawals"
387 )]
388 #[test_case(
389 fixture!("conway", "477981b76e218802d5ce8c673abefe0b4031f09b0be5283a5b577ca109671771"),
390 "decode error at position 87: empty set when expecting at least one element";
391 "empty proposals"
392 )]
393 #[test_case(
394 fixture!("conway", "675954a2fe5ad3638a360902a4c7307a598d6e13b977279df640a663023c14bd"),
395 "decode error: decoding 0 as PositiveCoin";
396 "null donation"
397 )]
398 #[test_case(
399 fixture!("conway", "5280ac2b10897dd26c9d7377ae681a6ea1dc3eec197563ab5bf3ab7907e0e709"),
400 "decode error: duplicate field entry with key 2";
401 "duplicate fields keys"
402 )]
403 fn decode_malformed(result: Result<TransactionBody, cbor::decode::Error>, expected_error: &str) {
404 assert_eq!(result.map_err(|e| e.to_string()), Err(expected_error.to_string()));
405 }
406}