diff --git a/src/api/entities/Asset/Fungible/TransferRestrictions/TransferRestrictionBase.ts b/src/api/entities/Asset/Fungible/TransferRestrictions/TransferRestrictionBase.ts index 9c1346ec78..f24cadbd0c 100644 --- a/src/api/entities/Asset/Fungible/TransferRestrictions/TransferRestrictionBase.ts +++ b/src/api/entities/Asset/Fungible/TransferRestrictions/TransferRestrictionBase.ts @@ -1,3 +1,4 @@ +import { PolymeshPrimitivesStatisticsStatType } from '@polkadot/types/lookup'; import BigNumber from 'bignumber.js'; import { @@ -339,42 +340,29 @@ export abstract class TransferRestrictionBase< let isSet = false; const claims: ActiveStats['claims'] = []; + const pushClaims = (stat: PolymeshPrimitivesStatisticsStatType): void => { + const [rawClaimType, rawIssuer] = stat.claimIssuer.unwrap(); + const claimType = meshClaimTypeToClaimType(rawClaimType); + const issuer = new Identity({ did: identityIdToString(rawIssuer) }, context); + + claims.push({ claimType, issuer }); + }; + [...currentStats].forEach(stat => { const statType = meshStatToStatType(stat); - if ( - type === TransferRestrictionType.ClaimCount && - !stat.claimIssuer.isNone && - statType === StatType.ScopedCount - ) { + if (type === TransferRestrictionType.ClaimCount && statType === StatType.ScopedCount) { isSet = true; - const [rawClaimType, rawIssuer] = stat.claimIssuer.unwrap(); - const claimType = meshClaimTypeToClaimType(rawClaimType); - const issuer = new Identity({ did: identityIdToString(rawIssuer) }, context); - - claims.push({ claimType, issuer }); + pushClaims(stat); } else if ( type === TransferRestrictionType.ClaimPercentage && - !stat.claimIssuer.isNone && statType === StatType.ScopedBalance ) { isSet = true; - const [rawClaimType, rawIssuer] = stat.claimIssuer.unwrap(); - const claimType = meshClaimTypeToClaimType(rawClaimType); - const issuer = new Identity({ did: identityIdToString(rawIssuer) }, context); - - claims.push({ claimType, issuer }); - } else if ( - type === TransferRestrictionType.Percentage && - stat.claimIssuer.isNone && - statType === StatType.Balance - ) { + pushClaims(stat); + } else if (type === TransferRestrictionType.Percentage && statType === StatType.Balance) { isSet = true; - } else if ( - type === TransferRestrictionType.Count && - stat.claimIssuer.isNone && - statType === StatType.Count - ) { + } else if (type === TransferRestrictionType.Count && statType === StatType.Count) { isSet = true; } }); diff --git a/src/api/entities/Asset/Fungible/TransferRestrictions/__tests__/TransferRestrictionBase.ts b/src/api/entities/Asset/Fungible/TransferRestrictions/__tests__/TransferRestrictionBase.ts index 99aa34aeab..71e84d28b1 100644 --- a/src/api/entities/Asset/Fungible/TransferRestrictions/__tests__/TransferRestrictionBase.ts +++ b/src/api/entities/Asset/Fungible/TransferRestrictions/__tests__/TransferRestrictionBase.ts @@ -1,4 +1,8 @@ -import { PolymeshPrimitivesTransferComplianceTransferCondition } from '@polkadot/types/lookup'; +import { + PolymeshPrimitivesIdentityId, + PolymeshPrimitivesStatisticsStatType, + PolymeshPrimitivesTransferComplianceTransferCondition, +} from '@polkadot/types/lookup'; import BigNumber from 'bignumber.js'; import { when } from 'jest-when'; @@ -683,4 +687,143 @@ describe('TransferRestrictionBase class', () => { expect(transaction).toBe(expectedTransaction); }); }); + + describe('method: getStat', () => { + let context: Context; + let asset: FungibleAsset; + + let rawCountStatType: PolymeshPrimitivesStatisticsStatType; + let rawPercentageStatType: PolymeshPrimitivesStatisticsStatType; + let rawClaimCountStatType: PolymeshPrimitivesStatisticsStatType; + let rawClaimPercentageStatType: PolymeshPrimitivesStatisticsStatType; + + let activeAssetStatsMock: jest.Mock; + let issuerDid: PolymeshPrimitivesIdentityId; + + beforeEach(() => { + context = dsMockUtils.getContextInstance(); + issuerDid = dsMockUtils.createMockIdentityId(); + asset = entityMockUtils.getFungibleAssetInstance(); + activeAssetStatsMock = dsMockUtils.createQueryMock('statistics', 'activeAssetStats'); + + rawCountStatType = dsMockUtils.createMockStatisticsStatType({ + op: dsMockUtils.createMockStatisticsOpType(StatType.Count), + claimIssuer: dsMockUtils.createMockOption(), + }); + rawPercentageStatType = dsMockUtils.createMockStatisticsStatType({ + op: dsMockUtils.createMockStatisticsOpType(StatType.Balance), + claimIssuer: dsMockUtils.createMockOption(), + }); + rawClaimCountStatType = dsMockUtils.createMockStatisticsStatType({ + op: dsMockUtils.createMockStatisticsOpType(StatType.Count), + claimIssuer: dsMockUtils.createMockOption([ + dsMockUtils.createMockClaimType(ClaimType.Affiliate), + issuerDid, + ]), + }); + rawClaimPercentageStatType = dsMockUtils.createMockStatisticsStatType({ + op: dsMockUtils.createMockStatisticsOpType(StatType.ScopedBalance), + claimIssuer: dsMockUtils.createMockOption([ + dsMockUtils.createMockClaimType(ClaimType.Accredited), + issuerDid, + ]), + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should return the active stats status for (Count) when stats disabled', async () => { + activeAssetStatsMock.mockResolvedValueOnce([]); + const count = new Count(asset, context); + + const result = await count.getStat(); + + expect(result).toEqual({ + isSet: false, + }); + }); + + it('should return the active stats status for (Count) when stats enabled', async () => { + activeAssetStatsMock.mockResolvedValueOnce([rawCountStatType]); + + const count = new Count(asset, context); + const result = await count.getStat(); + + expect(result).toEqual({ + isSet: true, + }); + }); + + it('should return the active stats status for (Percentage) when stats disabled', async () => { + activeAssetStatsMock.mockResolvedValueOnce([]); + + const percentage = new Percentage(asset, context); + const result = await percentage.getStat(); + + expect(result).toEqual({ + isSet: false, + }); + }); + + it('should return the active stats status for (Percentage) when stats enabled', async () => { + activeAssetStatsMock.mockResolvedValueOnce([rawPercentageStatType]); + const percentage = new Percentage(asset, context); + + const result = await percentage.getStat(); + + expect(result).toEqual({ + isSet: true, + }); + }); + + it('should return the active stats status for (ClaimCount) when stats disabled', async () => { + activeAssetStatsMock.mockResolvedValueOnce([]); + const claimCount = new ClaimCount(asset, context); + + const result = await claimCount.getStat(); + + expect(result).toEqual({ + isSet: false, + }); + }); + + it('should return the active stats status for (ClaimCount) when stats enabled', async () => { + activeAssetStatsMock.mockResolvedValueOnce([rawClaimCountStatType]); + const claimCount = new ClaimCount(asset, context); + + const result = await claimCount.getStat(); + + expect(result.isSet).toBeTruthy(); + expect(result.claims).toBeDefined(); + + const [claim] = result.claims || []; + expect(claim.claimType).toEqual(ClaimType.Affiliate); + }); + + it('should return the active stats status for (ClaimPercentage) when stats disabled', async () => { + activeAssetStatsMock.mockResolvedValueOnce([]); + const claimPercentage = new ClaimPercentage(asset, context); + + const result = await claimPercentage.getStat(); + + expect(result).toEqual({ + isSet: false, + }); + }); + + it('should return the active stats status for (ClaimPercentage) when stats enabled', async () => { + activeAssetStatsMock.mockResolvedValueOnce([rawClaimPercentageStatType]); + const claimPercentage = new ClaimPercentage(asset, context); + + const result = await claimPercentage.getStat(); + + expect(result.isSet).toBeTruthy(); + expect(result.claims).toBeDefined(); + + const [claim] = result.claims || []; + expect(claim.claimType).toEqual(ClaimType.Accredited); + }); + }); }); diff --git a/src/api/procedures/__tests__/setTransferRestrictions.ts b/src/api/procedures/__tests__/setTransferRestrictions.ts index 0e3a4f441d..a65c29b544 100644 --- a/src/api/procedures/__tests__/setTransferRestrictions.ts +++ b/src/api/procedures/__tests__/setTransferRestrictions.ts @@ -21,7 +21,7 @@ import { prepareStorage, Storage, } from '~/api/procedures/setTransferRestrictions'; -import { Context, PolymeshError } from '~/internal'; +import { Context, PolymeshError, Procedure, setTransferRestrictions } from '~/internal'; import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mocks'; import { Mocked } from '~/testUtils/types'; import { @@ -103,6 +103,7 @@ describe('setTransferRestrictions procedure', () => { let rawClaimCountRestrictionBtreeSet: BTreeSet; let rawClaimPercentageRestrictionBtreeSet: BTreeSet; let booleanToBoolSpy: jest.SpyInstance; + let identityIdToStringSpy: jest.SpyInstance; const min = new BigNumber(10); const max = new BigNumber(20); @@ -170,6 +171,7 @@ describe('setTransferRestrictions procedure', () => { type: TransferRestrictionType.ClaimPercentage, value: claimPercentageRestrictionValue, }; + identityIdToStringSpy = jest.spyOn(utilsConversionModule, 'identityIdToString'); }); let setAssetTransferComplianceTransaction: PolymeshTx< @@ -813,6 +815,38 @@ describe('setTransferRestrictions procedure', () => { expect(err.message).toBe('Duplicate Jurisdiction CountryCode found in input'); expect(err.data).toEqual({ countryCode: CountryCode.Us }); + + args = { + ticker, + restrictions: [ + { + min, + max, + issuer, + claim: { + type: ClaimType.Jurisdiction, + }, + }, + { + min, + max, + issuer, + claim: { + type: ClaimType.Jurisdiction, + }, + }, + ], + type: TransferRestrictionType.ClaimCount, + }; + + try { + await prepareSetTransferRestrictions.call(proc, args); + } catch (error) { + err = error; + } + + expect(err.message).toBe('Duplicate Jurisdiction CountryCode found in input'); + expect(err.data).toEqual({ countryCode: undefined }); }); describe('getAuthorization', () => { @@ -881,11 +915,7 @@ describe('setTransferRestrictions procedure', () => { beforeAll(() => { identityScopeId = 'someScopeId'; - dsMockUtils.createQueryMock('statistics', 'activeAssetStats'); - dsMockUtils.createQueryMock('statistics', 'assetTransferCompliances'); - rawIdentityScopeId = dsMockUtils.createMockIdentityId(identityScopeId); - queryMultiMock = dsMockUtils.getQueryMultiMock(); rawCountStatType = dsMockUtils.createMockStatisticsStatType(); rawBalanceStatType = dsMockUtils.createMockStatisticsStatType({ @@ -908,6 +938,8 @@ describe('setTransferRestrictions procedure', () => { }); beforeEach(() => { + queryMultiMock = dsMockUtils.getQueryMultiMock(); + when(stringToIdentityIdSpy) .calledWith(identityScopeId, mockContext) .mockReturnValue(rawIdentityScopeId); @@ -931,6 +963,8 @@ describe('setTransferRestrictions procedure', () => { dsMockUtils.createMockIdentityId(), ]), }); + dsMockUtils.createQueryMock('statistics', 'activeAssetStats'); + dsMockUtils.createQueryMock('statistics', 'assetTransferCompliances'); }); afterAll(() => { @@ -1061,7 +1095,65 @@ describe('setTransferRestrictions procedure', () => { }); }); - it('should return current exemptions for the restriction', async () => {}); + it('should return current exemptions for the restriction', async () => { + identityIdToStringSpy.mockReturnValue('someDid'); + queryMultiResult = [ + statBtreeSet, + { + paused: dsMockUtils.createMockBool(false), + requirements: [ + rawClaimCountRestriction, + rawClaimPercentageRestriction, + rawCountRestriction, + rawPercentageRestriction, + ], + } as unknown as PolymeshPrimitivesTransferComplianceAssetTransferCompliance, + ]; + + queryMultiMock.mockReturnValue(queryMultiResult); + const mock = dsMockUtils.createQueryMock('statistics', 'transferConditionExemptEntities'); + mock.entries.mockResolvedValue([ + [ + { + args: [ + { + claimType: dsMockUtils.createMockOption( + dsMockUtils.createMockClaimType(ClaimType.Accredited) + ), + }, + dsMockUtils.createMockIdentityId('someDid'), + ], + }, + true, + ], + ]); + + const proc = procedureMockUtils.getInstance< + SetTransferRestrictionsParams, + BigNumber, + Storage + >(mockContext); + const boundFunc = prepareStorage.bind(proc); + + args = { + ticker, + type: TransferRestrictionType.Count, + restrictions: [ + { + count, + }, + ], + }; + + const mockIdentity = entityMockUtils.getIdentityInstance({ did: 'someDid' }); + const result = await boundFunc(args); + expect(JSON.stringify(result.currentExemptions)).toEqual( + JSON.stringify({ + ...emptyExemptions, + Accredited: [mockIdentity], + }) + ); + }); }); describe('addExemptionIfNotPresent', () => { @@ -1087,4 +1179,12 @@ describe('setTransferRestrictions procedure', () => { expect(exemptionRecords[claimType]).toEqual([toInsertId]); }); }); + + describe('setTransferRestrictions', () => { + it('should return the result of the prepareSetTransferRestrictions call', async () => { + const result = setTransferRestrictions(); + + expect(result).toBeInstanceOf(Procedure); + }); + }); }); diff --git a/src/api/procedures/setTransferRestrictions.ts b/src/api/procedures/setTransferRestrictions.ts index df3c438290..f32e10b0ae 100644 --- a/src/api/procedures/setTransferRestrictions.ts +++ b/src/api/procedures/setTransferRestrictions.ts @@ -124,7 +124,7 @@ function assertInputValid( data: { countryCode: claim.countryCode }, }); } - seenJurisdictions.add(claim.countryCode || 'none'); + seenJurisdictions.add(claim.countryCode ?? 'none'); } else { // cannot add two ClaimType.Accredited or ClaimType.Affiliate restrictions will result in internal error if (seenClaimTypes.has(claim.type)) {