Completed
Pull Request — master (#110)
by Ruben de
29:47
created

BlocktrailSDK::batchSubscribeAddressTransactions()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

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