Completed
Pull Request — master (#110)
by Ruben de
40:03 queued 34:00
created

BlocktrailSDK::deleteWallet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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