Skip to content

Commit

Permalink
feat(restrictions): allow identities as exempted
Browse files Browse the repository at this point in the history
Also added an `Identity.getScopeId` getter

BREAKING CHANGE:
 - rename `exempted` to `exemptedScopeIds` in `CountTransferRestriction` and `PercentageTransferRestriction`
 - rename `exempted` to `exemptedScopeIds` and add `exemptedIdentities` to the parameters of `addRestriction` and `setRestrictions` in both restriction namespaces
  • Loading branch information
monitz87 committed Feb 8, 2021
1 parent 72b8af5 commit 6e84dff
Show file tree
Hide file tree
Showing 12 changed files with 424 additions and 70 deletions.
51 changes: 50 additions & 1 deletion src/api/entities/Identity/__tests__/index.ts
@@ -1,7 +1,7 @@
import { u64 } from '@polkadot/types';
import { AccountId, Balance } from '@polkadot/types/interfaces';
import BigNumber from 'bignumber.js';
import { DidRecord, IdentityId, Ticker } from 'polymesh-types/types';
import { DidRecord, IdentityId, ScopeId, Ticker } from 'polymesh-types/types';
import sinon from 'sinon';

import { Context, Entity, Identity } from '~/internal';
Expand Down Expand Up @@ -583,4 +583,53 @@ describe('Identity class', () => {
sinon.assert.calledWithExactly(callback, fakeResult);
});
});

describe('method: getScopeId', () => {
let did: string;
let ticker: string;
let scopeId: string;

let rawDid: IdentityId;
let rawTicker: Ticker;
let rawScopeId: ScopeId;

let stringToTickerStub: sinon.SinonStub<[string, Context], Ticker>;

beforeAll(() => {
did = 'someDid';
ticker = 'SOME_TICKER';
scopeId = 'someScopeId';

rawDid = dsMockUtils.createMockIdentityId(did);
rawTicker = dsMockUtils.createMockTicker(ticker);
rawScopeId = dsMockUtils.createMockScopeId(scopeId);

stringToTickerStub = sinon.stub(utilsConversionModule, 'stringToTicker');
});

beforeEach(() => {
stringToIdentityIdStub.withArgs(did, context).returns(rawDid);
stringToTickerStub.withArgs(ticker, context).returns(rawTicker);

dsMockUtils.createQueryStub('asset', 'scopeIdOf', {
returnValue: rawScopeId,
});
});

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

test("should return the Identity's scopeId associated to the token", async () => {
const identity = new Identity({ did }, context);

let result = await identity.getScopeId({ token: ticker });
expect(result).toEqual(scopeId);

result = await identity.getScopeId({
token: entityMockUtils.getSecurityTokenInstance({ ticker }),
});
expect(result).toEqual(scopeId);
});
});
});
20 changes: 20 additions & 0 deletions src/api/entities/Identity/index.ts
Expand Up @@ -33,6 +33,7 @@ import {
cddStatusToBoolean,
identityIdToString,
portfolioIdToPortfolio,
scopeIdToString,
stringToIdentityId,
stringToTicker,
u64ToBigNumber,
Expand Down Expand Up @@ -386,4 +387,23 @@ export class Identity extends Entity<UniqueIdentifiers> {

return assembleResult(venueIds);
}

/**
* Retrieve the Scope ID associated to this Identity's Investor Uniqueness Claim for a specific Security Token
*
* @note more on Investor Uniqueness: https://developers.polymesh.live/confidential_identity
*/
public async getScopeId(args: { token: SecurityToken | string }): Promise<string> {
const { context, did } = this;
const { token } = args;

const ticker = typeof token === 'string' ? token : token.ticker;

const scopeId = await context.polymeshApi.query.asset.scopeIdOf(
stringToTicker(ticker, context),
stringToIdentityId(did, context)
);

return scopeIdToString(scopeId);
}
}
5 changes: 3 additions & 2 deletions src/api/entities/SecurityToken/TransferRestrictions/Count.ts
Expand Up @@ -13,14 +13,15 @@ export class Count extends TransferRestrictionBase<TransferRestrictionType.Count
* Add a Count Transfer Restriction to this Security Token
*
* @param args.count - limit on the amount of different (unique) investors that can hold this Security Token at once
* @param args.exempted - array of Scope IDs that are exempted from the Restriction
* @param args.exemptedScopeIds - array of Scope IDs that are exempted from the Restriction
* @param args.exemptedIdentities - array of Identities (or DIDs) that are exempted from the Restriction
*
* @note the result is the total amount of restrictions after the procedure has run
*/
public addRestriction!: ProcedureMethod<Omit<AddCountTransferRestrictionParams, 'type'>, number>;

/**
* Sets all Count Transfer Restrictions type on this Security Token
* Sets all Count Transfer Restrictions on this Security Token
*
* @param args.restrictions - array of Count Transfer Restrictions with their corresponding exemptions (if applicable)
*
Expand Down
Expand Up @@ -16,7 +16,8 @@ export class Percentage extends TransferRestrictionBase<TransferRestrictionType.
* Add a Percentage Transfer Restriction to this Security Token
*
* @param args.percentage - limit on the proportion of the total supply of this Security Token that can be held by a single investor at once
* @param args.exempted - array of Scope IDs that are exempted from the Restriction
* @param args.exemptedScopeIds - array of Scope IDs that are exempted from the Restriction
* @param args.exemptedIdentities - array of Identities (or DIDs) that are exempted from the Restriction
*
* @note the result is the total amount of restrictions after the procedure has run
*/
Expand All @@ -26,7 +27,7 @@ export class Percentage extends TransferRestrictionBase<TransferRestrictionType.
>;

/**
* Sets all Percentage Transfer Restrictions type on this Security Token
* Sets all Percentage Transfer Restrictions on this Security Token
*
* @param args.restrictions - array of Percentage Transfer Restrictions with their corresponding exemptions (if applicable)
*
Expand Down
Expand Up @@ -157,7 +157,7 @@ export abstract class TransferRestrictionBase<
);

const restrictions = rawExemptedLists.map((list, index) => {
const exempted = list.map(([{ args: [, scopeId] }]) => scopeIdToString(scopeId));
const exemptedScopeIds = list.map(([{ args: [, scopeId] }]) => scopeIdToString(scopeId));
const { value } = transferManagerToTransferRestriction(filteredTms[index]);
let restriction;

Expand All @@ -171,10 +171,10 @@ export abstract class TransferRestrictionBase<
};
}

if (exempted.length) {
if (exemptedScopeIds.length) {
return {
...restriction,
exempted,
exemptedScopeIds,
};
}
return restriction;
Expand Down
Expand Up @@ -63,7 +63,7 @@ describe('TransferRestrictionBase class', () => {

const args: Omit<AddCountTransferRestrictionParams, 'type'> = {
count: new BigNumber(3),
exempted: ['someScopeId'],
exemptedScopeIds: ['someScopeId'],
};

const expectedQueue = ('someQueue' as unknown) as TransactionQueue<number>;
Expand All @@ -85,7 +85,7 @@ describe('TransferRestrictionBase class', () => {

const args: Omit<AddPercentageTransferRestrictionParams, 'type'> = {
percentage: new BigNumber(3),
exempted: ['someScopeId'],
exemptedScopeIds: ['someScopeId'],
};

const expectedQueue = ('someQueue' as unknown) as TransactionQueue<number>;
Expand Down Expand Up @@ -123,7 +123,7 @@ describe('TransferRestrictionBase class', () => {
const count = new Count(token, context);

const args: Omit<SetCountTransferRestrictionsParams, 'type'> = {
restrictions: [{ count: new BigNumber(3), exempted: ['someScopeId'] }],
restrictions: [{ count: new BigNumber(3), exemptedScopeIds: ['someScopeId'] }],
};

const expectedQueue = ('someQueue' as unknown) as TransactionQueue<number>;
Expand All @@ -144,7 +144,7 @@ describe('TransferRestrictionBase class', () => {
const percentage = new Percentage(token, context);

const args: Omit<SetPercentageTransferRestrictionsParams, 'type'> = {
restrictions: [{ percentage: new BigNumber(49), exempted: ['someScopeId'] }],
restrictions: [{ percentage: new BigNumber(49), exemptedScopeIds: ['someScopeId'] }],
};

const expectedQueue = ('someQueue' as unknown) as TransactionQueue<number>;
Expand Down Expand Up @@ -227,11 +227,11 @@ describe('TransferRestrictionBase class', () => {
beforeAll(() => {
scopeId = 'someScopeId';
countRestriction = {
exempted: [scopeId],
exemptedScopeIds: [scopeId],
count: new BigNumber(10),
};
percentageRestriction = {
exempted: [scopeId],
exemptedScopeIds: [scopeId],
percentage: new BigNumber(49),
};
rawCountRestriction = dsMockUtils.createMockTransferManager({
Expand Down
85 changes: 65 additions & 20 deletions src/api/procedures/__tests__/addTransferRestriction.ts
Expand Up @@ -12,14 +12,18 @@ import {
import { Context } from '~/internal';
import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mocks';
import { Mocked } from '~/testUtils/types';
import { RoleType, TickerReservationStatus } from '~/types';
import { RoleType } from '~/types';
import { PolymeshTx, TransferRestriction, TransferRestrictionType } from '~/types/internal';
import * as utilsConversionModule from '~/utils/conversion';

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

describe('addTransferRestriction procedure', () => {
let mockContext: Mocked<Context>;
Expand Down Expand Up @@ -64,12 +68,6 @@ describe('addTransferRestriction procedure', () => {
beforeEach(() => {
addTransactionStub = procedureMockUtils.getAddTransactionStub();

entityMockUtils.getTickerReservationDetailsStub().resolves({
owner: entityMockUtils.getIdentityInstance(),
expiryDate: null,
status: TickerReservationStatus.Free,
});

addTransferManagerTransaction = dsMockUtils.createTxStub('statistics', 'addTransferManager');
addExemptedEntitiesTransaction = dsMockUtils.createTxStub('statistics', 'addExemptedEntities');

Expand Down Expand Up @@ -105,7 +103,7 @@ describe('addTransferRestriction procedure', () => {
test('should add an add transfer manager transaction to the queue', async () => {
args = {
type: TransferRestrictionType.Count,
exempted: [],
exemptedScopeIds: [],
count,
ticker,
};
Expand All @@ -129,7 +127,7 @@ describe('addTransferRestriction procedure', () => {

args = {
type: TransferRestrictionType.Percentage,
exempted: [],
exemptedScopeIds: [],
percentage,
ticker,
};
Expand All @@ -148,11 +146,16 @@ describe('addTransferRestriction procedure', () => {
});

test('should add an add exempted entities transaction to the queue', async () => {
const did = 'someDid';
const scopeId = 'someScopeId';
const rawScopeId = dsMockUtils.createMockScopeId(scopeId);
const identityScopeId = 'anotherScopeId';
const rawIdentityScopeId = dsMockUtils.createMockScopeId(identityScopeId);
entityMockUtils.configureMocks({ identityOptions: { getScopeId: identityScopeId } });
args = {
type: TransferRestrictionType.Count,
exempted: [scopeId],
exemptedScopeIds: [scopeId],
exemptedIdentities: [did],
count,
ticker,
};
Expand All @@ -162,20 +165,36 @@ describe('addTransferRestriction procedure', () => {
returnValue: [],
});

sinon
.stub(utilsConversionModule, 'stringToScopeId')
.withArgs(scopeId, mockContext)
.returns(rawScopeId);
const stringToScopeIdStub = sinon.stub(utilsConversionModule, 'stringToScopeId');

stringToScopeIdStub.withArgs(scopeId, mockContext).returns(rawScopeId);
stringToScopeIdStub.withArgs(identityScopeId, mockContext).returns(rawIdentityScopeId);

let result = await prepareAddTransferRestriction.call(proc, args);

sinon.assert.calledWith(
addTransactionStub,
addExemptedEntitiesTransaction,
{ batchSize: 2 },
rawTicker,
rawCountTm,
[rawScopeId, rawIdentityScopeId]
);

expect(result).toEqual(1);

const result = await prepareAddTransferRestriction.call(proc, args);
result = await prepareAddTransferRestriction.call(proc, {
...args,
exemptedIdentities: [entityMockUtils.getIdentityInstance()],
});

sinon.assert.calledWith(
addTransactionStub,
addExemptedEntitiesTransaction,
{ batchSize: 1 },
{ batchSize: 2 },
rawTicker,
rawCountTm,
[rawScopeId]
[rawScopeId, rawIdentityScopeId]
);

expect(result).toEqual(1);
Expand All @@ -184,7 +203,7 @@ describe('addTransferRestriction procedure', () => {
test('should throw an error if attempting to add a restriction that already exists', async () => {
args = {
type: TransferRestrictionType.Count,
exempted: [],
exemptedScopeIds: [],
count,
ticker,
};
Expand All @@ -205,7 +224,7 @@ describe('addTransferRestriction procedure', () => {
expect(err.message).toBe('Cannot add the same restriction more than once');
});

test('should throw an error if attempting to add a restriction that already exists', async () => {
test('should throw an error if attempting to add a restriction when the restriction limit has been reached', async () => {
args = {
type: TransferRestrictionType.Count,
count,
Expand All @@ -229,6 +248,32 @@ describe('addTransferRestriction procedure', () => {
expect(err.data).toEqual({ limit: 3 });
});

test('should throw an error if exempted scope IDs are repeated', async () => {
args = {
type: TransferRestrictionType.Count,
exemptedScopeIds: ['someScopeId', 'someScopeId'],
count,
ticker,
};
const proc = procedureMockUtils.getInstance<AddTransferRestrictionParams, number>(mockContext);

dsMockUtils.createQueryStub('statistics', 'activeTransferManagers', {
returnValue: [],
});

let err;

try {
await prepareAddTransferRestriction.call(proc, args);
} catch (error) {
err = error;
}

expect(err.message).toBe(
'One or more of the passed exempted Scope IDs/Identities are repeated'
);
});

describe('getAuthorization', () => {
test('should return the appropriate roles and permissions', () => {
args = {
Expand All @@ -250,7 +295,7 @@ describe('addTransferRestriction procedure', () => {
portfolios: [],
},
});
expect(boundFunc({ ...args, exempted: ['someScopeId'] })).toEqual({
expect(boundFunc({ ...args, exemptedScopeIds: ['someScopeId'] })).toEqual({
identityRoles: [{ type: RoleType.TokenOwner, ticker }],
signerPermissions: {
tokens: [entityMockUtils.getSecurityTokenInstance({ ticker })],
Expand Down

0 comments on commit 6e84dff

Please sign in to comment.