diff --git a/CHANGELOG.md b/CHANGELOG.md index a673220613fd..e33334eab0a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,15 @@ # 0.82.0-beta.x - **Breaking change** The `ContractsAbi` type has been moved from `@polkadot/types` to `import { Abi } from '@polkadot/api-contract`. This paves the way for an enhanced contracts interface, instead of dealing with low-level API calls. -- **Breaking change** `usize` is now a blacklisted type that will throw on construction. Since it is platform-specific, it creates incompatibilities between native (generally u64) and WASM (always u32) code. Use one of the `u32` or `u64` types explicitly. - **Breaking change** `api.derive.contract` is now `api.derive.contracts` to align with the substrate 2.x rename. (Feture detection is used so it supports both 1.x and 2.x chains) - **Breaking change** The api now uses the module name instead of the prefix to generate the storage methods. The methods of the grandpa module changed from `api.query.grandpaFinality` to `api.query.grandpa`. -- Update with latest substrate 2.x types - **Breaking Change** StorageFunction has been renamed to StorageEntry. - **Breaking Change** `@polkadot/extrinsics` and `@polkadot/storage` have been moved to `@polkadot/api-metadata` and are now accessible as `@polkadot/api-metadata/extrinsics` and `@polkadot/api-metadata/storage`, respectively. - **Breaking Change** Vote interface extends U8a instead of I8. Vote properties can be accessed via the `isAye`, `isNay`, and `conviction` getters. Votes can still be constructed as before with a raw JS boolean, a SCALE encoded Boolean, an i8 number, or a JS object with properties `aye` and `conviction` defined. - Support latest substrate 2.x v6 metadata with module constants using `api.consts`. +- Support V2 Extrinsics in addition to V1 +- Addition of `api.derive.elections` +- `usize` is now a blacklisted type that will throw on construction. Since it is platform-specific, it creates incompatibilities between native (generally u64) and WASM (always u32) code. Use one of the `u32` or `u64` types explicitly. # 0.81.1 diff --git a/packages/api-metadata/src/extrinsics/index.spec.ts b/packages/api-metadata/src/extrinsics/index.spec.ts index 4a1d80b89bad..291c8eba0f30 100644 --- a/packages/api-metadata/src/extrinsics/index.spec.ts +++ b/packages/api-metadata/src/extrinsics/index.spec.ts @@ -10,22 +10,6 @@ import extrinsics from './static'; const keyring = testingPairs({ type: 'ed25519' }, false); describe('extrinsics', (): void => { - it('encodes extrinsic correctly (nobody)', (): void => { - expect( - new Extrinsic( - extrinsics.timestamp.set(10101) - ).sign( - keyring.nobody, - { - blockHash: new Uint8Array(), - nonce: 1234 - } - ).toU8a(true) - ).toEqual(new Uint8Array([ - 129, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 73, 19, 0, 2, 0, 213, 157 - ])); - }); - it('encodes an actual transfer (actual data)', (): void => { expect( new Extrinsic( @@ -41,7 +25,7 @@ describe('extrinsics', (): void => { 'ffd172a74cda4c865912c32ba0a80a57ae69abae410e5ccb59dee84e2f4432db4f' + // who 'fa4c192f6960a3bcdbdee5bcd9c26a3f971b131081912abcc31eab6a0b7589ab' + // sig1 '7f99b81a01738cb5e2a911a19d5daa5c0b654d4b8dbc521a6b29090c6d205903' + // sig2 - '0000' + // nonce + '0000' + // nonce & era '0500' + // balances.transfer 'ffd7568e5f0a7eda67a82691ff379ac4bba4f9c9b859fe779b5d46363b61ad2db9' + // to 'e56c' // value diff --git a/packages/api/src/Base.ts b/packages/api/src/Base.ts index 4f3abdf8d2de..943b41c9904b 100644 --- a/packages/api/src/Base.ts +++ b/packages/api/src/Base.ts @@ -22,8 +22,9 @@ import { Storage } from '@polkadot/api-metadata/storage/types'; import storageFromMeta from '@polkadot/api-metadata/storage/fromMetadata'; import RpcCore from '@polkadot/rpc-core'; import { WsProvider } from '@polkadot/rpc-provider'; -import { Event, getTypeRegistry, Hash, Metadata, Method, RuntimeVersion, Null, U64 } from '@polkadot/types'; +import { Event, getTypeRegistry, Hash, Metadata, Method, RuntimeVersion, SignedBlock, Null, U64 } from '@polkadot/types'; import Linkage, { LinkageResult } from '@polkadot/types/codec/Linkage'; +import { DEFAULT_VERSION as EXTRINSIC_DEFAULT_VERSION } from '@polkadot/types/primitive/Extrinsic/constants'; import { MethodFunction, ModulesWithMethods } from '@polkadot/types/primitive/Method'; import * as srmlTypes from '@polkadot/types/srml/definitions'; import { StorageEntry } from '@polkadot/types/primitive/StorageKey'; @@ -72,6 +73,8 @@ export default abstract class ApiBase { private _extrinsics?: SubmittableExtrinsics; + private _extrinsicType: number = EXTRINSIC_DEFAULT_VERSION; + private _genesisHash?: Hash; protected _isConnected: BehaviorSubject; @@ -150,6 +153,13 @@ export default abstract class ApiBase { this.init(); } + /** + * @description Returns th version of extrinsics in-use on this chain + */ + public get extrinsicVersion (): number { + return this._extrinsicType; + } + /** * @description Contains the genesis Hash of the attached chain. Apart from being useful to determine the actual chain, it can also be used to sign immortal transactions. */ @@ -498,7 +508,9 @@ export default abstract class ApiBase { this._rpcCore.chain.getBlockHash(0).toPromise(), this._rpcCore.chain.getRuntimeVersion().toPromise() ]); + const metadataKey = `${this._genesisHash}-${(this._runtimeVersion as RuntimeVersion).specVersion}`; + if (metadataKey in metadata) { this._runtimeMetadata = new Metadata(metadata[metadataKey]); } else { @@ -508,6 +520,7 @@ export default abstract class ApiBase { // get unique types & validate this.runtimeMetadata.getUniqTypes(false); } else { + this._extrinsicType = this._options.source.extrinsicVersion; this._runtimeMetadata = this._options.source.runtimeMetadata; this._runtimeVersion = this._options.source.runtimeVersion; this._genesisHash = this._options.source.genesisHash; @@ -517,22 +530,29 @@ export default abstract class ApiBase { const storage = storageFromMeta(this.runtimeMetadata); const constants = constantsFromMeta(this.runtimeMetadata); + // only inject if we are not a clone (global init) + if (!this._options.source) { + Event.injectMetadata(this.runtimeMetadata); + Method.injectMethods(extrinsics); + + // detect the extrinsic version in-use based on the last block + const lastBlock: SignedBlock = await this._rpcCore.chain.getBlock().toPromise(); + + this._extrinsicType = lastBlock.block.extrinsics[0].type; + } + this._extrinsics = this.decorateExtrinsics(extrinsics, this.decorateMethod); this._query = this.decorateStorage(storage, this.decorateMethod); this._consts = constants; + this._rx.extrinsicType = this._extrinsicType; this._rx.genesisHash = this._genesisHash; this._rx.runtimeVersion = this._runtimeVersion; this._rx.tx = this.decorateExtrinsics(extrinsics, rxDecorateMethod); this._rx.query = this.decorateStorage(storage, rxDecorateMethod); this._rx.consts = constants; - this._derive = this.decorateDerive(this._rx as ApiInterfaceRx, this.decorateMethod); - // only inject if we are not a clone (global init) - if (!this._options.source) { - Event.injectMetadata(this.runtimeMetadata); - Method.injectMethods(extrinsics); - } + this._derive = this.decorateDerive(this._rx as ApiInterfaceRx, this.decorateMethod); return true; } diff --git a/packages/api/src/SignerPayload.spec.ts b/packages/api/src/SignerPayload.spec.ts new file mode 100644 index 000000000000..68262e8a3e05 --- /dev/null +++ b/packages/api/src/SignerPayload.spec.ts @@ -0,0 +1,77 @@ +// Copyright 2017-2019 @polkadot/api authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import extrinsics from '@polkadot/api-metadata/extrinsics/static'; +import { Method, ExtrinsicEra, SignaturePayload } from '@polkadot/types'; + +import SignerPayload from './SignerPayload'; + +describe('SignerPayload', (): void => { + const TEST = { + address: '5DTestUPts3kjeXSTMyerHihn1uwMfLj8vU8sqF7qYrFabHE', + blockHash: '0xde8f69eeb5e065e18c6950ff708d7e551f68dc9bf59a07c52367c0280f805ec7', + blockNumber: '0x0000000000231d30', + era: '0x0703', + genesisHash: '0xdcd1346701ca8396496e52aa2785b1748deb6db09551b72159dcb3e08991025b', + method: '0x0500ffd7568e5f0a7eda67a82691ff379ac4bba4f9c9b859fe779b5d46363b61ad2db9e56c', + nonce: '0x0000000000001234', + tip: '0x00000000000000000000000000005678', + version: 2 + }; + + beforeEach((): void => { + Method.injectMethods(extrinsics); + }); + + it('creates a valid JSON output', (): void => { + expect( + new SignerPayload({ + address: '5DTestUPts3kjeXSTMyerHihn1uwMfLj8vU8sqF7qYrFabHE', + blockHash: '0xde8f69eeb5e065e18c6950ff708d7e551f68dc9bf59a07c52367c0280f805ec7', + blockNumber: '0x231d30', + era: new ExtrinsicEra({ current: 2301232, period: 200 }), + genesisHash: '0xdcd1346701ca8396496e52aa2785b1748deb6db09551b72159dcb3e08991025b', + method: new Method('0x0500ffd7568e5f0a7eda67a82691ff379ac4bba4f9c9b859fe779b5d46363b61ad2db9e56c'), + nonce: 0x1234, + tip: 0x5678, + version: 2 + }).toPayload() + ).toEqual({ + address: '5DTestUPts3kjeXSTMyerHihn1uwMfLj8vU8sqF7qYrFabHE', + blockHash: '0xde8f69eeb5e065e18c6950ff708d7e551f68dc9bf59a07c52367c0280f805ec7', + blockNumber: '0x0000000000231d30', + era: '0x0703', + genesisHash: '0xdcd1346701ca8396496e52aa2785b1748deb6db09551b72159dcb3e08991025b', + method: '0x0500ffd7568e5f0a7eda67a82691ff379ac4bba4f9c9b859fe779b5d46363b61ad2db9e56c', + nonce: '0x0000000000001234', + tip: '0x00000000000000000000000000005678', + version: 2 + }); + }); + + it('re-constructs from JSON', (): void => { + expect( + new SignerPayload(TEST).toPayload() + ).toEqual(TEST); + }); + + it('re-constructs from itself', (): void => { + expect( + new SignerPayload( + new SignerPayload(TEST) + ).toPayload() + ).toEqual(TEST); + }); + + it('can be used as a feed to SignaturePayload', (): void => { + const signer = new SignerPayload(TEST).toPayload(); + const payload = new SignaturePayload(signer, { version: signer.version }); + + expect(payload.era.toHex()).toEqual(TEST.era); + expect(payload.method.toHex()).toEqual(TEST.method); + expect(payload.blockHash.toHex()).toEqual(TEST.blockHash); + expect(payload.nonce.eq(TEST.nonce)).toBe(true); + expect(payload.tip.eq(TEST.tip)).toBe(true); + }); +}); diff --git a/packages/api/src/SignerPayload.ts b/packages/api/src/SignerPayload.ts new file mode 100644 index 000000000000..7ce4cbbd1fc7 --- /dev/null +++ b/packages/api/src/SignerPayload.ts @@ -0,0 +1,57 @@ +// Copyright 2017-2019 @polkadot/api authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { SignerPayload as ISignerPayload } from './types'; + +import { Address, Balance, BlockNumber, Compact, ExtrinsicEra, Hash, Nonce, Struct, U8, Method } from '@polkadot/types'; + +export interface SignerPayloadType { + address: Address; + blockHash: Hash; + blockNumber: BlockNumber; + era: ExtrinsicEra; + genesisHash: Hash; + method: Method; + nonce: Compact; + tip: Compact; + version: U8; +} + +export default class SignerPayload extends Struct.with({ + address: Address, + blockHash: Hash, + blockNumber: BlockNumber, + era: ExtrinsicEra, + genesisHash: Hash, + method: Method, + nonce: Compact.with(Nonce), + tip: Compact.with(Balance), + version: U8 +}) { + /** + * @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 (): ISignerPayload { + const { address, blockHash, blockNumber, era, genesisHash, method, nonce, tip, version } = this.self; + + return { + address: address.toString(), + blockHash: blockHash.toHex(), + blockNumber: blockNumber.toHex(), + era: era.toHex(), + genesisHash: genesisHash.toHex(), + method: method.toHex(), + nonce: nonce.toHex(), + tip: tip.toHex(), + version: version.toNumber() + }; + } +} diff --git a/packages/api/src/SubmittableExtrinsic.ts b/packages/api/src/SubmittableExtrinsic.ts index 2618c1d1eaee..c6032962cc80 100644 --- a/packages/api/src/SubmittableExtrinsic.ts +++ b/packages/api/src/SubmittableExtrinsic.ts @@ -2,17 +2,18 @@ // 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, ExtrinsicStatus, EventRecord, getTypeRegistry, Hash, Header, Index, Method, SignedBlock, Vector, ExtrinsicEra } from '@polkadot/types'; import { AnyNumber, AnyU8a, Callback, Codec, IExtrinsic, IExtrinsicEra, IKeyringPair, SignatureOptions } from '@polkadot/types/types'; import { ApiInterfaceRx, ApiTypes } from './types'; import BN from 'bn.js'; import { Observable, combineLatest, of } from 'rxjs'; import { first, map, mergeMap, switchMap, tap } from 'rxjs/operators'; +import { AccountId, Address, ExtrinsicStatus, EventRecord, getTypeRegistry, Hash, Header, Index, Method, SignedBlock, Vector, ExtrinsicEra } from '@polkadot/types'; import { isBn, isFunction, isNumber, isUndefined } from '@polkadot/util'; -import ApiBase from './Base'; import filterEvents from './util/filterEvents'; +import ApiBase from './Base'; +import SignerPayload from './SignerPayload'; // eslint-disable-next-line @typescript-eslint/interface-name-prefix export interface ISubmittableResult { @@ -44,10 +45,14 @@ interface SignerOptions { blockHash: AnyU8a; era?: IExtrinsicEra | number; nonce: AnyNumber; + tip?: AnyNumber; } -// pick a default - in the case of 4s blocktimes, this translates to 60 seconds -const ONE_MINUTE = 15; +// The default for 6s allowing for 5min eras. When translating this to faster blocks - +// - 4s = (10 / 15) * 5 = 3.33m +// - 2s = (10 / 30) * 5 = 1.66m +const BLOCKTIME = 6; +const ONE_MINUTE = 60 / BLOCKTIME; const DEFAULT_MORTAL_LENGTH = 5 * ONE_MINUTE; function isKeyringPair (account: string | IKeyringPair | AccountId | Address): account is IKeyringPair { @@ -107,7 +112,7 @@ export default function createSubmittableExtrinsic ( extrinsic: Method | Uint8Array | string, trackingCb?: Callback ): SubmittableExtrinsic { - const _extrinsic = new (getTypeRegistry().getOrThrow('Extrinsic'))(extrinsic) as SubmittableExtrinsic; + const _extrinsic = new (getTypeRegistry().getOrThrow('Extrinsic'))(extrinsic, { version: api.extrinsicType }) as SubmittableExtrinsic; const _noStatusCb = type === 'rxjs'; function updateSigner (updateId: number, status: Hash | ISubmittableResult): void { @@ -257,14 +262,39 @@ export default function createSubmittableExtrinsic ( 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) { - updateId = await api.signer.sign(_extrinsic, address, { - ...eraOptions, - blockNumber: header ? header.blockNumber : new BN(0), - genesisHash: api.genesisHash - }); + if (api.signer.signPayload) { + const signPayload = new SignerPayload({ + ...eraOptions, + address, + method: _extrinsic.method, + blockNumber: header ? header.blockNumber : 0, + genesisHash: api.genesisHash, + version: api.extrinsicType + }); + const result = await api.signer.signPayload(signPayload.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, result.signature, signPayload.toPayload()); + updateId = result.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.blockNumber : new BN(0), + genesisHash: api.genesisHash + }); + } else { + throw new Error('Invalid signer interface'); + } } else { throw new Error('no signer exists'); } diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 87242e6b8bfe..333f06161ce4 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -208,6 +208,7 @@ export interface ApiOptions { // A smaller interface of ApiRx, used in derive and in SubmittableExtrinsic export interface ApiInterfaceRx { consts: Constants; + extrinsicType: number; genesisHash: Hash; hasSubscriptions: boolean; runtimeMetadata: Metadata; @@ -228,11 +229,76 @@ export interface SignerOptions extends SignatureOptions { genesisHash: Hash; } +export interface SignerPayload { + /** + * @description The ss-58 encoded address + */ + address: string; + + /** + * @description The checkpoint hash of the block, in hex + */ + blockHash: string; + + /** + * @description The checkpoint block number, in hex + */ + blockNumber: string; + + /** + * @description The era for this transaction, in hex + */ + era: string; + + /** + * @description The genesis hash of the chain, in hex + */ + genesisHash: string; + + /** + * @description The encoded method (with arguments) in hex + */ + method: string; + + /** + * @description The nonce for this transaction, in hex + */ + nonce: string; + + /** + * @description The tip for this transaction, in hex + */ + tip: string; + + /** + * @description The version of the extrinsic we are dealing with + */ + version: number; +} + +export interface SignerResult { + /** + * @description The id for this request + */ + id: number; + + /** + * @description The resulting signature in hex + */ + signature: string; +} + export interface Signer { /** + * @deprecated Implement and use signPayload instead * @description Signs an extrinsic, returning an id (>0) that can be used to retrieve updates */ - sign (extrinsic: IExtrinsic, address: string, options: SignerOptions): Promise; + sign?: (extrinsic: IExtrinsic, address: string, options: SignerOptions) => Promise; + + /** + * @description signs an extrinsic payload from a serialized form + */ + signPayload (payload: SignerPayload): Promise; /** * @description Receives an update for the extrinsic signed by a `signer.sign` diff --git a/packages/api/test/e2e/api/promise-tx.spec.ts b/packages/api/test/e2e/api/promise-tx.spec.ts index 2607fe128316..4a7224eae396 100644 --- a/packages/api/test/e2e/api/promise-tx.spec.ts +++ b/packages/api/test/e2e/api/promise-tx.spec.ts @@ -41,6 +41,7 @@ describeE2E({ let api: ApiPromise; beforeEach(async (done): Promise => { + jest.setTimeout(30000); api = await ApiPromise.create(new WsProvider(wsUrl)); done(); @@ -182,7 +183,10 @@ describeE2E({ try { await ex.signAndSend(keyring.alice, { blockHash, era: exERA, nonce } as any); } catch (error) { - expect(error.message).toMatch(/1010: Invalid Transaction \(0\)/); + // NOTE This will fail on any version with v1 Extrinsics, the code returned there + // is simply (0), so it doesn't have an "invalid-era" specific message. (the -127 + // error code is introduced along with the transaction version 2 + expect(error.message).toMatch(/1010: Invalid Transaction \(-127\)/); done(); } } @@ -207,7 +211,8 @@ describeE2E({ try { await ex.signAndSend(keyring.alice.address, { blockHash, era: exERA, nonce } as any); } catch (error) { - expect(error.message).toMatch(/1010: Invalid Transaction \(0\)/); + // NOTE As per above 0 vs -127 note + expect(error.message).toMatch(/1010: Invalid Transaction \(-127\)/); done(); } } diff --git a/packages/api/test/util/SingleAccountSigner.ts b/packages/api/test/util/SingleAccountSigner.ts index a2bc2e6dc3ad..172b8bd7e2d0 100644 --- a/packages/api/test/util/SingleAccountSigner.ts +++ b/packages/api/test/util/SingleAccountSigner.ts @@ -2,14 +2,14 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. +import { Signer, SignerPayload, SignerResult } from '@polkadot/api/types'; import { KeyringPair } from '@polkadot/keyring/types'; -import { SignatureOptions } from '@polkadot/types/types'; -import { Extrinsic } from '@polkadot/types'; +import { SignaturePayload } from '@polkadot/types'; let id = 0; -export default class SingleAccountSigner { +export default class SingleAccountSigner implements Signer { private keyringPair: KeyringPair; private signDelay: number; @@ -19,16 +19,32 @@ export default class SingleAccountSigner { this.signDelay = signDelay; } - public async sign (extrinsic: Extrinsic, address: string, options: SignatureOptions): Promise { - if (!this.keyringPair || String(address) !== this.keyringPair.address) { + // @deprecated Kept here until we have it removed completely + // public async sign (extrinsic: IExtrinsic, address: string, options: SignatureOptions): Promise { + // if (!this.keyringPair || String(address) !== this.keyringPair.address) { + // throw new Error('does not have the keyringPair'); + // } + + // return new Promise((resolve): void => { + // setTimeout((): void => { + // extrinsic.sign(this.keyringPair, options); + + // resolve(++id); + // }, this.signDelay); + // }); + // } + + public async signPayload (payload: SignerPayload): Promise { + if (!this.keyringPair || payload.address !== this.keyringPair.address) { throw new Error('does not have the keyringPair'); } return new Promise((resolve): void => { setTimeout((): void => { - extrinsic.sign(this.keyringPair, options); + const signed = new SignaturePayload(payload, { version: payload.version }).sign(this.keyringPair); + const result: SignerResult = { id: ++id, ...signed }; - resolve(++id); + resolve(result); }, this.signDelay); }); } diff --git a/packages/rpc-provider/src/ws/Provider.ts b/packages/rpc-provider/src/ws/Provider.ts index f9af744040cf..395a38b3b145 100644 --- a/packages/rpc-provider/src/ws/Provider.ts +++ b/packages/rpc-provider/src/ws/Provider.ts @@ -189,7 +189,7 @@ export default class WsProvider implements WSProviderInterface { : resolve(result); }; - l.debug((): any[] => ['calling', method, params, json, !!subscription]); + l.debug((): string[] => ['calling', method, json]); this.handlers[id] = { callback, @@ -249,7 +249,7 @@ export default class WsProvider implements WSProviderInterface { // a slight complication in solving - since we cannot rely on the send id, but rather // need to find the actual subscription id to map it if (isUndefined(this.subscriptions[subscription])) { - l.debug((): any => `Unable to find active subscription=${subscription}`); + l.debug((): string => `Unable to find active subscription=${subscription}`); return false; } diff --git a/packages/types/README.md b/packages/types/README.md index e682d50f2678..7d9f6ef0222d 100644 --- a/packages/types/README.md +++ b/packages/types/README.md @@ -42,6 +42,9 @@ These primitive types are available: | [[Data]] | A raw data structure. It is an encoding of a U8a without any length encoding | | [[Event]] | Wrapper for the actual data that forms part of an [[Event]] | | [[EventRecord]] | A record for an [[Event]] (as specified by [[Metadata]]) with the specific [[Phase]] of application | +| [[Extrinsic]] | Representation of an Extrinsic in the system | +| [[ExtrinsicEra]] | The era for an extrinsic, indicating either a mortal or immortal extrinsic | +| [[ExtrinsicSignature]] | A container for the [[Signature]] associated with a specific [[Extrinsic]] | | [[H160]] | Hash containing 160 bits (20 bytes), typically used in blocks, extrinsics and as a sane default | | [[H256]] | Hash containing 256 bits (32 bytes), typically used in blocks, extrinsics and as a sane default | | [[H512]] | Hash containing 512 bits (64 bytes), typically used for signatures | @@ -57,6 +60,7 @@ These primitive types are available: | [[Null]] | Implements a type that does not contain anything (apart from `null`) | | [[Origin]] | Where Origin occurs, it should be ignored as an internal-only value | | [[Signature]] | The default signature that is used accross the system | +| [[SignaturePayload]] | A signing payload for an [[Extrinsic]]. For the final encoding, it is variable length based on the contents included | | [[StorageData]] | Data retrieved via Storage queries and data for key-value pairs | | [[StorageKey]] | A representation of a storage key (typically hashed) in the system | | [[Text]] | This is a string wrapper, along with the length. | @@ -99,10 +103,7 @@ These custom types implement specific types that are found as part of the Substr | [[ContractStorageKey]] | A representation of a storage key for contracts | | [[EraIndex]] | A representation for the era count | | [[Exposure]] | A snapshot of the stake backing a single validator in the system | -| [[Extrinsic]] | Representation of an Extrinsic in the system | -| [[ExtrinsicEra]] | The era for an extrinsic, indicating either a mortal or immortal extrinsic | | [[Extrinsics]] | A [[Vector]] of [[Extrinsic]] | -| [[ExtrinsicSignature]] | A container for the [[Signature]] associated with a specific [[Extrinsic]] | | [[Gas]] | A gas number type for Substrate, extending [[U64]] | | [[Heartbeat]] | Heartbeat which is send/received. | | [[IndividualExposure]] | The Substrate IndividualExposure for staking | @@ -138,8 +139,6 @@ These custom types implement specific types that are found as part of the Substr | [[SessionKey]] | Wrapper for a SessionKey. Same as an normal [[AuthorityId]], i.e. a wrapper around publicKey | | [[SessionKeys]] | Wrapper for the session and authority ids | | [[SetIndex]] | Set index, implemented as a [[U32]] | -| [[SignaturePayload]] | A signing payload for an [[Extrinsic]]. For the final encoding, it is variable length based on the contents included | -| [[SignaturePayloadRaw]] | A version of the [[SignaturePayload]] where it doesn't rely on [[Method]] with metadata, rather it treats the values as a raw byte stream | | [[StakingLedger]] | The ledger of a (bonded) stash | | [[StoredPendingChange]] | Stored pending change for a Grandpa events | | [[StoredState]] | Current state of the GRANDPA authority set. State transitions must happen in the same order of states defined below, e.g. `Paused` implies a prior `PendingPause` | diff --git a/packages/types/src/codec/Base.ts b/packages/types/src/codec/Base.ts index 7db0cb3b3f73..73d1d24c326c 100644 --- a/packages/types/src/codec/Base.ts +++ b/packages/types/src/codec/Base.ts @@ -2,14 +2,73 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. +import { AnyJson, Codec } from '../types'; + /** * @name Base * @description A type extends the Base class, when it holds a value */ -export default class Base { +export default abstract class Base implements Codec { protected raw: T; public constructor (value?: any) { this.raw = value; } + + /** + * @description The length of the value when encoded as a Uint8Array + */ + public get encodedLength (): number { + return this.toU8a().length; + } + + /** + * @description Checks if the value is an empty value + */ + public get isEmpty (): boolean { + return this.raw.isEmpty; + } + + /** + * @description Compares the value of the input to see if there is a match + */ + public eq (other?: any): boolean { + return this.raw.eq(other); + } + + /** + * @description Returns a hex string representation of the value. isLe returns a LE (number-only) representation + */ + public toHex (isLe?: boolean): string { + return this.raw.toHex(isLe); + } + + /** + * @description Converts the Object to JSON, typically used for RPC transfers + */ + public toJSON (): AnyJson { + return this.raw.toJSON(); + } + + /** + * @description Returns the string representation of the value + */ + public toString (): string { + return this.raw.toString(); + } + + /** + * @description Encodes the value as a Uint8Array as per the SCALE specifications + * @param isBare true when the value has none of the type-specific prefixes (internal) + */ + public toU8a (isBare?: boolean): Uint8Array { + return this.raw.toU8a(isBare); + } + + /** + * @description Returns the base runtime type name for this instance + */ + public toRawType (): string { + return 'Base'; + } } diff --git a/packages/types/src/codec/Compact.ts b/packages/types/src/codec/Compact.ts index 97492a0cd80f..0bffeed30138 100644 --- a/packages/types/src/codec/Compact.ts +++ b/packages/types/src/codec/Compact.ts @@ -7,7 +7,7 @@ import { bnToBn, compactAddLength, compactFromU8a, compactStripLength, compactTo import { DEFAULT_BITLENGTH } from '@polkadot/util/compact/defaults'; import Moment from '../primitive/Moment'; -import { AnyNumber, Codec, Constructor } from '../types'; +import { AnyNumber, Constructor } from '../types'; import { UIntBitLength } from './AbstractInt'; import Base from './Base'; import UInt from './UInt'; @@ -20,7 +20,7 @@ import UInt from './UInt'; * used by other types to add length-prefixed encoding, or in the case of wrapped types, taking * a number and making the compact representation thereof */ -export default class Compact extends Base implements Codec { +export default class Compact extends Base { public constructor (Type: Constructor, value: Compact | AnyNumber = 0) { super(Compact.decodeCompact(Type, value)); } @@ -68,20 +68,6 @@ export default class Compact extends Base implements return new Type(_value); } - /** - * @description The length of the value when encoded as a Uint8Array - */ - public get encodedLength (): number { - return this.toU8a().length; - } - - /** - * @description Checks if the value is an empty value - */ - public get isEmpty (): boolean { - return this.raw.isEmpty; - } - /** * @description Returns the number of bits in the value */ @@ -107,20 +93,6 @@ export default class Compact extends Base implements return this.raw.toBn(); } - /** - * @description Returns a hex string representation of the value - */ - public toHex (isLe?: boolean): any { - return this.raw.toHex(isLe); - } - - /** - * @description Converts the Object to JSON, typically used for RPC transfers - */ - public toJSON (): string | number { - return this.raw.toJSON(); - } - /** * @description Returns the number representation for the value */ @@ -135,13 +107,6 @@ export default class Compact extends Base implements return `Compact<${this.raw.toRawType()}>`; } - /** - * @description Returns the string representation of the value - */ - public toString (): string { - return this.raw.toString(); - } - /** * @description Encodes the value as a Uint8Array as per the SCALE specifications * @param isBare true when the value has none of the type-specific prefixes (internal) diff --git a/packages/types/src/codec/EnumType.ts b/packages/types/src/codec/EnumType.ts index 8b1b97a40814..df266335edcc 100644 --- a/packages/types/src/codec/EnumType.ts +++ b/packages/types/src/codec/EnumType.ts @@ -29,7 +29,7 @@ interface Decoded { // TODO: // - As per Enum, actually use TS enum // - It should rather probably extend Enum instead of copying code -export default class Enum extends Base implements Codec { +export default class Enum extends Base { private _def: TypesDef; private _index: number; @@ -172,13 +172,6 @@ export default class Enum extends Base implements Codec { return this._index; } - /** - * @description Checks if the value is an empty value - */ - public get isEmpty (): boolean { - return this.isEmpty; - } - /** * @description Checks if the Enum points to a [[Null]] type */ diff --git a/packages/types/src/codec/Option.ts b/packages/types/src/codec/Option.ts index b115d06f44c1..00d55c14f4b1 100644 --- a/packages/types/src/codec/Option.ts +++ b/packages/types/src/codec/Option.ts @@ -5,7 +5,7 @@ import { isNull, isU8a, isUndefined, u8aToHex } from '@polkadot/util'; import Base from './Base'; -import { AnyJson, Codec, Constructor } from '../types'; +import { Codec, Constructor } from '../types'; import Null from '../primitive/Null'; /** @@ -16,7 +16,7 @@ import Null from '../primitive/Null'; * implements that - decodes, checks for optionality and wraps the required structure * with a value if/as required/found. */ -export default class Option extends Base implements Codec { +export default class Option extends Base { private _Type: Constructor; public constructor (Type: Constructor, value?: any) { @@ -112,13 +112,6 @@ export default class Option extends Base implements Codec { : u8aToHex(this.toU8a().subarray(1)); } - /** - * @description Converts the Object to JSON, typically used for RPC transfers - */ - public toJSON (): AnyJson { - return this.raw.toJSON(); - } - /** * @description Returns the base runtime type name for this instance */ @@ -126,13 +119,6 @@ export default class Option extends Base implements Codec { return `Option<${new this._Type().toRawType()}>`; } - /** - * @description Returns the string representation of the value - */ - public toString (): string { - return this.raw.toString(); - } - /** * @description Encodes the value as a Uint8Array as per the SCALE specifications * @param isBare true when the value has none of the type-specific prefixes (internal) diff --git a/packages/types/src/codec/Struct.ts b/packages/types/src/codec/Struct.ts index 571fe6b882fc..89c2e083defe 100644 --- a/packages/types/src/codec/Struct.ts +++ b/packages/types/src/codec/Struct.ts @@ -30,7 +30,7 @@ export default class Struct< protected _Types: S; - public constructor (Types: S, value: V | Map | any[] = {} as unknown as V, jsonMap: Map = new Map()) { + public constructor (Types: S, value: V | Map | any[] | string = {} as unknown as V, jsonMap: Map = new Map()) { const decoded = Struct.decodeStruct(Types, value, jsonMap); super( diff --git a/packages/types/src/index.spec.ts b/packages/types/src/index.spec.ts index 5d15a644da16..604bb3237aa3 100644 --- a/packages/types/src/index.spec.ts +++ b/packages/types/src/index.spec.ts @@ -7,7 +7,7 @@ import extrinsics from '@polkadot/api-metadata/extrinsics/static'; import { Codec, Constructor } from './types'; import * as Classes from './index.types'; -const Types = Classes as { [index: string]: Constructor }; +const Types = Classes as Record; const UNCONSTRUCTABLE = ['origin', 'usize', 'vote']; describe('types', (): void => { diff --git a/packages/types/src/primitive/Address.ts b/packages/types/src/primitive/Address.ts index 33a36919a52e..21095fd6c364 100644 --- a/packages/types/src/primitive/Address.ts +++ b/packages/types/src/primitive/Address.ts @@ -2,8 +2,6 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { Codec } from '../types'; - import BN from 'bn.js'; import { hexToU8a, isBn, isHex, isNumber, isU8a, u8aConcat, u8aToHex, u8aToU8a, u8aToBn } from '@polkadot/util'; import { decodeAddress } from '@polkadot/util-crypto'; @@ -24,7 +22,7 @@ export const ACCOUNT_ID_PREFIX = new Uint8Array([0xff]); * we extend from Base with an AccountId/AccountIndex wrapper. Basically the Address * is encoded as `[ , ...publicKey/...bytes ]` as per spec */ -export default class Address extends Base implements Codec { +export default class Address extends Base { public constructor (value: AnyAddress = new Uint8Array()) { super( Address.decodeAddress(value) @@ -77,13 +75,6 @@ export default class Address extends Base implements C ); } - /** - * @description Checks if the value is an empty value - */ - public get isEmpty (): boolean { - return this.raw.isEmpty; - } - /** * @description The length of the raw value, either AccountIndex or AccountId */ @@ -93,13 +84,6 @@ export default class Address extends Base implements C : this.raw.encodedLength; } - /** - * @description Compares the value of the input to see if there is a match - */ - public eq (other?: any): boolean { - return this.raw.eq(other); - } - /** * @description Returns a hex string representation of the value */ @@ -107,20 +91,6 @@ export default class Address extends Base implements C return u8aToHex(this.toU8a()); } - /** - * @description Converts the Object to JSON, typically used for RPC transfers - */ - public toJSON (): string { - return this.raw.toJSON(); - } - - /** - * @description Returns the string representation of the value - */ - public toString (): string { - return this.raw.toString(); - } - /** * @description Returns the base runtime type name for this instance */ diff --git a/packages/types/src/primitive/BalanceCompact.ts b/packages/types/src/primitive/BalanceCompact.ts new file mode 100644 index 000000000000..4f3b043a970c --- /dev/null +++ b/packages/types/src/primitive/BalanceCompact.ts @@ -0,0 +1,15 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import Compact from '../codec/Compact'; +import Balance from './Balance'; + +/** + * @name BalanceCompact + * @description + * The Compact or number of transactions sent by a specific account. Generally used + * with extrinsics to determine the order of execution. + */ +export default class BalanceCompact extends Compact.with(Balance) { +} diff --git a/packages/types/src/primitive/Extrinsic/Extrinsic.spec.ts b/packages/types/src/primitive/Extrinsic/Extrinsic.spec.ts new file mode 100644 index 000000000000..144754a7b935 --- /dev/null +++ b/packages/types/src/primitive/Extrinsic/Extrinsic.spec.ts @@ -0,0 +1,123 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import extrinsics from '@polkadot/api-metadata/extrinsics/static'; + +import { hexToU8a } from '@polkadot/util'; + +import Method from '../Method'; +import Extrinsic from './Extrinsic'; + +describe('Extrinsic', (): void => { + beforeAll((): void => { + Method.injectMethods(extrinsics); + }); + + describe('V1', (): void => { + it('decodes an actual transaction (length prefix)', (): void => { + const extrinsic = new Extrinsic( + '0x' + + '2502' + + '81' + + 'ff' + + 'bfc823aa75c30058eeec21abe2c2d6b7247418a4af89d67a2084c2ac864da080' + + 'c0aa4df3b4926c3cd78bbdced31d8bdccb8604b779b71b90e58b2848df4a9ad6' + + 'b0aa1aae6be7a05c9413a172b0325e4d214e5ff2b25098028b30f1a50be9c90e' + + '0c' + // nonce + '00' + // era + '0500' + // balances.transfer + 'ff' + + '4a83f1c09be797bc3d9adce29818368b276a84e6b545ced492c25c948978d7f8' + + 'e5c0' + ); + + expect(extrinsic.isSigned).toEqual(true); + expect(extrinsic.signer.toU8a()).toEqual(new Uint8Array([255, 191, 200, 35, 170, 117, 195, 0, 88, 238, 236, 33, 171, 226, 194, 214, 183, 36, 116, 24, 164, 175, 137, 214, 122, 32, 132, 194, 172, 134, 77, 160, 128])); + expect(extrinsic.signature.toHex()).toEqual('0xc0aa4df3b4926c3cd78bbdced31d8bdccb8604b779b71b90e58b2848df4a9ad6b0aa1aae6be7a05c9413a172b0325e4d214e5ff2b25098028b30f1a50be9c90e'); + expect(extrinsic.nonce.toNumber()).toEqual(3); + expect(extrinsic.era.toU8a()).toEqual(new Uint8Array([0])); + expect(extrinsic.callIndex).toEqual(new Uint8Array([5, 0])); + expect(`${extrinsic.method.sectionName}.${extrinsic.method.methodName}`).toEqual('balances.transfer'); + expect(extrinsic.args[0].toString()).toEqual('5DkQbYAExs3M2sZgT1Ec3mKfZnAQCL4Dt9beTCknkCUn5jzo'); + }); + + it('decodes an actual transaction (no length prefix, old version)', (): void => { + const extrinsic = new Extrinsic( + '0x' + + '81' + + 'ff' + + 'bfc823aa75c30058eeec21abe2c2d6b7247418a4af89d67a2084c2ac864da080' + + 'c0aa4df3b4926c3cd78bbdced31d8bdccb8604b779b71b90e58b2848df4a9ad6' + + 'b0aa1aae6be7a05c9413a172b0325e4d214e5ff2b25098028b30f1a50be9c90e' + + '0c' + + '00' + + '0500' + // balances.transfer + 'ff' + + '4a83f1c09be797bc3d9adce29818368b276a84e6b545ced492c25c948978d7f8' + + 'e5c0' + ); + + expect(extrinsic.isSigned).toEqual(true); + expect(extrinsic.signer.toU8a()).toEqual(new Uint8Array([255, 191, 200, 35, 170, 117, 195, 0, 88, 238, 236, 33, 171, 226, 194, 214, 183, 36, 116, 24, 164, 175, 137, 214, 122, 32, 132, 194, 172, 134, 77, 160, 128])); + expect(extrinsic.signature.toHex()).toEqual('0xc0aa4df3b4926c3cd78bbdced31d8bdccb8604b779b71b90e58b2848df4a9ad6b0aa1aae6be7a05c9413a172b0325e4d214e5ff2b25098028b30f1a50be9c90e'); + expect(extrinsic.nonce.toNumber()).toEqual(3); + expect(extrinsic.era.toU8a()).toEqual(new Uint8Array([0])); + expect(extrinsic.callIndex).toEqual(new Uint8Array([5, 0])); + expect(`${extrinsic.method.sectionName}.${extrinsic.method.methodName}`).toEqual('balances.transfer'); + expect(extrinsic.args[0].toString()).toEqual('5DkQbYAExs3M2sZgT1Ec3mKfZnAQCL4Dt9beTCknkCUn5jzo'); + }); + }); + + describe('V2', (): void => { + it('decodes an actual transaction (length prefix)', (): void => { + const extrinsic = new Extrinsic( + '0x' + + '2902' + // yes, longer than in v1, 1 byte for the tip + '82' + + 'ff' + + 'bfc823aa75c30058eeec21abe2c2d6b7247418a4af89d67a2084c2ac864da080' + + 'c0aa4df3b4926c3cd78bbdced31d8bdccb8604b779b71b90e58b2848df4a9ad6' + + 'b0aa1aae6be7a05c9413a172b0325e4d214e5ff2b25098028b30f1a50be9c90e' + + '00' + // era + '0c' + // nonce + '08' + // tip + '0500' + // balances.transfer + 'ff' + + '4a83f1c09be797bc3d9adce29818368b276a84e6b545ced492c25c948978d7f8' + + 'e5c0' + ); + + expect(extrinsic.isSigned).toEqual(true); + expect(extrinsic.signer.toU8a()).toEqual(new Uint8Array([255, 191, 200, 35, 170, 117, 195, 0, 88, 238, 236, 33, 171, 226, 194, 214, 183, 36, 116, 24, 164, 175, 137, 214, 122, 32, 132, 194, 172, 134, 77, 160, 128])); + expect(extrinsic.signature.toHex()).toEqual('0xc0aa4df3b4926c3cd78bbdced31d8bdccb8604b779b71b90e58b2848df4a9ad6b0aa1aae6be7a05c9413a172b0325e4d214e5ff2b25098028b30f1a50be9c90e'); + expect(extrinsic.era.toU8a()).toEqual(new Uint8Array([0])); + expect(extrinsic.nonce.toNumber()).toEqual(3); + expect(extrinsic.tip.toNumber()).toEqual(2); + expect(extrinsic.callIndex).toEqual(new Uint8Array([5, 0])); + expect(`${extrinsic.method.sectionName}.${extrinsic.method.methodName}`).toEqual('balances.transfer'); + expect(extrinsic.args[0].toString()).toEqual('5DkQbYAExs3M2sZgT1Ec3mKfZnAQCL4Dt9beTCknkCUn5jzo'); + }); + + it('constructs from itself', (): void => { + const input = '0x' + + '2902' + + '82' + + 'ff' + + 'bfc823aa75c30058eeec21abe2c2d6b7247418a4af89d67a2084c2ac864da080' + + 'c0aa4df3b4926c3cd78bbdced31d8bdccb8604b779b71b90e58b2848df4a9ad6' + + 'b0aa1aae6be7a05c9413a172b0325e4d214e5ff2b25098028b30f1a50be9c90e' + + '00' + // era + '00' + // nonce + '00' + // tip + '0500' + // balances.transfer + 'ff' + + '4a83f1c09be797bc3d9adce29818368b276a84e6b545ced492c25c948978d7f8' + + 'e5c0'; + + expect( + new Extrinsic(new Extrinsic(input)).toU8a() + ).toEqual(hexToU8a(input)); + }); + }); +}); diff --git a/packages/types/src/primitive/Extrinsic/Extrinsic.ts b/packages/types/src/primitive/Extrinsic/Extrinsic.ts new file mode 100644 index 000000000000..a74796536455 --- /dev/null +++ b/packages/types/src/primitive/Extrinsic/Extrinsic.ts @@ -0,0 +1,295 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { AnyU8a, ArgsDef, Codec, ExtrinsicPayloadValue, IExtrinsic, IHash, IKeyringPair, SignatureOptions } from '../../types'; + +import { assert, isHex, isU8a, u8aConcat, u8aToHex, u8aToU8a } from '@polkadot/util'; +import { blake2AsU8a } from '@polkadot/util-crypto'; + +import Base from '../../codec/Base'; +import Compact from '../../codec/Compact'; +import { FunctionMetadata } from '../../Metadata/v6/Calls'; +import NonceCompact from '../../type/NonceCompact'; +import Address from '../Address'; +import BalanceCompact from '../BalanceCompact'; +import Method from '../Method'; +import Hash from '../Hash'; +import ExtrinsicV1, { ExtrinsicValueV1 } from './v1/Extrinsic'; +import ExtrinsicV2, { ExtrinsicValueV2 } from './v2/Extrinsic'; +import ExtrinsicEra from './ExtrinsicEra'; +import { BIT_SIGNED, BIT_UNSIGNED, DEFAULT_VERSION, UNMASK_VERSION } from './constants'; + +type ExtrinsicValue = ExtrinsicValueV1 | ExtrinsicValueV2; + +interface ExtrinsicOptions { + version?: number; +} + +/** + * @name Extrinsic + * @description + * Representation of an Extrinsic in the system. It contains the actual call, + * (optional) signature and encodes with an actual length prefix + * + * {@link https://github.com/paritytech/wiki/blob/master/Extrinsic.md#the-extrinsic-format-for-node}. + * + * Can be: + * - signed, to create a transaction + * - left as is, to create an inherent + */ +export default class Extrinsic extends Base implements IExtrinsic { + public constructor (value: Extrinsic | ExtrinsicValue | AnyU8a | Method | undefined, { version }: ExtrinsicOptions = {}) { + super(Extrinsic.decodeExtrinsic(value, version)); + } + + private static newFromValue (value: any, version: number): ExtrinsicV1 | ExtrinsicV2 { + if (value instanceof Extrinsic) { + return value.raw; + } + + const isSigned = (version & BIT_SIGNED) === BIT_SIGNED; + const type = version & UNMASK_VERSION; + + switch (type) { + case 1: return new ExtrinsicV1(value, { isSigned }); + case 2: return new ExtrinsicV2(value, { isSigned }); + default: throw new Error(`Unsupported extrinsic version ${type}`); + } + } + + public static decodeExtrinsic (value: Extrinsic | ExtrinsicValue | AnyU8a | Method | undefined, version: number = DEFAULT_VERSION): ExtrinsicV1 | ExtrinsicV2 { + if (Array.isArray(value) || isHex(value)) { + // Instead of the block below, it should simply be: + // return Extrinsic.decodeExtrinsic(hexToU8a(value as string)); + const u8a = u8aToU8a(value); + + // HACK 11 Jan 2019 - before https://github.com/paritytech/substrate/pull/1388 + // extrinsics didn't have the length, cater for both approaches + const [offset, length] = Compact.decodeU8a(u8a); + const withPrefix = u8a.length === (offset + length.toNumber()); + + return Extrinsic.decodeExtrinsic( + withPrefix + ? u8a + : Compact.addLengthPrefix(u8a), + version + ); + } else if (isU8a(value)) { + if (!value.length) { + return Extrinsic.newFromValue(new Uint8Array(), version); + } + + const [offset, length] = Compact.decodeU8a(value); + const total = offset + length.toNumber(); + + assert(total <= value.length, `Extrinsic: required length less than remainder, expected at least ${total}, found ${value.length}`); + + return Extrinsic.decodeU8a(value.subarray(offset, total)); + } else if (value instanceof Method) { + return Extrinsic.newFromValue({ method: value }, version); + } + + return Extrinsic.newFromValue(value, version); + } + + private static decodeU8a (value: Uint8Array): ExtrinsicV1 | ExtrinsicV2 { + return Extrinsic.newFromValue(value.subarray(1), value[0]); + } + + /** + * @description The arguments passed to for the call, exposes args so it is compatible with [[Method]] + */ + public get args (): Codec[] { + return this.method.args; + } + + /** + * @description Thge argument defintions, compatible with [[Method]] + */ + public get argsDef (): ArgsDef { + return this.method.argsDef; + } + + /** + * @description The actual `[sectionIndex, methodIndex]` as used in the Method + */ + public get callIndex (): Uint8Array { + return this.method.callIndex; + } + + /** + * @description The actual data for the Method + */ + public get data (): Uint8Array { + return this.method.data; + } + + /** + * @description The era for thios extrinsic + */ + public get era (): ExtrinsicEra { + return this.raw.signature.era; + } + + /** + * @description The length of the value when encoded as a Uint8Array + */ + public get encodedLength (): number { + return this.toU8a().length; + } + + /** + * @description Convernience function, encodes the extrinsic and returns the actual hash + */ + public get hash (): Hash { + return new Hash( + blake2AsU8a(this.toU8a(), 256) + ); + } + + /** + * @description `true` is method has `Origin` argument (compatibility with [[Method]]) + */ + public get hasOrigin (): boolean { + return this.method.hasOrigin; + } + + /** + * @description `true` id the extrinsic is signed + */ + public get isSigned (): boolean { + return this.raw.signature.isSigned; + } + + /** + * @description The length of the actual data, excluding prefix + */ + public get length (): number { + return this.toU8a(true).length; + } + + /** + * @description The [[FunctionMetadata]] that describes the extrinsic + */ + public get meta (): FunctionMetadata { + return this.method.meta; + } + + /** + * @description The [[Method]] this extrinsic wraps + */ + public get method (): Method { + return this.raw.method; + } + + /** + * @description The nonce for this extrinsic + */ + public get nonce (): NonceCompact { + return this.raw.signature.nonce; + } + + /** + * @description The [[ExtrinsicSignature]] + */ + public get signature (): IHash { + return this.raw.signature.signature; + } + + /** + * @description The [[Address]] that signed + */ + public get signer (): Address { + return this.raw.signature.signer; + } + + /** + * @description Forwards compat + */ + public get tip (): BalanceCompact { + return this.raw.signature.tip; + } + + /** + * @description Returns the raw transaction version (not flagged with signing information) + */ + public get type (): number { + return this.raw.version; + } + + /** + * @description Returns the encoded version flag + */ + public get version (): number { + return this.type | (this.isSigned ? BIT_SIGNED : BIT_UNSIGNED); + } + + /** + * @description Add an [[ExtrinsicSignature]] to the extrinsic (already generated) + */ + public addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | string, ...args: [ExtrinsicPayloadValue | Uint8Array | string]): Extrinsic { + // FIXME Support for current extensions where 2 values are being passed in here, i.e. + // addSignature(signer, signature, nonce, era); + // The above signature should be changed to the correct format in the next cycle, i.e. + // payload: ExtrinsicPayloadValue | Uint8Array | string + let payload = args[0]; + + // @ts-ignore + if (args.length === 2) { + payload = { + // @ts-ignore + era: args[1] as string, + method: this.method.toHex(), + nonce: args[0] as string, + tip: 0 + }; + } + + this.raw.addSignature(signer, signature, payload); + + return this; + } + + /** + * @description Sign the extrinsic with a specific keypair + */ + public sign (account: IKeyringPair, options: SignatureOptions): Extrinsic { + this.raw.sign(account, options); + + return this; + } + + /** + * @description Returns a hex string representation of the value + */ + public toHex (): string { + return u8aToHex(this.toU8a()); + } + + /** + * @description Converts the Object to JSON, typically used for RPC transfers + */ + public toJSON (): string { + return this.toHex(); + } + + /** + * @description Returns the base runtime type name for this instance + */ + public toRawType (): string { + return 'Extrinsic'; + } + + /** + * @description Encodes the value as a Uint8Array as per the SCALE specifications + * @param isBare true when the value has none of the type-specific prefixes (internal) + */ + public toU8a (isBare?: boolean): Uint8Array { + const encoded = u8aConcat(new Uint8Array([this.version]), this.raw.toU8a(isBare)); + + return isBare + ? encoded + : Compact.addLengthPrefix(encoded); + } +} diff --git a/packages/types/src/type/ExtrinsicEra.spec.ts b/packages/types/src/primitive/Extrinsic/ExtrinsicEra.spec.ts similarity index 100% rename from packages/types/src/type/ExtrinsicEra.spec.ts rename to packages/types/src/primitive/Extrinsic/ExtrinsicEra.spec.ts diff --git a/packages/types/src/type/ExtrinsicEra.ts b/packages/types/src/primitive/Extrinsic/ExtrinsicEra.ts similarity index 95% rename from packages/types/src/type/ExtrinsicEra.ts rename to packages/types/src/primitive/Extrinsic/ExtrinsicEra.ts index 73909f26d3e3..2994645953e0 100644 --- a/packages/types/src/type/ExtrinsicEra.ts +++ b/packages/types/src/primitive/Extrinsic/ExtrinsicEra.ts @@ -2,15 +2,16 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { AnyU8a, IExtrinsicEra } from '../types'; +import { AnyU8a, IExtrinsicEra } from '../../types'; import BN from 'bn.js'; import { assert, bnToBn, hexToU8a, isHex, isU8a, isObject, u8aToBn } from '@polkadot/util'; -import Enum from '../codec/Enum'; -import Tuple from '../codec/Tuple'; -import U8a from '../codec/U8a'; -import U64 from '../primitive/U64'; +import Enum from '../../codec/Enum'; +import Tuple from '../../codec/Tuple'; +import U8a from '../../codec/U8a'; +import U64 from '../U64'; +import { IMMORTAL_ERA } from './constants'; type MortalEraValue = [U64, U64]; @@ -27,8 +28,6 @@ interface ImmortalEnumDef { ImmortalEra: string; } -const VALID_IMMORTAL = new U8a([0]); - /** * @name ImmortalEra * @description @@ -39,7 +38,7 @@ export class ImmortalEra extends U8a { public constructor (value?: AnyU8a) { // For immortals, we always provide the known value (i.e. treated as a // constant no matter how it is constructed - it is a fixed structure) - super(VALID_IMMORTAL); + super(IMMORTAL_ERA); } } @@ -193,13 +192,15 @@ export default class ExtrinsicEra extends Enum implements IExtrinsicEra { // eslint-disable-next-line @typescript-eslint/ban-types private static decodeExtrinsicEra (value: IExtrinsicEra | MortalMethod | MortalEnumDef | ImmortalEnumDef | Uint8Array | string = new Uint8Array()): Uint8Array | Object | undefined { - if (value instanceof ExtrinsicEra) { + if (!value) { + return new Uint8Array([0]); + } else if (value instanceof ExtrinsicEra) { return ExtrinsicEra.decodeExtrinsicEra(value.toU8a()); } else if (isHex(value)) { return ExtrinsicEra.decodeExtrinsicEra(hexToU8a(value)); } else if (isU8a(value)) { if (!value.length || value[0] === 0) { - return new Uint8Array([0, 0]); + return new Uint8Array([0]); } else { return new Uint8Array([1, value[0], value[1]]); } diff --git a/packages/types/src/primitive/Extrinsic/SignaturePayload.ts b/packages/types/src/primitive/Extrinsic/SignaturePayload.ts new file mode 100644 index 000000000000..975519028a0a --- /dev/null +++ b/packages/types/src/primitive/Extrinsic/SignaturePayload.ts @@ -0,0 +1,118 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { IKeyringPair } from '../../types'; + +import { u8aToHex } from '@polkadot/util'; + +import Base from '../../codec/Base'; +import U8a from '../../codec/U8a'; +import NonceCompact from '../../type/NonceCompact'; +import BalanceCompact from '../BalanceCompact'; +import Hash from '../Hash'; +import SignaturePayloadV1, { SignaturePayloadValueV1 } from './v1/SignaturePayload'; +import SignaturePayloadV2, { SignaturePayloadValueV2 } from './v2/SignaturePayload'; +import ExtrinsicEra from './ExtrinsicEra'; +import { DEFAULT_VERSION } from './constants'; + +interface SignaturePayloadOptions { + version?: number; +} + +/** + * @name SignaturePayload + * @description + * A signing payload for an [[Extrinsic]]. For the final encoding, it is variable length based + * on the contents included + */ +export default class SignaturePayload extends Base { + public constructor (value: SignaturePayloadValueV1 | SignaturePayloadValueV2 | Uint8Array | string | undefined, { version }: SignaturePayloadOptions = {}) { + super( + SignaturePayload.decodeSignaturePayload(value, version) + ); + } + + public static decodeSignaturePayload (value: SignaturePayload | SignaturePayloadValueV1 | SignaturePayloadValueV2 | Uint8Array | string | undefined, version: number = DEFAULT_VERSION): SignaturePayloadV1 | SignaturePayloadV2 { + if (value instanceof SignaturePayload) { + return value.raw; + } + + switch (version) { + case 1: return new SignaturePayloadV1(value as Uint8Array); + case 2: return new SignaturePayloadV2(value as Uint8Array); + default: throw new Error(`Unsupported extrinsic version ${version}`); + } + } + + /** + * @description The block [[Hash]] the signature applies to (mortal/immortal) + */ + public get blockHash (): Hash { + return this.raw.blockHash; + } + + /** + * @description The [[U8a]] contained in the payload + */ + public get method (): U8a { + return this.raw.method; + } + + /** + * @description The [[ExtrinsicEra]] + */ + public get era (): ExtrinsicEra { + return this.raw.era; + } + + /** + * @description The [[NonceCompact]] + */ + public get nonce (): NonceCompact { + return this.raw.nonce; + } + + /** + * @description The [[BalanceCompact]] + */ + public get tip (): BalanceCompact { + return (this.raw as SignaturePayloadV2).tip || new BalanceCompact(0); + } + + /** + * @description Compares the value of the input to see if there is a match + */ + public eq (other?: any): boolean { + return this.raw.eq(other); + } + + /** + * @description Sign the payload with the keypair + */ + public sign (signerPair: IKeyringPair): { signature: string } { + const signature = this.raw.sign(signerPair); + + // This is extensible, so we could quite readily extend to send back extra + // information, such as for instance the payload, i.e. `payload: this.toHex()` + // For the case here we sign via the extrinsic, we ignore the return, so generally + // thisis applicable for external signing + return { + signature: u8aToHex(signature) + }; + } + + /** + * @description Converts the Object to JSON, typically used for RPC transfers + */ + public toJSON (): any { + return this.toHex(); + } + + /** + * @description Returns the string representation of the value + */ + public toString (): string { + return this.toHex(); + } +} diff --git a/packages/types/src/primitive/Extrinsic/constants.ts b/packages/types/src/primitive/Extrinsic/constants.ts new file mode 100644 index 000000000000..1dd2eb27789f --- /dev/null +++ b/packages/types/src/primitive/Extrinsic/constants.ts @@ -0,0 +1,17 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +export const BIT_SIGNED = 0b10000000; + +export const BIT_UNSIGNED = 0; + +export const EMPTY_U8A = new Uint8Array(); + +// TODO We really want to swap this to V2, however all the test data is setup +// for V1, so this will take some time to convert... "some" time :) +export const DEFAULT_VERSION = 1; + +export const IMMORTAL_ERA = new Uint8Array([0]); + +export const UNMASK_VERSION = 0b01111111; diff --git a/packages/types/src/primitive/Extrinsic/util.ts b/packages/types/src/primitive/Extrinsic/util.ts new file mode 100644 index 000000000000..773117efeb69 --- /dev/null +++ b/packages/types/src/primitive/Extrinsic/util.ts @@ -0,0 +1,16 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { IKeyringPair } from '../../types'; + +import { blake2AsU8a } from '@polkadot/util-crypto'; + +// a helper function for both types of payloads, Raw and metadata-known +export function sign (signerPair: IKeyringPair, u8a: Uint8Array): Uint8Array { + const encoded = u8a.length > 256 + ? blake2AsU8a(u8a) + : u8a; + + return signerPair.sign(encoded); +} diff --git a/packages/types/src/primitive/Extrinsic/v1/Extrinsic.ts b/packages/types/src/primitive/Extrinsic/v1/Extrinsic.ts new file mode 100644 index 000000000000..53e2dcd09d24 --- /dev/null +++ b/packages/types/src/primitive/Extrinsic/v1/Extrinsic.ts @@ -0,0 +1,102 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { ExtrinsicPayloadValue, IExtrinsicImpl, IKeyringPair, SignatureOptions } from '../../../types'; + +import { isU8a } from '@polkadot/util'; + +import Struct from '../../../codec/Struct'; +import Method from '../../Method'; +import Address from '../../Address'; +import ExtrinsicSignature from './ExtrinsicSignature'; + +export interface ExtrinsicValueV1 { + method?: Method; + signature?: ExtrinsicSignature; +} + +interface ExtrinsicV1Options { + isSigned?: boolean; +} + +const TRANSACTION_VERSION = 1; + +/** + * @name ExtrinsicV1 + * @description + * The first generation of compact extrinsics + */ +export default class ExtrinsicV1 extends Struct implements IExtrinsicImpl { + public constructor (value?: Uint8Array | ExtrinsicValueV1, { isSigned }: ExtrinsicV1Options = {}) { + super({ + signature: ExtrinsicSignature, + method: Method + }, ExtrinsicV1.decodeExtrinsic(value, isSigned)); + } + + public static decodeExtrinsic (value?: Uint8Array | ExtrinsicValueV1, isSigned: boolean = false): ExtrinsicValueV1 { + if (!value) { + return {}; + } else if (value instanceof ExtrinsicV1) { + return value; + } else if (isU8a(value)) { + // here we decode manually since we need to pull through the version information + const signature = new ExtrinsicSignature(value, { isSigned }); + const method = new Method(value.subarray(signature.encodedLength)); + + return { + method, + signature + }; + } + + return value; + } + + /** + * @description The length of the value when encoded as a Uint8Array + */ + public get encodedLength (): number { + return this.toU8a().length; + } + + /** + * @description The [[Method]] this extrinsic wraps + */ + public get method (): Method { + return this.get('method') as Method; + } + + /** + * @description The [[ExtrinsicSignature]] + */ + public get signature (): ExtrinsicSignature { + return this.get('signature') as ExtrinsicSignature; + } + + /** + * @description The version for the signature + */ + public get version (): number { + return TRANSACTION_VERSION; + } + + /** + * @description Add an [[ExtrinsicSignature]] to the extrinsic (already generated) + */ + public addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | string, payload: ExtrinsicPayloadValue | Uint8Array | string): ExtrinsicV1 { + this.signature.addSignature(signer, signature, payload); + + return this; + } + + /** + * @description Sign the extrinsic with a specific keypair + */ + public sign (account: IKeyringPair, options: SignatureOptions): ExtrinsicV1 { + this.signature.sign(this.method, account, options); + + return this; + } +} diff --git a/packages/types/src/primitive/Extrinsic/v1/ExtrinsicSignature.ts b/packages/types/src/primitive/Extrinsic/v1/ExtrinsicSignature.ts new file mode 100644 index 000000000000..147f4474a2ab --- /dev/null +++ b/packages/types/src/primitive/Extrinsic/v1/ExtrinsicSignature.ts @@ -0,0 +1,140 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { ExtrinsicPayloadValue, IExtrinsicSignature, IKeyringPair, SignatureOptions } from '../../../types'; + +import Struct from '../../../codec/Struct'; +import Address from '../../Address'; +import BalanceCompact from '../../BalanceCompact'; +import Method from '../../Method'; +import Signature from '../../Signature'; +import NonceCompact from '../../../type/NonceCompact'; +import ExtrinsicEra from '../ExtrinsicEra'; +import { EMPTY_U8A, IMMORTAL_ERA } from '../constants'; +import SignaturePayload from './SignaturePayload'; + +interface ExtrinsicSignatureV1Options { + isSigned?: boolean; +} + +/** + * @name ExtrinsicSignature + * @description + * A container for the [[Signature]] associated with a specific [[Extrinsic]] + */ +export default class ExtrinsicSignatureV1 extends Struct implements IExtrinsicSignature { + // Signature Information. + // 1/3/5/9/33 bytes: The signing account identity, in Address format + // 64 bytes: The sr25519/ed25519 signature of the Signing Payload + // 1-8 bytes: The Compact of the signing account + // 1/2 bytes: The Transaction Era + public constructor (value?: ExtrinsicSignatureV1 | Uint8Array, { isSigned }: ExtrinsicSignatureV1Options = {}) { + super({ + signer: Address, + signature: Signature, + nonce: NonceCompact, + era: ExtrinsicEra + }, ExtrinsicSignatureV1.decodeExtrinsicSignature(value, isSigned)); + } + + public static decodeExtrinsicSignature (value: ExtrinsicSignatureV1 | Uint8Array | undefined, isSigned: boolean = false): ExtrinsicSignatureV1 | Uint8Array { + if (!value) { + return EMPTY_U8A; + } else if (value instanceof ExtrinsicSignatureV1) { + return value; + } + + return isSigned + ? value + : EMPTY_U8A; + } + + /** + * @description `true` if the signature is valid + */ + public get isSigned (): boolean { + return !this.signature.isEmpty; + } + + /** + * @description The [[ExtrinsicEra]] (mortal or immortal) this signature applies to + */ + public get era (): ExtrinsicEra { + return this.get('era') as ExtrinsicEra; + } + + /** + * @description The [[Nonce]] for the signature + */ + public get nonce (): NonceCompact { + return this.get('nonce') as NonceCompact; + } + + /** + * @description The actuall [[Signature]] hash + */ + public get signature (): Signature { + return this.get('signature') as Signature; + } + + /** + * @description The [[Address]] that signed + */ + public get signer (): Address { + return this.get('signer') as Address; + } + + /** + * @description Forwards compat + */ + public get tip (): BalanceCompact { + return new BalanceCompact(0); + } + + private injectSignature (signer: Address, signature: Signature, { era, nonce }: SignaturePayload): IExtrinsicSignature { + this.set('era', era); + this.set('nonce', nonce); + this.set('signer', signer); + this.set('signature', signature); + + return this; + } + + /** + * @description Adds a raw signature + */ + public addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | string, payload: ExtrinsicPayloadValue | Uint8Array | string): IExtrinsicSignature { + return this.injectSignature( + new Address(signer), + new Signature(signature), + new SignaturePayload(payload) + ); + } + + /** + * @description Generate a payload and pplies the signature from a keypair + */ + public sign (method: Method, account: IKeyringPair, { blockHash, era, nonce }: SignatureOptions): IExtrinsicSignature { + const signer = new Address(account.publicKey); + const payload = new SignaturePayload({ + nonce, + method: method.toU8a(), + era: era || IMMORTAL_ERA, + blockHash + }); + const signature = new Signature(payload.sign(account)); + + return this.injectSignature(signer, signature, payload); + } + + /** + * @description Encodes the value as a Uint8Array as per the SCALE specifications + * @param isBare true when the value has none of the type-specific prefixes (internal) + */ + public toU8a (isBare?: boolean): Uint8Array { + return this.isSigned + ? super.toU8a(isBare) + : new Uint8Array(); + } +} diff --git a/packages/types/src/primitive/Extrinsic/v1/SignaturePayload.ts b/packages/types/src/primitive/Extrinsic/v1/SignaturePayload.ts new file mode 100644 index 000000000000..136e65c8027e --- /dev/null +++ b/packages/types/src/primitive/Extrinsic/v1/SignaturePayload.ts @@ -0,0 +1,77 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { AnyNumber, AnyU8a, ExtrinsicPayloadValue, IExtrinsicEra, IKeyringPair, IMethod } from '../../../types'; + +import Struct from '../../../codec/Struct'; +import U8a from '../../../codec/U8a'; +import Hash from '../../Hash'; +import NonceCompact from '../../../type/NonceCompact'; +import ExtrinsicEra from '../ExtrinsicEra'; +import { sign } from '../util'; + +export interface SignaturePayloadValueV1 { + blockHash: AnyU8a; + era: AnyU8a | IExtrinsicEra; + method: AnyU8a | IMethod; + nonce: AnyNumber; +} + +/** + * @name SignaturePayloadV1 + * @description + * A signing payload for an [[Extrinsic]]. For the final encoding, it is variable length based + * on the contents included + * + * 1-8 bytes: The Transaction Compact as provided in the transaction itself. + * 2+ bytes: The Function Descriptor as provided in the transaction itself. + * 1/2 bytes: The Transaction Era as provided in the transaction itself. + * 32 bytes: The hash of the authoring block implied by the Transaction Era and the current block. + */ +export default class SignaturePayloadV1 extends Struct { + public constructor (value?: ExtrinsicPayloadValue | SignaturePayloadValueV1 | Uint8Array | string) { + super({ + nonce: NonceCompact, + method: U8a, + era: ExtrinsicEra, + blockHash: Hash + }, value); + } + + /** + * @description The block [[Hash]] the signature applies to (mortal/immortal) + */ + public get blockHash (): Hash { + return this.get('blockHash') as Hash; + } + + /** + * @description The [[U8a]] contained in the payload + */ + public get method (): U8a { + return this.get('method') as U8a; + } + + /** + * @description The [[ExtrinsicEra]] + */ + public get era (): ExtrinsicEra { + return this.get('era') as ExtrinsicEra; + } + + /** + * @description The [[NonceCompact]] + */ + public get nonce (): NonceCompact { + return this.get('nonce') as NonceCompact; + } + + /** + * @description Sign the payload with the keypair + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public sign (signerPair: IKeyringPair): Uint8Array { + return sign(signerPair, this.toU8a()); + } +} diff --git a/packages/types/src/primitive/Extrinsic/v2/Extrinsic.spec.ts b/packages/types/src/primitive/Extrinsic/v2/Extrinsic.spec.ts new file mode 100644 index 000000000000..0514c9da339b --- /dev/null +++ b/packages/types/src/primitive/Extrinsic/v2/Extrinsic.spec.ts @@ -0,0 +1,60 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import extrinsics from '@polkadot/api-metadata/extrinsics/static'; +import testingPairs from '@polkadot/keyring/testingPairs'; + +import Method from '../../Method'; +import Extrinsic from './Extrinsic'; + +const keyring = testingPairs({ type: 'ed25519' }, false); + +describe('ExtrinsicV2', (): void => { + beforeEach((): void => { + Method.injectMethods(extrinsics); + }); + + it('constructs a sane Uint8Array (default)', (): void => { + expect( + new Extrinsic().toU8a() + ).toEqual(new Uint8Array([0, 0])); + }); + + it('creates a unsigned extrinsic', (): void => { + expect( + new Extrinsic( + extrinsics.balances.transfer(keyring.bob.publicKey, 6969) + ).toHex() + ).toEqual( + '0x' + + '0500' + // balance.transfer + 'ff' + + 'd7568e5f0a7eda67a82691ff379ac4bba4f9c9b859fe779b5d46363b61ad2db9' + + 'e56c' + ); + }); + + it('creates a signed extrinsic', (): void => { + expect( + new Extrinsic( + extrinsics.balances.transfer(keyring.bob.publicKey, 6969) + ).sign(keyring.alice, { + blockHash: '0xec7afaf1cca720ce88c1d1b689d81f0583cc15a97d621cf046dd9abf605ef22f', + nonce: 1, + tip: 2 + }).toHex() + ).toEqual( + '0x' + + 'ff' + + 'd172a74cda4c865912c32ba0a80a57ae69abae410e5ccb59dee84e2f4432db4f' + + '975c79b3c7c57898153faa55a97744f71aa8b4c44ec319978c1206f7941db65e' + + '6cea25fe0bc700f8cb3c5eeef3894b21ed88398e8d8ab93b6af2e9d2fd9f5404' + + '000408' + // era. nonce, tip + '0500' + + 'ff' + + 'd7568e5f0a7eda67a82691ff379ac4bba4f9c9b859fe779b5d46363b61ad2db9' + + 'e56c' + ); + }); +}); diff --git a/packages/types/src/primitive/Extrinsic/v2/Extrinsic.ts b/packages/types/src/primitive/Extrinsic/v2/Extrinsic.ts new file mode 100644 index 000000000000..e03a3b16871b --- /dev/null +++ b/packages/types/src/primitive/Extrinsic/v2/Extrinsic.ts @@ -0,0 +1,104 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { ExtrinsicPayloadValue, IExtrinsicImpl, IKeyringPair, SignatureOptions } from '../../../types'; + +import { isU8a } from '@polkadot/util'; + +import Struct from '../../../codec/Struct'; +import Method from '../../Method'; +import Address from '../../Address'; +import ExtrinsicSignature from './ExtrinsicSignature'; + +const TRANSACTION_VERSION = 2; + +export interface ExtrinsicValueV2 { + method?: Method; + signature?: ExtrinsicSignature; +} + +interface ExtrinsicV2Options { + isSigned?: boolean; +} + +/** + * @name ExtrinsicV1 + * @description + * The first generation of compact extrinsics + */ +export default class ExtrinsicV2 extends Struct implements IExtrinsicImpl { + public constructor (value?: Uint8Array | ExtrinsicValueV2 | Method, { isSigned }: ExtrinsicV2Options = {}) { + super({ + signature: ExtrinsicSignature, + method: Method + }, ExtrinsicV2.decodeExtrinsic(value, isSigned)); + } + + public static decodeExtrinsic (value?: Method | Uint8Array | ExtrinsicValueV2, isSigned: boolean = false): ExtrinsicValueV2 { + if (!value) { + return {}; + } else if (value instanceof ExtrinsicV2) { + return value; + } else if (value instanceof Method) { + return { method: value }; + } else if (isU8a(value)) { + // here we decode manually since we need to pull through the version information + const signature = new ExtrinsicSignature(value, { isSigned }); + const method = new Method(value.subarray(signature.encodedLength)); + + return { + method, + signature + }; + } + + return value; + } + + /** + * @description The length of the value when encoded as a Uint8Array + */ + public get encodedLength (): number { + return this.toU8a().length; + } + + /** + * @description The [[Method]] this extrinsic wraps + */ + public get method (): Method { + return this.get('method') as Method; + } + + /** + * @description The [[ExtrinsicSignature]] + */ + public get signature (): ExtrinsicSignature { + return this.get('signature') as ExtrinsicSignature; + } + + /** + * @description The version for the signature + */ + public get version (): number { + return TRANSACTION_VERSION; + } + + /** + * @description Add an [[ExtrinsicSignature]] to the extrinsic (already generated) + */ + public addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | string, payload: ExtrinsicPayloadValue | Uint8Array | string): ExtrinsicV2 { + this.signature.addSignature(signer, signature, payload); + + return this; + } + + /** + * @description Sign the extrinsic with a specific keypair + */ + public sign (account: IKeyringPair, options: SignatureOptions): ExtrinsicV2 { + this.signature.sign(this.method, account, options); + + return this; + } +} diff --git a/packages/types/src/primitive/Extrinsic/v2/ExtrinsicExtra.spec.ts b/packages/types/src/primitive/Extrinsic/v2/ExtrinsicExtra.spec.ts new file mode 100644 index 000000000000..38a24d667476 --- /dev/null +++ b/packages/types/src/primitive/Extrinsic/v2/ExtrinsicExtra.spec.ts @@ -0,0 +1,25 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import ExtrinsicExtra from './ExtrinsicExtra'; + +describe('ExtrinsicExtraV2', (): void => { + it('encodes to a sane Uint8Array (default construction)', (): void => { + expect( + new ExtrinsicExtra().toU8a() + ).toEqual(new Uint8Array([0, 0, 0])); + }); + + it('encodes to a sane Uint8Array (struct in)', (): void => { + expect( + new ExtrinsicExtra({ nonce: 1, tip: 2 }).toU8a() + ).toEqual(new Uint8Array([0, 4, 8])); // numbers are Compact + }); + + it('encodes to a sane Uint8Array (Uint8Array in)', (): void => { + expect( + new ExtrinsicExtra(new Uint8Array([0, 8, 4])).toU8a() + ).toEqual(new Uint8Array([0, 8, 4])); + }); +}); diff --git a/packages/types/src/primitive/Extrinsic/v2/ExtrinsicExtra.ts b/packages/types/src/primitive/Extrinsic/v2/ExtrinsicExtra.ts new file mode 100644 index 000000000000..cbf29deb75bf --- /dev/null +++ b/packages/types/src/primitive/Extrinsic/v2/ExtrinsicExtra.ts @@ -0,0 +1,62 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { AnyNumber } from '../../../types'; + +import Struct from '../../../codec/Struct'; +import Address from '../../Address'; +import BalanceCompact from '../../BalanceCompact'; +import ExtrinsicEra from '../ExtrinsicEra'; +import NonceCompact from '../../../type/NonceCompact'; + +interface ExtrinsicExtraValueV2 { + era?: Uint8Array; + nonce?: AnyNumber; + tip?: AnyNumber; +} + +export const extraDefinition = { + era: ExtrinsicEra, + nonce: NonceCompact, + tip: BalanceCompact +}; + +/** + * @name ExtrinsicExtraV2 + * @description + * A container for the extra information in an extrinsic + */ +export default class ExtrinsicExtraV2 extends Struct { + public constructor (value?: ExtrinsicExtraValueV2 | Uint8Array) { + super(extraDefinition, value); + } + + /** + * @description The [[ExtrinsicEra]] (mortal or immortal) this signature applies to + */ + public get era (): ExtrinsicEra { + return this.get('era') as ExtrinsicEra; + } + + /** + * @description The [[NonceCompact]] for the signature + */ + public get nonce (): NonceCompact { + return this.get('nonce') as NonceCompact; + } + + /** + * @description The [[Address]] that signed + */ + public get signer (): Address { + return this.get('signer') as Address; + } + + /** + * @description The [[BalanceCompact]] tip + */ + public get tip (): BalanceCompact { + return this.get('tip') as BalanceCompact; + } +} diff --git a/packages/types/src/primitive/Extrinsic/v2/ExtrinsicSignature.spec.ts b/packages/types/src/primitive/Extrinsic/v2/ExtrinsicSignature.spec.ts new file mode 100644 index 000000000000..130921201eaa --- /dev/null +++ b/packages/types/src/primitive/Extrinsic/v2/ExtrinsicSignature.spec.ts @@ -0,0 +1,27 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import ExtrinsicSignature from './ExtrinsicSignature'; + +describe('ExtrinsicSignatureV2', (): void => { + it('encodes to a sane Uint8Array', (): void => { + const u8a = new Uint8Array([ + // signer as an AccountIndex + 0x09, + // signature + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + // extra stuff + 0x00, // immortal, + 0x04, // nonce, compact + 0x08 // tip, compact + ]); + + expect( + new ExtrinsicSignature(u8a, { isSigned: true }).toU8a() + ).toEqual(u8a); + }); +}); diff --git a/packages/types/src/primitive/Extrinsic/v2/ExtrinsicSignature.ts b/packages/types/src/primitive/Extrinsic/v2/ExtrinsicSignature.ts new file mode 100644 index 000000000000..7c41c019e924 --- /dev/null +++ b/packages/types/src/primitive/Extrinsic/v2/ExtrinsicSignature.ts @@ -0,0 +1,145 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { ExtrinsicPayloadValue, IExtrinsicSignature, IKeyringPair, SignatureOptions } from '../../../types'; + +import Struct from '../../../codec/Struct'; +import Address from '../../Address'; +import BalanceCompact from '../../BalanceCompact'; +import Method from '../../Method'; +import Signature from '../../Signature'; +import ExtrinsicEra from '../ExtrinsicEra'; +import NonceCompact from '../../../type/NonceCompact'; +import SignaturePayload from './SignaturePayload'; +import { EMPTY_U8A, IMMORTAL_ERA } from '../constants'; +import ExtrinsicExtra from './ExtrinsicExtra'; + +interface ExtrinsicSignatureV2Options { + isSigned?: boolean; +} + +/** + * @name ExtrinsicSignature + * @description + * A container for the [[Signature]] associated with a specific [[Extrinsic]] + */ +export default class ExtrinsicSignatureV2 extends Struct implements IExtrinsicSignature { + public constructor (value: ExtrinsicSignatureV2 | Uint8Array | undefined, { isSigned }: ExtrinsicSignatureV2Options = {}) { + super({ + signer: Address, + signature: Signature, + extra: ExtrinsicExtra + }, ExtrinsicSignatureV2.decodeExtrinsicSignature(value, isSigned)); + } + + public static decodeExtrinsicSignature (value: ExtrinsicSignatureV2 | Uint8Array | undefined, isSigned: boolean = false): ExtrinsicSignatureV2 | Uint8Array { + if (!value) { + return EMPTY_U8A; + } else if (value instanceof ExtrinsicSignatureV2) { + return value; + } + + return isSigned + ? value + : EMPTY_U8A; + } + + /** + * @description `true` if the signature is valid + */ + public get isSigned (): boolean { + return !this.signature.isEmpty; + } + + /** + * @description Returns the extra extrinsic info + */ + public get extra (): ExtrinsicExtra { + return this.get('extra') as ExtrinsicExtra; + } + + /** + * @description The [[ExtrinsicEra]] (mortal or immortal) this signature applies to + */ + public get era (): ExtrinsicEra { + return this.extra.era; + } + + /** + * @description The [[NonceCompact]] for the signature + */ + public get nonce (): NonceCompact { + return this.extra.nonce; + } + + /** + * @description The actuall [[Signature]] hash + */ + public get signature (): Signature { + return this.get('signature') as Signature; + } + + /** + * @description The [[Address]] that signed + */ + public get signer (): Address { + return this.get('signer') as Address; + } + + /** + * @description The [[Balance]] tip + */ + public get tip (): BalanceCompact { + return this.extra.tip; + } + + private injectSignature (signer: Address, signature: Signature, { era, nonce, tip }: SignaturePayload): IExtrinsicSignature { + this.extra.set('era', era); + this.extra.set('nonce', nonce); + this.extra.set('tip', tip); + + this.set('signer', signer); + this.set('signature', signature); + + return this; + } + + /** + * @description Adds a raw signature + */ + public addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | string, payload: ExtrinsicPayloadValue | Uint8Array | string): IExtrinsicSignature { + return this.injectSignature( + new Address(signer), + new Signature(signature), + new SignaturePayload(payload) + ); + } + + /** + * @description Generate a payload and pplies the signature from a keypair + */ + public sign (method: Method, account: IKeyringPair, { blockHash, era, nonce, tip }: SignatureOptions): IExtrinsicSignature { + const signer = new Address(account.publicKey); + const payload = new SignaturePayload({ + blockHash, + era: era || IMMORTAL_ERA, + method: method.toU8a(), + nonce, + tip: tip || 0 + }); + const signature = new Signature(payload.sign(account)); + + return this.injectSignature(signer, signature, payload); + } + + /** + * @description Encodes the value as a Uint8Array as per the SCALE specifications + * @param isBare true when the value has none of the type-specific prefixes (internal) + */ + public toU8a (isBare?: boolean): Uint8Array { + return this.isSigned + ? super.toU8a(isBare) + : new Uint8Array(); + } +} diff --git a/packages/types/src/primitive/Extrinsic/v2/SignaturePayload.ts b/packages/types/src/primitive/Extrinsic/v2/SignaturePayload.ts new file mode 100644 index 000000000000..a4b863ab062e --- /dev/null +++ b/packages/types/src/primitive/Extrinsic/v2/SignaturePayload.ts @@ -0,0 +1,85 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { AnyNumber, AnyU8a, ExtrinsicPayloadValue, IExtrinsicEra, IKeyringPair, IMethod } from '../../../types'; + +import Struct from '../../../codec/Struct'; +import U8a from '../../../codec/U8a'; +import BalanceCompact from '../../BalanceCompact'; +import Hash from '../../Hash'; +import NonceCompact from '../../../type/NonceCompact'; +import ExtrinsicEra from '../ExtrinsicEra'; +import { extraDefinition } from './ExtrinsicExtra'; +import { sign } from '../util'; + +export interface SignaturePayloadValueV2 { + blockHash: AnyU8a; + era: AnyU8a | IExtrinsicEra; + method: AnyU8a | IMethod; + nonce: AnyNumber; + tip: AnyNumber; +} + +const basePayload = { + ...extraDefinition, + blockHash: Hash +}; + +/** + * @name SignaturePayloadV2 + * @description + * A signing payload for an [[Extrinsic]]. For the final encoding, it is variable length based + * on the contents included + */ +export default class SignaturePayloadV2 extends Struct { + public constructor (value?: ExtrinsicPayloadValue | SignaturePayloadValueV2 | Uint8Array | string) { + super({ + method: U8a, + ...basePayload + }, value); + } + + /** + * @description The block [[Hash]] the signature applies to (mortal/immortal) + */ + public get blockHash (): Hash { + return this.get('blockHash') as Hash; + } + + /** + * @description The [[U8a]] contained in the payload + */ + public get method (): U8a { + return this.get('method') as U8a; + } + + /** + * @description The [[ExtrinsicEra]] + */ + public get era (): ExtrinsicEra { + return this.get('era') as ExtrinsicEra; + } + + /** + * @description The [[NonceCompact]] + */ + public get nonce (): NonceCompact { + return this.get('nonce') as NonceCompact; + } + + /** + * @description The tip [[BalanceCompact]] + */ + public get tip (): BalanceCompact { + return this.get('tip') as BalanceCompact; + } + + /** + * @description Sign the payload with the keypair + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public sign (signerPair: IKeyringPair): Uint8Array { + return sign(signerPair, this.toU8a()); + } +} diff --git a/packages/types/src/primitive/index.ts b/packages/types/src/primitive/index.ts index bee79a53d3e6..5ca7fcd6fdb7 100644 --- a/packages/types/src/primitive/index.ts +++ b/packages/types/src/primitive/index.ts @@ -22,6 +22,9 @@ export { default as EventRecord, EventRecord0to76 } from './EventRecord'; export { default as Bool, default as bool } from './Bool'; export { default as Bytes } from './Bytes'; export { default as Data } from './Data'; +export { default as Extrinsic } from './Extrinsic/Extrinsic'; +export { default as ExtrinsicEra, MortalEra, ImmortalEra } from './Extrinsic/ExtrinsicEra'; +export { default as SignaturePayload } from './Extrinsic/SignaturePayload'; export { default as H160 } from './H160'; export { default as H256 } from './H256'; export { default as H512 } from './H512'; diff --git a/packages/types/src/type/Extrinsic.spec.ts b/packages/types/src/type/Extrinsic.spec.ts deleted file mode 100644 index 23e99bdf3287..000000000000 --- a/packages/types/src/type/Extrinsic.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2017-2019 @polkadot/types authors & contributors -// This software may be modified and distributed under the terms -// of the Apache-2.0 license. See the LICENSE file for details. - -import extrinsics from '@polkadot/api-metadata/extrinsics/static'; - -import Method from '../primitive/Method'; -import Extrinsic from './Extrinsic'; - -describe('Extrinsic', (): void => { - beforeAll((): void => { - Method.injectMethods(extrinsics); - }); - - it.skip('decodes a non-signed properly via JSON', (): void => { - const extrinsic = new Extrinsic('0x010300ea51b75b00000000'); - - expect(extrinsic.isSigned).toEqual(false); - expect(extrinsic.callIndex).toEqual(new Uint8Array([3, 0])); - expect(extrinsic.data).toEqual(new Uint8Array([234, 81, 183, 91, 0, 0, 0, 0])); - }); - - it('decodes an actual transaction (new format)', (): void => { - const extrinsic = new Extrinsic('0x250281ffbfc823aa75c30058eeec21abe2c2d6b7247418a4af89d67a2084c2ac864da080c0aa4df3b4926c3cd78bbdced31d8bdccb8604b779b71b90e58b2848df4a9ad6b0aa1aae6be7a05c9413a172b0325e4d214e5ff2b25098028b30f1a50be9c90e0c00' + - '0500' + // balances.transfer - 'ff4a83f1c09be797bc3d9adce29818368b276a84e6b545ced492c25c948978d7f8e5c0'); - - expect(extrinsic.isSigned).toEqual(true); - expect(extrinsic.signature.signer.toU8a()).toEqual(new Uint8Array([255, 191, 200, 35, 170, 117, 195, 0, 88, 238, 236, 33, 171, 226, 194, 214, 183, 36, 116, 24, 164, 175, 137, 214, 122, 32, 132, 194, 172, 134, 77, 160, 128])); - expect(extrinsic.signature.signature.toHex()).toEqual('0xc0aa4df3b4926c3cd78bbdced31d8bdccb8604b779b71b90e58b2848df4a9ad6b0aa1aae6be7a05c9413a172b0325e4d214e5ff2b25098028b30f1a50be9c90e'); - expect(extrinsic.signature.nonce.toNumber()).toEqual(3); - expect(extrinsic.signature.era.toU8a()).toEqual(new Uint8Array([0])); - expect(extrinsic.callIndex).toEqual(new Uint8Array([5, 0])); - expect(`${extrinsic.method.sectionName}.${extrinsic.method.methodName}`).toEqual('balances.transfer'); - expect(extrinsic.args[0].toString()).toEqual('5DkQbYAExs3M2sZgT1Ec3mKfZnAQCL4Dt9beTCknkCUn5jzo'); - }); - - it('decodes an actual transaction (old format)', (): void => { - const extrinsic = new Extrinsic( - '0x81ffbfc823aa75c30058eeec21abe2c2d6b7247418a4af89d67a2084c2ac864da080c0aa4df3b4926c3cd78bbdced31d8bdccb8604b779b71b90e58b2848df4a9ad6b0aa1aae6be7a05c9413a172b0325e4d214e5ff2b25098028b30f1a50be9c90e0c00' + - '0500' + // balances.transfer - 'ff4a83f1c09be797bc3d9adce29818368b276a84e6b545ced492c25c948978d7f8e5c0' - ); - - expect(extrinsic.isSigned).toEqual(true); - expect(extrinsic.signature.signer.toU8a()).toEqual(new Uint8Array([255, 191, 200, 35, 170, 117, 195, 0, 88, 238, 236, 33, 171, 226, 194, 214, 183, 36, 116, 24, 164, 175, 137, 214, 122, 32, 132, 194, 172, 134, 77, 160, 128])); - expect(extrinsic.signature.signature.toHex()).toEqual('0xc0aa4df3b4926c3cd78bbdced31d8bdccb8604b779b71b90e58b2848df4a9ad6b0aa1aae6be7a05c9413a172b0325e4d214e5ff2b25098028b30f1a50be9c90e'); - expect(extrinsic.signature.nonce.toNumber()).toEqual(3); - expect(extrinsic.signature.era.toU8a()).toEqual(new Uint8Array([0])); - expect(extrinsic.callIndex).toEqual(new Uint8Array([5, 0])); - expect(`${extrinsic.method.sectionName}.${extrinsic.method.methodName}`).toEqual('balances.transfer'); - expect(extrinsic.args[0].toString()).toEqual('5DkQbYAExs3M2sZgT1Ec3mKfZnAQCL4Dt9beTCknkCUn5jzo'); - }); -}); diff --git a/packages/types/src/type/Extrinsic.ts b/packages/types/src/type/Extrinsic.ts deleted file mode 100644 index a5c1fea0ccdf..000000000000 --- a/packages/types/src/type/Extrinsic.ts +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright 2017-2019 @polkadot/types authors & contributors -// This software may be modified and distributed under the terms -// of the Apache-2.0 license. See the LICENSE file for details. - -import { AnyNumber, AnyU8a, ArgsDef, Codec, IExtrinsic, IExtrinsicEra, IKeyringPair, SignatureOptions } from '../types'; - -import { assert, isHex, isU8a, u8aToU8a } from '@polkadot/util'; - -import Base from '../codec/Base'; -import Compact from '../codec/Compact'; -import { FunctionMetadata } from '../Metadata/v6/Calls'; -import Method from '../primitive/Method'; -import Address from '../primitive/Address'; -import Hash from '../primitive/Hash'; -import ExtrinsicSignature from './ExtrinsicSignature'; -import ExtrinsicV1, { ExtrinsicValueV1 } from './ExtrinsicV1'; - -type ExtrinsicValue = ExtrinsicValueV1; - -const UNMASK_VERSION = 0b01111111; - -/** - * @name Extrinsic - * @description - * Representation of an Extrinsic in the system. It contains the actual call, - * (optional) signature and encodes with an actual length prefix - * - * {@link https://github.com/paritytech/wiki/blob/master/Extrinsic.md#the-extrinsic-format-for-node}. - * - * Can be: - * - signed, to create a transaction - * - left as is, to create an inherent - */ -export default class Extrinsic extends Base implements IExtrinsic, Codec { - public constructor (value?: ExtrinsicValue | AnyU8a | Method) { - super(Extrinsic.decodeExtrinsic(value)); - } - - public static decodeExtrinsic (value?: ExtrinsicValue | AnyU8a | Method): ExtrinsicV1 { - if (Array.isArray(value) || isHex(value)) { - // Instead of the block below, it should simply be: - // return Extrinsic.decodeExtrinsic(hexToU8a(value as string)); - const u8a = u8aToU8a(value); - - // HACK 11 Jan 2019 - before https://github.com/paritytech/substrate/pull/1388 - // extrinsics didn't have the length, cater for both approaches - const [offset, length] = Compact.decodeU8a(u8a); - const withPrefix = u8a.length === (offset + length.toNumber()); - - return Extrinsic.decodeExtrinsic( - withPrefix - ? u8a - : Compact.addLengthPrefix(u8a) - ); - } else if (isU8a(value)) { - if (!value.length) { - return new ExtrinsicV1(new Uint8Array()); - } - - const [offset, length] = Compact.decodeU8a(value); - const total = offset + length.toNumber(); - - assert(total <= value.length, `Extrinsic: required length less than remainder, expected at least ${total}, found ${value.length}`); - - return Extrinsic.decodeU8a(value.subarray(offset, total)); - } else if (value instanceof Method) { - return new ExtrinsicV1({ - method: value - }); - } - - return new ExtrinsicV1(value); - } - - public static decodeU8a (value: Uint8Array): ExtrinsicV1 { - // decode the actual version string - const version = value[0] & UNMASK_VERSION; - - switch (version) { - case 1: - return new ExtrinsicV1(value); - - default: - throw new Error(`Unsupported extrinsic version ${version}`); - } - } - - /** - * @description The arguments passed to for the call, exposes args so it is compatible with [[Method]] - */ - public get args (): Codec[] { - return this.method.args; - } - - /** - * @description Thge argument defintions, compatible with [[Method]] - */ - public get argsDef (): ArgsDef { - return this.method.argsDef; - } - - /** - * @description The actual `[sectionIndex, methodIndex]` as used in the Method - */ - public get callIndex (): Uint8Array { - return this.method.callIndex; - } - - /** - * @description The actual data for the Method - */ - public get data (): Uint8Array { - return this.method.data; - } - - /** - * @description The length of the value when encoded as a Uint8Array - */ - public get encodedLength (): number { - return this.raw.encodedLength; - } - - /** - * @description Convernience function, encodes the extrinsic and returns the actual hash - */ - public get hash (): Hash { - return this.raw.hash; - } - - /** - * @description `true` is method has `Origin` argument (compatibility with [[Method]]) - */ - public get hasOrigin (): boolean { - return this.raw.hasOrigin; - } - - /** - * @description Checks if the value is an empty value - */ - public get isEmpty (): boolean { - return this.raw.isEmpty; - } - - /** - * @description `true` id the extrinsic is signed - */ - public get isSigned (): boolean { - return this.raw.isSigned; - } - - /** - * @description The length of the encoded value - */ - public get length (): number { - return this.raw.length; - } - - /** - * @description The [[FunctionMetadata]] that describes the extrinsic - */ - public get meta (): FunctionMetadata { - return this.raw.meta; - } - - /** - * @description The [[Method]] this extrinsic wraps - */ - public get method (): Method { - return this.raw.method; - } - - /** - * @description The [[ExtrinsicSignature]] - */ - public get signature (): ExtrinsicSignature { - return this.raw.signature; - } - - /** - * @description Add an [[ExtrinsicSignature]] to the extrinsic (already generated) - */ - public addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | string, nonce: AnyNumber, era: Uint8Array | IExtrinsicEra): Extrinsic { - this.raw.addSignature(signer, signature, nonce, era); - - return this; - } - - /** - * @description Sign the extrinsic with a specific keypair - */ - public sign (account: IKeyringPair, options: SignatureOptions): Extrinsic { - this.raw.sign(account, options); - - return this; - } - - /** - * @description Compares the value of the input to see if there is a match - */ - public eq (other?: any): boolean { - return this.raw.eq(other); - } - - /** - * @description Returns a hex string representation of the value - */ - public toHex (): string { - return this.raw.toHex(); - } - - /** - * @description Converts the Object to JSON, typically used for RPC transfers - */ - public toJSON (): string { - return this.raw.toJSON(); - } - - /** - * @description Returns the string representation of the value - */ - public toString (): string { - return this.raw.toString(); - } - - /** - * @description Returns the base runtime type name for this instance - */ - public toRawType (): string { - return 'Extrinsic'; - } - - /** - * @description Encodes the value as a Uint8Array as per the SCALE specifications - * @param isBare true when the value has none of the type-specific prefixes (internal) - */ - public toU8a (isBare?: boolean): Uint8Array { - return this.raw.toU8a(isBare); - } -} diff --git a/packages/types/src/type/ExtrinsicSignature.ts b/packages/types/src/type/ExtrinsicSignature.ts deleted file mode 100644 index ba69a9c88bb8..000000000000 --- a/packages/types/src/type/ExtrinsicSignature.ts +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright 2017-2019 @polkadot/types authors & contributors -// This software may be modified and distributed under the terms -// of the Apache-2.0 license. See the LICENSE file for details. - -import { AnyNumber, IExtrinsicEra, IExtrinsicSignature, IKeyringPair, SignatureOptions } from '../types'; - -import Struct from '../codec/Struct'; -import Address from '../primitive/Address'; -import Method from '../primitive/Method'; -import Signature from '../primitive/Signature'; -import U8 from '../primitive/U8'; -import RuntimeVersion from '../rpc/RuntimeVersion'; -import ExtrinsicEra from './ExtrinsicEra'; -import Nonce from './NonceCompact'; -import SignaturePayload from './SignaturePayload'; - -export const IMMORTAL_ERA = new Uint8Array([0]); - -const BIT_SIGNED = 0b10000000; -const BIT_UNSIGNED = 0; -const BIT_VERSION = 0b0000001; - -/** - * @name ExtrinsicSignature - * @description - * A container for the [[Signature]] associated with a specific [[Extrinsic]] - */ -export default class ExtrinsicSignature extends Struct implements IExtrinsicSignature { - // Signature Information. - // 1 byte version: BIT_VERSION | (isSigned ? BIT_SIGNED : BIT_UNSIGNED) - // 1/3/5/9/33 bytes: The signing account identity, in Address format - // 64 bytes: The sr25519/ed25519 signature of the Signing Payload - // 1-8 bytes: The Compact of the signing account - // 1/2 bytes: The Transaction Era - public constructor (value?: Uint8Array) { - super({ - version: U8, - signer: Address, - signature: Signature, - nonce: Nonce, - era: ExtrinsicEra - }, ExtrinsicSignature.decodeExtrinsicSignature(value)); - } - - public static decodeExtrinsicSignature (value?: Uint8Array): object | Uint8Array { - if (!value) { - return { - // we always explicitly set the unsigned version - version: BIT_VERSION | BIT_UNSIGNED - }; - } - - const version = value[0]; - - // only decode the full Uint8Array if we have the signed indicator, - // alternatively only return the version (default for others) - return (version & BIT_SIGNED) === BIT_SIGNED - ? value - : { version }; - } - - /** - * @description The length of the value when encoded as a Uint8Array - */ - public get encodedLength (): number { - return this.isSigned - ? super.encodedLength - : 1; - } - - /** - * @description `true` if the signature is valid - */ - public get isSigned (): boolean { - return (this.version & BIT_SIGNED) === BIT_SIGNED; - } - - /** - * @description The [[ExtrinsicEra]] (mortal or immortal) this signature applies to - */ - public get era (): ExtrinsicEra { - return this.get('era') as ExtrinsicEra; - } - - /** - * @description The [[ExtrinsicEra]] (mortal or immortal) this signature applies to - */ - public set era (era: ExtrinsicEra) { - this.set('era', era); - } - - /** - * @description The [[Nonce]] for the signature - */ - public get nonce (): Nonce { - return this.get('nonce') as Nonce; - } - - /** - * @description The actuall [[Signature]] hash - */ - public get signature (): Signature { - return this.get('signature') as Signature; - } - - /** - * @description The [[Address]] that signed - */ - public get signer (): Address { - return this.get('signer') as Address; - } - - /** - * @description The encoded version for the signature - */ - public get version (): number { - // Version Information. - // 1 byte: version information: - // - 7 low bits: version identifier (should be 0b0000001). - // - 1 high bit: signed flag: 1 if this is a transaction (e.g. contains a signature). - return (this.get('version') as U8).toNumber(); - } - - private injectSignature (signature: Signature, signer: Address, nonce: Nonce, era: ExtrinsicEra): ExtrinsicSignature { - this.set('era', era); - this.set('nonce', nonce); - this.set('signer', signer); - this.set('signature', signature); - this.set('version', new U8(BIT_VERSION | BIT_SIGNED)); - - return this; - } - - /** - * @description Adds a raw signature - */ - public addSignature (_signer: Address | Uint8Array | string, _signature: Uint8Array | string, _nonce: AnyNumber, _era: Uint8Array | IExtrinsicEra): ExtrinsicSignature { - const signer = new Address(_signer); - const nonce = new Nonce(_nonce); - const era = new ExtrinsicEra(_era); - const signature = new Signature(_signature); - - return this.injectSignature(signature, signer, nonce, era); - } - - /** - * @description Generate a payload and pplies the signature from a keypair - */ - public sign (method: Method, account: IKeyringPair, { blockHash, era, nonce, version }: SignatureOptions): ExtrinsicSignature { - const signer = new Address(account.publicKey); - const signingPayload = new SignaturePayload({ - nonce, - method, - era: era || this.era || IMMORTAL_ERA, - blockHash - }); - const signature = new Signature(signingPayload.sign(account, version as RuntimeVersion)); - - return this.injectSignature(signature, signer, signingPayload.nonce, signingPayload.era); - } - - /** - * @description Encodes the value as a Uint8Array as per the SCALE specifications - * @param isBare true when the value has none of the type-specific prefixes (internal) - */ - public toU8a (isBare?: boolean): Uint8Array { - return this.isSigned - ? super.toU8a(isBare) - : new Uint8Array([this.version]); - } -} diff --git a/packages/types/src/type/ExtrinsicV1.ts b/packages/types/src/type/ExtrinsicV1.ts deleted file mode 100644 index be806692b357..000000000000 --- a/packages/types/src/type/ExtrinsicV1.ts +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright 2017-2019 @polkadot/types authors & contributors -// This software may be modified and distributed under the terms -// of the Apache-2.0 license. See the LICENSE file for details. - -import { AnyNumber, ArgsDef, Codec, IExtrinsic, IExtrinsicEra, IKeyringPair, SignatureOptions } from '../types'; - -import { u8aToHex } from '@polkadot/util'; -import { blake2AsU8a } from '@polkadot/util-crypto'; - -import Compact from '../codec/Compact'; -import Struct from '../codec/Struct'; -import { FunctionMetadata } from '../Metadata/v6/Calls'; -import Method from '../primitive/Method'; -import Address from '../primitive/Address'; -import Hash from '../primitive/Hash'; -import ExtrinsicSignature from './ExtrinsicSignature'; - -export interface ExtrinsicValueV1 { - method?: Method; - signature?: ExtrinsicSignature; -} - -/** - * @name ExtrinsicV1 - * @description - * The first generation of compact extrinsics - */ -export default class ExtrinsicV1 extends Struct implements IExtrinsic { - public constructor (value?: Uint8Array | ExtrinsicValueV1) { - super({ - signature: ExtrinsicSignature, - method: Method - }, value); - } - - /** - * @description The arguments passed to for the call, exposes args so it is compatible with [[Method]] - */ - public get args (): Codec[] { - return this.method.args; - } - - /** - * @description Thge argument defintions, compatible with [[Method]] - */ - public get argsDef (): ArgsDef { - return this.method.argsDef; - } - - /** - * @description The actual `[sectionIndex, methodIndex]` as used in the Method - */ - public get callIndex (): Uint8Array { - return this.method.callIndex; - } - - /** - * @description The actual data for the Method - */ - public get data (): Uint8Array { - return this.method.data; - } - - /** - * @description The length of the value when encoded as a Uint8Array - */ - public get encodedLength (): number { - const length = this.length; - - return length + Compact.encodeU8a(length).length; - } - - /** - * @description Convernience function, encodes the extrinsic and returns the actual hash - */ - public get hash (): Hash { - return new Hash( - blake2AsU8a(this.toU8a(), 256) - ); - } - - /** - * @description `true` is method has `Origin` argument (compatibility with [[Method]]) - */ - public get hasOrigin (): boolean { - return this.method.hasOrigin; - } - - /** - * @description `true` id the extrinsic is signed - */ - public get isSigned (): boolean { - return this.signature.isSigned; - } - - /** - * @description The length of the encoded value - */ - public get length (): number { - return this.toU8a(true).length; - } - - /** - * @description The [[FunctionMetadata]] that describes the extrinsic - */ - public get meta (): FunctionMetadata { - return this.method.meta; - } - - /** - * @description The [[Method]] this extrinsic wraps - */ - public get method (): Method { - return this.get('method') as Method; - } - - /** - * @description The [[ExtrinsicSignature]] - */ - public get signature (): ExtrinsicSignature { - return this.get('signature') as ExtrinsicSignature; - } - - /** - * @description Add an [[ExtrinsicSignature]] to the extrinsic (already generated) - */ - public addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | string, nonce: AnyNumber, era: Uint8Array | IExtrinsicEra): ExtrinsicV1 { - this.signature.addSignature(signer, signature, nonce, era); - - return this; - } - - /** - * @description Sign the extrinsic with a specific keypair - */ - public sign (account: IKeyringPair, options: SignatureOptions): ExtrinsicV1 { - this.signature.sign(this.method, account, options); - - return this; - } - - /** - * @description Returns a hex string representation of the value - */ - public toHex (): string { - return u8aToHex(this.toU8a()); - } - - /** - * @description Converts the Object to JSON, typically used for RPC transfers - */ - public toJSON (): string { - return this.toHex(); - } - - /** - * @description Returns the base runtime type name for this instance - */ - public toRawType (): string { - // We are treating this in the same way we do a primitive, this is known - return 'Extrinsic'; - } - - /** - * @description Encodes the value as a Uint8Array as per the SCALE specifications - * @param isBare true when the value has none of the type-specific prefixes (internal) - */ - public toU8a (isBare?: boolean): Uint8Array { - const encoded = super.toU8a(); - - return isBare - ? encoded - : Compact.addLengthPrefix(encoded); - } -} diff --git a/packages/types/src/type/Extrinsics.ts b/packages/types/src/type/Extrinsics.ts index 18f9379e6483..dbcad7c1c8da 100644 --- a/packages/types/src/type/Extrinsics.ts +++ b/packages/types/src/type/Extrinsics.ts @@ -3,7 +3,7 @@ // of the Apache-2.0 license. See the LICENSE file for details. import Vector from '../codec/Vector'; -import Extrinsic from './Extrinsic'; +import Extrinsic from '../primitive/Extrinsic/Extrinsic'; /** * @name Extrinsics diff --git a/packages/types/src/type/SignaturePayload.ts b/packages/types/src/type/SignaturePayload.ts deleted file mode 100644 index 11cb7afac886..000000000000 --- a/packages/types/src/type/SignaturePayload.ts +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2017-2019 @polkadot/types authors & contributors -// This software may be modified and distributed under the terms -// of the Apache-2.0 license. See the LICENSE file for details. - -import { AnyNumber, AnyU8a, IExtrinsicEra, IKeyringPair } from '../types'; - -import { blake2AsU8a } from '@polkadot/util-crypto'; - -import Struct from '../codec/Struct'; -import U8a from '../codec/U8a'; -import Hash from '../primitive/Hash'; -import Method from '../primitive/Method'; -import RuntimeVersion from '../rpc/RuntimeVersion'; -import ExtrinsicEra from './ExtrinsicEra'; -import Nonce from './NonceCompact'; - -interface SignaturePayloadValue { - nonce?: AnyNumber; - method?: Method; - era?: AnyU8a | IExtrinsicEra; - blockHash?: AnyU8a; -} - -// a helper function for both types of payloads, Raw and metadata-known -function sign (signerPair: IKeyringPair, u8a: Uint8Array): Uint8Array { - const encoded = u8a.length > 256 - ? blake2AsU8a(u8a) - : u8a; - - return signerPair.sign(encoded); -} - -/** - * @name SignaturePayload - * @description - * A signing payload for an [[Extrinsic]]. For the final encoding, it is variable length based - * on the contents included - * - * 1-8 bytes: The Transaction Compact as provided in the transaction itself. - * 2+ bytes: The Function Descriptor as provided in the transaction itself. - * 1/2 bytes: The Transaction Era as provided in the transaction itself. - * 32 bytes: The hash of the authoring block implied by the Transaction Era and the current block. - */ -export default class SignaturePayload extends Struct { - protected _signature?: Uint8Array; - - public constructor (value?: SignaturePayloadValue | Uint8Array) { - super({ - nonce: Nonce, - method: Method, - era: ExtrinsicEra, - blockHash: Hash - }, value); - } - - /** - * @description `true` if the payload refers to a valid signature - */ - public get isSigned (): boolean { - return !!(this._signature && this._signature.length === 64); - } - - /** - * @description The block [[Hash]] the signature applies to (mortal/immortal) - */ - public get blockHash (): Hash { - return this.get('blockHash') as Hash; - } - - /** - * @description The [[Method]] contained in the payload - */ - public get method (): Method { - return this.get('method') as Method; - } - - /** - * @description The [[ExtrinsicEra]] - */ - public get era (): ExtrinsicEra { - return this.get('era') as ExtrinsicEra; - } - - /** - * @description The [[Nonce]] - */ - public get nonce (): Nonce { - return this.get('nonce') as Nonce; - } - - /** - * @description The raw signature as a `Uint8Array` - */ - public get signature (): Uint8Array { - if (!this.isSigned) { - throw new Error('Transaction is not signed'); - } - - return this._signature as Uint8Array; - } - - /** - * @description Sign the payload with the keypair - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public sign (signerPair: IKeyringPair, version?: RuntimeVersion): Uint8Array { - this._signature = sign(signerPair, this.toU8a()); - - return this._signature; - } -} - -/** - * @name SignaturePayloadRaw - * @description - * A version of [[SignaturePayload]] where it does not rely on [[Method]] being initalized with metadata. When constructing, it treats the [[Method]] as a raw stream of bytes, so will always apply the signature over this without any additional checking. Unlike the [[SignaturePayload]], it assumed that you will only construct and sign, thereby providing no insigt into constructed values - */ -export class SignaturePayloadRaw extends Struct { - public constructor (value?: any) { - super({ - nonce: Nonce, - method: U8a, - era: ExtrinsicEra, - blockHash: Hash - }, value); - } - - /** - * @description The [[ExtrinsicEra]] - */ - public get era (): ExtrinsicEra { - return this.get('era') as ExtrinsicEra; - } - - /** - * @description Sign the payload with the keypair - */ - public sign (signerPair: IKeyringPair): Uint8Array { - return sign(signerPair, this.toU8a()); - } -} diff --git a/packages/types/src/type/index.ts b/packages/types/src/type/index.ts index c23ad8c2acfb..8de78403f8ba 100644 --- a/packages/types/src/type/index.ts +++ b/packages/types/src/type/index.ts @@ -19,9 +19,6 @@ export { default as BlockNumber } from './BlockNumber'; export { default as Conviction } from './Conviction'; export { default as EraIndex } from './EraIndex'; export { default as Exposure } from './Exposure'; -export { default as Extrinsic } from './Extrinsic'; -export { default as ExtrinsicEra, MortalEra, ImmortalEra } from './ExtrinsicEra'; -export { default as ExtrinsicSignature } from './ExtrinsicSignature'; // NOTE Only used internally, exported as PendingExtrinsics // export { default as Extrinsics } from './Extrinsics'; export { default as IndividualExposure } from './IndividualExposure'; @@ -52,7 +49,6 @@ export { default as SessionIndex } from './SessionIndex'; export { default as SessionKey } from './SessionKey'; export { default as SessionKeys } from './SessionKeys'; export { default as SetIndex } from './SetIndex'; -export { default as SignaturePayload, SignaturePayloadRaw } from './SignaturePayload'; export { default as StakingLedger } from './StakingLedger'; export { default as TreasuryProposal } from './TreasuryProposal'; export { default as UncleEntryItem } from './UncleEntryItem'; diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 83193027ae14..dbaf74bf81e7 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -6,8 +6,10 @@ import BN from 'bn.js'; import U8a from './codec/U8a'; import { FunctionMetadata } from './Metadata/v6/Calls'; +import BalanceCompact from './primitive/BalanceCompact'; import Method from './primitive/Method'; import Address from './primitive/Address'; +import NonceCompact from './type/NonceCompact'; // eslint-disable-next-line @typescript-eslint/interface-name-prefix export interface IKeyringPair { @@ -62,7 +64,6 @@ export interface Codec { /** * @description Compares the value of the input to see if there is a match */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any eq (other?: any): boolean; /** @@ -123,12 +124,11 @@ export interface SignatureOptions { blockHash: AnyU8a; era?: IExtrinsicEra; nonce: AnyNumber; + tip?: AnyNumber; version?: RuntimeVersionInterface; } -export interface ArgsDef { - [index: string]: Constructor; -} +export type ArgsDef = Record; // eslint-disable-next-line @typescript-eslint/interface-name-prefix,@typescript-eslint/no-empty-interface export interface IHash extends U8a { } @@ -143,24 +143,52 @@ export interface IMethod extends Codec { readonly meta: FunctionMetadata; } -// eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface IExtrinsicSignature extends Codec { +interface ExtrinsicSignatureBase { readonly isSigned: boolean; readonly era: IExtrinsicEra; + readonly nonce: NonceCompact; + readonly signature: IHash; + readonly signer: Address; + readonly tip: BalanceCompact; +} + +export interface ExtrinsicPayloadValue { + era: IExtrinsicEra | AnyU8a; + method: AnyU8a; + nonce: AnyNumber; + tip: AnyNumber; } // eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface IExtrinsicEra { +export interface IExtrinsicSignature extends ExtrinsicSignatureBase, Codec { + addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | string, payload: Uint8Array | string): IExtrinsicSignature; + sign (method: Method, account: IKeyringPair, options: SignatureOptions): IExtrinsicSignature; +} + +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface IExtrinsicEra extends Codec { asImmortalEra: Codec; asMortalEra: Codec; } // eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface IExtrinsic extends IMethod { - hash: IHash; - isSigned: boolean; - method: Method; - signature: IExtrinsicSignature; - addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | string, nonce: AnyNumber, era: Uint8Array | IExtrinsicEra): IExtrinsic; +export interface IExtrinsicImpl extends Codec { + readonly method: Method; + readonly signature: IExtrinsicSignature; + readonly version: number; + + addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | string, payload: ExtrinsicPayloadValue | Uint8Array | string): IExtrinsicImpl; + sign (account: IKeyringPair, options: SignatureOptions): IExtrinsicImpl; +} + +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface IExtrinsic extends ExtrinsicSignatureBase, IMethod { + readonly hash: IHash; + readonly length: number; + readonly method: Method; + readonly type: number; + readonly version: number; + + addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | string, payload: ExtrinsicPayloadValue | Uint8Array | string): IExtrinsic; sign (account: IKeyringPair, options: SignatureOptions): IExtrinsic; }