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.
This commit is contained in:
Satoshi Qazi Muhammed committed 2026-10-19 21:08:01 +04:00
1 parent 622f49e4a8
commit ea8cb96cea
67 files changed
+4678 -6

No files matched your search

+12 -1
View File
@@ -1,4 +1,15 @@
# @polkadot-api/metadata-builders
# @pezkuwi/metadata-builders
**Source:** `src/` is [`@polkadot-api/metadata-builders`](https://www.npmjs.com/package/@polkadot-api/metadata-builders/v/0.13.2) 0.13.2 from
[polkadot-api](https://github.com/polkadot-api/polkadot-api) (MIT), with imports of
`@polkadot-api/*` renamed to `@pezkuwi/*` (`substrate-bindings` → `bizinikiwi-bindings`). `dist/` is its build;
`scripts/check-papi-sources.mjs` fails CI unless the source embedded in `dist/` equals `src/`.
The upstream README follows.
---
## @polkadot-api/metadata-builders
This package has multiple functions that read a metadata object, denormalizes it, and builds other structures needed for different processes
@@ -0,0 +1,530 @@
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)),
}
}
@@ -0,0 +1,191 @@
import type { Codec, StringRecord } from "@pezkuwi/bizinikiwi-bindings"
import * as scale from "@pezkuwi/bizinikiwi-bindings"
import { mapObject } from "@pezkuwi/papi-utils"
import type { EnumVar, MetadataLookup } from "./lookups"
import { getLookupCodecBuilder } from "./lookup-codec-builder"
const nullCodec = scale.enhanceCodec(
scale._void,
() => undefined,
() => null,
)
export const getDynamicBuilder = (getLookupEntryDef: MetadataLookup) => {
const { metadata } = getLookupEntryDef
let buildDefinition = getLookupCodecBuilder(getLookupEntryDef)
const prefix = metadata.pallets
.find((x) => x.name === "System")
?.constants.find((x) => x.name === "SS58Prefix")
let ss58Prefix: number | undefined
if (prefix) {
try {
const prefixVal = buildDefinition(prefix.type).dec(prefix.value)
if (typeof prefixVal === "number") {
ss58Prefix = prefixVal
buildDefinition = getLookupCodecBuilder(
getLookupEntryDef,
scale.AccountId(prefixVal),
)
}
} catch (_) {}
}
const storagePallets = new Map<string, ReturnType<typeof scale.Storage>>()
const buildStorage = (pallet: string, entry: string) => {
let storagePallet = storagePallets.get(pallet)
if (!storagePallet)
storagePallets.set(pallet, (storagePallet = scale.Storage(pallet)))
const storageEntry = metadata.pallets
.find((x) => x.name === pallet)!
.storage!.items.find((s) => s.name === entry)!
// if val is `void` it decodes to `undefined`, making it impossible
// to differentiate from a non-existant key
// therefore, if the key exists => null, if it doesn't => undefined
const withNullVoid = (codec: Codec<any>) =>
codec === scale._void ? nullCodec : codec
const storageWithFallback = (
len: number,
value: Codec<any>,
...args: Parameters<ReturnType<typeof scale.Storage>>
) => {
const keys = storagePallet!(...args)
const [, ...encodersWithHash] = args
return {
args: scale.Tuple(...encodersWithHash.map(([codec]) => codec)),
keys,
value,
len,
fallback:
storageEntry.modifier === 1
? value.dec(storageEntry.fallback)
: undefined,
}
}
if (storageEntry.type.tag === "plain")
return storageWithFallback(
0,
withNullVoid(buildDefinition(storageEntry.type.value)),
entry,
)
const { key, value, hashers } = storageEntry.type.value
const val = withNullVoid(buildDefinition(value))
const hashes = hashers.map((x) => scale[x.tag])
const hashArgs: scale.EncoderWithHash<unknown>[] = (() => {
if (hashes.length === 1) {
return [[buildDefinition(key), hashes[0]]]
}
const keyDef = getLookupEntryDef(key)
switch (keyDef.type) {
case "array":
return hashes.map((hash) => [buildDefinition(keyDef.value.id), hash])
case "tuple":
return keyDef.value.map((x, idx) => [
buildDefinition(x.id),
hashes[idx],
])
default:
throw new Error("Invalid key type")
}
})()
return storageWithFallback(hashes.length, val, entry, ...hashArgs)
}
const buildEnumEntry = (
entry: EnumVar["value"][keyof EnumVar["value"]],
): Codec<any> => {
switch (entry.type) {
case "void":
return scale._void
case "lookupEntry":
return buildDefinition(entry.value.id)
case "tuple":
return scale.Tuple(
...Object.values(entry.value).map((l) => buildDefinition(l.id)),
)
case "struct":
return scale.Struct(
mapObject(entry.value, (x) => buildDefinition(x.id)) as StringRecord<
Codec<any>
>,
)
case "array":
return scale.Vector(buildDefinition(entry.value.id), entry.len)
}
}
const buildConstant = (pallet: string, constantName: string) => {
const storageEntry = metadata.pallets
.find((x) => x.name === pallet)!
.constants!.find((s) => s.name === constantName)!
return buildDefinition(storageEntry.type as number)
}
const buildVariant =
(type: "errors" | "events" | "calls") =>
(
pallet: string,
name: string,
): {
codec: Codec<any>
location: [number, number]
} => {
const palletEntry = metadata.pallets.find((x) => x.name === pallet)!
const lookup = getLookupEntryDef(palletEntry[type]!.type)
if (lookup.type !== "enum") throw null
const entry = lookup.value[name]
return {
location: [palletEntry.index, entry.idx],
codec: buildEnumEntry(lookup.value[name]),
}
}
const buildViewFn = (pallet: string, entry: string) => {
const fn = metadata.pallets
.find((x) => x.name === pallet)
?.viewFns.find((x) => x.name === entry)
if (!fn) throw null
return {
args: scale.Tuple(...fn.inputs.map((x) => buildDefinition(x.type))),
value: buildDefinition(fn.output),
}
}
const buildRuntimeCall = (api: string, method: string) => {
const entry = metadata.apis
.find((x) => x.name === api)
?.methods.find((x) => x.name === method)
if (!entry) throw null
return {
args: scale.Tuple(...entry.inputs.map((x) => buildDefinition(x.type))),
value: buildDefinition(entry.output),
}
}
return {
buildDefinition,
buildStorage,
buildEvent: buildVariant("events"),
buildError: buildVariant("errors"),
buildViewFn,
buildRuntimeCall,
buildCall: buildVariant("calls"),
buildConstant,
ss58Prefix,
}
}
@@ -0,0 +1,119 @@
import type { Codec, StringRecord } from "@pezkuwi/bizinikiwi-bindings"
import * as scale from "@pezkuwi/bizinikiwi-bindings"
import type { LookupEntry } from "./lookups"
import { withCache } from "./with-cache"
const _bytes = scale.Bin()
const _buildCodec = (
input: LookupEntry,
cache: Map<number, Codec<any>>,
stack: Set<number>,
_accountId: Codec<scale.SS58String>,
): Codec<any> => {
if (input.type === "primitive") return scale[input.value]
if (input.type === "void") return scale._void
if (input.type === "AccountId32") return _accountId
if (input.type === "AccountId20") return scale.ethAccount
if (input.type === "compact")
return input.isBig ? scale.compactBn : scale.compactNumber
if (input.type === "bitSequence") return scale.BitSeq(input.isLSB)
const buildNextCodec = (nextInput: LookupEntry): Codec<any> =>
buildCodec(nextInput, cache, stack, _accountId)
const buildVector = (inner: LookupEntry, len?: number) => {
const innerCodec = buildNextCodec(inner)
return len ? scale.Vector(innerCodec, len) : scale.Vector(innerCodec)
}
const buildTuple = (value: LookupEntry[]) =>
scale.Tuple(...value.map(buildNextCodec))
const buildStruct = (value: StringRecord<LookupEntry>) => {
const inner = Object.fromEntries(
Object.entries(value).map(([key, value]) => [key, buildNextCodec(value)]),
) as StringRecord<Codec<any>>
return scale.Struct(inner)
}
if (
input.type === "sequence" &&
input.value.type === "primitive" &&
input.value.value === "u8"
) {
return _bytes
}
if (input.type === "array") {
// Bytes case
if (input.value.type === "primitive" && input.value.value === "u8")
return scale.Bin(input.len)
return buildVector(input.value, input.len)
}
if (input.type === "sequence") return buildVector(input.value)
if (input.type === "tuple") return buildTuple(input.value)
if (input.type === "struct") return buildStruct(input.value)
if (input.type === "option") return scale.Option(buildNextCodec(input.value))
if (input.type === "result")
return scale.Result(
buildNextCodec(input.value.ok),
buildNextCodec(input.value.ko),
)
// it has to be an enum by now
const dependencies = Object.values(input.value).map((v) => {
switch (v.type) {
case "void":
return scale._void
case "lookupEntry":
return buildNextCodec(v.value)
case "tuple":
return buildTuple(v.value)
case "struct":
return buildStruct(v.value)
case "array":
return buildVector(v.value, v.len)
}
})
const inner = Object.fromEntries(
Object.keys(input.value).map((key, idx) => {
return [key, dependencies[idx]]
}),
) as StringRecord<Codec<any>>
const indexes = Object.values(input.value).map((x) => x.idx)
const areIndexesSorted = indexes.every((idx, i) => idx === i)
const variantCodec = areIndexesSorted
? scale.Variant(inner)
: scale.Variant(inner, indexes as any)
return input.byteLength
? fixedSizeCodec(variantCodec, input.byteLength)
: variantCodec
}
const buildCodec = withCache(_buildCodec, scale.Self, (res) => res)
export const getLookupCodecBuilder = (
lookup: (id: number) => LookupEntry,
accountId = scale.AccountId(),
) => {
const cache = new Map()
const buildDefinition = (id: number) =>
buildCodec(lookup(id), cache, new Set(), accountId)
return (id: number) => buildDefinition(id)
}
const fixedSizeCodec = <T>(codec: Codec<T>, size: number): Codec<T> => {
const allBytes = scale.Bytes(size)
return scale.createCodec<T>(
(value: T) => allBytes.enc(codec.enc(value)),
(data) => codec.dec(allBytes.dec(data)),
)
}
@@ -0,0 +1,214 @@
import { LookupEntry } from "./lookups"
export type LookupGraph = Map<
number,
{ entry: LookupEntry; backRefs: Set<number>; refs: Set<number> }
>
export function buildLookupGraph(
lookupFn: (id: number) => LookupEntry,
lookupLength: number,
): LookupGraph {
const result: LookupGraph = new Map()
const visited = new Set<number>()
const addEdge = (from: number, to: number) => {
if (!result.has(from))
result.set(from, {
entry: lookupFn(from),
backRefs: new Set(),
refs: new Set(),
})
if (!result.has(to))
result.set(to, {
entry: lookupFn(to),
backRefs: new Set(),
refs: new Set(),
})
result.get(from)!.refs.add(to)
result.get(to)!.backRefs.add(from)
}
for (let i = 0; i < lookupLength; i++) {
const entry = lookupFn(i)
if (i !== entry.id) {
// Lookup solved a pointer, but we still need to add that node into our
// graph in case someone access it from there.
addEdge(i, entry.id)
}
if (visited.has(entry.id)) continue
visited.add(entry.id)
switch (entry.type) {
case "array":
case "option":
case "sequence":
addEdge(entry.id, entry.value.id)
break
case "enum":
Object.values(entry.value).forEach((enumEntry) => {
switch (enumEntry.type) {
case "array":
case "lookupEntry":
addEdge(entry.id, enumEntry.value.id)
break
case "struct":
case "tuple":
Object.values(enumEntry.value).forEach((v) =>
addEdge(entry.id, v.id),
)
break
}
})
break
case "result":
addEdge(entry.id, entry.value.ok.id)
addEdge(entry.id, entry.value.ko.id)
break
case "struct":
case "tuple":
Object.values(entry.value).forEach((v) => addEdge(entry.id, v.id))
break
}
// It could be that this node is not being referenced by any other type
// nor it references anything. We still have to add it into the graph.
if (!result.has(entry.id)) {
result.set(entry.id, {
backRefs: new Set(),
refs: new Set(),
entry,
})
}
}
return result
}
const subgraphCache = new WeakMap<LookupGraph, Map<number, LookupGraph>>()
function _getSubgraph(
id: number,
graph: LookupGraph,
result: LookupGraph,
cache: Map<number, LookupGraph>,
) {
if (result.has(id)) return
const node = graph.get(id)!
result.set(id, node)
cache.set(id, result)
node.refs.forEach((ref) => _getSubgraph(ref, graph, result, cache))
node.backRefs.forEach((ref) => _getSubgraph(ref, graph, result, cache))
}
export function getSubgraph(id: number, graph: LookupGraph) {
if (!subgraphCache.has(graph)) {
subgraphCache.set(graph, new Map())
}
const cache = subgraphCache.get(graph)!
if (cache.has(id)) return cache.get(id)!
const result: LookupGraph = new Map()
_getSubgraph(id, graph, result, cache)
return result
}
export function getStronglyConnectedComponents(graph: LookupGraph) {
// Tarjan's strongly connected components
const tarjanState = new Map<
number,
{
index: number
lowLink: number
onStack: boolean
}
>()
let index = 0
const stack: number[] = []
const result: Array<Set<number>> = []
function strongConnect(v: number): void {
const state = {
index: index,
lowLink: index,
onStack: true,
}
tarjanState.set(v, state)
index++
stack.push(v)
const edges = graph.get(v)!.refs
for (let w of edges) {
const edgeState = tarjanState.get(w)
if (!edgeState) {
strongConnect(w)
state.lowLink = Math.min(state.lowLink, tarjanState.get(w)!.lowLink)
} else if (edgeState.onStack) {
state.lowLink = Math.min(state.lowLink, edgeState.index)
}
}
if (state.lowLink === state.index) {
const component = new Set<number>()
let poppedNode = -1
do {
poppedNode = stack.pop()!
tarjanState.get(poppedNode)!.onStack = false
component.add(poppedNode)
} while (poppedNode !== v)
result.push(component)
}
}
for (const node of graph.keys()) {
if (!tarjanState.has(node)) {
strongConnect(node)
}
}
return result
}
export function mergeSCCsWithCommonNodes(
stronglyConnectedComponents: Array<Set<number>>,
) {
/**
* For Nodes that are shared between two sets of SCCs, we need to calculate
* the checksum for the both of them, which wouldn't work (it would give
* different checksums).
* So we merge the SCCs that are using shared nodes into one group.
*/
const scc = stronglyConnectedComponents
const ungroupedCycles = new Set(scc.map((_, i) => i))
const edges = new Map(scc.map((_, i) => [i, new Set<number>()]))
scc.forEach((cycle, i) => {
scc.slice(i + 1).forEach((otherCycle, _j) => {
const j = _j + i + 1
const combined = new Set([...cycle, ...otherCycle])
if (combined.size !== cycle.size + otherCycle.size) {
edges.get(i)!.add(j)
edges.get(j)!.add(i)
}
})
})
const groups: Array<Set<number>> = []
while (ungroupedCycles.size) {
const group = new Set<number>()
const toVisit = [ungroupedCycles.values().next().value!]
while (toVisit.length) {
const idx = toVisit.pop()!
if (!ungroupedCycles.has(idx)) continue
ungroupedCycles.delete(idx)
const cycle = scc[idx]
cycle.forEach((v) => group.add(Number(v)))
edges.get(idx)!.forEach((n) => toVisit.push(n))
}
groups.push(group)
}
return groups
}
+456
View File
@@ -0,0 +1,456 @@
import type {
StringRecord,
UnifiedMetadata,
V14Lookup,
} from "@pezkuwi/bizinikiwi-bindings"
export type SignedPrimitive = "i8" | "i16" | "i32" | "i64" | "i128" | "i256"
export type UnsignedPrimitive = "u8" | "u16" | "u32" | "u64" | "u128" | "u256"
export type MetadataPrimitives =
| "bool"
| "char"
| "str"
| SignedPrimitive
| UnsignedPrimitive
export type PrimitiveVar = {
type: "primitive"
value: MetadataPrimitives
}
export type VoidVar = { type: "void" }
export type CompactVar = {
type: "compact"
isBig: boolean
size: UnsignedPrimitive
}
export type BitSequenceVar = {
type: "bitSequence"
isLSB: boolean
}
export type AccountId32 = { type: "AccountId32" }
export type AccountId20 = { type: "AccountId20" }
export type TerminalVar =
| PrimitiveVar
| VoidVar
| CompactVar
| BitSequenceVar
| AccountId32
| AccountId20
/* Array-like vars:
* - TupleVar: Mixed types, fixed length
* - Sequence: One type, arbitrary length
* - Array: One type, fixed length
*/
export type TupleVar = {
type: "tuple"
value: LookupEntry[]
innerDocs: Array<string[]>
}
export type StructVar = {
type: "struct"
value: StringRecord<LookupEntry>
innerDocs: StringRecord<string[]>
}
export type EnumVar = {
type: "enum"
value: StringRecord<
(
| { type: "lookupEntry"; value: LookupEntry }
| VoidVar
| TupleVar
| StructVar
| ArrayVar
) & { idx: number }
>
innerDocs: StringRecord<string[]>
byteLength?: number
}
export type OptionVar = {
type: "option"
value: LookupEntry
}
export type ResultVar = {
type: "result"
value: { ok: LookupEntry; ko: LookupEntry }
}
export type SequenceVar = {
type: "sequence"
value: LookupEntry
}
export type ArrayVar = {
type: "array"
value: LookupEntry
len: number
}
export type ComposedVar =
| TupleVar
| StructVar
| SequenceVar
| ArrayVar
| OptionVar
| ResultVar
| EnumVar
export type Var = TerminalVar | ComposedVar
export type LookupEntry = {
id: number
} & Var
const isBytes = (value: LookupEntry, nBytes: number) =>
value.type === "array" &&
value.len === nBytes &&
value.value.type === "primitive" &&
value.value.value === "u8"
const _void: VoidVar = { type: "void" }
export interface MetadataLookup {
(id: number): LookupEntry
metadata: UnifiedMetadata
call: number | null
}
const _denormalizeLookup = (
lookupData: V14Lookup,
customMap: (value: V14Lookup[number]) => Var | null = () => null,
): ((id: number) => LookupEntry) => {
const lookups = new Map<number, LookupEntry>()
const from = new Set<number>()
const withCache = (
fn: (id: number) => Var,
): ((id: number) => LookupEntry) => {
return (id) => {
let entry = lookups.get(id)
if (entry) return entry
if (from.has(id)) {
const entry = {
id,
} as LookupEntry
lookups.set(id, entry)
return entry
}
from.add(id)
const value = fn(id)
entry = lookups.get(id)
if (entry) {
Object.assign(entry, value)
} else {
entry = {
id,
...value,
}
lookups.set(id, entry!)
}
from.delete(id)
return entry
}
}
let isAccountId32SearchOn = true
let isAccountId20SearchOn = true
const getLookupEntryDef = withCache((id): Var => {
const custom = customMap(lookupData[id])
if (custom) return custom
const { def, path, params } = lookupData[id]
if (def.tag === "composite") {
if (def.value.length === 0) return _void
// used to be a "pointer"
if (def.value.length === 1) {
const inner = getLookupEntryDef(def.value[0].type as number)
if (
isAccountId32SearchOn &&
path.at(-1) === "AccountId32" &&
isBytes(inner, 32)
) {
isAccountId32SearchOn = false
return { type: "AccountId32" }
}
if (
isAccountId20SearchOn &&
path.at(-1) === "AccountId20" &&
isBytes(inner, 20)
) {
isAccountId20SearchOn = false
return { type: "AccountId20" }
}
return inner
}
return getComplexVar(def.value)
}
if (def.tag === "variant") {
if (
path.length === 1 &&
path[0] === "Option" &&
params.length === 1 &&
params[0].name === "T"
) {
const value = getLookupEntryDef(params[0].type!)
return value.type === "void"
? // Option<void> would return a Codec<undefined> which makes no sense
// Therefore, we better treat it as a bool
{ type: "primitive", value: "bool" }
: {
type: "option",
value,
}
}
if (
path.length === 1 &&
path[0] === "Result" &&
params.length === 2 &&
params[0].name === "T" &&
params[1].name === "E"
) {
return {
type: "result",
value: {
ok: getLookupEntryDef(params[0].type as number),
ko: getLookupEntryDef(params[1].type as number),
},
}
}
if (def.value.length === 0) return _void
const enumValue: StringRecord<EnumVar["value"][keyof EnumVar["value"]]> =
{}
const enumDocs: StringRecord<string[]> = {}
def.value.forEach((x) => {
const key = x.name
enumDocs[key] = x.docs
if (x.fields.length === 0) {
enumValue[key] = { ..._void, idx: x.index }
return
}
if (x.fields.length === 1 && !x.fields[0].name) {
enumValue[key] = {
type: "lookupEntry",
value: getLookupEntryDef(x.fields[0].type),
idx: x.index,
}
return
}
enumValue[key] = { ...getComplexVar(x.fields), idx: x.index }
})
return {
type: "enum",
value: enumValue,
innerDocs: enumDocs,
}
}
if (def.tag === "sequence")
return {
type: "sequence",
value: getLookupEntryDef(def.value as number),
}
if (def.tag === "array") {
const { len } = def.value
const value = getLookupEntryDef(def.value.type)
return !len || value.type === "void"
? _void
: len > 1
? {
type: "array",
value,
len: def.value.len,
}
: value
}
if (def.tag === "tuple") {
if (def.value.length === 0) return _void
return def.value.length > 1
? getArrayOrTuple(
def.value.map((x) => getLookupEntryDef(x as number)),
def.value.map((x) => lookupData[x].docs),
)
: getLookupEntryDef(def.value[0] as number) // use to be a "pointer"
}
if (def.tag === "primitive") {
return {
type: "primitive",
value: def.value.tag,
}
}
if (def.tag === "compact") {
const translated = getLookupEntryDef(def.value) as PrimitiveVar | VoidVar
if (translated.type === "void") return _void
const isBig = Number(translated.value.slice(1)) > 32
return {
type: "compact",
isBig,
size: translated.value as UnsignedPrimitive,
}
}
// bitSequence
return {
type: def.tag,
isLSB: (lookupData[def.value.bitOrderType].path.at(-1) ?? "LSB")
.toUpperCase()
.startsWith("LSB"),
}
})
const getComplexVar = (
input: Array<{ type: number; name?: string; docs: string[] }>,
): TupleVar | StructVar | ArrayVar | VoidVar => {
let allKey = true
const values: Record<string | number, LookupEntry> = {}
const innerDocs: Record<string | number, string[]> = {}
input.forEach((x, idx) => {
allKey = allKey && !!x.name
const key = x.name || idx
const value = getLookupEntryDef(x.type as number)
if (value.type !== "void") {
values[key] = value
innerDocs[key] = x.docs
}
})
return allKey
? {
type: "struct",
value: values as StringRecord<LookupEntry>,
innerDocs: innerDocs as StringRecord<string[]>,
}
: getArrayOrTuple(Object.values(values), Object.values(innerDocs))
}
const getArrayOrTuple = (
values: Array<LookupEntry>,
innerDocs: Array<string[]>,
): TupleVar | ArrayVar | VoidVar => {
if (
values.every((v) => v.id === values[0].id) &&
innerDocs.every((doc) => !doc.length)
) {
const [value] = values
return value.type === "void"
? _void
: {
type: "array",
value: values[0],
len: values.length,
}
}
return {
type: "tuple",
value: values,
innerDocs: innerDocs,
}
}
return getLookupEntryDef
}
export const denormalizeLookup = (lookupData: V14Lookup) =>
_denormalizeLookup(lookupData)
export const getLookupFn = (metadata: UnifiedMetadata): MetadataLookup => {
const getLookupEntryDef = _denormalizeLookup(metadata.lookup, ({ def }) => {
if (def.tag === "composite") {
const moduleErrorLength = getModuleErrorLength(def)
if (moduleErrorLength) {
return {
type: "enum",
innerDocs: {},
value: Object.fromEntries(
metadata.pallets.map((p) => [
p.name,
p.errors == null
? { ..._void, idx: p.index }
: {
type: "lookupEntry" as const,
value: getLookupEntryDef(p.errors.type),
idx: p.index,
},
]),
) as StringRecord<
(
| VoidVar
| {
type: "lookupEntry"
value: LookupEntry
}
) & { idx: number }
>,
byteLength: moduleErrorLength,
}
}
}
return null
})
function getModuleErrorLength(def: {
tag: "composite"
value: {
name: string | undefined
type: number
typeName: string | undefined
docs: string[]
}[]
}) {
const preChecks =
def.value.length === 2 &&
def.value[0].name === "index" &&
def.value[1].name === "error"
if (!preChecks) return null
const index = getLookupEntryDef(def.value[0].type)
const error = getLookupEntryDef(def.value[1].type)
return index.type === "primitive" &&
index.value === "u8" &&
error.type === "array" &&
error.value.type === "primitive" &&
error.value.value === "u8"
? 1 + error.len
: null
}
const getCall = () => {
if ("call" in metadata.extrinsic) {
return metadata.extrinsic.call
}
const extrinsic = metadata.lookup[metadata.extrinsic.type]
const call = extrinsic?.params.find((p) => p.name === "Call")
return call?.type ?? null
}
return Object.assign(getLookupEntryDef, { metadata, call: getCall() })
}
@@ -0,0 +1,44 @@
import { LookupEntry } from "./lookups"
type FnWithStack<Other extends Array<any>, T> = (
input: LookupEntry,
cache: Map<number, T>,
stack: Set<number>,
...rest: Other
) => T
export const withCache =
<Other extends Array<any>, T>(
fn: FnWithStack<Other, T>,
onEnterCircular: (
cacheGetter: () => T,
circular: LookupEntry,
...rest: Other
) => T,
onExitCircular: (
outter: T,
inner: T,
circular: LookupEntry,
...rest: Other
) => T,
): FnWithStack<Other, T> =>
(input, cache, stack, ...rest) => {
const { id } = input
if (cache.has(id)) return cache.get(id)!
if (stack.has(id)) {
const res = onEnterCircular(() => cache.get(id)!, input, ...rest)
cache.set(id, res)
return res
}
stack.add(id)
let result = fn(input, cache, stack, ...rest)
stack.delete(id)
if (cache.has(id))
result = onExitCircular(result, cache.get(id)!, input, ...rest)
cache.set(id, result)
return result
}