polymesh-api-client 3.8.2

Polymesh API client core
Documentation
#![allow(deprecated)]
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::prelude::*;

use anyhow::Result;

use codec::Decode;
use frame_metadata::v14::*;
use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed};
use scale_info::{form::PortableForm, PortableRegistry, TypeDef};

pub struct StorageMetadata {
  pub prefix: String,
  pub entries: HashMap<String, StorageEntryMetadata<PortableForm>>,
}

impl StorageMetadata {
  pub fn from_v14(md: PalletStorageMetadata<PortableForm>) -> Self {
    Self {
      prefix: md.prefix,
      entries: md
        .entries
        .into_iter()
        .map(|entry| (entry.name.clone(), entry))
        .collect(),
    }
  }
}

pub struct Metadata {
  pub types: PortableRegistry,
  pub storage: HashMap<String, StorageMetadata>,
}

impl Metadata {
  pub fn from_v14(md: RuntimeMetadataV14) -> Self {
    let mut storage_prefix = HashMap::new();
    Self {
      types: md.types,
      storage: md
        .pallets
        .into_iter()
        .filter_map(|p| {
          p.storage.map(|s| {
            if let Some(old_pallet) = storage_prefix.insert(s.prefix.clone(), p.name.clone()) {
              log::error!(
                "Duplicate storage prefix '{}' used by {} and {}",
                s.prefix,
                old_pallet,
                p.name
              );
            }
            (p.name.clone(), StorageMetadata::from_v14(s))
          })
        })
        .collect(),
    }
  }

  pub fn from_file(filename: String) -> Result<Self> {
    let mut file = File::open(&filename)?;
    let mut buf = Vec::new();
    file.read_to_end(&mut buf)?;
    let metadata = RuntimeMetadataPrefixed::decode(&mut buf.as_slice())?;
    match metadata.1 {
      RuntimeMetadata::V14(md) => Ok(Self::from_v14(md)),
      _ => {
        panic!("Unsupported metadata version: {:?}", metadata);
      }
    }
  }

  fn resolve(&self, id: u32) -> &scale_info::Type<PortableForm> {
    self.types.resolve(id).expect("expect type")
  }

  fn resolve_name(&self, id: u32) -> String {
    let ty = self.resolve(id);
    ty.path()
      .ident()
      .unwrap_or_else(|| format!("{:?}", ty.type_def()))
  }

  pub fn is_types_compatible(
    &self,
    seen: &mut HashMap<(u32, u32), bool>,
    other: &Self,
    id1: u32,
    id2: u32,
  ) -> bool {
    let mut compatible = true;

    if let Some(comp) = seen.get(&(id1, id2)) {
      return *comp;
    }
    // Mark the type pair as seen to prevent recursive checks.
    seen.insert((id1, id2), true);

    let ty1 = self.resolve(id1);
    let ty2 = other.resolve(id2);

    // Ignore `RuntimeCall`.
    let ident1 = ty1.path().ident().unwrap_or_default();
    let ident2 = ty2.path().ident().unwrap_or_default();
    match ident1.as_str() {
      "RuntimeCall" | "RuntimeEvent" => {
        return true;
      }
      _ => (),
    }

    match (ty1.type_def(), ty2.type_def()) {
      (TypeDef::Composite(v1), TypeDef::Composite(v2)) => {
        if v1.fields().len() != v2.fields().len() {
          compatible = false;
          log::trace!("Composites have different number of fields: {v1:?} != {v2:?}");
        } else {
          for (f1, f2) in v1.fields().into_iter().zip(v2.fields().into_iter()) {
            if !self.is_types_compatible(seen, other, f1.ty().id(), f2.ty().id()) {
              compatible = false;
            }
          }
        }
      }
      (TypeDef::Variant(v1), TypeDef::Variant(v2)) => {
        let variants2: HashMap<u8, _> = v2.variants().iter().map(|v| (v.index, v)).collect();
        for variant1 in v1.variants() {
          match variants2.get(&variant1.index) {
            Some(variant2) => {
              if variant1.fields.len() != variant2.fields.len() {
                compatible = false;
                log::trace!(
                  "Enum variant has different number of fields: {variant1:?} != {variant2:?}"
                );
              } else {
                for (f1, f2) in variant1.fields.iter().zip(variant2.fields.iter()) {
                  if !self.is_types_compatible(seen, other, f1.ty().id(), f2.ty().id()) {
                    compatible = false;
                  }
                }
              }
            }
            None => {
              compatible = false;
              log::trace!("Enum variant removed: {:?}", variant1);
            }
          }
        }
      }
      (TypeDef::Sequence(v1), TypeDef::Sequence(v2)) => {
        if !self.is_types_compatible(seen, other, v1.type_param().id(), v2.type_param().id()) {
          compatible = false;
        }
      }
      (TypeDef::Array(v1), TypeDef::Array(v2)) => {
        if v1.len() != v2.len() {
          compatible = false;
          log::trace!("Different Array lengths: {v1:?} != {v2:?}");
        }
        if !self.is_types_compatible(seen, other, v1.type_param().id(), v2.type_param().id()) {
          compatible = false;
        }
      }
      (TypeDef::Tuple(v1), TypeDef::Tuple(v2)) => {
        if v1.fields().len() != v2.fields().len() {
          compatible = false;
          log::trace!("Tuples have different number of fields: {v1:?} != {v2:?}");
        }
        for (f1, f2) in v1.fields().into_iter().zip(v2.fields().into_iter()) {
          if !self.is_types_compatible(seen, other, f1.id(), f2.id()) {
            compatible = false;
          }
        }
      }
      (TypeDef::Primitive(v1), TypeDef::Primitive(v2)) => {
        if v1 != v2 {
          compatible = false;
        }
      }
      (TypeDef::Compact(v1), TypeDef::Compact(v2)) => {
        if !self.is_types_compatible(seen, other, v1.type_param().id(), v2.type_param().id()) {
          compatible = false;
        }
      }
      (TypeDef::BitSequence(v1), TypeDef::BitSequence(v2)) => {
        if !self.is_types_compatible(
          seen,
          other,
          v1.bit_order_type().id(),
          v2.bit_order_type().id(),
        ) {
          compatible = false;
        }
        if !self.is_types_compatible(
          seen,
          other,
          v1.bit_store_type().id(),
          v2.bit_store_type().id(),
        ) {
          compatible = false;
        }
      }
      _ => {
        compatible = false;
        log::trace!("Different TypeDef: {ident1:?}.type_def != {ident2:?}.type_def");
      }
    }

    if !compatible {
      // If not compatible, update `seen` cache.
      seen.insert((id1, id2), false);
      log::trace!("Different types: {ident1:?} != {ident2:?}");
    }

    return compatible;
  }

  pub fn is_storage_entry_compatible(
    &self,
    seen: &mut HashMap<(u32, u32), bool>,
    other: &Self,
    entry: &StorageEntryMetadata<PortableForm>,
    entry2: &StorageEntryMetadata<PortableForm>,
  ) -> bool {
    let mut compatible = true;

    if entry.modifier != entry2.modifier {
      compatible = false;
    }

    // Check storage types.
    match &entry.ty {
      StorageEntryType::Plain(ty1) => match &entry2.ty {
        StorageEntryType::Plain(ty2) => {
          if !self.is_types_compatible(seen, other, ty1.id(), ty2.id()) {
            log::trace!(
              "Storage entry {:?}: Plain type changed '{}' -> '{}'",
              entry.name,
              self.resolve_name(ty1.id()),
              other.resolve_name(ty2.id())
            );
            compatible = false;
          }
        }
        _ => {
          log::trace!("Storage entry {:?} type changed", entry.name);
          compatible = false;
        }
      },
      StorageEntryType::Map {
        hashers,
        key,
        value,
      } => match &entry2.ty {
        StorageEntryType::Map {
          hashers: hashers2,
          key: key2,
          value: value2,
        } => {
          if hashers != hashers2 {
            compatible = false;
            log::trace!(
              "Hashers changed on storage entry {:?}: {hashers:?} -> {hashers2:?}",
              entry.name
            );
          }
          if !self.is_types_compatible(seen, other, key.id(), key2.id()) {
            log::trace!(
              "Storage entry {:?}: map key type changed '{}' -> '{}'",
              entry.name,
              self.resolve_name(key.id()),
              other.resolve_name(key2.id())
            );
            compatible = false;
          }
          if !self.is_types_compatible(seen, other, value.id(), value2.id()) {
            log::trace!(
              "Storage entry {:?}: map value type changed '{}' -> '{}'",
              entry.name,
              self.resolve_name(value.id()),
              other.resolve_name(value2.id())
            );
            compatible = false;
          }
        }
        _ => {
          log::trace!("Storage entry {:?} type changed", entry.name);
          compatible = false;
        }
      },
    }

    return compatible;
  }

  pub fn is_compatible(&self, other: &Self) -> bool {
    let mut compatible = true;
    let mut seen = HashMap::new();

    // Check each pallet's storage.
    for (pallet_name, storage) in &self.storage {
      match other.storage.get(pallet_name) {
        Some(storage2) => {
          log::trace!("Check pallet: {pallet_name:?}");
          // Check each storage entry in the pallet of both metadata.
          for (name, entry) in &storage.entries {
            match storage2.entries.get(name) {
              Some(entry2) => {
                log::trace!("  -- Storage entry: {name:?}");
                if !self.is_storage_entry_compatible(&mut seen, other, entry, entry2) {
                  compatible = false;
                  log::warn!("Storage entries different: {pallet_name}.{name}");
                }
              }
              None => {
                log::warn!("Removed pallet storage entry: {pallet_name}.{name}");
              }
            }
          }
        }
        None => {
          log::warn!("Removed pallet storage: {pallet_name}");
        }
      }
    }

    return compatible;
  }
}

fn main() -> Result<()> {
  dotenv::dotenv().ok();
  env_logger::init();

  let metadata1 = Metadata::from_file(env::args().nth(1).expect("Missing metadata file1."))?;
  let metadata2 = Metadata::from_file(env::args().nth(2).expect("Missing metadata file2."))?;

  let compatible = metadata1.is_compatible(&metadata2);

  eprintln!("results = {compatible}");

  Ok(())
}