Completed
Pull Request — master (#49)
by thomas
27:41
created

BlocktrailSDK::walletAddresses()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 4
dl 0
loc 9
ccs 6
cts 6
cp 1
crap 1
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\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\Connection\RestClientInterface;
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\RestClientInterface
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
     * @param   string      $apiKey         the API_KEY to use for authentication
52
     * @param   string      $apiSecret      the API_SECRET to use for authentication
53
     * @param   string      $network        the cryptocurrency 'network' to consume, eg BTC, LTC, etc
54
     * @param   bool        $testnet        testnet yes/no
55
     * @param   string      $apiVersion     the version of the API to consume
56
     * @param   null        $apiEndpoint    overwrite the endpoint used
57
     *                                       this will cause the $network, $testnet and $apiVersion to be ignored!
58
     */
59 87
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
60
61 87
        list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet);
62
63 87
        if (is_null($apiEndpoint)) {
64 87
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com";
65 87
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
66
        }
67
68
        // normalize network and set bitcoinlib to the right magic-bytes
69 87
        list($this->network, $this->testnet) = $this->normalizeNetwork($network, $testnet);
70 87
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet);
71
72 87
        $this->client = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
73 87
    }
74
75
    /**
76
     * normalize network string
77
     *
78
     * @param $network
79
     * @param $testnet
80
     * @return array
81
     * @throws \Exception
82
     */
83 87
    protected function normalizeNetwork($network, $testnet) {
84 87
        return Util::normalizeNetwork($network, $testnet);
85
    }
86
87
    /**
88
     * set BitcoinLib to the correct magic-byte defaults for the selected network
89
     *
90
     * @param $network
91
     * @param $testnet
92
     */
93 87
    protected function setBitcoinLibMagicBytes($network, $testnet) {
94 87
        assert($network == "bitcoin" || $network == "bitcoincash");
95 87
        Bitcoin::setNetwork($testnet ? NetworkFactory::bitcoinTestnet() : NetworkFactory::bitcoin());
96 87
    }
97
98
    /**
99
     * enable CURL debugging output
100
     *
101
     * @param   bool        $debug
102
     *
103
     * @codeCoverageIgnore
104
     */
105
    public function setCurlDebugging($debug = true) {
106
        $this->client->setCurlDebugging($debug);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Blocktrail\SDK\Connection\RestClientInterface as the method setCurlDebugging() does only exist in the following implementations of said interface: Blocktrail\SDK\Connection\RestClient.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
107
    }
108
109
    /**
110
     * enable verbose errors
111
     *
112
     * @param   bool        $verboseErrors
113
     *
114
     * @codeCoverageIgnore
115
     */
116
    public function setVerboseErrors($verboseErrors = true) {
117
        $this->client->setVerboseErrors($verboseErrors);
118
    }
119
    
120
    /**
121
     * set cURL default option on Guzzle client
122
     * @param string    $key
123
     * @param bool      $value
124
     *
125
     * @codeCoverageIgnore
126
     */
127
    public function setCurlDefaultOption($key, $value) {
128
        $this->client->setCurlDefaultOption($key, $value);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Blocktrail\SDK\Connection\RestClientInterface as the method setCurlDefaultOption() does only exist in the following implementations of said interface: Blocktrail\SDK\Connection\RestClient.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
129
    }
130
131
    /**
132
     * @return  RestClientInterface
133
     */
134 2
    public function getRestClient() {
135 2
        return $this->client;
136
    }
137
138
    /**
139
     * @param RestClientInterface $restClient
140
     */
141
    public function setRestClient(RestClientInterface $restClient) {
142
        $this->client = $restClient;
143
    }
144
145
    /**
146
     * get a single address
147
     * @param  string $address address hash
148
     * @return array           associative array containing the response
149
     */
150 1
    public function address($address) {
151 1
        $response = $this->client->get("address/{$address}");
152 1
        return self::jsonDecode($response->body(), true);
153
    }
154
155
    /**
156
     * get all transactions for an address (paginated)
157
     * @param  string  $address address hash
158
     * @param  integer $page    pagination: page number
159
     * @param  integer $limit   pagination: records per page (max 500)
160
     * @param  string  $sortDir pagination: sort direction (asc|desc)
161
     * @return array            associative array containing the response
162
     */
163 1
    public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
164
        $queryString = [
165 1
            'page' => $page,
166 1
            'limit' => $limit,
167 1
            'sort_dir' => $sortDir
168
        ];
169 1
        $response = $this->client->get("address/{$address}/transactions", $queryString);
170 1
        return self::jsonDecode($response->body(), true);
171
    }
172
173
    /**
174
     * get all unconfirmed transactions for an address (paginated)
175
     * @param  string  $address address hash
176
     * @param  integer $page    pagination: page number
177
     * @param  integer $limit   pagination: records per page (max 500)
178
     * @param  string  $sortDir pagination: sort direction (asc|desc)
179
     * @return array            associative array containing the response
180
     */
181 1
    public function addressUnconfirmedTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
182
        $queryString = [
183 1
            'page' => $page,
184 1
            'limit' => $limit,
185 1
            'sort_dir' => $sortDir
186
        ];
187 1
        $response = $this->client->get("address/{$address}/unconfirmed-transactions", $queryString);
188 1
        return self::jsonDecode($response->body(), true);
189
    }
190
191
    /**
192
     * get all unspent outputs for an address (paginated)
193
     * @param  string  $address address hash
194
     * @param  integer $page    pagination: page number
195
     * @param  integer $limit   pagination: records per page (max 500)
196
     * @param  string  $sortDir pagination: sort direction (asc|desc)
197
     * @return array            associative array containing the response
198
     */
199 1
    public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') {
200
        $queryString = [
201 1
            'page' => $page,
202 1
            'limit' => $limit,
203 1
            'sort_dir' => $sortDir
204
        ];
205 1
        $response = $this->client->get("address/{$address}/unspent-outputs", $queryString);
206 1
        return self::jsonDecode($response->body(), true);
207
    }
208
209
    /**
210
     * get all unspent outputs for a batch of addresses (paginated)
211
     *
212
     * @param  string[] $addresses
213
     * @param  integer  $page    pagination: page number
214
     * @param  integer  $limit   pagination: records per page (max 500)
215
     * @param  string   $sortDir pagination: sort direction (asc|desc)
216
     * @return array associative array containing the response
217
     * @throws \Exception
218
     */
219
    public function batchAddressUnspentOutputs($addresses, $page = 1, $limit = 20, $sortDir = 'asc') {
220
        $queryString = [
221
            'page' => $page,
222
            'limit' => $limit,
223
            'sort_dir' => $sortDir
224
        ];
225
        $response = $this->client->post("address/unspent-outputs", $queryString, ['addresses' => $addresses]);
226
        return self::jsonDecode($response->body(), true);
227
    }
228
229
    /**
230
     * verify ownership of an address
231
     * @param  string  $address     address hash
232
     * @param  string  $signature   a signed message (the address hash) using the private key of the address
233
     * @return array                associative array containing the response
234
     */
235 2
    public function verifyAddress($address, $signature) {
236 2
        $postData = ['signature' => $signature];
237
238 2
        $response = $this->client->post("address/{$address}/verify", null, $postData, RestClient::AUTH_HTTP_SIG);
239
240 2
        return self::jsonDecode($response->body(), true);
241
    }
242
243
    /**
244
     * get all blocks (paginated)
245
     * @param  integer $page    pagination: page number
246
     * @param  integer $limit   pagination: records per page
247
     * @param  string  $sortDir pagination: sort direction (asc|desc)
248
     * @return array            associative array containing the response
249
     */
250 1
    public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') {
251
        $queryString = [
252 1
            'page' => $page,
253 1
            'limit' => $limit,
254 1
            'sort_dir' => $sortDir
255
        ];
256 1
        $response = $this->client->get("all-blocks", $queryString);
257 1
        return self::jsonDecode($response->body(), true);
258
    }
259
260
    /**
261
     * get the latest block
262
     * @return array            associative array containing the response
263
     */
264 1
    public function blockLatest() {
265 1
        $response = $this->client->get("block/latest");
266 1
        return self::jsonDecode($response->body(), true);
267
    }
268
269
    /**
270
     * get an individual block
271
     * @param  string|integer $block    a block hash or a block height
272
     * @return array                    associative array containing the response
273
     */
274 1
    public function block($block) {
275 1
        $response = $this->client->get("block/{$block}");
276 1
        return self::jsonDecode($response->body(), true);
277
    }
278
279
    /**
280
     * get all transaction in a block (paginated)
281
     * @param  string|integer   $block   a block hash or a block height
282
     * @param  integer          $page    pagination: page number
283
     * @param  integer          $limit   pagination: records per page
284
     * @param  string           $sortDir pagination: sort direction (asc|desc)
285
     * @return array                     associative array containing the response
286
     */
287 1
    public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') {
288
        $queryString = [
289 1
            'page' => $page,
290 1
            'limit' => $limit,
291 1
            'sort_dir' => $sortDir
292
        ];
293 1
        $response = $this->client->get("block/{$block}/transactions", $queryString);
294 1
        return self::jsonDecode($response->body(), true);
295
    }
296
297
    /**
298
     * get a single transaction
299
     * @param  string $txhash transaction hash
300
     * @return array          associative array containing the response
301
     */
302 5
    public function transaction($txhash) {
303 5
        $response = $this->client->get("transaction/{$txhash}");
304 5
        return self::jsonDecode($response->body(), true);
305
    }
306
307
    /**
308
     * get a single transaction
309
     * @param  string[] $txhashes list of transaction hashes (up to 20)
310
     * @return array[]            array containing the response
311
     */
312
    public function transactions($txhashes) {
313
        $response = $this->client->get("transactions/" . implode(",", $txhashes));
314
        return self::jsonDecode($response->body(), true);
315
    }
316
    
317
    /**
318
     * get a paginated list of all webhooks associated with the api user
319
     * @param  integer          $page    pagination: page number
320
     * @param  integer          $limit   pagination: records per page
321
     * @return array                     associative array containing the response
322
     */
323 1
    public function allWebhooks($page = 1, $limit = 20) {
324
        $queryString = [
325 1
            'page' => $page,
326 1
            'limit' => $limit
327
        ];
328 1
        $response = $this->client->get("webhooks", $queryString);
329 1
        return self::jsonDecode($response->body(), true);
330
    }
331
332
    /**
333
     * get an existing webhook by it's identifier
334
     * @param string    $identifier     a unique identifier associated with the webhook
335
     * @return array                    associative array containing the response
336
     */
337 1
    public function getWebhook($identifier) {
338 1
        $response = $this->client->get("webhook/".$identifier);
339 1
        return self::jsonDecode($response->body(), true);
340
    }
341
342
    /**
343
     * create a new webhook
344
     * @param  string  $url        the url to receive the webhook events
345
     * @param  string  $identifier a unique identifier to associate with this webhook
346
     * @return array               associative array containing the response
347
     */
348 1
    public function setupWebhook($url, $identifier = null) {
349
        $postData = [
350 1
            'url'        => $url,
351 1
            'identifier' => $identifier
352
        ];
353 1
        $response = $this->client->post("webhook", null, $postData, RestClient::AUTH_HTTP_SIG);
354 1
        return self::jsonDecode($response->body(), true);
355
    }
356
357
    /**
358
     * update an existing webhook
359
     * @param  string  $identifier      the unique identifier of the webhook to update
360
     * @param  string  $newUrl          the new url to receive the webhook events
361
     * @param  string  $newIdentifier   a new unique identifier to associate with this webhook
362
     * @return array                    associative array containing the response
363
     */
364 1
    public function updateWebhook($identifier, $newUrl = null, $newIdentifier = null) {
365
        $putData = [
366 1
            'url'        => $newUrl,
367 1
            'identifier' => $newIdentifier
368
        ];
369 1
        $response = $this->client->put("webhook/{$identifier}", null, $putData, RestClient::AUTH_HTTP_SIG);
370 1
        return self::jsonDecode($response->body(), true);
371
    }
372
373
    /**
374
     * deletes an existing webhook and any event subscriptions associated with it
375
     * @param  string  $identifier      the unique identifier of the webhook to delete
376
     * @return boolean                  true on success
377
     */
378 1
    public function deleteWebhook($identifier) {
379 1
        $response = $this->client->delete("webhook/{$identifier}", null, null, RestClient::AUTH_HTTP_SIG);
380 1
        return self::jsonDecode($response->body(), true);
381
    }
382
383
    /**
384
     * get a paginated list of all the events a webhook is subscribed to
385
     * @param  string  $identifier  the unique identifier of the webhook
386
     * @param  integer $page        pagination: page number
387
     * @param  integer $limit       pagination: records per page
388
     * @return array                associative array containing the response
389
     */
390 2
    public function getWebhookEvents($identifier, $page = 1, $limit = 20) {
391
        $queryString = [
392 2
            'page' => $page,
393 2
            'limit' => $limit
394
        ];
395 2
        $response = $this->client->get("webhook/{$identifier}/events", $queryString);
396 2
        return self::jsonDecode($response->body(), true);
397
    }
398
    
399
    /**
400
     * subscribes a webhook to transaction events of one particular transaction
401
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
402
     * @param  string  $transaction     the transaction hash
403
     * @param  integer $confirmations   the amount of confirmations to send.
404
     * @return array                    associative array containing the response
405
     */
406 1
    public function subscribeTransaction($identifier, $transaction, $confirmations = 6) {
407
        $postData = [
408 1
            'event_type'    => 'transaction',
409 1
            'transaction'   => $transaction,
410 1
            'confirmations' => $confirmations,
411
        ];
412 1
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
413 1
        return self::jsonDecode($response->body(), true);
414
    }
415
416
    /**
417
     * subscribes a webhook to transaction events on a particular address
418
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
419
     * @param  string  $address         the address hash
420
     * @param  integer $confirmations   the amount of confirmations to send.
421
     * @return array                    associative array containing the response
422
     */
423 1
    public function subscribeAddressTransactions($identifier, $address, $confirmations = 6) {
424
        $postData = [
425 1
            'event_type'    => 'address-transactions',
426 1
            'address'       => $address,
427 1
            'confirmations' => $confirmations,
428
        ];
429 1
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
430 1
        return self::jsonDecode($response->body(), true);
431
    }
432
433
    /**
434
     * batch subscribes a webhook to multiple transaction events
435
     *
436
     * @param  string $identifier   the unique identifier of the webhook
437
     * @param  array  $batchData    A 2D array of event data:
438
     *                              [address => $address, confirmations => $confirmations]
439
     *                              where $address is the address to subscibe to
440
     *                              and optionally $confirmations is the amount of confirmations
441
     * @return boolean              true on success
442
     */
443 1
    public function batchSubscribeAddressTransactions($identifier, $batchData) {
444 1
        $postData = [];
445 1
        foreach ($batchData as $record) {
446 1
            $postData[] = [
447 1
                'event_type' => 'address-transactions',
448 1
                'address' => $record['address'],
449 1
                'confirmations' => isset($record['confirmations']) ? $record['confirmations'] : 6,
450
            ];
451
        }
452 1
        $response = $this->client->post("webhook/{$identifier}/events/batch", null, $postData, RestClient::AUTH_HTTP_SIG);
453 1
        return self::jsonDecode($response->body(), true);
454
    }
455
456
    /**
457
     * subscribes a webhook to a new block event
458
     * @param  string  $identifier  the unique identifier of the webhook to be triggered
459
     * @return array                associative array containing the response
460
     */
461 1
    public function subscribeNewBlocks($identifier) {
462
        $postData = [
463 1
            'event_type'    => 'block',
464
        ];
465 1
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
466 1
        return self::jsonDecode($response->body(), true);
467
    }
468
469
    /**
470
     * removes an transaction event subscription from a webhook
471
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
472
     * @param  string  $transaction     the transaction hash of the event subscription
473
     * @return boolean                  true on success
474
     */
475 1
    public function unsubscribeTransaction($identifier, $transaction) {
476 1
        $response = $this->client->delete("webhook/{$identifier}/transaction/{$transaction}", null, null, RestClient::AUTH_HTTP_SIG);
477 1
        return self::jsonDecode($response->body(), true);
478
    }
479
480
    /**
481
     * removes an address transaction event subscription from a webhook
482
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
483
     * @param  string  $address         the address hash of the event subscription
484
     * @return boolean                  true on success
485
     */
486 1
    public function unsubscribeAddressTransactions($identifier, $address) {
487 1
        $response = $this->client->delete("webhook/{$identifier}/address-transactions/{$address}", null, null, RestClient::AUTH_HTTP_SIG);
488 1
        return self::jsonDecode($response->body(), true);
489
    }
490
491
    /**
492
     * removes a block event subscription from a webhook
493
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
494
     * @return boolean                  true on success
495
     */
496 1
    public function unsubscribeNewBlocks($identifier) {
497 1
        $response = $this->client->delete("webhook/{$identifier}/block", null, null, RestClient::AUTH_HTTP_SIG);
498 1
        return self::jsonDecode($response->body(), true);
499
    }
500
501
    /**
502
     * create a new wallet
503
     *   - will generate a new primary seed (with password) and backup seed (without password)
504
     *   - send the primary seed (BIP39 'encrypted') and backup public key to the server
505
     *   - receive the blocktrail co-signing public key from the server
506
     *
507
     * Either takes one argument:
508
     * @param array $options
509
     *
510
     * Or takes three arguments (old, deprecated syntax):
511
     * (@nonPHP-doc) @param      $identifier
512
     * (@nonPHP-doc) @param      $password
513
     * (@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...
514
     *
515
     * @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...
516
     * @throws \Exception
517
     */
518 7
    public function createNewWallet($options) {
519 7
        if (!is_array($options)) {
520 1
            $args = func_get_args();
521
            $options = [
522 1
                "identifier" => $args[0],
523 1
                "password" => $args[1],
524 1
                "key_index" => isset($args[2]) ? $args[2] : null,
525
            ];
526
        }
527
528 7
        if (isset($options['password'])) {
529 1
            if (isset($options['passphrase'])) {
530
                throw new \InvalidArgumentException("Can only provide either passphrase or password");
531
            } else {
532 1
                $options['passphrase'] = $options['password'];
533
            }
534
        }
535
536 7
        if (!isset($options['passphrase'])) {
537 1
            $options['passphrase'] = null;
538
        }
539
540 7
        if (!isset($options['key_index'])) {
541
            $options['key_index'] = 0;
542
        }
543
544 7
        if (!isset($options['wallet_version'])) {
545 3
            $options['wallet_version'] = Wallet::WALLET_VERSION_V3;
546
        }
547
548 7
        switch ($options['wallet_version']) {
549 7
            case Wallet::WALLET_VERSION_V1:
550 1
                return $this->createNewWalletV1($options);
551
552 6
            case Wallet::WALLET_VERSION_V2:
553 2
                return $this->createNewWalletV2($options);
554
555 4
            case Wallet::WALLET_VERSION_V3:
556 4
                return $this->createNewWalletV3($options);
557
558
            default:
559
                throw new \InvalidArgumentException("Invalid wallet version");
560
        }
561
    }
562
563 1
    protected function createNewWalletV1($options) {
564 1
        $walletPath = WalletPath::create($options['key_index']);
565
566 1
        $storePrimaryMnemonic = isset($options['store_primary_mnemonic']) ? $options['store_primary_mnemonic'] : null;
567
568 1
        if (isset($options['primary_mnemonic']) && isset($options['primary_private_key'])) {
569
            throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey");
570
        }
571
572 1
        $primaryMnemonic = null;
573 1
        $primaryPrivateKey = null;
574 1
        if (!isset($options['primary_mnemonic']) && !isset($options['primary_private_key'])) {
575 1
            if (!$options['passphrase']) {
576
                throw new \InvalidArgumentException("Can't generate Primary Mnemonic without a passphrase");
577
            } else {
578
                // create new primary seed
579
                /** @var HierarchicalKey $primaryPrivateKey */
580 1
                list($primaryMnemonic, , $primaryPrivateKey) = $this->newPrimarySeed($options['passphrase']);
581 1
                if ($storePrimaryMnemonic !== false) {
582 1
                    $storePrimaryMnemonic = true;
583
                }
584
            }
585
        } elseif (isset($options['primary_mnemonic'])) {
586
            $primaryMnemonic = $options['primary_mnemonic'];
587
        } elseif (isset($options['primary_private_key'])) {
588
            $primaryPrivateKey = $options['primary_private_key'];
589
        }
590
591 1
        if ($storePrimaryMnemonic && $primaryMnemonic && !$options['passphrase']) {
592
            throw new \InvalidArgumentException("Can't store Primary Mnemonic on server without a passphrase");
593
        }
594
595 1
        if ($primaryPrivateKey) {
596 1
            if (is_string($primaryPrivateKey)) {
597 1
                $primaryPrivateKey = [$primaryPrivateKey, "m"];
598
            }
599
        } else {
600
            $primaryPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($primaryMnemonic, $options['passphrase']));
601
        }
602
603 1
        if (!$storePrimaryMnemonic) {
604
            $primaryMnemonic = false;
605
        }
606
607
        // create primary public key from the created private key
608 1
        $path = $walletPath->keyIndexPath()->publicPath();
609 1
        $primaryPublicKey = BIP32Key::create($primaryPrivateKey, "m")->buildKey($path);
610
611 1
        if (isset($options['backup_mnemonic']) && $options['backup_public_key']) {
612
            throw new \InvalidArgumentException("Can't specify Backup Mnemonic and Backup PublicKey");
613
        }
614
615 1
        $backupMnemonic = null;
616 1
        $backupPublicKey = null;
617 1
        if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) {
618
            /** @var HierarchicalKey $backupPrivateKey */
619 1
            list($backupMnemonic, , ) = $this->newBackupSeed();
620
        } else if (isset($options['backup_mnemonic'])) {
621
            $backupMnemonic = $options['backup_mnemonic'];
622
        } elseif (isset($options['backup_public_key'])) {
623
            $backupPublicKey = $options['backup_public_key'];
624
        }
625
626 1
        if ($backupPublicKey) {
627
            if (is_string($backupPublicKey)) {
628
                $backupPublicKey = [$backupPublicKey, "m"];
629
            }
630
        } else {
631 1
            $backupPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, ""));
632 1
            $backupPublicKey = BIP32Key::create($backupPrivateKey->toPublic(), "M");
633
        }
634
635
        // create a checksum of our private key which we'll later use to verify we used the right password
636 1
        $checksum = $primaryPrivateKey->getPublicKey()->getAddress()->getAddress();
637
638
        // send the public keys to the server to store them
639
        //  and the mnemonic, which is safe because it's useless without the password
640 1
        $data = $this->storeNewWalletV1(
641 1
            $options['identifier'],
642 1
            $primaryPublicKey->tuple(),
643 1
            $backupPublicKey->tuple(),
644 1
            $primaryMnemonic,
645 1
            $checksum,
646 1
            $options['key_index'],
647 1
            array_key_exists('segwit', $options) ? $options['segwit'] : false
648
        );
649
650
        // received the blocktrail public keys
651
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
652 1
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
653 1
        }, $data['blocktrail_public_keys']);
654
655 1
        $wallet = new WalletV1(
656 1
            $this,
657 1
            $options['identifier'],
658 1
            $primaryMnemonic,
659 1
            [$options['key_index'] => $primaryPublicKey],
660 1
            $backupPublicKey,
661 1
            $blocktrailPublicKeys,
662 1
            $options['key_index'],
663 1
            $this->network,
664 1
            $this->testnet,
665 1
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
666 1
            $checksum
667
        );
668
669 1
        $wallet->unlock($options);
670
671
        // return wallet and backup mnemonic
672
        return [
673 1
            $wallet,
674
            [
675 1
                'primary_mnemonic' => $primaryMnemonic,
676 1
                'backup_mnemonic' => $backupMnemonic,
677 1
                'blocktrail_public_keys' => $blocktrailPublicKeys,
678
            ],
679
        ];
680
    }
681
682 5
    public static function randomBits($bits) {
683 5
        return self::randomBytes($bits / 8);
684
    }
685
686 5
    public static function randomBytes($bytes) {
687 5
        return (new Random())->bytes($bytes)->getBinary();
688
    }
689
690 2
    protected function createNewWalletV2($options) {
691 2
        $walletPath = WalletPath::create($options['key_index']);
692
693 2
        if (isset($options['store_primary_mnemonic'])) {
694
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
695
        }
696
697 2
        if (!isset($options['store_data_on_server'])) {
698 2
            if (isset($options['primary_private_key'])) {
699 1
                $options['store_data_on_server'] = false;
700
            } else {
701 1
                $options['store_data_on_server'] = true;
702
            }
703
        }
704
705 2
        $storeDataOnServer = $options['store_data_on_server'];
706
707 2
        $secret = null;
708 2
        $encryptedSecret = null;
709 2
        $primarySeed = null;
710 2
        $encryptedPrimarySeed = null;
711 2
        $recoverySecret = null;
712 2
        $recoveryEncryptedSecret = null;
713 2
        $backupSeed = null;
714
715 2
        if (!isset($options['primary_private_key'])) {
716 1
            $primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : self::randomBits(256);
717
        }
718
719 2
        if ($storeDataOnServer) {
720 1
            if (!isset($options['secret'])) {
721 1
                if (!$options['passphrase']) {
722
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
723
                }
724
725 1
                $secret = bin2hex(self::randomBits(256)); // string because we use it as passphrase
726 1
                $encryptedSecret = CryptoJSAES::encrypt($secret, $options['passphrase']);
727
            } else {
728
                $secret = $options['secret'];
729
            }
730
731 1
            $encryptedPrimarySeed = CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
732 1
            $recoverySecret = bin2hex(self::randomBits(256));
733
734 1
            $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);
735
        }
736
737 2
        if (!isset($options['backup_public_key'])) {
738 1
            $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : self::randomBits(256);
739
        }
740
741 2
        if (isset($options['primary_private_key'])) {
742 1
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
743
        } else {
744 1
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m");
745
        }
746
747
        // create primary public key from the created private key
748 2
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
749
750 2
        if (!isset($options['backup_public_key'])) {
751 1
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M");
752
        }
753
754
        // create a checksum of our private key which we'll later use to verify we used the right password
755 2
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
756
757
        // send the public keys and encrypted data to server
758 2
        $data = $this->storeNewWalletV2(
759 2
            $options['identifier'],
760 2
            $options['primary_public_key']->tuple(),
761 2
            $options['backup_public_key']->tuple(),
762 2
            $storeDataOnServer ? $encryptedPrimarySeed : false,
763 2
            $storeDataOnServer ? $encryptedSecret : false,
764 2
            $storeDataOnServer ? $recoverySecret : false,
765 2
            $checksum,
766 2
            $options['key_index'],
767 2
            array_key_exists('segwit', $options) ? $options['segwit'] : false
768
        );
769
770
        // received the blocktrail public keys
771
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
772 2
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
773 2
        }, $data['blocktrail_public_keys']);
774
775 2
        $wallet = new WalletV2(
776 2
            $this,
777 2
            $options['identifier'],
778 2
            $encryptedPrimarySeed,
779 2
            $encryptedSecret,
780 2
            [$options['key_index'] => $options['primary_public_key']],
781 2
            $options['backup_public_key'],
782 2
            $blocktrailPublicKeys,
783 2
            $options['key_index'],
784 2
            $this->network,
785 2
            $this->testnet,
786 2
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
787 2
            $checksum
788
        );
789
790 2
        $wallet->unlock([
791 2
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
792 2
            'primary_private_key' => $options['primary_private_key'],
793 2
            'primary_seed' => $primarySeed,
794 2
            'secret' => $secret,
795
        ]);
796
797
        // return wallet and mnemonics for backup sheet
798
        return [
799 2
            $wallet,
800
            [
801 2
                'encrypted_primary_seed' => $encryptedPrimarySeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedPrimarySeed))) : null,
802 2
                'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer($backupSeed)) : null,
803 2
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($recoveryEncryptedSecret))) : null,
804 2
                'encrypted_secret' => $encryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedSecret))) : null,
805
                'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
806 2
                    return [$keyIndex, $pubKey->tuple()];
807 2
                }, $blocktrailPublicKeys),
808
            ],
809
        ];
810
    }
811
812 4
    protected function createNewWalletV3($options) {
813 4
        $walletPath = WalletPath::create($options['key_index']);
814
815 4
        if (isset($options['store_primary_mnemonic'])) {
816
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
817
        }
818
819 4
        if (!isset($options['store_data_on_server'])) {
820 4
            if (isset($options['primary_private_key'])) {
821
                $options['store_data_on_server'] = false;
822
            } else {
823 4
                $options['store_data_on_server'] = true;
824
            }
825
        }
826
827 4
        $storeDataOnServer = $options['store_data_on_server'];
828
829 4
        $secret = null;
830 4
        $encryptedSecret = null;
831 4
        $primarySeed = null;
832 4
        $encryptedPrimarySeed = null;
833 4
        $recoverySecret = null;
834 4
        $recoveryEncryptedSecret = null;
835 4
        $backupSeed = null;
836
837 4
        if (!isset($options['primary_private_key'])) {
838 4
            if (isset($options['primary_seed'])) {
839
                if (!$options['primary_seed'] instanceof BufferInterface) {
840
                    throw new \InvalidArgumentException('Primary Seed should be passed as a Buffer');
841
                }
842
                $primarySeed = $options['primary_seed'];
843
            } else {
844 4
                $primarySeed = new Buffer(self::randomBits(256));
845
            }
846
        }
847
848 4
        if ($storeDataOnServer) {
849 4
            if (!isset($options['secret'])) {
850 4
                if (!$options['passphrase']) {
851
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
852
                }
853
854 4
                $secret = new Buffer(self::randomBits(256));
855 4
                $encryptedSecret = Encryption::encrypt($secret, new Buffer($options['passphrase']), KeyDerivation::DEFAULT_ITERATIONS);
856
            } else {
857
                if (!$options['secret'] instanceof Buffer) {
858
                    throw new \RuntimeException('Secret must be provided as a Buffer');
859
                }
860
861
                $secret = $options['secret'];
862
            }
863
864 4
            $encryptedPrimarySeed = Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS);
865 4
            $recoverySecret = new Buffer(self::randomBits(256));
866
867 4
            $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS);
868
        }
869
870 4
        if (!isset($options['backup_public_key'])) {
871 4
            if (isset($options['backup_seed'])) {
872
                if (!$options['backup_seed'] instanceof Buffer) {
873
                    throw new \RuntimeException('Backup seed must be an instance of Buffer');
874
                }
875
                $backupSeed = $options['backup_seed'];
876
            } else {
877 4
                $backupSeed = new Buffer(self::randomBits(256));
878
            }
879
        }
880
881 4
        if (isset($options['primary_private_key'])) {
882
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
883
        } else {
884 4
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m");
885
        }
886
887
        // create primary public key from the created private key
888 4
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
889
890 4
        if (!isset($options['backup_public_key'])) {
891 4
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($backupSeed), "m")->buildKey("M");
892
        }
893
894
        // create a checksum of our private key which we'll later use to verify we used the right password
895 4
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
896
897
        // send the public keys and encrypted data to server
898 4
        $data = $this->storeNewWalletV3(
899 4
            $options['identifier'],
900 4
            $options['primary_public_key']->tuple(),
901 4
            $options['backup_public_key']->tuple(),
902 4
            $storeDataOnServer ? base64_encode($encryptedPrimarySeed->getBinary()) : false,
903 4
            $storeDataOnServer ? base64_encode($encryptedSecret->getBinary()) : false,
904 4
            $storeDataOnServer ? $recoverySecret->getHex() : false,
905 4
            $checksum,
906 4
            $options['key_index'],
907 4
            array_key_exists('segwit', $options) ? $options['segwit'] : false
908
        );
909
910
        // received the blocktrail public keys
911
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
912 4
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
913 4
        }, $data['blocktrail_public_keys']);
914
915 4
        $wallet = new WalletV3(
916 4
            $this,
917 4
            $options['identifier'],
918 4
            $encryptedPrimarySeed,
0 ignored issues
show
Bug introduced by
It seems like $encryptedPrimarySeed defined by null on line 832 can be null; however, Blocktrail\SDK\WalletV3::__construct() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
919 4
            $encryptedSecret,
0 ignored issues
show
Bug introduced by
It seems like $encryptedSecret defined by null on line 830 can be null; however, Blocktrail\SDK\WalletV3::__construct() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

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