diff --git a/packages/api/src/checkTypes.manual.ts b/packages/api/src/checkTypes.manual.ts index e5dc317cbb..fa8b79b2f0 100644 --- a/packages/api/src/checkTypes.manual.ts +++ b/packages/api/src/checkTypes.manual.ts @@ -52,7 +52,7 @@ export default async function test (): Promise { // constants has actual value & metadata console.log( api.consts.balances.creationFee.toHex(), - api.consts.balances.creationFee.meta.documentation.map((s) => s.toString()).join('') + api.consts.balances.creationFee.meta.documentation.map((s): string => s.toString()).join('') ); const transfer = api.tx.balances.transfer(keyring.bob.address, 12345); diff --git a/packages/api/test/e2e/api/promise-tx.spec.ts b/packages/api/test/e2e/api/promise-tx.spec.ts index b7a05f420a..2607fe1283 100644 --- a/packages/api/test/e2e/api/promise-tx.spec.ts +++ b/packages/api/test/e2e/api/promise-tx.spec.ts @@ -5,6 +5,7 @@ import Keyring from '@polkadot/keyring'; import testingPairs from '@polkadot/keyring/testingPairs'; import WsProvider from '@polkadot/rpc-provider/ws'; +import { u8aToHex } from '@polkadot/util'; import { randomAsHex } from '@polkadot/util-crypto'; import { EventRecord, ExtrinsicEra, Hash, Header, Index, SignedBlock } from '@polkadot/types'; @@ -55,6 +56,28 @@ describeE2E({ return api.tx(hex).send(logEvents(done)); }); + it('invalid hex does throw a catchable exception', async (done): Promise => { + const nonce = await api.query.system.accountNonce(keyring.dave.address) as Index; + const hex = api.tx.balances + .transfer(keyring.eve.address, 12345) + .sign(keyring.dave, { nonce }) + .toHex(); + + // change to an invalid signature, 32 * 2 for hex + const sigIdx = hex.indexOf(u8aToHex(keyring.dave.publicKey).substr(2)) + 64; + const mangled = hex.replace( + hex.substr(sigIdx, 32), // first 16 bytes of sig + hex.substr(2, 32) // replaced by first 16 bytes of tx + ); + + try { + await api.tx(mangled).send(logEvents(done)); + } catch (error) { + console.error(error); + done(); + } + }); + it('makes a transfer (sign, then send)', async (done): Promise<() => void> => { const nonce = await api.query.system.accountNonce(keyring.dave.address) as Index; @@ -166,6 +189,31 @@ describeE2E({ }); }); + it('fails on a transfer with invalid time (via Signer)', async (done): Promise => { + const signer: Signer = new SingleAccountSigner(keyring.alice); + + api.setSigner(signer); + + const nonce = await api.query.system.accountNonce(keyring.alice.address) as Index; + const signedBlock = await api.rpc.chain.getBlock() as SignedBlock; + const currentHeight = signedBlock.block.header.number; + const exERA = new ExtrinsicEra({ current: currentHeight, period: 4 }); + const eraDeath = exERA.asMortalEra.death(currentHeight.toNumber()); + const blockHash = signedBlock.block.header.hash; + const ex = api.tx.balances.transfer(keyring.eve.address, 12345); + + await api.rpc.chain.subscribeNewHead(async (header: Header): Promise => { + if (header.blockNumber.toNumber() === eraDeath - 1) { + try { + await ex.signAndSend(keyring.alice.address, { blockHash, era: exERA, nonce } as any); + } catch (error) { + expect(error.message).toMatch(/1010: Invalid Transaction \(0\)/); + done(); + } + } + }); + }); + it('makes a transfer with custom numeric era', async (done): Promise => { await api.tx.balances .transfer(keyring.eve.address, 12345) diff --git a/packages/rpc-core/src/index.ts b/packages/rpc-core/src/index.ts index d4cbf65f4b..4063a0bd70 100644 --- a/packages/rpc-core/src/index.ts +++ b/packages/rpc-core/src/index.ts @@ -12,7 +12,7 @@ import { combineLatest, from, Observable, Observer, of, throwError } from 'rxjs' import { catchError, map, publishReplay, refCount, switchMap } from 'rxjs/operators'; import interfaces from '@polkadot/jsonrpc'; import { Option, StorageChangeSet, StorageData, StorageKey, Vector, createClass, createType } from '@polkadot/types'; -import { ExtError, assert, isFunction, isNull, logger } from '@polkadot/util'; +import { ExtError, assert, isFunction, isNull, isNumber, logger } from '@polkadot/util'; const l = logger('rpc-core'); @@ -173,7 +173,15 @@ export default class Rpc implements RpcInterface { const call = (...values: any[]): Observable => { return new Observable((observer: Observer): VoidCallback => { - let subscriptionPromise: Promise; + let subscriptionPromise: Promise; + + const errorHandler = (error: Error): void => { + const message = this.createErrorMessage(method, error); + + l.error(message); + + observer.error(new ExtError(message, (error as ExtError).code, undefined)); + }; try { const params = this.formatInputs(method, values); @@ -187,13 +195,11 @@ export default class Rpc implements RpcInterface { observer.next(this.formatOutput(method, params, result)); }; - subscriptionPromise = this.provider.subscribe(subType, subName, paramsJson, update); + subscriptionPromise = this.provider + .subscribe(subType, subName, paramsJson, update) + .catch(errorHandler); } catch (error) { - const message = this.createErrorMessage(method, error); - - l.error(message); - - observer.error(new ExtError(message, (error as ExtError).code, undefined)); + errorHandler(error); } // Teardown logic @@ -209,10 +215,13 @@ export default class Rpc implements RpcInterface { // ``` // eslint-disable-next-line @typescript-eslint/no-use-before-define memoized.delete(...values); + // Unsubscribe from provider - subscriptionPromise + subscriptionPromise && subscriptionPromise .then((subscriptionId): Promise => - this.provider.unsubscribe(subType, unsubName, subscriptionId) + isNumber(subscriptionId) + ? this.provider.unsubscribe(subType, unsubName, subscriptionId) + : Promise.resolve(false) ) .catch((error: Error): void => { const message = this.createErrorMessage(method, error);