Files
api/packages/api-contract/src/Abi.ts
T
Jaco d62bfc55ac Adjust scale-info types (updated v14, contracts rename) (#3776)
* Adjust scale-info types (updated v14, contracts rename)

* Adjust v14 metadata

* PortableRegistry -> lookup

* Rewrite json
2021-07-16 13:57:44 +02:00

199 lines
6.8 KiB
TypeScript

// Copyright 2017-2021 @polkadot/api-contract authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Bytes } from '@polkadot/types';
import type { ChainProperties, ContractConstructorSpec, ContractEventSpec, ContractMessageParamSpec, ContractMessageSpec, ContractProject } from '@polkadot/types/interfaces';
import type { AnyJson, Codec, CodecArg } from '@polkadot/types/types';
import type { AbiConstructor, AbiEvent, AbiMessage, AbiParam, DecodedEvent, DecodedMessage } from './types';
import { assert, assertReturn, compactAddLength, compactStripLength, isNumber, isObject, isString, logger, stringCamelCase, stringify, u8aConcat, u8aToHex } from '@polkadot/util';
import { MetaRegistry } from './MetaRegistry';
const l = logger('Abi');
function findMessage <T extends AbiMessage> (list: T[], messageOrId: T | string | number): T {
const message = isNumber(messageOrId)
? list[messageOrId]
: isString(messageOrId)
? list.find(({ identifier }) => [identifier, stringCamelCase(identifier)].includes(messageOrId.toString()))
: messageOrId;
return assertReturn(message, () => `Attempted to call an invalid contract interface, ${stringify(messageOrId)}`);
}
export class Abi {
readonly #events: AbiEvent[];
public readonly constructors: AbiConstructor[];
public readonly json: AnyJson;
public readonly messages: AbiMessage[];
public readonly project: ContractProject;
public readonly registry: MetaRegistry;
constructor (abiJson: AnyJson, chainProperties?: ChainProperties) {
const json = isString(abiJson)
? JSON.parse(abiJson) as AnyJson
: abiJson;
assert(isObject(json) && !Array.isArray(json) && json.metadataVersion && isObject(json.spec) && !Array.isArray(json.spec) && Array.isArray(json.spec.constructors) && Array.isArray(json.spec.messages), 'Invalid JSON ABI structure supplied, expected a recent metadata version');
this.json = json;
this.registry = new MetaRegistry(json.metadataVersion as string, chainProperties);
this.project = this.registry.createType('ContractProject', json);
this.registry.setMetaTypes(this.project.types);
this.project.types.forEach((_, index) =>
this.registry.getMetaTypeDef({ type: this.registry.createType('Si0LookupTypeId', index + this.registry.typeOffset) })
);
this.constructors = this.project.spec.constructors.map((spec: ContractConstructorSpec, index) =>
this.#createMessage(spec, index, {
isConstructor: true
})
);
this.#events = this.project.spec.events.map((spec: ContractEventSpec, index) =>
this.#createEvent(spec, index)
);
this.messages = this.project.spec.messages.map((spec: ContractMessageSpec, index): AbiMessage => {
const typeSpec = spec.returnType.unwrapOr(null);
return this.#createMessage(spec, index, {
isMutating: spec.mutates.isTrue,
isPayable: spec.payable.isTrue,
returnType: typeSpec
? this.registry.getMetaTypeDef(typeSpec)
: null
});
});
}
/**
* Warning: Unstable API, bound to change
*/
public decodeEvent (data: Bytes | Uint8Array): DecodedEvent {
const index = data[0];
const event = this.#events[index];
assert(event, () => `Unable to find event with index ${index}`);
return event.fromU8a(data.subarray(1));
}
/**
* Warning: Unstable API, bound to change
*/
public decodeConstructor (data: Uint8Array): DecodedMessage {
return this.#decodeMessage('message', this.constructors, data);
}
/**
* Warning: Unstable API, bound to change
*/
public decodeMessage (data: Uint8Array): DecodedMessage {
return this.#decodeMessage('message', this.messages, data);
}
public findConstructor (constructorOrId: AbiConstructor | string | number): AbiConstructor {
return findMessage(this.constructors, constructorOrId);
}
public findMessage (messageOrId: AbiMessage | string | number): AbiMessage {
return findMessage(this.messages, messageOrId);
}
#createArgs = (args: ContractMessageParamSpec[], spec: unknown): AbiParam[] => {
return args.map((arg, index): AbiParam => {
try {
assert(isObject(arg.type), 'Invalid type definition found');
return {
name: stringCamelCase(arg.name),
type: this.registry.getMetaTypeDef(arg.type)
};
} catch (error) {
l.error(`Error expanding argument ${index} in ${stringify(spec)}`);
throw error;
}
});
}
#createEvent = (spec: ContractEventSpec, index: number): AbiEvent => {
const args = this.#createArgs(spec.args, spec);
const event = {
args,
docs: spec.docs.map((d) => d.toString()),
fromU8a: (data: Uint8Array): DecodedEvent => ({
args: this.#decodeArgs(args, data),
event
}),
identifier: spec.name.toString(),
index
};
return event;
}
#createMessage = (spec: ContractMessageSpec | ContractConstructorSpec, index: number, add: Partial<AbiMessage> = {}): AbiMessage => {
const args = this.#createArgs(spec.args, spec);
const identifier = spec.name.toString();
const message = {
...add,
args,
docs: spec.docs.map((d) => d.toString()),
fromU8a: (data: Uint8Array): DecodedMessage => ({
args: this.#decodeArgs(args, data),
message
}),
identifier,
index,
method: stringCamelCase(identifier),
selector: spec.selector,
toU8a: (params: CodecArg[]) =>
this.#encodeArgs(spec, args, params)
};
return message;
}
#decodeArgs = (args: AbiParam[], data: Uint8Array): Codec[] => {
// for decoding we expect the input to be just the arg data, no selectors
// no length added (this allows use with events as well)
let offset = 0;
return args.map(({ type }): Codec => {
const value = this.registry.createType(type.type as 'Text', data.subarray(offset));
offset += value.encodedLength;
return value;
});
}
#decodeMessage = (type: 'constructor' | 'message', list: AbiMessage[], data: Uint8Array): DecodedMessage => {
const [, trimmed] = compactStripLength(data);
const selector = trimmed.subarray(0, 4);
const message = list.find((m) => m.selector.eq(selector));
assert(message, `Unable to find ${type} with selector ${u8aToHex(selector)}`);
return message.fromU8a(trimmed.subarray(4));
}
#encodeArgs = ({ name, selector }: ContractMessageSpec | ContractConstructorSpec, args: AbiParam[], data: CodecArg[]): Uint8Array => {
assert(data.length === args.length, () => `Expected ${args.length} arguments to contract message '${name.toString()}', found ${data.length}`);
return compactAddLength(
u8aConcat(
this.registry.createType('ContractSelector', selector).toU8a(),
...args.map(({ type }, index) => this.registry.createType(type.type as 'Text', data[index]).toU8a())
)
);
}
}