Repository Public
1// Substrate runtime metadata (version 14, what `state_getMetadata` returns) — a reader written by hand for exactly what the2// Polkadot adapter relies on: the type registry, the pallets (calls, events, storage, constants) and the list of transaction3// extensions. It also decodes any SCALE value from its type id (used for System.Events and System.Account) and computes4// storage keys. API side only: nothing here is in the signer.5// Format: frame-metadata `RuntimeMetadataV14` + scale-info `PortableRegistry`6// (https://github.com/paritytech/frame-metadata/blob/main/frame-metadata/src/v14.rs, https://github.com/paritytech/scale-info).7// Proven against @polkadot/types on the live Asset Hub metadata in services/api/test/polkadot-adapter.test.ts.8import { blake2b } from '@noble/hashes/blake2.js';9import { ScaleReader } from '@spw/core';1011export type TypeDef =12 | { kind: 'composite'; fields: Field[] }13 | { kind: 'variant'; variants: { name: string; index: number; fields: Field[] }[] }14 | { kind: 'sequence'; of: number }15 | { kind: 'array'; len: number; of: number }16 | { kind: 'tuple'; of: number[] }17 | { kind: 'primitive'; name: string }18 | { kind: 'compact'; of: number }19 | { kind: 'bits'; store: number; order: number };20export interface Field { name: string | null; type: number; typeName: string | null }21export interface PortableType { path: string[]; params: { name: string; type: number | null }[]; def: TypeDef }22export interface StorageEntry { name: string; modifier: number; hashers: number[]; key: number | null; value: number }23export interface Pallet { name: string; index: number; calls: number | null; events: number | null; storage: StorageEntry[]; constants: { name: string; type: number; value: Uint8Array }[] }24export interface Metadata { types: PortableType[]; pallets: Pallet[]; extrinsic: { type: number; version: number; extensions: { id: string; type: number; implicit: number }[] } }2526const PRIMITIVES = ['bool', 'char', 'str', 'u8', 'u16', 'u32', 'u64', 'u128', 'u256', 'i8', 'i16', 'i32', 'i64', 'i128', 'i256'];27const PRIMITIVE_SIZE: Record<string, number> = { bool: 1, char: 4, u8: 1, u16: 2, u32: 4, u64: 8, u128: 16, u256: 32, i8: 1, i16: 2, i32: 4, i64: 8, i128: 16, i256: 32 };28const dec = new TextDecoder('utf-8', { fatal: true });2930/** Parse `RuntimeMetadataPrefixed` (magic "meta", version 14). Throws on any other version or a malformed document. */31export function parseMetadata(bytes: Uint8Array): Metadata {32 const r = new ScaleReader(bytes);33 if (r.uint(4) !== 0x6174656dn) throw new Error('metadata: bad magic');34 if (r.u8() !== 14) throw new Error('metadata: unsupported version (expected 14)');35 const count = (): number => { const n = r.compact(); if (n > BigInt(bytes.length)) throw new Error('metadata: bad length'); return Number(n); };36 const vec = <T>(f: () => T): T[] => { const n = count(), out: T[] = []; for (let k = 0; k < n; k++) out.push(f()); return out; };37 const str = (): string => dec.decode(r.take(count()));38 const opt = <T>(f: () => T): T | null => { const b = r.u8(); if (b === 0) return null; if (b !== 1) throw new Error('metadata: bad option'); return f(); };39 const id = (): number => Number(r.compact());40 const docs = () => { vec(str); };41 const field = (): Field => { const name = opt(str), type = id(), typeName = opt(str); docs(); return { name, type, typeName }; };42 const typeDef = (): TypeDef => {43 const tag = r.u8();44 if (tag === 0) return { kind: 'composite', fields: vec(field) };45 if (tag === 1) return { kind: 'variant', variants: vec(() => { const name = str(), fields = vec(field), index = r.u8(); docs(); return { name, index, fields }; }) };46 if (tag === 2) return { kind: 'sequence', of: id() };47 if (tag === 3) { const len = Number(r.uint(4)); return { kind: 'array', len, of: id() }; }48 if (tag === 4) return { kind: 'tuple', of: vec(id) };49 if (tag === 5) { const p = PRIMITIVES[r.u8()]; if (!p) throw new Error('metadata: unknown primitive'); return { kind: 'primitive', name: p }; }50 if (tag === 6) return { kind: 'compact', of: id() };51 if (tag === 7) return { kind: 'bits', store: id(), order: id() };52 throw new Error('metadata: unknown type definition');53 };54 const types: PortableType[] = [];55 const n = count();56 for (let k = 0; k < n; k++) {57 if (id() !== k) throw new Error('metadata: type ids are not sequential');58 const path = vec(str), params = vec(() => ({ name: str(), type: opt(id) })), def = typeDef();59 docs();60 types.push({ path, params, def });61 }62 const pallets = vec((): Pallet => {63 const name = str();64 const storage = opt(() => { str(); return vec((): StorageEntry => {65 const sname = str(), modifier = r.u8(), tag = r.u8();66 let hashers: number[] = [], key: number | null = null, value: number;67 if (tag === 0) value = id(); else if (tag === 1) { hashers = vec(() => r.u8()); key = id(); value = id(); } else throw new Error('metadata: bad storage entry');68 r.take(count()); docs();69 return { name: sname, modifier, hashers, key, value };70 }); }) ?? [];71 const calls = opt(id), events = opt(id);72 const constants = vec(() => { const cname = str(), type = id(), value = r.take(count()).slice(); docs(); return { name: cname, type, value }; });73 opt(id); // errors74 return { name, index: r.u8(), calls, events, storage, constants };75 });76 const type = id(), version = r.u8();77 const extensions = vec(() => ({ id: str(), type: id(), implicit: id() }));78 id(); // runtime type79 if (!r.done) throw new Error('metadata: trailing bytes');80 const check = (t: number) => { if (!Number.isInteger(t) || t < 0 || t >= types.length) throw new Error('metadata: dangling type id'); };81 for (const e of extensions) { check(e.type); check(e.implicit); }82 return { types, pallets, extrinsic: { type, version, extensions } };83}8485/**86 * Canonical text of a type, used to compare what the node declares with what was reviewed. Rules: primitives by name;87 * Compact<T>, Vec<T>, [T;n], tuples structurally ("()" for a zero-sized type); Option<T>; any other enum by its name;88 * a struct as Name{field:type,…} when it is the type itself or one of its direct fields, by its name deeper down.89 */90export function renderType(m: Metadata, id: number, depth = 0): string {91 const t = m.types[id];92 if (!t) throw new Error('metadata: dangling type id');93 const name = t.path[t.path.length - 1] ?? '', d = t.def;94 if (d.kind === 'primitive') return d.name;95 if (d.kind === 'compact') return `Compact<${renderType(m, d.of, depth + 1)}>`;96 if (d.kind === 'sequence') return `Vec<${renderType(m, d.of, depth + 1)}>`;97 if (d.kind === 'array') return `[${renderType(m, d.of, depth + 1)};${d.len}]`;98 if (d.kind === 'tuple') return d.of.length === 0 ? '()' : `(${d.of.map((x) => renderType(m, x, depth + 1)).join(',')})`;99 if (d.kind === 'bits') return 'BitVec';100 if (d.kind === 'variant') {101 if (name === 'Option' && t.path.length === 1 && d.variants.length === 2 && d.variants[1]!.fields.length === 1) return `Option<${renderType(m, d.variants[1]!.fields[0]!.type, depth + 1)}>`;102 return name || 'enum';103 }104 if (d.fields.length === 0) return '()';105 if (depth > 1 && name) return name;106 return `${name}{${d.fields.map((f) => (f.name ? f.name + ':' : '') + renderType(m, f.type, depth + 1)).join(',')}}`;107}108export const renderFields = (m: Metadata, fields: Field[]): string => fields.map((f) => (f.name ? f.name + ':' : '') + renderType(m, f.type, 1)).join(',');109110export const palletOf = (m: Metadata, name: string): Pallet => { const p = m.pallets.find((x) => x.name === name); if (!p) throw new Error(`metadata: no ${name} pallet`); return p; };111export function variantsOf(m: Metadata, id: number | null): { name: string; index: number; fields: Field[] }[] {112 const t = id === null ? undefined : m.types[id];113 if (!t || t.def.kind !== 'variant') throw new Error('metadata: expected an enum');114 return t.def.variants;115}116/** First type whose path ends with the given segments (e.g. ['OriginCaller']). */117export function typeByPath(m: Metadata, ...tail: string[]): number {118 const k = m.types.findIndex((t) => t.path.length >= tail.length && tail.every((s, n) => t.path[t.path.length - tail.length + n] === s));119 if (k < 0) throw new Error(`metadata: no type ${tail.join('::')}`);120 return k;121}122123export type Value = bigint | number | boolean | string | Uint8Array | null | Value[] | { [k: string]: Value };124/** Decode one SCALE value of the given type. Enums become { variant, index, value }; byte arrays and Vec<u8> become Uint8Array. */125export function decodeValue(m: Metadata, id: number, r: ScaleReader, depth = 0): Value {126 if (depth > 96) throw new Error('scale: value nested too deeply');127 const t = m.types[id];128 if (!t) throw new Error('metadata: dangling type id');129 const d = t.def;130 const fields = (fs: Field[]): Value => {131 if (fs.length === 0) return null;132 if (fs.every((f) => f.name !== null)) return Object.fromEntries(fs.map((f) => [f.name!, decodeValue(m, f.type, r, depth + 1)]));133 const vs = fs.map((f) => decodeValue(m, f.type, r, depth + 1));134 return vs.length === 1 ? vs[0]! : vs;135 };136 const isU8 = (x: number) => { const e = m.types[x]?.def; return e?.kind === 'primitive' && e.name === 'u8'; };137 switch (d.kind) {138 case 'primitive': {139 if (d.name === 'bool') { const b = r.u8(); if (b > 1) throw new Error('scale: bad bool'); return b === 1; }140 if (d.name === 'str') return dec.decode(r.take(Number(r.compact())));141 const n = PRIMITIVE_SIZE[d.name]!;142 const v = r.uint(n);143 return d.name.startsWith('i') && v >> BigInt(8 * n - 1) ? v - (1n << BigInt(8 * n)) : v;144 }145 case 'compact': { const inner = m.types[d.of]?.def; if (inner && ((inner.kind === 'tuple' && inner.of.length === 0) || (inner.kind === 'composite' && inner.fields.length === 0))) return null; return r.compact(); }146 case 'sequence': { const n = Number(r.compact()); if (isU8(d.of)) return r.take(n).slice(); const out: Value[] = []; for (let k = 0; k < n; k++) out.push(decodeValue(m, d.of, r, depth + 1)); return out; }147 case 'array': { if (isU8(d.of)) return r.take(d.len).slice(); const out: Value[] = []; for (let k = 0; k < d.len; k++) out.push(decodeValue(m, d.of, r, depth + 1)); return out; }148 case 'tuple': return d.of.map((x) => decodeValue(m, x, r, depth + 1));149 case 'composite': return fields(d.fields);150 case 'variant': { const ix = r.u8(), v = d.variants.find((x) => x.index === ix); if (!v) throw new Error('scale: unknown enum variant'); return { variant: v.name, index: ix, value: fields(v.fields) }; }151 case 'bits': {152 const store = m.types[d.store]?.def, unit = store?.kind === 'primitive' ? PRIMITIVE_SIZE[store.name] ?? 1 : 1;153 const bits = Number(r.compact());154 return r.take(Math.ceil(bits / (8 * unit)) * unit).slice();155 }156 }157}158159// ---------------------------------------------------------------- storage keys160const M64 = (1n << 64n) - 1n, P1 = 11400714785074694791n, P2 = 14029467366897019727n, P3 = 1609587929392839161n, P4 = 9650029242287828579n, P5 = 2870177450012600261n;161const rotl = (v: bigint, n: bigint) => ((v << n) | (v >> (64n - n))) & M64;162const rd = (b: Uint8Array, o: number, n: number): bigint => { let v = 0n; for (let k = n - 1; k >= 0; k--) v = (v << 8n) | BigInt(b[o + k]!); return v; };163/** XXH64 (https://github.com/Cyan4973/xxHash/blob/dev/doc/xxhash_spec.md). */164export function xxhash64(input: Uint8Array, seed: bigint): bigint {165 const round = (acc: bigint, lane: bigint) => (rotl((acc + lane * P2) & M64, 31n) * P1) & M64;166 let o = 0, h: bigint;167 if (input.length >= 32) {168 let v1 = (seed + P1 + P2) & M64, v2 = (seed + P2) & M64, v3 = seed, v4 = (seed - P1) & M64;169 for (; o + 32 <= input.length; o += 32) { v1 = round(v1, rd(input, o, 8)); v2 = round(v2, rd(input, o + 8, 8)); v3 = round(v3, rd(input, o + 16, 8)); v4 = round(v4, rd(input, o + 24, 8)); }170 h = (rotl(v1, 1n) + rotl(v2, 7n) + rotl(v3, 12n) + rotl(v4, 18n)) & M64;171 for (const v of [v1, v2, v3, v4]) h = (((h ^ round(0n, v)) * P1) + P4) & M64;172 } else h = (seed + P5) & M64;173 h = (h + BigInt(input.length)) & M64;174 for (; o + 8 <= input.length; o += 8) h = ((rotl(h ^ round(0n, rd(input, o, 8)), 27n) * P1) + P4) & M64;175 if (o + 4 <= input.length) { h = ((rotl(h ^ ((rd(input, o, 4) * P1) & M64), 23n) * P2) + P3) & M64; o += 4; }176 for (; o < input.length; o++) h = (rotl(h ^ ((BigInt(input[o]!) * P5) & M64), 11n) * P1) & M64;177 h ^= h >> 33n; h = (h * P2) & M64; h ^= h >> 29n; h = (h * P3) & M64; h ^= h >> 32n;178 return h;179}180const le64 = (v: bigint): Uint8Array => { const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, v, true); return b; };181const hexOf = (b: Uint8Array) => Array.from(b, (v) => v.toString(16).padStart(2, '0')).join('');182/** twox128(text): the hash of pallet and storage item names in a storage key. */183export function twox128(text: string): Uint8Array {184 const b = new TextEncoder().encode(text), out = new Uint8Array(16);185 out.set(le64(xxhash64(b, 0n)), 0); out.set(le64(xxhash64(b, 1n)), 8);186 return out;187}188/** Storage key (0x hex) of a plain item, or of a map entry hashed with Blake2_128Concat. */189export function storageKey(pallet: string, item: string, blake2_128ConcatKey?: Uint8Array): string {190 return '0x' + hexOf(twox128(pallet)) + hexOf(twox128(item)) + (blake2_128ConcatKey ? hexOf(blake2b(blake2_128ConcatKey, { dkLen: 16 })) + hexOf(blake2_128ConcatKey) : '');191}192