diff --git a/common/changes/@subsquid/scale-type-system/is-storage-exists_2024-07-25-20-24.json b/common/changes/@subsquid/scale-type-system/is-storage-exists_2024-07-25-20-24.json new file mode 100644 index 000000000..222a7bcc8 --- /dev/null +++ b/common/changes/@subsquid/scale-type-system/is-storage-exists_2024-07-25-20-24.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@subsquid/scale-type-system", + "comment": "export more utility types", + "type": "minor" + } + ], + "packageName": "@subsquid/scale-type-system" +} \ No newline at end of file diff --git a/common/changes/@subsquid/substrate-runtime/is-storage-exists_2024-07-25-20-24.json b/common/changes/@subsquid/substrate-runtime/is-storage-exists_2024-07-25-20-24.json new file mode 100644 index 000000000..db1a10b73 --- /dev/null +++ b/common/changes/@subsquid/substrate-runtime/is-storage-exists_2024-07-25-20-24.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@subsquid/substrate-runtime", + "comment": "add `.hasStorage()` method", + "type": "minor" + } + ], + "packageName": "@subsquid/substrate-runtime" +} \ No newline at end of file diff --git a/common/changes/@subsquid/substrate-typegen/is-storage-exists_2024-07-25-20-24.json b/common/changes/@subsquid/substrate-typegen/is-storage-exists_2024-07-25-20-24.json new file mode 100644 index 000000000..02a0eed50 --- /dev/null +++ b/common/changes/@subsquid/substrate-typegen/is-storage-exists_2024-07-25-20-24.json @@ -0,0 +1,15 @@ +{ + "changes": [ + { + "packageName": "@subsquid/substrate-typegen", + "comment": "add `.at()` method on generated classes", + "type": "minor" + }, + { + "packageName": "@subsquid/substrate-typegen", + "comment": "add `index.ts` to pallet modules", + "type": "minor" + }, + ], + "packageName": "@subsquid/substrate-typegen" +} \ No newline at end of file diff --git a/substrate/scale-type-system/src/dsl.ts b/substrate/scale-type-system/src/dsl.ts index b91a94afb..5ea441607 100644 --- a/substrate/scale-type-system/src/dsl.ts +++ b/substrate/scale-type-system/src/dsl.ts @@ -18,11 +18,11 @@ import { UnknownType } from './types/primitives' import {GetStructType, StructType} from './types/struct' -import {TupleType} from './types/tuple' +import {TupleType, GetTupleType} from './types/tuple' import {UnionType} from './types/union' -export {GetType, ExternalEnum, ValueCase} +export {GetType, ExternalEnum, ValueCase, GetEnumType, GetTupleType, GetStructType} const numberType = new NumberType() diff --git a/substrate/scale-type-system/src/types/tuple.ts b/substrate/scale-type-system/src/types/tuple.ts index 3f2be1482..49b898bf0 100644 --- a/substrate/scale-type-system/src/types/tuple.ts +++ b/substrate/scale-type-system/src/types/tuple.ts @@ -5,9 +5,7 @@ import {BaseType, ScaleType, Type, TypeChecker} from '../type-checker' import {GetType} from '../type-util' -export type GetTupleType = { - [I in keyof T]: GetType -} +export type GetTupleType = T extends readonly [infer A, ...infer R] ? [GetType, ...GetTupleType] : [] export class TupleType extends BaseType> { diff --git a/substrate/substrate-runtime/src/runtime/runtime.ts b/substrate/substrate-runtime/src/runtime/runtime.ts index 87c989819..2355ec350 100644 --- a/substrate/substrate-runtime/src/runtime/runtime.ts +++ b/substrate/substrate-runtime/src/runtime/runtime.ts @@ -424,6 +424,11 @@ export class Runtime { return this.checkType(def.type, ty) } + hasStorage(name: QualifiedName): boolean { + let qn = parseQualifiedName(name) + return !!this.description.storage[qn[0]]?.items[qn[1]] + } + checkStorageType( name: QualifiedName, modifier: StorageItem['modifier'] | StorageItem['modifier'][], diff --git a/substrate/substrate-typegen/src/support.ts b/substrate/substrate-typegen/src/support.ts index 456dfc2fb..e80d83563 100644 --- a/substrate/substrate-typegen/src/support.ts +++ b/substrate/substrate-typegen/src/support.ts @@ -7,6 +7,11 @@ import assert from 'assert' export {sts, Bytes, BitSequence, Option, Result} +type Simplify = { + [K in keyof T]: T[K] +} & {} + + export interface RuntimeCtx { _runtime: Runtime } @@ -32,6 +37,71 @@ interface Call { } +export class UnknownVersionError extends Error { + constructor(name: string) { + super(`Item "${name}" has unknown version`) + } +} + + +export class NonExistentItemError extends Error { + constructor(name: string) { + super(`Item "${name}" does not exist`) + } +} + + +export interface VersionedType { + is(block: RuntimeCtx): boolean + at(block: RuntimeCtx): T +} + + +export type GetVersionedItemEntries> = Simplify< + {[K in keyof V]: [item: V[K] extends VersionedType ? R : never, version: K]}[keyof V] +> + + +export class VersionedItem> { + isExists: (block: RuntimeCtx) => boolean + + constructor( + readonly name: string, + protected versions: V, + isExists: (this: VersionedItem, block: RuntimeCtx) => boolean, + protected UnknownVersionError: new (...args: any[]) => Error = UnknownVersionError, + protected NonExistentItemError: new (...args: any[]) => Error = NonExistentItemError, + ) { + this.isExists = isExists.bind(this) + } + + at(block: RuntimeCtx, match: (this: this, ...args: GetVersionedItemEntries) => T): T { + this.at = this.createMatchVersion() + return this.at(block, match) + } + + private createMatchVersion(): any { + let body = '' + for (let key in this.versions) { + let version = `this.versions['${key}']` + body += `if (${version}.is(block)) return match.call(this, ${version}.at(block), '${key}')\n` + } + body += `if (!this.isExists(block)) throw new this.NonExistentItemError(this.name)\n` + body += `throw new this.UnknownVersionError(this.name)\n` + return new Function('block', 'match', 'fallback', body) + } +} + + +export class EventAtBlockType { + constructor(private event: EventType, private block: RuntimeCtx) {} + + decode(event: Omit): sts.GetType { + return this.event.decode(this.block, event) + } +} + + export class EventType { constructor(public readonly name: QualifiedName, private type: T) {} @@ -39,13 +109,55 @@ export class EventType { return block._runtime.events.checkType(this.name, this.type) } - is(event: Event): boolean { - return this.name == event.name && this.matches(event.block) + is(block: RuntimeCtx): boolean + is(event: Event): boolean + is(blockOrEvent: RuntimeCtx | Event): boolean { + let [block, name] = 'block' in blockOrEvent ? [blockOrEvent.block, blockOrEvent.name] : [blockOrEvent, null] + return (name == null || this.name == name) && this.matches(block) + } + + decode(block: RuntimeCtx, event: Omit): sts.GetType + decode(event: Event): sts.GetType + decode(blockOrEvent: RuntimeCtx | Event, event?: Omit) { + if ('block' in blockOrEvent) { + assert(this.is(blockOrEvent)) + return blockOrEvent.block._runtime.decodeJsonEventRecordArguments(blockOrEvent) + } else { + assert(this.is(blockOrEvent)) + assert(event != null) + return blockOrEvent._runtime.decodeJsonEventRecordArguments(event) + } + } + + at(block: RuntimeCtx): EventAtBlockType { + assert(this.is(block)) + return new EventAtBlockType(this, block) + } +} + + +export type VersionedEvent< + T extends Record, + R extends Record = { + [K in keyof T]: EventType + } +> = Simplify & R> + + +export function event>(name: string, versions: V): VersionedEvent { + let items: any = {} + for (let prop in versions) { + items[prop] = new EventType(name, versions[prop]) } + return Object.assign(new VersionedItem(name, items, (b) => b._runtime.hasEvent(name)), items) +} + - decode(event: Event): sts.GetType { - assert(this.is(event)) - return event.block._runtime.decodeJsonEventRecordArguments(event) +export class CallAtBlockType { + constructor(private call: CallType, private block: RuntimeCtx) {} + + decode(call: Omit): sts.GetType { + return this.call.decode(this.block, call) } } @@ -57,13 +169,55 @@ export class CallType { return block._runtime.calls.checkType(this.name, this.type) } - is(call: Call): boolean { - return this.name == call.name && this.matches(call.block) + is(block: RuntimeCtx): boolean + is(call: Call): boolean + is(blockOrCall: RuntimeCtx | Call): boolean { + let [block, name] = 'block' in blockOrCall ? [blockOrCall.block, blockOrCall.name] : [blockOrCall, null] + return (name == null || this.name == name) && this.matches(block) + } + + decode(block: RuntimeCtx, call: Omit): sts.GetType + decode(call: Call): sts.GetType + decode(blockOrCall: RuntimeCtx | Call, call?: Omit) { + if ('block' in blockOrCall) { + assert(this.is(blockOrCall)) + return blockOrCall.block._runtime.decodeJsonCallRecordArguments(blockOrCall) + } else { + assert(this.is(blockOrCall)) + assert(call != null) + return blockOrCall._runtime.decodeJsonCallRecordArguments(call) + } + } + + at(block: RuntimeCtx): CallAtBlockType { + assert(this.is(block)) + return new CallAtBlockType(this, block) + } +} + + +export type VersionedCall< + T extends Record, + R extends Record = { + [K in keyof T]: CallType + } +> = Simplify & R> + + +export function call>(name: string, versions: V): VersionedCall { + let items: any = {} + for (let prop in versions) { + items[prop] = new EventType(name, versions[prop]) } + return Object.assign(new VersionedItem(name, items, (b) => b._runtime.hasCall(name)), items) +} + + +export class ConstantAtBlockType { + constructor(private constant: ConstantType, private block: RuntimeCtx) {} - decode(call: Call): sts.GetType { - assert(this.is(call)) - return call.block._runtime.decodeJsonCallRecordArguments(call) + get(): sts.GetType { + return this.constant.get(this.block) } } @@ -79,15 +233,132 @@ export class ConstantType { assert(this.is(block)) return block._runtime.getConstant(this.name) } + + at(block: RuntimeCtx): ConstantAtBlockType { + assert(this.is(block)) + return new ConstantAtBlockType(this, block) + } } +export type VersionedConstant< + T extends Record, + R extends Record = { + [K in keyof T]: ConstantType + } +> = Simplify & R> + + +export function constant>(name: string, versions: V): VersionedConstant { + let items: any = {} + for (let prop in versions) { + items[prop] = new ConstantType(name, versions[prop]) + } + return Object.assign(new VersionedItem(name, items, (b) => b._runtime.hasConstant(name)), items) +} + + +export class StorageAtBlockType { + constructor(private storage: StorageType, private block: Block) {} + + get(...key: any[]): Promise { + return this.storage.get(this.block, ...key) + } + + getAll(): Promise { + return this.storage.getAll(this.block) + } + + getMany(keys: any[]): Promise { + return this.storage.getMany(this.block, keys) + } + + getKeys(...args: any[]): Promise { + return this.storage.getKeys(this.block, ...args) + } + + getRawKeys(...args: any[]): Promise { + return this.storage.getRawKeys(this.block, ...args) + } + + getKeysPaged(pageSize: number, ...args: any[]): AsyncIterable { + return this.storage.getKeysPaged(pageSize, this.block, ...args) + } + + getPairs(...args: any[]): Promise<[key: any, value: any][]> { + return this.storage.getPairs(this.block, ...args) + } + + getPairsPaged(pageSize: number, ...args: any[]): AsyncIterable<[key: any, value: any][]> { + return this.storage.getPairsPaged(pageSize, this.block, ...args) + } + + getDefault(): any { + return this.storage.getDefault(this.block) + } +} + + + +type EnumerateKeys = K extends [...infer A, any] + ? EnumerateKeys & ((...args: [...Extra, ...K]) => V) + : ((...args: Extra) => V) + + +export type StorageMethods< + K extends any[], + V, + M extends 'Required' | 'Optional' | 'Default', + D extends boolean, + Extra extends any[] = [], + ReturnValue = M extends 'Required' ? V : V | undefined, + Key = K extends readonly [any] ? K[0] : K +> = Simplify< + { + get(...args: [...Extra, ...K]): Promise + } & (M extends 'Default' + ? { + getDefault(...args: Extra): V + } + : {}) & + ([] extends K + ? {} + : { + getAll(...args: Extra): Promise + getMany(...args: [...Extra, ...[keys: Key[]]]): Promise + } & (D extends false + ? {} + : { + getKeys: EnumerateKeys, Extra> + getKeysPaged: EnumerateKeys, [...[pageSize: number], ...Extra]> + getPairs: EnumerateKeys, Extra> + getPairsPaged: EnumerateKeys< + K, + AsyncIterable<[key: K, value: V][]>, + [...[pageSize: number], ...Extra] + > + })) +> + + +export type GetStorageType< + K extends any[], + V, + M extends 'Required' | 'Optional' | 'Default', + D extends boolean +> = Simplify< + Pick & { + at(block: Block): StorageMethods + } & StorageMethods +> + + export class StorageType { constructor( - private name: QualifiedName, + readonly name: QualifiedName, private modifier: 'Required' | 'Optional' | 'Default', private key: sts.Type[], - private value: sts.Type + private value: sts.Type, ) {} is(block: RuntimeCtx): boolean { @@ -139,4 +410,38 @@ export class StorageType { assert(this.is(block)) return block._runtime.getStorageFallback(this.name) } + + at(block: Block): StorageAtBlockType { + assert(this.is(block)) + return new StorageAtBlockType(this, block) + } +} + + +export type StorageOptions< + K extends sts.Type[], + V extends sts.Type, + M extends 'Required' | 'Optional' | 'Default', + D extends boolean +> = {key: K, value: V, modifier: M, isKeyDecodable: D} + + +export type VersionedStorage< + T extends Record>, + R extends Record = { + [K in keyof T]: GetStorageType, sts.GetType, T[K]['modifier'], T[K]['isKeyDecodable']> + } +> = Simplify & R> + + +export function storage>>( + name: string, + versions: V +): VersionedStorage { + let items: any = {} + for (let prop in versions) { + let {modifier, key, value} = versions[prop] + items[prop] = new StorageType(name, modifier, key, value) + } + return Object.assign(new VersionedItem(name, items, (b) => b._runtime.hasStorage(name), items)) } diff --git a/substrate/substrate-typegen/src/typegen.ts b/substrate/substrate-typegen/src/typegen.ts index 75fd610f7..b732bdcf8 100644 --- a/substrate/substrate-typegen/src/typegen.ts +++ b/substrate/substrate-typegen/src/typegen.ts @@ -45,6 +45,7 @@ export class Typegen { } private sts = new Map() + private palletModules = new Map> public readonly dir: OutDir @@ -58,6 +59,7 @@ export class Typegen { this.generateEnums('calls') this.generateStorage() this.generateConsts() + this.generatePalletModules() let index = this.dir.file('index.ts') @@ -100,44 +102,41 @@ export class Typegen { let file = new ItemFile(this, pallet, fix) let out = file.out + this.getPalletModule(pallet).add(file.name) + for (let [name, versions] of groupBy(palletItems, it => it.def.name)) { out.line() - out.block(`export const ${toJsName(name)} = `, () => { - out.line(`name: '${pallet}.${name}',`) + out.line(`export const ${toJsName(name)} = ${fix.toLowerCase()}_('${pallet}.${name}', {`) + out.indentation(() => { for (let it of versions) { let useSts = file.useSts(it.runtime) out.blockComment(it.def.docs) - out.line(`${this.getVersionName(it.runtime)}: new ${fix}Type(`) - out.indentation(() => { - out.line(`'${pallet}.${name}',`) - if (it.def.fields.length == 0 || it.def.fields[0].name == null) { - if (it.def.fields.length == 1) { - out.line( - useSts(it.def.fields[0].type) - ) + let versionName = this.getVersionName(it.runtime) + if (it.def.fields.length == 0 || it.def.fields[0].name == null) { + if (it.def.fields.length == 1) { + out.line(`${versionName}: ${useSts(it.def.fields[0].type)},` ) + } else { + let list = it.def.fields.map(f => useSts(f.type)).join(', ') + if (list) { + out.line(`${versionName}: sts.tuple([${list}]),`) } else { - let list = it.def.fields.map(f => useSts(f.type)).join(', ') - if (list) { - out.line(`sts.tuple([${list}])`) - } else { - out.line('sts.unit()') - } + out.line(`${versionName}: sts.unit(),`) } - } else { - out.line('sts.struct({') - out.indentation(() => { - for (let f of it.def.fields) { - out.blockComment(f.docs) - out.line(`${f.name}: ${useSts(f.type)},`) - } - }) - out.line('})') } - }) - out.line('),') + } else { + out.line(`${versionName}: sts.struct({`) + out.indentation(() => { + for (let f of it.def.fields) { + out.blockComment(f.docs) + out.line(`${f.name}: ${useSts(f.type)},`) + } + }) + out.line('}),') + } } }) + out.line('})') } file.write() @@ -151,21 +150,20 @@ export class Typegen { let file = new ItemFile(this, pallet, 'Constant') let out = file.out + this.getPalletModule(pallet).add(file.name) + for (let [name, versions] of groupBy(palletItems, it => splitQualifiedName(it.name)[1])) { out.line() - out.block(`export const ${toJsName(name)} = `, () => { + out.line(`export const ${toJsName(name)} = constant_('${pallet}.${name}', {`) + out.indentation(() => { for (let it of versions) { let useSts = file.useSts(it.runtime) out.blockComment(it.def.docs) - out.line(`${this.getVersionName(it.runtime)}: new ConstantType(`) - out.indentation(() => { - out.line(`'${it.name}',`) - out.line(useSts(it.def.type)) - }) - out.line('),') + out.line(`${this.getVersionName(it.runtime)}: ${useSts(it.def.type)},`) } }) + out.line(`})`) } file.write() @@ -179,11 +177,16 @@ export class Typegen { let file = new ItemFile(this, pallet, 'Storage') let out = file.out - for (let [jsName, versions] of groupBy(palletItems, it => toJsName(splitQualifiedName(it.name)[1]))) { + this.getPalletModule(pallet).add(file.name) + + for (let [name, versions] of groupBy(palletItems, it => toJsName(splitQualifiedName(it.name)[1]))) { + let jsName = toJsName(name) + let ifs: (() => void)[] = [] out.line() - out.block(`export const ${jsName} = `, () => { + out.line(`export const ${jsName} = storage_('${pallet}.${name}', {`) + out.indentation(() => { for (let it of versions) { let ifName = upperCaseFirst( toCamelCase(`${jsName}_${this.getVersionName(it.runtime)}`) @@ -197,73 +200,29 @@ export class Typegen { out.blockComment(it.def.docs) out.line( - `${this.getVersionName(it.runtime)}: new StorageType(` + - `'${it.name}', ` + - `'${it.def.modifier}', ` + - `[${keyListExp}], ` + - `${valueExp}` + - `) as ${ifName},` + `${this.getVersionName(it.runtime)}: {` + + `key: [${keyListExp}], ` + + `value: ${valueExp}, ` + + `modifier: '${it.def.modifier}', ` + + `isKeyDecodable: ${isStorageKeyDecodable(it.def)}` + + `} as const,` ) ifs.push(() => { + let value = useIfs(it.def.value) + + let keys = it.def.keys.map(ti => useIfs(ti)) + let args = keys.length == 1 + ? [`key: ${keys[0]}`] + : keys.map((exp, idx) => `key${idx+1}: ${exp}`) + out.line() out.blockComment(it.def.docs) - out.block(`export interface ${ifName} `, () => { - out.line(`is(block: RuntimeCtx): boolean`) - - let value = useIfs(it.def.value) - let keys = it.def.keys.map(ti => useIfs(ti)) - - let args = keys.length == 1 - ? [`key: ${keys[0]}`] - : keys.map((exp, idx) => `key${idx+1}: ${exp}`) - - let fullKey = keys.length == 1 ? keys[0] : `[${keys.join(', ')}]` - - let ret = it.def.modifier == 'Required' ? value : `(${value} | undefined)` - - let kv = `[k: ${fullKey}, v: ${ret}]` - - function* enumeratePartialApps(leading?: string): Iterable { - let list: string[] = [] - if (leading) { - list.push(leading) - } - list.push('block: Block') - yield list.join(', ') - for (let arg of args) { - list.push(arg) - yield list.join(', ') - } - } - - if (it.def.modifier == 'Default') { - out.line(`getDefault(block: Block): ${value}`) - } - - out.line(`get(${['block: Block'].concat(args).join(', ')}): Promise<${ret}>`) - - if (args.length > 0) { - out.line(`getMany(block: Block, keys: ${fullKey}[]): Promise<${ret}[]>`) - if (isStorageKeyDecodable(it.def)) { - for (let args of enumeratePartialApps()) { - out.line(`getKeys(${args}): Promise<${fullKey}[]>`) - } - for (let args of enumeratePartialApps('pageSize: number')) { - out.line(`getKeysPaged(${args}): AsyncIterable<${fullKey}[]>`) - } - for (let args of enumeratePartialApps()) { - out.line(`getPairs(${args}): Promise<${kv}[]>`) - } - for (let args of enumeratePartialApps('pageSize: number')) { - out.line(`getPairsPaged(${args}): AsyncIterable<${kv}[]>`) - } - } - } - }) + out.line(`export type ${ifName} = GetStorageType<[${args.join(`, `)}], ${value}, '${it.def.modifier}', ${isStorageKeyDecodable(it.def)}>`) }) } }) + out.line('})') for (let i of ifs) { i() @@ -274,6 +233,19 @@ export class Typegen { } } + private generatePalletModules() { + for (let [pallet, modules] of this.palletModules) { + let out = this.dir.child(getPalletDir(pallet)).file('index.ts') + + for (let module of modules) { + let moduleName = module.slice(0, module.length - 3) // remove '.ts' + out.line(`export * as ${moduleName} from './${moduleName}'`) + } + + out.write() + } + } + @def private events(): Item[] { return this.collectItems( @@ -422,23 +394,39 @@ export class Typegen { this.sts.set(runtime, sts) return sts } + + getPalletModule(pallet: string) { + let module = this.palletModules.get(pallet) + if (module == null) { + module = new Set() + this.palletModules.set(pallet, module) + } + return module + } } class ItemFile { private imported = new Set() - public readonly out: FileOutput + readonly out: FileOutput + readonly name: string constructor( private typegen: Typegen, pallet: string, type: 'Event' | 'Call' | 'Constant' | 'Storage' ) { + this.name = type == 'Storage' ? 'storage.ts' : type.toLowerCase() + 's.ts' this.out = this.typegen.dir .child(getPalletDir(pallet)) - .file(type == 'Storage' ? 'storage.ts' : type.toLowerCase() + 's.ts') + .file(this.name) + + let imports = ['sts', 'Block', 'Bytes', 'Option', 'Result', `${type}Type`, `${type.toLowerCase()} as ${type.toLowerCase()}_`, 'RuntimeCtx' ] + if (type === 'Storage') { + imports.push('GetStorageType') + } - this.out.line(`import {sts, Block, Bytes, Option, Result, ${type}Type, RuntimeCtx} from '../support'`) + this.out.line(`import {${imports.join(', ')}} from '../support'`) this.out.lazy(() => { Array.from(this.imported) diff --git a/test/balances/src/processor.ts b/test/balances/src/processor.ts index e8ddb7c17..11d488640 100644 --- a/test/balances/src/processor.ts +++ b/test/balances/src/processor.ts @@ -1,10 +1,9 @@ import {BigDecimal} from '@subsquid/big-decimal' import * as ss58 from '@subsquid/ss58' import {SubstrateBatchProcessor} from '@subsquid/substrate-processor' -import {Bytes} from '@subsquid/substrate-runtime' import {TypeormDatabase} from '@subsquid/typeorm-store' import {Transfer} from './model' -import {events} from './types' +import * as balances from './types/balances' const processor = new SubstrateBatchProcessor() @@ -17,7 +16,7 @@ const processor = new SubstrateBatchProcessor() }) .setBlockRange({from: 19_666_100}) .addEvent({ - name: [events.balances.transfer.name] + name: [balances.events.transfer.name] }) @@ -26,16 +25,28 @@ processor.run(new TypeormDatabase(), async ctx => { for (let block of ctx.blocks) { for (let event of block.events) { - let rec: {from: Bytes, to: Bytes, amount: bigint} - if (events.balances.transfer.v1020.is(event)) { - let [from, to, amount, fee] = events.balances.transfer.v1020.decode(event) - rec = {from, to, amount} - } else if (events.balances.transfer.v1050.is(event)) { - let [from, to, amount] = events.balances.transfer.v1050.decode(event) - rec = {from, to, amount} - } else { - rec = events.balances.transfer.v9130.decode(event) - } + let rec = balances.events.transfer.at(block.header, function (e, v) { + switch (v) { + case 'v1020': + case 'v1050': { + let [from, to, amount] = e.decode(event) + return {from, to, amount} + } + case 'v9130': { + return e.decode(event) + } + } + }) + + /** + * Just a demo + */ + // let data = await balances.storage.account.at(block.header, async (s, _) => { + // let d = s.getDefault() + // let [from, to] = await s.getMany([rec.from, rec.to]) + // return {from: from?.free ?? d.free, to: to?.free ?? d.free} + // }) + transfers.push(new Transfer({ id: event.id, from: ss58.codec('kusama').encode(rec.from), diff --git a/test/balances/src/types/balances/events.ts b/test/balances/src/types/balances/events.ts index b1b83aa92..e5f0717c3 100644 --- a/test/balances/src/types/balances/events.ts +++ b/test/balances/src/types/balances/events.ts @@ -1,33 +1,23 @@ -import {sts, Block, Bytes, Option, Result, EventType} from '../support' +import {sts, Block, Bytes, Option, Result, EventType, event as event_, RuntimeCtx} from '../support' import * as v1020 from '../v1020' import * as v1050 from '../v1050' import * as v9130 from '../v9130' -export const transfer = { - name: 'Balances.Transfer', +export const transfer = event_('Balances.Transfer', { /** * Transfer succeeded (from, to, value, fees). */ - v1020: new EventType( - 'Balances.Transfer', - sts.tuple([v1020.AccountId, v1020.AccountId, v1020.Balance, v1020.Balance]) - ), + v1020: sts.tuple([v1020.AccountId, v1020.AccountId, v1020.Balance, v1020.Balance]), /** * Transfer succeeded (from, to, value). */ - v1050: new EventType( - 'Balances.Transfer', - sts.tuple([v1050.AccountId, v1050.AccountId, v1050.Balance]) - ), + v1050: sts.tuple([v1050.AccountId, v1050.AccountId, v1050.Balance]), /** * Transfer succeeded. */ - v9130: new EventType( - 'Balances.Transfer', - sts.struct({ - from: v9130.AccountId32, - to: v9130.AccountId32, - amount: sts.bigint(), - }) - ), -} + v9130: sts.struct({ + from: v9130.AccountId32, + to: v9130.AccountId32, + amount: sts.bigint(), + }), +}) diff --git a/test/balances/src/types/balances/index.ts b/test/balances/src/types/balances/index.ts new file mode 100644 index 000000000..8d3bf94ae --- /dev/null +++ b/test/balances/src/types/balances/index.ts @@ -0,0 +1,2 @@ +export * as events from './events' +export * as storage from './storage' diff --git a/test/balances/src/types/balances/storage.ts b/test/balances/src/types/balances/storage.ts new file mode 100644 index 000000000..c43054247 --- /dev/null +++ b/test/balances/src/types/balances/storage.ts @@ -0,0 +1,80 @@ +import {sts, Block, Bytes, Option, Result, StorageType, storage as storage_, RuntimeCtx, GetStorageType} from '../support' +import * as v1050 from '../v1050' +import * as v9420 from '../v9420' + +export const account = storage_('Balances.Account', { + /** + * The balance of an account. + * + * NOTE: THIS MAY NEVER BE IN EXISTENCE AND YET HAVE A `total().is_zero()`. If the total + * is ever zero, then the entry *MUST* be removed. + * + * NOTE: This is only used in the case that this module is used to store balances. + */ + v1050: {key: [v1050.AccountId], value: v1050.AccountData, modifier: 'Default', isKeyDecodable: false} as const, + /** + * The Balances pallet example of storing the balance of an account. + * + * # Example + * + * ```nocompile + * impl pallet_balances::Config for Runtime { + * type AccountStore = StorageMapShim, frame_system::Provider, AccountId, Self::AccountData> + * } + * ``` + * + * You can also store the balance of an account in the `System` pallet. + * + * # Example + * + * ```nocompile + * impl pallet_balances::Config for Runtime { + * type AccountStore = System + * } + * ``` + * + * But this comes with tradeoffs, storing account balances in the system pallet stores + * `frame_system` data alongside the account data contrary to storing account balances in the + * `Balances` pallet, which uses a `StorageMap` to store balances data only. + * NOTE: This is only used in the case that this pallet is used to store balances. + */ + v9420: {key: [v9420.AccountId32], value: v9420.AccountData, modifier: 'Default', isKeyDecodable: true} as const, +}) + +/** + * The balance of an account. + * + * NOTE: THIS MAY NEVER BE IN EXISTENCE AND YET HAVE A `total().is_zero()`. If the total + * is ever zero, then the entry *MUST* be removed. + * + * NOTE: This is only used in the case that this module is used to store balances. + */ +export type AccountV1050 = GetStorageType<[key: v1050.AccountId], v1050.AccountData, 'Default', false> + +/** + * The Balances pallet example of storing the balance of an account. + * + * # Example + * + * ```nocompile + * impl pallet_balances::Config for Runtime { + * type AccountStore = StorageMapShim, frame_system::Provider, AccountId, Self::AccountData> + * } + * ``` + * + * You can also store the balance of an account in the `System` pallet. + * + * # Example + * + * ```nocompile + * impl pallet_balances::Config for Runtime { + * type AccountStore = System + * } + * ``` + * + * But this comes with tradeoffs, storing account balances in the system pallet stores + * `frame_system` data alongside the account data contrary to storing account balances in the + * `Balances` pallet, which uses a `StorageMap` to store balances data only. + * NOTE: This is only used in the case that this pallet is used to store balances. + */ +export type AccountV9420 = GetStorageType<[key: v9420.AccountId32], v9420.AccountData, 'Default', true> diff --git a/test/balances/src/types/index.ts b/test/balances/src/types/index.ts index d90cf2431..29e9a452f 100644 --- a/test/balances/src/types/index.ts +++ b/test/balances/src/types/index.ts @@ -1,4 +1,6 @@ export * as v1020 from './v1020' export * as v1050 from './v1050' export * as v9130 from './v9130' +export * as v9420 from './v9420' export * as events from './events' +export * as storage from './storage' diff --git a/test/balances/src/types/storage.ts b/test/balances/src/types/storage.ts new file mode 100644 index 000000000..1bc758a7e --- /dev/null +++ b/test/balances/src/types/storage.ts @@ -0,0 +1 @@ +export * as balances from './balances/storage' diff --git a/test/balances/src/types/support.ts b/test/balances/src/types/support.ts index d60896d97..307f809fd 100644 --- a/test/balances/src/types/support.ts +++ b/test/balances/src/types/support.ts @@ -1,16 +1,18 @@ import type {BitSequence, Bytes, QualifiedName, Runtime} from '@subsquid/substrate-runtime' import * as sts from '@subsquid/substrate-runtime/lib/sts' -import {Result} from '@subsquid/substrate-runtime/lib/sts' +import {Option, Result} from '@subsquid/substrate-runtime/lib/sts' import assert from 'assert' -export {sts, Bytes, BitSequence, Result} +export {sts, Bytes, BitSequence, Option, Result} -export type Option = sts.ValueCase<'Some', T> | {__kind: 'None'} +type Simplify = { + [K in keyof T]: T[K] +} & {} -interface RuntimeCtx { +export interface RuntimeCtx { _runtime: Runtime } @@ -35,6 +37,62 @@ interface Call { } +export class UnknownVersionError extends Error { + constructor(name: string) { + super(`Got unknown version for ${name}`) + } +} + + +export interface VersionedType { + is(block: RuntimeCtx): boolean + at(block: RuntimeCtx): T +} + + +export type GetVersionedItemEntries> = Simplify< + {[K in keyof V]: [item: V[K] extends VersionedType ? R : never, version: K]}[keyof V] +> + + +export class VersionedItem> { + isExists: (block: RuntimeCtx) => boolean + + constructor( + readonly name: string, + protected versions: V, + isExists: (this: VersionedItem, block: RuntimeCtx) => boolean, + protected NoVersionError: new (...args: any[]) => Error = UnknownVersionError + ) { + this.isExists = isExists.bind(this) + } + + at(block: RuntimeCtx, cb: (this: this, ...args: GetVersionedItemEntries) => T): T { + this.at = this.createMatchVersion() + return this.at(block, cb) + } + + private createMatchVersion(): any { + let body = '' + for (let key in this.versions) { + let version = `this.versions['${key}']` + body += `if (${version}.is(block)) return cb.call(this, ${version}.at(block), '${key}')\n` + } + body += `throw new this.NoVersionError(this.name)\n` + return new Function('block', 'cb', body) + } +} + + +export class EventAtBlockType { + constructor(private event: EventType, private block: RuntimeCtx) {} + + decode(event: Omit): sts.GetType { + return this.event.decode(this.block, event) + } +} + + export class EventType { constructor(public readonly name: QualifiedName, private type: T) {} @@ -42,13 +100,54 @@ export class EventType { return block._runtime.events.checkType(this.name, this.type) } - is(event: Event): boolean { - return this.name == event.name && this.matches(event.block) + is(block: RuntimeCtx): boolean + is(event: Event): boolean + is(blockOrEvent: RuntimeCtx | Event): boolean { + let [block, name] = 'block' in blockOrEvent ? [blockOrEvent.block, blockOrEvent.name] : [blockOrEvent, null] + return (name == null || this.name == name) && this.matches(block) } - decode(event: Event): sts.GetType { - assert(this.is(event)) - return event.block._runtime.decodeJsonEventRecordArguments(event) + decode(block: RuntimeCtx, event: Omit): sts.GetType + decode(event: Event): sts.GetType + decode(blockOrEvent: RuntimeCtx | Event, event?: Omit) { + if ('block' in blockOrEvent) { + assert(this.is(blockOrEvent)) + return blockOrEvent.block._runtime.decodeJsonEventRecordArguments(blockOrEvent) + } else { + assert(this.is(blockOrEvent)) + assert(event != null) + return blockOrEvent._runtime.decodeJsonEventRecordArguments(event) + } + } + + at(block: RuntimeCtx): EventAtBlockType { + return new EventAtBlockType(this, block) + } +} + + +export type VersionedEvent< + T extends Record, + R extends Record = { + [K in keyof T]: EventType + } +> = Simplify & R> + + +export function event>(name: string, versions: V): VersionedEvent { + let items: any = {} + for (let prop in versions) { + items[prop] = new EventType(name, versions[prop]) + } + return Object.assign(new VersionedItem(name, items, (b) => b._runtime.hasEvent(name)), items) +} + + +export class CallAtBlockType { + constructor(private call: CallType, private block: RuntimeCtx) {} + + decode(call: Omit): sts.GetType { + return this.call.decode(this.block, call) } } @@ -60,13 +159,54 @@ export class CallType { return block._runtime.calls.checkType(this.name, this.type) } - is(call: Call): boolean { - return this.name == call.name && this.matches(call.block) + is(block: RuntimeCtx): boolean + is(call: Call): boolean + is(blockOrCall: RuntimeCtx | Call): boolean { + let [block, name] = 'block' in blockOrCall ? [blockOrCall.block, blockOrCall.name] : [blockOrCall, null] + return (name == null || this.name == name) && this.matches(block) + } + + decode(block: RuntimeCtx, call: Omit): sts.GetType + decode(call: Call): sts.GetType + decode(blockOrCall: RuntimeCtx | Call, call?: Omit) { + if ('block' in blockOrCall) { + assert(this.is(blockOrCall)) + return blockOrCall.block._runtime.decodeJsonCallRecordArguments(blockOrCall) + } else { + assert(this.is(blockOrCall)) + assert(call != null) + return blockOrCall._runtime.decodeJsonCallRecordArguments(call) + } + } + + at(block: RuntimeCtx): CallAtBlockType { + return new CallAtBlockType(this, block) + } +} + + +export type VersionedCall< + T extends Record, + R extends Record = { + [K in keyof T]: CallType + } +> = Simplify & R> + + +export function call>(name: string, versions: V): VersionedCall { + let items: any = {} + for (let prop in versions) { + items[prop] = new EventType(name, versions[prop]) } + return Object.assign(new VersionedItem(name, items, (b) => b._runtime.hasCall(name)), items) +} + - decode(call: Call): sts.GetType { - assert(this.is(call)) - return call.block._runtime.decodeJsonCallRecordArguments(call) +export class ConstantAtBlockType { + constructor(private constant: ConstantType, private block: RuntimeCtx) {} + + get(): sts.GetType { + return this.constant.get(this.block) } } @@ -82,15 +222,131 @@ export class ConstantType { assert(this.is(block)) return block._runtime.getConstant(this.name) } + + at(block: RuntimeCtx): ConstantAtBlockType { + return new ConstantAtBlockType(this, block) + } +} + + +export type VersionedConstant< + T extends Record, + R extends Record = { + [K in keyof T]: ConstantType + } +> = Simplify & R> + + +export function constant>(name: string, versions: V): VersionedConstant { + let items: any = {} + for (let prop in versions) { + items[prop] = new ConstantType(name, versions[prop]) + } + return Object.assign(new VersionedItem(name, items, (b) => b._runtime.hasConstant(name)), items) +} + + +export class StorageAtBlockType { + constructor(private storage: StorageType, private block: Block) {} + + get(...key: any[]): Promise { + return this.storage.get(this.block, ...key) + } + + getAll(): Promise { + return this.storage.getAll(this.block) + } + + getMany(keys: any[]): Promise { + return this.storage.getMany(this.block, keys) + } + + getKeys(...args: any[]): Promise { + return this.storage.getKeys(this.block, ...args) + } + + getRawKeys(...args: any[]): Promise { + return this.storage.getRawKeys(this.block, ...args) + } + + getKeysPaged(pageSize: number, ...args: any[]): AsyncIterable { + return this.storage.getKeysPaged(pageSize, this.block, ...args) + } + + getPairs(...args: any[]): Promise<[key: any, value: any][]> { + return this.storage.getPairs(this.block, ...args) + } + + getPairsPaged(pageSize: number, ...args: any[]): AsyncIterable<[key: any, value: any][]> { + return this.storage.getPairsPaged(pageSize, this.block, ...args) + } + + getDefault(): any { + return this.storage.getDefault(this.block) + } } + +type EnumerateKeys = K extends [...infer A, any] + ? EnumerateKeys & ((...args: [...Extra, ...K]) => V) + : ((...args: Extra) => V) + + +export type StorageMethods< + K extends any[], + V, + M extends 'Required' | 'Optional' | 'Default', + D extends boolean, + Extra extends any[] = [], + ReturnValue = M extends 'Required' ? V : V | undefined, + Key = K extends readonly [any] ? K[0] : K +> = Simplify< + { + get(...args: [...Extra, ...K]): Promise + } & (M extends 'Default' + ? { + getDefault(...args: Extra): V + } + : {}) & + ([] extends K + ? {} + : { + getAll(...args: Extra): Promise + getMany(...args: [...Extra, keys: Key[]]): Promise + } & (D extends false + ? {} + : { + getKeys: EnumerateKeys, Extra> + getKeysPaged: EnumerateKeys, [pageSize: number, ...Extra]> + getPairs: EnumerateKeys, Extra> + getPairsPaged: EnumerateKeys< + K, + AsyncIterable<[key: K, value: V][]>, + [pageSize: number, ...Extra] + > + })) +> + + +export type GetStorageType< + K extends any[], + V, + M extends 'Required' | 'Optional' | 'Default', + D extends boolean +> = Simplify< + Pick & { + at(block: Block): StorageMethods + } & StorageMethods +> + + export class StorageType { constructor( - private name: QualifiedName, + readonly name: QualifiedName, private modifier: 'Required' | 'Optional' | 'Default', private key: sts.Type[], - private value: sts.Type + private value: sts.Type, ) {} is(block: RuntimeCtx): boolean { @@ -142,4 +398,37 @@ export class StorageType { assert(this.is(block)) return block._runtime.getStorageFallback(this.name) } + + at(block: Block): StorageAtBlockType { + return new StorageAtBlockType(this, block) + } +} + + +export type StorageOptions< + K extends sts.Type[], + V extends sts.Type, + M extends 'Required' | 'Optional' | 'Default', + D extends boolean +> = {key: K, value: V, modifier: M, isKeyDecodable: D} + + +export type VersionedStorage< + T extends Record>, + R extends Record = { + [K in keyof T]: GetStorageType, sts.GetType, T[K]['modifier'], T[K]['isKeyDecodable']> + } +> = Simplify & R> + + +export function storage>>( + name: string, + versions: V +): VersionedStorage { + let items: any = {} + for (let prop in versions) { + let {modifier, key, value} = versions[prop] + items[prop] = new StorageType(name, modifier, key, value) + } + return Object.assign(new VersionedItem(name, items, (b) => b._runtime.hasStorage(name), items)) } diff --git a/test/balances/src/types/v1050.ts b/test/balances/src/types/v1050.ts index 8a2c59378..68993c5a7 100644 --- a/test/balances/src/types/v1050.ts +++ b/test/balances/src/types/v1050.ts @@ -1,5 +1,25 @@ import {sts, Result, Option, Bytes, BitSequence} from './support' +export type AccountId = Bytes + +export interface AccountData { + free: Balance + reserved: Balance + miscFrozen: Balance + feeFrozen: Balance +} + +export type Balance = bigint + +export const AccountData: sts.Type = sts.struct(() => { + return { + free: Balance, + reserved: Balance, + miscFrozen: Balance, + feeFrozen: Balance, + } +}) + export const Balance = sts.bigint() export const AccountId = sts.bytes() diff --git a/test/balances/src/types/v9420.ts b/test/balances/src/types/v9420.ts new file mode 100644 index 000000000..85877775b --- /dev/null +++ b/test/balances/src/types/v9420.ts @@ -0,0 +1,25 @@ +import {sts, Result, Option, Bytes, BitSequence} from './support' + +export type AccountId32 = Bytes + +export interface AccountData { + free: bigint + reserved: bigint + frozen: bigint + flags: ExtraFlags +} + +export type ExtraFlags = bigint + +export const AccountData: sts.Type = sts.struct(() => { + return { + free: sts.bigint(), + reserved: sts.bigint(), + frozen: sts.bigint(), + flags: ExtraFlags, + } +}) + +export const ExtraFlags = sts.bigint() + +export const AccountId32 = sts.bytes() diff --git a/test/balances/typegen.json b/test/balances/typegen.json index f1aad053d..865813568 100644 --- a/test/balances/typegen.json +++ b/test/balances/typegen.json @@ -3,7 +3,8 @@ "specVersions": "https://v2.archive.subsquid.io/metadata/kusama", "pallets": { "Balances": { - "events": ["Transfer"] + "events": ["Transfer"], + "storage": ["Account"] } } }