Skip to content

Commit 6372758

Browse files
committed
feat: 🎸 initiate ca validation improvements
1 parent 299750c commit 6372758

File tree

2 files changed

+119
-5
lines changed

2 files changed

+119
-5
lines changed

src/api/procedures/__tests__/initiateCorporateAction.ts

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ import {
1515
Params,
1616
prepareInitiateCorporateAction,
1717
} from '~/api/procedures/initiateCorporateAction';
18-
import { Checkpoint, CheckpointSchedule, Context, CorporateAction, Procedure } from '~/internal';
18+
import {
19+
Checkpoint,
20+
CheckpointSchedule,
21+
Context,
22+
CorporateAction,
23+
Identity,
24+
Procedure,
25+
} from '~/internal';
1926
import { dsMockUtils, entityMockUtils, procedureMockUtils } from '~/testUtils/mocks';
2027
import { Mocked } from '~/testUtils/types';
2128
import {
@@ -45,7 +52,7 @@ describe('assertCheckpointValue', () => {
4552
});
4653

4754
it('should throw an error if the provided Date is in the past', () => {
48-
const checkpoint = new Date(new Date().getTime() - 1000 * 60 * 60 * 24);
55+
const checkpoint = new Date(Date.now() - 1000 * 60 * 60 * 24);
4956

5057
return expect(assertCheckpointValue(checkpoint)).rejects.toThrow(
5158
'Checkpoint must be in the future'
@@ -56,7 +63,7 @@ describe('assertCheckpointValue', () => {
5663
const mockCheckpoint = new Checkpoint({ id: new BigNumber(1), assetId: asset.id }, context);
5764
mockCheckpoint.createdAt = jest
5865
.fn()
59-
.mockResolvedValue(new Date(new Date().getTime() - 1000 * 60 * 60 * 24));
66+
.mockResolvedValue(new Date(Date.now() - 1000 * 60 * 60 * 24));
6067

6168
return expect(assertCheckpointValue(mockCheckpoint)).rejects.toThrow(
6269
'Checkpoint must be in the future'
@@ -89,6 +96,9 @@ describe('initiateCorporateAction procedure', () => {
8996
let initiateCorporateActionTransaction: PolymeshTx<unknown[]>;
9097

9198
let corporateActionParamsToMeshCorporateActionArgsSpy: jest.SpyInstance;
99+
let stringToIdentityIdSpy: jest.SpyInstance;
100+
let assetToMeshAssetIdSpy: jest.SpyInstance;
101+
let signingIdentity: Identity;
92102

93103
beforeAll(() => {
94104
entityMockUtils.initMocks();
@@ -97,10 +107,10 @@ describe('initiateCorporateAction procedure', () => {
97107

98108
assetId = '0x12341234123412341234123412341234';
99109
asset = entityMockUtils.getFungibleAssetInstance({ assetId });
100-
checkpoint = new Date(new Date().getTime() + 1000 * 60 * 60 * 24);
110+
checkpoint = new Date(Date.now() + 1000 * 60 * 60 * 24);
101111
kind = CorporateActionKind.IssuerNotice;
102112

103-
declarationDate = new Date(new Date().getTime() - 1000 * 60 * 60 * 24);
113+
declarationDate = new Date(Date.now() - 1000 * 60 * 60 * 24);
104114
description = 'someDescription';
105115
rawCorporateActionArgs = dsMockUtils.createMockInitiateCorporateActionArgs({
106116
assetId,
@@ -117,6 +127,10 @@ describe('initiateCorporateAction procedure', () => {
117127
utilsConversionModule,
118128
'corporateActionParamsToMeshCorporateActionArgs'
119129
);
130+
stringToIdentityIdSpy = jest.spyOn(utilsConversionModule, 'stringToIdentityId');
131+
assetToMeshAssetIdSpy = jest.spyOn(utilsConversionModule, 'assetToMeshAssetId');
132+
133+
signingIdentity = entityMockUtils.getIdentityInstance({ did: 'someDid' });
120134
});
121135

122136
beforeEach(() => {
@@ -126,6 +140,20 @@ describe('initiateCorporateAction procedure', () => {
126140
);
127141

128142
mockContext = dsMockUtils.getContextInstance();
143+
mockContext.getSigningIdentity = jest.fn().mockResolvedValue(signingIdentity);
144+
145+
const rawAssetId = dsMockUtils.createMockAssetId(assetId);
146+
const rawIdentityId = dsMockUtils.createMockIdentityId(signingIdentity.did);
147+
148+
when(assetToMeshAssetIdSpy).calledWith(asset, mockContext).mockReturnValue(rawAssetId);
149+
when(stringToIdentityIdSpy)
150+
.calledWith(signingIdentity.did, mockContext)
151+
.mockReturnValue(rawIdentityId);
152+
153+
// Default: mock Full agent (asset owner scenario)
154+
dsMockUtils.createQueryMock('externalAgents', 'groupOfAgent', {
155+
returnValue: dsMockUtils.createMockOption(dsMockUtils.createMockAgentGroup('Full')),
156+
});
129157

130158
when(corporateActionParamsToMeshCorporateActionArgsSpy)
131159
.calledWith(
@@ -213,6 +241,81 @@ describe('initiateCorporateAction procedure', () => {
213241
});
214242
});
215243

244+
describe('tax withholdings validation', () => {
245+
it('should validate tax withholdings and detect duplicates', async () => {
246+
const proc = procedureMockUtils.getInstance<Params, CorporateAction>(mockContext);
247+
248+
// Set maxDidWhts to at least 2 to allow duplicate check to run before limit check
249+
dsMockUtils.setConstMock('corporateAction', 'maxDidWhts', {
250+
returnValue: dsMockUtils.createMockU32(new BigNumber(10)),
251+
});
252+
253+
const identity1 = entityMockUtils.getIdentityInstance({ did: 'did1' });
254+
const identity2 = entityMockUtils.getIdentityInstance({ did: 'did2' });
255+
256+
const asIdentitySpy = jest.spyOn(utilsInternalModule, 'asIdentity');
257+
when(asIdentitySpy).calledWith(identity1, mockContext).mockReturnValue(identity1);
258+
when(asIdentitySpy).calledWith(identity2, mockContext).mockReturnValue(identity2);
259+
260+
await expect(
261+
prepareInitiateCorporateAction.call(proc, {
262+
asset,
263+
declarationDate,
264+
description,
265+
kind,
266+
checkpoint,
267+
taxWithholdings: [
268+
{ identity: identity1, percentage: new BigNumber(10) },
269+
{ identity: identity1, percentage: new BigNumber(20) }, // Duplicate
270+
],
271+
targets: null,
272+
defaultTaxWithholding: null,
273+
})
274+
).rejects.toMatchObject({
275+
message: 'Identity included more than once in the tax withholding list',
276+
data: {
277+
identity: identity1,
278+
},
279+
});
280+
281+
asIdentitySpy.mockRestore();
282+
});
283+
284+
it('should validate tax withholdings limit', async () => {
285+
const proc = procedureMockUtils.getInstance<Params, CorporateAction>(mockContext);
286+
287+
const identity1 = entityMockUtils.getIdentityInstance({ did: 'did1' });
288+
const identity2 = entityMockUtils.getIdentityInstance({ did: 'did2' });
289+
290+
const asIdentitySpy = jest.spyOn(utilsInternalModule, 'asIdentity');
291+
when(asIdentitySpy).calledWith(identity1, mockContext).mockReturnValue(identity1);
292+
when(asIdentitySpy).calledWith(identity2, mockContext).mockReturnValue(identity2);
293+
294+
// Set maxDidWhts to 1, but provide 2 tax withholdings
295+
dsMockUtils.setConstMock('corporateAction', 'maxDidWhts', {
296+
returnValue: dsMockUtils.createMockU32(new BigNumber(1)),
297+
});
298+
299+
await expect(
300+
prepareInitiateCorporateAction.call(proc, {
301+
asset,
302+
declarationDate,
303+
description,
304+
kind,
305+
checkpoint,
306+
taxWithholdings: [
307+
{ identity: identity1, percentage: new BigNumber(10) },
308+
{ identity: identity2, percentage: new BigNumber(20) },
309+
],
310+
targets: null,
311+
defaultTaxWithholding: null,
312+
})
313+
).rejects.toThrow(); // Should throw from assertCaTaxWithholdingsValid
314+
315+
asIdentitySpy.mockRestore();
316+
});
317+
});
318+
216319
describe('initiateCorporateActionResolver', () => {
217320
let filterEventRecordsSpy: jest.SpyInstance;
218321
let getCorporateActionWithDescriptionSpy: jest.SpyInstance;

src/api/procedures/initiateCorporateAction.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ISubmittableResult } from '@polkadot/types/types';
22

3+
import { assertCaTaxWithholdingsValid } from '~/api/procedures/utils';
34
import {
45
Checkpoint,
56
CheckpointSchedule,
@@ -132,8 +133,10 @@ export async function prepareInitiateCorporateAction(
132133
defaultTaxWithholding,
133134
} = args;
134135

136+
// Validate declarationDate is in the past
135137
assertDeclarationDate(declarationDate);
136138

139+
// Validate checkpoint/recordDate
137140
let checkpointValue: Checkpoint | CheckpointSchedule | Date | null = null;
138141

139142
if (checkpoint) {
@@ -142,6 +145,7 @@ export async function prepareInitiateCorporateAction(
142145
await assertCheckpointValue(checkpointValue);
143146
}
144147

148+
// Validate details length (max_details_length)
145149
const rawMaxDetailsLength = await query.corporateAction.maxDetailsLength();
146150
const maxDetailsLength = u32ToBigNumber(rawMaxDetailsLength);
147151

@@ -156,6 +160,13 @@ export async function prepareInitiateCorporateAction(
156160
});
157161
}
158162

163+
// Validate targets limit (handled in targetsToTargetIdentities conversion)
164+
// Validate withholding_tax: check limit and detect duplicates (chain throws error on duplicates)
165+
if (taxWithholdings && taxWithholdings.length > 0) {
166+
// Check limit and detect duplicates (assertCaTaxWithholdingsValid handles both)
167+
assertCaTaxWithholdingsValid(taxWithholdings, context);
168+
}
169+
159170
const rawArgs = corporateActionParamsToMeshCorporateActionArgs(
160171
{
161172
asset,

0 commit comments

Comments
 (0)