Skip to content

Commit c86cf54

Browse files
authored
Merge pull request #5725 from BitGo/BTC-0-add-custodial-ln-wallet
feat(sdk-core): add custodial lightning wallet creation
2 parents e5796c0 + d19802d commit c86cf54

File tree

3 files changed

+99
-74
lines changed

3 files changed

+99
-74
lines changed

modules/bitgo/test/v2/unit/lightning/lightningWallets.ts

Lines changed: 88 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ describe('Lightning wallets', function () {
7272
passphrase: 'pass123',
7373
enterprise: 'ent123',
7474
passcodeEncryptionCode: 'code123',
75+
type: 'custodial',
7576
})
7677
.should.be.rejectedWith(
7778
"error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.label, expected string."
@@ -82,6 +83,7 @@ describe('Lightning wallets', function () {
8283
label: 'my ln wallet',
8384
enterprise: 'ent123',
8485
passcodeEncryptionCode: 'code123',
86+
type: 'custodial',
8587
})
8688
.should.be.rejectedWith(
8789
"error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.passphrase, expected string."
@@ -92,6 +94,7 @@ describe('Lightning wallets', function () {
9294
label: 'my ln wallet',
9395
passphrase: 'pass123',
9496
passcodeEncryptionCode: 'code123',
97+
type: 'custodial',
9598
})
9699
.should.be.rejectedWith(
97100
"error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.enterprise, expected string."
@@ -102,6 +105,7 @@ describe('Lightning wallets', function () {
102105
label: 'my ln wallet',
103106
passphrase: 'pass123',
104107
enterprise: 'ent123',
108+
type: 'custodial',
105109
})
106110
.should.be.rejectedWith(
107111
"error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.passcodeEncryptionCode, expected string."
@@ -113,6 +117,7 @@ describe('Lightning wallets', function () {
113117
passphrase: 'pass123',
114118
enterprise: 'ent123',
115119
passcodeEncryptionCode: 'code123',
120+
type: 'custodial',
116121
})
117122
.should.be.rejectedWith(
118123
"error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.label, expected string."
@@ -124,6 +129,7 @@ describe('Lightning wallets', function () {
124129
passphrase: 123 as any,
125130
enterprise: 'ent123',
126131
passcodeEncryptionCode: 'code123',
132+
type: 'custodial',
127133
})
128134
.should.be.rejectedWith(
129135
"error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.passphrase, expected string."
@@ -135,6 +141,7 @@ describe('Lightning wallets', function () {
135141
passphrase: 'pass123',
136142
enterprise: 123 as any,
137143
passcodeEncryptionCode: 'code123',
144+
type: 'custodial',
138145
})
139146
.should.be.rejectedWith(
140147
"error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.enterprise, expected string."
@@ -146,77 +153,94 @@ describe('Lightning wallets', function () {
146153
passphrase: 'pass123',
147154
enterprise: 'ent123',
148155
passcodeEncryptionCode: 123 as any,
156+
type: 'custodial',
149157
})
150158
.should.be.rejectedWith(
151159
"error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.passcodeEncryptionCode, expected string."
152160
);
153-
});
154-
155-
it('should generate wallet', async function () {
156-
const params: GenerateLightningWalletOptions = {
157-
label: 'my ln wallet',
158-
passphrase: 'pass123',
159-
enterprise: 'ent123',
160-
passcodeEncryptionCode: 'code123',
161-
};
162-
163-
const validateKeyRequest = (body) => {
164-
const baseChecks =
165-
body.pub.startsWith('xpub') &&
166-
!!body.encryptedPrv &&
167-
body.keyType === 'independent' &&
168-
body.source === 'user';
169-
170-
if (body.originalPasscodeEncryptionCode !== undefined) {
171-
return baseChecks && body.originalPasscodeEncryptionCode === 'code123' && body.coinSpecific === undefined;
172-
} else {
173-
const coinSpecific = body.coinSpecific && body.coinSpecific.tlnbtc;
174-
return baseChecks && !!coinSpecific && ['userAuth', 'nodeAuth'].includes(coinSpecific.purpose);
175-
}
176-
};
177161

178-
const validateWalletRequest = (body) => {
179-
return (
180-
body.label === 'my ln wallet' &&
181-
body.m === 1 &&
182-
body.n === 1 &&
183-
body.type === 'hot' &&
184-
body.enterprise === 'ent123' &&
185-
Array.isArray(body.keys) &&
186-
body.keys.length === 1 &&
187-
body.keys[0] === 'keyId1' &&
188-
body.coinSpecific &&
189-
body.coinSpecific.tlnbtc &&
190-
Array.isArray(body.coinSpecific.tlnbtc.keys) &&
191-
body.coinSpecific.tlnbtc.keys.length === 2 &&
192-
body.coinSpecific.tlnbtc.keys.includes('keyId2') &&
193-
body.coinSpecific.tlnbtc.keys.includes('keyId3')
162+
await wallets
163+
.generateWallet({
164+
label: 'my ln wallet',
165+
passphrase: 'pass123',
166+
enterprise: 'ent123',
167+
passcodeEncryptionCode: 'code123',
168+
type: 'cold',
169+
})
170+
.should.be.rejectedWith(
171+
'error(s) parsing generate lightning wallet request params: Invalid value \'"cold"\' supplied to GenerateLightningWalletOptions.type.0, expected "custodial".\n' +
172+
'Invalid value \'"cold"\' supplied to GenerateLightningWalletOptions.type.1, expected "hot".'
194173
);
195-
};
196-
197-
nock(bgUrl)
198-
.post('/api/v2/' + coinName + '/key', (body) => validateKeyRequest(body))
199-
.reply(200, { id: 'keyId1' });
200-
nock(bgUrl)
201-
.post('/api/v2/' + coinName + '/key', (body) => validateKeyRequest(body))
202-
.reply(200, { id: 'keyId2' });
203-
nock(bgUrl)
204-
.post('/api/v2/' + coinName + '/key', (body) => validateKeyRequest(body))
205-
.reply(200, { id: 'keyId3' });
206-
207-
nock(bgUrl)
208-
.post('/api/v2/' + coinName + '/wallet/add', (body) => validateWalletRequest(body))
209-
.reply(200, { id: 'walletId' });
210-
211-
const response = await wallets.generateWallet(params);
212-
213-
assert.ok(response.wallet);
214-
assert.ok(response.encryptedWalletPassphrase);
215-
assert.equal(
216-
bitgo.decrypt({ input: response.encryptedWalletPassphrase, password: params.passcodeEncryptionCode }),
217-
params.passphrase
218-
);
219174
});
175+
176+
for (const type of ['hot', 'custodial'] as const) {
177+
it(`should generate ${type} lightning wallet`, async function () {
178+
const params: GenerateLightningWalletOptions = {
179+
label: 'my ln wallet',
180+
passphrase: 'pass123',
181+
enterprise: 'ent123',
182+
passcodeEncryptionCode: 'code123',
183+
type,
184+
};
185+
186+
const validateKeyRequest = (body) => {
187+
const baseChecks =
188+
body.pub.startsWith('xpub') &&
189+
!!body.encryptedPrv &&
190+
body.keyType === 'independent' &&
191+
body.source === 'user';
192+
193+
if (body.originalPasscodeEncryptionCode !== undefined) {
194+
return baseChecks && body.originalPasscodeEncryptionCode === 'code123' && body.coinSpecific === undefined;
195+
} else {
196+
const coinSpecific = body.coinSpecific && body.coinSpecific.tlnbtc;
197+
return baseChecks && !!coinSpecific && ['userAuth', 'nodeAuth'].includes(coinSpecific.purpose);
198+
}
199+
};
200+
201+
const validateWalletRequest = (body) => {
202+
return (
203+
body.label === 'my ln wallet' &&
204+
body.m === 1 &&
205+
body.n === 1 &&
206+
body.type === type &&
207+
body.enterprise === 'ent123' &&
208+
Array.isArray(body.keys) &&
209+
body.keys.length === 1 &&
210+
body.keys[0] === 'keyId1' &&
211+
body.coinSpecific &&
212+
body.coinSpecific.tlnbtc &&
213+
Array.isArray(body.coinSpecific.tlnbtc.keys) &&
214+
body.coinSpecific.tlnbtc.keys.length === 2 &&
215+
body.coinSpecific.tlnbtc.keys.includes('keyId2') &&
216+
body.coinSpecific.tlnbtc.keys.includes('keyId3')
217+
);
218+
};
219+
220+
nock(bgUrl)
221+
.post('/api/v2/' + coinName + '/key', (body) => validateKeyRequest(body))
222+
.reply(200, { id: 'keyId1' });
223+
nock(bgUrl)
224+
.post('/api/v2/' + coinName + '/key', (body) => validateKeyRequest(body))
225+
.reply(200, { id: 'keyId2' });
226+
nock(bgUrl)
227+
.post('/api/v2/' + coinName + '/key', (body) => validateKeyRequest(body))
228+
.reply(200, { id: 'keyId3' });
229+
230+
nock(bgUrl)
231+
.post('/api/v2/' + coinName + '/wallet/add', (body) => validateWalletRequest(body))
232+
.reply(200, { id: 'walletId' });
233+
234+
const response = await wallets.generateWallet(params);
235+
236+
assert.ok(response.wallet);
237+
assert.ok(response.encryptedWalletPassphrase);
238+
assert.equal(
239+
bitgo.decrypt({ input: response.encryptedWalletPassphrase, password: params.passcodeEncryptionCode }),
240+
params.passphrase
241+
);
242+
});
243+
}
220244
});
221245

222246
describe('invoices', function () {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ export const GenerateLightningWalletOptionsCodec = t.strict(
8080
passphrase: t.string,
8181
enterprise: t.string,
8282
passcodeEncryptionCode: t.string,
83+
// custodial - bitgo controls the node private key
84+
// hot - client controls the node private key by running remote signer node
85+
type: t.union([t.literal('custodial'), t.literal('hot')]),
8386
},
8487
'GenerateLightningWalletOptions'
8588
);

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { CoinFeature } from '@bitgo/statics';
99

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

163-
const { label, passphrase, enterprise, passcodeEncryptionCode } = params;
163+
const { label, passphrase, enterprise, passcodeEncryptionCode, type } = params;
164164

165+
// TODO BTC-1899: only userAuth key is required for custodial lightning wallet. all 3 keys are required for self custodial lightning.
166+
// to avoid changing the platform for custodial flow, let us all 3 keys both wallet types.
165167
const keychainPromises = ([undefined, 'userAuth', 'nodeAuth'] as const).map((purpose) => {
166168
return async (): Promise<Keychain> => {
167-
let keychain: KeyPair | null = this.baseCoin.keychains().create();
168-
const pub = keychain.pub;
169-
const encryptedPrv = this.bitgo.encrypt({ password: passphrase, input: keychain.prv });
170-
delete (keychain as any).prv;
171-
keychain = null;
169+
const keychain = this.baseCoin.keychains().create();
172170
const keychainParams: AddKeychainOptions = {
173-
pub,
174-
encryptedPrv,
171+
pub: keychain.pub,
172+
encryptedPrv: this.bitgo.encrypt({ password: passphrase, input: keychain.prv }),
175173
originalPasscodeEncryptionCode: purpose === undefined ? passcodeEncryptionCode : undefined,
176174
coinSpecific: purpose === undefined ? undefined : { [this.baseCoin.getChain()]: { purpose } },
177175
keyType: 'independent',
@@ -191,7 +189,7 @@ export class Wallets implements IWallets {
191189
label,
192190
m: 1,
193191
n: 1,
194-
type: 'hot',
192+
type,
195193
enterprise,
196194
keys: [userKeychain.id],
197195
coinSpecific: { [this.baseCoin.getChain()]: { keys: [userAuthKeychain.id, nodeAuthKeychain.id] } },

0 commit comments

Comments
 (0)