Completed
Branch master (e62670)
by
unknown
02:05
created

BlocktrailSDK::setBitcoinLibMagicBytes()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 22
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 7.392

Importance

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

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1777
1778 1
        $adapter = Bitcoin::getEcAdapter();
1779 1
        $addr = \BitWasp\Bitcoin\Address\AddressFactory::fromString($address);
1780 1
        if (!$addr instanceof PayToPubKeyHashAddress) {
1781
            throw new \RuntimeException('Can only verify a message with a pay-to-pubkey-hash address');
1782
        }
1783
1784
        /** @var CompactSignatureSerializerInterface $csSerializer */
1785 1
        $csSerializer = EcSerializer::getSerializer(CompactSignatureSerializerInterface::class, $adapter);
0 ignored issues
show
Documentation introduced by
$adapter is of type object<BitWasp\Bitcoin\C...ter\EcAdapterInterface>, but the function expects a boolean|object<BitWasp\B...\Crypto\EcAdapter\true>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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