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:
@@ -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())
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user