Skip to content

Commit c87ec62

Browse files
mrdanish26alextse-bg
authored andcommitted
feat(express): support optional passphrase in handleV2OFCSignPayload
When no walletPassphrase is present in the request body or environment, pass undefined to tradingAccount.signPayload() instead of throwing. The SDK routes passphrase-less signing through KMS internally. Ticket: WCN-215-1
1 parent c5a372d commit c87ec62

2 files changed

Lines changed: 105 additions & 1 deletion

File tree

modules/express/src/clientRoutes.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,15 @@ function getWalletPwFromEnv(walletId: string): string {
403403
return walletPw;
404404
}
405405

406+
/**
407+
* Returns the wallet passphrase from the environment, or undefined if not set.
408+
* Unlike getWalletPwFromEnv, this does not throw when the env variable is absent.
409+
* Use this when the passphrase is optional (e.g. KMS-backed wallets).
410+
*/
411+
function findWalletPwFromEnv(walletId: string): string | undefined {
412+
return process.env[`WALLET_${walletId}_PASSPHRASE`];
413+
}
414+
406415
async function getEncryptedPrivKey(path: string, walletId: string): Promise<string> {
407416
const privKeyFile = await fs.readFile(path, { encoding: 'utf8' });
408417
const encryptedPrivKey = JSON.parse(privKeyFile);
@@ -629,7 +638,9 @@ export async function handleV2OFCSignPayload(
629638
throw new ApiResponseError(`Could not find OFC wallet ${walletId}`, 404);
630639
}
631640

632-
const walletPassphrase = bodyWalletPassphrase || getWalletPwFromEnv(wallet.id());
641+
// Prefer the passphrase from the request body; fall back to the env var.
642+
// If neither is present, pass undefined — signPayload() routes to KMS internally.
643+
const walletPassphrase = bodyWalletPassphrase ?? findWalletPwFromEnv(wallet.id());
633644
const tradingAccount = wallet.toTradingAccount();
634645
const stringifiedPayload = typeof payload === 'string' ? payload : JSON.stringify(payload);
635646
const signature = await tradingAccount.signPayload({

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

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,103 @@ describe('OfcSignPayload codec tests', function () {
223223
const decodedResponse = assertDecode(OfcSignPayloadResponse200, result.body);
224224
assert.strictEqual(decodedResponse.signature, mockSignPayloadResponse.signature);
225225

226+
// Verify env passphrase was forwarded to signPayload
227+
const signCall = mockTradingAccount.signPayload.getCall(0);
228+
assert.ok(signCall, 'tradingAccount.signPayload should have been called');
229+
assert.strictEqual(signCall.args[0].walletPassphrase, 'env_passphrase', 'env passphrase should be forwarded');
230+
226231
// Cleanup environment variable
227232
delete process.env['WALLET_ofc-wallet-id-123_PASSPHRASE'];
228233
});
229234

235+
it('should pass undefined walletPassphrase to signPayload when no passphrase in body or env (KMS path)', async function () {
236+
const requestBody = {
237+
walletId: 'ofc-wallet-id-no-passphrase',
238+
payload: { amount: '1000000', currency: 'USD' },
239+
// no walletPassphrase
240+
};
241+
242+
// Ensure no env var is set for this wallet
243+
delete process.env['WALLET_ofc-wallet-id-no-passphrase_PASSPHRASE'];
244+
245+
const mockTradingAccount = {
246+
signPayload: sinon.stub().resolves(mockSignPayloadResponse.signature),
247+
};
248+
249+
const mockWallet = {
250+
id: () => requestBody.walletId,
251+
toTradingAccount: sinon.stub().returns(mockTradingAccount),
252+
};
253+
254+
const walletsGetStub = sinon.stub().resolves(mockWallet);
255+
const mockWallets = { get: walletsGetStub };
256+
const mockCoin = { wallets: sinon.stub().returns(mockWallets) };
257+
sinon.stub(BitGo.prototype, 'coin').returns(mockCoin as any);
258+
259+
const result = await agent
260+
.post('/api/v2/ofc/signPayload')
261+
.set('Authorization', 'Bearer test_access_token_12345')
262+
.set('Content-Type', 'application/json')
263+
.send(requestBody);
264+
265+
assert.strictEqual(result.status, 200);
266+
const decodedResponse = assertDecode(OfcSignPayloadResponse200, result.body);
267+
assert.strictEqual(decodedResponse.signature, mockSignPayloadResponse.signature);
268+
269+
// signPayload must be called with walletPassphrase=undefined so the SDK routes to KMS
270+
const signCall = mockTradingAccount.signPayload.getCall(0);
271+
assert.ok(signCall, 'tradingAccount.signPayload should have been called');
272+
assert.strictEqual(
273+
signCall.args[0].walletPassphrase,
274+
undefined,
275+
'walletPassphrase should be undefined to trigger KMS signing'
276+
);
277+
});
278+
279+
it('should prefer body walletPassphrase over env passphrase', async function () {
280+
const requestBody = {
281+
walletId: 'ofc-wallet-id-123',
282+
payload: { amount: '500' },
283+
walletPassphrase: 'body_passphrase',
284+
};
285+
286+
// Set a different env passphrase — body should win
287+
process.env['WALLET_ofc-wallet-id-123_PASSPHRASE'] = 'env_passphrase';
288+
289+
const mockTradingAccount = {
290+
signPayload: sinon.stub().resolves(mockSignPayloadResponse.signature),
291+
};
292+
293+
const mockWallet = {
294+
id: () => requestBody.walletId,
295+
toTradingAccount: sinon.stub().returns(mockTradingAccount),
296+
};
297+
298+
const walletsGetStub = sinon.stub().resolves(mockWallet);
299+
const mockWallets = { get: walletsGetStub };
300+
const mockCoin = { wallets: sinon.stub().returns(mockWallets) };
301+
sinon.stub(BitGo.prototype, 'coin').returns(mockCoin as any);
302+
303+
const result = await agent
304+
.post('/api/v2/ofc/signPayload')
305+
.set('Authorization', 'Bearer test_access_token_12345')
306+
.set('Content-Type', 'application/json')
307+
.send(requestBody);
308+
309+
assert.strictEqual(result.status, 200);
310+
311+
// body passphrase should take precedence
312+
const signCall = mockTradingAccount.signPayload.getCall(0);
313+
assert.ok(signCall, 'tradingAccount.signPayload should have been called');
314+
assert.strictEqual(
315+
signCall.args[0].walletPassphrase,
316+
'body_passphrase',
317+
'body passphrase should take precedence over env'
318+
);
319+
320+
delete process.env['WALLET_ofc-wallet-id-123_PASSPHRASE'];
321+
});
322+
230323
it('should successfully sign complex nested JSON payload', async function () {
231324
const requestBody = {
232325
walletId: 'ofc-wallet-id-123',

0 commit comments

Comments
 (0)