Completed
Pull Request — master (#49)
by thomas
16:35
created

BlocktrailSDK::createNewWalletV1()   F

Complexity

Conditions 26
Paths 794

Size

Total Lines 120
Code Lines 77

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 56
CRAP Score 33.4161

Importance

Changes 0
Metric Value
cc 26
eloc 77
nc 794
nop 1
dl 0
loc 120
ccs 56
cts 72
cp 0.7778
crap 33.4161
rs 2.2136
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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