Skip to content

Commit 8eec0e6

Browse files
feature(express): migrated ofcSignPayload to typed routes
2 parents 9951417 + c2d1287 commit 8eec0e6

File tree

6 files changed

+135
-20
lines changed

6 files changed

+135
-20
lines changed

modules/express/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"dotenv": "^16.0.0",
5151
"express": "4.21.2",
5252
"io-ts": "npm:@bitgo-forks/[email protected]",
53+
"io-ts-types": "^0.5.19",
5354
"lodash": "^4.17.20",
5455
"morgan": "^1.9.1",
5556
"proxy-agent": "6.4.0",

modules/express/src/clientRoutes.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ import { handleLightningWithdraw } from './lightning/lightningWithdrawRoutes';
6262
import createExpressRouter from './typedRoutes';
6363
import { ExpressApiRouteRequest } from './typedRoutes/api';
6464
import { TypedRequestHandler, WrappedRequest, WrappedResponse } from '@api-ts/typed-express-router';
65-
import { isJsonString } from './utils';
6665

6766
const { version } = require('bitgo/package.json');
6867
const pjson = require('../package.json');
@@ -590,10 +589,10 @@ export async function handleV2OFCSignPayloadInExtSigningMode(
590589
}
591590
}
592591

593-
export async function handleV2OFCSignPayload(req: express.Request): Promise<{ payload: string; signature: string }> {
594-
const walletId = req.body.walletId;
595-
const payload = req.body.payload;
596-
const bodyWalletPassphrase = req.body.walletPassphrase;
592+
export async function handleV2OFCSignPayload(
593+
req: ExpressApiRouteRequest<'express.ofc.signPayload', 'post'>
594+
): Promise<{ payload: string; signature: string }> {
595+
const { walletId, payload, walletPassphrase: bodyWalletPassphrase } = req.decoded;
597596
const ofcCoinName = 'ofc';
598597

599598
// If the externalSignerUrl is set, forward the request to the express server hosted on the externalSignerUrl
@@ -612,14 +611,6 @@ export async function handleV2OFCSignPayload(req: express.Request): Promise<{ pa
612611
return payloadWithSignature;
613612
}
614613

615-
if (!payload) {
616-
throw new ApiResponseError('Missing required field: payload', 400);
617-
}
618-
619-
if (!walletId) {
620-
throw new ApiResponseError('Missing required field: walletId', 400);
621-
}
622-
623614
const bitgo = req.bitgo;
624615

625616
// This is to set us up for multiple trading accounts per enterprise
@@ -631,7 +622,7 @@ export async function handleV2OFCSignPayload(req: express.Request): Promise<{ pa
631622

632623
const walletPassphrase = bodyWalletPassphrase || getWalletPwFromEnv(wallet.id());
633624
const tradingAccount = wallet.toTradingAccount();
634-
const stringifiedPayload = isJsonString(req.body.payload) ? req.body.payload : JSON.stringify(req.body.payload);
625+
const stringifiedPayload = JSON.stringify(payload);
635626
const signature = await tradingAccount.signPayload({
636627
payload: stringifiedPayload,
637628
walletPassphrase,
@@ -1639,7 +1630,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
16391630
);
16401631

16411632
// sign arbitrary payloads w/ trading account key
1642-
app.post(`/api/v2/ofc/signPayload`, parseBody, prepareBitGo(config), promiseWrapper(handleV2OFCSignPayload));
1633+
router.post('express.ofc.signPayload', [prepareBitGo(config), typedPromiseWrapper(handleV2OFCSignPayload)]);
16431634

16441635
// sign transaction
16451636
app.post('/api/v2/:coin/signtx', parseBody, prepareBitGo(config), promiseWrapper(handleV2SignTx));

modules/express/src/typedRoutes/api/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { PostCreateLocalKeyChain } from './v1/createLocalKeyChain';
2323
import { PutConstructPendingApprovalTx } from './v1/constructPendingApprovalTx';
2424
import { PutConsolidateUnspents } from './v1/consolidateUnspents';
2525
import { PutFanoutUnspents } from './v1/fanoutUnspents';
26+
import { PostOfcSignPayload } from './v2/ofcSignPayload';
2627

2728
export const ExpressApi = apiSpec({
2829
'express.ping': {
@@ -88,6 +89,9 @@ export const ExpressApi = apiSpec({
8889
'express.v1.wallet.fanoutunspents': {
8990
put: PutFanoutUnspents,
9091
},
92+
'express.ofc.signPayload': {
93+
post: PostOfcSignPayload,
94+
},
9195
});
9296

9397
export type ExpressApi = typeof ExpressApi;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import * as t from 'io-ts';
2+
import { Json, NonEmptyString, JsonFromString } from 'io-ts-types';
3+
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
4+
import { BitgoExpressError } from '../../schemas/error';
5+
6+
/**
7+
* Sign an arbitrary payload using an OFC trading account key.
8+
*/
9+
export const OfcSignPayloadBody = {
10+
/** The ID of the OFC wallet to sign the payload with. */
11+
walletId: NonEmptyString,
12+
/** The payload to sign. The input can either be a stringified JSON, or a JSON object. */
13+
payload: t.union([Json, t.string.pipe(JsonFromString)]),
14+
/** The passphrase to decrypt the user key. */
15+
walletPassphrase: optional(t.string),
16+
} as const;
17+
18+
export const OfcSignPayloadResponse200 = t.type({
19+
/** The string form of the JSON payload that was signed. */
20+
payload: t.string,
21+
/** Hex-encoded signature generated by the trading account key. */
22+
signature: t.string,
23+
});
24+
25+
/**
26+
* Response for signing an arbitrary payload with an OFC wallet key.
27+
*/
28+
export const OfcSignPayloadResponse = {
29+
/** Signed payload and signature */
30+
200: OfcSignPayloadResponse200,
31+
/** BitGo Express error payload. */
32+
400: BitgoExpressError,
33+
} as const;
34+
35+
/** Sign payload with an OFC wallet key.
36+
* Signs an arbitrary payload with an OFC wallet key.
37+
*
38+
* @tag express
39+
* @operationId express.ofc.signPayload
40+
*/
41+
export const PostOfcSignPayload = httpRoute({
42+
path: '/api/v2/ofc/signPayload',
43+
method: 'POST',
44+
request: httpRequest({ body: OfcSignPayloadBody }),
45+
response: OfcSignPayloadResponse,
46+
});

modules/express/test/unit/clientRoutes/signPayload.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import 'should';
55
import * as fs from 'fs';
66
import { Request } from 'express';
77
import { BitGo, Coin, BaseCoin, Wallet, Wallets } from 'bitgo';
8-
98
import '../../lib/asserts';
109
import { handleV2OFCSignPayload, handleV2OFCSignPayloadInExtSigningMode } from '../../../src/clientRoutes';
10+
import { ExpressApiRouteRequest } from '../../../src/typedRoutes/api';
11+
import { OfcSignPayloadResponse } from '../../../src/typedRoutes/api/v2/ofcSignPayload';
12+
import { decodeOrElse } from '@bitgo/sdk-core';
1113

1214
describe('Sign an arbitrary payload with trading account key', function () {
1315
const coin = 'ofc';
@@ -53,9 +55,16 @@ describe('Sign an arbitrary payload with trading account key', function () {
5355
payload,
5456
walletId,
5557
},
58+
decoded: {
59+
walletId,
60+
payload,
61+
},
5662
query: {},
57-
} as unknown as Request;
58-
await handleV2OFCSignPayload(req).should.be.resolvedWith(expectedResponse);
63+
} as unknown as ExpressApiRouteRequest<'express.ofc.signPayload', 'post'>;
64+
const result = await handleV2OFCSignPayload(req).should.be.resolvedWith(expectedResponse);
65+
decodeOrElse('OfcSignPayloadResponse200', OfcSignPayloadResponse[200], result, (_) => {
66+
throw new Error(`Response did not match expected codec`);
67+
});
5968
});
6069
it('should return a signed payload with type as json string', async function () {
6170
const expectedResponse = {
@@ -68,9 +77,40 @@ describe('Sign an arbitrary payload with trading account key', function () {
6877
payload: stringifiedPayload,
6978
walletId,
7079
},
80+
decoded: {
81+
walletId,
82+
payload,
83+
},
7184
query: {},
72-
} as unknown as Request;
73-
await handleV2OFCSignPayload(req).should.be.resolvedWith(expectedResponse);
85+
} as unknown as ExpressApiRouteRequest<'express.ofc.signPayload', 'post'>;
86+
const result = await handleV2OFCSignPayload(req).should.be.resolvedWith(expectedResponse);
87+
decodeOrElse('OfcSignPayloadResponse200', OfcSignPayloadResponse[200], result, (_) => {
88+
throw new Error(`Response did not match expected codec`);
89+
});
90+
});
91+
it('should decode handler response with OfcSignPayloadResponse codec', async function () {
92+
const expected = {
93+
payload: JSON.stringify(payload),
94+
signature,
95+
};
96+
const req = {
97+
bitgo: bitGoStub,
98+
body: {
99+
walletId,
100+
payload,
101+
},
102+
decoded: {
103+
walletId,
104+
payload,
105+
},
106+
} as unknown as ExpressApiRouteRequest<'express.ofc.signPayload', 'post'>;
107+
108+
const result = await handleV2OFCSignPayload(req);
109+
decodeOrElse('OfcSignPayloadResponse200', OfcSignPayloadResponse[200], result, (_) => {
110+
throw new Error(`Response did not match expected codec`);
111+
});
112+
result.should.eql(expected);
113+
OfcSignPayloadResponse[200].is(result).should.be.true();
74114
});
75115
});
76116

modules/express/test/unit/typedRoutes/decode.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
LightningInitWalletParams,
1414
} from '../../../src/typedRoutes/api/v2/lightningInitWallet';
1515
import { UnlockLightningWalletBody, UnlockLightningWalletParams } from '../../../src/typedRoutes/api/v2/unlockWallet';
16+
import { OfcSignPayloadBody } from '../../../src/typedRoutes/api/v2/ofcSignPayload';
1617

1718
export function assertDecode<T>(codec: t.Type<T, unknown>, input: unknown): T {
1819
const result = codec.decode(input);
@@ -190,4 +191,36 @@ describe('io-ts decode tests', function () {
190191
// valid body
191192
assertDecode(t.type(UnlockLightningWalletBody), { passphrase: 'secret' });
192193
});
194+
it('express.ofc.signPayload', function () {
195+
// missing walletId
196+
assert.throws(() =>
197+
assertDecode(t.type(OfcSignPayloadBody), {
198+
payload: { a: 1 },
199+
})
200+
);
201+
// empty walletId
202+
assert.throws(() =>
203+
assertDecode(t.type(OfcSignPayloadBody), {
204+
walletId: '',
205+
payload: { a: 1 },
206+
})
207+
);
208+
// missing payload
209+
assert.throws(() =>
210+
assertDecode(t.type(OfcSignPayloadBody), {
211+
walletId: 'w1',
212+
})
213+
);
214+
// valid minimal
215+
assertDecode(t.type(OfcSignPayloadBody), {
216+
walletId: 'w1',
217+
payload: { a: 1 },
218+
});
219+
// valid with nested and optional passphrase
220+
assertDecode(t.type(OfcSignPayloadBody), {
221+
walletId: 'w1',
222+
payload: { nested: ['x', { y: true }] },
223+
walletPassphrase: 'secret',
224+
});
225+
});
193226
});

0 commit comments

Comments
 (0)