Completed
Pull Request — master (#110)
by Ruben de
72:04
created

BlocktrailSDK::verifyAddress()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

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

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

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

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

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

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1810
        $signedMessage = new SignedMessage($message, $csSerializer->parse(new Buffer(base64_decode($signature))));
1811
1812 1
        $signer = new MessageSigner($adapter);
1813
        return $signer->verify($signedMessage, $addr);
1814
    }
1815
1816
    /**
1817
     * Take a base58 or cashaddress, and return only
1818
     * the cash address.
1819
     * This function only works on bitcoin cash.
1820
     * @param string $input
1821
     * @return string
1822
     * @throws BlocktrailSDKException
1823
     */
1824
    public function getLegacyBitcoinCashAddress($input) {
1825
        if ($this->network === "bitcoincash") {
1826
            $address = $this
1827
                ->makeAddressReader([
1828
                    "use_cashaddress" => true
1829
                ])
1830
                ->fromString($input);
1831
1832
            if ($address instanceof CashAddress) {
1833
                $address = $address->getLegacyAddress();
1834
            }
1835
1836
            return $address->getAddress();
1837
        }
1838
1839
        throw new BlocktrailSDKException("Only request a legacy address when using bitcoin cash");
1840
    }
1841
1842
    /**
1843
     * convert a Satoshi value to a BTC value
1844 12
     *
1845 12
     * @param int       $satoshi
1846
     * @return float
1847
     */
1848
    public static function toBTC($satoshi) {
1849
        return bcdiv((int)(string)$satoshi, 100000000, 8);
1850
    }
1851
1852
    /**
1853
     * convert a Satoshi value to a BTC value and return it as a string
1854 12
1855 12
     * @param int       $satoshi
1856
     * @return string
1857
     */
1858
    public static function toBTCString($satoshi) {
1859
        return sprintf("%.8f", self::toBTC($satoshi));
1860
    }
1861
1862
    /**
1863
     * convert a BTC value to a Satoshi value
1864
     *
1865
     * @param float     $btc
1866 32
     * @return string
1867 32
     */
1868
    public static function toSatoshiString($btc) {
1869
        return bcmul(sprintf("%.8f", (float)$btc), 100000000, 0);
1870
    }
1871 32
1872
    /**
1873 32
     * convert a BTC value to a Satoshi value
1874
     *
1875
     * @param float     $btc
1876
     * @return string
1877 32
     */
1878
    public static function toSatoshi($btc) {
1879
        return (int)self::toSatoshiString($btc);
1880
    }
1881
1882
    /**
1883
     * json_decode helper that throws exceptions when it fails to decode
1884
     *
1885
     * @param      $json
1886 18
     * @param bool $assoc
1887 18
     * @return mixed
1888
     * @throws \Exception
1889 18
     */
1890 18
    public static function jsonDecode($json, $assoc = false) {
1891 18
        if (!$json) {
1892 18
            throw new \Exception("Can't json_decode empty string [{$json}]");
1893
        }
1894 18
1895
        $data = json_decode($json, $assoc);
1896
1897
        if ($data === null) {
1898
            throw new \Exception("Failed to json_decode [{$json}]");
1899
        }
1900
1901
        return $data;
1902
    }
1903
1904
    /**
1905
     * sort public keys for multisig script
1906
     *
1907
     * @param PublicKeyInterface[] $pubKeys
1908
     * @return PublicKeyInterface[]
1909
     */
1910
    public static function sortMultisigKeys(array $pubKeys) {
1911
        $result = array_values($pubKeys);
1912
        usort($result, function (PublicKeyInterface $a, PublicKeyInterface $b) {
1913
            $av = $a->getHex();
1914 26
            $bv = $b->getHex();
1915 26
            return $av == $bv ? 0 : $av > $bv ? 1 : -1;
1916 26
        });
1917
1918
        return $result;
1919
    }
1920
1921
    /**
1922
     * read and decode the json payload from a webhook's POST request.
1923
     *
1924 26
     * @param bool $returnObject    flag to indicate if an object or associative array should be returned
1925 26
     * @return mixed|null
1926 10
     * @throws \Exception
1927
     */
1928
    public static function getWebhookPayload($returnObject = false) {
1929 26
        $data = file_get_contents("php://input");
1930 26
        if ($data) {
1931 26
            return self::jsonDecode($data, !$returnObject);
1932
        } else {
1933 26
            return null;
1934 26
        }
1935
    }
1936
1937 26
    public static function normalizeBIP32KeyArray($keys) {
1938
        return Util::arrayMapWithIndex(function ($idx, $key) {
1939
            return [$idx, self::normalizeBIP32Key($key)];
1940
        }, $keys);
1941
    }
1942
1943
    /**
1944
     * @param array|BIP32Key $key
1945
     * @return BIP32Key
1946
     * @throws \Exception
1947
     */
1948
    public static function normalizeBIP32Key($key) {
1949
        if ($key instanceof BIP32Key) {
1950
            return $key;
1951
        }
1952
1953
        if (is_array($key) && count($key) === 2) {
1954
            $path = $key[1];
1955
            $hk = $key[0];
1956
1957
            if (!($hk instanceof HierarchicalKey)) {
1958
                $hk = HierarchicalKeyFactory::fromExtended($hk);
1959
            }
1960
1961
            return BIP32Key::create($hk, $path);
1962
        } else {
1963
            throw new \Exception("Bad Input");
1964
        }
1965
    }
1966
}
1967