Skip to content

Commit

Permalink
feat: 🎸 multiSig changes
Browse files Browse the repository at this point in the history
  • Loading branch information
sansan committed Oct 17, 2024
1 parent 3a093f3 commit bc19443
Show file tree
Hide file tree
Showing 5 changed files with 450 additions and 4 deletions.
15 changes: 15 additions & 0 deletions src/multi-sigs/dto/set-multi-sig-admin.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* istanbul ignore file */

import { ApiProperty } from '@nestjs/swagger';

import { IsDid } from '~/common/decorators';
import { TransactionBaseDto } from '~/common/dto/transaction-base-dto';

export class SetMultiSigAdminDto extends TransactionBaseDto {
@ApiProperty({
description: 'The DID of the MultiSig Admin',
example: '0x0600000000000000000000000000000000000000000000000000000000000000',
})
@IsDid()
readonly admin: string;
}
128 changes: 128 additions & 0 deletions src/multi-sigs/multi-sigs.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { NotFoundException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { BigNumber } from '@polymeshassociation/polymesh-sdk';
import {
Identity,
MultiSig,
MultiSigProposal,
MultiSigProposalDetails,
ResultSet,
TxTags,
} from '@polymeshassociation/polymesh-sdk/types';
import { when } from 'jest-when';

import { PaginatedResultsModel } from '~/common/models/paginated-results.model';
import { IdentitySignerModel } from '~/identities/models/identity-signer.model';
import { SetMultiSigAdminDto } from '~/multi-sigs/dto/set-multi-sig-admin.dto';
import { MultiSigProposalModel } from '~/multi-sigs/models/multi-sig-proposal.model';
import { MultiSigProposalDetailsModel } from '~/multi-sigs/models/multi-sig-proposal-details.model';
import { MultiSigsController } from '~/multi-sigs/multi-sigs.controller';
import { MultiSigsService } from '~/multi-sigs/multi-sigs.service';
import { processedTxResult, txResult } from '~/test-utils/consts';
Expand Down Expand Up @@ -126,4 +134,124 @@ describe('MultiSigsController', () => {
expect(result).toEqual(processedTxResult);
});
});

describe('getAdmin', () => {
it('should return the admin identity', async () => {
const mockIdentity = createMock<Identity>({ did: 'ADMIN_DID' });
when(service.getAdmin).calledWith(multiSigAddress).mockResolvedValue(mockIdentity);

const result = await controller.getAdmin({ multiSigAddress });

expect(result).toEqual(new IdentitySignerModel({ did: 'ADMIN_DID' }));
});

it('should throw NotFoundException if no identity is associated', async () => {
when(service.getAdmin).calledWith(multiSigAddress).mockResolvedValue(null);

await expect(controller.getAdmin({ multiSigAddress })).rejects.toThrow(NotFoundException);
});
});

describe('setAdmin', () => {
it('should call the service and return the result', async () => {
const params = {
signers: ['someAddress'],
requiredSignatures: new BigNumber(1),
admin: 'NEW_ADMIN_DID',
};
when(service.setAdmin).calledWith(multiSigAddress, params).mockResolvedValue(txResult);

const result = await controller.setAdmin({ multiSigAddress }, params);

expect(result).toEqual(processedTxResult);
});
});

describe('getHistoricalProposals', () => {
it('should return paginated historical proposals', async () => {
const mockDetails = createMock<MultiSigProposalDetails>({ txTag: TxTags.asset.Issue });

const mockProposal1 = createMock<MultiSigProposal>({
multiSig: mockMultiSig,
details: jest.fn().mockResolvedValue(mockDetails),
id: new BigNumber(1),
});
const mockProposal2 = createMock<MultiSigProposal>({
multiSig: mockMultiSig,
details: jest.fn().mockResolvedValue(mockDetails),
id: new BigNumber(2),
});
const mockPaginatedResult = createMock<ResultSet<MultiSigProposal>>({
data: [mockProposal1, mockProposal2],
next: new BigNumber(2),
count: new BigNumber(2),
});

when(service.getHistoricalProposals)
.calledWith(multiSigAddress)
.mockResolvedValue(mockPaginatedResult);

const result = await controller.getHistoricalProposals({ multiSigAddress });

expect(result).toEqual(
new PaginatedResultsModel({
results: [
new MultiSigProposalModel({
multiSigAddress,
proposalId: mockProposal1.id,
details: new MultiSigProposalDetailsModel(mockDetails),
}),
new MultiSigProposalModel({
multiSigAddress,
proposalId: mockProposal2.id,
details: new MultiSigProposalDetailsModel(mockDetails),
}),
],
total: new BigNumber(2),
next: new BigNumber(2),
})
);
});
});

describe('getProposals', () => {
it('should return active proposals', async () => {
const mockProposal1 = createMock<MultiSigProposal>({
multiSig: mockMultiSig,
id: new BigNumber(1),
});
const mockProposal2 = createMock<MultiSigProposal>({
multiSig: mockMultiSig,
id: new BigNumber(2),
});

when(service.getProposals)
.calledWith(multiSigAddress)
.mockResolvedValue([mockProposal1, mockProposal2]);

const result = await controller.getProposals({ multiSigAddress });

expect(result).toEqual([
expect.objectContaining({ multiSigAddress, proposalId: new BigNumber(1) }),
expect.objectContaining({ multiSigAddress, proposalId: new BigNumber(2) }),
]);
});
});

describe('getPayer', () => {
it('should return the payer identity', async () => {
const mockIdentity = createMock<Identity>({ did: 'PAYER_DID' });
when(service.getPayer).calledWith(multiSigAddress).mockResolvedValue(mockIdentity);

const result = await controller.getPayer({ multiSigAddress });

expect(result).toEqual(new IdentitySignerModel({ did: 'PAYER_DID' }));
});

it('should throw NotFoundException if no identity is associated', async () => {
when(service.getPayer).calledWith(multiSigAddress).mockResolvedValue(null);

await expect(controller.getPayer({ multiSigAddress })).rejects.toThrow(NotFoundException);
});
});
});
151 changes: 149 additions & 2 deletions src/multi-sigs/multi-sigs.controller.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { Body, Controller, Get, HttpStatus, NotFoundException, Param, Post } from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
ApiUnprocessableEntityResponse,
} from '@nestjs/swagger';
import { MultiSig } from '@polymeshassociation/polymesh-sdk/types';

import { ApiTransactionResponse } from '~/common/decorators';
import {
ApiArrayResponse,
ApiTransactionFailedResponse,
ApiTransactionResponse,
} from '~/common/decorators';
import { TransactionBaseDto } from '~/common/dto/transaction-base-dto';
import { PaginatedResultsModel } from '~/common/models/paginated-results.model';
import { TransactionQueueModel } from '~/common/models/transaction-queue.model';
import { handleServiceResult, TransactionResolver, TransactionResponseModel } from '~/common/utils';
import { IdentityModel } from '~/identities/models/identity.model';
import { IdentitySignerModel } from '~/identities/models/identity-signer.model';
import { CreateMultiSigDto } from '~/multi-sigs/dto/create-multi-sig.dto';
import { JoinCreatorDto } from '~/multi-sigs/dto/join-creator.dto';
import { ModifyMultiSigDto } from '~/multi-sigs/dto/modify-multi-sig.dto';
import { MultiSigParamsDto } from '~/multi-sigs/dto/multi-sig-params.dto';
import { MultiSigProposalParamsDto } from '~/multi-sigs/dto/multisig-proposal-params.dto';
import { SetMultiSigAdminDto } from '~/multi-sigs/dto/set-multi-sig-admin.dto';
import { MultiSigCreatedModel } from '~/multi-sigs/models/multi-sig-created.model';
import { MultiSigProposalModel } from '~/multi-sigs/models/multi-sig-proposal.model';
import { MultiSigsService } from '~/multi-sigs/multi-sigs.service';
Expand Down Expand Up @@ -112,6 +121,10 @@ export class MultiSigsController {
summary: 'Get proposal details',
description: 'This endpoint returns details for a multiSig proposal',
})
@ApiOkResponse({
description: 'Details about the proposal',
type: MultiSigProposalModel,
})
@Get(':multiSigAddress/proposals/:proposalId')
async getProposal(@Param() params: MultiSigProposalParamsDto): Promise<unknown> {
const proposal = await this.multiSigService.findProposal(params);
Expand Down Expand Up @@ -177,4 +190,138 @@ export class MultiSigsController {

return handleServiceResult(serviceResult);
}

@ApiOperation({
summary: "Get the Identity of the MultiSig's admin",
})
@ApiOkResponse({
description: 'Returns basic details of the admin Identity for the MultiSig',
type: IdentityModel,
})
@ApiNotFoundResponse({
description: 'MultiSig account not found',
})
@ApiNotFoundResponse({
description: 'No DID is associated with the given account',
})
@Get(':multiSigAddress/admin')
async getAdmin(@Param() { multiSigAddress }: MultiSigParamsDto): Promise<IdentitySignerModel> {
const identity = await this.multiSigService.getAdmin(multiSigAddress);

if (!identity) {
throw new NotFoundException('No DID is associated with the given account');
}
return new IdentitySignerModel({ did: identity.did });
}

@ApiOperation({
summary: "Get the Identity covering transaction fees for the MultiSig's ",
})
@ApiOkResponse({
description: 'Returns basic details of the Identity covering fees for the MultiSig',
type: IdentityModel,
})
@ApiNotFoundResponse({
description: 'MultiSig account not found',
})
@ApiNotFoundResponse({
description: 'No DID is associated with the given account',
})
@Get(':multiSigAddress/payer')
async getPayer(@Param() { multiSigAddress }: MultiSigParamsDto): Promise<IdentitySignerModel> {
const identity = await this.multiSigService.getPayer(multiSigAddress);

if (!identity) {
throw new NotFoundException('No DID is associated with the given account');
}
return new IdentitySignerModel({ did: identity.did });
}

@ApiOperation({
summary: 'Set admin for the MultiSig account',
})
@ApiTransactionResponse({
description: 'Details about the transaction',
type: TransactionQueueModel,
})
@ApiTransactionFailedResponse({
[HttpStatus.NOT_FOUND]: ['MultiSig account not found'],
[HttpStatus.BAD_REQUEST]: ['MultiSig admins are not supported on v6 chains'],
[HttpStatus.UNPROCESSABLE_ENTITY]: [
'The identity is already the admin of the MultiSig',
'The signing account is not part of the MultiSig',
],
})
@Post(':multiSigAddress/admin')
async setAdmin(
@Param() { multiSigAddress }: MultiSigParamsDto,
@Body() body: SetMultiSigAdminDto
): Promise<TransactionResponseModel> {
const serviceResult = await this.multiSigService.setAdmin(multiSigAddress, body);

return handleServiceResult(serviceResult);
}

@ApiOperation({
summary: "Get the active proposals for the MultiSig's account",
})
@ApiArrayResponse(MultiSigProposalModel, {
description: 'Returns a list of active proposals for the MultiSig',
paginated: false,
})
@ApiNotFoundResponse({
description: 'MultiSig account not found',
})
@Get(':multiSigAddress/active-proposals')
async getProposals(
@Param() { multiSigAddress }: MultiSigParamsDto
): Promise<MultiSigProposalModel[]> {
const proposals = await this.multiSigService.getProposals(multiSigAddress);

const detailsPromises = proposals.map(proposal => proposal.details());
const details = await Promise.all(detailsPromises);

return proposals.map((proposal, i) => {
return new MultiSigProposalModel({
multiSigAddress,
proposalId: proposal.id,
details: details[i],
});
});
}

@ApiOperation({
summary: 'Get the historical proposals for the MultiSig account',
})
@ApiNotFoundResponse({
description: 'MultiSig account not found',
})
@ApiArrayResponse(MultiSigProposalModel, {
description: 'List of historical proposals for the MultiSig',
paginated: true,
})
@Get(':multiSigAddress/historical-proposals')
async getHistoricalProposals(
@Param() { multiSigAddress }: MultiSigParamsDto
): Promise<PaginatedResultsModel<MultiSigProposalModel>> {
const { data, next, count } = await this.multiSigService.getHistoricalProposals(
multiSigAddress
);

const detailsPromises = data.map(proposal => proposal.details());
const details = await Promise.all(detailsPromises);

return new PaginatedResultsModel({
results: data.map(
(proposal, i) =>
new MultiSigProposalModel({
multiSigAddress,
proposalId: proposal.id,
details: details[i],
})
),
total: count,
next,
});
}
}
Loading

0 comments on commit bc19443

Please sign in to comment.