Skip to content

Commit 9f3813b

Browse files
Merge pull request #5785 from BitGo/BTC-1934-lightning-wallet-sharing
feat(sdk-core): lightning wallet sharing support
2 parents ac354b4 + 2d1d566 commit 9f3813b

File tree

3 files changed

+105
-27
lines changed

3 files changed

+105
-27
lines changed

modules/bitgo/test/v2/unit/wallet.ts

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2218,42 +2218,77 @@ describe('V2 Wallet:', function () {
22182218
createShareNock.isDone().should.be.True();
22192219
});
22202220

2221-
it('should use keychain pub to share hot wallet', async function () {
2221+
describe('Hot Wallet Sharing', function () {
22222222
const userId = '123';
22232223
const email = '[email protected]';
22242224
const permissions = 'view,spend';
22252225
const toKeychain = utxoLib.bip32.fromSeed(Buffer.from('deadbeef02deadbeef02deadbeef02deadbeef02', 'hex'));
22262226
const path = 'm/999999/1/1';
22272227
const pubkey = toKeychain.derivePath(path).publicKey.toString('hex');
22282228
const walletPassphrase = 'bitgo1234';
2229-
2230-
const getSharingKeyNock = nock(bgUrl)
2231-
.post('/api/v1/user/sharingkey', { email })
2232-
.reply(200, { userId, pubkey, path });
2233-
22342229
const pub = 'Zo1ggzTUKMY5bYnDvT5mtVeZxzf2FaLTbKkmvGUhUQk';
2235-
const getKeyNock = nock(bgUrl)
2236-
.get(`/api/v2/tbtc/key/${wallet.keyIds()[0]}`)
2237-
.reply(200, {
2238-
id: wallet.keyIds()[0],
2239-
pub,
2240-
source: 'user',
2241-
encryptedPrv: bitgo.encrypt({ input: 'xprv1', password: walletPassphrase }),
2242-
coinSpecific: {},
2243-
});
22442230

2245-
const stub = sinon.stub(wallet, 'createShare').callsFake(async (options) => {
2246-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2247-
options!.keychain!.pub!.should.not.be.undefined();
2248-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2249-
options!.keychain!.pub!.should.equal(pub);
2250-
return undefined;
2251-
});
2252-
await wallet.shareWallet({ email, permissions, walletPassphrase });
2231+
const lightningCoin: any = bitgo.coin('tlnbtc');
2232+
const lightningWalletData = {
2233+
id: '5b34252f1bf349930e34020a00000001',
2234+
coin: 'tlnbtc',
2235+
keys: ['5b3424f91bf349930e34017500000001'],
2236+
coinSpecific: { keys: ['5b3424f91bf349930e34017600000000', '5b3424f91bf349930e34017700000000'] },
2237+
type: 'hot',
2238+
};
2239+
const lightningWallet = new Wallet(bitgo, lightningCoin, lightningWalletData);
2240+
2241+
for (const hotWallet of [wallet, lightningWallet] as const) {
2242+
it(`should use keychain pub to share ${hotWallet.coin()} hot wallet`, async function () {
2243+
const getSharingKeyNock = nock(bgUrl)
2244+
.post('/api/v1/user/sharingkey', { email })
2245+
.reply(200, { userId, pubkey, path });
2246+
2247+
const getKeyNocks: nock.Scope[] = [];
2248+
if (hotWallet.baseCoin.getFamily() === 'lnbtc') {
2249+
for (let i = 0; i < 2; i++) {
2250+
const keyId = lightningWalletData.coinSpecific.keys[i];
2251+
const getKeyNock = nock(bgUrl)
2252+
.get(`/api/v2/tlnbtc/key/${keyId}`)
2253+
.reply(200, {
2254+
id: keyId,
2255+
pub: i === 0 ? pub : 'Zo1ggzTUKMY5bYnDvT5mtVeZxzf2FaLTbKkmvGUhUQm',
2256+
source: 'user',
2257+
encryptedPrv: bitgo.encrypt({ input: 'xprv' + i, password: walletPassphrase }),
2258+
coinSpecific:
2259+
i === 0
2260+
? { [hotWallet.baseCoin.getChain()]: { purpose: 'userAuth' } }
2261+
: { [hotWallet.baseCoin.getChain()]: { purpose: 'nodeAuth' } },
2262+
});
2263+
getKeyNocks.push(getKeyNock);
2264+
}
2265+
} else {
2266+
const getKeyNock = nock(bgUrl)
2267+
.get(`/api/v2/tbtc/key/${wallet.keyIds()[0]}`)
2268+
.reply(200, {
2269+
id: wallet.keyIds()[0],
2270+
pub,
2271+
source: 'user',
2272+
encryptedPrv: bitgo.encrypt({ input: 'xprv1', password: walletPassphrase }),
2273+
coinSpecific: {},
2274+
});
2275+
getKeyNocks.push(getKeyNock);
2276+
}
22532277

2254-
stub.calledOnce.should.be.true();
2255-
getSharingKeyNock.isDone().should.be.True();
2256-
getKeyNock.isDone().should.be.True();
2278+
const stub = sinon.stub(hotWallet, 'createShare').callsFake(async (options) => {
2279+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2280+
options!.keychain!.pub!.should.not.be.undefined();
2281+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2282+
options!.keychain!.pub!.should.equal(pub);
2283+
return undefined;
2284+
});
2285+
await hotWallet.shareWallet({ email, permissions, walletPassphrase });
2286+
2287+
stub.calledOnce.should.be.true();
2288+
getSharingKeyNock.isDone().should.be.True();
2289+
getKeyNocks.every((v) => v.isDone().should.be.True());
2290+
});
2291+
}
22572292
});
22582293

22592294
it('should provide skipKeychain to wallet share api for hot wallet', async function () {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Wallet } from '../wallet';
2+
import { KeychainWithEncryptedPrv } from '../keychain';
3+
4+
/**
5+
* Get the lightning auth key for the given purpose
6+
*/
7+
export async function getLightningAuthKey(
8+
wallet: Wallet,
9+
purpose: 'userAuth' | 'nodeAuth'
10+
): Promise<KeychainWithEncryptedPrv> {
11+
if (wallet.baseCoin.getFamily() !== 'lnbtc') {
12+
throw new Error(`Invalid lightning coin family: ${wallet.baseCoin.getFamily()}`);
13+
}
14+
const authKeyIds = wallet.coinSpecific()?.keys;
15+
if (authKeyIds?.length !== 2) {
16+
throw new Error(`Invalid number of auth keys in lightning wallet: ${authKeyIds?.length}`);
17+
}
18+
const keychains = await Promise.all(authKeyIds.map((id) => wallet.baseCoin.keychains().get({ id })));
19+
const userAuthKeychain = keychains.find((v) => {
20+
const coinSpecific = v?.coinSpecific?.[wallet.baseCoin.getChain()];
21+
return (
22+
coinSpecific && typeof coinSpecific === 'object' && 'purpose' in coinSpecific && coinSpecific.purpose === purpose
23+
);
24+
});
25+
if (userAuthKeychain?.encryptedPrv) {
26+
return userAuthKeychain as KeychainWithEncryptedPrv;
27+
}
28+
throw new Error(`Missing lightning ${purpose} keychain with encrypted private key`);
29+
}

modules/sdk-core/src/bitgo/wallet/wallet.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ import { TxSendBody } from '@bitgo/public-types';
111111
import { AddressBook, IAddressBook } from '../address-book';
112112
import { IRequestTracer } from '../../api';
113113
import { getTxRequestApiVersion, validateTxRequestApiVersion } from '../utils/txRequest';
114+
import { getLightningAuthKey } from '../lightning/lightningWalletUtil';
114115

115116
const debug = require('debug')('bitgo:v2:wallet');
116117

@@ -1609,6 +1610,19 @@ export class Wallet implements IWallet {
16091610
return this.bitgo.post(url).send({ shareOptions: params }).result();
16101611
}
16111612

1613+
/**
1614+
* Gets keychain with encrypted private key to be shared for wallet sharing.
1615+
*/
1616+
private async getEncryptedWalletKeychainForWalletSharing(): Promise<KeychainWithEncryptedPrv> {
1617+
if (this.baseCoin.getFamily() === 'lnbtc') {
1618+
// lightning coin does not use user key to sign the transactions from SDK.
1619+
// it uses user auth key instead.
1620+
return await getLightningAuthKey(this, 'userAuth');
1621+
} else {
1622+
return await this.getEncryptedUserKeychain();
1623+
}
1624+
}
1625+
16121626
async prepareSharedKeychain(
16131627
walletPassphrase: string | undefined,
16141628
pubkey: string,
@@ -1617,7 +1631,7 @@ export class Wallet implements IWallet {
16171631
let sharedKeychain: SharedKeyChain = {};
16181632

16191633
try {
1620-
const keychain = await this.getEncryptedUserKeychain();
1634+
const keychain = await this.getEncryptedWalletKeychainForWalletSharing();
16211635

16221636
// Decrypt the user key with a passphrase
16231637
if (keychain.encryptedPrv) {

0 commit comments

Comments
 (0)