Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 10 additions & 16 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1031,27 +1031,23 @@ async function handleWalletUpdate(req: express.Request): Promise<unknown> {
* Changes a keychain's passphrase, re-encrypting the key to a new password
* @param req
*/
export async function handleKeychainChangePassword(req: express.Request): Promise<unknown> {
const { oldPassword, newPassword, otp } = req.body;
if (!oldPassword || !newPassword) {
throw new ApiResponseError('Missing 1 or more required fields: [oldPassword, newPassword]', 400);
}
export async function handleKeychainChangePassword(
req: ExpressApiRouteRequest<'express.keychain.changePassword', 'post'>
): Promise<unknown> {
const { oldPassword, newPassword, otp, coin: coinName, id } = req.decoded;
const reqId = new RequestTracer();

const bitgo = req.bitgo;
const coin = bitgo.coin(req.params.coin);
const coin = bitgo.coin(coinName);

if (otp) {
await bitgo.unlock({ otp });
}

const keychain = await coin.keychains().get({
id: req.params.id,
id: id,
reqId,
});
if (!keychain) {
throw new ApiResponseError(`Keychain ${req.params.id} not found`, 404);
}

const updatedKeychain = coin.keychains().updateSingleKeychainPassword({
keychain,
Expand Down Expand Up @@ -1586,6 +1582,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
);

router.post('express.v1.wallet.signTransaction', [prepareBitGo(config), typedPromiseWrapper(handleSignTransaction)]);
router.get('express.lightning.getState', [prepareBitGo(config), typedPromiseWrapper(handleGetLightningWalletState)]);

app.post('/api/v1/wallet/:id/simpleshare', parseBody, prepareBitGo(config), promiseWrapper(handleShareWallet));
router.post('express.v1.wallet.acceptShare', [prepareBitGo(config), typedPromiseWrapper(handleAcceptShare)]);
Expand Down Expand Up @@ -1621,12 +1618,10 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
app.put('/express/api/v2/:coin/wallet/:id', parseBody, prepareBitGo(config), promiseWrapper(handleWalletUpdate));

// change wallet passphrase
app.post(
'/api/v2/:coin/keychain/:id/changepassword',
parseBody,
router.post('express.keychain.changePassword', [
prepareBitGo(config),
promiseWrapper(handleKeychainChangePassword)
);
typedPromiseWrapper(handleKeychainChangePassword),
]);

// create address
app.post('/api/v2/:coin/wallet/:id/address', parseBody, prepareBitGo(config), promiseWrapper(handleV2CreateAddress));
Expand Down Expand Up @@ -1787,5 +1782,4 @@ export function setupLightningSignerNodeRoutes(app: express.Application, config:
prepareBitGo(config),
promiseWrapper(handleCreateSignerMacaroon)
);
app.get('/api/v2/:coin/wallet/:id/state', prepareBitGo(config), promiseWrapper(handleGetLightningWalletState));
}
10 changes: 4 additions & 6 deletions modules/express/src/lightning/lightningSignerRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,15 +170,13 @@ export async function handleCreateSignerMacaroon(req: express.Request): Promise<
/**
* Handle the request to get the state of a wallet from the signer.
*/
export async function handleGetLightningWalletState(req: express.Request): Promise<GetWalletStateResponse> {
const coinName = req.params.coin;
export async function handleGetLightningWalletState(
req: ExpressApiRouteRequest<'express.lightning.getState', 'get'>
): Promise<GetWalletStateResponse> {
const { coin: coinName, walletId } = req.decoded;
if (!isLightningCoinName(coinName)) {
throw new ApiResponseError(`Invalid coin to get lightning wallet state: ${coinName}`, 400);
}
const walletId = req.params.id;
if (typeof walletId !== 'string') {
throw new ApiResponseError(`Invalid wallet id: ${walletId}`, 400);
}

const lndSignerClient = await LndSignerClient.create(walletId, req.config);
return await lndSignerClient.getWalletState();
Expand Down
8 changes: 8 additions & 0 deletions modules/express/src/typedRoutes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { PostSimpleCreate } from './v1/simpleCreate';
import { PutPendingApproval } from './v1/pendingApproval';
import { PostSignTransaction } from './v1/signTransaction';
import { PostKeychainLocal } from './v2/keychainLocal';
import { PostKeychainChangePassword } from './v2/keychainChangePassword';
import { GetLightningState } from './v2/lightningState';
import { PostLightningInitWallet } from './v2/lightningInitWallet';
import { PostUnlockLightningWallet } from './v2/unlockWallet';
import { PostVerifyCoinAddress } from './v2/verifyAddress';
Expand Down Expand Up @@ -53,6 +55,12 @@ export const ExpressApi = apiSpec({
'express.keychain.local': {
post: PostKeychainLocal,
},
'express.keychain.changePassword': {
post: PostKeychainChangePassword,
},
'express.lightning.getState': {
get: GetLightningState,
},
'express.lightning.initWallet': {
post: PostLightningInitWallet,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
import { BitgoExpressError } from '../../schemas/error';

/**
* Path parameters for changing a keychain's password
*/
export const KeychainChangePasswordParams = {
/** Coin identifier (e.g. btc, tbtc, eth) */
coin: t.string,
/** The keychain id */
id: t.string,
} as const;

/**
* Request body for changing a keychain's password
*/
export const KeychainChangePasswordBody = {
/** The old password used to encrypt the keychain */
oldPassword: t.string,
/** The new password to re-encrypt the keychain */
newPassword: t.string,
/** Optional OTP to unlock the session if required */
otp: optional(t.string),
} as const;

/**
* Response for changing a keychain's password
*/
export const KeychainChangePasswordResponse = {
/** Successful update */
200: t.unknown,
/** Invalid request or not found */
400: BitgoExpressError,
404: BitgoExpressError,
} as const;

/**
* Change a keychain's passphrase, re-encrypting the key to a new password.
*
* @operationId express.v2.keychain.changePassword
*/
export const PostKeychainChangePassword = httpRoute({
path: '/api/v2/{coin}/keychain/{id}/changepassword',
method: 'POST',
request: httpRequest({
params: KeychainChangePasswordParams,
body: KeychainChangePasswordBody,
}),
response: KeychainChangePasswordResponse,
});
44 changes: 44 additions & 0 deletions modules/express/src/typedRoutes/api/v2/lightningState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest } from '@api-ts/io-ts-http';
import { BitgoExpressError } from '../../schemas/error';
import { WalletState } from '../../../lightning/codecs';

/**
* Path parameters for getting lightning node state
*/
export const LightningStateParams = {
/** A lightning coin name (e.g., lnbtc or tlnbtc) */
coin: t.string,
/** The ID of the lightning self-custody wallet */
walletId: t.string,
} as const;

export const LightningStateResponse200 = t.type({
state: WalletState,
});

/**
* Response for getting lightning node state
*/
export const LightningStateResponse = {
/** Current Lightning wallet/node state('NON_EXISTING' | 'LOCKED' | 'UNLOCKED' | 'RPC_ACTIVE' | 'SERVER_ACTIVE' | 'WAITING_TO_START') */
200: LightningStateResponse200,
/** BitGo Express error payload when the request is invalid (e.g., invalid coin or wallet not a self-custody lightning wallet). */
400: BitgoExpressError,
} as const;

/**
* Lightning - Get node state
*
* This is only used for self-custody lightning. Get the current state of the lightning node.
*
* @operationId express.lightning.getState
*/
export const GetLightningState = httpRoute({
method: 'GET',
path: '/api/v2/{coin}/wallet/{walletId}/state',
request: httpRequest({
params: LightningStateParams,
}),
response: LightningStateResponse,
});
33 changes: 22 additions & 11 deletions modules/express/test/unit/clientRoutes/changeKeychainPassword.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import * as sinon from 'sinon';

import 'should-http';
import 'should-sinon';
import '../../lib/asserts';

import * as express from 'express';

import { handleKeychainChangePassword } from '../../../src/clientRoutes';

import { BitGo } from 'bitgo';
import { ExpressApiRouteRequest } from 'modules/express/src/typedRoutes/api';
import { decodeOrElse } from '@bitgo/sdk-core';
import { KeychainChangePasswordResponse } from '../../../src/typedRoutes/api/v2/keychainChangePassword';

describe('Change Wallet Password', function () {
it('should change wallet password', async function () {
Expand Down Expand Up @@ -36,19 +34,32 @@ describe('Change Wallet Password', function () {
}),
});

const coin = 'talgo';
const id = '23423423423423';
const oldPassword = 'oldPasswordString';
const newPassword = 'newPasswordString';
const mockRequest = {
bitgo: stubBitgo,
params: {
coin: 'talgo',
id: '23423423423423',
coin,
id,
},
body: {
oldPassword: 'oldPasswordString',
newPassword: 'newPasswordString',
oldPassword,
newPassword,
},
};
decoded: {
oldPassword,
newPassword,
coin,
id,
},
} as unknown as ExpressApiRouteRequest<'express.keychain.changePassword', 'post'>;

const result = await handleKeychainChangePassword(mockRequest as express.Request & typeof mockRequest);
const result = await handleKeychainChangePassword(mockRequest);
decodeOrElse('KeychainChangePasswordResponse200', KeychainChangePasswordResponse[200], result, (errors) => {
throw new Error(`Response did not match expected codec: ${errors}`);
});
({ result: '200 OK' }).should.be.eql(result);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '../../../../src/lightning/lightningSignerRoutes';
import { ExpressApiRouteRequest } from '../../../../src/typedRoutes/api';
import { PostLightningInitWallet } from '../../../../src/typedRoutes/api/v2/lightningInitWallet';
import { LightningStateResponse } from '../../../../src/typedRoutes/api/v2/lightningState';

describe('Lightning signer routes', () => {
let bitgo: TestBitGoAPI;
Expand Down Expand Up @@ -165,13 +166,21 @@ describe('Lightning signer routes', () => {
params: {
coin: 'tlnbtc',
id: apiData.wallet.id,
walletId: apiData.wallet.id,
},
decoded: {
coin: 'tlnbtc',
walletId: apiData.wallet.id,
},
config: {
lightningSignerFileSystemPath: 'lightningSignerFileSystemPath',
},
} as unknown as express.Request;
} as unknown as ExpressApiRouteRequest<'express.lightning.getState', 'get'>;

await handleGetLightningWalletState(req);
const res = await handleGetLightningWalletState(req);
decodeOrElse('LightningStateResponse200', LightningStateResponse[200], res, () => {
throw new Error('Response did not match expected codec');
});

walletStateNock.done();
readFileStub.calledOnceWith('lightningSignerFileSystemPath').should.be.true();
Expand Down
31 changes: 31 additions & 0 deletions modules/express/test/unit/typedRoutes/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ import { VerifyAddressBody } from '../../../src/typedRoutes/api/common/verifyAdd
import { VerifyAddressV2Body, VerifyAddressV2Params } from '../../../src/typedRoutes/api/v2/verifyAddress';
import { SimpleCreateRequestBody } from '../../../src/typedRoutes/api/v1/simpleCreate';
import { KeychainLocalRequestParams } from '../../../src/typedRoutes/api/v2/keychainLocal';
import { LightningStateParams } from '../../../src/typedRoutes/api/v2/lightningState';
import {
LightningInitWalletBody,
LightningInitWalletParams,
} from '../../../src/typedRoutes/api/v2/lightningInitWallet';
import { UnlockLightningWalletBody, UnlockLightningWalletParams } from '../../../src/typedRoutes/api/v2/unlockWallet';
import {
KeychainChangePasswordBody,
KeychainChangePasswordParams,
} from '../../../src/typedRoutes/api/v2/keychainChangePassword';

export function assertDecode<T>(codec: t.Type<T, unknown>, input: unknown): T {
const result = codec.decode(input);
Expand Down Expand Up @@ -157,6 +162,12 @@ describe('io-ts decode tests', function () {
coin: 'tbtc',
});
});
it('express.lightning.getState params valid', function () {
assertDecode(t.type(LightningStateParams), { coin: 'lnbtc', walletId: 'wallet123' });
});
it('express.lightning.getState params invalid', function () {
assert.throws(() => assertDecode(t.type(LightningStateParams), { coin: 'lnbtc' }));
});
it('express.lightning.initWallet params', function () {
// missing walletId
assert.throws(() => assertDecode(t.type(LightningInitWalletParams), { coin: 'ltc' }));
Expand All @@ -183,4 +194,24 @@ describe('io-ts decode tests', function () {
// valid body
assertDecode(t.type(UnlockLightningWalletBody), { passphrase: 'secret' });
});

it('express.keychain.changePassword', function () {
// missing id
assert.throws(() => assertDecode(t.type(KeychainChangePasswordParams), { coin: 'btc' }));
// invalid coin type
assert.throws(() => assertDecode(t.type(KeychainChangePasswordParams), { coin: 123, id: 'abc' }));
// valid params
assertDecode(t.type(KeychainChangePasswordParams), { coin: 'btc', id: 'abc' });
// missing required fields
assert.throws(() => assertDecode(t.type(KeychainChangePasswordBody), {}));
assert.throws(() => assertDecode(t.type(KeychainChangePasswordBody), { oldPassword: 'a' }));
assert.throws(() => assertDecode(t.type(KeychainChangePasswordBody), { newPassword: 'b' }));
// invalid types
assert.throws(() => assertDecode(t.type(KeychainChangePasswordBody), { oldPassword: 1, newPassword: 'b' }));
assert.throws(() => assertDecode(t.type(KeychainChangePasswordBody), { oldPassword: 'a', newPassword: 2 }));
// valid minimal
assertDecode(t.type(KeychainChangePasswordBody), { oldPassword: 'a', newPassword: 'b' });
// valid with optional otp
assertDecode(t.type(KeychainChangePasswordBody), { oldPassword: 'a', newPassword: 'b', otp: '123456' });
});
});
Loading