Blockchain.php
1 <?php 2 3 namespace BitcoindRPC\Blockchain; 4 5 use BitcoindRPC\RpcClient; 6 use BitcoindRPC\RpcResponse; 7 8 class Blockchain 9 { 10 private RpcClient $client; 11 12 public function __construct(RpcClient $client) 13 { 14 $this->client = $client; 15 } 16 17 public function getBestBlockHash(): BlockchainResult 18 { 19 $response = $this->client->call('getbestblockhash'); 20 return new BlockchainResult($response); 21 } 22 23 public function getBlock(string $blockHash, int $verbosity = 1): BlockchainResult 24 { 25 if ($verbosity < 0 || $verbosity > 2) { 26 return new BlockchainResult(new RpcResponse( 27 data: null, 28 error: "verbosity must be 0, 1, or 2", 29 statusCode: 400 30 )); 31 } 32 33 $response = $this->client->call('getblock', [$blockHash, $verbosity]); 34 return new BlockchainResult($response); 35 } 36 37 public function getBlockchainInfo(): BlockchainResult 38 { 39 $response = $this->client->call('getblockchaininfo'); 40 return new BlockchainResult($response); 41 } 42 43 public function getBlockCount(): BlockchainResult 44 { 45 $response = $this->client->call('getblockcount'); 46 return new BlockchainResult($response); 47 } 48 49 public function getBlockFilter(string $blockHash, string $filterType = "basic"): BlockchainResult 50 { 51 $response = $this->client->call('getblockfilter', [$blockHash, $filterType]); 52 return new BlockchainResult($response); 53 } 54 55 public function getBlockHash(int $height): BlockchainResult 56 { 57 $response = $this->client->call('getblockhash', [$height]); 58 return new BlockchainResult($response); 59 } 60 61 public function getBlockHeader(string $blockHash, bool $verbose = true): BlockchainResult 62 { 63 $response = $this->client->call('getblockheader', [$blockHash, $verbose]); 64 return new BlockchainResult($response); 65 } 66 67 public function getBlockStats($hashOrHeight, ?array $stats = null): BlockchainResult 68 { 69 $params = [$hashOrHeight]; 70 if ($stats !== null) { 71 $params[] = $stats; 72 } 73 74 $response = $this->client->call('getblockstats', $params); 75 return new BlockchainResult($response); 76 } 77 78 public function getChainTips(): BlockchainResult 79 { 80 $response = $this->client->call('getchaintips'); 81 return new BlockchainResult($response); 82 } 83 84 public function getChainTxStats(?int $nblocks = null, ?string $blockHash = null): BlockchainResult 85 { 86 $params = []; 87 if ($nblocks !== null) { 88 $params[] = $nblocks; 89 if ($blockHash !== null) { 90 $params[] = $blockHash; 91 } 92 } 93 94 $response = $this->client->call('getchaintxstats', $params); 95 return new BlockchainResult($response); 96 } 97 98 public function getDifficulty(): BlockchainResult 99 { 100 $response = $this->client->call('getdifficulty'); 101 return new BlockchainResult($response); 102 } 103 104 public function getMempoolAncestors(string $txid, bool $verbose = false): BlockchainResult 105 { 106 $response = $this->client->call('getmempoolancestors', [$txid, $verbose]); 107 return new BlockchainResult($response); 108 } 109 110 public function getMempoolDescendants(string $txid, bool $verbose = false): BlockchainResult 111 { 112 $response = $this->client->call('getmempooldescendants', [$txid, $verbose]); 113 return new BlockchainResult($response); 114 } 115 116 public function getMempoolEntry(string $txid): BlockchainResult 117 { 118 $response = $this->client->call('getmempoolentry', [$txid]); 119 return new BlockchainResult($response); 120 } 121 122 public function getMempoolInfo(): BlockchainResult 123 { 124 $response = $this->client->call('getmempoolinfo'); 125 return new BlockchainResult($response); 126 } 127 128 public function getRawMempool(bool $verbose = false): BlockchainResult 129 { 130 $response = $this->client->call('getrawmempool', [$verbose]); 131 return new BlockchainResult($response); 132 } 133 134 public function getTxOut(string $txid, int $n, bool $includeMempool = true): BlockchainResult 135 { 136 $response = $this->client->call('gettxout', [$txid, $n, $includeMempool]); 137 return new BlockchainResult($response); 138 } 139 140 public function getTxOutProof(array $txids, ?string $blockHash = null): BlockchainResult 141 { 142 $params = [$txids]; 143 if ($blockHash !== null) { 144 $params[] = $blockHash; 145 } 146 147 $response = $this->client->call('gettxoutproof', $params); 148 return new BlockchainResult($response); 149 } 150 151 public function getTxOutSetInfo(): BlockchainResult 152 { 153 $response = $this->client->call('gettxoutsetinfo'); 154 return new BlockchainResult($response); 155 } 156 157 public function preciousBlock(string $blockHash): BlockchainResult 158 { 159 $response = $this->client->call('preciousblock', [$blockHash]); 160 return new BlockchainResult($response); 161 } 162 163 public function pruneBlockchain(int $height): BlockchainResult 164 { 165 $response = $this->client->call('pruneblockchain', [$height]); 166 return new BlockchainResult($response); 167 } 168 169 public function saveMempool(): BlockchainResult 170 { 171 $response = $this->client->call('savemempool'); 172 return new BlockchainResult($response); 173 } 174 175 public function scanTxOutSet(string $action, array $scanObjects): BlockchainResult 176 { 177 $response = $this->client->call('scantxoutset', [$action, $scanObjects]); 178 return new BlockchainResult($response); 179 } 180 181 public function verifyChain(int $checkLevel = 3, int $nblocks = 6): BlockchainResult 182 { 183 $response = $this->client->call('verifychain', [$checkLevel, $nblocks]); 184 return new BlockchainResult($response); 185 } 186 187 public function verifyTxOutProof(string $proof): BlockchainResult 188 { 189 $response = $this->client->call('verifytxoutproof', [$proof]); 190 return new BlockchainResult($response); 191 } 192 193 // Utility methods 194 public function getBlockByHeight(int $height, int $verbosity = 1): BlockchainResult 195 { 196 // First get the block hash 197 $hashResult = $this->getBlockHash($height); 198 if (!$hashResult->isSuccess()) { 199 return $hashResult; 200 } 201 202 $blockHash = $hashResult->getData(); 203 if ($blockHash === null) { 204 return new BlockchainResult(new RpcResponse( 205 data: null, 206 error: "Failed to get block hash for height $height", 207 statusCode: 404 208 )); 209 } 210 211 // Then get the block data 212 return $this->getBlock($blockHash, $verbosity); 213 } 214 215 public function getLatestBlocks(int $count = 10): array 216 { 217 // Get current block count 218 $countResult = $this->getBlockCount(); 219 if (!$countResult->isSuccess()) { 220 return [ 221 'success' => false, 222 'error' => $countResult->getError(), 223 'blocks' => [] 224 ]; 225 } 226 227 $currentHeight = $countResult->getData(); 228 $blocks = []; 229 230 // Retrieve the last N blocks 231 $startHeight = max($currentHeight - $count + 1, 0); 232 for ($height = $startHeight; $height <= $currentHeight; $height++) { 233 $blockResult = $this->getBlockByHeight($height); 234 if ($blockResult->isSuccess()) { 235 $blocks[] = $blockResult->getData(); 236 } else { 237 return [ 238 'success' => false, 239 'error' => "Failed to get block at height $height: " . $blockResult->getError(), 240 'blocks' => $blocks 241 ]; 242 } 243 } 244 245 return [ 246 'success' => true, 247 'current_height' => $currentHeight, 248 'block_count' => count($blocks), 249 'blocks' => $blocks 250 ]; 251 } 252 253 public function getTransactionInBlock(string $blockHash, int $txIndex): BlockchainResult 254 { 255 // Get block with full transaction data 256 $blockResult = $this->getBlock($blockHash, 2); 257 if (!$blockResult->isSuccess()) { 258 return $blockResult; 259 } 260 261 $blockData = $blockResult->getData(); 262 if (!isset($blockData['tx']) || $txIndex >= count($blockData['tx'])) { 263 return new BlockchainResult(new RpcResponse( 264 data: null, 265 error: "Transaction index $txIndex out of range", 266 statusCode: 400 267 )); 268 } 269 270 return new BlockchainResult(new RpcResponse( 271 data: $blockData['tx'][$txIndex] 272 )); 273 } 274 275 public function getBlockchainSummary(): array 276 { 277 $blockchainInfo = $this->getBlockchainInfo(); 278 $blockCount = $this->getBlockCount(); 279 $difficulty = $this->getDifficulty(); 280 $mempoolInfo = $this->getMempoolInfo(); 281 $txOutSetInfo = $this->getTxOutSetInfo(); 282 283 $summary = [ 284 'blockchain_info_success' => $blockchainInfo->isSuccess(), 285 'block_count_success' => $blockCount->isSuccess(), 286 'difficulty_success' => $difficulty->isSuccess(), 287 'mempool_info_success' => $mempoolInfo->isSuccess(), 288 'txoutset_info_success' => $txOutSetInfo->isSuccess(), 289 'chain' => null, 290 'blocks' => null, 291 'headers' => null, 292 'best_block_hash' => null, 293 'difficulty' => null, 294 'mempool_size' => null, 295 'mempool_bytes' => null, 296 'utxo_count' => null, 297 'utxo_total' => null, 298 'errors' => [] 299 ]; 300 301 if ($blockchainInfo->isSuccess()) { 302 $data = $blockchainInfo->getData(); 303 $summary['chain'] = $data['chain'] ?? null; 304 $summary['blocks'] = $data['blocks'] ?? null; 305 $summary['headers'] = $data['headers'] ?? null; 306 $summary['best_block_hash'] = $data['bestblockhash'] ?? null; 307 $summary['pruned'] = $data['pruned'] ?? false; 308 $summary['prune_height'] = $data['pruneheight'] ?? null; 309 $summary['size_on_disk'] = $data['size_on_disk'] ?? null; 310 $summary['verification_progress'] = $data['verificationprogress'] ?? null; 311 $summary['initial_block_download'] = $data['initialblockdownload'] ?? null; 312 $summary['warnings'] = $data['warnings'] ?? null; 313 } else { 314 $summary['errors'][] = "Failed to get blockchain info: " . $blockchainInfo->getError(); 315 } 316 317 if ($blockCount->isSuccess()) { 318 $summary['blocks'] = $blockCount->getData(); 319 } else { 320 $summary['errors'][] = "Failed to get block count: " . $blockCount->getError(); 321 } 322 323 if ($difficulty->isSuccess()) { 324 $summary['difficulty'] = $difficulty->getData(); 325 } else { 326 $summary['errors'][] = "Failed to get difficulty: " . $difficulty->getError(); 327 } 328 329 if ($mempoolInfo->isSuccess()) { 330 $data = $mempoolInfo->getData(); 331 $summary['mempool_size'] = $data['size'] ?? null; 332 $summary['mempool_bytes'] = $data['bytes'] ?? null; 333 $summary['mempool_usage'] = $data['usage'] ?? null; 334 $summary['mempool_maxmempool'] = $data['maxmempool'] ?? null; 335 $summary['mempool_mempoolminfee'] = $data['mempoolminfee'] ?? null; 336 $summary['mempool_minrelaytxfee'] = $data['minrelaytxfee'] ?? null; 337 } else { 338 $summary['errors'][] = "Failed to get mempool info: " . $mempoolInfo->getError(); 339 } 340 341 if ($txOutSetInfo->isSuccess()) { 342 $data = $txOutSetInfo->getData(); 343 $summary['utxo_count'] = $data['txouts'] ?? null; 344 $summary['utxo_total'] = $data['total_amount'] ?? null; 345 $summary['utxo_height'] = $data['height'] ?? null; 346 $summary['utxo_best_block'] = $data['bestblock'] ?? null; 347 $summary['utxo_hash_serialized'] = $data['hash_serialized'] ?? null; 348 $summary['utxo_disk_size'] = $data['disk_size'] ?? null; 349 } else { 350 $summary['errors'][] = "Failed to get UTXO set info: " . $txOutSetInfo->getError(); 351 } 352 353 return $summary; 354 } 355 356 public function getMempoolTransactions(?bool $verbose = null): array 357 { 358 $verbose = $verbose ?? true; 359 $mempoolResult = $this->getRawMempool($verbose); 360 361 if (!$mempoolResult->isSuccess()) { 362 return [ 363 'success' => false, 364 'error' => $mempoolResult->getError(), 365 'transactions' => [] 366 ]; 367 } 368 369 $mempoolData = $mempoolResult->getData(); 370 371 return [ 372 'success' => true, 373 'verbose' => $verbose, 374 'transaction_count' => is_array($mempoolData) ? count($mempoolData) : 0, 375 'transactions' => $mempoolData 376 ]; 377 } 378 379 public function getTransactionDetails(string $txid): array 380 { 381 $txResult = $this->getRawTransaction($txid, true); 382 $mempoolEntry = $this->getMempoolEntry($txid); 383 $ancestors = $this->getMempoolAncestors($txid, true); 384 $descendants = $this->getMempoolDescendants($txid, true); 385 386 $details = [ 387 'txid' => $txid, 388 'transaction_success' => $txResult->isSuccess(), 389 'mempool_entry_success' => $mempoolEntry->isSuccess(), 390 'ancestors_success' => $ancestors->isSuccess(), 391 'descendants_success' => $descendants->isSuccess(), 392 'transaction' => null, 393 'mempool_entry' => null, 394 'ancestors' => null, 395 'descendants' => null, 396 'errors' => [] 397 ]; 398 399 if ($txResult->isSuccess()) { 400 $details['transaction'] = $txResult->getData(); 401 } else { 402 $details['errors'][] = "Failed to get transaction: " . $txResult->getError(); 403 } 404 405 if ($mempoolEntry->isSuccess()) { 406 $details['mempool_entry'] = $mempoolEntry->getData(); 407 } else { 408 $details['errors'][] = "Failed to get mempool entry: " . $mempoolEntry->getError(); 409 } 410 411 if ($ancestors->isSuccess()) { 412 $details['ancestors'] = $ancestors->getData(); 413 } else { 414 $details['errors'][] = "Failed to get ancestors: " . $ancestors->getError(); 415 } 416 417 if ($descendants->isSuccess()) { 418 $details['descendants'] = $descendants->getData(); 419 } else { 420 $details['errors'][] = "Failed to get descendants: " . $descendants->getError(); 421 } 422 423 return $details; 424 } 425 426 public function getChainHealth(): array 427 { 428 $summary = $this->getBlockchainSummary(); 429 $chainTips = $this->getChainTips(); 430 $verification = $this->verifyChain(); 431 432 $health = [ 433 'status' => 'unknown', 434 'blocks' => $summary['blocks'] ?? 0, 435 'headers' => $summary['headers'] ?? 0, 436 'verification_progress' => $summary['verification_progress'] ?? 0, 437 'initial_block_download' => $summary['initial_block_download'] ?? false, 438 'chain_tips' => 0, 439 'verification_success' => false, 440 'issues' => [] 441 ]; 442 443 // Check block synchronization 444 if ($health['blocks'] !== $health['headers']) { 445 $health['status'] = 'syncing'; 446 $health['issues'][] = "Blocks behind headers: " . ($health['headers'] - $health['blocks']); 447 } else { 448 $health['status'] = 'synced'; 449 } 450 451 // Check verification progress 452 if ($health['verification_progress'] < 0.999) { 453 $health['status'] = 'verifying'; 454 $health['issues'][] = "Verification progress: " . round($health['verification_progress'] * 100, 2) . "%"; 455 } 456 457 // Check initial block download 458 if ($health['initial_block_download']) { 459 $health['status'] = 'downloading'; 460 $health['issues'][] = "Initial block download in progress"; 461 } 462 463 // Check chain tips 464 if ($chainTips->isSuccess()) { 465 $tips = $chainTips->getData(); 466 $health['chain_tips'] = is_array($tips) ? count($tips) : 0; 467 468 if ($health['chain_tips'] > 1) { 469 $health['status'] = 'forked'; 470 $health['issues'][] = "Multiple chain tips detected: " . $health['chain_tips']; 471 } 472 } 473 474 // Check chain verification 475 if ($verification->isSuccess()) { 476 $health['verification_success'] = $verification->getData() === true; 477 if (!$health['verification_success']) { 478 $health['status'] = 'corrupted'; 479 $health['issues'][] = "Chain verification failed"; 480 } 481 } 482 483 // Check for errors in summary 484 if (!empty($summary['errors'])) { 485 $health['status'] = 'error'; 486 $health['issues'] = array_merge($health['issues'], $summary['errors']); 487 } 488 489 return $health; 490 } 491 492 public function getUTXOSetAnalysis(): array 493 { 494 $txOutSetInfo = $this->getTxOutSetInfo(); 495 496 if (!$txOutSetInfo->isSuccess()) { 497 return [ 498 'success' => false, 499 'error' => $txOutSetInfo->getError(), 500 'analysis' => null 501 ]; 502 } 503 504 $data = $txOutSetInfo->getData(); 505 506 $analysis = [ 507 'success' => true, 508 'height' => $data['height'] ?? null, 509 'best_block' => $data['bestblock'] ?? null, 510 'transactions' => $data['transactions'] ?? null, 511 'txouts' => $data['txouts'] ?? null, 512 'bogosize' => $data['bogosize'] ?? null, 513 'hash_serialized' => $data['hash_serialized'] ?? null, 514 'disk_size' => $data['disk_size'] ?? null, 515 'total_amount' => $data['total_amount'] ?? null, 516 'analysis' => [] 517 ]; 518 519 // Calculate some statistics 520 if ($analysis['txouts'] > 0 && $analysis['transactions'] > 0) { 521 $analysis['analysis']['average_utxos_per_tx'] = $analysis['txouts'] / $analysis['transactions']; 522 $analysis['analysis']['average_utxo_value'] = $analysis['total_amount'] / $analysis['txouts']; 523 $analysis['analysis']['utxo_set_size_gb'] = $analysis['disk_size'] / (1024 * 1024 * 1024); 524 } 525 526 return $analysis; 527 } 528 529 public function scanForAddresses(array $descriptors): BlockchainResult 530 { 531 $scanObjects = []; 532 533 foreach ($descriptors as $descriptor) { 534 $scanObjects[] = [ 535 'desc' => $descriptor, 536 'range' => [0, 1000] // Scan first 1000 addresses 537 ]; 538 } 539 540 return $this->scanTxOutSet("start", $scanObjects); 541 } 542 543 public function getBlockRange(int $startHeight, int $endHeight, int $verbosity = 1): array 544 { 545 $blocks = []; 546 547 for ($height = $startHeight; $height <= $endHeight; $height++) { 548 $blockResult = $this->getBlockByHeight($height, $verbosity); 549 if ($blockResult->isSuccess()) { 550 $blocks[$height] = $blockResult->getData(); 551 } else { 552 return [ 553 'success' => false, 554 'error' => "Failed to get block at height $height: " . $blockResult->getError(), 555 'blocks' => $blocks 556 ]; 557 } 558 } 559 560 return [ 561 'success' => true, 562 'start_height' => $startHeight, 563 'end_height' => $endHeight, 564 'block_count' => count($blocks), 565 'blocks' => $blocks 566 ]; 567 } 568 569 public function getTransactionConfirmationStatus(string $txid): array 570 { 571 $txResult = $this->getRawTransaction($txid, true); 572 $mempoolEntry = $this->getMempoolEntry($txid); 573 574 $status = [ 575 'txid' => $txid, 576 'found' => false, 577 'in_mempool' => false, 578 'confirmed' => false, 579 'confirmations' => 0, 580 'block_hash' => null, 581 'block_height' => null, 582 'block_time' => null 583 ]; 584 585 if ($txResult->isSuccess()) { 586 $txData = $txResult->getData(); 587 $status['found'] = true; 588 $status['confirmations'] = $txData['confirmations'] ?? 0; 589 $status['confirmed'] = $status['confirmations'] > 0; 590 $status['block_hash'] = $txData['blockhash'] ?? null; 591 $status['block_time'] = $txData['blocktime'] ?? null; 592 593 if ($status['block_hash']) { 594 // Get block height from block hash 595 $blockResult = $this->getBlockHeader($status['block_hash']); 596 if ($blockResult->isSuccess()) { 597 $blockData = $blockResult->getData(); 598 $status['block_height'] = $blockData['height'] ?? null; 599 } 600 } 601 } 602 603 if ($mempoolEntry->isSuccess()) { 604 $status['in_mempool'] = true; 605 $mempoolData = $mempoolEntry->getData(); 606 $status['mempool_fee'] = $mempoolData['fee'] ?? null; 607 $status['mempool_size'] = $mempoolData['size'] ?? null; 608 $status['mempool_time'] = $mempoolData['time'] ?? null; 609 } 610 611 return $status; 612 } 613 }