Completed
Pull Request — master (#128)
by thomas
05:59 queued 02:02
created

BlocktrailSDK::createNewWalletV1()   F

Complexity

Conditions 27
Paths 794

Size

Total Lines 121

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 756

Importance

Changes 0
Metric Value
cc 27
nc 794
nop 1
dl 0
loc 121
ccs 0
cts 73
cp 0
crap 756
rs 0.2288
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\Backend\BlocktrailConverter;
27
use Blocktrail\SDK\Backend\BtccomConverter;
28
use Blocktrail\SDK\Backend\ConverterInterface;
29
use Blocktrail\SDK\Bitcoin\BIP32Key;
30
use Blocktrail\SDK\Connection\RestClient;
31
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
32
use Blocktrail\SDK\Network\BitcoinCash;
33
use Blocktrail\SDK\Connection\RestClientInterface;
34
use Blocktrail\SDK\Network\BitcoinCashRegtest;
35
use Blocktrail\SDK\Network\BitcoinCashTestnet;
36
use Btccom\JustEncrypt\Encryption;
37
use Btccom\JustEncrypt\EncryptionMnemonic;
38
use Btccom\JustEncrypt\KeyDerivation;
39
40
/**
41
 * Class BlocktrailSDK
42
 */
43
class BlocktrailSDK implements BlocktrailSDKInterface {
44
    /**
45
     * @var Connection\RestClientInterface
46
     */
47
    protected $blocktrailClient;
48
49
    /**
50
     * @var Connection\RestClient
51
     */
52
    protected $dataClient;
53
54
    /**
55
     * @var string          currently only supporting; bitcoin
56
     */
57
    protected $network;
58
59
    /**
60
     * @var bool
61
     */
62
    protected $testnet;
63
64
    /**
65
     * @var ConverterInterface
66
     */
67
    protected $converter;
68
69
    /**
70
     * @param   string      $apiKey         the API_KEY to use for authentication
71
     * @param   string      $apiSecret      the API_SECRET to use for authentication
72
     * @param   string      $network        the cryptocurrency 'network' to consume, eg BTC, LTC, etc
73
     * @param   bool        $testnet        testnet yes/no
74
     * @param   string      $apiVersion     the version of the API to consume
75
     * @param   null        $apiEndpoint    overwrite the endpoint used
76
     *                                       this will cause the $network, $testnet and $apiVersion to be ignored!
77
     */
78 119
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
79
80 119
        list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet);
81
82 119
        if (is_null($apiEndpoint)) {
83 119
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com";
84 119
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
85
        }
86
87
        // normalize network and set bitcoinlib to the right magic-bytes
88 119
        list($this->network, $this->testnet, $regtest) = $this->normalizeNetwork($network, $testnet);
89 119
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet, $regtest);
90
91 119
        $btccomEndpoint = getenv('BLOCKTRAIL_SDK_BTCCOM_API_ENDPOINT');
92 119
        if (!$btccomEndpoint) {
93
            $btccomEndpoint = "https://" . ($this->network === "BCC" ? "bch-chain" : "chain") . ".api.btc.com";
94
        }
95 119
        $btccomEndpoint = "{$btccomEndpoint}/v3/";
96
97 119
        if ($this->testnet && strpos($btccomEndpoint, "tchain") === false) {
98 33
            $btccomEndpoint = \str_replace("chain", "tchain", $btccomEndpoint);
99
        }
100
101 119
        $this->blocktrailClient = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
102 119
        $this->dataClient = new RestClient($btccomEndpoint, $apiVersion, $apiKey, $apiSecret);
103
104 119
        $this->converter = new BtccomConverter();
105 119
    }
106
107
    /**
108
     * normalize network string
109
     *
110
     * @param $network
111
     * @param $testnet
112
     * @return array
113
     * @throws \Exception
114
     */
115 119
    protected function normalizeNetwork($network, $testnet) {
116
        // [name, testnet, network]
117 119
        return Util::normalizeNetwork($network, $testnet);
118
    }
119
120
    /**
121
     * set BitcoinLib to the correct magic-byte defaults for the selected network
122
     *
123
     * @param $network
124
     * @param bool $testnet
125
     * @param bool $regtest
126
     */
127 119
    protected function setBitcoinLibMagicBytes($network, $testnet, $regtest) {
128
129 119
        if ($network === "bitcoin") {
130 119
            if ($regtest) {
131
                $useNetwork = NetworkFactory::bitcoinRegtest();
132 119
            } else if ($testnet) {
133 29
                $useNetwork = NetworkFactory::bitcoinTestnet();
134
            } else {
135 119
                $useNetwork = NetworkFactory::bitcoin();
136
            }
137 4
        } else if ($network === "bitcoincash") {
138 4
            if ($regtest) {
139
                $useNetwork = new BitcoinCashRegtest();
140 4
            } else if ($testnet) {
141 4
                $useNetwork = new BitcoinCashTestnet();
142
            } else {
143
                $useNetwork = new BitcoinCash();
144
            }
145
        }
146
147 119
        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...
148 119
    }
149
150
    /**
151
     * enable CURL debugging output
152
     *
153
     * @param   bool        $debug
154
     *
155
     * @codeCoverageIgnore
156
     */
157
    public function setCurlDebugging($debug = true) {
158
        $this->blocktrailClient->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...
159
        $this->dataClient->setCurlDebugging($debug);
160
    }
161
162
    /**
163
     * enable verbose errors
164
     *
165
     * @param   bool        $verboseErrors
166
     *
167
     * @codeCoverageIgnore
168
     */
169
    public function setVerboseErrors($verboseErrors = true) {
170
        $this->blocktrailClient->setVerboseErrors($verboseErrors);
171
        $this->dataClient->setVerboseErrors($verboseErrors);
172
    }
173
    
174
    /**
175
     * set cURL default option on Guzzle client
176
     * @param string    $key
177
     * @param bool      $value
178
     *
179
     * @codeCoverageIgnore
180
     */
181
    public function setCurlDefaultOption($key, $value) {
182
        $this->blocktrailClient->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...
183
        $this->dataClient->setCurlDefaultOption($key, $value);
184
    }
185
186
    /**
187
     * @return  RestClientInterface
188
     */
189 2
    public function getRestClient() {
190 2
        return $this->blocktrailClient;
191
    }
192
193
    /**
194
     * @return  RestClient
195
     */
196
    public function getDataRestClient() {
197
        return $this->dataClient;
198
    }
199
200
    /**
201
     * @param RestClientInterface $restClient
202
     */
203
    public function setRestClient(RestClientInterface $restClient) {
204
        $this->client = $restClient;
0 ignored issues
show
Bug introduced by
The property client does not seem to exist. Did you mean blocktrailClient?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
205
    }
206
207
    /**
208
     * get a single address
209
     * @param  string $address address hash
210
     * @return array           associative array containing the response
211
     */
212 1
    public function address($address) {
213 1
        $response = $this->dataClient->get($this->converter->getUrlForAddress($address));
214 1
        return $this->converter->convertAddress($response->body());
215
    }
216
217
    /**
218
     * get all transactions for an address (paginated)
219
     * @param  string  $address address hash
220
     * @param  integer $page    pagination: page number
221
     * @param  integer $limit   pagination: records per page (max 500)
222
     * @param  string  $sortDir pagination: sort direction (asc|desc)
223
     * @return array            associative array containing the response
224
     */
225 1
    public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
226
        $queryString = [
227 1
            'page' => $page,
228 1
            'limit' => $limit,
229 1
            'sort_dir' => $sortDir
230
        ];
231 1
        $response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
232 1
        return $this->converter->convertAddressTxs($response->body());
233
    }
234
235
    /**
236
     * get all unconfirmed transactions for an address (paginated)
237
     * @param  string  $address address hash
238
     * @param  integer $page    pagination: page number
239
     * @param  integer $limit   pagination: records per page (max 500)
240
     * @param  string  $sortDir pagination: sort direction (asc|desc)
241
     * @return array            associative array containing the response
242
     */
243
    public function addressUnconfirmedTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
244
        $queryString = [
245
            'page' => $page,
246
            'limit' => $limit,
247
            'sort_dir' => $sortDir
248
        ];
249
        $response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
250
        return $this->converter->convertAddressTxs($response->body());
251
    }
252
253
    /**
254
     * get all unspent outputs for an address (paginated)
255
     * @param  string  $address address hash
256
     * @param  integer $page    pagination: page number
257
     * @param  integer $limit   pagination: records per page (max 500)
258
     * @param  string  $sortDir pagination: sort direction (asc|desc)
259
     * @return array            associative array containing the response
260
     */
261 1
    public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') {
262
        $queryString = [
263 1
            'page' => $page,
264 1
            'limit' => $limit,
265 1
            'sort_dir' => $sortDir
266
        ];
267 1
        $response = $this->dataClient->get($this->converter->getUrlForAddressUnspent($address), $this->converter->paginationParams($queryString));
268 1
        return $this->converter->convertAddressUnspentOutputs($response->body(), $address);
269
    }
270
271
    /**
272
     * get all unspent outputs for a batch of addresses (paginated)
273
     *
274
     * @param  string[] $addresses
275
     * @param  integer  $page    pagination: page number
276
     * @param  integer  $limit   pagination: records per page (max 500)
277
     * @param  string   $sortDir pagination: sort direction (asc|desc)
278
     * @return array associative array containing the response
279
     * @throws \Exception
280
     */
281
    public function batchAddressUnspentOutputs($addresses, $page = 1, $limit = 20, $sortDir = 'asc') {
282
        $queryString = [
283
            'page' => $page,
284
            'limit' => $limit,
285
            'sort_dir' => $sortDir
286
        ];
287
288
        if ($this->converter instanceof BtccomConverter) {
289
            if ($page > 1) {
290
                return [
291
                    'data' => [],
292
                    'current_page' => 2,
293
                    'per_page' => null,
294
                    'total' => null,
295
                ];
296
            }
297
298
            $response = $this->dataClient->get($this->converter->getUrlForBatchAddressesUnspent($addresses), $this->converter->paginationParams($queryString));
299
            return $this->converter->convertBatchAddressesUnspentOutputs($response->body());
300
        } else {
301
            $response = $this->client->post("address/unspent-outputs", $queryString, ['addresses' => $addresses]);
0 ignored issues
show
Bug introduced by
The property client does not seem to exist. Did you mean blocktrailClient?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
302
            return self::jsonDecode($response->body(), true);
303
        }
304
    }
305
306
    /**
307
     * verify ownership of an address
308
     * @param  string  $address     address hash
309
     * @param  string  $signature   a signed message (the address hash) using the private key of the address
310
     * @return array                associative array containing the response
311
     */
312 1
    public function verifyAddress($address, $signature) {
313 1
        if ($this->verifyMessage($address, $address, $signature)) {
314 1
            return ['result' => true, 'msg' => 'Successfully verified'];
315
        } else {
316
            return ['result' => false];
317
        }
318
    }
319
320
    /**
321
     * get all blocks (paginated)
322
     * @param  integer $page    pagination: page number
323
     * @param  integer $limit   pagination: records per page
324
     * @param  string  $sortDir pagination: sort direction (asc|desc)
325
     * @return array            associative array containing the response
326
     */
327 1
    public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') {
328
        $queryString = [
329 1
            'page' => $page,
330 1
            'limit' => $limit,
331 1
            'sort_dir' => $sortDir
332
        ];
333 1
        $response = $this->dataClient->get($this->converter->getUrlForAllBlocks(), $this->converter->paginationParams($queryString));
334 1
        return $this->converter->convertBlocks($response->body());
335
    }
336
337
    /**
338
     * get the latest block
339
     * @return array            associative array containing the response
340
     */
341 1
    public function blockLatest() {
342 1
        $response = $this->dataClient->get($this->converter->getUrlForBlock("latest"));
343 1
        return $this->converter->convertBlock($response->body());
344
    }
345
346
    /**
347
     * get the wallet API's latest block ['hash' => x, 'height' => y]
348
     * @return array            associative array containing the response
349
     */
350 1
    public function getWalletBlockLatest() {
351 1
        $response = $this->blocktrailClient->get("block/latest");
352
        return BlocktrailSDK::jsonDecode($response->body(), true);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
353
    }
354
355
    /**
356
     * get an individual block
357
     * @param  string|integer $block    a block hash or a block height
358
     * @return array                    associative array containing the response
359
     */
360 1
    public function block($block) {
361 1
        $response = $this->dataClient->get($this->converter->getUrlForBlock($block));
362 1
        return $this->converter->convertBlock($response->body());
363
    }
364
365
    /**
366
     * get all transaction in a block (paginated)
367
     * @param  string|integer   $block   a block hash or a block height
368
     * @param  integer          $page    pagination: page number
369
     * @param  integer          $limit   pagination: records per page
370
     * @param  string           $sortDir pagination: sort direction (asc|desc)
371
     * @return array                     associative array containing the response
372
     */
373
    public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') {
374
        $queryString = [
375
            'page' => $page,
376
            'limit' => $limit,
377
            'sort_dir' => $sortDir
378
        ];
379
        $response = $this->dataClient->get($this->converter->getUrlForBlockTransaction($block), $this->converter->paginationParams($queryString));
380
        return $this->converter->convertBlockTxs($response->body());
381
    }
382
383
    /**
384
     * get a single transaction
385
     * @param  string $txhash transaction hash
386
     * @return array          associative array containing the response
387
     */
388 1
    public function transaction($txhash) {
389 1
        $response = $this->dataClient->get($this->converter->getUrlForTransaction($txhash));
390 1
        $res = $this->converter->convertTx($response->body(), null);
391
392 1
        if ($this->converter instanceof BtccomConverter) {
393 1
            $res['raw'] = \json_decode($this->dataClient->get("tx/{$txhash}/raw")->body(), true)['data'];
394
        }
395
396 1
        return $res;
397
    }
398
399
    /**
400
     * get a single transaction
401
     * @param  string[] $txhashes list of transaction hashes (up to 20)
402
     * @return array[]            array containing the response
403
     */
404 1
    public function transactions($txhashes) {
405 1
        $response = $this->dataClient->get($this->converter->getUrlForTransactions($txhashes));
406 1
        return $this->converter->convertTxs($response->body());
407
    }
408
    
409
    /**
410
     * get a paginated list of all webhooks associated with the api user
411
     * @param  integer          $page    pagination: page number
412
     * @param  integer          $limit   pagination: records per page
413
     * @return array                     associative array containing the response
414
     */
415
    public function allWebhooks($page = 1, $limit = 20) {
416
        $queryString = [
417
            'page' => $page,
418
            'limit' => $limit
419
        ];
420
        $response = $this->blocktrailClient->get("webhooks", $this->converter->paginationParams($queryString));
421
        return self::jsonDecode($response->body(), true);
422
    }
423
424
    /**
425
     * get an existing webhook by it's identifier
426
     * @param string    $identifier     a unique identifier associated with the webhook
427
     * @return array                    associative array containing the response
428
     */
429
    public function getWebhook($identifier) {
430
        $response = $this->blocktrailClient->get("webhook/".$identifier);
431
        return self::jsonDecode($response->body(), true);
432
    }
433
434
    /**
435
     * create a new webhook
436
     * @param  string  $url        the url to receive the webhook events
437
     * @param  string  $identifier a unique identifier to associate with this webhook
438
     * @return array               associative array containing the response
439
     */
440 1
    public function setupWebhook($url, $identifier = null) {
441
        $postData = [
442 1
            'url'        => $url,
443 1
            'identifier' => $identifier
444
        ];
445 1
        $response = $this->blocktrailClient->post("webhook", null, $postData, RestClient::AUTH_HTTP_SIG);
446
        return self::jsonDecode($response->body(), true);
447
    }
448
449
    /**
450
     * update an existing webhook
451
     * @param  string  $identifier      the unique identifier of the webhook to update
452
     * @param  string  $newUrl          the new url to receive the webhook events
453
     * @param  string  $newIdentifier   a new unique identifier to associate with this webhook
454
     * @return array                    associative array containing the response
455
     */
456
    public function updateWebhook($identifier, $newUrl = null, $newIdentifier = null) {
457
        $putData = [
458
            'url'        => $newUrl,
459
            'identifier' => $newIdentifier
460
        ];
461
        $response = $this->blocktrailClient->put("webhook/{$identifier}", null, $putData, RestClient::AUTH_HTTP_SIG);
462
        return self::jsonDecode($response->body(), true);
463
    }
464
465
    /**
466
     * deletes an existing webhook and any event subscriptions associated with it
467
     * @param  string  $identifier      the unique identifier of the webhook to delete
468
     * @return boolean                  true on success
469
     */
470
    public function deleteWebhook($identifier) {
471
        $response = $this->blocktrailClient->delete("webhook/{$identifier}", null, null, RestClient::AUTH_HTTP_SIG);
472
        return self::jsonDecode($response->body(), true);
473
    }
474
475
    /**
476
     * get a paginated list of all the events a webhook is subscribed to
477
     * @param  string  $identifier  the unique identifier of the webhook
478
     * @param  integer $page        pagination: page number
479
     * @param  integer $limit       pagination: records per page
480
     * @return array                associative array containing the response
481
     */
482
    public function getWebhookEvents($identifier, $page = 1, $limit = 20) {
483
        $queryString = [
484
            'page' => $page,
485
            'limit' => $limit
486
        ];
487
        $response = $this->blocktrailClient->get("webhook/{$identifier}/events", $this->converter->paginationParams($queryString));
488
        return self::jsonDecode($response->body(), true);
489
    }
490
    
491
    /**
492
     * subscribes a webhook to transaction events of one particular transaction
493
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
494
     * @param  string  $transaction     the transaction hash
495
     * @param  integer $confirmations   the amount of confirmations to send.
496
     * @return array                    associative array containing the response
497
     */
498
    public function subscribeTransaction($identifier, $transaction, $confirmations = 6) {
499
        $postData = [
500
            'event_type'    => 'transaction',
501
            'transaction'   => $transaction,
502
            'confirmations' => $confirmations,
503
        ];
504
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
505
        return self::jsonDecode($response->body(), true);
506
    }
507
508
    /**
509
     * subscribes a webhook to transaction events on a particular address
510
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
511
     * @param  string  $address         the address hash
512
     * @param  integer $confirmations   the amount of confirmations to send.
513
     * @return array                    associative array containing the response
514
     */
515
    public function subscribeAddressTransactions($identifier, $address, $confirmations = 6) {
516
        $postData = [
517
            'event_type'    => 'address-transactions',
518
            'address'       => $address,
519
            'confirmations' => $confirmations,
520
        ];
521
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
522
        return self::jsonDecode($response->body(), true);
523
    }
524
525
    /**
526
     * batch subscribes a webhook to multiple transaction events
527
     *
528
     * @param  string $identifier   the unique identifier of the webhook
529
     * @param  array  $batchData    A 2D array of event data:
530
     *                              [address => $address, confirmations => $confirmations]
531
     *                              where $address is the address to subscibe to
532
     *                              and optionally $confirmations is the amount of confirmations
533
     * @return boolean              true on success
534
     */
535
    public function batchSubscribeAddressTransactions($identifier, $batchData) {
536
        $postData = [];
537
        foreach ($batchData as $record) {
538
            $postData[] = [
539
                'event_type' => 'address-transactions',
540
                'address' => $record['address'],
541
                'confirmations' => isset($record['confirmations']) ? $record['confirmations'] : 6,
542
            ];
543
        }
544
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events/batch", null, $postData, RestClient::AUTH_HTTP_SIG);
545
        return self::jsonDecode($response->body(), true);
546
    }
547
548
    /**
549
     * subscribes a webhook to a new block event
550
     * @param  string  $identifier  the unique identifier of the webhook to be triggered
551
     * @return array                associative array containing the response
552
     */
553
    public function subscribeNewBlocks($identifier) {
554
        $postData = [
555
            'event_type'    => 'block',
556
        ];
557
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
558
        return self::jsonDecode($response->body(), true);
559
    }
560
561
    /**
562
     * removes an transaction event subscription from a webhook
563
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
564
     * @param  string  $transaction     the transaction hash of the event subscription
565
     * @return boolean                  true on success
566
     */
567
    public function unsubscribeTransaction($identifier, $transaction) {
568
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/transaction/{$transaction}", null, null, RestClient::AUTH_HTTP_SIG);
569
        return self::jsonDecode($response->body(), true);
570
    }
571
572
    /**
573
     * removes an address transaction event subscription from a webhook
574
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
575
     * @param  string  $address         the address hash of the event subscription
576
     * @return boolean                  true on success
577
     */
578
    public function unsubscribeAddressTransactions($identifier, $address) {
579
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/address-transactions/{$address}", null, null, RestClient::AUTH_HTTP_SIG);
580
        return self::jsonDecode($response->body(), true);
581
    }
582
583
    /**
584
     * removes a block event subscription from a webhook
585
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
586
     * @return boolean                  true on success
587
     */
588
    public function unsubscribeNewBlocks($identifier) {
589
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/block", null, null, RestClient::AUTH_HTTP_SIG);
590
        return self::jsonDecode($response->body(), true);
591
    }
592
593
    /**
594
     * create a new wallet
595
     *   - will generate a new primary seed (with password) and backup seed (without password)
596
     *   - send the primary seed (BIP39 'encrypted') and backup public key to the server
597
     *   - receive the blocktrail co-signing public key from the server
598
     *
599
     * Either takes one argument:
600
     * @param array $options
601
     *
602
     * Or takes three arguments (old, deprecated syntax):
603
     * (@nonPHP-doc) @param      $identifier
604
     * (@nonPHP-doc) @param      $password
605
     * (@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...
606
     *
607
     * @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...
608
     * @throws \Exception
609
     */
610
    public function createNewWallet($options) {
611
        if (!is_array($options)) {
612
            $args = func_get_args();
613
            $options = [
614
                "identifier" => $args[0],
615
                "password" => $args[1],
616
                "key_index" => isset($args[2]) ? $args[2] : null,
617
            ];
618
        }
619
620
        if (isset($options['password'])) {
621
            if (isset($options['passphrase'])) {
622
                throw new \InvalidArgumentException("Can only provide either passphrase or password");
623
            } else {
624
                $options['passphrase'] = $options['password'];
625
            }
626
        }
627
628
        if (!isset($options['passphrase'])) {
629
            $options['passphrase'] = null;
630
        }
631
632
        if (!isset($options['key_index'])) {
633
            $options['key_index'] = 0;
634
        }
635
636
        if (!isset($options['wallet_version'])) {
637
            $options['wallet_version'] = Wallet::WALLET_VERSION_V3;
638
        }
639
640
        switch ($options['wallet_version']) {
641
            case Wallet::WALLET_VERSION_V1:
642
                return $this->createNewWalletV1($options);
643
644
            case Wallet::WALLET_VERSION_V2:
645
                return $this->createNewWalletV2($options);
646
647
            case Wallet::WALLET_VERSION_V3:
648
                return $this->createNewWalletV3($options);
649
650
            default:
651
                throw new \InvalidArgumentException("Invalid wallet version");
652
        }
653
    }
654
655
    protected function createNewWalletV1($options) {
656
        $walletPath = WalletPath::create($options['key_index']);
657
658
        $storePrimaryMnemonic = isset($options['store_primary_mnemonic']) ? $options['store_primary_mnemonic'] : null;
659
660
        if (isset($options['primary_mnemonic']) && isset($options['primary_private_key'])) {
661
            throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey");
662
        }
663
664
        $primaryMnemonic = null;
665
        $primaryPrivateKey = null;
666
        if (!isset($options['primary_mnemonic']) && !isset($options['primary_private_key'])) {
667
            if (!$options['passphrase']) {
668
                throw new \InvalidArgumentException("Can't generate Primary Mnemonic without a passphrase");
669
            } else {
670
                // create new primary seed
671
                /** @var HierarchicalKey $primaryPrivateKey */
672
                list($primaryMnemonic, , $primaryPrivateKey) = $this->newPrimarySeed($options['passphrase']);
673
                if ($storePrimaryMnemonic !== false) {
674
                    $storePrimaryMnemonic = true;
675
                }
676
            }
677
        } elseif (isset($options['primary_mnemonic'])) {
678
            $primaryMnemonic = $options['primary_mnemonic'];
679
        } elseif (isset($options['primary_private_key'])) {
680
            $primaryPrivateKey = $options['primary_private_key'];
681
        }
682
683
        if ($storePrimaryMnemonic && $primaryMnemonic && !$options['passphrase']) {
684
            throw new \InvalidArgumentException("Can't store Primary Mnemonic on server without a passphrase");
685
        }
686
687
        if ($primaryPrivateKey) {
688
            if (is_string($primaryPrivateKey)) {
689
                $primaryPrivateKey = [$primaryPrivateKey, "m"];
690
            }
691
        } else {
692
            $primaryPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($primaryMnemonic, $options['passphrase']));
693
        }
694
695
        if (!$storePrimaryMnemonic) {
696
            $primaryMnemonic = false;
697
        }
698
699
        // create primary public key from the created private key
700
        $path = $walletPath->keyIndexPath()->publicPath();
701
        $primaryPublicKey = BIP32Key::create($primaryPrivateKey, "m")->buildKey($path);
702
703
        if (isset($options['backup_mnemonic']) && $options['backup_public_key']) {
704
            throw new \InvalidArgumentException("Can't specify Backup Mnemonic and Backup PublicKey");
705
        }
706
707
        $backupMnemonic = null;
708
        $backupPublicKey = null;
709
        if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) {
710
            /** @var HierarchicalKey $backupPrivateKey */
711
            list($backupMnemonic, , ) = $this->newBackupSeed();
712
        } else if (isset($options['backup_mnemonic'])) {
713
            $backupMnemonic = $options['backup_mnemonic'];
714
        } elseif (isset($options['backup_public_key'])) {
715
            $backupPublicKey = $options['backup_public_key'];
716
        }
717
718
        if ($backupPublicKey) {
719
            if (is_string($backupPublicKey)) {
720
                $backupPublicKey = [$backupPublicKey, "m"];
721
            }
722
        } else {
723
            $backupPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, ""));
724
            $backupPublicKey = BIP32Key::create($backupPrivateKey->toPublic(), "M");
725
        }
726
727
        // create a checksum of our private key which we'll later use to verify we used the right password
728
        $checksum = $primaryPrivateKey->getPublicKey()->getAddress()->getAddress();
729
        $addressReader = $this->makeAddressReader($options);
730
731
        // send the public keys to the server to store them
732
        //  and the mnemonic, which is safe because it's useless without the password
733
        $data = $this->storeNewWalletV1(
734
            $options['identifier'],
735
            $primaryPublicKey->tuple(),
736
            $backupPublicKey->tuple(),
737
            $primaryMnemonic,
738
            $checksum,
739
            $options['key_index'],
740
            array_key_exists('segwit', $options) ? $options['segwit'] : false,
741
            array_key_exists('require_activation', $options) ? $options['require_activation'] : false
742
        );
743
744
        // received the blocktrail public keys
745
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
746
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
747
        }, $data['blocktrail_public_keys']);
748
749
        $wallet = new WalletV1(
750
            $this,
751
            $options['identifier'],
752
            $primaryMnemonic,
753
            [$options['key_index'] => $primaryPublicKey],
754
            $backupPublicKey,
755
            $blocktrailPublicKeys,
756
            $options['key_index'],
757
            $this->network,
758
            $this->testnet,
759
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
760
            $addressReader,
761
            $checksum
762
        );
763
764
        $wallet->unlock($options);
765
766
        // return wallet and backup mnemonic
767
        return [
768
            $wallet,
769
            [
770
                'primary_mnemonic' => $primaryMnemonic,
771
                'backup_mnemonic' => $backupMnemonic,
772
                'blocktrail_public_keys' => $blocktrailPublicKeys,
773
            ],
774
        ];
775
    }
776
777
    public static function randomBits($bits) {
778
        return self::randomBytes($bits / 8);
779
    }
780
781
    public static function randomBytes($bytes) {
782
        return (new Random())->bytes($bytes)->getBinary();
783
    }
784
785
    protected function createNewWalletV2($options) {
786
        $walletPath = WalletPath::create($options['key_index']);
787
788
        if (isset($options['store_primary_mnemonic'])) {
789
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
790
        }
791
792
        if (!isset($options['store_data_on_server'])) {
793
            if (isset($options['primary_private_key'])) {
794
                $options['store_data_on_server'] = false;
795
            } else {
796
                $options['store_data_on_server'] = true;
797
            }
798
        }
799
800
        $storeDataOnServer = $options['store_data_on_server'];
801
802
        $secret = null;
803
        $encryptedSecret = null;
804
        $primarySeed = null;
805
        $encryptedPrimarySeed = null;
806
        $recoverySecret = null;
807
        $recoveryEncryptedSecret = null;
808
        $backupSeed = null;
809
810
        if (!isset($options['primary_private_key'])) {
811
            $primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : self::randomBits(256);
812
        }
813
814
        if ($storeDataOnServer) {
815
            if (!isset($options['secret'])) {
816
                if (!$options['passphrase']) {
817
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
818
                }
819
820
                $secret = bin2hex(self::randomBits(256)); // string because we use it as passphrase
821
                $encryptedSecret = CryptoJSAES::encrypt($secret, $options['passphrase']);
822
            } else {
823
                $secret = $options['secret'];
824
            }
825
826
            $encryptedPrimarySeed = CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
827
            $recoverySecret = bin2hex(self::randomBits(256));
828
829
            $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);
830
        }
831
832
        if (!isset($options['backup_public_key'])) {
833
            $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : self::randomBits(256);
834
        }
835
836
        if (isset($options['primary_private_key'])) {
837
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
838
        } else {
839
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m");
840
        }
841
842
        // create primary public key from the created private key
843
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
844
845
        if (!isset($options['backup_public_key'])) {
846
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M");
847
        }
848
849
        // create a checksum of our private key which we'll later use to verify we used the right password
850
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
851
        $addressReader = $this->makeAddressReader($options);
852
853
        // send the public keys and encrypted data to server
854
        $data = $this->storeNewWalletV2(
855
            $options['identifier'],
856
            $options['primary_public_key']->tuple(),
857
            $options['backup_public_key']->tuple(),
858
            $storeDataOnServer ? $encryptedPrimarySeed : false,
859
            $storeDataOnServer ? $encryptedSecret : false,
860
            $storeDataOnServer ? $recoverySecret : false,
861
            $checksum,
862
            $options['key_index'],
863
            array_key_exists('segwit', $options) ? $options['segwit'] : false,
864
            array_key_exists('require_activation', $options) ? $options['require_activation'] : false
865
        );
866
867
        // received the blocktrail public keys
868
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
869
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
870
        }, $data['blocktrail_public_keys']);
871
872
        $wallet = new WalletV2(
873
            $this,
874
            $options['identifier'],
875
            $encryptedPrimarySeed,
876
            $encryptedSecret,
877
            [$options['key_index'] => $options['primary_public_key']],
878
            $options['backup_public_key'],
879
            $blocktrailPublicKeys,
880
            $options['key_index'],
881
            $this->network,
882
            $this->testnet,
883
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
884
            $addressReader,
885
            $checksum
886
        );
887
888
        $wallet->unlock([
889
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
890
            'primary_private_key' => $options['primary_private_key'],
891
            'primary_seed' => $primarySeed,
892
            'secret' => $secret,
893
        ]);
894
895
        // return wallet and mnemonics for backup sheet
896
        return [
897
            $wallet,
898
            [
899
                'encrypted_primary_seed' => $encryptedPrimarySeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedPrimarySeed))) : null,
900
                'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer($backupSeed)) : null,
901
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($recoveryEncryptedSecret))) : null,
902
                'encrypted_secret' => $encryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedSecret))) : null,
903
                'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
904
                    return [$keyIndex, $pubKey->tuple()];
905
                }, $blocktrailPublicKeys),
906
            ],
907
        ];
908
    }
909
910
    protected function createNewWalletV3($options) {
911
        $walletPath = WalletPath::create($options['key_index']);
912
913
        if (isset($options['store_primary_mnemonic'])) {
914
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
915
        }
916
917
        if (!isset($options['store_data_on_server'])) {
918
            if (isset($options['primary_private_key'])) {
919
                $options['store_data_on_server'] = false;
920
            } else {
921
                $options['store_data_on_server'] = true;
922
            }
923
        }
924
925
        $storeDataOnServer = $options['store_data_on_server'];
926
927
        $secret = null;
928
        $encryptedSecret = null;
929
        $primarySeed = null;
930
        $encryptedPrimarySeed = null;
931
        $recoverySecret = null;
932
        $recoveryEncryptedSecret = null;
933
        $backupSeed = null;
934
935
        if (!isset($options['primary_private_key'])) {
936
            if (isset($options['primary_seed'])) {
937
                if (!$options['primary_seed'] instanceof BufferInterface) {
938
                    throw new \InvalidArgumentException('Primary Seed should be passed as a Buffer');
939
                }
940
                $primarySeed = $options['primary_seed'];
941
            } else {
942
                $primarySeed = new Buffer(self::randomBits(256));
943
            }
944
        }
945
946
        if ($storeDataOnServer) {
947
            if (!isset($options['secret'])) {
948
                if (!$options['passphrase']) {
949
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
950
                }
951
952
                $secret = new Buffer(self::randomBits(256));
953
                $encryptedSecret = Encryption::encrypt($secret, new Buffer($options['passphrase']), KeyDerivation::DEFAULT_ITERATIONS)
954
                    ->getBuffer();
955
            } else {
956
                if (!$options['secret'] instanceof Buffer) {
957
                    throw new \RuntimeException('Secret must be provided as a Buffer');
958
                }
959
960
                $secret = $options['secret'];
961
            }
962
963
            $encryptedPrimarySeed = Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS)
964
                ->getBuffer();
965
            $recoverySecret = new Buffer(self::randomBits(256));
966
967
            $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS)
968
                ->getBuffer();
969
        }
970
971
        if (!isset($options['backup_public_key'])) {
972
            if (isset($options['backup_seed'])) {
973
                if (!$options['backup_seed'] instanceof Buffer) {
974
                    throw new \RuntimeException('Backup seed must be an instance of Buffer');
975
                }
976
                $backupSeed = $options['backup_seed'];
977
            } else {
978
                $backupSeed = new Buffer(self::randomBits(256));
979
            }
980
        }
981
982
        if (isset($options['primary_private_key'])) {
983
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

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