mirror of
https://github.com/pezkuwichain/pezkuwi-api.git
synced 2026-11-02 22:51:07 +00:00
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:
1 parent
622f49e4a8
commit
ea8cb96cea
67 files changed
+4678
-6
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user