/ src / Blockchain / Blockchain.php
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  }