diff --git a/packages/api-derive/src/tx/signingInfo.ts b/packages/api-derive/src/tx/signingInfo.ts index b5ecf60c666b..e92c6fe33daf 100644 --- a/packages/api-derive/src/tx/signingInfo.ts +++ b/packages/api-derive/src/tx/signingInfo.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { Observable } from 'rxjs'; -import type { Header, Index } from '@polkadot/types/interfaces'; +import type { BlockHash, BlockNumber, Header, Index } from '@polkadot/types/interfaces'; import type { AnyNumber, Codec, IExtrinsicEra } from '@polkadot/types/types'; import type { DeriveApi } from '../types'; @@ -13,7 +13,8 @@ import { isNumber, isUndefined } from '@polkadot/util'; import { FALLBACK_MAX_HASH_COUNT, FALLBACK_PERIOD, MAX_FINALITY_LAG, MORTAL_PERIOD } from './constants'; interface Result { - header: Header | null; + blockHash: BlockHash | null; + blockNumber: BlockNumber | null; mortalLength: number; nonce: Index; } @@ -30,31 +31,53 @@ function nextNonce (api: DeriveApi, address: string): Observable { : latestNonce(api, address); } -function signingHeader (api: DeriveApi): Observable
{ +function getHeader (api: DeriveApi, hash: BlockHash): Observable<[BlockHash, Header]> { return combineLatest([ - api.rpc.chain.getHeader().pipe( - switchMap((header) => - // check for chains at genesis (until block 1 is produced, e.g. 6s), since - // we do need to allow transactions at chain start (also dev/seal chains) - header.parentHash.isEmpty - ? of(header) - // in the case of the current block, we use the parent to minimize the - // impact of forks on the system, but not completely remove it - : api.rpc.chain.getHeader(header.parentHash) - ) - ), - api.rpc.chain.getFinalizedHead().pipe( - switchMap((hash) => - api.rpc.chain.getHeader(hash) + of(hash), + api.rpc.chain.getHeader(hash) + ]); +} + +function getBest (api: DeriveApi): Observable<[BlockHash, Header]> { + return api.rpc.chain.getBlockHash().pipe( + switchMap((hash) => + api.rpc.chain.getHeader(hash).pipe( + switchMap((header) => + // check for chains at genesis (until block 1 is produced, e.g. 6s), since + // we do need to allow transactions at chain start (also dev/seal chains) + hash.eq(api.genesisHash) + ? of<[BlockHash, Header]>([hash, header]) + // in the case of the current block, we use the parent to minimize the + // impact of forks on the system, but not completely remove it + : getHeader(api, header.parentHash) + ) ) ) + ); +} + +function getFin (api: DeriveApi): Observable<[BlockHash, Header]> { + return api.rpc.chain.getFinalizedHead().pipe( + switchMap((hash) => + getHeader(api, hash) + ) + ); +} + +function signingHeader (api: DeriveApi): Observable<[BlockHash, BlockNumber]> { + return combineLatest([ + getBest(api), + getFin(api) ]).pipe( - map(([current, finalized]) => + map(([[hash, head], [finHash, finHead]]): [BlockHash, BlockNumber] => { + const bestNum = head.number.unwrap(); + const finNum = finHead.number.unwrap(); + // determine the hash to use, current when lag > max, else finalized - current.number.unwrap().sub(finalized.number.unwrap()).gt(MAX_FINALITY_LAG) - ? current - : finalized - ) + return bestNum.sub(finNum).gt(MAX_FINALITY_LAG) + ? [hash, bestNum] + : [finHash, finNum]; + }) ); } @@ -71,10 +94,11 @@ export function signingInfo (_instanceId: string, api: DeriveApi): (address: str // if no era (create) or era > 0 (mortal), do block retrieval (isUndefined(era) || (isNumber(era) && era > 0)) ? signingHeader(api) - : of(null) + : of([null, null, null]) ]).pipe( - map(([nonce, header]) => ({ - header, + map(([nonce, [blockHash, blockNumber]]) => ({ + blockHash, + blockNumber, mortalLength: Math.min( api.consts.system?.blockHashCount?.toNumber() || FALLBACK_MAX_HASH_COUNT, MORTAL_PERIOD diff --git a/packages/api/src/submittable/createClass.ts b/packages/api/src/submittable/createClass.ts index 48f5b2e7fc62..c6f90da31c63 100644 --- a/packages/api/src/submittable/createClass.ts +++ b/packages/api/src/submittable/createClass.ts @@ -4,7 +4,7 @@ /* eslint-disable no-dupe-class-members */ import type { Observable, OperatorFunction } from 'rxjs'; -import type { Address, ApplyExtrinsicResult, Call, Extrinsic, ExtrinsicEra, ExtrinsicStatus, Hash, Header, Index, RuntimeDispatchInfo, SignerPayload } from '@polkadot/types/interfaces'; +import type { Address, ApplyExtrinsicResult, BlockNumber, Call, Extrinsic, ExtrinsicEra, ExtrinsicStatus, Hash, Index, RuntimeDispatchInfo, SignerPayload } from '@polkadot/types/interfaces'; import type { Callback, Codec, Constructor, IKeyringPair, ISubmittableResult, SignatureOptions } from '@polkadot/types/types'; import type { Registry } from '@polkadot/types-codec/types'; import type { ApiInterfaceRx, ApiTypes, PromiseOrObs, SignerResult } from '../types'; @@ -26,8 +26,8 @@ interface SubmittableOptions { const identity = (input: T): T => input; -function makeEraOptions (api: ApiInterfaceRx, registry: Registry, partialOptions: Partial, { header, mortalLength, nonce }: { header: Header | null; mortalLength: number; nonce: Index }): SignatureOptions { - if (!header) { +function makeEraOptions (api: ApiInterfaceRx, registry: Registry, partialOptions: Partial, { blockHash, blockNumber, mortalLength, nonce }: { blockHash: Hash | null; blockNumber: BlockNumber | null; mortalLength: number; nonce: Index }): SignatureOptions { + if (!blockHash || !blockNumber) { if (isNumber(partialOptions.era)) { // since we have no header, it is immortal, remove any option overrides // so we only supply the genesisHash and no era to the construction @@ -39,9 +39,9 @@ function makeEraOptions (api: ApiInterfaceRx, registry: Registry, partialOptions } return makeSignOptions(api, partialOptions, { - blockHash: header.hash, + blockHash, era: registry.createTypeUnsafe('ExtrinsicEra', [{ - current: header.number, + current: blockNumber, period: partialOptions.era || mortalLength }]), nonce @@ -226,7 +226,7 @@ export function createClass ({ api, apiType, decorate if (isKeyringPair(account)) { this.sign(account, eraOptions); } else { - updateId = await this.#signViaSigner(address, eraOptions, signingInfo.header); + updateId = await this.#signViaSigner(address, eraOptions, signingInfo.blockNumber); } }), mapTo(updateId) as OperatorFunction @@ -284,14 +284,14 @@ export function createClass ({ api, apiType, decorate ); }; - #signViaSigner = async (address: Address | string | Uint8Array, options: SignatureOptions, header: Header | null): Promise => { + #signViaSigner = async (address: Address | string | Uint8Array, options: SignatureOptions, blockNumber: BlockNumber | null): Promise => { const signer = options.signer || api.signer; assert(signer, 'No signer specified, either via api.setSigner or via sign options. You possibly need to pass through an explicit keypair for the origin so it can be used for signing.'); const payload = this.registry.createTypeUnsafe('SignerPayload', [objectSpread({}, options, { address, - blockNumber: header ? header.number : 0, + blockNumber: blockNumber || 0, method: this.method })]); let result: SignerResult;