Skip to content

Commit

Permalink
feat: moveFunds to owned portfolios
Browse files Browse the repository at this point in the history
  • Loading branch information
shuffledex committed Nov 3, 2020
1 parent 9e7a8f4 commit 4dd981c
Show file tree
Hide file tree
Showing 10 changed files with 635 additions and 3 deletions.
24 changes: 23 additions & 1 deletion src/api/entities/Portfolio/__tests__/index.ts
Expand Up @@ -4,7 +4,9 @@ import { PortfolioId, Ticker } from 'polymesh-types/types';
import sinon from 'sinon';

import { Entity, Identity, Portfolio, SecurityToken } from '~/api/entities';
import { Context } from '~/base';
import { NumberedPortfolio } from '~/api/entities/NumberedPortfolio';
import { moveFunds } from '~/api/procedures';
import { Context, TransactionQueue } from '~/base';
import { dsMockUtils } from '~/testUtils/mocks';
import { tuple } from '~/types/utils';
import * as utilsModule from '~/utils';
Expand Down Expand Up @@ -166,4 +168,24 @@ describe('Portfolio class', () => {
expect(result[1].locked).toEqual(new BigNumber(0));
});
});

describe('method: moveFunds', () => {
test('should prepare the procedure and return the resulting transaction queue', async () => {
const args = {
to: new NumberedPortfolio({ id: new BigNumber(1), did: 'someDid' }, context),
items: [{ token: 'someToken', amount: new BigNumber(100) }],
};
const portfolio = new Portfolio({ did: 'someDid' }, context);
const expectedQueue = ('someQueue' as unknown) as TransactionQueue<void>;

sinon
.stub(moveFunds, 'prepare')
.withArgs({ ...args, from: portfolio }, context)
.resolves(expectedQueue);

const queue = await portfolio.moveFunds(args);

expect(queue).toBe(expectedQueue);
});
});
});
13 changes: 12 additions & 1 deletion src/api/entities/Portfolio/index.ts
Expand Up @@ -3,7 +3,8 @@ import { values } from 'lodash';
import { Ticker } from 'polymesh-types/types';

import { Entity, Identity, SecurityToken } from '~/api/entities';
import { Context } from '~/base';
import { moveFunds, MoveFundsParams } from '~/api/procedures';
import { Context, TransactionQueue } from '~/base';
import { balanceToBigNumber, portfolioIdToMeshPortfolioId, tickerToString } from '~/utils';

import { PortfolioBalance } from './types';
Expand Down Expand Up @@ -130,4 +131,14 @@ export class Portfolio extends Entity<UniqueIdentifiers> {

return values(assetBalances);
}

/**
* Moves funds from one owned portfolio to another owned portfolio
*
* @param args.to - owned portfolio who will receive the funds
* @param args.items - list of tokens and amounts to be move
*/
public async moveFunds(args: MoveFundsParams): Promise<TransactionQueue<void>> {
return moveFunds.prepare({ ...args, from: this }, this.context);
}
}
5 changes: 5 additions & 0 deletions src/api/entities/Portfolio/types.ts
Expand Up @@ -7,3 +7,8 @@ export interface PortfolioBalance {
total: BigNumber;
locked: BigNumber;
}

export interface PortfolioItem {
token: string | SecurityToken;
amount: BigNumber;
}
288 changes: 288 additions & 0 deletions src/api/procedures/__tests__/moveFunds.ts
@@ -0,0 +1,288 @@
import BigNumber from 'bignumber.js';
import { MovePortfolioItem, PortfolioId as MeshPortfolioId } from 'polymesh-types/types';
import sinon from 'sinon';

import { DefaultPortfolio, NumberedPortfolio, SecurityToken } from '~/api/entities';
import { Params, prepareMoveFunds } from '~/api/procedures/moveFunds';
import { Context } from '~/base';
import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mocks';
import { Mocked } from '~/testUtils/types';
import { PortfolioBalance, PortfolioItem } from '~/types';
import { PortfolioId } from '~/types/internal';
import * as utilsModule from '~/utils';

jest.mock(
'~/api/entities/NumberedPortfolio',
require('~/testUtils/mocks/entities').mockNumberedPortfolioModule(
'~/api/entities/NumberedPortfolio'
)
);

jest.mock(
'~/api/entities/DefaultPortfolio',
require('~/testUtils/mocks/entities').mockDefaultPortfolioModule(
'~/api/entities/DefaultPortfolio'
)
);

describe('moveFunds procedure', () => {
let mockContext: Mocked<Context>;
let portfolioIdToMeshPortfolioIdStub: sinon.SinonStub<[PortfolioId, Context], MeshPortfolioId>;
let portfolioItemToMovePortfolioItemStub: sinon.SinonStub<
[PortfolioItem, Context],
MovePortfolioItem
>;

beforeAll(() => {
dsMockUtils.initMocks();
procedureMockUtils.initMocks();
entityMockUtils.initMocks();
portfolioIdToMeshPortfolioIdStub = sinon.stub(utilsModule, 'portfolioIdToMeshPortfolioId');
portfolioItemToMovePortfolioItemStub = sinon.stub(
utilsModule,
'portfolioItemToMovePortfolioItem'
);
});

beforeEach(() => {
mockContext = dsMockUtils.getContextInstance();
entityMockUtils.configureMocks({
numberedPortfolioOptions: {
isOwned: true,
},
});
});

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

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

test('should throw an error if both portfolios are the same', async () => {
const id = new BigNumber(1);
const did = 'someDid';
const samePortfolio = new NumberedPortfolio({ id, did }, mockContext);
const proc = procedureMockUtils.getInstance<Params, void>(mockContext);

return expect(
prepareMoveFunds.call(proc, {
from: samePortfolio,
to: samePortfolio,
items: [],
})
).rejects.toThrow('You cannot move tokens to the same portfolio of origin');
});

test('should throw an error if the Identity is not the owner of both Portfolios', async () => {
const fromId = new BigNumber(1);
const toId = new BigNumber(2);
const did = 'someDid';
const from = new NumberedPortfolio({ id: fromId, did }, mockContext);
const to = new NumberedPortfolio({ id: toId, did }, mockContext);

entityMockUtils.configureMocks({
numberedPortfolioOptions: {
isOwned: false,
},
});

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

return expect(
prepareMoveFunds.call(proc, {
from,
to,
items: [],
})
).rejects.toThrow('You are not the owner of these Portfolios');
});

test('should throw an error if some of the amount token to move exceed its balance', async () => {
const fromId = new BigNumber(1);
const toId = new BigNumber(2);
const fromDid = 'someDid';
const toDid = 'otherDid';
const from = new NumberedPortfolio({ id: fromId, did: fromDid }, mockContext);
const to = new NumberedPortfolio({ id: toId, did: toDid }, mockContext);
const securityToken01 = new SecurityToken({ ticker: 'TICKER001' }, mockContext);
const securityToken02 = new SecurityToken({ ticker: 'TICKER002' }, mockContext);
const items: PortfolioItem[] = [
{
token: securityToken01.ticker,
amount: new BigNumber(100),
},
{
token: securityToken02,
amount: new BigNumber(20),
},
];

entityMockUtils.configureMocks({
numberedPortfolioOptions: {
tokenBalances: [
{ token: securityToken01, total: new BigNumber(50) },
{ token: securityToken02, total: new BigNumber(10) },
] as PortfolioBalance[],
},
});

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

let error;

try {
await prepareMoveFunds.call(proc, {
from,
to,
items,
});
} catch (err) {
error = err;
}

expect(error.message).toBe('Some of the token amount exceed the actual balance');
expect(error.data.balanceExceeded).toMatchObject(items);
});

test('should add a move portfolio funds transaction to the queue', async () => {
const fromId = new BigNumber(1);
const toId = new BigNumber(2);
const did = 'someDid';
const from = new NumberedPortfolio({ id: fromId, did }, mockContext);
const to = new NumberedPortfolio({ id: toId, did }, mockContext);
const securityToken = new SecurityToken({ ticker: 'TICKER001' }, mockContext);
const items = [
{
token: securityToken.ticker,
amount: new BigNumber(100),
},
];

entityMockUtils.configureMocks({
numberedPortfolioOptions: {
did,
tokenBalances: [{ token: securityToken, total: new BigNumber(150) }] as PortfolioBalance[],
},
defaultPortfolioOptions: {
did,
tokenBalances: [{ token: securityToken, total: new BigNumber(150) }] as PortfolioBalance[],
},
});

let rawFromMeshPortfolioId = dsMockUtils.createMockPortfolioId({
did: dsMockUtils.createMockIdentityId(did),
kind: dsMockUtils.createMockPortfolioKind({
User: dsMockUtils.createMockU64(fromId.toNumber()),
}),
});
portfolioIdToMeshPortfolioIdStub
.withArgs({ did, number: fromId }, mockContext)
.returns(rawFromMeshPortfolioId);

let rawToMeshPortfolioId = dsMockUtils.createMockPortfolioId({
did: dsMockUtils.createMockIdentityId(did),
kind: dsMockUtils.createMockPortfolioKind({
User: dsMockUtils.createMockU64(toId.toNumber()),
}),
});
portfolioIdToMeshPortfolioIdStub
.withArgs({ did, number: toId }, mockContext)
.returns(rawToMeshPortfolioId);

const rawMovePortfolioItem = dsMockUtils.createMockMovePortfolioItem({
ticker: dsMockUtils.createMockTicker(items[0].token),
amount: dsMockUtils.createMockBalance(items[0].amount.toNumber()),
});
portfolioItemToMovePortfolioItemStub
.withArgs(items[0], mockContext)
.returns(rawMovePortfolioItem);

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

const transaction = dsMockUtils.createTxStub('portfolio', 'movePortfolioFunds');

await prepareMoveFunds.call(proc, {
from,
to,
items,
});

let addTransactionStub = procedureMockUtils.getAddTransactionStub();

sinon.assert.calledWith(
addTransactionStub,
transaction,
{},
rawFromMeshPortfolioId,
rawToMeshPortfolioId,
[rawMovePortfolioItem]
);

const defaultTo = new DefaultPortfolio({ did }, mockContext);

rawToMeshPortfolioId = dsMockUtils.createMockPortfolioId({
did: dsMockUtils.createMockIdentityId(did),
kind: dsMockUtils.createMockPortfolioKind('Default'),
});
portfolioIdToMeshPortfolioIdStub.withArgs({ did }, mockContext).returns(rawToMeshPortfolioId);

await prepareMoveFunds.call(proc, {
from,
to: defaultTo,
items,
});

addTransactionStub = procedureMockUtils.getAddTransactionStub();

sinon.assert.calledWith(
addTransactionStub,
transaction,
{},
rawFromMeshPortfolioId,
rawToMeshPortfolioId,
[rawMovePortfolioItem]
);

const defaultFrom = new DefaultPortfolio({ did }, mockContext);

rawFromMeshPortfolioId = dsMockUtils.createMockPortfolioId({
did: dsMockUtils.createMockIdentityId(did),
kind: dsMockUtils.createMockPortfolioKind('Default'),
});
portfolioIdToMeshPortfolioIdStub.withArgs({ did }, mockContext).returns(rawFromMeshPortfolioId);

rawToMeshPortfolioId = dsMockUtils.createMockPortfolioId({
did: dsMockUtils.createMockIdentityId(did),
kind: dsMockUtils.createMockPortfolioKind({
User: dsMockUtils.createMockU64(toId.toNumber()),
}),
});
portfolioIdToMeshPortfolioIdStub
.withArgs({ did, number: toId }, mockContext)
.returns(rawToMeshPortfolioId);

await prepareMoveFunds.call(proc, {
from: defaultFrom,
to,
items,
});

addTransactionStub = procedureMockUtils.getAddTransactionStub();

sinon.assert.calledWith(
addTransactionStub,
transaction,
{},
rawFromMeshPortfolioId,
rawToMeshPortfolioId,
[rawMovePortfolioItem]
);
});
});
1 change: 1 addition & 0 deletions src/api/procedures/index.ts
Expand Up @@ -41,3 +41,4 @@ export { transferTokenOwnership, TransferTokenOwnershipParams } from './transfer
export { removePrimaryIssuanceAgent } from './removePrimaryIssuanceAgent';
export { deletePortfolio } from './deletePortfolio';
export { renamePortfolio, RenamePortfolioParams } from './renamePortfolio';
export { moveFunds, MoveFundsParams } from './moveFunds';

0 comments on commit 4dd981c

Please sign in to comment.