ab_system_contract_simple_wallet_base/payload/
builder.rs

1//! Transaction payload creation utilities
2
3#[cfg(test)]
4mod tests;
5
6extern crate alloc;
7
8use crate::payload::{TransactionInput, TransactionMethodContext, TransactionSlot};
9use ab_contracts_common::MAX_TOTAL_METHOD_ARGS;
10use ab_contracts_common::metadata::decode::{
11    ArgumentKind, MetadataDecodingError, MethodMetadataDecoder, MethodMetadataItem,
12    MethodsContainerKind,
13};
14use ab_contracts_common::method::{ExternalArgs, MethodFingerprint};
15use ab_core_primitives::address::Address;
16use ab_io_type::MAX_ALIGNMENT;
17use ab_io_type::metadata::IoTypeDetails;
18use ab_io_type::trivial_type::TrivialType;
19use alloc::vec::Vec;
20use core::ffi::c_void;
21use core::mem::MaybeUninit;
22use core::num::NonZeroU8;
23use core::ptr::NonNull;
24use core::{ptr, slice};
25
26const _: () = {
27    // Make sure bit flags for all arguments will fit into a single u8 below
28    assert!(MAX_TOTAL_METHOD_ARGS as u32 == u8::BITS);
29};
30
31/// Errors for [`TransactionPayloadBuilder`]
32#[derive(Debug, thiserror::Error)]
33pub enum TransactionPayloadBuilderError<'a> {
34    /// Metadata decoding error
35    #[error("Metadata decoding error: {0}")]
36    MetadataDecodingError(MetadataDecodingError<'a>),
37    /// Too many arguments
38    #[error("Too many arguments")]
39    TooManyArguments(u8),
40    /// Invalid alignment
41    #[error("Invalid alignment: {0}")]
42    InvalidAlignment(NonZeroU8),
43    /// Invalid output index
44    #[error("Invalid output index: {0}")]
45    InvalidOutputIndex(u8),
46}
47
48/// Builder for payload to be used with [`TxHandler`] (primarily for [`SimpleWallet`]).
49///
50/// Decoding can be done with [`TransactionPayloadDecoder`]
51///
52/// [`TxHandler`]: ab_contracts_standards::tx_handler::TxHandler
53/// [`SimpleWallet`]: crate::SimpleWalletBase
54/// [`TransactionPayloadDecoder`]: crate::payload::TransactionPayloadDecoder
55#[derive(Debug, Clone)]
56pub struct TransactionPayloadBuilder {
57    payload: Vec<u8>,
58}
59
60impl Default for TransactionPayloadBuilder {
61    #[inline]
62    fn default() -> Self {
63        Self {
64            payload: Vec::with_capacity(1024),
65        }
66    }
67}
68
69impl TransactionPayloadBuilder {
70    /// Add method call to the payload.
71    ///
72    /// The wallet will call this method in addition order.
73    ///
74    /// `slot_output_index` and `input_output_index` are used for referencing earlier outputs as
75    /// slots or inputs of this method, its values are optional, see [`TransactionInput`] for more
76    /// details.
77    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
78    pub fn with_method_call<Args>(
79        &mut self,
80        contract: &Address,
81        external_args: &Args,
82        method_context: TransactionMethodContext,
83        slot_output_index: &[Option<u8>],
84        input_output_index: &[Option<u8>],
85    ) -> Result<(), TransactionPayloadBuilderError<'static>>
86    where
87        Args: ExternalArgs,
88    {
89        let external_args = NonNull::from_ref(external_args).cast::<*const c_void>();
90
91        // SAFETY: Called with statically valid data
92        unsafe {
93            self.with_method_call_untyped(
94                contract,
95                &external_args,
96                Args::METADATA,
97                &Args::FINGERPRINT,
98                method_context,
99                slot_output_index,
100                input_output_index,
101            )
102        }
103    }
104
105    /// Other than unsafe API, this method is identical to [`Self::with_method_call()`].
106    ///
107    /// # Safety
108    /// `external_args` must correspond to `method_metadata` and `method_fingerprint`. Outputs are
109    /// never read from `external_args` and inputs that have corresponding `input_output_index`
110    /// are not read either.
111    #[expect(
112        clippy::too_many_arguments,
113        reason = "Only exceeds the limit due to being untyped, while above typed version is not"
114    )]
115    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
116    pub unsafe fn with_method_call_untyped<'a>(
117        &mut self,
118        contract: &Address,
119        external_args: &NonNull<*const c_void>,
120        mut method_metadata: &'a [u8],
121        method_fingerprint: &MethodFingerprint,
122        method_context: TransactionMethodContext,
123        slot_output_index: &[Option<u8>],
124        input_output_index: &[Option<u8>],
125    ) -> Result<(), TransactionPayloadBuilderError<'a>> {
126        let mut external_args = *external_args;
127
128        let (mut metadata_decoder, method_metadata_item) =
129            MethodMetadataDecoder::new(&mut method_metadata, MethodsContainerKind::Unknown)
130                .decode_next()
131                .map_err(TransactionPayloadBuilderError::MetadataDecodingError)?;
132
133        let MethodMetadataItem {
134            method_kind,
135            num_arguments,
136            ..
137        } = method_metadata_item;
138        let number_of_arguments =
139            num_arguments.saturating_add(if method_kind.has_self() { 1 } else { 0 });
140
141        if number_of_arguments > MAX_TOTAL_METHOD_ARGS {
142            return Err(TransactionPayloadBuilderError::TooManyArguments(
143                number_of_arguments,
144            ));
145        }
146
147        self.extend_payload_with_alignment(contract.as_bytes(), align_of_val(contract));
148        self.extend_payload_with_alignment(
149            method_fingerprint.as_bytes(),
150            align_of_val(method_fingerprint),
151        );
152        self.push_payload_byte(method_context as u8);
153
154        let mut num_slot_arguments = 0u8;
155        let mut num_input_arguments = 0u8;
156        let mut num_output_arguments = 0u8;
157
158        let mut input_output_type_details =
159            [MaybeUninit::<IoTypeDetails>::uninit(); MAX_TOTAL_METHOD_ARGS as usize];
160        // Collect information about all arguments so everything below is able to be purely additive
161        while let Some(item) = metadata_decoder
162            .decode_next()
163            .transpose()
164            .map_err(TransactionPayloadBuilderError::MetadataDecodingError)?
165        {
166            match item.argument_kind {
167                ArgumentKind::EnvRo
168                | ArgumentKind::EnvRw
169                | ArgumentKind::TmpRo
170                | ArgumentKind::TmpRw => {
171                    // Not represented in external args
172                }
173                ArgumentKind::SlotRo | ArgumentKind::SlotRw => {
174                    num_slot_arguments += 1;
175                }
176                ArgumentKind::Input => {
177                    input_output_type_details[usize::from(num_input_arguments)]
178                        .write(item.type_details.unwrap_or(IoTypeDetails::bytes(0)));
179                    num_input_arguments += 1;
180                }
181                ArgumentKind::Output => {
182                    input_output_type_details
183                        [usize::from(num_input_arguments + num_output_arguments)]
184                    .write(item.type_details.unwrap_or(IoTypeDetails::bytes(0)));
185                    num_output_arguments += 1;
186                }
187            }
188        }
189        // SAFETY: Just initialized elements above
190        let (input_type_details, output_type_details) = unsafe {
191            let (input_type_details, output_type_details) =
192                input_output_type_details.split_at_unchecked(usize::from(num_input_arguments));
193            let (output_type_details, _) =
194                output_type_details.split_at_unchecked(usize::from(num_output_arguments));
195
196            (
197                input_type_details.assume_init_ref(),
198                output_type_details.assume_init_ref(),
199            )
200        };
201
202        // Store number of slots and `TransactionSlot` for each slot
203        self.push_payload_byte(num_slot_arguments);
204        for slot_offset in 0..usize::from(num_slot_arguments) {
205            let slot_type = if let Some(&Some(output_index)) = slot_output_index.get(slot_offset) {
206                TransactionSlot::new_output_index(output_index).ok_or(
207                    TransactionPayloadBuilderError::InvalidOutputIndex(output_index),
208                )?
209            } else {
210                TransactionSlot::new_address()
211            };
212            self.push_payload_byte(slot_type.into_u8());
213        }
214
215        // Store number of inputs and `TransactionInput` for each input
216        self.push_payload_byte(num_input_arguments);
217        for (input_offset, type_details) in input_type_details.iter().enumerate() {
218            let input_type = if let Some(&Some(output_index)) = input_output_index.get(input_offset)
219            {
220                TransactionInput::new_output_index(output_index).ok_or(
221                    TransactionPayloadBuilderError::InvalidOutputIndex(output_index),
222                )?
223            } else {
224                TransactionInput::new_value(type_details.alignment).ok_or(
225                    TransactionPayloadBuilderError::InvalidAlignment(type_details.alignment),
226                )?
227            };
228            self.push_payload_byte(input_type.into_u8());
229        }
230
231        // Store number of outputs
232        self.push_payload_byte(num_output_arguments);
233
234        for slot_offset in 0..usize::from(num_slot_arguments) {
235            // SAFETY: Method description requires the layout to correspond to metadata
236            let address = unsafe {
237                let address = external_args.cast::<NonNull<Address>>().read().as_ref();
238                external_args = external_args.offset(1);
239                address
240            };
241
242            if slot_output_index
243                .get(slot_offset)
244                .copied()
245                .flatten()
246                .is_none()
247            {
248                self.extend_payload_with_alignment(address.as_bytes(), align_of_val(address));
249            }
250        }
251
252        for (input_offset, type_details) in input_type_details.iter().enumerate() {
253            // SAFETY: Method description requires the layout to correspond to metadata
254            let (size, data) = unsafe {
255                let data = external_args.cast::<NonNull<u8>>().read();
256                external_args = external_args.offset(1);
257                let size = external_args.cast::<NonNull<u32>>().read().read();
258                external_args = external_args.offset(1);
259
260                let data = slice::from_raw_parts(data.as_ptr().cast_const(), size as usize);
261
262                (size, data)
263            };
264
265            if input_output_index
266                .get(input_offset)
267                .copied()
268                .flatten()
269                .is_none()
270            {
271                self.extend_payload_with_alignment(&size.to_le_bytes(), align_of_val(&size));
272                self.extend_payload_with_alignment(data, type_details.alignment.get() as usize);
273            }
274        }
275
276        for type_details in output_type_details {
277            self.extend_payload_with_alignment(
278                &type_details.recommended_capacity.to_le_bytes(),
279                align_of_val(&type_details.recommended_capacity),
280            );
281            self.extend_payload_with_alignment(
282                &[type_details.alignment.ilog2() as u8],
283                align_of::<u8>(),
284            );
285        }
286
287        Ok(())
288    }
289
290    /// Returns 16-byte aligned bytes.
291    ///
292    /// The contents is a concatenated sequence of method calls with their arguments. All data
293    /// structures are correctly aligned in the returned byte buffer with `0` used as padding when
294    /// necessary.
295    ///
296    /// Each method is serialized in the following way:
297    /// * Contract to call: [`Address`]
298    /// * Fingerprint of the method to call: [`MethodFingerprint`]
299    /// * Method context: [`TransactionMethodContext`]
300    /// * Number of `#[slot]` arguments: `u8`
301    /// * For each `#[slot]` argument:
302    ///     * [`TransactionSlot`]  as `u8`
303    /// * Number of `#[input]` arguments: `u8`
304    /// * For each `#[input]` argument:
305    ///     * [`TransactionInput`] for each `#[input]` argument as `u8`
306    /// * Number of `#[output]` arguments: `u8`
307    /// * For each [`TransactionSlot`], whose type is [`TransactionSlotType::Address`]:
308    ///     * [`Address`]
309    /// * For each [`TransactionInput`], whose type is [`TransactionInputType::Value`]:
310    ///     * Input size as little-endian `u32` followed by the input itself
311    /// * For each `#[output]`:
312    ///     * recommended capacity as little-endian `u32` followed by alignment power as `u8`
313    ///       (`NonZeroU8::ilog2(alignment)`)
314    ///
315    /// [`TransactionSlotType::Address`]: crate::payload::TransactionSlotType::Address
316    /// [`TransactionInputType::Value`]: crate::payload::TransactionInputType::Value
317    // TODO: Figure out how sand make it possible to apply `no-panic` here
318    pub fn into_aligned_bytes(mut self) -> Vec<u128> {
319        // Fill bytes to make it multiple of `u128` before creating `u128`-based vector
320        self.ensure_alignment(usize::from(MAX_ALIGNMENT));
321
322        let output_len = self.payload.len() / size_of::<u128>();
323        let mut output = Vec::<u128>::with_capacity(output_len);
324
325        // SAFETY: Pointers are valid for reads/writes, aligned and not overlapping
326        unsafe {
327            ptr::copy_nonoverlapping(
328                self.payload.as_ptr(),
329                output.as_mut_ptr().cast::<u8>(),
330                self.payload.len(),
331            );
332            output.set_len(output_len);
333        }
334
335        debug_assert_eq!(align_of_val(output.as_slice()), usize::from(MAX_ALIGNMENT));
336
337        output
338    }
339
340    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
341    fn extend_payload_with_alignment(&mut self, bytes: &[u8], alignment: usize) {
342        self.ensure_alignment(alignment);
343
344        self.payload.extend_from_slice(bytes);
345    }
346
347    // TODO: Figure out how sand make it possible to apply `no-panic` here
348    fn ensure_alignment(&mut self, alignment: usize) {
349        debug_assert!(alignment <= usize::from(MAX_ALIGNMENT));
350
351        // Optimized version of the following that expects `alignment` to be a power of 2:
352        // let unaligned_by = self.payload.len() % alignment;
353        let unaligned_by = self.payload.len() & (alignment - 1);
354        if unaligned_by > 0 {
355            // SAFETY: Subtracted value is always smaller than alignment
356            let padding_bytes = unsafe { alignment.unchecked_sub(unaligned_by) };
357            self.payload.resize(self.payload.len() + padding_bytes, 0);
358        }
359    }
360
361    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
362    fn push_payload_byte(&mut self, byte: u8) {
363        self.payload.push(byte);
364    }
365}