ERA changes (#927)
This commit is contained in:
committed by
Jaco Greeff
parent
a81929de09
commit
0f3a745ed8
@@ -4,6 +4,7 @@
|
||||
|
||||
import Keyring from '@polkadot/keyring';
|
||||
import testingPairs from '@polkadot/keyring/testingPairs';
|
||||
import { ExtrinsicEra } from '@polkadot/types/type';
|
||||
import { randomAsHex } from '@polkadot/util-crypto';
|
||||
|
||||
import Api from '../../src/promise';
|
||||
@@ -154,4 +155,44 @@ describe.skip('e2e transactions', () => {
|
||||
doTwo(done)
|
||||
});
|
||||
});
|
||||
|
||||
it('makes a transfer with ERA (signAndSend)', async (done) => {
|
||||
const nonce = await api.query.system.accountNonce(keyring.dave.address());
|
||||
const signedBlock = await api.rpc.chain.getBlock();
|
||||
const currentHeight = signedBlock.block.header.number;
|
||||
const exERA = new ExtrinsicEra({ current: currentHeight, period: 10 });
|
||||
// eraBirth - start of ERA which is always less than current block height
|
||||
// eraDeath - end of ERA validity (EXPIRY)
|
||||
const eraBirth = exERA.asMortalEra.birth(currentHeight.toNumber());
|
||||
const eraDeath = exERA.asMortalEra.death(currentHeight.toNumber());
|
||||
console.log('STARTED AT :'+eraBirth+' EXPIRED AT :'+eraDeath);
|
||||
const eraHash = await api.rpc.chain.getBlockHash(eraBirth);
|
||||
const ex = api.tx.balances
|
||||
.transfer(keyring.eve.address(), 12345);
|
||||
const hash = await ex.signAndSend(keyring.dave, {blockHash: eraHash, era:exERA, nonce});
|
||||
|
||||
expect(hash.toHex()).toHaveLength(66);
|
||||
done();
|
||||
});
|
||||
|
||||
it('makes a transfer with ERA (signAndSend) with invalid time', async (done) => {
|
||||
const nonce = await api.query.system.accountNonce(keyring.alice.address());
|
||||
const signedBlock = await api.rpc.chain.getBlock();
|
||||
const currentHeight = signedBlock.block.header.number;
|
||||
const exERA = new ExtrinsicEra({ current: currentHeight, period: 4 });
|
||||
const eraBirth = exERA.asMortalEra.birth(currentHeight.toNumber());
|
||||
const eraDeath = exERA.asMortalEra.death(currentHeight.toNumber());
|
||||
console.log('STARTED AT :'+eraBirth+' EXPIRED AT :'+eraDeath);
|
||||
const eraHash = await api.rpc.chain.getBlockHash(eraBirth);
|
||||
const ex = api.tx.balances.transfer(keyring.eve.address(), 12345);
|
||||
const unsubscribe = await api.rpc.chain.subscribeNewHead(async(header) => {
|
||||
console.log(`Chain is at block: #${header.blockNumber}`);
|
||||
if (header.blockNumber.toNumber() === eraDeath-1) {
|
||||
const hash = await ex.signAndSend(keyring.alice, {blockHash: eraHash, era:exERA, nonce});
|
||||
|
||||
expect(hash).toBeUndefined();
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2017-2019 @polkadot/types authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import ExtrinsicEra from './ExtrinsicEra';
|
||||
|
||||
describe('ExtrinsicEra', () => {
|
||||
it('decodes an Extrinsic Era with immortal', () => {
|
||||
const extrinsicEra = new ExtrinsicEra(new Uint8Array([0]));
|
||||
|
||||
expect(extrinsicEra.asImmortalEra).toBeDefined();
|
||||
});
|
||||
|
||||
it('decodes an Extrinsic Era from u8 as mortal', () => {
|
||||
const extrinsicEra = new ExtrinsicEra(new Uint8Array([78, 156]));
|
||||
|
||||
expect(extrinsicEra.asMortalEra.period.toNumber()).toEqual(32768);
|
||||
expect(extrinsicEra.asMortalEra.phase.toNumber()).toEqual(20000);
|
||||
});
|
||||
|
||||
it('encode an Extrinsic Era from Object with blocknumber & period as mortal instance', () => {
|
||||
const extrinsicEra = new ExtrinsicEra({ current: 1400, period: 200 });
|
||||
|
||||
expect(extrinsicEra.asMortalEra.period.toNumber()).toEqual(256);
|
||||
expect(extrinsicEra.asMortalEra.phase.toNumber()).toEqual(120);
|
||||
});
|
||||
});
|
||||
@@ -2,33 +2,225 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { AnyU8a } from '../types';
|
||||
|
||||
import { u8aToU8a } from '@polkadot/util';
|
||||
import { assert, hexToU8a, isHex, isU8a, isObject, u8aToBn } from '@polkadot/util';
|
||||
|
||||
import Enum from '../codec/Enum';
|
||||
import Tuple from '../codec/Tuple';
|
||||
import U8a from '../codec/U8a';
|
||||
import U64 from '../primitive/U64';
|
||||
import { AnyU8a, IExtrinsicEra } from '../types';
|
||||
|
||||
type MortalEraValue = [U64, U64];
|
||||
|
||||
interface MortalMethod {
|
||||
current: number;
|
||||
period: number;
|
||||
}
|
||||
|
||||
const VALID_IMMORTAL = new U8a([0]);
|
||||
|
||||
/**
|
||||
* @name ExtrinsicEra
|
||||
* @description
|
||||
* The era for an extrinsic, indicating either a mortal or immortal extrinsic
|
||||
*/
|
||||
export default class ExtrinsicEra extends U8a {
|
||||
constructor (value?: AnyU8a) {
|
||||
super(
|
||||
ExtrinsicEra.decodeExtrinsicEra(value)
|
||||
);
|
||||
export default class ExtrinsicEra extends Enum implements IExtrinsicEra {
|
||||
constructor (value?: any) {
|
||||
super({
|
||||
ImmortalEra,
|
||||
MortalEra
|
||||
}, ExtrinsicEra.decodeExtrinsicEra(value));
|
||||
}
|
||||
|
||||
static decodeExtrinsicEra (value?: AnyU8a): Uint8Array {
|
||||
if (value) {
|
||||
const u8a = u8aToU8a(value);
|
||||
|
||||
// If we have a zero byte, it is immortal (1 byte in length), otherwise we have
|
||||
// the era details following as another byte
|
||||
return u8a.subarray(0, (u8a[0] === 0) ? 1 : 2);
|
||||
private static decodeExtrinsicEra (value: MortalMethod | Uint8Array | string = new Uint8Array()): Uint8Array | Object | undefined {
|
||||
if (isHex(value)) {
|
||||
return ExtrinsicEra.decodeExtrinsicEra(hexToU8a(value));
|
||||
} else if (isU8a(value)) {
|
||||
if (!value.length || value[0] === 0) {
|
||||
return new Uint8Array([0, 0]);
|
||||
} else {
|
||||
return new Uint8Array([1, value[0], value[1]]);
|
||||
}
|
||||
} else if (isObject(value)) {
|
||||
return { MortalEra: value };
|
||||
}
|
||||
|
||||
return new Uint8Array([0]);
|
||||
throw new Error('Invalid data passed to Era');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Overide the encoded length method
|
||||
*/
|
||||
get encodedLength (): number {
|
||||
if (this.index === 0) {
|
||||
return this.asImmortalEra.encodedLength;
|
||||
} else {
|
||||
return this.asMortalEra.encodedLength;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Returns the item as a [[ImmortalEra]]
|
||||
*/
|
||||
get asImmortalEra (): ImmortalEra {
|
||||
assert(this.isImmortalEra, `Cannot convert '${this.type}' via asImmortalEra`);
|
||||
|
||||
return this.value as ImmortalEra;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Returns the item as a [[MortalEra]]
|
||||
*/
|
||||
get asMortalEra (): MortalEra {
|
||||
assert(this.isMortalEra, `Cannot convert '${this.type}' via asMortalEra`);
|
||||
|
||||
return this.value as MortalEra;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description `true` if Immortal
|
||||
*/
|
||||
get isImmortalEra (): boolean {
|
||||
return this.index === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description `true` if Mortal
|
||||
*/
|
||||
get isMortalEra (): boolean {
|
||||
return this.index > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Encodes the value as a Uint8Array as per the parity-codec specifications
|
||||
* @param isBare true when the value has none of the type-specific prefixes (internal)
|
||||
*/
|
||||
toU8a (isBare?: boolean): Uint8Array {
|
||||
if (this.index === 0) {
|
||||
return this.asImmortalEra.toU8a(isBare);
|
||||
} else {
|
||||
return this.asMortalEra.toU8a(isBare);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @name ImmortalEra
|
||||
* @description
|
||||
* The ImmortalEra for an extrinsic
|
||||
*/
|
||||
export class ImmortalEra extends U8a {
|
||||
constructor (value?: AnyU8a) {
|
||||
super(value);
|
||||
|
||||
assert(this.eq(VALID_IMMORTAL), `IMMORTAL: expected ${VALID_IMMORTAL.toHex()}, found ${this.toHex()}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @name MortalEra
|
||||
* @description
|
||||
* The MortalEra for an extrinsic, indicating period and phase
|
||||
*/
|
||||
export class MortalEra extends Tuple {
|
||||
constructor (value?: any) {
|
||||
super({
|
||||
period: U64,
|
||||
phase: U64
|
||||
}, MortalEra.decodeMortalEra(value));
|
||||
}
|
||||
|
||||
private static decodeMortalEra (value: MortalMethod | Uint8Array): MortalEraValue {
|
||||
if (isU8a(value)) {
|
||||
const first = u8aToBn(value.subarray(0, 1)).toNumber();
|
||||
const second = u8aToBn(value.subarray(1, 2)).toNumber();
|
||||
const encoded: number = first + (second << 8);
|
||||
const period = 2 << (encoded % (1 << 4));
|
||||
const quantizeFactor = Math.max(period >> 12, 1);
|
||||
const phase = (encoded >> 4) * quantizeFactor;
|
||||
if (period >= 4 && phase < period) {
|
||||
|
||||
return [new U64(period), new U64(phase)];
|
||||
}
|
||||
|
||||
throw new Error('Invalid data passed to Mortal era');
|
||||
} else if (isObject(value)) {
|
||||
const { current } = value;
|
||||
const { period } = value;
|
||||
|
||||
let calPeriod = Math.pow(2, Math.ceil(Math.log2(period)));
|
||||
calPeriod = Math.min(Math.max(calPeriod, 4), 1 << 16);
|
||||
const phase = current % calPeriod;
|
||||
const quantizeFactor = Math.max(calPeriod >> 12, 1);
|
||||
const quantizedPhase = phase / quantizeFactor * quantizeFactor;
|
||||
|
||||
return [new U64(calPeriod), new U64(quantizedPhase)];
|
||||
}
|
||||
|
||||
throw new Error('Invalid data passed to Mortal era');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description The period of this Mortal wraps as a [[U64]]
|
||||
*/
|
||||
get period (): U64 {
|
||||
return this[0] as U64;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description The phase of this Mortal wraps as a [[U64]]
|
||||
*/
|
||||
get phase (): U64 {
|
||||
return this[1] as U64;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Encodes the value as a Uint8Array as per the parity-codec specifications
|
||||
* @param isBare true when the value has none of the type-specific prefixes (internal)
|
||||
* Period and phase are encoded:
|
||||
* The period of validity from the block hash found in the signing material.
|
||||
* The phase in the period that this transaction's lifetime begins (and, importantly,
|
||||
* implies which block hash is included in the signature material). If the `period` is
|
||||
* greater than 1 << 12, then it will be a factor of the times greater than 1<<12 that
|
||||
* `period` is.
|
||||
*/
|
||||
toU8a (isBare?: boolean): Uint8Array {
|
||||
const period = this.period.toNumber();
|
||||
const phase = this.phase.toNumber();
|
||||
const quantizeFactor = Math.max(period >> 12, 1);
|
||||
const trailingZeros = this.getTrailingZeros(period);
|
||||
const encoded = Math.min(15, Math.max(1, trailingZeros - 1)) + (((phase / quantizeFactor) << 4));
|
||||
const first = encoded >> 8;
|
||||
const second = encoded & 0xff;
|
||||
|
||||
return new Uint8Array([second, first]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Get the block number of the start of the era whose properties this object describes that `current` belongs to.
|
||||
*/
|
||||
birth (current: number) {
|
||||
return Math.floor((Math.max(current,this.phase.toNumber()) - this.phase.toNumber()) / this.period.toNumber()) * this.period.toNumber() + this.phase.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Get the block number of the first block at which the era has ended.
|
||||
*/
|
||||
death (current: number) {
|
||||
return this.birth(current) + this.period.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description convert the number to binary and get the trailing zero's.
|
||||
*/
|
||||
private getTrailingZeros (period: number) {
|
||||
const binary = period.toString(2);
|
||||
let index = 0;
|
||||
|
||||
while (binary[binary.length - 1 - index] === '0') {
|
||||
index++;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,13 @@ export default class ExtrinsicSignature extends Struct implements IExtrinsicSign
|
||||
return (this.get('version') as U8).toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description The [[ExtrinsicEra]] (mortal or immortal) this signature applies to
|
||||
*/
|
||||
set era (era: ExtrinsicEra) {
|
||||
this.set('era', era);
|
||||
}
|
||||
|
||||
private injectSignature (signature: Signature, signer: Address, nonce: Nonce, era: ExtrinsicEra): ExtrinsicSignature {
|
||||
this.set('era', era);
|
||||
this.set('nonce', nonce);
|
||||
@@ -128,7 +135,7 @@ export default class ExtrinsicSignature extends Struct implements IExtrinsicSign
|
||||
/**
|
||||
* @description Adds a raw signature
|
||||
*/
|
||||
addSignature (_signer: Address | Uint8Array | string, _signature: Uint8Array | string, _nonce: AnyNumber, _era: Uint8Array = IMMORTAL_ERA): ExtrinsicSignature {
|
||||
addSignature (_signer: Address | Uint8Array | string, _signature: Uint8Array | string, _nonce: AnyNumber, _era: Uint8Array | ExtrinsicEra = IMMORTAL_ERA): ExtrinsicSignature {
|
||||
const signer = new Address(_signer);
|
||||
const nonce = new Nonce(_nonce);
|
||||
const era = new ExtrinsicEra(_era);
|
||||
@@ -145,7 +152,7 @@ export default class ExtrinsicSignature extends Struct implements IExtrinsicSign
|
||||
const signingPayload = new SignaturePayload({
|
||||
nonce,
|
||||
method,
|
||||
era: era || IMMORTAL_ERA,
|
||||
era: era || this.era || IMMORTAL_ERA,
|
||||
blockHash
|
||||
});
|
||||
const signature = new Signature(signingPayload.sign(account, version as RuntimeVersion));
|
||||
|
||||
@@ -126,6 +126,12 @@ export interface IMethod extends Codec {
|
||||
|
||||
export interface IExtrinsicSignature extends Codec {
|
||||
readonly isSigned: boolean;
|
||||
era: IExtrinsicEra;
|
||||
}
|
||||
|
||||
export interface IExtrinsicEra {
|
||||
asImmortalEra: Codec;
|
||||
asMortalEra: Codec;
|
||||
}
|
||||
|
||||
export interface IExtrinsic extends IMethod {
|
||||
|
||||
Reference in New Issue
Block a user