polkadot-js/tools

View on GitHub
packages/metadata-cmp/src/runcli.ts

Summary

Maintainability
B
5 hrs
Test Coverage
// Copyright 2018-2024 @polkadot/metadata-cmp authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { Metadata } from '@polkadot/types';
import type { RuntimeVersion, StorageEntryMetadataLatest } from '@polkadot/types/interfaces';
import type { Registry } from '@polkadot/types/types';

import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

import { ApiPromise, WsProvider } from '@polkadot/api';
import { expandMetadata } from '@polkadot/types';
import { getSiName } from '@polkadot/types/metadata/util';
import { unwrapStorageType } from '@polkadot/types/util';
import { assert, stringCamelCase } from '@polkadot/util';

interface ArgV {
  _: [string, string]
}

const [ws1, ws2] = (yargs(hideBin(process.argv)).demandCommand(2).argv as unknown as ArgV)._;

// configure padding
const lvlInc = 14;
const deltaInc = 4;
const lvl1 = 20;
const lvl2 = lvl1 + lvlInc;
const lvl3 = lvl1 + 2 * lvlInc;
const lvl5 = lvl1 + 4 * lvlInc;
const chunkSize = 5;

function chunk (array: string[], size: number): string[][] {
  const chunked = [];
  const copied = [...array];
  const numOfChild = Math.ceil(copied.length / size);

  for (let i = 0; i < numOfChild; i++) {
    chunked.push(copied.splice(0, size));
  }

  return chunked;
}

function log (pad: number, title: string, pre: string, text: string, post?: string) {
  console.log(createLog(pad, title, pre, text, post));
}

function createLog (pad: number, title: string, pre: string, text: string, post?: string): string {
  const titleStr = pad > 0 ? (title ? `[${title}]` : '') : title;

  return `${titleStr.padStart(pad)}${pre ? ` ${pre}` : ''} ${text}${post ? ` (${post})` : ''}`;
}

function createCompare (a: string | number = '-', b: string | number = '-'): string {
  return `${a === b ? a : `${a} -> ${b}`}`;
}

function logArray (pad: number, title: string, pre: string, arr: string[], chunkSize: number) {
  for (const ch of chunk(arr, chunkSize)) {
    console.log(createLog(pad, title, pre, ch.join(', ')));
    title = pre = '';
  }
}

function expandMapKey ({ lookup }: Registry, { type }: StorageEntryMetadataLatest): [string, string, string] {
  const map = type.asMap;

  return [
    map.hashers.map((h) => h.toString()).join(', '),
    (
      map.hashers.length === 1
        ? [map.key]
        : lookup.getSiType(map.key).def.asTuple
    ).map((t) => getSiName(lookup, t)).join(', '),
    getSiName(lookup, map.value)
  ];
}

async function getMetadata (url: string): Promise<[Registry, Metadata, RuntimeVersion]> {
  assert(url.startsWith('ws://') || url.startsWith('wss://'), `Invalid WebSocket endpoint ${url}, expected ws:// or wss://`);

  const provider = new WsProvider(url);
  const api = await ApiPromise.create({ provider });

  provider.on('error', () => process.exit());

  return Promise.all([
    Promise.resolve(api.registry),
    api.rpc.state.getMetadata(),
    api.rpc.state.getRuntimeVersion()
  ]);
}

// our main entry point - from here we call out
async function main (): Promise<number> {
  const [[regA, metaA, verA], [regB, metaB, verB]] = await Promise.all([getMetadata(ws1), getMetadata(ws2)]);
  const a = metaA.asLatest;
  const b = metaB.asLatest;

  log(lvl1, 'Spec', 'name:', createCompare(verA.specName.toString(), verB.specName.toString()));
  log(lvl1, '', 'spec_version:', createCompare(verA.specVersion.toNumber(), verB.specVersion.toNumber()));
  log(lvl1, '', 'transaction_version:', createCompare(verA.transactionVersion.toNumber(), verB.transactionVersion.toNumber()));
  log(lvl1, 'Metadata', 'version:', createCompare(metaA.version, metaB.version));

  const mA = a.pallets.map(({ name }) => name.toString());
  const mB = b.pallets.map(({ name }) => name.toString());

  log(lvl1, 'Modules', 'num:', createCompare(mA.length, mB.length));

  const mAdd = mB.filter((m) => !mA.includes(m));
  const mDel = mA.filter((m) => !mB.includes(m));

  logArray(lvl1 + deltaInc, '+', 'modules:', mAdd, chunkSize);
  logArray(lvl1 + deltaInc, '-', 'modules:', mDel, chunkSize);
  console.log();

  const decA = expandMetadata(regA, metaA);
  const decB = expandMetadata(regB, metaB);

  mA
    .filter((m) => mB.includes(m))
    .forEach((m): void => {
      const n = stringCamelCase(m);
      const eA = Object.keys(decA.tx[n] || {});
      const eB = Object.keys(decB.tx[n] || {});

      if (eA.length === eB.length && eA.length === 0) {
        return;
      }

      const sA = Object.keys(decA.query[n] || {});
      const sB = Object.keys(decB.query[n] || {});

      if (sA.length === sB.length && sA.length === 0) {
        return;
      }

      const count = createCompare(eA.length, eB.length);
      const storage = createCompare(sA.length, sB.length);
      const post = `calls: ${count}, storage: ${storage}`;
      const index = createCompare(decA.tx[n]?.[eA[0]]?.callIndex[0], decB.tx[n]?.[eB[0]]?.callIndex[0]);

      log(lvl2, m, 'idx:', index, post);

      const eAdd = eB.filter((e) => !eA.includes(e));
      const eDel = eA.filter((e) => !eB.includes(e));

      logArray(lvl2 + deltaInc, '+', 'calls:', eAdd, chunkSize);
      logArray(lvl2 + deltaInc, '-', 'calls:', eDel, chunkSize);

      eA
        .filter((c) => eB.includes(c))
        .forEach((c): void => {
          const cA = decA.tx[n][c];
          const cB = decB.tx[n][c];
          const tA = cA.meta.args.map(({ type }) => type.toString());
          const tB = cB.meta.args.map(({ type }) => type.toString());
          const typeDiff = tA.length !== tB.length || tA.some((t, index) => tB[index] !== t);

          if (cA.callIndex[1] !== cB.callIndex[1] || typeDiff) {
            const params = `args: ${createCompare(tA.length, tB.length)}`;

            log(lvl3, c, 'idx:', createCompare(cA.callIndex[1], cB.callIndex[1]), params);
            const signature = createCompare(`(${tA.join(', ')})`, `(${tB.join(', ')})`);

            if (signature !== '()') {
              log(lvl3, '', '', signature);
            }
          }
        });

      const sAdd = sB.filter((e) => !sA.includes(e));
      const sDel = sA.filter((e) => !sB.includes(e));

      logArray(lvl2 + deltaInc, '+', 'storage:', sAdd, 5);
      logArray(lvl2 + deltaInc, '-', 'storage:', sDel, 5);

      sA
        .filter((c) => sB.includes(c))
        .forEach((c): void => {
          const cA = decA.query[n][c];
          const cB = decB.query[n][c];
          const tA = unwrapStorageType(regA, cA.meta.type, cA.meta.modifier.isOptional);
          const tB = unwrapStorageType(regB, cB.meta.type, cB.meta.modifier.isOptional);

          // storage types differ
          if (tA !== tB) {
            if (cA.meta.type.isMap && cB.meta.type.isMap) {
              const diffs = [];
              const [hA, kA, vA] = expandMapKey(regA, cA.meta);
              const [hB, kB, vB] = expandMapKey(regB, cB.meta);

              if (hA !== hB) {
                diffs.push(`hashers: ${createCompare(hA, hB)}`);
              }

              if (kA !== kB) {
                diffs.push(`keys: ${createCompare(kA, kB)}`);
              }

              if (vA !== vB) {
                diffs.push(`value: ${createCompare(vA, vB)}`);
              }

              logArray(lvl3, c, '', diffs, 1);
            } else if (cA.meta.type.isPlain && cB.meta.type.isPlain) {
              // diff plain type
              log(lvl3, c, 'type:', createCompare(tA, tB));
            } else {
              // fallback diff if types are completely different
              log(lvl3, c, '', tA);
              log(lvl5, '', '', '->');
              log(lvl3, '', '', tB);
            }
          }
        });

      console.log();
    });

  return 0;
}

process.on('unhandledRejection', (error): void => {
  console.error(error);
  process.exit(1);
});

main()
  .then((code) => process.exit(code))
  .catch((error): void => {
    console.error(error);
    process.exit(1);
  });