From 4032217ba6eefffc049fd6aa43fe1491e6f28176 Mon Sep 17 00:00:00 2001 From: Evan Kaloudis Date: Mon, 16 Dec 2024 21:06:27 -0500 Subject: [PATCH] Add scriptPubKeyToAddress to AddressUtils + add tests --- backends/CoreLightningRequestHandler.ts | 122 ++----------------- utils/AddressUtils-testnet.test.ts | 150 ++++++++++++++++++++++++ utils/AddressUtils.test.ts | 144 +++++++++++++++++++++++ utils/AddressUtils.ts | 143 +++++++++++++++++++++- 4 files changed, 443 insertions(+), 116 deletions(-) create mode 100644 utils/AddressUtils-testnet.test.ts diff --git a/backends/CoreLightningRequestHandler.ts b/backends/CoreLightningRequestHandler.ts index 7c5470fce..e9349a5f5 100644 --- a/backends/CoreLightningRequestHandler.ts +++ b/backends/CoreLightningRequestHandler.ts @@ -1,11 +1,5 @@ -import * as bitcoin from 'bitcoinjs-lib'; -import ecc from '../zeus_modules/noble_ecc'; - -import stores from '../stores/Stores'; - -bitcoin.initEccLib(ecc); - import CLNRest from './CLNRest'; +import AddressUtils from '../utils/AddressUtils'; const api = new CLNRest(); @@ -151,114 +145,12 @@ export const getChainTransactions = async () => { listTxsResult?.value?.transactions?.forEach((tx: any) => { const addresses: Array = []; tx.outputs.forEach((output: any) => { - const nodeInfo = stores?.nodeInfoStore?.nodeInfo; - const { isTestnet, isRegtest } = nodeInfo; - - let network = bitcoin.networks.bitcoin; - if (isTestnet) network = bitcoin.networks.testnet; - if (isRegtest) network = bitcoin.networks.regtest; - - const scriptPubKeyHex = output.scriptPubKey; - - const scriptBuffer = Buffer.from(scriptPubKeyHex, 'hex'); - - const decodedScript = bitcoin.script.decompile(scriptBuffer); - - // Handle P2PKH (Pay-to-PubKey-Hash) - if ( - decodedScript && - decodedScript[0] === bitcoin.opcodes.OP_DUP && - decodedScript[1] === bitcoin.opcodes.OP_HASH160 - ) { - try { - const pubKeyHash: any = decodedScript[2]; - const { address } = bitcoin.payments.p2pkh({ - hash: pubKeyHash, - network - }); - if (address) addresses.push(address); - } catch (e) { - console.log('error decoding p2pkh pkscript', e); - } - return; - } - - // Handle P2PK (Pay-to-PubKey) - if ( - decodedScript && - decodedScript[1] === bitcoin.opcodes.OP_CHECKSIG - ) { - try { - const pubkey: any = decodedScript[0]; - const { address } = bitcoin.payments.p2pk({ - pubkey, - network - }); - if (address) addresses.push(address); - } catch (e) { - console.log('error decoding p2kh pkscript', e); - } - return; - } - - // Handle P2SH (Pay-to-Script-Hash) - if ( - decodedScript && - decodedScript[0] === bitcoin.opcodes.OP_HASH160 - ) { - try { - const scriptHash: any = decodedScript[1]; - const { address } = bitcoin.payments.p2sh({ - hash: scriptHash, - network - }); - if (address) addresses.push(address); - } catch (e) { - console.log('error decoding p2sh pkscript', e); - } - return; - } - - // Handle P2WPKH (Pay-to-Witness-PubKey-Hash) - SegWit - if ( - decodedScript && - decodedScript[0] === bitcoin.opcodes.OP_0 && - decodedScript[1] === 0x14 - ) { - try { - const pubKeyHash: any = decodedScript[2]; - const { address } = bitcoin.payments.p2wpkh({ - pubkey: pubKeyHash, - network - }); - if (address) addresses.push(address); - } catch (e) { - console.log('error decoding p2wpkh pkscript', e); - } - return; - } - - // Handle P2TR (Pay-to-Taproot) - Taproot address - if (decodedScript && decodedScript[0] === 0x51) { - // OP_CHECKSIG - console.log('attempting to decode taproot pkscript'); - try { - const taprootPubKey: any = decodedScript[1]; - const { address } = bitcoin.payments.p2tr({ - pubkey: taprootPubKey, - network - }); - if (address) addresses.push(address); - } catch (e) { - console.log('error decoding taproot pkscript', e); - } - return; - } - - console.log( - 'unknown address type for script pubkey', - scriptPubKeyHex - ); + try { + const address = AddressUtils.scriptPubKeyToAddress( + output.scriptPubKey + ); + if (address) addresses.push(address); + } catch (e) {} }); tx.dest_addresses = addresses; }); diff --git a/utils/AddressUtils-testnet.test.ts b/utils/AddressUtils-testnet.test.ts new file mode 100644 index 000000000..5bb992a34 --- /dev/null +++ b/utils/AddressUtils-testnet.test.ts @@ -0,0 +1,150 @@ +jest.mock('react-native-encrypted-storage', () => ({ + setItem: jest.fn(() => Promise.resolve()), + getItem: jest.fn(() => Promise.resolve()), + removeItem: jest.fn(() => Promise.resolve()), + clear: jest.fn(() => Promise.resolve()) +})); + +jest.mock('../stores/Stores', () => ({ + SettingsStore: { + settings: { + display: { + removeDecimalSpaces: false + } + } + }, + nodeInfoStore: { + nodeInfo: { + isTestNet: true, + isRegTest: false + } + } +})); + +import AddressUtils from './AddressUtils'; + +describe('AddressUtils', () => { + describe('scriptPubKeyToAddress', () => { + test('should correctly decode a P2WSH scriptPubKey - testnet', () => { + let scriptPubKey, expectedAddress; + scriptPubKey = + '002008737603b10129fc2dcb2e5167eb556ba2c84aec6622ff4d46767d186f63150d'; + expectedAddress = + 'tb1qppehvqa3qy5lctwt9egk0664dw3vsjhvvc307n2xwe73smmrz5xsrsq8yw'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '0020a1157ce5620e1e93ad8a98a9765971c89b5920a343add041b47ec34b7302d951'; + expectedAddress = + 'tb1q5y2heetzpc0f8tv2nz5hvkt3ezd4jg9rgwkaqsd50mp5kuczm9gssy38uc'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '0020983cca35d586a96de538166552d2773ed291b96a3af65917560ee7eda5e9e106'; + expectedAddress = + 'tb1qnq7v5dw4s65kmefczej495nh8mffrwt28tm9j96kpmn7mf0fuyrqs8aldv'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '0020c273ca3b47fcab3a53c9cc4daacfb37d921e62014c794f6d25606f43797fc0f0'; + expectedAddress = + 'tb1qcfeu5w68lj4n557fe3x64nan0kfpucspf3u57mf9vph5x7tlcrcquq4gza'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + + test('should correctly decode a P2SH scriptPubKey - testnet', () => { + let scriptPubKey, expectedAddress; + scriptPubKey = 'a91426101b3dae044fddcd71e6dfe831ebe383f23a5887'; + expectedAddress = '2MviUxfcKQdqszQPxofWjY4gCFQifcfG31b'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = 'a914b40fce0fca1eefcec03e61e833bf6326c11ccaf087'; + expectedAddress = '2N9fJYxagvUGVCcN4utpBhRMgG7eLAjZT9F'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = 'a9142d2ecbffc89a98365e2b45e90f00d3f6edec68d687'; + expectedAddress = '2MwN8TRK2jz44dBxvcebZzbqAYLC1MV3e3N'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + + test('should correctly decode a P2WPKH scriptPubKey - testnet', () => { + let scriptPubKey, expectedAddress; + + scriptPubKey = + '00201ff7ed9fecf23980cb3e6d9db8331054942aee8ca34b7190450e20a69ffeeda2'; + expectedAddress = + 'tb1qrlm7m8lv7gucpje7dkwmsvcs2j2z4m5v5d9hryz9pcs2d8l7ak3q2c7047'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = '0014def7b24f3e42b0858240cf3b993a3d44a7f5abe9'; + expectedAddress = 'tb1qmmmmyne7g2cgtqjqeuaejw3agjnlt2lftcxm8w'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = '0014e8550bc74d38fd002481339349ed4780cb1d77b3'; + expectedAddress = 'tb1qap2sh36d8r7sqfypxwf5nm28sr936aan8980fa'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = '0014647f217856160110ce3abcc8cc2ec77ff39561ad'; + expectedAddress = 'tb1qv3ljz7zkzcq3pn36hnyvctk80lee2cddknywz0'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + + test('should correctly decode a P2TR scriptPubKey - testnet', () => { + let scriptPubKey, expectedAddress; + + scriptPubKey = + '5120fb3c4af6e6471fe9319afcfd25eb3daf2b83e5be7411b1932bb36ed84701337f'; + expectedAddress = + 'tb1plv7y4ahxgu07jvv6ln7jt6ea4u4c8ed7wsgmryetkdhds3cpxdlsp8yh6m'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '512071d18973aa5daf214d500de76c1b860fcac1228260af21a62d5f6eed74cb0547'; + expectedAddress = + 'tb1pw8gcjua2tkhjzn2sphnkcxuxpl9vzg5zvzhjrf3dtahw6axtq4rsnn9w4l'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '5120bd6b2524312d1ce75e7e00676b1da2f6b72a846ca767b3009f09b471623a5865'; + expectedAddress = + 'tb1ph44j2fp395wwwhn7qpnkk8dz76mj4prv5anmxqylpx68zc36tpjs7hyrpn'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '5120ad55091d54ca1938cebe01f5435790c352021df23d8153a301cd90d06171ceed'; + expectedAddress = + 'tb1p442sj825egvn3n47q865x4uscdfqy80j8kq48gcpekgdqct3emksxu4anx'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + }); +}); diff --git a/utils/AddressUtils.test.ts b/utils/AddressUtils.test.ts index d968bd574..c54ee3c2d 100644 --- a/utils/AddressUtils.test.ts +++ b/utils/AddressUtils.test.ts @@ -12,6 +12,12 @@ jest.mock('../stores/Stores', () => ({ removeDecimalSpaces: false } } + }, + nodeInfoStore: { + nodeInfo: { + isTestNet: false, + isRegTest: false + } } })); @@ -965,4 +971,142 @@ describe('AddressUtils', () => { ).toEqual('Unused taproot pubkey'); }); }); + + describe('scriptPubKeyToAddress', () => { + test('should correctly decode a P2WSH scriptPubKey - mainnet', () => { + let scriptPubKey, expectedAddress; + scriptPubKey = + '002008737603b10129fc2dcb2e5167eb556ba2c84aec6622ff4d46767d186f63150d'; + expectedAddress = + 'bc1qppehvqa3qy5lctwt9egk0664dw3vsjhvvc307n2xwe73smmrz5xs5ckg7p'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '0020a1157ce5620e1e93ad8a98a9765971c89b5920a343add041b47ec34b7302d951'; + expectedAddress = + 'bc1q5y2heetzpc0f8tv2nz5hvkt3ezd4jg9rgwkaqsd50mp5kuczm9gs8v8gxh'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '0020983cca35d586a96de538166552d2773ed291b96a3af65917560ee7eda5e9e106'; + expectedAddress = + 'bc1qnq7v5dw4s65kmefczej495nh8mffrwt28tm9j96kpmn7mf0fuyrq80tshr'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '0020c273ca3b47fcab3a53c9cc4daacfb37d921e62014c794f6d25606f43797fc0f0'; + expectedAddress = + 'bc1qcfeu5w68lj4n557fe3x64nan0kfpucspf3u57mf9vph5x7tlcrcqtgr8cj'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + + test('should correctly decode a P2SH scriptPubKey - mainnet', () => { + let scriptPubKey, expectedAddress; + scriptPubKey = 'a91426101b3dae044fddcd71e6dfe831ebe383f23a5887'; + expectedAddress = '35AGtvgHoBLXncmR8Xtrv7gw34WVqkLLeU'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = 'a914b40fce0fca1eefcec03e61e833bf6326c11ccaf087'; + expectedAddress = '3J76VDefK1m8zpjXEmCK5UNR3mSAN7BcBD'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = 'a9142d2ecbffc89a98365e2b45e90f00d3f6edec68d687'; + expectedAddress = '35ovPgP18XYiRQLNwWyhNequKyyqavs3Sw'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + + test('should correctly decode a P2WPKH scriptPubKey - mainnet', () => { + let scriptPubKey, expectedAddress; + + scriptPubKey = + '00201ff7ed9fecf23980cb3e6d9db8331054942aee8ca34b7190450e20a69ffeeda2'; + expectedAddress = + 'bc1qrlm7m8lv7gucpje7dkwmsvcs2j2z4m5v5d9hryz9pcs2d8l7ak3qasgq03'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = '0014def7b24f3e42b0858240cf3b993a3d44a7f5abe9'; + expectedAddress = 'bc1qmmmmyne7g2cgtqjqeuaejw3agjnlt2lfp7agua'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = '0014e8550bc74d38fd002481339349ed4780cb1d77b3'; + expectedAddress = 'bc1qap2sh36d8r7sqfypxwf5nm28sr936aandruujw'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = '0014647f217856160110ce3abcc8cc2ec77ff39561ad'; + expectedAddress = 'bc1qv3ljz7zkzcq3pn36hnyvctk80lee2cddu4laeu'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + + test('should correctly decode a P2TR scriptPubKey - mainnet', () => { + let scriptPubKey, expectedAddress; + + scriptPubKey = + '5120fb3c4af6e6471fe9319afcfd25eb3daf2b83e5be7411b1932bb36ed84701337f'; + expectedAddress = + 'bc1plv7y4ahxgu07jvv6ln7jt6ea4u4c8ed7wsgmryetkdhds3cpxdlsk0jcq5'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '512071d18973aa5daf214d500de76c1b860fcac1228260af21a62d5f6eed74cb0547'; + expectedAddress = + 'bc1pw8gcjua2tkhjzn2sphnkcxuxpl9vzg5zvzhjrf3dtahw6axtq4rsymnp0s'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '5120bd6b2524312d1ce75e7e00676b1da2f6b72a846ca767b3009f09b471623a5865'; + expectedAddress = + 'bc1ph44j2fp395wwwhn7qpnkk8dz76mj4prv5anmxqylpx68zc36tpjsfljvmu'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + + scriptPubKey = + '5120ad55091d54ca1938cebe01f5435790c352021df23d8153a301cd90d06171ceed'; + expectedAddress = + 'bc1p442sj825egvn3n47q865x4uscdfqy80j8kq48gcpekgdqct3emks35rjff'; + expect(AddressUtils.scriptPubKeyToAddress(scriptPubKey)).toBe( + expectedAddress + ); + }); + + test('should throw an error for an invalid scriptPubKey (non-hex input)', () => { + const invalidScriptPubKey = 'invalid_script'; + expect(() => { + AddressUtils.scriptPubKeyToAddress(invalidScriptPubKey); + }).toThrow('Unknown scriptPubKey format'); + }); + + test('should throw an error for empty scriptPubKey', () => { + const emptyScriptPubKey = ''; + expect(() => { + AddressUtils.scriptPubKeyToAddress(emptyScriptPubKey); + }).toThrow('Unknown scriptPubKey format'); + }); + }); }); diff --git a/utils/AddressUtils.ts b/utils/AddressUtils.ts index 5882a2a22..b15f7238c 100644 --- a/utils/AddressUtils.ts +++ b/utils/AddressUtils.ts @@ -1,7 +1,11 @@ import BigNumber from 'bignumber.js'; -const bitcoin = require('bitcoinjs-lib'); +import * as bitcoin from 'bitcoinjs-lib'; +import ecc from '../zeus_modules/noble_ecc'; + +bitcoin.initEccLib(ecc); import Base64Utils from '../utils/Base64Utils'; +import stores from '../stores/Stores'; import { SATS_PER_BTC } from '../utils/UnitsUtils'; @@ -327,6 +331,143 @@ class AddressUtils { } return output; }; + + scriptPubKeyToAddress = (scriptPubKeyHex: string) => { + console.log('scriptPubKeyHex', scriptPubKeyHex); + const nodeInfo = stores?.nodeInfoStore?.nodeInfo; + const { isTestNet, isRegTest } = nodeInfo; + + let network = bitcoin.networks.bitcoin; + if (isTestNet) network = bitcoin.networks.testnet; + if (isRegTest) network = bitcoin.networks.regtest; + + const scriptBuffer = Buffer.from(scriptPubKeyHex, 'hex'); + + const decodedScript = bitcoin.script.decompile(scriptBuffer); + + // Handle P2WSH (Pay-to-Witness-Script-Hash) + if ( + decodedScript && // Ensure the script is decoded + decodedScript.length === 2 && // P2WSH scripts have exactly 2 elements + decodedScript[0] === bitcoin.opcodes.OP_0 && // First element is OP_0 + Buffer.isBuffer(decodedScript[1]) && // Second element is push data (a Buffer) + decodedScript[1].length === 32 // Push data length is exactly 32 bytes + ) { + console.log('attempting to decode P2WSH pkscript'); + try { + const witnessProgram = decodedScript[1]; + const { address } = bitcoin.payments.p2wsh({ + hash: witnessProgram, + network + }); + console.log('address', address); + return address; + } catch (e) { + console.log('error decoding P2WSH pkscript', e); + throw new Error('Invalid scriptPubKey format'); + } + } + + // Handle P2PKH (Pay-to-PubKey-Hash) + if ( + decodedScript && + decodedScript[0] === bitcoin.opcodes.OP_DUP && + decodedScript[1] === bitcoin.opcodes.OP_HASH160 && + Buffer.isBuffer(decodedScript[2]) && // Ensure it's a valid public key hash (20 bytes) + decodedScript[2].length === 20 + ) { + console.log('attempting to decode P2PKH pkscript'); + try { + const pubKeyHash = decodedScript[2]; + const { address } = bitcoin.payments.p2pkh({ + hash: pubKeyHash, + network + }); + console.log('address', address); + return address; + } catch (e) { + console.log('error decoding P2PKH pkscript', e); + throw new Error('Invalid scriptPubKey format'); + } + } + + // Handle P2PK (Pay-to-PubKey) + if (decodedScript && decodedScript[1] === bitcoin.opcodes.OP_CHECKSIG) { + console.log('attempting to decode P2PK pkscript'); + try { + const pubkey: any = decodedScript[0]; + const { address } = bitcoin.payments.p2pk({ + pubkey, + network + }); + console.log('address', address); + return address; + } catch (e) { + console.log('error decoding P2PK pkscript', e); + throw new Error('Invalid scriptPubKey format'); + } + } + + // Handle P2SH (Pay-to-Script-Hash) + if (decodedScript && decodedScript[0] === bitcoin.opcodes.OP_HASH160) { + console.log('attempting to decode P2SH pkscript'); + try { + const scriptHash: any = decodedScript[1]; + const { address } = bitcoin.payments.p2sh({ + hash: scriptHash, + network + }); + console.log('address', address); + return address; + } catch (e) { + console.log('error decoding P2SH pkscript', e); + throw new Error('Invalid scriptPubKey format'); + } + } + + // Handle P2WPKH (Pay-to-Witness-PubKey-Hash) - SegWit + if ( + decodedScript && + decodedScript[0] === bitcoin.opcodes.OP_0 && // First element is OP_0 + Buffer.isBuffer(decodedScript[1]) && // Second element is a buffer (push data) + decodedScript[1].length === 20 // Push data is exactly 20 bytes + ) { + console.log('attempting to decode P2WPKH pkscript'); + try { + const pubKeyHash = decodedScript[1]; // Use the hash part, not the entire script + const { address } = bitcoin.payments.p2wpkh({ + hash: pubKeyHash, + network + }); + console.log('address', address); + return address; + } catch (e) { + console.log('error decoding P2WPKH pkscript', e); + throw new Error('Invalid scriptPubKey format'); + } + } + + // Handle P2TR (Pay-to-Taproot) - Taproot address + if (decodedScript && decodedScript[0] === 0x51) { + // OP_CHECKSIG + console.log('attempting to decode P2TR pkscript'); + try { + const taprootPubKey: any = decodedScript[1]; + const { address } = bitcoin.payments.p2tr({ + pubkey: taprootPubKey, + network + }); + console.log('address', address); + return address; + } catch (e) { + console.log('error decoding P2TR pkscript', e); + throw new Error('Invalid scriptPubKey format'); + } + } + + console.log('unknown address type for script pubkey', scriptPubKeyHex); + throw new Error('Unknown scriptPubKey format'); + }; } const addressUtils = new AddressUtils();