UNPKG

@polkadot/api

Version:

Promise and RxJS wrappers around the Polkadot JS RPC

252 lines (210 loc) • 9.71 kB
// Copyright 2017-2022 @polkadot/api authors & contributors // SPDX-License-Identifier: Apache-2.0 /* eslint-disable no-dupe-class-members */ import { catchError, first, map, mapTo, mergeMap, of, switchMap, tap } from 'rxjs'; import { assert, isBn, isFunction, isNumber, isString, isU8a, objectSpread } from '@polkadot/util'; import { filterEvents, isKeyringPair } from "../util/index.js"; import { SubmittableResult } from "./Result.js"; const identity = input => input; function makeEraOptions(api, registry, partialOptions, { header, mortalLength, nonce }) { if (!header) { 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 delete partialOptions.era; delete partialOptions.blockHash; } return makeSignOptions(api, partialOptions, { nonce }); } return makeSignOptions(api, partialOptions, { blockHash: header.hash, era: registry.createTypeUnsafe('ExtrinsicEra', [{ current: header.number, period: partialOptions.era || mortalLength }]), nonce }); } function makeSignAndSendOptions(partialOptions, statusCb) { let options = {}; if (isFunction(partialOptions)) { statusCb = partialOptions; } else { options = objectSpread({}, partialOptions); } return [options, statusCb]; } function makeSignOptions(api, partialOptions, extras) { return objectSpread({ blockHash: api.genesisHash, genesisHash: api.genesisHash }, partialOptions, extras, { runtimeVersion: api.runtimeVersion, signedExtensions: api.registry.signedExtensions, version: api.extrinsicType }); } function optionsOrNonce(partialOptions = {}) { return isBn(partialOptions) || isNumber(partialOptions) ? { nonce: partialOptions } : partialOptions; } export function createClass({ api, apiType, blockHash, decorateMethod }) { // an instance of the base extrinsic for us to extend const ExtrinsicBase = api.registry.createClass('Extrinsic'); class Submittable extends ExtrinsicBase { #ignoreStatusCb; #transformResult = identity; constructor(registry, extrinsic) { super(registry, extrinsic, { version: api.extrinsicType }); this.#ignoreStatusCb = apiType === 'rxjs'; } // dry run an extrinsic dryRun(account, optionsOrHash) { if (blockHash || isString(optionsOrHash) || isU8a(optionsOrHash)) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return decorateMethod(() => api.rpc.system.dryRun(this.toHex(), blockHash || optionsOrHash)); } // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call return decorateMethod(() => this.#observeSign(account, optionsOrHash).pipe(switchMap(() => api.rpc.system.dryRun(this.toHex()))))(); } // calculate the payment info for this transaction (if signed and submitted) paymentInfo(account, optionsOrHash) { if (blockHash || isString(optionsOrHash) || isU8a(optionsOrHash)) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return decorateMethod(() => api.rpc.payment.queryInfo(this.toHex(), blockHash || optionsOrHash)); } const [allOptions] = makeSignAndSendOptions(optionsOrHash); const address = isKeyringPair(account) ? account.address : account.toString(); // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call return decorateMethod(() => api.derive.tx.signingInfo(address, allOptions.nonce, allOptions.era).pipe(first(), switchMap(signingInfo => { // setup our options (same way as in signAndSend) const eraOptions = makeEraOptions(api, this.registry, allOptions, signingInfo); const signOptions = makeSignOptions(api, eraOptions, {}); return api.rpc.payment.queryInfo(this.isSigned ? api.tx(this).signFake(address, signOptions).toHex() : this.signFake(address, signOptions).toHex()); })))(); } // send with an immediate Hash result // send implementation for both immediate Hash and statusCb variants send(statusCb) { const isSubscription = api.hasSubscriptions && (this.#ignoreStatusCb || !!statusCb); // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call return decorateMethod(isSubscription ? this.#observeSubscribe : this.#observeSend)(statusCb); } /** * @description Sign a transaction, returning the this to allow chaining, i.e. .sign(...).send(). When options, e.g. nonce/blockHash are not specified, it will be inferred. To retrieve eg. nonce use `signAsync` (the preferred interface, this is provided for backwards compatibility) * @deprecated */ sign(account, partialOptions) { super.sign(account, makeSignOptions(api, optionsOrNonce(partialOptions), {})); return this; } /** * @description Signs a transaction, returning `this` to allow chaining. E.g.: `sign(...).send()`. Like `.signAndSend` this will retrieve the nonce and blockHash to send the tx with. */ signAsync(account, partialOptions) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call return decorateMethod(() => this.#observeSign(account, partialOptions).pipe(mapTo(this)))(); } // signAndSend with an immediate Hash result // signAndSend implementation for all 3 cases above signAndSend(account, partialOptions, optionalStatusCb) { const [options, statusCb] = makeSignAndSendOptions(partialOptions, optionalStatusCb); const isSubscription = api.hasSubscriptions && (this.#ignoreStatusCb || !!statusCb); // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call return decorateMethod(() => this.#observeSign(account, options).pipe(switchMap(info => isSubscription ? this.#observeSubscribe(info) : this.#observeSend(info))) // FIXME This is wrong, SubmittableResult is _not_ a codec )(statusCb); } // adds a transform to the result, applied before result is returned withResultTransform(transform) { this.#transformResult = transform; return this; } #observeSign = (account, partialOptions) => { const address = isKeyringPair(account) ? account.address : account.toString(); const options = optionsOrNonce(partialOptions); return api.derive.tx.signingInfo(address, options.nonce, options.era).pipe(first(), mergeMap(async signingInfo => { const eraOptions = makeEraOptions(api, this.registry, options, signingInfo); let updateId = -1; if (isKeyringPair(account)) { this.sign(account, eraOptions); } else { updateId = await this.#signViaSigner(address, eraOptions, signingInfo.header); } return { options: eraOptions, updateId }; })); }; #observeStatus = (txHash, status) => { if (!status.isFinalized && !status.isInBlock) { return of(this.#transformResult(new SubmittableResult({ status, txHash }))); } const blockHash = status.isInBlock ? status.asInBlock : status.asFinalized; return api.derive.tx.events(blockHash).pipe(map(({ block, events }) => this.#transformResult(new SubmittableResult({ ...filterEvents(txHash, block, events, status), status, txHash }))), catchError(internalError => of(this.#transformResult(new SubmittableResult({ internalError, status, txHash }))))); }; #observeSend = info => { return api.rpc.author.submitExtrinsic(this).pipe(tap(hash => { this.#updateSigner(hash, info); })); }; #observeSubscribe = info => { const txHash = this.hash; return api.rpc.author.submitAndWatchExtrinsic(this).pipe(switchMap(status => this.#observeStatus(txHash, status)), tap(status => { this.#updateSigner(status, info); })); }; #signViaSigner = async (address, options, header) => { 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, method: this.method })]); let result; if (isFunction(signer.signPayload)) { result = await signer.signPayload(payload.toPayload()); } else if (isFunction(signer.signRaw)) { result = await signer.signRaw(payload.toRaw()); } else { throw new Error('Invalid signer interface, it should implement either signPayload or signRaw (or both)'); } // Here we explicitly call `toPayload()` again instead of working with an object // (reference) as passed to the signer. This means that we are sure that the // payload data is not modified from our inputs, but the signer super.addSignature(address, result.signature, payload.toPayload()); return result.id; }; #updateSigner = (status, info) => { if (info && info.updateId !== -1) { const { options, updateId } = info; const signer = options.signer || api.signer; if (signer && isFunction(signer.update)) { signer.update(updateId, status); } } }; } return Submittable; }