Skip to content

Commit 5477bb1

Browse files
feat(ebe): api-tsify ebe
Ticket: WP-4662 # Conflicts: # package.json
1 parent 227650d commit 5477bb1

File tree

17 files changed

+339
-136
lines changed

17 files changed

+339
-136
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
"dependencies": {
2020
"@api-ts/io-ts-http": "^3.2.1",
2121
"@api-ts/openapi-generator": "^5.7.0",
22+
"@api-ts/typed-express-router": "^1.1.13",
23+
"@api-ts/superagent-wrapper": "^1.3.3",
2224
"@api-ts/response": "^2.1.0",
23-
"@api-ts/typed-express-router": "^1.1.13",
2425
"@bitgo/sdk-core": "^35.2.0",
2526
"bitgo": "^48.0.0",
2627
"body-parser": "^1.20.3",
@@ -51,6 +52,7 @@
5152
"@types/sinon": "^10.0.11",
5253
"@types/supertest": "^2.0.11",
5354
"@types/winston": "^2.4.4",
55+
"@types/superagent": "^8.1.9",
5456
"@typescript-eslint/eslint-plugin": "^5.0.0",
5557
"@typescript-eslint/parser": "^5.0.0",
5658
"eslint": "^8.0.0",

src/__tests__/routes.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@ import 'should';
22
import express from 'express';
33
import request from 'supertest';
44
import { setupRoutes } from '../routes/enclaved';
5+
import { AppMode, TlsMode } from '../types';
56

67
describe('Routes', () => {
78
let app: express.Application;
89

910
beforeEach(() => {
1011
app = express();
11-
setupRoutes(app);
12+
setupRoutes(app, {
13+
appMode: AppMode.ENCLAVED,
14+
tlsMode: TlsMode.DISABLED,
15+
mtlsRequestCert: false,
16+
kmsUrl: 'http://localhost:3000/kms',
17+
timeout: 5000,
18+
port: 3000,
19+
bind: 'localhost',
20+
});
1221
});
1322

1423
describe('Health Check Routes', () => {

src/api/enclaved/postIndependentKey.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import { BitGo } from 'bitgo';
2-
import * as express from 'express';
32
import { KmsClient } from '../../kms/kmsClient';
3+
import { EnclavedApiSpecRouteRequest } from '../../enclavedBitgoExpress/routers/enclavedApiSpec';
44

55
export async function postIndependentKey(
6-
req: express.Request,
7-
res: express.Response,
8-
): Promise<any> {
9-
const { source, seed }: { source: string; seed?: string } = req.body;
6+
req: EnclavedApiSpecRouteRequest<'v1.key.independent', 'post'>,
7+
) {
8+
const { source, seed }: { source: string; seed?: string } = req.decoded;
109
if (!source) {
1110
throw new Error('Source is required for key generation');
1211
}
1312

1413
// setup clients
15-
const bitgo: BitGo = req.body.bitgo;
14+
const bitgo: BitGo = req.bitgo;
1615
const kms = new KmsClient();
1716

1817
// create public and private key pairs on BitGo SDK
@@ -34,9 +33,9 @@ export async function postIndependentKey(
3433
seed,
3534
});
3635
} catch (error: any) {
37-
res.status(error.status || 500).json({
36+
throw {
37+
status: error.status || 500,
3838
message: error.message || 'Failed to post key to KMS',
39-
});
40-
return;
39+
};
4140
}
4241
}

src/api/enclaved/signMultisigTransaction.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import * as express from 'express';
21
import { KmsClient } from '../../kms/kmsClient';
3-
import { BitGo, RequestTracer, TransactionPrebuild } from 'bitgo';
2+
import { RequestTracer, TransactionPrebuild } from 'bitgo';
43
import logger from '../../logger';
4+
import { EnclavedApiSpecRouteRequest } from '../../enclavedBitgoExpress/routers/enclavedApiSpec';
55

66
export async function signMultisigTransaction(
7-
req: express.Request,
8-
res: express.Response,
7+
req: EnclavedApiSpecRouteRequest<'v1.multisig.sign', 'post'>,
98
): Promise<any> {
109
const {
1110
source,
@@ -20,7 +19,7 @@ export async function signMultisigTransaction(
2019
}
2120

2221
const reqId = new RequestTracer();
23-
const bitgo: BitGo = req.body.bitgo;
22+
const bitgo = req.bitgo;
2423
const baseCoin = bitgo.coin(req.params.coin);
2524
const kms = new KmsClient();
2625

@@ -47,18 +46,20 @@ export async function signMultisigTransaction(
4746
const res = await kms.getKey({ pub, source });
4847
prv = res.prv;
4948
} catch (error: any) {
50-
res.status(error.status || 500).json({
49+
throw {
50+
status: error.status || 500,
5151
message: error.message || 'Failed to retrieve key from KMS',
52-
});
53-
return;
52+
};
5453
}
5554

5655
// Sign the transaction using BitGo SDK
5756
const coin = bitgo.coin(req.params.coin);
5857
try {
59-
return await coin.signTransaction({ txPrebuild, prv });
58+
const signedTx = await coin.signTransaction({ txPrebuild, prv });
59+
// The signed transaction format depends on the coin type
60+
return signedTx;
6061
} catch (error) {
61-
console.log('error while signing wallet transaction ', error);
62+
logger.error('error while signing wallet transaction:', error);
6263
throw error;
6364
}
6465
}

src/enclavedApp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export function app(cfg: EnclavedConfig): express.Application {
108108
}
109109

110110
// Setup routes
111-
setupRoutes(app);
111+
setupRoutes(app, cfg);
112112

113113
// Add error handler
114114
app.use(createErrorHandler());
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import * as t from 'io-ts';
2+
import {
3+
apiSpec,
4+
httpRoute,
5+
httpRequest,
6+
HttpResponse,
7+
Method as HttpMethod,
8+
} from '@api-ts/io-ts-http';
9+
import {
10+
createRouter,
11+
type WrappedRouter,
12+
TypedRequestHandler,
13+
} from '@api-ts/typed-express-router';
14+
import { Response } from '@api-ts/response';
15+
import express from 'express';
16+
import { BitGoRequest } from '../../types/request';
17+
import { EnclavedConfig } from '../../types';
18+
import { postIndependentKey } from '../../api/enclaved/postIndependentKey';
19+
import { signMultisigTransaction } from '../../api/enclaved/signMultisigTransaction';
20+
import { prepareBitGo, responseHandler } from '../../shared/middleware';
21+
22+
// Request type for /key/independent endpoint
23+
const IndependentKeyRequest = {
24+
source: t.string,
25+
seed: t.union([t.undefined, t.string]),
26+
};
27+
28+
// Response type for /key/independent endpoint
29+
const IndependentKeyResponse: HttpResponse = {
30+
// TODO: Define proper response type
31+
200: t.any,
32+
500: t.type({
33+
error: t.string,
34+
details: t.string,
35+
}),
36+
};
37+
38+
// Request type for /multisig/sign endpoint
39+
const SignMultisigRequest = {
40+
source: t.string,
41+
pub: t.string,
42+
txPrebuild: t.any, // TransactionPrebuild type from BitGo
43+
};
44+
45+
// Response type for /multisig/sign endpoint
46+
const SignMultisigResponse: HttpResponse = {
47+
// TODO: Define proper response type for signed multisig transaction
48+
200: t.any,
49+
500: t.type({
50+
error: t.string,
51+
details: t.string,
52+
}),
53+
};
54+
55+
// API Specification
56+
export const EnclavedAPiSpec = apiSpec({
57+
'v1.multisig.sign': {
58+
post: httpRoute({
59+
method: 'POST',
60+
path: '/{coin}/multisig/sign',
61+
request: httpRequest({
62+
params: {
63+
coin: t.string,
64+
},
65+
body: SignMultisigRequest,
66+
}),
67+
response: SignMultisigResponse,
68+
description: 'Sign a multisig transaction',
69+
}),
70+
},
71+
'v1.key.independent': {
72+
post: httpRoute({
73+
method: 'POST',
74+
path: '/{coin}/key/independent',
75+
request: httpRequest({
76+
params: {
77+
coin: t.string,
78+
},
79+
body: IndependentKeyRequest,
80+
}),
81+
response: IndependentKeyResponse,
82+
description: 'Generate an independent key',
83+
}),
84+
},
85+
});
86+
87+
export type EnclavedApiSpecRouteHandler<
88+
ApiName extends keyof typeof EnclavedAPiSpec,
89+
Method extends keyof (typeof EnclavedAPiSpec)[ApiName] & HttpMethod,
90+
> = TypedRequestHandler<typeof EnclavedAPiSpec, ApiName, Method>;
91+
92+
export type EnclavedApiSpecRouteRequest<
93+
ApiName extends keyof typeof EnclavedAPiSpec,
94+
Method extends keyof (typeof EnclavedAPiSpec)[ApiName] & HttpMethod,
95+
> = BitGoRequest<EnclavedConfig> & Parameters<EnclavedApiSpecRouteHandler<ApiName, Method>>[0];
96+
97+
export type GenericEnclavedApiSpecRouteRequest = EnclavedApiSpecRouteRequest<any, any>;
98+
99+
// Create router with handlers
100+
export function createKeyGenRouter(config: EnclavedConfig): WrappedRouter<typeof EnclavedAPiSpec> {
101+
const router = createRouter(EnclavedAPiSpec);
102+
// Add middleware
103+
router.use(express.json());
104+
router.use(prepareBitGo(config));
105+
106+
// Independent key generation endpoint handler
107+
router.post('v1.key.independent', [
108+
responseHandler<EnclavedConfig>(async (req) => {
109+
const typedReq = req as EnclavedApiSpecRouteRequest<'v1.key.independent', 'post'>;
110+
const result = await postIndependentKey(typedReq);
111+
return Response.ok(result);
112+
}),
113+
]);
114+
115+
// Multisig transaction signing endpoint handler
116+
router.post('v1.multisig.sign', [
117+
responseHandler<EnclavedConfig>(async (req) => {
118+
const typedReq = req as EnclavedApiSpecRouteRequest<'v1.multisig.sign', 'post'>;
119+
const result = await signMultisigTransaction(typedReq);
120+
return Response.ok(result);
121+
}),
122+
]);
123+
124+
return router;
125+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 pjson from '../../../package.json';
6+
import { responseHandler } from '../../shared/middleware';
7+
8+
// Response type for /ping endpoint
9+
const PingResponse: HttpResponse = {
10+
200: t.type({
11+
status: t.string,
12+
timestamp: t.string,
13+
}),
14+
};
15+
16+
// Response type for /version endpoint
17+
const VersionResponse: HttpResponse = {
18+
200: t.type({
19+
version: t.string,
20+
name: t.string,
21+
}),
22+
};
23+
24+
// API Specification
25+
export const HealthCheckApiSpec = apiSpec({
26+
'v1.health.ping': {
27+
post: httpRoute({
28+
method: 'POST',
29+
path: '/ping',
30+
request: httpRequest({}),
31+
response: PingResponse,
32+
description: 'Health check endpoint that returns server status',
33+
}),
34+
},
35+
'v1.health.version': {
36+
get: httpRoute({
37+
method: 'GET',
38+
path: '/version',
39+
request: httpRequest({}),
40+
response: VersionResponse,
41+
description: 'Returns the current version of the server',
42+
}),
43+
},
44+
});
45+
46+
// Create router with handlers
47+
export function createHealthCheckRouter(): WrappedRouter<typeof HealthCheckApiSpec> {
48+
const router = createRouter(HealthCheckApiSpec);
49+
// Ping endpoint handler
50+
router.post('v1.health.ping', [
51+
responseHandler(() =>
52+
Response.ok({
53+
status: 'enclaved express server is ok!',
54+
timestamp: new Date().toISOString(),
55+
}),
56+
),
57+
]);
58+
59+
// Version endpoint handler
60+
router.get('v1.health.version', [
61+
responseHandler(() =>
62+
Response.ok({
63+
version: pjson.version,
64+
name: pjson.name,
65+
}),
66+
),
67+
]);
68+
69+
return router;
70+
}

src/masterBitgoExpress/generateWallet.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
import { createEnclavedExpressClient } from './enclavedExpressClient';
1212
import _ from 'lodash';
1313
import { MasterApiSpecRouteRequest } from './routers/masterApiSpec';
14+
import { isMasterExpressConfig } from '../types';
15+
import assert from 'assert';
1416

1517
/**
1618
* This route is used to generate a multisig wallet when enclaved express is enabled
@@ -21,6 +23,11 @@ export async function handleGenerateWalletOnPrem(
2123
const bitgo = req.bitgo;
2224
const baseCoin = bitgo.coin(req.params.coin);
2325

26+
assert(
27+
isMasterExpressConfig(req.config),
28+
'Expected req.config to be of type MasterExpressConfig',
29+
);
30+
2431
const enclavedExpressClient = createEnclavedExpressClient(req.config, req.params.coin);
2532
if (!enclavedExpressClient) {
2633
throw new Error(

src/masterBitgoExpress/routers/enclavedExpressHealth.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import https from 'https';
66
import superagent from 'superagent';
77
import { MasterExpressConfig, TlsMode } from '../../config';
88
import logger from '../../logger';
9-
import { withResponseHandler } from '../../shared/responseHandler';
9+
import { responseHandler } from '../../shared/middleware';
1010

1111
// Response type for /ping/enclavedExpress endpoint
1212
const PingEnclavedResponse: HttpResponse = {
@@ -52,7 +52,7 @@ export function createEnclavedExpressRouter(
5252

5353
// Ping endpoint handler
5454
router.post('v1.enclaved.ping', [
55-
withResponseHandler(async () => {
55+
responseHandler(async () => {
5656
logger.debug('Pinging enclaved express');
5757

5858
try {

0 commit comments

Comments
 (0)