generated from PolymeshAssociation/typescript-boilerplate
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5a261ad
commit e68c1d0
Showing
5 changed files
with
287 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters