From 8201d5cd690764e3f6d865281fbf75c685e5abdd Mon Sep 17 00:00:00 2001 From: Jeremias Diaz Date: Mon, 9 Nov 2020 23:55:28 -0300 Subject: [PATCH] feat(compliance): add support for `IsIdentity` condition --- .../SecurityToken/Compliance/Requirements.ts | 4 +- .../__tests__/setAssetRequirements.ts | 8 +- src/api/procedures/setAssetRequirements.ts | 2 +- src/testUtils/mocks/dataSources.ts | 10 ++ src/types/index.ts | 17 +- src/utils/__tests__/index.ts | 166 ++++++++++++++++-- src/utils/index.ts | 62 +++++-- 7 files changed, 239 insertions(+), 30 deletions(-) diff --git a/src/api/entities/SecurityToken/Compliance/Requirements.ts b/src/api/entities/SecurityToken/Compliance/Requirements.ts index fb947f12d4..3438ead5b1 100644 --- a/src/api/entities/SecurityToken/Compliance/Requirements.ts +++ b/src/api/entities/SecurityToken/Compliance/Requirements.ts @@ -73,7 +73,7 @@ export class Requirements extends Namespace { const defaultTrustedClaimIssuers = claimIssuers.map(identityIdToString); return assetCompliance.requirements.map(complianceRequirement => { - const requirement = complianceRequirementToRequirement(complianceRequirement); + const requirement = complianceRequirementToRequirement(complianceRequirement, context); requirement.conditions.forEach(condition => { if (!condition.trustedClaimIssuers || !condition.trustedClaimIssuers.length) { @@ -174,7 +174,7 @@ export class Requirements extends Namespace { primaryIssuanceAgent ? stringToIdentityId(primaryIssuanceAgent.did, context) : null ); - return assetComplianceResultToRequirementCompliance(res); + return assetComplianceResultToRequirementCompliance(res, context); } /** diff --git a/src/api/procedures/__tests__/setAssetRequirements.ts b/src/api/procedures/__tests__/setAssetRequirements.ts index 6b8619e854..87f853a034 100644 --- a/src/api/procedures/__tests__/setAssetRequirements.ts +++ b/src/api/procedures/__tests__/setAssetRequirements.ts @@ -22,7 +22,10 @@ describe('setAssetRequirements procedure', () => { [Requirement, Context], ComplianceRequirement >; - let complianceRequirementToRequirementStub: sinon.SinonStub<[ComplianceRequirement], Requirement>; + let complianceRequirementToRequirementStub: sinon.SinonStub< + [ComplianceRequirement, Context], + Requirement + >; let assetCompliancesStub: sinon.SinonStub; let ticker: string; let requirements: Condition[][]; @@ -124,7 +127,8 @@ describe('setAssetRequirements procedure', () => { sender_conditions: senderConditions[index], receiver_conditions: receiverConditions[index], /* eslint-enable @typescript-eslint/camelcase */ - }) + }), + mockContext ) .returns({ conditions: condition, id: 1 }); }); diff --git a/src/api/procedures/setAssetRequirements.ts b/src/api/procedures/setAssetRequirements.ts index ab79cd7deb..ec70137a6f 100644 --- a/src/api/procedures/setAssetRequirements.ts +++ b/src/api/procedures/setAssetRequirements.ts @@ -40,7 +40,7 @@ export async function prepareSetAssetRequirements( const rawCurrentAssetCompliance = await query.complianceManager.assetCompliances(rawTicker); const currentRequirements = rawCurrentAssetCompliance.requirements.map( - requirement => complianceRequirementToRequirement(requirement).conditions + requirement => complianceRequirementToRequirement(requirement, context).conditions ); const comparator = (a: Condition[], b: Condition[]): boolean => { diff --git a/src/testUtils/mocks/dataSources.ts b/src/testUtils/mocks/dataSources.ts index 09ea081c35..e1b153dbbc 100644 --- a/src/testUtils/mocks/dataSources.ts +++ b/src/testUtils/mocks/dataSources.ts @@ -74,6 +74,7 @@ import { SecurityToken, SettlementType, Signatory, + TargetIdentity, Ticker, TickerRegistration, TickerRegistrationConfig, @@ -1639,6 +1640,14 @@ export const createMockClaim = ( | 'NoData' ): Claim => createMockEnum(claim) as Claim; +/** + * @hidden + * NOTE: `isEmpty` will be set to true if no value is passed + */ +export const createMockTargetIdentity = ( + targetIdentity?: { Specific: IdentityId } | 'PrimaryIssuanceAgent' +): TargetIdentity => createMockEnum(targetIdentity) as TargetIdentity; + /** * @hidden * NOTE: `isEmpty` will be set to true if no value is passed @@ -1649,6 +1658,7 @@ export const createMockConditionType = ( | { IsAbsent: Claim } | { IsAnyOf: Claim[] } | { IsNoneOf: Claim[] } + | { IsIdentity: TargetIdentity } ): ConditionType => createMockEnum(conditionType) as ConditionType; /** diff --git a/src/types/index.ts b/src/types/index.ts index 99d50d2a75..54ea99b7a6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -290,6 +290,8 @@ export enum ConditionType { IsAbsent = 'IsAbsent', IsAnyOf = 'IsAnyOf', IsNoneOf = 'IsNoneOf', + IsPrimaryIssuanceAgent = 'IsPrimaryIssuanceAgent', + IsIdentity = 'IsIdentity', } export type ConditionBase = { target: ConditionTarget; trustedClaimIssuers?: string[] }; @@ -304,7 +306,20 @@ export type MultiClaimCondition = ConditionBase & { claims: Claim[]; }; -export type Condition = SingleClaimCondition | MultiClaimCondition; +export type IdentityCondition = ConditionBase & { + type: ConditionType.IsIdentity; + identity: Identity; +}; + +export type PrimaryIssuanceAgentCondition = ConditionBase & { + type: ConditionType.IsPrimaryIssuanceAgent; +}; + +export type Condition = + | SingleClaimCondition + | MultiClaimCondition + | IdentityCondition + | PrimaryIssuanceAgentCondition; /** * @hidden diff --git a/src/utils/__tests__/index.ts b/src/utils/__tests__/index.ts index 06e3ded070..a20ab84af0 100644 --- a/src/utils/__tests__/index.ts +++ b/src/utils/__tests__/index.ts @@ -105,6 +105,7 @@ import { extrinsicIdentifierToTxTag, findEventRecord, fundingRoundNameToString, + getDid, identityIdToString, isCusipValid, isIsinValid, @@ -116,6 +117,7 @@ import { meshPermissionToPermission, meshScopeToScope, meshVenueTypeToVenueType, + middlewareScopeToScope, // middlewareProposalToProposalDetails, moduleAddressToString, momentToDate, @@ -167,6 +169,7 @@ import { u8ToTransferStatus, u64ToBigNumber, unserialize, + unwrapValue, unwrapValues, venueDetailsToString, venueTypeToMeshVenueType, @@ -426,6 +429,46 @@ describe('signerToString', () => { }); }); +describe('getDid', () => { + let context: Context; + let did: string; + + beforeAll(() => { + dsMockUtils.initMocks(); + did = 'aDid'; + }); + + beforeEach(() => { + context = dsMockUtils.getContextInstance(); + }); + + afterEach(() => { + dsMockUtils.reset(); + }); + + afterAll(() => { + dsMockUtils.cleanup(); + }); + + test('getDid should extract the DID from an Identity', async () => { + const result = await getDid(new Identity({ did }, context), context); + + expect(result).toBe(did); + }); + + test('getDid should return the passed DID', async () => { + const result = await getDid(did, context); + + expect(result).toBe(did); + }); + + test('getDid should return the current Identity DID if nothing is passed', async () => { + const result = await getDid(undefined, context); + + expect(result).toBe((await context.getCurrentIdentity()).did); + }); +}); + describe('numberToBalance and balanceToBigNumber', () => { beforeAll(() => { dsMockUtils.initMocks(); @@ -1430,6 +1473,21 @@ describe('cddStatusToBoolean', () => { }); }); +describe('unwrapValue', () => { + test('should unwrap a Post Transactin Value', async () => { + const wrapped = new PostTransactionValue(async () => 1); + await wrapped.run({} as ISubmittableResult); + + const unwrapped = unwrapValue(wrapped); + + expect(unwrapped).toEqual(1); + }); + + test('should return a non Post Transaction Value as is', () => { + expect(unwrapValue(1)).toBe(1); + }); +}); + describe('unwrapValues', () => { test('should unwrap all Post Transaction Values in the array', async () => { const values = [1, 2, 3, 4, 5]; @@ -2104,6 +2162,8 @@ describe('requirementToComplianceRequirement and complianceRequirementToRequirem }); test('requirementToComplianceRequirement should convert a Requirement to a polkadot ComplianceRequirement object', () => { + const did = 'someDid'; + const context = dsMockUtils.getContextInstance(); const conditions: Condition[] = [ { type: ConditionType.IsPresent, @@ -2137,33 +2197,51 @@ describe('requirementToComplianceRequirement and complianceRequirementToRequirem code: CountryCode.Cl, }, }, + { + type: ConditionType.IsIdentity, + target: ConditionTarget.Sender, + identity: new Identity({ did }, context), + }, + { + type: ConditionType.IsPrimaryIssuanceAgent, + target: ConditionTarget.Receiver, + }, ]; const value = { conditions, id: 1, }; const fakeResult = ('convertedComplianceRequirement' as unknown) as ComplianceRequirement; - const context = dsMockUtils.getContextInstance(); const createTypeStub = dsMockUtils.getCreateTypeStub(); - conditions.forEach(({ type }, index) => { + conditions.forEach(({ type }) => { + const meshType = + type === ConditionType.IsPrimaryIssuanceAgent ? ConditionType.IsIdentity : type; createTypeStub .withArgs( 'Condition', sinon.match({ // eslint-disable-next-line @typescript-eslint/camelcase - condition_type: sinon.match.has(type), + condition_type: sinon.match.has(meshType), }) ) - .returns(`meshCondition${index}${type}`); + .returns(`meshCondition${meshType}`); }); createTypeStub .withArgs('ComplianceRequirement', { /* eslint-disable @typescript-eslint/camelcase */ - sender_conditions: ['meshCondition0IsPresent', 'meshCondition1IsNoneOf'], - receiver_conditions: ['meshCondition0IsPresent', 'meshCondition2IsAbsent'], + sender_conditions: [ + 'meshConditionIsPresent', + 'meshConditionIsNoneOf', + 'meshConditionIsIdentity', + ], + receiver_conditions: [ + 'meshConditionIsPresent', + 'meshConditionIsAbsent', + 'meshConditionIsIdentity', + ], id: numberToU32(value.id, context), /* eslint-enable @typescript-eslint/camelcase */ }) @@ -2179,6 +2257,8 @@ describe('requirementToComplianceRequirement and complianceRequirementToRequirem const tokenDid = 'someTokenDid'; const cddId = 'someCddId'; const issuerDids = ['someDid', 'otherDid']; + const targetIdentityDid = 'someDid'; + const context = dsMockUtils.getContextInstance(); const conditions: Condition[] = [ { type: ConditionType.IsPresent, @@ -2228,6 +2308,17 @@ describe('requirementToComplianceRequirement and complianceRequirementToRequirem ], trustedClaimIssuers: issuerDids, }, + { + type: ConditionType.IsIdentity, + target: ConditionTarget.Sender, + identity: new Identity({ did: targetIdentityDid }, context), + trustedClaimIssuers: issuerDids, + }, + { + type: ConditionType.IsPrimaryIssuanceAgent, + target: ConditionTarget.Receiver, + trustedClaimIssuers: issuerDids, + }, ]; const fakeResult = { id, @@ -2272,15 +2363,44 @@ describe('requirementToComplianceRequirement and complianceRequirementToRequirem }), issuers, }), + dsMockUtils.createMockCondition({ + condition_type: dsMockUtils.createMockConditionType({ + IsIdentity: dsMockUtils.createMockTargetIdentity({ + Specific: dsMockUtils.createMockIdentityId(targetIdentityDid), + }), + }), + issuers, + }), + dsMockUtils.createMockCondition({ + condition_type: dsMockUtils.createMockConditionType({ + IsIdentity: dsMockUtils.createMockTargetIdentity('PrimaryIssuanceAgent'), + }), + issuers, + }), ]; const complianceRequirement = dsMockUtils.createMockComplianceRequirement({ - sender_conditions: [rawConditions[0], rawConditions[2], rawConditions[2], rawConditions[3]], - receiver_conditions: [rawConditions[0], rawConditions[1], rawConditions[1], rawConditions[3]], + sender_conditions: [ + rawConditions[0], + rawConditions[2], + rawConditions[2], + rawConditions[3], + rawConditions[4], + ], + receiver_conditions: [ + rawConditions[0], + rawConditions[1], + rawConditions[1], + rawConditions[3], + rawConditions[5], + ], id: dsMockUtils.createMockU32(1), }); /* eslint-enable @typescript-eslint/camelcase */ - const result = complianceRequirementToRequirement(complianceRequirement); + const result = complianceRequirementToRequirement( + complianceRequirement, + dsMockUtils.getContextInstance() + ); expect(result.conditions).toEqual(expect.arrayContaining(fakeResult.conditions)); }); }); @@ -2303,6 +2423,7 @@ describe('assetComplianceResultToRequirementCompliance', () => { const tokenDid = 'someTokenDid'; const cddId = 'someCddId'; const issuerDids = ['someDid', 'otherDid']; + const context = dsMockUtils.getContextInstance(); const conditions: Condition[] = [ { type: ConditionType.IsPresent, @@ -2412,7 +2533,7 @@ describe('assetComplianceResultToRequirementCompliance', () => { result: dsMockUtils.createMockBool(true), }); - let result = assetComplianceResultToRequirementCompliance(assetComplianceResult); + let result = assetComplianceResultToRequirementCompliance(assetComplianceResult, context); expect(result.requirements[0].conditions).toEqual( expect.arrayContaining(fakeResult.conditions) ); @@ -2424,7 +2545,7 @@ describe('assetComplianceResultToRequirementCompliance', () => { result: dsMockUtils.createMockBool(true), }); - result = assetComplianceResultToRequirementCompliance(assetComplianceResult); + result = assetComplianceResultToRequirementCompliance(assetComplianceResult, context); expect(result.complies).toBeTruthy(); }); }); @@ -2891,7 +3012,7 @@ describe('batchArguments', () => { expect(batches[0]).toEqual(range(0, 2 * expectedBatchLength, 2)); expect(batches[1]).toEqual(range(1, 2 * expectedBatchLength, 2)); - batches = batchArguments(elements, tag, element => `${element % 3}`); // separate in 3 groups + batches = batchArguments(elements, tag, element => `${element % 5}`); // separate in 5 groups expect(batches.length).toBe(3); expect(batches[0].length).toBeLessThan(expectedBatchLength); @@ -3436,8 +3557,25 @@ describe('endConditionToSettlementType', () => { }); }); -describe('scopeToMiddlewareScope', () => { - test('scopeToMiddlewareScope should convert a different Scopes to a middlware Scops', () => { +describe('middlewareScopeToScope and scopeToMiddlewareScope', () => { + test('should convert a MiddlewareScope object to a Scope', () => { + let result = middlewareScopeToScope({ + type: ClaimScopeTypeEnum.Ticker, + value: 'SOMETHING\u0000\u0000\u0000', + }); + + expect(result).toEqual({ type: ScopeType.Ticker, value: 'SOMETHING' }); + + result = middlewareScopeToScope({ type: ClaimScopeTypeEnum.Identity, value: 'someDid' }); + + expect(result).toEqual({ type: ScopeType.Identity, value: 'someDid' }); + + result = middlewareScopeToScope({ type: ClaimScopeTypeEnum.Custom, value: 'SOMETHINGELSE' }); + + expect(result).toEqual({ type: ScopeType.Custom, value: 'SOMETHINGELSE' }); + }); + + test('scopeToMiddlewareScope should convert a Scope to a MiddlewareScope object', () => { let scope: Scope = { type: ScopeType.Identity, value: 'someDid' }; let result = scopeToMiddlewareScope(scope); expect(result).toEqual({ type: ClaimScopeTypeEnum.Identity, value: scope.value }); diff --git a/src/utils/index.ts b/src/utils/index.ts index 06d418336f..46adaa3c8a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -59,6 +59,7 @@ import { SecondaryKey as MeshSecondaryKey, SettlementType, Signatory, + TargetIdentity, Ticker, TxTag, TxTags, @@ -89,6 +90,7 @@ import { ConditionType, CountryCode, ErrorCode, + IdentityCondition, IdentityWithClaims, InstructionStatus, InstructionType, @@ -100,6 +102,7 @@ import { PaginationOptions, Permission, PortfolioLike, + PrimaryIssuanceAgentCondition, Requirement, RequirementCompliance, Scope, @@ -1247,6 +1250,16 @@ export function meshClaimToClaim(claim: MeshClaim): Claim { }; } +/** + * @hidden + */ +export function stringToTargetIdentity(did: string | null, context: Context): TargetIdentity { + return context.polymeshApi.createType( + 'TargetIdentity', + did ? { Specific: stringToIdentityId(did, context) } : 'PrimaryIssuanceAgent' + ); +} + /** * @hidden */ @@ -1259,21 +1272,31 @@ export function requirementToComplianceRequirement( const receiverConditions: MeshCondition[] = []; requirement.conditions.forEach(condition => { - let claimContent: MeshClaim | MeshClaim[]; + let conditionContent: MeshClaim | MeshClaim[] | TargetIdentity; + let { type } = condition; if (isSingleClaimCondition(condition)) { const { claim } = condition; - claimContent = claimToMeshClaim(claim, context); - } else { + conditionContent = claimToMeshClaim(claim, context); + } else if (isMultiClaimCondition(condition)) { const { claims } = condition; - claimContent = claims.map(claim => claimToMeshClaim(claim, context)); + conditionContent = claims.map(claim => claimToMeshClaim(claim, context)); + } else if (condition.type === ConditionType.IsIdentity) { + const { + identity: { did }, + } = condition; + conditionContent = stringToTargetIdentity(did, context); + } else { + // IsPrimaryIssuanceAgent does not exist as a condition type in Polymesh, it's SDK sugar + type = ConditionType.IsIdentity; + conditionContent = stringToTargetIdentity(null, context); } - const { target, type, trustedClaimIssuers = [] } = condition; + const { target, trustedClaimIssuers = [] } = condition; const meshCondition = polymeshApi.createType('Condition', { // eslint-disable-next-line @typescript-eslint/camelcase condition_type: { - [type]: claimContent, + [type]: conditionContent, }, issuers: trustedClaimIssuers.map(issuer => stringToIdentityId(issuer, context)), }); @@ -1300,13 +1323,16 @@ export function requirementToComplianceRequirement( * @hidden */ export function complianceRequirementToRequirement( - complianceRequirement: ComplianceRequirement + complianceRequirement: ComplianceRequirement, + context: Context ): Requirement { const meshConditionTypeToCondition = ( meshConditionType: MeshConditionType ): | Pick - | Pick => { + | Pick + | Pick + | Pick => { if (meshConditionType.isIsPresent) { return { type: ConditionType.IsPresent, @@ -1328,6 +1354,21 @@ export function complianceRequirementToRequirement( }; } + if (meshConditionType.isIsIdentity) { + const target = meshConditionType.asIsIdentity; + + if (target.isPrimaryIssuanceAgent) { + return { + type: ConditionType.IsPrimaryIssuanceAgent, + }; + } + + return { + type: ConditionType.IsIdentity, + identity: new Identity({ did: identityIdToString(target.asSpecific) }, context), + }; + } + return { type: ConditionType.IsNoneOf, claims: meshConditionType.asIsNoneOf.map(claim => meshClaimToClaim(claim)), @@ -1468,11 +1509,12 @@ export function portfolioIdToMeshPortfolioId( * @hidden */ export function assetComplianceResultToRequirementCompliance( - assetComplianceResult: AssetComplianceResult + assetComplianceResult: AssetComplianceResult, + context: Context ): RequirementCompliance { const { requirements: rawRequirements, result, paused } = assetComplianceResult; const requirements = rawRequirements.map(requirement => ({ - ...complianceRequirementToRequirement(requirement), + ...complianceRequirementToRequirement(requirement, context), complies: boolToBoolean(requirement.result), }));