Skip to content

Commit d19802d

Browse files
feat(sdk-core): add custodial lightning wallet creation
use wallet type to add custodial lightning Ticket: BTC-0
1 parent ba93a73 commit d19802d

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
@@ -69,6 +69,7 @@ describe('Lightning wallets', function () {
6969
passphrase: 'pass123',
7070
enterprise: 'ent123',
7171
passcodeEncryptionCode: 'code123',
72+
type: 'custodial',
7273
})
7374
.should.be.rejectedWith(
7475
"error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.label, expected string."
@@ -79,6 +80,7 @@ describe('Lightning wallets', function () {
7980
label: 'my ln wallet',
8081
enterprise: 'ent123',
8182
passcodeEncryptionCode: 'code123',
83+
type: 'custodial',
8284
})
8385
.should.be.rejectedWith(
8486
"error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.passphrase, expected string."
@@ -89,6 +91,7 @@ describe('Lightning wallets', function () {
8991
label: 'my ln wallet',
9092
passphrase: 'pass123',
9193
passcodeEncryptionCode: 'code123',
94+
type: 'custodial',
9295
})
9396
.should.be.rejectedWith(
9497
"error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.enterprise, expected string."
@@ -99,6 +102,7 @@ describe('Lightning wallets', function () {
99102
label: 'my ln wallet',
100103
passphrase: 'pass123',
101104
enterprise: 'ent123',
105+
type: 'custodial',
102106
})
103107
.should.be.rejectedWith(
104108
"error(s) parsing generate lightning wallet request params: Invalid value 'undefined' supplied to GenerateLightningWalletOptions.passcodeEncryptionCode, expected string."
@@ -110,6 +114,7 @@ describe('Lightning wallets', function () {
110114
passphrase: 'pass123',
111115
enterprise: 'ent123',
112116
passcodeEncryptionCode: 'code123',
117+
type: 'custodial',
113118
})
114119
.should.be.rejectedWith(
115120
"error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.label, expected string."
@@ -121,6 +126,7 @@ describe('Lightning wallets', function () {
121126
passphrase: 123 as any,
122127
enterprise: 'ent123',
123128
passcodeEncryptionCode: 'code123',
129+
type: 'custodial',
124130
})
125131
.should.be.rejectedWith(
126132
"error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.passphrase, expected string."
@@ -132,6 +138,7 @@ describe('Lightning wallets', function () {
132138
passphrase: 'pass123',
133139
enterprise: 123 as any,
134140
passcodeEncryptionCode: 'code123',
141+
type: 'custodial',
135142
})
136143
.should.be.rejectedWith(
137144
"error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.enterprise, expected string."
@@ -143,77 +150,94 @@ describe('Lightning wallets', function () {
143150
passphrase: 'pass123',
144151
enterprise: 'ent123',
145152
passcodeEncryptionCode: 123 as any,
153+
type: 'custodial',
146154
})
147155
.should.be.rejectedWith(
148156
"error(s) parsing generate lightning wallet request params: Invalid value '123' supplied to GenerateLightningWalletOptions.passcodeEncryptionCode, expected string."
149157
);
150-
});
151-
152-
it('should generate wallet', async function () {
153-
const params: GenerateLightningWalletOptions = {
154-
label: 'my ln wallet',
155-
passphrase: 'pass123',
156-
enterprise: 'ent123',
157-
passcodeEncryptionCode: 'code123',
158-
};
159-
160-
const validateKeyRequest = (body) => {
161-
const baseChecks =
162-
body.pub.startsWith('xpub') &&
163-
!!body.encryptedPrv &&
164-
body.keyType === 'independent' &&
165-
body.source === 'user';
166-
167-
if (body.originalPasscodeEncryptionCode !== undefined) {
168-
return baseChecks && body.originalPasscodeEncryptionCode === 'code123' && body.coinSpecific === undefined;
169-
} else {
170-
const coinSpecific = body.coinSpecific && body.coinSpecific.tlnbtc;
171-
return baseChecks && !!coinSpecific && ['userAuth', 'nodeAuth'].includes(coinSpecific.purpose);
172-
}
173-
};
174158

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

219243
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)