Skip to content

Commit

Permalink
feat(sdk-core): add custodial lightning wallet creation
Browse files Browse the repository at this point in the history
use wallet type to add custodial lightning

Ticket: BTC-0
  • Loading branch information
saravanan7mani committed Mar 9, 2025
1 parent 2d9cb55 commit 8ff9738
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 74 deletions.
152 changes: 88 additions & 64 deletions modules/bitgo/test/v2/unit/lightning/lightningWallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ describe('Lightning wallets', function () {
passphrase: 'pass123',
enterprise: 'ent123',
passcodeEncryptionCode: 'code123',
type: 'custodial',
})
.should.be.rejectedWith(
"error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.label, expected string."
Expand All @@ -79,6 +80,7 @@ describe('Lightning wallets', function () {
label: 'my ln wallet',
enterprise: 'ent123',
passcodeEncryptionCode: 'code123',
type: 'custodial',
})
.should.be.rejectedWith(
"error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.passphrase, expected string."
Expand All @@ -89,6 +91,7 @@ describe('Lightning wallets', function () {
label: 'my ln wallet',
passphrase: 'pass123',
passcodeEncryptionCode: 'code123',
type: 'custodial',
})
.should.be.rejectedWith(
"error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.enterprise, expected string."
Expand All @@ -99,6 +102,7 @@ describe('Lightning wallets', function () {
label: 'my ln wallet',
passphrase: 'pass123',
enterprise: 'ent123',
type: 'custodial',
})
.should.be.rejectedWith(
"error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.passcodeEncryptionCode, expected string."
Expand All @@ -110,6 +114,7 @@ describe('Lightning wallets', function () {
passphrase: 'pass123',
enterprise: 'ent123',
passcodeEncryptionCode: 'code123',
type: 'custodial',
})
.should.be.rejectedWith(
"error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.label, expected string."
Expand All @@ -121,6 +126,7 @@ describe('Lightning wallets', function () {
passphrase: 123 as any,
enterprise: 'ent123',
passcodeEncryptionCode: 'code123',
type: 'custodial',
})
.should.be.rejectedWith(
"error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.passphrase, expected string."
Expand All @@ -132,6 +138,7 @@ describe('Lightning wallets', function () {
passphrase: 'pass123',
enterprise: 123 as any,
passcodeEncryptionCode: 'code123',
type: 'custodial',
})
.should.be.rejectedWith(
"error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.enterprise, expected string."
Expand All @@ -143,77 +150,94 @@ describe('Lightning wallets', function () {
passphrase: 'pass123',
enterprise: 'ent123',
passcodeEncryptionCode: 123 as any,
type: 'custodial',
})
.should.be.rejectedWith(
"error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.passcodeEncryptionCode, expected string."
);
});

it('should generate wallet', async function () {
const params: GenerateLightningWalletOptions = {
label: 'my ln wallet',
passphrase: 'pass123',
enterprise: 'ent123',
passcodeEncryptionCode: 'code123',
};

const validateKeyRequest = (body) => {
const baseChecks =
body.pub.startsWith('xpub') &&
!!body.encryptedPrv &&
body.keyType === 'independent' &&
body.source === 'user';

if (body.originalPasscodeEncryptionCode !== undefined) {
return baseChecks && body.originalPasscodeEncryptionCode === 'code123' && body.coinSpecific === undefined;
} else {
const coinSpecific = body.coinSpecific && body.coinSpecific.tlnbtc;
return baseChecks && !!coinSpecific && ['userAuth', 'nodeAuth'].includes(coinSpecific.purpose);
}
};

const validateWalletRequest = (body) => {
return (
body.label === 'my ln wallet' &&
body.m === 1 &&
body.n === 1 &&
body.type === 'hot' &&
body.enterprise === 'ent123' &&
Array.isArray(body.keys) &&
body.keys.length === 1 &&
body.keys[0] === 'keyId1' &&
body.coinSpecific &&
body.coinSpecific.tlnbtc &&
Array.isArray(body.coinSpecific.tlnbtc.keys) &&
body.coinSpecific.tlnbtc.keys.length === 2 &&
body.coinSpecific.tlnbtc.keys.includes('keyId2') &&
body.coinSpecific.tlnbtc.keys.includes('keyId3')
await wallets
.generateWallet({
label: 'my ln wallet',
passphrase: 'pass123',
enterprise: 'ent123',
passcodeEncryptionCode: 'code123',
type: 'cold',
})
.should.be.rejectedWith(
'error(s) parsing generate lightning wallet request params: Invalid value \'"cold"\' supplied to GenerateLightningWalletOptions.type.0, expected "custodial".\n' +
'Invalid value \'"cold"\' supplied to GenerateLightningWalletOptions.type.1, expected "hot".'
);
};

nock(bgUrl)
.post('/api/v2/' + coinName + '/key', (body) => validateKeyRequest(body))
.reply(200, { id: 'keyId1' });
nock(bgUrl)
.post('/api/v2/' + coinName + '/key', (body) => validateKeyRequest(body))
.reply(200, { id: 'keyId2' });
nock(bgUrl)
.post('/api/v2/' + coinName + '/key', (body) => validateKeyRequest(body))
.reply(200, { id: 'keyId3' });

nock(bgUrl)
.post('/api/v2/' + coinName + '/wallet/add', (body) => validateWalletRequest(body))
.reply(200, { id: 'walletId' });

const response = await wallets.generateWallet(params);

assert.ok(response.wallet);
assert.ok(response.encryptedWalletPassphrase);
assert.equal(
bitgo.decrypt({ input: response.encryptedWalletPassphrase, password: params.passcodeEncryptionCode }),
params.passphrase
);
});

for (const type of ['hot', 'custodial'] as const) {
it(`should generate ${type} lightning wallet`, async function () {
const params: GenerateLightningWalletOptions = {
label: 'my ln wallet',
passphrase: 'pass123',
enterprise: 'ent123',
passcodeEncryptionCode: 'code123',
type,
};

const validateKeyRequest = (body) => {
const baseChecks =
body.pub.startsWith('xpub') &&
!!body.encryptedPrv &&
body.keyType === 'independent' &&
body.source === 'user';

if (body.originalPasscodeEncryptionCode !== undefined) {
return baseChecks && body.originalPasscodeEncryptionCode === 'code123' && body.coinSpecific === undefined;
} else {
const coinSpecific = body.coinSpecific && body.coinSpecific.tlnbtc;
return baseChecks && !!coinSpecific && ['userAuth', 'nodeAuth'].includes(coinSpecific.purpose);
}
};

const validateWalletRequest = (body) => {
return (
body.label === 'my ln wallet' &&
body.m === 1 &&
body.n === 1 &&
body.type === type &&
body.enterprise === 'ent123' &&
Array.isArray(body.keys) &&
body.keys.length === 1 &&
body.keys[0] === 'keyId1' &&
body.coinSpecific &&
body.coinSpecific.tlnbtc &&
Array.isArray(body.coinSpecific.tlnbtc.keys) &&
body.coinSpecific.tlnbtc.keys.length === 2 &&
body.coinSpecific.tlnbtc.keys.includes('keyId2') &&
body.coinSpecific.tlnbtc.keys.includes('keyId3')
);
};

nock(bgUrl)
.post('/api/v2/' + coinName + '/key', (body) => validateKeyRequest(body))
.reply(200, { id: 'keyId1' });
nock(bgUrl)
.post('/api/v2/' + coinName + '/key', (body) => validateKeyRequest(body))
.reply(200, { id: 'keyId2' });
nock(bgUrl)
.post('/api/v2/' + coinName + '/key', (body) => validateKeyRequest(body))
.reply(200, { id: 'keyId3' });

nock(bgUrl)
.post('/api/v2/' + coinName + '/wallet/add', (body) => validateWalletRequest(body))
.reply(200, { id: 'walletId' });

const response = await wallets.generateWallet(params);

assert.ok(response.wallet);
assert.ok(response.encryptedWalletPassphrase);
assert.equal(
bitgo.decrypt({ input: response.encryptedWalletPassphrase, password: params.passcodeEncryptionCode }),
params.passphrase
);
});
}
});

describe('invoices', function () {
Expand Down
3 changes: 3 additions & 0 deletions modules/sdk-core/src/bitgo/wallet/iWallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ export const GenerateLightningWalletOptionsCodec = t.strict(
passphrase: t.string,
enterprise: t.string,
passcodeEncryptionCode: t.string,
// custodial - bitgo controls the node private key
// hot - client controls the node private key by running remote signer node
type: t.union([t.literal('custodial'), t.literal('hot')]),
},
'GenerateLightningWalletOptions'
);
Expand Down
18 changes: 8 additions & 10 deletions modules/sdk-core/src/bitgo/wallet/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { CoinFeature } from '@bitgo/statics';

import { sanitizeLegacyPath } from '../../api';
import * as common from '../../common';
import { IBaseCoin, KeychainsTriplet, KeyPair, SupplementGenerateWalletOptions } from '../baseCoin';
import { IBaseCoin, KeychainsTriplet, SupplementGenerateWalletOptions } from '../baseCoin';
import { BitGoBase } from '../bitgoBase';
import { getSharedSecret } from '../ecdh';
import { AddKeychainOptions, Keychain, KeyIndices } from '../keychain';
Expand Down Expand Up @@ -160,18 +160,16 @@ export class Wallets implements IWallets {
const reqId = new RequestTracer();
this.bitgo.setRequestTracer(reqId);

const { label, passphrase, enterprise, passcodeEncryptionCode } = params;
const { label, passphrase, enterprise, passcodeEncryptionCode, type } = params;

// TODO BTC-1899: only userAuth key is required for custodial lightning wallet. all 3 keys are required for self custodial lightning.
// to avoid changing the platform for custodial flow, let us all 3 keys both wallet types.
const keychainPromises = ([undefined, 'userAuth', 'nodeAuth'] as const).map((purpose) => {
return async (): Promise<Keychain> => {
let keychain: KeyPair | null = this.baseCoin.keychains().create();
const pub = keychain.pub;
const encryptedPrv = this.bitgo.encrypt({ password: passphrase, input: keychain.prv });
delete (keychain as any).prv;
keychain = null;
const keychain = this.baseCoin.keychains().create();
const keychainParams: AddKeychainOptions = {
pub,
encryptedPrv,
pub: keychain.pub,
encryptedPrv: this.bitgo.encrypt({ password: passphrase, input: keychain.prv }),
originalPasscodeEncryptionCode: purpose === undefined ? passcodeEncryptionCode : undefined,
coinSpecific: purpose === undefined ? undefined : { [this.baseCoin.getChain()]: { purpose } },
keyType: 'independent',
Expand All @@ -191,7 +189,7 @@ export class Wallets implements IWallets {
label,
m: 1,
n: 1,
type: 'hot',
type,
enterprise,
keys: [userKeychain.id],
coinSpecific: { [this.baseCoin.getChain()]: { keys: [userAuthKeychain.id, nodeAuthKeychain.id] } },
Expand Down

0 comments on commit 8ff9738

Please sign in to comment.