UNPKG

9.88 kBJavaScriptView Raw
1// Copyright 2017-2022 @polkadot/api authors & contributors
2// SPDX-License-Identifier: Apache-2.0
3
4/* eslint-disable no-dupe-class-members */
5import { catchError, first, map, mapTo, mergeMap, of, switchMap, tap } from 'rxjs';
6import { assert, isBn, isFunction, isNumber, isString, isU8a, isUndefined, objectSpread } from '@polkadot/util';
7import { filterEvents, isKeyringPair } from "../util/index.js";
8import { SubmittableResult } from "./Result.js";
9
10const identity = input => input;
11
12function makeEraOptions(api, registry, partialOptions, {
13 header,
14 mortalLength,
15 nonce
16}) {
17 if (!header) {
18 assert(partialOptions.era === 0 || !isUndefined(partialOptions.blockHash), 'Expected blockHash to be passed alongside non-immortal era options');
19
20 if (isNumber(partialOptions.era)) {
21 // since we have no header, it is immortal, remove any option overrides
22 // so we only supply the genesisHash and no era to the construction
23 delete partialOptions.era;
24 delete partialOptions.blockHash;
25 }
26
27 return makeSignOptions(api, partialOptions, {
28 nonce
29 });
30 }
31
32 return makeSignOptions(api, partialOptions, {
33 blockHash: header.hash,
34 era: registry.createTypeUnsafe('ExtrinsicEra', [{
35 current: header.number,
36 period: partialOptions.era || mortalLength
37 }]),
38 nonce
39 });
40}
41
42function makeSignAndSendOptions(partialOptions, statusCb) {
43 let options = {};
44
45 if (isFunction(partialOptions)) {
46 statusCb = partialOptions;
47 } else {
48 options = objectSpread({}, partialOptions);
49 }
50
51 return [options, statusCb];
52}
53
54function makeSignOptions(api, partialOptions, extras) {
55 return objectSpread({
56 blockHash: api.genesisHash,
57 genesisHash: api.genesisHash
58 }, partialOptions, extras, {
59 runtimeVersion: api.runtimeVersion,
60 signedExtensions: api.registry.signedExtensions,
61 version: api.extrinsicType
62 });
63}
64
65function optionsOrNonce(partialOptions = {}) {
66 return isBn(partialOptions) || isNumber(partialOptions) ? {
67 nonce: partialOptions
68 } : partialOptions;
69}
70
71export function createClass({
72 api,
73 apiType,
74 blockHash,
75 decorateMethod
76}) {
77 // an instance of the base extrinsic for us to extend
78 const ExtrinsicBase = api.registry.createClass('Extrinsic');
79
80 class Submittable extends ExtrinsicBase {
81 #ignoreStatusCb;
82 #transformResult = identity;
83
84 constructor(registry, extrinsic) {
85 super(registry, extrinsic, {
86 version: api.extrinsicType
87 });
88 this.#ignoreStatusCb = apiType === 'rxjs';
89 } // dry run an extrinsic
90
91
92 dryRun(account, optionsOrHash) {
93 if (blockHash || isString(optionsOrHash) || isU8a(optionsOrHash)) {
94 // eslint-disable-next-line @typescript-eslint/no-unsafe-return
95 return decorateMethod(() => api.rpc.system.dryRun(this.toHex(), blockHash || optionsOrHash));
96 } // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call
97
98
99 return decorateMethod(() => this.#observeSign(account, optionsOrHash).pipe(switchMap(() => api.rpc.system.dryRun(this.toHex()))))();
100 } // calculate the payment info for this transaction (if signed and submitted)
101
102
103 paymentInfo(account, optionsOrHash) {
104 if (blockHash || isString(optionsOrHash) || isU8a(optionsOrHash)) {
105 // eslint-disable-next-line @typescript-eslint/no-unsafe-return
106 return decorateMethod(() => api.rpc.payment.queryInfo(this.toHex(), blockHash || optionsOrHash));
107 }
108
109 const [allOptions] = makeSignAndSendOptions(optionsOrHash);
110 const address = isKeyringPair(account) ? account.address : account.toString(); // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call
111
112 return decorateMethod(() => api.derive.tx.signingInfo(address, allOptions.nonce, allOptions.era).pipe(first(), switchMap(signingInfo => {
113 // setup our options (same way as in signAndSend)
114 const eraOptions = makeEraOptions(api, this.registry, allOptions, signingInfo);
115 const signOptions = makeSignOptions(api, eraOptions, {});
116 return api.rpc.payment.queryInfo(this.isSigned ? api.tx(this).signFake(address, signOptions).toHex() : this.signFake(address, signOptions).toHex());
117 })))();
118 } // send with an immediate Hash result
119
120
121 // send implementation for both immediate Hash and statusCb variants
122 send(statusCb) {
123 const isSubscription = api.hasSubscriptions && (this.#ignoreStatusCb || !!statusCb); // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call
124
125 return decorateMethod(isSubscription ? this.#observeSubscribe : this.#observeSend)(statusCb);
126 }
127 /**
128 * @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)
129 * @deprecated
130 */
131
132
133 sign(account, partialOptions) {
134 super.sign(account, makeSignOptions(api, optionsOrNonce(partialOptions), {}));
135 return this;
136 }
137 /**
138 * @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.
139 */
140
141
142 signAsync(account, partialOptions) {
143 // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call
144 return decorateMethod(() => this.#observeSign(account, partialOptions).pipe(mapTo(this)))();
145 } // signAndSend with an immediate Hash result
146
147
148 // signAndSend implementation for all 3 cases above
149 signAndSend(account, partialOptions, optionalStatusCb) {
150 const [options, statusCb] = makeSignAndSendOptions(partialOptions, optionalStatusCb);
151 const isSubscription = api.hasSubscriptions && (this.#ignoreStatusCb || !!statusCb); // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call
152
153 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
154 )(statusCb);
155 } // adds a transform to the result, applied before result is returned
156
157
158 withResultTransform(transform) {
159 this.#transformResult = transform;
160 return this;
161 }
162
163 #observeSign = (account, partialOptions) => {
164 const address = isKeyringPair(account) ? account.address : account.toString();
165 const options = optionsOrNonce(partialOptions);
166 return api.derive.tx.signingInfo(address, options.nonce, options.era).pipe(first(), mergeMap(async signingInfo => {
167 const eraOptions = makeEraOptions(api, this.registry, options, signingInfo);
168 let updateId = -1;
169
170 if (isKeyringPair(account)) {
171 this.sign(account, eraOptions);
172 } else {
173 updateId = await this.#signViaSigner(address, eraOptions, signingInfo.header);
174 }
175
176 return {
177 options: eraOptions,
178 updateId
179 };
180 }));
181 };
182 #observeStatus = (txHash, status) => {
183 if (!status.isFinalized && !status.isInBlock) {
184 return of(this.#transformResult(new SubmittableResult({
185 status,
186 txHash
187 })));
188 }
189
190 const blockHash = status.isInBlock ? status.asInBlock : status.asFinalized;
191 return api.derive.tx.events(blockHash).pipe(map(({
192 block,
193 events
194 }) => this.#transformResult(new SubmittableResult({ ...filterEvents(txHash, block, events, status),
195 status,
196 txHash
197 }))), catchError(internalError => of(this.#transformResult(new SubmittableResult({
198 internalError,
199 status,
200 txHash
201 })))));
202 };
203 #observeSend = info => {
204 return api.rpc.author.submitExtrinsic(this).pipe(tap(hash => {
205 this.#updateSigner(hash, info);
206 }));
207 };
208 #observeSubscribe = info => {
209 const txHash = this.hash;
210 return api.rpc.author.submitAndWatchExtrinsic(this).pipe(switchMap(status => this.#observeStatus(txHash, status)), tap(status => {
211 this.#updateSigner(status, info);
212 }));
213 };
214 #signViaSigner = async (address, options, header) => {
215 const signer = options.signer || api.signer;
216 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.');
217 const payload = this.registry.createTypeUnsafe('SignerPayload', [objectSpread({}, options, {
218 address,
219 blockNumber: header ? header.number : 0,
220 method: this.method
221 })]);
222 let result;
223
224 if (isFunction(signer.signPayload)) {
225 result = await signer.signPayload(payload.toPayload());
226 } else if (isFunction(signer.signRaw)) {
227 result = await signer.signRaw(payload.toRaw());
228 } else {
229 throw new Error('Invalid signer interface, it should implement either signPayload or signRaw (or both)');
230 } // Here we explicitly call `toPayload()` again instead of working with an object
231 // (reference) as passed to the signer. This means that we are sure that the
232 // payload data is not modified from our inputs, but the signer
233
234
235 super.addSignature(address, result.signature, payload.toPayload());
236 return result.id;
237 };
238 #updateSigner = (status, info) => {
239 if (info && info.updateId !== -1) {
240 const {
241 options,
242 updateId
243 } = info;
244 const signer = options.signer || api.signer;
245
246 if (signer && isFunction(signer.update)) {
247 signer.update(updateId, status);
248 }
249 }
250 };
251 }
252
253 return Submittable;
254}
\No newline at end of file