Skip to content

Commit

Permalink
feat: reject instruction
Browse files Browse the repository at this point in the history
  • Loading branch information
shuffledex committed Oct 13, 2020
1 parent 1854613 commit 9edec2c
Show file tree
Hide file tree
Showing 5 changed files with 348 additions and 3 deletions.
29 changes: 27 additions & 2 deletions src/api/entities/Instruction/__tests__/index.ts
@@ -1,9 +1,10 @@
import { u64 } from '@polkadot/types';
import BigNumber from 'bignumber.js';
import sinon from 'sinon';
import sinon, { SinonStub } from 'sinon';

import { Entity, Instruction } from '~/api/entities';
import { Context } from '~/base';
import { Params, rejectInstruction } from '~/api/procedures/rejectInstruction';
import { Context, TransactionQueue } from '~/base';
import { dsMockUtils, entityMockUtils } from '~/testUtils/mocks';
import { Mocked } from '~/testUtils/types';
import { InstructionStatus, InstructionType } from '~/types';
Expand All @@ -22,6 +23,10 @@ jest.mock(
describe('Instruction class', () => {
let context: Mocked<Context>;
let instruction: Instruction;
let prepareRejectInstructionStub: SinonStub<
[Params, Context],
Promise<TransactionQueue<void, unknown[][]>>
>;

let id: BigNumber;

Expand Down Expand Up @@ -171,4 +176,24 @@ describe('Instruction class', () => {
expect(leg.token).toEqual(entityMockUtils.getSecurityTokenInstance());
});
});

describe('method: reject', () => {
beforeAll(() => {
prepareRejectInstructionStub = sinon.stub(rejectInstruction, 'prepare');
});

afterAll(() => {
sinon.restore();
});

test('should prepare the procedure and return the resulting transaction queue', async () => {
const expectedQueue = ('someQueue' as unknown) as TransactionQueue<void>;

prepareRejectInstructionStub.withArgs({ id }, context).resolves(expectedQueue);

const queue = await instruction.reject();

expect(queue).toBe(expectedQueue);
});
});
});
14 changes: 13 additions & 1 deletion src/api/entities/Instruction/index.ts
@@ -1,7 +1,8 @@
import BigNumber from 'bignumber.js';

import { Entity, Identity, SecurityToken } from '~/api/entities';
import { Context } from '~/base';
import { rejectInstruction } from '~/api/procedures';
import { Context, TransactionQueue } from '~/base';
import {
balanceToBigNumber,
identityIdToString,
Expand Down Expand Up @@ -120,4 +121,15 @@ export class Instruction extends Entity<UniqueIdentifiers> {
};
});
}

/**
* Reject this instruction
*
* @note reject on `SettleOnAuthorization` will execute the settlement and it will fail immediately.
* @note reject on `SettleOnBlock` behaves just like unauthorize
*/
public reject(): Promise<TransactionQueue<void>> {
const { id, context } = this;
return rejectInstruction.prepare({ id }, context);
}
}
207 changes: 207 additions & 0 deletions src/api/procedures/__tests__/rejectInstruction.ts
@@ -0,0 +1,207 @@
import { u64 } from '@polkadot/types';
import BigNumber from 'bignumber.js';
import {
AuthorizationStatus as MeshAuthorizationStatus,
PortfolioId as MeshPortfolioId,
} from 'polymesh-types/types';
import sinon from 'sinon';

import { Params, prepareRejectInstruction } from '~/api/procedures/rejectInstruction';
import { Context } from '~/base';
import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mocks';
import { Mocked } from '~/testUtils/types';
import {
AuthorizationStatus,
InstructionDetails,
InstructionStatus,
InstructionType,
} from '~/types';
import { PortfolioId } from '~/types/internal';
import * as utilsModule from '~/utils';

jest.mock(
'~/api/entities/Instruction',
require('~/testUtils/mocks/entities').mockInstructionModule('~/api/entities/Instruction')
);

describe('rejectInstruction procedure', () => {
const id = new BigNumber(1);
const rawInstructionId = dsMockUtils.createMockU64(1);
const rawPortfolioId = dsMockUtils.createMockPortfolioId({
did: dsMockUtils.createMockIdentityId('someDid'),
kind: dsMockUtils.createMockPortfolioKind('Default'),
});
let mockContext: Mocked<Context>;
let numberToU64Stub: sinon.SinonStub<[number | BigNumber, Context], u64>;
let portfolioIdToMeshPortfolioIdStub: sinon.SinonStub<[PortfolioId, Context], MeshPortfolioId>;
let meshAuthorizationStatusToAuthorizationStatusStub: sinon.SinonStub<
[MeshAuthorizationStatus],
AuthorizationStatus
>;

beforeAll(() => {
dsMockUtils.initMocks();
procedureMockUtils.initMocks();
entityMockUtils.initMocks();
numberToU64Stub = sinon.stub(utilsModule, 'numberToU64');
portfolioIdToMeshPortfolioIdStub = sinon.stub(utilsModule, 'portfolioIdToMeshPortfolioId');
meshAuthorizationStatusToAuthorizationStatusStub = sinon.stub(
utilsModule,
'meshAuthorizationStatusToAuthorizationStatus'
);
});

let addTransactionStub: sinon.SinonStub;

beforeEach(() => {
addTransactionStub = procedureMockUtils.getAddTransactionStub();
mockContext = dsMockUtils.getContextInstance();
numberToU64Stub.returns(rawInstructionId);
portfolioIdToMeshPortfolioIdStub.returns(rawPortfolioId);
});

afterEach(() => {
entityMockUtils.reset();
procedureMockUtils.reset();
dsMockUtils.reset();
});

afterAll(() => {
entityMockUtils.cleanup();
procedureMockUtils.cleanup();
dsMockUtils.cleanup();
});

test('should throw an error if instruction is not in pending state', () => {
entityMockUtils.configureMocks({
instructionOptions: {
details: {
status: InstructionStatus.Unknown,
} as InstructionDetails,
},
});

const proc = procedureMockUtils.getInstance<Params, void>(mockContext);

return expect(
prepareRejectInstruction.call(proc, {
id,
})
).rejects.toThrow('The Instruction must be in pending state');
});

test('should throw an error if instruction is blocked', async () => {
const validFrom = new Date('12/12/2050');

entityMockUtils.configureMocks({
instructionOptions: {
details: {
status: InstructionStatus.Pending,
validFrom,
} as InstructionDetails,
},
});

const proc = procedureMockUtils.getInstance<Params, void>(mockContext);

let error;

try {
await prepareRejectInstruction.call(proc, {
id,
});
} catch (err) {
error = err;
}

expect(error.message).toBe('The instruction has not reached its validity period');
expect(error.data.validFrom).toBe(validFrom);
});

test('should throw an error if the instruction can not be modified', async () => {
const endBlock = new BigNumber(10);

entityMockUtils.configureMocks({
instructionOptions: {
details: {
status: InstructionStatus.Pending,
type: InstructionType.SettleOnBlock,
endBlock,
} as InstructionDetails,
},
});

const proc = procedureMockUtils.getInstance<Params, void>(mockContext);

let error;

try {
await prepareRejectInstruction.call(proc, {
id,
});
} catch (err) {
error = err;
}

expect(error.message).toBe(
'The instruction cannot be modified; it has already reached its end block'
);
expect(error.data.endBlock).toBe(endBlock);
});

test('should throw an error if authorization status is rejected', () => {
entityMockUtils.configureMocks({
instructionOptions: {
details: {
status: InstructionStatus.Pending,
type: InstructionType.SettleOnBlock,
endBlock: new BigNumber(1000),
} as InstructionDetails,
},
});

const rawAuthorizationStatus = dsMockUtils.createMockAuthorizationStatus('Rejected');
dsMockUtils.createQueryStub('settlement', 'userAuths').resolves(rawAuthorizationStatus);
meshAuthorizationStatusToAuthorizationStatusStub
.withArgs(rawAuthorizationStatus)
.returns(AuthorizationStatus.Rejected);

const proc = procedureMockUtils.getInstance<Params, void>(mockContext);

return expect(
prepareRejectInstruction.call(proc, {
id,
})
).rejects.toThrow('The instruction cannot be rejected');
});

/*
test('should add an authorize instruction transaction to the queue', async () => {
entityMockUtils.configureMocks({
instructionOptions: {
details: {
status: InstructionStatus.Pending,
} as InstructionDetails,
},
});
const rawAuthorizationStatus = dsMockUtils.createMockAuthorizationStatus('Pending');
dsMockUtils.createQueryStub('settlement', 'userAuths').resolves(rawAuthorizationStatus);
meshAuthorizationStatusToAuthorizationStatusStub
.withArgs(rawAuthorizationStatus)
.returns(AuthorizationStatus.Pending);
const proc = procedureMockUtils.getInstance<Params, void>(mockContext);
const transaction = dsMockUtils.createTxStub('settlement', 'authorizeInstruction');
await prepareRejectInstruction.call(proc, {
id,
});
sinon.assert.calledWith(addTransactionStub, transaction, {}, rawInstructionId, [
rawPortfolioId,
]);
});
*/
});
1 change: 1 addition & 0 deletions src/api/procedures/index.ts
Expand Up @@ -33,3 +33,4 @@ export {
ModifyPrimaryIssuanceAgentParams,
} from './modifyPrimaryIssuanceAgent';
export { removePrimaryIssuanceAgent } from './removePrimaryIssuanceAgent';
export { rejectInstruction } from './rejectInstruction';
100 changes: 100 additions & 0 deletions src/api/procedures/rejectInstruction.ts
@@ -0,0 +1,100 @@
import BigNumber from 'bignumber.js';

import { Instruction } from '~/api/entities';
import { PolymeshError, Procedure } from '~/base';
import { AuthorizationStatus, ErrorCode, InstructionStatus, InstructionType } from '~/types';
import {
meshAuthorizationStatusToAuthorizationStatus,
numberToU64,
portfolioIdToMeshPortfolioId,
} from '~/utils';

/**
* @hidden
*/
export type Params = { id: BigNumber };

/**
* @hidden
*/
export async function prepareRejectInstruction(
this: Procedure<Params, void>,
args: Params
): Promise<void> {
const {
context: {
polymeshApi: {
tx,
query: { settlement },
},
},
context,
} = this;

const { id } = args;

const instruction = new Instruction({ id }, context);

const [details, currentIdentity] = await Promise.all([
instruction.details(),
context.getCurrentIdentity(),
]);

const { status, validFrom } = details;

if (status !== InstructionStatus.Pending) {
throw new PolymeshError({
code: ErrorCode.ValidationError,
message: 'The Instruction must be in pending state',
});
}

if (validFrom) {
const now = new Date();

if (now < validFrom) {
throw new PolymeshError({
code: ErrorCode.ValidationError,
message: 'The instruction has not reached its validity period',
data: {
validFrom,
},
});
}
}

if (details.type === InstructionType.SettleOnBlock) {
const latestBlock = await context.getLatestBlock();
const { endBlock } = details;

if (latestBlock >= endBlock) {
throw new PolymeshError({
code: ErrorCode.ValidationError,
message: 'The instruction cannot be modified; it has already reached its end block',
data: {
currentBlock: latestBlock,
endBlock,
},
});
}
}

const rawInstructionId = numberToU64(id, context);
const rawPortfolioId = portfolioIdToMeshPortfolioId({ did: currentIdentity.did }, context);
const rawAuthorizationStatus = await settlement.userAuths(rawPortfolioId, rawInstructionId);
const authorizationStatus = meshAuthorizationStatusToAuthorizationStatus(rawAuthorizationStatus);

if (authorizationStatus === AuthorizationStatus.Rejected) {
throw new PolymeshError({
code: ErrorCode.ValidationError,
message: 'The instruction cannot be rejected',
});
}

this.addTransaction(tx.settlement.rejectInstruction, {}, rawInstructionId, [rawPortfolioId]);
}

/**
* @hidden
*/
export const rejectInstruction = new Procedure(prepareRejectInstruction);

0 comments on commit 9edec2c

Please sign in to comment.