bee_block/payload/transaction/essence/
regular.rs

1// Copyright 2020-2021 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use alloc::vec::Vec;
5
6use hashbrown::HashSet;
7use packable::{bounded::BoundedU16, prefix::BoxedSlicePrefix, Packable};
8
9use crate::{
10    input::{Input, INPUT_COUNT_RANGE},
11    output::{InputsCommitment, NativeTokens, Output, OUTPUT_COUNT_RANGE},
12    payload::{OptionalPayload, Payload},
13    protocol::ProtocolParameters,
14    Error,
15};
16
17/// A builder to build a [`RegularTransactionEssence`].
18#[derive(Debug, Clone)]
19#[must_use]
20pub struct RegularTransactionEssenceBuilder {
21    inputs: Vec<Input>,
22    inputs_commitment: InputsCommitment,
23    outputs: Vec<Output>,
24    payload: Option<Payload>,
25}
26
27impl RegularTransactionEssenceBuilder {
28    /// Creates a new [`RegularTransactionEssenceBuilder`].
29    pub fn new(inputs_commitment: InputsCommitment) -> Self {
30        Self {
31            inputs: Vec::new(),
32            inputs_commitment,
33            outputs: Vec::new(),
34            payload: None,
35        }
36    }
37
38    /// Adds inputs to a [`RegularTransactionEssenceBuilder`].
39    pub fn with_inputs(mut self, inputs: Vec<Input>) -> Self {
40        self.inputs = inputs;
41        self
42    }
43
44    /// Add an input to a [`RegularTransactionEssenceBuilder`].
45    pub fn add_input(mut self, input: Input) -> Self {
46        self.inputs.push(input);
47        self
48    }
49
50    /// Add outputs to a [`RegularTransactionEssenceBuilder`].
51    pub fn with_outputs(mut self, outputs: Vec<Output>) -> Self {
52        self.outputs = outputs;
53        self
54    }
55
56    /// Add an output to a [`RegularTransactionEssenceBuilder`].
57    pub fn add_output(mut self, output: Output) -> Self {
58        self.outputs.push(output);
59        self
60    }
61
62    /// Add a payload to a [`RegularTransactionEssenceBuilder`].
63    pub fn with_payload(mut self, payload: Payload) -> Self {
64        self.payload = Some(payload);
65        self
66    }
67
68    /// Finishes a [`RegularTransactionEssenceBuilder`] into a [`RegularTransactionEssence`].
69    pub fn finish(self, protocol_parameters: &ProtocolParameters) -> Result<RegularTransactionEssence, Error> {
70        let inputs: BoxedSlicePrefix<Input, InputCount> = self
71            .inputs
72            .into_boxed_slice()
73            .try_into()
74            .map_err(Error::InvalidInputCount)?;
75
76        verify_inputs::<true>(&inputs)?;
77
78        let outputs: BoxedSlicePrefix<Output, OutputCount> = self
79            .outputs
80            .into_boxed_slice()
81            .try_into()
82            .map_err(Error::InvalidOutputCount)?;
83
84        verify_outputs::<true>(&outputs, protocol_parameters)?;
85
86        let payload = OptionalPayload::from(self.payload);
87
88        verify_payload::<true>(&payload)?;
89
90        Ok(RegularTransactionEssence {
91            network_id: protocol_parameters.network_id(),
92            inputs,
93            inputs_commitment: self.inputs_commitment,
94            outputs,
95            payload,
96        })
97    }
98}
99
100pub(crate) type InputCount = BoundedU16<{ *INPUT_COUNT_RANGE.start() }, { *INPUT_COUNT_RANGE.end() }>;
101pub(crate) type OutputCount = BoundedU16<{ *OUTPUT_COUNT_RANGE.start() }, { *OUTPUT_COUNT_RANGE.end() }>;
102
103/// A transaction regular essence consuming inputs, creating outputs and carrying an optional payload.
104#[derive(Clone, Debug, Eq, PartialEq, Packable)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
106#[packable(unpack_error = Error)]
107#[packable(unpack_visitor = ProtocolParameters)]
108pub struct RegularTransactionEssence {
109    /// The unique value denoting whether the block was meant for mainnet, testnet, or a private network.
110    #[packable(verify_with = verify_network_id)]
111    network_id: u64,
112    #[packable(verify_with = verify_inputs_packable)]
113    #[packable(unpack_error_with = |e| e.unwrap_item_err_or_else(|p| Error::InvalidInputCount(p.into())))]
114    inputs: BoxedSlicePrefix<Input, InputCount>,
115    /// BLAKE2b-256 hash of the serialized outputs referenced in inputs by their OutputId.
116    inputs_commitment: InputsCommitment,
117    #[packable(verify_with = verify_outputs)]
118    #[packable(unpack_error_with = |e| e.unwrap_item_err_or_else(|p| Error::InvalidOutputCount(p.into())))]
119    outputs: BoxedSlicePrefix<Output, OutputCount>,
120    #[packable(verify_with = verify_payload_packable)]
121    payload: OptionalPayload,
122}
123
124impl RegularTransactionEssence {
125    /// The essence kind of a [`RegularTransactionEssence`].
126    pub const KIND: u8 = 1;
127
128    /// Creates a new [`RegularTransactionEssenceBuilder`] to build a [`RegularTransactionEssence`].
129    pub fn builder(inputs_commitment: InputsCommitment) -> RegularTransactionEssenceBuilder {
130        RegularTransactionEssenceBuilder::new(inputs_commitment)
131    }
132
133    /// Returns the network ID of a [`RegularTransactionEssence`].
134    pub fn network_id(&self) -> u64 {
135        self.network_id
136    }
137
138    /// Returns the inputs of a [`RegularTransactionEssence`].
139    pub fn inputs(&self) -> &[Input] {
140        &self.inputs
141    }
142
143    /// Returns the inputs commitment of a [`RegularTransactionEssence`].
144    pub fn inputs_commitment(&self) -> &InputsCommitment {
145        &self.inputs_commitment
146    }
147
148    /// Returns the outputs of a [`RegularTransactionEssence`].
149    pub fn outputs(&self) -> &[Output] {
150        &self.outputs
151    }
152
153    /// Returns the optional payload of a [`RegularTransactionEssence`].
154    pub fn payload(&self) -> Option<&Payload> {
155        self.payload.as_ref()
156    }
157}
158
159fn verify_network_id<const VERIFY: bool>(network_id: &u64, visitor: &ProtocolParameters) -> Result<(), Error> {
160    if VERIFY {
161        let expected = visitor.network_id();
162
163        if *network_id != expected {
164            return Err(Error::NetworkIdMismatch {
165                expected,
166                actual: *network_id,
167            });
168        }
169    }
170
171    Ok(())
172}
173
174fn verify_inputs<const VERIFY: bool>(inputs: &[Input]) -> Result<(), Error> {
175    if VERIFY {
176        let mut seen_utxos = HashSet::new();
177
178        for input in inputs.iter() {
179            match input {
180                Input::Utxo(utxo) => {
181                    if !seen_utxos.insert(utxo) {
182                        return Err(Error::DuplicateUtxo(utxo.clone()));
183                    }
184                }
185                _ => return Err(Error::InvalidInputKind(input.kind())),
186            }
187        }
188    }
189
190    Ok(())
191}
192
193fn verify_inputs_packable<const VERIFY: bool>(inputs: &[Input], _visitor: &ProtocolParameters) -> Result<(), Error> {
194    verify_inputs::<VERIFY>(inputs)
195}
196
197fn verify_outputs<const VERIFY: bool>(outputs: &[Output], visitor: &ProtocolParameters) -> Result<(), Error> {
198    if VERIFY {
199        let mut amount_sum: u64 = 0;
200        let mut native_tokens_count: u8 = 0;
201
202        for output in outputs.iter() {
203            let (amount, native_tokens) = match output {
204                Output::Basic(output) => (output.amount(), output.native_tokens()),
205                Output::Alias(output) => (output.amount(), output.native_tokens()),
206                Output::Foundry(output) => (output.amount(), output.native_tokens()),
207                Output::Nft(output) => (output.amount(), output.native_tokens()),
208                _ => return Err(Error::InvalidOutputKind(output.kind())),
209            };
210
211            amount_sum = amount_sum
212                .checked_add(amount)
213                .ok_or(Error::InvalidTransactionAmountSum(amount_sum as u128 + amount as u128))?;
214
215            // Accumulated output balance must not exceed the total supply of tokens.
216            if amount_sum > visitor.token_supply() {
217                return Err(Error::InvalidTransactionAmountSum(amount_sum as u128));
218            }
219
220            native_tokens_count = native_tokens_count.checked_add(native_tokens.len() as u8).ok_or(
221                Error::InvalidTransactionNativeTokensCount(native_tokens_count as u16 + native_tokens.len() as u16),
222            )?;
223
224            if native_tokens_count > NativeTokens::COUNT_MAX {
225                return Err(Error::InvalidTransactionNativeTokensCount(native_tokens_count as u16));
226            }
227
228            output.verify_storage_deposit(visitor.rent_structure().clone(), visitor.token_supply())?;
229        }
230    }
231
232    Ok(())
233}
234
235fn verify_payload<const VERIFY: bool>(payload: &OptionalPayload) -> Result<(), Error> {
236    if VERIFY {
237        match &payload.0 {
238            Some(Payload::TaggedData(_)) | None => Ok(()),
239            Some(payload) => Err(Error::InvalidPayloadKind(payload.kind())),
240        }
241    } else {
242        Ok(())
243    }
244}
245
246fn verify_payload_packable<const VERIFY: bool>(
247    payload: &OptionalPayload,
248    _visitor: &ProtocolParameters,
249) -> Result<(), Error> {
250    verify_payload::<VERIFY>(payload)
251}
252
253#[cfg(feature = "dto")]
254#[allow(missing_docs)]
255pub mod dto {
256    use core::str::FromStr;
257
258    use serde::{Deserialize, Serialize};
259
260    use super::*;
261    use crate::{error::dto::DtoError, input::dto::InputDto, output::dto::OutputDto, payload::dto::PayloadDto};
262
263    /// Describes the essence data making up a transaction by defining its inputs and outputs and an optional payload.
264    #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
265    pub struct RegularTransactionEssenceDto {
266        #[serde(rename = "type")]
267        pub kind: u8,
268        #[serde(rename = "networkId")]
269        pub network_id: String,
270        pub inputs: Vec<InputDto>,
271        #[serde(rename = "inputsCommitment")]
272        pub inputs_commitment: String,
273        pub outputs: Vec<OutputDto>,
274        #[serde(skip_serializing_if = "Option::is_none")]
275        pub payload: Option<PayloadDto>,
276    }
277
278    impl From<&RegularTransactionEssence> for RegularTransactionEssenceDto {
279        fn from(value: &RegularTransactionEssence) -> Self {
280            RegularTransactionEssenceDto {
281                kind: RegularTransactionEssence::KIND,
282                network_id: value.network_id().to_string(),
283                inputs: value.inputs().iter().map(Into::into).collect::<Vec<_>>(),
284                inputs_commitment: value.inputs_commitment().to_string(),
285                outputs: value.outputs().iter().map(Into::into).collect::<Vec<_>>(),
286                payload: match value.payload() {
287                    Some(Payload::TaggedData(i)) => Some(PayloadDto::TaggedData(Box::new(i.as_ref().into()))),
288                    Some(_) => unimplemented!(),
289                    None => None,
290                },
291            }
292        }
293    }
294
295    impl RegularTransactionEssence {
296        pub fn try_from_dto(
297            value: &RegularTransactionEssenceDto,
298            protocol_parameters: &ProtocolParameters,
299        ) -> Result<RegularTransactionEssence, DtoError> {
300            let inputs = value
301                .inputs
302                .iter()
303                .map(TryInto::try_into)
304                .collect::<Result<Vec<Input>, DtoError>>()?;
305            let outputs = value
306                .outputs
307                .iter()
308                .map(|o| Output::try_from_dto(o, protocol_parameters.token_supply()))
309                .collect::<Result<Vec<Output>, DtoError>>()?;
310
311            let mut builder = RegularTransactionEssence::builder(InputsCommitment::from_str(&value.inputs_commitment)?)
312                .with_inputs(inputs)
313                .with_outputs(outputs);
314            builder = if let Some(p) = &value.payload {
315                if let PayloadDto::TaggedData(i) = p {
316                    builder.with_payload(Payload::TaggedData(Box::new((i.as_ref()).try_into()?)))
317                } else {
318                    return Err(DtoError::InvalidField("payload"));
319                }
320            } else {
321                builder
322            };
323
324            builder.finish(protocol_parameters).map_err(Into::into)
325        }
326    }
327}