Skip to content

Commit

Permalink
feat: security token redeem
Browse files Browse the repository at this point in the history
  • Loading branch information
shuffledex committed Dec 21, 2020
1 parent 3fbb80e commit bc207ac
Show file tree
Hide file tree
Showing 9 changed files with 394 additions and 3 deletions.
41 changes: 41 additions & 0 deletions src/api/entities/Identity/__tests__/index.ts
Expand Up @@ -13,6 +13,7 @@ import {
Role,
RoleType,
TickerOwnerRole,
TokenOwnerOrPiaRole,
TokenOwnerRole,
VenueOwnerRole,
} from '~/types';
Expand Down Expand Up @@ -132,6 +133,46 @@ describe('Identity class', () => {
expect(hasRole).toBe(false);
});

test('hasRole should check whether the Identity has the Token Owner role or the PIA role', async () => {
const identity = new Identity({ did: 'someDid' }, context);
const role: TokenOwnerOrPiaRole = { type: RoleType.TokenOwnerOrPia, ticker: 'someTicker' };

let hasRole = await identity.hasRole(role);

expect(hasRole).toBe(true);

identity.did = 'otherDid';

hasRole = await identity.hasRole(role);

expect(hasRole).toBe(false);

entityMockUtils.configureMocks({
securityTokenOptions: {
details: {
primaryIssuanceAgent: identity,
},
},
});

hasRole = await identity.hasRole(role);

expect(hasRole).toBe(true);

entityMockUtils.reset();
entityMockUtils.configureMocks({
securityTokenOptions: {
details: {
primaryIssuanceAgent: new Identity({ did: 'anotherDid' }, context),
},
},
});

hasRole = await identity.hasRole(role);

expect(hasRole).toBe(false);
});

test('hasRole should check whether the Identity has the CDD Provider role', async () => {
const did = 'someDid';
const identity = new Identity({ did }, context);
Expand Down
14 changes: 14 additions & 0 deletions src/api/entities/Identity/index.ts
Expand Up @@ -20,6 +20,7 @@ import {
isCddProviderRole,
isPortfolioCustodianRole,
isTickerOwnerRole,
isTokenOwnerOrPiaRole,
isTokenOwnerRole,
isVenueOwnerRole,
Order,
Expand Down Expand Up @@ -104,6 +105,19 @@ export class Identity extends Entity<UniqueIdentifiers> {
const token = new SecurityToken({ ticker }, context);
const { owner } = await token.details();

return owner.did === did;
} else if (isTokenOwnerOrPiaRole(role)) {
const { ticker } = role;

const token = new SecurityToken({ ticker }, context);
const { owner, primaryIssuanceAgent } = await token.details();

if (primaryIssuanceAgent) {
if (primaryIssuanceAgent.did === did) {
return true;
}
}

return owner.did === did;
} else if (isCddProviderRole(role)) {
const {
Expand Down
21 changes: 21 additions & 0 deletions src/api/entities/SecurityToken/__tests__/index.ts
Expand Up @@ -15,6 +15,7 @@ import {
Identity,
modifyPrimaryIssuanceAgent,
modifyToken,
redeemToken,
removePrimaryIssuanceAgent,
SecurityToken,
toggleFreezeTransfers,
Expand Down Expand Up @@ -501,4 +502,24 @@ describe('SecurityToken class', () => {
expect(queue).toBe(expectedQueue);
});
});

describe('method: redeem', () => {
test('should prepare the procedure and return the resulting transaction queue', async () => {
const ticker = 'TICKER';
const balance = new BigNumber(100);
const context = dsMockUtils.getContextInstance();
const securityToken = new SecurityToken({ ticker }, context);

const expectedQueue = ('someQueue' as unknown) as TransactionQueue<void>;

sinon
.stub(redeemToken, 'prepare')
.withArgs({ balance, ticker }, context)
.resolves(expectedQueue);

const queue = await securityToken.redeem({ balance });

expect(queue).toBe(expectedQueue);
});
});
});
10 changes: 10 additions & 0 deletions src/api/entities/SecurityToken/index.ts
@@ -1,6 +1,7 @@
import BigNumber from 'bignumber.js';
import { SecurityToken as MeshSecurityToken } from 'polymesh-types/types';

import { redeemToken, RedeemTokenParams } from '~/api/procedures/redeemToken';
import {
Context,
Entity,
Expand Down Expand Up @@ -355,4 +356,13 @@ export class SecurityToken extends Entity<UniqueIdentifiers> {
const { ticker, context } = this;
return removePrimaryIssuanceAgent.prepare({ ticker }, context);
}

/**
* Redeem the Security Tokens
*/
public redeem(args: RedeemTokenParams): Promise<TransactionQueue<void>> {
const { ticker, context } = this;
const { balance } = args;
return redeemToken.prepare({ ticker, balance }, context);
}
}
202 changes: 202 additions & 0 deletions src/api/procedures/__tests__/redeemToken.ts
@@ -0,0 +1,202 @@
import { Balance } from '@polkadot/types/interfaces';
import BigNumber from 'bignumber.js';
import { Ticker } from 'polymesh-types/types';
import sinon from 'sinon';

import { DefaultPortfolio } from '~/api/entities/DefaultPortfolio';
import { getAuthorization, Params, prepareRedeemToken } from '~/api/procedures/redeemToken';
import { Context, Identity, SecurityToken } from '~/internal';
import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mocks';
import { Mocked } from '~/testUtils/types';
import { RoleType, TxTags } from '~/types';
import * as utilsConversionModule from '~/utils/conversion';

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

describe('redeemToken procedure', () => {
let mockContext: Mocked<Context>;
let addTransactionStub: sinon.SinonStub;
let ticker: string;
let rawTicker: Ticker;
let balance: BigNumber;
let rawBalance: Balance;
let stringToTickerStub: sinon.SinonStub<[string, Context], Ticker>;
let numberToBalanceStub: sinon.SinonStub<
[number | BigNumber, Context, (boolean | undefined)?],
Balance
>;

beforeAll(() => {
dsMockUtils.initMocks();
procedureMockUtils.initMocks();
entityMockUtils.initMocks();
ticker = 'SOMETICKER';
rawTicker = dsMockUtils.createMockTicker(ticker);
balance = new BigNumber(100);
rawBalance = dsMockUtils.createMockBalance(balance.toNumber());
stringToTickerStub = sinon.stub(utilsConversionModule, 'stringToTicker');
numberToBalanceStub = sinon.stub(utilsConversionModule, 'numberToBalance');
});

beforeEach(() => {
mockContext = dsMockUtils.getContextInstance();
addTransactionStub = procedureMockUtils.getAddTransactionStub();
stringToTickerStub.withArgs(ticker, mockContext).returns(rawTicker);
numberToBalanceStub.withArgs(balance, mockContext).returns(rawBalance);
});

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

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

test('should add a redeem transaction to the queue', async () => {
const proc = procedureMockUtils.getInstance<Params, void>(mockContext);

const transaction = dsMockUtils.createTxStub('asset', 'redeem');

entityMockUtils.configureMocks({
securityTokenOptions: {
details: {
isDivisible: true,
},
},
defaultPortfolioOptions: {
tokenBalances: [
{
token: new SecurityToken({ ticker }, mockContext),
total: new BigNumber(1000),
locked: new BigNumber(500),
},
],
},
});

await prepareRedeemToken.call(proc, {
ticker,
balance,
});

sinon.assert.calledWith(addTransactionStub, transaction, {}, rawTicker, rawBalance);
});

test('should throw an error if the portfolio has not sufficient balance to redeem', () => {
entityMockUtils.configureMocks({
securityTokenOptions: {
details: {
isDivisible: true,
primaryIssuanceAgent: new Identity({ did: 'primaryDid' }, mockContext),
},
},
defaultPortfolioOptions: {
tokenBalances: [
{
token: new SecurityToken({ ticker }, mockContext),
total: new BigNumber(20),
locked: new BigNumber(20),
},
],
},
});

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

return expect(
prepareRedeemToken.call(proc, {
ticker,
balance,
})
).rejects.toThrow('Insufficient balance');
});

test('should throw an error if the security token is not divisible', () => {
entityMockUtils.configureMocks({
defaultPortfolioOptions: {
tokenBalances: [
{
token: new SecurityToken({ ticker }, mockContext),
total: new BigNumber(1000),
locked: new BigNumber(500),
},
],
},
});

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

return expect(
prepareRedeemToken.call(proc, {
ticker,
balance,
})
).rejects.toThrow('The Security Token must be divisible');
});

describe('getAuthorization', () => {
test('should return the appropriate roles and permissions', async () => {
const params = {
ticker,
balance,
};
const ownerDid = 'ownerDid';
const someDid = 'someDid';

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

entityMockUtils.configureMocks({
securityTokenOptions: {
details: {
owner: new Identity({ did: ownerDid }, mockContext),
},
},
});

let result = await boundFunc(params);

expect(result).toEqual({
identityRoles: [{ type: RoleType.TokenOwnerOrPia, ticker }],
signerPermissions: {
transactions: [TxTags.asset.Redeem],
tokens: [new SecurityToken({ ticker }, mockContext)],
portfolios: [new DefaultPortfolio({ did: ownerDid }, mockContext)],
},
});

entityMockUtils.configureMocks({
securityTokenOptions: {
details: {
primaryIssuanceAgent: new Identity({ did: someDid }, mockContext),
},
},
});

result = await boundFunc(params);

expect(result).toEqual({
identityRoles: [{ type: RoleType.TokenOwnerOrPia, ticker }],
signerPermissions: {
transactions: [TxTags.asset.Redeem],
tokens: [new SecurityToken({ ticker }, mockContext)],
portfolios: [new DefaultPortfolio({ did: someDid }, mockContext)],
},
});
});
});
});
3 changes: 0 additions & 3 deletions src/api/procedures/modifyToken.ts
Expand Up @@ -91,9 +91,6 @@ export async function prepareModifyToken(
return securityToken;
}

/**
* @hidden
*/
/**
* @hidden
*/
Expand Down

0 comments on commit bc207ac

Please sign in to comment.