From d905b4fef338bf5f8cb797e37e7d1b091393339d Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Thu, 29 Aug 2019 22:33:57 +0200 Subject: [PATCH] Split Submittable into actual classes (#1350) * Split Submittable into actual classes * *sigh* Fix VSCode's "helpful" imports * Rename option creation functions * Extrinsic already has version embedded * V3 --- packages/api-contract/src/RxBlueprint.ts | 6 +- packages/api-contract/src/RxCode.ts | 8 +- packages/api-contract/src/RxContract.ts | 3 +- packages/api/src/SubmittableExtrinsic.ts | 315 ------------------ packages/api/src/base/Decorate.ts | 14 +- packages/api/src/index.ts | 2 +- packages/api/src/promise/types.ts | 2 +- packages/api/src/submittable/Extrinsic.ts | 262 +++++++++++++++ packages/api/src/submittable/Result.ts | 38 +++ packages/api/src/submittable/index.ts | 6 + packages/api/src/submittable/types.ts | 54 +++ packages/api/src/types.ts | 14 +- packages/api/src/util/index.ts | 8 + packages/api/src/util/isKeyringPair.ts | 12 + .../src/primitive/Extrinsic/Extrinsic.ts | 2 +- 15 files changed, 403 insertions(+), 343 deletions(-) delete mode 100644 packages/api/src/SubmittableExtrinsic.ts create mode 100644 packages/api/src/submittable/Extrinsic.ts create mode 100644 packages/api/src/submittable/Result.ts create mode 100644 packages/api/src/submittable/index.ts create mode 100644 packages/api/src/submittable/types.ts create mode 100644 packages/api/src/util/index.ts create mode 100644 packages/api/src/util/isKeyringPair.ts diff --git a/packages/api-contract/src/RxBlueprint.ts b/packages/api-contract/src/RxBlueprint.ts index 09ce5a4cde..5c5e234f4c 100644 --- a/packages/api-contract/src/RxBlueprint.ts +++ b/packages/api-contract/src/RxBlueprint.ts @@ -2,7 +2,7 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { ISubmittableResult, SubmittableResult } from '@polkadot/api/SubmittableExtrinsic'; +import { SubmittableResultImpl } from '@polkadot/api/types'; import { AccountId, Address, Hash } from '@polkadot/types/interfaces'; import { IKeyringPair } from '@polkadot/types/types'; import { ContractABI } from './types'; @@ -10,7 +10,7 @@ import { ContractABI } from './types'; import BN from 'bn.js'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiRx } from '@polkadot/api'; +import { ApiRx, SubmittableResult } from '@polkadot/api'; import { createType } from '@polkadot/types'; import Abi from './Abi'; @@ -26,7 +26,7 @@ export interface BlueprintCreate { class BlueprintCreateResult extends SubmittableResult { public readonly contract?: RxContract; - public constructor (result: ISubmittableResult, contract?: RxContract) { + public constructor (result: SubmittableResultImpl, contract?: RxContract) { super(result); this.contract = contract; diff --git a/packages/api-contract/src/RxCode.ts b/packages/api-contract/src/RxCode.ts index d5fce9c4c5..c4716387fb 100644 --- a/packages/api-contract/src/RxCode.ts +++ b/packages/api-contract/src/RxCode.ts @@ -2,7 +2,7 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { ISubmittableResult, SubmittableResult } from '@polkadot/api/SubmittableExtrinsic'; +import { SubmittableResultImpl } from '@polkadot/api/types'; import { AccountId, Address, Hash } from '@polkadot/types/interfaces'; import { IKeyringPair } from '@polkadot/types/types'; import { ContractABI } from './types'; @@ -10,7 +10,7 @@ import { ContractABI } from './types'; import BN from 'bn.js'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiRx } from '@polkadot/api'; +import { ApiRx, SubmittableResult } from '@polkadot/api'; import { compactAddLength, u8aToU8a } from '@polkadot/util'; import Abi from './Abi'; @@ -39,7 +39,7 @@ export interface CodePutCode { class CodePutCodeResult extends SubmittableResult { public readonly blueprint?: RxBlueprint; - public constructor (result: ISubmittableResult, blueprint?: RxBlueprint) { + public constructor (result: SubmittableResultImpl, blueprint?: RxBlueprint) { super(result); this.blueprint = blueprint; @@ -67,7 +67,7 @@ export default class RxCode extends RxBase { return { signAndSend }; } - private createResult = (result: ISubmittableResult): CodePutCodeResult => { + private createResult = (result: SubmittableResultImpl): CodePutCodeResult => { let blueprint: RxBlueprint | undefined; if (result.isFinalized) { diff --git a/packages/api-contract/src/RxContract.ts b/packages/api-contract/src/RxContract.ts index 25a168241f..6e09a7e346 100644 --- a/packages/api-contract/src/RxContract.ts +++ b/packages/api-contract/src/RxContract.ts @@ -8,8 +8,7 @@ import { ContractABI, ContractABIFn, InterfaceContract, InterfaceContractCalls } import BN from 'bn.js'; import { Observable } from 'rxjs'; -import { ApiRx } from '@polkadot/api'; -import { SubmittableResult } from '@polkadot/api/SubmittableExtrinsic'; +import { ApiRx, SubmittableResult } from '@polkadot/api'; import { createType } from '@polkadot/types'; import Abi from './Abi'; import RxBase from './RxBase'; diff --git a/packages/api/src/SubmittableExtrinsic.ts b/packages/api/src/SubmittableExtrinsic.ts deleted file mode 100644 index b963912680..0000000000 --- a/packages/api/src/SubmittableExtrinsic.ts +++ /dev/null @@ -1,315 +0,0 @@ -// Copyright 2017-2019 @polkadot/api authors & contributors -// This software may be modified and distributed under the terms -// of the Apache-2.0 license. See the LICENSE file for details. - -import { AccountId, Address, Call, ExtrinsicEra, ExtrinsicStatus, EventRecord, Hash, Header, Index } from '@polkadot/types/interfaces'; -import { AnyNumber, AnyU8a, Callback, Codec, IExtrinsic, IExtrinsicEra, IKeyringPair, SignatureOptions } from '@polkadot/types/types'; -import { ApiInterfaceRx, ApiTypes } from './types'; - -import BN from 'bn.js'; -import { Observable, combineLatest, of } from 'rxjs'; -import { first, map, mergeMap, switchMap, tap } from 'rxjs/operators'; -import { createType, Vec } from '@polkadot/types'; -import { isBn, isFunction, isNumber, isUndefined } from '@polkadot/util'; - -import filterEvents from './util/filterEvents'; -import ApiBase from './base'; -import SignerPayload from './SignerPayload'; - -// eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface ISubmittableResult { - readonly events: EventRecord[]; - readonly status: ExtrinsicStatus; - readonly isCompleted: boolean; - readonly isError: boolean; - readonly isFinalized: boolean; - - findRecord(section: string, method: string): EventRecord | undefined; -} - -export type SumbitableResultResult = - ApiType extends 'rxjs' - ? Observable - : Promise; - -export type SumbitableResultSubscription = - ApiType extends 'rxjs' - ? Observable - : Promise<() => void>; - -interface SubmittableResultValue { - events?: EventRecord[]; - status: ExtrinsicStatus; -} - -interface SignerOptions { - blockHash: AnyU8a; - era?: IExtrinsicEra | number; - nonce: AnyNumber; - tip?: AnyNumber; -} - -// The default for 6s allowing for 5min eras. When translating this to faster blocks - -// - 4s = (10 / 15) * 5 = 3.33m -// - 2s = (10 / 30) * 5 = 1.66m -const BLOCKTIME = 6; -const ONE_MINUTE = 60 / BLOCKTIME; -const DEFAULT_MORTAL_LENGTH = 5 * ONE_MINUTE; - -function isKeyringPair (account: string | IKeyringPair | AccountId | Address): account is IKeyringPair { - return isFunction((account as IKeyringPair).sign); -} - -export class SubmittableResult implements ISubmittableResult { - public readonly events: EventRecord[]; - - public readonly status: ExtrinsicStatus; - - public constructor ({ events, status }: SubmittableResultValue) { - this.events = events || []; - this.status = status; - } - - public get isCompleted (): boolean { - return this.isError || this.isFinalized; - } - - public get isError (): boolean { - return this.status.isDropped || this.status.isInvalid || this.status.isUsurped; - } - - public get isFinalized (): boolean { - return this.status.isFinalized; - } - - /** - * @description Finds an EventRecord for the specified method & section - */ - public findRecord (section: string, method: string): EventRecord | undefined { - return this.events.find(({ event }): boolean => - event.section === section && event.method === method - ); - } -} - -export interface SubmittableExtrinsic extends IExtrinsic { - send(): SumbitableResultResult; - - send(statusCb: Callback): SumbitableResultSubscription; - - sign(account: IKeyringPair, _options: Partial): this; - - signAndSend(account: IKeyringPair | string | AccountId | Address, options?: Partial): SumbitableResultResult; - - signAndSend(account: IKeyringPair | string | AccountId | Address, statusCb: Callback): SumbitableResultSubscription; - - signAndSend(account: IKeyringPair | string | AccountId | Address, options: Partial, statusCb?: Callback): SumbitableResultSubscription; -} - -export default function createSubmittableExtrinsic ( - type: ApiTypes, - api: ApiInterfaceRx, - decorateMethod: ApiBase['decorateMethod'], - extrinsic: Call | Uint8Array | string -): SubmittableExtrinsic { - const _extrinsic = createType('Extrinsic', extrinsic, { version: api.extrinsicType }) as unknown as SubmittableExtrinsic; - const _noStatusCb = type === 'rxjs'; - - function updateSigner (updateId: number, status: Hash | ISubmittableResult): void { - if ((updateId !== -1) && api.signer && api.signer.update) { - api.signer.update(updateId, status); - } - } - - function statusObservable (status: ExtrinsicStatus): Observable { - if (!status.isFinalized) { - return of(new SubmittableResult({ status })); - } - - const blockHash = status.asFinalized; - - return combineLatest([ - api.rpc.chain.getBlock(blockHash), - api.query.system.events.at(blockHash) as Observable> - ]).pipe( - map(([signedBlock, allEvents]): SubmittableResult => - new SubmittableResult({ - events: filterEvents(_extrinsic.hash, signedBlock, allEvents), - status - }) - ) - ); - } - - function sendObservable (updateId: number = -1): Observable { - return api.rpc.author - .submitExtrinsic(_extrinsic) - .pipe( - tap((hash): void => { - updateSigner(updateId, hash); - }) - ); - } - - function subscribeObservable (updateId: number = -1): Observable { - return api.rpc.author - .submitAndWatchExtrinsic(_extrinsic) - .pipe( - switchMap((status): Observable => - statusObservable(status) - ), - tap((status): void => { - updateSigner(updateId, status); - }) - ); - } - - function expandOptions (options: Partial, extras: { blockHash?: Hash; era?: ExtrinsicEra; nonce?: Index }): SignatureOptions { - return { - blockHash: api.genesisHash, - ...options, - ...extras, - genesisHash: api.genesisHash, - runtimeVersion: api.runtimeVersion, - version: api.extrinsicType - } as unknown as SignatureOptions; - } - - function expandEraOptions (options: Partial, { header, nonce }: { header: Header | null; nonce: Index }): SignatureOptions { - if (!header) { - if (isNumber(options.era)) { - // since we have no header, it is immortal, remove any option overrides - // so we only supply the genesisHash and no era to the construction - delete options.era; - delete options.blockHash; - } - - return expandOptions(options, { nonce }); - } - - return expandOptions(options, { - blockHash: header.hash, - era: createType('ExtrinsicEra', { - current: header.number, - period: options.era || DEFAULT_MORTAL_LENGTH - }), - nonce - }); - } - - function getPrelimState (address: string, options: Partial): Observable<[Index, Header | null]> { - return combineLatest([ - // if we have a nonce already, don't retrieve the latest, use what is there - isUndefined(options.nonce) - ? api.query.system.accountNonce(address) - : of(createType('Index', options.nonce)), - // if we have an era provided already or eraLength is <= 0 (immortal) - // don't get the latest block, just pass null, handle in mergeMap - (isUndefined(options.era) || (isNumber(options.era) && options.era > 0)) - ? api.rpc.chain.getHeader() - : of(null) - ]); - } - - const signOrigin = _extrinsic.sign; - - Object.defineProperties( - _extrinsic, - { - send: { - value: function (statusCb?: Callback): SumbitableResultResult | SumbitableResultSubscription { - const isSubscription = _noStatusCb || !!statusCb; - - return decorateMethod(isSubscription ? subscribeObservable : sendObservable)(statusCb); - } - }, - sign: { - value: function (account: IKeyringPair, optionOrNonce: 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(optionOrNonce) || isNumber(optionOrNonce) - ? { nonce: optionOrNonce } - : optionOrNonce; - - signOrigin.apply(_extrinsic, [account, expandOptions(options, {})]); - - return this; - } - }, - signAndSend: { - value: function (account: IKeyringPair | string | AccountId | Address, optionsOrStatus?: Partial | Callback, statusCb?: Callback): SumbitableResultResult | SumbitableResultSubscription { - let options: Partial = {}; - - if (isFunction(optionsOrStatus)) { - statusCb = optionsOrStatus; - } else { - options = { ...optionsOrStatus }; - } - - const isSubscription = _noStatusCb || !!statusCb; - const address = isKeyringPair(account) ? account.address : account.toString(); - let updateId: number | undefined; - - return decorateMethod( - (): Observable => ( - getPrelimState(address, options).pipe( - first(), - mergeMap(async ([nonce, header]): Promise => { - const eraOptions = expandEraOptions(options, { header, nonce }); - - // FIXME This is becoming real messy with all the options - way past - // "a method should fit on a single screen" stage. (Probably want to - // clean this when we remove `api.signer.sign` in the next beta cycle) - if (isKeyringPair(account)) { - this.sign(account, eraOptions); - } else if (api.signer) { - const payload = new SignerPayload({ - ...eraOptions, - address, - method: _extrinsic.method, - blockNumber: header ? header.number : 0 - }); - - if (api.signer.signPayload) { - const { id, signature } = await api.signer.signPayload(payload.toPayload()); - - // Here we explicitly call `toPayload()` again instead of working with an object - // (reference) as passed to the signer. This means that we are sure that the - // payload data is not modified from our inputs, but the signer - _extrinsic.addSignature(address, signature, payload.toPayload()); - updateId = id; - } else if (api.signer.signRaw) { - const { id, signature } = await api.signer.signRaw(payload.toRaw()); - - // as above, always trust our payload as the signle sourec of truth - _extrinsic.addSignature(address, signature, payload.toPayload()); - updateId = id; - } else if (api.signer.sign) { - console.warn('The Signer.sign interface is deprecated and will be removed in a future version, Swap to using the Signer.signPayload interface instead.'); - - updateId = await api.signer.sign(_extrinsic, address, { - ...eraOptions, - blockNumber: header ? header.number.toBn() : new BN(0), - genesisHash: api.genesisHash - }); - } else { - throw new Error('Invalid signer interface'); - } - } else { - throw new Error('no signer exists'); - } - }), - switchMap((): Observable | Observable => { - return isSubscription - ? subscribeObservable(updateId) - : sendObservable(updateId); - }) - ) as Observable) // FIXME This is wrong, SubmittableResult is _not_ a codec - )(statusCb); - } - } - } - ); - - return _extrinsic; -} diff --git a/packages/api/src/base/Decorate.ts b/packages/api/src/base/Decorate.ts index af9d238b56..e7c1113eba 100644 --- a/packages/api/src/base/Decorate.ts +++ b/packages/api/src/base/Decorate.ts @@ -3,8 +3,9 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { RpcInterface } from '@polkadot/rpc-core/jsonrpc.types'; -import { Hash, RuntimeVersion } from '@polkadot/types/interfaces'; +import { Call, Hash, RuntimeVersion } from '@polkadot/types/interfaces'; import { AnyFunction, CallFunction, Codec, CodecArg as Arg, ModulesWithCalls } from '@polkadot/types/types'; +import { SubmittableExtrinsic } from '../submittable/types'; import { ApiInterfaceRx, ApiOptions, ApiTypes, DecorateMethodOptions, DecoratedRpc, DecoratedRpcSection, QueryableModuleStorage, QueryableStorage, QueryableStorageEntry, QueryableStorageMulti, QueryableStorageMultiArg, QueryableStorageMultiArgs, SubmittableExtrinsicFunction, SubmittableExtrinsics, SubmittableModuleExtrinsics } from '../types'; import BN from 'bn.js'; @@ -21,7 +22,7 @@ import { DEFAULT_VERSION as EXTRINSIC_DEFAULT_VERSION } from '@polkadot/types/pr import { StorageEntry } from '@polkadot/types/primitive/StorageKey'; import { compactStripLength, u8aToHex } from '@polkadot/util'; -import createSubmittable, { SubmittableExtrinsic } from '../SubmittableExtrinsic'; +import { createSubmittable } from '../submittable'; import { decorateSections } from '../util/decorate'; import Events from './Events'; @@ -158,12 +159,11 @@ export default abstract class Decorate extends Events { } protected decorateExtrinsics (extrinsics: ModulesWithCalls, decorateMethod: Decorate['decorateMethod']): SubmittableExtrinsics { - const creator = (value: Uint8Array | string): SubmittableExtrinsic => - createSubmittable(this._type, this._rx as ApiInterfaceRx, decorateMethod, value); + const creator = createSubmittable(this._type, this._rx as ApiInterfaceRx, decorateMethod); return Object.entries(extrinsics).reduce((out, [name, section]): SubmittableExtrinsics => { out[name] = Object.entries(section).reduce((out, [name, method]): SubmittableModuleExtrinsics => { - out[name] = this.decorateExtrinsicEntry(method, decorateMethod); + out[name] = this.decorateExtrinsicEntry(method, creator); return out; }, {} as unknown as SubmittableModuleExtrinsics); @@ -172,9 +172,9 @@ export default abstract class Decorate extends Events { }, creator as unknown as SubmittableExtrinsics); } - private decorateExtrinsicEntry (method: CallFunction, decorateMethod: Decorate['decorateMethod']): SubmittableExtrinsicFunction { + private decorateExtrinsicEntry (method: CallFunction, creator: (value: Call | Uint8Array | string) => SubmittableExtrinsic): SubmittableExtrinsicFunction { const decorated = (...params: Arg[]): SubmittableExtrinsic => - createSubmittable(this._type, this._rx as ApiInterfaceRx, decorateMethod, method(...params)); + creator(method(...params)); return this.decorateFunctionMeta(method, decorated as any) as SubmittableExtrinsicFunction; } diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 182b0f1dd6..5c3108855b 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -10,4 +10,4 @@ export { WsProvider } from '@polkadot/rpc-provider'; export { default as ApiPromise } from './promise'; export { default as ApiRx } from './rx'; -export { default as SubmittableExtrinsic, SubmittableResult } from './SubmittableExtrinsic'; +export * from './submittable'; diff --git a/packages/api/src/promise/types.ts b/packages/api/src/promise/types.ts index 71b55a9d43..22c0c72609 100644 --- a/packages/api/src/promise/types.ts +++ b/packages/api/src/promise/types.ts @@ -4,7 +4,7 @@ import { QueryableStorageEntry as QueryableStorageEntryBase, SubmittableExtrinsicFunction as SubmittableExtrinsicFunctionBase } from '../types'; -import { SubmittableExtrinsic as SubmittableExtrinsicBase } from '../SubmittableExtrinsic'; +import { SubmittableExtrinsic as SubmittableExtrinsicBase } from '../submittable/types'; export type QueryableStorageEntry = QueryableStorageEntryBase<'promise'>; export type SubmittableExtrinsic = SubmittableExtrinsicBase<'promise'>; diff --git a/packages/api/src/submittable/Extrinsic.ts b/packages/api/src/submittable/Extrinsic.ts new file mode 100644 index 0000000000..86f8a806b4 --- /dev/null +++ b/packages/api/src/submittable/Extrinsic.ts @@ -0,0 +1,262 @@ +/* eslint-disable no-dupe-class-members */ +// Copyright 2017-2019 @polkadot/api authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { AccountId, Address, Call, Extrinsic, ExtrinsicEra, ExtrinsicStatus, EventRecord, Hash, Header, Index } from '@polkadot/types/interfaces'; +import { Callback, Codec, Constructor, IKeyringPair, SignatureOptions } from '@polkadot/types/types'; +import { ApiInterfaceRx, ApiTypes, SignerResult } from '../types'; +import { SignerOptions, SubmittableExtrinsic, SubmittableResultImpl, SubmitableResultResult, SubmitableResultSubscription } from './types'; + +import { Observable, combineLatest, of } from 'rxjs'; +import { first, map, mergeMap, switchMap, tap } from 'rxjs/operators'; +import { createType, ClassOf, Vec } from '@polkadot/types'; +import { isBn, isFunction, isNumber, isUndefined } from '@polkadot/util'; + +import { filterEvents, isKeyringPair } from '../util'; +import ApiBase from '../base'; +import SignerPayload from '../SignerPayload'; +import SubmittableResult from './Result'; + +type Creator = (extrinsic: Call | Uint8Array | string) => SubmittableExtrinsic; + +interface SubmittableOptions { + api: ApiInterfaceRx; + decorateMethod: ApiBase['decorateMethod']; + type: ApiTypes; +} + +// The default for 6s allowing for 5min eras. When translating this to faster blocks - +// - 4s = (10 / 15) * 5 = 3.33m +// - 2s = (10 / 30) * 5 = 1.66m +const BLOCKTIME = 6; +const ONE_MINUTE = 60 / BLOCKTIME; +const DEFAULT_MORTAL_LENGTH = 5 * ONE_MINUTE; + +const _Extrinsic: Constructor = ClassOf('Extrinsic'); + +class Submittable extends _Extrinsic implements SubmittableExtrinsic { + private readonly _api: ApiInterfaceRx; + + private readonly _decorateMethod: ApiBase['decorateMethod']; + + private readonly _ignoreStatusCb: boolean; + + public constructor (extrinsic: Call | Uint8Array | string, { api, decorateMethod, type }: SubmittableOptions) { + super(extrinsic, { version: api.extrinsicType }); + + this._api = api; + this._decorateMethod = decorateMethod; + this._ignoreStatusCb = type === 'rxjs'; + } + + // sign a transaction, returning the this to allow cianing, i.e. .sign(...).send() + public sign (account: IKeyringPair, optionsOrNonce: Partial): this { + // NOTE 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(optionsOrNonce) || isNumber(optionsOrNonce) + ? { nonce: optionsOrNonce } + : optionsOrNonce; + + super.sign(account, this._makeSignOptions(options, {})); + + return this; + } + + // signAndSend with an immediate Hash result + public signAndSend(account: IKeyringPair | string | AccountId | Address, options?: Partial): SubmitableResultResult; + + // signAndSend with a subscription, i.e. callback provided + public signAndSend(account: IKeyringPair | string | AccountId | Address, statusCb: Callback): SubmitableResultSubscription; + + // signAndSend with options and a callback + public signAndSend(account: IKeyringPair | string | AccountId | Address, options: Partial, statusCb?: Callback): SubmitableResultSubscription; + + // signAndSend implementation for all 3 cases above + public signAndSend (account: IKeyringPair | string | AccountId | Address, optionsOrStatus?: Partial | Callback, optionalStatusCb?: Callback): SubmitableResultResult | SubmitableResultSubscription { + const [options, statusCb] = this._makeSignAndSendOptions(optionsOrStatus, optionalStatusCb); + const isSubscription = this._ignoreStatusCb || !!statusCb; + const address = isKeyringPair(account) ? account.address : account.toString(); + let updateId: number | undefined; + + return this._decorateMethod( + (): Observable => ( + this._getPrelimState(address, options).pipe( + first(), + mergeMap(async ([nonce, header]): Promise => { + const eraOptions = this._makeEraOptions(options, { header, nonce }); + + if (isKeyringPair(account)) { + this.sign(account, eraOptions); + } else { + updateId = await this._signViaSigner(address, eraOptions, header); + } + }), + switchMap((): Observable | Observable => { + return isSubscription + ? this._subscribeObservable(updateId) + : this._sendObservable(updateId); + }) + ) as Observable) // FIXME This is wrong, SubmittableResult is _not_ a codec + )(statusCb) as SubmitableResultResult | SubmitableResultSubscription; + } + + // send with an immediate Hash result + public send (): SubmitableResultResult; + + // send with a status callback + public send (statusCb: Callback): SubmitableResultSubscription; + + // send implementation for both immediate Hash and statusCb variants + public send (statusCb?: Callback): SubmitableResultResult | SubmitableResultSubscription { + const isSubscription = this._ignoreStatusCb || !!statusCb; + + return this._decorateMethod( + isSubscription + ? this._subscribeObservable + : this._sendObservable + )(statusCb); + } + + private _makeSignAndSendOptions (optionsOrStatus?: Partial | Callback, statusCb?: Callback): [Partial, Callback?] { + let options: Partial = {}; + + if (isFunction(optionsOrStatus)) { + statusCb = optionsOrStatus; + } else { + options = { ...optionsOrStatus }; + } + + return [options, statusCb]; + } + + private async _signViaSigner (address: string, eraOptions: SignatureOptions, header: Header | null): Promise { + if (!this._api.signer) { + throw new Error('no signer attached'); + } + + const payload = new SignerPayload({ + ...eraOptions, + address, + method: this.method, + blockNumber: header ? header.number : 0 + }); + let result: SignerResult; + + if (this._api.signer.signPayload) { + result = await this._api.signer.signPayload(payload.toPayload()); + } else if (this._api.signer.signRaw) { + result = await this._api.signer.signRaw(payload.toRaw()); + } else { + throw new Error('Invalid signer interface, it should implement either signPayload or signRaw (or both)'); + } + + // Here we explicitly call `toPayload()` again instead of working with an object + // (reference) as passed to the signer. This means that we are sure that the + // payload data is not modified from our inputs, but the signer + super.addSignature(address, result.signature, payload.toPayload()); + + return result.id; + } + + private _makeSignOptions (options: Partial, extras: { blockHash?: Hash; era?: ExtrinsicEra; nonce?: Index }): SignatureOptions { + return { + blockHash: this._api.genesisHash, + ...options, + ...extras, + genesisHash: this._api.genesisHash, + runtimeVersion: this._api.runtimeVersion, + version: this._api.extrinsicType + } as unknown as SignatureOptions; + } + + private _makeEraOptions (options: Partial, { header, nonce }: { header: Header | null; nonce: Index }): SignatureOptions { + if (!header) { + if (isNumber(options.era)) { + // since we have no header, it is immortal, remove any option overrides + // so we only supply the genesisHash and no era to the construction + delete options.era; + delete options.blockHash; + } + + return this._makeSignOptions(options, { nonce }); + } + + return this._makeSignOptions(options, { + blockHash: header.hash, + era: createType('ExtrinsicEra', { + current: header.number, + period: options.era || DEFAULT_MORTAL_LENGTH + }), + nonce + }); + } + + private _getPrelimState (address: string, options: Partial): Observable<[Index, Header | null]> { + return combineLatest([ + // if we have a nonce already, don't retrieve the latest, use what is there + isUndefined(options.nonce) + ? this._api.query.system.accountNonce(address) + : of(createType('Index', options.nonce)), + // if we have an era provided already or eraLength is <= 0 (immortal) + // don't get the latest block, just pass null, handle in mergeMap + (isUndefined(options.era) || (isNumber(options.era) && options.era > 0)) + ? this._api.rpc.chain.getHeader() + : of(null) + ]); + } + + private _updateSigner (updateId: number, status: Hash | SubmittableResultImpl): void { + if ((updateId !== -1) && this._api.signer && this._api.signer.update) { + this._api.signer.update(updateId, status); + } + } + + private _statusObservable (status: ExtrinsicStatus): Observable { + if (!status.isFinalized) { + return of(new SubmittableResult({ status })); + } + + const blockHash = status.asFinalized; + + return combineLatest([ + this._api.rpc.chain.getBlock(blockHash), + this._api.query.system.events.at(blockHash) as Observable> + ]).pipe( + map(([signedBlock, allEvents]): SubmittableResultImpl => + new SubmittableResult({ + events: filterEvents(this.hash, signedBlock, allEvents), + status + }) + ) + ); + } + + private _sendObservable (updateId: number = -1): Observable { + return this._api.rpc.author + .submitExtrinsic(this) + .pipe( + tap((hash): void => { + this._updateSigner(updateId, hash); + }) + ); + } + + private _subscribeObservable (updateId: number = -1): Observable { + return this._api.rpc.author + .submitAndWatchExtrinsic(this) + .pipe( + switchMap((status): Observable => + this._statusObservable(status) + ), + tap((status): void => { + this._updateSigner(updateId, status); + }) + ); + } +} + +export default function createSubmittableExtrinsic (type: ApiTypes, api: ApiInterfaceRx, decorateMethod: ApiBase['decorateMethod']): Creator { + return (extrinsic: Call | Uint8Array | string): SubmittableExtrinsic => + new Submittable(extrinsic, { api, decorateMethod, type }); +} diff --git a/packages/api/src/submittable/Result.ts b/packages/api/src/submittable/Result.ts new file mode 100644 index 0000000000..6c1ca8de72 --- /dev/null +++ b/packages/api/src/submittable/Result.ts @@ -0,0 +1,38 @@ +// Copyright 2017-2019 @polkadot/api authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { ExtrinsicStatus, EventRecord } from '@polkadot/types/interfaces'; +import { SubmittableResultImpl, SubmittableResultValue } from './types'; + +export default class SubmittableResult implements SubmittableResultImpl { + public readonly events: EventRecord[]; + + public readonly status: ExtrinsicStatus; + + public constructor ({ events, status }: SubmittableResultValue) { + this.events = events || []; + this.status = status; + } + + public get isCompleted (): boolean { + return this.isError || this.isFinalized; + } + + public get isError (): boolean { + return this.status.isDropped || this.status.isInvalid || this.status.isUsurped; + } + + public get isFinalized (): boolean { + return this.status.isFinalized; + } + + /** + * @description Finds an EventRecord for the specified method & section + */ + public findRecord (section: string, method: string): EventRecord | undefined { + return this.events.find(({ event }): boolean => + event.section === section && event.method === method + ); + } +} diff --git a/packages/api/src/submittable/index.ts b/packages/api/src/submittable/index.ts new file mode 100644 index 0000000000..c5de5e33da --- /dev/null +++ b/packages/api/src/submittable/index.ts @@ -0,0 +1,6 @@ +// Copyright 2017-2019 @polkadot/api authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +export { default as createSubmittable } from './Extrinsic'; +export { default as SubmittableResult } from './Result'; diff --git a/packages/api/src/submittable/types.ts b/packages/api/src/submittable/types.ts new file mode 100644 index 0000000000..ebfc58b164 --- /dev/null +++ b/packages/api/src/submittable/types.ts @@ -0,0 +1,54 @@ +// Copyright 2017-2019 @polkadot/api authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { AccountId, Address, ExtrinsicStatus, EventRecord, Hash } from '@polkadot/types/interfaces'; +import { AnyNumber, AnyU8a, Callback, IExtrinsic, IExtrinsicEra, IKeyringPair, SignatureOptions } from '@polkadot/types/types'; + +import { Observable } from 'rxjs'; + +export interface SubmittableResultImpl { + readonly events: EventRecord[]; + readonly status: ExtrinsicStatus; + readonly isCompleted: boolean; + readonly isError: boolean; + readonly isFinalized: boolean; + + findRecord(section: string, method: string): EventRecord | undefined; +} + +export interface SubmittableResultValue { + events?: EventRecord[]; + status: ExtrinsicStatus; +} + +export type SubmitableResultResult = + ApiType extends 'rxjs' + ? Observable + : Promise; + +export type SubmitableResultSubscription = + ApiType extends 'rxjs' + ? Observable + : Promise<() => void>; + +export interface SignerOptions { + blockHash: AnyU8a; + era?: IExtrinsicEra | number; + nonce: AnyNumber; + tip?: AnyNumber; +} + +export interface SubmittableExtrinsic extends IExtrinsic { + send(): SubmitableResultResult; + + send(statusCb: Callback): SubmitableResultSubscription; + + sign(account: IKeyringPair, _options: Partial): this; + + signAndSend(account: IKeyringPair | string | AccountId | Address, options?: Partial): SubmitableResultResult; + + signAndSend(account: IKeyringPair | string | AccountId | Address, statusCb: Callback): SubmitableResultSubscription; + + signAndSend(account: IKeyringPair | string | AccountId | Address, options: Partial, statusCb?: Callback): SubmitableResultSubscription; +} diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 009f261c38..b3fde4a747 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -3,7 +3,8 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { Hash, RuntimeVersion } from '@polkadot/types/interfaces'; -import { AnyFunction, Callback, CallFunction, Codec, CodecArg, IExtrinsic, RegistryTypes, SignatureOptions } from '@polkadot/types/types'; +import { AnyFunction, Callback, CallFunction, Codec, CodecArg, RegistryTypes, SignatureOptions } from '@polkadot/types/types'; +import { SubmittableResultImpl, SubmittableExtrinsic } from './submittable/types'; import BN from 'bn.js'; import { Observable } from 'rxjs'; @@ -15,7 +16,8 @@ import { Metadata, u64 } from '@polkadot/types'; import { StorageEntry } from '@polkadot/types/primitive/StorageKey'; import ApiBase from './base'; -import { ISubmittableResult, SubmittableExtrinsic } from './SubmittableExtrinsic'; + +export * from './submittable/types'; // Prepend an element V onto the beginning of a tuple T. // Cons<1, [2,3,4]> is [1,2,3,4] @@ -299,12 +301,6 @@ export interface SignerResult { } export interface Signer { - /** - * @deprecated Implement and use signPayload and/or signRaw instead - * @description Signs an extrinsic, returning an id (>0) that can be used to retrieve updates - */ - sign?: (extrinsic: IExtrinsic, address: string, options: SignerOptions) => Promise; - /** * @description signs an extrinsic payload from a serialized form */ @@ -318,5 +314,5 @@ export interface Signer { /** * @description Receives an update for the extrinsic signed by a `signer.sign` */ - update?: (id: number, status: Hash | ISubmittableResult) => void; + update?: (id: number, status: Hash | SubmittableResultImpl) => void; } diff --git a/packages/api/src/util/index.ts b/packages/api/src/util/index.ts new file mode 100644 index 0000000000..a636300de9 --- /dev/null +++ b/packages/api/src/util/index.ts @@ -0,0 +1,8 @@ +// Copyright 2017-2019 @polkadot/api authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +export * from './decorate'; +export { default as filterEvents } from './filterEvents'; +export { default as isKeyringPair } from './isKeyringPair'; +export { default as l } from './logging'; diff --git a/packages/api/src/util/isKeyringPair.ts b/packages/api/src/util/isKeyringPair.ts new file mode 100644 index 0000000000..2a8cd14b8f --- /dev/null +++ b/packages/api/src/util/isKeyringPair.ts @@ -0,0 +1,12 @@ +// Copyright 2017-2019 @polkadot/api authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { AccountId, Address } from '@polkadot/types/interfaces'; +import { IKeyringPair } from '@polkadot/types/types'; + +import { isFunction } from '@polkadot/util'; + +export default function isKeyringPair (account: string | IKeyringPair | AccountId | Address): account is IKeyringPair { + return isFunction((account as IKeyringPair).sign); +} diff --git a/packages/types/src/primitive/Extrinsic/Extrinsic.ts b/packages/types/src/primitive/Extrinsic/Extrinsic.ts index a67e774409..e4e6f2f076 100644 --- a/packages/types/src/primitive/Extrinsic/Extrinsic.ts +++ b/packages/types/src/primitive/Extrinsic/Extrinsic.ts @@ -31,7 +31,7 @@ const VERSIONS: InterfaceTypes[] = [ 'ExtrinsicUnknown', // v0 is unknown 'ExtrinsicV1', 'ExtrinsicV2', - 'ExtrinsicV2' + 'ExtrinsicV3' ]; const VERSION_UNKNOWN = VERSIONS[0];