Completed
Pull Request — master (#125)
by thomas
03:07 queued 45s
created

BlocktrailSDK::normalizeNetwork()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 4
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\PayToPubKeyHashAddress;
6
use BitWasp\Bitcoin\Bitcoin;
7
use BitWasp\Bitcoin\Crypto\EcAdapter\EcSerializer;
8
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
9
use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\CompactSignatureSerializerInterface;
10
use BitWasp\Bitcoin\Crypto\Random\Random;
11
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKey;
12
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
13
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
14
use BitWasp\Bitcoin\MessageSigner\SignedMessage;
15
use BitWasp\Bitcoin\Mnemonic\Bip39\Bip39SeedGenerator;
16
use BitWasp\Bitcoin\Mnemonic\MnemonicFactory;
17
use BitWasp\Bitcoin\Network\NetworkFactory;
18
use BitWasp\Bitcoin\Transaction\TransactionFactory;
19
use BitWasp\Buffertools\Buffer;
20
use BitWasp\Buffertools\BufferInterface;
21
use Blocktrail\CryptoJSAES\CryptoJSAES;
22
use Blocktrail\SDK\Address\AddressReaderBase;
23
use Blocktrail\SDK\Address\BitcoinAddressReader;
24
use Blocktrail\SDK\Address\BitcoinCashAddressReader;
25
use Blocktrail\SDK\Address\CashAddress;
26
use Blocktrail\SDK\Backend\BlocktrailConverter;
27
use Blocktrail\SDK\Backend\BtccomConverter;
28
use Blocktrail\SDK\Backend\ConverterInterface;
29
use Blocktrail\SDK\Bitcoin\BIP32Key;
30
use Blocktrail\SDK\Connection\RestClient;
31
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
32
use Blocktrail\SDK\Network\BitcoinCash;
33
use Blocktrail\SDK\Connection\RestClientInterface;
34
use Blocktrail\SDK\Network\BitcoinCashRegtest;
35
use Blocktrail\SDK\Network\BitcoinCashTestnet;
36
use 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
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
79
80
        list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet);
81
82
        if (is_null($apiEndpoint)) {
83
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://wallet-api.btc.com";
84
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
85
        }
86
87
        // normalize network and set bitcoinlib to the right magic-bytes
88
        list($this->network, $this->testnet, $regtest) = $this->normalizeNetwork($network, $testnet);
89
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet, $regtest);
90
91
        $btccomEndpoint = getenv('BLOCKTRAIL_SDK_BTCCOM_API_ENDPOINT');
92
        if (!$btccomEndpoint) {
93
            $btccomEndpoint = "https://" . ($this->network === "BCC" ? "bch-chain" : "chain") . ".api.btc.com";
94
        }
95
        $btccomEndpoint = "{$btccomEndpoint}/v3/";
96
97
        if ($this->testnet && strpos($btccomEndpoint, "tchain") === false) {
98
            $btccomEndpoint = \str_replace("chain", "tchain", $btccomEndpoint);
99
        }
100
101
        echo $apiEndpoint.PHP_EOL;
102
        $this->blocktrailClient = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
103
        $this->blocktrailClient->setVerboseErrors(true);
104
        $this->blocktrailClient->setCurlDebugging(true);
105
106
        $this->dataClient = new RestClient($btccomEndpoint, $apiVersion, $apiKey, $apiSecret);
107
        $this->converter = new BtccomConverter();
108
    }
109
110
    /**
111
     * normalize network string
112
     *
113
     * @param $network
114
     * @param $testnet
115
     * @return array
116
     * @throws \Exception
117
     */
118
    protected function normalizeNetwork($network, $testnet) {
119
        // [name, testnet, network]
120
        return Util::normalizeNetwork($network, $testnet);
121
    }
122
123
    /**
124
     * set BitcoinLib to the correct magic-byte defaults for the selected network
125
     *
126
     * @param $network
127
     * @param bool $testnet
128
     * @param bool $regtest
129
     */
130
    protected function setBitcoinLibMagicBytes($network, $testnet, $regtest) {
131
132
        if ($network === "bitcoin") {
133
            if ($regtest) {
134
                $useNetwork = NetworkFactory::bitcoinRegtest();
135
            } else if ($testnet) {
136
                $useNetwork = NetworkFactory::bitcoinTestnet();
137
            } else {
138
                $useNetwork = NetworkFactory::bitcoin();
139
            }
140
        } else if ($network === "bitcoincash") {
141
            if ($regtest) {
142
                $useNetwork = new BitcoinCashRegtest();
143
            } else if ($testnet) {
144
                $useNetwork = new BitcoinCashTestnet();
145
            } else {
146
                $useNetwork = new BitcoinCash();
147
            }
148
        }
149
150
        Bitcoin::setNetwork($useNetwork);
0 ignored issues
show
Bug introduced by
The variable $useNetwork does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
151
    }
152
153
    /**
154
     * enable CURL debugging output
155
     *
156
     * @param   bool        $debug
157
     *
158
     * @codeCoverageIgnore
159
     */
160
    public function setCurlDebugging($debug = true) {
161
        $this->blocktrailClient->setCurlDebugging($debug);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Blocktrail\SDK\Connection\RestClientInterface as the method setCurlDebugging() does only exist in the following implementations of said interface: Blocktrail\SDK\Connection\RestClient.

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

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

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
186
        $this->dataClient->setCurlDefaultOption($key, $value);
187
    }
188
189
    /**
190
     * @return  RestClientInterface
191
     */
192
    public function getRestClient() {
193
        return $this->blocktrailClient;
194
    }
195
196
    /**
197
     * @return  RestClient
198
     */
199
    public function getDataRestClient() {
200
        return $this->dataClient;
201
    }
202
203
    /**
204
     * @param RestClientInterface $restClient
205
     */
206
    public function setRestClient(RestClientInterface $restClient) {
207
        $this->blocktrailClient = $restClient;
208
    }
209
210
    /**
211
     * get a single address
212
     * @param  string $address address hash
213
     * @return array           associative array containing the response
214
     */
215
    public function address($address) {
216
        $response = $this->dataClient->get($this->converter->getUrlForAddress($address));
217
        return $this->converter->convertAddress($response->body());
218
    }
219
220
    /**
221
     * get all transactions for an address (paginated)
222
     * @param  string  $address address hash
223
     * @param  integer $page    pagination: page number
224
     * @param  integer $limit   pagination: records per page (max 500)
225
     * @param  string  $sortDir pagination: sort direction (asc|desc)
226
     * @return array            associative array containing the response
227
     */
228
    public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
229
        $queryString = [
230
            'page' => $page,
231
            'limit' => $limit,
232
            'sort_dir' => $sortDir,
233
        ];
234
        $response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
235
        return $this->converter->convertAddressTxs($response->body());
236
    }
237
238
    /**
239
     * get all unconfirmed transactions for an address (paginated)
240
     * @param  string  $address address hash
241
     * @param  integer $page    pagination: page number
242
     * @param  integer $limit   pagination: records per page (max 500)
243
     * @param  string  $sortDir pagination: sort direction (asc|desc)
244
     * @return array            associative array containing the response
245
     */
246
    public function addressUnconfirmedTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
247
        $queryString = [
248
            'page' => $page,
249
            'limit' => $limit,
250
            'sort_dir' => $sortDir
251
        ];
252
        $response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
253
        return $this->converter->convertAddressTxs($response->body());
254
    }
255
256
    /**
257
     * get all unspent outputs for an address (paginated)
258
     * @param  string  $address address hash
259
     * @param  integer $page    pagination: page number
260
     * @param  integer $limit   pagination: records per page (max 500)
261
     * @param  string  $sortDir pagination: sort direction (asc|desc)
262
     * @return array            associative array containing the response
263
     */
264
    public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') {
265
        $queryString = [
266
            'page' => $page,
267
            'limit' => $limit,
268
            'sort_dir' => $sortDir
269
        ];
270
        $response = $this->dataClient->get($this->converter->getUrlForAddressUnspent($address), $this->converter->paginationParams($queryString));
271
        return $this->converter->convertAddressUnspentOutputs($response->body(), $address);
272
    }
273
274
    /**
275
     * get all unspent outputs for a batch of addresses (paginated)
276
     *
277
     * @param  string[] $addresses
278
     * @param  integer  $page    pagination: page number
279
     * @param  integer  $limit   pagination: records per page (max 500)
280
     * @param  string   $sortDir pagination: sort direction (asc|desc)
281
     * @return array associative array containing the response
282
     * @throws \Exception
283
     */
284
    public function batchAddressUnspentOutputs($addresses, $page = 1, $limit = 20, $sortDir = 'asc') {
285
        $queryString = [
286
            'page' => $page,
287
            'limit' => $limit,
288
            'sort_dir' => $sortDir
289
        ];
290
291
        if ($this->converter instanceof BtccomConverter) {
292
            if ($page > 1) {
293
                return [
294
                    'data' => [],
295
                    'current_page' => 2,
296
                    'per_page' => null,
297
                    'total' => null,
298
                ];
299
            }
300
301
            $response = $this->dataClient->get($this->converter->getUrlForBatchAddressesUnspent($addresses), $this->converter->paginationParams($queryString));
302
            return $this->converter->convertBatchAddressesUnspentOutputs($response->body());
303
        } else {
304
            $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...
305
            return self::jsonDecode($response->body(), true);
306
        }
307
    }
308
309
    /**
310
     * verify ownership of an address
311
     * @param  string  $address     address hash
312
     * @param  string  $signature   a signed message (the address hash) using the private key of the address
313
     * @return array                associative array containing the response
314
     */
315
    public function verifyAddress($address, $signature) {
316
        if ($this->verifyMessage($address, $address, $signature)) {
317
            return ['result' => true, 'msg' => 'Successfully verified'];
318
        } else {
319
            return ['result' => false];
320
        }
321
    }
322
323
    /**
324
     * get all blocks (paginated)
325
     * @param  integer $page    pagination: page number
326
     * @param  integer $limit   pagination: records per page
327
     * @param  string  $sortDir pagination: sort direction (asc|desc)
328
     * @return array            associative array containing the response
329
     */
330
    public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') {
331
        $queryString = [
332
            'page' => $page,
333
            'limit' => $limit,
334
            'sort_dir' => $sortDir
335
        ];
336
        $response = $this->dataClient->get($this->converter->getUrlForAllBlocks(), $this->converter->paginationParams($queryString));
337
        return $this->converter->convertBlocks($response->body());
338
    }
339
340
    /**
341
     * get the latest block
342
     * @return array            associative array containing the response
343
     */
344
    public function blockLatest() {
345
        $response = $this->dataClient->get($this->converter->getUrlForBlock("latest"));
346
        return $this->converter->convertBlock($response->body());
347
    }
348
349
    /**
350
     * get the wallet API's latest block ['hash' => x, 'height' => y]
351
     * @return array            associative array containing the response
352
     */
353
    public function getWalletBlockLatest() {
354
        $response = $this->blocktrailClient->get("block/latest");
355
        return BlocktrailSDK::jsonDecode($response->body(), true);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
356
    }
357
358
    /**
359
     * get an individual block
360
     * @param  string|integer $block    a block hash or a block height
361
     * @return array                    associative array containing the response
362
     */
363
    public function block($block) {
364
        $response = $this->dataClient->get($this->converter->getUrlForBlock($block));
365
        return $this->converter->convertBlock($response->body());
366
    }
367
368
    /**
369
     * get all transaction in a block (paginated)
370
     * @param  string|integer   $block   a block hash or a block height
371
     * @param  integer          $page    pagination: page number
372
     * @param  integer          $limit   pagination: records per page
373
     * @param  string           $sortDir pagination: sort direction (asc|desc)
374
     * @return array                     associative array containing the response
375
     */
376
    public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') {
377
        $queryString = [
378
            'page' => $page,
379
            'limit' => $limit,
380
            'sort_dir' => $sortDir
381
        ];
382
        $response = $this->dataClient->get($this->converter->getUrlForBlockTransaction($block), $this->converter->paginationParams($queryString));
383
        return $this->converter->convertBlockTxs($response->body());
384
    }
385
386
    /**
387
     * get a single transaction
388
     * @param  string $txhash transaction hash
389
     * @return array          associative array containing the response
390
     */
391
    public function transaction($txhash) {
392
        $response = $this->dataClient->get($this->converter->getUrlForTransaction($txhash));
393
        $res = $this->converter->convertTx($response->body(), null);
394
395
        if ($this->converter instanceof BtccomConverter) {
396
            $res['raw'] = \json_decode($this->dataClient->get("tx/{$txhash}/raw")->body(), true)['data'];
397
        }
398
399
        return $res;
400
    }
401
402
    /**
403
     * get a single transaction
404
     * @param  string[] $txhashes list of transaction hashes (up to 20)
405
     * @return array[]            array containing the response
406
     */
407
    public function transactions($txhashes) {
408
        $response = $this->dataClient->get($this->converter->getUrlForTransactions($txhashes));
409
        return $this->converter->convertTxs($response->body());
410
    }
411
    
412
    /**
413
     * get a paginated list of all webhooks associated with the api user
414
     * @param  integer          $page    pagination: page number
415
     * @param  integer          $limit   pagination: records per page
416
     * @return array                     associative array containing the response
417
     */
418
    public function allWebhooks($page = 1, $limit = 20) {
419
        $queryString = [
420
            'page' => $page,
421
            'limit' => $limit
422
        ];
423
        $response = $this->blocktrailClient->get("webhooks", $this->converter->paginationParams($queryString));
424
        return self::jsonDecode($response->body(), true);
425
    }
426
427
    /**
428
     * get an existing webhook by it's identifier
429
     * @param string    $identifier     a unique identifier associated with the webhook
430
     * @return array                    associative array containing the response
431
     */
432
    public function getWebhook($identifier) {
433
        $response = $this->blocktrailClient->get("webhook/".$identifier);
434
        return self::jsonDecode($response->body(), true);
435
    }
436
437
    /**
438
     * create a new webhook
439
     * @param  string  $url        the url to receive the webhook events
440
     * @param  string  $identifier a unique identifier to associate with this webhook
441
     * @return array               associative array containing the response
442
     */
443
    public function setupWebhook($url, $identifier = null) {
444
        $postData = [
445
            'url'        => $url,
446
            'identifier' => $identifier
447
        ];
448
        $response = $this->blocktrailClient->post("webhook", null, $postData, RestClient::AUTH_HTTP_SIG);
449
        return self::jsonDecode($response->body(), true);
450
    }
451
452
    /**
453
     * update an existing webhook
454
     * @param  string  $identifier      the unique identifier of the webhook to update
455
     * @param  string  $newUrl          the new url to receive the webhook events
456
     * @param  string  $newIdentifier   a new unique identifier to associate with this webhook
457
     * @return array                    associative array containing the response
458
     */
459
    public function updateWebhook($identifier, $newUrl = null, $newIdentifier = null) {
460
        $putData = [
461
            'url'        => $newUrl,
462
            'identifier' => $newIdentifier
463
        ];
464
        $response = $this->blocktrailClient->put("webhook/{$identifier}", null, $putData, RestClient::AUTH_HTTP_SIG);
465
        return self::jsonDecode($response->body(), true);
466
    }
467
468
    /**
469
     * deletes an existing webhook and any event subscriptions associated with it
470
     * @param  string  $identifier      the unique identifier of the webhook to delete
471
     * @return boolean                  true on success
472
     */
473
    public function deleteWebhook($identifier) {
474
        $response = $this->blocktrailClient->delete("webhook/{$identifier}", null, null, RestClient::AUTH_HTTP_SIG);
475
        return self::jsonDecode($response->body(), true);
476
    }
477
478
    /**
479
     * get a paginated list of all the events a webhook is subscribed to
480
     * @param  string  $identifier  the unique identifier of the webhook
481
     * @param  integer $page        pagination: page number
482
     * @param  integer $limit       pagination: records per page
483
     * @return array                associative array containing the response
484
     */
485
    public function getWebhookEvents($identifier, $page = 1, $limit = 20) {
486
        $queryString = [
487
            'page' => $page,
488
            'limit' => $limit
489
        ];
490
        $response = $this->blocktrailClient->get("webhook/{$identifier}/events", $this->converter->paginationParams($queryString));
491
        return self::jsonDecode($response->body(), true);
492
    }
493
    
494
    /**
495
     * subscribes a webhook to transaction events of one particular transaction
496
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
497
     * @param  string  $transaction     the transaction hash
498
     * @param  integer $confirmations   the amount of confirmations to send.
499
     * @return array                    associative array containing the response
500
     */
501
    public function subscribeTransaction($identifier, $transaction, $confirmations = 6) {
502
        $postData = [
503
            'event_type'    => 'transaction',
504
            'transaction'   => $transaction,
505
            'confirmations' => $confirmations,
506
        ];
507
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
508
        return self::jsonDecode($response->body(), true);
509
    }
510
511
    /**
512
     * subscribes a webhook to transaction events on a particular address
513
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
514
     * @param  string  $address         the address hash
515
     * @param  integer $confirmations   the amount of confirmations to send.
516
     * @return array                    associative array containing the response
517
     */
518
    public function subscribeAddressTransactions($identifier, $address, $confirmations = 6) {
519
        $postData = [
520
            'event_type'    => 'address-transactions',
521
            'address'       => $address,
522
            'confirmations' => $confirmations,
523
        ];
524
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
525
        return self::jsonDecode($response->body(), true);
526
    }
527
528
    /**
529
     * batch subscribes a webhook to multiple transaction events
530
     *
531
     * @param  string $identifier   the unique identifier of the webhook
532
     * @param  array  $batchData    A 2D array of event data:
533
     *                              [address => $address, confirmations => $confirmations]
534
     *                              where $address is the address to subscibe to
535
     *                              and optionally $confirmations is the amount of confirmations
536
     * @return boolean              true on success
537
     */
538
    public function batchSubscribeAddressTransactions($identifier, $batchData) {
539
        $postData = [];
540
        foreach ($batchData as $record) {
541
            $postData[] = [
542
                'event_type' => 'address-transactions',
543
                'address' => $record['address'],
544
                'confirmations' => isset($record['confirmations']) ? $record['confirmations'] : 6,
545
            ];
546
        }
547
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events/batch", null, $postData, RestClient::AUTH_HTTP_SIG);
548
        return self::jsonDecode($response->body(), true);
549
    }
550
551
    /**
552
     * subscribes a webhook to a new block event
553
     * @param  string  $identifier  the unique identifier of the webhook to be triggered
554
     * @return array                associative array containing the response
555
     */
556
    public function subscribeNewBlocks($identifier) {
557
        $postData = [
558
            'event_type'    => 'block',
559
        ];
560
        $response = $this->blocktrailClient->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
561
        return self::jsonDecode($response->body(), true);
562
    }
563
564
    /**
565
     * removes an transaction event subscription from a webhook
566
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
567
     * @param  string  $transaction     the transaction hash of the event subscription
568
     * @return boolean                  true on success
569
     */
570
    public function unsubscribeTransaction($identifier, $transaction) {
571
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/transaction/{$transaction}", null, null, RestClient::AUTH_HTTP_SIG);
572
        return self::jsonDecode($response->body(), true);
573
    }
574
575
    /**
576
     * removes an address transaction event subscription from a webhook
577
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
578
     * @param  string  $address         the address hash of the event subscription
579
     * @return boolean                  true on success
580
     */
581
    public function unsubscribeAddressTransactions($identifier, $address) {
582
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/address-transactions/{$address}", null, null, RestClient::AUTH_HTTP_SIG);
583
        return self::jsonDecode($response->body(), true);
584
    }
585
586
    /**
587
     * removes a block event subscription from a webhook
588
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
589
     * @return boolean                  true on success
590
     */
591
    public function unsubscribeNewBlocks($identifier) {
592
        $response = $this->blocktrailClient->delete("webhook/{$identifier}/block", null, null, RestClient::AUTH_HTTP_SIG);
593
        return self::jsonDecode($response->body(), true);
594
    }
595
596
    /**
597
     * create a new wallet
598
     *   - will generate a new primary seed (with password) and backup seed (without password)
599
     *   - send the primary seed (BIP39 'encrypted') and backup public key to the server
600
     *   - receive the blocktrail co-signing public key from the server
601
     *
602
     * Either takes one argument:
603
     * @param array $options
604
     *
605
     * Or takes three arguments (old, deprecated syntax):
606
     * (@nonPHP-doc) @param      $identifier
607
     * (@nonPHP-doc) @param      $password
608
     * (@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...
609
     *
610
     * @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...
611
     * @throws \Exception
612
     */
613
    public function createNewWallet($options) {
614
        if (!is_array($options)) {
615
            $args = func_get_args();
616
            $options = [
617
                "identifier" => $args[0],
618
                "password" => $args[1],
619
                "key_index" => isset($args[2]) ? $args[2] : null,
620
            ];
621
        }
622
623
        if (isset($options['password'])) {
624
            if (isset($options['passphrase'])) {
625
                throw new \InvalidArgumentException("Can only provide either passphrase or password");
626
            } else {
627
                $options['passphrase'] = $options['password'];
628
            }
629
        }
630
631
        if (!isset($options['passphrase'])) {
632
            $options['passphrase'] = null;
633
        }
634
635
        if (!isset($options['key_index'])) {
636
            $options['key_index'] = 0;
637
        }
638
639
        if (!isset($options['wallet_version'])) {
640
            $options['wallet_version'] = Wallet::WALLET_VERSION_V3;
641
        }
642
643
        switch ($options['wallet_version']) {
644
            case Wallet::WALLET_VERSION_V1:
645
                return $this->createNewWalletV1($options);
646
647
            case Wallet::WALLET_VERSION_V2:
648
                return $this->createNewWalletV2($options);
649
650
            case Wallet::WALLET_VERSION_V3:
651
                return $this->createNewWalletV3($options);
652
653
            default:
654
                throw new \InvalidArgumentException("Invalid wallet version");
655
        }
656
    }
657
658
    protected function createNewWalletV1($options) {
659
        $walletPath = WalletPath::create($options['key_index']);
660
661
        $storePrimaryMnemonic = isset($options['store_primary_mnemonic']) ? $options['store_primary_mnemonic'] : null;
662
663
        if (isset($options['primary_mnemonic']) && isset($options['primary_private_key'])) {
664
            throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey");
665
        }
666
667
        $primaryMnemonic = null;
668
        $primaryPrivateKey = null;
669
        if (!isset($options['primary_mnemonic']) && !isset($options['primary_private_key'])) {
670
            if (!$options['passphrase']) {
671
                throw new \InvalidArgumentException("Can't generate Primary Mnemonic without a passphrase");
672
            } else {
673
                // create new primary seed
674
                /** @var HierarchicalKey $primaryPrivateKey */
675
                list($primaryMnemonic, , $primaryPrivateKey) = $this->newV1PrimarySeed($options['passphrase']);
676
                if ($storePrimaryMnemonic !== false) {
677
                    $storePrimaryMnemonic = true;
678
                }
679
            }
680
        } elseif (isset($options['primary_mnemonic'])) {
681
            $primaryMnemonic = $options['primary_mnemonic'];
682
        } elseif (isset($options['primary_private_key'])) {
683
            $primaryPrivateKey = $options['primary_private_key'];
684
        }
685
686
        if ($storePrimaryMnemonic && $primaryMnemonic && !$options['passphrase']) {
687
            throw new \InvalidArgumentException("Can't store Primary Mnemonic on server without a passphrase");
688
        }
689
690
        if ($primaryPrivateKey) {
691
            if (is_string($primaryPrivateKey)) {
692
                $primaryPrivateKey = [$primaryPrivateKey, "m"];
693
            }
694
        } else {
695
            $primaryPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($primaryMnemonic, $options['passphrase']));
696
        }
697
698
        if (!$storePrimaryMnemonic) {
699
            $primaryMnemonic = false;
700
        }
701
702
        // create primary public key from the created private key
703
        $path = $walletPath->keyIndexPath()->publicPath();
704
        $primaryPublicKey = BIP32Key::create($primaryPrivateKey, "m")->buildKey($path);
705
706
        if (isset($options['backup_mnemonic']) && $options['backup_public_key']) {
707
            throw new \InvalidArgumentException("Can't specify Backup Mnemonic and Backup PublicKey");
708
        }
709
710
        $backupMnemonic = null;
711
        $backupPublicKey = null;
712
        if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) {
713
            /** @var HierarchicalKey $backupPrivateKey */
714
            list($backupMnemonic, , ) = $this->newV1BackupSeed();
715
        } else if (isset($options['backup_mnemonic'])) {
716
            $backupMnemonic = $options['backup_mnemonic'];
717
        } elseif (isset($options['backup_public_key'])) {
718
            $backupPublicKey = $options['backup_public_key'];
719
        }
720
721
        if ($backupPublicKey) {
722
            if (is_string($backupPublicKey)) {
723
                $backupPublicKey = [$backupPublicKey, "m"];
724
            }
725
        } else {
726
            $backupPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, ""));
727
            $backupPublicKey = BIP32Key::create($backupPrivateKey->toPublic(), "M");
728
        }
729
730
        // create a checksum of our private key which we'll later use to verify we used the right password
731
        $checksum = $primaryPrivateKey->getPublicKey()->getAddress()->getAddress();
732
        $addressReader = $this->makeAddressReader($options);
733
734
        // send the public keys to the server to store them
735
        //  and the mnemonic, which is safe because it's useless without the password
736
        $data = $this->storeNewWalletV1(
737
            $options['identifier'],
738
            $primaryPublicKey->tuple(),
739
            $backupPublicKey->tuple(),
740
            $primaryMnemonic,
741
            $checksum,
742
            $options['key_index'],
743
            array_key_exists('segwit', $options) ? $options['segwit'] : false
744
        );
745
746
        // received the blocktrail public keys
747
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
748
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
749
        }, $data['blocktrail_public_keys']);
750
751
        $wallet = new WalletV1(
752
            $this,
753
            $options['identifier'],
754
            $primaryMnemonic,
755
            [$options['key_index'] => $primaryPublicKey],
756
            $backupPublicKey,
757
            $blocktrailPublicKeys,
758
            $options['key_index'],
759
            $this->network,
760
            $this->testnet,
761
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
762
            $addressReader,
763
            $checksum
764
        );
765
766
        $wallet->unlock($options);
767
768
        // return wallet and backup mnemonic
769
        return [
770
            $wallet,
771
            [
772
                'primary_mnemonic' => $primaryMnemonic,
773
                'backup_mnemonic' => $backupMnemonic,
774
                'blocktrail_public_keys' => $blocktrailPublicKeys,
775
            ],
776
        ];
777
    }
778
779
    public function randomBits($bits) {
780
        return $this->randomBytes($bits / 8);
781
    }
782
783
    public function randomBytes($bytes) {
784
        return (new Random())->bytes($bytes)->getBinary();
785
    }
786
787
    protected function createNewWalletV2($options) {
788
        $walletPath = WalletPath::create($options['key_index']);
789
790
        if (isset($options['store_primary_mnemonic'])) {
791
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
792
        }
793
794
        if (!isset($options['store_data_on_server'])) {
795
            if (isset($options['primary_private_key'])) {
796
                $options['store_data_on_server'] = false;
797
            } else {
798
                $options['store_data_on_server'] = true;
799
            }
800
        }
801
802
        $storeDataOnServer = $options['store_data_on_server'];
803
804
        $secret = null;
805
        $encryptedSecret = null;
806
        $primarySeed = null;
807
        $encryptedPrimarySeed = null;
808
        $recoverySecret = null;
809
        $recoveryEncryptedSecret = null;
810
        $backupSeed = null;
811
812
        if (!isset($options['primary_private_key'])) {
813
            $primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : $this->newV2PrimarySeed();
814
        }
815
816
        if ($storeDataOnServer) {
817
            if (!isset($options['secret'])) {
818
                if (!$options['passphrase']) {
819
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
820
                }
821
822
                list($secret, $encryptedSecret) = $this->newV2Secret($options['passphrase']);
823
            } else {
824
                $secret = $options['secret'];
825
            }
826
827
            $encryptedPrimarySeed = $this->newV2EncryptedPrimarySeed($primarySeed, $secret);
828
            list($recoverySecret, $recoveryEncryptedSecret) = $this->newV2RecoverySecret($secret);
829
        }
830
831
        if (!isset($options['backup_public_key'])) {
832
            $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : $this->newV2BackupSeed();
833
        }
834
835
        if (isset($options['primary_private_key'])) {
836
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
837
        } else {
838
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m");
839
        }
840
841
        // create primary public key from the created private key
842
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
843
844
        if (!isset($options['backup_public_key'])) {
845
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M");
846
        }
847
848
        // create a checksum of our private key which we'll later use to verify we used the right password
849
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
850
        $addressReader = $this->makeAddressReader($options);
851
852
        // send the public keys and encrypted data to server
853
        $data = $this->storeNewWalletV2(
854
            $options['identifier'],
855
            $options['primary_public_key']->tuple(),
856
            $options['backup_public_key']->tuple(),
857
            $storeDataOnServer ? $encryptedPrimarySeed : false,
858
            $storeDataOnServer ? $encryptedSecret : false,
859
            $storeDataOnServer ? $recoverySecret : false,
860
            $checksum,
861
            $options['key_index'],
862
            array_key_exists('segwit', $options) ? $options['segwit'] : false
863
        );
864
865
        // received the blocktrail public keys
866
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
867
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
868
        }, $data['blocktrail_public_keys']);
869
870
        $wallet = new WalletV2(
871
            $this,
872
            $options['identifier'],
873
            $encryptedPrimarySeed,
874
            $encryptedSecret,
875
            [$options['key_index'] => $options['primary_public_key']],
876
            $options['backup_public_key'],
877
            $blocktrailPublicKeys,
878
            $options['key_index'],
879
            $this->network,
880
            $this->testnet,
881
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
882
            $addressReader,
883
            $checksum
884
        );
885
886
        $wallet->unlock([
887
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
888
            'primary_private_key' => $options['primary_private_key'],
889
            'primary_seed' => $primarySeed,
890
            'secret' => $secret,
891
        ]);
892
893
        // return wallet and mnemonics for backup sheet
894
        return [
895
            $wallet,
896
            [
897
                'encrypted_primary_seed' => $encryptedPrimarySeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedPrimarySeed))) : null,
898
                'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer($backupSeed)) : null,
899
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($recoveryEncryptedSecret))) : null,
900
                'encrypted_secret' => $encryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedSecret))) : null,
901
                'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
902
                    return [$keyIndex, $pubKey->tuple()];
903
                }, $blocktrailPublicKeys),
904
            ],
905
        ];
906
    }
907
908
    protected function createNewWalletV3($options) {
909
        $walletPath = WalletPath::create($options['key_index']);
910
911
        if (isset($options['store_primary_mnemonic'])) {
912
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
913
        }
914
915
        if (!isset($options['store_data_on_server'])) {
916
            if (isset($options['primary_private_key'])) {
917
                $options['store_data_on_server'] = false;
918
            } else {
919
                $options['store_data_on_server'] = true;
920
            }
921
        }
922
923
        $storeDataOnServer = $options['store_data_on_server'];
924
925
        $secret = null;
926
        $encryptedSecret = null;
927
        $primarySeed = null;
928
        $encryptedPrimarySeed = null;
929
        $recoverySecret = null;
930
        $recoveryEncryptedSecret = null;
931
        $backupSeed = null;
932
933
        if (!isset($options['primary_private_key'])) {
934
            if (isset($options['primary_seed'])) {
935
                if (!$options['primary_seed'] instanceof BufferInterface) {
936
                    throw new \InvalidArgumentException('Primary Seed should be passed as a Buffer');
937
                }
938
                $primarySeed = $options['primary_seed'];
939
            } else {
940
                $primarySeed = $this->newV3PrimarySeed();
941
            }
942
        }
943
944
        if ($storeDataOnServer) {
945
            if (!isset($options['secret'])) {
946
                if (!$options['passphrase']) {
947
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
948
                }
949
950
                list($secret, $encryptedSecret) = $this->newV3Secret($options['passphrase']);
951
            } else {
952
                if (!$options['secret'] instanceof Buffer) {
953
                    throw new \InvalidArgumentException('Secret must be provided as a Buffer');
954
                }
955
956
                $secret = $options['secret'];
957
            }
958
959
            $encryptedPrimarySeed = $this->newV3EncryptedPrimarySeed($primarySeed, $secret);
960
            list($recoverySecret, $recoveryEncryptedSecret) = $this->newV3RecoverySecret($secret);
961
        }
962
963
        if (!isset($options['backup_public_key'])) {
964
            if (isset($options['backup_seed'])) {
965
                if (!$options['backup_seed'] instanceof Buffer) {
966
                    throw new \InvalidArgumentException('Backup seed must be an instance of Buffer');
967
                }
968
                $backupSeed = $options['backup_seed'];
969
            } else {
970
                $backupSeed = $this->newV3BackupSeed();
971
            }
972
        }
973
974
        if (isset($options['primary_private_key'])) {
975
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
976
        } else {
977
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m");
978
        }
979
980
        // create primary public key from the created private key
981
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
982
983
        if (!isset($options['backup_public_key'])) {
984
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($backupSeed), "m")->buildKey("M");
985
        }
986
987
        // create a checksum of our private key which we'll later use to verify we used the right password
988
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
989
        $addressReader = $this->makeAddressReader($options);
990
991
        // send the public keys and encrypted data to server
992
        $data = $this->storeNewWalletV3(
993
            $options['identifier'],
994
            $options['primary_public_key']->tuple(),
995
            $options['backup_public_key']->tuple(),
996
            $storeDataOnServer ? base64_encode($encryptedPrimarySeed->getBinary()) : false,
997
            $storeDataOnServer ? base64_encode($encryptedSecret->getBinary()) : false,
998
            $storeDataOnServer ? $recoverySecret->getHex() : false,
999
            $checksum,
1000
            $options['key_index'],
1001
            array_key_exists('segwit', $options) ? $options['segwit'] : false
1002
        );
1003
1004
        // received the blocktrail public keys
1005
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
1006
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
1007
        }, $data['blocktrail_public_keys']);
1008
1009
        $wallet = new WalletV3(
1010
            $this,
1011
            $options['identifier'],
1012
            $encryptedPrimarySeed,
1013
            $encryptedSecret,
1014
            [$options['key_index'] => $options['primary_public_key']],
1015
            $options['backup_public_key'],
1016
            $blocktrailPublicKeys,
1017
            $options['key_index'],
1018
            $this->network,
1019
            $this->testnet,
1020
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
1021
            $addressReader,
1022
            $checksum
1023
        );
1024
1025
        $wallet->unlock([
1026
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
1027
            'primary_private_key' => $options['primary_private_key'],
1028
            'primary_seed' => $primarySeed,
1029
            'secret' => $secret,
1030
        ]);
1031
1032
        // return wallet and mnemonics for backup sheet
1033
        return [
1034
            $wallet,
1035
            [
1036
                'encrypted_primary_seed'    => $encryptedPrimarySeed ? EncryptionMnemonic::encode($encryptedPrimarySeed) : null,
1037
                'backup_seed'               => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic($backupSeed) : null,
1038
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? EncryptionMnemonic::encode($recoveryEncryptedSecret) : null,
1039
                'encrypted_secret'          => $encryptedSecret ? EncryptionMnemonic::encode($encryptedSecret) : null,
1040
                'blocktrail_public_keys'    => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
1041
                    return [$keyIndex, $pubKey->tuple()];
1042
                }, $blocktrailPublicKeys),
1043
            ]
1044
        ];
1045
    }
1046
1047
    public function newV2PrimarySeed() {
1048
        return $this->randomBits(256);
1049
    }
1050
1051
    public function newV2BackupSeed() {
1052
        return $this->randomBits(256);
1053
    }
1054
1055
    public function newV2Secret($passphrase) {
1056
        $secret = bin2hex($this->randomBits(256)); // string because we use it as passphrase
1057
        $encryptedSecret = CryptoJSAES::encrypt($secret, $passphrase);
1058
1059
        return [$secret, $encryptedSecret];
1060
    }
1061
1062
    public function newV2EncryptedPrimarySeed($primarySeed, $secret) {
1063
        return CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
1064
    }
1065
1066
    public function newV2RecoverySecret($secret) {
1067
        $recoverySecret = bin2hex($this->randomBits(256));
1068
        $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);
1069
1070
        return [$recoverySecret, $recoveryEncryptedSecret];
1071
    }
1072
1073
    public function newV3PrimarySeed() {
1074
        return new Buffer($this->randomBits(256));
1075
    }
1076
1077
    public function newV3BackupSeed() {
1078
        return new Buffer($this->randomBits(256));
1079
    }
1080
1081
    public function newV3Secret($passphrase) {
1082
        $secret = new Buffer($this->randomBits(256));
1083
        $encryptedSecret = Encryption::encrypt($secret, new Buffer($passphrase), KeyDerivation::DEFAULT_ITERATIONS)
1084
            ->getBuffer();
1085
1086
        return [$secret, $encryptedSecret];
1087
    }
1088
1089
    public function newV3EncryptedPrimarySeed(Buffer $primarySeed, Buffer $secret) {
1090
        return Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS)
1091
            ->getBuffer();
1092
    }
1093
1094
    public function newV3RecoverySecret(Buffer $secret) {
1095
        $recoverySecret = new Buffer($this->randomBits(256));
1096
        $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS)
1097
            ->getBuffer();
1098
1099
        return [$recoverySecret, $recoveryEncryptedSecret];
1100
    }
1101
1102
    /**
1103
     * @param array $bip32Key
1104
     * @throws BlocktrailSDKException
1105
     */
1106
    private function verifyPublicBIP32Key(array $bip32Key) {
1107
        $hk = HierarchicalKeyFactory::fromExtended($bip32Key[0]);
1108
        if ($hk->isPrivate()) {
1109
            throw new BlocktrailSDKException('Private key was included in request, abort');
1110
        }
1111
1112
        if (substr($bip32Key[1], 0, 1) === "m") {
1113
            throw new BlocktrailSDKException("Private path was included in the request, abort");
1114
        }
1115
    }
1116
1117
    /**
1118
     * @param array $walletData
1119
     * @throws BlocktrailSDKException
1120
     */
1121
    private function verifyPublicOnly(array $walletData) {
1122
        $this->verifyPublicBIP32Key($walletData['primary_public_key']);
1123
        $this->verifyPublicBIP32Key($walletData['backup_public_key']);
1124
    }
1125
1126
    /**
1127
     * create wallet using the API
1128
     *
1129
     * @param string    $identifier             the wallet identifier to create
1130
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1131
     * @param array     $backupPublicKey        BIP32 extended public key - [backup key, path "M"]
1132
     * @param string    $primaryMnemonic        mnemonic to store
1133
     * @param string    $checksum               checksum to store
1134
     * @param int       $keyIndex               account that we expect to use
1135
     * @param bool      $segwit                 opt in to segwit
1136
     * @return mixed
1137
     */
1138
    public function storeNewWalletV1($identifier, $primaryPublicKey, $backupPublicKey, $primaryMnemonic, $checksum, $keyIndex, $segwit = false) {
1139
        $data = [
1140
            'identifier' => $identifier,
1141
            'primary_public_key' => $primaryPublicKey,
1142
            'backup_public_key' => $backupPublicKey,
1143
            'primary_mnemonic' => $primaryMnemonic,
1144
            'checksum' => $checksum,
1145
            'key_index' => $keyIndex,
1146
            'segwit' => $segwit,
1147
        ];
1148
        $this->verifyPublicOnly($data);
1149
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1150
        return self::jsonDecode($response->body(), true);
1151
    }
1152
1153
    /**
1154
     * create wallet using the API
1155
     *
1156
     * @param string $identifier       the wallet identifier to create
1157
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1158
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1159
     * @param        $encryptedPrimarySeed
1160
     * @param        $encryptedSecret
1161
     * @param        $recoverySecret
1162
     * @param string $checksum         checksum to store
1163
     * @param int    $keyIndex         account that we expect to use
1164
     * @param bool   $segwit           opt in to segwit
1165
     * @return mixed
1166
     * @throws \Exception
1167
     */
1168
    public function storeNewWalletV2($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1169
        $data = [
1170
            'identifier' => $identifier,
1171
            'wallet_version' => Wallet::WALLET_VERSION_V2,
1172
            'primary_public_key' => $primaryPublicKey,
1173
            'backup_public_key' => $backupPublicKey,
1174
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1175
            'encrypted_secret' => $encryptedSecret,
1176
            'recovery_secret' => $recoverySecret,
1177
            'checksum' => $checksum,
1178
            'key_index' => $keyIndex,
1179
            'segwit' => $segwit,
1180
        ];
1181
        $this->verifyPublicOnly($data);
1182
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1183
        return self::jsonDecode($response->body(), true);
1184
    }
1185
1186
    /**
1187
     * create wallet using the API
1188
     *
1189
     * @param string $identifier       the wallet identifier to create
1190
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1191
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1192
     * @param        $encryptedPrimarySeed
1193
     * @param        $encryptedSecret
1194
     * @param        $recoverySecret
1195
     * @param string $checksum         checksum to store
1196
     * @param int    $keyIndex         account that we expect to use
1197
     * @param bool   $segwit           opt in to segwit
1198
     * @return mixed
1199
     * @throws \Exception
1200
     */
1201
    public function storeNewWalletV3($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1202
1203
        $data = [
1204
            'identifier' => $identifier,
1205
            'wallet_version' => Wallet::WALLET_VERSION_V3,
1206
            'primary_public_key' => $primaryPublicKey,
1207
            'backup_public_key' => $backupPublicKey,
1208
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1209
            'encrypted_secret' => $encryptedSecret,
1210
            'recovery_secret' => $recoverySecret,
1211
            'checksum' => $checksum,
1212
            'key_index' => $keyIndex,
1213
            'segwit' => $segwit,
1214
        ];
1215
1216
        $this->verifyPublicOnly($data);
1217
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1218
        return self::jsonDecode($response->body(), true);
1219
    }
1220
1221
    /**
1222
     * upgrade wallet to use a new account number
1223
     *  the account number specifies which blocktrail cosigning key is used
1224
     *
1225
     * @param string    $identifier             the wallet identifier to be upgraded
1226
     * @param int       $keyIndex               the new account to use
1227
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1228
     * @return mixed
1229
     */
1230
    public function upgradeKeyIndex($identifier, $keyIndex, $primaryPublicKey) {
1231
        $data = [
1232
            'key_index' => $keyIndex,
1233
            'primary_public_key' => $primaryPublicKey
1234
        ];
1235
1236
        $response = $this->blocktrailClient->post("wallet/{$identifier}/upgrade", null, $data, RestClient::AUTH_HTTP_SIG);
1237
        return self::jsonDecode($response->body(), true);
1238
    }
1239
1240
    /**
1241
     * @param array $options
1242
     * @return AddressReaderBase
1243
     */
1244
    private function makeAddressReader(array $options) {
1245
        if ($this->network == "bitcoincash") {
1246
            $useCashAddress = false;
1247
            if (array_key_exists("use_cashaddress", $options) && $options['use_cashaddress']) {
1248
                $useCashAddress = true;
1249
            }
1250
            return new BitcoinCashAddressReader($useCashAddress);
1251
        } else {
1252
            return new BitcoinAddressReader();
1253
        }
1254
    }
1255
1256
    /**
1257
     * initialize a previously created wallet
1258
     *
1259
     * Takes an options object, or accepts identifier/password for backwards compatiblity.
1260
     *
1261
     * Some of the options:
1262
     *  - "readonly/readOnly/read-only" can be to a boolean value,
1263
     *    so the wallet is loaded in read-only mode (no private key)
1264
     *  - "check_backup_key" can be set to your own backup key:
1265
     *    Format: ["M', "xpub..."]
1266
     *    Setting this will allow the SDK to check the server hasn't
1267
     *    a different key (one it happens to control)
1268
1269
     * Either takes one argument:
1270
     * @param array $options
1271
     *
1272
     * Or takes two arguments (old, deprecated syntax):
1273
     * (@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...
1274
     * (@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...
1275
     *
1276
     * @return WalletInterface
1277
     * @throws \Exception
1278
     */
1279
    public function initWallet($options) {
1280
        if (!is_array($options)) {
1281
            $args = func_get_args();
1282
            $options = [
1283
                "identifier" => $args[0],
1284
                "password" => $args[1],
1285
            ];
1286
        }
1287
1288
        $identifier = $options['identifier'];
1289
        $readonly = isset($options['readonly']) ? $options['readonly'] :
1290
                    (isset($options['readOnly']) ? $options['readOnly'] :
1291
                        (isset($options['read-only']) ? $options['read-only'] :
1292
                            false));
1293
1294
        // get the wallet data from the server
1295
        $data = $this->getWallet($identifier);
1296
        if (!$data) {
1297
            throw new \Exception("Failed to get wallet");
1298
        }
1299
1300
        if (array_key_exists('check_backup_key', $options)) {
1301
            if (!is_string($options['check_backup_key'])) {
1302
                throw new \InvalidArgumentException("check_backup_key should be a string (the xpub)");
1303
            }
1304
            if ($options['check_backup_key'] !== $data['backup_public_key'][0]) {
1305
                throw new \InvalidArgumentException("Backup key returned from server didn't match our own");
1306
            }
1307
        }
1308
1309
        $addressReader = $this->makeAddressReader($options);
1310
1311
        switch ($data['wallet_version']) {
1312
            case Wallet::WALLET_VERSION_V1:
1313
                $wallet = new WalletV1(
1314
                    $this,
1315
                    $identifier,
1316
                    isset($options['primary_mnemonic']) ? $options['primary_mnemonic'] : $data['primary_mnemonic'],
1317
                    $data['primary_public_keys'],
1318
                    $data['backup_public_key'],
1319
                    $data['blocktrail_public_keys'],
1320
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1321
                    $this->network,
1322
                    $this->testnet,
1323
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1324
                    $addressReader,
1325
                    $data['checksum']
1326
                );
1327
                break;
1328
            case Wallet::WALLET_VERSION_V2:
1329
                $wallet = new WalletV2(
1330
                    $this,
1331
                    $identifier,
1332
                    isset($options['encrypted_primary_seed']) ? $options['encrypted_primary_seed'] : $data['encrypted_primary_seed'],
1333
                    isset($options['encrypted_secret']) ? $options['encrypted_secret'] : $data['encrypted_secret'],
1334
                    $data['primary_public_keys'],
1335
                    $data['backup_public_key'],
1336
                    $data['blocktrail_public_keys'],
1337
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1338
                    $this->network,
1339
                    $this->testnet,
1340
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1341
                    $addressReader,
1342
                    $data['checksum']
1343
                );
1344
                break;
1345
            case Wallet::WALLET_VERSION_V3:
1346
                if (isset($options['encrypted_primary_seed'])) {
1347
                    if (!$options['encrypted_primary_seed'] instanceof Buffer) {
1348
                        throw new \InvalidArgumentException('Encrypted PrimarySeed must be provided as a Buffer');
1349
                    }
1350
                    $encryptedPrimarySeed = $data['encrypted_primary_seed'];
1351
                } else {
1352
                    $encryptedPrimarySeed = new Buffer(base64_decode($data['encrypted_primary_seed']));
1353
                }
1354
1355
                if (isset($options['encrypted_secret'])) {
1356
                    if (!$options['encrypted_secret'] instanceof Buffer) {
1357
                        throw new \InvalidArgumentException('Encrypted secret must be provided as a Buffer');
1358
                    }
1359
1360
                    $encryptedSecret = $data['encrypted_secret'];
1361
                } else {
1362
                    $encryptedSecret = new Buffer(base64_decode($data['encrypted_secret']));
1363
                }
1364
1365
                $wallet = new WalletV3(
1366
                    $this,
1367
                    $identifier,
1368
                    $encryptedPrimarySeed,
1369
                    $encryptedSecret,
1370
                    $data['primary_public_keys'],
1371
                    $data['backup_public_key'],
1372
                    $data['blocktrail_public_keys'],
1373
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1374
                    $this->network,
1375
                    $this->testnet,
1376
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1377
                    $addressReader,
1378
                    $data['checksum']
1379
                );
1380
                break;
1381
            default:
1382
                throw new \InvalidArgumentException("Invalid wallet version");
1383
        }
1384
1385
        if (!$readonly) {
1386
            $wallet->unlock($options);
1387
        }
1388
1389
        return $wallet;
1390
    }
1391
1392
    /**
1393
     * get the wallet data from the server
1394
     *
1395
     * @param string    $identifier             the identifier of the wallet
1396
     * @return mixed
1397
     */
1398
    public function getWallet($identifier) {
1399
        $response = $this->blocktrailClient->get("wallet/{$identifier}", null, RestClient::AUTH_HTTP_SIG);
1400
        return self::jsonDecode($response->body(), true);
1401
    }
1402
1403
    /**
1404
     * update the wallet data on the server
1405
     *
1406
     * @param string    $identifier
1407
     * @param $data
1408
     * @return mixed
1409
     */
1410
    public function updateWallet($identifier, $data) {
1411
        $response = $this->blocktrailClient->post("wallet/{$identifier}", null, $data, RestClient::AUTH_HTTP_SIG);
1412
        return self::jsonDecode($response->body(), true);
1413
    }
1414
1415
    /**
1416
     * delete a wallet from the server
1417
     *  the checksum address and a signature to verify you ownership of the key of that checksum address
1418
     *  is required to be able to delete a wallet
1419
     *
1420
     * @param string    $identifier             the identifier of the wallet
1421
     * @param string    $checksumAddress        the address for your master private key (and the checksum used when creating the wallet)
1422
     * @param string    $signature              a signature of the checksum address as message signed by the private key matching that address
1423
     * @param bool      $force                  ignore warnings (such as a non-zero balance)
1424
     * @return mixed
1425
     */
1426
    public function deleteWallet($identifier, $checksumAddress, $signature, $force = false) {
1427
        $response = $this->blocktrailClient->delete("wallet/{$identifier}", ['force' => $force], [
1428
            'checksum' => $checksumAddress,
1429
            'signature' => $signature
1430
        ], RestClient::AUTH_HTTP_SIG, 360);
1431
        return self::jsonDecode($response->body(), true);
1432
    }
1433
1434
    /**
1435
     * create new backup key;
1436
     *  1) a BIP39 mnemonic
1437
     *  2) a seed from that mnemonic with a blank password
1438
     *  3) a private key from that seed
1439
     *
1440
     * @return array [mnemonic, seed, key]
1441
     */
1442
    protected function newV1BackupSeed() {
1443
        list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed("");
1444
1445
        return [$backupMnemonic, $backupSeed, $backupPrivateKey];
1446
    }
1447
1448
    /**
1449
     * create new primary key;
1450
     *  1) a BIP39 mnemonic
1451
     *  2) a seed from that mnemonic with the password
1452
     *  3) a private key from that seed
1453
     *
1454
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1455
     * @return array [mnemonic, seed, key]
1456
     * @TODO: require a strong password?
1457
     */
1458
    protected function newV1PrimarySeed($passphrase) {
1459
        list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase);
1460
1461
        return [$primaryMnemonic, $primarySeed, $primaryPrivateKey];
1462
    }
1463
1464
    /**
1465
     * create a new key;
1466
     *  1) a BIP39 mnemonic
1467
     *  2) a seed from that mnemonic with the password
1468
     *  3) a private key from that seed
1469
     *
1470
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1471
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1472
     * @return array
1473
     */
1474
    protected function generateNewSeed($passphrase = "", $forceEntropy = null) {
1475
        // generate master seed, retry if the generated private key isn't valid (FALSE is returned)
1476
        do {
1477
            $mnemonic = $this->generateNewMnemonic($forceEntropy);
1478
1479
            $seed = (new Bip39SeedGenerator)->getSeed($mnemonic, $passphrase);
1480
1481
            $key = null;
1482
            try {
1483
                $key = HierarchicalKeyFactory::fromEntropy($seed);
1484
            } catch (\Exception $e) {
1485
                // try again
1486
            }
1487
        } while (!$key);
1488
1489
        return [$mnemonic, $seed, $key];
1490
    }
1491
1492
    /**
1493
     * generate a new mnemonic from some random entropy (512 bit)
1494
     *
1495
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1496
     * @return string
1497
     * @throws \Exception
1498
     */
1499
    public function generateNewMnemonic($forceEntropy = null) {
1500
        if ($forceEntropy === null) {
1501
            $random = new Random();
1502
            $entropy = $random->bytes(512 / 8);
1503
        } else {
1504
            $entropy = $forceEntropy;
1505
        }
1506
1507
        return MnemonicFactory::bip39()->entropyToMnemonic($entropy);
0 ignored issues
show
Bug introduced by
It seems like $entropy defined by $forceEntropy on line 1504 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...
1508
    }
1509
1510
    /**
1511
     * get the balance for the wallet
1512
     *
1513
     * @param string    $identifier             the identifier of the wallet
1514
     * @return array
1515
     */
1516
    public function getWalletBalance($identifier) {
1517
        $response = $this->blocktrailClient->get("wallet/{$identifier}/balance", null, RestClient::AUTH_HTTP_SIG);
1518
        return self::jsonDecode($response->body(), true);
1519
    }
1520
1521
    /**
1522
     * get a new derivation number for specified parent path
1523
     *  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
1524
     *
1525
     * returns the path
1526
     *
1527
     * @param string    $identifier             the identifier of the wallet
1528
     * @param string    $path                   the parent path for which to get a new derivation
1529
     * @return string
1530
     */
1531
    public function getNewDerivation($identifier, $path) {
1532
        $result = $this->_getNewDerivation($identifier, $path);
1533
        return $result['path'];
1534
    }
1535
1536
    /**
1537
     * get a new derivation number for specified parent path
1538
     *  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
1539
     *
1540
     * @param string    $identifier             the identifier of the wallet
1541
     * @param string    $path                   the parent path for which to get a new derivation
1542
     * @return mixed
1543
     */
1544
    public function _getNewDerivation($identifier, $path) {
1545
        $response = $this->blocktrailClient->post("wallet/{$identifier}/path", null, ['path' => $path], RestClient::AUTH_HTTP_SIG);
1546
        return self::jsonDecode($response->body(), true);
1547
    }
1548
1549
    /**
1550
     * get the path (and redeemScript) to specified address
1551
     *
1552
     * @param string $identifier
1553
     * @param string $address
1554
     * @return array
1555
     * @throws \Exception
1556
     */
1557
    public function getPathForAddress($identifier, $address) {
1558
        $response = $this->blocktrailClient->post("wallet/{$identifier}/path_for_address", null, ['address' => $address], RestClient::AUTH_HTTP_SIG);
1559
        return self::jsonDecode($response->body(), true)['path'];
1560
    }
1561
1562
    /**
1563
     * send the transaction using the API
1564
     *
1565
     * @param string       $identifier     the identifier of the wallet
1566
     * @param string|array $rawTransaction raw hex of the transaction (should be partially signed)
1567
     * @param array        $paths          list of the paths that were used for the UTXO
1568
     * @param bool         $checkFee       let the server verify the fee after signing
1569
     * @param null         $twoFactorToken
1570
     * @return string                                the complete raw transaction
1571
     * @throws \Exception
1572
     */
1573
    public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee = false, $twoFactorToken = null) {
1574
        $data = [
1575
            'paths' => $paths,
1576
            'two_factor_token' => $twoFactorToken,
1577
        ];
1578
1579
        if (is_array($rawTransaction)) {
1580
            if (array_key_exists('base_transaction', $rawTransaction)
1581
            && array_key_exists('signed_transaction', $rawTransaction)) {
1582
                $data['base_transaction'] = $rawTransaction['base_transaction'];
1583
                $data['signed_transaction'] = $rawTransaction['signed_transaction'];
1584
            } else {
1585
                throw new \InvalidArgumentException("Invalid value for transaction. For segwit transactions, pass ['base_transaction' => '...', 'signed_transaction' => '...']");
1586
            }
1587
        } else {
1588
            $data['raw_transaction'] = $rawTransaction;
1589
        }
1590
1591
        // dynamic TTL for when we're signing really big transactions
1592
        $ttl = max(5.0, count($paths) * 0.25) + 4.0;
1593
1594
        $response = $this->blocktrailClient->post("wallet/{$identifier}/send", ['check_fee' => (int)!!$checkFee], $data, RestClient::AUTH_HTTP_SIG, $ttl);
1595
        $signed = self::jsonDecode($response->body(), true);
1596
1597
        if (!$signed['complete'] || $signed['complete'] == 'false') {
1598
            throw new \Exception("Failed to completely sign transaction");
1599
        }
1600
1601
        // create TX hash from the raw signed hex
1602
        return TransactionFactory::fromHex($signed['hex'])->getTxId()->getHex();
1603
    }
1604
1605
    /**
1606
     * use the API to get the best inputs to use based on the outputs
1607
     *
1608
     * the return array has the following format:
1609
     * [
1610
     *  "utxos" => [
1611
     *      [
1612
     *          "hash" => "<txHash>",
1613
     *          "idx" => "<index of the output of that <txHash>",
1614
     *          "scriptpubkey_hex" => "<scriptPubKey-hex>",
1615
     *          "value" => 32746327,
1616
     *          "address" => "1address",
1617
     *          "path" => "m/44'/1'/0'/0/13",
1618
     *          "redeem_script" => "<redeemScript-hex>",
1619
     *      ],
1620
     *  ],
1621
     *  "fee"   => 10000,
1622
     *  "change"=> 1010109201,
1623
     * ]
1624
     *
1625
     * @param string   $identifier              the identifier of the wallet
1626
     * @param array    $outputs                 the outputs you want to create - array[address => satoshi-value]
1627
     * @param bool     $lockUTXO                when TRUE the UTXOs selected will be locked for a few seconds
1628
     *                                          so you have some time to spend them without race-conditions
1629
     * @param bool     $allowZeroConf
1630
     * @param string   $feeStrategy
1631
     * @param null|int $forceFee
1632
     * @return array
1633
     * @throws \Exception
1634
     */
1635
    public function coinSelection($identifier, $outputs, $lockUTXO = false, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1636
        $args = [
1637
            'lock' => (int)!!$lockUTXO,
1638
            'zeroconf' => (int)!!$allowZeroConf,
1639
            'fee_strategy' => $feeStrategy,
1640
        ];
1641
1642
        if ($forceFee !== null) {
1643
            $args['forcefee'] = (int)$forceFee;
1644
        }
1645
1646
        $response = $this->blocktrailClient->post(
1647
            "wallet/{$identifier}/coin-selection",
1648
            $args,
1649
            $outputs,
1650
            RestClient::AUTH_HTTP_SIG
1651
        );
1652
1653
        \var_export(self::jsonDecode($response->body(), true));
1654
1655
        return self::jsonDecode($response->body(), true);
1656
    }
1657
1658
    /**
1659
     *
1660
     * @param string   $identifier the identifier of the wallet
1661
     * @param bool     $allowZeroConf
1662
     * @param string   $feeStrategy
1663
     * @param null|int $forceFee
1664
     * @param int      $outputCnt
1665
     * @return array
1666
     * @throws \Exception
1667
     */
1668
    public function walletMaxSpendable($identifier, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
1669
        $args = [
1670
            'zeroconf' => (int)!!$allowZeroConf,
1671
            'fee_strategy' => $feeStrategy,
1672
            'outputs' => $outputCnt,
1673
        ];
1674
1675
        if ($forceFee !== null) {
1676
            $args['forcefee'] = (int)$forceFee;
1677
        }
1678
1679
        $response = $this->blocktrailClient->get(
1680
            "wallet/{$identifier}/max-spendable",
1681
            $args,
1682
            RestClient::AUTH_HTTP_SIG
1683
        );
1684
1685
        return self::jsonDecode($response->body(), true);
1686
    }
1687
1688
    /**
1689
     * @return array        ['optimal_fee' => 10000, 'low_priority_fee' => 5000]
1690
     */
1691
    public function feePerKB() {
1692
        $response = $this->blocktrailClient->get("fee-per-kb");
1693
        return self::jsonDecode($response->body(), true);
1694
    }
1695
1696
    /**
1697
     * get the current price index
1698
     *
1699
     * @return array        eg; ['USD' => 287.30]
1700
     */
1701
    public function price() {
1702
        $response = $this->blocktrailClient->get("price");
1703
        return self::jsonDecode($response->body(), true);
1704
    }
1705
1706
    /**
1707
     * setup webhook for wallet
1708
     *
1709
     * @param string    $identifier         the wallet identifier for which to create the webhook
1710
     * @param string    $webhookIdentifier  the webhook identifier to use
1711
     * @param string    $url                the url to receive the webhook events
1712
     * @return array
1713
     */
1714
    public function setupWalletWebhook($identifier, $webhookIdentifier, $url) {
1715
        $response = $this->blocktrailClient->post("wallet/{$identifier}/webhook", null, ['url' => $url, 'identifier' => $webhookIdentifier], RestClient::AUTH_HTTP_SIG);
1716
        return self::jsonDecode($response->body(), true);
1717
    }
1718
1719
    /**
1720
     * delete webhook for wallet
1721
     *
1722
     * @param string    $identifier         the wallet identifier for which to delete the webhook
1723
     * @param string    $webhookIdentifier  the webhook identifier to delete
1724
     * @return array
1725
     */
1726
    public function deleteWalletWebhook($identifier, $webhookIdentifier) {
1727
        $response = $this->blocktrailClient->delete("wallet/{$identifier}/webhook/{$webhookIdentifier}", null, null, RestClient::AUTH_HTTP_SIG);
1728
        return self::jsonDecode($response->body(), true);
1729
    }
1730
1731
    /**
1732
     * lock a specific unspent output
1733
     *
1734
     * @param     $identifier
1735
     * @param     $txHash
1736
     * @param     $txIdx
1737
     * @param int $ttl
1738
     * @return bool
1739
     */
1740
    public function lockWalletUTXO($identifier, $txHash, $txIdx, $ttl = 3) {
1741
        $response = $this->blocktrailClient->post("wallet/{$identifier}/lock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx, 'ttl' => $ttl], RestClient::AUTH_HTTP_SIG);
1742
        return self::jsonDecode($response->body(), true)['locked'];
1743
    }
1744
1745
    /**
1746
     * unlock a specific unspent output
1747
     *
1748
     * @param     $identifier
1749
     * @param     $txHash
1750
     * @param     $txIdx
1751
     * @return bool
1752
     */
1753
    public function unlockWalletUTXO($identifier, $txHash, $txIdx) {
1754
        $response = $this->blocktrailClient->post("wallet/{$identifier}/unlock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx], RestClient::AUTH_HTTP_SIG);
1755
        return self::jsonDecode($response->body(), true)['unlocked'];
1756
    }
1757
1758
    /**
1759
     * get all transactions for wallet (paginated)
1760
     *
1761
     * @param  string  $identifier  the wallet identifier for which to get transactions
1762
     * @param  integer $page        pagination: page number
1763
     * @param  integer $limit       pagination: records per page (max 500)
1764
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1765
     * @return array                associative array containing the response
1766
     */
1767
    public function walletTransactions($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1768
        $queryString = [
1769
            'page' => $page,
1770
            'limit' => $limit,
1771
            'sort_dir' => $sortDir
1772
        ];
1773
        $response = $this->blocktrailClient->get("wallet/{$identifier}/transactions", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1774
        return self::jsonDecode($response->body(), true);
1775
    }
1776
1777
    /**
1778
     * get all addresses for wallet (paginated)
1779
     *
1780
     * @param  string  $identifier  the wallet identifier for which to get addresses
1781
     * @param  integer $page        pagination: page number
1782
     * @param  integer $limit       pagination: records per page (max 500)
1783
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1784
     * @return array                associative array containing the response
1785
     */
1786
    public function walletAddresses($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1787
        $queryString = [
1788
            'page' => $page,
1789
            'limit' => $limit,
1790
            'sort_dir' => $sortDir
1791
        ];
1792
        $response = $this->blocktrailClient->get("wallet/{$identifier}/addresses", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1793
        return self::jsonDecode($response->body(), true);
1794
    }
1795
1796
    /**
1797
     * get all UTXOs for wallet (paginated)
1798
     *
1799
     * @param  string  $identifier  the wallet identifier for which to get addresses
1800
     * @param  integer $page        pagination: page number
1801
     * @param  integer $limit       pagination: records per page (max 500)
1802
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1803
     * @param  boolean $zeroconf    include zero confirmation transactions
1804
     * @return array                associative array containing the response
1805
     */
1806
    public function walletUTXOs($identifier, $page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1807
        $queryString = [
1808
            'page' => $page,
1809
            'limit' => $limit,
1810
            'sort_dir' => $sortDir,
1811
            'zeroconf' => (int)!!$zeroconf,
1812
        ];
1813
        $response = $this->blocktrailClient->get("wallet/{$identifier}/utxos", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1814
        return self::jsonDecode($response->body(), true);
1815
    }
1816
1817
    /**
1818
     * get a paginated list of all wallets associated with the api user
1819
     *
1820
     * @param  integer          $page    pagination: page number
1821
     * @param  integer          $limit   pagination: records per page
1822
     * @return array                     associative array containing the response
1823
     */
1824
    public function allWallets($page = 1, $limit = 20) {
1825
        $queryString = [
1826
            'page' => $page,
1827
            'limit' => $limit
1828
        ];
1829
        $response = $this->blocktrailClient->get("wallets", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1830
        return self::jsonDecode($response->body(), true);
1831
    }
1832
1833
    /**
1834
     * send raw transaction
1835
     *
1836
     * @param     $txHex
1837
     * @return bool
1838
     */
1839
    public function sendRawTransaction($txHex) {
1840
        $response = $this->blocktrailClient->post("send-raw-tx", null, ['hex' => $txHex], RestClient::AUTH_HTTP_SIG);
1841
        return self::jsonDecode($response->body(), true);
1842
    }
1843
1844
    /**
1845
     * testnet only ;-)
1846
     *
1847
     * @param     $address
1848
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1849
     * @return mixed
1850
     * @throws \Exception
1851
     */
1852
    public function faucetWithdrawal($address, $amount = 10000) {
1853
        $response = $this->blocktrailClient->post("faucet/withdrawl", null, [
1854
            'address' => $address,
1855
            'amount' => $amount,
1856
        ], RestClient::AUTH_HTTP_SIG);
1857
        return self::jsonDecode($response->body(), true);
1858
    }
1859
1860
    /**
1861
     * Exists for BC. Remove at major bump.
1862
     *
1863
     * @see faucetWithdrawal
1864
     * @deprecated
1865
     * @param     $address
1866
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1867
     * @return mixed
1868
     * @throws \Exception
1869
     */
1870
    public function faucetWithdrawl($address, $amount = 10000) {
1871
        return $this->faucetWithdrawal($address, $amount);
1872
    }
1873
1874
    /**
1875
     * verify a message signed bitcoin-core style
1876
     *
1877
     * @param  string           $message
1878
     * @param  string           $address
1879
     * @param  string           $signature
1880
     * @return boolean
1881
     */
1882
    public function verifyMessage($message, $address, $signature) {
1883
        $adapter = Bitcoin::getEcAdapter();
1884
        $addr = \BitWasp\Bitcoin\Address\AddressFactory::fromString($address);
1885
        if (!$addr instanceof PayToPubKeyHashAddress) {
1886
            throw new \InvalidArgumentException('Can only verify a message with a pay-to-pubkey-hash address');
1887
        }
1888
1889
        /** @var CompactSignatureSerializerInterface $csSerializer */
1890
        $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...
1891
        $signedMessage = new SignedMessage($message, $csSerializer->parse(new Buffer(base64_decode($signature))));
1892
1893
        $signer = new MessageSigner($adapter);
1894
        return $signer->verify($signedMessage, $addr);
1895
    }
1896
1897
    /**
1898
     * Take a base58 or cashaddress, and return only
1899
     * the cash address.
1900
     * This function only works on bitcoin cash.
1901
     * @param string $input
1902
     * @return string
1903
     * @throws BlocktrailSDKException
1904
     */
1905
    public function getLegacyBitcoinCashAddress($input) {
1906
        if ($this->network === "bitcoincash") {
1907
            $address = $this
1908
                ->makeAddressReader([
1909
                    "use_cashaddress" => true
1910
                ])
1911
                ->fromString($input);
1912
1913
            if ($address instanceof CashAddress) {
1914
                $address = $address->getLegacyAddress();
1915
            }
1916
1917
            return $address->getAddress();
1918
        }
1919
1920
        throw new BlocktrailSDKException("Only request a legacy address when using bitcoin cash");
1921
    }
1922
1923
    /**
1924
     * convert a Satoshi value to a BTC value
1925
     *
1926
     * @param int       $satoshi
1927
     * @return float
1928
     */
1929 1
    public static function toBTC($satoshi) {
1930 1
        return bcdiv((int)(string)$satoshi, 100000000, 8);
1931
    }
1932
1933
    /**
1934
     * convert a Satoshi value to a BTC value and return it as a string
1935
1936
     * @param int       $satoshi
1937
     * @return string
1938
     */
1939
    public static function toBTCString($satoshi) {
1940
        return sprintf("%.8f", self::toBTC($satoshi));
1941
    }
1942
1943
    /**
1944
     * convert a BTC value to a Satoshi value
1945
     *
1946
     * @param float     $btc
1947
     * @return string
1948
     */
1949 1
    public static function toSatoshiString($btc) {
1950 1
        return bcmul(sprintf("%.8f", (float)$btc), 100000000, 0);
1951
    }
1952
1953
    /**
1954
     * convert a BTC value to a Satoshi value
1955
     *
1956
     * @param float     $btc
1957
     * @return string
1958
     */
1959 1
    public static function toSatoshi($btc) {
1960 1
        return (int)self::toSatoshiString($btc);
1961
    }
1962
1963
    /**
1964
     * json_decode helper that throws exceptions when it fails to decode
1965
     *
1966
     * @param      $json
1967
     * @param bool $assoc
1968
     * @return mixed
1969
     * @throws \Exception
1970
     */
1971
    public static function jsonDecode($json, $assoc = false) {
1972
        if (!$json) {
1973
            throw new \Exception("Can't json_decode empty string [{$json}]");
1974
        }
1975
1976
        $data = json_decode($json, $assoc);
1977
1978
        if ($data === null) {
1979
            throw new \Exception("Failed to json_decode [{$json}]");
1980
        }
1981
1982
        return $data;
1983
    }
1984
1985
    /**
1986
     * sort public keys for multisig script
1987
     *
1988
     * @param PublicKeyInterface[] $pubKeys
1989
     * @return PublicKeyInterface[]
1990
     */
1991
    public static function sortMultisigKeys(array $pubKeys) {
1992
        $result = array_values($pubKeys);
1993
        usort($result, function (PublicKeyInterface $a, PublicKeyInterface $b) {
1994
            $av = $a->getHex();
1995
            $bv = $b->getHex();
1996
            return $av == $bv ? 0 : $av > $bv ? 1 : -1;
1997
        });
1998
1999
        return $result;
2000
    }
2001
2002
    /**
2003
     * read and decode the json payload from a webhook's POST request.
2004
     *
2005
     * @param bool $returnObject    flag to indicate if an object or associative array should be returned
2006
     * @return mixed|null
2007
     * @throws \Exception
2008
     */
2009
    public static function getWebhookPayload($returnObject = false) {
2010
        $data = file_get_contents("php://input");
2011
        if ($data) {
2012
            return self::jsonDecode($data, !$returnObject);
2013
        } else {
2014
            return null;
2015
        }
2016
    }
2017
2018
    public static function normalizeBIP32KeyArray($keys) {
2019
        return Util::arrayMapWithIndex(function ($idx, $key) {
2020
            return [$idx, self::normalizeBIP32Key($key)];
2021
        }, $keys);
2022
    }
2023
2024
    /**
2025
     * @param array|BIP32Key $key
2026
     * @return BIP32Key
2027
     * @throws \Exception
2028
     */
2029
    public static function normalizeBIP32Key($key) {
2030
        if ($key instanceof BIP32Key) {
2031
            return $key;
2032
        }
2033
2034
        if (is_array($key) && count($key) === 2) {
2035
            $path = $key[1];
2036
            $hk = $key[0];
2037
2038
            if (!($hk instanceof HierarchicalKey)) {
2039
                $hk = HierarchicalKeyFactory::fromExtended($hk);
2040
            }
2041
2042
            return BIP32Key::create($hk, $path);
2043
        } else {
2044
            throw new \Exception("Bad Input");
2045
        }
2046
    }
2047
2048
    public function shuffle($arr) {
2049
        \shuffle($arr);
2050
    }
2051
}
2052