Signing cleanups (w/ additional tests) (#1134)
This commit is contained in:
@@ -50,6 +50,10 @@ interface SignerOptions {
|
||||
const ONE_MINUTE = 15;
|
||||
const DEFAULT_MORTAL_LENGTH = 5 * ONE_MINUTE;
|
||||
|
||||
function isKeyringPair (account: string | IKeyringPair | AccountId | Address): account is IKeyringPair {
|
||||
return isFunction((account as IKeyringPair).sign);
|
||||
}
|
||||
|
||||
export class SubmittableResult implements ISubmittableResult {
|
||||
public readonly events: EventRecord[];
|
||||
|
||||
@@ -163,15 +167,16 @@ export default function createSubmittableExtrinsic<ApiType> (
|
||||
);
|
||||
}
|
||||
|
||||
function expandOptions (options: Partial<SignerOptions>): SignatureOptions {
|
||||
function expandOptions (options: Partial<SignerOptions>, extras: { blockHash?: Hash; era?: ExtrinsicEra; nonce?: Index }): SignatureOptions {
|
||||
return {
|
||||
blockHash: api.genesisHash,
|
||||
version: api.runtimeVersion,
|
||||
...options
|
||||
...options,
|
||||
...extras
|
||||
} as unknown as SignatureOptions;
|
||||
}
|
||||
|
||||
function setupEraOptions (header: Header | null, options: Partial<SignerOptions>): Partial<SignatureOptions> {
|
||||
function expandEraOptions (options: Partial<SignerOptions>, { header, nonce }: { header: Header | null; nonce: Index }): SignatureOptions {
|
||||
if (!header) {
|
||||
if (isNumber(options.era)) {
|
||||
// since we have no header, it is immortal, remove any option overrides
|
||||
@@ -180,18 +185,19 @@ export default function createSubmittableExtrinsic<ApiType> (
|
||||
delete options.blockHash;
|
||||
}
|
||||
|
||||
return {};
|
||||
return expandOptions(options, { nonce });
|
||||
}
|
||||
|
||||
const { blockNumber, hash } = header;
|
||||
|
||||
return {
|
||||
return expandOptions(options, {
|
||||
blockHash: hash,
|
||||
era: new ExtrinsicEra({
|
||||
current: blockNumber,
|
||||
period: options.era || DEFAULT_MORTAL_LENGTH
|
||||
})
|
||||
};
|
||||
}),
|
||||
nonce
|
||||
});
|
||||
}
|
||||
|
||||
const signOrigin = _extrinsic.sign;
|
||||
@@ -207,14 +213,14 @@ export default function createSubmittableExtrinsic<ApiType> (
|
||||
}
|
||||
},
|
||||
sign: {
|
||||
value: function (account: IKeyringPair, _options: Partial<SignerOptions>): SubmittableExtrinsic<ApiType> {
|
||||
value: function (account: IKeyringPair, optionOrNonce: Partial<SignerOptions>): SubmittableExtrinsic<ApiType> {
|
||||
// HACK here we actually override nonce if it was specified (backwards compat for
|
||||
// the previous signature - don't let userspace break, but allow then time to upgrade)
|
||||
const options: Partial<SignerOptions> = isBn(_options) || isNumber(_options)
|
||||
? { nonce: _options as any as number }
|
||||
: _options;
|
||||
const options: Partial<SignerOptions> = isBn(optionOrNonce) || isNumber(optionOrNonce)
|
||||
? { nonce: optionOrNonce }
|
||||
: optionOrNonce;
|
||||
|
||||
signOrigin.apply(_extrinsic, [account, expandOptions(options)]);
|
||||
signOrigin.apply(_extrinsic, [account, expandOptions(options, {})]);
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -230,8 +236,7 @@ export default function createSubmittableExtrinsic<ApiType> (
|
||||
}
|
||||
|
||||
const isSubscription = _noStatusCb || !!statusCb;
|
||||
const isKeyringPair = isFunction((account as IKeyringPair).sign);
|
||||
const address = isKeyringPair ? (account as IKeyringPair).address : account.toString();
|
||||
const address = isKeyringPair(account) ? account.address : account.toString();
|
||||
let updateId: number | undefined;
|
||||
|
||||
return decorateMethod(
|
||||
@@ -250,13 +255,13 @@ export default function createSubmittableExtrinsic<ApiType> (
|
||||
).pipe(
|
||||
first(),
|
||||
mergeMap(async ([nonce, header]): Promise<void> => {
|
||||
const eraOptions = setupEraOptions(header, options);
|
||||
const eraOptions = expandEraOptions(options, { header, nonce });
|
||||
|
||||
if (isKeyringPair) {
|
||||
this.sign(account as IKeyringPair, { ...options, ...eraOptions, nonce });
|
||||
if (isKeyringPair(account)) {
|
||||
this.sign(account, eraOptions);
|
||||
} else if (api.signer) {
|
||||
updateId = await api.signer.sign(_extrinsic, address, {
|
||||
...expandOptions({ ...options, ...eraOptions, nonce }),
|
||||
...eraOptions,
|
||||
blockNumber: header ? header.blockNumber : new BN(0),
|
||||
genesisHash: api.genesisHash
|
||||
});
|
||||
@@ -267,7 +272,7 @@ export default function createSubmittableExtrinsic<ApiType> (
|
||||
switchMap((): Observable<ISubmittableResult> | Observable<Hash> => {
|
||||
return isSubscription
|
||||
? subscribeObservable(updateId)
|
||||
: sendObservable(updateId) as any; // ???
|
||||
: sendObservable(updateId);
|
||||
})
|
||||
) as Observable<Codec>)
|
||||
)(statusCb);
|
||||
|
||||
@@ -6,7 +6,7 @@ import Keyring from '@polkadot/keyring';
|
||||
import testingPairs from '@polkadot/keyring/testingPairs';
|
||||
import WsProvider from '@polkadot/rpc-provider/ws';
|
||||
import { randomAsHex } from '@polkadot/util-crypto';
|
||||
import { Balance, EventRecord, ExtrinsicEra, Hash, Header, Index, SignedBlock } from '@polkadot/types';
|
||||
import { EventRecord, ExtrinsicEra, Hash, Header, Index, SignedBlock } from '@polkadot/types';
|
||||
|
||||
import { SubmittableResult } from './../../src';
|
||||
import ApiPromise from '../../src/promise';
|
||||
@@ -79,45 +79,6 @@ describeE2E({
|
||||
.signAndSend(keyring.charlie, { era: 0 }, logEvents(done));
|
||||
});
|
||||
|
||||
it('makes a transfer (signAndSend via Signer)', async (done): Promise<() => void> => {
|
||||
const signer = new SingleAccountSigner(keyring.charlie) as Signer;
|
||||
|
||||
api.setSigner(signer);
|
||||
|
||||
return api.tx.balances
|
||||
.transfer(keyring.eve.address, 12345)
|
||||
.signAndSend(keyring.charlie.address, logEvents(done));
|
||||
});
|
||||
|
||||
it('makes a transfer (signAndSend via Signer) with undefined Signer', async (): Promise<void> => {
|
||||
const signer: any = undefined;
|
||||
// no signer
|
||||
api.setSigner(signer);
|
||||
|
||||
await expect(api.tx.balances
|
||||
.transfer(keyring.eve.address, 12345)
|
||||
.signAndSend(keyring.alice.address)).rejects.toThrow('no signer exists');
|
||||
});
|
||||
|
||||
it('makes a transfer (signAndSend via Signer) with the wrong keyring pair', async (): Promise<void> => {
|
||||
const signer: Signer = new SingleAccountSigner(keyring.dave);
|
||||
|
||||
api.setSigner(signer);
|
||||
|
||||
// no callback
|
||||
await expect(api.tx.balances
|
||||
.transfer(keyring.eve.address, 12345)
|
||||
.signAndSend(keyring.alice.address)).rejects.toThrow('does not have the keyringPair');
|
||||
});
|
||||
|
||||
it('makes a transfer (signAndSend via Signer) with the wrong keyring pair with a callback', async (): Promise<void> => {
|
||||
// with callback
|
||||
await expect(api.tx.balances
|
||||
.transfer(keyring.eve.address, 12345)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.signAndSend(keyring.alice.address, (cb: any): void => { /* do nothing */ })).rejects.toThrow('does not have the keyringPair');
|
||||
});
|
||||
|
||||
it('makes a transfer (no callback)', async (): Promise<void> => {
|
||||
const hash = await api.tx.balances
|
||||
.transfer(keyring.eve.address, 12345)
|
||||
@@ -158,22 +119,33 @@ describeE2E({
|
||||
});
|
||||
});
|
||||
|
||||
// TODO split this into a separate file
|
||||
describe('eras', (): void => {
|
||||
it('makes a transfer (specified era)', async (done): Promise<void> => {
|
||||
const signedBlock = await api.rpc.chain.getBlock() as SignedBlock;
|
||||
const currentHeight = signedBlock.block.header.number;
|
||||
const exERA = new ExtrinsicEra({ current: currentHeight, period: 10 });
|
||||
const ex = api.tx.balances.transfer(keyring.eve.address, 12345);
|
||||
const hash = await ex.signAndSend(keyring.charlie, {
|
||||
|
||||
await ex.signAndSend(keyring.charlie, {
|
||||
blockHash: signedBlock.block.header.hash,
|
||||
era: exERA
|
||||
});
|
||||
|
||||
expect(hash.toHex()).toHaveLength(66);
|
||||
done();
|
||||
}, logEvents(done));
|
||||
});
|
||||
|
||||
it('makes a transfer with invalid time', async (done): Promise<() => void> => {
|
||||
it('makes a transfer (specified era, previous block)', async (done): Promise<void> => {
|
||||
const signedBlock = await api.rpc.chain.getBlock() as SignedBlock;
|
||||
const currentHeight = signedBlock.block.header.number.subn(1);
|
||||
const exERA = new ExtrinsicEra({ current: currentHeight, period: 10 });
|
||||
const ex = api.tx.balances.transfer(keyring.eve.address, 12345);
|
||||
|
||||
await ex.signAndSend(keyring.charlie, {
|
||||
blockHash: signedBlock.block.header.parentHash,
|
||||
era: exERA
|
||||
}, logEvents(done));
|
||||
});
|
||||
|
||||
it('fails on a transfer with invalid time', async (done): Promise<void> => {
|
||||
const nonce = await api.query.system.accountNonce(keyring.alice.address) as Index;
|
||||
const signedBlock = await api.rpc.chain.getBlock() as SignedBlock;
|
||||
const currentHeight = signedBlock.block.header.number;
|
||||
@@ -182,38 +154,75 @@ describeE2E({
|
||||
const blockHash = signedBlock.block.header.hash;
|
||||
const ex = api.tx.balances.transfer(keyring.eve.address, 12345);
|
||||
|
||||
return (
|
||||
api.rpc.chain.subscribeNewHead(async (header: Header): Promise<void> => {
|
||||
if (header.blockNumber.toNumber() === eraDeath - 1) {
|
||||
try {
|
||||
await ex.signAndSend(keyring.alice, { blockHash, era: exERA, nonce } as any);
|
||||
} catch (error) {
|
||||
expect(error.message).toMatch(/1010: Invalid Transaction \(0\)/);
|
||||
done();
|
||||
}
|
||||
await api.rpc.chain.subscribeNewHead(async (header: Header): Promise<void> => {
|
||||
if (header.blockNumber.toNumber() === eraDeath - 1) {
|
||||
try {
|
||||
await ex.signAndSend(keyring.alice, { blockHash, era: exERA, nonce } as any);
|
||||
} catch (error) {
|
||||
expect(error.message).toMatch(/1010: Invalid Transaction \(0\)/);
|
||||
done();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('makes a transfer with custom numeric era', async (done): Promise<void> => {
|
||||
const hash = await api.tx.balances
|
||||
await api.tx.balances
|
||||
.transfer(keyring.eve.address, 12345)
|
||||
.signAndSend(keyring.charlie, { era: 2 });
|
||||
.signAndSend(keyring.charlie, { era: 2 }, logEvents(done));
|
||||
});
|
||||
});
|
||||
|
||||
expect(hash.toHex()).toHaveLength(66);
|
||||
done();
|
||||
// TODO split this into a seperate file
|
||||
describe('Signer injection', (): void => {
|
||||
it('makes a transfer (signAndSend via Signer)', async (done): Promise<void> => {
|
||||
const signer = new SingleAccountSigner(keyring.charlie);
|
||||
|
||||
api.setSigner(signer);
|
||||
|
||||
await api.tx.balances
|
||||
.transfer(keyring.eve.address, 12345)
|
||||
.signAndSend(keyring.charlie.address, logEvents(done));
|
||||
});
|
||||
|
||||
it('should fire an error with invalid transaction', async (done): Promise<Hash | void> => {
|
||||
const aliceBalance = await api.query.balances.freeBalance(keyring.alice.address) as Balance;
|
||||
it('succeeds when waiting some blocks before submission', async (done): Promise<void> => {
|
||||
// 10 second delay
|
||||
const signer = new SingleAccountSigner(keyring.charlie, 10000);
|
||||
|
||||
return api.tx.balances
|
||||
.transfer(keyring.bob.address, aliceBalance.muln(2))
|
||||
.signAndSend(keyring.alice).catch((error): void => {
|
||||
expect(error.message).toMatch(/1010: Invalid Transaction \(0\)/);
|
||||
done();
|
||||
});
|
||||
api.setSigner(signer);
|
||||
|
||||
await api.tx.balances
|
||||
.transfer(keyring.eve.address, 12345)
|
||||
.signAndSend(keyring.charlie.address, logEvents(done));
|
||||
});
|
||||
|
||||
it('fails (signAndSend via Signer) with undefined Signer', async (): Promise<void> => {
|
||||
const signer: any = undefined;
|
||||
// no signer
|
||||
api.setSigner(signer);
|
||||
|
||||
await expect(api.tx.balances
|
||||
.transfer(keyring.eve.address, 12345)
|
||||
.signAndSend(keyring.alice.address)).rejects.toThrow('no signer exists');
|
||||
});
|
||||
|
||||
it('fails (signAndSend via Signer) with the wrong keyring pair', async (): Promise<void> => {
|
||||
const signer: Signer = new SingleAccountSigner(keyring.dave);
|
||||
|
||||
api.setSigner(signer);
|
||||
|
||||
// no callback
|
||||
await expect(api.tx.balances
|
||||
.transfer(keyring.eve.address, 12345)
|
||||
.signAndSend(keyring.alice.address)).rejects.toThrow('does not have the keyringPair');
|
||||
});
|
||||
|
||||
it('fails (signAndSend via Signer) with the wrong keyring pair with a callback', async (): Promise<void> => {
|
||||
// with callback
|
||||
await expect(api.tx.balances
|
||||
.transfer(keyring.eve.address, 12345)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.signAndSend(keyring.alice.address, (cb: any): void => { /* do nothing */ })).rejects.toThrow('no signer exists');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,8 +12,11 @@ let id = 0;
|
||||
export default class SingleAccountSigner {
|
||||
private keyringPair: KeyringPair;
|
||||
|
||||
public constructor (keyringPair: KeyringPair) {
|
||||
private signDelay: number;
|
||||
|
||||
public constructor (keyringPair: KeyringPair, signDelay: number = 0) {
|
||||
this.keyringPair = keyringPair;
|
||||
this.signDelay = signDelay;
|
||||
}
|
||||
|
||||
public async sign (extrinsic: Extrinsic, address: string, options: SignatureOptions): Promise<number> {
|
||||
@@ -21,8 +24,12 @@ export default class SingleAccountSigner {
|
||||
throw new Error('does not have the keyringPair');
|
||||
}
|
||||
|
||||
extrinsic.sign(this.keyringPair, options);
|
||||
return new Promise((resolve): void => {
|
||||
setTimeout((): void => {
|
||||
extrinsic.sign(this.keyringPair, options);
|
||||
|
||||
return ++id;
|
||||
resolve(++id);
|
||||
}, this.signDelay);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user