Skip to content

Commit

Permalink
feat(abstract-lightning): add custodial lightning api functions
Browse files Browse the repository at this point in the history
split lightning wallet to self and custodial

Ticket: BTC-0
  • Loading branch information
saravanan7mani committed Mar 8, 2025
1 parent a8f5223 commit 0a0ce29
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 194 deletions.
13 changes: 13 additions & 0 deletions modules/abstract-lightning/src/wallet/custodialLightning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ILightningWallet, LightningWallet } from './lightning';
import * as sdkcore from '@bitgo/sdk-core';

export type ICustodialLightningWallet = ILightningWallet;

export class CustodialLightningWallet extends LightningWallet implements ICustodialLightningWallet {
constructor(wallet: sdkcore.IWallet) {
super(wallet);
if (wallet.type() !== 'custodial') {
throw new Error(`Invalid lightning wallet type for custodial lightning: ${wallet.type()}`);
}
}
}
2 changes: 2 additions & 0 deletions modules/abstract-lightning/src/wallet/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './lightning';
export * from './custodialLightning';
export * from './selfCustodialLightning';
export * from './wallet';
288 changes: 139 additions & 149 deletions modules/abstract-lightning/src/wallet/lightning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
Transaction,
TransactionQuery,
PaymentInfo,
BackupResponse,
PaymentQuery,
} from '../codecs';
import { LightningPaymentIntent, LightningPaymentRequest } from '@bitgo/public-types';
Expand All @@ -38,34 +37,145 @@ export type PayInvoiceResponse = {
paymentStatus?: LndCreatePaymentResponse;
};

export interface ILightningWallet {
/**
* Get the lightning keychain for the given wallet.
*/
getLightningKeychain(): Promise<LightningKeychain>;
/**
* Get the lightning keychain for the given wallet.
*/
export async function getLightningKeychain(wallet: sdkcore.IWallet): Promise<LightningKeychain> {
const coin = wallet.baseCoin;
if (coin.getFamily() !== 'lnbtc') {
throw new Error(`Invalid coin to get lightning wallet key: ${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 sdkcore.decodeOrElse(LightningKeychain.name, LightningKeychain, keychain, (_) => {
throw new Error(`Invalid user key`);
});
}

/**
* Get the lightning auth keychains for the given wallet.
*/
getLightningAuthKeychains(): Promise<{ userAuthKey: LightningAuthKeychain; nodeAuthKey: LightningAuthKeychain }>;
/**
* Get the lightning auth keychains for the given wallet.
*/
export async function getLightningAuthKeychains(wallet: sdkcore.IWallet): Promise<{
userAuthKey: LightningAuthKeychain;
nodeAuthKey: LightningAuthKeychain;
}> {
const coin = wallet.baseCoin;
if (coin.getFamily() !== 'lnbtc') {
throw new Error(`Invalid coin to get lightning wallet auth keys: ${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 sdkcore.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;
});

/**
* Updates the coin-specific configuration for a Lightning Wallet.
*
* @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.
* @returns {Promise<unknown>} A promise resolving to the updated wallet response or throwing an error if the update fails.
*/
updateWalletCoinSpecific(params: UpdateLightningWalletClientRequest): Promise<unknown>;
return { userAuthKey, nodeAuthKey };
}

function encryptWalletUpdateRequest(
wallet: sdkcore.IWallet,
params: UpdateLightningWalletClientRequest,
userAuthKey: LightningAuthKeychain
): UpdateLightningWalletEncryptedRequest {
const coinName = wallet.coin() as 'tlnbtc' | 'lnbtc';

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

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

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

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

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

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

/**
* Updates the coin-specific configuration for a Lightning Wallet.
*
* @param {Wallet} wallet - Wallet.
* @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.
* @returns {Promise<unknown>} A promise resolving to the updated wallet response or throwing an error if the update fails.
*/
export async function updateWalletCoinSpecific(
wallet: sdkcore.IWallet,
params: UpdateLightningWalletClientRequest
): Promise<unknown> {
sdkcore.decodeOrElse(
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`);
}
);

const { userAuthKey } = await getLightningAuthKeychains(wallet);
const updateRequestWithEncryption = encryptWalletUpdateRequest(wallet, params, userAuthKey);
const signature = createMessageSignature(
updateRequestWithEncryption,
wallet.bitgo.decrypt({ password: params.passphrase, input: userAuthKey.encryptedPrv })
);
const coinSpecific = {
[wallet.coin()]: {
signedRequest: updateRequestWithEncryption,
signature,
},
};
return await wallet.bitgo.put(wallet.url()).send({ coinSpecific }).result();
}

export interface ILightningWallet {
/**
* Creates a lightning invoice
* @param {object} params Invoice parameters
Expand Down Expand Up @@ -138,130 +248,19 @@ export interface ILightningWallet {
* @returns {Promise<Transaction[]>} List of transactions
*/
listTransactions(params: TransactionQuery): Promise<Transaction[]>;

/**
* Get the channel backup for the given wallet.
* @returns {Promise<BackupResponse>} A promise resolving to the channel backup
*/
getChannelBackup(): Promise<BackupResponse>;
}

export class SelfCustodialLightningWallet implements ILightningWallet {
export class LightningWallet implements ILightningWallet {
public wallet: sdkcore.IWallet;

constructor(wallet: sdkcore.IWallet) {
const coin = wallet.baseCoin;
if (coin.getFamily() !== 'lnbtc') {
throw new Error(`Invalid coin to update lightning wallet: ${coin.getFamily()}`);
throw new Error(`Invalid coin for lightning wallet: ${coin.getFamily()}`);
}
this.wallet = wallet;
}

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 getLightningKeychain(): Promise<LightningKeychain> {
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 sdkcore.decodeOrElse(LightningKeychain.name, LightningKeychain, keychain, (_) => {
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 sdkcore.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: UpdateLightningWalletClientRequest): Promise<unknown> {
sdkcore.decodeOrElse(
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`);
}
);

const { userAuthKey } = await this.getLightningAuthKeychains();
const updateRequestWithEncryption = this.encryptWalletUpdateRequest(params, userAuthKey);
const signature = createMessageSignature(
updateRequestWithEncryption,
this.wallet.bitgo.decrypt({ password: params.passphrase, input: userAuthKey.encryptedPrv })
);
const coinSpecific = {
[this.wallet.coin()]: {
signedRequest: updateRequestWithEncryption,
signature,
},
};
return await this.wallet.bitgo.put(this.wallet.url()).send({ coinSpecific }).result();
}

async createInvoice(params: CreateInvoiceBody): Promise<Invoice> {
const createInvoiceResponse = await this.wallet.bitgo
.post(this.wallet.baseCoin.url(`/wallet/${this.wallet.id()}/lightning/invoice`))
Expand Down Expand Up @@ -297,7 +296,7 @@ export class SelfCustodialLightningWallet implements ILightningWallet {
const reqId = new RequestTracer();
this.wallet.bitgo.setRequestTracer(reqId);

const { userAuthKey } = await this.getLightningAuthKeychains();
const { userAuthKey } = await getLightningAuthKeychains(this.wallet);
const signature = createMessageSignature(
t.exact(LightningPaymentRequest).encode(params),
this.wallet.bitgo.decrypt({ password: params.passphrase, input: userAuthKey.encryptedPrv })
Expand Down Expand Up @@ -387,13 +386,4 @@ export class SelfCustodialLightningWallet implements ILightningWallet {
throw new Error(`Invalid transaction list response: ${error}`);
});
}

async getChannelBackup(): Promise<BackupResponse> {
const backupResponse = await this.wallet.bitgo
.get(this.wallet.baseCoin.url(`/wallet/${this.wallet.id()}/lightning/backup`))
.result();
return sdkcore.decodeOrElse(BackupResponse.name, BackupResponse, backupResponse, (error) => {
throw new Error(`Invalid backup response: ${error}`);
});
}
}
29 changes: 29 additions & 0 deletions modules/abstract-lightning/src/wallet/selfCustodialLightning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as sdkcore from '@bitgo/sdk-core';
import { BackupResponse } from '../codecs';
import { ILightningWallet, LightningWallet } from './lightning';

export interface ISelfCustodialLightningWallet extends ILightningWallet {
/**
* Get the channel backup for the given wallet.
* @returns {Promise<BackupResponse>} A promise resolving to the channel backup
*/
getChannelBackup(): Promise<BackupResponse>;
}

export class SelfCustodialLightningWallet extends LightningWallet implements ISelfCustodialLightningWallet {
constructor(wallet: sdkcore.IWallet) {
super(wallet);
if (wallet.type() !== 'hot') {
throw new Error(`Invalid lightning wallet type for self custodial lightning: ${wallet.type()}`);
}
}

async getChannelBackup(): Promise<BackupResponse> {
const backupResponse = await this.wallet.bitgo
.get(this.wallet.baseCoin.url(`/wallet/${this.wallet.id()}/lightning/backup`))
.result();
return sdkcore.decodeOrElse(BackupResponse.name, BackupResponse, backupResponse, (error) => {
throw new Error(`Invalid backup response: ${error}`);
});
}
}
Loading

0 comments on commit 0a0ce29

Please sign in to comment.