Skip to content

Commit 6f68bd7

Browse files
committed
feat: 🎸 add local signing key endpoint
add POST /signer with a local signing manager to load keys after start
1 parent 74d8839 commit 6f68bd7

9 files changed

+102
-6
lines changed

‎docker-compose.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ services:
2222
start_period: 10s
2323

2424
subquery:
25-
platform: 'linux/amd64'
2625
image: '${SUBQUERY_IMAGE}'
2726
init: true
2827
restart: unless-stopped

‎src/metadata/metadata.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ export class MetadataController {
250250
@ApiParam({
251251
name: 'type',
252252
description: 'The type of Asset Metadata',
253-
enum: MetadataType.Local,
253+
enum: MetadataType,
254254
example: MetadataType.Local,
255255
})
256256
@ApiParam({
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNotEmpty, IsString } from 'class-validator';
3+
4+
export class AddLocalSignerDto {
5+
@ApiProperty({
6+
description: 'The value used to reference the key when signing',
7+
example: 'alice',
8+
})
9+
@IsString()
10+
@IsNotEmpty()
11+
handle: string;
12+
13+
@ApiProperty({
14+
description: 'The 12 word mnemonic for the signer',
15+
example: 'clap reveal pledge miss useful motion pair goat book snow scrub bag',
16+
})
17+
@IsString()
18+
@IsNotEmpty()
19+
mnemonic: string;
20+
}

‎src/signing/services/local-signing.service.spec.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { LocalSigningManager } from '@polymeshassociation/local-signing-manager'
44
import { PolkadotSigner } from '@polymeshassociation/signing-manager-types';
55
import { when } from 'jest-when';
66

7-
import { AppNotFoundError } from '~/common/errors';
7+
import { AppConflictError, AppNotFoundError } from '~/common/errors';
88
import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger';
99
import { PolymeshLogger } from '~/logger/polymesh-logger.service';
1010
import { POLYMESH_API } from '~/polymesh/polymesh.consts';
@@ -102,4 +102,25 @@ describe('LocalSigningService', () => {
102102
expect(result).toEqual(signature);
103103
});
104104
});
105+
106+
describe('addSigner', () => {
107+
it('should add a new signer and return its address', async () => {
108+
const handle = 'newSigner';
109+
const mnemonic = '//Alice';
110+
const expectedAddress = '15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5';
111+
112+
const result = await service.addSigner(handle, mnemonic);
113+
114+
expect(result).toBe(expectedAddress);
115+
});
116+
117+
it('should throw AppConflictError when adding a signer with existing handle', async () => {
118+
const handle = 'existingSigner';
119+
const mnemonic = '//Alice';
120+
121+
await service.addSigner(handle, mnemonic);
122+
123+
await expect(service.addSigner(handle, mnemonic)).rejects.toThrow(AppConflictError);
124+
});
125+
});
105126
});

‎src/signing/services/local-signing.service.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { LocalSigningManager } from '@polymeshassociation/local-signing-manager';
22
import { forEach } from 'lodash';
33

4+
import { AppConflictError } from '~/common/errors';
45
import { PolymeshLogger } from '~/logger/polymesh-logger.service';
56
import { PolymeshService } from '~/polymesh/polymesh.service';
67
import { SigningService } from '~/signing/services/signing.service';
@@ -44,4 +45,17 @@ export class LocalSigningService extends SigningService {
4445
private logKey(handle: string, address: string): void {
4546
this.logger.log(`Key "${handle}" with address "${address}" was loaded`);
4647
}
48+
49+
public async addSigner(handle: string, mnemonic: string): Promise<string> {
50+
const existingAddress = this.addressBook[handle];
51+
if (existingAddress) {
52+
throw new AppConflictError(existingAddress, handle);
53+
}
54+
55+
const address = this.signingManager.addAccount({ mnemonic });
56+
this.setAddressByHandle(handle, address);
57+
this.logKey(handle, address);
58+
59+
return address;
60+
}
4761
}

‎src/signing/services/signing.service.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
22
import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types';
33
import { SigningManager } from '@polymeshassociation/signing-manager-types';
44

5-
import { AppNotFoundError } from '~/common/errors';
5+
import { AppInternalError, AppNotFoundError } from '~/common/errors';
66
import { PolymeshService } from '~/polymesh/polymesh.service';
77

88
@Injectable()
@@ -29,4 +29,9 @@ export abstract class SigningService {
2929
protected throwNoSigner(handle: string): never {
3030
throw new AppNotFoundError(handle, 'signer');
3131
}
32+
33+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
34+
public async addSigner(handle: string, mnemonic: string): Promise<string> {
35+
throw new AppInternalError('Adding signers is not supported with the configured service');
36+
}
3237
}

‎src/signing/services/vault-signing.service.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Test, TestingModule } from '@nestjs/testing';
22
import { HashicorpVaultSigningManager } from '@polymeshassociation/hashicorp-vault-signing-manager';
33

4-
import { AppNotFoundError } from '~/common/errors';
4+
import { AppInternalError, AppNotFoundError } from '~/common/errors';
55
import { LoggerModule } from '~/logger/logger.module';
66
import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger';
77
import { PolymeshLogger } from '~/logger/polymesh-logger.service';
@@ -87,4 +87,10 @@ describe('VaultSigningService', () => {
8787
return expect(service.getAddressByHandle('badId')).rejects.toBeInstanceOf(AppNotFoundError);
8888
});
8989
});
90+
91+
describe('add signer', () => {
92+
it('should throw a not implemented error', () => {
93+
return expect(service.addSigner('bad', 'apple')).rejects.toThrow(AppInternalError);
94+
});
95+
});
9096
});

‎src/signing/signing.controller.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,15 @@ describe('SigningController', () => {
3333
return expect(controller.getSignerAddress({ signer })).resolves.toEqual(expectedResult);
3434
});
3535
});
36+
37+
describe('addSigner', () => {
38+
it('should call the service and return the result', () => {
39+
const handle = 'test-handle';
40+
const mnemonic = 'test mnemonic phrase';
41+
const expectedResult = new SignerModel({ address });
42+
43+
when(signingService.addSigner).calledWith(handle, mnemonic).mockResolvedValue(address);
44+
return expect(controller.addSigner({ handle, mnemonic })).resolves.toEqual(expectedResult);
45+
});
46+
});
3647
});

‎src/signing/signing.controller.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import { Controller, Get, Param } from '@nestjs/common';
1+
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
22
import {
33
ApiBadRequestResponse,
4+
ApiCreatedResponse,
45
ApiNotFoundResponse,
56
ApiOkResponse,
67
ApiOperation,
78
ApiParam,
89
ApiTags,
910
} from '@nestjs/swagger';
1011

12+
import { AddLocalSignerDto } from '~/signing/dto/add-local-signer.dto';
1113
import { SignerDetailsDto } from '~/signing/dto/signer-details.dto';
1214
import { SignerModel } from '~/signing/models/signer.model';
1315
import { SigningService } from '~/signing/services';
@@ -44,4 +46,22 @@ export class SigningController {
4446

4547
return new SignerModel({ address });
4648
}
49+
50+
@ApiOperation({
51+
summary: 'Add a new signer',
52+
description: 'Adds a new key to the signing manager',
53+
})
54+
@ApiCreatedResponse({
55+
description: 'The signer was successfully added',
56+
type: SignerModel,
57+
})
58+
@ApiBadRequestResponse({
59+
description: 'Invalid mnemonic or handle provided',
60+
})
61+
@Post()
62+
public async addSigner(@Body() { handle, mnemonic }: AddLocalSignerDto): Promise<SignerModel> {
63+
const address = await this.signingService.addSigner(handle, mnemonic);
64+
65+
return new SignerModel({ address });
66+
}
4767
}

0 commit comments

Comments
 (0)