From 0987e493ca2c027d0938b9bb185d50929f42f3cf Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Mon, 23 Mar 2026 14:40:21 -0400 Subject: [PATCH] fix(abstract-lightning): gracefully handle no m prefix BTC-3203 --- .../src/lightning/parseWithdrawPsbt.ts | 6 ++- .../test/unit/lightning/parseWithdrawPsbt.ts | 47 ++++++++++++++++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/modules/abstract-lightning/src/lightning/parseWithdrawPsbt.ts b/modules/abstract-lightning/src/lightning/parseWithdrawPsbt.ts index 8ead88b468..e3a722302f 100644 --- a/modules/abstract-lightning/src/lightning/parseWithdrawPsbt.ts +++ b/modules/abstract-lightning/src/lightning/parseWithdrawPsbt.ts @@ -17,7 +17,11 @@ function parseDerivationPath(derivationPath: string): { addressIndex: number; } { const pathSegments = derivationPath.split('/'); - const purpose = Number(pathSegments[1].replace(/'/g, '')); + // Paths may or may not include the 'm/' prefix depending on how they were created: + // bip174 binary deserialization omits it (e.g. "84'/0'/0'/1/6"), + // while manually constructed paths typically include it (e.g. "m/84'/0'/0'/1/6"). + const offset = pathSegments[0] === 'm' ? 1 : 0; + const purpose = Number(pathSegments[offset].replace(/'/g, '')); const change = Number(pathSegments[pathSegments.length - 2]); const addressIndex = Number(pathSegments[pathSegments.length - 1]); if (purpose !== PURPOSE_WRAPPED_P2WKH && purpose !== PURPOSE_P2WKH && purpose !== PURPOSE_P2TR) { diff --git a/modules/abstract-lightning/test/unit/lightning/parseWithdrawPsbt.ts b/modules/abstract-lightning/test/unit/lightning/parseWithdrawPsbt.ts index 6a4289b810..35b4d7f2d9 100644 --- a/modules/abstract-lightning/test/unit/lightning/parseWithdrawPsbt.ts +++ b/modules/abstract-lightning/test/unit/lightning/parseWithdrawPsbt.ts @@ -1,6 +1,8 @@ -import { validatePsbtForWithdraw } from '../../../src'; -import * as utxolib from '@bitgo/utxo-lib'; import assert from 'assert'; + +import * as utxolib from '@bitgo/utxo-lib'; + +import { validatePsbtForWithdraw } from '../../../src'; import { createTestPsbt } from './createPsbt'; describe('parseWithdrawPsbt', () => { @@ -42,6 +44,47 @@ describe('parseWithdrawPsbt', () => { }); }); + describe('regression tests with created PSBT from LND (no `m/` prefix)', () => { + const unsignedPsbtHex = + '70736274ff01007d0200000001814f07febb372cf9e303c40938d7f1372cf7f5bd3d43de763be10c1b2ac539200000000000ffffffff028813000000000000220020ac06117832f08e952b16c220dde35dd505b5c54fa45a4eb9f2b040efd27ecb12f86a040000000000160014f1f4753cf0f9171dd3724f17ea511baa49e3c22600000000000100fde70401000000000104e55600b7d0003c6b2be7dc105f6c3499b8d426e92fd56c3dca06728e060c80720000000000fdffffff366f5356038e883dd7dd621d0b340c526018329664ccbeb1a723b0a6f8f312a40000000023220020b4b69dea98d8d5fb32a22a0c9ad208fc28efb44943f8e576d5978d215d784d49fdffffff15978f903d63dac56026cec90fc568fbe6e8a5a8b53bf8b5a34eee3723e63eff0000000000fdffffff58a9e6be1da0f25e39af4c95ed102336a64baf9bc8eb7c1ffca97ff0d0d651ff0000000000fdffffff0145860400000000001600146b618e8d1d709711104f92a857032a8d66f1bf7d0400483045022100d85bc99bf6d610978630518f79eeee025c8ce40eb65f12360f0f47d5b30ca547022036d79384a55bbba00520d626c3ae05a5f341fd7d65f211fa64bfedad5e827c7301483045022100dacce37fbffeb2e9ee198ab3a78cfdf01e34655313482ff75376bb5e2152a05d02205a7d1b7072480368d274de0229bbb9e6f4e4cda202f6750b9a90f8517604413e0169522102dd6b15d5c483080aaeaf3be51c8ee6cd794dbfa65e5c8036fe2c67f0080929702103370c44e05dddfc29fdb418ae583652b4fb7ee62d07562fc1e5b1696caaf3908f2103c6d980811cb03048a7b48f639bbb0575444af1f670af8462bb1c93112af2080a53ae040048304502210094646e8c1c25368ed9cdf5ba10b54a88ce20bb9b452877d0d2f27823897ab9d40220183c80560c1860d22d7ae8aaeee712d930e427252345bddece1a333d345c6d9801483045022100d123c1f5a79d32905f9d725b590160afc8e14bb01cb1a5d258304c6b6673113c0220722003050f57366b3f3486c1fdd5559579f1c67c3d644d31ac6f20bb8022f7d201695221034a2c8e0891e30698aa7015f1210451f1e5336be96bf846cd43528c15b0645b032103e6305f797838299f4188595b1b01bd9fe60db0e4bb9331d957750248d055f299210383c028341d5d273afca5f5265e2c2ee9619c0f1b328956ef3000ab09eccaed3b53ae0400473044022044a43ae38b86c375aa57b3d96386fca52f74a5bc4fb37c77738efa4c0566662202200d1af93d2022c48f3f3cc134d3d75bbcfce30615e53d9246c200c5349a7d978c0147304402207ce3043211d9344dad8f4e2bf848bf1ca151310a188e707663b28475e12099d0022060d2b3533e7c362d39b041102edf17e0486b1aee55a01c7895485b8630daa39c0169522103c71a350e2b06ec9d0710a13f608bb76f35a35fed491fbb5b97280caed4c9590f2103692ea685162a1da7e86e0beeb1ed0331e8d0fbbf2e05f002071b15044c402b782102960087d0d5b8e1a449ed562553ef3149885a930f60c40324165221a7fc7627ea53ae0400483045022100a8382cfa2582abdc77417f9aa53e5ce0e2d31eb5e14b7836961da8c045de3a500220398555b93816ef7ec417bb7555bc51e0477b3c65f83d0951cff2a5040457314e0147304402204f66fd4507bd54cc25028b5660c00fdcfe15ce6121eabff7911e7307d24c7f1102200591e969897109a2372a6b22e41d158d4e0ae53ca0963166ffd8a23e06522ac9016952210282aaf28d528e6b6326880b1539a14a96dcee5d9a0920a31f3581e5ac21a9de462103f84e26e78f2c587821b581761b134f7721e33b1ad97bd1599125ee23369ea1202103da6eddca9c0eaaa08b179c035ed0adb516a592ba1321ffc5cb11dcae8c3a704f53ae0000000001011f45860400000000001600146b618e8d1d709711104f92a857032a8d66f1bf7d010304010000002206038361bfad9b6896c938690df1e67c669ecd7a021b117382bbfa81ff6de8ec153718000000005400008000000080000000800000000000000000000022020376cb4ac899d130e2d655fca6d362fe7c745cc15486849e401f7c21b3d3df37c81800000000540000800000008000000080010000000800000000'; + const recipients = [ + { + amountSat: 5000n, + address: 'tb1q4srpz7pj7z8f22ckcgsdmc6a65zmt32053dyaw0jkpqwl5n7evfqhc07u4', + }, + ]; + const accounts = [ + { + purpose: 49, + coin_type: 0, + account: 0, + xpub: 'upub5EW2cim2FSeBDqote8jcKnBSohtZjPRbw9HsXNototipVVj31AzWjgXUSonbvi7RNALY73rkgXTQY4YUpovrG1WV4ATKXyQpMa1sMnJdCbm', + }, + { + purpose: 84, + coin_type: 0, + account: 0, + xpub: 'vpub5Z8kVs9L6fSK4qc1G36VqKdTiSdfCGSzrcz4EYGkywkWX43EJiLcSBEd1XE4fxgXir7Wkwaia4eTwbakngVDHHnToc9qQMBcxerr2U5MHnN', + }, + { + purpose: 86, + coin_type: 0, + account: 0, + xpub: 'tpubDCyXDnLqhmTxjcGV2KexrojfJPBept2gWs2mg9V46y6HAp9nLzg3idmESEJ1RDtfaLyVL6x2MoJsv66hQPWrCxoR2jYQbGbHd5FmEa6BfW3', + }, + ]; + + it('should parse a valid withdraw PSBT', () => { + validatePsbtForWithdraw(unsignedPsbtHex, network, recipients, accounts); + }); + + it('should throw for invalid PSBT hex', () => { + assert.throws(() => { + validatePsbtForWithdraw('asdasd', network, recipients, accounts); + }, /ERR_BUFFER_OUT_OF_BOUNDS/); + }); + }); + describe('test cases with creating psbt on the go', () => { it('should validate PSBT with P2WPKH (purpose 84) change address', () => { const { psbt, masterKey } = createTestPsbt({