From 7c289162388d133d05fdbf622146877c8763289c Mon Sep 17 00:00:00 2001 From: Parv Tiwari Date: Wed, 20 Aug 2025 13:56:10 +0530 Subject: [PATCH] feat: adding signature in withdraw lightning calling new API to save signature in txrequest TICKET: BTC-2343 BREAKING CHANGE: withdraw request changed --- .../src/codecs/api/withdraw.ts | 20 ++--- .../src/wallet/lightning.ts | 51 ++++++++++-- .../v2/unit/lightning/lightningWallets.ts | 77 +++++++++++++++++++ modules/express/encryptedPrivKeys.json | 2 +- .../lightning/lightningWithdrawRoutes.test.ts | 26 +++++++ 5 files changed, 159 insertions(+), 17 deletions(-) diff --git a/modules/abstract-lightning/src/codecs/api/withdraw.ts b/modules/abstract-lightning/src/codecs/api/withdraw.ts index c0494dfba3..74800c1b37 100644 --- a/modules/abstract-lightning/src/codecs/api/withdraw.ts +++ b/modules/abstract-lightning/src/codecs/api/withdraw.ts @@ -1,20 +1,22 @@ import * as t from 'io-ts'; -import { LightningOnchainRecipient } from '@bitgo/public-types'; +import { LightningOnchainRequest, optionalString } from '@bitgo/public-types'; import { PendingApprovalData, TxRequestState } from '@bitgo/sdk-core'; -import { BigIntFromString } from 'io-ts-types'; export const WithdrawStatusDelivered = 'delivered'; export const WithdrawStatusFailed = 'failed'; export const WithdrawStatus = t.union([t.literal(WithdrawStatusDelivered), t.literal(WithdrawStatusFailed)]); -export const LightningOnchainWithdrawParams = t.type({ - recipients: t.array(LightningOnchainRecipient), - satsPerVbyte: BigIntFromString, - // todo:(current) add passphrase - // passphrase: t.string, -}); - +export const LightningOnchainWithdrawParams = t.intersection([ + LightningOnchainRequest, + t.type({ + passphrase: t.string, + }), + t.partial({ + sequenceId: optionalString, + comment: optionalString, + }), +]); export type LightningOnchainWithdrawParams = t.TypeOf; export const LndCreateWithdrawResponse = t.intersection( diff --git a/modules/abstract-lightning/src/wallet/lightning.ts b/modules/abstract-lightning/src/wallet/lightning.ts index 3801d4a9a2..06044de58f 100644 --- a/modules/abstract-lightning/src/wallet/lightning.ts +++ b/modules/abstract-lightning/src/wallet/lightning.ts @@ -167,6 +167,9 @@ export interface ILightningWallet { * @param {LightningOnchainWithdrawParams} params - Withdraw parameters * @param {LightningOnchainRecipient[]} params.recipients - The recipients to pay * @param {bigint} params.satsPerVbyte - Value for sats per virtual byte + * @param {string} params.passphrase - The wallet passphrase + * @param {string} [params.sequenceId] - Optional sequence ID for the respective withdraw transfer + * @param {string} [params.comment] - Optional comment for the respective withdraw transfer * @returns {Promise} Withdraw result containing transaction request details and status */ withdrawOnchain(params: LightningOnchainWithdrawParams): Promise; @@ -338,6 +341,8 @@ export class LightningWallet implements ILightningWallet { const paymentIntent: { intent: LightningPaymentIntent } = { intent: { + comment: params.comment, + sequenceId: params.sequenceId, onchainRequest: { recipients: params.recipients, satsPerVbyte: params.satsPerVbyte, @@ -351,20 +356,52 @@ export class LightningWallet implements ILightningWallet { .send(t.type({ intent: LightningPaymentIntent }).encode(paymentIntent)) .result()) as TxRequest; - if (transactionRequestCreate.state === 'pendingApproval') { + if ( + !transactionRequestCreate.transactions || + transactionRequestCreate.transactions.length === 0 || + !transactionRequestCreate.transactions[0].unsignedTx.serializedTxHex + ) { + throw new Error(`serialized txHex is missing`); + } + + const { userAuthKey } = await getLightningAuthKeychains(this.wallet); + const userAuthKeyEncryptedPrv = userAuthKey.encryptedPrv; + if (!userAuthKeyEncryptedPrv) { + throw new Error(`user auth key is missing encrypted private key`); + } + const signature = createMessageSignature( + transactionRequestCreate.transactions[0].unsignedTx.serializedTxHex, + this.wallet.bitgo.decrypt({ password: params.passphrase, input: userAuthKeyEncryptedPrv }) + ); + + const transactionRequestWithSignature = (await this.wallet.bitgo + .put( + this.wallet.bitgo.url( + '/wallet/' + this.wallet.id() + '/txrequests/' + transactionRequestCreate.txRequestId + '/coinSpecific', + 2 + ) + ) + .send({ + unsignedCoinSpecific: { + signature, + }, + }) + .result()) as TxRequest; + + if (transactionRequestWithSignature.state === 'pendingApproval') { const pendingApprovals = new PendingApprovals(this.wallet.bitgo, this.wallet.baseCoin); - const pendingApproval = await pendingApprovals.get({ id: transactionRequestCreate.pendingApprovalId }); + const pendingApproval = await pendingApprovals.get({ id: transactionRequestWithSignature.pendingApprovalId }); return { pendingApproval: pendingApproval.toJSON(), - txRequestId: transactionRequestCreate.txRequestId, - txRequestState: transactionRequestCreate.state, + txRequestId: transactionRequestWithSignature.txRequestId, + txRequestState: transactionRequestWithSignature.state, }; } const transfer: { id: string } = await this.wallet.bitgo .post( this.wallet.bitgo.url( - '/wallet/' + this.wallet.id() + '/txrequests/' + transactionRequestCreate.txRequestId + '/transfers', + '/wallet/' + this.wallet.id() + '/txrequests/' + transactionRequestWithSignature.txRequestId + '/transfers', 2 ) ) @@ -374,7 +411,7 @@ export class LightningWallet implements ILightningWallet { const transactionRequestSend = await commonTssMethods.sendTxRequest( this.wallet.bitgo, this.wallet.id(), - transactionRequestCreate.txRequestId, + transactionRequestWithSignature.txRequestId, RequestType.tx, reqId ); @@ -390,7 +427,7 @@ export class LightningWallet implements ILightningWallet { } return { - txRequestId: transactionRequestCreate.txRequestId, + txRequestId: transactionRequestWithSignature.txRequestId, txRequestState: transactionRequestSend.state, transfer: updatedTransfer, withdrawStatus: diff --git a/modules/bitgo/test/v2/unit/lightning/lightningWallets.ts b/modules/bitgo/test/v2/unit/lightning/lightningWallets.ts index 784823e29b..a27d06089d 100644 --- a/modules/bitgo/test/v2/unit/lightning/lightningWallets.ts +++ b/modules/bitgo/test/v2/unit/lightning/lightningWallets.ts @@ -825,11 +825,34 @@ describe('Lightning wallets', function () { }, ], satsPerVbyte: 15n, + passphrase: 'password123', }; const txRequestResponse = { txRequestId: 'txReq123', state: 'pendingDelivery', + transactions: [ + { + unsignedTx: { + serializedTxHex: 'unsignedTx123', + }, + }, + ], + }; + + const txRequestWithSignatureResponse = { + txRequestId: 'txReq123', + state: 'pendingDelivery', + transactions: [ + { + unsignedTx: { + serializedTxHex: 'unsignedTx123', + coinSpecific: { + signature: 'someSignature', + }, + }, + }, + ], }; const finalWithdrawResponse = { @@ -838,7 +861,9 @@ describe('Lightning wallets', function () { transactions: [ { unsignedTx: { + serializedTxHex: 'unsignedTx123', coinSpecific: { + signature: 'someSignature', status: 'delivered', txid: 'tx123', }, @@ -939,6 +964,10 @@ describe('Lightning wallets', function () { .post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests`) .reply(200, txRequestResponse); + const storeSignatureNock = nock(bgUrl) + .put(`/api/v2/wallet/${wallet.wallet.id()}/txrequests/${txRequestResponse.txRequestId}/coinSpecific`) + .reply(200, txRequestWithSignatureResponse); + const createTransferNock = nock(bgUrl) .post(`/api/v2/wallet/${wallet.wallet.id()}/txrequests/${txRequestResponse.txRequestId}/transfers`) .reply(200, transferResponse); @@ -951,14 +980,25 @@ describe('Lightning wallets', function () { .get(`/api/v2/${coinName}/wallet/${wallet.wallet.id()}/transfer/${transferResponse.id}`) .reply(200, updatedTransferResponse); + const userAuthKeyNock = nock(bgUrl) + .get('/api/v2/' + coinName + '/key/def') + .reply(200, userAuthKey); + const nodeAuthKeyNock = nock(bgUrl) + .get('/api/v2/' + coinName + '/key/ghi') + .reply(200, nodeAuthKey); + const response = await wallet.withdrawOnchain(params); assert.strictEqual(response.txRequestId, 'txReq123'); assert.strictEqual(response.txRequestState, 'delivered'); assert.strictEqual(response.withdrawStatus?.status, 'delivered'); assert.strictEqual(response.withdrawStatus?.txid, 'tx123'); + assert.strictEqual((response.withdrawStatus as any).signature, undefined); assert.deepStrictEqual(response.transfer, updatedTransferResponse); + userAuthKeyNock.done(); + nodeAuthKeyNock.done(); createTxRequestNock.done(); + storeSignatureNock.done(); createTransferNock.done(); sendTxRequestNock.done(); getTransferNock.done(); @@ -973,12 +1013,35 @@ describe('Lightning wallets', function () { }, ], satsPerVbyte: 15n, + passphrase: 'password123', }; const txRequestResponse = { txRequestId: 'txReq123', state: 'pendingApproval', pendingApprovalId: 'approval123', + transactions: [ + { + unsignedTx: { + serializedTxHex: 'unsignedTx123', + }, + }, + ], + }; + const txRequestWithSignatureResponse = { + txRequestId: 'txReq123', + state: 'pendingApproval', + pendingApprovalId: 'approval123', + transactions: [ + { + unsignedTx: { + serializedTxHex: 'unsignedTx123', + coinSpecific: { + signature: 'someSignature', + }, + }, + }, + ], }; const pendingApprovalData: PendingApprovalData = { @@ -998,11 +1061,25 @@ describe('Lightning wallets', function () { .get(`/api/v2/${coinName}/pendingapprovals/${txRequestResponse.pendingApprovalId}`) .reply(200, pendingApprovalData); + const userAuthKeyNock = nock(bgUrl) + .get('/api/v2/' + coinName + '/key/def') + .reply(200, userAuthKey); + const nodeAuthKeyNock = nock(bgUrl) + .get('/api/v2/' + coinName + '/key/ghi') + .reply(200, nodeAuthKey); + + const storeSignatureNock = nock(bgUrl) + .put(`/api/v2/wallet/${wallet.wallet.id()}/txrequests/${txRequestResponse.txRequestId}/coinSpecific`) + .reply(200, txRequestWithSignatureResponse); + const response = await wallet.withdrawOnchain(params); assert.strictEqual(response.txRequestId, 'txReq123'); assert.strictEqual(response.txRequestState, 'pendingApproval'); assert(response.pendingApproval); + userAuthKeyNock.done(); + nodeAuthKeyNock.done(); + storeSignatureNock.done(); createTxRequestNock.done(); getPendingApprovalNock.done(); }); diff --git a/modules/express/encryptedPrivKeys.json b/modules/express/encryptedPrivKeys.json index d5adfe00d3..9895bf1ebd 100644 --- a/modules/express/encryptedPrivKeys.json +++ b/modules/express/encryptedPrivKeys.json @@ -1,3 +1,3 @@ { - "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=\"}" + "61f039aad587c2000745c687373e0fa9": "{\"iv\":\"AQOxTybJ4Ty5SsJmIp9C7A==\",\"v\":1,\"iter\":10000,\"ks\":256,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"n04GKxOSJes=\",\"ct\":\"efeKamNNHz2pReZHDfAQOmcqZ04+mTHRMv0wZSJgnih3sf9lcXGhRqD7iURlHv0NHH6S8Gido24eMiEPK9AXSowxpq/pG53bd9FdONIzD1LSS0KtegPzWqg38W2My4RUZvN9nbeE9bL9Ifnnq2Zw24vdXL+lj4w=\"}" } \ No newline at end of file diff --git a/modules/express/test/unit/lightning/lightningWithdrawRoutes.test.ts b/modules/express/test/unit/lightning/lightningWithdrawRoutes.test.ts index 29a69cfef7..b9e0b6ecd7 100644 --- a/modules/express/test/unit/lightning/lightningWithdrawRoutes.test.ts +++ b/modules/express/test/unit/lightning/lightningWithdrawRoutes.test.ts @@ -32,6 +32,7 @@ describe('Lightning Withdraw Routes', () => { }, ], satsPerVbyte: '15', + passphrase: 'password123', }; const expectedResponse: LightningOnchainWithdrawResponse = { @@ -84,6 +85,7 @@ describe('Lightning Withdraw Routes', () => { // we decode the amountMsat string to bigint, it should be in bigint format when passed to payInvoice should(firstArg).have.property('recipients', decodedRecipients); should(firstArg).have.property('satsPerVbyte', BigInt(inputParams.satsPerVbyte)); + should(firstArg).have.property('passphrase', inputParams.passphrase); }); it('should throw an error if the satsPerVbyte is missing in the request params', async () => { @@ -94,6 +96,7 @@ describe('Lightning Withdraw Routes', () => { address: 'bcrt1qjq48cqk2u80hewdcndf539m8nnnvt845nl68x7', }, ], + passphrase: 'password123', }; const req = mockRequestObject({ @@ -110,6 +113,29 @@ describe('Lightning Withdraw Routes', () => { it('should throw an error if the recipients is missing in the request params', async () => { const inputParams = { satsPerVbyte: '15', + passphrase: 'password123', + }; + + const req = mockRequestObject({ + params: { id: 'testWalletId', coin }, + body: inputParams, + }); + req.bitgo = bitgo; + + await should(handleLightningWithdraw(req)).be.rejectedWith( + 'Invalid request body for withdrawing on chain lightning balance' + ); + }); + + it('should throw an error if passphrase is missing in the request params', async () => { + const inputParams = { + satsPerVbyte: '15', + recipients: [ + { + amountSat: '500000', + address: 'bcrt1qjq48cqk2u80hewdcndf539m8nnnvt845nl68x7', + }, + ], }; const req = mockRequestObject({