Skip to content

Commit 7b7882d

Browse files
committed
fix(express): createLocalKeyChain type codec
Ticket: WP-6717
1 parent b967950 commit 7b7882d

File tree

2 files changed

+37
-129
lines changed

2 files changed

+37
-129
lines changed

modules/express/src/typedRoutes/api/v1/createLocalKeyChain.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,41 @@ import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
33
import { BitgoExpressError } from '../../schemas/error';
44

55
/**
6-
* Request parameters for creating a local keychain
6+
* Request body for creating a local keychain
77
*/
88
export const CreateLocalKeyChainRequestBody = {
9-
/** Optional seed for key generation (use with caution) */
9+
/**
10+
* Optional seed for key generation. If not provided, a random seed with 512 bits
11+
* of entropy will be generated for maximum security. The seed is used to derive a BIP32
12+
* extended key pair.
13+
*/
1014
seed: optional(t.string),
1115
};
1216

1317
/**
1418
* Response for creating a local keychain
1519
*/
1620
export const CreateLocalKeyChainResponse = t.type({
17-
/** The extended private key */
21+
/** The extended private key in BIP32 format (xprv...) */
1822
xprv: t.string,
19-
/** The extended public key */
23+
/** The extended public key in BIP32 format (xpub...) */
2024
xpub: t.string,
21-
/** The Ethereum address derived from the xpub (if available) */
25+
/** Ethereum address derived from the extended public key (only available when Ethereum utilities are accessible) */
2226
ethAddress: optional(t.string),
2327
});
2428

2529
/**
2630
* Create a local keychain
2731
*
28-
* Locally creates a new keychain. This is a client-side function that does not
29-
* involve any server-side operations. Returns an object containing the xprv and xpub
30-
* for the new chain. The created keychain is not known to the BitGo service.
31-
* To use it with the BitGo service, use the 'Add Keychain' API call.
32+
* Locally creates a new keychain using BIP32 HD (Hierarchical Deterministic) key derivation.
33+
* This is a client-side operation that does not involve any server-side operations.
3234
*
33-
* For security reasons, it is highly recommended that you encrypt and destroy
34-
* the original xprv immediately to prevent theft.
35+
* Returns an object containing the xprv and xpub keys in BIP32 extended key format.
36+
* The created keychain is not known to the BitGo service. To use it with BitGo,
37+
* you must add it using the 'Add Keychain' API call.
38+
*
39+
* For security reasons, it is highly recommended that you encrypt the private key
40+
* immediately and securely destroy the unencrypted original to prevent theft.
3541
*
3642
* @operationId express.v1.keychain.local
3743
* @tag express

modules/express/test/unit/typedRoutes/createLocalKeyChain.ts

Lines changed: 20 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe('CreateLocalKeyChain codec tests', function () {
4343
});
4444

4545
describe('CreateLocalKeyChainResponse', function () {
46-
it('should validate response with required fields', function () {
46+
it('should validate response with required xprv field', function () {
4747
const validResponse = {
4848
xprv: 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2',
4949
xpub: 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8',
@@ -52,20 +52,17 @@ describe('CreateLocalKeyChain codec tests', function () {
5252
const decoded = assertDecode(CreateLocalKeyChainResponse, validResponse);
5353
assert.strictEqual(decoded.xprv, validResponse.xprv);
5454
assert.strictEqual(decoded.xpub, validResponse.xpub);
55-
assert.strictEqual(decoded.ethAddress, undefined); // Optional field
5655
});
5756

58-
it('should validate response with all fields including optional ones', function () {
57+
it('should validate response with both xprv and xpub fields', function () {
5958
const validResponse = {
6059
xprv: 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2',
6160
xpub: 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8',
62-
ethAddress: '0x1234567890123456789012345678901234567890',
6361
};
6462

6563
const decoded = assertDecode(CreateLocalKeyChainResponse, validResponse);
6664
assert.strictEqual(decoded.xprv, validResponse.xprv);
6765
assert.strictEqual(decoded.xpub, validResponse.xpub);
68-
assert.strictEqual(decoded.ethAddress, validResponse.ethAddress);
6966
});
7067

7168
it('should reject response with missing xprv', function () {
@@ -78,14 +75,15 @@ describe('CreateLocalKeyChain codec tests', function () {
7875
});
7976
});
8077

81-
it('should reject response with missing xpub', function () {
82-
const invalidResponse = {
78+
it('should allow response with missing ethAddress (optional)', function () {
79+
const validResponse = {
8380
xprv: 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2',
81+
xpub: 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8',
8482
};
8583

86-
assert.throws(() => {
87-
assertDecode(CreateLocalKeyChainResponse, invalidResponse);
88-
});
84+
const decoded = assertDecode(CreateLocalKeyChainResponse, validResponse);
85+
assert.strictEqual(decoded.xprv, validResponse.xprv);
86+
assert.strictEqual(decoded.xpub, validResponse.xpub);
8987
});
9088

9189
it('should reject response with non-string xprv', function () {
@@ -109,18 +107,6 @@ describe('CreateLocalKeyChain codec tests', function () {
109107
assertDecode(CreateLocalKeyChainResponse, invalidResponse);
110108
});
111109
});
112-
113-
it('should reject response with non-string ethAddress', function () {
114-
const invalidResponse = {
115-
xprv: 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2',
116-
xpub: 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8',
117-
ethAddress: 123, // number instead of string
118-
};
119-
120-
assert.throws(() => {
121-
assertDecode(CreateLocalKeyChainResponse, invalidResponse);
122-
});
123-
});
124110
});
125111

126112
describe('Edge cases', function () {
@@ -177,6 +163,7 @@ describe('CreateLocalKeyChain codec tests', function () {
177163
const mockKeychainResponse = {
178164
xprv: 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2',
179165
xpub: 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8',
166+
ethAddress: '0x1234567890123456789012345678901234567890',
180167
};
181168

182169
afterEach(function () {
@@ -240,36 +227,6 @@ describe('CreateLocalKeyChain codec tests', function () {
240227
assert.strictEqual(mockKeychains.create.calledOnceWith(requestBody), true);
241228
});
242229

243-
it('should successfully create a local keychain with ethAddress', async function () {
244-
const requestBody = {};
245-
246-
const mockResponseWithEthAddress = {
247-
...mockKeychainResponse,
248-
ethAddress: '0x1234567890123456789012345678901234567890',
249-
};
250-
251-
const mockKeychains = {
252-
create: sinon.stub().resolves(mockResponseWithEthAddress),
253-
};
254-
255-
sinon.stub(BitGo.prototype, 'keychains').returns(mockKeychains as any);
256-
257-
const result = await agent
258-
.post('/api/v1/keychain/local')
259-
.set('Authorization', 'Bearer test_access_token_12345')
260-
.set('Content-Type', 'application/json')
261-
.send(requestBody);
262-
263-
assert.strictEqual(result.status, 200);
264-
result.body.should.have.property('xprv');
265-
result.body.should.have.property('xpub');
266-
result.body.should.have.property('ethAddress');
267-
assert.strictEqual(result.body.ethAddress, mockResponseWithEthAddress.ethAddress);
268-
269-
const decodedResponse = assertDecode(CreateLocalKeyChainResponse, result.body);
270-
assert.strictEqual(decodedResponse.ethAddress, mockResponseWithEthAddress.ethAddress);
271-
});
272-
273230
it('should create keychain with very long seed value', async function () {
274231
const requestBody = {
275232
seed: 'a'.repeat(1000), // Very long seed
@@ -541,6 +498,7 @@ describe('CreateLocalKeyChain codec tests', function () {
541498
create: sinon.stub().resolves({
542499
xprv: longXprv,
543500
xpub: longXpub,
501+
ethAddress: '0x1234567890123456789012345678901234567890',
544502
}),
545503
};
546504

@@ -560,32 +518,6 @@ describe('CreateLocalKeyChain codec tests', function () {
560518
assert.strictEqual(decodedResponse.xprv, longXprv);
561519
});
562520

563-
it('should handle response with empty ethAddress', async function () {
564-
const requestBody = {};
565-
566-
const mockKeychains = {
567-
create: sinon.stub().resolves({
568-
xprv: 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2',
569-
xpub: 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8',
570-
ethAddress: '',
571-
}),
572-
};
573-
574-
sinon.stub(BitGo.prototype, 'keychains').returns(mockKeychains as any);
575-
576-
const result = await agent
577-
.post('/api/v1/keychain/local')
578-
.set('Authorization', 'Bearer test_access_token_12345')
579-
.set('Content-Type', 'application/json')
580-
.send(requestBody);
581-
582-
assert.strictEqual(result.status, 200);
583-
assert.strictEqual(result.body.ethAddress, '');
584-
585-
const decodedResponse = assertDecode(CreateLocalKeyChainResponse, result.body);
586-
assert.strictEqual(decodedResponse.ethAddress, '');
587-
});
588-
589521
it('should handle response with additional unexpected fields', async function () {
590522
const requestBody = {};
591523

@@ -608,11 +540,7 @@ describe('CreateLocalKeyChain codec tests', function () {
608540

609541
assert.strictEqual(result.status, 200);
610542
// Codec validation should still pass with required fields present
611-
const decodedResponse = assertDecode(CreateLocalKeyChainResponse, {
612-
xprv: result.body.xprv,
613-
xpub: result.body.xpub,
614-
ethAddress: result.body.ethAddress,
615-
});
543+
const decodedResponse = assertDecode(CreateLocalKeyChainResponse, result.body);
616544
assert.ok(decodedResponse);
617545
});
618546

@@ -683,15 +611,17 @@ describe('CreateLocalKeyChain codec tests', function () {
683611
});
684612
});
685613

686-
it('should reject response with missing xpub field', async function () {
614+
it('should allow response with missing ethAddress field (optional)', async function () {
687615
const requestBody = {};
688616

689-
const invalidResponse = {
617+
const validResponse = {
690618
xprv: 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2',
619+
xpub: 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8',
620+
// ethAddress is optional, so missing it is valid
691621
};
692622

693623
const mockKeychains = {
694-
create: sinon.stub().resolves(invalidResponse),
624+
create: sinon.stub().resolves(validResponse),
695625
};
696626

697627
sinon.stub(BitGo.prototype, 'keychains').returns(mockKeychains as any);
@@ -702,11 +632,11 @@ describe('CreateLocalKeyChain codec tests', function () {
702632
.set('Content-Type', 'application/json')
703633
.send(requestBody);
704634

705-
// Framework returns 200 with invalid response, codec validation should fail
635+
// pub is optional, so this should pass
706636
assert.strictEqual(result.status, 200);
707-
assert.throws(() => {
708-
assertDecode(CreateLocalKeyChainResponse, result.body);
709-
});
637+
const decodedResponse = assertDecode(CreateLocalKeyChainResponse, result.body);
638+
assert.strictEqual(decodedResponse.xprv, validResponse.xprv);
639+
assert.strictEqual(decodedResponse.xpub, validResponse.xpub);
710640
});
711641

712642
it('should reject response with wrong type for xprv', async function () {
@@ -763,34 +693,6 @@ describe('CreateLocalKeyChain codec tests', function () {
763693
});
764694
});
765695

766-
it('should reject response with wrong type for ethAddress', async function () {
767-
const requestBody = {};
768-
769-
const invalidResponse = {
770-
xprv: 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2',
771-
xpub: 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8',
772-
ethAddress: 123, // number instead of string
773-
};
774-
775-
const mockKeychains = {
776-
create: sinon.stub().resolves(invalidResponse),
777-
};
778-
779-
sinon.stub(BitGo.prototype, 'keychains').returns(mockKeychains as any);
780-
781-
const result = await agent
782-
.post('/api/v1/keychain/local')
783-
.set('Authorization', 'Bearer test_access_token_12345')
784-
.set('Content-Type', 'application/json')
785-
.send(requestBody);
786-
787-
// Framework returns 200 with invalid response, codec validation should fail
788-
assert.strictEqual(result.status, 200);
789-
assert.throws(() => {
790-
assertDecode(CreateLocalKeyChainResponse, result.body);
791-
});
792-
});
793-
794696
it('should reject response with empty object', async function () {
795697
const requestBody = {};
796698

0 commit comments

Comments
 (0)