diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 71cee0b..d738380 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2833,38 +2833,38 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - EXConstants: 3feb66fd1d94202fc1f0946d74e029d8b224b60e - EXImageLoader: e501c001bc40b8326605e82e6e80363c80fe06b5 + EXConstants: fce59a631a06c4151602843667f7cfe35f81e271 + EXImageLoader: 189e3476581efe3ad4d1d3fb4735b7179eb26f05 EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd - EXManifests: 83ef0844fcf06d6099b12a7bdbd7d36fc0e1dd16 - Expo: 7b40139dd13e65c0a0685d712b218add2d8b9ff8 - expo-dev-client: 12ef7d5b14d93e309922acea78dcd851db583a87 - expo-dev-launcher: 6ce51bf060db8ffe742bdc2ac2cc20aad9669462 - expo-dev-menu: d09251b31cb394323f09de45764937ff4d340a32 + EXManifests: a8d97683e5c7a3b026ffbd58559c64dc655b747b + Expo: 58cbcad4f708fc1b41d861586310ac1903282d85 + expo-dev-client: 425ee077d6754a98cfe3a2e2410d29b440b24c9d + expo-dev-launcher: 2559cd405f6e69bd1f1794b4756e46b7f0f648ef + expo-dev-menu: cf3e52f2e4402e54e9a36a25c7b38569a82c2c54 expo-dev-menu-interface: 600df12ea01efecdd822daaf13cc0ac091775533 - ExpoAsset: d999f3bbd998a750f3b74cb913229848901b926b - ExpoBlur: 688c53d5d26575cbb6c72e7fbd4caef8d8cfadf6 - ExpoCamera: 44705f35b32741582d00c69187c8ee59669ed46e - ExpoClipboard: 50fa3332136758be7ca2ea912bae585f74bb93c3 - ExpoFileSystem: aefcd337b94b874f88752ebefc52813b84992fad - ExpoFont: c625dbd97ed57e9089b172b2a7bb99003d074664 - ExpoGlassEffect: 387b1124a00166283d9c762c4dc895e84fdb60e8 - ExpoHaptics: 7240781406f4563c3f4eb1e72864302a8c643fd0 - ExpoHead: b691a2ed7ab02ed820b6c6468941832d34969c29 - ExpoImage: f6bcd30bb98d4c484869668fbc32834bb3ee5bfb - ExpoImagePicker: 115fada5d2e90bf900dea64151953ca39f67b47b - ExpoKeepAwake: 44bf6715bc1d2ddb17afe19d927cd039cda123f0 - ExpoLinearGradient: 814a21fc4056c3cf606e4f19e31e47074c5b5a86 - ExpoLinking: ebf543fd411d56375cb4eee07f6ab4e31c7ad959 - ExpoLocalAuthentication: a5d1c60718a852faa1852ef76298d8a167dc99f5 - ExpoLocation: 3579b0f9cbfb04495746d637a9e9057beba72992 - ExpoModulesCore: 7539a1ca866bc75274589535c95a338c0f74e667 - ExpoSecureStore: decd2943fa19fa6f02c5ef3689f9ba5012ad6a6d - ExpoSplashScreen: 72fbc6dd9d6404dd9d0725a56c9ac1383bc0b14f - ExpoSymbols: a0e8217dc2789d9ed0f28b63a466fc5beafd5100 - ExpoSystemUI: 4611247a411f231229f7e04449856035bd18ba21 - ExpoWebBrowser: 88b116cd378d9609c776c0903fe4070fca461588 - EXUpdatesInterface: 1436757deb0d574b84bba063bd024c315e0ec08b + ExpoAsset: f867e55ceb428aab99e1e8c082b5aee7c159ea18 + ExpoBlur: b90747a3f22a8b6ceffd9cb0dc41a4184efdc656 + ExpoCamera: 6a326deb45ba840749652e4c15198317aa78497e + ExpoClipboard: b36b287d8356887844bb08ed5c84b5979bb4dd1e + ExpoFileSystem: 858a44267a3e6e9057e0888ad7c7cfbf55d52063 + ExpoFont: f543ce20a228dd702813668b1a07b46f51878d47 + ExpoGlassEffect: 8ce45eca31f12e949e23a4ee13e2bfb59e9b0785 + ExpoHaptics: d3a6375d8dcc3a1083d003bc2298ff654fafb536 + ExpoHead: 4425246bc93411f0fe7f6945f95f698e91db8780 + ExpoImage: 686f972bff29525733aa13357f6691dc90aa03d8 + ExpoImagePicker: 1af3e4e31512d2f34c95c2a3018a3edc40aee748 + ExpoKeepAwake: 55f75eca6499bb9e4231ebad6f3e9cb8f99c0296 + ExpoLinearGradient: 809102bdb979f590083af49f7fa4805cd931bd58 + ExpoLinking: 8f0aaf69aa56f832913030503b6263dc6f647f37 + ExpoLocalAuthentication: 8a31808565da7af926dd9b595e98594d8b1553b6 + ExpoLocation: d5b61cb4970fa982e39ca94246a206a0c3b812ca + ExpoModulesCore: 031cf9b42fab2e84dcf34087dbd12ae666c100e0 + ExpoSecureStore: d32f751874a2ceb5aaeebeb3578e165c1ba2b24a + ExpoSplashScreen: bc3cffefca2716e5f22350ca109badd7e50ec14d + ExpoSymbols: 349ee2b4d7d5ff3ea8436467914f8a67635aa354 + ExpoSystemUI: 2ad325f361a2fcd96a464e8574e19935c461c9cc + ExpoWebBrowser: 17b064c621789e41d4816c95c93f429b84971f52 + EXUpdatesInterface: 5adf50cb41e079c861da6d9b4b954c3db9a50734 FBLazyVector: e95a291ad2dadb88e42b06e0c5fb8262de53ec12 Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d FirebaseABTesting: 31266c7845f9adde0f2e8a59267e9c82e4050898 @@ -2892,78 +2892,78 @@ SPEC CHECKSUMS: RCTTypeSafety: 16a4144ca3f959583ab019b57d5633df10b5e97c React: 914f8695f9bf38e6418228c2ffb70021e559f92f React-callinvoker: 1c0808402aee0c6d4a0d8e7220ce6547af9fba71 - React-Core: 4ae98f9e8135b8ddbd7c98730afb6fdae883db90 - React-Core-prebuilt: 8f4cca589c14e8cf8fc6db4587ef1c2056b5c151 - React-CoreModules: e878a90bb19b8f3851818af997dbae3b3b0a27ac - React-cxxreact: 28af9844f6dc87be1385ab521fbfb3746f19563c + React-Core: c61410ef0ca6055e204a963992e363227e0fd1c5 + React-Core-prebuilt: 02f0ad625ddd47463c009c2d0c5dd35c0d982599 + React-CoreModules: 1f6d1744b5f9f2ec684a4bb5ced25370f87e5382 + React-cxxreact: 3af79478e8187b63ffc22b794cd42d3fc1f1f2da React-debug: dfe7f2f01b36058f2a9b94872e071eb40e063a3c - React-defaultsnativemodule: 97a21185f62a12ee43066962e4786ef784de609c - React-domnativemodule: 7e6146dd8f6ea1b2ef0ae445d6c074291dde9e55 - React-Fabric: 5e75e04864d4112ec1a97aa5367a9f68c91abb99 - React-FabricComponents: 1d6b2f485acff0b79a10b9b637c70fea8359bed7 - React-FabricImage: 19d5e71d52febd292a560369482c7d19958494f5 - React-featureflags: 2ac1f10be17bd846f58746796697e8611a1d9f5b - React-featureflagsnativemodule: 02fa160389138c7142b88c946dc3c1917a90197e - React-graphics: b3688efb708952b76f9b282fefe1c54938724ae1 - React-hermes: fcfad3b917400f49026f3232561e039c9d1c34bf - React-idlecallbacksnativemodule: 10b74cbc6b2f8bb249696ed2fb352d12d384ecee - React-ImageManager: c02958d614171f56612393b35c355206780bf3a5 - React-jserrorhandler: 8e063c4be9723e87445dbcb22ac46e849c50e2e1 - React-jsi: d2c3f8555175371c02da6dfe7ed1b64b55a9d6c0 - React-jsiexecutor: ba537434eb45ee018b590ed7d29ee233fddb8669 - React-jsinspector: b9b6058146d39b1b8989b0d8b60f95746b5c9b66 - React-jsinspectorcdp: 603914629da96f3916e527a213913ac285466867 - React-jsinspectornetwork: 8c280e193c0b163905532ef6b6925c1a9b53d668 - React-jsinspectortracing: 76168f616aa2c0bcf0de4a999efab5f68f8d6f5c - React-jsitooling: c788e7fad8459826c6c692dc94d802bca85669b6 - React-jsitracing: bf304b323bc5f7cd6dfcc10e90298a02cb60ba0b - React-logger: c3e2f8a2e284341205f61eef3d4677ab5a309dfd - React-Mapbuffer: abad17e497845ba728b779e2fac123ee961d4545 - React-microtasksnativemodule: 90dfab2adb37c8c90da7afffcec427d7c90acb29 - react-native-biometrics: 352e5a794bfffc46a0c86725ea7dc62deb085bdc - react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06 - react-native-quick-base64: d87272898345e0fc3c3afb8e5dff335d51214f3e - react-native-safe-area-context: 3f031fa5a6c36a3731f6fa2202bfa8f411850c6b - React-NativeModulesApple: 1664340b8750d64e0ef3907c5e53d9481f74bcbd + React-defaultsnativemodule: 414bc0a2e02166cd7fa0a41e104434935ae01708 + React-domnativemodule: 9ec14a7fed0d3e4e76fd48e07d911f8934ab282d + React-Fabric: a2c0c50b9f0ea1f67e6b4bbde55589cacb8c3213 + React-FabricComponents: 796f19e3beb5e44bcdd72bd4d8a5954d6a61d225 + React-FabricImage: f75d4b23f449364c3aeae684cd2995c2be68149a + React-featureflags: 3285df1e581749640b797db0ab2120e9c6974da5 + React-featureflagsnativemodule: 181e9aa5afcdbc27ca560a6872d9510e0f7d9179 + React-graphics: 22b2dce86291d73fe7e37c7492d44865d9077a67 + React-hermes: e875778b496c86d07ab2ccaa36a9505d248a254b + React-idlecallbacksnativemodule: db0dae0874018750aedb4f6f574d68c05c6478cd + React-ImageManager: c659ddac4e196a28cefd036f5881b2c355363882 + React-jserrorhandler: 23a4e4e66c95a68300a4d01f359f9b8a5500aaa3 + React-jsi: 89d43d1e7d4d0663f8ba67e0b39eb4e4672c27de + React-jsiexecutor: abe4874aaab90dfee5dec480680220b2f8af07e3 + React-jsinspector: 91f19a57032075d0f820fdee080080a21c4afabe + React-jsinspectorcdp: 20537446838cf73f826cb1f78ec1180c58da6eb5 + React-jsinspectornetwork: fc9a317f3a57c33da6252c9cc160d321b85a3a48 + React-jsinspectortracing: 9dcf5dd0b7a4373704c4804cac75790b7d7977b4 + React-jsitooling: 65c430d41f8df1e5014863dbc569068d0458359b + React-jsitracing: 12d9474cefa6ce0ac4d0affde93dddabb17ea343 + React-logger: 50fdb9a8236da90c0b1072da5c32ee03aeb5bf28 + React-Mapbuffer: 0164bc6ba0866e6314d86851620e6f1f95a1b4fb + React-microtasksnativemodule: f5972fb404f7c363ff34b2292e72a716956c590c + react-native-biometrics: 43ed5b828646a7862dbc7945556446be00798e7d + react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba + react-native-quick-base64: 6568199bb2ac8e72ecdfdc73a230fbc5c1d3aac4 + react-native-safe-area-context: c5d42c4294aa175415947e23c2ac956253abfc7d + React-NativeModulesApple: 8969913947d5b576de4ed371a939455a8daf28aa React-oscompat: ce47230ed20185e91de62d8c6d139ae61763d09c - React-perflogger: b1af3cfb3f095f819b2814910000392a8e17ba9f - React-performancetimeline: 856a78d5841c3abee88376291315ac8a0c48a13e + React-perflogger: 02b010e665772c7dcb859d85d44c1bfc5ac7c0e4 + React-performancetimeline: bdad83ed66209c5cd252a38ed2c7b16c535c2286 React-RCTActionSheet: 0b14875b3963e9124a5a29a45bd1b22df8803916 - React-RCTAnimation: 60f6eca214a62b9673f64db6df3830cee902b5af - React-RCTAppDelegate: 37734b39bac108af30a0fd9d3e1149ec68b82c28 - React-RCTBlob: 83fbcbd57755caf021787324aac2fe9b028cc264 - React-RCTFabric: 31bf00d2759294baecbcb75643eeddd23163b9e4 - React-RCTFBReactNativeSpec: f8cd2d6381702b2039f3b9b3de025dbf36dbbc54 - React-RCTImage: 47aba3be7c6c64f956b7918ab933769602406aac - React-RCTLinking: 2dbaa4df2e4523f68baa07936bd8efdfa34d5f31 - React-RCTNetwork: 1fca7455f9dedf7de2b95bec438da06680f3b000 - React-RCTRuntime: 69ff58e90dbdb17160734f0df16527c18ff61f0c - React-RCTSettings: 01bf91c856862354d3d2f642ccb82f3697a4284a - React-RCTText: cb576a3797dcb64933613c522296a07eaafc0461 - React-RCTVibration: 560af8c086741f3525b8456a482cdbe27f9d098e + React-RCTAnimation: a7b90fd2af7bb9c084428867445a1481a8cb112e + React-RCTAppDelegate: 3262bedd01263f140ec62b7989f4355f57cec016 + React-RCTBlob: c17531368702f1ebed5d0ada75a7cf5915072a53 + React-RCTFabric: 14cd088b95d6c00c9dadad25cf946fb1770cc898 + React-RCTFBReactNativeSpec: 8948374e74e37e65923abfffeb12bf78e0aa7318 + React-RCTImage: c68078a120d0123f4f07a5ac77bea3bb10242f32 + React-RCTLinking: cf8f9391fe7fe471f96da3a5f0435235eca18c5b + React-RCTNetwork: ca31f7c879355760c2d9832a06ee35f517938a20 + React-RCTRuntime: 9669e40cc60ac65f09e75730e925c4a6a66debd4 + React-RCTSettings: e0e140b2ff4bf86d34e9637f6316848fc00be035 + React-RCTText: 75915bace6f7877c03a840cc7b6c622fb62bfa6b + React-RCTVibration: 25f26b85e5e432bb3c256f8b384f9269e9529f25 React-rendererconsistency: 51d190444383ec3b4c87321c700d1f6513c751c9 - React-renderercss: 71b1849846dd0c1765bafd2392e56e19d197bd48 - React-rendererdebug: 4cb736002d8dcaf2abcab40c71b2a5ec75c54b72 - React-RuntimeApple: 3db10ad73c93b61f5544d92dea7e47585850d3c8 - React-RuntimeCore: 5352d1df7bcca57c82b2b8d5f9e43e1206e0b3c3 - React-runtimeexecutor: 99c6e2de25a17554fd096d9150459a5b0a6e66b4 - React-RuntimeHermes: cbd16cf2df1f2c681fbbd7b88d5ee99c88cab8e3 - React-runtimescheduler: 390a9e98b919d165d6f9f9d08535ff65bcc75631 - React-timing: b4d33249d3105a9f890f271383bf02e07dbbe524 - React-utils: 0beba7df8da743c0703bbb24f0cbda1b1d03aaba - ReactAppDependencyProvider: c277c5b231881ad4f00cd59e3aa0671b99d7ebee - ReactCodegen: 8cdd5dde550463bc215652a888a61c8ca8f1e787 - ReactCommon: e6e232202a447d353e5531f2be82f50f47cbaa9a + React-renderercss: 876db927db3e30259c47be13bfa36d2c646b6c13 + React-rendererdebug: c1056f8c458501ef6022a076d47bcc9fd1a84b01 + React-RuntimeApple: 0766031d217ff53b8c5bb3a90a114c698890ccbb + React-RuntimeCore: b3c96e5eaa457d8f5680e7986d8694a8329bcfea + React-runtimeexecutor: d1f52523b0b66051e22be1aabd8dd5de1dbf5eef + React-RuntimeHermes: 2396631900bdb9fb05667de66345365d0d394e2b + React-runtimescheduler: 2032e7fa094a7494809b478ef4434588b5ab992a + React-timing: 32f695c27e3896d6f1bef77de042781b1bc999b3 + React-utils: 9f498dae9ce2da37772ab680a37ba516a4ea0a7e + ReactAppDependencyProvider: 1bcd3527ac0390a1c898c114f81ff954be35ed79 + ReactCodegen: 9a5f0d4c80fe9bb1ff29309a76392825aabe68c7 + ReactCommon: 08810150b1206cc44aecf5f6ae19af32f29151a8 ReactNativeDependencies: 71ce9c28beb282aa720ea7b46980fff9669f428a - RNCAsyncStorage: 0574155b95b96b89a51717c4dd42fb6057926bbc - RNFBApp: 47ce617b11851619d1a2088965c1d9cb0d54d735 - RNFBCrashlytics: cfd4b6cffd3c5318824a0eef3a4116867ac45be4 - RNFBPerf: 9f524b371620442da00bb5715997608f8edf759f - RNGestureHandler: c971f0a10fdffe00dfb4c142e87f34cdcd704376 - RNReanimated: f6923d4159aab5c12a6fa54e468aee947c70750d - RNScreens: e91463674394a1969b4688eb5db419d7045fce2d - RNSVG: 88b3b3e9e675fa4a2e14405c4c7f886ea8c82e06 - RNWorklets: 28ee7370ca8da356fcc914e3e68b97e9752196d2 + RNCAsyncStorage: c8407cc394627a3a4cf56b457d594bfa822b6ab8 + RNFBApp: 4f45ffec5426079cd552c756b1b1e5ab5dd06de4 + RNFBCrashlytics: 1ff12a1d04c0c2d9a26059f77d8f764583a09d2e + RNFBPerf: dda84537f516c76c14a78a051644f8a045279229 + RNGestureHandler: c3831b3b1b5014c6afab7fff7e4171394a3419ab + RNReanimated: 8315d083516a6690fcdb479b56fca8fabf65a73d + RNScreens: 33bbd1d0f86ed957557620dc94054b71db0a94ee + RNSVG: ac3047fdd9c0ebac9d0b731a352bddf216af6242 + RNWorklets: 76fce72926e28e304afb44f0da23b2d24f2c1fa0 SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838 SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c diff --git a/services/bitcoin-service.ts b/services/bitcoin-service.ts index 58e41e4..085c61d 100644 --- a/services/bitcoin-service.ts +++ b/services/bitcoin-service.ts @@ -5,6 +5,11 @@ import { BitcoinPrice, UTXO } from '@/types/wallet'; import { loadBip32Module } from './bip32-loader'; +import { + validateECCLibraryFull, + estimateTransactionSize, + generateChangeAddress, +} from './ecc-utils'; // Use centralized bip32 loader let bip32: any = null; @@ -101,25 +106,56 @@ export const ensureECC = async () => { }; /** - * Validate Bitcoin address format + * Validate Bitcoin address format with proper checksum verification + * Supports: P2WPKH (bc1q), P2WSH (bc1q 62 chars), P2TR (bc1p), P2PKH (1...), P2SH (3...) */ export const isValidBitcoinAddress = (address: string): boolean => { try { - // Basic validation for bech32 addresses (P2WPKH) - if (address.startsWith('bc1q') && address.length === 42) { - return true; + if (!address || typeof address !== 'string') { + return false; } - - // Basic validation for legacy addresses (P2PKH) - if (address.startsWith('1') && address.length >= 26 && address.length <= 35) { - return true; + + const trimmed = address.trim(); + + // Bech32/Bech32m addresses (SegWit v0 and v1/Taproot) + if (trimmed.toLowerCase().startsWith('bc1')) { + try { + const bitcoin = require('bitcoinjs-lib'); + // Use bitcoinjs-lib's address validation which handles both bech32 and bech32m + bitcoin.address.fromBech32(trimmed); + return true; + } catch { + // Bech32 decoding failed - invalid checksum or format + return false; + } } - - // Basic validation for P2SH addresses - if (address.startsWith('3') && address.length >= 26 && address.length <= 35) { - return true; + + // Legacy P2PKH addresses (start with '1') + if (trimmed.startsWith('1')) { + try { + const bitcoin = require('bitcoinjs-lib'); + // Validate Base58Check encoding + const decoded = bitcoin.address.fromBase58Check(trimmed); + // P2PKH version byte is 0x00 for mainnet + return decoded.version === 0x00; + } catch { + return false; + } } - + + // P2SH addresses (start with '3') + if (trimmed.startsWith('3')) { + try { + const bitcoin = require('bitcoinjs-lib'); + // Validate Base58Check encoding + const decoded = bitcoin.address.fromBase58Check(trimmed); + // P2SH version byte is 0x05 for mainnet + return decoded.version === 0x05; + } catch { + return false; + } + } + return false; } catch (error) { console.error('โŒ Address validation failed:', error); @@ -437,67 +473,9 @@ function calculateOutputCount(changeAmount: number): number { return outputCount; } -/** - * Estimate transaction size in bytes - */ -function estimateTransactionSize(inputCount: number, outputCount: number): number { - // Base transaction size - let size = 10; // version (4) + input count (1) + output count (1) + locktime (4) - - // P2WPKH input size (68 bytes each) - size += inputCount * 68; - - // P2WPKH output size (34 bytes each) - size += outputCount * 34; - - return size; -} +// estimateTransactionSize is imported from ecc-utils.ts -/** - * Generate a change address using the wallet's derivation path - */ -async function generateChangeAddress(mnemonic: string, changeIndex: number = 0): Promise { - try { - console.log('๐Ÿ”ง Generating change address for index:', changeIndex); - - // Ensure bip32 module is loaded - if (!bip32) { - bip32 = await loadBip32Module(); - } - - if (!bip32 || !bip32.BIP32Factory) { - throw new Error('BIP32 module or BIP32Factory not available'); - } - const bip39 = require('bip39'); - const ecc = (global as any).ecc; - const bip32Instance = bip32.BIP32Factory(ecc); - - // Derive private key for change address (chain 1) - const seed = await bip39.mnemonicToSeed(mnemonic); - const root = bip32Instance.fromSeed(seed); - const child = root.derive(`m/84'/0'/0'/1/${changeIndex}`); - - if (!child.publicKey) { - throw new Error('Failed to derive public key for change address'); - } - - // Generate P2WPKH address - const bech32 = await import('bech32'); - const { sha256 } = await import('@noble/hashes/sha256'); - const { ripemd160 } = await import('@noble/hashes/ripemd160'); - - const sha256Hash = sha256(child.publicKey); - const hash160 = ripemd160(sha256Hash); - const words = bech32.bech32.toWords(hash160); - const address = bech32.bech32.encode('bc', [0, ...words]); - - console.log('โœ… Generated change address:', address); - return address; - } catch (error) { - console.error('โŒ Failed to generate change address:', error); - throw error; - } -} +// generateChangeAddress is imported from ecc-utils.ts /** * Create and sign transaction @@ -522,113 +500,34 @@ async function createTransaction( if (!ecc) { throw new Error('ECC library not available'); } - - // Validate ECC library before using it + + // Validate ECC library and initialize bitcoinjs-lib using shared utility console.log('๐Ÿ”ง Validating ECC library before bitcoinjs-lib initialization...'); - - // Test basic ECC functionality - const testPrivateKey = new Uint8Array(32); - testPrivateKey[31] = 1; // Set to 1 to ensure it's a valid private key - + try { - // Test private key validation - if (!ecc.isPrivate(testPrivateKey)) { - throw new Error('ECC private key validation failed'); - } - - // Test point generation - const publicKey = ecc.pointFromScalar(testPrivateKey, true); - if (!publicKey || publicKey.length !== 33) { - throw new Error('ECC point generation failed'); - } - - // Test signing and verification - const testHash = new Uint8Array(32); - testHash.fill(0xaa); // Fill with test data - - const signature = ecc.sign(testHash, testPrivateKey); - if (!signature || signature.length === 0) { - throw new Error('ECC signing failed'); - } - - const isValid = ecc.verify(testHash, publicKey, signature); - if (!isValid) { - throw new Error('ECC signature verification failed'); - } - + validateECCLibraryFull(ecc); console.log('โœ… ECC library validation passed'); } catch (eccError) { console.error('โŒ ECC library validation failed:', eccError); throw new Error(`ECC library invalid: ${eccError instanceof Error ? eccError.message : 'Unknown error'}`); } - + // Initialize bitcoinjs-lib with ECC try { console.log('๐Ÿ”ง Initializing bitcoinjs-lib with ECC...'); - console.log('๐Ÿ”ง ECC object keys:', Object.keys(ecc)); - console.log('๐Ÿ”ง ECC object type:', typeof ecc); - - // Check if bitcoinjs-lib has the initEccLib method + if (typeof bitcoin.initEccLib !== 'function') { throw new Error('bitcoinjs-lib.initEccLib is not a function'); } - - // No arbitrary delay; rely on synchronous/asynchronous initEccLib - - console.log('๐Ÿ”ง Calling bitcoin.initEccLib...'); + const initResult = bitcoin.initEccLib(ecc); if (initResult instanceof Promise) { await initResult; } - - // Verify the initialization worked by checking if ECC is properly set - console.log('๐Ÿ”ง Verifying ECC initialization...'); - - // Log what's available on the bitcoin object for debugging - console.log('๐Ÿ”ง Available bitcoin object keys:', Object.keys(bitcoin)); - console.log('๐Ÿ”ง bitcoin.ECPair:', typeof bitcoin.ECPair); - console.log('๐Ÿ”ง bitcoin.ECPairFactory:', typeof bitcoin.ECPairFactory); - - // In bitcoinjs-lib 7.0.0, ECPair was removed and is no longer exported - // The library works with PSBT (Partially Signed Bitcoin Transactions) instead - // We just need to verify that our ECC library works correctly - console.log('๐Ÿ”ง Testing ECC library functionality (bitcoinjs-lib 7.x compatible)...'); - try { - const testPrivateKey = new Uint8Array(32); - testPrivateKey[31] = 1; - - // Test if our ECC library can create a public key - const publicKey = ecc.pointFromScalar(testPrivateKey, true); - if (!publicKey || publicKey.length !== 33) { - throw new Error('ECC library cannot create valid public keys'); - } - - // Test signing - const testHash = new Uint8Array(32); - testHash.fill(0xaa); - const signature = ecc.sign(testHash, testPrivateKey); - if (!signature || signature.length === 0) { - throw new Error('ECC library cannot create signatures'); - } - - // Test verification - const isValid = ecc.verify(testHash, publicKey, signature); - if (!isValid) { - throw new Error('ECC library signature verification failed'); - } - - console.log('โœ… ECC library verification successful - ready for transaction signing'); - } catch (eccTestError) { - console.error('โŒ ECC library test failed:', eccTestError); - throw new Error(`ECC library not working properly: ${eccTestError instanceof Error ? eccTestError.message : 'Unknown error'}`); - } - + console.log('โœ… bitcoinjs-lib initialized with ECC successfully'); } catch (initError) { console.error('โŒ Failed to initialize bitcoinjs-lib with ECC:', initError); - console.error('โŒ Error type:', typeof initError); - console.error('โŒ Error message:', initError instanceof Error ? initError.message : 'Unknown error'); - console.error('โŒ Error stack:', initError instanceof Error ? initError.stack : 'No stack'); throw new Error(`Failed to initialize bitcoinjs-lib: ${initError instanceof Error ? initError.message : 'Unknown error'}`); } @@ -763,7 +662,8 @@ async function createTransaction( // Derive private key for this specific address index and chain // Chain 0 = external/receiving addresses, Chain 1 = internal/change addresses - const child = root.derive(`m/84'/0'/0'/${utxoChain}/${utxoAddressIndex}`); + // Use derivePath for path strings, not derive (which takes a single index number) + const child = root.derivePath(`m/84'/0'/0'/${utxoChain}/${utxoAddressIndex}`); if (!child.privateKey) { throw new Error(`Failed to derive private key for address index ${utxoAddressIndex}`); diff --git a/services/cpfp-service.ts b/services/cpfp-service.ts index 2c0ff79..128c198 100644 --- a/services/cpfp-service.ts +++ b/services/cpfp-service.ts @@ -7,6 +7,13 @@ import { CPFPOptions, CPFPTransaction, CPFPValidationResult, UTXO } from '@/type import { loadBip32Module } from './bip32-loader'; import { ensureECC } from './bitcoin-service'; import { esploraGet } from './esplora-service'; +import { + validateECCLibraryFull, + estimateTransactionSize, + deriveAddressIndexAndChainFromAddress, + generateChangeAddress, + clearAddressIndexCache as clearCacheFromUtils, +} from './ecc-utils'; // Use centralized bip32 loader let bip32: any = null; @@ -213,77 +220,22 @@ export async function createCPFPTransaction( if (!ecc) { throw new Error('ECC library not available'); } - - // Validate ECC library before using it + + // Validate ECC library and initialize bitcoinjs-lib using shared utilities console.log('๐Ÿ”ง Validating ECC library before bitcoinjs-lib initialization...'); - - // Test basic ECC functionality - const testPrivateKey = new Uint8Array(32); - testPrivateKey[31] = 1; // Set to 1 to ensure it's a valid private key - + try { - // Test private key validation - if (!ecc.isPrivate(testPrivateKey)) { - throw new Error('ECC private key validation failed'); - } - - // Test point generation - const publicKey = ecc.pointFromScalar(testPrivateKey, true); - if (!publicKey || publicKey.length !== 33) { - throw new Error('ECC point generation failed'); - } - + validateECCLibraryFull(ecc); console.log('โœ… ECC library validation passed'); } catch (eccError) { console.error('โŒ ECC library validation failed:', eccError); throw new Error(`ECC library invalid: ${eccError instanceof Error ? eccError.message : 'Unknown error'}`); } - + // Initialize bitcoinjs-lib with ECC try { console.log('๐Ÿ”ง Initializing bitcoinjs-lib with ECC...'); bitcoin.initEccLib(ecc); - - // Verify the initialization worked by checking if ECC is properly set - console.log('๐Ÿ”ง Verifying ECC initialization...'); - - // Add a small delay to ensure ECC is fully initialized - await new Promise(resolve => setTimeout(resolve, 200)); - - // In bitcoinjs-lib 7.0.0, ECPair was removed and is no longer exported - // The library works with PSBT (Partially Signed Bitcoin Transactions) instead - // We just need to verify that our ECC library works correctly - console.log('๐Ÿ”ง Testing ECC library functionality (bitcoinjs-lib 7.x compatible)...'); - try { - const verifyPrivateKey = new Uint8Array(32); - verifyPrivateKey[31] = 1; // Set to 1 to ensure it's a valid private key - - // Test if our ECC library can create a public key - const publicKey = ecc.pointFromScalar(verifyPrivateKey, true); - if (!publicKey || publicKey.length !== 33) { - throw new Error('ECC library cannot create valid public keys'); - } - - // Test signing - const testHash = new Uint8Array(32); - testHash.fill(0xaa); - const signature = ecc.sign(testHash, verifyPrivateKey); - if (!signature || signature.length === 0) { - throw new Error('ECC library cannot create signatures'); - } - - // Test verification - const isValid = ecc.verify(testHash, publicKey, signature); - if (!isValid) { - throw new Error('ECC library signature verification failed'); - } - - console.log('โœ… ECC library verification successful - ready for CPFP transaction'); - } catch (verifyError) { - console.error('โŒ ECC verification failed:', verifyError); - throw new Error(`ECC library not working properly: ${verifyError instanceof Error ? verifyError.message : 'Unknown error'}`); - } - console.log('โœ… bitcoinjs-lib initialized with ECC successfully'); } catch (initError) { console.error('โŒ Failed to initialize bitcoinjs-lib with ECC:', initError); @@ -567,262 +519,13 @@ function calculateParentFee(parentTx: any): number { return totalInputValue - totalOutputValue; } -/** - * Estimate transaction size in bytes - */ -function estimateTransactionSize(inputCount: number, outputCount: number): number { - // Base transaction size - let size = 10; // version (4) + input count (1) + output count (1) + locktime (4) - - // P2WPKH input size (68 bytes each) - size += inputCount * 68; - - // P2WPKH output size (34 bytes each) - size += outputCount * 34; - - return size; -} - -// Cache for address-to-index mappings to avoid redundant derivations -const addressIndexCache = new Map(); +// estimateTransactionSize, deriveAddressIndexAndChainFromAddress, and generateChangeAddress +// are imported from ecc-utils.ts -/** - * Clear the address index cache - */ +// Re-export clearCPFPAddressIndexCache for backward compatibility export function clearCPFPAddressIndexCache(): void { - addressIndexCache.clear(); - console.log(`๐Ÿงน Cleared CPFP address index cache`); -} - -/** - * Derive the BIP32 address index and chain from an address by testing derivation paths - * Returns { index, chain } where chain is 0 for external/receiving, 1 for internal/change - */ -export async function deriveAddressIndexAndChainFromAddress(mnemonic: string, targetAddress: string): Promise<{ index: number; chain: number }> { - try { - // Check cache first (need to cache both index and chain) - const cacheKey = `${targetAddress}_full`; - if (addressIndexCache.has(cacheKey)) { - const cached = addressIndexCache.get(cacheKey)!; - console.log(`โœ… Found cached BIP32 index ${cached} for address: ${targetAddress}`); - // Parse the cached value (format: "chain:index") - const [chain, index] = cached.toString().split(':').map(Number); - return { index, chain }; - } - - console.log(`๐Ÿ” Deriving BIP32 index and chain for address: ${targetAddress}`); - - // Ensure bip32 module is loaded - if (!bip32) { - bip32 = await loadBip32Module(); - } - - if (!bip32 || !bip32.BIP32Factory) { - throw new Error('BIP32 module or BIP32Factory not available'); - } - const bip39 = require('bip39'); - const ecc = (global as any).ecc; - const bip32Instance = bip32.BIP32Factory(ecc); - const bech32 = await import('bech32'); - const { sha256 } = await import('@noble/hashes/sha256'); - const { ripemd160 } = await import('@noble/hashes/ripemd160'); - - const seed = await bip39.mnemonicToSeed(mnemonic); - const root = bip32Instance.fromSeed(seed); - - // Check both external (chain 0) and internal/change (chain 1) chains - for (const chain of [0, 1]) { - const chainNode = root.derivePath(`m/84'/0'/0'/${chain}`); - - // Use optimized linear search with batching to prevent UI blocking - let foundIndex = -1; - const batchSize = 50; - const batchDelay = 5; - let currentIndex = 0; - const maxSearchRange = 10000; - - while (foundIndex === -1 && currentIndex < maxSearchRange) { - const endIndex = Math.min(currentIndex + batchSize, maxSearchRange); - - for (let i = currentIndex; i < endIndex; i++) { - try { - const child = chainNode.derive(i); - if (!child.publicKey) continue; - - // Generate P2WPKH address - const sha256Hash = sha256(child.publicKey); - const hash160 = ripemd160(sha256Hash); - const words = bech32.bech32.toWords(hash160); - const address = bech32.bech32.encode('bc', [0, ...words]); - - if (address === targetAddress) { - foundIndex = i; - break; - } - } catch (error) { - console.warn(`โš ๏ธ Failed to derive address at chain ${chain}, index ${i}:`, error); - continue; - } - } - - currentIndex = endIndex; - - // Small delay between batches to prevent UI blocking - if (foundIndex === -1 && currentIndex < maxSearchRange) { - await new Promise(resolve => setTimeout(resolve, batchDelay)); - } - } - - if (foundIndex !== -1) { - // Cache the result - addressIndexCache.set(cacheKey, `${chain}:${foundIndex}`); - console.log(`โœ… Found BIP32 chain ${chain}, index ${foundIndex} for address: ${targetAddress}`); - return { index: foundIndex, chain }; - } - } - - throw new Error(`Could not find BIP32 index for address: ${targetAddress} (searched both chains up to index ${10000})`); - } catch (error) { - console.error(`โŒ Failed to derive address index and chain:`, error); - throw error; - } -} - -/** - * Derive the BIP32 address index from an address by testing derivation paths - * Legacy function - prefer deriveAddressIndexAndChainFromAddress for new code - */ -export async function deriveAddressIndexFromAddress(mnemonic: string, targetAddress: string): Promise { - try { - // Check cache first - if (addressIndexCache.has(targetAddress)) { - const cachedValue = addressIndexCache.get(targetAddress)!; - console.log(`โœ… Found cached BIP32 index ${cachedValue} for address: ${targetAddress}`); - // If cached value is a string, parse the index from it - const cachedIndex = typeof cachedValue === 'string' - ? parseInt(cachedValue.split(':')[1], 10) - : cachedValue; - return cachedIndex; - } - - console.log(`๐Ÿ” Deriving BIP32 index for address: ${targetAddress}`); - - // Ensure bip32 module is loaded - if (!bip32) { - bip32 = await loadBip32Module(); - } - - if (!bip32 || !bip32.BIP32Factory) { - throw new Error('BIP32 module or BIP32Factory not available'); - } - const bip39 = require('bip39'); - const ecc = (global as any).ecc; - const bip32Instance = bip32.BIP32Factory(ecc); - const bech32 = await import('bech32'); - const { sha256 } = await import('@noble/hashes/sha256'); - const { ripemd160 } = await import('@noble/hashes/ripemd160'); - - const seed = await bip39.mnemonicToSeed(mnemonic); - const root = bip32Instance.fromSeed(seed); - const externalChain = root.derivePath(`m/84'/0'/0'/0`); - - // Use optimized linear search with batching to prevent UI blocking - let foundIndex = -1; - const batchSize = 50; - const batchDelay = 5; - let currentIndex = 0; - const maxSearchRange = 10000; - - while (foundIndex === -1 && currentIndex < maxSearchRange) { - const endIndex = Math.min(currentIndex + batchSize, maxSearchRange); - - for (let i = currentIndex; i < endIndex; i++) { - try { - const child = externalChain.derive(i); - if (!child.publicKey) continue; - - // Generate P2WPKH address - const sha256Hash = sha256(child.publicKey); - const hash160 = ripemd160(sha256Hash); - const words = bech32.bech32.toWords(hash160); - const address = bech32.bech32.encode('bc', [0, ...words]); - - if (address === targetAddress) { - foundIndex = i; - break; - } - } catch (error) { - console.warn(`โš ๏ธ Failed to derive address at index ${i}:`, error); - continue; - } - } - - currentIndex = endIndex; - - // Small delay between batches to prevent UI blocking - if (foundIndex === -1 && currentIndex < maxSearchRange) { - await new Promise(resolve => setTimeout(resolve, batchDelay)); - } - } - - if (foundIndex === -1) { - throw new Error(`Could not find BIP32 index for address: ${targetAddress} (searched up to index ${currentIndex})`); - } - - // Cache the result - addressIndexCache.set(targetAddress, foundIndex); - console.log(`โœ… Found BIP32 index ${foundIndex} for address: ${targetAddress}`); - return foundIndex; - } catch (error) { - console.error(`โŒ Failed to derive address index:`, error); - throw error; - } -} - -/** - * Generate a change address using the wallet's derivation path - */ -async function generateChangeAddress(mnemonic: string, changeIndex: number = 0): Promise { - try { - console.log(`๐Ÿ”ง Generating change address for index: ${changeIndex}`); - - // Ensure bip32 module is loaded - if (!bip32) { - bip32 = await loadBip32Module(); - } - - if (!bip32 || !bip32.BIP32Factory) { - throw new Error('BIP32 module or BIP32Factory not available'); - } - const bip39 = require('bip39'); - const ecc = (global as any).ecc; - const bip32Instance = bip32.BIP32Factory(ecc); - - // Derive private key for change address (chain 1) - const seed = await bip39.mnemonicToSeed(mnemonic); - const root = bip32Instance.fromSeed(seed); - const child = root.derivePath(`m/84'/0'/0'/1/${changeIndex}`); - - if (!child.publicKey) { - throw new Error('Failed to derive public key for change address'); - } - - // Generate P2WPKH address - const bech32 = await import('bech32'); - const { sha256 } = await import('@noble/hashes/sha256'); - const { ripemd160 } = await import('@noble/hashes/ripemd160'); - - const sha256Hash = sha256(child.publicKey); - const hash160 = ripemd160(sha256Hash); - const words = bech32.bech32.toWords(hash160); - const address = bech32.bech32.encode('bc', [0, ...words]); - - console.log(`โœ… Generated change address: ${address}`); - return address; - } catch (error) { - console.error(`โŒ Failed to generate change address:`, error); - throw error; - } + clearCacheFromUtils(); + console.log('๐Ÿงน Cleared CPFP address index cache (via ecc-utils)'); } /** diff --git a/services/ecc-utils.ts b/services/ecc-utils.ts new file mode 100644 index 0000000..6c55ea0 --- /dev/null +++ b/services/ecc-utils.ts @@ -0,0 +1,640 @@ +/** + * Shared ECC Utilities + * Consolidates ECC validation, initialization, and address derivation utilities + * used across bitcoin-service, rbf-service, and cpfp-service. + */ + +import { loadBip32Module } from './bip32-loader'; + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Interface for the ECC library functions used by bitcoinjs-lib and related services. + * The library is typically @noble/secp256k1 exposed via crypto-polyfill. + */ +export interface ECCLibrary { + isPrivate(privKey: Uint8Array): boolean; + pointFromScalar(scalar: Uint8Array, compressed: boolean): Uint8Array | null; + sign(hash: Uint8Array, privKey: Uint8Array): Uint8Array; + verify(hash: Uint8Array, pubKey: Uint8Array, signature: Uint8Array): boolean; +} + +// ============================================================================ +// Transaction Size Constants +// ============================================================================ + +/** + * Standard SegWit v0 P2WPKH input size: 68 bytes + * Derived from: 32 (txid) + 4 (vout) + 1 (scriptSig length) + 4 (sequence) + + * 1 (witness item count) + 72 (signature) + 33 (pubkey), weighted by witness discount + */ +export const P2WPKH_INPUT_SIZE = 68; + +/** + * Standard SegWit v0 P2WPKH output size: 34 bytes + * Derived from: 8 (value) + 1 (scriptPubKey length) + 25 (scriptPubKey for P2WPKH) + */ +export const P2WPKH_OUTPUT_SIZE = 34; + +/** + * Base transaction overhead: 10 bytes + * version (4) + input count (1) + output count (1) + locktime (4) + */ +export const TX_BASE_SIZE = 10; + +// ============================================================================ +// ECC Validation Functions +// ============================================================================ + +/** + * Validates basic ECC library functionality (private key validation and point generation). + * Throws an error if validation fails. + */ +export function validateECCLibrary(ecc: ECCLibrary): void { + const testPrivateKey = new Uint8Array(32); + testPrivateKey[31] = 1; // Set to 1 to ensure it's a valid private key + + // Test private key validation + if (!ecc.isPrivate(testPrivateKey)) { + throw new Error('ECC private key validation failed'); + } + + // Test point generation + const publicKey = ecc.pointFromScalar(testPrivateKey, true); + if (!publicKey || publicKey.length !== 33) { + throw new Error('ECC point generation failed'); + } +} + +/** + * Validates full ECC library functionality including signing and verification. + * This is a more comprehensive test than validateECCLibrary. + * Throws an error if validation fails. + */ +export function validateECCLibraryFull(ecc: ECCLibrary): void { + // First run basic validation + validateECCLibrary(ecc); + + const testPrivateKey = new Uint8Array(32); + testPrivateKey[31] = 1; + + // Get public key for verification + const publicKey = ecc.pointFromScalar(testPrivateKey, true); + if (!publicKey) { + throw new Error('ECC point generation failed'); + } + + // Test signing + const testHash = new Uint8Array(32); + testHash.fill(0xaa); + + const signature = ecc.sign(testHash, testPrivateKey); + if (!signature || signature.length === 0) { + throw new Error('ECC signing failed'); + } + + // Test verification + const isValid = ecc.verify(testHash, publicKey, signature); + if (!isValid) { + throw new Error('ECC signature verification failed'); + } +} + +/** + * Initializes bitcoinjs-lib with the provided ECC library. + * Validates the ECC library and performs verification tests. + * + * @param ecc - The ECC library to use (typically from global.ecc) + * @returns The bitcoin module after initialization + */ +export async function initializeBitcoinJsWithECC(ecc: ECCLibrary): Promise { + if (!ecc) { + throw new Error('ECC library not available'); + } + + // Validate ECC library before using it + console.log('๐Ÿ”ง Validating ECC library before bitcoinjs-lib initialization...'); + + try { + validateECCLibrary(ecc); + console.log('โœ… ECC library validation passed'); + } catch (eccError) { + console.error('โŒ ECC library validation failed:', eccError); + throw new Error(`ECC library invalid: ${eccError instanceof Error ? eccError.message : 'Unknown error'}`); + } + + // Initialize bitcoinjs-lib with ECC + const bitcoin = require('bitcoinjs-lib'); + + try { + console.log('๐Ÿ”ง Initializing bitcoinjs-lib with ECC...'); + console.log('๐Ÿ”ง ECC object keys:', Object.keys(ecc)); + + // Check if bitcoinjs-lib has the initEccLib method + if (typeof bitcoin.initEccLib !== 'function') { + throw new Error('bitcoinjs-lib.initEccLib is not a function'); + } + + console.log('๐Ÿ”ง Calling bitcoin.initEccLib...'); + const initResult = bitcoin.initEccLib(ecc); + if (initResult instanceof Promise) { + await initResult; + } + + // Verify the initialization worked + console.log('๐Ÿ”ง Verifying ECC initialization...'); + console.log('๐Ÿ”ง Available bitcoin object keys:', Object.keys(bitcoin)); + + // In bitcoinjs-lib 7.0.0+, ECPair was removed. The library works with PSBT instead. + // We verify that our ECC library works correctly with signing/verification tests. + console.log('๐Ÿ”ง Testing ECC library functionality (bitcoinjs-lib 7.x compatible)...'); + + try { + validateECCLibraryFull(ecc); + console.log('โœ… ECC library verification successful'); + } catch (verifyError) { + console.error('โŒ ECC verification failed:', verifyError); + throw new Error(`ECC library not working properly: ${verifyError instanceof Error ? verifyError.message : 'Unknown error'}`); + } + + console.log('โœ… bitcoinjs-lib initialized with ECC successfully'); + return bitcoin; + } catch (initError) { + console.error('โŒ Failed to initialize bitcoinjs-lib with ECC:', initError); + console.error('โŒ Error type:', typeof initError); + console.error('โŒ Error message:', initError instanceof Error ? initError.message : 'Unknown error'); + throw new Error(`Failed to initialize bitcoinjs-lib: ${initError instanceof Error ? initError.message : 'Unknown error'}`); + } +} + +// ============================================================================ +// Transaction Size Estimation +// ============================================================================ + +/** + * Estimate transaction size in virtual bytes. + * Assumes all inputs/outputs are SegWit v0 P2WPKH type. + * + * @param inputCount - Number of transaction inputs + * @param outputCount - Number of transaction outputs + * @returns Estimated transaction size in vBytes + */ +export function estimateTransactionSize(inputCount: number, outputCount: number): number { + let size = TX_BASE_SIZE; + size += inputCount * P2WPKH_INPUT_SIZE; + size += outputCount * P2WPKH_OUTPUT_SIZE; + return size; +} + +// ============================================================================ +// Address Index Cache +// ============================================================================ + +/** + * Cache for address-to-index mappings to avoid redundant derivations. + * Format: Map + */ +const addressIndexCache = new Map(); + +/** + * Clear the address index cache. + * Call this when switching wallets or when cache becomes stale. + */ +export function clearAddressIndexCache(): void { + addressIndexCache.clear(); + console.log('๐Ÿงน Cleared address index cache'); +} + +/** + * Get the current size of the address index cache. + */ +export function getAddressIndexCacheSize(): number { + return addressIndexCache.size; +} + +// ============================================================================ +// Address Derivation Utilities +// ============================================================================ + +/** + * Derive the BIP32 address index and chain from an address by testing derivation paths. + * Returns { index, chain } where chain is 0 for external/receiving, 1 for internal/change. + * + * Performance improvements: + * 1. Uses caching to avoid redundant derivations + * 2. Implements optimized linear search with batching + * 3. Adds small delays between batches to prevent UI blocking + * + * @param mnemonic - The wallet mnemonic + * @param targetAddress - The Bitcoin address to find the derivation path for + * @returns Object containing the address index and chain (0=external, 1=change) + */ +export async function deriveAddressIndexAndChainFromAddress( + mnemonic: string, + targetAddress: string +): Promise<{ index: number; chain: number }> { + try { + // Check cache first + const cacheKey = `${targetAddress}_full`; + if (addressIndexCache.has(cacheKey)) { + const cached = addressIndexCache.get(cacheKey)!; + console.log(`โœ… Found cached BIP32 index ${cached} for address: ${targetAddress}`); + const [chain, index] = cached.split(':').map(Number); + return { index, chain }; + } + + console.log(`๐Ÿ” Deriving BIP32 index and chain for address: ${targetAddress}`); + + // Load required modules + const bip32Module = await loadBip32Module(); + if (!bip32Module || !bip32Module.BIP32Factory) { + throw new Error('BIP32 module or BIP32Factory not available'); + } + + const bip39 = require('bip39'); + const ecc = (global as any).ecc; + const bip32Instance = bip32Module.BIP32Factory(ecc); + const bech32 = await import('bech32'); + const { sha256 } = await import('@noble/hashes/sha256'); + const { ripemd160 } = await import('@noble/hashes/ripemd160'); + + const seed = await bip39.mnemonicToSeed(mnemonic); + const root = bip32Instance.fromSeed(seed); + + // Search parameters + const batchSize = 50; + const batchDelay = 5; + const maxSearchRange = 10000; + + // Check both external (chain 0) and internal/change (chain 1) chains + for (const chain of [0, 1]) { + const chainNode = root.derivePath(`m/84'/0'/0'/${chain}`); + + let foundIndex = -1; + let currentIndex = 0; + + while (foundIndex === -1 && currentIndex < maxSearchRange) { + const endIndex = Math.min(currentIndex + batchSize, maxSearchRange); + + for (let i = currentIndex; i < endIndex; i++) { + try { + const child = chainNode.derive(i); + if (!child.publicKey) continue; + + // Generate P2WPKH address + const sha256Hash = sha256(child.publicKey); + const hash160 = ripemd160(sha256Hash); + const words = bech32.bech32.toWords(hash160); + const address = bech32.bech32.encode('bc', [0, ...words]); + + if (address === targetAddress) { + foundIndex = i; + break; + } + } catch (error) { + console.warn(`โš ๏ธ Failed to derive address at chain ${chain}, index ${i}:`, error); + continue; + } + } + + currentIndex = endIndex; + + // Small delay between batches to prevent UI blocking + if (foundIndex === -1 && currentIndex < maxSearchRange) { + await new Promise(resolve => setTimeout(resolve, batchDelay)); + } + } + + if (foundIndex !== -1) { + // Cache the result + addressIndexCache.set(cacheKey, `${chain}:${foundIndex}`); + console.log(`โœ… Found BIP32 chain ${chain}, index ${foundIndex} for address: ${targetAddress}`); + return { index: foundIndex, chain }; + } + } + + throw new Error(`Could not find BIP32 index for address: ${targetAddress} (searched both chains up to index ${maxSearchRange})`); + } catch (error) { + console.error('โŒ Failed to derive address index and chain:', error); + throw error; + } +} + +/** + * Derive the BIP32 address index from an address (external chain only). + * Legacy function - prefer deriveAddressIndexAndChainFromAddress for new code. + * + * @param mnemonic - The wallet mnemonic + * @param targetAddress - The Bitcoin address to find the derivation path for + * @returns The address index on the external chain (chain 0) + */ +export async function deriveAddressIndexFromAddress( + mnemonic: string, + targetAddress: string +): Promise { + try { + // Check cache first (simple key without chain) + if (addressIndexCache.has(targetAddress)) { + const cachedValue = addressIndexCache.get(targetAddress)!; + console.log(`โœ… Found cached BIP32 index ${cachedValue} for address: ${targetAddress}`); + // Parse the index from cached value + const cachedIndex = cachedValue.includes(':') + ? parseInt(cachedValue.split(':')[1], 10) + : parseInt(cachedValue, 10); + return cachedIndex; + } + + console.log(`๐Ÿ” Deriving BIP32 index for address: ${targetAddress}`); + + // Load required modules + const bip32Module = await loadBip32Module(); + if (!bip32Module || !bip32Module.BIP32Factory) { + throw new Error('BIP32 module or BIP32Factory not available'); + } + + const bip39 = require('bip39'); + const ecc = (global as any).ecc; + const bip32Instance = bip32Module.BIP32Factory(ecc); + const bech32 = await import('bech32'); + const { sha256 } = await import('@noble/hashes/sha256'); + const { ripemd160 } = await import('@noble/hashes/ripemd160'); + + const seed = await bip39.mnemonicToSeed(mnemonic); + const root = bip32Instance.fromSeed(seed); + const externalChain = root.derivePath(`m/84'/0'/0'/0`); + + // Search parameters + const batchSize = 50; + const batchDelay = 5; + let currentIndex = 0; + const maxSearchRange = 10000; + + let foundIndex = -1; + + while (foundIndex === -1 && currentIndex < maxSearchRange) { + const endIndex = Math.min(currentIndex + batchSize, maxSearchRange); + + for (let i = currentIndex; i < endIndex; i++) { + try { + const child = externalChain.derive(i); + if (!child.publicKey) continue; + + // Generate P2WPKH address + const sha256Hash = sha256(child.publicKey); + const hash160 = ripemd160(sha256Hash); + const words = bech32.bech32.toWords(hash160); + const address = bech32.bech32.encode('bc', [0, ...words]); + + if (address === targetAddress) { + foundIndex = i; + break; + } + } catch (error) { + console.warn(`โš ๏ธ Failed to derive address at index ${i}:`, error); + continue; + } + } + + currentIndex = endIndex; + + // Small delay between batches to prevent UI blocking + if (foundIndex === -1 && currentIndex < maxSearchRange) { + await new Promise(resolve => setTimeout(resolve, batchDelay)); + } + } + + // If not found in the initial range, expand the search + if (foundIndex === -1) { + console.log('๐Ÿ” Address not found in initial range, expanding search...'); + + const expandedBatchSize = 25; + const expandedBatchDelay = 10; + let expandedHigh = maxSearchRange * 2; + + while (foundIndex === -1 && expandedHigh <= 100000) { + console.log(`๐Ÿ” Searching range ${currentIndex} to ${expandedHigh}...`); + + for (let i = currentIndex; i < expandedHigh; i += expandedBatchSize) { + const endIndex = Math.min(i + expandedBatchSize, expandedHigh); + + for (let j = i; j < endIndex; j++) { + try { + const child = externalChain.derive(j); + if (!child.publicKey) continue; + + const sha256Hash = sha256(child.publicKey); + const hash160 = ripemd160(sha256Hash); + const words = bech32.bech32.toWords(hash160); + const address = bech32.bech32.encode('bc', [0, ...words]); + + if (address === targetAddress) { + foundIndex = j; + break; + } + } catch (error) { + console.warn(`โš ๏ธ Failed to derive address at index ${j}:`, error); + continue; + } + } + + if (foundIndex !== -1) break; + + if (endIndex < expandedHigh) { + await new Promise(resolve => setTimeout(resolve, expandedBatchDelay)); + } + } + + currentIndex = expandedHigh; + expandedHigh *= 2; + } + } + + if (foundIndex === -1) { + throw new Error(`Could not find BIP32 index for address: ${targetAddress} (searched up to index ${currentIndex})`); + } + + // Cache the result + addressIndexCache.set(targetAddress, String(foundIndex)); + console.log(`โœ… Found BIP32 index ${foundIndex} for address: ${targetAddress}`); + return foundIndex; + } catch (error) { + console.error('โŒ Failed to derive address index:', error); + throw error; + } +} + +/** + * Find the next unused address index for generating new addresses. + * This ensures we don't reuse addresses and follow proper BIP32 gap limit. + * + * @param mnemonic - The wallet mnemonic + * @param walletAddresses - Array of known wallet addresses + * @returns The next unused address index + */ +export async function findNextUnusedAddressIndex( + mnemonic: string, + walletAddresses: string[] +): Promise { + try { + console.log(`๐Ÿ” Finding next unused address index for ${walletAddresses.length} addresses...`); + + const usedIndices = new Set(); + let successfulDerivations = 0; + const batchSize = 5; + const batchDelay = 10; + + for (let i = 0; i < walletAddresses.length; i += batchSize) { + const batch = walletAddresses.slice(i, i + batchSize); + + for (const address of batch) { + try { + const index = await deriveAddressIndexFromAddress(mnemonic, address); + usedIndices.add(index); + successfulDerivations++; + } catch (error) { + console.warn(`โš ๏ธ Could not derive index for address ${address}:`, error); + } + } + + // Small delay between batches to prevent UI blocking + if (i + batchSize < walletAddresses.length) { + await new Promise(resolve => setTimeout(resolve, batchDelay)); + } + } + + // If we couldn't derive any addresses, fall back to 0 + if (successfulDerivations === 0) { + console.warn('โš ๏ธ Could not derive any address indices, using fallback index 0'); + return 0; + } + + // Find the next unused index by looking for the first gap or the next index after max + let nextIndex = 0; + while (usedIndices.has(nextIndex)) { + nextIndex++; + } + + const maxUsedIndex = Math.max(...usedIndices); + const hasGaps = nextIndex < maxUsedIndex; + + console.log(`โœ… Next unused address index: ${nextIndex} (max used: ${maxUsedIndex}, ${successfulDerivations}/${walletAddresses.length} addresses processed${hasGaps ? ', gaps detected' : ''})`); + return nextIndex; + } catch (error) { + console.error('โŒ Failed to find next unused address index:', error); + console.warn('โš ๏ธ Using fallback index 0'); + return 0; + } +} + +// ============================================================================ +// Change Address Generation +// ============================================================================ + +/** + * Generate a change address using the wallet's derivation path. + * Change addresses use chain 1 (internal/change chain) per BIP84. + * + * @param mnemonic - The wallet mnemonic + * @param changeIndex - The index on the change chain (default: 0) + * @returns The generated change address (P2WPKH/bc1q format) + */ +export async function generateChangeAddress( + mnemonic: string, + changeIndex: number = 0 +): Promise { + try { + console.log('๐Ÿ”ง Generating change address for index:', changeIndex); + + // Load required modules + const bip32Module = await loadBip32Module(); + if (!bip32Module || !bip32Module.BIP32Factory) { + throw new Error('BIP32 module or BIP32Factory not available'); + } + + const bip39 = require('bip39'); + const ecc = (global as any).ecc; + const bip32Instance = bip32Module.BIP32Factory(ecc); + + // Derive key for change address (chain 1) + const seed = await bip39.mnemonicToSeed(mnemonic); + const root = bip32Instance.fromSeed(seed); + const child = root.derivePath(`m/84'/0'/0'/1/${changeIndex}`); + + if (!child.publicKey) { + throw new Error('Failed to derive public key for change address'); + } + + // Generate P2WPKH address + const bech32 = await import('bech32'); + const { sha256 } = await import('@noble/hashes/sha256'); + const { ripemd160 } = await import('@noble/hashes/ripemd160'); + + const sha256Hash = sha256(child.publicKey); + const hash160 = ripemd160(sha256Hash); + const words = bech32.bech32.toWords(hash160); + const address = bech32.bech32.encode('bc', [0, ...words]); + + console.log('โœ… Generated change address:', address); + return address; + } catch (error) { + console.error('โŒ Failed to generate change address:', error); + throw error; + } +} + +/** + * Generate a cancellation address for RBF transaction cancellation. + * Uses the next unused address index to avoid address reuse. + * + * @param mnemonic - The wallet mnemonic + * @param walletAddresses - Array of known wallet addresses + * @returns The generated address for receiving cancelled funds + */ +export async function generateCancellationAddress( + mnemonic: string, + walletAddresses: string[] +): Promise { + try { + console.log('๐Ÿ”ง Generating cancellation address...'); + + // Find the next unused address index instead of using array length + const addressIndex = await findNextUnusedAddressIndex(mnemonic, walletAddresses); + + // Load required modules + const bip32Module = await loadBip32Module(); + if (!bip32Module || !bip32Module.BIP32Factory) { + throw new Error('BIP32 module or BIP32Factory not available'); + } + + const bip39 = require('bip39'); + const ecc = (global as any).ecc; + const bip32Instance = bip32Module.BIP32Factory(ecc); + + // Derive key for cancellation address (external chain) + const seed = await bip39.mnemonicToSeed(mnemonic); + const root = bip32Instance.fromSeed(seed); + const child = root.derivePath(`m/84'/0'/0'/0/${addressIndex}`); + + if (!child.publicKey) { + throw new Error('Failed to derive public key for cancellation address'); + } + + // Generate P2WPKH address + const bech32 = await import('bech32'); + const { sha256 } = await import('@noble/hashes/sha256'); + const { ripemd160 } = await import('@noble/hashes/ripemd160'); + + const sha256Hash = sha256(child.publicKey); + const hash160 = ripemd160(sha256Hash); + const words = bech32.bech32.toWords(hash160); + const address = bech32.bech32.encode('bc', [0, ...words]); + + console.log(`โœ… Generated cancellation address: ${address} (index: ${addressIndex})`); + return address; + } catch (error) { + console.error('โŒ Failed to generate cancellation address:', error); + throw error; + } +} diff --git a/services/rbf-service.ts b/services/rbf-service.ts index 9e9ac9c..f315508 100644 --- a/services/rbf-service.ts +++ b/services/rbf-service.ts @@ -7,6 +7,16 @@ import { UTXO } from '@/types/wallet'; import { loadBip32Module } from './bip32-loader'; import { ensureECC } from './bitcoin-service'; import { esploraGet } from './esplora-service'; +import { + validateECCLibrary, + validateECCLibraryFull, + estimateTransactionSize, + deriveAddressIndexAndChainFromAddress, + deriveAddressIndexFromAddress, + findNextUnusedAddressIndex, + generateCancellationAddress, + clearAddressIndexCache, +} from './ecc-utils'; // Use centralized bip32 loader let bip32Module: unknown = null; @@ -25,30 +35,7 @@ const NON_RBF_SEQUENCE = 0xFFFFFFFF; */ const MIN_RBF_FEE_INCREASE_RATE = 0.1; -/** - * Validates the given ECC library by checking basic functionality. - * Throws an error if validation fails. - */ -interface ECCLibrary { - isPrivate(privKey: Uint8Array): boolean; - pointFromScalar(scalar: Uint8Array, compressed: boolean): Uint8Array | null; -} - -function validateECCLibrary(ecc: ECCLibrary): void { - const testPrivateKey = new Uint8Array(32); - testPrivateKey[31] = 1; // Set to 1 to ensure it's a valid private key - - // Test private key validation - if (!ecc.isPrivate(testPrivateKey)) { - throw new Error('ECC private key validation failed'); - } - - // Test point generation - const publicKey = ecc.pointFromScalar(testPrivateKey, true); - if (!publicKey || publicKey.length !== 33) { - throw new Error('ECC point generation failed'); - } -} +// validateECCLibrary is imported from ecc-utils.ts export interface EsploraTransactionVin { txid?: string; @@ -392,64 +379,22 @@ export async function createReplacementTransaction( if (!ecc) { throw new Error('ECC library not available'); } - - // Validate ECC library before using it + + // Validate ECC library and initialize bitcoinjs-lib using shared utilities console.log('๐Ÿ”ง Validating ECC library before bitcoinjs-lib initialization...'); - - // Validate ECC library using reusable function + try { - validateECCLibrary(ecc); + validateECCLibraryFull(ecc); + console.log('โœ… ECC library validation passed'); } catch (eccError) { console.error('โŒ ECC library validation failed:', eccError); throw new Error(`ECC library invalid: ${eccError instanceof Error ? eccError.message : 'Unknown error'}`); } - + // Initialize bitcoinjs-lib with ECC try { console.log('๐Ÿ”ง Initializing bitcoinjs-lib with ECC...'); bitcoin.initEccLib(ecc); - - // Verify the initialization worked by checking if ECC is properly set - console.log('๐Ÿ”ง Verifying ECC initialization...'); - - // Add a small delay to ensure ECC is fully initialized - await new Promise(resolve => setTimeout(resolve, 200)); - - // In bitcoinjs-lib 7.0.0, ECPair was removed and is no longer exported - // The library works with PSBT (Partially Signed Bitcoin Transactions) instead - // We just need to verify that our ECC library works correctly - console.log('๐Ÿ”ง Testing ECC library functionality (bitcoinjs-lib 7.x compatible)...'); - try { - // Define test private key for verification - const testPrivateKey = new Uint8Array(32); - testPrivateKey[31] = 1; // Set to 1 to ensure it's a valid private key - - // Test if our ECC library can create a public key - const publicKey = ecc.pointFromScalar(testPrivateKey, true); - if (!publicKey || publicKey.length !== 33) { - throw new Error('ECC library cannot create valid public keys'); - } - - // Test signing - const testHash = new Uint8Array(32); - testHash.fill(0xaa); - const signature = ecc.sign(testHash, testPrivateKey); - if (!signature || signature.length === 0) { - throw new Error('ECC library cannot create signatures'); - } - - // Test verification - const isValid = ecc.verify(testHash, publicKey, signature); - if (!isValid) { - throw new Error('ECC library signature verification failed'); - } - - console.log('โœ… ECC library verification successful - ready for RBF transaction'); - } catch (verifyError) { - console.error('โŒ ECC verification failed:', verifyError); - throw new Error(`ECC library not working properly: ${verifyError instanceof Error ? verifyError.message : 'Unknown error'}`); - } - console.log('โœ… bitcoinjs-lib initialized with ECC successfully'); } catch (initError) { console.error('โŒ Failed to initialize bitcoinjs-lib with ECC:', initError); @@ -929,64 +874,22 @@ async function createCancellationTransaction( if (!ecc) { throw new Error('ECC library not available'); } - - // Validate ECC library before using it + + // Validate ECC library and initialize bitcoinjs-lib using shared utilities console.log('๐Ÿ”ง Validating ECC library before bitcoinjs-lib initialization...'); - - // Validate ECC library using reusable function + try { - validateECCLibrary(ecc); + validateECCLibraryFull(ecc); + console.log('โœ… ECC library validation passed'); } catch (eccError) { console.error('โŒ ECC library validation failed:', eccError); throw new Error(`ECC library invalid: ${eccError instanceof Error ? eccError.message : 'Unknown error'}`); } - + // Initialize bitcoinjs-lib with ECC try { console.log('๐Ÿ”ง Initializing bitcoinjs-lib with ECC...'); bitcoin.initEccLib(ecc); - - // Verify the initialization worked by checking if ECC is properly set - console.log('๐Ÿ”ง Verifying ECC initialization...'); - - // Add a small delay to ensure ECC is fully initialized - await new Promise(resolve => setTimeout(resolve, 200)); - - // In bitcoinjs-lib 7.0.0, ECPair was removed and is no longer exported - // The library works with PSBT (Partially Signed Bitcoin Transactions) instead - // We just need to verify that our ECC library works correctly - console.log('๐Ÿ”ง Testing ECC library functionality (bitcoinjs-lib 7.x compatible)...'); - try { - // Define test private key for verification - const testPrivateKey = new Uint8Array(32); - testPrivateKey[31] = 1; // Set to 1 to ensure it's a valid private key - - // Test if our ECC library can create a public key - const publicKey = ecc.pointFromScalar(testPrivateKey, true); - if (!publicKey || publicKey.length !== 33) { - throw new Error('ECC library cannot create valid public keys'); - } - - // Test signing - const testHash = new Uint8Array(32); - testHash.fill(0xaa); - const signature = ecc.sign(testHash, testPrivateKey); - if (!signature || signature.length === 0) { - throw new Error('ECC library cannot create signatures'); - } - - // Test verification - const isValid = ecc.verify(testHash, publicKey, signature); - if (!isValid) { - throw new Error('ECC library signature verification failed'); - } - - console.log('โœ… ECC library verification successful - ready for RBF transaction'); - } catch (verifyError) { - console.error('โŒ ECC verification failed:', verifyError); - throw new Error(`ECC library not working properly: ${verifyError instanceof Error ? verifyError.message : 'Unknown error'}`); - } - console.log('โœ… bitcoinjs-lib initialized with ECC successfully'); } catch (initError) { console.error('โŒ Failed to initialize bitcoinjs-lib with ECC:', initError); @@ -1170,406 +1073,6 @@ async function createCancellationTransaction( } } -// Cache for address-to-index mappings to avoid redundant derivations -const addressIndexCache = new Map(); - -/** - * Clear the address index cache - * Call this when switching wallets or when cache becomes stale - */ -export function clearAddressIndexCache(): void { - addressIndexCache.clear(); - console.log(`๐Ÿงน Cleared address index cache`); -} - -/** - * Derive the BIP32 address index from an address by testing derivation paths - * This is necessary because walletAddresses array order doesn't correspond to BIP32 indices - * - * Performance improvements: - * 1. Uses caching to avoid redundant derivations - * 2. Implements optimized linear search with batching - * 3. Removes arbitrary 1000 address limit - * 4. Optimizes imports to avoid repeated dynamic imports - * 5. Adds small delays between batches to prevent UI blocking - */ -/** - * Derive the BIP32 address index and chain from an address by testing derivation paths - * Returns { index, chain } where chain is 0 for external/receiving, 1 for internal/change - */ -export async function deriveAddressIndexAndChainFromAddress(mnemonic: string, targetAddress: string): Promise<{ index: number; chain: number }> { - try { - // Check cache first (need to cache both index and chain) - const cacheKey = `${targetAddress}_full`; - if (addressIndexCache.has(cacheKey)) { - const cached = addressIndexCache.get(cacheKey)!; - console.log(`โœ… Found cached BIP32 index ${cached} for address: ${targetAddress}`); - // Parse the cached value (format: "chain:index") - const [chain, index] = cached.toString().split(':').map(Number); - return { index, chain }; - } - - console.log(`๐Ÿ” Deriving BIP32 index and chain for address: ${targetAddress}`); - - // Ensure bip32 module is loaded - if (!bip32Module) { - bip32Module = await loadBip32Module(); - } - - if (!bip32Module || !(bip32Module as any).BIP32Factory) { - throw new Error('BIP32 module or BIP32Factory not available'); - } - const bip32 = bip32Module as any; - const bip39 = require('bip39'); - const ecc = (global as any).ecc; - const bip32Instance = bip32.BIP32Factory(ecc); - const bech32 = await import('bech32'); - const { sha256 } = await import('@noble/hashes/sha256'); - const { ripemd160 } = await import('@noble/hashes/ripemd160'); - - const seed = await bip39.mnemonicToSeed(mnemonic); - const root = bip32Instance.fromSeed(seed); - - // Check both external (chain 0) and internal/change (chain 1) chains - for (const chain of [0, 1]) { - const chainNode = root.derivePath(`m/84'/0'/0'/${chain}`); - - // Use optimized linear search with batching to prevent UI blocking - let foundIndex = -1; - const batchSize = 50; - const batchDelay = 5; - let currentIndex = 0; - const maxSearchRange = 10000; - - while (foundIndex === -1 && currentIndex < maxSearchRange) { - const endIndex = Math.min(currentIndex + batchSize, maxSearchRange); - - for (let i = currentIndex; i < endIndex; i++) { - try { - const child = chainNode.derive(i); - if (!child.publicKey) continue; - - // Generate P2WPKH address - const sha256Hash = sha256(child.publicKey); - const hash160 = ripemd160(sha256Hash); - const words = bech32.bech32.toWords(hash160); - const address = bech32.bech32.encode('bc', [0, ...words]); - - if (address === targetAddress) { - foundIndex = i; - break; - } - } catch (error) { - console.warn(`โš ๏ธ Failed to derive address at chain ${chain}, index ${i}:`, error); - continue; - } - } - - currentIndex = endIndex; - - // Small delay between batches to prevent UI blocking - if (foundIndex === -1 && currentIndex < maxSearchRange) { - await new Promise(resolve => setTimeout(resolve, batchDelay)); - } - } - - if (foundIndex !== -1) { - // Cache the result - addressIndexCache.set(cacheKey, `${chain}:${foundIndex}`); - console.log(`โœ… Found BIP32 chain ${chain}, index ${foundIndex} for address: ${targetAddress}`); - return { index: foundIndex, chain }; - } - } - - throw new Error(`Could not find BIP32 index for address: ${targetAddress} (searched both chains up to index ${10000})`); - } catch (error) { - console.error(`โŒ Failed to derive address index and chain:`, error); - throw error; - } -} - -/** - * Derive the BIP32 address index from an address by testing derivation paths - * Legacy function - prefer deriveAddressIndexAndChainFromAddress for new code - */ -export async function deriveAddressIndexFromAddress(mnemonic: string, targetAddress: string): Promise { - try { - // Check cache first - if (addressIndexCache.has(targetAddress)) { - const cachedValue = addressIndexCache.get(targetAddress)!; - console.log(`โœ… Found cached BIP32 index ${cachedValue} for address: ${targetAddress}`); - // If cached value is a string, parse the index from it - const cachedIndex = typeof cachedValue === 'string' - ? parseInt(cachedValue.split(':')[1], 10) - : cachedValue; - return cachedIndex; - } - - console.log(`๐Ÿ” Deriving BIP32 index for address: ${targetAddress}`); - - // Ensure bip32 module is loaded - if (!bip32Module) { - bip32Module = await loadBip32Module(); - } - - if (!bip32Module || !(bip32Module as any).BIP32Factory) { - throw new Error('BIP32 module or BIP32Factory not available'); - } - const bip32 = bip32Module as any; - const bip39 = require('bip39'); - const ecc = (global as any).ecc; - const bip32Instance = bip32.BIP32Factory(ecc); - const bech32 = await import('bech32'); - const { sha256 } = await import('@noble/hashes/sha256'); - const { ripemd160 } = await import('@noble/hashes/ripemd160'); - - const seed = await bip39.mnemonicToSeed(mnemonic); - const root = bip32Instance.fromSeed(seed); - const externalChain = root.derivePath(`m/84'/0'/0'/0`); - - // Use optimized linear search with batching to prevent UI blocking - let foundIndex = -1; - const batchSize = 50; // Smaller batches for better responsiveness - const batchDelay = 5; // 5ms delay between batches - let currentIndex = 0; - const maxSearchRange = 10000; // Reasonable limit for most wallets - - while (foundIndex === -1 && currentIndex < maxSearchRange) { - const endIndex = Math.min(currentIndex + batchSize, maxSearchRange); - - for (let i = currentIndex; i < endIndex; i++) { - try { - const child = externalChain.derive(i); - if (!child.publicKey) continue; - - // Generate P2WPKH address - const sha256Hash = sha256(child.publicKey); - const hash160 = ripemd160(sha256Hash); - const words = bech32.bech32.toWords(hash160); - const address = bech32.bech32.encode('bc', [0, ...words]); - - if (address === targetAddress) { - foundIndex = i; - break; - } - } catch (error) { - console.warn(`โš ๏ธ Failed to derive address at index ${i}:`, error); - continue; - } - } - - currentIndex = endIndex; - - // Small delay between batches to prevent UI blocking - if (foundIndex === -1 && currentIndex < maxSearchRange) { - await new Promise(resolve => setTimeout(resolve, batchDelay)); - } - } - - // If not found in the initial range, expand the search with larger delays - if (foundIndex === -1) { - console.log(`๐Ÿ” Address not found in initial range, expanding search...`); - - // Expand search range with larger delays for better responsiveness - let expandedHigh = maxSearchRange * 2; - const expandedBatchSize = 25; // Smaller batches for expanded search - const expandedBatchDelay = 10; // Longer delays for expanded search - - while (foundIndex === -1 && expandedHigh <= 100000) { // Cap at 100k for safety - console.log(`๐Ÿ” Searching range ${currentIndex} to ${expandedHigh}...`); - - for (let i = currentIndex; i < expandedHigh; i += expandedBatchSize) { - const endIndex = Math.min(i + expandedBatchSize, expandedHigh); - - for (let j = i; j < endIndex; j++) { - try { - const child = externalChain.derive(j); - if (!child.publicKey) continue; - - const sha256Hash = sha256(child.publicKey); - const hash160 = ripemd160(sha256Hash); - const words = bech32.bech32.toWords(hash160); - const address = bech32.bech32.encode('bc', [0, ...words]); - - if (address === targetAddress) { - foundIndex = j; - break; - } - } catch (error) { - console.warn(`โš ๏ธ Failed to derive address at index ${j}:`, error); - continue; - } - } - - if (foundIndex !== -1) break; - - // Delay between batches in expanded search - if (endIndex < expandedHigh) { - await new Promise(resolve => setTimeout(resolve, expandedBatchDelay)); - } - } - - currentIndex = expandedHigh; - expandedHigh *= 2; - } - } - - if (foundIndex === -1) { - throw new Error(`Could not find BIP32 index for address: ${targetAddress} (searched up to index ${currentIndex})`); - } - - // Cache the result - addressIndexCache.set(targetAddress, foundIndex); - console.log(`โœ… Found BIP32 index ${foundIndex} for address: ${targetAddress}`); - return foundIndex; - } catch (error) { - console.error(`โŒ Failed to derive address index:`, error); - throw error; - } -} - -/** - * Find the next unused address index for generating new addresses - * This ensures we don't reuse addresses and follow proper BIP32 gap limit - * - * Fix: Instead of assuming sequential usage (max + 1), we now find the actual - * next unused index by checking for gaps in the address sequence. This prevents - * address reuse when there are gaps in the wallet's address usage. - * - * Performance improvements: - * 1. Uses cached results from deriveAddressIndexFromAddress - * 2. Processes addresses sequentially to prevent app freezing - * 3. Provides better error handling and fallback logic - * 4. Handles non-sequential address usage properly - * 5. Uses batching with small delays to prevent UI blocking - */ -export async function findNextUnusedAddressIndex(mnemonic: string, walletAddresses: string[]): Promise { - try { - console.log(`๐Ÿ” Finding next unused address index for ${walletAddresses.length} addresses...`); - - // Process addresses sequentially in small batches to prevent app freezing - const usedIndices = new Set(); - let successfulDerivations = 0; - const batchSize = 5; // Process 5 addresses at a time - const batchDelay = 10; // 10ms delay between batches - - for (let i = 0; i < walletAddresses.length; i += batchSize) { - const batch = walletAddresses.slice(i, i + batchSize); - - // Process batch sequentially to avoid overwhelming the system - for (const address of batch) { - try { - const index = await deriveAddressIndexFromAddress(mnemonic, address); - usedIndices.add(index); - successfulDerivations++; - } catch (error) { - console.warn(`โš ๏ธ Could not derive index for address ${address}:`, error); - } - } - - // Small delay between batches to prevent UI blocking - if (i + batchSize < walletAddresses.length) { - await new Promise(resolve => setTimeout(resolve, batchDelay)); - } - } - - // If we couldn't derive any addresses, fall back to 0 - if (successfulDerivations === 0) { - console.warn(`โš ๏ธ Could not derive any address indices, using fallback index 0`); - return 0; - } - - // Find the next unused index by looking for the first gap or the next index after max - let nextIndex = 0; - while (usedIndices.has(nextIndex)) { - nextIndex++; - } - - const maxUsedIndex = Math.max(...usedIndices); - const hasGaps = nextIndex < maxUsedIndex; - - console.log(`โœ… Next unused address index: ${nextIndex} (max used: ${maxUsedIndex}, ${successfulDerivations}/${walletAddresses.length} addresses processed${hasGaps ? ', gaps detected' : ''})`); - return nextIndex; - } catch (error) { - console.error(`โŒ Failed to find next unused address index:`, error); - // Fallback to 0 if we can't determine the next index - console.warn(`โš ๏ธ Using fallback index 0`); - return 0; - } -} - -/** - * Generate a cancellation address using the wallet's derivation path - */ -async function generateCancellationAddress(mnemonic: string, walletAddresses: string[]): Promise { - try { - console.log(`๐Ÿ”ง Generating cancellation address...`); - - // Find the next unused address index instead of using array length - const addressIndex = await findNextUnusedAddressIndex(mnemonic, walletAddresses); - - // Ensure bip32 module is loaded - if (!bip32Module) { - bip32Module = await loadBip32Module(); - } - - if (!bip32Module || !(bip32Module as any).BIP32Factory) { - throw new Error('BIP32 module or BIP32Factory not available'); - } - const bip32 = bip32Module as any; - const bip39 = require('bip39'); - const ecc = (global as any).ecc; - const bip32Instance = bip32.BIP32Factory(ecc); - - // Derive private key for cancellation address - const seed = await bip39.mnemonicToSeed(mnemonic); - const root = bip32Instance.fromSeed(seed); - const child = root.derivePath(`m/84'/0'/0'/0/${addressIndex}`); - - if (!child.publicKey) { - throw new Error('Failed to derive public key for cancellation address'); - } - - // Generate P2WPKH address - const bech32 = await import('bech32'); - const { sha256 } = await import('@noble/hashes/sha256'); - const { ripemd160 } = await import('@noble/hashes/ripemd160'); - - const sha256Hash = sha256(child.publicKey); - const hash160 = ripemd160(sha256Hash); - const words = bech32.bech32.toWords(hash160); - const address = bech32.bech32.encode('bc', [0, ...words]); - - console.log(`โœ… Generated cancellation address: ${address} (index: ${addressIndex})`); - return address; - } catch (error) { - console.error(`โŒ Failed to generate cancellation address:`, error); - throw error; - } -} - -/** - * Estimate transaction size in bytes - * - * Assumes all inputs/outputs are SegWit v0 P2WPKH type. - */ - -// Standard SegWit v0 P2WPKH input size: 68 bytes -// Derived from: 32 (txid) + 4 (vout) + 1 (scriptSig length) + 4 (sequence) + -// 1 (witness item count) + 72 (signature) + 33 (pubkey), weighted by witness discount -const P2WPKH_INPUT_SIZE = 68; -// Standard SegWit v0 P2WPKH output size: 34 bytes -// Derived from: 8 (value) + 1 (scriptPubKey length) + 25 (scriptPubKey for P2WPKH) -const P2WPKH_OUTPUT_SIZE = 34; - -function estimateTransactionSize(inputCount: number, outputCount: number): number { - // Base transaction size: 10 bytes - // (version [4 bytes] + input count [1 byte] + output count [1 byte] + locktime [4 bytes]) - let size = 10; - - // Add inputs and outputs (SegWit v0 P2WPKH only) - size += inputCount * P2WPKH_INPUT_SIZE; - size += outputCount * P2WPKH_OUTPUT_SIZE; - - return size; -} +// Address derivation utilities and estimateTransactionSize are imported from ecc-utils.ts +// Re-export clearAddressIndexCache for backward compatibility +export { clearAddressIndexCache };