From 1cb847a63afa20d938934fc4619b9ce363df7f7f Mon Sep 17 00:00:00 2001 From: Ian He <39037239+ianhe8x@users.noreply.github.com> Date: Sat, 2 Mar 2019 01:18:34 +1300 Subject: [PATCH] allow custom Extrinsic implementation (#739) * refactor: allow custom Extrinsic implementation * decorate Extrinsic into SubmittableExtrinsic * per comments * avoid cyclical dependency * try to make codeclimate happy * code clean up * fix after rebase master --- packages/api/src/Base.ts | 8 +- packages/api/src/SubmittableExtrinsic.ts | 209 +++++++++--------- packages/api/src/promise/types.ts | 2 +- packages/api/src/types.ts | 14 +- packages/api/src/util/filterEvents.ts | 4 +- .../types/src/Metadata/util/validateTypes.ts | 5 +- packages/types/src/codec/createType.ts | 14 +- packages/types/src/codec/index.ts | 2 +- packages/types/src/codec/typeRegistry.ts | 44 +++- packages/types/src/types.ts | 17 ++ 10 files changed, 186 insertions(+), 133 deletions(-) diff --git a/packages/api/src/Base.ts b/packages/api/src/Base.ts index 783f576002..67be73b7f3 100644 --- a/packages/api/src/Base.ts +++ b/packages/api/src/Base.ts @@ -23,13 +23,13 @@ import extrinsicsFromMeta from '@polkadot/extrinsics/fromMetadata'; import RpcBase from '@polkadot/rpc-core/index'; import RpcRx from '@polkadot/rpc-rx/index'; import storageFromMeta from '@polkadot/storage/fromMetadata'; -import { Event, Hash, Metadata, Method, RuntimeVersion, typeRegistry } from '@polkadot/types/index'; +import { Event, getTypeRegistry, Hash, Metadata, Method, RuntimeVersion } from '@polkadot/types/index'; import { MethodFunction, ModulesWithMethods } from '@polkadot/types/primitive/Method'; import { StorageFunction } from '@polkadot/types/primitive/StorageKey'; import { assert, compactStripLength, isFunction, isObject, isUndefined, logger, u8aToHex } from '@polkadot/util'; import { cryptoWaitReady } from '@polkadot/util-crypto'; -import SubmittableExtrinsic from './SubmittableExtrinsic'; +import { createSubmittableExtrinsic, SubmittableExtrinsic } from './SubmittableExtrinsic'; type MetaDecoration = { callIndex?: Uint8Array, @@ -319,7 +319,7 @@ export default abstract class ApiBase implement */ registerTypes (types?: RegistryTypes): void { if (types) { - typeRegistry.register(types); + getTypeRegistry().register(types); } } @@ -460,7 +460,7 @@ export default abstract class ApiBase implement private decorateExtrinsicEntry (method: MethodFunction, onCall: OnCallDefinition): SubmittableExtrinsicFunction { const decorated: any = (...params: Array): SubmittableExtrinsic => - new SubmittableExtrinsic(this.type, this._rx as ApiInterface$Rx, onCall, method(...params)); + createSubmittableExtrinsic(this.type, this._rx as ApiInterface$Rx, onCall, method(...params)); return this.decorateFunctionMeta(method, decorated) as SubmittableExtrinsicFunction; } diff --git a/packages/api/src/SubmittableExtrinsic.ts b/packages/api/src/SubmittableExtrinsic.ts index b7577c75e5..b00ffb5e05 100644 --- a/packages/api/src/SubmittableExtrinsic.ts +++ b/packages/api/src/SubmittableExtrinsic.ts @@ -3,8 +3,8 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { KeyringPair } from '@polkadot/keyring/types'; -import { AccountId, Address, Extrinsic, ExtrinsicStatus, EventRecord, Hash, Index, Method, SignedBlock, Struct, Text, Vector } from '@polkadot/types/index'; -import { Codec, CodecCallback, SignatureOptions } from '@polkadot/types/types'; +import { AccountId, Address, ExtrinsicStatus, EventRecord, getTypeRegistry, Hash, Index, Method, SignedBlock, Struct, Text, Vector } from '@polkadot/types/index'; +import { Codec, CodecCallback, IExtrinsic, SignatureOptions } from '@polkadot/types/types'; import { ApiInterface$Rx, ApiType, OnCallDefinition, Signer } from './types'; import { Observable, of, combineLatest } from 'rxjs'; @@ -18,7 +18,7 @@ type StatusCb = (result: SubmittableResult) => any; type SumbitableResultResult = CodecResult extends Observable ? Observable - : Promise; + : Promise; type SumbitableResultSubscription = SubscriptionResult extends Observable @@ -56,26 +56,29 @@ export class SubmittableResult extends Struct { } } -export default class SubmittableExtrinsic extends Extrinsic { - private _api: ApiInterface$Rx; - private _onCall: OnCallDefinition; - private _noStatusCb?: boolean; +export interface SubmittableExtrinsic extends IExtrinsic { + send (): SumbitableResultResult; - constructor (type: ApiType, api: ApiInterface$Rx, onCall: OnCallDefinition, extrinsic: Extrinsic | Method) { - super(extrinsic); + send (statusCb: (result: SubmittableResult) => any): SumbitableResultSubscription; - this._noStatusCb = type === 'rxjs'; - this._api = api; - this._onCall = onCall; - } + sign (account: KeyringPair, _options: Partial): this; - private updateSigner (updateId: number, status: Hash | SubmittableResult): void { - if ((updateId !== -1) && this._api.signer && this._api.signer.update) { - this._api.signer.update(updateId, status); + signAndSend (account: KeyringPair | string | AccountId | Address, options?: Partial>): SumbitableResultResult; + + signAndSend (account: KeyringPair | string | AccountId | Address, statusCb: StatusCb): SumbitableResultSubscription; +} + +export function createSubmittableExtrinsic (type: ApiType, api: ApiInterface$Rx, onCall: OnCallDefinition, extrinsic: Method): SubmittableExtrinsic { + const _extrinsic = new (getTypeRegistry().getOrThrow('Extrinsic'))(extrinsic) as SubmittableExtrinsic; + const _noStatusCb = type === 'rxjs'; + + function updateSigner (updateId: number, status: Hash | SubmittableResult): void { + if ((updateId !== -1) && api.signer && api.signer.update) { + api.signer.update(updateId, status); } } - private statusObservable (status: ExtrinsicStatus): Observable { + function statusObservable (status: ExtrinsicStatus): Observable { if (status.type !== 'Finalised') { return of( new SubmittableResult({ @@ -88,12 +91,12 @@ export default class SubmittableExtrinsic exten const blockHash = status.asFinalised; return combineLatest( - this._api.rpc.chain.getBlock(blockHash) as Observable, - this._api.query.system.events.at(blockHash) as Observable> + api.rpc.chain.getBlock(blockHash) as Observable, + api.query.system.events.at(blockHash) as Observable> ).pipe( map(([signedBlock, allEvents]) => new SubmittableResult({ - events: filterEvents(this.hash, signedBlock, allEvents), + events: filterEvents(_extrinsic.hash, signedBlock, allEvents), status, type: status.type }) @@ -101,108 +104,116 @@ export default class SubmittableExtrinsic exten ); } - private sendObservable (updateId: number = -1): Observable { - return (this._api.rpc.author - .submitExtrinsic(this) as Observable) + function sendObservable (updateId: number = -1): Observable { + return (api.rpc.author + .submitExtrinsic(_extrinsic) as Observable) .pipe( tap((hash) => { - this.updateSigner(updateId, hash); + updateSigner(updateId, hash); }) ); } - private subscribeObservable (updateId: number = -1): Observable { - return (this._api.rpc.author - .submitAndWatchExtrinsic(this) as Observable) + function subscribeObservable (updateId: number = -1): Observable { + return (api.rpc.author + .submitAndWatchExtrinsic(_extrinsic) as Observable) .pipe( switchMap((status) => - this.statusObservable(status) + statusObservable(status) ), tap((status) => { - this.updateSigner(updateId, status); + updateSigner(updateId, status); }) ); } - private expandOptions (options: Partial): SignatureOptions { + function expandOptions (options: Partial): SignatureOptions { return { - blockHash: this._api.genesisHash, - version: this._api.runtimeVersion, + blockHash: api.genesisHash, + version: api.runtimeVersion, ...options } as SignatureOptions; } - send (): SumbitableResultResult; - send (statusCb: (result: SubmittableResult) => any): SumbitableResultSubscription; - send (statusCb?: (result: SubmittableResult) => any): SumbitableResultResult | SumbitableResultSubscription { - const isSubscription = this._noStatusCb || !!statusCb; + const signOrigin = _extrinsic.sign; + Object.defineProperties( + _extrinsic, + { + send: { + value: function (statusCb?: (result: SubmittableResult) => any): SumbitableResultResult | SumbitableResultSubscription { + const isSubscription = _noStatusCb || !!statusCb; - return this._onCall( - () => isSubscription - ? this.subscribeObservable() - : this.sendObservable(), - [], - statusCb as CodecCallback, - isSubscription - ) as unknown as SumbitableResultSubscription; - } + return onCall( + () => isSubscription + ? subscribeObservable() + : sendObservable(), + [], + statusCb as CodecCallback, + isSubscription + ) as unknown as SumbitableResultSubscription; + } + }, + sign: { + value: function (account: KeyringPair, _options: Partial): SubmittableExtrinsic { + // HACK here we actually override nonce if it was specified (backwards compat for + // the previous signature - don't let userspace break, but allow then time to upgrade) + const options: Partial = isBn(_options) || isNumber(_options) + ? { nonce: _options as any as number } + : _options; - // NOTE with the nonce hack, we don't add a signature overload for 2 reasons - - // - it makes it incompatible with the base Extrinsic - // - for those using TS, we would like to move them towards the new version - sign (account: KeyringPair, _options: Partial): SubmittableExtrinsic { - // HACK here we actually override nonce if it was specified (backwards compat for - // the previous signature - don't let userspace break, but allow then time to upgrade) - const options: Partial = isBn(_options) || isNumber(_options) - ? { nonce: _options as any as number } - : _options; + signOrigin.apply(_extrinsic, [account, expandOptions(options)]); - super.sign(account, this.expandOptions(options)); + return this; + } + }, + signAndSend: { + value: function (account: KeyringPair | string | AccountId | Address, _options?: Partial> | StatusCb, statusCb?: StatusCb): SumbitableResultResult | SumbitableResultSubscription { + let options: Partial> = {}; - return this; - } - - signAndSend (account: KeyringPair | string | AccountId | Address, options?: Partial>): SumbitableResultResult; - signAndSend (account: KeyringPair | string | AccountId | Address, statusCb: StatusCb): SumbitableResultSubscription; - signAndSend (account: KeyringPair | string | AccountId | Address, _options?: Partial> | StatusCb, statusCb?: StatusCb): SumbitableResultResult | SumbitableResultSubscription { - let options: Partial> = {}; - - if (isFunction(_options)) { - statusCb = _options; - } else { - options = _options || {}; - } - - const isSubscription = this._noStatusCb || !!statusCb; - const isKeyringPair = isFunction((account as KeyringPair).address) && isFunction((account as KeyringPair).sign); - const address = isKeyringPair ? (account as KeyringPair).address() : account.toString(); - let updateId: number | undefined; - - return this._onCall( - () => (( - isUndefined(options.nonce) - ? this._api.query.system.accountNonce(address) as Observable - : of(new Index(options.nonce)) - ).pipe( - first(), - mergeMap(async (nonce) => { - if (isKeyringPair) { - this.sign(account as KeyringPair, { ...options, nonce }); + if (isFunction(_options)) { + statusCb = _options; } else { - assert(this._api.signer, 'no signer exists'); - - updateId = await (this._api.signer as Signer).sign(this, address, this.expandOptions({ ...options, nonce })); + options = _options || {}; } - }), - switchMap(() => { - return isSubscription - ? this.subscribeObservable(updateId) - : this.sendObservable(updateId) as any; // ??? - }) - ) as Observable), - [], - statusCb as CodecCallback, - isSubscription - ) as unknown as SumbitableResultSubscription; - } + + const isSubscription = _noStatusCb || !!statusCb; + const isKeyringPair = isFunction((account as KeyringPair).address) && isFunction((account as KeyringPair).sign); + const address = isKeyringPair ? (account as KeyringPair).address() : account.toString(); + let updateId: number | undefined; + + return onCall( + () => (( + isUndefined(options.nonce) + ? api.query.system.accountNonce(address) as Observable + : of(new Index(options.nonce)) + ).pipe( + first(), + mergeMap(async (nonce) => { + if (isKeyringPair) { + this.sign(account as KeyringPair, { ...options, nonce }); + } else { + assert(api.signer, 'no signer exists'); + + updateId = await (api.signer as Signer).sign(_extrinsic, address, expandOptions({ + ...options, + nonce + })); + } + }), + switchMap(() => { + return isSubscription + ? subscribeObservable(updateId) + : sendObservable(updateId) as any; // ??? + }) + ) as Observable), + [], + statusCb as CodecCallback, + isSubscription + ) as unknown as SumbitableResultSubscription; + } + } + } + ); + + return _extrinsic; } diff --git a/packages/api/src/promise/types.ts b/packages/api/src/promise/types.ts index ec0a3ee92d..01968cd0da 100644 --- a/packages/api/src/promise/types.ts +++ b/packages/api/src/promise/types.ts @@ -6,7 +6,7 @@ import { Codec } from '@polkadot/types/types'; import { QueryableStorageFunction as QueryableStorageFunctionBase, SubmittableExtrinsicFunction as SubmittableExtrinsicFunctionBase } from '../types'; import ApiBase from '../Base'; -import SubmittableExtrinsicBase from '../SubmittableExtrinsic'; +import { SubmittableExtrinsic as SubmittableExtrinsicBase } from '../SubmittableExtrinsic'; export type SubscriptionResult = Promise<() => any>; diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 946d0d08df..6ec6b74982 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -6,13 +6,19 @@ import { Observable } from 'rxjs'; import { DeriveCustom } from '@polkadot/api-derive/index'; import { ProviderInterface } from '@polkadot/rpc-provider/types'; import { RpcRxInterface$Events } from '@polkadot/rpc-rx/types'; -import { Extrinsic, Hash, Metadata, RuntimeVersion, u64 as U64 } from '@polkadot/types/index'; -import { CodecArg, CodecCallback, RegistryTypes, SignatureOptions } from '@polkadot/types/types'; +import { Hash, Metadata, RuntimeVersion, u64 as U64 } from '@polkadot/types/index'; +import { + CodecArg, + CodecCallback, + IExtrinsic, + RegistryTypes, + SignatureOptions +} from '@polkadot/types/types'; import { MethodFunction } from '@polkadot/types/primitive/Method'; import { StorageFunction } from '@polkadot/types/primitive/StorageKey'; import { RxResult } from './rx/types'; -import SubmittableExtrinsic, { SubmittableResult } from './SubmittableExtrinsic'; +import { SubmittableResult, SubmittableExtrinsic } from './SubmittableExtrinsic'; import ApiBase from './Base'; export type OnCallDefinition = (method: OnCallFunction, params?: Array, callback?: CodecCallback, needsCallback?: boolean) => CodecResult | SubscriptionResult; @@ -163,7 +169,7 @@ export interface Signer { /** * @description Signs an extrinsic, returning an id (>0) that can be used to retrieve updates */ - sign (extrinsic: Extrinsic, address: string, options: SignatureOptions): Promise; + sign (extrinsic: IExtrinsic, address: string, options: SignatureOptions): Promise; /** * @description Receives an update for the extrinsic signed by a `signer.sign` diff --git a/packages/api/src/util/filterEvents.ts b/packages/api/src/util/filterEvents.ts index 686ce36c0a..c323c7f549 100644 --- a/packages/api/src/util/filterEvents.ts +++ b/packages/api/src/util/filterEvents.ts @@ -2,11 +2,11 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { EventRecord, Hash, SignedBlock } from '@polkadot/types/index'; +import { EventRecord, SignedBlock, U8a } from '@polkadot/types/index'; import l from './logging'; -export default function filterEvents (extHash: Hash, { block: { extrinsics, header } }: SignedBlock, allEvents: Array): Array | undefined { +export default function filterEvents (extHash: U8a, { block: { extrinsics, header } }: SignedBlock, allEvents: Array): Array | undefined { // extrinsics to hashes const myHash = extHash.toHex(); const allHashes = extrinsics.map((ext) => ext.hash.toHex()); diff --git a/packages/types/src/Metadata/util/validateTypes.ts b/packages/types/src/Metadata/util/validateTypes.ts index 6f972dd6d2..c3407072e9 100644 --- a/packages/types/src/Metadata/util/validateTypes.ts +++ b/packages/types/src/Metadata/util/validateTypes.ts @@ -6,6 +6,7 @@ import { isUndefined } from '@polkadot/util'; import { getTypeDef, TypeDef, TypeDefInfo } from '../../codec/createType'; import flattenUniq from './flattenUniq'; +import { getTypeRegistry } from '../../codec'; export default function validateTypes (types: Array, throwError: boolean): void { const extractTypes = (types: Array): Array => { @@ -32,9 +33,9 @@ export default function validateTypes (types: Array, throwError: boolean }); }; - const allTypes = require('../../index'); + const typeRegistry = getTypeRegistry(); const missing = flattenUniq(extractTypes(types)).filter((type) => - isUndefined(allTypes[type]) + isUndefined(typeRegistry.get(type)) ); if (missing.length !== 0) { diff --git a/packages/types/src/codec/createType.ts b/packages/types/src/codec/createType.ts index ff747f9898..9d633f90be 100644 --- a/packages/types/src/codec/createType.ts +++ b/packages/types/src/codec/createType.ts @@ -13,7 +13,7 @@ import Struct from './Struct'; import Tuple from './Tuple'; import UInt from './UInt'; import Vector from './Vector'; -import registry from './typeRegistry'; +import getRegistry from './typeRegistry'; export enum TypeDefInfo { Compact, @@ -137,6 +137,10 @@ export function getTypeDef (_type: Text | string, name?: string): TypeDef { // Returns the type Class for construction export function getTypeClass (value: TypeDef): Constructor { + const Type = getRegistry().get(value.type); + if (Type) { + return Type; + } if (value.info === TypeDefInfo.Compact) { assert(value.sub && !Array.isArray(value.sub), 'Expected subtype for Compact'); @@ -183,13 +187,7 @@ export function getTypeClass (value: TypeDef): Constructor { ); } - // NOTE We only load types via require - we have to avoid circular deps between type usage and creation - const Types = require('../index.types'); - const Type = registry.get(value.type) || Types[value.type]; - - assert(Type, `Unable to determine type from '${value.type}'`); - - return Type; + throw new Error(`Unable to determine type from '${value.type}'`); } export function createClass (type: Text | string, value?: any): Constructor { diff --git a/packages/types/src/codec/index.ts b/packages/types/src/codec/index.ts index 9b1ea41109..d838a9e49e 100644 --- a/packages/types/src/codec/index.ts +++ b/packages/types/src/codec/index.ts @@ -23,4 +23,4 @@ export { default as UInt } from './UInt'; // Type management helper functions export { default as createType, createClass, getTypeClass, getTypeDef, TypeDef, TypeDefInfo } from './createType'; -export { default as typeRegistry } from './typeRegistry'; +export { default as getTypeRegistry } from './typeRegistry'; diff --git a/packages/types/src/codec/typeRegistry.ts b/packages/types/src/codec/typeRegistry.ts index 9fc27ea803..3c794c9f36 100644 --- a/packages/types/src/codec/typeRegistry.ts +++ b/packages/types/src/codec/typeRegistry.ts @@ -2,10 +2,9 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. +import { isFunction, isString, isUndefined } from '@polkadot/util'; + import { Constructor, RegistryTypes } from '../types'; - -import { isFunction, isString } from '@polkadot/util'; - import { createClass } from './createType'; export class TypeRegistry { @@ -31,15 +30,17 @@ export class TypeRegistry { } } - private registerObject (obj: RegistryTypes) { + private registerObject (obj: RegistryTypes, overwrite: boolean = true) { Object.entries(obj).forEach(([name, type]) => { - if (isString(type)) { - this._registry.set(name, createClass(type)); - } else if (isFunction(type)) { - // This _looks_ a bit funny, but `typeof Clazz === 'function' - this._registry.set(name, type); - } else { - this._registry.set(name, createClass(JSON.stringify(type))); + if (overwrite || !this.get(name)) { + if (isString(type)) { + this._registry.set(name, createClass(type)); + } else if (isFunction(type)) { + // This _looks_ a bit funny, but `typeof Clazz === 'function' + this._registry.set(name, type); + } else { + this._registry.set(name, createClass(JSON.stringify(type))); + } } }); } @@ -47,6 +48,25 @@ export class TypeRegistry { get (name: string): Constructor | undefined { return this._registry.get(name); } + + getOrThrow (name: string, msg?: string): Constructor { + const type = this.get(name); + if (isUndefined(type)) { + throw new Error(msg || `type ${name} not found`); + } + + return type; + } } -export default TypeRegistry.defaultRegistry; +let defaultRegistry: TypeRegistry; + +export default function getDefaultRegistry () { + if (!defaultRegistry) { + const defaultTypes = require('../index.types'); + defaultRegistry = new TypeRegistry(); + defaultRegistry.register({ ...defaultTypes }); + } + + return defaultRegistry; +} diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index ae2e013bcf..3a5c432239 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -2,7 +2,11 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. +import { KeyringPair } from '@polkadot/keyring/types'; import BN from 'bn.js'; +import Method from './primitive/Method'; +import Address from './type/Address'; +import U8a from './codec/U8a'; export type CodecArg = Codec | BN | Boolean | String | Uint8Array | boolean | number | string | undefined | CodecArgArray | CodecArgObject; @@ -94,3 +98,16 @@ export type SignatureOptions = { nonce: AnyNumber, version?: RuntimeVersionInterface }; + +export interface IHash extends U8a {} + +export interface ISignature extends Codec {} + +export interface IExtrinsic extends Codec { + hash: IHash; + isSigned: boolean; + method: Method; + signature: ISignature; + addSignature (signer: Address | Uint8Array, signature: Uint8Array, nonce: AnyNumber, era?: Uint8Array): this; + sign (account: KeyringPair, options: SignatureOptions): this; +}