Skip to content

Commit cbf3084

Browse files
committed
feat(express): migrated ofcSignPayload to typed routes
TICKET: WP-5443
1 parent 1a34ab8 commit cbf3084

File tree

5 files changed

+125
-18
lines changed

5 files changed

+125
-18
lines changed

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,
@@ -1637,7 +1628,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
16371628
);
16381629

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

16421633
// sign transaction
16431634
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
@@ -16,6 +16,7 @@ import { PostSignTransaction } from './v1/signTransaction';
1616
import { PostKeychainLocal } from './v2/keychainLocal';
1717
import { PostLightningInitWallet } from './v2/lightningInitWallet';
1818
import { PostVerifyCoinAddress } from './v2/verifyAddress';
19+
import { PostOfcSignPayload } from './v2/ofcSignPayload';
1920

2021
export const ExpressApi = apiSpec({
2122
'express.ping': {
@@ -60,6 +61,9 @@ export const ExpressApi = apiSpec({
6061
'express.calculateminerfeeinfo': {
6162
post: PostCalculateMinerFeeInfo,
6263
},
64+
'express.ofc.signPayload': {
65+
post: PostOfcSignPayload,
66+
},
6367
});
6468

6569
export type ExpressApi = typeof ExpressApi;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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. Will always decode into 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+
};
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+
/**
36+
* Request body for signing an arbitrary payload with an OFC wallet key.
37+
*
38+
* @operationId express.ofc.signPayload
39+
*/
40+
export const PostOfcSignPayload = httpRoute({
41+
path: '/api/v2/ofc/signPayload',
42+
method: 'POST',
43+
request: httpRequest({ body: OfcSignPayloadBody }),
44+
response: OfcSignPayloadResponse,
45+
});

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

Lines changed: 37 additions & 3 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,8 +55,12 @@ 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;
63+
} as unknown as ExpressApiRouteRequest<'express.ofc.signPayload', 'post'>;
5864
await handleV2OFCSignPayload(req).should.be.resolvedWith(expectedResponse);
5965
});
6066
it('should return a signed payload with type as json string', async function () {
@@ -68,10 +74,38 @@ describe('Sign an arbitrary payload with trading account key', function () {
6874
payload: stringifiedPayload,
6975
walletId,
7076
},
77+
decoded: {
78+
walletId,
79+
payload,
80+
},
7181
query: {},
72-
} as unknown as Request;
82+
} as unknown as ExpressApiRouteRequest<'express.ofc.signPayload', 'post'>;
7383
await handleV2OFCSignPayload(req).should.be.resolvedWith(expectedResponse);
7484
});
85+
it('should decode handler response with OfcSignPayloadResponse codec', async function () {
86+
const expected = {
87+
payload: JSON.stringify(payload),
88+
signature,
89+
};
90+
const req = {
91+
bitgo: bitGoStub,
92+
body: {
93+
walletId,
94+
payload,
95+
},
96+
decoded: {
97+
walletId,
98+
payload,
99+
},
100+
} as unknown as ExpressApiRouteRequest<'express.ofc.signPayload', 'post'>;
101+
102+
const result = await handleV2OFCSignPayload(req);
103+
decodeOrElse('OfcSignPayloadResponse200', OfcSignPayloadResponse[200], result, (_) => {
104+
throw new Error(`Response did not match expected codec`);
105+
});
106+
result.should.eql(expected);
107+
OfcSignPayloadResponse[200].is(result).should.be.true();
108+
});
75109
});
76110

77111
describe('With the handler to sign an arbitrary payload in external signing mode', () => {

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
LightningInitWalletBody,
1212
LightningInitWalletParams,
1313
} from '../../../src/typedRoutes/api/v2/lightningInitWallet';
14+
import { OfcSignPayloadBody } from '../../../src/typedRoutes/api/v2/ofcSignPayload';
1415

1516
export function assertDecode<T>(codec: t.Type<T, unknown>, input: unknown): T {
1617
const result = codec.decode(input);
@@ -174,4 +175,36 @@ describe('io-ts decode tests', function () {
174175
// valid with expressHost
175176
assertDecode(t.type(LightningInitWalletBody), { passphrase: 'p', expressHost: 'host.example' });
176177
});
178+
it('express.ofc.signPayload', function () {
179+
// missing walletId
180+
assert.throws(() =>
181+
assertDecode(t.type(OfcSignPayloadBody), {
182+
payload: { a: 1 },
183+
})
184+
);
185+
// empty walletId
186+
assert.throws(() =>
187+
assertDecode(t.type(OfcSignPayloadBody), {
188+
walletId: '',
189+
payload: { a: 1 },
190+
})
191+
);
192+
// missing payload
193+
assert.throws(() =>
194+
assertDecode(t.type(OfcSignPayloadBody), {
195+
walletId: 'w1',
196+
})
197+
);
198+
// valid minimal
199+
assertDecode(t.type(OfcSignPayloadBody), {
200+
walletId: 'w1',
201+
payload: { a: 1 },
202+
});
203+
// valid with nested and optional passphrase
204+
assertDecode(t.type(OfcSignPayloadBody), {
205+
walletId: 'w1',
206+
payload: { nested: ['x', { y: true }] },
207+
walletPassphrase: 'secret',
208+
});
209+
});
177210
});

0 commit comments

Comments
 (0)