From bb63de1653781deb0887741cdfb09779b60802f8 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Mon, 26 Oct 2020 08:14:51 +0100 Subject: [PATCH] Add to/fromU8a on contract events and messages (#2778) * Add to/fromU8a on contract events and messages * Add constructor decoding --- packages/api-contract/src/Abi.ts | 94 ++++++++++++++++----- packages/api-contract/src/base/Blueprint.ts | 4 +- packages/api-contract/src/base/Contract.ts | 6 +- packages/api-contract/src/types.ts | 20 +++-- packages/api-contract/src/util.ts | 26 +----- 5 files changed, 95 insertions(+), 55 deletions(-) diff --git a/packages/api-contract/src/Abi.ts b/packages/api-contract/src/Abi.ts index 7513c58080..71e33704a0 100644 --- a/packages/api-contract/src/Abi.ts +++ b/packages/api-contract/src/Abi.ts @@ -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 => { - 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()) + ) + ); } } diff --git a/packages/api-contract/src/base/Blueprint.ts b/packages/api-contract/src/base/Blueprint.ts index 2bba68ff54..04d58154c1 100644 --- a/packages/api-contract/src/base/Blueprint.ts +++ b/packages/api-contract/src/base/Blueprint.ts @@ -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 extends Base { #deploy = (constructorOrId: AbiConstructor | string| number, endowment: BigInt | string | number | BN, gasLimit: BigInt | string | number | BN, params: CodecArg[]): SubmittableExtrinsic> => { 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(this.api, this.abi, record.event.data[1] as AccountId, this._decorateMethod) diff --git a/packages/api-contract/src/base/Contract.ts b/packages/api-contract/src/base/Contract.ts index 14400d07df..746d2e06da 100644 --- a/packages/api-contract/src/base/Contract.ts +++ b/packages/api-contract/src/base/Contract.ts @@ -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 extends Base { } #exec = (messageOrId: AbiMessage | string | number, value: BigInt | BN | string | number, gasLimit: BigInt | BN | string | number, params: CodecArg[]): SubmittableExtrinsic => { - 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 => { @@ -83,7 +83,7 @@ export default class Contract extends Base { 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( diff --git a/packages/api-contract/src/types.ts b/packages/api-contract/src/types.ts index 0f067f39c4..f5346a8673 100644 --- a/packages/api-contract/src/types.ts +++ b/packages/api-contract/src/types.ts @@ -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; +} diff --git a/packages/api-contract/src/util.ts b/packages/api-contract/src/util.ts index 54ea2fc7d5..e7de80ba78 100644 --- a/packages/api-contract/src/util.ts +++ b/packages/api-contract/src/util.ts @@ -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 (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, { name, type }): Record => { - r[name] = type.displayName || encodeTypeDef(type); - - return r; - }, { __selector: 'ContractSelector' }) - )); - - return compactAddLength(new Clazz(registry, message.args.reduce((r: Record, { name }, index): Record => { - r[name] = params[index]; - - return r; - }, { __selector: message.selector })).toU8a()); -}