Completed
Pull Request — master (#85)
by thomas
69:21
created

BlocktrailSDK::sendTransaction()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 30
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6.0493

Importance

Changes 0
Metric Value
cc 6
eloc 18
nc 5
nop 4
dl 0
loc 30
ccs 8
cts 9
cp 0.8889
crap 6.0493
rs 8.439
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 29
    public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null) {
59
60 29
        list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet);
61
62 29
        if (is_null($apiEndpoint)) {
63 29
            $apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com";
64 29
            $apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
65
        }
66
67
        // normalize network and set bitcoinlib to the right magic-bytes
68 29
        list($this->network, $this->testnet) = $this->normalizeNetwork($network, $testnet);
69 29
        $this->setBitcoinLibMagicBytes($this->network, $this->testnet);
70
71 29
        $this->client = new RestClient($apiEndpoint, $apiVersion, $apiKey, $apiSecret);
72 29
    }
73
74
    /**
75
     * normalize network string
76
     *
77
     * @param $network
78
     * @param $testnet
79
     * @return array
80
     * @throws \Exception
81
     */
82 29
    protected function normalizeNetwork($network, $testnet) {
83 29
        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 29
    protected function setBitcoinLibMagicBytes($network, $testnet) {
93 29
        assert($network == "bitcoin" || $network == "bitcoincash");
94 29
        Bitcoin::setNetwork($testnet ? NetworkFactory::bitcoinTestnet() : NetworkFactory::bitcoin());
95 29
    }
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
     * Either takes one argument:
1067
     * @param array $options
1068
     *
1069
     * Or takes two arguments (old, deprecated syntax):
1070
     * (@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...
1071
     * (@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...
1072
     *
1073
     * @return WalletInterface
1074
     * @throws \Exception
1075
     */
1076 11
    public function initWallet($options) {
1077 11
        if (!is_array($options)) {
1078 1
            $args = func_get_args();
1079
            $options = [
1080 1
                "identifier" => $args[0],
1081 1
                "password" => $args[1],
1082
            ];
1083
        }
1084
1085 11
        $identifier = $options['identifier'];
1086 11
        $readonly = isset($options['readonly']) ? $options['readonly'] :
1087 11
                    (isset($options['readOnly']) ? $options['readOnly'] :
1088 11
                        (isset($options['read-only']) ? $options['read-only'] :
1089 11
                            false));
1090
1091
        // get the wallet data from the server
1092 11
        $data = $this->getWallet($identifier);
1093
        if (!$data) {
1094 11
            throw new \Exception("Failed to get wallet");
1095
        }
1096
1097
        switch ($data['wallet_version']) {
1098 11
            case Wallet::WALLET_VERSION_V1:
1099 11
                $wallet = new WalletV1(
1100 5
                    $this,
1101 5
                    $identifier,
1102 5
                    isset($options['primary_mnemonic']) ? $options['primary_mnemonic'] : $data['primary_mnemonic'],
1103 5
                    $data['primary_public_keys'],
1104 5
                    $data['backup_public_key'],
1105 5
                    $data['blocktrail_public_keys'],
1106 5
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1107 5
                    $this->network,
1108 5
                    $this->testnet,
1109 5
                    $data['checksum']
1110 5
                );
1111
                break;
1112 5
            case Wallet::WALLET_VERSION_V2:
1113 6
                $wallet = new WalletV2(
1114 2
                    $this,
1115 2
                    $identifier,
1116 2
                    isset($options['encrypted_primary_seed']) ? $options['encrypted_primary_seed'] : $data['encrypted_primary_seed'],
1117 2
                    isset($options['encrypted_secret']) ? $options['encrypted_secret'] : $data['encrypted_secret'],
1118 2
                    $data['primary_public_keys'],
1119 2
                    $data['backup_public_key'],
1120 2
                    $data['blocktrail_public_keys'],
1121 2
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1122 2
                    $this->network,
1123 2
                    $this->testnet,
1124 2
                    $data['checksum']
1125 2
                );
1126
                break;
1127 2
            case Wallet::WALLET_VERSION_V3:
1128 4
                if (isset($options['encrypted_primary_seed'])) {
1129 4
                    if (!$options['encrypted_primary_seed'] instanceof Buffer) {
1130
                        throw new \InvalidArgumentException('Encrypted PrimarySeed must be provided as a Buffer');
1131
                    }
1132
                    $encryptedPrimarySeed = $data['encrypted_primary_seed'];
1133
                } else {
1134
                    $encryptedPrimarySeed = new Buffer(base64_decode($data['encrypted_primary_seed']));
1135 4
                }
1136
1137
                if (isset($options['encrypted_secret'])) {
1138 4
                    if (!$options['encrypted_secret'] instanceof Buffer) {
1139
                        throw new \InvalidArgumentException('Encrypted secret must be provided as a Buffer');
1140
                    }
1141
1142
                    $encryptedSecret = $data['encrypted_secret'];
1143
                } else {
1144
                    $encryptedSecret = new Buffer(base64_decode($data['encrypted_secret']));
1145 4
                }
1146
1147
                $wallet = new WalletV3(
1148 4
                    $this,
1149 4
                    $identifier,
1150 4
                    $encryptedPrimarySeed,
1151 4
                    $encryptedSecret,
1152 4
                    $data['primary_public_keys'],
1153 4
                    $data['backup_public_key'],
1154 4
                    $data['blocktrail_public_keys'],
1155 4
                    isset($options['key_index']) ? $options['key_index'] : $data['key_index'],
1156 4
                    $this->network,
1157 4
                    $this->testnet,
1158 4
                    $data['checksum']
1159 4
                );
1160
                break;
1161 4
            default:
1162
                throw new \InvalidArgumentException("Invalid wallet version");
1163
        }
1164
1165
        if (!$readonly) {
1166 11
            $wallet->unlock($options);
1167 11
        }
1168
1169
        return $wallet;
1170 11
    }
1171
1172
    /**
1173
     * get the wallet data from the server
1174
     *
1175
     * @param string    $identifier             the identifier of the wallet
1176
     * @return mixed
1177
     */
1178
    public function getWallet($identifier) {
1179 11
        $response = $this->client->get("wallet/{$identifier}", null, RestClient::AUTH_HTTP_SIG);
1180 11
        return self::jsonDecode($response->body(), true);
1181 11
    }
1182
1183
    /**
1184
     * update the wallet data on the server
1185
     *
1186
     * @param string    $identifier
1187
     * @param $data
1188
     * @return mixed
1189
     */
1190
    public function updateWallet($identifier, $data) {
1191 3
        $response = $this->client->post("wallet/{$identifier}", null, $data, RestClient::AUTH_HTTP_SIG);
1192 3
        return self::jsonDecode($response->body(), true);
1193 3
    }
1194
1195
    /**
1196
     * delete a wallet from the server
1197
     *  the checksum address and a signature to verify you ownership of the key of that checksum address
1198
     *  is required to be able to delete a wallet
1199
     *
1200
     * @param string    $identifier             the identifier of the wallet
1201
     * @param string    $checksumAddress        the address for your master private key (and the checksum used when creating the wallet)
1202
     * @param string    $signature              a signature of the checksum address as message signed by the private key matching that address
1203
     * @param bool      $force                  ignore warnings (such as a non-zero balance)
1204
     * @return mixed
1205
     */
1206
    public function deleteWallet($identifier, $checksumAddress, $signature, $force = false) {
1207 10
        $response = $this->client->delete("wallet/{$identifier}", ['force' => $force], [
1208 10
            'checksum' => $checksumAddress,
1209 10
            'signature' => $signature
1210 10
        ], RestClient::AUTH_HTTP_SIG, 360);
1211 10
        return self::jsonDecode($response->body(), true);
1212 10
    }
1213
1214
    /**
1215
     * create new backup key;
1216
     *  1) a BIP39 mnemonic
1217
     *  2) a seed from that mnemonic with a blank password
1218
     *  3) a private key from that seed
1219
     *
1220
     * @return array [mnemonic, seed, key]
1221
     */
1222
    protected function newBackupSeed() {
1223 1
        list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed("");
1224 1
1225
        return [$backupMnemonic, $backupSeed, $backupPrivateKey];
1226 1
    }
1227
1228
    /**
1229
     * create new primary key;
1230
     *  1) a BIP39 mnemonic
1231
     *  2) a seed from that mnemonic with the password
1232
     *  3) a private key from that seed
1233
     *
1234
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1235
     * @return array [mnemonic, seed, key]
1236
     * @TODO: require a strong password?
1237
     */
1238
    protected function newPrimarySeed($passphrase) {
1239 1
        list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase);
1240 1
1241
        return [$primaryMnemonic, $primarySeed, $primaryPrivateKey];
1242 1
    }
1243
1244
    /**
1245
     * create a new key;
1246
     *  1) a BIP39 mnemonic
1247
     *  2) a seed from that mnemonic with the password
1248
     *  3) a private key from that seed
1249
     *
1250
     * @param string    $passphrase             the password to use in the BIP39 creation of the seed
1251
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1252
     * @return array
1253
     */
1254
    protected function generateNewSeed($passphrase = "", $forceEntropy = null) {
1255 1
        // generate master seed, retry if the generated private key isn't valid (FALSE is returned)
1256
        do {
1257
            $mnemonic = $this->generateNewMnemonic($forceEntropy);
1258 1
1259
            $seed = (new Bip39SeedGenerator)->getSeed($mnemonic, $passphrase);
1260 1
1261
            $key = null;
1262 1
            try {
1263
                $key = HierarchicalKeyFactory::fromEntropy($seed);
1264 1
            } catch (\Exception $e) {
1265
                // try again
1266
            }
1267
        } while (!$key);
1268 1
1269
        return [$mnemonic, $seed, $key];
1270 1
    }
1271
1272
    /**
1273
     * generate a new mnemonic from some random entropy (512 bit)
1274
     *
1275
     * @param string    $forceEntropy           forced entropy instead of random entropy for testing purposes
1276
     * @return string
1277
     * @throws \Exception
1278
     */
1279
    protected function generateNewMnemonic($forceEntropy = null) {
1280 1
        if ($forceEntropy === null) {
1281 1
            $random = new Random();
1282 1
            $entropy = $random->bytes(512 / 8);
1283 1
        } else {
1284
            $entropy = $forceEntropy;
1285
        }
1286
1287
        return MnemonicFactory::bip39()->entropyToMnemonic($entropy);
0 ignored issues
show
Bug introduced by
It seems like $entropy defined by $forceEntropy on line 1284 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...
1288 1
    }
1289
1290
    /**
1291
     * get the balance for the wallet
1292
     *
1293
     * @param string    $identifier             the identifier of the wallet
1294
     * @return array
1295
     */
1296
    public function getWalletBalance($identifier) {
1297 9
        $response = $this->client->get("wallet/{$identifier}/balance", null, RestClient::AUTH_HTTP_SIG);
1298 9
        return self::jsonDecode($response->body(), true);
1299 9
    }
1300
1301
    /**
1302
     * do HD wallet discovery for the wallet
1303
     *
1304
     * this can be REALLY slow, so we've set the timeout to 120s ...
1305
     *
1306
     * @param string    $identifier             the identifier of the wallet
1307
     * @param int       $gap                    the gap setting to use for discovery
1308
     * @return mixed
1309
     */
1310
    public function doWalletDiscovery($identifier, $gap = 200) {
1311 2
        $response = $this->client->get("wallet/{$identifier}/discovery", ['gap' => $gap], RestClient::AUTH_HTTP_SIG, 360.0);
1312 2
        return self::jsonDecode($response->body(), true);
1313 2
    }
1314
1315
    /**
1316
     * get a new derivation number for specified parent path
1317
     *  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
1318
     *
1319
     * returns the path
1320
     *
1321
     * @param string    $identifier             the identifier of the wallet
1322
     * @param string    $path                   the parent path for which to get a new derivation
1323
     * @return string
1324
     */
1325
    public function getNewDerivation($identifier, $path) {
1326 1
        $result = $this->_getNewDerivation($identifier, $path);
1327 1
        return $result['path'];
1328 1
    }
1329
1330
    /**
1331
     * get a new derivation number for specified parent path
1332
     *  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
1333
     *
1334
     * @param string    $identifier             the identifier of the wallet
1335
     * @param string    $path                   the parent path for which to get a new derivation
1336
     * @return mixed
1337
     */
1338
    public function _getNewDerivation($identifier, $path) {
1339 10
        $response = $this->client->post("wallet/{$identifier}/path", null, ['path' => $path], RestClient::AUTH_HTTP_SIG);
1340 10
        return self::jsonDecode($response->body(), true);
1341 10
    }
1342
1343
    /**
1344
     * get the path (and redeemScript) to specified address
1345
     *
1346
     * @param string $identifier
1347
     * @param string $address
1348
     * @return array
1349
     * @throws \Exception
1350
     */
1351
    public function getPathForAddress($identifier, $address) {
1352
        $response = $this->client->post("wallet/{$identifier}/path_for_address", null, ['address' => $address], RestClient::AUTH_HTTP_SIG);
1353
        return self::jsonDecode($response->body(), true)['path'];
1354
    }
1355
1356
    /**
1357
     * send the transaction using the API
1358
     *
1359
     * @param string    $identifier             the identifier of the wallet
1360
     * @param string    $rawTransaction         raw hex of the transaction (should be partially signed)
1361
     * @param array     $paths                  list of the paths that were used for the UTXO
1362
     * @param bool      $checkFee               let the server verify the fee after signing
1363
     * @return string                           the complete raw transaction
1364
     * @throws \Exception
1365
     */
1366
    public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee = false) {
1367 2
        $data = [
1368
            'paths' => $paths
1369 2
        ];
1370 2
1371
        if (is_array($rawTransaction)) {
1372
            if (array_key_exists('base_transaction', $rawTransaction)
1373
            && array_key_exists('signed_transaction', $rawTransaction)) {
1374 2
                $data['base_transaction'] = $rawTransaction['base_transaction'];
1375
                $data['signed_transaction'] = $rawTransaction['signed_transaction'];
1376 2
            } else {
1377 2
                throw new \RuntimeException("Invalid value for transaction. For segwit transactions, pass ['base_transaction' => '...', 'signed_transaction' => '...']");
1378
            }
1379 2
        } else {
1380
            $data['raw_transaction'] = $rawTransaction;
1381
        }
1382
1383
        // dynamic TTL for when we're signing really big transactions
1384 2
        $ttl = max(5.0, count($paths) * 0.25) + 4.0;
1385
1386
        $response = $this->client->post("wallet/{$identifier}/send", ['check_fee' => (int)!!$checkFee], $data, RestClient::AUTH_HTTP_SIG, $ttl);
1387
        $signed = self::jsonDecode($response->body(), true);
1388
1389
        if (!$signed['complete'] || $signed['complete'] == 'false') {
1390
            throw new \Exception("Failed to completely sign transaction");
1391
        }
1392
1393
        // create TX hash from the raw signed hex
1394
        return TransactionFactory::fromHex($signed['hex'])->getTxId()->getHex();
1395
    }
1396
1397
    /**
1398
     * use the API to get the best inputs to use based on the outputs
1399
     *
1400
     * the return array has the following format:
1401
     * [
1402
     *  "utxos" => [
1403
     *      [
1404
     *          "hash" => "<txHash>",
1405
     *          "idx" => "<index of the output of that <txHash>",
1406
     *          "scriptpubkey_hex" => "<scriptPubKey-hex>",
1407
     *          "value" => 32746327,
1408
     *          "address" => "1address",
1409
     *          "path" => "m/44'/1'/0'/0/13",
1410
     *          "redeem_script" => "<redeemScript-hex>",
1411
     *      ],
1412
     *  ],
1413
     *  "fee"   => 10000,
1414
     *  "change"=> 1010109201,
1415
     * ]
1416
     *
1417 8
     * @param string   $identifier              the identifier of the wallet
1418
     * @param array    $outputs                 the outputs you want to create - array[address => satoshi-value]
1419 8
     * @param bool     $lockUTXO                when TRUE the UTXOs selected will be locked for a few seconds
1420 8
     *                                          so you have some time to spend them without race-conditions
1421 8
     * @param bool     $allowZeroConf
1422
     * @param string   $feeStrategy
1423
     * @param null|int $forceFee
1424 8
     * @return array
1425 1
     * @throws \Exception
1426
     */
1427
    public function coinSelection($identifier, $outputs, $lockUTXO = false, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null) {
1428 8
        $args = [
1429 8
            'lock' => (int)!!$lockUTXO,
1430 8
            'zeroconf' => (int)!!$allowZeroConf,
1431 8
            'fee_strategy' => $feeStrategy,
1432 8
        ];
1433
1434
        if ($forceFee !== null) {
1435 2
            $args['forcefee'] = (int)$forceFee;
1436
        }
1437
1438
        $response = $this->client->post(
1439
            "wallet/{$identifier}/coin-selection",
1440
            $args,
1441
            $outputs,
1442
            RestClient::AUTH_HTTP_SIG
1443
        );
1444
1445
        return self::jsonDecode($response->body(), true);
1446
    }
1447
1448
    /**
1449
     *
1450
     * @param string   $identifier the identifier of the wallet
1451
     * @param bool     $allowZeroConf
1452
     * @param string   $feeStrategy
1453
     * @param null|int $forceFee
1454
     * @param int      $outputCnt
1455
     * @return array
1456
     * @throws \Exception
1457
     */
1458
    public function walletMaxSpendable($identifier, $allowZeroConf = false, $feeStrategy = Wallet::FEE_STRATEGY_OPTIMAL, $forceFee = null, $outputCnt = 1) {
1459
        $args = [
1460
            'zeroconf' => (int)!!$allowZeroConf,
1461
            'fee_strategy' => $feeStrategy,
1462
            'outputs' => $outputCnt,
1463
        ];
1464
1465
        if ($forceFee !== null) {
1466
            $args['forcefee'] = (int)$forceFee;
1467
        }
1468
1469
        $response = $this->client->get(
1470
            "wallet/{$identifier}/max-spendable",
1471 1
            $args,
1472 1
            RestClient::AUTH_HTTP_SIG
1473 1
        );
1474
1475
        return self::jsonDecode($response->body(), true);
1476
    }
1477
1478
    /**
1479
     * @return array        ['optimal_fee' => 10000, 'low_priority_fee' => 5000]
1480
     */
1481 1
    public function feePerKB() {
1482 1
        $response = $this->client->get("fee-per-kb");
1483 1
        return self::jsonDecode($response->body(), true);
1484
    }
1485
1486
    /**
1487
     * get the current price index
1488
     *
1489
     * @return array        eg; ['USD' => 287.30]
1490
     */
1491
    public function price() {
1492
        $response = $this->client->get("price");
1493
        return self::jsonDecode($response->body(), true);
1494 1
    }
1495 1
1496 1
    /**
1497
     * setup webhook for wallet
1498
     *
1499
     * @param string    $identifier         the wallet identifier for which to create the webhook
1500
     * @param string    $webhookIdentifier  the webhook identifier to use
1501
     * @param string    $url                the url to receive the webhook events
1502
     * @return array
1503
     */
1504
    public function setupWalletWebhook($identifier, $webhookIdentifier, $url) {
1505
        $response = $this->client->post("wallet/{$identifier}/webhook", null, ['url' => $url, 'identifier' => $webhookIdentifier], RestClient::AUTH_HTTP_SIG);
1506 1
        return self::jsonDecode($response->body(), true);
1507 1
    }
1508 1
1509
    /**
1510
     * delete webhook for wallet
1511
     *
1512
     * @param string    $identifier         the wallet identifier for which to delete the webhook
1513
     * @param string    $webhookIdentifier  the webhook identifier to delete
1514
     * @return array
1515
     */
1516
    public function deleteWalletWebhook($identifier, $webhookIdentifier) {
1517
        $response = $this->client->delete("wallet/{$identifier}/webhook/{$webhookIdentifier}", null, null, RestClient::AUTH_HTTP_SIG);
1518
        return self::jsonDecode($response->body(), true);
1519
    }
1520
1521
    /**
1522
     * lock a specific unspent output
1523
     *
1524
     * @param     $identifier
1525
     * @param     $txHash
1526
     * @param     $txIdx
1527
     * @param int $ttl
1528
     * @return bool
1529
     */
1530
    public function lockWalletUTXO($identifier, $txHash, $txIdx, $ttl = 3) {
1531
        $response = $this->client->post("wallet/{$identifier}/lock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx, 'ttl' => $ttl], RestClient::AUTH_HTTP_SIG);
1532
        return self::jsonDecode($response->body(), true)['locked'];
1533
    }
1534
1535
    /**
1536
     * unlock a specific unspent output
1537
     *
1538
     * @param     $identifier
1539
     * @param     $txHash
1540
     * @param     $txIdx
1541
     * @return bool
1542
     */
1543
    public function unlockWalletUTXO($identifier, $txHash, $txIdx) {
1544
        $response = $this->client->post("wallet/{$identifier}/unlock-utxo", null, ['hash' => $txHash, 'idx' => $txIdx], RestClient::AUTH_HTTP_SIG);
1545
        return self::jsonDecode($response->body(), true)['unlocked'];
1546
    }
1547 1
1548
    /**
1549 1
     * get all transactions for wallet (paginated)
1550 1
     *
1551 1
     * @param  string  $identifier  the wallet identifier for which to get transactions
1552
     * @param  integer $page        pagination: page number
1553 1
     * @param  integer $limit       pagination: records per page (max 500)
1554 1
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1555
     * @return array                associative array containing the response
1556
     */
1557
    public function walletTransactions($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1558
        $queryString = [
1559
            'page' => $page,
1560
            'limit' => $limit,
1561
            'sort_dir' => $sortDir
1562
        ];
1563
        $response = $this->client->get("wallet/{$identifier}/transactions", $queryString, RestClient::AUTH_HTTP_SIG);
1564
        return self::jsonDecode($response->body(), true);
1565
    }
1566 1
1567
    /**
1568 1
     * get all addresses for wallet (paginated)
1569 1
     *
1570 1
     * @param  string  $identifier  the wallet identifier for which to get addresses
1571
     * @param  integer $page        pagination: page number
1572 1
     * @param  integer $limit       pagination: records per page (max 500)
1573 1
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1574
     * @return array                associative array containing the response
1575
     */
1576
    public function walletAddresses($identifier, $page = 1, $limit = 20, $sortDir = 'asc') {
1577
        $queryString = [
1578
            'page' => $page,
1579
            'limit' => $limit,
1580
            'sort_dir' => $sortDir
1581
        ];
1582
        $response = $this->client->get("wallet/{$identifier}/addresses", $queryString, RestClient::AUTH_HTTP_SIG);
1583
        return self::jsonDecode($response->body(), true);
1584
    }
1585
1586 1
    /**
1587
     * get all UTXOs for wallet (paginated)
1588 1
     *
1589 1
     * @param  string  $identifier  the wallet identifier for which to get addresses
1590 1
     * @param  integer $page        pagination: page number
1591 1
     * @param  integer $limit       pagination: records per page (max 500)
1592
     * @param  string  $sortDir     pagination: sort direction (asc|desc)
1593 1
     * @param  boolean $zeroconf    include zero confirmation transactions
1594 1
     * @return array                associative array containing the response
1595
     */
1596
    public function walletUTXOs($identifier, $page = 1, $limit = 20, $sortDir = 'asc', $zeroconf = true) {
1597
        $queryString = [
1598
            'page' => $page,
1599
            'limit' => $limit,
1600
            'sort_dir' => $sortDir,
1601
            'zeroconf' => (int)!!$zeroconf,
1602
        ];
1603
        $response = $this->client->get("wallet/{$identifier}/utxos", $queryString, RestClient::AUTH_HTTP_SIG);
1604 2
        return self::jsonDecode($response->body(), true);
1605
    }
1606 2
1607 2
    /**
1608
     * get a paginated list of all wallets associated with the api user
1609 2
     *
1610 2
     * @param  integer          $page    pagination: page number
1611
     * @param  integer          $limit   pagination: records per page
1612
     * @return array                     associative array containing the response
1613
     */
1614
    public function allWallets($page = 1, $limit = 20) {
1615
        $queryString = [
1616
            'page' => $page,
1617
            'limit' => $limit
1618
        ];
1619
        $response = $this->client->get("wallets", $queryString, RestClient::AUTH_HTTP_SIG);
1620
        return self::jsonDecode($response->body(), true);
1621
    }
1622
1623
    /**
1624
     * send raw transaction
1625
     *
1626
     * @param     $txHex
1627
     * @return bool
1628
     */
1629
    public function sendRawTransaction($txHex) {
1630
        $response = $this->client->post("send-raw-tx", null, ['hex' => $txHex], RestClient::AUTH_HTTP_SIG);
1631
        return self::jsonDecode($response->body(), true);
1632
    }
1633
1634
    /**
1635
     * testnet only ;-)
1636
     *
1637
     * @param     $address
1638
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1639
     * @return mixed
1640
     * @throws \Exception
1641
     */
1642
    public function faucetWithdrawal($address, $amount = 10000) {
1643
        $response = $this->client->post("faucet/withdrawl", null, [
1644
            'address' => $address,
1645
            'amount' => $amount,
1646
        ], RestClient::AUTH_HTTP_SIG);
1647
        return self::jsonDecode($response->body(), true);
1648
    }
1649
1650
    /**
1651
     * Exists for BC. Remove at major bump.
1652
     *
1653
     * @see faucetWithdrawal
1654
     * @deprecated
1655
     * @param     $address
1656
     * @param int $amount       defaults to 0.0001 BTC, max 0.001 BTC
1657
     * @return mixed
1658
     * @throws \Exception
1659
     */
1660
    public function faucetWithdrawl($address, $amount = 10000) {
1661
        return $this->faucetWithdrawal($address, $amount);
1662 1
    }
1663
1664
    /**
1665
     * verify a message signed bitcoin-core style
1666 1
     *
1667 1
     * @param  string           $message
1668 1
     * @param  string           $address
1669
     * @param  string           $signature
1670
     * @return boolean
1671
     */
1672
    public function verifyMessage($message, $address, $signature) {
1673 1
        // we could also use the API instead of the using BitcoinLib to verify
1674 1
        // $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...
1675
1676 1
        $adapter = Bitcoin::getEcAdapter();
1677 1
        $addr = AddressFactory::fromString($address);
1678
        if (!$addr instanceof PayToPubKeyHashAddress) {
1679
            throw new \RuntimeException('Can only verify a message with a pay-to-pubkey-hash address');
1680
        }
1681
1682
        /** @var CompactSignatureSerializerInterface $csSerializer */
1683
        $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...
1684
        $signedMessage = new SignedMessage($message, $csSerializer->parse(new Buffer(base64_decode($signature))));
1685
1686
        $signer = new MessageSigner($adapter);
1687
        return $signer->verify($signedMessage, $addr);
1688
    }
1689
1690
    /**
1691
     * convert a Satoshi value to a BTC value
1692
     *
1693
     * @param int       $satoshi
1694
     * @return float
1695
     */
1696
    public static function toBTC($satoshi) {
1697
        return bcdiv((int)(string)$satoshi, 100000000, 8);
1698
    }
1699
1700
    /**
1701
     * convert a Satoshi value to a BTC value and return it as a string
1702
1703
     * @param int       $satoshi
1704
     * @return string
1705
     */
1706 9
    public static function toBTCString($satoshi) {
1707 9
        return sprintf("%.8f", self::toBTC($satoshi));
1708
    }
1709
1710
    /**
1711
     * convert a BTC value to a Satoshi value
1712
     *
1713
     * @param float     $btc
1714
     * @return string
1715
     */
1716 9
    public static function toSatoshiString($btc) {
1717 9
        return bcmul(sprintf("%.8f", (float)$btc), 100000000, 0);
1718
    }
1719
1720
    /**
1721
     * convert a BTC value to a Satoshi value
1722
     *
1723
     * @param float     $btc
1724
     * @return string
1725
     */
1726
    public static function toSatoshi($btc) {
1727
        return (int)self::toSatoshiString($btc);
1728 20
    }
1729 20
1730
    /**
1731
     * json_decode helper that throws exceptions when it fails to decode
1732
     *
1733 20
     * @param      $json
1734
     * @param bool $assoc
1735 20
     * @return mixed
1736
     * @throws \Exception
1737
     */
1738
    protected static function jsonDecode($json, $assoc = false) {
1739 20
        if (!$json) {
1740
            throw new \Exception("Can't json_decode empty string [{$json}]");
1741
        }
1742
1743
        $data = json_decode($json, $assoc);
1744
1745
        if ($data === null) {
1746
            throw new \Exception("Failed to json_decode [{$json}]");
1747
        }
1748 10
1749 10
        return $data;
1750
    }
1751 10
1752 10
    /**
1753 10
     * sort public keys for multisig script
1754 10
     *
1755
     * @param PublicKeyInterface[] $pubKeys
1756 10
     * @return PublicKeyInterface[]
1757
     */
1758
    public static function sortMultisigKeys(array $pubKeys) {
1759
        $result = array_values($pubKeys);
1760
        usort($result, function (PublicKeyInterface $a, PublicKeyInterface $b) {
1761
            $av = $a->getHex();
1762
            $bv = $b->getHex();
1763
            return $av == $bv ? 0 : $av > $bv ? 1 : -1;
1764
        });
1765
1766
        return $result;
1767
    }
1768
1769
    /**
1770
     * read and decode the json payload from a webhook's POST request.
1771
     *
1772
     * @param bool $returnObject    flag to indicate if an object or associative array should be returned
1773
     * @return mixed|null
1774
     * @throws \Exception
1775
     */
1776 14
    public static function getWebhookPayload($returnObject = false) {
1777 14
        $data = file_get_contents("php://input");
1778 14
        if ($data) {
1779
            return self::jsonDecode($data, !$returnObject);
1780
        } else {
1781 14
            return null;
1782 14
        }
1783 10
    }
1784
1785
    public static function normalizeBIP32KeyArray($keys) {
1786 14
        return Util::arrayMapWithIndex(function ($idx, $key) {
1787 14
            return [$idx, self::normalizeBIP32Key($key)];
1788 14
        }, $keys);
1789
    }
1790 14
1791 14
    public static function normalizeBIP32Key($key) {
1792
        if ($key instanceof BIP32Key) {
1793
            return $key;
1794 14
        }
1795
1796
        if (is_array($key)) {
1797
            $path = $key[1];
1798
            $key = $key[0];
1799
1800
            if (!($key instanceof HierarchicalKey)) {
1801
                $key = HierarchicalKeyFactory::fromExtended($key);
1802
            }
1803
1804
            return BIP32Key::create($key, $path);
1805
        } else {
1806
            throw new \Exception("Bad Input");
1807
        }
1808
    }
1809
}
1810