Skip to main content

sov_universal_wallet/schema/
mod.rs

1use std::any::TypeId;
2use std::collections::HashMap;
3use std::fmt::Debug;
4
5use once_cell::sync::OnceCell;
6
7use sha2::{Digest, Sha256};
8pub mod container;
9mod primitive;
10pub mod safe_string;
11pub mod transaction_templates;
12use borsh::{BorshDeserialize, BorshSerialize};
13pub use container::Container;
14use nmt_rs::simple_merkle::db::MemDb;
15use nmt_rs::simple_merkle::tree::MerkleTree;
16use nmt_rs::TmSha2Hasher;
17pub use primitive::Primitive;
18#[cfg(feature = "serde")]
19use serde::{Deserialize, Serialize};
20mod schema_impls;
21
22use thiserror::Error;
23use transaction_templates::TransactionTemplateSet;
24
25use crate::display::{Context as DisplayContext, DisplayVisitor, FormatError};
26#[cfg(feature = "serde")]
27use crate::json_to_borsh::{Context as EncodeContext, EncodeError, EncodeVisitor};
28use crate::ty::byte_display::ByteParseError;
29use crate::ty::{ContainerSerdeMetadata, LinkingScheme, Ty};
30#[cfg(feature = "eip712")]
31use crate::visitors::eip712::{Context as Eip712Context, Eip712Error, Eip712Visitor};
32#[cfg(feature = "eip712")]
33use alloy_dyn_abi::{Eip712Types, Error as AlloyEip712Error, PropertyDef, TypedData};
34
35/// Returns the 64-bit fragment used to identify a full chain hash in transaction
36/// details.
37///
38/// The fragment is the first eight bytes of the chain hash. Interpreting it as
39/// little-endian means Borsh serializes the `u64` back to those same eight bytes.
40#[must_use]
41pub const fn chain_hash_fragment(chain_hash: &[u8; 32]) -> u64 {
42    u64::from_le_bytes([
43        chain_hash[0],
44        chain_hash[1],
45        chain_hash[2],
46        chain_hash[3],
47        chain_hash[4],
48        chain_hash[5],
49        chain_hash[6],
50        chain_hash[7],
51    ])
52}
53
54#[derive(Debug, Error)]
55pub enum SchemaError {
56    #[error(transparent)]
57    FormatError(#[from] FormatError),
58    #[error(transparent)]
59    BorshError(#[from] borsh::io::Error),
60    #[cfg(feature = "serde")]
61    #[error(transparent)]
62    EncodeError(#[from] EncodeError),
63    #[cfg(feature = "serde")]
64    #[error(transparent)]
65    JsonError(#[from] serde_json::Error),
66    #[cfg(feature = "eip712")]
67    #[error(transparent)]
68    Eip712Error(#[from] Eip712Error),
69    #[cfg(feature = "eip712")]
70    #[error(transparent)]
71    AlloyEip712Error(#[from] AlloyEip712Error),
72    #[error(transparent)]
73    Bech32Error(#[from] ByteParseError),
74    #[error("Rollup type {0:?} was missing from schema")]
75    MissingRollupRoot(RollupRoots),
76    #[error("Template {0} not found in schema")]
77    UnknownTemplate(String),
78    #[error("Index {0} not found in schema")]
79    InvalidIndex(usize),
80    #[error("Metadata hash must be provided but was not initialized. The schema was not properly finalized, or the serialized schema was invalid.")]
81    MetadataHashNotInitialized,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
85#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
86pub struct IndexLinking;
87
88impl LinkingScheme for IndexLinking {
89    type TypeLink = Link;
90}
91
92// TODO: Some type safety for fully-constructed schemas.
93// It should be possible to use the type system to ensure at compile-time that
94// a) constructed Schemas do not have any Link::Placeholder; and
95// b) it is not possible to call construction methods (the ones that edit the links) on a finished
96// Schema.
97// This could be done with, for example, an intermediate SchemaUnderConstruction type using a
98// ConstrutionIndexLinking, which implements into::<Schema>().
99//
100// Right now this is mostly achieved using member visibility (nobody outside can call the private
101// construction methods) and sanity checking (on a derived schema, if under_construction is empty,
102// there won't be any placeholders); but a separate type would provide a stronger guarantee.
103#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
104#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
105pub enum Link {
106    ByIndex(usize),
107    Immediate(Primitive),
108    Placeholder,
109    /// Placeholder indexed by its place in the parent datastructure
110    IndexedPlaceholder(usize),
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114enum MaybePartialLink {
115    Partial(Link),
116    Complete(Link),
117}
118
119impl MaybePartialLink {
120    fn into_inner(self) -> Link {
121        match self {
122            MaybePartialLink::Partial(link) => link,
123            MaybePartialLink::Complete(link) => link,
124        }
125    }
126}
127
128/// This newtype is mainly necessary to allow the schema to derive Debug ergonomically
129/// Stores both the tree and its root since MerkleTree::root() requires &mut self
130#[derive(Default)]
131#[allow(clippy::type_complexity)] // This is only used internally
132struct ConstructedMerkleTree(OnceCell<(MerkleTree<MemDb<[u8; 32]>, TmSha2Hasher>, [u8; 32])>);
133
134impl Debug for ConstructedMerkleTree {
135    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        Ok(())
137    }
138}
139
140/// This extra metadata is used in contexts where serde features are enabled; thus, we do not
141/// serialize it in serde formats, as it should be recomputed before using the data committed
142/// using it. (Otherwise a frontend could supply malicious metadata and a mismatching hash to
143/// make the hash pass chain ID checks.)
144/// When serde is disabled (i.e. usecases using borsh serialization), this metadata is unused, so
145/// the committment is the only relevant information and a mismatch is not possible.
146/// TL;DR:
147/// - In borsh: serializes/deserializes the actual hash value, while corresponding metada is empty
148/// - In serde: skips serialization and recalculates on first use, ensuring hash matches the
149///   deserialized metadata
150#[derive(Default)]
151struct MetadataHash(OnceCell<[u8; 32]>);
152
153impl Debug for MetadataHash {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        self.0.get().fmt(f)
156    }
157}
158
159impl BorshSerialize for MetadataHash {
160    fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
161        // The hash must be calculated before serialization (via finalize())
162        // It's an error to serialize a schema that hasn't been finalized
163        let hash = self.0.get().copied()
164            .ok_or_else(|| std::io::Error::new(
165                std::io::ErrorKind::InvalidData,
166                "Cannot serialize Schema: metadata_hash not initialized. Call finalize() before serializing"
167            ))?;
168        BorshSerialize::serialize(&hash, writer)
169    }
170}
171
172impl BorshDeserialize for MetadataHash {
173    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
174        let hash: [u8; 32] = BorshDeserialize::deserialize_reader(reader)?;
175        let metadata_hash = MetadataHash::default();
176        // Set the hash - this should always succeed for a new MetadataHash
177        metadata_hash.0.set(hash).map_err(|_| {
178            std::io::Error::new(
179                std::io::ErrorKind::InvalidData,
180                "Failed to set metadata_hash in OnceCell during deserialization",
181            )
182        })?;
183        Ok(metadata_hash)
184    }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Hash)]
188pub struct ItemId(pub TypeId);
189
190impl ItemId {
191    pub fn of<T: 'static + UniversalWallet>() -> Self {
192        T::id_override().unwrap_or(ItemId(TypeId::of::<T>()))
193    }
194}
195
196/// Not enforced in the types, but the expected convention that should be followed when generating
197/// the schema.
198#[derive(Debug, Copy, Clone)]
199pub enum RollupRoots {
200    Transaction = 0,
201    TransactionSigningPayload = 1,
202    RuntimeCall = 2,
203    Address = 3,
204}
205
206/// The standard metadata format for every chain. Includes a numeric chain_id and a human-readable
207/// chain name.
208#[derive(Debug, Default, Clone, BorshSerialize, BorshDeserialize)]
209#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
210pub struct ChainData {
211    pub chain_id: u64,
212    pub chain_name: String,
213}
214
215/// A schema, representing set of types (i.e. rust code) as a data structure.
216/// The schema allows any included type's borsh serialization to be displayed as a human readable string,
217/// and the type's JSON serialisation to be re-serialised to borsh without depending on the
218/// original Rust type.
219/// It is also serialisable and therefore, once generated for a rollup, can be imported and used with
220/// non-Rust languages, enabling toolkits in any language to implement the same functionality as above.
221///
222/// A schema can be instantiated for any type that implements either `UniversalWallet` or
223/// `OverrideSchema`. In turn, `UniversalWallet` is intended to be automatically derived using the
224/// `UniversalWallet` macro.
225#[derive(Default, Debug, BorshSerialize, BorshDeserialize)]
226#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
227pub struct Schema {
228    /// The types described by this schema. This is an array of type descriptions, where complex
229    /// types refer to their sub-types by index within the array.
230    /// Any of the types here can be used for schema operations (such as borsh-to-human or
231    /// json-to-borsh reserialisations).
232    types: Vec<Ty<IndexLinking>>,
233
234    /// A mapping from the complex root types that parametrized the schema generation invocation (in
235    /// order, skipping primitives) to the actual indices they ended up at in the type array above.
236    root_type_indices: Vec<usize>,
237
238    /// Global metadata for the chain.
239    chain_data: ChainData,
240
241    /// Extra metadata hash. "Extra" metadata is defined as metadata irrelevant when no additional
242    /// features are enabled.
243    #[cfg_attr(feature = "serde", serde(skip))]
244    extra_metadata_hash: MetadataHash,
245
246    /// The chain hash: the top-level hash committing to the entire schema, including all types and
247    /// all metadata.
248    /// This should be recalculated independently whenever the schema is used, thus is not included
249    /// in serializations. This field caches the results of the calculation for subsequent uses
250    /// after the schema has been deserialized and constructed.
251    #[cfg_attr(feature = "serde", serde(skip))]
252    #[borsh(skip)]
253    chain_hash: OnceCell<[u8; 32]>,
254
255    /// A list of templatable objects that can be constructed from standard input, per root type (in
256    /// order corresponding to root_type_indices). Mapped by template name.
257    /// Should be skipped in binary serialisation for hardware wallet apps.
258    #[borsh(skip)]
259    templates: Vec<TransactionTemplateSet>,
260
261    /// A set of metadata items for each field in the `types` vec, used for serde-compatible
262    /// deserialisation (i.e. `json_to_borsh()`).
263    /// It is separated from the main vec of `Ty` structs to allow non-serde implementations of the
264    /// schema, meaning ones only concerned with `borsh`-serialized interpretation (i.e.
265    /// `display()` functionality), such as hardware wallets, to avoid deserializing this. Not only
266    /// does this save resources but it also allows the format to be modified to implement
267    /// additional serde compatibility features without causing breaking changes for non-serde
268    /// implementations.
269    #[borsh(skip)]
270    serde_metadata: Vec<ContainerSerdeMetadata>,
271
272    /// Cached (lazily-constructed) merkelization of the entire schema.
273    #[cfg_attr(feature = "serde", serde(skip))]
274    #[borsh(skip)]
275    merkle_tree: ConstructedMerkleTree,
276
277    /// A map from the type ID of an item to its index in the types array. Note that primitives and "virtual" structs/tuples
278    /// (i.e. the contents of an enum variant) are not included in this map.
279    /// Only used during schema construction.
280    #[cfg_attr(feature = "serde", serde(skip))]
281    #[borsh(skip)]
282    known_types: HashMap<ItemId, usize>,
283
284    /// Keeps track of all the types which are partially constructed. By the end of schema generation, this
285    /// must be empty.
286    #[cfg_attr(feature = "serde", serde(skip))]
287    #[borsh(skip)]
288    under_construction: HashMap<ItemId, usize>,
289}
290
291impl Schema {
292    /// Instantiate a schema for a single type.
293    /// This root type will be at index 0
294    pub fn of_single_type<T: UniversalWallet>() -> Result<Self, SchemaError> {
295        // TODO: this could easily be implemented with a macro for N types for any N >= 1, if ever needed
296        let mut schema = Self::default();
297        T::make_root_of(&mut schema);
298        schema.finalize()?;
299        Ok(schema)
300    }
301
302    /// Instantiate a schema for a standard set of rollup types: its complete transaction, its
303    /// transaction signing payload, and its call message type.
304    /// The types will be accessible using the indices stored in root_type_indices (in the above
305    /// order); they can also be queried using the `RollupRoots` enum through the `_rollup`-tagged
306    /// functions on the schema
307    pub fn of_rollup_types_with_chain_data<
308        Transaction: UniversalWallet,
309        TransactionSigningPayload: UniversalWallet,
310        RuntimeCall: UniversalWallet,
311        Address: UniversalWallet,
312    >(
313        chain_data: ChainData,
314    ) -> Result<Self, SchemaError> {
315        let mut schema = Schema {
316            chain_data,
317            ..Self::default()
318        };
319        Transaction::make_root_of(&mut schema);
320        TransactionSigningPayload::make_root_of(&mut schema);
321        RuntimeCall::make_root_of(&mut schema);
322        Address::make_root_of(&mut schema);
323
324        schema.finalize()?;
325        Ok(schema)
326    }
327
328    /// Get the chain data for the schema.
329    pub fn chain_data(&self) -> &ChainData {
330        &self.chain_data
331    }
332
333    #[cfg(not(feature = "serde"))]
334    pub fn metadata_hash(&self) -> Result<[u8; 32], SchemaError> {
335        // In borsh-only context, the hash must have been deserialized
336        // If it's not present, that's a critical error
337        self.extra_metadata_hash
338            .0
339            .get()
340            .copied()
341            .ok_or(SchemaError::MetadataHashNotInitialized)
342    }
343
344    #[cfg(feature = "serde")]
345    pub fn metadata_hash(&self) -> Result<[u8; 32], SchemaError> {
346        // In serde context, calculate on first use if not present
347        self.extra_metadata_hash
348            .0
349            .get_or_try_init(|| self.calculate_metadata_hash())
350            .copied()
351    }
352
353    #[cfg(feature = "serde")]
354    fn calculate_metadata_hash(&self) -> Result<[u8; 32], SchemaError> {
355        let mut hasher = Sha256::new();
356        hasher.update(&borsh::to_vec(&self.templates)?);
357        hasher.update(&borsh::to_vec(&self.serde_metadata)?);
358        Ok(hasher.finalize().into())
359    }
360
361    #[cfg(feature = "serde")]
362    pub fn from_json(input: &str) -> Result<Self, serde_json::Error> {
363        serde_json::from_str(input)
364    }
365
366    pub fn rollup_expected_index(&self, rollup_type: RollupRoots) -> Result<usize, SchemaError> {
367        self.root_type_indices
368            .get(rollup_type as usize)
369            .copied()
370            .ok_or(SchemaError::MissingRollupRoot(rollup_type))
371    }
372
373    /// Use the schema to display the given type using the provided borsh encoded input
374    pub fn display(&self, type_index: usize, input: &[u8]) -> Result<String, SchemaError> {
375        let mut output = String::new();
376        let input = &mut &input[..];
377        let mut visitor = DisplayVisitor::new(input, &mut output);
378        self.types
379            .get(type_index)
380            .ok_or(SchemaError::InvalidIndex(type_index))?
381            .visit(self, &mut visitor, DisplayContext::default())?;
382
383        if !visitor.has_displayed_whole_input() {
384            return Err(FormatError::UnusedInput.into());
385        }
386        Ok(output)
387    }
388
389    /// EIP712-compatible JSON encoding of an object, which can be used directly as a parameter
390    /// for `eth_signTypedData_v4` RPCs.
391    #[cfg(feature = "eip712")]
392    pub fn eip712_json(&self, type_index: usize, input: &[u8]) -> Result<String, SchemaError> {
393        let Some(typed_data) = self.eip712_get_typed_data_inner(type_index, input)? else {
394            return Ok(String::default());
395        };
396        Ok(serde_json::to_string(&typed_data)?)
397    }
398
399    /// The EIP712 signing hash of an object, corresponding to its `eip712_json` encoding.
400    /// This hash should be signed directly with a secp256k1 key to obtain the EIP712 signature.
401    #[cfg(feature = "eip712")]
402    pub fn eip712_signing_hash(
403        &self,
404        type_index: usize,
405        input: &[u8],
406    ) -> Result<[u8; 32], SchemaError> {
407        let Some(typed_data) = self.eip712_get_typed_data_inner(type_index, input)? else {
408            return Ok(Default::default());
409        };
410        Ok(typed_data.eip712_signing_hash()?.into())
411    }
412
413    /// The unashed EIP712 signing digest of an object, corresponding to its `eip712_json` encoding.
414    /// This value needs to be keccak256 hashed then secp256k1 signed to obtain the EIP712 signature.
415    #[cfg(feature = "eip712")]
416    pub fn eip712_signing_digest(
417        &self,
418        type_index: usize,
419        input: &[u8],
420    ) -> Result<[u8; 66], SchemaError> {
421        let Some(typed_data) = self.eip712_get_typed_data_inner(type_index, input)? else {
422            return Ok([0; 66]);
423        };
424        // References:
425        // - https://eips.ethereum.org/EIPS/eip-712#specification-of-the-eth_signtypeddata-json-rpc
426        // - https://github.com/alloy-rs/core/blob/main/crates/dyn-abi/src/eip712/typed_data.rs#L212
427        let mut buf = [0u8; 66];
428        buf[0] = 0x19;
429        buf[1] = 0x01;
430        buf[2..34].copy_from_slice(typed_data.domain.separator().as_slice());
431        buf[34..].copy_from_slice(typed_data.hash_struct()?.as_slice());
432        Ok(buf)
433    }
434
435    #[cfg(feature = "eip712")]
436    fn eip712_get_typed_data_inner(
437        &self,
438        type_index: usize,
439        input: &[u8],
440    ) -> Result<Option<TypedData>, SchemaError> {
441        let mut out_types = Eip712Types::default();
442        let input = &mut &input[..];
443        let mut visitor = Eip712Visitor::new(input, &mut out_types);
444        let root_type = self
445            .types()
446            .get(type_index)
447            .ok_or(SchemaError::InvalidIndex(type_index))?;
448        let Some(visitor_return) = root_type.visit(self, &mut visitor, Eip712Context::default())?
449        else {
450            return Ok(None);
451        };
452        if !visitor.has_displayed_whole_input() {
453            return Err(FormatError::UnusedInput.into());
454        }
455
456        // We manually add the EIP712Domain type outside of the visitor
457        // unwrap: hardcoded types are known to be valid and should never fail to construct
458        out_types.insert(
459            "EIP712Domain".to_string(),
460            vec![
461                PropertyDef::new("string", "name").unwrap(),
462                PropertyDef::new("uint256", "chainId").unwrap(),
463                PropertyDef::new("bytes32", "salt").unwrap(),
464            ],
465        );
466
467        Ok(Some(TypedData {
468            domain: alloy_dyn_abi::Eip712Domain {
469                name: Some(self.chain_data.chain_name.clone().into()),
470                version: None,
471                chain_id: Some(alloy_primitives::U256::from(self.chain_data.chain_id)),
472                // Our chain hash is 32 bytes. We could truncate it to fit in the 20-byte ethereum
473                // Address, but by putting it in the salt we retain the full entropy and security.
474                verifying_contract: None,
475                salt: Some(self.chain_hash()?.into()),
476            },
477            resolver: out_types.into(),
478            primary_type: visitor_return.unique_type_name,
479            message: visitor_return.json_value,
480        }))
481    }
482
483    /// Use the schema to convert a serde-compatible JSON string of the given type into its borsh
484    /// encoding
485    #[cfg(feature = "serde")]
486    pub fn json_to_borsh(&self, type_index: usize, input: &str) -> Result<Vec<u8>, SchemaError> {
487        let mut output = Vec::new();
488
489        let mut visitor = EncodeVisitor::new(&mut output)?;
490
491        self.types
492            .get(type_index)
493            .ok_or(SchemaError::InvalidIndex(type_index))?
494            .visit(self, &mut visitor, EncodeContext::new(input, type_index)?)?;
495
496        Ok(output)
497    }
498
499    /// Use a stub JSON to create a full type using the named template.
500    #[cfg(feature = "serde")]
501    pub fn fill_template_from_json(
502        &self,
503        root_index: usize,
504        template_name: &str,
505        input: &str,
506    ) -> Result<Vec<u8>, SchemaError> {
507        fn serde_to_schema_err(e: serde_json::Error) -> SchemaError {
508            SchemaError::EncodeError(EncodeError::Json(e.to_string()))
509        }
510
511        let template = self
512            .templates
513            .get(root_index)
514            .ok_or(SchemaError::InvalidIndex(root_index))?
515            .0
516            .get(template_name)
517            .ok_or(SchemaError::UnknownTemplate(template_name.to_string()))?;
518
519        // Parse the JSON as a map/object of inputs
520        let mut input_map: serde_json::Map<String, serde_json::Value> =
521            serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(input)
522                .map_err(serde_to_schema_err)?;
523
524        let mut output = template.preencoded_bytes().to_owned();
525
526        // For every input in the template, starting from the end (to preserve the `offset` values
527        // of previous inputs)...
528        for (name, input) in template.inputs().iter().rev() {
529            // ...get its type from the schema,
530            let ty = match input.type_link() {
531                Link::ByIndex(i) => self.types.get(*i).expect("Template {name} contained an invalid link: {i}. This is a major bug with template generation."),
532                Link::Immediate(ty) => &ty.clone().into(),
533                Link::Placeholder | Link::IndexedPlaceholder(_) => panic!("Template {name} contained placeholder link. This is a major bug with template generation.")
534            };
535            // find the corresponding JSON value,
536            let json_value = input_map.remove(name).ok_or(EncodeError::MissingType {
537                name: name.to_owned(),
538            })?;
539            // and use our json_to_borsh functionality to get the bytes for the input.
540            let mut buf = Vec::new();
541            let mut visitor = EncodeVisitor::new(&mut buf)?;
542            ty.visit(
543                self,
544                &mut visitor,
545                EncodeContext::from_val(json_value, input.type_link()),
546            )?;
547
548            // Finally, splice the obtained bytes at the specified offset into the template.
549            output.splice(input.offset()..input.offset(), buf);
550        }
551
552        if !input_map.is_empty() {
553            // Unwrap: we know input_map isn't empty, so it must have at least one entry
554            return Err(SchemaError::EncodeError(EncodeError::UnusedInput {
555                value: input_map.iter().next().unwrap().0.to_owned(),
556            }));
557        }
558
559        Ok(output)
560    }
561
562    /// Lists all templates available for the given root type.
563    #[cfg(feature = "serde")]
564    pub fn templates(&self, index: usize) -> Result<Vec<String>, SchemaError> {
565        Ok(self
566            .templates
567            .get(index)
568            .ok_or(SchemaError::InvalidIndex(index))?
569            .0
570            .keys()
571            .cloned()
572            .collect())
573    }
574
575    /// Returns the chain ID calculated using the merkle root of all the schema types, combined
576    /// with any chain-specific metadata.
577    /// This allows the chain ID to be used for verification of the schema (and thus verification
578    /// that a transaction claiming to correspond to a given schema will have the effect it claims).
579    pub fn chain_hash(&self) -> Result<[u8; 32], SchemaError> {
580        self.chain_hash
581            .get_or_try_init(|| {
582                // First, merkleize the schema
583                let merkle_root = self.merkle_root()?;
584
585                // Then, hash the auxilliary internal data - root indices and chain data
586                let mut hasher = Sha256::new();
587                hasher.update(&borsh::to_vec(&self.root_type_indices)?);
588                hasher.update(&borsh::to_vec(&self.chain_data)?);
589                let internal_data_hash: [u8; 32] = hasher.finalize().into();
590
591                // Get the metadata hash
592                let metadata_hash = self.metadata_hash()?;
593
594                // Finally, combine the three hashes in order to get the final chain hash
595                let mut hasher = Sha256::new();
596                hasher.update(merkle_root);
597                hasher.update(internal_data_hash);
598                hasher.update(metadata_hash);
599
600                let chain_hash: [u8; 32] = hasher.finalize().into();
601                Ok(chain_hash)
602            })
603            .copied()
604    }
605
606    fn merkle_root(&self) -> Result<[u8; 32], SchemaError> {
607        let (_, root) = self.merkle_tree.0.get_or_try_init(|| {
608            let mut tree = MerkleTree::new();
609            for ty in &self.types {
610                tree.push_raw_leaf(&borsh::to_vec(ty)?)
611            }
612            let root = tree.root();
613            Ok::<_, SchemaError>((tree, root))
614        })?;
615        Ok(*root)
616    }
617
618    fn finalize(&self) -> Result<(), SchemaError> {
619        // Ensure both hashes are calculated and cached
620        self.metadata_hash()?;
621        self.chain_hash()?;
622        Ok(())
623    }
624
625    pub fn types(&self) -> &[Ty<IndexLinking>] {
626        &self.types
627    }
628
629    pub fn serde_metadata(&self) -> &[ContainerSerdeMetadata] {
630        &self.serde_metadata
631    }
632
633    pub fn root_types(&self) -> &[usize] {
634        &self.root_type_indices
635    }
636
637    fn find_item_by_id(&self, item_id: &ItemId) -> Option<usize> {
638        self.known_types.get(item_id).copied()
639    }
640
641    /// Link a child type to its parent, panicking if the parent type is not in the schema or if the parent type has no more placeholders.
642    fn link_child_to_parent(&mut self, parent: ItemId, child: Link) {
643        let idx = self.known_types.get(&parent).unwrap_or_else(|| panic!("Tried to link a child to a parent ({parent:?}) that the schema doesn't have. This is a bug in a hand-written schema."));
644
645        let remaining_children = *self.under_construction.get(&parent).unwrap_or_else(|| panic!("Tried to link too many children to parent ({parent:?}). This is a bug in a hand-written schema."));
646        if remaining_children == 1 {
647            self.under_construction.remove(&parent);
648        } else {
649            self.under_construction
650                .insert(parent, remaining_children - 1);
651        }
652        self.types[*idx].fill_next_placholder(child);
653    }
654
655    /// Get a link to the given type, adding it to the top-level schema if necessary.
656    /// Unlike all other methods in this crate, the linked type returned by this method is allowed to be only partially generated.
657    ///
658    /// It is the responsibility of the caller to complete the returned link.
659    fn get_partial_link_to(
660        &mut self,
661        item: Item<IndexLinking>,
662        item_id: ItemId,
663    ) -> MaybePartialLink {
664        match item {
665            Item::Container(c) => {
666                if let Some(location) = self.find_item_by_id(&item_id) {
667                    MaybePartialLink::Complete(Link::ByIndex(location))
668                } else {
669                    let num_children = c.num_children();
670                    let serde_metadata = c.serde();
671                    let location = self.types.len();
672                    self.known_types.insert(item_id.clone(), location);
673                    self.types.push(c.into());
674                    self.serde_metadata.push(serde_metadata);
675                    if num_children != 0 {
676                        self.under_construction.insert(item_id, num_children);
677                        MaybePartialLink::Partial(Link::ByIndex(location))
678                    } else {
679                        MaybePartialLink::Complete(Link::ByIndex(location))
680                    }
681                }
682            }
683            Item::Atom(primitive) => MaybePartialLink::Complete(Link::Immediate(primitive)),
684        }
685    }
686
687    /// After generating a root type, register it with the schema for ease of reference. Sets
688    /// the canonical "root links", so has to be carefully called in the right order (normally,
689    /// immediately after root type construction, with the link to the newly created type).
690    /// No-op for primitive links.
691    /// Panics on placeholder links.
692    fn push_root_link(&mut self, link: Link) {
693        match link {
694            Link::ByIndex(i) => self.root_type_indices.push(i),
695            Link::Immediate(..) => {},
696            Link::Placeholder | Link::IndexedPlaceholder(_) => panic!("Attempted to register a placeholder link as a schema root - are you passing the right link?"),
697        }
698    }
699}
700
701pub enum Item<L: LinkingScheme> {
702    Container(Container<L>),
703    Atom(Primitive),
704}
705
706/// Generate the schema for a type.
707/// For complex types, this should typically be derived with a macro,
708/// rather than implemented by hand.
709/// This is also automatically implemented for all types implementing `OverrideSchema`.
710pub trait UniversalWallet: Sized + 'static {
711    /// Ensure that each type contained in the outer type (i.e. the type of each struct/tuple field) is added to the schema,
712    /// and return a `Link` connecting the child to the parent.
713    ///
714    /// Ideally, this function would return something like `Box<dyn UniversalWallet>`.
715    /// Unfortunately, we need to return *types*, not instances (because we don't want to
716    /// add a `Default` bound on all types that implement UniversalWallet) which Rustc doesn't like.
717    /// So, we have a slightly messier signature where the type is expected to register each of its child
718    /// types with the schema directly rather than returning them to the caller for future registration.
719    fn get_child_links(schema: &mut Schema) -> Vec<Link>;
720
721    /// Generate the "scaffolding" of the item. If the item is a primtive, this is just the corresponding primtive.
722    /// If the type is composed of other types, this is the container with all links set to [`Link::Placeholder`].
723    fn scaffold() -> Item<IndexLinking>;
724
725    /// Writes the type to the schema if it is not already present and returns a link to it.
726    ///
727    /// Any child types will have their schemas generated as well, but the placement of those types is left
728    /// to the discretion of the implementation - they may or may not appear at the top level of the schema.
729    fn write_schema(schema: &mut Schema) -> Link {
730        let item = Self::scaffold();
731        let item_id = ItemId::of::<Self>();
732        match item {
733            Item::Atom(_primitive) => {
734                // When recursively building the schema, primitives get filled in directly as
735                // Link::Immediate and do not get `write_schema` called for them. Thus this can
736                // only happen from a user call.
737                // Forbidding this makes managing metadata significantly easier.
738                panic!("Creating a schema for primitive root types is not supported. If this is necessary, wrap the primitive in a newtype struct. If you did not specify a primitive root type, this may be a bug in schema generation.");
739            }
740            Item::Container(container) => {
741                let link = schema.get_partial_link_to(Item::Container(container), item_id.clone());
742                if let MaybePartialLink::Complete(link) = link {
743                    return link;
744                }
745
746                for child in Self::get_child_links(schema) {
747                    schema.link_child_to_parent(item_id.clone(), child);
748                }
749                link.into_inner()
750            }
751        }
752    }
753
754    /// Writes the type and all its children to the schema, if not already present, and sets the
755    /// type as a root type. Generates any templates defined on that type.
756    fn make_root_of(schema: &mut Schema) {
757        let link = Self::write_schema(schema);
758        assert!(
759            schema.under_construction.is_empty(),
760            "Schema generation left some types partially constructed. This is a bug in the schema. {schema:?}"
761        );
762        schema.push_root_link(link);
763        let templates = Self::get_child_templates(schema);
764        schema.templates.push(templates);
765    }
766
767    /// Empty by default
768    /// When derived by the macro, builds a template set from annotations on the fields + the field
769    /// types' own get_child_templates()
770    fn get_child_templates(_schema: &mut Schema) -> TransactionTemplateSet {
771        Default::default()
772    }
773
774    /// Gets a link to the type, writing the type to the schema if necessary.
775    fn make_linkable(schema: &mut Schema) -> Link {
776        match Self::scaffold() {
777            Item::Container(_) => Self::write_schema(schema),
778            Item::Atom(atom) => Link::Immediate(atom),
779        }
780    }
781
782    /// Override the type ID of the item. This should typically not be written by hand. Instead,
783    /// use the [`OverrideSchema`] trait.
784    fn id_override() -> Option<ItemId> {
785        None
786    }
787}
788
789/// Establish that this type should use the `Output` type to generate its schema.
790/// This is appropriate for cases where different types represent the same kind of data structure.
791/// For instance, HashMap and BTreeMap both represent a `Container::Map` in the data model of the
792/// schema; their internal implementation differences don't affect the shape of their schemas.
793///
794/// Note that, for types to be considered equivalent in the schema, their borsh and JSON
795/// serialisations must both also be equivalent.
796pub trait OverrideSchema {
797    type Output: UniversalWallet;
798}
799
800impl<T: OverrideSchema + 'static> UniversalWallet for T {
801    fn scaffold() -> Item<IndexLinking> {
802        <Self as OverrideSchema>::Output::scaffold()
803    }
804    fn get_child_links(schema: &mut Schema) -> Vec<Link> {
805        <Self as OverrideSchema>::Output::get_child_links(schema)
806    }
807    fn id_override() -> Option<ItemId> {
808        <Self as OverrideSchema>::Output::id_override()
809    }
810    fn make_linkable(schema: &mut Schema) -> Link {
811        <Self as OverrideSchema>::Output::make_linkable(schema)
812    }
813    fn write_schema(schema: &mut Schema) -> Link {
814        <Self as OverrideSchema>::Output::write_schema(schema)
815    }
816}