Completed
Branch master (6e123f)
by
unknown
08:39
created

BlocktrailSDK::verifyMessage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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