diff --git a/packages/api/src/promise/SubmittableExtrinsic.ts b/packages/api/src/promise/SubmittableExtrinsic.ts index ed83f5bde3..141ff94555 100644 --- a/packages/api/src/promise/SubmittableExtrinsic.ts +++ b/packages/api/src/promise/SubmittableExtrinsic.ts @@ -6,7 +6,7 @@ import { KeyringPair } from '@polkadot/util-keyring/types'; import { AnyNumber, AnyU8a } from '@polkadot/types/types'; import { ApiPromiseInterface } from './types'; -import { Extrinsic, Hash } from '@polkadot/types/index'; +import { Extrinsic, ExtrinsicStatus, Hash } from '@polkadot/types/index'; export default class SubmittableExtrinsic extends Extrinsic { private _api: ApiPromiseInterface; @@ -17,9 +17,11 @@ export default class SubmittableExtrinsic extends Extrinsic { this._api = api; } - // TODO We probably want a subscriptable sendAndWatch here as well + send (statusCb?: (status: ExtrinsicStatus) => any): Promise { + if (status) { + return this._api.rpc.author.extrinsicUpdate(this, statusCb); + } - send (): Promise { return this._api.rpc.author.submitExtrinsic(this); } diff --git a/packages/api/src/promise/index.ts b/packages/api/src/promise/index.ts index 03d35d5172..a9b6fa661a 100644 --- a/packages/api/src/promise/index.ts +++ b/packages/api/src/promise/index.ts @@ -90,9 +90,11 @@ import { StorageFunction } from '@polkadot/types/StorageKey'; * transfer(keyring.bob.address(), 12345) * // sign the transcation * .sign(keyring.alice, nonce) - * // send the transaction - * .send() - * // retrieve the overall result + * // send the transaction (optional status callback) + * .send((status) => { + * console.log(`current status ${status.type}`); + * }) + * // retrieve the submitted extrinsic hash * .then((hash) => { * console.log(`submitted with hash ${hash}`); * }); diff --git a/packages/api/src/rx/SubmittableExtrinsic.ts b/packages/api/src/rx/SubmittableExtrinsic.ts index 07768e98a1..7825c80031 100644 --- a/packages/api/src/rx/SubmittableExtrinsic.ts +++ b/packages/api/src/rx/SubmittableExtrinsic.ts @@ -7,7 +7,7 @@ import { AnyNumber, AnyU8a } from '@polkadot/types/types'; import { ApiRxInterface } from './types'; import { Observable } from 'rxjs'; -import { Extrinsic } from '@polkadot/types/index'; +import { Extrinsic, ExtrinsicStatus } from '@polkadot/types/index'; export default class SubmittableExtrinsic extends Extrinsic { private _api: ApiRxInterface; @@ -18,9 +18,8 @@ export default class SubmittableExtrinsic extends Extrinsic { this._api = api; } - // TODO needs to be submitAndWatch - send (): Observable { - return this._api.rpc.author.submitExtrinsic(this); + send (): Observable { + return this._api.rpc.author.extrinsicUpdate(this); } sign (signerPair: KeyringPair, nonce: AnyNumber, blockHash?: AnyU8a): SubmittableExtrinsic { diff --git a/packages/rpc-core/src/index.ts b/packages/rpc-core/src/index.ts index 55f6989a6c..7e83adddbf 100644 --- a/packages/rpc-core/src/index.ts +++ b/packages/rpc-core/src/index.ts @@ -157,10 +157,15 @@ export default class Rpc implements RpcInterface { } private formatInputs (method: RpcMethod, inputs: Array): Array { - assert(method.params.length === inputs.length, `Expected ${method.params.length} parameters, ${inputs.length} found instead`); + const reqArgCount = method.params.filter(({ isOptional }) => !isOptional).length; + const optText = reqArgCount === method.params.length + ? '' + : ` (${method.params.length - reqArgCount} optional)`; - return method.params.map(({ type }, index) => - createType(type as string, inputs[index]) + assert(inputs.length >= reqArgCount && inputs.length <= method.params.length, `Expected ${method.params.length} parameters${optText}, ${inputs.length} found instead`); + + return inputs.map((input, index) => + createType(method.params[index].type, input) ); } diff --git a/packages/type-extrinsics/test/e2e/transfer.spec.js b/packages/type-extrinsics/test/e2e/transfer.spec.js index 58252b2578..66e0ef25b7 100644 --- a/packages/type-extrinsics/test/e2e/transfer.spec.js +++ b/packages/type-extrinsics/test/e2e/transfer.spec.js @@ -14,7 +14,6 @@ describe.skip('e2e transfer', () => { let api; beforeAll(() => { - // check other e2e tests here... api = new Rpc(new Ws('ws://127.0.0.1:9944')); }); @@ -25,7 +24,7 @@ describe.skip('e2e transfer', () => { return api.author.submitExtrinsic(inherent.toU8a()).then(console.log); }); - it('makes a transfer for a transaction', () => { + it.skip('makes a transfer for a transaction', () => { return api.chain .getBlockHash(0) .then((genesisHash) => { @@ -37,4 +36,23 @@ describe.skip('e2e transfer', () => { }) .then(console.log); }); + + it('makes a transfer via watch', (done) => { + return api.chain + .getBlockHash(0) + .then((genesisHash) => { + const extrinsic = extrinsics.balances.transfer(keyring.bob.publicKey(), 6969); + + extrinsic.sign(keyring.alice, 0, genesisHash); + + return api.author.extrinsicUpdate(extrinsic, (status) => { + console.log(status); + + if (status.type === 'Finalised') { + done(); + } + }); + }) + .then(console.log); + }); }); diff --git a/packages/type-jsonrpc/src/author.ts b/packages/type-jsonrpc/src/author.ts index f132f29270..3bc48374cc 100644 --- a/packages/type-jsonrpc/src/author.ts +++ b/packages/type-jsonrpc/src/author.ts @@ -22,6 +22,20 @@ const submitExtrinsic: RpcMethodOpt = { type: 'Hash' }; +const extrinsicUpdate: RpcMethodOpt = { + description: 'Subscribe and watch an extrinsic until unsubscribed', + isSigned: true, + isSubscription: true, + subscribe: [ + 'author_submitAndWatchExtrinsic', + 'author_unwatchExtrinsic' + ], + params: [ + createParam('extinsic', 'Extrinsic') + ], + type: 'ExtrinsicStatus' +}; + const section = 'author'; /** @@ -34,6 +48,7 @@ export default { section, methods: { pendingExtrinsics: createMethod(section, 'pendingExtrinsics', pendingExtrinsics), - submitExtrinsic: createMethod(section, 'submitExtrinsic', submitExtrinsic) + submitExtrinsic: createMethod(section, 'submitExtrinsic', submitExtrinsic), + extrinsicUpdate: createMethod(section, 'extrinsicUpdate', extrinsicUpdate) } } as RpcSection; diff --git a/packages/type-jsonrpc/src/chain.ts b/packages/type-jsonrpc/src/chain.ts index c2228b7e56..a27af5edb6 100644 --- a/packages/type-jsonrpc/src/chain.ts +++ b/packages/type-jsonrpc/src/chain.ts @@ -7,10 +7,20 @@ import { RpcMethodOpt, RpcSection } from './types'; import createMethod from './create/method'; import createParam from './create/param'; +// NOTE Order is the same as in Rust + +const getHeader: RpcMethodOpt = { + description: 'Retrieves the header for a specific block', + params: [ + createParam('hash', 'Hash', { isOptional: true }) + ], + type: 'Header' +}; + const getBlock: RpcMethodOpt = { description: 'Get header and body of a relay chain block', params: [ - createParam('hash', 'Hash') + createParam('hash', 'Hash', { isOptional: true }) ], type: 'SignedBlock' }; @@ -29,24 +39,10 @@ const getHead: RpcMethodOpt = { type: 'Hash' }; -const getHeader: RpcMethodOpt = { - description: 'Retrieves the header for a specific block', - params: [ - createParam('hash', 'Hash') - ], - type: 'Header' -}; - const getRuntimeVersion: RpcMethodOpt = { description: ' Get the runtime version', - params: [], - type: 'RuntimeVersion' -}; - -const getRuntimeVersionAt: RpcMethodOpt = { - description: ' Get the runtime version at a specific block', params: [ - createParam('hash', 'Hash') + createParam('hash', 'Hash', { isOptional: true }) ], type: 'RuntimeVersion' }; @@ -77,7 +73,6 @@ export default { getHead: createMethod(section, 'getHead', getHead), getHeader: createMethod(section, 'getHeader', getHeader), getRuntimeVersion: createMethod(section, 'getRuntimeVersion', getRuntimeVersion), - getRuntimeVersionAt: createMethod(section, 'getRuntimeVersionAt', getRuntimeVersionAt), newHead: createMethod(section, 'newHead', newHead) } } as RpcSection; diff --git a/packages/type-jsonrpc/src/state.ts b/packages/type-jsonrpc/src/state.ts index 0c8cdea176..903b7ff34e 100644 --- a/packages/type-jsonrpc/src/state.ts +++ b/packages/type-jsonrpc/src/state.ts @@ -7,6 +7,8 @@ import { RpcMethodOpt, RpcSection } from './types'; import createMethod from './create/method'; import createParam from './create/param'; +// NOTE Order here is the same as in the Rust code + const call: RpcMethodOpt = { description: 'Perform a call to a builtin on the chain', params: [ @@ -26,20 +28,6 @@ const callAt: RpcMethodOpt = { type: 'Bytes' }; -const getMetadata: RpcMethodOpt = { - description: 'Returns the runtime metadata', - params: [], - type: 'Metadata' -}; - -const getMetadataAt: RpcMethodOpt = { - description: 'Returns the runtime metadata', - params: [ - createParam('block', 'Hash') - ], - type: 'Metadata' -}; - const getStorage: RpcMethodOpt = { description: 'Retrieves the storage for a key', params: [ @@ -91,6 +79,26 @@ const getStorageSizeAt: RpcMethodOpt = { type: 'u64' }; +const getMetadata: RpcMethodOpt = { + description: 'Returns the runtime metadata', + params: [ + createParam('block', 'Hash', { isOptional: true }) + ], + type: 'Metadata' +}; + +const queryStorage: RpcMethodOpt = { + description: 'Query historical storage entries (by key) starting from a start block', + params: [ + // @ts-ignore The Vec<> wrap is fine + createParam('keys', 'Vec'), + createParam('startBlock', 'Hash'), + createParam('block', 'Hash', { isOptional: true }) + ], + // @ts-ignore The Vec<> wrap is fine + type: 'Vec' +}; + const storage: RpcMethodOpt = { description: 'Subscribes to storage changes for the provided keys', subscribe: [ @@ -118,13 +126,13 @@ export default { call: createMethod(section, 'call', call), callAt: createMethod(section, 'callAt', callAt), getMetadata: createMethod(section, 'getMetadata', getMetadata), - getMetadataAt: createMethod(section, 'getMetadataAt', getMetadataAt), getStorage: createMethod(section, 'getStorage', getStorage), getStorageAt: createMethod(section, 'getStorageAt', getStorageAt), getStorageHash: createMethod(section, 'getStorageHash', getStorageHash), getStorageHashAt: createMethod(section, 'getStorageHashAt', getStorageHashAt), getStorageSize: createMethod(section, 'getStorageSize', getStorageSize), getStorageSizeAt: createMethod(section, 'getStorageSizeAt', getStorageSizeAt), + queryStorage: createMethod(section, 'queryStorage', queryStorage), storage: createMethod(section, 'storage', storage) } } as RpcSection; diff --git a/packages/type-jsonrpc/src/system.ts b/packages/type-jsonrpc/src/system.ts index ef1d826908..0e35a0fe6f 100644 --- a/packages/type-jsonrpc/src/system.ts +++ b/packages/type-jsonrpc/src/system.ts @@ -6,11 +6,7 @@ import { RpcMethodOpt, RpcSection } from './types'; import createMethod from './create/method'; -const chain: RpcMethodOpt = { - description: 'Retrieves the chain', - params: [], - type: 'Text' -}; +// NOTE order here is the same as in Rust, alphabetical below const name: RpcMethodOpt = { description: 'Retrieves the node name', @@ -24,6 +20,12 @@ const version: RpcMethodOpt = { type: 'Text' }; +const chain: RpcMethodOpt = { + description: 'Retrieves the chain', + params: [], + type: 'Text' +}; + const section = 'system'; /** diff --git a/packages/types/src/ExtrinsicStatus.rpc.json b/packages/types/src/ExtrinsicStatus.rpc.json new file mode 100644 index 0000000000..d7fd1e2d4d --- /dev/null +++ b/packages/types/src/ExtrinsicStatus.rpc.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","method":"author_extrinsicUpdate","params":{"result":{"finalised":"0xc465b92a72b1d20918d64cd4effa70c2bb58b53a3f8c24c3ac8fd8f465f059b4"},"subscription":9}} diff --git a/packages/types/src/ExtrinsicStatus.spec.js b/packages/types/src/ExtrinsicStatus.spec.js new file mode 100644 index 0000000000..0fa15ba56d --- /dev/null +++ b/packages/types/src/ExtrinsicStatus.spec.js @@ -0,0 +1,22 @@ +// Copyright 2017-2018 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +import rpc from './ExtrinsicStatus.rpc.json'; +import ExtrinsicStatus from './ExtrinsicStatus'; + +describe('ExtrinsicStatus', () => { + const status = new ExtrinsicStatus().fromJSON(rpc.params.result); + + it('has the correct type', () => { + expect( + status.type + ).toEqual('Finalised'); + }); + + it('has the correct hash', () => { + expect( + status.value.toHex() + ).toEqual('0xc465b92a72b1d20918d64cd4effa70c2bb58b53a3f8c24c3ac8fd8f465f059b4'); + }); +}); diff --git a/packages/types/src/Status.ts b/packages/types/src/ExtrinsicStatus.ts similarity index 86% rename from packages/types/src/Status.ts rename to packages/types/src/ExtrinsicStatus.ts index 2e03feb6f5..158fc4b565 100644 --- a/packages/types/src/Status.ts +++ b/packages/types/src/ExtrinsicStatus.ts @@ -20,7 +20,7 @@ export class Finalised extends Hash { export class Usurped extends Hash { } -export default class Status extends EnumType { +export default class ExtrinsicStatus extends EnumType { constructor (index?: number) { super([ Finalised, diff --git a/packages/types/src/MisbehaviorReport.spec.js b/packages/types/src/MisbehaviorReport.spec.js index c16df5e627..9032e35e1e 100644 --- a/packages/types/src/MisbehaviorReport.spec.js +++ b/packages/types/src/MisbehaviorReport.spec.js @@ -39,7 +39,7 @@ describe('MisbehaviorReport', () => { }); it('has the correct misbehavior type', () => { - expect(report.misbehavior.Type).toEqual('BftDoubleCommit'); + expect(report.misbehavior.type).toEqual('BftDoubleCommit'); }); it('has the correct parent block', () => { diff --git a/packages/types/src/codec/EnumType.ts b/packages/types/src/codec/EnumType.ts index cc6073eca2..e906236684 100644 --- a/packages/types/src/codec/EnumType.ts +++ b/packages/types/src/codec/EnumType.ts @@ -2,6 +2,8 @@ // This software may be modified and distributed under the terms // of the ISC license. See the LICENSE file for details. +import isUndefined from '@polkadot/util/is/undefined'; + import Base from './Base'; type TypesArray = Array<{ new(value?: any): Base }>; @@ -37,14 +39,37 @@ export default class EnumType extends Base> { this.setValue(index, value); } - get Type (): string { + get type (): string { return this._Types[this._index].name; } + get value (): Base { + return this.raw; + } + byteLength (): number { return 1 + this.raw.byteLength(); } + fromJSON (input: any = {}): EnumType { + // JSON comes in the form of { "": "" }, here we + // additionally force to lower to ensure forward compat + const key = Object.keys(input)[0]; + const lowerKey = key.toLowerCase(); + const index = this._indexes.find((value, index) => + this._Types[index].name.toLowerCase() === lowerKey + ); + + if (isUndefined(index)) { + throw new Error('Unable to reliably map input on JSON'); + } + + this.setValue(index); + this.raw.fromJSON(input[key]); + + return this; + } + fromU8a (input: Uint8Array): EnumType { this.setValue(input[0]); this.raw.fromU8a(input.subarray(1)); @@ -78,6 +103,6 @@ export default class EnumType extends Base> { } toString (): string { - return this._Types[this._index].name; + return this.type; } } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index d3df78fefd..4abb3af8a8 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -27,6 +27,7 @@ export { default as Compact256 } from './Compact256'; export { default as Extrinsic } from './Extrinsic'; export { default as ExtrinsicEra } from './ExtrinsicEra'; export { default as ExtrinsicSignature } from './ExtrinsicSignature'; +export { default as ExtrinsicStatus } from './ExtrinsicStatus'; // NOTE Only used internally, exported as PendingExtrinsics // export { default as Extrinsics } from './Extrinsics'; export { default as Gas } from './Gas'; @@ -58,7 +59,6 @@ export { default as ReferendumIndex } from './ReferendumIndex'; export { default as RuntimeVersion } from './RuntimeVersion'; export { default as Signature } from './Signature'; export { default as SignedBlock } from './SignedBlock'; -export { default as Status } from './Status'; export { default as StorageChangeSet } from './StorageChangeSet'; export { default as StorageData } from './StorageData'; export { default as StorageKey } from './StorageKey';