Add to/fromU8a on contract events and messages (#2778)

* Add to/fromU8a on contract events and messages

* Add constructor decoding
This commit is contained in:
Jaco Greeff
2020-10-26 08:14:51 +01:00
committed by GitHub
parent a2c2b24198
commit bb63de1653
5 changed files with 95 additions and 55 deletions
+74 -20
View File
@@ -1,11 +1,11 @@
// Copyright 2017-2020 @polkadot/api-contract authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { AnyJson, Codec } from '@polkadot/types/types';
import { AnyJson, Codec, CodecArg } from '@polkadot/types/types';
import { ChainProperties, ContractConstructorSpec, ContractEventSpec, ContractMessageSpec, ContractMessageParamSpec, ContractProject } from '@polkadot/types/interfaces';
import { AbiConstructor, AbiEvent, AbiMessage, AbiParam, DecodedEvent } from './types';
import { AbiConstructor, AbiEvent, AbiMessage, AbiParam, DecodedEvent, DecodedMessage } from './types';
import { assert, isNumber, isObject, isString, stringCamelCase } from '@polkadot/util';
import { assert, compactAddLength, compactStripLength, isNumber, isObject, isString, stringCamelCase, u8aConcat, u8aToHex } from '@polkadot/util';
import MetaRegistry from './MetaRegistry';
@@ -74,24 +74,27 @@ export default class Abi {
/**
* Warning: Unstable API, bound to change
*/
public decodeEvent (eventU8a: Uint8Array): DecodedEvent {
const index = eventU8a[0];
public decodeEvent (data: Uint8Array): DecodedEvent {
const index = data[0];
const event = this.#events[index];
assert(event, `Unable to find event with index ${index}`);
let offset = 1;
return event.fromU8a(data.subarray(1));
}
return {
args: event.args.map(({ type }): Codec => {
const value = this.registry.createType(type.type as 'Text', eventU8a.subarray(offset));
/**
* Warning: Unstable API, bound to change
*/
public decodeConstructor (data: Uint8Array): DecodedMessage {
return this.#decodeMessage('message', this.constructors, data);
}
offset += value.encodedLength;
return value;
}),
event
};
/**
* 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 {
@@ -120,22 +123,73 @@ export default class Abi {
}
#createEvent = (spec: ContractEventSpec, index: number): AbiEvent => {
return {
args: this.#createArgs(spec.args, spec),
const args = this.#createArgs(spec.args, spec);
const event = {
args,
docs: spec.docs.map((doc) => doc.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 => {
return {
const args = this.#createArgs(spec.args, spec);
const message = {
...add,
args: this.#createArgs(spec.args, spec),
args,
docs: spec.docs.map((doc) => doc.toString()),
fromU8a: (data: Uint8Array): DecodedMessage => ({
args: this.#decodeArgs(args, data),
message
}),
identifier: spec.name.toString(),
index,
selector: spec.selector
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())
)
);
}
}
+2 -2
View File
@@ -14,7 +14,7 @@ import ApiBase from '@polkadot/api/base';
import { isUndefined, stringCamelCase } from '@polkadot/util';
import Abi from '../Abi';
import { applyOnEvent, encodeMessage } from '../util';
import { applyOnEvent } from '../util';
import Base from './Base';
import Contract from './Contract';
@@ -54,7 +54,7 @@ export default class Blueprint<ApiType extends ApiTypes> extends Base<ApiType> {
#deploy = (constructorOrId: AbiConstructor | string| number, endowment: BigInt | string | number | BN, gasLimit: BigInt | string | number | BN, params: CodecArg[]): SubmittableExtrinsic<ApiType, BlueprintSubmittableResult<ApiType>> => {
return this.api.tx.contracts
.instantiate(endowment, gasLimit, this.codeHash, encodeMessage(this.registry, this.abi.findConstructor(constructorOrId), params))
.instantiate(endowment, gasLimit, this.codeHash, this.abi.findConstructor(constructorOrId).toU8a(params))
.withResultTransform((result: ISubmittableResult) =>
new BlueprintSubmittableResult(result, applyOnEvent(result, 'Instantiated', (record: EventRecord) =>
new Contract<ApiType>(this.api, this.abi, record.event.data[1] as AccountId, this._decorateMethod)
+3 -3
View File
@@ -14,7 +14,7 @@ import ApiBase from '@polkadot/api/base';
import { assert, bnToBn, isFunction, isUndefined, stringCamelCase } from '@polkadot/util';
import Abi from '../Abi';
import { encodeMessage, formatData } from '../util';
import { formatData } from '../util';
import Base from './Base';
const ERROR_NO_CALL = 'Your node does not expose the contracts.call RPC. This is most probably due to a runtime configuration.';
@@ -69,7 +69,7 @@ export default class Contract<ApiType extends ApiTypes> extends Base<ApiType> {
}
#exec = (messageOrId: AbiMessage | string | number, value: BigInt | BN | string | number, gasLimit: BigInt | BN | string | number, params: CodecArg[]): SubmittableExtrinsic<ApiType> => {
return this.api.tx.contracts.call(this.address, value, this.#getGas(gasLimit), encodeMessage(this.registry, this.abi.findMessage(messageOrId), params));
return this.api.tx.contracts.call(this.address, value, this.#getGas(gasLimit), this.abi.findMessage(messageOrId).toU8a(params));
}
#read = (messageOrId: AbiMessage | string | number, value: BigInt | BN | string | number, gasLimit: BigInt | BN | string | number, params: CodecArg[]): ContractRead<ApiType> => {
@@ -83,7 +83,7 @@ export default class Contract<ApiType extends ApiTypes> extends Base<ApiType> {
this.api.rx.rpc.contracts.call({
dest: this.address,
gasLimit: this.#getGas(gasLimit),
inputData: encodeMessage(this.registry, message, params),
inputData: message.toU8a(params),
origin,
value
}).pipe(
+14 -6
View File
@@ -3,7 +3,7 @@
import { ApiTypes } from '@polkadot/api/types';
import { ContractExecResult, ContractSelector } from '@polkadot/types/interfaces';
import { Codec, TypeDef } from '@polkadot/types/types';
import { Codec, CodecArg, TypeDef } from '@polkadot/types/types';
import ApiBase from '@polkadot/api/base';
import Abi from './Abi';
@@ -24,18 +24,15 @@ export interface AbiParam {
export interface AbiEvent {
args: AbiParam[];
docs: string[];
fromU8a: (data: Uint8Array) => DecodedEvent;
identifier: string;
index: number;
}
export interface DecodedEvent {
args: Codec[];
event: AbiEvent;
}
export interface AbiMessage {
args: AbiParam[];
docs: string[];
fromU8a: (data: Uint8Array) => DecodedMessage;
identifier: string;
index: number;
isConstructor?: boolean;
@@ -43,6 +40,7 @@ export interface AbiMessage {
isPayable?: boolean;
returnType?: TypeDef | null;
selector: ContractSelector;
toU8a: (params: CodecArg[]) => Uint8Array;
}
export type AbiConstructor = AbiMessage;
@@ -56,3 +54,13 @@ export interface ContractCallOutcome {
output: Codec | null;
result: ContractExecResult;
}
export interface DecodedEvent {
args: Codec[];
event: AbiEvent;
}
export interface DecodedMessage {
args: Codec[];
message: AbiMessage;
}
+2 -24
View File
@@ -3,12 +3,9 @@
import { SubmittableResult } from '@polkadot/api';
import { EventRecord } from '@polkadot/types/interfaces';
import { Codec, CodecArg, Registry, TypeDef } from '@polkadot/types/types';
import { AbiConstructor, AbiMessage } from './types';
import { Codec, Registry, TypeDef } from '@polkadot/types/types';
import { Raw, createClass, createTypeUnsafe, encodeTypeDef } from '@polkadot/types';
import { assert, compactAddLength } from '@polkadot/util';
import { Raw, createTypeUnsafe } from '@polkadot/types';
export function formatData (registry: Registry, data: Raw, { type }: TypeDef): Codec {
return createTypeUnsafe(registry, type, [data], true);
@@ -25,22 +22,3 @@ export function applyOnEvent <T> (result: SubmittableResult, type: 'CodeStored'
return undefined;
}
export function encodeMessage (registry: Registry, message: AbiMessage | AbiConstructor, params: CodecArg[]): Uint8Array {
assert(message, 'Attempted to call an invalid contract message');
assert(params.length === message.args.length, `Expected ${message.args.length} arguments to contract message '${message.identifier}', found ${params.length}`);
const Clazz = createClass(registry, JSON.stringify(
message.args.reduce((r: Record<string, any>, { name, type }): Record<string, any> => {
r[name] = type.displayName || encodeTypeDef(type);
return r;
}, { __selector: 'ContractSelector' })
));
return compactAddLength(new Clazz(registry, message.args.reduce((r: Record<string, CodecArg>, { name }, index): Record<string, CodecArg> => {
r[name] = params[index];
return r;
}, { __selector: message.selector })).toU8a());
}