Skip to content

Commit 8a58773

Browse files
wip(mbe): migrate generate wallet to api-ts
Ticket: WP-46222
1 parent 99ebebd commit 8a58773

6 files changed

Lines changed: 127 additions & 80 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919
"dependencies": {
2020
"@api-ts/io-ts-http": "^3.2.1",
2121
"@api-ts/response": "^2.1.0",
22-
"@api-ts/openapi-generator": "^5.7.0",
23-
"@api-ts/typed-express-router": "^1.1.13",
22+
"@api-ts/typed-express-router": "^1.1.13",
2423
"@bitgo/sdk-core": "^33.2.0",
2524
"bitgo": "^44.2.0",
2625
"body-parser": "^1.20.3",
@@ -37,6 +36,7 @@
3736
"zod": "^3.25.48"
3837
},
3938
"devDependencies": {
39+
"@api-ts/openapi-generator": "^5.7.0",
4040
"nodemon": "^3.1.10",
4141
"@types/body-parser": "^1.17.0",
4242
"@types/connect-timeout": "^1.9.0",

src/masterBitgoExpress/generateWallet.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Wallet,
99
WalletWithKeychains,
1010
AddKeychainOptions,
11+
BitGoBase,
1112
} from '@bitgo/sdk-core';
1213
import { createEnclavedExpressClient } from './enclavedExpressClient';
1314
import _ from 'lodash';
@@ -117,7 +118,7 @@ export async function handleGenerateWalletOnPrem(req: BitGoRequest) {
117118
const newWallet = await bitgo.post(baseCoin.url('/wallet/add')).send(finalWalletParams).result();
118119

119120
const result: WalletWithKeychains = {
120-
wallet: new Wallet(bitgo, baseCoin, newWallet),
121+
wallet: new Wallet(bitgo as unknown as BitGoBase, baseCoin, newWallet),
121122
userKeychain: userKeychain,
122123
backupKeychain: backupKeychain,
123124
bitgoKeychain: bitgoKeychain,
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import * as t from 'io-ts';
2+
import { apiSpec, httpRoute, httpRequest, HttpResponse } from '@api-ts/io-ts-http';
3+
import { createRouter, type WrappedRouter } from '@api-ts/typed-express-router';
4+
import { Response } from '@api-ts/response';
5+
import express, { Request } from 'express';
6+
import { BitGo } from 'bitgo';
7+
import { BitGoRequest, isBitGoRequest } from '../../types/request';
8+
import { MasterExpressConfig } from '../../config';
9+
import { handleGenerateWalletOnPrem } from '../generateWallet';
10+
import { withResponseHandler } from '../../shared/responseHandler';
11+
12+
// Middleware functions
13+
export function parseBody(req: express.Request, res: express.Response, next: express.NextFunction) {
14+
req.headers['content-type'] = req.headers['content-type'] || 'application/json';
15+
return express.json({ limit: '20mb' })(req, res, next);
16+
}
17+
18+
export function prepareBitGo(config: MasterExpressConfig) {
19+
const { env, customRootUri } = config;
20+
const BITGOEXPRESS_USER_AGENT = `BitGoExpress/${process.env.npm_package_version}`;
21+
22+
return function prepBitGo(
23+
req: express.Request,
24+
res: express.Response,
25+
next: express.NextFunction,
26+
) {
27+
let accessToken;
28+
if (req.headers.authorization) {
29+
const authSplit = req.headers.authorization.split(' ');
30+
if (authSplit.length === 2 && authSplit[0].toLowerCase() === 'bearer') {
31+
accessToken = authSplit[1];
32+
}
33+
}
34+
const userAgent = req.headers['user-agent']
35+
? BITGOEXPRESS_USER_AGENT + ' ' + req.headers['user-agent']
36+
: BITGOEXPRESS_USER_AGENT;
37+
38+
const bitgoConstructorParams = {
39+
env,
40+
customRootURI: customRootUri,
41+
accessToken,
42+
userAgent,
43+
};
44+
45+
(req as BitGoRequest).bitgo = new BitGo(bitgoConstructorParams);
46+
(req as BitGoRequest).config = config;
47+
48+
next();
49+
};
50+
}
51+
52+
// Response type for /generate endpoint
53+
const GenerateWalletResponse: HttpResponse = {
54+
// TODO: Get type from public types repo
55+
200: t.any,
56+
500: t.type({
57+
error: t.string,
58+
details: t.string,
59+
}),
60+
};
61+
62+
// Request type for /generate endpoint
63+
const GenerateWalletRequest = {
64+
label: t.string,
65+
multisigType: t.union([t.undefined, t.literal('onchain'), t.literal('tss')]),
66+
enterprise: t.string,
67+
disableTransactionNotifications: t.union([t.undefined, t.boolean]),
68+
isDistributedCustody: t.union([t.undefined, t.boolean]),
69+
};
70+
71+
// API Specification
72+
export const EnclavedApiSpec = apiSpec({
73+
'v1.wallet.generate': {
74+
post: httpRoute({
75+
method: 'POST',
76+
path: '/{coin}/wallet/generate',
77+
request: httpRequest({
78+
params: {
79+
coin: t.string,
80+
},
81+
body: GenerateWalletRequest,
82+
}),
83+
response: GenerateWalletResponse,
84+
description: 'Generate a new wallet',
85+
}),
86+
},
87+
});
88+
89+
// Create router with handlers
90+
export function createEnclavedApiRouter(
91+
cfg: MasterExpressConfig,
92+
): WrappedRouter<typeof EnclavedApiSpec> {
93+
const router = createRouter(EnclavedApiSpec);
94+
95+
// Add middleware to all routes
96+
router.use(parseBody);
97+
router.use(prepareBitGo(cfg));
98+
99+
// Generate wallet endpoint handler
100+
router.post('v1.wallet.generate', [
101+
withResponseHandler(async (req: BitGoRequest | Request) => {
102+
if (!isBitGoRequest(req)) {
103+
throw new Error('Invalid request type');
104+
}
105+
const result = await handleGenerateWalletOnPrem(req);
106+
return Response.ok(result);
107+
}),
108+
]);
109+
110+
return router;
111+
}

src/routes/master.ts

Lines changed: 3 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,9 @@
11
import express from 'express';
2-
import { BitGo, BitGoOptions } from 'bitgo';
3-
import { BitGoBase } from '@bitgo/sdk-core';
4-
import { version } from 'bitgo/package.json';
5-
import pjson from '../../package.json';
6-
import { MasterExpressConfig } from '../config';
7-
import { BitGoRequest } from '../types/request';
8-
import { handleGenerateWalletOnPrem } from '../masterBitgoExpress/generateWallet';
2+
import { MasterExpressConfig } from '../types';
93
import { createHealthCheckRouter } from '../masterBitgoExpress/routers/healthCheck';
104
import { createEnclavedExpressRouter } from '../masterBitgoExpress/routers/enclavedExpressHealth';
11-
import { promiseWrapper } from './utils';
125
import logger from '../logger';
13-
14-
const BITGOEXPRESS_USER_AGENT = `BitGoExpress/${pjson.version} BitGoJS/${version}`;
15-
16-
/**
17-
* Perform body parsing here only on routes we want
18-
*/
19-
function parseBody(req: express.Request, res: express.Response, next: express.NextFunction) {
20-
// Set the default Content-Type, in case the client doesn't set it. If
21-
// Content-Type isn't specified, Express silently refuses to parse the
22-
// request body.
23-
req.headers['content-type'] = req.headers['content-type'] || 'application/json';
24-
return express.json({ limit: '20mb' })(req, res, next);
25-
}
26-
27-
/**
28-
* Create the bitgo object in the request
29-
* @param config
30-
*/
31-
function prepareBitGo(config: MasterExpressConfig) {
32-
const { env, customRootUri } = config;
33-
34-
return function prepBitGo(
35-
req: express.Request,
36-
res: express.Response,
37-
next: express.NextFunction,
38-
) {
39-
// Get access token
40-
let accessToken;
41-
if (req.headers.authorization) {
42-
const authSplit = req.headers.authorization.split(' ');
43-
if (authSplit.length === 2 && authSplit[0].toLowerCase() === 'bearer') {
44-
accessToken = authSplit[1];
45-
}
46-
}
47-
const userAgent = req.headers['user-agent']
48-
? BITGOEXPRESS_USER_AGENT + ' ' + req.headers['user-agent']
49-
: BITGOEXPRESS_USER_AGENT;
50-
51-
const bitgoConstructorParams: BitGoOptions = {
52-
env,
53-
customRootURI: customRootUri,
54-
accessToken,
55-
userAgent,
56-
};
57-
58-
(req as BitGoRequest).bitgo = new BitGo(bitgoConstructorParams) as unknown as BitGoBase;
59-
(req as BitGoRequest).config = config;
60-
61-
next();
62-
};
63-
}
6+
import { createEnclavedApiRouter } from '../masterBitgoExpress/routers/enclavedApiSpec';
647

658
/**
669
* Setup master express specific routes
@@ -73,20 +16,7 @@ export function setupRoutes(app: express.Application, cfg: MasterExpressConfig):
7316
// TODO: Add version endpoint to enclaved express
7417
app.use(createEnclavedExpressRouter(cfg));
7518

76-
// TODO: Add api-ts to these new API routes
77-
app.post(
78-
'/api/:coin/wallet/generate',
79-
parseBody,
80-
prepareBitGo(cfg),
81-
promiseWrapper(handleGenerateWalletOnPrem),
82-
);
83-
84-
// Add a catch-all for unsupported routes
85-
app.use('*', (_req, res) => {
86-
res.status(404).json({
87-
error: 'Route not found or not supported in master express mode',
88-
});
89-
});
19+
app.use('/api', createEnclavedApiRouter(cfg));
9020

9121
logger.debug('Master express routes configured');
9222
}

src/shared/responseHandler.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BitGoRequest } from '../types/request';
12
import { Request, Response as ExpressResponse, NextFunction } from 'express';
23

34
// Extend Express Response to include sendEncoded
@@ -12,7 +13,7 @@ interface ApiResponse {
1213
}
1314

1415
type ServiceFunction = (
15-
req: Request,
16+
req: Request | BitGoRequest,
1617
res: EncodedResponse,
1718
next: NextFunction,
1819
) => Promise<ApiResponse> | ApiResponse;
@@ -23,7 +24,7 @@ type ServiceFunction = (
2324
* @returns Express middleware function that handles the response encoding
2425
*/
2526
export function withResponseHandler(fn: ServiceFunction) {
26-
return async (req: Request, res: EncodedResponse, next: NextFunction) => {
27+
return async (req: Request | BitGoRequest, res: EncodedResponse, next: NextFunction) => {
2728
try {
2829
const result = await Promise.resolve(fn(req, res, next));
2930
return res.sendEncoded(result.type, result.payload);

src/types/request.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import express from 'express';
2-
import { BitGoBase } from '@bitgo/sdk-core';
2+
import { type BitGo } from 'bitgo';
33
import { MasterExpressConfig } from '../types';
44

55
// Extended request type for BitGo Express
66
export interface BitGoRequest extends express.Request {
7-
bitgo: BitGoBase;
7+
bitgo: BitGo;
88
config: MasterExpressConfig;
99
}
10+
11+
export function isBitGoRequest(req: express.Request): req is BitGoRequest {
12+
return (req as BitGoRequest).bitgo !== undefined && (req as BitGoRequest).config !== undefined;
13+
}

0 commit comments

Comments
 (0)