Skip to content

Commit 76e7faa

Browse files
authored
Merge pull request #104 from PolymathNetwork/DA-297/mock-cdd
feat: 🎸 Add mock CDD endpoint
2 parents 9a281d9 + 360b383 commit 76e7faa

14 files changed

+261
-11
lines changed

src/accounts/accounts.module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ import { SigningModule } from '~/signing/signing.module';
1111
imports: [PolymeshModule, SigningModule],
1212
controllers: [AccountsController],
1313
providers: [AccountsService],
14+
exports: [AccountsService],
1415
})
1516
export class AccountsModule {}

src/accounts/dto/transaction-history-filters.dto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class TransactionHistoryFiltersDto {
1717
example: '1000000',
1818
})
1919
@IsOptional()
20-
@IsBigNumber()
20+
@IsBigNumber({ min: 0 })
2121
@ToBigNumber()
2222
readonly blockNumber?: BigNumber;
2323

src/common/decorators/validation.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ export function IsTicker(validationOptions?: ValidationOptions) {
4242
);
4343
}
4444

45-
export function IsBigNumber(validationOptions?: ValidationOptions) {
45+
export function IsBigNumber(
46+
numericValidations: { min?: number; max?: number } = {},
47+
validationOptions?: ValidationOptions
48+
) {
49+
const isDefined = (v: number | undefined): v is number => typeof v !== 'undefined';
50+
const { min, max } = numericValidations;
4651
// eslint-disable-next-line @typescript-eslint/ban-types
4752
return function (object: Object, propertyName: string) {
4853
registerDecorator({
@@ -52,10 +57,33 @@ export function IsBigNumber(validationOptions?: ValidationOptions) {
5257
options: validationOptions,
5358
validator: {
5459
validate(value: unknown) {
55-
return value instanceof BigNumber && !value.isNaN();
60+
if (!(value instanceof BigNumber)) {
61+
return false;
62+
}
63+
if (value.isNaN()) {
64+
return false;
65+
}
66+
if (isDefined(min) && value.lt(min)) {
67+
return false;
68+
}
69+
if (isDefined(max) && value.gt(max)) {
70+
return false;
71+
}
72+
73+
return true;
5674
},
5775
defaultMessage(args: ValidationArguments) {
58-
return `${args.property} must be a number`;
76+
let message = `${args.property} must be a number`;
77+
const hasMin = isDefined(min);
78+
const hasMax = isDefined(max);
79+
if (hasMin && hasMax) {
80+
message += ` that is between ${min} and ${max}`;
81+
} else if (hasMin) {
82+
message += ` that is at least ${min}`;
83+
} else if (hasMax) {
84+
message += ` that is at most ${max}`;
85+
}
86+
return message;
5987
},
6088
},
6189
});

src/corporate-actions/dto/tax-withholding.dto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ApiProperty } from '@nestjs/swagger';
44
import { BigNumber } from '@polymathnetwork/polymesh-sdk';
55

66
import { ToBigNumber } from '~/common/decorators/transformation';
7-
import { IsBigNumber, IsDid } from '~/common/decorators/validation';
7+
import { IsBigNumber,IsDid } from '~/common/decorators/validation';
88

99
export class TaxWithholdingDto {
1010
@ApiProperty({
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/* istanbul ignore file */
2+
3+
import { ApiProperty } from '@nestjs/swagger';
4+
import { BigNumber } from '@polymathnetwork/polymesh-sdk';
5+
import { IsString } from 'class-validator';
6+
7+
import { ToBigNumber } from '~/common/decorators/transformation';
8+
import { IsBigNumber } from '~/common/decorators/validation';
9+
10+
export class CreateMockIdentityDto {
11+
@ApiProperty({
12+
description: 'Account address to create an Identity for',
13+
example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV',
14+
})
15+
@IsString()
16+
readonly address: string;
17+
18+
@ApiProperty({
19+
description: 'Starting POLYX balance to initialize the Account with',
20+
example: 100000,
21+
})
22+
@IsBigNumber({ min: 0 })
23+
@ToBigNumber()
24+
readonly initialPolyx: BigNumber;
25+
}

src/identities/identities.controller.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ClaimsService } from '~/claims/claims.service';
1515
import { ResultsModel } from '~/common/models/results.model';
1616
import { IdentitiesController } from '~/identities/identities.controller';
1717
import { IdentitiesService } from '~/identities/identities.service';
18+
import * as identityUtil from '~/identities/identities.util';
1819
import { AccountModel } from '~/identities/models/account.model';
1920
import { IdentitySignerModel } from '~/identities/models/identity-signer.model';
2021
import { IdentityModel } from '~/identities/models/identity.model';
@@ -472,4 +473,24 @@ describe('IdentitiesController', () => {
472473
expect(mockTickerReservationsService.findAllByOwner).toHaveBeenCalledWith(did);
473474
});
474475
});
476+
477+
describe('mockCdd', () => {
478+
it('should call the service and return the Identity', async () => {
479+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
480+
const fakeIdentityModel = 'fakeIdentityModel' as any;
481+
const createIdentityModelSpy = jest
482+
.spyOn(identityUtil, 'createIdentityModel')
483+
.mockResolvedValue(fakeIdentityModel);
484+
485+
const params = {
486+
address: '5abc',
487+
initialPolyx: new BigNumber(10),
488+
};
489+
490+
const result = await controller.createMockCdd(params);
491+
expect(result).toEqual(fakeIdentityModel);
492+
expect(mockIdentitiesService.createMockCdd).toHaveBeenCalledWith(params);
493+
createIdentityModelSpy.mockRestore();
494+
});
495+
});
475496
});

src/identities/identities.controller.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { DidDto, IncludeExpiredFilterDto } from '~/common/dto/params.dto';
3737
import { PaginatedResultsModel } from '~/common/models/paginated-results.model';
3838
import { ResultsModel } from '~/common/models/results.model';
3939
import { AddSecondaryAccountParamsDto } from '~/identities/dto/add-secondary-account-params.dto';
40+
import { CreateMockIdentityDto } from '~/identities/dto/create-mock-identity.dto';
4041
import { IdentitiesService } from '~/identities/identities.service';
4142
import { createIdentityModel } from '~/identities/identities.util';
4243
import { IdentityModel } from '~/identities/models/identity.model';
@@ -447,4 +448,23 @@ export class IdentitiesController {
447448
const results = await this.tickerReservationsService.findAllByOwner(did);
448449
return new ResultsModel({ results });
449450
}
451+
452+
@ApiOperation({
453+
summary: 'Creates a fake Identity for an Account and sets its POLYX balance (DEV ONLY)',
454+
description:
455+
'This endpoint creates a Identity for an Account and sets its POLYX balance. Will only work with development chains. Alice must exist, be able to call `testUtils.mockCddRegisterDid` and have `sudo` permission',
456+
})
457+
@ApiOkResponse({ description: 'The details of the newly created Identity' })
458+
@ApiBadRequestResponse({
459+
description:
460+
'This instance of the REST API is pointing to a chain that lacks development features. A proper CDD provider must be used instead',
461+
})
462+
@ApiInternalServerErrorResponse({
463+
description: 'Failed to execute an extrinsic, or something unexpected',
464+
})
465+
@Post('/mock-cdd')
466+
public async createMockCdd(@Body() params: CreateMockIdentityDto): Promise<IdentityModel> {
467+
const identity = await this.identitiesService.createMockCdd(params);
468+
return createIdentityModel(identity);
469+
}
450470
}

src/identities/identities.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { forwardRef, Module } from '@nestjs/common';
44

5+
import { AccountsModule } from '~/accounts/accounts.module';
56
import { AssetsModule } from '~/assets/assets.module';
67
import { AuthorizationsModule } from '~/authorizations/authorizations.module';
78
import { ClaimsModule } from '~/claims/claims.module';
@@ -23,6 +24,7 @@ import { TickerReservationsModule } from '~/ticker-reservations/ticker-reservati
2324
forwardRef(() => SettlementsModule),
2425
forwardRef(() => AuthorizationsModule),
2526
forwardRef(() => PortfoliosModule),
27+
AccountsModule,
2628
ClaimsModule,
2729
TickerReservationsModule,
2830
],

src/identities/identities.service.spec.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Test, TestingModule } from '@nestjs/testing';
1111
import { BigNumber } from '@polymathnetwork/polymesh-sdk';
1212
import { ErrorCode, TxTags } from '@polymathnetwork/polymesh-sdk/types';
1313

14+
import { AccountsService } from '~/accounts/accounts.service';
1415
import { TransactionType } from '~/common/types';
1516
import { IdentitiesService } from '~/identities/identities.service';
1617
import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger';
@@ -19,7 +20,7 @@ import { PolymeshModule } from '~/polymesh/polymesh.module';
1920
import { PolymeshService } from '~/polymesh/polymesh.service';
2021
import { mockSigningProvider } from '~/signing/signing.mock';
2122
import { MockIdentity, MockPolymesh, MockTransactionQueue } from '~/test-utils/mocks';
22-
import { MockSigningService } from '~/test-utils/service-mocks';
23+
import { MockAccountsService, MockSigningService } from '~/test-utils/service-mocks';
2324
import { ErrorCase } from '~/test-utils/types';
2425

2526
jest.mock('@polymathnetwork/polymesh-sdk/utils', () => ({
@@ -28,20 +29,37 @@ jest.mock('@polymathnetwork/polymesh-sdk/utils', () => ({
2829
isPolymeshTransaction: mockIsPolymeshTransaction,
2930
}));
3031

32+
jest.mock('@polkadot/keyring', () => ({
33+
...jest.requireActual('@polkadot/keyring'),
34+
Keyring: jest.fn().mockImplementation(() => {
35+
return {
36+
addFromUri: jest.fn(),
37+
};
38+
}),
39+
}));
40+
3141
describe('IdentitiesService', () => {
3242
let service: IdentitiesService;
3343
let polymeshService: PolymeshService;
3444
let mockPolymeshApi: MockPolymesh;
3545
let mockSigningService: MockSigningService;
46+
const mockAccountsService = new MockAccountsService();
3647

3748
beforeEach(async () => {
3849
mockPolymeshApi = new MockPolymesh();
3950
mockSigningService = mockSigningProvider.useValue;
4051

4152
const module: TestingModule = await Test.createTestingModule({
4253
imports: [PolymeshModule],
43-
providers: [IdentitiesService, mockPolymeshLoggerProvider, mockSigningProvider],
54+
providers: [
55+
IdentitiesService,
56+
AccountsService,
57+
mockPolymeshLoggerProvider,
58+
mockSigningProvider,
59+
],
4460
})
61+
.overrideProvider(AccountsService)
62+
.useValue(mockAccountsService)
4563
.overrideProvider(POLYMESH_API)
4664
.useValue(mockPolymeshApi)
4765
.compile();
@@ -218,4 +236,16 @@ describe('IdentitiesService', () => {
218236
});
219237
});
220238
});
239+
240+
describe('createMockCdd', () => {
241+
it('should return a promise', async () => {
242+
const params = {
243+
address: 'address',
244+
initialPolyx: new BigNumber(10),
245+
};
246+
mockPolymeshApi.network.getSs58Format.mockReturnValue(new BigNumber(42));
247+
const result = service.createMockCdd(params);
248+
expect(result).toBeInstanceOf(Promise);
249+
});
250+
});
221251
});

src/identities/identities.service.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { Injectable, NotFoundException } from '@nestjs/common';
1+
import {
2+
BadRequestException,
3+
Injectable,
4+
InternalServerErrorException,
5+
NotFoundException,
6+
} from '@nestjs/common';
7+
import { Keyring } from '@polkadot/keyring';
8+
import { KeyringPair } from '@polkadot/keyring/types';
29
import {
310
Asset,
411
AuthorizationRequest,
@@ -7,18 +14,23 @@ import {
714
} from '@polymathnetwork/polymesh-sdk/types';
815
import { isPolymeshError } from '@polymathnetwork/polymesh-sdk/utils';
916

17+
import { AccountsService } from '~/accounts/accounts.service';
1018
import { processQueue, QueueResult } from '~/common/utils';
1119
import { AddSecondaryAccountParamsDto } from '~/identities/dto/add-secondary-account-params.dto';
20+
import { CreateMockIdentityDto } from '~/identities/dto/create-mock-identity.dto';
1221
import { PolymeshLogger } from '~/logger/polymesh-logger.service';
1322
import { PolymeshService } from '~/polymesh/polymesh.service';
1423
import { SigningService } from '~/signing/signing.service';
1524

1625
@Injectable()
1726
export class IdentitiesService {
27+
private alicePair: KeyringPair;
28+
1829
constructor(
1930
private readonly polymeshService: PolymeshService,
2031
private readonly logger: PolymeshLogger,
21-
private readonly signingService: SigningService
32+
private readonly signingService: SigningService,
33+
private readonly accountsService: AccountsService
2234
) {
2335
logger.setContext(IdentitiesService.name);
2436
}
@@ -65,4 +77,46 @@ export class IdentitiesService {
6577
const { inviteAccount } = this.polymeshService.polymeshApi.accountManagement;
6678
return processQueue(inviteAccount, params, { signingAccount: address });
6779
}
80+
81+
/**
82+
* @note intended for development chains only (i.e. Alice exists and can call `testUtils.createMockCddClaim`)
83+
*/
84+
public async createMockCdd({ address, initialPolyx }: CreateMockIdentityDto): Promise<Identity> {
85+
const {
86+
_polkadotApi: {
87+
tx: { testUtils, balances, sudo },
88+
},
89+
network,
90+
} = this.polymeshService.polymeshApi;
91+
92+
if (!testUtils) {
93+
throw new BadRequestException(
94+
'The chain does not have the `testUtils` pallet enabled. This endpoint is intended for development use only'
95+
);
96+
}
97+
98+
const targetAccount = await this.accountsService.findOne(address);
99+
100+
if (!this.alicePair) {
101+
const ss58Format = network.getSs58Format().toNumber();
102+
const keyring = new Keyring({ type: 'sr25519', ss58Format });
103+
this.alicePair = keyring.addFromUri('//Alice');
104+
}
105+
106+
await this.polymeshService.execTransaction(
107+
this.alicePair,
108+
testUtils.mockCddRegisterDid,
109+
address
110+
);
111+
const setBalance = balances.setBalance(address, initialPolyx.shiftedBy(6).toNumber(), 0);
112+
await this.polymeshService.execTransaction(this.alicePair, sudo.sudo, setBalance);
113+
114+
const id = await targetAccount.getIdentity();
115+
116+
if (!id) {
117+
throw new InternalServerErrorException('The Identity was not created');
118+
}
119+
120+
return id;
121+
}
68122
}

0 commit comments

Comments
 (0)