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