Completed
Branch master (fa2f13)
by
unknown
19:25
created

BlocktrailSDK::normalizeNetwork()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 26
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 19
nc 7
nop 2
dl 0
loc 26
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\AddressFactory;
6
use BitWasp\Bitcoin\Address\PayToPubKeyHashAddress;
7
use BitWasp\Bitcoin\Bitcoin;
8
use BitWasp\Bitcoin\Crypto\EcAdapter\EcSerializer;
9
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
10
use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\CompactSignatureSerializerInterface;
11
use BitWasp\Bitcoin\Crypto\Random\Random;
12
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKey;
13
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
14
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
15
use BitWasp\Bitcoin\MessageSigner\SignedMessage;
16
use BitWasp\Bitcoin\Mnemonic\Bip39\Bip39SeedGenerator;
17
use BitWasp\Bitcoin\Mnemonic\MnemonicFactory;
18
use BitWasp\Bitcoin\Network\NetworkFactory;
19
use BitWasp\Bitcoin\Transaction\TransactionFactory;
20
use BitWasp\Buffertools\Buffer;
21
use BitWasp\Buffertools\BufferInterface;
22
use Blocktrail\CryptoJSAES\CryptoJSAES;
23
use Blocktrail\SDK\Bitcoin\BIP32Key;
24
use Blocktrail\SDK\Connection\RestClient;
25
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
26
use Blocktrail\SDK\V3Crypt\Encryption;
27
use Blocktrail\SDK\V3Crypt\EncryptionMnemonic;
28
use Blocktrail\SDK\V3Crypt\KeyDerivation;
29
30
/**
31
 * Class BlocktrailSDK
32
 */
33
class BlocktrailSDK implements BlocktrailSDKInterface {
34
    /**
35
     * @var Connection\RestClient
36
     */
37
    protected $client;
38
39
    /**
40
     * @var string          currently only supporting; bitcoin
41
     */
42
    protected $network;
43
44
    /**
45
     * @var bool
46
     */
47
    protected $testnet;
48
49
    /**
50
     * @param   string      $apiKey         the API_KEY to use for authentication
51
     * @param   string      $apiSecret      the API_SECRET to use for authentication
52
     * @param   string      $network        the cryptocurrency 'network' to consume, eg BTC, LTC, etc
53
     * @param   bool        $testnet        testnet yes/no
54
     * @param   string      $apiVersion     the version of the API to consume
55
     * @param   null        $apiEndpoint    overwrite the endpoint used
56
     *                                       this will cause the $network, $testnet and $apiVersion to be ignored!
57
     */
58
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
59
        if (is_null($apiEndpoint)) {
60
            $network = strtoupper($network);
61
62
            if ($network === "TBTC") {
63
                $apiNetwork = "tBTC";
64
            } else if ($network === "RBTC") {
65
                $apiNetwork = "rBTC";
66
            } else {
67
                if ($testnet) {
68
                    $apiNetwork = "tBTC";
69
                    $network = "tBTC";
70
                } else {
71
                    $apiNetwork = "BTC";
72
                }
73
            }
74
75
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com";
76
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
77
        }
78
79
        // normalize network and set bitcoinlib to the right magic-bytes
80
        list($this->network, $this->testnet) = $this->normalizeNetwork($network, $testnet);
81
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet);
82
83
        $this->client = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
84
    }
85
86
    /**
87
     * normalize network string
88
     *
89
     * @param $network
90
     * @param $testnet
91
     * @return array
92
     * @throws \Exception
93
     */
94
    protected function normalizeNetwork($network, $testnet) {
95
        switch (strtolower($network)) {
96
            case 'btc':
97
            case 'bitcoin':
98
                $network = 'bitcoin';
99
                break;
100
101
            case 'tbtc':
102
            case 'bitcoin-testnet':
103
                $network = 'bitcoin';
104
                $testnet = true;
105
                break;
106
107
            case 'rbtc':
108
            case 'bitcoin-regtest':
109
                $network = 'bitcoin';
110
                $testnet = true;
111
                break;
112
113
            default:
114
                throw new \Exception("Unknown network [{$network}]");
115
                // this comment silences a phpcs error.
116
        }
117
118
        return [$network, $testnet];
119
    }
120
121
    /**
122
     * set BitcoinLib to the correct magic-byte defaults for the selected network
123
     *
124
     * @param $network
125
     * @param $testnet
126
     */
127
    protected function setBitcoinLibMagicBytes($network, $testnet) {
128
        assert($network == "bitcoin");
129
        Bitcoin::setNetwork($testnet ? NetworkFactory::bitcoinTestnet() : NetworkFactory::bitcoin());
130
    }
131
132
    /**
133
     * enable CURL debugging output
134
     *
135
     * @param   bool        $debug
136
     *
137
     * @codeCoverageIgnore
138
     */
139
    public function setCurlDebugging($debug = true) {
140
        $this->client->setCurlDebugging($debug);
141
    }
142
143
    /**
144
     * enable verbose errors
145
     *
146
     * @param   bool        $verboseErrors
147
     *
148
     * @codeCoverageIgnore
149
     */
150
    public function setVerboseErrors($verboseErrors = true) {
151
        $this->client->setVerboseErrors($verboseErrors);
152
    }
153
    
154
    /**
155
     * set cURL default option on Guzzle client
156
     * @param string    $key
157
     * @param bool      $value
158
     *
159
     * @codeCoverageIgnore
160
     */
161
    public function setCurlDefaultOption($key, $value) {
162
        $this->client->setCurlDefaultOption($key, $value);
163
    }
164
165
    /**
166
     * @return  RestClient
167
     */
168
    public function getRestClient() {
169
        return $this->client;
170
    }
171
172
    /**
173
     * get a single address
174
     * @param  string $address address hash
175
     * @return array           associative array containing the response
176
     */
177
    public function address($address) {
178
        $response = $this->client->get("address/{$address}");
179
        return self::jsonDecode($response->body(), true);
180
    }
181
182
    /**
183
     * get all transactions for an address (paginated)
184
     * @param  string  $address address hash
185
     * @param  integer $page    pagination: page number
186
     * @param  integer $limit   pagination: records per page (max 500)
187
     * @param  string  $sortDir pagination: sort direction (asc|desc)
188
     * @return array            associative array containing the response
189
     */
190
    public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
191
        $queryString = [
192
            'page' => $page,
193
            'limit' => $limit,
194
            'sort_dir' => $sortDir
195
        ];
196
        $response = $this->client->get("address/{$address}/transactions", $queryString);
197
        return self::jsonDecode($response->body(), true);
198
    }
199
200
    /**
201
     * get all unconfirmed transactions for an address (paginated)
202
     * @param  string  $address address hash
203
     * @param  integer $page    pagination: page number
204
     * @param  integer $limit   pagination: records per page (max 500)
205
     * @param  string  $sortDir pagination: sort direction (asc|desc)
206
     * @return array            associative array containing the response
207
     */
208
    public function addressUnconfirmedTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
209
        $queryString = [
210
            'page' => $page,
211
            'limit' => $limit,
212
            'sort_dir' => $sortDir
213
        ];
214
        $response = $this->client->get("address/{$address}/unconfirmed-transactions", $queryString);
215
        return self::jsonDecode($response->body(), true);
216
    }
217
218
    /**
219
     * get all unspent outputs for an address (paginated)
220
     * @param  string  $address address hash
221
     * @param  integer $page    pagination: page number
222
     * @param  integer $limit   pagination: records per page (max 500)
223
     * @param  string  $sortDir pagination: sort direction (asc|desc)
224
     * @return array            associative array containing the response
225
     */
226
    public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') {
227
        $queryString = [
228
            'page' => $page,
229
            'limit' => $limit,
230
            'sort_dir' => $sortDir
231
        ];
232
        $response = $this->client->get("address/{$address}/unspent-outputs", $queryString);
233
        return self::jsonDecode($response->body(), true);
234
    }
235
236
    /**
237
     * get all unspent outputs for a batch of addresses (paginated)
238
     *
239
     * @param  string[] $addresses
240
     * @param  integer  $page    pagination: page number
241
     * @param  integer  $limit   pagination: records per page (max 500)
242
     * @param  string   $sortDir pagination: sort direction (asc|desc)
243
     * @return array associative array containing the response
244
     * @throws \Exception
245
     */
246
    public function batchAddressUnspentOutputs($addresses, $page = 1, $limit = 20, $sortDir = 'asc') {
247
        $queryString = [
248
            'page' => $page,
249
            'limit' => $limit,
250
            'sort_dir' => $sortDir
251
        ];
252
        $response = $this->client->post("address/unspent-outputs", $queryString, ['addresses' => $addresses]);
253
        return self::jsonDecode($response->body(), true);
254
    }
255
256
    /**
257
     * verify ownership of an address
258
     * @param  string  $address     address hash
259
     * @param  string  $signature   a signed message (the address hash) using the private key of the address
260
     * @return array                associative array containing the response
261
     */
262
    public function verifyAddress($address, $signature) {
263
        $postData = ['signature' => $signature];
264
265
        $response = $this->client->post("address/{$address}/verify", null, $postData, RestClient::AUTH_HTTP_SIG);
266
267
        return self::jsonDecode($response->body(), true);
268
    }
269
270
    /**
271
     * get all blocks (paginated)
272
     * @param  integer $page    pagination: page number
273
     * @param  integer $limit   pagination: records per page
274
     * @param  string  $sortDir pagination: sort direction (asc|desc)
275
     * @return array            associative array containing the response
276
     */
277
    public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') {
278
        $queryString = [
279
            'page' => $page,
280
            'limit' => $limit,
281
            'sort_dir' => $sortDir
282
        ];
283
        $response = $this->client->get("all-blocks", $queryString);
284
        return self::jsonDecode($response->body(), true);
285
    }
286
287
    /**
288
     * get the latest block
289
     * @return array            associative array containing the response
290
     */
291
    public function blockLatest() {
292
        $response = $this->client->get("block/latest");
293
        return self::jsonDecode($response->body(), true);
294
    }
295
296
    /**
297
     * get an individual block
298
     * @param  string|integer $block    a block hash or a block height
299
     * @return array                    associative array containing the response
300
     */
301
    public function block($block) {
302
        $response = $this->client->get("block/{$block}");
303
        return self::jsonDecode($response->body(), true);
304
    }
305
306
    /**
307
     * get all transaction in a block (paginated)
308
     * @param  string|integer   $block   a block hash or a block height
309
     * @param  integer          $page    pagination: page number
310
     * @param  integer          $limit   pagination: records per page
311
     * @param  string           $sortDir pagination: sort direction (asc|desc)
312
     * @return array                     associative array containing the response
313
     */
314
    public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') {
315
        $queryString = [
316
            'page' => $page,
317
            'limit' => $limit,
318
            'sort_dir' => $sortDir
319
        ];
320
        $response = $this->client->get("block/{$block}/transactions", $queryString);
321
        return self::jsonDecode($response->body(), true);
322
    }
323
324
    /**
325
     * get a single transaction
326
     * @param  string $txhash transaction hash
327
     * @return array          associative array containing the response
328
     */
329
    public function transaction($txhash) {
330
        $response = $this->client->get("transaction/{$txhash}");
331
        return self::jsonDecode($response->body(), true);
332
    }
333
334
    /**
335
     * get a single transaction
336
     * @param  string[] $txhashes list of transaction hashes (up to 20)
337
     * @return array[]            array containing the response
338
     */
339
    public function transactions($txhashes) {
340
        $response = $this->client->get("transactions/" . implode(",", $txhashes));
341
        return self::jsonDecode($response->body(), true);
342
    }
343
    
344
    /**
345
     * get a paginated list of all webhooks associated with the api user
346
     * @param  integer          $page    pagination: page number
347
     * @param  integer          $limit   pagination: records per page
348
     * @return array                     associative array containing the response
349
     */
350
    public function allWebhooks($page = 1, $limit = 20) {
351
        $queryString = [
352
            'page' => $page,
353
            'limit' => $limit
354
        ];
355
        $response = $this->client->get("webhooks", $queryString);
356
        return self::jsonDecode($response->body(), true);
357
    }
358
359
    /**
360
     * get an existing webhook by it's identifier
361
     * @param string    $identifier     a unique identifier associated with the webhook
362
     * @return array                    associative array containing the response
363
     */
364
    public function getWebhook($identifier) {
365
        $response = $this->client->get("webhook/".$identifier);
366
        return self::jsonDecode($response->body(), true);
367
    }
368
369
    /**
370
     * create a new webhook
371
     * @param  string  $url        the url to receive the webhook events
372
     * @param  string  $identifier a unique identifier to associate with this webhook
373
     * @return array               associative array containing the response
374
     */
375 View Code Duplication
    public function setupWebhook($url, $identifier = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
376
        $postData = [
377
            'url'        => $url,
378
            'identifier' => $identifier
379
        ];
380
        $response = $this->client->post("webhook", null, $postData, RestClient::AUTH_HTTP_SIG);
381
        return self::jsonDecode($response->body(), true);
382
    }
383
384
    /**
385
     * update an existing webhook
386
     * @param  string  $identifier      the unique identifier of the webhook to update
387
     * @param  string  $newUrl          the new url to receive the webhook events
388
     * @param  string  $newIdentifier   a new unique identifier to associate with this webhook
389
     * @return array                    associative array containing the response
390
     */
391
    public function updateWebhook($identifier, $newUrl = null, $newIdentifier = null) {
392
        $putData = [
393
            'url'        => $newUrl,
394
            'identifier' => $newIdentifier
395
        ];
396
        $response = $this->client->put("webhook/{$identifier}", null, $putData, RestClient::AUTH_HTTP_SIG);
397
        return self::jsonDecode($response->body(), true);
398
    }
399
400
    /**
401
     * deletes an existing webhook and any event subscriptions associated with it
402
     * @param  string  $identifier      the unique identifier of the webhook to delete
403
     * @return boolean                  true on success
404
     */
405
    public function deleteWebhook($identifier) {
406
        $response = $this->client->delete("webhook/{$identifier}", null, null, RestClient::AUTH_HTTP_SIG);
407
        return self::jsonDecode($response->body(), true);
408
    }
409
410
    /**
411
     * get a paginated list of all the events a webhook is subscribed to
412
     * @param  string  $identifier  the unique identifier of the webhook
413
     * @param  integer $page        pagination: page number
414
     * @param  integer $limit       pagination: records per page
415
     * @return array                associative array containing the response
416
     */
417
    public function getWebhookEvents($identifier, $page = 1, $limit = 20) {
418
        $queryString = [
419
            'page' => $page,
420
            'limit' => $limit
421
        ];
422
        $response = $this->client->get("webhook/{$identifier}/events", $queryString);
423
        return self::jsonDecode($response->body(), true);
424
    }
425
    
426
    /**
427
     * subscribes a webhook to transaction events of one particular transaction
428
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
429
     * @param  string  $transaction     the transaction hash
430
     * @param  integer $confirmations   the amount of confirmations to send.
431
     * @return array                    associative array containing the response
432
     */
433 View Code Duplication
    public function subscribeTransaction($identifier, $transaction, $confirmations = 6) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
434
        $postData = [
435
            'event_type'    => 'transaction',
436
            'transaction'   => $transaction,
437
            'confirmations' => $confirmations,
438
        ];
439
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
440
        return self::jsonDecode($response->body(), true);
441
    }
442
443
    /**
444
     * subscribes a webhook to transaction events on a particular address
445
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
446
     * @param  string  $address         the address hash
447
     * @param  integer $confirmations   the amount of confirmations to send.
448
     * @return array                    associative array containing the response
449
     */
450 View Code Duplication
    public function subscribeAddressTransactions($identifier, $address, $confirmations = 6) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
451
        $postData = [
452
            'event_type'    => 'address-transactions',
453
            'address'       => $address,
454
            'confirmations' => $confirmations,
455
        ];
456
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
457
        return self::jsonDecode($response->body(), true);
458
    }
459
460
    /**
461
     * batch subscribes a webhook to multiple transaction events
462
     *
463
     * @param  string $identifier   the unique identifier of the webhook
464
     * @param  array  $batchData    A 2D array of event data:
465
     *                              [address => $address, confirmations => $confirmations]
466
     *                              where $address is the address to subscibe to
467
     *                              and optionally $confirmations is the amount of confirmations
468
     * @return boolean              true on success
469
     */
470
    public function batchSubscribeAddressTransactions($identifier, $batchData) {
471
        $postData = [];
472
        foreach ($batchData as $record) {
473
            $postData[] = [
474
                'event_type' => 'address-transactions',
475
                'address' => $record['address'],
476
                'confirmations' => isset($record['confirmations']) ? $record['confirmations'] : 6,
477
            ];
478
        }
479
        $response = $this->client->post("webhook/{$identifier}/events/batch", null, $postData, RestClient::AUTH_HTTP_SIG);
480
        return self::jsonDecode($response->body(), true);
481
    }
482
483
    /**
484
     * subscribes a webhook to a new block event
485
     * @param  string  $identifier  the unique identifier of the webhook to be triggered
486
     * @return array                associative array containing the response
487
     */
488 View Code Duplication
    public function subscribeNewBlocks($identifier) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
489
        $postData = [
490
            'event_type'    => 'block',
491
        ];
492
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
493
        return self::jsonDecode($response->body(), true);
494
    }
495
496
    /**
497
     * removes an transaction event subscription from a webhook
498
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
499
     * @param  string  $transaction     the transaction hash of the event subscription
500
     * @return boolean                  true on success
501
     */
502
    public function unsubscribeTransaction($identifier, $transaction) {
503
        $response = $this->client->delete("webhook/{$identifier}/transaction/{$transaction}", null, null, RestClient::AUTH_HTTP_SIG);
504
        return self::jsonDecode($response->body(), true);
505
    }
506
507
    /**
508
     * removes an address transaction event subscription from a webhook
509
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
510
     * @param  string  $address         the address hash of the event subscription
511
     * @return boolean                  true on success
512
     */
513
    public function unsubscribeAddressTransactions($identifier, $address) {
514
        $response = $this->client->delete("webhook/{$identifier}/address-transactions/{$address}", null, null, RestClient::AUTH_HTTP_SIG);
515
        return self::jsonDecode($response->body(), true);
516
    }
517
518
    /**
519
     * removes a block event subscription from a webhook
520
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
521
     * @return boolean                  true on success
522
     */
523
    public function unsubscribeNewBlocks($identifier) {
524
        $response = $this->client->delete("webhook/{$identifier}/block", null, null, RestClient::AUTH_HTTP_SIG);
525
        return self::jsonDecode($response->body(), true);
526
    }
527
528
    /**
529
     * create a new wallet
530
     *   - will generate a new primary seed (with password) and backup seed (without password)
531
     *   - send the primary seed (BIP39 'encrypted') and backup public key to the server
532
     *   - receive the blocktrail co-signing public key from the server
533
     *
534
     * Either takes one argument:
535
     * @param array $options
536
     *
537
     * Or takes three arguments (old, deprecated syntax):
538
     * (@nonPHP-doc) @param      $identifier
539
     * (@nonPHP-doc) @param      $password
540
     * (@nonPHP-doc) @param int  $keyIndex          override for the blocktrail cosigning key to use
0 ignored issues
show
Bug introduced by
There is no parameter named $keyIndex. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
541
     *
542
     * @return array[WalletInterface, array]      list($wallet, $backupInfo)
0 ignored issues
show
Documentation introduced by
The doc-type array[WalletInterface, could not be parsed: Expected "]" at position 2, but found "WalletInterface". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
543
     * @throws \Exception
544
     */
545
    public function createNewWallet($options) {
546
        if (!is_array($options)) {
547
            $args = func_get_args();
548
            $options = [
549
                "identifier" => $args[0],
550
                "password" => $args[1],
551
                "key_index" => isset($args[2]) ? $args[2] : null,
552
            ];
553
        }
554
555
        if (isset($options['password'])) {
556
            if (isset($options['passphrase'])) {
557
                throw new \InvalidArgumentException("Can only provide either passphrase or password");
558
            } else {
559
                $options['passphrase'] = $options['password'];
560
            }
561
        }
562
563
        if (!isset($options['passphrase'])) {
564
            $options['passphrase'] = null;
565
        }
566
567
        if (!isset($options['key_index'])) {
568
            $options['key_index'] = 0;
569
        }
570
571
        if (!isset($options['wallet_version'])) {
572
            $options['wallet_version'] = Wallet::WALLET_VERSION_V3;
573
        }
574
575
        switch ($options['wallet_version']) {
576
            case Wallet::WALLET_VERSION_V1:
577
                return $this->createNewWalletV1($options);
578
579
            case Wallet::WALLET_VERSION_V2:
580
                return $this->createNewWalletV2($options);
581
582
            case Wallet::WALLET_VERSION_V3:
583
                return $this->createNewWalletV3($options);
584
585
            default:
586
                throw new \InvalidArgumentException("Invalid wallet version");
587
        }
588
    }
589
590
    protected function createNewWalletV1($options) {
591
        $walletPath = WalletPath::create($options['key_index']);
592
593
        $storePrimaryMnemonic = isset($options['store_primary_mnemonic']) ? $options['store_primary_mnemonic'] : null;
594
595
        if (isset($options['primary_mnemonic']) && isset($options['primary_private_key'])) {
596
            throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey");
597
        }
598
599
        $primaryMnemonic = null;
600
        $primaryPrivateKey = null;
601
        if (!isset($options['primary_mnemonic']) && !isset($options['primary_private_key'])) {
602
            if (!$options['passphrase']) {
603
                throw new \InvalidArgumentException("Can't generate Primary Mnemonic without a passphrase");
604
            } else {
605
                // create new primary seed
606
                /** @var HierarchicalKey $primaryPrivateKey */
607
                list($primaryMnemonic, , $primaryPrivateKey) = $this->newPrimarySeed($options['passphrase']);
608
                if ($storePrimaryMnemonic !== false) {
609
                    $storePrimaryMnemonic = true;
610
                }
611
            }
612
        } elseif (isset($options['primary_mnemonic'])) {
613
            $primaryMnemonic = $options['primary_mnemonic'];
614
        } elseif (isset($options['primary_private_key'])) {
615
            $primaryPrivateKey = $options['primary_private_key'];
616
        }
617
618
        if ($storePrimaryMnemonic && $primaryMnemonic && !$options['passphrase']) {
619
            throw new \InvalidArgumentException("Can't store Primary Mnemonic on server without a passphrase");
620
        }
621
622 View Code Duplication
        if ($primaryPrivateKey) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
623
            if (is_string($primaryPrivateKey)) {
624
                $primaryPrivateKey = [$primaryPrivateKey, "m"];
625
            }
626
        } else {
627
            $primaryPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($primaryMnemonic, $options['passphrase']));
628
        }
629
630
        if (!$storePrimaryMnemonic) {
631
            $primaryMnemonic = false;
632
        }
633
634
        // create primary public key from the created private key
635
        $path = $walletPath->keyIndexPath()->publicPath();
636
        $primaryPublicKey = BIP32Key::create($primaryPrivateKey, "m")->buildKey($path);
637
638
        if (isset($options['backup_mnemonic']) && $options['backup_public_key']) {
639
            throw new \InvalidArgumentException("Can't specify Backup Mnemonic and Backup PublicKey");
640
        }
641
642
        $backupMnemonic = null;
643
        $backupPublicKey = null;
644
        if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) {
645
            /** @var HierarchicalKey $backupPrivateKey */
646
            list($backupMnemonic, , ) = $this->newBackupSeed();
647
        } else if (isset($options['backup_mnemonic'])) {
648
            $backupMnemonic = $options['backup_mnemonic'];
649
        } elseif (isset($options['backup_public_key'])) {
650
            $backupPublicKey = $options['backup_public_key'];
651
        }
652
653 View Code Duplication
        if ($backupPublicKey) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
654
            if (is_string($backupPublicKey)) {
655
                $backupPublicKey = [$backupPublicKey, "m"];
656
            }
657
        } else {
658
            $backupPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, ""));
659
            $backupPublicKey = BIP32Key::create($backupPrivateKey->toPublic(), "M");
660
        }
661
662
        // create a checksum of our private key which we'll later use to verify we used the right password
663
        $checksum = $primaryPrivateKey->getPublicKey()->getAddress()->getAddress();
664
665
        // send the public keys to the server to store them
666
        //  and the mnemonic, which is safe because it's useless without the password
667
        $data = $this->storeNewWalletV1($options['identifier'], $primaryPublicKey->tuple(), $backupPublicKey->tuple(), $primaryMnemonic, $checksum, $options['key_index']);
668
669
        // received the blocktrail public keys
670 View Code Duplication
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
671
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
672
        }, $data['blocktrail_public_keys']);
673
674
        $wallet = new WalletV1(
675
            $this,
676
            $options['identifier'],
677
            $primaryMnemonic,
678
            [$options['key_index'] => $primaryPublicKey],
679
            $backupPublicKey,
680
            $blocktrailPublicKeys,
681
            $options['key_index'],
682
            $this->network,
683
            $this->testnet,
684
            $checksum
685
        );
686
687
        $wallet->unlock($options);
688
689
        // return wallet and backup mnemonic
690
        return [
691
            $wallet,
692
            [
693
                'primary_mnemonic' => $primaryMnemonic,
694
                'backup_mnemonic' => $backupMnemonic,
695
                'blocktrail_public_keys' => $blocktrailPublicKeys,
696
            ],
697
        ];
698
    }
699
700
    public static function randomBits($bits) {
701
        return self::randomBytes($bits / 8);
702
    }
703
704
    public static function randomBytes($bytes) {
705
        return (new Random())->bytes($bytes)->getBinary();
706
    }
707
708
    protected function createNewWalletV2($options) {
709
        $walletPath = WalletPath::create($options['key_index']);
710
711
        if (isset($options['store_primary_mnemonic'])) {
712
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
713
        }
714
715 View Code Duplication
        if (!isset($options['store_data_on_server'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
716
            if (isset($options['primary_private_key'])) {
717
                $options['store_data_on_server'] = false;
718
            } else {
719
                $options['store_data_on_server'] = true;
720
            }
721
        }
722
723
        $storeDataOnServer = $options['store_data_on_server'];
724
725
        $secret = null;
726
        $encryptedSecret = null;
727
        $primarySeed = null;
728
        $encryptedPrimarySeed = null;
729
        $recoverySecret = null;
730
        $recoveryEncryptedSecret = null;
731
        $backupSeed = null;
732
733
        if (!isset($options['primary_private_key'])) {
734
            $primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : self::randomBits(256);
735
        }
736
737
        if ($storeDataOnServer) {
738
            if (!isset($options['secret'])) {
739
                if (!$options['passphrase']) {
740
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
741
                }
742
743
                $secret = bin2hex(self::randomBits(256)); // string because we use it as passphrase
744
                $encryptedSecret = CryptoJSAES::encrypt($secret, $options['passphrase']);
745
            } else {
746
                $secret = $options['secret'];
747
            }
748
749
            $encryptedPrimarySeed = CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
750
            $recoverySecret = bin2hex(self::randomBits(256));
751
752
            $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);
753
        }
754
755
        if (!isset($options['backup_public_key'])) {
756
            $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : self::randomBits(256);
757
        }
758
759 View Code Duplication
        if (isset($options['primary_private_key'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
760
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
761
        } else {
762
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m");
763
        }
764
765
        // create primary public key from the created private key
766
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
767
768 View Code Duplication
        if (!isset($options['backup_public_key'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
769
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M");
770
        }
771
772
        // create a checksum of our private key which we'll later use to verify we used the right password
773
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
774
775
        // send the public keys and encrypted data to server
776
        $data = $this->storeNewWalletV2(
777
            $options['identifier'],
778
            $options['primary_public_key']->tuple(),
779
            $options['backup_public_key']->tuple(),
0 ignored issues
show
Documentation introduced by
$options['backup_public_key']->tuple() is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
780
            $storeDataOnServer ? $encryptedPrimarySeed : false,
781
            $storeDataOnServer ? $encryptedSecret : false,
782
            $storeDataOnServer ? $recoverySecret : false,
783
            $checksum,
784
            $options['key_index']
785
        );
786
787
        // received the blocktrail public keys
788 View Code Duplication
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
789
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
790
        }, $data['blocktrail_public_keys']);
791
792
        $wallet = new WalletV2(
793
            $this,
794
            $options['identifier'],
795
            $encryptedPrimarySeed,
796
            $encryptedSecret,
797
            [$options['key_index'] => $options['primary_public_key']],
798
            $options['backup_public_key'],
799
            $blocktrailPublicKeys,
800
            $options['key_index'],
801
            $this->network,
802
            $this->testnet,
803
            $checksum
804
        );
805
806
        $wallet->unlock([
807
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
808
            'primary_private_key' => $options['primary_private_key'],
809
            'primary_seed' => $primarySeed,
810
            'secret' => $secret,
811
        ]);
812
813
        // return wallet and mnemonics for backup sheet
814
        return [
815
            $wallet,
816
            [
817
                'encrypted_primary_seed' => $encryptedPrimarySeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedPrimarySeed))) : null,
818
                'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer($backupSeed)) : null,
819
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($recoveryEncryptedSecret))) : null,
820
                'encrypted_secret' => $encryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedSecret))) : null,
821
                'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
822
                    return [$keyIndex, $pubKey->tuple()];
823
                }, $blocktrailPublicKeys),
824
            ],
825
        ];
826
    }
827
828
    protected function createNewWalletV3($options) {
829
        $walletPath = WalletPath::create($options['key_index']);
830
831
        if (isset($options['store_primary_mnemonic'])) {
832
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
833
        }
834
835 View Code Duplication
        if (!isset($options['store_data_on_server'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
836
            if (isset($options['primary_private_key'])) {
837
                $options['store_data_on_server'] = false;
838
            } else {
839
                $options['store_data_on_server'] = true;
840
            }
841
        }
842
843
        $storeDataOnServer = $options['store_data_on_server'];
844
845
        $secret = null;
846
        $encryptedSecret = null;
847
        $primarySeed = null;
848
        $encryptedPrimarySeed = null;
849
        $recoverySecret = null;
850
        $recoveryEncryptedSecret = null;
851
        $backupSeed = null;
852
853 View Code Duplication
        if (!isset($options['primary_private_key'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
854
            if (isset($options['primary_seed'])) {
855
                if (!$options['primary_seed'] instanceof BufferInterface) {
856
                    throw new \InvalidArgumentException('Primary Seed should be passed as a Buffer');
857
                }
858
                $primarySeed = $options['primary_seed'];
859
            } else {
860
                $primarySeed = new Buffer(self::randomBits(256));
861
            }
862
        }
863
864
        if ($storeDataOnServer) {
865
            if (!isset($options['secret'])) {
866
                if (!$options['passphrase']) {
867
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
868
                }
869
870
                $secret = new Buffer(self::randomBits(256));
871
                $encryptedSecret = Encryption::encrypt($secret, new Buffer($options['passphrase']), KeyDerivation::DEFAULT_ITERATIONS);
872
            } else {
873
                if (!$options['secret'] instanceof Buffer) {
874
                    throw new \RuntimeException('Secret must be provided as a Buffer');
875
                }
876
877
                $secret = $options['secret'];
878
            }
879
880
            $encryptedPrimarySeed = Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS);
881
            $recoverySecret = new Buffer(self::randomBits(256));
882
883
            $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS);
884
        }
885
886 View Code Duplication
        if (!isset($options['backup_public_key'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
887
            if (isset($options['backup_seed'])) {
888
                if (!$options['backup_seed'] instanceof Buffer) {
889
                    throw new \RuntimeException('Backup seed must be an instance of Buffer');
890
                }
891
                $backupSeed = $options['backup_seed'];
892
            } else {
893
                $backupSeed = new Buffer(self::randomBits(256));
894
            }
895
        }
896
897 View Code Duplication
        if (isset($options['primary_private_key'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
898
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
899
        } else {
900
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m");
901
        }
902
903
        // create primary public key from the created private key
904
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
905
906 View Code Duplication
        if (!isset($options['backup_public_key'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
907
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($backupSeed), "m")->buildKey("M");
908
        }
909
910
        // create a checksum of our private key which we'll later use to verify we used the right password
911
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
912
913
        // send the public keys and encrypted data to server
914
        $data = $this->storeNewWalletV3(
915
            $options['identifier'],
916
            $options['primary_public_key']->tuple(),
917
            $options['backup_public_key']->tuple(),
0 ignored issues
show
Documentation introduced by
$options['backup_public_key']->tuple() is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
918
            $storeDataOnServer ? base64_encode($encryptedPrimarySeed->getBinary()) : false,
919
            $storeDataOnServer ? base64_encode($encryptedSecret->getBinary()) : false,
920
            $storeDataOnServer ? $recoverySecret->getHex() : false,
921
            $checksum,
922
            $options['key_index']
923
        );
924
925
        // received the blocktrail public keys
926
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
927
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
928
        }, $data['blocktrail_public_keys']);
929
930
        $wallet = new WalletV3(
931
            $this,
932
            $options['identifier'],
933
            $encryptedPrimarySeed,
0 ignored issues
show
Bug introduced by
It seems like $encryptedPrimarySeed defined by null on line 848 can be null; however, Blocktrail\SDK\WalletV3::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
934
            $encryptedSecret,
0 ignored issues
show
Bug introduced by
It seems like $encryptedSecret defined by null on line 846 can be null; however, Blocktrail\SDK\WalletV3::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
935
            [$options['key_index'] => $options['primary_public_key']],
936
            $options['backup_public_key'],
937
            $blocktrailPublicKeys,
938
            $options['key_index'],
939
            $this->network,
940
            $this->testnet,
941
            $checksum
942
        );
943
944
        $wallet->unlock([
945
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
946
            'primary_private_key' => $options['primary_private_key'],
947
            'primary_seed' => $primarySeed,
948
            'secret' => $secret,
949
        ]);
950
951
        // return wallet and mnemonics for backup sheet
952
        return [
953
            $wallet,
954
            [
955
                'encrypted_primary_seed'    => $encryptedPrimarySeed ? EncryptionMnemonic::encode($encryptedPrimarySeed) : null,
956
                'backup_seed'               => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic($backupSeed) : null,
957
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? EncryptionMnemonic::encode($recoveryEncryptedSecret) : null,
958
                'encrypted_secret'          => $encryptedSecret ? EncryptionMnemonic::encode($encryptedSecret) : null,
959
                'blocktrail_public_keys'    => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
960
                    return [$keyIndex, $pubKey->tuple()];
961
                }, $blocktrailPublicKeys),
962
            ]
963
        ];
964
    }
965
966
    /**
967
     * @param array $bip32Key
968
     * @throws BlocktrailSDKException
969
     */
970
    private function verifyPublicBIP32Key(array $bip32Key) {
971
        $hk = HierarchicalKeyFactory::fromExtended($bip32Key[0]);
972
        if ($hk->isPrivate()) {
973
            throw new BlocktrailSDKException('Private key was included in request, abort');
974
        }
975
976
        if (substr($bip32Key[1], 0, 1) === "m") {
977
            throw new BlocktrailSDKException("Private path was included in the request, abort");
978
        }
979
    }
980
981
    /**
982
     * @param array $walletData
983
     * @throws BlocktrailSDKException
984
     */
985
    private function verifyPublicOnly(array $walletData) {
986
        $this->verifyPublicBIP32Key($walletData['primary_public_key']);
987
        $this->verifyPublicBIP32Key($walletData['backup_public_key']);
988
    }
989
990
    /**
991
     * create wallet using the API
992
     *
993
     * @param string    $identifier             the wallet identifier to create
994
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
995
     * @param string    $backupPublicKey        plain public key
996
     * @param string    $primaryMnemonic        mnemonic to store
997
     * @param string    $checksum               checksum to store
998
     * @param int       $keyIndex               account that we expect to use
999
     * @return mixed
1000
     */
1001
    public function storeNewWalletV1($identifier, $primaryPublicKey, $backupPublicKey, $primaryMnemonic, $checksum, $keyIndex) {
1002
        $data = [
1003
            'identifier' => $identifier,
1004
            'primary_public_key' => $primaryPublicKey,
1005
            'backup_public_key' => $backupPublicKey,
1006
            'primary_mnemonic' => $primaryMnemonic,
1007
            'checksum' => $checksum,
1008
            'key_index' => $keyIndex
1009
        ];
1010
        $this->verifyPublicOnly($data);
1011
        $response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1012
        return self::jsonDecode($response->body(), true);
1013
    }
1014
1015
    /**
1016
     * create wallet using the API
1017
     *
1018
     * @param string $identifier       the wallet identifier to create
1019
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1020
     * @param string $backupPublicKey  plain public key
1021
     * @param        $encryptedPrimarySeed
1022
     * @param        $encryptedSecret
1023
     * @param        $recoverySecret
1024
     * @param string $checksum         checksum to store
1025
     * @param int    $keyIndex         account that we expect to use
1026
     * @return mixed
1027
     * @throws \Exception
1028
     */
1029 View Code Duplication
    public function storeNewWalletV2($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1030
        $data = [
1031
            'identifier' => $identifier,
1032
            'wallet_version' => Wallet::WALLET_VERSION_V2,
1033
            'primary_public_key' => $primaryPublicKey,
1034
            'backup_public_key' => $backupPublicKey,
1035
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1036
            'encrypted_secret' => $encryptedSecret,
1037
            'recovery_secret' => $recoverySecret,
1038
            'checksum' => $checksum,
1039
            'key_index' => $keyIndex
1040
        ];
1041
        $this->verifyPublicOnly($data);
1042
        $response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1043
        return self::jsonDecode($response->body(), true);
1044
    }
1045
1046
    /**
1047
     * create wallet using the API
1048
     *
1049
     * @param string $identifier       the wallet identifier to create
1050
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1051
     * @param string $backupPublicKey  plain public key
1052
     * @param        $encryptedPrimarySeed
1053
     * @param        $encryptedSecret
1054
     * @param        $recoverySecret
1055
     * @param string $checksum         checksum to store
1056
     * @param int    $keyIndex         account that we expect to use
1057
     * @return mixed
1058
     * @throws \Exception
1059
     */
1060 View Code Duplication
    public function storeNewWalletV3($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1061
1062
        $data = [
1063
            'identifier' => $identifier,
1064
            'wallet_version' => Wallet::WALLET_VERSION_V3,
1065
            'primary_public_key' => $primaryPublicKey,
1066
            'backup_public_key' => $backupPublicKey,
1067
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1068
            'encrypted_secret' => $encryptedSecret,
1069
            'recovery_secret' => $recoverySecret,
1070
            'checksum' => $checksum,
1071
            'key_index' => $keyIndex
1072
        ];
1073
1074
        $this->verifyPublicOnly($data);
1075
        $response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1076
        return self::jsonDecode($response->body(), true);
1077
    }
1078
1079
    /**
1080
     * upgrade wallet to use a new account number
1081
     *  the account number specifies which blocktrail cosigning key is used
1082
     *
1083
     * @param string    $identifier             the wallet identifier to be upgraded
1084
     * @param int       $keyIndex               the new account to use
1085
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1086
     * @return mixed
1087
     */
1088
    public function upgradeKeyIndex($identifier, $keyIndex, $primaryPublicKey) {
1089
        $data = [
1090
            'key_index' => $keyIndex,
1091
            'primary_public_key' => $primaryPublicKey
1092
        ];
1093
1094
        $response = $this->client->post("wallet/{$identifier}/upgrade", null, $data, RestClient::AUTH_HTTP_SIG);
1095
        return self::jsonDecode($response->body(), true);
1096
    }
1097
1098
    /**
1099
     * initialize a previously created wallet
1100
     *
1101
     * Either takes one argument:
1102
     * @param array $options
1103
     *
1104
     * Or takes two arguments (old, deprecated syntax):
1105
     * (@nonPHP-doc) @param string    $identifier             the wallet identifier to be initialized
0 ignored issues
show
Bug introduced by
There is no parameter named $identifier. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1106
     * (@nonPHP-doc) @param string    $password               the password to decrypt the mnemonic with
0 ignored issues
show
Bug introduced by
There is no parameter named $password. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1107
     *
1108
     * @return WalletInterface
1109
     * @throws \Exception
1110
     */
1111
    public function initWallet($options) {
1112
        if (!is_array($options)) {
1113
            $args = func_get_args();
1114
            $options = [
1115
                "identifier" => $args[0],
1116
                "password" => $args[1],
1117
            ];
1118
        }
1119
1120
        $identifier = $options['identifier'];
1121
        $readonly = isset($options['readonly']) ? $options['readonly'] :
1122
                    (isset($options['readOnly']) ? $options['readOnly'] :
1123
                        (isset($options['read-only']) ? $options['read-only'] :
1124
                            false));
1125
1126
        // get the wallet data from the server
1127
        $data = $this->getWallet($identifier);
1128
1129
        if (!$data) {
1130
            throw new \Exception("Failed to get wallet");
1131
        }
1132
1133
        switch ($data['wallet_version']) {
1134
            case Wallet::WALLET_VERSION_V1:
1135
                $wallet = new WalletV1(
1136
                    $this,
1137
                    $identifier,
1138
                    isset($options['primary_mnemonic']) ? $options['primary_mnemonic'] : $data['primary_mnemonic'],
1139
                    $data['primary_public_keys'],
1140
                    $data['backup_public_key'],
1141
                    $data['blocktrail_public_keys'],
1142
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1143
                    $this->network,
1144
                    $this->testnet,
1145
                    $data['checksum']
1146
                );
1147
                break;
1148
            case Wallet::WALLET_VERSION_V2:
1149
                $wallet = new WalletV2(
1150
                    $this,
1151
                    $identifier,
1152
                    isset($options['encrypted_primary_seed']) ? $options['encrypted_primary_seed'] : $data['encrypted_primary_seed'],
1153
                    isset($options['encrypted_secret']) ? $options['encrypted_secret'] : $data['encrypted_secret'],
1154
                    $data['primary_public_keys'],
1155
                    $data['backup_public_key'],
1156
                    $data['blocktrail_public_keys'],
1157
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1158
                    $this->network,
1159
                    $this->testnet,
1160
                    $data['checksum']
1161
                );
1162
                break;
1163
            case Wallet::WALLET_VERSION_V3:
1164 View Code Duplication
                if (isset($options['encrypted_primary_seed'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1165
                    if (!$options['encrypted_primary_seed'] instanceof Buffer) {
1166
                        throw new \InvalidArgumentException('Encrypted PrimarySeed must be provided as a Buffer');
1167
                    }
1168
                    $encryptedPrimarySeed = $data['encrypted_primary_seed'];
1169
                } else {
1170
                    $encryptedPrimarySeed = new Buffer(base64_decode($data['encrypted_primary_seed']));
1171
                }
1172
1173 View Code Duplication
                if (isset($options['encrypted_secret'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1174
                    if (!$options['encrypted_secret'] instanceof Buffer) {
1175
                        throw new \InvalidArgumentException('Encrypted secret must be provided as a Buffer');
1176
                    }
1177
1178
                    $encryptedSecret = $data['encrypted_secret'];
1179
                } else {
1180
                    $encryptedSecret = new Buffer(base64_decode($data['encrypted_secret']));
1181
                }
1182
1183
                $wallet = new WalletV3(
1184
                    $this,
1185
                    $identifier,
1186
                    $encryptedPrimarySeed,
1187
                    $encryptedSecret,
1188
                    $data['primary_public_keys'],
1189
                    $data['backup_public_key'],
1190
                    $data['blocktrail_public_keys'],
1191
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1192
                    $this->network,
1193
                    $this->testnet,
1194
                    $data['checksum']
1195
                );
1196
                break;
1197
            default:
1198
                throw new \InvalidArgumentException("Invalid wallet version");
1199
        }
1200
1201
        if (!$readonly) {
1202
            $wallet->unlock($options);
1203
        }
1204
1205
        return $wallet;
1206
    }
1207
1208
    /**
1209
     * get the wallet data from the server
1210
     *
1211
     * @param string    $identifier             the identifier of the wallet
1212
     * @return mixed
1213
     */
1214
    public function getWallet($identifier) {
1215
        $response = $this->client->get("wallet/{$identifier}", null, RestClient::AUTH_HTTP_SIG);
1216
        return self::jsonDecode($response->body(), true);
1217
    }
1218
1219
    /**
1220
     * update the wallet data on the server
1221
     *
1222
     * @param string    $identifier
1223
     * @param $data
1224
     * @return mixed
1225
     */
1226
    public function updateWallet($identifier, $data) {
1227
        $response = $this->client->post("wallet/{$identifier}", null, $data, RestClient::AUTH_HTTP_SIG);
1228
        return self::jsonDecode($response->body(), true);
1229
    }
1230
1231
    /**
1232
     * delete a wallet from the server
1233
     *  the checksum address and a signature to verify you ownership of the key of that checksum address
1234
     *  is required to be able to delete a wallet
1235
     *
1236
     * @param string    $identifier             the identifier of the wallet
1237
     * @param string    $checksumAddress        the address for your master private key (and the checksum used when creating the wallet)
1238
     * @param string    $signature              a signature of the checksum address as message signed by the private key matching that address
1239
     * @param bool      $force                  ignore warnings (such as a non-zero balance)
1240
     * @return mixed
1241
     */
1242
    public function deleteWallet($identifier, $checksumAddress, $signature, $force = false) {
1243
        $response = $this->client->delete("wallet/{$identifier}", ['force' => $force], [
1244
            'checksum' => $checksumAddress,
1245
            'signature' => $signature
1246
        ], RestClient::AUTH_HTTP_SIG, 360);
1247
        return self::jsonDecode($response->body(), true);
1248
    }
1249
1250
    /**
1251
     * create new backup key;
1252
     *  1) a BIP39 mnemonic
1253
     *  2) a seed from that mnemonic with a blank password
1254
     *  3) a private key from that seed
1255
     *
1256
     * @return array [mnemonic, seed, key]
1257
     */
1258
    protected function newBackupSeed() {
1259
        list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed("");
1260
1261
        return [$backupMnemonic, $backupSeed, $backupPrivateKey];
1262
    }
1263
1264
    /**
1265
     * create new primary key;
1266
     *  1) a BIP39 mnemonic
1267
     *  2) a seed from that mnemonic with the password
1268
     *  3) a private key from that seed
1269
     *
1270
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1271
     * @return array [mnemonic, seed, key]
1272
     * @TODO: require a strong password?
1273
     */
1274
    protected function newPrimarySeed($passphrase) {
1275
        list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase);
1276
1277
        return [$primaryMnemonic, $primarySeed, $primaryPrivateKey];
1278
    }
1279
1280
    /**
1281
     * create a new key;
1282
     *  1) a BIP39 mnemonic
1283
     *  2) a seed from that mnemonic with the password
1284
     *  3) a private key from that seed
1285
     *
1286
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1287
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1288
     * @return array
1289
     */
1290
    protected function generateNewSeed($passphrase = "", $forceEntropy = null) {
1291
        // generate master seed, retry if the generated private key isn't valid (FALSE is returned)
1292
        do {
1293
            $mnemonic = $this->generateNewMnemonic($forceEntropy);
1294
1295
            $seed = (new Bip39SeedGenerator)->getSeed($mnemonic, $passphrase);
1296
1297
            $key = null;
1298
            try {
1299
                $key = HierarchicalKeyFactory::fromEntropy($seed);
1300
            } catch (\Exception $e) {
1301
                // try again
1302
            }
1303
        } while (!$key);
1304
1305
        return [$mnemonic, $seed, $key];
1306
    }
1307
1308
    /**
1309
     * generate a new mnemonic from some random entropy (512 bit)
1310
     *
1311
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1312
     * @return string
1313
     * @throws \Exception
1314
     */
1315
    protected function generateNewMnemonic($forceEntropy = null) {
1316
        if ($forceEntropy === null) {
1317
            $random = new Random();
1318
            $entropy = $random->bytes(512 / 8);
1319
        } else {
1320
            $entropy = $forceEntropy;
1321
        }
1322
1323
        return MnemonicFactory::bip39()->entropyToMnemonic($entropy);
0 ignored issues
show
Bug introduced by
It seems like $entropy defined by $forceEntropy on line 1320 can also be of type string; however, BitWasp\Bitcoin\Mnemonic...ic::entropyToMnemonic() does only seem to accept object<BitWasp\Buffertools\BufferInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1324
    }
1325
1326
    /**
1327
     * get the balance for the wallet
1328
     *
1329
     * @param string    $identifier             the identifier of the wallet
1330
     * @return array
1331
     */
1332
    public function getWalletBalance($identifier) {
1333
        $response = $this->client->get("wallet/{$identifier}/balance", null, RestClient::AUTH_HTTP_SIG);
1334
        return self::jsonDecode($response->body(), true);
1335
    }
1336
1337
    /**
1338
     * do HD wallet discovery for the wallet
1339
     *
1340
     * this can be REALLY slow, so we've set the timeout to 120s ...
1341
     *
1342
     * @param string    $identifier             the identifier of the wallet
1343
     * @param int       $gap                    the gap setting to use for discovery
1344
     * @return mixed
1345
     */
1346
    public function doWalletDiscovery($identifier, $gap = 200) {
1347
        $response = $this->client->get("wallet/{$identifier}/discovery", ['gap' => $gap], RestClient::AUTH_HTTP_SIG, 360.0);
1348
        return self::jsonDecode($response->body(), true);
1349
    }
1350
1351
    /**
1352
     * get a new derivation number for specified parent path
1353
     *  eg; m/44'/1'/0/0 results in m/44'/1'/0/0/0 and next time in m/44'/1'/0/0/1 and next time in m/44'/1'/0/0/2
1354
     *
1355
     * returns the path
1356
     *
1357
     * @param string    $identifier             the identifier of the wallet
1358
     * @param string    $path                   the parent path for which to get a new derivation
1359
     * @return string
1360
     */
1361
    public function getNewDerivation($identifier, $path) {
1362
        $result = $this->_getNewDerivation($identifier, $path);
1363
        return $result['path'];
1364
    }
1365
1366
    /**
1367
     * get a new derivation number for specified parent path
1368
     *  eg; m/44'/1'/0/0 results in m/44'/1'/0/0/0 and next time in m/44'/1'/0/0/1 and next time in m/44'/1'/0/0/2
1369
     *
1370
     * @param string    $identifier             the identifier of the wallet
1371
     * @param string    $path                   the parent path for which to get a new derivation
1372
     * @return mixed
1373
     */
1374
    public function _getNewDerivation($identifier, $path) {
1375
        $response = $this->client->post("wallet/{$identifier}/path", null, ['path' => $path], RestClient::AUTH_HTTP_SIG);
1376
        return self::jsonDecode($response->body(), true);
1377
    }
1378
1379
    /**
1380
     * get the path (and redeemScript) to specified address
1381
     *
1382
     * @param string $identifier
1383
     * @param string $address
1384
     * @return array
1385
     * @throws \Exception
1386
     */
1387
    public function getPathForAddress($identifier, $address) {
1388
        $response = $this->client->post("wallet/{$identifier}/path_for_address", null, ['address' => $address], RestClient::AUTH_HTTP_SIG);
1389
        return self::jsonDecode($response->body(), true)['path'];
1390
    }
1391
1392
    /**
1393
     * send the transaction using the API
1394
     *
1395
     * @param string    $identifier             the identifier of the wallet
1396
     * @param string    $rawTransaction         raw hex of the transaction (should be partially signed)
1397
     * @param array     $paths                  list of the paths that were used for the UTXO
1398
     * @param bool      $checkFee               let the server verify the fee after signing
1399
     * @return string                           the complete raw transaction
1400
     * @throws \Exception
1401
     */
1402
    public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee = false) {
1403
        $data = [
1404
            'raw_transaction' => $rawTransaction,
1405
            'paths' => $paths
1406
        ];
1407
1408
        // dynamic TTL for when we're signing really big transactions
1409
        $ttl = max(5.0, count($paths) * 0.25) + 4.0;
1410
1411
        $response = $this->client->post("wallet/{$identifier}/send", ['check_fee' => (int)!!$checkFee], $data, RestClient::AUTH_HTTP_SIG, $ttl);
1412
        $signed = self::jsonDecode($response->body(), true);
1413
1414
        if (!$signed['complete'] || $signed['complete'] == 'false') {
1415
            throw new \Exception("Failed to completely sign transaction");
1416
        }
1417
1418
        // create TX hash from the raw signed hex
1419
        return TransactionFactory::fromHex($signed['hex'])->getTxId()->getHex();
1420
    }
1421
1422
    /**
1423
     * use the API to get the best inputs to use based on the outputs
1424
     *
1425
     * the return array has the following format:
1426
     * [
1427
     *  "utxos" => [
1428
     *      [
1429
     *          "hash" => "<txHash>",
1430
     *          "idx" => "<index of the output of that <txHash>",
1431
     *          "scriptpubkey_hex" => "<scriptPubKey-hex>",
1432
     *          "value" => 32746327,
1433
     *          "address" => "1address",
1434
     *          "path" => "m/44'/1'/0'/0/13",
1435
     *          "redeem_script" => "<redeemScript-hex>",
1436
     *      ],
1437
     *  ],
1438
     *  "fee"   => 10000,
1439
     *  "change"=> 1010109201,
1440
     * ]
1441
     *
1442
     * @param string   $identifier              the identifier of the wallet
1443
     * @param array    $outputs                 the outputs you want to create - array[address => satoshi-value]
1444
     * @param bool     $lockUTXO                when TRUE the UTXOs selected will be locked for a few seconds
1445
     *                                          so you have some time to spend them without race-conditions
1446
     * @param bool     $allowZeroConf
1447
     * @param string   $feeStrategy
1448
     * @param null|int $forceFee
1449
     * @return array
1450
     * @throws \Exception
1451
     */
1452 View Code Duplication
    public function coinSelection($identifier, $outputs, $lockUTXO = false, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1453
        $args = [
1454
            'lock' => (int)!!$lockUTXO,
1455
            'zeroconf' => (int)!!$allowZeroConf,
1456
            'fee_strategy' => $feeStrategy,
1457
        ];
1458
1459
        if ($forceFee !== null) {
1460
            $args['forcefee'] = (int)$forceFee;
1461
        }
1462
1463
        $response = $this->client->post(
1464
            "wallet/{$identifier}/coin-selection",
1465
            $args,
1466
            $outputs,
1467
            RestClient::AUTH_HTTP_SIG
1468
        );
1469
1470
        return self::jsonDecode($response->body(), true);
1471
    }
1472
1473
    /**
1474
     *
1475
     * @param string   $identifier the identifier of the wallet
1476
     * @param bool     $allowZeroConf
1477
     * @param string   $feeStrategy
1478
     * @param null|int $forceFee
1479
     * @param int      $outputCnt
1480
     * @return array
1481
     * @throws \Exception
1482
     */
1483 View Code Duplication
    public function walletMaxSpendable($identifier, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1484
        $args = [
1485
            'zeroconf' => (int)!!$allowZeroConf,
1486
            'fee_strategy' => $feeStrategy,
1487
            'outputs' => $outputCnt,
1488
        ];
1489
1490
        if ($forceFee !== null) {
1491
            $args['forcefee'] = (int)$forceFee;
1492
        }
1493
1494
        $response = $this->client->get(
1495
            "wallet/{$identifier}/max-spendable",
1496
            $args,
1497
            RestClient::AUTH_HTTP_SIG
1498
        );
1499
1500
        return self::jsonDecode($response->body(), true);
1501
    }
1502
1503
    /**
1504
     * @return array        ['optimal_fee' => 10000, 'low_priority_fee' => 5000]
1505
     */
1506
    public function feePerKB() {
1507
        $response = $this->client->get("fee-per-kb");
1508
        return self::jsonDecode($response->body(), true);
1509
    }
1510
1511
    /**
1512
     * get the current price index
1513
     *
1514
     * @return array        eg; ['USD' => 287.30]
1515
     */
1516
    public function price() {
1517
        $response = $this->client->get("price");
1518
        return self::jsonDecode($response->body(), true);
1519
    }
1520
1521
    /**
1522
     * setup webhook for wallet
1523
     *
1524
     * @param string    $identifier         the wallet identifier for which to create the webhook
1525
     * @param string    $webhookIdentifier  the webhook identifier to use
1526
     * @param string    $url                the url to receive the webhook events
1527
     * @return array
1528
     */
1529
    public function setupWalletWebhook($identifier, $webhookIdentifier, $url) {
1530
        $response = $this->client->post("wallet/{$identifier}/webhook", null, ['url' => $url, 'identifier' => $webhookIdentifier], RestClient::AUTH_HTTP_SIG);
1531
        return self::jsonDecode($response->body(), true);
1532
    }
1533
1534
    /**
1535
     * delete webhook for wallet
1536
     *
1537
     * @param string    $identifier         the wallet identifier for which to delete the webhook
1538
     * @param string    $webhookIdentifier  the webhook identifier to delete
1539
     * @return array
1540
     */
1541
    public function deleteWalletWebhook($identifier, $webhookIdentifier) {
1542
        $response = $this->client->delete("wallet/{$identifier}/webhook/{$webhookIdentifier}", null, null, RestClient::AUTH_HTTP_SIG);
1543
        return self::jsonDecode($response->body(), true);
1544
    }
1545
1546
    /**
1547
     * lock a specific unspent output
1548
     *
1549
     * @param     $identifier
1550
     * @param     $txHash
1551
     * @param     $txIdx
1552
     * @param int $ttl
1553
     * @return bool
1554
     */
1555
    public function lockWalletUTXO($identifier, $txHash, $txIdx, $ttl = 3) {
1556
        $response = $this->client->post("wallet/{$identifier}/lock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx, 'ttl' => $ttl], RestClient::AUTH_HTTP_SIG);
1557
        return self::jsonDecode($response->body(), true)['locked'];
1558
    }
1559
1560
    /**
1561
     * unlock a specific unspent output
1562
     *
1563
     * @param     $identifier
1564
     * @param     $txHash
1565
     * @param     $txIdx
1566
     * @return bool
1567
     */
1568
    public function unlockWalletUTXO($identifier, $txHash, $txIdx) {
1569
        $response = $this->client->post("wallet/{$identifier}/unlock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx], RestClient::AUTH_HTTP_SIG);
1570
        return self::jsonDecode($response->body(), true)['unlocked'];
1571
    }
1572
1573
    /**
1574
     * get all transactions for wallet (paginated)
1575
     *
1576
     * @param  string  $identifier  the wallet identifier for which to get transactions
1577
     * @param  integer $page        pagination: page number
1578
     * @param  integer $limit       pagination: records per page (max 500)
1579
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1580
     * @return array                associative array containing the response
1581
     */
1582 View Code Duplication
    public function walletTransactions($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1583
        $queryString = [
1584
            'page' => $page,
1585
            'limit' => $limit,
1586
            'sort_dir' => $sortDir
1587
        ];
1588
        $response = $this->client->get("wallet/{$identifier}/transactions", $queryString, RestClient::AUTH_HTTP_SIG);
1589
        return self::jsonDecode($response->body(), true);
1590
    }
1591
1592
    /**
1593
     * get all addresses for wallet (paginated)
1594
     *
1595
     * @param  string  $identifier  the wallet identifier for which to get addresses
1596
     * @param  integer $page        pagination: page number
1597
     * @param  integer $limit       pagination: records per page (max 500)
1598
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1599
     * @return array                associative array containing the response
1600
     */
1601 View Code Duplication
    public function walletAddresses($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1602
        $queryString = [
1603
            'page' => $page,
1604
            'limit' => $limit,
1605
            'sort_dir' => $sortDir
1606
        ];
1607
        $response = $this->client->get("wallet/{$identifier}/addresses", $queryString, RestClient::AUTH_HTTP_SIG);
1608
        return self::jsonDecode($response->body(), true);
1609
    }
1610
1611
    /**
1612
     * get all UTXOs for wallet (paginated)
1613
     *
1614
     * @param  string  $identifier  the wallet identifier for which to get addresses
1615
     * @param  integer $page        pagination: page number
1616
     * @param  integer $limit       pagination: records per page (max 500)
1617
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1618
     * @param  boolean $zeroconf    include zero confirmation transactions
1619
     * @return array                associative array containing the response
1620
     */
1621
    public function walletUTXOs($identifier, $page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1622
        $queryString = [
1623
            'page' => $page,
1624
            'limit' => $limit,
1625
            'sort_dir' => $sortDir,
1626
            'zeroconf' => (int)!!$zeroconf,
1627
        ];
1628
        $response = $this->client->get("wallet/{$identifier}/utxos", $queryString, RestClient::AUTH_HTTP_SIG);
1629
        return self::jsonDecode($response->body(), true);
1630
    }
1631
1632
    /**
1633
     * get a paginated list of all wallets associated with the api user
1634
     *
1635
     * @param  integer          $page    pagination: page number
1636
     * @param  integer          $limit   pagination: records per page
1637
     * @return array                     associative array containing the response
1638
     */
1639 View Code Duplication
    public function allWallets($page = 1, $limit = 20) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1640
        $queryString = [
1641
            'page' => $page,
1642
            'limit' => $limit
1643
        ];
1644
        $response = $this->client->get("wallets", $queryString, RestClient::AUTH_HTTP_SIG);
1645
        return self::jsonDecode($response->body(), true);
1646
    }
1647
1648
    /**
1649
     * send raw transaction
1650
     *
1651
     * @param     $txHex
1652
     * @return bool
1653
     */
1654
    public function sendRawTransaction($txHex) {
1655
        $response = $this->client->post("send-raw-tx", null, ['hex' => $txHex], RestClient::AUTH_HTTP_SIG);
1656
        return self::jsonDecode($response->body(), true);
1657
    }
1658
1659
    /**
1660
     * testnet only ;-)
1661
     *
1662
     * @param     $address
1663
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1664
     * @return mixed
1665
     * @throws \Exception
1666
     */
1667 View Code Duplication
    public function faucetWithdrawal($address, $amount = 10000) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1668
        $response = $this->client->post("faucet/withdrawl", null, [
1669
            'address' => $address,
1670
            'amount' => $amount,
1671
        ], RestClient::AUTH_HTTP_SIG);
1672
        return self::jsonDecode($response->body(), true);
1673
    }
1674
1675
    /**
1676
     * Exists for BC. Remove at major bump.
1677
     *
1678
     * @see faucetWithdrawal
1679
     * @deprecated
1680
     * @param     $address
1681
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1682
     * @return mixed
1683
     * @throws \Exception
1684
     */
1685
    public function faucetWithdrawl($address, $amount = 10000) {
1686
        return $this->faucetWithdrawal($address, $amount);
1687
    }
1688
1689
    /**
1690
     * verify a message signed bitcoin-core style
1691
     *
1692
     * @param  string           $message
1693
     * @param  string           $address
1694
     * @param  string           $signature
1695
     * @return boolean
1696
     */
1697
    public function verifyMessage($message, $address, $signature) {
1698
        // we could also use the API instead of the using BitcoinLib to verify
1699
        // $this->client->post("verify_message", null, ['message' => $message, 'address' => $address, 'signature' => $signature])['result'];
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1700
1701
        $adapter = Bitcoin::getEcAdapter();
1702
        $addr = AddressFactory::fromString($address);
1703
        if (!$addr instanceof PayToPubKeyHashAddress) {
1704
            throw new \RuntimeException('Can only verify a message with a pay-to-pubkey-hash address');
1705
        }
1706
1707
        /** @var CompactSignatureSerializerInterface $csSerializer */
1708
        $csSerializer = EcSerializer::getSerializer(CompactSignatureSerializerInterface::class, $adapter);
0 ignored issues
show
Documentation introduced by
$adapter is of type object<BitWasp\Bitcoin\C...ter\EcAdapterInterface>, but the function expects a boolean|object<BitWasp\B...\Crypto\EcAdapter\true>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1709
        $signedMessage = new SignedMessage($message, $csSerializer->parse(new Buffer(base64_decode($signature))));
1710
1711
        $signer = new MessageSigner($adapter);
1712
        return $signer->verify($signedMessage, $addr);
1713
    }
1714
1715
    /**
1716
     * convert a Satoshi value to a BTC value
1717
     *
1718
     * @param int       $satoshi
1719
     * @return float
1720
     */
1721
    public static function toBTC($satoshi) {
1722
        return bcdiv((int)(string)$satoshi, 100000000, 8);
1723
    }
1724
1725
    /**
1726
     * convert a Satoshi value to a BTC value and return it as a string
1727
1728
     * @param int       $satoshi
1729
     * @return string
1730
     */
1731
    public static function toBTCString($satoshi) {
1732
        return sprintf("%.8f", self::toBTC($satoshi));
1733
    }
1734
1735
    /**
1736
     * convert a BTC value to a Satoshi value
1737
     *
1738
     * @param float     $btc
1739
     * @return string
1740
     */
1741
    public static function toSatoshiString($btc) {
1742
        return bcmul(sprintf("%.8f", (float)$btc), 100000000, 0);
1743
    }
1744
1745
    /**
1746
     * convert a BTC value to a Satoshi value
1747
     *
1748
     * @param float     $btc
1749
     * @return string
1750
     */
1751
    public static function toSatoshi($btc) {
1752
        return (int)self::toSatoshiString($btc);
1753
    }
1754
1755
    /**
1756
     * json_decode helper that throws exceptions when it fails to decode
1757
     *
1758
     * @param      $json
1759
     * @param bool $assoc
1760
     * @return mixed
1761
     * @throws \Exception
1762
     */
1763
    protected static function jsonDecode($json, $assoc = false) {
1764
        if (!$json) {
1765
            throw new \Exception("Can't json_decode empty string [{$json}]");
1766
        }
1767
1768
        $data = json_decode($json, $assoc);
1769
1770
        if ($data === null) {
1771
            throw new \Exception("Failed to json_decode [{$json}]");
1772
        }
1773
1774
        return $data;
1775
    }
1776
1777
    /**
1778
     * sort public keys for multisig script
1779
     *
1780
     * @param PublicKeyInterface[] $pubKeys
1781
     * @return PublicKeyInterface[]
1782
     */
1783
    public static function sortMultisigKeys(array $pubKeys) {
1784
        $result = array_values($pubKeys);
1785
        usort($result, function (PublicKeyInterface $a, PublicKeyInterface $b) {
1786
            $av = $a->getHex();
1787
            $bv = $b->getHex();
1788
            return $av == $bv ? 0 : $av > $bv ? 1 : -1;
1789
        });
1790
1791
        return $result;
1792
    }
1793
1794
    /**
1795
     * read and decode the json payload from a webhook's POST request.
1796
     *
1797
     * @param bool $returnObject    flag to indicate if an object or associative array should be returned
1798
     * @return mixed|null
1799
     * @throws \Exception
1800
     */
1801
    public static function getWebhookPayload($returnObject = false) {
1802
        $data = file_get_contents("php://input");
1803
        if ($data) {
1804
            return self::jsonDecode($data, !$returnObject);
1805
        } else {
1806
            return null;
1807
        }
1808
    }
1809
1810
    public static function normalizeBIP32KeyArray($keys) {
1811
        return Util::arrayMapWithIndex(function ($idx, $key) {
1812
            return [$idx, self::normalizeBIP32Key($key)];
1813
        }, $keys);
1814
    }
1815
1816
    public static function normalizeBIP32Key($key) {
1817
        if ($key instanceof BIP32Key) {
1818
            return $key;
1819
        }
1820
1821
        if (is_array($key)) {
1822
            $path = $key[1];
1823
            $key = $key[0];
1824
1825
            if (!($key instanceof HierarchicalKey)) {
1826
                $key = HierarchicalKeyFactory::fromExtended($key);
1827
            }
1828
1829
            return BIP32Key::create($key, $path);
1830
        } else {
1831
            throw new \Exception("Bad Input");
1832
        }
1833
    }
1834
}
1835