Files
pezkuwi-api/packages/metadata-builders/upstream-src/checksum-builder.ts
T
SatoshiQaziMuhammed e3c72817fb chore(papi): commit the sources behind the four polkadot-api packages (#15)
* chore(papi): commit the sources behind the four polkadot-api packages

papi-utils, bizinikiwi-bindings, metadata-builders and merkleize-metadata
held only dist/ builds, and no repository held their source. Every
source map in dist/ embeds the TypeScript it was compiled from; that is
now committed as src/ (59 files), and it matches upstream polkadot-api
(MIT) exactly once @polkadot-api/* imports are renamed:

  papi-utils           @polkadot-api/utils 0.2.0
  bizinikiwi-bindings  @polkadot-api/substrate-bindings 0.16.6
  metadata-builders    @polkadot-api/metadata-builders 0.13.0-0.13.2
  merkleize-metadata   @polkadot-api/merkleize-metadata 1.1.21-1.1.23

scripts/check-papi-sources.mjs, run in the lint job, fails unless the
source embedded in dist/ is exactly the committed src/. tsconfig.json
excludes these src/ trees; ESLint already skipped the packages. Each
README now states the upstream package and version.

* chore(papi): keep the upstream sources in upstream-src/

The pezkuwi-dev build treats any package with a src/ directory as its
own: it requires our license header on every file and builds it. These
are upstream files that must stay byte-identical to the source embedded
in dist/, so they live in upstream-src/ instead, which neither the build
nor the root tsconfig picks up.
2026-10-19 22:50:59 +04:00

531 lines
15 KiB
TypeScript

import type { StringRecord } from "@pezkuwi/bizinikiwi-bindings"
import { h64 } from "@pezkuwi/bizinikiwi-bindings"
import {
LookupGraph,
buildLookupGraph,
getStronglyConnectedComponents,
getSubgraph,
mergeSCCsWithCommonNodes,
} from "./lookup-graph"
import {
ArrayVar,
LookupEntry,
MetadataLookup,
MetadataPrimitives,
StructVar,
TupleVar,
VoidVar,
} from "./lookups"
const textEncoder = new TextEncoder()
const encodeText = textEncoder.encode.bind(textEncoder)
const getChecksum = (values: Array<bigint>) => {
const res = new Uint8Array(values.length * 8)
const dv = new DataView(res.buffer)
for (let i = 0; i < values.length; i++) dv.setBigUint64(i * 8, values[i])
return h64(res)
}
const getStringChecksum = (values: Array<string>) =>
getChecksum(values.map((v) => h64(encodeText(v))))
type Shape =
| "primitive"
| "void"
| "vector"
| "tuple"
| "struct"
| "option"
| "result"
| "enum"
const shapeIds: Record<Shape, bigint> = {
primitive: 0n,
vector: 1n,
tuple: 2n,
struct: 3n,
option: 4n,
result: 5n,
enum: 6n,
void: 7n,
}
type RuntimePrimitives =
| "undefined"
| "number"
| "string"
| "bigint"
| "boolean"
| "bitSequence"
| "byteSequence"
| "accountId32"
| "accountId20"
const runtimePrimitiveIds: Record<RuntimePrimitives, bigint> = {
undefined: 0n,
number: 1n,
string: 2n,
bigint: 3n,
boolean: 4n,
bitSequence: 5n, // {bitsLen: number, bytes: Uint8Array}
byteSequence: 6n, // Binary
accountId32: 7n, // SS58String
accountId20: 8n, // EthAccount
}
const metadataPrimitiveIds: Record<MetadataPrimitives, bigint> = {
bool: runtimePrimitiveIds.boolean,
char: runtimePrimitiveIds.string,
str: runtimePrimitiveIds.string,
u8: runtimePrimitiveIds.number,
u16: runtimePrimitiveIds.number,
u32: runtimePrimitiveIds.number,
u64: runtimePrimitiveIds.bigint,
u128: runtimePrimitiveIds.bigint,
u256: runtimePrimitiveIds.bigint,
i8: runtimePrimitiveIds.number,
i16: runtimePrimitiveIds.number,
i32: runtimePrimitiveIds.number,
i64: runtimePrimitiveIds.bigint,
i128: runtimePrimitiveIds.bigint,
i256: runtimePrimitiveIds.bigint,
}
const structLikeBuilder = <T>(
shapeId: bigint,
input: StringRecord<T>,
innerChecksum: (value: T) => bigint,
) => {
const sortedEntries = Object.entries(input).sort(([a], [b]) =>
a.localeCompare(b),
)
const keysChecksum = getStringChecksum(sortedEntries.map(([key]) => key))
const valuesChecksum = getChecksum(
sortedEntries.map(([, entry]) => innerChecksum(entry)),
)
return getChecksum([shapeId, keysChecksum, valuesChecksum])
}
const _buildChecksum = (
input: LookupEntry,
buildNextChecksum: (entry: LookupEntry) => bigint,
): bigint => {
if (input.type === "primitive")
return getChecksum([shapeIds.primitive, metadataPrimitiveIds[input.value]])
if (input.type === "void") return getChecksum([shapeIds.void])
if (input.type === "compact")
return getChecksum([
shapeIds.primitive,
runtimePrimitiveIds[input.isBig ? "bigint" : "number"],
])
if (input.type === "bitSequence")
return getChecksum([shapeIds.primitive, runtimePrimitiveIds.bitSequence])
if (input.type === "AccountId32") {
return getChecksum([shapeIds.primitive, runtimePrimitiveIds.accountId32])
}
if (input.type === "AccountId20") {
return getChecksum([shapeIds.primitive, runtimePrimitiveIds.accountId20])
}
const buildVector = (entry: LookupEntry, length?: number) => {
const innerChecksum = buildNextChecksum(entry)
return getChecksum(
length !== undefined
? [shapeIds.vector, innerChecksum, BigInt(length)]
: [shapeIds.vector, innerChecksum],
)
}
if (input.type === "array") {
const innerValue = input.value
if (innerValue.type === "primitive" && innerValue.value === "u8") {
return getChecksum([
shapeIds.primitive,
runtimePrimitiveIds.byteSequence,
BigInt(input.len),
])
}
return buildVector(innerValue, input.len)
}
if (input.type === "sequence") {
const innerValue = input.value
if (innerValue.type === "primitive" && innerValue.value === "u8") {
return getChecksum([shapeIds.primitive, runtimePrimitiveIds.byteSequence])
}
return buildVector(innerValue)
}
const buildTuple = (entries: LookupEntry[]) =>
getChecksum([shapeIds.tuple, ...entries.map(buildNextChecksum)])
const buildStruct = (entries: StringRecord<LookupEntry>) =>
structLikeBuilder(shapeIds.struct, entries, buildNextChecksum)
if (input.type === "tuple") return buildTuple(input.value)
if (input.type === "struct") return buildStruct(input.value)
if (input.type === "option")
return getChecksum([shapeIds.option, buildNextChecksum(input.value)])
if (input.type === "result")
return getChecksum([
shapeIds.result,
buildNextChecksum(input.value.ok),
buildNextChecksum(input.value.ko),
])
return structLikeBuilder(shapeIds.enum, input.value, (entry) => {
if (entry.type === "lookupEntry") return buildNextChecksum(entry.value)
switch (entry.type) {
case "void":
return getChecksum([shapeIds.void])
case "tuple":
return buildTuple(entry.value)
case "struct":
return buildStruct(entry.value)
case "array":
return buildVector(entry.value, entry.len)
}
})
}
const sortCyclicGroups = (groups: Array<Set<number>>, graph: LookupGraph) => {
const getReachableNodes = (group: Set<number>) => {
const result = new Set<number>()
const toVisit = Array.from(group)
while (toVisit.length) {
const id = toVisit.pop()!
if (result.has(id)) continue
result.add(id)
graph.get(id)?.refs.forEach((id) => toVisit.push(id))
}
return Array.from(result)
}
const result: Array<Set<number>> = new Array()
function dependentsFirst(group: Set<number>) {
if (result.includes(group)) return
const dependents = groups.filter(
(candidate) =>
candidate !== group &&
getReachableNodes(group).some((node) => candidate.has(node)),
)
dependents.forEach((group) => dependentsFirst(group))
if (result.includes(group)) return
result.push(group)
}
groups.forEach((group) => dependentsFirst(group))
return result
}
function iterateChecksums(
group: Set<number>,
iterations: number,
cache: Map<number, bigint>,
graph: LookupGraph,
) {
// Keep the values that are getting changed on each iteration in a separate
// cache, because two nodes referencing the same one should read the same
// previous iteration checksum for that node.
const groupReadCache = new Map([...group].map((id) => [id, 0n]))
const groupWriteCache = new Map<number, bigint>()
const recursiveBuildChecksum = (
entry: LookupEntry,
// The first call has to skip the cache, otherwise it would return the
// previous iteration result.
skipCache = true,
): bigint => {
if (!skipCache && (groupReadCache.has(entry.id) || cache.has(entry.id))) {
return groupReadCache.get(entry.id) ?? cache.get(entry.id)!
}
const result = _buildChecksum(entry, (nextEntry) =>
recursiveBuildChecksum(nextEntry, false),
)
if (group.has(entry.id)) {
groupWriteCache.set(entry.id, result)
} else {
cache.set(entry.id, result)
}
return result
}
for (let i = 0; i < iterations; i++) {
group.forEach((id) => recursiveBuildChecksum(graph.get(id)!.entry))
group.forEach((id) => groupReadCache.set(id, groupWriteCache.get(id)!))
}
return groupReadCache
}
function getMirroredNodes(
cyclicGroups: Array<Set<number>>,
graph: LookupGraph,
) {
const maxSize = cyclicGroups.reduce(
(acc, group) => Math.max(acc, group.size),
0,
)
const allEntries = new Set([...graph.values()].map((v) => v.entry.id))
const resultingChecksums = iterateChecksums(
allEntries,
maxSize,
// Cache won't be used, since it's using the internal one for every node.
new Map(),
graph,
)
const checksumToNodes = new Map<bigint, number[]>()
for (const id of allEntries) {
const checksum = resultingChecksums.get(id)
if (checksum == undefined) throw new Error("Unreachable")
if (!checksumToNodes.has(checksum)) {
checksumToNodes.set(checksum, [])
}
checksumToNodes.get(checksum)!.push(id)
}
const checksumsWithDuplicates = [...checksumToNodes.entries()].filter(
([, nodes]) => nodes.length > 1,
)
const duplicatesMap: Record<number, number[]> = {}
checksumsWithDuplicates.forEach(([, nodes]) => {
nodes.forEach((n) => (duplicatesMap[n] = nodes))
})
return duplicatesMap
}
const buildChecksum = (
entry: LookupEntry,
cache: Map<number, bigint>,
graph: LookupGraph,
) => {
if (cache.has(entry.id)) return cache.get(entry.id)!
const subGraph = getSubgraph(entry.id, graph)
const cycles = getStronglyConnectedComponents(subGraph).filter(
// SCCs can be of length=1, but for those we're only interested with those that are circular with themselves
(group) => group.size > 1 || isSelfCircular(group, subGraph),
)
const cyclicGroups = mergeSCCsWithCommonNodes(cycles).filter((group) => {
// Exclude groups that were previously calculated
return !cache.has(group.values().next().value!)
})
const mirrored = getMirroredNodes(cyclicGroups, subGraph)
const sortedCyclicGroups = sortCyclicGroups(cyclicGroups, subGraph)
sortedCyclicGroups.forEach((group) => {
if (cache.has(group.values().next().value!)) {
// exclude mirrored groups
return
}
const result = iterateChecksums(group, group.size, cache, graph)
group.forEach((id) => {
const checksum = result.get(id)!
if (id in mirrored) {
mirrored[id].forEach((id) => cache.set(id, checksum))
} else {
cache.set(id, checksum)
}
})
})
const getChecksum = (entry: LookupEntry) => {
if (cache.has(entry.id)) return cache.get(entry.id)!
return _buildChecksum(entry, getChecksum)
}
return getChecksum(entry)
}
const isSelfCircular = (group: Set<number>, graph: LookupGraph) => {
if (group.size !== 1) return false
const [id] = group
return graph.get(id)!.refs.has(id)
}
export const getChecksumBuilder = (getLookupEntryDef: MetadataLookup) => {
const { metadata } = getLookupEntryDef
const graph = buildLookupGraph(getLookupEntryDef, metadata.lookup.length)
const cache = new Map<number, bigint>()
const buildDefinition = (id: number): bigint =>
buildChecksum(getLookupEntryDef(id), cache, graph)
const buildStorage = (pallet: string, entry: string): bigint | null => {
try {
const storageEntry = metadata.pallets
.find((x) => x.name === pallet)!
.storage!.items.find((s) => s.name === entry)!
if (storageEntry.type.tag === "plain")
return buildDefinition(storageEntry.type.value)
const { key, value } = storageEntry.type.value
const val = buildDefinition(value)
const returnKey = buildDefinition(key)
return getChecksum([val, returnKey])
} catch (_) {
return null
}
}
const buildViewFns = (pallet: string, entry: string): bigint | null => {
try {
const viewFn = metadata.pallets
.find((x) => x.name === pallet)
?.viewFns.find((x) => x.name === entry)
if (!viewFn) throw null
const argNamesChecksum = getStringChecksum(
viewFn.inputs.map((x) => x.name),
)
const argValuesChecksum = getChecksum(
viewFn.inputs.map((x) => buildDefinition(x.type)),
)
const outputChecksum = buildDefinition(viewFn.output)
return getChecksum([argNamesChecksum, argValuesChecksum, outputChecksum])
} catch (_) {
return null
}
}
const buildRuntimeCall = (api: string, method: string): bigint | null => {
try {
const entry = metadata.apis
.find((x) => x.name === api)
?.methods.find((x) => x.name === method)
if (!entry) throw null
const argNamesChecksum = getStringChecksum(
entry.inputs.map((x) => x.name),
)
const argValuesChecksum = getChecksum(
entry.inputs.map((x) => buildDefinition(x.type)),
)
const outputChecksum = buildDefinition(entry.output)
return getChecksum([argNamesChecksum, argValuesChecksum, outputChecksum])
} catch (_) {
return null
}
}
const buildComposite = (
input: TupleVar | StructVar | VoidVar | ArrayVar,
): bigint => {
if (input.type === "void") return getChecksum([0n])
if (input.type === "tuple") {
const values = Object.values(input.value).map((entry) =>
buildDefinition(entry.id),
)
return getChecksum([shapeIds.tuple, ...values])
}
if (input.type === "array") {
return getChecksum([
shapeIds.vector,
buildDefinition(input.value.id),
BigInt(input.len),
])
}
// Otherwise struct
return structLikeBuilder(shapeIds.struct, input.value, (entry) =>
buildDefinition(entry.id),
)
}
const buildNamedTuple = (input: StructVar): bigint => {
return structLikeBuilder(shapeIds.tuple, input.value, (entry) =>
buildDefinition(entry.id),
)
}
const variantShapeId = {
errors: 1n,
events: 2n,
calls: 3n,
}
const buildVariant =
(variantType: "errors" | "events" | "calls") =>
(pallet: string, name: string): bigint | null => {
try {
const palletEntry = metadata.pallets.find((x) => x.name === pallet)!
const enumLookup = getLookupEntryDef(palletEntry[variantType]!.type)
buildDefinition(enumLookup.id)
if (enumLookup.type !== "enum") throw null
const entry = enumLookup.value[name]
const valueChecksum =
entry.type === "lookupEntry"
? buildDefinition(entry.value.id)
: buildComposite(entry)
return getChecksum([variantShapeId[variantType], valueChecksum])
} catch (_) {
return null
}
}
const buildConstant = (
pallet: string,
constantName: string,
): bigint | null => {
try {
const storageEntry = metadata.pallets
.find((x) => x.name === pallet)!
.constants!.find((s) => s.name === constantName)!
return buildDefinition(storageEntry.type)
} catch (_) {
return null
}
}
const toStringEnhancer =
<Args extends Array<any>>(
fn: (...args: Args) => bigint | null,
): ((...args: Args) => string | null) =>
(...args) =>
fn(...args)?.toString(32) ?? null
return {
buildDefinition: toStringEnhancer(buildDefinition),
buildRuntimeCall: toStringEnhancer(buildRuntimeCall),
buildStorage: toStringEnhancer(buildStorage),
buildViewFns: toStringEnhancer(buildViewFns),
buildCall: toStringEnhancer(buildVariant("calls")),
buildEvent: toStringEnhancer(buildVariant("events")),
buildError: toStringEnhancer(buildVariant("errors")),
buildConstant: toStringEnhancer(buildConstant),
buildComposite: toStringEnhancer(buildComposite),
buildNamedTuple: toStringEnhancer(buildNamedTuple),
getAllGeneratedChecksums: () =>
Array.from(cache.values()).map((v) => v.toString(32)),
}
}