Completed
Branch master (b7c8d8)
by
unknown
28:56 queued 12:06
created

BlocktrailSDK::setBitcoinLibMagicBytes()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 7.9295

Importance

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