Minor API-related readability cleanups (#1333)

* Minor API-related readability cleanups

* Oops, remove trial protected, revert to private

* Unneeded as Observable<...> casts

* Split state retrieval from signAndSend

* Typo in comment

* Cleanup descriptions
This commit is contained in:
Jaco Greeff
2019-08-21 17:38:49 +02:00
committed by GitHub
parent cab78c42b6
commit 7bfdd88ef6
8 changed files with 187 additions and 180 deletions
+1 -8
View File
@@ -37,18 +37,11 @@ const _Payload: Constructor<SignerPayloadType> = Struct.with({
});
export default class Payload extends _Payload {
/**
* @description Returns this as a SignerPayloadType. This works since the Struct.with injects all the getters automatically (just ensure the 2 definitiona are matching)
*/
public get self (): SignerPayloadType {
return this as any as SignerPayloadType;
}
/**
* @description Creates an representation of the structure as an ISignerPayload JSON
*/
public toPayload (): SignerPayload {
const { address, blockHash, blockNumber, era, genesisHash, method, nonce, runtimeVersion: { specVersion }, tip, version } = this.self;
const { address, blockHash, blockNumber, era, genesisHash, method, nonce, runtimeVersion: { specVersion }, tip, version } = this;
return {
address: address.toString(),
+75 -81
View File
@@ -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 { AccountId, Address, Call, ExtrinsicEra, ExtrinsicStatus, EventRecord, Hash, Header, Index, SignedBlock } from '@polkadot/types/interfaces';
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';
@@ -110,8 +110,7 @@ export default function createSubmittableExtrinsic<ApiType> (
type: ApiTypes,
api: ApiInterfaceRx,
decorateMethod: ApiBase<ApiType>['decorateMethod'],
extrinsic: Call | Uint8Array | string,
trackingCb?: Callback<ISubmittableResult>
extrinsic: Call | Uint8Array | string
): SubmittableExtrinsic<ApiType> {
const _extrinsic = createType('Extrinsic', extrinsic, { version: api.extrinsicType }) as unknown as SubmittableExtrinsic<ApiType>;
const _noStatusCb = type === 'rxjs';
@@ -124,35 +123,27 @@ export default function createSubmittableExtrinsic<ApiType> (
function statusObservable (status: ExtrinsicStatus): Observable<ISubmittableResult> {
if (!status.isFinalized) {
const result = new SubmittableResult({ status });
trackingCb && trackingCb(result);
return of(result);
return of(new SubmittableResult({ status }));
}
const blockHash = status.asFinalized;
return combineLatest([
api.rpc.chain.getBlock(blockHash) as Observable<SignedBlock>,
api.rpc.chain.getBlock(blockHash),
api.query.system.events.at(blockHash) as Observable<Vec<EventRecord>>
]).pipe(
map(([signedBlock, allEvents]): SubmittableResult => {
const result = new SubmittableResult({
map(([signedBlock, allEvents]): SubmittableResult =>
new SubmittableResult({
events: filterEvents(_extrinsic.hash, signedBlock, allEvents),
status
});
trackingCb && trackingCb(result);
return result;
})
})
)
);
}
function sendObservable (updateId: number = -1): Observable<Hash> {
return (api.rpc.author
.submitExtrinsic(_extrinsic) as Observable<Hash>)
return api.rpc.author
.submitExtrinsic(_extrinsic)
.pipe(
tap((hash): void => {
updateSigner(updateId, hash);
@@ -161,8 +152,8 @@ export default function createSubmittableExtrinsic<ApiType> (
}
function subscribeObservable (updateId: number = -1): Observable<ISubmittableResult> {
return (api.rpc.author
.submitAndWatchExtrinsic(_extrinsic) as Observable<ExtrinsicStatus>)
return api.rpc.author
.submitAndWatchExtrinsic(_extrinsic)
.pipe(
switchMap((status): Observable<ISubmittableResult> =>
statusObservable(status)
@@ -206,6 +197,20 @@ export default function createSubmittableExtrinsic<ApiType> (
});
}
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(
@@ -246,71 +251,60 @@ export default function createSubmittableExtrinsic<ApiType> (
let updateId: number | undefined;
return decorateMethod(
(): Observable<Codec> => ((
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() as Observable<Header>
: of(null)
])
).pipe(
first(),
mergeMap(async ([nonce, header]): Promise<void> => {
const eraOptions = expandEraOptions(options, { header, nonce });
(): 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, {
// 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,
blockNumber: header ? header.number.toBn() : new BN(0),
genesisHash: api.genesisHash
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('Invalid signer interface');
throw new Error('no signer exists');
}
} else {
throw new Error('no signer exists');
}
}),
switchMap((): Observable<ISubmittableResult> | Observable<Hash> => {
return isSubscription
? subscribeObservable(updateId)
: sendObservable(updateId);
})
) as Observable<Codec>)
}),
switchMap((): Observable<ISubmittableResult> | Observable<Hash> => {
return isSubscription
? subscribeObservable(updateId)
: sendObservable(updateId);
})
) as Observable<Codec>) // FIXME This is wrong, SubmittableResult is _not_ a codec
)(statusCb);
}
}
+1 -1
View File
@@ -218,7 +218,7 @@ export default abstract class Decorate<ApiType> extends Events {
decorated.key = (arg1?: Arg, arg2?: Arg): string =>
u8aToHex(compactStripLength(creator(creator.meta.type.isDoubleMap ? [arg1, arg2] : arg1))[1]);
// When using double map storage function, user need to path double map key as an array
// When using double map storage function, user need to pass double map key as an array
decorated.multi = decorateMethod((args: (Arg | Arg[])[]): Observable<Codec[]> =>
this._rpcCore.state.subscribeStorage(
args.map((arg: Arg[] | Arg): [StorageEntry, Arg | Arg[]] => [creator, arg])));
+56 -34
View File
@@ -12,13 +12,57 @@ import { isFunction, assert } from '@polkadot/util';
import ApiBase from '../base';
import Combinator, { CombinatorCallback, CombinatorFunction } from './Combinator';
interface Tracker {
reject: (value: Error) => Observable<never>;
resolve: (value: () => void) => void;
}
// extract the arguments and callback params from a value array possibly containing a callback
function extractArgs (args: any[], needsCallback: boolean): [any[], Callback<Codec> | undefined] {
let callback: Callback<Codec> | undefined;
const actualArgs = args.slice();
// If the last arg is a function, we pop it, put it into callback.
// actualArgs will then hold the actual arguments to be passed to `method`
if (args.length && isFunction(args[args.length - 1])) {
callback = actualArgs.pop();
}
// When we need a subscription, ensure that a valid callback is actually passed
assert(!needsCallback || isFunction(callback), 'Expected a callback to be passed with subscriptions');
return [actualArgs, callback];
}
// a Promise completion tracker, wrapping an isComplete variable that ensures the promise only resolves once
function promiseTracker (resolve: (value: () => void) => void, reject: (value: Error) => void): Tracker {
let isCompleted = false;
const complete = (fn: Function, value: any): void => {
if (!isCompleted) {
isCompleted = true;
fn(value);
}
};
return {
reject: (error: Error): Observable<never> => {
complete(reject, error);
return EMPTY;
},
resolve: (value: any): void => {
complete(resolve, value);
}
};
}
/**
* # @polkadot/api/promise
*
* ## Overview
*
* @name ApiPromise
*
* @description
* ApiPromise is a standard JavaScript wrapper around the RPC and interfaces on the Polkadot network. As a full Promise-based, all interface calls return Promises, including the static `.create(...)`. Subscription calls utilise `(value) => {}` callbacks to pass through the latest values.
*
@@ -102,10 +146,8 @@ export default class ApiPromise extends ApiBase<'promise'> {
/**
* @description Creates an ApiPromise instance using the supplied provider. Returns an Promise containing the actual Api instance.
*
* @param options options that is passed to the class contructor. Can be either [[ApiOptions]] or a
* provider (see the constructor arguments)
*
* @example
* <BR>
*
@@ -125,10 +167,8 @@ export default class ApiPromise extends ApiBase<'promise'> {
/**
* @description Creates an instance of the ApiPromise class
*
* @param options Options to create an instance. This can be either [[ApiOptions]] or
* an [[WsProvider]].
*
* @example
* <BR>
*
@@ -197,49 +237,31 @@ export default class ApiPromise extends ApiBase<'promise'> {
};
}
/**
* @description Decorate method for ApiPromise, where the results are converted to the Promise equivalent
*/
protected decorateMethod<Method extends AnyFunction> (method: Method, options?: DecorateMethodOptions): StorageEntryPromiseOverloads {
const needsCallback = options && options.methodName && options.methodName.includes('subscribe');
return function (...args: any[]): Promise<ObsInnerType<ReturnType<Method>>> | UnsubscribePromise {
let callback: Callback<Codec> | undefined;
const actualArgs = args.slice();
// If the last arg is a function, we pop it, put it into callback.
// actualArgs will then hold the actual arguments to be passed to `method`
if (args.length && isFunction(args[args.length - 1])) {
callback = actualArgs.pop();
}
// When we need a subscription, ensure that a valid callback is actually passed
assert(!needsCallback || isFunction(callback), 'Expected a callback to be passed with subscriptions');
const [actualArgs, callback] = extractArgs(args, !!needsCallback);
if (!callback) {
return method(...actualArgs).pipe(first()).toPromise() as Promise<ObsInnerType<ReturnType<Method>>>;
}
return new Promise((resolve, reject): void => {
let isCompleted = false;
const tracker = promiseTracker(resolve, reject);
const subscription = method(...actualArgs)
.pipe(
// if we find an error (invalid params, etc), reject the promise
catchError((error): Observable<never> => {
if (!isCompleted) {
isCompleted = true;
reject(error);
}
// we don't want to continue, so empty observable it is
return EMPTY;
}),
catchError((error): Observable<never> =>
tracker.reject(error)
),
// upon the first result, resolve the with the unsub function
tap((): void => {
if (!isCompleted) {
isCompleted = true;
resolve((): void => subscription.unsubscribe());
}
})
tap((): void =>
tracker.resolve((): void => subscription.unsubscribe())
)
)
.subscribe(callback);
}) as UnsubscribePromise;
+1 -5
View File
@@ -114,9 +114,7 @@ export default class ApiRx extends ApiBase<'rxjs'> {
/**
* @description Creates an ApiRx instance using the supplied provider. Returns an Observable containing the actual Api instance.
*
* @param options options that is passed to the class contructor. Can be either [[ApiOptions]] or [[WsProvider]]
*
* @example
* <BR>
*
@@ -140,9 +138,7 @@ export default class ApiRx extends ApiBase<'rxjs'> {
/**
* @description Create an instance of the ApiRx class
*
* @param options Options to create an instance. Can be either [[ApiOptions]] or [[WsProvider]]
*
* @example
* <BR>
*
@@ -164,7 +160,7 @@ export default class ApiRx extends ApiBase<'rxjs'> {
super(options, 'rxjs');
this._isReadyRx = from(
// convinced you can observable from an event, however my mind groks this form better
// You can create an observable from an event, however my mind groks this form better
new Promise((resolve): void => {
super.on('ready', (): void => {
resolve(this);
+2 -1
View File
@@ -31,7 +31,8 @@ describeE2E()('Rx e2e queries', (wsUrl: string): void => {
});
it('makes a query at a specific block', (done): void => {
(api.rpc.chain.getHeader() as Observable<Header>)
api.rpc.chain
.getHeader()
.pipe(
switchMap(({ hash }: Header): Observable<any> =>
api.query.system.events.at(hash)
+9 -7
View File
@@ -27,7 +27,8 @@ describeE2E()('Rx e2e transactions', (wsUrl: string): void => {
});
it('makes a transfer', (done): void => {
(api.query.system.accountNonce(keyring.bob_stash.address) as Observable<Index>)
api.query.system
.accountNonce<Index>(keyring.bob_stash.address)
.pipe(
first(),
switchMap((nonce: Index): Observable<SubmittableResult> =>
@@ -46,16 +47,17 @@ describeE2E()('Rx e2e transactions', (wsUrl: string): void => {
it('makes a proposal', (done): void => {
const amount = calculateAccountDeposit(api);
(api.query.system.accountNonce(keyring.bob_stash.address) as Observable<Index>)
const proposal = api.tx.system && api.tx.system.setCode
? api.tx.system.setCode(randomAsHex2097152) // since impl_version 94 https://github.com/paritytech/substrate/pull/2802
: api.tx.consensus.setCode(randomAsHex(4096)); // impl_version 0 - 93
api.query.system
.accountNonce<Index>(keyring.bob_stash.address)
.pipe(
first(),
switchMap((nonce: Index): Observable<SubmittableResult> =>
api.tx.democracy
.propose(
api.tx.system && api.tx.system.setCode
? api.tx.system.setCode(randomAsHex2097152) // since impl_version 94 https://github.com/paritytech/substrate/pull/2802
: api.tx.consensus.setCode(randomAsHex(4096)) // impl_version 0 - 93
, amount)
.propose(proposal, amount)
.sign(keyring.bob_stash, { nonce })
.send()
)
+42 -43
View File
@@ -7,7 +7,6 @@ import Text from './Text';
type Mapper = (value: string) => string;
const ALLOWED_BOXES = ['Compact', 'Option', 'Vec'];
/**
* @name Type
* @description
@@ -18,6 +17,47 @@ const ALLOWED_BOXES = ['Compact', 'Option', 'Vec'];
export default class Type extends Text {
private _originalLength: number;
private static _mappings: Mapper[] = [
// alias <T::InherentOfflineReport as InherentOfflineReport>::Inherent -> InherentOfflineReport
Type._alias('<T::InherentOfflineReport as InherentOfflineReport>::Inherent', 'InherentOfflineReport'),
// alias TreasuryProposal from Proposal<T::AccountId, BalanceOf<T>>
Type._alias('Proposal<T::AccountId, BalanceOf<T>>', 'TreasuryProposal'),
// <T::Balance as HasCompact>
Type._cleanupCompact(),
// Remove all the trait prefixes
Type._removeTraits(),
// remove PairOf<T> -> (T, T)
Type._removePairOf(),
// remove boxing, `Box<Proposal>` -> `Proposal`
Type._removeWrap('Box'),
// remove generics, `MisbehaviorReport<Hash, BlockNumber>` -> `MisbehaviorReport`
Type._removeGenerics(),
// alias String -> Text (compat with jsonrpc methods)
Type._alias('String', 'Text'),
// alias () -> Null
Type._alias('\\(\\)', 'Null'),
// alias Vec<u8> -> Bytes
Type._alias('Vec<u8>', 'Bytes'),
// alias &[u8] -> Bytes
Type._alias('&\\[u8\\]', 'Bytes'),
// alias RawAddress -> Address
Type._alias('RawAddress', 'Address'),
// alias Lookup::Source to Address (_could_ be AccountId on certain chains)
Type._alias('Lookup::Source', 'Address'),
// alias Lookup::Target to AccountId (always the case)
Type._alias('Lookup::Target', 'AccountId'),
// alias for grandpa, as used in polkadot
Type._alias('grandpa::AuthorityId', 'AuthorityId'),
// specific for SessionIndex (could make this session::, but be conservative)
Type._alias('session::SessionIndex', 'SessionIndex'),
// HACK duplication between contracts & primitives, however contracts prefixed with exec
Type._alias('exec::StorageKey', 'ContractStorageKey'),
// flattens tuples with one value, `(AccountId)` -> `AccountId`
Type._flattenSingleTuple(),
// converts ::Type to Type, <T as Trait<I>>::Proposal -> ::Proposal
Type._removeColonPrefix()
];
public constructor (value: Text | Uint8Array | string = '') {
// First decode it with Text
const textValue = new Text(value);
@@ -34,48 +74,7 @@ export default class Type extends Text {
}
private static decodeType (value: string): string {
const mappings: Mapper[] = [
// alias <T::InherentOfflineReport as InherentOfflineReport>::Inherent -> InherentOfflineReport
Type._alias('<T::InherentOfflineReport as InherentOfflineReport>::Inherent', 'InherentOfflineReport'),
// alias TreasuryProposal from Proposal<T::AccountId, BalanceOf<T>>
Type._alias('Proposal<T::AccountId, BalanceOf<T>>', 'TreasuryProposal'),
// <T::Balance as HasCompact>
Type._cleanupCompact(),
// Remove all the trait prefixes
Type._removeTraits(),
// remove PairOf<T> -> (T, T)
Type._removePairOf(),
// remove boxing, `Box<Proposal>` -> `Proposal`
Type._removeWrap('Box'),
// remove generics, `MisbehaviorReport<Hash, BlockNumber>` -> `MisbehaviorReport`
Type._removeGenerics(),
// alias String -> Text (compat with jsonrpc methods)
Type._alias('String', 'Text'),
// alias () -> Null
Type._alias('\\(\\)', 'Null'),
// alias Vec<u8> -> Bytes
Type._alias('Vec<u8>', 'Bytes'),
// alias &[u8] -> Bytes
Type._alias('&\\[u8\\]', 'Bytes'),
// alias RawAddress -> Address
Type._alias('RawAddress', 'Address'),
// alias Lookup::Source to Address (_could_ be AccountId on certain chains)
Type._alias('Lookup::Source', 'Address'),
// alias Lookup::Target to AccountId (always the case)
Type._alias('Lookup::Target', 'AccountId'),
// alias for grandpa, as used in polkadot
Type._alias('grandpa::AuthorityId', 'AuthorityId'),
// specific for SessionIndex (could make this session::, but be conservative)
Type._alias('session::SessionIndex', 'SessionIndex'),
// HACK duplication between contracts & primitives, however contracts prefixed with exec
Type._alias('exec::StorageKey', 'ContractStorageKey'),
// flattens tuples with one value, `(AccountId)` -> `AccountId`
Type._flattenSingleTuple(),
// converts ::Type to Type, <T as Trait<I>>::Proposal -> ::Proposal
Type._removeColonPrefix()
];
return mappings.reduce((result, fn): string => {
return Type._mappings.reduce((result, fn): string => {
return fn(result);
}, value).trim();
}