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.
This commit is contained in:
SatoshiQaziMuhammed authored and GitHub committed 2026-10-19 22:50:59 +04:00
1 parent 622f49e4a8
commit e3c72817fb
66 files changed
+4674 -5

No files matched your search

+5
View File
@@ -31,3 +31,8 @@ jobs:
- name: check built manifests
if: matrix.step == 'build'
run: python3 scripts/check-built-manifests.py .
# The four polkadot-api packages ship committed dist/ builds; their source
# maps must embed exactly the src/ committed beside them.
- name: check papi sources
if: matrix.step == 'lint'
run: node scripts/check-papi-sources.mjs
+2 -1
View File
@@ -9,7 +9,8 @@ export default [
// see the tsconfig.eslint.json for explanation
'packages/api-augment/src/dicle/*.ts',
'packages/api-augment/src/pezkuwi/*.ts',
// compiled packages without sources here; there is nothing to lint
// upstream polkadot-api packages (src/ + committed dist/), kept in upstream's style;
// scripts/check-papi-sources.mjs ties dist/ to src/
'packages/bizinikiwi-bindings/**',
'packages/merkleize-metadata/**',
'packages/metadata-builders/**',
+12 -1
View File
@@ -1 +1,12 @@
# @polkadot-api/substrate-bindings
# @pezkuwi/bizinikiwi-bindings
**Source:** `upstream-src/` is [`@polkadot-api/substrate-bindings`](https://www.npmjs.com/package/@polkadot-api/substrate-bindings/v/0.16.6) 0.16.6 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 `upstream-src/`.
The upstream README follows.
---
## @polkadot-api/substrate-bindings
@@ -0,0 +1,47 @@
import {
Bytes,
CodecType,
Struct,
Vector,
_void,
enhanceCodec,
Hex,
Variant,
compactNumber,
} from "./scale"
const textEncoder = new TextEncoder()
const textDecoder = new TextDecoder()
const fourChars = enhanceCodec(
Bytes(4),
textEncoder.encode.bind(textEncoder),
textDecoder.decode.bind(textDecoder),
)
const diggestVal = Struct({
engine: fourChars,
payload: Hex(),
})
const diggest = Variant(
{
other: Bytes(),
consensus: diggestVal,
seal: diggestVal,
preRuntime: diggestVal,
runtimeUpdated: _void,
},
[0, 4, 5, 6, 8],
)
const hex32 = Hex(32)
export const blockHeader = Struct({
parentHash: hex32,
number: compactNumber,
stateRoot: hex32,
extrinsicRoot: hex32,
digests: Vector(diggest),
})
export type BlockHeader = CodecType<typeof blockHeader>
@@ -0,0 +1,26 @@
import { _void, Enum, Option, str, Struct, u8, Vector } from "scale-ts"
export const itemDeprecation = Enum({
NotDeprecated: _void,
DeprecatedWithoutNote: _void,
Deprecated: Struct({
note: str,
since: Option(str),
}),
})
export const variantDeprecation = Vector(
Struct({
index: u8,
deprecation: Enum(
{
DeprecatedWithoutNote: _void,
Deprecated: Struct({
note: str,
since: Option(str),
}),
},
[1, 2],
),
}),
)
@@ -0,0 +1,3 @@
import { Vector, str } from "scale-ts"
export const docs = Vector(str)
@@ -0,0 +1,89 @@
import { compactNumber } from "../scale/compact"
import {
CodecType,
Enum,
Option,
Struct,
Vector,
_void,
str,
u32,
u8,
} from "scale-ts"
import { docs } from "./docs"
const oStr = Option(str)
const primitive = Enum({
bool: _void,
char: _void,
str: _void,
u8: _void,
u16: _void,
u32: _void,
u64: _void,
u128: _void,
u256: _void,
i8: _void,
i16: _void,
i32: _void,
i64: _void,
i128: _void,
i256: _void,
})
const fields = Vector(
Struct({
name: oStr,
type: compactNumber,
typeName: oStr,
docs,
}),
)
const arr = Struct({
len: u32,
type: compactNumber,
})
const bitSequence = Struct({
bitStoreType: compactNumber,
bitOrderType: compactNumber,
})
const variant = Vector(
Struct({
name: str,
fields,
index: u8,
docs,
}),
)
const def = Enum({
composite: fields,
variant,
sequence: compactNumber,
array: arr,
tuple: Vector(compactNumber),
primitive,
compact: compactNumber,
bitSequence,
})
const param = Struct({
name: str,
type: Option(compactNumber),
})
const params = Vector(param)
const entry = Struct({
id: compactNumber,
path: docs,
params,
def,
docs,
})
export const lookup = Vector(entry)
export type V14Lookup = CodecType<typeof lookup>
@@ -0,0 +1,78 @@
import {
Bytes,
type Codec,
type CodecType,
Enum,
Option,
Struct,
Tuple,
compact,
createCodec,
u32,
} from "scale-ts"
import type { HexString } from "../scale"
import { v14 } from "./v14"
import { v15 } from "./v15"
import { v16 } from "./v16"
const unsupportedFn = () => {
throw new Error("Unsupported metadata version!")
}
const unsupported = createCodec(
unsupportedFn,
unsupportedFn,
) as unknown as Codec<unknown>
export const metadata = Struct({
magicNumber: u32,
metadata: Enum({
v0: unsupported,
v1: unsupported,
v2: unsupported,
v3: unsupported,
v4: unsupported,
v5: unsupported,
v6: unsupported,
v7: unsupported,
v8: unsupported,
v9: unsupported,
v10: unsupported,
v11: unsupported,
v12: unsupported,
v13: unsupported,
v14,
v15,
v16,
}),
})
export type Metadata = CodecType<typeof metadata>
const opaqueBytes = Bytes()
const optionOpaque = Option(opaqueBytes)
const opaqueOpaqueBytes = Tuple(compact, opaqueBytes)
export const decAnyMetadata = (
input: Uint8Array | HexString,
): CodecType<typeof metadata> => {
try {
return metadata.dec(input)
} catch (_) {}
// comes from metadata.metadata_at_version
try {
return metadata.dec(optionOpaque.dec(input)!)
} catch (_) {}
// comes from state.getMetadata
try {
return metadata.dec(opaqueBytes.dec(input))
} catch (_) {}
// comes from metadata.metadata
try {
return metadata.dec(opaqueOpaqueBytes.dec(input)[1])
} catch (_) {}
throw null
}
@@ -0,0 +1,105 @@
import { Hex, compactNumber } from "@/codecs/scale"
import { Struct, Option, Vector, u8, str, Enum, _void } from "scale-ts"
import { docs } from "./docs"
import { itemDeprecation, variantDeprecation } from "./deprecation"
import { viewFunction } from "./runtime-api"
const hashType = Enum({
Blake2128: _void,
Blake2256: _void,
Blake2128Concat: _void,
Twox128: _void,
Twox256: _void,
Twox64Concat: _void,
Identity: _void,
})
const hashers = Vector(hashType)
export const storageMap = Struct({
hashers,
key: compactNumber,
value: compactNumber,
})
const storageItem = {
name: str,
modifier: u8,
type: Enum({
plain: compactNumber,
map: storageMap,
}),
fallback: Hex(),
docs,
}
const enumRefV14 = Option(compactNumber)
export const v14Pallet = {
name: str,
storage: Option(
Struct({
prefix: str,
items: Vector(Struct(storageItem)),
}),
),
calls: enumRefV14,
events: enumRefV14,
constants: Vector(
Struct({
name: str,
type: compactNumber,
value: Hex(),
docs,
}),
),
errors: enumRefV14,
index: u8,
}
export const v15Pallet = {
...v14Pallet,
docs,
}
const enumRef = Option(
Struct({ type: compactNumber, deprecationInfo: variantDeprecation }),
)
export const v16Pallet = {
name: str,
storage: Option(
Struct({
prefix: str,
items: Vector(
Struct({
...storageItem,
deprecationInfo: itemDeprecation,
}),
),
}),
),
calls: enumRef,
events: enumRef,
constants: Vector(
Struct({
name: str,
type: compactNumber,
value: Hex(),
docs,
deprecationInfo: itemDeprecation,
}),
),
errors: enumRef,
associatedTypes: Vector(
Struct({
name: str,
type: compactNumber,
docs,
}),
),
viewFns: Vector(viewFunction),
index: u8,
docs,
deprecationInfo: itemDeprecation,
}
@@ -0,0 +1,38 @@
import { Struct, Vector, str } from "scale-ts"
import { docs } from "./docs"
import { compactNumber, Hex, compactNumber as ty } from "../scale"
import { itemDeprecation } from "./deprecation"
export const runtimeApiMethod = {
name: str,
inputs: Vector(
Struct({
name: str,
type: ty,
}),
),
output: ty,
docs,
}
export const runtimeApiV15 = Struct({
name: str,
methods: Vector(Struct(runtimeApiMethod)),
docs,
})
export const runtimeApi = Struct({
name: str,
methods: Vector(
Struct({ ...runtimeApiMethod, deprecationInfo: itemDeprecation }),
),
docs,
version: compactNumber,
deprecationInfo: itemDeprecation,
})
export const viewFunction = Struct({
id: Hex(32),
...runtimeApiMethod,
deprecationInfo: itemDeprecation,
})
@@ -0,0 +1,161 @@
import { CodecType } from "scale-ts"
import { V14Lookup } from "./lookup"
import { storageMap } from "./pallets"
import { HexString } from "../scale"
import { itemDeprecation, variantDeprecation } from "./deprecation"
import { viewFunction } from "./runtime-api"
import { V14 } from "./v14"
import { V16 } from "./v16"
import { V15 } from "./v15"
import { Metadata } from "./metadata"
type EnumRef<T> =
| ({
type: number
} & (T extends 16
? { deprecationInfo: CodecType<typeof variantDeprecation> }
: {}))
| undefined
type DeprecationInfo<T> = T extends 16
? { deprecationInfo: CodecType<typeof itemDeprecation> }
: {}
export type UnifiedMetadata<T extends 14 | 15 | 16 = 14 | 15 | 16> = {
version: T
lookup: V14Lookup
pallets: Array<
{
name: string
storage:
| {
prefix: string
items: Array<
{
name: string
modifier: number
type:
| { tag: "plain"; value: number }
| { tag: "map"; value: CodecType<typeof storageMap> }
fallback: HexString
docs: string[]
} & DeprecationInfo<T>
>
}
| undefined
calls: EnumRef<T>
events: EnumRef<T>
constants: Array<
{
name: string
type: number
value: HexString
docs: string[]
} & DeprecationInfo<T>
>
errors: EnumRef<T>
associatedTypes: Array<{
name: string
type: number
docs: string[]
}>
viewFns: Array<CodecType<typeof viewFunction>>
index: number
docs: string[]
} & DeprecationInfo<T>
>
extrinsic: {
version: number[]
signedExtensions: Array<{
identifier: string
type: number
additionalSigned: number
}>
} & (T extends 14
? {
type: number
}
: { address: number; call: number; signature: number }) &
(T extends 16
? { signedExtensionsByVersion: Array<[number, number[]]> }
: {})
apis: Array<
{
name: string
methods: Array<
{
name: string
inputs: Array<{ name: string; type: number }>
output: number
docs: string[]
} & DeprecationInfo<T>
>
docs: string[]
} & (T extends 16 ? { version: number } : {}) &
DeprecationInfo<T>
>
} & (T extends 14
? {}
: {
outerEnums: { call: number; event: number; error: number }
custom: Array<[string, { type: number; value: HexString }]>
})
export const unifyMetadata = (
metadata: Metadata | Metadata["metadata"] | V14 | V15 | V16,
): UnifiedMetadata => {
// complete metadata
if ("magicNumber" in metadata) metadata = metadata.metadata
if ("tag" in metadata) {
if (
metadata.tag !== "v14" &&
metadata.tag !== "v15" &&
metadata.tag !== "v16"
)
throw new Error("Only metadata 14, 15, and 16 are supported")
metadata = metadata.value
}
// v16
if ("signedExtensionsByVersion" in metadata.extrinsic) {
return { version: 16, ...(metadata as V16) }
}
// v15
if ("custom" in metadata) {
const { lookup, extrinsic, custom, apis, pallets, outerEnums } =
metadata as V15
return {
version: 15,
lookup,
pallets: pallets.map((p): UnifiedMetadata<15>["pallets"][number] => ({
...p,
calls: p.calls != null ? { type: p.calls } : undefined,
events: p.events != null ? { type: p.events } : undefined,
errors: p.errors != null ? { type: p.errors } : undefined,
viewFns: [],
associatedTypes: [],
})),
extrinsic: { ...extrinsic, version: [extrinsic.version] },
apis,
outerEnums,
custom,
}
}
// fallback, v14
const { lookup, extrinsic, pallets } = metadata as V14
return {
version: 14,
lookup,
pallets: pallets.map((p): UnifiedMetadata<14>["pallets"][number] => ({
...p,
calls: p.calls != null ? { type: p.calls } : undefined,
events: p.events != null ? { type: p.events } : undefined,
errors: p.errors != null ? { type: p.errors } : undefined,
viewFns: [],
associatedTypes: [],
})),
extrinsic: { ...extrinsic, version: [extrinsic.version] },
apis: [],
}
}
@@ -0,0 +1,33 @@
import { CodecType, Struct, Vector, createCodec, str, u8 } from "scale-ts"
import { lookup } from "./lookup"
import { compactNumber } from "../scale"
import { v14Pallet } from "./pallets"
import { runtimeApi } from "./runtime-api"
const empty = new Uint8Array()
const Always = <T>(value: T) =>
createCodec<T>(
() => empty,
() => value,
)
const extrinsic = Struct({
type: compactNumber,
version: u8,
signedExtensions: Vector(
Struct({
identifier: str,
type: compactNumber,
additionalSigned: compactNumber,
}),
),
})
export const v14 = Struct({
lookup,
pallets: Vector(Struct({ ...v14Pallet, docs: Always([] as string[]) })),
extrinsic,
type: compactNumber,
apis: Always([] as Array<CodecType<typeof runtimeApi>>),
})
export type V14 = CodecType<typeof v14>
@@ -0,0 +1,35 @@
import { CodecType, Struct, Tuple, Vector, str, u8 } from "scale-ts"
import { lookup } from "./lookup"
import { v15Pallet } from "./pallets"
import { Hex, compactNumber as ty } from "../scale"
import { runtimeApiV15 } from "./runtime-api"
const extrinsic = Struct({
version: u8,
address: ty,
call: ty,
signature: ty,
extra: ty,
signedExtensions: Vector(
Struct({
identifier: str,
type: ty,
additionalSigned: ty,
}),
),
})
export const v15 = Struct({
lookup,
pallets: Vector(Struct(v15Pallet)),
extrinsic,
type: ty,
apis: Vector(runtimeApiV15),
outerEnums: Struct({
call: ty,
event: ty,
error: ty,
}),
custom: Vector(Tuple(str, Struct({ type: ty, value: Hex() }))),
})
export type V15 = CodecType<typeof v15>
@@ -0,0 +1,34 @@
import { CodecType, Struct, Tuple, Vector, str, u8 } from "scale-ts"
import { lookup } from "./lookup"
import { v16Pallet } from "./pallets"
import { compactNumber, Hex, compactNumber as ty } from "../scale"
import { runtimeApi } from "./runtime-api"
const extrinsic = Struct({
version: Vector(u8),
address: ty,
call: ty,
signature: ty,
signedExtensionsByVersion: Vector(Tuple(u8, Vector(compactNumber))),
signedExtensions: Vector(
Struct({
identifier: str,
type: ty,
additionalSigned: ty,
}),
),
})
export const v16 = Struct({
lookup,
pallets: Vector(Struct(v16Pallet)),
extrinsic,
apis: Vector(runtimeApi),
outerEnums: Struct({
call: ty,
event: ty,
error: ty,
}),
custom: Vector(Tuple(str, Struct({ type: ty, value: Hex() }))),
})
export type V16 = CodecType<typeof v16>
@@ -0,0 +1,25 @@
import { Bytes, enhanceCodec } from "scale-ts"
import {
getSs58AddressInfo,
SS58String,
fromBufferToBase58,
} from "@/utils/ss58-util"
function fromBase58ToBuffer(nBytes: number, _ss58Format: number) {
return (address: SS58String) => {
const info = getSs58AddressInfo(address)
if (!info.isValid) throw new Error("Invalid checksum")
const { publicKey } = info
if (publicKey.length !== nBytes)
throw new Error("Invalid public key length")
return publicKey
}
}
export const AccountId = (ss58Format: number = 42, nBytes: 32 | 33 = 32) =>
enhanceCodec(
Bytes(nBytes),
fromBase58ToBuffer(nBytes, ss58Format),
fromBufferToBase58(ss58Format),
)
@@ -0,0 +1,106 @@
import {
Bytes,
Codec,
Decoder,
Encoder,
Tuple,
compact,
createCodec,
} from "scale-ts"
import { fromHex, mergeUint8, toHex } from "@pezkuwi/papi-utils"
import type { HexString } from "./Hex"
import { SS58String } from "@/utils"
import { AccountId } from "./AccountId"
const textEncoder = new TextEncoder()
const textDecoder = new TextDecoder()
const opaqueBytesDec = Tuple(compact, Bytes(Infinity))[1]
export class Binary {
#bytes: Uint8Array
#opaqueBytes: Uint8Array | null = null
#hex: HexString | null = null
#opaqueHex: HexString | null = null
#str: string | null = null
constructor(data: Uint8Array, opaque = false) {
if (opaque) {
try {
const [len, bytes] = opaqueBytesDec(data)
if (len === bytes.length) {
this.#bytes = bytes
this.#opaqueBytes = data
return
}
} catch (_) {}
throw new Error("Invalid opaque bytes")
} else this.#bytes = data
}
asText = () => (this.#str ??= textDecoder.decode(this.#bytes))
asHex = () => (this.#hex ??= toHex(this.#bytes)) as `0x${string}`
asOpaqueHex = () =>
(this.#opaqueHex ??= toHex(this.asOpaqueBytes())) as `0x${string}`
asBytes = () => this.#bytes
asOpaqueBytes = () =>
(this.#opaqueBytes ??= mergeUint8([
compact[0](this.#bytes.length),
this.#bytes,
]))
static fromText(input: string): Binary {
return new this(textEncoder.encode(input))
}
static fromHex(input: HexString): Binary {
return new this(fromHex(input))
}
static fromOpaqueHex(input: HexString): Binary {
return new this(fromHex(input), true)
}
static fromBytes(input: Uint8Array): Binary {
return new this(input)
}
static fromOpaqueBytes(input: Uint8Array): Binary {
return new this(input, true)
}
}
const [accountIdEncoder] = AccountId()
export class FixedSizeBinary<_L extends number> extends Binary {
constructor(data: Uint8Array) {
super(data)
}
static fromArray<L extends number, I extends Array<number> & { length: L }>(
input: I,
) {
return new this<L>(new Uint8Array(input))
}
static fromAccountId32<L extends number>(
input: L extends 32 ? SS58String : never,
) {
return new this<L>(accountIdEncoder(input))
}
}
const enc = (nBytes?: number): Encoder<Binary> => {
const _enc = Bytes.enc(nBytes)
return (value) => _enc(value.asBytes())
}
const dec = (nBytes?: number): Decoder<Binary> => {
const _dec = Bytes.dec(nBytes)
const Bin = nBytes == null ? Binary : FixedSizeBinary
return (value) => Bin.fromBytes(_dec(value))
}
export const Bin = (nBytes?: number): Codec<Binary> =>
createCodec(enc(nBytes), dec(nBytes))
Bin.enc = enc
Bin.dec = dec
@@ -0,0 +1,53 @@
import {
Bytes,
Codec,
Decoder,
Encoder,
createCodec,
createDecoder,
} from "scale-ts"
import { compactNumber } from "./compact"
import { mergeUint8 } from "@pezkuwi/papi-utils"
export type BitSeq = Array<0 | 1>
const dec: (isLsb?: boolean) => Decoder<BitSeq> = (isLsb = true) =>
createDecoder((data) => {
const bitsLen = compactNumber.dec(data)
const bytesLen = Math.ceil(bitsLen / 8)
const bytes = Bytes(bytesLen).dec(data)
const result = new Array<0 | 1>(bitsLen)
let resultIdx = 0
bytes.forEach((val) => {
for (let i = 0; i < 8 && resultIdx < bitsLen; i++) {
const actualIdx = isLsb ? i : 7 - i
result[resultIdx++] = ((val >> actualIdx) & 1) as 1 | 0
}
})
return result
})
const enc: (isLsb?: boolean) => Encoder<BitSeq> =
(isLsb = true) =>
(input) => {
const lenEncoded = compactNumber.enc(input.length)
const nBytes = Math.ceil(input.length / 8)
const bytes = new Uint8Array(nBytes)
for (let byteIdx = 0; byteIdx < nBytes; byteIdx++) {
let inputIdx = byteIdx * 8
let byte = 0
for (let i = 0; i < 8 && inputIdx < input.length; i++, inputIdx++)
byte |= input[inputIdx] << (isLsb ? i : 7 - i)
bytes[byteIdx] = byte
}
return mergeUint8([lenEncoded, bytes])
}
export const BitSeq = (isLsb?: boolean): Codec<BitSeq> =>
createCodec(enc(isLsb), dec(isLsb))
BitSeq.enc = enc
BitSeq.dec = dec
@@ -0,0 +1,20 @@
import { fromHex, toHex } from "@pezkuwi/papi-utils"
import { Bytes, Codec, Decoder, Encoder, createCodec } from "scale-ts"
export type HexString = string & { __hexString?: unknown }
const enc = (nBytes?: number): Encoder<HexString> => {
const _enc = Bytes.enc(nBytes)
return (value: string) => _enc(fromHex(value))
}
const dec = (nBytes?: number): Decoder<HexString> => {
const _dec = Bytes.dec(nBytes)
return (value) => toHex(_dec(value)) as HexString
}
export const Hex = (nBytes?: number): Codec<HexString> =>
createCodec(enc(nBytes), dec(nBytes))
Hex.enc = enc
Hex.dec = dec
@@ -0,0 +1,28 @@
import { Codec, Decoder, Encoder, createCodec, _void } from "scale-ts"
export const selfEncoder = <T>(value: () => Encoder<T>): Encoder<T> => {
let cache: Encoder<T> = (x) => {
const encoder = value()
cache = encoder
return encoder(x)
}
return (x) => cache(x)
}
export const selfDecoder = <T>(value: () => Decoder<T>): Decoder<T> => {
let cache: Decoder<T> = (x) => {
const decoder = value()
const result = decoder
cache = decoder
return result(x)
}
return (x) => cache(x)
}
export const Self = <T>(value: () => Codec<T>): Codec<T> =>
createCodec(
selfEncoder(() => value().enc),
selfDecoder(() => value().dec),
)
@@ -0,0 +1,149 @@
import {
Codec,
CodecType,
Decoder,
DecoderType,
Encoder,
EncoderType,
Enum as OEnum,
StringRecord,
createCodec,
} from "scale-ts"
import { mapObject } from "@pezkuwi/papi-utils"
import { Enum } from "@/types/enum"
import { withInner } from "./with-inner"
type Tuple<T, N extends number> = readonly [T, ...T[]] & { length: N }
type Push<T extends any[], V> = [...T, V]
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I,
) => void
? I
: never
type LastOf<T> =
UnionToIntersection<T extends any ? () => T : never> extends () => infer R
? R
: never
type TuplifyUnion<
T,
L = LastOf<T>,
N = [T] extends [never] ? true : false,
> = true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L>
type RestrictedLenTuple<T, O extends StringRecord<any>> = Tuple<
T,
TuplifyUnion<keyof O> extends Tuple<any, infer V> ? V : 0
>
const VariantEnc: <O extends StringRecord<Encoder<any>>>(
inner: O,
x?: RestrictedLenTuple<number, O> | undefined,
) => Encoder<
Enum<{
[K in keyof O]: EncoderType<O[K]>
}>
> & { inner: O } = (...args) => {
const enc = OEnum.enc(...(args as [any, any]))
return withInner((v: any) => enc({ tag: v.type, value: v.value }), args[0])
}
const VariantDec: <O extends StringRecord<Decoder<any>>>(
inner: O,
x?: RestrictedLenTuple<number, O> | undefined,
) => Decoder<
Enum<{
[K in keyof O]: DecoderType<O[K]>
}>
> & { inner: O } = (...args) => {
const dec = OEnum.dec(...(args as [any]))
return withInner((v: any) => {
const { tag, value } = dec(v)
return Enum(tag as any, value as any) as any
}, args[0])
}
export const Variant: {
<O extends StringRecord<Codec<any>>>(
inner: O,
indexes?: RestrictedLenTuple<number, O> | undefined,
): Codec<
Enum<{
[K in keyof O]: CodecType<O[K]>
}>
> & { inner: O }
enc: <O_1 extends StringRecord<Encoder<any>>>(
inner: O_1,
x?: RestrictedLenTuple<number, O_1> | undefined,
) => Encoder<
Enum<{
[K in keyof O_1]: EncoderType<O_1[K]>
}>
> & { inner: O_1 }
dec: <O_2 extends StringRecord<Decoder<any>>>(
inner: O_2,
x?: RestrictedLenTuple<number, O_2> | undefined,
) => Decoder<
Enum<{
[K in keyof O_2]: DecoderType<O_2[K]>
}>
> & { inner: O_2 }
} = (inner, ...args) =>
withInner(
createCodec(
VariantEnc(
mapObject(inner, ([encoder]) => encoder) as any,
...(args as any[]),
),
VariantDec(
mapObject(inner, ([, decoder]) => decoder) as any,
...(args as any[]),
) as any,
),
inner,
) as any
Variant.enc = VariantEnc
Variant.dec = VariantDec
export const ScaleEnum: {
<O extends StringRecord<Codec<any>>>(
inner: O,
indexes?: RestrictedLenTuple<number, O> | undefined,
): Codec<
{
[K in keyof O]: {
tag: K
value: CodecType<O[K]>
}
}[keyof O]
> & { inner: O }
enc: <O_1 extends StringRecord<Encoder<any>>>(
inner: O_1,
x?: RestrictedLenTuple<number, O_1> | undefined,
) => Encoder<
{
[K_1 in keyof O_1]: {
tag: K_1
value: EncoderType<O_1[K_1]>
}
}[keyof O_1]
> & { inner: O_1 }
dec: <O_2 extends StringRecord<Decoder<any>>>(
inner: O_2,
x?: RestrictedLenTuple<number, O_2> | undefined,
) => Decoder<
{
[K_2 in keyof O_2]: {
tag: K_2
value: DecoderType<O_2[K_2]>
}
}[keyof O_2]
> & { inner: O_2 }
} = (inner, ...args) => withInner(OEnum(inner, ...(args as any[])), inner)
ScaleEnum.enc = (inner, ...rest) =>
withInner(OEnum.enc(inner, ...(rest as any[])), inner)
ScaleEnum.dec = (inner, ...rest) =>
withInner(OEnum.dec(inner, ...(rest as any[])), inner)
@@ -0,0 +1,30 @@
import { Bytes, Decoder, Encoder, createCodec, createDecoder } from "scale-ts"
import { compactNumber } from "./compact"
export interface BitSequence {
bitsLen: number
bytes: Uint8Array
}
const bitSequenceDecoder: Decoder<BitSequence> = createDecoder((data) => {
const bitsLen = compactNumber.dec(data)
const bytesLen = Math.ceil(bitsLen / 8)
const bytes = Bytes(bytesLen).dec(data)
return { bytes, bitsLen }
})
const bitSequenceEncoder: Encoder<BitSequence> = (input) => {
if (input.bitsLen > input.bytes.length * 8)
throw new Error(
`Not enough bytes. (bitsLen:${input.bitsLen}, bytesLen:${input.bytes.length})`,
)
const lenEncoded = compactNumber.enc(input.bitsLen)
const result = new Uint8Array(input.bytes.length + lenEncoded.length)
result.set(lenEncoded, 0)
result.set(input.bytes, lenEncoded.length)
return result
}
export const bitSequence = createCodec(bitSequenceEncoder, bitSequenceDecoder)
@@ -0,0 +1,7 @@
import { enhanceCodec, u8 } from "scale-ts"
export const char = enhanceCodec(
u8,
(str: string) => str.charCodeAt(0),
String.fromCharCode,
)
@@ -0,0 +1,4 @@
import { compact, enhanceCodec } from "scale-ts"
export const compactNumber = enhanceCodec(compact, (v) => v, Number)
export const compactBn = enhanceCodec(compact, (v) => v, BigInt)
@@ -0,0 +1,39 @@
import { fromHex, toHex } from "@pezkuwi/papi-utils"
import { Bytes, createCodec, createDecoder } from "scale-ts"
import { Keccak256 } from "../../hashes"
import { textEncoder } from "./fixed-str"
const getFormattedAddress = (hexAddress: string) => {
const nonChecksum = hexAddress.slice(2)
const hashedAddress = toHex(Keccak256(textEncoder.encode(nonChecksum))).slice(
2,
)
const result = new Array(40)
for (let i = 0; i < 40; i++) {
const checksumVal = parseInt(hashedAddress[i], 16)
const char = nonChecksum[i]
result[i] = checksumVal > 7 ? char.toUpperCase() : char
}
return `0x${result.join("")}`
}
const bytes20Dec = Bytes(20)[1]
export const ethAccount = createCodec<string>(
(input: string) => {
const bytes = fromHex(input)
if (bytes.length !== 20)
throw new Error(`Invalid length found on EthAddress(${input})`)
const hexAddress = toHex(bytes)
if (input === hexAddress || input === hexAddress.toUpperCase()) return bytes
if (getFormattedAddress(hexAddress) !== input)
throw new Error(`Invalid checksum found on EthAddress(${input})`)
return bytes
},
createDecoder((bytes) => getFormattedAddress(toHex(bytes20Dec(bytes)))),
)
@@ -0,0 +1,11 @@
import { Bytes, enhanceCodec } from "scale-ts"
export const textEncoder = new TextEncoder()
const textDecoder = new TextDecoder()
export const fixedStr = (nBytes: number) =>
enhanceCodec(
Bytes(nBytes),
(str: string) => textEncoder.encode(str),
(bytes) => textDecoder.decode(bytes),
)
@@ -0,0 +1,100 @@
import {
Struct as OStruct,
Tuple as OTuple,
Vector as OVector,
Result as OResult,
Option as OOption,
Codec,
Encoder,
Decoder,
StringRecord,
CodecType,
EncoderType,
DecoderType,
ResultPayload,
} from "scale-ts"
import { withInner } from "./with-inner"
export const Struct: {
<A extends StringRecord<Codec<any>>>(
codecs: A,
): Codec<{ [K in keyof A]: CodecType<A[K]> }> & { inner: A }
enc: <A_1 extends StringRecord<Encoder<any>>>(
encoders: A_1,
) => Encoder<{ [K_1 in keyof A_1]: EncoderType<A_1[K_1]> }> & { inner: A_1 }
dec: <A_2 extends StringRecord<Decoder<any>>>(
decoders: A_2,
) => Decoder<{ [K_2 in keyof A_2]: DecoderType<A_2[K_2]> }> & { inner: A_2 }
} = (codecs) => withInner(OStruct(codecs), codecs)
Struct.enc = (x) => withInner(OStruct.enc(x), x)
Struct.dec = (x) => withInner(OStruct.dec(x), x)
export const Tuple: {
<A extends Codec<any>[]>(
...inner: A
): Codec<{ [K in keyof A]: A[K] extends Codec<infer D> ? D : unknown }> & {
inner: A
}
enc: <A_1 extends Encoder<any>[]>(
...encoders: A_1
) => Encoder<{
[K_1 in keyof A_1]: A_1[K_1] extends Encoder<infer D_1> ? D_1 : unknown
}> & { inner: A_1 }
dec: <A_2 extends Decoder<any>[]>(
...decoders: A_2
) => Decoder<{
[K_2 in keyof A_2]: A_2[K_2] extends Decoder<infer D_2> ? D_2 : unknown
}> & { inner: A_2 }
} = (...inner) => withInner(OTuple(...inner), inner)
Tuple.enc = (...inner) => withInner(OTuple.enc(...inner), inner)
Tuple.dec = (...inner) => withInner(OTuple.dec(...inner), inner)
export const Vector: {
<T>(
inner: Codec<T>,
size?: number | undefined,
): Codec<T[]> & { inner: Codec<T> }
enc: <T_1>(
inner: Encoder<T_1>,
size?: number | undefined,
) => Encoder<T_1[]> & { inner: Encoder<T_1> }
dec: <T_2>(
getter: Decoder<T_2>,
size?: number | undefined,
) => Decoder<T_2[]> & { inner: Decoder<T_2> }
} = (inner, ...rest) => withInner(OVector(inner, ...rest), inner)
Vector.enc = (inner, ...rest) => withInner(OVector.enc(inner, ...rest), inner)
Vector.dec = (inner, ...rest) => withInner(OVector.dec(inner, ...rest), inner)
export const Result: {
<OK, KO>(
okCodec: Codec<OK>,
koCodec: Codec<KO>,
): Codec<ResultPayload<OK, KO>> & { inner: { ok: Codec<OK>; ko: Codec<KO> } }
dec: <OK_1, KO_1>(
okDecoder: Decoder<OK_1>,
koDecoder: Decoder<KO_1>,
) => Decoder<ResultPayload<OK_1, KO_1>> & {
inner: { ok: Decoder<OK_1>; ko: Decoder<KO_1> }
}
enc: <OK_2, KO_2>(
okEncoder: Encoder<OK_2>,
koEncoder: Encoder<KO_2>,
) => Encoder<ResultPayload<OK_2, KO_2>> & {
inner: { ok: Encoder<OK_2>; ko: Encoder<KO_2> }
}
} = (ok, ko) => withInner(OResult(ok, ko), { ok, ko })
Result.enc = (ok, ko) => withInner(OResult.enc(ok, ko), { ok, ko })
Result.dec = (ok, ko) => withInner(OResult.dec(ok, ko), { ok, ko })
export const Option: {
<T>(inner: Codec<T>): Codec<T | undefined> & { inner: Codec<T> }
enc: <T_1>(
inner: Encoder<T_1>,
) => Encoder<T_1 | undefined> & { inner: Encoder<T_1> }
dec: <T_2>(
inner: Decoder<T_2>,
) => Decoder<T_2 | undefined> & { inner: Decoder<T_2> }
} = (inner) => withInner(OOption(inner), inner)
Option.enc = (inner) => withInner(OOption.enc(inner), inner)
Option.dec = (inner) => withInner(OOption.dec(inner), inner)
@@ -0,0 +1,5 @@
export const withInner = <T, I>(codec: T, inner: I): T & { inner: I } => {
const result: T & { inner: I } = codec as any
result.inner = inner
return result
}
@@ -0,0 +1,28 @@
import { enhanceCodec, u8 } from "scale-ts"
const TYPES = {
bare: 0b00,
0b00: "bare",
general: 0b01,
0b01: "general",
signed: 0b10,
0b10: "signed",
} as const
export type ExtrinsicFormat =
| { version: 4; type: "bare" | "signed" }
| { version: 5; type: "bare" | "general" }
export const extrinsicFormat = enhanceCodec<number, ExtrinsicFormat>(
u8,
({ version, type }) => version + (TYPES[type] << 6),
(v) => {
const version = v & 0x3f // 0b0011_1111
const type = v >> 6
if (version === 4 && (type === TYPES.bare || type === TYPES.signed))
return { version, type: TYPES[type] }
if (version === 5 && (type === TYPES.bare || type === TYPES.general))
return { version, type: TYPES[type] }
throw new Error(`ExtrinsicFormat ${v} not valid`)
},
)
@@ -0,0 +1,11 @@
import { mergeUint8 } from "@pezkuwi/papi-utils"
import { blake2b } from "@noble/hashes/blake2.js"
const len32 = { dkLen: 32 }
export const Blake2256 = (encoded: Uint8Array) => blake2b(encoded, len32)
const len16 = { dkLen: 16 }
export const Blake2128 = (encoded: Uint8Array) => blake2b(encoded, len16)
export const Blake2128Concat = (encoded: Uint8Array) =>
mergeUint8([Blake2128(encoded), encoded])
@@ -0,0 +1,8 @@
import { mergeUint8 } from "@pezkuwi/papi-utils"
import { blake3 } from "@noble/hashes/blake3.js"
const len32 = { dkLen: 32 }
export const Blake3256 = (encoded: Uint8Array) => blake3(encoded, len32)
export const Blake3256Concat = (encoded: Uint8Array) =>
mergeUint8([Blake3256(encoded), encoded])
@@ -0,0 +1,177 @@
// Adapted implementation based on: https://github.com/pierrec/js-xxhash/blob/7ff5ced282f97dba121109d7013e0fa80360398c/lib/xxhash64.js
// Distributed under the MIT software license, see the accompanying
// file LICENSE or http://www.opensource.org/licenses/mit-license.php.
// helper functions
const bigintFromU16 = (
v0: number,
v1: number,
v2: number,
v3: number,
): bigint =>
new DataView(new Uint16Array([v0, v1, v2, v3]).buffer).getBigUint64(0, true)
const MASK_64 = 2n ** 64n - 1n
const rotl = (input: bigint, nBits: bigint) =>
((input << nBits) & MASK_64) | (input >> (64n - nBits))
const multiply = (a: bigint, b: bigint) => (a * b) & MASK_64
const add = (a: bigint, b: bigint) => (a + b) & MASK_64
// constants
const PRIME64_1 = 11400714785074694791n
const PRIME64_2 = 14029467366897019727n
const PRIME64_3 = 1609587929392839161n
const PRIME64_4 = 9650029242287828579n
const PRIME64_5 = 2870177450012600261n
export function h64(input: Uint8Array, seed: bigint = 0n) {
let v1 = add(add(seed, PRIME64_1), PRIME64_2)
let v2 = add(seed, PRIME64_2)
let v3 = seed
let v4 = seed - PRIME64_1
let totalLen = input.length
let memsize = 0
let memory: Uint8Array | null = null
;(function update() {
let p = 0
let bEnd = p + totalLen
if (!totalLen) return
memory = new Uint8Array(32)
if (totalLen < 32) {
memory.set(input.subarray(0, totalLen), memsize)
memsize += totalLen
return
}
if (p <= bEnd - 32) {
const limit = bEnd - 32
do {
let other
other = bigintFromU16(
(input[p + 1] << 8) | input[p],
(input[p + 3] << 8) | input[p + 2],
(input[p + 5] << 8) | input[p + 4],
(input[p + 7] << 8) | input[p + 6],
)
v1 = multiply(rotl(add(v1, multiply(other, PRIME64_2)), 31n), PRIME64_1)
p += 8
other = bigintFromU16(
(input[p + 1] << 8) | input[p],
(input[p + 3] << 8) | input[p + 2],
(input[p + 5] << 8) | input[p + 4],
(input[p + 7] << 8) | input[p + 6],
)
v2 = multiply(rotl(add(v2, multiply(other, PRIME64_2)), 31n), PRIME64_1)
p += 8
other = bigintFromU16(
(input[p + 1] << 8) | input[p],
(input[p + 3] << 8) | input[p + 2],
(input[p + 5] << 8) | input[p + 4],
(input[p + 7] << 8) | input[p + 6],
)
v3 = multiply(rotl(add(v3, multiply(other, PRIME64_2)), 31n), PRIME64_1)
p += 8
other = bigintFromU16(
(input[p + 1] << 8) | input[p],
(input[p + 3] << 8) | input[p + 2],
(input[p + 5] << 8) | input[p + 4],
(input[p + 7] << 8) | input[p + 6],
)
v4 = multiply(rotl(add(v4, multiply(other, PRIME64_2)), 31n), PRIME64_1)
p += 8
} while (p <= limit)
}
if (p < bEnd) {
memory.set(input.subarray(p, bEnd), memsize)
memsize = bEnd - p
}
})()
input = memory || input
let result: bigint
let p = 0
if (totalLen >= 32) {
result = rotl(v1, 1n)
result = add(result, rotl(v2, 7n))
result = add(result, rotl(v3, 12n))
result = add(result, rotl(v4, 18n))
v1 = multiply(rotl(multiply(v1, PRIME64_2), 31n), PRIME64_1)
result = result ^ v1
result = add(multiply(result, PRIME64_1), PRIME64_4)
v2 = multiply(rotl(multiply(v2, PRIME64_2), 31n), PRIME64_1)
result = result ^ v2
result = add(multiply(result, PRIME64_1), PRIME64_4)
v3 = multiply(rotl(multiply(v3, PRIME64_2), 31n), PRIME64_1)
result = result ^ v3
result = add(multiply(result, PRIME64_1), PRIME64_4)
v4 = multiply(rotl(multiply(v4, PRIME64_2), 31n), PRIME64_1)
result = result ^ v4
result = add(multiply(result, PRIME64_1), PRIME64_4)
} else {
result = add(seed, PRIME64_5)
}
result = add(result, BigInt(totalLen))
while (p <= memsize - 8) {
let temp = bigintFromU16(
(input[p + 1] << 8) | input[p],
(input[p + 3] << 8) | input[p + 2],
(input[p + 5] << 8) | input[p + 4],
(input[p + 7] << 8) | input[p + 6],
)
temp = multiply(rotl(multiply(temp, PRIME64_2), 31n), PRIME64_1)
result = add(multiply(rotl(result ^ temp, 27n), PRIME64_1), PRIME64_4)
p += 8
}
if (p + 4 <= memsize) {
let temp = multiply(
bigintFromU16(
(input[p + 1] << 8) | input[p],
(input[p + 3] << 8) | input[p + 2],
0,
0,
),
PRIME64_1,
)
result = add(multiply(rotl(result ^ temp, 23n), PRIME64_2), PRIME64_3)
p += 4
}
while (p < memsize) {
const temp = multiply(bigintFromU16(input[p++], 0, 0, 0), PRIME64_5)
result = multiply(rotl(result ^ temp, 11n), PRIME64_1)
}
let temp = result >> 33n
result = multiply(result ^ temp, PRIME64_2)
temp = result >> 29n
result = multiply(result ^ temp, PRIME64_3)
temp = result >> 32n
result ^= temp
return result
}
@@ -0,0 +1 @@
export const Identity = (encoded: Uint8Array): Uint8Array => encoded
@@ -0,0 +1,3 @@
import { keccak_256 } from "@noble/hashes/sha3.js"
export const Keccak256: (input: Uint8Array) => Uint8Array = keccak_256
@@ -0,0 +1,28 @@
import { mergeUint8 } from "@pezkuwi/papi-utils"
import { u64 } from "scale-ts"
import { h64 } from "./h64"
export const Twox128 = (input: Uint8Array): Uint8Array => {
const result = new Uint8Array(16)
const dv = new DataView(result.buffer)
dv.setBigUint64(0, h64(input), true)
dv.setBigUint64(8, h64(input, 1n), true)
return result
}
export const Twox256 = (input: Uint8Array): Uint8Array => {
const result = new Uint8Array(32)
const dv = new DataView(result.buffer)
dv.setBigUint64(0, h64(input), true)
dv.setBigUint64(8, h64(input, 1n), true)
dv.setBigUint64(16, h64(input, 2n), true)
dv.setBigUint64(24, h64(input, 3n), true)
return result
}
export const Twox64Concat = (encoded: Uint8Array): Uint8Array =>
mergeUint8([u64.enc(h64(encoded)), encoded])
@@ -0,0 +1,106 @@
import { fromHex, mergeUint8, toHex } from "@pezkuwi/papi-utils"
import type { Codec } from "scale-ts"
import {
Blake2128,
Blake2128Concat,
Blake2256,
Identity,
Twox128,
Twox256,
Twox64Concat,
} from "./hashes"
export type EncoderWithHash<T> = [Codec<T>, (input: Uint8Array) => Uint8Array]
const textEncoder = new TextEncoder()
// the value indicates:
// - when positive: the number of bytes to skip before reaching the transparent-encoded key
// - when negative: the number of bytes that the opaque hasher will generate
const hashers: Map<(input: Uint8Array) => Uint8Array, number> = new Map([
[Identity, 0],
[Twox64Concat, 8],
[Blake2128Concat, 16],
[Blake2128, -16],
[Blake2256, -32],
[Twox128, -16],
[Twox256, -32],
])
export type OpaqueKeyHash = string & { __opaqueKeyHash?: unknown }
export const Storage = (pallet: string) => {
const palledEncoded = Twox128(textEncoder.encode(pallet))
return <A extends Array<EncoderWithHash<any>>>(
name: string,
...encoders: [...A]
): {
enc: (
...args: {
[K in keyof A]: A[K] extends EncoderWithHash<infer V> ? V : unknown
}
) => string
dec: (value: string) => {
[K in keyof A]: A[K] extends EncoderWithHash<infer V> ? V : unknown
}
} => {
const palletItemEncoded = mergeUint8([
palledEncoded,
Twox128(textEncoder.encode(name)),
])
const palletItemEncodedHex = toHex(palletItemEncoded)
const dec = (
key: string,
): {
[K in keyof A]: A[K] extends EncoderWithHash<infer V> ? V : unknown
} => {
if (!key.startsWith(palletItemEncodedHex))
throw new Error(`key does not match this storage (${pallet}.${name})`)
if (encoders.length === 0) return [] as any
const argsKey = fromHex(key.slice(palletItemEncodedHex.length))
const result = new Array<any>(encoders.length)
for (let i = 0, cur = 0; i < encoders.length; i++) {
const [codec, hasher] = encoders[i]
const hBytes = hashers.get(hasher)
if (hBytes == null) throw new Error("Unknown hasher")
if (hBytes < 0) {
const opaqueBytes = hBytes * -1
result[i] = toHex(argsKey.slice(cur, cur + opaqueBytes))
cur += opaqueBytes
} else {
cur += hBytes
result[i] = codec.dec(argsKey.slice(cur))
cur += codec.enc(result[i]).length
}
}
return result as any
}
const fns = encoders.map(
([{ enc }, hash]) =>
(val: any) =>
hash(enc(val)),
)
const enc = (
...args: {
[K in keyof A]: A[K] extends EncoderWithHash<infer V> ? V : unknown
}
): string =>
toHex(
mergeUint8([
palletItemEncoded,
...args.map((val, idx) => fns[idx](val)),
]),
)
return {
enc,
dec,
}
}
}
@@ -0,0 +1,70 @@
import { createDecoder, Hex, u16, u8, type HexString } from "../codecs"
import { type TrieNodeHeaderKey, TrieNodeHeaders, type TrieNode } from "./types"
const varHex = Hex().dec
const allHex = Hex(Infinity).dec
const hex32 = Hex(32).dec
const byte = u8.dec
const getHeader = (
bytes: Uint8Array,
): { type: TrieNodeHeaderKey; partialKey: string } => {
const firstByte = byte(bytes)
let bitsLeft = 6
const typeId = firstByte >> bitsLeft
const type: TrieNodeHeaderKey = typeId
? typeId === 1
? TrieNodeHeaders.Leaf
: typeId === 2
? TrieNodeHeaders.Branch
: TrieNodeHeaders.BranchWithVal
: firstByte >> --bitsLeft
? TrieNodeHeaders.LeafWithHash
: firstByte >> --bitsLeft
? TrieNodeHeaders.BranchWithHash
: firstByte
? TrieNodeHeaders.Reserved
: TrieNodeHeaders.Empty
let nNibles = firstByte & (0xff >> (8 - bitsLeft))
if (nNibles === 2 ** bitsLeft - 1) {
let current: number
do nNibles += current = byte(bytes)
while (current === 255)
}
return {
type,
partialKey: Hex(Math.ceil(nNibles / 2))
.dec(bytes)
.slice(nNibles % 2 ? 3 : 2),
}
}
export const trieNodeDec = createDecoder((bytes): TrieNode => {
const header = getHeader(bytes)
const { type } = header
if (type === "Empty" || type === "Reserved") return header as TrieNode
if (type === "Leaf" || type === "LeafWithHash")
return {
...header,
value: allHex(bytes),
} as TrieNode
const bitmap = u16.dec(bytes)
const keys: string[] = []
for (let i = 0; i < 16; i++) if ((bitmap >> i) & 1) keys.push(i.toString(16))
let value: null | HexString = null
if (type === "BranchWithVal") value = varHex(bytes)
if (type === "BranchWithHash") value = hex32(bytes)
const result: any = {
...header,
children: Object.fromEntries(keys.map((key) => [key, varHex(bytes)])),
}
if (value !== null) result.value = value
return result
})
@@ -0,0 +1,72 @@
import { toHex } from "@pezkuwi/papi-utils"
import { createDecoder, type HexString } from "../codecs"
import { trieNodeDec } from "./node-decoder"
import { Blake2256 } from "@/hashes"
import { type ProofTrieNode } from "./types"
export const TrieNodeWithHash = (hasher: (input: Uint8Array) => Uint8Array) =>
createDecoder((input): ProofTrieNode => {
const hash = toHex(hasher(new Uint8Array(input.buffer)))
try {
return {
hash,
...trieNodeDec(input),
}
} catch {
// Sometimes the proofs include random raw-values which are not trie-nodes
return { type: "Raw", hash, value: "" }
}
})
export const validateProofs = <T extends HexString | Uint8Array>(
proofs: Array<T>,
hasher: (input: Uint8Array) => Uint8Array = Blake2256,
): { rootHash: HexString; proofs: Record<HexString, ProofTrieNode> } | null => {
const proofsList = proofs.map(TrieNodeWithHash(hasher))
const proofsRecord = Object.fromEntries(proofsList.map((p) => [p.hash, p]))
const hashes = proofsList.map((p) => p.hash)
const roots = new Set(hashes)
const setRawValue = (input: {
type: "Raw"
hash: HexString
value: HexString
}) => {
if (input.value) return
const val = proofs[hashes.indexOf(input.hash)!]
input.value = typeof val === "string" ? val : toHex(val)
}
proofsList.forEach((p) => {
if ("children" in p) {
Object.values(p.children).forEach((hash) => {
const child = proofsRecord[hash]
if (child) {
child.parent = p.hash
roots.delete(hash)
}
})
}
if (p.type === "BranchWithHash" || p.type === "LeafWithHash") {
const childHash = p.value
const child = proofsRecord[childHash]
if (!child) return
roots.delete(childHash)
if (child.type !== "Raw") {
Object.keys(child).forEach((k) => delete (child as any)[k])
;(child as any).type = "Raw"
child.hash = childHash
}
child.parent = p.hash
setRawValue(child as any)
}
if (p.type === "Raw") setRawValue(p)
})
return roots.size === 1
? { rootHash: roots.values().next().value!, proofs: proofsRecord }
: null
}
@@ -0,0 +1,55 @@
import { type HexString } from "../codecs"
export const TrieNodeHeaders = {
Leaf: "Leaf",
Branch: "Branch",
BranchWithVal: "BranchWithVal",
LeafWithHash: "LeafWithHash",
BranchWithHash: "BranchWithHash",
Empty: "Empty",
Reserved: "Reserved",
} as const
type TrieNodeHeaders = typeof TrieNodeHeaders
export type TrieNodeHeaderKey =
(typeof TrieNodeHeaders)[keyof typeof TrieNodeHeaders]
export type Nibble =
| "0"
| "1"
| "2"
| "3"
| "4"
| "5"
| "6"
| "7"
| "8"
| "9"
| "a"
| "b"
| "c"
| "d"
| "e"
| "f"
export type TrieNode = { partialKey: string } & (
| {
type: TrieNodeHeaders["Empty"] | TrieNodeHeaders["Reserved"]
}
| {
type: TrieNodeHeaders["Leaf"] | TrieNodeHeaders["LeafWithHash"]
value: HexString
}
| ({ children: Record<Nibble, HexString> } & (
| { type: TrieNodeHeaders["Branch"] }
| {
type:
| TrieNodeHeaders["BranchWithHash"]
| TrieNodeHeaders["BranchWithVal"]
value: HexString
}
))
)
export type ProofTrieNode = {
hash: HexString
parent?: HexString
} & (TrieNode | { type: "Raw"; value: HexString })
@@ -0,0 +1,93 @@
export type Enum<T extends {}> = {
[K in keyof T & string]: {
type: K
value: T[K]
}
}[keyof T & string]
export type EnumVariant<
T extends { type: string; value?: any },
K extends T["type"],
> = T & {
type: K
}
export type ExtractEnumValue<
T extends { type: string; value?: any },
K extends string,
> = EnumVariant<T, K>["value"]
type ValueArg<V> = undefined extends V ? [value?: V] : [value: V]
interface Discriminant {
is<T extends { type: string; value: any }, K extends T["type"]>(
value: T,
type: K,
): value is T & { type: K }
as<T extends { type: string; value: any }, K extends T["type"]>(
value: T,
type: K,
): ExtractEnumValue<T, K>
}
const discriminant: Discriminant = {
is<T extends { type: string; value: any }, K extends T["type"]>(
value: T,
type: K,
): value is T & { type: K } {
return value.type === type
},
as(value, type) {
if (type !== value.type)
throw new Error(
`Enum.as(enum, ${type}) used with actual type ${value.type}`,
)
return value
},
}
interface EnumFn extends Discriminant {
<T extends { type: string; value: any }, K extends T["type"]>(
type: K,
...[value]: ValueArg<ExtractEnumValue<T, K>>
): EnumVariant<T, K>
}
export const Enum: EnumFn = Object.assign((type: string, value?: any) => {
return {
type,
value,
} as any
}, discriminant)
// well-known enums
export type GetEnum<T extends Enum<any>> = {
[K in T["type"]]: (
...args: ExtractEnumValue<T, K> extends undefined
? []
: [value: ExtractEnumValue<T, K>]
) => EnumVariant<T, K>
}
export const _Enum = new Proxy(
{},
{
get(_, prop: string) {
return (value: string) => Enum(prop, value)
},
},
)
// type Bar = Enum<{
// Kaka: 1
// Bar: 2
// }>
// type FooInput = Enum<{
// foo: "foo" | undefined
// bar: Bar
// baz: number
// wtf: boolean
// }>
// declare function foo(foo: FooInput): void
// foo(Enum("bar", Enum("Bar", 2)))
// const InputEnum: GetEnum<FooInput> = null as any;
// InputEnum.bar(Enum('Bar', 2))
@@ -0,0 +1,34 @@
import { mergeUint8 } from "@pezkuwi/papi-utils"
import { Binary, compact, u16 } from "../codecs"
import { Blake2256 } from "../hashes"
const PREFIX = Binary.fromText("modlpy/utilisuba").asBytes()
export const getMultisigAccountId = ({
threshold,
signatories,
}: {
threshold: number
signatories: Uint8Array[]
}) => {
const sortedSignatories = sortMultisigSignatories(signatories)
const payload = mergeUint8([
PREFIX,
compact.enc(sortedSignatories.length),
...sortedSignatories,
u16.enc(threshold),
])
return Blake2256(payload)
}
export const sortMultisigSignatories = (signatories: Uint8Array[]) =>
signatories.slice().sort((a, b) => {
for (let i = 0; ; i++) {
const overA = i >= a.length
const overB = i >= b.length
if (overA && overB) return 0
else if (overA) return -1
else if (overB) return 1
else if (a[i] !== b[i]) return a[i] > b[i] ? 1 : -1
}
})
@@ -0,0 +1,93 @@
import { base58 } from "@scure/base"
import { blake2b } from "@noble/hashes/blake2.js"
const SS58_PREFIX = new TextEncoder().encode("SS58PRE")
const CHECKSUM_LENGTH = 2
export type SS58String = string & { __SS58String?: unknown }
export type SS58AddressInfo =
| { isValid: false }
| { isValid: true; ss58Format: number; publicKey: Uint8Array }
export const getSs58AddressInfo = (address: SS58String): SS58AddressInfo => {
try {
const decoded = base58.decode(address)
const prefixBytes = decoded.subarray(0, decoded[0] & 0b0100_0000 ? 2 : 1)
const publicKey = decoded.subarray(
prefixBytes.length,
decoded.length - CHECKSUM_LENGTH,
)
const checksum = decoded.subarray(prefixBytes.length + publicKey.length)
const expectedChecksum = blake2b(
Uint8Array.of(...SS58_PREFIX, ...prefixBytes, ...publicKey),
{
dkLen: 64,
},
).subarray(0, CHECKSUM_LENGTH)
const isChecksumValid =
checksum[0] === expectedChecksum[0] && checksum[1] === expectedChecksum[1]
if (!isChecksumValid) return { isValid: false }
return {
isValid: true,
ss58Format: prefixBytesToNumber(prefixBytes),
publicKey: publicKey.slice(),
}
} catch (_) {
return { isValid: false }
}
}
const prefixBytesToNumber = (bytes: Uint8Array) => {
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
return dv.byteLength === 1 ? dv.getUint8(0) : dv.getUint16(0)
}
const withSs58Cache = (fn: (publicKey: Uint8Array) => SS58String) => {
let cache: Record<number, any> = {}
let activityCount = 0
let latestCount = 0
const checkActivity = () => {
if (activityCount === latestCount) {
cache = {}
activityCount = latestCount = 0
} else {
latestCount = activityCount
setTimeout(checkActivity, 0)
}
}
return (publicKey: Uint8Array): SS58String => {
if (++activityCount === 1) checkActivity()
let entry = cache
const lastIdx = publicKey.length - 1
for (let i = 0; i <= lastIdx; i++) entry = entry[publicKey[i]] ||= {}
return (entry[publicKey[lastIdx]] ||= fn(publicKey))
}
}
export const fromBufferToBase58 = (ss58Format: number) => {
const prefixBytes =
ss58Format < 64
? Uint8Array.of(ss58Format)
: Uint8Array.of(
((ss58Format & 0b0000_0000_1111_1100) >> 2) | 0b0100_0000,
(ss58Format >> 8) | ((ss58Format & 0b0000_0000_0000_0011) << 6),
)
return withSs58Cache((publicKey: Uint8Array): SS58String => {
const checksum = blake2b(
Uint8Array.of(...SS58_PREFIX, ...prefixBytes, ...publicKey),
{
dkLen: 64,
},
).subarray(0, CHECKSUM_LENGTH)
return base58.encode(
Uint8Array.of(...prefixBytes, ...publicKey, ...checksum),
)
})
}
+12 -1
View File
@@ -1,4 +1,15 @@
# @polkadot-api/merkleize-metadata
# @pezkuwi/merkleize-metadata
**Source:** `upstream-src/` is [`@polkadot-api/merkleize-metadata`](https://www.npmjs.com/package/@polkadot-api/merkleize-metadata/v/1.1.22) 1.1.22 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 `upstream-src/`.
The upstream README follows.
---
## @polkadot-api/merkleize-metadata
This TS package provides utils for the merkleization of [`frame_metadata`](https://docs.rs/frame-metadata/latest/frame_metadata/) as described in
[RFC78](https://polkadot-fellows.github.io/RFCs/approved/0078-merkleized-metadata.html).
@@ -0,0 +1,152 @@
import {
Bytes,
Codec,
CodecType,
Option,
ScaleEnum,
Struct,
Tuple,
V14Lookup,
Vector,
_void,
bool,
compact,
compactNumber,
extrinsicFormat,
str,
u16,
u32,
u8,
} from "@pezkuwi/bizinikiwi-bindings"
const extraInfoInner = {
specVersion: u32,
specName: str,
base58Prefix: u16,
decimals: u8,
tokenSymbol: str,
}
export const extraInfo = Struct(extraInfoInner)
export type ExtraInfo = CodecType<typeof extraInfo>
export const hash = Bytes(32)
export const metadataDigest = ScaleEnum({
V0: _void,
V1: Struct({
typeInformationTreeRoot: hash,
extrinsicMetadataHash: hash,
...extraInfoInner,
}),
})
export type MetadataDigest = CodecType<typeof metadataDigest>
export const scalePrimitive = ScaleEnum({
bool: _void,
char: _void,
str: _void,
u8: _void,
u16: _void,
u32: _void,
u64: _void,
u128: _void,
u256: _void,
i8: _void,
i16: _void,
i32: _void,
i64: _void,
i128: _void,
i256: _void,
})
export type ScalePrimitive = CodecType<typeof scalePrimitive>
export const typeRef = ScaleEnum({
bool: _void,
char: _void,
str: _void,
u8: _void,
u16: _void,
u32: _void,
u64: _void,
u128: _void,
u256: _void,
i8: _void,
i16: _void,
i32: _void,
i64: _void,
i128: _void,
i256: _void,
compactU8: _void,
compactU16: _void,
compactU32: _void,
compactU64: _void,
compactU128: _void,
compactU256: _void,
void: _void,
perId: compactNumber,
})
export type TypeRef = CodecType<typeof typeRef>
const field = Struct({
name: Option(str),
ty: typeRef,
typeName: Option(str),
})
export const typeDef = ScaleEnum({
composite: Vector(field),
enumeration: Struct({
name: str,
fields: Vector(field),
index: compactNumber,
}),
sequence: typeRef,
array: Struct({
len: u32,
typeParam: typeRef,
}),
tuple: Vector(typeRef),
bitSequence: Struct({
numBytes: u8,
leastSignificantBitFirst: bool,
}),
})
export type TypeDef = CodecType<typeof typeDef>
export const lookupType = Struct({
path: Vector(str),
typeDef: typeDef,
typeId: compactNumber,
})
export type LookupEntry = CodecType<typeof lookupType>
export type Lookup = Array<LookupEntry>
export const lookup: Codec<Lookup> = Vector(lookupType)
export const extrinsicMetadata = Struct({
version: u8,
addressTy: typeRef,
callTy: typeRef,
signatureTy: typeRef,
signedExtensions: Vector(
Struct({
identifier: str,
includedInExtrinsic: typeRef,
includedInSignedData: typeRef,
}),
),
})
export type ExtrinsicMetadata = CodecType<typeof extrinsicMetadata>
export type LookupValue = V14Lookup[number]
export const extrinsicDec = Tuple.dec(
compact[1],
extrinsicFormat[1],
Bytes(Infinity)[1],
)
export const proof = Struct({
leaves: lookup,
leafIdxs: Vector(u32),
proofs: Vector(hash),
extrinsic: extrinsicMetadata,
info: extraInfo,
})
export type Proof = CodecType<typeof proof>
@@ -0,0 +1,135 @@
import {
HexString,
_void,
compact,
createDecoder,
i128,
i16,
i256,
i32,
i64,
i8,
str,
u128,
u16,
u256,
u32,
u64,
u8,
} from "@pezkuwi/bizinikiwi-bindings"
import { Lookup, TypeDef, TypeRef } from "./codecs"
const typeRefDecoders = {
bool: u8,
char: u8,
str,
u8,
u16,
u32,
u64,
u128,
u256,
i8,
i16,
i32,
i64,
i128,
i256,
void: _void,
compactU8: compact,
compactU16: compact,
compactU32: compact,
compactU64: compact,
compactU128: compact,
compactU256: compact,
}
const innerDecodeAndCollect = (
input: Uint8Array,
typeRef: TypeRef,
idToLookups: Map<number, Array<number>>,
lookup: Lookup,
collected: Set<number>,
) => {
if (typeRef.tag !== "perId") {
typeRefDecoders[typeRef.tag][1](input)
return
}
const handleTypeRef = (typeRef: TypeRef) => {
innerDecodeAndCollect(input, typeRef, idToLookups, lookup, collected)
}
const lookupIdxs = idToLookups.get(typeRef.value)!
const [currentIdx] = lookupIdxs
const current = lookup[currentIdx]
if (lookupIdxs.length === 1) collected.add(currentIdx)
switch (current.typeDef.tag) {
case "enumeration": {
const selectedIdx = u8.dec(input)
const [selected, collectedIdx] = lookupIdxs
.map(
(lookupIdx) =>
[lookup[lookupIdx].typeDef, lookupIdx] as [
TypeDef & { tag: "enumeration" },
number,
],
)
.find(([x]) => x.value.index === selectedIdx)!
collected.add(collectedIdx)
selected.value.fields.forEach(({ ty }) => {
handleTypeRef(ty)
})
break
}
case "sequence": {
const len = compact.dec(input)
for (let i = 0; i < len; i++) handleTypeRef(current.typeDef.value)
break
}
case "array": {
for (let i = 0; i < current.typeDef.value.len; i++)
handleTypeRef(current.typeDef.value.typeParam)
break
}
case "composite": {
current.typeDef.value.forEach((x) => {
handleTypeRef(x.ty)
})
break
}
case "tuple": {
current.typeDef.value.forEach(handleTypeRef)
break
}
case "bitSequence":
throw new Error("bitSequence is not supported")
}
}
export const decodeAndCollectKnownLeafs = (
data: Uint8Array | HexString,
typeRefs: Array<TypeRef>,
lookup: Lookup,
): Array<number> => {
let input: Uint8Array<ArrayBufferLike> = new Uint8Array()
createDecoder((_input) => {
input = _input
})(data)
const idToLookups = new Map<number, number[]>()
lookup.forEach((lookup, idx) => {
const arr = idToLookups.get(lookup.typeId)
if (arr) arr.push(idx)
else idToLookups.set(lookup.typeId, [idx])
})
const result = new Set<number>()
typeRefs.forEach((typeRef) => {
innerDecodeAndCollect(input, typeRef, idToLookups, lookup, result)
})
return [...result].sort((a, b) => a - b)
}
@@ -0,0 +1,60 @@
import { UnifiedMetadata } from "@pezkuwi/bizinikiwi-bindings"
import { LookupValue } from "./codecs"
export const getAccessibleTypes = (
metadata: UnifiedMetadata<15 | 16>,
definitions: Map<number, LookupValue>,
): Map<number, number> => {
const types = new Set<number>()
const collectTypesFromId = (id: number) => {
if (types.has(id)) return
const { tag, value } = definitions.get(id)!.def
switch (tag) {
case "composite":
if (!value.length) break
types.add(id)
value.forEach(({ type }) => {
collectTypesFromId(type)
})
break
case "variant":
if (!value.length) break
types.add(id)
value.forEach(({ fields }) => {
fields.forEach(({ type }) => {
collectTypesFromId(type)
})
})
break
case "tuple":
if (!value.length) break
types.add(id)
value.forEach(collectTypesFromId)
break
case "sequence":
types.add(id)
collectTypesFromId(value)
break
case "array":
types.add(id)
collectTypesFromId(value.type)
break
case "bitSequence": // bitSequence inner types are not stored
types.add(id)
// primitive and compact are not stored
}
}
collectTypesFromId(metadata.extrinsic.call)
collectTypesFromId(metadata.extrinsic.address)
collectTypesFromId(metadata.extrinsic.signature)
metadata.extrinsic.signedExtensions.forEach(({ type, additionalSigned }) => {
collectTypesFromId(type)
collectTypesFromId(additionalSigned)
})
const sortedTypes = [...types].sort((a, b) => a - b)
return new Map(sortedTypes.map((value, idx) => [value, idx]))
}
@@ -0,0 +1,119 @@
import { Lookup, LookupValue, ScalePrimitive, TypeDef, TypeRef } from "./codecs"
const bitSequenceBytes = {
u8: 1,
u16: 2,
u32: 4,
u64: 8,
}
const constructTypeDef = (
definitions: Map<number, LookupValue>,
getTypeRef: (frameId: number) => TypeRef,
getPrimitive: (frameId: number) => ScalePrimitive["tag"] | null,
frameId: number,
): TypeDef[] => {
const {
def: { tag, value },
} = definitions.get(frameId)!
switch (tag) {
case "composite":
return [
{
tag,
value: value.map((f) => ({
name: f.name,
typeName: f.typeName,
ty: getTypeRef(f.type),
})),
},
]
case "variant": {
return value.map((v) => ({
tag: "enumeration",
value: {
name: v.name,
index: v.index,
fields: v.fields.map((f) => ({
name: f.name,
typeName: f.typeName,
ty: getTypeRef(f.type),
})),
},
}))
}
case "sequence":
return [
{
tag,
value: getTypeRef(value),
},
]
case "array":
return [
{
tag,
value: {
len: value.len,
typeParam: getTypeRef(value.type),
},
},
]
case "tuple":
return [
{
tag,
value: value.map(getTypeRef),
},
]
case "bitSequence": {
const primitive = getPrimitive(value.bitStoreType)
const numBytes = bitSequenceBytes[primitive as "u8"]
if (!numBytes) throw new Error("Invalid primitive for BitSequence")
const storeOrderPath = definitions.get(value.bitOrderType)!.path
const leastSignificantBitFirst = storeOrderPath.includes("Lsb0")
if (!leastSignificantBitFirst && !storeOrderPath.includes("Msb0"))
throw new Error("BitOrderType not recognized")
return [
{
tag: "bitSequence",
value: { numBytes, leastSignificantBitFirst },
},
]
}
}
throw new Error(`FrameId(${frameId}) should have been filtered out`)
}
export const getLookup = (
definitions: Map<number, LookupValue>,
accessibleTypes: Map<number, number>,
getTypeRef: (frameId: number) => TypeRef,
getPrimitive: (frameId: number) => ScalePrimitive["tag"] | null,
) => {
const typeTree: Lookup = []
;[...accessibleTypes.entries()].forEach(([frameId, typeId]) => {
const { path } = definitions.get(frameId)!
constructTypeDef(definitions, getTypeRef, getPrimitive, frameId).forEach(
(typeDef) => {
typeTree.push({
path,
typeId,
typeDef,
})
},
)
})
typeTree.sort((a, b) => {
if (a.typeId !== b.typeId) return a.typeId - b.typeId // in general
// should only happen for variants
if (a.typeDef.tag !== "enumeration" || b.typeDef.tag !== "enumeration")
throw new Error("Found two types with same id")
return a.typeDef.value.index - b.typeDef.value.index
})
return typeTree
}
@@ -0,0 +1,18 @@
import {
decAnyMetadata,
UnifiedMetadata,
type HexString,
unifyMetadata,
} from "@pezkuwi/bizinikiwi-bindings"
export const getMetadata = (
input: Uint8Array | HexString,
): UnifiedMetadata<15 | 16> => {
try {
const metadata = unifyMetadata(decAnyMetadata(input))
if (metadata.version <= 14) throw new Error("Wrong metadata version")
return metadata as UnifiedMetadata<15 | 16>
} catch (e) {
throw e || new Error("Unable to decode metadata")
}
}
@@ -0,0 +1,294 @@
import {
Blake3256,
HexString,
compact,
u32,
} from "@pezkuwi/bizinikiwi-bindings"
import {
ExtraInfo,
ExtrinsicMetadata,
LookupValue,
MetadataDigest,
ScalePrimitive,
TypeRef,
extraInfo,
extrinsicMetadata,
lookupType,
metadataDigest,
extrinsicDec,
} from "./codecs"
import { getAccessibleTypes } from "./get-accessible-types"
import { getLookup } from "./get-lookup"
import { getMetadata } from "./get-metadata"
import { compactTypeRefs, toBytes } from "./utils"
import { decodeAndCollectKnownLeafs } from "./decode-and-collect"
import { getProofData } from "./proof"
import { getDynamicBuilder, getLookupFn } from "@pezkuwi/metadata-builders"
import { mergeUint8 } from "@pezkuwi/papi-utils"
export interface MetadataMerkleizer {
/**
* @returns Digest value of the metadata (aka its merkleized root-hash)
*/
digest: () => Uint8Array
/**
* Get proof for an `Extrinsic`.
*
* @param transaction Encoded `Extrinsic`, both signed or unsigned.
* @param txAdditionalSigned Optionally collect types for given
* `additionalSigned` part of signedExtensions.
* @returns Encoded `Proof`
*/
getProofForExtrinsic: (
transaction: Uint8Array | HexString,
txAdditionalSigned?: Uint8Array | HexString,
) => Uint8Array
/**
* Get proof for `ExtrinsicPayload` parts.
*
* @param callData Call data of the transaction. It has to be
* bare; i.e.
* without prepended compact length.
* @param includedInExtrinsic Extra part of the signed extensions, all of
* them concatenated.
* @param includedInSignedData Additional signed part of the signed
* extensions, all of them concatenated.
* @returns Encoded `Proof`
*/
getProofForExtrinsicParts: (
callData: Uint8Array | HexString,
includedInExtrinsic: Uint8Array | HexString,
includedInSignedData: Uint8Array | HexString,
) => Uint8Array
/**
* Get proof for `ExtrinsicPayload`.
*
* @param extrinsicPayload Call data, extra part of signedExtensions and
* additional signed part of signedExtensions
* concatenated. It has to be bare; i.e. without
* prepended compact length.
* @returns Encoded `Proof`
*/
getProofForExtrinsicPayload: (
extrinsicPayload: Uint8Array | HexString,
) => Uint8Array
}
const assertExpected = <T>(name: string, expected: T, received?: T): void => {
if (received != null && received !== expected)
throw new Error(
`${name} not expected. Received ${received} expected ${expected}`,
)
}
export const merkleizeMetadata = (
metadataBytes: Uint8Array | HexString,
{
decimals,
tokenSymbol,
...hinted
}: { decimals: number; tokenSymbol: string } & Partial<ExtraInfo>,
): MetadataMerkleizer => {
const metadata = getMetadata(metadataBytes)
const checkedVersion = metadata.extrinsic.version.includes(4) ? 4 : null
if (checkedVersion == null) throw new Error("Only extrinsic v4 is supported")
const { ss58Prefix, buildDefinition } = getDynamicBuilder(
getLookupFn(metadata),
)
if (ss58Prefix == null) throw new Error("SS58 prefix not found in metadata")
assertExpected("SS58 prefix", ss58Prefix, hinted.base58Prefix)
const version = metadata.pallets
.find((x) => x.name === "System")
?.constants.find((x) => x.name === "Version")
if (version == null) throw new Error("System.Version constant not found")
const { spec_name: specName, spec_version: specVersion } = buildDefinition(
version.type,
).dec(version.value)
if (typeof specName !== "string" || typeof specVersion !== "number")
throw new Error("Spec name or spec version not found")
assertExpected("Spec name", specName, hinted.specName)
assertExpected("Spec version", specVersion, hinted.specVersion)
const info: ExtraInfo = {
decimals,
tokenSymbol,
specVersion,
specName,
base58Prefix: ss58Prefix,
}
const definitions = new Map<number, LookupValue>(
metadata.lookup.map((value) => [value.id, value]),
)
const accessibleTypes = getAccessibleTypes(metadata, definitions)
const getPrimitive = (frameId: number): ScalePrimitive["tag"] | null => {
const {
def: { tag, value },
} = definitions.get(frameId)!
if (tag === "primitive") return value.tag
if ((tag !== "composite" && tag !== "tuple") || value.length > 1)
throw new Error("The provided definition doesn't map to a primitive")
return value.length === 0
? null // signals `void`
: getPrimitive(tag === "tuple" ? value[0] : value[0].type)
}
const getTypeRef = (frameId: number): TypeRef => {
const { def } = definitions.get(frameId)!
if (def.tag === "primitive") return { tag: def.value.tag, value: undefined }
if (def.tag === "compact") {
const primitive = getPrimitive(def.value)
const tag = compactTypeRefs[primitive as "u8"]
if (!tag) throw new Error("Invalid primitive for Compact")
return { tag, value: undefined }
}
return accessibleTypes.has(frameId)
? { tag: "perId", value: accessibleTypes.get(frameId)! }
: { tag: "void", value: undefined }
}
const extrinsic: ExtrinsicMetadata = {
version: checkedVersion,
addressTy: getTypeRef(metadata.extrinsic.address),
callTy: getTypeRef(metadata.extrinsic.call),
signatureTy: getTypeRef(metadata.extrinsic.signature),
signedExtensions: metadata.extrinsic.signedExtensions.map((se) => ({
identifier: se.identifier,
includedInExtrinsic: getTypeRef(se.type),
includedInSignedData: getTypeRef(se.additionalSigned),
})),
}
const lookup = getLookup(
definitions,
accessibleTypes,
getTypeRef,
getPrimitive,
)
const lookupEncoded = lookup.map(lookupType.enc)
let hashTree: Array<Uint8Array> | undefined
const getHashTree = (): Array<Uint8Array> => {
if (hashTree) return hashTree
if (!lookupEncoded.length) return (hashTree = [new Uint8Array(32).fill(0)])
hashTree = new Array(lookupEncoded.length * 2 - 1)
let leavesStartIdx = lookupEncoded.length - 1
for (let i = 0; i < lookupEncoded.length; i++)
hashTree[leavesStartIdx + i] = Blake3256(lookupEncoded[i])
for (let i = hashTree.length - 2; i > 0; i -= 2)
hashTree[(i - 1) / 2] = Blake3256(
mergeUint8([hashTree[i], hashTree[i + 1]]),
)
return hashTree
}
let digested: undefined | Uint8Array
const digest = () => {
if (digested) return digested
const rootLookupHash = getHashTree()[0]
const digest: MetadataDigest = {
tag: "V1",
value: {
typeInformationTreeRoot: rootLookupHash,
extrinsicMetadataHash: Blake3256(extrinsicMetadata.enc(extrinsic)),
...info,
},
}
return (digested = Blake3256(metadataDigest.enc(digest)))
}
const generateProof = (knownIndexes: number[]) => {
const proofData = getProofData(lookupEncoded, knownIndexes)
const hashTree = getHashTree()
const proofs = proofData.proofIdxs.map((idx) => hashTree[idx])
return mergeUint8([
compact.enc(proofData.leaves.length),
...proofData.leaves,
compact.enc(proofData.leafIdxs.length),
...proofData.leafIdxs.map((x) => u32.enc(x)),
compact.enc(proofs.length),
...proofs,
extrinsicMetadata.enc(extrinsic),
extraInfo.enc(info),
])
}
const getProofForExtrinsicPayload = (
extrinsicPayload: Uint8Array | HexString,
) => {
const typeRefs: Array<TypeRef> = [
extrinsic.callTy,
...extrinsic.signedExtensions.map((x) => x.includedInExtrinsic),
...extrinsic.signedExtensions.map((x) => x.includedInSignedData),
]
return generateProof(
decodeAndCollectKnownLeafs(extrinsicPayload, typeRefs, lookup),
)
}
const getProofForExtrinsicParts = (
callData: Uint8Array | HexString,
includedInExtrinsic: Uint8Array | HexString,
includedInSignedData: Uint8Array | HexString,
) => {
const bytes = mergeUint8(
[callData, includedInExtrinsic, includedInSignedData].map(toBytes),
)
return getProofForExtrinsicPayload(bytes)
}
const getProofForExtrinsic = (
transaction: Uint8Array | HexString,
txAdditionalSigned?: Uint8Array | HexString,
) => {
let [, { version, type }, bytes] = extrinsicDec(transaction)
if (version !== extrinsic.version)
throw new Error("Incorrect extrinsic version")
const typeRefs: TypeRef[] =
type === "signed"
? [
extrinsic.addressTy,
extrinsic.signatureTy,
...extrinsic.signedExtensions.map((x) => x.includedInExtrinsic),
extrinsic.callTy,
]
: [extrinsic.callTy]
if (txAdditionalSigned) {
bytes = mergeUint8([bytes, toBytes(txAdditionalSigned)])
typeRefs.push(
...extrinsic.signedExtensions.map((x) => x.includedInSignedData),
)
}
return generateProof(decodeAndCollectKnownLeafs(bytes, typeRefs, lookup))
}
return {
digest,
getProofForExtrinsic,
getProofForExtrinsicParts,
getProofForExtrinsicPayload,
}
}
@@ -0,0 +1,57 @@
const getLevelFromIdx = (idx: number) => Math.log2(idx + 1) | 0
const getAncestorIdx = (from: number, nLevels: number) =>
((from + 1) >> nLevels) - 1
export function getProofData(
leaves: Array<Uint8Array>,
knownLeavesIdxs: Array<number>,
) {
const knownLeaves = knownLeavesIdxs.map((idx) => leaves[idx])
const startingIdx = leaves.length - 1
const leafIdxs = knownLeavesIdxs.map((idx) => startingIdx + idx)
const proofIdxs: Array<number> = []
if (leafIdxs.length) {
const nLevels = getLevelFromIdx(leafIdxs.at(-1)!)
const splitPosition = Math.pow(2, nLevels) - 1
const splitIdx = leafIdxs.findIndex((x) => x >= splitPosition)
if (splitIdx > 0) {
leafIdxs.unshift(...leafIdxs.splice(splitIdx))
knownLeaves.unshift(...knownLeaves.splice(splitIdx))
}
}
let targetIdx = 0
const traverse = (nodeIdx: number): void => {
if (targetIdx === leafIdxs.length) {
proofIdxs.push(nodeIdx)
return
}
const target = leafIdxs[targetIdx]
if (target === nodeIdx) {
++targetIdx
return
}
const currentLevel = getLevelFromIdx(nodeIdx)
const targetLevel = getLevelFromIdx(target)
if (nodeIdx !== getAncestorIdx(target, targetLevel - currentLevel)) {
proofIdxs.push(nodeIdx)
return
}
const leftSon = 2 * nodeIdx + 1
traverse(leftSon)
traverse(leftSon + 1)
}
traverse(0)
return {
leaves: knownLeaves,
leafIdxs,
proofIdxs,
}
}
@@ -0,0 +1,15 @@
import { type HexString } from "@pezkuwi/bizinikiwi-bindings"
import { fromHex } from "@pezkuwi/papi-utils"
export const toBytes = (input: Uint8Array | HexString) =>
typeof input === "string" ? fromHex(input) : input
export const compactTypeRefs = {
null: "void" as const,
u8: "compactU8" as const,
u16: "compactU16" as const,
u32: "compactU32" as const,
u64: "compactU64" as const,
u128: "compactU128" as const,
u256: "compactU256" as const,
}
+12 -1
View File
@@ -1,4 +1,15 @@
# @polkadot-api/metadata-builders
# @pezkuwi/metadata-builders
**Source:** `upstream-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 `upstream-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
}
+12 -1
View File
@@ -1 +1,12 @@
# @polkadot-api/utils
# @pezkuwi/papi-utils
**Source:** `upstream-src/` is [`@polkadot-api/utils`](https://www.npmjs.com/package/@polkadot-api/utils/v/0.2.0) 0.2.0 from
[polkadot-api](https://github.com/polkadot-api/polkadot-api) (MIT), with imports of
`@polkadot-api/*` renamed to `@pezkuwi/*`. `dist/` is its build;
`scripts/check-papi-sources.mjs` fails CI unless the source embedded in `dist/` equals `upstream-src/`.
The upstream README follows.
---
## @polkadot-api/utils
@@ -0,0 +1,6 @@
export class AbortError extends Error {
constructor() {
super("Abort Error")
this.name = "AbortError"
}
}
@@ -0,0 +1,8 @@
export function filterObject<K extends string | number | symbol, I>(
input: Record<K, I>,
filterFn: (i: I, k: K) => boolean,
): Record<K, I> {
return Object.fromEntries(
Object.entries(input).filter(([key, value]: any) => filterFn(value, key)),
) as any
}
+57
View File
@@ -0,0 +1,57 @@
// https://jsben.ch/uWZw3
const HEX_STR = "0123456789abcdef"
export function toHex(bytes: Uint8Array): string {
const result = new Array<string>(bytes.length + 1)
result[0] = "0x"
for (let i = 0; i < bytes.length; ) {
const b = bytes[i++]
result[i] = HEX_STR[b >> 4] + HEX_STR[b & 15]
}
return result.join("")
}
// https://jsben.ch/URe1X
const HEX_MAP: Record<string, number> = {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9,
a: 10,
b: 11,
c: 12,
d: 13,
e: 14,
f: 15,
A: 10,
B: 11,
C: 12,
D: 13,
E: 14,
F: 15,
}
export function fromHex(hexString: string): Uint8Array {
const isOdd = hexString.length % 2
const base = (hexString[1] === "x" ? 2 : 0) + isOdd
const nBytes = (hexString.length - base) / 2 + isOdd
const bytes = new Uint8Array(nBytes)
if (isOdd) bytes[0] = 0 | HEX_MAP[hexString[2]]
for (let i = 0; i < nBytes; ) {
const idx = base + i * 2
const a = HEX_MAP[hexString[idx]]
const b = HEX_MAP[hexString[idx + 1]]
bytes[isOdd + i++] = (a << 4) | b
}
return bytes
}
@@ -0,0 +1,11 @@
export const jsonPrint = (value: any, indent = 2) =>
JSON.stringify(
value,
(_, v) =>
typeof v === "bigint"
? `${v}n`
: typeof v === "object" && typeof v?.asHex === "function"
? v.asHex()
: v,
indent,
)
@@ -0,0 +1,29 @@
export function mapObject<K extends string | number | symbol, I, O>(
input: Record<K, I>,
mapper: (i: I, k: K) => O,
): Record<K, O>
export function mapObject<K extends string | number | symbol, I, O>(
input: Record<K, I>,
mapper: (i: I, k?: K) => O,
): Record<K, O> {
return Object.fromEntries(
Object.entries(input).map(
([key, value]: any) => [key, mapper(value, key)] as const,
),
) as any
}
export type StringRecord<T> = {
[Sym: symbol]: never
[Num: number]: never
[Str: string]: T
}
export const mapStringRecord = <I, O>(
input: StringRecord<I>,
mapper: (value: I, key: string) => O,
): StringRecord<O> =>
Object.fromEntries(
Object.entries(input).map(([key, value]) => [key, mapper(value, key)]),
) as StringRecord<O>
@@ -0,0 +1,25 @@
// TODO: deprecate spread overload in papi v2
interface MergeUint8 {
/**
* @deprecated This overload will be removed in PAPI v2. Migrate as
* follows:
* mergeUint8(arr1, arr2) => mergeUint8([arr1, arr2])
*/
(...inputs: Array<Uint8Array>): Uint8Array
(inputs: Array<Uint8Array>): Uint8Array
}
export const mergeUint8: MergeUint8 = (...i) => {
const inputs = (Array.isArray(i[0]) ? i[0] : i) as Uint8Array[]
const totalLen = inputs.reduce((acc, a) => acc + a.byteLength, 0)
const result = new Uint8Array(totalLen)
for (let idx = 0, at = 0; idx < inputs.length; idx++) {
const current = inputs[idx]
result.set(current, at)
at += current.byteLength
}
return result
}
+1
View File
@@ -0,0 +1 @@
export const noop: () => void = Function.prototype as any
+73
View File
@@ -0,0 +1,73 @@
// Copyright 2017-2026 @pezkuwi/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
// papi-utils, bizinikiwi-bindings, metadata-builders and merkleize-metadata
// ship committed dist/ builds of upstream polkadot-api packages. Every source
// map in dist/ embeds the TypeScript it was compiled from; this check fails
// unless that embedded source is exactly the upstream-src/ committed beside it,
// so a published dist/ can only come from source held in this repository.
// (Not src/: the pezkuwi-dev build treats any package with src/ as its own and
// requires our license header on every file.)
import fs from 'node:fs';
import path from 'node:path';
const PACKAGES = ['papi-utils', 'bizinikiwi-bindings', 'metadata-builders', 'merkleize-metadata'];
const failures = [];
function walk (dir) {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
const p = path.join(dir, e.name);
return e.isDirectory() ? walk(p) : [p];
});
}
for (const name of PACKAGES) {
const pkg = path.join('packages', name);
const srcDir = path.join(pkg, 'upstream-src');
const embedded = new Map();
for (const map of walk(path.join(pkg, 'dist')).filter((f) => f.endsWith('.map'))) {
const { sources = [], sourcesContent = [] } = JSON.parse(fs.readFileSync(map, 'utf8'));
sources.forEach((source, i) => {
const resolved = path.normalize(path.join(path.dirname(map), source));
const rel = path.join('upstream-src', resolved.slice(resolved.indexOf(`${path.sep}src${path.sep}`) + 5));
const content = sourcesContent[i];
if (content == null) {
failures.push(`${map}: no embedded source for ${source}`);
} else if (embedded.has(rel) && embedded.get(rel) !== content) {
failures.push(`${map}: ${rel} differs between source maps`);
} else {
embedded.set(rel, content);
}
});
}
const committed = new Set(walk(srcDir).map((f) => path.relative(pkg, f)));
for (const [rel, content] of embedded) {
const file = path.join(pkg, rel);
if (!committed.has(rel)) {
failures.push(`${file}: in dist/ source maps but not committed`);
} else if (fs.readFileSync(file, 'utf8') !== content) {
failures.push(`${file}: differs from the source embedded in dist/`);
}
committed.delete(rel);
}
for (const rel of committed) {
failures.push(`${path.join(pkg, rel)}: committed but not part of the dist/ build`);
}
console.log(`${name}: ${embedded.size} source files match dist/`);
}
if (failures.length) {
console.error(failures.join('\n'));
process.exit(1);
}