Skip to content

Commit fa41527

Browse files
authored
Merge pull request #6757 from BitGo/BTC-2343-withdraw-save-signature
feat: adding signature in withdraw lightning
2 parents 9d116c3 + 514aaee commit fa41527

File tree

5 files changed

+162
-16
lines changed

5 files changed

+162
-16
lines changed

modules/abstract-lightning/src/codecs/api/withdraw.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as t from 'io-ts';
2-
import { LightningOnchainRecipient } from '@bitgo/public-types';
2+
import { LightningOnchainRecipient, optionalString } from '@bitgo/public-types';
33
import { PendingApprovalData, TxRequestState } from '@bitgo/sdk-core';
44
import { BigIntFromString } from 'io-ts-types';
55

@@ -8,13 +8,19 @@ export const WithdrawStatusFailed = 'failed';
88

99
export const WithdrawStatus = t.union([t.literal(WithdrawStatusDelivered), t.literal(WithdrawStatusFailed)]);
1010

11-
export const LightningOnchainWithdrawParams = t.type({
12-
recipients: t.array(LightningOnchainRecipient),
13-
satsPerVbyte: BigIntFromString,
14-
// todo:(current) add passphrase
15-
// passphrase: t.string,
16-
});
17-
11+
// todo: import LightningOnchainRequest from public-types after it is published
12+
export const LightningOnchainWithdrawParams = t.intersection([
13+
// LightningOnchainRequest,
14+
t.type({
15+
recipients: t.array(LightningOnchainRecipient),
16+
satsPerVbyte: BigIntFromString,
17+
passphrase: t.string,
18+
}),
19+
t.partial({
20+
sequenceId: optionalString,
21+
comment: optionalString,
22+
}),
23+
]);
1824
export type LightningOnchainWithdrawParams = t.TypeOf<typeof LightningOnchainWithdrawParams>;
1925

2026
export const LndCreateWithdrawResponse = t.intersection(

modules/abstract-lightning/src/wallet/lightning.ts

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ export interface ILightningWallet {
167167
* @param {LightningOnchainWithdrawParams} params - Withdraw parameters
168168
* @param {LightningOnchainRecipient[]} params.recipients - The recipients to pay
169169
* @param {bigint} params.satsPerVbyte - Value for sats per virtual byte
170+
* @param {string} params.passphrase - The wallet passphrase
171+
* @param {string} [params.sequenceId] - Optional sequence ID for the respective withdraw transfer
172+
* @param {string} [params.comment] - Optional comment for the respective withdraw transfer
170173
* @returns {Promise<LightningOnchainWithdrawResponse>} Withdraw result containing transaction request details and status
171174
*/
172175
withdrawOnchain(params: LightningOnchainWithdrawParams): Promise<LightningOnchainWithdrawResponse>;
@@ -338,6 +341,8 @@ export class LightningWallet implements ILightningWallet {
338341

339342
const paymentIntent: { intent: LightningPaymentIntent } = {
340343
intent: {
344+
comment: params.comment,
345+
sequenceId: params.sequenceId,
341346
onchainRequest: {
342347
recipients: params.recipients,
343348
satsPerVbyte: params.satsPerVbyte,
@@ -351,20 +356,52 @@ export class LightningWallet implements ILightningWallet {
351356
.send(t.type({ intent: LightningPaymentIntent }).encode(paymentIntent))
352357
.result()) as TxRequest;
353358

354-
if (transactionRequestCreate.state === 'pendingApproval') {
359+
if (
360+
!transactionRequestCreate.transactions ||
361+
transactionRequestCreate.transactions.length === 0 ||
362+
!transactionRequestCreate.transactions[0].unsignedTx.serializedTxHex
363+
) {
364+
throw new Error(`serialized txHex is missing`);
365+
}
366+
367+
const { userAuthKey } = await getLightningAuthKeychains(this.wallet);
368+
const userAuthKeyEncryptedPrv = userAuthKey.encryptedPrv;
369+
if (!userAuthKeyEncryptedPrv) {
370+
throw new Error(`user auth key is missing encrypted private key`);
371+
}
372+
const signature = createMessageSignature(
373+
transactionRequestCreate.transactions[0].unsignedTx.serializedTxHex,
374+
this.wallet.bitgo.decrypt({ password: params.passphrase, input: userAuthKeyEncryptedPrv })
375+
);
376+
377+
const transactionRequestWithSignature = (await this.wallet.bitgo
378+
.put(
379+
this.wallet.bitgo.url(
380+
'/wallet/' + this.wallet.id() + '/txrequests/' + transactionRequestCreate.txRequestId + '/coinSpecific',
381+
2
382+
)
383+
)
384+
.send({
385+
unsignedCoinSpecific: {
386+
signature,
387+
},
388+
})
389+
.result()) as TxRequest;
390+
391+
if (transactionRequestWithSignature.state === 'pendingApproval') {
355392
const pendingApprovals = new PendingApprovals(this.wallet.bitgo, this.wallet.baseCoin);
356-
const pendingApproval = await pendingApprovals.get({ id: transactionRequestCreate.pendingApprovalId });
393+
const pendingApproval = await pendingApprovals.get({ id: transactionRequestWithSignature.pendingApprovalId });
357394
return {
358395
pendingApproval: pendingApproval.toJSON(),
359-
txRequestId: transactionRequestCreate.txRequestId,
360-
txRequestState: transactionRequestCreate.state,
396+
txRequestId: transactionRequestWithSignature.txRequestId,
397+
txRequestState: transactionRequestWithSignature.state,
361398
};
362399
}
363400

364401
const transfer: { id: string } = await this.wallet.bitgo
365402
.post(
366403
this.wallet.bitgo.url(
367-
'/wallet/' + this.wallet.id() + '/txrequests/' + transactionRequestCreate.txRequestId + '/transfers',
404+
'/wallet/' + this.wallet.id() + '/txrequests/' + transactionRequestWithSignature.txRequestId + '/transfers',
368405
2
369406
)
370407
)
@@ -374,7 +411,7 @@ export class LightningWallet implements ILightningWallet {
374411
const transactionRequestSend = await commonTssMethods.sendTxRequest(
375412
this.wallet.bitgo,
376413
this.wallet.id(),
377-
transactionRequestCreate.txRequestId,
414+
transactionRequestWithSignature.txRequestId,
378415
RequestType.tx,
379416
reqId
380417
);
@@ -390,7 +427,7 @@ export class LightningWallet implements ILightningWallet {
390427
}
391428

392429
return {
393-
txRequestId: transactionRequestCreate.txRequestId,
430+
txRequestId: transactionRequestWithSignature.txRequestId,
394431
txRequestState: transactionRequestSend.state,
395432
transfer: updatedTransfer,
396433
withdrawStatus:

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,11 +825,34 @@ describe('Lightning wallets', function () {
825825
},
826826
],
827827
satsPerVbyte: 15n,
828+
passphrase: 'password123',
828829
};
829830

830831
const txRequestResponse = {
831832
txRequestId: 'txReq123',
832833
state: 'pendingDelivery',
834+
transactions: [
835+
{
836+
unsignedTx: {
837+
serializedTxHex: 'unsignedTx123',
838+
},
839+
},
840+
],
841+
};
842+
843+
const txRequestWithSignatureResponse = {
844+
txRequestId: 'txReq123',
845+
state: 'pendingDelivery',
846+
transactions: [
847+
{
848+
unsignedTx: {
849+
serializedTxHex: 'unsignedTx123',
850+
coinSpecific: {
851+
signature: 'someSignature',
852+
},
853+
},
854+
},
855+
],
833856
};
834857

835858
const finalWithdrawResponse = {
@@ -838,7 +861,9 @@ describe('Lightning wallets', function () {
838861
transactions: [
839862
{
840863
unsignedTx: {
864+
serializedTxHex: 'unsignedTx123',
841865
coinSpecific: {
866+
signature: 'someSignature',
842867
status: 'delivered',
843868
txid: 'tx123',
844869
},
@@ -939,6 +964,10 @@ describe('Lightning wallets', function () {
939964
.post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests`)
940965
.reply(200, txRequestResponse);
941966

967+
const storeSignatureNock = nock(bgUrl)
968+
.put(`/api/v2/wallet/${wallet.wallet.id()}/txrequests/${txRequestResponse.txRequestId}/coinSpecific`)
969+
.reply(200, txRequestWithSignatureResponse);
970+
942971
const createTransferNock = nock(bgUrl)
943972
.post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests/${txRequestResponse.txRequestId}/transfers`)
944973
.reply(200, transferResponse);
@@ -951,14 +980,25 @@ describe('Lightning wallets', function () {
951980
.get(`/api/v2/${coinName}/wallet/${wallet.wallet.id()}/transfer/${transferResponse.id}`)
952981
.reply(200, updatedTransferResponse);
953982

983+
const userAuthKeyNock = nock(bgUrl)
984+
.get('/api/v2/' + coinName + '/key/def')
985+
.reply(200, userAuthKey);
986+
const nodeAuthKeyNock = nock(bgUrl)
987+
.get('/api/v2/' + coinName + '/key/ghi')
988+
.reply(200, nodeAuthKey);
989+
954990
const response = await wallet.withdrawOnchain(params);
955991
assert.strictEqual(response.txRequestId, 'txReq123');
956992
assert.strictEqual(response.txRequestState, 'delivered');
957993
assert.strictEqual(response.withdrawStatus?.status, 'delivered');
958994
assert.strictEqual(response.withdrawStatus?.txid, 'tx123');
995+
assert.strictEqual((response.withdrawStatus as any).signature, undefined);
959996
assert.deepStrictEqual(response.transfer, updatedTransferResponse);
960997

998+
userAuthKeyNock.done();
999+
nodeAuthKeyNock.done();
9611000
createTxRequestNock.done();
1001+
storeSignatureNock.done();
9621002
createTransferNock.done();
9631003
sendTxRequestNock.done();
9641004
getTransferNock.done();
@@ -973,12 +1013,35 @@ describe('Lightning wallets', function () {
9731013
},
9741014
],
9751015
satsPerVbyte: 15n,
1016+
passphrase: 'password123',
9761017
};
9771018

9781019
const txRequestResponse = {
9791020
txRequestId: 'txReq123',
9801021
state: 'pendingApproval',
9811022
pendingApprovalId: 'approval123',
1023+
transactions: [
1024+
{
1025+
unsignedTx: {
1026+
serializedTxHex: 'unsignedTx123',
1027+
},
1028+
},
1029+
],
1030+
};
1031+
const txRequestWithSignatureResponse = {
1032+
txRequestId: 'txReq123',
1033+
state: 'pendingApproval',
1034+
pendingApprovalId: 'approval123',
1035+
transactions: [
1036+
{
1037+
unsignedTx: {
1038+
serializedTxHex: 'unsignedTx123',
1039+
coinSpecific: {
1040+
signature: 'someSignature',
1041+
},
1042+
},
1043+
},
1044+
],
9821045
};
9831046

9841047
const pendingApprovalData: PendingApprovalData = {
@@ -998,11 +1061,25 @@ describe('Lightning wallets', function () {
9981061
.get(`/api/v2/${coinName}/pendingapprovals/${txRequestResponse.pendingApprovalId}`)
9991062
.reply(200, pendingApprovalData);
10001063

1064+
const userAuthKeyNock = nock(bgUrl)
1065+
.get('/api/v2/' + coinName + '/key/def')
1066+
.reply(200, userAuthKey);
1067+
const nodeAuthKeyNock = nock(bgUrl)
1068+
.get('/api/v2/' + coinName + '/key/ghi')
1069+
.reply(200, nodeAuthKey);
1070+
1071+
const storeSignatureNock = nock(bgUrl)
1072+
.put(`/api/v2/wallet/${wallet.wallet.id()}/txrequests/${txRequestResponse.txRequestId}/coinSpecific`)
1073+
.reply(200, txRequestWithSignatureResponse);
1074+
10011075
const response = await wallet.withdrawOnchain(params);
10021076
assert.strictEqual(response.txRequestId, 'txReq123');
10031077
assert.strictEqual(response.txRequestState, 'pendingApproval');
10041078
assert.ok(response.pendingApproval);
10051079

1080+
userAuthKeyNock.done();
1081+
nodeAuthKeyNock.done();
1082+
storeSignatureNock.done();
10061083
createTxRequestNock.done();
10071084
getPendingApprovalNock.done();
10081085
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"61f039aad587c2000745c687373e0fa9": "{\"iv\":\"O74H8BBv86GBpoTzjVyzWw==\",\"v\":1,\"iter\":10000,\"ks\":256,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"7n8pAjXCfug=\",\"ct\":\"14MjiKBksaaayrwuc/w8vJ5C3yflQ15//dhLiOgYVqjhJJ7iKrcrjtgfLoI3+MKLaKCycNKi6vTs2xs8xJeSm/XhsOE9EfapkfGHdYuf4C6O1whNOyugZ0ZSOA/buDC3rvBbvCNtLDOxN5XWJN/RADOnZdHuVGk=\"}"
2+
"61f039aad587c2000745c687373e0fa9": "{\"iv\":\"O74H8BBv86GBpoTzjVyzWw==\",\"v\":1,\"iter\":10000,\"ks\":256,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"7n8pAjXCfug=\",\"ct\":\"14MjiKBksaaayrwuc/w8vJ5C3yflQ15//dhLiOgYVqjhJJ7iKrcrjtgfLoI3+MKLaKCycNKi6vTs2xs8xJeSm/XhsOE9EfapkfGHdYuf4C6O1whNOyugZ0ZSOA/buDC3rvBbvCNtLDOxN5XWJN/RADOnZdHuVGk=\"}"
33
}

modules/express/test/unit/lightning/lightningWithdrawRoutes.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe('Lightning Withdraw Routes', () => {
3232
},
3333
],
3434
satsPerVbyte: '15',
35+
passphrase: 'password123',
3536
};
3637

3738
const expectedResponse: LightningOnchainWithdrawResponse = {
@@ -84,6 +85,7 @@ describe('Lightning Withdraw Routes', () => {
8485
// we decode the amountMsat string to bigint, it should be in bigint format when passed to payInvoice
8586
should(firstArg).have.property('recipients', decodedRecipients);
8687
should(firstArg).have.property('satsPerVbyte', BigInt(inputParams.satsPerVbyte));
88+
should(firstArg).have.property('passphrase', inputParams.passphrase);
8789
});
8890

8991
it('should throw an error if the satsPerVbyte is missing in the request params', async () => {
@@ -94,6 +96,7 @@ describe('Lightning Withdraw Routes', () => {
9496
address: 'bcrt1qjq48cqk2u80hewdcndf539m8nnnvt845nl68x7',
9597
},
9698
],
99+
passphrase: 'password123',
97100
};
98101

99102
const req = mockRequestObject({
@@ -110,6 +113,29 @@ describe('Lightning Withdraw Routes', () => {
110113
it('should throw an error if the recipients is missing in the request params', async () => {
111114
const inputParams = {
112115
satsPerVbyte: '15',
116+
passphrase: 'password123',
117+
};
118+
119+
const req = mockRequestObject({
120+
params: { id: 'testWalletId', coin },
121+
body: inputParams,
122+
});
123+
req.bitgo = bitgo;
124+
125+
await should(handleLightningWithdraw(req)).be.rejectedWith(
126+
'Invalid request body for withdrawing on chain lightning balance'
127+
);
128+
});
129+
130+
it('should throw an error if passphrase is missing in the request params', async () => {
131+
const inputParams = {
132+
satsPerVbyte: '15',
133+
recipients: [
134+
{
135+
amountSat: '500000',
136+
address: 'bcrt1qjq48cqk2u80hewdcndf539m8nnnvt845nl68x7',
137+
},
138+
],
113139
};
114140

115141
const req = mockRequestObject({

0 commit comments

Comments
 (0)