From 0d04d598d95529fc0d9046eef0deb5eef64d0f0a Mon Sep 17 00:00:00 2001 From: Arjun Porwal Date: Wed, 15 Oct 2025 11:20:43 +0530 Subject: [PATCH 1/5] feat: added ts interface and basic skeleton --- packages/api/src/submittable/createClass.ts | 70 +++++++++++++++++- packages/types/src/types/extrinsic.ts | 81 +++++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/packages/api/src/submittable/createClass.ts b/packages/api/src/submittable/createClass.ts index 30b6403357a..2f67784afd3 100644 --- a/packages/api/src/submittable/createClass.ts +++ b/packages/api/src/submittable/createClass.ts @@ -5,7 +5,7 @@ import type { Observable } from 'rxjs'; import type { Address, ApplyExtrinsicResult, Call, Extrinsic, ExtrinsicEra, ExtrinsicStatus, Hash, Header, Index, RuntimeDispatchInfo, SignerPayload } from '@polkadot/types/interfaces'; -import type { Callback, Codec, CodecClass, ISubmittableResult, SignatureOptions } from '@polkadot/types/types'; +import type { Callback, Codec, CodecClass, ISubmittableResult, SignatureOptions, TxPayloadV1 } from '@polkadot/types/types'; import type { Registry } from '@polkadot/types-codec/types'; import type { HexString } from '@polkadot/util/types'; import type { ApiBase } from '../base/index.js'; @@ -260,6 +260,67 @@ export function createClass ({ api, apiType, blockHas return this; } + #createTxPayloadV1 = async (payload: SignerPayload): Promise => { + const txPayload: TxPayloadV1 = { + version: 1, + signer: payload.address.toHex(), + callData: payload.method.toHex(), + extensions: [], // TODO + txExtVersion: 0, // TODO: 0 For Extrinsic v4 + context: { + metadata: this.registry.metadata.toHex(), + tokenSymbol: this.registry.chainTokens[0], + tokenDecimals: this.registry.chainDecimals[0], + bestBlockHeight: this.registry.createType('u32', payload.blockNumber) + } + }; + + + // const [ + // { specVersion, transactionVersion }, + // { header: { hash: bestHash, number: bestNumber } } + // ] = await Promise.all([ + // api.rpc.state.getRuntimeVersion(), + // api.rpc.chain.getHeader() + // ]); + + // const metadataExtensions = api.registry.metadata.extrinsic.signedExtensions; + + // for (const ext of metadataExtensions) { + // const id = ext.identifier.toString(); + + // const extraDef = this.registry.lookup.getTypeDef(ext.type); + // const extra = this.registry.createType(extraDef.type); + + // if (extraDef.fields) { + // for (const field of extraDef.fields) { + // const fieldName = field.name.toString(); + + // extra.set(fieldName, payload.get(fieldName)); + // } + // } + + // const additionalSignedDef = this.registry.lookup.getTypeDef(ext.additionalSigned); + // const additionalSigned = this.registry.createType(additionalSignedDef.type); + + // if (additionalSignedDef.fields) { + // for (const field of additionalSignedDef.fields) { + // const fieldName = field.name.toString(); + + // additionalSigned.set(fieldName, payload.get(fieldName)); + // } + // } + + // txPayload.extensions.push({ + // id, + // extra: extra.toHex(), + // additionalSigned: additionalSigned.toHex() + // }); + // } + + return txPayload; + }; + #observeSign = (account: AddressOrPair, partialOptions?: Partial): Observable => { const address = isKeyringPair(account) ? account.address : account.toString(); const options = optionsOrNonce(partialOptions); @@ -352,6 +413,13 @@ export function createClass ({ api, apiType, blockHas blockNumber: header ? header.number : 0, method: this.method })]); + + if (isFunction(signer.createTransaction)) { + const txPayload = await this.#createTxPayloadV1(payload); + const signedTransaction = await signer.createTransaction(txPayload); + return signedTransaction; + } + let result: SignerResult; if (isFunction(signer.signPayload)) { diff --git a/packages/types/src/types/extrinsic.ts b/packages/types/src/types/extrinsic.ts index 2095c7e10c1..4ef5036f33c 100644 --- a/packages/types/src/types/extrinsic.ts +++ b/packages/types/src/types/extrinsic.ts @@ -7,6 +7,7 @@ import type { ExtrinsicStatus } from '../interfaces/author/index.js'; import type { EcdsaSignature, Ed25519Signature, Sr25519Signature } from '../interfaces/extrinsics/index.js'; import type { Address, Call, H256, Hash } from '../interfaces/runtime/index.js'; import type { DispatchError, DispatchInfo, EventRecord } from '../interfaces/system/index.js'; +import type { BlockNumber } from '../interfaces/types.js'; import type { ICompact, IKeyringPair, IMethod, INumber, IRuntimeVersionBase } from './interfaces.js'; import type { Registry } from './registry.js'; @@ -29,6 +30,81 @@ export interface ISubmittableResult { toHuman (isExtended?: boolean): AnyJson; } +export interface TxPayloadV1 { + /** + * @description Payload version. MUST be 1. + */ + version: 1; + + /** + * @description Signer selection hint. Allows the implementer to identify which private-key. (e.g., SS58 address, account-name). + * The DApp MUST provide the SS58 address of the intended signer. + * This is used by the implementer to identify the correct key/account to use. + */ + signer: string | null; + + /** + * @description SCALE-encoded Call (module indicator + function indicator + params) + */ + callData: HexString; + + /** + * @description Transaction extensions supplied by the caller (order irrelevant). + * The consumer (DApp/API) is responsible for calculating and providing all components. + * The implementer (Signer) MAY attempt to infer missing ones if necessary. + */ + extensions: { + /** Identifier as defined in metadata (e.g., "CheckSpecVersion", "ChargeAssetTxPayment"). */ + id: string; + + /** + * Explicit "extra" to sign (goes into the extrinsic body). + * SCALE-encoded per the extension's "extra" type as defined in the metadata. + */ + extra: HexString; + + /** + * "Implicit" data to sign (known by the chain, not included into the extrinsic body). + * SCALE-encoded per the extension's "additionalSigned" type as defined in the metadata. + */ + additionalSigned: HexString; + }[]; + + /** + * @description Transaction Extension Version + * - MUST be 0 for Extrinsic V4 transactions. + * - Set to a runtime-supported version (> 0) for Extrinsic V5 transactions. + * + * The implementer: + * - MUST use this field to determine the required extensions for creating the extrinsic. + * - MAY use this field to infer missing extensions that the implementer could know how to handle. + */ + txExtVersion: number; + + /** + * @description Context needed for decoding, display, construction and (optionally) inferring certain extensions. + */ + context: { + /** + * RuntimeMetadataPrefixed blob (SCALE), starting with ASCII "meta" magic (`0x6d657461`). + * Must be V14+. For V5+ versioned extensions, MUST provide V16+. + */ + metadata: HexString; + + /** + * Native token display info (used by some implementers), also needed to compute + * the `CheckMetadataHash` value. + */ + tokenSymbol: string; + tokenDecimals: number; + + /** + * Highest known block number to aid mortality UX. + */ + bestBlockHeight: BlockNumber; + }; +} + export interface SignerPayloadJSON { /** * @description The ss-58 encoded address @@ -166,6 +242,11 @@ export interface SignerResult { } export interface Signer { + /** + * @description The new createTransaction function + */ + createTransaction?: (payload: TxPayloadV1) => Promise; + /** * @description signs an extrinsic payload from a serialized form */ From 4f2679ec02301b0bee613dc13bd3935697728109 Mon Sep 17 00:00:00 2001 From: Arjun Porwal Date: Tue, 21 Oct 2025 10:04:49 +0530 Subject: [PATCH 2/5] refactor: update createTransaction return type to HexString --- packages/api/src/submittable/createClass.ts | 148 +++++++++++++------- packages/types/src/types/extrinsic.ts | 4 +- 2 files changed, 102 insertions(+), 50 deletions(-) diff --git a/packages/api/src/submittable/createClass.ts b/packages/api/src/submittable/createClass.ts index 2f67784afd3..219d3f2af6e 100644 --- a/packages/api/src/submittable/createClass.ts +++ b/packages/api/src/submittable/createClass.ts @@ -54,6 +54,8 @@ function makeEraOptions (api: ApiInterfaceRx, registry: Registry, partialOptions } return makeSignOptions(api, partialOptions, { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore blockHash: header.hash, era: registry.createTypeUnsafe('ExtrinsicEra', [{ current: header.number, @@ -260,63 +262,112 @@ export function createClass ({ api, apiType, blockHas return this; } - #createTxPayloadV1 = async (payload: SignerPayload): Promise => { - const txPayload: TxPayloadV1 = { - version: 1, - signer: payload.address.toHex(), - callData: payload.method.toHex(), - extensions: [], // TODO - txExtVersion: 0, // TODO: 0 For Extrinsic v4 - context: { - metadata: this.registry.metadata.toHex(), - tokenSymbol: this.registry.chainTokens[0], - tokenDecimals: this.registry.chainDecimals[0], - bestBlockHeight: this.registry.createType('u32', payload.blockNumber) - } - }; + /** + * @private + * Transforms the flat, legacy SignerPayload into the structured TxPayloadV1 'extensions' array. + * This logic should match the SignedExtension configuration in the chain's runtime. + */ + #mapPayloadToV1Extensions (payload: SignerPayload): TxPayloadV1['extensions'] { + const registry = this.registry; + const extensions: TxPayloadV1['extensions'] = []; + const activeExtensions = payload.signedExtensions.toArray(); - // const [ - // { specVersion, transactionVersion }, - // { header: { hash: bestHash, number: bestNumber } } - // ] = await Promise.all([ - // api.rpc.state.getRuntimeVersion(), - // api.rpc.chain.getHeader() - // ]); + const genesisHash = registry.createType('Hash', payload.genesisHash); - // const metadataExtensions = api.registry.metadata.extrinsic.signedExtensions; + for (const id of activeExtensions) { + let extra: Codec = registry.createType('Null'); + let additional: Codec = registry.createType('Null'); - // for (const ext of metadataExtensions) { - // const id = ext.identifier.toString(); + switch (id.toString()) { + case 'CheckNonZeroSender': + extra = registry.createType('Address', payload.address); + break; - // const extraDef = this.registry.lookup.getTypeDef(ext.type); - // const extra = this.registry.createType(extraDef.type); + case 'CheckMortality': { + extra = payload.era; + additional = payload.era.isMortalEra + ? payload.blockHash || genesisHash + : genesisHash; + break; + } - // if (extraDef.fields) { - // for (const field of extraDef.fields) { - // const fieldName = field.name.toString(); + case 'CheckNonce': + extra = registry.createType('Compact', payload.nonce); + break; + + case 'ChargeTransactionPayment': + extra = payload.tip + ? registry.createType('Compact', payload.tip) + : registry.createType('Null'); + break; + + case 'CheckSpecVersion': + additional = registry.createType('u32', payload.runtimeVersion.specVersion); + break; + + case 'CheckTxVersion': + additional = registry.createType('u32', payload.version); + break; + + case 'CheckGenesis': + additional = genesisHash; + break; + + case 'ChargeAssetTxPayment': + extra = payload.assetId + ? registry.createType('Option', payload.assetId) + : registry.createType('Null'); + break; + + case 'CheckMetadataHash': + additional = this.registry.metadata.hash; + break; + + case 'CheckWeight': + case 'CheckBlockGasLimit': + extra = registry.createType('Null'); + additional = registry.createType('Null'); + break; + + default: + extra = registry.createType('Null'); + additional = registry.createType('Null'); + break; + } - // extra.set(fieldName, payload.get(fieldName)); - // } - // } + extensions.push({ + additionalSigned: additional.toHex(), + extra: extra.toHex(), + id: id.toString() + }); + } - // const additionalSignedDef = this.registry.lookup.getTypeDef(ext.additionalSigned); - // const additionalSigned = this.registry.createType(additionalSignedDef.type); + return extensions; + } - // if (additionalSignedDef.fields) { - // for (const field of additionalSignedDef.fields) { - // const fieldName = field.name.toString(); + #createTxPayloadV1 = (address: Address | string | Uint8Array, options: SignatureOptions, header: Header | null): TxPayloadV1 => { + const payload = this.registry.createTypeUnsafe('SignerPayload', [objectSpread({}, options, { + address, + blockNumber: header ? header.number : 0, + method: this.method + })]); - // additionalSigned.set(fieldName, payload.get(fieldName)); - // } - // } + const extensions: TxPayloadV1['extensions'] = this.#mapPayloadToV1Extensions(payload); - // txPayload.extensions.push({ - // id, - // extra: extra.toHex(), - // additionalSigned: additionalSigned.toHex() - // }); - // } + const txPayload: TxPayloadV1 = { + callData: payload.method.toHex(), + context: { + bestBlockHeight: this.registry.createType('u32', payload.blockNumber), + metadata: this.registry.metadata.toHex(), + tokenDecimals: this.registry.chainDecimals[0], + tokenSymbol: this.registry.chainTokens[0] + }, + extensions, + signer: payload.address.toString(), + txExtVersion: api.extrinsicType === 4 ? 0 : api.runtimeVersion.transactionVersion.toNumber(), + version: 1 + }; return txPayload; }; @@ -415,9 +466,10 @@ export function createClass ({ api, apiType, blockHas })]); if (isFunction(signer.createTransaction)) { - const txPayload = await this.#createTxPayloadV1(payload); + const txPayload = this.#createTxPayloadV1(address, options, header); const signedTransaction = await signer.createTransaction(txPayload); - return signedTransaction; + + return { id: Date.now(), signedTransaction }; } let result: SignerResult; diff --git a/packages/types/src/types/extrinsic.ts b/packages/types/src/types/extrinsic.ts index 4ef5036f33c..a4678b0afb2 100644 --- a/packages/types/src/types/extrinsic.ts +++ b/packages/types/src/types/extrinsic.ts @@ -243,9 +243,9 @@ export interface SignerResult { export interface Signer { /** - * @description The new createTransaction function + * @description The new createTransaction function; Returns ready-to-broadcast extrinsic after internal computation by signer */ - createTransaction?: (payload: TxPayloadV1) => Promise; + createTransaction?: (payload: TxPayloadV1) => Promise; /** * @description signs an extrinsic payload from a serialized form From ff4776e5e383784fbb941c7b215e7fd672ceed6a Mon Sep 17 00:00:00 2001 From: Arjun Porwal Date: Tue, 28 Oct 2025 12:55:20 +0530 Subject: [PATCH 3/5] feat: implement EXTENSION_MATCHER for handling signed extensions in createClass --- packages/api/src/submittable/createClass.ts | 66 ++------------------- packages/api/src/submittable/matcher.ts | 46 ++++++++++++++ 2 files changed, 50 insertions(+), 62 deletions(-) create mode 100644 packages/api/src/submittable/matcher.ts diff --git a/packages/api/src/submittable/createClass.ts b/packages/api/src/submittable/createClass.ts index 219d3f2af6e..fc3af40b174 100644 --- a/packages/api/src/submittable/createClass.ts +++ b/packages/api/src/submittable/createClass.ts @@ -17,6 +17,7 @@ import { catchError, first, map, mergeMap, of, switchMap, tap } from 'rxjs'; import { identity, isBn, isFunction, isNumber, isString, isU8a, objectSpread } from '@polkadot/util'; import { filterEvents, isKeyringPair } from '../util/index.js'; +import { EXTENSION_MATCHER } from './matcher.js'; import { SubmittableResult } from './Result.js'; interface SubmittableOptions { @@ -273,71 +274,12 @@ export function createClass ({ api, apiType, blockHas const activeExtensions = payload.signedExtensions.toArray(); - const genesisHash = registry.createType('Hash', payload.genesisHash); - for (const id of activeExtensions) { - let extra: Codec = registry.createType('Null'); - let additional: Codec = registry.createType('Null'); - - switch (id.toString()) { - case 'CheckNonZeroSender': - extra = registry.createType('Address', payload.address); - break; - - case 'CheckMortality': { - extra = payload.era; - additional = payload.era.isMortalEra - ? payload.blockHash || genesisHash - : genesisHash; - break; - } - - case 'CheckNonce': - extra = registry.createType('Compact', payload.nonce); - break; - - case 'ChargeTransactionPayment': - extra = payload.tip - ? registry.createType('Compact', payload.tip) - : registry.createType('Null'); - break; - - case 'CheckSpecVersion': - additional = registry.createType('u32', payload.runtimeVersion.specVersion); - break; - - case 'CheckTxVersion': - additional = registry.createType('u32', payload.version); - break; - - case 'CheckGenesis': - additional = genesisHash; - break; - - case 'ChargeAssetTxPayment': - extra = payload.assetId - ? registry.createType('Option', payload.assetId) - : registry.createType('Null'); - break; - - case 'CheckMetadataHash': - additional = this.registry.metadata.hash; - break; - - case 'CheckWeight': - case 'CheckBlockGasLimit': - extra = registry.createType('Null'); - additional = registry.createType('Null'); - break; - - default: - extra = registry.createType('Null'); - additional = registry.createType('Null'); - break; - } + const { additionalSigned = registry.createType('Null'), extra = registry.createType('Null') } = + EXTENSION_MATCHER[id.toString()]?.(payload, registry) ?? {}; extensions.push({ - additionalSigned: additional.toHex(), + additionalSigned: additionalSigned.toHex(), extra: extra.toHex(), id: id.toString() }); diff --git a/packages/api/src/submittable/matcher.ts b/packages/api/src/submittable/matcher.ts new file mode 100644 index 00000000000..22d2e774d29 --- /dev/null +++ b/packages/api/src/submittable/matcher.ts @@ -0,0 +1,46 @@ +// Copyright 2017-2025 @polkadot/api authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { SignerPayload } from '@polkadot/types/interfaces'; +import type { Codec, Registry } from '@polkadot/types-codec/types'; + +type ExtensionHandler = (payload: SignerPayload, registry: Registry) => { extra?: Codec; additionalSigned?: Codec }; + +export const EXTENSION_MATCHER: Record = { + ChargeAssetTxPayment: (payload, registry) => ({ + extra: payload.assetId + ? registry.createType('Option', payload.assetId) + : registry.createType('Null') + }), + ChargeTransactionPayment: (payload) => ({ + extra: payload.tip + }), + CheckGenesis: (payload, registry) => ({ + additionalSigned: registry.createType('Hash', payload.genesisHash) + }), + CheckMetadataHash: (_, registry) => ({ + additionalSigned: registry.metadata.hash + }), + CheckMortality: (payload, registry) => { + const genesisHash = registry.createType('Hash', payload.genesisHash); + + return { + additional: payload.era.isMortalEra + ? payload.blockHash || genesisHash + : genesisHash, + extra: payload.era + }; + }, + CheckNonZeroSender: (payload, registry) => ({ + extra: registry.createType('Address', payload.address) + }), + CheckNonce: (payload, registry) => ({ + extra: registry.createType('Compact', payload.nonce) + }), + CheckSpecVersion: (payload, registry) => ({ + additionalSigned: registry.createType('u32', payload.runtimeVersion.specVersion) + }), + CheckTxVersion: (payload, registry) => ({ + additionalSigned: registry.createType('u32', payload.version) + }) +}; From b2bbc783445cac264c9796f57ce206d7e8bc3c23 Mon Sep 17 00:00:00 2001 From: Arjun Porwal Date: Wed, 29 Oct 2025 10:30:12 +0530 Subject: [PATCH 4/5] feat: add tests for createTransaction --- .../api/src/promise/createTransaction.spec.ts | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 packages/api/src/promise/createTransaction.spec.ts diff --git a/packages/api/src/promise/createTransaction.spec.ts b/packages/api/src/promise/createTransaction.spec.ts new file mode 100644 index 00000000000..f467a17c260 --- /dev/null +++ b/packages/api/src/promise/createTransaction.spec.ts @@ -0,0 +1,142 @@ +// Copyright 2017-2025 @polkadot/api authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/// + +import type { KeyringPair } from '@polkadot/keyring/types'; +import type { Hash, SignerPayload } from '@polkadot/types/interfaces'; +import type { TxPayloadV1 } from '@polkadot/types/types'; +import type { HexString } from '@polkadot/util/types'; +import type { Signer } from '../types/index.js'; + +import { createTestPairs } from '@polkadot/keyring'; +import { MockProvider } from '@polkadot/rpc-provider/mock'; +import { GenericExtrinsic, TypeRegistry } from '@polkadot/types'; + +import { ApiPromise } from './index.js'; + +// Helper to extract 'extra' from extensions +const findExtra = (payloadV1: TxPayloadV1, id: string) => { + const ext = payloadV1.extensions.find((e) => e.id === id); + + return ext && ext.extra !== '0x' ? ext.extra : null; +}; + +// Helper to extract 'additionalSigned' from extensions +const findAdditional = (payloadV1: TxPayloadV1, id: string) => { + const ext = payloadV1.extensions.find((e) => e.id === id); + + return ext && ext.additionalSigned !== '0x' ? ext.additionalSigned : null; +}; + +class MockModernSigner implements Signer { + private readonly keypair: KeyringPair; + private readonly api: ApiPromise; + + constructor (keypair: KeyringPair, api: ApiPromise) { + this.keypair = keypair; + this.api = api; + } + + public async createTransaction (payloadV1: TxPayloadV1): Promise { + const bestBlockHeight = payloadV1.context.bestBlockHeight; + + const blockHash = ( + await this.api.rpc.chain.getBlockHash(bestBlockHeight) + ).toHex(); + + const payloadToSign = this.api.registry.createType( + 'SignerPayload', + { + address: payloadV1.signer, + assetId: findExtra(payloadV1, 'ChargeAssetTxPayment'), + blockHash, + blockNumber: bestBlockHeight, + era: findExtra(payloadV1, 'CheckMortality'), + genesisHash: findAdditional(payloadV1, 'CheckGenesis'), + method: payloadV1.callData, + mode: null, + nonce: findExtra(payloadV1, 'CheckNonce'), + runtimeVersion: this.api.runtimeVersion, + signedExtensions: payloadV1.extensions.map((e) => e.id), + tip: findExtra(payloadV1, 'ChargeTransactionPayment'), + version: findAdditional(payloadV1, 'CheckTxVersion') + } + ); + + const { signature: signatureHex } = this.api.registry + .createType('ExtrinsicPayload', payloadToSign.toPayload(), { + version: payloadToSign.version + }) + .sign(this.keypair); + + const extrinsic = new GenericExtrinsic( + this.api.registry, + payloadToSign.method + ); + + if (!payloadV1.signer) { + throw new Error('Signer is required but not provided'); + } + + extrinsic.addSignature( + payloadV1.signer, + signatureHex, + payloadToSign.toPayload() + ); + + return extrinsic.toHex(); + } +} + +describe('createTransaction', () => { + const registry = new TypeRegistry(); + let api: ApiPromise; + let provider: MockProvider; + const { alice } = createTestPairs({ type: 'ed25519' }, false); + + beforeEach(async () => { + provider = new MockProvider(registry); + provider.subscriptions.state_subscribeStorage.lastValue = { + changes: [ + [ + '0x26aa394eea5630e07c48ae0c9558cef79c2f82b23e5fd031fb54c292794b4cc4d560eb8d00e57357cf76492334e43bb2ecaa9f28df6a8c4426d7b6090f7ad3c9', + '0x00' + ] + ] + }; + + // Set up the API + const rpcData = await provider.send('state_getMetadata', []); + const genesisHash = registry.createType('Hash', await provider.send('chain_getBlockHash', [])).toHex(); + const specVersion = 0; + + api = await ApiPromise.create({ + metadata: { [`${genesisHash}-${specVersion}`]: rpcData }, + provider, + registry, + throwOnConnect: true + }); + }); + + afterEach(async () => { + await provider.disconnect(); + }); + + it('should create a valid signed transaction', async (): Promise => { + const mockSigner = new MockModernSigner(alice, api); + let extrinsic = api.tx.balances.transferKeepAlive('16kVKdv56dtV13tPttUZonKgj7qqkJget5Xkf3yMqEgdwhZK', 1); + const blockHash = await api.rpc.chain.getBlockHash(); + + extrinsic = await extrinsic.signAsync(alice.address, { blockHash, signer: mockSigner }); + + expect(extrinsic.isSigned).toEqual(true); + expect(extrinsic.signer.toString()).toEqual(alice.address); + expect(extrinsic.method.toHex()).toEqual(extrinsic.method.toHex()); + expect(extrinsic.nonce.toNumber()).toEqual(0); + expect(extrinsic.tip.toNumber()).toEqual(0); + expect(extrinsic.era.isMortalEra).toEqual(true); + + await api.disconnect(); + }); +}); From 9e057801f4a6a40e63bde181db85a39e7c3ed426 Mon Sep 17 00:00:00 2001 From: Arjun Porwal Date: Mon, 3 Nov 2025 12:55:16 +0530 Subject: [PATCH 5/5] feat: add more tests --- .../api/src/promise/createTransaction.spec.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/api/src/promise/createTransaction.spec.ts b/packages/api/src/promise/createTransaction.spec.ts index f467a17c260..a154a402a7f 100644 --- a/packages/api/src/promise/createTransaction.spec.ts +++ b/packages/api/src/promise/createTransaction.spec.ts @@ -137,6 +137,49 @@ describe('createTransaction', () => { expect(extrinsic.tip.toNumber()).toEqual(0); expect(extrinsic.era.isMortalEra).toEqual(true); + const payloadToSign = api.registry.createType( + 'SignerPayload', + { + address: alice.address, + blockHash, + blockNumber: '0x000014fd', + era: '0xd607', + genesisHash: api.genesisHash, + method: extrinsic.method, + signedExtensions: api.registry.signedExtensions, + specVersion: '0x00000000', + transactionVersion: '0x00000000', + version: 4 + } + ); + + const signatureHex = api.registry + .createType('ExtrinsicPayload', payloadToSign.toPayload(), { + version: payloadToSign.version + }) + .sign(alice).signature.replace('00', ''); + + expect(extrinsic.signature.toString()).toEqual(signatureHex); + + await api.disconnect(); + }); + + it('should create a valid signed transaction, even with invalid blockhash', async (): Promise => { + const mockSigner = new MockModernSigner(alice, api); + let extrinsic = api.tx.balances.transferKeepAlive('16kVKdv56dtV13tPttUZonKgj7qqkJget5Xkf3yMqEgdwhZK', 1); + const blockHash = '0x0000jh930000000000hui3w9200000000000000000'; // some random blockhash + + extrinsic = await extrinsic.signAsync(alice.address, { blockHash, signer: mockSigner }); + const hash = extrinsic.send(); + + expect(extrinsic.isSigned).toEqual(true); + expect(extrinsic.signer.toString()).toEqual(alice.address); + expect(extrinsic.method.toHex()).toEqual(extrinsic.method.toHex()); + expect(extrinsic.nonce.toNumber()).toEqual(0); + expect(extrinsic.tip.toNumber()).toEqual(0); + expect(extrinsic.era.isMortalEra).toEqual(true); + expect(hash).toBeDefined(); + await api.disconnect(); }); });