Skip to content

Commit 78ef37e

Browse files
Merge pull request #4 from BitGo/WP-4593-add-bitgo-generate-wallet
feat(mbe): add bitgo integration and generate wallet api
2 parents 0d97d24 + 41827fb commit 78ef37e

File tree

14 files changed

+9904
-169
lines changed

14 files changed

+9904
-169
lines changed

.github/workflows/build-and-test.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,9 @@ jobs:
6969
- name: Test
7070
run: yarn test
7171
env:
72+
NODE_OPTIONS: "--max-old-space-size=4096"
7273
MASTER_BITGO_EXPRESS_KEYPATH: ./test-ssl-key.pem
7374
MASTER_BITGO_EXPRESS_CRTPATH: ./test-ssl-cert.pem
7475
MTLS_ENABLED: true
7576
MTLS_REQUEST_CERT: true
76-
MTLS_REJECT_UNAUTHORIZED: false
77+
MTLS_REJECT_UNAUTHORIZED: false

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
dist/
22
node_modules/
3-
coverage/
3+
coverage/
4+
.idea/

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
20

enclaved-express-cert.pem

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDCTCCAfGgAwIBAgIUbE+vqSu9IgPoLJncJqX5aiXh2GkwDQYJKoZIhvcNAQEL
3+
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDUzMDIwMDgxNVoXDTI2MDUz
4+
MDIwMDgxNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
5+
AAOCAQ8AMIIBCgKCAQEAsK1g8ts/QRdEHVnmiZSijvtKl08yf13JWY0yksJW0O6x
6+
mrt2uotvONxMNKhGtS+hPjcJ2OC7fCyift8oaCDs7PfIXjVNcN2zRKPci8ihNWvQ
7+
XrYGLTvL9EVHpH7CdlJU43BTaeFusH+k/qv2pW5WQnz13ULdq7yvnDFvJAeahm9X
8+
ptvr9RX9f8Aki0Y82Zi04PCiaHdqBPPl1OfHi+brf4xl7pQUq7Pub94/IDywe+QK
9+
lGFPQ0exSVm5X/7hWv/AxqEFa/Bqb6Uw0qatVqhrgLEHlLUYVXs9NDNXm+865+aT
10+
kvW2dnBpTVRZjnXO+N+BwSj+PfI28RqMXsmIhraN4QIDAQABo1MwUTAdBgNVHQ4E
11+
FgQUnsZxpWiuxqDq/1kV12rMos4NN/cwHwYDVR0jBBgwFoAUnsZxpWiuxqDq/1kV
12+
12rMos4NN/cwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAsAX4
13+
CCEsIVrKQKJKluEDqOFiuKg0SSe4xVqlSW9vy9z3UYLfOpw14EtB6Lzbtgw7z47w
14+
AnZZS99Zzn3tbYd06/X+b3jThF5TU1gqBcYDCC9HCd9xpmQEC7Ss1Xa88ubjuh+U
15+
E/7xN5xRt85S07VihJWscfY7JCUAELBo3gDCZLfgHjw8xMfPRceE36rkc5B2p60b
16+
WEmmOBWjrSboMOfocasBTUVUMDvGgmxEGEmKgTYshr5lWKcIteisbZi7+OZlkflp
17+
PUZNu5DUyQyjftr2EShndaceZrjgXt6ezoyQBVgPRA+N+NAJn+uBr3B3nZZb/mft
18+
n3XsbtsAAoU49kEVOg==
19+
-----END CERTIFICATE-----

enclaved-express-key.pem

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwrWDy2z9BF0Qd
3+
WeaJlKKO+0qXTzJ/XclZjTKSwlbQ7rGau3a6i2843Ew0qEa1L6E+NwnY4Lt8LKJ+
4+
3yhoIOzs98heNU1w3bNEo9yLyKE1a9BetgYtO8v0RUekfsJ2UlTjcFNp4W6wf6T+
5+
q/alblZCfPXdQt2rvK+cMW8kB5qGb1em2+v1Ff1/wCSLRjzZmLTg8KJod2oE8+XU
6+
58eL5ut/jGXulBSrs+5v3j8gPLB75AqUYU9DR7FJWblf/uFa/8DGoQVr8GpvpTDS
7+
pq1WqGuAsQeUtRhVez00M1eb7zrn5pOS9bZ2cGlNVFmOdc7434HBKP498jbxGoxe
8+
yYiGto3hAgMBAAECggEACrbTnJwBJQBf3WvN7Y/5n7Qg3ODXo3Ow5Iu0gm6N9z5S
9+
akYYuCKr4bCHtTXIkUT3K/UIgCzOEdoJwf85zb7EFMbIpuCSoVfrKYF1EXZe7a9r
10+
w81EE0rUs9aJDAeyi/Gy5iwHUvIcf/rtqugLcr194QBU+fsLwwC+oY6POonKLCwg
11+
gXMxRJKx+tvp86x6s7FU8+vi40L/mGbCC1Bl1YqraVp8nT17ICivlcHVZESxCcKN
12+
tCAY+xKK+zH+s5sHAQvM4OlGEvCeT1VISlw/VqkODxcGzMGUc+mbnGtWvDcYm9Pa
13+
54F29QapkdUwBVnucpTICenaMrLr19H9l6Zgvfvx1QKBgQDnHHeGnSD1bpZUhIUt
14+
2vIQpj7o26zsx472h9PmqIZwODpcYSfw8MknymXVnL78gdHqVDL0mgj59zemUpC7
15+
CR9RJAlV7/3TghUPdDFQ/SGj9+xGG/L/HNyy6bQeLIZiGOlURcdFqAtKCIk+51oK
16+
eTDCOuy3Ijrq6F6FbYnkXHmwcwKBgQDDtDdCo9EjnyiZ1qGOx5jLRNbGVlN77QFS
17+
tSmegODAwfLQpm4c4fE2WnzeWlNXnzs8GRLSRASXIYunjQdvgpX1KTpffZPoSP3k
18+
tvL8cbh1zk7X8cmvkrVJjcpn/ecWPgeHGV6MjuhxqhaVoEMjFsKRqQaSQ7r/gZsm
19+
Vba82tuXWwKBgHFlSlBGcJF7/U7i5uWk8/ivWVav0p0rHT5hTttx/OS68ge5s/tI
20+
aaqYaHbzPdJvcCvlvEq/+X+MiUWWZWUgCLmrUNlVs9k/jk3S2Q+/4+2sC8YqmIQM
21+
CU3P1YyolBc12eZ7hlbrKP7eSVkP8uIIrJ/ggZ0psnboJNia8nmV1i95AoGAPNWE
22+
Z/6sQDp1UHzbc5qv8F/Rs42aHeeqhZ8y9MZzFvgzFpDloazKYm72adgCGDazHxdc
23+
NmhWVPRkiQzZxtv86VyLfKt4krg91B7aoYZoJJahA5dxblZYbCjbRkAy2UMm6+QC
24+
9AZoUwzgQFq1A+9LRCQamtTbCBmttNjoGQSfRgkCgYEAi03ZXB+B0/4C2HRUz/GQ
25+
6moLgB7FzC4MLY2KUDeiP+3zBPnfbGQM0OgPYu7OOWPC6lebS4C6DuPCTOSW1z8u
26+
f4FeVSKGrofPx+DmEvUMsUQ5TvjRPwNL40PVlrdxytZi6nV01GveScPfljvQUd2r
27+
DRaZg+YgV9Yl6wi8y2G5RD0=
28+
-----END PRIVATE KEY-----

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@
1717
"generate-test-ssl": "openssl req -x509 -newkey rsa:2048 -keyout test-ssl-key.pem -out test-ssl-cert.pem -days 365 -nodes -subj '/CN=localhost'"
1818
},
1919
"dependencies": {
20+
"bitgo": "^44.2.0",
21+
"@bitgo/sdk-core": "^33.2.0",
2022
"body-parser": "^1.20.3",
2123
"connect-timeout": "^1.9.0",
2224
"debug": "^3.1.0",
2325
"express": "4.17.3",
2426
"lodash": "^4.17.20",
2527
"morgan": "^1.9.1",
26-
"superagent": "^8.0.9"
28+
"superagent": "^8.0.9",
29+
"proxy-agent": "6.4.0",
30+
"proxyquire": "^2.1.3"
2731
},
2832
"devDependencies": {
2933
"@types/body-parser": "^1.17.0",
@@ -54,6 +58,6 @@
5458
"typescript": "^4.2.4"
5559
},
5660
"engines": {
57-
"node": ">=14"
61+
"node": ">=20.18.0"
5862
}
5963
}

src/__tests__/routes.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('Routes', () => {
1212

1313
describe('Health Check Routes', () => {
1414
it('should return 200 and status message for /ping', async () => {
15-
const response = await request(app).get('/ping');
15+
const response = await request(app).post('/ping');
1616
expect(response.status).toBe(200);
1717
expect(response.body).toHaveProperty('status', 'enclaved express server is ok!');
1818
expect(response.body).toHaveProperty('timestamp');
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import * as superagent from 'superagent';
2+
import debug from 'debug';
3+
import { config } from '../config';
4+
import { isMasterExpressConfig } from '../types';
5+
import https from 'https';
6+
7+
const debugLogger = debug('bitgo:express:enclavedExpressClient');
8+
9+
interface CreateIndependentKeychainParams {
10+
source: 'user' | 'backup';
11+
coin?: string;
12+
type: 'independent';
13+
seed?: string;
14+
}
15+
16+
export interface IndependentKeychainResponse {
17+
id: string;
18+
pub: string;
19+
encryptedPrv?: string;
20+
type: 'independent';
21+
source: 'user' | 'backup' | 'bitgo';
22+
coin: string;
23+
}
24+
25+
export class EnclavedExpressClient {
26+
private readonly url: string;
27+
private readonly sslCert: string;
28+
private readonly coin?: string;
29+
private readonly enableSSL: boolean;
30+
31+
constructor(coin?: string) {
32+
const cfg = config();
33+
if (!isMasterExpressConfig(cfg)) {
34+
throw new Error('Configuration is not in master express mode');
35+
}
36+
37+
if (!cfg.enclavedExpressUrl || !cfg.enclavedExpressSSLCert) {
38+
throw new Error(
39+
'Enclaved Express URL not configured. Please set BITGO_ENCLAVED_EXPRESS_URL and BITGO_ENCLAVED_EXPRESS_SSL_CERT in your environment.',
40+
);
41+
}
42+
43+
this.url = cfg.enclavedExpressUrl;
44+
this.sslCert = cfg.enclavedExpressSSLCert;
45+
this.coin = coin;
46+
this.enableSSL = !!cfg.enableSSL;
47+
debugLogger('EnclavedExpressClient initialized with URL: %s', this.url);
48+
}
49+
50+
async ping(): Promise<void> {
51+
try {
52+
debugLogger('Pinging enclaved express at %s', this.url);
53+
await superagent.get(`${this.url}/ping`).ca(this.sslCert).send();
54+
} catch (error) {
55+
const err = error as Error;
56+
debugLogger('Failed to ping enclaved express: %s', err.message);
57+
throw new Error(`Failed to ping enclaved express: ${err.message}`);
58+
}
59+
}
60+
61+
/**
62+
* Create an independent multisig key for a given source and coin
63+
*/
64+
async createIndependentKeychain(
65+
params: CreateIndependentKeychainParams,
66+
): Promise<IndependentKeychainResponse> {
67+
if (!this.coin) {
68+
throw new Error('Coin not configured');
69+
}
70+
try {
71+
debugLogger('Creating independent keychain for coin: %s', this.coin);
72+
const { body: keychain } = await superagent
73+
.post(`${this.url}/api/${this.coin}/key/independent`)
74+
.ca(this.sslCert)
75+
.agent(
76+
new https.Agent({
77+
rejectUnauthorized: this.enableSSL,
78+
ca: this.sslCert,
79+
}),
80+
)
81+
.type('json')
82+
.send(params);
83+
return keychain;
84+
} catch (error) {
85+
const err = error as Error;
86+
debugLogger('Failed to create independent keychain: %s', err.message);
87+
throw new Error(`Failed to create independent keychain: ${err.message}`);
88+
}
89+
}
90+
}
91+
92+
/**
93+
* Create an enclaved express client if the configuration is present
94+
*/
95+
export function createEnclavedExpressClient(coin?: string): EnclavedExpressClient | undefined {
96+
try {
97+
return new EnclavedExpressClient(coin);
98+
} catch (error) {
99+
const err = error as Error;
100+
// If URL isn't configured, return undefined instead of throwing
101+
if (err.message.includes('URL not configured')) {
102+
debugLogger('Enclaved express URL not configured, returning undefined');
103+
return undefined;
104+
}
105+
throw err;
106+
}
107+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import {
2+
GenerateWalletOptions,
3+
promiseProps,
4+
RequestTracer,
5+
SupplementGenerateWalletOptions,
6+
Keychain,
7+
KeychainsTriplet,
8+
Wallet,
9+
WalletWithKeychains,
10+
AddKeychainOptions,
11+
} from '@bitgo/sdk-core';
12+
import { createEnclavedExpressClient } from './enclavedExpressClient';
13+
import _ from 'lodash';
14+
import { BitGoRequest } from '../types/request';
15+
16+
/**
17+
* This route is used to generate a multisig wallet when enclaved express is enabled
18+
*/
19+
export async function handleGenerateWalletOnPrem(req: BitGoRequest) {
20+
const bitgo = req.bitgo;
21+
const baseCoin = bitgo.coin(req.params.coin);
22+
23+
const enclavedExpressClient = createEnclavedExpressClient(req.params.coin);
24+
if (!enclavedExpressClient) {
25+
throw new Error(
26+
'Enclaved express client not configured - enclaved express features will be disabled',
27+
);
28+
}
29+
30+
const params = req.body as GenerateWalletOptions;
31+
const reqId = new RequestTracer();
32+
33+
// Assign the default multiSig type value based on the coin
34+
if (!params.multisigType) {
35+
params.multisigType = baseCoin.getDefaultMultisigType();
36+
}
37+
38+
if (typeof params.label !== 'string') {
39+
throw new Error('missing required string parameter label');
40+
}
41+
42+
const { label, enterprise } = params;
43+
44+
// Create wallet parameters with type assertion to allow 'onprem' subtype
45+
const walletParams = {
46+
label: label,
47+
m: 2,
48+
n: 3,
49+
keys: [],
50+
type: 'cold',
51+
subType: 'onprem',
52+
multisigType: 'onchain',
53+
} as unknown as SupplementGenerateWalletOptions; // TODO: Add onprem to the SDK subType and remove "unknown" type casting
54+
55+
if (!_.isUndefined(enterprise)) {
56+
if (!_.isString(enterprise)) {
57+
throw new Error('invalid enterprise argument, expecting string');
58+
}
59+
walletParams.enterprise = enterprise;
60+
}
61+
62+
const userKeychainPromise = async (): Promise<Keychain> => {
63+
const userKeychain = await enclavedExpressClient.createIndependentKeychain({
64+
source: 'user',
65+
coin: req.params.coin,
66+
type: 'independent',
67+
});
68+
const userKeychainParams: AddKeychainOptions = {
69+
pub: userKeychain.pub,
70+
keyType: userKeychain.type,
71+
source: userKeychain.source,
72+
reqId,
73+
};
74+
75+
const newUserKeychain = await baseCoin.keychains().add(userKeychainParams);
76+
return _.extend({}, newUserKeychain, userKeychain);
77+
};
78+
79+
const backupKeychainPromise = async (): Promise<Keychain> => {
80+
const backupKeychain = await enclavedExpressClient.createIndependentKeychain({
81+
source: 'backup',
82+
coin: req.params.coin,
83+
type: 'independent',
84+
});
85+
const backupKeychainParams: AddKeychainOptions = {
86+
pub: backupKeychain.pub,
87+
keyType: backupKeychain.type,
88+
source: backupKeychain.source,
89+
reqId,
90+
};
91+
92+
const newBackupKeychain = await baseCoin.keychains().add(backupKeychainParams);
93+
return _.extend({}, newBackupKeychain, backupKeychain);
94+
};
95+
96+
const { userKeychain, backupKeychain, bitgoKeychain }: KeychainsTriplet = await promiseProps({
97+
userKeychain: userKeychainPromise(),
98+
backupKeychain: backupKeychainPromise(),
99+
bitgoKeychain: baseCoin.keychains().createBitGo({
100+
enterprise: params.enterprise,
101+
reqId,
102+
isDistributedCustody: params.isDistributedCustody,
103+
}),
104+
});
105+
106+
walletParams.keys = [userKeychain.id, backupKeychain.id, bitgoKeychain.id];
107+
108+
const keychains = {
109+
userKeychain,
110+
backupKeychain,
111+
bitgoKeychain,
112+
};
113+
114+
const finalWalletParams = await baseCoin.supplementGenerateWallet(walletParams, keychains);
115+
116+
bitgo.setRequestTracer(reqId);
117+
const newWallet = await bitgo.post(baseCoin.url('/wallet/add')).send(finalWalletParams).result();
118+
119+
const result: WalletWithKeychains = {
120+
wallet: new Wallet(bitgo, baseCoin, newWallet),
121+
userKeychain: userKeychain,
122+
backupKeychain: backupKeychain,
123+
bitgoKeychain: bitgoKeychain,
124+
responseType: 'WalletWithKeychains',
125+
};
126+
127+
return { ...result, wallet: result.wallet.toJSON() };
128+
}

0 commit comments

Comments
 (0)