Skip to content

Commit 1bf290e

Browse files
committed
feat(mbe): api to sign and send unsigned txrequest
Ticket: WP-5238
1 parent f4288f9 commit 1bf290e

File tree

3 files changed

+301
-3
lines changed

3 files changed

+301
-3
lines changed

masterBitgoExpress.json

Lines changed: 201 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,120 @@
66
"description": "BitGo Enclaved Express - Secure enclave for BitGo signing operations with mTLS"
77
},
88
"paths": {
9+
"/api/{coin}/wallet/{walletId}/accelerate": {
10+
"post": {
11+
"parameters": [
12+
{
13+
"name": "walletId",
14+
"in": "path",
15+
"required": true,
16+
"schema": {
17+
"type": "string"
18+
}
19+
},
20+
{
21+
"name": "coin",
22+
"in": "path",
23+
"required": true,
24+
"schema": {
25+
"type": "string"
26+
}
27+
}
28+
],
29+
"requestBody": {
30+
"content": {
31+
"application/json": {
32+
"schema": {
33+
"type": "object",
34+
"properties": {
35+
"pubkey": {
36+
"type": "string"
37+
},
38+
"source": {
39+
"type": "string",
40+
"enum": [
41+
"user",
42+
"backup"
43+
]
44+
},
45+
"cpfpTxIds": {
46+
"type": "array",
47+
"items": {
48+
"type": "string"
49+
}
50+
},
51+
"cpfpFeeRate": {
52+
"type": "number"
53+
},
54+
"maxFee": {
55+
"type": "number"
56+
},
57+
"rbfTxIds": {
58+
"type": "array",
59+
"items": {
60+
"type": "string"
61+
}
62+
},
63+
"feeMultiplier": {
64+
"type": "number"
65+
}
66+
},
67+
"required": [
68+
"pubkey",
69+
"source"
70+
]
71+
}
72+
}
73+
}
74+
},
75+
"responses": {
76+
"200": {
77+
"description": "OK",
78+
"content": {
79+
"application/json": {
80+
"schema": {
81+
"type": "object",
82+
"properties": {
83+
"txid": {
84+
"type": "string"
85+
},
86+
"tx": {
87+
"type": "string"
88+
}
89+
},
90+
"required": [
91+
"txid",
92+
"tx"
93+
]
94+
}
95+
}
96+
}
97+
},
98+
"500": {
99+
"description": "Internal Server Error",
100+
"content": {
101+
"application/json": {
102+
"schema": {
103+
"type": "object",
104+
"properties": {
105+
"error": {
106+
"type": "string"
107+
},
108+
"details": {
109+
"type": "string"
110+
}
111+
},
112+
"required": [
113+
"error",
114+
"details"
115+
]
116+
}
117+
}
118+
}
119+
}
120+
}
121+
}
122+
},
9123
"/api/{coin}/wallet/{walletId}/consolidate": {
10124
"post": {
11125
"parameters": [
@@ -142,9 +256,6 @@
142256
"backup"
143257
]
144258
},
145-
"walletPassphrase": {
146-
"type": "string"
147-
},
148259
"feeRate": {
149260
"type": "number"
150261
},
@@ -463,6 +574,92 @@
463574
}
464575
}
465576
},
577+
"/api/{coin}/wallet/{walletId}/txrequest/{txRequestId}/signAndSend": {
578+
"post": {
579+
"parameters": [
580+
{
581+
"name": "walletId",
582+
"in": "path",
583+
"required": true,
584+
"schema": {
585+
"type": "string"
586+
}
587+
},
588+
{
589+
"name": "coin",
590+
"in": "path",
591+
"required": true,
592+
"schema": {
593+
"type": "string"
594+
}
595+
},
596+
{
597+
"name": "txRequestId",
598+
"in": "path",
599+
"required": true,
600+
"schema": {
601+
"type": "string"
602+
}
603+
}
604+
],
605+
"requestBody": {
606+
"content": {
607+
"application/json": {
608+
"schema": {
609+
"type": "object",
610+
"properties": {
611+
"source": {
612+
"type": "string",
613+
"enum": [
614+
"user",
615+
"backup"
616+
]
617+
},
618+
"commonKeychain": {
619+
"type": "string"
620+
}
621+
},
622+
"required": [
623+
"source"
624+
]
625+
}
626+
}
627+
}
628+
},
629+
"responses": {
630+
"200": {
631+
"description": "OK",
632+
"content": {
633+
"application/json": {
634+
"schema": {}
635+
}
636+
}
637+
},
638+
"500": {
639+
"description": "Internal Server Error",
640+
"content": {
641+
"application/json": {
642+
"schema": {
643+
"type": "object",
644+
"properties": {
645+
"error": {
646+
"type": "string"
647+
},
648+
"details": {
649+
"type": "string"
650+
}
651+
},
652+
"required": [
653+
"error",
654+
"details"
655+
]
656+
}
657+
}
658+
}
659+
}
660+
}
661+
}
662+
},
466663
"/api/{coin}/wallet/generate": {
467664
"post": {
468665
"parameters": [
@@ -503,6 +700,7 @@
503700
},
504701
"required": [
505702
"label",
703+
"multisigType",
506704
"enterprise"
507705
]
508706
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { getTxRequest, KeyIndices, RequestTracer } from '@bitgo/sdk-core';
2+
import logger from '../../../logger';
3+
import { signAndSendTxRequests } from './transactionRequests';
4+
import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec';
5+
6+
export async function handleSignAndSendTxRequest(
7+
req: MasterApiSpecRouteRequest<'v1.wallet.txrequest.signAndSend', 'post'>,
8+
) {
9+
const enclavedExpressClient = req.enclavedExpressClient;
10+
const reqId = new RequestTracer();
11+
const bitgo = req.bitgo;
12+
const baseCoin = bitgo.coin(req.params.coin);
13+
14+
const params = req.decoded;
15+
16+
const walletId = req.params.walletId;
17+
const wallet = await baseCoin.wallets().get({ id: walletId, reqId });
18+
if (!wallet) {
19+
throw new Error(`Wallet ${walletId} not found`);
20+
}
21+
22+
if (wallet.type() !== 'cold' || wallet.subType() !== 'onPrem') {
23+
throw new Error('Wallet is not an on-prem wallet');
24+
}
25+
26+
const keyIdIndex = params.source === 'user' ? KeyIndices.USER : KeyIndices.BACKUP;
27+
logger.info(`Key ID index: ${keyIdIndex}`);
28+
logger.info(`Key IDs: ${JSON.stringify(wallet.keyIds(), null, 2)}`);
29+
30+
// Get the signing keychain
31+
const signingKeychain = await baseCoin.keychains().get({
32+
id: wallet.keyIds()[keyIdIndex],
33+
});
34+
35+
if (!signingKeychain) {
36+
throw new Error(`Signing keychain for ${params.source} not found`);
37+
}
38+
if (params.commonKeychain && signingKeychain.commonKeychain !== params.commonKeychain) {
39+
throw new Error(
40+
`Common keychain provided does not match the keychain on wallet for ${params.source}`,
41+
);
42+
}
43+
44+
logger.debug(`Signing keychain: ${JSON.stringify(signingKeychain, null, 2)}`);
45+
logger.debug(`Params: ${JSON.stringify(params, null, 2)}`);
46+
47+
const txRequest = await getTxRequest(bitgo, wallet.id(), params.txRequestId, reqId);
48+
if (!txRequest) {
49+
throw new Error(`TxRequest ${params.txRequestId} not found`);
50+
}
51+
52+
logger.debug(`TxRequest: ${JSON.stringify(txRequest, null, 2)}`);
53+
54+
return signAndSendTxRequests(
55+
bitgo,
56+
wallet,
57+
txRequest,
58+
enclavedExpressClient,
59+
signingKeychain,
60+
reqId,
61+
);
62+
}

src/api/master/routers/masterApiSpec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { handleRecoveryWalletOnPrem } from '../handlers/recoveryWallet';
2424
import { handleConsolidate } from '../handlers/handleConsolidate';
2525
import { handleAccelerate } from '../handlers/handleAccelerate';
2626
import { handleConsolidateUnspents } from '../handlers/handleConsolidateUnspents';
27+
import { handleSignAndSendTxRequest } from '../handlers/handleSignAndSendTxRequest';
2728

2829
// Middleware functions
2930
export function parseBody(req: express.Request, res: express.Response, next: express.NextFunction) {
@@ -218,6 +219,19 @@ const ConsolidateUnspentsResponse: HttpResponse = {
218219
}),
219220
};
220221

222+
const SignMpcRequest = {
223+
source: t.union([t.literal('user'), t.literal('backup')]),
224+
commonKeychain: t.union([t.undefined, t.string]),
225+
};
226+
227+
const SignMpcResponse: HttpResponse = {
228+
200: t.any,
229+
500: t.type({
230+
error: t.string,
231+
details: t.string,
232+
}),
233+
};
234+
221235
// API Specification
222236
export const MasterApiSpec = apiSpec({
223237
'v1.wallet.generate': {
@@ -249,6 +263,22 @@ export const MasterApiSpec = apiSpec({
249263
description: 'Send many transactions',
250264
}),
251265
},
266+
'v1.wallet.txrequest.signAndSend': {
267+
post: httpRoute({
268+
method: 'POST',
269+
path: '/api/{coin}/wallet/{walletId}/txrequest/{txRequestId}/signAndSend',
270+
request: httpRequest({
271+
params: {
272+
walletId: t.string,
273+
coin: t.string,
274+
txRequestId: t.string,
275+
},
276+
body: SignMpcRequest,
277+
}),
278+
response: SignMpcResponse,
279+
description: 'Sign MPC with TxRequest',
280+
}),
281+
},
252282
'v1.wallet.recovery': {
253283
post: httpRoute({
254284
method: 'POST',
@@ -384,5 +414,13 @@ export function createMasterApiRouter(
384414
}),
385415
]);
386416

417+
router.post('v1.wallet.txrequest.signAndSend', [
418+
responseHandler<MasterExpressConfig>(async (req: express.Request) => {
419+
const typedReq = req as GenericMasterApiSpecRouteRequest;
420+
const result = await handleSignAndSendTxRequest(typedReq);
421+
return Response.ok(result);
422+
}),
423+
]);
424+
387425
return router;
388426
}

0 commit comments

Comments
 (0)