Skip to content

Commit 7bfdd88

Browse files
authored
Minor API-related readability cleanups (#1333)
* Minor API-related readability cleanups * Oops, remove trial protected, revert to private * Unneeded as Observable<...> casts * Split state retrieval from signAndSend * Typo in comment * Cleanup descriptions
1 parent cab78c4 commit 7bfdd88

File tree

8 files changed

+188
-181
lines changed

8 files changed

+188
-181
lines changed

packages/api/src/SignerPayload.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,11 @@ const _Payload: Constructor<SignerPayloadType> = Struct.with({
3737
});
3838

3939
export default class Payload extends _Payload {
40-
/**
41-
* @description Returns this as a SignerPayloadType. This works since the Struct.with injects all the getters automatically (just ensure the 2 definitiona are matching)
42-
*/
43-
public get self (): SignerPayloadType {
44-
return this as any as SignerPayloadType;
45-
}
46-
4740
/**
4841
* @description Creates an representation of the structure as an ISignerPayload JSON
4942
*/
5043
public toPayload (): SignerPayload {
51-
const { address, blockHash, blockNumber, era, genesisHash, method, nonce, runtimeVersion: { specVersion }, tip, version } = this.self;
44+
const { address, blockHash, blockNumber, era, genesisHash, method, nonce, runtimeVersion: { specVersion }, tip, version } = this;
5245

5346
return {
5447
address: address.toString(),

packages/api/src/SubmittableExtrinsic.ts

Lines changed: 76 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// This software may be modified and distributed under the terms
33
// of the Apache-2.0 license. See the LICENSE file for details.
44

5-
import { AccountId, Address, Call, ExtrinsicEra, ExtrinsicStatus, EventRecord, Hash, Header, Index, SignedBlock } from '@polkadot/types/interfaces';
5+
import { AccountId, Address, Call, ExtrinsicEra, ExtrinsicStatus, EventRecord, Hash, Header, Index } from '@polkadot/types/interfaces';
66
import { AnyNumber, AnyU8a, Callback, Codec, IExtrinsic, IExtrinsicEra, IKeyringPair, SignatureOptions } from '@polkadot/types/types';
77
import { ApiInterfaceRx, ApiTypes } from './types';
88

@@ -110,8 +110,7 @@ export default function createSubmittableExtrinsic<ApiType> (
110110
type: ApiTypes,
111111
api: ApiInterfaceRx,
112112
decorateMethod: ApiBase<ApiType>['decorateMethod'],
113-
extrinsic: Call | Uint8Array | string,
114-
trackingCb?: Callback<ISubmittableResult>
113+
extrinsic: Call | Uint8Array | string
115114
): SubmittableExtrinsic<ApiType> {
116115
const _extrinsic = createType('Extrinsic', extrinsic, { version: api.extrinsicType }) as unknown as SubmittableExtrinsic<ApiType>;
117116
const _noStatusCb = type === 'rxjs';
@@ -124,35 +123,27 @@ export default function createSubmittableExtrinsic<ApiType> (
124123

125124
function statusObservable (status: ExtrinsicStatus): Observable<ISubmittableResult> {
126125
if (!status.isFinalized) {
127-
const result = new SubmittableResult({ status });
128-
129-
trackingCb && trackingCb(result);
130-
131-
return of(result);
126+
return of(new SubmittableResult({ status }));
132127
}
133128

134129
const blockHash = status.asFinalized;
135130

136131
return combineLatest([
137-
api.rpc.chain.getBlock(blockHash) as Observable<SignedBlock>,
132+
api.rpc.chain.getBlock(blockHash),
138133
api.query.system.events.at(blockHash) as Observable<Vec<EventRecord>>
139134
]).pipe(
140-
map(([signedBlock, allEvents]): SubmittableResult => {
141-
const result = new SubmittableResult({
135+
map(([signedBlock, allEvents]): SubmittableResult =>
136+
new SubmittableResult({
142137
events: filterEvents(_extrinsic.hash, signedBlock, allEvents),
143138
status
144-
});
145-
146-
trackingCb && trackingCb(result);
147-
148-
return result;
149-
})
139+
})
140+
)
150141
);
151142
}
152143

153144
function sendObservable (updateId: number = -1): Observable<Hash> {
154-
return (api.rpc.author
155-
.submitExtrinsic(_extrinsic) as Observable<Hash>)
145+
return api.rpc.author
146+
.submitExtrinsic(_extrinsic)
156147
.pipe(
157148
tap((hash): void => {
158149
updateSigner(updateId, hash);
@@ -161,8 +152,8 @@ export default function createSubmittableExtrinsic<ApiType> (
161152
}
162153

163154
function subscribeObservable (updateId: number = -1): Observable<ISubmittableResult> {
164-
return (api.rpc.author
165-
.submitAndWatchExtrinsic(_extrinsic) as Observable<ExtrinsicStatus>)
155+
return api.rpc.author
156+
.submitAndWatchExtrinsic(_extrinsic)
166157
.pipe(
167158
switchMap((status): Observable<ISubmittableResult> =>
168159
statusObservable(status)
@@ -206,6 +197,20 @@ export default function createSubmittableExtrinsic<ApiType> (
206197
});
207198
}
208199

200+
function getPrelimState (address: string, options: Partial<SignerOptions>): Observable<[Index, Header | null]> {
201+
return combineLatest([
202+
// if we have a nonce already, don't retrieve the latest, use what is there
203+
isUndefined(options.nonce)
204+
? api.query.system.accountNonce<Index>(address)
205+
: of(createType('Index', options.nonce)),
206+
// if we have an era provided already or eraLength is <= 0 (immortal)
207+
// don't get the latest block, just pass null, handle in mergeMap
208+
(isUndefined(options.era) || (isNumber(options.era) && options.era > 0))
209+
? api.rpc.chain.getHeader()
210+
: of(null)
211+
]);
212+
}
213+
209214
const signOrigin = _extrinsic.sign;
210215

211216
Object.defineProperties(
@@ -246,71 +251,60 @@ export default function createSubmittableExtrinsic<ApiType> (
246251
let updateId: number | undefined;
247252

248253
return decorateMethod(
249-
(): Observable<Codec> => ((
250-
combineLatest([
251-
// if we have a nonce already, don't retrieve the latest, use what is there
252-
isUndefined(options.nonce)
253-
? api.query.system.accountNonce<Index>(address)
254-
: of(createType('Index', options.nonce)),
255-
// if we have an era provided already or eraLength is <= 0 (immortal)
256-
// don't get the latest block, just pass null, handle in mergeMap
257-
(isUndefined(options.era) || (isNumber(options.era) && options.era > 0))
258-
? api.rpc.chain.getHeader() as Observable<Header>
259-
: of(null)
260-
])
261-
).pipe(
262-
first(),
263-
mergeMap(async ([nonce, header]): Promise<void> => {
264-
const eraOptions = expandEraOptions(options, { header, nonce });
265-
266-
// FIXME This is becoming real messy with all the options - way past
267-
// "a method should fit on a single screen" stage. (Probably want to
268-
// clean this when we remove `api.signer.sign` in the next beta cycle)
269-
if (isKeyringPair(account)) {
270-
this.sign(account, eraOptions);
271-
} else if (api.signer) {
272-
const payload = new SignerPayload({
273-
...eraOptions,
274-
address,
275-
method: _extrinsic.method,
276-
blockNumber: header ? header.number : 0
277-
});
278-
279-
if (api.signer.signPayload) {
280-
const { id, signature } = await api.signer.signPayload(payload.toPayload());
281-
282-
// Here we explicitly call `toPayload()` again instead of working with an object
283-
// (reference) as passed to the signer. This means that we are sure that the
284-
// payload data is not modified from our inputs, but the signer
285-
_extrinsic.addSignature(address, signature, payload.toPayload());
286-
updateId = id;
287-
} else if (api.signer.signRaw) {
288-
const { id, signature } = await api.signer.signRaw(payload.toRaw());
289-
290-
// as above, always trust our payload as the signle sourec of truth
291-
_extrinsic.addSignature(address, signature, payload.toPayload());
292-
updateId = id;
293-
} else if (api.signer.sign) {
294-
console.warn('The Signer.sign interface is deprecated and will be removed in a future version, Swap to using the Signer.signPayload interface instead.');
295-
296-
updateId = await api.signer.sign(_extrinsic, address, {
254+
(): Observable<Codec> => (
255+
getPrelimState(address, options).pipe(
256+
first(),
257+
mergeMap(async ([nonce, header]): Promise<void> => {
258+
const eraOptions = expandEraOptions(options, { header, nonce });
259+
260+
// FIXME This is becoming real messy with all the options - way past
261+
// "a method should fit on a single screen" stage. (Probably want to
262+
// clean this when we remove `api.signer.sign` in the next beta cycle)
263+
if (isKeyringPair(account)) {
264+
this.sign(account, eraOptions);
265+
} else if (api.signer) {
266+
const payload = new SignerPayload({
297267
...eraOptions,
298-
blockNumber: header ? header.number.toBn() : new BN(0),
299-
genesisHash: api.genesisHash
268+
address,
269+
method: _extrinsic.method,
270+
blockNumber: header ? header.number : 0
300271
});
272+
273+
if (api.signer.signPayload) {
274+
const { id, signature } = await api.signer.signPayload(payload.toPayload());
275+
276+
// Here we explicitly call `toPayload()` again instead of working with an object
277+
// (reference) as passed to the signer. This means that we are sure that the
278+
// payload data is not modified from our inputs, but the signer
279+
_extrinsic.addSignature(address, signature, payload.toPayload());
280+
updateId = id;
281+
} else if (api.signer.signRaw) {
282+
const { id, signature } = await api.signer.signRaw(payload.toRaw());
283+
284+
// as above, always trust our payload as the signle sourec of truth
285+
_extrinsic.addSignature(address, signature, payload.toPayload());
286+
updateId = id;
287+
} else if (api.signer.sign) {
288+
console.warn('The Signer.sign interface is deprecated and will be removed in a future version, Swap to using the Signer.signPayload interface instead.');
289+
290+
updateId = await api.signer.sign(_extrinsic, address, {
291+
...eraOptions,
292+
blockNumber: header ? header.number.toBn() : new BN(0),
293+
genesisHash: api.genesisHash
294+
});
295+
} else {
296+
throw new Error('Invalid signer interface');
297+
}
301298
} else {
302-
throw new Error('Invalid signer interface');
299+
throw new Error('no signer exists');
303300
}
304-
} else {
305-
throw new Error('no signer exists');
306-
}
307-
}),
308-
switchMap((): Observable<ISubmittableResult> | Observable<Hash> => {
309-
return isSubscription
310-
? subscribeObservable(updateId)
311-
: sendObservable(updateId);
312-
})
313-
) as Observable<Codec>)
301+
}),
302+
switchMap((): Observable<ISubmittableResult> | Observable<Hash> => {
303+
return isSubscription
304+
? subscribeObservable(updateId)
305+
: sendObservable(updateId);
306+
})
307+
) as Observable<Codec>) // FIXME This is wrong, SubmittableResult is _not_ a codec
314308
)(statusCb);
315309
}
316310
}

packages/api/src/base/Decorate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ export default abstract class Decorate<ApiType> extends Events {
218218
decorated.key = (arg1?: Arg, arg2?: Arg): string =>
219219
u8aToHex(compactStripLength(creator(creator.meta.type.isDoubleMap ? [arg1, arg2] : arg1))[1]);
220220

221-
// When using double map storage function, user need to path double map key as an array
221+
// When using double map storage function, user need to pass double map key as an array
222222
decorated.multi = decorateMethod((args: (Arg | Arg[])[]): Observable<Codec[]> =>
223223
this._rpcCore.state.subscribeStorage(
224224
args.map((arg: Arg[] | Arg): [StorageEntry, Arg | Arg[]] => [creator, arg])));

packages/api/src/promise/Api.ts

Lines changed: 56 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,57 @@ import { isFunction, assert } from '@polkadot/util';
1212
import ApiBase from '../base';
1313
import Combinator, { CombinatorCallback, CombinatorFunction } from './Combinator';
1414

15+
interface Tracker {
16+
reject: (value: Error) => Observable<never>;
17+
resolve: (value: () => void) => void;
18+
}
19+
20+
// extract the arguments and callback params from a value array possibly containing a callback
21+
function extractArgs (args: any[], needsCallback: boolean): [any[], Callback<Codec> | undefined] {
22+
let callback: Callback<Codec> | undefined;
23+
const actualArgs = args.slice();
24+
25+
// If the last arg is a function, we pop it, put it into callback.
26+
// actualArgs will then hold the actual arguments to be passed to `method`
27+
if (args.length && isFunction(args[args.length - 1])) {
28+
callback = actualArgs.pop();
29+
}
30+
31+
// When we need a subscription, ensure that a valid callback is actually passed
32+
assert(!needsCallback || isFunction(callback), 'Expected a callback to be passed with subscriptions');
33+
34+
return [actualArgs, callback];
35+
}
36+
37+
// a Promise completion tracker, wrapping an isComplete variable that ensures the promise only resolves once
38+
function promiseTracker (resolve: (value: () => void) => void, reject: (value: Error) => void): Tracker {
39+
let isCompleted = false;
40+
const complete = (fn: Function, value: any): void => {
41+
if (!isCompleted) {
42+
isCompleted = true;
43+
44+
fn(value);
45+
}
46+
};
47+
48+
return {
49+
reject: (error: Error): Observable<never> => {
50+
complete(reject, error);
51+
52+
return EMPTY;
53+
},
54+
resolve: (value: any): void => {
55+
complete(resolve, value);
56+
}
57+
};
58+
}
59+
1560
/**
1661
* # @polkadot/api/promise
1762
*
1863
* ## Overview
1964
*
2065
* @name ApiPromise
21-
*
2266
* @description
2367
* ApiPromise is a standard JavaScript wrapper around the RPC and interfaces on the Polkadot network. As a full Promise-based, all interface calls return Promises, including the static `.create(...)`. Subscription calls utilise `(value) => {}` callbacks to pass through the latest values.
2468
*
@@ -102,10 +146,8 @@ export default class ApiPromise extends ApiBase<'promise'> {
102146

103147
/**
104148
* @description Creates an ApiPromise instance using the supplied provider. Returns an Promise containing the actual Api instance.
105-
*
106149
* @param options options that is passed to the class contructor. Can be either [[ApiOptions]] or a
107150
* provider (see the constructor arguments)
108-
*
109151
* @example
110152
* <BR>
111153
*
@@ -125,10 +167,8 @@ export default class ApiPromise extends ApiBase<'promise'> {
125167

126168
/**
127169
* @description Creates an instance of the ApiPromise class
128-
*
129170
* @param options Options to create an instance. This can be either [[ApiOptions]] or
130171
* an [[WsProvider]].
131-
*
132172
* @example
133173
* <BR>
134174
*
@@ -197,49 +237,31 @@ export default class ApiPromise extends ApiBase<'promise'> {
197237
};
198238
}
199239

240+
/**
241+
* @description Decorate method for ApiPromise, where the results are converted to the Promise equivalent
242+
*/
200243
protected decorateMethod<Method extends AnyFunction> (method: Method, options?: DecorateMethodOptions): StorageEntryPromiseOverloads {
201244
const needsCallback = options && options.methodName && options.methodName.includes('subscribe');
202245

203246
return function (...args: any[]): Promise<ObsInnerType<ReturnType<Method>>> | UnsubscribePromise {
204-
let callback: Callback<Codec> | undefined;
205-
const actualArgs = args.slice();
206-
207-
// If the last arg is a function, we pop it, put it into callback.
208-
// actualArgs will then hold the actual arguments to be passed to `method`
209-
if (args.length && isFunction(args[args.length - 1])) {
210-
callback = actualArgs.pop();
211-
}
212-
213-
// When we need a subscription, ensure that a valid callback is actually passed
214-
assert(!needsCallback || isFunction(callback), 'Expected a callback to be passed with subscriptions');
247+
const [actualArgs, callback] = extractArgs(args, !!needsCallback);
215248

216249
if (!callback) {
217250
return method(...actualArgs).pipe(first()).toPromise() as Promise<ObsInnerType<ReturnType<Method>>>;
218251
}
219252

220253
return new Promise((resolve, reject): void => {
221-
let isCompleted = false;
254+
const tracker = promiseTracker(resolve, reject);
222255
const subscription = method(...actualArgs)
223256
.pipe(
224257
// if we find an error (invalid params, etc), reject the promise
225-
catchError((error): Observable<never> => {
226-
if (!isCompleted) {
227-
isCompleted = true;
228-
229-
reject(error);
230-
}
231-
232-
// we don't want to continue, so empty observable it is
233-
return EMPTY;
234-
}),
258+
catchError((error): Observable<never> =>
259+
tracker.reject(error)
260+
),
235261
// upon the first result, resolve the with the unsub function
236-
tap((): void => {
237-
if (!isCompleted) {
238-
isCompleted = true;
239-
240-
resolve((): void => subscription.unsubscribe());
241-
}
242-
})
262+
tap((): void =>
263+
tracker.resolve((): void => subscription.unsubscribe())
264+
)
243265
)
244266
.subscribe(callback);
245267
}) as UnsubscribePromise;

0 commit comments

Comments
 (0)