Completed
Pull Request — master (#84)
by thomas
22:17
created

BlocktrailSDK::lockWalletUTXO()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 4
dl 0
loc 4
ccs 0
cts 3
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Blocktrail\SDK;
4
5
use BitWasp\Bitcoin\Address\AddressFactory;
6
use BitWasp\Bitcoin\Address\PayToPubKeyHashAddress;
7
use BitWasp\Bitcoin\Bitcoin;
8
use BitWasp\Bitcoin\Crypto\EcAdapter\EcSerializer;
9
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
10
use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\CompactSignatureSerializerInterface;
11
use BitWasp\Bitcoin\Crypto\Random\Random;
12
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKey;
13
use BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory;
14
use BitWasp\Bitcoin\MessageSigner\MessageSigner;
15
use BitWasp\Bitcoin\MessageSigner\SignedMessage;
16
use BitWasp\Bitcoin\Mnemonic\Bip39\Bip39SeedGenerator;
17
use BitWasp\Bitcoin\Mnemonic\MnemonicFactory;
18
use BitWasp\Bitcoin\Network\NetworkFactory;
19
use BitWasp\Bitcoin\Transaction\TransactionFactory;
20
use BitWasp\Buffertools\Buffer;
21
use BitWasp\Buffertools\BufferInterface;
22
use Blocktrail\CryptoJSAES\CryptoJSAES;
23
use Blocktrail\SDK\Bitcoin\BIP32Key;
24
use Blocktrail\SDK\Connection\RestClient;
25
use Blocktrail\SDK\Exceptions\BlocktrailSDKException;
26
use Blocktrail\SDK\V3Crypt\Encryption;
27
use Blocktrail\SDK\V3Crypt\EncryptionMnemonic;
28
use Blocktrail\SDK\V3Crypt\KeyDerivation;
29
30
/**
31
 * Class BlocktrailSDK
32
 */
33
class BlocktrailSDK implements BlocktrailSDKInterface {
34
    /**
35
     * @var Connection\RestClient
36
     */
37
    protected $client;
38
39
    /**
40
     * @var string          currently only supporting; bitcoin
41
     */
42
    protected $network;
43
44
    /**
45
     * @var bool
46
     */
47
    protected $testnet;
48
49
    /**
50
     * @param   string      $apiKey         the API_KEY to use for authentication
51
     * @param   string      $apiSecret      the API_SECRET to use for authentication
52
     * @param   string      $network        the cryptocurrency 'network' to consume, eg BTC, LTC, etc
53
     * @param   bool        $testnet        testnet yes/no
54
     * @param   string      $apiVersion     the version of the API to consume
55
     * @param   null        $apiEndpoint    overwrite the endpoint used
56
     *                                       this will cause the $network, $testnet and $apiVersion to be ignored!
57
     */
58 30
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
59
60 30
        list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet);
61
62 30
        if (is_null($apiEndpoint)) {
63 30
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com";
64 30
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
65
        }
66
67
        // normalize network and set bitcoinlib to the right magic-bytes
68 30
        list($this->network, $this->testnet) = $this->normalizeNetwork($network, $testnet);
69 30
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet);
70
71 30
        $this->client = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
72 30
    }
73
74
    /**
75
     * normalize network string
76
     *
77
     * @param $network
78
     * @param $testnet
79
     * @return array
80
     * @throws \Exception
81
     */
82 30
    protected function normalizeNetwork($network, $testnet) {
83 30
        return Util::normalizeNetwork($network, $testnet);
84
    }
85
86
    /**
87
     * set BitcoinLib to the correct magic-byte defaults for the selected network
88
     *
89
     * @param $network
90
     * @param $testnet
91
     */
92 30
    protected function setBitcoinLibMagicBytes($network, $testnet) {
93 30
        assert($network == "bitcoin" || $network == "bitcoincash");
94 30
        Bitcoin::setNetwork($testnet ? NetworkFactory::bitcoinTestnet() : NetworkFactory::bitcoin());
95 30
    }
96
97
    /**
98
     * enable CURL debugging output
99
     *
100
     * @param   bool        $debug
101
     *
102
     * @codeCoverageIgnore
103
     */
104
    public function setCurlDebugging($debug = true) {
105
        $this->client->setCurlDebugging($debug);
106
    }
107
108
    /**
109
     * enable verbose errors
110
     *
111
     * @param   bool        $verboseErrors
112
     *
113
     * @codeCoverageIgnore
114
     */
115
    public function setVerboseErrors($verboseErrors = true) {
116
        $this->client->setVerboseErrors($verboseErrors);
117
    }
118
    
119
    /**
120
     * set cURL default option on Guzzle client
121
     * @param string    $key
122
     * @param bool      $value
123
     *
124
     * @codeCoverageIgnore
125
     */
126
    public function setCurlDefaultOption($key, $value) {
127
        $this->client->setCurlDefaultOption($key, $value);
128
    }
129
130
    /**
131
     * @return  RestClient
132
     */
133 2
    public function getRestClient() {
134 2
        return $this->client;
135
    }
136
137
    /**
138
     * get a single address
139
     * @param  string $address address hash
140
     * @return array           associative array containing the response
141
     */
142 1
    public function address($address) {
143 1
        $response = $this->client->get("address/{$address}");
144 1
        return self::jsonDecode($response->body(), true);
145
    }
146
147
    /**
148
     * get all transactions for an address (paginated)
149
     * @param  string  $address address hash
150
     * @param  integer $page    pagination: page number
151
     * @param  integer $limit   pagination: records per page (max 500)
152
     * @param  string  $sortDir pagination: sort direction (asc|desc)
153
     * @return array            associative array containing the response
154
     */
155 1
    public function addressTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
156
        $queryString = [
157 1
            'page' => $page,
158 1
            'limit' => $limit,
159 1
            'sort_dir' => $sortDir
160
        ];
161 1
        $response = $this->client->get("address/{$address}/transactions", $queryString);
162 1
        return self::jsonDecode($response->body(), true);
163
    }
164
165
    /**
166
     * get all unconfirmed transactions for an address (paginated)
167
     * @param  string  $address address hash
168
     * @param  integer $page    pagination: page number
169
     * @param  integer $limit   pagination: records per page (max 500)
170
     * @param  string  $sortDir pagination: sort direction (asc|desc)
171
     * @return array            associative array containing the response
172
     */
173 1
    public function addressUnconfirmedTransactions($address, $page = 1, $limit = 20, $sortDir = 'asc') {
174
        $queryString = [
175 1
            'page' => $page,
176 1
            'limit' => $limit,
177 1
            'sort_dir' => $sortDir
178
        ];
179 1
        $response = $this->client->get("address/{$address}/unconfirmed-transactions", $queryString);
180 1
        return self::jsonDecode($response->body(), true);
181
    }
182
183
    /**
184
     * get all unspent outputs for an address (paginated)
185
     * @param  string  $address address hash
186
     * @param  integer $page    pagination: page number
187
     * @param  integer $limit   pagination: records per page (max 500)
188
     * @param  string  $sortDir pagination: sort direction (asc|desc)
189
     * @return array            associative array containing the response
190
     */
191 1
    public function addressUnspentOutputs($address, $page = 1, $limit = 20, $sortDir = 'asc') {
192
        $queryString = [
193 1
            'page' => $page,
194 1
            'limit' => $limit,
195 1
            'sort_dir' => $sortDir
196
        ];
197 1
        $response = $this->client->get("address/{$address}/unspent-outputs", $queryString);
198 1
        return self::jsonDecode($response->body(), true);
199
    }
200
201
    /**
202
     * get all unspent outputs for a batch of addresses (paginated)
203
     *
204
     * @param  string[] $addresses
205
     * @param  integer  $page    pagination: page number
206
     * @param  integer  $limit   pagination: records per page (max 500)
207
     * @param  string   $sortDir pagination: sort direction (asc|desc)
208
     * @return array associative array containing the response
209
     * @throws \Exception
210
     */
211
    public function batchAddressUnspentOutputs($addresses, $page = 1, $limit = 20, $sortDir = 'asc') {
212
        $queryString = [
213
            'page' => $page,
214
            'limit' => $limit,
215
            'sort_dir' => $sortDir
216
        ];
217
        $response = $this->client->post("address/unspent-outputs", $queryString, ['addresses' => $addresses]);
218
        return self::jsonDecode($response->body(), true);
219
    }
220
221
    /**
222
     * verify ownership of an address
223
     * @param  string  $address     address hash
224
     * @param  string  $signature   a signed message (the address hash) using the private key of the address
225
     * @return array                associative array containing the response
226
     */
227 2
    public function verifyAddress($address, $signature) {
228 2
        $postData = ['signature' => $signature];
229
230 2
        $response = $this->client->post("address/{$address}/verify", null, $postData, RestClient::AUTH_HTTP_SIG);
231
232 2
        return self::jsonDecode($response->body(), true);
233
    }
234
235
    /**
236
     * get all blocks (paginated)
237
     * @param  integer $page    pagination: page number
238
     * @param  integer $limit   pagination: records per page
239
     * @param  string  $sortDir pagination: sort direction (asc|desc)
240
     * @return array            associative array containing the response
241
     */
242 1
    public function allBlocks($page = 1, $limit = 20, $sortDir = 'asc') {
243
        $queryString = [
244 1
            'page' => $page,
245 1
            'limit' => $limit,
246 1
            'sort_dir' => $sortDir
247
        ];
248 1
        $response = $this->client->get("all-blocks", $queryString);
249 1
        return self::jsonDecode($response->body(), true);
250
    }
251
252
    /**
253
     * get the latest block
254
     * @return array            associative array containing the response
255
     */
256 1
    public function blockLatest() {
257 1
        $response = $this->client->get("block/latest");
258 1
        return self::jsonDecode($response->body(), true);
259
    }
260
261
    /**
262
     * get an individual block
263
     * @param  string|integer $block    a block hash or a block height
264
     * @return array                    associative array containing the response
265
     */
266 1
    public function block($block) {
267 1
        $response = $this->client->get("block/{$block}");
268 1
        return self::jsonDecode($response->body(), true);
269
    }
270
271
    /**
272
     * get all transaction in a block (paginated)
273
     * @param  string|integer   $block   a block hash or a block height
274
     * @param  integer          $page    pagination: page number
275
     * @param  integer          $limit   pagination: records per page
276
     * @param  string           $sortDir pagination: sort direction (asc|desc)
277
     * @return array                     associative array containing the response
278
     */
279 1
    public function blockTransactions($block, $page = 1, $limit = 20, $sortDir = 'asc') {
280
        $queryString = [
281 1
            'page' => $page,
282 1
            'limit' => $limit,
283 1
            'sort_dir' => $sortDir
284
        ];
285 1
        $response = $this->client->get("block/{$block}/transactions", $queryString);
286 1
        return self::jsonDecode($response->body(), true);
287
    }
288
289
    /**
290
     * get a single transaction
291
     * @param  string $txhash transaction hash
292
     * @return array          associative array containing the response
293
     */
294 3
    public function transaction($txhash) {
295 3
        $response = $this->client->get("transaction/{$txhash}");
296 3
        return self::jsonDecode($response->body(), true);
297
    }
298
299
    /**
300
     * get a single transaction
301
     * @param  string[] $txhashes list of transaction hashes (up to 20)
302
     * @return array[]            array containing the response
303
     */
304
    public function transactions($txhashes) {
305
        $response = $this->client->get("transactions/" . implode(",", $txhashes));
306
        return self::jsonDecode($response->body(), true);
307
    }
308
    
309
    /**
310
     * get a paginated list of all webhooks associated with the api user
311
     * @param  integer          $page    pagination: page number
312
     * @param  integer          $limit   pagination: records per page
313
     * @return array                     associative array containing the response
314
     */
315 1
    public function allWebhooks($page = 1, $limit = 20) {
316
        $queryString = [
317 1
            'page' => $page,
318 1
            'limit' => $limit
319
        ];
320 1
        $response = $this->client->get("webhooks", $queryString);
321 1
        return self::jsonDecode($response->body(), true);
322
    }
323
324
    /**
325
     * get an existing webhook by it's identifier
326
     * @param string    $identifier     a unique identifier associated with the webhook
327
     * @return array                    associative array containing the response
328
     */
329 1
    public function getWebhook($identifier) {
330 1
        $response = $this->client->get("webhook/".$identifier);
331 1
        return self::jsonDecode($response->body(), true);
332
    }
333
334
    /**
335
     * create a new webhook
336
     * @param  string  $url        the url to receive the webhook events
337
     * @param  string  $identifier a unique identifier to associate with this webhook
338
     * @return array               associative array containing the response
339
     */
340 1
    public function setupWebhook($url, $identifier = null) {
341
        $postData = [
342 1
            'url'        => $url,
343 1
            'identifier' => $identifier
344
        ];
345 1
        $response = $this->client->post("webhook", null, $postData, RestClient::AUTH_HTTP_SIG);
346 1
        return self::jsonDecode($response->body(), true);
347
    }
348
349
    /**
350
     * update an existing webhook
351
     * @param  string  $identifier      the unique identifier of the webhook to update
352
     * @param  string  $newUrl          the new url to receive the webhook events
353
     * @param  string  $newIdentifier   a new unique identifier to associate with this webhook
354
     * @return array                    associative array containing the response
355
     */
356 1
    public function updateWebhook($identifier, $newUrl = null, $newIdentifier = null) {
357
        $putData = [
358 1
            'url'        => $newUrl,
359 1
            'identifier' => $newIdentifier
360
        ];
361 1
        $response = $this->client->put("webhook/{$identifier}", null, $putData, RestClient::AUTH_HTTP_SIG);
362 1
        return self::jsonDecode($response->body(), true);
363
    }
364
365
    /**
366
     * deletes an existing webhook and any event subscriptions associated with it
367
     * @param  string  $identifier      the unique identifier of the webhook to delete
368
     * @return boolean                  true on success
369
     */
370 1
    public function deleteWebhook($identifier) {
371 1
        $response = $this->client->delete("webhook/{$identifier}", null, null, RestClient::AUTH_HTTP_SIG);
372 1
        return self::jsonDecode($response->body(), true);
373
    }
374
375
    /**
376
     * get a paginated list of all the events a webhook is subscribed to
377
     * @param  string  $identifier  the unique identifier of the webhook
378
     * @param  integer $page        pagination: page number
379
     * @param  integer $limit       pagination: records per page
380
     * @return array                associative array containing the response
381
     */
382 2
    public function getWebhookEvents($identifier, $page = 1, $limit = 20) {
383
        $queryString = [
384 2
            'page' => $page,
385 2
            'limit' => $limit
386
        ];
387 2
        $response = $this->client->get("webhook/{$identifier}/events", $queryString);
388 2
        return self::jsonDecode($response->body(), true);
389
    }
390
    
391
    /**
392
     * subscribes a webhook to transaction events of one particular transaction
393
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
394
     * @param  string  $transaction     the transaction hash
395
     * @param  integer $confirmations   the amount of confirmations to send.
396
     * @return array                    associative array containing the response
397
     */
398 1
    public function subscribeTransaction($identifier, $transaction, $confirmations = 6) {
399
        $postData = [
400 1
            'event_type'    => 'transaction',
401 1
            'transaction'   => $transaction,
402 1
            'confirmations' => $confirmations,
403
        ];
404 1
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
405 1
        return self::jsonDecode($response->body(), true);
406
    }
407
408
    /**
409
     * subscribes a webhook to transaction events on a particular address
410
     * @param  string  $identifier      the unique identifier of the webhook to be triggered
411
     * @param  string  $address         the address hash
412
     * @param  integer $confirmations   the amount of confirmations to send.
413
     * @return array                    associative array containing the response
414
     */
415 1
    public function subscribeAddressTransactions($identifier, $address, $confirmations = 6) {
416
        $postData = [
417 1
            'event_type'    => 'address-transactions',
418 1
            'address'       => $address,
419 1
            'confirmations' => $confirmations,
420
        ];
421 1
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
422 1
        return self::jsonDecode($response->body(), true);
423
    }
424
425
    /**
426
     * batch subscribes a webhook to multiple transaction events
427
     *
428
     * @param  string $identifier   the unique identifier of the webhook
429
     * @param  array  $batchData    A 2D array of event data:
430
     *                              [address => $address, confirmations => $confirmations]
431
     *                              where $address is the address to subscibe to
432
     *                              and optionally $confirmations is the amount of confirmations
433
     * @return boolean              true on success
434
     */
435 1
    public function batchSubscribeAddressTransactions($identifier, $batchData) {
436 1
        $postData = [];
437 1
        foreach ($batchData as $record) {
438 1
            $postData[] = [
439 1
                'event_type' => 'address-transactions',
440 1
                'address' => $record['address'],
441 1
                'confirmations' => isset($record['confirmations']) ? $record['confirmations'] : 6,
442
            ];
443
        }
444 1
        $response = $this->client->post("webhook/{$identifier}/events/batch", null, $postData, RestClient::AUTH_HTTP_SIG);
445 1
        return self::jsonDecode($response->body(), true);
446
    }
447
448
    /**
449
     * subscribes a webhook to a new block event
450
     * @param  string  $identifier  the unique identifier of the webhook to be triggered
451
     * @return array                associative array containing the response
452
     */
453 1
    public function subscribeNewBlocks($identifier) {
454
        $postData = [
455 1
            'event_type'    => 'block',
456
        ];
457 1
        $response = $this->client->post("webhook/{$identifier}/events", null, $postData, RestClient::AUTH_HTTP_SIG);
458 1
        return self::jsonDecode($response->body(), true);
459
    }
460
461
    /**
462
     * removes an transaction event subscription from a webhook
463
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
464
     * @param  string  $transaction     the transaction hash of the event subscription
465
     * @return boolean                  true on success
466
     */
467 1
    public function unsubscribeTransaction($identifier, $transaction) {
468 1
        $response = $this->client->delete("webhook/{$identifier}/transaction/{$transaction}", null, null, RestClient::AUTH_HTTP_SIG);
469 1
        return self::jsonDecode($response->body(), true);
470
    }
471
472
    /**
473
     * removes an address transaction event subscription from a webhook
474
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
475
     * @param  string  $address         the address hash of the event subscription
476
     * @return boolean                  true on success
477
     */
478 1
    public function unsubscribeAddressTransactions($identifier, $address) {
479 1
        $response = $this->client->delete("webhook/{$identifier}/address-transactions/{$address}", null, null, RestClient::AUTH_HTTP_SIG);
480 1
        return self::jsonDecode($response->body(), true);
481
    }
482
483
    /**
484
     * removes a block event subscription from a webhook
485
     * @param  string  $identifier      the unique identifier of the webhook associated with the event subscription
486
     * @return boolean                  true on success
487
     */
488 1
    public function unsubscribeNewBlocks($identifier) {
489 1
        $response = $this->client->delete("webhook/{$identifier}/block", null, null, RestClient::AUTH_HTTP_SIG);
490 1
        return self::jsonDecode($response->body(), true);
491
    }
492
493
    /**
494
     * create a new wallet
495
     *   - will generate a new primary seed (with password) and backup seed (without password)
496
     *   - send the primary seed (BIP39 'encrypted') and backup public key to the server
497
     *   - receive the blocktrail co-signing public key from the server
498
     *
499
     * Either takes one argument:
500
     * @param array $options
501
     *
502
     * Or takes three arguments (old, deprecated syntax):
503
     * (@nonPHP-doc) @param      $identifier
504
     * (@nonPHP-doc) @param      $password
505
     * (@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...
506
     *
507
     * @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...
508
     * @throws \Exception
509
     */
510 7
    public function createNewWallet($options) {
511 7
        if (!is_array($options)) {
512 1
            $args = func_get_args();
513
            $options = [
514 1
                "identifier" => $args[0],
515 1
                "password" => $args[1],
516 1
                "key_index" => isset($args[2]) ? $args[2] : null,
517
            ];
518
        }
519
520 7
        if (isset($options['password'])) {
521 1
            if (isset($options['passphrase'])) {
522
                throw new \InvalidArgumentException("Can only provide either passphrase or password");
523
            } else {
524 1
                $options['passphrase'] = $options['password'];
525
            }
526
        }
527
528 7
        if (!isset($options['passphrase'])) {
529 1
            $options['passphrase'] = null;
530
        }
531
532 7
        if (!isset($options['key_index'])) {
533
            $options['key_index'] = 0;
534
        }
535
536 7
        if (!isset($options['wallet_version'])) {
537 3
            $options['wallet_version'] = Wallet::WALLET_VERSION_V3;
538
        }
539
540 7
        switch ($options['wallet_version']) {
541 7
            case Wallet::WALLET_VERSION_V1:
542 1
                return $this->createNewWalletV1($options);
543
544 6
            case Wallet::WALLET_VERSION_V2:
545 2
                return $this->createNewWalletV2($options);
546
547 4
            case Wallet::WALLET_VERSION_V3:
548 4
                return $this->createNewWalletV3($options);
549
550
            default:
551
                throw new \InvalidArgumentException("Invalid wallet version");
552
        }
553
    }
554
555 1
    protected function createNewWalletV1($options) {
556 1
        $walletPath = WalletPath::create($options['key_index']);
557
558 1
        $storePrimaryMnemonic = isset($options['store_primary_mnemonic']) ? $options['store_primary_mnemonic'] : null;
559
560 1
        if (isset($options['primary_mnemonic']) && isset($options['primary_private_key'])) {
561
            throw new \InvalidArgumentException("Can't specify Primary Mnemonic and Primary PrivateKey");
562
        }
563
564 1
        $primaryMnemonic = null;
565 1
        $primaryPrivateKey = null;
566 1
        if (!isset($options['primary_mnemonic']) && !isset($options['primary_private_key'])) {
567 1
            if (!$options['passphrase']) {
568
                throw new \InvalidArgumentException("Can't generate Primary Mnemonic without a passphrase");
569
            } else {
570
                // create new primary seed
571
                /** @var HierarchicalKey $primaryPrivateKey */
572 1
                list($primaryMnemonic, , $primaryPrivateKey) = $this->newPrimarySeed($options['passphrase']);
573 1
                if ($storePrimaryMnemonic !== false) {
574 1
                    $storePrimaryMnemonic = true;
575
                }
576
            }
577
        } elseif (isset($options['primary_mnemonic'])) {
578
            $primaryMnemonic = $options['primary_mnemonic'];
579
        } elseif (isset($options['primary_private_key'])) {
580
            $primaryPrivateKey = $options['primary_private_key'];
581
        }
582
583 1
        if ($storePrimaryMnemonic && $primaryMnemonic && !$options['passphrase']) {
584
            throw new \InvalidArgumentException("Can't store Primary Mnemonic on server without a passphrase");
585
        }
586
587 1
        if ($primaryPrivateKey) {
588 1
            if (is_string($primaryPrivateKey)) {
589 1
                $primaryPrivateKey = [$primaryPrivateKey, "m"];
590
            }
591
        } else {
592
            $primaryPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($primaryMnemonic, $options['passphrase']));
593
        }
594
595 1
        if (!$storePrimaryMnemonic) {
596
            $primaryMnemonic = false;
597
        }
598
599
        // create primary public key from the created private key
600 1
        $path = $walletPath->keyIndexPath()->publicPath();
601 1
        $primaryPublicKey = BIP32Key::create($primaryPrivateKey, "m")->buildKey($path);
602
603 1
        if (isset($options['backup_mnemonic']) && $options['backup_public_key']) {
604
            throw new \InvalidArgumentException("Can't specify Backup Mnemonic and Backup PublicKey");
605
        }
606
607 1
        $backupMnemonic = null;
608 1
        $backupPublicKey = null;
609 1
        if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) {
610
            /** @var HierarchicalKey $backupPrivateKey */
611 1
            list($backupMnemonic, , ) = $this->newBackupSeed();
612
        } else if (isset($options['backup_mnemonic'])) {
613
            $backupMnemonic = $options['backup_mnemonic'];
614
        } elseif (isset($options['backup_public_key'])) {
615
            $backupPublicKey = $options['backup_public_key'];
616
        }
617
618 1
        if ($backupPublicKey) {
619
            if (is_string($backupPublicKey)) {
620
                $backupPublicKey = [$backupPublicKey, "m"];
621
            }
622
        } else {
623 1
            $backupPrivateKey = HierarchicalKeyFactory::fromEntropy((new Bip39SeedGenerator())->getSeed($backupMnemonic, ""));
624 1
            $backupPublicKey = BIP32Key::create($backupPrivateKey->toPublic(), "M");
625
        }
626
627
        // create a checksum of our private key which we'll later use to verify we used the right password
628 1
        $checksum = $primaryPrivateKey->getPublicKey()->getAddress()->getAddress();
629
630
        // send the public keys to the server to store them
631
        //  and the mnemonic, which is safe because it's useless without the password
632 1
        $data = $this->storeNewWalletV1($options['identifier'], $primaryPublicKey->tuple(), $backupPublicKey->tuple(), $primaryMnemonic, $checksum, $options['key_index']);
633
634
        // received the blocktrail public keys
635
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
636 1
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
637 1
        }, $data['blocktrail_public_keys']);
638
639 1
        $wallet = new WalletV1(
640 1
            $this,
641 1
            $options['identifier'],
642 1
            $primaryMnemonic,
643 1
            [$options['key_index'] => $primaryPublicKey],
644 1
            $backupPublicKey,
645 1
            $blocktrailPublicKeys,
646 1
            $options['key_index'],
647 1
            $this->network,
648 1
            $this->testnet,
649 1
            $checksum
650
        );
651
652 1
        $wallet->unlock($options);
653
654
        // return wallet and backup mnemonic
655
        return [
656 1
            $wallet,
657
            [
658 1
                'primary_mnemonic' => $primaryMnemonic,
659 1
                'backup_mnemonic' => $backupMnemonic,
660 1
                'blocktrail_public_keys' => $blocktrailPublicKeys,
661
            ],
662
        ];
663
    }
664
665 5
    public static function randomBits($bits) {
666 5
        return self::randomBytes($bits / 8);
667
    }
668
669 5
    public static function randomBytes($bytes) {
670 5
        return (new Random())->bytes($bytes)->getBinary();
671
    }
672
673 2
    protected function createNewWalletV2($options) {
674 2
        $walletPath = WalletPath::create($options['key_index']);
675
676 2
        if (isset($options['store_primary_mnemonic'])) {
677
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
678
        }
679
680 2
        if (!isset($options['store_data_on_server'])) {
681 2
            if (isset($options['primary_private_key'])) {
682 1
                $options['store_data_on_server'] = false;
683
            } else {
684 1
                $options['store_data_on_server'] = true;
685
            }
686
        }
687
688 2
        $storeDataOnServer = $options['store_data_on_server'];
689
690 2
        $secret = null;
691 2
        $encryptedSecret = null;
692 2
        $primarySeed = null;
693 2
        $encryptedPrimarySeed = null;
694 2
        $recoverySecret = null;
695 2
        $recoveryEncryptedSecret = null;
696 2
        $backupSeed = null;
697
698 2
        if (!isset($options['primary_private_key'])) {
699 1
            $primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : self::randomBits(256);
700
        }
701
702 2
        if ($storeDataOnServer) {
703 1
            if (!isset($options['secret'])) {
704 1
                if (!$options['passphrase']) {
705
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
706
                }
707
708 1
                $secret = bin2hex(self::randomBits(256)); // string because we use it as passphrase
709 1
                $encryptedSecret = CryptoJSAES::encrypt($secret, $options['passphrase']);
710
            } else {
711
                $secret = $options['secret'];
712
            }
713
714 1
            $encryptedPrimarySeed = CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
715 1
            $recoverySecret = bin2hex(self::randomBits(256));
716
717 1
            $recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);
718
        }
719
720 2
        if (!isset($options['backup_public_key'])) {
721 1
            $backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : self::randomBits(256);
722
        }
723
724 2
        if (isset($options['primary_private_key'])) {
725 1
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
726
        } else {
727 1
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($primarySeed)), "m");
728
        }
729
730
        // create primary public key from the created private key
731 2
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
732
733 2
        if (!isset($options['backup_public_key'])) {
734 1
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy(new Buffer($backupSeed)), "m")->buildKey("M");
735
        }
736
737
        // create a checksum of our private key which we'll later use to verify we used the right password
738 2
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
739
740
        // send the public keys and encrypted data to server
741 2
        $data = $this->storeNewWalletV2(
742 2
            $options['identifier'],
743 2
            $options['primary_public_key']->tuple(),
744 2
            $options['backup_public_key']->tuple(),
0 ignored issues
show
Documentation introduced by
$options['backup_public_key']->tuple() is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

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...
745 2
            $storeDataOnServer ? $encryptedPrimarySeed : false,
746 2
            $storeDataOnServer ? $encryptedSecret : false,
747 2
            $storeDataOnServer ? $recoverySecret : false,
748 2
            $checksum,
749 2
            $options['key_index']
750
        );
751
752
        // received the blocktrail public keys
753
        $blocktrailPublicKeys = Util::arrayMapWithIndex(function ($keyIndex, $pubKeyTuple) {
754 2
            return [$keyIndex, BIP32Key::create(HierarchicalKeyFactory::fromExtended($pubKeyTuple[0]), $pubKeyTuple[1])];
755 2
        }, $data['blocktrail_public_keys']);
756
757 2
        $wallet = new WalletV2(
758 2
            $this,
759 2
            $options['identifier'],
760 2
            $encryptedPrimarySeed,
761 2
            $encryptedSecret,
762 2
            [$options['key_index'] => $options['primary_public_key']],
763 2
            $options['backup_public_key'],
764 2
            $blocktrailPublicKeys,
765 2
            $options['key_index'],
766 2
            $this->network,
767 2
            $this->testnet,
768 2
            $checksum
769
        );
770
771 2
        $wallet->unlock([
772 2
            'passphrase' => isset($options['passphrase']) ? $options['passphrase'] : null,
773 2
            'primary_private_key' => $options['primary_private_key'],
774 2
            'primary_seed' => $primarySeed,
775 2
            'secret' => $secret,
776
        ]);
777
778
        // return wallet and mnemonics for backup sheet
779
        return [
780 2
            $wallet,
781
            [
782 2
                'encrypted_primary_seed' => $encryptedPrimarySeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedPrimarySeed))) : null,
783 2
                'backup_seed' => $backupSeed ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer($backupSeed)) : null,
784 2
                'recovery_encrypted_secret' => $recoveryEncryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($recoveryEncryptedSecret))) : null,
785 2
                'encrypted_secret' => $encryptedSecret ? MnemonicFactory::bip39()->entropyToMnemonic(new Buffer(base64_decode($encryptedSecret))) : null,
786
                'blocktrail_public_keys' => Util::arrayMapWithIndex(function ($keyIndex, BIP32Key $pubKey) {
787 2
                    return [$keyIndex, $pubKey->tuple()];
788 2
                }, $blocktrailPublicKeys),
789
            ],
790
        ];
791
    }
792
793 4
    protected function createNewWalletV3($options) {
794 4
        $walletPath = WalletPath::create($options['key_index']);
795
796 4
        if (isset($options['store_primary_mnemonic'])) {
797
            $options['store_data_on_server'] = $options['store_primary_mnemonic'];
798
        }
799
800 4
        if (!isset($options['store_data_on_server'])) {
801 4
            if (isset($options['primary_private_key'])) {
802
                $options['store_data_on_server'] = false;
803
            } else {
804 4
                $options['store_data_on_server'] = true;
805
            }
806
        }
807
808 4
        $storeDataOnServer = $options['store_data_on_server'];
809
810 4
        $secret = null;
811 4
        $encryptedSecret = null;
812 4
        $primarySeed = null;
813 4
        $encryptedPrimarySeed = null;
814 4
        $recoverySecret = null;
815 4
        $recoveryEncryptedSecret = null;
816 4
        $backupSeed = null;
817
818 4
        if (!isset($options['primary_private_key'])) {
819 4
            if (isset($options['primary_seed'])) {
820
                if (!$options['primary_seed'] instanceof BufferInterface) {
821
                    throw new \InvalidArgumentException('Primary Seed should be passed as a Buffer');
822
                }
823
                $primarySeed = $options['primary_seed'];
824
            } else {
825 4
                $primarySeed = new Buffer(self::randomBits(256));
826
            }
827
        }
828
829 4
        if ($storeDataOnServer) {
830 4
            if (!isset($options['secret'])) {
831 4
                if (!$options['passphrase']) {
832
                    throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
833
                }
834
835 4
                $secret = new Buffer(self::randomBits(256));
836 4
                $encryptedSecret = Encryption::encrypt($secret, new Buffer($options['passphrase']), KeyDerivation::DEFAULT_ITERATIONS);
837
            } else {
838
                if (!$options['secret'] instanceof Buffer) {
839
                    throw new \RuntimeException('Secret must be provided as a Buffer');
840
                }
841
842
                $secret = $options['secret'];
843
            }
844
845 4
            $encryptedPrimarySeed = Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS);
846 4
            $recoverySecret = new Buffer(self::randomBits(256));
847
848 4
            $recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS);
849
        }
850
851 4
        if (!isset($options['backup_public_key'])) {
852 4
            if (isset($options['backup_seed'])) {
853
                if (!$options['backup_seed'] instanceof Buffer) {
854
                    throw new \RuntimeException('Backup seed must be an instance of Buffer');
855
                }
856
                $backupSeed = $options['backup_seed'];
857
            } else {
858 4
                $backupSeed = new Buffer(self::randomBits(256));
859
            }
860
        }
861
862 4
        if (isset($options['primary_private_key'])) {
863
            $options['primary_private_key'] = BlocktrailSDK::normalizeBIP32Key($options['primary_private_key']);
864
        } else {
865 4
            $options['primary_private_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($primarySeed), "m");
866
        }
867
868
        // create primary public key from the created private key
869 4
        $options['primary_public_key'] = $options['primary_private_key']->buildKey($walletPath->keyIndexPath()->publicPath());
870
871 4
        if (!isset($options['backup_public_key'])) {
872 4
            $options['backup_public_key'] = BIP32Key::create(HierarchicalKeyFactory::fromEntropy($backupSeed), "m")->buildKey("M");
873
        }
874
875
        // create a checksum of our private key which we'll later use to verify we used the right password
876 4
        $checksum = $options['primary_private_key']->publicKey()->getAddress()->getAddress();
877
878
        // send the public keys and encrypted data to server
879 4
        $data = $this->storeNewWalletV3(
880 4
            $options['identifier'],
881 4
            $options['primary_public_key']->tuple(),
882 4
            $options['backup_public_key']->tuple(),
0 ignored issues
show
Documentation introduced by
$options['backup_public_key']->tuple() is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

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