Completed
Branch master (fe98a4)
by
unknown
08:33
created

BlocktrailSDK::transactions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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