Completed
Pull Request — master (#113)
by thomas
20:21
created

BlocktrailSDK::batchAddressUnspentOutputs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 4
dl 0
loc 9
ccs 0
cts 6
cp 0
crap 2
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use Btccom\BitcoinCash\Address\AddressCreator as BitcoinCashAddressCreator;
6
use Btccom\BitcoinCash\Address\CashAddress;
7
use Btccom\BitcoinCash\Network\NetworkFactory as BitcoinCashNetworkFactory;
8
use BitWasp\Bitcoin\Address\AddressCreator as BitcoinAddressCreator;
9
use BitWasp\Bitcoin\Address\PayToPubKeyHashAddress;
10
use BitWasp\Bitcoin\Bitcoin;
11
use BitWasp\Bitcoin\Crypto\EcAdapter\EcSerializer;
12
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
13
use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\CompactSignatureSerializerInterface;
14
use BitWasp\Bitcoin\Crypto\Random\Random;
15
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKey;
16
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
17
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
18
use BitWasp\Bitcoin\MessageSigner\SignedMessage;
19
use BitWasp\Bitcoin\Mnemonic\Bip39\Bip39SeedGenerator;
20
use BitWasp\Bitcoin\Mnemonic\MnemonicFactory;
21
use BitWasp\Bitcoin\Network\NetworkFactory as BitcoinNetworkFactory;
22
use BitWasp\Bitcoin\Transaction\TransactionFactory;
23
use BitWasp\Buffertools\Buffer;
24
use BitWasp\Buffertools\BufferInterface;
25
use Blocktrail\CryptoJSAES\CryptoJSAES;
26
use BitWasp\Bitcoin\Address\BaseAddressCreator;
27
use Blocktrail\SDK\Bitcoin\BIP32Key;
28
use Blocktrail\SDK\Connection\RestClient;
29
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
30
use Blocktrail\SDK\Connection\RestClientInterface;
31
use Btccom\JustEncrypt\Encryption;
32
use Btccom\JustEncrypt\EncryptionMnemonic;
33
use Btccom\JustEncrypt\KeyDerivation;
34
35
/**
36
 * Class BlocktrailSDK
37
 */
38
class BlocktrailSDK implements BlocktrailSDKInterface {
39
    /**
40
     * @var Connection\RestClientInterface
41
     */
42
    protected $client;
43
44
    /**
45
     * @var string          currently only supporting; bitcoin
46
     */
47
    protected $network;
48
49
    /**
50
     * @var bool
51
     */
52
    protected $testnet;
53
54
    /**
55
     * @param   string      $apiKey         the API_KEY to use for authentication
56
     * @param   string      $apiSecret      the API_SECRET to use for authentication
57
     * @param   string      $network        the cryptocurrency 'network' to consume, eg BTC, LTC, etc
58
     * @param   bool        $testnet        testnet yes/no
59
     * @param   string      $apiVersion     the version of the API to consume
60
     * @param   null        $apiEndpoint    overwrite the endpoint used
61
     *                                       this will cause the $network, $testnet and $apiVersion to be ignored!
62
     */
63 118
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
64
65 118
        list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet);
66
67 118
        if (is_null($apiEndpoint)) {
68 118
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com";
69 118
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
70
        }
71
72
        // normalize network and set bitcoinlib to the right magic-bytes
73 118
        list($this->network, $this->testnet, $regtest) = $this->normalizeNetwork($network, $testnet);
74 118
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet, $regtest);
75
76 118
        $this->client = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
77 118
    }
78
79
    /**
80
     * normalize network string
81
     *
82
     * @param $network
83
     * @param $testnet
84
     * @return array
85
     * @throws \Exception
86
     */
87 118
    protected function normalizeNetwork($network, $testnet) {
88
        // [name, testnet, network]
89 118
        return Util::normalizeNetwork($network, $testnet);
90
    }
91
92
    /**
93
     * set BitcoinLib to the correct magic-byte defaults for the selected network
94
     *
95
     * @param $network
96
     * @param bool $testnet
97
     * @param bool $regtest
98
     */
99 118
    protected function setBitcoinLibMagicBytes($network, $testnet, $regtest) {
100
101 118
        if ($network === "bitcoin") {
102 118
            if ($regtest) {
103
                $useNetwork = BitcoinNetworkFactory::bitcoinRegtest();
104 118
            } else if ($testnet) {
105 29
                $useNetwork = BitcoinNetworkFactory::bitcoinTestnet();
106
            } else {
107 118
                $useNetwork = BitcoinNetworkFactory::bitcoin();
108
            }
109 4
        } else if ($network === "bitcoincash") {
110 4
            if ($regtest) {
111
                $useNetwork = BitcoinCashNetworkFactory::bitcoinCashRegtest();
112 4
            } else if ($testnet) {
113 4
                $useNetwork = BitcoinCashNetworkFactory::bitcoinCashTestnet();
114
            } else {
115
                $useNetwork = BitcoinCashNetworkFactory::bitcoinCash();
116
            }
117
        }
118
119 118
        Bitcoin::setNetwork($useNetwork);
0 ignored issues
show
Bug introduced by
The variable $useNetwork does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1767
1768 1
        $adapter = Bitcoin::getEcAdapter();
1769 1
        $addressCreator = new BitcoinAddressCreator();
1770 1
        $addr = $addressCreator->fromString($address);
1771 1
        if (!$addr instanceof PayToPubKeyHashAddress) {
1772
            throw new \RuntimeException('Can only verify a message with a pay-to-pubkey-hash address');
1773
        }
1774
1775
        /** @var CompactSignatureSerializerInterface $csSerializer */
1776 1
        $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...
1777 1
        $signedMessage = new SignedMessage($message, $csSerializer->parse(new Buffer(base64_decode($signature))));
1778
1779 1
        $signer = new MessageSigner($adapter);
1780 1
        return $signer->verify($signedMessage, $addr);
1781
    }
1782
1783
    /**
1784
     * Take a base58 or cashaddress, and return only
1785
     * the cash address.
1786
     * This function only works on bitcoin cash.
1787
     * @param string $input
1788
     * @return string
1789
     * @throws BlocktrailSDKException
1790
     */
1791 1
    public function getLegacyBitcoinCashAddress($input) {
1792 1
        if ($this->network === "bitcoincash") {
1793
            $address = $this
1794 1
                ->makeAddressReader([
1795 1
                    "use_cashaddress" => true
1796
                ])
1797 1
                ->fromString($input);
1798
1799 1
            if ($address instanceof CashAddress) {
1800 1
                $address = $address->getLegacyAddress();
1801
            }
1802
1803 1
            return $address->getAddress();
1804
        }
1805
1806
        throw new BlocktrailSDKException("Only request a legacy address when using bitcoin cash");
1807
    }
1808
1809
    /**
1810
     * convert a Satoshi value to a BTC value
1811
     *
1812
     * @param int       $satoshi
1813
     * @return float
1814
     */
1815
    public static function toBTC($satoshi) {
1816
        return bcdiv((int)(string)$satoshi, 100000000, 8);
1817
    }
1818
1819
    /**
1820
     * convert a Satoshi value to a BTC value and return it as a string
1821
1822
     * @param int       $satoshi
1823
     * @return string
1824
     */
1825
    public static function toBTCString($satoshi) {
1826
        return sprintf("%.8f", self::toBTC($satoshi));
1827
    }
1828
1829
    /**
1830
     * convert a BTC value to a Satoshi value
1831
     *
1832
     * @param float     $btc
1833
     * @return string
1834
     */
1835 12
    public static function toSatoshiString($btc) {
1836 12
        return bcmul(sprintf("%.8f", (float)$btc), 100000000, 0);
1837
    }
1838
1839
    /**
1840
     * convert a BTC value to a Satoshi value
1841
     *
1842
     * @param float     $btc
1843
     * @return string
1844
     */
1845 12
    public static function toSatoshi($btc) {
1846 12
        return (int)self::toSatoshiString($btc);
1847
    }
1848
1849
    /**
1850
     * json_decode helper that throws exceptions when it fails to decode
1851
     *
1852
     * @param      $json
1853
     * @param bool $assoc
1854
     * @return mixed
1855
     * @throws \Exception
1856
     */
1857 32
    protected static function jsonDecode($json, $assoc = false) {
1858 32
        if (!$json) {
1859
            throw new \Exception("Can't json_decode empty string [{$json}]");
1860
        }
1861
1862 32
        $data = json_decode($json, $assoc);
1863
1864 32
        if ($data === null) {
1865
            throw new \Exception("Failed to json_decode [{$json}]");
1866
        }
1867
1868 32
        return $data;
1869
    }
1870
1871
    /**
1872
     * sort public keys for multisig script
1873
     *
1874
     * @param PublicKeyInterface[] $pubKeys
1875
     * @return PublicKeyInterface[]
1876
     */
1877 18
    public static function sortMultisigKeys(array $pubKeys) {
1878 18
        $result = array_values($pubKeys);
1879
        usort($result, function (PublicKeyInterface $a, PublicKeyInterface $b) {
1880 18
            $av = $a->getHex();
1881 18
            $bv = $b->getHex();
1882 18
            return $av == $bv ? 0 : $av > $bv ? 1 : -1;
1883 18
        });
1884
1885 18
        return $result;
1886
    }
1887
1888
    /**
1889
     * read and decode the json payload from a webhook's POST request.
1890
     *
1891
     * @param bool $returnObject    flag to indicate if an object or associative array should be returned
1892
     * @return mixed|null
1893
     * @throws \Exception
1894
     */
1895
    public static function getWebhookPayload($returnObject = false) {
1896
        $data = file_get_contents("php://input");
1897
        if ($data) {
1898
            return self::jsonDecode($data, !$returnObject);
1899
        } else {
1900
            return null;
1901
        }
1902
    }
1903
1904
    public static function normalizeBIP32KeyArray($keys) {
1905 26
        return Util::arrayMapWithIndex(function ($idx, $key) {
1906 26
            return [$idx, self::normalizeBIP32Key($key)];
1907 26
        }, $keys);
1908
    }
1909
1910
    /**
1911
     * @param array|BIP32Key $key
1912
     * @return BIP32Key
1913
     * @throws \Exception
1914
     */
1915 26
    public static function normalizeBIP32Key($key) {
1916 26
        if ($key instanceof BIP32Key) {
1917 10
            return $key;
1918
        }
1919
1920 26
        if (is_array($key) && count($key) === 2) {
1921 26
            $path = $key[1];
1922 26
            $hk = $key[0];
1923
1924 26
            if (!($hk instanceof HierarchicalKey)) {
1925 26
                $hk = HierarchicalKeyFactory::fromExtended($hk);
1926
            }
1927
1928 26
            return BIP32Key::create($hk, $path);
1929
        } else {
1930
            throw new \Exception("Bad Input");
1931
        }
1932
    }
1933
}
1934