Completed
Pull Request — master (#89)
by thomas
16:20
created

BlocktrailSDK::getNetworkParams()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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