Completed
Pull Request — master (#99)
by thomas
42:14 queued 39:21
created

BlocktrailSDK::addressTransactions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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