Completed
Pull Request — master (#127)
by thomas
87:16 queued 16:40
created

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