Completed
Pull Request — master (#88)
by thomas
05:05
created

BlocktrailSDK::jsonDecode()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.2098

Importance

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

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
99 81
        assert($network == "bitcoin" || $network == "bitcoincash");
100
        //Bitcoin::setNetwork($testnet ? NetworkFactory::bitcoinTestnet() : NetworkFactory::bitcoin());
0 ignored issues
show
Unused Code Comprehensibility introduced by
57% 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...
101 81
    }
102
103
    /**
104
     * enable CURL debugging output
105
     *
106
     * @param   bool        $debug
107
     *
108
     * @codeCoverageIgnore
109
     */
110
    public function setCurlDebugging($debug = true) {
111
        $this->client->setCurlDebugging($debug);
112
    }
113
114
    /**
115
     * enable verbose errors
116
     *
117
     * @param   bool        $verboseErrors
118
     *
119
     * @codeCoverageIgnore
120
     */
121
    public function setVerboseErrors($verboseErrors = true) {
122
        $this->client->setVerboseErrors($verboseErrors);
123
    }
124
    
125
    /**
126
     * set cURL default option on Guzzle client
127
     * @param string    $key
128
     * @param bool      $value
129
     *
130
     * @codeCoverageIgnore
131
     */
132
    public function setCurlDefaultOption($key, $value) {
133
        $this->client->setCurlDefaultOption($key, $value);
134
    }
135
136
    /**
137
     * @return  RestClient
138
     */
139 2
    public function getRestClient() {
140 2
        return $this->client;
141
    }
142
143
    /**
144
     * get a single address
145
     * @param  string $address address hash
146
     * @return array           associative array containing the response
147
     */
148 1
    public function address($address) {
149 1
        $response = $this->client->get("address/{$address}");
150 1
        return self::jsonDecode($response->body(), true);
151
    }
152
153
    /**
154
     * get all transactions for an address (paginated)
155
     * @param  string  $address address hash
156
     * @param  integer $page    pagination: page number
157
     * @param  integer $limit   pagination: records per page (max 500)
158
     * @param  string  $sortDir pagination: sort direction (asc|desc)
159
     * @return array            associative array containing the response
160
     */
161 1
    public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
162
        $queryString = [
163 1
            'page' => $page,
164 1
            'limit' => $limit,
165 1
            'sort_dir' => $sortDir
166
        ];
167 1
        $response = $this->client->get("address/{$address}/transactions", $queryString);
168 1
        return self::jsonDecode($response->body(), true);
169
    }
170
171
    /**
172
     * get all unconfirmed transactions for an address (paginated)
173
     * @param  string  $address address hash
174
     * @param  integer $page    pagination: page number
175
     * @param  integer $limit   pagination: records per page (max 500)
176
     * @param  string  $sortDir pagination: sort direction (asc|desc)
177
     * @return array            associative array containing the response
178
     */
179 1
    public function addressUnconfirmedTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
180
        $queryString = [
181 1
            'page' => $page,
182 1
            'limit' => $limit,
183 1
            'sort_dir' => $sortDir
184
        ];
185 1
        $response = $this->client->get("address/{$address}/unconfirmed-transactions", $queryString);
186 1
        return self::jsonDecode($response->body(), true);
187
    }
188
189
    /**
190
     * get all unspent outputs for an address (paginated)
191
     * @param  string  $address address hash
192
     * @param  integer $page    pagination: page number
193
     * @param  integer $limit   pagination: records per page (max 500)
194
     * @param  string  $sortDir pagination: sort direction (asc|desc)
195
     * @return array            associative array containing the response
196
     */
197 1
    public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') {
198
        $queryString = [
199 1
            'page' => $page,
200 1
            'limit' => $limit,
201 1
            'sort_dir' => $sortDir
202
        ];
203 1
        $response = $this->client->get("address/{$address}/unspent-outputs", $queryString);
204 1
        return self::jsonDecode($response->body(), true);
205
    }
206
207
    /**
208
     * get all unspent outputs for a batch of addresses (paginated)
209
     *
210
     * @param  string[] $addresses
211
     * @param  integer  $page    pagination: page number
212
     * @param  integer  $limit   pagination: records per page (max 500)
213
     * @param  string   $sortDir pagination: sort direction (asc|desc)
214
     * @return array associative array containing the response
215
     * @throws \Exception
216
     */
217
    public function batchAddressUnspentOutputs($addresses, $page = 1, $limit = 20, $sortDir = 'asc') {
218
        $queryString = [
219
            'page' => $page,
220
            'limit' => $limit,
221
            'sort_dir' => $sortDir
222
        ];
223
        $response = $this->client->post("address/unspent-outputs", $queryString, ['addresses' => $addresses]);
224
        return self::jsonDecode($response->body(), true);
225
    }
226
227
    /**
228
     * verify ownership of an address
229
     * @param  string  $address     address hash
230
     * @param  string  $signature   a signed message (the address hash) using the private key of the address
231
     * @return array                associative array containing the response
232
     */
233 2
    public function verifyAddress($address, $signature) {
234 2
        $postData = ['signature' => $signature];
235
236 2
        $response = $this->client->post("address/{$address}/verify", null, $postData, RestClient::AUTH_HTTP_SIG);
237
238 2
        return self::jsonDecode($response->body(), true);
239
    }
240
241
    /**
242
     * get all blocks (paginated)
243
     * @param  integer $page    pagination: page number
244
     * @param  integer $limit   pagination: records per page
245
     * @param  string  $sortDir pagination: sort direction (asc|desc)
246
     * @return array            associative array containing the response
247
     */
248 1
    public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') {
249
        $queryString = [
250 1
            'page' => $page,
251 1
            'limit' => $limit,
252 1
            'sort_dir' => $sortDir
253
        ];
254 1
        $response = $this->client->get("all-blocks", $queryString);
255 1
        return self::jsonDecode($response->body(), true);
256
    }
257
258
    /**
259
     * get the latest block
260
     * @return array            associative array containing the response
261
     */
262 1
    public function blockLatest() {
263 1
        $response = $this->client->get("block/latest");
264 1
        return self::jsonDecode($response->body(), true);
265
    }
266
267
    /**
268
     * get an individual block
269
     * @param  string|integer $block    a block hash or a block height
270
     * @return array                    associative array containing the response
271
     */
272 1
    public function block($block) {
273 1
        $response = $this->client->get("block/{$block}");
274 1
        return self::jsonDecode($response->body(), true);
275
    }
276
277
    /**
278
     * get all transaction in a block (paginated)
279
     * @param  string|integer   $block   a block hash or a block height
280
     * @param  integer          $page    pagination: page number
281
     * @param  integer          $limit   pagination: records per page
282
     * @param  string           $sortDir pagination: sort direction (asc|desc)
283
     * @return array                     associative array containing the response
284
     */
285 1
    public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') {
286
        $queryString = [
287 1
            'page' => $page,
288 1
            'limit' => $limit,
289 1
            'sort_dir' => $sortDir
290
        ];
291 1
        $response = $this->client->get("block/{$block}/transactions", $queryString);
292 1
        return self::jsonDecode($response->body(), true);
293
    }
294
295
    /**
296
     * get a single transaction
297
     * @param  string $txhash transaction hash
298
     * @return array          associative array containing the response
299
     */
300 1
    public function transaction($txhash) {
301 1
        $response = $this->client->get("transaction/{$txhash}");
302 1
        return self::jsonDecode($response->body(), true);
303
    }
304
305
    /**
306
     * get a single transaction
307
     * @param  string[] $txhashes list of transaction hashes (up to 20)
308
     * @return array[]            array containing the response
309
     */
310
    public function transactions($txhashes) {
311
        $response = $this->client->get("transactions/" . implode(",", $txhashes));
312
        return self::jsonDecode($response->body(), true);
313
    }
314
    
315
    /**
316
     * get a paginated list of all webhooks associated with the api user
317
     * @param  integer          $page    pagination: page number
318
     * @param  integer          $limit   pagination: records per page
319
     * @return array                     associative array containing the response
320
     */
321 1
    public function allWebhooks($page = 1, $limit = 20) {
322
        $queryString = [
323 1
            'page' => $page,
324 1
            'limit' => $limit
325
        ];
326 1
        $response = $this->client->get("webhooks", $queryString);
327 1
        return self::jsonDecode($response->body(), true);
328
    }
329
330
    /**
331
     * get an existing webhook by it's identifier
332
     * @param string    $identifier     a unique identifier associated with the webhook
333
     * @return array                    associative array containing the response
334
     */
335 1
    public function getWebhook($identifier) {
336 1
        $response = $this->client->get("webhook/".$identifier);
337 1
        return self::jsonDecode($response->body(), true);
338
    }
339
340
    /**
341
     * create a new webhook
342
     * @param  string  $url        the url to receive the webhook events
343
     * @param  string  $identifier a unique identifier to associate with this webhook
344
     * @return array               associative array containing the response
345
     */
346 1
    public function setupWebhook($url, $identifier = null) {
347
        $postData = [
348 1
            'url'        => $url,
349 1
            'identifier' => $identifier
350
        ];
351 1
        $response = $this->client->post("webhook", null, $postData, RestClient::AUTH_HTTP_SIG);
352 1
        return self::jsonDecode($response->body(), true);
353
    }
354
355
    /**
356
     * update an existing webhook
357
     * @param  string  $identifier      the unique identifier of the webhook to update
358
     * @param  string  $newUrl          the new url to receive the webhook events
359
     * @param  string  $newIdentifier   a new unique identifier to associate with this webhook
360
     * @return array                    associative array containing the response
361
     */
362 1
    public function updateWebhook($identifier, $newUrl = null, $newIdentifier = null) {
363
        $putData = [
364 1
            'url'        => $newUrl,
365 1
            'identifier' => $newIdentifier
366
        ];
367 1
        $response = $this->client->put("webhook/{$identifier}", null, $putData, RestClient::AUTH_HTTP_SIG);
368 1
        return self::jsonDecode($response->body(), true);
369
    }
370
371
    /**
372
     * deletes an existing webhook and any event subscriptions associated with it
373
     * @param  string  $identifier      the unique identifier of the webhook to delete
374
     * @return boolean                  true on success
375
     */
376 1
    public function deleteWebhook($identifier) {
377 1
        $response = $this->client->delete("webhook/{$identifier}", null, null, RestClient::AUTH_HTTP_SIG);
378 1
        return self::jsonDecode($response->body(), true);
379
    }
380
381
    /**
382
     * get a paginated list of all the events a webhook is subscribed to
383
     * @param  string  $identifier  the unique identifier of the webhook
384
     * @param  integer $page        pagination: page number
385
     * @param  integer $limit       pagination: records per page
386
     * @return array                associative array containing the response
387
     */
388 1
    public function getWebhookEvents($identifier, $page = 1, $limit = 20) {
389
        $queryString = [
390 1
            'page' => $page,
391 1
            'limit' => $limit
392
        ];
393 1
        $response = $this->client->get("webhook/{$identifier}/events", $queryString);
394 1
        return self::jsonDecode($response->body(), true);
395
    }
396
    
397
    /**
398
     * subscribes a webhook to transaction events of one particular transaction
399
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
400
     * @param  string  $transaction     the transaction hash
401
     * @param  integer $confirmations   the amount of confirmations to send.
402
     * @return array                    associative array containing the response
403
     */
404 1
    public function subscribeTransaction($identifier, $transaction, $confirmations = 6) {
405
        $postData = [
406 1
            'event_type'    => 'transaction',
407 1
            'transaction'   => $transaction,
408 1
            'confirmations' => $confirmations,
409
        ];
410 1
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
411 1
        return self::jsonDecode($response->body(), true);
412
    }
413
414
    /**
415
     * subscribes a webhook to transaction events on a particular address
416
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
417
     * @param  string  $address         the address hash
418
     * @param  integer $confirmations   the amount of confirmations to send.
419
     * @return array                    associative array containing the response
420
     */
421 1
    public function subscribeAddressTransactions($identifier, $address, $confirmations = 6) {
422
        $postData = [
423 1
            'event_type'    => 'address-transactions',
424 1
            'address'       => $address,
425 1
            'confirmations' => $confirmations,
426
        ];
427 1
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
428 1
        return self::jsonDecode($response->body(), true);
429
    }
430
431
    /**
432
     * batch subscribes a webhook to multiple transaction events
433
     *
434
     * @param  string $identifier   the unique identifier of the webhook
435
     * @param  array  $batchData    A 2D array of event data:
436
     *                              [address => $address, confirmations => $confirmations]
437
     *                              where $address is the address to subscibe to
438
     *                              and optionally $confirmations is the amount of confirmations
439
     * @return boolean              true on success
440
     */
441 1
    public function batchSubscribeAddressTransactions($identifier, $batchData) {
442 1
        $postData = [];
443 1
        foreach ($batchData as $record) {
444 1
            $postData[] = [
445 1
                'event_type' => 'address-transactions',
446 1
                'address' => $record['address'],
447 1
                'confirmations' => isset($record['confirmations']) ? $record['confirmations'] : 6,
448
            ];
449
        }
450 1
        $response = $this->client->post("webhook/{$identifier}/events/batch", null, $postData, RestClient::AUTH_HTTP_SIG);
451 1
        return self::jsonDecode($response->body(), true);
452
    }
453
454
    /**
455
     * subscribes a webhook to a new block event
456
     * @param  string  $identifier  the unique identifier of the webhook to be triggered
457
     * @return array                associative array containing the response
458
     */
459 1
    public function subscribeNewBlocks($identifier) {
460
        $postData = [
461 1
            'event_type'    => 'block',
462
        ];
463 1
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
464 1
        return self::jsonDecode($response->body(), true);
465
    }
466
467
    /**
468
     * removes an transaction event subscription from a webhook
469
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
470
     * @param  string  $transaction     the transaction hash of the event subscription
471
     * @return boolean                  true on success
472
     */
473 1
    public function unsubscribeTransaction($identifier, $transaction) {
474 1
        $response = $this->client->delete("webhook/{$identifier}/transaction/{$transaction}", null, null, RestClient::AUTH_HTTP_SIG);
475 1
        return self::jsonDecode($response->body(), true);
476
    }
477
478
    /**
479
     * removes an address transaction event subscription from a webhook
480
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
481
     * @param  string  $address         the address hash of the event subscription
482
     * @return boolean                  true on success
483
     */
484 1
    public function unsubscribeAddressTransactions($identifier, $address) {
485 1
        $response = $this->client->delete("webhook/{$identifier}/address-transactions/{$address}", null, null, RestClient::AUTH_HTTP_SIG);
486 1
        return self::jsonDecode($response->body(), true);
487
    }
488
489
    /**
490
     * removes a block event subscription from a webhook
491
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
492
     * @return boolean                  true on success
493
     */
494 1
    public function unsubscribeNewBlocks($identifier) {
495 1
        $response = $this->client->delete("webhook/{$identifier}/block", null, null, RestClient::AUTH_HTTP_SIG);
496 1
        return self::jsonDecode($response->body(), true);
497
    }
498
499
    /**
500
     * create a new wallet
501
     *   - will generate a new primary seed (with password) and backup seed (without password)
502
     *   - send the primary seed (BIP39 'encrypted') and backup public key to the server
503
     *   - receive the blocktrail co-signing public key from the server
504
     *
505
     * Either takes one argument:
506
     * @param array $options
507
     *
508
     * Or takes three arguments (old, deprecated syntax):
509
     * (@nonPHP-doc) @param      $identifier
510
     * (@nonPHP-doc) @param      $password
511
     * (@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...
512
     *
513
     * @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...
514
     * @throws \Exception
515
     */
516 7
    public function createNewWallet($options) {
517 7
        if (!is_array($options)) {
518 1
            $args = func_get_args();
519
            $options = [
520 1
                "identifier" => $args[0],
521 1
                "password" => $args[1],
522 1
                "key_index" => isset($args[2]) ? $args[2] : null,
523
            ];
524
        }
525
526 7
        if (isset($options['password'])) {
527 1
            if (isset($options['passphrase'])) {
528
                throw new \InvalidArgumentException("Can only provide either passphrase or password");
529
            } else {
530 1
                $options['passphrase'] = $options['password'];
531
            }
532
        }
533
534 7
        if (!isset($options['passphrase'])) {
535 1
            $options['passphrase'] = null;
536
        }
537
538 7
        if (!isset($options['key_index'])) {
539
            $options['key_index'] = 0;
540
        }
541
542 7
        if (!isset($options['wallet_version'])) {
543 3
            $options['wallet_version'] = Wallet::WALLET_VERSION_V3;
544
        }
545
546 7
        switch ($options['wallet_version']) {
547 7
            case Wallet::WALLET_VERSION_V1:
548 1
                return $this->createNewWalletV1($options);
549
550 6
            case Wallet::WALLET_VERSION_V2:
551 2
                return $this->createNewWalletV2($options);
552
553 4
            case Wallet::WALLET_VERSION_V3:
554 4
                return $this->createNewWalletV3($options);
555
556
            default:
557
                throw new \InvalidArgumentException("Invalid wallet version");
558
        }
559
    }
560
561 1
    protected function createNewWalletV1($options) {
562 1
        $walletPath = WalletPath::create($options['key_index']);
563
564 1
        $storePrimaryMnemonic = isset($options['store_primary_mnemonic']) ? $options['store_primary_mnemonic'] : null;
565
566 1
        if (isset($options['primary_mnemonic']) && isset($options['primary_private_key'])) {
567
            throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey");
568
        }
569
570 1
        $primaryMnemonic = null;
571 1
        $primaryPrivateKey = null;
572 1
        if (!isset($options['primary_mnemonic']) && !isset($options['primary_private_key'])) {
573 1
            if (!$options['passphrase']) {
574
                throw new \InvalidArgumentException("Can't generate Primary Mnemonic without a passphrase");
575
            } else {
576
                // create new primary seed
577
                /** @var HierarchicalKey $primaryPrivateKey */
578 1
                list($primaryMnemonic, , $primaryPrivateKey) = $this->newPrimarySeed($options['passphrase']);
579 1
                if ($storePrimaryMnemonic !== false) {
580 1
                    $storePrimaryMnemonic = true;
581
                }
582
            }
583
        } elseif (isset($options['primary_mnemonic'])) {
584
            $primaryMnemonic = $options['primary_mnemonic'];
585
        } elseif (isset($options['primary_private_key'])) {
586
            $primaryPrivateKey = $options['primary_private_key'];
587
        }
588
589 1
        if ($storePrimaryMnemonic && $primaryMnemonic && !$options['passphrase']) {
590
            throw new \InvalidArgumentException("Can't store Primary Mnemonic on server without a passphrase");
591
        }
592
593 1
        if ($primaryPrivateKey) {
594 1
            if (is_string($primaryPrivateKey)) {
595 1
                $primaryPrivateKey = [$primaryPrivateKey, "m"];
596
            }
597
        } else {
598
            $primaryPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($primaryMnemonic, $options['passphrase']));
599
        }
600
601 1
        if (!$storePrimaryMnemonic) {
602
            $primaryMnemonic = false;
603
        }
604
605
        // create primary public key from the created private key
606 1
        $path = $walletPath->keyIndexPath()->publicPath();
607 1
        $primaryPublicKey = BIP32Key::create($primaryPrivateKey, "m")->buildKey($path);
608
609 1
        if (isset($options['backup_mnemonic']) && $options['backup_public_key']) {
610
            throw new \InvalidArgumentException("Can't specify Backup Mnemonic and Backup PublicKey");
611
        }
612
613 1
        $backupMnemonic = null;
614 1
        $backupPublicKey = null;
615 1
        if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) {
616
            /** @var HierarchicalKey $backupPrivateKey */
617 1
            list($backupMnemonic, , ) = $this->newBackupSeed();
618
        } else if (isset($options['backup_mnemonic'])) {
619
            $backupMnemonic = $options['backup_mnemonic'];
620
        } elseif (isset($options['backup_public_key'])) {
621
            $backupPublicKey = $options['backup_public_key'];
622
        }
623
624 1
        if ($backupPublicKey) {
625
            if (is_string($backupPublicKey)) {
626
                $backupPublicKey = [$backupPublicKey, "m"];
627
            }
628
        } else {
629 1
            $backupPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, ""));
630 1
            $backupPublicKey = BIP32Key::create($backupPrivateKey->toPublic(), "M");
631
        }
632
633
        // create a checksum of our private key which we'll later use to verify we used the right password
634 1
        $checksum = $primaryPrivateKey->getPublicKey()->getAddress()->getAddress();
635
636
        // send the public keys to the server to store them
637
        //  and the mnemonic, which is safe because it's useless without the password
638 1
        $data = $this->storeNewWalletV1($options['identifier'], $primaryPublicKey->tuple(), $backupPublicKey->tuple(), $primaryMnemonic, $checksum, $options['key_index']);
639
640
        // received the blocktrail public keys
641
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
642 1
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
643 1
        }, $data['blocktrail_public_keys']);
644
645
        $wallet = new WalletV1(
646
            $this,
647
            $options['identifier'],
648
            $primaryMnemonic,
649
            [$options['key_index'] => $primaryPublicKey],
650
            $backupPublicKey,
651
            $blocktrailPublicKeys,
652
            $options['key_index'],
653
            $this->network,
654
            $this->testnet,
655
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
656
            $checksum
657
        );
658
659
        $wallet->unlock($options);
660
661
        // return wallet and backup mnemonic
662
        return [
663
            $wallet,
664
            [
665
                'primary_mnemonic' => $primaryMnemonic,
666
                'backup_mnemonic' => $backupMnemonic,
667
                'blocktrail_public_keys' => $blocktrailPublicKeys,
668
            ],
669
        ];
670
    }
671
672 5
    public static function randomBits($bits) {
673 5
        return self::randomBytes($bits / 8);
674
    }
675
676 5
    public static function randomBytes($bytes) {
677 5
        return (new Random())->bytes($bytes)->getBinary();
678
    }
679
680 2
    protected function createNewWalletV2($options) {
681 2
        $walletPath = WalletPath::create($options['key_index']);
682
683 2
        if (isset($options['store_primary_mnemonic'])) {
684
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
685
        }
686
687 2
        if (!isset($options['store_data_on_server'])) {
688 2
            if (isset($options['primary_private_key'])) {
689 1
                $options['store_data_on_server'] = false;
690
            } else {
691 1
                $options['store_data_on_server'] = true;
692
            }
693
        }
694
695 2
        $storeDataOnServer = $options['store_data_on_server'];
696
697 2
        $secret = null;
698 2
        $encryptedSecret = null;
699 2
        $primarySeed = null;
700 2
        $encryptedPrimarySeed = null;
701 2
        $recoverySecret = null;
702 2
        $recoveryEncryptedSecret = null;
703 2
        $backupSeed = null;
704
705 2
        if (!isset($options['primary_private_key'])) {
706 1
            $primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : self::randomBits(256);
707
        }
708
709 2
        if ($storeDataOnServer) {
710 1
            if (!isset($options['secret'])) {
711 1
                if (!$options['passphrase']) {
712
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
713
                }
714
715 1
                $secret = bin2hex(self::randomBits(256)); // string because we use it as passphrase
716 1
                $encryptedSecret = CryptoJSAES::encrypt($secret, $options['passphrase']);
717
            } else {
718
                $secret = $options['secret'];
719
            }
720
721 1
            $encryptedPrimarySeed = CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
722 1
            $recoverySecret = bin2hex(self::randomBits(256));
723
724 1
            $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);
725
        }
726
727 2
        if (!isset($options['backup_public_key'])) {
728 1
            $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : self::randomBits(256);
729
        }
730
731 2
        if (isset($options['primary_private_key'])) {
732 1
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
733
        } else {
734 1
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m");
735
        }
736
737
        // create primary public key from the created private key
738 2
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
739
740 2
        if (!isset($options['backup_public_key'])) {
741 1
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M");
742
        }
743
744
        // create a checksum of our private key which we'll later use to verify we used the right password
745 2
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
746
747
        // send the public keys and encrypted data to server
748 2
        $data = $this->storeNewWalletV2(
749 2
            $options['identifier'],
750 2
            $options['primary_public_key']->tuple(),
751 2
            $options['backup_public_key']->tuple(),
0 ignored issues
show
Documentation introduced by
$options['backup_public_key']->tuple() is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
752 2
            $storeDataOnServer ? $encryptedPrimarySeed : false,
753 2
            $storeDataOnServer ? $encryptedSecret : false,
754 2
            $storeDataOnServer ? $recoverySecret : false,
755 2
            $checksum,
756 2
            $options['key_index']
757
        );
758
759
        // received the blocktrail public keys
760
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
761 2
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
762 2
        }, $data['blocktrail_public_keys']);
763
764
        $wallet = new WalletV2(
765
            $this,
766
            $options['identifier'],
767
            $encryptedPrimarySeed,
768
            $encryptedSecret,
769
            [$options['key_index'] => $options['primary_public_key']],
770
            $options['backup_public_key'],
771
            $blocktrailPublicKeys,
772
            $options['key_index'],
773
            $this->network,
774
            $this->testnet,
775
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
776
            $checksum
777
        );
778
779
        $wallet->unlock([
780
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
781
            'primary_private_key' => $options['primary_private_key'],
782
            'primary_seed' => $primarySeed,
783
            'secret' => $secret,
784
        ]);
785
786
        // return wallet and mnemonics for backup sheet
787
        return [
788
            $wallet,
789
            [
790
                'encrypted_primary_seed' => $encryptedPrimarySeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedPrimarySeed))) : null,
791
                'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer($backupSeed)) : null,
792
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($recoveryEncryptedSecret))) : null,
793
                'encrypted_secret' => $encryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedSecret))) : null,
794
                'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
795
                    return [$keyIndex, $pubKey->tuple()];
796
                }, $blocktrailPublicKeys),
797
            ],
798
        ];
799
    }
800
801 4
    protected function createNewWalletV3($options) {
802 4
        $walletPath = WalletPath::create($options['key_index']);
803
804 4
        if (isset($options['store_primary_mnemonic'])) {
805
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
806
        }
807
808 4
        if (!isset($options['store_data_on_server'])) {
809 4
            if (isset($options['primary_private_key'])) {
810
                $options['store_data_on_server'] = false;
811
            } else {
812 4
                $options['store_data_on_server'] = true;
813
            }
814
        }
815
816 4
        $storeDataOnServer = $options['store_data_on_server'];
817
818 4
        $secret = null;
819 4
        $encryptedSecret = null;
820 4
        $primarySeed = null;
821 4
        $encryptedPrimarySeed = null;
822 4
        $recoverySecret = null;
823 4
        $recoveryEncryptedSecret = null;
824 4
        $backupSeed = null;
825
826 4
        if (!isset($options['primary_private_key'])) {
827 4
            if (isset($options['primary_seed'])) {
828
                if (!$options['primary_seed'] instanceof BufferInterface) {
829
                    throw new \InvalidArgumentException('Primary Seed should be passed as a Buffer');
830
                }
831
                $primarySeed = $options['primary_seed'];
832
            } else {
833 4
                $primarySeed = new Buffer(self::randomBits(256));
834
            }
835
        }
836
837 4
        if ($storeDataOnServer) {
838 4
            if (!isset($options['secret'])) {
839 4
                if (!$options['passphrase']) {
840
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
841
                }
842
843 4
                $secret = new Buffer(self::randomBits(256));
844 4
                $encryptedSecret = Encryption::encrypt($secret, new Buffer($options['passphrase']), KeyDerivation::DEFAULT_ITERATIONS);
845
            } else {
846
                if (!$options['secret'] instanceof Buffer) {
847
                    throw new \RuntimeException('Secret must be provided as a Buffer');
848
                }
849
850
                $secret = $options['secret'];
851
            }
852
853 4
            $encryptedPrimarySeed = Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS);
854 4
            $recoverySecret = new Buffer(self::randomBits(256));
855
856 4
            $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS);
857
        }
858
859 4
        if (!isset($options['backup_public_key'])) {
860 4
            if (isset($options['backup_seed'])) {
861
                if (!$options['backup_seed'] instanceof Buffer) {
862
                    throw new \RuntimeException('Backup seed must be an instance of Buffer');
863
                }
864
                $backupSeed = $options['backup_seed'];
865
            } else {
866 4
                $backupSeed = new Buffer(self::randomBits(256));
867
            }
868
        }
869
870 4
        if (isset($options['primary_private_key'])) {
871
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
872
        } else {
873 4
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m");
874
        }
875
876
        // create primary public key from the created private key
877 4
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
878
879 4
        if (!isset($options['backup_public_key'])) {
880 4
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($backupSeed), "m")->buildKey("M");
881
        }
882
883
        // create a checksum of our private key which we'll later use to verify we used the right password
884 4
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
885
886
        // send the public keys and encrypted data to server
887 4
        $data = $this->storeNewWalletV3(
888 4
            $options['identifier'],
889 4
            $options['primary_public_key']->tuple(),
890 4
            $options['backup_public_key']->tuple(),
0 ignored issues
show
Documentation introduced by
$options['backup_public_key']->tuple() is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
891 4
            $storeDataOnServer ? base64_encode($encryptedPrimarySeed->getBinary()) : false,
892 4
            $storeDataOnServer ? base64_encode($encryptedSecret->getBinary()) : false,
893 4
            $storeDataOnServer ? $recoverySecret->getHex() : false,
894 4
            $checksum,
895 4
            $options['key_index']
896
        );
897
898
        // received the blocktrail public keys
899
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
900 4
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
901 4
        }, $data['blocktrail_public_keys']);
902
903
        $wallet = new WalletV3(
904
            $this,
905
            $options['identifier'],
906
            $encryptedPrimarySeed,
0 ignored issues
show
Bug introduced by
It seems like $encryptedPrimarySeed defined by null on line 821 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...
907
            $encryptedSecret,
0 ignored issues
show
Bug introduced by
It seems like $encryptedSecret defined by null on line 819 can be null; however, Blocktrail\SDK\WalletV3::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
908
            [$options['key_index'] => $options['primary_public_key']],
909
            $options['backup_public_key'],
910
            $blocktrailPublicKeys,
911
            $options['key_index'],
912
            $this->network,
913
            $this->testnet,
914
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
915
            $checksum
916
        );
917
918
        $wallet->unlock([
919
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
920
            'primary_private_key' => $options['primary_private_key'],
921
            'primary_seed' => $primarySeed,
922
            'secret' => $secret,
923
        ]);
924
925
        // return wallet and mnemonics for backup sheet
926
        return [
927
            $wallet,
928
            [
929
                'encrypted_primary_seed'    => $encryptedPrimarySeed ? EncryptionMnemonic::encode($encryptedPrimarySeed) : null,
930
                'backup_seed'               => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic($backupSeed) : null,
931
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? EncryptionMnemonic::encode($recoveryEncryptedSecret) : null,
932
                'encrypted_secret'          => $encryptedSecret ? EncryptionMnemonic::encode($encryptedSecret) : null,
933
                'blocktrail_public_keys'    => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
934
                    return [$keyIndex, $pubKey->tuple()];
935
                }, $blocktrailPublicKeys),
936
            ]
937
        ];
938
    }
939
940
    /**
941
     * @param array $bip32Key
942
     * @throws BlocktrailSDKException
943
     */
944 10
    private function verifyPublicBIP32Key(array $bip32Key) {
945 10
        $hk = HierarchicalKeyFactory::fromExtended($bip32Key[0]);
946 10
        if ($hk->isPrivate()) {
947
            throw new BlocktrailSDKException('Private key was included in request, abort');
948
        }
949
950 10
        if (substr($bip32Key[1], 0, 1) === "m") {
951
            throw new BlocktrailSDKException("Private path was included in the request, abort");
952
        }
953 10
    }
954
955
    /**
956
     * @param array $walletData
957
     * @throws BlocktrailSDKException
958
     */
959 10
    private function verifyPublicOnly(array $walletData) {
960 10
        $this->verifyPublicBIP32Key($walletData['primary_public_key']);
961 10
        $this->verifyPublicBIP32Key($walletData['backup_public_key']);
962 10
    }
963
964
    /**
965
     * create wallet using the API
966
     *
967
     * @param string    $identifier             the wallet identifier to create
968
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
969
     * @param string    $backupPublicKey        plain public key
970
     * @param string    $primaryMnemonic        mnemonic to store
971
     * @param string    $checksum               checksum to store
972
     * @param int       $keyIndex               account that we expect to use
973
     * @return mixed
974
     */
975 1
    public function storeNewWalletV1($identifier, $primaryPublicKey, $backupPublicKey, $primaryMnemonic, $checksum, $keyIndex) {
976
        $data = [
977 1
            'identifier' => $identifier,
978 1
            'primary_public_key' => $primaryPublicKey,
979 1
            'backup_public_key' => $backupPublicKey,
980 1
            'primary_mnemonic' => $primaryMnemonic,
981 1
            'checksum' => $checksum,
982 1
            'key_index' => $keyIndex
983
        ];
984 1
        $this->verifyPublicOnly($data);
985 1
        $response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
986 1
        return self::jsonDecode($response->body(), true);
987
    }
988
989
    /**
990
     * create wallet using the API
991
     *
992
     * @param string $identifier       the wallet identifier to create
993
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
994
     * @param string $backupPublicKey  plain public key
995
     * @param        $encryptedPrimarySeed
996
     * @param        $encryptedSecret
997
     * @param        $recoverySecret
998
     * @param string $checksum         checksum to store
999
     * @param int    $keyIndex         account that we expect to use
1000
     * @return mixed
1001
     * @throws \Exception
1002
     */
1003 5
    public function storeNewWalletV2($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex) {
1004
        $data = [
1005 5
            'identifier' => $identifier,
1006
            'wallet_version' => Wallet::WALLET_VERSION_V2,
1007 5
            'primary_public_key' => $primaryPublicKey,
1008 5
            'backup_public_key' => $backupPublicKey,
1009 5
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1010 5
            'encrypted_secret' => $encryptedSecret,
1011 5
            'recovery_secret' => $recoverySecret,
1012 5
            'checksum' => $checksum,
1013 5
            'key_index' => $keyIndex
1014
        ];
1015 5
        $this->verifyPublicOnly($data);
1016 5
        $response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1017 5
        return self::jsonDecode($response->body(), true);
1018
    }
1019
1020
    /**
1021
     * create wallet using the API
1022
     *
1023
     * @param string $identifier       the wallet identifier to create
1024
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1025
     * @param string $backupPublicKey  plain public key
1026
     * @param        $encryptedPrimarySeed
1027
     * @param        $encryptedSecret
1028
     * @param        $recoverySecret
1029
     * @param string $checksum         checksum to store
1030
     * @param int    $keyIndex         account that we expect to use
1031
     * @return mixed
1032
     * @throws \Exception
1033
     */
1034 4
    public function storeNewWalletV3($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex) {
1035
1036
        $data = [
1037 4
            'identifier' => $identifier,
1038
            'wallet_version' => Wallet::WALLET_VERSION_V3,
1039 4
            'primary_public_key' => $primaryPublicKey,
1040 4
            'backup_public_key' => $backupPublicKey,
1041 4
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1042 4
            'encrypted_secret' => $encryptedSecret,
1043 4
            'recovery_secret' => $recoverySecret,
1044 4
            'checksum' => $checksum,
1045 4
            'key_index' => $keyIndex
1046
        ];
1047
1048 4
        $this->verifyPublicOnly($data);
1049 4
        $response = $this->client->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1050 4
        return self::jsonDecode($response->body(), true);
1051
    }
1052
1053
    /**
1054
     * upgrade wallet to use a new account number
1055
     *  the account number specifies which blocktrail cosigning key is used
1056
     *
1057
     * @param string    $identifier             the wallet identifier to be upgraded
1058
     * @param int       $keyIndex               the new account to use
1059
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1060
     * @return mixed
1061
     */
1062
    public function upgradeKeyIndex($identifier, $keyIndex, $primaryPublicKey) {
1063
        $data = [
1064
            'key_index' => $keyIndex,
1065
            'primary_public_key' => $primaryPublicKey
1066
        ];
1067
1068
        $response = $this->client->post("wallet/{$identifier}/upgrade", null, $data, RestClient::AUTH_HTTP_SIG);
1069
        return self::jsonDecode($response->body(), true);
1070
    }
1071
1072
    /**
1073
     * initialize a previously created wallet
1074
     *
1075
     * Takes an options object, or accepts identifier/password for backwards compatiblity.
1076
     *
1077
     * Some of the options:
1078
     *  - "readonly/readOnly/read-only" can be to a boolean value,
1079
     *    so the wallet is loaded in read-only mode (no private key)
1080
     *  - "check_backup_key" can be set to your own backup key:
1081
     *    Format: ["M', "xpub..."]
1082
     *    Setting this will allow the SDK to check the server hasn't
1083
     *    a different key (one it happens to control)
1084
1085
     * Either takes one argument:
1086
     * @param array $options
1087
     *
1088
     * Or takes two arguments (old, deprecated syntax):
1089
     * (@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...
1090
     * (@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...
1091
     *
1092
     * @return WalletInterface
1093
     * @throws \Exception
1094
     */
1095 16
    public function initWallet($options) {
1096 16
        if (!is_array($options)) {
1097 1
            $args = func_get_args();
1098
            $options = [
1099 1
                "identifier" => $args[0],
1100 1
                "password" => $args[1],
1101
            ];
1102
        }
1103
1104 16
        $identifier = $options['identifier'];
1105 16
        $readonly = isset($options['readonly']) ? $options['readonly'] :
1106 16
                    (isset($options['readOnly']) ? $options['readOnly'] :
1107 16
                        (isset($options['read-only']) ? $options['read-only'] :
1108 16
                            false));
1109
1110
        // get the wallet data from the server
1111 16
        $data = $this->getWallet($identifier);
1112 9
        if (!$data) {
1113
            throw new \Exception("Failed to get wallet");
1114
        }
1115
1116 9
        if (array_key_exists('check_backup_key', $options)) {
1117 1
            if (!is_string($options['check_backup_key'])) {
1118 1
                throw new \RuntimeException("check_backup_key should be a string (the xpub)");
1119
            }
1120 1
            if ($options['check_backup_key'] !== $data['backup_public_key'][0]) {
1121 1
                throw new \RuntimeException("Backup key returned from server didn't match our own");
1122
            }
1123
        }
1124
1125 9
        switch ($data['wallet_version']) {
1126 9
            case Wallet::WALLET_VERSION_V1:
1127 9
                $wallet = new WalletV1(
1128 9
                    $this,
1129 9
                    $identifier,
1130 9
                    isset($options['primary_mnemonic']) ? $options['primary_mnemonic'] : $data['primary_mnemonic'],
1131 9
                    $data['primary_public_keys'],
1132 9
                    $data['backup_public_key'],
1133 9
                    $data['blocktrail_public_keys'],
1134 9
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1135 9
                    $this->network,
1136 9
                    $this->testnet,
1137 9
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1138 9
                    $data['checksum']
1139
                );
1140
                break;
1141
            case Wallet::WALLET_VERSION_V2:
1142
                $wallet = new WalletV2(
1143
                    $this,
1144
                    $identifier,
1145
                    isset($options['encrypted_primary_seed']) ? $options['encrypted_primary_seed'] : $data['encrypted_primary_seed'],
1146
                    isset($options['encrypted_secret']) ? $options['encrypted_secret'] : $data['encrypted_secret'],
1147
                    $data['primary_public_keys'],
1148
                    $data['backup_public_key'],
1149
                    $data['blocktrail_public_keys'],
1150
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1151
                    $this->network,
1152
                    $this->testnet,
1153
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1154
                    $data['checksum']
1155
                );
1156
                break;
1157
            case Wallet::WALLET_VERSION_V3:
1158
                if (isset($options['encrypted_primary_seed'])) {
1159
                    if (!$options['encrypted_primary_seed'] instanceof Buffer) {
1160
                        throw new \InvalidArgumentException('Encrypted PrimarySeed must be provided as a Buffer');
1161
                    }
1162
                    $encryptedPrimarySeed = $data['encrypted_primary_seed'];
1163
                } else {
1164
                    $encryptedPrimarySeed = new Buffer(base64_decode($data['encrypted_primary_seed']));
1165
                }
1166
1167
                if (isset($options['encrypted_secret'])) {
1168
                    if (!$options['encrypted_secret'] instanceof Buffer) {
1169
                        throw new \InvalidArgumentException('Encrypted secret must be provided as a Buffer');
1170
                    }
1171
1172
                    $encryptedSecret = $data['encrypted_secret'];
1173
                } else {
1174
                    $encryptedSecret = new Buffer(base64_decode($data['encrypted_secret']));
1175
                }
1176
1177
                $wallet = new WalletV3(
1178
                    $this,
1179
                    $identifier,
1180
                    $encryptedPrimarySeed,
1181
                    $encryptedSecret,
1182
                    $data['primary_public_keys'],
1183
                    $data['backup_public_key'],
1184
                    $data['blocktrail_public_keys'],
1185
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1186
                    $this->network,
1187
                    $this->testnet,
1188
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1189
                    $data['checksum']
1190
                );
1191
                break;
1192
            default:
1193
                throw new \InvalidArgumentException("Invalid wallet version");
1194
        }
1195
1196
        if (!$readonly) {
1197
            $wallet->unlock($options);
1198
        }
1199
1200
        return $wallet;
1201
    }
1202
1203
    /**
1204
     * get the wallet data from the server
1205
     *
1206
     * @param string    $identifier             the identifier of the wallet
1207
     * @return mixed
1208
     */
1209 16
    public function getWallet($identifier) {
1210 16
        $response = $this->client->get("wallet/{$identifier}", null, RestClient::AUTH_HTTP_SIG);
1211 9
        return self::jsonDecode($response->body(), true);
1212
    }
1213
1214
    /**
1215
     * update the wallet data on the server
1216
     *
1217
     * @param string    $identifier
1218
     * @param $data
1219
     * @return mixed
1220
     */
1221
    public function updateWallet($identifier, $data) {
1222
        $response = $this->client->post("wallet/{$identifier}", null, $data, RestClient::AUTH_HTTP_SIG);
1223
        return self::jsonDecode($response->body(), true);
1224
    }
1225
1226
    /**
1227
     * delete a wallet from the server
1228
     *  the checksum address and a signature to verify you ownership of the key of that checksum address
1229
     *  is required to be able to delete a wallet
1230
     *
1231
     * @param string    $identifier             the identifier of the wallet
1232
     * @param string    $checksumAddress        the address for your master private key (and the checksum used when creating the wallet)
1233
     * @param string    $signature              a signature of the checksum address as message signed by the private key matching that address
1234
     * @param bool      $force                  ignore warnings (such as a non-zero balance)
1235
     * @return mixed
1236
     */
1237
    public function deleteWallet($identifier, $checksumAddress, $signature, $force = false) {
1238
        $response = $this->client->delete("wallet/{$identifier}", ['force' => $force], [
1239
            'checksum' => $checksumAddress,
1240
            'signature' => $signature
1241
        ], RestClient::AUTH_HTTP_SIG, 360);
1242
        return self::jsonDecode($response->body(), true);
1243
    }
1244
1245
    /**
1246
     * create new backup key;
1247
     *  1) a BIP39 mnemonic
1248
     *  2) a seed from that mnemonic with a blank password
1249
     *  3) a private key from that seed
1250
     *
1251
     * @return array [mnemonic, seed, key]
1252
     */
1253 1
    protected function newBackupSeed() {
1254 1
        list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed("");
1255
1256 1
        return [$backupMnemonic, $backupSeed, $backupPrivateKey];
1257
    }
1258
1259
    /**
1260
     * create new primary key;
1261
     *  1) a BIP39 mnemonic
1262
     *  2) a seed from that mnemonic with the password
1263
     *  3) a private key from that seed
1264
     *
1265
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1266
     * @return array [mnemonic, seed, key]
1267
     * @TODO: require a strong password?
1268
     */
1269 1
    protected function newPrimarySeed($passphrase) {
1270 1
        list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase);
1271
1272 1
        return [$primaryMnemonic, $primarySeed, $primaryPrivateKey];
1273
    }
1274
1275
    /**
1276
     * create a new key;
1277
     *  1) a BIP39 mnemonic
1278
     *  2) a seed from that mnemonic with the password
1279
     *  3) a private key from that seed
1280
     *
1281
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1282
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1283
     * @return array
1284
     */
1285 1
    protected function generateNewSeed($passphrase = "", $forceEntropy = null) {
1286
        // generate master seed, retry if the generated private key isn't valid (FALSE is returned)
1287
        do {
1288 1
            $mnemonic = $this->generateNewMnemonic($forceEntropy);
1289
1290 1
            $seed = (new Bip39SeedGenerator)->getSeed($mnemonic, $passphrase);
1291
1292 1
            $key = null;
1293
            try {
1294 1
                $key = HierarchicalKeyFactory::fromEntropy($seed);
1295
            } catch (\Exception $e) {
1296
                // try again
1297
            }
1298 1
        } while (!$key);
1299
1300 1
        return [$mnemonic, $seed, $key];
1301
    }
1302
1303
    /**
1304
     * generate a new mnemonic from some random entropy (512 bit)
1305
     *
1306
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1307
     * @return string
1308
     * @throws \Exception
1309
     */
1310 1
    protected function generateNewMnemonic($forceEntropy = null) {
1311 1
        if ($forceEntropy === null) {
1312 1
            $random = new Random();
1313 1
            $entropy = $random->bytes(512 / 8);
1314
        } else {
1315
            $entropy = $forceEntropy;
1316
        }
1317
1318 1
        return MnemonicFactory::bip39()->entropyToMnemonic($entropy);
0 ignored issues
show
Bug introduced by
It seems like $entropy defined by $forceEntropy on line 1315 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...
1319
    }
1320
1321
    /**
1322
     * get the balance for the wallet
1323
     *
1324
     * @param string    $identifier             the identifier of the wallet
1325
     * @return array
1326
     */
1327
    public function getWalletBalance($identifier) {
1328
        $response = $this->client->get("wallet/{$identifier}/balance", null, RestClient::AUTH_HTTP_SIG);
1329
        return self::jsonDecode($response->body(), true);
1330
    }
1331
1332
    /**
1333
     * do HD wallet discovery for the wallet
1334
     *
1335
     * this can be REALLY slow, so we've set the timeout to 120s ...
1336
     *
1337
     * @param string    $identifier             the identifier of the wallet
1338
     * @param int       $gap                    the gap setting to use for discovery
1339
     * @return mixed
1340
     */
1341
    public function doWalletDiscovery($identifier, $gap = 200) {
1342
        $response = $this->client->get("wallet/{$identifier}/discovery", ['gap' => $gap], RestClient::AUTH_HTTP_SIG, 360.0);
1343
        return self::jsonDecode($response->body(), true);
1344
    }
1345
1346
    /**
1347
     * get a new derivation number for specified parent path
1348
     *  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
1349
     *
1350
     * returns the path
1351
     *
1352
     * @param string    $identifier             the identifier of the wallet
1353
     * @param string    $path                   the parent path for which to get a new derivation
1354
     * @return string
1355
     */
1356
    public function getNewDerivation($identifier, $path) {
1357
        $result = $this->_getNewDerivation($identifier, $path);
1358
        return $result['path'];
1359
    }
1360
1361
    /**
1362
     * get a new derivation number for specified parent path
1363
     *  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
1364
     *
1365
     * @param string    $identifier             the identifier of the wallet
1366
     * @param string    $path                   the parent path for which to get a new derivation
1367
     * @return mixed
1368
     */
1369
    public function _getNewDerivation($identifier, $path) {
1370
        $response = $this->client->post("wallet/{$identifier}/path", null, ['path' => $path], RestClient::AUTH_HTTP_SIG);
1371
        return self::jsonDecode($response->body(), true);
1372
    }
1373
1374
    /**
1375
     * get the path (and redeemScript) to specified address
1376
     *
1377
     * @param string $identifier
1378
     * @param string $address
1379
     * @return array
1380
     * @throws \Exception
1381
     */
1382
    public function getPathForAddress($identifier, $address) {
1383
        $response = $this->client->post("wallet/{$identifier}/path_for_address", null, ['address' => $address], RestClient::AUTH_HTTP_SIG);
1384
        return self::jsonDecode($response->body(), true)['path'];
1385
    }
1386
1387
    /**
1388
     * send the transaction using the API
1389
     *
1390
     * @param string         $identifier             the identifier of the wallet
1391
     * @param string|array   $rawTransaction         raw hex of the transaction (should be partially signed)
1392
     * @param array          $paths                  list of the paths that were used for the UTXO
1393
     * @param bool           $checkFee               let the server verify the fee after signing
1394
     * @return string                                the complete raw transaction
1395
     * @throws \Exception
1396
     */
1397
    public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee = false) {
1398
        $data = [
1399
            'paths' => $paths
1400
        ];
1401
1402
        if (is_array($rawTransaction)) {
1403
            if (array_key_exists('base_transaction', $rawTransaction)
1404
            && array_key_exists('signed_transaction', $rawTransaction)) {
1405
                $data['base_transaction'] = $rawTransaction['base_transaction'];
1406
                $data['signed_transaction'] = $rawTransaction['signed_transaction'];
1407
            } else {
1408
                throw new \RuntimeException("Invalid value for transaction. For segwit transactions, pass ['base_transaction' => '...', 'signed_transaction' => '...']");
1409
            }
1410
        } else {
1411
            $data['raw_transaction'] = $rawTransaction;
1412
        }
1413
1414
        // dynamic TTL for when we're signing really big transactions
1415
        $ttl = max(5.0, count($paths) * 0.25) + 4.0;
1416
1417
        $response = $this->client->post("wallet/{$identifier}/send", ['check_fee' => (int)!!$checkFee], $data, RestClient::AUTH_HTTP_SIG, $ttl);
1418
        $signed = self::jsonDecode($response->body(), true);
1419
1420
        if (!$signed['complete'] || $signed['complete'] == 'false') {
1421
            throw new \Exception("Failed to completely sign transaction");
1422
        }
1423
1424
        // create TX hash from the raw signed hex
1425
        return TransactionFactory::fromHex($signed['hex'])->getTxId()->getHex();
1426
    }
1427
1428
    /**
1429
     * use the API to get the best inputs to use based on the outputs
1430
     *
1431
     * the return array has the following format:
1432
     * [
1433
     *  "utxos" => [
1434
     *      [
1435
     *          "hash" => "<txHash>",
1436
     *          "idx" => "<index of the output of that <txHash>",
1437
     *          "scriptpubkey_hex" => "<scriptPubKey-hex>",
1438
     *          "value" => 32746327,
1439
     *          "address" => "1address",
1440
     *          "path" => "m/44'/1'/0'/0/13",
1441
     *          "redeem_script" => "<redeemScript-hex>",
1442
     *      ],
1443
     *  ],
1444
     *  "fee"   => 10000,
1445
     *  "change"=> 1010109201,
1446
     * ]
1447
     *
1448
     * @param string   $identifier              the identifier of the wallet
1449
     * @param array    $outputs                 the outputs you want to create - array[address => satoshi-value]
1450
     * @param bool     $lockUTXO                when TRUE the UTXOs selected will be locked for a few seconds
1451
     *                                          so you have some time to spend them without race-conditions
1452
     * @param bool     $allowZeroConf
1453
     * @param string   $feeStrategy
1454
     * @param null|int $forceFee
1455
     * @return array
1456
     * @throws \Exception
1457
     */
1458
    public function coinSelection($identifier, $outputs, $lockUTXO = false, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1459
        $args = [
1460
            'lock' => (int)!!$lockUTXO,
1461
            'zeroconf' => (int)!!$allowZeroConf,
1462
            'fee_strategy' => $feeStrategy,
1463
        ];
1464
1465
        if ($forceFee !== null) {
1466
            $args['forcefee'] = (int)$forceFee;
1467
        }
1468
1469
        $response = $this->client->post(
1470
            "wallet/{$identifier}/coin-selection",
1471
            $args,
1472
            $outputs,
1473
            RestClient::AUTH_HTTP_SIG
1474
        );
1475
1476
        return self::jsonDecode($response->body(), true);
1477
    }
1478
1479
    /**
1480
     *
1481
     * @param string   $identifier the identifier of the wallet
1482
     * @param bool     $allowZeroConf
1483
     * @param string   $feeStrategy
1484
     * @param null|int $forceFee
1485
     * @param int      $outputCnt
1486
     * @return array
1487
     * @throws \Exception
1488
     */
1489
    public function walletMaxSpendable($identifier, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
1490
        $args = [
1491
            'zeroconf' => (int)!!$allowZeroConf,
1492
            'fee_strategy' => $feeStrategy,
1493
            'outputs' => $outputCnt,
1494
        ];
1495
1496
        if ($forceFee !== null) {
1497
            $args['forcefee'] = (int)$forceFee;
1498
        }
1499
1500
        $response = $this->client->get(
1501
            "wallet/{$identifier}/max-spendable",
1502
            $args,
1503
            RestClient::AUTH_HTTP_SIG
1504
        );
1505
1506
        return self::jsonDecode($response->body(), true);
1507
    }
1508
1509
    /**
1510
     * @return array        ['optimal_fee' => 10000, 'low_priority_fee' => 5000]
1511
     */
1512
    public function feePerKB() {
1513
        $response = $this->client->get("fee-per-kb");
1514
        return self::jsonDecode($response->body(), true);
1515
    }
1516
1517
    /**
1518
     * get the current price index
1519
     *
1520
     * @return array        eg; ['USD' => 287.30]
1521
     */
1522 1
    public function price() {
1523 1
        $response = $this->client->get("price");
1524 1
        return self::jsonDecode($response->body(), true);
1525
    }
1526
1527
    /**
1528
     * setup webhook for wallet
1529
     *
1530
     * @param string    $identifier         the wallet identifier for which to create the webhook
1531
     * @param string    $webhookIdentifier  the webhook identifier to use
1532
     * @param string    $url                the url to receive the webhook events
1533
     * @return array
1534
     */
1535
    public function setupWalletWebhook($identifier, $webhookIdentifier, $url) {
1536
        $response = $this->client->post("wallet/{$identifier}/webhook", null, ['url' => $url, 'identifier' => $webhookIdentifier], RestClient::AUTH_HTTP_SIG);
1537
        return self::jsonDecode($response->body(), true);
1538
    }
1539
1540
    /**
1541
     * delete webhook for wallet
1542
     *
1543
     * @param string    $identifier         the wallet identifier for which to delete the webhook
1544
     * @param string    $webhookIdentifier  the webhook identifier to delete
1545
     * @return array
1546
     */
1547
    public function deleteWalletWebhook($identifier, $webhookIdentifier) {
1548
        $response = $this->client->delete("wallet/{$identifier}/webhook/{$webhookIdentifier}", null, null, RestClient::AUTH_HTTP_SIG);
1549
        return self::jsonDecode($response->body(), true);
1550
    }
1551
1552
    /**
1553
     * lock a specific unspent output
1554
     *
1555
     * @param     $identifier
1556
     * @param     $txHash
1557
     * @param     $txIdx
1558
     * @param int $ttl
1559
     * @return bool
1560
     */
1561
    public function lockWalletUTXO($identifier, $txHash, $txIdx, $ttl = 3) {
1562
        $response = $this->client->post("wallet/{$identifier}/lock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx, 'ttl' => $ttl], RestClient::AUTH_HTTP_SIG);
1563
        return self::jsonDecode($response->body(), true)['locked'];
1564
    }
1565
1566
    /**
1567
     * unlock a specific unspent output
1568
     *
1569
     * @param     $identifier
1570
     * @param     $txHash
1571
     * @param     $txIdx
1572
     * @return bool
1573
     */
1574
    public function unlockWalletUTXO($identifier, $txHash, $txIdx) {
1575
        $response = $this->client->post("wallet/{$identifier}/unlock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx], RestClient::AUTH_HTTP_SIG);
1576
        return self::jsonDecode($response->body(), true)['unlocked'];
1577
    }
1578
1579
    /**
1580
     * get all transactions for wallet (paginated)
1581
     *
1582
     * @param  string  $identifier  the wallet identifier for which to get transactions
1583
     * @param  integer $page        pagination: page number
1584
     * @param  integer $limit       pagination: records per page (max 500)
1585
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1586
     * @return array                associative array containing the response
1587
     */
1588
    public function walletTransactions($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1589
        $queryString = [
1590
            'page' => $page,
1591
            'limit' => $limit,
1592
            'sort_dir' => $sortDir
1593
        ];
1594
        $response = $this->client->get("wallet/{$identifier}/transactions", $queryString, RestClient::AUTH_HTTP_SIG);
1595
        return self::jsonDecode($response->body(), true);
1596
    }
1597
1598
    /**
1599
     * get all addresses for wallet (paginated)
1600
     *
1601
     * @param  string  $identifier  the wallet identifier for which to get addresses
1602
     * @param  integer $page        pagination: page number
1603
     * @param  integer $limit       pagination: records per page (max 500)
1604
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1605
     * @return array                associative array containing the response
1606
     */
1607
    public function walletAddresses($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1608
        $queryString = [
1609
            'page' => $page,
1610
            'limit' => $limit,
1611
            'sort_dir' => $sortDir
1612
        ];
1613
        $response = $this->client->get("wallet/{$identifier}/addresses", $queryString, RestClient::AUTH_HTTP_SIG);
1614
        return self::jsonDecode($response->body(), true);
1615
    }
1616
1617
    /**
1618
     * get all UTXOs for wallet (paginated)
1619
     *
1620
     * @param  string  $identifier  the wallet identifier for which to get addresses
1621
     * @param  integer $page        pagination: page number
1622
     * @param  integer $limit       pagination: records per page (max 500)
1623
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1624
     * @param  boolean $zeroconf    include zero confirmation transactions
1625
     * @return array                associative array containing the response
1626
     */
1627
    public function walletUTXOs($identifier, $page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1628
        $queryString = [
1629
            'page' => $page,
1630
            'limit' => $limit,
1631
            'sort_dir' => $sortDir,
1632
            'zeroconf' => (int)!!$zeroconf,
1633
        ];
1634
        $response = $this->client->get("wallet/{$identifier}/utxos", $queryString, RestClient::AUTH_HTTP_SIG);
1635
        return self::jsonDecode($response->body(), true);
1636
    }
1637
1638
    /**
1639
     * get a paginated list of all wallets associated with the api user
1640
     *
1641
     * @param  integer          $page    pagination: page number
1642
     * @param  integer          $limit   pagination: records per page
1643
     * @return array                     associative array containing the response
1644
     */
1645
    public function allWallets($page = 1, $limit = 20) {
1646
        $queryString = [
1647
            'page' => $page,
1648
            'limit' => $limit
1649
        ];
1650
        $response = $this->client->get("wallets", $queryString, RestClient::AUTH_HTTP_SIG);
1651
        return self::jsonDecode($response->body(), true);
1652
    }
1653
1654
    /**
1655
     * send raw transaction
1656
     *
1657
     * @param     $txHex
1658
     * @return bool
1659
     */
1660
    public function sendRawTransaction($txHex) {
1661
        $response = $this->client->post("send-raw-tx", null, ['hex' => $txHex], RestClient::AUTH_HTTP_SIG);
1662
        return self::jsonDecode($response->body(), true);
1663
    }
1664
1665
    /**
1666
     * testnet only ;-)
1667
     *
1668
     * @param     $address
1669
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1670
     * @return mixed
1671
     * @throws \Exception
1672
     */
1673
    public function faucetWithdrawal($address, $amount = 10000) {
1674
        $response = $this->client->post("faucet/withdrawl", null, [
1675
            'address' => $address,
1676
            'amount' => $amount,
1677
        ], RestClient::AUTH_HTTP_SIG);
1678
        return self::jsonDecode($response->body(), true);
1679
    }
1680
1681
    /**
1682
     * Exists for BC. Remove at major bump.
1683
     *
1684
     * @see faucetWithdrawal
1685
     * @deprecated
1686
     * @param     $address
1687
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1688
     * @return mixed
1689
     * @throws \Exception
1690
     */
1691
    public function faucetWithdrawl($address, $amount = 10000) {
1692
        return $this->faucetWithdrawal($address, $amount);
1693
    }
1694
1695
    /**
1696
     * verify a message signed bitcoin-core style
1697
     *
1698
     * @param  string           $message
1699
     * @param  string           $address
1700
     * @param  string           $signature
1701
     * @return boolean
1702
     */
1703 1
    public function verifyMessage($message, $address, $signature) {
1704
        // we could also use the API instead of the using BitcoinLib to verify
1705
        // $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...
1706
1707 1
        $adapter = Bitcoin::getEcAdapter();
1708 1
        $addr = AddressFactory::fromString($address);
1709 1
        if (!$addr instanceof PayToPubKeyHashAddress) {
1710
            throw new \RuntimeException('Can only verify a message with a pay-to-pubkey-hash address');
1711
        }
1712
1713
        /** @var CompactSignatureSerializerInterface $csSerializer */
1714 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...
1715 1
        $signedMessage = new SignedMessage($message, $csSerializer->parse(new Buffer(base64_decode($signature))));
1716
1717 1
        $signer = new MessageSigner($adapter);
1718 1
        return $signer->verify($signedMessage, $addr);
1719
    }
1720
1721
    /**
1722
     * convert a Satoshi value to a BTC value
1723
     *
1724
     * @param int       $satoshi
1725
     * @return float
1726
     */
1727
    public static function toBTC($satoshi) {
1728
        return bcdiv((int)(string)$satoshi, 100000000, 8);
1729
    }
1730
1731
    /**
1732
     * convert a Satoshi value to a BTC value and return it as a string
1733
1734
     * @param int       $satoshi
1735
     * @return string
1736
     */
1737
    public static function toBTCString($satoshi) {
1738
        return sprintf("%.8f", self::toBTC($satoshi));
1739
    }
1740
1741
    /**
1742
     * convert a BTC value to a Satoshi value
1743
     *
1744
     * @param float     $btc
1745
     * @return string
1746
     */
1747
    public static function toSatoshiString($btc) {
1748
        return bcmul(sprintf("%.8f", (float)$btc), 100000000, 0);
1749
    }
1750
1751
    /**
1752
     * convert a BTC value to a Satoshi value
1753
     *
1754
     * @param float     $btc
1755
     * @return string
1756
     */
1757
    public static function toSatoshi($btc) {
1758
        return (int)self::toSatoshiString($btc);
1759
    }
1760
1761
    /**
1762
     * json_decode helper that throws exceptions when it fails to decode
1763
     *
1764
     * @param      $json
1765
     * @param bool $assoc
1766
     * @return mixed
1767
     * @throws \Exception
1768
     */
1769 25
    protected static function jsonDecode($json, $assoc = false) {
1770 25
        if (!$json) {
1771
            throw new \Exception("Can't json_decode empty string [{$json}]");
1772
        }
1773
1774 25
        $data = json_decode($json, $assoc);
1775
1776 25
        if ($data === null) {
1777
            throw new \Exception("Failed to json_decode [{$json}]");
1778
        }
1779
1780 25
        return $data;
1781
    }
1782
1783
    /**
1784
     * sort public keys for multisig script
1785
     *
1786
     * @param PublicKeyInterface[] $pubKeys
1787
     * @return PublicKeyInterface[]
1788
     */
1789
    public static function sortMultisigKeys(array $pubKeys) {
1790
        $result = array_values($pubKeys);
1791
        usort($result, function (PublicKeyInterface $a, PublicKeyInterface $b) {
1792
            $av = $a->getHex();
1793
            $bv = $b->getHex();
1794
            return $av == $bv ? 0 : $av > $bv ? 1 : -1;
1795
        });
1796
1797
        return $result;
1798
    }
1799
1800
    /**
1801
     * read and decode the json payload from a webhook's POST request.
1802
     *
1803
     * @param bool $returnObject    flag to indicate if an object or associative array should be returned
1804
     * @return mixed|null
1805
     * @throws \Exception
1806
     */
1807
    public static function getWebhookPayload($returnObject = false) {
1808
        $data = file_get_contents("php://input");
1809
        if ($data) {
1810
            return self::jsonDecode($data, !$returnObject);
1811
        } else {
1812
            return null;
1813
        }
1814
    }
1815
1816
    public static function normalizeBIP32KeyArray($keys) {
1817 3
        return Util::arrayMapWithIndex(function ($idx, $key) {
1818 3
            return [$idx, self::normalizeBIP32Key($key)];
1819 3
        }, $keys);
1820
    }
1821
1822
    /**
1823
     * @param array|BIP32Key $key
1824
     * @return BIP32Key
1825
     * @throws \Exception
1826
     */
1827 13
    public static function normalizeBIP32Key($key) {
1828 13
        if ($key instanceof BIP32Key) {
1829 4
            return $key;
1830
        }
1831
1832 12
        if (is_array($key) && count($key) === 2) {
1833 12
            $path = $key[1];
1834 12
            $hk = $key[0];
1835
1836 12
            if (!($hk instanceof HierarchicalKey)) {
1837 12
                $hk = HierarchicalKeyFactory::fromExtended($hk);
1838
            }
1839
1840
            return BIP32Key::create($hk, $path);
1841
        } else {
1842
            throw new \Exception("Bad Input");
1843
        }
1844
    }
1845
}
1846