252 lines
7.8 KiB
TypeScript
252 lines
7.8 KiB
TypeScript
// Copyright 2017-2021 @polkadot/api-contract authors & contributors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
import type { ChainProperties, ContractDisplayName, SiField, SiLookupTypeId, SiType0, SiTypeDefArray, SiTypeDefSequence, SiTypeDefTuple, SiTypeDefVariant, SiVariant } from '@polkadot/types/interfaces';
|
|
import type { InterfaceTypes, TypeDef } from '@polkadot/types/types';
|
|
|
|
import { TypeDefInfo, TypeRegistry, withTypeString } from '@polkadot/types';
|
|
import { assert, isUndefined } from '@polkadot/util';
|
|
|
|
interface PartialTypeSpec {
|
|
readonly type: SiLookupTypeId;
|
|
readonly displayName?: ContractDisplayName;
|
|
}
|
|
|
|
// convert the offset into project-specific, index-1
|
|
export function getRegistryOffset (id: SiLookupTypeId, typeOffset: number): number {
|
|
return id.toNumber() - typeOffset;
|
|
}
|
|
|
|
const PRIMITIVE_ALIAS: Record<string, keyof InterfaceTypes> = {
|
|
Char: 'u32', // Rust char is 4-bytes
|
|
Str: 'Text'
|
|
};
|
|
|
|
const PRIMITIVE_ALWAYS = ['AccountId', 'AccountIndex', 'Address', 'Balance'];
|
|
|
|
export class MetaRegistry extends TypeRegistry {
|
|
public readonly metaTypeDefs: TypeDef[] = [];
|
|
|
|
public readonly typeOffset;
|
|
|
|
#siTypes: SiType0[] = [];
|
|
|
|
constructor (metadataVersion: string, chainProperties?: ChainProperties) {
|
|
super();
|
|
|
|
const [major, minor] = metadataVersion.split('.').map((n) => parseInt(n, 10));
|
|
|
|
// type indexes are 1-based pre 0.7 and 0-based post
|
|
this.typeOffset = (major === 0 && minor < 7) ? 1 : 0;
|
|
|
|
if (chainProperties) {
|
|
this.setChainProperties(chainProperties);
|
|
}
|
|
}
|
|
|
|
public setMetaTypes (metaTypes: SiType0[]): void {
|
|
this.#siTypes = metaTypes;
|
|
}
|
|
|
|
public getMetaTypeDef (typeSpec: PartialTypeSpec): TypeDef {
|
|
const offset = getRegistryOffset(typeSpec.type, this.typeOffset);
|
|
let typeDef = this.metaTypeDefs[offset];
|
|
|
|
if (!typeDef) {
|
|
typeDef = this.#extract(this.#getMetaType(typeSpec.type), typeSpec.type);
|
|
|
|
this.metaTypeDefs[offset] = typeDef;
|
|
}
|
|
|
|
if (typeSpec.displayName && typeSpec.displayName.length) {
|
|
const displayName = typeSpec.displayName[typeSpec.displayName.length - 1].toString();
|
|
|
|
if (!typeDef.type.startsWith(displayName)) {
|
|
typeDef = {
|
|
...typeDef,
|
|
displayName,
|
|
type: PRIMITIVE_ALWAYS.includes(displayName)
|
|
? displayName
|
|
: typeDef.type
|
|
};
|
|
}
|
|
}
|
|
|
|
// Here we protect against the following cases
|
|
// - No displayName present, these are generally known primitives
|
|
// - displayName === type, these generate circular references
|
|
// - displayName Option & type Option<...something...>
|
|
if (typeDef.displayName && !this.hasType(typeDef.displayName) && !(typeDef.type === typeDef.displayName || typeDef.type.startsWith(`${typeDef.displayName}<`))) {
|
|
this.register({ [typeDef.displayName]: typeDef.type });
|
|
}
|
|
|
|
return typeDef;
|
|
}
|
|
|
|
#getMetaType = (id: SiLookupTypeId): SiType0 => {
|
|
const type = this.#siTypes[getRegistryOffset(id, this.typeOffset)];
|
|
|
|
assert(!isUndefined(type), () => `getMetaType:: Unable to find ${id.toNumber()} in type values`);
|
|
|
|
return this.createType('SiType0', type);
|
|
}
|
|
|
|
#extract = (type: SiType0, id: SiLookupTypeId): TypeDef => {
|
|
const path = [...type.path];
|
|
const isPrimitivePath = !!path.length && (
|
|
(path.length > 2 && path[0].eq('ink_env') && path[1].eq('types')) ||
|
|
PRIMITIVE_ALWAYS.includes(path[path.length - 1].toString())
|
|
);
|
|
let typeDef: Omit<TypeDef, 'type'>;
|
|
|
|
if (isPrimitivePath) {
|
|
typeDef = this.#extractPrimitivePath(type);
|
|
} else if (type.def.isPrimitive) {
|
|
typeDef = this.#extractPrimitive(type);
|
|
} else if (type.def.isComposite) {
|
|
typeDef = this.#extractFields(type.def.asComposite.fields);
|
|
} else if (type.def.isVariant) {
|
|
typeDef = this.#extractVariant(type.def.asVariant, id);
|
|
} else if (type.def.isArray) {
|
|
typeDef = this.#extractArray(type.def.asArray);
|
|
} else if (type.def.isSequence) {
|
|
typeDef = this.#extractSequence(type.def.asSequence, id);
|
|
} else if (type.def.isTuple) {
|
|
typeDef = this.#extractTuple(type.def.asTuple);
|
|
} else {
|
|
throw new Error(`Invalid ink! type at index ${id.toString()}`);
|
|
}
|
|
|
|
const displayName = path.pop()?.toString();
|
|
|
|
return withTypeString({
|
|
...(displayName
|
|
? { displayName }
|
|
: {}
|
|
),
|
|
...(path.length > 1
|
|
? { namespace: path.map((s) => s.toString()).join('::') }
|
|
: {}
|
|
),
|
|
...(type.params.length > 0
|
|
? { sub: type.params.map((type) => this.getMetaTypeDef({ type })) }
|
|
: {}
|
|
),
|
|
...typeDef
|
|
});
|
|
}
|
|
|
|
#extractArray = ({ len: length, type }: SiTypeDefArray): Omit<TypeDef, 'type'> => {
|
|
assert(!length || length.toNumber() <= 256, 'MetaRegistry: Only support for [Type; <length>], where length <= 256');
|
|
|
|
return {
|
|
info: TypeDefInfo.VecFixed,
|
|
length: length.toNumber(),
|
|
sub: this.getMetaTypeDef({ type })
|
|
};
|
|
}
|
|
|
|
#extractFields = (fields: SiField[]): Omit<TypeDef, 'type'> => {
|
|
const [isStruct, isTuple] = fields.reduce(([isAllNamed, isAllUnnamed], { name }) => ([
|
|
isAllNamed && name.isSome,
|
|
isAllUnnamed && name.isNone
|
|
]),
|
|
[true, true]);
|
|
|
|
assert(isTuple || isStruct, 'Invalid fields type detected, expected either Tuple or Struct');
|
|
|
|
const sub = fields.map(({ name, type }) => ({
|
|
...this.getMetaTypeDef({ type }),
|
|
...(name.isSome
|
|
? { name: name.unwrap().toString() }
|
|
: {}
|
|
)
|
|
}));
|
|
|
|
return isTuple && sub.length === 1
|
|
? sub[0]
|
|
: {
|
|
// check for tuple first (no fields may be available)
|
|
info: isTuple
|
|
? TypeDefInfo.Tuple
|
|
: TypeDefInfo.Struct,
|
|
sub
|
|
};
|
|
}
|
|
|
|
#extractPrimitive = (type: SiType0): TypeDef => {
|
|
const typeStr = type.def.asPrimitive.type.toString();
|
|
|
|
return {
|
|
info: TypeDefInfo.Plain,
|
|
type: PRIMITIVE_ALIAS[typeStr] || typeStr.toLowerCase()
|
|
};
|
|
}
|
|
|
|
#extractPrimitivePath = (type: SiType0): TypeDef => {
|
|
return {
|
|
info: TypeDefInfo.Plain,
|
|
type: type.path[type.path.length - 1].toString()
|
|
};
|
|
}
|
|
|
|
#extractSequence = ({ type }: SiTypeDefSequence, id: SiLookupTypeId): Omit<TypeDef, 'type'> => {
|
|
assert(!!type, () => `ContractRegistry: Invalid sequence type found at id ${id.toString()}`);
|
|
|
|
return {
|
|
info: TypeDefInfo.Vec,
|
|
sub: this.getMetaTypeDef({ type })
|
|
};
|
|
}
|
|
|
|
#extractTuple = (ids: SiTypeDefTuple): Omit<TypeDef, 'type'> => {
|
|
return ids.length === 1
|
|
? this.getMetaTypeDef({ type: ids[0] })
|
|
: {
|
|
info: TypeDefInfo.Tuple,
|
|
sub: ids.map((type) => this.getMetaTypeDef({ type }))
|
|
};
|
|
}
|
|
|
|
#extractVariant = ({ variants }: SiTypeDefVariant, id: SiLookupTypeId): Omit<TypeDef, 'type'> => {
|
|
const { params, path } = this.#getMetaType(id);
|
|
const specialVariant = path[0].toString();
|
|
|
|
return specialVariant === 'Option'
|
|
? {
|
|
info: TypeDefInfo.Option,
|
|
sub: this.getMetaTypeDef({ type: params[0] })
|
|
}
|
|
: specialVariant === 'Result'
|
|
? {
|
|
info: TypeDefInfo.Result,
|
|
sub: params.map((type, index) => ({
|
|
name: ['Ok', 'Error'][index],
|
|
...this.getMetaTypeDef({ type })
|
|
}))
|
|
}
|
|
: {
|
|
info: TypeDefInfo.Enum,
|
|
sub: this.#extractVariantSub(variants)
|
|
};
|
|
}
|
|
|
|
#extractVariantSub = (variants: SiVariant[]): TypeDef[] => {
|
|
return variants.every(({ fields }) => fields.length === 0)
|
|
? variants.map(({ discriminant, name }) => ({
|
|
...(
|
|
discriminant.isSome
|
|
? { ext: { discriminant: discriminant.unwrap().toNumber() } }
|
|
: {}
|
|
),
|
|
info: TypeDefInfo.Plain,
|
|
name: name.toString(),
|
|
type: 'Null'
|
|
}))
|
|
: variants.map(({ fields, name }) => withTypeString({
|
|
...this.#extractFields(fields),
|
|
name: name.toString()
|
|
}));
|
|
}
|
|
}
|