Completed
Pull Request — master (#99)
by thomas
20:15 queued 36s
created

BlocktrailSDK::address()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\PayToPubKeyHashAddress;
6
use BitWasp\Bitcoin\Bitcoin;
7
use BitWasp\Bitcoin\Crypto\EcAdapter\EcSerializer;
8
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
9
use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\CompactSignatureSerializerInterface;
10
use BitWasp\Bitcoin\Crypto\Random\Random;
11
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKey;
12
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
13
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
14
use BitWasp\Bitcoin\MessageSigner\SignedMessage;
15
use BitWasp\Bitcoin\Mnemonic\Bip39\Bip39SeedGenerator;
16
use BitWasp\Bitcoin\Mnemonic\MnemonicFactory;
17
use BitWasp\Bitcoin\Network\NetworkFactory;
18
use BitWasp\Bitcoin\Transaction\TransactionFactory;
19
use BitWasp\Buffertools\Buffer;
20
use BitWasp\Buffertools\BufferInterface;
21
use Blocktrail\CryptoJSAES\CryptoJSAES;
22
use Blocktrail\SDK\Address\AddressReaderBase;
23
use Blocktrail\SDK\Address\BitcoinAddressReader;
24
use Blocktrail\SDK\Address\BitcoinCashAddressReader;
25
use Blocktrail\SDK\Address\CashAddress;
26
use Blocktrail\SDK\Bitcoin\BIP32Key;
27
use Blocktrail\SDK\Connection\RestClient;
28
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
29
use Blocktrail\SDK\Network\BitcoinCash;
30
use Blocktrail\SDK\V3Crypt\Encryption;
31
use Blocktrail\SDK\V3Crypt\EncryptionMnemonic;
32
use Blocktrail\SDK\V3Crypt\KeyDerivation;
33
34
/**
35
 * Class BlocktrailSDK
36
 */
37
class BlocktrailSDK implements BlocktrailSDKInterface {
38
    /**
39
     * @var Connection\RestClient
40
     */
41
    protected $client;
42
43
    /**
44
     * @var string          currently only supporting; bitcoin
45
     */
46
    protected $network;
47
48
    /**
49
     * @var bool
50
     */
51
    protected $testnet;
52
53
    /**
54
     * @param   string      $apiKey         the API_KEY to use for authentication
55
     * @param   string      $apiSecret      the API_SECRET to use for authentication
56
     * @param   string      $network        the cryptocurrency 'network' to consume, eg BTC, LTC, etc
57
     * @param   bool        $testnet        testnet yes/no
58
     * @param   string      $apiVersion     the version of the API to consume
59
     * @param   null        $apiEndpoint    overwrite the endpoint used
60
     *                                       this will cause the $network, $testnet and $apiVersion to be ignored!
61
     */
62 116
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
63
64 116
        list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet);
65
66 116
        if (is_null($apiEndpoint)) {
67 116
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com";
68 116
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
69
        }
70
71
        // normalize network and set bitcoinlib to the right magic-bytes
72 116
        list($this->network, $this->testnet) = $this->normalizeNetwork($network, $testnet);
73 116
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet);
74
75 116
        $this->client = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
76 116
    }
77
78
    /**
79
     * normalize network string
80
     *
81
     * @param $network
82
     * @param $testnet
83
     * @return array
84
     * @throws \Exception
85
     */
86 116
    protected function normalizeNetwork($network, $testnet) {
87 116
        return Util::normalizeNetwork($network, $testnet);
88
    }
89
90
    /**
91
     * set BitcoinLib to the correct magic-byte defaults for the selected network
92
     *
93
     * @param $network
94
     * @param $testnet
95
     */
96 116
    protected function setBitcoinLibMagicBytes($network, $testnet) {
97 116
        assert($network == "bitcoin" || $network == "bitcoincash");
98 116
        if ($network === "bitcoin") {
99 116
            if ($testnet) {
100 29
                $useNetwork = NetworkFactory::bitcoinTestnet();
101
            } else {
102 116
                $useNetwork = NetworkFactory::bitcoin();
103
            }
104 4
        } else if ($network === "bitcoincash") {
105 4
            $useNetwork = new BitcoinCash((bool) $testnet);
106
        }
107
108 116
        Bitcoin::setNetwork($useNetwork);
0 ignored issues
show
Bug introduced by
The variable $useNetwork does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

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