Completed
Branch master (95c3c5)
by
unknown
05:09
created

BlocktrailSDK::createNewWallet()   D

Complexity

Conditions 11
Paths 195

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 44
rs 4.9629
c 0
b 0
f 0
cc 11
eloc 27
nc 195
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\V3Crypt\Encryption;
26
use Blocktrail\SDK\V3Crypt\Mnemonic;
27
28
/**
29
 * Class BlocktrailSDK
30
 */
31
class BlocktrailSDK implements BlocktrailSDKInterface {
32
    /**
33
     * @var Connection\RestClient
34
     */
35
    protected $client;
36
37
    /**
38
     * @var string          currently only supporting; bitcoin
39
     */
40
    protected $network;
41
42
    /**
43
     * @var bool
44
     */
45
    protected $testnet;
46
47
    /**
48
     * @param   string      $apiKey         the API_KEY to use for authentication
49
     * @param   string      $apiSecret      the API_SECRET to use for authentication
50
     * @param   string      $network        the cryptocurrency 'network' to consume, eg BTC, LTC, etc
51
     * @param   bool        $testnet        testnet yes/no
52
     * @param   string      $apiVersion     the version of the API to consume
53
     * @param   null        $apiEndpoint    overwrite the endpoint used
54
     *                                       this will cause the $network, $testnet and $apiVersion to be ignored!
55
     */
56
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
57
        if (is_null($apiEndpoint)) {
58
            $network = strtoupper($network);
59
60
            if ($testnet) {
61
                $network = "t{$network}";
62
            }
63
64
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com";
65
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$network}/";
66
        }
67
68
        // normalize network and set bitcoinlib to the right magic-bytes
69
        list($this->network, $this->testnet) = $this->normalizeNetwork($network, $testnet);
70
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet);
71
72
        $this->client = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
73
    }
74
75
    /**
76
     * normalize network string
77
     *
78
     * @param $network
79
     * @param $testnet
80
     * @return array
81
     * @throws \Exception
82
     */
83 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...
84
        switch (strtolower($network)) {
85
            case 'btc':
86
            case 'bitcoin':
87
                $network = 'bitcoin';
88
89
                break;
90
91
            case 'tbtc':
92
            case 'bitcoin-testnet':
93
                $network = 'bitcoin';
94
                $testnet = true;
95
96
                break;
97
98
            default:
99
                throw new \Exception("Unknown network [{$network}]");
100
        }
101
102
        return [$network, $testnet];
103
    }
104
105
    /**
106
     * set BitcoinLib to the correct magic-byte defaults for the selected network
107
     *
108
     * @param $network
109
     * @param $testnet
110
     */
111
    protected function setBitcoinLibMagicBytes($network, $testnet) {
112
        assert($network == "bitcoin");
113
        Bitcoin::setNetwork($testnet ? NetworkFactory::bitcoinTestnet() : NetworkFactory::bitcoin());
114
    }
115
116
    /**
117
     * enable CURL debugging output
118
     *
119
     * @param   bool        $debug
120
     *
121
     * @codeCoverageIgnore
122
     */
123
    public function setCurlDebugging($debug = true) {
124
        $this->client->setCurlDebugging($debug);
125
    }
126
127
    /**
128
     * enable verbose errors
129
     *
130
     * @param   bool        $verboseErrors
131
     *
132
     * @codeCoverageIgnore
133
     */
134
    public function setVerboseErrors($verboseErrors = true) {
135
        $this->client->setVerboseErrors($verboseErrors);
136
    }
137
    
138
    /**
139
     * set cURL default option on Guzzle client
140
     * @param string    $key
141
     * @param bool      $value
142
     *
143
     * @codeCoverageIgnore
144
     */
145
    public function setCurlDefaultOption($key, $value) {
146
        $this->client->setCurlDefaultOption($key, $value);
147
    }
148
149
    /**
150
     * @return  RestClient
151
     */
152
    public function getRestClient() {
153
        return $this->client;
154
    }
155
156
    /**
157
     * get a single address
158
     * @param  string $address address hash
159
     * @return array           associative array containing the response
160
     */
161
    public function address($address) {
162
        $response = $this->client->get("address/{$address}");
163
        return self::jsonDecode($response->body(), true);
164
    }
165
166
    /**
167
     * get all transactions for an address (paginated)
168
     * @param  string  $address address hash
169
     * @param  integer $page    pagination: page number
170
     * @param  integer $limit   pagination: records per page (max 500)
171
     * @param  string  $sortDir pagination: sort direction (asc|desc)
172
     * @return array            associative array containing the response
173
     */
174
    public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
175
        $queryString = [
176
            'page' => $page,
177
            'limit' => $limit,
178
            'sort_dir' => $sortDir
179
        ];
180
        $response = $this->client->get("address/{$address}/transactions", $queryString);
181
        return self::jsonDecode($response->body(), true);
182
    }
183
184
    /**
185
     * get all unconfirmed transactions for an address (paginated)
186
     * @param  string  $address address hash
187
     * @param  integer $page    pagination: page number
188
     * @param  integer $limit   pagination: records per page (max 500)
189
     * @param  string  $sortDir pagination: sort direction (asc|desc)
190
     * @return array            associative array containing the response
191
     */
192
    public function addressUnconfirmedTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
193
        $queryString = [
194
            'page' => $page,
195
            'limit' => $limit,
196
            'sort_dir' => $sortDir
197
        ];
198
        $response = $this->client->get("address/{$address}/unconfirmed-transactions", $queryString);
199
        return self::jsonDecode($response->body(), true);
200
    }
201
202
    /**
203
     * get all unspent outputs for an address (paginated)
204
     * @param  string  $address address hash
205
     * @param  integer $page    pagination: page number
206
     * @param  integer $limit   pagination: records per page (max 500)
207
     * @param  string  $sortDir pagination: sort direction (asc|desc)
208
     * @return array            associative array containing the response
209
     */
210
    public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') {
211
        $queryString = [
212
            'page' => $page,
213
            'limit' => $limit,
214
            'sort_dir' => $sortDir
215
        ];
216
        $response = $this->client->get("address/{$address}/unspent-outputs", $queryString);
217
        return self::jsonDecode($response->body(), true);
218
    }
219
220
    /**
221
     * get all unspent outputs for a batch of addresses (paginated)
222
     *
223
     * @param  string[] $addresses
224
     * @param  integer  $page    pagination: page number
225
     * @param  integer  $limit   pagination: records per page (max 500)
226
     * @param  string   $sortDir pagination: sort direction (asc|desc)
227
     * @return array associative array containing the response
228
     * @throws \Exception
229
     */
230
    public function batchAddressUnspentOutputs($addresses, $page = 1, $limit = 20, $sortDir = 'asc') {
231
        $queryString = [
232
            'page' => $page,
233
            'limit' => $limit,
234
            'sort_dir' => $sortDir
235
        ];
236
        $response = $this->client->post("address/unspent-outputs", $queryString, ['addresses' => $addresses]);
237
        return self::jsonDecode($response->body(), true);
238
    }
239
240
    /**
241
     * verify ownership of an address
242
     * @param  string  $address     address hash
243
     * @param  string  $signature   a signed message (the address hash) using the private key of the address
244
     * @return array                associative array containing the response
245
     */
246
    public function verifyAddress($address, $signature) {
247
        $postData = ['signature' => $signature];
248
249
        $response = $this->client->post("address/{$address}/verify", null, $postData, RestClient::AUTH_HTTP_SIG);
250
251
        return self::jsonDecode($response->body(), true);
252
    }
253
254
    /**
255
     * get all blocks (paginated)
256
     * @param  integer $page    pagination: page number
257
     * @param  integer $limit   pagination: records per page
258
     * @param  string  $sortDir pagination: sort direction (asc|desc)
259
     * @return array            associative array containing the response
260
     */
261
    public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') {
262
        $queryString = [
263
            'page' => $page,
264
            'limit' => $limit,
265
            'sort_dir' => $sortDir
266
        ];
267
        $response = $this->client->get("all-blocks", $queryString);
268
        return self::jsonDecode($response->body(), true);
269
    }
270
271
    /**
272
     * get the latest block
273
     * @return array            associative array containing the response
274
     */
275
    public function blockLatest() {
276
        $response = $this->client->get("block/latest");
277
        return self::jsonDecode($response->body(), true);
278
    }
279
280
    /**
281
     * get an individual block
282
     * @param  string|integer $block    a block hash or a block height
283
     * @return array                    associative array containing the response
284
     */
285
    public function block($block) {
286
        $response = $this->client->get("block/{$block}");
287
        return self::jsonDecode($response->body(), true);
288
    }
289
290
    /**
291
     * get all transaction in a block (paginated)
292
     * @param  string|integer   $block   a block hash or a block height
293
     * @param  integer          $page    pagination: page number
294
     * @param  integer          $limit   pagination: records per page
295
     * @param  string           $sortDir pagination: sort direction (asc|desc)
296
     * @return array                     associative array containing the response
297
     */
298
    public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') {
299
        $queryString = [
300
            'page' => $page,
301
            'limit' => $limit,
302
            'sort_dir' => $sortDir
303
        ];
304
        $response = $this->client->get("block/{$block}/transactions", $queryString);
305
        return self::jsonDecode($response->body(), true);
306
    }
307
308
    /**
309
     * get a single transaction
310
     * @param  string $txhash transaction hash
311
     * @return array          associative array containing the response
312
     */
313
    public function transaction($txhash) {
314
        $response = $this->client->get("transaction/{$txhash}");
315
        return self::jsonDecode($response->body(), true);
316
    }
317
    
318
    /**
319
     * get a paginated list of all webhooks associated with the api user
320
     * @param  integer          $page    pagination: page number
321
     * @param  integer          $limit   pagination: records per page
322
     * @return array                     associative array containing the response
323
     */
324
    public function allWebhooks($page = 1, $limit = 20) {
325
        $queryString = [
326
            'page' => $page,
327
            'limit' => $limit
328
        ];
329
        $response = $this->client->get("webhooks", $queryString);
330
        return self::jsonDecode($response->body(), true);
331
    }
332
333
    /**
334
     * get an existing webhook by it's identifier
335
     * @param string    $identifier     a unique identifier associated with the webhook
336
     * @return array                    associative array containing the response
337
     */
338
    public function getWebhook($identifier) {
339
        $response = $this->client->get("webhook/".$identifier);
340
        return self::jsonDecode($response->body(), true);
341
    }
342
343
    /**
344
     * create a new webhook
345
     * @param  string  $url        the url to receive the webhook events
346
     * @param  string  $identifier a unique identifier to associate with this webhook
347
     * @return array               associative array containing the response
348
     */
349 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...
350
        $postData = [
351
            'url'        => $url,
352
            'identifier' => $identifier
353
        ];
354
        $response = $this->client->post("webhook", null, $postData, RestClient::AUTH_HTTP_SIG);
355
        return self::jsonDecode($response->body(), true);
356
    }
357
358
    /**
359
     * update an existing webhook
360
     * @param  string  $identifier      the unique identifier of the webhook to update
361
     * @param  string  $newUrl          the new url to receive the webhook events
362
     * @param  string  $newIdentifier   a new unique identifier to associate with this webhook
363
     * @return array                    associative array containing the response
364
     */
365
    public function updateWebhook($identifier, $newUrl = null, $newIdentifier = null) {
366
        $putData = [
367
            'url'        => $newUrl,
368
            'identifier' => $newIdentifier
369
        ];
370
        $response = $this->client->put("webhook/{$identifier}", null, $putData, RestClient::AUTH_HTTP_SIG);
371
        return self::jsonDecode($response->body(), true);
372
    }
373
374
    /**
375
     * deletes an existing webhook and any event subscriptions associated with it
376
     * @param  string  $identifier      the unique identifier of the webhook to delete
377
     * @return boolean                  true on success
378
     */
379
    public function deleteWebhook($identifier) {
380
        $response = $this->client->delete("webhook/{$identifier}", null, null, RestClient::AUTH_HTTP_SIG);
381
        return self::jsonDecode($response->body(), true);
382
    }
383
384
    /**
385
     * get a paginated list of all the events a webhook is subscribed to
386
     * @param  string  $identifier  the unique identifier of the webhook
387
     * @param  integer $page        pagination: page number
388
     * @param  integer $limit       pagination: records per page
389
     * @return array                associative array containing the response
390
     */
391
    public function getWebhookEvents($identifier, $page = 1, $limit = 20) {
392
        $queryString = [
393
            'page' => $page,
394
            'limit' => $limit
395
        ];
396
        $response = $this->client->get("webhook/{$identifier}/events", $queryString);
397
        return self::jsonDecode($response->body(), true);
398
    }
399
    
400
    /**
401
     * subscribes a webhook to transaction events of one particular transaction
402
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
403
     * @param  string  $transaction     the transaction hash
404
     * @param  integer $confirmations   the amount of confirmations to send.
405
     * @return array                    associative array containing the response
406
     */
407 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...
408
        $postData = [
409
            'event_type'    => 'transaction',
410
            'transaction'   => $transaction,
411
            'confirmations' => $confirmations,
412
        ];
413
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
414
        return self::jsonDecode($response->body(), true);
415
    }
416
417
    /**
418
     * subscribes a webhook to transaction events on a particular address
419
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
420
     * @param  string  $address         the address hash
421
     * @param  integer $confirmations   the amount of confirmations to send.
422
     * @return array                    associative array containing the response
423
     */
424 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...
425
        $postData = [
426
            'event_type'    => 'address-transactions',
427
            'address'       => $address,
428
            'confirmations' => $confirmations,
429
        ];
430
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
431
        return self::jsonDecode($response->body(), true);
432
    }
433
434
    /**
435
     * batch subscribes a webhook to multiple transaction events
436
     *
437
     * @param  string $identifier   the unique identifier of the webhook
438
     * @param  array  $batchData    A 2D array of event data:
439
     *                              [address => $address, confirmations => $confirmations]
440
     *                              where $address is the address to subscibe to
441
     *                              and optionally $confirmations is the amount of confirmations
442
     * @return boolean              true on success
443
     */
444
    public function batchSubscribeAddressTransactions($identifier, $batchData) {
445
        $postData = [];
446
        foreach ($batchData as $record) {
447
            $postData[] = [
448
                'event_type' => 'address-transactions',
449
                'address' => $record['address'],
450
                'confirmations' => isset($record['confirmations']) ? $record['confirmations'] : 6,
451
            ];
452
        }
453
        $response = $this->client->post("webhook/{$identifier}/events/batch", null, $postData, RestClient::AUTH_HTTP_SIG);
454
        return self::jsonDecode($response->body(), true);
455
    }
456
457
    /**
458
     * subscribes a webhook to a new block event
459
     * @param  string  $identifier  the unique identifier of the webhook to be triggered
460
     * @return array                associative array containing the response
461
     */
462 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...
463
        $postData = [
464
            'event_type'    => 'block',
465
        ];
466
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
467
        return self::jsonDecode($response->body(), true);
468
    }
469
470
    /**
471
     * removes an transaction event subscription from a webhook
472
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
473
     * @param  string  $transaction     the transaction hash of the event subscription
474
     * @return boolean                  true on success
475
     */
476
    public function unsubscribeTransaction($identifier, $transaction) {
477
        $response = $this->client->delete("webhook/{$identifier}/transaction/{$transaction}", null, null, RestClient::AUTH_HTTP_SIG);
478
        return self::jsonDecode($response->body(), true);
479
    }
480
481
    /**
482
     * removes an address transaction event subscription from a webhook
483
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
484
     * @param  string  $address         the address hash of the event subscription
485
     * @return boolean                  true on success
486
     */
487
    public function unsubscribeAddressTransactions($identifier, $address) {
488
        $response = $this->client->delete("webhook/{$identifier}/address-transactions/{$address}", null, null, RestClient::AUTH_HTTP_SIG);
489
        return self::jsonDecode($response->body(), true);
490
    }
491
492
    /**
493
     * removes a block event subscription from a webhook
494
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
495
     * @return boolean                  true on success
496
     */
497
    public function unsubscribeNewBlocks($identifier) {
498
        $response = $this->client->delete("webhook/{$identifier}/block", null, null, RestClient::AUTH_HTTP_SIG);
499
        return self::jsonDecode($response->body(), true);
500
    }
501
502
    /**
503
     * create a new wallet
504
     *   - will generate a new primary seed (with password) and backup seed (without password)
505
     *   - send the primary seed (BIP39 'encrypted') and backup public key to the server
506
     *   - receive the blocktrail co-signing public key from the server
507
     *
508
     * Either takes one argument:
509
     * @param array $options
510
     *
511
     * Or takes three arguments (old, deprecated syntax):
512
     * (@nonPHP-doc) @param      $identifier
513
     * (@nonPHP-doc) @param      $password
514
     * (@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...
515
     *
516
     * @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...
517
     * @throws \Exception
518
     */
519
    public function createNewWallet($options) {
520
        if (!is_array($options)) {
521
            $args = func_get_args();
522
            $options = [
523
                "identifier" => $args[0],
524
                "password" => $args[1],
525
                "key_index" => isset($args[2]) ? $args[2] : null,
526
            ];
527
        }
528
529
        if (isset($options['password'])) {
530
            if (isset($options['passphrase'])) {
531
                throw new \InvalidArgumentException("Can only provide either passphrase or password");
532
            } else {
533
                $options['passphrase'] = $options['password'];
534
            }
535
        }
536
537
        if (!isset($options['passphrase'])) {
538
            $options['passphrase'] = null;
539
        }
540
541
        if (!isset($options['key_index'])) {
542
            $options['key_index'] = 0;
543
        }
544
545
        if (!isset($options['wallet_version'])) {
546
            $options['wallet_version'] = Wallet::WALLET_VERSION_V3;
547
        }
548
549
        switch ($options['wallet_version']) {
550
            case Wallet::WALLET_VERSION_V1:
551
                return $this->createNewWalletV1($options);
552
553
            case Wallet::WALLET_VERSION_V2:
554
                return $this->createNewWalletV2($options);
555
556
            case Wallet::WALLET_VERSION_V3:
557
                return $this->createNewWalletV3($options);
558
559
            default:
560
                throw new \InvalidArgumentException("Invalid wallet version");
561
        }
562
    }
563
564
    protected function createNewWalletV1($options) {
565
        $walletPath = WalletPath::create($options['key_index']);
566
567
        $storePrimaryMnemonic = isset($options['store_primary_mnemonic']) ? $options['store_primary_mnemonic'] : null;
568
569
        if (isset($options['primary_mnemonic']) && $options['primary_private_key']) {
570
            throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey");
571
        }
572
573
        $primaryMnemonic = null;
574
        $primaryPrivateKey = null;
575
        if (!isset($options['primary_mnemonic']) && !isset($options['primary_private_key'])) {
576
            if (!$options['passphrase']) {
577
                throw new \InvalidArgumentException("Can't generate Primary Mnemonic without a passphrase");
578
            } else {
579
                // create new primary seed
580
                /** @var HierarchicalKey $primaryPrivateKey */
581
                list($primaryMnemonic, , $primaryPrivateKey) = $this->newPrimarySeed($options['passphrase']);
582
                if ($storePrimaryMnemonic !== false) {
583
                    $storePrimaryMnemonic = true;
584
                }
585
            }
586
        } elseif (isset($options['primary_mnemonic'])) {
587
            $primaryMnemonic = $options['primary_mnemonic'];
588
        } elseif (isset($options['primary_private_key'])) {
589
            $primaryPrivateKey = $options['primary_private_key'];
590
        }
591
592
        if ($storePrimaryMnemonic && $primaryMnemonic && !$options['passphrase']) {
593
            throw new \InvalidArgumentException("Can't store Primary Mnemonic on server without a passphrase");
594
        }
595
596 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...
597
            if (is_string($primaryPrivateKey)) {
598
                $primaryPrivateKey = [$primaryPrivateKey, "m"];
599
            }
600
        } else {
601
            $primaryPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($primaryMnemonic, $options['passphrase']));
602
        }
603
604
        if (!$storePrimaryMnemonic) {
605
            $primaryMnemonic = false;
606
        }
607
608
        // create primary public key from the created private key
609
        $path = (string)$walletPath->keyIndexPath()->publicPath();
610
        $primaryPublicKey = BIP32Key::create($primaryPrivateKey->derivePath($path), $path);
611
612
        if (isset($options['backup_mnemonic']) && $options['backup_public_key']) {
613
            throw new \InvalidArgumentException("Can't specify Backup Mnemonic and Backup PublicKey");
614
        }
615
616
        $backupMnemonic = null;
617
        $backupPublicKey = null;
618
        if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) {
619
            /** @var HierarchicalKey $backupPrivateKey */
620
            list($backupMnemonic, , ) = $this->newBackupSeed();
621
        } else if (isset($options['backup_mnemonic'])) {
622
            $backupMnemonic = $options['backup_mnemonic'];
623
        } elseif (isset($options['backup_public_key'])) {
624
            $backupPublicKey = $options['backup_public_key'];
625
        }
626
627 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...
628
            if (is_string($backupPublicKey)) {
629
                $backupPublicKey = [$backupPublicKey, "m"];
630
            }
631
        } else {
632
            $backupPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, ""));
633
            $backupPublicKey = BIP32Key::create($backupPrivateKey->toPublic(), "M");
634
        }
635
636
        // create a checksum of our private key which we'll later use to verify we used the right password
637
        $checksum = $primaryPrivateKey->getPublicKey()->getAddress()->getAddress();
638
639
        // send the public keys to the server to store them
640
        //  and the mnemonic, which is safe because it's useless without the password
641
        $data = $this->storeNewWalletV1($options['identifier'], $primaryPublicKey->tuple(), $backupPublicKey->tuple(), $primaryMnemonic, $checksum, $options['key_index']);
642
643
        // received the blocktrail public keys
644 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...
645
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
646
        }, $data['blocktrail_public_keys']);
647
648
        $wallet = new WalletV1(
649
            $this,
650
            $options['identifier'],
651
            $primaryMnemonic,
652
            [$options['key_index'] => $primaryPublicKey],
653
            $backupPublicKey,
654
            $blocktrailPublicKeys,
655
            $options['key_index'],
656
            $this->network,
657
            $this->testnet,
658
            $checksum
659
        );
660
661
        $wallet->unlock($options);
662
663
        // return wallet and backup mnemonic
664
        return [
665
            $wallet,
666
            [
667
                'primary_mnemonic' => $primaryMnemonic,
668
                'backup_mnemonic' => $backupMnemonic,
669
                'blocktrail_public_keys' => $blocktrailPublicKeys,
670
            ],
671
        ];
672
    }
673
674
    public static function randomBits($bits) {
675
        return self::randomBytes($bits / 8);
676
    }
677
678
    public static function randomBytes($bytes) {
679
        return (new Random())->bytes($bytes)->getBinary();
680
    }
681
682
    protected function createNewWalletV2($options) {
683
        $walletPath = WalletPath::create($options['key_index']);
684
685
        if (isset($options['store_primary_mnemonic'])) {
686
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
687
        }
688
689 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...
690
            if (isset($options['primary_private_key'])) {
691
                $options['store_data_on_server'] = false;
692
            } else {
693
                $options['store_data_on_server'] = true;
694
            }
695
        }
696
697
        $storeDataOnServer = $options['store_data_on_server'];
698
699
        $secret = null;
700
        $encryptedSecret = null;
701
        $primarySeed = null;
702
        $encryptedPrimarySeed = null;
703
        $recoverySecret = null;
704
        $recoveryEncryptedSecret = null;
705
        $backupSeed = null;
706
707
        if (!isset($options['primary_private_key'])) {
708
            $primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : self::randomBits(256);
709
        }
710
711
        if ($storeDataOnServer) {
712
            if (!isset($options['secret'])) {
713
                if (!$options['passphrase']) {
714
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
715
                }
716
717
                $secret = bin2hex(self::randomBits(256)); // string because we use it as passphrase
718
                $encryptedSecret = CryptoJSAES::encrypt($secret, $options['passphrase']);
719
            } else {
720
                $secret = $options['secret'];
721
            }
722
723
            $encryptedPrimarySeed = CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
724
            $recoverySecret = bin2hex(self::randomBits(256));
725
726
            $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);
727
        }
728
729
        if (!isset($options['backup_public_key'])) {
730
            $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : self::randomBits(256);
731
        }
732
733 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...
734
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
735
        } else {
736
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m");
737
        }
738
739
        // create primary public key from the created private key
740
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
741
742 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...
743
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M");
744
        }
745
746
        // create a checksum of our private key which we'll later use to verify we used the right password
747
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
748
749
        // send the public keys and encrypted data to server
750
        $data = $this->storeNewWalletV2(
751
            $options['identifier'],
752
            $options['primary_public_key']->tuple(),
753
            $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...
754
            $storeDataOnServer ? $encryptedPrimarySeed : false,
755
            $storeDataOnServer ? $encryptedSecret : false,
756
            $storeDataOnServer ? $recoverySecret : false,
757
            $checksum,
758
            $options['key_index']
759
        );
760
761
        // received the blocktrail public keys
762 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...
763
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
764
        }, $data['blocktrail_public_keys']);
765
766
        $wallet = new WalletV2(
767
            $this,
768
            $options['identifier'],
769
            $encryptedPrimarySeed,
770
            $encryptedSecret,
771
            [$options['key_index'] => $options['primary_public_key']],
772
            $options['backup_public_key'],
773
            $blocktrailPublicKeys,
774
            $options['key_index'],
775
            $this->network,
776
            $this->testnet,
777
            $checksum
778
        );
779
780
        $wallet->unlock([
781
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
782
            'primary_private_key' => $options['primary_private_key'],
783
            'primary_seed' => $primarySeed,
784
            'secret' => $secret,
785
        ]);
786
787
        // return wallet and mnemonics for backup sheet
788
        return [
789
            $wallet,
790
            [
791
                'encrypted_primary_seed' => $encryptedPrimarySeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedPrimarySeed))) : null,
792
                'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer($backupSeed)) : null,
793
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($recoveryEncryptedSecret))) : null,
794
                'encrypted_secret' => $encryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedSecret))) : null,
795
                'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
796
                    return [$keyIndex, $pubKey->tuple()];
797
                }, $blocktrailPublicKeys),
798
            ],
799
        ];
800
    }
801
802
    protected function createNewWalletV3($options) {
803
        $walletPath = WalletPath::create($options['key_index']);
804
805
        if (isset($options['store_primary_mnemonic'])) {
806
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
807
        }
808
809 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...
810
            if (isset($options['primary_private_key'])) {
811
                $options['store_data_on_server'] = false;
812
            } else {
813
                $options['store_data_on_server'] = true;
814
            }
815
        }
816
817
        $storeDataOnServer = $options['store_data_on_server'];
818
819
        $secret = null;
820
        $encryptedSecret = null;
821
        $primarySeed = null;
822
        $encryptedPrimarySeed = null;
823
        $recoverySecret = null;
824
        $recoveryEncryptedSecret = null;
825
        $backupSeed = null;
826
827 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...
828
            if (isset($options['primary_seed'])) {
829
                if (!$options['primary_seed'] instanceof BufferInterface) {
830
                    throw new \InvalidArgumentException('Primary Seed should be passed as a Buffer');
831
                }
832
                $primarySeed = $options['primary_seed'];
833
            } else {
834
                $primarySeed = new Buffer(self::randomBits(256));
835
            }
836
        }
837
838
        if ($storeDataOnServer) {
839
            if (!isset($options['secret'])) {
840
                if (!$options['passphrase']) {
841
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
842
                }
843
844
                $secret = new Buffer(self::randomBits(256));
845
                $encryptedSecret = Encryption::encrypt($secret, new Buffer($options['passphrase']));
846
            } else {
847
                if (!$options['secret'] instanceof Buffer) {
848
                    throw new \RuntimeException('Secret must be provided as a Buffer');
849
                }
850
851
                $secret = $options['secret'];
852
            }
853
854
            $encryptedPrimarySeed = Encryption::encrypt($primarySeed, $secret);
855
            $recoverySecret = new Buffer(self::randomBits(256));
856
857
            $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret);
858
        }
859
860 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...
861
            if (isset($options['backup_seed'])) {
862
                if (!$options['backup_seed'] instanceof Buffer) {
863
                    throw new \RuntimeException('Backup seed must be an instance of Buffer');
864
                }
865
                $backupSeed = $options['backup_seed'];
866
            } else {
867
                $backupSeed = new Buffer(self::randomBits(256));
868
            }
869
        }
870
871 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...
872
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
873
        } else {
874
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m");
875
        }
876
877
        // create primary public key from the created private key
878
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
879
880 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...
881
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($backupSeed), "m")->buildKey("M");
882
        }
883
884
        // create a checksum of our private key which we'll later use to verify we used the right password
885
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
886
887
        // send the public keys and encrypted data to server
888
        $data = $this->storeNewWalletV3(
889
            $options['identifier'],
890
            $options['primary_public_key']->tuple(),
891
            $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...
892
            $storeDataOnServer ? base64_encode($encryptedPrimarySeed->getBinary()) : false,
893
            $storeDataOnServer ? base64_encode($encryptedSecret->getBinary()) : false,
894
            $storeDataOnServer ? $recoverySecret->getHex() : false,
895
            $checksum,
896
            $options['key_index']
897
        );
898
899
        // received the blocktrail public keys
900
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
901
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
902
        }, $data['blocktrail_public_keys']);
903
904
        $wallet = new WalletV3(
905
            $this,
906
            $options['identifier'],
907
            $encryptedPrimarySeed,
0 ignored issues
show
Bug introduced by
It seems like $encryptedPrimarySeed defined by null on line 822 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...
908
            $encryptedSecret,
0 ignored issues
show
Bug introduced by
It seems like $encryptedSecret defined by null on line 820 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...
909
            [$options['key_index'] => $options['primary_public_key']],
910
            $options['backup_public_key'],
911
            $blocktrailPublicKeys,
912
            $options['key_index'],
913
            $this->network,
914
            $this->testnet,
915
            $checksum
916
        );
917
918
        $wallet->unlock([
919
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
920
            'primary_private_key' => $options['primary_private_key'],
921
            'primary_seed' => $primarySeed,
922
            'secret' => $secret,
923
        ]);
924
925
        // return wallet and mnemonics for backup sheet
926
        return [
927
            $wallet,
928
            [
929
                'encrypted_primary_seed'    => $encryptedPrimarySeed ? Mnemonic::encode($encryptedPrimarySeed) : null,
930
                'backup_seed'               => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic($backupSeed) : null,
931
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? Mnemonic::encode($recoveryEncryptedSecret) : null,
932
                'encrypted_secret'          => $encryptedSecret ? Mnemonic::encode($encryptedSecret) : null,
933
                'blocktrail_public_keys'    => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
934
                    return [$keyIndex, $pubKey->tuple()];
935
                }, $blocktrailPublicKeys),
936
            ]
937
        ];
938
    }
939
940
    /**
941
     * create wallet using the API
942
     *
943
     * @param string    $identifier             the wallet identifier to create
944
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
945
     * @param string    $backupPublicKey        plain public key
946
     * @param string    $primaryMnemonic        mnemonic to store
947
     * @param string    $checksum               checksum to store
948
     * @param int       $keyIndex               account that we expect to use
949
     * @return mixed
950
     */
951
    public function storeNewWalletV1($identifier, $primaryPublicKey, $backupPublicKey, $primaryMnemonic, $checksum, $keyIndex) {
952
        $data = [
953
            'identifier' => $identifier,
954
            'primary_public_key' => $primaryPublicKey,
955
            'backup_public_key' => $backupPublicKey,
956
            'primary_mnemonic' => $primaryMnemonic,
957
            'checksum' => $checksum,
958
            'key_index' => $keyIndex
959
        ];
960
961
        $response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
962
        return self::jsonDecode($response->body(), true);
963
    }
964
965
    /**
966
     * create wallet using the API
967
     *
968
     * @param string $identifier       the wallet identifier to create
969
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
970
     * @param string $backupPublicKey  plain public key
971
     * @param        $encryptedPrimarySeed
972
     * @param        $encryptedSecret
973
     * @param        $recoverySecret
974
     * @param string $checksum         checksum to store
975
     * @param int    $keyIndex         account that we expect to use
976
     * @return mixed
977
     * @throws \Exception
978
     */
979 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...
980
        $data = [
981
            'identifier' => $identifier,
982
            'wallet_version' => Wallet::WALLET_VERSION_V2,
983
            'primary_public_key' => $primaryPublicKey,
984
            'backup_public_key' => $backupPublicKey,
985
            'encrypted_primary_seed' => $encryptedPrimarySeed,
986
            'encrypted_secret' => $encryptedSecret,
987
            'recovery_secret' => $recoverySecret,
988
            'checksum' => $checksum,
989
            'key_index' => $keyIndex
990
        ];
991
992
        $response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
993
        return self::jsonDecode($response->body(), true);
994
    }
995
996
    /**
997
     * create wallet using the API
998
     *
999
     * @param string $identifier       the wallet identifier to create
1000
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1001
     * @param string $backupPublicKey  plain public key
1002
     * @param        $encryptedPrimarySeed
1003
     * @param        $encryptedSecret
1004
     * @param        $recoverySecret
1005
     * @param string $checksum         checksum to store
1006
     * @param int    $keyIndex         account that we expect to use
1007
     * @return mixed
1008
     * @throws \Exception
1009
     */
1010 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...
1011
        $data = [
1012
            'identifier' => $identifier,
1013
            'wallet_version' => Wallet::WALLET_VERSION_V3,
1014
            'primary_public_key' => $primaryPublicKey,
1015
            'backup_public_key' => $backupPublicKey,
1016
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1017
            'encrypted_secret' => $encryptedSecret,
1018
            'recovery_secret' => $recoverySecret,
1019
            'checksum' => $checksum,
1020
            'key_index' => $keyIndex
1021
        ];
1022
1023
        $response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1024
        return self::jsonDecode($response->body(), true);
1025
    }
1026
1027
    /**
1028
     * upgrade wallet to use a new account number
1029
     *  the account number specifies which blocktrail cosigning key is used
1030
     *
1031
     * @param string    $identifier             the wallet identifier to be upgraded
1032
     * @param int       $keyIndex               the new account to use
1033
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1034
     * @return mixed
1035
     */
1036
    public function upgradeKeyIndex($identifier, $keyIndex, $primaryPublicKey) {
1037
        $data = [
1038
            'key_index' => $keyIndex,
1039
            'primary_public_key' => $primaryPublicKey
1040
        ];
1041
1042
        $response = $this->client->post("wallet/{$identifier}/upgrade", null, $data, RestClient::AUTH_HTTP_SIG);
1043
        return self::jsonDecode($response->body(), true);
1044
    }
1045
1046
    /**
1047
     * initialize a previously created wallet
1048
     *
1049
     * Either takes one argument:
1050
     * @param array $options
1051
     *
1052
     * Or takes two arguments (old, deprecated syntax):
1053
     * (@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...
1054
     * (@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...
1055
     *
1056
     * @return WalletInterface
1057
     * @throws \Exception
1058
     */
1059
    public function initWallet($options) {
1060
        if (!is_array($options)) {
1061
            $args = func_get_args();
1062
            $options = [
1063
                "identifier" => $args[0],
1064
                "password" => $args[1],
1065
            ];
1066
        }
1067
1068
        $identifier = $options['identifier'];
1069
        $readonly = isset($options['readonly']) ? $options['readonly'] :
1070
                    (isset($options['readOnly']) ? $options['readOnly'] :
1071
                        (isset($options['read-only']) ? $options['read-only'] :
1072
                            false));
1073
1074
        // get the wallet data from the server
1075
        $data = $this->getWallet($identifier);
1076
1077
        if (!$data) {
1078
            throw new \Exception("Failed to get wallet");
1079
        }
1080
1081
        switch ($data['wallet_version']) {
1082
            case Wallet::WALLET_VERSION_V1:
1083
                $wallet = new WalletV1(
1084
                    $this,
1085
                    $identifier,
1086
                    isset($options['primary_mnemonic']) ? $options['primary_mnemonic'] : $data['primary_mnemonic'],
1087
                    $data['primary_public_keys'],
1088
                    $data['backup_public_key'],
1089
                    $data['blocktrail_public_keys'],
1090
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1091
                    $this->network,
1092
                    $this->testnet,
1093
                    $data['checksum']
1094
                );
1095
                break;
1096
            case Wallet::WALLET_VERSION_V2:
1097
                $wallet = new WalletV2(
1098
                    $this,
1099
                    $identifier,
1100
                    isset($options['encrypted_primary_seed']) ? $options['encrypted_primary_seed'] : $data['encrypted_primary_seed'],
1101
                    isset($options['encrypted_secret']) ? $options['encrypted_secret'] : $data['encrypted_secret'],
1102
                    $data['primary_public_keys'],
1103
                    $data['backup_public_key'],
1104
                    $data['blocktrail_public_keys'],
1105
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1106
                    $this->network,
1107
                    $this->testnet,
1108
                    $data['checksum']
1109
                );
1110
                break;
1111
            case Wallet::WALLET_VERSION_V3:
1112 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...
1113
                    if (!$options['encrypted_primary_seed'] instanceof Buffer) {
1114
                        throw new \InvalidArgumentException('Encrypted PrimarySeed must be provided as a Buffer');
1115
                    }
1116
                    $encryptedPrimarySeed = $data['encrypted_primary_seed'];
1117
                } else {
1118
                    $encryptedPrimarySeed = new Buffer(base64_decode($data['encrypted_primary_seed']));
1119
                }
1120
1121 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...
1122
                    if (!$options['encrypted_secret'] instanceof Buffer) {
1123
                        throw new \InvalidArgumentException('Encrypted secret must be provided as a Buffer');
1124
                    }
1125
1126
                    $encryptedSecret = $data['encrypted_secret'];
1127
                } else {
1128
                    $encryptedSecret = new Buffer(base64_decode($data['encrypted_secret']));
1129
                }
1130
1131
                $wallet = new WalletV3(
1132
                    $this,
1133
                    $identifier,
1134
                    $encryptedPrimarySeed,
1135
                    $encryptedSecret,
1136
                    $data['primary_public_keys'],
1137
                    $data['backup_public_key'],
1138
                    $data['blocktrail_public_keys'],
1139
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1140
                    $this->network,
1141
                    $this->testnet,
1142
                    $data['checksum']
1143
                );
1144
                break;
1145
            default:
1146
                throw new \InvalidArgumentException("Invalid wallet version");
1147
        }
1148
1149
        if (!$readonly) {
1150
            $wallet->unlock($options);
1151
        }
1152
1153
        return $wallet;
1154
    }
1155
1156
    /**
1157
     * get the wallet data from the server
1158
     *
1159
     * @param string    $identifier             the identifier of the wallet
1160
     * @return mixed
1161
     */
1162
    public function getWallet($identifier) {
1163
        $response = $this->client->get("wallet/{$identifier}", null, RestClient::AUTH_HTTP_SIG);
1164
        return self::jsonDecode($response->body(), true);
1165
    }
1166
1167
    /**
1168
     * update the wallet data on the server
1169
     *
1170
     * @param string    $identifier
1171
     * @param $data
1172
     * @return mixed
1173
     */
1174
    public function updateWallet($identifier, $data) {
1175
        $response = $this->client->post("wallet/{$identifier}", null, $data, RestClient::AUTH_HTTP_SIG);
1176
        return self::jsonDecode($response->body(), true);
1177
    }
1178
1179
    /**
1180
     * delete a wallet from the server
1181
     *  the checksum address and a signature to verify you ownership of the key of that checksum address
1182
     *  is required to be able to delete a wallet
1183
     *
1184
     * @param string    $identifier             the identifier of the wallet
1185
     * @param string    $checksumAddress        the address for your master private key (and the checksum used when creating the wallet)
1186
     * @param string    $signature              a signature of the checksum address as message signed by the private key matching that address
1187
     * @param bool      $force                  ignore warnings (such as a non-zero balance)
1188
     * @return mixed
1189
     */
1190
    public function deleteWallet($identifier, $checksumAddress, $signature, $force = false) {
1191
        $response = $this->client->delete("wallet/{$identifier}", ['force' => $force], [
1192
            'checksum' => $checksumAddress,
1193
            'signature' => $signature
1194
        ], RestClient::AUTH_HTTP_SIG, 360);
1195
        return self::jsonDecode($response->body(), true);
1196
    }
1197
1198
    /**
1199
     * create new backup key;
1200
     *  1) a BIP39 mnemonic
1201
     *  2) a seed from that mnemonic with a blank password
1202
     *  3) a private key from that seed
1203
     *
1204
     * @return array [mnemonic, seed, key]
1205
     */
1206
    protected function newBackupSeed() {
1207
        list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed("");
1208
1209
        return [$backupMnemonic, $backupSeed, $backupPrivateKey];
1210
    }
1211
1212
    /**
1213
     * create new primary key;
1214
     *  1) a BIP39 mnemonic
1215
     *  2) a seed from that mnemonic with the password
1216
     *  3) a private key from that seed
1217
     *
1218
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1219
     * @return array [mnemonic, seed, key]
1220
     * @TODO: require a strong password?
1221
     */
1222
    protected function newPrimarySeed($passphrase) {
1223
        list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase);
1224
1225
        return [$primaryMnemonic, $primarySeed, $primaryPrivateKey];
1226
    }
1227
1228
    /**
1229
     * create a new key;
1230
     *  1) a BIP39 mnemonic
1231
     *  2) a seed from that mnemonic with the password
1232
     *  3) a private key from that seed
1233
     *
1234
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1235
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1236
     * @return array
1237
     */
1238
    protected function generateNewSeed($passphrase = "", $forceEntropy = null) {
1239
        // generate master seed, retry if the generated private key isn't valid (FALSE is returned)
1240
        do {
1241
            $mnemonic = $this->generateNewMnemonic($forceEntropy);
1242
1243
            $seed = (new Bip39SeedGenerator)->getSeed($mnemonic, $passphrase);
1244
1245
            $key = null;
1246
            try {
1247
                $key = HierarchicalKeyFactory::fromEntropy($seed);
1248
            } catch (\Exception $e) {
1249
                // try again
1250
            }
1251
        } while (!$key);
1252
1253
        return [$mnemonic, $seed, $key];
1254
    }
1255
1256
    /**
1257
     * generate a new mnemonic from some random entropy (512 bit)
1258
     *
1259
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1260
     * @return string
1261
     * @throws \Exception
1262
     */
1263
    protected function generateNewMnemonic($forceEntropy = null) {
1264
        if ($forceEntropy === null) {
1265
            $random = new Random();
1266
            $entropy = $random->bytes(512 / 8);
1267
        } else {
1268
            $entropy = $forceEntropy;
1269
        }
1270
1271
        return MnemonicFactory::bip39()->entropyToMnemonic($entropy);
0 ignored issues
show
Bug introduced by
It seems like $entropy defined by $forceEntropy on line 1268 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...
1272
    }
1273
1274
    /**
1275
     * get the balance for the wallet
1276
     *
1277
     * @param string    $identifier             the identifier of the wallet
1278
     * @return array
1279
     */
1280
    public function getWalletBalance($identifier) {
1281
        $response = $this->client->get("wallet/{$identifier}/balance", null, RestClient::AUTH_HTTP_SIG);
1282
        return self::jsonDecode($response->body(), true);
1283
    }
1284
1285
    /**
1286
     * do HD wallet discovery for the wallet
1287
     *
1288
     * this can be REALLY slow, so we've set the timeout to 120s ...
1289
     *
1290
     * @param string    $identifier             the identifier of the wallet
1291
     * @param int       $gap                    the gap setting to use for discovery
1292
     * @return mixed
1293
     */
1294
    public function doWalletDiscovery($identifier, $gap = 200) {
1295
        $response = $this->client->get("wallet/{$identifier}/discovery", ['gap' => $gap], RestClient::AUTH_HTTP_SIG, 360.0);
1296
        return self::jsonDecode($response->body(), true);
1297
    }
1298
1299
    /**
1300
     * get a new derivation number for specified parent path
1301
     *  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
1302
     *
1303
     * returns the path
1304
     *
1305
     * @param string    $identifier             the identifier of the wallet
1306
     * @param string    $path                   the parent path for which to get a new derivation
1307
     * @return string
1308
     */
1309
    public function getNewDerivation($identifier, $path) {
1310
        $result = $this->_getNewDerivation($identifier, $path);
1311
        return $result['path'];
1312
    }
1313
1314
    /**
1315
     * get a new derivation number for specified parent path
1316
     *  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
1317
     *
1318
     * @param string    $identifier             the identifier of the wallet
1319
     * @param string    $path                   the parent path for which to get a new derivation
1320
     * @return mixed
1321
     */
1322
    public function _getNewDerivation($identifier, $path) {
1323
        $response = $this->client->post("wallet/{$identifier}/path", null, ['path' => $path], RestClient::AUTH_HTTP_SIG);
1324
        return self::jsonDecode($response->body(), true);
1325
    }
1326
1327
    /**
1328
     * get the path (and redeemScript) to specified address
1329
     *
1330
     * @param string $identifier
1331
     * @param string $address
1332
     * @return array
1333
     * @throws \Exception
1334
     */
1335
    public function getPathForAddress($identifier, $address) {
1336
        $response = $this->client->post("wallet/{$identifier}/path_for_address", null, ['address' => $address], RestClient::AUTH_HTTP_SIG);
1337
        return self::jsonDecode($response->body(), true)['path'];
1338
    }
1339
1340
    /**
1341
     * send the transaction using the API
1342
     *
1343
     * @param string    $identifier             the identifier of the wallet
1344
     * @param string    $rawTransaction         raw hex of the transaction (should be partially signed)
1345
     * @param array     $paths                  list of the paths that were used for the UTXO
1346
     * @param bool      $checkFee               let the server verify the fee after signing
1347
     * @return string                           the complete raw transaction
1348
     * @throws \Exception
1349
     */
1350
    public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee = false) {
1351
        $data = [
1352
            'raw_transaction' => $rawTransaction,
1353
            'paths' => $paths
1354
        ];
1355
1356
        // dynamic TTL for when we're signing really big transactions
1357
        $ttl = max(5.0, count($paths) * 0.25) + 4.0;
1358
1359
        $response = $this->client->post("wallet/{$identifier}/send", ['check_fee' => (int)!!$checkFee], $data, RestClient::AUTH_HTTP_SIG, $ttl);
1360
        $signed = self::jsonDecode($response->body(), true);
1361
1362
        if (!$signed['complete'] || $signed['complete'] == 'false') {
1363
            throw new \Exception("Failed to completely sign transaction");
1364
        }
1365
1366
        // create TX hash from the raw signed hex
1367
        return TransactionFactory::fromHex($signed['hex'])->getTxId()->getHex();
1368
    }
1369
1370
    /**
1371
     * use the API to get the best inputs to use based on the outputs
1372
     *
1373
     * the return array has the following format:
1374
     * [
1375
     *  "utxos" => [
1376
     *      [
1377
     *          "hash" => "<txHash>",
1378
     *          "idx" => "<index of the output of that <txHash>",
1379
     *          "scriptpubkey_hex" => "<scriptPubKey-hex>",
1380
     *          "value" => 32746327,
1381
     *          "address" => "1address",
1382
     *          "path" => "m/44'/1'/0'/0/13",
1383
     *          "redeem_script" => "<redeemScript-hex>",
1384
     *      ],
1385
     *  ],
1386
     *  "fee"   => 10000,
1387
     *  "change"=> 1010109201,
1388
     * ]
1389
     *
1390
     * @param string   $identifier              the identifier of the wallet
1391
     * @param array    $outputs                 the outputs you want to create - array[address => satoshi-value]
1392
     * @param bool     $lockUTXO                when TRUE the UTXOs selected will be locked for a few seconds
1393
     *                                          so you have some time to spend them without race-conditions
1394
     * @param bool     $allowZeroConf
1395
     * @param string   $feeStrategy
1396
     * @param null|int $forceFee
1397
     * @return array
1398
     * @throws \Exception
1399
     */
1400 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...
1401
        $args = [
1402
            'lock' => (int)!!$lockUTXO,
1403
            'zeroconf' => (int)!!$allowZeroConf,
1404
            'fee_strategy' => $feeStrategy,
1405
        ];
1406
1407
        if ($forceFee !== null) {
1408
            $args['forcefee'] = (int)$forceFee;
1409
        }
1410
1411
        $response = $this->client->post(
1412
            "wallet/{$identifier}/coin-selection",
1413
            $args,
1414
            $outputs,
1415
            RestClient::AUTH_HTTP_SIG
1416
        );
1417
1418
        return self::jsonDecode($response->body(), true);
1419
    }
1420
1421
    /**
1422
     *
1423
     * @param string   $identifier the identifier of the wallet
1424
     * @param bool     $allowZeroConf
1425
     * @param string   $feeStrategy
1426
     * @param null|int $forceFee
1427
     * @param int      $outputCnt
1428
     * @return array
1429
     * @throws \Exception
1430
     */
1431 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...
1432
        $args = [
1433
            'zeroconf' => (int)!!$allowZeroConf,
1434
            'fee_strategy' => $feeStrategy,
1435
            'outputs' => $outputCnt,
1436
        ];
1437
1438
        if ($forceFee !== null) {
1439
            $args['forcefee'] = (int)$forceFee;
1440
        }
1441
1442
        $response = $this->client->get(
1443
            "wallet/{$identifier}/max-spendable",
1444
            $args,
1445
            RestClient::AUTH_HTTP_SIG
1446
        );
1447
1448
        return self::jsonDecode($response->body(), true);
1449
    }
1450
1451
    /**
1452
     * @return array        ['optimal_fee' => 10000, 'low_priority_fee' => 5000]
1453
     */
1454
    public function feePerKB() {
1455
        $response = $this->client->get("fee-per-kb");
1456
        return self::jsonDecode($response->body(), true);
1457
    }
1458
1459
    /**
1460
     * get the current price index
1461
     *
1462
     * @return array        eg; ['USD' => 287.30]
1463
     */
1464
    public function price() {
1465
        $response = $this->client->get("price");
1466
        return self::jsonDecode($response->body(), true);
1467
    }
1468
1469
    /**
1470
     * setup webhook for wallet
1471
     *
1472
     * @param string    $identifier         the wallet identifier for which to create the webhook
1473
     * @param string    $webhookIdentifier  the webhook identifier to use
1474
     * @param string    $url                the url to receive the webhook events
1475
     * @return array
1476
     */
1477
    public function setupWalletWebhook($identifier, $webhookIdentifier, $url) {
1478
        $response = $this->client->post("wallet/{$identifier}/webhook", null, ['url' => $url, 'identifier' => $webhookIdentifier], RestClient::AUTH_HTTP_SIG);
1479
        return self::jsonDecode($response->body(), true);
1480
    }
1481
1482
    /**
1483
     * delete webhook for wallet
1484
     *
1485
     * @param string    $identifier         the wallet identifier for which to delete the webhook
1486
     * @param string    $webhookIdentifier  the webhook identifier to delete
1487
     * @return array
1488
     */
1489
    public function deleteWalletWebhook($identifier, $webhookIdentifier) {
1490
        $response = $this->client->delete("wallet/{$identifier}/webhook/{$webhookIdentifier}", null, null, RestClient::AUTH_HTTP_SIG);
1491
        return self::jsonDecode($response->body(), true);
1492
    }
1493
1494
    /**
1495
     * lock a specific unspent output
1496
     *
1497
     * @param     $identifier
1498
     * @param     $txHash
1499
     * @param     $txIdx
1500
     * @param int $ttl
1501
     * @return bool
1502
     */
1503
    public function lockWalletUTXO($identifier, $txHash, $txIdx, $ttl = 3) {
1504
        $response = $this->client->post("wallet/{$identifier}/lock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx, 'ttl' => $ttl], RestClient::AUTH_HTTP_SIG);
1505
        return self::jsonDecode($response->body(), true)['locked'];
1506
    }
1507
1508
    /**
1509
     * unlock a specific unspent output
1510
     *
1511
     * @param     $identifier
1512
     * @param     $txHash
1513
     * @param     $txIdx
1514
     * @return bool
1515
     */
1516
    public function unlockWalletUTXO($identifier, $txHash, $txIdx) {
1517
        $response = $this->client->post("wallet/{$identifier}/unlock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx], RestClient::AUTH_HTTP_SIG);
1518
        return self::jsonDecode($response->body(), true)['unlocked'];
1519
    }
1520
1521
    /**
1522
     * get all transactions for wallet (paginated)
1523
     *
1524
     * @param  string  $identifier  the wallet identifier for which to get transactions
1525
     * @param  integer $page        pagination: page number
1526
     * @param  integer $limit       pagination: records per page (max 500)
1527
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1528
     * @return array                associative array containing the response
1529
     */
1530 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...
1531
        $queryString = [
1532
            'page' => $page,
1533
            'limit' => $limit,
1534
            'sort_dir' => $sortDir
1535
        ];
1536
        $response = $this->client->get("wallet/{$identifier}/transactions", $queryString, RestClient::AUTH_HTTP_SIG);
1537
        return self::jsonDecode($response->body(), true);
1538
    }
1539
1540
    /**
1541
     * get all addresses for wallet (paginated)
1542
     *
1543
     * @param  string  $identifier  the wallet identifier for which to get addresses
1544
     * @param  integer $page        pagination: page number
1545
     * @param  integer $limit       pagination: records per page (max 500)
1546
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1547
     * @return array                associative array containing the response
1548
     */
1549 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...
1550
        $queryString = [
1551
            'page' => $page,
1552
            'limit' => $limit,
1553
            'sort_dir' => $sortDir
1554
        ];
1555
        $response = $this->client->get("wallet/{$identifier}/addresses", $queryString, RestClient::AUTH_HTTP_SIG);
1556
        return self::jsonDecode($response->body(), true);
1557
    }
1558
1559
    /**
1560
     * get all UTXOs for wallet (paginated)
1561
     *
1562
     * @param  string  $identifier  the wallet identifier for which to get addresses
1563
     * @param  integer $page        pagination: page number
1564
     * @param  integer $limit       pagination: records per page (max 500)
1565
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1566
     * @return array                associative array containing the response
1567
     */
1568 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...
1569
        $queryString = [
1570
            'page' => $page,
1571
            'limit' => $limit,
1572
            'sort_dir' => $sortDir
1573
        ];
1574
        $response = $this->client->get("wallet/{$identifier}/utxos", $queryString, RestClient::AUTH_HTTP_SIG);
1575
        return self::jsonDecode($response->body(), true);
1576
    }
1577
1578
    /**
1579
     * get a paginated list of all wallets associated with the api user
1580
     *
1581
     * @param  integer          $page    pagination: page number
1582
     * @param  integer          $limit   pagination: records per page
1583
     * @return array                     associative array containing the response
1584
     */
1585 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...
1586
        $queryString = [
1587
            'page' => $page,
1588
            'limit' => $limit
1589
        ];
1590
        $response = $this->client->get("wallets", $queryString, RestClient::AUTH_HTTP_SIG);
1591
        return self::jsonDecode($response->body(), true);
1592
    }
1593
1594
    /**
1595
     * send raw transaction
1596
     *
1597
     * @param     $txHex
1598
     * @return bool
1599
     */
1600
    public function sendRawTransaction($txHex) {
1601
        $response = $this->client->post("send-raw-tx", null, ['hex' => $txHex], RestClient::AUTH_HTTP_SIG);
1602
        return self::jsonDecode($response->body(), true);
1603
    }
1604
1605
    /**
1606
     * testnet only ;-)
1607
     *
1608
     * @param     $address
1609
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1610
     * @return mixed
1611
     * @throws \Exception
1612
     */
1613 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...
1614
        $response = $this->client->post("faucet/withdrawl", null, [
1615
            'address' => $address,
1616
            'amount' => $amount,
1617
        ], RestClient::AUTH_HTTP_SIG);
1618
        return self::jsonDecode($response->body(), true);
1619
    }
1620
1621
    /**
1622
     * verify a message signed bitcoin-core style
1623
     *
1624
     * @param  string           $message
1625
     * @param  string           $address
1626
     * @param  string           $signature
1627
     * @return boolean
1628
     */
1629
    public function verifyMessage($message, $address, $signature) {
1630
        // we could also use the API instead of the using BitcoinLib to verify
1631
        // $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...
1632
1633
        $adapter = Bitcoin::getEcAdapter();
1634
        $addr = AddressFactory::fromString($address);
1635
        if (!$addr instanceof PayToPubKeyHashAddress) {
1636
            throw new \RuntimeException('Can only verify a message with a pay-to-pubkey-hash address');
1637
        }
1638
1639
        /** @var CompactSignatureSerializerInterface $csSerializer */
1640
        $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...
1641
        $signedMessage = new SignedMessage($message, $csSerializer->parse(new Buffer(base64_decode($signature))));
1642
1643
        $signer = new MessageSigner($adapter);
1644
        return $signer->verify($signedMessage, $addr);
1645
    }
1646
1647
    /**
1648
     * convert a Satoshi value to a BTC value
1649
     *
1650
     * @param int       $satoshi
1651
     * @return float
1652
     */
1653
    public static function toBTC($satoshi) {
1654
        return bcdiv((int)(string)$satoshi, 100000000, 8);
1655
    }
1656
1657
    /**
1658
     * convert a Satoshi value to a BTC value and return it as a string
1659
1660
     * @param int       $satoshi
1661
     * @return string
1662
     */
1663
    public static function toBTCString($satoshi) {
1664
        return sprintf("%.8f", self::toBTC($satoshi));
1665
    }
1666
1667
    /**
1668
     * convert a BTC value to a Satoshi value
1669
     *
1670
     * @param float     $btc
1671
     * @return string
1672
     */
1673
    public static function toSatoshiString($btc) {
1674
        return bcmul(sprintf("%.8f", (float)$btc), 100000000, 0);
1675
    }
1676
1677
    /**
1678
     * convert a BTC value to a Satoshi value
1679
     *
1680
     * @param float     $btc
1681
     * @return string
1682
     */
1683
    public static function toSatoshi($btc) {
1684
        return (int)self::toSatoshiString($btc);
1685
    }
1686
1687
    /**
1688
     * json_decode helper that throws exceptions when it fails to decode
1689
     *
1690
     * @param      $json
1691
     * @param bool $assoc
1692
     * @return mixed
1693
     * @throws \Exception
1694
     */
1695
    protected static function jsonDecode($json, $assoc = false) {
1696
        if (!$json) {
1697
            throw new \Exception("Can't json_decode empty string [{$json}]");
1698
        }
1699
1700
        $data = json_decode($json, $assoc);
1701
1702
        if ($data === null) {
1703
            throw new \Exception("Failed to json_decode [{$json}]");
1704
        }
1705
1706
        return $data;
1707
    }
1708
1709
    /**
1710
     * sort public keys for multisig script
1711
     *
1712
     * @param PublicKeyInterface[] $pubKeys
1713
     * @return PublicKeyInterface[]
1714
     */
1715
    public static function sortMultisigKeys(array $pubKeys) {
1716
        $result = array_values($pubKeys);
1717
        usort($result, function (PublicKeyInterface $a, PublicKeyInterface $b) {
1718
            $av = $a->getHex();
1719
            $bv = $b->getHex();
1720
            return $av == $bv ? 0 : $av > $bv ? 1 : -1;
1721
        });
1722
1723
        return $result;
1724
    }
1725
1726
    /**
1727
     * read and decode the json payload from a webhook's POST request.
1728
     *
1729
     * @param bool $returnObject    flag to indicate if an object or associative array should be returned
1730
     * @return mixed|null
1731
     * @throws \Exception
1732
     */
1733
    public static function getWebhookPayload($returnObject = false) {
1734
        $data = file_get_contents("php://input");
1735
        if ($data) {
1736
            return self::jsonDecode($data, !$returnObject);
1737
        } else {
1738
            return null;
1739
        }
1740
    }
1741
1742
    public static function normalizeBIP32KeyArray($keys) {
1743
        return Util::arrayMapWithIndex(function ($idx, $key) {
1744
            return [$idx, self::normalizeBIP32Key($key)];
1745
        }, $keys);
1746
    }
1747
1748
    public static function normalizeBIP32Key($key) {
1749
        if ($key instanceof BIP32Key) {
1750
            return $key;
1751
        }
1752
1753
        if (is_array($key)) {
1754
            $path = $key[1];
1755
            $key = $key[0];
1756
1757
            if (!($key instanceof HierarchicalKey)) {
1758
                $key = HierarchicalKeyFactory::fromExtended($key);
1759
            }
1760
1761
            return BIP32Key::create($key, $path);
1762
        } else {
1763
            throw new \Exception("Bad Input");
1764
        }
1765
    }
1766
}
1767