Skip to content

Commit

Permalink
feat: 🎸 identities rotatePrimaryKey/attestPrimaryKeyRotation (#251)
Browse files Browse the repository at this point in the history
* feat: 🎸 identities rotatePrimaryKey

* feat: 🎸 identities attestPrimaryKeyRotation
  • Loading branch information
sansan authored Mar 13, 2024
1 parent bfd272d commit 942ad2e
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 3 deletions.
10 changes: 10 additions & 0 deletions src/common/utils/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,13 @@ export function isNftLeg(leg: Leg): leg is NftLeg {
export async function clearEventLoop(): Promise<void> {
await new Promise(resolve => setImmediate(resolve));
}

/**
* helper to get current date plus one year in ISO format
*/
export function getNextYearISO(): string {
const nextYear = new Date();
nextYear.setFullYear(nextYear.getFullYear() + 1);

return nextYear.toISOString();
}
25 changes: 25 additions & 0 deletions src/identities/dto/rotate-primary-key-params.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* istanbul ignore file */

import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsDate, IsOptional, IsString } from 'class-validator';

import { TransactionBaseDto } from '~/common/dto/transaction-base-dto';
import { getNextYearISO } from '~/common/utils';

export class RotatePrimaryKeyParamsDto extends TransactionBaseDto {
@ApiProperty({
description: 'Account address which will become the primary key',
example: '5grwXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXx',
})
@IsString()
readonly targetAccount: string;

@ApiPropertyOptional({
description: 'Date at which the Identity will expire',
example: getNextYearISO(),
type: 'string',
})
@IsOptional()
@IsDate()
readonly expiry?: Date;
}
51 changes: 51 additions & 0 deletions src/identities/identities.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,4 +657,55 @@ describe('IdentitiesController', () => {
});
});
});

describe('rotatePrimaryKey', () => {
it('should return the transaction details on rotating primary key for signing Identity', async () => {
const mockAuthorization = new MockAuthorizationRequest();
const mockData = {
...txResult,
result: mockAuthorization,
};
mockIdentitiesService.rotatePrimaryKey.mockResolvedValue(mockData);

const result = await controller.rotatePrimaryKey({
signer: 'Ox60',
targetAccount: '5xdd',
});

expect(result).toEqual({
...txResult,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
authorizationRequest: createAuthorizationRequestModel(mockAuthorization as any),
});
});
});

describe('attestPrimaryKeyRotation', () => {
it('should return the transaction details on attesting primary key rotation for DID', async () => {
const mockAuthorization = new MockAuthorizationRequest();
const mockData = {
...txResult,
result: mockAuthorization,
};
mockIdentitiesService.attestPrimaryKeyRotation.mockResolvedValue(mockData);

const mockIdentity = new MockIdentity();
mockIdentity.did = did;
mockIdentitiesService.findOne.mockResolvedValue(mockIdentity);

const result = await controller.attestPrimaryKeyRotation(
{ did },
{
signer: 'Ox60',
targetAccount: '5xdd',
}
);

expect(result).toEqual({
...txResult,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
authorizationRequest: createAuthorizationRequestModel(mockAuthorization as any),
});
});
});
});
58 changes: 58 additions & 0 deletions src/identities/identities.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { DeveloperTestingService } from '~/developer-testing/developer-testing.s
import { CreateMockIdentityDto } from '~/developer-testing/dto/create-mock-identity.dto';
import { AddSecondaryAccountParamsDto } from '~/identities/dto/add-secondary-account-params.dto';
import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto';
import { RotatePrimaryKeyParamsDto } from '~/identities/dto/rotate-primary-key-params.dto';
import { IdentitiesService } from '~/identities/identities.service';
import { createIdentityModel } from '~/identities/identities.util';
import { CreatedIdentityModel } from '~/identities/models/created-identity.model';
Expand Down Expand Up @@ -622,4 +623,61 @@ export class IdentitiesController {

return new GroupedInstructionModel(result);
}

@ApiOperation({
summary: 'Rotate Primary Key',
description:
'Creates an Authorization to rotate primary key of the signing Identity by the `targetAccount`. <br />' +
'The existing key for the signing Identity will be unlinked once the new primary key is attested.',
})
@ApiTransactionResponse({
description: 'Newly created Authorization Request along with transaction details',
type: CreatedAuthorizationRequestModel,
})
@ApiBadRequestResponse({
description:
'The target Account already has a pending invitation to become the primary key of the given Identity',
})
@Post('rotate-primary-key')
async rotatePrimaryKey(
@Body() rotatePrimaryKeyDto: RotatePrimaryKeyParamsDto
): Promise<TransactionResponseModel> {
const serviceResult = await this.identitiesService.rotatePrimaryKey(rotatePrimaryKeyDto);

return handleServiceResult(serviceResult, authorizationRequestResolver);
}

@ApiOperation({
summary: 'Attest Primary Key Rotation',
description:
'The transaction signer must be a CDD provider. <br />' +
'This will create Authorization Request to accept the `targetAccount` to become as the primary key of the given Identity.',
})
@ApiTransactionResponse({
description: 'Newly created Authorization Request along with transaction details',
type: CreatedAuthorizationRequestModel,
})
@ApiBadRequestResponse({
description:
'The target Account already has a pending attestation to become the primary key of the target Identity',
})
@ApiParam({
name: 'did',
description: 'The DID of the Identity for which to attest primary key rotation',
type: 'string',
required: true,
example: '0x0600000000000000000000000000000000000000000000000000000000000000',
})
@Post(':did/rotate-primary-key')
async attestPrimaryKeyRotation(
@Param() { did }: DidDto,
@Body() rotatePrimaryKeyDto: RotatePrimaryKeyParamsDto
): Promise<TransactionResponseModel> {
const serviceResult = await this.identitiesService.attestPrimaryKeyRotation(
did,
rotatePrimaryKeyDto
);

return handleServiceResult(serviceResult, authorizationRequestResolver);
}
}
47 changes: 46 additions & 1 deletion src/identities/identities.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import { PolymeshModule } from '~/polymesh/polymesh.module';
import { PolymeshService } from '~/polymesh/polymesh.service';
import { mockSigningProvider } from '~/signing/signing.mock';
import { testValues } from '~/test-utils/consts';
import { MockIdentity, MockPolymesh, MockTransaction } from '~/test-utils/mocks';
import {
createMockTxResult,
MockIdentity,
MockPolymesh,
MockTransaction,
} from '~/test-utils/mocks';
import {
MockAccountsService,
mockTransactionsProvider,
Expand Down Expand Up @@ -207,4 +212,44 @@ describe('IdentitiesService', () => {
expect(mockTransactionsService.submit).toHaveBeenCalled();
});
});

describe('rotatePrimaryKey', () => {
it('should return the transaction details', async () => {
const txResult = createMockTxResult(TxTags.identity.AddAuthorization);

mockTransactionsService.submit.mockResolvedValue(txResult);

const body = {
signer,
targetAccount: 'address',
};

const result = await service.rotatePrimaryKey(body);
expect(result).toEqual(txResult);
expect(mockTransactionsService.submit).toHaveBeenCalled();
});
});

describe('attestPrimaryKeyRotation', () => {
it('should return the transaction details', async () => {
const txResult = createMockTxResult(TxTags.identity.AddAuthorization);

mockTransactionsService.submit.mockResolvedValue(txResult);

const mockIdentity = new MockIdentity();

const findOneSpy = jest.spyOn(service, 'findOne');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
findOneSpy.mockResolvedValue(mockIdentity as any);

const body = {
signer,
targetAccount: 'address',
};

const result = await service.attestPrimaryKeyRotation(mockIdentity.did, body);
expect(result).toEqual(txResult);
expect(mockTransactionsService.submit).toHaveBeenCalled();
});
});
});
39 changes: 39 additions & 0 deletions src/identities/identities.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { extractTxOptions, ServiceReturn } from '~/common/utils';
import { AddSecondaryAccountParamsDto } from '~/identities/dto/add-secondary-account-params.dto';
import { RegisterIdentityDto } from '~/identities/dto/register-identity.dto';
import { RotatePrimaryKeyParamsDto } from '~/identities/dto/rotate-primary-key-params.dto';
import { PolymeshLogger } from '~/logger/polymesh-logger.service';
import { PolymeshService } from '~/polymesh/polymesh.service';
import { TransactionsService } from '~/transactions/transactions.service';
Expand Down Expand Up @@ -99,4 +100,42 @@ export class IdentitiesService {

return this.transactionsService.submit(registerIdentity, params, options);
}

public async rotatePrimaryKey(
rotatePrimaryKeyDto: RotatePrimaryKeyParamsDto
): ServiceReturn<AuthorizationRequest> {
const {
polymeshService: { polymeshApi },
} = this;

const { options, args } = extractTxOptions(rotatePrimaryKeyDto);

const { rotatePrimaryKey } = polymeshApi.identities;

return this.transactionsService.submit(rotatePrimaryKey, args, options);
}

public async attestPrimaryKeyRotation(
did: string,
rotatePrimaryKeyDto: RotatePrimaryKeyParamsDto
): ServiceReturn<AuthorizationRequest> {
const {
polymeshService: { polymeshApi },
} = this;

const identity = await this.findOne(did);

const {
options,
args: { targetAccount, expiry },
} = extractTxOptions(rotatePrimaryKeyDto);

const { attestPrimaryKeyRotation } = polymeshApi.identities;

return this.transactionsService.submit(
attestPrimaryKeyRotation,
{ identity, targetAccount, expiry },
options
);
}
}
3 changes: 1 addition & 2 deletions src/test-utils/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { SettlementResultEnum } from '@polymeshassociation/polymesh-sdk/middlewa
import {
Account,
AuthorizationType,
ComplianceManagerTx,
HistoricSettlement,
MetadataEntry,
MetadataType,
Expand Down Expand Up @@ -481,7 +480,7 @@ export function createMockResultSet<T extends any[]>(data: T): ResultSet<T> {
}

export function createMockTxResult(
transactionTag: ComplianceManagerTx
transactionTag: TxTag
): TransactionResult<void> | ServiceReturn<void> | NotificationPayload<EventType> {
const transaction = {
blockHash: '0x1',
Expand Down
2 changes: 2 additions & 0 deletions src/test-utils/service-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ export class MockIdentitiesService {
addSecondaryAccount = jest.fn();
createMockCdd = jest.fn();
registerDid = jest.fn();
rotatePrimaryKey = jest.fn();
attestPrimaryKeyRotation = jest.fn();
}

export class MockSettlementsService {
Expand Down

0 comments on commit 942ad2e

Please sign in to comment.