UNPKG

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