Completed
Branch master (ebb53c)
by
unknown
05:48
created

BlocktrailSDK::unsubscribeTransaction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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