Completed
Pull Request — master (#126)
by thomas
09:18
created

BlocktrailSDK::getWalletBlockLatest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1.037

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 4
ccs 2
cts 3
cp 0.6667
crap 1.037
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://api.blocktrail.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 === "BCC" ? "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
104 119
        $this->converter = new BtccomConverter();
105 119
    }
106
107
    /**
108
     * normalize network string
109
     *
110
     * @param $network
111
     * @param $testnet
112
     * @return array
113
     * @throws \Exception
114
     */
115 119
    protected function normalizeNetwork($network, $testnet) {
116
        // [name, testnet, network]
117 119
        return Util::normalizeNetwork($network, $testnet);
118
    }
119
120
    /**
121
     * set BitcoinLib to the correct magic-byte defaults for the selected network
122
     *
123
     * @param $network
124
     * @param bool $testnet
125
     * @param bool $regtest
126
     */
127 119
    protected function setBitcoinLibMagicBytes($network, $testnet, $regtest) {
128
129 119
        if ($network === "bitcoin") {
130 119
            if ($regtest) {
131
                $useNetwork = NetworkFactory::bitcoinRegtest();
132 119
            } else if ($testnet) {
133 29
                $useNetwork = NetworkFactory::bitcoinTestnet();
134
            } else {
135 119
                $useNetwork = NetworkFactory::bitcoin();
136
            }
137 4
        } else if ($network === "bitcoincash") {
138 4
            if ($regtest) {
139
                $useNetwork = new BitcoinCashRegtest();
140 4
            } else if ($testnet) {
141 4
                $useNetwork = new BitcoinCashTestnet();
142
            } else {
143
                $useNetwork = new BitcoinCash();
144
            }
145
        }
146
147 119
        Bitcoin::setNetwork($useNetwork);
0 ignored issues
show
Bug introduced by
The variable $useNetwork does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

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

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

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

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

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

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

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

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

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

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
837
        } else {
838
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m");
839
        }
840
841
        // create primary public key from the created private key
842
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
843
844
        if (!isset($options['backup_public_key'])) {
845
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M");
846
        }
847
848
        // create a checksum of our private key which we'll later use to verify we used the right password
849
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
850
        $addressReader = $this->makeAddressReader($options);
851
852
        // send the public keys and encrypted data to server
853
        $data = $this->storeNewWalletV2(
854
            $options['identifier'],
855
            $options['primary_public_key']->tuple(),
856
            $options['backup_public_key']->tuple(),
857
            $storeDataOnServer ? $encryptedPrimarySeed : false,
858
            $storeDataOnServer ? $encryptedSecret : false,
859
            $storeDataOnServer ? $recoverySecret : false,
860
            $checksum,
861
            $options['key_index'],
862
            array_key_exists('segwit', $options) ? $options['segwit'] : false
863
        );
864
865
        // received the blocktrail public keys
866
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
867
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
868
        }, $data['blocktrail_public_keys']);
869
870
        $wallet = new WalletV2(
871
            $this,
872
            $options['identifier'],
873
            $encryptedPrimarySeed,
874
            $encryptedSecret,
875
            [$options['key_index'] => $options['primary_public_key']],
876
            $options['backup_public_key'],
877
            $blocktrailPublicKeys,
878
            $options['key_index'],
879
            $this->network,
880
            $this->testnet,
881
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
882
            $addressReader,
883
            $checksum
884
        );
885
886
        $wallet->unlock([
887
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
888
            'primary_private_key' => $options['primary_private_key'],
889
            'primary_seed' => $primarySeed,
890
            'secret' => $secret,
891
        ]);
892
893
        // return wallet and mnemonics for backup sheet
894
        return [
895
            $wallet,
896
            [
897
                'encrypted_primary_seed' => $encryptedPrimarySeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedPrimarySeed))) : null,
898
                'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer($backupSeed)) : null,
899
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($recoveryEncryptedSecret))) : null,
900
                'encrypted_secret' => $encryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedSecret))) : null,
901
                'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
902
                    return [$keyIndex, $pubKey->tuple()];
903
                }, $blocktrailPublicKeys),
904
            ],
905
        ];
906
    }
907
908
    protected function createNewWalletV3($options) {
909
        $walletPath = WalletPath::create($options['key_index']);
910
911
        if (isset($options['store_primary_mnemonic'])) {
912
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
913
        }
914
915
        if (!isset($options['store_data_on_server'])) {
916
            if (isset($options['primary_private_key'])) {
917
                $options['store_data_on_server'] = false;
918
            } else {
919
                $options['store_data_on_server'] = true;
920
            }
921
        }
922
923
        $storeDataOnServer = $options['store_data_on_server'];
924
925
        $secret = null;
926
        $encryptedSecret = null;
927
        $primarySeed = null;
928
        $encryptedPrimarySeed = null;
929
        $recoverySecret = null;
930
        $recoveryEncryptedSecret = null;
931
        $backupSeed = null;
932
933
        if (!isset($options['primary_private_key'])) {
934
            if (isset($options['primary_seed'])) {
935
                if (!$options['primary_seed'] instanceof BufferInterface) {
936
                    throw new \InvalidArgumentException('Primary Seed should be passed as a Buffer');
937
                }
938
                $primarySeed = $options['primary_seed'];
939
            } else {
940
                $primarySeed = new Buffer(self::randomBits(256));
941
            }
942
        }
943
944
        if ($storeDataOnServer) {
945
            if (!isset($options['secret'])) {
946
                if (!$options['passphrase']) {
947
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
948
                }
949
950
                $secret = new Buffer(self::randomBits(256));
951
                $encryptedSecret = Encryption::encrypt($secret, new Buffer($options['passphrase']), KeyDerivation::DEFAULT_ITERATIONS)
952
                    ->getBuffer();
953
            } else {
954
                if (!$options['secret'] instanceof Buffer) {
955
                    throw new \RuntimeException('Secret must be provided as a Buffer');
956
                }
957
958
                $secret = $options['secret'];
959
            }
960
961
            $encryptedPrimarySeed = Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS)
962
                ->getBuffer();
963
            $recoverySecret = new Buffer(self::randomBits(256));
964
965
            $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS)
966
                ->getBuffer();
967
        }
968
969
        if (!isset($options['backup_public_key'])) {
970
            if (isset($options['backup_seed'])) {
971
                if (!$options['backup_seed'] instanceof Buffer) {
972
                    throw new \RuntimeException('Backup seed must be an instance of Buffer');
973
                }
974
                $backupSeed = $options['backup_seed'];
975
            } else {
976
                $backupSeed = new Buffer(self::randomBits(256));
977
            }
978
        }
979
980
        if (isset($options['primary_private_key'])) {
981
            $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...
982
        } else {
983
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m");
984
        }
985
986
        // create primary public key from the created private key
987
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
988
989
        if (!isset($options['backup_public_key'])) {
990
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($backupSeed), "m")->buildKey("M");
991
        }
992
993
        // create a checksum of our private key which we'll later use to verify we used the right password
994
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
995
        $addressReader = $this->makeAddressReader($options);
996
997
        // send the public keys and encrypted data to server
998
        $data = $this->storeNewWalletV3(
999
            $options['identifier'],
1000
            $options['primary_public_key']->tuple(),
1001
            $options['backup_public_key']->tuple(),
1002
            $storeDataOnServer ? base64_encode($encryptedPrimarySeed->getBinary()) : false,
1003
            $storeDataOnServer ? base64_encode($encryptedSecret->getBinary()) : false,
1004
            $storeDataOnServer ? $recoverySecret->getHex() : false,
1005
            $checksum,
1006
            $options['key_index'],
1007
            array_key_exists('segwit', $options) ? $options['segwit'] : false
1008
        );
1009
1010
        // received the blocktrail public keys
1011
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
1012
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
1013
        }, $data['blocktrail_public_keys']);
1014
1015
        $wallet = new WalletV3(
1016
            $this,
1017
            $options['identifier'],
1018
            $encryptedPrimarySeed,
0 ignored issues
show
Bug introduced by
It seems like $encryptedPrimarySeed defined by null on line 928 can be null; however, Blocktrail\SDK\WalletV3::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1019
            $encryptedSecret,
0 ignored issues
show
Bug introduced by
It seems like $encryptedSecret defined by null on line 926 can be null; however, Blocktrail\SDK\WalletV3::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1020
            [$options['key_index'] => $options['primary_public_key']],
1021
            $options['backup_public_key'],
1022
            $blocktrailPublicKeys,
1023
            $options['key_index'],
1024
            $this->network,
1025
            $this->testnet,
1026
            array_key_exists('segwit', $data) ? $data['segwit'] : false,
1027
            $addressReader,
1028
            $checksum
1029
        );
1030
1031
        $wallet->unlock([
1032
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
1033
            'primary_private_key' => $options['primary_private_key'],
1034
            'primary_seed' => $primarySeed,
1035
            'secret' => $secret,
1036
        ]);
1037
1038
        // return wallet and mnemonics for backup sheet
1039
        return [
1040
            $wallet,
1041
            [
1042
                'encrypted_primary_seed'    => $encryptedPrimarySeed ? EncryptionMnemonic::encode($encryptedPrimarySeed) : null,
1043
                'backup_seed'               => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic($backupSeed) : null,
1044
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? EncryptionMnemonic::encode($recoveryEncryptedSecret) : null,
1045
                'encrypted_secret'          => $encryptedSecret ? EncryptionMnemonic::encode($encryptedSecret) : null,
1046
                'blocktrail_public_keys'    => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
1047
                    return [$keyIndex, $pubKey->tuple()];
1048
                }, $blocktrailPublicKeys),
1049
            ]
1050
        ];
1051
    }
1052
1053
    /**
1054
     * @param array $bip32Key
1055
     * @throws BlocktrailSDKException
1056
     */
1057 3
    private function verifyPublicBIP32Key(array $bip32Key) {
1058 3
        $hk = HierarchicalKeyFactory::fromExtended($bip32Key[0]);
1059 3
        if ($hk->isPrivate()) {
1060
            throw new BlocktrailSDKException('Private key was included in request, abort');
1061
        }
1062
1063 3
        if (substr($bip32Key[1], 0, 1) === "m") {
1064
            throw new BlocktrailSDKException("Private path was included in the request, abort");
1065
        }
1066 3
    }
1067
1068
    /**
1069
     * @param array $walletData
1070
     * @throws BlocktrailSDKException
1071
     */
1072 3
    private function verifyPublicOnly(array $walletData) {
1073 3
        $this->verifyPublicBIP32Key($walletData['primary_public_key']);
1074 3
        $this->verifyPublicBIP32Key($walletData['backup_public_key']);
1075 3
    }
1076
1077
    /**
1078
     * create wallet using the API
1079
     *
1080
     * @param string    $identifier             the wallet identifier to create
1081
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1082
     * @param array     $backupPublicKey        BIP32 extended public key - [backup key, path "M"]
1083
     * @param string    $primaryMnemonic        mnemonic to store
1084
     * @param string    $checksum               checksum to store
1085
     * @param int       $keyIndex               account that we expect to use
1086
     * @param bool      $segwit                 opt in to segwit
1087
     * @return mixed
1088
     */
1089
    public function storeNewWalletV1($identifier, $primaryPublicKey, $backupPublicKey, $primaryMnemonic, $checksum, $keyIndex, $segwit = false) {
1090
        $data = [
1091
            'identifier' => $identifier,
1092
            'primary_public_key' => $primaryPublicKey,
1093
            'backup_public_key' => $backupPublicKey,
1094
            'primary_mnemonic' => $primaryMnemonic,
1095
            'checksum' => $checksum,
1096
            'key_index' => $keyIndex,
1097
            'segwit' => $segwit,
1098
        ];
1099
        $this->verifyPublicOnly($data);
1100
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1101
        return self::jsonDecode($response->body(), true);
1102
    }
1103
1104
    /**
1105
     * create wallet using the API
1106
     *
1107
     * @param string $identifier       the wallet identifier to create
1108
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1109
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1110
     * @param        $encryptedPrimarySeed
1111
     * @param        $encryptedSecret
1112
     * @param        $recoverySecret
1113
     * @param string $checksum         checksum to store
1114
     * @param int    $keyIndex         account that we expect to use
1115
     * @param bool   $segwit           opt in to segwit
1116
     * @return mixed
1117
     * @throws \Exception
1118
     */
1119 3
    public function storeNewWalletV2($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1120
        $data = [
1121 3
            'identifier' => $identifier,
1122
            'wallet_version' => Wallet::WALLET_VERSION_V2,
1123 3
            'primary_public_key' => $primaryPublicKey,
1124 3
            'backup_public_key' => $backupPublicKey,
1125 3
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1126 3
            'encrypted_secret' => $encryptedSecret,
1127 3
            'recovery_secret' => $recoverySecret,
1128 3
            'checksum' => $checksum,
1129 3
            'key_index' => $keyIndex,
1130 3
            'segwit' => $segwit,
1131
        ];
1132 3
        $this->verifyPublicOnly($data);
1133 3
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1134
        return self::jsonDecode($response->body(), true);
1135
    }
1136
1137
    /**
1138
     * create wallet using the API
1139
     *
1140
     * @param string $identifier       the wallet identifier to create
1141
     * @param array  $primaryPublicKey BIP32 extended public key - [key, path]
1142
     * @param array  $backupPublicKey  BIP32 extended public key - [backup key, path "M"]
1143
     * @param        $encryptedPrimarySeed
1144
     * @param        $encryptedSecret
1145
     * @param        $recoverySecret
1146
     * @param string $checksum         checksum to store
1147
     * @param int    $keyIndex         account that we expect to use
1148
     * @param bool   $segwit           opt in to segwit
1149
     * @return mixed
1150
     * @throws \Exception
1151
     */
1152
    public function storeNewWalletV3($identifier, $primaryPublicKey, $backupPublicKey, $encryptedPrimarySeed, $encryptedSecret, $recoverySecret, $checksum, $keyIndex, $segwit = false) {
1153
1154
        $data = [
1155
            'identifier' => $identifier,
1156
            'wallet_version' => Wallet::WALLET_VERSION_V3,
1157
            'primary_public_key' => $primaryPublicKey,
1158
            'backup_public_key' => $backupPublicKey,
1159
            'encrypted_primary_seed' => $encryptedPrimarySeed,
1160
            'encrypted_secret' => $encryptedSecret,
1161
            'recovery_secret' => $recoverySecret,
1162
            'checksum' => $checksum,
1163
            'key_index' => $keyIndex,
1164
            'segwit' => $segwit,
1165
        ];
1166
1167
        $this->verifyPublicOnly($data);
1168
        $response = $this->blocktrailClient->post("wallet", null, $data, RestClient::AUTH_HTTP_SIG);
1169
        return self::jsonDecode($response->body(), true);
1170
    }
1171
1172
    /**
1173
     * upgrade wallet to use a new account number
1174
     *  the account number specifies which blocktrail cosigning key is used
1175
     *
1176
     * @param string    $identifier             the wallet identifier to be upgraded
1177
     * @param int       $keyIndex               the new account to use
1178
     * @param array     $primaryPublicKey       BIP32 extended public key - [key, path]
1179
     * @return mixed
1180
     */
1181
    public function upgradeKeyIndex($identifier, $keyIndex, $primaryPublicKey) {
1182
        $data = [
1183
            'key_index' => $keyIndex,
1184
            'primary_public_key' => $primaryPublicKey
1185
        ];
1186
1187
        $response = $this->blocktrailClient->post("wallet/{$identifier}/upgrade", null, $data, RestClient::AUTH_HTTP_SIG);
1188
        return self::jsonDecode($response->body(), true);
1189
    }
1190
1191
    /**
1192
     * @param array $options
1193
     * @return AddressReaderBase
1194
     */
1195
    private function makeAddressReader(array $options) {
1196
        if ($this->network == "bitcoincash") {
1197
            $useCashAddress = false;
1198
            if (array_key_exists("use_cashaddress", $options) && $options['use_cashaddress']) {
1199
                $useCashAddress = true;
1200
            }
1201
            return new BitcoinCashAddressReader($useCashAddress);
1202
        } else {
1203
            return new BitcoinAddressReader();
1204
        }
1205
    }
1206
1207
    /**
1208
     * initialize a previously created wallet
1209
     *
1210
     * Takes an options object, or accepts identifier/password for backwards compatiblity.
1211
     *
1212
     * Some of the options:
1213
     *  - "readonly/readOnly/read-only" can be to a boolean value,
1214
     *    so the wallet is loaded in read-only mode (no private key)
1215
     *  - "check_backup_key" can be set to your own backup key:
1216
     *    Format: ["M', "xpub..."]
1217
     *    Setting this will allow the SDK to check the server hasn't
1218
     *    a different key (one it happens to control)
1219
1220
     * Either takes one argument:
1221
     * @param array $options
1222
     *
1223
     * Or takes two arguments (old, deprecated syntax):
1224
     * (@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...
1225
     * (@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...
1226
     *
1227
     * @return WalletInterface
1228
     * @throws \Exception
1229
     */
1230 23
    public function initWallet($options) {
1231 23
        if (!is_array($options)) {
1232 1
            $args = func_get_args();
1233
            $options = [
1234 1
                "identifier" => $args[0],
1235 1
                "password" => $args[1],
1236
            ];
1237
        }
1238
1239 23
        $identifier = $options['identifier'];
1240 23
        $readonly = isset($options['readonly']) ? $options['readonly'] :
1241 23
                    (isset($options['readOnly']) ? $options['readOnly'] :
1242 23
                        (isset($options['read-only']) ? $options['read-only'] :
1243 23
                            false));
1244
1245
        // get the wallet data from the server
1246 23
        $data = $this->getWallet($identifier);
1247
        if (!$data) {
1248
            throw new \Exception("Failed to get wallet");
1249
        }
1250
1251
        if (array_key_exists('check_backup_key', $options)) {
1252
            if (!is_string($options['check_backup_key'])) {
1253
                throw new \RuntimeException("check_backup_key should be a string (the xpub)");
1254
            }
1255
            if ($options['check_backup_key'] !== $data['backup_public_key'][0]) {
1256
                throw new \RuntimeException("Backup key returned from server didn't match our own");
1257
            }
1258
        }
1259
1260
        $addressReader = $this->makeAddressReader($options);
1261
1262
        switch ($data['wallet_version']) {
1263
            case Wallet::WALLET_VERSION_V1:
1264
                $wallet = new WalletV1(
1265
                    $this,
1266
                    $identifier,
1267
                    isset($options['primary_mnemonic']) ? $options['primary_mnemonic'] : $data['primary_mnemonic'],
1268
                    $data['primary_public_keys'],
1269
                    $data['backup_public_key'],
1270
                    $data['blocktrail_public_keys'],
1271
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1272
                    $this->network,
1273
                    $this->testnet,
1274
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1275
                    $addressReader,
1276
                    $data['checksum']
1277
                );
1278
                break;
1279
            case Wallet::WALLET_VERSION_V2:
1280
                $wallet = new WalletV2(
1281
                    $this,
1282
                    $identifier,
1283
                    isset($options['encrypted_primary_seed']) ? $options['encrypted_primary_seed'] : $data['encrypted_primary_seed'],
1284
                    isset($options['encrypted_secret']) ? $options['encrypted_secret'] : $data['encrypted_secret'],
1285
                    $data['primary_public_keys'],
1286
                    $data['backup_public_key'],
1287
                    $data['blocktrail_public_keys'],
1288
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1289
                    $this->network,
1290
                    $this->testnet,
1291
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1292
                    $addressReader,
1293
                    $data['checksum']
1294
                );
1295
                break;
1296
            case Wallet::WALLET_VERSION_V3:
1297
                if (isset($options['encrypted_primary_seed'])) {
1298
                    if (!$options['encrypted_primary_seed'] instanceof Buffer) {
1299
                        throw new \InvalidArgumentException('Encrypted PrimarySeed must be provided as a Buffer');
1300
                    }
1301
                    $encryptedPrimarySeed = $data['encrypted_primary_seed'];
1302
                } else {
1303
                    $encryptedPrimarySeed = new Buffer(base64_decode($data['encrypted_primary_seed']));
1304
                }
1305
1306
                if (isset($options['encrypted_secret'])) {
1307
                    if (!$options['encrypted_secret'] instanceof Buffer) {
1308
                        throw new \InvalidArgumentException('Encrypted secret must be provided as a Buffer');
1309
                    }
1310
1311
                    $encryptedSecret = $data['encrypted_secret'];
1312
                } else {
1313
                    $encryptedSecret = new Buffer(base64_decode($data['encrypted_secret']));
1314
                }
1315
1316
                $wallet = new WalletV3(
1317
                    $this,
1318
                    $identifier,
1319
                    $encryptedPrimarySeed,
1320
                    $encryptedSecret,
1321
                    $data['primary_public_keys'],
1322
                    $data['backup_public_key'],
1323
                    $data['blocktrail_public_keys'],
1324
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1325
                    $this->network,
1326
                    $this->testnet,
1327
                    array_key_exists('segwit', $data) ? $data['segwit'] : false,
1328
                    $addressReader,
1329
                    $data['checksum']
1330
                );
1331
                break;
1332
            default:
1333
                throw new \InvalidArgumentException("Invalid wallet version");
1334
        }
1335
1336
        if (!$readonly) {
1337
            $wallet->unlock($options);
1338
        }
1339
1340
        return $wallet;
1341
    }
1342
1343
    /**
1344
     * get the wallet data from the server
1345
     *
1346
     * @param string    $identifier             the identifier of the wallet
1347
     * @return mixed
1348
     */
1349 23
    public function getWallet($identifier) {
1350 23
        $response = $this->blocktrailClient->get("wallet/{$identifier}", null, RestClient::AUTH_HTTP_SIG);
1351
        return self::jsonDecode($response->body(), true);
1352
    }
1353
1354
    /**
1355
     * update the wallet data on the server
1356
     *
1357
     * @param string    $identifier
1358
     * @param $data
1359
     * @return mixed
1360
     */
1361
    public function updateWallet($identifier, $data) {
1362
        $response = $this->blocktrailClient->post("wallet/{$identifier}", null, $data, RestClient::AUTH_HTTP_SIG);
1363
        return self::jsonDecode($response->body(), true);
1364
    }
1365
1366
    /**
1367
     * delete a wallet from the server
1368
     *  the checksum address and a signature to verify you ownership of the key of that checksum address
1369
     *  is required to be able to delete a wallet
1370
     *
1371
     * @param string    $identifier             the identifier of the wallet
1372
     * @param string    $checksumAddress        the address for your master private key (and the checksum used when creating the wallet)
1373
     * @param string    $signature              a signature of the checksum address as message signed by the private key matching that address
1374
     * @param bool      $force                  ignore warnings (such as a non-zero balance)
1375
     * @return mixed
1376
     */
1377
    public function deleteWallet($identifier, $checksumAddress, $signature, $force = false) {
1378
        $response = $this->blocktrailClient->delete("wallet/{$identifier}", ['force' => $force], [
1379
            'checksum' => $checksumAddress,
1380
            'signature' => $signature
1381
        ], RestClient::AUTH_HTTP_SIG, 360);
1382
        return self::jsonDecode($response->body(), true);
1383
    }
1384
1385
    /**
1386
     * create new backup key;
1387
     *  1) a BIP39 mnemonic
1388
     *  2) a seed from that mnemonic with a blank password
1389
     *  3) a private key from that seed
1390
     *
1391
     * @return array [mnemonic, seed, key]
1392
     */
1393
    protected function newBackupSeed() {
1394
        list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed("");
1395
1396
        return [$backupMnemonic, $backupSeed, $backupPrivateKey];
1397
    }
1398
1399
    /**
1400
     * create new primary key;
1401
     *  1) a BIP39 mnemonic
1402
     *  2) a seed from that mnemonic with the password
1403
     *  3) a private key from that seed
1404
     *
1405
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1406
     * @return array [mnemonic, seed, key]
1407
     * @TODO: require a strong password?
1408
     */
1409
    protected function newPrimarySeed($passphrase) {
1410
        list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase);
1411
1412
        return [$primaryMnemonic, $primarySeed, $primaryPrivateKey];
1413
    }
1414
1415
    /**
1416
     * create a new key;
1417
     *  1) a BIP39 mnemonic
1418
     *  2) a seed from that mnemonic with the password
1419
     *  3) a private key from that seed
1420
     *
1421
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1422
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1423
     * @return array
1424
     */
1425
    protected function generateNewSeed($passphrase = "", $forceEntropy = null) {
1426
        // generate master seed, retry if the generated private key isn't valid (FALSE is returned)
1427
        do {
1428
            $mnemonic = $this->generateNewMnemonic($forceEntropy);
1429
1430
            $seed = (new Bip39SeedGenerator)->getSeed($mnemonic, $passphrase);
1431
1432
            $key = null;
1433
            try {
1434
                $key = HierarchicalKeyFactory::fromEntropy($seed);
1435
            } catch (\Exception $e) {
1436
                // try again
1437
            }
1438
        } while (!$key);
1439
1440
        return [$mnemonic, $seed, $key];
1441
    }
1442
1443
    /**
1444
     * generate a new mnemonic from some random entropy (512 bit)
1445
     *
1446
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1447
     * @return string
1448
     * @throws \Exception
1449
     */
1450
    protected function generateNewMnemonic($forceEntropy = null) {
1451
        if ($forceEntropy === null) {
1452
            $random = new Random();
1453
            $entropy = $random->bytes(512 / 8);
1454
        } else {
1455
            $entropy = $forceEntropy;
1456
        }
1457
1458
        return MnemonicFactory::bip39()->entropyToMnemonic($entropy);
0 ignored issues
show
Bug introduced by
It seems like $entropy defined by $forceEntropy on line 1455 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...
1459
    }
1460
1461
    /**
1462
     * get the balance for the wallet
1463
     *
1464
     * @param string    $identifier             the identifier of the wallet
1465
     * @return array
1466
     */
1467
    public function getWalletBalance($identifier) {
1468
        $response = $this->blocktrailClient->get("wallet/{$identifier}/balance", null, RestClient::AUTH_HTTP_SIG);
1469
        return self::jsonDecode($response->body(), true);
1470
    }
1471
1472
    /**
1473
     * get a new derivation number for specified parent path
1474
     *  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
1475
     *
1476
     * returns the path
1477
     *
1478
     * @param string    $identifier             the identifier of the wallet
1479
     * @param string    $path                   the parent path for which to get a new derivation
1480
     * @return string
1481
     */
1482
    public function getNewDerivation($identifier, $path) {
1483
        $result = $this->_getNewDerivation($identifier, $path);
1484
        return $result['path'];
1485
    }
1486
1487
    /**
1488
     * get a new derivation number for specified parent path
1489
     *  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
1490
     *
1491
     * @param string    $identifier             the identifier of the wallet
1492
     * @param string    $path                   the parent path for which to get a new derivation
1493
     * @return mixed
1494
     */
1495
    public function _getNewDerivation($identifier, $path) {
1496
        $response = $this->blocktrailClient->post("wallet/{$identifier}/path", null, ['path' => $path], RestClient::AUTH_HTTP_SIG);
1497
        return self::jsonDecode($response->body(), true);
1498
    }
1499
1500
    /**
1501
     * get the path (and redeemScript) to specified address
1502
     *
1503
     * @param string $identifier
1504
     * @param string $address
1505
     * @return array
1506
     * @throws \Exception
1507
     */
1508
    public function getPathForAddress($identifier, $address) {
1509
        $response = $this->blocktrailClient->post("wallet/{$identifier}/path_for_address", null, ['address' => $address], RestClient::AUTH_HTTP_SIG);
1510
        return self::jsonDecode($response->body(), true)['path'];
1511
    }
1512
1513
    /**
1514
     * send the transaction using the API
1515
     *
1516
     * @param string       $identifier     the identifier of the wallet
1517
     * @param string|array $rawTransaction raw hex of the transaction (should be partially signed)
1518
     * @param array        $paths          list of the paths that were used for the UTXO
1519
     * @param bool         $checkFee       let the server verify the fee after signing
1520
     * @param null         $twoFactorToken
1521
     * @return string                                the complete raw transaction
1522
     * @throws \Exception
1523
     */
1524
    public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee = false, $twoFactorToken = null) {
1525
        $data = [
1526
            'paths' => $paths,
1527
            'two_factor_token' => $twoFactorToken,
1528
        ];
1529
1530
        if (is_array($rawTransaction)) {
1531
            if (array_key_exists('base_transaction', $rawTransaction)
1532
            && array_key_exists('signed_transaction', $rawTransaction)) {
1533
                $data['base_transaction'] = $rawTransaction['base_transaction'];
1534
                $data['signed_transaction'] = $rawTransaction['signed_transaction'];
1535
            } else {
1536
                throw new \RuntimeException("Invalid value for transaction. For segwit transactions, pass ['base_transaction' => '...', 'signed_transaction' => '...']");
1537
            }
1538
        } else {
1539
            $data['raw_transaction'] = $rawTransaction;
1540
        }
1541
1542
        // dynamic TTL for when we're signing really big transactions
1543
        $ttl = max(5.0, count($paths) * 0.25) + 4.0;
1544
1545
        $response = $this->blocktrailClient->post("wallet/{$identifier}/send", ['check_fee' => (int)!!$checkFee], $data, RestClient::AUTH_HTTP_SIG, $ttl);
1546
        $signed = self::jsonDecode($response->body(), true);
1547
1548
        if (!$signed['complete'] || $signed['complete'] == 'false') {
1549
            throw new \Exception("Failed to completely sign transaction");
1550
        }
1551
1552
        // create TX hash from the raw signed hex
1553
        return TransactionFactory::fromHex($signed['hex'])->getTxId()->getHex();
1554
    }
1555
1556
    /**
1557
     * use the API to get the best inputs to use based on the outputs
1558
     *
1559
     * the return array has the following format:
1560
     * [
1561
     *  "utxos" => [
1562
     *      [
1563
     *          "hash" => "<txHash>",
1564
     *          "idx" => "<index of the output of that <txHash>",
1565
     *          "scriptpubkey_hex" => "<scriptPubKey-hex>",
1566
     *          "value" => 32746327,
1567
     *          "address" => "1address",
1568
     *          "path" => "m/44'/1'/0'/0/13",
1569
     *          "redeem_script" => "<redeemScript-hex>",
1570
     *      ],
1571
     *  ],
1572
     *  "fee"   => 10000,
1573
     *  "change"=> 1010109201,
1574
     * ]
1575
     *
1576
     * @param string   $identifier              the identifier of the wallet
1577
     * @param array    $outputs                 the outputs you want to create - array[address => satoshi-value]
1578
     * @param bool     $lockUTXO                when TRUE the UTXOs selected will be locked for a few seconds
1579
     *                                          so you have some time to spend them without race-conditions
1580
     * @param bool     $allowZeroConf
1581
     * @param string   $feeStrategy
1582
     * @param null|int $forceFee
1583
     * @return array
1584
     * @throws \Exception
1585
     */
1586
    public function coinSelection($identifier, $outputs, $lockUTXO = false, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1587
        $args = [
1588
            'lock' => (int)!!$lockUTXO,
1589
            'zeroconf' => (int)!!$allowZeroConf,
1590
            'fee_strategy' => $feeStrategy,
1591
        ];
1592
1593
        if ($forceFee !== null) {
1594
            $args['forcefee'] = (int)$forceFee;
1595
        }
1596
1597
        $response = $this->blocktrailClient->post(
1598
            "wallet/{$identifier}/coin-selection",
1599
            $args,
1600
            $outputs,
1601
            RestClient::AUTH_HTTP_SIG
1602
        );
1603
1604
        return self::jsonDecode($response->body(), true);
1605
    }
1606
1607
    /**
1608
     *
1609
     * @param string   $identifier the identifier of the wallet
1610
     * @param bool     $allowZeroConf
1611
     * @param string   $feeStrategy
1612
     * @param null|int $forceFee
1613
     * @param int      $outputCnt
1614
     * @return array
1615
     * @throws \Exception
1616
     */
1617
    public function walletMaxSpendable($identifier, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
1618
        $args = [
1619
            'zeroconf' => (int)!!$allowZeroConf,
1620
            'fee_strategy' => $feeStrategy,
1621
            'outputs' => $outputCnt,
1622
        ];
1623
1624
        if ($forceFee !== null) {
1625
            $args['forcefee'] = (int)$forceFee;
1626
        }
1627
1628
        $response = $this->blocktrailClient->get(
1629
            "wallet/{$identifier}/max-spendable",
1630
            $args,
1631
            RestClient::AUTH_HTTP_SIG
1632
        );
1633
1634
        return self::jsonDecode($response->body(), true);
1635
    }
1636
1637
    /**
1638
     * @return array        ['optimal_fee' => 10000, 'low_priority_fee' => 5000]
1639
     */
1640
    public function feePerKB() {
1641
        $response = $this->blocktrailClient->get("fee-per-kb");
1642
        return self::jsonDecode($response->body(), true);
1643
    }
1644
1645
    /**
1646
     * get the current price index
1647
     *
1648
     * @return array        eg; ['USD' => 287.30]
1649
     */
1650 1
    public function price() {
1651 1
        $response = $this->blocktrailClient->get("price");
1652
        return self::jsonDecode($response->body(), true);
1653
    }
1654
1655
    /**
1656
     * setup webhook for wallet
1657
     *
1658
     * @param string    $identifier         the wallet identifier for which to create the webhook
1659
     * @param string    $webhookIdentifier  the webhook identifier to use
1660
     * @param string    $url                the url to receive the webhook events
1661
     * @return array
1662
     */
1663
    public function setupWalletWebhook($identifier, $webhookIdentifier, $url) {
1664
        $response = $this->blocktrailClient->post("wallet/{$identifier}/webhook", null, ['url' => $url, 'identifier' => $webhookIdentifier], RestClient::AUTH_HTTP_SIG);
1665
        return self::jsonDecode($response->body(), true);
1666
    }
1667
1668
    /**
1669
     * delete webhook for wallet
1670
     *
1671
     * @param string    $identifier         the wallet identifier for which to delete the webhook
1672
     * @param string    $webhookIdentifier  the webhook identifier to delete
1673
     * @return array
1674
     */
1675
    public function deleteWalletWebhook($identifier, $webhookIdentifier) {
1676
        $response = $this->blocktrailClient->delete("wallet/{$identifier}/webhook/{$webhookIdentifier}", null, null, RestClient::AUTH_HTTP_SIG);
1677
        return self::jsonDecode($response->body(), true);
1678
    }
1679
1680
    /**
1681
     * lock a specific unspent output
1682
     *
1683
     * @param     $identifier
1684
     * @param     $txHash
1685
     * @param     $txIdx
1686
     * @param int $ttl
1687
     * @return bool
1688
     */
1689
    public function lockWalletUTXO($identifier, $txHash, $txIdx, $ttl = 3) {
1690
        $response = $this->blocktrailClient->post("wallet/{$identifier}/lock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx, 'ttl' => $ttl], RestClient::AUTH_HTTP_SIG);
1691
        return self::jsonDecode($response->body(), true)['locked'];
1692
    }
1693
1694
    /**
1695
     * unlock a specific unspent output
1696
     *
1697
     * @param     $identifier
1698
     * @param     $txHash
1699
     * @param     $txIdx
1700
     * @return bool
1701
     */
1702
    public function unlockWalletUTXO($identifier, $txHash, $txIdx) {
1703
        $response = $this->blocktrailClient->post("wallet/{$identifier}/unlock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx], RestClient::AUTH_HTTP_SIG);
1704
        return self::jsonDecode($response->body(), true)['unlocked'];
1705
    }
1706
1707
    /**
1708
     * get all transactions for wallet (paginated)
1709
     *
1710
     * @param  string  $identifier  the wallet identifier for which to get transactions
1711
     * @param  integer $page        pagination: page number
1712
     * @param  integer $limit       pagination: records per page (max 500)
1713
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1714
     * @return array                associative array containing the response
1715
     */
1716
    public function walletTransactions($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1717
        $queryString = [
1718
            'page' => $page,
1719
            'limit' => $limit,
1720
            'sort_dir' => $sortDir
1721
        ];
1722
        $response = $this->blocktrailClient->get("wallet/{$identifier}/transactions", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1723
        return self::jsonDecode($response->body(), true);
1724
    }
1725
1726
    /**
1727
     * get all addresses for wallet (paginated)
1728
     *
1729
     * @param  string  $identifier  the wallet identifier for which to get addresses
1730
     * @param  integer $page        pagination: page number
1731
     * @param  integer $limit       pagination: records per page (max 500)
1732
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1733
     * @return array                associative array containing the response
1734
     */
1735
    public function walletAddresses($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1736
        $queryString = [
1737
            'page' => $page,
1738
            'limit' => $limit,
1739
            'sort_dir' => $sortDir
1740
        ];
1741
        $response = $this->blocktrailClient->get("wallet/{$identifier}/addresses", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1742
        return self::jsonDecode($response->body(), true);
1743
    }
1744
1745
    /**
1746
     * get all UTXOs for wallet (paginated)
1747
     *
1748
     * @param  string  $identifier  the wallet identifier for which to get addresses
1749
     * @param  integer $page        pagination: page number
1750
     * @param  integer $limit       pagination: records per page (max 500)
1751
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1752
     * @param  boolean $zeroconf    include zero confirmation transactions
1753
     * @return array                associative array containing the response
1754
     */
1755
    public function walletUTXOs($identifier, $page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1756
        $queryString = [
1757
            'page' => $page,
1758
            'limit' => $limit,
1759
            'sort_dir' => $sortDir,
1760
            'zeroconf' => (int)!!$zeroconf,
1761
        ];
1762
        $response = $this->blocktrailClient->get("wallet/{$identifier}/utxos", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1763
        return self::jsonDecode($response->body(), true);
1764
    }
1765
1766
    /**
1767
     * get a paginated list of all wallets associated with the api user
1768
     *
1769
     * @param  integer          $page    pagination: page number
1770
     * @param  integer          $limit   pagination: records per page
1771
     * @return array                     associative array containing the response
1772
     */
1773
    public function allWallets($page = 1, $limit = 20) {
1774
        $queryString = [
1775
            'page' => $page,
1776
            'limit' => $limit
1777
        ];
1778
        $response = $this->blocktrailClient->get("wallets", $this->converter->paginationParams($queryString), RestClient::AUTH_HTTP_SIG);
1779
        return self::jsonDecode($response->body(), true);
1780
    }
1781
1782
    /**
1783
     * send raw transaction
1784
     *
1785
     * @param     $txHex
1786
     * @return bool
1787
     */
1788
    public function sendRawTransaction($txHex) {
1789
        $response = $this->blocktrailClient->post("send-raw-tx", null, ['hex' => $txHex], RestClient::AUTH_HTTP_SIG);
1790
        return self::jsonDecode($response->body(), true);
1791
    }
1792
1793
    /**
1794
     * testnet only ;-)
1795
     *
1796
     * @param     $address
1797
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1798
     * @return mixed
1799
     * @throws \Exception
1800
     */
1801
    public function faucetWithdrawal($address, $amount = 10000) {
1802
        $response = $this->blocktrailClient->post("faucet/withdrawl", null, [
1803
            'address' => $address,
1804
            'amount' => $amount,
1805
        ], RestClient::AUTH_HTTP_SIG);
1806
        return self::jsonDecode($response->body(), true);
1807
    }
1808
1809
    /**
1810
     * Exists for BC. Remove at major bump.
1811
     *
1812
     * @see faucetWithdrawal
1813
     * @deprecated
1814
     * @param     $address
1815
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1816
     * @return mixed
1817
     * @throws \Exception
1818
     */
1819
    public function faucetWithdrawl($address, $amount = 10000) {
1820
        return $this->faucetWithdrawal($address, $amount);
1821
    }
1822
1823
    /**
1824
     * verify a message signed bitcoin-core style
1825
     *
1826
     * @param  string           $message
1827
     * @param  string           $address
1828
     * @param  string           $signature
1829
     * @return boolean
1830
     */
1831 2
    public function verifyMessage($message, $address, $signature) {
1832 2
        $adapter = Bitcoin::getEcAdapter();
1833 2
        $addr = \BitWasp\Bitcoin\Address\AddressFactory::fromString($address);
1834 2
        if (!$addr instanceof PayToPubKeyHashAddress) {
1835
            throw new \RuntimeException('Can only verify a message with a pay-to-pubkey-hash address');
1836
        }
1837
1838
        /** @var CompactSignatureSerializerInterface $csSerializer */
1839 2
        $csSerializer = EcSerializer::getSerializer(CompactSignatureSerializerInterface::class, $adapter);
0 ignored issues
show
Documentation introduced by
$adapter is of type object<BitWasp\Bitcoin\C...ter\EcAdapterInterface>, but the function expects a boolean|object<BitWasp\B...\Crypto\EcAdapter\true>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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