Completed
Pull Request — master (#110)
by Ruben de
75:02 queued 05:01
created

BlocktrailSDK::storeNewWalletV3()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 15
nc 1
nop 9
dl 0
loc 19
rs 9.4285
c 0
b 0
f 0
ccs 7
cts 7
cp 1
crap 1

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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