Skip to content

Commit

Permalink
feat: createPortfolio method
Browse files Browse the repository at this point in the history
  • Loading branch information
shuffledex committed Oct 19, 2020
1 parent 5a261ad commit e68c1d0
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 3 deletions.
15 changes: 13 additions & 2 deletions src/api/entities/Identity/Portfolios.ts
@@ -1,6 +1,17 @@
import { Identity, Namespace } from '~/api/entities';
import { Identity, Namespace, NumberedPortfolio } from '~/api/entities';
import { createPortfolio } from '~/api/procedures';
import { TransactionQueue } from '~/base';

/**
* Handles all Portfolio related functionality on the Identity side
*/
export class Portfolios extends Namespace<Identity> {}
export class Portfolios extends Namespace<Identity> {
/**
* Create a new portfolio to the current Identity
*/
public createPortfolio(args: { name: string }): Promise<TransactionQueue<NumberedPortfolio>> {
const { name } = args;
const { context } = this;
return createPortfolio.prepare({ name }, context);
}
}
53 changes: 52 additions & 1 deletion src/api/entities/Identity/__tests__/Portfolios.ts
@@ -1,9 +1,60 @@
import { Namespace } from '~/api/entities';
import sinon, { SinonStub } from 'sinon';

import { Namespace, NumberedPortfolio } from '~/api/entities';
import { Identity } from '~/api/entities/Identity';
import { createPortfolio } from '~/api/procedures';
import { Context, TransactionQueue } from '~/base';
import { dsMockUtils } from '~/testUtils/mocks';
import { Mocked } from '~/testUtils/types';

import { Portfolios } from '../Portfolios';

describe('Portfolios class', () => {
let context: Mocked<Context>;
let portfolios: Portfolios;
let identity: Identity;
let prepareCreatePortfolioStub: SinonStub;

beforeAll(() => {
dsMockUtils.initMocks();
});

beforeEach(() => {
context = dsMockUtils.getContextInstance();
identity = new Identity({ did: 'someDid' }, context);
portfolios = new Portfolios(identity, context);
});

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

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

test('should extend namespace', () => {
expect(Portfolios.prototype instanceof Namespace).toBe(true);
});

describe('method: createPortfolio', () => {
beforeAll(() => {
prepareCreatePortfolioStub = sinon.stub(createPortfolio, 'prepare');
});

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

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

prepareCreatePortfolioStub.withArgs({ name }, context).resolves(expectedQueue);

const queue = await portfolios.createPortfolio({ name });

expect(queue).toBe(expectedQueue);
});
});
});
135 changes: 135 additions & 0 deletions src/api/procedures/__tests__/createPortfolio.ts
@@ -0,0 +1,135 @@
import { Bytes, u64 } from '@polkadot/types';
import { ISubmittableResult } from '@polkadot/types/types';
import BigNumber from 'bignumber.js';
import { IdentityId, PortfolioName } from 'polymesh-types/types';
import sinon from 'sinon';

import { NumberedPortfolio } from '~/api/entities';
import {
createPortfolioResolver,
Params,
prepareCreatePortfolio,
} from '~/api/procedures/createPortfolio';
import { Context, PostTransactionValue } from '~/base';
import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mocks';
import { Mocked } from '~/testUtils/types';
import { PolymeshTx } from '~/types/internal';
import { tuple } from '~/types/utils';
import * as utilsModule from '~/utils';

describe('createPortfolio procedure', () => {
let mockContext: Mocked<Context>;
let numberedPortfolio: PostTransactionValue<NumberedPortfolio>;
let bytesToStringStub: sinon.SinonStub;
let stringToBytesStub: sinon.SinonStub;
let rawPortfolios: [PortfolioName][];
let portfolioEntries: [[], PortfolioName][];
let portfoliosName: { name: string }[];
let newPortfolioName: string;
let addTransactionStub: sinon.SinonStub;
let rawNewPortfolioName: Bytes;

beforeAll(() => {
dsMockUtils.initMocks();
procedureMockUtils.initMocks();
entityMockUtils.initMocks();
numberedPortfolio = ('numberedPortfolio' as unknown) as PostTransactionValue<NumberedPortfolio>;
bytesToStringStub = sinon.stub(utilsModule, 'bytesToString');
stringToBytesStub = sinon.stub(utilsModule, 'stringToBytes');

portfoliosName = [
{
name: 'portfolioName1',
},
];

rawPortfolios = portfoliosName.map(({ name }) => tuple(dsMockUtils.createMockBytes(name)));

portfolioEntries = rawPortfolios.map(([name]) => tuple([], name));

newPortfolioName = 'newPortfolioName';
rawNewPortfolioName = dsMockUtils.createMockBytes(newPortfolioName);
});

beforeEach(() => {
mockContext = dsMockUtils.getContextInstance();
addTransactionStub = procedureMockUtils.getAddTransactionStub().returns([numberedPortfolio]);
bytesToStringStub.withArgs(rawPortfolios[0][0]).returns(portfoliosName[0].name);
stringToBytesStub.withArgs(newPortfolioName, mockContext).returns(rawNewPortfolioName);

dsMockUtils.createQueryStub('portfolio', 'portfolios', {
entries: [portfolioEntries[0]],
});
});

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

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

test('should throw an error if the portfolio name is duplicated', async () => {
const proc = procedureMockUtils.getInstance<Params, NumberedPortfolio>(mockContext);

return expect(
prepareCreatePortfolio.call(proc, { name: portfoliosName[0].name })
).rejects.toThrow('Already exists a portfolio with the same name');
});

test('should add a create portfolio transaction and an add documents transaction to the queue', async () => {
const proc = procedureMockUtils.getInstance<Params, NumberedPortfolio>(mockContext);
const createPortfolioTransaction = dsMockUtils.createTxStub('portfolio', 'createPortfolio');

const result = await prepareCreatePortfolio.call(proc, { name: newPortfolioName });

sinon.assert.calledWith(
addTransactionStub,
createPortfolioTransaction,
sinon.match({
resolvers: sinon.match.array,
}),
rawNewPortfolioName
);
expect(result).toBe(numberedPortfolio);
});
});

describe('createPortfolioResolver', () => {
const findEventRecordStub = sinon.stub(utilsModule, 'findEventRecord');
const did = 'someDid';
const rawIdentityId = dsMockUtils.createMockIdentityId(did);
const id = new BigNumber(1);
const rawId = dsMockUtils.createMockU64(id.toNumber());
let identityIdToStringStub: sinon.SinonStub<[IdentityId], string>;
let u64ToBigNumberStub: sinon.SinonStub<[u64], BigNumber>;

beforeAll(() => {
identityIdToStringStub = sinon.stub(utilsModule, 'identityIdToString');
u64ToBigNumberStub = sinon.stub(utilsModule, 'u64ToBigNumber');
});

beforeEach(() => {
identityIdToStringStub.withArgs(rawIdentityId).returns(did);
u64ToBigNumberStub.withArgs(rawId).returns(id);
findEventRecordStub.returns(dsMockUtils.createMockEventRecord([rawIdentityId, rawId]));
});

afterEach(() => {
sinon.reset();
findEventRecordStub.reset();
});

test('should return the new Numbered Portfolio', () => {
const fakeContext = {} as Context;

const result = createPortfolioResolver(fakeContext)({} as ISubmittableResult);

expect(result.id).toEqual(id);
});
});
86 changes: 86 additions & 0 deletions src/api/procedures/createPortfolio.ts
@@ -0,0 +1,86 @@
import { u64 } from '@polkadot/types';
import { ISubmittableResult } from '@polkadot/types/types';
import { IdentityId } from 'polymesh-types/types';

import { NumberedPortfolio } from '~/api/entities';
import { Context, PolymeshError, PostTransactionValue, Procedure } from '~/base';
import { ErrorCode } from '~/types';
import {
bytesToString,
findEventRecord,
identityIdToString,
stringToBytes,
stringToIdentityId,
u64ToBigNumber,
} from '~/utils';

/**
* @hidden
*/
export type Params = {
name: string;
};

/**
* @hidden
*/
export const createPortfolioResolver = (context: Context) => (
receipt: ISubmittableResult
): NumberedPortfolio => {
const eventRecord = findEventRecord(receipt, 'portfolio', 'PortfolioCreated');
const data = eventRecord.event.data;
const did = identityIdToString(data[0] as IdentityId);
const id = u64ToBigNumber(data[1] as u64);

return new NumberedPortfolio({ did, id }, context);
};

/**
* @hidden
*/
export async function prepareCreatePortfolio(
this: Procedure<Params, NumberedPortfolio>,
args: Params
): Promise<PostTransactionValue<NumberedPortfolio>> {
const {
context: {
polymeshApi: {
tx,
query: { portfolio },
},
},
context,
} = this;
const { name: portfolioName } = args;

const { did } = await context.getCurrentIdentity();

const rawPortfolios = await portfolio.portfolios.entries(stringToIdentityId(did, context));

const portfoliosNames: string[] = [];
rawPortfolios.forEach(([, name]) => portfoliosNames.push(bytesToString(name)));

if (portfoliosNames.includes(portfolioName)) {
throw new PolymeshError({
code: ErrorCode.ValidationError,
message: 'Already exists a portfolio with the same name',
});
}

const rawName = stringToBytes(portfolioName, context);

const [newNumberedPortfolio] = this.addTransaction(
tx.portfolio.createPortfolio,
{
resolvers: [createPortfolioResolver(context)],
},
rawName
);

return newNumberedPortfolio;
}

/**
* @hidden
*/
export const createPortfolio = new Procedure(prepareCreatePortfolio);
1 change: 1 addition & 0 deletions src/api/procedures/index.ts
Expand Up @@ -37,3 +37,4 @@ export {
modifyInstructionAuthorization,
ModifyInstructionAuthorizationParams,
} from './modifyInstructionAuthorization';
export { createPortfolio } from './createPortfolio';

0 comments on commit e68c1d0

Please sign in to comment.