Network.php
1 <?php 2 3 namespace BitcoindRPC\Network; 4 5 use BitcoindRPC\RpcClient; 6 use BitcoindRPC\RpcResponse; 7 8 class Network 9 { 10 private RpcClient $client; 11 12 public function __construct(RpcClient $client) 13 { 14 $this->client = $client; 15 } 16 17 public function addNode(string $node, string $command = "add"): NetworkResult 18 { 19 if (!in_array($command, ["add", "remove", "onetry"])) { 20 return new NetworkResult(new RpcResponse( 21 data: null, 22 error: "Command must be 'add', 'remove', or 'onetry'", 23 statusCode: 400 24 )); 25 } 26 27 $response = $this->client->call('addnode', [$node, $command]); 28 return new NetworkResult($response); 29 } 30 31 public function clearBanned(): NetworkResult 32 { 33 $response = $this->client->call('clearbanned'); 34 return new NetworkResult($response); 35 } 36 37 public function disconnectNode(?string $address = null, ?int $nodeId = null): NetworkResult 38 { 39 if ($address === null && $nodeId === null) { 40 return new NetworkResult(new RpcResponse( 41 data: null, 42 error: "Either address or node_id must be specified", 43 statusCode: 400 44 )); 45 } 46 47 $params = []; 48 if ($address !== null) { 49 $params[] = $address; 50 } elseif ($nodeId !== null) { 51 $params[] = $nodeId; 52 } 53 54 $response = $this->client->call('disconnectnode', $params); 55 return new NetworkResult($response); 56 } 57 58 public function getAddedNodeInfo(?string $node = null): NetworkResult 59 { 60 $params = []; 61 if ($node !== null) { 62 $params[] = $node; 63 } 64 65 $response = $this->client->call('getaddednodeinfo', $params); 66 return new NetworkResult($response); 67 } 68 69 public function getConnectionCount(): NetworkResult 70 { 71 $response = $this->client->call('getconnectioncount'); 72 return new NetworkResult($response); 73 } 74 75 public function getNetTotals(): NetworkResult 76 { 77 $response = $this->client->call('getnettotals'); 78 return new NetworkResult($response); 79 } 80 81 public function getNetworkInfo(): NetworkResult 82 { 83 $response = $this->client->call('getnetworkinfo'); 84 return new NetworkResult($response); 85 } 86 87 public function getNodeAddresses(?int $count = null): NetworkResult 88 { 89 $params = []; 90 if ($count !== null) { 91 if ($count < 1 || $count > 2048) { 92 return new NetworkResult(new RpcResponse( 93 data: null, 94 error: "count must be between 1 and 2048", 95 statusCode: 400 96 )); 97 } 98 $params[] = $count; 99 } 100 101 $response = $this->client->call('getnodeaddresses', $params); 102 return new NetworkResult($response); 103 } 104 105 public function getPeerInfo(): NetworkResult 106 { 107 $response = $this->client->call('getpeerinfo'); 108 return new NetworkResult($response); 109 } 110 111 public function listBanned(): NetworkResult 112 { 113 $response = $this->client->call('listbanned'); 114 return new NetworkResult($response); 115 } 116 117 public function setBan( 118 string $subnet, 119 string $command = "add", 120 ?int $bantime = null, 121 bool $absolute = false 122 ): NetworkResult { 123 if (!in_array($command, ["add", "remove"])) { 124 return new NetworkResult(new RpcResponse( 125 data: null, 126 error: "Command must be 'add' or 'remove'", 127 statusCode: 400 128 )); 129 } 130 131 $params = [$subnet, $command]; 132 if ($bantime !== null) { 133 $params[] = $bantime; 134 if ($absolute) { 135 $params[] = $absolute; 136 } 137 } 138 139 $response = $this->client->call('setban', $params); 140 return new NetworkResult($response); 141 } 142 143 public function setNetworkActive(bool $state): NetworkResult 144 { 145 $response = $this->client->call('setnetworkactive', [$state]); 146 return new NetworkResult($response); 147 } 148 149 public function ping(): NetworkResult 150 { 151 $response = $this->client->call('ping'); 152 return new NetworkResult($response); 153 } 154 155 // Utility methods 156 public function getNetworkSummary(): array 157 { 158 $networkInfo = $this->getNetworkInfo(); 159 $connectionCount = $this->getConnectionCount(); 160 $netTotals = $this->getNetTotals(); 161 $peerInfo = $this->getPeerInfo(); 162 163 $summary = [ 164 'network_info_success' => $networkInfo->isSuccess(), 165 'connection_count_success' => $connectionCount->isSuccess(), 166 'net_totals_success' => $netTotals->isSuccess(), 167 'peer_info_success' => $peerInfo->isSuccess(), 168 'version' => null, 169 'subversion' => null, 170 'protocol_version' => null, 171 'connections' => null, 172 'network_active' => null, 173 'bytes_sent' => null, 174 'bytes_received' => null, 175 'peers' => [], 176 'errors' => [] 177 ]; 178 179 if ($networkInfo->isSuccess()) { 180 $data = $networkInfo->getData(); 181 $summary['version'] = $data['version'] ?? null; 182 $summary['subversion'] = $data['subversion'] ?? null; 183 $summary['protocol_version'] = $data['protocolversion'] ?? null; 184 $summary['network_active'] = $data['networkactive'] ?? null; 185 $summary['local_services'] = $data['localservices'] ?? null; 186 $summary['local_relay'] = $data['localrelay'] ?? null; 187 $summary['time_offset'] = $data['timeoffset'] ?? null; 188 $summary['warnings'] = $data['warnings'] ?? null; 189 } else { 190 $summary['errors'][] = "Failed to get network info: " . $networkInfo->getError(); 191 } 192 193 if ($connectionCount->isSuccess()) { 194 $summary['connections'] = $connectionCount->getData(); 195 } else { 196 $summary['errors'][] = "Failed to get connection count: " . $connectionCount->getError(); 197 } 198 199 if ($netTotals->isSuccess()) { 200 $data = $netTotals->getData(); 201 $summary['bytes_sent'] = $data['totalbytesrecv'] ?? null; 202 $summary['bytes_received'] = $data['totalbytessent'] ?? null; 203 $summary['time_millis'] = $data['timemillis'] ?? null; 204 } else { 205 $summary['errors'][] = "Failed to get net totals: " . $netTotals->getError(); 206 } 207 208 if ($peerInfo->isSuccess()) { 209 $summary['peers'] = $peerInfo->getData(); 210 } else { 211 $summary['errors'][] = "Failed to get peer info: " . $peerInfo->getError(); 212 } 213 214 return $summary; 215 } 216 217 public function getActiveConnections(): ?array 218 { 219 $peerInfo = $this->getPeerInfo(); 220 if (!$peerInfo->isSuccess()) { 221 return null; 222 } 223 224 $peers = $peerInfo->getData(); 225 $activePeers = []; 226 227 foreach ($peers as $peer) { 228 if (($peer['connected'] ?? false) === true) { 229 $activePeers[] = [ 230 'address' => $peer['addr'] ?? null, 231 'services' => $peer['services'] ?? null, 232 'version' => $peer['version'] ?? null, 233 'subversion' => $peer['subver'] ?? null, 234 'bytes_sent' => $peer['bytessent'] ?? null, 235 'bytes_received' => $peer['bytesrecv'] ?? null, 236 'connection_time' => $peer['conntime'] ?? null, 237 'ping_time' => $peer['pingtime'] ?? null 238 ]; 239 } 240 } 241 242 return $activePeers; 243 } 244 245 public function getBannedCount(): ?int 246 { 247 $banned = $this->listBanned(); 248 if (!$banned->isSuccess()) { 249 return null; 250 } 251 252 $bannedList = $banned->getData(); 253 return is_array($bannedList) ? count($bannedList) : 0; 254 } 255 256 public function isNodeConnected(string $address): bool 257 { 258 $peerInfo = $this->getPeerInfo(); 259 if (!$peerInfo->isSuccess()) { 260 return false; 261 } 262 263 $peers = $peerInfo->getData(); 264 foreach ($peers as $peer) { 265 if (($peer['addr'] ?? '') === $address && ($peer['connected'] ?? false) === true) { 266 return true; 267 } 268 } 269 270 return false; 271 } 272 273 public function getNodeStatistics(): ?array 274 { 275 $peerInfo = $this->getPeerInfo(); 276 if (!$peerInfo->isSuccess()) { 277 return null; 278 } 279 280 $peers = $peerInfo->getData(); 281 $stats = [ 282 'total_peers' => count($peers), 283 'connected_peers' => 0, 284 'inbound_peers' => 0, 285 'outbound_peers' => 0, 286 'versions' => [], 287 'countries' => [], 288 'total_bytes_sent' => 0, 289 'total_bytes_received' => 0 290 ]; 291 292 foreach ($peers as $peer) { 293 if ($peer['connected'] ?? false) { 294 $stats['connected_peers']++; 295 296 if (($peer['inbound'] ?? false) === true) { 297 $stats['inbound_peers']++; 298 } else { 299 $stats['outbound_peers']++; 300 } 301 302 $version = $peer['version'] ?? 'unknown'; 303 $stats['versions'][$version] = ($stats['versions'][$version] ?? 0) + 1; 304 305 // Extract country from address (simplified) 306 $address = $peer['addr'] ?? ''; 307 if (str_contains($address, '.')) { 308 // IPv4 address 309 // In a real implementation, you might want to use geoip here 310 $stats['countries']['unknown'] = ($stats['countries']['unknown'] ?? 0) + 1; 311 } 312 313 $stats['total_bytes_sent'] += $peer['bytessent'] ?? 0; 314 $stats['total_bytes_received'] += $peer['bytesrecv'] ?? 0; 315 } 316 } 317 318 return $stats; 319 } 320 321 public function banIP(string $ip, int $bantime = 86400, bool $absolute = false): NetworkResult 322 { 323 return $this->setBan($ip, "add", $bantime, $absolute); 324 } 325 326 public function unbanIP(string $ip): NetworkResult 327 { 328 return $this->setBan($ip, "remove"); 329 } 330 331 public function addNodeWithRetry(string $node, int $maxRetries = 3): NetworkResult 332 { 333 $result = $this->addNode($node, "onetry"); 334 335 if ($result->isSuccess() || $maxRetries <= 1) { 336 return $result; 337 } 338 339 // Wait a bit and retry 340 sleep(1); 341 return $this->addNodeWithRetry($node, $maxRetries - 1); 342 } 343 344 public function getNetworkHealth(): array 345 { 346 $summary = $this->getNetworkSummary(); 347 $connections = $this->getConnectionCount(); 348 $bannedCount = $this->getBannedCount(); 349 350 $health = [ 351 'status' => 'unknown', 352 'connections' => $connections->isSuccess() ? $connections->getData() : 0, 353 'banned_ips' => $bannedCount ?? 0, 354 'network_active' => $summary['network_active'] ?? false, 355 'issues' => [] 356 ]; 357 358 // Check connection count 359 if ($health['connections'] < 3) { 360 $health['status'] = 'poor'; 361 $health['issues'][] = "Low connection count: " . $health['connections']; 362 } elseif ($health['connections'] < 8) { 363 $health['status'] = 'fair'; 364 } else { 365 $health['status'] = 'good'; 366 } 367 368 // Check network activity 369 if ($health['network_active'] === false) { 370 $health['status'] = 'poor'; 371 $health['issues'][] = "Network is not active"; 372 } 373 374 // Check for errors in summary 375 if (!empty($summary['errors'])) { 376 $health['status'] = 'poor'; 377 $health['issues'] = array_merge($health['issues'], $summary['errors']); 378 } 379 380 return $health; 381 } 382 383 public function getTrafficStatistics(): ?array 384 { 385 $netTotals = $this->getNetTotals(); 386 if (!$netTotals->isSuccess()) { 387 return null; 388 } 389 390 $data = $netTotals->getData(); 391 392 return [ 393 'total_bytes_received' => $data['totalbytesrecv'] ?? 0, 394 'total_bytes_sent' => $data['totalbytessent'] ?? 0, 395 'total_time_millis' => $data['timemillis'] ?? 0, 396 'upload_target' => $data['uploadtarget'] ?? [], 397 'download_target' => $data['downloadtarget'] ?? [] 398 ]; 399 } 400 }