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
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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> =
|
||||
ApiType extends 'rxjs'
|
||||
? Observable<ISubmittableResult>
|
||||
: Promise<Hash>;
|
||||
|
||||
export type SumbitableResultSubscription<ApiType> =
|
||||
ApiType extends 'rxjs'
|
||||
? Observable<ISubmittableResult>
|
||||
: 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<ApiType> extends IExtrinsic {
|
||||
send(): SumbitableResultResult<ApiType>;
|
||||
|
||||
send(statusCb: Callback<ISubmittableResult>): SumbitableResultSubscription<ApiType>;
|
||||
|
||||
sign(account: IKeyringPair, _options: Partial<SignatureOptions>): this;
|
||||
|
||||
signAndSend(account: IKeyringPair | string | AccountId | Address, options?: Partial<SignerOptions>): SumbitableResultResult<ApiType>;
|
||||
|
||||
signAndSend(account: IKeyringPair | string | AccountId | Address, statusCb: Callback<ISubmittableResult>): SumbitableResultSubscription<ApiType>;
|
||||
|
||||
signAndSend(account: IKeyringPair | string | AccountId | Address, options: Partial<SignerOptions>, statusCb?: Callback<ISubmittableResult>): SumbitableResultSubscription<ApiType>;
|
||||
}
|
||||
|
||||
export default function createSubmittableExtrinsic<ApiType> (
|
||||
type: ApiTypes,
|
||||
api: ApiInterfaceRx,
|
||||
decorateMethod: ApiBase<ApiType>['decorateMethod'],
|
||||
extrinsic: Call | Uint8Array | string
|
||||
): SubmittableExtrinsic<ApiType> {
|
||||
const _extrinsic = createType('Extrinsic', extrinsic, { version: api.extrinsicType }) as unknown as SubmittableExtrinsic<ApiType>;
|
||||
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<ISubmittableResult> {
|
||||
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<Vec<EventRecord>>
|
||||
]).pipe(
|
||||
map(([signedBlock, allEvents]): SubmittableResult =>
|
||||
new SubmittableResult({
|
||||
events: filterEvents(_extrinsic.hash, signedBlock, allEvents),
|
||||
status
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function sendObservable (updateId: number = -1): Observable<Hash> {
|
||||
return api.rpc.author
|
||||
.submitExtrinsic(_extrinsic)
|
||||
.pipe(
|
||||
tap((hash): void => {
|
||||
updateSigner(updateId, hash);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function subscribeObservable (updateId: number = -1): Observable<ISubmittableResult> {
|
||||
return api.rpc.author
|
||||
.submitAndWatchExtrinsic(_extrinsic)
|
||||
.pipe(
|
||||
switchMap((status): Observable<ISubmittableResult> =>
|
||||
statusObservable(status)
|
||||
),
|
||||
tap((status): void => {
|
||||
updateSigner(updateId, status);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function expandOptions (options: Partial<SignerOptions>, 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<SignerOptions>, { 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<SignerOptions>): 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<Index>(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<ISubmittableResult>): SumbitableResultResult<ApiType> | SumbitableResultSubscription<ApiType> {
|
||||
const isSubscription = _noStatusCb || !!statusCb;
|
||||
|
||||
return decorateMethod(isSubscription ? subscribeObservable : sendObservable)(statusCb);
|
||||
}
|
||||
},
|
||||
sign: {
|
||||
value: function (account: IKeyringPair, optionOrNonce: Partial<SignerOptions>): SubmittableExtrinsic<ApiType> {
|
||||
// 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<SignerOptions> = 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<SignerOptions> | Callback<ISubmittableResult>, statusCb?: Callback<ISubmittableResult>): SumbitableResultResult<ApiType> | SumbitableResultSubscription<ApiType> {
|
||||
let options: Partial<SignerOptions> = {};
|
||||
|
||||
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<Codec> => (
|
||||
getPrelimState(address, options).pipe(
|
||||
first(),
|
||||
mergeMap(async ([nonce, header]): Promise<void> => {
|
||||
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<ISubmittableResult> | Observable<Hash> => {
|
||||
return isSubscription
|
||||
? subscribeObservable(updateId)
|
||||
: sendObservable(updateId);
|
||||
})
|
||||
) as Observable<Codec>) // FIXME This is wrong, SubmittableResult is _not_ a codec
|
||||
)(statusCb);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return _extrinsic;
|
||||
}
|
||||
@@ -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<ApiType> extends Events {
|
||||
}
|
||||
|
||||
protected decorateExtrinsics<ApiType> (extrinsics: ModulesWithCalls, decorateMethod: Decorate<ApiType>['decorateMethod']): SubmittableExtrinsics<ApiType> {
|
||||
const creator = (value: Uint8Array | string): SubmittableExtrinsic<ApiType> =>
|
||||
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<ApiType> => {
|
||||
out[name] = Object.entries(section).reduce((out, [name, method]): SubmittableModuleExtrinsics<ApiType> => {
|
||||
out[name] = this.decorateExtrinsicEntry(method, decorateMethod);
|
||||
out[name] = this.decorateExtrinsicEntry(method, creator);
|
||||
|
||||
return out;
|
||||
}, {} as unknown as SubmittableModuleExtrinsics<ApiType>);
|
||||
@@ -172,9 +172,9 @@ export default abstract class Decorate<ApiType> extends Events {
|
||||
}, creator as unknown as SubmittableExtrinsics<ApiType>);
|
||||
}
|
||||
|
||||
private decorateExtrinsicEntry<ApiType> (method: CallFunction, decorateMethod: Decorate<ApiType>['decorateMethod']): SubmittableExtrinsicFunction<ApiType> {
|
||||
private decorateExtrinsicEntry<ApiType> (method: CallFunction, creator: (value: Call | Uint8Array | string) => SubmittableExtrinsic<ApiType>): SubmittableExtrinsicFunction<ApiType> {
|
||||
const decorated = (...params: Arg[]): SubmittableExtrinsic<ApiType> =>
|
||||
createSubmittable(this._type, this._rx as ApiInterfaceRx, decorateMethod, method(...params));
|
||||
creator(method(...params));
|
||||
|
||||
return this.decorateFunctionMeta(method, decorated as any) as SubmittableExtrinsicFunction<ApiType>;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'>;
|
||||
|
||||
@@ -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<ApiType> = (extrinsic: Call | Uint8Array | string) => SubmittableExtrinsic<ApiType>;
|
||||
|
||||
interface SubmittableOptions<ApiType> {
|
||||
api: ApiInterfaceRx;
|
||||
decorateMethod: ApiBase<ApiType>['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<Extrinsic> = ClassOf('Extrinsic');
|
||||
|
||||
class Submittable<ApiType> extends _Extrinsic implements SubmittableExtrinsic<ApiType> {
|
||||
private readonly _api: ApiInterfaceRx;
|
||||
|
||||
private readonly _decorateMethod: ApiBase<ApiType>['decorateMethod'];
|
||||
|
||||
private readonly _ignoreStatusCb: boolean;
|
||||
|
||||
public constructor (extrinsic: Call | Uint8Array | string, { api, decorateMethod, type }: SubmittableOptions<ApiType>) {
|
||||
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<SignerOptions>): 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<SignerOptions> = 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<SignerOptions>): SubmitableResultResult<ApiType>;
|
||||
|
||||
// signAndSend with a subscription, i.e. callback provided
|
||||
public signAndSend(account: IKeyringPair | string | AccountId | Address, statusCb: Callback<SubmittableResultImpl>): SubmitableResultSubscription<ApiType>;
|
||||
|
||||
// signAndSend with options and a callback
|
||||
public signAndSend(account: IKeyringPair | string | AccountId | Address, options: Partial<SignerOptions>, statusCb?: Callback<SubmittableResultImpl>): SubmitableResultSubscription<ApiType>;
|
||||
|
||||
// signAndSend implementation for all 3 cases above
|
||||
public signAndSend (account: IKeyringPair | string | AccountId | Address, optionsOrStatus?: Partial<SignerOptions> | Callback<SubmittableResultImpl>, optionalStatusCb?: Callback<SubmittableResultImpl>): SubmitableResultResult<ApiType> | SubmitableResultSubscription<ApiType> {
|
||||
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<Codec> => (
|
||||
this._getPrelimState(address, options).pipe(
|
||||
first(),
|
||||
mergeMap(async ([nonce, header]): Promise<void> => {
|
||||
const eraOptions = this._makeEraOptions(options, { header, nonce });
|
||||
|
||||
if (isKeyringPair(account)) {
|
||||
this.sign(account, eraOptions);
|
||||
} else {
|
||||
updateId = await this._signViaSigner(address, eraOptions, header);
|
||||
}
|
||||
}),
|
||||
switchMap((): Observable<SubmittableResultImpl> | Observable<Hash> => {
|
||||
return isSubscription
|
||||
? this._subscribeObservable(updateId)
|
||||
: this._sendObservable(updateId);
|
||||
})
|
||||
) as Observable<Codec>) // FIXME This is wrong, SubmittableResult is _not_ a codec
|
||||
)(statusCb) as SubmitableResultResult<ApiType> | SubmitableResultSubscription<ApiType>;
|
||||
}
|
||||
|
||||
// send with an immediate Hash result
|
||||
public send (): SubmitableResultResult<ApiType>;
|
||||
|
||||
// send with a status callback
|
||||
public send (statusCb: Callback<SubmittableResultImpl>): SubmitableResultSubscription<ApiType>;
|
||||
|
||||
// send implementation for both immediate Hash and statusCb variants
|
||||
public send (statusCb?: Callback<SubmittableResultImpl>): SubmitableResultResult<ApiType> | SubmitableResultSubscription<ApiType> {
|
||||
const isSubscription = this._ignoreStatusCb || !!statusCb;
|
||||
|
||||
return this._decorateMethod(
|
||||
isSubscription
|
||||
? this._subscribeObservable
|
||||
: this._sendObservable
|
||||
)(statusCb);
|
||||
}
|
||||
|
||||
private _makeSignAndSendOptions (optionsOrStatus?: Partial<SignerOptions> | Callback<SubmittableResultImpl>, statusCb?: Callback<SubmittableResultImpl>): [Partial<SignerOptions>, Callback<SubmittableResultImpl>?] {
|
||||
let options: Partial<SignerOptions> = {};
|
||||
|
||||
if (isFunction(optionsOrStatus)) {
|
||||
statusCb = optionsOrStatus;
|
||||
} else {
|
||||
options = { ...optionsOrStatus };
|
||||
}
|
||||
|
||||
return [options, statusCb];
|
||||
}
|
||||
|
||||
private async _signViaSigner (address: string, eraOptions: SignatureOptions, header: Header | null): Promise<number> {
|
||||
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<SignerOptions>, 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<SignerOptions>, { 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<SignerOptions>): 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<Index>(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<SubmittableResultImpl> {
|
||||
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<Vec<EventRecord>>
|
||||
]).pipe(
|
||||
map(([signedBlock, allEvents]): SubmittableResultImpl =>
|
||||
new SubmittableResult({
|
||||
events: filterEvents(this.hash, signedBlock, allEvents),
|
||||
status
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _sendObservable (updateId: number = -1): Observable<Hash> {
|
||||
return this._api.rpc.author
|
||||
.submitExtrinsic(this)
|
||||
.pipe(
|
||||
tap((hash): void => {
|
||||
this._updateSigner(updateId, hash);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _subscribeObservable (updateId: number = -1): Observable<SubmittableResultImpl> {
|
||||
return this._api.rpc.author
|
||||
.submitAndWatchExtrinsic(this)
|
||||
.pipe(
|
||||
switchMap((status): Observable<SubmittableResultImpl> =>
|
||||
this._statusObservable(status)
|
||||
),
|
||||
tap((status): void => {
|
||||
this._updateSigner(updateId, status);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function createSubmittableExtrinsic<ApiType> (type: ApiTypes, api: ApiInterfaceRx, decorateMethod: ApiBase<ApiType>['decorateMethod']): Creator<ApiType> {
|
||||
return (extrinsic: Call | Uint8Array | string): SubmittableExtrinsic<ApiType> =>
|
||||
new Submittable(extrinsic, { api, decorateMethod, type });
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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> =
|
||||
ApiType extends 'rxjs'
|
||||
? Observable<SubmittableResultImpl>
|
||||
: Promise<Hash>;
|
||||
|
||||
export type SubmitableResultSubscription<ApiType> =
|
||||
ApiType extends 'rxjs'
|
||||
? Observable<SubmittableResultImpl>
|
||||
: Promise<() => void>;
|
||||
|
||||
export interface SignerOptions {
|
||||
blockHash: AnyU8a;
|
||||
era?: IExtrinsicEra | number;
|
||||
nonce: AnyNumber;
|
||||
tip?: AnyNumber;
|
||||
}
|
||||
|
||||
export interface SubmittableExtrinsic<ApiType> extends IExtrinsic {
|
||||
send(): SubmitableResultResult<ApiType>;
|
||||
|
||||
send(statusCb: Callback<SubmittableResultImpl>): SubmitableResultSubscription<ApiType>;
|
||||
|
||||
sign(account: IKeyringPair, _options: Partial<SignatureOptions>): this;
|
||||
|
||||
signAndSend(account: IKeyringPair | string | AccountId | Address, options?: Partial<SignerOptions>): SubmitableResultResult<ApiType>;
|
||||
|
||||
signAndSend(account: IKeyringPair | string | AccountId | Address, statusCb: Callback<SubmittableResultImpl>): SubmitableResultSubscription<ApiType>;
|
||||
|
||||
signAndSend(account: IKeyringPair | string | AccountId | Address, options: Partial<SignerOptions>, statusCb?: Callback<SubmittableResultImpl>): SubmitableResultSubscription<ApiType>;
|
||||
}
|
||||
@@ -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<number>;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ const VERSIONS: InterfaceTypes[] = [
|
||||
'ExtrinsicUnknown', // v0 is unknown
|
||||
'ExtrinsicV1',
|
||||
'ExtrinsicV2',
|
||||
'ExtrinsicV2'
|
||||
'ExtrinsicV3'
|
||||
];
|
||||
|
||||
const VERSION_UNKNOWN = VERSIONS[0];
|
||||
|
||||
Reference in New Issue
Block a user