Skip to content

Commit

Permalink
feat(identity): add getInstructions to fetch grouped instructions
Browse files Browse the repository at this point in the history
Also deprecated `getPendingInstructions`
  • Loading branch information
monitz87 committed Jul 27, 2021
1 parent 2b098f1 commit f5714b8
Show file tree
Hide file tree
Showing 4 changed files with 273 additions and 1 deletion.
175 changes: 175 additions & 0 deletions src/api/entities/Identity/__tests__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ jest.mock(
'~/api/entities/DefaultPortfolio'
)
);
jest.mock(
'~/api/entities/Instruction',
require('~/testUtils/mocks/entities').mockInstructionModule('~/api/entities/Instruction')
);

describe('Identity class', () => {
let context: MockContext;
Expand Down Expand Up @@ -851,7 +855,178 @@ describe('Identity class', () => {
});
});

describe('method: getInstructions', () => {
afterAll(() => {
sinon.restore();
});

test('should return all instructions in which the identity is involved, grouped by status', async () => {
const id1 = new BigNumber(1);
const id2 = new BigNumber(2);
const id3 = new BigNumber(3);
const id4 = new BigNumber(4);
const id5 = new BigNumber(5);

const did = 'someDid';
const identity = new Identity({ did }, context);

const defaultPortfolioDid = 'someDid';
const numberedPortfolioDid = 'someDid';
const numberedPortfolioId = new BigNumber(1);

const defaultPortfolio = entityMockUtils.getDefaultPortfolioInstance({
did: defaultPortfolioDid,
isCustodiedBy: true,
});

const numberedPortfolio = entityMockUtils.getNumberedPortfolioInstance({
did: numberedPortfolioDid,
id: numberedPortfolioId,
isCustodiedBy: false,
});

identity.portfolios.getPortfolios = sinon
.stub()
.resolves([defaultPortfolio, numberedPortfolio]);

identity.portfolios.getCustodiedPortfolios = sinon.stub().resolves({ data: [], next: null });

const portfolioLikeToPortfolioIdStub = sinon.stub(
utilsConversionModule,
'portfolioLikeToPortfolioId'
);

portfolioLikeToPortfolioIdStub
.withArgs(defaultPortfolio)
.returns({ did: defaultPortfolioDid, number: undefined });
portfolioLikeToPortfolioIdStub
.withArgs(numberedPortfolio)
.returns({ did: numberedPortfolioDid, number: numberedPortfolioId });

const rawPortfolio = dsMockUtils.createMockPortfolioId({
did: dsMockUtils.createMockIdentityId(did),
kind: dsMockUtils.createMockPortfolioKind('Default'),
});

const portfolioIdToMeshPortfolioIdStub = sinon.stub(
utilsConversionModule,
'portfolioIdToMeshPortfolioId'
);

portfolioIdToMeshPortfolioIdStub
.withArgs({ did, number: undefined }, context)
.returns(rawPortfolio);

const userAuthsStub = dsMockUtils.createQueryStub('settlement', 'userAffirmations');

const rawId1 = dsMockUtils.createMockU64(id1.toNumber());
const rawId2 = dsMockUtils.createMockU64(id2.toNumber());
const rawId3 = dsMockUtils.createMockU64(id3.toNumber());
const rawId4 = dsMockUtils.createMockU64(id4.toNumber());
const rawId5 = dsMockUtils.createMockU64(id5.toNumber());

const entriesStub = sinon.stub();
entriesStub
.withArgs(rawPortfolio)
.resolves([
tuple(
{ args: [rawPortfolio, rawId1] },
dsMockUtils.createMockAffirmationStatus('Affirmed')
),
tuple(
{ args: [rawPortfolio, rawId2] },
dsMockUtils.createMockAffirmationStatus('Pending')
),
tuple(
{ args: [rawPortfolio, rawId3] },
dsMockUtils.createMockAffirmationStatus('Rejected')
),
tuple(
{ args: [rawPortfolio, rawId4] },
dsMockUtils.createMockAffirmationStatus('Affirmed')
),
tuple(
{ args: [rawPortfolio, rawId5] },
dsMockUtils.createMockAffirmationStatus('Unknown')
),
]);

userAuthsStub.entries = entriesStub;

const instructionDetailsStub = dsMockUtils.createQueryStub(
'settlement',
'instructionDetails',
{
multi: [],
}
);

const multiStub = sinon.stub();

multiStub.withArgs([rawId1, rawId2, rawId3, rawId4, rawId5]).resolves([
dsMockUtils.createMockInstruction({
instruction_id: dsMockUtils.createMockU64(id1.toNumber()),
venue_id: dsMockUtils.createMockU64(),
status: dsMockUtils.createMockInstructionStatus('Pending'),
settlement_type: dsMockUtils.createMockSettlementType('SettleOnAffirmation'),
created_at: dsMockUtils.createMockOption(),
trade_date: dsMockUtils.createMockOption(),
value_date: dsMockUtils.createMockOption(),
}),
dsMockUtils.createMockInstruction({
instruction_id: dsMockUtils.createMockU64(id2.toNumber()),
venue_id: dsMockUtils.createMockU64(),
status: dsMockUtils.createMockInstructionStatus('Pending'),
settlement_type: dsMockUtils.createMockSettlementType('SettleOnAffirmation'),
created_at: dsMockUtils.createMockOption(),
trade_date: dsMockUtils.createMockOption(),
value_date: dsMockUtils.createMockOption(),
}),
dsMockUtils.createMockInstruction({
instruction_id: dsMockUtils.createMockU64(id3.toNumber()),
venue_id: dsMockUtils.createMockU64(),
status: dsMockUtils.createMockInstructionStatus('Pending'),
settlement_type: dsMockUtils.createMockSettlementType('SettleOnAffirmation'),
created_at: dsMockUtils.createMockOption(),
trade_date: dsMockUtils.createMockOption(),
value_date: dsMockUtils.createMockOption(),
}),
dsMockUtils.createMockInstruction({
instruction_id: dsMockUtils.createMockU64(id4.toNumber()),
venue_id: dsMockUtils.createMockU64(),
status: dsMockUtils.createMockInstructionStatus('Failed'),
settlement_type: dsMockUtils.createMockSettlementType('SettleOnAffirmation'),
created_at: dsMockUtils.createMockOption(),
trade_date: dsMockUtils.createMockOption(),
value_date: dsMockUtils.createMockOption(),
}),
dsMockUtils.createMockInstruction({
instruction_id: dsMockUtils.createMockU64(id4.toNumber()),
venue_id: dsMockUtils.createMockU64(),
status: dsMockUtils.createMockInstructionStatus('Unknown'),
settlement_type: dsMockUtils.createMockSettlementType('SettleOnAffirmation'),
created_at: dsMockUtils.createMockOption(),
trade_date: dsMockUtils.createMockOption(),
value_date: dsMockUtils.createMockOption(),
}),
]);

instructionDetailsStub.multi = multiStub;

const result = await identity.getInstructions();

expect(result.affirmed).toEqual([entityMockUtils.getInstructionInstance({ id: id1 })]);
expect(result.pending).toEqual([entityMockUtils.getInstructionInstance({ id: id2 })]);
expect(result.rejected).toEqual([entityMockUtils.getInstructionInstance({ id: id3 })]);
expect(result.failed).toEqual([entityMockUtils.getInstructionInstance({ id: id4 })]);
});
});

describe('method: getPendingInstructions', () => {
afterAll(() => {
sinon.restore();
});

test('should return all pending instructions in which the identity is involved', async () => {
const id1 = new BigNumber(1);
const id2 = new BigNumber(2);
Expand Down
75 changes: 75 additions & 0 deletions src/api/entities/Identity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
DistributionWithDetails,
Ensured,
ErrorCode,
GroupedInstructions,
isCddProviderRole,
isPortfolioCustodianRole,
isTickerOwnerRole,
Expand Down Expand Up @@ -561,8 +562,82 @@ export class Identity extends Entity<UniqueIdentifiers, string> {
return scopeIdToString(scopeId);
}

/**
* Retrieve all Instructions where this Identity is a participant,
* grouped by status
*/
public async getInstructions(): Promise<GroupedInstructions> {
const {
context: {
polymeshApi: {
query: { settlement },
},
},
did,
portfolios,
context,
} = this;

const ownedPortfolios = await portfolios.getPortfolios();

const [ownedCustodiedPortfolios, { data: custodiedPortfolios }] = await Promise.all([
P.filter(ownedPortfolios, portfolio => portfolio.isCustodiedBy({ identity: did })),
this.portfolios.getCustodiedPortfolios(),
]);

const allPortfolios = [...ownedCustodiedPortfolios, ...custodiedPortfolios];

const portfolioIds = allPortfolios.map(portfolioLikeToPortfolioId);

await P.map(portfolioIds, portfolioId => assertPortfolioExists(portfolioId, context));

const portfolioIdChunks = chunk(portfolioIds, MAX_CONCURRENT_REQUESTS);

const affirmed: Instruction[] = [];
const rejected: Instruction[] = [];
const pending: Instruction[] = [];
const failed: Instruction[] = [];

await P.each(portfolioIdChunks, async portfolioIdChunk => {
const auths = await P.map(portfolioIdChunk, portfolioId =>
settlement.userAffirmations.entries(portfolioIdToMeshPortfolioId(portfolioId, context))
);

const uniqueEntries = uniqBy(
flatten(auths).map(([key, status]) => ({ id: key.args[1], status })),
({ id }) => id.toNumber()
);
const instructions = await settlement.instructionDetails.multi<MeshInstruction>(
uniqueEntries.map(({ id }) => id)
);

uniqueEntries.forEach(({ id, status }, index) => {
const instruction = new Instruction({ id: u64ToBigNumber(id) }, context);

if (instructions[index].status.isFailed) {
failed.push(instruction);
} else if (status.isAffirmed) {
affirmed.push(instruction);
} else if (status.isRejected) {
rejected.push(instruction);
} else if (status.isPending) {
pending.push(instruction);
}
});
});

return {
affirmed,
rejected,
pending,
failed,
};
}

/**
* Retrieve all pending Instructions involving this Identity
*
* @deprecated in favor of `getInstructions`
*/
public async getPendingInstructions(): Promise<Instruction[]> {
const {
Expand Down
2 changes: 1 addition & 1 deletion src/testUtils/mocks/dataSources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2587,7 +2587,7 @@ export const createMockVenue = (venue?: {
* NOTE: `isEmpty` will be set to true if no value is passed
*/
export const createMockInstructionStatus = (
instructionStatus?: 'Pending' | 'Unknown'
instructionStatus?: 'Pending' | 'Unknown' | 'Failed'
): InstructionStatus => {
return createMockEnum(instructionStatus) as InstructionStatus;
};
Expand Down
22 changes: 22 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
DefaultPortfolio,
DividendDistribution,
Identity,
Instruction,
NumberedPortfolio,
/*, Proposal */
SecurityToken,
Expand Down Expand Up @@ -1094,6 +1095,27 @@ export interface SignerValue {
value: string;
}

export interface GroupedInstructions {
/**
* Instructions that have already been affirmed by the Identity
*/
affirmed: Instruction[];
/**
* Instructions that have already been rejected by the Identity
*/
rejected: Instruction[];
/**
* Instructions that still need to be affirmed/rejected by the Identity
*/
pending: Instruction[];
/**
* Instructions that failed in their execution (can be rescheduled).
* This group supercedes the other three, so for example, a failed Instruction
* might also belong in the `affirmed` group, but it will only be included in this one
*/
failed: Instruction[];
}

export { TxTags, TxTag, ModuleName };
export { Signer as PolkadotSigner } from '@polkadot/api/types';
export { EventRecord } from '@polkadot/types/interfaces';
Expand Down

0 comments on commit f5714b8

Please sign in to comment.