diff --git a/packages/api/src/SignerPayload.ts b/packages/api/src/SignerPayload.ts index c86eeb98c5a2..4c70c3c51e09 100644 --- a/packages/api/src/SignerPayload.ts +++ b/packages/api/src/SignerPayload.ts @@ -37,18 +37,11 @@ const _Payload: Constructor = 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(), diff --git a/packages/api/src/SubmittableExtrinsic.ts b/packages/api/src/SubmittableExtrinsic.ts index cd2284b1cd75..b9639126800c 100644 --- a/packages/api/src/SubmittableExtrinsic.ts +++ b/packages/api/src/SubmittableExtrinsic.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 { 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 ( type: ApiTypes, api: ApiInterfaceRx, decorateMethod: ApiBase['decorateMethod'], - extrinsic: Call | Uint8Array | string, - trackingCb?: Callback + extrinsic: Call | Uint8Array | string ): SubmittableExtrinsic { const _extrinsic = createType('Extrinsic', extrinsic, { version: api.extrinsicType }) as unknown as SubmittableExtrinsic; const _noStatusCb = type === 'rxjs'; @@ -124,35 +123,27 @@ export default function createSubmittableExtrinsic ( function statusObservable (status: ExtrinsicStatus): Observable { 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, + api.rpc.chain.getBlock(blockHash), api.query.system.events.at(blockHash) as Observable> ]).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 { - return (api.rpc.author - .submitExtrinsic(_extrinsic) as Observable) + return api.rpc.author + .submitExtrinsic(_extrinsic) .pipe( tap((hash): void => { updateSigner(updateId, hash); @@ -161,8 +152,8 @@ export default function createSubmittableExtrinsic ( } function subscribeObservable (updateId: number = -1): Observable { - return (api.rpc.author - .submitAndWatchExtrinsic(_extrinsic) as Observable) + return api.rpc.author + .submitAndWatchExtrinsic(_extrinsic) .pipe( switchMap((status): Observable => statusObservable(status) @@ -206,6 +197,20 @@ export default function createSubmittableExtrinsic ( }); } + 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( @@ -246,71 +251,60 @@ export default function createSubmittableExtrinsic ( let updateId: number | undefined; return decorateMethod( - (): Observable => (( - 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() as Observable
- : of(null) - ]) - ).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, { + (): 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, - 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 | Observable => { - return isSubscription - ? subscribeObservable(updateId) - : sendObservable(updateId); - }) - ) as Observable) + }), + switchMap((): Observable | Observable => { + return isSubscription + ? subscribeObservable(updateId) + : sendObservable(updateId); + }) + ) as Observable) // FIXME This is wrong, SubmittableResult is _not_ a codec )(statusCb); } } diff --git a/packages/api/src/base/Decorate.ts b/packages/api/src/base/Decorate.ts index 38ba559e11d5..27c35bc17810 100644 --- a/packages/api/src/base/Decorate.ts +++ b/packages/api/src/base/Decorate.ts @@ -218,7 +218,7 @@ export default abstract class Decorate 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 => this._rpcCore.state.subscribeStorage( args.map((arg: Arg[] | Arg): [StorageEntry, Arg | Arg[]] => [creator, arg]))); diff --git a/packages/api/src/promise/Api.ts b/packages/api/src/promise/Api.ts index 4b16e389b629..2f021cb18163 100644 --- a/packages/api/src/promise/Api.ts +++ b/packages/api/src/promise/Api.ts @@ -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; + 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 | undefined] { + let callback: Callback | 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 => { + 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 *
* @@ -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 *
* @@ -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: Method, options?: DecorateMethodOptions): StorageEntryPromiseOverloads { const needsCallback = options && options.methodName && options.methodName.includes('subscribe'); return function (...args: any[]): Promise>> | UnsubscribePromise { - let callback: Callback | 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>>; } 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 => { - if (!isCompleted) { - isCompleted = true; - - reject(error); - } - - // we don't want to continue, so empty observable it is - return EMPTY; - }), + catchError((error): Observable => + 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; diff --git a/packages/api/src/rx/Api.ts b/packages/api/src/rx/Api.ts index 253ae6c537d3..6181c758fe0c 100644 --- a/packages/api/src/rx/Api.ts +++ b/packages/api/src/rx/Api.ts @@ -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 *
* @@ -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 *
* @@ -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); diff --git a/packages/api/test/e2e/api/rx-queries.spec.ts b/packages/api/test/e2e/api/rx-queries.spec.ts index 13e273aa81d8..9d2d9260eb0f 100644 --- a/packages/api/test/e2e/api/rx-queries.spec.ts +++ b/packages/api/test/e2e/api/rx-queries.spec.ts @@ -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
) + api.rpc.chain + .getHeader() .pipe( switchMap(({ hash }: Header): Observable => api.query.system.events.at(hash) diff --git a/packages/api/test/e2e/api/rx-tx.spec.ts b/packages/api/test/e2e/api/rx-tx.spec.ts index 9595f6580005..d430454a90a1 100644 --- a/packages/api/test/e2e/api/rx-tx.spec.ts +++ b/packages/api/test/e2e/api/rx-tx.spec.ts @@ -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) + api.query.system + .accountNonce(keyring.bob_stash.address) .pipe( first(), switchMap((nonce: Index): Observable => @@ -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) + 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(keyring.bob_stash.address) .pipe( first(), switchMap((nonce: Index): Observable => 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() ) diff --git a/packages/types/src/primitive/Type.ts b/packages/types/src/primitive/Type.ts index 3b7504622478..d5756659b586 100644 --- a/packages/types/src/primitive/Type.ts +++ b/packages/types/src/primitive/Type.ts @@ -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 ::Inherent -> InherentOfflineReport + Type._alias('::Inherent', 'InherentOfflineReport'), + // alias TreasuryProposal from Proposal> + Type._alias('Proposal>', 'TreasuryProposal'), + // + Type._cleanupCompact(), + // Remove all the trait prefixes + Type._removeTraits(), + // remove PairOf -> (T, T) + Type._removePairOf(), + // remove boxing, `Box` -> `Proposal` + Type._removeWrap('Box'), + // remove generics, `MisbehaviorReport` -> `MisbehaviorReport` + Type._removeGenerics(), + // alias String -> Text (compat with jsonrpc methods) + Type._alias('String', 'Text'), + // alias () -> Null + Type._alias('\\(\\)', 'Null'), + // alias Vec -> Bytes + Type._alias('Vec', '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, >::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 ::Inherent -> InherentOfflineReport - Type._alias('::Inherent', 'InherentOfflineReport'), - // alias TreasuryProposal from Proposal> - Type._alias('Proposal>', 'TreasuryProposal'), - // - Type._cleanupCompact(), - // Remove all the trait prefixes - Type._removeTraits(), - // remove PairOf -> (T, T) - Type._removePairOf(), - // remove boxing, `Box` -> `Proposal` - Type._removeWrap('Box'), - // remove generics, `MisbehaviorReport` -> `MisbehaviorReport` - Type._removeGenerics(), - // alias String -> Text (compat with jsonrpc methods) - Type._alias('String', 'Text'), - // alias () -> Null - Type._alias('\\(\\)', 'Null'), - // alias Vec -> Bytes - Type._alias('Vec', '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, >::Proposal -> ::Proposal - Type._removeColonPrefix() - ]; - - return mappings.reduce((result, fn): string => { + return Type._mappings.reduce((result, fn): string => { return fn(result); }, value).trim(); }