Completed
Pull Request — master (#110)
by Ruben de
22:48 queued 04:26
created

BlocktrailSDK::toSatoshiString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\PayToPubKeyHashAddress;
6
use BitWasp\Bitcoin\Bitcoin;
7
use BitWasp\Bitcoin\Crypto\EcAdapter\EcSerializer;
8
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
9
use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\CompactSignatureSerializerInterface;
10
use BitWasp\Bitcoin\Crypto\Random\Random;
11
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKey;
12
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
13
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
14
use BitWasp\Bitcoin\MessageSigner\SignedMessage;
15
use BitWasp\Bitcoin\Mnemonic\Bip39\Bip39SeedGenerator;
16
use BitWasp\Bitcoin\Mnemonic\MnemonicFactory;
17
use BitWasp\Bitcoin\Network\NetworkFactory;
18
use BitWasp\Bitcoin\Transaction\TransactionFactory;
19
use BitWasp\Buffertools\Buffer;
20
use BitWasp\Buffertools\BufferInterface;
21
use Blocktrail\CryptoJSAES\CryptoJSAES;
22
use Blocktrail\SDK\Address\AddressReaderBase;
23
use Blocktrail\SDK\Address\BitcoinAddressReader;
24
use Blocktrail\SDK\Address\BitcoinCashAddressReader;
25
use Blocktrail\SDK\Address\CashAddress;
26
use Blocktrail\SDK\Backend\BlocktrailConverter;
27
use Blocktrail\SDK\Backend\BtccomConverter;
28
use Blocktrail\SDK\Backend\ConverterInterface;
29
use Blocktrail\SDK\Bitcoin\BIP32Key;
30
use Blocktrail\SDK\Connection\RestClient;
31
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
32
use Blocktrail\SDK\Network\BitcoinCash;
33
use Blocktrail\SDK\Connection\RestClientInterface;
34
use Blocktrail\SDK\Network\BitcoinCashRegtest;
35
use Blocktrail\SDK\Network\BitcoinCashTestnet;
36
use 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
     * @var bool
66
     */
67
    protected $testnet;
68
69
    /**
70
     * @var ConverterInterface
71
     */
72
    protected $converter;
73
74
    /**
75
     * @param   string      $apiKey         the API_KEY to use for authentication
76
     * @param   string      $apiSecret      the API_SECRET to use for authentication
77
     * @param   string      $network        the cryptocurrency 'network' to consume, eg BTC, LTC, etc
78
     * @param   bool        $testnet        testnet yes/no
79
     * @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 118
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
84
85 118
        list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet);
86
87 118
        if (is_null($apiEndpoint)) {
88 118
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com";
89 118
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
90
        }
91
92 118
        $btccomEndpoint = getenv('BLOCKTRAIL_SDK_BTCCOM_API_ENDPOINT') ?: "https://chain.api.btc.com";
93 118
        $btccomEndpoint = "{$btccomEndpoint}/v3/";
94
95
        // normalize network and set bitcoinlib to the right magic-bytes
96 118
        list($this->network, $this->testnet, $regtest) = $this->normalizeNetwork($network, $testnet);
97 118
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet, $regtest);
98
99 118
        if ($this->testnet && strpos($btccomEndpoint, "tchain") === false) {
100 33
            $btccomEndpoint = \str_replace("chain", "tchain", $btccomEndpoint);
101
        }
102
103 118
        $this->blocktrailClient = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
104 118
        $this->dataClient = new RestClient($btccomEndpoint, $apiVersion, $apiKey, $apiSecret);
105 118
        $this->btccomRawTxClient = new RestClient(($this->testnet ? "t" : "") . "chain.btc.com", $apiVersion, $apiKey, $apiSecret);
106
107 118
        $this->converter = new BtccomConverter();
108 118
    }
109
110
    /**
111
     * normalize network string
112
     *
113
     * @param $network
114
     * @param $testnet
115
     * @return array
116
     * @throws \Exception
117
     */
118 118
    protected function normalizeNetwork($network, $testnet) {
119
        // [name, testnet, network]
120 118
        return Util::normalizeNetwork($network, $testnet);
121
    }
122
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 118
    protected function setBitcoinLibMagicBytes($network, $testnet, $regtest) {
131
132 118
        if ($network === "bitcoin") {
133 118
            if ($regtest) {
134
                $useNetwork = NetworkFactory::bitcoinRegtest();
135 118
            } else if ($testnet) {
136 29
                $useNetwork = NetworkFactory::bitcoinTestnet();
137
            } else {
138 118
                $useNetwork = NetworkFactory::bitcoin();
139
            }
140 4
        } else if ($network === "bitcoincash") {
141 4
            if ($regtest) {
142
                $useNetwork = new BitcoinCashRegtest();
143 4
            } else if ($testnet) {
144 4
                $useNetwork = new BitcoinCashTestnet();
145
            } else {
146
                $useNetwork = new BitcoinCash();
147
            }
148
        }
149
150 118
        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 118
    }
152
153
    /**
154
     * enable CURL debugging output
155
     *
156
     * @param   bool        $debug
157
     *
158
     * @codeCoverageIgnore
159
     */
160
    public function setCurlDebugging($debug = true) {
161
        $this->blocktrailClient->setCurlDebugging($debug);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Blocktrail\SDK\Connection\RestClientInterface as the method setCurlDebugging() does only exist in the following implementations of said interface: Blocktrail\SDK\Connection\RestClient.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
162
        $this->dataClient->setCurlDebugging($debug);
163
    }
164
165
    /**
166
     * enable verbose errors
167
     *
168
     * @param   bool        $verboseErrors
169
     *
170
     * @codeCoverageIgnore
171
     */
172
    public function setVerboseErrors($verboseErrors = true) {
173
        $this->blocktrailClient->setVerboseErrors($verboseErrors);
174
        $this->dataClient->setVerboseErrors($verboseErrors);
175
    }
176
    
177
    /**
178
     * set cURL default option on Guzzle client
179
     * @param string    $key
180
     * @param bool      $value
181
     *
182
     * @codeCoverageIgnore
183
     */
184
    public function setCurlDefaultOption($key, $value) {
185
        $this->blocktrailClient->setCurlDefaultOption($key, $value);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Blocktrail\SDK\Connection\RestClientInterface as the method setCurlDefaultOption() does only exist in the following implementations of said interface: Blocktrail\SDK\Connection\RestClient.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
186
        $this->dataClient->setCurlDefaultOption($key, $value);
187
    }
188
189
    /**
190
     * @return  RestClientInterface
191
     */
192 2
    public function getRestClient() {
193 2
        return $this->blocktrailClient;
194
    }
195
196
    /**
197
     * @return  RestClient
198
     */
199
    public function getDataRestClient() {
200
        return $this->dataClient;
201
    }
202
203
    /**
204
     * @param RestClientInterface $restClient
205
     */
206
    public function setRestClient(RestClientInterface $restClient) {
207
        $this->client = $restClient;
0 ignored issues
show
Bug introduced by
The property client does not seem to exist. Did you mean blocktrailClient?

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

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

Loading history...
208
    }
209
210
    /**
211
     * get a single address
212
     * @param  string $address address hash
213
     * @return array           associative array containing the response
214
     */
215 1
    public function address($address) {
216 1
        $response = $this->dataClient->get($this->converter->getUrlForAddress($address));
217 1
        return $this->converter->convertAddress($response->body());
218
    }
219
220
    /**
221
     * get all transactions for an address (paginated)
222
     * @param  string  $address address hash
223
     * @param  integer $page    pagination: page number
224
     * @param  integer $limit   pagination: records per page (max 500)
225
     * @param  string  $sortDir pagination: sort direction (asc|desc)
226
     * @return array            associative array containing the response
227
     */
228 1
    public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
229
        $queryString = [
230 1
            'page' => $page,
231 1
            'limit' => $limit,
232 1
            'sort_dir' => $sortDir
233
        ];
234 1
        $response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
235 1
        return $this->converter->convertAddressTxs($response->body());
236
    }
237
238
    /**
239
     * get all unconfirmed transactions for an address (paginated)
240
     * @param  string  $address address hash
241
     * @param  integer $page    pagination: page number
242
     * @param  integer $limit   pagination: records per page (max 500)
243
     * @param  string  $sortDir pagination: sort direction (asc|desc)
244
     * @return array            associative array containing the response
245
     */
246
    public function addressUnconfirmedTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
247
        $queryString = [
248
            'page' => $page,
249
            'limit' => $limit,
250
            'sort_dir' => $sortDir
251
        ];
252
        $response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
253
        return $this->converter->convertAddressTxs($response->body());
254
    }
255
256
    /**
257
     * get all unspent outputs for an address (paginated)
258
     * @param  string  $address address hash
259
     * @param  integer $page    pagination: page number
260
     * @param  integer $limit   pagination: records per page (max 500)
261
     * @param  string  $sortDir pagination: sort direction (asc|desc)
262
     * @return array            associative array containing the response
263
     */
264 1
    public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') {
265
        $queryString = [
266 1
            'page' => $page,
267 1
            'limit' => $limit,
268 1
            'sort_dir' => $sortDir
269
        ];
270 1
        $response = $this->dataClient->get($this->converter->getUrlForAddressUnspent($address), $this->converter->paginationParams($queryString));
271 1
        return $this->converter->convertAddressUnspentOutputs($response->body(), $address);
272
    }
273
274
    /**
275
     * get all unspent outputs for a batch of addresses (paginated)
276
     *
277
     * @param  string[] $addresses
278
     * @param  integer  $page    pagination: page number
279
     * @param  integer  $limit   pagination: records per page (max 500)
280
     * @param  string   $sortDir pagination: sort direction (asc|desc)
281
     * @return array associative array containing the response
282
     * @throws \Exception
283
     */
284
    public function batchAddressUnspentOutputs($addresses, $page = 1, $limit = 20, $sortDir = 'asc') {
285
        throw new \Exception("deprecated");
286
    }
287
288
    /**
289
     * verify ownership of an address
290
     * @param  string  $address     address hash
291
     * @param  string  $signature   a signed message (the address hash) using the private key of the address
292
     * @return array                associative array containing the response
293
     */
294 1
    public function verifyAddress($address, $signature) {
295 1
        if ($this->verifyMessage($address, $address, $signature)) {
296 1
            return ['result' => true, 'msg' => 'Successfully verified'];
297
        } else {
298
            return ['result' => false];
299
        }
300
    }
301
302
    /**
303
     * get all blocks (paginated)
304
     * @param  integer $page    pagination: page number
305
     * @param  integer $limit   pagination: records per page
306
     * @param  string  $sortDir pagination: sort direction (asc|desc)
307
     * @return array            associative array containing the response
308
     */
309 1
    public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') {
310
        $queryString = [
311 1
            'page' => $page,
312 1
            'limit' => $limit,
313 1
            'sort_dir' => $sortDir
314
        ];
315 1
        $response = $this->dataClient->get($this->converter->getUrlForAllBlocks(), $this->converter->paginationParams($queryString));
316 1
        return $this->converter->convertBlocks($response->body());
317
    }
318
319
    /**
320
     * get the latest block
321
     * @return array            associative array containing the response
322
     */
323 1
    public function blockLatest() {
324 1
        $response = $this->dataClient->get($this->converter->getUrlForBlock("latest"));
325 1
        return $this->converter->convertBlock($response->body());
326
    }
327
328
    /**
329
     * get an individual block
330
     * @param  string|integer $block    a block hash or a block height
331
     * @return array                    associative array containing the response
332
     */
333 1
    public function block($block) {
334 1
        $response = $this->dataClient->get($this->converter->getUrlForBlock($block));
335 1
        return $this->converter->convertBlock($response->body());
336
    }
337
338
    /**
339
     * get all transaction in a block (paginated)
340
     * @param  string|integer   $block   a block hash or a block height
341
     * @param  integer          $page    pagination: page number
342
     * @param  integer          $limit   pagination: records per page
343
     * @param  string           $sortDir pagination: sort direction (asc|desc)
344
     * @return array                     associative array containing the response
345
     */
346
    public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') {
347
        $queryString = [
348
            'page' => $page,
349
            'limit' => $limit,
350
            'sort_dir' => $sortDir
351
        ];
352
        $response = $this->dataClient->get($this->converter->getUrlForBlockTransaction($block), $this->converter->paginationParams($queryString));
353
        return $this->converter->convertBlockTxs($response->body());
354
    }
355
356
    /**
357
     * get a single transaction
358
     * @param  string $txhash transaction hash
359
     * @return array          associative array containing the response
360
     */
361 5
    public function transaction($txhash) {
362 5
        $response = $this->dataClient->get($this->converter->getUrlForTransaction($txhash));
363 5
        $res = $this->converter->convertTx($response->body(), null);
364
365 5
        if ($this->converter instanceof BtccomConverter) {
366 5
            $res['raw'] = $this->btccomRawTxClient->get("/{$txhash}.rawhex")->body();
367
        }
368
369 4
        return $res;
370
    }
371
372
    /**
373
     * get a single transaction
374
     * @param  string[] $txhashes list of transaction hashes (up to 20)
375
     * @return array[]            array containing the response
376
     */
377
    public function transactions($txhashes) {
378
        $response = $this->dataClient->get($this->converter->getUrlForTransactions($txhashes));
379
        return $this->converter->convertTxs($response->body());
380
    }
381
    
382
    /**
383
     * get a paginated list of all webhooks associated with the api user
384
     * @param  integer          $page    pagination: page number
385
     * @param  integer          $limit   pagination: records per page
386
     * @return array                     associative array containing the response
387
     */
388 1
    public function allWebhooks($page = 1, $limit = 20) {
389
        $queryString = [
390 1
            'page' => $page,
391 1
            'limit' => $limit
392
        ];
393 1
        $response = $this->blocktrailClient->get("webhooks", $this->converter->paginationParams($queryString));
394 1
        return self::jsonDecode($response->body(), true);
395
    }
396
397
    /**
398
     * get an existing webhook by it's identifier
399
     * @param string    $identifier     a unique identifier associated with the webhook
400
     * @return array                    associative array containing the response
401
     */
402 1
    public function getWebhook($identifier) {
403 1
        $response = $this->blocktrailClient->get("webhook/".$identifier);
404 1
        return self::jsonDecode($response->body(), true);
405
    }
406
407
    /**
408
     * create a new webhook
409
     * @param  string  $url        the url to receive the webhook events
410
     * @param  string  $identifier a unique identifier to associate with this webhook
411
     * @return array               associative array containing the response
412
     */
413 1
    public function setupWebhook($url, $identifier = null) {
414
        $postData = [
415 1
            'url'        => $url,
416 1
            'identifier' => $identifier
417
        ];
418 1
        $response = $this->blocktrailClient->post("webhook", null, $postData, RestClient::AUTH_HTTP_SIG);
419 1
        return self::jsonDecode($response->body(), true);
420
    }
421
422
    /**
423
     * update an existing webhook
424
     * @param  string  $identifier      the unique identifier of the webhook to update
425
     * @param  string  $newUrl          the new url to receive the webhook events
426
     * @param  string  $newIdentifier   a new unique identifier to associate with this webhook
427
     * @return array                    associative array containing the response
428
     */
429 1
    public function updateWebhook($identifier, $newUrl = null, $newIdentifier = null) {
430
        $putData = [
431 1
            'url'        => $newUrl,
432 1
            'identifier' => $newIdentifier
433
        ];
434 1
        $response = $this->blocktrailClient->put("webhook/{$identifier}", null, $putData, RestClient::AUTH_HTTP_SIG);
435 1
        return self::jsonDecode($response->body(), true);
436
    }
437
438
    /**
439
     * deletes an existing webhook and any event subscriptions associated with it
440
     * @param  string  $identifier      the unique identifier of the webhook to delete
441
     * @return boolean                  true on success
442
     */
443 1
    public function deleteWebhook($identifier) {
444 1
        $response = $this->blocktrailClient->delete("webhook/{$identifier}", null, null, RestClient::AUTH_HTTP_SIG);
445 1
        return self::jsonDecode($response->body(), true);
446
    }
447
448
    /**
449
     * get a paginated list of all the events a webhook is subscribed to
450
     * @param  string  $identifier  the unique identifier of the webhook
451
     * @param  integer $page        pagination: page number
452
     * @param  integer $limit       pagination: records per page
453
     * @return array                associative array containing the response
454
     */
455 2
    public function getWebhookEvents($identifier, $page = 1, $limit = 20) {
456
        $queryString = [
457 2
            'page' => $page,
458 2
            'limit' => $limit
459
        ];
460 2
        $response = $this->blocktrailClient->get("webhook/{$identifier}/events", $this->converter->paginationParams($queryString));
461 2
        return self::jsonDecode($response->body(), true);
462
    }
463
    
464
    /**
465
     * subscribes a webhook to transaction events of one particular transaction
466
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
467
     * @param  string  $transaction     the transaction hash
468
     * @param  integer $confirmations   the amount of confirmations to send.
469
     * @return array                    associative array containing the response
470
     */
471 1
    public function subscribeTransaction($identifier, $transaction, $confirmations = 6) {
472
        $postData = [
473 1
            'event_type'    => 'transaction',
474 1
            'transaction'   => $transaction,
475 1
            'confirmations' => $confirmations,
476
        ];
477 1
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
478 1
        return self::jsonDecode($response->body(), true);
479
    }
480
481
    /**
482
     * subscribes a webhook to transaction events on a particular address
483
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
484
     * @param  string  $address         the address hash
485
     * @param  integer $confirmations   the amount of confirmations to send.
486
     * @return array                    associative array containing the response
487
     */
488 1
    public function subscribeAddressTransactions($identifier, $address, $confirmations = 6) {
489
        $postData = [
490 1
            'event_type'    => 'address-transactions',
491 1
            'address'       => $address,
492 1
            'confirmations' => $confirmations,
493
        ];
494 1
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
495 1
        return self::jsonDecode($response->body(), true);
496
    }
497
498
    /**
499
     * batch subscribes a webhook to multiple transaction events
500
     *
501
     * @param  string $identifier   the unique identifier of the webhook
502
     * @param  array  $batchData    A 2D array of event data:
503
     *                              [address => $address, confirmations => $confirmations]
504
     *                              where $address is the address to subscibe to
505
     *                              and optionally $confirmations is the amount of confirmations
506
     * @return boolean              true on success
507
     */
508 1
    public function batchSubscribeAddressTransactions($identifier, $batchData) {
509 1
        $postData = [];
510 1
        foreach ($batchData as $record) {
511 1
            $postData[] = [
512 1
                'event_type' => 'address-transactions',
513 1
                'address' => $record['address'],
514 1
                'confirmations' => isset($record['confirmations']) ? $record['confirmations'] : 6,
515
            ];
516
        }
517 1
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events/batch", null, $postData, RestClient::AUTH_HTTP_SIG);
518 1
        return self::jsonDecode($response->body(), true);
519
    }
520
521
    /**
522
     * subscribes a webhook to a new block event
523
     * @param  string  $identifier  the unique identifier of the webhook to be triggered
524
     * @return array                associative array containing the response
525
     */
526 1
    public function subscribeNewBlocks($identifier) {
527
        $postData = [
528 1
            'event_type'    => 'block',
529
        ];
530 1
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
531 1
        return self::jsonDecode($response->body(), true);
532
    }
533
534
    /**
535
     * removes an transaction event subscription from a webhook
536
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
537
     * @param  string  $transaction     the transaction hash of the event subscription
538
     * @return boolean                  true on success
539
     */
540 1
    public function unsubscribeTransaction($identifier, $transaction) {
541 1
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/transaction/{$transaction}", null, null, RestClient::AUTH_HTTP_SIG);
542 1
        return self::jsonDecode($response->body(), true);
543
    }
544
545
    /**
546
     * removes an address transaction event subscription from a webhook
547
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
548
     * @param  string  $address         the address hash of the event subscription
549
     * @return boolean                  true on success
550
     */
551 1
    public function unsubscribeAddressTransactions($identifier, $address) {
552 1
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/address-transactions/{$address}", null, null, RestClient::AUTH_HTTP_SIG);
553 1
        return self::jsonDecode($response->body(), true);
554
    }
555
556
    /**
557
     * removes a block event subscription from a webhook
558
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
559
     * @return boolean                  true on success
560
     */
561 1
    public function unsubscribeNewBlocks($identifier) {
562 1
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/block", null, null, RestClient::AUTH_HTTP_SIG);
563 1
        return self::jsonDecode($response->body(), true);
564
    }
565
566
    /**
567
     * create a new wallet
568
     *   - will generate a new primary seed (with password) and backup seed (without password)
569
     *   - send the primary seed (BIP39 'encrypted') and backup public key to the server
570
     *   - receive the blocktrail co-signing public key from the server
571
     *
572
     * Either takes one argument:
573
     * @param array $options
574
     *
575
     * Or takes three arguments (old, deprecated syntax):
576
     * (@nonPHP-doc) @param      $identifier
577
     * (@nonPHP-doc) @param      $password
578
     * (@nonPHP-doc) @param int  $keyIndex          override for the blocktrail cosigning key to use
0 ignored issues
show
Bug introduced by
There is no parameter named $keyIndex. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
579
     *
580
     * @return array[WalletInterface, array]      list($wallet, $backupInfo)
0 ignored issues
show
Documentation introduced by
The doc-type array[WalletInterface, could not be parsed: Expected "]" at position 2, but found "WalletInterface". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
581
     * @throws \Exception
582
     */
583 7
    public function createNewWallet($options) {
584 7
        if (!is_array($options)) {
585 1
            $args = func_get_args();
586
            $options = [
587 1
                "identifier" => $args[0],
588 1
                "password" => $args[1],
589 1
                "key_index" => isset($args[2]) ? $args[2] : null,
590
            ];
591
        }
592
593 7
        if (isset($options['password'])) {
594 1
            if (isset($options['passphrase'])) {
595
                throw new \InvalidArgumentException("Can only provide either passphrase or password");
596
            } else {
597 1
                $options['passphrase'] = $options['password'];
598
            }
599
        }
600
601 7
        if (!isset($options['passphrase'])) {
602 1
            $options['passphrase'] = null;
603
        }
604
605 7
        if (!isset($options['key_index'])) {
606
            $options['key_index'] = 0;
607
        }
608
609 7
        if (!isset($options['wallet_version'])) {
610 3
            $options['wallet_version'] = Wallet::WALLET_VERSION_V3;
611
        }
612
613 7
        switch ($options['wallet_version']) {
614 7
            case Wallet::WALLET_VERSION_V1:
615 1
                return $this->createNewWalletV1($options);
616
617 6
            case Wallet::WALLET_VERSION_V2:
618 2
                return $this->createNewWalletV2($options);
619
620 4
            case Wallet::WALLET_VERSION_V3:
621 4
                return $this->createNewWalletV3($options);
622
623
            default:
624
                throw new \InvalidArgumentException("Invalid wallet version");
625
        }
626
    }
627
628 1
    protected function createNewWalletV1($options) {
629 1
        $walletPath = WalletPath::create($options['key_index']);
630
631 1
        $storePrimaryMnemonic = isset($options['store_primary_mnemonic']) ? $options['store_primary_mnemonic'] : null;
632
633 1
        if (isset($options['primary_mnemonic']) && isset($options['primary_private_key'])) {
634
            throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey");
635
        }
636
637 1
        $primaryMnemonic = null;
638 1
        $primaryPrivateKey = null;
639 1
        if (!isset($options['primary_mnemonic']) && !isset($options['primary_private_key'])) {
640 1
            if (!$options['passphrase']) {
641
                throw new \InvalidArgumentException("Can't generate Primary Mnemonic without a passphrase");
642
            } else {
643
                // create new primary seed
644
                /** @var HierarchicalKey $primaryPrivateKey */
645 1
                list($primaryMnemonic, , $primaryPrivateKey) = $this->newPrimarySeed($options['passphrase']);
646 1
                if ($storePrimaryMnemonic !== false) {
647 1
                    $storePrimaryMnemonic = true;
648
                }
649
            }
650
        } elseif (isset($options['primary_mnemonic'])) {
651
            $primaryMnemonic = $options['primary_mnemonic'];
652
        } elseif (isset($options['primary_private_key'])) {
653
            $primaryPrivateKey = $options['primary_private_key'];
654
        }
655
656 1
        if ($storePrimaryMnemonic && $primaryMnemonic && !$options['passphrase']) {
657
            throw new \InvalidArgumentException("Can't store Primary Mnemonic on server without a passphrase");
658
        }
659
660 1
        if ($primaryPrivateKey) {
661 1
            if (is_string($primaryPrivateKey)) {
662 1
                $primaryPrivateKey = [$primaryPrivateKey, "m"];
663
            }
664
        } else {
665
            $primaryPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($primaryMnemonic, $options['passphrase']));
666
        }
667
668 1
        if (!$storePrimaryMnemonic) {
669
            $primaryMnemonic = false;
670
        }
671
672
        // create primary public key from the created private key
673 1
        $path = $walletPath->keyIndexPath()->publicPath();
674 1
        $primaryPublicKey = BIP32Key::create($primaryPrivateKey, "m")->buildKey($path);
675
676 1
        if (isset($options['backup_mnemonic']) && $options['backup_public_key']) {
677
            throw new \InvalidArgumentException("Can't specify Backup Mnemonic and Backup PublicKey");
678
        }
679
680 1
        $backupMnemonic = null;
681 1
        $backupPublicKey = null;
682 1
        if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) {
683
            /** @var HierarchicalKey $backupPrivateKey */
684 1
            list($backupMnemonic, , ) = $this->newBackupSeed();
685
        } else if (isset($options['backup_mnemonic'])) {
686
            $backupMnemonic = $options['backup_mnemonic'];
687
        } elseif (isset($options['backup_public_key'])) {
688
            $backupPublicKey = $options['backup_public_key'];
689
        }
690
691 1
        if ($backupPublicKey) {
692
            if (is_string($backupPublicKey)) {
693
                $backupPublicKey = [$backupPublicKey, "m"];
694
            }
695
        } else {
696 1
            $backupPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, ""));
697 1
            $backupPublicKey = BIP32Key::create($backupPrivateKey->toPublic(), "M");
698
        }
699
700
        // create a checksum of our private key which we'll later use to verify we used the right password
701 1
        $checksum = $primaryPrivateKey->getPublicKey()->getAddress()->getAddress();
702 1
        $addressReader = $this->makeAddressReader($options);
703
704
        // send the public keys to the server to store them
705
        //  and the mnemonic, which is safe because it's useless without the password
706 1
        $data = $this->storeNewWalletV1(
707 1
            $options['identifier'],
708 1
            $primaryPublicKey->tuple(),
709 1
            $backupPublicKey->tuple(),
710 1
            $primaryMnemonic,
711 1
            $checksum,
712 1
            $options['key_index'],
713 1
            array_key_exists('segwit', $options) ? $options['segwit'] : false
714
        );
715
716
        // received the blocktrail public keys
717
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
718 1
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
719 1
        }, $data['blocktrail_public_keys']);
720
721 1
        $wallet = new WalletV1(
722 1
            $this,
723 1
            $options['identifier'],
724 1
            $primaryMnemonic,
725 1
            [$options['key_index'] => $primaryPublicKey],
726 1
            $backupPublicKey,
727 1
            $blocktrailPublicKeys,
728 1
            $options['key_index'],
729 1
            $this->network,
730 1
            $this->testnet,
731 1
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
732 1
            $addressReader,
733 1
            $checksum
734
        );
735
736 1
        $wallet->unlock($options);
737
738
        // return wallet and backup mnemonic
739
        return [
740 1
            $wallet,
741
            [
742 1
                'primary_mnemonic' => $primaryMnemonic,
743 1
                'backup_mnemonic' => $backupMnemonic,
744 1
                'blocktrail_public_keys' => $blocktrailPublicKeys,
745
            ],
746
        ];
747
    }
748
749 5
    public static function randomBits($bits) {
750 5
        return self::randomBytes($bits / 8);
751
    }
752
753 5
    public static function randomBytes($bytes) {
754 5
        return (new Random())->bytes($bytes)->getBinary();
755
    }
756
757 2
    protected function createNewWalletV2($options) {
758 2
        $walletPath = WalletPath::create($options['key_index']);
759
760 2
        if (isset($options['store_primary_mnemonic'])) {
761
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
762
        }
763
764 2
        if (!isset($options['store_data_on_server'])) {
765 2
            if (isset($options['primary_private_key'])) {
766 1
                $options['store_data_on_server'] = false;
767
            } else {
768 1
                $options['store_data_on_server'] = true;
769
            }
770
        }
771
772 2
        $storeDataOnServer = $options['store_data_on_server'];
773
774 2
        $secret = null;
775 2
        $encryptedSecret = null;
776 2
        $primarySeed = null;
777 2
        $encryptedPrimarySeed = null;
778 2
        $recoverySecret = null;
779 2
        $recoveryEncryptedSecret = null;
780 2
        $backupSeed = null;
781
782 2
        if (!isset($options['primary_private_key'])) {
783 1
            $primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : self::randomBits(256);
784
        }
785
786 2
        if ($storeDataOnServer) {
787 1
            if (!isset($options['secret'])) {
788 1
                if (!$options['passphrase']) {
789
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
790
                }
791
792 1
                $secret = bin2hex(self::randomBits(256)); // string because we use it as passphrase
793 1
                $encryptedSecret = CryptoJSAES::encrypt($secret, $options['passphrase']);
794
            } else {
795
                $secret = $options['secret'];
796
            }
797
798 1
            $encryptedPrimarySeed = CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
799 1
            $recoverySecret = bin2hex(self::randomBits(256));
800
801 1
            $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);
802
        }
803
804 2
        if (!isset($options['backup_public_key'])) {
805 1
            $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : self::randomBits(256);
806
        }
807
808 2
        if (isset($options['primary_private_key'])) {
809 1
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
810
        } else {
811 1
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m");
812
        }
813
814
        // create primary public key from the created private key
815 2
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
816
817 2
        if (!isset($options['backup_public_key'])) {
818 1
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M");
819
        }
820
821
        // create a checksum of our private key which we'll later use to verify we used the right password
822 2
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
823 2
        $addressReader = $this->makeAddressReader($options);
824
825
        // send the public keys and encrypted data to server
826 2
        $data = $this->storeNewWalletV2(
827 2
            $options['identifier'],
828 2
            $options['primary_public_key']->tuple(),
829 2
            $options['backup_public_key']->tuple(),
830 2
            $storeDataOnServer ? $encryptedPrimarySeed : false,
831 2
            $storeDataOnServer ? $encryptedSecret : false,
832 2
            $storeDataOnServer ? $recoverySecret : false,
833 2
            $checksum,
834 2
            $options['key_index'],
835 2
            array_key_exists('segwit', $options) ? $options['segwit'] : false
836
        );
837
838
        // received the blocktrail public keys
839
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
840 2
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
841 2
        }, $data['blocktrail_public_keys']);
842
843 2
        $wallet = new WalletV2(
844 2
            $this,
845 2
            $options['identifier'],
846 2
            $encryptedPrimarySeed,
847 2
            $encryptedSecret,
848 2
            [$options['key_index'] => $options['primary_public_key']],
849 2
            $options['backup_public_key'],
850 2
            $blocktrailPublicKeys,
851 2
            $options['key_index'],
852 2
            $this->network,
853 2
            $this->testnet,
854 2
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
855 2
            $addressReader,
856 2
            $checksum
857
        );
858
859 2
        $wallet->unlock([
860 2
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
861 2
            'primary_private_key' => $options['primary_private_key'],
862 2
            'primary_seed' => $primarySeed,
863 2
            'secret' => $secret,
864
        ]);
865
866
        // return wallet and mnemonics for backup sheet
867
        return [
868 2
            $wallet,
869
            [
870 2
                'encrypted_primary_seed' => $encryptedPrimarySeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedPrimarySeed))) : null,
871 2
                'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer($backupSeed)) : null,
872 2
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($recoveryEncryptedSecret))) : null,
873 2
                'encrypted_secret' => $encryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedSecret))) : null,
874
                'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
875 2
                    return [$keyIndex, $pubKey->tuple()];
876 2
                }, $blocktrailPublicKeys),
877
            ],
878
        ];
879
    }
880
881 4
    protected function createNewWalletV3($options) {
882 4
        $walletPath = WalletPath::create($options['key_index']);
883
884 4
        if (isset($options['store_primary_mnemonic'])) {
885
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
886
        }
887
888 4
        if (!isset($options['store_data_on_server'])) {
889 4
            if (isset($options['primary_private_key'])) {
890
                $options['store_data_on_server'] = false;
891
            } else {
892 4
                $options['store_data_on_server'] = true;
893
            }
894
        }
895
896 4
        $storeDataOnServer = $options['store_data_on_server'];
897
898 4
        $secret = null;
899 4
        $encryptedSecret = null;
900 4
        $primarySeed = null;
901 4
        $encryptedPrimarySeed = null;
902 4
        $recoverySecret = null;
903 4
        $recoveryEncryptedSecret = null;
904 4
        $backupSeed = null;
905
906 4
        if (!isset($options['primary_private_key'])) {
907 4
            if (isset($options['primary_seed'])) {
908
                if (!$options['primary_seed'] instanceof BufferInterface) {
909
                    throw new \InvalidArgumentException('Primary Seed should be passed as a Buffer');
910
                }
911
                $primarySeed = $options['primary_seed'];
912
            } else {
913 4
                $primarySeed = new Buffer(self::randomBits(256));
914
            }
915
        }
916
917 4
        if ($storeDataOnServer) {
918 4
            if (!isset($options['secret'])) {
919 4
                if (!$options['passphrase']) {
920
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
921
                }
922
923 4
                $secret = new Buffer(self::randomBits(256));
924 4
                $encryptedSecret = Encryption::encrypt($secret, new Buffer($options['passphrase']), KeyDerivation::DEFAULT_ITERATIONS);
925
            } else {
926
                if (!$options['secret'] instanceof Buffer) {
927
                    throw new \RuntimeException('Secret must be provided as a Buffer');
928
                }
929
930
                $secret = $options['secret'];
931
            }
932
933 4
            $encryptedPrimarySeed = Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS);
934 4
            $recoverySecret = new Buffer(self::randomBits(256));
935
936 4
            $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS);
937
        }
938
939 4
        if (!isset($options['backup_public_key'])) {
940 4
            if (isset($options['backup_seed'])) {
941
                if (!$options['backup_seed'] instanceof Buffer) {
942
                    throw new \RuntimeException('Backup seed must be an instance of Buffer');
943
                }
944
                $backupSeed = $options['backup_seed'];
945
            } else {
946 4
                $backupSeed = new Buffer(self::randomBits(256));
947
            }
948
        }
949
950 4
        if (isset($options['primary_private_key'])) {
951
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
952
        } else {
953 4
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m");
954
        }
955
956
        // create primary public key from the created private key
957 4
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
958
959 4
        if (!isset($options['backup_public_key'])) {
960 4
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($backupSeed), "m")->buildKey("M");
961
        }
962
963
        // create a checksum of our private key which we'll later use to verify we used the right password
964 4
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
965 4
        $addressReader = $this->makeAddressReader($options);
966
967
        // send the public keys and encrypted data to server
968 4
        $data = $this->storeNewWalletV3(
969 4
            $options['identifier'],
970 4
            $options['primary_public_key']->tuple(),
971 4
            $options['backup_public_key']->tuple(),
972 4
            $storeDataOnServer ? base64_encode($encryptedPrimarySeed->getBinary()) : false,
973 4
            $storeDataOnServer ? base64_encode($encryptedSecret->getBinary()) : false,
974 4
            $storeDataOnServer ? $recoverySecret->getHex() : false,
975 4
            $checksum,
976 4
            $options['key_index'],
977 4
            array_key_exists('segwit', $options) ? $options['segwit'] : false
978
        );
979
980
        // received the blocktrail public keys
981
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
982 4
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
983 4
        }, $data['blocktrail_public_keys']);
984
985 4
        $wallet = new WalletV3(
986 4
            $this,
987 4
            $options['identifier'],
988 4
            $encryptedPrimarySeed,
0 ignored issues
show
Bug introduced by
It seems like $encryptedPrimarySeed defined by null on line 901 can be null; however, Blocktrail\SDK\WalletV3::__construct() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
990 4
            [$options['key_index'] => $options['primary_public_key']],
991 4
            $options['backup_public_key'],
992 4
            $blocktrailPublicKeys,
993 4
            $options['key_index'],
994 4
            $this->network,
995 4
            $this->testnet,
996 4
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
997 4
            $addressReader,
998 4
            $checksum
999
        );
1000
1001 4
        $wallet->unlock([
1002 4
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
1003 4
            'primary_private_key' => $options['primary_private_key'],
1004 4
            'primary_seed' => $primarySeed,
1005 4
            'secret' => $secret,
1006
        ]);
1007
1008
        // return wallet and mnemonics for backup sheet
1009
        return [
1010 4
            $wallet,
1011
            [
1012 4
                'encrypted_primary_seed'    => $encryptedPrimarySeed ? EncryptionMnemonic::encode($encryptedPrimarySeed) : null,
1013 4
                'backup_seed'               => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic($backupSeed) : null,
1014 4
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? EncryptionMnemonic::encode($recoveryEncryptedSecret) : null,
1015 4
                'encrypted_secret'          => $encryptedSecret ? EncryptionMnemonic::encode($encryptedSecret) : null,
1016
                'blocktrail_public_keys'    => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
1017 4
                    return [$keyIndex, $pubKey->tuple()];
1018 4
                }, $blocktrailPublicKeys),
1019
            ]
1020
        ];
1021
    }
1022
1023
    /**
1024
     * @param array $bip32Key
1025
     * @throws BlocktrailSDKException
1026
     */
1027 10
    private function verifyPublicBIP32Key(array $bip32Key) {
1028 10
        $hk = HierarchicalKeyFactory::fromExtended($bip32Key[0]);
1029 10
        if ($hk->isPrivate()) {
1030
            throw new BlocktrailSDKException('Private key was included in request, abort');
1031
        }
1032
1033 10
        if (substr($bip32Key[1], 0, 1) === "m") {
1034
            throw new BlocktrailSDKException("Private path was included in the request, abort");
1035
        }
1036 10
    }
1037
1038
    /**
1039
     * @param array $walletData
1040
     * @throws BlocktrailSDKException
1041
     */
1042 10
    private function verifyPublicOnly(array $walletData) {
1043 10
        $this->verifyPublicBIP32Key($walletData['primary_public_key']);
1044 10
        $this->verifyPublicBIP32Key($walletData['backup_public_key']);
1045 10
    }
1046
1047
    /**
1048
     * create wallet using the API
1049
     *
1050
     * @param string    $identifier             the wallet identifier to create
1051
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1052
     * @param array     $backupPublicKey        BIP32 extended public key - [backup key, path "M"]
1053
     * @param string    $primaryMnemonic        mnemonic to store
1054
     * @param string    $checksum               checksum to store
1055
     * @param int       $keyIndex               account that we expect to use
1056
     * @param bool      $segwit                 opt in to segwit
1057
     * @return mixed
1058
     */
1059 1
    public function storeNewWalletV1($identifier, $primaryPublicKey, $backupPublicKey, $primaryMnemonic, $checksum, $keyIndex, $segwit = false) {
1060
        $data = [
1061 1
            'identifier' => $identifier,
1062 1
            'primary_public_key' => $primaryPublicKey,
1063 1
            'backup_public_key' => $backupPublicKey,
1064 1
            'primary_mnemonic' => $primaryMnemonic,
1065 1
            'checksum' => $checksum,
1066 1
            'key_index' => $keyIndex,
1067 1
            'segwit' => $segwit,
1068
        ];
1069 1
        $this->verifyPublicOnly($data);
1070 1
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1071 1
        return self::jsonDecode($response->body(), true);
1072
    }
1073
1074
    /**
1075
     * create wallet using the API
1076
     *
1077
     * @param string $identifier       the wallet identifier to create
1078
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1079
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1080
     * @param        $encryptedPrimarySeed
1081
     * @param        $encryptedSecret
1082
     * @param        $recoverySecret
1083
     * @param string $checksum         checksum to store
1084
     * @param int    $keyIndex         account that we expect to use
1085
     * @param bool   $segwit           opt in to segwit
1086
     * @return mixed
1087
     * @throws \Exception
1088
     */
1089 5
    public function storeNewWalletV2($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1090
        $data = [
1091 5
            'identifier' => $identifier,
1092
            'wallet_version' => Wallet::WALLET_VERSION_V2,
1093 5
            'primary_public_key' => $primaryPublicKey,
1094 5
            'backup_public_key' => $backupPublicKey,
1095 5
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1096 5
            'encrypted_secret' => $encryptedSecret,
1097 5
            'recovery_secret' => $recoverySecret,
1098 5
            'checksum' => $checksum,
1099 5
            'key_index' => $keyIndex,
1100 5
            'segwit' => $segwit,
1101
        ];
1102 5
        $this->verifyPublicOnly($data);
1103 5
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1104 5
        return self::jsonDecode($response->body(), true);
1105
    }
1106
1107
    /**
1108
     * create wallet using the API
1109
     *
1110
     * @param string $identifier       the wallet identifier to create
1111
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1112
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1113
     * @param        $encryptedPrimarySeed
1114
     * @param        $encryptedSecret
1115
     * @param        $recoverySecret
1116
     * @param string $checksum         checksum to store
1117
     * @param int    $keyIndex         account that we expect to use
1118
     * @param bool   $segwit           opt in to segwit
1119
     * @return mixed
1120
     * @throws \Exception
1121
     */
1122 4
    public function storeNewWalletV3($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1123
1124
        $data = [
1125 4
            'identifier' => $identifier,
1126
            'wallet_version' => Wallet::WALLET_VERSION_V3,
1127 4
            'primary_public_key' => $primaryPublicKey,
1128 4
            'backup_public_key' => $backupPublicKey,
1129 4
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1130 4
            'encrypted_secret' => $encryptedSecret,
1131 4
            'recovery_secret' => $recoverySecret,
1132 4
            'checksum' => $checksum,
1133 4
            'key_index' => $keyIndex,
1134 4
            'segwit' => $segwit,
1135
        ];
1136
1137 4
        $this->verifyPublicOnly($data);
1138 4
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1139 4
        return self::jsonDecode($response->body(), true);
1140
    }
1141
1142
    /**
1143
     * upgrade wallet to use a new account number
1144
     *  the account number specifies which blocktrail cosigning key is used
1145
     *
1146
     * @param string    $identifier             the wallet identifier to be upgraded
1147
     * @param int       $keyIndex               the new account to use
1148
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1149
     * @return mixed
1150
     */
1151 5
    public function upgradeKeyIndex($identifier, $keyIndex, $primaryPublicKey) {
1152
        $data = [
1153 5
            'key_index' => $keyIndex,
1154 5
            'primary_public_key' => $primaryPublicKey
1155
        ];
1156
1157 5
        $response = $this->blocktrailClient->post("wallet/{$identifier}/upgrade", null, $data, RestClient::AUTH_HTTP_SIG);
1158 5
        return self::jsonDecode($response->body(), true);
1159
    }
1160
1161
    /**
1162
     * @param array $options
1163
     * @return AddressReaderBase
1164
     */
1165 23
    private function makeAddressReader(array $options) {
1166 23
        if ($this->network == "bitcoincash") {
1167 4
            $useCashAddress = false;
1168 4
            if (array_key_exists("use_cashaddress", $options) && $options['use_cashaddress']) {
1169 3
                $useCashAddress = true;
1170
            }
1171 4
            return new BitcoinCashAddressReader($useCashAddress);
1172
        } else {
1173 19
            return new BitcoinAddressReader();
1174
        }
1175
    }
1176
1177
    /**
1178
     * initialize a previously created wallet
1179
     *
1180
     * Takes an options object, or accepts identifier/password for backwards compatiblity.
1181
     *
1182
     * Some of the options:
1183
     *  - "readonly/readOnly/read-only" can be to a boolean value,
1184
     *    so the wallet is loaded in read-only mode (no private key)
1185
     *  - "check_backup_key" can be set to your own backup key:
1186
     *    Format: ["M', "xpub..."]
1187
     *    Setting this will allow the SDK to check the server hasn't
1188
     *    a different key (one it happens to control)
1189
1190
     * Either takes one argument:
1191
     * @param array $options
1192
     *
1193
     * Or takes two arguments (old, deprecated syntax):
1194
     * (@nonPHP-doc) @param string    $identifier             the wallet identifier to be initialized
0 ignored issues
show
Bug introduced by
There is no parameter named $identifier. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1195
     * (@nonPHP-doc) @param string    $password               the password to decrypt the mnemonic with
0 ignored issues
show
Bug introduced by
There is no parameter named $password. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1196
     *
1197
     * @return WalletInterface
1198
     * @throws \Exception
1199
     */
1200 23
    public function initWallet($options) {
1201 23
        if (!is_array($options)) {
1202 1
            $args = func_get_args();
1203
            $options = [
1204 1
                "identifier" => $args[0],
1205 1
                "password" => $args[1],
1206
            ];
1207
        }
1208
1209 23
        $identifier = $options['identifier'];
1210 23
        $readonly = isset($options['readonly']) ? $options['readonly'] :
1211 23
                    (isset($options['readOnly']) ? $options['readOnly'] :
1212 23
                        (isset($options['read-only']) ? $options['read-only'] :
1213 23
                            false));
1214
1215
        // get the wallet data from the server
1216 23
        $data = $this->getWallet($identifier);
1217 23
        if (!$data) {
1218
            throw new \Exception("Failed to get wallet");
1219
        }
1220
1221 23
        if (array_key_exists('check_backup_key', $options)) {
1222 1
            if (!is_string($options['check_backup_key'])) {
1223 1
                throw new \RuntimeException("check_backup_key should be a string (the xpub)");
1224
            }
1225 1
            if ($options['check_backup_key'] !== $data['backup_public_key'][0]) {
1226 1
                throw new \RuntimeException("Backup key returned from server didn't match our own");
1227
            }
1228
        }
1229
1230 23
        $addressReader = $this->makeAddressReader($options);
1231
1232 23
        switch ($data['wallet_version']) {
1233 23
            case Wallet::WALLET_VERSION_V1:
1234 17
                $wallet = new WalletV1(
1235 17
                    $this,
1236 17
                    $identifier,
1237 17
                    isset($options['primary_mnemonic']) ? $options['primary_mnemonic'] : $data['primary_mnemonic'],
1238 17
                    $data['primary_public_keys'],
1239 17
                    $data['backup_public_key'],
1240 17
                    $data['blocktrail_public_keys'],
1241 17
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1242 17
                    $this->network,
1243 17
                    $this->testnet,
1244 17
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1245 17
                    $addressReader,
1246 17
                    $data['checksum']
1247
                );
1248 17
                break;
1249 6
            case Wallet::WALLET_VERSION_V2:
1250 2
                $wallet = new WalletV2(
1251 2
                    $this,
1252 2
                    $identifier,
1253 2
                    isset($options['encrypted_primary_seed']) ? $options['encrypted_primary_seed'] : $data['encrypted_primary_seed'],
1254 2
                    isset($options['encrypted_secret']) ? $options['encrypted_secret'] : $data['encrypted_secret'],
1255 2
                    $data['primary_public_keys'],
1256 2
                    $data['backup_public_key'],
1257 2
                    $data['blocktrail_public_keys'],
1258 2
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1259 2
                    $this->network,
1260 2
                    $this->testnet,
1261 2
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1262 2
                    $addressReader,
1263 2
                    $data['checksum']
1264
                );
1265 2
                break;
1266 4
            case Wallet::WALLET_VERSION_V3:
1267 4
                if (isset($options['encrypted_primary_seed'])) {
1268
                    if (!$options['encrypted_primary_seed'] instanceof Buffer) {
1269
                        throw new \InvalidArgumentException('Encrypted PrimarySeed must be provided as a Buffer');
1270
                    }
1271
                    $encryptedPrimarySeed = $data['encrypted_primary_seed'];
1272
                } else {
1273 4
                    $encryptedPrimarySeed = new Buffer(base64_decode($data['encrypted_primary_seed']));
1274
                }
1275
1276 4
                if (isset($options['encrypted_secret'])) {
1277
                    if (!$options['encrypted_secret'] instanceof Buffer) {
1278
                        throw new \InvalidArgumentException('Encrypted secret must be provided as a Buffer');
1279
                    }
1280
1281
                    $encryptedSecret = $data['encrypted_secret'];
1282
                } else {
1283 4
                    $encryptedSecret = new Buffer(base64_decode($data['encrypted_secret']));
1284
                }
1285
1286 4
                $wallet = new WalletV3(
1287 4
                    $this,
1288 4
                    $identifier,
1289 4
                    $encryptedPrimarySeed,
1290 4
                    $encryptedSecret,
1291 4
                    $data['primary_public_keys'],
1292 4
                    $data['backup_public_key'],
1293 4
                    $data['blocktrail_public_keys'],
1294 4
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1295 4
                    $this->network,
1296 4
                    $this->testnet,
1297 4
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1298 4
                    $addressReader,
1299 4
                    $data['checksum']
1300
                );
1301 4
                break;
1302
            default:
1303
                throw new \InvalidArgumentException("Invalid wallet version");
1304
        }
1305
1306 23
        if (!$readonly) {
1307 23
            $wallet->unlock($options);
1308
        }
1309
1310 23
        return $wallet;
1311
    }
1312
1313
    /**
1314
     * get the wallet data from the server
1315
     *
1316
     * @param string    $identifier             the identifier of the wallet
1317
     * @return mixed
1318
     */
1319 23
    public function getWallet($identifier) {
1320 23
        $response = $this->blocktrailClient->get("wallet/{$identifier}", null, RestClient::AUTH_HTTP_SIG);
1321 23
        return self::jsonDecode($response->body(), true);
1322
    }
1323
1324
    /**
1325
     * update the wallet data on the server
1326
     *
1327
     * @param string    $identifier
1328
     * @param $data
1329
     * @return mixed
1330
     */
1331 3
    public function updateWallet($identifier, $data) {
1332 3
        $response = $this->blocktrailClient->post("wallet/{$identifier}", null, $data, RestClient::AUTH_HTTP_SIG);
1333 3
        return self::jsonDecode($response->body(), true);
1334
    }
1335
1336
    /**
1337
     * delete a wallet from the server
1338
     *  the checksum address and a signature to verify you ownership of the key of that checksum address
1339
     *  is required to be able to delete a wallet
1340
     *
1341
     * @param string    $identifier             the identifier of the wallet
1342
     * @param string    $checksumAddress        the address for your master private key (and the checksum used when creating the wallet)
1343
     * @param string    $signature              a signature of the checksum address as message signed by the private key matching that address
1344
     * @param bool      $force                  ignore warnings (such as a non-zero balance)
1345
     * @return mixed
1346
     */
1347 10
    public function deleteWallet($identifier, $checksumAddress, $signature, $force = false) {
1348 10
        $response = $this->blocktrailClient->delete("wallet/{$identifier}", ['force' => $force], [
1349 10
            'checksum' => $checksumAddress,
1350 10
            'signature' => $signature
1351 10
        ], RestClient::AUTH_HTTP_SIG, 360);
1352 10
        return self::jsonDecode($response->body(), true);
1353
    }
1354
1355
    /**
1356
     * create new backup key;
1357
     *  1) a BIP39 mnemonic
1358
     *  2) a seed from that mnemonic with a blank password
1359
     *  3) a private key from that seed
1360
     *
1361
     * @return array [mnemonic, seed, key]
1362
     */
1363 1
    protected function newBackupSeed() {
1364 1
        list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed("");
1365
1366 1
        return [$backupMnemonic, $backupSeed, $backupPrivateKey];
1367
    }
1368
1369
    /**
1370
     * create new primary key;
1371
     *  1) a BIP39 mnemonic
1372
     *  2) a seed from that mnemonic with the password
1373
     *  3) a private key from that seed
1374
     *
1375
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1376
     * @return array [mnemonic, seed, key]
1377
     * @TODO: require a strong password?
1378
     */
1379 1
    protected function newPrimarySeed($passphrase) {
1380 1
        list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase);
1381
1382 1
        return [$primaryMnemonic, $primarySeed, $primaryPrivateKey];
1383
    }
1384
1385
    /**
1386
     * create a new key;
1387
     *  1) a BIP39 mnemonic
1388
     *  2) a seed from that mnemonic with the password
1389
     *  3) a private key from that seed
1390
     *
1391
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1392
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1393
     * @return array
1394
     */
1395 1
    protected function generateNewSeed($passphrase = "", $forceEntropy = null) {
1396
        // generate master seed, retry if the generated private key isn't valid (FALSE is returned)
1397
        do {
1398 1
            $mnemonic = $this->generateNewMnemonic($forceEntropy);
1399
1400 1
            $seed = (new Bip39SeedGenerator)->getSeed($mnemonic, $passphrase);
1401
1402 1
            $key = null;
1403
            try {
1404 1
                $key = HierarchicalKeyFactory::fromEntropy($seed);
1405
            } catch (\Exception $e) {
1406
                // try again
1407
            }
1408 1
        } while (!$key);
1409
1410 1
        return [$mnemonic, $seed, $key];
1411
    }
1412
1413
    /**
1414
     * generate a new mnemonic from some random entropy (512 bit)
1415
     *
1416
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1417
     * @return string
1418
     * @throws \Exception
1419
     */
1420 1
    protected function generateNewMnemonic($forceEntropy = null) {
1421 1
        if ($forceEntropy === null) {
1422 1
            $random = new Random();
1423 1
            $entropy = $random->bytes(512 / 8);
1424
        } else {
1425
            $entropy = $forceEntropy;
1426
        }
1427
1428 1
        return MnemonicFactory::bip39()->entropyToMnemonic($entropy);
0 ignored issues
show
Bug introduced by
It seems like $entropy defined by $forceEntropy on line 1425 can also be of type string; however, BitWasp\Bitcoin\Mnemonic...ic::entropyToMnemonic() does only seem to accept object<BitWasp\Buffertools\BufferInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1429
    }
1430
1431
    /**
1432
     * get the balance for the wallet
1433
     *
1434
     * @param string    $identifier             the identifier of the wallet
1435
     * @return array
1436
     */
1437 9
    public function getWalletBalance($identifier) {
1438 9
        $response = $this->blocktrailClient->get("wallet/{$identifier}/balance", null, RestClient::AUTH_HTTP_SIG);
1439 9
        return self::jsonDecode($response->body(), true);
1440
    }
1441
1442
    /**
1443
     * get a new derivation number for specified parent path
1444
     *  eg; m/44'/1'/0/0 results in m/44'/1'/0/0/0 and next time in m/44'/1'/0/0/1 and next time in m/44'/1'/0/0/2
1445
     *
1446
     * returns the path
1447
     *
1448
     * @param string    $identifier             the identifier of the wallet
1449
     * @param string    $path                   the parent path for which to get a new derivation
1450
     * @return string
1451
     */
1452 1
    public function getNewDerivation($identifier, $path) {
1453 1
        $result = $this->_getNewDerivation($identifier, $path);
1454 1
        return $result['path'];
1455
    }
1456
1457
    /**
1458
     * get a new derivation number for specified parent path
1459
     *  eg; m/44'/1'/0/0 results in m/44'/1'/0/0/0 and next time in m/44'/1'/0/0/1 and next time in m/44'/1'/0/0/2
1460
     *
1461
     * @param string    $identifier             the identifier of the wallet
1462
     * @param string    $path                   the parent path for which to get a new derivation
1463
     * @return mixed
1464
     */
1465 15
    public function _getNewDerivation($identifier, $path) {
1466 15
        $response = $this->blocktrailClient->post("wallet/{$identifier}/path", null, ['path' => $path], RestClient::AUTH_HTTP_SIG);
1467 15
        return self::jsonDecode($response->body(), true);
1468
    }
1469
1470
    /**
1471
     * get the path (and redeemScript) to specified address
1472
     *
1473
     * @param string $identifier
1474
     * @param string $address
1475
     * @return array
1476
     * @throws \Exception
1477
     */
1478 1
    public function getPathForAddress($identifier, $address) {
1479 1
        $response = $this->blocktrailClient->post("wallet/{$identifier}/path_for_address", null, ['address' => $address], RestClient::AUTH_HTTP_SIG);
1480 1
        return self::jsonDecode($response->body(), true)['path'];
1481
    }
1482
1483
    /**
1484
     * send the transaction using the API
1485
     *
1486
     * @param string       $identifier     the identifier of the wallet
1487
     * @param string|array $rawTransaction raw hex of the transaction (should be partially signed)
1488
     * @param array        $paths          list of the paths that were used for the UTXO
1489
     * @param bool         $checkFee       let the server verify the fee after signing
1490
     * @param null         $twoFactorToken
1491
     * @return string                                the complete raw transaction
1492
     * @throws \Exception
1493
     */
1494 4
    public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee = false, $twoFactorToken = null) {
1495
        $data = [
1496 4
            'paths' => $paths,
1497 4
            'two_factor_token' => $twoFactorToken,
1498
        ];
1499
1500 4
        if (is_array($rawTransaction)) {
1501 4
            if (array_key_exists('base_transaction', $rawTransaction)
1502 4
            && array_key_exists('signed_transaction', $rawTransaction)) {
1503 4
                $data['base_transaction'] = $rawTransaction['base_transaction'];
1504 4
                $data['signed_transaction'] = $rawTransaction['signed_transaction'];
1505
            } else {
1506 4
                throw new \RuntimeException("Invalid value for transaction. For segwit transactions, pass ['base_transaction' => '...', 'signed_transaction' => '...']");
1507
            }
1508
        } else {
1509
            $data['raw_transaction'] = $rawTransaction;
1510
        }
1511
1512
        // dynamic TTL for when we're signing really big transactions
1513 4
        $ttl = max(5.0, count($paths) * 0.25) + 4.0;
1514
1515 4
        $response = $this->blocktrailClient->post("wallet/{$identifier}/send", ['check_fee' => (int)!!$checkFee], $data, RestClient::AUTH_HTTP_SIG, $ttl);
1516 4
        $signed = self::jsonDecode($response->body(), true);
1517
1518 4
        if (!$signed['complete'] || $signed['complete'] == 'false') {
1519
            throw new \Exception("Failed to completely sign transaction");
1520
        }
1521
1522
        // create TX hash from the raw signed hex
1523 4
        return TransactionFactory::fromHex($signed['hex'])->getTxId()->getHex();
1524
    }
1525
1526
    /**
1527
     * use the API to get the best inputs to use based on the outputs
1528
     *
1529
     * the return array has the following format:
1530
     * [
1531
     *  "utxos" => [
1532
     *      [
1533
     *          "hash" => "<txHash>",
1534
     *          "idx" => "<index of the output of that <txHash>",
1535
     *          "scriptpubkey_hex" => "<scriptPubKey-hex>",
1536
     *          "value" => 32746327,
1537
     *          "address" => "1address",
1538
     *          "path" => "m/44'/1'/0'/0/13",
1539
     *          "redeem_script" => "<redeemScript-hex>",
1540
     *      ],
1541
     *  ],
1542
     *  "fee"   => 10000,
1543
     *  "change"=> 1010109201,
1544
     * ]
1545
     *
1546
     * @param string   $identifier              the identifier of the wallet
1547
     * @param array    $outputs                 the outputs you want to create - array[address => satoshi-value]
1548
     * @param bool     $lockUTXO                when TRUE the UTXOs selected will be locked for a few seconds
1549
     *                                          so you have some time to spend them without race-conditions
1550
     * @param bool     $allowZeroConf
1551
     * @param string   $feeStrategy
1552
     * @param null|int $forceFee
1553
     * @return array
1554
     * @throws \Exception
1555
     */
1556 12
    public function coinSelection($identifier, $outputs, $lockUTXO = false, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1557
        $args = [
1558 12
            'lock' => (int)!!$lockUTXO,
1559 12
            'zeroconf' => (int)!!$allowZeroConf,
1560 12
            'fee_strategy' => $feeStrategy,
1561
        ];
1562
1563 12
        if ($forceFee !== null) {
1564 1
            $args['forcefee'] = (int)$forceFee;
1565
        }
1566
1567 12
        $response = $this->blocktrailClient->post(
1568 12
            "wallet/{$identifier}/coin-selection",
1569 12
            $args,
1570 12
            $outputs,
1571 12
            RestClient::AUTH_HTTP_SIG
1572
        );
1573
1574 6
        return self::jsonDecode($response->body(), true);
1575
    }
1576
1577
    /**
1578
     *
1579
     * @param string   $identifier the identifier of the wallet
1580
     * @param bool     $allowZeroConf
1581
     * @param string   $feeStrategy
1582
     * @param null|int $forceFee
1583
     * @param int      $outputCnt
1584
     * @return array
1585
     * @throws \Exception
1586
     */
1587
    public function walletMaxSpendable($identifier, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
1588
        $args = [
1589
            'zeroconf' => (int)!!$allowZeroConf,
1590
            'fee_strategy' => $feeStrategy,
1591
            'outputs' => $outputCnt,
1592
        ];
1593
1594
        if ($forceFee !== null) {
1595
            $args['forcefee'] = (int)$forceFee;
1596
        }
1597
1598
        $response = $this->blocktrailClient->get(
1599
            "wallet/{$identifier}/max-spendable",
1600
            $args,
1601
            RestClient::AUTH_HTTP_SIG
1602
        );
1603
1604
        return self::jsonDecode($response->body(), true);
1605
    }
1606
1607
    /**
1608
     * @return array        ['optimal_fee' => 10000, 'low_priority_fee' => 5000]
1609
     */
1610 3
    public function feePerKB() {
1611 3
        $response = $this->blocktrailClient->get("fee-per-kb");
1612 3
        return self::jsonDecode($response->body(), true);
1613
    }
1614
1615
    /**
1616
     * get the current price index
1617
     *
1618
     * @return array        eg; ['USD' => 287.30]
1619
     */
1620 1
    public function price() {
1621 1
        $response = $this->blocktrailClient->get("price");
1622 1
        return self::jsonDecode($response->body(), true);
1623
    }
1624
1625
    /**
1626
     * setup webhook for wallet
1627
     *
1628
     * @param string    $identifier         the wallet identifier for which to create the webhook
1629
     * @param string    $webhookIdentifier  the webhook identifier to use
1630
     * @param string    $url                the url to receive the webhook events
1631
     * @return array
1632
     */
1633 1
    public function setupWalletWebhook($identifier, $webhookIdentifier, $url) {
1634 1
        $response = $this->blocktrailClient->post("wallet/{$identifier}/webhook", null, ['url' => $url, 'identifier' => $webhookIdentifier], RestClient::AUTH_HTTP_SIG);
1635 1
        return self::jsonDecode($response->body(), true);
1636
    }
1637
1638
    /**
1639
     * delete webhook for wallet
1640
     *
1641
     * @param string    $identifier         the wallet identifier for which to delete the webhook
1642
     * @param string    $webhookIdentifier  the webhook identifier to delete
1643
     * @return array
1644
     */
1645 1
    public function deleteWalletWebhook($identifier, $webhookIdentifier) {
1646 1
        $response = $this->blocktrailClient->delete("wallet/{$identifier}/webhook/{$webhookIdentifier}", null, null, RestClient::AUTH_HTTP_SIG);
1647 1
        return self::jsonDecode($response->body(), true);
1648
    }
1649
1650
    /**
1651
     * lock a specific unspent output
1652
     *
1653
     * @param     $identifier
1654
     * @param     $txHash
1655
     * @param     $txIdx
1656
     * @param int $ttl
1657
     * @return bool
1658
     */
1659
    public function lockWalletUTXO($identifier, $txHash, $txIdx, $ttl = 3) {
1660
        $response = $this->blocktrailClient->post("wallet/{$identifier}/lock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx, 'ttl' => $ttl], RestClient::AUTH_HTTP_SIG);
1661
        return self::jsonDecode($response->body(), true)['locked'];
1662
    }
1663
1664
    /**
1665
     * unlock a specific unspent output
1666
     *
1667
     * @param     $identifier
1668
     * @param     $txHash
1669
     * @param     $txIdx
1670
     * @return bool
1671
     */
1672
    public function unlockWalletUTXO($identifier, $txHash, $txIdx) {
1673
        $response = $this->blocktrailClient->post("wallet/{$identifier}/unlock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx], RestClient::AUTH_HTTP_SIG);
1674
        return self::jsonDecode($response->body(), true)['unlocked'];
1675
    }
1676
1677
    /**
1678
     * get all transactions for wallet (paginated)
1679
     *
1680
     * @param  string  $identifier  the wallet identifier for which to get transactions
1681
     * @param  integer $page        pagination: page number
1682
     * @param  integer $limit       pagination: records per page (max 500)
1683
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1684
     * @return array                associative array containing the response
1685
     */
1686 1
    public function walletTransactions($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1687
        $queryString = [
1688 1
            'page' => $page,
1689 1
            'limit' => $limit,
1690 1
            'sort_dir' => $sortDir
1691
        ];
1692 1
        $response = $this->blocktrailClient->get("wallet/{$identifier}/transactions", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1693 1
        return self::jsonDecode($response->body(), true);
1694
    }
1695
1696
    /**
1697
     * get all addresses for wallet (paginated)
1698
     *
1699
     * @param  string  $identifier  the wallet identifier for which to get addresses
1700
     * @param  integer $page        pagination: page number
1701
     * @param  integer $limit       pagination: records per page (max 500)
1702
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1703
     * @return array                associative array containing the response
1704
     */
1705 1
    public function walletAddresses($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1706
        $queryString = [
1707 1
            'page' => $page,
1708 1
            'limit' => $limit,
1709 1
            'sort_dir' => $sortDir
1710
        ];
1711 1
        $response = $this->blocktrailClient->get("wallet/{$identifier}/addresses", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1712 1
        return self::jsonDecode($response->body(), true);
1713
    }
1714
1715
    /**
1716
     * get all UTXOs for wallet (paginated)
1717
     *
1718
     * @param  string  $identifier  the wallet identifier for which to get addresses
1719
     * @param  integer $page        pagination: page number
1720
     * @param  integer $limit       pagination: records per page (max 500)
1721
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1722
     * @param  boolean $zeroconf    include zero confirmation transactions
1723
     * @return array                associative array containing the response
1724
     */
1725 1
    public function walletUTXOs($identifier, $page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1726
        $queryString = [
1727 1
            'page' => $page,
1728 1
            'limit' => $limit,
1729 1
            'sort_dir' => $sortDir,
1730 1
            'zeroconf' => (int)!!$zeroconf,
1731
        ];
1732 1
        $response = $this->blocktrailClient->get("wallet/{$identifier}/utxos", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1733 1
        return self::jsonDecode($response->body(), true);
1734
    }
1735
1736
    /**
1737
     * get a paginated list of all wallets associated with the api user
1738
     *
1739
     * @param  integer          $page    pagination: page number
1740
     * @param  integer          $limit   pagination: records per page
1741
     * @return array                     associative array containing the response
1742
     */
1743 2
    public function allWallets($page = 1, $limit = 20) {
1744
        $queryString = [
1745 2
            'page' => $page,
1746 2
            'limit' => $limit
1747
        ];
1748 2
        $response = $this->blocktrailClient->get("wallets", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1749 2
        return self::jsonDecode($response->body(), true);
1750
    }
1751
1752
    /**
1753
     * send raw transaction
1754
     *
1755
     * @param     $txHex
1756
     * @return bool
1757
     */
1758
    public function sendRawTransaction($txHex) {
1759
        $response = $this->blocktrailClient->post("send-raw-tx", null, ['hex' => $txHex], RestClient::AUTH_HTTP_SIG);
1760
        return self::jsonDecode($response->body(), true);
1761
    }
1762
1763
    /**
1764
     * testnet only ;-)
1765
     *
1766
     * @param     $address
1767
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1768
     * @return mixed
1769
     * @throws \Exception
1770
     */
1771
    public function faucetWithdrawal($address, $amount = 10000) {
1772
        $response = $this->blocktrailClient->post("faucet/withdrawl", null, [
1773
            'address' => $address,
1774
            'amount' => $amount,
1775
        ], RestClient::AUTH_HTTP_SIG);
1776
        return self::jsonDecode($response->body(), true);
1777
    }
1778
1779
    /**
1780
     * Exists for BC. Remove at major bump.
1781
     *
1782
     * @see faucetWithdrawal
1783
     * @deprecated
1784
     * @param     $address
1785
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1786
     * @return mixed
1787
     * @throws \Exception
1788
     */
1789
    public function faucetWithdrawl($address, $amount = 10000) {
1790
        return $this->faucetWithdrawal($address, $amount);
1791
    }
1792
1793
    /**
1794
     * verify a message signed bitcoin-core style
1795
     *
1796
     * @param  string           $message
1797
     * @param  string           $address
1798
     * @param  string           $signature
1799
     * @return boolean
1800
     */
1801 2
    public function verifyMessage($message, $address, $signature) {
1802 2
        $adapter = Bitcoin::getEcAdapter();
1803 2
        $addr = \BitWasp\Bitcoin\Address\AddressFactory::fromString($address);
1804 2
        if (!$addr instanceof PayToPubKeyHashAddress) {
1805
            throw new \RuntimeException('Can only verify a message with a pay-to-pubkey-hash address');
1806
        }
1807
1808
        /** @var CompactSignatureSerializerInterface $csSerializer */
1809 2
        $csSerializer = EcSerializer::getSerializer(CompactSignatureSerializerInterface::class, $adapter);
0 ignored issues
show
Documentation introduced by
$adapter is of type object<BitWasp\Bitcoin\C...ter\EcAdapterInterface>, but the function expects a boolean|object<BitWasp\B...\Crypto\EcAdapter\true>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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