Completed
Branch master (6cbd51)
by
unknown
02:55
created

BlocktrailSDK::__construct()   C

Complexity

Conditions 7
Paths 18

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7.0099

Importance

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

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

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

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

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

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

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

Loading history...
205
    }
206
207
    /**
208
     * get a single address
209
     * @param  string $address address hash
210
     * @return array           associative array containing the response
211
     */
212 1
    public function address($address) {
213 1
        $response = $this->dataClient->get($this->converter->getUrlForAddress($address));
214 1
        return $this->converter->convertAddress($response->body());
215
    }
216
217
    /**
218
     * get all transactions 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 addressTransactions($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->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
232 1
        return $this->converter->convertAddressTxs($response->body());
233
    }
234
235
    /**
236
     * get all unconfirmed transactions for an address (paginated)
237
     * @param  string  $address address hash
238
     * @param  integer $page    pagination: page number
239
     * @param  integer $limit   pagination: records per page (max 500)
240
     * @param  string  $sortDir pagination: sort direction (asc|desc)
241
     * @return array            associative array containing the response
242
     */
243
    public function addressUnconfirmedTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
244
        $queryString = [
245
            'page' => $page,
246
            'limit' => $limit,
247
            'sort_dir' => $sortDir
248
        ];
249
        $response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
250
        return $this->converter->convertAddressTxs($response->body());
251
    }
252
253
    /**
254
     * get all unspent outputs for an address (paginated)
255
     * @param  string  $address address hash
256
     * @param  integer $page    pagination: page number
257
     * @param  integer $limit   pagination: records per page (max 500)
258
     * @param  string  $sortDir pagination: sort direction (asc|desc)
259
     * @return array            associative array containing the response
260
     */
261 1
    public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') {
262
        $queryString = [
263 1
            'page' => $page,
264 1
            'limit' => $limit,
265 1
            'sort_dir' => $sortDir
266
        ];
267 1
        $response = $this->dataClient->get($this->converter->getUrlForAddressUnspent($address), $this->converter->paginationParams($queryString));
268 1
        return $this->converter->convertAddressUnspentOutputs($response->body(), $address);
269
    }
270
271
    /**
272
     * get all unspent outputs for a batch of addresses (paginated)
273
     *
274
     * @param  string[] $addresses
275
     * @param  integer  $page    pagination: page number
276
     * @param  integer  $limit   pagination: records per page (max 500)
277
     * @param  string   $sortDir pagination: sort direction (asc|desc)
278
     * @return array associative array containing the response
279
     * @throws \Exception
280
     */
281
    public function batchAddressUnspentOutputs($addresses, $page = 1, $limit = 20, $sortDir = 'asc') {
282
        $queryString = [
283
            'page' => $page,
284
            'limit' => $limit,
285
            'sort_dir' => $sortDir
286
        ];
287
288
        if ($this->converter instanceof BtccomConverter) {
289
            if ($page > 1) {
290
                return [
291
                    'data' => [],
292
                    'current_page' => 2,
293
                    'per_page' => null,
294
                    'total' => null,
295
                ];
296
            }
297
298
            $response = $this->dataClient->get($this->converter->getUrlForBatchAddressesUnspent($addresses), $this->converter->paginationParams($queryString));
299
            return $this->converter->convertBatchAddressesUnspentOutputs($response->body());
300
        } else {
301
            $response = $this->client->post("address/unspent-outputs", $queryString, ['addresses' => $addresses]);
0 ignored issues
show
Bug introduced by
The property client does not seem to exist. Did you mean blocktrailClient?

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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