Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(express): get external ip from wallet instead of request #5653

Merged
merged 3 commits into from
Feb 27, 2025
Merged
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
3 changes: 2 additions & 1 deletion modules/express/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ BITGO_AUTH_VERSION=2
BITGO_EXTERNAL_SIGNER_URL=
BITGO_SIGNER_MODE=
BITGO_SIGNER_FILE_SYSTEM_PATH=
BITGO_LIGHTNING_SIGNER_FILE_SYSTEM_PATH=

# SIGN TRANSACTION FOR A SPECIFIC WALLET (Replace <ID> with your wallet ID)
# See clientRoutes.ts => getWalletPwFromEnv()
Expand All @@ -33,4 +34,4 @@ BITGO_EXTERNAL_SIGNER_ACCESS_TOKEN=
BITGO_EXTERNAL_SIGNER_WALLET_IDS=

# Examples
# BITGO_EXTERNAL_SIGNER_WALLET_IDS="{"tbtc":[{"walletId":"xxx","walletPassword":"xxx","secret":"xxx"}],"hteth":["walletId":"xxx","walletPassword":"xxx"]}"
# BITGO_EXTERNAL_SIGNER_WALLET_IDS="{"tbtc":[{"walletId":"xxx","walletPassword":"xxx","secret":"xxx"}],"hteth":["walletId":"xxx","walletPassword":"xxx"]}"
1 change: 1 addition & 0 deletions modules/express/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ BitGo Express is able to take configuration options from either command line arg
| N/A | --externalSignerUrl | `BITGO_EXTERNAL_SIGNER_URL` | N/A | URL specifying the external API to call for remote signing. |
| N/A | --signerMode | `BITGO_SIGNER_MODE ` | N/A | If set, run Express as a remote signer. |
| N/A | --signerFileSystemPath | `BITGO_SIGNER_FILE_SYSTEM_PATH ` | N/A | Local path specifying where an Express signer machine keeps the encrypted user private keys. Required when signerMode is set. |
| N/A | --lightningSignerFileSystemPath | `BITGO_LIGHTNING_SIGNER_FILE_SYSTEM_PATH` | N/A | Local path specifying how to contact the lightning signer node.

\[0]: BitGo will also check the additional environment variables for some options for backwards compatibility, but these environment variables should be considered deprecated:

Expand Down
14 changes: 5 additions & 9 deletions modules/express/src/lightning/codecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,11 @@ export const InitLightningWalletRequest = t.intersection(

export type InitLightningWalletRequest = t.TypeOf<typeof InitLightningWalletRequest>;

export const CreateSignerMacaroonRequest = t.intersection(
[
t.strict({
passphrase: t.string,
}),
t.partial({
watchOnlyIp: t.string,
}),
],
export const CreateSignerMacaroonRequest = t.type(
{
passphrase: t.string,
addIpCaveatToMacaroon: t.boolean,
},
'CreateSignerMacaroonRequest'
);

Expand Down
16 changes: 12 additions & 4 deletions modules/express/src/lightning/lightningSignerRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export async function handleCreateSignerMacaroon(req: express.Request): Promise<
throw new ApiResponseError(`Invalid wallet id: ${walletId}`, 400);
}

const { passphrase, watchOnlyIp } = decodeOrElse(
const { passphrase, addIpCaveatToMacaroon } = decodeOrElse(
CreateSignerMacaroonRequest.name,
CreateSignerMacaroonRequest,
req.body,
Expand All @@ -141,13 +141,21 @@ export async function handleCreateSignerMacaroon(req: express.Request): Promise<
}
);

const wallet = await coin.wallets().get({ id: walletId });
const watchOnlyExternalIp = wallet.coinSpecific()?.watchOnlyExternalIp;
if (!watchOnlyExternalIp && addIpCaveatToMacaroon) {
throw new ApiResponseError(
'Cannot create signer macaroon because the external IP is not set. This can take some time. Contact [email protected] if longer than 24 hours.',
400
);
}
const watchOnlyIp = watchOnlyExternalIp === null ? undefined : watchOnlyExternalIp;

if (watchOnlyIp !== undefined && !isIP(watchOnlyIp)) {
throw new ApiResponseError(`Invalid IP address: ${watchOnlyIp}`, 400);
throw new ApiResponseError(`Invalid IP address: ${watchOnlyIp}. Contact [email protected]`, 500);
}

const lndSignerClient = await LndSignerClient.create(walletId, req.config);

const wallet = await coin.wallets().get({ id: walletId });
const lightningWallet = getLightningWallet(wallet);

const encryptedSignerAdminMacaroon = wallet.coinSpecific()?.encryptedSignerAdminMacaroon;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export const apiData = {
},
signerMacaroonRequestBody: {
passphrase: 'password123',
watchOnlyIp: '127.0.0.1',
},
unlockWalletRequestBody: {
passphrase: 'password123',
Expand All @@ -16,6 +15,7 @@ export const apiData = {
keys: ['abc'],
coinSpecific: {
keys: ['def', 'ghi'],
watchOnlyExternalIp: '1.2.3.4',
encryptedSignerAdminMacaroon:
'{"iv":"mf/5PSEGdLTKlc8t1IwOBA==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"mZcamRzITwg=","ct":"KCCc+/ly37EZPRoVBgE9T2mAUufWuqmtSadZAAcECevmNbGgGtAhi7P8/zpge49EdsKOP1Mx1DkwnZBMqCVQBTIWZO4XrFI+OOI0YWDrJIaGcnXDFgZWCbgGaomzYNRvt3EoJ1+yMn1EsYdFYgM0NBS0YsvNHx6PsK2eSLpAK2UrhHAkm9X2uhVRMOjjiGr0UW6r4BKuzxCA06fKKQk6bb8LEF54EZFwigjLSztebW5ivNVT/6MxMnjlO7YPW1ClwM9cqJy1oNLUuRK1vnr6hHCas+3F0PCt5XhJJlsgsm1Vz45wWEGdZiyb0XbqOKHyxCI2WOF5Nj1ALiA0D4o9bqfzasNgrvYlMJ4Ld7ayHDtfhiFve/cUZkcQdVqNbS1TPuyvYT8vPKmL5JwuABoTkLH2LtBOh0afz9UFZajo7pxmJ9TtN+B+/GUoiR9v4e2Jw+IpMIIv3ATMqQl9Kot6yefiuP+1DfYNBPvcUqJMc8ibpP56BUA0qWLoAIg5DoocoMybXi0+eA1S0c8Lhe0PsA=="}',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,47 +84,60 @@ describe('Lightning signer routes', () => {
});
}

for (const includingOptionalFields of [true, false]) {
it(`should create signer macaroon ${includingOptionalFields ? 'with' : 'without'} optional fields`, async () => {
const readFileStub = sinon.stub(fs.promises, 'readFile').resolves(JSON.stringify(lightningSignerConfigs));
const wpWalletnock = nock(bgUrl).get(`/api/v2/tlnbtc/wallet/${apiData.wallet.id}`).reply(200, apiData.wallet);

const wpKeychainNocks = [
nock(bgUrl).get(`/api/v2/tlnbtc/key/${apiData.userAuthKey.id}`).reply(200, apiData.userAuthKey),
nock(bgUrl).get(`/api/v2/tlnbtc/key/${apiData.nodeAuthKey.id}`).reply(200, apiData.nodeAuthKey),
nock(bgUrl).get(`/api/v2/tlnbtc/key/${apiData.userAuthKey.id}`).reply(200, apiData.userAuthKey),
nock(bgUrl).get(`/api/v2/tlnbtc/key/${apiData.nodeAuthKey.id}`).reply(200, apiData.nodeAuthKey),
];

const signerMacaroon = nock(lightningSignerConfigs.fakeid.url)
.post(`/v1/macaroon`)
.reply(200, signerApiData.bakeMacaroon);

const wpWalletUpdateNock = nock(bgUrl).put(`/api/v2/tlnbtc/wallet/${apiData.wallet.id}`).reply(200);

const req = {
bitgo: bitgo,
body: includingOptionalFields
? apiData.signerMacaroonRequestBody
: { ...apiData.signerMacaroonRequestBody, watchOnlyIp: undefined },
params: {
coin: 'tlnbtc',
id: 'fakeid',
},
config: {
lightningSignerFileSystemPath: 'lightningSignerFileSystemPath',
},
} as unknown as express.Request;

await handleCreateSignerMacaroon(req);

wpWalletUpdateNock.done();
signerMacaroon.done();
wpKeychainNocks.forEach((s) => s.done());
wpWalletnock.done();
readFileStub.calledOnceWith('lightningSignerFileSystemPath').should.be.true();
readFileStub.restore();
});
for (const addIpCaveatToMacaroon of [true, false]) {
for (const includeWatchOnlyIp of [true, false]) {
it(`create signer macaroon ${addIpCaveatToMacaroon ? 'with' : 'without'} including IP caveat when it ${
includeWatchOnlyIp ? 'does' : `doesn't`
} exist`, async () => {
const readFileStub = sinon.stub(fs.promises, 'readFile').resolves(JSON.stringify(lightningSignerConfigs));
const wpWalletnock = nock(bgUrl)
.get(`/api/v2/tlnbtc/wallet/${apiData.wallet.id}`)
.reply(200, {
...apiData.wallet,
...(includeWatchOnlyIp ? {} : { watchOnlyExternalIp: null }),
});

const wpKeychainNocks = [
nock(bgUrl).get(`/api/v2/tlnbtc/key/${apiData.userAuthKey.id}`).reply(200, apiData.userAuthKey),
nock(bgUrl).get(`/api/v2/tlnbtc/key/${apiData.nodeAuthKey.id}`).reply(200, apiData.nodeAuthKey),
nock(bgUrl).get(`/api/v2/tlnbtc/key/${apiData.userAuthKey.id}`).reply(200, apiData.userAuthKey),
nock(bgUrl).get(`/api/v2/tlnbtc/key/${apiData.nodeAuthKey.id}`).reply(200, apiData.nodeAuthKey),
];

const signerMacaroon = nock(lightningSignerConfigs.fakeid.url)
.post(`/v1/macaroon`)
.reply(200, signerApiData.bakeMacaroon);

const wpWalletUpdateNock = nock(bgUrl).put(`/api/v2/tlnbtc/wallet/${apiData.wallet.id}`).reply(200);

const req = {
bitgo: bitgo,
body: { ...apiData.signerMacaroonRequestBody, addIpCaveatToMacaroon },
params: {
coin: 'tlnbtc',
id: 'fakeid',
},
config: {
lightningSignerFileSystemPath: 'lightningSignerFileSystemPath',
},
} as unknown as express.Request;

try {
await handleCreateSignerMacaroon(req);
} catch (e) {
if (!includeWatchOnlyIp || addIpCaveatToMacaroon) {
throw e;
}
}

wpWalletUpdateNock.done();
signerMacaroon.done();
wpKeychainNocks.forEach((s) => s.done());
wpWalletnock.done();
readFileStub.calledOnceWith('lightningSignerFileSystemPath').should.be.true();
readFileStub.restore();
});
}
}

it('should get signer wallet state', async () => {
Expand Down
9 changes: 8 additions & 1 deletion modules/sdk-core/src/bitgo/wallet/iWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,14 @@ export interface WalletCoinSpecific {
* Lightning coin specific data starts
*/
keys?: string[];
encryptedSignerAdminMacaroon?: string;
encryptedSignerAdminMacaroon?: string | null;
encryptedSignerMacaroon?: string | null;
watchOnlyExternalIp?: string | null;
signerHost?: string | null;
encryptedSignerTlsKey?: string | null;
signerTlsCert?: string | null;
watchOnlyAccounts?: Record<string, unknown> | null;
signedUserAuthKey?: string;
/**
* Lightning coin specific data ends
*/
Expand Down