diff --git a/modules/bitgo/test/v2/unit/lightning/lightningWallets.ts b/modules/bitgo/test/v2/unit/lightning/lightningWallets.ts index b0a0c6004b..3dd87908cc 100644 --- a/modules/bitgo/test/v2/unit/lightning/lightningWallets.ts +++ b/modules/bitgo/test/v2/unit/lightning/lightningWallets.ts @@ -1,7 +1,7 @@ import * as assert from 'assert'; import { TestBitGo } from '@bitgo/sdk-test'; import * as nock from 'nock'; -import { BaseCoin, getLightningAuthKeychains, getLightningKeychain } from '@bitgo/sdk-core'; +import { BaseCoin } from '@bitgo/sdk-core'; import { BitGo, common, GenerateLightningWalletOptions, Wallet, Wallets } from '../../../../src'; @@ -219,19 +219,19 @@ describe('Lightning wallets', function () { }; it('should get lightning key', async function () { - const wallet = new Wallet(bitgo, basecoin, walletData); + const wallet = new Wallet(bitgo, basecoin, walletData).lightningV2(); const keyNock = nock(bgUrl) .get('/api/v2/' + coinName + '/key/abc') .reply(200, userKeyData); - const key = await getLightningKeychain(wallet); + const key = await wallet.getLightningKeychain(); assert.deepStrictEqual(key, userKeyData); keyNock.done(); }); it('should get lightning auth keys', async function () { - const wallet = new Wallet(bitgo, basecoin, walletData); + const wallet = new Wallet(bitgo, basecoin, walletData).lightningV2(); const userAuthKeyNock = nock(bgUrl) .get('/api/v2/' + coinName + '/key/def') @@ -240,57 +240,41 @@ describe('Lightning wallets', function () { .get('/api/v2/' + coinName + '/key/ghi') .reply(200, nodeAuthKeyData); - const { userAuthKey, nodeAuthKey } = await getLightningAuthKeychains(wallet); + const { userAuthKey, nodeAuthKey } = await wallet.getLightningAuthKeychains(); assert.deepStrictEqual(userAuthKey, userAuthKeyData); assert.deepStrictEqual(nodeAuthKey, nodeAuthKeyData); userAuthKeyNock.done(); nodeAuthKeyNock.done(); }); - it('should fail to get lightning key for invalid coin', async function () { - const wallet = new Wallet(bitgo, bitgo.coin('tltc'), walletData); - await assert.rejects( - async () => await getLightningKeychain(wallet), - /Error: Invalid coin to get lightning Keychain: ltc/ - ); - }); - - it('should fail to get lightning auth keys for invalid coin', async function () { - const wallet = new Wallet(bitgo, bitgo.coin('tltc'), walletData); - await assert.rejects( - async () => await getLightningAuthKeychains(wallet), - /Error: Invalid coin to get lightning auth keychains: ltc/ - ); - }); - it('should fail to get lightning key for invalid number of keys', async function () { - const wallet = new Wallet(bitgo, basecoin, { ...walletData, keys: [] }); + const wallet = new Wallet(bitgo, basecoin, { ...walletData, keys: [] }).lightningV2(); await assert.rejects( - async () => await getLightningKeychain(wallet), + async () => await wallet.getLightningKeychain(), /Error: Invalid number of key in lightning wallet: 0/ ); }); it('should fail to get lightning auth keys for invalid number of keys', async function () { - const wallet = new Wallet(bitgo, basecoin, { ...walletData, coinSpecific: { keys: ['def'] } }); + const wallet = new Wallet(bitgo, basecoin, { ...walletData, coinSpecific: { keys: ['def'] } }).lightningV2(); await assert.rejects( - async () => await getLightningAuthKeychains(wallet), + async () => await wallet.getLightningAuthKeychains(), /Error: Invalid number of auth keys in lightning wallet: 1/ ); }); it('should fail to get lightning key for invalid response', async function () { - const wallet = new Wallet(bitgo, basecoin, walletData); + const wallet = new Wallet(bitgo, basecoin, walletData).lightningV2(); nock(bgUrl) .get('/api/v2/' + coinName + '/key/abc') .reply(200, { ...userKeyData, source: 'backup' }); - await assert.rejects(async () => await getLightningKeychain(wallet), /Error: Invalid user key/); + await assert.rejects(async () => await wallet.getLightningKeychain(), /Error: Invalid user key/); }); it('should fail to get lightning auth keys for invalid response', async function () { - const wallet = new Wallet(bitgo, basecoin, walletData); + const wallet = new Wallet(bitgo, basecoin, walletData).lightningV2(); nock(bgUrl) .get('/api/v2/' + coinName + '/key/def') @@ -301,9 +285,89 @@ describe('Lightning wallets', function () { .reply(200, nodeAuthKeyData); await assert.rejects( - async () => await getLightningAuthKeychains(wallet), + async () => await wallet.getLightningAuthKeychains(), /Error: Invalid lightning auth key: def/ ); }); }); + + describe('Update lightning wallet coin specific', function () { + const walletData = { + id: 'fakeid', + coin: coinName, + keys: ['abc'], + coinSpecific: { keys: ['def', 'ghi'] }, + }; + + const userAuthKey = { + id: 'def', + pub: 'xpub661MyMwAqRbcGYjYsnsDj1SHdiXynWEXNnfNgMSpokN54FKyMqbu7rWEfVNDs6uAJmz86UVFtq4sefhQpXZhSAzQcL9zrEPtiLNNZoeSxCG', + encryptedPrv: + '{"iv":"zYhhaNdW0wPfJEoBjZ4pvg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tgAMua9jjhw=","ct":"HcrbxQvNlWG5tLMndYzdNCYa1l+1h7o+vSsweA0+q1le3tWt6jLUJSEjZN+JI8lTZ2KPFQgLulQQhsUa+ytUCBi0vSgjF7x7CprT7l2Cfjkew00XsEd7wnmtJUsrQk8m69Co7tIRA3oEgzrnYwy4qOM81lbNNyQ="}', + source: 'user', + coinSpecific: { + tlnbtc: { + purpose: 'userAuth', + }, + }, + }; + + const nodeAuthKey = { + id: 'ghi', + pub: 'xpub661MyMwAqRbcG9xnTnAnRbJPo3MAHyRtH4zeehN8exYk4VFz5buepUzebhix33BKhS5Eb4V3LEfW5pYiSR8qmaEnyrpeghhKY8JfzAsUDpq', + encryptedPrv: + '{"iv":"bH6eGbnl9x8PZECPrgvcng==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"o8yknV6nTI8=","ct":"nGyzAToIzYkQeIdcVafoWHtMx7+Fgj0YldCme3WA1yxJAA0QulZVhblMZN/7efCRIumA0NNmpH7dxH6n8cVlz/Z+RUgC2q9lgvZKUoJcYNTjWUfkmkJutXX2tr8yVxm+eC/hnRiyfVLZ2qPxctvDlBVBfgLuPyc="}', + source: 'user', + coinSpecific: { + tlnbtc: { + purpose: 'nodeAuth', + }, + }, + }; + + const watchOnlyAccounts = { + master_key_birthday_timestamp: 'dummy', + master_key_fingerprint: 'dummy', + accounts: [ + { + xpub: 'upub5Eep7H5q39PzQZLVEYLBytDyBNeV74E8mQsyeL6UozFq9Y3MsZ52G7YGuqrJPgoyAqF7TBeJdnkrHrVrB5pkWkPJ9cJGAePMU6F1Gyw6aFH', + purpose: 49, + coin_type: 0, + account: 0, + }, + { + xpub: 'vpub5ZU1PHGpQoDSHckYico4nsvwsD3mTh6UjqL5zyGWXZXzBjTYMNKot7t9eRPQY71hJcnNN9r1ss25g3xA9rmoJ5nWPg8jEWavrttnsVa1qw1', + purpose: 84, + coin_type: 0, + account: 0, + }, + ], + }; + + const params = { + encryptedSignerMacaroon: 'test encryptedSignerMacaroon', + encryptedSignerAdminMacaroon: 'test encryptedSignerAdminMacaroon', + signerIp: 'test signerIp', + encryptedSignerTlsKey: 'test encryptedSignerTlsKey', + signerTlsCert: 'test signerTlsCert', + watchOnlyAccounts, + }; + + it('should update wallet', async function () { + const wallet = new Wallet(bitgo, basecoin, walletData).lightningV2(); + + const userAuthKeyNock = nock(bgUrl) + .get('/api/v2/' + coinName + '/key/def') + .reply(200, userAuthKey); + 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); + + await assert.doesNotReject(async () => await wallet.updateWalletCoinSpecific(params, 'password123')); + userAuthKeyNock.done(); + nodeAuthKeyNock.done(); + wpWalletUpdateNock.done(); + }); + }); }); diff --git a/modules/express/src/lightning/codecs.ts b/modules/express/src/lightning/codecs.ts index 6733092f0b..7e9c938704 100644 --- a/modules/express/src/lightning/codecs.ts +++ b/modules/express/src/lightning/codecs.ts @@ -33,15 +33,19 @@ export const GetWalletStateResponse = t.type( export type GetWalletStateResponse = t.TypeOf; -export const InitLightningWalletRequest = t.strict( - { - walletId: t.string, - passphrase: t.string, - signerIP: IPAddress, - signerTlsCert: t.string, - signerTlsKey: t.string, - expressIP: IPAddress, - }, +export const InitLightningWalletRequest = t.intersection( + [ + t.strict({ + walletId: t.string, + passphrase: t.string, + signerIp: IPAddress, + signerTlsCert: t.string, + expressIp: IPAddress, + }), + t.partial({ + signerTlsKey: t.string, + }), + ], 'InitLightningWalletRequest' ); @@ -51,7 +55,7 @@ export const CreateSignerMacaroonRequest = t.strict( { walletId: t.string, passphrase: t.string, - watchOnlyIP: IPAddress, + watchOnlyIp: IPAddress, }, 'CreateSignerMacaroonRequest' ); diff --git a/modules/express/src/lightning/lightningSignerRoutes.ts b/modules/express/src/lightning/lightningSignerRoutes.ts index 0416b03cf4..480b5b1147 100644 --- a/modules/express/src/lightning/lightningSignerRoutes.ts +++ b/modules/express/src/lightning/lightningSignerRoutes.ts @@ -1,15 +1,10 @@ import * as express from 'express'; import { decodeOrElse, - createMessageSignature, getUtxolibNetwork, signerMacaroonPermissions, createWatchOnly, addIPCaveatToMacaroon, - getLightningAuthKeychains, - getLightningKeychain, - updateLightningWallet, - LightningWalletCoinSpecific, isLightningCoinName, deriveLightningServiceSharedSecret, } from '@bitgo/sdk-core'; @@ -27,12 +22,12 @@ import { LndSignerClient } from './lndSignerClient'; type Decrypt = (params: { input: string; password: string }) => string; async function createSignerMacaroon( - watchOnlyIP: string, + watchOnlyIp: string, header: { adminMacaroonHex: string }, lndSignerClient: LndSignerClient ) { const { macaroon } = await lndSignerClient.bakeMacaroon({ permissions: signerMacaroonPermissions }, header); - const macaroonBase64 = addIPCaveatToMacaroon(Buffer.from(macaroon, 'hex').toString('base64'), watchOnlyIP); + const macaroonBase64 = addIPCaveatToMacaroon(Buffer.from(macaroon, 'hex').toString('base64'), watchOnlyIp); return Buffer.from(macaroonBase64, 'base64').toString('hex'); } @@ -58,7 +53,7 @@ function getMacaroonRootKey(passphrase: string, nodeAuthEncryptedPrv: string, de * Handle the request to initialise remote signer LND for a wallet. */ export async function handleInitLightningWallet(req: express.Request): Promise { - const { walletId, passphrase, signerTlsKey, signerTlsCert, signerIP, expressIP } = decodeOrElse( + const { walletId, passphrase, signerTlsKey, signerTlsCert, signerIp, expressIp } = decodeOrElse( InitLightningWalletRequest.name, InitLightningWalletRequest, req.body, @@ -77,10 +72,10 @@ export async function handleInitLightningWallet(req: express.Request): Promise { - const { walletId, passphrase, watchOnlyIP } = decodeOrElse( + const { walletId, passphrase, watchOnlyIp } = decodeOrElse( CreateSignerMacaroonRequest.name, CreateSignerMacaroonRequest, req.body, @@ -145,6 +130,7 @@ export async function handleCreateSignerMacaroon(req: express.Request): Promise< const coin = bitgo.coin(coinName); const wallet = await coin.wallets().get({ id: walletId }); + const lightningWallet = wallet.lightningV2(); const encryptedSignerAdminMacaroon = wallet.coinSpecific()?.encryptedSignerAdminMacaroon; if (!encryptedSignerAdminMacaroon) { @@ -152,10 +138,10 @@ export async function handleCreateSignerMacaroon(req: express.Request): Promise< } const adminMacaroon = bitgo.decrypt({ password: passphrase, input: encryptedSignerAdminMacaroon }); - const { userAuthKey } = await getLightningAuthKeychains(wallet); + const { userAuthKey } = await lightningWallet.getLightningAuthKeychains(); const signerMacaroon = await createSignerMacaroon( - watchOnlyIP, + watchOnlyIp, { adminMacaroonHex: Buffer.from(adminMacaroon, 'base64').toString('hex') }, lndSignerClient ); @@ -166,20 +152,12 @@ export async function handleCreateSignerMacaroon(req: express.Request): Promise< password: deriveLightningServiceSharedSecret(coinName, userAuthXprv).toString('hex'), input: signerMacaroon, }); - - const coinSpecific = { - [coin.getChain()]: { + return await lightningWallet.updateWalletCoinSpecific( + { encryptedSignerMacaroon, }, - }; - - if (!LightningWalletCoinSpecific.is(coinSpecific)) { - throw new Error('Invalid lightning wallet coin specific data'); - } - - const signature = createMessageSignature(coinSpecific, userAuthXprv); - - return await updateLightningWallet(wallet, { coinSpecific, signature }); + passphrase + ); } /** diff --git a/modules/express/test/unit/clientRoutes/lightning/lightningSignerFixture.ts b/modules/express/test/unit/clientRoutes/lightning/lightningSignerFixture.ts index d842e18ebc..6dfb89b435 100644 --- a/modules/express/test/unit/clientRoutes/lightning/lightningSignerFixture.ts +++ b/modules/express/test/unit/clientRoutes/lightning/lightningSignerFixture.ts @@ -2,8 +2,8 @@ export const apiData = { initWalletRequestBody: { walletId: 'fakeid', passphrase: 'password123', - expressIP: '127.0.0.1', - signerIP: '127.0.0.1', + expressIp: '127.0.0.1', + signerIp: '127.0.0.1', signerTlsCert: 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNQRENDQWVLZ0F3SUJBZ0lSQU02TEFoaGxOMGo4ZlhxV2dLTWdENmN3Q2dZSUtvWkl6ajBFQXdJd09ERWYKTUIwR0ExVUVDaE1XYkc1a0lHRjFkRzluWlc1bGNtRjBaV1FnWTJWeWRERVZNQk1HQTFVRUF4TU1aV1UxTVdZeApOREV4TUdVMk1CNFhEVEkwTURneE9ERXlNVE14TWxvWERUSTFNVEF4TXpFeU1UTXhNbG93T0RFZk1CMEdBMVVFCkNoTVdiRzVrSUdGMWRHOW5aVzVsY21GMFpXUWdZMlZ5ZERFVk1CTUdBMVVFQXhNTVpXVTFNV1l4TkRFeE1HVTIKTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFclA0d2NXWFEwUWFFazhsVFNVTXBCa1d3ditFbQpxNTNyOWVSeVJUOTRkZGdVR0tTMFlRK0liZzFseVBRU3hiN0dXYloyWG9GUFdiK1VOM0lFMVlMQ2thT0J6RENCCnlUQU9CZ05WSFE4QkFmOEVCQU1DQXFRd0V3WURWUjBsQkF3d0NnWUlLd1lCQlFVSEF3RXdEd1lEVlIwVEFRSC8KQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVb3JmUkNVQytmaUNjZlE4cEhEUTFWaE1uMXBBd2NnWURWUjBSQkdzdwphWUlNWldVMU1XWXhOREV4TUdVMmdnbHNiMk5oYkdodmMzU0NDbk5wWjI1bGNtNXZaR1dDQ1d4dlkyRnNhRzl6CmRJSUVkVzVwZUlJS2RXNXBlSEJoWTJ0bGRJSUhZblZtWTI5dWJvY0Vmd0FBQVljUUFBQUFBQUFBQUFBQUFBQUEKQUFBQUFZY0VyQlFBQWpBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlFQXJuQ0xRTlgzeDZ1NjhIM2xCOG9wOUFKaApBd2RrUjhXOXNSaUZnZDJKM2tZQ0lHczFOVGM0T0toRzByNzVHUWpXb2x0SkJyOUtjWWVyR1V3aklCaCtvZ1h0Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K', signerTlsKey: @@ -12,7 +12,7 @@ export const apiData = { signerMacaroonRequestBody: { walletId: 'fakeid', passphrase: 'password123', - watchOnlyIP: '127.0.0.1', + watchOnlyIp: '127.0.0.1', }, unlockWalletRequestBody: { walletId: 'fakeid', diff --git a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts index d1cc6a275e..a5d385bde2 100644 --- a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts +++ b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts @@ -38,42 +38,50 @@ describe('Lightning signer routes', () => { } }); - it('should initialize lightning signer wallet', 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.userKey.id}`).reply(200, apiData.userKey), - 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 signerInitWalletNock = nock(lightningSignerConfigs.fakeid.url) - .post(`/v1/initwallet`) - .reply(200, signerApiData.initWallet); - - const wpWalletUpdateNock = nock(bgUrl).put(`/api/v2/tlnbtc/wallet/${apiData.wallet.id}`).reply(200); - - const req = { - bitgo: bitgo, - body: apiData.initWalletRequestBody, - params: { - coin: 'tlnbtc', - }, - config: { - lightningSignerFileSystemPath: 'lightningSignerFileSystemPath', - }, - } as unknown as express.Request; - - await handleInitLightningWallet(req); - - wpWalletUpdateNock.done(); - signerInitWalletNock.done(); - wpKeychainNocks.forEach((s) => s.done()); - wpWalletnock.done(); - readFileStub.calledOnceWith('lightningSignerFileSystemPath').should.be.true(); - readFileStub.restore(); - }); + for (const includingOptionalFields of [true, false]) { + it(`should initialize lightning signer wallet ${ + 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.userKey.id}`).reply(200, apiData.userKey), + 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 signerInitWalletNock = nock(lightningSignerConfigs.fakeid.url) + .post(`/v1/initwallet`) + .reply(200, signerApiData.initWallet); + + const wpWalletUpdateNock = nock(bgUrl).put(`/api/v2/tlnbtc/wallet/${apiData.wallet.id}`).reply(200); + + const req = { + bitgo: bitgo, + body: includingOptionalFields + ? apiData.initWalletRequestBody + : { ...apiData.initWalletRequestBody, signerTlsKey: undefined }, + params: { + coin: 'tlnbtc', + }, + config: { + lightningSignerFileSystemPath: 'lightningSignerFileSystemPath', + }, + } as unknown as express.Request; + + await handleInitLightningWallet(req); + + wpWalletUpdateNock.done(); + signerInitWalletNock.done(); + wpKeychainNocks.forEach((s) => s.done()); + wpWalletnock.done(); + readFileStub.calledOnceWith('lightningSignerFileSystemPath').should.be.true(); + readFileStub.restore(); + }); + } it('should get signer wallet state', async () => { const readFileStub = sinon.stub(fs.promises, 'readFile').resolves(JSON.stringify(lightningSignerConfigs)); @@ -107,6 +115,8 @@ describe('Lightning signer routes', () => { 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) diff --git a/modules/sdk-core/src/bitgo/lightning/codecs.ts b/modules/sdk-core/src/bitgo/lightning/codecs.ts index 8be02e62a0..6b959e630f 100644 --- a/modules/sdk-core/src/bitgo/lightning/codecs.ts +++ b/modules/sdk-core/src/bitgo/lightning/codecs.ts @@ -71,28 +71,16 @@ export const WatchOnly = t.type({ export type WatchOnly = t.TypeOf; -export const LightningWalletCoinSpecific = getCodecPair( - t.partial({ - encryptedSignerAdminMacaroon: t.string, - signerIP: IPAddress, - signerTlsCert: t.string, - encryptedSignerTlsKey: t.string, - watchOnly: WatchOnly, - encryptedSignerMacaroon: t.string, - }) -); - -export type LightningWalletCoinSpecific = t.TypeOf; - -export const UpdateLightningWallet = t.partial( - { - coinSpecific: LightningWalletCoinSpecific, - signature: t.string, - }, - 'UpdateLightningWallet' -); +export const UpdateLightningWalletSignedRequest = t.partial({ + encryptedSignerMacaroon: t.string, + encryptedSignerAdminMacaroon: t.string, + signerIp: t.string, + encryptedSignerTlsKey: t.string, + signerTlsCert: t.string, + watchOnlyAccounts: WatchOnly, +}); -export type UpdateLightningWallet = t.TypeOf; +export type UpdateLightningWalletSignedRequest = t.TypeOf; export const LndAmount = t.strict( { diff --git a/modules/sdk-core/src/bitgo/lightning/index.ts b/modules/sdk-core/src/bitgo/lightning/index.ts index 4acdc26d95..8658ade8da 100644 --- a/modules/sdk-core/src/bitgo/lightning/index.ts +++ b/modules/sdk-core/src/bitgo/lightning/index.ts @@ -1,5 +1,4 @@ export * from './signableJson'; export * from './signature'; export * from './lightningUtils'; -export * from './lightningWallet'; export * from './codecs'; diff --git a/modules/sdk-core/src/bitgo/lightning/lightningWallet.ts b/modules/sdk-core/src/bitgo/lightning/lightningWallet.ts deleted file mode 100644 index 8c1fee60ef..0000000000 --- a/modules/sdk-core/src/bitgo/lightning/lightningWallet.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Wallet } from '../wallet'; -import { LightningAuthKeychain, LightningKeychain, UpdateLightningWallet } from './codecs'; -import { decodeOrElse } from '../utils'; -import { unwrapLightningCoinSpecific } from './lightningUtils'; - -/** - * Get the lightning keychain for the given wallet. - */ -export async function getLightningKeychain(wallet: Wallet): Promise { - const coin = wallet.baseCoin; - if (coin.getFamily() !== 'lnbtc') { - throw new Error(`Invalid coin to get lightning Keychain: ${coin.getFamily()}`); - } - const keyIds = wallet.keyIds(); - if (keyIds.length !== 1) { - throw new Error(`Invalid number of key in lightning wallet: ${keyIds.length}`); - } - const keychain = await coin.keychains().get({ id: keyIds[0] }); - return decodeOrElse(LightningKeychain.name, LightningKeychain, keychain, (_) => { - // DON'T throw errors from decodeOrElse. It could leak sensitive information. - throw new Error(`Invalid user key`); - }); -} - -/** - * Get the lightning auth keychains for the given wallet. - */ -export async function getLightningAuthKeychains( - wallet: Wallet -): Promise<{ userAuthKey: LightningAuthKeychain; nodeAuthKey: LightningAuthKeychain }> { - const coin = wallet.baseCoin; - if (coin.getFamily() !== 'lnbtc') { - throw new Error(`Invalid coin to get lightning auth keychains: ${coin.getFamily()}`); - } - const authKeyIds = wallet.coinSpecific()?.keys; - if (authKeyIds?.length !== 2) { - throw new Error(`Invalid number of auth keys in lightning wallet: ${authKeyIds?.length}`); - } - const keychains = await Promise.all(authKeyIds.map((id) => coin.keychains().get({ id }))); - const authKeychains = keychains.map((keychain) => { - return decodeOrElse(LightningAuthKeychain.name, LightningAuthKeychain, keychain, (_) => { - // DON'T throw errors from decodeOrElse. It could leak sensitive information. - throw new Error(`Invalid lightning auth key: ${keychain?.id}`); - }); - }); - const [userAuthKey, nodeAuthKey] = (['userAuth', 'nodeAuth'] as const).map((purpose) => { - const keychain = authKeychains.find( - (k) => unwrapLightningCoinSpecific(k.coinSpecific, coin.getChain()).purpose === purpose - ); - if (!keychain) { - throw new Error(`Missing ${purpose} key`); - } - return keychain; - }); - - return { userAuthKey, nodeAuthKey }; -} - -/** - * Update the lightning wallet with the given data. - */ -export async function updateLightningWallet(wallet: Wallet, data: UpdateLightningWallet): Promise { - const coin = wallet.baseCoin; - if (coin.getFamily() !== 'lnbtc') { - throw new Error(`Invalid coin to update lightning wallet: ${coin.getFamily()}`); - } - return await wallet.bitgo.put(wallet.url()).send(data).result(); -} diff --git a/modules/sdk-core/src/bitgo/lightning/signature.ts b/modules/sdk-core/src/bitgo/lightning/signature.ts index 35ab1d1f0b..e7d650de5e 100644 --- a/modules/sdk-core/src/bitgo/lightning/signature.ts +++ b/modules/sdk-core/src/bitgo/lightning/signature.ts @@ -33,10 +33,10 @@ export function verifyMessageSignature( */ export function createMessageSignature( message: Signable, - prv: string, + xprv: string, network: utxolib.Network = utxolib.networks.bitcoin ): string { const requestString = JSON.stringify(canonicalizeObject(message)); - const prvKey = utxolib.bip32.fromBase58(prv, network); + const prvKey = utxolib.bip32.fromBase58(xprv, network); return signMessage(requestString, prvKey, network).toString('hex'); } diff --git a/modules/sdk-core/src/bitgo/wallet/lightning.ts b/modules/sdk-core/src/bitgo/wallet/lightning.ts index bc5788ed84..1b715ec41e 100644 --- a/modules/sdk-core/src/bitgo/wallet/lightning.ts +++ b/modules/sdk-core/src/bitgo/wallet/lightning.ts @@ -1,4 +1,12 @@ import { IWallet } from './iWallet'; +import { + createMessageSignature, + LightningAuthKeychain, + LightningKeychain, + unwrapLightningCoinSpecific, + UpdateLightningWalletSignedRequest, +} from '../lightning'; +import { decodeOrElse } from '../utils'; export interface ILightningWallet { /** @@ -12,12 +20,43 @@ export interface ILightningWallet { * @param params Payment parameters (to be defined) */ payInvoice(params: unknown): Promise; + + /** + * Get the lightning keychain for the given wallet. + */ + getLightningKeychain(): Promise; + + /** + * Get the lightning auth keychains for the given wallet. + */ + getLightningAuthKeychains(): Promise<{ userAuthKey: LightningAuthKeychain; nodeAuthKey: LightningAuthKeychain }>; + + /** + * Updates the coin-specific configuration for a Lightning Wallet. + * + * @param {UpdateLightningWalletSignedRequest} 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. + * - `signerIp` (optional): The IP address of the Lightning signer node. + * - `encryptedSignerTlsKey` (optional): The wallet passphrase encrypted TLS key of the signer. + * - `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} A promise resolving to the updated wallet response or throwing an error if the update fails. + */ + updateWalletCoinSpecific(params: UpdateLightningWalletSignedRequest, passphrase: string): Promise; } export class SelfCustodialLightningWallet implements ILightningWallet { public wallet: IWallet; constructor(wallet: IWallet) { + const coin = wallet.baseCoin; + if (coin.getFamily() !== 'lnbtc') { + throw new Error(`Invalid coin to update lightning wallet: ${coin.getFamily()}`); + } this.wallet = wallet; } @@ -28,4 +67,60 @@ export class SelfCustodialLightningWallet implements ILightningWallet { async payInvoice(params: unknown): Promise { throw new Error('Method not implemented.'); } + + async getLightningKeychain(): Promise { + const keyIds = this.wallet.keyIds(); + if (keyIds.length !== 1) { + throw new Error(`Invalid number of key in lightning wallet: ${keyIds.length}`); + } + const keychain = await this.wallet.baseCoin.keychains().get({ id: keyIds[0] }); + return decodeOrElse(LightningKeychain.name, LightningKeychain, keychain, (_) => { + // DON'T throw errors from decodeOrElse. It could leak sensitive information. + throw new Error(`Invalid user key`); + }); + } + + async getLightningAuthKeychains(): Promise<{ + userAuthKey: LightningAuthKeychain; + nodeAuthKey: LightningAuthKeychain; + }> { + const authKeyIds = this.wallet.coinSpecific()?.keys; + if (authKeyIds?.length !== 2) { + throw new Error(`Invalid number of auth keys in lightning wallet: ${authKeyIds?.length}`); + } + const coin = this.wallet.baseCoin; + const keychains = await Promise.all(authKeyIds.map((id) => coin.keychains().get({ id }))); + const authKeychains = keychains.map((keychain) => { + return decodeOrElse(LightningAuthKeychain.name, LightningAuthKeychain, keychain, (_) => { + // DON'T throw errors from decodeOrElse. It could leak sensitive information. + throw new Error(`Invalid lightning auth key: ${keychain?.id}`); + }); + }); + const [userAuthKey, nodeAuthKey] = (['userAuth', 'nodeAuth'] as const).map((purpose) => { + const keychain = authKeychains.find( + (k) => unwrapLightningCoinSpecific(k.coinSpecific, coin.getChain()).purpose === purpose + ); + if (!keychain) { + throw new Error(`Missing ${purpose} key`); + } + return keychain; + }); + + return { userAuthKey, nodeAuthKey }; + } + + async updateWalletCoinSpecific(params: UpdateLightningWalletSignedRequest, passphrase: string): Promise { + const { userAuthKey } = await this.getLightningAuthKeychains(); + const signature = createMessageSignature( + params, + this.wallet.bitgo.decrypt({ password: passphrase, input: userAuthKey.encryptedPrv }) + ); + const coinSpecific = { + [this.wallet.coin()]: { + signedRequest: params, + signature, + }, + }; + return await this.wallet.bitgo.put(this.wallet.url()).send({ coinSpecific }).result(); + } } diff --git a/modules/sdk-core/test/unit/bitgo/lightning/codecs.ts b/modules/sdk-core/test/unit/bitgo/lightning/codecs.ts index 853cd84e78..4dc6e950b2 100644 --- a/modules/sdk-core/test/unit/bitgo/lightning/codecs.ts +++ b/modules/sdk-core/test/unit/bitgo/lightning/codecs.ts @@ -2,7 +2,12 @@ import * as t from 'io-ts'; import assert from 'assert'; import * as E from 'fp-ts/Either'; import { pipe } from 'fp-ts/function'; -import { IPAddress, LightningAuthKeychain, LightningKeychain, UpdateLightningWallet } from '../../../../src'; +import { + IPAddress, + LightningAuthKeychain, + LightningKeychain, + UpdateLightningWalletSignedRequest, +} from '../../../../src'; function describeCodec(c: t.Type, valid: unknown[], invalid: unknown[]) { describe('Codec ' + c.name, function () { @@ -96,36 +101,24 @@ describe('Codecs', function () { ); describeCodec( - UpdateLightningWallet, + UpdateLightningWalletSignedRequest, [ { - coinSpecific: { - lnbtc: { - encryptedSignerAdminMacaroon: 'encryptedSignerAdminMacaroon', - signerIP: '127.0.0.1', - signerTlsCert: 'signerTlsCert', - encryptedSignerTlsKey: 'encryptedSignerTlsKey', - 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', - }, - }, - }, - { - coinSpecific: { - tlnbtc: { - encryptedSignerAdminMacaroon: 'encryptedSignerAdminMacaroon', - }, + encryptedSignerAdminMacaroon: 'encryptedSignerAdminMacaroon', + signerIp: '127.0.0.1', + signerTlsCert: 'signerTlsCert', + encryptedSignerTlsKey: 'encryptedSignerTlsKey', + 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', }, { - coinSpecific: { - tlnbtc: {}, - }, + encryptedSignerAdminMacaroon: 'encryptedSignerAdminMacaroon', }, + {}, ], [null, 'abg', 1] );