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