diff --git a/packages/api-contract/src/base/Contract.ts b/packages/api-contract/src/base/Contract.ts index 746d2e06da62..5e07693dd316 100644 --- a/packages/api-contract/src/base/Contract.ts +++ b/packages/api-contract/src/base/Contract.ts @@ -4,14 +4,14 @@ import { ApiTypes, DecorateMethod } from '@polkadot/api/types'; import { SubmittableExtrinsic } from '@polkadot/api/submittable/types'; import { AccountId, ContractExecResult } from '@polkadot/types/interfaces'; -import { AnyJson, CodecArg } from '@polkadot/types/types'; +import { AnyJson, CodecArg, Registry } from '@polkadot/types/types'; import { AbiMessage, ContractCallOutcome } from '../types'; import { ContractRead, MapMessageExec, MapMessageRead } from './types'; import BN from 'bn.js'; import { map } from 'rxjs/operators'; import ApiBase from '@polkadot/api/base'; -import { assert, bnToBn, isFunction, isUndefined, stringCamelCase } from '@polkadot/util'; +import { assert, bnToBn, isFunction, isObject, isUndefined, stringCamelCase } from '@polkadot/util'; import Abi from '../Abi'; import { formatData } from '../util'; @@ -19,6 +19,35 @@ 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.'; +// map from a JSON result to current-style ContractExecResult +function mapExecResult (registry: Registry, json: AnyJson): ContractExecResult { + assert(isObject(json) && !Array.isArray(json), 'Invalid JSON result retrieved'); + + if (!Object.keys(json).some((key) => ['error', 'success'].includes(key))) { + return registry.createType('ContractExecResult', json); + } + + const from = registry.createType('ContractExecResultTo260', json); + + if (from.isSuccess) { + const s = from.asSuccess; + + return registry.createType('ContractExecResult', { + gasConsumed: s.gasConsumed, + result: { + ok: { + data: s.data, + flags: s.flags + } + } + }); + } + + // in the old format error has no additional information, + // map it as-is with an "unknown" error + return registry.createType('ContractExecResult', { result: { err: { other: 'unknown' } } }); +} + export default class Contract extends Base { public readonly address: AccountId; @@ -80,20 +109,24 @@ export default class Contract extends Base { return { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment send: this._decorateMethod((origin: string | AccountId | Uint8Array) => - this.api.rx.rpc.contracts.call({ + this.api.rx.rpc.contracts.call.json({ dest: this.address, gasLimit: this.#getGas(gasLimit), inputData: message.toU8a(params), origin, value - }).pipe( - map((result: ContractExecResult): ContractCallOutcome => ({ - output: result.isSuccess && message.returnType - ? formatData(this.registry, result.asSuccess.data, message.returnType) + }).pipe(map((json): ContractCallOutcome => { + const { debugMessage, gasConsumed, result } = mapExecResult(this.registry, json.toJSON()); + + return { + debugMessage, + gasConsumed, + output: result.isOk && message.returnType + ? formatData(this.registry, result.asOk.data, message.returnType) : null, result - })) - ) + }; + })) ) }; } diff --git a/packages/api-contract/src/types.ts b/packages/api-contract/src/types.ts index f5346a867356..b72f6ee82999 100644 --- a/packages/api-contract/src/types.ts +++ b/packages/api-contract/src/types.ts @@ -2,10 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { ApiTypes } from '@polkadot/api/types'; -import { ContractExecResult, ContractSelector } from '@polkadot/types/interfaces'; +import { ContractExecResultResult, ContractSelector } from '@polkadot/types/interfaces'; import { Codec, CodecArg, TypeDef } from '@polkadot/types/types'; import ApiBase from '@polkadot/api/base'; +import { Text, u64 } from '@polkadot/types'; + import Abi from './Abi'; export interface ContractBase { @@ -51,8 +53,10 @@ export interface InterfaceContractCalls { } export interface ContractCallOutcome { + debugMessage: Text; + gasConsumed: u64; output: Codec | null; - result: ContractExecResult; + result: ContractExecResultResult; } export interface DecodedEvent { diff --git a/packages/api/src/base/Decorate.ts b/packages/api/src/base/Decorate.ts index 138d40fd1c70..f2ebca90e636 100644 --- a/packages/api/src/base/Decorate.ts +++ b/packages/api/src/base/Decorate.ts @@ -274,6 +274,8 @@ export default abstract class Decorate extends Events if (this.hasSubscriptions || !(methodName.startsWith('subscribe') || methodName.startsWith('unsubscribe'))) { (section as Record)[methodName] = decorateMethod(method, { methodName }) as unknown; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (section as Record)[methodName].json = decorateMethod(method.json, { methodName }) as unknown; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (section as Record)[methodName].raw = decorateMethod(method.raw, { methodName }) as unknown; } diff --git a/packages/api/src/checkTypes.manual.ts b/packages/api/src/checkTypes.manual.ts index 26adbaae19e5..e3493010c51b 100644 --- a/packages/api/src/checkTypes.manual.ts +++ b/packages/api/src/checkTypes.manual.ts @@ -115,7 +115,8 @@ async function rpc (api: ApiPromise): Promise { console.log('current balance:', balance.toString()); }); - // using raw + // using json & raw + await api.rpc.chain.getBlock.json('0x123456'); await api.rpc.chain.getBlock.raw('0x123456'); // using raw subs diff --git a/packages/api/src/types/rpc.ts b/packages/api/src/types/rpc.ts index 1bca3f4bc12f..08044a56ff15 100644 --- a/packages/api/src/types/rpc.ts +++ b/packages/api/src/types/rpc.ts @@ -1,24 +1,26 @@ // Copyright 2017-2020 @polkadot/api authors & contributors // SPDX-License-Identifier: Apache-2.0 -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { AnyFunction, Callback, Codec } from '@polkadot/types/types'; +import { AnyFunction, Callback } from '@polkadot/types/types'; import { Observable } from 'rxjs'; +import { Json, Raw } from '@polkadot/types/codec'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { ApiTypes, Push, PromiseResult, RxResult, UnsubscribePromise } from './base'; export interface RpcRxResult extends RxResult { - raw (...args: Parameters): Observable; + json (...args: Parameters): Observable; + raw (...args: Parameters): Observable; } export interface RpcPromiseResult extends PromiseResult { - raw (...args: Parameters): Promise; - raw (...args: Push, Callback>): UnsubscribePromise; + json (...args: Parameters): Promise; + json (...args: Push, Callback>): UnsubscribePromise; + raw (...args: Parameters): Promise; + raw (...args: Push, Callback>): UnsubscribePromise; } -// FIXME The day TS has higher-kinded types, we can remove this hardcoded stuff export type RpcMethodResult = ApiType extends 'rxjs' ? RpcRxResult : RpcPromiseResult; diff --git a/packages/rpc-core/src/index.ts b/packages/rpc-core/src/index.ts index aa7325cb0bb2..3524f5e484d2 100644 --- a/packages/rpc-core/src/index.ts +++ b/packages/rpc-core/src/index.ts @@ -21,6 +21,8 @@ interface StorageChangeSetJSON { changes: [string, string | null][]; } +type OutputType = 'json' | 'raw' | 'scale'; + const l = logger('rpc-core'); const EMPTY_META = { @@ -211,12 +213,20 @@ export default class Rpc implements RpcInterface { }, {} as RpcInterface[Section]); } - private _createMethodWithRaw (creator: (isRaw: boolean) => (...values: any[]) => Observable): RpcInterfaceMethod { - const call = creator(false) as Partial; + private _memomize (creator: (outputAs: OutputType) => (...values: any[]) => Observable): RpcInterfaceMethod & memoizee.Memoized { + const memoized = memoizee(creator('scale') as RpcInterfaceMethod, { + // Dynamic length for argument + length: false, + // Normalize args so that different args that should be cached + // together are cached together. + // E.g.: `query.my.method('abc') === query.my.method(createType('AccountId', 'abc'));` + normalizer: normalizer(this.#instanceId) + }); - call.raw = creator(true); + memoized.json = creator('json'); + memoized.raw = creator('raw'); - return call as RpcInterfaceMethod; + return memoized; } private _createMethodSend (section: string, method: string, def: DefinitionRpc): RpcInterfaceMethod { @@ -226,7 +236,7 @@ export default class Rpc implements RpcInterface { let memoized: null | RpcInterfaceMethod & memoizee.Memoized = null; // execute the RPC call, doing a registry swap for historic as applicable - const callWithRegistry = async (isRaw: boolean, values: any[]): Promise => { + const callWithRegistry = async (outputAs: OutputType, values: any[]): Promise => { const hash = hashIndex === -1 ? undefined : values[hashIndex] as Uint8Array; @@ -236,16 +246,16 @@ export default class Rpc implements RpcInterface { const params = this._formatInputs(registry, def, values); const data = await this.provider.send(rpcName, params.map((param): AnyJson => param.toJSON())) as AnyJson; - return isRaw - ? registry.createType('Raw', data) - : this._formatOutput(registry, method, def, params, data); + return outputAs === 'scale' + ? this._formatOutput(registry, method, def, params, data) + : registry.createType(outputAs === 'raw' ? 'Raw' : 'Json', data); }; - const creator = (isRaw: boolean) => (...values: any[]): Observable => { + const creator = (outputAs: OutputType) => (...values: any[]): Observable => { const isDelayed = (hashIndex !== -1 && !!values[hashIndex]) || (cacheIndex !== -1 && !!values[cacheIndex]); return new Observable((observer: Observer): VoidCallback => { - callWithRegistry(isRaw, values) + callWithRegistry(outputAs, values) .then((value): void => { observer.next(value); observer.complete(); @@ -269,10 +279,7 @@ export default class Rpc implements RpcInterface { ); }; - memoized = memoizee(this._createMethodWithRaw(creator), { - length: false, - normalizer: normalizer(this.#instanceId) - }); + memoized = this._memomize(creator); return memoized; } @@ -297,7 +304,7 @@ export default class Rpc implements RpcInterface { const subType = `${section}_${updateType}`; let memoized: null | RpcInterfaceMethod & memoizee.Memoized = null; - const creator = (isRaw: boolean) => (...values: unknown[]): Observable => { + const creator = (outputAs: OutputType) => (...values: unknown[]): Observable => { return new Observable((observer: Observer): VoidCallback => { // Have at least an empty promise, as used in the unsubscribe let subscriptionPromise: Promise = Promise.resolve(null); @@ -322,9 +329,9 @@ export default class Rpc implements RpcInterface { try { observer.next( - isRaw - ? registry.createType('Raw', result) - : this._formatOutput(registry, method, def, params, result) + outputAs === 'scale' + ? this._formatOutput(registry, method, def, params, result) + : registry.createType(outputAs === 'raw' ? 'Raw' : 'Json', result) ); } catch (error) { observer.error(error); @@ -353,14 +360,7 @@ export default class Rpc implements RpcInterface { }).pipe(drr()); }; - memoized = memoizee(this._createMethodWithRaw(creator), { - // Dynamic length for argument - length: false, - // Normalize args so that different args that should be cached - // together are cached together. - // E.g.: `query.my.method('abc') === query.my.method(createType('AccountId', 'abc'));` - normalizer: normalizer(this.#instanceId) - }); + memoized = this._memomize(creator); return memoized; } diff --git a/packages/rpc-core/src/types.ts b/packages/rpc-core/src/types.ts index f4eced4ce879..34f554460826 100644 --- a/packages/rpc-core/src/types.ts +++ b/packages/rpc-core/src/types.ts @@ -7,5 +7,6 @@ export * from './types.jsonrpc'; export interface RpcInterfaceMethod { (...params: any[]): Observable; + json (...params: any[]): Observable; raw (...params: any[]): Observable; } diff --git a/packages/typegen/src/generate/interfaceRegistry.ts b/packages/typegen/src/generate/interfaceRegistry.ts index a2639af8af2c..b0ea83badb0f 100644 --- a/packages/typegen/src/generate/interfaceRegistry.ts +++ b/packages/typegen/src/generate/interfaceRegistry.ts @@ -3,6 +3,7 @@ import Handlebars from 'handlebars'; +import Json from '@polkadot/types/codec/Json'; import Raw from '@polkadot/types/codec/Raw'; import { TypeRegistry } from '@polkadot/types/create'; import * as defaultDefinitions from '@polkadot/types/interfaces/definitions'; @@ -13,6 +14,7 @@ import { ModuleTypes } from '../util/imports'; const primitiveClasses = { ...defaultPrimitives, + Json, Raw }; diff --git a/packages/types/src/augment/registry.ts b/packages/types/src/augment/registry.ts index 4b32c10c245b..f32a0446ac69 100644 --- a/packages/types/src/augment/registry.ts +++ b/packages/types/src/augment/registry.ts @@ -1,7 +1,7 @@ // Auto-generated via `yarn polkadot-types-from-defs`, do not edit /* eslint-disable */ -import { Compact, Option, Raw, Vec } from '@polkadot/types/codec'; +import { Compact, Json, Option, Raw, Vec } from '@polkadot/types/codec'; import { BitVec, Bytes, Data, DoNotConstruct, Null, StorageKey, Text, Type, U256, bool, i128, i16, i256, i32, i64, i8, u128, u16, u256, u32, u64, u8, usize } from '@polkadot/types/primitive'; import { BlockAttestations, IncludedBlocks, MoreAttestations } from '@polkadot/types/interfaces/attestations'; import { RawAuraPreDigest } from '@polkadot/types/interfaces/aura'; @@ -14,7 +14,7 @@ import { PrefixedStorageKey } from '@polkadot/types/interfaces/childstate'; import { EthereumAddress, StatementKind } from '@polkadot/types/interfaces/claims'; import { CollectiveOrigin, MemberCount, ProposalIndex, Votes, VotesTo230 } from '@polkadot/types/interfaces/collective'; import { AuthorityId, RawVRFOutput } from '@polkadot/types/interfaces/consensus'; -import { AliveContractInfo, CodeHash, ContractCallRequest, ContractExecResult, ContractExecResultSuccess, ContractExecResultSuccessTo255, ContractExecResultTo255, ContractInfo, ContractStorageKey, Gas, HostFnWeights, InstructionWeights, PrefabWasmModule, PrefabWasmModuleReserved, Schedule, ScheduleTo212, ScheduleTo258, SeedOf, TombstoneContractInfo, TrieId } from '@polkadot/types/interfaces/contracts'; +import { AliveContractInfo, CodeHash, ContractCallRequest, ContractExecResult, ContractExecResultErr, ContractExecResultErrModule, ContractExecResultOk, ContractExecResultResult, ContractExecResultSuccessTo255, ContractExecResultSuccessTo260, ContractExecResultTo255, ContractExecResultTo260, ContractInfo, ContractStorageKey, Gas, HostFnWeights, InstructionWeights, PrefabWasmModule, PrefabWasmModuleReserved, Schedule, ScheduleTo212, ScheduleTo258, SeedOf, TombstoneContractInfo, TrieId } from '@polkadot/types/interfaces/contracts'; import { ContractConstructorSpec, ContractContractSpec, ContractCryptoHasher, ContractDiscriminant, ContractDisplayName, ContractEventParamSpec, ContractEventSpec, ContractLayoutArray, ContractLayoutCell, ContractLayoutEnum, ContractLayoutHash, ContractLayoutHashingStrategy, ContractLayoutKey, ContractLayoutStruct, ContractLayoutStructField, ContractMessageParamSpec, ContractMessageSpec, ContractProject, ContractProjectContract, ContractProjectSource, ContractSelector, ContractStorageLayout, ContractTypeSpec } from '@polkadot/types/interfaces/contractsAbi'; import { AccountVote, AccountVoteSplit, AccountVoteStandard, Conviction, Delegations, PreimageStatus, PreimageStatusAvailable, PriorLock, PropIndex, Proposal, ProxyState, ReferendumIndex, ReferendumInfo, ReferendumInfoFinished, ReferendumInfoTo239, ReferendumStatus, Tally, Voting, VotingDelegating, VotingDirect, VotingDirectVote } from '@polkadot/types/interfaces/democracy'; import { ApprovalFlag, DefunctVoter, Renouncing, SetIndex, Vote, VoteIndex, VoteThreshold, VoterInfo } from '@polkadot/types/interfaces/elections'; @@ -129,6 +129,9 @@ declare module '@polkadot/types/types/registry' { 'Compact': Compact; 'Option': Option; 'Vec': Vec; + Json: Json; + 'Option': Option; + 'Vec': Vec; Raw: Raw; 'Option': Option; 'Vec': Vec; @@ -487,9 +490,24 @@ declare module '@polkadot/types/types/registry' { ContractExecResultTo255: ContractExecResultTo255; 'Option': Option; 'Vec': Vec; - ContractExecResultSuccess: ContractExecResultSuccess; - 'Option': Option; - 'Vec': Vec; + ContractExecResultSuccessTo260: ContractExecResultSuccessTo260; + 'Option': Option; + 'Vec': Vec; + ContractExecResultTo260: ContractExecResultTo260; + 'Option': Option; + 'Vec': Vec; + ContractExecResultErrModule: ContractExecResultErrModule; + 'Option': Option; + 'Vec': Vec; + ContractExecResultErr: ContractExecResultErr; + 'Option': Option; + 'Vec': Vec; + ContractExecResultOk: ContractExecResultOk; + 'Option': Option; + 'Vec': Vec; + ContractExecResultResult: ContractExecResultResult; + 'Option': Option; + 'Vec': Vec; ContractExecResult: ContractExecResult; 'Option': Option; 'Vec': Vec; diff --git a/packages/types/src/interfaces/contracts/definitions.ts b/packages/types/src/interfaces/contracts/definitions.ts index 8bb4b84656fc..e48935ea93ff 100644 --- a/packages/types/src/interfaces/contracts/definitions.ts +++ b/packages/types/src/interfaces/contracts/definitions.ts @@ -90,17 +90,45 @@ export default { Error: 'Null' } }, - ContractExecResultSuccess: { + ContractExecResultSuccessTo260: { flags: 'u32', data: 'Bytes', gasConsumed: 'u64' }, - ContractExecResult: { + ContractExecResultTo260: { _enum: { - Success: 'ContractExecResultSuccess', + Success: 'ContractExecResultSuccessTo260', Error: 'Null' } }, + ContractExecResultErrModule: { + index: 'u8', + error: 'u8', + message: 'Option' + }, + ContractExecResultErr: { + _enum: { + Other: 'Text', + CannotLookup: 'Null', + BadOrigin: 'Null', + Module: 'ContractExecResultErrModule' + } + }, + ContractExecResultOk: { + flags: 'u32', + data: 'Bytes' + }, + ContractExecResultResult: { + _enum: { + Ok: 'ContractExecResultOk', + Err: 'ContractExecResultErr' + } + }, + ContractExecResult: { + gasConsumed: 'u64', + debugMessage: 'Text', + result: 'ContractExecResultResult' + }, ContractInfo: { _enum: { Alive: 'AliveContractInfo', diff --git a/packages/types/src/interfaces/contracts/types.ts b/packages/types/src/interfaces/contracts/types.ts index 2276003a7b6b..1fba8fe879e1 100644 --- a/packages/types/src/interfaces/contracts/types.ts +++ b/packages/types/src/interfaces/contracts/types.ts @@ -2,7 +2,7 @@ /* eslint-disable */ import { Compact, Enum, Option, Raw, Struct, U8aFixed } from '@polkadot/types/codec'; -import { Bytes, Null, bool, u32, u64, u8 } from '@polkadot/types/primitive'; +import { Bytes, Null, Text, bool, u32, u64, u8 } from '@polkadot/types/primitive'; import { AccountId, Balance, BlockNumber, Hash, Weight } from '@polkadot/types/interfaces/runtime'; /** @name AliveContractInfo */ @@ -30,17 +30,41 @@ export interface ContractCallRequest extends Struct { } /** @name ContractExecResult */ -export interface ContractExecResult extends Enum { - readonly isSuccess: boolean; - readonly asSuccess: ContractExecResultSuccess; - readonly isError: boolean; +export interface ContractExecResult extends Struct { + readonly gasConsumed: u64; + readonly debugMessage: Text; + readonly result: ContractExecResultResult; +} + +/** @name ContractExecResultErr */ +export interface ContractExecResultErr extends Enum { + readonly isOther: boolean; + readonly asOther: Text; + readonly isCannotLookup: boolean; + readonly isBadOrigin: boolean; + readonly isModule: boolean; + readonly asModule: ContractExecResultErrModule; } -/** @name ContractExecResultSuccess */ -export interface ContractExecResultSuccess extends Struct { +/** @name ContractExecResultErrModule */ +export interface ContractExecResultErrModule extends Struct { + readonly index: u8; + readonly error: u8; + readonly message: Option; +} + +/** @name ContractExecResultOk */ +export interface ContractExecResultOk extends Struct { readonly flags: u32; readonly data: Bytes; - readonly gasConsumed: u64; +} + +/** @name ContractExecResultResult */ +export interface ContractExecResultResult extends Enum { + readonly isOk: boolean; + readonly asOk: ContractExecResultOk; + readonly isErr: boolean; + readonly asErr: ContractExecResultErr; } /** @name ContractExecResultSuccessTo255 */ @@ -49,6 +73,13 @@ export interface ContractExecResultSuccessTo255 extends Struct { readonly data: Raw; } +/** @name ContractExecResultSuccessTo260 */ +export interface ContractExecResultSuccessTo260 extends Struct { + readonly flags: u32; + readonly data: Bytes; + readonly gasConsumed: u64; +} + /** @name ContractExecResultTo255 */ export interface ContractExecResultTo255 extends Enum { readonly isSuccess: boolean; @@ -56,6 +87,13 @@ export interface ContractExecResultTo255 extends Enum { readonly isError: boolean; } +/** @name ContractExecResultTo260 */ +export interface ContractExecResultTo260 extends Enum { + readonly isSuccess: boolean; + readonly asSuccess: ContractExecResultSuccessTo260; + readonly isError: boolean; +} + /** @name ContractInfo */ export interface ContractInfo extends Enum { readonly isAlive: boolean;