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