Skip to content

Commit 761f87f

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

File tree

5 files changed

+179
-7
lines changed

5 files changed

+179
-7
lines changed

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: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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+
200: t.type({
55+
id: t.string,
56+
users: t.array(
57+
t.type({
58+
user: t.string,
59+
permissions: t.array(t.array(t.string)),
60+
}),
61+
),
62+
coin: t.string,
63+
label: t.string,
64+
m: t.number,
65+
n: t.number,
66+
keys: t.array(t.string),
67+
keySignatures: t.type({
68+
backupPub: t.string,
69+
bitgoPub: t.string,
70+
}),
71+
tags: t.array(t.string),
72+
receiveAddress: t.type({
73+
id: t.string,
74+
address: t.string,
75+
chain: t.number,
76+
index: t.number,
77+
coin: t.string,
78+
wallet: t.string,
79+
coinSpecific: t.record(t.string, t.unknown),
80+
label: t.union([t.string, t.null]),
81+
addressType: t.string,
82+
}),
83+
balance: t.number,
84+
balanceString: t.string,
85+
confirmedBalance: t.number,
86+
confirmedBalanceString: t.string,
87+
spendableBalance: t.number,
88+
spendableBalanceString: t.string,
89+
deleted: t.boolean,
90+
isCold: t.boolean,
91+
freeze: t.record(t.string, t.unknown),
92+
disableTransactionNotifications: t.boolean,
93+
admin: t.record(t.string, t.unknown),
94+
approvalsRequired: t.number,
95+
pendingApprovals: t.array(t.unknown),
96+
allowBackupKeySigning: t.boolean,
97+
coinSpecific: t.record(t.string, t.unknown),
98+
clientFlags: t.array(t.string),
99+
recoverable: t.boolean,
100+
startDate: t.string,
101+
hasLargeNumberOfAddresses: t.boolean,
102+
config: t.record(t.string, t.unknown),
103+
}),
104+
};
105+
106+
// Request type for /generate endpoint
107+
const GenerateWalletRequest = {
108+
label: t.string,
109+
multisigType: t.union([t.undefined, t.literal('onchain'), t.literal('tss'), t.literal('blsdkg')]),
110+
type: t.union([t.undefined, t.literal('hot'), t.literal('cold'), t.literal('custodial')]),
111+
passphrase: t.union([t.undefined, t.string]),
112+
userKey: t.union([t.undefined, t.string]),
113+
backupXpub: t.union([t.undefined, t.string]),
114+
backupXpubProvider: t.union([t.undefined, t.literal('dai')]),
115+
enterprise: t.string,
116+
disableTransactionNotifications: t.union([t.undefined, t.boolean]),
117+
passcodeEncryptionCode: t.union([t.undefined, t.string]),
118+
coldDerivationSeed: t.union([t.undefined, t.string]),
119+
gasPrice: t.union([t.undefined, t.number]),
120+
disableKRSEmail: t.union([t.undefined, t.boolean]),
121+
walletVersion: t.union([t.undefined, t.number]),
122+
isDistributedCustody: t.union([t.undefined, t.boolean]),
123+
bitgoKeyId: t.union([t.undefined, t.string]),
124+
commonKeychain: t.union([t.undefined, t.string]),
125+
};
126+
127+
// API Specification
128+
export const MultiSigApiSpec = apiSpec({
129+
'v1.wallet.generate': {
130+
post: httpRoute({
131+
method: 'POST',
132+
path: '/api/{coin}/wallet/generate',
133+
request: httpRequest({
134+
params: {
135+
coin: t.string,
136+
},
137+
body: GenerateWalletRequest,
138+
}),
139+
response: GenerateWalletResponse,
140+
description: 'Generate a new wallet',
141+
}),
142+
},
143+
});
144+
145+
// Create router with handlers
146+
export function createMultiSigRouter(
147+
cfg: MasterExpressConfig,
148+
): WrappedRouter<typeof MultiSigApiSpec> {
149+
const router = createRouter(MultiSigApiSpec);
150+
151+
// Add middleware to all routes
152+
router.use(parseBody);
153+
router.use(prepareBitGo(cfg));
154+
155+
// Generate wallet endpoint handler
156+
router.post('v1.wallet.generate', [
157+
withResponseHandler(async (req: BitGoRequest | Request) => {
158+
if (!isBitGoRequest(req)) {
159+
throw new Error('Invalid request type');
160+
}
161+
const result = await handleGenerateWalletOnPrem(req);
162+
return Response.ok(result);
163+
}),
164+
]);
165+
166+
return router;
167+
}

src/routes/master.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import express from 'express';
22
import { BitGo, BitGoOptions } from 'bitgo';
3-
import { BitGoBase } from '@bitgo/sdk-core';
43
import { version } from 'bitgo/package.json';
54
import pjson from '../../package.json';
65
import { MasterExpressConfig } from '../config';
@@ -55,7 +54,7 @@ function prepareBitGo(config: MasterExpressConfig) {
5554
userAgent,
5655
};
5756

58-
(req as BitGoRequest).bitgo = new BitGo(bitgoConstructorParams) as unknown as BitGoBase;
57+
(req as BitGoRequest).bitgo = new BitGo(bitgoConstructorParams);
5958
(req as BitGoRequest).config = config;
6059

6160
next();

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)