Skip to content

Commit

Permalink
feat: 🎸 add endpoints to manage multi sig accounts
Browse files Browse the repository at this point in the history
add endpoints to create and modify multiSigs, get proposal details and
approve/reject them
  • Loading branch information
polymath-eric committed Aug 13, 2024
1 parent aac249b commit 07d38f5
Show file tree
Hide file tree
Showing 24 changed files with 931 additions and 11 deletions.
Empty file added AccountsService
Empty file.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"@polymeshassociation/fireblocks-signing-manager": "^2.5.0",
"@polymeshassociation/hashicorp-vault-signing-manager": "^3.4.0",
"@polymeshassociation/local-signing-manager": "^3.3.0",
"@polymeshassociation/polymesh-sdk": "24.7.0-alpha.11",
"@polymeshassociation/polymesh-sdk": "24.7.0-alpha.13",
"@polymeshassociation/signing-manager-types": "^3.2.0",
"class-transformer": "0.5.1",
"class-validator": "^0.14.0",
Expand Down
26 changes: 23 additions & 3 deletions src/accounts/accounts.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,13 @@ describe('AccountsController', () => {
});

describe('getDetails', () => {
it('should call the service and return AccountDetailsModel', async () => {
const fakeIdentityModel = 'fakeIdentityModel' as unknown as IdentityModel;
const fakeIdentityModel = 'fakeIdentityModel' as unknown as IdentityModel;

beforeEach(() => {
jest.spyOn(identityUtil, 'createIdentityModel').mockResolvedValue(fakeIdentityModel);
});

it('should call the service and return AccountDetailsModel', async () => {
const mockResponse: AccountDetails = {
identity: new MockIdentity() as unknown as Identity,
multiSigDetails: null,
Expand All @@ -311,10 +314,27 @@ describe('AccountsController', () => {

expect(result).toEqual({ identity: fakeIdentityModel });
});

it('should handle MultiSig details', async () => {
const mockResponse: AccountDetails = {
identity: new MockIdentity() as unknown as Identity,
multiSigDetails: { signers: [], requiredSignatures: new BigNumber(1) },
};

mockAccountsService.getDetails.mockReturnValue(mockResponse);

const result = await controller.getAccountDetails({ account: '5xdd' });

expect(result).toEqual(
expect.objectContaining({
multiSig: expect.objectContaining({ signers: [], requiredSignatures: new BigNumber(1) }),
})
);
});
});

describe('getOffChainReceipts', () => {
it('should call the service and return AccountDetailsModel', async () => {
it('should call the service and return off chain receipts', async () => {
const mockResponse = [new BigNumber(1), new BigNumber(2)];
mockAccountsService.fetchOffChainReceipts.mockReturnValue(mockResponse);

Expand Down
1 change: 1 addition & 0 deletions src/accounts/accounts.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ export class AccountsController {
@ApiNotFoundResponse({
description: 'No Account found for the given address',
})
@ApiTags('multi-sigs')
@Get(':account')
async getAccountDetails(@Param() { account }: AccountParamsDto): Promise<AccountDetailsModel> {
const { identity, multiSigDetails } = await this.accountsService.getDetails(account);
Expand Down
2 changes: 1 addition & 1 deletion src/accounts/accounts.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class AccountsService {
private readonly transactionsService: TransactionsService
) {}

public async findOne(address: string): Promise<Account> {
public async findOne(address: string): Promise<Account | MultiSig> {
const {
polymeshService: { polymeshApi },
} = this;
Expand Down
2 changes: 1 addition & 1 deletion src/accounts/models/multi-sig-details.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class MultiSigDetailsModel {
readonly signers: SignerModel[];

@ApiProperty({
description: 'Required signers',
description: 'The required number of signers needed to approve a proposal',
type: 'string',
example: '2',
})
Expand Down
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { DeveloperTestingModule } from '~/developer-testing/developer-testing.mo
import { EventsModule } from '~/events/events.module';
import { IdentitiesModule } from '~/identities/identities.module';
import { MetadataModule } from '~/metadata/metadata.module';
import { MultiSigsModule } from '~/multi-sigs/multi-sigs.module';
import { NetworkModule } from '~/network/network.module';
import { NftsModule } from '~/nfts/nfts.module';
import { NotificationsModule } from '~/notifications/notifications.module';
Expand Down Expand Up @@ -105,6 +106,7 @@ import { UsersModule } from '~/users/users.module';
OfflineSubmitterModule,
OfflineStarterModule,
OfflineRecorderModule,
MultiSigsModule,
],
})
export class AppModule {}
27 changes: 27 additions & 0 deletions src/multi-sigs/dto/create-multi-sig.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* istanbul ignore file */

import { ApiProperty } from '@nestjs/swagger';
import { BigNumber } from '@polymeshassociation/polymesh-sdk';
import { IsString } from 'class-validator';

import { IsBigNumber, ToBigNumber } from '~/common/decorators';
import { TransactionBaseDto } from '~/common/dto/transaction-base-dto';

export class CreateMultiSigDto extends TransactionBaseDto {
@ApiProperty({
description: 'The number of approvals required in order for a proposal to be accepted',
example: '1',
})
@IsBigNumber()
@ToBigNumber()
readonly requiredSignatures: BigNumber;

@ApiProperty({
description: 'The signers for the MultiSig',
type: 'string',
isArray: true,
example: ['5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV'],
})
@IsString({ each: true })
readonly signers: string[];
}
29 changes: 29 additions & 0 deletions src/multi-sigs/dto/join-creator.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* istanbul ignore file */

import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsBoolean, IsOptional, ValidateNested } from 'class-validator';

import { TransactionBaseDto } from '~/common/dto/transaction-base-dto';
import { IsPermissionsLike } from '~/identities/decorators/validation';
import { PermissionsLikeDto } from '~/identities/dto/permissions-like.dto';

export class JoinCreatorDto extends TransactionBaseDto {
@ApiPropertyOptional({
description: 'Whether or not to join the creator as the new primary key',
type: 'boolean',
})
@IsOptional()
@IsBoolean()
readonly asPrimary?: boolean;

@ApiPropertyOptional({
description: 'Permissions to be granted to the multiSig if joining as a `secondaryAccount`',
type: PermissionsLikeDto,
})
@IsOptional()
@ValidateNested()
@Type(() => PermissionsLikeDto)
@IsPermissionsLike()
readonly permissions?: PermissionsLikeDto;
}
28 changes: 28 additions & 0 deletions src/multi-sigs/dto/modify-multi-sig.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* istanbul ignore file */

import { ApiProperty } from '@nestjs/swagger';
import { BigNumber } from '@polymeshassociation/polymesh-sdk';
import { IsString } from 'class-validator';

import { IsBigNumber, ToBigNumber } from '~/common/decorators';
import { TransactionBaseDto } from '~/common/dto/transaction-base-dto';

export class ModifyMultiSigDto extends TransactionBaseDto {
@ApiProperty({
description: 'The number of approvals required in order for a proposal to be accepted',
example: '2',
type: 'string',
})
@IsBigNumber()
@ToBigNumber()
readonly requiredSignatures: BigNumber;

@ApiProperty({
description: 'The signers for the MultiSig',
type: 'string',
isArray: true,
example: ['5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV'],
})
@IsString({ each: true })
readonly signers: string[];
}
14 changes: 14 additions & 0 deletions src/multi-sigs/dto/multi-sig-params.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* istanbul ignore file */

import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';

export class MultiSigParamsDto {
@ApiProperty({
description: 'The address of the MultiSig',
example: '5HCKs1tNprs5S1pHHmsHXaQacSQbYDhLUCyoMZiM7KT8JkNb',
type: 'string',
})
@IsString()
readonly multiSigAddress: string;
}
26 changes: 26 additions & 0 deletions src/multi-sigs/dto/multisig-proposal-params.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* istanbul ignore file */

import { ApiProperty } from '@nestjs/swagger';
import { BigNumber } from '@polymeshassociation/polymesh-sdk';
import { IsString } from 'class-validator';

import { IsBigNumber, ToBigNumber } from '~/common/decorators';

export class MultiSigProposalParamsDto {
@ApiProperty({
description: 'The MultiSig address',
type: 'string',
example: '5HCKs1tNprs5S1pHHmsHXaQacSQbYDhLUCyoMZiM7KT8JkNb',
})
@IsString()
readonly multiSigAddress: string;

@ApiProperty({
description: 'The proposal ID',
type: 'string',
example: '7',
})
@IsBigNumber()
@ToBigNumber()
readonly proposalId: BigNumber;
}
22 changes: 22 additions & 0 deletions src/multi-sigs/models/multi-sig-created.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* istanbul ignore file */

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

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

export class MultiSigCreatedModel extends TransactionQueueModel {
@ApiProperty({
description: 'The address of the multiSig',
type: 'string',
example: '5HCKs1tNprs5S1pHHmsHXaQacSQbYDhLUCyoMZiM7KT8JkNb',
})
readonly multiSigAddress: string;

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

super({ transactions, details });

Object.assign(this, rest);
}
}
87 changes: 87 additions & 0 deletions src/multi-sigs/models/multi-sig-proposal-details.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ApiProperty } from '@nestjs/swagger';
import { BigNumber } from '@polymeshassociation/polymesh-sdk';
import {
Account,
AnyJson,
ProposalStatus,
TxTag,
TxTags,
} from '@polymeshassociation/polymesh-sdk/types';

import { FromBigNumber, FromEntityObject } from '~/common/decorators';
import { getTxTags } from '~/common/utils';

export class MultiSigProposalDetailsModel {
@ApiProperty({
description: 'The number of approvals this proposal has received',
type: 'string',
example: '1',
})
@FromBigNumber()
approvalAmount: BigNumber;

@ApiProperty({
description: 'The number of rejections this proposal has received',
type: 'string',
example: '0',
})
@FromBigNumber()
rejectionAmount: BigNumber;

@ApiProperty({
description: 'The current status of the proposal',
enum: ProposalStatus,
type: 'string',
example: ProposalStatus.Active,
})
readonly status: string;

@ApiProperty({
description:
"An optional time in which this proposal will expire if a decision isn't reached by then",
example: null,
})
readonly expiry: Date | null;

@ApiProperty({
description:
'Determines if the proposal will automatically be closed once a threshold of reject votes has been reached',
type: 'boolean',
example: true,
})
readonly autoClose: boolean;

@ApiProperty({
description: 'The tag for the transaction being proposed for the MultiSig to execute',
type: 'string',
enum: getTxTags(),
example: TxTags.asset.Issue,
})
readonly txTag: TxTag;

@ApiProperty({
description: 'The arguments to be passed to the transaction for this proposal',
type: 'string',
example: {
ticker: '0x5449434b4552000000000000',
amount: 1000000000,
portfolio_kind: {
default: null,
},
},
})
readonly args: AnyJson;

@ApiProperty({
description: 'Accounts of signing keys that have already voted on this proposal',
isArray: true,
type: 'string',
example: ['5EyGPbr94Hw2r5kYR4eW21U9CuNwW87pk2bpkR5WGE2STK2r'],
})
@FromEntityObject()
voted: Account[];

constructor(model: MultiSigProposalDetailsModel) {
Object.assign(this, model);
}
}
51 changes: 51 additions & 0 deletions src/multi-sigs/models/multi-sig-proposal.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* istanbul ignore file */

import { ApiProperty } from '@nestjs/swagger';
import { BigNumber } from '@polymeshassociation/polymesh-sdk';

import { FromBigNumber } from '~/common/decorators';
import { MultiSigProposalDetailsModel } from '~/multi-sigs/models/multi-sig-proposal-details.model';

export class MultiSigProposalModel {
@ApiProperty({
description: 'The multiSig for which the proposal if for',
type: 'string',
example: '5EjsqfmY4JqMSrt7YQCe3if5DK4FrG98uUwZsaXmNW7aKdNM',
})
readonly multiSigAddress: string;

@ApiProperty({
description: 'The ID of the proposal',
example: '1',
})
@FromBigNumber()
readonly proposalId: BigNumber;

@ApiProperty({
description: 'Proposal details',
example: {
approvalAmount: '1',
rejectionAmount: '0',
status: 'Active',
expiry: null,
autoClose: true,
args: {
ticker: '0x5449434b4552000000000000',
amount: 1000000000,
portfolio_kind: {
default: null,
},
},
txTag: 'asset.issue',
voted: ['5EyGPbr94Hw2r5kYR4eW21U9CuNwW87pk2bpkR5WGE2STK2r'],
},
})
readonly details: MultiSigProposalDetailsModel;

constructor(model: MultiSigProposalModel) {
const { details: rawDetails, ...rest } = model;
const details = new MultiSigProposalDetailsModel(rawDetails);

Object.assign(this, { ...rest, details });
}
}
Loading

0 comments on commit 07d38f5

Please sign in to comment.