Skip to content

Commit 2d1d566

Browse files
feat(sdk-core): lightning wallet sharing support
Lightning uses user auth key instead of user key to authorise transactions to BitGo backend. Actual transaction signing are done using user key, but that is controlled by BitGo for custodial lightning wallets. So user auth key from coinSpecific of a wallet should be the one shared by the users during wallet sharings. TICKET: BTC-1934
1 parent ec69f1c commit 2d1d566

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)