Util.php
1 <?php 2 3 namespace BitcoindRPC\Util; 4 5 use BitcoindRPC\RpcClient; 6 use BitcoindRPC\RpcResponse; 7 8 class Util 9 { 10 private RpcClient $client; 11 12 public function __construct(RpcClient $client) 13 { 14 $this->client = $client; 15 } 16 17 public function createMultisig( 18 int $nrequired, 19 array $keys, 20 string $addressType = "legacy" 21 ): UtilResult { 22 if ($nrequired <= 0 || $nrequired > count($keys)) { 23 return new UtilResult(new RpcResponse( 24 data: null, 25 error: "nrequired must be between 1 and number of keys", 26 statusCode: 400 27 )); 28 } 29 30 if (!in_array($addressType, ["legacy", "p2sh-segwit", "bech32"])) { 31 return new UtilResult(new RpcResponse( 32 data: null, 33 error: "address_type must be 'legacy', 'p2sh-segwit', or 'bech32'", 34 statusCode: 400 35 )); 36 } 37 38 $response = $this->client->call('createmultisig', [$nrequired, $keys, $addressType]); 39 return new UtilResult($response); 40 } 41 42 public function deriveAddresses(string $descriptor, ?array $range = null): UtilResult 43 { 44 $params = [$descriptor]; 45 if ($range !== null) { 46 if (count($range) !== 2) { 47 return new UtilResult(new RpcResponse( 48 data: null, 49 error: "range must be an array of two integers [start, end]", 50 statusCode: 400 51 )); 52 } 53 $params[] = $range; 54 } 55 56 $response = $this->client->call('deriveaddresses', $params); 57 return new UtilResult($response); 58 } 59 60 public function estimateSmartFee( 61 int $confTarget, 62 string $estimateMode = "CONSERVATIVE", 63 ?float $threshold = null 64 ): UtilResult { 65 if ($confTarget < 1 || $confTarget > 1008) { 66 return new UtilResult(new RpcResponse( 67 data: null, 68 error: "conf_target must be between 1 and 1008", 69 statusCode: 400 70 )); 71 } 72 73 if (!in_array($estimateMode, ["UNSET", "ECONOMICAL", "CONSERVATIVE"])) { 74 return new UtilResult(new RpcResponse( 75 data: null, 76 error: "estimate_mode must be 'UNSET', 'ECONOMICAL', or 'CONSERVATIVE'", 77 statusCode: 400 78 )); 79 } 80 81 $params = [$confTarget, $estimateMode]; 82 if ($threshold !== null) { 83 $params[] = $threshold; 84 } 85 86 $response = $this->client->call('estimatesmartfee', $params); 87 return new UtilResult($response); 88 } 89 90 public function getDescriptorInfo(string $descriptor): UtilResult 91 { 92 $response = $this->client->call('getdescriptorinfo', [$descriptor]); 93 return new UtilResult($response); 94 } 95 96 public function getIndexInfo(?string $descriptor = null): UtilResult 97 { 98 $params = []; 99 if ($descriptor !== null) { 100 $params[] = $descriptor; 101 } 102 103 $response = $this->client->call('getindexinfo', $params); 104 return new UtilResult($response); 105 } 106 107 public function signMessageWithPrivKey(string $privkey, string $message): UtilResult 108 { 109 $response = $this->client->call('signmessagewithprivkey', [$privkey, $message]); 110 return new UtilResult($response); 111 } 112 113 public function validateAddress(string $address): UtilResult 114 { 115 $response = $this->client->call('validateaddress', [$address]); 116 return new UtilResult($response); 117 } 118 119 public function verifyMessage(string $address, string $signature, string $message): UtilResult 120 { 121 $response = $this->client->call('verifymessage', [$address, $signature, $message]); 122 return new UtilResult($response); 123 } 124 125 // Utility methods 126 public function getStructuredAddressInfo(string $address): array 127 { 128 $result = $this->validateAddress($address); 129 130 if (!$result->isSuccess()) { 131 return [ 132 'is_valid' => false, 133 'error' => $result->getError() 134 ]; 135 } 136 137 $data = $result->getData(); 138 139 return [ 140 'is_valid' => $data['isvalid'] ?? false, 141 'address' => $data['address'] ?? null, 142 'script_pub_key' => $data['scriptPubKey'] ?? null, 143 'is_script' => $data['isscript'] ?? false, 144 'is_witness' => $data['iswitness'] ?? false, 145 'witness_version' => $data['witness_version'] ?? null, 146 'witness_program' => $data['witness_program'] ?? null, 147 'pubkey' => $data['pubkey'] ?? null, 148 'is_compressed' => $data['iscompressed'] ?? null, 149 'account' => $data['account'] ?? null, 150 'timestamp' => $data['timestamp'] ?? null, 151 'hdkeypath' => $data['hdkeypath'] ?? null, 152 'hdseedid' => $data['hdseedid'] ?? null, 153 'hdmasterfingerprint' => $data['hdmasterfingerprint'] ?? null, 154 'labels' => $data['labels'] ?? [] 155 ]; 156 } 157 158 public function getFeeEstimation( 159 int $confTarget, 160 string $estimateMode = "CONSERVATIVE" 161 ): array { 162 $result = $this->estimateSmartFee($confTarget, $estimateMode); 163 164 if (!$result->isSuccess()) { 165 return [ 166 'feerate' => null, 167 'blocks' => null, 168 'errors' => ["Failed to estimate fee: " . $result->getError()] 169 ]; 170 } 171 172 $data = $result->getData(); 173 174 if (isset($data['errors']) && !empty($data['errors'])) { 175 return [ 176 'feerate' => null, 177 'blocks' => null, 178 'errors' => $data['errors'] 179 ]; 180 } 181 182 return [ 183 'feerate' => $data['feerate'] ?? null, 184 'blocks' => $data['blocks'] ?? null, 185 'errors' => $data['errors'] ?? [] 186 ]; 187 } 188 189 public function createMultisigWithDescriptor( 190 int $nrequired, 191 array $keys, 192 string $addressType = "legacy" 193 ): array { 194 // Create multisig 195 $multisigResult = $this->createMultisig($nrequired, $keys, $addressType); 196 197 if (!$multisigResult->isSuccess()) { 198 return [ 199 'multisig' => $multisigResult, 200 'descriptor' => new UtilResult(new RpcResponse( 201 data: null, 202 error: "Multisig creation failed", 203 statusCode: 400 204 )) 205 ]; 206 } 207 208 // Get descriptor info 209 $multisigData = $multisigResult->getData(); 210 $descriptor = $multisigData['descriptor'] ?? ''; 211 $descriptorResult = $this->getDescriptorInfo($descriptor); 212 213 return [ 214 'multisig' => $multisigResult, 215 'descriptor' => $descriptorResult 216 ]; 217 } 218 219 public function signAndVerifyMessage( 220 string $privkey, 221 string $address, 222 string $message 223 ): array { 224 // Sign the message 225 $signResult = $this->signMessageWithPrivKey($privkey, $message); 226 227 if (!$signResult->isSuccess()) { 228 return [ 229 'sign' => $signResult, 230 'verify' => new UtilResult(new RpcResponse( 231 data: null, 232 error: "Signing failed", 233 statusCode: 400 234 )) 235 ]; 236 } 237 238 // Verify the signature 239 $signature = $signResult->getData(); 240 $verifyResult = $this->verifyMessage($address, $signature, $message); 241 242 return [ 243 'sign' => $signResult, 244 'verify' => $verifyResult 245 ]; 246 } 247 248 public function batchValidateAddresses(array $addresses): array 249 { 250 $results = []; 251 252 foreach ($addresses as $address) { 253 $results[$address] = $this->getStructuredAddressInfo($address); 254 } 255 256 return $results; 257 } 258 259 public function getOptimalFeeRates(array $confTargets = null): array 260 { 261 if ($confTargets === null) { 262 $confTargets = [1, 3, 6, 12, 24]; 263 } 264 265 $feeEstimations = []; 266 267 foreach ($confTargets as $target) { 268 $feeEstimations[$target] = $this->getFeeEstimation($target); 269 } 270 271 return $feeEstimations; 272 } 273 274 public function isSegwitAddress(string $address): bool 275 { 276 $addressInfo = $this->getStructuredAddressInfo($address); 277 return $addressInfo['is_witness'] ?? false; 278 } 279 280 public function getAddressType(string $address): string 281 { 282 $addressInfo = $this->getStructuredAddressInfo($address); 283 284 if (!$addressInfo['is_valid']) { 285 return 'invalid'; 286 } 287 288 if ($addressInfo['is_witness']) { 289 if ($addressInfo['witness_version'] === 0) { 290 return 'bech32'; 291 } else { 292 return 'witness_v' . $addressInfo['witness_version']; 293 } 294 } elseif ($addressInfo['is_script']) { 295 return 'p2sh'; 296 } else { 297 return 'legacy'; 298 } 299 } 300 301 public function deriveAddressesFromDescriptor(string $descriptor, int $start = 0, int $end = 10): array 302 { 303 $range = [$start, $end]; 304 $result = $this->deriveAddresses($descriptor, $range); 305 306 if (!$result->isSuccess()) { 307 return [ 308 'success' => false, 309 'error' => $result->getError(), 310 'addresses' => [] 311 ]; 312 } 313 314 $addresses = $result->getData(); 315 316 return [ 317 'success' => true, 318 'descriptor' => $descriptor, 319 'range' => $range, 320 'address_count' => is_array($addresses) ? count($addresses) : 0, 321 'addresses' => is_array($addresses) ? $addresses : [] 322 ]; 323 } 324 325 public function getDescriptorDetails(string $descriptor): array 326 { 327 $result = $this->getDescriptorInfo($descriptor); 328 329 if (!$result->isSuccess()) { 330 return [ 331 'success' => false, 332 'error' => $result->getError(), 333 'details' => null 334 ]; 335 } 336 337 $data = $result->getData(); 338 339 return [ 340 'success' => true, 341 'descriptor' => $descriptor, 342 'details' => [ 343 'descriptor' => $data['descriptor'] ?? null, 344 'checksum' => $data['checksum'] ?? null, 345 'is_range' => $data['isrange'] ?? false, 346 'is_solvable' => $data['issolvable'] ?? false, 347 'has_private_keys' => $data['hasprivatekeys'] ?? false 348 ] 349 ]; 350 } 351 352 public function getIndexStatus(): array 353 { 354 $result = $this->getIndexInfo(); 355 356 if (!$result->isSuccess()) { 357 return [ 358 'success' => false, 359 'error' => $result->getError(), 360 'indexes' => [] 361 ]; 362 } 363 364 $data = $result->getData(); 365 366 $indexes = []; 367 foreach ($data as $name => $info) { 368 $indexes[$name] = [ 369 'name' => $name, 370 'synced' => $info['synced'] ?? false, 371 'best_block_height' => $info['best_block_height'] ?? null 372 ]; 373 } 374 375 return [ 376 'success' => true, 377 'index_count' => count($indexes), 378 'indexes' => $indexes 379 ]; 380 } 381 382 public function create2Of3Multisig(array $pubkeys, string $addressType = "legacy"): array 383 { 384 return $this->createMultisigWithDescriptor(2, $pubkeys, $addressType); 385 } 386 387 public function create3Of5Multisig(array $pubkeys, string $addressType = "legacy"): array 388 { 389 return $this->createMultisigWithDescriptor(3, $pubkeys, $addressType); 390 } 391 392 public function validateAndGetAddressType(string $address): array 393 { 394 $addressInfo = $this->getStructuredAddressInfo($address); 395 396 if (!$addressInfo['is_valid']) { 397 return [ 398 'valid' => false, 399 'type' => 'invalid', 400 'error' => $addressInfo['error'] ?? 'Invalid address' 401 ]; 402 } 403 404 $type = $this->getAddressType($address); 405 406 return [ 407 'valid' => true, 408 'type' => $type, 409 'is_segwit' => $addressInfo['is_witness'], 410 'is_script' => $addressInfo['is_script'], 411 'address' => $addressInfo['address'] 412 ]; 413 } 414 415 public function getMessageSigningStatus(string $address, string $signature, string $message): array 416 { 417 $verifyResult = $this->verifyMessage($address, $signature, $message); 418 419 return [ 420 'verified' => $verifyResult->isSuccess() && $verifyResult->getData() === true, 421 'address' => $address, 422 'message' => $message, 423 'signature_valid' => $verifyResult->getData(), 424 'error' => $verifyResult->isSuccess() ? null : $verifyResult->getError() 425 ]; 426 } 427 428 public function getFeeRecommendations(): array 429 { 430 $feeRates = $this->getOptimalFeeRates(); 431 432 $recommendations = []; 433 foreach ($feeRates as $blocks => $estimation) { 434 if ($estimation['feerate'] !== null) { 435 $recommendations[$blocks] = [ 436 'blocks' => $blocks, 437 'feerate_btc_kb' => $estimation['feerate'], 438 'feerate_sat_vbyte' => $estimation['feerate'] * 100000, // Convert to sat/vbyte 439 'estimation_quality' => empty($estimation['errors']) ? 'high' : 'low' 440 ]; 441 } 442 } 443 444 return [ 445 'success' => true, 446 'recommendations' => $recommendations, 447 'timestamp' => time() 448 ]; 449 } 450 }