Skip to main content

sov_universal_wallet/schema/
transaction_templates.rs

1use core::panic;
2use std::collections::{btree_map, BTreeMap, HashSet};
3
4use borsh::{BorshDeserialize, BorshSerialize};
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8use super::Link;
9
10#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12pub struct TemplateInput {
13    type_link: Link,
14    offset: usize,
15}
16
17impl TemplateInput {
18    pub fn type_link(&self) -> &Link {
19        &self.type_link
20    }
21
22    pub fn offset(&self) -> &usize {
23        &self.offset
24    }
25}
26
27/// Data structure holding a single template for a standard transaction type.
28/// Consists of pre-encoded default bytes, and a list of input fields that must be filled in to use
29/// the template (with name bindings).
30/// During usage, each template input is serialized using the schema accordings to its type, and
31/// inserted into the existing template bytes according to its offset index. The final result is a
32/// well-formed fully encoded transaction.
33/// It is important that input fields must be filled in from last to first (to ensure the `offset`
34/// values of earlier fields remain valid - as encoded fields are variable-length and cannot be
35/// accounted for).
36#[derive(Debug, Default, Clone, BorshSerialize, BorshDeserialize)]
37#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
38pub struct TransactionTemplate {
39    preencoded_bytes: Vec<u8>,
40    inputs: Vec<(String, TemplateInput)>,
41}
42
43impl TransactionTemplate {
44    /// Construct a new template from one pre-encoded field.
45    /// Used during template generation (normally in the UniversalWallet derive macro), at the
46    /// leaf levels of recursive template definition.
47    pub fn from_bytes(preencoded_bytes: Vec<u8>) -> Self {
48        Self {
49            preencoded_bytes,
50            inputs: vec![],
51        }
52    }
53
54    /// Construct a new template from one input field, with a placeholder link type.
55    /// While the offset gets fixed in `concat()`, the field_index has to be correct at
56    /// construction time.
57    /// Used during template generation (normally in the UniversalWallet derive macro), at the
58    /// leaf levels of recursive template definition.
59    pub fn from_input(name: String, field_index: usize) -> Self {
60        Self {
61            inputs: vec![(
62                name,
63                TemplateInput {
64                    type_link: Link::IndexedPlaceholder(field_index),
65                    offset: 0,
66                },
67            )],
68            preencoded_bytes: vec![],
69        }
70    }
71
72    pub fn preencoded_bytes(&self) -> &[u8] {
73        &self.preencoded_bytes
74    }
75
76    pub fn inputs(&self) -> &[(String, TemplateInput)] {
77        &self.inputs
78    }
79
80    /// Combining template definitions on multiple fields into one template on the parent type.
81    /// Concatenation in this context means: pre-encoded bytes are concatenated directly, and inputs
82    /// are added to the map in order, with offsets adjusted to accout for earlier pre-encoded
83    /// bytes.
84    /// Note that placeholder field indexes are NOT adjusted.
85    pub fn concat(templates: Vec<Self>) -> Self {
86        let mut ret = Self::default();
87        let mut prev_bytes_len = 0usize;
88        for t in templates {
89            ret.preencoded_bytes.extend(t.preencoded_bytes);
90            for (name, TemplateInput { type_link, offset }) in t.inputs {
91                if ret.inputs.iter().any(|input| input.0 == name) {
92                    panic!("Schema transaction template contained duplicate input binding name: {name}");
93                }
94                ret.inputs.push((
95                    name,
96                    TemplateInput {
97                        type_link,
98                        offset: offset + prev_bytes_len,
99                    },
100                ));
101            }
102            prev_bytes_len = ret.preencoded_bytes.len();
103        }
104        ret
105    }
106
107    /// Adjust a template definition for a wrapping enum. This means: the discriminant is
108    /// prepended to the pre-encoded bytes, and all input offsets are adjusted.
109    pub fn prepend_discriminant(&mut self, discriminant: u8) {
110        self.preencoded_bytes.insert(0, discriminant);
111        for (_, TemplateInput { offset, .. }) in self.inputs.iter_mut() {
112            *offset += 1;
113        }
114    }
115}
116
117/// Temporary data structure used to track the origins of each field's template parameters before
118/// constructing the parent type's template.
119/// This is necessary because subtype-originating templates do not force the parent type to
120/// implement this template: the subtype might have been annotated for use elsewhere. A type only
121/// inherits a template from subtypes if _every_ subtype implements the template - otherwise, the
122/// template is dropped. Only explicitly annotated templates are enforced as mandatory on every
123/// field.
124#[derive(Debug, Default, Clone)]
125#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
126pub struct AttributeAndChildTemplateSet {
127    pub attribute_templates: TransactionTemplateSet,
128    pub type_templates: TransactionTemplateSet,
129}
130
131/// Data structure denoting a set of templates on a type, indexed by (string) name.
132#[derive(Debug, Default, Clone, BorshSerialize, BorshDeserialize)]
133#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
134pub struct TransactionTemplateSet(pub BTreeMap<String, TransactionTemplate>);
135
136impl TransactionTemplateSet {
137    /// Replace placeholder links in templates with actual typelinks from the schema (provided by
138    /// the type's child_links).
139    /// Template placeholders must be `IndexedPlaceholder`s because a template does not necessarily
140    /// include placeholders for every field - some fields may be provided with pre-encoded default
141    /// values and thus need to be skipped. The index allows looking up the correct type in the
142    /// child_links vector.
143    pub fn fill_links(
144        field_templates: Vec<(String, TransactionTemplate)>,
145        child_links: Vec<Link>,
146    ) -> Self {
147        let mut attribute_templates = BTreeMap::<String, TransactionTemplate>::default();
148        for (name, mut template) in field_templates.into_iter() {
149            // 1. Fill in any type links
150            for (_, input) in template.inputs.iter_mut() {
151                match input.type_link {
152                    Link::IndexedPlaceholder(i) => input.type_link = child_links[i].clone(),
153                    Link::Placeholder => panic!("Templates should not have unindexed Placeholder links. This is a bug in a hand-written implementation."),
154                    _ => ()
155                }
156            }
157            // 2. Insert into map, sanity checking for no duplicates
158            if let btree_map::Entry::Vacant(e) = attribute_templates.entry(name.clone()) {
159                e.insert(template);
160            } else {
161                panic!("Duplicate template definitions (name: \"{name}\" in attributes. This should never happen (macro should have already errored out).")
162            }
163        }
164
165        Self(attribute_templates)
166    }
167
168    /// Input: a vector of template sets, normally one for each field of a type.
169    /// Outputs: a merged template set. This means:
170    ///  - Every individual template is concatenated from each field portion of it, across the set
171    ///    of templates.
172    ///  - The entire set is enforced to be present on every field, so no fields are skipped.
173    ///
174    /// The exception to the 2nd rule above is for templates only present in the set through field
175    /// types (i.e. subtype templates). See the documentation of AttributeAndChildTemplateSet for a
176    /// full explanation.
177    pub fn concatenate_template_sets(
178        template_sets: Vec<AttributeAndChildTemplateSet>,
179        type_name_for_diagnostics: &'static str,
180    ) -> Self {
181        // Eventual return value
182        let mut final_template_set = Self::default();
183
184        // The union of elements of each set (as indexed by name) originating directly from
185        // attribute annotations on this type's fields
186        let attribute_template_names_set: HashSet<_> = template_sets
187            .iter()
188            .flat_map(|t| t.attribute_templates.0.keys().cloned())
189            .collect();
190        // The union of elements of each set originating from subtypes...
191        let type_template_names_set: HashSet<_> = template_sets
192            .iter()
193            .flat_map(|t| t.type_templates.0.keys().cloned())
194            .collect();
195        // And the difference of the two, giving the subtype-only templates
196        let type_only_names_set: HashSet<_> = type_template_names_set
197            .difference(&attribute_template_names_set)
198            .cloned()
199            .collect();
200
201        // With the template names tracked, the two components of AttributeAndChildTemplateSet are
202        // merged into a single Vec<TransactionTemplateSet> which is then concatenated/merged
203        // according to the rules set out above
204        let mut zipped_templates: Vec<_> = template_sets.into_iter().map(|mut separated| {
205            for (name, template) in separated.type_templates.0.into_iter() {
206                if let btree_map::Entry::Vacant(e) = separated.attribute_templates.0.entry(name.clone()) {
207                    e.insert(template);
208                } else {
209                    panic!("Field type's template definitions for \"{}\" overlap with field's own template annotations.", name);
210                }
211            }
212            separated.attribute_templates
213        }).collect();
214
215        // Helper function doing the actual merging - as the only difference between the two
216        // origins of templates is whether we abort with an error or not on a missing template
217        fn collect_concated_templates<F: Fn(&String)>(
218            names: HashSet<String>,
219            all_templates: &mut [TransactionTemplateSet],
220            panic_if_needed: F,
221        ) -> TransactionTemplateSet {
222            let mut ret = TransactionTemplateSet::default();
223            // For every potential element in the set...
224            'template: for name in names {
225                // build a vector of individual templates...
226                let mut template_vec = Vec::<TransactionTemplate>::new();
227                for template_set in all_templates.iter_mut() {
228                    match template_set.0.remove(&name) {
229                        None => {
230                            // handle the case where the template is not present on a field
231                            panic_if_needed(&name);
232                            break 'template;
233                        }
234                        Some(template) => template_vec.push(template),
235                    }
236                }
237                // finally, with the vector consisting of the sub-components of that template
238                // across every single field on our type, concatenate them all into one template -
239                // concatenating pre-encoded values, adjusting input offsets etc. - and add it to
240                // our return set
241                ret.0
242                    .insert(name, TransactionTemplate::concat(template_vec));
243            }
244            ret
245        }
246
247        // For templates explicitly annotated on a field of this type, abort immediately if not
248        // every field provides this template.
249        final_template_set.0.extend(collect_concated_templates(attribute_template_names_set, &mut zipped_templates, |name| {
250            panic!("Partial template transaction definition! Metadata for transaction \"{}\" is found on some, but not all, fields of data type {}.\nDouble-check the transaction template attributes and child type definitions. Any template defined on an attribute on any one field must be present for every field, either directly from annotations or from a field's type", name, type_name_for_diagnostics);
251        }).0);
252
253        // For templates only inherited from subtypes, ignore any that aren't implemented for every
254        // field.
255        // Note that calling `final_template_set.extend()` is safe because, earlier, we ensured
256        // type_only_names_set has zero intersection with attribute_template_names_set, thus there
257        // is no danger of duplicate or overwritten templates: the two `collect_concated_templates`
258        // are guaranteed to be disjoint on the space of template names.
259        final_template_set.0.extend(
260            collect_concated_templates(type_only_names_set, &mut zipped_templates, |_| {}).0,
261        );
262
263        final_template_set
264    }
265
266    /// Explicitly filter which templates will be part of a given enum variant.
267    /// Only templates whose name is in the list will be set on the variant. Additionally, any
268    /// names in the list NOT present in the templates will cause an error.
269    ///
270    /// Optionally, an override can be set to inherit all templates from the variant (primarily
271    /// intended for use on the `RuntimeCall`, to inherit all templates from every module's
272    /// `CallMessage`). In this case, the filter list is only used for error checking, to
273    /// explicitly enforce that all templates named in it MUST be present.
274    pub fn filter_enum_variant_templates(
275        mut self,
276        filter: Vec<String>,
277        inherit_all: bool,
278        variant_name_for_diagnostics: &'static str,
279    ) -> Self {
280        let mut filter_set: HashSet<String> = HashSet::from_iter(filter);
281        self.0
282            .retain(|name, _| filter_set.remove(name) || inherit_all);
283        if !filter_set.is_empty() {
284            // Unwrap: we know the set isn't empty, so it must have at least one entry
285            panic!("Enum variant {variant_name_for_diagnostics} specified template \"{}\" which was not defined on the variant's fields", filter_set.iter().next().unwrap());
286        }
287        self
288    }
289
290    pub fn merge_enum_template_sets(
291        template_sets: Vec<TransactionTemplateSet>,
292        type_name_for_diagnostics: &'static str,
293    ) -> Self {
294        // Eventual return value
295        let mut final_template_set = Self::default();
296
297        // For every variant (one entry in the input vec), ensure none of the templates were
298        // already seen in previous variants, then adjust the template for the discriminant and add
299        // it to the output
300        for (discriminant, set) in template_sets.into_iter().enumerate() {
301            for (name, mut template) in set.0.into_iter() {
302                if final_template_set.0.contains_key(&name) {
303                    panic!("Different variants of the enum {} both contained definitions for the transaction template \"{}\". At this time, only one enum branch can be defined for any transaction template.", type_name_for_diagnostics, name);
304                }
305                template.prepend_discriminant(
306                    discriminant
307                        .try_into()
308                        .expect("Enum discriminants are only supported up to size u8"),
309                );
310                final_template_set.0.insert(name, template);
311            }
312        }
313
314        final_template_set
315    }
316}