diff --git a/packages/types-augment/src/registry/interfaces.ts b/packages/types-augment/src/registry/interfaces.ts index 3a28ca1ca0b5..5b77684993c2 100644 --- a/packages/types-augment/src/registry/interfaces.ts +++ b/packages/types-augment/src/registry/interfaces.ts @@ -35,7 +35,7 @@ import type { ApprovalFlag, DefunctVoter, Renouncing, SetIndex, Vote, VoteIndex, import type { CreatedBlock, ImportedAux } from '@polkadot/types/interfaces/engine'; import type { BlockV0, BlockV1, BlockV2, EIP1559Transaction, EIP2930Transaction, EthAccessList, EthAccessListItem, EthAccount, EthAddress, EthBlock, EthBloom, EthCallRequest, EthFeeHistory, EthFilter, EthFilterAddress, EthFilterChanges, EthFilterTopic, EthFilterTopicEntry, EthFilterTopicInner, EthHeader, EthLog, EthReceipt, EthReceiptV0, EthReceiptV3, EthRichBlock, EthRichHeader, EthStorageProof, EthSubKind, EthSubParams, EthSubResult, EthSyncInfo, EthSyncStatus, EthTransaction, EthTransactionAction, EthTransactionCondition, EthTransactionRequest, EthTransactionSignature, EthTransactionStatus, EthWork, EthereumAccountId, EthereumAddress, EthereumLookupSource, EthereumSignature, LegacyTransaction, TransactionV0, TransactionV1, TransactionV2 } from '@polkadot/types/interfaces/eth'; import type { EvmAccount, EvmCallInfo, EvmCallInfoV2, EvmCreateInfo, EvmCreateInfoV2, EvmLog, EvmVicinity, EvmWeightInfo, ExitError, ExitFatal, ExitReason, ExitRevert, ExitSucceed } from '@polkadot/types/interfaces/evm'; -import type { AnySignature, EcdsaSignature, Ed25519Signature, Era, Extrinsic, ExtrinsicEra, ExtrinsicPayload, ExtrinsicPayloadUnknown, ExtrinsicPayloadV4, ExtrinsicSignature, ExtrinsicSignatureV4, ExtrinsicUnknown, ExtrinsicV4, ImmortalEra, MortalEra, MultiSignature, Signature, SignerPayload, Sr25519Signature } from '@polkadot/types/interfaces/extrinsics'; +import type { AnySignature, EcdsaSignature, Ed25519Signature, Era, Extrinsic, ExtrinsicEra, ExtrinsicPayload, ExtrinsicPayloadUnknown, ExtrinsicPayloadV4, ExtrinsicPayloadV5, ExtrinsicSignature, ExtrinsicSignatureV4, ExtrinsicSignatureV5, ExtrinsicUnknown, ExtrinsicV4, ExtrinsicV5, ImmortalEra, MortalEra, MultiSignature, Signature, SignerPayload, Sr25519Signature } from '@polkadot/types/interfaces/extrinsics'; import type { FungiblesAccessError } from '@polkadot/types/interfaces/fungibles'; import type { AssetOptions, Owner, PermissionLatest, PermissionVersions, PermissionsV1 } from '@polkadot/types/interfaces/genericAsset'; import type { GenesisBuildErr } from '@polkadot/types/interfaces/genesisBuilder'; @@ -504,12 +504,15 @@ declare module '@polkadot/types/types/registry' { ExtrinsicPayload: ExtrinsicPayload; ExtrinsicPayloadUnknown: ExtrinsicPayloadUnknown; ExtrinsicPayloadV4: ExtrinsicPayloadV4; + ExtrinsicPayloadV5: ExtrinsicPayloadV5; ExtrinsicSignature: ExtrinsicSignature; ExtrinsicSignatureV4: ExtrinsicSignatureV4; + ExtrinsicSignatureV5: ExtrinsicSignatureV5; ExtrinsicStatus: ExtrinsicStatus; ExtrinsicsWeight: ExtrinsicsWeight; ExtrinsicUnknown: ExtrinsicUnknown; ExtrinsicV4: ExtrinsicV4; + ExtrinsicV5: ExtrinsicV5; f32: f32; F32: F32; f64: f64; diff --git a/packages/types-codec/src/types/registry.ts b/packages/types-codec/src/types/registry.ts index 5f7aecbe698c..373fa84e5d19 100644 --- a/packages/types-codec/src/types/registry.ts +++ b/packages/types-codec/src/types/registry.ts @@ -71,6 +71,7 @@ export interface Registry { getClassName (clazz: CodecClass): string | undefined; getOrThrow (name: K, msg?: string): CodecClass; getOrUnknown (name: K): CodecClass; + getTransactionExtensionVersion (): number; getSignedExtensionExtra (): Record; getSignedExtensionTypes (): Record; diff --git a/packages/types/src/create/registry.ts b/packages/types/src/create/registry.ts index 2ae82f0f9abd..7059025faf4d 100644 --- a/packages/types/src/create/registry.ts +++ b/packages/types/src/create/registry.ts @@ -441,6 +441,11 @@ export class TypeRegistry implements Registry { return this.get(name, true) as unknown as CodecClass; } + // Only used in extrinsic version 5 + public getTransactionExtensionVersion (): number { + return 0; + } + public getSignedExtensionExtra (): Record { return expandExtensionTypes(this.#signedExtensions, 'payload', this.#userExtensions); } diff --git a/packages/types/src/extrinsic/Extrinsic.spec.ts b/packages/types/src/extrinsic/Extrinsic.spec.ts index b76d97d8a208..5ff4a5dccb71 100644 --- a/packages/types/src/extrinsic/Extrinsic.spec.ts +++ b/packages/types/src/extrinsic/Extrinsic.spec.ts @@ -48,4 +48,81 @@ describe('Extrinsic', (): void => { expect(extrinsic.toPrimitive()).toEqual({ method: { args: { dest: { id: '5DiuK2zR4asj2CEh77SKtUgTswTLkD8eiAKrByg5G3wL5w9b' }, value: 104560923320000 }, callIndex: '0x0600' }, signature: { era: { mortalEra: [1024, 186] }, nonce: 68, signature: { ed25519: '0xd99ffe3e610ad234e1414bda5831395a6df9098bf80b01561ce89a5065ae89d5c10e1619c6c99131b0bea4fb73ef04d07c07770e2ae9df5c325c331769ccb300' }, signer: { id: '5Hn8KKEp8qruCGWaN9MEsjTs4FXB4wv9xn7g1RWkNeKKNXCr' }, tip: 30000000000 } }); }); }); + + describe('V5', () => { + // Ensure it does not have its registry modified by the fallback extensions. + const registry = new TypeRegistry(); + const metadata = new Metadata(registry, rpcMetadata); + + registry.setMetadata(metadata); + + describe('SignedExtrinsic', () => { + it('Should work when the version and preamble is passed in', () => { + const extrinsic = new Extrinsic( + registry, + '0x51028500d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d011e0b7d9438899333c50121f8e10144952d51c3bb8d0ea11dd1f24940d8ff615ad351d95ed9f41f078748ed7cf182864a20b38eebfaef6629433365eb90c0148c007502000000000603008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a480b00a0724e1809', + { preamble: 'signed', version: 5 } + ); + + expect(extrinsic.signer.toString()).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'); + expect(extrinsic.era.toHuman()).toEqual({ MortalEra: { period: '64', phase: '39' } }); + expect(extrinsic.nonce.toNumber()).toEqual(0); + expect(extrinsic.tip.toHuman()).toEqual('0'); + expect(extrinsic.callIndex).toEqual(new Uint8Array([6, 3])); + expect(extrinsic.args[0].toHex()).toEqual('0x008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48'); + expect(extrinsic.args[1].toHuman()).toEqual('10,000,000,000,000'); + }); + + it('Should work when the version and preamble is not passed in', () => { + const extrinsic = new Extrinsic( + registry, + '0x51028500d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d011e0b7d9438899333c50121f8e10144952d51c3bb8d0ea11dd1f24940d8ff615ad351d95ed9f41f078748ed7cf182864a20b38eebfaef6629433365eb90c0148c007502000000000603008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a480b00a0724e1809' + ); + + expect(extrinsic.version).toEqual(133); + expect(extrinsic.signer.toString()).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'); + expect(extrinsic.era.toHuman()).toEqual({ MortalEra: { period: '64', phase: '39' } }); + expect(extrinsic.nonce.toNumber()).toEqual(0); + expect(extrinsic.tip.toHuman()).toEqual('0'); + expect(extrinsic.callIndex).toEqual(new Uint8Array([6, 3])); + expect(extrinsic.args[0].toHex()).toEqual('0x008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48'); + expect(extrinsic.args[1].toHuman()).toEqual('10,000,000,000,000'); + }); + }); + + describe('GeneralExtrinsic', () => { + it('Should work when the version and preamble is passed in', () => { + const extrinsic = new Extrinsic( + registry, + '0xc44500650000000000060000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0700e40b5402', + { preamble: 'general', version: 5 } + ); + + expect(extrinsic.version).toEqual(69); + // expect(extrinsic.transactionExtensionVersion.toNumber()).toEqual(0); + expect(extrinsic.method.toHuman()).toEqual({ args: { dest: { Id: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' }, value: '10,000,000,000' }, method: 'transferAllowDeath', section: 'balances' }); + expect(extrinsic.era.toHuman()).toEqual({ MortalEra: { period: '64', phase: '6' } }); + expect(extrinsic.tip.toNumber()).toEqual(0); + expect(extrinsic.mode.toNumber()).toEqual(0); + expect(extrinsic.assetId.toHuman()).toEqual(null); + expect(extrinsic.nonce.toNumber()).toEqual(0); + }); + + it('Should work when there is no version and preamble is passed in', () => { + const extrinsic = new Extrinsic( + registry, + '0xc44500650000000000060000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0700e40b5402' + ); + + expect(extrinsic.version).toEqual(69); + // expect(extrinsic.transactionExtensionVersion.toNumber()).toEqual(0); + expect(extrinsic.method.toHuman()).toEqual({ args: { dest: { Id: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' }, value: '10,000,000,000' }, method: 'transferAllowDeath', section: 'balances' }); + expect(extrinsic.era.toHuman()).toEqual({ MortalEra: { period: '64', phase: '6' } }); + expect(extrinsic.tip.toNumber()).toEqual(0); + expect(extrinsic.mode.toNumber()).toEqual(0); + expect(extrinsic.assetId.toHuman()).toEqual(null); + expect(extrinsic.nonce.toNumber()).toEqual(0); + }); + }); + }); }); diff --git a/packages/types/src/extrinsic/Extrinsic.ts b/packages/types/src/extrinsic/Extrinsic.ts index 6695ff56aaa8..a1d17faa4ac0 100644 --- a/packages/types/src/extrinsic/Extrinsic.ts +++ b/packages/types/src/extrinsic/Extrinsic.ts @@ -3,48 +3,71 @@ import type { AnyJson, AnyTuple, AnyU8a, ArgsDef, IMethod, Inspect, IOption } from '@polkadot/types-codec/types'; import type { HexString } from '@polkadot/util/types'; -import type { EcdsaSignature, Ed25519Signature, ExtrinsicUnknown, ExtrinsicV4, Sr25519Signature } from '../interfaces/extrinsics/index.js'; +import type { GeneralExtrinsic } from '../index.types.js'; +import type { EcdsaSignature, Ed25519Signature, ExtrinsicSignatureV5, ExtrinsicUnknown, ExtrinsicV5, Sr25519Signature } from '../interfaces/extrinsics/index.js'; import type { FunctionMetadataLatest } from '../interfaces/metadata/index.js'; import type { Address, Call, CodecHash, Hash } from '../interfaces/runtime/index.js'; import type { MultiLocation } from '../interfaces/types.js'; import type { CallBase, ExtrinsicPayloadValue, ICompact, IExtrinsic, IKeyringPair, INumber, Registry, SignatureOptions } from '../types/index.js'; import type { GenericExtrinsicEra } from './ExtrinsicEra.js'; -import type { ExtrinsicValueV4 } from './v4/Extrinsic.js'; +import type { Preamble } from './types.js'; +import type { ExtrinsicValueV5 } from './v5/Extrinsic.js'; import { AbstractBase } from '@polkadot/types-codec'; import { compactAddLength, compactFromU8a, compactToU8a, isHex, isU8a, objectProperty, objectSpread, u8aConcat, u8aToHex, u8aToU8a } from '@polkadot/util'; -import { EXTRINSIC_VERSION as LATEST_EXTRINSIC_VERSION } from './v4/Extrinsic.js'; -import { BIT_SIGNED, BIT_UNSIGNED, DEFAULT_VERSION, UNMASK_VERSION } from './constants.js'; +import { BARE_EXTRINSIC, BIT_SIGNED, BIT_UNSIGNED, DEFAULT_PREAMBLE, GENERAL_EXTRINSIC, LATEST_EXTRINSIC_VERSION, LOWEST_SUPPORTED_EXTRINSIC_FORMAT_VERSION, SIGNED_EXTRINSIC, TYPE_MASK, VERSION_MASK } from './constants.js'; interface CreateOptions { version?: number; + preamble?: Preamble; } // NOTE The following 2 types, as well as the VERSION structure and the latest export // is to be changed with the addition of a new extrinsic version -type ExtrinsicVx = ExtrinsicV4; -type ExtrinsicValue = ExtrinsicValueV4; +type ExtrinsicVx = ExtrinsicV5 | GeneralExtrinsic; +type ExtrinsicValue = ExtrinsicValueV5; const VERSIONS = [ 'ExtrinsicUnknown', // v0 is unknown 'ExtrinsicUnknown', 'ExtrinsicUnknown', 'ExtrinsicUnknown', - 'ExtrinsicV4' + 'ExtrinsicV4', + 'ExtrinsicV5' ]; +const PREAMBLE = { + bare: 'ExtrinsicV5', + general: 'GeneralExtrinsic', + signed: 'ExtrinsicV5' +}; + +const PreambleMask = { + bare: BARE_EXTRINSIC, + general: GENERAL_EXTRINSIC, + signed: SIGNED_EXTRINSIC +}; + +const preambleUnMask: Record = { + 0: 'bare', + // eslint-disable-next-line sort-keys + 64: 'general', + // eslint-disable-next-line sort-keys + 128: 'signed' +}; + export { LATEST_EXTRINSIC_VERSION }; /** @internal */ -function newFromValue (registry: Registry, value: any, version: number): ExtrinsicVx | ExtrinsicUnknown { +function newFromValue (registry: Registry, value: any, version: number, preamble: Preamble): ExtrinsicVx | ExtrinsicUnknown { if (value instanceof GenericExtrinsic) { return value.unwrap(); } const isSigned = (version & BIT_SIGNED) === BIT_SIGNED; - const type = VERSIONS[version & UNMASK_VERSION] || VERSIONS[0]; + const type = (version & VERSION_MASK) === 5 ? PREAMBLE[preamble] : VERSIONS[version & VERSION_MASK] || VERSIONS[0]; // we cast here since the VERSION definition is incredibly broad - we don't have a // slice for "only add extrinsic types", and more string definitions become unwieldy @@ -52,20 +75,20 @@ function newFromValue (registry: Registry, value: any, version: number): Extrins } /** @internal */ -function decodeExtrinsic (registry: Registry, value?: GenericExtrinsic | ExtrinsicValue | AnyU8a | Call, version: number = DEFAULT_VERSION): ExtrinsicVx | ExtrinsicUnknown { +function decodeExtrinsic (registry: Registry, value?: GenericExtrinsic | ExtrinsicValue | AnyU8a | Call, version: number = LOWEST_SUPPORTED_EXTRINSIC_FORMAT_VERSION, preamble: Preamble = DEFAULT_PREAMBLE): ExtrinsicVx | ExtrinsicUnknown { if (isU8a(value) || Array.isArray(value) || isHex(value)) { - return decodeU8a(registry, u8aToU8a(value), version); + return decodeU8a(registry, u8aToU8a(value), version, preamble); } else if (value instanceof registry.createClassUnsafe('Call')) { - return newFromValue(registry, { method: value }, version); + return newFromValue(registry, { method: value }, version, preamble); } - return newFromValue(registry, value, version); + return newFromValue(registry, value, version, preamble); } /** @internal */ -function decodeU8a (registry: Registry, value: Uint8Array, version: number): ExtrinsicVx | ExtrinsicUnknown { +function decodeU8a (registry: Registry, value: Uint8Array, version: number, preamble: Preamble): ExtrinsicVx | ExtrinsicUnknown { if (!value.length) { - return newFromValue(registry, new Uint8Array(), version); + return newFromValue(registry, new Uint8Array(), version, preamble); } const [offset, length] = compactFromU8a(value); @@ -76,22 +99,41 @@ function decodeU8a (registry: Registry, value: Uint8Array, version: number): Ext } const data = value.subarray(offset, total); + const unmaskedPreamble = data[0] & TYPE_MASK; - return newFromValue(registry, data.subarray(1), data[0]); + if (preambleUnMask[`${unmaskedPreamble}`] === 'general') { + // NOTE: GeneralExtrinsic needs to have the full data to validate the preamble and version + return newFromValue(registry, value, data[0], preambleUnMask[`${unmaskedPreamble}`] || preamble); + } else { + return newFromValue(registry, data.subarray(1), data[0], preambleUnMask[`${unmaskedPreamble}`] || preamble); + } } abstract class ExtrinsicBase extends AbstractBase { - constructor (registry: Registry, value: ExtrinsicV4 | ExtrinsicUnknown, initialU8aLength?: number) { + readonly #preamble: Preamble; + + constructor (registry: Registry, value: ExtrinsicVx | ExtrinsicUnknown, initialU8aLength?: number, preamble?: Preamble) { super(registry, value, initialU8aLength); const signKeys = Object.keys(registry.getSignedExtensionTypes()); - const getter = (key: string) => this.inner.signature[key as 'signer']; - // This is on the abstract class, ensuring that hasOwnProperty operates - // correctly, i.e. it needs to be on the base class exposing it - for (let i = 0, count = signKeys.length; i < count; i++) { - objectProperty(this, signKeys[i], getter); + if (this.version === 5 && preamble !== 'general') { + const getter = (key: string) => (this.inner.signature as unknown as ExtrinsicSignatureV5)[key as 'signer']; + + // This is on the abstract class, ensuring that hasOwnProperty operates + // correctly, i.e. it needs to be on the base class exposing it + for (let i = 0, count = signKeys.length; i < count; i++) { + objectProperty(this, signKeys[i], getter); + } } + + const unmaskedPreamble = this.type & TYPE_MASK; + + this.#preamble = preamble || preambleUnMask[`${unmaskedPreamble}`]; + } + + public isGeneral () { + return this.#preamble === 'general'; } /** @@ -126,7 +168,9 @@ abstract class ExtrinsicBase extends AbstractBase extends AbstractBase extends AbstractBase { - return this.inner.signature.nonce; + return this.isGeneral() + ? (this.inner as unknown as GeneralExtrinsic).nonce + : (this.inner.signature as unknown as ExtrinsicSignatureV5).nonce; } /** * @description The actual [[EcdsaSignature]], [[Ed25519Signature]] or [[Sr25519Signature]] */ public get signature (): EcdsaSignature | Ed25519Signature | Sr25519Signature { - return this.inner.signature.signature; + if (this.isGeneral()) { + throw new Error('Extrinsic: GeneralExtrinsic does not have signature implemented'); + } + + return (this.inner.signature as unknown as ExtrinsicSignatureV5).signature; } /** * @description The [[Address]] that signed */ public get signer (): Address { - return this.inner.signature.signer; + if (this.isGeneral()) { + throw new Error('Extrinsic: GeneralExtrinsic does not have signer implemented'); + } + + return (this.inner.signature as unknown as ExtrinsicSignatureV5).signer; } /** * @description Forwards compat */ public get tip (): ICompact { - return this.inner.signature.tip; + return this.isGeneral() + ? (this.inner as unknown as GeneralExtrinsic).tip + : (this.inner.signature as unknown as ExtrinsicSignatureV5).tip; } /** * @description Forward compat */ public get assetId (): IOption { - return this.inner.signature.assetId; + return this.isGeneral() + ? (this.inner as unknown as GeneralExtrinsic).assetId + : (this.inner.signature as unknown as ExtrinsicSignatureV5).assetId; } /** * @description Forward compat */ public get metadataHash (): IOption { - return this.inner.signature.metadataHash; + return this.isGeneral() + ? (this.inner as unknown as GeneralExtrinsic).metadataHash + : (this.inner.signature as unknown as ExtrinsicSignatureV5).metadataHash; } /** * @description Forward compat */ public get mode (): INumber { - return this.inner.signature.mode; + return this.isGeneral() + ? (this.inner as unknown as GeneralExtrinsic).mode + : (this.inner.signature as unknown as ExtrinsicSignatureV5).mode; } /** @@ -228,7 +292,11 @@ abstract class ExtrinsicBase extends AbstractBase extends ExtrinsicBa static LATEST_EXTRINSIC_VERSION = LATEST_EXTRINSIC_VERSION; - constructor (registry: Registry, value?: GenericExtrinsic | ExtrinsicValue | AnyU8a | Call, { version }: CreateOptions = {}) { - super(registry, decodeExtrinsic(registry, value, version)); + constructor (registry: Registry, value?: GenericExtrinsic | ExtrinsicValue | AnyU8a | Call, { preamble, version }: CreateOptions = {}) { + super(registry, decodeExtrinsic(registry, value, version || registry.metadata.extrinsic.version?.toNumber(), preamble), undefined, preamble); } /** diff --git a/packages/types/src/extrinsic/ExtrinsicPayload.ts b/packages/types/src/extrinsic/ExtrinsicPayload.ts index c5b38fbe25ea..a088caddadba 100644 --- a/packages/types/src/extrinsic/ExtrinsicPayload.ts +++ b/packages/types/src/extrinsic/ExtrinsicPayload.ts @@ -5,37 +5,49 @@ import type { Bytes } from '@polkadot/types-codec'; import type { AnyJson, BareOpts, Registry } from '@polkadot/types-codec/types'; import type { HexString } from '@polkadot/util/types'; import type { BlockHash } from '../interfaces/chain/index.js'; -import type { ExtrinsicPayloadV4 } from '../interfaces/extrinsics/index.js'; +import type { ExtrinsicPayloadV5 } from '../interfaces/extrinsics/index.js'; import type { Hash, MultiLocation } from '../interfaces/types.js'; import type { ExtrinsicPayloadValue, ICompact, IKeyringPair, INumber, IOption } from '../types/index.js'; import type { GenericExtrinsicEra } from './ExtrinsicEra.js'; +import type { Preamble } from './types.js'; import { AbstractBase } from '@polkadot/types-codec'; import { hexToU8a, isHex, u8aToHex } from '@polkadot/util'; -import { DEFAULT_VERSION } from './constants.js'; +import { DEFAULT_PREAMBLE, LATEST_EXTRINSIC_VERSION } from './constants.js'; interface ExtrinsicPayloadOptions { version?: number; + preamble?: Preamble; } // all our known types that can be returned -type ExtrinsicPayloadVx = ExtrinsicPayloadV4; +type ExtrinsicPayloadVx = ExtrinsicPayloadV5; const VERSIONS = [ 'ExtrinsicPayloadUnknown', // v0 is unknown 'ExtrinsicPayloadUnknown', 'ExtrinsicPayloadUnknown', 'ExtrinsicPayloadUnknown', - 'ExtrinsicPayloadV4' + 'ExtrinsicPayloadV4', + 'ExtrinsicPayloadV5' ]; +const PREAMBLES = { + bare: 'ExtrinsicPayloadV5', + // Not supported yet + general: 'ExtrinsicPayloadV5', + signed: 'ExtrinsicPayloadV5' +}; + /** @internal */ -function decodeExtrinsicPayload (registry: Registry, value?: GenericExtrinsicPayload | ExtrinsicPayloadValue | Uint8Array | string, version: number = DEFAULT_VERSION): ExtrinsicPayloadVx { +function decodeExtrinsicPayload (registry: Registry, value?: GenericExtrinsicPayload | ExtrinsicPayloadValue | Uint8Array | string, version = LATEST_EXTRINSIC_VERSION, preamble: Preamble = DEFAULT_PREAMBLE): ExtrinsicPayloadVx { if (value instanceof GenericExtrinsicPayload) { return value.unwrap(); } + const extVersion = version === 5 ? PREAMBLES[preamble] : VERSIONS[version] || VERSIONS[0]; + /** * HACK: In order to change the assetId from `number | object` to HexString (While maintaining the true type ie Option), * to allow for easier generalization of the SignerPayloadJSON interface the below check is necessary. The ExtrinsicPayloadV4 class does not like @@ -51,10 +63,10 @@ function decodeExtrinsicPayload (registry: Registry, value?: GenericExtrinsicPay assetId: registry.createType('TAssetConversion', hexToU8a((value as ExtrinsicPayloadValue).assetId)).toJSON() }; - return registry.createTypeUnsafe(VERSIONS[version] || VERSIONS[0], [adjustedPayload, { version }]); + return registry.createTypeUnsafe(extVersion, [adjustedPayload, { version }]); } - return registry.createTypeUnsafe(VERSIONS[version] || VERSIONS[0], [value, { version }]); + return registry.createTypeUnsafe(extVersion, [value, { version }]); } /** @@ -64,8 +76,8 @@ function decodeExtrinsicPayload (registry: Registry, value?: GenericExtrinsicPay * on the contents included */ export class GenericExtrinsicPayload extends AbstractBase { - constructor (registry: Registry, value?: Partial | Uint8Array | string, { version }: ExtrinsicPayloadOptions = {}) { - super(registry, decodeExtrinsicPayload(registry, value as ExtrinsicPayloadValue, version)); + constructor (registry: Registry, value?: Partial | Uint8Array | string, { preamble, version }: ExtrinsicPayloadOptions = {}) { + super(registry, decodeExtrinsicPayload(registry, value as ExtrinsicPayloadValue, version, preamble)); } /** diff --git a/packages/types/src/extrinsic/constants.ts b/packages/types/src/extrinsic/constants.ts index bf827ea09795..19811962054a 100644 --- a/packages/types/src/extrinsic/constants.ts +++ b/packages/types/src/extrinsic/constants.ts @@ -7,8 +7,23 @@ export const BIT_UNSIGNED = 0; export const EMPTY_U8A = new Uint8Array(); -export const DEFAULT_VERSION = 4; - export const IMMORTAL_ERA = new Uint8Array([0]); export const UNMASK_VERSION = 0b01111111; + +export const DEFAULT_PREAMBLE = 'bare'; + +// Latest extrinsic version is v5, which has backwards compatibility for v4 signed extrinsics +export const LATEST_EXTRINSIC_VERSION = 5; + +export const VERSION_MASK = 0b00111111; + +export const TYPE_MASK = 0b11000000; + +export const BARE_EXTRINSIC = 0b00000000; + +export const SIGNED_EXTRINSIC = 0b10000000; + +export const GENERAL_EXTRINSIC = 0b01000000; + +export const LOWEST_SUPPORTED_EXTRINSIC_FORMAT_VERSION = 4; diff --git a/packages/types/src/extrinsic/index.ts b/packages/types/src/extrinsic/index.ts index 66813951d39b..14689bd86ef6 100644 --- a/packages/types/src/extrinsic/index.ts +++ b/packages/types/src/extrinsic/index.ts @@ -11,3 +11,4 @@ export { GenericSignerPayload } from './SignerPayload.js'; // all starred export * from './v4/index.js'; +export * from './v5/index.js'; diff --git a/packages/types/src/extrinsic/types.ts b/packages/types/src/extrinsic/types.ts index c0a04bee5794..c468dc8582d2 100644 --- a/packages/types/src/extrinsic/types.ts +++ b/packages/types/src/extrinsic/types.ts @@ -21,3 +21,5 @@ export interface ExtrinsicExtraValue { nonce?: AnyNumber; tip?: AnyNumber; } + +export type Preamble = 'signed' | 'bare' | 'general'; diff --git a/packages/types/src/extrinsic/util.ts b/packages/types/src/extrinsic/util.ts index 84a5361b19be..476e8404f62f 100644 --- a/packages/types/src/extrinsic/util.ts +++ b/packages/types/src/extrinsic/util.ts @@ -13,3 +13,16 @@ export function sign (registry: Registry, signerPair: IKeyringPair, u8a: Uint8Ar return signerPair.sign(encoded, options); } + +// a helper function for both types of payloads, Raw and metadata-known +export function signV5 (registry: Registry, signerPair: IKeyringPair, u8a: Uint8Array, options?: SignOptions): Uint8Array { + const encoded = registry.hash(u8a); + + return signerPair.sign(encoded, options); +} + +export function signGeneral (registry: Registry, u8a: Uint8Array): Uint8Array { + const encoded = registry.hash(u8a); + + return encoded; +} diff --git a/packages/types/src/extrinsic/v5/Extrinsic.spec.ts b/packages/types/src/extrinsic/v5/Extrinsic.spec.ts new file mode 100644 index 000000000000..8239b4b1beb0 --- /dev/null +++ b/packages/types/src/extrinsic/v5/Extrinsic.spec.ts @@ -0,0 +1,87 @@ +// Copyright 2017-2024 @polkadot/types authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/// + +import { createTestPairs } from '@polkadot/keyring/testingPairs'; +import rpcMetadata from '@polkadot/types-support/metadata/static-substrate'; +import { BN } from '@polkadot/util'; + +import { TypeRegistry } from '../../create/index.js'; +import { decorateExtrinsics, Metadata } from '../../metadata/index.js'; +import { GenericExtrinsicV5 as Extrinsic } from './index.js'; + +const registry = new TypeRegistry(); +const metadata = new Metadata(registry, rpcMetadata); +const keyring = createTestPairs({ type: 'ed25519' }, false); + +registry.setMetadata(metadata); + +const tx = decorateExtrinsics(registry, metadata.asLatest, metadata.version); + +describe('ExtrinsicV5', (): void => { + it('constructs a sane Uint8Array (default)', (): void => { + const xt = new Extrinsic(registry); + + // expect(`${xt.method.section}${xt.method.method}`).toEqual('system.fillBlock'); + expect(`${xt.method.section}.${xt.method.method}`).toEqual('system.remark'); + + expect(xt.toU8a()).toEqual(new Uint8Array([ + 0, 0, // index + // 0, 0, 0, 0 // fillBlock, Perbill + 0 // remark, Vec + ])); + }); + + it('creates a unsigned extrinsic', (): void => { + expect( + new Extrinsic( + registry, + tx['balances']['transferAllowDeath'](keyring.bob.publicKey, 6969n) + ).toHex() + ).toEqual( + '0x' + + '0600' + // balance.transferAllowDeath + '00' + + 'd7568e5f0a7eda67a82691ff379ac4bba4f9c9b859fe779b5d46363b61ad2db9' + + 'e56c' + ); + }); + + it('creates a signed extrinsic', (): void => { + expect( + new Extrinsic( + registry, + tx['balances']['transferAllowDeath'](keyring.bob.publicKey, 6969n) + ).sign(keyring.alice, { + blockHash: '0xec7afaf1cca720ce88c1d1b689d81f0583cc15a97d621cf046dd9abf605ef22f', + genesisHash: '0xdcd1346701ca8396496e52aa2785b1748deb6db09551b72159dcb3e08991025b', + mode: 0, + nonce: 1, + runtimeVersion: { + apis: [], + authoringVersion: new BN(123), + implName: 'test', + implVersion: new BN(123), + specName: 'test', + specVersion: new BN(123), + transactionVersion: new BN(123) + }, + tip: 2 + }).toHex() + ).toEqual( + '0x' + + '00' + + 'd172a74cda4c865912c32ba0a80a57ae69abae410e5ccb59dee84e2f4432db4f' + + '00' + // ed25519 + '84181ebef350cc212e70e042b6ebcd33ca955bf9497711a64aa7c64e2b8c2732' + + 'ab837715364eab7be5cc76f74eefa36d3ba9ee698264ed28a286c8360fc9aa0c' + + '00' + // TransactionExtension version + '0004080000' + // era. nonce, tip, mode + '0600' + + '00' + + 'd7568e5f0a7eda67a82691ff379ac4bba4f9c9b859fe779b5d46363b61ad2db9' + + 'e56c' + ); + }); +}); diff --git a/packages/types/src/extrinsic/v5/Extrinsic.ts b/packages/types/src/extrinsic/v5/Extrinsic.ts new file mode 100644 index 000000000000..473732ce58af --- /dev/null +++ b/packages/types/src/extrinsic/v5/Extrinsic.ts @@ -0,0 +1,116 @@ +// Copyright 2017-2024 @polkadot/types authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { HexString } from '@polkadot/util/types'; +import type { ExtrinsicSignatureV5 } from '../../interfaces/extrinsics/index.js'; +import type { Address, Call } from '../../interfaces/runtime/index.js'; +import type { ExtrinsicPayloadValue, IExtrinsicV5Impl, IKeyringPair, Registry, SignatureOptions } from '../../types/index.js'; +import type { ExtrinsicOptions, Preamble } from '../types.js'; + +import { Struct } from '@polkadot/types-codec'; +import { isU8a } from '@polkadot/util'; + +export const EXTRINSIC_VERSION = 5; + +export interface ExtrinsicValueV5 { + method?: Call; + signature?: ExtrinsicSignatureV5; +} + +/** + * @name GenericExtrinsicV5 + * @description + * The fourth generation of compact extrinsics + */ +export class GenericExtrinsicV5 extends Struct implements IExtrinsicV5Impl { + constructor (registry: Registry, value?: Uint8Array | ExtrinsicValueV5 | Call, { isSigned }: Partial = {}) { + super(registry, { + signature: 'ExtrinsicSignatureV5', + // eslint-disable-next-line sort-keys + method: 'Call' + }, GenericExtrinsicV5.decodeExtrinsic(registry, value, isSigned)); + } + + /** @internal */ + public static decodeExtrinsic (registry: Registry, value?: Call | Uint8Array | ExtrinsicValueV5, isSigned = false): ExtrinsicValueV5 { + if (value instanceof GenericExtrinsicV5) { + return value; + } else if (value instanceof registry.createClassUnsafe('Call')) { + return { method: value }; + } else if (isU8a(value)) { + // here we decode manually since we need to pull through the version information + const signature = registry.createTypeUnsafe('ExtrinsicSignatureV5', [value, { isSigned }]); + // We add 2 here since the length of the TransactionExtension Version needs to be accounted for + const method = registry.createTypeUnsafe('Call', [value.subarray(signature.encodedLength)]); + + return { + method, + signature + }; + } + + return value || {}; + } + + /** + * @description The length of the value when encoded as a Uint8Array + */ + public override get encodedLength (): number { + return this.toU8a().length; + } + + /** + * @description The [[Call]] this extrinsic wraps + */ + public get method (): Call { + return this.getT('method'); + } + + /** + * @description The [[ExtrinsicSignatureV5]] + */ + public get signature (): ExtrinsicSignatureV5 { + return this.getT('signature'); + } + + /** + * @description The version for the signature + */ + public get version (): number { + return EXTRINSIC_VERSION; + } + + /** + * @description The [[Preamble]] for the extrinsic + */ + public get preamble (): Preamble { + return this.getT('preamble'); + } + + /** + * @description Add an [[ExtrinsicSignatureV5]] to the extrinsic (already generated) + */ + public addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | HexString, payload: ExtrinsicPayloadValue | Uint8Array | HexString): GenericExtrinsicV5 { + this.signature.addSignature(signer, signature, payload); + + return this; + } + + /** + * @description Sign the extrinsic with a specific keypair + */ + public sign (account: IKeyringPair, options: SignatureOptions): GenericExtrinsicV5 { + this.signature.sign(this.method, account, options); + + return this; + } + + /** + * @describe Adds a fake signature to the extrinsic + */ + public signFake (signer: Address | Uint8Array | string, options: SignatureOptions): GenericExtrinsicV5 { + this.signature.signFake(this.method, signer, options); + + return this; + } +} diff --git a/packages/types/src/extrinsic/v5/ExtrinsicPayload.spec.ts b/packages/types/src/extrinsic/v5/ExtrinsicPayload.spec.ts new file mode 100644 index 000000000000..e2854568f244 --- /dev/null +++ b/packages/types/src/extrinsic/v5/ExtrinsicPayload.spec.ts @@ -0,0 +1,39 @@ +// Copyright 2017-2024 @polkadot/types authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/// + +import rpcMetadata from '@polkadot/types-support/metadata/static-substrate'; + +import { TypeRegistry } from '../../create/index.js'; +import { decorateExtrinsics, Metadata } from '../../metadata/index.js'; +import { GenericExtrinsicPayloadV5 as ExtrinsicPayload } from './index.js'; + +const registry = new TypeRegistry(); +const metadata = new Metadata(registry, rpcMetadata); + +registry.setMetadata(metadata); + +const tx = decorateExtrinsics(registry, metadata.asLatest, metadata.version); + +describe('ExtrinsicPayload', (): void => { + it('has a sane inspect', (): void => { + // we don't expect this to fail, however it is actually a good + // reference for the ordering in base Substrate + expect(new ExtrinsicPayload(registry, { method: tx['timestamp']['set'](0).toHex() } as never).inspect()).toEqual({ + inner: [ + { name: 'method', outer: [new Uint8Array([3, 0, 0])] }, + { inner: undefined, name: 'era', outer: [new Uint8Array([0]), new Uint8Array([0])] }, + { name: 'nonce', outer: [new Uint8Array([0])] }, + { name: 'tip', outer: [new Uint8Array([0])] }, + { name: 'assetId', outer: [new Uint8Array([0])] }, + { name: 'mode', outer: [new Uint8Array([0])] }, + { name: 'specVersion', outer: [new Uint8Array([0, 0, 0, 0])] }, + { name: 'transactionVersion', outer: [new Uint8Array([0, 0, 0, 0])] }, + { name: 'genesisHash', outer: [new Uint8Array(32)] }, + { name: 'blockHash', outer: [new Uint8Array(32)] }, + { name: 'metadataHash', outer: [new Uint8Array([0])] } + ] + }); + }); +}); diff --git a/packages/types/src/extrinsic/v5/ExtrinsicPayload.ts b/packages/types/src/extrinsic/v5/ExtrinsicPayload.ts new file mode 100644 index 000000000000..1d2222b3eb0f --- /dev/null +++ b/packages/types/src/extrinsic/v5/ExtrinsicPayload.ts @@ -0,0 +1,130 @@ +// Copyright 2017-2024 @polkadot/types authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { SignOptions } from '@polkadot/keyring/types'; +import type { Hash, MultiLocation } from '@polkadot/types/interfaces'; +import type { Bytes } from '@polkadot/types-codec'; +import type { Inspect, Registry } from '@polkadot/types-codec/types'; +import type { HexString } from '@polkadot/util/types'; +import type { BlockHash } from '../../interfaces/chain/index.js'; +import type { ExtrinsicEra } from '../../interfaces/extrinsics/index.js'; +import type { ExtrinsicPayloadValue, ICompact, IKeyringPair, INumber, IOption } from '../../types/index.js'; + +import { Enum, Struct } from '@polkadot/types-codec'; +import { objectSpread } from '@polkadot/util'; + +import { signV5 } from '../util.js'; + +/** + * @name GenericExtrinsicPayloadV5 + * @description + * A signing payload for an [[Extrinsic]]. For the final encoding, it is + * variable length based on the contents included + */ +export class GenericExtrinsicPayloadV5 extends Struct { + #signOptions: SignOptions; + + constructor (registry: Registry, value?: ExtrinsicPayloadValue | Uint8Array | HexString) { + super(registry, objectSpread( + { method: 'Bytes' }, + registry.getSignedExtensionTypes(), + registry.getSignedExtensionExtra() + ), value); + + // Do detection for the type of extrinsic, in the case of MultiSignature + // this is an enum, in the case of AnySignature, this is a Hash only + // (which may be 64 or 65 bytes) + this.#signOptions = { + withType: registry.createTypeUnsafe('ExtrinsicSignature', []) instanceof Enum + }; + } + + /** + * @description Returns a breakdown of the hex encoding for this Codec + */ + public override inspect (): Inspect { + return super.inspect({ method: true }); + } + + /** + * @description The block [[BlockHash]] the signature applies to (mortal/immortal) + */ + public get blockHash (): BlockHash { + return this.getT('blockHash'); + } + + /** + * @description The [[ExtrinsicEra]] + */ + public get era (): ExtrinsicEra { + return this.getT('era'); + } + + /** + * @description The genesis [[BlockHash]] the signature applies to (mortal/immortal) + */ + public get genesisHash (): BlockHash { + return this.getT('genesisHash'); + } + + /** + * @description The [[Bytes]] contained in the payload + */ + public get method (): Bytes { + return this.getT('method'); + } + + /** + * @description The [[Index]] + */ + public get nonce (): ICompact { + return this.getT('nonce'); + } + + /** + * @description The specVersion for this signature + */ + public get specVersion (): INumber { + return this.getT('specVersion'); + } + + /** + * @description The tip [[Balance]] + */ + public get tip (): ICompact { + return this.getT('tip'); + } + + /** + * @description The transactionVersion for this signature + */ + public get transactionVersion (): INumber { + return this.getT('transactionVersion'); + } + + /** + * @description The (optional) asset id for this signature for chains that support transaction fees in assets + */ + public get assetId (): IOption { + return this.getT('assetId'); + } + + /** + * @description The (optional) metadataHash proof for the CheckMetadataHash TransactionExtension + */ + public get metadataHash (): IOption { + return this.getT('metadataHash'); + } + + /** + * @description Sign the payload with the keypair + */ + public sign (signerPair: IKeyringPair): Uint8Array { + // NOTE The `toU8a({ method: true })` argument is absolutely critical, we + // don't want the method (Bytes) to have the length prefix included. This + // means that the data-as-signed is un-decodable, but is also doesn't need + // the extra information, only the pure data (and is not decoded) ... + // The same applies to V1..V3, if we have a V6, carry this comment + return signV5(this.registry, signerPair, this.toU8a({ method: true }), this.#signOptions); + } +} diff --git a/packages/types/src/extrinsic/v5/ExtrinsicSignature.spec.ts b/packages/types/src/extrinsic/v5/ExtrinsicSignature.spec.ts new file mode 100644 index 000000000000..7fd7d58f9d70 --- /dev/null +++ b/packages/types/src/extrinsic/v5/ExtrinsicSignature.spec.ts @@ -0,0 +1,165 @@ +// Copyright 2017-2024 @polkadot/types authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/// + +import { createTestPairs } from '@polkadot/keyring/testingPairs'; +import metadataStatic from '@polkadot/types-support/metadata/static-substrate'; +import { BN_ZERO } from '@polkadot/util'; + +import { TypeRegistry } from '../../create/index.js'; +import { Metadata } from '../../metadata/index.js'; +import { GenericExtrinsicSignatureV5 as ExtrinsicSignature } from './index.js'; + +const signOptions = { + blockHash: '0x1234567890123456789012345678901234567890123456789012345678901234', + genesisHash: '0x1234567890123456789012345678901234567890123456789012345678901234', + nonce: '0x69', + runtimeVersion: { + apis: [], + authoringVersion: BN_ZERO, + implName: String('test'), + implVersion: BN_ZERO, + specName: String('test'), + specVersion: BN_ZERO, + transactionVersion: BN_ZERO + } +}; + +describe('ExtrinsicSignatureV4', (): void => { + const pairs = createTestPairs({ type: 'ed25519' }); + + it('encodes to a sane Uint8Array (default)', (): void => { + const registry = new TypeRegistry(); + + const u8a = new Uint8Array([ + // signer as an AccountIndex + 0x01, 0x08, // 4 << 2 + // signature type + 0x01, + // 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, + 0x00, // TransactionExtension version + // extra stuff + 0x00, // immortal, + 0x04, // nonce, compact + 0x08 // tip, compact + ]); + + expect( + new ExtrinsicSignature(registry, u8a, { isSigned: true }).toU8a() + ).toEqual(u8a); + }); + + it('fake signs default', (): void => { + const registry = new TypeRegistry(); + const metadata = new Metadata(registry, metadataStatic); + + registry.setMetadata(metadata); + + expect( + new ExtrinsicSignature(registry).signFake( + registry.createType('Call'), + pairs.alice.publicKey, + signOptions + ).toHex() + ).toEqual( + '0x' + + '00' + // MultiAddress + 'd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d' + + '01' + + '0101010101010101010101010101010101010101010101010101010101010101' + + '0101010101010101010101010101010101010101010101010101010101010101' + + '00' + // TransactionExtension version + '00a5010000' + + '00' // Mode + ); + }); + + it('fake signs default (AccountId address)', (): void => { + const registry = new TypeRegistry(); + const metadata = new Metadata(registry, metadataStatic); + + registry.setMetadata(metadata); + registry.register({ + Address: 'AccountId', + ExtrinsicSignature: 'AnySignature' + }); + + expect( + new ExtrinsicSignature(registry).signFake( + registry.createType('Call'), + pairs.alice.address, + signOptions + ).toHex() + ).toEqual( + '0x' + + // Address = AccountId, no prefix + 'd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d' + + // This is a prefix-less signature, anySignture as opposed to Multi above + '0101010101010101010101010101010101010101010101010101010101010101' + + '0101010101010101010101010101010101010101010101010101010101010101' + + '00' + // TransactionExtension version + '00a5010000' + + '00' // mode + ); + }); + + it('fake signs with non-enum signature', (): void => { + const registry = new TypeRegistry(); + const metadata = new Metadata(registry, metadataStatic); + + registry.setMetadata(metadata); + registry.register({ + Address: 'AccountId', + ExtrinsicSignature: '[u8;65]' + }); + + expect( + new ExtrinsicSignature(registry).signFake( + registry.createType('Call'), + pairs.alice.address, + signOptions + ).toHex() + ).toEqual( + '0x' + + // Address = AccountId, no prefix + 'd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d' + + // 65 bytes here + '01' + + '0101010101010101010101010101010101010101010101010101010101010101' + + '0101010101010101010101010101010101010101010101010101010101010101' + + '00' + // TransactionExtension version + '00a5010000' + + '00' // mode + ); + }); + + it('injects a signature', (): void => { + const registry = new TypeRegistry(); + const metadata = new Metadata(registry, metadataStatic); + + registry.setMetadata(metadata); + + expect( + new ExtrinsicSignature(registry).addSignature( + pairs.alice.publicKey, + new Uint8Array(65).fill(1), + new Uint8Array(0) + ).toHex() + ).toEqual( + '0x' + + '00' + // MultiAddress + 'd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d' + + '01' + + '0101010101010101010101010101010101010101010101010101010101010101' + + '0101010101010101010101010101010101010101010101010101010101010101' + + '00' + // TransactionExtension version + '00000000' + + '00' // mode + ); + }); +}); diff --git a/packages/types/src/extrinsic/v5/ExtrinsicSignature.ts b/packages/types/src/extrinsic/v5/ExtrinsicSignature.ts new file mode 100644 index 000000000000..9191850e47c8 --- /dev/null +++ b/packages/types/src/extrinsic/v5/ExtrinsicSignature.ts @@ -0,0 +1,241 @@ +// Copyright 2017-2024 @polkadot/types authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { MultiLocation } from '@polkadot/types/interfaces'; +import type { HexString } from '@polkadot/util/types'; +import type { EcdsaSignature, Ed25519Signature, ExtrinsicEra, ExtrinsicSignature, Sr25519Signature } from '../../interfaces/extrinsics/index.js'; +import type { Address, Call, Hash } from '../../interfaces/runtime/index.js'; +import type { ExtrinsicPayloadValue, ICompact, IExtrinsicSignature, IKeyringPair, INumber, IOption, Registry, SignatureOptions } from '../../types/index.js'; +import type { ExtrinsicSignatureOptions } from '../types.js'; + +import { Struct } from '@polkadot/types-codec'; +import { isU8a, isUndefined, objectProperties, objectSpread, stringify, u8aToHex } from '@polkadot/util'; + +import { EMPTY_U8A, IMMORTAL_ERA } from '../constants.js'; +import { GenericExtrinsicPayloadV5 } from './ExtrinsicPayload.js'; + +// Ensure we have enough data for all types of signatures +const FAKE_SIGNATURE = new Uint8Array(256).fill(1); + +function toAddress (registry: Registry, address: Address | Uint8Array | string): Address { + return registry.createTypeUnsafe('Address', [isU8a(address) ? u8aToHex(address) : address]); +} + +/** + * @name GenericExtrinsicSignatureV5 + * @description + * A container for the [[Signature]] associated with a specific [[Extrinsic]] + */ +export class GenericExtrinsicSignatureV5 extends Struct implements IExtrinsicSignature { + #signKeys: string[]; + #transactionExtensionVersion: number; + + constructor (registry: Registry, value?: GenericExtrinsicSignatureV5 | Uint8Array, { isSigned }: ExtrinsicSignatureOptions = {}) { + const signTypes = registry.getSignedExtensionTypes(); + const signedVersion = registry.getTransactionExtensionVersion(); + + super( + registry, + objectSpread( + // eslint-disable-next-line sort-keys + { signer: 'Address', signature: 'ExtrinsicSignature', transactionExtensionVersion: 'u8' }, + signTypes + ), + GenericExtrinsicSignatureV5.decodeExtrinsicSignature(value, isSigned) + ); + + this.#transactionExtensionVersion = signedVersion; + this.#signKeys = Object.keys(signTypes); + + objectProperties(this, this.#signKeys, (k) => this.get(k)); + } + + /** @internal */ + public static decodeExtrinsicSignature (value?: GenericExtrinsicSignatureV5 | Uint8Array, isSigned = false): GenericExtrinsicSignatureV5 | Uint8Array { + if (!value) { + return EMPTY_U8A; + } else if (value instanceof GenericExtrinsicSignatureV5) { + return value; + } + + return isSigned + ? value + : EMPTY_U8A; + } + + /** + * @description The length of the value when encoded as a Uint8Array + */ + public override get encodedLength (): number { + return this.isSigned + ? super.encodedLength + : 0; + } + + /** + * @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.getT('era'); + } + + /** + * @description The [[Index]] for the signature + */ + public get nonce (): ICompact { + return this.getT('nonce'); + } + + /** + * @description The actual [[EcdsaSignature]], [[Ed25519Signature]] or [[Sr25519Signature]] + */ + public get signature (): EcdsaSignature | Ed25519Signature | Sr25519Signature { + // the second case here is when we don't have an enum signature, treat as raw + return (this.multiSignature.value || this.multiSignature) as Sr25519Signature; + } + + /** + * @description The raw [[ExtrinsicSignature]] + */ + public get multiSignature (): ExtrinsicSignature { + return this.getT('signature'); + } + + /** + * @description The [[Address]] that signed + */ + public get signer (): Address { + return this.getT('signer'); + } + + /** + * @description The [[Balance]] tip + */ + public get tip (): ICompact { + return this.getT('tip'); + } + + /** + * @description The [[u32]] or [[MultiLocation]] assetId + */ + public get assetId (): IOption { + return this.getT('assetId'); + } + + /** + * @description the [[u32]] mode + */ + public get mode (): INumber { + return this.getT('mode'); + } + + /** + * @description The (optional) [[Hash]] for the metadata proof + */ + public get metadataHash (): IOption { + return this.getT('metadataHash'); + } + + /** + * @description The [[u8]] for the TransactionExtension version + */ + public get transactionExtensionVersion (): INumber { + return this.getT('transactionExtensionVersion'); + } + + protected _injectSignature (signer: Address, signature: ExtrinsicSignature, payload: GenericExtrinsicPayloadV5): IExtrinsicSignature { + // use the fields exposed to guide the getters + for (let i = 0, count = this.#signKeys.length; i < count; i++) { + const k = this.#signKeys[i]; + const v = payload.get(k); + + if (k === 'transactionExtensionVersion') { + this.set(k, this.registry.createType('u8', this.#transactionExtensionVersion)); + } else if (!isUndefined(v)) { + this.set(k, v); + } + } + + // additional fields (exposed in struct itself) + this.set('signer', signer); + this.set('signature', signature); + + return this; + } + + /** + * @description Adds a raw signature + */ + public addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | HexString, payload: ExtrinsicPayloadValue | Uint8Array | HexString): IExtrinsicSignature { + return this._injectSignature( + toAddress(this.registry, signer), + this.registry.createTypeUnsafe('ExtrinsicSignature', [signature]), + new GenericExtrinsicPayloadV5(this.registry, payload) + ); + } + + /** + * @description Creates a payload from the supplied options + */ + public createPayload (method: Call, options: SignatureOptions): GenericExtrinsicPayloadV5 { + const { era, runtimeVersion: { specVersion, transactionVersion } } = options; + + return new GenericExtrinsicPayloadV5(this.registry, objectSpread({}, options, { + era: era || IMMORTAL_ERA, + method: method.toHex(), + specVersion, + transactionVersion + })); + } + + /** + * @description Generate a payload and applies the signature from a keypair + */ + public sign (method: Call, account: IKeyringPair, options: SignatureOptions): IExtrinsicSignature { + if (!account?.addressRaw) { + throw new Error(`Expected a valid keypair for signing, found ${stringify(account)}`); + } + + const payload = this.createPayload(method, options); + + return this._injectSignature( + toAddress(this.registry, account.addressRaw), + this.registry.createTypeUnsafe('ExtrinsicSignature', [payload.sign(account)]), + payload + ); + } + + /** + * @description Generate a payload and applies a fake signature + */ + public signFake (method: Call, address: Address | Uint8Array | string, options: SignatureOptions): IExtrinsicSignature { + if (!address) { + throw new Error(`Expected a valid address for signing, found ${stringify(address)}`); + } + + const payload = this.createPayload(method, options); + + return this._injectSignature( + toAddress(this.registry, address), + this.registry.createTypeUnsafe('ExtrinsicSignature', [FAKE_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 override toU8a (isBare?: boolean): Uint8Array { + return this.isSigned + ? super.toU8a(isBare) + : EMPTY_U8A; + } +} diff --git a/packages/types/src/extrinsic/v5/GeneralExtrinsic.spec.ts b/packages/types/src/extrinsic/v5/GeneralExtrinsic.spec.ts new file mode 100644 index 000000000000..2691e8c79ef7 --- /dev/null +++ b/packages/types/src/extrinsic/v5/GeneralExtrinsic.spec.ts @@ -0,0 +1,47 @@ +// Copyright 2017-2024 @polkadot/types authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/// + +import rpcMetadata from '@polkadot/types-support/metadata/static-substrate'; + +import { TypeRegistry } from '../../create/index.js'; +import { Metadata } from '../../metadata/index.js'; +import { GeneralExtrinsic } from './GeneralExtrinsic.js'; + +const registry = new TypeRegistry(); +const metadata = new Metadata(registry, rpcMetadata); + +registry.setMetadata(metadata); + +describe('GeneralExt', (): void => { + const extrinsic = '0xc44500650000000000060000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0700e40b5402'; + + it('Can decode a general extrinsic', (): void => { + const genExt = new GeneralExtrinsic(registry, extrinsic); + + expect(genExt.version).toEqual(5); + expect(genExt.transactionExtensionVersion.toNumber()).toEqual(0); + expect(genExt.method.toHuman()).toEqual({ args: { dest: { Id: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' }, value: '10,000,000,000' }, method: 'transferAllowDeath', section: 'balances' }); + expect(genExt.era.toHuman()).toEqual({ MortalEra: { period: '64', phase: '6' } }); + expect(genExt.tip.toNumber()).toEqual(0); + expect(genExt.mode.toNumber()).toEqual(0); + expect(genExt.assetId.toHuman()).toEqual(null); + expect(genExt.nonce.toNumber()).toEqual(0); + }); + + it('Can encode a general extrinsic', (): void => { + const payload = { + era: '0x6500', + method: '0x060000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0700e40b5402', + nonce: '0x00000000', + tip: '0x00000000000000000000000000000000', + transactionVersion: '0x00000002' + }; + const genExt = new GeneralExtrinsic(registry, { + payload + }); + + expect(genExt.toHex()).toEqual(extrinsic); + }); +}); diff --git a/packages/types/src/extrinsic/v5/GeneralExtrinsic.ts b/packages/types/src/extrinsic/v5/GeneralExtrinsic.ts new file mode 100644 index 000000000000..d1ba6b225618 --- /dev/null +++ b/packages/types/src/extrinsic/v5/GeneralExtrinsic.ts @@ -0,0 +1,208 @@ +// Copyright 2017-2024 @polkadot/types authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Call, ExtrinsicEra, Hash, MultiLocation } from '@polkadot/types/interfaces'; +import type { AnyNumber, AnyU8a, ICompact, IExtrinsicEra, INumber, IOption, Registry } from '@polkadot/types/types'; +import type { AnyTuple, IMethod } from '@polkadot/types-codec/types'; +import type { HexString } from '@polkadot/util/types'; + +import { Struct } from '@polkadot/types-codec'; +import { compactAddLength, compactFromU8a, isHex, isObject, isU8a, objectSpread, u8aConcat, u8aToHex, u8aToU8a } from '@polkadot/util'; + +import { EMPTY_U8A, UNMASK_VERSION } from '../constants.js'; + +interface TransactionExtensionValues { + era: AnyU8a | IExtrinsicEra; + nonce: AnyNumber; + tip: AnyNumber; + transactionVersion: AnyNumber; + assetId?: HexString; + mode?: AnyNumber; + metadataHash?: AnyU8a; +} + +interface GeneralExtrinsicPayloadValues extends TransactionExtensionValues { + method: AnyU8a | IMethod; +} + +interface GeneralExtrinsicValue { + payload?: GeneralExtrinsicPayloadValues; + transactionExtensionVersion?: number; +} + +function decodeU8a (u8a: Uint8Array) { + if (!u8a.length) { + return new Uint8Array(); + } + + const [offset, length] = compactFromU8a(u8a); + const total = offset + length.toNumber(); + + if (total > u8a.length) { + throw new Error(`Extrinsic: length less than remainder, expected at least ${total}, found ${u8a.length}`); + } + + const data = u8a.subarray(offset, total); + + // 69 denotes 0b01000101 which is the version and preamble for this Extrinsic + if (data[0] !== 69) { + throw new Error(`Extrinsic: incorrect version for General Transactions, expected 5, found ${data[0] & UNMASK_VERSION}`); + } + + return data.subarray(1); +} + +export class GeneralExtrinsic extends Struct { + #version: number; + #preamble: number; + + constructor (registry: Registry, value?: GeneralExtrinsicValue | Uint8Array | HexString, opt?: { version: number }) { + const extTypes = registry.getSignedExtensionTypes(); + + super(registry, objectSpread( + { + transactionExtensionVersion: 'u8' + }, + extTypes, + { + method: 'Call' + } + ), GeneralExtrinsic.decodeExtrinsic(registry, value)); + + this.#version = opt?.version || 0b00000101; + this.#preamble = 0b01000000; + } + + public static decodeExtrinsic (registry: Registry, value?: GeneralExtrinsicValue | Uint8Array | HexString) { + if (!value) { + return EMPTY_U8A; + } else if (value instanceof GeneralExtrinsic) { + return value; + } else if (isU8a(value) || Array.isArray(value) || isHex(value)) { + return decodeU8a(u8aToU8a(value)); + } else if (isObject(value)) { + const { payload, transactionExtensionVersion } = value; + + return objectSpread(payload || {}, { + transactionExtensionVersion: transactionExtensionVersion || registry.getTransactionExtensionVersion() + }); + } + + return {}; + } + + /** + * @description The length of the value when encoded as a Uint8Array + */ + public override get encodedLength (): number { + return super.encodedLength; + } + + /** + * @description The [[ExtrinsicEra]] + */ + public get era (): ExtrinsicEra { + return this.getT('era'); + } + + /** + * @description The [[Index]] + */ + public get nonce (): ICompact { + return this.getT('nonce'); + } + + /** + * @description The tip [[Balance]] + */ + public get tip (): ICompact { + return this.getT('tip'); + } + + /** + * @description The (optional) asset id for this signature for chains that support transaction fees in assets + */ + public get assetId (): IOption { + return this.getT('assetId'); + } + + /** + * @description The mode used for the CheckMetadataHash TransactionExtension + */ + public get mode (): INumber { + return this.getT('mode'); + } + + /** + * @description The (optional) [[Hash]] for the metadata proof + */ + public get metadataHash (): IOption { + return this.getT('metadataHash'); + } + + /** + * @description The version of the TransactionExtensions used in this extrinsic + */ + public get transactionExtensionVersion (): INumber { + return this.getT('transactionExtensionVersion'); + } + + /** + * @description The [[Call]] this extrinsic wraps + */ + public get method (): Call { + return this.getT('method'); + } + + /** + * @description The extrinsic's version + */ + public get version () { + return this.#version; + } + + /** + * @description The [[Preamble]] for the extrinsic + */ + public get preamble () { + return this.#preamble; + } + + public override toHex (isBare?: boolean): HexString { + return u8aToHex(this.toU8a(isBare)); + } + + public override toU8a (isBare?: boolean): Uint8Array { + return isBare + ? this.encode() + : compactAddLength(this.encode()); + } + + public override toRawType () { + return 'GeneralExt'; + } + + /** + * + * @description Returns an encoded GeneralExtrinsic + */ + public encode () { + return u8aConcat(new Uint8Array([this.version | this.preamble]), super.toU8a()); + } + + public signFake () { + throw new Error('Extrinsic: Type GeneralExtrinsic does not have signFake implemented'); + } + + public addSignature () { + throw new Error('Extrinsic: Type GeneralExtrinsic does not have addSignature implemented'); + } + + public sign () { + throw new Error('Extrinsic: Type GeneralExtrinsic does not have sign implemented'); + } + + public signature () { + throw new Error('Extrinsic: Type GeneralExtrinsic does not have the signature getter'); + } +} diff --git a/packages/types/src/extrinsic/v5/index.ts b/packages/types/src/extrinsic/v5/index.ts new file mode 100644 index 000000000000..63d44b65c146 --- /dev/null +++ b/packages/types/src/extrinsic/v5/index.ts @@ -0,0 +1,7 @@ +// Copyright 2017-2024 @polkadot/types authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export { GenericExtrinsicV5 } from './Extrinsic.js'; +export { GenericExtrinsicPayloadV5 } from './ExtrinsicPayload.js'; +export { GenericExtrinsicSignatureV5 } from './ExtrinsicSignature.js'; +export { GeneralExtrinsic } from './GeneralExtrinsic.js'; diff --git a/packages/types/src/interfaces/extrinsics/definitions.ts b/packages/types/src/interfaces/extrinsics/definitions.ts index dd3a381018a7..a0c78d17492f 100644 --- a/packages/types/src/interfaces/extrinsics/definitions.ts +++ b/packages/types/src/interfaces/extrinsics/definitions.ts @@ -18,6 +18,9 @@ export default { ExtrinsicSignatureV4: 'GenericExtrinsicSignatureV4', ExtrinsicUnknown: 'GenericExtrinsicUnknown', ExtrinsicPayloadUnknown: 'GenericExtrinsicPayloadUnknown', + ExtrinsicV5: 'GenericExtrinsicV5', + ExtrinsicPayloadV5: 'GenericExtrinsicPayloadV5', + ExtrinsicSignatureV5: 'GenericExtrinsicSignatureV5', // eras Era: 'ExtrinsicEra', diff --git a/packages/types/src/interfaces/extrinsics/types.ts b/packages/types/src/interfaces/extrinsics/types.ts index 016c520b2ee0..056443f08841 100644 --- a/packages/types/src/interfaces/extrinsics/types.ts +++ b/packages/types/src/interfaces/extrinsics/types.ts @@ -1,7 +1,7 @@ // Auto-generated via `yarn polkadot-types-from-defs`, do not edit /* eslint-disable */ -import type { GenericExtrinsic, GenericExtrinsicEra, GenericExtrinsicPayload, GenericExtrinsicPayloadUnknown, GenericExtrinsicPayloadV4, GenericExtrinsicSignatureV4, GenericExtrinsicUnknown, GenericExtrinsicV4, GenericImmortalEra, GenericMortalEra, GenericSignerPayload } from '@polkadot/types'; +import type { GenericExtrinsic, GenericExtrinsicEra, GenericExtrinsicPayload, GenericExtrinsicPayloadUnknown, GenericExtrinsicPayloadV4, GenericExtrinsicPayloadV5, GenericExtrinsicSignatureV4, GenericExtrinsicSignatureV5, GenericExtrinsicUnknown, GenericExtrinsicV4, GenericExtrinsicV5, GenericImmortalEra, GenericMortalEra, GenericSignerPayload } from '@polkadot/types'; import type { Enum, U8aFixed } from '@polkadot/types-codec'; import type { H512 } from '@polkadot/types/interfaces/runtime'; @@ -32,18 +32,27 @@ export interface ExtrinsicPayloadUnknown extends GenericExtrinsicPayloadUnknown /** @name ExtrinsicPayloadV4 */ export interface ExtrinsicPayloadV4 extends GenericExtrinsicPayloadV4 {} +/** @name ExtrinsicPayloadV5 */ +export interface ExtrinsicPayloadV5 extends GenericExtrinsicPayloadV5 {} + /** @name ExtrinsicSignature */ export interface ExtrinsicSignature extends MultiSignature {} /** @name ExtrinsicSignatureV4 */ export interface ExtrinsicSignatureV4 extends GenericExtrinsicSignatureV4 {} +/** @name ExtrinsicSignatureV5 */ +export interface ExtrinsicSignatureV5 extends GenericExtrinsicSignatureV5 {} + /** @name ExtrinsicUnknown */ export interface ExtrinsicUnknown extends GenericExtrinsicUnknown {} /** @name ExtrinsicV4 */ export interface ExtrinsicV4 extends GenericExtrinsicV4 {} +/** @name ExtrinsicV5 */ +export interface ExtrinsicV5 extends GenericExtrinsicV5 {} + /** @name ImmortalEra */ export interface ImmortalEra extends GenericImmortalEra {} diff --git a/packages/types/src/types/extrinsic.ts b/packages/types/src/types/extrinsic.ts index 6a0fe33cdd46..ed0546aef045 100644 --- a/packages/types/src/types/extrinsic.ts +++ b/packages/types/src/types/extrinsic.ts @@ -247,6 +247,12 @@ export interface IExtrinsicImpl extends IExtrinsicSignable { readonly version: number; } +export interface IExtrinsicV5Impl extends IExtrinsicSignable { + readonly method: Call; + readonly signature: IExtrinsicSignature; + readonly version: number; +} + export interface IExtrinsic extends IExtrinsicSignable>, ExtrinsicSignatureBase, IMethod { readonly length: number; readonly method: IMethod;