Skip to content

Commit

Permalink
feat!: add isImmortalEra option for decoding and encoding era perio…
Browse files Browse the repository at this point in the history
…ds (#201)

* fix: add `isImmortalEra` option for decoding

* fix docs

* lint

* fix: inline docs

* fix!: rework defineMethod by adding checkEra

* Update packages/txwrapper-core/src/core/method/defineMethod.ts

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* fix grumbles with enum error message naming, and update PR link for era reference

* createEra

* cleanup some tests syntax

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>
  • Loading branch information
TarikGul and joepetrowski authored Apr 6, 2022
1 parent 088c6ca commit ab5873f
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 23 deletions.
16 changes: 16 additions & 0 deletions packages/txwrapper-core/src/core/decode/decodeUnsignedTx.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,20 @@ describe('decodeUnsignedTx', () => {

itDecodesBalancesTransferCommon(decoded);
});

it('Should decode balances::transfer for an immortal era', () => {
const adjustedOptions = {
...POLKADOT_25_TEST_OPTIONS,
isImmortalEra: true,
};
const unsigned = balancesTransfer(
TEST_METHOD_ARGS.balances.transfer,
TEST_BASE_TX_INFO,
adjustedOptions
);

const decoded = decodeUnsignedTx(unsigned, adjustedOptions);

expect(decoded.eraPeriod).toBe(0);
});
});
10 changes: 8 additions & 2 deletions packages/txwrapper-core/src/core/decode/decodeUnsignedTx.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* @ignore
*/ /** */
import { hexToNumber } from '@polkadot/util';

import {
DecodedUnsignedTx,
OptionsWithMeta,
Expand All @@ -18,20 +20,24 @@ export function decodeUnsignedTx(
unsigned: UnsignedTransaction,
options: OptionsWithMeta
): DecodedUnsignedTx {
const { metadataRpc, registry, asCallsOnlyArg } = options;
const { metadataRpc, registry, asCallsOnlyArg, isImmortalEra } = options;

registry.setMetadata(createMetadata(registry, metadataRpc, asCallsOnlyArg));

const methodCall = registry.createType('Call', unsigned.method);
const method = toTxMethod(registry, methodCall);

const eraPeriod = isImmortalEra
? hexToNumber(registry.createType('ImmortalEra', unsigned.era).toHex())
: registry.createType('MortalEra', unsigned.era).period.toNumber();

return {
address: unsigned.address,
blockHash: unsigned.blockHash,
blockNumber: registry
.createType('BlockNumber', unsigned.blockNumber)
.toNumber(),
eraPeriod: registry.createType('MortalEra', unsigned.era).period.toNumber(),
eraPeriod,
genesisHash: unsigned.genesisHash,
metadataRpc,
method,
Expand Down
69 changes: 64 additions & 5 deletions packages/txwrapper-core/src/core/method/defineMethod.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import {
TEST_BASE_TX_INFO,
TEST_METHOD_ARGS,
} from '../../test-helpers/';
import { defineMethod } from './defineMethod';
import { createEra, defineMethod, MethodErrorMessages } from './defineMethod';

describe('defineMethod', () => {
const { InvalidEraPeriodTooLow, InvalidEraPeriodTooHigh } =
MethodErrorMessages;

it('should create correct default era', () => {
const txBaseInfo = {
...TEST_BASE_TX_INFO,
Expand All @@ -31,11 +34,38 @@ describe('defineMethod', () => {
expect(unsigned.era).toBe('0xe500');
});

it('should handle `info.eraPeriod` correctly when 0', () => {
it('should handle `info.eraPeriod` correctly when less than 4', () => {
const txBaseInfo = {
...TEST_BASE_TX_INFO,
eraPeriod: 0,
};
expect(() =>
defineMethod(
{
...txBaseInfo,
method: {
args: {},
name: 'chill',
pallet: 'staking',
},
},
POLKADOT_25_TEST_OPTIONS
)
).toThrow(InvalidEraPeriodTooLow);
});

it('should handle `info.eraPeriod` when `isImmortalEra` is true', () => {
const txBaseInfo = {
...TEST_BASE_TX_INFO,
eraPeriod: 0,
};
/**
* Adds isImmortalEra to the options.
*/
const adjustedOptions = {
...POLKADOT_25_TEST_OPTIONS,
isImmortalEra: true,
};
const unsigned = defineMethod(
{
...txBaseInfo,
Expand All @@ -45,16 +75,16 @@ describe('defineMethod', () => {
pallet: 'staking',
},
},
POLKADOT_25_TEST_OPTIONS
adjustedOptions
);

expect(unsigned.era).toBe('0x2100');
expect(unsigned.era).toBe('0x00');
});

it('should work', () => {
const txBaseInfo = {
...TEST_BASE_TX_INFO,
eraPeriod: 2,
eraPeriod: 4,
};
const unsigned = defineMethod(
{
Expand Down Expand Up @@ -121,4 +151,33 @@ describe('defineMethod', () => {
() => new Metadata(registry, unsignedPayload.metadataRpc)
).not.toThrow();
});

describe('createEra', () => {
const { registry } = POLKADOT_9122_TEST_OPTIONS;

it('Should handle values less than 4 correctly', () => {
expect(() => {
createEra(registry, { kind: 'mortal', blockNumber: 10, period: 0 });
}).toThrowError(InvalidEraPeriodTooLow);
expect(() => {
createEra(registry, { kind: 'mortal', blockNumber: 10, period: 3 });
}).toThrowError(InvalidEraPeriodTooLow);
});

it('Should handle values greater than 65536 correctly', () => {
expect(() => {
createEra(registry, { kind: 'mortal', blockNumber: 10, period: 65537 });
}).toThrowError(InvalidEraPeriodTooHigh);
expect(() => {
createEra(registry, { kind: 'mortal', blockNumber: 10, period: 70000 });
}).toThrowError(InvalidEraPeriodTooHigh);
});

it('Should handle immortal transactions correctly', () => {
const eraImmortal = createEra(registry, { kind: 'immortal' });

expect(eraImmortal.isImmortalEra).toBe(true);
expect(eraImmortal.toHex()).toBe('0x00');
});
});
});
99 changes: 83 additions & 16 deletions packages/txwrapper-core/src/core/method/defineMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
* @ignore
*/ /** */
import { EXTRINSIC_VERSION } from '@polkadot/types/extrinsic/v4/Extrinsic';
import { ExtrinsicEra } from '@polkadot/types/interfaces/extrinsics';
import { ModuleExtrinsics } from '@polkadot/types/metadata/decorate/types';
import { stringCamelCase } from '@polkadot/util';

import { OptionsWithMeta, TxInfo, UnsignedTransaction } from '../../types/';
import {
OptionsWithMeta,
TxInfo,
TypeRegistry,
UnsignedTransaction,
} from '../../types/';
import { createDecoratedTx, createMetadata } from '../metadata';

/**
Expand All @@ -22,6 +28,69 @@ const DEFAULTS = {
eraPeriod: 64,
};

type EraOptions =
| { kind: 'immortal' }
| { kind: 'mortal'; blockNumber: number; period?: number };

/**
* Error messages for defineMethod
*/
export enum MethodErrorMessages {
// An era period cannot be less than 4
InvalidEraPeriodTooLow = 'lowest possible era period for a mortal tx is 4',
// An era period cannot be greater than 65536
InvalidEraPeriodTooHigh = 'largest possible era period for a mortal tx is 65536',
// Decorated tx doesn't have the inputted pallet or method
InvalidPalletOrMethod = 'pallet or method not found in metadata',
}

/**
* Check the information relevant to an era period, and return the correct
* `ExtrinsicEra` as an Immortal or Mortal era.
*
* @param registry
* @param options
* @returns
*/
export function createEra(
registry: TypeRegistry,
options: EraOptions
): ExtrinsicEra {
const { InvalidEraPeriodTooLow, InvalidEraPeriodTooHigh } =
MethodErrorMessages;
/**
* Immortal transactions will be represented by the default value '0x00' for
* an era.
*/
if (options.kind === 'immortal') {
return registry.createType('ExtrinsicEra');
}

/**
* When the eraPeriod is not defined, set it to a default value of 64
*/
const eraPeriod =
options.period === undefined ? DEFAULTS.eraPeriod : options.period;

/**
* An era period cannot be less than 4 or greater than 65536.
* ie. (https://github.com/paritytech/substrate/pull/758)
*
* It is encouraged to send mortal transactions, but in the use case for an immortal transaction
* instead of passing in zero, you must use the `option`, `isImmortalEra`.
*/
if (eraPeriod < 4) {
throw Error(InvalidEraPeriodTooLow);
} else if (eraPeriod > 65536) {
throw Error(InvalidEraPeriodTooHigh);
}

return registry.createType('ExtrinsicEra', {
current: options.blockNumber,
period: eraPeriod,
});
}

/**
* Helper function to construct an offline method.
*
Expand All @@ -38,7 +107,9 @@ export function defineMethod(
asCallsOnlyArg,
signedExtensions,
userExtensions,
isImmortalEra,
} = options;
const { InvalidPalletOrMethod } = MethodErrorMessages;
const generatedMetadata = createMetadata(
registry,
metadataRpc,
Expand All @@ -53,7 +124,7 @@ export function defineMethod(
!!tx[info.method.pallet] &&
(tx[info.method.pallet] as unknown as ModuleExtrinsics)[info.method.name];
if (!methodFunction) {
throw new Error('pallet or method not found in metadata');
throw new Error(InvalidPalletOrMethod);
}

const method = methodFunction(
Expand All @@ -72,25 +143,21 @@ export function defineMethod(
})
).toHex();

/**
* If the `info.eraPeriod` is set use it. (This also checks for the edgecase zero).
* As a last resort, it will use the default value.
*/
const eraPeriod =
info.eraPeriod === 0 || info.eraPeriod
? info.eraPeriod
: DEFAULTS.eraPeriod;
const eraOptions: EraOptions = isImmortalEra
? { kind: 'immortal' }
: {
kind: 'mortal',
blockNumber: info.blockNumber,
period: info.eraPeriod,
};

const extrinsicEra = createEra(registry, eraOptions);

return {
address: info.address,
blockHash: info.blockHash,
blockNumber: registry.createType('BlockNumber', info.blockNumber).toHex(),
era: registry
.createType('ExtrinsicEra', {
current: info.blockNumber,
period: eraPeriod,
})
.toHex(),
era: extrinsicEra.toHex(),
genesisHash: info.genesisHash,
metadataRpc: generatedMetadata.toHex(),
method,
Expand Down
8 changes: 8 additions & 0 deletions packages/txwrapper-core/src/types/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,12 @@ export interface Options {
* The type registry of the runtime.
*/
registry: TypeRegistry;
/**
* Option to choose whether the constructed transaction will be immortal. If
* immortal the default value will be '0x00', and when decoded it will return 0.
* This option is used exclusively for unsigned transactions.
*
* Note: When creating an Immortal tx, the blockHash should be set as the genesis hash.
*/
isImmortalEra?: boolean;
}

0 comments on commit ab5873f

Please sign in to comment.