Skip to content

Commit e5796c0

Browse files
authored
Merge pull request #5722 from BitGo/BTC-0-custodial-ln-api
feat(abstract-lightning): add custodial lightning api functions
2 parents ba93a73 + 16b825a commit e5796c0

File tree

12 files changed

+280
-211
lines changed

12 files changed

+280
-211
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { ILightningWallet, LightningWallet } from './lightning';
2+
import * as sdkcore from '@bitgo/sdk-core';
3+
4+
export type ICustodialLightningWallet = ILightningWallet;
5+
6+
export class CustodialLightningWallet extends LightningWallet implements ICustodialLightningWallet {
7+
constructor(wallet: sdkcore.IWallet) {
8+
super(wallet);
9+
if (wallet.type() !== 'custodial') {
10+
throw new Error(`Invalid lightning wallet type for custodial lightning: ${wallet.type()}`);
11+
}
12+
}
13+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export * from './lightning';
2+
export * from './custodialLightning';
3+
export * from './selfCustodialLightning';
24
export * from './wallet';

modules/abstract-lightning/src/wallet/lightning.ts

Lines changed: 139 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {
2525
Transaction,
2626
TransactionQuery,
2727
PaymentInfo,
28-
BackupResponse,
2928
PaymentQuery,
3029
} from '../codecs';
3130
import { LightningPaymentIntent, LightningPaymentRequest } from '@bitgo/public-types';
@@ -38,34 +37,145 @@ export type PayInvoiceResponse = {
3837
paymentStatus?: LndCreatePaymentResponse;
3938
};
4039

41-
export interface ILightningWallet {
42-
/**
43-
* Get the lightning keychain for the given wallet.
44-
*/
45-
getLightningKeychain(): Promise<LightningKeychain>;
40+
/**
41+
* Get the lightning keychain for the given wallet.
42+
*/
43+
export async function getLightningKeychain(wallet: sdkcore.IWallet): Promise<LightningKeychain> {
44+
const coin = wallet.baseCoin;
45+
if (coin.getFamily() !== 'lnbtc') {
46+
throw new Error(`Invalid coin to get lightning wallet key: ${coin.getFamily()}`);
47+
}
48+
const keyIds = wallet.keyIds();
49+
if (keyIds.length !== 1) {
50+
throw new Error(`Invalid number of key in lightning wallet: ${keyIds.length}`);
51+
}
52+
const keychain = await coin.keychains().get({ id: keyIds[0] });
53+
return sdkcore.decodeOrElse(LightningKeychain.name, LightningKeychain, keychain, (_) => {
54+
throw new Error(`Invalid user key`);
55+
});
56+
}
4657

47-
/**
48-
* Get the lightning auth keychains for the given wallet.
49-
*/
50-
getLightningAuthKeychains(): Promise<{ userAuthKey: LightningAuthKeychain; nodeAuthKey: LightningAuthKeychain }>;
58+
/**
59+
* Get the lightning auth keychains for the given wallet.
60+
*/
61+
export async function getLightningAuthKeychains(wallet: sdkcore.IWallet): Promise<{
62+
userAuthKey: LightningAuthKeychain;
63+
nodeAuthKey: LightningAuthKeychain;
64+
}> {
65+
const coin = wallet.baseCoin;
66+
if (coin.getFamily() !== 'lnbtc') {
67+
throw new Error(`Invalid coin to get lightning wallet auth keys: ${coin.getFamily()}`);
68+
}
69+
const authKeyIds = wallet.coinSpecific()?.keys;
70+
if (authKeyIds?.length !== 2) {
71+
throw new Error(`Invalid number of auth keys in lightning wallet: ${authKeyIds?.length}`);
72+
}
73+
const keychains = await Promise.all(authKeyIds.map((id) => coin.keychains().get({ id })));
74+
const authKeychains = keychains.map((keychain) => {
75+
return sdkcore.decodeOrElse(LightningAuthKeychain.name, LightningAuthKeychain, keychain, (_) => {
76+
// DON'T throw errors from decodeOrElse. It could leak sensitive information.
77+
throw new Error(`Invalid lightning auth key: ${keychain?.id}`);
78+
});
79+
});
80+
const [userAuthKey, nodeAuthKey] = (['userAuth', 'nodeAuth'] as const).map((purpose) => {
81+
const keychain = authKeychains.find(
82+
(k) => unwrapLightningCoinSpecific(k.coinSpecific, coin.getChain()).purpose === purpose
83+
);
84+
if (!keychain) {
85+
throw new Error(`Missing ${purpose} key`);
86+
}
87+
return keychain;
88+
});
5189

52-
/**
53-
* Updates the coin-specific configuration for a Lightning Wallet.
54-
*
55-
* @param {UpdateLightningWalletClientRequest} params - The parameters containing the updated wallet-specific details.
56-
* - `encryptedSignerMacaroon` (optional): This macaroon is used by the watch-only node to ask the signer node to sign transactions.
57-
* Encrypted with ECDH secret key from private key of wallet's user auth key and public key of lightning service.
58-
* - `encryptedSignerAdminMacaroon` (optional): Generated when initializing the wallet of the signer node.
59-
* Encrypted with client's wallet passphrase.
60-
* - `signerHost` (optional): The host address of the Lightning signer node.
61-
* - `encryptedSignerTlsKey` (optional): The wallet passphrase encrypted TLS key of the signer.
62-
* - `passphrase` (required): The wallet passphrase.
63-
* - `signerTlsCert` (optional): The TLS certificate of the signer.
64-
* - `watchOnlyAccounts` (optional): These are the accounts used to initialize the watch-only wallet.
65-
* @returns {Promise<unknown>} A promise resolving to the updated wallet response or throwing an error if the update fails.
66-
*/
67-
updateWalletCoinSpecific(params: UpdateLightningWalletClientRequest): Promise<unknown>;
90+
return { userAuthKey, nodeAuthKey };
91+
}
92+
93+
function encryptWalletUpdateRequest(
94+
wallet: sdkcore.IWallet,
95+
params: UpdateLightningWalletClientRequest,
96+
userAuthKey: LightningAuthKeychain
97+
): UpdateLightningWalletEncryptedRequest {
98+
const coinName = wallet.coin() as 'tlnbtc' | 'lnbtc';
99+
100+
const requestWithEncryption: Partial<UpdateLightningWalletClientRequest & UpdateLightningWalletEncryptedRequest> = {
101+
...params,
102+
};
103+
104+
const userAuthXprv = wallet.bitgo.decrypt({
105+
password: params.passphrase,
106+
input: userAuthKey.encryptedPrv,
107+
});
68108

109+
if (params.signerTlsKey) {
110+
requestWithEncryption.encryptedSignerTlsKey = wallet.bitgo.encrypt({
111+
password: params.passphrase,
112+
input: params.signerTlsKey,
113+
});
114+
}
115+
116+
if (params.signerAdminMacaroon) {
117+
requestWithEncryption.encryptedSignerAdminMacaroon = wallet.bitgo.encrypt({
118+
password: params.passphrase,
119+
input: params.signerAdminMacaroon,
120+
});
121+
}
122+
123+
if (params.signerMacaroon) {
124+
requestWithEncryption.encryptedSignerMacaroon = wallet.bitgo.encrypt({
125+
password: deriveLightningServiceSharedSecret(coinName, userAuthXprv).toString('hex'),
126+
input: params.signerMacaroon,
127+
});
128+
}
129+
130+
return t.exact(UpdateLightningWalletEncryptedRequest).encode(requestWithEncryption);
131+
}
132+
133+
/**
134+
* Updates the coin-specific configuration for a Lightning Wallet.
135+
*
136+
* @param {Wallet} wallet - Wallet.
137+
* @param {UpdateLightningWalletClientRequest} params - The parameters containing the updated wallet-specific details.
138+
* - `encryptedSignerMacaroon` (optional): This macaroon is used by the watch-only node to ask the signer node to sign transactions.
139+
* Encrypted with ECDH secret key from private key of wallet's user auth key and public key of lightning service.
140+
* - `encryptedSignerAdminMacaroon` (optional): Generated when initializing the wallet of the signer node.
141+
* Encrypted with client's wallet passphrase.
142+
* - `signerHost` (optional): The host address of the Lightning signer node.
143+
* - `encryptedSignerTlsKey` (optional): The wallet passphrase encrypted TLS key of the signer.
144+
* - `passphrase` (required): The wallet passphrase.
145+
* - `signerTlsCert` (optional): The TLS certificate of the signer.
146+
* - `watchOnlyAccounts` (optional): These are the accounts used to initialize the watch-only wallet.
147+
* @returns {Promise<unknown>} A promise resolving to the updated wallet response or throwing an error if the update fails.
148+
*/
149+
export async function updateWalletCoinSpecific(
150+
wallet: sdkcore.IWallet,
151+
params: UpdateLightningWalletClientRequest
152+
): Promise<unknown> {
153+
sdkcore.decodeOrElse(
154+
UpdateLightningWalletClientRequest.name,
155+
UpdateLightningWalletClientRequest,
156+
params,
157+
(errors) => {
158+
// DON'T throw errors from decodeOrElse. It could leak sensitive information.
159+
throw new Error(`Invalid params for lightning specific update wallet`);
160+
}
161+
);
162+
163+
const { userAuthKey } = await getLightningAuthKeychains(wallet);
164+
const updateRequestWithEncryption = encryptWalletUpdateRequest(wallet, params, userAuthKey);
165+
const signature = createMessageSignature(
166+
updateRequestWithEncryption,
167+
wallet.bitgo.decrypt({ password: params.passphrase, input: userAuthKey.encryptedPrv })
168+
);
169+
const coinSpecific = {
170+
[wallet.coin()]: {
171+
signedRequest: updateRequestWithEncryption,
172+
signature,
173+
},
174+
};
175+
return await wallet.bitgo.put(wallet.url()).send({ coinSpecific }).result();
176+
}
177+
178+
export interface ILightningWallet {
69179
/**
70180
* Creates a lightning invoice
71181
* @param {object} params Invoice parameters
@@ -138,130 +248,19 @@ export interface ILightningWallet {
138248
* @returns {Promise<Transaction[]>} List of transactions
139249
*/
140250
listTransactions(params: TransactionQuery): Promise<Transaction[]>;
141-
142-
/**
143-
* Get the channel backup for the given wallet.
144-
* @returns {Promise<BackupResponse>} A promise resolving to the channel backup
145-
*/
146-
getChannelBackup(): Promise<BackupResponse>;
147251
}
148252

149-
export class SelfCustodialLightningWallet implements ILightningWallet {
253+
export class LightningWallet implements ILightningWallet {
150254
public wallet: sdkcore.IWallet;
151255

152256
constructor(wallet: sdkcore.IWallet) {
153257
const coin = wallet.baseCoin;
154258
if (coin.getFamily() !== 'lnbtc') {
155-
throw new Error(`Invalid coin to update lightning wallet: ${coin.getFamily()}`);
259+
throw new Error(`Invalid coin for lightning wallet: ${coin.getFamily()}`);
156260
}
157261
this.wallet = wallet;
158262
}
159263

160-
private encryptWalletUpdateRequest(
161-
params: UpdateLightningWalletClientRequest,
162-
userAuthKey: LightningAuthKeychain
163-
): UpdateLightningWalletEncryptedRequest {
164-
const coinName = this.wallet.coin() as 'tlnbtc' | 'lnbtc';
165-
166-
const requestWithEncryption: Partial<UpdateLightningWalletClientRequest & UpdateLightningWalletEncryptedRequest> = {
167-
...params,
168-
};
169-
170-
const userAuthXprv = this.wallet.bitgo.decrypt({
171-
password: params.passphrase,
172-
input: userAuthKey.encryptedPrv,
173-
});
174-
175-
if (params.signerTlsKey) {
176-
requestWithEncryption.encryptedSignerTlsKey = this.wallet.bitgo.encrypt({
177-
password: params.passphrase,
178-
input: params.signerTlsKey,
179-
});
180-
}
181-
182-
if (params.signerAdminMacaroon) {
183-
requestWithEncryption.encryptedSignerAdminMacaroon = this.wallet.bitgo.encrypt({
184-
password: params.passphrase,
185-
input: params.signerAdminMacaroon,
186-
});
187-
}
188-
189-
if (params.signerMacaroon) {
190-
requestWithEncryption.encryptedSignerMacaroon = this.wallet.bitgo.encrypt({
191-
password: deriveLightningServiceSharedSecret(coinName, userAuthXprv).toString('hex'),
192-
input: params.signerMacaroon,
193-
});
194-
}
195-
196-
return t.exact(UpdateLightningWalletEncryptedRequest).encode(requestWithEncryption);
197-
}
198-
199-
async getLightningKeychain(): Promise<LightningKeychain> {
200-
const keyIds = this.wallet.keyIds();
201-
if (keyIds.length !== 1) {
202-
throw new Error(`Invalid number of key in lightning wallet: ${keyIds.length}`);
203-
}
204-
const keychain = await this.wallet.baseCoin.keychains().get({ id: keyIds[0] });
205-
return sdkcore.decodeOrElse(LightningKeychain.name, LightningKeychain, keychain, (_) => {
206-
throw new Error(`Invalid user key`);
207-
});
208-
}
209-
210-
async getLightningAuthKeychains(): Promise<{
211-
userAuthKey: LightningAuthKeychain;
212-
nodeAuthKey: LightningAuthKeychain;
213-
}> {
214-
const authKeyIds = this.wallet.coinSpecific()?.keys;
215-
if (authKeyIds?.length !== 2) {
216-
throw new Error(`Invalid number of auth keys in lightning wallet: ${authKeyIds?.length}`);
217-
}
218-
const coin = this.wallet.baseCoin;
219-
const keychains = await Promise.all(authKeyIds.map((id) => coin.keychains().get({ id })));
220-
const authKeychains = keychains.map((keychain) => {
221-
return sdkcore.decodeOrElse(LightningAuthKeychain.name, LightningAuthKeychain, keychain, (_) => {
222-
// DON'T throw errors from decodeOrElse. It could leak sensitive information.
223-
throw new Error(`Invalid lightning auth key: ${keychain?.id}`);
224-
});
225-
});
226-
const [userAuthKey, nodeAuthKey] = (['userAuth', 'nodeAuth'] as const).map((purpose) => {
227-
const keychain = authKeychains.find(
228-
(k) => unwrapLightningCoinSpecific(k.coinSpecific, coin.getChain()).purpose === purpose
229-
);
230-
if (!keychain) {
231-
throw new Error(`Missing ${purpose} key`);
232-
}
233-
return keychain;
234-
});
235-
236-
return { userAuthKey, nodeAuthKey };
237-
}
238-
239-
async updateWalletCoinSpecific(params: UpdateLightningWalletClientRequest): Promise<unknown> {
240-
sdkcore.decodeOrElse(
241-
UpdateLightningWalletClientRequest.name,
242-
UpdateLightningWalletClientRequest,
243-
params,
244-
(errors) => {
245-
// DON'T throw errors from decodeOrElse. It could leak sensitive information.
246-
throw new Error(`Invalid params for lightning specific update wallet`);
247-
}
248-
);
249-
250-
const { userAuthKey } = await this.getLightningAuthKeychains();
251-
const updateRequestWithEncryption = this.encryptWalletUpdateRequest(params, userAuthKey);
252-
const signature = createMessageSignature(
253-
updateRequestWithEncryption,
254-
this.wallet.bitgo.decrypt({ password: params.passphrase, input: userAuthKey.encryptedPrv })
255-
);
256-
const coinSpecific = {
257-
[this.wallet.coin()]: {
258-
signedRequest: updateRequestWithEncryption,
259-
signature,
260-
},
261-
};
262-
return await this.wallet.bitgo.put(this.wallet.url()).send({ coinSpecific }).result();
263-
}
264-
265264
async createInvoice(params: CreateInvoiceBody): Promise<Invoice> {
266265
const createInvoiceResponse = await this.wallet.bitgo
267266
.post(this.wallet.baseCoin.url(`/wallet/${this.wallet.id()}/lightning/invoice`))
@@ -297,7 +296,7 @@ export class SelfCustodialLightningWallet implements ILightningWallet {
297296
const reqId = new RequestTracer();
298297
this.wallet.bitgo.setRequestTracer(reqId);
299298

300-
const { userAuthKey } = await this.getLightningAuthKeychains();
299+
const { userAuthKey } = await getLightningAuthKeychains(this.wallet);
301300
const signature = createMessageSignature(
302301
t.exact(LightningPaymentRequest).encode(params),
303302
this.wallet.bitgo.decrypt({ password: params.passphrase, input: userAuthKey.encryptedPrv })
@@ -387,13 +386,4 @@ export class SelfCustodialLightningWallet implements ILightningWallet {
387386
throw new Error(`Invalid transaction list response: ${error}`);
388387
});
389388
}
390-
391-
async getChannelBackup(): Promise<BackupResponse> {
392-
const backupResponse = await this.wallet.bitgo
393-
.get(this.wallet.baseCoin.url(`/wallet/${this.wallet.id()}/lightning/backup`))
394-
.result();
395-
return sdkcore.decodeOrElse(BackupResponse.name, BackupResponse, backupResponse, (error) => {
396-
throw new Error(`Invalid backup response: ${error}`);
397-
});
398-
}
399389
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as sdkcore from '@bitgo/sdk-core';
2+
import { BackupResponse } from '../codecs';
3+
import { ILightningWallet, LightningWallet } from './lightning';
4+
5+
export interface ISelfCustodialLightningWallet extends ILightningWallet {
6+
/**
7+
* Get the channel backup for the given wallet.
8+
* @returns {Promise<BackupResponse>} A promise resolving to the channel backup
9+
*/
10+
getChannelBackup(): Promise<BackupResponse>;
11+
}
12+
13+
export class SelfCustodialLightningWallet extends LightningWallet implements ISelfCustodialLightningWallet {
14+
constructor(wallet: sdkcore.IWallet) {
15+
super(wallet);
16+
if (wallet.type() !== 'hot') {
17+
throw new Error(`Invalid lightning wallet type for self custodial lightning: ${wallet.type()}`);
18+
}
19+
}
20+
21+
async getChannelBackup(): Promise<BackupResponse> {
22+
const backupResponse = await this.wallet.bitgo
23+
.get(this.wallet.baseCoin.url(`/wallet/${this.wallet.id()}/lightning/backup`))
24+
.result();
25+
return sdkcore.decodeOrElse(BackupResponse.name, BackupResponse, backupResponse, (error) => {
26+
throw new Error(`Invalid backup response: ${error}`);
27+
});
28+
}
29+
}

0 commit comments

Comments
 (0)