Completed
Branch master (56c2f5)
by
unknown
07:05
created

BlocktrailSDK::upgradeKeyIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 3
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\Serializer\MessageSigner\SignedMessageSerializer;
20
use BitWasp\Bitcoin\Transaction\Transaction;
21
use BitWasp\Bitcoin\Transaction\TransactionFactory;
22
use BitWasp\Buffertools\Buffer;
23
use Blocktrail\CryptoJSAES\CryptoJSAES;
24
use Blocktrail\SDK\Bitcoin\BIP32Key;
25
use Blocktrail\SDK\Connection\RestClient;
26
27
/**
28
 * Class BlocktrailSDK
29
 */
30
class BlocktrailSDK implements BlocktrailSDKInterface {
31
    /**
32
     * @var Connection\RestClient
33
     */
34
    protected $client;
35
36
    /**
37
     * @var string          currently only supporting; bitcoin
38
     */
39
    protected $network;
40
41
    /**
42
     * @var bool
43
     */
44
    protected $testnet;
45
46
    /**
47
     * @param   string      $apiKey         the API_KEY to use for authentication
48
     * @param   string      $apiSecret      the API_SECRET to use for authentication
49
     * @param   string      $network        the cryptocurrency 'network' to consume, eg BTC, LTC, etc
50
     * @param   bool        $testnet        testnet yes/no
51
     * @param   string      $apiVersion     the version of the API to consume
52
     * @param   null        $apiEndpoint    overwrite the endpoint used
53
     *                                       this will cause the $network, $testnet and $apiVersion to be ignored!
54
     */
55
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
56
        if (is_null($apiEndpoint)) {
57
            $network = strtoupper($network);
58
59
            if ($testnet) {
60
                $network = "t{$network}";
61
            }
62
63
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com";
64
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$network}/";
65
        }
66
67
        // normalize network and set bitcoinlib to the right magic-bytes
68
        list($this->network, $this->testnet) = $this->normalizeNetwork($network, $testnet);
69
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet);
70
71
        $this->client = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
72
    }
73
74
    /**
75
     * normalize network string
76
     *
77
     * @param $network
78
     * @param $testnet
79
     * @return array
80
     * @throws \Exception
81
     */
82 View Code Duplication
    protected function normalizeNetwork($network, $testnet) {
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...
83
        switch (strtolower($network)) {
84
            case 'btc':
85
            case 'bitcoin':
86
                $network = 'bitcoin';
87
88
                break;
89
90
            case 'tbtc':
91
            case 'bitcoin-testnet':
92
                $network = 'bitcoin';
93
                $testnet = true;
94
95
                break;
96
97
            default:
98
                throw new \Exception("Unknown network [{$network}]");
99
        }
100
101
        return [$network, $testnet];
102
    }
103
104
    /**
105
     * set BitcoinLib to the correct magic-byte defaults for the selected network
106
     *
107
     * @param $network
108
     * @param $testnet
109
     */
110
    protected function setBitcoinLibMagicBytes($network, $testnet) {
111
        assert($network == "bitcoin");
112
        Bitcoin::setNetwork($testnet ? NetworkFactory::bitcoinTestnet() : NetworkFactory::bitcoin());
113
    }
114
115
    /**
116
     * enable CURL debugging output
117
     *
118
     * @param   bool        $debug
119
     *
120
     * @codeCoverageIgnore
121
     */
122
    public function setCurlDebugging($debug = true) {
123
        $this->client->setCurlDebugging($debug);
124
    }
125
126
    /**
127
     * enable verbose errors
128
     *
129
     * @param   bool        $verboseErrors
130
     *
131
     * @codeCoverageIgnore
132
     */
133
    public function setVerboseErrors($verboseErrors = true) {
134
        $this->client->setVerboseErrors($verboseErrors);
135
    }
136
    
137
    /**
138
     * set cURL default option on Guzzle client
139
     * @param string    $key
140
     * @param bool      $value
141
     *
142
     * @codeCoverageIgnore
143
     */
144
    public function setCurlDefaultOption($key, $value) {
145
        $this->client->setCurlDefaultOption($key, $value);
146
    }
147
148
    /**
149
     * @return  RestClient
150
     */
151
    public function getRestClient() {
152
        return $this->client;
153
    }
154
155
    /**
156
     * get a single address
157
     * @param  string $address address hash
158
     * @return array           associative array containing the response
159
     */
160
    public function address($address) {
161
        $response = $this->client->get("address/{$address}");
162
        return self::jsonDecode($response->body(), true);
163
    }
164
165
    /**
166
     * get all transactions for an address (paginated)
167
     * @param  string  $address address hash
168
     * @param  integer $page    pagination: page number
169
     * @param  integer $limit   pagination: records per page (max 500)
170
     * @param  string  $sortDir pagination: sort direction (asc|desc)
171
     * @return array            associative array containing the response
172
     */
173
    public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
174
        $queryString = [
175
            'page' => $page,
176
            'limit' => $limit,
177
            'sort_dir' => $sortDir
178
        ];
179
        $response = $this->client->get("address/{$address}/transactions", $queryString);
180
        return self::jsonDecode($response->body(), true);
181
    }
182
183
    /**
184
     * get all unconfirmed transactions for an address (paginated)
185
     * @param  string  $address address hash
186
     * @param  integer $page    pagination: page number
187
     * @param  integer $limit   pagination: records per page (max 500)
188
     * @param  string  $sortDir pagination: sort direction (asc|desc)
189
     * @return array            associative array containing the response
190
     */
191
    public function addressUnconfirmedTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
192
        $queryString = [
193
            'page' => $page,
194
            'limit' => $limit,
195
            'sort_dir' => $sortDir
196
        ];
197
        $response = $this->client->get("address/{$address}/unconfirmed-transactions", $queryString);
198
        return self::jsonDecode($response->body(), true);
199
    }
200
201
    /**
202
     * get all unspent outputs for an address (paginated)
203
     * @param  string  $address address hash
204
     * @param  integer $page    pagination: page number
205
     * @param  integer $limit   pagination: records per page (max 500)
206
     * @param  string  $sortDir pagination: sort direction (asc|desc)
207
     * @return array            associative array containing the response
208
     */
209
    public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') {
210
        $queryString = [
211
            'page' => $page,
212
            'limit' => $limit,
213
            'sort_dir' => $sortDir
214
        ];
215
        $response = $this->client->get("address/{$address}/unspent-outputs", $queryString);
216
        return self::jsonDecode($response->body(), true);
217
    }
218
219
    /**
220
     * get all unspent outputs for a batch of addresses (paginated)
221
     *
222
     * @param  string[] $addresses
223
     * @param  integer  $page    pagination: page number
224
     * @param  integer  $limit   pagination: records per page (max 500)
225
     * @param  string   $sortDir pagination: sort direction (asc|desc)
226
     * @return array associative array containing the response
227
     * @throws \Exception
228
     */
229
    public function batchAddressUnspentOutputs($addresses, $page = 1, $limit = 20, $sortDir = 'asc') {
230
        $queryString = [
231
            'page' => $page,
232
            'limit' => $limit,
233
            'sort_dir' => $sortDir
234
        ];
235
        $response = $this->client->post("address/unspent-outputs", $queryString, ['addresses' => $addresses]);
236
        return self::jsonDecode($response->body(), true);
237
    }
238
239
    /**
240
     * verify ownership of an address
241
     * @param  string  $address     address hash
242
     * @param  string  $signature   a signed message (the address hash) using the private key of the address
243
     * @return array                associative array containing the response
244
     */
245
    public function verifyAddress($address, $signature) {
246
        $postData = ['signature' => $signature];
247
248
        $response = $this->client->post("address/{$address}/verify", null, $postData, RestClient::AUTH_HTTP_SIG);
249
250
        return self::jsonDecode($response->body(), true);
251
    }
252
253
    /**
254
     * get all blocks (paginated)
255
     * @param  integer $page    pagination: page number
256
     * @param  integer $limit   pagination: records per page
257
     * @param  string  $sortDir pagination: sort direction (asc|desc)
258
     * @return array            associative array containing the response
259
     */
260
    public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') {
261
        $queryString = [
262
            'page' => $page,
263
            'limit' => $limit,
264
            'sort_dir' => $sortDir
265
        ];
266
        $response = $this->client->get("all-blocks", $queryString);
267
        return self::jsonDecode($response->body(), true);
268
    }
269
270
    /**
271
     * get the latest block
272
     * @return array            associative array containing the response
273
     */
274
    public function blockLatest() {
275
        $response = $this->client->get("block/latest");
276
        return self::jsonDecode($response->body(), true);
277
    }
278
279
    /**
280
     * get an individual block
281
     * @param  string|integer $block    a block hash or a block height
282
     * @return array                    associative array containing the response
283
     */
284
    public function block($block) {
285
        $response = $this->client->get("block/{$block}");
286
        return self::jsonDecode($response->body(), true);
287
    }
288
289
    /**
290
     * get all transaction in a block (paginated)
291
     * @param  string|integer   $block   a block hash or a block height
292
     * @param  integer          $page    pagination: page number
293
     * @param  integer          $limit   pagination: records per page
294
     * @param  string           $sortDir pagination: sort direction (asc|desc)
295
     * @return array                     associative array containing the response
296
     */
297
    public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') {
298
        $queryString = [
299
            'page' => $page,
300
            'limit' => $limit,
301
            'sort_dir' => $sortDir
302
        ];
303
        $response = $this->client->get("block/{$block}/transactions", $queryString);
304
        return self::jsonDecode($response->body(), true);
305
    }
306
307
    /**
308
     * get a single transaction
309
     * @param  string $txhash transaction hash
310
     * @return array          associative array containing the response
311
     */
312
    public function transaction($txhash) {
313
        $response = $this->client->get("transaction/{$txhash}");
314
        return self::jsonDecode($response->body(), true);
315
    }
316
    
317
    /**
318
     * get a paginated list of all webhooks associated with the api user
319
     * @param  integer          $page    pagination: page number
320
     * @param  integer          $limit   pagination: records per page
321
     * @return array                     associative array containing the response
322
     */
323
    public function allWebhooks($page = 1, $limit = 20) {
324
        $queryString = [
325
            'page' => $page,
326
            'limit' => $limit
327
        ];
328
        $response = $this->client->get("webhooks", $queryString);
329
        return self::jsonDecode($response->body(), true);
330
    }
331
332
    /**
333
     * get an existing webhook by it's identifier
334
     * @param string    $identifier     a unique identifier associated with the webhook
335
     * @return array                    associative array containing the response
336
     */
337
    public function getWebhook($identifier) {
338
        $response = $this->client->get("webhook/".$identifier);
339
        return self::jsonDecode($response->body(), true);
340
    }
341
342
    /**
343
     * create a new webhook
344
     * @param  string  $url        the url to receive the webhook events
345
     * @param  string  $identifier a unique identifier to associate with this webhook
346
     * @return array               associative array containing the response
347
     */
348 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...
349
        $postData = [
350
            'url'        => $url,
351
            'identifier' => $identifier
352
        ];
353
        $response = $this->client->post("webhook", null, $postData, RestClient::AUTH_HTTP_SIG);
354
        return self::jsonDecode($response->body(), true);
355
    }
356
357
    /**
358
     * update an existing webhook
359
     * @param  string  $identifier      the unique identifier of the webhook to update
360
     * @param  string  $newUrl          the new url to receive the webhook events
361
     * @param  string  $newIdentifier   a new unique identifier to associate with this webhook
362
     * @return array                    associative array containing the response
363
     */
364
    public function updateWebhook($identifier, $newUrl = null, $newIdentifier = null) {
365
        $putData = [
366
            'url'        => $newUrl,
367
            'identifier' => $newIdentifier
368
        ];
369
        $response = $this->client->put("webhook/{$identifier}", null, $putData, RestClient::AUTH_HTTP_SIG);
370
        return self::jsonDecode($response->body(), true);
371
    }
372
373
    /**
374
     * deletes an existing webhook and any event subscriptions associated with it
375
     * @param  string  $identifier      the unique identifier of the webhook to delete
376
     * @return boolean                  true on success
377
     */
378
    public function deleteWebhook($identifier) {
379
        $response = $this->client->delete("webhook/{$identifier}", null, null, RestClient::AUTH_HTTP_SIG);
380
        return self::jsonDecode($response->body(), true);
381
    }
382
383
    /**
384
     * get a paginated list of all the events a webhook is subscribed to
385
     * @param  string  $identifier  the unique identifier of the webhook
386
     * @param  integer $page        pagination: page number
387
     * @param  integer $limit       pagination: records per page
388
     * @return array                associative array containing the response
389
     */
390
    public function getWebhookEvents($identifier, $page = 1, $limit = 20) {
391
        $queryString = [
392
            'page' => $page,
393
            'limit' => $limit
394
        ];
395
        $response = $this->client->get("webhook/{$identifier}/events", $queryString);
396
        return self::jsonDecode($response->body(), true);
397
    }
398
    
399
    /**
400
     * subscribes a webhook to transaction events of one particular transaction
401
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
402
     * @param  string  $transaction     the transaction hash
403
     * @param  integer $confirmations   the amount of confirmations to send.
404
     * @return array                    associative array containing the response
405
     */
406 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...
407
        $postData = [
408
            'event_type'    => 'transaction',
409
            'transaction'   => $transaction,
410
            'confirmations' => $confirmations,
411
        ];
412
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
413
        return self::jsonDecode($response->body(), true);
414
    }
415
416
    /**
417
     * subscribes a webhook to transaction events on a particular address
418
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
419
     * @param  string  $address         the address hash
420
     * @param  integer $confirmations   the amount of confirmations to send.
421
     * @return array                    associative array containing the response
422
     */
423 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...
424
        $postData = [
425
            'event_type'    => 'address-transactions',
426
            'address'       => $address,
427
            'confirmations' => $confirmations,
428
        ];
429
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
430
        return self::jsonDecode($response->body(), true);
431
    }
432
433
    /**
434
     * batch subscribes a webhook to multiple transaction events
435
     *
436
     * @param  string $identifier   the unique identifier of the webhook
437
     * @param  array  $batchData    A 2D array of event data:
438
     *                              [address => $address, confirmations => $confirmations]
439
     *                              where $address is the address to subscibe to
440
     *                              and optionally $confirmations is the amount of confirmations
441
     * @return boolean              true on success
442
     */
443
    public function batchSubscribeAddressTransactions($identifier, $batchData) {
444
        $postData = [];
445
        foreach ($batchData as $record) {
446
            $postData[] = [
447
                'event_type' => 'address-transactions',
448
                'address' => $record['address'],
449
                'confirmations' => isset($record['confirmations']) ? $record['confirmations'] : 6,
450
            ];
451
        }
452
        $response = $this->client->post("webhook/{$identifier}/events/batch", null, $postData, RestClient::AUTH_HTTP_SIG);
453
        return self::jsonDecode($response->body(), true);
454
    }
455
456
    /**
457
     * subscribes a webhook to a new block event
458
     * @param  string  $identifier  the unique identifier of the webhook to be triggered
459
     * @return array                associative array containing the response
460
     */
461 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...
462
        $postData = [
463
            'event_type'    => 'block',
464
        ];
465
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
466
        return self::jsonDecode($response->body(), true);
467
    }
468
469
    /**
470
     * removes an transaction event subscription from a webhook
471
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
472
     * @param  string  $transaction     the transaction hash of the event subscription
473
     * @return boolean                  true on success
474
     */
475
    public function unsubscribeTransaction($identifier, $transaction) {
476
        $response = $this->client->delete("webhook/{$identifier}/transaction/{$transaction}", null, null, RestClient::AUTH_HTTP_SIG);
477
        return self::jsonDecode($response->body(), true);
478
    }
479
480
    /**
481
     * removes an address transaction event subscription from a webhook
482
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
483
     * @param  string  $address         the address hash of the event subscription
484
     * @return boolean                  true on success
485
     */
486
    public function unsubscribeAddressTransactions($identifier, $address) {
487
        $response = $this->client->delete("webhook/{$identifier}/address-transactions/{$address}", null, null, RestClient::AUTH_HTTP_SIG);
488
        return self::jsonDecode($response->body(), true);
489
    }
490
491
    /**
492
     * removes a block event subscription from a webhook
493
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
494
     * @return boolean                  true on success
495
     */
496
    public function unsubscribeNewBlocks($identifier) {
497
        $response = $this->client->delete("webhook/{$identifier}/block", null, null, RestClient::AUTH_HTTP_SIG);
498
        return self::jsonDecode($response->body(), true);
499
    }
500
501
    /**
502
     * create a new wallet
503
     *   - will generate a new primary seed (with password) and backup seed (without password)
504
     *   - send the primary seed (BIP39 'encrypted') and backup public key to the server
505
     *   - receive the blocktrail co-signing public key from the server
506
     *
507
     * Either takes one argument:
508
     * @param array $options
509
     *
510
     * Or takes three arguments (old, deprecated syntax):
511
     * (@nonPHP-doc) @param      $identifier
512
     * (@nonPHP-doc) @param      $password
513
     * (@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...
514
     *
515
     * @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...
516
     * @throws \Exception
517
     */
518
    public function createNewWallet($options) {
519
        if (!is_array($options)) {
520
            $args = func_get_args();
521
            $options = [
522
                "identifier" => $args[0],
523
                "password" => $args[1],
524
                "key_index" => isset($args[2]) ? $args[2] : null,
525
            ];
526
        }
527
528
        if (isset($options['password'])) {
529
            if (isset($options['passphrase'])) {
530
                throw new \InvalidArgumentException("Can only provide either passphrase or password");
531
            } else {
532
                $options['passphrase'] = $options['password'];
533
            }
534
        }
535
536
        if (!isset($options['passphrase'])) {
537
            $options['passphrase'] = null;
538
        }
539
540
        if (!isset($options['key_index'])) {
541
            $options['key_index'] = 0;
542
        }
543
544
        if (!isset($options['wallet_version'])) {
545
            $options['wallet_version'] = Wallet::WALLET_VERSION_V2;
546
        }
547
548
        switch ($options['wallet_version']) {
549
            case Wallet::WALLET_VERSION_V1:
550
                return $this->createNewWalletV1($options);
551
552
            case Wallet::WALLET_VERSION_V2:
553
                return $this->createNewWalletV2($options);
554
555
            default:
556
                throw new \InvalidArgumentException("Invalid wallet version");
557
        }
558
    }
559
560
    protected function createNewWalletV1($options) {
561
        $walletPath = WalletPath::create($options['key_index']);
562
563
        $storePrimaryMnemonic = isset($options['store_primary_mnemonic']) ? $options['store_primary_mnemonic'] : null;
564
565
        if (isset($options['primary_mnemonic']) && $options['primary_private_key']) {
566
            throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey");
567
        }
568
569
        $primaryMnemonic = null;
570
        $primaryPrivateKey = null;
571
        if (!isset($options['primary_mnemonic']) && !isset($options['primary_private_key'])) {
572
            if (!$options['passphrase']) {
573
                throw new \InvalidArgumentException("Can't generate Primary Mnemonic without a passphrase");
574
            } else {
575
                // create new primary seed
576
                /** @var HierarchicalKey $primaryPrivateKey */
577
                list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->newPrimarySeed($options['passphrase']);
0 ignored issues
show
Unused Code introduced by
The assignment to $primarySeed is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
578
                if ($storePrimaryMnemonic !== false) {
579
                    $storePrimaryMnemonic = true;
580
                }
581
            }
582
        } else if (isset($options['primary_mnemonic'])) {
583
            $primaryMnemonic = $options['primary_mnemonic'];
584
        } else if (isset($options['primary_private_key'])) {
585
            $primaryPrivateKey = $options['primary_private_key'];
586
        }
587
588
        if ($storePrimaryMnemonic && $primaryMnemonic && !$options['passphrase']) {
589
            throw new \InvalidArgumentException("Can't store Primary Mnemonic on server without a passphrase");
590
        }
591
592 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...
593
            if (is_string($primaryPrivateKey)) {
594
                $primaryPrivateKey = [$primaryPrivateKey, "m"];
595
            }
596
        } else {
597
            $primaryPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($primaryMnemonic, $options['passphrase']));
598
        }
599
600
        if (!$storePrimaryMnemonic) {
601
            $primaryMnemonic = false;
602
        }
603
604
        // create primary public key from the created private key
605
        $path = (string)$walletPath->keyIndexPath()->publicPath();
606
        $primaryPublicKey = BIP32Key::create($primaryPrivateKey->derivePath($path), $path);
607
608
        if (isset($options['backup_mnemonic']) && $options['backup_public_key']) {
609
            throw new \InvalidArgumentException("Can't specify Backup Mnemonic and Backup PublicKey");
610
        }
611
612
        $backupMnemonic = null;
613
        $backupPublicKey = null;
614
        if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) {
615
            /** @var HierarchicalKey $backupPrivateKey */
616
            list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->newBackupSeed();
0 ignored issues
show
Unused Code introduced by
The assignment to $backupSeed is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $backupPrivateKey is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
617
        } else if (isset($options['backup_mnemonic'])) {
618
            $backupMnemonic = $options['backup_mnemonic'];
619
        } else if (isset($options['backup_public_key'])) {
620
            $backupPublicKey = $options['backup_public_key'];
621
        }
622
623 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...
624
            if (is_string($backupPublicKey)) {
625
                $backupPublicKey = [$backupPublicKey, "m"];
626
            }
627
        } else {
628
            $backupPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, ""));
629
            $backupPublicKey = BIP32Key::create($backupPrivateKey->toPublic(), "M");
630
        }
631
632
        // create a checksum of our private key which we'll later use to verify we used the right password
633
        $checksum = $primaryPrivateKey->getPublicKey()->getAddress()->getAddress();
634
635
        // send the public keys to the server to store them
636
        //  and the mnemonic, which is safe because it's useless without the password
637
        $data = $this->storeNewWalletV1($options['identifier'], $primaryPublicKey->tuple(), $backupPublicKey->tuple(), $primaryMnemonic, $checksum, $options['key_index']);
638
639
        // received the blocktrail public keys
640 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...
641
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
642
        }, $data['blocktrail_public_keys']);
643
644
        $wallet = new WalletV1(
645
            $this,
646
            $options['identifier'],
647
            $primaryMnemonic,
648
            [$options['key_index'] => $primaryPublicKey],
649
            $backupPublicKey,
650
            $blocktrailPublicKeys,
651
            $options['key_index'],
652
            $this->network,
653
            $this->testnet,
654
            $checksum
655
        );
656
657
        $wallet->unlock($options);
658
659
        // return wallet and backup mnemonic
660
        return [
661
            $wallet,
662
            [
663
                'primary_mnemonic' => $primaryMnemonic,
664
                'backup_mnemonic' => $backupMnemonic,
665
                'blocktrail_public_keys' => $blocktrailPublicKeys,
666
            ],
667
        ];
668
    }
669
670
    public static function randomBits($bits) {
671
        return self::randomBytes($bits / 8);
672
    }
673
674
    public static function randomBytes($bytes) {
675
        return (new Random())->bytes($bytes)->getBinary();
676
    }
677
678
    protected function createNewWalletV2($options) {
679
        $walletPath = WalletPath::create($options['key_index']);
680
681
        if (isset($options['store_primary_mnemonic'])) {
682
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
683
        }
684
685
        if (!isset($options['store_data_on_server'])) {
686
            if (isset($options['primary_private_key'])) {
687
                $options['store_data_on_server'] = false;
688
            } else {
689
                $options['store_data_on_server'] = true;
690
            }
691
        }
692
693
        $storeDataOnServer = $options['store_data_on_server'];
694
695
        $secret = null;
696
        $encryptedSecret = null;
697
        $primarySeed = null;
698
        $encryptedPrimarySeed = null;
699
        $recoverySecret = null;
700
        $recoveryEncryptedSecret = null;
701
        $backupSeed = null;
702
703
        if (!isset($options['primary_private_key'])) {
704
            $primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : self::randomBits(256);
705
        }
706
707
        if ($storeDataOnServer) {
708
            if (!isset($options['secret'])) {
709
                if (!$options['passphrase']) {
710
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
711
                }
712
713
                $secret = bin2hex(self::randomBits(256)); // string because we use it as passphrase
714
                $encryptedSecret = CryptoJSAES::encrypt($secret, $options['passphrase']);
715
            } else {
716
                $secret = $options['secret'];
717
            }
718
719
            $encryptedPrimarySeed = CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
720
            $recoverySecret = bin2hex(self::randomBits(256));
721
722
            $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);
723
        }
724
725
        if (!isset($options['backup_public_key'])) {
726
            $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : self::randomBits(256);
727
        }
728
729
        if (isset($options['primary_private_key'])) {
730
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
731
        } else {
732
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m");
733
        }
734
735
        // create primary public key from the created private key
736
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
737
738
        if (!isset($options['backup_public_key'])) {
739
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M");
740
        }
741
742
        // create a checksum of our private key which we'll later use to verify we used the right password
743
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
744
745
        // send the public keys and encrypted data to server
746
        $data = $this->storeNewWalletV2(
747
            $options['identifier'],
748
            $options['primary_public_key']->tuple(),
749
            $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...
750
            $storeDataOnServer ? $encryptedPrimarySeed : false,
751
            $storeDataOnServer ? $encryptedSecret : false,
752
            $storeDataOnServer ? $recoverySecret : false,
753
            $checksum,
754
            $options['key_index']
755
        );
756
757
        // received the blocktrail public keys
758 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...
759
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
760
        }, $data['blocktrail_public_keys']);
761
762
        $wallet = new WalletV2(
763
            $this,
764
            $options['identifier'],
765
            $encryptedPrimarySeed,
766
            $encryptedSecret,
767
            [$options['key_index'] => $options['primary_public_key']],
768
            $options['backup_public_key'],
769
            $blocktrailPublicKeys,
770
            $options['key_index'],
771
            $this->network,
772
            $this->testnet,
773
            $checksum
774
        );
775
776
        $wallet->unlock([
777
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
778
            'primary_private_key' => $options['primary_private_key'],
779
            'primary_seed' => $primarySeed,
780
            'secret' => $secret,
781
        ]);
782
783
        // return wallet and mnemonics for backup sheet
784
        return [
785
            $wallet,
786
            [
787
                'encrypted_primary_seed' => $encryptedPrimarySeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedPrimarySeed))) : null,
788
                'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer($backupSeed)) : null,
789
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($recoveryEncryptedSecret))) : null,
790
                'encrypted_secret' => $encryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedSecret))) : null,
791
                'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
792
                    return [$keyIndex, $pubKey->tuple()];
793
                }, $blocktrailPublicKeys),
794
            ],
795
        ];
796
    }
797
798
    /**
799
     * create wallet using the API
800
     *
801
     * @param string    $identifier             the wallet identifier to create
802
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
803
     * @param string    $backupPublicKey        plain public key
804
     * @param string    $primaryMnemonic        mnemonic to store
805
     * @param string    $checksum               checksum to store
806
     * @param int       $keyIndex               account that we expect to use
807
     * @return mixed
808
     */
809
    public function storeNewWalletV1($identifier, $primaryPublicKey, $backupPublicKey, $primaryMnemonic, $checksum, $keyIndex) {
810
        $data = [
811
            'identifier' => $identifier,
812
            'primary_public_key' => $primaryPublicKey,
813
            'backup_public_key' => $backupPublicKey,
814
            'primary_mnemonic' => $primaryMnemonic,
815
            'checksum' => $checksum,
816
            'key_index' => $keyIndex
817
        ];
818
819
        $response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
820
        return self::jsonDecode($response->body(), true);
821
    }
822
823
    /**
824
     * create wallet using the API
825
     *
826
     * @param string $identifier       the wallet identifier to create
827
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
828
     * @param string $backupPublicKey  plain public key
829
     * @param        $encryptedPrimarySeed
830
     * @param        $encryptedSecret
831
     * @param        $recoverySecret
832
     * @param string $checksum         checksum to store
833
     * @param int    $keyIndex         account that we expect to use
834
     * @return mixed
835
     * @throws \Exception
836
     */
837
    public function storeNewWalletV2($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex) {
838
        $data = [
839
            'identifier' => $identifier,
840
            'wallet_version' => Wallet::WALLET_VERSION_V2,
841
            'primary_public_key' => $primaryPublicKey,
842
            'backup_public_key' => $backupPublicKey,
843
            'encrypted_primary_seed' => $encryptedPrimarySeed,
844
            'encrypted_secret' => $encryptedSecret,
845
            'recovery_secret' => $recoverySecret,
846
            'checksum' => $checksum,
847
            'key_index' => $keyIndex
848
        ];
849
850
        $response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
851
        return self::jsonDecode($response->body(), true);
852
    }
853
854
    /**
855
     * upgrade wallet to use a new account number
856
     *  the account number specifies which blocktrail cosigning key is used
857
     *
858
     * @param string    $identifier             the wallet identifier to be upgraded
859
     * @param int       $keyIndex               the new account to use
860
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
861
     * @return mixed
862
     */
863
    public function upgradeKeyIndex($identifier, $keyIndex, $primaryPublicKey) {
864
        $data = [
865
            'key_index' => $keyIndex,
866
            'primary_public_key' => $primaryPublicKey
867
        ];
868
869
        $response = $this->client->post("wallet/{$identifier}/upgrade", null, $data, RestClient::AUTH_HTTP_SIG);
870
        return self::jsonDecode($response->body(), true);
871
    }
872
873
    /**
874
     * initialize a previously created wallet
875
     *
876
     * Either takes one argument:
877
     * @param array $options
878
     *
879
     * Or takes two arguments (old, deprecated syntax):
880
     * (@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...
881
     * (@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...
882
     *
883
     * @return WalletInterface
884
     * @throws \Exception
885
     */
886
    public function initWallet($options) {
887
        if (!is_array($options)) {
888
            $args = func_get_args();
889
            $options = [
890
                "identifier" => $args[0],
891
                "password" => $args[1],
892
            ];
893
        }
894
895
        $identifier = $options['identifier'];
896
        $readonly = isset($options['readonly']) ? $options['readonly'] :
897
                    (isset($options['readOnly']) ? $options['readOnly'] :
898
                        (isset($options['read-only']) ? $options['read-only'] :
899
                            false));
900
901
        // get the wallet data from the server
902
        $data = $this->getWallet($identifier);
903
904
        if (!$data) {
905
            throw new \Exception("Failed to get wallet");
906
        }
907
908
        switch ($data['wallet_version']) {
909
            case Wallet::WALLET_VERSION_V1:
910
                $wallet = new WalletV1(
911
                    $this,
912
                    $identifier,
913
                    isset($options['primary_mnemonic']) ? $options['primary_mnemonic'] : $data['primary_mnemonic'],
914
                    $data['primary_public_keys'],
915
                    $data['backup_public_key'],
916
                    $data['blocktrail_public_keys'],
917
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
918
                    $this->network,
919
                    $this->testnet,
920
                    $data['checksum']
921
                );
922
                break;
923
            case Wallet::WALLET_VERSION_V2:
924
                $wallet = new WalletV2(
925
                    $this,
926
                    $identifier,
927
                    isset($options['encrypted_primary_seed']) ? $options['encrypted_primary_seed'] : $data['encrypted_primary_seed'],
928
                    isset($options['encrypted_secret']) ? $options['encrypted_secret'] : $data['encrypted_secret'],
929
                    $data['primary_public_keys'],
930
                    $data['backup_public_key'],
931
                    $data['blocktrail_public_keys'],
932
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
933
                    $this->network,
934
                    $this->testnet,
935
                    $data['checksum']
936
                );
937
                break;
938
            default:
939
                throw new \InvalidArgumentException("Invalid wallet version");
940
        }
941
942
        if (!$readonly) {
943
            $wallet->unlock($options);
944
        }
945
946
        return $wallet;
947
    }
948
949
    /**
950
     * get the wallet data from the server
951
     *
952
     * @param string    $identifier             the identifier of the wallet
953
     * @return mixed
954
     */
955
    public function getWallet($identifier) {
956
        $response = $this->client->get("wallet/{$identifier}", null, RestClient::AUTH_HTTP_SIG);
957
        return self::jsonDecode($response->body(), true);
958
    }
959
960
    /**
961
     * update the wallet data on the server
962
     *
963
     * @param string    $identifier
964
     * @param $data
965
     * @return mixed
966
     */
967
    public function updateWallet($identifier, $data) {
968
        $response = $this->client->post("wallet/{$identifier}", null, $data, RestClient::AUTH_HTTP_SIG);
969
        return self::jsonDecode($response->body(), true);
970
    }
971
972
    /**
973
     * delete a wallet from the server
974
     *  the checksum address and a signature to verify you ownership of the key of that checksum address
975
     *  is required to be able to delete a wallet
976
     *
977
     * @param string    $identifier             the identifier of the wallet
978
     * @param string    $checksumAddress        the address for your master private key (and the checksum used when creating the wallet)
979
     * @param string    $signature              a signature of the checksum address as message signed by the private key matching that address
980
     * @param bool      $force                  ignore warnings (such as a non-zero balance)
981
     * @return mixed
982
     */
983
    public function deleteWallet($identifier, $checksumAddress, $signature, $force = false) {
984
        $response = $this->client->delete("wallet/{$identifier}", ['force' => $force], [
985
            'checksum' => $checksumAddress,
986
            'signature' => $signature
987
        ], RestClient::AUTH_HTTP_SIG, 360);
988
        return self::jsonDecode($response->body(), true);
989
    }
990
991
    /**
992
     * create new backup key;
993
     *  1) a BIP39 mnemonic
994
     *  2) a seed from that mnemonic with a blank password
995
     *  3) a private key from that seed
996
     *
997
     * @return array [mnemonic, seed, key]
998
     */
999
    protected function newBackupSeed() {
1000
        list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed("");
1001
1002
        return [$backupMnemonic, $backupSeed, $backupPrivateKey];
1003
    }
1004
1005
    /**
1006
     * create new primary key;
1007
     *  1) a BIP39 mnemonic
1008
     *  2) a seed from that mnemonic with the password
1009
     *  3) a private key from that seed
1010
     *
1011
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1012
     * @return array [mnemonic, seed, key]
1013
     * @TODO: require a strong password?
1014
     */
1015
    protected function newPrimarySeed($passphrase) {
1016
        list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase);
1017
1018
        return [$primaryMnemonic, $primarySeed, $primaryPrivateKey];
1019
    }
1020
1021
    /**
1022
     * create a new key;
1023
     *  1) a BIP39 mnemonic
1024
     *  2) a seed from that mnemonic with the password
1025
     *  3) a private key from that seed
1026
     *
1027
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1028
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1029
     * @return array
1030
     */
1031
    protected function generateNewSeed($passphrase = "", $forceEntropy = null) {
1032
        // generate master seed, retry if the generated private key isn't valid (FALSE is returned)
1033
        do {
1034
            $mnemonic = $this->generateNewMnemonic($forceEntropy);
1035
1036
            $seed = (new Bip39SeedGenerator)->getSeed($mnemonic, $passphrase);
1037
1038
            $key = null;
1039
            try {
1040
                $key = HierarchicalKeyFactory::fromEntropy($seed);
1041
            } catch (\Exception $e) {
1042
                // try again
1043
            }
1044
        } while (!$key);
1045
1046
        return [$mnemonic, $seed, $key];
1047
    }
1048
1049
    /**
1050
     * generate a new mnemonic from some random entropy (512 bit)
1051
     *
1052
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1053
     * @return string
1054
     * @throws \Exception
1055
     */
1056
    protected function generateNewMnemonic($forceEntropy = null) {
1057
        if ($forceEntropy === null) {
1058
            $random = new Random();
1059
            $entropy = $random->bytes(512 / 8);
1060
        } else {
1061
            $entropy = $forceEntropy;
1062
        }
1063
1064
        return MnemonicFactory::bip39()->entropyToMnemonic($entropy);
0 ignored issues
show
Bug introduced by
It seems like $entropy defined by $forceEntropy on line 1061 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...
1065
    }
1066
1067
    /**
1068
     * get the balance for the wallet
1069
     *
1070
     * @param string    $identifier             the identifier of the wallet
1071
     * @return array
1072
     */
1073
    public function getWalletBalance($identifier) {
1074
        $response = $this->client->get("wallet/{$identifier}/balance", null, RestClient::AUTH_HTTP_SIG);
1075
        return self::jsonDecode($response->body(), true);
1076
    }
1077
1078
    /**
1079
     * do HD wallet discovery for the wallet
1080
     *
1081
     * this can be REALLY slow, so we've set the timeout to 120s ...
1082
     *
1083
     * @param string    $identifier             the identifier of the wallet
1084
     * @param int       $gap                    the gap setting to use for discovery
1085
     * @return mixed
1086
     */
1087
    public function doWalletDiscovery($identifier, $gap = 200) {
1088
        $response = $this->client->get("wallet/{$identifier}/discovery", ['gap' => $gap], RestClient::AUTH_HTTP_SIG, 360.0);
1089
        return self::jsonDecode($response->body(), true);
1090
    }
1091
1092
    /**
1093
     * get a new derivation number for specified parent path
1094
     *  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
1095
     *
1096
     * returns the path
1097
     *
1098
     * @param string    $identifier             the identifier of the wallet
1099
     * @param string    $path                   the parent path for which to get a new derivation
1100
     * @return string
1101
     */
1102
    public function getNewDerivation($identifier, $path) {
1103
        $result = $this->_getNewDerivation($identifier, $path);
1104
        return $result['path'];
1105
    }
1106
1107
    /**
1108
     * get a new derivation number for specified parent path
1109
     *  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
1110
     *
1111
     * @param string    $identifier             the identifier of the wallet
1112
     * @param string    $path                   the parent path for which to get a new derivation
1113
     * @return mixed
1114
     */
1115
    public function _getNewDerivation($identifier, $path) {
1116
        $response = $this->client->post("wallet/{$identifier}/path", null, ['path' => $path], RestClient::AUTH_HTTP_SIG);
1117
        return self::jsonDecode($response->body(), true);
1118
    }
1119
1120
    /**
1121
     * get the path (and redeemScript) to specified address
1122
     *
1123
     * @param string $identifier
1124
     * @param string $address
1125
     * @return array
1126
     * @throws \Exception
1127
     */
1128
    public function getPathForAddress($identifier, $address) {
1129
        $response = $this->client->post("wallet/{$identifier}/path_for_address", null, ['address' => $address], RestClient::AUTH_HTTP_SIG);
1130
        return self::jsonDecode($response->body(), true)['path'];
1131
    }
1132
1133
    /**
1134
     * send the transaction using the API
1135
     *
1136
     * @param string    $identifier             the identifier of the wallet
1137
     * @param string    $rawTransaction         raw hex of the transaction (should be partially signed)
1138
     * @param array     $paths                  list of the paths that were used for the UTXO
1139
     * @param bool      $checkFee               let the server verify the fee after signing
1140
     * @return string                           the complete raw transaction
1141
     * @throws \Exception
1142
     */
1143
    public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee = false) {
1144
        $data = [
1145
            'raw_transaction' => $rawTransaction,
1146
            'paths' => $paths
1147
        ];
1148
1149
        // dynamic TTL for when we're signing really big transactions
1150
        $ttl = max(5.0, count($paths) * 0.25) + 4.0;
1151
1152
        $response = $this->client->post("wallet/{$identifier}/send", ['check_fee' => (int)!!$checkFee], $data, RestClient::AUTH_HTTP_SIG, $ttl);
1153
        $signed = self::jsonDecode($response->body(), true);
1154
1155
        if (!$signed['complete'] || $signed['complete'] == 'false') {
1156
            throw new \Exception("Failed to completely sign transaction");
1157
        }
1158
1159
        // create TX hash from the raw signed hex
1160
        return TransactionFactory::fromHex($signed['hex'])->getTxId()->getHex();
1161
    }
1162
1163
    /**
1164
     * use the API to get the best inputs to use based on the outputs
1165
     *
1166
     * the return array has the following format:
1167
     * [
1168
     *  "utxos" => [
1169
     *      [
1170
     *          "hash" => "<txHash>",
1171
     *          "idx" => "<index of the output of that <txHash>",
1172
     *          "scriptpubkey_hex" => "<scriptPubKey-hex>",
1173
     *          "value" => 32746327,
1174
     *          "address" => "1address",
1175
     *          "path" => "m/44'/1'/0'/0/13",
1176
     *          "redeem_script" => "<redeemScript-hex>",
1177
     *      ],
1178
     *  ],
1179
     *  "fee"   => 10000,
1180
     *  "change"=> 1010109201,
1181
     * ]
1182
     *
1183
     * @param string   $identifier              the identifier of the wallet
1184
     * @param array    $outputs                 the outputs you want to create - array[address => satoshi-value]
1185
     * @param bool     $lockUTXO                when TRUE the UTXOs selected will be locked for a few seconds
1186
     *                                          so you have some time to spend them without race-conditions
1187
     * @param bool     $allowZeroConf
1188
     * @param string   $feeStrategy
1189
     * @param null|int $forceFee
1190
     * @return array
1191
     * @throws \Exception
1192
     */
1193 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...
1194
        $args = [
1195
            'lock' => (int)!!$lockUTXO,
1196
            'zeroconf' => (int)!!$allowZeroConf,
1197
            'fee_strategy' => $feeStrategy,
1198
        ];
1199
1200
        if ($forceFee !== null) {
1201
            $args['forcefee'] = (int)$forceFee;
1202
        }
1203
1204
        $response = $this->client->post(
1205
            "wallet/{$identifier}/coin-selection",
1206
            $args,
1207
            $outputs,
1208
            RestClient::AUTH_HTTP_SIG
1209
        );
1210
1211
        return self::jsonDecode($response->body(), true);
1212
    }
1213
1214
    /**
1215
     *
1216
     * @param string   $identifier the identifier of the wallet
1217
     * @param bool     $allowZeroConf
1218
     * @param string   $feeStrategy
1219
     * @param null|int $forceFee
1220
     * @param int      $outputCnt
1221
     * @return array
1222
     * @throws \Exception
1223
     */
1224 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...
1225
        $args = [
1226
            'zeroconf' => (int)!!$allowZeroConf,
1227
            'fee_strategy' => $feeStrategy,
1228
            'outputs' => $outputCnt,
1229
        ];
1230
1231
        if ($forceFee !== null) {
1232
            $args['forcefee'] = (int)$forceFee;
1233
        }
1234
1235
        $response = $this->client->get(
1236
            "wallet/{$identifier}/max-spendable",
1237
            $args,
1238
            RestClient::AUTH_HTTP_SIG
1239
        );
1240
1241
        return self::jsonDecode($response->body(), true);
1242
    }
1243
1244
    /**
1245
     * @return array        ['optimal_fee' => 10000, 'low_priority_fee' => 5000]
1246
     */
1247
    public function feePerKB() {
1248
        $response = $this->client->get("fee-per-kb");
1249
        return self::jsonDecode($response->body(), true);
1250
    }
1251
1252
    /**
1253
     * get the current price index
1254
     *
1255
     * @return array        eg; ['USD' => 287.30]
1256
     */
1257
    public function price() {
1258
        $response = $this->client->get("price");
1259
        return self::jsonDecode($response->body(), true);
1260
    }
1261
1262
    /**
1263
     * setup webhook for wallet
1264
     *
1265
     * @param string    $identifier         the wallet identifier for which to create the webhook
1266
     * @param string    $webhookIdentifier  the webhook identifier to use
1267
     * @param string    $url                the url to receive the webhook events
1268
     * @return array
1269
     */
1270
    public function setupWalletWebhook($identifier, $webhookIdentifier, $url) {
1271
        $response = $this->client->post("wallet/{$identifier}/webhook", null, ['url' => $url, 'identifier' => $webhookIdentifier], RestClient::AUTH_HTTP_SIG);
1272
        return self::jsonDecode($response->body(), true);
1273
    }
1274
1275
    /**
1276
     * delete webhook for wallet
1277
     *
1278
     * @param string    $identifier         the wallet identifier for which to delete the webhook
1279
     * @param string    $webhookIdentifier  the webhook identifier to delete
1280
     * @return array
1281
     */
1282
    public function deleteWalletWebhook($identifier, $webhookIdentifier) {
1283
        $response = $this->client->delete("wallet/{$identifier}/webhook/{$webhookIdentifier}", null, null, RestClient::AUTH_HTTP_SIG);
1284
        return self::jsonDecode($response->body(), true);
1285
    }
1286
1287
    /**
1288
     * lock a specific unspent output
1289
     *
1290
     * @param     $identifier
1291
     * @param     $txHash
1292
     * @param     $txIdx
1293
     * @param int $ttl
1294
     * @return bool
1295
     */
1296
    public function lockWalletUTXO($identifier, $txHash, $txIdx, $ttl = 3) {
1297
        $response = $this->client->post("wallet/{$identifier}/lock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx, 'ttl' => $ttl], RestClient::AUTH_HTTP_SIG);
1298
        return self::jsonDecode($response->body(), true)['locked'];
1299
    }
1300
1301
    /**
1302
     * unlock a specific unspent output
1303
     *
1304
     * @param     $identifier
1305
     * @param     $txHash
1306
     * @param     $txIdx
1307
     * @return bool
1308
     */
1309
    public function unlockWalletUTXO($identifier, $txHash, $txIdx) {
1310
        $response = $this->client->post("wallet/{$identifier}/unlock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx], RestClient::AUTH_HTTP_SIG);
1311
        return self::jsonDecode($response->body(), true)['unlocked'];
1312
    }
1313
1314
    /**
1315
     * get all transactions for wallet (paginated)
1316
     *
1317
     * @param  string  $identifier  the wallet identifier for which to get transactions
1318
     * @param  integer $page        pagination: page number
1319
     * @param  integer $limit       pagination: records per page (max 500)
1320
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1321
     * @return array                associative array containing the response
1322
     */
1323 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...
1324
        $queryString = [
1325
            'page' => $page,
1326
            'limit' => $limit,
1327
            'sort_dir' => $sortDir
1328
        ];
1329
        $response = $this->client->get("wallet/{$identifier}/transactions", $queryString, RestClient::AUTH_HTTP_SIG);
1330
        return self::jsonDecode($response->body(), true);
1331
    }
1332
1333
    /**
1334
     * get all addresses for wallet (paginated)
1335
     *
1336
     * @param  string  $identifier  the wallet identifier for which to get addresses
1337
     * @param  integer $page        pagination: page number
1338
     * @param  integer $limit       pagination: records per page (max 500)
1339
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1340
     * @return array                associative array containing the response
1341
     */
1342 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...
1343
        $queryString = [
1344
            'page' => $page,
1345
            'limit' => $limit,
1346
            'sort_dir' => $sortDir
1347
        ];
1348
        $response = $this->client->get("wallet/{$identifier}/addresses", $queryString, RestClient::AUTH_HTTP_SIG);
1349
        return self::jsonDecode($response->body(), true);
1350
    }
1351
1352
    /**
1353
     * get all UTXOs for wallet (paginated)
1354
     *
1355
     * @param  string  $identifier  the wallet identifier for which to get addresses
1356
     * @param  integer $page        pagination: page number
1357
     * @param  integer $limit       pagination: records per page (max 500)
1358
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1359
     * @return array                associative array containing the response
1360
     */
1361 View Code Duplication
    public function walletUTXOs($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...
1362
        $queryString = [
1363
            'page' => $page,
1364
            'limit' => $limit,
1365
            'sort_dir' => $sortDir
1366
        ];
1367
        $response = $this->client->get("wallet/{$identifier}/utxos", $queryString, RestClient::AUTH_HTTP_SIG);
1368
        return self::jsonDecode($response->body(), true);
1369
    }
1370
1371
    /**
1372
     * get a paginated list of all wallets associated with the api user
1373
     *
1374
     * @param  integer          $page    pagination: page number
1375
     * @param  integer          $limit   pagination: records per page
1376
     * @return array                     associative array containing the response
1377
     */
1378 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...
1379
        $queryString = [
1380
            'page' => $page,
1381
            'limit' => $limit
1382
        ];
1383
        $response = $this->client->get("wallets", $queryString, RestClient::AUTH_HTTP_SIG);
1384
        return self::jsonDecode($response->body(), true);
1385
    }
1386
1387
    /**
1388
     * send raw transaction
1389
     *
1390
     * @param     $txHex
1391
     * @return bool
1392
     */
1393
    public function sendRawTransaction($txHex) {
1394
        $response = $this->client->post("send-raw-tx", null, ['hex' => $txHex], RestClient::AUTH_HTTP_SIG);
1395
        return self::jsonDecode($response->body(), true);
1396
    }
1397
1398
    /**
1399
     * testnet only ;-)
1400
     *
1401
     * @param     $address
1402
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1403
     * @return mixed
1404
     * @throws \Exception
1405
     */
1406 View Code Duplication
    public function faucetWithdrawl($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...
1407
        $response = $this->client->post("faucet/withdrawl", null, [
1408
            'address' => $address,
1409
            'amount' => $amount,
1410
        ], RestClient::AUTH_HTTP_SIG);
1411
        return self::jsonDecode($response->body(), true);
1412
    }
1413
1414
    /**
1415
     * verify a message signed bitcoin-core style
1416
     *
1417
     * @param  string           $message
1418
     * @param  string           $address
1419
     * @param  string           $signature
1420
     * @return boolean
1421
     */
1422
    public function verifyMessage($message, $address, $signature) {
1423
        // we could also use the API instead of the using BitcoinLib to verify
1424
        // $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...
1425
        $addr = AddressFactory::fromString($address);
1426
        if (!$addr instanceof PayToPubKeyHashAddress) {
1427
            throw new \RuntimeException('Can only verify a message with a pay-to-pubkey-hash address');
1428
        }
1429
1430
        /** @var CompactSignatureSerializerInterface $csSerializer */
1431
        $adapter = Bitcoin::getEcAdapter();
1432
        $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...
1433
        $signedMessage = new SignedMessage($message, $csSerializer->parse(new Buffer(base64_decode($signature))));
1434
1435
        $signer = new MessageSigner($adapter);
1436
        return $signer->verify($signedMessage, $addr);
1437
    }
1438
1439
    /**
1440
     * convert a Satoshi value to a BTC value
1441
     *
1442
     * @param int       $satoshi
1443
     * @return float
1444
     */
1445
    public static function toBTC($satoshi) {
1446
        return bcdiv((int)(string)$satoshi, 100000000, 8);
1447
    }
1448
1449
    /**
1450
     * convert a Satoshi value to a BTC value and return it as a string
1451
1452
     * @param int       $satoshi
1453
     * @return string
1454
     */
1455
    public static function toBTCString($satoshi) {
1456
        return sprintf("%.8f", self::toBTC($satoshi));
1457
    }
1458
1459
    /**
1460
     * convert a BTC value to a Satoshi value
1461
     *
1462
     * @param float     $btc
1463
     * @return string
1464
     */
1465
    public static function toSatoshiString($btc) {
1466
        return bcmul(sprintf("%.8f", (float)$btc), 100000000, 0);
1467
    }
1468
1469
    /**
1470
     * convert a BTC value to a Satoshi value
1471
     *
1472
     * @param float     $btc
1473
     * @return string
1474
     */
1475
    public static function toSatoshi($btc) {
1476
        return (int)self::toSatoshiString($btc);
1477
    }
1478
1479
    /**
1480
     * json_decode helper that throws exceptions when it fails to decode
1481
     *
1482
     * @param      $json
1483
     * @param bool $assoc
1484
     * @return mixed
1485
     * @throws \Exception
1486
     */
1487
    protected static function jsonDecode($json, $assoc = false) {
1488
        if (!$json) {
1489
            throw new \Exception("Can't json_decode empty string [{$json}]");
1490
        }
1491
1492
        $data = json_decode($json, $assoc);
1493
1494
        if ($data === null) {
1495
            throw new \Exception("Failed to json_decode [{$json}]");
1496
        }
1497
1498
        return $data;
1499
    }
1500
1501
    /**
1502
     * sort public keys for multisig script
1503
     *
1504
     * @param PublicKeyInterface[] $pubKeys
1505
     * @return PublicKeyInterface[]
1506
     */
1507
    public static function sortMultisigKeys(array $pubKeys) {
1508
        $result = array_values($pubKeys);
1509
        usort($result, function (PublicKeyInterface $a, PublicKeyInterface $b) {
1510
            $av = $a->getHex();
1511
            $bv = $b->getHex();
1512
            return $av == $bv ? 0 : $av > $bv ? 1 : -1;
1513
        });
1514
1515
        return $result;
1516
    }
1517
1518
    /**
1519
     * read and decode the json payload from a webhook's POST request.
1520
     *
1521
     * @param bool $returnObject    flag to indicate if an object or associative array should be returned
1522
     * @return mixed|null
1523
     * @throws \Exception
1524
     */
1525
    public static function getWebhookPayload($returnObject = false) {
1526
        $data = file_get_contents("php://input");
1527
        if ($data) {
1528
            return self::jsonDecode($data, !$returnObject);
1529
        } else {
1530
            return null;
1531
        }
1532
    }
1533
1534
    public static function normalizeBIP32KeyArray($keys) {
1535
        return Util::arrayMapWithIndex(function ($idx, $key) {
1536
            return [$idx, self::normalizeBIP32Key($key)];
1537
        }, $keys);
1538
    }
1539
1540
    public static function normalizeBIP32Key($key) {
1541
        if ($key instanceof BIP32Key) {
1542
            return $key;
1543
        }
1544
1545
        if (is_array($key)) {
1546
            $path = $key[1];
1547
            $key = $key[0];
1548
1549
            if (!($key instanceof HierarchicalKey)) {
1550
                $key = HierarchicalKeyFactory::fromExtended($key);
1551
            }
1552
1553
            return BIP32Key::create($key, $path);
1554
        } else {
1555
            throw new \Exception("Bad Input");
1556
        }
1557
    }
1558
}
1559