Skip to content

Commit

Permalink
feat: add express endpoints for lightning invoice
Browse files Browse the repository at this point in the history
Add
- list invoices
- update wallet coin specific

Ticket: BTC-1836
  • Loading branch information
007harshmahajan authored and lcovar committed Feb 28, 2025
1 parent cb3afdb commit a31979f
Show file tree
Hide file tree
Showing 9 changed files with 386 additions and 70 deletions.
29 changes: 24 additions & 5 deletions modules/abstract-lightning/src/codecs/api/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,32 @@ export const WatchOnly = t.type({

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

export const UpdateLightningWalletSignedRequest = t.partial({
encryptedSignerMacaroon: t.string,
encryptedSignerAdminMacaroon: t.string,
const CommonLightningUpdateWalletFields = t.partial({
signerHost: t.string,
encryptedSignerTlsKey: t.string,
signerTlsCert: t.string,
watchOnlyAccounts: WatchOnly,
});

export type UpdateLightningWalletSignedRequest = t.TypeOf<typeof UpdateLightningWalletSignedRequest>;
export const UpdateLightningWalletEncryptedRequest = t.intersection([
CommonLightningUpdateWalletFields,
t.partial({
encryptedSignerMacaroon: t.string,
encryptedSignerAdminMacaroon: t.string,
encryptedSignerTlsKey: t.string,
}),
]);

export const UpdateLightningWalletClientRequest = t.intersection([
CommonLightningUpdateWalletFields,
t.type({
passphrase: t.string,
}),
t.partial({
signerMacaroon: t.string,
signerAdminMacaroon: t.string,
signerTlsKey: t.string,
}),
]);

export type UpdateLightningWalletEncryptedRequest = t.TypeOf<typeof UpdateLightningWalletEncryptedRequest>;
export type UpdateLightningWalletClientRequest = t.TypeOf<typeof UpdateLightningWalletClientRequest>;
66 changes: 54 additions & 12 deletions modules/abstract-lightning/src/wallet/lightning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
TxRequestState,
} from '@bitgo/sdk-core';
import * as t from 'io-ts';
import { createMessageSignature, unwrapLightningCoinSpecific } from '../lightning';
import { createMessageSignature, deriveLightningServiceSharedSecret, unwrapLightningCoinSpecific } from '../lightning';
import {
CreateInvoiceBody,
Invoice,
Expand All @@ -19,7 +19,8 @@ import {
LightningKeychain,
LndCreatePaymentResponse,
SubmitPaymentParams,
UpdateLightningWalletSignedRequest,
UpdateLightningWalletClientRequest,
UpdateLightningWalletEncryptedRequest,
} from '../codecs';
import { LightningPaymentIntent, LightningPaymentRequest } from '@bitgo/public-types';

Expand Down Expand Up @@ -80,19 +81,19 @@ export interface ILightningWallet {
/**
* Updates the coin-specific configuration for a Lightning Wallet.
*
* @param {UpdateLightningWalletSignedRequest} params - The parameters containing the updated wallet-specific details.
* @param {UpdateLightningWalletClientRequest} params - The parameters containing the updated wallet-specific details.
* - `encryptedSignerMacaroon` (optional): This macaroon is used by the watch-only node to ask the signer node to sign transactions.
* Encrypted with ECDH secret key from private key of wallet's user auth key and public key of lightning service.
* - `encryptedSignerAdminMacaroon` (optional): Generated when initializing the wallet of the signer node.
* Encrypted with client's wallet passphrase.
* - `signerHost` (optional): The host address of the Lightning signer node.
* - `encryptedSignerTlsKey` (optional): The wallet passphrase encrypted TLS key of the signer.
* - `passphrase` (required): The wallet passphrase.
* - `signerTlsCert` (optional): The TLS certificate of the signer.
* - `watchOnlyAccounts` (optional): These are the accounts used to initialize the watch-only wallet.
* @param {string} passphrase - wallet passphrase.
* @returns {Promise<unknown>} A promise resolving to the updated wallet response or throwing an error if the update fails.
*/
updateWalletCoinSpecific(params: UpdateLightningWalletSignedRequest, passphrase: string): Promise<unknown>;
updateWalletCoinSpecific(params: UpdateLightningWalletClientRequest): Promise<unknown>;
}

export class SelfCustodialLightningWallet implements ILightningWallet {
Expand Down Expand Up @@ -225,24 +226,65 @@ export class SelfCustodialLightningWallet implements ILightningWallet {
return { userAuthKey, nodeAuthKey };
}

async updateWalletCoinSpecific(params: UpdateLightningWalletSignedRequest, passphrase: string): Promise<unknown> {
private encryptWalletUpdateRequest(
params: UpdateLightningWalletClientRequest,
userAuthKey: LightningAuthKeychain
): UpdateLightningWalletEncryptedRequest {
const coinName = this.wallet.coin() as 'tlnbtc' | 'lnbtc';

const requestWithEncryption: Partial<UpdateLightningWalletClientRequest & UpdateLightningWalletEncryptedRequest> = {
...params,
};

const userAuthXprv = this.wallet.bitgo.decrypt({
password: params.passphrase,
input: userAuthKey.encryptedPrv,
});

if (params.signerTlsKey) {
requestWithEncryption.encryptedSignerTlsKey = this.wallet.bitgo.encrypt({
password: params.passphrase,
input: params.signerTlsKey,
});
}

if (params.signerAdminMacaroon) {
requestWithEncryption.encryptedSignerAdminMacaroon = this.wallet.bitgo.encrypt({
password: params.passphrase,
input: params.signerAdminMacaroon,
});
}

if (params.signerMacaroon) {
requestWithEncryption.encryptedSignerMacaroon = this.wallet.bitgo.encrypt({
password: deriveLightningServiceSharedSecret(coinName, userAuthXprv).toString('hex'),
input: params.signerMacaroon,
});
}

return t.exact(UpdateLightningWalletEncryptedRequest).encode(requestWithEncryption);
}

async updateWalletCoinSpecific(params: UpdateLightningWalletClientRequest): Promise<unknown> {
sdkcore.decodeOrElse(
UpdateLightningWalletSignedRequest.name,
UpdateLightningWalletSignedRequest,
UpdateLightningWalletClientRequest.name,
UpdateLightningWalletClientRequest,
params,
(errors) => {
// DON'T throw errors from decodeOrElse. It could leak sensitive information.
throw new Error(`Invalid params for lightning specific update wallet: ${errors}`);
throw new Error(`Invalid params for lightning specific update wallet`);
}
);

const { userAuthKey } = await this.getLightningAuthKeychains();
const updateRequestWithEncryption = this.encryptWalletUpdateRequest(params, userAuthKey);
const signature = createMessageSignature(
params,
this.wallet.bitgo.decrypt({ password: passphrase, input: userAuthKey.encryptedPrv })
updateRequestWithEncryption,
this.wallet.bitgo.decrypt({ password: params.passphrase, input: userAuthKey.encryptedPrv })
);
const coinSpecific = {
[this.wallet.coin()]: {
signedRequest: params,
signedRequest: updateRequestWithEncryption,
signature,
},
};
Expand Down
16 changes: 9 additions & 7 deletions modules/abstract-lightning/test/unit/lightning/codecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as t from 'io-ts';
import assert from 'assert';
import * as E from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';
import { LightningAuthKeychain, LightningKeychain, UpdateLightningWalletSignedRequest } from '../../../src/codecs';
import { LightningAuthKeychain, LightningKeychain, UpdateLightningWalletClientRequest } from '../../../src/codecs';

function describeCodec(c: t.Type<any>, valid: unknown[], invalid: unknown[]) {
describe('Codec ' + c.name, function () {
Expand Down Expand Up @@ -90,24 +90,26 @@ describe('Codecs', function () {
);

describeCodec(
UpdateLightningWalletSignedRequest,
UpdateLightningWalletClientRequest,
[
{
encryptedSignerAdminMacaroon: 'encryptedSignerAdminMacaroon',
signerAdminMacaroon: 'signerAdminMacaroon',
signerHost: '127.0.0.1',
signerTlsCert: 'signerTlsCert',
encryptedSignerTlsKey: 'encryptedSignerTlsKey',
signerTlsKey: 'signerTlsKey',
watchOnly: {
master_key_birthday_timestamp: 'master_key_birthday_timestamp',
master_key_fingerprint: 'master_key_fingerprint',
accounts: [{ purpose: 1, coin_type: 1, account: 1, xpub: 'xpub' }],
},
encryptedSignerMacaroon: 'encryptedSignerMacaroon',
signerMacaroon: 'signerMacaroon',
passphrase: 'passphrase',
},
{
encryptedSignerAdminMacaroon: 'encryptedSignerAdminMacaroon',
signerAdminMacaroon: 'signerAdminMacaroon',
passphrase: 'passphrase',
},
{},
{ passphrase: 'passphrase' },
],
[null, 'abg', 1]
);
Expand Down
52 changes: 37 additions & 15 deletions modules/bitgo/test/v2/unit/lightning/lightningWallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
LndCreatePaymentResponse,
SelfCustodialLightningWallet,
SubmitPaymentParams,
UpdateLightningWalletClientRequest,
} from '@bitgo/abstract-lightning';

import { BitGo, common, GenerateLightningWalletOptions, Wallet, Wallets } from '../../../../src';
Expand Down Expand Up @@ -569,16 +570,6 @@ describe('Lightning wallets', function () {
},
],
};

const params = {
encryptedSignerMacaroon: 'test encryptedSignerMacaroon',
encryptedSignerAdminMacaroon: 'test encryptedSignerAdminMacaroon',
signerHost: '1.1.1.1',
encryptedSignerTlsKey: 'test encryptedSignerTlsKey',
signerTlsCert: 'test signerTlsCert',
watchOnlyAccounts,
};

it('should update wallet', async function () {
const wallet = getLightningWallet(new Wallet(bitgo, basecoin, walletData));

Expand All @@ -588,12 +579,43 @@ describe('Lightning wallets', function () {
const nodeAuthKeyNock = nock(bgUrl)
.get('/api/v2/' + coinName + '/key/ghi')
.reply(200, nodeAuthKey);
const wpWalletUpdateNock = nock(bgUrl).put(`/api/v2/tlnbtc/wallet/${walletData.id}`).reply(200);
let capturedBody;
const wpWalletUpdateNock = nock(bgUrl)
.put(`/api/v2/tlnbtc/wallet/${walletData.id}`)
.reply(function (uri, requestBody) {
capturedBody = requestBody;
return [200];
});

const params: UpdateLightningWalletClientRequest = {
signerMacaroon: 'signerMacaroon',
signerAdminMacaroon: 'signerAdminMacaroon',
signerTlsKey: 'signerTlsKey',
signerTlsCert: 'signerTlsCert',
watchOnlyAccounts,
passphrase: 'password123',
};

await assert.doesNotReject(async () => await wallet.updateWalletCoinSpecific(params, 'password123'));
userAuthKeyNock.done();
nodeAuthKeyNock.done();
wpWalletUpdateNock.done();
await assert.doesNotReject(async () => await wallet.updateWalletCoinSpecific(params));
assert(userAuthKeyNock.isDone());
assert(nodeAuthKeyNock.isDone());
assert(wpWalletUpdateNock.isDone());

// Verify structure and required fields
assert.ok(capturedBody.coinSpecific?.tlnbtc?.signedRequest, 'signedRequest should exist');
const signedRequest = capturedBody.coinSpecific.tlnbtc.signedRequest;

assert.ok(signedRequest.signerTlsCert, 'signerTlsCert should exist');
assert.ok(signedRequest.watchOnlyAccounts, 'watchOnlyAccounts should exist');
assert.ok(signedRequest.encryptedSignerTlsKey, 'encryptedSignerTlsKey should exist');
assert.ok(signedRequest.encryptedSignerAdminMacaroon, 'encryptedSignerAdminMacaroon should exist');
assert.ok(signedRequest.encryptedSignerMacaroon, 'encryptedSignerMacaroon should exist');

// Verify signature exists
assert.ok(capturedBody.coinSpecific.tlnbtc.signature, 'signature should exist');

// we should not pass passphrase to the backend
assert.strictEqual(signedRequest.passphrase, undefined, 'passphrase should not exist in request');
});
});
});
30 changes: 30 additions & 0 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ import {
handleUnlockLightningWallet,
} from './lightning/lightningSignerRoutes';
import { handleCreateLightningInvoice, handlePayLightningInvoice } from './lightning/lightningInvoiceRoutes';
import {
handleListLightningInvoices,
handleUpdateLightningWalletCoinSpecific,
} from './lightning/lightningWalletRoutes';
import { ProxyAgent } from 'proxy-agent';
import { isLightningCoinName } from '@bitgo/abstract-lightning';

const { version } = require('bitgo/package.json');
const pjson = require('../package.json');
Expand Down Expand Up @@ -1004,6 +1009,23 @@ export async function handleV2EnableTokens(req: express.Request) {
}
}

/**
* Handle Update Wallet
* @param req
*/
async function handleWalletUpdate(req: express.Request): Promise<unknown> {
// If it's a lightning coin, use the lightning-specific handler
if (isLightningCoinName(req.params.coin)) {
return handleUpdateLightningWalletCoinSpecific(req);
}

const bitgo = req.bitgo;
const coin = bitgo.coin(req.params.coin);
// For non-lightning coins, directly update the wallet
const wallet = await coin.wallets().get({ id: req.params.id });
return await bitgo.put(wallet.url()).send(req.body).result();
}

/**
* handle any other API call
* @param req
Expand Down Expand Up @@ -1554,6 +1576,8 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
// generate wallet
app.post('/api/v2/:coin/wallet/generate', parseBody, prepareBitGo(config), promiseWrapper(handleV2GenerateWallet));

app.put('/api/v2/:coin/wallet/:id', parseBody, prepareBitGo(config), promiseWrapper(handleWalletUpdate));

// create address
app.post('/api/v2/:coin/wallet/:id/address', parseBody, prepareBitGo(config), promiseWrapper(handleV2CreateAddress));

Expand Down Expand Up @@ -1670,6 +1694,12 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
prepareBitGo(config),
promiseWrapper(handlePayLightningInvoice)
);
// lightning - list invoices
app.get(
'/api/v2/:coin/wallet/:id/lightning/invoices',
prepareBitGo(config),
promiseWrapper(handleListLightningInvoices)
);

// everything else should use the proxy handler
if (config.disableProxy !== true) {
Expand Down
37 changes: 8 additions & 29 deletions modules/express/src/lightning/lightningSignerRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
createWatchOnly,
addIPCaveatToMacaroon,
isLightningCoinName,
deriveLightningServiceSharedSecret,
getLightningWallet,
} from '@bitgo/abstract-lightning';
import * as utxolib from '@bitgo/utxo-lib';
Expand Down Expand Up @@ -101,19 +100,12 @@ export async function handleInitLightningWallet(req: express.Request): Promise<u
macaroon_root_key: macaroonRootKey,
});

const encryptedSignerAdminMacaroon = bitgo.encrypt({
password: passphrase,
input: expressHost && !!isIP(expressHost) ? addIPCaveatToMacaroon(adminMacaroon, expressHost) : adminMacaroon,
return await lightningWallet.updateWalletCoinSpecific({
signerAdminMacaroon:
expressHost && !!isIP(expressHost) ? addIPCaveatToMacaroon(adminMacaroon, expressHost) : adminMacaroon,
watchOnlyAccounts: createWatchOnly(signerRootKey, network),
passphrase,
});
const watchOnlyAccounts = createWatchOnly(signerRootKey, network);

return await lightningWallet.updateWalletCoinSpecific(
{
encryptedSignerAdminMacaroon,
watchOnlyAccounts,
},
passphrase
);
}

/**
Expand Down Expand Up @@ -167,29 +159,16 @@ export async function handleCreateSignerMacaroon(req: express.Request): Promise<
input: encryptedSignerAdminMacaroon,
});

const { userAuthKey } = await lightningWallet.getLightningAuthKeychains();

const signerMacaroon = await createSignerMacaroon(
lndSignerClient,
{ adminMacaroonHex: Buffer.from(adminMacaroon, 'base64').toString('hex') },
watchOnlyIp
);

const userAuthXprv = bitgo.decrypt({
password: passphrase,
input: userAuthKey.encryptedPrv,
return await lightningWallet.updateWalletCoinSpecific({
signerMacaroon,
passphrase,
});

const encryptedSignerMacaroon = bitgo.encrypt({
password: deriveLightningServiceSharedSecret(coinName, userAuthXprv).toString('hex'),
input: signerMacaroon,
});
return await lightningWallet.updateWalletCoinSpecific(
{
encryptedSignerMacaroon,
},
passphrase
);
}

/**
Expand Down
Loading

0 comments on commit a31979f

Please sign in to comment.