Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions packages/api/test/e2e/promise-tx.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { randomAsHex } from '@polkadot/util-crypto';
import Api from '../../src/promise';
import WsProvider from '../../../rpc-provider/src/ws';
import SingleAccountSigner from "../util/SingleAccountSigner";
import {ExtrinsicEra} from '@polkadot/types/type';

describe.skip('e2e transactions', () => {
const keyring = testingPairs({ type: 'ed25519' });
Expand Down Expand Up @@ -211,4 +212,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 }, 1);
// 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 tx = await ex.signAndSend(keyring.dave, {blockHash: eraHash, era:exERA, nonce});

expect(tx.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 }, 1);
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 tx = await ex.signAndSend(keyring.alice, {blockHash: eraHash, era:exERA, nonce});

expect(tx).toBeUndefined();
done();
}
});
});
});
29 changes: 29 additions & 0 deletions packages/types/src/type/ExtrinsicEra.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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([1, 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 mortalIndex = 1;
const extrinsicEra = new ExtrinsicEra({ current: 1400, period: 200 }, mortalIndex);

expect(extrinsicEra.asMortalEra.period.toNumber()).toEqual(256);
expect(extrinsicEra.asMortalEra.phase.toNumber()).toEqual(120);
});
});
175 changes: 161 additions & 14 deletions packages/types/src/type/ExtrinsicEra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,180 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import EnumType from '../codec/EnumType';
import { isHex, isU8a, hexToU8a, isObject, assert, u8aToBn } from '@polkadot/util';
import Tuple from '../codec/Tuple';
import U8 from '../primitive/U8';
import { AnyU8a } from '../types';

import { u8aToU8a } from '@polkadot/util';

import U8a from '../codec/U8a';

/**
* @name ExtrinsicEra
* @description
* The era for an extrinsic, indicating either a mortal or immortal extrinsic
*/
export default class ExtrinsicEra extends U8a {
export default class ExtrinsicEra extends EnumType<ImmortalEra | MortalEra> {
constructor (value?: any, index?: number) {
super({ ImmortalEra, MortalEra }, value, index);
}

/**
* @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 === 1;
}

/**
* @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 super.toU8a();
} else {
return this.asMortalEra.toU8a(isBare);
}
}
}

const VALID_IMMORTAL = new U8a([]);
/**
* @name ImmortalEra
* @description
* The ImmortalEra for an extrinsic
*/
export class ImmortalEra extends U8a {
constructor (value?: AnyU8a) {
super(
ExtrinsicEra.decodeExtrinsicEra(value)
);
super(value);

assert(this.eq(VALID_IMMORTAL), `IMMORTAL: expected ${VALID_IMMORTAL.toHex()}, found ${this.toHex()}`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you try this.eq([0]) if this works, then you can just let VALID_IMMORTAL = [0]

}
}

export type MortalEraValue = [U8, U8];

static decodeExtrinsicEra (value?: AnyU8a): Uint8Array {
if (value) {
const u8a = u8aToU8a(value);
interface MortalMethod {
current: number;
period: number;
}

/**
* @name MortalEra
* @description
* The MortalEra for an extrinsic, indicating period and phase
*/
export class MortalEra extends Tuple {
constructor (value?: any) {
super({
period : U8, phase : U8
}, MortalEra.decodeMortalEra(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 decodeMortalEra (value: MortalMethod | Uint8Array | string): MortalEraValue {
if (isHex(value)) {
return MortalEra.decodeMortalEra(hexToU8a(value.toString()));
} else if (isU8a(value)) {
const first = u8aToBn(value.subarray(0, 1)).toNumber();
let 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);
let phase = (encoded >> 4) * quantizeFactor;
if (period >= 4 && phase < period) {
return [new U8(period), new U8(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 U8(calPeriod), new U8(quantizedPhase)];
}
throw new Error('Invalid data passed to Mortal era');
}
/**
* @description The period of this Mortal wraps as a [[U8]]
*/
get period (): U8 {
return this[0] as U8;
}

return new Uint8Array([0]);
/**
* @description The phase of this Mortal wraps as a [[U8]]
*/
get phase (): U8 {
return this[1] as U8;
}

/**
* @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 {
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 zeros: number[] = [];
let periodN = period;
periodN = parseInt(periodN.toString(2), 10);

while (periodN % 10 === 0) {
periodN = periodN /= 10;
zeros.push(0);
}
return zeros.length;
}
}
9 changes: 8 additions & 1 deletion packages/types/src/type/ExtrinsicSignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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));
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export interface IMethod extends Codec {

export interface IExtrinsicSignature extends Codec {
readonly isSigned: boolean;
era: Codec;
}

export interface IExtrinsic extends IMethod {
Expand Down