Skip to content

Commit

Permalink
feat: 🎸 add endpoint to create permission group for asset
Browse files Browse the repository at this point in the history
  • Loading branch information
sansan committed Nov 27, 2024
1 parent b0d380c commit e24b94d
Show file tree
Hide file tree
Showing 10 changed files with 339 additions and 5 deletions.
17 changes: 17 additions & 0 deletions src/assets/assets.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { BigNumber } from '@polymeshassociation/polymesh-sdk';
import {
CustomPermissionGroup,
Identity,
KnownAssetType,
SecurityIdentifierType,
TxGroup,
} from '@polymeshassociation/polymesh-sdk/types';

import { MAX_CONTENT_HASH_LENGTH } from '~/assets/assets.consts';
Expand Down Expand Up @@ -468,4 +470,19 @@ describe('AssetsController', () => {
expect(mockAssetsService.unlinkTickerFromAsset).toHaveBeenCalledWith(assetId, { signer });
});
});

describe('createGroup', () => {
it('should call the service and return the results', async () => {
const mockGroup = createMock<CustomPermissionGroup>({ id: 'someId' });

mockAssetsService.createPermissionGroup.mockResolvedValue({ ...txResult, result: mockGroup });

const result = await controller.createGroup(
{ asset: assetId },
{ signer, transactionGroups: [TxGroup.Distribution] }
);

expect(result).toEqual({ ...processedTxResult, id: mockGroup.id });
});
});
});
50 changes: 47 additions & 3 deletions src/assets/assets.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { Body, Controller, Get, HttpStatus, Param, Post, Query } from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiGoneResponse,
Expand All @@ -10,13 +10,14 @@ import {
ApiTags,
ApiUnprocessableEntityResponse,
} from '@nestjs/swagger';
import { Asset } from '@polymeshassociation/polymesh-sdk/types';
import { Asset, CustomPermissionGroup } from '@polymeshassociation/polymesh-sdk/types';

import { AssetsService } from '~/assets/assets.service';
import { createAssetDetailsModel } from '~/assets/assets.util';
import { AssetParamsDto } from '~/assets/dto/asset-params.dto';
import { ControllerTransferDto } from '~/assets/dto/controller-transfer.dto';
import { CreateAssetDto } from '~/assets/dto/create-asset.dto';
import { CreatePermissionGroupDto } from '~/assets/dto/create-permission-group.dto';
import { IssueDto } from '~/assets/dto/issue.dto';
import { LinkTickerDto } from '~/assets/dto/link-ticker.dto';
import { RedeemTokensDto } from '~/assets/dto/redeem-tokens.dto';
Expand All @@ -26,11 +27,16 @@ import { AgentOperationModel } from '~/assets/models/agent-operation.model';
import { AssetDetailsModel } from '~/assets/models/asset-details.model';
import { AssetDocumentModel } from '~/assets/models/asset-document.model';
import { CreatedAssetModel } from '~/assets/models/created-asset.model';
import { CreatedCustomPermissionGroupModel } from '~/assets/models/created-custom-permission-group.model';
import { IdentityBalanceModel } from '~/assets/models/identity-balance.model';
import { RequiredMediatorsModel } from '~/assets/models/required-mediators.model';
import { authorizationRequestResolver } from '~/authorizations/authorizations.util';
import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model';
import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/';
import {
ApiArrayResponse,
ApiTransactionFailedResponse,
ApiTransactionResponse,
} from '~/common/decorators/';
import { PaginatedParamsDto } from '~/common/dto/paginated-params.dto';
import { TransactionBaseDto } from '~/common/dto/transaction-base-dto';
import { TransferOwnershipDto } from '~/common/dto/transfer-ownership.dto';
Expand Down Expand Up @@ -627,4 +633,42 @@ export class AssetsController {
const result = await this.assetsService.unlinkTickerFromAsset(asset, params);
return handleServiceResult(result);
}

@ApiOperation({
summary: 'Create a permission group',
description: 'This endpoint allows for the creation of a permission group for an asset',
})
@ApiTransactionResponse({
description: 'Details about the transaction',
type: TransactionQueueModel,
})
@ApiNotFoundResponse({
description: 'The Asset does not exist',
})
@ApiTransactionFailedResponse({
[HttpStatus.BAD_REQUEST]: ['There already exists a group with the exact same permissions'],
[HttpStatus.UNAUTHORIZED]: [
'The signing identity does not have the required permissions to create a permission group',
],
})
@Post(':asset/create-permission-group')
public async createGroup(
@Param() { asset }: AssetParamsDto,
@Body() params: CreatePermissionGroupDto
): Promise<TransactionResponseModel> {
const result = await this.assetsService.createPermissionGroup(asset, params);

const resolver: TransactionResolver<CustomPermissionGroup> = ({
result: group,
transactions,
details,
}) =>
new CreatedCustomPermissionGroupModel({
id: group.id,
transactions,
details,
});

return handleServiceResult(result, resolver);
}
}
87 changes: 86 additions & 1 deletion src/assets/assets.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
/* eslint-disable import/first */
const mockIsPolymeshTransaction = jest.fn();

import { createMock } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { BigNumber } from '@polymeshassociation/polymesh-sdk';
import { AffirmationStatus, KnownAssetType, TxTags } from '@polymeshassociation/polymesh-sdk/types';
import {
AffirmationStatus,
CustomPermissionGroup,
KnownAssetType,
PermissionType,
TxGroup,
TxTags,
} from '@polymeshassociation/polymesh-sdk/types';
import { when } from 'jest-when';

import { MAX_CONTENT_HASH_LENGTH } from '~/assets/assets.consts';
import { AssetsService } from '~/assets/assets.service';
import { AssetDocumentDto } from '~/assets/dto/asset-document.dto';
import { AppNotFoundError } from '~/common/errors';
import { TransactionPermissionsDto } from '~/identities/dto/transaction-permissions.dto';
import { POLYMESH_API } from '~/polymesh/polymesh.consts';
import { PolymeshModule } from '~/polymesh/polymesh.module';
import { PolymeshService } from '~/polymesh/polymesh.service';
Expand Down Expand Up @@ -778,4 +787,80 @@ describe('AssetsService', () => {
);
});
});

describe('createPermissionGroup', () => {
describe('createPermissionGroup', () => {
let findSpy: jest.SpyInstance;
let mockAsset: MockAsset;
let mockPermissionGroup: CustomPermissionGroup;
let mockTransaction: MockTransaction;

beforeEach(() => {
findSpy = jest.spyOn(service, 'findOne');
mockAsset = new MockAsset();
mockPermissionGroup = createMock<CustomPermissionGroup>();
const transaction = {
blockHash: '0x1',
txHash: '0x2',
blockNumber: new BigNumber(1),
tag: TxTags.externalAgents.CreateGroup,
};
mockTransaction = new MockTransaction(transaction);
mockTransactionsService.submit.mockResolvedValue({
transactions: [mockTransaction],
result: mockPermissionGroup,
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
findSpy.mockResolvedValue(mockAsset as any);
});

it('should create a permission group with the given transaction group permissions', async () => {
const result = await service.createPermissionGroup(assetId, {
signer,
transactionGroups: [TxGroup.Distribution],
});

expect(result).toEqual({
result: mockPermissionGroup,
transactions: [mockTransaction],
});

expect(mockTransactionsService.submit).toHaveBeenCalledWith(
mockAsset.permissions.createGroup,
expect.objectContaining({
permissions: {
transactionGroups: [TxGroup.Distribution],
},
}),
expect.objectContaining({ signer })
);
});

it('should create a permission group with the given transaction permissions', async () => {
const transactions = new TransactionPermissionsDto({
values: [TxTags.asset.RegisterUniqueTicker],
type: PermissionType.Include,
exceptions: [TxTags.asset.AcceptTickerTransfer],
});

const result = await service.createPermissionGroup(assetId, { signer, transactions });

expect(result).toEqual({
result: mockPermissionGroup,
transactions: [mockTransaction],
});

expect(mockTransactionsService.submit).toHaveBeenCalledWith(
mockAsset.permissions.createGroup,
expect.objectContaining({
permissions: {
transactions,
},
}),
expect.objectContaining({ signer })
);
});
});
});
});
39 changes: 39 additions & 0 deletions src/assets/assets.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ import {
Asset,
AssetDocument,
AuthorizationRequest,
CreateGroupParams,
CustomPermissionGroup,
FungibleAsset,
HistoricAgentOperation,
Identity,
IdentityBalance,
NftCollection,
ResultSet,
TransactionPermissions,
} from '@polymeshassociation/polymesh-sdk/types';

import { ControllerTransferDto } from '~/assets/dto/controller-transfer.dto';
import { CreateAssetDto } from '~/assets/dto/create-asset.dto';
import { CreatePermissionGroupDto } from '~/assets/dto/create-permission-group.dto';
import { IssueDto } from '~/assets/dto/issue.dto';
import { LinkTickerDto } from '~/assets/dto/link-ticker.dto';
import { RedeemTokensDto } from '~/assets/dto/redeem-tokens.dto';
Expand Down Expand Up @@ -252,4 +256,39 @@ export class AssetsService {
const { unlinkTicker } = await this.findOne(assetInput);
return this.transactionsService.submit(unlinkTicker, {}, options);
}

public async createPermissionGroup(
assetId: string,
params: CreatePermissionGroupDto
): ServiceReturn<CustomPermissionGroup> {
const { options, args } = extractTxOptions(params);

const {
permissions: { createGroup },
} = await this.findOne(assetId);

const toCreateGroupParams = (
input: CreatePermissionGroupDto
): CreateGroupParams['permissions'] => {
const { transactions, transactionGroups } = input;

let permissions = {} as CreateGroupParams['permissions'];

if (transactions) {
permissions = {
transactions: transactions.toTransactionPermissions() as TransactionPermissions,
};
} else if (transactionGroups) {
permissions = { transactionGroups };
}

return permissions;
};

return this.transactionsService.submit(
createGroup,
{ permissions: toCreateGroupParams(args) },
options
);
}
}
39 changes: 39 additions & 0 deletions src/assets/dto/create-permission-group.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* istanbul ignore file */

import { ApiPropertyOptional } from '@nestjs/swagger';
import { TxGroup } from '@polymeshassociation/polymesh-sdk/types';
import { Type } from 'class-transformer';
import { IsArray, IsEnum, ValidateNested } from 'class-validator';

import { IncompatibleWith } from '~/common/decorators';
import { TransactionBaseDto } from '~/common/dto/transaction-base-dto';
import { TransactionPermissionsDto } from '~/identities/dto/transaction-permissions.dto';

export class CreatePermissionGroupDto extends TransactionBaseDto {
@ApiPropertyOptional({
description:
'Transactions that the `external agent` has permission to execute. This value should not be passed along with the `transactionGroups`.',
type: TransactionPermissionsDto,
nullable: true,
})
@ValidateNested()
@IncompatibleWith(['transactionGroups'], {
message: 'Cannot specify both transactions and transactionGroups',
})
@Type(() => TransactionPermissionsDto)
readonly transactions?: TransactionPermissionsDto;

@ApiPropertyOptional({
description:
'Transaction Groups that `external agent` has permission to execute. This value should not be passed along with the `transactions`.',
isArray: true,
enum: TxGroup,
example: [TxGroup.Distribution],
})
@IncompatibleWith(['transactions'], {
message: 'Cannot specify both transactions and transactionGroups',
})
@IsArray()
@IsEnum(TxGroup, { each: true })
readonly transactionGroups?: TxGroup[];
}
21 changes: 21 additions & 0 deletions src/assets/models/created-custom-permission-group.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* istanbul ignore file */

import { ApiPropertyOptional } from '@nestjs/swagger';
import { BigNumber } from 'bignumber.js';

import { TransactionQueueModel } from '~/common/models/transaction-queue.model';

export class CreatedCustomPermissionGroupModel extends TransactionQueueModel {
@ApiPropertyOptional({
description: 'The newly created ID',
example: '1',
})
readonly id: BigNumber;

constructor(model: CreatedCustomPermissionGroupModel) {
const { transactions, details, ...rest } = model;
super({ transactions, details });

Object.assign(this, rest);
}
}
7 changes: 6 additions & 1 deletion src/common/decorators/swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ApiOkResponse,
ApiProperty,
ApiPropertyOptions,
ApiUnauthorizedResponse,
ApiUnprocessableEntityResponse,
getSchemaPath,
OmitType,
Expand Down Expand Up @@ -178,7 +179,8 @@ export const ApiPropertyOneOf = ({
type SupportedHttpStatusCodes =
| HttpStatus.NOT_FOUND
| HttpStatus.BAD_REQUEST
| HttpStatus.UNPROCESSABLE_ENTITY;
| HttpStatus.UNPROCESSABLE_ENTITY
| HttpStatus.UNAUTHORIZED;

/**
* A helper that combines responses for SDK Errors like `BadRequestException`, `NotFoundException`, `UnprocessableEntityException`
Expand All @@ -203,6 +205,9 @@ export function ApiTransactionFailedResponse(
case HttpStatus.BAD_REQUEST:
decorators.push(ApiBadRequestResponse({ description }));
break;
case HttpStatus.UNAUTHORIZED:
decorators.push(ApiUnauthorizedResponse({ description }));
break;
case HttpStatus.UNPROCESSABLE_ENTITY:
decorators.push(ApiUnprocessableEntityResponse({ description }));
break;
Expand Down
Loading

0 comments on commit e24b94d

Please sign in to comment.