Skip to content

Commit e24b94d

Browse files
committed
feat: 🎸 add endpoint to create permission group for asset
1 parent b0d380c commit e24b94d

10 files changed

+339
-5
lines changed

‎src/assets/assets.controller.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest';
55
import { Test, TestingModule } from '@nestjs/testing';
66
import { BigNumber } from '@polymeshassociation/polymesh-sdk';
77
import {
8+
CustomPermissionGroup,
89
Identity,
910
KnownAssetType,
1011
SecurityIdentifierType,
12+
TxGroup,
1113
} from '@polymeshassociation/polymesh-sdk/types';
1214

1315
import { MAX_CONTENT_HASH_LENGTH } from '~/assets/assets.consts';
@@ -468,4 +470,19 @@ describe('AssetsController', () => {
468470
expect(mockAssetsService.unlinkTickerFromAsset).toHaveBeenCalledWith(assetId, { signer });
469471
});
470472
});
473+
474+
describe('createGroup', () => {
475+
it('should call the service and return the results', async () => {
476+
const mockGroup = createMock<CustomPermissionGroup>({ id: 'someId' });
477+
478+
mockAssetsService.createPermissionGroup.mockResolvedValue({ ...txResult, result: mockGroup });
479+
480+
const result = await controller.createGroup(
481+
{ asset: assetId },
482+
{ signer, transactionGroups: [TxGroup.Distribution] }
483+
);
484+
485+
expect(result).toEqual({ ...processedTxResult, id: mockGroup.id });
486+
});
487+
});
471488
});

‎src/assets/assets.controller.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
1+
import { Body, Controller, Get, HttpStatus, Param, Post, Query } from '@nestjs/common';
22
import {
33
ApiBadRequestResponse,
44
ApiGoneResponse,
@@ -10,13 +10,14 @@ import {
1010
ApiTags,
1111
ApiUnprocessableEntityResponse,
1212
} from '@nestjs/swagger';
13-
import { Asset } from '@polymeshassociation/polymesh-sdk/types';
13+
import { Asset, CustomPermissionGroup } from '@polymeshassociation/polymesh-sdk/types';
1414

1515
import { AssetsService } from '~/assets/assets.service';
1616
import { createAssetDetailsModel } from '~/assets/assets.util';
1717
import { AssetParamsDto } from '~/assets/dto/asset-params.dto';
1818
import { ControllerTransferDto } from '~/assets/dto/controller-transfer.dto';
1919
import { CreateAssetDto } from '~/assets/dto/create-asset.dto';
20+
import { CreatePermissionGroupDto } from '~/assets/dto/create-permission-group.dto';
2021
import { IssueDto } from '~/assets/dto/issue.dto';
2122
import { LinkTickerDto } from '~/assets/dto/link-ticker.dto';
2223
import { RedeemTokensDto } from '~/assets/dto/redeem-tokens.dto';
@@ -26,11 +27,16 @@ import { AgentOperationModel } from '~/assets/models/agent-operation.model';
2627
import { AssetDetailsModel } from '~/assets/models/asset-details.model';
2728
import { AssetDocumentModel } from '~/assets/models/asset-document.model';
2829
import { CreatedAssetModel } from '~/assets/models/created-asset.model';
30+
import { CreatedCustomPermissionGroupModel } from '~/assets/models/created-custom-permission-group.model';
2931
import { IdentityBalanceModel } from '~/assets/models/identity-balance.model';
3032
import { RequiredMediatorsModel } from '~/assets/models/required-mediators.model';
3133
import { authorizationRequestResolver } from '~/authorizations/authorizations.util';
3234
import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model';
33-
import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/';
35+
import {
36+
ApiArrayResponse,
37+
ApiTransactionFailedResponse,
38+
ApiTransactionResponse,
39+
} from '~/common/decorators/';
3440
import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto';
3541
import { TransactionBaseDto } from '~/common/dto/transaction-base-dto';
3642
import { TransferOwnershipDto } from '~/common/dto/transfer-ownership.dto';
@@ -627,4 +633,42 @@ export class AssetsController {
627633
const result = await this.assetsService.unlinkTickerFromAsset(asset, params);
628634
return handleServiceResult(result);
629635
}
636+
637+
@ApiOperation({
638+
summary: 'Create a permission group',
639+
description: 'This endpoint allows for the creation of a permission group for an asset',
640+
})
641+
@ApiTransactionResponse({
642+
description: 'Details about the transaction',
643+
type: TransactionQueueModel,
644+
})
645+
@ApiNotFoundResponse({
646+
description: 'The Asset does not exist',
647+
})
648+
@ApiTransactionFailedResponse({
649+
[HttpStatus.BAD_REQUEST]: ['There already exists a group with the exact same permissions'],
650+
[HttpStatus.UNAUTHORIZED]: [
651+
'The signing identity does not have the required permissions to create a permission group',
652+
],
653+
})
654+
@Post(':asset/create-permission-group')
655+
public async createGroup(
656+
@Param() { asset }: AssetParamsDto,
657+
@Body() params: CreatePermissionGroupDto
658+
): Promise<TransactionResponseModel> {
659+
const result = await this.assetsService.createPermissionGroup(asset, params);
660+
661+
const resolver: TransactionResolver<CustomPermissionGroup> = ({
662+
result: group,
663+
transactions,
664+
details,
665+
}) =>
666+
new CreatedCustomPermissionGroupModel({
667+
id: group.id,
668+
transactions,
669+
details,
670+
});
671+
672+
return handleServiceResult(result, resolver);
673+
}
630674
}

‎src/assets/assets.service.spec.ts

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
/* eslint-disable import/first */
22
const mockIsPolymeshTransaction = jest.fn();
33

4+
import { createMock } from '@golevelup/ts-jest';
45
import { Test, TestingModule } from '@nestjs/testing';
56
import { BigNumber } from '@polymeshassociation/polymesh-sdk';
6-
import { AffirmationStatus, KnownAssetType, TxTags } from '@polymeshassociation/polymesh-sdk/types';
7+
import {
8+
AffirmationStatus,
9+
CustomPermissionGroup,
10+
KnownAssetType,
11+
PermissionType,
12+
TxGroup,
13+
TxTags,
14+
} from '@polymeshassociation/polymesh-sdk/types';
715
import { when } from 'jest-when';
816

917
import { MAX_CONTENT_HASH_LENGTH } from '~/assets/assets.consts';
1018
import { AssetsService } from '~/assets/assets.service';
1119
import { AssetDocumentDto } from '~/assets/dto/asset-document.dto';
1220
import { AppNotFoundError } from '~/common/errors';
21+
import { TransactionPermissionsDto } from '~/identities/dto/transaction-permissions.dto';
1322
import { POLYMESH_API } from '~/polymesh/polymesh.consts';
1423
import { PolymeshModule } from '~/polymesh/polymesh.module';
1524
import { PolymeshService } from '~/polymesh/polymesh.service';
@@ -778,4 +787,80 @@ describe('AssetsService', () => {
778787
);
779788
});
780789
});
790+
791+
describe('createPermissionGroup', () => {
792+
describe('createPermissionGroup', () => {
793+
let findSpy: jest.SpyInstance;
794+
let mockAsset: MockAsset;
795+
let mockPermissionGroup: CustomPermissionGroup;
796+
let mockTransaction: MockTransaction;
797+
798+
beforeEach(() => {
799+
findSpy = jest.spyOn(service, 'findOne');
800+
mockAsset = new MockAsset();
801+
mockPermissionGroup = createMock<CustomPermissionGroup>();
802+
const transaction = {
803+
blockHash: '0x1',
804+
txHash: '0x2',
805+
blockNumber: new BigNumber(1),
806+
tag: TxTags.externalAgents.CreateGroup,
807+
};
808+
mockTransaction = new MockTransaction(transaction);
809+
mockTransactionsService.submit.mockResolvedValue({
810+
transactions: [mockTransaction],
811+
result: mockPermissionGroup,
812+
});
813+
814+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
815+
findSpy.mockResolvedValue(mockAsset as any);
816+
});
817+
818+
it('should create a permission group with the given transaction group permissions', async () => {
819+
const result = await service.createPermissionGroup(assetId, {
820+
signer,
821+
transactionGroups: [TxGroup.Distribution],
822+
});
823+
824+
expect(result).toEqual({
825+
result: mockPermissionGroup,
826+
transactions: [mockTransaction],
827+
});
828+
829+
expect(mockTransactionsService.submit).toHaveBeenCalledWith(
830+
mockAsset.permissions.createGroup,
831+
expect.objectContaining({
832+
permissions: {
833+
transactionGroups: [TxGroup.Distribution],
834+
},
835+
}),
836+
expect.objectContaining({ signer })
837+
);
838+
});
839+
840+
it('should create a permission group with the given transaction permissions', async () => {
841+
const transactions = new TransactionPermissionsDto({
842+
values: [TxTags.asset.RegisterUniqueTicker],
843+
type: PermissionType.Include,
844+
exceptions: [TxTags.asset.AcceptTickerTransfer],
845+
});
846+
847+
const result = await service.createPermissionGroup(assetId, { signer, transactions });
848+
849+
expect(result).toEqual({
850+
result: mockPermissionGroup,
851+
transactions: [mockTransaction],
852+
});
853+
854+
expect(mockTransactionsService.submit).toHaveBeenCalledWith(
855+
mockAsset.permissions.createGroup,
856+
expect.objectContaining({
857+
permissions: {
858+
transactions,
859+
},
860+
}),
861+
expect.objectContaining({ signer })
862+
);
863+
});
864+
});
865+
});
781866
});

‎src/assets/assets.service.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@ import {
44
Asset,
55
AssetDocument,
66
AuthorizationRequest,
7+
CreateGroupParams,
8+
CustomPermissionGroup,
79
FungibleAsset,
810
HistoricAgentOperation,
911
Identity,
1012
IdentityBalance,
1113
NftCollection,
1214
ResultSet,
15+
TransactionPermissions,
1316
} from '@polymeshassociation/polymesh-sdk/types';
1417

1518
import { ControllerTransferDto } from '~/assets/dto/controller-transfer.dto';
1619
import { CreateAssetDto } from '~/assets/dto/create-asset.dto';
20+
import { CreatePermissionGroupDto } from '~/assets/dto/create-permission-group.dto';
1721
import { IssueDto } from '~/assets/dto/issue.dto';
1822
import { LinkTickerDto } from '~/assets/dto/link-ticker.dto';
1923
import { RedeemTokensDto } from '~/assets/dto/redeem-tokens.dto';
@@ -252,4 +256,39 @@ export class AssetsService {
252256
const { unlinkTicker } = await this.findOne(assetInput);
253257
return this.transactionsService.submit(unlinkTicker, {}, options);
254258
}
259+
260+
public async createPermissionGroup(
261+
assetId: string,
262+
params: CreatePermissionGroupDto
263+
): ServiceReturn<CustomPermissionGroup> {
264+
const { options, args } = extractTxOptions(params);
265+
266+
const {
267+
permissions: { createGroup },
268+
} = await this.findOne(assetId);
269+
270+
const toCreateGroupParams = (
271+
input: CreatePermissionGroupDto
272+
): CreateGroupParams['permissions'] => {
273+
const { transactions, transactionGroups } = input;
274+
275+
let permissions = {} as CreateGroupParams['permissions'];
276+
277+
if (transactions) {
278+
permissions = {
279+
transactions: transactions.toTransactionPermissions() as TransactionPermissions,
280+
};
281+
} else if (transactionGroups) {
282+
permissions = { transactionGroups };
283+
}
284+
285+
return permissions;
286+
};
287+
288+
return this.transactionsService.submit(
289+
createGroup,
290+
{ permissions: toCreateGroupParams(args) },
291+
options
292+
);
293+
}
255294
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* istanbul ignore file */
2+
3+
import { ApiPropertyOptional } from '@nestjs/swagger';
4+
import { TxGroup } from '@polymeshassociation/polymesh-sdk/types';
5+
import { Type } from 'class-transformer';
6+
import { IsArray, IsEnum, ValidateNested } from 'class-validator';
7+
8+
import { IncompatibleWith } from '~/common/decorators';
9+
import { TransactionBaseDto } from '~/common/dto/transaction-base-dto';
10+
import { TransactionPermissionsDto } from '~/identities/dto/transaction-permissions.dto';
11+
12+
export class CreatePermissionGroupDto extends TransactionBaseDto {
13+
@ApiPropertyOptional({
14+
description:
15+
'Transactions that the `external agent` has permission to execute. This value should not be passed along with the `transactionGroups`.',
16+
type: TransactionPermissionsDto,
17+
nullable: true,
18+
})
19+
@ValidateNested()
20+
@IncompatibleWith(['transactionGroups'], {
21+
message: 'Cannot specify both transactions and transactionGroups',
22+
})
23+
@Type(() => TransactionPermissionsDto)
24+
readonly transactions?: TransactionPermissionsDto;
25+
26+
@ApiPropertyOptional({
27+
description:
28+
'Transaction Groups that `external agent` has permission to execute. This value should not be passed along with the `transactions`.',
29+
isArray: true,
30+
enum: TxGroup,
31+
example: [TxGroup.Distribution],
32+
})
33+
@IncompatibleWith(['transactions'], {
34+
message: 'Cannot specify both transactions and transactionGroups',
35+
})
36+
@IsArray()
37+
@IsEnum(TxGroup, { each: true })
38+
readonly transactionGroups?: TxGroup[];
39+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/* istanbul ignore file */
2+
3+
import { ApiPropertyOptional } from '@nestjs/swagger';
4+
import { BigNumber } from 'bignumber.js';
5+
6+
import { TransactionQueueModel } from '~/common/models/transaction-queue.model';
7+
8+
export class CreatedCustomPermissionGroupModel extends TransactionQueueModel {
9+
@ApiPropertyOptional({
10+
description: 'The newly created ID',
11+
example: '1',
12+
})
13+
readonly id: BigNumber;
14+
15+
constructor(model: CreatedCustomPermissionGroupModel) {
16+
const { transactions, details, ...rest } = model;
17+
super({ transactions, details });
18+
19+
Object.assign(this, rest);
20+
}
21+
}

‎src/common/decorators/swagger.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ApiOkResponse,
99
ApiProperty,
1010
ApiPropertyOptions,
11+
ApiUnauthorizedResponse,
1112
ApiUnprocessableEntityResponse,
1213
getSchemaPath,
1314
OmitType,
@@ -178,7 +179,8 @@ export const ApiPropertyOneOf = ({
178179
type SupportedHttpStatusCodes =
179180
| HttpStatus.NOT_FOUND
180181
| HttpStatus.BAD_REQUEST
181-
| HttpStatus.UNPROCESSABLE_ENTITY;
182+
| HttpStatus.UNPROCESSABLE_ENTITY
183+
| HttpStatus.UNAUTHORIZED;
182184

183185
/**
184186
* A helper that combines responses for SDK Errors like `BadRequestException`, `NotFoundException`, `UnprocessableEntityException`
@@ -203,6 +205,9 @@ export function ApiTransactionFailedResponse(
203205
case HttpStatus.BAD_REQUEST:
204206
decorators.push(ApiBadRequestResponse({ description }));
205207
break;
208+
case HttpStatus.UNAUTHORIZED:
209+
decorators.push(ApiUnauthorizedResponse({ description }));
210+
break;
206211
case HttpStatus.UNPROCESSABLE_ENTITY:
207212
decorators.push(ApiUnprocessableEntityResponse({ description }));
208213
break;

0 commit comments

Comments
 (0)