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
This commit is contained in:
Ian He
2019-03-01 13:18:34 +01:00
committed by Jaco Greeff
parent 2b97c20bea
commit 1cb847a63a
10 changed files with 186 additions and 133 deletions
+4 -4
View File
@@ -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<CodecResult, SubscriptionResult> implement
*/
registerTypes (types?: RegistryTypes): void {
if (types) {
typeRegistry.register(types);
getTypeRegistry().register(types);
}
}
@@ -460,7 +460,7 @@ export default abstract class ApiBase<CodecResult, SubscriptionResult> implement
private decorateExtrinsicEntry<C, S> (method: MethodFunction, onCall: OnCallDefinition<C, S>): SubmittableExtrinsicFunction<C, S> {
const decorated: any = (...params: Array<CodecArg>): SubmittableExtrinsic<C, S> =>
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<C, S>;
}
+110 -99
View File
@@ -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, SubscriptionResult> =
CodecResult extends Observable<any>
? Observable<SubmittableResult>
: Promise<Hash>;
: Promise<Hash>;
type SumbitableResultSubscription<CodecResult, SubscriptionResult> =
SubscriptionResult extends Observable<any>
@@ -56,26 +56,29 @@ export class SubmittableResult extends Struct {
}
}
export default class SubmittableExtrinsic<CodecResult, SubscriptionResult> extends Extrinsic {
private _api: ApiInterface$Rx;
private _onCall: OnCallDefinition<CodecResult, SubscriptionResult>;
private _noStatusCb?: boolean;
export interface SubmittableExtrinsic<CodecResult, SubscriptionResult> extends IExtrinsic {
send (): SumbitableResultResult<CodecResult, SubscriptionResult>;
constructor (type: ApiType, api: ApiInterface$Rx, onCall: OnCallDefinition<CodecResult, SubscriptionResult>, extrinsic: Extrinsic | Method) {
super(extrinsic);
send (statusCb: (result: SubmittableResult) => any): SumbitableResultSubscription<CodecResult, SubscriptionResult>;
this._noStatusCb = type === 'rxjs';
this._api = api;
this._onCall = onCall;
}
sign (account: KeyringPair, _options: Partial<SignatureOptions>): 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<Partial<SignatureOptions>>): SumbitableResultResult<CodecResult, SubscriptionResult>;
signAndSend (account: KeyringPair | string | AccountId | Address, statusCb: StatusCb): SumbitableResultSubscription<CodecResult, SubscriptionResult>;
}
export function createSubmittableExtrinsic<CodecResult, SubscriptionResult> (type: ApiType, api: ApiInterface$Rx, onCall: OnCallDefinition<CodecResult, SubscriptionResult>, extrinsic: Method): SubmittableExtrinsic<CodecResult, SubscriptionResult> {
const _extrinsic = new (getTypeRegistry().getOrThrow('Extrinsic'))(extrinsic) as SubmittableExtrinsic<CodecResult, SubscriptionResult>;
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<SubmittableResult> {
function statusObservable (status: ExtrinsicStatus): Observable<SubmittableResult> {
if (status.type !== 'Finalised') {
return of(
new SubmittableResult({
@@ -88,12 +91,12 @@ export default class SubmittableExtrinsic<CodecResult, SubscriptionResult> exten
const blockHash = status.asFinalised;
return combineLatest(
this._api.rpc.chain.getBlock(blockHash) as Observable<SignedBlock>,
this._api.query.system.events.at(blockHash) as Observable<Vector<EventRecord>>
api.rpc.chain.getBlock(blockHash) as Observable<SignedBlock>,
api.query.system.events.at(blockHash) as Observable<Vector<EventRecord>>
).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<CodecResult, SubscriptionResult> exten
);
}
private sendObservable (updateId: number = -1): Observable<Hash> {
return (this._api.rpc.author
.submitExtrinsic(this) as Observable<Hash>)
function sendObservable (updateId: number = -1): Observable<Hash> {
return (api.rpc.author
.submitExtrinsic(_extrinsic) as Observable<Hash>)
.pipe(
tap((hash) => {
this.updateSigner(updateId, hash);
updateSigner(updateId, hash);
})
);
}
private subscribeObservable (updateId: number = -1): Observable<SubmittableResult> {
return (this._api.rpc.author
.submitAndWatchExtrinsic(this) as Observable<ExtrinsicStatus>)
function subscribeObservable (updateId: number = -1): Observable<SubmittableResult> {
return (api.rpc.author
.submitAndWatchExtrinsic(_extrinsic) as Observable<ExtrinsicStatus>)
.pipe(
switchMap((status) =>
this.statusObservable(status)
statusObservable(status)
),
tap((status) => {
this.updateSigner(updateId, status);
updateSigner(updateId, status);
})
);
}
private expandOptions (options: Partial<SignatureOptions>): SignatureOptions {
function expandOptions (options: Partial<SignatureOptions>): SignatureOptions {
return {
blockHash: this._api.genesisHash,
version: this._api.runtimeVersion,
blockHash: api.genesisHash,
version: api.runtimeVersion,
...options
} as SignatureOptions;
}
send (): SumbitableResultResult<CodecResult, SubscriptionResult>;
send (statusCb: (result: SubmittableResult) => any): SumbitableResultSubscription<CodecResult, SubscriptionResult>;
send (statusCb?: (result: SubmittableResult) => any): SumbitableResultResult<CodecResult, SubscriptionResult> | SumbitableResultSubscription<CodecResult, SubscriptionResult> {
const isSubscription = this._noStatusCb || !!statusCb;
const signOrigin = _extrinsic.sign;
Object.defineProperties(
_extrinsic,
{
send: {
value: function (statusCb?: (result: SubmittableResult) => any): SumbitableResultResult<CodecResult, SubscriptionResult> | SumbitableResultSubscription<CodecResult, SubscriptionResult> {
const isSubscription = _noStatusCb || !!statusCb;
return this._onCall(
() => isSubscription
? this.subscribeObservable()
: this.sendObservable(),
[],
statusCb as CodecCallback,
isSubscription
) as unknown as SumbitableResultSubscription<CodecResult, SubscriptionResult>;
}
return onCall(
() => isSubscription
? subscribeObservable()
: sendObservable(),
[],
statusCb as CodecCallback,
isSubscription
) as unknown as SumbitableResultSubscription<CodecResult, SubscriptionResult>;
}
},
sign: {
value: function (account: KeyringPair, _options: Partial<SignatureOptions>): SubmittableExtrinsic<CodecResult, SubscriptionResult> {
// 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<SignatureOptions> = 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<SignatureOptions>): SubmittableExtrinsic<CodecResult, SubscriptionResult> {
// 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<SignatureOptions> = 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<Partial<SignatureOptions>> | StatusCb, statusCb?: StatusCb): SumbitableResultResult<CodecResult, SubscriptionResult> | SumbitableResultSubscription<CodecResult, SubscriptionResult> {
let options: Partial<Partial<SignatureOptions>> = {};
return this;
}
signAndSend (account: KeyringPair | string | AccountId | Address, options?: Partial<Partial<SignatureOptions>>): SumbitableResultResult<CodecResult, SubscriptionResult>;
signAndSend (account: KeyringPair | string | AccountId | Address, statusCb: StatusCb): SumbitableResultSubscription<CodecResult, SubscriptionResult>;
signAndSend (account: KeyringPair | string | AccountId | Address, _options?: Partial<Partial<SignatureOptions>> | StatusCb, statusCb?: StatusCb): SumbitableResultResult<CodecResult, SubscriptionResult> | SumbitableResultSubscription<CodecResult, SubscriptionResult> {
let options: Partial<Partial<SignatureOptions>> = {};
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<Index>
: 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<Codec>),
[],
statusCb as CodecCallback,
isSubscription
) as unknown as SumbitableResultSubscription<CodecResult, SubscriptionResult>;
}
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<Index>
: 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<Codec>),
[],
statusCb as CodecCallback,
isSubscription
) as unknown as SumbitableResultSubscription<CodecResult, SubscriptionResult>;
}
}
}
);
return _extrinsic;
}
+1 -1
View File
@@ -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>;
+10 -4
View File
@@ -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<CodecResult, SubscriptionResult> = (method: OnCallFunction<RxResult, RxResult>, params?: Array<CodecArg>, 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<number>;
sign (extrinsic: IExtrinsic, address: string, options: SignatureOptions): Promise<number>;
/**
* @description Receives an update for the extrinsic signed by a `signer.sign`
+2 -2
View File
@@ -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<EventRecord>): Array<EventRecord> | undefined {
export default function filterEvents (extHash: U8a, { block: { extrinsics, header } }: SignedBlock, allEvents: Array<EventRecord>): Array<EventRecord> | undefined {
// extrinsics to hashes
const myHash = extHash.toHex();
const allHashes = extrinsics.map((ext) => ext.hash.toHex());
@@ -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<string>, throwError: boolean): void {
const extractTypes = (types: Array<string>): Array<any> => {
@@ -32,9 +33,9 @@ export default function validateTypes (types: Array<string>, 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) {
+6 -8
View File
@@ -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 {
+1 -1
View File
@@ -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';
+32 -12
View File
@@ -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;
}
+17
View File
@@ -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;
}